diff --git a/backend/models/canvas.py b/backend/models/canvas.py new file mode 100644 index 0000000..27deac0 --- /dev/null +++ b/backend/models/canvas.py @@ -0,0 +1,71 @@ +"""JSON Canvas 1.0 validation; retain extension fields for lossless round trips.""" +import json +import math +import re + + +def validate_canvas(content: str) -> str: + try: + value = json.loads(content) + except (ValueError, TypeError) as exc: + raise ValueError("Canvas content must be valid JSON") from exc + if not isinstance(value, dict): + raise ValueError("Canvas must be a JSON object") + nodes, edges = value.get("nodes", []), value.get("edges", []) + if not isinstance(nodes, list) or not isinstance(edges, list): + raise ValueError("Canvas nodes and edges must be arrays") + ids = set() + + def text(item, key, required=False): + if required or key in item: + if not isinstance(item.get(key), str): + raise ValueError(f"Canvas {key} must be a string") + + def identity(item): + if not isinstance(item, dict) or not isinstance(item.get("id"), str) or not item["id"]: + raise ValueError("Canvas elements require a nonempty string id") + if item["id"] in ids: + raise ValueError("Canvas element ids must be unique") + ids.add(item["id"]) + if "color" in item and (not isinstance(item["color"], str) or not re.fullmatch(r"[1-6]|#[0-9a-fA-F]{6}", item["color"])): + raise ValueError("Canvas color must be a preset 1–6 or #RRGGBB") + + for node in nodes: + identity(node) + if node.get("type") not in ("text", "file", "link", "group"): + raise ValueError("Unknown canvas node type") + for key in ("x", "y", "width", "height"): + number = node.get(key) + if isinstance(number, bool) or not isinstance(number, (int, float)) or not math.isfinite(number) or number != int(number): + raise ValueError(f"Canvas {key} must be a finite integer") + if key in ("width", "height") and number <= 0: + raise ValueError("Canvas dimensions must be positive") + if node["type"] == "text": + text(node, "text", True) + elif node["type"] == "file": + text(node, "file", True) + text(node, "subpath") + if "subpath" in node and not node["subpath"].startswith("#"): + raise ValueError("Canvas subpath must start with #") + elif node["type"] == "link": + text(node, "url", True) + else: + text(node, "label") + text(node, "background") + if "backgroundStyle" in node and node["backgroundStyle"] not in ("cover", "ratio", "repeat"): + raise ValueError("Invalid canvas background style") + node_ids = set(ids) + for edge in edges: + identity(edge) + for key in ("fromNode", "toNode"): + text(edge, key, True) + if edge[key] not in node_ids: + raise ValueError("Canvas edges must reference existing nodes") + for key in ("fromSide", "toSide"): + if key in edge and edge[key] not in ("top", "right", "bottom", "left"): + raise ValueError("Invalid canvas connection side") + for key in ("fromEnd", "toEnd"): + if key in edge and edge[key] not in ("none", "arrow"): + raise ValueError("Invalid canvas connection endpoint") + text(edge, "label") + return content diff --git a/backend/models/note.py b/backend/models/note.py index 1380241..3eb41ec 100644 --- a/backend/models/note.py +++ b/backend/models/note.py @@ -1,4 +1,6 @@ -from typing import Optional +from typing import Literal, Optional +from pydantic import model_validator +from .canvas import validate_canvas from .base import CamelModel @@ -12,7 +14,7 @@ class Note(CamelModel): experiment_id: Optional[str] = None name: str parent_id: Optional[str] = None - type: str = "file" # file | folder + type: str = "file" # file | folder | canvas content: str = "" is_pinned: bool = False created_at: str @@ -22,9 +24,17 @@ class Note(CamelModel): class NoteCreate(CamelModel): name: str parent_id: Optional[str] = None - type: str = "file" + type: Literal["file", "folder", "canvas"] = "file" content: str = "" + @model_validator(mode="after") + def validate_document(self): + if self.type == "canvas": + self.content = validate_canvas(self.content or '{"nodes":[],"edges":[]}') + if not self.name.lower().endswith(".canvas"): + self.name += ".canvas" + return self + class NoteUpdate(CamelModel): name: Optional[str] = None diff --git a/backend/services/note_service.py b/backend/services/note_service.py index 2fe867b..0a5ea6d 100644 --- a/backend/services/note_service.py +++ b/backend/services/note_service.py @@ -10,6 +10,8 @@ from services.cost_service import record_openai_usage from models.note import Note, NoteCreate, NoteUpdate +from models.canvas import validate_canvas +from fastapi import HTTPException from models.note_batch import ItemNotes, NoteBatchRequest from services.db import get_client @@ -124,6 +126,14 @@ def update_note(note_id: str, data: NoteUpdate) -> Optional[Note]: return None # exclude_unset (not exclude_none) so callers can explicitly set fields to null updates = data.model_dump(exclude_unset=True) + if existing.type == "canvas": + if "content" in updates: + try: + validate_canvas(updates["content"]) + except ValueError as exc: + raise HTTPException(status_code=422, detail=str(exc)) from exc + if updates.get("name") and not updates["name"].lower().endswith(".canvas"): + updates["name"] += ".canvas" # When reassigning to a different source, null out all other source fields so # a note always belongs to exactly one source at a time. if any(f in updates for f in _SOURCE_FIELDS): diff --git a/backend/tests/test_canvas.py b/backend/tests/test_canvas.py new file mode 100644 index 0000000..c88e9d7 --- /dev/null +++ b/backend/tests/test_canvas.py @@ -0,0 +1,71 @@ +import json +from unittest.mock import Mock + +import pytest +from pydantic import ValidationError + +from models.canvas import validate_canvas +from models.note import Note, NoteCreate, NoteUpdate +from services import note_service + + +def node(**updates): + return {"id": "n1", "type": "text", "x": 0, "y": 0, "width": 300, "height": 200, "text": "Hello", **updates} + + +def test_canvas_creation_defaults_and_extensions(): + data = NoteCreate(name="Map", type="canvas") + assert data.name == "Map.canvas" + assert json.loads(data.content) == {"nodes": [], "edges": []} + content = json.dumps({"nodes": [node(researchos={"custom": True})], "edges": [], "vendor": {"future": 1}}) + assert NoteCreate(name="Map.canvas", type="canvas", content=content).content == content + + +@pytest.mark.parametrize("value", [None, [], {"nodes": None}, {"nodes": [node(), node()]}, {"nodes": [node(width=0)]}, {"nodes": [node(x=True)]}, {"nodes": [node(x=0.5)]}, {"nodes": [node(color="red")]}, {"nodes": [node(type="file", file="a.md", subpath="heading")]}, {"nodes": [node()], "edges": [{"id": "e", "fromNode": "n1", "toNode": "missing"}]}]) +def test_invalid_canvas_is_rejected(value): + with pytest.raises(ValueError): + validate_canvas(json.dumps(value)) + + +def test_optional_arrays_and_all_node_types(): + assert validate_canvas('{}') == '{}' + data = {"nodes": [node(), node(id="f", type="file", file="Nested.canvas", subpath="#Heading"), node(id="l", type="link", url="https://example.com"), node(id="g", type="group", label="Group", background="image.png", backgroundStyle="ratio")], "edges": [{"id": "edge", "fromNode": "f", "toNode": "l", "fromSide": "bottom", "toSide": "top", "fromEnd": "arrow", "toEnd": "none", "color": "#123456", "label": "reference"}]} + assert json.loads(validate_canvas(json.dumps(data))) == data + + +def test_canvas_create_api_rejects_bad_json_before_write(client, monkeypatch): + create = Mock() + monkeypatch.setattr(note_service, 'create_note', create) + result = client.post('/api/libraries/lib/notes', json={"name": "Bad", "type": "canvas", "content": "invalid"}) + assert result.status_code == 422 + create.assert_not_called() + + +def test_update_rejects_invalid_content_before_database_write(monkeypatch): + existing = Note(id="c", name="Map.canvas", type="canvas", content='{}', created_at="now", updated_at="now") + monkeypatch.setattr(note_service, "get_note", lambda _: existing) + db = Mock() + monkeypatch.setattr(note_service, "get_client", lambda: db) + from fastapi import HTTPException + with pytest.raises(HTTPException) as error: + note_service.update_note("c", NoteUpdate(content="

Accidental HTML overwrite

")) + assert error.value.status_code == 422 + db.table.assert_not_called() + + +@pytest.mark.parametrize('scope,identifier', [('paper_id', 'paper'), ('website_id', 'site'), ('github_repo_id', 'repo'), ('library_id', 'library'), ('project_id', 'project'), ('experiment_id', 'experiment')]) +def test_canvas_persists_in_every_notes_scope(monkeypatch, scope, identifier): + db = Mock() + monkeypatch.setattr(note_service, 'get_client', lambda: db) + created = note_service.create_note(NoteCreate(name='Map', type='canvas', parent_id='folder'), **{scope: identifier}) + assert created.type == 'canvas' + assert getattr(created, scope) == identifier + saved = db.table.return_value.insert.call_args.args[0] + assert saved['parent_id'] == 'folder' + assert json.loads(saved['content']) == {'nodes': [], 'edges': []} + + +def test_existing_note_html_is_unchanged(): + assert NoteCreate(name='Note', content='

Hello

').content == '

Hello

' + with pytest.raises(ValidationError): + NoteCreate(name='Invalid', type='unknown') diff --git a/docs/README.md b/docs/README.md index 48cb695..00dc21a 100644 --- a/docs/README.md +++ b/docs/README.md @@ -2,6 +2,7 @@ ## For Users +- [Canvas documents](canvas.md) — Visual notes, nested canvases, file cards, and portable exports - [User Guide](user-guide.md) — Walkthrough: first papers to projects to AI features - [Costs & Privacy](costs-and-privacy.md) — What goes to OpenAI, cost estimates, data ownership - [FAQ & Troubleshooting](faq.md) — Common questions and fixes diff --git a/docs/api/notes.md b/docs/api/notes.md index 8c35ab2..6a77148 100644 --- a/docs/api/notes.md +++ b/docs/api/notes.md @@ -1,6 +1,6 @@ # Notes API -Notes implement a file-tree system. Each note is either a `file` (with HTML content) or a `folder`. Notes belong to exactly one parent scope. +Notes implement a file-tree system. Each entry is a `file` (with HTML content), a `canvas` (with JSON Canvas content), or a `folder`. Notes belong to exactly one parent scope. --- @@ -90,12 +90,31 @@ Request body (`NoteCreate`): } ``` -- `type`: `"file"` or `"folder"`. Folders have empty `content`. +- `type`: `"file"`, `"folder"`, or `"canvas"`. Folders have empty `content`. - `parentId`: ID of parent note (folder) for nested structure; `null` for root-level. -- `content`: HTML string (tiptap editor output). Empty string for folders. +- `content`: HTML string for files, a serialized JSON Canvas 1.0 object for canvases, or an empty string for folders. Returns the created Note at `201`. +### Canvas documents + +The same creation endpoint works in every scope: + +```json +{ + "name": "Research map.canvas", + "parentId": null, + "type": "canvas", + "content": "{\"nodes\":[],\"edges\":[]}" +} +``` + +Omitting canvas content initializes empty `nodes` and `edges` arrays. Creation and rename append `.canvas` if absent. Save using `PATCH /api/notes/{id}` with the serialized document in `content`. The type of an existing entry is not changed by PATCH. + +Canvas creation and content updates validate JSON Canvas node types, geometry, unique IDs, colors, file subpaths, and connection endpoints. Invalid content returns `422` before any database write. Unknown extension fields are retained. Existing HTML note behavior is unchanged. No database migration is needed: the existing `notes.type` and `notes.content` columns store the document. + +ResearchOS-created file nodes retain their portable `file` path and add `researchos.noteId` for stable references across renames and moves. Local attachments may include `researchos.mediaData` or `researchos.textContent`; group backgrounds may include `researchos.backgroundData`. These optional extensions do not alter the JSON Canvas schema. See [Canvas guide](../canvas.md) for exports and interoperability. + --- ## Update Note diff --git a/docs/canvas.md b/docs/canvas.md new file mode 100644 index 0000000..f16606e --- /dev/null +++ b/docs/canvas.md @@ -0,0 +1,63 @@ +# Canvas documents + +Use **New canvas** in a notes explorer, or right-click a folder and choose **New canvas**. Canvases appear alongside notes and folders in library, paper, website, repository, project, and experiment notes. Names end in `.canvas`; storage uses the existing notes API and database, with no migration or new service required. + +The editor stores [JSON Canvas 1.0](https://jsoncanvas.org/spec/1.0/), the open format used by [Obsidian Canvas](https://obsidian.md/help/plugins/canvas). It preserves node ordering, standard optional fields, and extension fields on import and export. + +## Cards and connections + +- **Text** adds a Markdown card. Double-click to edit. Text supports headings, lists, tasks, links, code, tables, and images. **Convert to note** creates a rich-text note and replaces the card with a reference. +- **File / canvas** adds a note or another canvas. Pick a folder to add its files, or drag entries from the explorer. Referenced notes show live content. **Edit linked note** edits the original through the existing rich-text editor; **Open file** opens its tab; **Swap file** relinks the card. **Heading / block** accepts `#Heading` or `#^block-id`. +- Nested canvases show previews, with a depth limit and circular-reference detection. Open the nested canvas to continue navigating. Missing or ambiguous file paths show an unavailable state and can be relinked. +- **Media** imports images, audio, video, PDF, or text files; local files can also be dropped. Each attachment is limited to 15 MB. Imported `.canvas` attachments become canvas documents in the current notes folder. Remote media can be referenced by an imported file node's HTTP(S) URL. +- **Web page** embeds an HTTP(S) page in a sandboxed frame. The inspector can change or copy its URL and reload it. Some sites block embedding; their external link remains available. +- **Group** creates a visual container around selected cards, or an empty group. Moving a group carries its contained cards. Set its label, color, and image background, with cover, aspect-ratio, or repeat rendering. +- Drag a side handle to another card to connect them. Dropping a new connection on empty space creates a text card. Connections support labels, colors, side selection, and arrows at either or both ends. Drag endpoints to reconnect; drop an existing endpoint on empty space to disconnect. **Follow start/end** navigates to either card. + +## Editing and navigation + +Drag a card's header or preview body to move it, and selected card borders to resize. Move groups by their headers. Text editors, links, and media controls retain their normal interactions. Drag blank space for a selection box, Shift-click for multiple selection, or use Ctrl/Command+A. Alt-drag leaves a duplicate at the original position. The inspector offers duplication, deletion, layer ordering, grouping, six alignments, row/column/grid layouts, spacing distribution, and horizontal/vertical justification. + +Use Space-drag or the middle mouse button to pan, wheel to scroll, Ctrl/Command-wheel or pinch to zoom, and the minimap to navigate. The controls include fit-all, fit-selection, 100% zoom, group navigation, and card text search. Settings expose grid snapping, object snapping, resize aspect ratio, minimap visibility, and read-only mode. + +| Shortcut | Action | +| --- | --- | +| Ctrl/Command+S | Save canvas | +| Ctrl/Command+Z | Undo | +| Ctrl/Command+Y or Ctrl/Command+Shift+Z | Redo | +| Ctrl/Command+D | Duplicate selection | +| Ctrl/Command+C / V | Copy/paste cards within the open canvas | +| Delete / Backspace | Remove selection and its connections | +| Arrow keys | Move selected cards | +| Shift+arrow | Move farther | +| Shift+1 / Shift+2 | Fit all / fit selection | +| Escape | Finish text editing or clear selection | + +Undo history holds the last 100 canvas operations and resets when the editor closes. Read-only mode prevents document changes while retaining navigation and export. + +## Saving and references + +Changes autosave after a short debounce. Writes are serialized per document, and switching tabs flushes pending changes. The directory receives each draft immediately. Unsaved content is also backed up in browser storage when available; a failed save shows **Retry save**, and reopening a canvas recovers its draft. Browser storage limits can prevent large media drafts from being backed up; API saving still works. The application warns before leaving with a pending save. + +Internal references use the note ID as an optional `researchos.noteId` extension. Renaming or moving a referenced note keeps the reference intact. File paths refresh on export. Imported paths resolve within the current source first, then across loaded sources if unique. A deleted ID does not silently resolve to a different file with the same name. References resolve only within notes loaded by the current library/project/panel. + +Canvases participate in directory search, pinning, recent notes, graph links, and backlinks. In a rich-text note, use **Embed canvas** or type `![[Name.canvas]]` to insert a preview. Ordinary wiki links can also target canvases. + +## Import and export + +- **Import .canvas** validates and replaces the current canvas, asking before replacing a nonempty document. Undo restores the previous document. Invalid imports leave it untouched. The import limit is 60 MB. +- **Download .canvas** downloads the JSON document, including ResearchOS extensions. Other JSON Canvas applications use its standard file paths and ignore extensions they do not understand. +- **Canvas + files (.zip)** packages the canvas, referenced notes and nested canvases, and local attachments/backgrounds. It strips internal note IDs so paths can resolve in another application. Export stops with a clear error if distinct files would overwrite the same archive path. Remote URLs remain references. +- **PNG** exports the full canvas at 1× or 2×, the current viewport, or a privacy image hiding text. Very large exports are capped at 64 megapixels; use the viewport or lower scale. Browser restrictions can prevent remote media or embedded pages from appearing in an image. + +Attachments are stored in optional ResearchOS extensions inside the canvas, rather than separate backend uploads. Use ZIP export when moving media to Obsidian; a standalone `.canvas` file cannot supply separate attachment files to applications that ignore those extensions. Importing an Obsidian canvas does not grant this web application access to its local vault: matching notes must already exist in ResearchOS, or be added/relinked. ZIP export is supported; ZIP/vault import is not. + +## Compatibility boundaries + +This is an implementation of the JSON Canvas format and the core visual Canvas workflow, not the Obsidian application runtime. Obsidian Bases (`.base` databases), community plugin behaviors, vault filesystem services, Obsidian-specific commands/branding, and exact mobile long-press menus are not implemented. Generic file nodes referencing those files are preserved. Browser/touch interaction uses the graph editor's gestures; desktop and touch menus are not identical to Obsidian's. + +Local SVG and HTML media are not executed. Web embeds run in sandboxed frames, HTML note previews are sanitized, and Markdown cards do not execute embedded HTML. Cyclic previews stop rather than recursively rendering forever. + +## Development checks + +From `frontend`, run `npm run test:run`, `npm run test:e2e -- canvas.spec.js`, and `npm run build`. The repository currently has a pre-existing KaTeX/Tiptap peer-version mismatch; dependency installation may require `npm install --legacy-peer-deps`. From `backend`, run `.venv/Scripts/python.exe -m pytest tests/test_canvas.py tests/test_note_batch.py tests/test_chat_and_notes_routes.py -q` on Windows. diff --git a/frontend/e2e/canvas.spec.js b/frontend/e2e/canvas.spec.js new file mode 100644 index 0000000..8ff5e88 --- /dev/null +++ b/frontend/e2e/canvas.spec.js @@ -0,0 +1,259 @@ +import { expect, test } from '@playwright/test' + +const text = (id, x = 0, value = 'A thought') => ({ id, type: 'text', x, y: 0, width: 300, height: 200, text: value }) +const makeCanvas = (nodes = [], edges = []) => JSON.stringify({ nodes, edges }) + +async function setup(page, initial = []) { + const notes = [ + { id: 'folder', name: 'Research', type: 'folder', parentId: null, content: '' }, + { id: 'summary', name: 'Summary', type: 'file', parentId: 'folder', content: '

Results

Referenced evidence

Other

More evidence

' }, + { id: 'nested', name: 'Nested.canvas', type: 'canvas', parentId: null, content: makeCanvas([text('nested-text', 0, 'Nested thought')]) }, + ...initial, + ] + const writes = [], errors = [] + page.on('pageerror', err => errors.push(err.message)) + await page.route('**/api/**', async route => { + const request = route.request(), path = new URL(request.url()).pathname + if (path === '/api/libraries') return route.fulfill({ json: [{ id: 'lib', name: 'Research' }] }) + if (path === '/api/libraries/lib/notes' && request.method() === 'POST') { + const note = { id: `created-${notes.length}`, ...request.postDataJSON() } + notes.push(note); return route.fulfill({ status: 201, json: note }) + } + if (path === '/api/libraries/lib/notes') return route.fulfill({ json: notes }) + if (path.startsWith('/api/notes/') && request.method() === 'PATCH') { + const note = notes.find(n => n.id === path.split('/').at(-1)) + Object.assign(note, request.postDataJSON()); writes.push({ id: note.id, ...request.postDataJSON() }) + return route.fulfill({ json: note }) + } + return route.fulfill({ json: [] }) + }) + await page.goto('/library/notes') + await expect(page.getByText('Nested.canvas').first()).toBeVisible() + return { notes, writes, errors } +} + +async function openCanvas(page, name = 'Map.canvas') { + await page.getByRole('button', { name: new RegExp(name.replace('.', '\\.')) }).first().click() + await expect(page.getByRole('toolbar', { name: 'Canvas tools' })).toBeVisible() +} + +test('creates a canvas in the directory and saves text through tab switches and reload', async ({ page }, testInfo) => { + const { notes, writes, errors } = await setup(page) + await page.getByTitle('New canvas in Library').click() + const input = page.getByPlaceholder('File name') + await input.fill('Map'); await input.press('Enter') + await expect(page.getByRole('toolbar', { name: 'Canvas tools' })).toBeVisible() + await page.getByRole('button', { name: '+ Text', exact: true }).click() + await page.getByRole('textbox', { name: 'Card text', exact: true }).fill('# Main idea\n\nEvidence matters.') + await openCanvas(page, 'Nested.canvas') + await expect.poll(() => writes.some(w => w.id.startsWith('created-') && w.content?.includes('Evidence matters'))).toBe(true) + await openCanvas(page) + await expect(page.locator('.canvas-card-content').getByText('Evidence matters.')).toBeVisible() + await page.reload() + await openCanvas(page) + await expect(page.locator('.canvas-card-content').getByText('Evidence matters.')).toBeVisible() + await page.screenshot({ path: testInfo.outputPath('canvas-editor.png') }) + expect(notes.find(n => n.name === 'Map.canvas').type).toBe('canvas') + expect(errors).toEqual([]) +}) + +test('embeds notes and canvases, narrows headings, and protects against recursive previews', async ({ page }) => { + const { writes, errors } = await setup(page, [{ id: 'map', name: 'Map.canvas', type: 'canvas', content: makeCanvas() }]) + await openCanvas(page) + await page.getByRole('button', { name: 'File / canvas', exact: true }).click() + await page.getByRole('dialog').getByRole('button', { name: /Summary/ }).click() + await expect(page.locator('.canvas-card-content').getByText('Referenced evidence')).toBeVisible() + await page.getByLabel('Heading / block', { exact: true }).fill('#Results') + await expect(page.locator('.canvas-card-content').getByText('More evidence')).toHaveCount(0) + await page.getByRole('button', { name: 'File / canvas', exact: true }).click() + await page.getByRole('dialog').getByRole('button', { name: /Nested.canvas/ }).click() + await expect(page.getByRole('img', { name: 'Nested canvas preview' })).toBeVisible() + await page.getByRole('button', { name: 'File / canvas', exact: true }).click() + await page.getByRole('dialog').getByRole('button', { name: /Map.canvas/ }).click() + await expect(page.getByText(/Circular canvas reference/)).toBeVisible() + await page.getByRole('button', { name: 'Save canvas', exact: true }).click() + await expect.poll(() => writes.some(w => JSON.parse(w.content).nodes.length === 3)).toBe(true) + expect(errors).toEqual([]) +}) + +test('imports full JSON Canvas, edits connections, and preserves extensions on export', async ({ page }) => { + const { writes } = await setup(page, [{ id: 'map', name: 'Map.canvas', type: 'canvas', content: makeCanvas() }]) + await openCanvas(page) + const imported = { nodes: [text('a', 0), text('b', 450)], edges: [{ id: 'edge', fromNode: 'a', toNode: 'b', fromSide: 'right', toSide: 'left', color: '5', label: 'supports', fromEnd: 'arrow', custom: 42 }], vendor: { version: 7 } } + await page.getByLabel('Import canvas file').setInputFiles({ name: 'test.canvas', mimeType: 'application/json', buffer: Buffer.from(JSON.stringify(imported)) }) + await expect(page.locator('.react-flow__edge')).toHaveCount(1) + await page.locator('.react-flow__edge').click() + await page.getByLabel('Connection label', { exact: true }).fill('contradicts') + await page.getByLabel('to endpoint', { exact: true }).selectOption('none') + await page.getByRole('button', { name: 'Save canvas', exact: true }).click() + await expect.poll(() => writes.at(-1)?.content.includes('contradicts')).toBe(true) + const saved = JSON.parse(writes.at(-1).content) + expect(saved.vendor).toEqual({ version: 7 }) + expect(saved.edges[0]).toMatchObject({ custom: 42, fromEnd: 'arrow', toEnd: 'none', label: 'contradicts' }) + await page.getByText('Export', { exact: true }).click() + const download = page.waitForEvent('download') + await page.getByRole('button', { name: 'Download .canvas', exact: true }).click() + expect((await download).suggestedFilename()).toBe('Map.canvas') +}) + +for (const surface of ['.canvas-card-title', '.canvas-card-content']) { + test(`cards follow the pointer throughout a drag from ${surface}`, async ({ page }) => { + const { writes } = await setup(page, [{ id: 'map', name: 'Map.canvas', type: 'canvas', content: makeCanvas([text('a'), text('b', 500)]) }]) + await openCanvas(page) + await page.getByText('Settings', { exact: true }).click() + await page.getByRole('checkbox', { name: 'Snap to grid', exact: true }).uncheck() + await page.getByText('Settings', { exact: true }).click() + const card = page.locator('[data-id="a"].react-flow__node') + // Test both the initial selection and a subsequent drag of the selected card. + for (let attempt = 0; attempt < 2; attempt++) { + const handle = await card.locator(surface).boundingBox() + const pointer = { x: handle.x + 25, y: handle.y + 15 } + await page.mouse.move(pointer.x, pointer.y) + await page.mouse.down() + // Cross the graph library's drag threshold before measuring movement. + await page.mouse.move(pointer.x + 2, pointer.y + 2) + await page.evaluate(() => new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)))) + const draggingStart = await card.boundingBox() + for (const distance of [25, 55, 90]) { + await page.mouse.move(pointer.x + 2 + distance, pointer.y + 2 + distance / 2, { steps: 4 }) + await expect.poll(async () => Math.abs((await card.boundingBox()).x - draggingStart.x - distance)).toBeLessThan(3) + await expect.poll(async () => Math.abs((await card.boundingBox()).y - draggingStart.y - distance / 2)).toBeLessThan(3) + } + await page.mouse.up() + } + await page.getByRole('button', { name: 'Save canvas', exact: true }).click() + await expect.poll(() => writes.length).toBeGreaterThan(0) + expect(JSON.parse(writes.at(-1).content).nodes.find(n => n.id === 'a').x).toBeGreaterThan(0) + }) +} + +test('editing card text does not drag the card', async ({ page }) => { + await setup(page, [{ id: 'map', name: 'Map.canvas', type: 'canvas', content: makeCanvas([text('a'), text('b', 500)]) }]) + await openCanvas(page) + const card = page.locator('[data-id="a"].react-flow__node') + await card.locator('.canvas-card-content').dblclick() + const editor = page.getByRole('textbox', { name: 'Card text', exact: true }) + await editor.fill('Select and edit this text') + const before = await card.boundingBox(), field = await editor.boundingBox() + await page.mouse.move(field.x + 10, field.y + 10); await page.mouse.down() + await page.mouse.move(field.x + 80, field.y + 20, { steps: 6 }); await page.mouse.up() + const after = await card.boundingBox() + expect(after.x).toBeCloseTo(before.x, 0) + expect(after.y).toBeCloseTo(before.y, 0) + await expect(editor).toHaveValue('Select and edit this text') +}) + +test('moves and resizes cards, groups selection, and undoes deletion', async ({ page }) => { + const { writes, errors } = await setup(page, [{ id: 'map', name: 'Map.canvas', type: 'canvas', content: makeCanvas([text('a'), text('b', 400)]) }]) + await openCanvas(page) + const title = page.locator('[data-id="a"] .canvas-card-title') + const rect = await title.boundingBox() + await page.mouse.move(rect.x + 30, rect.y + 10); await page.mouse.down(); await page.mouse.move(rect.x + 100, rect.y + 60, { steps: 5 }); await page.mouse.up() + await page.getByRole('button', { name: 'Save canvas', exact: true }).click() + await expect.poll(() => JSON.parse(writes.at(-1)?.content || '{}').nodes?.find(n => n.id === 'a')?.x).not.toBe(0) + await page.getByLabel('Card width', { exact: true }).fill('420') + await page.getByLabel('Card width', { exact: true }).press('Tab') + await page.locator('.canvas-editor').focus() + await page.keyboard.press('Control+a') + await page.getByRole('button', { name: 'Group selection', exact: true }).click() + await expect(page.locator('.canvas-card-group')).toHaveCount(1) + await page.getByRole('button', { name: 'Remove selection', exact: true }).click() + await expect(page.locator('.canvas-card-group')).toHaveCount(0) + await page.getByRole('button', { name: 'Undo', exact: true }).click() + await expect(page.locator('.canvas-card-group')).toHaveCount(1) + expect(errors).toEqual([]) +}) + +test('invalid imports preserve the current canvas and read-only blocks edits', async ({ page }) => { + await setup(page, [{ id: 'map', name: 'Map.canvas', type: 'canvas', content: makeCanvas([text('a')]) }]) + await openCanvas(page) + await page.getByLabel('Import canvas file').setInputFiles({ name: 'bad.canvas', mimeType: 'application/json', buffer: Buffer.from('{"nodes":[{"id":"bad"}]}') }) + await expect(page.getByRole('alert')).toContainText('Unknown canvas node type') + await expect(page.locator('.canvas-card')).toHaveCount(1) + await page.getByText('Settings', { exact: true }).click() + await page.getByRole('checkbox', { name: 'Read-only', exact: true }).check() + await expect(page.getByRole('button', { name: '+ Text', exact: true })).toBeDisabled() + await page.locator('.canvas-editor').focus() + await page.keyboard.press('Control+a'); await page.keyboard.press('Delete') + await expect(page.locator('.canvas-card')).toHaveCount(1) +}) + +test('draws connections between card sides and exports PNG and a portable bundle', async ({ page }) => { + const { writes, errors } = await setup(page, [{ id: 'map', name: 'Map.canvas', type: 'canvas', content: makeCanvas([text('a'), text('b', 440)]) }]) + await openCanvas(page) + const from = await page.locator('[data-id="a"] .react-flow__handle-right').boundingBox() + const to = await page.locator('[data-id="b"] .react-flow__handle-left').boundingBox() + await page.mouse.move(from.x + from.width / 2, from.y + from.height / 2) + await page.mouse.down(); await page.mouse.move(to.x + to.width / 2, to.y + to.height / 2, { steps: 10 }); await page.mouse.up() + await expect(page.locator('.react-flow__edge')).toHaveCount(1) + await page.getByRole('button', { name: 'Save canvas', exact: true }).click() + await expect.poll(() => JSON.parse(writes.at(-1)?.content || '{}').edges?.length).toBe(1) + expect(JSON.parse(writes.at(-1).content).edges[0]).toMatchObject({ fromNode: 'a', toNode: 'b', fromSide: 'right', toSide: 'left' }) + await page.getByText('Export', { exact: true }).click() + const png = page.waitForEvent('download') + await page.getByRole('button', { name: 'Full canvas PNG (1×)', exact: true }).click() + expect((await png).suggestedFilename()).toBe('Map.png') + const zip = page.waitForEvent('download') + await page.getByRole('button', { name: 'Canvas + files (.zip)', exact: true }).click() + expect((await zip).suggestedFilename()).toBe('Map.zip') + expect(errors).toEqual([]) +}) + +test('edits a linked note in place and embeds a canvas inside a note', async ({ page }) => { + const { notes, errors } = await setup(page, [{ id: 'map', name: 'Map.canvas', type: 'canvas', content: makeCanvas([{ id: 'ref', type: 'file', file: 'Research/Summary.md', researchos: { noteId: 'summary' }, x: 0, y: 0, width: 360, height: 300 }]) }]) + await openCanvas(page) + await page.locator('.canvas-card-title').click() + await page.getByRole('button', { name: 'Edit linked note', exact: true }).click() + const dialog = page.getByRole('dialog', { name: 'Edit Summary', exact: true }) + await dialog.locator('.tiptap').fill('Edited evidence from canvas') + await dialog.getByRole('button', { name: 'Save changes', exact: true }).click() + await expect(page.locator('.canvas-card-content')).toContainText('Edited evidence from canvas') + expect(notes.find(n => n.id === 'summary').content).toContain('Edited evidence') + await page.getByRole('button', { name: 'Open file', exact: true }).click() + await page.getByLabel('Embed canvas', { exact: true }).selectOption('nested') + await expect(page.locator('.canvas-note-embed').getByRole('img', { name: 'Nested canvas preview' })).toBeVisible() + await page.getByRole('button', { name: 'Save', exact: true }).click() + await expect.poll(() => notes.find(n => n.id === 'summary').content.includes('data-canvas-file')).toBe(true) + expect(errors).toEqual([]) +}) + +test('creates a canvas inside a notes folder and imports local media', async ({ page }) => { + const { notes, writes, errors } = await setup(page) + await page.getByRole('button', { name: /Research.*star/ }).click({ button: 'right' }) + await page.getByRole('button', { name: /New canvas$/ }).click() + const input = page.getByPlaceholder('File name') + await input.fill('Folder map'); await input.press('Enter') + await expect(page.getByRole('toolbar', { name: 'Canvas tools' })).toBeVisible() + const created = notes.find(n => n.name === 'Folder map.canvas') + expect(created.parentId).toBe('folder') + await page.getByLabel('Import media files').setInputFiles({ name: 'sample.png', mimeType: 'image/png', buffer: Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+a8WQAAAAASUVORK5CYII=', 'base64') }) + await expect(page.locator('.canvas-card-content img')).toBeVisible() + await page.getByRole('button', { name: 'Save canvas', exact: true }).click() + await expect.poll(() => writes.some(w => w.id === created.id && w.content.includes('data:image/png'))).toBe(true) + expect(errors).toEqual([]) +}) + +test('creates and persists a project canvas', async ({ page }) => { + const notes = [], errors = [] + page.on('pageerror', e => errors.push(e.message)) + await page.route('**/api/**', async route => { + const request = route.request(), path = new URL(request.url()).pathname + if (path === '/api/projects/project') return route.fulfill({ json: { id: 'project', name: 'Canvas project', description: '', status: 'active' } }) + if (path === '/api/projects/project/notes') { + if (request.method() === 'POST') { const note = { id: 'project-canvas', ...request.postDataJSON() }; notes.push(note); return route.fulfill({ status: 201, json: note }) } + return route.fulfill({ json: notes }) + } + if (path === '/api/notes/project-canvas') { Object.assign(notes[0], request.postDataJSON()); return route.fulfill({ json: notes[0] }) } + return route.fulfill({ json: [] }) + }) + await page.goto('/projects/project/notes') + await page.getByTitle('New canvas', { exact: true }).click() + const input = page.getByPlaceholder('File name') + await input.fill('Project map'); await input.press('Enter') + await expect(page.getByRole('toolbar', { name: 'Canvas tools' })).toBeVisible() + await page.getByRole('button', { name: '+ Text', exact: true }).click() + await page.getByRole('textbox', { name: 'Card text', exact: true }).fill('Project idea') + await page.getByRole('button', { name: 'Save canvas', exact: true }).click() + await expect.poll(() => notes[0]?.content.includes('Project idea')).toBe(true) + expect(errors).toEqual([]) +}) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index a12376f..72482a6 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -25,20 +25,27 @@ "@tiptap/react": "^3.20.1", "@tiptap/starter-kit": "^3.20.1", "@tiptap/suggestion": "^3.20.1", + "@xyflow/react": "^12.12.0", "d3": "^7.9.0", + "dompurify": "^3.4.17", + "html-to-image": "^1.11.13", "jszip": "^3.10.1", "katex": "^0.19.1", "lowlight": "^3.3.0", "papaparse": "^5.5.3", "react": "^18.3.1", "react-dom": "^18.3.1", + "react-is": "^18.3.1", + "react-markdown": "^10.1.0", "react-router-dom": "^7.18.4", "react-syntax-highlighter": "^16.1.1", "recharts": "^3.7.0", + "remark-gfm": "^4.0.1", "xlsx": "^0.18.5" }, "devDependencies": { "@playwright/test": "^1.52.0", + "@testing-library/dom": "^10.4.2", "@testing-library/jest-dom": "^6.8.0", "@testing-library/react": "^16.3.0", "@types/react": "^18.3.12", @@ -1532,12 +1539,11 @@ "license": "MIT" }, "node_modules/@testing-library/dom": { - "version": "10.4.1", - "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", - "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", + "version": "10.4.2", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.2.tgz", + "integrity": "sha512-yzr2S9HyAIdhz2/6qHgbs665Q7PKVcDF05vsOlHPxG1mo36gKVesdYVeDLnXgfjJ03CrKRk08knc6+E/9m8v2Q==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", @@ -2167,8 +2173,7 @@ "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@types/babel__core": { "version": "7.20.5", @@ -2238,6 +2243,15 @@ "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", "license": "MIT" }, + "node_modules/@types/d3-drag": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz", + "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, "node_modules/@types/d3-ease": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", @@ -2268,6 +2282,12 @@ "@types/d3-time": "*" } }, + "node_modules/@types/d3-selection": { + "version": "3.0.12", + "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.12.tgz", + "integrity": "sha512-Qe/KWYhEiIIxGs7HrAAjMfShxKldx19SJtr5zu53f3afPsdZNz7HHtdTLXo/kqeiWNXVycI24kSnfzBYkTzpgw==", + "license": "MIT" + }, "node_modules/@types/d3-shape": { "version": "3.1.8", "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz", @@ -2289,6 +2309,34 @@ "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", "license": "MIT" }, + "node_modules/@types/d3-transition": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz", + "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-zoom": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.9.tgz", + "integrity": "sha512-0sE1406XBYJGiqD3AusTl9ZqC//2mIXix51tbom25gDCA8ri4xnSZg28CaSE8Srl6FClqABUKfsn0qghgdepMA==", + "license": "MIT", + "dependencies": { + "@types/d3-interpolate": "*", + "@types/d3-selection": "*" + } + }, + "node_modules/@types/debug": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", + "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==", + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, "node_modules/@types/deep-eql": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", @@ -2300,9 +2348,17 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "dev": true, "license": "MIT" }, + "node_modules/@types/estree-jsx": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz", + "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==", + "license": "MIT", + "dependencies": { + "@types/estree": "*" + } + }, "node_modules/@types/hast": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", @@ -2312,6 +2368,21 @@ "@types/unist": "*" } }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "license": "MIT" + }, "node_modules/@types/prismjs": { "version": "1.26.6", "resolved": "https://registry.npmjs.org/@types/prismjs/-/prismjs-1.26.6.tgz", @@ -2322,12 +2393,14 @@ "version": "15.7.15", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "dev": true, "license": "MIT" }, "node_modules/@types/react": { "version": "18.3.28", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.28.tgz", "integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==", + "dev": true, "license": "MIT", "dependencies": { "@types/prop-types": "*", @@ -2338,11 +2411,19 @@ "version": "18.3.7", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "dev": true, "license": "MIT", "peerDependencies": { "@types/react": "^18.0.0" } }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT", + "optional": true + }, "node_modules/@types/unist": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", @@ -2355,6 +2436,12 @@ "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", "license": "MIT" }, + "node_modules/@ungap/structured-clone": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.4.0.tgz", + "integrity": "sha512-1mEZtMKPM09vDmQt5y7YvmN2+DFTP7Tg0EWXdic8/C6VRnpb33e4ghisCIE3WZjsE2N8mf+QV1Zqh7ZFYLWInQ==", + "license": "ISC" + }, "node_modules/@vitejs/plugin-react": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", @@ -2414,6 +2501,48 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/@xyflow/react": { + "version": "12.12.0", + "resolved": "https://registry.npmjs.org/@xyflow/react/-/react-12.12.0.tgz", + "integrity": "sha512-74oXI0Rgm1Eu33cHQAd74J6oVgCH9mbFyTVQbsGgp2kmWwh0HdtBr2VP04z8mbPD+M/xO69nqG5dDEt7/EN2rQ==", + "license": "MIT", + "dependencies": { + "@xyflow/system": "0.0.83", + "classcat": "^5.0.3", + "zustand": "^4.4.0" + }, + "peerDependencies": { + "@types/react": ">=17", + "@types/react-dom": ">=17", + "react": ">=17", + "react-dom": ">=17" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@xyflow/system": { + "version": "0.0.83", + "resolved": "https://registry.npmjs.org/@xyflow/system/-/system-0.0.83.tgz", + "integrity": "sha512-a//eDjez3WZF47aaPTfVUDD0OINOq+WVwBZCRTqG+m0a5euX8EwlJ4jMY1Wxz0Ocj4xOeekJ70V22Pq0ll8RJw==", + "license": "MIT", + "dependencies": { + "@types/d3-drag": "^3.0.7", + "@types/d3-interpolate": "^3.0.4", + "@types/d3-selection": "^3.0.10", + "@types/d3-transition": "^3.0.8", + "@types/d3-zoom": "^3.0.8", + "d3-drag": "^3.0.0", + "d3-interpolate": "^3.0.1", + "d3-selection": "^3.0.0", + "d3-zoom": "^3.0.0" + } + }, "node_modules/adler-32": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz", @@ -2439,7 +2568,6 @@ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=8" } @@ -2450,7 +2578,6 @@ "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=10" }, @@ -2543,6 +2670,16 @@ "postcss": "^8.1.0" } }, + "node_modules/bail": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/baseline-browser-mapping": { "version": "2.11.25", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.25.tgz", @@ -2647,6 +2784,16 @@ ], "license": "CC-BY-4.0" }, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/cfb": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz", @@ -2680,6 +2827,16 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/character-entities-legacy": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", @@ -2738,6 +2895,12 @@ "node": ">= 6" } }, + "node_modules/classcat": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/classcat/-/classcat-5.0.5.tgz", + "integrity": "sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==", + "license": "MIT" + }, "node_modules/clsx": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", @@ -2852,6 +3015,7 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, "license": "MIT" }, "node_modules/d3": { @@ -3282,7 +3446,6 @@ "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -3372,8 +3535,16 @@ "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" + }, + "node_modules/dompurify": { + "version": "3.4.17", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.17.tgz", + "integrity": "sha512-B/pSlYVHQ58kqISD/yDjuJ1yfzI34m1gF4lAod2bwSRhiN4K5Cl3H8Af7hqB2PmTL911tdEudnnXn5FnhRoz0w==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } }, "node_modules/electron-to-chromium": { "version": "1.5.438", @@ -3451,6 +3622,28 @@ "node": ">=6" } }, + "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/estree-util-is-identifier-name": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz", + "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/estree-walker": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", @@ -3477,6 +3670,12 @@ "node": ">=12.0.0" } }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, "node_modules/fast-equals": { "version": "5.4.0", "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.4.0.tgz", @@ -3657,6 +3856,46 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/hast-util-to-jsx-runtime": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz", + "integrity": "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "style-to-js": "^1.0.0", + "unist-util-position": "^5.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-whitespace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/hastscript": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-9.0.1.tgz", @@ -3702,6 +3941,22 @@ "node": ">=18" } }, + "node_modules/html-to-image": { + "version": "1.11.13", + "resolved": "https://registry.npmjs.org/html-to-image/-/html-to-image-1.11.13.tgz", + "integrity": "sha512-cuOPoI7WApyhBElTTb9oqsawRvZ0rHhaHwghRLlTuffoD1B2aDemlCruLeZrUIIdvG7gs9xeELEPm6PhuASqrg==", + "license": "MIT" + }, + "node_modules/html-url-attributes": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz", + "integrity": "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/http-proxy-agent": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", @@ -3774,6 +4029,12 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, + "node_modules/inline-style-parser": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz", + "integrity": "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==", + "license": "MIT" + }, "node_modules/internmap": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", @@ -3889,6 +4150,18 @@ "node": ">=0.12.0" } }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-potential-custom-element-name": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", @@ -4056,6 +4329,16 @@ "integrity": "sha512-InTexvtbai+2c+dEVGcc6O6P2DMVIiwD9a/i4zFroazOIYLOS6FSVmAJ7kFeumnCMx6/gQqNAkbUvgMubV4+MA==", "license": "MIT" }, + "node_modules/longest-streak": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", + "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -4099,7 +4382,6 @@ "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", "dev": true, "license": "MIT", - "peer": true, "bin": { "lz-string": "bin/bin.js" } @@ -4114,90 +4396,955 @@ "@jridgewell/sourcemap-codec": "^1.6.0" } }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, + "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", - "engines": { - "node": ">= 8" + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, + "node_modules/mdast-util-find-and-replace": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.4.tgz", + "integrity": "sha512-z4B5c6jrnJ1ltOQemhWYPBSAo6y42Z8Gt2yEgfgNTMcLSU59B8bfj8Z8AnAlJrBGLnqkdAUZ7jdDEqQw3KuipQ==", "license": "MIT", "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" + "@types/mdast": "^4.0.0", + "escape-string-regexp": "^5.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.1.0" }, - "engines": { - "node": ">=8.6" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/min-indent": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", - "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", - "dev": true, + "node_modules/mdast-util-from-markdown": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.1.0.tgz", + "integrity": "sha512-KRLgmPaP2H1ul2bpoJkOS+9pAcDAAj9Nu1mqLnh8NLeJG5tlvj42TH80PIGBpROWlRXLk0lDrWNuZCFy/tsB4A==", "license": "MIT", - "engines": { - "node": ">=4" + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark": "^4.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/mz": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", - "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", - "dev": true, + "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": { - "any-promise": "^1.0.0", - "object-assign": "^4.0.1", - "thenify-all": "^1.0.0" + "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/nanoid": { - "version": "3.3.19", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.19.tgz", - "integrity": "sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], + "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", - "bin": { - "nanoid": "bin/nanoid.cjs" + "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" }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/node-releases": { - "version": "2.0.57", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.57.tgz", - "integrity": "sha512-kQK9LGGFiHtrWiNhZtA7Qbw17AQz+dmsEKODRIVTXA9+e5MS/2gZEBhYJt13GrAz5/IOZKddH/0Z3TP/Zgo+yw==", - "dev": true, + "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", - "engines": { - "node": ">=18" + "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/normalize-path": { - "version": "3.0.0", + "node_modules/mdast-util-gfm-strikethrough": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.1.tgz", + "integrity": "sha512-OuJHqvr455pwu2OaOrir7dbzqs4jlWKOtlu9L6GjcIjjQwNyw02VntQsr/Ek6/A13vgnXfUhBekWRUP1OkNivw==", + "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", + "integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@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-jsx": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz", + "integrity": "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "parse-entities": "^4.0.0", + "stringify-entities": "^4.0.0", + "unist-util-stringify-position": "^4.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdxjs-esm": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz", + "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@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-phrasing": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", + "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-hast": { + "version": "13.2.1", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", + "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-markdown": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.2.1.tgz", + "integrity": "sha512-ThatlCBxG2GE+mljhNSfZPv56d3QrGiRh97jjYgIk80IzS4uU6d7cHAaexCYSAmUCCcOs3grJ07kX3oTGsFUOw==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^4.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-html-tag-name": "^2.0.0", + "unist-util-visit": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromark": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.3.tgz", + "integrity": "sha512-oGYfQzHSG5dOMovQcJ3fyTmZlWAWpi0XA0sJwJs+i6OT88o1+Jtw/8z0CmdowSLxGhhFK89rQ/oXph/wN02PNw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-edit-map": "^1.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-core-commonmark": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.4.tgz", + "integrity": "sha512-wxEeE8v8XVvOrxn1TZj74qYhAtTQsSqufHVS3uNVyT1MupXxhyaHk8/9xDNPh9BmL77QNDgWTkZ3RsYHB2ry7w==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-destination": "^2.0.0", + "micromark-factory-label": "^2.0.0", + "micromark-factory-space": "^2.1.0", + "micromark-factory-title": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-edit-map": "^1.0.0", + "micromark-util-html-tag-name": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.3" + } + }, + "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.2", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.2.tgz", + "integrity": "sha512-pRzm4kDTu0MjlmBkxmS9yYhw60nncfcEwu9NNdPFSQEFXS95ZKyIIyTSHu/o3ReBUrLKYEq+7YaXCRn/bPB4MA==", + "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.1", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", + "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-label": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", + "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-space": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.1.0.tgz", + "integrity": "sha512-fS8hnLIjnjvdQIj39Geug8wWsR0HrYZ43KShKxNfwT7t2LOHo/LbWZEzEaSOjwtjdCsjoE6syHhonEzW0zv0+Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-title": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", + "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-whitespace": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", + "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-chunked": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", + "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-classify-character": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", + "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-combine-extensions": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", + "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-chunked": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-numeric-character-reference": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", + "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-string": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", + "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-edit-map": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-edit-map/-/micromark-util-edit-map-1.0.0.tgz", + "integrity": "sha512-Pa2ljlsEL6sVwFaYeyrOLSYbQt73JvGbPOYFq+9AElXiSnfI5Q4465RRZ1sHv7lDjDOnGRDDaqPXqClqWK/q1Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-html-tag-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", + "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-normalize-identifier": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", + "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-resolve-all": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", + "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-subtokenize": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", + "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-types": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.3.tgz", + "integrity": "sha512-oxB2Ik03hI0gv+VNn9tnh1t1YEe9MDPptViAEgfdf3YQHsn0pzGTgCdlSCJXcwhqm8phaEuM7zeEu3QQzVBrPg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.19", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.19.tgz", + "integrity": "sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.57", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.57.tgz", + "integrity": "sha512-kQK9LGGFiHtrWiNhZtA7Qbw17AQz+dmsEKODRIVTXA9+e5MS/2gZEBhYJt13GrAz5/IOZKddH/0Z3TP/Zgo+yw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", "dev": true, @@ -4579,7 +5726,6 @@ "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", @@ -4594,8 +5740,7 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/prismjs": { "version": "1.30.0", @@ -4818,11 +5963,37 @@ } }, "node_modules/react-is": { - "version": "19.2.4", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.4.tgz", - "integrity": "sha512-W+EWGn2v0ApPKgKKCy/7s7WHXkboGcsrXE+2joLyVxkbyVQfO3MUEaUQDHoSmb8TFFrSKYa9mw64WZHNHSDzYA==", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "license": "MIT" + }, + "node_modules/react-markdown": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-10.1.0.tgz", + "integrity": "sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==", "license": "MIT", - "peer": true + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "hast-util-to-jsx-runtime": "^2.0.0", + "html-url-attributes": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.0.0", + "unified": "^11.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + }, + "peerDependencies": { + "@types/react": ">=18", + "react": ">=18" + } }, "node_modules/react-redux": { "version": "9.2.0", @@ -5051,6 +6222,72 @@ "url": "https://github.com/sponsors/wooorm" } }, + "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", + "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-rehype": { + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.2.tgz", + "integrity": "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "mdast-util-to-hast": "^13.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "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/reselect": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", @@ -5287,6 +6524,20 @@ "safe-buffer": "~5.1.0" } }, + "node_modules/stringify-entities": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "license": "MIT", + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/strip-indent": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", @@ -5300,6 +6551,24 @@ "node": ">=8" } }, + "node_modules/style-to-js": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.21.tgz", + "integrity": "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==", + "license": "MIT", + "dependencies": { + "style-to-object": "1.0.14" + } + }, + "node_modules/style-to-object": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.14.tgz", + "integrity": "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==", + "license": "MIT", + "dependencies": { + "inline-style-parser": "0.2.7" + } + }, "node_modules/sucrase": { "version": "3.35.1", "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", @@ -5537,6 +6806,26 @@ "node": ">=18" } }, + "node_modules/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/trough": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", + "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/ts-interface-checker": { "version": "0.1.13", "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", @@ -5550,6 +6839,93 @@ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, + "node_modules/unified": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", + "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "bail": "^2.0.0", + "devlop": "^1.0.0", + "extend": "^3.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-is": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", + "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.2.0.tgz", + "integrity": "sha512-fpsYs/VVoZxfGY9/j3FQpZmN8wdLJaV1Pt38AaJNZu9uKIHcFU3halCdBveEq1HGKO1OEfJY65w1zvoFV1b3qQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.1.0.tgz", + "integrity": "sha512-vKXxz6O38sKDj1mtx/k2cWvkkFBuYGx6uGrlzBkVeWCRF/F8sdI0j3Wbkz/5rVNRRuVc8UrgRdbCxNhmYXgP3w==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/update-browserslist-db": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.3.tgz", @@ -5596,6 +6972,34 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "license": "MIT" }, + "node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/victory-vendor": { "version": "37.3.6", "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz", @@ -5984,6 +7388,44 @@ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", "dev": true, "license": "ISC" + }, + "node_modules/zustand": { + "version": "4.5.7", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz", + "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==", + "license": "MIT", + "dependencies": { + "use-sync-external-store": "^1.2.2" + }, + "engines": { + "node": ">=12.7.0" + }, + "peerDependencies": { + "@types/react": ">=16.8", + "immer": ">=9.0.6", + "react": ">=16.8" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + } + } + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } } } } diff --git a/frontend/package.json b/frontend/package.json index d56a56b..a8b2aa1 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -29,20 +29,27 @@ "@tiptap/react": "^3.20.1", "@tiptap/starter-kit": "^3.20.1", "@tiptap/suggestion": "^3.20.1", + "@xyflow/react": "^12.12.0", "d3": "^7.9.0", + "dompurify": "^3.4.17", + "html-to-image": "^1.11.13", "jszip": "^3.10.1", "katex": "^0.19.1", "lowlight": "^3.3.0", "papaparse": "^5.5.3", "react": "^18.3.1", "react-dom": "^18.3.1", + "react-is": "^18.3.1", + "react-markdown": "^10.1.0", "react-router-dom": "^7.18.4", "react-syntax-highlighter": "^16.1.1", "recharts": "^3.7.0", + "remark-gfm": "^4.0.1", "xlsx": "^0.18.5" }, "devDependencies": { "@playwright/test": "^1.52.0", + "@testing-library/dom": "^10.4.2", "@testing-library/jest-dom": "^6.8.0", "@testing-library/react": "^16.3.0", "@types/react": "^18.3.12", diff --git a/frontend/src/components/NoteGraphView.jsx b/frontend/src/components/NoteGraphView.jsx index 861878e..f121856 100644 --- a/frontend/src/components/NoteGraphView.jsx +++ b/frontend/src/components/NoteGraphView.jsx @@ -1,3 +1,4 @@ +import { canvasReferences } from '../utils/jsonCanvas' import { useEffect, useRef, useCallback, useState } from 'react' import * as d3 from 'd3' import { extractWikiLinks } from './WikiLinkExtension' @@ -127,7 +128,7 @@ export default function NoteGraphView({ // ── Build graph data (filtered by visibleTypes and selected collections) ─────── const buildGraph = useCallback(() => { const fileNotes = allNotes.filter(n => { - if (n.type !== 'file') return false + if (n.type === 'folder') return false if (visibleTypes[n.source ?? 'library'] === false) return false if (hullCollections.length > 0 && (n.sourceKey ?? n.source ?? 'library') !== 'library') { const collIds = sourceKeyCollections[n.sourceKey ?? n.source ?? 'library'] || [] @@ -157,7 +158,7 @@ export default function NoteGraphView({ const links = [] for (const note of fileNotes) { - for (const { name: targetName, noteId: targetId } of extractWikiLinks(note.content || '')) { + for (const { name: targetName, noteId: targetId } of (note.type === 'canvas' ? canvasReferences(note, allNotes) : extractWikiLinks(note.content || ''))) { let target if (targetId) { target = idMap.get(targetId) diff --git a/frontend/src/components/NotesPanel.canvas.test.jsx b/frontend/src/components/NotesPanel.canvas.test.jsx new file mode 100644 index 0000000..073abe3 --- /dev/null +++ b/frontend/src/components/NotesPanel.canvas.test.jsx @@ -0,0 +1,21 @@ +import { useState } from 'react' +import { fireEvent, render, screen, waitFor } from '@testing-library/react' +import { expect, it, vi } from 'vitest' +import NotesPanel from './NotesPanel' + +vi.mock('@tiptap/react', () => ({ useEditor: () => null, EditorContent: () => null, NodeViewWrapper: () => null, ReactNodeViewRenderer: vi.fn() })) +vi.mock('./canvas/CanvasDocument', () => ({ default: ({ note }) =>
{note.name}
})) + +it('creates canvas documents through the supplied notes-scope API and opens the canvas editor', async () => { + const create = vi.fn(async data => ({ id: 'canvas1', ...data })) + function Harness() { + const [notes, setNotes] = useState([]) + return + } + render() + fireEvent.click(screen.getByTitle('New canvas')) + fireEvent.change(screen.getByPlaceholderText('File name'), { target: { value: 'Research map' } }) + fireEvent.submit(screen.getByPlaceholderText('File name').closest('form')) + await waitFor(() => expect(screen.getByTestId('canvas-document')).toHaveTextContent('Research map.canvas')) + expect(create).toHaveBeenCalledWith({ name: 'Research map.canvas', type: 'canvas', content: '{"nodes":[],"edges":[]}', parentId: null }) +}) diff --git a/frontend/src/components/NotesPanel.jsx b/frontend/src/components/NotesPanel.jsx index 1de1ec9..2fd5c2d 100644 --- a/frontend/src/components/NotesPanel.jsx +++ b/frontend/src/components/NotesPanel.jsx @@ -1,3 +1,6 @@ +import { CanvasEmbed, CanvasEmbedPicker } from './canvas/CanvasEmbed' +import CanvasDocument from './canvas/CanvasDocument' +import { EMPTY_CANVAS, canvasName } from '../utils/jsonCanvas' import { useState, useEffect, useRef, useCallback } from 'react' import { useEditor, EditorContent } from '@tiptap/react' import StarterKit from '@tiptap/starter-kit' @@ -34,7 +37,9 @@ function ToolBtn({ icon, label, active, onClick, className = '' }) { } /* ─── Tiptap WYSIWYG Editor ─── */ -function TiptapEditor({ content, onUpdate, onSave }) { +function TiptapEditor({ content, onUpdate, onSave, notes, onOpen }) { + const notesRef = useRef(notes); notesRef.current = notes + const openRef = useRef(onOpen); openRef.current = onOpen const editor = useEditor({ extensions: [ StarterKit.configure({ @@ -54,6 +59,7 @@ function TiptapEditor({ content, onUpdate, onSave }) { Highlight.configure({ multicolor: false }), Typography, Mathematics, + CanvasEmbed.configure({ getNotes: () => notesRef.current || [], onOpen: (_, id) => openRef.current?.(id) }), ], content: content || '', editorProps: { @@ -83,6 +89,7 @@ function TiptapEditor({ content, onUpdate, onSave }) { return ( <> {/* Fixed toolbar */} +