url-pattern-list
Version:
Efficiently match URLs against a collection of URL patterns
220 lines • 8.48 kB
JavaScript
import { PrefixTreeNode, RootPrefixTreeNode, FixedPrefixTreeNode, WildcardPrefixTreeNode, FullWildcardPrefixTreeNode, RegexPrefixTreeNode, } from './index.js';
import { Modifier } from './lib/parse-pattern.js';
/**
* Utility class for visualizing the prefix tree structure of a URLPatternList.
* This is intended for debugging and development purposes.
*/
export class URLPatternListVisualizer {
/**
* Generate a text representation of the prefix tree.
*/
static visualizeAsText(patternList, options = {}) {
const { showPatterns = true, maxDepth = Infinity, verbose = false, useUnicode = true, } = options;
const root = patternList._treeRoot;
const lines = [];
lines.push('URLPatternList Prefix Tree:');
lines.push('');
this.#renderNode(root, lines, '', true, 0, maxDepth, showPatterns, verbose, useUnicode);
return lines.join('\n');
}
/**
* Generate a structured representation of the prefix tree.
*/
static visualizeAsStructure(patternList, options = {}) {
const { maxDepth = Infinity } = options;
const root = patternList._treeRoot;
return this.#buildStructureNode(root, 0, maxDepth);
}
/**
* Recursively render a node and its children as text.
*/
static #renderNode(node, lines, prefix, isLast, depth, maxDepth, showPatterns, verbose, useUnicode) {
if (depth > maxDepth) {
return;
}
const connector = useUnicode
? isLast
? '└── '
: '├── '
: isLast
? '`-- '
: '|-- ';
const nodeInfo = this.#getNodeInfo(node, verbose);
lines.push(`${prefix}${connector}${nodeInfo.label}`);
if (verbose && nodeInfo.details) {
const detailPrefix = prefix +
(isLast
? useUnicode
? ' '
: ' '
: useUnicode
? '│ '
: '| ');
lines.push(`${detailPrefix}${useUnicode ? '├─ ' : '|- '}${nodeInfo.details}`);
}
if (showPatterns && node.patterns.length > 0) {
const patternPrefix = prefix +
(isLast
? useUnicode
? ' '
: ' '
: useUnicode
? '│ '
: '| ');
const patternConnector = useUnicode ? '├─ ' : '|- ';
node.patterns.forEach((pattern, index) => {
const isLastPattern = index === node.patterns.length - 1 && node.children.length === 0;
const finalConnector = isLastPattern
? useUnicode
? '└─ '
: '`- '
: patternConnector;
lines.push(`${patternPrefix}${finalConnector}[PATTERN] ${pattern.pattern.pathname}`);
});
}
const childPrefix = prefix +
(isLast ? (useUnicode ? ' ' : ' ') : useUnicode ? '│ ' : '| ');
node.children.forEach((child, index) => {
const isLastChild = index === node.children.length - 1;
this.#renderNode(child, lines, childPrefix, isLastChild, depth + 1, maxDepth, showPatterns, verbose, useUnicode);
});
}
/**
* Build a structured representation of a node.
*/
static #buildStructureNode(node, depth, maxDepth) {
const nodeInfo = this.#getNodeInfo(node, true);
const structureNode = {
label: nodeInfo.label,
nodeType: nodeInfo.type,
patterns: node.patterns.map((p) => p.pattern.pathname),
children: [],
depth,
};
if (depth < maxDepth) {
structureNode.children = node.children.map((child) => this.#buildStructureNode(child, depth + 1, maxDepth));
}
return structureNode;
}
/**
* Extract information about a node for display.
*/
static #getNodeInfo(node, verbose) {
if (node instanceof RootPrefixTreeNode) {
return {
label: '<ROOT>',
type: 'root',
details: verbose
? `${node.patterns.length} patterns, ${node.children.length} children`
: undefined,
};
}
if (node instanceof FixedPrefixTreeNode) {
return {
label: `"${node.value}"`,
type: 'fixed',
details: verbose
? `value: "${node.value}", ${node.patterns.length} patterns, ${node.children.length} children`
: undefined,
};
}
if (node instanceof WildcardPrefixTreeNode) {
const modifierStr = this.#getModifierString(node.modifier);
const prefixSuffix = node.prefix || node.suffix ? ` (${node.prefix}*${node.suffix})` : '';
return {
label: `:param${modifierStr}${prefixSuffix}`,
type: 'wildcard',
details: verbose
? `modifier: ${modifierStr}, prefix: "${node.prefix}", suffix: "${node.suffix}", ${node.patterns.length} patterns, ${node.children.length} children`
: undefined,
};
}
if (node instanceof FullWildcardPrefixTreeNode) {
const modifierStr = this.#getModifierString(node.modifier);
return {
label: `*${modifierStr}`,
type: 'full-wildcard',
details: verbose
? `modifier: ${modifierStr}, ${node.patterns.length} patterns, ${node.children.length} children`
: undefined,
};
}
if (node instanceof RegexPrefixTreeNode) {
return {
label: `/(${node.regexString})/`,
type: 'regex',
details: verbose
? `regex: "${node.regexString}", ${node.patterns.length} patterns, ${node.children.length} children`
: undefined,
};
}
return {
label: '<UNKNOWN>',
type: 'unknown',
details: verbose
? `${node.patterns.length} patterns, ${node.children.length} children`
: undefined,
};
}
/**
* Convert modifier enum to readable string.
*/
static #getModifierString(modifier) {
switch (modifier) {
case Modifier.None:
return '';
case Modifier.Optional:
return '?';
case Modifier.ZeroOrMore:
return '*';
case Modifier.OneOrMore:
return '+';
default:
return `(${modifier})`;
}
}
/**
* Generate statistics about the prefix tree.
*/
static getStatistics(patternList) {
const root = patternList._treeRoot;
const stats = {
totalNodes: 0,
totalPatterns: 0,
maxDepth: 0,
nodeTypeCount: {},
totalChildren: 0,
nodesWithChildren: 0,
};
this.#collectStatistics(root, 0, stats);
const averageBranchingFactor = stats.nodesWithChildren > 0
? stats.totalChildren / stats.nodesWithChildren
: 0;
return {
totalNodes: stats.totalNodes,
totalPatterns: stats.totalPatterns,
maxDepth: stats.maxDepth,
nodeTypeCount: stats.nodeTypeCount,
averageBranchingFactor,
};
}
/**
* Recursively collect statistics about the tree.
*/
static #collectStatistics(node, depth, stats) {
stats.totalNodes++;
stats.totalPatterns += node.patterns.length;
stats.maxDepth = Math.max(stats.maxDepth, depth);
const nodeInfo = this.#getNodeInfo(node, false);
stats.nodeTypeCount[nodeInfo.type] =
(stats.nodeTypeCount[nodeInfo.type] || 0) + 1;
if (node.children.length > 0) {
stats.nodesWithChildren++;
stats.totalChildren += node.children.length;
}
for (const child of node.children) {
this.#collectStatistics(child, depth + 1, stats);
}
}
}
//# sourceMappingURL=visualizer.js.map