Skip to content

Commit

Permalink
✨ NEW: Fence directives (#15)
Browse files Browse the repository at this point in the history
Now supports not parsing the internals of a directive, which is helpful for things like math directives.
Supported by the skipParsing option on the directive.

See #11 for the math directive changes!
  • Loading branch information
rowanc1 authored Nov 22, 2020
1 parent 7f66be4 commit 32be5d7
Show file tree
Hide file tree
Showing 13 changed files with 181 additions and 31 deletions.
22 changes: 22 additions & 0 deletions fixtures/directives.math.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
Math directive:
.
```{math}
:label: my_label
w_{t+1} = (1 + r_{t+1}) s(w_t) + y_{t+1}
```
.
<div class="math numbered" id="eq-my_label" number="1">
w_{t+1} = (1 + r_{t+1}) s(w_t) + y_{t+1}
</div>
.

Math directive:
.
```{math}
w_{t+1} = (1 + r_{t+1}) s(w_t) + y_{t+1}
```
.
<div class="math">
w_{t+1} = (1 + r_{t+1}) s(w_t) + y_{t+1}
</div>
.
6 changes: 4 additions & 2 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,10 @@
// Setup Mathjax
MathJax = {
tex: {
inlineMath: [['$', '$'], ['\\(', '\\)']],
displayMath: [['$$', '$$'],['\\[', '\\]']],
inlineMath: [['\\(', '\\)']],
displayMath: [['\\[', '\\]']],
processEnvironments: false,
processRefs: false,
},
svg: {
fontCache: 'global'
Expand Down
4 changes: 2 additions & 2 deletions src/blocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -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 (
'<!-- Block Break -->\n'
Expand Down
5 changes: 2 additions & 3 deletions src/directives/admonition.ts
Original file line number Diff line number Diff line change
@@ -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',
Expand Down Expand Up @@ -26,9 +27,7 @@ const createAdmonition = (kind: AdmonitionTypes): Directive<Args, Opts> => {
},
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) => {
Expand Down
2 changes: 2 additions & 0 deletions src/directives/default.ts
Original file line number Diff line number Diff line change
@@ -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;
8 changes: 7 additions & 1 deletion src/directives/figure.ts
Original file line number Diff line number Diff line change
@@ -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 = {
Expand All @@ -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 ?? {};
Expand Down
77 changes: 67 additions & 10 deletions src/directives/index.ts
Original file line number Diff line number Diff line change
@@ -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*(.*)$/;
Expand All @@ -17,14 +18,22 @@ 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) {
const match = params.trim().match(DIRECTIVE_PATTERN);
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];
Expand All @@ -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;
Expand All @@ -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'));
Expand All @@ -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 = '';
Expand All @@ -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;
}
}
Expand All @@ -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);
};
34 changes: 34 additions & 0 deletions src/directives/math.ts
Original file line number Diff line number Diff line change
@@ -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<Args, Opts>,
};

export default math;
33 changes: 22 additions & 11 deletions src/directives/options.ts
Original file line number Diff line number Diff line change
@@ -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\-_]+):(.*)$/;

Expand Down Expand Up @@ -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) => {
Expand All @@ -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 };
Expand All @@ -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;
Expand Down
8 changes: 8 additions & 0 deletions src/directives/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Args extends {} = {}, Opts extends {} = {}> = {
token: string;
numbered?: TargetKind;
skipParsing?: true;
getArguments: (info: string) => { args: Args; content?: string };
getOptions: (data: Record<string, string>) => Opts;
renderer: (
Expand Down
6 changes: 6 additions & 0 deletions src/directives/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@

export const unusedOptionsWarning = (kind: string, opts: Record<string, any>) => {
if (Object.keys(opts).length > 0) {
console.warn(`Unknown ${kind} options`, opts);
}
};
Loading

0 comments on commit 32be5d7

Please sign in to comment.