From 9aa30daf5452f68fb3ffcc5740e9db80064c2d07 Mon Sep 17 00:00:00 2001 From: cm-igarashi-ryosuke Date: Thu, 18 Dec 2025 17:59:59 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20prism=E3=82=92shiki=E3=81=AB=E7=BD=AE?= =?UTF-8?q?=E3=81=8D=E6=8F=9B=E3=81=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/zenn-cli/src/server/api/articles.ts | 4 +- packages/zenn-cli/src/server/api/books.ts | 4 +- packages/zenn-cli/src/server/lib/articles.ts | 4 +- packages/zenn-cli/src/server/lib/books.ts | 6 +- packages/zenn-content-css/README.md | 9 + packages/zenn-content-css/src/_content.scss | 6 - packages/zenn-content-css/src/_prism.scss | 9 + packages/zenn-content-css/src/_shiki.scss | 29 ++ packages/zenn-content-css/src/index.scss | 2 + packages/zenn-markdown-html/.babelrc.json | 10 +- .../__tests__/basic.test.ts | 16 +- .../zenn-markdown-html/__tests__/br.test.ts | 20 +- .../custom-syntax/embed/blueprintue.test.ts | 12 +- .../custom-syntax/embed/card.test.ts | 20 +- .../custom-syntax/embed/codepen.test.ts | 12 +- .../custom-syntax/embed/codesandbox.test.ts | 12 +- .../custom-syntax/embed/common.test.ts | 32 +- .../custom-syntax/embed/docswell.test.ts | 32 +- .../custom-syntax/embed/figma.test.ts | 12 +- .../custom-syntax/embed/gist.test.ts | 16 +- .../custom-syntax/embed/github.test.ts | 18 +- .../custom-syntax/embed/jsfiddle.test.ts | 12 +- .../custom-syntax/embed/mermaid.test.ts | 18 +- .../custom-syntax/embed/slideshare.test.ts | 12 +- .../custom-syntax/embed/speakerdeck.test.ts | 22 +- .../custom-syntax/embed/stackblitz.test.ts | 12 +- .../custom-syntax/embed/tweet.test.ts | 16 +- .../custom-syntax/embed/youtube.test.ts | 12 +- .../custom-syntax/messagebox.test.ts | 20 +- .../__tests__/dollar.test.ts | 44 ++- .../__tests__/highlight.test.ts | 98 +++-- .../zenn-markdown-html/__tests__/link.test.ts | 130 +++---- .../__tests__/source-map.test.ts | 47 +-- .../zenn-markdown-html/__tests__/xss.test.ts | 55 +-- packages/zenn-markdown-html/package.json | 6 +- .../zenn-markdown-html/src/@types/prism.d.ts | 1 - packages/zenn-markdown-html/src/index.ts | 49 ++- .../src/prism-plugins/prism-diff-highlight.ts | 125 ------- packages/zenn-markdown-html/src/sanitizer.ts | 4 +- .../zenn-markdown-html/src/utils/highlight.ts | 288 +++++++++++++-- .../src/utils/md-renderer-fence.ts | 234 ++++++++++-- pnpm-lock.yaml | 345 ++++++++++++++++-- 42 files changed, 1258 insertions(+), 577 deletions(-) create mode 100644 packages/zenn-content-css/src/_shiki.scss delete mode 100644 packages/zenn-markdown-html/src/@types/prism.d.ts delete mode 100644 packages/zenn-markdown-html/src/prism-plugins/prism-diff-highlight.ts diff --git a/packages/zenn-cli/src/server/api/articles.ts b/packages/zenn-cli/src/server/api/articles.ts index 2436ca74..6144058d 100644 --- a/packages/zenn-cli/src/server/api/articles.ts +++ b/packages/zenn-cli/src/server/api/articles.ts @@ -3,8 +3,8 @@ import Express from 'express'; import { getLocalArticle, getLocalArticleMetaList } from '../lib/articles'; import { getValidSortTypes } from '../../common/helper'; -export function getArticle(req: Express.Request, res: Express.Response) { - const article = getLocalArticle(req.params.slug); +export async function getArticle(req: Express.Request, res: Express.Response) { + const article = await getLocalArticle(req.params.slug); if (!article) { res .status(404) diff --git a/packages/zenn-cli/src/server/api/books.ts b/packages/zenn-cli/src/server/api/books.ts index 0cec7803..5661b607 100644 --- a/packages/zenn-cli/src/server/api/books.ts +++ b/packages/zenn-cli/src/server/api/books.ts @@ -32,7 +32,7 @@ export function getBooks(req: Express.Request, res: Express.Response) { res.json({ books }); } -export function getChapter(req: Express.Request, res: Express.Response) { +export async function getChapter(req: Express.Request, res: Express.Response) { const bookSlug = req.params.book_slug; const book = getLocalBookMeta(bookSlug); if (!book) { @@ -42,7 +42,7 @@ export function getChapter(req: Express.Request, res: Express.Response) { return; } const chapterFilename = req.params.chapter_filename; - const chapter = getLocalChapter(book, chapterFilename); + const chapter = await getLocalChapter(book, chapterFilename); if (!chapter) { res .status(404) diff --git a/packages/zenn-cli/src/server/lib/articles.ts b/packages/zenn-cli/src/server/lib/articles.ts index a8ffad51..f84e34aa 100644 --- a/packages/zenn-cli/src/server/lib/articles.ts +++ b/packages/zenn-cli/src/server/lib/articles.ts @@ -14,11 +14,11 @@ import { ItemSortType } from '../../common/types'; import markdownToHtml from 'zenn-markdown-html'; import { parseToc } from 'zenn-markdown-html/lib/utils'; -export function getLocalArticle(slug: string): null | Article { +export async function getLocalArticle(slug: string): Promise { const data = readArticleFile(slug); if (!data) return null; const { meta, bodyMarkdown } = data; - const rawHtml = markdownToHtml(bodyMarkdown, { + const rawHtml = await markdownToHtml(bodyMarkdown, { embedOrigin: process.env.VITE_EMBED_SERVER_ORIGIN, }); const bodyHtml = completeHtml(rawHtml); diff --git a/packages/zenn-cli/src/server/lib/books.ts b/packages/zenn-cli/src/server/lib/books.ts index c90bedbe..c5109dd0 100644 --- a/packages/zenn-cli/src/server/lib/books.ts +++ b/packages/zenn-cli/src/server/lib/books.ts @@ -85,15 +85,15 @@ export function getLocalBookMetaList(sort?: ItemSortType): BookMeta[] { return books; } -export function getLocalChapter( +export async function getLocalChapter( book: BookMeta, chapterFilename: string -): null | Chapter { +): Promise { const data = readChapterFile(book, chapterFilename); if (!data) return null; const { meta, bodyMarkdown } = data; - const rawHtml = markdownToHtml(bodyMarkdown, { + const rawHtml = await markdownToHtml(bodyMarkdown, { embedOrigin: process.env.VITE_EMBED_SERVER_ORIGIN, }); const bodyHtml = completeHtml(rawHtml); diff --git a/packages/zenn-content-css/README.md b/packages/zenn-content-css/README.md index 60f1130e..cbad45b9 100644 --- a/packages/zenn-content-css/README.md +++ b/packages/zenn-content-css/README.md @@ -20,6 +20,15 @@ import 'zenn-content-css'; zncの外側の要素にはスタイルが適用されないことに注意してください。 +## シンタックスハイライトについて + +コードブロックのシンタックスハイライトは **Shiki** を使用しています。 + +- `_shiki.scss`: Shiki 用スタイル(現行) +- `_prism.scss`: Prism.js 用スタイル(非推奨・後方互換性のため残存) + +`_prism.scss` は、Prism.js で既に変換された既存記事のために残しています。すぐに削除する予定はありませんが、将来的に削除される可能性があります。 + ## 開発者向けドキュメント https://zenn-dev.github.io/zenn-docs-for-developers/guides/zenn-editor/zenn-content-css diff --git a/packages/zenn-content-css/src/_content.scss b/packages/zenn-content-css/src/_content.scss index 4e67d811..545e0f4c 100644 --- a/packages/zenn-content-css/src/_content.scss +++ b/packages/zenn-content-css/src/_content.scss @@ -208,13 +208,7 @@ border-radius: variables.$rounded-xs; word-break: normal; // iOSで折り返されるのを防ぐ word-wrap: normal; // iOSで折り返されるのを防ぐ - /* flex + codeの隣に疑似要素を配置することで横スクロール時の右端の余白を作る */ display: flex; - &:after { - content: ''; - width: 8px; - flex-shrink: 0; - } code { margin: 0; padding: 0; diff --git a/packages/zenn-content-css/src/_prism.scss b/packages/zenn-content-css/src/_prism.scss index 76416ac8..486dd78f 100644 --- a/packages/zenn-content-css/src/_prism.scss +++ b/packages/zenn-content-css/src/_prism.scss @@ -1,3 +1,12 @@ +// ============================================================ +// [非推奨] Prism.js 用スタイル +// ============================================================ +// このファイルは Prism.js で既に変換された HTML のために残しています。 +// 新規記事は Shiki でハイライトされるため、このファイルは使用されません。 +// +// すぐに削除する予定はありませんが、将来的に削除される可能性があります。 +// ============================================================ + @mixin styles { pre[class*='language-'] { position: relative; diff --git a/packages/zenn-content-css/src/_shiki.scss b/packages/zenn-content-css/src/_shiki.scss new file mode 100644 index 00000000..d8d47304 --- /dev/null +++ b/packages/zenn-content-css/src/_shiki.scss @@ -0,0 +1,29 @@ +@mixin styles { + // Shiki base styles + pre.shiki { + position: relative; + overflow-x: auto; + } + + // diff 行は背景色を全幅にするため inline-block + min-width + .shiki .line.diff { + display: inline-block; + min-width: 100%; + } + + // diff モード: 追加行(+)の背景色 + .shiki .line.diff.add { + background: rgba(0, 146, 27, 0.2); + } + + // diff モード: 削除行(-)の背景色 + .shiki .line.diff.remove { + background: rgba(218, 54, 50, 0.2); + } + + // diff プレフィックス(+, -, <, >)は選択不可にする + // ref: https://github.com/zenn-dev/zenn-editor/issues/148 + .shiki .diff-prefix { + user-select: none; + } +} diff --git a/packages/zenn-content-css/src/index.scss b/packages/zenn-content-css/src/index.scss index 5c7e9d4c..aca76451 100644 --- a/packages/zenn-content-css/src/index.scss +++ b/packages/zenn-content-css/src/index.scss @@ -2,6 +2,7 @@ @use './content' as content; @use './embed' as embed; @use './prism' as prism; +@use './shiki' as shiki; @use './message' as message; @use './footnotes' as footnotes; @@ -76,6 +77,7 @@ @include content.styles; @include embed.styles; @include prism.styles; + @include shiki.styles; @include message.styles; @include footnotes.styles; } diff --git a/packages/zenn-markdown-html/.babelrc.json b/packages/zenn-markdown-html/.babelrc.json index b6135236..a2745900 100644 --- a/packages/zenn-markdown-html/.babelrc.json +++ b/packages/zenn-markdown-html/.babelrc.json @@ -1,12 +1,4 @@ { "targets": "node 16.0", - "presets": ["@babel/preset-env", "@babel/preset-typescript"], - "plugins": [ - [ - "prismjs", - { - "languages": "all" - } - ] - ] + "presets": ["@babel/preset-env", "@babel/preset-typescript"] } diff --git a/packages/zenn-markdown-html/__tests__/basic.test.ts b/packages/zenn-markdown-html/__tests__/basic.test.ts index 95d9cf91..8948ded5 100644 --- a/packages/zenn-markdown-html/__tests__/basic.test.ts +++ b/packages/zenn-markdown-html/__tests__/basic.test.ts @@ -3,8 +3,8 @@ import markdownToHtml from '../src/index'; import { parse } from 'node-html-parser'; describe('MarkdownからHTMLへの変換テスト', () => { - test('markdownからhtmlへ変換する', () => { - const html = markdownToHtml('Hello\n## hey\n\n- first\n- second\n'); + test('markdownからhtmlへ変換する', async () => { + const html = await markdownToHtml('Hello\n## hey\n\n- first\n- second\n'); const p = parse(html).querySelector('p'); const h2 = parse(html).querySelector('h2'); const ul = parse(html).querySelector('ul'); @@ -20,13 +20,13 @@ describe('MarkdownからHTMLへの変換テスト', () => { expect(liElms[1].innerHTML).toBe('second'); }); - test('インラインコメントはhtmlに変換しない', () => { - const html = markdownToHtml(``); + test('インラインコメントはhtmlに変換しない', async () => { + const html = await markdownToHtml(``); expect(html).not.toContain('hey'); }); - test('脚注に docId を設定する', () => { - const html = markdownToHtml(`Hello[^1]World!\n\n[^1]: hey`); + test('脚注に docId を設定する', async () => { + const html = await markdownToHtml(`Hello[^1]World!\n\n[^1]: hey`); // expect(html).toContain('[1]'); expect(html).toEqual( expect.stringMatching( @@ -35,8 +35,8 @@ describe('MarkdownからHTMLへの変換テスト', () => { ); }); - test('dataスキーマの画像は除外する', () => { - const html = markdownToHtml(`![](data:image/png;base64,xxxx)`); + test('dataスキーマの画像は除外する', async () => { + const html = await markdownToHtml(`![](data:image/png;base64,xxxx)`); expect(html).toContain(''); }); }); diff --git a/packages/zenn-markdown-html/__tests__/br.test.ts b/packages/zenn-markdown-html/__tests__/br.test.ts index b9058dad..1a7769b1 100644 --- a/packages/zenn-markdown-html/__tests__/br.test.ts +++ b/packages/zenn-markdown-html/__tests__/br.test.ts @@ -2,28 +2,28 @@ import { describe, test, expect } from 'vitest'; import markdownToHtml from '../src/index'; describe('
のテスト', () => { - test('段落内の
は保持する', () => { + test('段落内の
は保持する', async () => { const patterns = ['foo
bar', 'foo
bar', 'foo
bar']; - patterns.forEach((pattern) => { - const html = markdownToHtml(pattern); + for (const pattern of patterns) { + const html = await markdownToHtml(pattern); expect(html).toContain('foo
bar'); - }); + } }); - test('テーブル内の
は保持する', () => { + test('テーブル内の
は保持する', async () => { const tableString = [ `| a | b |`, `| --- | --- |`, `| foo
bar | c |`, ].join('\n'); - const html = markdownToHtml(tableString); + const html = await markdownToHtml(tableString); expect(html).toContain('foo
bar'); }); - test('インラインコード内の
はエスケープする', () => { - const html = markdownToHtml('foo`
`bar'); + test('インラインコード内の
はエスケープする', async () => { + const html = await markdownToHtml('foo`
`bar'); expect(html).toContain('foo<br>bar'); }); - test('コードブロック内の
はエスケープする', () => { - const html = markdownToHtml('```\n
\n```'); + test('コードブロック内の
はエスケープする', async () => { + const html = await markdownToHtml('```\n
\n```'); expect(html).toContain('<br>'); }); }); diff --git a/packages/zenn-markdown-html/__tests__/custom-syntax/embed/blueprintue.test.ts b/packages/zenn-markdown-html/__tests__/custom-syntax/embed/blueprintue.test.ts index cb1f20e4..617b5761 100644 --- a/packages/zenn-markdown-html/__tests__/custom-syntax/embed/blueprintue.test.ts +++ b/packages/zenn-markdown-html/__tests__/custom-syntax/embed/blueprintue.test.ts @@ -8,8 +8,8 @@ describe('Blueprintue埋め込み要素のテスト', () => { describe('デフォルトの挙動', () => { describe('有効なURLの場合', () => { - test('' ); }); - test('ページ番号(ハッシュ形式)が指定されたDocswellのiframeを返すこと', () => { - const html = markdownToHtml( + test('ページ番号(ハッシュ形式)が指定されたDocswellのiframeを返すこと', async () => { + const html = await markdownToHtml( '@[docswell](https://www.docswell.com/slide/LK7J5V/embed#p12)' ); expect(html).toContain( '' ); }); - test('ページ番号にXSS文字列が含まれる場合はエラーメッセージを返すこと', () => { - const html = markdownToHtml( + test('ページ番号にXSS文字列が含まれる場合はエラーメッセージを返すこと', async () => { + const html = await markdownToHtml( '@[docswell](https://www.docswell.com/slide/LK7J5V/embed#p12">)' ); expect(html).toContain('DocswellのスライドURLが不正です'); @@ -28,8 +28,8 @@ describe('Docswell', () => { }); describe('DocswellのスライドURLの場合', () => { - test('Docswellのiframeを返すこと', () => { - const html = markdownToHtml( + test('Docswellのiframeを返すこと', async () => { + const html = await markdownToHtml( '@[docswell](https://www.docswell.com/s/ku-suke/LK7J5V-hello-docswell)' ); expect(html).toContain( @@ -39,8 +39,8 @@ describe('Docswell', () => { }); describe('DocswellのスライドURL(ページ番号付き)の場合', () => { - test('ページ番号(パス形式)が指定されたDocswellのiframeを返すこと', () => { - const html = markdownToHtml( + test('ページ番号(パス形式)が指定されたDocswellのiframeを返すこと', async () => { + const html = await markdownToHtml( '@[docswell](https://www.docswell.com/s/ku-suke/LK7J5V-hello-docswell/12)' ); expect(html).toContain( @@ -48,16 +48,16 @@ describe('Docswell', () => { ); }); - test('ページ番号(ハッシュ形式)が指定されたDocswellのiframeを返すこと', () => { - const html = markdownToHtml( + test('ページ番号(ハッシュ形式)が指定されたDocswellのiframeを返すこと', async () => { + const html = await markdownToHtml( '@[docswell](https://www.docswell.com/s/ku-suke/LK7J5V-hello-docswell#p18)' ); expect(html).toContain( '' ); }); - test('ページ番号にXSS文字列が含まれる場合はエラーメッセージを返すこと', () => { - const html = markdownToHtml( + test('ページ番号にXSS文字列が含まれる場合はエラーメッセージを返すこと', async () => { + const html = await markdownToHtml( '@[docswell](https://www.docswell.com/s/ku-suke/LK7J5V-hello-docswell/12">)' ); expect(html).toContain('DocswellのスライドURLが不正です'); @@ -65,8 +65,8 @@ describe('Docswell', () => { }); describe('DocswellのURLが不正な場合', () => { - test('エラーメッセージを返すこと', () => { - const html = markdownToHtml( + test('エラーメッセージを返すこと', async () => { + const html = await markdownToHtml( '@[docswell](https://www.docswell.com/invalid)' ); expect(html).toContain('DocswellのスライドURLが不正です'); diff --git a/packages/zenn-markdown-html/__tests__/custom-syntax/embed/figma.test.ts b/packages/zenn-markdown-html/__tests__/custom-syntax/embed/figma.test.ts index 6ec9a85c..c98a5fe0 100644 --- a/packages/zenn-markdown-html/__tests__/custom-syntax/embed/figma.test.ts +++ b/packages/zenn-markdown-html/__tests__/custom-syntax/embed/figma.test.ts @@ -10,8 +10,8 @@ describe('Figma埋め込み要素のテスト', () => { describe('デフォルトの挙動', () => { describe('有効なURLの場合', () => { - test(')`; - const html = markdownToHtml(content); + const html = await markdownToHtml(content); expect(html).toContain( 'ファイルまたはプロトタイプのFigma URLを指定してください' ); diff --git a/packages/zenn-markdown-html/package.json b/packages/zenn-markdown-html/package.json index 37b597f9..be55ff63 100644 --- a/packages/zenn-markdown-html/package.json +++ b/packages/zenn-markdown-html/package.json @@ -40,9 +40,7 @@ "@eslint/js": "^9.38.0", "@types/markdown-it": "^14.1.2", "@types/node": "^24.9.1", - "@types/prismjs": "^1.26.5", "@types/sanitize-html": "^2.16.0", - "babel-plugin-prismjs": "^2.1.0", "eslint": "^9.38.0", "eslint-config-prettier": "^10.1.8", "node-html-parser": "^7.0.1", @@ -61,8 +59,8 @@ "markdown-it-inline-comments": "^1.0.1", "markdown-it-link-attributes": "^4.0.1", "markdown-it-task-lists": "^2.1.1", - "prismjs": "^1.30.0", - "sanitize-html": "^2.17.0" + "sanitize-html": "^2.17.0", + "shiki": "^1.24.0" }, "gitHead": "7da0b06004cf615e42e475de47011c4670eb7318", "publishConfig": { diff --git a/packages/zenn-markdown-html/src/@types/prism.d.ts b/packages/zenn-markdown-html/src/@types/prism.d.ts deleted file mode 100644 index 195a5888..00000000 --- a/packages/zenn-markdown-html/src/@types/prism.d.ts +++ /dev/null @@ -1 +0,0 @@ -declare module 'prismjs/components/'; diff --git a/packages/zenn-markdown-html/src/index.ts b/packages/zenn-markdown-html/src/index.ts index 23979de2..29321bdf 100644 --- a/packages/zenn-markdown-html/src/index.ts +++ b/packages/zenn-markdown-html/src/index.ts @@ -4,7 +4,7 @@ import { sanitize } from './sanitizer'; import { embedGenerators } from './embed'; import { MarkdownOptions } from './types'; -// plugis +// plugins import markdownItImSize from '@steelydylan/markdown-it-imsize'; import markdownItAnchor from 'markdown-it-anchor'; import { mdBr } from './utils/md-br'; @@ -13,7 +13,11 @@ import { mdCustomBlock } from './utils/md-custom-block'; import { mdLinkAttributes } from './utils/md-link-attributes'; import { mdSourceMap } from './utils/md-source-map'; import { mdLinkifyToCard } from './utils/md-linkify-to-card'; -import { mdRendererFence } from './utils/md-renderer-fence'; +import { + mdRendererFence, + applyHighlighting, + CodeBlockInfo, +} from './utils/md-renderer-fence'; import { mdImage } from './utils/md-image'; import { containerDetailsOptions, @@ -25,7 +29,21 @@ const mdFootnote = require('markdown-it-footnote'); const mdTaskLists = require('markdown-it-task-lists'); const mdInlineComments = require('markdown-it-inline-comments'); -const markdownToHtml = (text: string, options?: MarkdownOptions): string => { +/** + * Markdown を HTML に変換する(非同期) + * + * Shiki によるシンタックスハイライトを使用。 + * 詳細なアーキテクチャについては md-renderer-fence.ts のコメントを参照。 + * + * 処理フロー: + * 1. [Phase 1] md.render() - Markdown を HTML に変換(コードブロックはプレースホルダーに) + * 2. [Phase 2 & 3] applyHighlighting() - プレースホルダーをハイライト済み HTML に置換 + * 3. sanitize() - XSS 対策のためサニタイズ + */ +const markdownToHtml = async ( + text: string, + options?: MarkdownOptions +): Promise => { if (!(text && text.length)) return ''; const markdownOptions: MarkdownOptions = { @@ -41,6 +59,10 @@ const markdownToHtml = (text: string, options?: MarkdownOptions): string => { md.linkify.set({ fuzzyLink: false }); md.linkify.set({ fuzzyEmail: false }); // refs: https://github.com/markdown-it/linkify-it + // コードブロック情報を保存する配列 + // Phase 1 で mdRendererFence によって追加され、Phase 2 でハイライト処理に使用される + const codeBlocks: CodeBlockInfo[] = []; + md.use(mdBr) .use(mdKatex) .use(mdFootnote) @@ -48,7 +70,7 @@ const markdownToHtml = (text: string, options?: MarkdownOptions): string => { .use(markdownItImSize) .use(mdLinkAttributes) .use(mdCustomBlock, markdownOptions) - .use(mdRendererFence, markdownOptions) + .use(mdRendererFence, markdownOptions, codeBlocks) .use(mdLinkifyToCard, markdownOptions) .use(mdTaskLists, { enabled: true }) .use(mdContainer, 'details', containerDetailsOptions) @@ -76,7 +98,24 @@ const markdownToHtml = (text: string, options?: MarkdownOptions): string => { // - https://github.com/zenn-dev/zenn-community/issues/356 // - https://github.com/markdown-it/markdown-it-footnote/pull/8 const docId = crypto.randomBytes(2).toString('hex'); - return sanitize(md.render(text, { docId })); + + // ============================================================ + // Phase 1: Markdown → HTML 変換(同期) + // ============================================================ + // markdown-it がコードブロックを検出すると mdRendererFence が呼ばれ、 + // コードブロック情報が codeBlocks 配列に保存され、 + // HTML にはプレースホルダー()が挿入される + const rawHtml = md.render(text, { docId }); + + // ============================================================ + // Phase 2 & 3: シンタックスハイライト適用(非同期) + // ============================================================ + // - Phase 2: 全コードブロックを Shiki で並列ハイライト + // - Phase 3: プレースホルダーをハイライト済み HTML に置換 + const highlightedHtml = await applyHighlighting(rawHtml, codeBlocks); + + // サニタイズして返す(XSS 対策) + return sanitize(highlightedHtml); }; export default markdownToHtml; diff --git a/packages/zenn-markdown-html/src/prism-plugins/prism-diff-highlight.ts b/packages/zenn-markdown-html/src/prism-plugins/prism-diff-highlight.ts deleted file mode 100644 index 4f496628..00000000 --- a/packages/zenn-markdown-html/src/prism-plugins/prism-diff-highlight.ts +++ /dev/null @@ -1,125 +0,0 @@ -import Prism, { TokenStream } from 'prismjs'; - -/** - * PrismJSのDiff構文を使用できるようにするためのプラグイン - * ソースコードの大部分は、以下のファイルより抜き出したもの - * @reference https://github.com/PrismJS/prism/blob/master/plugins/diff-highlight/prism-diff-highlight.js - * @note `babel-plugin-prismjs`によって全ての言語プラグインを読み込んでいるため`locaLanguages()`の実行はしていない - */ -export function enableDiffHighlight() { - const LANGUAGE_REGEX = /^diff-([\w-]+)/i; - const HTML_TAG = - /<\/?(?!\d)[^\s>/=$<%]+(?:\s(?:\s*[^\s>/=]+(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s'">=]+(?=[\s>]))|(?=[\s/>])))+)?\s*\/?>/gi; - - //this will match a line plus the line break while ignoring the line breaks HTML tags may contain. - const HTML_LINE = RegExp( - /(?:__|[^\r\n<])*(?:\r\n?|\n|(?:__|[^\r\n<])(?![^\r\n])(?:__)?)(?:__)?/.source.replace( - /__/g, - function () { - return HTML_TAG.source; - } - ), - 'gi' - ); - - let warningLogged = false; - - Prism.hooks.add('before-sanity-check', function (env) { - const lang = env.language; - if (LANGUAGE_REGEX.test(lang) && !env.grammar) { - env.grammar = Prism.languages[lang] = Prism.languages.diff; - } - }); - Prism.hooks.add('before-tokenize', function (env) { - if (!warningLogged && !Prism.languages.diff && !Prism.plugins.autoloader) { - warningLogged = true; - console.warn( - "Prism's Diff Highlight plugin requires the Diff language definition (prism-diff.js)." + - "Make sure the language definition is loaded or use Prism's Autoloader plugin." - ); - } - - const lang = env.language; - if (LANGUAGE_REGEX.test(lang) && !Prism.languages[lang]) { - Prism.languages[lang] = Prism.languages.diff; - } - }); - - Prism.hooks.add('wrap', function (env) { - let diffLanguage = '', - diffGrammar; - - if (env.language !== 'diff') { - const langMatch = LANGUAGE_REGEX.exec(env.language); - if (!langMatch) { - return; // not a language specific diff - } - - diffLanguage = langMatch[1]; - diffGrammar = Prism.languages[diffLanguage]; - } - - /** - * A map from the name of a block to its line prefix, same as `Prism.languages.diff.PREFIXES` - * - * @type {Object} - */ - const DIFF_PREFIXES = { - 'deleted-sign': '-', - 'deleted-arrow': '<', - 'inserted-sign': '+', - 'inserted-arrow': '>', - unchanged: ' ', - diff: '!', - }; - - const PREFIXES = Prism.languages.diff && DIFF_PREFIXES; - - type PREFIXEKeys = keyof typeof PREFIXES; - - // one of the diff tokens without any nested tokens - if (PREFIXES && env.type in PREFIXES) { - /** @type {string} */ - const content = env.content.replace(HTML_TAG, ''); // remove all HTML tags - - /** @type {string} */ - const decoded = content.replace(/</g, '<').replace(/&/g, '&'); - - // remove any one-character prefix - const code = decoded.replace(/(^|[\r\n])./g, '$1'); - - // highlight, if possible - let highlighted: string | TokenStream; - if (diffLanguage && diffGrammar) { - highlighted = Prism.highlight(code, diffGrammar, diffLanguage); - } else { - highlighted = Prism.util.encode(code); - } - - // get the HTML source of the prefix token - const prefixToken = new Prism.Token( - 'prefix', - PREFIXES[env.type as PREFIXEKeys], - [(/\w+/.exec(env.type) as string[])[0]] - ); - const prefix = Prism.Token.stringify(prefixToken, env.language); - - // add prefix - const lines = []; - let m; - HTML_LINE.lastIndex = 0; - while ((m = HTML_LINE.exec(highlighted as string))) { - lines.push(prefix + m[0]); - } - if (/(?:^|[\r\n]).$/.test(decoded)) { - // because both "+a\n+" and "+a\n" will map to "a\n" after the line prefixes are removed - lines.push(prefix); - } - env.content = lines.join(''); - - if (diffGrammar) { - env.classes.push('language-' + diffLanguage); - } - } - }); -} diff --git a/packages/zenn-markdown-html/src/sanitizer.ts b/packages/zenn-markdown-html/src/sanitizer.ts index a3ac5f18..5fa6cca9 100644 --- a/packages/zenn-markdown-html/src/sanitizer.ts +++ b/packages/zenn-markdown-html/src/sanitizer.ts @@ -82,10 +82,10 @@ const attributes = { li: ['class', 'id', 'data-line'], ol: ['class', 'start', 'data-line'], p: ['class', 'data-line'], - pre: ['class'], + pre: ['class', 'style'], s: [], section: ['class', 'data-line'], - span: ['class', 'title'], + span: ['class', 'style', 'title'], strong: [], summary: [], sup: ['class'], diff --git a/packages/zenn-markdown-html/src/utils/highlight.ts b/packages/zenn-markdown-html/src/utils/highlight.ts index d462b105..d7f355d3 100644 --- a/packages/zenn-markdown-html/src/utils/highlight.ts +++ b/packages/zenn-markdown-html/src/utils/highlight.ts @@ -1,37 +1,269 @@ -import Prism, { Grammar } from 'prismjs'; -import { md } from './markdown-it'; -import { enableDiffHighlight } from '../prism-plugins/prism-diff-highlight'; - -// diffプラグインを有効化 -enableDiffHighlight(); - -function highlightContent({ - text, - prismGrammar, - langName, - hasDiff, -}: { - text: string; - prismGrammar?: Grammar; - langName?: string; +/** + * Shiki によるシンタックスハイライト処理 + * + * このモジュールは Phase 2(applyHighlighting)から呼び出され、 + * 個々のコードブロックをハイライトする役割を持つ。 + * + * ## 特徴 + * + * - シングルトン: ハイライターインスタンスは1つだけ作成され再利用される + * - 遅延ロード: 言語定義は初回使用時にのみロードされる + * - diff サポート: ベース言語のハイライト + diff 背景色の両方を適用 + * - transformers: Shiki の transformers API で AST レベルの変換を実行 + * + * ## 関連ファイル + * + * - `md-renderer-fence.ts`: Phase 1(収集)と Phase 3(置換) + * - `index.ts`: 全体の統合 + */ + +import { + createHighlighter, + Highlighter, + bundledLanguages, + BundledLanguage, + ShikiTransformer, +} from 'shiki'; + +/** + * Shiki ハイライターのシングルトンインスタンス + * getHighlighter() で初期化され、以降は再利用される + */ +let highlighterInstance: Highlighter | null = null; + +const SHIKI_THEME = 'github-dark'; + +/** + * Shiki ハイライターを初期化する + * 最初は最低限のセットで初期化し、必要に応じて言語をロードする + */ +export async function getHighlighter(): Promise { + if (highlighterInstance) { + return highlighterInstance; + } + + // 最初は空の言語セットで初期化(高速) + highlighterInstance = await createHighlighter({ + themes: [SHIKI_THEME], + langs: [], + }); + + return highlighterInstance; +} + +/** + * 言語がサポートされているかチェックし、必要に応じてロードする + */ +async function ensureLanguageLoaded( + highlighter: Highlighter, + langName: string +): Promise { + // 既にロード済みかチェック + const loadedLangs = highlighter.getLoadedLanguages(); + if (loadedLangs.includes(langName)) { + return true; + } + + // bundledLanguages に含まれているかチェック + if (langName in bundledLanguages) { + await highlighter.loadLanguage(langName as BundledLanguage); + return true; + } + + return false; +} + +/** + * ハイライトオプション + */ +export interface HighlightOptions { + /** diff モードかどうか */ hasDiff: boolean; -}): string { - if (prismGrammar && langName) { - if (hasDiff) - return Prism.highlight(text, Prism.languages.diff, `diff-${langName}`); + /** 追加するクラス名 */ + className: string; + /** Markdown ソースの行番号(ソースマップ用) */ + line?: number; +} + +/** + * diff プレフィックスの定義 + * Prism.js の diff-highlight プラグインと互換性を持たせる + */ +const DIFF_PREFIXES = { + // 削除行 + '-': 'remove', // deleted-sign + '<': 'remove', // deleted-arrow + // 挿入行 + '+': 'add', // inserted-sign + '>': 'add', // inserted-arrow +} as const; + +/** コンテキスト行(変更なし)のプレフィックス */ +const DIFF_CONTEXT_PREFIX = ' '; + +/** + * diff 行スタイルを適用する transformer を作成 + * 行頭のプレフィックス (+, -, <, >, スペース) を検出して処理 + * - +, -, <, >: 挿入/削除のクラスを追加し、プレフィックスをラップ + * - スペース: プレフィックスのみラップ(コンテキスト行) + */ +function createDiffTransformer(): ShikiTransformer { + return { + line(node, lineNumber) { + // ソースコードの該当行を取得 + const lines = this.source.split('\n'); + const lineText = lines[lineNumber - 1] ?? ''; + const firstChar = lineText.charAt(0); - return Prism.highlight(text, prismGrammar, langName); + if (firstChar in DIFF_PREFIXES) { + // 追加/削除行 + this.addClassToHast(node, 'diff'); + this.addClassToHast( + node, + DIFF_PREFIXES[firstChar as keyof typeof DIFF_PREFIXES] + ); + wrapDiffPrefix(node, firstChar); + } else if (firstChar === DIFF_CONTEXT_PREFIX) { + // コンテキスト行(先頭スペース) + wrapDiffPrefix(node, firstChar); + } + }, + }; +} + +/** + * 行の最初の文字(diff プレフィックス)を別の span 要素にラップする + * これにより CSS で user-select: none を適用可能になる + */ +function wrapDiffPrefix( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + lineNode: any, + prefix: string +): void { + const line = lineNode; + + if (!line.children || line.children.length === 0) return; + + const firstChild = line.children[0]; + + // 最初の子要素がテキストノードの場合 + if (firstChild.type === 'text' && firstChild.value.startsWith(prefix)) { + // プレフィックスを分離 + const prefixSpan = { + type: 'element', + tagName: 'span', + properties: { class: 'diff-prefix' }, + children: [{ type: 'text', value: prefix }], + }; + firstChild.value = firstChild.value.slice(1); + + // 空になった場合は削除 + if (firstChild.value === '') { + line.children.shift(); + } + + // プレフィックス span を先頭に挿入 + line.children.unshift(prefixSpan); + return; + } + + // 最初の子要素が element(span など)の場合 + if ( + firstChild.type === 'element' && + firstChild.children && + firstChild.children.length > 0 + ) { + const innerFirst = firstChild.children[0]; + if (innerFirst.type === 'text' && innerFirst.value.startsWith(prefix)) { + // プレフィックスを分離 + const prefixSpan = { + type: 'element', + tagName: 'span', + properties: { class: 'diff-prefix' }, + children: [{ type: 'text', value: prefix }], + }; + innerFirst.value = innerFirst.value.slice(1); + + // 空になった場合は削除 + if (innerFirst.value === '') { + firstChild.children.shift(); + } + + // プレフィックス span を先頭に挿入 + line.children.unshift(prefixSpan); + } } +} - if (hasDiff) return Prism.highlight(text, Prism.languages.diff, 'diff'); - return md.utils.escapeHtml(text); +/** + * pre/code タグにクラスと属性を追加する transformer を作成 + */ +function createClassTransformer(options: { + className: string; + line?: number; +}): ShikiTransformer { + const { className, line } = options; + return { + pre(node) { + if (className) { + this.addClassToHast(node, className); + } + }, + code(node) { + this.addClassToHast(node, 'code-line'); + if (className) { + this.addClassToHast(node, className); + } + if (line !== undefined) { + node.properties['data-line'] = line; + } + }, + }; } -export function highlight( +/** + * [Phase 2 の実処理] コードをハイライトする + * + * applyHighlighting() から各コードブロックに対して呼び出される。 + * 言語が未ロードの場合は自動的にロードする(遅延ロード)。 + * Shiki の transformers API を使用して AST レベルで変換を行う。 + * + * @param text - ハイライト対象のコード文字列 + * @param langName - 言語名(例: "javascript", "python") + * @param options - ハイライトオプション + * @returns ハイライト済み HTML(常に
 構造)
+ */
+export async function highlight(
   text: string,
   langName: string,
-  hasDiff: boolean
-): string {
-  const prismGrammar = Prism.languages[langName];
-  return highlightContent({ text, prismGrammar, langName, hasDiff });
+  options: HighlightOptions
+): Promise {
+  const { hasDiff, className, line } = options;
+  const highlighter = await getHighlighter();
+
+  // 言語がサポートされているか確認し、必要に応じてロード
+  const isSupported = langName
+    ? await ensureLanguageLoaded(highlighter, langName)
+    : false;
+
+  // サポートされていない言語は text(プレーンテキスト)として処理
+  // ref: https://shiki.style/languages#plain-text
+  const lang = isSupported ? langName : 'text';
+  if (!isSupported) {
+    await ensureLanguageLoaded(highlighter, 'text');
+  }
+
+  // transformers を構築
+  const transformers: ShikiTransformer[] = [
+    createClassTransformer({ className, line }),
+  ];
+  if (hasDiff) {
+    transformers.push(createDiffTransformer());
+  }
+
+  return highlighter.codeToHtml(text, {
+    lang,
+    theme: SHIKI_THEME,
+    transformers,
+  });
 }
diff --git a/packages/zenn-markdown-html/src/utils/md-renderer-fence.ts b/packages/zenn-markdown-html/src/utils/md-renderer-fence.ts
index e0bc52bf..d5603e4f 100644
--- a/packages/zenn-markdown-html/src/utils/md-renderer-fence.ts
+++ b/packages/zenn-markdown-html/src/utils/md-renderer-fence.ts
@@ -1,32 +1,127 @@
+/**
+ * コードブロックのレンダリングとシンタックスハイライト
+ *
+ * ## 非同期処理アーキテクチャの概要
+ *
+ * Shiki(シンタックスハイライター)は非同期APIを持つが、
+ * markdown-it のレンダラーは同期的に文字列を返す必要がある。
+ * この制約を解決するため、プレースホルダー方式を採用している。
+ *
+ * ### 処理フロー(3フェーズ)
+ *
+ * ```
+ * [Phase 1: 収集] markdown-it レンダリング(同期)
+ *     ↓
+ *     コードブロックを検出するたびに:
+ *     1. コードブロック情報を配列に保存
+ *     2. プレースホルダー(HTMLコメント)を返す
+ *     ↓
+ *     出力: プレースホルダー付きHTML + コードブロック情報配列
+ *
+ * [Phase 2: ハイライト] Shiki によるハイライト(非同期・並列)
+ *     ↓
+ *     Promise.all で全コードブロックを並列処理
+ *     ↓
+ *     出力: ハイライト済みHTML配列
+ *
+ * [Phase 3: 置換] プレースホルダーを置換(同期)
+ *     ↓
+ *     プレースホルダーをハイライト済みHTMLに置換
+ *     ↓
+ *     出力: 最終HTML
+ * ```
+ *
+ * ### この方式のメリット
+ *
+ * 1. 同期/非同期の不一致を解決: markdown-it の同期的なプラグインシステムを維持
+ * 2. 並列処理: 複数のコードブロックを Promise.all で同時にハイライト
+ * 3. 遅延ロード: Shiki の言語定義を必要に応じてロード(メモリ効率)
+ *
+ * ### 関連ファイル
+ *
+ * - `index.ts`: markdownToHtml() - 3フェーズを統合
+ * - `highlight.ts`: highlight() - Shiki によるハイライト処理
+ * - `md-renderer-fence.ts`: このファイル - Phase 1 と 3 を担当
+ */
+
 import MarkdownIt from 'markdown-it';
 import { md } from './markdown-it';
 import { MarkdownOptions } from '../types';
 import { highlight } from './highlight';
 
-function getHtml({
+/**
+ * コードブロック情報を保存するインターフェース
+ * Phase 1 で収集し、Phase 2 でハイライト処理に使用
+ */
+export interface CodeBlockInfo {
+  content: string;
+  langName: string;
+  hasDiff: boolean;
+  fileName?: string;
+  line?: number;
+  placeholder: string;
+}
+
+// プレースホルダーのプレフィックス
+const PLACEHOLDER_PREFIX = '';
+
+/**
+ * ランダムな8文字の文字列を生成する
+ */
+function generateRandomId(): string {
+  return Math.random().toString(36).slice(2, 10);
+}
+
+/**
+ * プレースホルダーを生成する
+ * ユーザーが本文中に同じ文字列を書いても衝突しないようランダムIDを使用
+ */
+function createPlaceholder(): string {
+  return `${PLACEHOLDER_PREFIX}${generateRandomId()}${PLACEHOLDER_SUFFIX}`;
+}
+
+/**
+ * コードブロックの HTML を生成する
+ * Shiki の出力(
...
)を外側のコンテナでラップする + * 注:
 へのクラス・属性追加は highlight() の transformers で行う
+ */
+function wrapHighlightedCode({
+  highlightedHtml,
+  fileName,
+}: {
+  highlightedHtml: string;
+  fileName?: string;
+}): string {
+  // ファイル名コンテナを追加
+  const fileNameHtml = fileName
+    ? `
${md.utils.escapeHtml( + fileName + )}
` + : ''; + + return `
${fileNameHtml}${highlightedHtml}
`; +} + +/** + * エラー時のフォールバック HTML を生成 + * Shiki でのハイライトに失敗した場合に使用 + */ +function getPlainHtml({ content, - className, fileName, line, }: { content: string; - className: string; fileName?: string; line?: number; -}) { - const escapedClass = md.utils.escapeHtml(className); - - return `
${ - fileName - ? `
${md.utils.escapeHtml( - fileName - )}
` - : '' - }
${content}
`; +}): string { + const escapedContent = md.utils.escapeHtml(content); + const lineAttr = line !== undefined ? ` data-line="${line}"` : ''; + + const preHtml = `
${escapedContent}
`; + + return wrapHighlightedCode({ highlightedHtml: preHtml, fileName }); } function getClassName({ @@ -40,22 +135,17 @@ function getClassName({ if (!isSafe) return ''; if (hasDiff) { - return `diff-highlight ${ - langName.length ? `language-diff-${langName}` : '' - }`; + return `diff-highlight ${langName.length ? `language-diff-${langName}` : ''}`; } return langName ? `language-${langName}` : ''; } +// Shiki がネイティブサポートしていない言語のフォールバック const fallbackLanguages: { [key: string]: string; } = { - vue: 'html', react: 'jsx', - fish: 'shell', - sh: 'shell', cwl: 'yaml', - tf: 'hcl', // ref: https://github.com/PrismJS/prism/issues/1252 }; function normalizeLangName(str?: string): string { @@ -99,7 +189,26 @@ export function parseInfo(str: string): { }; } -export function mdRendererFence(md: MarkdownIt, options?: MarkdownOptions) { +/** + * [Phase 1] markdown-it にコードブロックのレンダラーを登録する + * + * markdown-it がコードブロック(```)を検出するたびに呼ばれ、 + * 以下の処理を行う: + * 1. コードブロックの情報(内容、言語、diff有無など)を codeBlocks 配列に追加 + * 2. プレースホルダー(例: )を返す + * + * このレンダラーは同期的に動作し、実際のハイライト処理は + * Phase 2(applyHighlighting)で非同期に行われる。 + * + * @param md - markdown-it インスタンス + * @param options - Markdown 変換オプション + * @param codeBlocks - コードブロック情報を格納する配列(副作用で変更される) + */ +export function mdRendererFence( + md: MarkdownIt, + options: MarkdownOptions, + codeBlocks: CodeBlockInfo[] +) { // override fence md.renderer.rules.fence = function (...args) { const [tokens, idx] = args; @@ -107,23 +216,80 @@ export function mdRendererFence(md: MarkdownIt, options?: MarkdownOptions) { const { langName, fileName, hasDiff } = parseInfo(info); if (langName === 'mermaid') { - const generator = options?.customEmbed?.mermaid; + const generator = options.customEmbed?.mermaid; // generator が(上書きされて)定義されてない場合はそのまま出力する return generator ? generator(content.trim(), options) : content; } - const className = getClassName({ - langName, - hasDiff, - }); - const highlightedContent = highlight(content, langName, hasDiff); const fenceStart = tokens[idx].map?.[0]; + const placeholder = createPlaceholder(); - return getHtml({ - content: highlightedContent, - className, + codeBlocks.push({ + content, + langName, + hasDiff, fileName, line: fenceStart, + placeholder, }); + return placeholder; }; } + +/** + * [Phase 2 & 3] プレースホルダーをハイライトされたコードに置換する + * + * Phase 2: 全コードブロックを Shiki で並列ハイライト + * - Promise.all により、複数のコードブロックを同時に処理 + * - 各コードブロックに対して highlight() を呼び出し + * - 言語が未ロードの場合は自動的にロード(遅延ロード) + * + * Phase 3: プレースホルダーを置換 + * - を実際のハイライト済み HTML に置換 + * + * @param html - プレースホルダーを含む HTML 文字列 + * @param codeBlocks - Phase 1 で収集したコードブロック情報の配列 + * @returns ハイライト済みの完全な HTML 文字列 + */ +export async function applyHighlighting( + html: string, + codeBlocks: CodeBlockInfo[] +): Promise { + // すべてのコードブロックを並列でハイライト + const highlightedBlocks = await Promise.all( + codeBlocks.map(async (block) => { + const className = getClassName({ + langName: block.langName, + hasDiff: block.hasDiff, + }); + + try { + const highlightedHtml = await highlight(block.content, block.langName, { + hasDiff: block.hasDiff, + className, + line: block.line, + }); + + return wrapHighlightedCode({ + highlightedHtml, + fileName: block.fileName, + }); + } catch { + // エラー時はプレーンテキストとして出力 + return getPlainHtml({ + content: block.content, + fileName: block.fileName, + line: block.line, + }); + } + }) + ); + + // プレースホルダーを置換 + let result = html; + for (let i = 0; i < highlightedBlocks.length; i++) { + result = result.replace(codeBlocks[i].placeholder, highlightedBlocks[i]); + } + + return result; +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1c4007c9..bb1131e2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -278,12 +278,12 @@ importers: markdown-it-task-lists: specifier: ^2.1.1 version: 2.1.1 - prismjs: - specifier: ^1.30.0 - version: 1.30.0 sanitize-html: specifier: ^2.17.0 version: 2.17.0 + shiki: + specifier: ^1.24.0 + version: 1.29.2 devDependencies: '@babel/cli': specifier: ^7.28.3 @@ -306,15 +306,9 @@ importers: '@types/node': specifier: ^24.9.1 version: 24.10.1 - '@types/prismjs': - specifier: ^1.26.5 - version: 1.26.5 '@types/sanitize-html': specifier: ^2.16.0 version: 2.16.0 - babel-plugin-prismjs: - specifier: ^2.1.0 - version: 2.1.0(prismjs@1.30.0) eslint: specifier: ^9.38.0 version: 9.39.1 @@ -2028,6 +2022,27 @@ packages: resolution: {integrity: sha512-iA7E+uXuiEydOwv8glEYM4tCHnl8C7wTgLxg+3upHhH/iSSnefWfoRqrJwVBhwxPg4MDoypVI7Oal7bX7/ne+w==} engines: {node: '>=20.0.0'} + '@shikijs/core@1.29.2': + resolution: {integrity: sha512-vju0lY9r27jJfOY4Z7+Rt/nIOjzJpZ3y+nYpqtUZInVoXQ/TJZcfGnNOGnKjFdVZb8qexiCuSlZRKcGfhhTTZQ==} + + '@shikijs/engine-javascript@1.29.2': + resolution: {integrity: sha512-iNEZv4IrLYPv64Q6k7EPpOCE/nuvGiKl7zxdq0WFuRPF5PAE9PRo2JGq/d8crLusM59BRemJ4eOqrFrC4wiQ+A==} + + '@shikijs/engine-oniguruma@1.29.2': + resolution: {integrity: sha512-7iiOx3SG8+g1MnlzZVDYiaeHe7Ez2Kf2HrJzdmGwkRisT7r4rak0e655AcM/tF9JG/kg5fMNYlLLKglbN7gBqA==} + + '@shikijs/langs@1.29.2': + resolution: {integrity: sha512-FIBA7N3LZ+223U7cJDUYd5shmciFQlYkFXlkKVaHsCPgfVLiO+e12FmQE6Tf9vuyEsFe3dIl8qGWKXgEHL9wmQ==} + + '@shikijs/themes@1.29.2': + resolution: {integrity: sha512-i9TNZlsq4uoyqSbluIcZkmPL9Bfi3djVxRnofUHwvx/h6SRW3cwgBC5SML7vsDcWyukY0eCzVN980rqP6qNl9g==} + + '@shikijs/types@1.29.2': + resolution: {integrity: sha512-VJjK0eIijTZf0QSTODEXCqinjBn0joAHQ+aPSBzrv4O2d/QSbsMw+ZeSRx03kV34Hy7NzUvV/7NqfYGRLrASmw==} + + '@shikijs/vscode-textmate@10.0.2': + resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==} + '@sigstore/bundle@4.0.0': resolution: {integrity: sha512-NwCl5Y0V6Di0NexvkTqdoVfmjTaQwoLM236r89KEojGmq/jMls8S+zb7yOwAPdXvbwfKDlP+lmXgAL4vKSQT+A==} engines: {node: ^20.17.0 || >=22.9.0} @@ -2164,6 +2179,9 @@ packages: '@types/fs-extra@11.0.4': resolution: {integrity: sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ==} + '@types/hast@3.0.4': + resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} + '@types/hoist-non-react-statics@3.3.7': resolution: {integrity: sha512-PQTyIulDkIDro8P+IHbKCsw7U2xxBYflVzW/FgWdCAePD9xGSidgA76/GeJ6lBKoblyhf9pBY763gbrN+1dI8g==} peerDependencies: @@ -2190,6 +2208,9 @@ packages: '@types/markdown-it@14.1.2': resolution: {integrity: sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==} + '@types/mdast@4.0.4': + resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} + '@types/mdurl@2.0.0': resolution: {integrity: sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==} @@ -2218,9 +2239,6 @@ packages: resolution: {integrity: sha512-EULJ8LApcVEPbrfND0cRQqutIOdiIgJ1Mgrhpy755r14xMohPTEpkV/k28SJvuOs9bHRFW8x+KeDAEPiGQPB9Q==} deprecated: This is a stub types definition. parse-path provides its own type definitions, so you do not need this installed. - '@types/prismjs@1.26.5': - resolution: {integrity: sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ==} - '@types/qs@6.14.0': resolution: {integrity: sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==} @@ -2268,6 +2286,9 @@ packages: '@types/supertest@6.0.3': resolution: {integrity: sha512-8WzXq62EXFhJ7QsH3Ocb/iKQ/Ty9ZVWnVzoTKc9tyyFRRF3a74Tk2+TLFgaFFw364Ere+npzHKEJ6ga2LzIL7w==} + '@types/unist@3.0.3': + resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + '@types/ws@8.18.1': resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} @@ -2330,6 +2351,9 @@ packages: resolution: {integrity: sha512-/++5CYLQqsO9HFGLI7APrxBJYo+5OCMpViuhV8q5/Qa3o5mMrF//eQHks+PXcsAVaLdn817fMuS7zqoXNNZGaw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@ungap/structured-clone@1.3.0': + resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} + '@vitejs/plugin-react@5.1.0': resolution: {integrity: sha512-4LuWrg7EKWgQaMJfnN+wcmbAW+VSsCmqGohftWjuct47bv8uE4n/nPpq4XjJPsxgq00GGG5J8dvBczp8uxScew==} engines: {node: ^20.19.0 || >=22.12.0} @@ -2560,11 +2584,6 @@ packages: peerDependencies: '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 - babel-plugin-prismjs@2.1.0: - resolution: {integrity: sha512-ehzSKYfeAz4U78zi/sfwsjDPlq0LvDKxNefcZTJ/iKBu+plsHsLqZhUeGf1+82LAcA35UZGbU6ksEx2Utphc/g==} - peerDependencies: - prismjs: ^1.18.0 - balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} @@ -2688,6 +2707,9 @@ packages: caniuse-lite@1.0.30001754: resolution: {integrity: sha512-x6OeBXueoAceOmotzx3PO4Zpt4rzpeIFsSr6AAePTZxSkXiYDUmpypEl7e2+8NCd9bD7bXjqyef8CJYPC1jfxg==} + ccount@2.0.1: + resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} + chai@6.2.1: resolution: {integrity: sha512-p4Z49OGG5W/WBCPSS/dH3jQ73kD6tiMmUM+bckNK6Jr5JHMG3k9bg/BvKR8lKmtVBKmOiuVaV2ws8s9oSbwysg==} engines: {node: '>=18'} @@ -2700,6 +2722,12 @@ packages: resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + character-entities-html4@2.1.0: + resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} + + character-entities-legacy@3.0.0: + resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==} + cheerio-select@2.1.0: resolution: {integrity: sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==} @@ -2773,6 +2801,9 @@ packages: resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} engines: {node: '>= 0.8'} + comma-separated-tokens@2.0.3: + resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} + commander@2.20.3: resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} @@ -2998,6 +3029,9 @@ packages: detect-node@2.1.0: resolution: {integrity: sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==} + devlop@1.1.0: + resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} + dezalgo@1.0.4: resolution: {integrity: sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==} @@ -3056,6 +3090,9 @@ packages: electron-to-chromium@1.5.250: resolution: {integrity: sha512-/5UMj9IiGDMOFBnN4i7/Ry5onJrAGSbOGo3s9FEKmwobGq6xw832ccET0CE3CkkMBZ8GJSlUIesZofpyurqDXw==} + emoji-regex-xs@1.0.0: + resolution: {integrity: sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg==} + emoji-regex@10.6.0: resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==} @@ -3523,6 +3560,12 @@ packages: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} + hast-util-to-html@9.0.5: + resolution: {integrity: sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==} + + hast-util-whitespace@3.0.0: + resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} + he@1.2.0: resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} hasBin: true @@ -3551,6 +3594,9 @@ packages: html-escaper@2.0.2: resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + html-void-elements@3.0.0: + resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} + htmlparser2@10.0.0: resolution: {integrity: sha512-TwAZM+zE5Tq3lrEHvOlvwgj1XLWQCtaaibSN11Q+gGBAS7Y1uZSWwXXRe4iF6OXnaq1riyQAPFOBtYc77Mxq0g==} @@ -3960,6 +4006,9 @@ packages: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} + mdast-util-to-hast@13.2.1: + resolution: {integrity: sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==} + mdurl@2.0.0: resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==} @@ -4000,6 +4049,21 @@ packages: resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} engines: {node: '>= 0.6'} + micromark-util-character@2.1.1: + resolution: {integrity: sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==} + + micromark-util-encode@2.0.1: + resolution: {integrity: sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==} + + micromark-util-sanitize-uri@2.0.1: + resolution: {integrity: sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==} + + micromark-util-symbol@2.0.1: + resolution: {integrity: sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==} + + micromark-util-types@2.0.2: + resolution: {integrity: sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==} + micromatch@4.0.8: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} @@ -4239,6 +4303,9 @@ packages: once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + oniguruma-to-es@2.3.0: + resolution: {integrity: sha512-bwALDxriqfKGfUufKGGepCzu9x7nJQuoRoAFp4AnwehhC2crqrDIAP/uN2qdlsAvSMpeRC3+Yzhqc7hLmle5+g==} + open@10.2.0: resolution: {integrity: sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==} engines: {node: '>=18'} @@ -4452,10 +4519,6 @@ packages: resolution: {integrity: sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==} engines: {node: '>=18'} - prismjs@1.30.0: - resolution: {integrity: sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==} - engines: {node: '>=6'} - proc-log@5.0.0: resolution: {integrity: sha512-Azwzvl90HaF0aCz1JrDdXQykFakSSNPaPoiZ9fm5qJIMHioDZEi7OAdRwSm6rSoPtY3Qutnm3L7ogmg3dc+wbQ==} engines: {node: ^18.17.0 || >=20.5.0} @@ -4485,6 +4548,9 @@ packages: resolution: {integrity: sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==} engines: {node: '>=10'} + property-information@7.1.0: + resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==} + proto-list@1.2.4: resolution: {integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==} @@ -4593,6 +4659,15 @@ packages: regenerate@1.4.2: resolution: {integrity: sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==} + regex-recursion@5.1.1: + resolution: {integrity: sha512-ae7SBCbzVNrIjgSbh7wMznPcQel1DNlDtzensnFxpiNpXt1U2ju/bHugH422r+4LAVS1FpW1YCwilmnNsjum9w==} + + regex-utilities@2.3.0: + resolution: {integrity: sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==} + + regex@5.1.1: + resolution: {integrity: sha512-dN5I359AVGPnwzJm2jN1k0W9LPZ+ePvoOeVMMfqIMFz53sSwXkxaJoxr50ptnsC771lK95BnTrVSZxq0b9yCGw==} + regexpu-core@6.4.0: resolution: {integrity: sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA==} engines: {node: '>=4'} @@ -4768,6 +4843,9 @@ packages: resolution: {integrity: sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==} engines: {node: '>= 0.4'} + shiki@1.29.2: + resolution: {integrity: sha512-njXuliz/cP+67jU2hukkxCNuH1yUi4QfdZZY+sMr5PPrIyXSu5iTb/qYC4BiWWB0vZ+7TbdvYUCeL23zpwCfbg==} + side-channel-list@1.0.0: resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} engines: {node: '>= 0.4'} @@ -4853,6 +4931,9 @@ packages: resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} engines: {node: '>=0.10.0'} + space-separated-tokens@2.0.2: + resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} + spdx-correct@3.2.0: resolution: {integrity: sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==} @@ -4915,6 +4996,9 @@ packages: string_decoder@1.3.0: resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + stringify-entities@4.0.4: + resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} + strip-ansi@6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} @@ -5077,6 +5161,9 @@ packages: resolution: {integrity: sha512-gcANaAnd2QDZFmHFEOF4k7uc1J/6a6z3DJMd/QwEyxLoKGiptJRwid582r7QIsFlFMIZ3SnxfS52S4hm2DHkuQ==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + trim-lines@3.0.1: + resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} + ts-api-utils@2.1.0: resolution: {integrity: sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==} engines: {node: '>=18.12'} @@ -5217,6 +5304,21 @@ packages: resolution: {integrity: sha512-9OdaqO5kwqR+1kVgHAhsp5vPNU0hnxRa26rBFNfNgM7M6pNtgzeBn3s/xbyCQL3dcjzOatcef6UUHpB/6MaETg==} engines: {node: ^18.17.0 || >=20.5.0} + unist-util-is@6.0.1: + resolution: {integrity: sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==} + + unist-util-position@5.0.0: + resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==} + + unist-util-stringify-position@4.0.0: + resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} + + unist-util-visit-parents@6.0.2: + resolution: {integrity: sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==} + + unist-util-visit@5.0.0: + resolution: {integrity: sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==} + universal-user-agent@7.0.3: resolution: {integrity: sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A==} @@ -5275,6 +5377,12 @@ packages: resolution: {integrity: sha512-Ck0EJbAGxHwprkzFO966t4/5QkRuzh+/I1RxhLgUKKwEn+Cd8NwM60mE3AqBZg5gYODoXW0EFsQvbZjRlvdqbg==} engines: {node: '>=4'} + vfile-message@4.0.3: + resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==} + + vfile@6.0.3: + resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} + vite@7.2.2: resolution: {integrity: sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==} engines: {node: ^20.19.0 || >=22.12.0} @@ -5582,6 +5690,9 @@ packages: zeptomatch@2.1.0: resolution: {integrity: sha512-KiGErG2J0G82LSpniV0CtIzjlJ10E04j02VOudJsPyPwNZgGnRKQy7I1R7GMyg/QswnE4l7ohSGrQbQbjXPPDA==} + zwitch@2.0.4: + resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} + snapshots: '@azu/format-text@1.0.2': {} @@ -7391,6 +7502,41 @@ snapshots: '@secretlint/types@11.2.5': {} + '@shikijs/core@1.29.2': + dependencies: + '@shikijs/engine-javascript': 1.29.2 + '@shikijs/engine-oniguruma': 1.29.2 + '@shikijs/types': 1.29.2 + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + hast-util-to-html: 9.0.5 + + '@shikijs/engine-javascript@1.29.2': + dependencies: + '@shikijs/types': 1.29.2 + '@shikijs/vscode-textmate': 10.0.2 + oniguruma-to-es: 2.3.0 + + '@shikijs/engine-oniguruma@1.29.2': + dependencies: + '@shikijs/types': 1.29.2 + '@shikijs/vscode-textmate': 10.0.2 + + '@shikijs/langs@1.29.2': + dependencies: + '@shikijs/types': 1.29.2 + + '@shikijs/themes@1.29.2': + dependencies: + '@shikijs/types': 1.29.2 + + '@shikijs/types@1.29.2': + dependencies: + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + + '@shikijs/vscode-textmate@10.0.2': {} + '@sigstore/bundle@4.0.0': dependencies: '@sigstore/protobuf-specs': 0.5.0 @@ -7581,6 +7727,10 @@ snapshots: '@types/jsonfile': 6.1.1 '@types/node': 24.10.1 + '@types/hast@3.0.4': + dependencies: + '@types/unist': 3.0.3 + '@types/hoist-non-react-statics@3.3.7(@types/react@19.2.3)': dependencies: '@types/react': 19.2.3 @@ -7607,6 +7757,10 @@ snapshots: '@types/linkify-it': 5.0.0 '@types/mdurl': 2.0.0 + '@types/mdast@4.0.4': + dependencies: + '@types/unist': 3.0.3 + '@types/mdurl@2.0.0': {} '@types/methods@1.1.4': {} @@ -7636,8 +7790,6 @@ snapshots: dependencies: parse-path: 7.1.0 - '@types/prismjs@1.26.5': {} - '@types/qs@6.14.0': {} '@types/range-parser@1.2.7': {} @@ -7699,6 +7851,8 @@ snapshots: '@types/methods': 1.1.4 '@types/superagent': 8.1.9 + '@types/unist@3.0.3': {} + '@types/ws@8.18.1': dependencies: '@types/node': 24.10.1 @@ -7796,6 +7950,8 @@ snapshots: '@typescript-eslint/types': 8.46.4 eslint-visitor-keys: 4.2.1 + '@ungap/structured-clone@1.3.0': {} + '@vitejs/plugin-react@5.1.0(vite@7.2.2(@types/node@24.10.1)(sass@1.94.0)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))': dependencies: '@babel/core': 7.28.5 @@ -8082,10 +8238,6 @@ snapshots: transitivePeerDependencies: - supports-color - babel-plugin-prismjs@2.1.0(prismjs@1.30.0): - dependencies: - prismjs: 1.30.0 - balanced-match@1.0.2: {} baseline-browser-mapping@2.8.24: {} @@ -8253,6 +8405,8 @@ snapshots: caniuse-lite@1.0.30001754: optional: true + ccount@2.0.1: {} + chai@6.2.1: {} chalk@4.1.2: @@ -8262,6 +8416,10 @@ snapshots: chalk@5.6.2: {} + character-entities-html4@2.1.0: {} + + character-entities-legacy@3.0.0: {} + cheerio-select@2.1.0: dependencies: boolbase: 1.0.0 @@ -8343,6 +8501,8 @@ snapshots: dependencies: delayed-stream: 1.0.0 + comma-separated-tokens@2.0.3: {} + commander@2.20.3: optional: true @@ -8537,6 +8697,10 @@ snapshots: detect-node@2.1.0: {} + devlop@1.1.0: + dependencies: + dequal: 2.0.3 + dezalgo@1.0.4: dependencies: asap: 2.0.6 @@ -8601,6 +8765,8 @@ snapshots: electron-to-chromium@1.5.250: optional: true + emoji-regex-xs@1.0.0: {} + emoji-regex@10.6.0: {} emoji-regex@8.0.0: {} @@ -9205,6 +9371,24 @@ snapshots: dependencies: function-bind: 1.1.2 + hast-util-to-html@9.0.5: + dependencies: + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + ccount: 2.0.1 + comma-separated-tokens: 2.0.3 + hast-util-whitespace: 3.0.0 + html-void-elements: 3.0.0 + mdast-util-to-hast: 13.2.1 + property-information: 7.1.0 + space-separated-tokens: 2.0.2 + stringify-entities: 4.0.4 + zwitch: 2.0.4 + + hast-util-whitespace@3.0.0: + dependencies: + '@types/hast': 3.0.4 + he@1.2.0: {} history@5.3.0: @@ -9236,6 +9420,8 @@ snapshots: html-escaper@2.0.2: {} + html-void-elements@3.0.0: {} + htmlparser2@10.0.0: dependencies: domelementtype: 2.3.0 @@ -9651,6 +9837,18 @@ snapshots: math-intrinsics@1.1.0: {} + mdast-util-to-hast@13.2.1: + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@ungap/structured-clone': 1.3.0 + devlop: 1.1.0 + micromark-util-sanitize-uri: 2.0.1 + trim-lines: 3.0.1 + unist-util-position: 5.0.0 + unist-util-visit: 5.0.0 + vfile: 6.0.3 + mdurl@2.0.0: {} media-typer@0.3.0: {} @@ -9681,6 +9879,23 @@ snapshots: methods@1.1.2: {} + micromark-util-character@2.1.1: + dependencies: + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-encode@2.0.1: {} + + micromark-util-sanitize-uri@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-encode: 2.0.1 + micromark-util-symbol: 2.0.1 + + micromark-util-symbol@2.0.1: {} + + micromark-util-types@2.0.2: {} + micromatch@4.0.8: dependencies: braces: 3.0.3 @@ -9929,6 +10144,12 @@ snapshots: dependencies: wrappy: 1.0.2 + oniguruma-to-es@2.3.0: + dependencies: + emoji-regex-xs: 1.0.0 + regex: 5.1.1 + regex-recursion: 5.1.1 + open@10.2.0: dependencies: default-browser: 5.2.1 @@ -10136,8 +10357,6 @@ snapshots: dependencies: parse-ms: 4.0.0 - prismjs@1.30.0: {} - proc-log@5.0.0: {} proc-log@6.0.0: {} @@ -10157,6 +10376,8 @@ snapshots: err-code: 2.0.3 retry: 0.12.0 + property-information@7.1.0: {} + proto-list@1.2.4: {} protocols@2.0.2: {} @@ -10272,6 +10493,17 @@ snapshots: regenerate@1.4.2: {} + regex-recursion@5.1.1: + dependencies: + regex: 5.1.1 + regex-utilities: 2.3.0 + + regex-utilities@2.3.0: {} + + regex@5.1.1: + dependencies: + regex-utilities: 2.3.0 + regexpu-core@6.4.0: dependencies: regenerate: 1.4.2 @@ -10518,6 +10750,17 @@ snapshots: shell-quote@1.8.3: {} + shiki@1.29.2: + dependencies: + '@shikijs/core': 1.29.2 + '@shikijs/engine-javascript': 1.29.2 + '@shikijs/engine-oniguruma': 1.29.2 + '@shikijs/langs': 1.29.2 + '@shikijs/themes': 1.29.2 + '@shikijs/types': 1.29.2 + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + side-channel-list@1.0.0: dependencies: es-errors: 1.3.0 @@ -10622,6 +10865,8 @@ snapshots: source-map@0.6.1: {} + space-separated-tokens@2.0.2: {} + spdx-correct@3.2.0: dependencies: spdx-expression-parse: 3.0.1 @@ -10701,6 +10946,11 @@ snapshots: dependencies: safe-buffer: 5.2.1 + stringify-entities@4.0.4: + dependencies: + character-entities-html4: 2.1.0 + character-entities-legacy: 3.0.0 + strip-ansi@6.0.1: dependencies: ansi-regex: 5.0.1 @@ -10867,6 +11117,8 @@ snapshots: treeverse@3.0.0: {} + trim-lines@3.0.1: {} + ts-api-utils@2.1.0(typescript@5.9.3): dependencies: typescript: 5.9.3 @@ -10986,6 +11238,29 @@ snapshots: dependencies: imurmurhash: 0.1.4 + unist-util-is@6.0.1: + dependencies: + '@types/unist': 3.0.3 + + unist-util-position@5.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-stringify-position@4.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-visit-parents@6.0.2: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + + unist-util-visit@5.0.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + unist-util-visit-parents: 6.0.2 + universal-user-agent@7.0.3: {} universalify@2.0.0: {} @@ -11036,6 +11311,16 @@ snapshots: version-range@4.15.0: {} + vfile-message@4.0.3: + dependencies: + '@types/unist': 3.0.3 + unist-util-stringify-position: 4.0.0 + + vfile@6.0.3: + dependencies: + '@types/unist': 3.0.3 + vfile-message: 4.0.3 + vite@7.2.2(@types/node@24.10.1)(sass@1.94.0)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1): dependencies: esbuild: 0.25.12 @@ -11368,3 +11653,5 @@ snapshots: dependencies: grammex: 3.1.11 graphmatch: 1.1.0 + + zwitch@2.0.4: {}