tenfourfox/testing/eslint-plugin-mozilla/lib/processors/xbl-bindings.js
Cameron Kaiser c9b2922b70 hello FPR
2017-04-19 00:56:45 -07:00

250 lines
7.3 KiB
JavaScript

/**
* @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])
}
}
// 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.oncdata = this.onText.bind(this);
this.document = {
local: "#document",
uri: null,
children: [],
}
this._currentNode = this.document;
}
XMLParser.prototype = {
parser: null,
onOpenTag: function(tag) {
let node = {
parentNode: this._currentNode,
local: tag.local,
namespace: tag.uri,
attributes: {},
children: [],
textContent: "",
textStart: 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 = this._currentNode.parentNode;
},
onText: function(text) {
this._currentNode.textContent += text;
}
}
// Strips the indentation from lines of text and adds a fixed two spaces indent
function reindent(text) {
let lines = text.split("\n");
// The last line is likely indentation for the XML closing tag.
if (lines[lines.length - 1].trim() == "") {
lines.pop();
}
if (!lines.length) {
return "";
}
// Find the preceeding whitespace for all lines that aren't entirely whitespace
let indents = lines.filter(s => 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);
// Strip off the found indent level and prepend the new indent level, but only
// if the string isn't already empty.
lines = lines.map(s => s.length > 0 ? " " + s.substring(minIndent) : s);
return lines.join("\n") + "\n";
}
// -----------------------------------------------------------------------------
// Processor Definition
// -----------------------------------------------------------------------------
// Stores any XML parse error
let xmlParseError = null;
// Stores the starting line for each script block generated
let blockLines = [];
module.exports = {
preprocess: function(text, filename) {
xmlParseError = null;
blockLines = [];
// 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 [];
}
let scripts = [];
for (let binding of bindings.children) {
if (binding.local != "binding" || binding.namespace != NS_XBL) {
continue;
}
for (let part of binding.children) {
if (part.namespace != NS_XBL) {
continue;
}
if (part.local != "implementation" && part.local != "handlers") {
continue;
}
for (let item of part.children) {
if (item.namespace != NS_XBL) {
continue;
}
switch (item.local) {
case "field": {
// Fields get converted into variable declarations
let def = item.textContent.trimRight();
// Ignore empty fields
if (def.trim().length == 0) {
continue;
}
blockLines.push(item.textStart);
scripts.push(`let ${item.attributes.name} = ${def}\n`);
break;
}
case "constructor":
case "destructor": {
// Constructors and destructors become function declarations
blockLines.push(item.textStart);
let content = reindent(item.textContent);
scripts.push(`function ${item.local}() {${content}}\n`);
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];
blockLines.push(body.textStart);
body = reindent(body.textContent);
scripts.push(`function ${item.attributes.name}(${params}) {${body}}\n`)
break;
}
case "property": {
// Properties become one or two function declarations
for (let propdef of item.children) {
if (propdef.namespace != NS_XBL) {
continue;
}
blockLines.push(propdef.textStart);
let content = reindent(propdef.textContent);
let params = propdef.local == "setter" ? "val" : "";
scripts.push(`function ${item.attributes.name}_${propdef.local}(${params}) {${content}}\n`);
}
break;
}
case "handler": {
// Handlers become a function declaration with an `event` parameter
blockLines.push(item.textStart);
let content = reindent(item.textContent);
scripts.push(`function on${item.attributes.event}(event) {${content}}\n`);
break;
}
default:
continue;
}
}
}
}
return scripts;
},
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++) {
let line = blockLines[i];
for (let message of messages[i]) {
message.line += line;
errors.push(message);
}
}
return errors;
}
};