411 lines
8.7 KiB
JavaScript
411 lines
8.7 KiB
JavaScript
/**
|
||
* @typedef {import('micromark-util-types').Construct} Construct
|
||
* @typedef {import('micromark-util-types').ConstructRecord} ConstructRecord
|
||
* @typedef {import('micromark-util-types').Extension} Extension
|
||
* @typedef {import('micromark-util-types').State} State
|
||
* @typedef {import('micromark-util-types').TokenType} TokenType
|
||
* @typedef {import('micromark-util-types').TokenizeContext} TokenizeContext
|
||
* @typedef {import('micromark-util-types').Tokenizer} Tokenizer
|
||
*
|
||
* @typedef {import('./to-matters.js').Info} Info
|
||
* @typedef {import('./to-matters.js').Matter} Matter
|
||
* @typedef {import('./to-matters.js').Options} Options
|
||
*/
|
||
|
||
import {markdownLineEnding, markdownSpace} from 'micromark-util-character'
|
||
import {codes, types} from 'micromark-util-symbol'
|
||
import {toMatters} from './to-matters.js'
|
||
|
||
/**
|
||
* Create an extension for `micromark` to enable frontmatter syntax.
|
||
*
|
||
* @param {Options | null | undefined} [options='yaml']
|
||
* Configuration (default: `'yaml'`).
|
||
* @returns {Extension}
|
||
* Extension for `micromark` that can be passed in `extensions`, to
|
||
* enable frontmatter syntax.
|
||
*/
|
||
export function frontmatter(options) {
|
||
const matters = toMatters(options)
|
||
/** @type {ConstructRecord} */
|
||
const flow = {}
|
||
let index = -1
|
||
|
||
while (++index < matters.length) {
|
||
const matter = matters[index]
|
||
const code = fence(matter, 'open').charCodeAt(0)
|
||
const construct = createConstruct(matter)
|
||
const existing = flow[code]
|
||
|
||
if (Array.isArray(existing)) {
|
||
existing.push(construct)
|
||
} else {
|
||
// Never a single object, always an array.
|
||
flow[code] = [construct]
|
||
}
|
||
}
|
||
|
||
return {flow}
|
||
}
|
||
|
||
/**
|
||
* @param {Matter} matter
|
||
* @returns {Construct}
|
||
*/
|
||
function createConstruct(matter) {
|
||
const anywhere = matter.anywhere
|
||
const frontmatterType = /** @type {TokenType} */ (matter.type)
|
||
const fenceType = /** @type {TokenType} */ (frontmatterType + 'Fence')
|
||
const sequenceType = /** @type {TokenType} */ (fenceType + 'Sequence')
|
||
const valueType = /** @type {TokenType} */ (frontmatterType + 'Value')
|
||
const closingFenceConstruct = {tokenize: tokenizeClosingFence, partial: true}
|
||
|
||
/**
|
||
* Fence to look for.
|
||
*
|
||
* @type {string}
|
||
*/
|
||
let buffer
|
||
let bufferIndex = 0
|
||
|
||
return {tokenize: tokenizeFrontmatter, concrete: true}
|
||
|
||
/**
|
||
* @this {TokenizeContext}
|
||
* @type {Tokenizer}
|
||
*/
|
||
function tokenizeFrontmatter(effects, ok, nok) {
|
||
const self = this
|
||
|
||
return start
|
||
|
||
/**
|
||
* Start of frontmatter.
|
||
*
|
||
* ```markdown
|
||
* > | ---
|
||
* ^
|
||
* | title: "Venus"
|
||
* | ---
|
||
* ```
|
||
*
|
||
* @type {State}
|
||
*/
|
||
function start(code) {
|
||
const position = self.now()
|
||
|
||
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)
|
||
}
|
||
}
|
||
|
||
return nok(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)
|
||
}
|
||
|
||
/**
|
||
* 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 contentEnd(code)
|
||
}
|
||
|
||
effects.enter(valueType)
|
||
return contentInside(code)
|
||
}
|
||
|
||
/**
|
||
* In content chunk.
|
||
*
|
||
* ```markdown
|
||
* | ---
|
||
* > | title: "Venus"
|
||
* ^
|
||
* | ---
|
||
* ```
|
||
*
|
||
* @type {State}
|
||
*/
|
||
function contentInside(code) {
|
||
if (code === codes.eof || markdownLineEnding(code)) {
|
||
effects.exit(valueType)
|
||
return contentEnd(code)
|
||
}
|
||
|
||
effects.consume(code)
|
||
return contentInside
|
||
}
|
||
|
||
/**
|
||
* End of content chunk.
|
||
*
|
||
* ```markdown
|
||
* | ---
|
||
* > | title: "Venus"
|
||
* ^
|
||
* | ---
|
||
* ```
|
||
*
|
||
* @type {State}
|
||
*/
|
||
function contentEnd(code) {
|
||
// Require a closing fence.
|
||
if (code === codes.eof) {
|
||
return nok(code)
|
||
}
|
||
|
||
// Can only be an eol.
|
||
effects.enter(types.lineEnding)
|
||
effects.consume(code)
|
||
effects.exit(types.lineEnding)
|
||
return effects.attempt(closingFenceConstruct, after, contentStart)
|
||
}
|
||
|
||
/**
|
||
* After frontmatter.
|
||
*
|
||
* ```markdown
|
||
* | ---
|
||
* | title: "Venus"
|
||
* > | ---
|
||
* ^
|
||
* ```
|
||
*
|
||
* @type {State}
|
||
*/
|
||
function after(code) {
|
||
// `code` must be eol/eof.
|
||
effects.exit(frontmatterType)
|
||
return ok(code)
|
||
}
|
||
}
|
||
|
||
/** @type {Tokenizer} */
|
||
function tokenizeClosingFence(effects, ok, nok) {
|
||
let bufferIndex = 0
|
||
|
||
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 closeSequence(code)
|
||
}
|
||
|
||
return nok(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 closeSequenceWhitespace(code)
|
||
}
|
||
|
||
return closeAfter(code)
|
||
}
|
||
|
||
if (code === buffer.charCodeAt(bufferIndex++)) {
|
||
effects.consume(code)
|
||
return closeSequence
|
||
}
|
||
|
||
return nok(code)
|
||
}
|
||
|
||
/**
|
||
* In whitespace after close sequence.
|
||
*
|
||
* ```markdown
|
||
* > | ---
|
||
* | title: "Venus"
|
||
* | ---␠
|
||
* ^
|
||
* ```
|
||
*
|
||
* @type {State}
|
||
*/
|
||
function closeSequenceWhitespace(code) {
|
||
if (markdownSpace(code)) {
|
||
effects.consume(code)
|
||
return closeSequenceWhitespace
|
||
}
|
||
|
||
effects.exit(types.whitespace)
|
||
return closeAfter(code)
|
||
}
|
||
|
||
/**
|
||
* After close sequence.
|
||
*
|
||
* ```markdown
|
||
* | ---
|
||
* | title: "Venus"
|
||
* > | ---
|
||
* ^
|
||
* ```
|
||
*
|
||
* @type {State}
|
||
*/
|
||
function closeAfter(code) {
|
||
if (code === codes.eof || markdownLineEnding(code)) {
|
||
effects.exit(fenceType)
|
||
return ok(code)
|
||
}
|
||
|
||
return nok(code)
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @param {Matter} matter
|
||
* @param {'close' | 'open'} prop
|
||
* @returns {string}
|
||
*/
|
||
function fence(matter, prop) {
|
||
return matter.marker
|
||
? pick(matter.marker, prop).repeat(3)
|
||
: // @ts-expect-error: They’re mutually exclusive.
|
||
pick(matter.fence, prop)
|
||
}
|
||
|
||
/**
|
||
* @param {Info | string} schema
|
||
* @param {'close' | 'open'} prop
|
||
* @returns {string}
|
||
*/
|
||
function pick(schema, prop) {
|
||
return typeof schema === 'string' ? schema : schema[prop]
|
||
}
|