/** * @fileoverview Converts functions and handlers from XBL bindings into JS * functions * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ "use strict"; const NS_XBL = "http://www.mozilla.org/xbl"; let sax = require("sax"); // Converts sax's error message to something that eslint will understand let errorRegex = /(.*)\nLine: (\d+)\nColumn: (\d+)\nChar: (.*)/ function parseError(err) { let matches = err.message.match(errorRegex); if (!matches) return null; return { fatal: true, message: matches[1], line: parseInt(matches[2]) + 1, column: parseInt(matches[3]) } } let entityRegex = /&[\w][\w-\.]*;/g; // A simple sax listener that generates a tree of element information function XMLParser(parser) { this.parser = parser; parser.onopentag = this.onOpenTag.bind(this); parser.onclosetag = this.onCloseTag.bind(this); parser.ontext = this.onText.bind(this); parser.onopencdata = this.onOpenCDATA.bind(this); parser.oncdata = this.onCDATA.bind(this); parser.oncomment = this.onComment.bind(this); this.document = { local: "#document", uri: null, children: [], comments: [], } this._currentNode = this.document; } XMLParser.prototype = { parser: null, onOpenTag: function(tag) { let node = { parentNode: this._currentNode, local: tag.local, namespace: tag.uri, attributes: {}, children: [], comments: [], textContent: "", textLine: this.parser.line, textColumn: this.parser.column, textEndLine: this.parser.line } for (let attr of Object.keys(tag.attributes)) { if (tag.attributes[attr].uri == "") { node.attributes[attr] = tag.attributes[attr].value; } } this._currentNode.children.push(node); this._currentNode = node; }, onCloseTag: function(tagname) { this._currentNode.textEndLine = this.parser.line; this._currentNode = this._currentNode.parentNode; }, addText: function(text) { this._currentNode.textContent += text; }, onText: function(text) { // Replace entities with some valid JS token. this.addText(text.replace(entityRegex, "null")); }, onOpenCDATA: function() { // Turn the CDATA opening tag into whitespace for indent alignment this.addText(" ".repeat(" s.trim().length > 0) .map(s => s.length - s.trimLeft().length); // Find the smallest indent level in use let minIndent = Math.min.apply(null, indents); for (let line of lines) { if (line.trim().length == 0) { // Don't offset lines that are only whitespace, the only possible JS error // is trailing whitespace and we want it to point at the right place lineMap[scriptLines.length] = { line: startLine, offset: 0 }; } else { line = " ".repeat(reindent * INDENT_LEVEL) + line.substring(minIndent); lineMap[scriptLines.length] = { line: startLine, offset: reindent * INDENT_LEVEL - (minIndent - 1) }; } scriptLines.push(line); startLine++; } } module.exports = { preprocess: function(text, filename) { xmlParseError = null; scriptLines = []; lineMap = []; // Non-strict allows us to ignore many errors from entities and // preprocessing at the expense of failing to report some XML errors. // Unfortunately it also throws away the case of tagnames and attributes let parser = sax.parser(false, { lowercase: true, xmlns: true, }); parser.onerror = function(err) { xmlParseError = parseError(err); } let xp = new XMLParser(parser); parser.write(text); // Sanity checks to make sure we're dealing with an XBL document let document = xp.document; if (document.children.length != 1) { return []; } let bindings = document.children[0]; if (bindings.local != "bindings" || bindings.namespace != NS_XBL) { return []; } for (let comment of document.comments) { addSyntheticLine(`/*`, 0, true); for (let line of comment.split("\n")) { addSyntheticLine(`${line.trim()}`, 0, true); } addSyntheticLine(`*/`, 0, true); } addSyntheticLine(`this.bindings = {`, bindings.textLine); for (let binding of bindings.children) { if (binding.local != "binding" || binding.namespace != NS_XBL) { continue; } addSyntheticLine(indent(1) + `"${binding.attributes.id}": {`, binding.textLine); for (let part of binding.children) { if (part.namespace != NS_XBL) { continue; } if (part.local == "implementation") { addSyntheticLine(indent(2) + `implementation: {`, part.textLine); } else if (part.local == "handlers") { addSyntheticLine(indent(2) + `handlers: [`, part.textLine); } else { continue; } for (let item of part.children) { if (item.namespace != NS_XBL) { continue; } switch (item.local) { case "field": { // Fields are something like lazy getter functions // Ignore empty fields if (item.textContent.trim().length == 0) { continue; } addSyntheticLine(indent(3) + `get ${item.attributes.name}() {`, item.textLine); addSyntheticLine(indent(4) + `return (`, item.textLine); // Remove trailing semicolons, as we are adding our own item.textContent = item.textContent.replace(/;(?=\s*$)/, ""); addNodeLines(item, 5); addSyntheticLine(indent(4) + `);`, item.textLine); addSyntheticLine(indent(3) + `},`, item.textEndLine); break; } case "constructor": case "destructor": { // Constructors and destructors become function declarations addSyntheticLine(indent(3) + `${item.local}() {`, item.textLine); addNodeLines(item, 4); addSyntheticLine(indent(3) + `},`, item.textEndLine); break; } case "method": { // Methods become function declarations with the appropriate params let params = item.children.filter(n => n.local == "parameter" && n.namespace == NS_XBL) .map(n => n.attributes.name) .join(", "); let body = item.children.filter(n => n.local == "body" && n.namespace == NS_XBL)[0]; addSyntheticLine(indent(3) + `${item.attributes.name}(${params}) {`, item.textLine); addNodeLines(body, 4); addSyntheticLine(indent(3) + `},`, item.textEndLine); break; } case "property": { // Properties become one or two function declarations for (let propdef of item.children) { if (propdef.namespace != NS_XBL) { continue; } if (propdef.local == "setter") { addSyntheticLine(indent(3) + `set ${item.attributes.name}(val) {`, propdef.textLine); } else if (propdef.local == "getter") { addSyntheticLine(indent(3) + `get ${item.attributes.name}() {`, propdef.textLine); } else { continue; } addNodeLines(propdef, 4); addSyntheticLine(indent(3) + `},`, propdef.textEndLine); } break; } case "handler": { // Handlers become a function declaration with an `event` parameter addSyntheticLine(indent(3) + `function(event) {`, item.textLine); addNodeLines(item, 4); addSyntheticLine(indent(3) + `},`, item.textEndLine); break; } default: continue; } } addSyntheticLine(indent(2) + (part.local == "implementation" ? `},` : `],`), part.textEndLine); } addSyntheticLine(indent(1) + `},`, binding.textEndLine); } addSyntheticLine(`};`, bindings.textEndLine); let script = scriptLines.join("\n") + "\n"; return [script]; }, postprocess: function(messages, filename) { // If there was an XML parse error then just return that if (xmlParseError) { return [xmlParseError]; } // For every message from every script block update the line to point to the // correct place. let errors = []; for (let i = 0; i < messages.length; i++) { for (let message of messages[i]) { // ESLint indexes lines starting at 1 but our arrays start at 0 let mapped = lineMap[message.line - 1]; message.line = mapped.line + 1; if (mapped.offset) { message.column -= mapped.offset; } else { message.column = NaN; } errors.push(message); } } return errors; } };