diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..c6c8b36 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,9 @@ +root = true + +[*] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fdefc8c --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +.DS_Store +*.log +.nyc_output/ +coverage/ +node_modules/ +yarn.lock diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..43c97e7 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +package-lock=false diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..2ac20d2 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,4 @@ +coverage/ +*.html +*.json +*.md diff --git a/.remarkignore b/.remarkignore new file mode 100644 index 0000000..65e3ba2 --- /dev/null +++ b/.remarkignore @@ -0,0 +1 @@ +test/ diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..e108609 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,5 @@ +language: node_js +node_js: + - lts/dubnium + - node +after_script: bash <(curl -s https://codecov.io/bash) diff --git a/html.js b/html.js new file mode 100644 index 0000000..227dd9a --- /dev/null +++ b/html.js @@ -0,0 +1 @@ +module.exports = require('./lib/html') diff --git a/index.js b/index.js new file mode 100644 index 0000000..624255d --- /dev/null +++ b/index.js @@ -0,0 +1 @@ +module.exports = require('./lib/syntax') diff --git a/lib/html.js b/lib/html.js new file mode 100644 index 0000000..7f55b28 --- /dev/null +++ b/lib/html.js @@ -0,0 +1,10 @@ +exports.enter = {strikethrough: onenterstrikethrough} +exports.exit = {strikethrough: onexitstrikethrough} + +function onenterstrikethrough() { + this.tag('') +} + +function onexitstrikethrough() { + this.tag('') +} diff --git a/lib/syntax.js b/lib/syntax.js new file mode 100644 index 0000000..cab2027 --- /dev/null +++ b/lib/syntax.js @@ -0,0 +1,156 @@ +var classifyCharacter = require('micromark/dist/util/classify-character') +var shallow = require('micromark/dist/util/shallow') +var resolveAll = require('micromark/dist/util/resolve-all') + +var characterGroupPunctuation = 2 + +var tokenizer = { + tokenize: tokenizeStrikethrough, + resolveAll: resolveAllStrikethrough +} + +exports.text = {126: tokenizer} +exports.insideSpan = {null: tokenizer} + +// Take events and resolve strikethrough. +function resolveAllStrikethrough(events, context) { + var length = events.length + var index = -1 + var strikethrough + var opening + var closing + var text + var indexOpen + var eventsUpTo + + // Walk through all events. + while (++index < length) { + closing = events[index][1] + + // Find a token that can close. + if (closing.type === 'strikethroughSequenceTemporary' && closing._close) { + indexOpen = index + + // Now walk back to find an opener. + while (indexOpen--) { + opening = events[indexOpen][1] + + // Find a token that can open the closer. + if ( + opening.type === 'strikethroughSequenceTemporary' && + opening._open && + closing.end.offset - closing.start.offset === + opening.end.offset - opening.start.offset && + !(closing._open || opening._close) + ) { + closing.type = 'strikethroughSequence' + opening.type = 'strikethroughSequence' + + strikethrough = { + type: 'strikethrough', + start: shallow(opening.start), + end: shallow(closing.end) + } + + text = { + type: 'strikethroughText', + start: shallow(opening.end), + end: shallow(closing.start) + } + + eventsUpTo = [].concat( + // Before. + events.slice(0, indexOpen - 1), + // Opening. + [ + ['enter', strikethrough, context], + ['enter', opening, context], + ['exit', opening, context], + ['enter', text, context] + ], + // Between. + resolveAll( + context.parser.constructs.insideSpan.null, + events.slice(indexOpen + 1, index), + context + ), + // Closing. + [ + ['exit', text, context], + ['enter', closing, context], + ['exit', closing, context], + ['exit', strikethrough, context] + ] + ) + + // After. + events = eventsUpTo.concat(events.slice(index + 2)) + length = events.length + index = eventsUpTo.length - 1 + break + } + } + } + } + + return removeRemainingSequences(events) +} + +function removeRemainingSequences(events) { + var index = -1 + var length = events.length + + while (++index < length) { + if (events[index][1].type === 'strikethroughSequenceTemporary') { + events[index][1].type = 'data' + } + } + + return events +} + +function tokenizeStrikethrough(effects, ok, nok) { + var previous = this.previous + var events = this.events + var size = 0 + + return start + + function start(code) { + if ( + code !== 126 || + (previous === 126 && + events[events.length - 1][1].type !== 'characterEscape') + ) { + return nok(code) + } + + effects.enter('strikethroughSequenceTemporary') + return more(code) + } + + function more(code) { + var token + var before + var after + + if (code === 126) { + // If this is the third marker, exit. + if (size > 1) { + return nok(code) + } + + effects.consume(code) + size++ + return more + } + + before = classifyCharacter(previous) + after = classifyCharacter(code) + token = effects.exit('strikethroughSequenceTemporary') + token._open = !after || (before && after === characterGroupPunctuation) + token._close = !before || (after && before === characterGroupPunctuation) + + return ok(code) + } +} diff --git a/license b/license new file mode 100644 index 0000000..3937235 --- /dev/null +++ b/license @@ -0,0 +1,22 @@ +(The MIT License) + +Copyright (c) 2020 Titus Wormer + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/package.json b/package.json new file mode 100644 index 0000000..3f26b23 --- /dev/null +++ b/package.json @@ -0,0 +1,75 @@ +{ + "name": "micromark-extension-gfm-strikethrough", + "version": "0.0.0", + "description": "micromark extension to support GFM strikethrough", + "license": "MIT", + "keywords": [ + "micromark", + "micromark-extension", + "strikethrough", + "strike", + "through", + "del", + "delete", + "deletion", + "gfm", + "markdown", + "unified" + ], + "repository": "micromark/micromark-extension-gfm-strikethrough", + "bugs": "https://github.com/micromark/micromark-extension-gfm-strikethrough/issues", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + }, + "author": "Titus Wormer (https://wooorm.com)", + "contributors": [ + "Titus Wormer (https://wooorm.com)" + ], + "files": [ + "lib/", + "index.js", + "html.js" + ], + "dependencies": { + "fault": "^1.0.0", + "micromark": "^2.3.0" + }, + "devDependencies": { + "nyc": "^15.0.0", + "prettier": "^2.0.0", + "remark-cli": "^8.0.0", + "remark-preset-wooorm": "^7.0.0", + "tape": "^5.0.0", + "xo": "^0.33.0" + }, + "scripts": { + "format": "remark . -qfo && prettier . -w --loglevel warn && xo --fix", + "test-api": "node test", + "test-coverage": "nyc --reporter lcov tape test/index.js", + "test": "npm run format && npm run test-coverage" + }, + "nyc": { + "check-coverage": true, + "lines": 100, + "functions": 100, + "branches": 100 + }, + "prettier": { + "tabWidth": 2, + "useTabs": false, + "singleQuote": true, + "bracketSpacing": false, + "semi": false, + "trailingComma": "none" + }, + "xo": { + "prettier": true, + "esnext": false + }, + "remarkConfig": { + "plugins": [ + "preset-wooorm" + ] + } +} diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..ade081f --- /dev/null +++ b/readme.md @@ -0,0 +1,126 @@ +# micromark-extension-gfm-strikethrough + +[![Build][build-badge]][build] +[![Coverage][coverage-badge]][coverage] +[![Downloads][downloads-badge]][downloads] +[![Size][size-badge]][size] +[![Sponsors][sponsors-badge]][collective] +[![Backers][backers-badge]][collective] +[![Chat][chat-badge]][chat] + +**[micromark][]** extension to support GitHub flavored markdown strikethrough +(~~like this~~). + +This package provides the low-level modules for integrating with the micromark +tokenizer and the micromark HTML compiler. + +While GFM defines [strikethrough][], GitHub does not adhere to their +specification. +This syntax extension matches the GitHub parser, not the specification. + +You probably shouldn’t use this package directly, but instead use +[`mdast-util-gfm-strikethrough`][mdast-util-gfm-strikethrough] with +**[mdast][]**. + +## Install + +[npm][]: + +```sh +npm install micromark-extension-gfm-strikethrough +``` + +## API + +### `syntax` + +### `html` + +> Note: `syntax` is the default export of this module, html is available at +> `micromark-extension-gfm-strikethrough/html`. + +Support strikethrough (~~like this~~) +The exports are extensions, respectively for the micromark parser (to tokenize +strikethrough, can be passed in `extensions`) and the default HTML compiler +(to compile as a `` element, can be passed in `htmlExtensions`). + +## Related + +* [`remarkjs/remark`][remark] + — markdown processor powered by plugins +* [`micromark/micromark`][micromark] + — the smallest commonmark-compliant markdown parser that exists +* [`syntax-tree/mdast-util-gfm-strikethrough`][mdast-util-gfm-strikethrough] + — mdast utility to support strikethrough +* [`syntax-tree/mdast-util-from-markdown`][from-markdown] + — mdast parser using `micromark` to create mdast from markdown +* [`syntax-tree/mdast-util-to-markdown`][to-markdown] + — mdast serializer to create markdown from mdast + +## Contribute + +See [`contributing.md` in `micromark/.github`][contributing] for ways to get +started. +See [`support.md`][support] for ways to get help. + +This project has a [code of conduct][coc]. +By interacting with this repository, organization, or community you agree to +abide by its terms. + +## License + +[MIT][license] © [Titus Wormer][author] + + + +[build-badge]: https://img.shields.io/travis/micromark/micromark-extension-gfm-strikethrough.svg + +[build]: https://travis-ci.org/micromark/micromark-extension-gfm-strikethrough + +[coverage-badge]: https://img.shields.io/codecov/c/github/micromark/micromark-extension-gfm-strikethrough.svg + +[coverage]: https://codecov.io/github/micromark/micromark-extension-gfm-strikethrough + +[downloads-badge]: https://img.shields.io/npm/dm/micromark-extension-gfm-strikethrough.svg + +[downloads]: https://www.npmjs.com/package/micromark-extension-gfm-strikethrough + +[size-badge]: https://img.shields.io/bundlephobia/minzip/micromark-extension-gfm-strikethrough.svg + +[size]: https://bundlephobia.com/result?p=micromark-extension-gfm-strikethrough + +[sponsors-badge]: https://opencollective.com/unified/sponsors/badge.svg + +[backers-badge]: https://opencollective.com/unified/backers/badge.svg + +[collective]: https://opencollective.com/unified + +[chat-badge]: https://img.shields.io/badge/chat-discussions-success.svg + +[chat]: https://github.com/micromark/unist/discussions + +[npm]: https://docs.npmjs.com/cli/install + +[license]: license + +[author]: https://wooorm.com + +[contributing]: https://github.com/micromark/.github/blob/HEAD/contributing.md + +[support]: https://github.com/micromark/.github/blob/HEAD/support.md + +[coc]: https://github.com/micromark/.github/blob/HEAD/code-of-conduct.md + +[micromark]: https://github.com/micromark/micromark + +[from-markdown]: https://github.com/syntax-tree/mdast-util-from-markdown + +[to-markdown]: https://github.com/syntax-tree/mdast-util-to-markdown + +[remark]: https://github.com/remarkjs/remark + +[mdast]: https://github.com/syntax-tree/mdast + +[mdast-util-gfm-strikethrough]: https://github.com/syntax-tree/mdast-util-gfm-strikethrough + +[strikethrough]: https://github.github.com/gfm/#strikethrough-extension- diff --git a/test/index.js b/test/index.js new file mode 100644 index 0000000..7a0bbad --- /dev/null +++ b/test/index.js @@ -0,0 +1,82 @@ +var fs = require('fs') +var path = require('path') +var test = require('tape') +var micromark = require('micromark') +var syntax = require('..') +var html = require('../html') + +var input = fs.readFileSync(path.join(__dirname, 'input.md')) +var output = fs.readFileSync(path.join(__dirname, 'output.html'), 'utf8') + +test('markdown -> html (micromark)', function (t) { + t.deepEqual( + micromark('a ~b~', { + extensions: [syntax], + htmlExtensions: [html] + }), + '

a b

', + 'should support strikethrough w/ one tilde' + ) + + t.deepEqual( + micromark('a ~~b~~', { + extensions: [syntax], + htmlExtensions: [html] + }), + '

a b

', + 'should support strikethrough w/ two tildes' + ) + + t.deepEqual( + micromark('a ~~~b~~~', { + extensions: [syntax], + htmlExtensions: [html] + }), + '

a ~~~b~~~

', + 'should not support strikethrough w/ three tildes' + ) + + t.deepEqual( + micromark(input, {extensions: [syntax], htmlExtensions: [html]}), + output, + 'should support strikethrough matching how GH does it' + ) + + t.deepEqual( + micromark('a \\~~~b~~ c', { + extensions: [syntax], + htmlExtensions: [html] + }), + '

a ~b c

', + 'should support strikethrough w/ after an escaped tilde' + ) + + t.deepEqual( + micromark('a ~~b ~~c~~ d~~ e', { + extensions: [syntax], + htmlExtensions: [html] + }), + '

a b c d e

', + 'should support nested strikethrough' + ) + + t.deepEqual( + micromark('a ~-1~ b', { + extensions: [syntax], + htmlExtensions: [html] + }), + '

a -1 b

', + 'should open if preceded by whitespace and followed by punctuation' + ) + + t.deepEqual( + micromark('a ~b.~ c', { + extensions: [syntax], + htmlExtensions: [html] + }), + '

a b. c

', + 'should close if preceded by punctuation and followed by whitespace' + ) + + t.end() +}) diff --git a/test/input.md b/test/input.md new file mode 100644 index 0000000..1f01832 --- /dev/null +++ b/test/input.md @@ -0,0 +1,137 @@ +# Strike through / delete + +## Balanced + +a ~one~ b + +a ~~two~~ b + +a ~~~three~~~ b + +a ~~~~four~~~~ b + +## Unbalanced + +a ~one/two~~ b + +a ~one/three~~~ b + +a ~one/four~~~~ b + +*** + +a ~~two/one~ b + +a ~~two/three~~~ b + +a ~~two/four~~~~ b + +*** + +a ~~~three/one~ b + +a ~~~three/two~~ b + +a ~~~three/four~~~~ b + +*** + +a ~~~~four/one~ b + +a ~~~~four/two~~ b + +a ~~~~four/three~~~ b + +## Multiple + +a ~one b one~ c one~ d + +a ~one b two~~ c one~ d + +a ~one b one~ c two~~ d + +a ~~two b two~~ c two~~ d + +a ~~two b one~ c two~~ d + +a ~~two b two~~ c one~ d + +## Flanking + +a oneRight~ b oneRight~ c oneRight~ d + +a oneRight~ b oneRight~ c ~oneLeft d + +a oneRight~ b ~oneLeft c oneRight~ d + +a ~oneLeft b oneRight~ c oneRight~ d + +a ~oneLeft b oneRight~ c ~oneLeft d + +a ~oneLeft b ~oneLeft c oneRight~ d + +a ~oneLeft b ~oneLeft c ~oneLeft d + +*** + +a twoRight~~ b twoRight~~ c twoRight~~ d + +a twoRight~~ b twoRight~~ c ~~twoLeft d + +a twoRight~~ b ~~twoLeft c twoRight~~ d + +a ~~twoLeft b twoRight~~ c twoRight~~ d + +a ~~twoLeft b twoRight~~ c ~~twoLeft d + +a ~~twoLeft b ~~twoLeft c twoRight~~ d + +a ~~twoLeft b ~~twoLeft c ~~twoLeft d + +## Interleave with attention + +a ~~two *emphasis* two~~ b + +a ~~two **strong** two~~ b + +a *marker ~~two marker* two~~ b + +a ~~two *marker two~~ marker* b + +## Interleave with links + +a ~~two [resource](#) two~~ b + +a ~~two [reference][#] two~~ b + +a [label start ~~two label end](#) two~~ b + +a ~~two [label start two~~ label end](#) b + +a ~~two [label start ~one one~ label end](#) two~~ b + +a ~one [label start ~~two two~~ label end](#) one~ b + +a ~one [label start ~one one~ label end](#) one~ b + +a ~~two [label start ~~two two~~ label end](#) two~~ b + +[#]: # + +## Interleave with code (text) + +a ~~two `code` two~~ b + +a ~~two `code two~~` b + +a `code start ~~two code end` two~~ b + +a ~~two `code start two~~ code end` b + +a ~~two `code start ~one one~ code end` two~~ b + +a ~one `code start ~~two two~~ code end` one~ b + +a ~one `code start ~one one~ code end` one~ b + +a ~~two `code start ~~two two~~ code end` two~~ b diff --git a/test/output.html b/test/output.html new file mode 100644 index 0000000..33d849d --- /dev/null +++ b/test/output.html @@ -0,0 +1,68 @@ +

Strike through / delete

+

Balanced

+

a one b

+

a two b

+

a ~~~three~~~ b

+

a ~~~~four~~~~ b

+

Unbalanced

+

a ~one/two~~ b

+

a ~one/three~~~ b

+

a ~one/four~~~~ b

+
+

a ~~two/one~ b

+

a ~~two/three~~~ b

+

a ~~two/four~~~~ b

+
+

a ~~~three/one~ b

+

a ~~~three/two~~ b

+

a ~~~three/four~~~~ b

+
+

a ~~~~four/one~ b

+

a ~~~~four/two~~ b

+

a ~~~~four/three~~~ b

+

Multiple

+

a one b one c one~ d

+

a one b two~~ c one d

+

a one b one c two~~ d

+

a two b two c two~~ d

+

a two b one~ c two d

+

a two b two c one~ d

+

Flanking

+

a oneRight~ b oneRight~ c oneRight~ d

+

a oneRight~ b oneRight~ c ~oneLeft d

+

a oneRight~ b oneLeft c oneRight d

+

a oneLeft b oneRight c oneRight~ d

+

a oneLeft b oneRight c ~oneLeft d

+

a ~oneLeft b oneLeft c oneRight d

+

a ~oneLeft b ~oneLeft c ~oneLeft d

+
+

a twoRight~~ b twoRight~~ c twoRight~~ d

+

a twoRight~~ b twoRight~~ c ~~twoLeft d

+

a twoRight~~ b twoLeft c twoRight d

+

a twoLeft b twoRight c twoRight~~ d

+

a twoLeft b twoRight c ~~twoLeft d

+

a ~~twoLeft b twoLeft c twoRight d

+

a ~~twoLeft b ~~twoLeft c ~~twoLeft d

+

Interleave with attention

+

a two emphasis two b

+

a two strong two b

+

a marker ~~two marker two~~ b

+

a two *marker two marker* b

+

Interleave with links

+

a two resource two b

+

a two reference two b

+

a label start ~~two label end two~~ b

+

a ~~two label start two~~ label end b

+

a two label start one one label end two b

+

a one label start two two label end one b

+

a one label start one one label end one b

+

a two label start two two label end two b

+

Interleave with code (text)

+

a two code two b

+

a ~~two code two~~ b

+

a code start ~~two code end two~~ b

+

a ~~two code start two~~ code end b

+

a two code start ~one one~ code end two b

+

a one code start ~~two two~~ code end one b

+

a one code start ~one one~ code end one b

+

a two code start ~~two two~~ code end two b