Skip to content

Commit ee30198

Browse files
committed
[remark-ping] Migrate to micromark
1 parent 188befe commit ee30198

File tree

17 files changed

+1266
-2118
lines changed

17 files changed

+1266
-2118
lines changed

package-lock.json

Lines changed: 421 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@
5757
},
5858
"scripts": {
5959
"pretest": "lerna run pretest --scope zmarkdown",
60-
"test": "cross-env NODE_OPTIONS=--experimental-vm-modules DEST=/tmp jest packages/remark-kbd packages/remark-iframes packages/micromark-extension-kbd packages/micromark-extension-iframes",
60+
"test": "cross-env NODE_OPTIONS=--experimental-vm-modules DEST=/tmp jest packages/remark-kbd packages/remark-iframes packages/remark-ping packages/micromark-extension-kbd packages/micromark-extension-iframes packages/micromark-extension-ping",
6161
"lint": "eslint .",
6262
"posttest": "lerna run posttest --scope zmarkdown",
6363
"build": "lerna run build",
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
__tests__/
2+
specs/
3+
.npmignore
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# `micromark-extension-ping`
2+
3+
**[micromark][]** extension that parses custom Markdown syntax to handle
4+
user mentions, or pings.
5+
This syntax extension follows a [specification][spec];
6+
in short, you can ping an user with the syntax `@user`.
7+
For usernames containing a space, use the alternative syntax `@**user space**`.
8+
9+
This package provides the low-level modules for integrating with the micromark
10+
tokenizer and the micromark HTML compiler.
11+
12+
## Install
13+
14+
[npm][]:
15+
16+
```sh
17+
npm install micromark-extension-ping
18+
```
19+
20+
## API
21+
22+
### `html`
23+
24+
### `syntax(options?)`
25+
26+
> Note: `syntax` is the default export of this module, `html` is available at
27+
> `micromark-extension-ping/lib/html`.
28+
29+
Support custom syntax to handle user mentions.
30+
The export of `syntax` is a function that can be called with options and returns
31+
an extension for the micromark parser (to tokenize user mentions; can be passed
32+
in `extensions`).
33+
The export of `html` is an extension for the default HTML compiler (to compile
34+
as `<a href="/@user">` elements; can be passed in `htmlExtensions`).
35+
36+
##### `options`
37+
38+
- `options.pingChar`: the pipe character used to ping a simple user name. Defaults to `@`.
39+
- `options.sequenceChar`: the star character added to ping user names containing a space. Defaults to `*` (star character).
40+
41+
## License
42+
43+
[MIT][license] © [Zeste de Savoir][zds]
44+
45+
<!-- Definitions -->
46+
47+
[license]: LICENCE
48+
49+
[micromark]: https://github.com/micromark/micromark
50+
51+
[npm]: https://docs.npmjs.com/cli/install
52+
53+
[spec]: specs/extension.md
54+
55+
[zds]: https://zestedesavoir.com
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { micromark } from 'micromark'
2+
import micromarkPing from '../lib/index'
3+
import micromarkPingHtml from '../lib/html'
4+
5+
const specificationTests = {
6+
'works': ['@foo', '<p><a href="/@foo">@foo</a></p>'],
7+
'with stars': ['@**foo bar**', '<p><a href="/@foo%20bar">@foo bar</a></p>'],
8+
'opening with two stars': ['@*foo bar**', '<p>@<em>foo bar</em>*</p>'],
9+
'closing with two stars': ['@**foo bar*', '<p>@*<em>foo bar</em></p>'],
10+
'opening must close': ['@**foo bar', '<p>@**foo bar</p>'],
11+
'escape opening': ['@\\**foo bar**', '<p>@**foo bar**</p>'],
12+
'escape closing': ['@**foo bar\\**', '<p>@*<em>foo bar*</em></p>'],
13+
'escape at': ['\\@foo', '<p>@foo</p>'],
14+
'needs content - simple': ['@', '<p>@</p>'],
15+
'needs content - starred': ['@****', '<p>@****</p>'],
16+
'can contain Unicode': ['@Moté', '<p><a href="/@Mot%C3%A9">@Moté</a></p>'],
17+
'can contain star - lonely': ['@*', '<p><a href="/@*">@*</a></p>'],
18+
'can contain star - surrounded': ['@foo*bar', '<p><a href="/@foo*bar">@foo*bar</a></p>'],
19+
'no unescaped star': ['@*****', '<p>@*****</p>'],
20+
'escaped star': ['@**\\***', '<p><a href="/@*">@*</a></p>'],
21+
'space break ping': ['@foo bar', '<p><a href="/@foo">@foo</a> bar</p>'],
22+
'cannot contain inline - link': ['@**[link](hello)**', '<p><a href="/@%5Blink%5D(hello)">@[link](hello)</a></p>'],
23+
'is textual': ['**@foo**', '<p><strong><a href="/@foo">@foo</a></strong></p>', true],
24+
'intertwines with strong': ['**@**foo**', '<p><strong>@</strong>foo**</p>', true],
25+
'can contain references': ['@**&#35;**', '<p><a href="/@#">@#</a></p>'],
26+
'can contain references': ['@&#35;', '<p><a href="/@&amp;amp;#35;">@&amp;#35;</a></p>'],
27+
}
28+
29+
const renderString = (fixture) =>
30+
micromark(fixture, {
31+
extensions: [micromarkPing()],
32+
htmlExtensions: [micromarkPingHtml]
33+
})
34+
35+
describe('conforms to the specification', () => {
36+
for (const test in specificationTests) {
37+
const jestFunction = (!specificationTests[test][2]) ? it : it.skip
38+
39+
jestFunction(test, () => {
40+
const [input, expectedOutput] = specificationTests[test]
41+
const output = renderString(input)
42+
43+
expect(output).toEqual(expectedOutput)
44+
})
45+
}
46+
})
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { sanitizeUri } from 'micromark-util-sanitize-uri'
2+
3+
export default {
4+
enter: {
5+
pingCall: enterPingCall
6+
},
7+
exit: {
8+
pingCall: exitPingCall
9+
}
10+
}
11+
12+
function enterPingCall () {
13+
this.buffer()
14+
}
15+
16+
function exitPingCall () {
17+
const pingName = '@'.concat(this.resume())
18+
const url = sanitizeUri('/'.concat(pingName))
19+
20+
this.tag(`<a href="${url}">`)
21+
this.raw(pingName)
22+
this.tag('</a>')
23+
}
Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
import { markdownLineEnding, markdownLineEndingOrSpace } from 'micromark-util-character'
2+
import { codes } from 'micromark-util-symbol'
3+
4+
export default function micromarkPing (options = {}) {
5+
// Character definitions, see specification, part 1
6+
const escapeChar = 92
7+
const atChar = options.pingChar || 64
8+
const sequenceChar = options.sequenceChar || 42
9+
10+
const call = {
11+
name: 'ping',
12+
tokenize: tokenizeFactory({
13+
atChar,
14+
escapeChar,
15+
sequenceChar
16+
})
17+
}
18+
19+
// Inject a hook on the at symbol
20+
return {
21+
text: { [atChar]: call }
22+
}
23+
}
24+
25+
function tokenizeFactory (charCodes) {
26+
// Extract character code
27+
const {
28+
atChar,
29+
escapeChar,
30+
sequenceChar
31+
} = charCodes
32+
33+
return tokenizePing
34+
35+
function pingEnd (code) {
36+
return (markdownLineEndingOrSpace(code) || code === codes.eof)
37+
}
38+
39+
function pingForcedEnd (code) {
40+
return (markdownLineEnding(code) || code === codes.eof)
41+
}
42+
43+
function tokenizePing (effects, ok, nok) {
44+
let hasSequence = false
45+
let hasContent = false
46+
let token
47+
48+
return atSymbol
49+
50+
// Define a state `pingAtSymbol` that consumes the at symbol
51+
function atSymbol (code) {
52+
// Discard invalid characters
53+
if (code !== atChar) return nok(code)
54+
55+
effects.enter('pingCall')
56+
effects.enter('pingAtSymbol')
57+
effects.consume(code)
58+
effects.exit('pingAtSymbol')
59+
60+
return start
61+
}
62+
63+
// Define a state `pingStart` that matches starting star sequence
64+
function start (code) {
65+
// Disallow empty pings
66+
if (pingEnd(code)) return nok(code)
67+
// Handle star sequences
68+
if (code === sequenceChar) return potentialStartSequence(code)
69+
// Handle escaped opening sequence
70+
if (code === escapeChar) return nok(code)
71+
72+
effects.enter('pingContent')
73+
effects.enter('data')
74+
75+
return content(code)
76+
}
77+
78+
// Define a state `pingPotentialStartSequence` that consumes the first star in a sequence
79+
function potentialStartSequence (code) {
80+
if (code !== sequenceChar) return nok(code)
81+
82+
token = effects.enter('pingStarSequence')
83+
effects.consume(code)
84+
85+
return startSequence
86+
}
87+
88+
// Define a state `pingStartSequence` that handles a star sequence
89+
function startSequence (code) {
90+
// Sequences of only one star are content if ending
91+
if (pingEnd(code)) {
92+
token.type = 'pingContent'
93+
94+
const { start } = token
95+
96+
token = effects.enter('data')
97+
token.start = start
98+
effects.exit('data')
99+
effects.exit('pingContent')
100+
effects.exit('pingCall')
101+
102+
return ok(code)
103+
}
104+
105+
if (code !== sequenceChar) return nok(code)
106+
107+
hasSequence = true
108+
109+
effects.consume(code)
110+
effects.exit('pingStarSequence')
111+
112+
effects.enter('pingContent')
113+
effects.enter('chunkString', { contentType: 'string' })
114+
115+
return content
116+
}
117+
118+
// Define a state `pingContent` that consumes the ping content
119+
function content (code) {
120+
// May end with star sequence
121+
if (code === sequenceChar) {
122+
return potentialEndSequence(code)
123+
}
124+
125+
// Ends with space
126+
if (!hasSequence && pingEnd(code)) {
127+
effects.exit('data')
128+
effects.exit('pingContent')
129+
effects.exit('pingCall')
130+
131+
return ok(code)
132+
}
133+
134+
// Forced end
135+
if (pingForcedEnd(code)) return nok(code)
136+
137+
hasContent = true
138+
effects.consume(code)
139+
140+
return (code === escapeChar) ? contentEscape : content
141+
}
142+
143+
// Define a state `pingContentEscape` to allow end sequence escape
144+
function contentEscape (code) {
145+
effects.consume(code)
146+
147+
return content
148+
}
149+
150+
// Define a state `pingPotentialEndSequence` that matches an end star sequence
151+
function potentialEndSequence (code) {
152+
if (code !== sequenceChar) {
153+
return content(code)
154+
}
155+
156+
effects.exit('chunkString')
157+
effects.exit('pingContent')
158+
token = effects.enter('pingStarSequence')
159+
effects.consume(code)
160+
161+
return endSequence
162+
}
163+
164+
// Define a state `pingEndSequence` that handles a star sequence
165+
function endSequence (code) {
166+
// Ends with star sequence
167+
if (code === sequenceChar) {
168+
if (!hasContent) {
169+
return nok(code)
170+
}
171+
172+
effects.consume(code)
173+
effects.exit('pingStarSequence')
174+
effects.exit('pingCall')
175+
176+
return ok
177+
}
178+
179+
// Sequences of only one star are content if ending
180+
if (pingEnd(code)) return nok(code)
181+
182+
token.type = 'pingContent'
183+
184+
const { start } = token
185+
token = effects.enter('chunkString', { contentType: 'string' })
186+
token.start = start
187+
188+
return content(code)
189+
}
190+
}
191+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
{
2+
"name": "micromark-extension-ping",
3+
"version": "0.0.0",
4+
"description": "Add Markdown syntax to handle user mentions",
5+
"type": "module",
6+
"keywords": [
7+
"micromark",
8+
"ping",
9+
"mentions",
10+
"plugin",
11+
"extension"
12+
],
13+
"author": "Stalone <[email protected]>",
14+
"homepage": "https://github.com/zestedesavoir/zmarkdown/tree/master/packages/micromark-extension-ping",
15+
"license": "MIT",
16+
"main": "lib/index.js",
17+
"module": "lib/index.js",
18+
"directories": {
19+
"lib": "lib",
20+
"test": "__tests__"
21+
},
22+
"files": [
23+
"lib"
24+
],
25+
"repository": {
26+
"type": "git",
27+
"url": "git+https://github.com/zestedesavoir/zmarkdown.git#master"
28+
},
29+
"scripts": {
30+
"pretest": "eslint .",
31+
"test": "cross-env NODE_OPTIONS=--experimental-vm-modules jest",
32+
"coverage": "cross-env NODE_OPTIONS=--experimental-vm-modules jest --coverage"
33+
},
34+
"bugs": {
35+
"url": "https://github.com/zestedesavoir/zmarkdown/issues"
36+
},
37+
"dependencies": {
38+
"micromark-util-character": "^2.1.0",
39+
"micromark-util-symbol": "^2.0.0",
40+
"micromark-util-sanitize-uri": "^2.0.0"
41+
}
42+
}

0 commit comments

Comments
 (0)