Skip to content

Commit a18ffcc

Browse files
rpaul-stripemfix-stripe
andauthoredSep 20, 2022
Adds comment syntax to Markdoc (#198)
* Adds native support for comments * Prettier * Update src/tokenizer/plugins/comments.ts Co-authored-by: Mike Fix <62121649+mfix-stripe@users.noreply.github.com> * Fixes bug in the comment plugin Co-authored-by: Mike Fix <62121649+mfix-stripe@users.noreply.github.com>
1 parent 7a2e3f8 commit a18ffcc

File tree

8 files changed

+271
-3
lines changed

8 files changed

+271
-3
lines changed
 

‎spec/marktest/index.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,10 @@ class Loader extends yaml.loader.Loader {
1818
}
1919
}
2020

21-
const tokenizer = new markdoc.Tokenizer({ allowIndentation: true });
21+
const tokenizer = new markdoc.Tokenizer({
22+
allowIndentation: true,
23+
allowComments: true,
24+
});
2225

2326
function parse(content: string, file?: string) {
2427
const tokens = tokenizer.tokenize(content);

‎spec/marktest/tests.yaml

+89
Original file line numberDiff line numberDiff line change
@@ -438,6 +438,51 @@
438438
expected: |
439439
<article><h1 class="foo bar">This is a test </h1></article>
440440
441+
- name: Ignoring tags in fenced code blocks
442+
code: |
443+
```javascript {% process=false %}
444+
foo
445+
{% bar %}
446+
```
447+
expected:
448+
- tag: pre
449+
attributes:
450+
data-language: javascript
451+
children:
452+
- "foo\n{% bar %}\n"
453+
454+
- name: Using a backtick in a fenced code block string attribute
455+
config:
456+
nodes:
457+
fence:
458+
render: pre
459+
attributes:
460+
content:
461+
type: String
462+
render: false
463+
required: true
464+
language:
465+
type: String
466+
render: 'data-language'
467+
process:
468+
type: Boolean
469+
render: false
470+
default: true
471+
title:
472+
type: String
473+
474+
code: |
475+
~~~yaml {% title="this is a `test`" %}
476+
example
477+
~~~
478+
expected:
479+
- tag: pre
480+
attributes:
481+
data-language: yaml
482+
title: 'this is a `test`'
483+
children:
484+
- "example\n"
485+
441486
- name: Conditional and variable in code example
442487
config:
443488
variables:
@@ -1503,3 +1548,47 @@
15031548
children:
15041549
- tag: p
15051550
children: [testing]
1551+
1552+
- name: Ignoring comments
1553+
code: |
1554+
# Example <!-- foo -->
1555+
1556+
This is a test <!-- bar
1557+
-->
1558+
1559+
<!--
1560+
baz
1561+
-->
1562+
expected:
1563+
- tag: h1
1564+
children: ['Example ']
1565+
- tag: p
1566+
children: ['This is a test ']
1567+
1568+
- name: Escaped quotes in tag strings
1569+
config:
1570+
tags:
1571+
foo:
1572+
render: foo
1573+
attributes:
1574+
bar:
1575+
type: String
1576+
code: |
1577+
{% foo bar="this is a test of \"quoted\" strings" /%}
1578+
expected:
1579+
- tag: foo
1580+
attributes:
1581+
bar: 'this is a test of "quoted" strings'
1582+
1583+
- name: Escaped quotes in tag strings with html renderer
1584+
renderer: html
1585+
config:
1586+
tags:
1587+
foo:
1588+
render: foo
1589+
attributes:
1590+
bar:
1591+
type: String
1592+
code: |
1593+
{% foo bar="this is a test of \"quoted\" strings" /%}
1594+
expected: <article><foo bar="this is a test of &quot;quoted&quot; strings"></foo></article>

‎src/parser.test.ts

+17-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { any } from 'deep-assert';
77

88
describe('Markdown parser', function () {
99
const fence = '```';
10-
const tokenizer = new Tokenizer();
10+
const tokenizer = new Tokenizer({ allowComments: true });
1111

1212
function convert(example) {
1313
const content = example.replace(/\n\s+/gm, '\n').trim();
@@ -636,4 +636,20 @@ describe('Markdown parser', function () {
636636
`);
637637
}).not.toThrow();
638638
});
639+
640+
it('parsing comments', function () {
641+
const example = convert(`
642+
this is a test
643+
644+
<!-- foo -->
645+
`);
646+
647+
expect(example).toDeepEqualSubset({
648+
type: 'document',
649+
children: [
650+
{ type: 'paragraph' },
651+
{ type: 'comment', attributes: { content: 'foo' } },
652+
],
653+
});
654+
});
639655
});

‎src/parser.ts

+1
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ function handleAttrs(token: Token, type: string) {
4545
}
4646
case 'text':
4747
case 'code':
48+
case 'comment':
4849
return { content: (token.meta || {}).variable || token.content };
4950
case 'fence': {
5051
const [language] = token.info.split(' ', 1);

‎src/schema.ts

+8
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export const document: Schema = {
1111
'tag',
1212
'fence',
1313
'blockquote',
14+
'comment',
1415
'list',
1516
'hr',
1617
],
@@ -189,6 +190,7 @@ export const inline: Schema = {
189190
'image',
190191
'hardbreak',
191192
'softbreak',
193+
'comment',
192194
],
193195
};
194196

@@ -231,5 +233,11 @@ export const softbreak: Schema = {
231233
},
232234
};
233235

236+
export const comment = {
237+
attributes: {
238+
content: { type: String, required: true },
239+
},
240+
};
241+
234242
export const error = {};
235243
export const node = {};

‎src/tokenizer/index.ts

+7-1
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
11
import MarkdownIt from 'markdown-it/lib';
22
import annotations from './plugins/annotations';
33
import frontmatter from './plugins/frontmatter';
4+
import comments from './plugins/comments';
45
import type Token from 'markdown-it/lib/token';
56

67
export default class Tokenizer {
78
private parser: MarkdownIt;
89

910
constructor(
10-
config: MarkdownIt.Options & { allowIndentation?: boolean } = {}
11+
config: MarkdownIt.Options & {
12+
allowIndentation?: boolean;
13+
allowComments?: boolean;
14+
} = {}
1115
) {
1216
this.parser = new MarkdownIt(config);
1317
this.parser.use(annotations, 'annotations', {});
@@ -17,6 +21,8 @@ export default class Tokenizer {
1721
// Disable indented `code_block` support https://spec.commonmark.org/0.30/#indented-code-block
1822
'code',
1923
]);
24+
25+
if (config.allowComments) this.parser.use(comments, 'comments', {});
2026
}
2127

2228
tokenize(content: string): Token[] {
+94
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import Tokenizer from '..';
2+
3+
describe('MarkdownIt Comments plugin', function () {
4+
const tokenizer = new Tokenizer({ allowComments: true });
5+
6+
function parse(example) {
7+
const content = example.replace(/\n\s+/gm, '\n').trim();
8+
return tokenizer.tokenize(content);
9+
}
10+
11+
describe('inline comments', function () {
12+
const output = [
13+
{ type: 'paragraph_open' },
14+
{
15+
type: 'inline',
16+
children: [
17+
{ type: 'text', content: 'this is a test ' },
18+
{ type: 'comment', content: 'example comment' },
19+
{ type: 'text', content: ' foo' },
20+
],
21+
},
22+
{ type: 'paragraph_close' },
23+
];
24+
25+
it('simple inline comment', function () {
26+
const example = parse(`
27+
this is a test <!-- example comment --> foo
28+
`);
29+
30+
expect(example).toDeepEqualSubset(output);
31+
});
32+
33+
it('inline comment with a newline', function () {
34+
const example = parse(`
35+
this is a test <!--
36+
example comment
37+
--> foo
38+
`);
39+
40+
expect(example).toDeepEqualSubset(output);
41+
});
42+
});
43+
44+
describe('block comments', function () {
45+
const output = [
46+
{ type: 'paragraph_open' },
47+
{ type: 'inline' },
48+
{ type: 'paragraph_close' },
49+
{ type: 'comment', content: 'example comment' },
50+
{ type: 'paragraph_open' },
51+
{ type: 'inline', content: 'foo' },
52+
{ type: 'paragraph_close' },
53+
];
54+
55+
it('simple block comment after a paragraph', function () {
56+
const example = parse(`
57+
this is a test
58+
59+
<!--
60+
example comment
61+
-->
62+
63+
foo
64+
`);
65+
66+
expect(example).toDeepEqualSubset(output);
67+
});
68+
69+
it('block comment with ending on same line as content', function () {
70+
const example = parse(`
71+
this is a test
72+
73+
<!--
74+
example comment -->
75+
76+
foo
77+
`);
78+
79+
expect(example).toDeepEqualSubset(output);
80+
});
81+
82+
it('block comment one one line', function () {
83+
const example = parse(`
84+
this is a test
85+
86+
<!-- example comment -->
87+
88+
foo
89+
`);
90+
91+
expect(example).toDeepEqualSubset(output);
92+
});
93+
});
94+
});

‎src/tokenizer/plugins/comments.ts

+51
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import type MarkdownIt from 'markdown-it/lib';
2+
import type StateBlock from 'markdown-it/lib/rules_block/state_block';
3+
import type StateInline from 'markdown-it/lib/rules_inline/state_inline';
4+
5+
const OPEN = '<!--';
6+
const CLOSE = '-->';
7+
8+
function block(
9+
state: StateBlock,
10+
startLine: number,
11+
endLine: number,
12+
silent: boolean
13+
): boolean {
14+
const start = state.bMarks[startLine] + state.tShift[startLine];
15+
if (!state.src.startsWith(OPEN, start)) return false;
16+
17+
const close = state.src.indexOf(CLOSE, start);
18+
19+
if (!close) return false;
20+
if (silent) return true;
21+
22+
const content = state.src.slice(start + OPEN.length, close);
23+
const lines = content.split('\n').length;
24+
const token = state.push('comment', '', 0);
25+
token.content = content.trim();
26+
token.map = [startLine, startLine + lines];
27+
state.line += lines;
28+
29+
return true;
30+
}
31+
32+
function inline(state: StateInline, silent: boolean): boolean {
33+
if (!state.src.startsWith(OPEN, state.pos)) return false;
34+
35+
const close = state.src.indexOf(CLOSE, state.pos);
36+
37+
if (!close) return false;
38+
if (silent) return true;
39+
40+
const content = state.src.slice(state.pos + OPEN.length, close);
41+
const token = state.push('comment', '', 0);
42+
token.content = content.trim();
43+
state.pos = close + CLOSE.length;
44+
45+
return true;
46+
}
47+
48+
export default function plugin(md: MarkdownIt) {
49+
md.block.ruler.before('table', 'comment', block, { alt: ['paragraph'] });
50+
md.inline.ruler.push('comment', inline);
51+
}

0 commit comments

Comments
 (0)
Please sign in to comment.