diff --git a/__tests__/unit/node/markdown/markdown.test.ts b/__tests__/unit/node/markdown/markdown.test.ts index e8219a54..c042d6ba 100644 --- a/__tests__/unit/node/markdown/markdown.test.ts +++ b/__tests__/unit/node/markdown/markdown.test.ts @@ -101,5 +101,13 @@ describe('node/markdown/markdown', () => { 'tabindex' ) }) + + test('cjkFriendlyEmphasis', async () => { + const src = 'これは**「テスト」**です' + expect(await render(src)).toContain('「テスト」') + expect(await render(src, { cjkFriendlyEmphasis: false })).not.toContain( + '' + ) + }) }) }) diff --git a/src/node/markdown/markdown.ts b/src/node/markdown/markdown.ts index 6293b7bc..ee5c3954 100644 --- a/src/node/markdown/markdown.ts +++ b/src/node/markdown/markdown.ts @@ -54,17 +54,21 @@ export interface MarkdownOptions extends MarkdownItAsyncOptions { /* ==================== General Options ==================== */ /** - * Setup markdown-it instance before applying plugins + * Configure the markdown-it instance before any plugins are applied. */ preConfig?: (md: MarkdownItAsync) => Awaitable /** - * Setup markdown-it instance + * Configure the markdown-it instance after all built-in plugins are applied. */ config?: (md: MarkdownItAsync) => Awaitable /** * Disable cache (experimental) */ cache?: boolean + /** + * HTML attributes applied to external links. + * @default { target: '_blank', rel: 'noreferrer' } + */ externalLinks?: Record /* ==================== Syntax Highlighting ==================== */ @@ -72,7 +76,8 @@ export interface MarkdownOptions extends MarkdownItAsyncOptions { /** * Custom theme for syntax highlighting. * - * You can also pass an object with `light` and `dark` themes to support dual themes. + * You can also pass an object with `light` and `dark` themes to support + * dual themes. * * @example { theme: 'github-dark' } * @example { theme: { light: 'github-light', dark: 'github-dark' } } @@ -91,7 +96,8 @@ export interface MarkdownOptions extends MarkdownItAsyncOptions { /** * Custom language aliases for syntax highlighting. * Maps custom language names to existing languages. - * Alias lookup is case-insensitive and underscores in language names are displayed as spaces. + * Alias lookup is case-insensitive and underscores in language names are + * displayed as spaces. * * @example * @@ -113,31 +119,11 @@ export interface MarkdownOptions extends MarkdownItAsyncOptions { */ languageAlias?: Record /** - * Custom language labels for display. - * Overrides the default language label shown in code blocks. - * Keys are case-insensitive. - * - * @example { 'vue': 'Vue SFC' } - */ - languageLabel?: Record - /** - * Show line numbers in code blocks. Requires the `preWrapper` plugin. - * @default false - */ - lineNumbers?: boolean - /** - * Wrap code blocks in a container carrying the language label and the - * copy button. The default theme's code block styling relies on this - * markup. Disabling it also disables `lineNumbers`. - * @default true - */ - preWrapper?: boolean - /** - * Fallback language when the specified language is not available. + * Fallback language used when the specified language is not available. */ defaultHighlightLang?: string /** - * Transformers applied to code blocks + * Transformers applied to code blocks. * @see https://shiki.style/guide/transformers */ codeTransformers?: ShikiTransformer[] @@ -148,24 +134,46 @@ export interface MarkdownOptions extends MarkdownItAsyncOptions { */ colorReplacements?: CodeToHastOptions['colorReplacements'] /** - * Setup Shiki instance + * Configure the Shiki instance. */ shikiSetup?: (shiki: Highlighter) => void | Promise + + /* ==================== Code Blocks ==================== */ + + /** + * Wrap code blocks in a container carrying the language label and the + * copy button. The default theme's code block styling relies on this + * markup. Disabling it also disables `lineNumbers`. + * @default true + */ + preWrapper?: boolean /** - * The tooltip text for the copy button in code blocks + * The tooltip text for the copy button in code blocks. * @default 'Copy Code' */ codeCopyButtonTitle?: string - - /* ==================== Markdown It Plugins ==================== */ - /** - * Options for `markdown-it-anchor`. Set to `false` to disable adding ids - * and anchor links to headings. Note that the default theme's outline and - * heading hash links rely on these ids. - * @see https://github.com/valeriangalliat/markdown-it-anchor + * Custom language labels for display. + * Overrides the default language label shown in code blocks. + * Keys are case-insensitive. + * + * @example { 'vue': 'Vue SFC' } */ - anchor?: anchorPlugin.AnchorOptions | false + languageLabel?: Record + /** + * Show line numbers in code blocks. Requires the `preWrapper` plugin. + * @default false + */ + lineNumbers?: boolean + /** + * Enables importing code snippets from files with `<<<`. + * @default true + * @see https://vitepress.dev/guide/markdown#import-code-snippets + */ + snippet?: boolean + + /* ==================== Markdown Extensions ==================== */ + /** * Options for `markdown-it-attrs`. Set to `false` to disable. * @see https://github.com/arve0/markdown-it-attrs @@ -183,20 +191,26 @@ export interface MarkdownOptions extends MarkdownItAsyncOptions { } | false /** - * Options for `@mdit-vue/plugin-frontmatter` - * @see https://github.com/mdit-vue/mdit-vue/tree/main/packages/plugin-frontmatter + * Improves emphasis (`**bold**`) handling in Japanese, Chinese, and + * Korean text. + * @default true + * @see https://github.com/tats-u/markdown-cjk-friendly */ - frontmatter?: FrontmatterPluginOptions + cjkFriendlyEmphasis?: boolean /** - * Options for `@mdit-vue/plugin-headers` - * @see https://github.com/mdit-vue/mdit-vue/tree/main/packages/plugin-headers + * Options for `markdown-it-anchor`. Set to `false` to disable adding ids + * and anchor links to headings. Note that the default theme's outline and + * heading hash links rely on these ids. + * @see https://github.com/valeriangalliat/markdown-it-anchor */ - headers?: HeadersPluginOptions | boolean + anchor?: anchorPlugin.AnchorOptions | false /** - * Options for `@mdit-vue/plugin-sfc` - * @see https://github.com/mdit-vue/mdit-vue/tree/main/packages/plugin-sfc + * Options for `@mdit-vue/plugin-headers`. Set to `true` or pass options + * to collect page headers into page data. + * @default false + * @see https://github.com/mdit-vue/mdit-vue/tree/main/packages/plugin-headers */ - sfc?: SfcPluginOptions + headers?: HeadersPluginOptions | boolean /** * Options for `@mdit-vue/plugin-toc`. Set to `false` to disable the * `[[toc]]` syntax. @@ -204,30 +218,32 @@ export interface MarkdownOptions extends MarkdownItAsyncOptions { */ toc?: TocPluginOptions | false /** - * Options for `@mdit-vue/plugin-component`. Set to `false` to disable. - * @see https://github.com/mdit-vue/mdit-vue/tree/main/packages/plugin-component + * Math support. + * + * You need to install `markdown-it-mathjax3` and set `math` to `true` to + * enable it. You can also pass options to `markdown-it-mathjax3` here. + * @default false + * @see https://vitepress.dev/guide/markdown#math-equations */ - component?: ComponentPluginOptions | false + math?: boolean | any /** - * Enables importing code snippets from files with `<<<`. - * @default true - * @see https://vitepress.dev/guide/markdown#import-code-snippets + * Custom labels for the built-in containers (`::: tip` etc.). Also used + * as the default titles of GitHub-flavored alerts. + * @see https://vitepress.dev/guide/markdown#custom-containers */ - snippet?: boolean + container?: ContainerOptions /** - * Options for `markdown-it-container` - * @see https://github.com/markdown-it/markdown-it-container + * Whether to enable GitHub-flavored alerts (`> [!NOTE]`). + * @default true + * @see https://vitepress.dev/guide/markdown#github-flavored-alerts */ - container?: ContainerOptions + gfmAlerts?: boolean /** - * Math support - * - * You need to install `markdown-it-mathjax3` and set `math` to `true` to enable it. - * You can also pass options to `markdown-it-mathjax3` here. - * @default false - * @see https://vitepress.dev/guide/markdown#math-equations + * Add `tabindex="0"` to tables so keyboard users can focus and scroll + * them. + * @default true */ - math?: boolean | any + tableTabIndex?: boolean /** * Options for the image plugin (resolves image sources against the public * directory, adds dimensions, and supports lazy loading). Set to `false` @@ -235,24 +251,24 @@ export interface MarkdownOptions extends MarkdownItAsyncOptions { * @see https://vitepress.dev/guide/markdown#image-lazy-loading */ image?: ImageOptions | false + + /* ==================== Vue Integration ==================== */ + /** - * Allows disabling the github alerts plugin - * @default true - * @see https://vitepress.dev/guide/markdown#github-flavored-alerts + * Options for `@mdit-vue/plugin-component`. Set to `false` to disable. + * @see https://github.com/mdit-vue/mdit-vue/tree/main/packages/plugin-component */ - gfmAlerts?: boolean + component?: ComponentPluginOptions | false /** - * Add `tabindex="0"` to tables so keyboard users can focus and scroll them. - * @default true + * Options for `@mdit-vue/plugin-frontmatter`. + * @see https://github.com/mdit-vue/mdit-vue/tree/main/packages/plugin-frontmatter */ - tableTabIndex?: boolean + frontmatter?: FrontmatterPluginOptions /** - * Allows disabling the CJK-friendly plugin. - * This plugin adds support for emphasis marks (**bold**) in Japanese, Chinese, and Korean text. - * @default true - * @see https://github.com/tats-u/markdown-cjk-friendly + * Options for `@mdit-vue/plugin-sfc`. + * @see https://github.com/mdit-vue/mdit-vue/tree/main/packages/plugin-sfc */ - cjkFriendlyEmphasis?: boolean + sfc?: SfcPluginOptions } export type MarkdownRenderer = MarkdownItAsync @@ -287,7 +303,7 @@ export async function createMarkdownRenderer( const theme = options.theme ?? { light: 'github-light', dark: 'github-dark' } const codeCopyButtonTitle = options.codeCopyButtonTitle || 'Copy Code' - let [highlight, dispose] = options.highlight + const [highlight, dispose] = options.highlight ? [options.highlight, () => {}] : await createHighlighter(theme, options, logger) @@ -305,21 +321,22 @@ export async function createMarkdownRenderer( const slugify = (options.anchor ? options.anchor.slugify : undefined) ?? defaultSlugify - // custom plugins - if (options.component !== false) { - componentPlugin(md, options.component) - } + // VitePress customizations if (options.preWrapper !== false) { preWrapperPlugin(md, { codeCopyButtonTitle, languageLabel: options.languageLabel }) + // must be applied after preWrapper as it augments its output lineNumberPlugin(md, options.lineNumbers) } if (options.snippet !== false) { snippetPlugin(md, srcDir) } containerPlugin(md, options.container) + if (options.gfmAlerts !== false) { + gitHubAlertsPlugin(md, options.container) + } if (options.image !== false) { imagePlugin(md, publicDir, options.image) } @@ -329,25 +346,23 @@ export async function createMarkdownRenderer( base, slugify ) - if (options.tableTabIndex !== false) { tablePlugin(md) } - if (options.gfmAlerts !== false) { - gitHubAlertsPlugin(md, options.container) - } - - // third party plugins + // community plugins if (options.attrs !== false) { attrsPlugin(md, options.attrs) } if (options.emoji !== false) { emojiPlugin(md, options.emoji) } - - // mdit-vue plugins + if (options.cjkFriendlyEmphasis !== false) { + mditCjkFriendly(md) + } if (options.anchor !== false) { + // must be applied after attrs so that user-defined ids from curly + // attributes take precedence over slugified ones anchorPlugin(md, { slugify, getTokensText: (tokens) => { @@ -384,32 +399,6 @@ export async function createMarkdownRenderer( ...options.anchor }) } - - frontmatterPlugin(md, options.frontmatter) - - if (options.headers) { - headersPlugin(md, { - level: [2, 3, 4, 5, 6], - slugify, - ...(typeof options.headers === 'boolean' ? undefined : options.headers) - }) - } - - sfcPlugin(md, options.sfc) - titlePlugin(md) - - const tocOptions = options.toc - if (tocOptions !== false) { - tocPlugin(md, { - slugify, - ...tocOptions, - format: (s) => { - const title = s.replaceAll('&', '&') // encoded twice because of restoreEntities - return tocOptions?.format?.(title) ?? title - } - }) - } - if (options.math) { try { const mathPlugin = await import('markdown-it-mathjax3') @@ -435,8 +424,30 @@ export async function createMarkdownRenderer( } } - if (options.cjkFriendlyEmphasis !== false) { - mditCjkFriendly(md) + // mdit-vue plugins + if (options.component !== false) { + componentPlugin(md, options.component) + } + frontmatterPlugin(md, options.frontmatter) + if (options.headers) { + headersPlugin(md, { + level: [2, 3, 4, 5, 6], + slugify, + ...(typeof options.headers === 'boolean' ? undefined : options.headers) + }) + } + sfcPlugin(md, options.sfc) + titlePlugin(md) + const tocOptions = options.toc + if (tocOptions !== false) { + tocPlugin(md, { + slugify, + ...tocOptions, + format: (s) => { + const title = s.replaceAll('&', '&') // encoded twice because of restoreEntities + return tocOptions?.format?.(title) ?? title + } + }) } // apply user config