mirror of
https://github.com/cinnyapp/cinny.git
synced 2025-11-15 03:30:29 +03:00
Escape markdown sequences (#2208)
* escape inline markdown character * fix typo * improve document around custom markdown plugin and add escape sequence utils * recover inline escape sequences on edit * remove escape sequences from plain text body * use `s` for strike-through instead of del * escape block markdown sequences * fix remove escape sequence was not removing all slashes from plain text * recover block sequences on edit
This commit is contained in:
parent
b63868bbb5
commit
7456c152b7
19 changed files with 764 additions and 476 deletions
|
|
@ -1,368 +0,0 @@
|
|||
export type MatchResult = RegExpMatchArray | RegExpExecArray;
|
||||
export type RuleMatch = (text: string) => MatchResult | null;
|
||||
|
||||
export const beforeMatch = (text: string, match: RegExpMatchArray | RegExpExecArray): string =>
|
||||
text.slice(0, match.index);
|
||||
export const afterMatch = (text: string, match: RegExpMatchArray | RegExpExecArray): string =>
|
||||
text.slice((match.index ?? 0) + match[0].length);
|
||||
|
||||
export const replaceMatch = <C>(
|
||||
convertPart: (txt: string) => Array<string | C>,
|
||||
text: string,
|
||||
match: MatchResult,
|
||||
content: C
|
||||
): Array<string | C> => [
|
||||
...convertPart(beforeMatch(text, match)),
|
||||
content,
|
||||
...convertPart(afterMatch(text, match)),
|
||||
];
|
||||
|
||||
/*
|
||||
*****************
|
||||
* INLINE PARSER *
|
||||
*****************
|
||||
*/
|
||||
|
||||
export type InlineMDParser = (text: string) => string;
|
||||
|
||||
export type InlineMatchConverter = (parse: InlineMDParser, match: MatchResult) => string;
|
||||
|
||||
export type InlineMDRule = {
|
||||
match: RuleMatch;
|
||||
html: InlineMatchConverter;
|
||||
};
|
||||
|
||||
export type InlineRuleRunner = (
|
||||
parse: InlineMDParser,
|
||||
text: string,
|
||||
rule: InlineMDRule
|
||||
) => string | undefined;
|
||||
export type InlineRulesRunner = (
|
||||
parse: InlineMDParser,
|
||||
text: string,
|
||||
rules: InlineMDRule[]
|
||||
) => string | undefined;
|
||||
|
||||
const MIN_ANY = '(.+?)';
|
||||
const URL_NEG_LB = '(?<!(https?|ftp|mailto|magnet):\\/\\/\\S*)';
|
||||
|
||||
const BOLD_MD_1 = '**';
|
||||
const BOLD_PREFIX_1 = '\\*{2}';
|
||||
const BOLD_NEG_LA_1 = '(?!\\*)';
|
||||
const BOLD_REG_1 = new RegExp(
|
||||
`${URL_NEG_LB}${BOLD_PREFIX_1}${MIN_ANY}${BOLD_PREFIX_1}${BOLD_NEG_LA_1}`
|
||||
);
|
||||
const BoldRule: InlineMDRule = {
|
||||
match: (text) => text.match(BOLD_REG_1),
|
||||
html: (parse, match) => {
|
||||
const [, , g2] = match;
|
||||
return `<strong data-md="${BOLD_MD_1}">${parse(g2)}</strong>`;
|
||||
},
|
||||
};
|
||||
|
||||
const ITALIC_MD_1 = '*';
|
||||
const ITALIC_PREFIX_1 = '\\*';
|
||||
const ITALIC_NEG_LA_1 = '(?!\\*)';
|
||||
const ITALIC_REG_1 = new RegExp(
|
||||
`${URL_NEG_LB}${ITALIC_PREFIX_1}${MIN_ANY}${ITALIC_PREFIX_1}${ITALIC_NEG_LA_1}`
|
||||
);
|
||||
const ItalicRule1: InlineMDRule = {
|
||||
match: (text) => text.match(ITALIC_REG_1),
|
||||
html: (parse, match) => {
|
||||
const [, , g2] = match;
|
||||
return `<i data-md="${ITALIC_MD_1}">${parse(g2)}</i>`;
|
||||
},
|
||||
};
|
||||
|
||||
const ITALIC_MD_2 = '_';
|
||||
const ITALIC_PREFIX_2 = '_';
|
||||
const ITALIC_NEG_LA_2 = '(?!_)';
|
||||
const ITALIC_REG_2 = new RegExp(
|
||||
`${URL_NEG_LB}${ITALIC_PREFIX_2}${MIN_ANY}${ITALIC_PREFIX_2}${ITALIC_NEG_LA_2}`
|
||||
);
|
||||
const ItalicRule2: InlineMDRule = {
|
||||
match: (text) => text.match(ITALIC_REG_2),
|
||||
html: (parse, match) => {
|
||||
const [, , g2] = match;
|
||||
return `<i data-md="${ITALIC_MD_2}">${parse(g2)}</i>`;
|
||||
},
|
||||
};
|
||||
|
||||
const UNDERLINE_MD_1 = '__';
|
||||
const UNDERLINE_PREFIX_1 = '_{2}';
|
||||
const UNDERLINE_NEG_LA_1 = '(?!_)';
|
||||
const UNDERLINE_REG_1 = new RegExp(
|
||||
`${URL_NEG_LB}${UNDERLINE_PREFIX_1}${MIN_ANY}${UNDERLINE_PREFIX_1}${UNDERLINE_NEG_LA_1}`
|
||||
);
|
||||
const UnderlineRule: InlineMDRule = {
|
||||
match: (text) => text.match(UNDERLINE_REG_1),
|
||||
html: (parse, match) => {
|
||||
const [, , g2] = match;
|
||||
return `<u data-md="${UNDERLINE_MD_1}">${parse(g2)}</u>`;
|
||||
},
|
||||
};
|
||||
|
||||
const STRIKE_MD_1 = '~~';
|
||||
const STRIKE_PREFIX_1 = '~{2}';
|
||||
const STRIKE_NEG_LA_1 = '(?!~)';
|
||||
const STRIKE_REG_1 = new RegExp(
|
||||
`${URL_NEG_LB}${STRIKE_PREFIX_1}${MIN_ANY}${STRIKE_PREFIX_1}${STRIKE_NEG_LA_1}`
|
||||
);
|
||||
const StrikeRule: InlineMDRule = {
|
||||
match: (text) => text.match(STRIKE_REG_1),
|
||||
html: (parse, match) => {
|
||||
const [, , g2] = match;
|
||||
return `<del data-md="${STRIKE_MD_1}">${parse(g2)}</del>`;
|
||||
},
|
||||
};
|
||||
|
||||
const CODE_MD_1 = '`';
|
||||
const CODE_PREFIX_1 = '`';
|
||||
const CODE_NEG_LA_1 = '(?!`)';
|
||||
const CODE_REG_1 = new RegExp(`${URL_NEG_LB}${CODE_PREFIX_1}(.+?)${CODE_PREFIX_1}${CODE_NEG_LA_1}`);
|
||||
const CodeRule: InlineMDRule = {
|
||||
match: (text) => text.match(CODE_REG_1),
|
||||
html: (parse, match) => {
|
||||
const [, , g2] = match;
|
||||
return `<code data-md="${CODE_MD_1}">${g2}</code>`;
|
||||
},
|
||||
};
|
||||
|
||||
const SPOILER_MD_1 = '||';
|
||||
const SPOILER_PREFIX_1 = '\\|{2}';
|
||||
const SPOILER_NEG_LA_1 = '(?!\\|)';
|
||||
const SPOILER_REG_1 = new RegExp(
|
||||
`${URL_NEG_LB}${SPOILER_PREFIX_1}${MIN_ANY}${SPOILER_PREFIX_1}${SPOILER_NEG_LA_1}`
|
||||
);
|
||||
const SpoilerRule: InlineMDRule = {
|
||||
match: (text) => text.match(SPOILER_REG_1),
|
||||
html: (parse, match) => {
|
||||
const [, , g2] = match;
|
||||
return `<span data-md="${SPOILER_MD_1}" data-mx-spoiler>${parse(g2)}</span>`;
|
||||
},
|
||||
};
|
||||
|
||||
const LINK_ALT = `\\[${MIN_ANY}\\]`;
|
||||
const LINK_URL = `\\((https?:\\/\\/.+?)\\)`;
|
||||
const LINK_REG_1 = new RegExp(`${LINK_ALT}${LINK_URL}`);
|
||||
const LinkRule: InlineMDRule = {
|
||||
match: (text) => text.match(LINK_REG_1),
|
||||
html: (parse, match) => {
|
||||
const [, g1, g2] = match;
|
||||
return `<a data-md href="${g2}">${parse(g1)}</a>`;
|
||||
},
|
||||
};
|
||||
|
||||
const runInlineRule: InlineRuleRunner = (parse, text, rule) => {
|
||||
const matchResult = rule.match(text);
|
||||
if (matchResult) {
|
||||
const content = rule.html(parse, matchResult);
|
||||
return replaceMatch((txt) => [parse(txt)], text, matchResult, content).join('');
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* Runs multiple rules at the same time to better handle nested rules.
|
||||
* Rules will be run in the order they appear.
|
||||
*/
|
||||
const runInlineRules: InlineRulesRunner = (parse, text, rules) => {
|
||||
const matchResults = rules.map((rule) => rule.match(text));
|
||||
|
||||
let targetRule: InlineMDRule | undefined;
|
||||
let targetResult: MatchResult | undefined;
|
||||
|
||||
for (let i = 0; i < matchResults.length; i += 1) {
|
||||
const currentResult = matchResults[i];
|
||||
if (currentResult && typeof currentResult.index === 'number') {
|
||||
if (
|
||||
!targetResult ||
|
||||
(typeof targetResult?.index === 'number' && currentResult.index < targetResult.index)
|
||||
) {
|
||||
targetResult = currentResult;
|
||||
targetRule = rules[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (targetRule && targetResult) {
|
||||
const content = targetRule.html(parse, targetResult);
|
||||
return replaceMatch((txt) => [parse(txt)], text, targetResult, content).join('');
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const LeveledRules = [
|
||||
BoldRule,
|
||||
ItalicRule1,
|
||||
UnderlineRule,
|
||||
ItalicRule2,
|
||||
StrikeRule,
|
||||
SpoilerRule,
|
||||
LinkRule,
|
||||
];
|
||||
|
||||
export const parseInlineMD: InlineMDParser = (text) => {
|
||||
if (text === '') return text;
|
||||
let result: string | undefined;
|
||||
if (!result) result = runInlineRule(parseInlineMD, text, CodeRule);
|
||||
|
||||
if (!result) result = runInlineRules(parseInlineMD, text, LeveledRules);
|
||||
|
||||
return result ?? text;
|
||||
};
|
||||
|
||||
/*
|
||||
****************
|
||||
* BLOCK PARSER *
|
||||
****************
|
||||
*/
|
||||
|
||||
export type BlockMDParser = (test: string, parseInline?: (txt: string) => string) => string;
|
||||
|
||||
export type BlockMatchConverter = (
|
||||
match: MatchResult,
|
||||
parseInline?: (txt: string) => string
|
||||
) => string;
|
||||
|
||||
export type BlockMDRule = {
|
||||
match: RuleMatch;
|
||||
html: BlockMatchConverter;
|
||||
};
|
||||
|
||||
export type BlockRuleRunner = (
|
||||
parse: BlockMDParser,
|
||||
text: string,
|
||||
rule: BlockMDRule,
|
||||
parseInline?: (txt: string) => string
|
||||
) => string | undefined;
|
||||
|
||||
const HEADING_REG_1 = /^(#{1,6}) +(.+)\n?/m;
|
||||
const HeadingRule: BlockMDRule = {
|
||||
match: (text) => text.match(HEADING_REG_1),
|
||||
html: (match, parseInline) => {
|
||||
const [, g1, g2] = match;
|
||||
const level = g1.length;
|
||||
return `<h${level} data-md="${g1}">${parseInline ? parseInline(g2) : g2}</h${level}>`;
|
||||
},
|
||||
};
|
||||
|
||||
const CODEBLOCK_MD_1 = '```';
|
||||
const CODEBLOCK_REG_1 = /^`{3}(\S*)\n((?:.*\n)+?)`{3} *(?!.)\n?/m;
|
||||
const CodeBlockRule: BlockMDRule = {
|
||||
match: (text) => text.match(CODEBLOCK_REG_1),
|
||||
html: (match) => {
|
||||
const [, g1, g2] = match;
|
||||
const classNameAtt = g1 ? ` class="language-${g1}"` : '';
|
||||
return `<pre data-md="${CODEBLOCK_MD_1}"><code${classNameAtt}>${g2}</code></pre>`;
|
||||
},
|
||||
};
|
||||
|
||||
const BLOCKQUOTE_MD_1 = '>';
|
||||
const QUOTE_LINE_PREFIX = /^> */;
|
||||
const BLOCKQUOTE_TRAILING_NEWLINE = /\n$/;
|
||||
const BLOCKQUOTE_REG_1 = /(^>.*\n?)+/m;
|
||||
const BlockQuoteRule: BlockMDRule = {
|
||||
match: (text) => text.match(BLOCKQUOTE_REG_1),
|
||||
html: (match, parseInline) => {
|
||||
const [blockquoteText] = match;
|
||||
|
||||
const lines = blockquoteText
|
||||
.replace(BLOCKQUOTE_TRAILING_NEWLINE, '')
|
||||
.split('\n')
|
||||
.map((lineText) => {
|
||||
const line = lineText.replace(QUOTE_LINE_PREFIX, '');
|
||||
if (parseInline) return `${parseInline(line)}<br/>`;
|
||||
return `${line}<br/>`;
|
||||
})
|
||||
.join('');
|
||||
return `<blockquote data-md="${BLOCKQUOTE_MD_1}">${lines}</blockquote>`;
|
||||
},
|
||||
};
|
||||
|
||||
const ORDERED_LIST_MD_1 = '-';
|
||||
const O_LIST_ITEM_PREFIX = /^(-|[\da-zA-Z]\.) */;
|
||||
const O_LIST_START = /^([\d])\./;
|
||||
const O_LIST_TYPE = /^([aAiI])\./;
|
||||
const O_LIST_TRAILING_NEWLINE = /\n$/;
|
||||
const ORDERED_LIST_REG_1 = /(^(?:-|[\da-zA-Z]\.) +.+\n?)+/m;
|
||||
const OrderedListRule: BlockMDRule = {
|
||||
match: (text) => text.match(ORDERED_LIST_REG_1),
|
||||
html: (match, parseInline) => {
|
||||
const [listText] = match;
|
||||
const [, listStart] = listText.match(O_LIST_START) ?? [];
|
||||
const [, listType] = listText.match(O_LIST_TYPE) ?? [];
|
||||
|
||||
const lines = listText
|
||||
.replace(O_LIST_TRAILING_NEWLINE, '')
|
||||
.split('\n')
|
||||
.map((lineText) => {
|
||||
const line = lineText.replace(O_LIST_ITEM_PREFIX, '');
|
||||
const txt = parseInline ? parseInline(line) : line;
|
||||
return `<li><p>${txt}</p></li>`;
|
||||
})
|
||||
.join('');
|
||||
|
||||
const dataMdAtt = `data-md="${listType || listStart || ORDERED_LIST_MD_1}"`;
|
||||
const startAtt = listStart ? ` start="${listStart}"` : '';
|
||||
const typeAtt = listType ? ` type="${listType}"` : '';
|
||||
return `<ol ${dataMdAtt}${startAtt}${typeAtt}>${lines}</ol>`;
|
||||
},
|
||||
};
|
||||
|
||||
const UNORDERED_LIST_MD_1 = '*';
|
||||
const U_LIST_ITEM_PREFIX = /^\* */;
|
||||
const U_LIST_TRAILING_NEWLINE = /\n$/;
|
||||
const UNORDERED_LIST_REG_1 = /(^\* +.+\n?)+/m;
|
||||
const UnorderedListRule: BlockMDRule = {
|
||||
match: (text) => text.match(UNORDERED_LIST_REG_1),
|
||||
html: (match, parseInline) => {
|
||||
const [listText] = match;
|
||||
|
||||
const lines = listText
|
||||
.replace(U_LIST_TRAILING_NEWLINE, '')
|
||||
.split('\n')
|
||||
.map((lineText) => {
|
||||
const line = lineText.replace(U_LIST_ITEM_PREFIX, '');
|
||||
const txt = parseInline ? parseInline(line) : line;
|
||||
return `<li><p>${txt}</p></li>`;
|
||||
})
|
||||
.join('');
|
||||
|
||||
return `<ul data-md="${UNORDERED_LIST_MD_1}">${lines}</ul>`;
|
||||
},
|
||||
};
|
||||
|
||||
const runBlockRule: BlockRuleRunner = (parse, text, rule, parseInline) => {
|
||||
const matchResult = rule.match(text);
|
||||
if (matchResult) {
|
||||
const content = rule.html(matchResult, parseInline);
|
||||
return replaceMatch((txt) => [parse(txt, parseInline)], text, matchResult, content).join('');
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export const parseBlockMD: BlockMDParser = (text, parseInline) => {
|
||||
if (text === '') return text;
|
||||
let result: string | undefined;
|
||||
|
||||
if (!result) result = runBlockRule(parseBlockMD, text, CodeBlockRule, parseInline);
|
||||
if (!result) result = runBlockRule(parseBlockMD, text, BlockQuoteRule, parseInline);
|
||||
if (!result) result = runBlockRule(parseBlockMD, text, OrderedListRule, parseInline);
|
||||
if (!result) result = runBlockRule(parseBlockMD, text, UnorderedListRule, parseInline);
|
||||
if (!result) result = runBlockRule(parseBlockMD, text, HeadingRule, parseInline);
|
||||
|
||||
// replace \n with <br/> because want to preserve empty lines
|
||||
if (!result) {
|
||||
if (parseInline) {
|
||||
result = text
|
||||
.split('\n')
|
||||
.map((lineText) => parseInline(lineText))
|
||||
.join('<br/>');
|
||||
} else {
|
||||
result = text.replace(/\n/g, '<br/>');
|
||||
}
|
||||
}
|
||||
|
||||
return result ?? text;
|
||||
};
|
||||
1
src/app/plugins/markdown/block/index.ts
Normal file
1
src/app/plugins/markdown/block/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export * from './parser';
|
||||
47
src/app/plugins/markdown/block/parser.ts
Normal file
47
src/app/plugins/markdown/block/parser.ts
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
import { replaceMatch } from '../internal';
|
||||
import {
|
||||
BlockQuoteRule,
|
||||
CodeBlockRule,
|
||||
ESC_BLOCK_SEQ,
|
||||
HeadingRule,
|
||||
OrderedListRule,
|
||||
UnorderedListRule,
|
||||
} from './rules';
|
||||
import { runBlockRule } from './runner';
|
||||
import { BlockMDParser } from './type';
|
||||
|
||||
/**
|
||||
* Parses block-level markdown text into HTML using defined block rules.
|
||||
*
|
||||
* @param text - The markdown text to be parsed.
|
||||
* @param parseInline - Optional function to parse inline elements.
|
||||
* @returns The parsed HTML or the original text if no block-level markdown was found.
|
||||
*/
|
||||
export const parseBlockMD: BlockMDParser = (text, parseInline) => {
|
||||
if (text === '') return text;
|
||||
let result: string | undefined;
|
||||
|
||||
if (!result) result = runBlockRule(text, CodeBlockRule, parseBlockMD, parseInline);
|
||||
if (!result) result = runBlockRule(text, BlockQuoteRule, parseBlockMD, parseInline);
|
||||
if (!result) result = runBlockRule(text, OrderedListRule, parseBlockMD, parseInline);
|
||||
if (!result) result = runBlockRule(text, UnorderedListRule, parseBlockMD, parseInline);
|
||||
if (!result) result = runBlockRule(text, HeadingRule, parseBlockMD, parseInline);
|
||||
|
||||
// replace \n with <br/> because want to preserve empty lines
|
||||
if (!result) {
|
||||
result = text
|
||||
.split('\n')
|
||||
.map((lineText) => {
|
||||
const match = lineText.match(ESC_BLOCK_SEQ);
|
||||
if (!match) {
|
||||
return parseInline?.(lineText) ?? lineText;
|
||||
}
|
||||
|
||||
const [, g1] = match;
|
||||
return replaceMatch(lineText, match, g1, (t) => [parseInline?.(t) ?? t]).join('');
|
||||
})
|
||||
.join('<br/>');
|
||||
}
|
||||
|
||||
return result ?? text;
|
||||
};
|
||||
100
src/app/plugins/markdown/block/rules.ts
Normal file
100
src/app/plugins/markdown/block/rules.ts
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
import { BlockMDRule } from './type';
|
||||
|
||||
const HEADING_REG_1 = /^(#{1,6}) +(.+)\n?/m;
|
||||
export const HeadingRule: BlockMDRule = {
|
||||
match: (text) => text.match(HEADING_REG_1),
|
||||
html: (match, parseInline) => {
|
||||
const [, g1, g2] = match;
|
||||
const level = g1.length;
|
||||
return `<h${level} data-md="${g1}">${parseInline ? parseInline(g2) : g2}</h${level}>`;
|
||||
},
|
||||
};
|
||||
|
||||
const CODEBLOCK_MD_1 = '```';
|
||||
const CODEBLOCK_REG_1 = /^`{3}(\S*)\n((?:.*\n)+?)`{3} *(?!.)\n?/m;
|
||||
export const CodeBlockRule: BlockMDRule = {
|
||||
match: (text) => text.match(CODEBLOCK_REG_1),
|
||||
html: (match) => {
|
||||
const [, g1, g2] = match;
|
||||
const classNameAtt = g1 ? ` class="language-${g1}"` : '';
|
||||
return `<pre data-md="${CODEBLOCK_MD_1}"><code${classNameAtt}>${g2}</code></pre>`;
|
||||
},
|
||||
};
|
||||
|
||||
const BLOCKQUOTE_MD_1 = '>';
|
||||
const QUOTE_LINE_PREFIX = /^> */;
|
||||
const BLOCKQUOTE_TRAILING_NEWLINE = /\n$/;
|
||||
const BLOCKQUOTE_REG_1 = /(^>.*\n?)+/m;
|
||||
export const BlockQuoteRule: BlockMDRule = {
|
||||
match: (text) => text.match(BLOCKQUOTE_REG_1),
|
||||
html: (match, parseInline) => {
|
||||
const [blockquoteText] = match;
|
||||
|
||||
const lines = blockquoteText
|
||||
.replace(BLOCKQUOTE_TRAILING_NEWLINE, '')
|
||||
.split('\n')
|
||||
.map((lineText) => {
|
||||
const line = lineText.replace(QUOTE_LINE_PREFIX, '');
|
||||
if (parseInline) return `${parseInline(line)}<br/>`;
|
||||
return `${line}<br/>`;
|
||||
})
|
||||
.join('');
|
||||
return `<blockquote data-md="${BLOCKQUOTE_MD_1}">${lines}</blockquote>`;
|
||||
},
|
||||
};
|
||||
|
||||
const ORDERED_LIST_MD_1 = '-';
|
||||
const O_LIST_ITEM_PREFIX = /^(-|[\da-zA-Z]\.) */;
|
||||
const O_LIST_START = /^([\d])\./;
|
||||
const O_LIST_TYPE = /^([aAiI])\./;
|
||||
const O_LIST_TRAILING_NEWLINE = /\n$/;
|
||||
const ORDERED_LIST_REG_1 = /(^(?:-|[\da-zA-Z]\.) +.+\n?)+/m;
|
||||
export const OrderedListRule: BlockMDRule = {
|
||||
match: (text) => text.match(ORDERED_LIST_REG_1),
|
||||
html: (match, parseInline) => {
|
||||
const [listText] = match;
|
||||
const [, listStart] = listText.match(O_LIST_START) ?? [];
|
||||
const [, listType] = listText.match(O_LIST_TYPE) ?? [];
|
||||
|
||||
const lines = listText
|
||||
.replace(O_LIST_TRAILING_NEWLINE, '')
|
||||
.split('\n')
|
||||
.map((lineText) => {
|
||||
const line = lineText.replace(O_LIST_ITEM_PREFIX, '');
|
||||
const txt = parseInline ? parseInline(line) : line;
|
||||
return `<li><p>${txt}</p></li>`;
|
||||
})
|
||||
.join('');
|
||||
|
||||
const dataMdAtt = `data-md="${listType || listStart || ORDERED_LIST_MD_1}"`;
|
||||
const startAtt = listStart ? ` start="${listStart}"` : '';
|
||||
const typeAtt = listType ? ` type="${listType}"` : '';
|
||||
return `<ol ${dataMdAtt}${startAtt}${typeAtt}>${lines}</ol>`;
|
||||
},
|
||||
};
|
||||
|
||||
const UNORDERED_LIST_MD_1 = '*';
|
||||
const U_LIST_ITEM_PREFIX = /^\* */;
|
||||
const U_LIST_TRAILING_NEWLINE = /\n$/;
|
||||
const UNORDERED_LIST_REG_1 = /(^\* +.+\n?)+/m;
|
||||
export const UnorderedListRule: BlockMDRule = {
|
||||
match: (text) => text.match(UNORDERED_LIST_REG_1),
|
||||
html: (match, parseInline) => {
|
||||
const [listText] = match;
|
||||
|
||||
const lines = listText
|
||||
.replace(U_LIST_TRAILING_NEWLINE, '')
|
||||
.split('\n')
|
||||
.map((lineText) => {
|
||||
const line = lineText.replace(U_LIST_ITEM_PREFIX, '');
|
||||
const txt = parseInline ? parseInline(line) : line;
|
||||
return `<li><p>${txt}</p></li>`;
|
||||
})
|
||||
.join('');
|
||||
|
||||
return `<ul data-md="${UNORDERED_LIST_MD_1}">${lines}</ul>`;
|
||||
},
|
||||
};
|
||||
|
||||
export const UN_ESC_BLOCK_SEQ = /^\\*(#{1,6} +|```|>|(-|[\da-zA-Z]\.) +|\* +)/;
|
||||
export const ESC_BLOCK_SEQ = /^\\(\\*(#{1,6} +|```|>|(-|[\da-zA-Z]\.) +|\* +))/;
|
||||
25
src/app/plugins/markdown/block/runner.ts
Normal file
25
src/app/plugins/markdown/block/runner.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import { replaceMatch } from '../internal';
|
||||
import { BlockMDParser, BlockMDRule } from './type';
|
||||
|
||||
/**
|
||||
* Parses block-level markdown text into HTML using defined block rules.
|
||||
*
|
||||
* @param text - The text to parse.
|
||||
* @param rule - The markdown rule to run.
|
||||
* @param parse - A function that run the parser on remaining parts..
|
||||
* @param parseInline - Optional function to parse inline elements.
|
||||
* @returns The text with the markdown rule applied or `undefined` if no match is found.
|
||||
*/
|
||||
export const runBlockRule = (
|
||||
text: string,
|
||||
rule: BlockMDRule,
|
||||
parse: BlockMDParser,
|
||||
parseInline?: (txt: string) => string
|
||||
): string | undefined => {
|
||||
const matchResult = rule.match(text);
|
||||
if (matchResult) {
|
||||
const content = rule.html(matchResult, parseInline);
|
||||
return replaceMatch(text, matchResult, content, (txt) => [parse(txt, parseInline)]).join('');
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
30
src/app/plugins/markdown/block/type.ts
Normal file
30
src/app/plugins/markdown/block/type.ts
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
import { MatchResult, MatchRule } from '../internal';
|
||||
|
||||
/**
|
||||
* Type for a function that parses block-level markdown into HTML.
|
||||
*
|
||||
* @param text - The markdown text to be parsed.
|
||||
* @param parseInline - Optional function to parse inline elements.
|
||||
* @returns The parsed HTML.
|
||||
*/
|
||||
export type BlockMDParser = (text: string, parseInline?: (txt: string) => string) => string;
|
||||
|
||||
/**
|
||||
* Type for a function that converts a block match to output.
|
||||
*
|
||||
* @param match - The match result.
|
||||
* @param parseInline - Optional function to parse inline elements.
|
||||
* @returns The output string after processing the match.
|
||||
*/
|
||||
export type BlockMatchConverter = (
|
||||
match: MatchResult,
|
||||
parseInline?: (txt: string) => string
|
||||
) => string;
|
||||
|
||||
/**
|
||||
* Type representing a block-level markdown rule that includes a matching pattern and HTML conversion.
|
||||
*/
|
||||
export type BlockMDRule = {
|
||||
match: MatchRule; // A function that matches a specific markdown pattern.
|
||||
html: BlockMatchConverter; // A function that converts the match to HTML.
|
||||
};
|
||||
3
src/app/plugins/markdown/index.ts
Normal file
3
src/app/plugins/markdown/index.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export * from './utils';
|
||||
export * from './block';
|
||||
export * from './inline';
|
||||
1
src/app/plugins/markdown/inline/index.ts
Normal file
1
src/app/plugins/markdown/inline/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export * from './parser';
|
||||
40
src/app/plugins/markdown/inline/parser.ts
Normal file
40
src/app/plugins/markdown/inline/parser.ts
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
import {
|
||||
BoldRule,
|
||||
CodeRule,
|
||||
EscapeRule,
|
||||
ItalicRule1,
|
||||
ItalicRule2,
|
||||
LinkRule,
|
||||
SpoilerRule,
|
||||
StrikeRule,
|
||||
UnderlineRule,
|
||||
} from './rules';
|
||||
import { runInlineRule, runInlineRules } from './runner';
|
||||
import { InlineMDParser } from './type';
|
||||
|
||||
const LeveledRules = [
|
||||
BoldRule,
|
||||
ItalicRule1,
|
||||
UnderlineRule,
|
||||
ItalicRule2,
|
||||
StrikeRule,
|
||||
SpoilerRule,
|
||||
LinkRule,
|
||||
EscapeRule,
|
||||
];
|
||||
|
||||
/**
|
||||
* Parses inline markdown text into HTML using defined rules.
|
||||
*
|
||||
* @param text - The markdown text to be parsed.
|
||||
* @returns The parsed HTML or the original text if no markdown was found.
|
||||
*/
|
||||
export const parseInlineMD: InlineMDParser = (text) => {
|
||||
if (text === '') return text;
|
||||
let result: string | undefined;
|
||||
if (!result) result = runInlineRule(text, CodeRule, parseInlineMD);
|
||||
|
||||
if (!result) result = runInlineRules(text, LeveledRules, parseInlineMD);
|
||||
|
||||
return result ?? text;
|
||||
};
|
||||
123
src/app/plugins/markdown/inline/rules.ts
Normal file
123
src/app/plugins/markdown/inline/rules.ts
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
import { InlineMDRule } from './type';
|
||||
|
||||
const MIN_ANY = '(.+?)';
|
||||
const URL_NEG_LB = '(?<!(https?|ftp|mailto|magnet):\\/\\/\\S*)';
|
||||
const ESC_NEG_LB = '(?<!\\\\)';
|
||||
|
||||
const BOLD_MD_1 = '**';
|
||||
const BOLD_PREFIX_1 = `${ESC_NEG_LB}\\*{2}`;
|
||||
const BOLD_NEG_LA_1 = '(?!\\*)';
|
||||
const BOLD_REG_1 = new RegExp(
|
||||
`${URL_NEG_LB}${BOLD_PREFIX_1}${MIN_ANY}${BOLD_PREFIX_1}${BOLD_NEG_LA_1}`
|
||||
);
|
||||
export const BoldRule: InlineMDRule = {
|
||||
match: (text) => text.match(BOLD_REG_1),
|
||||
html: (parse, match) => {
|
||||
const [, , g2] = match;
|
||||
return `<strong data-md="${BOLD_MD_1}">${parse(g2)}</strong>`;
|
||||
},
|
||||
};
|
||||
|
||||
const ITALIC_MD_1 = '*';
|
||||
const ITALIC_PREFIX_1 = `${ESC_NEG_LB}\\*`;
|
||||
const ITALIC_NEG_LA_1 = '(?!\\*)';
|
||||
const ITALIC_REG_1 = new RegExp(
|
||||
`${URL_NEG_LB}${ITALIC_PREFIX_1}${MIN_ANY}${ITALIC_PREFIX_1}${ITALIC_NEG_LA_1}`
|
||||
);
|
||||
export const ItalicRule1: InlineMDRule = {
|
||||
match: (text) => text.match(ITALIC_REG_1),
|
||||
html: (parse, match) => {
|
||||
const [, , g2] = match;
|
||||
return `<i data-md="${ITALIC_MD_1}">${parse(g2)}</i>`;
|
||||
},
|
||||
};
|
||||
|
||||
const ITALIC_MD_2 = '_';
|
||||
const ITALIC_PREFIX_2 = `${ESC_NEG_LB}_`;
|
||||
const ITALIC_NEG_LA_2 = '(?!_)';
|
||||
const ITALIC_REG_2 = new RegExp(
|
||||
`${URL_NEG_LB}${ITALIC_PREFIX_2}${MIN_ANY}${ITALIC_PREFIX_2}${ITALIC_NEG_LA_2}`
|
||||
);
|
||||
export const ItalicRule2: InlineMDRule = {
|
||||
match: (text) => text.match(ITALIC_REG_2),
|
||||
html: (parse, match) => {
|
||||
const [, , g2] = match;
|
||||
return `<i data-md="${ITALIC_MD_2}">${parse(g2)}</i>`;
|
||||
},
|
||||
};
|
||||
|
||||
const UNDERLINE_MD_1 = '__';
|
||||
const UNDERLINE_PREFIX_1 = `${ESC_NEG_LB}_{2}`;
|
||||
const UNDERLINE_NEG_LA_1 = '(?!_)';
|
||||
const UNDERLINE_REG_1 = new RegExp(
|
||||
`${URL_NEG_LB}${UNDERLINE_PREFIX_1}${MIN_ANY}${UNDERLINE_PREFIX_1}${UNDERLINE_NEG_LA_1}`
|
||||
);
|
||||
export const UnderlineRule: InlineMDRule = {
|
||||
match: (text) => text.match(UNDERLINE_REG_1),
|
||||
html: (parse, match) => {
|
||||
const [, , g2] = match;
|
||||
return `<u data-md="${UNDERLINE_MD_1}">${parse(g2)}</u>`;
|
||||
},
|
||||
};
|
||||
|
||||
const STRIKE_MD_1 = '~~';
|
||||
const STRIKE_PREFIX_1 = `${ESC_NEG_LB}~{2}`;
|
||||
const STRIKE_NEG_LA_1 = '(?!~)';
|
||||
const STRIKE_REG_1 = new RegExp(
|
||||
`${URL_NEG_LB}${STRIKE_PREFIX_1}${MIN_ANY}${STRIKE_PREFIX_1}${STRIKE_NEG_LA_1}`
|
||||
);
|
||||
export const StrikeRule: InlineMDRule = {
|
||||
match: (text) => text.match(STRIKE_REG_1),
|
||||
html: (parse, match) => {
|
||||
const [, , g2] = match;
|
||||
return `<s data-md="${STRIKE_MD_1}">${parse(g2)}</s>`;
|
||||
},
|
||||
};
|
||||
|
||||
const CODE_MD_1 = '`';
|
||||
const CODE_PREFIX_1 = `${ESC_NEG_LB}\``;
|
||||
const CODE_NEG_LA_1 = '(?!`)';
|
||||
const CODE_REG_1 = new RegExp(`${URL_NEG_LB}${CODE_PREFIX_1}(.+?)${CODE_PREFIX_1}${CODE_NEG_LA_1}`);
|
||||
export const CodeRule: InlineMDRule = {
|
||||
match: (text) => text.match(CODE_REG_1),
|
||||
html: (parse, match) => {
|
||||
const [, , g2] = match;
|
||||
return `<code data-md="${CODE_MD_1}">${g2}</code>`;
|
||||
},
|
||||
};
|
||||
|
||||
const SPOILER_MD_1 = '||';
|
||||
const SPOILER_PREFIX_1 = `${ESC_NEG_LB}\\|{2}`;
|
||||
const SPOILER_NEG_LA_1 = '(?!\\|)';
|
||||
const SPOILER_REG_1 = new RegExp(
|
||||
`${URL_NEG_LB}${SPOILER_PREFIX_1}${MIN_ANY}${SPOILER_PREFIX_1}${SPOILER_NEG_LA_1}`
|
||||
);
|
||||
export const SpoilerRule: InlineMDRule = {
|
||||
match: (text) => text.match(SPOILER_REG_1),
|
||||
html: (parse, match) => {
|
||||
const [, , g2] = match;
|
||||
return `<span data-md="${SPOILER_MD_1}" data-mx-spoiler>${parse(g2)}</span>`;
|
||||
},
|
||||
};
|
||||
|
||||
const LINK_ALT = `\\[${MIN_ANY}\\]`;
|
||||
const LINK_URL = `\\((https?:\\/\\/.+?)\\)`;
|
||||
const LINK_REG_1 = new RegExp(`${LINK_ALT}${LINK_URL}`);
|
||||
export const LinkRule: InlineMDRule = {
|
||||
match: (text) => text.match(LINK_REG_1),
|
||||
html: (parse, match) => {
|
||||
const [, g1, g2] = match;
|
||||
return `<a data-md href="${g2}">${parse(g1)}</a>`;
|
||||
},
|
||||
};
|
||||
|
||||
export const INLINE_SEQUENCE_SET = '[*_~`|]';
|
||||
const ESC_SEQ_1 = `\\\\(${INLINE_SEQUENCE_SET})`;
|
||||
const ESC_REG_1 = new RegExp(`${URL_NEG_LB}${ESC_SEQ_1}`);
|
||||
export const EscapeRule: InlineMDRule = {
|
||||
match: (text) => text.match(ESC_REG_1),
|
||||
html: (parse, match) => {
|
||||
const [, , g2] = match;
|
||||
return g2;
|
||||
},
|
||||
};
|
||||
62
src/app/plugins/markdown/inline/runner.ts
Normal file
62
src/app/plugins/markdown/inline/runner.ts
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
import { MatchResult, replaceMatch } from '../internal';
|
||||
import { InlineMDParser, InlineMDRule } from './type';
|
||||
|
||||
/**
|
||||
* Runs a single markdown rule on the provided text.
|
||||
*
|
||||
* @param text - The text to parse.
|
||||
* @param rule - The markdown rule to run.
|
||||
* @param parse - A function that run the parser on remaining parts.
|
||||
* @returns The text with the markdown rule applied or `undefined` if no match is found.
|
||||
*/
|
||||
export const runInlineRule = (
|
||||
text: string,
|
||||
rule: InlineMDRule,
|
||||
parse: InlineMDParser
|
||||
): string | undefined => {
|
||||
const matchResult = rule.match(text);
|
||||
if (matchResult) {
|
||||
const content = rule.html(parse, matchResult);
|
||||
return replaceMatch(text, matchResult, content, (txt) => [parse(txt)]).join('');
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* Runs multiple rules at the same time to better handle nested rules.
|
||||
* Rules will be run in the order they appear.
|
||||
*
|
||||
* @param text - The text to parse.
|
||||
* @param rules - The markdown rules to run.
|
||||
* @param parse - A function that run the parser on remaining parts.
|
||||
* @returns The text with the markdown rules applied or `undefined` if no match is found.
|
||||
*/
|
||||
export const runInlineRules = (
|
||||
text: string,
|
||||
rules: InlineMDRule[],
|
||||
parse: InlineMDParser
|
||||
): string | undefined => {
|
||||
const matchResults = rules.map((rule) => rule.match(text));
|
||||
|
||||
let targetRule: InlineMDRule | undefined;
|
||||
let targetResult: MatchResult | undefined;
|
||||
|
||||
for (let i = 0; i < matchResults.length; i += 1) {
|
||||
const currentResult = matchResults[i];
|
||||
if (currentResult && typeof currentResult.index === 'number') {
|
||||
if (
|
||||
!targetResult ||
|
||||
(typeof targetResult?.index === 'number' && currentResult.index < targetResult.index)
|
||||
) {
|
||||
targetResult = currentResult;
|
||||
targetRule = rules[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (targetRule && targetResult) {
|
||||
const content = targetRule.html(parse, targetResult);
|
||||
return replaceMatch(text, targetResult, content, (txt) => [parse(txt)]).join('');
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
26
src/app/plugins/markdown/inline/type.ts
Normal file
26
src/app/plugins/markdown/inline/type.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import { MatchResult, MatchRule } from '../internal';
|
||||
|
||||
/**
|
||||
* Type for a function that parses inline markdown into HTML.
|
||||
*
|
||||
* @param text - The markdown text to be parsed.
|
||||
* @returns The parsed HTML.
|
||||
*/
|
||||
export type InlineMDParser = (text: string) => string;
|
||||
|
||||
/**
|
||||
* Type for a function that converts a match to output.
|
||||
*
|
||||
* @param parse - The inline markdown parser function.
|
||||
* @param match - The match result.
|
||||
* @returns The output string after processing the match.
|
||||
*/
|
||||
export type InlineMatchConverter = (parse: InlineMDParser, match: MatchResult) => string;
|
||||
|
||||
/**
|
||||
* Type representing a markdown rule that includes a matching pattern and HTML conversion.
|
||||
*/
|
||||
export type InlineMDRule = {
|
||||
match: MatchRule; // A function that matches a specific markdown pattern.
|
||||
html: InlineMatchConverter; // A function that converts the match to HTML.
|
||||
};
|
||||
1
src/app/plugins/markdown/internal/index.ts
Normal file
1
src/app/plugins/markdown/internal/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export * from './utils';
|
||||
61
src/app/plugins/markdown/internal/utils.ts
Normal file
61
src/app/plugins/markdown/internal/utils.ts
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
/**
|
||||
* @typedef {RegExpMatchArray | RegExpExecArray} MatchResult
|
||||
*
|
||||
* Represents the result of a regular expression match.
|
||||
* This type can be either a `RegExpMatchArray` or a `RegExpExecArray`,
|
||||
* which are returned when performing a match with a regular expression.
|
||||
*
|
||||
* @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/exec}
|
||||
* @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/match}
|
||||
*/
|
||||
export type MatchResult = RegExpMatchArray | RegExpExecArray;
|
||||
|
||||
/**
|
||||
* @typedef {function(string): MatchResult | null} MatchRule
|
||||
*
|
||||
* A function type that takes a string and returns a `MatchResult` or `null` if no match is found.
|
||||
*
|
||||
* @param {string} text The string to match against.
|
||||
* @returns {MatchResult | null} The result of the regular expression match, or `null` if no match is found.
|
||||
*/
|
||||
export type MatchRule = (text: string) => MatchResult | null;
|
||||
|
||||
/**
|
||||
* Returns the part of the text before a match.
|
||||
*
|
||||
* @param text - The input text string.
|
||||
* @param match - The match result (e.g., `RegExpMatchArray` or `RegExpExecArray`).
|
||||
* @returns A string containing the part of the text before the match.
|
||||
*/
|
||||
export const beforeMatch = (text: string, match: RegExpMatchArray | RegExpExecArray): string =>
|
||||
text.slice(0, match.index);
|
||||
|
||||
/**
|
||||
* Returns the part of the text after a match.
|
||||
*
|
||||
* @param text - The input text string.
|
||||
* @param match - The match result (e.g., `RegExpMatchArray` or `RegExpExecArray`).
|
||||
* @returns A string containing the part of the text after the match.
|
||||
*/
|
||||
export const afterMatch = (text: string, match: RegExpMatchArray | RegExpExecArray): string =>
|
||||
text.slice((match.index ?? 0) + match[0].length);
|
||||
|
||||
/**
|
||||
* Replaces a match in the text with a content.
|
||||
*
|
||||
* @param text - The input text string.
|
||||
* @param match - The match result (e.g., `RegExpMatchArray` or `RegExpExecArray`).
|
||||
* @param content - The content to replace the match with.
|
||||
* @param processPart - A function to further process remaining parts of the text.
|
||||
* @returns An array containing the processed parts of the text, including the content.
|
||||
*/
|
||||
export const replaceMatch = <C>(
|
||||
text: string,
|
||||
match: MatchResult,
|
||||
content: C,
|
||||
processPart: (txt: string) => Array<string | C>
|
||||
): Array<string | C> => [
|
||||
...processPart(beforeMatch(text, match)),
|
||||
content,
|
||||
...processPart(afterMatch(text, match)),
|
||||
];
|
||||
83
src/app/plugins/markdown/utils.ts
Normal file
83
src/app/plugins/markdown/utils.ts
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
import { findAndReplace } from '../../utils/findAndReplace';
|
||||
import { ESC_BLOCK_SEQ, UN_ESC_BLOCK_SEQ } from './block/rules';
|
||||
import { EscapeRule, INLINE_SEQUENCE_SET } from './inline/rules';
|
||||
import { runInlineRule } from './inline/runner';
|
||||
import { replaceMatch } from './internal';
|
||||
|
||||
/**
|
||||
* Removes escape sequences from markdown inline elements in the given plain-text.
|
||||
* This function unescapes characters that are escaped with backslashes (e.g., `\*`, `\_`)
|
||||
* in markdown syntax, returning the original plain-text with markdown characters in effect.
|
||||
*
|
||||
* @param text - The input markdown plain-text containing escape characters (e.g., `"some \*italic\*"`)
|
||||
* @returns The plain-text with markdown escape sequences removed (e.g., `"some *italic*"`)
|
||||
*/
|
||||
export const unescapeMarkdownInlineSequences = (text: string): string =>
|
||||
runInlineRule(text, EscapeRule, (t) => {
|
||||
if (t === '') return t;
|
||||
return unescapeMarkdownInlineSequences(t);
|
||||
}) ?? text;
|
||||
|
||||
/**
|
||||
* Recovers the markdown escape sequences in the given plain-text.
|
||||
* This function adds backslashes (`\`) before markdown characters that may need escaping
|
||||
* (e.g., `*`, `_`) to ensure they are treated as literal characters and not part of markdown formatting.
|
||||
*
|
||||
* @param text - The input plain-text that may contain markdown sequences (e.g., `"some *italic*"`)
|
||||
* @returns The plain-text with markdown escape sequences added (e.g., `"some \*italic\*"`)
|
||||
*/
|
||||
export const escapeMarkdownInlineSequences = (text: string): string => {
|
||||
const regex = new RegExp(`(${INLINE_SEQUENCE_SET})`, 'g');
|
||||
const parts = findAndReplace(
|
||||
text,
|
||||
regex,
|
||||
(match) => {
|
||||
const [, g1] = match;
|
||||
return `\\${g1}`;
|
||||
},
|
||||
(t) => t
|
||||
);
|
||||
|
||||
return parts.join('');
|
||||
};
|
||||
|
||||
/**
|
||||
* Removes escape sequences from markdown block elements in the given plain-text.
|
||||
* This function unescapes characters that are escaped with backslashes (e.g., `\>`, `\#`)
|
||||
* in markdown syntax, returning the original plain-text with markdown characters in effect.
|
||||
*
|
||||
* @param {string} text - The input markdown plain-text containing escape characters (e.g., `\> block quote`).
|
||||
* @param {function} processPart - It takes the plain-text as input and returns a modified version of it.
|
||||
* @returns {string} The plain-text with markdown escape sequences removed and markdown formatting applied.
|
||||
*/
|
||||
export const unescapeMarkdownBlockSequences = (
|
||||
text: string,
|
||||
processPart: (text: string) => string
|
||||
): string => {
|
||||
const match = text.match(ESC_BLOCK_SEQ);
|
||||
|
||||
if (!match) return processPart(text);
|
||||
|
||||
const [, g1] = match;
|
||||
return replaceMatch(text, match, g1, (t) => [processPart(t)]).join('');
|
||||
};
|
||||
|
||||
/**
|
||||
* Escapes markdown block elements by adding backslashes before markdown characters
|
||||
* (e.g., `\>`, `\#`) that are normally interpreted as markdown syntax.
|
||||
*
|
||||
* @param {string} text - The input markdown plain-text that may contain markdown elements (e.g., `> block quote`).
|
||||
* @param {function} processPart - It takes the plain-text as input and returns a modified version of it.
|
||||
* @returns {string} The plain-text with markdown escape sequences added, preventing markdown formatting.
|
||||
*/
|
||||
export const escapeMarkdownBlockSequences = (
|
||||
text: string,
|
||||
processPart: (text: string) => string
|
||||
): string => {
|
||||
const match = text.match(UN_ESC_BLOCK_SEQ);
|
||||
|
||||
if (!match) return processPart(text);
|
||||
|
||||
const [, g1] = match;
|
||||
return replaceMatch(text, match, `\\${g1}`, (t) => [processPart(t)]).join('');
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue