 b6442c233d
			
		
	
	b6442c233d
	
	
	
		
			
			web: Fix issues surrounding markdown rendering. - Fix issue where Mermaid diagrams do not render. - Fix link colors in dark mode. - Fix anchored links triggering router. - Fix issue where links occasionally link to missing page.
		
			
				
	
	
		
			300 lines
		
	
	
		
			8.7 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			300 lines
		
	
	
		
			8.7 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| /**
 | |
|  * @import {Options as HighlightOptions} from 'rehype-highlight'
 | |
|  * @import {CompileOptions} from '@mdx-js/mdx'
 | |
|  * @import {mdxmermaid} from 'mdx-mermaid'
 | |
|  * @import {Message,
 | |
|       OnLoadArgs,
 | |
|       OnLoadResult,
 | |
|       Plugin,
 | |
|       PluginBuild
 | |
|  * } from 'esbuild'
 | |
|  */
 | |
| import { run as runMDX } from "@mdx-js/mdx";
 | |
| import { createFormatAwareProcessors } from "@mdx-js/mdx/internal-create-format-aware-processors";
 | |
| import { extnamesToRegex } from "@mdx-js/mdx/internal-extnames-to-regex";
 | |
| import apacheGrammar from "highlight.js/lib/languages/apache";
 | |
| import diffGrammar from "highlight.js/lib/languages/diff";
 | |
| import confGrammar from "highlight.js/lib/languages/ini";
 | |
| import nginxGrammar from "highlight.js/lib/languages/nginx";
 | |
| import { common } from "lowlight";
 | |
| import mdxMermaid from "mdx-mermaid";
 | |
| import { Mermaid } from "mdx-mermaid/lib/Mermaid";
 | |
| import assert from "node:assert";
 | |
| import fs from "node:fs/promises";
 | |
| import path from "node:path";
 | |
| import React from "react";
 | |
| import { renderToStaticMarkup } from "react-dom/server";
 | |
| import * as runtime from "react/jsx-runtime";
 | |
| import rehypeHighlight from "rehype-highlight";
 | |
| import remarkDirective from "remark-directive";
 | |
| import remarkFrontmatter from "remark-frontmatter";
 | |
| import remarkGFM from "remark-gfm";
 | |
| import remarkMdxFrontmatter from "remark-mdx-frontmatter";
 | |
| import remarkParse from "remark-parse";
 | |
| import { SourceMapGenerator } from "source-map";
 | |
| import { VFile } from "vfile";
 | |
| import { VFileMessage } from "vfile-message";
 | |
| 
 | |
| import { remarkAdmonition } from "./remark/remark-admonition.mjs";
 | |
| import { remarkHeadings } from "./remark/remark-headings.mjs";
 | |
| import { remarkLinks } from "./remark/remark-links.mjs";
 | |
| import { remarkLists } from "./remark/remark-lists.mjs";
 | |
| 
 | |
| /**
 | |
|  * @typedef {Omit<OnLoadArgs, 'pluginData'> & LoadDataFields} LoadData
 | |
|  *   Data passed to `onload`.
 | |
|  *
 | |
|  * @typedef LoadDataFields
 | |
|  *   Extra fields given in `data` to `onload`.
 | |
|  * @property {PluginData | null | undefined} [pluginData]
 | |
|  *   Plugin data.
 | |
|  *
 | |
|  * @typedef {CompileOptions} Options
 | |
|  *   Configuration.
 | |
|  *
 | |
|  *   Options are the same as `compile` from `@mdx-js/mdx`.
 | |
|  *
 | |
|  * @typedef PluginData
 | |
|  *   Extra data passed.
 | |
|  * @property {Buffer | string | null | undefined} [contents]
 | |
|  *   File contents.
 | |
|  *
 | |
|  * @typedef State
 | |
|  *   Info passed around.
 | |
|  * @property {string} doc
 | |
|  *   File value.
 | |
|  * @property {string} name
 | |
|  *   Plugin name.
 | |
|  * @property {string} path
 | |
|  *   File path.
 | |
|  */
 | |
| 
 | |
| const eol = /\r\n|\r|\n|\u2028|\u2029/g;
 | |
| 
 | |
| const name = "@mdx-js/esbuild";
 | |
| 
 | |
| /**
 | |
|  * Compile MDX to HTML.
 | |
|  * *
 | |
|  * @param {Readonly<Options> | null | undefined} [mdxOptions]
 | |
|  *   Configuration (optional).
 | |
|  * @return {Plugin}
 | |
|  *   Plugin.
 | |
|  */
 | |
| export function mdxPlugin(mdxOptions) {
 | |
|     /** @type {mdxmermaid.Config} */
 | |
|     const mermaidConfig = {
 | |
|         output: "svg",
 | |
|     };
 | |
| 
 | |
|     /**
 | |
|      * @type {HighlightOptions}
 | |
|      */
 | |
|     const highlightThemeOptions = {
 | |
|         languages: {
 | |
|             ...common,
 | |
|             nginx: nginxGrammar,
 | |
|             apache: apacheGrammar,
 | |
|             conf: confGrammar,
 | |
|             diff: diffGrammar,
 | |
|         },
 | |
|     };
 | |
| 
 | |
|     const { extnames, process } = createFormatAwareProcessors({
 | |
|         ...mdxOptions,
 | |
|         SourceMapGenerator,
 | |
|         outputFormat: "function-body",
 | |
| 
 | |
|         remarkPlugins: [
 | |
|             remarkParse,
 | |
|             remarkDirective,
 | |
|             remarkAdmonition,
 | |
|             remarkGFM,
 | |
|             remarkFrontmatter,
 | |
|             remarkMdxFrontmatter,
 | |
|             remarkHeadings,
 | |
|             remarkLinks,
 | |
|             remarkLists,
 | |
|             [mdxMermaid, mermaidConfig],
 | |
|         ],
 | |
|         rehypePlugins: [[rehypeHighlight, highlightThemeOptions]],
 | |
|     });
 | |
| 
 | |
|     return { name, setup };
 | |
| 
 | |
|     /**
 | |
|      * @param {PluginBuild} build
 | |
|      *   Build.
 | |
|      * @returns {undefined}
 | |
|      *   Nothing.
 | |
|      */
 | |
|     function setup(build) {
 | |
|         build.onLoad({ filter: extnamesToRegex(extnames) }, onload);
 | |
| 
 | |
|         /**
 | |
|          * @param {LoadData} data
 | |
|          *   Data.
 | |
|          * @returns {Promise<OnLoadResult>}
 | |
|          *   Result.
 | |
|          */
 | |
|         async function onload(data) {
 | |
|             const document = String(
 | |
|                 data.pluginData &&
 | |
|                     data.pluginData.contents !== null &&
 | |
|                     data.pluginData.contents !== undefined
 | |
|                     ? data.pluginData.contents
 | |
|                     : await fs.readFile(data.path),
 | |
|             );
 | |
| 
 | |
|             /** @type {State} */
 | |
|             const state = {
 | |
|                 doc: document,
 | |
|                 name,
 | |
|                 path: data.path,
 | |
|             };
 | |
| 
 | |
|             let file = new VFile({
 | |
|                 path: data.path,
 | |
|                 value: document,
 | |
|             });
 | |
| 
 | |
|             /** @type {string | undefined} */
 | |
|             let value;
 | |
| 
 | |
|             /** @type {Array<VFileMessage>} */
 | |
|             let messages = [];
 | |
| 
 | |
|             /** @type {Array<Message>} */
 | |
|             const errors = [];
 | |
| 
 | |
|             /** @type {Array<Message>} */
 | |
|             const warnings = [];
 | |
| 
 | |
|             /**
 | |
|              * @type {React.ComponentType<{children: React.ReactNode, frontmatter: Record<string, string>}>}
 | |
|              */
 | |
|             const wrapper = ({ children, frontmatter }) => {
 | |
|                 const title = frontmatter.title;
 | |
|                 const nextChildren = React.Children.toArray(children);
 | |
| 
 | |
|                 if (title) {
 | |
|                     nextChildren.unshift(React.createElement("h1", { key: "title" }, title));
 | |
|                 }
 | |
| 
 | |
|                 return React.createElement(React.Fragment, null, nextChildren);
 | |
|             };
 | |
| 
 | |
|             try {
 | |
|                 file = await process(file);
 | |
|                 const { default: Content, ...mdxExports } = await runMDX(file, {
 | |
|                     ...runtime,
 | |
|                     useMDXComponents: () => {
 | |
|                         return {
 | |
|                             mermaid: Mermaid,
 | |
|                             Mermaid,
 | |
|                         };
 | |
|                     },
 | |
|                     baseUrl: import.meta.url,
 | |
|                 });
 | |
| 
 | |
|                 const { frontmatter = {} } = mdxExports;
 | |
|                 const result = renderToStaticMarkup(
 | |
|                     Content({
 | |
|                         frontmatter,
 | |
|                         components: {
 | |
|                             wrapper,
 | |
|                         },
 | |
|                     }),
 | |
|                 );
 | |
| 
 | |
|                 value = result;
 | |
| 
 | |
|                 messages = file.messages;
 | |
|             } catch (error_) {
 | |
|                 const cause = /** @type {VFileMessage | Error} */ (error_);
 | |
| 
 | |
|                 console.error(cause);
 | |
| 
 | |
|                 const message =
 | |
|                     "reason" in cause
 | |
|                         ? cause
 | |
|                         : new VFileMessage("Cannot process MDX file with esbuild", {
 | |
|                               cause,
 | |
|                               ruleId: "process-error",
 | |
|                               source: "@mdx-js/esbuild",
 | |
|                           });
 | |
| 
 | |
|                 message.fatal = true;
 | |
|                 messages.push(message);
 | |
|             }
 | |
| 
 | |
|             for (const message of messages) {
 | |
|                 const list = message.fatal ? errors : warnings;
 | |
|                 list.push(vfileMessageToEsbuild(state, message));
 | |
|             }
 | |
| 
 | |
|             // Safety check: the file has a path, so there has to be a `dirname`.
 | |
|             assert(file.dirname, "expected `dirname` to be defined");
 | |
| 
 | |
|             return {
 | |
|                 contents: value || "",
 | |
|                 loader: "text",
 | |
|                 errors,
 | |
|                 resolveDir: path.resolve(file.cwd, file.dirname),
 | |
|                 warnings,
 | |
|             };
 | |
|         }
 | |
|     }
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * @param {Readonly<State>} state
 | |
|  *   Info passed around.
 | |
|  * @param {Readonly<VFileMessage>} message
 | |
|  *   VFile message or error.
 | |
|  * @returns {Message}
 | |
|  *   ESBuild message.
 | |
|  */
 | |
| function vfileMessageToEsbuild(state, message) {
 | |
|     const place = message.place;
 | |
|     const start = place ? ("start" in place ? place.start : place) : undefined;
 | |
|     const end = place && "end" in place ? place.end : undefined;
 | |
|     let length = 0;
 | |
|     let lineStart = 0;
 | |
|     let line = 0;
 | |
|     let column = 0;
 | |
| 
 | |
|     if (start && start.offset !== undefined) {
 | |
|         line = start.line;
 | |
|         column = start.column - 1;
 | |
|         lineStart = start.offset - column;
 | |
|         length = 1;
 | |
| 
 | |
|         if (end && end.offset !== undefined) {
 | |
|             length = end.offset - start.offset;
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     eol.lastIndex = lineStart;
 | |
| 
 | |
|     const match = eol.exec(state.doc);
 | |
|     const lineEnd = match ? match.index : state.doc.length;
 | |
| 
 | |
|     return {
 | |
|         detail: message,
 | |
|         id: "",
 | |
|         location: {
 | |
|             column,
 | |
|             file: state.path,
 | |
|             length: Math.min(length, lineEnd),
 | |
|             line,
 | |
|             lineText: state.doc.slice(lineStart, lineEnd),
 | |
|             namespace: "file",
 | |
|             suggestion: "",
 | |
|         },
 | |
|         notes: [],
 | |
|         pluginName: state.name,
 | |
|         text: message.reason,
 | |
|     };
 | |
| }
 |