UNPKG

markuplint-angular-parser

Version:
200 lines 6.97 kB
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