/**
* @import {Directive, HtmlOptions} from 'micromark-extension-directive'
* @import {CompileContext, Handle as MicromarkHandle, HtmlExtension} from 'micromark-util-types'
*/
import {ok as assert} from 'devlop'
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 (default: `{}`).
* @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() {
enter.call(this, 'containerDirective')
},
directiveContainerAttributes: enterAttributes,
directiveContainerLabel: enterLabel,
directiveContainerContent() {
this.buffer()
},
directiveLeaf() {
enter.call(this, 'leafDirective')
},
directiveLeafAttributes: enterAttributes,
directiveLeafLabel: enterLabel,
directiveText() {
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 {Directive['type']} type
*/
function enter(type) {
let stack = this.getData('directiveStack')
if (!stack) this.setData('directiveStack', (stack = []))
stack.push({type, name: ''})
}
/**
* @this {CompileContext}
* @type {MicromarkHandle}
*/
function exitName(token) {
const stack = this.getData('directiveStack')
assert(stack, 'expected directive stack')
stack[stack.length - 1].name = this.sliceSerialize(token)
}
/**
* @this {CompileContext}
* @type {MicromarkHandle}
*/
function enterLabel() {
this.buffer()
}
/**
* @this {CompileContext}
* @type {MicromarkHandle}
*/
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 {MicromarkHandle}
*/
function enterAttributes() {
this.buffer()
this.setData('directiveAttributes', [])
}
/**
* @this {CompileContext}
* @type {MicromarkHandle}
*/
function exitAttributeIdValue(token) {
const attributes = this.getData('directiveAttributes')
assert(attributes, 'expected attributes')
attributes.push([
'id',
parseEntities(this.sliceSerialize(token), {
attribute: true
})
])
}
/**
* @this {CompileContext}
* @type {MicromarkHandle}
*/
function exitAttributeClassValue(token) {
const attributes = this.getData('directiveAttributes')
assert(attributes, 'expected attributes')
attributes.push([
'class',
parseEntities(this.sliceSerialize(token), {
attribute: true
})
])
}
/**
* @this {CompileContext}
* @type {MicromarkHandle}
*/
function exitAttributeName(token) {
// Attribute names in CommonMark are significantly limited, so character
// references can’t exist.
const attributes = this.getData('directiveAttributes')
assert(attributes, 'expected attributes')
attributes.push([this.sliceSerialize(token), ''])
}
/**
* @this {CompileContext}
* @type {MicromarkHandle}
*/
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 {MicromarkHandle}
*/
function exitAttributes() {
const stack = this.getData('directiveStack')
assert(stack, 'expected directive stack')
const attributes = this.getData('directiveAttributes')
assert(attributes, 'expected attributes')
/** @type {Record} */
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 {MicromarkHandle}
*/
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 {MicromarkHandle}
*/
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 {MicromarkHandle}
*/
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 | undefined} */
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)
}
}
}