UNPKG

polymer-lint

Version:
244 lines (212 loc) 7.32 kB
'use strict'; // SAXParser /** * @external parse5 * @see {@link https://github.com/inikulin/parse5/wiki/Documentation} */ /** * @typedef LocationInfo * @type {Object} * @property {number} line - A 1-based line number * @property {number} col - A 1-based column number * @see {@link https://github.com/inikulin/parse5/wiki/Documentation} */ const parse5 = require('parse5'); const getAttribute = require('./util/getAttribute'); const isValidCustomElementName = require('./util/isValidCustomElementName'); /** * * @typedef ElementAttributes * @description An array of attributes as an array of objects with `name` and * `value` properties. * @type {Array<{name: string, value: string}>} * @memberof SAXParser */ // Events /** * @event startTag * @memberof SAXParser * @see {@link https://github.com/inikulin/parse5/wiki/Documentation#parse5+SAXParser+event_startTag} */ /** * * @event domModuleStartTag * @memberof SAXParser * @description Emitted when the parser encounters a `<dom-module>` start tag. * @param {string} id - The `id` attribute of the dom-module tag * @param {SAXParser.ElementAttributes} attrs - The tag's attributes * @param {boolean} selfClosing - Indicates if the tag is self-closing * @param {LocationInfo} location * The tag source code location info. * @see {@link https://github.com/inikulin/parse5/wiki/Documentation#starttag-name-attrs-selfclosing-location} */ /** * @event domModuleEndTag * @memberof SAXParser * @description Emitted when the parser encounters a `</dom-module>` end tag. * @param {LocationInfo} location * The tag source code location info. * @see {@link https://github.com/inikulin/parse5/wiki/Documentation#parse5+SAXParser+event_endTag} */ /** * @event importTag * @memberof SAXParser * @description Emitted when the parser encounters a * `<link rel="import" href="..."/>` element. * @param {string} href - The import href * @param {LocationInfo} location * The tag source code location info. * @see {@link https://github.com/inikulin/parse5/wiki/Documentation#parse5+SAXParser+event_endTag} */ /** * @event customElementStartTag * @memberof SAXParser * @description Emitted when the parser encounters a custom element's start tag. * @param {string} name - The name of the custom element * @param {SAXParser.ElementAttributes} attrs - The element's attributes * @param {boolean} selfClosing - Indicates if the element is self-closing * @param {LocationInfo} - The tag source code location info * @see {@link https://github.com/inikulin/parse5/wiki/Documentation#starttag-name-attrs-selfclosing-location} */ /** * @event customElementEndTag * @memberof SAXParser * @description Emitted when the parser encounters a custom element's end tag. * @param {string} name - The name of the custom element * @param {LocationInfo} - The tag source code location info * @see {@link https://github.com/inikulin/parse5/wiki/Documentation#parse5+SAXParser+event_endTag} */ /** * @event linterDirective * @memberof SAXParser * @description Emitted when the parser encounters a comment tag with a linter * directive e.g. `<!-- bplint-disable missing-imports -->`. * @param {string} name - The name of the directive e.g. `bplint-disable` * @param {string[]} args - An array of any comma-delimited arguments following the directive name */ /** * @event enterScope * @memberof SAXParser * @description Emitted when the linter enters a scope (i.e. a start tag that is * neither self-closing nor a void element (see `VOID_ELEMENTS`). * @param {LocationInfo} location */ /** * @event leaveScope * @memberof SAXParser * @description Emitted when the linter leaves a scope (i.e. an end tag). * @param {LocationInfo} location */ const SPLIT_DIRECTIVE_ARGS_EXPR = /\s*(?:,\s*)+/; function parseDirectiveArgs(argsStr) { return argsStr.split(SPLIT_DIRECTIVE_ARGS_EXPR).filter(arg => arg.length); } const MATCH_DIRECTIVE_COMMENT = /^\s*(bplint-(?:enable|disable))\s*(.*?)\s*$/; // Elements that are implicitly self-closing const VOID_ELEMENTS = { /* eslint-disable no-multi-spaces */ area: true, base: true, br: true, col: true, embed: true, hr: true, img: true, input: true, keygen: true, link: true, meta: true, param: true, source: true, track: true, wbr: true }; function isVoidElement(name) { return VOID_ELEMENTS[name]; } const onStartTag = Symbol('onStartTag'); const onEndTag = Symbol('onEndTag'); const onComment = Symbol('onComment'); /** * @classdesc A class that extends parse5.SAXParser to add some Polymer-specific * events (see Events above). * @extends external:parse5.SAXParser * @emits SAXParser.event:domModuleStartTag * @emits SAXParser.event:domModuleEndTag * @see {@link https://github.com/inikulin/parse5/wiki/Documentation#parse5+SAXParser} */ class SAXParser extends parse5.SAXParser { /** * @constructor * @override * @param {Object} options * @see {@link https://github.com/inikulin/parse5/wiki/Documentation#new_parse5+SAXParser_new} */ constructor(options) { super(Object.assign({}, { locationInfo: true }, options)); this.on('startTag', this[onStartTag]); this.on('endTag', this[onEndTag]); this.on('comment', this[onComment]); } /** * @param {string} name * @param {Object<string,string>} attrs * @param {boolean} selfClosing * @param {parse5.StartTagLocationInfo} location * * @emits SAXParser.event:customElementStartTag * @emits SAXParser.event:domModuleStartTag * @emits SAXParser.event:importTag * @emits SAXParser.event:enterScope * * @return {void} */ [onStartTag](name, attrs, selfClosing, location) { switch (name) { case 'dom-module': { const id = getAttribute(attrs, 'id'); this.emit('domModuleStartTag', id, attrs, selfClosing, location); break; } case 'link': { const rel = getAttribute(attrs, 'rel'); if (rel === 'import') { const href = getAttribute(attrs, 'href'); this.emit('importTag', href, location); } break; } default: if (isValidCustomElementName(name)) { this.emit('customElementStartTag', name, attrs, selfClosing, location); } } if (!selfClosing && !isVoidElement(name)) { this.emit('enterScope', location); } } /** * @param {string} name * @param {parse5.LocationInfo} location * * @emits SAXParser.event:domModuleEndTag * @emits SAXParser.event:customElementEndTag * @emits SAXParser.event:leaveScope * * @return {void} */ [onEndTag](name, location) { if (name === 'dom-module') { this.emit('domModuleEndTag', location); } else if (isValidCustomElementName(name)) { this.emit('customElementEndTag', name, location); } this.emit('leaveScope', location); } /** * @param {string} text * @param {parse5.LocationInfo} location * * @emits SAXParser.event:linterDirective * * @return {void} */ [onComment](text, location) { if (MATCH_DIRECTIVE_COMMENT.test(text)) { const name = RegExp.$1; const args = RegExp.$2; this.emit('linterDirective', name, parseDirectiveArgs(args), location); } } } module.exports = SAXParser;