Skip to content

Commit

Permalink
feat(micromark-extension-kbd-nested)!: add support for variable seque…
Browse files Browse the repository at this point in the history
…nces
  • Loading branch information
shivjm committed Nov 2, 2021
1 parent 1094c49 commit d725d24
Show file tree
Hide file tree
Showing 2 changed files with 205 additions and 24 deletions.
202 changes: 181 additions & 21 deletions packages/micromark-extension-kbd-nested/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { markdownLineEndingOrSpace } from "micromark-util-character";

export interface IOptions {
delimiter?: string | number;
variableDelimiter?: string | number;
}

const MINIMUM_MARKER_LENGTH = 2;
Expand All @@ -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("<kbd>");
},
[KEYBOARD_VARIABLE_TYPE]: function (this: CompileContext): void {
this.tag("<var>");
},
},
exit: {
[KEYBOARD_TYPE]: function (this: CompileContext): void {
this.tag("</kbd>");
},
[KEYBOARD_VARIABLE_TYPE]: function (this: CompileContext): void {
this.tag("</var>");
},
},
};
/* eslint-enable @typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-call */

// adapted from <https://github.com/micromark/micromark/blob/1b378e72675b15caff021f957a824d1f01420774/packages/micromark-core-commonmark/dev/lib/code-text.js>
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 {
Expand Down Expand Up @@ -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);
Expand All @@ -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,
Expand All @@ -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;
}
};

Expand Down Expand Up @@ -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 {
Expand All @@ -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;
};
}
27 changes: 24 additions & 3 deletions packages/micromark-extension-kbd-nested/tests/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -68,6 +70,25 @@ describe("handles nesting", () => {
]);
});

describe("handles variable sections", () => {
runCases([
["||//k//||", undefined, "<p><kbd><var>k</var></kbd></p>", undefined],
[
"||| ||Ctrl|| + ||// key //|| |||",
undefined,
"<p><kbd><kbd>Ctrl</kbd> + <kbd><var>key</var></kbd></kbd></p>",
undefined,
],
["++||k|| ++", "+", "<p><kbd><var>k</var></kbd></p>", "|"],
[
"!!! !!Ctrl!! + !! __key __!! !!!",
"!",
"<p><kbd><kbd>Ctrl</kbd> + <kbd><var>key</var></kbd></kbd></p>",
"_",
],
]);
});

describe("handles everything together", () => {
runCases([
[
Expand Down

0 comments on commit d725d24

Please sign in to comment.