From 48682f582bb5347f3abfeb7d2644a0f2b5608a1f Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Wed, 29 Mar 2023 10:04:28 +0200 Subject: [PATCH] Refactor types, jsdocs, match `markdown-rs` --- dev/lib/syntax.js | 325 +++++++++++++++++++++++++++++++++++++--------- dev/matters.js | 77 +++++++---- 2 files changed, 311 insertions(+), 91 deletions(-) diff --git a/dev/lib/syntax.js b/dev/lib/syntax.js index a47572a..86bc7b2 100644 --- a/dev/lib/syntax.js +++ b/dev/lib/syntax.js @@ -1,13 +1,14 @@ /** - * @typedef {import('micromark-util-types').Extension} Extension - * @typedef {import('micromark-util-types').ConstructRecord} ConstructRecord * @typedef {import('micromark-util-types').Construct} Construct - * @typedef {import('micromark-util-types').Tokenizer} Tokenizer - * @typedef {import('micromark-util-types').TokenizeContext} TokenizeContext + * @typedef {import('micromark-util-types').ConstructRecord} ConstructRecord + * @typedef {import('micromark-util-types').Extension} Extension * @typedef {import('micromark-util-types').State} State - * @typedef {import('../matters.js').Options} Options - * @typedef {import('../matters.js').Matter} Matter + * @typedef {import('micromark-util-types').TokenizeContext} TokenizeContext + * @typedef {import('micromark-util-types').Tokenizer} Tokenizer + * * @typedef {import('../matters.js').Info} Info + * @typedef {import('../matters.js').Matter} Matter + * @typedef {import('../matters.js').Options} Options */ import {markdownLineEnding, markdownSpace} from 'micromark-util-character' @@ -16,7 +17,7 @@ import {types} from 'micromark-util-symbol/types.js' import {matters} from '../matters.js' /** - * Add support for parsing frontmatter in markdown. + * `micromark` extension to add support for parsing frontmatter in markdown. * * Function that can be called to get a syntax extension for micromark (passed * in `extensions`). @@ -24,25 +25,28 @@ import {matters} from '../matters.js' * Supports YAML by default. * Can be configured to support TOML and more. * - * @param {Options} [options='yaml'] + * @param {Options | null | undefined} [options='yaml'] * Configuration (optional). * @returns {Extension} * Syntax extension for micromark (passed in `extensions`). */ export function frontmatter(options) { - const settings = matters(options) + const listOfMatters = matters(options) /** @type {ConstructRecord} */ const flow = {} let index = -1 - while (++index < settings.length) { - const matter = settings[index] + while (++index < listOfMatters.length) { + const matter = listOfMatters[index] const code = fence(matter, 'open').charCodeAt(0) - if (code in flow) { - // @ts-expect-error it clearly does exist. - flow[code].push(parse(matter)) + const construct = createConstruct(matter) + const existing = flow[code] + + if (Array.isArray(existing)) { + existing.push(construct) } else { - flow[code] = [parse(matter)] + // Never a single object, always an array. + flow[code] = [construct] } } @@ -53,15 +57,21 @@ export function frontmatter(options) { * @param {Matter} matter * @returns {Construct} */ -function parse(matter) { - const name = matter.type +function createConstruct(matter) { const anywhere = matter.anywhere - const valueType = name + 'Value' - const fenceType = name + 'Fence' + const frontmatterType = matter.type + const fenceType = frontmatterType + 'Fence' const sequenceType = fenceType + 'Sequence' - const fenceConstruct = {tokenize: tokenizeFence, partial: true} - /** @type {string} */ + const valueType = frontmatterType + 'Value' + const closingFenceConstruct = {tokenize: tokenizeClosingFence, partial: true} + + /** + * Fence to look for. + * + * @type {string} + */ let buffer + let bufferIndex = 0 return {tokenize: tokenizeFrontmatter, concrete: true} @@ -74,48 +84,179 @@ function parse(matter) { return start - /** @type {State} */ + /** + * Start of frontmatter. + * + * ```markdown + * > | --- + * ^ + * | title: "Venus" + * | --- + * ``` + * + * @type {State} + */ function start(code) { const position = self.now() - if (position.column !== 1 || (!anywhere && position.line !== 1)) { - return nok(code) + if ( + // Indent not allowed. + position.column === 1 && + // Normally, only allowed in first line. + (position.line === 1 || anywhere) + ) { + buffer = fence(matter, 'open') + bufferIndex = 0 + + if (code === buffer.charCodeAt(bufferIndex)) { + effects.enter(frontmatterType) + effects.enter(fenceType) + effects.enter(sequenceType) + return openSequence(code) + } } - effects.enter(name) - buffer = fence(matter, 'open') - return effects.attempt(fenceConstruct, afterOpeningFence, nok)(code) + return nok(code) } - /** @type {State} */ - function afterOpeningFence(code) { - buffer = fence(matter, 'close') - return lineEnd(code) + /** + * In open sequence. + * + * ```markdown + * > | --- + * ^ + * | title: "Venus" + * | --- + * ``` + * + * @type {State} + */ + function openSequence(code) { + if (bufferIndex === buffer.length) { + effects.exit(sequenceType) + + if (markdownSpace(code)) { + effects.enter(types.whitespace) + return openSequenceWhitespace(code) + } + + return openAfter(code) + } + + if (code === buffer.charCodeAt(bufferIndex++)) { + effects.consume(code) + return openSequence + } + + return nok(code) } - /** @type {State} */ - function lineStart(code) { + /** + * In whitespace after open sequence. + * + * ```markdown + * > | ---␠ + * ^ + * | title: "Venus" + * | --- + * ``` + * + * @type {State} + */ + function openSequenceWhitespace(code) { + if (markdownSpace(code)) { + effects.consume(code) + return openSequenceWhitespace + } + + effects.exit(types.whitespace) + return openAfter(code) + } + + /** + * After open sequence. + * + * ```markdown + * > | --- + * ^ + * | title: "Venus" + * | --- + * ``` + * + * @type {State} + */ + function openAfter(code) { + if (markdownLineEnding(code)) { + effects.exit(fenceType) + effects.enter(types.lineEnding) + effects.consume(code) + effects.exit(types.lineEnding) + // Get ready for closing fence. + buffer = fence(matter, 'close') + bufferIndex = 0 + return effects.attempt(closingFenceConstruct, after, contentStart) + } + + // EOF is not okay. + return nok(code) + } + + /** + * Start of content chunk. + * + * ```markdown + * | --- + * > | title: "Venus" + * ^ + * | --- + * ``` + * + * @type {State} + */ + function contentStart(code) { if (code === codes.eof || markdownLineEnding(code)) { - return lineEnd(code) + return contentEnd(code) } effects.enter(valueType) - return lineData(code) + return contentInside(code) } - /** @type {State} */ - function lineData(code) { + /** + * In content chunk. + * + * ```markdown + * | --- + * > | title: "Venus" + * ^ + * | --- + * ``` + * + * @type {State} + */ + function contentInside(code) { if (code === codes.eof || markdownLineEnding(code)) { effects.exit(valueType) - return lineEnd(code) + return contentEnd(code) } effects.consume(code) - return lineData + return contentInside } - /** @type {State} */ - function lineEnd(code) { + /** + * End of content chunk. + * + * ```markdown + * | --- + * > | title: "Venus" + * ^ + * | --- + * ``` + * + * @type {State} + */ + function contentEnd(code) { // Require a closing fence. if (code === codes.eof) { return nok(code) @@ -125,67 +266,123 @@ function parse(matter) { effects.enter(types.lineEnding) effects.consume(code) effects.exit(types.lineEnding) - return effects.attempt(fenceConstruct, after, lineStart) + return effects.attempt(closingFenceConstruct, after, contentStart) } - /** @type {State} */ + /** + * After frontmatter. + * + * ```markdown + * | --- + * | title: "Venus" + * > | --- + * ^ + * ``` + * + * @type {State} + */ function after(code) { - effects.exit(name) + // `code` must be eol/eof. + effects.exit(frontmatterType) return ok(code) } } /** @type {Tokenizer} */ - function tokenizeFence(effects, ok, nok) { + function tokenizeClosingFence(effects, ok, nok) { let bufferIndex = 0 - return start - - /** @type {State} */ - function start(code) { + return closeStart + + /** + * Start of close sequence. + * + * ```markdown + * | --- + * | title: "Venus" + * > | --- + * ^ + * ``` + * + * @type {State} + */ + function closeStart(code) { if (code === buffer.charCodeAt(bufferIndex)) { effects.enter(fenceType) effects.enter(sequenceType) - return insideSequence(code) + return closeSequence(code) } return nok(code) } - /** @type {State} */ - function insideSequence(code) { + /** + * In close sequence. + * + * ```markdown + * | --- + * | title: "Venus" + * > | --- + * ^ + * ``` + * + * @type {State} + */ + function closeSequence(code) { if (bufferIndex === buffer.length) { effects.exit(sequenceType) if (markdownSpace(code)) { effects.enter(types.whitespace) - return insideWhitespace(code) + return closeSequenceWhitespace(code) } - return fenceEnd(code) + return closeAfter(code) } if (code === buffer.charCodeAt(bufferIndex++)) { effects.consume(code) - return insideSequence + return closeSequence } return nok(code) } - /** @type {State} */ - function insideWhitespace(code) { + /** + * In whitespace after close sequence. + * + * ```markdown + * > | --- + * | title: "Venus" + * | ---␠ + * ^ + * ``` + * + * @type {State} + */ + function closeSequenceWhitespace(code) { if (markdownSpace(code)) { effects.consume(code) - return insideWhitespace + return closeSequenceWhitespace } effects.exit(types.whitespace) - return fenceEnd(code) + return closeAfter(code) } - /** @type {State} */ - function fenceEnd(code) { + /** + * After close sequence. + * + * ```markdown + * | --- + * | title: "Venus" + * > | --- + * ^ + * ``` + * + * @type {State} + */ + function closeAfter(code) { if (code === codes.eof || markdownLineEnding(code)) { effects.exit(fenceType) return ok(code) @@ -198,7 +395,7 @@ function parse(matter) { /** * @param {Matter} matter - * @param {'open'|'close'} prop + * @param {'open' | 'close'} prop * @returns {string} */ function fence(matter, prop) { @@ -209,8 +406,8 @@ function fence(matter, prop) { } /** - * @param {Info|string} schema - * @param {'open'|'close'} prop + * @param {Info | string} schema + * @param {'open' | 'close'} prop * @returns {string} */ function pick(schema, prop) { diff --git a/dev/matters.js b/dev/matters.js index 22428a7..6d0aa49 100644 --- a/dev/matters.js +++ b/dev/matters.js @@ -1,46 +1,59 @@ /** - * @typedef {'yaml'|'toml'} Preset - * Either `'yaml'` or `'toml'`. + * @typedef {'toml' | 'yaml'} Preset + * Known name of a frontmatter style. * * @typedef Info + * Frontmatter style. + * + * Depending on how this structure is used, it reflects a marker or a fence. * @property {string} open + * Opening. * @property {string} close + * Closing. * * @typedef MatterProps + * Fields of matter. * @property {string} type - * Type to tokenize as. + * Node type to tokenize as. * @property {boolean} [anywhere=false] - * If `true`, matter can be found anywhere in the document. - * If `false` (default), only matter at the start of the document is - * recognized. + * Whether matter can be found anywhere in the document (`boolean`, default: + * `false`). + * Normally, only matter at the start of the document is recognized. + * + * > 👉 **Note**: using this is a terrible idea. + * > It’s called frontmatter, not matter-in-the-middle or so. + * > This makes your markdown less portable. * * @typedef MarkerProps * Marker configuration. - * @property {string|Info} marker + * @property {Info | string} marker * Character used to construct fences. - * By providing an object with `open` and `close` different characters can be - * used for opening and closing fences. + * + * The marker will be repeated. * For example the character `'-'` will result in `'---'` being used as the * fence + * Pass an `Info` interface to specify different characters for opening and + * closing fences. * @property {never} [fence] * If `marker` is set, `fence` must not be set. * * @typedef FenceProps * Fence configuration. - * @property {string|Info} fence + * @property {Info | string} fence * String used as the complete fence. - * By providing an object with `open` and `close` different values can be used - * for opening and closing fences. - * This can be used too if fences contain different characters or lengths + * + * This can be used when fences contain different characters or lengths * other than 3. + * Pass an `Info` interface to specify different characters for opening and + * closing fences. * @property {never} [marker] * If `fence` is set, `marker` must not be set. * - * @typedef {(MatterProps & FenceProps)|(MatterProps & MarkerProps)} Matter + * @typedef {(MatterProps & FenceProps) | (MatterProps & MarkerProps)} Matter * Matter object describing frontmatter. * - * @typedef {Preset|Matter|Array} Options - * Matter object or preset, or many. + * @typedef {Matter | Preset | Array} Options + * Configuration. */ import {fault} from 'fault' @@ -49,29 +62,39 @@ const own = {}.hasOwnProperty const markers = {yaml: '-', toml: '+'} /** - * @param {Options} [options='yaml'] + * Simplify one or more options. + * + * @param {Options | null | undefined} [options='yaml'] + * Configuration. * @returns {Array} + * List of matters. */ -export function matters(options = 'yaml') { +export function matters(options) { /** @type {Array} */ - const results = [] + const result = [] let index = -1 - // One preset or matter. - if (!Array.isArray(options)) { - options = [options] - } + /** @type {Array} */ + const presetsOrMatters = Array.isArray(options) + ? options + : options + ? [options] + : ['yaml'] - while (++index < options.length) { - results[index] = matter(options[index]) + while (++index < presetsOrMatters.length) { + result[index] = matter(presetsOrMatters[index]) } - return results + return result } /** - * @param {Preset|Matter} option + * Simplify an option. + * + * @param {Matter | Preset} option + * Configuration. * @returns {Matter} + * Matters. */ function matter(option) { let result = option