feat: publish Regex Tools 0.2.0
This commit is contained in:
407
src/components/Pcre2TracePanel.tsx
Normal file
407
src/components/Pcre2TracePanel.tsx
Normal file
@@ -0,0 +1,407 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { Pcre2TraceSupervisor } from "../regex/execution/Pcre2TraceSupervisor";
|
||||
import { WorkerRequestError } from "../regex/execution/WorkerSupervisor";
|
||||
import { DEFAULT_REGEX_LIMITS } from "../regex/execution/request-limits";
|
||||
import type {
|
||||
RegexEngineOptions,
|
||||
RegexFlavourId,
|
||||
} from "../regex/model/flavour";
|
||||
import type { Pcre2TraceEvent, Pcre2TraceResult } from "../regex/model/trace";
|
||||
import type { SourceRange } from "../regex/model/syntax";
|
||||
|
||||
const DEFAULT_TRACE_EVENTS = 5_000;
|
||||
const DEFAULT_TRACE_BYTES = 2 * 1024 * 1024;
|
||||
const MAXIMUM_RENDERED_TRACE_EVENTS = 2_000;
|
||||
|
||||
type TraceState =
|
||||
| { readonly status: "idle"; readonly message: string }
|
||||
| { readonly status: "running"; readonly message: string }
|
||||
| { readonly status: "ready"; readonly message: string }
|
||||
| { readonly status: "timeout"; readonly message: string }
|
||||
| { readonly status: "error"; readonly message: string };
|
||||
|
||||
function movementLabel(event: Pcre2TraceEvent): string {
|
||||
switch (event.movement.classification) {
|
||||
case "first-event":
|
||||
return "first";
|
||||
case "same-position":
|
||||
return "same position";
|
||||
case "apparent-backtrack":
|
||||
return "apparent backtrack";
|
||||
case "forward":
|
||||
return "forward";
|
||||
}
|
||||
}
|
||||
|
||||
export function Pcre2TracePanel({
|
||||
active,
|
||||
flavour,
|
||||
pattern,
|
||||
flags,
|
||||
options,
|
||||
subject,
|
||||
timeoutMs,
|
||||
onClose,
|
||||
onSelectRanges,
|
||||
}: {
|
||||
readonly active: boolean;
|
||||
readonly flavour: RegexFlavourId;
|
||||
readonly pattern: string;
|
||||
readonly flags: readonly string[];
|
||||
readonly options: RegexEngineOptions;
|
||||
readonly subject: string;
|
||||
readonly timeoutMs: number;
|
||||
readonly onClose: () => void;
|
||||
readonly onSelectRanges: (
|
||||
patternRange: SourceRange,
|
||||
subjectRange: SourceRange,
|
||||
) => void;
|
||||
}) {
|
||||
const supervisor = useRef<Pcre2TraceSupervisor | null>(null);
|
||||
const requestRevision = useRef(0);
|
||||
const [maximumEvents, setMaximumEvents] = useState(DEFAULT_TRACE_EVENTS);
|
||||
const [maximumBytes, setMaximumBytes] = useState(DEFAULT_TRACE_BYTES);
|
||||
const [completed, setCompleted] = useState<{
|
||||
readonly result: Pcre2TraceResult;
|
||||
readonly input: {
|
||||
readonly pattern: string;
|
||||
readonly subject: string;
|
||||
readonly flags: string;
|
||||
readonly options: string;
|
||||
};
|
||||
}>();
|
||||
const [state, setState] = useState<TraceState>({
|
||||
status: "idle",
|
||||
message: "Run an isolated PCRE2 automatic-callout trace.",
|
||||
});
|
||||
|
||||
const traceEngine = () => {
|
||||
supervisor.current ??= new Pcre2TraceSupervisor();
|
||||
return supervisor.current;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
return () => supervisor.current?.dispose();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (active) return;
|
||||
supervisor.current?.cancel();
|
||||
}, [active]);
|
||||
|
||||
const limitsValid =
|
||||
Number.isSafeInteger(maximumEvents) &&
|
||||
maximumEvents >= 1 &&
|
||||
maximumEvents <= DEFAULT_REGEX_LIMITS.maximumTraceEvents &&
|
||||
Number.isSafeInteger(maximumBytes) &&
|
||||
maximumBytes >= 32 &&
|
||||
maximumBytes <= DEFAULT_REGEX_LIMITS.maximumTraceBytes;
|
||||
|
||||
const runTrace = async () => {
|
||||
if (flavour !== "pcre2" || !limitsValid) return;
|
||||
const revision = (requestRevision.current += 1);
|
||||
setCompleted(undefined);
|
||||
setState({
|
||||
status: "running",
|
||||
message: "Collecting reported callouts in a dedicated worker…",
|
||||
});
|
||||
try {
|
||||
const next = await traceEngine().trace(
|
||||
{
|
||||
flavour,
|
||||
pattern,
|
||||
flags,
|
||||
options,
|
||||
subject,
|
||||
maximumTraceEvents: maximumEvents,
|
||||
maximumTraceBytes: maximumBytes,
|
||||
},
|
||||
timeoutMs,
|
||||
);
|
||||
if (revision !== requestRevision.current) return;
|
||||
setCompleted({
|
||||
result: next,
|
||||
input: {
|
||||
pattern,
|
||||
subject,
|
||||
flags: flags.join(""),
|
||||
options: JSON.stringify(options),
|
||||
},
|
||||
});
|
||||
const error = next.diagnostics.find(
|
||||
(diagnostic) => diagnostic.severity === "error",
|
||||
);
|
||||
setState(
|
||||
next.accepted
|
||||
? {
|
||||
status: "ready",
|
||||
message: next.truncated
|
||||
? "Bounded trace complete; collection stopped at a configured cap."
|
||||
: "Bounded trace complete.",
|
||||
}
|
||||
: {
|
||||
status: "error",
|
||||
message: error?.message ?? "PCRE2 rejected the trace request.",
|
||||
},
|
||||
);
|
||||
} catch (error) {
|
||||
if (revision !== requestRevision.current) return;
|
||||
if (error instanceof WorkerRequestError && error.kind === "timeout") {
|
||||
setState({
|
||||
status: "timeout",
|
||||
message: `${error.message}; the trace worker was terminated.`,
|
||||
});
|
||||
} else if (
|
||||
error instanceof WorkerRequestError &&
|
||||
error.kind === "cancelled"
|
||||
) {
|
||||
setState({
|
||||
status: "idle",
|
||||
message: "Trace cancelled; its worker was terminated.",
|
||||
});
|
||||
} else {
|
||||
setState({
|
||||
status: "error",
|
||||
message:
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: "The PCRE2 trace worker failed.",
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const resultIsCurrent =
|
||||
completed?.input.pattern === pattern &&
|
||||
completed.input.subject === subject &&
|
||||
completed.input.flags === flags.join("") &&
|
||||
completed.input.options === JSON.stringify(options);
|
||||
const visibleResult = resultIsCurrent ? completed.result : undefined;
|
||||
const visibleState =
|
||||
resultIsCurrent || state.status === "running"
|
||||
? state
|
||||
: {
|
||||
status: "idle" as const,
|
||||
message: "Inputs changed; run a new isolated trace.",
|
||||
};
|
||||
const renderedEvents =
|
||||
visibleResult?.events.slice(0, MAXIMUM_RENDERED_TRACE_EVENTS) ?? [];
|
||||
return (
|
||||
<section className="panel trace-panel" aria-labelledby="trace-heading">
|
||||
<header className="panel-heading">
|
||||
<div>
|
||||
<p className="eyebrow">PCRE2 10.47 · reported automatic callouts</p>
|
||||
<h2 id="trace-heading">Execution trace</h2>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="icon-button"
|
||||
aria-label="Close execution trace"
|
||||
data-dialog-initial-focus
|
||||
onClick={onClose}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</header>
|
||||
<p className="trace-disclosure">
|
||||
This is an actual stream reported by PCRE2_AUTO_CALLOUT for one{" "}
|
||||
<code>pcre2_match()</code> invocation. Movement labels are derived only
|
||||
from adjacent reported subject positions; the stream is not a complete
|
||||
record of every internal engine action.
|
||||
</p>
|
||||
<div className="trace-controls">
|
||||
<label>
|
||||
<span>Maximum events</span>
|
||||
<input
|
||||
type="number"
|
||||
min={1}
|
||||
max={DEFAULT_REGEX_LIMITS.maximumTraceEvents}
|
||||
step={1}
|
||||
value={maximumEvents}
|
||||
onChange={(event) => setMaximumEvents(Number(event.target.value))}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
<span>Maximum serialized bytes</span>
|
||||
<input
|
||||
type="number"
|
||||
min={32}
|
||||
max={DEFAULT_REGEX_LIMITS.maximumTraceBytes}
|
||||
step={1024}
|
||||
value={maximumBytes}
|
||||
onChange={(event) => setMaximumBytes(Number(event.target.value))}
|
||||
/>
|
||||
</label>
|
||||
<button
|
||||
type="button"
|
||||
className="primary-button"
|
||||
disabled={
|
||||
flavour !== "pcre2" ||
|
||||
!limitsValid ||
|
||||
visibleState.status === "running"
|
||||
}
|
||||
onClick={() => void runTrace()}
|
||||
>
|
||||
{visibleState.status === "running" ? "Tracing…" : "Run trace"}
|
||||
</button>
|
||||
{visibleState.status === "running" ? (
|
||||
<button
|
||||
type="button"
|
||||
className="secondary-button"
|
||||
onClick={() => {
|
||||
requestRevision.current += 1;
|
||||
supervisor.current?.cancel();
|
||||
setState({
|
||||
status: "idle",
|
||||
message: "Trace cancelled; its worker was terminated.",
|
||||
});
|
||||
}}
|
||||
>
|
||||
Cancel trace
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
{flavour !== "pcre2" ? (
|
||||
<p className="empty-state" role="status">
|
||||
Select PCRE2 to enable its automatic-callout trace.
|
||||
</p>
|
||||
) : null}
|
||||
<div
|
||||
className={`trace-status status-${visibleState.status}`}
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
>
|
||||
<strong>{visibleState.message}</strong>
|
||||
{visibleResult?.accepted ? (
|
||||
<small>
|
||||
{visibleResult.events.length.toLocaleString()} complete retained
|
||||
events of {visibleResult.totalEventCount.toLocaleString()} callback
|
||||
event(s) observed before return ·{" "}
|
||||
{visibleResult.traceBytes.toLocaleString()} serialized bytes · last
|
||||
complete event{" "}
|
||||
{visibleResult.lastCompleteEvent?.toLocaleString() ?? "none"} ·{" "}
|
||||
native status {visibleResult.nativeMatchStatus} ·{" "}
|
||||
{visibleResult.matched ? "matched" : "not matched or bounded stop"}{" "}
|
||||
· {visibleResult.elapsedMs.toFixed(2)} ms
|
||||
</small>
|
||||
) : null}
|
||||
</div>
|
||||
{visibleResult?.diagnostics.length ? (
|
||||
<ul className="trace-diagnostics">
|
||||
{visibleResult.diagnostics.map((diagnostic) => (
|
||||
<li
|
||||
key={diagnostic.id}
|
||||
className={`diagnostic-${diagnostic.severity}`}
|
||||
>
|
||||
<strong>{diagnostic.severity}</strong> {diagnostic.message}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : null}
|
||||
{visibleResult && visibleResult.events.length > renderedEvents.length ? (
|
||||
<p className="render-limit-banner" role="status">
|
||||
<strong>Trace viewer limited.</strong> Showing the first{" "}
|
||||
{renderedEvents.length.toLocaleString()} of{" "}
|
||||
{visibleResult.events.length.toLocaleString()} complete retained
|
||||
events. The worker result remains bounded by both configured caps.
|
||||
</p>
|
||||
) : null}
|
||||
{renderedEvents.length > 0 ? (
|
||||
<div className="trace-table-scroll">
|
||||
<table className="trace-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Event</th>
|
||||
<th scope="col">Callout</th>
|
||||
<th scope="col">Pattern</th>
|
||||
<th scope="col">Next item</th>
|
||||
<th scope="col">Subject</th>
|
||||
<th scope="col">Captures</th>
|
||||
<th scope="col">Mark</th>
|
||||
<th scope="col">Movement</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{renderedEvents.map((event) => (
|
||||
<tr key={event.eventNumber}>
|
||||
<td>
|
||||
<button
|
||||
type="button"
|
||||
className="trace-event-select"
|
||||
aria-label={`Select trace event ${event.eventNumber}`}
|
||||
onClick={() =>
|
||||
onSelectRanges(
|
||||
{
|
||||
startUtf16:
|
||||
event.reported.nextPatternItem.startUtf16,
|
||||
endUtf16: event.reported.nextPatternItem.endUtf16,
|
||||
},
|
||||
{
|
||||
startUtf16: event.reported.subjectPosition.utf16,
|
||||
endUtf16: event.reported.subjectPosition.utf16,
|
||||
},
|
||||
)
|
||||
}
|
||||
>
|
||||
{event.eventNumber}
|
||||
</button>
|
||||
</td>
|
||||
<td>{event.reported.calloutNumber}</td>
|
||||
<td>
|
||||
{event.reported.patternPosition.utf16} UTF-16
|
||||
<small>
|
||||
{event.reported.patternPosition.nativeByte} byte
|
||||
</small>
|
||||
</td>
|
||||
<td>
|
||||
<code>
|
||||
{pattern.slice(
|
||||
event.reported.nextPatternItem.startUtf16,
|
||||
event.reported.nextPatternItem.endUtf16,
|
||||
) || "end"}
|
||||
</code>
|
||||
<small>
|
||||
{event.reported.nextPatternItem.startNativeByte}…
|
||||
{event.reported.nextPatternItem.endNativeByte} bytes
|
||||
</small>
|
||||
</td>
|
||||
<td>
|
||||
{event.reported.subjectPosition.utf16} UTF-16
|
||||
<small>
|
||||
{event.reported.subjectPosition.nativeByte} byte
|
||||
</small>
|
||||
</td>
|
||||
<td>
|
||||
top {event.reported.captureTop} · last{" "}
|
||||
{event.reported.captureLast}
|
||||
</td>
|
||||
<td>
|
||||
{event.reported.mark ? (
|
||||
<code>{event.reported.mark}</code>
|
||||
) : (
|
||||
"—"
|
||||
)}
|
||||
</td>
|
||||
<td>
|
||||
<span
|
||||
className={`trace-movement trace-${event.movement.classification}`}
|
||||
title={event.movement.explanation}
|
||||
>
|
||||
{movementLabel(event)}
|
||||
</span>
|
||||
<small>
|
||||
{event.movement.subjectDeltaBytes > 0 ? "+" : ""}
|
||||
{event.movement.subjectDeltaBytes} bytes · derived
|
||||
</small>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
) : visibleResult?.accepted ? (
|
||||
<p className="empty-state">No callout event was retained.</p>
|
||||
) : null}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user