diff --git a/fixtures/directives.math.md b/fixtures/directives.math.md new file mode 100644 index 000000000..270e507f7 --- /dev/null +++ b/fixtures/directives.math.md @@ -0,0 +1,22 @@ +Math directive: +. +```{math} +:label: my_label +w_{t+1} = (1 + r_{t+1}) s(w_t) + y_{t+1} +``` +. +
+w_{t+1} = (1 + r_{t+1}) s(w_t) + y_{t+1} +
+. + +Math directive: +. +```{math} +w_{t+1} = (1 + r_{t+1}) s(w_t) + y_{t+1} +``` +. +
+w_{t+1} = (1 + r_{t+1}) s(w_t) + y_{t+1} +
+. diff --git a/index.html b/index.html index 8765a9f91..3f272a7c2 100644 --- a/index.html +++ b/index.html @@ -75,8 +75,10 @@ // Setup Mathjax MathJax = { tex: { - inlineMath: [['$', '$'], ['\\(', '\\)']], - displayMath: [['$$', '$$'],['\\[', '\\]']], + inlineMath: [['\\(', '\\)']], + displayMath: [['\\[', '\\]']], + processEnvironments: false, + processRefs: false, }, svg: { fontCache: 'global' diff --git a/src/blocks.ts b/src/blocks.ts index 21ae51d49..85ac7d0f9 100644 --- a/src/blocks.ts +++ b/src/blocks.ts @@ -53,7 +53,7 @@ function checkBlockBreak(state: StateBlock, startLine: number, str: string, sile } catch (error) { console.warn('Could not parse metadata for block break: ', metadataString); } - token.attrSet('metadata', JSON.stringify(metadata)); + token.meta = { ...token.meta, metadata }; token.map = [startLine, state.line]; return true; } @@ -90,7 +90,7 @@ const renderComment: Renderer.RenderRule = (tokens, idx, opts, env: StateEnv) => }; const renderBlockBreak: Renderer.RenderRule = (tokens, idx, opts, env: StateEnv) => { - const metadata = tokens[idx].attrGet('metadata') ?? ''; + const { metadata } = tokens[idx].meta; console.log('Not sure what to do with metadata for block break:', metadata); return ( '\n' diff --git a/src/directives/admonition.ts b/src/directives/admonition.ts index b33896df4..7b1a5d42a 100644 --- a/src/directives/admonition.ts +++ b/src/directives/admonition.ts @@ -1,4 +1,5 @@ import { Directive } from './types'; +import { unusedOptionsWarning } from './utils'; const admonitionTitles = { attention: 'Attention', caution: 'Caution', danger: 'Danger', error: 'Error', important: 'Important', hint: 'Hint', note: 'Note', seealso: 'See Also', tip: 'Tip', warning: 'Warning', @@ -26,9 +27,7 @@ const createAdmonition = (kind: AdmonitionTypes): Directive => { }, getOptions: (data) => { const { class: overrideClass, ...rest } = data; - if (Object.keys(rest).length > 0) { - console.warn('Unknown admonition options'); - } + unusedOptionsWarning(kind, rest); return { class: overrideClass as AdmonitionTypes }; }, renderer: (args, opts) => { diff --git a/src/directives/default.ts b/src/directives/default.ts index 81c7cb7dd..623dd61e3 100644 --- a/src/directives/default.ts +++ b/src/directives/default.ts @@ -1,10 +1,12 @@ import admonitions from './admonition'; import figure from './figure'; +import math from './math'; import { Directives } from './types'; const directives: Directives = { ...admonitions, ...figure, + ...math, }; export default directives; diff --git a/src/directives/figure.ts b/src/directives/figure.ts index 67b2d58c7..dc0f40928 100644 --- a/src/directives/figure.ts +++ b/src/directives/figure.ts @@ -1,11 +1,13 @@ import { TargetKind } from '../state'; import { Directive } from './types'; +import { unusedOptionsWarning } from './utils'; export type Args = { src: string; }; export type Opts = { + name: string; }; const figure = { @@ -16,7 +18,11 @@ const figure = { const args = { src: info.trim() }; return { args, content: '' }; }, - getOptions: (data) => data, + getOptions: (data) => { + const { name, ...rest } = data; + unusedOptionsWarning('figure', rest); + return { name }; + }, renderer: (args, opts, target) => { const { src } = args; const { id, number } = target ?? {}; diff --git a/src/directives/index.ts b/src/directives/index.ts index 06c5ee4de..8206bce82 100644 --- a/src/directives/index.ts +++ b/src/directives/index.ts @@ -1,11 +1,12 @@ /* eslint-disable no-param-reassign */ import MarkdownIt from 'markdown-it'; +import Renderer from 'markdown-it/lib/renderer'; import container from 'markdown-it-container'; import Token from 'markdown-it/lib/token'; import { RuleCore } from 'markdown-it/lib/parser_core'; import parseOptions from './options'; -import { Directive, Directives } from './types'; -import { newTarget, TargetKind } from '../state'; +import { Directive, Directives, DirectiveTokens } from './types'; +import { newTarget, TargetKind, StateEnv } from '../state'; import { toHTML } from '../utils'; const DIRECTIVE_PATTERN = /^\{([a-z]*)\}\s*(.*)$/; @@ -17,6 +18,14 @@ function getDirective(directives: Directives, kind: string | null) { return directives[kind]; } +/** + * Container that continues to render internally. + * + * For not rendering the internals (e.g. math), use `skipParsing` + * and the directive will modify a `fence` renderer. + * + * @param directives The directives to use + */ const directiveContainer = (directives: Directives): ContainerOpts => ({ marker: '`', validate(params) { @@ -24,7 +33,7 @@ const directiveContainer = (directives: Directives): ContainerOpts => ({ if (!match) return false; const kind = match[1]; const directive = getDirective(directives, kind); - return Boolean(directive); + return Boolean(directive) && !directive?.skipParsing; }, render(tokens, idx, options, env, self) { const token = tokens[idx]; @@ -37,20 +46,48 @@ const directiveContainer = (directives: Directives): ContainerOpts => ({ }, }); +/** + * This overrides the `fence` when `skipParsing` is set to true on a directive. + * + * @param directives The directives to use + */ +const fenceRenderer = (directives: Directives) => ( + tokens: Token[], idx: number, options: MarkdownIt.Options, env: StateEnv, self: Renderer, +) => { + const token = tokens[idx]; + const kind = token.attrGet('kind') ?? ''; + const directive = getDirective(directives, kind) as Directive; + const { args, opts, target } = token.meta; + const htmlTemplate = directive.renderer(args, opts, target, tokens, idx, options, env, self); + const [before, after] = toHTML(htmlTemplate); + return `${before}${token.content}${after}`; +}; + const setDirectiveKind = (directives: Directives): RuleCore => (state) => { const { tokens } = state; let kind: false | string = false; for (let index = 0; index < tokens.length; index += 1) { const token = tokens[index]; - if (token.type === 'container_directives_open') { + if (token.type === DirectiveTokens.open) { const match = token.info.trim().match(DIRECTIVE_PATTERN); const directive = getDirective(directives, match?.[1] ?? ''); if (!directive) throw new Error('Shoud not be able to get into here without having directive.'); kind = directive.token; token.attrSet('kind', kind); } - if (token.type === 'container_directives_close') { + if (token.type === 'fence') { + // Here we match the directives that `skipParsing`, and turn them into `directive_fences` + // The options are then added as normal, the rendering is done in `fenceRenderer` + const match = token.info.trim().match(DIRECTIVE_PATTERN); + const directive = getDirective(directives, match?.[1] ?? ''); + if (directive && directive.skipParsing) { + token.type = DirectiveTokens.fence; + kind = directive.token; + token.attrSet('kind', kind); + } + } + if (token.type === DirectiveTokens.close) { // Set the kind on the closing container as well, as that will have to render the closing tags token.attrSet('kind', kind as string); kind = false; @@ -67,7 +104,7 @@ const parseArguments = (directives: Directives): RuleCore => (state) => { let bumpArguments = ''; for (let index = 0; index < tokens.length; index += 1) { const token = tokens[index]; - if (token.type === 'container_directives_open') { + if (token.type === DirectiveTokens.open) { parent = token; const match = token.info.trim().match(DIRECTIVE_PATTERN); const directive = getDirective(directives, token.attrGet('kind')); @@ -77,7 +114,16 @@ const parseArguments = (directives: Directives): RuleCore => (state) => { token.meta = { ...token.meta, args }; if (modified) bumpArguments = modified; } - if (parent && token.type === 'container_directives_close') { + if (token.type === DirectiveTokens.fence) { + const match = token.info.trim().match(DIRECTIVE_PATTERN); + const directive = getDirective(directives, token.attrGet('kind')); + if (!match || !directive) throw new Error('Shoud not be able to get into here without matching?'); + const info = match[2].trim(); + const { args, content: modified } = directive.getArguments?.(info) ?? {}; + token.meta = { ...token.meta, args }; + if (modified) token.content = modified + token.content; + } + if (parent && token.type === DirectiveTokens.close) { // TODO: https://github.com/executablebooks/MyST-Parser/issues/154 // If the bumped title needs to be rendered - put it here somehow. bumpArguments = ''; @@ -96,11 +142,20 @@ const numbering = (directives: Directives): RuleCore => (state) => { const { tokens } = state; for (let index = 0; index < tokens.length; index += 1) { const token = tokens[index]; - if (token.type === 'container_directives_open') { + if (token.type === DirectiveTokens.open) { const directive = getDirective(directives, token.attrGet('kind')); if (directive?.numbered) { - const { name } = token.meta?.opts; - const target = newTarget(state, name, directive.numbered); + const { name, label } = token.meta?.opts; + const target = newTarget(state, name || label, directive.numbered); + token.meta.target = target; + } + } + if (token.type === DirectiveTokens.fence) { + const directive = getDirective(directives, token.attrGet('kind')); + const { name, label } = token.meta?.opts; + // Only number things if the directive supports numbering AND a name or label is provided + if (directive?.numbered && (name || label)) { + const target = newTarget(state, name || label, directive.numbered); token.meta.target = target; } } @@ -116,9 +171,11 @@ const numbering = (directives: Directives): RuleCore => (state) => { export const directivesPlugin = (directives: Directives) => (md: MarkdownIt) => { + const { renderer } = md; md.use(container, 'directives', directiveContainer(directives)); md.core.ruler.after('block', 'directive_kind', setDirectiveKind(directives)); md.core.ruler.after('directive_kind', 'parse_directive_opts', parseOptions(directives)); md.core.ruler.after('parse_directive_opts', 'parse_directive_args', parseArguments(directives)); md.core.ruler.after('parse_directive_args', 'numbering', numbering(directives)); + renderer.rules[DirectiveTokens.fence] = fenceRenderer(directives); }; diff --git a/src/directives/math.ts b/src/directives/math.ts new file mode 100644 index 000000000..fdad3fdb2 --- /dev/null +++ b/src/directives/math.ts @@ -0,0 +1,34 @@ +import { TargetKind } from '../state'; +import { Directive } from './types'; +import { unusedOptionsWarning } from './utils'; + +export type Args = { +}; + +export type Opts = { + label: string; +}; + +const math = { + math: { + token: 'math', + numbered: TargetKind.equation, + skipParsing: true, + getArguments: () => ({ args: {}, content: '' }), + getOptions: (data) => { + const { label, ...rest } = data; + unusedOptionsWarning('math', rest); + return { label }; + }, + renderer: (args, opts, target) => { + const { id, number } = target ?? {}; + return ['div', { + class: target ? ['math', 'numbered'] : 'math', + id, + number, + }, 0]; + }, + } as Directive, +}; + +export default math; diff --git a/src/directives/options.ts b/src/directives/options.ts index 34c56ad59..70e6197ce 100644 --- a/src/directives/options.ts +++ b/src/directives/options.ts @@ -1,6 +1,6 @@ import Token from 'markdown-it/lib/token'; import { RuleCore } from 'markdown-it/lib/parser_core'; -import { Directive, Directives } from './types'; +import { Directive, Directives, DirectiveTokens } from './types'; const QUICK_PARAMETERS = /^:([a-zA-Z0-9\-_]+):(.*)$/; @@ -29,26 +29,32 @@ function stripYaml(content: string) { } function addDirectiveOptions( - directive: Directive, parent: Token, tokens: Token[], index: number, + directive: Directive, parent: Token, tokens: Token[], index: number, isFence = false, ) { const [open, token, close] = tokens.slice(index - 1, index + 2); - const { content } = token; + const useToken = isFence ? parent : token; + const { content } = useToken; const firstLine = content.split('\n')[0].trim(); const isYaml = firstLine === '---'; const isQuickParams = QUICK_PARAMETERS.test(firstLine); - if (!isYaml && !isQuickParams) return; + if (!isYaml && !isQuickParams) { + const opts = directive.getOptions({}); + // eslint-disable-next-line no-param-reassign + parent.meta = { ...parent.meta, opts }; + return; + } const strip = isYaml ? stripYaml : stripParams; - const { data, modified } = strip(token.content); + const { data, modified } = strip(useToken.content); const opts = directive.getOptions(data); // eslint-disable-next-line no-param-reassign parent.meta = { ...parent.meta, opts }; - token.content = modified; + useToken.content = modified; // Here we will stop the tags from rendering if there is no content that is not metadata // This stops empty paragraph tags from rendering. const noContent = modified.length === 0; - if (open && noContent) open.hidden = true; - token.hidden = noContent; - if (close && noContent) close.hidden = true; + if (!isFence && open && noContent) open.hidden = true; + useToken.hidden = noContent; + if (!isFence && close && noContent) close.hidden = true; } const parseOptions = (directives: Directives): RuleCore => (state) => { @@ -58,12 +64,12 @@ const parseOptions = (directives: Directives): RuleCore => (state) => { let gotOptions = false; for (let index = 0; index < tokens.length; index += 1) { const token = tokens[index]; - if (token.type === 'container_directives_open') { + if (token.type === DirectiveTokens.open) { directive = directives[token.attrGet('kind') ?? '']; parent = token; gotOptions = false; } - if (token.type === 'container_directives_close') { + if (token.type === DirectiveTokens.close) { if (parent) { // Ensure there is metadata always defined for containers const meta = { opts: {}, ...parent.meta }; @@ -72,6 +78,11 @@ const parseOptions = (directives: Directives): RuleCore => (state) => { } parent = false; } + if (token.type === DirectiveTokens.fence) { + addDirectiveOptions( + directives[token.attrGet('kind') ?? ''] as Directive, token, tokens, index, true, + ); + } if (parent && !gotOptions && token.type === 'inline') { addDirectiveOptions(directive as Directive, parent, tokens, index); gotOptions = true; diff --git a/src/directives/types.ts b/src/directives/types.ts index 65f2e8e42..2034b51a1 100644 --- a/src/directives/types.ts +++ b/src/directives/types.ts @@ -4,9 +4,17 @@ import Renderer from 'markdown-it/lib/renderer'; import { HTMLOutputSpecArray } from '../utils'; import { StateEnv, TargetKind, Target } from '../state'; +export enum DirectiveTokens { + open = 'container_directives_open', + close = 'container_directives_close', + fence = 'fence_directive', + inline = 'inline', +} + export type Directive = { token: string; numbered?: TargetKind; + skipParsing?: true; getArguments: (info: string) => { args: Args; content?: string }; getOptions: (data: Record) => Opts; renderer: ( diff --git a/src/directives/utils.ts b/src/directives/utils.ts new file mode 100644 index 000000000..69aa3864a --- /dev/null +++ b/src/directives/utils.ts @@ -0,0 +1,6 @@ + +export const unusedOptionsWarning = (kind: string, opts: Record) => { + if (Object.keys(opts).length > 0) { + console.warn(`Unknown ${kind} options`, opts); + } +}; diff --git a/src/roles/index.ts b/src/roles/index.ts index dea064826..b1285de0a 100644 --- a/src/roles/index.ts +++ b/src/roles/index.ts @@ -20,11 +20,11 @@ const getRoleAttrs = (roles: Roles) => (name: string, content: string) => { const addRenderers = (roles: Roles) => (md: MarkdownIt) => { const { renderer } = md; - Object.entries(roles).forEach(([, { token, renderer: tokenRendered }]) => { + Object.entries(roles).forEach(([, { token, renderer: tokenRenderer }]) => { // Early return if the role is already defined // e.g. math_inline might be better handled by another plugin if (md.renderer.rules[token]) return; - renderer.rules[token] = tokenRendered; + renderer.rules[token] = tokenRenderer; }); }; diff --git a/tests/fixtures.spec.ts b/tests/fixtures.spec.ts index 910974081..586cd7519 100644 --- a/tests/fixtures.spec.ts +++ b/tests/fixtures.spec.ts @@ -14,6 +14,9 @@ describe('Math', () => { getFixtures('math').forEach(([name, md, html]) => { it(name, () => expect(tokenizer.render(md)).toEqual(`${html}\n`)); }); + getFixtures('directives.math').forEach(([name, md, html]) => { + it(name, () => expect(tokenizer.render(md)).toEqual(`${html}\n`)); + }); }); describe('Roles', () => {