Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: handle all admonitions via JSX component #7152

Merged
merged 27 commits into from Jun 3, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
d02485a
refactor: handle all admonitions via JSX component
lex111 Apr 11, 2022
3b74726
fixes
Josh-Cena Apr 12, 2022
d2f18d8
Merge branch 'main' into lex111/themed-admonitions
slorber May 5, 2022
d3a9711
ability to customize admonition keywords and tag
slorber May 5, 2022
4b8121f
move remark admonitions to mdx-loader
slorber May 5, 2022
3bbdbab
upgrade AdmonitionsSchema
slorber May 5, 2022
00a7e74
upgrade AdmonitionsSchema
slorber May 5, 2022
f6fb1b0
upgrade AdmonitionsSchema
slorber May 5, 2022
1f3fff4
wire admonition option to all content plugins
slorber May 5, 2022
25ae510
fix admonitions validation schema
slorber May 5, 2022
32dbb35
add license + readme
slorber May 5, 2022
eb94727
fix test
slorber May 5, 2022
e3d4a02
lint issue
slorber May 5, 2022
c3ecb56
update Admonition, make it fail-safe
slorber May 5, 2022
41d614f
lint
slorber May 5, 2022
c101ae1
fixes
Josh-Cena May 6, 2022
63afa4e
fix?
Josh-Cena May 6, 2022
048d8d4
refactor: migrate to CSS modules, fix a11y issue
lex111 May 6, 2022
0e9e357
Merge branch 'main' into lex111/themed-admonitions
Josh-Cena May 14, 2022
cc0719f
Merge branch 'main' into lex111/themed-admonitions
Josh-Cena May 22, 2022
0e4663c
Merge branch 'main' into lex111/themed-admonitions
Josh-Cena May 24, 2022
233ca61
Merge branch 'main' into lex111/themed-admonitions
Josh-Cena May 24, 2022
a18f60b
Merge branch 'main' into lex111/themed-admonitions
Josh-Cena May 25, 2022
2e82db9
Merge branch 'main' into lex111/themed-admonitions
slorber Jun 2, 2022
01f669c
Implement workaround to support interpolation in MDX admonition titles
slorber Jun 3, 2022
287e2f0
add link to relevant issue comment https://github.com/facebook/docusa…
slorber Jun 3, 2022
63e9e02
Little refactors
Josh-Cena Jun 3, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 0 additions & 1 deletion packages/docusaurus-plugin-content-blog/package.json
Expand Up @@ -29,7 +29,6 @@
"fs-extra": "^10.0.1",
"lodash": "^4.17.21",
"reading-time": "^1.5.0",
"remark-admonitions": "^1.2.1",
"tslib": "^2.3.1",
"utility-types": "^3.10.0",
"webpack": "^5.72.0"
Expand Down
13 changes: 0 additions & 13 deletions packages/docusaurus-plugin-content-blog/src/deps.d.ts

This file was deleted.

7 changes: 0 additions & 7 deletions packages/docusaurus-plugin-content-blog/src/index.ts
Expand Up @@ -6,7 +6,6 @@
*/

import path from 'path';
import admonitions from 'remark-admonitions';
import {
normalizeUrl,
docuHash,
Expand Down Expand Up @@ -55,12 +54,6 @@ export default async function pluginContentBlog(
context: LoadContext,
options: PluginOptions,
): Promise<Plugin<BlogContent>> {
if (options.admonitions) {
options.remarkPlugins = options.remarkPlugins.concat([
[admonitions, options.admonitions],
]);
}

const {
siteDir,
siteConfig,
Expand Down
1 change: 0 additions & 1 deletion packages/docusaurus-plugin-content-docs/package.json
Expand Up @@ -34,7 +34,6 @@
"import-fresh": "^3.3.0",
"js-yaml": "^4.1.0",
"lodash": "^4.17.21",
"remark-admonitions": "^1.2.1",
"tslib": "^2.3.1",
"utility-types": "^3.10.0",
"webpack": "^5.72.0"
Expand Down
13 changes: 0 additions & 13 deletions packages/docusaurus-plugin-content-docs/src/deps.d.ts

This file was deleted.

7 changes: 0 additions & 7 deletions packages/docusaurus-plugin-content-docs/src/options.ts
Expand Up @@ -17,7 +17,6 @@ import {GlobExcludeDefault} from '@docusaurus/utils';

import type {OptionValidationContext} from '@docusaurus/types';
import logger from '@docusaurus/logger';
import admonitions from 'remark-admonitions';
import {DefaultSidebarItemsGenerator} from './sidebars/generator';
import {
DefaultNumberPrefixParser,
Expand Down Expand Up @@ -168,11 +167,5 @@ export function validateOptions({

const normalizedOptions = validate(OptionsSchema, options) as PluginOptions;

if (normalizedOptions.admonitions) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is removed, but admonitions remains in schema + default value

We want to still allow custom admonitions? Are custom admonitions configured globally or per plugin?

Do we want to do a breaking change and add a custom error message, or try to keep retro-compatibility?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should keep this here for the time being, but when we eventually have a centralized markdown.remarkPlugins config (#5999) I'd like to move it there. Note that I also want to have admonitions as part of the default MDX loader preset, so it can be configured as part of the docusaurus-remark-preset.

Copy link
Collaborator

@slorber slorber Apr 20, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Josh-Cena I agree we should keep the options there, at least for now

The problem is that the provided options do not make sense anymore

https://github.com/lorikrell/gamerchic/blob/ec6e1595a51b7e65538262dd9308fb7517e42f9f/docusaurus.config.js#L359

{
        admonitions: {
          customTypes: {
            homebrew: {
              keyword: `homebrew`,
              infima: true,
              svg: '<svg /></svg>'
            },
            gamerchic: {
              keyword: `gamerchic`,
              infima: true,
              svg: '<svg /></svg>'
            },
          },
        },
}  

Now the options should look like:

{
        admonitions: {
          customKeywords: ["homebrew","gamerchic"]
        },
}  

And custom styling/svg should be applied by swizzling the admonition component.

@lex111 it's worth supporting that to ensure that the above site config have a way to upgrade Docusaurus without losing its custom admonitions

We also need to add option schema changes so that it prints a clear error message when user is trying to use the "old" customizations format

normalizedOptions.remarkPlugins = normalizedOptions.remarkPlugins.concat([
[admonitions, normalizedOptions.admonitions],
]);
}

return normalizedOptions;
}
1 change: 0 additions & 1 deletion packages/docusaurus-plugin-content-pages/package.json
Expand Up @@ -23,7 +23,6 @@
"@docusaurus/utils": "2.0.0-beta.18",
"@docusaurus/utils-validation": "2.0.0-beta.18",
"fs-extra": "^10.0.1",
"remark-admonitions": "^1.2.1",
"tslib": "^2.3.1",
"webpack": "^5.72.0"
},
Expand Down
13 changes: 0 additions & 13 deletions packages/docusaurus-plugin-content-pages/src/deps.d.ts

This file was deleted.

6 changes: 0 additions & 6 deletions packages/docusaurus-plugin-content-pages/src/index.ts
Expand Up @@ -22,7 +22,6 @@ import {
parseMarkdownString,
} from '@docusaurus/utils';
import type {LoadContext, Plugin} from '@docusaurus/types';
import admonitions from 'remark-admonitions';
import {validatePageFrontMatter} from './frontMatter';

import type {LoadedContent, PagesContentPaths} from './types';
Expand All @@ -39,11 +38,6 @@ export default async function pluginContentPages(
context: LoadContext,
options: PluginOptions,
): Promise<Plugin<LoadedContent | null>> {
if (options.admonitions) {
options.remarkPlugins = options.remarkPlugins.concat([
[admonitions, options.admonitions],
]);
}
const {
siteConfig,
siteDir,
Expand Down
2 changes: 2 additions & 0 deletions packages/docusaurus-theme-classic/package.json
Expand Up @@ -53,6 +53,8 @@
"cross-env": "^7.0.3",
"fs-extra": "^10.0.1",
"react-test-renderer": "^17.0.2",
"rehype-stringify": "^8.0.0",
"remark-rehype": "^8.1.0",
"utility-types": "^3.10.0"
},
"peerDependencies": {
Expand Down
37 changes: 25 additions & 12 deletions packages/docusaurus-theme-classic/src/index.ts
Expand Up @@ -15,6 +15,8 @@ import {readDefaultCodeTranslationMessages} from '@docusaurus/theme-translations
import type {Options} from '@docusaurus/theme-classic';
import type webpack from 'webpack';

import admonitions from './remark/admonitions';

const requireFromDocusaurusCore = createRequire(
require.resolve('@docusaurus/core/package.json'),
);
Expand Down Expand Up @@ -151,22 +153,33 @@ export default function docusaurusThemeClassic(
return modules;
},

configureWebpack() {
configureWebpack(config: webpack.Configuration) {
const prismLanguages = additionalLanguages
.map((lang) => `prism-${lang}`)
.join('|');

return {
plugins: [
// This allows better optimization by only bundling those components
// that the user actually needs, because the modules are dynamically
// required and can't be known during compile time.
new ContextReplacementPlugin(
/prismjs[\\/]components$/,
new RegExp(`^./(${prismLanguages})$`),
),
],
};
// This allows better optimization by only bundling those components
// that the user actually needs, because the modules are dynamically
// required and can't be known during compile time.
config.plugins?.push(
new ContextReplacementPlugin(
/prismjs[\\/]components$/,
new RegExp(`^./(${prismLanguages})$`),
),
);

(config.module?.rules as webpack.RuleSetRule[])?.forEach((rule) => {
if (Array.isArray(rule.use)) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
rule?.use.forEach((useItem: any) => {
if (useItem.loader!.includes('docusaurus-mdx-loader')) {
useItem?.options.remarkPlugins.push(admonitions);
}
});
}
});
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks kind of hacky. In the future we can use #6370 to handle this (this is already marked as one of the use-cases of configureMarkdownLoader).

Also, because the fallback MDX loader in the core is pushed after all plugins, I think this would make admonitions unavailable to documents using the fallback loader? Maybe we need to change that order?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was the only way to push remark plugin into the all MDX loaders. Are admonitions in the fallback loader really necessary?

Copy link
Collaborator

@Josh-Cena Josh-Cena Apr 11, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, it was added in core specifically because someone brought it up: #5333 it's mostly for partials not in the docs directory. It can be trivially fixed though, we just need to push the synthetic plugins before the others:

// 1. Plugin Lifecycle - Initialization/Constructor.
const plugins: InitializedPlugin[] = await initPlugins(context);
plugins.push(
createBootstrapPlugin(context),
createMDXFallbackPlugin(context),
);


return {mergeStrategy: {'*': 'replace'}, ...config};
},

configurePostCss(postCssOptions) {
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

@@ -0,0 +1,9 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`admonitions remark plugin base 1`] = `
"<p>The blog feature enables you to deploy in no time a full-featured blog.</p>
<admonition title="Sample Title" type="info"><p>Check the <a href="./api/plugins/plugin-content-blog.md">Blog Plugin API Reference documentation</a> for an exhaustive list of options.</p></admonition>
<h2>Initial setup {#initial-setup}</h2>
<p>To set up your site's blog, start by creating a <code>blog</code> directory.</p>
<admonition type="tip"><p>Use the <strong><a href="introduction.md#fast-track">Fast Track</a></strong> to understand Docusaurus in <strong>5 minutes ⏱</strong>!</p><p>Use <strong><a href="https://docusaurus.new">docusaurus.new</a></strong> to test Docusaurus immediately in your browser!</p></admonition>"
`;
@@ -0,0 +1,33 @@
/**
* 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 path from 'path';
import remark from 'remark';
import vfile from 'to-vfile';
import plugin from '../index';
import remark2rehype from 'remark-rehype';
import stringify from 'rehype-stringify';

const processFixture = async (name) => {
const filePath = path.join(__dirname, '__fixtures__', `${name}.md`);
const file = await vfile.read(filePath);

const result = await remark()
.use(plugin)
.use(remark2rehype)
.use(stringify)
.process(file);

return result.toString();
};

describe('admonitions remark plugin', () => {
it('base', async () => {
const result = await processFixture('base');
expect(result).toMatchSnapshot();
});
});
131 changes: 131 additions & 0 deletions packages/docusaurus-theme-classic/src/remark/admonitions/index.ts
@@ -0,0 +1,131 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is basically a modified copy of MIT-based code from https://github.com/elviswolcott/remark-admonitions/blob/master/lib/index.js on which you put Facebook copyright and not mention previous copyright

Technically I'm fine "owning" this code in the Docusaurus codebase, but I'm not sure this can be done this way legally, will have to check

Copy link
Collaborator

@slorber slorber Apr 14, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also we can probably publish this as a separate standalone package as it could be useful in other contexts.

We may eventually create a more generic tool that transforms custom syntax to JSX/MDX?

For example, we could also create a new custom syntax to handle tabs, similarly to this:

https://retype.com/components/tab/#multiple-tabs

(not so sure, maybe after we upgrade to mdx 2 we'll just need https://github.com/remarkjs/remark-directive)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like we can likely drop a LICENSE file in a subfolder to preserve the original license

Example: https://github.com/zpao/qrcode.react/tree/main/src/third-party/qrcodegen

Is all code in this subfolder coming from the original repo? Otherwise we can split between new files + copied files

Copy link
Collaborator

@Josh-Cena Josh-Cena Apr 20, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@slorber It's not a verbatim copy (and in fact significantly refactored, if not a total rewrite), so we need to have the licenses of both.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes I know, trying to figure this out. The above copy example has also been modified btw.

*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

/* eslint-disable */

import type {Transformer} from 'unified';
import visit from 'unist-util-visit';

const NEWLINE = '\n';

const config = {
tag: ':::',
keywords: [
'secondary',
'info',
'success',
'danger',
'note',
'tip',
'warning',
'important',
'caution',
],
};

function escapeRegExp(s: string): string {
return s.replace(new RegExp(`[-[\\]{}()*+?.\\\\^$|/]`, 'g'), '\\$&');
}

export default function plugin(this: any): Transformer {
const keywords = Object.values(config.keywords).map(escapeRegExp).join('|');
const tag = escapeRegExp(config.tag);
const regex = new RegExp(`${tag}(${keywords})(?: *(.*))?\n`);
const escapeTag = new RegExp(escapeRegExp(`\\${config.tag}`), 'g');

// the tokenizer is called on blocks to determine if there is an admonition present and create tags for it
function blockTokenizer(this: any, eat: any, value: string, silent: boolean) {
// stop if no match or match does not start at beginning of line
const match = regex.exec(value);
if (!match || match.index !== 0) return false;
// if silent return the match
if (silent) return true;

const now = eat.now();
const [opening, keyword, title] = match;
const food = [];
const content = [];

// consume lines until a closing tag
let idx = 0;
while ((idx = value.indexOf(NEWLINE)) !== -1) {
// grab this line and eat it
const next = value.indexOf(NEWLINE, idx + 1);
const line =
next !== -1 ? value.slice(idx + 1, next) : value.slice(idx + 1);
food.push(line);
value = value.slice(idx + 1);
// the closing tag is NOT part of the content
if (line.startsWith(config.tag)) break;
content.push(line);
}

// consume the processed tag and replace escape sequences
const contentString = content.join(NEWLINE).replace(escapeTag, config.tag);
const add = eat(opening + food.join(NEWLINE));

// parse the content in block mode
const exit = this.enterBlock();
const contentNodes = this.tokenizeBlock(contentString, now);
exit();

const element = {
type: 'admonitionHTML',
data: {
hName: 'admonition',
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is a pretty good step. This also means users can swizzle MDXComponents and re-map admonition to some other component, not necessarily @theme/Admonition.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 totally in favor of this

hProperties: {
title,
type: keyword,
},
},
children: contentNodes,
};

return add(element);
}

// add tokenizer to parser after fenced code blocks
const Parser = (this as any).Parser.prototype;
Parser.blockTokenizers.admonition = blockTokenizer;
Parser.blockMethods.splice(
Parser.blockMethods.indexOf('fencedCode') + 1,
0,
'admonition',
);
Parser.interruptParagraph.splice(
Parser.interruptParagraph.indexOf('fencedCode') + 1,
0,
['admonition'],
);
Parser.interruptList.splice(
Parser.interruptList.indexOf('fencedCode') + 1,
0,
['admonition'],
);
Parser.interruptBlockquote.splice(
Parser.interruptBlockquote.indexOf('fencedCode') + 1,
0,
['admonition'],
);

return function transformer(tree) {
// escape everything except admonitionHTML nodes
// @ts-ignore FIXME
visit(
tree,
(node: any) => {
return node.type !== 'admonitionHTML';
},
function visitor(node: any) {
if (node.value) {
node.value = node.value.replace(escapeTag, config.tag);
}
return node;
},
);
};
}