Skip to content

Commit

Permalink
fix(micromark-extension-kbd-nested)!: preserve space inside kbd seq…
Browse files Browse the repository at this point in the history
…uence
  • Loading branch information
shivjm committed Nov 2, 2021
1 parent 8074f36 commit 9a6a0eb
Show file tree
Hide file tree
Showing 3 changed files with 144 additions and 102 deletions.
12 changes: 8 additions & 4 deletions packages/micromark-extension-kbd-nested/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,15 +40,19 @@ console.log(output)
Yields:

```html
<p>Press <kbd><kbd>Ctrl</kbd>+<kbd>|</kbd></kbd>.</p>
<p>Press <kbd><kbd>Ctrl</kbd> + <kbd> | </kbd></kbd>.</p>
```

## Syntax

Recognizes any sequence of two or more unescaped occurrences of the
delimiter. Nesting is possible by using a longer sequence on the
outside and a shorter sequence on the inside, e.g. `||| ||Ctrl|| +
||x|| |||`. The sequence will be considered to end at the first whitespace character or non-delimiter, including escape characters. For example, these will all produce `<kbd>|</kbd>`:
delimiter. All whitespace will be preserved except immediately after
the opening sequence and immediately before the closing sequence.
Nesting is possible by using a longer sequence on the outside and a
shorter sequence on the inside, e.g. `||| ||Ctrl|| + ||x|| |||`. The
sequence will be considered to end at the first whitespace character
or non-delimiter, including escape characters. For example, these will
all produce `<kbd>|</kbd>`:

* `||\|||`
* `|| | ||`
Expand Down
212 changes: 121 additions & 91 deletions packages/micromark-extension-kbd-nested/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import type {
Code,
CompileContext,
Effects,
Event,
Extension,
State,
Tokenizer,
} from "micromark-util-types";
import { codes } from "micromark-util-symbol/codes";
import { types } from "micromark-util-symbol/types";
import { markdownLineEnding } from "micromark-util-character";
import { markdownLineEndingOrSpace } from "micromark-util-character";

export interface IOptions {
delimiter?: string | number;
Expand Down Expand Up @@ -41,114 +42,98 @@ export const html: Extension = {
export const syntax = (options: IOptions = {}): Extension => {
const delimiter = normalizeDelimiter(options.delimiter);

const tokenizeKeyboard: Tokenizer = function (effects, ok, nok): State {
let size = 0;
const makeTokenizer: (insideText: boolean) => Tokenizer = (insideText) =>
function (effects, ok, nok): State {
let size = 0;

return start;
return start;

function start(): void | State {
effects.enter(KEYBOARD_TYPE);
effects.enter(KEYBOARD_MARKER_TYPE);
return opening;
}

function opening(code: Code): void | State {
if (code !== delimiter && size < MINIMUM_MARKER_LENGTH) {
return nok(code);
}
function start(): void | State {
if (insideText) {
effects.exit(KEYBOARD_TEXT_TYPE);
}

if (code === delimiter) {
effects.consume(code);
size++;
effects.enter(KEYBOARD_TYPE);
effects.enter(KEYBOARD_MARKER_TYPE);
return opening;
}

effects.exit(KEYBOARD_MARKER_TYPE);
return gap;
}
function opening(code: Code): void | State {
if (code !== delimiter && size < MINIMUM_MARKER_LENGTH) {
return nok(code);
}

function gap(code: Code): void | State {
if (code === codes.eof) {
return nok(code);
}
if (code === delimiter) {
effects.consume(code);
size++;
return opening;
}

if (code === delimiter) {
// first try closing, then try parsing nested sequence, then
// just treat it as a literal
return effects.attempt(
{
tokenize: makeClosingTokenizer(delimiter, size),
partial: true,
},
ok,
() => {
return effects.attempt(
{
tokenize: tokenizeKeyboard,
partial: true,
},
gap,
(code) => {
consumeLiteral(code);
return gap;
},
);
},
);
effects.exit(KEYBOARD_MARKER_TYPE);
return openingGap;
}

if (code === codes.space) {
effects.enter(SPACE_TYPE);
effects.consume(code);
effects.exit(SPACE_TYPE);
return gap;
function openingGap(code: Code): void | State {
if (isEof(code)) {
return nok;
}

if (tryWhitespace(code, effects)) {
return openingGap;
}

return startData;
}

if (markdownLineEnding(code)) {
effects.enter(types.lineEnding);
effects.consume(code);
effects.exit(types.lineEnding);
return gap;
function startData(_code: Code): void | State {
effects.enter(KEYBOARD_TEXT_TYPE);
return data;
}

effects.enter(KEYBOARD_TEXT_TYPE);
return data(code);
}
function data(code: Code): void | State {
const t = makeClosingTokenizer(delimiter, size, true, insideText);

if (markdownLineEndingOrSpace(code) || code === delimiter) {
return effects.attempt(
{
tokenize: t,
partial: true,
},
ok,
code === delimiter
? () =>
effects.attempt(
{
tokenize: makeTokenizer(true),
partial: true,
},
data,
nok,
)
: literal,
);
}

function data(code: Code): void | State {
if (
code === codes.eof ||
code === codes.space ||
code === delimiter ||
markdownLineEnding(code)
) {
effects.exit(KEYBOARD_TEXT_TYPE);
return gap(code);
return literal;
}

if (code === codes.backslash) {
effects.exit(KEYBOARD_TEXT_TYPE);
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 literal;
}

effects.consume(code);
effects.enter(KEYBOARD_TEXT_TYPE);
return (code) => {
consumeLiteral(code);
return data;
};
return data;
}

effects.consume(code);
return data;
}

function consumeLiteral(code: Code): void | State {
effects.enter(KEYBOARD_TEXT_ESCAPE_TYPE);
effects.consume(code);
effects.exit(KEYBOARD_TEXT_ESCAPE_TYPE);
}
};
};

const tokenizer = {
tokenize: tokenizeKeyboard,
tokenize: makeTokenizer(false),
resolveAll: (events: Event[]) => events,
};

Expand All @@ -159,11 +144,31 @@ export const syntax = (options: IOptions = {}): Extension => {
};
};

function makeClosingTokenizer(delimiter: number, size: number): Tokenizer {
function makeClosingTokenizer(
delimiter: number,
size: number,
exitText: boolean,
enterText: boolean,
): Tokenizer {
return function (effects, ok, nok) {
let current = 0;
const { events } = this;

function start(): void | State {
if (exitText) {
effects.exit(KEYBOARD_TEXT_TYPE);
}

effects.enter(SPACE_TYPE);
return gap;
}

function gap(code: Code): State {
if (tryWhitespace(code, effects)) {
return gap;
}

effects.exit(SPACE_TYPE);
effects.enter(KEYBOARD_MARKER_TYPE);
return closing;
}
Expand All @@ -174,8 +179,11 @@ function makeClosingTokenizer(delimiter: number, size: number): Tokenizer {
current++;

if (current === size) {
effects.exit(KEYBOARD_MARKER_TYPE);
effects.exit(KEYBOARD_TYPE);
effects.exit(KEYBOARD_MARKER_TYPE)._close = true;
effects.exit(KEYBOARD_TYPE)._close = true;
if (enterText) {
effects.enter(KEYBOARD_TEXT_TYPE);
}
return ok(code);
} else {
return closing;
Expand All @@ -196,3 +204,25 @@ export function normalizeDelimiter(
? delimiter.charCodeAt(0)
: delimiter || codes.verticalBar;
}

function isEof(code: Code): boolean {
return code === codes.eof;
}

function tryWhitespace(code: Code, effects: Effects): boolean {
if (code === codes.space) {
effects.enter(SPACE_TYPE);
effects.consume(code);
effects.exit(SPACE_TYPE);
return true;
}

if (markdownLineEndingOrSpace(code)) {
effects.enter(types.lineEnding);
effects.consume(code);
effects.exit(types.lineEnding);
return true;
}

return false;
}
22 changes: 15 additions & 7 deletions packages/micromark-extension-kbd-nested/tests/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,46 +30,54 @@ describe("works for simple cases", () => {
["| ||Ctrl|| |", undefined, "<p>| <kbd>Ctrl</kbd> |</p>"],
["||Ctrl||", "|", "<p><kbd>Ctrl</kbd></p>"],
["|| Ctrl ||", "|", "<p><kbd>Ctrl</kbd></p>"],
["||a|| bc ||d", undefined, "<p><kbd>a</kbd> bc ||d</p>"], // remove orphans
["|| npm run build ||", undefined, "<p><kbd>npm run build</kbd></p>"],
["||a|| bc ||d", undefined, "<p><kbd>a</kbd> bc d</p>"], // remove orphans
]);
});

describe("handles different delimiters", () => {
runCases([
["++Ctrl++", "|", "<p>++Ctrl++</p>"],
["++Ctrl++", "+", "<p><kbd>Ctrl</kbd></p>"],
["|| \\| \\| || \\|", undefined, "<p><kbd>||</kbd> |</p>"],
]);
});

describe("handles escaping", () => {
runCases([
["||\\|||", undefined, "<p><kbd>|</kbd></p>"],
["|| \\| ||", undefined, "<p><kbd>|</kbd></p>"],
["|| \\| \\| || \\|", undefined, "<p><kbd>| |</kbd> |</p>"],
["|| \\ ||", undefined, "<p><kbd> </kbd></p>"],
["++ \\ ++", "+", "<p><kbd> </kbd></p>"],
]);
});

describe("handles nesting", () => {
runCases([
[
"||| ||Ctrl|| + ||Alt|| + ||x|| |||",
"||| ||Ctrl|| + ||Alt|| + ||x|| |||",
undefined,
"<p><kbd><kbd>Ctrl</kbd>+<kbd>Alt</kbd>+<kbd>x</kbd></kbd></p>",
"<p><kbd><kbd>Ctrl</kbd> + <kbd>Alt</kbd> + <kbd>x</kbd></kbd></p>",
],
[
"||||| ||Ctrl|| + |||| Alt |||| + |||x||| |||||",
undefined,
"<p><kbd><kbd>Ctrl</kbd>+<kbd>Alt</kbd>+<kbd>x</kbd></kbd></p>",
"<p><kbd><kbd>Ctrl</kbd> + <kbd>Alt</kbd> + <kbd>x</kbd></kbd></p>",
],
]);
});

describe("handles everything together", () => {
runCases([
[
"@@@@@ @@Ctrl@@ + @@@@ Alt @@@@ + @@@\\@@@@ @@@@@",
"@@@@@ @@Ctrl@@ + @@@@ Alt @@@@ + @@@ \\@@@@ @@@@@",
"@",
"<p><kbd><kbd>Ctrl</kbd>+<kbd>Alt</kbd>+<kbd>@</kbd></kbd></p>",
"<p><kbd><kbd>Ctrl</kbd> + <kbd>Alt</kbd> + <kbd>@</kbd></kbd></p>",
],
[
"@@@@@ @@Ctrl@@ + @@@@ Alt @@@@ + @@@ \\ @@@ @@@@@",
"@",
"<p><kbd><kbd>Ctrl</kbd> + <kbd>Alt</kbd> + <kbd> </kbd></kbd></p>",
],
]);
});
Expand Down

0 comments on commit 9a6a0eb

Please sign in to comment.