diff --git a/src/block.ts b/src/block.ts index 7bfb000..5a353d5 100644 --- a/src/block.ts +++ b/src/block.ts @@ -50,6 +50,7 @@ const pattNextBarOrTicks = pattern("[^`|\\r\\n]*(?:[|]|`+)"); const pattCaptionStart = pattern("\\^[ \\t]+"); const pattFootnoteStart = pattern("\\[\\^([^\\]]+)\\]:[ \\t\\r\\n]"); const pattThematicBreak = pattern("[-*][ \t]*[-*][ \\t]*[-*][-* \\t]*\\r?\\n"); +const pattCommentFence = pattern("(%%%%*)[ \\t]*\\r?\\n"); const pattDivFence = pattern("(::::*)[ \\t]*\\r?\\n"); const pattDivFenceStart = pattern("(::::*)[ \\t]*"); const pattDivFenceEnd = pattern("([\\w_-]*)[ \\t]*\\r?\\n"); @@ -626,6 +627,50 @@ class EventParser { } }, + { + name: "fenced_comment", + type: ContentType.Block, + content: ContentType.Text, + continue: (container) => { + const m = this.find(container.extra.closePattern); + if (m) { + container.extra.endFenceStartpos = m.startpos; + container.extra.endFenceEndpos = m.startpos + m.captures[0].length - 1; + this.pos = m.endpos; + this.finishedLine = true; + return false; + } else { + return true; + } + }, + open: (spec) => { + const m = this.find(pattCommentFence); + if (m) { + const border = m.captures[0]; + const closePattern = pattern("(" + border + + "%*)[ \\t]*[\\r\\n]"); + this.addContainer(new Container(spec, { closePattern: closePattern })); + this.addMatch(m.startpos, m.startpos + border.length - 1, + "+comment"); + this.pos = m.endpos; + this.finishedLine = true; + return true; + } else { + return false; + } + }, + close: () => { + const container = this.containers.pop(); + if (!container) return; + const sp = container.extra.endFenceStartpos || this.pos; + const ep = container.extra.endFenceEndpos || this.pos; + this.addMatch(sp, ep, "-comment"); + if (sp === ep) { + this.warn(new Warning("Unclosed comment block", this.pos)); + } + } + }, + { name: "fenced_div", type: ContentType.Block, diff --git a/src/functional.spec.ts b/src/functional.spec.ts index 0c4b3d9..9a780ce 100644 --- a/src/functional.spec.ts +++ b/src/functional.spec.ts @@ -11,6 +11,7 @@ const testfiles = [ "symb.test", "emphasis.test", "escapes.test", + "fenced_comments.test", "fenced_divs.test", "footnotes.test", "headings.test", diff --git a/src/parse.ts b/src/parse.ts index beb1f75..5f542a4 100644 --- a/src/parse.ts +++ b/src/parse.ts @@ -1075,6 +1075,18 @@ const parse = function(input: string, options: ParseOptions = {}): Doc { input.substring(startpos, endpos + 1); }, + ["+comment"]: (suffixes, startpos, endpos, pos) => { + pushContainer(pos); + context = Context.Verbatim; + }, + + ["-comment"]: (suffixes, startpos, endpos, pos) => { + popContainer(pos); + // Discard content - comments are not rendered + context = Context.Normal; + accumulatedText = ""; + }, + ["+code_block"]: (suffixes, startpos, endpos, pos) => { pushContainer(pos); context = Context.Verbatim; diff --git a/test/fenced_comments.test b/test/fenced_comments.test new file mode 100644 index 0000000..7a6d2ab --- /dev/null +++ b/test/fenced_comments.test @@ -0,0 +1,54 @@ +Basic fenced comment: + +``` +Before. + +%%% +This is a comment. +%%% + +After. +. +
Before.
+After.
+``` + +Fenced comment with blank lines: + +``` +%%% +First line. + +Second line. +%%% +. +``` + +Fenced comment with longer fence: + +``` +%%%% +%%% not the end +still inside +%%%% +. +``` + +Closing fence must match length: + +``` +%%%% +comment +%%% +still comment +%%%% +. +``` + +Unclosed fenced comment consumes rest of document: + +``` +%%% +This comment never closes. +. +```