feat(v2): markdown pages (#3158)

* markdown pages POC

* add remark admonition, mdx provider, yarn2npm...

* pluginContentPages md/mdx tests

* pluginContentPages md/mdx tests

* add relative file path test link to showcase link problem

* fix Markdown pages issues after merge

* fix broken links found in markdown pages

* fix tests

* factorize common validation in @docusaurus/utils-validation

* add some documentation

* add using plugins doc

* minor md pages fixes
This commit is contained in:
Sébastien Lorber 2020-07-31 16:04:56 +02:00 committed by GitHub
parent 53b28d2bb2
commit 7cceee7e38
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
30 changed files with 570 additions and 93 deletions

View file

@ -12,6 +12,7 @@ packages/docusaurus-1.x/lib/core/metadata.js
packages/docusaurus-1.x/lib/core/MetadataBlog.js packages/docusaurus-1.x/lib/core/MetadataBlog.js
packages/docusaurus-1.x/lib/core/__tests__/split-tab.test.js packages/docusaurus-1.x/lib/core/__tests__/split-tab.test.js
packages/docusaurus-utils/lib/ packages/docusaurus-utils/lib/
packages/docusaurus-utils-validation/lib/
packages/docusaurus/lib/ packages/docusaurus/lib/
packages/docusaurus-init/lib/ packages/docusaurus-init/lib/
packages/docusaurus-plugin-client-redirects/lib/ packages/docusaurus-plugin-client-redirects/lib/

1
.gitignore vendored
View file

@ -17,6 +17,7 @@ coverage
types types
test-website test-website
packages/docusaurus-utils/lib/ packages/docusaurus-utils/lib/
packages/docusaurus-utils-validation/lib/
packages/docusaurus/lib/ packages/docusaurus/lib/
packages/docusaurus-init/lib/ packages/docusaurus-init/lib/
packages/docusaurus-plugin-client-redirects/lib/ packages/docusaurus-plugin-client-redirects/lib/

View file

@ -4,6 +4,7 @@ build
coverage coverage
.docusaurus .docusaurus
packages/docusaurus-utils/lib/ packages/docusaurus-utils/lib/
packages/docusaurus-utils-validation/lib/
packages/docusaurus/lib/ packages/docusaurus/lib/
packages/docusaurus-init/lib/ packages/docusaurus-init/lib/
packages/docusaurus-plugin-client-redirects/lib/ packages/docusaurus-plugin-client-redirects/lib/

View file

@ -12,6 +12,7 @@ const ignorePatterns = [
'__fixtures__', '__fixtures__',
'/packages/docusaurus/lib', '/packages/docusaurus/lib',
'/packages/docusaurus-utils/lib', '/packages/docusaurus-utils/lib',
'/packages/docusaurus-utils-validation/lib',
'/packages/docusaurus-plugin-content-blog/lib', '/packages/docusaurus-plugin-content-blog/lib',
'/packages/docusaurus-plugin-content-docs/lib', '/packages/docusaurus-plugin-content-docs/lib',
'/packages/docusaurus-plugin-content-pages/lib', '/packages/docusaurus-plugin-content-pages/lib',

View file

@ -19,6 +19,7 @@
"@docusaurus/mdx-loader": "2.0.0-alpha.60", "@docusaurus/mdx-loader": "2.0.0-alpha.60",
"@docusaurus/types": "2.0.0-alpha.60", "@docusaurus/types": "2.0.0-alpha.60",
"@docusaurus/utils": "2.0.0-alpha.60", "@docusaurus/utils": "2.0.0-alpha.60",
"@docusaurus/utils-validation": "2.0.0-alpha.60",
"@hapi/joi": "^17.1.1", "@hapi/joi": "^17.1.1",
"feed": "^4.1.0", "feed": "^4.1.0",
"fs-extra": "^8.1.0", "fs-extra": "^8.1.0",

View file

@ -6,6 +6,11 @@
*/ */
import * as Joi from '@hapi/joi'; import * as Joi from '@hapi/joi';
import {
RemarkPluginsSchema,
RehypePluginsSchema,
AdmonitionsSchema,
} from '@docusaurus/utils-validation';
export const DEFAULT_OPTIONS = { export const DEFAULT_OPTIONS = {
feedOptions: {}, feedOptions: {},
@ -47,25 +52,11 @@ export const PluginOptionSchema = Joi.object({
.allow('') .allow('')
.default(DEFAULT_OPTIONS.blogDescription), .default(DEFAULT_OPTIONS.blogDescription),
showReadingTime: Joi.bool().default(DEFAULT_OPTIONS.showReadingTime), showReadingTime: Joi.bool().default(DEFAULT_OPTIONS.showReadingTime),
remarkPlugins: Joi.array() remarkPlugins: RemarkPluginsSchema.default(DEFAULT_OPTIONS.remarkPlugins),
.items( rehypePlugins: RehypePluginsSchema.default(DEFAULT_OPTIONS.rehypePlugins),
Joi.array() admonitions: AdmonitionsSchema.default(DEFAULT_OPTIONS.admonitions),
.items(Joi.function().required(), Joi.object().required())
.length(2),
Joi.function(),
)
.default(DEFAULT_OPTIONS.remarkPlugins),
rehypePlugins: Joi.array()
.items(
Joi.array()
.items(Joi.function().required(), Joi.object().required())
.length(2),
Joi.function(),
)
.default(DEFAULT_OPTIONS.rehypePlugins),
editUrl: Joi.string().uri(), editUrl: Joi.string().uri(),
truncateMarker: Joi.object().default(DEFAULT_OPTIONS.truncateMarker), truncateMarker: Joi.object().default(DEFAULT_OPTIONS.truncateMarker),
admonitions: Joi.object().default(DEFAULT_OPTIONS.admonitions),
beforeDefaultRemarkPlugins: Joi.array() beforeDefaultRemarkPlugins: Joi.array()
.items( .items(
Joi.array() Joi.array()

View file

@ -22,6 +22,7 @@
"@docusaurus/mdx-loader": "2.0.0-alpha.60", "@docusaurus/mdx-loader": "2.0.0-alpha.60",
"@docusaurus/types": "2.0.0-alpha.60", "@docusaurus/types": "2.0.0-alpha.60",
"@docusaurus/utils": "2.0.0-alpha.60", "@docusaurus/utils": "2.0.0-alpha.60",
"@docusaurus/utils-validation": "2.0.0-alpha.60",
"@hapi/joi": "17.1.1", "@hapi/joi": "17.1.1",
"execa": "^3.4.0", "execa": "^3.4.0",
"fs-extra": "^8.1.0", "fs-extra": "^8.1.0",

View file

@ -6,6 +6,11 @@
*/ */
import * as Joi from '@hapi/joi'; import * as Joi from '@hapi/joi';
import {PluginOptions} from './types'; import {PluginOptions} from './types';
import {
RemarkPluginsSchema,
RehypePluginsSchema,
AdmonitionsSchema,
} from '@docusaurus/utils-validation';
const REVERSED_DOCS_HOME_PAGE_ID = '_index'; const REVERSED_DOCS_HOME_PAGE_ID = '_index';
@ -35,27 +40,13 @@ export const PluginOptionSchema = Joi.object({
sidebarPath: Joi.string().default(DEFAULT_OPTIONS.sidebarPath), sidebarPath: Joi.string().default(DEFAULT_OPTIONS.sidebarPath),
docLayoutComponent: Joi.string().default(DEFAULT_OPTIONS.docLayoutComponent), docLayoutComponent: Joi.string().default(DEFAULT_OPTIONS.docLayoutComponent),
docItemComponent: Joi.string().default(DEFAULT_OPTIONS.docItemComponent), docItemComponent: Joi.string().default(DEFAULT_OPTIONS.docItemComponent),
remarkPlugins: Joi.array() remarkPlugins: RemarkPluginsSchema.default(DEFAULT_OPTIONS.remarkPlugins),
.items( rehypePlugins: RehypePluginsSchema.default(DEFAULT_OPTIONS.rehypePlugins),
Joi.array() admonitions: AdmonitionsSchema.default(DEFAULT_OPTIONS.admonitions),
.items(Joi.function().required(), Joi.object().required())
.length(2),
Joi.function(),
)
.default(DEFAULT_OPTIONS.remarkPlugins),
rehypePlugins: Joi.array()
.items(
Joi.array()
.items(Joi.function().required(), Joi.object().required())
.length(2),
Joi.function(),
)
.default(DEFAULT_OPTIONS.rehypePlugins),
showLastUpdateTime: Joi.bool().default(DEFAULT_OPTIONS.showLastUpdateTime), showLastUpdateTime: Joi.bool().default(DEFAULT_OPTIONS.showLastUpdateTime),
showLastUpdateAuthor: Joi.bool().default( showLastUpdateAuthor: Joi.bool().default(
DEFAULT_OPTIONS.showLastUpdateAuthor, DEFAULT_OPTIONS.showLastUpdateAuthor,
), ),
admonitions: Joi.object().default(DEFAULT_OPTIONS.admonitions),
excludeNextVersionDocs: Joi.bool().default( excludeNextVersionDocs: Joi.bool().default(
DEFAULT_OPTIONS.excludeNextVersionDocs, DEFAULT_OPTIONS.excludeNextVersionDocs,
), ),

View file

@ -15,10 +15,14 @@
"@types/hapi__joi": "^17.1.2" "@types/hapi__joi": "^17.1.2"
}, },
"dependencies": { "dependencies": {
"@docusaurus/mdx-loader": "2.0.0-alpha.60",
"@docusaurus/types": "2.0.0-alpha.60", "@docusaurus/types": "2.0.0-alpha.60",
"@docusaurus/utils": "2.0.0-alpha.60", "@docusaurus/utils": "2.0.0-alpha.60",
"@docusaurus/utils-validation": "2.0.0-alpha.60",
"@hapi/joi": "17.1.1", "@hapi/joi": "17.1.1",
"globby": "^10.0.1" "loader-utils": "^1.2.3",
"globby": "^10.0.1",
"remark-admonitions": "^1.2.1"
}, },
"peerDependencies": { "peerDependencies": {
"@docusaurus/core": "^2.0.0", "@docusaurus/core": "^2.0.0",

View file

@ -0,0 +1,14 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
module.exports = {
title: 'My Site',
tagline: 'The tagline of my site',
url: 'https://your-docusaurus-test-site.com',
baseUrl: '/',
favicon: 'img/favicon.ico',
};

View file

@ -0,0 +1,5 @@
---
title: mdx page
description: my mdx page
---
MDX page

View file

@ -6,23 +6,15 @@
*/ */
import path from 'path'; import path from 'path';
import {loadContext} from '@docusaurus/core/lib/server';
import pluginContentPages from '../index'; import pluginContentPages from '../index';
import {LoadContext} from '@docusaurus/types';
import normalizePluginOptions from './pluginOptionSchema.test'; import normalizePluginOptions from './pluginOptionSchema.test';
describe('docusaurus-plugin-content-pages', () => { describe('docusaurus-plugin-content-pages', () => {
test('simple pages', async () => { test('simple pages', async () => {
const siteDir = path.join(__dirname, '__fixtures__', 'website'); const siteDir = path.join(__dirname, '__fixtures__', 'website');
const siteConfig = { const context = loadContext(siteDir);
title: 'Hello',
baseUrl: '/',
url: 'https://docusaurus.io',
};
const context = {
siteDir,
siteConfig,
} as LoadContext;
const pluginPath = 'src/pages'; const pluginPath = 'src/pages';
const plugin = pluginContentPages( const plugin = pluginContentPages(
context, context,
@ -34,14 +26,27 @@ describe('docusaurus-plugin-content-pages', () => {
expect(pagesMetadatas).toEqual([ expect(pagesMetadatas).toEqual([
{ {
type: 'jsx',
permalink: '/', permalink: '/',
source: path.join('@site', pluginPath, 'index.js'), source: path.join('@site', pluginPath, 'index.js'),
}, },
{ {
type: 'jsx',
permalink: '/typescript', permalink: '/typescript',
source: path.join('@site', pluginPath, 'typescript.tsx'), source: path.join('@site', pluginPath, 'typescript.tsx'),
}, },
{ {
type: 'mdx',
permalink: '/hello/',
source: path.join('@site', pluginPath, 'hello', 'index.md'),
},
{
type: 'mdx',
permalink: '/hello/mdxPage',
source: path.join('@site', pluginPath, 'hello', 'mdxPage.mdx'),
},
{
type: 'jsx',
permalink: '/hello/world', permalink: '/hello/world',
source: path.join('@site', pluginPath, 'hello', 'world.js'), source: path.join('@site', pluginPath, 'hello', 'world.js'),
}, },

View file

@ -6,8 +6,11 @@
*/ */
import {PluginOptionSchema, DEFAULT_OPTIONS} from '../pluginOptionSchema'; import {PluginOptionSchema, DEFAULT_OPTIONS} from '../pluginOptionSchema';
import {PluginOptions} from '../types';
export default function normalizePluginOptions(options) { export default function normalizePluginOptions(
options: Partial<PluginOptions>,
) {
const {value, error} = PluginOptionSchema.validate(options, { const {value, error} = PluginOptionSchema.validate(options, {
convert: false, convert: false,
}); });
@ -19,29 +22,30 @@ export default function normalizePluginOptions(options) {
} }
describe('normalizePagesPluginOptions', () => { describe('normalizePagesPluginOptions', () => {
test('should return default options for undefined user options', async () => { test('should return default options for undefined user options', () => {
const {value} = await PluginOptionSchema.validate({}); const value = normalizePluginOptions({});
expect(value).toEqual(DEFAULT_OPTIONS); expect(value).toEqual(DEFAULT_OPTIONS);
}); });
test('should fill in default options for partially defined user options', async () => { test('should fill in default options for partially defined user options', () => {
const {value} = await PluginOptionSchema.validate({path: 'src/pages'}); const value = normalizePluginOptions({path: 'src/pages'});
expect(value).toEqual(DEFAULT_OPTIONS); expect(value).toEqual(DEFAULT_OPTIONS);
}); });
test('should accept correctly defined user options', async () => { test('should accept correctly defined user options', () => {
const userOptions = { const userOptions = {
path: 'src/my-pages', path: 'src/my-pages',
routeBasePath: 'my-pages', routeBasePath: 'my-pages',
include: ['**/*.{js,jsx,ts,tsx}'], include: ['**/*.{js,jsx,ts,tsx}'],
}; };
const {value} = await PluginOptionSchema.validate(userOptions); const value = normalizePluginOptions(userOptions);
expect(value).toEqual(userOptions); expect(value).toEqual({...DEFAULT_OPTIONS, ...userOptions});
}); });
test('should reject bad path inputs', () => { test('should reject bad path inputs', () => {
expect(() => { expect(() => {
normalizePluginOptions({ normalizePluginOptions({
// @ts-expect-error: bad attribute
path: 42, path: 42,
}); });
}).toThrowErrorMatchingInlineSnapshot(`"\\"path\\" must be a string"`); }).toThrowErrorMatchingInlineSnapshot(`"\\"path\\" must be a string"`);

View file

@ -8,23 +8,46 @@
import globby from 'globby'; import globby from 'globby';
import fs from 'fs'; import fs from 'fs';
import path from 'path'; import path from 'path';
import {encodePath, fileToPath, aliasedSitePath} from '@docusaurus/utils'; import {
encodePath,
fileToPath,
aliasedSitePath,
docuHash,
} from '@docusaurus/utils';
import { import {
LoadContext, LoadContext,
Plugin, Plugin,
OptionValidationContext, OptionValidationContext,
ValidationResult, ValidationResult,
ConfigureWebpackUtils,
} from '@docusaurus/types'; } from '@docusaurus/types';
import {Configuration, Loader} from 'webpack';
import {PluginOptions, LoadedContent} from './types'; import admonitions from 'remark-admonitions';
import {PluginOptionSchema} from './pluginOptionSchema'; import {PluginOptionSchema} from './pluginOptionSchema';
import {ValidationError} from '@hapi/joi'; import {ValidationError} from '@hapi/joi';
import {PluginOptions, LoadedContent, Metadata} from './types';
const isMarkdownSource = (source: string) =>
source.endsWith('.md') || source.endsWith('.mdx');
export default function pluginContentPages( export default function pluginContentPages(
context: LoadContext, context: LoadContext,
options: PluginOptions, options: PluginOptions,
): Plugin<LoadedContent | null, typeof PluginOptionSchema> { ): Plugin<LoadedContent | null, typeof PluginOptionSchema> {
const contentPath = path.resolve(context.siteDir, options.path); if (options.admonitions) {
options.remarkPlugins = options.remarkPlugins.concat([
[admonitions, options.admonitions || {}],
]);
}
const {siteConfig, siteDir, generatedFilesDir} = context;
const contentPath = path.resolve(siteDir, options.path);
const dataDir = path.join(
generatedFilesDir,
'docusaurus-plugin-content-pages',
);
return { return {
name: 'docusaurus-plugin-content-pages', name: 'docusaurus-plugin-content-pages',
@ -35,9 +58,18 @@ export default function pluginContentPages(
return [...globPattern]; return [...globPattern];
}, },
getClientModules() {
const modules = [];
if (options.admonitions) {
modules.push(require.resolve('remark-admonitions/styles/infima.css'));
}
return modules;
},
async loadContent() { async loadContent() {
const {include} = options; const {include} = options;
const {siteConfig, siteDir} = context;
const pagesDir = contentPath; const pagesDir = contentPath;
if (!fs.existsSync(pagesDir)) { if (!fs.existsSync(pagesDir)) {
@ -49,16 +81,27 @@ export default function pluginContentPages(
cwd: pagesDir, cwd: pagesDir,
}); });
return pagesFiles.map((relativeSource) => { function toMetadata(relativeSource: string): Metadata {
const source = path.join(pagesDir, relativeSource); const source = path.join(pagesDir, relativeSource);
const aliasedSource = aliasedSitePath(source, siteDir); const aliasedSource = aliasedSitePath(source, siteDir);
const pathName = encodePath(fileToPath(relativeSource)); const pathName = encodePath(fileToPath(relativeSource));
// Default Language. const permalink = pathName.replace(/^\//, baseUrl || '');
return { if (isMarkdownSource(relativeSource)) {
permalink: pathName.replace(/^\//, baseUrl || ''), return {
source: aliasedSource, type: 'mdx',
}; permalink,
}); source: aliasedSource,
};
} else {
return {
type: 'jsx',
permalink,
source: aliasedSource,
};
}
}
return pagesFiles.map(toMetadata);
}, },
async contentLoaded({content, actions}) { async contentLoaded({content, actions}) {
@ -66,22 +109,89 @@ export default function pluginContentPages(
return; return;
} }
const {addRoute} = actions; const {addRoute, createData} = actions;
await Promise.all( await Promise.all(
content.map(async (metadataItem) => { content.map(async (metadata) => {
const {permalink, source} = metadataItem; const {permalink, source} = metadata;
addRoute({ if (metadata.type === 'mdx') {
path: permalink, await createData(
component: source, // Note that this created data path must be in sync with
exact: true, // metadataPath provided to mdx-loader.
modules: { `${docuHash(metadata.source)}.json`,
config: `@generated/docusaurus.config`, JSON.stringify(metadata, null, 2),
}, );
}); addRoute({
path: permalink,
component: options.mdxPageComponent,
exact: true,
modules: {
content: source,
},
});
} else {
addRoute({
path: permalink,
component: source,
exact: true,
modules: {
config: `@generated/docusaurus.config`,
},
});
}
}), }),
); );
}, },
configureWebpack(
_config: Configuration,
isServer: boolean,
{getBabelLoader, getCacheLoader}: ConfigureWebpackUtils,
) {
const {rehypePlugins, remarkPlugins} = options;
return {
resolve: {
alias: {
'~pages': dataDir,
},
},
module: {
rules: [
{
test: /(\.mdx?)$/,
include: [contentPath],
use: [
getCacheLoader(isServer),
getBabelLoader(isServer),
{
loader: require.resolve('@docusaurus/mdx-loader'),
options: {
remarkPlugins,
rehypePlugins,
// Note that metadataPath must be the same/in-sync as
// the path from createData for each MDX.
metadataPath: (mdxPath: string) => {
const aliasedSource = aliasedSitePath(mdxPath, siteDir);
return path.join(
dataDir,
`${docuHash(aliasedSource)}.json`,
);
},
},
},
{
loader: path.resolve(__dirname, './markdownLoader.js'),
options: {
// siteDir,
// contentPath,
},
},
].filter(Boolean) as Loader[],
},
],
},
};
},
}; };
} }

View file

@ -0,0 +1,22 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import {loader} from 'webpack';
// import {getOptions} from 'loader-utils';
const markdownLoader: loader.Loader = function (fileString) {
const callback = this.async();
// const options = getOptions(this);
// TODO provide additinal md processing here? like interlinking pages?
// fileString = linkify(fileString)
return callback && callback(null, fileString);
};
export default markdownLoader;

View file

@ -6,15 +6,28 @@
*/ */
import * as Joi from '@hapi/joi'; import * as Joi from '@hapi/joi';
import {PluginOptions} from './types'; import {PluginOptions} from './types';
import {
RemarkPluginsSchema,
RehypePluginsSchema,
AdmonitionsSchema,
} from '@docusaurus/utils-validation';
export const DEFAULT_OPTIONS: PluginOptions = { export const DEFAULT_OPTIONS: PluginOptions = {
path: 'src/pages', // Path to data on filesystem, relative to site dir. path: 'src/pages', // Path to data on filesystem, relative to site dir.
routeBasePath: '', // URL Route. routeBasePath: '', // URL Route.
include: ['**/*.{js,jsx,ts,tsx}'], // Extensions to include. include: ['**/*.{js,jsx,ts,tsx,md,mdx}'], // Extensions to include.
mdxPageComponent: '@theme/MDXPage',
remarkPlugins: [],
rehypePlugins: [],
admonitions: {},
}; };
export const PluginOptionSchema = Joi.object({ export const PluginOptionSchema = Joi.object({
path: Joi.string().default(DEFAULT_OPTIONS.path), path: Joi.string().default(DEFAULT_OPTIONS.path),
routeBasePath: Joi.string().default(DEFAULT_OPTIONS.routeBasePath), routeBasePath: Joi.string().default(DEFAULT_OPTIONS.routeBasePath),
include: Joi.array().items(Joi.string()).default(DEFAULT_OPTIONS.include), include: Joi.array().items(Joi.string()).default(DEFAULT_OPTIONS.include),
mdxPageComponent: Joi.string().default(DEFAULT_OPTIONS.mdxPageComponent),
remarkPlugins: RemarkPluginsSchema.default(DEFAULT_OPTIONS.remarkPlugins),
rehypePlugins: RehypePluginsSchema.default(DEFAULT_OPTIONS.rehypePlugins),
admonitions: AdmonitionsSchema.default(DEFAULT_OPTIONS.admonitions),
}); });

View file

@ -9,11 +9,24 @@ export interface PluginOptions {
path: string; path: string;
routeBasePath: string; routeBasePath: string;
include: string[]; include: string[];
mdxPageComponent: string;
remarkPlugins: ([Function, object] | Function)[];
rehypePlugins: string[];
admonitions: any;
} }
export interface Metadata { export type JSXPageMetadata = {
type: 'jsx';
permalink: string; permalink: string;
source: string; source: string;
} };
export type MDXPageMetadata = {
type: 'mdx';
permalink: string;
source: string;
};
export type Metadata = JSXPageMetadata | MDXPageMetadata;
export type LoadedContent = Metadata[]; export type LoadedContent = Metadata[];

View file

@ -0,0 +1,13 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
declare module 'remark-admonitions' {
type Options = any;
const plugin: (options?: Options) => void;
export = plugin;
}

View file

@ -0,0 +1,32 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import React from 'react';
import Layout from '@theme/Layout';
import {MDXProvider} from '@mdx-js/react';
import MDXComponents from '@theme/MDXComponents';
function MDXPage(props) {
const {content: MDXPageContent} = props;
const {frontMatter, metadata} = MDXPageContent;
const {title, description} = frontMatter;
const {permalink} = metadata;
return (
<Layout title={title} description={description} permalink={permalink}>
<main>
<div className="container margin-vert--lg padding-vert--lg">
<MDXProvider components={MDXComponents}>
<MDXPageContent />
</MDXProvider>
</div>
</main>
</Layout>
);
}
export default MDXPage;

View file

@ -0,0 +1,24 @@
{
"name": "@docusaurus/utils-validation",
"version": "2.0.0-alpha.60",
"description": "Node validation utility functions for Docusaurus packages",
"main": "./lib/index.js",
"types": "./lib/index.d.ts",
"scripts": {
"build": "tsc",
"watch": "tsc --watch"
},
"publishConfig": {
"access": "public"
},
"license": "MIT",
"devDependencies": {
"@types/hapi__joi": "^17.1.2"
},
"dependencies": {
"@hapi/joi": "17.1.1"
},
"engines": {
"node": ">=10.15.1"
}
}

View file

@ -0,0 +1,45 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`validation schemas AdmonitionsSchema 1`] = `"\\"value\\" must be of type object"`;
exports[`validation schemas AdmonitionsSchema 2`] = `"\\"value\\" must be of type object"`;
exports[`validation schemas AdmonitionsSchema 3`] = `"\\"value\\" must be of type object"`;
exports[`validation schemas AdmonitionsSchema 4`] = `"\\"value\\" must be of type object"`;
exports[`validation schemas RehypePluginsSchema 1`] = `"\\"value\\" must be an array"`;
exports[`validation schemas RehypePluginsSchema 2`] = `"\\"value\\" must be an array"`;
exports[`validation schemas RehypePluginsSchema 3`] = `"\\"value\\" must be an array"`;
exports[`validation schemas RehypePluginsSchema 4`] = `"\\"[0]\\" does not match any of the allowed types"`;
exports[`validation schemas RehypePluginsSchema 5`] = `"\\"[0]\\" does not match any of the allowed types"`;
exports[`validation schemas RehypePluginsSchema 6`] = `"\\"[0]\\" does not match any of the allowed types"`;
exports[`validation schemas RehypePluginsSchema 7`] = `"\\"[0]\\" does not match any of the allowed types"`;
exports[`validation schemas RehypePluginsSchema 8`] = `"\\"[0]\\" does not match any of the allowed types"`;
exports[`validation schemas RehypePluginsSchema 9`] = `"\\"[0]\\" does not match any of the allowed types"`;
exports[`validation schemas RemarkPluginsSchema 1`] = `"\\"value\\" must be an array"`;
exports[`validation schemas RemarkPluginsSchema 2`] = `"\\"value\\" must be an array"`;
exports[`validation schemas RemarkPluginsSchema 3`] = `"\\"value\\" must be an array"`;
exports[`validation schemas RemarkPluginsSchema 4`] = `"\\"[0]\\" does not match any of the allowed types"`;
exports[`validation schemas RemarkPluginsSchema 5`] = `"\\"[0]\\" does not match any of the allowed types"`;
exports[`validation schemas RemarkPluginsSchema 6`] = `"\\"[0]\\" does not match any of the allowed types"`;
exports[`validation schemas RemarkPluginsSchema 7`] = `"\\"[0]\\" does not match any of the allowed types"`;
exports[`validation schemas RemarkPluginsSchema 8`] = `"\\"[0]\\" does not match any of the allowed types"`;
exports[`validation schemas RemarkPluginsSchema 9`] = `"\\"[0]\\" does not match any of the allowed types"`;

View file

@ -0,0 +1,84 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import * as Joi from '@hapi/joi';
import {
AdmonitionsSchema,
RehypePluginsSchema,
RemarkPluginsSchema,
} from '../index';
function createTestHelpers({
schema,
defaultValue,
}: {
schema: Joi.SchemaLike;
defaultValue?: unknown;
}) {
function testOK(value: unknown) {
expect(Joi.attempt(value, schema)).toEqual(value ?? defaultValue);
}
function testFail(value: unknown) {
expect(() => Joi.attempt(value, schema)).toThrowErrorMatchingSnapshot();
}
return {testOK, testFail};
}
function testMarkdownPluginSchemas(schema: Joi.SchemaLike) {
const {testOK, testFail} = createTestHelpers({
schema,
defaultValue: [],
});
testOK(undefined);
testOK([function () {}]);
testOK([[function () {}, {attr: 'val'}]]);
testOK([
[function () {}, {attr: 'val'}],
function () {},
[function () {}, {attr: 'val'}],
]);
testFail(null);
testFail(false);
testFail(3);
testFail([null]);
testFail([false]);
testFail([3]);
testFail([[]]);
testFail([[function () {}, undefined]]);
testFail([[function () {}, true]]);
}
describe('validation schemas', () => {
test('AdmonitionsSchema', () => {
const {testOK, testFail} = createTestHelpers({
schema: AdmonitionsSchema,
defaultValue: {},
});
testOK(undefined);
testOK({});
testOK({attr: 'val'});
testFail(null);
testFail(3);
testFail(true);
testFail([]);
});
test('RemarkPluginsSchema', () => {
testMarkdownPluginSchemas(RemarkPluginsSchema);
});
test('RehypePluginsSchema', () => {
testMarkdownPluginSchemas(RehypePluginsSchema);
});
});

View file

@ -0,0 +1,22 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import * as Joi from '@hapi/joi';
const MarkdownPluginsSchema = Joi.array()
.items(
Joi.array()
// TODO, this allows [config,fn] too?
.items(Joi.function().required(), Joi.object().required())
.length(2),
Joi.function(),
)
.default([]);
export const RemarkPluginsSchema = MarkdownPluginsSchema;
export const RehypePluginsSchema = MarkdownPluginsSchema;
export const AdmonitionsSchema = Joi.object().default({});

View file

@ -0,0 +1,9 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"incremental": true,
"tsBuildInfoFile": "./lib/.tsbuildinfo",
"rootDir": "src",
"outDir": "lib"
}
}

View file

@ -62,8 +62,8 @@ export function objectWithKeySorted(obj: {[index: string]: any}) {
}, {}); }, {});
} }
const indexRE = /(^|.*\/)index\.(md|js|jsx|ts|tsx)$/i; const indexRE = /(^|.*\/)index\.(md|mdx|js|jsx|ts|tsx)$/i;
const extRE = /\.(md|js|ts|tsx)$/; const extRE = /\.(md|mdx|js|jsx|ts|tsx)$/;
/** /**
* Convert filepath to url path. * Convert filepath to url path.

View file

@ -7,13 +7,13 @@ In this section, we will learn about creating ad-hoc pages in Docusaurus using R
The functionality of pages is powered by `@docusaurus/plugin-content-pages`. The functionality of pages is powered by `@docusaurus/plugin-content-pages`.
## Adding a new page You can use React components, or Markdown.
<!-- TODO: What will the user see if pages/ is empty? --> ## Add a React page
In the `/src/pages/` directory, create a file called `hello.js` with the following contents: Create a file `/src/pages/helloReact.js`:
```jsx title="/src/pages/hello.js" ```jsx title="/src/pages/helloReact.js"
import React from 'react'; import React from 'react';
import Layout from '@theme/Layout'; import Layout from '@theme/Layout';
@ -39,13 +39,40 @@ function Hello() {
export default Hello; export default Hello;
``` ```
Once you save the file, the development server will automatically reload the changes. Now open http://localhost:3000/hello, you will see the new page you just created. Once you save the file, the development server will automatically reload the changes. Now open `http://localhost:3000/helloReact`, you will see the new page you just created.
Each page doesn't come with any styling. You will need to import the `Layout` component from `@theme/Layout` and wrap your contents within that component if you want the navbar and/or footer to appear. Each page doesn't come with any styling. You will need to import the `Layout` component from `@theme/Layout` and wrap your contents within that component if you want the navbar and/or footer to appear.
:::tip :::tip
You can also create a page in TypeScript, in which case the file name should use the `.tsx` extension, eg. `hello.tsx`. You can also create TypeScript pages with the `.tsx` extension (`helloReact.tsx`).
:::
## Add a Markdown page
Create a file `/src/pages/helloMarkdown.md`:
```mdx title="/src/pages/helloMarkdown.md"
---
title: my hello page title
description: my hello page description
---
# Hello
How are you?
```
In the same way, a page will be created at `http://localhost:3000/helloMarkdown`.
Markdown pages are less flexible than React pages, because it always uses the theme layout.
Here's an [example markdown page](/examples/markdownPageExample).
:::tip
You can use the full power of React in Markdown pages too, refer to the [MDX](https://mdxjs.com/) documentation.
::: :::
@ -85,9 +112,4 @@ All JavaScript/TypeScript files within the `src/pages/` directory will have corr
## Using React ## Using React
React is used as the UI library to create pages. Every page component should export a React component and you can leverage on the expressiveness of React to build rich and interactive content. React is used as the UI library to create pages. Every page component should export a React component, and you can leverage on the expressiveness of React to build rich and interactive content.
<!--
TODO:
- That v2 is different from v1, users can write interactive components with lifecycles.
-->

View file

@ -376,6 +376,15 @@ module.exports = {
*/ */
routeBasePath: '', routeBasePath: '',
include: ['**/*.{js,jsx}'], include: ['**/*.{js,jsx}'],
/**
* Theme component used by markdown pages.
*/
mdxPageComponent: '@theme/MDXPage',
/**
* Remark and Rehype plugins passed to MDX
*/
remarkPlugins: [],
rehypePlugins: [],
}, },
], ],
], ],

View file

@ -136,6 +136,9 @@ module.exports = {
copyright: `Copyright © ${new Date().getFullYear()} Facebook, Inc.`, copyright: `Copyright © ${new Date().getFullYear()} Facebook, Inc.`,
}, },
}, },
pages: {
remarkPlugins: [require('./src/plugins/remark-npm2yarn')],
},
theme: { theme: {
customCss: require.resolve('./src/css/custom.css'), customCss: require.resolve('./src/css/custom.css'),
}, },

View file

@ -0,0 +1,34 @@
---
title: Markdown Page example title
description: Markdown Page example description
---
# Markdown page
This is a page generated from markdown to illustrate the markdown page feature.
It supports all the regular MDX features, as you can see:
:::info
Useful information.
:::
```jsx live
function Button() {
return (
<button type="button" onClick={() => alert('hey')}>
Click me!
</button>
);
}
```
![](../../../static/img/docusaurus.png)
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
<Tabs defaultValue="apple" values={[ {label: 'Apple', value: 'apple'}, {label: 'Orange', value: 'orange'}, {label: 'Banana', value: 'banana'} ]}><TabItem value="apple">This is an apple 🍎</TabItem><TabItem value="orange">This is an orange 🍊</TabItem><TabItem value="banana">This is a banana 🍌</TabItem></Tabs>