/** * @typedef {import('micromark-util-types').Extension} Extension * @typedef {import('micromark-util-types').State} State * @typedef {import('micromark-util-types').TokenizeContext} TokenizeContext * @typedef {import('micromark-util-types').Tokenizer} Tokenizer */ import {ok as assert} from 'devlop' import {factorySpace} from 'micromark-factory-space' import { markdownLineEnding, markdownLineEndingOrSpace, markdownSpace } from 'micromark-util-character' import {codes, types} from 'micromark-util-symbol' const tasklistCheck = {tokenize: tokenizeTasklistCheck} /** * Create an HTML extension for `micromark` to support GFM task list items * syntax. * * @returns {Extension} * Extension for `micromark` that can be passed in `htmlExtensions` to * support GFM task list items when serializing to HTML. */ export function gfmTaskListItem() { return { text: {[codes.leftSquareBracket]: tasklistCheck} } } /** * @this {TokenizeContext} * @type {Tokenizer} */ function tokenizeTasklistCheck(effects, ok, nok) { const self = this return open /** * At start of task list item check. * * ```markdown * > | * [x] y. * ^ * ``` * * @type {State} */ function open(code) { assert(code === codes.leftSquareBracket, 'expected `[`') if ( // Exit if there’s stuff before. self.previous !== codes.eof || // Exit if not in the first content that is the first child of a list // item. !self._gfmTasklistFirstContentOfListItem ) { return nok(code) } effects.enter('taskListCheck') effects.enter('taskListCheckMarker') effects.consume(code) effects.exit('taskListCheckMarker') return inside } /** * In task list item check. * * ```markdown * > | * [x] y. * ^ * ``` * * @type {State} */ function inside(code) { // Currently we match how GH works in files. // To match how GH works in comments, use `markdownSpace` (`[\t ]`) instead // of `markdownLineEndingOrSpace` (`[\t\n\r ]`). if (markdownLineEndingOrSpace(code)) { effects.enter('taskListCheckValueUnchecked') effects.consume(code) effects.exit('taskListCheckValueUnchecked') return close } if (code === codes.uppercaseX || code === codes.lowercaseX) { effects.enter('taskListCheckValueChecked') effects.consume(code) effects.exit('taskListCheckValueChecked') return close } return nok(code) } /** * At close of task list item check. * * ```markdown * > | * [x] y. * ^ * ``` * * @type {State} */ function close(code) { if (code === codes.rightSquareBracket) { effects.enter('taskListCheckMarker') effects.consume(code) effects.exit('taskListCheckMarker') effects.exit('taskListCheck') return after } return nok(code) } /** * @type {State} */ function after(code) { // EOL in paragraph means there must be something else after it. if (markdownLineEnding(code)) { return ok(code) } // Space or tab? // Check what comes after. if (markdownSpace(code)) { return effects.check({tokenize: spaceThenNonSpace}, ok, nok)(code) } // EOF, or non-whitespace, both wrong. return nok(code) } } /** * @this {TokenizeContext} * @type {Tokenizer} */ function spaceThenNonSpace(effects, ok, nok) { return factorySpace(effects, after, types.whitespace) /** * After whitespace, after task list item check. * * ```markdown * > | * [x] y. * ^ * ``` * * @type {State} */ function after(code) { // EOF means there was nothing, so bad. // EOL means there’s content after it, so good. // Impossible to have more spaces. // Anything else is good. return code === codes.eof ? nok(code) : ok(code) } }