diff --git a/.gitignore b/.gitignore index 4983efd..1c41cd6 100644 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,9 @@ public/workbox-* .yalc yalc.lock .vercel + +# react-prosemirror perf benchmark artifacts +articles/react-prosemirror/e2e/perf/results +test-results +playwright-report +*.tsbuildinfo diff --git a/articles/react-prosemirror/README.md b/articles/react-prosemirror/README.md new file mode 100644 index 0000000..3166a89 --- /dev/null +++ b/articles/react-prosemirror/README.md @@ -0,0 +1,194 @@ +# react-prosemirror vs vanilla ProseMirror vs Tiptap 3 — perf harness + +Supporting code for the blog post **[react-prosemirror.tsx](../../pages/blog/react-prosemirror.tsx)** +(`/blog/react-prosemirror`). It benchmarks the same minimal rich-text editor +built three ways and measures what each abstraction layer costs at scale +(memory, script time, layout/recalc, keystroke latency). + +The editors render into the **main blog Next app** as ordinary Pages-Router +routes, and a set of [Playwright](https://playwright.dev/) specs drive them via +the Chrome DevTools Protocol to collect metrics, then render comparison graphs. + +## The three implementations + +| impl | library | route | notes | +| ---- | ------- | ----- | ----- | +| `v3` | [`@handlewithcare/react-prosemirror`](https://github.com/handlewithcarecollective/react-prosemirror) | [`/perf-react-prosemirror`](../../pages/perf-react-prosemirror.tsx) | barebone, no custom nodeviews | +| `v4` | vanilla `prosemirror-view` | [`/perf-vanilla-prosemirror`](../../pages/perf-vanilla-prosemirror.tsx) | no React reconciliation — the floor | +| `v5` | [Tiptap 3](https://tiptap.dev/) | [`/perf-tiptap`](../../pages/perf-tiptap.tsx) | `Document + Paragraph + Text + History` only | + +Each has a **`-snv` ("static nodeview")** sibling that adds a realistic custom +per-paragraph nodeview (authorship gutter: avatar + chip), to measure custom +nodeview cost without context-driven re-render pathology: + +| impl | route | +| ---- | ----- | +| `v3-snv` | [`/perf-react-prosemirror-snv`](../../pages/perf-react-prosemirror-snv.tsx) | +| `v4-snv` | [`/perf-vanilla-prosemirror-snv`](../../pages/perf-vanilla-prosemirror-snv.tsx) | +| `v5-snv` | [`/perf-tiptap-snv`](../../pages/perf-tiptap-snv.tsx) | + +> The reactive `-nv` variants (`v3-nv` / `v4-nv` / `v5-nv`) and the `ctx-flip` +> scenario are referenced by the harness but their pages are **not yet wired**; +> add `editors/perf-v{3,4,5}-nv/page.tsx` + matching `pages/` routes to enable +> them. + +### Page query params + +- `?n=` — seed the doc with `N` paragraphs on load (cold-load / pre-sized runs). +- `?content=tech` — (snv pages) seed richer headings/code/quotes instead of empty paragraphs. + +### Browser hooks + +Every page exposes two `window` hooks the specs rely on: + +- `window.__perfNodes: number` — current top-level child count. +- `window.__perfGrow(k: number)` — append `k` empty paragraphs (used to ramp doc size). + +## Layout + +``` +articles/react-prosemirror/ +├── editors/ # editor React components (source of truth) +│ ├── lib/perf-gutter-style.ts # shared nodeview styling +│ ├── perf-v3/page.tsx # …and v3-snv, v4, v4-snv, v5, v5-snv +├── e2e/perf/ +│ ├── constants.ts # impl matrix, ports, thresholds, routeFor() +│ ├── stress.spec.ts # type-until-MAX_NODES; CDP metric sampling +│ ├── nodeview.spec.ts # scenarios: typing / cold-load / cursor / ctx-flip / keystroke-* +│ ├── keystroke-ramp.spec.ts # ramp doc size until p95 INP > threshold +│ ├── inp-stats.ts # per-interaction INP percentile math +│ └── create-graphs.ts # results JSON → PNG charts (chartjs-node-canvas) +├── scripts/ +│ ├── run-editor-perf.ts # orchestrate stress across v3/v4/v5 +│ ├── run-nodeview-perf.ts # orchestrate nodeview impls × scenarios +│ └── run-keystroke-ramp.ts # orchestrate keystroke-ramp across impls +├── playwright.perf.config.ts # boots the blog (next start / next dev) +└── tsconfig.json # typechecks this harness (excluded from main build) +``` + +The `editors/*/page.tsx` files are the source; the blog routes in +[`pages/perf-*.tsx`](../../pages) are thin `dynamic(..., { ssr: false })` +wrappers around them (client-only, to avoid hydration mismatch from `?n=`). +The `@/lib/*` import alias resolves to `editors/lib/*` (see root `tsconfig.json`). + +## Running + +All commands are run from the **repo root**. The npm scripts default to a +production build (`next build` once, then `next start` on port 3100) for +representative numbers. + +```bash +npm run perf # stress benchmark for v3/v4/v5 + graphs +npm run perf:nodeview # nodeview spec: typing + cold-load scenarios + graphs +npm run perf:keystroke-ramp # keystroke latency ramp for the -snv impls + graphs +npm run perf:test # raw `playwright test` (pass a spec path / EDITOR_IMPL yourself) +``` + +### Fast iteration (dev server, no build) + +```bash +PERF_DEV=1 npm run perf # orchestrator boots `next dev` instead of build+start +# or reuse a server you already have running: +npx next dev -p 3100 & +cd articles/react-prosemirror +EDITOR_IMPL=v3 PERF_MAX_NODES=300 \ + npx playwright test --config=playwright.perf.config.ts e2e/perf/stress.spec.ts +``` + +(`reuseExistingServer` is on, so any server already listening on the perf port +is reused.) + +## Environment variables + +Selection / orchestration: + +| var | default | meaning | +| --- | ------- | ------- | +| `EDITOR_IMPL` | `v1` | which impl a single spec run targets (`v3`/`v4`/`v5`/`-snv`/`-nv`) | +| `PERF_SCENARIO` | `typing` | nodeview scenario: `typing` `cold-load` `cursor` `ctx-flip` `keystroke-latency` `keystroke-inp` | +| `PERF_IMPLS` / `PERF_NV_IMPLS` / `PERF_RAMP_IMPLS` | per script | comma list the orchestrators iterate | +| `PERF_NV_SCENARIOS` | `typing,cold-load` | scenarios `run-nodeview-perf.ts` iterates | +| `PERF_DEV` | – | `1` → `next dev` instead of build+start | +| `PERF_SKIP_BUILD` | – | `1` → reuse an existing production build | + +Tuning (use low values for quick runs): + +| var | default | meaning | +| --- | ------- | ------- | +| `PERF_MAX_NODES` | `2000` (`20000` if `PERF_FULL=1`) | stress/nodeview ceiling | +| `PERF_NODECOUNT_STEP` | `200` | nodes per checkpoint sample | +| `PERF_MEASUREMENT_MS` | `2000` | CDP metric polling interval | +| `PERF_RAMP_STEP` | `500` | nodes added per keystroke-ramp iteration | +| `PERF_RAMP_PRESSES` | `150` | keystrokes measured per iteration | +| `PERF_RAMP_MAX_ITER` | `30` | safety cap on ramp iterations | +| `PERF_LAG_P95_MS` | `100` | p95 INP threshold that defines "laggy" | +| `PERF_TIMEOUT_MS` / `PERF_GLOBAL_TIMEOUT_MS` | 10min / 25min | per-test / whole-run timeouts | +| `PERF_STRESS_WALL` | – | `1` → keep typing until the editor breaks (heap/slow-batch/crash); writes `*-stress-wall-*` sidecars | +| `PERF_WEB_PORT` | `3100` | port the blog is served on | + +**Quick smoke (~1 min, low numbers):** + +```bash +npx next dev -p 3100 & +cd articles/react-prosemirror +for impl in v3 v4 v5; do + EDITOR_IMPL=$impl PERF_MAX_NODES=300 PERF_MEASUREMENT_MS=500 PERF_NODECOUNT_STEP=100 \ + npx playwright test --config=playwright.perf.config.ts e2e/perf/stress.spec.ts +done +``` + +## Output + +Specs write JSON to `e2e/perf/results/` (gitignored): + +- `-perfMetrics.json` — CDP metric time-series (`JSHeapUsedSize`, `ScriptDuration`, `LayoutCount`, `RecalcStyleCount`, `TaskDuration`, `Nodes`). +- `-nodecount.json` — nodes vs elapsed-ms. +- `--*.json`, `-keystroke-ramp-summary.json`, `*.trace.json` — scenario-specific outputs (the trace files load into Chrome DevTools' Performance tab). + +`create-graphs.ts` renders these to per-impl and `combined-*` PNGs in the same +folder. The curated charts that ship with the post live in +[`public/blog/react-prosemirror/`](../../public/blog/react-prosemirror). + +## Patched dependency: react-prosemirror `reactKeys` memory leak + +`@handlewithcare/react-prosemirror@3.0.6` ships a memory leak in its +`reactKeys()` plugin that the `v3`/`v3-snv` benchmarks would otherwise pin on +the library unfairly. We carry the same fix the upstream `proof` app uses, via +[`patch-package`](https://github.com/ds300/patch-package): + +- **Patch file:** [`patches/@handlewithcare+react-prosemirror+3.0.6.patch`](../../patches) +- **Auto-applied** on every install by the root `"postinstall": "patch-package"` script. + +**The bug.** The plugin's `apply(tr, value, …)` returns a freshly allocated +`{ posToKey, keyToPos }` (two new `Map`s) on every document-changing +transaction. React fiber `memoizedState` pins those objects for every memoized +node view, so each *unchanged* node keeps holding the plugin-state snapshot from +the time it last rendered. At `N` nodes this retains `Σ(1..N)` Map entries — +**O(N²)** heap growth — which shows up directly as runaway `JSHeapUsedSize` in +the stress / keystroke-ramp runs. + +**The fix.** Keep stable identity for the outer object *and* its two `Map`s by +mutating them in place: compute the next contents in temporary Maps (so +iteration over the old Map isn't disturbed), then `clear()` and re-fill the +originals, returning the same `value`. Mutating across an `EditorState` boundary +is normally a smell, but `prosemirror-history` doesn't snapshot plugin state and +the only consumer (`useReactKeys`) always reads from the latest `view.state`, so +observable behaviour is unchanged. Both the `dist/cjs` and `dist/esm` builds are +patched. + +> Regenerating / updating the patch: edit +> `node_modules/@handlewithcare/react-prosemirror/dist/{cjs,esm}/plugins/reactKeys.js`, +> then run `npx patch-package @handlewithcare/react-prosemirror`. If the pinned +> version ever changes, delete the old patch file and regenerate. + +## Requirements + +Runtime deps are in the blog's root `package.json` +(`@handlewithcare/react-prosemirror` + a pinned `react-reconciler@0.29.2` for +React 18, `@tiptap/*` v3) — plus the `patch-package` postinstall fix above. Test +deps: `@playwright/test`, `tsx`, `chartjs-node-canvas`, `chart.js`. Install the +browser once with: + +```bash +npx playwright install chromium +``` diff --git a/articles/react-prosemirror/e2e/perf/constants.ts b/articles/react-prosemirror/e2e/perf/constants.ts new file mode 100644 index 0000000..a57d8bc --- /dev/null +++ b/articles/react-prosemirror/e2e/perf/constants.ts @@ -0,0 +1,228 @@ +/// + +import { join } from "node:path"; + +const FULL = process.env.PERF_FULL === "1"; + +export type EditorImpl = + | "v1" + | "v2" + | "v3" + | "v4" + | "v5" + | "v3-nv" + | "v4-nv" + | "v5-nv" + | "v3-snv" + | "v4-snv" + | "v5-snv"; + +export const EDITOR_IMPL: EditorImpl = (process.env.EDITOR_IMPL as EditorImpl) ?? "v1"; + +export type PerfScenario = + | "typing" + | "cold-load" + | "cursor" + | "ctx-flip" + | "keystroke-latency" + | "keystroke-inp"; + +export const PERF_SCENARIO: PerfScenario = (process.env.PERF_SCENARIO as PerfScenario) ?? "typing"; + +export const PERF_COMPLEXITY = Number(process.env.PERF_COMPLEXITY ?? 3); + +export const PERF_SCENARIOS: readonly PerfScenario[] = [ + "typing", + "cold-load", + "cursor", + "ctx-flip", + "keystroke-latency", + "keystroke-inp", +] as const; + +export const MAX_NODES = Number(process.env.PERF_MAX_NODES ?? (FULL ? 20_000 : 2_000)); +export const MEASUREMENT_INTERVAL = Number(process.env.PERF_MEASUREMENT_MS ?? 2000); +export const NODECOUNT_CHECKPOINT = Number(process.env.PERF_NODECOUNT_STEP ?? 200); +export const TIMEOUT = Number(process.env.PERF_TIMEOUT_MS ?? (FULL ? 60 * 60_000 : 10 * 60_000)); +export const GLOBALTIMEOUT = Number( + process.env.PERF_GLOBAL_TIMEOUT_MS ?? (FULL ? 130 * 60_000 : 25 * 60_000), +); + +export const ACTIVE_METRICS = [ + "LayoutCount", + "ScriptDuration", + "JSHeapUsedSize", + "RecalcStyleCount", + "TaskDuration", + "Nodes", +] as const; + +export type MetricName = (typeof ACTIVE_METRICS)[number]; + +interface ImplConfig { + key: EditorImpl; + label: string; + selector: string; + color: string; + perfMetricsFile: string; + nodeCountFile: string; +} + +export const IMPL_CONFIG: Record = { + v1: { + key: "v1", + label: "editor-v1 (Tiptap 3)", + selector: ".ProseMirror", + color: "rgb(54, 162, 235)", + perfMetricsFile: "v1-perfMetrics.json", + nodeCountFile: "v1-nodecount.json", + }, + v2: { + key: "v2", + label: "editor-v2 (react-prosemirror, full proof stack)", + selector: ".ProseMirror", + color: "rgb(255, 99, 132)", + perfMetricsFile: "v2-perfMetrics.json", + nodeCountFile: "v2-nodecount.json", + }, + v3: { + key: "v3", + label: "editor-v3 (react-prosemirror, barebone)", + selector: ".ProseMirror", + color: "rgb(255, 159, 64)", + perfMetricsFile: "v3-perfMetrics.json", + nodeCountFile: "v3-nodecount.json", + }, + v4: { + key: "v4", + label: "editor-v4 (vanilla prosemirror, no React reconciler)", + selector: ".ProseMirror", + color: "rgb(75, 192, 192)", + perfMetricsFile: "v4-perfMetrics.json", + nodeCountFile: "v4-nodecount.json", + }, + v5: { + key: "v5", + label: "editor-v5 (bare Tiptap 3)", + selector: ".ProseMirror", + color: "rgb(153, 102, 255)", + perfMetricsFile: "v5-perfMetrics.json", + nodeCountFile: "v5-nodecount.json", + }, + "v3-nv": { + key: "v3-nv", + label: "v3-nv (react-prosemirror + React nodeview + ctx)", + selector: ".ProseMirror", + color: "rgb(255, 99, 64)", + perfMetricsFile: "v3-nv-perfMetrics.json", + nodeCountFile: "v3-nv-nodecount.json", + }, + "v4-nv": { + key: "v4-nv", + label: "v4-nv (vanilla PM + DOM nodeview)", + selector: ".ProseMirror", + color: "rgb(20, 130, 130)", + perfMetricsFile: "v4-nv-perfMetrics.json", + nodeCountFile: "v4-nv-nodecount.json", + }, + "v5-nv": { + key: "v5-nv", + label: "v5-nv (Tiptap 3 + ReactNodeViewRenderer + ctx)", + selector: ".ProseMirror", + color: "rgb(120, 60, 200)", + perfMetricsFile: "v5-nv-perfMetrics.json", + nodeCountFile: "v5-nv-nodecount.json", + }, + "v3-snv": { + key: "v3-snv", + label: "v3-snv (react-prosemirror + static React nodeview)", + selector: ".ProseMirror", + color: "rgb(255, 140, 100)", + perfMetricsFile: "v3-snv-perfMetrics.json", + nodeCountFile: "v3-snv-nodecount.json", + }, + "v4-snv": { + key: "v4-snv", + label: "v4-snv (vanilla PM + static DOM nodeview)", + selector: ".ProseMirror", + color: "rgb(60, 170, 170)", + perfMetricsFile: "v4-snv-perfMetrics.json", + nodeCountFile: "v4-snv-nodecount.json", + }, + "v5-snv": { + key: "v5-snv", + label: "v5-snv (Tiptap 3 + static React nodeview)", + selector: ".ProseMirror", + color: "rgb(170, 110, 230)", + perfMetricsFile: "v5-snv-perfMetrics.json", + nodeCountFile: "v5-snv-nodecount.json", + }, +}; + +/** + * Maps an editor impl key to its human-readable Next.js page route in the blog. + * + * v3 → /perf-react-prosemirror (react-prosemirror, barebone) + * v4 → /perf-vanilla-prosemirror (vanilla ProseMirror) + * v5 → /perf-tiptap (Tiptap 3) + * *-snv → same name + "-snv" (static nodeview variant) + * *-nv → same name + "-nv" (reactive nodeview variant; pages TBD) + */ +const IMPL_ROUTE_BASE: Record<"v3" | "v4" | "v5", string> = { + v3: "react-prosemirror", + v4: "vanilla-prosemirror", + v5: "tiptap", +}; + +export function routeFor(impl: EditorImpl): string { + const match = impl.match(/^(v[345])(-snv|-nv)?$/); + if (!match) throw new Error(`no blog route mapped for impl "${impl}"`); + const [, base, suffix] = match as unknown as [string, "v3" | "v4" | "v5", string | undefined]; + return `/perf-${IMPL_ROUTE_BASE[base]}${suffix ?? ""}`; +} + +/** + * Returns the filename for a given impl+scenario+kind. For the default scenario + * `"typing"` we keep the legacy filenames (no scenario suffix) so existing + * graphs/runs are not disturbed. + */ +export function perfFileFor( + impl: EditorImpl, + scenario: PerfScenario, + kind: "perfMetrics" | "nodecount" | "coldload" | "cursor" | "ctxflip" | "latency", +): string { + if (scenario === "typing" && kind === "perfMetrics") return IMPL_CONFIG[impl].perfMetricsFile; + if (scenario === "typing" && kind === "nodecount") return IMPL_CONFIG[impl].nodeCountFile; + return `${impl}-${scenario}-${kind}.json`; +} + +export const PERF_WEB_PORT = Number(process.env.PERF_WEB_PORT ?? 3100); +export const PERF_SERVER_PORT = Number(process.env.PERF_SERVER_PORT ?? 4100); + +// Keystroke-ramp scenario: grow the doc by KEYSTROKE_RAMP_STEP nodes between +// measurements, fire KEYSTROKE_RAMP_PRESSES paced keystrokes per iteration, +// bail once p95 INP > KEYSTROKE_LAG_P95_MS for two consecutive iterations or +// after KEYSTROKE_RAMP_MAX_ITER iterations. +export const KEYSTROKE_RAMP_STEP = Number(process.env.PERF_RAMP_STEP ?? 500); +export const KEYSTROKE_RAMP_MAX_ITER = Number(process.env.PERF_RAMP_MAX_ITER ?? 30); +export const KEYSTROKE_RAMP_PRESSES = Number(process.env.PERF_RAMP_PRESSES ?? 150); +// 50ms pacing keeps each press as its own interactionId (measures isolated +// keystroke latency). Lower values let Chromium coalesce consecutive presses +// into one interaction whose duration spans first-key → paint after last-key +// — closer to real fast typing. +export const KEYSTROKE_RAMP_PACING_MS = Number(process.env.PERF_RAMP_PACING_MS ?? 50); +export const KEYSTROKE_LAG_P95_MS = Number(process.env.PERF_LAG_P95_MS ?? 100); + +export const KEYSTROKE_RAMP_IMPLS: readonly EditorImpl[] = [ + "v3", + "v4", + "v5", + "v3-snv", + "v4-snv", + "v5-snv", +]; + +// Playwright + the orchestrator are always invoked from the repo root, +// so resolving relative to process.cwd() keeps this CJS/ESM-agnostic and +// avoids a `import.meta` TS module-target dance. +export const RESULTS_DIR = join(process.cwd(), "e2e", "perf", "results"); diff --git a/articles/react-prosemirror/e2e/perf/create-graphs.ts b/articles/react-prosemirror/e2e/perf/create-graphs.ts new file mode 100644 index 0000000..bf28edf --- /dev/null +++ b/articles/react-prosemirror/e2e/perf/create-graphs.ts @@ -0,0 +1,561 @@ +/// +import { existsSync, readFileSync, writeFileSync } from "node:fs"; +import { join } from "node:path"; +import { ChartJSNodeCanvas } from "chartjs-node-canvas"; +import { + ACTIVE_METRICS, + type EditorImpl, + IMPL_CONFIG, + type MetricName, + type PerfScenario, + perfFileFor, + RESULTS_DIR, +} from "./constants"; + +interface PerfSample { + t: number; + nodes: number; + metrics: Record; +} +interface NodeSample { + nodes: number; + elapsedMs: number; +} +interface ColdLoadSample { + n: number; + timeToVisibleMs: number; + metrics: Record; +} + +const WIDTH = 1200; +const HEIGHT = 600; + +const canvas = new ChartJSNodeCanvas({ + width: WIDTH, + height: HEIGHT, + backgroundColour: "white", +}); + +const VALID_IMPLS: EditorImpl[] = [ + "v1", + "v2", + "v3", + "v4", + "v5", + "v3-nv", + "v4-nv", + "v5-nv", + "v3-snv", + "v4-snv", + "v5-snv", +]; +const DEFAULT_IMPLS: EditorImpl[] = ["v1", "v2"]; +type GraphScenario = PerfScenario | "keystroke-ramp"; +const VALID_SCENARIOS: GraphScenario[] = [ + "typing", + "cold-load", + "cursor", + "ctx-flip", + "keystroke-ramp", +]; +const GRAPH_SCENARIO: GraphScenario = (() => { + const raw = process.env.GRAPH_SCENARIO as GraphScenario | undefined; + return raw && VALID_SCENARIOS.includes(raw) ? raw : "typing"; +})(); + +const impls: EditorImpl[] = (() => { + const raw = process.env.GRAPH_IMPLS; + if (!raw) return DEFAULT_IMPLS; + const parsed = raw + .split(",") + .map((s) => s.trim()) + .filter((s): s is EditorImpl => (VALID_IMPLS as string[]).includes(s)); + return parsed.length > 0 ? parsed : DEFAULT_IMPLS; +})(); + +// Use legacy filenames (no suffix) only when graphing the default v1+v2 pair +// for the default "typing" scenario; any other selection gets a suffix. +const isDefaultSet = + impls.length === DEFAULT_IMPLS.length && impls.every((i, idx) => i === DEFAULT_IMPLS[idx]); +const filenameSuffix = + isDefaultSet && GRAPH_SCENARIO === "typing" + ? "" + : `-${impls.join("-")}${GRAPH_SCENARIO === "typing" ? "" : `-${GRAPH_SCENARIO}`}`; + +interface LatencySample { + idx: number; + latencyMs: number; +} + +interface ImplData { + impl: EditorImpl; + perf: PerfSample[]; + nodes: NodeSample[]; + coldload: ColdLoadSample[]; + latency: LatencySample[]; +} + +function perfKindFor(scenario: PerfScenario): "perfMetrics" | "cursor" | "ctxflip" { + if (scenario === "cursor") return "cursor"; + if (scenario === "ctx-flip") return "ctxflip"; + return "perfMetrics"; +} + +const datasets: ImplData[] = + GRAPH_SCENARIO === "keystroke-ramp" + ? [] + : impls.map((impl) => ({ + impl, + perf: + GRAPH_SCENARIO === "cold-load" || GRAPH_SCENARIO === "keystroke-latency" + ? [] + : loadJson( + perfFileFor(impl, GRAPH_SCENARIO, perfKindFor(GRAPH_SCENARIO)), + ), + nodes: + GRAPH_SCENARIO === "typing" + ? loadJson(IMPL_CONFIG[impl].nodeCountFile) + : [], + coldload: + GRAPH_SCENARIO === "cold-load" + ? loadJson(perfFileFor(impl, "cold-load", "coldload")) + : [], + latency: + GRAPH_SCENARIO === "keystroke-latency" + ? loadJson(perfFileFor(impl, "keystroke-latency", "latency")) + : [], + })); + +main().catch((err) => { + console.error(err); + process.exit(1); +}); + +async function main(): Promise { + if (GRAPH_SCENARIO === "keystroke-ramp") { + await renderKeystrokeRamp(); + } else if (GRAPH_SCENARIO === "cold-load") { + await renderColdLoad(); + } else if (GRAPH_SCENARIO === "keystroke-latency") { + await renderKeystrokeLatency(); + } else if (GRAPH_SCENARIO === "typing") { + await renderNodeCountVsTime(); + for (const metric of ACTIVE_METRICS) { + await renderCombined(metric); + for (const d of datasets) { + await renderSingle(d.impl, metric, d.perf); + } + } + } else { + // cursor / ctx-flip — perf samples over time + for (const metric of ACTIVE_METRICS) { + await renderCombinedOverTime(metric); + } + } + console.log( + `graphs written to ${RESULTS_DIR} (scenario=${GRAPH_SCENARIO}, impls=${impls.join(",")})`, + ); +} + +async function renderCombined(metric: MetricName): Promise { + const chartDatasets: ChartDataset[] = []; + for (const d of datasets) { + const points = perfPoints(d.perf, metric); + if (points.length === 0) continue; + const cfg = IMPL_CONFIG[d.impl]; + chartDatasets.push({ + label: cfg.label, + data: points, + borderColor: cfg.color, + backgroundColor: cfg.color, + pointRadius: 0, + tension: 0.2, + }); + } + if (chartDatasets.length === 0) return; + + await writeChart(`combined${filenameSuffix}-${metric}.png`, { + title: `${metric} vs node count`, + xLabel: "node count", + yLabel: yLabelFor(metric), + datasets: chartDatasets, + }); +} + +async function renderSingle( + impl: EditorImpl, + metric: MetricName, + samples: PerfSample[], +): Promise { + const data = perfPoints(samples, metric); + if (data.length === 0) return; + const cfg = IMPL_CONFIG[impl]; + await writeChart(`${impl}-${metric}.png`, { + title: `${cfg.label} — ${metric} vs node count`, + xLabel: "node count", + yLabel: yLabelFor(metric), + datasets: [ + { + label: cfg.label, + data, + borderColor: cfg.color, + backgroundColor: cfg.color, + pointRadius: 0, + tension: 0.2, + }, + ], + }); +} + +async function renderNodeCountVsTime(): Promise { + const chartDatasets: ChartDataset[] = []; + for (const d of datasets) { + if (d.nodes.length === 0) continue; + const cfg = IMPL_CONFIG[d.impl]; + chartDatasets.push({ + label: cfg.label, + data: d.nodes.map((s) => ({ x: s.nodes, y: s.elapsedMs / 1000 })), + borderColor: cfg.color, + backgroundColor: cfg.color, + pointRadius: 0, + tension: 0.2, + }); + } + if (chartDatasets.length === 0) return; + await writeChart(`node-count-vs-time${filenameSuffix}.png`, { + title: "Time to reach N nodes", + xLabel: "node count", + yLabel: "elapsed (s)", + datasets: chartDatasets, + }); +} + +function perfPoints(samples: PerfSample[], metric: MetricName): Array<{ x: number; y: number }> { + return samples + .filter((s) => metric in s.metrics) + .map((s) => ({ + x: s.nodes, + y: metric === "JSHeapUsedSize" ? s.metrics[metric] / 1_000_000 : s.metrics[metric], + })); +} + +function perfPointsOverTime( + samples: PerfSample[], + metric: MetricName, +): Array<{ x: number; y: number }> { + return samples + .filter((s) => metric in s.metrics) + .map((s) => ({ + x: s.t, + y: metric === "JSHeapUsedSize" ? s.metrics[metric] / 1_000_000 : s.metrics[metric], + })); +} + +async function renderCombinedOverTime(metric: MetricName): Promise { + const chartDatasets: ChartDataset[] = []; + for (const d of datasets) { + const points = perfPointsOverTime(d.perf, metric); + if (points.length === 0) continue; + const cfg = IMPL_CONFIG[d.impl]; + chartDatasets.push({ + label: cfg.label, + data: points, + borderColor: cfg.color, + backgroundColor: cfg.color, + pointRadius: 0, + tension: 0.2, + }); + } + if (chartDatasets.length === 0) return; + await writeChart(`combined${filenameSuffix}-${metric}.png`, { + title: `${metric} vs elapsed time (${GRAPH_SCENARIO})`, + xLabel: "elapsed (ms)", + yLabel: yLabelFor(metric), + datasets: chartDatasets, + }); +} + +async function renderColdLoad(): Promise { + // Time-to-visible chart + const ttv: ChartDataset[] = []; + for (const d of datasets) { + if (d.coldload.length === 0) continue; + const cfg = IMPL_CONFIG[d.impl]; + ttv.push({ + label: cfg.label, + data: d.coldload.map((s) => ({ x: s.n, y: s.timeToVisibleMs })), + borderColor: cfg.color, + backgroundColor: cfg.color, + pointRadius: 4, + tension: 0.2, + }); + } + if (ttv.length > 0) { + await writeChart(`combined${filenameSuffix}-timeToVisible.png`, { + title: "Time to visible vs seeded paragraph count", + xLabel: "seeded paragraphs (n)", + yLabel: "ms", + datasets: ttv, + }); + } + + // Per-metric snapshot vs n + for (const metric of ACTIVE_METRICS) { + const ds: ChartDataset[] = []; + for (const d of datasets) { + if (d.coldload.length === 0) continue; + const cfg = IMPL_CONFIG[d.impl]; + ds.push({ + label: cfg.label, + data: d.coldload + .filter((s) => metric in s.metrics) + .map((s) => ({ + x: s.n, + y: metric === "JSHeapUsedSize" ? s.metrics[metric] / 1_000_000 : s.metrics[metric], + })), + borderColor: cfg.color, + backgroundColor: cfg.color, + pointRadius: 4, + tension: 0.2, + }); + } + if (ds.length === 0) continue; + await writeChart(`combined${filenameSuffix}-${metric}.png`, { + title: `${metric} at cold load (vs seeded paragraph count)`, + xLabel: "seeded paragraphs (n)", + yLabel: yLabelFor(metric), + datasets: ds, + }); + } +} + +function yLabelFor(metric: MetricName): string { + if (metric === "JSHeapUsedSize") return "MB"; + if (metric === "ScriptDuration" || metric === "TaskDuration") return "seconds"; + return "count"; +} + +interface ChartDataset { + label: string; + data: Array<{ x: number; y: number }>; + borderColor: string; + backgroundColor: string; + pointRadius: number; + tension: number; + borderDash?: number[]; + showLine?: boolean; +} + +async function renderKeystrokeLatency(): Promise { + // 1. Raw latency vs keystroke index (one line per impl) + const raw: ChartDataset[] = []; + for (const d of datasets) { + if (d.latency.length === 0) continue; + const cfg = IMPL_CONFIG[d.impl]; + raw.push({ + label: cfg.label, + data: d.latency.map((s) => ({ x: s.idx, y: s.latencyMs })), + borderColor: cfg.color, + backgroundColor: cfg.color, + pointRadius: 0, + tension: 0, + }); + } + if (raw.length > 0) { + await writeChart(`combined${filenameSuffix}-latency-over-time.png`, { + title: "Keystroke latency (raw, per keystroke)", + xLabel: "keystroke index", + yLabel: "ms (dispatch → next rAF)", + datasets: raw, + }); + } + + // 2. Sorted percentile chart — y = latency, x = percentile (0..100) + const sortedDs: ChartDataset[] = []; + for (const d of datasets) { + if (d.latency.length === 0) continue; + const cfg = IMPL_CONFIG[d.impl]; + const sorted = d.latency.map((s) => s.latencyMs).sort((a, b) => a - b); + sortedDs.push({ + label: cfg.label, + data: sorted.map((y, i) => ({ x: (i / (sorted.length - 1)) * 100, y })), + borderColor: cfg.color, + backgroundColor: cfg.color, + pointRadius: 0, + tension: 0, + }); + } + if (sortedDs.length > 0) { + await writeChart(`combined${filenameSuffix}-latency-percentile.png`, { + title: "Keystroke latency distribution (sorted)", + xLabel: "percentile", + yLabel: "ms (dispatch → next rAF)", + datasets: sortedDs, + }); + } + + // 3. Print a summary table + for (const d of datasets) { + if (d.latency.length === 0) continue; + const sorted = d.latency.map((s) => s.latencyMs).sort((a, b) => a - b); + const q = (p: number) => sorted[Math.min(sorted.length - 1, Math.floor(sorted.length * p))]; + const mean = sorted.reduce((s, v) => s + v, 0) / sorted.length; + console.log( + `[${d.impl}] n=${sorted.length} mean=${mean.toFixed(2)}ms p50=${q(0.5).toFixed(2)}ms p95=${q(0.95).toFixed(2)}ms p99=${q(0.99).toFixed(2)}ms max=${sorted[sorted.length - 1].toFixed(2)}ms`, + ); + } +} + +interface RampIteration { + iter: number; + n: number; + stats: { + count: number; + p50: number; + p95: number; + p99: number; + max: number; + mean: number; + meanInputDelay: number; + meanProcessing: number; + meanPresentation: number; + } | null; +} +interface RampSummaryFile { + impl: string; + stepSize: number; + pressesPerIter: number; + thresholdMs: number; + maxIter: number; + iterations: RampIteration[]; + breakpointN: number | null; + bailReason: string; +} + +async function renderKeystrokeRamp(): Promise { + const loaded = impls + .map((impl) => ({ impl, data: loadRampSummary(`${impl}-keystroke-ramp-summary.json`) })) + .filter((x): x is { impl: EditorImpl; data: RampSummaryFile } => x.data !== null); + + if (loaded.length === 0) { + console.log("no keystroke-ramp summaries found; nothing to graph"); + return; + } + + const chartDatasets: ChartDataset[] = []; + let maxN = 0; + let maxY = 0; + + for (const { impl, data } of loaded) { + const cfg = IMPL_CONFIG[impl]; + const points = data.iterations.flatMap((it) => + it.stats == null ? [] : [{ x: it.n, y: it.stats.p95 }], + ); + if (points.length === 0) continue; + chartDatasets.push({ + label: `${cfg.label} (p95 INP)`, + data: points, + borderColor: cfg.color, + backgroundColor: cfg.color, + pointRadius: 3, + tension: 0.2, + }); + for (const p of points) { + if (p.x > maxN) maxN = p.x; + if (p.y > maxY) maxY = p.y; + } + } + + const threshold = loaded[0].data.thresholdMs; + if (threshold > maxY) maxY = threshold; + + // Horizontal dashed threshold line. + chartDatasets.push({ + label: `lag threshold (p95 = ${threshold} ms)`, + data: [ + { x: 0, y: threshold }, + { x: maxN, y: threshold }, + ], + borderColor: "rgba(220, 38, 38, 0.9)", + backgroundColor: "rgba(220, 38, 38, 0.9)", + pointRadius: 0, + tension: 0, + borderDash: [6, 6], + }); + + // Vertical breakpoint markers per impl. + for (const { impl, data } of loaded) { + if (data.breakpointN == null) continue; + const cfg = IMPL_CONFIG[impl]; + chartDatasets.push({ + label: `${cfg.label} breakpoint (n=${data.breakpointN})`, + data: [ + { x: data.breakpointN, y: 0 }, + { x: data.breakpointN, y: maxY * 1.05 }, + ], + borderColor: cfg.color, + backgroundColor: cfg.color, + pointRadius: 0, + tension: 0, + borderDash: [3, 3], + }); + } + + await writeChart("keystroke-ramp.png", { + title: "Typing INP p95 vs document size — lag breakpoint per impl", + xLabel: "node count", + yLabel: "INP p95 (ms)", + datasets: chartDatasets, + }); + + for (const { impl, data } of loaded) { + const bp = data.breakpointN ?? `none (${data.bailReason})`; + console.log( + `[${impl}] breakpoint=${bp} | iterations=${data.iterations.length} | step=${data.stepSize} | threshold=${data.thresholdMs}ms`, + ); + } +} + +function loadRampSummary(name: string): RampSummaryFile | null { + const path = join(RESULTS_DIR, name); + if (!existsSync(path)) return null; + return JSON.parse(readFileSync(path, "utf8")) as RampSummaryFile; +} + +async function writeChart( + filename: string, + spec: { + title: string; + xLabel: string; + yLabel: string; + datasets: ChartDataset[]; + }, +): Promise { + const png = await canvas.renderToBuffer({ + type: "line", + data: { datasets: spec.datasets }, + options: { + responsive: false, + plugins: { + title: { display: true, text: spec.title, font: { size: 18 } }, + legend: { display: true, position: "top" }, + }, + scales: { + x: { + type: "linear", + title: { display: true, text: spec.xLabel }, + }, + y: { + title: { display: true, text: spec.yLabel }, + }, + }, + }, + }); + writeFileSync(join(RESULTS_DIR, filename), png); +} + +function loadJson(name: string): T extends unknown[] ? T : never { + const path = join(RESULTS_DIR, name); + if (!existsSync(path)) return [] as unknown as T extends unknown[] ? T : never; + return JSON.parse(readFileSync(path, "utf8")) as T extends unknown[] ? T : never; +} diff --git a/articles/react-prosemirror/e2e/perf/inp-stats.ts b/articles/react-prosemirror/e2e/perf/inp-stats.ts new file mode 100644 index 0000000..db429f2 --- /dev/null +++ b/articles/react-prosemirror/e2e/perf/inp-stats.ts @@ -0,0 +1,94 @@ +/** + * Per-interaction INP grouping + percentile math for keystroke perf scenarios. + * + * The PerformanceObserver collects every PerformanceEventTiming entry (one + * keystroke fires multiple: keydown, beforeinput, input, keyup — all sharing + * one interactionId). Web Vitals INP takes the MAX duration across each + * interaction's events, so we group by interactionId then reduce. + * + * Extracted from nodeview.spec.ts (runKeystrokeInp) so the keystroke-ramp + * scenario can reuse the same math. + */ + +export interface InpEntry { + name: string; + interactionId: number; + startTime: number; + processingStart: number; + processingEnd: number; + duration: number; +} + +export interface InpInteraction { + interactionId: number; + events: string[]; + inputDelay: number; + processingDuration: number; + presentationDelay: number; + duration: number; +} + +export interface InpStats { + count: number; + p50: number; + p95: number; + p99: number; + max: number; + mean: number; + meanInputDelay: number; + meanProcessing: number; + meanPresentation: number; +} + +export function groupInteractions(entries: readonly InpEntry[]): InpInteraction[] { + const byInteraction = new Map(); + for (const e of entries) { + const arr = byInteraction.get(e.interactionId); + if (arr) arr.push(e); + else byInteraction.set(e.interactionId, [e]); + } + return Array.from(byInteraction.entries()) + .map(([id, group]) => { + const dominant = group.reduce((a, b) => (a.duration >= b.duration ? a : b)); + return { + interactionId: id, + events: group.map((e) => e.name), + inputDelay: dominant.processingStart - dominant.startTime, + processingDuration: dominant.processingEnd - dominant.processingStart, + presentationDelay: dominant.startTime + dominant.duration - dominant.processingEnd, + duration: dominant.duration, + }; + }) + .sort((a, b) => a.interactionId - b.interactionId); +} + +export function computeStats(interactions: readonly InpInteraction[]): InpStats | null { + if (interactions.length === 0) return null; + const sorted = interactions.map((i) => i.duration).sort((a, b) => a - b); + const pick = (q: number): number => + sorted[Math.min(sorted.length - 1, Math.floor(sorted.length * q))]; + const mean = (xs: readonly number[]): number => xs.reduce((s, v) => s + v, 0) / xs.length; + return { + count: interactions.length, + p50: pick(0.5), + p95: pick(0.95), + p99: pick(0.99), + max: sorted[sorted.length - 1], + mean: mean(sorted), + meanInputDelay: mean(interactions.map((i) => i.inputDelay)), + meanProcessing: mean(interactions.map((i) => i.processingDuration)), + meanPresentation: mean(interactions.map((i) => i.presentationDelay)), + }; +} + +export function formatStatsLine(stats: InpStats): string { + return ( + `INP p50=${stats.p50.toFixed(2)}ms ` + + `p95=${stats.p95.toFixed(2)}ms ` + + `p99=${stats.p99.toFixed(2)}ms ` + + `max=${stats.max.toFixed(2)}ms ` + + `| mean inputDelay=${stats.meanInputDelay.toFixed(2)}ms ` + + `processing=${stats.meanProcessing.toFixed(2)}ms ` + + `presentation=${stats.meanPresentation.toFixed(2)}ms` + ); +} diff --git a/articles/react-prosemirror/e2e/perf/keystroke-ramp.spec.ts b/articles/react-prosemirror/e2e/perf/keystroke-ramp.spec.ts new file mode 100644 index 0000000..02e8959 --- /dev/null +++ b/articles/react-prosemirror/e2e/perf/keystroke-ramp.spec.ts @@ -0,0 +1,280 @@ +/** + * Keystroke latency ramp — find the node count at which typing starts to + * feel laggy in each `-snv` editor implementation. + * + * One persistent editor session per impl. Start empty, then loop: + * 1. Grow doc by KEYSTROKE_RAMP_STEP nodes (skipped on iter 0). + * 2. Reset INP collector. Start CDP Tracing. + * 3. Fire KEYSTROKE_RAMP_PRESSES paced keystrokes via real Chromium input. + * 4. Stop trace, compute per-iteration INP stats (p50/p95/p99 + breakdown). + * 5. Bail when p95 > KEYSTROKE_LAG_P95_MS for two CONSECUTIVE iterations. + * The FIRST crossing is recorded as the lag breakpoint. + * + * Trace files persisted: baseline (iter 0) and the confirming bail iteration. + * The trace JSON format matches Chrome DevTools' "Load profile…" — drag the + * file into the Performance tab to inspect the Interactions lane and flame + * chart that produced the recorded latencies. + * + * Each invocation runs ONE impl (set via EDITOR_IMPL). The sweep across + * impls lives in `scripts/run-keystroke-ramp.ts`. + */ +import { mkdirSync, writeFileSync } from "node:fs"; +import { join } from "node:path"; +import { expect, test } from "@playwright/test"; +import { + EDITOR_IMPL, + IMPL_CONFIG, + KEYSTROKE_LAG_P95_MS, + KEYSTROKE_RAMP_IMPLS, + KEYSTROKE_RAMP_MAX_ITER, + KEYSTROKE_RAMP_PACING_MS, + KEYSTROKE_RAMP_PRESSES, + KEYSTROKE_RAMP_STEP, + RESULTS_DIR, + routeFor, + TIMEOUT, +} from "./constants"; +import { + computeStats, + formatStatsLine, + groupInteractions, + type InpEntry, + type InpStats, +} from "./inp-stats"; + +const IS_RAMP_IMPL = (KEYSTROKE_RAMP_IMPLS as readonly string[]).includes(EDITOR_IMPL); +const cfg = IMPL_CONFIG[EDITOR_IMPL]; + +interface IterationResult { + iter: number; + n: number; + stats: InpStats | null; +} + +interface RampSummary { + impl: string; + stepSize: number; + pressesPerIter: number; + thresholdMs: number; + maxIter: number; + iterations: IterationResult[]; + breakpointN: number | null; + bailReason: "p95-threshold" | "safety-cap" | "early-error"; +} + +test(`[PERF] keystroke-ramp ${cfg?.label ?? EDITOR_IMPL}`, async ({ browser }) => { + test.skip( + !IS_RAMP_IMPL, + `keystroke-ramp only runs for ${KEYSTROKE_RAMP_IMPLS.join(", ")}; got ${EDITOR_IMPL}`, + ); + test.setTimeout(TIMEOUT); + mkdirSync(RESULTS_DIR, { recursive: true }); + + const ctx = await browser.newContext(); + const page = await ctx.newPage(); + const session = await page.context().newCDPSession(page); + + // Single page load. Grow doc programmatically between iterations via the + // window.__perfGrow(k) hook exposed by each -snv page. + await page.goto(routeFor(EDITOR_IMPL)); + await expect(page.locator(cfg.selector)).toBeVisible({ timeout: 30_000 }); + await page.waitForFunction( + () => typeof (window as unknown as { __perfGrow?: unknown }).__perfGrow === "function", + undefined, + { timeout: 15_000 }, + ); + + const iterations: IterationResult[] = []; + let breakpointN: number | null = null; + let bailReason: RampSummary["bailReason"] = "safety-cap"; + + for (let iter = 0; iter < KEYSTROKE_RAMP_MAX_ITER; iter++) { + if (iter > 0) { + await page.evaluate((k) => { + const fn = (window as unknown as { __perfGrow?: (k: number) => void }).__perfGrow; + if (!fn) throw new Error("__perfGrow missing"); + fn(k); + }, KEYSTROKE_RAMP_STEP); + } + + const n = await page.evaluate( + () => (window as unknown as { __perfNodes?: number }).__perfNodes ?? 0, + ); + + // Fresh Event Timing observer per iteration so we don't leak entries + // across the n-boundary. `durationThreshold: 16` is the spec minimum. + await page.evaluate(() => { + interface InpEntry { + name: string; + interactionId: number; + startTime: number; + processingStart: number; + processingEnd: number; + duration: number; + } + const w = window as unknown as { + __inpEntries: InpEntry[]; + __inpObserver?: PerformanceObserver; + }; + if (w.__inpObserver) w.__inpObserver.disconnect(); + w.__inpEntries = []; + type EventTimingEntry = PerformanceEventTiming & { interactionId: number }; + const observer = new PerformanceObserver((list) => { + for (const entry of list.getEntries()) { + const e = entry as EventTimingEntry; + if (!e.interactionId) continue; + w.__inpEntries.push({ + name: e.name, + interactionId: e.interactionId, + startTime: e.startTime, + processingStart: e.processingStart, + processingEnd: e.processingEnd, + duration: e.duration, + }); + } + }); + observer.observe({ + type: "event", + buffered: false, + durationThreshold: 16, + } as PerformanceObserverInit); + w.__inpObserver = observer; + }); + + // Position caret at end of last child so each keypress appends in place. + await page.locator(cfg.selector).click(); + await page.evaluate((selector) => { + const el = document.querySelector(selector) as HTMLElement | null; + if (!el) return; + el.focus(); + const sel = window.getSelection(); + const last = el.lastElementChild ?? el; + if (sel) { + sel.removeAllRanges(); + const range = document.createRange(); + range.selectNodeContents(last); + range.collapse(false); + sel.addRange(range); + } + }, cfg.selector); + + // Start CDP trace covering this iteration's keystroke loop. + // Categories mirror what DevTools' Performance panel records by default + // (Interactions lane + main-thread flame chart). + const traceEvents: unknown[] = []; + const onDataCollected = (params: { value: unknown[] }): void => { + traceEvents.push(...params.value); + }; + session.on("Tracing.dataCollected", onDataCollected); + await session.send("Tracing.start", { + transferMode: "ReportEvents", + traceConfig: { + recordMode: "recordContinuously", + includedCategories: [ + "devtools.timeline", + "v8.execute", + "latencyInfo", + "blink.user_timing", + "disabled-by-default-devtools.timeline", + "disabled-by-default-devtools.timeline.frame", + ], + }, + } as Record); + + // Warmup keystroke (first-frame plumbing). Not counted toward stats — + // the observer resets just above, so this entry simply records and is + // included; for clarity we accept that the warmup is in-band. + await page.keyboard.press("a"); + await page.evaluate( + () => new Promise((r) => requestAnimationFrame(() => requestAnimationFrame(() => r()))), + ); + + // Measured loop. Pacing controls whether presses are isolated (50 ms each + // is its own interactionId) or coalesced (e.g. 10 ms — Chromium merges + // consecutive presses into one interaction whose duration spans first + // key → paint after last key, matching real fast typing). + for (let i = 0; i < KEYSTROKE_RAMP_PRESSES; i++) { + await page.keyboard.press("a"); + if (KEYSTROKE_RAMP_PACING_MS > 0) await page.waitForTimeout(KEYSTROKE_RAMP_PACING_MS); + } + + // Flush: one more paint cycle so the observer captures tail entries. + await page.waitForTimeout(250); + + // Stop trace and wait for the tail of dataCollected events. + const tracingComplete = new Promise((resolve) => { + session.once("Tracing.tracingComplete", () => resolve()); + }); + await session.send("Tracing.end"); + await tracingComplete; + session.off("Tracing.dataCollected", onDataCollected); + + const entries = await page.evaluate((): InpEntry[] => { + const w = window as unknown as { __inpEntries: InpEntry[] }; + return w.__inpEntries; + }); + + const interactions = groupInteractions(entries); + const stats = computeStats(interactions); + iterations.push({ iter, n, stats }); + + const tag = `[perf-ramp:${EDITOR_IMPL}] iter=${iter} n=${n}`; + if (!stats) { + // eslint-disable-next-line no-console + console.log(`${tag} WARNING: 0 interactions recorded (sub-16ms keystrokes?)`); + } else { + // eslint-disable-next-line no-console + console.log(`${tag} ${formatStatsLine(stats)}`); + } + + // Persist baseline trace. + if (iter === 0) { + writeFileSync( + join(RESULTS_DIR, `${EDITOR_IMPL}-keystroke-ramp-baseline.trace.json`), + JSON.stringify({ traceEvents }), + ); + } + + // Bail check: two consecutive iterations with p95 > threshold. + const prev = iterations[iterations.length - 2]; + const overNow = stats !== null && stats.p95 > KEYSTROKE_LAG_P95_MS; + const overPrev = prev?.stats != null && prev.stats.p95 > KEYSTROKE_LAG_P95_MS; + if (overNow && overPrev) { + breakpointN = prev.n; // First crossing. + bailReason = "p95-threshold"; + writeFileSync( + join(RESULTS_DIR, `${EDITOR_IMPL}-keystroke-ramp-bail-n${n}.trace.json`), + JSON.stringify({ traceEvents }), + ); + // eslint-disable-next-line no-console + console.log( + `${tag} BAIL: two consecutive iterations over ${KEYSTROKE_LAG_P95_MS}ms p95; lag breakpoint at n=${breakpointN}`, + ); + break; + } + } + + const summary: RampSummary = { + impl: EDITOR_IMPL, + stepSize: KEYSTROKE_RAMP_STEP, + pressesPerIter: KEYSTROKE_RAMP_PRESSES, + thresholdMs: KEYSTROKE_LAG_P95_MS, + maxIter: KEYSTROKE_RAMP_MAX_ITER, + iterations, + breakpointN, + bailReason, + }; + writeFileSync( + join(RESULTS_DIR, `${EDITOR_IMPL}-keystroke-ramp-summary.json`), + JSON.stringify(summary, null, 2), + ); + + if (breakpointN == null) { + // eslint-disable-next-line no-console + console.log( + `[perf-ramp:${EDITOR_IMPL}] safety cap reached at ${KEYSTROKE_RAMP_MAX_ITER} iterations; no lag breakpoint found within ${KEYSTROKE_RAMP_MAX_ITER * KEYSTROKE_RAMP_STEP} nodes`, + ); + } + + await ctx.close(); +}); diff --git a/articles/react-prosemirror/e2e/perf/nodeview.spec.ts b/articles/react-prosemirror/e2e/perf/nodeview.spec.ts new file mode 100644 index 0000000..0406721 --- /dev/null +++ b/articles/react-prosemirror/e2e/perf/nodeview.spec.ts @@ -0,0 +1,565 @@ +/** + * Nodeview perf benchmark — runs one scenario, for one impl, per Playwright + * invocation. The orchestrator (`scripts/run-nodeview-perf.ts`) iterates + * `EDITOR_IMPL` × `PERF_SCENARIO` across runs. + * + * EDITOR_IMPL ∈ { v3-nv, v4-nv, v5-nv } + * PERF_SCENARIO ∈ { typing, cold-load, cursor, ctx-flip } + * + * Reuses the same CDP `Performance.getMetrics` polling pattern as + * `stress.spec.ts`, so output JSON drops straight into `create-graphs.ts`. + */ +import { mkdirSync, writeFileSync } from "node:fs"; +import { join } from "node:path"; +import { expect, test } from "@playwright/test"; +import { + ACTIVE_METRICS, + EDITOR_IMPL, + IMPL_CONFIG, + MAX_NODES, + MEASUREMENT_INTERVAL, + NODECOUNT_CHECKPOINT, + PERF_COMPLEXITY, + PERF_SCENARIO, + perfFileFor, + RESULTS_DIR, + routeFor, + TIMEOUT, +} from "./constants"; +import { computeStats, formatStatsLine, groupInteractions } from "./inp-stats"; + +const COMPLEXITY_PARAM = `complexity=${PERF_COMPLEXITY}`; + +const SPEC_IMPLS = new Set([ + "v3-nv", + "v4-nv", + "v5-nv", + "v3", + "v4", + "v5", + "v3-snv", + "v4-snv", + "v5-snv", +]); +const isNvImpl = SPEC_IMPLS.has(EDITOR_IMPL); +const cfg = IMPL_CONFIG[EDITOR_IMPL]; + +function parseSizes(raw: string | undefined): number[] | null { + if (!raw) return null; + const parsed = raw + .split(",") + .map((s) => Number(s.trim())) + .filter((n) => Number.isFinite(n) && n > 0); + return parsed.length > 0 ? parsed : null; +} + +interface PerfSample { + t: number; + nodes: number; + metrics: Record; +} + +interface ColdLoadSample { + n: number; + timeToVisibleMs: number; + metrics: Record; +} + +test(`[PERF] nodeview ${PERF_SCENARIO} ${cfg?.label ?? EDITOR_IMPL}`, async ({ browser }) => { + test.skip(!isNvImpl, `nodeview.spec.ts only runs for v{3,4,5}(-nv)?; got ${EDITOR_IMPL}`); + test.setTimeout(TIMEOUT); + mkdirSync(RESULTS_DIR, { recursive: true }); + + const ctx = await browser.newContext(); + const page = await ctx.newPage(); + const session = await page.context().newCDPSession(page); + await session.send("Performance.enable"); + + if (PERF_SCENARIO === "typing") { + await runTyping(page, session); + } else if (PERF_SCENARIO === "cold-load") { + await runColdLoad(page, session); + } else if (PERF_SCENARIO === "cursor") { + await runCursor(page, session); + } else if (PERF_SCENARIO === "ctx-flip") { + await runCtxFlip(page, session); + } else if (PERF_SCENARIO === "keystroke-latency") { + await runKeystrokeLatency(page); + } else if (PERF_SCENARIO === "keystroke-inp") { + await runKeystrokeInp(page); + } + + await ctx.close(); +}); + +interface CdpSession { + send(method: "Performance.getMetrics"): Promise<{ metrics: { name: string; value: number }[] }>; +} + +async function pollMetrics( + session: CdpSession, + page: import("@playwright/test").Page, + startedAt: number, + fallbackNodes: () => number, +): Promise<{ stop: () => void; samples: PerfSample[] }> { + const samples: PerfSample[] = []; + const interval = setInterval(async () => { + const { metrics } = await session.send("Performance.getMetrics"); + const picked: Record = {}; + for (const m of metrics) { + if ((ACTIVE_METRICS as readonly string[]).includes(m.name)) picked[m.name] = m.value; + } + const liveN = await page + .evaluate(() => (window as unknown as { __perfNodes?: number }).__perfNodes ?? 0) + .catch(() => fallbackNodes()); + samples.push({ t: Date.now() - startedAt, nodes: liveN || fallbackNodes(), metrics: picked }); + }, MEASUREMENT_INTERVAL); + return { stop: () => clearInterval(interval), samples }; +} + +async function runTyping( + page: import("@playwright/test").Page, + session: CdpSession, +): Promise { + await page.goto(`${routeFor(EDITOR_IMPL)}?${COMPLEXITY_PARAM}`); + await expect(page.locator(cfg.selector)).toBeVisible({ timeout: 15_000 }); + await page.locator(cfg.selector).click(); + + const start = Date.now(); + let n = await page.locator(`${cfg.selector} > *`).count(); + const poll = await pollMetrics(session, page, start, () => n); + + const nodeSamples: { nodes: number; elapsedMs: number }[] = []; + try { + const browserSamples = await page.evaluate( + async ({ maxNodes, checkpoint, selector, initialN }) => { + const el = document.querySelector(selector) as HTMLElement | null; + if (!el) throw new Error(`selector ${selector} not found`); + el.focus(); + const samples: { nodes: number; elapsedMs: number }[] = []; + const tStart = Date.now(); + let count = initialN; + (window as unknown as { __perfNodes?: number }).__perfNodes = count; + const enterInit: KeyboardEventInit & { keyCode?: number; which?: number } = { + key: "Enter", + code: "Enter", + keyCode: 13, + which: 13, + bubbles: true, + cancelable: true, + }; + while (count < maxNodes) { + for (const ch of "typing ") { + el.dispatchEvent( + new InputEvent("beforeinput", { + inputType: "insertText", + data: ch, + bubbles: true, + cancelable: true, + }), + ); + } + el.dispatchEvent(new KeyboardEvent("keydown", enterInit)); + count++; + await new Promise((r) => requestAnimationFrame(r)); + (window as unknown as { __perfNodes?: number }).__perfNodes = count; + if (count % checkpoint === 0) { + samples.push({ nodes: count, elapsedMs: Date.now() - tStart }); + await new Promise((r) => setTimeout(r, 0)); + } + } + return samples; + }, + { + maxNodes: MAX_NODES, + checkpoint: NODECOUNT_CHECKPOINT, + selector: cfg.selector, + initialN: n, + }, + ); + nodeSamples.push(...browserSamples); + if (browserSamples.length) n = browserSamples[browserSamples.length - 1].nodes; + } finally { + poll.stop(); + writeFileSync( + join(RESULTS_DIR, perfFileFor(EDITOR_IMPL, "typing", "perfMetrics")), + JSON.stringify(poll.samples, null, 2), + ); + writeFileSync( + join(RESULTS_DIR, perfFileFor(EDITOR_IMPL, "typing", "nodecount")), + JSON.stringify(nodeSamples, null, 2), + ); + // eslint-disable-next-line no-console + console.log( + `[perf-nv:${EDITOR_IMPL}:typing] ${nodeSamples.length} node samples, ${poll.samples.length} perf samples, final nodes=${n}`, + ); + } +} + +async function runColdLoad( + page: import("@playwright/test").Page, + session: CdpSession, +): Promise { + const sizes = parseSizes(process.env.PERF_COLD_LOAD_SIZES) ?? [100, 500, 1000, 2500, 5000]; + // Per-size cap. At 50k paragraphs the slower React-rendered engines can + // take a minute or more to mount; 5 min covers that comfortably. + const perSizeTimeoutMs = Number(process.env.PERF_COLD_LOAD_TIMEOUT_MS ?? 300_000); + const out: ColdLoadSample[] = []; + + for (const n of sizes) { + const t0 = Date.now(); + await page.goto(`${routeFor(EDITOR_IMPL)}?n=${n}&${COMPLEXITY_PARAM}`); + await expect(page.locator(cfg.selector)).toBeVisible({ timeout: perSizeTimeoutMs }); + // Wait until the page has settled enough to expose all paragraphs. + await page.waitForFunction( + ([sel, target]) => { + const root = document.querySelector(sel as string); + return root ? root.childElementCount >= (target as number) : false; + }, + [cfg.selector, n] as const, + { timeout: perSizeTimeoutMs }, + ); + const timeToVisibleMs = Date.now() - t0; + const { metrics } = await session.send("Performance.getMetrics"); + const picked: Record = {}; + for (const m of metrics) { + if ((ACTIVE_METRICS as readonly string[]).includes(m.name)) picked[m.name] = m.value; + } + out.push({ n, timeToVisibleMs, metrics: picked }); + // eslint-disable-next-line no-console + console.log(`[perf-nv:${EDITOR_IMPL}:cold-load] n=${n} timeToVisible=${timeToVisibleMs}ms`); + } + + writeFileSync( + join(RESULTS_DIR, perfFileFor(EDITOR_IMPL, "cold-load", "coldload")), + JSON.stringify(out, null, 2), + ); +} + +async function runCursor( + page: import("@playwright/test").Page, + session: CdpSession, +): Promise { + const seedN = Number(process.env.PERF_CURSOR_N ?? 10_000); + await page.goto(`${routeFor(EDITOR_IMPL)}?n=${seedN}&${COMPLEXITY_PARAM}`); + await expect(page.locator(cfg.selector)).toBeVisible({ timeout: 60_000 }); + await page.waitForFunction( + ([sel, target]) => { + const root = document.querySelector(sel as string); + return root ? root.childElementCount >= (target as number) : false; + }, + [cfg.selector, seedN] as const, + { timeout: 60_000 }, + ); + await page.locator(cfg.selector).click(); + + const start = Date.now(); + const poll = await pollMetrics(session, page, start, () => seedN); + + try { + await page.evaluate( + async ({ selector, presses }) => { + const el = document.querySelector(selector) as HTMLElement | null; + if (!el) throw new Error(`selector ${selector} not found`); + el.focus(); + const init: KeyboardEventInit & { keyCode?: number; which?: number } = { + key: "ArrowDown", + code: "ArrowDown", + keyCode: 40, + which: 40, + bubbles: true, + cancelable: true, + }; + for (let i = 0; i < presses; i++) { + el.dispatchEvent(new KeyboardEvent("keydown", init)); + await new Promise((r) => requestAnimationFrame(r)); + } + }, + { selector: cfg.selector, presses: 400 }, + ); + } finally { + poll.stop(); + writeFileSync( + join(RESULTS_DIR, perfFileFor(EDITOR_IMPL, "cursor", "cursor")), + JSON.stringify(poll.samples, null, 2), + ); + // eslint-disable-next-line no-console + console.log( + `[perf-nv:${EDITOR_IMPL}:cursor] ${poll.samples.length} perf samples over ${Date.now() - start}ms`, + ); + } +} + +async function runCtxFlip( + page: import("@playwright/test").Page, + session: CdpSession, +): Promise { + const seedN = Number(process.env.PERF_CTX_FLIP_N ?? 4_000); + await page.goto(`${routeFor(EDITOR_IMPL)}?n=${seedN}&${COMPLEXITY_PARAM}`); + await expect(page.locator(cfg.selector)).toBeVisible({ timeout: 60_000 }); + await page.waitForFunction( + ([sel, target]) => { + const root = document.querySelector(sel as string); + return root ? root.childElementCount >= (target as number) : false; + }, + [cfg.selector, seedN] as const, + { timeout: 60_000 }, + ); + await page.waitForFunction( + () => Boolean((window as unknown as { __perfNodeview?: unknown }).__perfNodeview), + undefined, + { timeout: 15_000 }, + ); + + const start = Date.now(); + const poll = await pollMetrics(session, page, start, () => seedN); + + try { + await page.evaluate( + async ({ flips }) => { + const api = (window as unknown as { __perfNodeview?: { setCtx: (v: number) => void } }) + .__perfNodeview; + if (!api) throw new Error("__perfNodeview hook missing"); + for (let i = 0; i < flips; i++) { + api.setCtx(i + 1); + await new Promise((r) => requestAnimationFrame(r)); + } + }, + { flips: 500 }, + ); + } finally { + poll.stop(); + writeFileSync( + join(RESULTS_DIR, perfFileFor(EDITOR_IMPL, "ctx-flip", "ctxflip")), + JSON.stringify(poll.samples, null, 2), + ); + // eslint-disable-next-line no-console + console.log( + `[perf-nv:${EDITOR_IMPL}:ctx-flip] ${poll.samples.length} perf samples over ${Date.now() - start}ms`, + ); + } +} + +async function runKeystrokeLatency(page: import("@playwright/test").Page): Promise { + const seedN = Number(process.env.PERF_KEYSTROKE_N ?? 2_000); + const count = Number(process.env.PERF_KEYSTROKE_COUNT ?? 1_000); + + await page.goto(`${routeFor(EDITOR_IMPL)}?n=${seedN}&${COMPLEXITY_PARAM}`); + await expect(page.locator(cfg.selector)).toBeVisible({ timeout: 60_000 }); + await page.waitForFunction( + ([sel, target]) => { + const root = document.querySelector(sel as string); + return root ? root.childElementCount >= (target as number) : false; + }, + [cfg.selector, seedN] as const, + { timeout: 60_000 }, + ); + await page.locator(cfg.selector).click(); + + const samples = await page.evaluate( + async ({ selector, count }) => { + const el = document.querySelector(selector) as HTMLElement | null; + if (!el) throw new Error(`selector ${selector} not found`); + el.focus(); + + // Place caret at end of last paragraph so insertText appends in place + const sel = window.getSelection(); + const last = el.lastElementChild ?? el; + if (sel) { + sel.removeAllRanges(); + const range = document.createRange(); + range.selectNodeContents(last); + range.collapse(false); + sel.addRange(range); + } + + // Warmup — first frame's latency includes selection plumbing, drop it. + el.dispatchEvent( + new InputEvent("beforeinput", { + inputType: "insertText", + data: "a", + bubbles: true, + cancelable: true, + }), + ); + await new Promise((r) => requestAnimationFrame(() => r(undefined))); + + const out: { idx: number; latencyMs: number }[] = []; + for (let i = 0; i < count; i++) { + const t0 = performance.now(); + el.dispatchEvent( + new InputEvent("beforeinput", { + inputType: "insertText", + data: "a", + bubbles: true, + cancelable: true, + }), + ); + await new Promise((r) => requestAnimationFrame(() => r(undefined))); + out.push({ idx: i, latencyMs: performance.now() - t0 }); + } + return out; + }, + { selector: cfg.selector, count }, + ); + + writeFileSync( + join(RESULTS_DIR, perfFileFor(EDITOR_IMPL, "keystroke-latency", "latency")), + JSON.stringify(samples, null, 2), + ); + + const sorted = samples.map((s) => s.latencyMs).sort((a, b) => a - b); + const p = (q: number) => sorted[Math.min(sorted.length - 1, Math.floor(sorted.length * q))]; + // eslint-disable-next-line no-console + console.log( + `[perf-nv:${EDITOR_IMPL}:keystroke-latency] n=${samples.length} p50=${p(0.5).toFixed(2)}ms p95=${p(0.95).toFixed(2)}ms p99=${p(0.99).toFixed(2)}ms max=${sorted[sorted.length - 1].toFixed(2)}ms mean=${(sorted.reduce((s, v) => s + v, 0) / sorted.length).toFixed(2)}ms`, + ); +} + +/** + * Real-world keystroke latency via the Event Timing API. + * + * Unlike `keystroke-latency` (which dispatches synthetic `beforeinput` events + * from JS and measures one rAF tick), this scenario uses Playwright's + * `page.keyboard.press()` to drive the real Chromium input pipeline + * (hardware → renderer → JS handler → DOM mutation → style/layout → paint), + * and collects `PerformanceEventTiming` entries to recover Chrome's own INP + * breakdown: + * + * inputDelay = processingStart - startTime + * processingDuration= processingEnd - processingStart + * presentationDelay = (startTime + duration) - processingEnd + * duration = startTime → next paint (Event Timing's INP) + * + * Events from one keypress (keydown / beforeinput / input / keyup) share an + * `interactionId`. Per the Web Vitals INP definition we take the MAX duration + * across each interaction's events. + * + * Note: `durationThreshold` minimum allowed by the spec is 16ms, so single + * keystrokes that complete and paint in <16ms are not reported. In practice + * presentation delay (vsync wait) alone exceeds 16ms, so virtually every real + * keystroke is captured. + */ +async function runKeystrokeInp(page: import("@playwright/test").Page): Promise { + const seedN = Number(process.env.PERF_KEYSTROKE_N ?? 2_000); + const count = Number(process.env.PERF_KEYSTROKE_COUNT ?? 200); + const pacingMs = Number(process.env.PERF_KEYSTROKE_PACING_MS ?? 50); + + await page.goto(`${routeFor(EDITOR_IMPL)}?n=${seedN}&${COMPLEXITY_PARAM}`); + await expect(page.locator(cfg.selector)).toBeVisible({ timeout: 60_000 }); + await page.waitForFunction( + ([sel, target]) => { + const root = document.querySelector(sel as string); + return root ? root.childElementCount >= (target as number) : false; + }, + [cfg.selector, seedN] as const, + { timeout: 60_000 }, + ); + + // Set up the Event Timing collector before any input. `durationThreshold: 16` + // is the spec minimum; entries shorter than that are not reported by Chrome. + await page.evaluate(() => { + interface InpEntry { + name: string; + interactionId: number; + startTime: number; + processingStart: number; + processingEnd: number; + duration: number; + } + const w = window as unknown as { + __inpEntries: InpEntry[]; + __inpObserver: PerformanceObserver; + }; + w.__inpEntries = []; + // `interactionId` is on the Event Timing L1 spec but missing from lib.dom + // in older TS versions, so widen the type locally. + type EventTimingEntry = PerformanceEventTiming & { interactionId: number }; + const observer = new PerformanceObserver((list) => { + for (const entry of list.getEntries()) { + const e = entry as EventTimingEntry; + if (!e.interactionId) continue; + w.__inpEntries.push({ + name: e.name, + interactionId: e.interactionId, + startTime: e.startTime, + processingStart: e.processingStart, + processingEnd: e.processingEnd, + duration: e.duration, + }); + } + }); + observer.observe({ + type: "event", + buffered: true, + durationThreshold: 16, + } as PerformanceObserverInit); + w.__inpObserver = observer; + }); + + // Focus + caret at end so each 'a' appends to the last paragraph. + await page.locator(cfg.selector).click(); + await page.evaluate((selector) => { + const el = document.querySelector(selector) as HTMLElement | null; + if (!el) return; + el.focus(); + const sel = window.getSelection(); + const last = el.lastElementChild ?? el; + if (sel) { + sel.removeAllRanges(); + const range = document.createRange(); + range.selectNodeContents(last); + range.collapse(false); + sel.addRange(range); + } + }, cfg.selector); + + // Warmup keystroke (its latency includes first-frame plumbing). + await page.keyboard.press("a"); + await page.evaluate( + () => new Promise((r) => requestAnimationFrame(() => requestAnimationFrame(() => r()))), + ); + + // Measured loop. Pace between keys so each press is its own interaction + // (otherwise Chrome may merge rapid presses into one interactionId). + for (let i = 0; i < count; i++) { + await page.keyboard.press("a"); + await page.waitForTimeout(pacingMs); + } + + // Flush: give the observer one more paint cycle to deliver final entries. + await page.waitForTimeout(200); + + const entries = await page.evaluate(() => { + interface InpEntry { + name: string; + interactionId: number; + startTime: number; + processingStart: number; + processingEnd: number; + duration: number; + } + const w = window as unknown as { __inpEntries: InpEntry[]; __inpObserver: PerformanceObserver }; + w.__inpObserver.disconnect(); + return w.__inpEntries; + }); + + const interactions = groupInteractions(entries); + + writeFileSync( + join(RESULTS_DIR, perfFileFor(EDITOR_IMPL, "keystroke-inp", "latency")), + JSON.stringify({ entries, interactions }, null, 2), + ); + + const stats = computeStats(interactions); + if (!stats) { + // eslint-disable-next-line no-console + console.log( + `[perf-nv:${EDITOR_IMPL}:keystroke-inp] WARNING: 0 interactions recorded — every keystroke completed in <16ms (Event Timing's durationThreshold minimum). Try a more complex doc, slower CPU, or higher seed n.`, + ); + return; + } + + // eslint-disable-next-line no-console + console.log(`[perf-nv:${EDITOR_IMPL}:keystroke-inp] n=${stats.count} ${formatStatsLine(stats)}`); +} diff --git a/articles/react-prosemirror/e2e/perf/stress.spec.ts b/articles/react-prosemirror/e2e/perf/stress.spec.ts new file mode 100644 index 0000000..9205bd8 --- /dev/null +++ b/articles/react-prosemirror/e2e/perf/stress.spec.ts @@ -0,0 +1,259 @@ +import { mkdirSync, writeFileSync } from "node:fs"; +import { join } from "node:path"; +import { expect, test } from "@playwright/test"; +import { + ACTIVE_METRICS, + EDITOR_IMPL, + IMPL_CONFIG, + MAX_NODES, + MEASUREMENT_INTERVAL, + NODECOUNT_CHECKPOINT, + PERF_SERVER_PORT, + RESULTS_DIR, + routeFor, + TIMEOUT, +} from "./constants"; + +const API = `http://localhost:${PERF_SERVER_PORT}`; +const cfg = IMPL_CONFIG[EDITOR_IMPL]; + +/** + * Wall mode: keep typing until the editor crashes / becomes unusable. + * Enabled with `PERF_STRESS_WALL=1`. Wall is declared on the first of: + * • JSHeapUsedSize > PERF_WALL_HEAP_BYTES (default 3.5 GB) + * • A single 200-node batch takes > PERF_WALL_BATCH_MS (default 30 s) + * • The renderer dies / page.evaluate rejects + * • PERF_WALL_MAX_NODES safety cap is hit (default 500 000) + * + * Wall mode writes sidecar files so existing baselines stay intact: + * ${impl}-stress-wall-perfMetrics.json + * ${impl}-stress-wall-nodecount.json + * ${impl}-stress-wall-meta.json (reason, nodes, elapsedMs, heapBytes) + */ +const WALL_MODE = process.env.PERF_STRESS_WALL === "1"; +const WALL_HEAP_LIMIT_BYTES = Number(process.env.PERF_WALL_HEAP_BYTES ?? 3.5e9); +const WALL_BATCH_TIMEOUT_MS = Number(process.env.PERF_WALL_BATCH_MS ?? 30_000); +const WALL_MAX_NODES_CAP = Number(process.env.PERF_WALL_MAX_NODES ?? 500_000); +const EFFECTIVE_MAX_NODES = WALL_MODE ? WALL_MAX_NODES_CAP : MAX_NODES; + +interface PerfSample { + t: number; + nodes: number; + metrics: Record; +} + +interface NodeSample { + nodes: number; + elapsedMs: number; +} + +type WallReason = "heap" | "slow-batch" | "crash" | "max-nodes" | "timeout" | "ok"; + +interface WallMeta { + reason: WallReason; + nodes: number; + elapsedMs: number; + lastHeapBytes: number; + slowestBatchMs: number; +} + +test(`[PERF] stress${WALL_MODE ? "-wall" : ""} ${cfg.label}`, async ({ browser }) => { + test.setTimeout(TIMEOUT); + mkdirSync(RESULTS_DIR, { recursive: true }); + + const ctx = await browser.newContext(); + const page = await ctx.newPage(); + + if (EDITOR_IMPL === "v3" || EDITOR_IMPL === "v4" || EDITOR_IMPL === "v5") { + await page.goto(routeFor(EDITOR_IMPL)); + await expect(page.locator(cfg.selector)).toBeVisible({ timeout: 15_000 }); + } else { + const create = await ctx.request.post(`${API}/api/documents`, { + data: { title: `perf-${cfg.key}-${Date.now()}`, markdown: "" }, + }); + expect(create.status(), `anon-create returned ${create.status()}`).toBe(201); + const { slug } = (await create.json()) as { slug: string }; + + await page.goto(`/d/${slug}`); + await expect(page.locator(cfg.selector)).toBeVisible({ timeout: 15_000 }); + await expect(page.getByLabel("Sync status: Synced")).toBeVisible({ + timeout: 30_000, + }); + } + await page.locator(cfg.selector).click(); + + const session = await page.context().newCDPSession(page); + await session.send("Performance.enable"); + + const perfSamples: PerfSample[] = []; + const nodeSamples: NodeSample[] = []; + const start = Date.now(); + let n = await page.locator(`${cfg.selector} > *`).count(); + + // Mutable wall state — populated by the outer monitor or the inner loop. + const wall: WallMeta = { + reason: "ok", + nodes: n, + elapsedMs: 0, + lastHeapBytes: 0, + slowestBatchMs: 0, + }; + + const setStopFlag = async (reason: WallReason) => { + if (wall.reason !== "ok") return; + wall.reason = reason; + await page + .evaluate(() => { + (window as unknown as { __perfStop?: boolean }).__perfStop = true; + }) + .catch(() => undefined); + }; + + const metricInterval = setInterval(async () => { + const { metrics } = await session.send("Performance.getMetrics"); + const picked: Record = {}; + for (const m of metrics) { + if ((ACTIVE_METRICS as readonly string[]).includes(m.name)) picked[m.name] = m.value; + } + const liveN = await page + .evaluate(() => (window as unknown as { __perfNodes?: number }).__perfNodes ?? 0) + .catch(() => n); + perfSamples.push({ t: Date.now() - start, nodes: liveN || n, metrics: picked }); + + if (WALL_MODE) { + const heap = picked.JSHeapUsedSize ?? 0; + wall.lastHeapBytes = heap; + if (heap > WALL_HEAP_LIMIT_BYTES && wall.reason === "ok") { + // eslint-disable-next-line no-console + console.log( + `[perf:${cfg.key}:wall] heap ${(heap / 1e9).toFixed(2)} GB > limit ${(WALL_HEAP_LIMIT_BYTES / 1e9).toFixed(2)} GB at ${liveN} nodes — stopping`, + ); + await setStopFlag("heap"); + } + } + }, MEASUREMENT_INTERVAL); + + try { + const browserSamples = await page.evaluate( + async ({ maxNodes, checkpoint, selector, initialN, wallMode, batchTimeoutMs }) => { + const el = document.querySelector(selector) as HTMLElement | null; + if (!el) throw new Error(`selector ${selector} not found`); + el.focus(); + + const samples: { nodes: number; elapsedMs: number }[] = []; + const tStart = Date.now(); + let count = initialN; + let lastCheckpointAt = tStart; + let slowestBatchMs = 0; + (window as unknown as { __perfNodes?: number; __perfStop?: boolean }).__perfNodes = count; + (window as unknown as { __perfStop?: boolean }).__perfStop = false; + + const enterInit: KeyboardEventInit & { keyCode?: number; which?: number } = { + key: "Enter", + code: "Enter", + keyCode: 13, + which: 13, + bubbles: true, + cancelable: true, + }; + + let bailReason: "ok" | "slow-batch" | "stopped" = "ok"; + + while (count < maxNodes) { + if ((window as unknown as { __perfStop?: boolean }).__perfStop) { + bailReason = "stopped"; + break; + } + + for (const ch of "typing ") { + el.dispatchEvent( + new InputEvent("beforeinput", { + inputType: "insertText", + data: ch, + bubbles: true, + cancelable: true, + }), + ); + } + el.dispatchEvent(new KeyboardEvent("keydown", enterInit)); + count++; + + if (count % checkpoint === 0) { + (window as unknown as { __perfNodes?: number }).__perfNodes = count; + const now = Date.now(); + const batchMs = now - lastCheckpointAt; + lastCheckpointAt = now; + if (batchMs > slowestBatchMs) slowestBatchMs = batchMs; + samples.push({ nodes: count, elapsedMs: now - tStart }); + if (wallMode && batchMs > batchTimeoutMs) { + bailReason = "slow-batch"; + break; + } + // Yield once per checkpoint, not per cycle: lets the browser paint, + // the outer CDP poller fire, and __perfStop become visible — but + // doesn't cap throughput at 1 node/frame (~60Hz) the way a per-cycle + // rAF would. Engine cost between checkpoints is now measured at + // full speed, which is the point of this test. + await new Promise((r) => requestAnimationFrame(r)); + } + } + return { samples, bailReason, finalCount: count, slowestBatchMs }; + }, + { + maxNodes: EFFECTIVE_MAX_NODES, + checkpoint: NODECOUNT_CHECKPOINT, + selector: cfg.selector, + initialN: n, + wallMode: WALL_MODE, + batchTimeoutMs: WALL_BATCH_TIMEOUT_MS, + }, + ); + + nodeSamples.push(...browserSamples.samples); + n = browserSamples.finalCount; + wall.nodes = n; + wall.slowestBatchMs = browserSamples.slowestBatchMs; + if (WALL_MODE && wall.reason === "ok") { + if (browserSamples.bailReason === "slow-batch") { + wall.reason = "slow-batch"; + } else if (browserSamples.bailReason === "stopped") { + // outer monitor already set wall.reason (heap, etc.); leave as-is + } else if (n >= EFFECTIVE_MAX_NODES) { + wall.reason = "max-nodes"; + } + } + } catch (err) { + if (WALL_MODE) { + wall.reason = "crash"; + // eslint-disable-next-line no-console + console.log(`[perf:${cfg.key}:wall] renderer crash / evaluate rejected: ${String(err)}`); + } else { + throw err; + } + } finally { + clearInterval(metricInterval); + wall.elapsedMs = Date.now() - start; + + const perfFile = WALL_MODE ? `${cfg.key}-stress-wall-perfMetrics.json` : cfg.perfMetricsFile; + const nodeFile = WALL_MODE ? `${cfg.key}-stress-wall-nodecount.json` : cfg.nodeCountFile; + writeFileSync(join(RESULTS_DIR, perfFile), JSON.stringify(perfSamples, null, 2)); + writeFileSync(join(RESULTS_DIR, nodeFile), JSON.stringify(nodeSamples, null, 2)); + if (WALL_MODE) { + writeFileSync( + join(RESULTS_DIR, `${cfg.key}-stress-wall-meta.json`), + JSON.stringify(wall, null, 2), + ); + // eslint-disable-next-line no-console + console.log( + `[perf:${cfg.key}:wall] reason=${wall.reason} nodes=${wall.nodes} elapsedMs=${wall.elapsedMs} slowestBatchMs=${wall.slowestBatchMs} heapBytes=${wall.lastHeapBytes}`, + ); + } else { + // eslint-disable-next-line no-console + console.log( + `[perf:${cfg.key}] ${nodeSamples.length} node samples, ${perfSamples.length} perf samples, final nodes=${n}`, + ); + } + } + + await ctx.close(); +}); diff --git a/articles/react-prosemirror/editors/lib/perf-gutter-style.ts b/articles/react-prosemirror/editors/lib/perf-gutter-style.ts new file mode 100644 index 0000000..d4616e8 --- /dev/null +++ b/articles/react-prosemirror/editors/lib/perf-gutter-style.ts @@ -0,0 +1,93 @@ +/** + * Shared visual styling for the perf-v{3,4,5}{,-nv,-snv} nodeviews. + * + * Goal: every paragraph carries a visible "authorship gutter" (avatar + chip) + * plus a debug pill (`ctx=N` for the -nv reactive variants, `static` for the + * -snv variants). Same shape across react-prosemirror, vanilla PM, and Tiptap + * pages so cross-impl manual testing actually shows what each nodeview is + * doing. + */ +import type { CSSProperties } from "react"; + +const LETTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + +export const ctxLetter = (n: number): string => LETTERS[((n % 26) + 26) % 26]; + +export const CTX_AVATAR_BG = (n: number): string => `hsl(${(n * 53) % 360}, 65%, 55%)`; + +export const STATIC_AVATAR_BG = "#94a3b8"; // slate-400 + +export const paragraphRowStyle: CSSProperties = { + display: "flex", + alignItems: "baseline", + gap: 10, + margin: "6px 0", +}; + +export const gutterStyle: CSSProperties = { + display: "inline-flex", + alignItems: "center", + gap: 6, + flexShrink: 0, + userSelect: "none", +}; + +export const contentColStyle: CSSProperties = { + flex: 1, + minWidth: 0, +}; + +export const avatarStyle = (bg: string): CSSProperties => ({ + display: "inline-flex", + alignItems: "center", + justifyContent: "center", + width: 22, + height: 22, + borderRadius: "50%", + background: bg, + color: "white", + fontWeight: 600, + fontSize: 12, + fontFamily: "system-ui, -apple-system, sans-serif", + lineHeight: 1, +}); + +export const ctxPillStyle: CSSProperties = { + display: "inline-block", + fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace", + fontSize: 11, + padding: "2px 6px", + borderRadius: 4, + background: "#eef2ff", + color: "#3730a3", + border: "1px solid #c7d2fe", + lineHeight: 1.2, +}; + +export const staticPillStyle: CSSProperties = { + display: "inline-block", + fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace", + fontSize: 11, + padding: "2px 6px", + borderRadius: 4, + background: "#f1f5f9", + color: "#475569", + border: "1px solid #cbd5e1", + lineHeight: 1.2, +}; + +export const ctxDotStyle = (n: number): CSSProperties => ({ + display: "inline-block", + width: 8, + height: 8, + borderRadius: "50%", + background: CTX_AVATAR_BG(n), +}); + +export const staticDotStyle: CSSProperties = { + display: "inline-block", + width: 8, + height: 8, + borderRadius: "50%", + background: STATIC_AVATAR_BG, +}; diff --git a/articles/react-prosemirror/editors/perf-v3-snv/page.tsx b/articles/react-prosemirror/editors/perf-v3-snv/page.tsx new file mode 100644 index 0000000..90f3818 --- /dev/null +++ b/articles/react-prosemirror/editors/perf-v3-snv/page.tsx @@ -0,0 +1,221 @@ +"use client"; + +/** + * perf-v3-snv — react-prosemirror with a STATIC React nodeview. + * + * Sibling of perf-v3-nv (which subscribes to PerfContext via useContext). + * The nodeview here: + * - has no context subscription + * - has no state, no effects + * - renders a fixed inline decoration + content area + * - is memoized: prev.nodeProps.node === next.nodeProps.node && prev.children === next.children + * + * Goal: measure the "realistic custom nodeview" cost — what you'd pay for + * something like an image-with-caption block — without the pathological + * context-flip rerender behavior of perf-v3-nv. + */ +import { + type NodeViewComponentProps, + ProseMirror, + ProseMirrorDoc, + reactKeys, +} from "@handlewithcare/react-prosemirror"; +import { baseKeymap } from "prosemirror-commands"; +import { history, redo, undo } from "prosemirror-history"; +import { keymap } from "prosemirror-keymap"; +import { schema as basicSchema } from "prosemirror-schema-basic"; +import { EditorState, type Transaction } from "prosemirror-state"; +import { type CSSProperties, memo, useEffect, useState } from "react"; +import { + avatarStyle, + contentColStyle, + gutterStyle, + paragraphRowStyle, + STATIC_AVATAR_BG, + staticDotStyle, + staticPillStyle, +} from "@/lib/perf-gutter-style"; + +const StaticParagraphView = memo( + function StaticParagraphView(props: NodeViewComponentProps) { + const { ref, children, nodeProps, ...domAttrs } = props; + const rowStyle: CSSProperties = { ...paragraphRowStyle, ...(domAttrs.style ?? {}) }; + return ( +

+ + P + static + + + + {children} + +

+ ); + }, + (prev, next) => prev.nodeProps.node === next.nodeProps.node && prev.children === next.children, +); + +function readNumberParam(name: string): number { + if (typeof window === "undefined") return 0; + const raw = new URLSearchParams(window.location.search).get(name); + if (!raw) return 0; + const n = Number.parseInt(raw, 10); + return Number.isFinite(n) && n > 0 ? n : 0; +} + +function readContentParam(): "plain" | "tech" { + if (typeof window === "undefined") return "plain"; + const raw = new URLSearchParams(window.location.search).get("content"); + return raw === "tech" ? "tech" : "plain"; +} + +// Inline diagram (SVG, two boxes + edge — stands in for an architecture diagram). +const DIAGRAM_SVG = + "data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20viewBox%3D%220%200%20240%20120%22%3E%3Crect%20width%3D%22240%22%20height%3D%22120%22%20fill%3D%22%23f6f6f6%22%20stroke%3D%22%23ccc%22/%3E%3Crect%20x%3D%2220%22%20y%3D%2230%22%20width%3D%2260%22%20height%3D%2240%22%20fill%3D%22%23dbe9ff%22%20stroke%3D%22%234a90e2%22/%3E%3Crect%20x%3D%22160%22%20y%3D%2230%22%20width%3D%2260%22%20height%3D%2240%22%20fill%3D%22%23ffe1d6%22%20stroke%3D%22%23e2734a%22/%3E%3Cline%20x1%3D%2280%22%20y1%3D%2250%22%20x2%3D%22160%22%20y2%3D%2250%22%20stroke%3D%22%23555%22%20stroke-width%3D%222%22/%3E%3Ctext%20x%3D%2250%22%20y%3D%2255%22%20text-anchor%3D%22middle%22%20font-family%3D%22monospace%22%20font-size%3D%2210%22%3EReact%3C/text%3E%3Ctext%20x%3D%22190%22%20y%3D%2255%22%20text-anchor%3D%22middle%22%20font-family%3D%22monospace%22%20font-size%3D%2210%22%3EPM%3C/text%3E%3C/svg%3E"; + +const TOPICS = [ + "Schema design", + "Transaction pipeline", + "Selection mapping", + "Plugin composition", + "View synchronization", + "Decoration strategy", + "NodeView lifecycle", + "Cursor anchoring", + "Mark application", + "Step inversion", + "History coalescing", + "Input rules", +]; + +const INTROS = [ + "The ${t} subsystem coordinates every state transition that touches the document.", + "Understanding ${t} requires walking through how transactions flow from input handlers to the view.", + "${t} is one of the trickier parts of ProseMirror to get right, because the contract crosses several layers.", + "Most performance regressions in ProseMirror-based editors trace back to ${t} being invoked too eagerly.", +]; + +const BODIES = [ + "Each transaction produces a derived state by applying steps to the previous document. The view then reconciles its DOM against the new state, reusing nodes wherever the descriptor hierarchy matches. When custom NodeViews are involved, the diff has to consult each NodeView's update() callback to decide whether to replace the DOM or keep it.", + "Position arithmetic in ProseMirror is byte-offset based: a paragraph of N characters occupies positions 0..N+1 (the +1 for the closing token). Mapping a position across a transaction means walking the ReplaceStep map, which is generally O(log n) but degenerates to O(n) when the transaction touches many siblings.", + "Collaboration plugins layer on top of the transaction pipeline by tagging every step with a client ID. When remote steps arrive, they are rebased against any local steps that have been applied since the last sync point. This is where prosemirror-collab does most of its work, and where memory pressure tends to manifest in long-lived editing sessions.", + "Decorations are a side-channel that the view honors but the state never sees. Inline decorations are cheap; widget decorations that render arbitrary DOM get expensive fast when the document is large, because the view has to render them on every redraw of the affected range.", +]; + +const CODES = [ + "const tr = state.tr;\ntr.replaceWith(from, to, schema.nodes.paragraph.create());\ndispatch(tr);", + "view.someProp('handleKeyDown', (view, event) => {\n if (event.key !== 'Enter') return false;\n return splitListItem(schema.nodes.list_item)(view.state, view.dispatch);\n});", + "const plugin = new Plugin({\n state: {\n init: () => DecorationSet.empty,\n apply: (tr, set) => set.map(tr.mapping, tr.doc),\n },\n});", + "addNodeView() {\n return ReactNodeViewRenderer(CalloutView, {\n contentDOMElementTag: 'div',\n });\n}", +]; + +const EXPLAINS = [ + "Note that the dispatch above runs synchronously inside the current event tick — any React state updates triggered by it will batch with the rest of the keystroke handler.", + "If you skip the explicit mapping step, your decoration positions will drift the first time a remote collaborator inserts content above the anchor.", + "The plugin state is a DecorationSet, which is structurally shared across transactions; only the changed range allocates new objects.", + "Returning false from stopEvent lets ProseMirror handle the event through its normal keymap path, which is usually what you want unless the NodeView genuinely consumes the input itself.", +]; + +const QUOTES = [ + "Don't reach into view.dom directly during a transaction — the view's reconciliation hasn't run yet, so the DOM you'd be querying is stale.", + "Treat every NodeView's update() as a hot path: if it doesn't bail out cheaply on equal-content cases, large documents will pay for it on every keystroke.", + "Marks are not paragraphs; their position mapping rules differ in subtle ways, especially around the start/end of inline content.", + "A plugin that subscribes to every transaction without filtering is the single most common cause of cubic-time slowdowns at high node counts.", +]; + +const CLOSINGS = [ + "We will revisit this pattern in the next section when we discuss undo coalescing.", + "These constraints are easier to internalize once you've seen them violated in a production codebase.", + "The takeaway: pay the cost at construction time, never on every keystroke.", + "If your editor needs to scale past ~2 000 nodes with React NodeViews, this is the layer to instrument first.", +]; + +function buildTechNodes(n: number) { + const ns = basicSchema.nodes; + const out: ReturnType[] = []; + let section = 0; + const pick = (arr: readonly T[]): T => arr[section % arr.length]; + const fill = (tpl: string, t: string) => tpl.replace(/\$\{t\}/g, t); + while (out.length + 9 <= n) { + const topic = pick(TOPICS); + out.push( + ns.heading.create({ level: 2 }, basicSchema.text(`${section + 1}. ${topic}`)), + ns.paragraph.create(null, basicSchema.text(fill(pick(INTROS), topic))), + ns.paragraph.create(null, basicSchema.text(pick(BODIES))), + ns.code_block.create(null, basicSchema.text(pick(CODES))), + ns.paragraph.create(null, basicSchema.text(pick(EXPLAINS))), + ns.blockquote.create(null, ns.paragraph.create(null, basicSchema.text(pick(QUOTES)))), + ns.paragraph.create(null, ns.image.create({ src: DIAGRAM_SVG })), + ns.paragraph.create(null, basicSchema.text(pick(CLOSINGS))), + ns.horizontal_rule.create(), + ); + section++; + } + while (out.length < n) out.push(ns.paragraph.create()); + return out; +} + +function buildInitialState(n: number, content: "plain" | "tech"): EditorState { + const nodes = + content === "tech" && n > 0 + ? buildTechNodes(n) + : Array.from({ length: n }, () => basicSchema.nodes.paragraph.create()); + const doc = nodes.length > 0 ? basicSchema.nodes.doc.create(null, nodes) : undefined; + return EditorState.create({ + doc, + schema: basicSchema, + plugins: [ + reactKeys(), + history(), + keymap({ + "Mod-z": undo, + "Mod-y": redo, + "Mod-Shift-z": redo, + ...baseKeymap, + }), + ], + }); +} + +export default function PerfV3SnvPage() { + const [state, setState] = useState(() => + buildInitialState(readNumberParam("n"), readContentParam()), + ); + + useEffect(() => { + (window as unknown as { __perfNodes?: number }).__perfNodes = state.doc.childCount; + }, [state.doc.childCount]); + + useEffect(() => { + (window as unknown as { __perfGrow?: (k: number) => void }).__perfGrow = (k: number) => { + setState((s: EditorState) => { + const paragraphs = Array.from({ length: k }, () => basicSchema.nodes.paragraph.create()); + const tr = s.tr.insert(s.doc.content.size, paragraphs); + return s.apply(tr); + }); + }; + return () => { + (window as unknown as { __perfGrow?: (k: number) => void }).__perfGrow = undefined; + }; + }, []); + + return ( +
+

+ perf-v3-snv — react-prosemirror + static React nodeview +

+

+ Memoized React nodeview with no context, no state. Each paragraph renders a static {""}{" "} + + content area. +

+ setState((s) => s.apply(tr))} + nodeViewComponents={{ paragraph: StaticParagraphView }} + > + + +
+ ); +} diff --git a/articles/react-prosemirror/editors/perf-v3/page.tsx b/articles/react-prosemirror/editors/perf-v3/page.tsx new file mode 100644 index 0000000..b0169be --- /dev/null +++ b/articles/react-prosemirror/editors/perf-v3/page.tsx @@ -0,0 +1,85 @@ +"use client"; + +/** + * Minimal react-prosemirror harness — perf-v3. + * + * No custom nodeviews. react-prosemirror renders paragraphs via its internal + * default — the true "out-of-the-box" baseline for the library. + * + * Plugins: ONLY `reactKeys()` (required by the library) + `history` + + * `baseKeymap`. Schema: vanilla `prosemirror-schema-basic`. No collab, no + * decorations, no extensions. + * + * Query params: + * ?n= — seed N empty paragraphs into the initial doc. + */ +import { ProseMirror, ProseMirrorDoc, reactKeys } from "@handlewithcare/react-prosemirror"; +import { baseKeymap } from "prosemirror-commands"; +import { history, redo, undo } from "prosemirror-history"; +import { keymap } from "prosemirror-keymap"; +import { schema as basicSchema } from "prosemirror-schema-basic"; +import { EditorState, type Transaction } from "prosemirror-state"; +import { useEffect, useState } from "react"; + +function readInitialN(): number { + if (typeof window === "undefined") return 0; + const raw = new URLSearchParams(window.location.search).get("n"); + if (!raw) return 0; + const n = Number.parseInt(raw, 10); + return Number.isFinite(n) && n > 0 ? n : 0; +} + +export default function PerfV3Page() { + const [state, setState] = useState(() => { + const n = readInitialN(); + const paragraphs = Array.from({ length: n }, () => basicSchema.nodes.paragraph.create()); + const doc = n > 0 ? basicSchema.nodes.doc.create(null, paragraphs) : undefined; + return EditorState.create({ + doc, + schema: basicSchema, + plugins: [ + reactKeys(), + history(), + keymap({ + "Mod-z": undo, + "Mod-y": redo, + "Mod-Shift-z": redo, + ...baseKeymap, + }), + ], + }); + }); + + useEffect(() => { + (window as unknown as { __perfNodes?: number }).__perfNodes = state.doc.childCount; + }, [state.doc.childCount]); + + useEffect(() => { + (window as unknown as { __perfGrow?: (k: number) => void }).__perfGrow = (k: number) => { + setState((s: EditorState) => { + const paragraphs = Array.from({ length: k }, () => basicSchema.nodes.paragraph.create()); + const tr = s.tr.insert(s.doc.content.size, paragraphs); + return s.apply(tr); + }); + }; + return () => { + (window as unknown as { __perfGrow?: (k: number) => void }).__perfGrow = undefined; + }; + }, []); + + return ( +
+

perf-v3 — barebone react-prosemirror

+

+ No Yjs · No Hocuspocus · No extensions · no custom nodeviews · plain + `prosemirror-schema-basic` + `history` + `baseKeymap`. +

+ setState((s: EditorState) => s.apply(tr))} + > + + +
+ ); +} diff --git a/articles/react-prosemirror/editors/perf-v4-snv/page.tsx b/articles/react-prosemirror/editors/perf-v4-snv/page.tsx new file mode 100644 index 0000000..e736177 --- /dev/null +++ b/articles/react-prosemirror/editors/perf-v4-snv/page.tsx @@ -0,0 +1,130 @@ +"use client"; + +/** + * perf-v4-snv — vanilla ProseMirror with a STATIC pure-DOM nodeview. + * + * Sibling of perf-v4-nv (which writes window.__PERF_CTX into dataset.ctx on + * every transaction). The nodeview here mounts once and never updates — no + * context dependency, no update() handler reacting to outside state. + * + * Goal: floor comparison for "static custom node" — the absolute cheapest + * possible custom NodeView implementation. + */ +import { baseKeymap } from "prosemirror-commands"; +import { history, redo, undo } from "prosemirror-history"; +import { keymap } from "prosemirror-keymap"; +import { schema as basicSchema } from "prosemirror-schema-basic"; +import { EditorState } from "prosemirror-state"; +import { EditorView, type NodeView } from "prosemirror-view"; +import { useEffect, useRef } from "react"; +import { STATIC_AVATAR_BG } from "@/lib/perf-gutter-style"; + +function readNumberParam(name: string): number { + if (typeof window === "undefined") return 0; + const raw = new URLSearchParams(window.location.search).get(name); + if (!raw) return 0; + const n = Number.parseInt(raw, 10); + return Number.isFinite(n) && n > 0 ? n : 0; +} + +class StaticParagraphView implements NodeView { + dom: HTMLParagraphElement; + contentDOM: HTMLElement; + constructor() { + const p = document.createElement("p"); + p.dataset.static = "1"; + p.style.cssText = "display:flex;align-items:baseline;gap:10px;margin:6px 0;"; + + const gutter = document.createElement("span"); + gutter.setAttribute("contenteditable", "false"); + gutter.dataset.perfDecorations = ""; + gutter.style.cssText = + "display:inline-flex;align-items:center;gap:6px;flex-shrink:0;user-select:none;"; + + const avatar = document.createElement("span"); + avatar.textContent = "P"; + avatar.style.cssText = + "display:inline-flex;align-items:center;justify-content:center;" + + "width:22px;height:22px;border-radius:50%;color:white;font-weight:600;" + + "font-size:12px;font-family:system-ui,-apple-system,sans-serif;line-height:1;" + + `background:${STATIC_AVATAR_BG};`; + gutter.appendChild(avatar); + + const pill = document.createElement("span"); + pill.textContent = "static"; + pill.style.cssText = + "display:inline-block;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;" + + "font-size:11px;padding:2px 6px;border-radius:4px;background:#f1f5f9;" + + "color:#475569;border:1px solid #cbd5e1;line-height:1.2;"; + gutter.appendChild(pill); + + const dot = document.createElement("span"); + dot.style.cssText = `display:inline-block;width:8px;height:8px;border-radius:50%;background:${STATIC_AVATAR_BG};`; + gutter.appendChild(dot); + + p.appendChild(gutter); + + const content = document.createElement("span"); + content.style.cssText = "flex:1;min-width:0;"; + p.appendChild(content); + + this.dom = p; + this.contentDOM = content; + } + // No update() handler — node is static. +} + +export default function PerfV4SnvPage() { + const hostRef = useRef(null); + + useEffect(() => { + const host = hostRef.current; + if (!host) return; + + const n = readNumberParam("n"); + const paragraphs = Array.from({ length: n }, () => basicSchema.nodes.paragraph.create()); + const doc = n > 0 ? basicSchema.nodes.doc.create(null, paragraphs) : undefined; + + const state = EditorState.create({ + doc, + schema: basicSchema, + plugins: [ + history(), + keymap({ + "Mod-z": undo, + "Mod-y": redo, + "Mod-Shift-z": redo, + ...baseKeymap, + }), + ], + }); + + const view = new EditorView(host, { + state, + nodeViews: { paragraph: () => new StaticParagraphView() }, + }); + + (window as unknown as { __perfNodes?: number }).__perfNodes = view.state.doc.childCount; + + (window as unknown as { __perfGrow?: (k: number) => void }).__perfGrow = (k: number) => { + const paragraphs = Array.from({ length: k }, () => basicSchema.nodes.paragraph.create()); + view.dispatch(view.state.tr.insert(view.state.doc.content.size, paragraphs)); + (window as unknown as { __perfNodes?: number }).__perfNodes = view.state.doc.childCount; + }; + + return () => { + (window as unknown as { __perfGrow?: (k: number) => void }).__perfGrow = undefined; + view.destroy(); + }; + }, []); + + return ( +
+

perf-v4-snv — vanilla PM + static DOM nodeview

+

+ Pure-DOM NodeView with no update() handler. Renders a static {""} + content area. +

+
+
+ ); +} diff --git a/articles/react-prosemirror/editors/perf-v4/page.tsx b/articles/react-prosemirror/editors/perf-v4/page.tsx new file mode 100644 index 0000000..a622f81 --- /dev/null +++ b/articles/react-prosemirror/editors/perf-v4/page.tsx @@ -0,0 +1,80 @@ +"use client"; + +/** + * Vanilla ProseMirror harness — perf-v4. + * + * Mounts a bare `EditorView` directly on a DOM ref. No React reconciliation + * of editor nodes: PM manages the DOM imperatively, React only mounts/unmounts + * the host div. This is the absolute floor for "react-prosemirror layer cost" + * since there is no react-prosemirror layer at all. + * + * Plugins: history + baseKeymap. Schema: `prosemirror-schema-basic`. + */ +import { useEffect, useRef } from "react"; + +import { baseKeymap } from "prosemirror-commands"; +import { history, redo, undo } from "prosemirror-history"; +import { keymap } from "prosemirror-keymap"; +import { schema as basicSchema } from "prosemirror-schema-basic"; +import { EditorState } from "prosemirror-state"; +import { EditorView } from "prosemirror-view"; + +function readInitialN(): number { + if (typeof window === "undefined") return 0; + const raw = new URLSearchParams(window.location.search).get("n"); + if (!raw) return 0; + const n = Number.parseInt(raw, 10); + return Number.isFinite(n) && n > 0 ? n : 0; +} + +export default function PerfV4Page() { + const hostRef = useRef(null); + + useEffect(() => { + const host = hostRef.current; + if (!host) return; + + const n = readInitialN(); + const paragraphs = Array.from({ length: n }, () => basicSchema.nodes.paragraph.create()); + const doc = n > 0 ? basicSchema.nodes.doc.create(null, paragraphs) : undefined; + + const state = EditorState.create({ + doc, + schema: basicSchema, + plugins: [ + history(), + keymap({ + "Mod-z": undo, + "Mod-y": redo, + "Mod-Shift-z": redo, + ...baseKeymap, + }), + ], + }); + + const view = new EditorView(host, { state }); + + (window as unknown as { __perfNodes?: number }).__perfNodes = view.state.doc.childCount; + (window as unknown as { __perfGrow?: (k: number) => void }).__perfGrow = (k: number) => { + const paragraphs = Array.from({ length: k }, () => basicSchema.nodes.paragraph.create()); + view.dispatch(view.state.tr.insert(view.state.doc.content.size, paragraphs)); + (window as unknown as { __perfNodes?: number }).__perfNodes = view.state.doc.childCount; + }; + + return () => { + (window as unknown as { __perfGrow?: (k: number) => void }).__perfGrow = undefined; + view.destroy(); + }; + }, []); + + return ( +
+

perf-v4 — vanilla ProseMirror

+

+ No React reconciliation · plain EditorView mounted on a div ref · + `prosemirror-schema-basic` + `history` + `baseKeymap`. +

+
+
+ ); +} diff --git a/articles/react-prosemirror/editors/perf-v5-snv/page.tsx b/articles/react-prosemirror/editors/perf-v5-snv/page.tsx new file mode 100644 index 0000000..958c287 --- /dev/null +++ b/articles/react-prosemirror/editors/perf-v5-snv/page.tsx @@ -0,0 +1,96 @@ +"use client"; + +/** + * perf-v5-snv — Tiptap 3 with a STATIC React nodeview (via ReactNodeViewRenderer). + * + * Sibling of perf-v5-nv (which subscribes to PerfContext via useContext). + * The nodeview here has no context, no state — just a memoized component + * that renders a fixed inline + content area. + * + * Goal: measure realistic ReactNodeViewRenderer cost without context-flip + * pathology. + */ +import { Document } from "@tiptap/extension-document"; +import { History } from "@tiptap/extension-history"; +import { Paragraph } from "@tiptap/extension-paragraph"; +import { Text } from "@tiptap/extension-text"; +import { + EditorContent, + NodeViewContent, + NodeViewWrapper, + ReactNodeViewRenderer, + useEditor, +} from "@tiptap/react"; +import { memo, useEffect, useState } from "react"; +import { + avatarStyle, + contentColStyle, + gutterStyle, + paragraphRowStyle, + STATIC_AVATAR_BG, + staticDotStyle, + staticPillStyle, +} from "@/lib/perf-gutter-style"; + +const StaticParagraphView = memo(function StaticParagraphView() { + return ( + + + P + static + + + + + ); +}); + +const StaticParagraph = Paragraph.extend({ + addNodeView() { + return ReactNodeViewRenderer(StaticParagraphView); + }, +}); + +function readNumberParam(name: string): number { + if (typeof window === "undefined") return 0; + const raw = new URLSearchParams(window.location.search).get(name); + if (!raw) return 0; + const n = Number.parseInt(raw, 10); + return Number.isFinite(n) && n > 0 ? n : 0; +} + +export default function PerfV5SnvPage() { + const [initialContent] = useState(() => { + const n = readNumberParam("n"); + return n > 0 ? "

".repeat(n) : ""; + }); + + const editor = useEditor({ + immediatelyRender: false, + extensions: [Document, StaticParagraph, Text, History], + content: initialContent, + }); + + useEffect(() => { + if (!editor) return; + (window as unknown as { __perfNodes?: number }).__perfNodes = editor.state.doc.childCount; + + (window as unknown as { __perfGrow?: (k: number) => void }).__perfGrow = (k: number) => { + editor.chain().focus("end").insertContent("

".repeat(k)).run(); + (window as unknown as { __perfNodes?: number }).__perfNodes = editor.state.doc.childCount; + }; + return () => { + (window as unknown as { __perfGrow?: (k: number) => void }).__perfGrow = undefined; + }; + }, [editor]); + + return ( +
+

perf-v5-snv — Tiptap 3 + static React nodeview

+

+ Memoized React nodeview via `ReactNodeViewRenderer`, no context, no state. +

+ +
+ ); +} diff --git a/articles/react-prosemirror/editors/perf-v5/page.tsx b/articles/react-prosemirror/editors/perf-v5/page.tsx new file mode 100644 index 0000000..9513f6d --- /dev/null +++ b/articles/react-prosemirror/editors/perf-v5/page.tsx @@ -0,0 +1,59 @@ +"use client"; + +/** + * Bare-bone Tiptap 3 harness — perf-v5. + * + * `useEditor` with the absolute minimum extensions: Document + Paragraph + + * Text + History. No collab, no decorations, no custom marks/nodes. Compares + * Tiptap's overhead vs vanilla ProseMirror (v4) and vs react-prosemirror (v3) + * at the same scale. + */ +import { Document } from "@tiptap/extension-document"; +import { History } from "@tiptap/extension-history"; +import { Paragraph } from "@tiptap/extension-paragraph"; +import { Text } from "@tiptap/extension-text"; +import { EditorContent, useEditor } from "@tiptap/react"; +import { useEffect, useState } from "react"; + +function readInitialN(): number { + if (typeof window === "undefined") return 0; + const raw = new URLSearchParams(window.location.search).get("n"); + if (!raw) return 0; + const n = Number.parseInt(raw, 10); + return Number.isFinite(n) && n > 0 ? n : 0; +} + +export default function PerfV5Page() { + const [initialContent] = useState(() => { + const n = readInitialN(); + return n > 0 ? "

".repeat(n) : ""; + }); + + const editor = useEditor({ + immediatelyRender: false, + extensions: [Document, Paragraph, Text, History], + content: initialContent, + }); + + useEffect(() => { + if (!editor) return; + (window as unknown as { __perfNodes?: number }).__perfNodes = editor.state.doc.childCount; + (window as unknown as { __perfGrow?: (k: number) => void }).__perfGrow = (k: number) => { + editor.chain().focus("end").insertContent("

".repeat(k)).run(); + (window as unknown as { __perfNodes?: number }).__perfNodes = editor.state.doc.childCount; + }; + return () => { + (window as unknown as { __perfGrow?: (k: number) => void }).__perfGrow = undefined; + }; + }, [editor]); + + return ( +
+

perf-v5 — bare-bone Tiptap 3

+

+ Document + Paragraph + Text + History only · no collab, no decorations. +

+ +
+ ); +} diff --git a/articles/react-prosemirror/playwright.perf.config.ts b/articles/react-prosemirror/playwright.perf.config.ts new file mode 100644 index 0000000..0e0fe36 --- /dev/null +++ b/articles/react-prosemirror/playwright.perf.config.ts @@ -0,0 +1,40 @@ +/// +import { join } from "node:path"; +import { defineConfig, devices } from "@playwright/test"; +import { GLOBALTIMEOUT, PERF_WEB_PORT, TIMEOUT } from "./e2e/perf/constants"; + +// Repo root (two levels up from articles/react-prosemirror). The perf editor +// routes live in the main blog Next app, so we boot it directly — no separate +// @proof/web / @proof/server processes like the original proof-stack config. +const REPO_ROOT = join(__dirname, "..", ".."); + +// Default to `next start` (production build — representative perf numbers). +// Set PERF_DEV=1 to boot `next dev` instead (faster iteration, noisier perf). +const DEV = process.env.PERF_DEV === "1"; + +export default defineConfig({ + testDir: "./e2e/perf", + testMatch: /(stress|nodeview|keystroke-ramp)\.spec\.ts$/, + fullyParallel: false, + workers: 1, + retries: 0, + reporter: [["list"]], + timeout: TIMEOUT, + globalTimeout: GLOBALTIMEOUT, + use: { + baseURL: `http://localhost:${PERF_WEB_PORT}`, + trace: "off", + video: "off", + screenshot: "off", + }, + projects: [{ name: "chromium", use: { ...devices["Desktop Chrome"] } }], + webServer: { + command: DEV + ? `npx next dev -p ${PERF_WEB_PORT}` + : `npx next start -p ${PERF_WEB_PORT}`, + cwd: REPO_ROOT, + url: `http://localhost:${PERF_WEB_PORT}/perf-react-prosemirror`, + timeout: 120_000, + reuseExistingServer: true, + }, +}); diff --git a/articles/react-prosemirror/scripts/run-editor-perf.ts b/articles/react-prosemirror/scripts/run-editor-perf.ts new file mode 100644 index 0000000..54a0b01 --- /dev/null +++ b/articles/react-prosemirror/scripts/run-editor-perf.ts @@ -0,0 +1,54 @@ +/// +/** + * Stress-perf orchestrator — runs `e2e/perf/stress.spec.ts` across the + * standalone editor impls (v3 / v4 / v5), one Playwright invocation per impl, + * then generates the comparison graphs. + * + * Each impl has its own static blog route (see `routeFor` in constants.ts), + * so — unlike the original proof-stack setup — there's no per-impl rebuild: + * we build the blog once, then the Playwright webServer boots `next start`. + * + * Usage (from repo root): + * npx tsx articles/react-prosemirror/scripts/run-editor-perf.ts + * PERF_IMPLS=v3,v5 npx tsx articles/react-prosemirror/scripts/run-editor-perf.ts + * PERF_SKIP_BUILD=1 ... # reuse an existing production build + * PERF_DEV=1 ... # boot `next dev` instead of build + start + */ +import { spawnSync } from "node:child_process"; +import { join } from "node:path"; + +type EditorImpl = "v3" | "v4" | "v5"; + +const ARTICLE_ROOT = join(__dirname, ".."); +const REPO_ROOT = join(__dirname, "..", "..", ".."); + +const impls = (process.env.PERF_IMPLS ?? "v3,v4,v5") + .split(",") + .map((s) => s.trim()) + .filter(Boolean) as EditorImpl[]; + +buildOnce(); + +for (const impl of impls) { + console.log(`\n=== running stress perf for ${impl} ===\n`); + run( + "npx", + ["playwright", "test", "--config=playwright.perf.config.ts", "e2e/perf/stress.spec.ts"], + ARTICLE_ROOT, + { EDITOR_IMPL: impl }, + ); +} + +console.log("\n=== generating graphs ===\n"); +run("npx", ["tsx", "e2e/perf/create-graphs.ts"], ARTICLE_ROOT, {}); + +function buildOnce(): void { + if (process.env.PERF_SKIP_BUILD === "1" || process.env.PERF_DEV === "1") return; + console.log("\n=== building blog (next build) ===\n"); + run("npm", ["run", "build"], REPO_ROOT, {}); +} + +function run(cmd: string, args: string[], cwd: string, env: Record): void { + const result = spawnSync(cmd, args, { stdio: "inherit", cwd, env: { ...process.env, ...env } }); + if (result.status !== 0) process.exit(result.status ?? 1); +} diff --git a/articles/react-prosemirror/scripts/run-keystroke-ramp.ts b/articles/react-prosemirror/scripts/run-keystroke-ramp.ts new file mode 100644 index 0000000..9007088 --- /dev/null +++ b/articles/react-prosemirror/scripts/run-keystroke-ramp.ts @@ -0,0 +1,69 @@ +/// +/** + * Keystroke-ramp orchestrator — runs `e2e/perf/keystroke-ramp.spec.ts` across + * the impls it supports (v3 / v4 / v5 / v3-snv / v4-snv / v5-snv), one + * Playwright invocation per impl, then generates the comparison graph. The + * spec sweeps node count internally and bails when typing becomes laggy, so + * this script just iterates impls. + * + * Usage (from repo root): + * npx tsx articles/react-prosemirror/scripts/run-keystroke-ramp.ts + * PERF_RAMP_IMPLS=v3-snv npx tsx articles/react-prosemirror/scripts/run-keystroke-ramp.ts + * PERF_RAMP_STEP=250 PERF_LAG_P95_MS=80 npx tsx ...run-keystroke-ramp.ts + * PERF_SKIP_BUILD=1 ... # reuse an existing production build + * PERF_DEV=1 ... # boot `next dev` instead of build + start + */ +import { spawnSync } from "node:child_process"; +import { join } from "node:path"; + +type RampImpl = "v3" | "v4" | "v5" | "v3-snv" | "v4-snv" | "v5-snv"; + +const ARTICLE_ROOT = join(__dirname, ".."); +const REPO_ROOT = join(__dirname, "..", "..", ".."); + +const DEFAULT_IMPLS: RampImpl[] = ["v3-snv", "v4-snv", "v5-snv"]; +const ALL_IMPLS: readonly RampImpl[] = ["v3", "v4", "v5", "v3-snv", "v4-snv", "v5-snv"]; +const IMPLS = parseList(process.env.PERF_RAMP_IMPLS, DEFAULT_IMPLS, ALL_IMPLS); + +buildOnce(); + +for (const impl of IMPLS) { + console.log(`\n=== running keystroke-ramp for ${impl} ===\n`); + run( + "npx", + ["playwright", "test", "--config=playwright.perf.config.ts", "e2e/perf/keystroke-ramp.spec.ts"], + ARTICLE_ROOT, + { EDITOR_IMPL: impl }, + ); +} + +console.log("\n=== generating keystroke-ramp graph ===\n"); +run("npx", ["tsx", "e2e/perf/create-graphs.ts"], ARTICLE_ROOT, { + GRAPH_IMPLS: IMPLS.join(","), + GRAPH_SCENARIO: "keystroke-ramp", +}); + +function buildOnce(): void { + if (process.env.PERF_SKIP_BUILD === "1" || process.env.PERF_DEV === "1") return; + console.log("\n=== building blog (next build) ===\n"); + run("npm", ["run", "build"], REPO_ROOT, {}); +} + +function run(cmd: string, args: string[], cwd: string, env: Record): void { + const result = spawnSync(cmd, args, { stdio: "inherit", cwd, env: { ...process.env, ...env } }); + if (result.status !== 0) process.exit(result.status ?? 1); +} + +function parseList( + raw: string | undefined, + fallback: T[], + allowed: readonly T[] = fallback, +): T[] { + if (!raw) return fallback; + const allowedSet = allowed as readonly string[]; + const parsed = raw + .split(",") + .map((s) => s.trim()) + .filter((s): s is T => allowedSet.includes(s)); + return parsed.length > 0 ? parsed : fallback; +} diff --git a/articles/react-prosemirror/scripts/run-nodeview-perf.ts b/articles/react-prosemirror/scripts/run-nodeview-perf.ts new file mode 100644 index 0000000..303a107 --- /dev/null +++ b/articles/react-prosemirror/scripts/run-nodeview-perf.ts @@ -0,0 +1,116 @@ +/// +/** + * Nodeview perf orchestrator — runs `e2e/perf/nodeview.spec.ts` across impls × + * scenarios, then generates per-scenario graphs. + * + * Each impl has its own static blog route (see `routeFor` in constants.ts), so + * we build the blog once and the Playwright webServer boots `next start`. + * + * NOTE: the reactive `-nv` impls (v3-nv / v4-nv / v5-nv) and the `ctx-flip` + * scenario require nodeview pages that expose a `window.__PERF_CTX` hook; those + * pages were not part of the initial copy, so the default impl set is the ones + * with live routes (v3 / v4 / v5). Add `-snv` or `-nv` via PERF_NV_IMPLS once + * their pages exist. + * + * Usage (from repo root): + * npx tsx articles/react-prosemirror/scripts/run-nodeview-perf.ts + * PERF_MAX_NODES=500 npx tsx ...run-nodeview-perf.ts + * PERF_NV_SCENARIOS=typing,cold-load npx tsx ...run-nodeview-perf.ts + * PERF_NV_IMPLS=v3,v5 npx tsx ...run-nodeview-perf.ts + * PERF_SKIP_BUILD=1 ... / PERF_DEV=1 ... + */ +import { spawnSync } from "node:child_process"; +import { join } from "node:path"; + +type NvImpl = + | "v3" + | "v4" + | "v5" + | "v3-snv" + | "v4-snv" + | "v5-snv" + | "v3-nv" + | "v4-nv" + | "v5-nv"; +type Scenario = + | "typing" + | "cold-load" + | "cursor" + | "ctx-flip" + | "keystroke-latency" + | "keystroke-inp"; + +const ARTICLE_ROOT = join(__dirname, ".."); +const REPO_ROOT = join(__dirname, "..", "..", ".."); + +const DEFAULT_IMPLS: NvImpl[] = ["v3", "v4", "v5"]; +const ALL_IMPLS: NvImpl[] = [ + "v3", + "v4", + "v5", + "v3-snv", + "v4-snv", + "v5-snv", + "v3-nv", + "v4-nv", + "v5-nv", +]; +const DEFAULT_SCENARIOS: Scenario[] = ["typing", "cold-load"]; +const ALL_SCENARIOS: Scenario[] = [ + "typing", + "cold-load", + "cursor", + "ctx-flip", + "keystroke-latency", + "keystroke-inp", +]; + +const IMPLS: NvImpl[] = parseList(process.env.PERF_NV_IMPLS, DEFAULT_IMPLS, ALL_IMPLS); +const SCENARIOS: Scenario[] = parseList(process.env.PERF_NV_SCENARIOS, DEFAULT_SCENARIOS, ALL_SCENARIOS); + +buildOnce(); + +for (const impl of IMPLS) { + for (const scenario of SCENARIOS) { + console.log(`\n=== running perf for ${impl} / ${scenario} ===\n`); + run( + "npx", + ["playwright", "test", "--config=playwright.perf.config.ts", "e2e/perf/nodeview.spec.ts"], + ARTICLE_ROOT, + { EDITOR_IMPL: impl, PERF_SCENARIO: scenario }, + ); + } +} + +for (const scenario of SCENARIOS) { + console.log(`\n=== generating nodeview graphs (${scenario}) ===\n`); + run("npx", ["tsx", "e2e/perf/create-graphs.ts"], ARTICLE_ROOT, { + GRAPH_IMPLS: IMPLS.join(","), + GRAPH_SCENARIO: scenario, + }); +} + +function buildOnce(): void { + if (process.env.PERF_SKIP_BUILD === "1" || process.env.PERF_DEV === "1") return; + console.log("\n=== building blog (next build) ===\n"); + run("npm", ["run", "build"], REPO_ROOT, {}); +} + +function run(cmd: string, args: string[], cwd: string, env: Record): void { + const result = spawnSync(cmd, args, { stdio: "inherit", cwd, env: { ...process.env, ...env } }); + if (result.status !== 0) process.exit(result.status ?? 1); +} + +function parseList( + raw: string | undefined, + fallback: T[], + allowed: T[] = fallback, +): T[] { + if (!raw) return fallback; + const validValues = allowed as readonly string[]; + const parsed = raw + .split(",") + .map((s) => s.trim()) + .filter((s): s is T => validValues.includes(s)); + return parsed.length > 0 ? parsed : fallback; +} diff --git a/articles/react-prosemirror/tsconfig.json b/articles/react-prosemirror/tsconfig.json new file mode 100644 index 0000000..718ccbe --- /dev/null +++ b/articles/react-prosemirror/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "types": ["node"], + "noEmit": true, + "noUnusedLocals": false + }, + "include": ["e2e", "scripts", "playwright.perf.config.ts"], + "exclude": ["../../node_modules"] +} diff --git a/features/article/components/Markdown.tsx b/features/article/components/Markdown.tsx index 9797ebd..e07067a 100644 --- a/features/article/components/Markdown.tsx +++ b/features/article/components/Markdown.tsx @@ -1,5 +1,6 @@ import React, { FunctionComponent } from "react"; import ReactMarkdown from "react-markdown"; +import remarkGfm from "remark-gfm"; import CodeHighlight from "./CodeHighlight"; @@ -15,6 +16,7 @@ const Markdown: FunctionComponent = (props: MarkDownProps) => { return ( @@ -75,9 +77,9 @@ const Markdown: FunctionComponent = (props: MarkDownProps) => { {props.children} ), - hr: ( ) => ( -
- ), + hr: () => ( +
+ ), strong: ({ ...props }) => ( {props.children} @@ -88,6 +90,29 @@ const Markdown: FunctionComponent = (props: MarkDownProps) => { {props.children} ), + table: ({ ...props }) => ( + + {props.children} +
+ ), + thead: ({ ...props }) => ( + + {props.children} + + ), + th: ({ ...props }) => ( + + {props.children} + + ), + td: ({ ...props }) => ( + + {props.children} + + ), }} > {source} @@ -95,4 +120,5 @@ const Markdown: FunctionComponent = (props: MarkDownProps) => { ); }; -export default Markdown; \ No newline at end of file +export default Markdown; + diff --git a/features/twBlog/TwBlog.tsx b/features/twBlog/TwBlog.tsx index 8fd7028..9f6bcb7 100644 --- a/features/twBlog/TwBlog.tsx +++ b/features/twBlog/TwBlog.tsx @@ -23,6 +23,7 @@ import { articlePMMetadata } from "../../pages/blog/prosemirror"; import PostCard from "./PostCard"; import TabComponent from "./TabComponent"; +import { articleReactProsemirrorMetadata } from "../../pages/blog/react-prosemirror"; const TwBlog: FunctionComponent = () => { const [tab, setTab] = useState<"article" | "tech">("tech"); @@ -49,6 +50,7 @@ const TwBlog: FunctionComponent = () => { )} {tab === "tech" && ( <> + diff --git a/package-lock.json b/package-lock.json index 76bdaea..3f1e9ae 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,12 +14,20 @@ "@codemirror/state": "^6.1.4", "@codemirror/view": "^6.7.1", "@emergence-engineering/lexical-slash-menu-plugin": "^0.0.3", + "@handlewithcare/react-prosemirror": "^3.0.6", "@inline-svg-unique-id/react": "^1.2.3", "@lexical/list": "^0.11.3", "@lexical/react": "^0.11.3", "@lexical/selection": "^0.11.3", "@popperjs/core": "^2.11.8", "@textea/json-viewer": "^2.12.0", + "@tiptap/core": "^3.24.0", + "@tiptap/extension-document": "^3.24.0", + "@tiptap/extension-history": "^3.24.0", + "@tiptap/extension-paragraph": "^3.24.0", + "@tiptap/extension-text": "^3.24.0", + "@tiptap/pm": "^3.24.0", + "@tiptap/react": "^3.24.0", "axios": "^1.2.1", "codemirror": "^6.0.1", "cors": "^2.8.5", @@ -58,15 +66,18 @@ "react-markdown": "^9.0.1", "react-popper": "^2.3.0", "react-recaptcha": "^2.3.10", + "react-reconciler": "^0.29.2", "react-responsive": "^9.0.2", "react-syntax-highlighter": "^15.5.0", "react-youtube": "^10.1.0", + "remark-gfm": "^4.0.1", "styled-components": "^5.3.6", "y-prosemirror": "^1.2.1", "y-protocols": "^1.0.5", "yjs": "^13.6.4" }, "devDependencies": { + "@playwright/test": "^1.60.0", "@svgr/webpack": "^8.1.0", "@types/cors": "^2.8.13", "@types/node": "^18.11.15", @@ -92,17 +103,21 @@ "@types/styled-components": "^5.1.26", "autoprefixer": "^10.4.20", "babel-plugin-react-inline-svg-unique-id": "^1.4.0", + "chart.js": "^4.5.1", + "chartjs-node-canvas": "^5.0.0", "eslint": "^8.29.0", "eslint-config-next": "^13.0.7", "eslint-plugin-import": "^2.29.1", "git-commit-id": "^2.0.1", "husky": "^8.0.2", "npm-check": "^6.0.1", + "patch-package": "^8.0.1", "postcss": "^8.4.41", "prettier": "^3.3.3", "prettier-plugin-tailwindcss": "^0.6.6", "tailwindcss": "^3.4.9", "ts-node": "^10.9.1", + "tsx": "^4.22.4", "typescript": "^4.9.4" }, "engines": { @@ -2625,6 +2640,448 @@ "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.3.0.tgz", "integrity": "sha512-AHPmaAx+RYfZz0eYu6Gviiagpmiyw98ySSlQvCUhVGDRtDFe4DBS0x1bSjdF3gqUDYOczB+yYvBTtEylYSdRhg==" }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.0.tgz", + "integrity": "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.0.tgz", + "integrity": "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.0.tgz", + "integrity": "sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.0.tgz", + "integrity": "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.0.tgz", + "integrity": "sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.0.tgz", + "integrity": "sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.0.tgz", + "integrity": "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.0.tgz", + "integrity": "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.0.tgz", + "integrity": "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.0.tgz", + "integrity": "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.0.tgz", + "integrity": "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.0.tgz", + "integrity": "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.0.tgz", + "integrity": "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.0.tgz", + "integrity": "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.0.tgz", + "integrity": "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.0.tgz", + "integrity": "sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.0.tgz", + "integrity": "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.0.tgz", + "integrity": "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.0.tgz", + "integrity": "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.0.tgz", + "integrity": "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.0.tgz", + "integrity": "sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.0.tgz", + "integrity": "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.0.tgz", + "integrity": "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.0.tgz", + "integrity": "sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.0.tgz", + "integrity": "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.0.tgz", + "integrity": "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@eslint/eslintrc": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.3.3.tgz", @@ -2681,6 +3138,26 @@ "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.1.6.tgz", "integrity": "sha512-OfX7E2oUDYxtBvsuS4e/jSn4Q9Qb6DzgeYtsAdkPZ47znpoNsMgZw0+tVijiv3uGNR6dgNlty6r9rzIzHjtd/A==" }, + "node_modules/@handlewithcare/react-prosemirror": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@handlewithcare/react-prosemirror/-/react-prosemirror-3.0.6.tgz", + "integrity": "sha512-7HNwItwpi1dZ0lq2KvqCvGhZaP3QXDFQOcyap3mNb9HyFPYXWdTQm1XQEGQ1sZ5656OOwAzzYU/q7ncRvIJ45w==", + "license": "Apache-2.0", + "dependencies": { + "classnames": "^2.5.1" + }, + "engines": { + "node": ">=16.9" + }, + "peerDependencies": { + "prosemirror-model": "^1.0.0", + "prosemirror-state": "^1.0.0", + "prosemirror-view": "1.41.7", + "react": ">=17 <20", + "react-dom": ">=17 <20", + "react-reconciler": ">=0.26.1 <=0.33.0" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.7", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.7.tgz", @@ -2869,6 +3346,13 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@kurkle/color": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz", + "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==", + "dev": true, + "license": "MIT" + }, "node_modules/@lexical/clipboard": { "version": "0.11.3", "resolved": "https://registry.npmjs.org/@lexical/clipboard/-/clipboard-0.11.3.tgz", @@ -3731,6 +4215,22 @@ "url": "https://opencollective.com/unts" } }, + "node_modules/@playwright/test": { + "version": "1.60.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.60.0.tgz", + "integrity": "sha512-O71yZIbAh/PxDMNGns37GHBIfrVkEVyn+AXyIa5dOTfb4/xNvRWV+Vv/NMbNCtODB/pO7vLlF2OTmMVLhmr7Ag==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.60.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@popperjs/core": { "version": "2.11.8", "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", @@ -4130,6 +4630,181 @@ "react-dom": "^17 || ^18" } }, + "node_modules/@tiptap/core": { + "version": "3.24.0", + "resolved": "https://registry.npmjs.org/@tiptap/core/-/core-3.24.0.tgz", + "integrity": "sha512-GTAsXAI32p4hEZgPzvUv2RPrObxamy9AFhmhG10fXSvN/cDUs8naEYVIqDV3Sh99jMwQEbTFKW1E1mcspsY6ow==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/pm": "3.24.0" + } + }, + "node_modules/@tiptap/extension-bubble-menu": { + "version": "3.24.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-bubble-menu/-/extension-bubble-menu-3.24.0.tgz", + "integrity": "sha512-jRXD+JPu9ayvq78g8hsCxx4q/qUFtrdfIYirRSf5YUseuuUbtfrq83AsGabcygpUTefjJkMQoXNITkh6294Ggw==", + "license": "MIT", + "optional": true, + "dependencies": { + "@floating-ui/dom": "^1.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.24.0", + "@tiptap/pm": "3.24.0" + } + }, + "node_modules/@tiptap/extension-document": { + "version": "3.24.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-document/-/extension-document-3.24.0.tgz", + "integrity": "sha512-yxgM3+yXy2XZzEwH43y2Kp8D1BkblxEWLXqo0YCoAKtxyKCcEaT8kdlf70kS7D0+VSzYU4D0iN7VdQIYHcL2mA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.24.0" + } + }, + "node_modules/@tiptap/extension-floating-menu": { + "version": "3.24.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-floating-menu/-/extension-floating-menu-3.24.0.tgz", + "integrity": "sha512-7QEbf3mUzFAkejjQGX9f0L507oMtnOBRwHt2skUTR+9yXgudsN8zaDBSSRHLeMWGk9b7L293ZMA6zCRrZaHrfA==", + "license": "MIT", + "optional": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@floating-ui/dom": "^1.0.0", + "@tiptap/core": "3.24.0", + "@tiptap/pm": "3.24.0" + } + }, + "node_modules/@tiptap/extension-history": { + "version": "3.24.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-history/-/extension-history-3.24.0.tgz", + "integrity": "sha512-pGRSXgJ+f3KIJq8kNq9mGJAN0ryJEZgBQq5Ag7Lk9Bo7OE+0SSYEs2gCEmY8+Thtw/YpICaVzx06pFy9DZt3eQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extensions": "3.24.0" + } + }, + "node_modules/@tiptap/extension-paragraph": { + "version": "3.24.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-paragraph/-/extension-paragraph-3.24.0.tgz", + "integrity": "sha512-wD06aB6hO7LgcrlhGiw7I64k2tus9kNoICX5R+UecBSB1DVJdzKvXoXL2kPNv4DqYvljHdkIeK/OpuOTQd6MJA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.24.0" + } + }, + "node_modules/@tiptap/extension-text": { + "version": "3.24.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-text/-/extension-text-3.24.0.tgz", + "integrity": "sha512-Im7keLPEihxm3+LyF+drYCoaOY5hlq35lvHAp/el6M8pJ/scts88HrYpdR1Yc4BtpZBIhfHSyWgPaupI4qwdeg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.24.0" + } + }, + "node_modules/@tiptap/extensions": { + "version": "3.24.0", + "resolved": "https://registry.npmjs.org/@tiptap/extensions/-/extensions-3.24.0.tgz", + "integrity": "sha512-z6gRYzy2ucJp07OQ0F2W07NxyhMTxPYH1ia2eGiQkWax1i56oExpjMsDHP8THWlg8Tb7NnbfKpkfh881EsmofA==", + "license": "MIT", + "peer": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.24.0", + "@tiptap/pm": "3.24.0" + } + }, + "node_modules/@tiptap/pm": { + "version": "3.24.0", + "resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-3.24.0.tgz", + "integrity": "sha512-QQP/78ryOZDN99gNBV7dgh69/8AYaOYQYFklq/iR+ZRFaaL3+qqHFvPVJapGkzPdymBgNJ34xjFM8n5pJ4QmMg==", + "license": "MIT", + "dependencies": { + "prosemirror-changeset": "^2.3.0", + "prosemirror-commands": "^1.6.2", + "prosemirror-dropcursor": "^1.8.1", + "prosemirror-gapcursor": "^1.3.2", + "prosemirror-history": "^1.4.1", + "prosemirror-inputrules": "^1.4.0", + "prosemirror-keymap": "^1.2.2", + "prosemirror-model": "^1.24.1", + "prosemirror-schema-list": "^1.5.0", + "prosemirror-state": "^1.4.3", + "prosemirror-tables": "^1.6.4", + "prosemirror-transform": "^1.10.2", + "prosemirror-view": "^1.38.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + } + }, + "node_modules/@tiptap/react": { + "version": "3.24.0", + "resolved": "https://registry.npmjs.org/@tiptap/react/-/react-3.24.0.tgz", + "integrity": "sha512-KxnrlQbzOgA02EMsfuGGHtNhfkJQGqVlQttmQctI9DOl/F3gcaRqg+wNTBY1Fof8yDaZ8Z1LL1F0C05W0o3vUw==", + "license": "MIT", + "dependencies": { + "@types/use-sync-external-store": "^0.0.6", + "fast-equals": "^5.3.3", + "use-sync-external-store": "^1.4.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "optionalDependencies": { + "@tiptap/extension-bubble-menu": "^3.24.0", + "@tiptap/extension-floating-menu": "^3.24.0" + }, + "peerDependencies": { + "@tiptap/core": "3.24.0", + "@tiptap/pm": "3.24.0", + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "@types/react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@tiptap/react/node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/@trysound/sax": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz", @@ -4610,7 +5285,6 @@ "version": "18.2.6", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.6.tgz", "integrity": "sha512-2et4PDvg6PVCyS7fuTc4gPoksV58bW0RwSxWKcPRcHZf0PRUGq03TKcD/rUHe3azfV6/5/biUBJw+HhCQjaP0A==", - "dev": true, "dependencies": { "@types/react": "*" } @@ -4705,6 +5379,12 @@ "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==" }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", + "license": "MIT" + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "5.46.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.46.1.tgz", @@ -5153,6 +5833,13 @@ "optional": true, "peer": true }, + "node_modules/@yarnpkg/lockfile": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz", + "integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==", + "dev": true, + "license": "BSD-2-Clause" + }, "node_modules/abort-controller": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", @@ -5906,15 +6593,45 @@ } }, "node_modules/call-bind": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", - "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", + "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "get-intrinsic": "^1.3.0", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", "dependencies": { - "es-define-property": "^1.0.0", "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "set-function-length": "^1.2.1" + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" }, "engines": { "node": ">= 0.4" @@ -6101,6 +6818,21 @@ } ] }, + "node_modules/canvas": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/canvas/-/canvas-3.2.3.tgz", + "integrity": "sha512-PzE5nJZPz72YUAfo8oTp0u3fqqY7IzlTubneAihqDYAUcBk7ryeCmBbdJBEdaH0bptSOe2VT2Zwcb3UaFyaSWw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-addon-api": "^7.0.0", + "prebuild-install": "^7.1.3" + }, + "engines": { + "node": "^18.12.0 || >= 20.9.0" + } + }, "node_modules/ccount": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", @@ -6167,6 +6899,33 @@ "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", "dev": true }, + "node_modules/chart.js": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz", + "integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@kurkle/color": "^0.3.0" + }, + "engines": { + "pnpm": ">=8" + } + }, + "node_modules/chartjs-node-canvas": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/chartjs-node-canvas/-/chartjs-node-canvas-5.0.0.tgz", + "integrity": "sha512-+Lc5phRWjb+UxAIiQpKgvOaG6Mw276YQx2jl2BrxoUtI3A4RYTZuGM5Dq+s4ReYmCY42WEPSR6viF3lDSTxpvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "canvas": "^3.1.0", + "tslib": "^2.8.1" + }, + "peerDependencies": { + "chart.js": "^4.4.8" + } + }, "node_modules/cheerio": { "version": "1.0.0-rc.11", "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.11.tgz", @@ -6243,6 +7002,13 @@ "node": ">= 6" } }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "dev": true, + "license": "ISC" + }, "node_modules/chrome-trace-event": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", @@ -6259,6 +7025,12 @@ "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", "dev": true }, + "node_modules/classnames": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", + "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==", + "license": "MIT" + }, "node_modules/cli-boxes": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-2.2.1.tgz", @@ -7081,6 +7853,16 @@ "node": ">=6" } }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, "node_modules/devlop": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", @@ -7223,6 +8005,20 @@ "resolved": "https://registry.npmjs.org/double-ended-queue/-/double-ended-queue-2.1.0-0.tgz", "integrity": "sha512-+BNfZ+deCo8hMNpDqDnvT+c0XpJ5cUa6mqYq89bho2Ifze4URTqRkcwR399hWoTrTkbZ/XJYDgP6rc7pRgffEQ==" }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/duplexer3": { "version": "0.1.5", "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.5.tgz", @@ -7389,12 +8185,10 @@ } }, "node_modules/es-define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", - "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", - "dependencies": { - "get-intrinsic": "^1.2.4" - }, + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", "engines": { "node": ">= 0.4" } @@ -7415,9 +8209,10 @@ "peer": true }, "node_modules/es-object-atoms": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz", - "integrity": "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", "dependencies": { "es-errors": "^1.3.0" }, @@ -7462,6 +8257,48 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/esbuild": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.0.tgz", + "integrity": "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.0", + "@esbuild/android-arm": "0.28.0", + "@esbuild/android-arm64": "0.28.0", + "@esbuild/android-x64": "0.28.0", + "@esbuild/darwin-arm64": "0.28.0", + "@esbuild/darwin-x64": "0.28.0", + "@esbuild/freebsd-arm64": "0.28.0", + "@esbuild/freebsd-x64": "0.28.0", + "@esbuild/linux-arm": "0.28.0", + "@esbuild/linux-arm64": "0.28.0", + "@esbuild/linux-ia32": "0.28.0", + "@esbuild/linux-loong64": "0.28.0", + "@esbuild/linux-mips64el": "0.28.0", + "@esbuild/linux-ppc64": "0.28.0", + "@esbuild/linux-riscv64": "0.28.0", + "@esbuild/linux-s390x": "0.28.0", + "@esbuild/linux-x64": "0.28.0", + "@esbuild/netbsd-arm64": "0.28.0", + "@esbuild/netbsd-x64": "0.28.0", + "@esbuild/openbsd-arm64": "0.28.0", + "@esbuild/openbsd-x64": "0.28.0", + "@esbuild/openharmony-arm64": "0.28.0", + "@esbuild/sunos-x64": "0.28.0", + "@esbuild/win32-arm64": "0.28.0", + "@esbuild/win32-ia32": "0.28.0", + "@esbuild/win32-x64": "0.28.0" + } + }, "node_modules/escalade": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", @@ -8106,6 +8943,16 @@ "url": "https://github.com/sindresorhus/execa?sponsor=1" } }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "dev": true, + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, "node_modules/extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", @@ -8135,6 +8982,15 @@ "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.2.0.tgz", "integrity": "sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==" }, + "node_modules/fast-equals": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.4.0.tgz", + "integrity": "sha512-jt2DW/aNFNwke7AUd+Z+e6pz39KO5rzdbbFCg2sGafS4mk13MI7Z8O5z9cADNn5lhGODIgLwug6TZO2ctf7kcw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/fast-glob": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", @@ -8268,6 +9124,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/find-yarn-workspace-root": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/find-yarn-workspace-root/-/find-yarn-workspace-root-2.0.0.tgz", + "integrity": "sha512-1IMnbjt4KzsQfnhnzNd8wUEgXZ44IzZaZmnLYx7D5FZlaHt2gW20Cri8Q+E/t5tIj4+epTBub+2Zxu/vNILzqQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "micromatch": "^4.0.2" + } + }, "node_modules/find-yarn-workspace-root2": { "version": "1.2.16", "resolved": "https://registry.npmjs.org/find-yarn-workspace-root2/-/find-yarn-workspace-root2-1.2.16.tgz", @@ -8472,6 +9338,38 @@ } } }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "dev": true, + "license": "MIT" + }, + "node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/fs-extra/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -8541,15 +9439,21 @@ } }, "node_modules/get-intrinsic": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", - "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "hasown": "^2.0.0" + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -8558,6 +9462,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/get-stream": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", @@ -8601,6 +9518,13 @@ "integrity": "sha512-ltEuT5LdiSLK0dc0S8cOWF97mmsQO81KrajzPpNZmCnynXyn6AJklJNVOpNKwcG6Lq2Vp/d/y7DDTkrRFDW9YQ==", "dev": true }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "dev": true, + "license": "MIT" + }, "node_modules/giturl": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/giturl/-/giturl-1.0.1.tgz", @@ -8769,11 +9693,12 @@ "dev": true }, "node_modules/gopd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", - "dependencies": { - "get-intrinsic": "^1.1.3" + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -8912,9 +9837,10 @@ } }, "node_modules/has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -10002,11 +10928,38 @@ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" }, + "node_modules/json-stable-stringify": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.3.0.tgz", + "integrity": "sha512-qtYiSSFlwot9XHtF9bD9c7rwKjr+RecWT//ZnPvSmEjpV5mmPOCN4j8UjY5hbjNkOwZ/jQv3J6R1/pL7RwgMsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "isarray": "^2.0.5", + "jsonify": "^0.0.1", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==" }, + "node_modules/json-stable-stringify/node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -10018,6 +10971,39 @@ "node": ">=6" } }, + "node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsonfile/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/jsonify": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.1.tgz", + "integrity": "sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg==", + "dev": true, + "license": "Public Domain", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/jsx-ast-utils": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.3.tgz", @@ -10048,6 +11034,16 @@ "node": ">=0.10.0" } }, + "node_modules/klaw-sync": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/klaw-sync/-/klaw-sync-6.0.0.tgz", + "integrity": "sha512-nIeuVSzdCCs6TDPTqI8w1Yre34sSq7AkZ4B3sfOBbI2CgVSB4Du4aLQijFU2+lhAFCwt9+42Hel6lQNIv6AntQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.11" + } + }, "node_modules/language-subtag-registry": { "version": "0.3.22", "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.22.tgz", @@ -10488,6 +11484,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/markdown-table": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz", + "integrity": "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/matchmediaquery": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/matchmediaquery/-/matchmediaquery-0.3.1.tgz", @@ -10496,6 +11502,43 @@ "css-mediaquery": "^0.1.2" } }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mdast-util-find-and-replace": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz", + "integrity": "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "escape-string-regexp": "^5.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-find-and-replace/node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/mdast-util-from-markdown": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.1.tgz", @@ -10519,6 +11562,107 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/mdast-util-gfm": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz", + "integrity": "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==", + "license": "MIT", + "dependencies": { + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-gfm-autolink-literal": "^2.0.0", + "mdast-util-gfm-footnote": "^2.0.0", + "mdast-util-gfm-strikethrough": "^2.0.0", + "mdast-util-gfm-table": "^2.0.0", + "mdast-util-gfm-task-list-item": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-autolink-literal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz", + "integrity": "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "ccount": "^2.0.0", + "devlop": "^1.0.0", + "mdast-util-find-and-replace": "^3.0.0", + "micromark-util-character": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-strikethrough": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz", + "integrity": "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-table": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz", + "integrity": "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "markdown-table": "^3.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-task-list-item": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz", + "integrity": "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/mdast-util-mdx-expression": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz", @@ -10887,6 +12031,127 @@ "micromark-util-types": "^2.0.0" } }, + "node_modules/micromark-extension-gfm": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz", + "integrity": "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==", + "license": "MIT", + "dependencies": { + "micromark-extension-gfm-autolink-literal": "^2.0.0", + "micromark-extension-gfm-footnote": "^2.0.0", + "micromark-extension-gfm-strikethrough": "^2.0.0", + "micromark-extension-gfm-table": "^2.0.0", + "micromark-extension-gfm-tagfilter": "^2.0.0", + "micromark-extension-gfm-task-list-item": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-autolink-literal": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz", + "integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==", + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-strikethrough": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz", + "integrity": "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-table": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz", + "integrity": "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-tagfilter": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz", + "integrity": "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==", + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-task-list-item": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz", + "integrity": "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/micromark-factory-destination": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.0.tgz", @@ -11341,6 +12606,13 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "dev": true, + "license": "MIT" + }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -11408,6 +12680,13 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "dev": true, + "license": "MIT" + }, "node_modules/napi-macros": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/napi-macros/-/napi-macros-2.0.0.tgz", @@ -11516,6 +12795,26 @@ "tslib": "^2.0.3" } }, + "node_modules/node-abi": { + "version": "3.92.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.92.0.tgz", + "integrity": "sha512-KdHvFWZjEKDf0cakgFjebl371GPsISX2oZHcuyKqM7DtogIsHrqKeLTo8wBHxaXRAQlY2PsPlZmfo+9ZCxEREQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "dev": true, + "license": "MIT" + }, "node_modules/node-emoji": { "version": "1.11.0", "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.11.0.tgz", @@ -16300,6 +17599,105 @@ "url": "https://github.com/inikulin/parse5?sponsor=1" } }, + "node_modules/patch-package": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/patch-package/-/patch-package-8.0.1.tgz", + "integrity": "sha512-VsKRIA8f5uqHQ7NGhwIna6Bx6D9s/1iXlA1hthBVBEbkq+t4kXD0HHt+rJhf/Z+Ci0F/HCB2hvn0qLdLG+Qxlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@yarnpkg/lockfile": "^1.1.0", + "chalk": "^4.1.2", + "ci-info": "^3.7.0", + "cross-spawn": "^7.0.3", + "find-yarn-workspace-root": "^2.0.0", + "fs-extra": "^10.0.0", + "json-stable-stringify": "^1.0.2", + "klaw-sync": "^6.0.0", + "minimist": "^1.2.6", + "open": "^7.4.2", + "semver": "^7.5.3", + "slash": "^2.0.0", + "tmp": "^0.2.4", + "yaml": "^2.2.2" + }, + "bin": { + "patch-package": "index.js" + }, + "engines": { + "node": ">=14", + "npm": ">5" + } + }, + "node_modules/patch-package/node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/patch-package/node_modules/open": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-7.4.2.tgz", + "integrity": "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0", + "is-wsl": "^2.1.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/patch-package/node_modules/slash": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", + "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/patch-package/node_modules/tmp": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.7.tgz", + "integrity": "sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.14" + } + }, + "node_modules/patch-package/node_modules/yaml": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -16451,6 +17849,38 @@ "node": ">=10" } }, + "node_modules/playwright": { + "version": "1.60.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.60.0.tgz", + "integrity": "sha512-hheHdokM8cdqCb0lcE3s+zT4t4W+vvjpGxsZlDnikarzx8tSzMebh3UiFtgqwFwnTnjYQcsyMF8ei2mCO/tpeA==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.60.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.60.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.60.0.tgz", + "integrity": "sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/please-upgrade-node": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/please-upgrade-node/-/please-upgrade-node-3.2.0.tgz", @@ -16733,6 +18163,34 @@ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", "integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==" }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/preferred-pm": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/preferred-pm/-/preferred-pm-3.0.3.tgz", @@ -16905,6 +18363,15 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/prosemirror-changeset": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/prosemirror-changeset/-/prosemirror-changeset-2.4.1.tgz", + "integrity": "sha512-96WBLhOaYhJ+kPhLg3uW359Tz6I/MfcrQfL4EGv4SrcqKEMC1gmoGrXHecPE8eOwTVCJ4IwgfzM8fFad25wNfw==", + "license": "MIT", + "dependencies": { + "prosemirror-transform": "^1.0.0" + } + }, "node_modules/prosemirror-codemirror-block": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/prosemirror-codemirror-block/-/prosemirror-codemirror-block-0.4.1.tgz", @@ -16949,13 +18416,14 @@ } }, "node_modules/prosemirror-commands": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/prosemirror-commands/-/prosemirror-commands-1.5.2.tgz", - "integrity": "sha512-hgLcPaakxH8tu6YvVAaILV2tXYsW3rAdDR8WNkeKGcgeMVQg3/TMhPdVoh7iAmfgVjZGtcOSjKiQaoeKjzd2mQ==", + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/prosemirror-commands/-/prosemirror-commands-1.7.1.tgz", + "integrity": "sha512-rT7qZnQtx5c0/y/KlYaGvtG411S97UaL6gdp6RIZ23DLHanMYLyfGBV5DtSnZdthQql7W+lEVbpSfwtO8T+L2w==", + "license": "MIT", "dependencies": { "prosemirror-model": "^1.0.0", "prosemirror-state": "^1.0.0", - "prosemirror-transform": "^1.0.0" + "prosemirror-transform": "^1.10.2" } }, "node_modules/prosemirror-dev-toolkit": { @@ -16969,9 +18437,10 @@ } }, "node_modules/prosemirror-dropcursor": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/prosemirror-dropcursor/-/prosemirror-dropcursor-1.6.1.tgz", - "integrity": "sha512-LtyqQpkIknaT7NnZl3vDr3TpkNcG4ABvGRXx37XJ8tJNUGtcrZBh40A0344rDwlRTfUEmynQS/grUsoSWz+HgA==", + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/prosemirror-dropcursor/-/prosemirror-dropcursor-1.8.2.tgz", + "integrity": "sha512-CCk6Gyx9+Tt2sbYk5NK0nB1ukHi2ryaRgadV/LvyNuO3ena1payM2z6Cg0vO1ebK8cxbzo41ku2DE5Axj1Zuiw==", + "license": "MIT", "dependencies": { "prosemirror-state": "^1.0.0", "prosemirror-transform": "^1.1.0", @@ -16995,9 +18464,10 @@ } }, "node_modules/prosemirror-gapcursor": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/prosemirror-gapcursor/-/prosemirror-gapcursor-1.3.1.tgz", - "integrity": "sha512-GKTeE7ZoMsx5uVfc51/ouwMFPq0o8YrZ7Hx4jTF4EeGbXxBveUV8CGv46mSHuBBeXGmvu50guoV2kSnOeZZnUA==", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/prosemirror-gapcursor/-/prosemirror-gapcursor-1.4.1.tgz", + "integrity": "sha512-pMdYaEnjNMSwl11yjEGtgTmLkR08m/Vl+Jj443167p9eB3HVQKhYCc4gmHVDsLPODfZfjr/MmirsdyZziXbQKw==", + "license": "MIT", "dependencies": { "prosemirror-keymap": "^1.0.0", "prosemirror-model": "^1.0.0", @@ -17006,12 +18476,14 @@ } }, "node_modules/prosemirror-history": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/prosemirror-history/-/prosemirror-history-1.3.0.tgz", - "integrity": "sha512-qo/9Wn4B/Bq89/YD+eNWFbAytu6dmIM85EhID+fz9Jcl9+DfGEo8TTSrRhP15+fFEoaPqpHSxlvSzSEbmlxlUA==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/prosemirror-history/-/prosemirror-history-1.5.0.tgz", + "integrity": "sha512-zlzTiH01eKA55UAf1MEjtssJeHnGxO0j4K4Dpx+gnmX9n+SHNlDqI2oO1Kv1iPN5B1dm5fsljCfqKF9nFL6HRg==", + "license": "MIT", "dependencies": { "prosemirror-state": "^1.2.2", "prosemirror-transform": "^1.0.0", + "prosemirror-view": "^1.31.0", "rope-sequence": "^1.3.0" } }, @@ -17045,18 +18517,20 @@ } }, "node_modules/prosemirror-inputrules": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/prosemirror-inputrules/-/prosemirror-inputrules-1.2.0.tgz", - "integrity": "sha512-eAW/M/NTSSzpCOxfR8Abw6OagdG0MiDAiWHQMQveIsZtoKVYzm0AflSPq/ymqJd56/Su1YPbwy9lM13wgHOFmQ==", + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/prosemirror-inputrules/-/prosemirror-inputrules-1.5.1.tgz", + "integrity": "sha512-7wj4uMjKaXWAQ1CDgxNzNtR9AlsuwzHfdFH1ygEHA2KHF2DOEaXl1CJfNPAKCg9qNEh4rum975QLaCiQPyY6Fw==", + "license": "MIT", "dependencies": { "prosemirror-state": "^1.0.0", "prosemirror-transform": "^1.0.0" } }, "node_modules/prosemirror-keymap": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/prosemirror-keymap/-/prosemirror-keymap-1.2.0.tgz", - "integrity": "sha512-TdSfu+YyLDd54ufN/ZeD1VtBRYpgZnTPnnbY+4R08DDgs84KrIPEPbJL8t1Lm2dkljFx6xeBE26YWH3aIzkPKg==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/prosemirror-keymap/-/prosemirror-keymap-1.2.3.tgz", + "integrity": "sha512-4HucRlpiLd1IPQQXNqeo81BGtkY8Ai5smHhKW9jjPKRc2wQIxksg7Hl1tTI2IfT2B/LgX6bfYvXxEpJl7aKYKw==", + "license": "MIT", "dependencies": { "prosemirror-state": "^1.0.0", "w3c-keyname": "^2.2.0" @@ -17122,9 +18596,10 @@ } }, "node_modules/prosemirror-model": { - "version": "1.19.2", - "resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.19.2.tgz", - "integrity": "sha512-RXl0Waiss4YtJAUY3NzKH0xkJmsZupCIccqcIFoLTIKFlKNbIvFDRl27/kQy1FP8iUAxrjRRfIVvOebnnXJgqQ==", + "version": "1.25.7", + "resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.25.7.tgz", + "integrity": "sha512-A79aN8QEFUwI6cax8Yq4Rpcx1TJZ3Kagn+ii7qLo4/V8H3mMiHrhFyhTyHHvpSnOgMPpWiDGSwM3etwrxE50ug==", + "license": "MIT", "dependencies": { "orderedmap": "^2.0.0" } @@ -17138,13 +18613,14 @@ } }, "node_modules/prosemirror-schema-list": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/prosemirror-schema-list/-/prosemirror-schema-list-1.2.2.tgz", - "integrity": "sha512-rd0pqSDp86p0MUMKG903g3I9VmElFkQpkZ2iOd3EOVg1vo5Cst51rAsoE+5IPy0LPXq64eGcCYlW1+JPNxOj2w==", + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/prosemirror-schema-list/-/prosemirror-schema-list-1.5.1.tgz", + "integrity": "sha512-927lFx/uwyQaGwJxLWCZRkjXG0p48KpMj6ueoYiu4JX05GGuGcgzAy62dfiV8eFZftgyBUvLx76RsMe20fJl+Q==", + "license": "MIT", "dependencies": { "prosemirror-model": "^1.0.0", "prosemirror-state": "^1.0.0", - "prosemirror-transform": "^1.0.0" + "prosemirror-transform": "^1.7.3" } }, "node_modules/prosemirror-slash-menu": { @@ -17183,9 +18659,10 @@ } }, "node_modules/prosemirror-state": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/prosemirror-state/-/prosemirror-state-1.4.3.tgz", - "integrity": "sha512-goFKORVbvPuAQaXhpbemJFRKJ2aixr+AZMGiquiqKxaucC6hlpHNZHWgz5R7dS4roHiwq9vDctE//CZ++o0W1Q==", + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/prosemirror-state/-/prosemirror-state-1.4.4.tgz", + "integrity": "sha512-6jiYHH2CIGbCfnxdHbXZ12gySFY/fz/ulZE333G6bPqIZ4F+TXo9ifiR86nAHpWnfoNjOb3o5ESi7J8Uz1jXHw==", + "license": "MIT", "dependencies": { "prosemirror-model": "^1.0.0", "prosemirror-transform": "^1.0.0", @@ -17193,31 +18670,34 @@ } }, "node_modules/prosemirror-tables": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/prosemirror-tables/-/prosemirror-tables-1.3.0.tgz", - "integrity": "sha512-ujzOb37O2ahmqI626Y0N0V/SZxuA9OGNYnsIMWdfecwkc8S8OShOqeD4kKUxpD0JcP81Z8qy/ulrXQuKhS4WUg==", + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/prosemirror-tables/-/prosemirror-tables-1.8.5.tgz", + "integrity": "sha512-V/0cDCsHKHe/tfWkeCmthNUcEp1IVO3p6vwN8XtwE9PZQLAZJigbw3QoraAdfJPir4NKJtNvOB8oYGKRl+t0Dw==", + "license": "MIT", "dependencies": { - "prosemirror-keymap": "^1.1.2", - "prosemirror-model": "^1.8.1", - "prosemirror-state": "^1.3.1", - "prosemirror-transform": "^1.2.1", - "prosemirror-view": "^1.13.3" + "prosemirror-keymap": "^1.2.3", + "prosemirror-model": "^1.25.4", + "prosemirror-state": "^1.4.4", + "prosemirror-transform": "^1.10.5", + "prosemirror-view": "^1.41.4" } }, "node_modules/prosemirror-transform": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/prosemirror-transform/-/prosemirror-transform-1.7.0.tgz", - "integrity": "sha512-O4T697Cqilw06Zvc3Wm+e237R6eZtJL/xGMliCi+Uo8VL6qHk6afz1qq0zNjT3eZMuYwnP8ZS0+YxX/tfcE9TQ==", + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/prosemirror-transform/-/prosemirror-transform-1.12.0.tgz", + "integrity": "sha512-GxboyN4AMIsoHNtz5uf2r2Ru551i5hWeCMD6E2Ib4Eogqoub0NflniaBPVQ4MrGE5yZ8JV9tUHg9qcZTTrcN4w==", + "license": "MIT", "dependencies": { - "prosemirror-model": "^1.0.0" + "prosemirror-model": "^1.21.0" } }, "node_modules/prosemirror-view": { - "version": "1.31.5", - "resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.31.5.tgz", - "integrity": "sha512-tobRCDeCp61elR1d97XE/JTL9FDIfswZpWeNs7GKJjAJvWyMGHWYFCq29850p6bbG2bckP+i9n1vT56RifosbA==", + "version": "1.41.7", + "resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.41.7.tgz", + "integrity": "sha512-jUwKNCEIGiqdvhlS91/2QAg21e4dfU5bH2iwmSDQeosXJgKF7smG0YSplOWK0cjSNgIqXe7VXqo7EIfUFJdt3w==", + "license": "MIT", "dependencies": { - "prosemirror-model": "^1.16.0", + "prosemirror-model": "^1.20.0", "prosemirror-state": "^1.0.0", "prosemirror-transform": "^1.1.0" } @@ -17499,6 +18979,22 @@ "resolved": "https://registry.npmjs.org/react-recaptcha/-/react-recaptcha-2.3.10.tgz", "integrity": "sha512-IyanbozsYCuHvTYDuskZTIEcRAMG/sdvAu5b29iQWoC8Kd3Zk9WGCv2oNxh6RfGHvSvgHAyaLjmC6ei/yMsJ7g==" }, + "node_modules/react-reconciler": { + "version": "0.29.2", + "resolved": "https://registry.npmjs.org/react-reconciler/-/react-reconciler-0.29.2.tgz", + "integrity": "sha512-zZQqIiYgDCTP/f1N/mAR10nJGrPD2ZR+jDSEsKWJHYC7Cm2wodlwbR3upZRdC3cjIjSlTLNVyO7Iu0Yy7t2AYg==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "engines": { + "node": ">=0.10.0" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, "node_modules/react-responsive": { "version": "9.0.2", "resolved": "https://registry.npmjs.org/react-responsive/-/react-responsive-9.0.2.tgz", @@ -17900,6 +19396,24 @@ "jsesc": "bin/jsesc" } }, + "node_modules/remark-gfm": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz", + "integrity": "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-gfm": "^3.0.0", + "micromark-extension-gfm": "^3.0.0", + "remark-parse": "^11.0.0", + "remark-stringify": "^11.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/remark-parse": { "version": "11.0.0", "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", @@ -17939,6 +19453,21 @@ "@types/unist": "*" } }, + "node_modules/remark-stringify": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz", + "integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-to-markdown": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -18364,6 +19893,82 @@ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "dev": true }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/simple-get/node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/simple-get/node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/sister": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/sister/-/sister-3.0.2.tgz", @@ -19046,6 +20651,36 @@ "node": ">=6" } }, + "node_modules/tar-fs": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/terser": { "version": "5.18.1", "resolved": "https://registry.npmjs.org/terser/-/terser-5.18.1.tgz", @@ -19320,9 +20955,10 @@ } }, "node_modules/tslib": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz", - "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==" + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" }, "node_modules/tsutils": { "version": "3.21.0", @@ -19343,6 +20979,53 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" }, + "node_modules/tsx": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.22.4.tgz", + "integrity": "sha512-X8EX+XV4QR5xCsrgxaED954zTDfY8KqlDtskKEL0cHhyS/P8b4IFOvGDQpsC9Q1XnLq915wEfwwY/zzskCtmhg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.28.0" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/tsx/node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -22129,6 +23812,188 @@ "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.3.0.tgz", "integrity": "sha512-AHPmaAx+RYfZz0eYu6Gviiagpmiyw98ySSlQvCUhVGDRtDFe4DBS0x1bSjdF3gqUDYOczB+yYvBTtEylYSdRhg==" }, + "@esbuild/aix-ppc64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.0.tgz", + "integrity": "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==", + "dev": true, + "optional": true + }, + "@esbuild/android-arm": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.0.tgz", + "integrity": "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==", + "dev": true, + "optional": true + }, + "@esbuild/android-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.0.tgz", + "integrity": "sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==", + "dev": true, + "optional": true + }, + "@esbuild/android-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.0.tgz", + "integrity": "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==", + "dev": true, + "optional": true + }, + "@esbuild/darwin-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.0.tgz", + "integrity": "sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==", + "dev": true, + "optional": true + }, + "@esbuild/darwin-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.0.tgz", + "integrity": "sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==", + "dev": true, + "optional": true + }, + "@esbuild/freebsd-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.0.tgz", + "integrity": "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==", + "dev": true, + "optional": true + }, + "@esbuild/freebsd-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.0.tgz", + "integrity": "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==", + "dev": true, + "optional": true + }, + "@esbuild/linux-arm": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.0.tgz", + "integrity": "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==", + "dev": true, + "optional": true + }, + "@esbuild/linux-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.0.tgz", + "integrity": "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==", + "dev": true, + "optional": true + }, + "@esbuild/linux-ia32": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.0.tgz", + "integrity": "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==", + "dev": true, + "optional": true + }, + "@esbuild/linux-loong64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.0.tgz", + "integrity": "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==", + "dev": true, + "optional": true + }, + "@esbuild/linux-mips64el": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.0.tgz", + "integrity": "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==", + "dev": true, + "optional": true + }, + "@esbuild/linux-ppc64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.0.tgz", + "integrity": "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==", + "dev": true, + "optional": true + }, + "@esbuild/linux-riscv64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.0.tgz", + "integrity": "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==", + "dev": true, + "optional": true + }, + "@esbuild/linux-s390x": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.0.tgz", + "integrity": "sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==", + "dev": true, + "optional": true + }, + "@esbuild/linux-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.0.tgz", + "integrity": "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==", + "dev": true, + "optional": true + }, + "@esbuild/netbsd-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.0.tgz", + "integrity": "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==", + "dev": true, + "optional": true + }, + "@esbuild/netbsd-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.0.tgz", + "integrity": "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==", + "dev": true, + "optional": true + }, + "@esbuild/openbsd-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.0.tgz", + "integrity": "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==", + "dev": true, + "optional": true + }, + "@esbuild/openbsd-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.0.tgz", + "integrity": "sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==", + "dev": true, + "optional": true + }, + "@esbuild/openharmony-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.0.tgz", + "integrity": "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==", + "dev": true, + "optional": true + }, + "@esbuild/sunos-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.0.tgz", + "integrity": "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==", + "dev": true, + "optional": true + }, + "@esbuild/win32-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.0.tgz", + "integrity": "sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==", + "dev": true, + "optional": true + }, + "@esbuild/win32-ia32": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.0.tgz", + "integrity": "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==", + "dev": true, + "optional": true + }, + "@esbuild/win32-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.0.tgz", + "integrity": "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==", + "dev": true, + "optional": true + }, "@eslint/eslintrc": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.3.3.tgz", @@ -22175,6 +24040,14 @@ "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.1.6.tgz", "integrity": "sha512-OfX7E2oUDYxtBvsuS4e/jSn4Q9Qb6DzgeYtsAdkPZ47znpoNsMgZw0+tVijiv3uGNR6dgNlty6r9rzIzHjtd/A==" }, + "@handlewithcare/react-prosemirror": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@handlewithcare/react-prosemirror/-/react-prosemirror-3.0.6.tgz", + "integrity": "sha512-7HNwItwpi1dZ0lq2KvqCvGhZaP3QXDFQOcyap3mNb9HyFPYXWdTQm1XQEGQ1sZ5656OOwAzzYU/q7ncRvIJ45w==", + "requires": { + "classnames": "^2.5.1" + } + }, "@humanwhocodes/config-array": { "version": "0.11.7", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.7.tgz", @@ -22311,6 +24184,12 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "@kurkle/color": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz", + "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==", + "dev": true + }, "@lexical/clipboard": { "version": "0.11.3", "resolved": "https://registry.npmjs.org/@lexical/clipboard/-/clipboard-0.11.3.tgz", @@ -22865,6 +24744,15 @@ "tslib": "^2.4.0" } }, + "@playwright/test": { + "version": "1.60.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.60.0.tgz", + "integrity": "sha512-O71yZIbAh/PxDMNGns37GHBIfrVkEVyn+AXyIa5dOTfb4/xNvRWV+Vv/NMbNCtODB/pO7vLlF2OTmMVLhmr7Ag==", + "devOptional": true, + "requires": { + "playwright": "1.60.0" + } + }, "@popperjs/core": { "version": "2.11.8", "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", @@ -23091,6 +24979,99 @@ "zustand": "^4.1.4" } }, + "@tiptap/core": { + "version": "3.24.0", + "resolved": "https://registry.npmjs.org/@tiptap/core/-/core-3.24.0.tgz", + "integrity": "sha512-GTAsXAI32p4hEZgPzvUv2RPrObxamy9AFhmhG10fXSvN/cDUs8naEYVIqDV3Sh99jMwQEbTFKW1E1mcspsY6ow==", + "requires": {} + }, + "@tiptap/extension-bubble-menu": { + "version": "3.24.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-bubble-menu/-/extension-bubble-menu-3.24.0.tgz", + "integrity": "sha512-jRXD+JPu9ayvq78g8hsCxx4q/qUFtrdfIYirRSf5YUseuuUbtfrq83AsGabcygpUTefjJkMQoXNITkh6294Ggw==", + "optional": true, + "requires": { + "@floating-ui/dom": "^1.0.0" + } + }, + "@tiptap/extension-document": { + "version": "3.24.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-document/-/extension-document-3.24.0.tgz", + "integrity": "sha512-yxgM3+yXy2XZzEwH43y2Kp8D1BkblxEWLXqo0YCoAKtxyKCcEaT8kdlf70kS7D0+VSzYU4D0iN7VdQIYHcL2mA==", + "requires": {} + }, + "@tiptap/extension-floating-menu": { + "version": "3.24.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-floating-menu/-/extension-floating-menu-3.24.0.tgz", + "integrity": "sha512-7QEbf3mUzFAkejjQGX9f0L507oMtnOBRwHt2skUTR+9yXgudsN8zaDBSSRHLeMWGk9b7L293ZMA6zCRrZaHrfA==", + "optional": true, + "requires": {} + }, + "@tiptap/extension-history": { + "version": "3.24.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-history/-/extension-history-3.24.0.tgz", + "integrity": "sha512-pGRSXgJ+f3KIJq8kNq9mGJAN0ryJEZgBQq5Ag7Lk9Bo7OE+0SSYEs2gCEmY8+Thtw/YpICaVzx06pFy9DZt3eQ==", + "requires": {} + }, + "@tiptap/extension-paragraph": { + "version": "3.24.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-paragraph/-/extension-paragraph-3.24.0.tgz", + "integrity": "sha512-wD06aB6hO7LgcrlhGiw7I64k2tus9kNoICX5R+UecBSB1DVJdzKvXoXL2kPNv4DqYvljHdkIeK/OpuOTQd6MJA==", + "requires": {} + }, + "@tiptap/extension-text": { + "version": "3.24.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-text/-/extension-text-3.24.0.tgz", + "integrity": "sha512-Im7keLPEihxm3+LyF+drYCoaOY5hlq35lvHAp/el6M8pJ/scts88HrYpdR1Yc4BtpZBIhfHSyWgPaupI4qwdeg==", + "requires": {} + }, + "@tiptap/extensions": { + "version": "3.24.0", + "resolved": "https://registry.npmjs.org/@tiptap/extensions/-/extensions-3.24.0.tgz", + "integrity": "sha512-z6gRYzy2ucJp07OQ0F2W07NxyhMTxPYH1ia2eGiQkWax1i56oExpjMsDHP8THWlg8Tb7NnbfKpkfh881EsmofA==", + "peer": true, + "requires": {} + }, + "@tiptap/pm": { + "version": "3.24.0", + "resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-3.24.0.tgz", + "integrity": "sha512-QQP/78ryOZDN99gNBV7dgh69/8AYaOYQYFklq/iR+ZRFaaL3+qqHFvPVJapGkzPdymBgNJ34xjFM8n5pJ4QmMg==", + "requires": { + "prosemirror-changeset": "^2.3.0", + "prosemirror-commands": "^1.6.2", + "prosemirror-dropcursor": "^1.8.1", + "prosemirror-gapcursor": "^1.3.2", + "prosemirror-history": "^1.4.1", + "prosemirror-inputrules": "^1.4.0", + "prosemirror-keymap": "^1.2.2", + "prosemirror-model": "^1.24.1", + "prosemirror-schema-list": "^1.5.0", + "prosemirror-state": "^1.4.3", + "prosemirror-tables": "^1.6.4", + "prosemirror-transform": "^1.10.2", + "prosemirror-view": "^1.38.1" + } + }, + "@tiptap/react": { + "version": "3.24.0", + "resolved": "https://registry.npmjs.org/@tiptap/react/-/react-3.24.0.tgz", + "integrity": "sha512-KxnrlQbzOgA02EMsfuGGHtNhfkJQGqVlQttmQctI9DOl/F3gcaRqg+wNTBY1Fof8yDaZ8Z1LL1F0C05W0o3vUw==", + "requires": { + "@tiptap/extension-bubble-menu": "^3.24.0", + "@tiptap/extension-floating-menu": "^3.24.0", + "@types/use-sync-external-store": "^0.0.6", + "fast-equals": "^5.3.3", + "use-sync-external-store": "^1.4.0" + }, + "dependencies": { + "use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "requires": {} + } + } + }, "@trysound/sax": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz", @@ -23556,7 +25537,6 @@ "version": "18.2.6", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.6.tgz", "integrity": "sha512-2et4PDvg6PVCyS7fuTc4gPoksV58bW0RwSxWKcPRcHZf0PRUGq03TKcD/rUHe3azfV6/5/biUBJw+HhCQjaP0A==", - "dev": true, "requires": { "@types/react": "*" } @@ -23651,6 +25631,11 @@ "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==" }, + "@types/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==" + }, "@typescript-eslint/eslint-plugin": { "version": "5.46.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.46.1.tgz", @@ -24006,6 +25991,12 @@ "optional": true, "peer": true }, + "@yarnpkg/lockfile": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz", + "integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==", + "dev": true + }, "abort-controller": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", @@ -24531,15 +26522,33 @@ } }, "call-bind": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", - "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", + "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==", + "requires": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "get-intrinsic": "^1.3.0", + "set-function-length": "^1.2.2" + } + }, + "call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", "requires": { - "es-define-property": "^1.0.0", "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "set-function-length": "^1.2.1" + "function-bind": "^1.1.2" + } + }, + "call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "requires": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" } }, "callsite": { @@ -24668,6 +26677,16 @@ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001651.tgz", "integrity": "sha512-9Cf+Xv1jJNe1xPZLGuUXLNkE1BoDkqRqYyFJ9TDYSqhduqA4hu4oR9HluGoWYQC/aj8WHjsGVV+bwkh0+tegRg==" }, + "canvas": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/canvas/-/canvas-3.2.3.tgz", + "integrity": "sha512-PzE5nJZPz72YUAfo8oTp0u3fqqY7IzlTubneAihqDYAUcBk7ryeCmBbdJBEdaH0bptSOe2VT2Zwcb3UaFyaSWw==", + "dev": true, + "requires": { + "node-addon-api": "^7.0.0", + "prebuild-install": "^7.1.3" + } + }, "ccount": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", @@ -24708,6 +26727,25 @@ "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", "dev": true }, + "chart.js": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz", + "integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==", + "dev": true, + "requires": { + "@kurkle/color": "^0.3.0" + } + }, + "chartjs-node-canvas": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/chartjs-node-canvas/-/chartjs-node-canvas-5.0.0.tgz", + "integrity": "sha512-+Lc5phRWjb+UxAIiQpKgvOaG6Mw276YQx2jl2BrxoUtI3A4RYTZuGM5Dq+s4ReYmCY42WEPSR6viF3lDSTxpvw==", + "dev": true, + "requires": { + "canvas": "^3.1.0", + "tslib": "^2.8.1" + } + }, "cheerio": { "version": "1.0.0-rc.11", "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.11.tgz", @@ -24763,6 +26801,12 @@ } } }, + "chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "dev": true + }, "chrome-trace-event": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", @@ -24776,6 +26820,11 @@ "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", "dev": true }, + "classnames": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", + "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==" + }, "cli-boxes": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-2.2.1.tgz", @@ -25400,6 +27449,12 @@ "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==" }, + "detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true + }, "devlop": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", @@ -25508,6 +27563,16 @@ "resolved": "https://registry.npmjs.org/double-ended-queue/-/double-ended-queue-2.1.0-0.tgz", "integrity": "sha512-+BNfZ+deCo8hMNpDqDnvT+c0XpJ5cUa6mqYq89bho2Ifze4URTqRkcwR399hWoTrTkbZ/XJYDgP6rc7pRgffEQ==" }, + "dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "requires": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + } + }, "duplexer3": { "version": "0.1.5", "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.5.tgz", @@ -25653,12 +27718,9 @@ } }, "es-define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", - "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", - "requires": { - "get-intrinsic": "^1.2.4" - } + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==" }, "es-errors": { "version": "1.3.0", @@ -25673,9 +27735,9 @@ "peer": true }, "es-object-atoms": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz", - "integrity": "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", "requires": { "es-errors": "^1.3.0" } @@ -25708,6 +27770,40 @@ "is-symbol": "^1.0.2" } }, + "esbuild": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.0.tgz", + "integrity": "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==", + "dev": true, + "requires": { + "@esbuild/aix-ppc64": "0.28.0", + "@esbuild/android-arm": "0.28.0", + "@esbuild/android-arm64": "0.28.0", + "@esbuild/android-x64": "0.28.0", + "@esbuild/darwin-arm64": "0.28.0", + "@esbuild/darwin-x64": "0.28.0", + "@esbuild/freebsd-arm64": "0.28.0", + "@esbuild/freebsd-x64": "0.28.0", + "@esbuild/linux-arm": "0.28.0", + "@esbuild/linux-arm64": "0.28.0", + "@esbuild/linux-ia32": "0.28.0", + "@esbuild/linux-loong64": "0.28.0", + "@esbuild/linux-mips64el": "0.28.0", + "@esbuild/linux-ppc64": "0.28.0", + "@esbuild/linux-riscv64": "0.28.0", + "@esbuild/linux-s390x": "0.28.0", + "@esbuild/linux-x64": "0.28.0", + "@esbuild/netbsd-arm64": "0.28.0", + "@esbuild/netbsd-x64": "0.28.0", + "@esbuild/openbsd-arm64": "0.28.0", + "@esbuild/openbsd-x64": "0.28.0", + "@esbuild/openharmony-arm64": "0.28.0", + "@esbuild/sunos-x64": "0.28.0", + "@esbuild/win32-arm64": "0.28.0", + "@esbuild/win32-ia32": "0.28.0", + "@esbuild/win32-x64": "0.28.0" + } + }, "escalade": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", @@ -26153,6 +28249,12 @@ "strip-final-newline": "^2.0.0" } }, + "expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "dev": true + }, "extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", @@ -26179,6 +28281,11 @@ "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.2.0.tgz", "integrity": "sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==" }, + "fast-equals": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.4.0.tgz", + "integrity": "sha512-jt2DW/aNFNwke7AUd+Z+e6pz39KO5rzdbbFCg2sGafS4mk13MI7Z8O5z9cADNn5lhGODIgLwug6TZO2ctf7kcw==" + }, "fast-glob": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", @@ -26282,6 +28389,15 @@ "path-exists": "^4.0.0" } }, + "find-yarn-workspace-root": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/find-yarn-workspace-root/-/find-yarn-workspace-root-2.0.0.tgz", + "integrity": "sha512-1IMnbjt4KzsQfnhnzNd8wUEgXZ44IzZaZmnLYx7D5FZlaHt2gW20Cri8Q+E/t5tIj4+epTBub+2Zxu/vNILzqQ==", + "dev": true, + "requires": { + "micromatch": "^4.0.2" + } + }, "find-yarn-workspace-root2": { "version": "1.2.16", "resolved": "https://registry.npmjs.org/find-yarn-workspace-root2/-/find-yarn-workspace-root2-1.2.16.tgz", @@ -26414,6 +28530,31 @@ "tslib": "^2.4.0" } }, + "fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "dev": true + }, + "fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "dependencies": { + "universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true + } + } + }, "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -26458,15 +28599,29 @@ "dev": true }, "get-intrinsic": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", - "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "requires": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "hasown": "^2.0.0" + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + } + }, + "get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "requires": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" } }, "get-stream": { @@ -26497,6 +28652,12 @@ "integrity": "sha512-ltEuT5LdiSLK0dc0S8cOWF97mmsQO81KrajzPpNZmCnynXyn6AJklJNVOpNKwcG6Lq2Vp/d/y7DDTkrRFDW9YQ==", "dev": true }, + "github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "dev": true + }, "giturl": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/giturl/-/giturl-1.0.1.tgz", @@ -26621,12 +28782,9 @@ "dev": true }, "gopd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", - "requires": { - "get-intrinsic": "^1.1.3" - } + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==" }, "got": { "version": "9.6.0", @@ -26729,9 +28887,9 @@ "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==" }, "has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==" + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==" }, "has-tostringtag": { "version": "1.0.2", @@ -27495,6 +29653,27 @@ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" }, + "json-stable-stringify": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.3.0.tgz", + "integrity": "sha512-qtYiSSFlwot9XHtF9bD9c7rwKjr+RecWT//ZnPvSmEjpV5mmPOCN4j8UjY5hbjNkOwZ/jQv3J6R1/pL7RwgMsg==", + "dev": true, + "requires": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "isarray": "^2.0.5", + "jsonify": "^0.0.1", + "object-keys": "^1.1.1" + }, + "dependencies": { + "isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true + } + } + }, "json-stable-stringify-without-jsonify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", @@ -27505,6 +29684,30 @@ "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==" }, + "jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.6", + "universalify": "^2.0.0" + }, + "dependencies": { + "universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true + } + } + }, + "jsonify": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.1.tgz", + "integrity": "sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg==", + "dev": true + }, "jsx-ast-utils": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.3.tgz", @@ -27529,6 +29732,15 @@ "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", "dev": true }, + "klaw-sync": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/klaw-sync/-/klaw-sync-6.0.0.tgz", + "integrity": "sha512-nIeuVSzdCCs6TDPTqI8w1Yre34sSq7AkZ4B3sfOBbI2CgVSB4Du4aLQijFU2+lhAFCwt9+42Hel6lQNIv6AntQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.11" + } + }, "language-subtag-registry": { "version": "0.3.22", "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.22.tgz", @@ -27868,6 +30080,11 @@ "integrity": "sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==", "dev": true }, + "markdown-table": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz", + "integrity": "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==" + }, "matchmediaquery": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/matchmediaquery/-/matchmediaquery-0.3.1.tgz", @@ -27876,6 +30093,29 @@ "css-mediaquery": "^0.1.2" } }, + "math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==" + }, + "mdast-util-find-and-replace": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz", + "integrity": "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==", + "requires": { + "@types/mdast": "^4.0.0", + "escape-string-regexp": "^5.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "dependencies": { + "escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==" + } + } + }, "mdast-util-from-markdown": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.1.tgz", @@ -27895,6 +30135,77 @@ "unist-util-stringify-position": "^4.0.0" } }, + "mdast-util-gfm": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz", + "integrity": "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==", + "requires": { + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-gfm-autolink-literal": "^2.0.0", + "mdast-util-gfm-footnote": "^2.0.0", + "mdast-util-gfm-strikethrough": "^2.0.0", + "mdast-util-gfm-table": "^2.0.0", + "mdast-util-gfm-task-list-item": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + } + }, + "mdast-util-gfm-autolink-literal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz", + "integrity": "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==", + "requires": { + "@types/mdast": "^4.0.0", + "ccount": "^2.0.0", + "devlop": "^1.0.0", + "mdast-util-find-and-replace": "^3.0.0", + "micromark-util-character": "^2.0.0" + } + }, + "mdast-util-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==", + "requires": { + "@types/mdast": "^4.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0" + } + }, + "mdast-util-gfm-strikethrough": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz", + "integrity": "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==", + "requires": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + } + }, + "mdast-util-gfm-table": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz", + "integrity": "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==", + "requires": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "markdown-table": "^3.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + } + }, + "mdast-util-gfm-task-list-item": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz", + "integrity": "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==", + "requires": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + } + }, "mdast-util-mdx-expression": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz", @@ -28180,6 +30491,92 @@ "micromark-util-types": "^2.0.0" } }, + "micromark-extension-gfm": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz", + "integrity": "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==", + "requires": { + "micromark-extension-gfm-autolink-literal": "^2.0.0", + "micromark-extension-gfm-footnote": "^2.0.0", + "micromark-extension-gfm-strikethrough": "^2.0.0", + "micromark-extension-gfm-table": "^2.0.0", + "micromark-extension-gfm-tagfilter": "^2.0.0", + "micromark-extension-gfm-task-list-item": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "micromark-extension-gfm-autolink-literal": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz", + "integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==", + "requires": { + "micromark-util-character": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "micromark-extension-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==", + "requires": { + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "micromark-extension-gfm-strikethrough": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz", + "integrity": "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==", + "requires": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "micromark-extension-gfm-table": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz", + "integrity": "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==", + "requires": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "micromark-extension-gfm-tagfilter": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz", + "integrity": "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==", + "requires": { + "micromark-util-types": "^2.0.0" + } + }, + "micromark-extension-gfm-task-list-item": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz", + "integrity": "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==", + "requires": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, "micromark-factory-destination": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.0.tgz", @@ -28414,6 +30811,12 @@ "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", "dev": true }, + "mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "dev": true + }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -28462,6 +30865,12 @@ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==" }, + "napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "dev": true + }, "napi-macros": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/napi-macros/-/napi-macros-2.0.0.tgz", @@ -28529,6 +30938,21 @@ "tslib": "^2.0.3" } }, + "node-abi": { + "version": "3.92.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.92.0.tgz", + "integrity": "sha512-KdHvFWZjEKDf0cakgFjebl371GPsISX2oZHcuyKqM7DtogIsHrqKeLTo8wBHxaXRAQlY2PsPlZmfo+9ZCxEREQ==", + "dev": true, + "requires": { + "semver": "^7.3.5" + } + }, + "node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "dev": true + }, "node-emoji": { "version": "1.11.0", "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.11.0.tgz", @@ -32045,6 +34469,64 @@ "parse5": "^7.0.0" } }, + "patch-package": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/patch-package/-/patch-package-8.0.1.tgz", + "integrity": "sha512-VsKRIA8f5uqHQ7NGhwIna6Bx6D9s/1iXlA1hthBVBEbkq+t4kXD0HHt+rJhf/Z+Ci0F/HCB2hvn0qLdLG+Qxlw==", + "dev": true, + "requires": { + "@yarnpkg/lockfile": "^1.1.0", + "chalk": "^4.1.2", + "ci-info": "^3.7.0", + "cross-spawn": "^7.0.3", + "find-yarn-workspace-root": "^2.0.0", + "fs-extra": "^10.0.0", + "json-stable-stringify": "^1.0.2", + "klaw-sync": "^6.0.0", + "minimist": "^1.2.6", + "open": "^7.4.2", + "semver": "^7.5.3", + "slash": "^2.0.0", + "tmp": "^0.2.4", + "yaml": "^2.2.2" + }, + "dependencies": { + "ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true + }, + "open": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-7.4.2.tgz", + "integrity": "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==", + "dev": true, + "requires": { + "is-docker": "^2.0.0", + "is-wsl": "^2.1.1" + } + }, + "slash": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", + "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==", + "dev": true + }, + "tmp": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.7.tgz", + "integrity": "sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==", + "dev": true + }, + "yaml": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", + "dev": true + } + } + }, "path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -32161,6 +34643,22 @@ "find-up": "^5.0.0" } }, + "playwright": { + "version": "1.60.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.60.0.tgz", + "integrity": "sha512-hheHdokM8cdqCb0lcE3s+zT4t4W+vvjpGxsZlDnikarzx8tSzMebh3UiFtgqwFwnTnjYQcsyMF8ei2mCO/tpeA==", + "devOptional": true, + "requires": { + "fsevents": "2.3.2", + "playwright-core": "1.60.0" + } + }, + "playwright-core": { + "version": "1.60.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.60.0.tgz", + "integrity": "sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA==", + "devOptional": true + }, "please-upgrade-node": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/please-upgrade-node/-/please-upgrade-node-3.2.0.tgz", @@ -32332,6 +34830,26 @@ } } }, + "prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "dev": true, + "requires": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + } + }, "preferred-pm": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/preferred-pm/-/preferred-pm-3.0.3.tgz", @@ -32407,6 +34925,14 @@ "resolved": "https://registry.npmjs.org/property-information/-/property-information-6.5.0.tgz", "integrity": "sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==" }, + "prosemirror-changeset": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/prosemirror-changeset/-/prosemirror-changeset-2.4.1.tgz", + "integrity": "sha512-96WBLhOaYhJ+kPhLg3uW359Tz6I/MfcrQfL4EGv4SrcqKEMC1gmoGrXHecPE8eOwTVCJ4IwgfzM8fFad25wNfw==", + "requires": { + "prosemirror-transform": "^1.0.0" + } + }, "prosemirror-codemirror-block": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/prosemirror-codemirror-block/-/prosemirror-codemirror-block-0.4.1.tgz", @@ -32444,13 +34970,13 @@ } }, "prosemirror-commands": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/prosemirror-commands/-/prosemirror-commands-1.5.2.tgz", - "integrity": "sha512-hgLcPaakxH8tu6YvVAaILV2tXYsW3rAdDR8WNkeKGcgeMVQg3/TMhPdVoh7iAmfgVjZGtcOSjKiQaoeKjzd2mQ==", + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/prosemirror-commands/-/prosemirror-commands-1.7.1.tgz", + "integrity": "sha512-rT7qZnQtx5c0/y/KlYaGvtG411S97UaL6gdp6RIZ23DLHanMYLyfGBV5DtSnZdthQql7W+lEVbpSfwtO8T+L2w==", "requires": { "prosemirror-model": "^1.0.0", "prosemirror-state": "^1.0.0", - "prosemirror-transform": "^1.0.0" + "prosemirror-transform": "^1.10.2" } }, "prosemirror-dev-toolkit": { @@ -32464,9 +34990,9 @@ } }, "prosemirror-dropcursor": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/prosemirror-dropcursor/-/prosemirror-dropcursor-1.6.1.tgz", - "integrity": "sha512-LtyqQpkIknaT7NnZl3vDr3TpkNcG4ABvGRXx37XJ8tJNUGtcrZBh40A0344rDwlRTfUEmynQS/grUsoSWz+HgA==", + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/prosemirror-dropcursor/-/prosemirror-dropcursor-1.8.2.tgz", + "integrity": "sha512-CCk6Gyx9+Tt2sbYk5NK0nB1ukHi2ryaRgadV/LvyNuO3ena1payM2z6Cg0vO1ebK8cxbzo41ku2DE5Axj1Zuiw==", "requires": { "prosemirror-state": "^1.0.0", "prosemirror-transform": "^1.1.0", @@ -32490,9 +35016,9 @@ } }, "prosemirror-gapcursor": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/prosemirror-gapcursor/-/prosemirror-gapcursor-1.3.1.tgz", - "integrity": "sha512-GKTeE7ZoMsx5uVfc51/ouwMFPq0o8YrZ7Hx4jTF4EeGbXxBveUV8CGv46mSHuBBeXGmvu50guoV2kSnOeZZnUA==", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/prosemirror-gapcursor/-/prosemirror-gapcursor-1.4.1.tgz", + "integrity": "sha512-pMdYaEnjNMSwl11yjEGtgTmLkR08m/Vl+Jj443167p9eB3HVQKhYCc4gmHVDsLPODfZfjr/MmirsdyZziXbQKw==", "requires": { "prosemirror-keymap": "^1.0.0", "prosemirror-model": "^1.0.0", @@ -32501,12 +35027,13 @@ } }, "prosemirror-history": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/prosemirror-history/-/prosemirror-history-1.3.0.tgz", - "integrity": "sha512-qo/9Wn4B/Bq89/YD+eNWFbAytu6dmIM85EhID+fz9Jcl9+DfGEo8TTSrRhP15+fFEoaPqpHSxlvSzSEbmlxlUA==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/prosemirror-history/-/prosemirror-history-1.5.0.tgz", + "integrity": "sha512-zlzTiH01eKA55UAf1MEjtssJeHnGxO0j4K4Dpx+gnmX9n+SHNlDqI2oO1Kv1iPN5B1dm5fsljCfqKF9nFL6HRg==", "requires": { "prosemirror-state": "^1.2.2", "prosemirror-transform": "^1.0.0", + "prosemirror-view": "^1.31.0", "rope-sequence": "^1.3.0" } }, @@ -32534,18 +35061,18 @@ } }, "prosemirror-inputrules": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/prosemirror-inputrules/-/prosemirror-inputrules-1.2.0.tgz", - "integrity": "sha512-eAW/M/NTSSzpCOxfR8Abw6OagdG0MiDAiWHQMQveIsZtoKVYzm0AflSPq/ymqJd56/Su1YPbwy9lM13wgHOFmQ==", + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/prosemirror-inputrules/-/prosemirror-inputrules-1.5.1.tgz", + "integrity": "sha512-7wj4uMjKaXWAQ1CDgxNzNtR9AlsuwzHfdFH1ygEHA2KHF2DOEaXl1CJfNPAKCg9qNEh4rum975QLaCiQPyY6Fw==", "requires": { "prosemirror-state": "^1.0.0", "prosemirror-transform": "^1.0.0" } }, "prosemirror-keymap": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/prosemirror-keymap/-/prosemirror-keymap-1.2.0.tgz", - "integrity": "sha512-TdSfu+YyLDd54ufN/ZeD1VtBRYpgZnTPnnbY+4R08DDgs84KrIPEPbJL8t1Lm2dkljFx6xeBE26YWH3aIzkPKg==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/prosemirror-keymap/-/prosemirror-keymap-1.2.3.tgz", + "integrity": "sha512-4HucRlpiLd1IPQQXNqeo81BGtkY8Ai5smHhKW9jjPKRc2wQIxksg7Hl1tTI2IfT2B/LgX6bfYvXxEpJl7aKYKw==", "requires": { "prosemirror-state": "^1.0.0", "w3c-keyname": "^2.2.0" @@ -32597,9 +35124,9 @@ } }, "prosemirror-model": { - "version": "1.19.2", - "resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.19.2.tgz", - "integrity": "sha512-RXl0Waiss4YtJAUY3NzKH0xkJmsZupCIccqcIFoLTIKFlKNbIvFDRl27/kQy1FP8iUAxrjRRfIVvOebnnXJgqQ==", + "version": "1.25.7", + "resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.25.7.tgz", + "integrity": "sha512-A79aN8QEFUwI6cax8Yq4Rpcx1TJZ3Kagn+ii7qLo4/V8H3mMiHrhFyhTyHHvpSnOgMPpWiDGSwM3etwrxE50ug==", "requires": { "orderedmap": "^2.0.0" } @@ -32613,13 +35140,13 @@ } }, "prosemirror-schema-list": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/prosemirror-schema-list/-/prosemirror-schema-list-1.2.2.tgz", - "integrity": "sha512-rd0pqSDp86p0MUMKG903g3I9VmElFkQpkZ2iOd3EOVg1vo5Cst51rAsoE+5IPy0LPXq64eGcCYlW1+JPNxOj2w==", + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/prosemirror-schema-list/-/prosemirror-schema-list-1.5.1.tgz", + "integrity": "sha512-927lFx/uwyQaGwJxLWCZRkjXG0p48KpMj6ueoYiu4JX05GGuGcgzAy62dfiV8eFZftgyBUvLx76RsMe20fJl+Q==", "requires": { "prosemirror-model": "^1.0.0", "prosemirror-state": "^1.0.0", - "prosemirror-transform": "^1.0.0" + "prosemirror-transform": "^1.7.3" } }, "prosemirror-slash-menu": { @@ -32647,9 +35174,9 @@ } }, "prosemirror-state": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/prosemirror-state/-/prosemirror-state-1.4.3.tgz", - "integrity": "sha512-goFKORVbvPuAQaXhpbemJFRKJ2aixr+AZMGiquiqKxaucC6hlpHNZHWgz5R7dS4roHiwq9vDctE//CZ++o0W1Q==", + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/prosemirror-state/-/prosemirror-state-1.4.4.tgz", + "integrity": "sha512-6jiYHH2CIGbCfnxdHbXZ12gySFY/fz/ulZE333G6bPqIZ4F+TXo9ifiR86nAHpWnfoNjOb3o5ESi7J8Uz1jXHw==", "requires": { "prosemirror-model": "^1.0.0", "prosemirror-transform": "^1.0.0", @@ -32657,31 +35184,31 @@ } }, "prosemirror-tables": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/prosemirror-tables/-/prosemirror-tables-1.3.0.tgz", - "integrity": "sha512-ujzOb37O2ahmqI626Y0N0V/SZxuA9OGNYnsIMWdfecwkc8S8OShOqeD4kKUxpD0JcP81Z8qy/ulrXQuKhS4WUg==", + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/prosemirror-tables/-/prosemirror-tables-1.8.5.tgz", + "integrity": "sha512-V/0cDCsHKHe/tfWkeCmthNUcEp1IVO3p6vwN8XtwE9PZQLAZJigbw3QoraAdfJPir4NKJtNvOB8oYGKRl+t0Dw==", "requires": { - "prosemirror-keymap": "^1.1.2", - "prosemirror-model": "^1.8.1", - "prosemirror-state": "^1.3.1", - "prosemirror-transform": "^1.2.1", - "prosemirror-view": "^1.13.3" + "prosemirror-keymap": "^1.2.3", + "prosemirror-model": "^1.25.4", + "prosemirror-state": "^1.4.4", + "prosemirror-transform": "^1.10.5", + "prosemirror-view": "^1.41.4" } }, "prosemirror-transform": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/prosemirror-transform/-/prosemirror-transform-1.7.0.tgz", - "integrity": "sha512-O4T697Cqilw06Zvc3Wm+e237R6eZtJL/xGMliCi+Uo8VL6qHk6afz1qq0zNjT3eZMuYwnP8ZS0+YxX/tfcE9TQ==", + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/prosemirror-transform/-/prosemirror-transform-1.12.0.tgz", + "integrity": "sha512-GxboyN4AMIsoHNtz5uf2r2Ru551i5hWeCMD6E2Ib4Eogqoub0NflniaBPVQ4MrGE5yZ8JV9tUHg9qcZTTrcN4w==", "requires": { - "prosemirror-model": "^1.0.0" + "prosemirror-model": "^1.21.0" } }, "prosemirror-view": { - "version": "1.31.5", - "resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.31.5.tgz", - "integrity": "sha512-tobRCDeCp61elR1d97XE/JTL9FDIfswZpWeNs7GKJjAJvWyMGHWYFCq29850p6bbG2bckP+i9n1vT56RifosbA==", + "version": "1.41.7", + "resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.41.7.tgz", + "integrity": "sha512-jUwKNCEIGiqdvhlS91/2QAg21e4dfU5bH2iwmSDQeosXJgKF7smG0YSplOWK0cjSNgIqXe7VXqo7EIfUFJdt3w==", "requires": { - "prosemirror-model": "^1.16.0", + "prosemirror-model": "^1.20.0", "prosemirror-state": "^1.0.0", "prosemirror-transform": "^1.1.0" } @@ -32898,6 +35425,15 @@ "resolved": "https://registry.npmjs.org/react-recaptcha/-/react-recaptcha-2.3.10.tgz", "integrity": "sha512-IyanbozsYCuHvTYDuskZTIEcRAMG/sdvAu5b29iQWoC8Kd3Zk9WGCv2oNxh6RfGHvSvgHAyaLjmC6ei/yMsJ7g==" }, + "react-reconciler": { + "version": "0.29.2", + "resolved": "https://registry.npmjs.org/react-reconciler/-/react-reconciler-0.29.2.tgz", + "integrity": "sha512-zZQqIiYgDCTP/f1N/mAR10nJGrPD2ZR+jDSEsKWJHYC7Cm2wodlwbR3upZRdC3cjIjSlTLNVyO7Iu0Yy7t2AYg==", + "requires": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + } + }, "react-responsive": { "version": "9.0.2", "resolved": "https://registry.npmjs.org/react-responsive/-/react-responsive-9.0.2.tgz", @@ -33208,6 +35744,19 @@ } } }, + "remark-gfm": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz", + "integrity": "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==", + "requires": { + "@types/mdast": "^4.0.0", + "mdast-util-gfm": "^3.0.0", + "micromark-extension-gfm": "^3.0.0", + "remark-parse": "^11.0.0", + "remark-stringify": "^11.0.0", + "unified": "^11.0.0" + } + }, "remark-parse": { "version": "11.0.0", "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", @@ -33241,6 +35790,16 @@ } } }, + "remark-stringify": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz", + "integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==", + "requires": { + "@types/mdast": "^4.0.0", + "mdast-util-to-markdown": "^2.0.0", + "unified": "^11.0.0" + } + }, "require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -33545,6 +36104,40 @@ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "dev": true }, + "simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "dev": true + }, + "simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "dev": true, + "requires": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + }, + "dependencies": { + "decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dev": true, + "requires": { + "mimic-response": "^3.1.0" + } + }, + "mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "dev": true + } + } + }, "sister": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/sister/-/sister-3.0.2.tgz", @@ -34066,6 +36659,31 @@ "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", "devOptional": true }, + "tar-fs": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "dev": true, + "requires": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "dev": true, + "requires": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + } + }, "terser": { "version": "5.18.1", "resolved": "https://registry.npmjs.org/terser/-/terser-5.18.1.tgz", @@ -34261,9 +36879,9 @@ } }, "tslib": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz", - "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==" + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" }, "tsutils": { "version": "3.21.0", @@ -34280,6 +36898,34 @@ } } }, + "tsx": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.22.4.tgz", + "integrity": "sha512-X8EX+XV4QR5xCsrgxaED954zTDfY8KqlDtskKEL0cHhyS/P8b4IFOvGDQpsC9Q1XnLq915wEfwwY/zzskCtmhg==", + "dev": true, + "requires": { + "esbuild": "~0.28.0", + "fsevents": "~2.3.3" + }, + "dependencies": { + "fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "optional": true + } + } + }, + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "dev": true, + "requires": { + "safe-buffer": "^5.0.1" + } + }, "type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", diff --git a/package.json b/package.json index 66c8e36..88d0b3d 100644 --- a/package.json +++ b/package.json @@ -11,8 +11,13 @@ "build": "next build", "start": "next start", "lint": "tsc --noEmit && next lint", + "postinstall": "patch-package", "upgrade-interactive": "npm-check --update", - "scripts:getconfig": "ts-node -r dotenv/config --project scripts/tsconfig.json scripts/config/getConfig.ts" + "scripts:getconfig": "ts-node -r dotenv/config --project scripts/tsconfig.json scripts/config/getConfig.ts", + "perf": "tsx articles/react-prosemirror/scripts/run-editor-perf.ts", + "perf:nodeview": "tsx articles/react-prosemirror/scripts/run-nodeview-perf.ts", + "perf:keystroke-ramp": "tsx articles/react-prosemirror/scripts/run-keystroke-ramp.ts", + "perf:test": "cd articles/react-prosemirror && playwright test --config=playwright.perf.config.ts" }, "husky": { "hooks": { @@ -23,6 +28,7 @@ "author": "", "license": "ISC", "devDependencies": { + "@playwright/test": "^1.60.0", "@svgr/webpack": "^8.1.0", "@types/cors": "^2.8.13", "@types/node": "^18.11.15", @@ -48,17 +54,21 @@ "@types/styled-components": "^5.1.26", "autoprefixer": "^10.4.20", "babel-plugin-react-inline-svg-unique-id": "^1.4.0", + "chart.js": "^4.5.1", + "chartjs-node-canvas": "^5.0.0", "eslint": "^8.29.0", "eslint-config-next": "^13.0.7", "eslint-plugin-import": "^2.29.1", "git-commit-id": "^2.0.1", "husky": "^8.0.2", "npm-check": "^6.0.1", + "patch-package": "^8.0.1", "postcss": "^8.4.41", "prettier": "^3.3.3", "prettier-plugin-tailwindcss": "^0.6.6", "tailwindcss": "^3.4.9", "ts-node": "^10.9.1", + "tsx": "^4.22.4", "typescript": "^4.9.4" }, "dependencies": { @@ -67,12 +77,20 @@ "@codemirror/state": "^6.1.4", "@codemirror/view": "^6.7.1", "@emergence-engineering/lexical-slash-menu-plugin": "^0.0.3", + "@handlewithcare/react-prosemirror": "^3.0.6", "@inline-svg-unique-id/react": "^1.2.3", "@lexical/list": "^0.11.3", "@lexical/react": "^0.11.3", "@lexical/selection": "^0.11.3", "@popperjs/core": "^2.11.8", "@textea/json-viewer": "^2.12.0", + "@tiptap/core": "^3.24.0", + "@tiptap/extension-document": "^3.24.0", + "@tiptap/extension-history": "^3.24.0", + "@tiptap/extension-paragraph": "^3.24.0", + "@tiptap/extension-text": "^3.24.0", + "@tiptap/pm": "^3.24.0", + "@tiptap/react": "^3.24.0", "axios": "^1.2.1", "codemirror": "^6.0.1", "cors": "^2.8.5", @@ -111,9 +129,11 @@ "react-markdown": "^9.0.1", "react-popper": "^2.3.0", "react-recaptcha": "^2.3.10", + "react-reconciler": "^0.29.2", "react-responsive": "^9.0.2", "react-syntax-highlighter": "^15.5.0", "react-youtube": "^10.1.0", + "remark-gfm": "^4.0.1", "styled-components": "^5.3.6", "y-prosemirror": "^1.2.1", "y-protocols": "^1.0.5", diff --git a/pages/blog/react-prosemirror.tsx b/pages/blog/react-prosemirror.tsx new file mode 100644 index 0000000..7a4b57e --- /dev/null +++ b/pages/blog/react-prosemirror.tsx @@ -0,0 +1,368 @@ +import React, { useState } from "react"; + +import ArticleWrapper from "../../features/article/components/ArticleWrapper"; +import { ArticleIntro } from "../../features/article/types"; +import Markdown from "../../features/article/components/Markdown"; +import ArticleShareOgTags from "../../features/article/components/ArticleShareOgTags"; +import ArticleHeader from "../../features/article/components/ArticleHeader"; +import { LightBox } from "../../features/twBlog/LightBox"; + +export const articleReactProsemirrorMetadata: ArticleIntro = { + title: + "React-ProseMirror vs Vanilla ProseMirror vs TipTap performance comparison", + author: "Viktor and matejcsok", + authorLink: null, + introText: /* language=md */ `Can TipTap or React-ProseMirror beat vanilla ProseMirror?`, + postId: "react-prosemirror", + timestamp: 1780066915415, + imgSrc: + "https://discuss.prosemirror.net/uploads/secondsite/original/1X/5005ab45edc1c7b72d1331d43feb55a5cad7b74c.png", + url: "https://emergence-engineering.com/blog/react-prosemirror", + tags: [ + "ProseMirror", + "Plugin Development", + "Comparison", + "React-ProseMirror", + "TipTap", + ], +}; + +const MD0 = /* language=md */ ` +# React-ProseMirror vs Vanilla ProseMirror vs TipTap performance comparison + +## TL;DR +React-ProseMirror makes development much easier if working with ProseMirror and React. +Can use all the same plugins and features. And if using NodeViews it is much easier, we can access React context, hooks and state. But it comes with a cost. +As we add more and more nodes to the document it has to pay the React reconciler tax. + +Vanilla ProseMirror is the fastest, but it needs the most amount of work for a full-featured editor. + +TipTap ships as a near-full-featured editor out of the box and performance wise almost identical to vanilla ProseMirror until we start adding NodeViews, we can use React context, hooks, +state with \`ReactNodeViewRenderer\` to make it easier to develop, but it uses React Portals which adds tax. +React-ProseMirror performance issue becomes noticeable at 5k nodes. TipTap performance issue becomes noticeable around 2.5k-5k nodes with NodeViews. + +## Introduction + +Vanilla ProseMirror is the standard technology for rich text editing in the browser. It is a powerful and flexible library that provides a wide range of features and customization options. +It comes with some basic plugins, but requires some work to make it a full-featured online editor. On the other hand it has a huge community and there are a lot of free plugins made by other developers you can use to customize your editor. + +TipTap is an abstraction layer built on top of ProseMirror, which provides a lot of pre-built plugins to start with. Much easier to plug and play with the official plugins. +It is also fully customizable, but the abstraction layer makes it harder to customize. +It has a solution for easier custom NodeViews using [ReactNodeViewRenderer](https://tiptap.dev/docs/editor/extensions/custom-extensions/node-views/react) which makes it possible to use React components as NodeViews, +and accessing state and context. But we will see that it comes with a cost. + +React-ProseMirror moved the rendering out of the editor view and into React. +This makes it much easier to use ProseMirror with React: you can use plain React components for custom NodeViews, share normal React context and hooks, +and let the editor UI live in the same component tree as the rest of your app. You still get ProseMirror's document model, transactions, plugins, and selection handling, but without fighting a separate DOM-rendering lifecycle. +[_This library provides an alternate implementation of ProseMirror's EditorView. +It uses React as the rendering engine, rather than ProseMirror's home-brewed DOM update system. +This allows us to provide a more comfortable integration with ProseMirror's powerful data model, transformations, and event management systems._](https://github.com/handlewithcarecollective/react-prosemirror#the-solution) + +## Test Cases + +### Test Case 1: Engine throughput and the cost of holding state as the document grows + +#### What the test does: +From an empty editor, in a single browser session, append paragraphs to the end of the document as fast as the engine allows. After every 200 appends, +record a checkpoint (nodes, elapsedMs) and yield one animation frame so the browser can paint and the test harness can read CDP metrics. +Every 2 seconds, an outer monitor polls Chrome's \`Performance.getMetrics\` and records the cumulative cost across six counters: ScriptDuration, TaskDuration, JSHeapUsedSize, Nodes (DOM), LayoutCount, RecalcStyleCount. + +How each "keystroke" is fired. Inside \`page.evaluate\`, for each cycle we dispatch synthetic \`InputEvent("beforeinput", {inputType: "insertText", data})\` events for the characters in "typing ", +followed by a synthetic \`KeyboardEvent("keydown", {key: "Enter"})\`. +Each editor's view-level DOM input handlers pick these up and apply them exactly as they would a real keystroke +No real OS-level input pipeline, no IME, no focus management — just the editor's reaction to the event. + +- We keep going until a single 200-node batch exceeds 5s here, ≈40 nodes/sec - well past usable, or heap > 3.5 GB, or the renderer crashes, or a 50,000-node safety cap. +- We don't measure per-keystroke latency. +- We always append at the end of the document. +- Synthetic events, we don't try to replicate a human typing experience, only pure engine throughput. +`; + +const MD1 = ` +#### Conclusions: +- React-ProseMirror pays a per-transaction reconciler tax that scales with the document size. for every new paragraph React needs to walk a growing tree, even when nothing has changed in the existing paragraphs +- Vanilla ProseMirror and TipTap let the \`EditorView\` handle DOM updates directly, the cost is proportional to "what has changed" , not to document size. +- Memory usage scales with document size for Vanilla ProseMirror and TipTap, but not for React-ProseMirror. +For React-ProseMirror every paragraph in the document also exists as a React fiber - a JS object holding props, hooks, refs, parent/sibling pointers, and an alternate fiber for the next render. + +#### Disclosure: +We needed to patch a memory leak in the official React-ProseMirror implementation, without it the memory usage skyrocketed, and the stress test ended pretty quickly. +`; + +const MD2 = ` +### Test Case 2: Cold load + +#### What the test does: +- We render the 3 editors with 500, 1000, 2500, 5000, 10k, 20k 50k nodes in the document and we measure time to React tree mount, ProseMirror view init, and rendering N paragraphs into the DOM. + +`; + +const MD3 = /* language=md */ ` +#### Conclusions: +- Mount cost scales with how many React fibers your engine creates, not how many DOM nodes the browser holds. +Vanilla ProseMirror has zero React fibers for nodes so it has a flat curve. +The two React-rendered engines each pay a per-paragraph reconciler cost at mount; TipTap's ReactNodeViewRenderer adds another layer on top, which is why it ends up worst. +`; + +const MD4 = ` +### Test Case 3: Keystroke latency (lag breakpoint) + +#### What the test does: +- Find the document size where typing feels laggy +- Increment doc size by 1k paragraphs with NodeView in each line +- Fire 150 real Chromium keystrokes via \`page.keyboard.press("a")\` +- Stop when p95 INP > 100 ms for two consecutive iterations. +`; + +const MD5 = ` +#### Conclusions: + - Vanilla ProseMirror's typing stays smooth ~5× longer than TipTap with the same NodeView. + Same per-paragraph React component, same paragraph schema, same keystrokes - the only thing different is what owns the editor surface. + Vanilla ProseMirror applies the mutation directly to the DOM and updates the affected fiber; TipTap's ReactNodeViewRenderer adds portal-bridge synchronization on top, which dominates per-keystroke cost at scale. + - A memoized React NodeView is not free at scale. Even with React.memo, the reconciler still has to visit each fiber on every keystroke to ask "should I render?" - it can skip the render phase, not the walk. + At 30k+ fibers, that walk alone exceeds the 100 ms budget. +`; + +const MDff = /* language=md */ ` +### Does this hold on Firefox? + We re-ran the portable test cases on Firefox 148 — same specs, same machine, only the engine swapped. + Memory is gone on purpose: Firefox exposes no JS-heap API to a page (\`performance.memory\` and \`measureUserAgentSpecificMemory\` are both Chromium-only), so the heap and script-duration graphs above can't be reproduced. +That leaves throughput, cold-load time-to-visible, and keystroke latency. + + One thing to know before reading the latency chart: Firefox's INP floor is ~56ms on an empty document vs Chromium's ~16ms, almost entirely paint/vsync quantization reported in coarse 8ms steps. +Don't compare the absolute numbers across engines - compare the slope above each engine's own + baseline. + + #### What changed, what didn't: + - Vanilla ProseMirror and React-ProseMirror keep their ranking and their shape. + React-ProseMirror is ~1.5× slower at sustained typing on Firefox, but the per-keystroke cost grows at the same rate on both engines - the reconciler tax belongs to the engine, not the browser. + - Vanilla ProseMirror stays flat on both. No surprises. + - TipTap with NodeViews is the one real divergence. The \`ReactNodeViewRenderer\` portal bridge is meaningfully more expensive on Firefox: steeper cold-load, and a keystroke lag breakpoint that lands roughly twice as early. + If you ship React NodeViews through TipTap, Firefox is your worst case, not Chrome. +`; + +const MD6 = /* language=md */ ` +### Verdict + + - For most use cases React-ProseMirror is fine (blog posts, articles, even thesis or Moby Dick), no visual lags, but makes development much easier (React components/hooks/context). + - For extremely large content Vanilla ProseMirror or TipTap perform much better, until you have many Node Views, in that case Vanilla ProseMirror is the absolute winner. + - But even Vanilla ProseMirror can't handle the whole Bible, but at that point it is not even ProseMirror, but the \`contentEditable\` is the bottleneck. + A solution could be virtualization - which has not been implemented with ProseMirror yet - or splitting by chapters, and mounting one chapter at a time. +`; + +const MD7 = /* language=md */ ` +### Some reference: +| Document | Paragraphs (≈) | +|---|---| +| Typical blog post | 30–80 | +| Long-read article (Wired feature, NYT Magazine) | 100–250 | +| Wikipedia article ("World War II") | ~500 | +| Master's thesis | 1,000–2,000 | +| The Great Gatsby | ~700 | +| Harry Potter and the Sorcerer's Stone | ~2,500 | +| Moby Dick | ~3,000 | +| The Lord of the Rings (entire trilogy) | ~6,500 | +| War and Peace | ~8,000–10,000 | +| All 7 Harry Potter books combined | ~14,500 | +| The complete works of Shakespeare | ~25,000 | +`; + +const stress1 = [ + { + src: "/blog/react-prosemirror/combined-v3-v4-v5-JSHeapUsedSize.png", + title: "JSHeapUsedSize", + }, + { + src: "/blog/react-prosemirror/combined-v3-v4-v5-ScriptDuration.png", + title: "script duration", + }, +]; + +const stress2 = [ + { + src: "/blog/react-prosemirror/node-count-vs-time-v3-v4-v5.png", + title: "React-ProseMirror's layout count", + }, + { + src: "/blog/react-prosemirror/node-count-vs-time-v4-v5.png", + title: "React-ProseMirror's js heap used size", + }, +]; + +const coldLoad = [ + { + src: "/blog/react-prosemirror/combined-v3-snv-v4-snv-v5-snv-cold-load-JSHeapUsedSize.png", + title: "Cold load JSHeapUsedSize", + }, + { + src: "/blog/react-prosemirror/combined-v3-snv-v4-snv-v5-snv-cold-load-timeToVisible.png", + title: "Cold load time to visible", + }, +]; + +const inputLatency = [ + { + src: "/blog/react-prosemirror/keystroke-ramp.png", + title: "Keystroke latency", + }, +]; + +const firefixDiagrams = [ + { + src: "/blog/react-prosemirror/ff-vs-chrome-tc1-throughput.png", + title: "Firefox vs Chrome Test Case 1", + }, + { + src: "/blog/react-prosemirror/ff-vs-chrome-tc2-coldload.png", + title: "Firefox vs Chrome Test Case 2", + }, +]; + +const firefixDiagrams2 = [ + { + src: "/blog/react-prosemirror/ff-vs-chrome-tc3-ramp.png", + title: "Firefox vs Chrome Test Case 3", + }, +]; + +const Article = () => { + const [isOpen, setIsOpen] = useState(false); + const [activeGraph, setActiveGraph] = useState(null); + + const handleToggleImage = (src: string) => { + if (isOpen && activeGraph === src) { + setIsOpen(false); + setActiveGraph(null); + } else { + setActiveGraph(src); + setIsOpen(true); + } + }; + + return ( + + + + + +
+ {stress1.map((image) => ( +
handleToggleImage(image.src)} + > + +
+ ))} +
+ +
+ {stress2.map((image) => ( +
handleToggleImage(image.src)} + > + +
+ ))} +
+ + + + +
+ {coldLoad.map((image) => ( +
handleToggleImage(image.src)} + > + +
+ ))} +
+ + + + +
+ {inputLatency.map((image) => ( +
handleToggleImage(image.src)} + > + +
+ ))} +
+ + + +
+ {firefixDiagrams.map((image) => ( +
handleToggleImage(image.src)} + > + +
+ ))} +
+ +
+ {firefixDiagrams2.map((image) => ( +
handleToggleImage(image.src)} + > + +
+ ))} +
+ + + +
+ ); +}; + +export default Article; diff --git a/pages/perf-react-prosemirror-snv.tsx b/pages/perf-react-prosemirror-snv.tsx new file mode 100644 index 0000000..6760dc8 --- /dev/null +++ b/pages/perf-react-prosemirror-snv.tsx @@ -0,0 +1,11 @@ +import dynamic from "next/dynamic"; + +// react-prosemirror + static React nodeview — impl key "v3-snv". +const PerfV3SnvPage = dynamic( + () => import("../articles/react-prosemirror/editors/perf-v3-snv/page"), + { ssr: false }, +); + +export default function Page() { + return ; +} diff --git a/pages/perf-react-prosemirror.tsx b/pages/perf-react-prosemirror.tsx new file mode 100644 index 0000000..5568462 --- /dev/null +++ b/pages/perf-react-prosemirror.tsx @@ -0,0 +1,13 @@ +import dynamic from "next/dynamic"; + +// react-prosemirror (barebone) perf harness — impl key "v3". +// Client-only: the editor reads `?n=` from window and manages its own DOM, +// so SSR is disabled to avoid hydration mismatch. +const PerfV3Page = dynamic( + () => import("../articles/react-prosemirror/editors/perf-v3/page"), + { ssr: false }, +); + +export default function Page() { + return ; +} diff --git a/pages/perf-tiptap-snv.tsx b/pages/perf-tiptap-snv.tsx new file mode 100644 index 0000000..53430c4 --- /dev/null +++ b/pages/perf-tiptap-snv.tsx @@ -0,0 +1,11 @@ +import dynamic from "next/dynamic"; + +// Tiptap 3 + static React nodeview — impl key "v5-snv". +const PerfV5SnvPage = dynamic( + () => import("../articles/react-prosemirror/editors/perf-v5-snv/page"), + { ssr: false }, +); + +export default function Page() { + return ; +} diff --git a/pages/perf-tiptap.tsx b/pages/perf-tiptap.tsx new file mode 100644 index 0000000..1ecf1c0 --- /dev/null +++ b/pages/perf-tiptap.tsx @@ -0,0 +1,11 @@ +import dynamic from "next/dynamic"; + +// Bare-bone Tiptap 3 perf harness — impl key "v5". +const PerfV5Page = dynamic( + () => import("../articles/react-prosemirror/editors/perf-v5/page"), + { ssr: false }, +); + +export default function Page() { + return ; +} diff --git a/pages/perf-vanilla-prosemirror-snv.tsx b/pages/perf-vanilla-prosemirror-snv.tsx new file mode 100644 index 0000000..e63fe58 --- /dev/null +++ b/pages/perf-vanilla-prosemirror-snv.tsx @@ -0,0 +1,11 @@ +import dynamic from "next/dynamic"; + +// Vanilla ProseMirror + static DOM nodeview — impl key "v4-snv". +const PerfV4SnvPage = dynamic( + () => import("../articles/react-prosemirror/editors/perf-v4-snv/page"), + { ssr: false }, +); + +export default function Page() { + return ; +} diff --git a/pages/perf-vanilla-prosemirror.tsx b/pages/perf-vanilla-prosemirror.tsx new file mode 100644 index 0000000..1b3856d --- /dev/null +++ b/pages/perf-vanilla-prosemirror.tsx @@ -0,0 +1,11 @@ +import dynamic from "next/dynamic"; + +// Vanilla ProseMirror (no React reconciler) perf harness — impl key "v4". +const PerfV4Page = dynamic( + () => import("../articles/react-prosemirror/editors/perf-v4/page"), + { ssr: false }, +); + +export default function Page() { + return ; +} diff --git a/patches/@handlewithcare+react-prosemirror+3.0.6.patch b/patches/@handlewithcare+react-prosemirror+3.0.6.patch new file mode 100644 index 0000000..57e5e82 --- /dev/null +++ b/patches/@handlewithcare+react-prosemirror+3.0.6.patch @@ -0,0 +1,138 @@ +diff --git a/node_modules/@handlewithcare/react-prosemirror/dist/cjs/plugins/reactKeys.js b/node_modules/@handlewithcare/react-prosemirror/dist/cjs/plugins/reactKeys.js +index 2bfe841..0e50870 100644 +--- a/node_modules/@handlewithcare/react-prosemirror/dist/cjs/plugins/reactKeys.js ++++ b/node_modules/@handlewithcare/react-prosemirror/dist/cjs/plugins/reactKeys.js +@@ -44,21 +44,16 @@ function reactKeys() { + return next; + }, + /** +- * Keeps node keys stable across transactions. +- * +- * To accomplish this, we map each node position forwards +- * through the transaction to identify its current position, +- * and assign its key to that new position, dropping it if the +- * node was deleted. +- */ apply (tr, value, _, newState) { ++ * PATCH(memory): see dist/esm/plugins/reactKeys.js for the ++ * full rationale. Short version: mutate the existing plugin ++ * state in place so React fibers can't pin historical Maps. ++ */ apply (tr, value, _, newState) { + if (!tr.docChanged || composing) { + return value; + } + const overrides = tr.getMeta(reactKeysPluginKey)?.overrides; +- const next = { +- posToKey: new Map(), +- keyToPos: new Map() +- }; ++ const nextPosToKey = new Map(); ++ const nextKeyToPos = new Map(); + const posToKeyEntries = Array.from(value.posToKey.entries()).sort((param, param1)=>{ + let [a] = param, [b] = param1; + return a - b; +@@ -70,17 +65,21 @@ function reactKeys() { + deleted: false + }; + if (deleted) continue; +- next.posToKey.set(newPos, key); +- next.keyToPos.set(key, newPos); ++ nextPosToKey.set(newPos, key); ++ nextKeyToPos.set(key, newPos); + } + newState.doc.descendants((_, pos)=>{ +- if (next.posToKey.has(pos)) return true; ++ if (nextPosToKey.has(pos)) return true; + const key = createNodeKey(); +- next.posToKey.set(pos, key); +- next.keyToPos.set(key, pos); ++ nextPosToKey.set(pos, key); ++ nextKeyToPos.set(key, pos); + return true; + }); +- return next; ++ value.posToKey.clear(); ++ value.keyToPos.clear(); ++ for (const [k, v] of nextPosToKey) value.posToKey.set(k, v); ++ for (const [k, v] of nextKeyToPos) value.keyToPos.set(k, v); ++ return value; + } + }, + props: { +diff --git a/node_modules/@handlewithcare/react-prosemirror/dist/esm/plugins/reactKeys.js b/node_modules/@handlewithcare/react-prosemirror/dist/esm/plugins/reactKeys.js +index 34f86a4..627a415 100644 +--- a/node_modules/@handlewithcare/react-prosemirror/dist/esm/plugins/reactKeys.js ++++ b/node_modules/@handlewithcare/react-prosemirror/dist/esm/plugins/reactKeys.js +@@ -29,21 +29,34 @@ export const reactKeysPluginKey = new PluginKey("@handlewithcare/react-prosemirr + return next; + }, + /** +- * Keeps node keys stable across transactions. +- * +- * To accomplish this, we map each node position forwards +- * through the transaction to identify its current position, +- * and assign its key to that new position, dropping it if the +- * node was deleted. +- */ apply (tr, value, _, newState) { ++ * Keeps node keys stable across transactions. ++ * ++ * To accomplish this, we map each node position forwards ++ * through the transaction to identify its current position, ++ * and assign its key to that new position, dropping it if the ++ * node was deleted. ++ * ++ * PATCH(memory): the upstream implementation returns a freshly ++ * allocated `{ posToKey, keyToPos }` on every apply. React fiber ++ * memoizedState pins those objects for memoized node views, so ++ * each unchanged node ends up holding the plugin-state snapshot ++ * from the time it last rendered — at N nodes this retains ++ * Σ(1..N) Map entries (O(N²)). To break the chain we keep stable ++ * identity for the outer object *and* its two Maps by mutating ++ * them in place: compute the next contents in temporary Maps ++ * (so iteration over the old Map isn't disturbed), then clear ++ * and re-fill the originals. Mutation across an EditorState ++ * boundary is normally a smell, but prosemirror-history doesn't ++ * snapshot plugin state and the only consumer (`useReactKeys`) ++ * reads from `view.state` (always the latest), so observable ++ * behaviour is unchanged. ++ */ apply (tr, value, _, newState) { + if (!tr.docChanged || composing) { + return value; + } + const overrides = tr.getMeta(reactKeysPluginKey)?.overrides; +- const next = { +- posToKey: new Map(), +- keyToPos: new Map() +- }; ++ const nextPosToKey = new Map(); ++ const nextKeyToPos = new Map(); + const posToKeyEntries = Array.from(value.posToKey.entries()).sort((param, param1)=>{ + let [a] = param, [b] = param1; + return a - b; +@@ -55,17 +68,21 @@ export const reactKeysPluginKey = new PluginKey("@handlewithcare/react-prosemirr + deleted: false + }; + if (deleted) continue; +- next.posToKey.set(newPos, key); +- next.keyToPos.set(key, newPos); ++ nextPosToKey.set(newPos, key); ++ nextKeyToPos.set(key, newPos); + } + newState.doc.descendants((_, pos)=>{ +- if (next.posToKey.has(pos)) return true; ++ if (nextPosToKey.has(pos)) return true; + const key = createNodeKey(); +- next.posToKey.set(pos, key); +- next.keyToPos.set(key, pos); ++ nextPosToKey.set(pos, key); ++ nextKeyToPos.set(key, pos); + return true; + }); +- return next; ++ value.posToKey.clear(); ++ value.keyToPos.clear(); ++ for (const [k, v] of nextPosToKey) value.posToKey.set(k, v); ++ for (const [k, v] of nextKeyToPos) value.keyToPos.set(k, v); ++ return value; + } + }, + props: { diff --git a/public/blog/react-prosemirror/combined-v3-snv-v4-snv-v5-snv-cold-load-JSHeapUsedSize.png b/public/blog/react-prosemirror/combined-v3-snv-v4-snv-v5-snv-cold-load-JSHeapUsedSize.png new file mode 100644 index 0000000..dbc019d Binary files /dev/null and b/public/blog/react-prosemirror/combined-v3-snv-v4-snv-v5-snv-cold-load-JSHeapUsedSize.png differ diff --git a/public/blog/react-prosemirror/combined-v3-snv-v4-snv-v5-snv-cold-load-timeToVisible.png b/public/blog/react-prosemirror/combined-v3-snv-v4-snv-v5-snv-cold-load-timeToVisible.png new file mode 100644 index 0000000..33593cc Binary files /dev/null and b/public/blog/react-prosemirror/combined-v3-snv-v4-snv-v5-snv-cold-load-timeToVisible.png differ diff --git a/public/blog/react-prosemirror/combined-v3-v4-v5-JSHeapUsedSize.png b/public/blog/react-prosemirror/combined-v3-v4-v5-JSHeapUsedSize.png new file mode 100644 index 0000000..6521717 Binary files /dev/null and b/public/blog/react-prosemirror/combined-v3-v4-v5-JSHeapUsedSize.png differ diff --git a/public/blog/react-prosemirror/combined-v3-v4-v5-ScriptDuration.png b/public/blog/react-prosemirror/combined-v3-v4-v5-ScriptDuration.png new file mode 100644 index 0000000..c0a8618 Binary files /dev/null and b/public/blog/react-prosemirror/combined-v3-v4-v5-ScriptDuration.png differ diff --git a/public/blog/react-prosemirror/ff-vs-chrome-tc1-throughput.png b/public/blog/react-prosemirror/ff-vs-chrome-tc1-throughput.png new file mode 100644 index 0000000..7ea0058 Binary files /dev/null and b/public/blog/react-prosemirror/ff-vs-chrome-tc1-throughput.png differ diff --git a/public/blog/react-prosemirror/ff-vs-chrome-tc2-coldload.png b/public/blog/react-prosemirror/ff-vs-chrome-tc2-coldload.png new file mode 100644 index 0000000..d570624 Binary files /dev/null and b/public/blog/react-prosemirror/ff-vs-chrome-tc2-coldload.png differ diff --git a/public/blog/react-prosemirror/ff-vs-chrome-tc3-ramp.png b/public/blog/react-prosemirror/ff-vs-chrome-tc3-ramp.png new file mode 100644 index 0000000..32fde19 Binary files /dev/null and b/public/blog/react-prosemirror/ff-vs-chrome-tc3-ramp.png differ diff --git a/public/blog/react-prosemirror/keystroke-ramp.png b/public/blog/react-prosemirror/keystroke-ramp.png new file mode 100644 index 0000000..4ba75da Binary files /dev/null and b/public/blog/react-prosemirror/keystroke-ramp.png differ diff --git a/public/blog/react-prosemirror/node-count-vs-time-v3-v4-v5.png b/public/blog/react-prosemirror/node-count-vs-time-v3-v4-v5.png new file mode 100644 index 0000000..294a87a Binary files /dev/null and b/public/blog/react-prosemirror/node-count-vs-time-v3-v4-v5.png differ diff --git a/public/blog/react-prosemirror/node-count-vs-time-v4-v5.png b/public/blog/react-prosemirror/node-count-vs-time-v4-v5.png new file mode 100644 index 0000000..e23c106 Binary files /dev/null and b/public/blog/react-prosemirror/node-count-vs-time-v4-v5.png differ diff --git a/tsconfig.json b/tsconfig.json index c8dda15..04e0a55 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -20,12 +20,19 @@ "forceConsistentCasingInFileNames": true, "resolveJsonModule": true, "isolatedModules": true, - "incremental": true + "incremental": true, + "baseUrl": ".", + "paths": { + "@/lib/*": ["articles/react-prosemirror/editors/lib/*"] + } }, "include": [ "." ], "exclude": [ - "node_modules" + "node_modules", + "articles/react-prosemirror/e2e", + "articles/react-prosemirror/scripts", + "articles/react-prosemirror/playwright.perf.config.ts" ] }