micromark-extension-directive
Version:
micromark extension to support generic directives (`:cite[smith04]`)
233 lines (218 loc) • 6.73 kB
JavaScript
/**
* @import {Directive, HtmlOptions} from 'micromark-extension-directive'
* @import {CompileContext, Handle as MicromarkHandle, HtmlExtension} from 'micromark-util-types'
*/
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');
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');
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');
attributes.push(['id', parseEntities(this.sliceSerialize(token), {
attribute: true
})]);
}
/**
* @this {CompileContext}
* @type {MicromarkHandle}
*/
function exitAttributeClassValue(token) {
const attributes = this.getData('directiveAttributes');
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');
attributes.push([this.sliceSerialize(token), '']);
}
/**
* @this {CompileContext}
* @type {MicromarkHandle}
*/
function exitAttributeValue(token) {
const attributes = this.getData('directiveAttributes');
attributes[attributes.length - 1][1] = parseEntities(this.sliceSerialize(token), {
attribute: true
});
}
/**
* @this {CompileContext}
* @type {MicromarkHandle}
*/
function exitAttributes() {
const stack = this.getData('directiveStack');
const attributes = this.getData('directiveAttributes');
/** @type {Record<string, string>} */
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');
stack[stack.length - 1].content = data;
}
/**
* @this {CompileContext}
* @type {MicromarkHandle}
*/
function exitContainerFence() {
const stack = this.getData('directiveStack');
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');
const directive = stack.pop();
/** @type {boolean | undefined} */
let found;
/** @type {boolean | undefined} */
let result;
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);
}
}
}