@microflash/fenceparser
Version:
A metadata parser for code fences in markdown
99 lines (87 loc) • 2.8 kB
JavaScript
import { createToken, Lexer } from "chevrotain";
import defu from "defu";
const keyValuePairPattern = /([-\w]+)=(?:'([^']*)'|"([^"]*)"|\`([^`]*)\`)/;
const WhiteSpace = createToken({ name: "WhiteSpace", pattern: /\s+/, group: Lexer.SKIPPED });
const Range0 = createToken({ name: "Range0", pattern: /\w*{\d+}/, label: "range" });
const Range2 = createToken({ name: "Range2", pattern: /\w*{\d+\.\.\d+}/, label: "range" });
const Ranges = createToken({ name: "Ranges", pattern: /\w*{(?:\d+|\d+\.\.\d+)(?:,\s*(?:\d+|\d+\.\.\d+))*}/, label: "range" });
const KeyValuePair = createToken({ name: "KeyValuePair", pattern: keyValuePairPattern, label: "pair" });
const tokens = [
WhiteSpace,
Range0,
Range2,
Ranges,
KeyValuePair
];
export default class FenceParser {
static FenceLexer = new Lexer(tokens);
static DefaultOptions = {
rangeKey: "highlight"
}
constructor(options = {}) {
this.options = defu(options, FenceParser.DefaultOptions);
}
extractRawPair(token, label) {
switch (label) {
case "range":
const startIndex = token.indexOf("{");
const stopIndex = token.indexOf("}");
return {
key: token.substring(0, startIndex) || this.options.rangeKey,
value: token.substring(startIndex + 1, stopIndex)
};
case "pair":
const [, key = "key", v1, v2, v3] = token.match(keyValuePairPattern);
return { key, value: v1 || v2 || v3 };
default:
console.warn(`Unsupported token type ${label}`);
}
}
extractPair(token0) {
const label = token0.tokenType.LABEL;
const token = token0.image;
const { key, value } = this.extractRawPair(token, label);
const type = token0.tokenType.name;
switch(type) {
case "Range0":
return { key, value: [Number(value)], label };
case "Range2":
const [ b0, b1 ] = value.split("..").map((bound) => Number(bound));
const start = Math.min(b0, b1);
const stop = Math.max(b0, b1);
const size = stop - start + 1;
return {
key,
value: Array.from({ length: size }, (_, i) => i + start),
label
};
case "Ranges":
return {
key,
value: value.split(",")
.map(res => `{${res.trim()}}`)
.flatMap(res => FenceParser.FenceLexer.tokenize(res).tokens)
.map(res => this.extractPair(res))
.flatMap(res => res.value),
label
}
case "KeyValuePair":
return { key, value, label };
default:
console.warn(`Unsupported token type ${type}`);
}
}
parse(text) {
const { tokens } = FenceParser.FenceLexer.tokenize(text);
const response = {};
for (const token of tokens) {
const { key, value, label } = this.extractPair(token);
const isRange = label === "range";
response[key] ??= isRange ? [] : value;
if (isRange) {
response[key] = response[key].concat(value);
}
}
return response;
}
};