| import { Type } from "./type.js"; |
| import { Argument } from "./argument.js"; |
| import { |
| ExtendedAttributes, |
| SimpleExtendedAttribute, |
| } from "./extended-attributes.js"; |
| import { Operation } from "./operation.js"; |
| import { Attribute } from "./attribute.js"; |
| import { Tokeniser } from "../tokeniser.js"; |
| |
| /** |
| * @param {string} identifier |
| */ |
| export function unescape(identifier) { |
| return identifier.startsWith("_") ? identifier.slice(1) : identifier; |
| } |
| |
| /** |
| * Parses comma-separated list |
| * @param {import("../tokeniser.js").Tokeniser} tokeniser |
| * @param {object} args |
| * @param {Function} args.parser parser function for each item |
| * @param {boolean} [args.allowDangler] whether to allow dangling comma |
| * @param {string} [args.listName] the name to be shown on error messages |
| */ |
| export function list(tokeniser, { parser, allowDangler, listName = "list" }) { |
| const first = parser(tokeniser); |
| if (!first) { |
| return []; |
| } |
| first.tokens.separator = tokeniser.consume(","); |
| const items = [first]; |
| while (first.tokens.separator) { |
| const item = parser(tokeniser); |
| if (!item) { |
| if (!allowDangler) { |
| tokeniser.error(`Trailing comma in ${listName}`); |
| } |
| break; |
| } |
| item.tokens.separator = tokeniser.consume(","); |
| items.push(item); |
| if (!item.tokens.separator) break; |
| } |
| return items; |
| } |
| |
| /** |
| * @param {import("../tokeniser.js").Tokeniser} tokeniser |
| */ |
| export function const_value(tokeniser) { |
| return ( |
| tokeniser.consumeKind("decimal", "integer") || |
| tokeniser.consume("true", "false", "Infinity", "-Infinity", "NaN") |
| ); |
| } |
| |
| /** |
| * @param {object} token |
| * @param {string} token.type |
| * @param {string} token.value |
| */ |
| export function const_data({ type, value }) { |
| switch (type) { |
| case "decimal": |
| case "integer": |
| return { type: "number", value }; |
| case "string": |
| return { type: "string", value: value.slice(1, -1) }; |
| } |
| |
| switch (value) { |
| case "true": |
| case "false": |
| return { type: "boolean", value: value === "true" }; |
| case "Infinity": |
| case "-Infinity": |
| return { type: "Infinity", negative: value.startsWith("-") }; |
| case "[": |
| return { type: "sequence", value: [] }; |
| case "{": |
| return { type: "dictionary" }; |
| default: |
| return { type: value }; |
| } |
| } |
| |
| /** |
| * @param {import("../tokeniser.js").Tokeniser} tokeniser |
| */ |
| export function primitive_type(tokeniser) { |
| function integer_type() { |
| const prefix = tokeniser.consume("unsigned"); |
| const base = tokeniser.consume("short", "long"); |
| if (base) { |
| const postfix = tokeniser.consume("long"); |
| return new Type({ source, tokens: { prefix, base, postfix } }); |
| } |
| if (prefix) tokeniser.error("Failed to parse integer type"); |
| } |
| |
| function decimal_type() { |
| const prefix = tokeniser.consume("unrestricted"); |
| const base = tokeniser.consume("float", "double"); |
| if (base) { |
| return new Type({ source, tokens: { prefix, base } }); |
| } |
| if (prefix) tokeniser.error("Failed to parse float type"); |
| } |
| |
| const { source } = tokeniser; |
| const num_type = integer_type() || decimal_type(); |
| if (num_type) return num_type; |
| const base = tokeniser.consume( |
| "bigint", |
| "boolean", |
| "byte", |
| "octet", |
| "undefined", |
| ); |
| if (base) { |
| return new Type({ source, tokens: { base } }); |
| } |
| } |
| |
| /** |
| * @param {import("../tokeniser.js").Tokeniser} tokeniser |
| */ |
| export function argument_list(tokeniser) { |
| return list(tokeniser, { |
| parser: Argument.parse, |
| listName: "arguments list", |
| }); |
| } |
| |
| /** |
| * @param {import("../tokeniser.js").Tokeniser} tokeniser |
| * @param {string=} typeName (TODO: See Type.type for more details) |
| */ |
| export function type_with_extended_attributes(tokeniser, typeName) { |
| const extAttrs = ExtendedAttributes.parse(tokeniser); |
| const ret = Type.parse(tokeniser, typeName); |
| if (ret) autoParenter(ret).extAttrs = extAttrs; |
| return ret; |
| } |
| |
| /** |
| * @param {import("../tokeniser.js").Tokeniser} tokeniser |
| * @param {string=} typeName (TODO: See Type.type for more details) |
| */ |
| export function return_type(tokeniser, typeName) { |
| const typ = Type.parse(tokeniser, typeName || "return-type"); |
| if (typ) { |
| return typ; |
| } |
| const voidToken = tokeniser.consume("void"); |
| if (voidToken) { |
| const ret = new Type({ |
| source: tokeniser.source, |
| tokens: { base: voidToken }, |
| }); |
| ret.type = "return-type"; |
| return ret; |
| } |
| } |
| |
| /** |
| * @param {import("../tokeniser.js").Tokeniser} tokeniser |
| */ |
| export function stringifier(tokeniser) { |
| const special = tokeniser.consume("stringifier"); |
| if (!special) return; |
| const member = |
| Attribute.parse(tokeniser, { special }) || |
| Operation.parse(tokeniser, { special }) || |
| tokeniser.error("Unterminated stringifier"); |
| return member; |
| } |
| |
| /** |
| * @param {string} str |
| */ |
| export function getLastIndentation(str) { |
| const lines = str.split("\n"); |
| // the first line visually binds to the preceding token |
| if (lines.length) { |
| const match = lines[lines.length - 1].match(/^\s+/); |
| if (match) { |
| return match[0]; |
| } |
| } |
| return ""; |
| } |
| |
| /** |
| * @param {string} parentTrivia |
| */ |
| export function getMemberIndentation(parentTrivia) { |
| const indentation = getLastIndentation(parentTrivia); |
| const indentCh = indentation.includes("\t") ? "\t" : " "; |
| return indentation + indentCh; |
| } |
| |
| /** |
| * @param {import("./interface.js").Interface} def |
| */ |
| export function autofixAddExposedWindow(def) { |
| return () => { |
| if (def.extAttrs.length) { |
| const tokeniser = new Tokeniser("Exposed=Window,"); |
| const exposed = SimpleExtendedAttribute.parse(tokeniser); |
| exposed.tokens.separator = tokeniser.consume(","); |
| const existing = def.extAttrs[0]; |
| if (!/^\s/.test(existing.tokens.name.trivia)) { |
| existing.tokens.name.trivia = ` ${existing.tokens.name.trivia}`; |
| } |
| def.extAttrs.unshift(exposed); |
| } else { |
| autoParenter(def).extAttrs = ExtendedAttributes.parse( |
| new Tokeniser("[Exposed=Window]"), |
| ); |
| const trivia = def.tokens.base.trivia; |
| def.extAttrs.tokens.open.trivia = trivia; |
| def.tokens.base.trivia = `\n${getLastIndentation(trivia)}`; |
| } |
| }; |
| } |
| |
| /** |
| * Get the first syntax token for the given IDL object. |
| * @param {*} data |
| */ |
| export function getFirstToken(data) { |
| if (data.extAttrs.length) { |
| return data.extAttrs.tokens.open; |
| } |
| if (data.type === "operation" && !data.special) { |
| return getFirstToken(data.idlType); |
| } |
| const tokens = Object.values(data.tokens).sort((x, y) => x.index - y.index); |
| return tokens[0]; |
| } |
| |
| /** |
| * @template T |
| * @param {T[]} array |
| * @param {(item: T) => boolean} predicate |
| */ |
| export function findLastIndex(array, predicate) { |
| const index = array.slice().reverse().findIndex(predicate); |
| if (index === -1) { |
| return index; |
| } |
| return array.length - index - 1; |
| } |
| |
| /** |
| * Returns a proxy that auto-assign `parent` field. |
| * @template {Record<string | symbol, any>} T |
| * @param {T} data |
| * @param {*} [parent] The object that will be assigned to `parent`. |
| * If absent, it will be `data` by default. |
| * @return {T} |
| */ |
| export function autoParenter(data, parent) { |
| if (!parent) { |
| // Defaults to `data` unless specified otherwise. |
| parent = data; |
| } |
| if (!data) { |
| // This allows `autoParenter(undefined)` which again allows |
| // `autoParenter(parse())` where the function may return nothing. |
| return data; |
| } |
| const proxy = new Proxy(data, { |
| get(target, p) { |
| const value = target[p]; |
| if (Array.isArray(value) && p !== "source") { |
| // Wraps the array so that any added items will also automatically |
| // get their `parent` values. |
| return autoParenter(value, target); |
| } |
| return value; |
| }, |
| set(target, p, value) { |
| // @ts-ignore https://github.com/microsoft/TypeScript/issues/47357 |
| target[p] = value; |
| if (!value) { |
| return true; |
| } else if (Array.isArray(value)) { |
| // Assigning an array will add `parent` to its items. |
| for (const item of value) { |
| if (typeof item.parent !== "undefined") { |
| item.parent = parent; |
| } |
| } |
| } else if (typeof value.parent !== "undefined") { |
| value.parent = parent; |
| } |
| return true; |
| }, |
| }); |
| return proxy; |
| } |