✨ Added markdown and mermaid preview
This commit is contained in:
@@ -0,0 +1,159 @@
|
||||
// Enclose abbreviations in <abbr> tags
|
||||
//
|
||||
import MarkdownIt, {StateBlock, StateCore, Token} from 'markdown-it';
|
||||
|
||||
/**
|
||||
* 环境接口,包含缩写定义
|
||||
*/
|
||||
interface AbbrEnv {
|
||||
abbreviations?: { [key: string]: string };
|
||||
}
|
||||
|
||||
/**
|
||||
* markdown-it-abbr 插件
|
||||
* 用于支持缩写语法
|
||||
*/
|
||||
export default function abbr_plugin(md: MarkdownIt): void {
|
||||
const escapeRE = md.utils.escapeRE;
|
||||
const arrayReplaceAt = md.utils.arrayReplaceAt;
|
||||
|
||||
// ASCII characters in Cc, Sc, Sm, Sk categories we should terminate on;
|
||||
// you can check character classes here:
|
||||
// http://www.unicode.org/Public/UNIDATA/UnicodeData.txt
|
||||
const OTHER_CHARS = ' \r\n$+<=>^`|~';
|
||||
|
||||
const UNICODE_PUNCT_RE = md.utils.lib.ucmicro.P.source;
|
||||
const UNICODE_SPACE_RE = md.utils.lib.ucmicro.Z.source;
|
||||
|
||||
function abbr_def(state: StateBlock, startLine: number, endLine: number, silent: boolean): boolean {
|
||||
let labelEnd: number;
|
||||
let pos = state.bMarks[startLine] + state.tShift[startLine];
|
||||
const max = state.eMarks[startLine];
|
||||
|
||||
if (pos + 2 >= max) { return false; }
|
||||
|
||||
if (state.src.charCodeAt(pos++) !== 0x2A/* * */) { return false; }
|
||||
if (state.src.charCodeAt(pos++) !== 0x5B/* [ */) { return false; }
|
||||
|
||||
const labelStart = pos;
|
||||
|
||||
for (; pos < max; pos++) {
|
||||
const ch = state.src.charCodeAt(pos);
|
||||
if (ch === 0x5B /* [ */) {
|
||||
return false;
|
||||
} else if (ch === 0x5D /* ] */) {
|
||||
labelEnd = pos;
|
||||
break;
|
||||
} else if (ch === 0x5C /* \ */) {
|
||||
pos++;
|
||||
}
|
||||
}
|
||||
|
||||
if (labelEnd! < 0 || state.src.charCodeAt(labelEnd! + 1) !== 0x3A/* : */) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (silent) { return true; }
|
||||
|
||||
const label = state.src.slice(labelStart, labelEnd!).replace(/\\(.)/g, '$1');
|
||||
const title = state.src.slice(labelEnd! + 2, max).trim();
|
||||
if (label.length === 0) { return false; }
|
||||
if (title.length === 0) { return false; }
|
||||
|
||||
const env = state.env as AbbrEnv;
|
||||
if (!env.abbreviations) { env.abbreviations = {}; }
|
||||
// prepend ':' to avoid conflict with Object.prototype members
|
||||
if (typeof env.abbreviations[':' + label] === 'undefined') {
|
||||
env.abbreviations[':' + label] = title;
|
||||
}
|
||||
|
||||
state.line = startLine + 1;
|
||||
return true;
|
||||
}
|
||||
|
||||
function abbr_replace(state: StateCore): void {
|
||||
const blockTokens = state.tokens;
|
||||
|
||||
const env = state.env as AbbrEnv;
|
||||
if (!env.abbreviations) { return; }
|
||||
|
||||
const regSimple = new RegExp('(?:' +
|
||||
Object.keys(env.abbreviations).map(function (x: string) {
|
||||
return x.substr(1);
|
||||
}).sort(function (a: string, b: string) {
|
||||
return b.length - a.length;
|
||||
}).map(escapeRE).join('|') +
|
||||
')');
|
||||
|
||||
const regText = '(^|' + UNICODE_PUNCT_RE + '|' + UNICODE_SPACE_RE +
|
||||
'|[' + OTHER_CHARS.split('').map(escapeRE).join('') + '])' +
|
||||
'(' + Object.keys(env.abbreviations).map(function (x: string) {
|
||||
return x.substr(1);
|
||||
}).sort(function (a: string, b: string) {
|
||||
return b.length - a.length;
|
||||
}).map(escapeRE).join('|') + ')' +
|
||||
'($|' + UNICODE_PUNCT_RE + '|' + UNICODE_SPACE_RE +
|
||||
'|[' + OTHER_CHARS.split('').map(escapeRE).join('') + '])'
|
||||
|
||||
const reg = new RegExp(regText, 'g');
|
||||
|
||||
for (let j = 0, l = blockTokens.length; j < l; j++) {
|
||||
if (blockTokens[j].type !== 'inline') { continue; }
|
||||
let tokens = blockTokens[j].children!;
|
||||
|
||||
// We scan from the end, to keep position when new tags added.
|
||||
for (let i = tokens.length - 1; i >= 0; i--) {
|
||||
const currentToken = tokens[i];
|
||||
if (currentToken.type !== 'text') { continue; }
|
||||
|
||||
let pos = 0;
|
||||
const text = currentToken.content;
|
||||
reg.lastIndex = 0;
|
||||
const nodes: Token[] = [];
|
||||
|
||||
// fast regexp run to determine whether there are any abbreviated words
|
||||
// in the current token
|
||||
if (!regSimple.test(text)) { continue; }
|
||||
|
||||
let m: RegExpExecArray | null;
|
||||
|
||||
while ((m = reg.exec(text))) {
|
||||
if (m.index > 0 || m[1].length > 0) {
|
||||
const token = new state.Token('text', '', 0);
|
||||
token.content = text.slice(pos, m.index + m[1].length);
|
||||
nodes.push(token);
|
||||
}
|
||||
|
||||
const token_o = new state.Token('abbr_open', 'abbr', 1);
|
||||
token_o.attrs = [['title', env.abbreviations[':' + m[2]]]];
|
||||
nodes.push(token_o);
|
||||
|
||||
const token_t = new state.Token('text', '', 0);
|
||||
token_t.content = m[2];
|
||||
nodes.push(token_t);
|
||||
|
||||
const token_c = new state.Token('abbr_close', 'abbr', -1);
|
||||
nodes.push(token_c);
|
||||
|
||||
reg.lastIndex -= m[3].length;
|
||||
pos = reg.lastIndex;
|
||||
}
|
||||
|
||||
if (!nodes.length) { continue; }
|
||||
|
||||
if (pos < text.length) {
|
||||
const token = new state.Token('text', '', 0);
|
||||
token.content = text.slice(pos);
|
||||
nodes.push(token);
|
||||
}
|
||||
|
||||
// replace current node
|
||||
blockTokens[j].children = tokens = arrayReplaceAt(tokens, i, nodes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
md.block.ruler.before('reference', 'abbr_def', abbr_def, { alt: ['paragraph', 'reference'] });
|
||||
|
||||
md.core.ruler.after('linkify', 'abbr_replace', abbr_replace);
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
// Process definition lists
|
||||
//
|
||||
import MarkdownIt, { StateBlock, Token } from 'markdown-it';
|
||||
|
||||
/**
|
||||
* markdown-it-deflist 插件
|
||||
* 用于支持定义列表语法
|
||||
*/
|
||||
export default function deflist_plugin(md: MarkdownIt): void {
|
||||
const isSpace = md.utils.isSpace;
|
||||
|
||||
// Search `[:~][\n ]`, returns next pos after marker on success
|
||||
// or -1 on fail.
|
||||
function skipMarker(state: StateBlock, line: number): number {
|
||||
let start = state.bMarks[line] + state.tShift[line];
|
||||
const max = state.eMarks[line];
|
||||
|
||||
if (start >= max) { return -1; }
|
||||
|
||||
// Check bullet
|
||||
const marker = state.src.charCodeAt(start++);
|
||||
if (marker !== 0x7E/* ~ */ && marker !== 0x3A/* : */) { return -1; }
|
||||
|
||||
const pos = state.skipSpaces(start);
|
||||
|
||||
// require space after ":"
|
||||
if (start === pos) { return -1; }
|
||||
|
||||
// no empty definitions, e.g. " : "
|
||||
if (pos >= max) { return -1; }
|
||||
|
||||
return start;
|
||||
}
|
||||
|
||||
function markTightParagraphs(state: StateBlock, idx: number): void {
|
||||
const level = state.level + 2;
|
||||
|
||||
for (let i = idx + 2, l = state.tokens.length - 2; i < l; i++) {
|
||||
if (state.tokens[i].level === level && state.tokens[i].type === 'paragraph_open') {
|
||||
state.tokens[i + 2].hidden = true;
|
||||
state.tokens[i].hidden = true;
|
||||
i += 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function deflist(state: StateBlock, startLine: number, endLine: number, silent: boolean): boolean {
|
||||
if (silent) {
|
||||
// quirk: validation mode validates a dd block only, not a whole deflist
|
||||
if (state.ddIndent < 0) { return false; }
|
||||
return skipMarker(state, startLine) >= 0;
|
||||
}
|
||||
|
||||
let nextLine = startLine + 1;
|
||||
if (nextLine >= endLine) { return false; }
|
||||
|
||||
if (state.isEmpty(nextLine)) {
|
||||
nextLine++;
|
||||
if (nextLine >= endLine) { return false; }
|
||||
}
|
||||
|
||||
if (state.sCount[nextLine] < state.blkIndent) { return false; }
|
||||
let contentStart = skipMarker(state, nextLine);
|
||||
if (contentStart < 0) { return false; }
|
||||
|
||||
// Start list
|
||||
const listTokIdx = state.tokens.length;
|
||||
let tight = true;
|
||||
|
||||
const token_dl_o: Token = state.push('dl_open', 'dl', 1);
|
||||
const listLines: [number, number] = [startLine, 0];
|
||||
token_dl_o.map = listLines;
|
||||
|
||||
//
|
||||
// Iterate list items
|
||||
//
|
||||
|
||||
let dtLine = startLine;
|
||||
let ddLine = nextLine;
|
||||
|
||||
// One definition list can contain multiple DTs,
|
||||
// and one DT can be followed by multiple DDs.
|
||||
//
|
||||
// Thus, there is two loops here, and label is
|
||||
// needed to break out of the second one
|
||||
//
|
||||
/* eslint no-labels:0,block-scoped-var:0 */
|
||||
OUTER:
|
||||
for (;;) {
|
||||
let prevEmptyEnd = false;
|
||||
|
||||
const token_dt_o: Token = state.push('dt_open', 'dt', 1);
|
||||
token_dt_o.map = [dtLine, dtLine];
|
||||
|
||||
const token_i: Token = state.push('inline', '', 0);
|
||||
token_i.map = [dtLine, dtLine];
|
||||
token_i.content = state.getLines(dtLine, dtLine + 1, state.blkIndent, false).trim();
|
||||
token_i.children = [];
|
||||
|
||||
state.push('dt_close', 'dt', -1);
|
||||
|
||||
for (;;) {
|
||||
const token_dd_o: Token = state.push('dd_open', 'dd', 1);
|
||||
const itemLines: [number, number] = [nextLine, 0];
|
||||
token_dd_o.map = itemLines;
|
||||
|
||||
let pos = contentStart;
|
||||
const max = state.eMarks[ddLine];
|
||||
let offset = state.sCount[ddLine] + contentStart - (state.bMarks[ddLine] + state.tShift[ddLine]);
|
||||
|
||||
while (pos < max) {
|
||||
const ch = state.src.charCodeAt(pos);
|
||||
|
||||
if (isSpace(ch)) {
|
||||
if (ch === 0x09) {
|
||||
offset += 4 - offset % 4;
|
||||
} else {
|
||||
offset++;
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
|
||||
pos++;
|
||||
}
|
||||
|
||||
contentStart = pos;
|
||||
|
||||
const oldTight = state.tight;
|
||||
const oldDDIndent = state.ddIndent;
|
||||
const oldIndent = state.blkIndent;
|
||||
const oldTShift = state.tShift[ddLine];
|
||||
const oldSCount = state.sCount[ddLine];
|
||||
const oldParentType = state.parentType;
|
||||
state.blkIndent = state.ddIndent = state.sCount[ddLine] + 2;
|
||||
state.tShift[ddLine] = contentStart - state.bMarks[ddLine];
|
||||
state.sCount[ddLine] = offset;
|
||||
state.tight = true;
|
||||
state.parentType = 'deflist' as any;
|
||||
|
||||
state.md.block.tokenize(state, ddLine, endLine);
|
||||
|
||||
// If any of list item is tight, mark list as tight
|
||||
if (!state.tight || prevEmptyEnd) {
|
||||
tight = false;
|
||||
}
|
||||
// Item become loose if finish with empty line,
|
||||
// but we should filter last element, because it means list finish
|
||||
prevEmptyEnd = (state.line - ddLine) > 1 && state.isEmpty(state.line - 1);
|
||||
|
||||
state.tShift[ddLine] = oldTShift;
|
||||
state.sCount[ddLine] = oldSCount;
|
||||
state.tight = oldTight;
|
||||
state.parentType = oldParentType;
|
||||
state.blkIndent = oldIndent;
|
||||
state.ddIndent = oldDDIndent;
|
||||
|
||||
state.push('dd_close', 'dd', -1);
|
||||
|
||||
itemLines[1] = nextLine = state.line;
|
||||
|
||||
if (nextLine >= endLine) { break OUTER; }
|
||||
|
||||
if (state.sCount[nextLine] < state.blkIndent) { break OUTER; }
|
||||
contentStart = skipMarker(state, nextLine);
|
||||
if (contentStart < 0) { break; }
|
||||
|
||||
ddLine = nextLine;
|
||||
|
||||
// go to the next loop iteration:
|
||||
// insert DD tag and repeat checking
|
||||
}
|
||||
|
||||
if (nextLine >= endLine) { break; }
|
||||
dtLine = nextLine;
|
||||
|
||||
if (state.isEmpty(dtLine)) { break; }
|
||||
if (state.sCount[dtLine] < state.blkIndent) { break; }
|
||||
|
||||
ddLine = dtLine + 1;
|
||||
if (ddLine >= endLine) { break; }
|
||||
if (state.isEmpty(ddLine)) { ddLine++; }
|
||||
if (ddLine >= endLine) { break; }
|
||||
|
||||
if (state.sCount[ddLine] < state.blkIndent) { break; }
|
||||
contentStart = skipMarker(state, ddLine);
|
||||
if (contentStart < 0) { break; }
|
||||
|
||||
// go to the next loop iteration:
|
||||
// insert DT and DD tags and repeat checking
|
||||
}
|
||||
|
||||
// Finilize list
|
||||
state.push('dl_close', 'dl', -1);
|
||||
|
||||
listLines[1] = nextLine;
|
||||
|
||||
state.line = nextLine;
|
||||
|
||||
// mark paragraphs tight if needed
|
||||
if (tight) {
|
||||
markTightParagraphs(state, listTokIdx);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
md.block.ruler.before('paragraph', 'deflist', deflist, { alt: ['paragraph', 'reference', 'blockquote'] });
|
||||
}
|
||||
@@ -0,0 +1,390 @@
|
||||
import MarkdownIt, {Renderer, StateBlock, StateCore, StateInline, Token} from 'markdown-it';
|
||||
|
||||
/**
|
||||
* 脚注元数据接口
|
||||
*/
|
||||
interface FootnoteMeta {
|
||||
id: number;
|
||||
subId: number;
|
||||
label: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 脚注列表项接口
|
||||
*/
|
||||
interface FootnoteItem {
|
||||
label?: string;
|
||||
content?: string;
|
||||
tokens?: Token[];
|
||||
count: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 环境接口
|
||||
*/
|
||||
interface FootnoteEnv {
|
||||
footnotes?: {
|
||||
refs?: { [key: string]: number };
|
||||
list?: FootnoteItem[];
|
||||
};
|
||||
docId?: string;
|
||||
}
|
||||
|
||||
/// /////////////////////////////////////////////////////////////////////////////
|
||||
// Renderer partials
|
||||
|
||||
function render_footnote_anchor_name(tokens: Token[], idx: number, options: any, env: FootnoteEnv): string {
|
||||
const n = Number(tokens[idx].meta.id + 1).toString();
|
||||
let prefix = '';
|
||||
|
||||
if (typeof env.docId === 'string') prefix = `-${env.docId}-`;
|
||||
|
||||
return prefix + n;
|
||||
}
|
||||
|
||||
function render_footnote_caption(tokens: Token[], idx: number): string {
|
||||
let n = Number(tokens[idx].meta.id + 1).toString();
|
||||
|
||||
if (tokens[idx].meta.subId > 0) n += `:${tokens[idx].meta.subId}`;
|
||||
|
||||
return `[${n}]`;
|
||||
}
|
||||
|
||||
function render_footnote_ref(tokens: Token[], idx: number, options: any, env: FootnoteEnv, slf: Renderer): string {
|
||||
const id = slf.rules.footnote_anchor_name!(tokens, idx, options, env, slf);
|
||||
const caption = slf.rules.footnote_caption!(tokens, idx, options, env, slf);
|
||||
let refid = id;
|
||||
|
||||
if (tokens[idx].meta.subId > 0) refid += `:${tokens[idx].meta.subId}`;
|
||||
|
||||
return `<sup class="footnote-ref"><a href="#fn${id}" id="fnref${refid}">${caption}</a></sup>`;
|
||||
}
|
||||
|
||||
function render_footnote_block_open(tokens: Token[], idx: number, options: any): string {
|
||||
return (options.xhtmlOut ? '<hr class="footnotes-sep" />\n' : '<hr class="footnotes-sep">\n') +
|
||||
'<section class="footnotes">\n' +
|
||||
'<ol class="footnotes-list">\n';
|
||||
}
|
||||
|
||||
function render_footnote_block_close(): string {
|
||||
return '</ol>\n</section>\n';
|
||||
}
|
||||
|
||||
function render_footnote_open(tokens: Token[], idx: number, options: any, env: FootnoteEnv, slf: Renderer): string {
|
||||
let id = slf.rules.footnote_anchor_name!(tokens, idx, options, env, slf);
|
||||
|
||||
if (tokens[idx].meta.subId > 0) id += `:${tokens[idx].meta.subId}`;
|
||||
|
||||
return `<li id="fn${id}" class="footnote-item">`;
|
||||
}
|
||||
|
||||
function render_footnote_close(): string {
|
||||
return '</li>\n';
|
||||
}
|
||||
|
||||
function render_footnote_anchor(tokens: Token[], idx: number, options: any, env: FootnoteEnv, slf: Renderer): string {
|
||||
let id = slf.rules.footnote_anchor_name!(tokens, idx, options, env, slf);
|
||||
|
||||
if (tokens[idx].meta.subId > 0) id += `:${tokens[idx].meta.subId}`;
|
||||
|
||||
/* ↩ with escape code to prevent display as Apple Emoji on iOS */
|
||||
return ` <a href="#fnref${id}" class="footnote-backref">\u21a9\uFE0E</a>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* markdown-it-footnote 插件
|
||||
* 用于支持脚注语法
|
||||
*/
|
||||
export default function footnote_plugin(md: MarkdownIt): void {
|
||||
const parseLinkLabel = md.helpers.parseLinkLabel;
|
||||
const isSpace = md.utils.isSpace;
|
||||
|
||||
md.renderer.rules.footnote_ref = render_footnote_ref;
|
||||
md.renderer.rules.footnote_block_open = render_footnote_block_open;
|
||||
md.renderer.rules.footnote_block_close = render_footnote_block_close;
|
||||
md.renderer.rules.footnote_open = render_footnote_open;
|
||||
md.renderer.rules.footnote_close = render_footnote_close;
|
||||
md.renderer.rules.footnote_anchor = render_footnote_anchor;
|
||||
|
||||
// helpers (only used in other rules, no tokens are attached to those)
|
||||
md.renderer.rules.footnote_caption = render_footnote_caption;
|
||||
md.renderer.rules.footnote_anchor_name = render_footnote_anchor_name;
|
||||
|
||||
// Process footnote block definition
|
||||
function footnote_def(state: StateBlock, startLine: number, endLine: number, silent: boolean): boolean {
|
||||
const start = state.bMarks[startLine] + state.tShift[startLine];
|
||||
const max = state.eMarks[startLine];
|
||||
|
||||
// line should be at least 5 chars - "[^x]:"
|
||||
if (start + 4 > max) return false;
|
||||
|
||||
if (state.src.charCodeAt(start) !== 0x5B/* [ */) return false;
|
||||
if (state.src.charCodeAt(start + 1) !== 0x5E/* ^ */) return false;
|
||||
|
||||
let pos: number;
|
||||
|
||||
for (pos = start + 2; pos < max; pos++) {
|
||||
if (state.src.charCodeAt(pos) === 0x20) return false;
|
||||
if (state.src.charCodeAt(pos) === 0x5D /* ] */) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (pos === start + 2) return false; // no empty footnote labels
|
||||
if (pos + 1 >= max || state.src.charCodeAt(++pos) !== 0x3A /* : */) return false;
|
||||
if (silent) return true;
|
||||
pos++;
|
||||
|
||||
const env = state.env as FootnoteEnv;
|
||||
if (!env.footnotes) env.footnotes = {};
|
||||
if (!env.footnotes.refs) env.footnotes.refs = {};
|
||||
const label = state.src.slice(start + 2, pos - 2);
|
||||
env.footnotes.refs[`:${label}`] = -1;
|
||||
|
||||
const token_fref_o = new state.Token('footnote_reference_open', '', 1);
|
||||
token_fref_o.meta = { label };
|
||||
token_fref_o.level = state.level++;
|
||||
state.tokens.push(token_fref_o);
|
||||
|
||||
const oldBMark = state.bMarks[startLine];
|
||||
const oldTShift = state.tShift[startLine];
|
||||
const oldSCount = state.sCount[startLine];
|
||||
const oldParentType = state.parentType;
|
||||
|
||||
const posAfterColon = pos;
|
||||
const initial = state.sCount[startLine] + pos - (state.bMarks[startLine] + state.tShift[startLine]);
|
||||
let offset = initial;
|
||||
|
||||
while (pos < max) {
|
||||
const ch = state.src.charCodeAt(pos);
|
||||
|
||||
if (isSpace(ch)) {
|
||||
if (ch === 0x09) {
|
||||
offset += 4 - offset % 4;
|
||||
} else {
|
||||
offset++;
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
|
||||
pos++;
|
||||
}
|
||||
|
||||
state.tShift[startLine] = pos - posAfterColon;
|
||||
state.sCount[startLine] = offset - initial;
|
||||
|
||||
state.bMarks[startLine] = posAfterColon;
|
||||
state.blkIndent += 4;
|
||||
state.parentType = 'footnote' as any;
|
||||
|
||||
if (state.sCount[startLine] < state.blkIndent) {
|
||||
state.sCount[startLine] += state.blkIndent;
|
||||
}
|
||||
|
||||
state.md.block.tokenize(state, startLine, endLine);
|
||||
|
||||
state.parentType = oldParentType;
|
||||
state.blkIndent -= 4;
|
||||
state.tShift[startLine] = oldTShift;
|
||||
state.sCount[startLine] = oldSCount;
|
||||
state.bMarks[startLine] = oldBMark;
|
||||
|
||||
const token_fref_c = new state.Token('footnote_reference_close', '', -1);
|
||||
token_fref_c.level = --state.level;
|
||||
state.tokens.push(token_fref_c);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Process inline footnotes (^[...])
|
||||
function footnote_inline(state: StateInline, silent: boolean): boolean {
|
||||
const max = state.posMax;
|
||||
const start = state.pos;
|
||||
|
||||
if (start + 2 >= max) return false;
|
||||
if (state.src.charCodeAt(start) !== 0x5E/* ^ */) return false;
|
||||
if (state.src.charCodeAt(start + 1) !== 0x5B/* [ */) return false;
|
||||
|
||||
const labelStart = start + 2;
|
||||
const labelEnd = parseLinkLabel(state, start + 1);
|
||||
|
||||
// parser failed to find ']', so it's not a valid note
|
||||
if (labelEnd < 0) return false;
|
||||
|
||||
// We found the end of the link, and know for a fact it's a valid link;
|
||||
// so all that's left to do is to call tokenizer.
|
||||
//
|
||||
if (!silent) {
|
||||
const env = state.env as FootnoteEnv;
|
||||
if (!env.footnotes) env.footnotes = {};
|
||||
if (!env.footnotes.list) env.footnotes.list = [];
|
||||
const footnoteId = env.footnotes.list.length;
|
||||
const tokens: Token[] = [];
|
||||
|
||||
state.md.inline.parse(
|
||||
state.src.slice(labelStart, labelEnd),
|
||||
state.md,
|
||||
state.env,
|
||||
tokens
|
||||
);
|
||||
|
||||
const token = state.push('footnote_ref', '', 0);
|
||||
token.meta = { id: footnoteId };
|
||||
|
||||
env.footnotes.list[footnoteId] = {
|
||||
content: state.src.slice(labelStart, labelEnd),
|
||||
tokens,
|
||||
count: 0
|
||||
};
|
||||
}
|
||||
|
||||
state.pos = labelEnd + 1;
|
||||
state.posMax = max;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Process footnote references ([^...])
|
||||
function footnote_ref(state: StateInline, silent: boolean): boolean {
|
||||
const max = state.posMax;
|
||||
const start = state.pos;
|
||||
|
||||
// should be at least 4 chars - "[^x]"
|
||||
if (start + 3 > max) return false;
|
||||
|
||||
const env = state.env as FootnoteEnv;
|
||||
if (!env.footnotes || !env.footnotes.refs) return false;
|
||||
if (state.src.charCodeAt(start) !== 0x5B/* [ */) return false;
|
||||
if (state.src.charCodeAt(start + 1) !== 0x5E/* ^ */) return false;
|
||||
|
||||
let pos: number;
|
||||
|
||||
for (pos = start + 2; pos < max; pos++) {
|
||||
if (state.src.charCodeAt(pos) === 0x20) return false;
|
||||
if (state.src.charCodeAt(pos) === 0x0A) return false;
|
||||
if (state.src.charCodeAt(pos) === 0x5D /* ] */) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (pos === start + 2) return false; // no empty footnote labels
|
||||
if (pos >= max) return false;
|
||||
pos++;
|
||||
|
||||
const label = state.src.slice(start + 2, pos - 1);
|
||||
if (typeof env.footnotes.refs[`:${label}`] === 'undefined') return false;
|
||||
|
||||
if (!silent) {
|
||||
if (!env.footnotes.list) env.footnotes.list = [];
|
||||
|
||||
let footnoteId: number;
|
||||
|
||||
if (env.footnotes.refs[`:${label}`] < 0) {
|
||||
footnoteId = env.footnotes.list.length;
|
||||
env.footnotes.list[footnoteId] = { label, count: 0 };
|
||||
env.footnotes.refs[`:${label}`] = footnoteId;
|
||||
} else {
|
||||
footnoteId = env.footnotes.refs[`:${label}`];
|
||||
}
|
||||
|
||||
const footnoteSubId = env.footnotes.list[footnoteId].count;
|
||||
env.footnotes.list[footnoteId].count++;
|
||||
|
||||
const token = state.push('footnote_ref', '', 0);
|
||||
token.meta = { id: footnoteId, subId: footnoteSubId, label };
|
||||
}
|
||||
|
||||
state.pos = pos;
|
||||
state.posMax = max;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Glue footnote tokens to end of token stream
|
||||
function footnote_tail(state: StateCore): void {
|
||||
let tokens: Token[] | null = null;
|
||||
let current: Token[];
|
||||
let currentLabel: string;
|
||||
let insideRef = false;
|
||||
const refTokens: { [key: string]: Token[] } = {};
|
||||
|
||||
const env = state.env as FootnoteEnv;
|
||||
if (!env.footnotes) { return; }
|
||||
|
||||
state.tokens = state.tokens.filter(function (tok) {
|
||||
if (tok.type === 'footnote_reference_open') {
|
||||
insideRef = true;
|
||||
current = [];
|
||||
currentLabel = tok.meta.label;
|
||||
return false;
|
||||
}
|
||||
if (tok.type === 'footnote_reference_close') {
|
||||
insideRef = false;
|
||||
// prepend ':' to avoid conflict with Object.prototype members
|
||||
refTokens[':' + currentLabel] = current;
|
||||
return false;
|
||||
}
|
||||
if (insideRef) { current.push(tok); }
|
||||
return !insideRef;
|
||||
});
|
||||
|
||||
if (!env.footnotes.list) { return; }
|
||||
const list = env.footnotes.list;
|
||||
|
||||
state.tokens.push(new state.Token('footnote_block_open', '', 1));
|
||||
|
||||
for (let i = 0, l = list.length; i < l; i++) {
|
||||
const token_fo = new state.Token('footnote_open', '', 1);
|
||||
token_fo.meta = { id: i, label: list[i].label };
|
||||
state.tokens.push(token_fo);
|
||||
|
||||
if (list[i].tokens) {
|
||||
tokens = [];
|
||||
|
||||
const token_po = new state.Token('paragraph_open', 'p', 1);
|
||||
token_po.block = true;
|
||||
tokens.push(token_po);
|
||||
|
||||
const token_i = new state.Token('inline', '', 0);
|
||||
token_i.children = list[i].tokens || null;
|
||||
token_i.content = list[i].content || '';
|
||||
tokens.push(token_i);
|
||||
|
||||
const token_pc = new state.Token('paragraph_close', 'p', -1);
|
||||
token_pc.block = true;
|
||||
tokens.push(token_pc);
|
||||
} else if (list[i].label) {
|
||||
tokens = refTokens[`:${list[i].label}`] || null;
|
||||
}
|
||||
|
||||
if (tokens) state.tokens = state.tokens.concat(tokens);
|
||||
|
||||
let lastParagraph: Token | null;
|
||||
|
||||
if (state.tokens[state.tokens.length - 1].type === 'paragraph_close') {
|
||||
lastParagraph = state.tokens.pop()!;
|
||||
} else {
|
||||
lastParagraph = null;
|
||||
}
|
||||
|
||||
const t = list[i].count > 0 ? list[i].count : 1;
|
||||
for (let j = 0; j < t; j++) {
|
||||
const token_a = new state.Token('footnote_anchor', '', 0);
|
||||
token_a.meta = { id: i, subId: j, label: list[i].label };
|
||||
state.tokens.push(token_a);
|
||||
}
|
||||
|
||||
if (lastParagraph) {
|
||||
state.tokens.push(lastParagraph);
|
||||
}
|
||||
|
||||
state.tokens.push(new state.Token('footnote_close', '', -1));
|
||||
}
|
||||
|
||||
state.tokens.push(new state.Token('footnote_block_close', '', -1));
|
||||
}
|
||||
|
||||
md.block.ruler.before('reference', 'footnote_def', footnote_def, { alt: ['paragraph', 'reference'] });
|
||||
md.inline.ruler.after('image', 'footnote_inline', footnote_inline);
|
||||
md.inline.ruler.after('footnote_inline', 'footnote_ref', footnote_ref);
|
||||
md.core.ruler.after('inline', 'footnote_tail', footnote_tail);
|
||||
}
|
||||
160
frontend/src/common/markdown-it/plugins/markdown-it-ins/index.ts
Normal file
160
frontend/src/common/markdown-it/plugins/markdown-it-ins/index.ts
Normal file
@@ -0,0 +1,160 @@
|
||||
import MarkdownIt, { StateInline, Token } from 'markdown-it';
|
||||
|
||||
/**
|
||||
* 分隔符接口定义
|
||||
*/
|
||||
interface Delimiter {
|
||||
marker: number;
|
||||
length: number;
|
||||
jump: number;
|
||||
token: number;
|
||||
end: number;
|
||||
open: boolean;
|
||||
close: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 扫描结果接口定义
|
||||
*/
|
||||
interface ScanResult {
|
||||
can_open: boolean;
|
||||
can_close: boolean;
|
||||
length: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Token 元数据接口定义
|
||||
*/
|
||||
interface TokenMeta {
|
||||
delimiters?: Delimiter[];
|
||||
}
|
||||
|
||||
/**
|
||||
* markdown-it-ins 插件
|
||||
* 用于支持插入文本语法 ++text++
|
||||
*/
|
||||
export default function ins_plugin(md: MarkdownIt): void {
|
||||
// Insert each marker as a separate text token, and add it to delimiter list
|
||||
//
|
||||
function tokenize(state: StateInline, silent: boolean): boolean {
|
||||
const start = state.pos;
|
||||
const marker = state.src.charCodeAt(start);
|
||||
|
||||
if (silent) { return false; }
|
||||
|
||||
if (marker !== 0x2B/* + */) { return false; }
|
||||
|
||||
const scanned = state.scanDelims(state.pos, true) as ScanResult;
|
||||
let len = scanned.length;
|
||||
const ch = String.fromCharCode(marker);
|
||||
|
||||
if (len < 2) { return false; }
|
||||
|
||||
if (len % 2) {
|
||||
const token: Token = state.push('text', '', 0);
|
||||
token.content = ch;
|
||||
len--;
|
||||
}
|
||||
|
||||
for (let i = 0; i < len; i += 2) {
|
||||
const token: Token = state.push('text', '', 0);
|
||||
token.content = ch + ch;
|
||||
|
||||
if (!scanned.can_open && !scanned.can_close) { continue; }
|
||||
|
||||
state.delimiters.push({
|
||||
marker,
|
||||
length: 0, // disable "rule of 3" length checks meant for emphasis
|
||||
jump: i / 2, // 1 delimiter = 2 characters
|
||||
token: state.tokens.length - 1,
|
||||
end: -1,
|
||||
open: scanned.can_open,
|
||||
close: scanned.can_close
|
||||
} as Delimiter);
|
||||
}
|
||||
|
||||
state.pos += scanned.length;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Walk through delimiter list and replace text tokens with tags
|
||||
//
|
||||
function postProcess(state: StateInline, delimiters: Delimiter[]): void {
|
||||
let token: Token;
|
||||
const loneMarkers: number[] = [];
|
||||
const max = delimiters.length;
|
||||
|
||||
for (let i = 0; i < max; i++) {
|
||||
const startDelim = delimiters[i];
|
||||
|
||||
if (startDelim.marker !== 0x2B/* + */) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (startDelim.end === -1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const endDelim = delimiters[startDelim.end];
|
||||
|
||||
token = state.tokens[startDelim.token];
|
||||
token.type = 'ins_open';
|
||||
token.tag = 'ins';
|
||||
token.nesting = 1;
|
||||
token.markup = '++';
|
||||
token.content = '';
|
||||
|
||||
token = state.tokens[endDelim.token];
|
||||
token.type = 'ins_close';
|
||||
token.tag = 'ins';
|
||||
token.nesting = -1;
|
||||
token.markup = '++';
|
||||
token.content = '';
|
||||
|
||||
if (state.tokens[endDelim.token - 1].type === 'text' &&
|
||||
state.tokens[endDelim.token - 1].content === '+') {
|
||||
loneMarkers.push(endDelim.token - 1);
|
||||
}
|
||||
}
|
||||
|
||||
// If a marker sequence has an odd number of characters, it's splitted
|
||||
// like this: `~~~~~` -> `~` + `~~` + `~~`, leaving one marker at the
|
||||
// start of the sequence.
|
||||
//
|
||||
// So, we have to move all those markers after subsequent s_close tags.
|
||||
//
|
||||
while (loneMarkers.length) {
|
||||
const i = loneMarkers.pop()!;
|
||||
let j = i + 1;
|
||||
|
||||
while (j < state.tokens.length && state.tokens[j].type === 'ins_close') {
|
||||
j++;
|
||||
}
|
||||
|
||||
j--;
|
||||
|
||||
if (i !== j) {
|
||||
token = state.tokens[j];
|
||||
state.tokens[j] = state.tokens[i];
|
||||
state.tokens[i] = token;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
md.inline.ruler.before('emphasis', 'ins', tokenize);
|
||||
md.inline.ruler2.before('emphasis', 'ins', function (state: StateInline): boolean {
|
||||
const tokens_meta = state.tokens_meta as TokenMeta[];
|
||||
const max = (state.tokens_meta || []).length;
|
||||
|
||||
postProcess(state, state.delimiters as Delimiter[]);
|
||||
|
||||
for (let curr = 0; curr < max; curr++) {
|
||||
if (tokens_meta[curr] && tokens_meta[curr].delimiters) {
|
||||
postProcess(state, tokens_meta[curr].delimiters!);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
import MarkdownIt, {StateInline, Token} from 'markdown-it';
|
||||
|
||||
/**
|
||||
* 分隔符接口定义
|
||||
*/
|
||||
interface Delimiter {
|
||||
marker: number;
|
||||
length: number;
|
||||
jump: number;
|
||||
token: number;
|
||||
end: number;
|
||||
open: boolean;
|
||||
close: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 扫描结果接口定义
|
||||
*/
|
||||
interface ScanResult {
|
||||
can_open: boolean;
|
||||
can_close: boolean;
|
||||
length: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Token 元数据接口定义
|
||||
*/
|
||||
interface TokenMeta {
|
||||
delimiters?: Delimiter[];
|
||||
}
|
||||
|
||||
/**
|
||||
* markdown-it-mark 插件
|
||||
* 用于支持 ==标记文本== 语法
|
||||
*/
|
||||
export default function markPlugin(md: MarkdownIt): void {
|
||||
// Insert each marker as a separate text token, and add it to delimiter list
|
||||
//
|
||||
function tokenize(state: StateInline, silent: boolean): boolean {
|
||||
const start = state.pos;
|
||||
const marker = state.src.charCodeAt(start);
|
||||
|
||||
if (silent) { return false; }
|
||||
|
||||
if (marker !== 0x3D/* = */) { return false; }
|
||||
|
||||
const scanned = state.scanDelims(state.pos, true) as ScanResult;
|
||||
let len = scanned.length;
|
||||
const ch = String.fromCharCode(marker);
|
||||
|
||||
if (len < 2) { return false; }
|
||||
|
||||
if (len % 2) {
|
||||
const token: Token = state.push('text', '', 0);
|
||||
token.content = ch;
|
||||
len--;
|
||||
}
|
||||
|
||||
for (let i = 0; i < len; i += 2) {
|
||||
const token: Token = state.push('text', '', 0);
|
||||
token.content = ch + ch;
|
||||
|
||||
if (!scanned.can_open && !scanned.can_close) { continue; }
|
||||
|
||||
state.delimiters.push({
|
||||
marker,
|
||||
length: 0, // disable "rule of 3" length checks meant for emphasis
|
||||
jump: i / 2, // 1 delimiter = 2 characters
|
||||
token: state.tokens.length - 1,
|
||||
end: -1,
|
||||
open: scanned.can_open,
|
||||
close: scanned.can_close
|
||||
} as Delimiter);
|
||||
}
|
||||
|
||||
state.pos += scanned.length;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Walk through delimiter list and replace text tokens with tags
|
||||
//
|
||||
function postProcess(state: StateInline, delimiters: Delimiter[]): void {
|
||||
const loneMarkers: number[] = [];
|
||||
const max = delimiters.length;
|
||||
|
||||
for (let i = 0; i < max; i++) {
|
||||
const startDelim = delimiters[i];
|
||||
|
||||
if (startDelim.marker !== 0x3D/* = */) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (startDelim.end === -1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const endDelim = delimiters[startDelim.end];
|
||||
|
||||
const token_o = state.tokens[startDelim.token];
|
||||
token_o.type = 'mark_open';
|
||||
token_o.tag = 'mark';
|
||||
token_o.nesting = 1;
|
||||
token_o.markup = '==';
|
||||
token_o.content = '';
|
||||
|
||||
const token_c = state.tokens[endDelim.token];
|
||||
token_c.type = 'mark_close';
|
||||
token_c.tag = 'mark';
|
||||
token_c.nesting = -1;
|
||||
token_c.markup = '==';
|
||||
token_c.content = '';
|
||||
|
||||
if (state.tokens[endDelim.token - 1].type === 'text' &&
|
||||
state.tokens[endDelim.token - 1].content === '=') {
|
||||
loneMarkers.push(endDelim.token - 1);
|
||||
}
|
||||
}
|
||||
|
||||
// If a marker sequence has an odd number of characters, it's splitted
|
||||
// like this: `~~~~~` -> `~` + `~~` + `~~`, leaving one marker at the
|
||||
// start of the sequence.
|
||||
//
|
||||
// So, we have to move all those markers after subsequent s_close tags.
|
||||
//
|
||||
while (loneMarkers.length) {
|
||||
const i = loneMarkers.pop()!;
|
||||
let j = i + 1;
|
||||
|
||||
while (j < state.tokens.length && state.tokens[j].type === 'mark_close') {
|
||||
j++;
|
||||
}
|
||||
|
||||
j--;
|
||||
|
||||
if (i !== j) {
|
||||
const token = state.tokens[j];
|
||||
state.tokens[j] = state.tokens[i];
|
||||
state.tokens[i] = token;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
md.inline.ruler.before('emphasis', 'mark', tokenize);
|
||||
md.inline.ruler2.before('emphasis', 'mark', function (state: StateInline): boolean {
|
||||
let curr: number;
|
||||
const tokens_meta = state.tokens_meta as TokenMeta[];
|
||||
const max = (state.tokens_meta || []).length;
|
||||
|
||||
postProcess(state, state.delimiters as Delimiter[]);
|
||||
|
||||
for (curr = 0; curr < max; curr++) {
|
||||
if (tokens_meta[curr] && tokens_meta[curr].delimiters) {
|
||||
postProcess(state, tokens_meta[curr].delimiters!);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
import mermaid from "mermaid";
|
||||
import {genUid, hashCode, sleep} from "./utils";
|
||||
|
||||
const mermaidCache = new Map<string, HTMLElement>();
|
||||
|
||||
// 缓存计数器,用于清除缓存
|
||||
const mermaidCacheCount = new Map<string, number>();
|
||||
let count = 0;
|
||||
|
||||
|
||||
let countTmo = setTimeout(() => undefined, 0);
|
||||
const addCount = () => {
|
||||
clearTimeout(countTmo);
|
||||
countTmo = setTimeout(() => {
|
||||
count++;
|
||||
clearCache();
|
||||
}, 500);
|
||||
};
|
||||
|
||||
const clearCache = () => {
|
||||
for (const key of mermaidCacheCount.keys()) {
|
||||
const value = mermaidCacheCount.get(key)!;
|
||||
if (value + 3 < count) {
|
||||
mermaidCache.delete(key);
|
||||
mermaidCacheCount.delete(key);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 渲染 mermaid
|
||||
* @param code mermaid 代码
|
||||
* @param targetId 目标 id
|
||||
* @param count 计数器
|
||||
*/
|
||||
const renderMermaid = async (code: string, targetId: string, count: number) => {
|
||||
let limit = 100;
|
||||
while (limit-- > 0) {
|
||||
const container = document.getElementById(targetId);
|
||||
if (!container) {
|
||||
await sleep(100);
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
const {svg} = await mermaid.render("mermaid-svg-" + genUid(), code, container);
|
||||
container.innerHTML = svg;
|
||||
mermaidCache.set(targetId, container);
|
||||
mermaidCacheCount.set(targetId, count);
|
||||
} catch (e) {
|
||||
}
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
export interface MermaidItOptions {
|
||||
theme?: "default" | "dark" | "forest" | "neutral" | "base";
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新 mermaid 主题
|
||||
*/
|
||||
export const updateMermaidTheme = (theme: "default" | "dark" | "forest" | "neutral" | "base") => {
|
||||
mermaid.initialize({
|
||||
startOnLoad: false,
|
||||
theme: theme
|
||||
});
|
||||
// 清空缓存,强制重新渲染
|
||||
mermaidCache.clear();
|
||||
mermaidCacheCount.clear();
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* mermaid 插件
|
||||
* @param md markdown-it
|
||||
* @param options 配置选项
|
||||
* @constructor MermaidIt
|
||||
*/
|
||||
export const MermaidIt = function (md: any, options?: MermaidItOptions): void {
|
||||
const theme = options?.theme || "default";
|
||||
mermaid.initialize({
|
||||
startOnLoad: false,
|
||||
theme: theme
|
||||
});
|
||||
const defaultRenderer = md.renderer.rules.fence.bind(md.renderer.rules);
|
||||
md.renderer.rules.fence = (tokens: any, idx: any, options: any, env: any, self: any) => {
|
||||
addCount();
|
||||
const token = tokens[idx];
|
||||
const info = token.info.trim();
|
||||
if (info === "mermaid") {
|
||||
const containerId = "mermaid-container-" + hashCode(token.content);
|
||||
const container = document.createElement("div");
|
||||
container.id = containerId;
|
||||
if (mermaidCache.has(containerId)) {
|
||||
container.innerHTML = mermaidCache.get(containerId)!.innerHTML;
|
||||
mermaidCacheCount.set(containerId, count);
|
||||
} else {
|
||||
renderMermaid(token.content, containerId, count).then();
|
||||
}
|
||||
return container.outerHTML;
|
||||
}
|
||||
// 使用默认的渲染规则
|
||||
return defaultRenderer(tokens, idx, options, env, self);
|
||||
};
|
||||
};
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
/**
|
||||
* uuid 生成函数
|
||||
* @param split 分隔符
|
||||
*/
|
||||
export const genUid = (split = "") => {
|
||||
return uuidv4().split("-").join(split);
|
||||
};
|
||||
|
||||
/**
|
||||
* 一个简易的sleep函数
|
||||
*/
|
||||
export const sleep = async (ms: number) => {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(resolve, ms);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 计算字符串的hash值
|
||||
* 返回一个数字
|
||||
* @param str
|
||||
*/
|
||||
export const hashCode = (str: string) => {
|
||||
let hash = 0;
|
||||
if (str.length === 0) return hash;
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
const char = str.charCodeAt(i);
|
||||
hash = (hash << 5) - hash + char;
|
||||
hash = hash & hash; // Convert to 32bit integer
|
||||
}
|
||||
return hash;
|
||||
};
|
||||
|
||||
/**
|
||||
* 一个简易的阻塞函数
|
||||
*/
|
||||
export const awaitFor = async (cb: () => boolean, timeout = 0, errText = "超时暂停阻塞") => {
|
||||
const start = Date.now();
|
||||
while (true) {
|
||||
if (cb()) return true;
|
||||
if (timeout && Date.now() - start > timeout) {
|
||||
console.error("阻塞超时: " + errText);
|
||||
return false;
|
||||
}
|
||||
await sleep(100);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,66 @@
|
||||
// Process ~subscript~
|
||||
|
||||
import MarkdownIt, { StateInline, Token } from 'markdown-it';
|
||||
|
||||
// same as UNESCAPE_MD_RE plus a space
|
||||
const UNESCAPE_RE = /\\([ \\!"#$%&'()*+,./:;<=>?@[\]^_`{|}~-])/g;
|
||||
|
||||
function subscript(state: StateInline, silent: boolean): boolean {
|
||||
const max = state.posMax;
|
||||
const start = state.pos;
|
||||
|
||||
if (state.src.charCodeAt(start) !== 0x7E/* ~ */) { return false; }
|
||||
if (silent) { return false; } // don't run any pairs in validation mode
|
||||
if (start + 2 >= max) { return false; }
|
||||
|
||||
state.pos = start + 1;
|
||||
let found = false;
|
||||
|
||||
while (state.pos < max) {
|
||||
if (state.src.charCodeAt(state.pos) === 0x7E/* ~ */) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
|
||||
state.md.inline.skipToken(state);
|
||||
}
|
||||
|
||||
if (!found || start + 1 === state.pos) {
|
||||
state.pos = start;
|
||||
return false;
|
||||
}
|
||||
|
||||
const content = state.src.slice(start + 1, state.pos);
|
||||
|
||||
// don't allow unescaped spaces/newlines inside
|
||||
if (content.match(/(^|[^\\])(\\\\)*\s/)) {
|
||||
state.pos = start;
|
||||
return false;
|
||||
}
|
||||
|
||||
// found!
|
||||
state.posMax = state.pos;
|
||||
state.pos = start + 1;
|
||||
|
||||
// Earlier we checked !silent, but this implementation does not need it
|
||||
const token_so: Token = state.push('sub_open', 'sub', 1);
|
||||
token_so.markup = '~';
|
||||
|
||||
const token_t: Token = state.push('text', '', 0);
|
||||
token_t.content = content.replace(UNESCAPE_RE, '$1');
|
||||
|
||||
const token_sc: Token = state.push('sub_close', 'sub', -1);
|
||||
token_sc.markup = '~';
|
||||
|
||||
state.pos = state.posMax + 1;
|
||||
state.posMax = max;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* markdown-it-sub 插件
|
||||
* 用于支持下标语法 ~text~
|
||||
*/
|
||||
export default function sub_plugin(md: MarkdownIt): void {
|
||||
md.inline.ruler.after('emphasis', 'sub', subscript);
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
// Process ^superscript^
|
||||
|
||||
import MarkdownIt, { StateInline, Token } from 'markdown-it';
|
||||
|
||||
// same as UNESCAPE_MD_RE plus a space
|
||||
const UNESCAPE_RE = /\\([ \\!"#$%&'()*+,./:;<=>?@[\]^_`{|}~-])/g;
|
||||
|
||||
function superscript(state: StateInline, silent: boolean): boolean {
|
||||
const max = state.posMax;
|
||||
const start = state.pos;
|
||||
|
||||
if (state.src.charCodeAt(start) !== 0x5E/* ^ */) { return false; }
|
||||
if (silent) { return false; } // don't run any pairs in validation mode
|
||||
if (start + 2 >= max) { return false; }
|
||||
|
||||
state.pos = start + 1;
|
||||
let found = false;
|
||||
|
||||
while (state.pos < max) {
|
||||
if (state.src.charCodeAt(state.pos) === 0x5E/* ^ */) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
|
||||
state.md.inline.skipToken(state);
|
||||
}
|
||||
|
||||
if (!found || start + 1 === state.pos) {
|
||||
state.pos = start;
|
||||
return false;
|
||||
}
|
||||
|
||||
const content = state.src.slice(start + 1, state.pos);
|
||||
|
||||
// don't allow unescaped spaces/newlines inside
|
||||
if (content.match(/(^|[^\\])(\\\\)*\s/)) {
|
||||
state.pos = start;
|
||||
return false;
|
||||
}
|
||||
|
||||
// found!
|
||||
state.posMax = state.pos;
|
||||
state.pos = start + 1;
|
||||
|
||||
// Earlier we checked !silent, but this implementation does not need it
|
||||
const token_so: Token = state.push('sup_open', 'sup', 1);
|
||||
token_so.markup = '^';
|
||||
|
||||
const token_t: Token = state.push('text', '', 0);
|
||||
token_t.content = content.replace(UNESCAPE_RE, '$1');
|
||||
|
||||
const token_sc: Token = state.push('sup_close', 'sup', -1);
|
||||
token_sc.markup = '^';
|
||||
|
||||
state.pos = state.posMax + 1;
|
||||
state.posMax = max;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* markdown-it-sup 插件
|
||||
* 用于支持上标语法 ^text^
|
||||
*/
|
||||
export default function sup_plugin(md: MarkdownIt): void {
|
||||
md.inline.ruler.after('emphasis', 'sup', superscript);
|
||||
}
|
||||
Reference in New Issue
Block a user