@diplodoc/transform
Version:
A simple transformer of text in YFM (Yandex Flavored Markdown) to HTML
146 lines (114 loc) • 5.04 kB
text/typescript
import {bold} from 'chalk';
import StateCore from 'markdown-it/lib/rules_core/state_core';
import Token from 'markdown-it/lib/token';
import {MarkdownItPluginCb} from '../typings';
import {MatchTokenFunction, nestedCloseTokenIdxFactory as closeTokenFactory} from '../utils';
import {TITLES} from './constants';
const ALERT_RE = /^{% note (alert|info|tip|warning)\s*(?:"(.*?)")? %}$/;
const WRONG_NOTES = /^{% note (.*)%}/;
function getTitle(type: string, originLang: keyof typeof TITLES) {
let lang = originLang;
if (!lang || !Object.keys(TITLES).includes(lang)) {
lang = 'ru';
}
return TITLES[lang][type];
}
const matchCloseToken: MatchTokenFunction = (tokens, i) => {
return (
tokens[i].type === 'paragraph_open' &&
tokens[i + 1].type === 'inline' &&
tokens[i + 1].content.trim() === '{% endnote %}'
);
};
function matchOpenToken(tokens: Token[], i: number) {
return (
tokens[i].type === 'paragraph_open' &&
tokens[i + 1].type === 'inline' &&
tokens[i + 1].content.match(ALERT_RE)
);
}
function matchWrongNotes(tokens: Token[], i: number) {
return (
tokens[i].type === 'paragraph_open' &&
tokens[i + 1].type === 'inline' &&
tokens[i + 1].content.match(WRONG_NOTES)
);
}
const findCloseTokenIdx = closeTokenFactory('Note', matchOpenToken, matchCloseToken);
// @ts-ignore
const index: MarkdownItPluginCb = (md, {lang, notesAutotitle, path: optPath, log}) => {
notesAutotitle = typeof notesAutotitle === 'boolean' ? notesAutotitle : true;
const plugin = (state: StateCore) => {
const {tokens, env} = state;
const path = env.path || optPath;
let i = 0;
while (i < tokens.length) {
const match = matchOpenToken(tokens, i);
if (match) {
// Skip paragraph_close for open token (+1) and paragraph_open for close token (+1)
// This is minimal useless content length
const closeTokenIdx = findCloseTokenIdx(tokens, i + 2, path, log);
if (!closeTokenIdx) {
i += 3;
continue;
}
const type = match[1].toLowerCase();
const newOpenToken = new state.Token('yfm_note_open', 'div', 1);
newOpenToken.attrSet('class', `yfm-note yfm-accent-${type}`);
newOpenToken.attrSet('note-type', type);
newOpenToken.map = tokens[i].map;
const closeTokenMap = tokens[closeTokenIdx].map;
if (closeTokenMap && newOpenToken.map) {
newOpenToken.map[1] = closeTokenMap[1];
}
const newCloseToken = new state.Token('yfm_note_close', 'div', -1);
newCloseToken.map = tokens[closeTokenIdx].map;
// Add extra paragraph
const titleOpen = new state.Token('yfm_note_title_open', 'p', 1);
titleOpen.attrSet('class', 'yfm-note-title');
const titleInline = new state.Token('inline', '', 0);
const titleClose = new state.Token('yfm_note_title_close', 'p', -1);
titleOpen.block = true;
titleClose.block = true;
const autotitle = notesAutotitle ? getTitle(type, lang) : '';
titleInline.content = match[2] === undefined ? autotitle : match[2];
titleInline.children = [];
const contentOpen = new state.Token('yfm_note_content_open', 'div', 1);
contentOpen.attrSet('class', 'yfm-note-content');
const contentClose = new state.Token('yfm_note_content_close', 'div', -1);
if (newOpenToken.map) {
contentOpen.map = [newOpenToken.map[0] + 2, newOpenToken.map[1] - 2];
}
state.md.inline.parse(
titleInline.content,
state.md,
state.env,
titleInline.children,
);
const insideTokens = [newOpenToken];
if (titleInline.content) {
insideTokens.push(titleOpen, titleInline, titleClose);
}
insideTokens.push(
contentOpen,
...tokens.slice(i + 3, closeTokenIdx),
contentClose,
newCloseToken,
);
tokens.splice(i, closeTokenIdx - i + 3, ...insideTokens);
i++;
} else if (matchWrongNotes(tokens, i) && tokens[i + 1].content !== '{% endnote %}') {
log.warn(`Incorrect syntax for notes${path ? `, file ${bold(path)}` : ''}`);
i += 3;
} else {
i++;
}
}
};
try {
md.core.ruler.before('curly_attributes', 'notes', plugin);
} catch (e) {
md.core.ruler.push('notes', plugin);
}
};
export = index;