diff --git a/.husky/.gitignore b/.husky/.gitignore deleted file mode 100644 index 31354ec13..000000000 --- a/.husky/.gitignore +++ /dev/null @@ -1 +0,0 @@ -_ diff --git a/.husky/pre-commit b/.husky/pre-commit deleted file mode 100755 index 36af21989..000000000 --- a/.husky/pre-commit +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/sh -. "$(dirname "$0")/_/husky.sh" - -npx lint-staged diff --git a/docs/404.server.mdx b/docs/404.mdx similarity index 80% rename from docs/404.server.mdx rename to docs/404.mdx index df0860264..9810d665b 100644 --- a/docs/404.server.mdx +++ b/docs/404.mdx @@ -1,5 +1,5 @@ -import {Note} from './_component/note.server.js' -export {Home as default} from './_component/home.server.js' +import {Note} from './_component/note.jsx' +export {Home as default} from './_component/home.jsx' export const navExclude = true # 404: Not found diff --git a/docs/_asset/editor.jsx b/docs/_asset/editor.jsx new file mode 100644 index 000000000..960de7f10 --- /dev/null +++ b/docs/_asset/editor.jsx @@ -0,0 +1,497 @@ +/* @jsxRuntime automatic @jsxImportSource react */ + +/** + * @typedef {import('@wooorm/starry-night').Grammar} Grammar + * @typedef {import('mdx/types.js').MDXModule} MDXModule + * @typedef {import('react-error-boundary').FallbackProps} FallbackProps + * @typedef {import('unified').PluggableList} PluggableList + * @typedef {import('unist').Node} UnistNode + */ + +/** + * @typedef EvalOk + * @property {true} ok + * @property {JSX.Element} value + * + * @typedef EvalNok + * @property {false} ok + * @property {Error} value + * + * @typedef {EvalOk | EvalNok} EvalResult + */ + +import {compile, nodeTypes, run} from '@mdx-js/mdx' +import {createStarryNight} from '@wooorm/starry-night' +import sourceCss from '@wooorm/starry-night/source.css' +import sourceJs from '@wooorm/starry-night/source.js' +import sourceJson from '@wooorm/starry-night/source.json' +import sourceMdx from '@wooorm/starry-night/source.mdx' +import sourceTs from '@wooorm/starry-night/source.ts' +import sourceTsx from '@wooorm/starry-night/source.tsx' +import textHtmlBasic from '@wooorm/starry-night/text.html.basic' +import textMd from '@wooorm/starry-night/text.md' +import {visit as visitEstree} from 'estree-util-visit' +import {toJsxRuntime} from 'hast-util-to-jsx-runtime' +import {useEffect, useState} from 'react' +// @ts-expect-error: untyped. +import {Fragment, jsx, jsxs} from 'react/jsx-runtime' +import ReactDom from 'react-dom/client' +import {ErrorBoundary} from 'react-error-boundary' +import rehypeRaw from 'rehype-raw' +import remarkDirective from 'remark-directive' +import remarkFrontmatter from 'remark-frontmatter' +import remarkGfm from 'remark-gfm' +import remarkMath from 'remark-math' +import {removePosition} from 'unist-util-remove-position' +import {visit} from 'unist-util-visit' +import {VFile} from 'vfile' + +const runtime = {Fragment, jsx, jsxs} + +const sample = `# Hello, world! + +Below is an example of markdown in JSX. + +
+ Try and change the background color to \`tomato\`. +
` + +/** @type {ReadonlyArray} */ +const grammars = [ + sourceCss, + sourceJs, + // @ts-expect-error: TS is wrong: this is not a JSON file. + sourceJson, + sourceMdx, + sourceTs, + sourceTsx, + textHtmlBasic, + textMd +] + +/** @type {Awaited>} */ +let starryNight + +const body = document.querySelector('.body') + +if (body && window.location.pathname === '/playground/') { + const root = document.createElement('div') + root.classList.add('playground') + body.after(root) + init(root) +} + +/** + * @param {Element} main + */ +function init(main) { + const root = ReactDom.createRoot(main) + + createStarryNight(grammars).then( + /** + * @returns {undefined} + */ + function (x) { + starryNight = x + + const missing = starryNight.missingScopes() + if (missing.length > 0) { + throw new Error('Unexpected missing required scopes: `' + missing + '`') + } + + root.render() + } + ) +} + +function Playground() { + const [value, setValue] = useState(sample) + const [directive, setDirective] = useState(false) + const [frontmatter, setFrontmatter] = useState(false) + const [gfm, setGfm] = useState(false) + const [markdown, setMarkdown] = useState(false) + const [math, setMath] = useState(false) + const [raw, setRaw] = useState(false) + const [positions, setPositions] = useState(false) + const [output, setOutput] = useState('result') + const [evalResult, setEvalResult] = useState( + /** @type {unknown} */ (undefined) + ) + + useEffect( + function () { + go().then( + function (ok) { + setEvalResult({ok: true, value: ok}) + }, + /** + * @param {Error} error + */ + function (error) { + setEvalResult({ok: false, value: error}) + } + ) + + async function go() { + /** @type {PluggableList} */ + const recmaPlugins = [] + /** @type {PluggableList} */ + const rehypePlugins = [] + /** @type {PluggableList} */ + const remarkPlugins = [] + + if (directive) remarkPlugins.unshift(remarkDirective) + if (frontmatter) remarkPlugins.unshift(remarkFrontmatter) + if (gfm) remarkPlugins.unshift(remarkGfm) + if (math) remarkPlugins.unshift(remarkMath) + if (raw) rehypePlugins.unshift([rehypeRaw, {passThrough: nodeTypes}]) + + const file = new VFile({ + basename: markdown ? 'example.md' : 'example.mdx', + value + }) + + if (output === 'mdast') remarkPlugins.push([capture, {name: 'mdast'}]) + if (output === 'hast') rehypePlugins.push([capture, {name: 'hast'}]) + if (output === 'esast') recmaPlugins.push([capture, {name: 'esast'}]) + /** @type {UnistNode | undefined} */ + let tree + + await compile(file, { + outputFormat: output === 'result' ? 'function-body' : 'program', + recmaPlugins, + rehypePlugins, + remarkPlugins, + useDynamicImport: true + }) + + if (output === 'result') { + /** @type {MDXModule} */ + const mod = await run(String(file), runtime) + + return ( + +
{mod.default({})}
+
+ ) + } + + if (tree) { + return ( +
+              
+                {toJsxRuntime(
+                  starryNight.highlight(
+                    JSON.stringify(tree, undefined, 2),
+                    'source.json'
+                  ),
+                  runtime
+                )}
+              
+            
+ ) + } + + // `output === 'code'` + return ( +
+            
+              {toJsxRuntime(
+                starryNight.highlight(String(file), 'source.js'),
+                runtime
+              )}
+            
+          
+ ) + + /** + * @param {{name: string}} options + */ + function capture(options) { + /** + * @param {UnistNode} node + */ + return function (node) { + const clone = structuredClone(node) + + if (!positions) { + if (options.name === 'esast') { + cleanEstree(clone) + } else { + cleanUnistTree(clone) + } + } + + tree = clone + } + } + } + }, + [directive, frontmatter, gfm, markdown, math, output, positions, raw, value] + ) + + const scope = markdown ? 'text.md' : 'source.mdx' + const compiledResult = /** @type {EvalResult | undefined} */ (evalResult) + /** @type {JSX.Element | undefined} */ + let display + + if (compiledResult) { + if (compiledResult.ok) { + display = compiledResult.value + } else { + display = ( +
+

Could not compile code:

+ +
+ ) + } + } + + return ( + <> +
+
+
+
+ {toJsxRuntime(starryNight.highlight(value, scope), runtime)} + {/* Trailing whitespace in a `textarea` is shown, but not in a `div` + with `white-space: pre-wrap`. + Add a `br` to make the last newline explicit. */} + {/\n[ \t]*$/.test(value) ?
: undefined} +
+