diff --git a/src/rich-text/schema.ts b/src/rich-text/schema.ts
index 7a8d2409..a639be09 100644
--- a/src/rich-text/schema.ts
+++ b/src/rich-text/schema.ts
@@ -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. `` or `` but not ` 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 `content` or ``
diff --git a/src/shared/markdown-it/html-comment.ts b/src/shared/markdown-it/html-comment.ts
new file mode 100644
index 00000000..429d7303
--- /dev/null
+++ b/src/shared/markdown-it/html-comment.ts
@@ -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 = //;
+
+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 "" 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);
+}
diff --git a/src/shared/markdown-parser.ts b/src/shared/markdown-parser.ts
index 928c3c5e..a182dade 100644
--- a/src/shared/markdown-parser.ts
+++ b/src/shared/markdown-parser.ts
@@ -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
@@ -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) => ({
@@ -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);
diff --git a/src/shared/markdown-serializer.ts b/src/shared/markdown-serializer.ts
index d1214914..8c91a175 100644
--- a/src/shared/markdown-serializer.ts
+++ b/src/shared/markdown-serializer.ts
@@ -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);
diff --git a/test/shared/markdown-it/html-comment.test.ts b/test/shared/markdown-it/html-comment.test.ts
new file mode 100644
index 00000000..59617ea9
--- /dev/null
+++ b/test/shared/markdown-it/html-comment.test.ts
@@ -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 = "";
+ 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 = ``;
+ 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 = ` `;
+ 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 ",
+ " other text",
+ "
other element
",
+ ])(
+ "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");
+ }
+ );
+});
diff --git a/test/shared/markdown-parser.test.ts b/test/shared/markdown-parser.test.ts
index 75aae185..bc207404 100644
--- a/test/shared/markdown-parser.test.ts
+++ b/test/shared/markdown-parser.test.ts
@@ -52,6 +52,19 @@ describe("SOMarkdownParser", () => {
});
});
+ it("should support html comments", () => {
+ const doc = markdownParser.parse(``);
+ expect(doc).toMatchNodeTree({
+ childCount: 1,
+ content: [
+ {
+ "type.name": "html_comment",
+ "attrs.content": "",
+ },
+ ],
+ });
+ });
+
it.skip("should support single block html without nesting", () => {
const doc = markdownParser.parse("test
");
diff --git a/test/shared/markdown-serializer.test.ts b/test/shared/markdown-serializer.test.ts
index 0d837473..94377925 100644
--- a/test/shared/markdown-serializer.test.ts
+++ b/test/shared/markdown-serializer.test.ts
@@ -160,6 +160,9 @@ describe("markdown-serializer", () => {
/* Tables */
`| foo | bar |\n| --- | --- |\n| baz | bim |`,
`| abc | def | ghi |\n|:---:|:--- | ---:|\n| foo | bar | baz |`,
+ /* Comments */
+ ``,
+ ``,
/* Marks */
`*test*`,
`_test_`,