Repository: w3c/webidl2.js
Pull request #241 (2018-11-29 11:48:48)

[WIP] write() with optional templates
This introduces a way to customize the output of `write()` so that tools e.g. ReSpec can add HTML tags on it. Basically what was described in https://github.com/w3c/webidl2.js/issues/210#issuecomment-400552326.

Should only be merged after [the respec side of PR](https://github.com/w3c/respec/pull/1920) gets approved, so that we can make sure that this covers everything ReSpec requires.

TODO:

- [ ] Add documentation.
- [ ] Add tests.

marcoscaceres: starting review... 
saschanaz: I'll merge this and add tests/docs as follow-up PRs. Thanks for your great review!

Patch:
diff --git a/lib/writer.js b/lib/writer.js
index adf4fdf..dade63e 100644
--- a/lib/writer.js
+++ b/lib/writer.js
@@ -1,33 +1,75 @@
 "use strict";
 
 (() => {
-  function write(ast) {
+  function noop(arg) {
+    return arg;
+  }
+
+  const templates = {
+    wrap: items => items.join(""),
+    trivia: noop,
+    name: noop,
+    reference: noop,
+    type: noop,
+    valueLiteral: noop,
+    inheritance: noop,
+    definition: noop,
+    extendedAttribute: noop,
+    extendedAttributeReference: noop
+  };
+
+  function write(ast, { templates: ts = templates } = {}) {
+    ts = Object.assign({}, templates, ts);
+
+    /**
+     * @param {string[]} strings 
+     * @param  {...any} args 
+     */
+    function wrap(strings, ...args) {
+      return ts.wrap([].concat(...strings.map((s, i) => [s, args[i] || ""])));
+    }
+    
+    function reference(wrapped, normalized) {
+      return ts.reference(wrapped, normalized || wrapped);
+    }
+
+    function extract_trivia(object) {
+      const batch = {};
+      for (const key in object.trivia) {
+        batch[key] = ts.trivia(object.trivia[key]);
+      }
+      return batch;
+    }
+
     function token(t, value) {
-      return t ? t.trivia + (value || t.escaped || t.value) : "";
+      return t ? wrap`${ts.trivia(t.trivia)}${value || t.value}` : "";
     }
 
     function type_body(it) {
+      const trivia = extract_trivia(it);
       if (it.union) {
-        const subtypes = it.idlType.map(type).join("");
-        return `${it.trivia.open}(${subtypes}${it.trivia.close})`;
+        const subtypes = ts.wrap(it.idlType.map(type));
+        return wrap`${trivia.open}(${subtypes}${trivia.close})`;
       } else if (it.generic) {
-        const genericName = `${it.trivia.base}${it.generic.value}`;
-        const subtypes = it.idlType.map(type).join("");
-        const { trivia } = it.generic;
-        const bracket = `${trivia.open}<${subtypes}${trivia.close}>`;
-        return `${genericName}${bracket}`;
+        const genericName = wrap`${trivia.base}${reference(it.generic.value)}`;
+        const subtypes = ts.wrap(it.idlType.map(type));
+        const gTrivia = extract_trivia(it.generic);
+        return wrap`${genericName}${gTrivia.open}<${subtypes}${gTrivia.close}>`;
       }
-      const prefix = token(it.prefix);
-      const base = it.trivia.base + it.baseName;
-      const postfix = token(it.postfix);
-      return `${prefix}${base}${postfix}`;
+      if (!it.prefix && !it.postfix) {
+        return wrap`${trivia.base}${reference(it.baseName)}`;
+      }
+      const precedingTrivia = it.prefix ? ts.trivia(it.prefix.trivia) : trivia.base;
+      const prefix = it.prefix ? wrap`${it.prefix.value}${trivia.base}` : "";
+      const ref = reference(wrap`${prefix}${it.baseName}${token(it.postfix)}`, it.idlType);
+      return wrap`${precedingTrivia}${ref}`;
     }
     function type(it) {
       const ext = extended_attributes(it.extAttrs);
       const body = type_body(it);
       const nullable = token(it.nullable, "?");
       const separator = token(it.separator);
-      return `${ext}${body}${nullable}${separator}`;
+      return wrap`${ext}${body}${nullable}${separator}`;
     }
     function const_value(it) {
       const tp = it.type;
@@ -38,136 +80,213 @@
       else if (tp === "number") return it.value;
       else return `"${it.value}"`;
     }
-    function default_(def) {
+    function default_(def, parent) {
       if (!def) {
         return "";
       }
-      const assign = `${def.trivia.assign}=`;
+      const trivia = extract_trivia(def);
+      const assign = wrap`${trivia.assign}=`;
       if (def.type === "sequence") {
-        return `${assign}${def.trivia.open}[${def.trivia.close}]`;
+        return wrap`${assign}${trivia.open}[${trivia.close}]`;
       }
-      return `${assign}${def.trivia.value}${const_value(def)}`;
+      return wrap`${assign}${trivia.value}${ts.valueLiteral(const_value(def), parent)}`;
     }
     function argument(arg) {
-      let ret = extended_attributes(arg.extAttrs);
-      ret += token(arg.optional, "optional");
-      ret += type(arg.idlType);
-      ret += token(arg.variadic, "...");
-      ret += `${arg.trivia.name}${arg.escapedName}`;
-      ret += default_(arg.default);
-      ret += token(arg.separator);
-      return ret;
+      const ext = extended_attributes(arg.extAttrs);
+      const optional = token(arg.optional, "optional");
+      const typePart = ts.type(type(arg.idlType));
+      const variadic = token(arg.variadic, "...");
+      const name = wrap`${ts.trivia(arg.trivia.name)}${ts.name(arg.escapedName, { data: arg })}`;
+      const defaultValue = default_(arg.default, arg);
+      const separator = token(arg.separator);
+      return wrap`${ext}${optional}${typePart}${variadic}${name}${defaultValue}${separator}`;
     }
     function identifier(id) {
-      return id.trivia + id.value + token(id.separator);
+      return wrap`${ts.trivia(id.trivia)}${reference(id.value)}${token(id.separator)}`;
     }
     function make_ext_at(it) {
-      let ret = it.trivia.name + it.name;
+      const name = ts.extendedAttributeReference(it.name);
+      let rhs = "";
       if (it.rhs) {
-        if (it.rhs.type === "identifier-list") ret += `${it.rhs.trivia.assign}=${it.rhs.trivia.open}(${it.rhs.value.map(identifier).join("")}${it.rhs.trivia.close})`;
-        else ret += `${it.rhs.trivia.assign}=${it.rhs.trivia.value}${it.rhs.value}`;
+        const trivia = extract_trivia(it.rhs);
+        rhs = (it.rhs.type === "identifier-list") ? 
+          wrap`${trivia.assign}=${trivia.open}(${ts.wrap(it.rhs.value.map(identifier))}${trivia.close})` : 
+          wrap`${trivia.assign}=${trivia.value}${reference(it.rhs.value)}`;
       }
-      if (it.signature) ret += `${it.signature.trivia.open}(${it.signature.arguments.map(argument).join("")}${it.signature.trivia.close})`;
-      ret += token(it.separator);
-      return ret;
+      const signature = it.signature ? wrap`${ts.trivia(it.signature.trivia.open)}(${ts.wrap(it.signature.arguments.map(argument))}${ts.trivia(it.signature.trivia.close)})` : "";
+      const separator = token(it.separator);
+      return wrap`${ts.trivia(it.trivia.name)}${ts.extendedAttribute(wrap`${name}${rhs}${signature}`)}${separator}`;
     }
     function extended_attributes(eats) {
       if (!eats) return "";
-      return `${eats.trivia.open}[${eats.items.map(make_ext_at).join("")}${eats.trivia.close}]`;
+      const members = ts.wrap(eats.items.map(make_ext_at));
+      const trivia = extract_trivia(eats);
+      return wrap`${trivia.open}[${members}${trivia.close}]`;
     }
 
-    function operation(it) {
-      let ret = extended_attributes(it.extAttrs);
-      ret += token(it.special);
+    function operation(it, parent) {
+      const ext = extended_attributes(it.extAttrs);
+      const modifier = token(it.special);
+      let bodyPart = "";
       if (it.body) {
         const { body } = it;
-        ret += type(body.idlType);
-        ret += token(body.name);
-        ret += `${body.trivia.open}(${body.arguments.map(argument).join("")}${body.trivia.close})`;
+        const trivia = extract_trivia(it.body);
+        const typePart = ts.type(type(body.idlType));
+        const name = body.name ?
+          token(body.name, ts.name(body.name.escaped, { data: it, parent })) :
+          "";
+        const args = wrap`${trivia.open}(${ts.wrap(body.arguments.map(argument))}${trivia.close})`;
+        bodyPart = wrap`${typePart}${name}${args}`;
       }
-      ret += it.trivia.termination + ";";
-      return ret;
+      return ts.definition(
+        wrap`${ext}${modifier}${bodyPart}${ts.trivia(it.trivia.termination)};`,
+        { data: it, parent }
+      );
     }
 
-    function attribute(it) {
-      let ret = extended_attributes(it.extAttrs);
-      ret += token(it.special);
-      ret += token(it.readonly, "readonly");
-      ret += `${it.trivia.base}attribute${type(it.idlType)}${it.trivia.name}${it.escapedName};`;
-      return ret;
+    function attribute(it, parent) {
+      const ext = extended_attributes(it.extAttrs);
+      const special = token(it.special);
+      const readonly = token(it.readonly, "readonly");
+      const prefixes = wrap`${ext}${special}${readonly}`;
+      const name = ts.name(it.escapedName, { data: it, parent });
+      const trivia = extract_trivia(it);
+      return ts.definition(
+        wrap`${prefixes}${typed_head(trivia, it, name)}${trivia.termination};`,
+        { data: it, parent }
+      );
+    }
+
+    function typed_head(trivia, it, name) {
+      return wrap`${trivia.base}${it.type}${ts.type(type(it.idlType))}${trivia.name}${name}`;
     }
 
     function inheritance(inh) {
       if (!inh) {
         return "";
       }
-      return `${inh.trivia.colon}:${inh.trivia.name}${inh.name}`;
+      const trivia = extract_trivia(inh);
+      return wrap`${trivia.colon}:${trivia.name}${ts.inheritance(reference(inh.name))}`;
     }
 
     function container(type) {
       return it => {
-        let ret = extended_attributes(it.extAttrs);
-        ret += token(it.partial, "partial");
-        ret += `${it.trivia.base}${type}${it.trivia.name}${it.escapedName}`;
-        ret += inheritance(it.inheritance);
-        ret += `${it.trivia.open}{${iterate(it.members)}${it.trivia.close}}${it.trivia.termination};`;
-        return ret;
+        const trivia = extract_trivia(it);
+        const ext = extended_attributes(it.extAttrs);
+        const partial = token(it.partial, "partial");
+        const name = ts.name(it.escapedName, { data: it });
+        const head = wrap`${trivia.base}${type}${trivia.name}${name}`;
+        const inherit = inheritance(it.inheritance);
+        const body = brace_body(trivia, iterate(it.members, it));
+        return ts.definition(
+          wrap`${ext}${partial}${head}${inherit}${body}`,
+          { data: it }
+        );
       };
     }
 
+    function brace_body(trivia, members) {
+      return wrap`${trivia.open}{${members}${trivia.close}}${trivia.termination};`;
+    }
+
     function interface_mixin(it) {
-      let ret = extended_attributes(it.extAttrs);
-      ret += token(it.partial, "partial");
-      ret += `${it.trivia.base}interface${it.trivia.mixin}mixin${it.trivia.name}${it.escapedName}`;
-      ret += `${it.trivia.open}{${iterate(it.members)}${it.trivia.close}}${it.trivia.termination};`;
-      return ret;
-    }
-
-    function field(it) {
-      let ret = extended_attributes(it.extAttrs);
-      ret += token(it.required, "required");
-      ret += `${type(it.idlType)}${it.trivia.name}${it.escapedName}`;
-      ret += default_(it.default);
-      ret += `${it.trivia.termination};`;
-      return ret;
-    }
-    function const_(it) {
-      const ret = extended_attributes(it.extAttrs);
-      return `${ret}${it.trivia.base}const${type(it.idlType)}${it.trivia.name}${it.name}${it.trivia.assign}=${it.trivia.value}${const_value(it.value)}${it.trivia.termination};`;
+      const trivia = extract_trivia(it);
+      const ext = extended_attributes(it.extAttrs);
+      const partial = token(it.partial, "partial");
+      const name = ts.name(it.escapedName, { data: it });
+      const head = wrap`${trivia.base}interface${trivia.mixin}mixin${trivia.name}${name}`;
+      const body = brace_body(trivia, iterate(it.members, it));
+      return ts.definition(
+        wrap`${ext}${partial}${head}${body}`,
+        { data: it }
+      );
+    }
+    function field(it, parent) {
+      const trivia = extract_trivia(it);
+      const ext = extended_attributes(it.extAttrs);
+      const required = token(it.required, "required");
+      const name = ts.name(it.escapedName, { data: it, parent });
+      const body = wrap`${ts.type(type(it.idlType))}${trivia.name}${name}`;
+      const defaultValue = default_(it.default, it);
+      return ts.definition(
+        wrap`${ext}${required}${body}${defaultValue}${trivia.termination};`,
+        { data: it, parent }
+      );
+    }
+    function const_(it, parent) {
+      const trivia = extract_trivia(it);
+      const ext = extended_attributes(it.extAttrs);
+      const name = ts.name(it.name, { data: it, parent });
+      const head = typed_head(trivia, it, name);
+      const assign = wrap`${trivia.assign}=${trivia.value}${ts.valueLiteral(const_value(it.value), it)}`;
+      return ts.definition(
+        wrap`${ext}${head}${assign}${trivia.termination};`,
+        { data: it, parent }
+      );
     }
     function typedef(it) {
+      const trivia = extract_trivia(it);
       const ret = extended_attributes(it.extAttrs);
-      return `${ret}${it.trivia.base}typedef${type(it.idlType)}${it.trivia.name}${it.escapedName}${it.trivia.termination};`;
+      const name = ts.name(it.escapedName, { data: it });
+      return ts.definition(
+        wrap`${ret}${typed_head(trivia, it, name)}${trivia.termination};`,
+        { data: it }
+      );
     }
     function includes(it) {
+      const trivia = extract_trivia(it);
       const ret = extended_attributes(it.extAttrs);
-      return `${ret}${it.trivia.target}${it.target}${it.trivia.includes}includes${it.trivia.mixin}${it.includes}${it.trivia.termination};`;
+      const target = reference(it.target);
+      const mixin = reference(it.includes);
+      return ts.definition(
+        wrap`${ret}${trivia.target}${target}${trivia.includes}includes${trivia.mixin}${mixin}${trivia.termination};`,
+        { data: it }
+      );
     }
     function callback(it) {
+      const trivia = extract_trivia(it);
       const ext = extended_attributes(it.extAttrs);
-      const head = `${it.trivia.base}callback${it.trivia.name}${it.name}`;
-      const args = it.arguments.map(argument).join("");
-      const signature = `${type(it.idlType)}${it.trivia.open}(${args}${it.trivia.close})`;
-      return `${ext}${head}${it.trivia.assign}=${signature}${it.trivia.termination};`;
+      const name = ts.name(it.name, { data: it });
+      const head = wrap`${trivia.base}callback${trivia.name}${name}`;
+      const args = ts.wrap(it.arguments.map(argument));
+      const signature = wrap`${ts.type(type(it.idlType))}${trivia.open}(${args}${trivia.close})`;
+      return ts.definition(
+        wrap`${ext}${head}${trivia.assign}=${signature}${trivia.termination};`,
+        { data: it }
+      );
     }
     function enum_(it) {
+      const trivia = extract_trivia(it);
       const ext = extended_attributes(it.extAttrs);
-      const values = it.values.map(v => {
-        const body = `${v.trivia}"${v.value}"`;
-        return body + token(v.separator);
-      }).join("");
-      return `${ext}${it.trivia.base}enum${it.trivia.name}${it.escapedName}${it.trivia.open}{${values}${it.trivia.close}}${it.trivia.termination};`;
+      const name = ts.name(it.escapedName, { data: it });
+      const values = iterate(it.values, it);
+      return ts.definition(
+        wrap`${ext}${trivia.base}enum${trivia.name}${name}${brace_body(trivia, values)}`,
+        { data: it }
+      );
+    }
+    function enum_value(v, parent) {
+      return wrap`${ts.trivia(v.trivia)}${
+        ts.definition(wrap`"${ts.name(v.value, { data: v, parent })}"`, { data: v, parent })
+      }${token(v.separator)}`;
     }
-    function iterable_like(it) {
-      const readonly = it.readonly ? `${it.readonly.trivia}readonly` : "";
-      const bracket = `${it.trivia.open}<${it.idlType.map(type).join("")}${it.trivia.close}>`;
-      return `${readonly}${it.trivia.base}${it.type}${bracket}${it.trivia.termination};`;
+    function iterable_like(it, parent) {
+      const trivia = extract_trivia(it);
+      const readonly = token(it.readonly, "readonly");
+      const bracket = wrap`${trivia.open}<${ts.wrap(it.idlType.map(type))}${trivia.close}>`;
+      return ts.definition(
+        wrap`${readonly}${trivia.base}${it.type}${bracket}${trivia.termination};`,
+        { data: it, parent }
+      );
     }
     function callbackInterface(it) {
-      return `${it.trivia.callback}callback${container("interface")(it)}`;
+      return ts.definition(
+        wrap`${ts.trivia(it.trivia.callback)}callback${container("interface", noop)(it)}`,
+        { data: it }
+      );
     }
     function eof(it) {
-      return it.trivia;
+      return ts.trivia(it.trivia);
     }
 
     const table = {
@@ -183,6 +302,7 @@
       includes,
       callback,
       enum: enum_,
+      "enum-value": enum_value,
       iterable: iterable_like,
       legacyiterable: iterable_like,
       maplike: iterable_like,
@@ -190,23 +310,21 @@
       "callback interface": callbackInterface,
       eof
     };
-    function dispatch(it) {
+    function dispatch(it, parent) {
       const dispatcher = table[it.type];
       if (!dispatcher) {
         throw new Error(`Type "${it.type}" is unsupported`);
       }
-      return table[it.type](it);
+      return table[it.type](it, parent);
     }
-    function iterate(things) {
+    function iterate(things, parent) {
       if (!things) return;
-      let ret = "";
-      for (const thing of things) ret += dispatch(thing);
-      return ret;
+      const results = things.map(thing => dispatch(thing, parent));
+      return ts.wrap(results);
     }
     return iterate(ast);
   }
 
-
   const obj = {
     write
   };


Test patch:
diff --git a/test/syntax/idl/attributes.widl b/test/syntax/idl/attributes.widl
index f665c1f..7b5d5a2 100644
--- a/test/syntax/idl/attributes.widl
+++ b/test/syntax/idl/attributes.widl
@@ -7,5 +7,5 @@ interface Person {
   attribute unsigned short age;
 
   // required is an allowed attribute name
-  attribute any required;
+  attribute any required ;
 };
diff --git a/test/syntax/json/attributes.json b/test/syntax/json/attributes.json
index f0d8d7c..19dd587 100644
--- a/test/syntax/json/attributes.json
+++ b/test/syntax/json/attributes.json
@@ -43,7 +43,7 @@
                 "trivia": {
                     "base": "\n\n  // required is an allowed attribute name\n  ",
                     "name": " ",
-                    "termination": ""
+                    "termination": " "
                 },
                 "idlType": {
                     "type": "attribute-type",