micromark-extension-directive/dev/lib/html.js
2023-05-30 16:32:08 +02:00

307 lines
8.3 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* @typedef {import('micromark-util-types').CompileContext} CompileContext
* @typedef {import('micromark-util-types').Handle} _Handle
* @typedef {import('micromark-util-types').HtmlExtension} HtmlExtension
*/
/**
* @typedef {[string, string]} Attribute
* Internal tuple representing an attribute.
*/
/**
* @typedef {Record<string, Handle>} HtmlOptions
* Configuration.
*
* > 👉 **Note**: the special field `'*'` can be used to specify a fallback
* > handle to handle all otherwise unhandled directives.
*
* @callback Handle
* Handle a directive.
* @param {CompileContext} this
* Current context.
* @param {Directive} directive
* Directive.
* @returns {boolean | void}
* Signal whether the directive was handled.
* Yield `false` to let the fallback (a special handle for `'*'`) handle it.
*
* @typedef Directive
* Structure representing a directive.
* @property {DirectiveType} type
* Kind.
* @property {string} name
* Name of directive.
* @property {string | undefined} [label]
* Compiled HTML content that was in `[brackets]`.
* @property {Record<string, string> | undefined} [attributes]
* Object w/ HTML attributes.
* @property {string | undefined} [content]
* Compiled HTML content inside container directive.
* @property {number | undefined} [_fenceCount]
* Private :)
*
* @typedef {'containerDirective' | 'leafDirective' | 'textDirective'} DirectiveType
* Kind.
*/
import {ok as assert} from 'uvu/assert'
import {parseEntities} from 'parse-entities'
const own = {}.hasOwnProperty
/**
* Create an extension for `micromark` to support directives when serializing
* to HTML.
*
* @param {HtmlOptions | null | undefined} [options]
* Configuration.
* @returns {HtmlExtension}
* Extension for `micromark` that can be passed in `htmlExtensions`, to
* support directives when serializing to HTML.
*/
export function directiveHtml(options) {
const options_ = options || {}
return {
enter: {
directiveContainer() {
return enter.call(this, 'containerDirective')
},
directiveContainerAttributes: enterAttributes,
directiveContainerLabel: enterLabel,
directiveContainerContent() {
this.buffer()
},
directiveLeaf() {
return enter.call(this, 'leafDirective')
},
directiveLeafAttributes: enterAttributes,
directiveLeafLabel: enterLabel,
directiveText() {
return enter.call(this, 'textDirective')
},
directiveTextAttributes: enterAttributes,
directiveTextLabel: enterLabel
},
exit: {
directiveContainer: exit,
directiveContainerAttributeClassValue: exitAttributeClassValue,
directiveContainerAttributeIdValue: exitAttributeIdValue,
directiveContainerAttributeName: exitAttributeName,
directiveContainerAttributeValue: exitAttributeValue,
directiveContainerAttributes: exitAttributes,
directiveContainerContent: exitContainerContent,
directiveContainerFence: exitContainerFence,
directiveContainerLabel: exitLabel,
directiveContainerName: exitName,
directiveLeaf: exit,
directiveLeafAttributeClassValue: exitAttributeClassValue,
directiveLeafAttributeIdValue: exitAttributeIdValue,
directiveLeafAttributeName: exitAttributeName,
directiveLeafAttributeValue: exitAttributeValue,
directiveLeafAttributes: exitAttributes,
directiveLeafLabel: exitLabel,
directiveLeafName: exitName,
directiveText: exit,
directiveTextAttributeClassValue: exitAttributeClassValue,
directiveTextAttributeIdValue: exitAttributeIdValue,
directiveTextAttributeName: exitAttributeName,
directiveTextAttributeValue: exitAttributeValue,
directiveTextAttributes: exitAttributes,
directiveTextLabel: exitLabel,
directiveTextName: exitName
}
}
/**
* @this {CompileContext}
* @param {DirectiveType} type
*/
function enter(type) {
let stack = this.getData('directiveStack')
if (!stack) this.setData('directiveStack', (stack = []))
stack.push({type, name: ''})
}
/**
* @this {CompileContext}
* @type {_Handle}
*/
function exitName(token) {
const stack = this.getData('directiveStack')
assert(stack, 'expected directive stack')
stack[stack.length - 1].name = this.sliceSerialize(token)
}
/**
* @this {CompileContext}
* @type {_Handle}
*/
function enterLabel() {
this.buffer()
}
/**
* @this {CompileContext}
* @type {_Handle}
*/
function exitLabel() {
const data = this.resume()
const stack = this.getData('directiveStack')
assert(stack, 'expected directive stack')
stack[stack.length - 1].label = data
}
/**
* @this {CompileContext}
* @type {_Handle}
*/
function enterAttributes() {
this.buffer()
this.setData('directiveAttributes', [])
}
/**
* @this {CompileContext}
* @type {_Handle}
*/
function exitAttributeIdValue(token) {
const attributes = this.getData('directiveAttributes')
assert(attributes, 'expected attributes')
attributes.push([
'id',
parseEntities(this.sliceSerialize(token), {
attribute: true
})
])
}
/**
* @this {CompileContext}
* @type {_Handle}
*/
function exitAttributeClassValue(token) {
const attributes = this.getData('directiveAttributes')
assert(attributes, 'expected attributes')
attributes.push([
'class',
parseEntities(this.sliceSerialize(token), {
attribute: true
})
])
}
/**
* @this {CompileContext}
* @type {_Handle}
*/
function exitAttributeName(token) {
// Attribute names in CommonMark are significantly limited, so character
// references cant exist.
const attributes = this.getData('directiveAttributes')
assert(attributes, 'expected attributes')
attributes.push([this.sliceSerialize(token), ''])
}
/**
* @this {CompileContext}
* @type {_Handle}
*/
function exitAttributeValue(token) {
const attributes = this.getData('directiveAttributes')
assert(attributes, 'expected attributes')
attributes[attributes.length - 1][1] = parseEntities(
this.sliceSerialize(token),
{attribute: true}
)
}
/**
* @this {CompileContext}
* @type {_Handle}
*/
function exitAttributes() {
const stack = this.getData('directiveStack')
assert(stack, 'expected directive stack')
const attributes = this.getData('directiveAttributes')
assert(attributes, 'expected attributes')
/** @type {Directive['attributes']} */
const cleaned = {}
let index = -1
while (++index < attributes.length) {
const attribute = attributes[index]
if (attribute[0] === 'class' && cleaned.class) {
cleaned.class += ' ' + attribute[1]
} else {
cleaned[attribute[0]] = attribute[1]
}
}
this.resume()
this.setData('directiveAttributes')
stack[stack.length - 1].attributes = cleaned
}
/**
* @this {CompileContext}
* @type {_Handle}
*/
function exitContainerContent() {
const data = this.resume()
const stack = this.getData('directiveStack')
assert(stack, 'expected directive stack')
stack[stack.length - 1].content = data
}
/**
* @this {CompileContext}
* @type {_Handle}
*/
function exitContainerFence() {
const stack = this.getData('directiveStack')
assert(stack, 'expected directive stack')
const directive = stack[stack.length - 1]
if (!directive._fenceCount) directive._fenceCount = 0
directive._fenceCount++
if (directive._fenceCount === 1) this.setData('slurpOneLineEnding', true)
}
/**
* @this {CompileContext}
* @type {_Handle}
*/
function exit() {
const stack = this.getData('directiveStack')
assert(stack, 'expected directive stack')
const directive = stack.pop()
assert(directive, 'expected directive')
/** @type {boolean|undefined} */
let found
/** @type {boolean|void} */
let result
assert(directive.name, 'expected `name`')
if (own.call(options_, directive.name)) {
result = options_[directive.name].call(this, directive)
found = result !== false
}
if (!found && own.call(options_, '*')) {
result = options_['*'].call(this, directive)
found = result !== false
}
if (!found && directive.type !== 'textDirective') {
this.setData('slurpOneLineEnding', true)
}
}
}