Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
17 changed files
with
610 additions
and
920 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -18,6 +18,7 @@ module.exports = { | |
"semi": [ | ||
"error", | ||
"always" | ||
] | ||
], | ||
"no-console": "warn", | ||
} | ||
}; |
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
} |
Oops, something went wrong.