From d725d24190649dfe0e7cb8e656b4571482c3554d Mon Sep 17 00:00:00 2001 From: Shiv Jha-Mathur Date: Tue, 2 Nov 2021 23:18:36 +0530 Subject: [PATCH] feat(micromark-extension-kbd-nested)!: add support for variable sequences --- .../src/index.ts | 202 ++++++++++++++++-- .../tests/index.ts | 27 ++- 2 files changed, 205 insertions(+), 24 deletions(-) diff --git a/packages/micromark-extension-kbd-nested/src/index.ts b/packages/micromark-extension-kbd-nested/src/index.ts index bbea329..3726b23 100644 --- a/packages/micromark-extension-kbd-nested/src/index.ts +++ b/packages/micromark-extension-kbd-nested/src/index.ts @@ -13,6 +13,7 @@ import { markdownLineEndingOrSpace } from "micromark-util-character"; export interface IOptions { delimiter?: string | number; + variableDelimiter?: string | number; } const MINIMUM_MARKER_LENGTH = 2; @@ -21,31 +22,54 @@ const KEYBOARD_TYPE = "keyboardSequence"; const KEYBOARD_TEXT_TYPE = types.codeTextData; // TODO check whether this is okay const KEYBOARD_TEXT_ESCAPE_TYPE = "keyboardSequenceEscape"; const KEYBOARD_MARKER_TYPE = "keyboardSequenceMarker"; +const KEYBOARD_VARIABLE_MARKER_TYPE = "keyboardSequenceVariableMarker"; +const KEYBOARD_VARIABLE_TYPE = "keyboardSequenceVariable"; // TODO check whether this is okay const SPACE_TYPE = "space"; +const DEFAULT_DELIMITER = codes.verticalBar; +const DEFAULT_VARIABLE_DELIMITER = codes.slash; + export const html: Extension = { enter: { [KEYBOARD_TYPE]: function (this: CompileContext): void { this.tag(""); }, + [KEYBOARD_VARIABLE_TYPE]: function (this: CompileContext): void { + this.tag(""); + }, }, exit: { [KEYBOARD_TYPE]: function (this: CompileContext): void { this.tag(""); }, + [KEYBOARD_VARIABLE_TYPE]: function (this: CompileContext): void { + this.tag(""); + }, }, }; /* eslint-enable @typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-call */ // adapted from export const syntax = (options: IOptions = {}): Extension => { - const delimiter = normalizeDelimiter(options.delimiter); + const delimiter = normalizeDelimiter(options.delimiter, DEFAULT_DELIMITER); + const variableDelimiter = normalizeDelimiter( + options.variableDelimiter, + DEFAULT_VARIABLE_DELIMITER, + ); const makeTokenizer: (insideText: boolean) => Tokenizer = (insideText) => function (effects, ok, nok): State { let size = 0; + const onlyLiteral = makeConsumeLiteral(effects, data); + const literal = makeLiteral( + effects, + onlyLiteral, + KEYBOARD_TEXT_TYPE, + KEYBOARD_TEXT_ESCAPE_TYPE, + ); + return start; function start(): void | State { @@ -91,7 +115,13 @@ export const syntax = (options: IOptions = {}): Extension => { } function data(code: Code): void | State { - const t = makeClosingTokenizer(delimiter, size, true, insideText); + const closingTokenizer = makeClosingTokenizer( + delimiter, + size, + true, + insideText, + ); + const varTokenizer = makeVariableTokenizer(variableDelimiter); if (isEof(code)) { effects.exit(KEYBOARD_TEXT_TYPE); @@ -103,7 +133,7 @@ export const syntax = (options: IOptions = {}): Extension => { if (markdownLineEndingOrSpace(code) || code === delimiter) { return effects.attempt( { - tokenize: t, + tokenize: closingTokenizer, partial: true, }, ok, @@ -121,25 +151,18 @@ export const syntax = (options: IOptions = {}): Extension => { ); } - return literal; - } - - function literal(code: Code): void | State { - if (code === codes.backslash) { - effects.exit(KEYBOARD_TEXT_TYPE); - effects.enter(KEYBOARD_TEXT_ESCAPE_TYPE); - effects.consume(code); - effects.exit(KEYBOARD_TEXT_ESCAPE_TYPE); - effects.enter(KEYBOARD_TEXT_TYPE); - return onlyLiteral; + if (code === variableDelimiter) { + return effects.attempt( + { + tokenize: varTokenizer, + partial: true, + }, + data, + nok, + ); } - return onlyLiteral; - } - - function onlyLiteral(code: Code): void | State { - effects.consume(code); - return data; + return literal; } }; @@ -207,12 +230,119 @@ function makeClosingTokenizer( }; } +function makeVariableTokenizer(delimiter: number): Tokenizer { + return function (effects, ok, nok) { + const onlyLiteral = makeConsumeLiteral(effects, data); + const literal = makeLiteral( + effects, + onlyLiteral, + KEYBOARD_VARIABLE_TYPE, + KEYBOARD_TEXT_ESCAPE_TYPE, + ); + + let size = 0; + + return start; + + function start(): void | State { + effects.exit(KEYBOARD_TEXT_TYPE); + + effects.enter(KEYBOARD_VARIABLE_TYPE); + effects.enter(KEYBOARD_VARIABLE_MARKER_TYPE); + return opening; + } + + function opening(code: Code): void | State { + if (delimiter !== code) { + return nok(code); + } + + size++; + effects.consume(delimiter); + + if (size === MINIMUM_MARKER_LENGTH) { + effects.exit(KEYBOARD_VARIABLE_MARKER_TYPE); + return gap; + } + + return opening; + } + + function gap(code: Code): void | State { + if (tryWhitespace(code, effects)) { + return gap; + } + + effects.enter(KEYBOARD_TEXT_TYPE); + return data; + } + + function data(code: Code): void | State { + if (markdownLineEndingOrSpace(code) || code === delimiter) { + return effects.attempt( + { + tokenize: makeVariableClosingTokenizer(delimiter), + partial: true, + }, + ok, + literal, + ); + } + + return literal; + } + }; +} + +function makeVariableClosingTokenizer(delimiter: number): Tokenizer { + return function (effects, ok, nok) { + let size = 0; + + return start; + + function start(): void | State { + effects.exit(KEYBOARD_TEXT_TYPE); + + return gap; + } + + function gap(code: Code): void | State { + if (tryWhitespace(code, effects)) { + return gap; + } + + effects.enter(KEYBOARD_VARIABLE_MARKER_TYPE); + return marker; + } + + function marker(code: Code): void | State { + if (code === delimiter) { + effects.consume(code); + size++; + if (size === MINIMUM_MARKER_LENGTH) { + effects.exit(KEYBOARD_VARIABLE_MARKER_TYPE); + effects.exit(KEYBOARD_VARIABLE_TYPE); + + effects.enter(KEYBOARD_TEXT_TYPE); + + return ok(code); + } + + return marker; + } + + return nok(code); + } + }; +} + export function normalizeDelimiter( delimiter: string | number | undefined, + defaultValue: number, ): number { return typeof delimiter === "string" ? delimiter.charCodeAt(0) - : delimiter || codes.verticalBar; + : delimiter || defaultValue; } function isEof(code: Code): boolean { @@ -236,3 +366,33 @@ function tryWhitespace(code: Code, effects: Effects): boolean { return false; } + +function makeConsumeLiteral( + effects: Effects, + next: State, +): (code: Code) => State { + return (code: Code) => { + effects.consume(code); + return next; + }; +} + +function makeLiteral( + effects: Effects, + next: State, + outerType: string, + escapeType: string, +): (code: Code) => State { + return (code: Code) => { + if (code === codes.backslash) { + effects.exit(outerType); + effects.enter(escapeType); + effects.consume(code); + effects.exit(escapeType); + effects.enter(outerType); + return next; + } + + return next; + }; +} diff --git a/packages/micromark-extension-kbd-nested/tests/index.ts b/packages/micromark-extension-kbd-nested/tests/index.ts index 568e5d0..dd23aec 100644 --- a/packages/micromark-extension-kbd-nested/tests/index.ts +++ b/packages/micromark-extension-kbd-nested/tests/index.ts @@ -5,16 +5,18 @@ import { html, syntax } from "../src/index.js"; import { micromark } from "micromark"; -type Cases = readonly [string, string | undefined, string][]; +type Cases = + | readonly [string, string | undefined, string][] + | readonly [string, string | undefined, string, string | undefined][]; function runCases(cases: Cases) { - for (const [input, delimiter, expected] of cases) { + for (const [input, delimiter, expected, variableDelimiter] of cases) { it(`delimiter: ${JSON.stringify(delimiter)}, input: ${JSON.stringify( input, )}`, () => { assert.equal( micromark(input, { - extensions: [syntax({ delimiter })], + extensions: [syntax({ delimiter, variableDelimiter })], htmlExtensions: [html], }), expected, @@ -68,6 +70,25 @@ describe("handles nesting", () => { ]); }); +describe("handles variable sections", () => { + runCases([ + ["||//k//||", undefined, "

k

", undefined], + [ + "||| ||Ctrl|| + ||// key //|| |||", + undefined, + "

Ctrl + key

", + undefined, + ], + ["++||k|| ++", "+", "

k

", "|"], + [ + "!!! !!Ctrl!! + !! __key __!! !!!", + "!", + "

Ctrl + key

", + "_", + ], + ]); +}); + describe("handles everything together", () => { runCases([ [