This commit is contained in:
2024-03-22 03:47:51 +05:30
parent 8bcf3d211e
commit 89819f6fe2
28440 changed files with 3211033 additions and 2 deletions

View File

@@ -0,0 +1,21 @@
/**
* Create an extension for `micromark` to support frontmatter when serializing
* to HTML.
*
* > 👉 **Note**: this makes sure nothing is generated in the output HTML for
* > frontmatter.
*
* @param {Options | null | undefined} [options='yaml']
* Configuration (default: `'yaml'`).
* @returns {HtmlExtension}
* Extension for `micromark` that can be passed in `htmlExtensions`, to
* support frontmatter when serializing to HTML.
*/
export function frontmatterHtml(
options?: Options | null | undefined
): HtmlExtension
export type CompileContext = import('micromark-util-types').CompileContext
export type Handle = import('micromark-util-types').Handle
export type HtmlExtension = import('micromark-util-types').HtmlExtension
export type TokenType = import('micromark-util-types').TokenType
export type Options = import('./to-matters.js').Options

View File

@@ -0,0 +1,56 @@
/**
* @typedef {import('micromark-util-types').CompileContext} CompileContext
* @typedef {import('micromark-util-types').Handle} Handle
* @typedef {import('micromark-util-types').HtmlExtension} HtmlExtension
* @typedef {import('micromark-util-types').TokenType} TokenType
* @typedef {import('./to-matters.js').Options} Options
*/
import {toMatters} from './to-matters.js'
/**
* Create an extension for `micromark` to support frontmatter when serializing
* to HTML.
*
* > 👉 **Note**: this makes sure nothing is generated in the output HTML for
* > frontmatter.
*
* @param {Options | null | undefined} [options='yaml']
* Configuration (default: `'yaml'`).
* @returns {HtmlExtension}
* Extension for `micromark` that can be passed in `htmlExtensions`, to
* support frontmatter when serializing to HTML.
*/
export function frontmatterHtml(options) {
const matters = toMatters(options)
/** @type {HtmlExtension['enter']} */
const enter = {}
/** @type {HtmlExtension['exit']} */
const exit = {}
let index = -1
while (++index < matters.length) {
const type = /** @type {TokenType} */ (matters[index].type)
enter[type] = start
exit[type] = end
}
return {enter, exit}
/**
* @this {CompileContext}
* @type {Handle}
*/
function start() {
this.buffer()
}
/**
* @this {CompileContext}
* @type {Handle}
*/
function end() {
this.resume()
this.setData('slurpOneLineEnding', true)
}
}

View File

@@ -0,0 +1,20 @@
/**
* 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?: Options | null | undefined): Extension
export type Construct = import('micromark-util-types').Construct
export type ConstructRecord = import('micromark-util-types').ConstructRecord
export type Extension = import('micromark-util-types').Extension
export type State = import('micromark-util-types').State
export type TokenType = import('micromark-util-types').TokenType
export type TokenizeContext = import('micromark-util-types').TokenizeContext
export type Tokenizer = import('micromark-util-types').Tokenizer
export type Info = import('./to-matters.js').Info
export type Matter = import('./to-matters.js').Matter
export type Options = import('./to-matters.js').Options

View File

@@ -0,0 +1,410 @@
/**
* @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: Theyre 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]
}

View File

@@ -0,0 +1,97 @@
/**
* Simplify options by normalizing them to an array of matters.
*
* @param {Options | null | undefined} [options='yaml']
* Configuration (default: `'yaml'`).
* @returns {Array<Matter>}
* List of matters.
*/
export function toMatters(options?: Options | null | undefined): Array<Matter>
/**
* Known name of a frontmatter style.
*/
export type Preset = 'toml' | 'yaml'
/**
* Sequence.
*
* Depending on how this structure is used, it reflects a marker or a fence.
*/
export type Info = {
/**
* Closing.
*/
close: string
/**
* Opening.
*/
open: string
}
/**
* Fields describing a kind of matter.
*/
export type MatterProps = {
/**
* Node type to tokenize as.
*/
type: string
/**
* Whether matter can be found anywhere in the document, normally, only matter
* at the start of the document is recognized.
*
* > 👉 **Note**: using this is a terrible idea.
* > Its called frontmatter, not matter-in-the-middle or so.
* > This makes your markdown less portable.
*/
anywhere?: boolean | null | undefined
}
/**
* Marker configuration.
*/
export type MarkerProps = {
/**
* Character repeated 3 times, used as complete fences.
*
* For example the character `'-'` will result in `'---'` being used as the
* fence
* Pass `open` and `close` to specify different characters for opening and
* closing fences.
*/
marker: Info | string
/**
* If `marker` is set, `fence` must not be set.
*/
fence?: never
}
/**
* Fence configuration.
*/
export type FenceProps = {
/**
* Complete fences.
*
* This can be used when fences contain different characters or lengths
* other than 3.
* Pass `open` and `close` to interface to specify different characters for opening and
* closing fences.
*/
fence: Info | string
/**
* If `fence` is set, `marker` must not be set.
*/
marker?: never
}
/**
* Fields describing a kind of matter.
*
* > 👉 **Note**: using `anywhere` is a terrible idea.
* > Its called frontmatter, not matter-in-the-middle or so.
* > This makes your markdown less portable.
*
* > 👉 **Note**: `marker` and `fence` are mutually exclusive.
* > If `marker` is set, `fence` must not be set, and vice versa.
*/
export type Matter = (MatterProps & FenceProps) | (MatterProps & MarkerProps)
/**
* Configuration.
*/
export type Options = Matter | Preset | Array<Matter | Preset>

View File

@@ -0,0 +1,126 @@
/**
* @typedef {'toml' | 'yaml'} Preset
* Known name of a frontmatter style.
*
* @typedef Info
* Sequence.
*
* Depending on how this structure is used, it reflects a marker or a fence.
* @property {string} close
* Closing.
* @property {string} open
* Opening.
*
* @typedef MatterProps
* Fields describing a kind of matter.
* @property {string} type
* Node type to tokenize as.
* @property {boolean | null | undefined} [anywhere=false]
* Whether matter can be found anywhere in the document, normally, only matter
* at the start of the document is recognized.
*
* > 👉 **Note**: using this is a terrible idea.
* > Its called frontmatter, not matter-in-the-middle or so.
* > This makes your markdown less portable.
*
* @typedef MarkerProps
* Marker configuration.
* @property {Info | string} marker
* Character repeated 3 times, used as complete fences.
*
* For example the character `'-'` will result in `'---'` being used as the
* fence
* Pass `open` and `close` 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 {Info | string} fence
* Complete fences.
*
* This can be used when fences contain different characters or lengths
* other than 3.
* Pass `open` and `close` to 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
* Fields describing a kind of matter.
*
* > 👉 **Note**: using `anywhere` is a terrible idea.
* > Its called frontmatter, not matter-in-the-middle or so.
* > This makes your markdown less portable.
*
* > 👉 **Note**: `marker` and `fence` are mutually exclusive.
* > If `marker` is set, `fence` must not be set, and vice versa.
*
* @typedef {Matter | Preset | Array<Matter | Preset>} Options
* Configuration.
*/
import {fault} from 'fault'
const own = {}.hasOwnProperty
const markers = {yaml: '-', toml: '+'}
/**
* Simplify options by normalizing them to an array of matters.
*
* @param {Options | null | undefined} [options='yaml']
* Configuration (default: `'yaml'`).
* @returns {Array<Matter>}
* List of matters.
*/
export function toMatters(options) {
/** @type {Array<Matter>} */
const result = []
let index = -1
/** @type {Array<Matter | Preset>} */
const presetsOrMatters = Array.isArray(options)
? options
: options
? [options]
: ['yaml']
while (++index < presetsOrMatters.length) {
result[index] = matter(presetsOrMatters[index])
}
return result
}
/**
* Simplify an option.
*
* @param {Matter | Preset} option
* Configuration.
* @returns {Matter}
* Matter.
*/
function matter(option) {
let result = option
if (typeof result === 'string') {
if (!own.call(markers, result)) {
throw fault('Missing matter definition for `%s`', result)
}
result = {type: result, marker: markers[result]}
} else if (typeof result !== 'object') {
throw fault('Expected matter to be an object, not `%j`', result)
}
if (!own.call(result, 'type')) {
throw fault('Missing `type` in matter `%j`', result)
}
if (!own.call(result, 'fence') && !own.call(result, 'marker')) {
throw fault('Missing `marker` or `fence` in matter `%j`', result)
}
return result
}