From ec030134e13b34f759a7122b9c4be618a0023336 Mon Sep 17 00:00:00 2001 From: Tavin Cole Date: Fri, 26 May 2023 20:37:45 +0200 Subject: [PATCH] wip --- packages/markdown-it-htmyst/package.json | 46 ++++++++++++ packages/markdown-it-htmyst/src/admonition.ts | 75 +++++++++++++++++++ packages/markdown-it-htmyst/src/index.ts | 57 ++++++++++++++ packages/markdown-it-htmyst/src/proof.ts | 65 ++++++++++++++++ packages/markdown-it-htmyst/src/util.ts | 36 +++++++++ packages/markdown-it-htmyst/tsconfig.json | 34 +++++++++ packages/markdown-it-myst/package.json | 3 +- 7 files changed, 315 insertions(+), 1 deletion(-) create mode 100644 packages/markdown-it-htmyst/package.json create mode 100644 packages/markdown-it-htmyst/src/admonition.ts create mode 100644 packages/markdown-it-htmyst/src/index.ts create mode 100644 packages/markdown-it-htmyst/src/proof.ts create mode 100644 packages/markdown-it-htmyst/src/util.ts create mode 100644 packages/markdown-it-htmyst/tsconfig.json diff --git a/packages/markdown-it-htmyst/package.json b/packages/markdown-it-htmyst/package.json new file mode 100644 index 000000000..fcb9e7a3b --- /dev/null +++ b/packages/markdown-it-htmyst/package.json @@ -0,0 +1,46 @@ +{ + "name": "markdown-it-htmyst", + "version": "0.1.3", + "sideEffects": false, + "license": "MIT", + "description": "markdown-it HTML hinter for MyST roles and directives", + "author": "Tavin Cole <7685034+tavin@users.noreply.github.com>", + "homepage": "https://github.com/executablebooks/mystjs/tree/main/packages/markdown-it-htmyst", + "main": "./dist/cjs/index.js", + "module": "./dist/esm/index.js", + "types": "./dist/types/index.d.ts", + "files": [ + "dist" + ], + "exports": { + ".": { + "import": "./dist/esm/index.js", + "require": "./dist/cjs/index.js" + } + }, + "publishConfig": { + "access": "public" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/executablebooks/mystjs.git" + }, + "scripts": { + "clean": "rimraf dist", + "build:esm": "tsc --project ./tsconfig.json --module es2015 --outDir dist/esm", + "build:cjs": "tsc --project ./tsconfig.json --module commonjs --outDir dist/cjs", + "declarations": "tsc --project ./tsconfig.json --declaration --emitDeclarationOnly --declarationMap --outDir dist/types", + "bundle": "esbuild dist/cjs/index.js --bundle --format=cjs --outfile=dist/markdown-it-htmyst.js", + "build": "npm-run-all -l clean -p build:cjs build:esm declarations -s bundle", + "lint": "eslint \"src/**/!(*.spec).ts\" -c ./.eslintrc.js", + "lint:format": "npx prettier --check \"src/**/*.ts\"", + "test": "jest", + "test:watch": "jest --watchAll" + }, + "bugs": { + "url": "https://github.com/executablebooks/mystjs/issues" + }, + "dependencies": { + "markdown-it": "^13.0.1" + } +} diff --git a/packages/markdown-it-htmyst/src/admonition.ts b/packages/markdown-it-htmyst/src/admonition.ts new file mode 100644 index 000000000..f91402e58 --- /dev/null +++ b/packages/markdown-it-htmyst/src/admonition.ts @@ -0,0 +1,75 @@ +import type StateCore from 'markdown-it/lib/rules_core/state_core'; +import type Token from 'markdown-it/lib/token'; + +import { + findTokenPair, + ARG_OPEN, + ARG_CLOSE, + BODY_OPEN, + BODY_CLOSE, +} from './util'; + +const ADMONITIONS = [ + 'admonition', + 'attention', + 'caution', + 'danger', + 'error', + 'hint', + 'important', + 'note', + 'seealso', + 'tip', + 'warning', +]; + +/** + * Enrich admonition directives with markup tags. + * + * @return true if new tokens are inserted into the array + */ +export function admonitionDecorator(state: StateCore, tokens: Token[]): boolean { + + let kind = tokens[0].info; + + if (!ADMONITIONS.includes(kind)) return false; + + let ins = false; + let [...arg] = findTokenPair(tokens, [ARG_OPEN, ARG_CLOSE], 1); + + if (arg[1] === undefined) { + const title = new state.Token('inline', '', 0); + title.content = kind.replace(/^./, char => char.toUpperCase()); + title.children = []; + tokens.splice(1, 0, new state.Token(ARG_OPEN, 'p', 1), + title, new state.Token(ARG_CLOSE, 'p', -1)); + arg = [1, 3]; + ins = true; + } + + let [...body] = findTokenPair(tokens, [BODY_OPEN, BODY_CLOSE], 1); + + tokens.at(0).attrSet('class', `admonition ${kind}`); + tokens.at(arg[0]).attrSet('class', 'admonition-title'); + + [0, -1].map(tokens.at, tokens).forEach(token => { + token.hidden = false; + token.block = true; + token.tag = 'aside'; + }); + + arg.map(tokens.at, tokens).forEach(token => { + token.hidden = false; + token.block = true; + token.tag = 'p'; + }); + + body.map(tokens.at, tokens).forEach(token => { + token.hidden = false; + token.block = true; + token.tag = 'div'; + }); + + return ins; +} + diff --git a/packages/markdown-it-htmyst/src/index.ts b/packages/markdown-it-htmyst/src/index.ts new file mode 100644 index 000000000..656fa30fb --- /dev/null +++ b/packages/markdown-it-htmyst/src/index.ts @@ -0,0 +1,57 @@ +import type MarkdownIt from 'markdown-it/lib'; +import type StateCore from 'markdown-it/lib/rules_core/state_core'; +import type Token from 'markdown-it/lib/token'; + +import { admonitionDecorator } from './admonition'; +import { proofDecorator } from './proof'; + +type Decorator = (state: StateCore, tokens: Token[]) => boolean | void; + +const DEFAULT_DECORATORS = [ + admonitionDecorator, + proofDecorator, +]; + +/** + * Factory to implement the following rule: + * + * - Run through the parsed token array. + * - Find all slices from `parsed_directive_open` to `parsed_directive_close`. + * - Pass the slices through the decorators. + */ +function rule(decorators: Decorator[] = DEFAULT_DECORATORS) { + + return (state: StateCore) => { + const tokens = state.tokens; + const stack = []; + for (let j = 0; j < tokens.length; ++j) { + if (tokens[j].type === 'parsed_directive_open') { + stack.push(j); + } else if (tokens[j].type === 'parsed_directive_close') { + let i = stack.pop()!; + // in principle we'd like a view of the array from i to j; + // hopefully all modern JS engines like V8 will optimize slice() as COW + const slice = tokens.slice(i, j + 1); + let subst = false; + for (let fn of decorators) { + subst = fn(state, slice) || subst; + } + if (subst) { + tokens.splice(i, j - i + 1, ...slice); + j = i + slice.length - 1; + } + } + } + }; +} + +/** + * A markdown-it plugin for adding markup to parsed myst directives. + */ +export function htmystPlugin(md: MarkdownIt, ...decorators: Decorator[]) { + md.core.ruler.after('run_directives', 'decorate_directives', + decorators.length ? rule(decorators) : rule()); +} + +export default htmystPlugin; + diff --git a/packages/markdown-it-htmyst/src/proof.ts b/packages/markdown-it-htmyst/src/proof.ts new file mode 100644 index 000000000..a56b6fd0c --- /dev/null +++ b/packages/markdown-it-htmyst/src/proof.ts @@ -0,0 +1,65 @@ +import type StateCore from 'markdown-it/lib/rules_core/state_core'; +import type Token from 'markdown-it/lib/token'; + +import { + findTokenPair, + ARG_OPEN, + ARG_CLOSE, + BODY_OPEN, + BODY_CLOSE, +} from './util'; + +/** + * Enrich proof directives with markup tags. + * + * @return true if new tokens are inserted into the array + */ +export function proofDecorator(state: StateCore, tokens: Token[]): boolean { + + let kind = tokens[0].info; + + if (!kind.startsWith('prf:')) return false; + + const title = new state.Token('inline', '', 0); + title.content = kind.replace(/^prf:(.)/, (_, char) => char.toUpperCase()); + title.children = []; + + let [...arg] = findTokenPair(tokens, [ARG_OPEN, ARG_CLOSE], 1); + + if (arg[1] === undefined) { + tokens.splice(1, 0, new state.Token(ARG_OPEN, 'p', 1), + title, new state.Token(ARG_CLOSE, 'p', -1)); + arg = [1, 3]; + } else { + title.content += ': '; + tokens.splice(arg[0] + 1, 0, title); + ++arg[1]; + } + + let [...body] = findTokenPair(tokens, [BODY_OPEN, BODY_CLOSE], 1); + + tokens.at(0).attrSet('class', 'admonition'); + tokens.at(0).attrJoin('class', kind.replace(':', '-')); + tokens.at(arg[0]).attrSet('class', 'admonition-title'); + + [0, -1].map(tokens.at, tokens).forEach(token => { + token.hidden = false; + token.block = true; + token.tag = 'aside'; + }); + + arg.map(tokens.at, tokens).forEach(token => { + token.hidden = false; + token.block = true; + token.tag = 'p'; + }); + + body.map(tokens.at, tokens).forEach(token => { + token.hidden = false; + token.block = true; + token.tag = 'div'; + }); + + return true; +} + diff --git a/packages/markdown-it-htmyst/src/util.ts b/packages/markdown-it-htmyst/src/util.ts new file mode 100644 index 000000000..6aea286c4 --- /dev/null +++ b/packages/markdown-it-htmyst/src/util.ts @@ -0,0 +1,36 @@ +import type Token from 'markdown-it/lib/token'; + +export const ARG_OPEN = 'directive_arg_open'; +export const ARG_CLOSE = 'directive_arg_close'; +export const BODY_OPEN = 'directive_body_open'; +export const BODY_CLOSE = 'directive_body_close'; + +/** + * Find open/close pairs in a token array. + */ +export function* findTokenPair( + tokens: Token[], + pair: [string, string], + from: number = 0 +): Generator { + + let pos = from - 1; + let depth = 0; + + while (++pos < tokens.length) { + depth += tokens[pos].nesting; + if (depth == 1 && tokens[pos].type == pair[0]) { + yield pos; + break; + } + } + + while (++pos < tokens.length) { + depth += tokens[pos].nesting; + if (depth == 0 && tokens[pos].type == pair[1]) { + yield pos; + break; + } + } +} + diff --git a/packages/markdown-it-htmyst/tsconfig.json b/packages/markdown-it-htmyst/tsconfig.json new file mode 100644 index 000000000..553c246db --- /dev/null +++ b/packages/markdown-it-htmyst/tsconfig.json @@ -0,0 +1,34 @@ +{ + "compilerOptions": { + "target": "es6", + // module is overridden from the build:esm/build:cjs scripts + "module": "es2015", + "jsx": "react-jsx", + "lib": ["es2020"], + "esModuleInterop": true, + "noImplicitAny": true, + "strict": true, + "strictNullChecks": false, + "moduleResolution": "node", + "sourceMap": false, + // outDir is overridden from the build:esm/build:cjs scripts + "outDir": "dist/types", + "baseUrl": "src", + "paths": { + "*": ["node_modules/*"] + }, + // Type roots allows it to be included in a workspace + "typeRoots": [ + "./types", + "./node_modules/@types", + "../../node_modules/@types", + "../../../node_modules/@types" + ], + "resolveJsonModule": true, + // Ignore node_modules, etc. + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["src/**/*"], + "exclude": ["tests/**/*"] +} diff --git a/packages/markdown-it-myst/package.json b/packages/markdown-it-myst/package.json index 1d064406b..e1df7bcde 100644 --- a/packages/markdown-it-myst/package.json +++ b/packages/markdown-it-myst/package.json @@ -30,7 +30,8 @@ "build:esm": "tsc --project ./tsconfig.json --module es2015 --outDir dist/esm", "build:cjs": "tsc --project ./tsconfig.json --module commonjs --outDir dist/cjs", "declarations": "tsc --project ./tsconfig.json --declaration --emitDeclarationOnly --declarationMap --outDir dist/types", - "build": "npm-run-all -l clean -p build:cjs build:esm declarations", + "bundle": "esbuild dist/cjs/index.js --bundle --format=cjs --outfile=dist/markdown-it-myst.js", + "build": "npm-run-all -l clean -p build:cjs build:esm declarations -s bundle", "lint": "eslint \"src/**/!(*.spec).ts\" -c ./.eslintrc.js", "lint:format": "npx prettier --check \"src/**/*.ts\"", "test": "jest",