Skip to content

Commit

Permalink
[WiP] Initial rewrite work
Browse files Browse the repository at this point in the history
  • Loading branch information
birjj committed Jul 14, 2018
1 parent 41853ac commit d1300a5
Show file tree
Hide file tree
Showing 17 changed files with 610 additions and 920 deletions.
3 changes: 2 additions & 1 deletion .eslintrc.js
Expand Up @@ -18,6 +18,7 @@ module.exports = {
"semi": [
"error",
"always"
]
],
"no-console": "warn",
}
};
35 changes: 0 additions & 35 deletions src/config.js

This file was deleted.

139 changes: 139 additions & 0 deletions src/lib/linting.js
@@ -0,0 +1,139 @@
/**
* @fileoverview The main linting file.
* This is the object responsible for the actual linting of each file.
* Each instance represents a single file being linted, including results and
* current state.
* It receives the parsed AST and rules from ../svglint.js, and then runs each
* rule and gathers the results.
*/
const EventEmitter = require("events").EventEmitter;
const path = require("path");
const Reporter = require("./reporter");
const logger = require("./logger");

const STATES = Object.freeze({
"ignored": -1,
"linting": undefined,
"success": "success",
"warn": "warning",
"error": "error",

"_-1": "ignored",
"_undefined": "linting",
"_success": "success",
"_warning": "warn",
"_error": "error",
});

/**
* Represents a single file that is being linted.
* Contains the status and potential result of the linting.
* @event rule Emitted when a rule is finished
* @event done Emitted when the linting is done
*/
class Linting extends EventEmitter {
/**
* Creates and starts a new linting.
* @param {String} file The file to lint
* @param {AST} ast The AST of the file
* @param {NormalizedRules} rules The rules that represent
*/
constructor(file, ast, rules) {
super();
this.ast = ast;
this.rules = rules;
this.path = file;
this.state = STATES.linting;
this.name = file
? path.relative(process.cwd(), file)
: "";
/** @type Object<string,Reporter> */
this.results = {};

this.lint();
// TODO: add reporter
}

/**
* Starts the linting.
* Errors from rules are safely caught and logged as exceptions from the rule.
*/
lint() {
this.state = STATES.linting;

// keep track of when every rule has finished
const rules = Object.keys(this.rules);
this.activeRules = rules.length;

logger.debug("Started linting", (this.name || "API-provided file"));
logger.debug(" Rules:", rules);

// start every rule
rules.forEach(ruleName => {
// gather results from the rule through a reporter
const reporter = this._generateReporter(ruleName);
const onDone = () => {
this._onRuleFinish(ruleName, reporter);
};

// execute the rule, potentially waiting for async rules
// also handles catching errors from the rule
Promise.resolve()
.then(() => this.rules[ruleName](reporter))
.catch(e => reporter.exception(e))
.then(onDone);
});
}

/**
* Handles a rule finishing.
* @param {String} ruleName The name of the rule that just finished
* @param {Reporter} reporter The reporter containing rule results
* @emits rule
* @private
*/
_onRuleFinish(ruleName, reporter) {
logger.debug("Rule finished", logger.colorize(ruleName));
this.emit("rule", {
name: ruleName,
reporter,
});
this.results[ruleName] = reporter;

--this.activeRules;
if (this.activeRules === 0) {
this.state = this._calculateState();
logger.debug("Linting finished", logger.colorize(STATES["_"+this.state]));
this.emit("done");
}
}

/**
* Calculates the current state from this.results.
* @returns One of the valid states
*/
_calculateState() {
let state = STATES.success;
for (let k in this.results) {
const result = this.results[k];
if (result.errors.length) { return STATES.error; }
if (result.warns.length || state === STATES.warn) {
state = STATES.warn;
}
}
return state;
}

/**
* Generates a Reporter for use with this file.
* Remember to call .done() on it.
* @param {String} ruleName The name of the rule that this reporter is used for
* @private
*/
_generateReporter(ruleName) {
return new Reporter(ruleName);
}
}
Linting.STATES = STATES;

module.exports = Linting;
41 changes: 41 additions & 0 deletions src/lib/logger.js
@@ -0,0 +1,41 @@
/**
* @fileoverview Exposes the logger we should use for displaying info.
* If called using the JS API, this will be `console` with methods prefixed.
* If called using the CLI, this will be our own custom logger.
*/
const chalk = require("chalk");
const inspect = require("util").inspect;

const CONSOLE_COLORS = Object.freeze({
debug: chalk.dim.gray,
log: chalk.blue,
warn: chalk.yellow,
error: chalk.red,
});

const wrappedConsole = Object.create(console);
const prefixRegexp = /^\[([^\s]+)\]$/;
["debug", "log", "warn", "error"].forEach(method => {
const color = CONSOLE_COLORS[method]
? CONSOLE_COLORS[method]
: v => v;

wrappedConsole[method] = function() {
let prefix = "[SVGLint";
const args = [...arguments];
// merge the two prefixes if given
if (typeof args[0] === "string") {
const prefixResult = prefixRegexp.exec(args[0]);
if (prefixResult) {
prefix = prefix + " " + prefixResult[1];
args.shift();
}
}
// eslint-disable-next-line no-console
console[method].apply(console, [color(prefix + "]"), ...args]);
};
});


module.exports = wrappedConsole;
module.exports.colorize = value => inspect(value, true, 2, true);
129 changes: 129 additions & 0 deletions src/lib/parse.js
@@ -0,0 +1,129 @@
/**
* @fileoverview The SVG -> AST parser.
* This handles turning an SVG source into an AST representing it.
* It uses htmlparser2 to parse the source, which it gathers from either
* a string or a file.
*/
const Parser = require("htmlparser2");
const fs = require("fs");

module.exports = {
/**
* Parses an SVG source into an AST
* @param {String} source The source to parse
* @returns {AST} The parsed AST
*/
parseSource(source) {
return normalizeAST(
sourceToAST(source),
source
);
},

/**
* Parses the content of a file into an AST
* @param {String} file The path of the file in question
* @returns {Promise<AST>} The parsed AST
*/
parseFile(file) {
return new Promise((res, rej) => {
fs.readFile(
file,
"utf8",
(err, data) => {
if (err) {
return rej(err);
}
try { return res(module.exports.parseSource(data)); }
catch (e) { return rej(e); }
}
);
});
}
};

/**
* @typedef {Object<string,string>} Attributes
*/
/**
* @typedef Node
* @property {String} type The type of node
* @property {Node} next The next sibling
* @property {Node} prev The previous sibling
* @property {Node} parent The parent of the node
* @property {Number} startIndex The string index at which the element starts
* @property {Number} endIndex The string index at which the element ends
* @property {Number} lineNum The line number at which the element starts
* @property {Number} lineIndex The index in the line at which the element starts
*
* @property {Attributes} [attribs] An object of attributes on the Node
* @property {AST} [children] The children of the Node
* @property {String} [data] If type==="text", the content of the Node
* @property {String} [name] If type!=="text", the tag name
*/
/**
* @typedef {Node[]} AST
* @property {String} source The source that generated the AST
* An AST representing an SVG document (or a list of children).
*/

/**
* Parses an SVG source code into an AST.
* @param {String} source
* @returns {AST} The parsed AST
*/
function sourceToAST(source) {
// @ts-ignore
return Parser.parseDOM(source, {
withStartIndices: true,
withEndIndices: true,
xmlMode: true,
});
}

/**
* Normalizes a Node to the format we want.
* Currently translates the startIndex to a line number+index.
* == MODIFIES THE NODE IN-PLACE! ==
* @param {Node} node The node to normalize
* @param {String} source The string the AST was generated from
*/
function normalizeNode(node, source) {
// calculate the distance from node start to line start
const lineStart = (
source.lastIndexOf("\n", node.startIndex +
// make sure newline text nodes are set to start on the proper line
((node.type === "text" && node.data.startsWith("\n")) ? -1 : 0))
) + 1;
node.lineIndex = node.startIndex - lineStart;

// calculate the line number
let numLines = 0;
let lineIndex = lineStart;
while ((lineIndex = source.lastIndexOf("\n", lineIndex - 1)) !== -1) {
++numLines;
}
node.lineNum = numLines;
return node;
}

/**
* Normalizes the AST returned by htmlparser2 to the format we want.
* Currently translates the startIndex to a line number+index.
* == MODIFIES THE AST IN-PLACE! ==
* @param {AST} ast The AST to normalize
* @param {String} source The source the AST was generated from
* @returns {AST} The normalized AST
*/
function normalizeAST(ast, source) {
const handleNode = node => {
normalizeNode(node, source);
if (node.children) {
node.children.forEach(handleNode);
}
};
ast.forEach(handleNode);
// @ts-ignore
ast.source = source;
return ast;
}

0 comments on commit d1300a5

Please sign in to comment.