markuplint-angular-parser
Version:
Angular parser for markuplint.
200 lines • 6.97 kB
JavaScript
import {} from '@markuplint/html-parser';
import { Parser } from '@markuplint/parser-utils';
import { parse, visitAll } from 'angular-html-parser';
import { v4 as uuid } from 'uuid';
import attrTokenizer from './attr-tokenizer.js';
import parseSelfClosingSolidus from './parse-self-closing-solidus.js';
const getSourceSpan = (nodeOrSourceSpan) => 'sourceSpan' in nodeOrSourceSpan
? nodeOrSourceSpan.sourceSpan
: nodeOrSourceSpan;
const getRaw = (nodeOrSourceSpan, text) => {
if (!nodeOrSourceSpan) {
return '';
}
const { start, end } = getSourceSpan(nodeOrSourceSpan);
return text.slice(start.offset, end.offset);
};
function nodeMapper(nodeOrSourceSpan, { parentNode, text, simpleToken }) {
const { start, end } = getSourceSpan(nodeOrSourceSpan);
const startOffset = start.offset;
const endOffset = end.offset;
const token = {
uuid: uuid(),
raw: getRaw(nodeOrSourceSpan, text),
startOffset,
endOffset,
startLine: start.line + 1,
endLine: end.line + 1,
startCol: start.col + 1,
endCol: end.col + 1,
};
return simpleToken
? token
: {
...token,
parentNode,
prevNode: null,
nextNode: null,
isFragment: false,
isGhost: false,
};
}
const DOCTYPE_REGEXP = /^<!doctype\s+html\s+public\s*(["'])([^"']*)\1\s*((["'])([^"']*)\4)?.*>$/i;
const visitor = {
visitElement({ startSourceSpan, endSourceSpan, name: nodeName, attrs, children }, { nodeList, namespace, ...options }) {
const partialStartTag = nodeMapper(startSourceSpan, options);
const { text } = options;
const startTagText = getRaw(startSourceSpan, text);
const endTagText = getRaw(endSourceSpan, text);
const attributes = [];
const childNodes = [];
nodeName = nodeName.startsWith(':') ? nodeName.slice(1) : nodeName;
namespace =
attrs.find(attr => attr.name === 'xmlns')?.value ||
(nodeName === 'svg' || nodeName.startsWith('svg:')
? 'http://www.w3.org/2000/svg'
: namespace || 'http://www.w3.org/1999/xhtml');
const selfClosingSolidus = parseSelfClosingSolidus(startTagText, partialStartTag.startLine, partialStartTag.startCol, partialStartTag.startOffset);
const isCustomElement = nodeName.includes('-');
const startTag = {
...partialStartTag,
elementType: isCustomElement ? 'authored' : 'html',
type: 'starttag',
depth: 0,
isFragment: false,
isGhost: false,
nodeName,
namespace,
attributes,
childNodes,
hasSpreadAttr: false,
pairNode: null,
selfClosingSolidus,
tagOpenChar: '<',
tagCloseChar: startTagText === endTagText ? '/>' : '>',
};
visitAll(visitor, attrs, {
parentNode: startTag,
nodeList: attributes,
text,
namespace,
});
visitAll(visitor, children, {
parentNode: startTag,
nodeList: childNodes,
text,
namespace,
});
let endTag = null;
if (startTagText !== endTagText && endTagText) {
startTag.pairNode = endTag = {
...nodeMapper(endSourceSpan, options),
type: 'endtag',
depth: 0,
parentNode: null,
pairNode: startTag,
nodeName,
tagOpenChar: '</',
tagCloseChar: '>',
};
}
nodeList.push(startTag);
if (endTag) {
nodeList.push(endTag);
}
},
visitAttribute(attribute, { nodeList, ...options }) {
const { name, sourceSpan: { start }, value, } = attribute;
const _value = value.trim();
const dynamicName = /^[#*]/.test(name) ||
/^\[[^.[\]]+\]$/.test(name) ||
/^\([^().]+\)$/.test(name);
const dynamicValue = /^\{\{.*\}\}$/.test(_value);
const node = attrTokenizer(getRaw(attribute, options.text), start.line + 1, start.col, start.offset, dynamicName || dynamicValue || undefined);
const potentialName = name
.replace(/^\[attr\./, '')
.replaceAll(/[()*@[\]]/g, '');
node.potentialName = potentialName;
nodeList.push(node);
},
visitText(text, { nodeList, ...options }) {
const node = {
...nodeMapper(text, options),
depth: 0,
type: 'text',
nodeName: '#text',
};
nodeList.push(node);
},
visitCdata(cdata, { nodeList, ...options }) {
const node = {
...nodeMapper(cdata, options),
depth: 0,
type: 'comment',
nodeName: '#comment',
isBogus: false,
};
nodeList.push(node);
},
visitComment(comment, { nodeList, ...options }) {
const node = {
...nodeMapper(comment, options),
depth: 0,
type: 'comment',
nodeName: '#comment',
isBogus: false,
};
nodeList.push(node);
},
visitDocType(docType, { nodeList, ...options }) {
const partialDocType = nodeMapper(docType, options);
const matched = DOCTYPE_REGEXP.exec(partialDocType.raw);
const node = {
...partialDocType,
depth: 0,
type: 'doctype',
name: docType.value.split(/\s/)[0],
nodeName: '#doctype',
publicId: matched?.[2] ?? '',
systemId: matched?.[5] ?? '',
};
nodeList.push(node);
},
visitExpansion(expansion, _context) {
throw new Error('unexpected expansion node: ' +
expansion.toString());
},
visitExpansionCase(expansionCase, _context) {
throw new Error('unexpected expansionCase node: ' +
expansionCase.toString());
},
visitBlock() {
},
visitBlockParameter() {
},
visitLetDeclaration() {
},
};
export class AngularParser extends Parser {
parse(text) {
const { rootNodes, errors } = parse(text);
const nodeList = [];
visitAll(visitor, rootNodes, {
parentNode: null,
nodeList,
text,
});
const document = {
raw: text,
nodeList: this.flattenNodes(nodeList),
isFragment: !nodeList.some(node => node.type === 'doctype' ||
(node.type === 'starttag' && node.nodeName.toLowerCase() === 'html')),
};
if (errors.length > 0) {
document.unknownParseError = errors.map(err => err.toString()).join('\n');
}
return document;
}
}
export const parser = new AngularParser();
//# sourceMappingURL=index.js.map