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:
@@ -0,0 +1 @@
|
||||
export * from './parser';
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
},
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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.
|
||||
};
|
||||
Reference in New Issue
Block a user