Skip to content

Commit

Permalink
fix(html-comment): fix html comments from being visible in the rich e…
Browse files Browse the repository at this point in the history
…ditor

fixes StackExchange#195
  • Loading branch information
giamir committed Aug 10, 2022
1 parent 00f189b commit b6ae4c4
Show file tree
Hide file tree
Showing 7 changed files with 184 additions and 1 deletion.
24 changes: 24 additions & 0 deletions src/rich-text/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,30 @@ const nodes: {

pre: genHtmlBlockNodeSpec("pre"),

/**
* Defines an uneditable html_comment node; Only appears when a user has written an html comment block
* i.e. `<!-- comment -->` or `<!-- comment\n continued -->` but not `<!-- comment --> other text`
*/
html_comment: {
content: "text*",
attrs: { content: { default: "" } },
group: "block",
atom: true,
inline: false,
selectable: false,
parseDOM: [{ tag: "div.html_comment" }],
toDOM(node) {
return [
"div",
{
class: "html_comment",
hidden: true,
},
node.attrs.content,
];
},
},

/**
* Defines an uneditable html_block node; Only appears when a user has written a "complicated" html_block
* i.e. anything not resembling `<tag>content</tag>` or `<tag />`
Expand Down
64 changes: 64 additions & 0 deletions src/shared/markdown-it/html-comment.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import MarkdownIt from "markdown-it/lib";
import StateBlock from "markdown-it/lib/rules_block/state_block";

const HTML_COMMENT_OPEN_TAG = /<!--/;
const HTML_COMMENT_CLOSE_TAG = /-->/;

function getLineText(state: StateBlock, line: number): string {
const pos = state.bMarks[line] + state.tShift[line];
const max = state.eMarks[line];
return state.src.slice(pos, max).trim();
}

function html_comment(
state: StateBlock,
startLine: number,
endLine: number,
silent: boolean
) {
if (!state.md.options.html) {
return false;
}

let lineText = getLineText(state, startLine);

// check if the open tag "<!--" is the first element in the line
if (!HTML_COMMENT_OPEN_TAG.test(lineText.slice(0, 4))) {
return false;
}

let nextLine = startLine + 1;
while (nextLine < endLine) {
if (HTML_COMMENT_CLOSE_TAG.test(lineText)) {
break;
}
lineText = getLineText(state, nextLine);
nextLine++;
}

// check if the first close tag "-->" occurence is the last element in the line
if (HTML_COMMENT_CLOSE_TAG.exec(lineText).index + 3 !== lineText.length) {
return false;
}

if (silent) {
return true;
}

state.line = nextLine;

const token = state.push("html_comment", "", 0);
token.map = [startLine, nextLine];
token.content = state.getLines(startLine, nextLine, state.blkIndent, true);

return true;
}

/**
* Parses out HTML comments blocks
* (HTML comments inlined with other text/elements are not parsed by this plugin)
* @param md
*/
export function htmlComment(md: MarkdownIt): void {
md.block.ruler.before("html_block", "html_comment", html_comment);
}
11 changes: 10 additions & 1 deletion src/shared/markdown-parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { spoiler } from "./markdown-it/spoiler";
import { stackLanguageComments } from "./markdown-it/stack-language-comments";
import { tagLinks } from "./markdown-it/tag-link";
import { tight_list } from "./markdown-it/tight-list";
import { htmlComment } from "./markdown-it/html-comment";
import type { CommonmarkParserFeatures } from "./view";

// extend the default markdown parser's tokens and add our own
Expand All @@ -27,7 +28,12 @@ const customMarkdownParserTokens: MarkdownParser["tokens"] = {
content: token.content,
}),
},

html_comment: {
node: "html_comment",
getAttrs: (token: Token) => ({
content: token.content,
}),
},
html_block: {
node: "html_block",
getAttrs: (token: Token) => ({
Expand Down Expand Up @@ -311,6 +317,9 @@ export function createDefaultMarkdownItInstance(
// ensure we can tell the difference between the different types of hardbreaks
defaultMarkdownItInstance.use(hardbreak_markup);

// parse html comments
defaultMarkdownItInstance.use(htmlComment);

// TODO should always exist, so remove the check once the param is made non-optional
externalPluginProvider?.alterMarkdownIt(defaultMarkdownItInstance);

Expand Down
5 changes: 5 additions & 0 deletions src/shared/markdown-serializer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,11 @@ const customMarkdownSerializerNodes: MarkdownSerializerNodes = {
state.write(node.attrs.content as string);
},

html_comment(state, node) {
state.write(node.attrs.content as string);
state.closeBlock(node);
},

html_block(state, node) {
state.write(node.attrs.content as string);
state.closeBlock(node);
Expand Down
65 changes: 65 additions & 0 deletions test/shared/markdown-it/html-comment.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import MarkdownIt from "markdown-it/lib";
import { htmlComment } from "../../../src/shared/markdown-it/html-comment";

function createParser() {
const instance = new MarkdownIt("default", { html: true });
instance.use(htmlComment);
return instance;
}

describe("html-comment markdown-it plugin", () => {
it("should add the html comment block rule to the instance", () => {
const instance = createParser();
const blockRulesNames = instance.block.ruler
.getRules("")
.map((r) => r.name);
expect(blockRulesNames).toContain("html_comment");
});

it("should detect single line html comment blocks", () => {
const singleLineComment = "<!-- an html comment -->";
const instance = createParser();
const tokens = instance.parse(singleLineComment, {});

expect(tokens).toHaveLength(1);
expect(tokens[0].type).toBe("html_comment");
expect(tokens[0].content).toBe(singleLineComment);
expect(tokens[0].map).toEqual([0, 1]);
});

it("should detect multiline html comment blocks", () => {
const multilineComment = `<!-- an html comment\n over multiple lines -->`;
const instance = createParser();
const tokens = instance.parse(multilineComment, {});

expect(tokens).toHaveLength(1);
expect(tokens[0].type).toBe("html_comment");
expect(tokens[0].content).toBe(multilineComment);
expect(tokens[0].map).toEqual([0, 2]);
});

it("should detect indented html comment blocks", () => {
const indentedComment = ` <!--\n an html comment\n 2 space indented\n -->`;
const instance = createParser();
const tokens = instance.parse(indentedComment, {});

expect(tokens).toHaveLength(1);
expect(tokens[0].type).toBe("html_comment");
expect(tokens[0].content).toBe(indentedComment);
expect(tokens[0].map).toEqual([0, 4]);
});

it.each([
"other text <!-- an html comment -->",
"<!-- an html comment --> other text",
"<!-- an html comment --> <div>other element</div> <!-- an html comment -->",
])(
"should ignore html comments inlined with other element/text (test #%#)",
(inlinedHtmlComment) => {
const instance = createParser();
const tokens = instance.parse(inlinedHtmlComment, {});

expect(tokens.map((t) => t.type)).not.toContain("html_comment");
}
);
});
13 changes: 13 additions & 0 deletions test/shared/markdown-parser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,19 @@ describe("SOMarkdownParser", () => {
});
});

it("should support html comments", () => {
const doc = markdownParser.parse(`<!-- an html comment -->`);
expect(doc).toMatchNodeTree({
childCount: 1,
content: [
{
"type.name": "html_comment",
"attrs.content": "<!-- an html comment -->",
},
],
});
});

it.skip("should support single block html without nesting", () => {
const doc = markdownParser.parse("<h1>test</h1>");

Expand Down
3 changes: 3 additions & 0 deletions test/shared/markdown-serializer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,9 @@ describe("markdown-serializer", () => {
/* Tables */
`| foo | bar |\n| --- | --- |\n| baz | bim |`,
`| abc | def | ghi |\n|:---:|:--- | ---:|\n| foo | bar | baz |`,
/* Comments */
`<!-- an html comment -->`,
`<!-- an html comment\n over multiple lines -->`,
/* Marks */
`*test*`,
`_test_`,
Expand Down

0 comments on commit b6ae4c4

Please sign in to comment.