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', () => {