Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(core/inlines): refactor inline-idl-parser #2256

Merged
merged 4 commits into from Apr 13, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
242 changes: 174 additions & 68 deletions src/core/inline-idl-parser.js
@@ -1,43 +1,150 @@
/**
* Parses an IDL string and returns its components as:
*
* Foo ->
* { base: "Foo" }
* Foo.bar ->
* { base: "Foo", attribute: "bar" }
* Foo.bar.baz() ->
* { base: "Foo.bar", method: "baz()", args: [] }
* Foo.baz(arg1, arg2) ->
* { base: "Foo", method: "baz(arg1, arg2)", args: ["arg1", "arg2"] }
* Dictionary["member"] ->
* { base: "Dictionary", member: "member" }
*/
// Parses an inline IDL string (`{{ idl string }}`)
// and renders its components as HTML

import hyperHTML from "hyperhtml";

const methodRegex = /\((.*)\)$/;
const idlSplitRegex = /\b\.\b|\.(?=\[\[)/;
const dictionaryRegex = /(\w+)\["(\w+)"\]/;
const methodRegex = /(\w+)\((.*)\)$/;
const slotRegex = /^\[\[(\w+)\]\]$/;
// matches: `value` or `[[value]]`
// NOTE: [[value]] is actually a slot, but database has this as type="attribute"
const attributeRegex = /^((?:\[\[)?(?:\w+)(?:\]\])?)$/;
const enumRegex = /^(\w+)\["([\w ]+)"\]$/;
const enumValueRegex = /^"([\w ]+)"$/;
// TODO: const splitRegex = /(?<=\]\]|\b)\./
// https://github.com/w3c/respec/pull/1848/files#r225087385
const methodSplitRegex = /\.?(\w+\(.*\)$)/;

/** @param {string} str */
function parseInlineIDL(str) {
const result = Object.create(null);
const splitted = str.split(idlSplitRegex);
if (methodRegex.test(splitted[splitted.length - 1])) {
result.method = splitted.pop();
result.args = result.method.match(methodRegex)[1].split(/,\s*/);
}
if (splitted.length > 1 && !result.method) {
result.attribute = splitted.pop();
}
const remaining = splitted.join(".");
if (dictionaryRegex.test(remaining)) {
const [, base, member] = remaining.match(dictionaryRegex);
result.base = base;
result.member = member;
} else {
result.base = remaining;
const [nonMethodPart, methodPart] = str.split(methodSplitRegex);
const tokens = nonMethodPart
.split(".")
.concat(methodPart)
.filter(s => s && s.trim());
const results = [];
while (tokens.length) {
const value = tokens.pop();
// Method
if (methodRegex.test(value)) {
const [, identifier, allArgs] = value.match(methodRegex);
const args = allArgs.split(/,\s*/).filter(arg => arg);
results.push({ type: "method", identifier, args });
continue;
}
// Enum["enum value"]
if (enumRegex.test(value)) {
const [, identifier, enumValue] = value.match(enumRegex);
results.push({ type: "enum", identifier, enumValue });
continue;
}
if (enumValueRegex.test(value)) {
const [, identifier] = value.match(enumValueRegex);
results.push({ type: "enum-value", identifier });
continue;
}
// internal slot
if (slotRegex.test(value)) {
const [, identifier] = value.match(slotRegex);
results.push({ type: "internal-slot", identifier });
continue;
}
// attribute
if (attributeRegex.test(value) && tokens.length) {
const [, identifier] = value.match(attributeRegex);
results.push({ type: "attribute", identifier });
continue;
}
// base, always final token
if (attributeRegex.test(value) && tokens.length === 0) {
results.push({ type: "base", identifier: value });
continue;
}
throw new SyntaxError(
`IDL micro-syntax parsing error: "${value}" in \`${str}\``
);
}
return result;
// link the list
results.forEach((item, i, list) => {
item.parent = list[i + 1] || null;
});
// return them in the order we found them...
return results.reverse();
}

function findDfnType(varName) {
const potentialElems = [...document.body.querySelectorAll("dfn[data-type]")];
const match = potentialElems.find(
({ textContent }) => textContent.trim() === varName
);
return match ? match.dataset.type : null;
}

function renderBase(details) {
// Check if base is a local variable in a section
const { identifier } = details;
// we can use the identifier as the base type
if (!details.idlType) details.idlType = identifier;
return hyperHTML`<a data-xref-type="_IDL_">${identifier}</a>`;
}

// Internal slot: .[[identifier]] or [[identifier]]
function renderInternalSlot(details) {
const { identifier, parent } = details;
details.idlType = findDfnType(`[[${identifier}]]`);
const lt = `[[${identifier}]]`;
const html = hyperHTML`${parent ? "." : ""}[[<a
class="respec-idl-xref"
data-xref-type="attribute"
data-link-for=${parent ? parent.identifier : undefined}
data-lt="${lt}">${identifier}</a>]]`;
return html;
}

// Attribute: .identifier
function renderAttribute(details) {
const { parent, identifier } = details;
const html = hyperHTML`.<a
class="respec-idl-xref"
data-xref-type="attribute|dict-member"
data-link-for="${parent.identifier}"
>${identifier}</a>`;
return html;
}

// Method: .identifier(arg1, arg2, ...), identifier(arg1, arg2, ...)
function renderMethod(details) {
const { args, identifier, type, parent } = details;
const { identifier: linkFor } = parent || {};
const argsText = args.map(arg => `<var>${arg}</var>`).join(", ");
const searchText = `${identifier}(${args.join(", ")})`;
const html = hyperHTML`${parent ? "." : ""}<a
class="respec-idl-xref"
data-xref-type="${type}"
data-link-for="${linkFor}"
data-lt="${searchText}"
>${identifier}</a>(${[argsText]})`;
return html;
}

// Enum: Identifier["enum value"]
function renderEnum(details) {
const { identifier, enumValue } = details;
const html = hyperHTML`<a class="respec-idl-xref"
data-xref-type="enum"
>${identifier}</a>["<a class="respec-idl-xref"
data-xref-type="enum-value" data-link-for="${identifier}"
>${enumValue}</a>]"`;
return html;
}

// Enum value: "enum value"
function renderEnumValue(details) {
const { identifier } = details;
const html = hyperHTML`"<a
class="respec-idl-xref"
data-xref-type="enum-value"
>${identifier}</a>"`;
return html;
}

/**
Expand All @@ -46,40 +153,39 @@ function parseInlineIDL(str) {
* @return {Node} html output
*/
export function idlStringToHtml(str) {
const { base, attribute, member, method, args } = parseInlineIDL(str);

if (base.startsWith("[[") && base.endsWith("]]")) {
// is internal slot (possibly local)
return hyperHTML`<code><a data-xref-type="attribute">${base}</a></code>`;
}

const baseHtml = base
? hyperHTML`<a data-xref-type="_IDL_">${base}</a>.`
: "";

if (member) {
// type: Dictionary["member"]
return hyperHTML`<code><a
class="respec-idl-xref" data-xref-type="dictionary">${base}</a>["<a
class="respec-idl-xref" data-xref-type="dict-member"
data-link-for="${base}" data-lt="${member}">${member}</a>"]</code>`;
let results;
try {
results = parseInlineIDL(str);
} catch (error) {
console.error(error);
return document.createTextNode(str);
}

if (attribute) {
// type: base.attribute
return hyperHTML`<code>${baseHtml}<a class="respec-idl-xref"
data-xref-type="attribute" data-link-for="${base}">${attribute}</a></code>`;
const render = hyperHTML(document.createDocumentFragment());
const output = [];
for (const details of results) {
switch (details.type) {
case "base":
output.push(renderBase(details));
break;
case "attribute":
output.push(renderAttribute(details));
break;
case "internal-slot":
output.push(renderInternalSlot(details));
break;
case "method":
output.push(renderMethod(details));
break;
case "enum":
output.push(renderEnum(details));
break;
case "enum-value":
output.push(renderEnumValue(details));
break;
default:
throw new Error("Unknown type.");
}
}

if (method) {
// base.method(args)
const [methodName] = method.split("(", 1);
return hyperHTML`<code>${baseHtml}<a class="respec-idl-xref"
data-xref-type="method" data-link-for="${base}"
data-lt="${method}">${methodName}</a>(${{
html: args.map(arg => `<var>${arg}</var>`).join(", "),
}})</code>`;
}

return hyperHTML`<code><a data-xref-type="_IDL_">${base}</a></code>`;
const result = render`<code>${output}</code>`;
return result;
}
2 changes: 1 addition & 1 deletion src/core/inlines.js
Expand Up @@ -57,7 +57,7 @@ function inlineXrefMatches(matched) {
// slices "{{" at the beginning and "}}" at the end
const ref = matched.slice(2, -2).trim();
return ref.startsWith("\\")
? document.createTextNode(`{{${ref.slice(1)}}}`)
? document.createTextNode(`${matched.replace("\\", "")}`)
: idlStringToHtml(ref);
}

Expand Down
34 changes: 31 additions & 3 deletions tests/data/xref/inline-idl-attributes.json
Expand Up @@ -8,7 +8,7 @@
"uri": "window-object.html#window"
}
],
"af8ec9cd32a9241135f61c57e7b0c9b900efb26b": [
"5530bca3f739b4cec44acfa985117a332bff078e": [
{
"shortname": "dom",
"type": "attribute",
Expand Down Expand Up @@ -50,6 +50,23 @@
"normative": true,
"uri": "#dom-publickeycredential-type-slot"
}
],
"e1ba145d75cb297f653c2276968729aa73b176d9": [
{
"shortname": "encoding",
"type": "dictionary",
"normative": true,
"uri": "#textdecoderoptions"
}
],
"fa3f68ead7def2fbf060d67db4c4e2d024de8116": [
{
"shortname": "encoding",
"type": "dict-member",
"for": ["TextDecoderOptions"],
"normative": true,
"uri": "#dom-textdecoderoptions-fatal"
}
]
},
"query": [
Expand All @@ -60,9 +77,9 @@
},
{
"term": "event",
"types": ["attribute"],
"types": ["attribute", "dict-member"],
"for": "Window",
"id": "af8ec9cd32a9241135f61c57e7b0c9b900efb26b"
"id": "5530bca3f739b4cec44acfa985117a332bff078e"
},
{
"term": "Credential",
Expand All @@ -85,6 +102,17 @@
"types": ["attribute"],
"for": "PublicKeyCredential",
"id": "4014fc92b1e56d111504f82e12c0053f9fd5b591"
},
{
"term": "TextDecoderOptions",
"types": ["_IDL_"],
"id": "e1ba145d75cb297f653c2276968729aa73b176d9"
},
{
"term": "fatal",
"types": ["attribute", "dict-member"],
"for": "TextDecoderOptions",
"id": "fa3f68ead7def2fbf060d67db4c4e2d024de8116"
}
]
}
34 changes: 0 additions & 34 deletions tests/data/xref/inline-idl-dict-member.json

This file was deleted.