mirror of
https://github.com/discourse/discourse.git
synced 2025-05-23 21:04:33 +08:00
DEV: allows chat to render specific html tags (#26591)
This commit adds two custom markdown rules: - chat-html-block - chat-html-inline For now it only allows `<kbd>` for inline and `<details>` for block.
This commit is contained in:
@ -0,0 +1,114 @@
|
||||
// inspired from https://github.com/markdown-it/markdown-it/blob/master/lib/rules_block/html_block.mjs
|
||||
// note that allow lister will run on top of it, so if a tag is allowed here but not on
|
||||
// the allow list, then it won't show up
|
||||
|
||||
const block_names = ["details"];
|
||||
|
||||
const attr_name = "[a-zA-Z_:][a-zA-Z0-9:._-]*";
|
||||
const unquoted = "[^\"'=<>`\\x00-\\x20]+";
|
||||
const single_quoted = "'[^']*'";
|
||||
const double_quoted = '"[^"]*"';
|
||||
const attr_value =
|
||||
"(?:" + unquoted + "|" + single_quoted + "|" + double_quoted + ")";
|
||||
const attribute = "(?:\\s+" + attr_name + "(?:\\s*=\\s*" + attr_value + ")?)";
|
||||
const open_tag = "<[A-Za-z][A-Za-z0-9\\-]*" + attribute + "*\\s*\\/?>";
|
||||
const close_tag = "<\\/[A-Za-z][A-Za-z0-9\\-]*\\s*>";
|
||||
|
||||
const HTML_OPEN_CLOSE_TAG_RE = new RegExp(
|
||||
"^(?:" + open_tag + "|" + close_tag + ")"
|
||||
);
|
||||
|
||||
// An array of opening and corresponding closing sequences for html tags,
|
||||
// last argument defines whether it can terminate a paragraph or not
|
||||
//
|
||||
const HTML_SEQUENCES = [
|
||||
[
|
||||
/^<(script|pre|style|textarea)(?=(\s|>|$))/i,
|
||||
/<\/(script|pre|style|textarea)>/i,
|
||||
true,
|
||||
],
|
||||
[/^<!--/, /-->/, true],
|
||||
[/^<\?/, /\?>/, true],
|
||||
[/^<![A-Z]/, />/, true],
|
||||
[/^<!\[CDATA\[/, /\]\]>/, true],
|
||||
[
|
||||
new RegExp("^</?(" + block_names.join("|") + ")(?=(\\s|/?>|$))", "i"),
|
||||
/^$/,
|
||||
true,
|
||||
],
|
||||
[new RegExp(HTML_OPEN_CLOSE_TAG_RE.source + "\\s*$"), /^$/, false],
|
||||
];
|
||||
|
||||
function chatHtmlBlock(state, startLine, endLine, silent) {
|
||||
let pos = state.bMarks[startLine] + state.tShift[startLine];
|
||||
let max = state.eMarks[startLine];
|
||||
|
||||
// if it's indented more than 3 spaces, it should be a code block
|
||||
if (state.sCount[startLine] - state.blkIndent >= 4) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!state.md.options.html) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (state.src.charCodeAt(pos) !== 0x3c /* < */) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let lineText = state.src.slice(pos, max);
|
||||
|
||||
let i = 0;
|
||||
for (; i < HTML_SEQUENCES.length; i++) {
|
||||
if (HTML_SEQUENCES[i][0].test(lineText)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (i === HTML_SEQUENCES.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (silent) {
|
||||
// true if this sequence can be a terminator, false otherwise
|
||||
return HTML_SEQUENCES[i][2];
|
||||
}
|
||||
|
||||
let nextLine = startLine + 1;
|
||||
|
||||
// If we are here - we detected HTML block.
|
||||
// Let's roll down till block end.
|
||||
if (!HTML_SEQUENCES[i][1].test(lineText)) {
|
||||
for (; nextLine < endLine; nextLine++) {
|
||||
if (state.sCount[nextLine] < state.blkIndent) {
|
||||
break;
|
||||
}
|
||||
|
||||
pos = state.bMarks[nextLine] + state.tShift[nextLine];
|
||||
max = state.eMarks[nextLine];
|
||||
lineText = state.src.slice(pos, max);
|
||||
|
||||
if (HTML_SEQUENCES[i][1].test(lineText)) {
|
||||
if (lineText.length !== 0) {
|
||||
nextLine++;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
state.line = nextLine;
|
||||
|
||||
const token = state.push("html_block", "", 0);
|
||||
token.map = [startLine, nextLine];
|
||||
token.content = state.getLines(startLine, nextLine, state.blkIndent, true);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export function setup(helper) {
|
||||
helper.registerPlugin((md) => {
|
||||
if (md.options.discourse.features["chat-html-block"]) {
|
||||
md.block.ruler.push("chat-html-block", chatHtmlBlock);
|
||||
}
|
||||
});
|
||||
}
|
@ -0,0 +1,106 @@
|
||||
// inspired from https://github.com/markdown-it/markdown-it/blob/master/lib/rules_inline/html_inline.mjs
|
||||
// note that allow lister will run on top of it, so if a tag is allowed here but not on
|
||||
// the allow list, then it won't show up
|
||||
|
||||
const inline_names = ["kbd"];
|
||||
|
||||
const patterns = inline_names.join("|");
|
||||
const attr_name = "[a-zA-Z_:][a-zA-Z0-9:._-]*";
|
||||
const unquoted = "[^\"'=<>`\\x00-\\x20]+";
|
||||
const single_quoted = "'[^']*'";
|
||||
const double_quoted = '"[^"]*"';
|
||||
const attr_value =
|
||||
"(?:" + unquoted + "|" + single_quoted + "|" + double_quoted + ")";
|
||||
const attribute = "(?:\\s+" + attr_name + "(?:\\s*=\\s*" + attr_value + ")?)";
|
||||
const open_tag = `<(${patterns})` + attribute + "*\\s*\\/?>";
|
||||
const close_tag = `<\\/(${patterns})\\s*>`;
|
||||
const comment = "<!---?>|<!--(?:[^-]|-[^-]|--[^>])*-->";
|
||||
const processing = "<[?][\\s\\S]*?[?]>";
|
||||
const declaration = "<![A-Za-z][^>]*>";
|
||||
const cdata = "<!\\[CDATA\\[[\\s\\S]*?\\]\\]>";
|
||||
const HTML_TAG_RE = new RegExp(
|
||||
"^(?:" +
|
||||
open_tag +
|
||||
"|" +
|
||||
close_tag +
|
||||
"|" +
|
||||
comment +
|
||||
"|" +
|
||||
processing +
|
||||
"|" +
|
||||
declaration +
|
||||
"|" +
|
||||
cdata +
|
||||
")"
|
||||
);
|
||||
|
||||
function isLinkOpen(str) {
|
||||
return /^<a[>\s]/i.test(str);
|
||||
}
|
||||
function isLinkClose(str) {
|
||||
return /^<\/a\s*>/i.test(str);
|
||||
}
|
||||
|
||||
function isLetter(ch) {
|
||||
/*eslint no-bitwise:0*/
|
||||
let lc = ch | 0x20; // to lower case
|
||||
return lc >= 0x61 /* a */ && lc <= 0x7a /* z */;
|
||||
}
|
||||
|
||||
function chatHtmlInlineRule(state, silent) {
|
||||
let ch,
|
||||
match,
|
||||
max,
|
||||
token,
|
||||
pos = state.pos;
|
||||
|
||||
if (!state.md.options.html) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check start
|
||||
max = state.posMax;
|
||||
if (state.src.charCodeAt(pos) !== 0x3c /* < */ || pos + 2 >= max) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Quick fail on second char
|
||||
ch = state.src.charCodeAt(pos + 1);
|
||||
if (
|
||||
ch !== 0x21 /* ! */ &&
|
||||
ch !== 0x3f /* ? */ &&
|
||||
ch !== 0x2f /* / */ &&
|
||||
!isLetter(ch)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
match = state.src.slice(pos).match(HTML_TAG_RE);
|
||||
|
||||
if (!match) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!silent) {
|
||||
token = state.push("html_inline", "", 0);
|
||||
|
||||
token.content = match[0];
|
||||
|
||||
if (isLinkOpen(token.content)) {
|
||||
state.linkLevel++;
|
||||
}
|
||||
if (isLinkClose(token.content)) {
|
||||
state.linkLevel--;
|
||||
}
|
||||
}
|
||||
state.pos += match[0].length;
|
||||
return true;
|
||||
}
|
||||
|
||||
export function setup(helper) {
|
||||
helper.registerPlugin((md) => {
|
||||
if (md.options.discourse.features["chat-html-inline"]) {
|
||||
md.inline.ruler.push("chat-html-inline", chatHtmlInlineRule);
|
||||
}
|
||||
});
|
||||
}
|
Reference in New Issue
Block a user