Skip to content

Commit

Permalink
[remark-ping] Migrate to micromark
Browse files Browse the repository at this point in the history
  • Loading branch information
StaloneLab committed Jun 9, 2024
1 parent 1dcd686 commit c06652d
Show file tree
Hide file tree
Showing 17 changed files with 1,266 additions and 2,118 deletions.
428 changes: 421 additions & 7 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@
},
"scripts": {
"pretest": "lerna run pretest --scope zmarkdown",
"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",
"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",
"lint": "eslint .",
"posttest": "lerna run posttest --scope zmarkdown",
"build": "lerna run build",
Expand Down
3 changes: 3 additions & 0 deletions packages/micromark-extension-ping/.npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
__tests__/
specs/
.npmignore
55 changes: 55 additions & 0 deletions packages/micromark-extension-ping/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# `micromark-extension-ping`

**[micromark][]** extension that parses custom Markdown syntax to handle
user mentions, or pings.
This syntax extension follows a [specification][spec];
in short, you can ping an user with the syntax `@user`.
For usernames containing a space, use the alternative syntax `@**user space**`.

This package provides the low-level modules for integrating with the micromark
tokenizer and the micromark HTML compiler.

## Install

[npm][]:

```sh
npm install micromark-extension-ping
```

## API

### `html`

### `syntax(options?)`

> Note: `syntax` is the default export of this module, `html` is available at
> `micromark-extension-ping/lib/html`.
Support custom syntax to handle user mentions.
The export of `syntax` is a function that can be called with options and returns
an extension for the micromark parser (to tokenize user mentions; can be passed
in `extensions`).
The export of `html` is an extension for the default HTML compiler (to compile
as `<a href="/@user">` elements; can be passed in `htmlExtensions`).

##### `options`

- `options.pingChar`: the pipe character used to ping a simple user name. Defaults to `@`.
- `options.sequenceChar`: the star character added to ping user names containing a space. Defaults to `*` (star character).

## License

[MIT][license] © [Zeste de Savoir][zds]

<!-- Definitions -->

[license]: LICENCE

[micromark]: https://github.com/micromark/micromark

[npm]: https://docs.npmjs.com/cli/install

[spec]: specs/extension.md

[zds]: https://zestedesavoir.com
46 changes: 46 additions & 0 deletions packages/micromark-extension-ping/__tests__/spec.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { micromark } from 'micromark'
import micromarkPing from '../lib/index'
import micromarkPingHtml from '../lib/html'

const specificationTests = {
'works': ['@foo', '<p><a href="/@foo">@foo</a></p>'],
'with stars': ['@**foo bar**', '<p><a href="/@foo%20bar">@foo bar</a></p>'],
'opening with two stars': ['@*foo bar**', '<p>@<em>foo bar</em>*</p>'],
'closing with two stars': ['@**foo bar*', '<p>@*<em>foo bar</em></p>'],
'opening must close': ['@**foo bar', '<p>@**foo bar</p>'],
'escape opening': ['@\\**foo bar**', '<p>@**foo bar**</p>'],
'escape closing': ['@**foo bar\\**', '<p>@*<em>foo bar*</em></p>'],
'escape at': ['\\@foo', '<p>@foo</p>'],
'needs content - simple': ['@', '<p>@</p>'],
'needs content - starred': ['@****', '<p>@****</p>'],
'can contain Unicode': ['@Moté', '<p><a href="/@Mot%C3%A9">@Moté</a></p>'],
'can contain star - lonely': ['@*', '<p><a href="/@*">@*</a></p>'],
'can contain star - surrounded': ['@foo*bar', '<p><a href="/@foo*bar">@foo*bar</a></p>'],
'no unescaped star': ['@*****', '<p>@*****</p>'],
'escaped star': ['@**\\***', '<p><a href="/@*">@*</a></p>'],
'space break ping': ['@foo bar', '<p><a href="/@foo">@foo</a> bar</p>'],
'cannot contain inline - link': ['@**[link](hello)**', '<p><a href="/@%5Blink%5D(hello)">@[link](hello)</a></p>'],
'is textual': ['**@foo**', '<p><strong><a href="/@foo">@foo</a></strong></p>', true],
'intertwines with strong': ['**@**foo**', '<p><strong>@</strong>foo**</p>', true],
'can contain references': ['@**&#35;**', '<p><a href="/@#">@#</a></p>'],
'can contain references': ['@&#35;', '<p><a href="/@&amp;amp;#35;">@&amp;#35;</a></p>'],
}

const renderString = (fixture) =>
micromark(fixture, {
extensions: [micromarkPing()],
htmlExtensions: [micromarkPingHtml]
})

describe('conforms to the specification', () => {
for (const test in specificationTests) {
const jestFunction = (!specificationTests[test][2]) ? it : it.skip

jestFunction(test, () => {
const [input, expectedOutput] = specificationTests[test]
const output = renderString(input)

expect(output).toEqual(expectedOutput)
})
}
})
23 changes: 23 additions & 0 deletions packages/micromark-extension-ping/lib/html.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { sanitizeUri } from 'micromark-util-sanitize-uri'

export default {
enter: {
pingCall: enterPingCall
},
exit: {
pingCall: exitPingCall
}
}

function enterPingCall () {
this.buffer()
}

function exitPingCall () {
const pingName = '@'.concat(this.resume())
const url = sanitizeUri('/'.concat(pingName))

this.tag(`<a href="${url}">`)
this.raw(pingName)
this.tag('</a>')
}
191 changes: 191 additions & 0 deletions packages/micromark-extension-ping/lib/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
import { markdownLineEnding, markdownLineEndingOrSpace } from 'micromark-util-character'
import { codes } from 'micromark-util-symbol'

export default function micromarkPing (options = {}) {
// Character definitions, see specification, part 1
const escapeChar = 92
const atChar = options.pingChar || 64
const sequenceChar = options.sequenceChar || 42

const call = {
name: 'ping',
tokenize: tokenizeFactory({
atChar,
escapeChar,
sequenceChar
})
}

// Inject a hook on the at symbol
return {
text: { [atChar]: call }
}
}

function tokenizeFactory (charCodes) {
// Extract character code
const {
atChar,
escapeChar,
sequenceChar
} = charCodes

return tokenizePing

function pingEnd (code) {
return (markdownLineEndingOrSpace(code) || code === codes.eof)
}

function pingForcedEnd (code) {
return (markdownLineEnding(code) || code === codes.eof)
}

function tokenizePing (effects, ok, nok) {
let hasSequence = false
let hasContent = false
let token

return atSymbol

// Define a state `pingAtSymbol` that consumes the at symbol
function atSymbol (code) {
// Discard invalid characters
if (code !== atChar) return nok(code)

effects.enter('pingCall')
effects.enter('pingAtSymbol')
effects.consume(code)
effects.exit('pingAtSymbol')

return start
}

// Define a state `pingStart` that matches starting star sequence
function start (code) {
// Disallow empty pings
if (pingEnd(code)) return nok(code)
// Handle star sequences
if (code === sequenceChar) return potentialStartSequence(code)
// Handle escaped opening sequence
if (code === escapeChar) return nok(code)

effects.enter('pingContent')
effects.enter('data')

return content(code)
}

// Define a state `pingPotentialStartSequence` that consumes the first star in a sequence
function potentialStartSequence (code) {
if (code !== sequenceChar) return nok(code)

token = effects.enter('pingStarSequence')
effects.consume(code)

return startSequence
}

// Define a state `pingStartSequence` that handles a star sequence
function startSequence (code) {
// Sequences of only one star are content if ending
if (pingEnd(code)) {
token.type = 'pingContent'

const { start } = token

token = effects.enter('data')
token.start = start
effects.exit('data')
effects.exit('pingContent')
effects.exit('pingCall')

return ok(code)
}

if (code !== sequenceChar) return nok(code)

hasSequence = true

effects.consume(code)
effects.exit('pingStarSequence')

effects.enter('pingContent')
effects.enter('chunkString', { contentType: 'string' })

return content
}

// Define a state `pingContent` that consumes the ping content
function content (code) {
// May end with star sequence
if (code === sequenceChar) {
return potentialEndSequence(code)
}

// Ends with space
if (!hasSequence && pingEnd(code)) {
effects.exit('data')
effects.exit('pingContent')
effects.exit('pingCall')

return ok(code)
}

// Forced end
if (pingForcedEnd(code)) return nok(code)

hasContent = true
effects.consume(code)

return (code === escapeChar) ? contentEscape : content
}

// Define a state `pingContentEscape` to allow end sequence escape
function contentEscape (code) {
effects.consume(code)

return content
}

// Define a state `pingPotentialEndSequence` that matches an end star sequence
function potentialEndSequence (code) {
if (code !== sequenceChar) {
return content(code)
}

effects.exit('chunkString')
effects.exit('pingContent')
token = effects.enter('pingStarSequence')
effects.consume(code)

return endSequence
}

// Define a state `pingEndSequence` that handles a star sequence
function endSequence (code) {
// Ends with star sequence
if (code === sequenceChar) {
if (!hasContent) {
return nok(code)
}

effects.consume(code)
effects.exit('pingStarSequence')
effects.exit('pingCall')

return ok
}

// Sequences of only one star are content if ending
if (pingEnd(code)) return nok(code)

token.type = 'pingContent'

const { start } = token
token = effects.enter('chunkString', { contentType: 'string' })
token.start = start

return content(code)
}
}
}
42 changes: 42 additions & 0 deletions packages/micromark-extension-ping/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
{
"name": "micromark-extension-ping",
"version": "0.0.0",
"description": "Add Markdown syntax to handle user mentions",
"type": "module",
"keywords": [
"micromark",
"ping",
"mentions",
"plugin",
"extension"
],
"author": "Titouan (Stalone) S. <[email protected]>",
"homepage": "https://github.com/zestedesavoir/zmarkdown/tree/master/packages/micromark-extension-ping",
"license": "MIT",
"main": "lib/index.js",
"module": "lib/index.js",
"directories": {
"lib": "lib",
"test": "__tests__"
},
"files": [
"lib"
],
"repository": {
"type": "git",
"url": "git+https://github.com/zestedesavoir/zmarkdown.git#master"
},
"scripts": {
"pretest": "eslint .",
"test": "cross-env NODE_OPTIONS=--experimental-vm-modules jest",
"coverage": "cross-env NODE_OPTIONS=--experimental-vm-modules jest --coverage"
},
"bugs": {
"url": "https://github.com/zestedesavoir/zmarkdown/issues"
},
"dependencies": {
"micromark-util-character": "^2.1.0",
"micromark-util-symbol": "^2.0.0",
"micromark-util-sanitize-uri": "^2.0.0"
}
}
Loading

0 comments on commit c06652d

Please sign in to comment.