Yuki Takei 3 лет назад
Родитель
Сommit
23d204fefb

+ 1 - 0
packages/remark-growi-plugin/src/micromark-extension-growi-plugin/index.js

@@ -4,3 +4,4 @@
  */
  */
 
 
 export { directive } from './lib/syntax.js';
 export { directive } from './lib/syntax.js';
+export { directiveHtml } from './lib/html.js';

+ 231 - 0
packages/remark-growi-plugin/src/micromark-extension-growi-plugin/lib/html.js

@@ -0,0 +1,231 @@
+/**
+ * @typedef {import('micromark-util-types').HtmlExtension} HtmlExtension
+ * @typedef {import('micromark-util-types').Handle} _Handle
+ * @typedef {import('micromark-util-types').CompileContext} CompileContext
+ */
+
+/**
+ * @typedef {[string, string]} Attribute
+ * @typedef {'containerGrowiPluginDirective'|'leafGrowiPluginDirective'|'textGrowiPluginDirective'} DirectiveType
+ *
+ * @typedef Directive
+ * @property {DirectiveType} type
+ * @property {string} name
+ * @property {string} [label]
+ * @property {Record<string, string>} [attributes]
+ * @property {string} [content]
+ * @property {number} [_fenceCount]
+ *
+ * @typedef {(this: CompileContext, directive: Directive) => boolean|void} Handle
+ *
+ * @typedef {Record<string, Handle>} HtmlOptions
+ */
+
+import { parseEntities } from 'parse-entities';
+import { ok as assert } from 'uvu/assert';
+
+const own = {}.hasOwnProperty;
+
+/**
+  * @param {HtmlOptions} [options]
+  * @returns {HtmlExtension}
+  */
+export function directiveHtml(options = {}) {
+  return {
+    enter: {
+      directiveContainer() {
+        return enter.call(this, 'containerGrowiPluginDirective');
+      },
+      directiveContainerAttributes: enterAttributes,
+      directiveContainerLabel: enterLabel,
+      directiveContainerContent() {
+        this.buffer();
+      },
+
+      directiveLeaf() {
+        return enter.call(this, 'leafGrowiPluginDirective');
+      },
+      directiveLeafAttributes: enterAttributes,
+      directiveLeafLabel: enterLabel,
+
+      directiveText() {
+        return enter.call(this, 'textGrowiPluginDirective');
+      },
+      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) {
+    /** @type {Directive[]} */
+    let stack = this.getData('directiveStack');
+    if (!stack) this.setData('directiveStack', (stack = []));
+    stack.push({ type, name: '' });
+  }
+
+  /** @type {_Handle} */
+  function exitName(token) {
+    /** @type {Directive[]} */
+    const stack = this.getData('directiveStack');
+    stack[stack.length - 1].name = this.sliceSerialize(token);
+  }
+
+  /** @type {_Handle} */
+  function enterLabel() {
+    this.buffer();
+  }
+
+  /** @type {_Handle} */
+  function exitLabel() {
+    const data = this.resume();
+    /** @type {Directive[]} */
+    const stack = this.getData('directiveStack');
+    stack[stack.length - 1].label = data;
+  }
+
+  /** @type {_Handle} */
+  function enterAttributes() {
+    this.buffer();
+    this.setData('directiveAttributes', []);
+  }
+
+  /** @type {_Handle} */
+  function exitAttributeIdValue(token) {
+    /** @type {Attribute[]} */
+    const attributes = this.getData('directiveAttributes');
+    attributes.push(['id', parseEntities(this.sliceSerialize(token))]);
+  }
+
+  /** @type {_Handle} */
+  function exitAttributeClassValue(token) {
+    /** @type {Attribute[]} */
+    const attributes = this.getData('directiveAttributes');
+
+    attributes.push(['class', parseEntities(this.sliceSerialize(token))]);
+  }
+
+  /** @type {_Handle} */
+  function exitAttributeName(token) {
+    // Attribute names in CommonMark are significantly limited, so character
+    // references can’t exist.
+    /** @type {Attribute[]} */
+    const attributes = this.getData('directiveAttributes');
+
+    attributes.push([this.sliceSerialize(token), '']);
+  }
+
+  /** @type {_Handle} */
+  function exitAttributeValue(token) {
+    /** @type {Attribute[]} */
+    const attributes = this.getData('directiveAttributes');
+    attributes[attributes.length - 1][1] = parseEntities(
+      this.sliceSerialize(token),
+    );
+  }
+
+  /** @type {_Handle} */
+  function exitAttributes() {
+    /** @type {Directive[]} */
+    const stack = this.getData('directiveStack');
+    /** @type {Attribute[]} */
+    const attributes = this.getData('directiveAttributes');
+    /** @type {Directive['attributes']} */
+    const cleaned = {};
+    /** @type {Attribute} */
+    let attribute;
+    let index = -1;
+
+    while (++index < attributes.length) {
+      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;
+  }
+
+  /** @type {_Handle} */
+  function exitContainerContent() {
+    const data = this.resume();
+    /** @type {Directive[]} */
+    const stack = this.getData('directiveStack');
+    stack[stack.length - 1].content = data;
+  }
+
+  /** @type {_Handle} */
+  function exitContainerFence() {
+    /** @type {Directive[]} */
+    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);
+  }
+
+  /** @type {_Handle} */
+  function exit() {
+    /** @type {Directive} */
+    const directive = this.getData('directiveStack').pop();
+    /** @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 !== 'textGrowiPluginDirective') {
+      this.setData('slurpOneLineEnding', true);
+    }
+  }
+}

+ 2 - 1
packages/remark-growi-plugin/test/micromark-extension-growi-plugin.test.js

@@ -7,7 +7,7 @@ import { htmlVoidElements } from 'html-void-elements';
 import { micromark } from 'micromark';
 import { micromark } from 'micromark';
 import test from 'tape';
 import test from 'tape';
 
 
-import { directive as syntax } from '../src/micromark-extension-growi-plugin/index.js';
+import { directive as syntax, directiveHtml as html } from '../src/micromark-extension-growi-plugin/index.js';
 
 
 const own = {}.hasOwnProperty;
 const own = {}.hasOwnProperty;
 
 
@@ -1546,5 +1546,6 @@ function options(options) {
   return {
   return {
     allowDangerousHtml: true,
     allowDangerousHtml: true,
     extensions: [syntax()],
     extensions: [syntax()],
+    htmlExtensions: [html(options)],
   };
   };
 }
 }