/** * @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} 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 | undefined} * 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 | 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 '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 {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 can’t 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 | 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) } } }