feat: publish Regex Tools 0.1.0

This commit is contained in:
2026-07-24 18:04:21 +02:00
commit 873b9b218d
136 changed files with 24212 additions and 0 deletions

View File

@@ -0,0 +1,223 @@
import { useMemo } from "react";
import type { ExtractionNode } from "../regex/model/match";
import { handleTreeNavigation } from "./tree-navigation";
const MAXIMUM_RENDERED_EXTRACTION_NODES = 2_000;
const MAXIMUM_VALUE_PREVIEW_UTF16 = 100;
export interface ExtractionTreeProps {
readonly nodes: readonly ExtractionNode[];
readonly selectedId?: string;
readonly onSelect: (node: ExtractionNode) => void;
readonly repeatedCaptureNumbers: ReadonlySet<number>;
}
function printable(node: ExtractionNode): string {
const value = node.value;
if (value === undefined) return "—";
if (value === "") return "∅ empty";
const visible = value
.slice(0, MAXIMUM_VALUE_PREVIEW_UTF16)
.replaceAll("\n", "↵")
.replaceAll("\r", "␍")
.replaceAll("\t", "⇥");
const exactLength = node.range
? node.range.endUtf16 - node.range.startUtf16
: undefined;
const uiTruncated = value.length > MAXIMUM_VALUE_PREVIEW_UTF16;
if (node.status === "truncated") {
return `${visible}${uiTruncated ? "…" : ""} (engine value prefix: ${value.length.toLocaleString()} of ${exactLength?.toLocaleString() ?? "unknown"} UTF-16 units${uiTruncated ? "; UI shows the first 100" : ""})`;
}
return uiTruncated
? `${visible}… (${(exactLength ?? value.length).toLocaleString()} UTF-16 units total; UI preview)`
: visible;
}
function countNodes(nodes: readonly ExtractionNode[]): number {
let count = 0;
const pending = [...nodes];
while (pending.length > 0) {
const node = pending.pop();
if (!node) continue;
count += 1;
pending.push(...node.children);
}
return count;
}
function boundedNodes(
nodes: readonly ExtractionNode[],
maximum: number,
): readonly ExtractionNode[] {
let remaining = maximum;
const takeNode = (node: ExtractionNode): ExtractionNode | undefined => {
if (remaining <= 0) return undefined;
remaining -= 1;
const children: ExtractionNode[] = [];
for (const child of node.children) {
const visible = takeNode(child);
if (!visible) break;
children.push(visible);
}
return { ...node, children };
};
const visible: ExtractionNode[] = [];
for (const node of nodes) {
const bounded = takeNode(node);
if (!bounded) break;
visible.push(bounded);
}
return visible;
}
function containsNode(nodes: readonly ExtractionNode[], id: string): boolean {
const pending = [...nodes];
while (pending.length > 0) {
const node = pending.pop();
if (!node) continue;
if (node.id === id) return true;
pending.push(...node.children);
}
return false;
}
function ExtractionTreeNode({
node,
selectedId,
onSelect,
repeatedCaptureNumbers,
depth,
focusableId,
}: {
readonly node: ExtractionNode;
readonly selectedId?: string;
readonly onSelect: (node: ExtractionNode) => void;
readonly repeatedCaptureNumbers: ReadonlySet<number>;
readonly depth: number;
readonly focusableId: string;
}) {
const repeatedFinal =
node.groupNumber !== undefined &&
repeatedCaptureNumbers.has(node.groupNumber);
return (
<li role="none">
<button
type="button"
role="treeitem"
aria-level={depth}
aria-selected={node.id === selectedId}
tabIndex={node.id === focusableId ? 0 : -1}
className={`tree-row extraction-row ${
node.id === selectedId ? "is-selected" : ""
}`}
onClick={() => onSelect(node)}
onKeyDown={handleTreeNavigation}
>
<span className="tree-token">{node.label}</span>
<span className="tree-explanation">{printable(node)}</span>
<span className={`status status-${node.status}`}>
{node.status.replaceAll("-", " ")}
</span>
<span className="tree-details extraction-details">
<span>
UTF-16 range:{" "}
{node.range
? `${node.range.startUtf16}${node.range.endUtf16}`
: "unavailable"}
</span>
<span>
native range:{" "}
{node.nativeRange
? `${node.nativeRange.start}${node.nativeRange.end} ${node.nativeRange.unit}`
: "unavailable"}
</span>
<span>provenance: {node.provenance}</span>
</span>
</button>
{repeatedFinal ? (
<p className="tree-note">
This engine exposes only the final retained capture for this repeated
group.
</p>
) : null}
{node.children.length > 0 ? (
<ul role="group">
{node.children.map((child) => (
<ExtractionTreeNode
key={child.id}
node={child}
selectedId={selectedId}
onSelect={onSelect}
repeatedCaptureNumbers={repeatedCaptureNumbers}
depth={depth + 1}
focusableId={focusableId}
/>
))}
</ul>
) : null}
</li>
);
}
export function ExtractionTree({
nodes,
selectedId,
onSelect,
repeatedCaptureNumbers,
}: ExtractionTreeProps) {
const totalNodes = useMemo(() => countNodes(nodes), [nodes]);
const visibleNodes = useMemo(
() => boundedNodes(nodes, MAXIMUM_RENDERED_EXTRACTION_NODES),
[nodes],
);
const renderedNodes = Math.min(totalNodes, MAXIMUM_RENDERED_EXTRACTION_NODES);
const selectedNodeVisible =
selectedId === undefined || containsNode(visibleNodes, selectedId);
const focusableId =
selectedId && selectedNodeVisible
? selectedId
: (visibleNodes[0]?.id ?? "");
return (
<section className="panel tree-panel" aria-labelledby="extraction-heading">
<header className="panel-heading">
<div>
<p className="eyebrow">Engine results</p>
<h2 id="extraction-heading">Extraction tree</h2>
</div>
<span className="provenance-badge is-engine">Actual engine result</span>
</header>
{totalNodes > renderedNodes ? (
<p
className="render-limit-banner"
role="status"
data-testid="extraction-render-limit"
>
<strong>Tree preview limited.</strong> Showing the first{" "}
{renderedNodes.toLocaleString()} of {totalNodes.toLocaleString()}{" "}
returned result nodes. All returned data remains available to the
other result views and exports.
{!selectedNodeVisible
? " The selected result is outside this bounded tree preview."
: ""}
</p>
) : null}
{nodes.length > 0 ? (
<ul className="tree" role="tree" aria-label="Match extraction">
{visibleNodes.map((node) => (
<ExtractionTreeNode
key={node.id}
node={node}
selectedId={selectedId}
onSelect={onSelect}
repeatedCaptureNumbers={repeatedCaptureNumbers}
depth={1}
focusableId={focusableId}
/>
))}
</ul>
) : (
<p className="empty-state">No matches to extract.</p>
)}
</section>
);
}