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
Changes from 1 commit
d02485a
3b74726
d2f18d8
d3a9711
4b8121f
3bbdbab
00a7e74
f6fb1b0
1f3fff4
25ae510
32dbb35
eb94727
e3d4a02
c3ecb56
41d614f
c101ae1
63afa4e
048d8d4
0e9e357
cc0719f
0e4663c
233ca61
a18f60b
2e82db9
01f669c
287e2f0
63e9e02
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
This file was deleted.
This file was deleted.
This file was deleted.
Original file line number | Diff line number | Diff line change | ||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -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'), | ||||||||||||||||
); | ||||||||||||||||
|
@@ -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); | ||||||||||||||||
} | ||||||||||||||||
}); | ||||||||||||||||
} | ||||||||||||||||
}); | ||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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: docusaurus/packages/docusaurus/src/server/plugins/index.ts Lines 38 to 44 in 0abf4d7
|
||||||||||||||||
|
||||||||||||||||
return {mergeStrategy: {'*': 'replace'}, ...config}; | ||||||||||||||||
}, | ||||||||||||||||
|
||||||||||||||||
configurePostCss(postCssOptions) { | ||||||||||||||||
|
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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>" | ||
`; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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(); | ||
}); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,131 @@ | ||
/** | ||
* Copyright (c) Facebook, Inc. and its affiliates. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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', | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||
}, | ||
); | ||
}; | ||
} |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 haveadmonitions
as part of the default MDX loader preset, so it can be configured as part of thedocusaurus-remark-preset
.There was a problem hiding this comment.
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
Now the options should look like:
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