/** * @import {Options} from 'micromark-extension-gfm-strikethrough' * @import {Event, Extension, Resolver, State, Token, TokenizeContext, Tokenizer} from 'micromark-util-types' */ import { splice } from 'micromark-util-chunked'; import { classifyCharacter } from 'micromark-util-classify-character'; import { resolveAll } from 'micromark-util-resolve-all'; /** * Create an extension for `micromark` to enable GFM strikethrough syntax. * * @param {Options | null | undefined} [options={}] * Configuration. * @returns {Extension} * Extension for `micromark` that can be passed in `extensions`, to * enable GFM strikethrough syntax. */ export function gfmStrikethrough(options) { const options_ = options || {}; let single = options_.singleTilde; const tokenizer = { name: 'strikethrough', tokenize: tokenizeStrikethrough, resolveAll: resolveAllStrikethrough }; if (single === null || single === undefined) { single = true; } return { text: { [126]: tokenizer }, insideSpan: { null: [tokenizer] }, attentionMarkers: { null: [126] } }; /** * Take events and resolve strikethrough. * * @type {Resolver} */ function resolveAllStrikethrough(events, context) { let index = -1; // Walk through all events. while (++index < events.length) { // Find a token that can close. if (events[index][0] === 'enter' && events[index][1].type === 'strikethroughSequenceTemporary' && events[index][1]._close) { let open = index; // Now walk back to find an opener. while (open--) { // Find a token that can open the closer. if (events[open][0] === 'exit' && events[open][1].type === 'strikethroughSequenceTemporary' && events[open][1]._open && // If the sizes are the same: events[index][1].end.offset - events[index][1].start.offset === events[open][1].end.offset - events[open][1].start.offset) { events[index][1].type = 'strikethroughSequence'; events[open][1].type = 'strikethroughSequence'; /** @type {Token} */ const strikethrough = { type: 'strikethrough', start: Object.assign({}, events[open][1].start), end: Object.assign({}, events[index][1].end) }; /** @type {Token} */ const text = { type: 'strikethroughText', start: Object.assign({}, events[open][1].end), end: Object.assign({}, events[index][1].start) }; // Opening. /** @type {Array<Event>} */ const nextEvents = [['enter', strikethrough, context], ['enter', events[open][1], context], ['exit', events[open][1], context], ['enter', text, context]]; const insideSpan = context.parser.constructs.insideSpan.null; if (insideSpan) { // Between. splice(nextEvents, nextEvents.length, 0, resolveAll(insideSpan, events.slice(open + 1, index), context)); } // Closing. splice(nextEvents, nextEvents.length, 0, [['exit', text, context], ['enter', events[index][1], context], ['exit', events[index][1], context], ['exit', strikethrough, context]]); splice(events, open - 1, index - open + 3, nextEvents); index = open + nextEvents.length - 2; break; } } } } index = -1; while (++index < events.length) { if (events[index][1].type === 'strikethroughSequenceTemporary') { events[index][1].type = "data"; } } return events; } /** * @this {TokenizeContext} * @type {Tokenizer} */ function tokenizeStrikethrough(effects, ok, nok) { const previous = this.previous; const events = this.events; let size = 0; return start; /** @type {State} */ function start(code) { if (previous === 126 && events[events.length - 1][1].type !== "characterEscape") { return nok(code); } effects.enter('strikethroughSequenceTemporary'); return more(code); } /** @type {State} */ function more(code) { const before = classifyCharacter(previous); if (code === 126) { // If this is the third marker, exit. if (size > 1) return nok(code); effects.consume(code); size++; return more; } if (size < 2 && !single) return nok(code); const token = effects.exit('strikethroughSequenceTemporary'); const after = classifyCharacter(code); token._open = !after || after === 2 && Boolean(before); token._close = !before || before === 2 && Boolean(after); return ok(code); } } }