debt-collector
Version:
a nodejs tool to identify, track and mesure technical debt
70 lines (69 loc) • 3.1 kB
JavaScript
// Flat single-level alternation avoids catastrophic backtracking.
// The previous nested `(?:\s+(?:...)*)*` pattern caused exponential backtracking
// because `\s+` and `[^>"'{}]` overlap (whitespace is matched by both).
const TAG_REGEX = /<([A-Za-z][A-Za-z0-9._:-]*)((?:[^>"'{}]|"[^"]*"|'[^']*'|\{[^}]*\})*)\s*\/?>/g;
const ATTR_REGEX = /([@:[(]*[A-Za-z_][A-Za-z0-9._:@\-\])]*)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|\{([^}]*)\}))?/g;
const parseTags = (data) => {
const tags = [];
let tagMatch;
while ((tagMatch = TAG_REGEX.exec(data)) !== null) {
const tagName = tagMatch[1];
const attrString = tagMatch[2] ?? '';
const attrs = [];
let attrMatch;
ATTR_REGEX.lastIndex = 0;
while ((attrMatch = ATTR_REGEX.exec(attrString)) !== null) {
attrs.push({
name: attrMatch[1],
value: attrMatch[2] ?? attrMatch[3] ?? attrMatch[4],
});
}
tags.push({ tagName, attrs });
}
TAG_REGEX.lastIndex = 0;
return tags;
};
const matchesFilter = (value, filter) => {
if (filter instanceof RegExp)
return filter.test(value);
if (Array.isArray(filter))
return filter.some((f) => f instanceof RegExp ? f.test(value) : value === f);
return value === filter;
};
const BINDING_SYNTAX_RE = /^\[?\(?(.*?)\)?\]?$/;
const stripBindingSyntax = (name) => BINDING_SYNTAX_RE.exec(name)?.[1] ?? name;
const tagMatchesAttrFilter = (tag, { name, value }) => {
const matchingAttrs = tag.attrs.filter((a) => matchesFilter(a.name, name) ||
matchesFilter(stripBindingSyntax(a.name), name));
if (matchingAttrs.length === 0)
return false;
if (value === undefined)
return true;
return matchingAttrs.some((a) => a.value !== undefined && matchesFilter(a.value, value));
};
// Accepts already-parsed tags so chains derived via .tag()/.attribute() reuse
// the same ParsedTag[] without re-running parseTags on every .count()/.find().
const execute = (tags, tagFilter, attrFilters) => tags.filter((tag) => {
if (tagFilter && !matchesFilter(tag.tagName, tagFilter))
return false;
return attrFilters.every((af) => tagMatchesAttrFilter(tag, af));
}).length;
const createChain = (tags, tagFilter, attrFilters = []) => ({
tag: (filter) => createChain(tags, filter, attrFilters),
attribute: (name, value) => createChain(tags, tagFilter, [...attrFilters, { name, value }]),
count: () => execute(tags, tagFilter, attrFilters),
find: () => (execute(tags, tagFilter, attrFilters) > 0 ? 1 : 0),
});
/**
* Chainable utility for querying DOM-like markup (HTML, JSX, Vue, Angular).
* Supports filtering by tag names, attribute names, and attribute values.
* All filters accept `string | string[] | RegExp`.
*
* @example
* const dm = domMatch(content)
* dm.tag('div').attribute('className').count()
* dm.tag(['input', 'select']).attribute('required').find()
* dm.attribute('disabled').count()
* dm.tag(/^Legacy/).attribute('variant', 'primary').count()
*/
export const domMatch = (data) => createChain(parseTags(data));