diff --git a/CHANGELOG.md b/CHANGELOG.md index 3f2c52c..5f2af2a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,64 +1,199 @@ # Changelog -All notable changes to this project will be documented in this file. +All notable changes to Graph Notes will be documented in this file. -The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), -and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +The format is based on [Keep a Changelog](https://keepachangelog.com/), and this project adheres to [Semantic Versioning](https://semver.org/). -## [0.4.0] - 2026-03-07 +## [0.9.0] — 2026-03-09 + +### Added +- **Import/Export Hub** — Export vault as ZIP, import .md folders from Obsidian/Notion +- **Keyboard Shortcuts Editor** — View, rebind, persist all keyboard shortcuts +- **Graph Analytics** — Stats dashboard with orphan detection, most-connected notes, link density +- **Note Pinning** — Pin notes to sidebar top, persisted to `.graph-notes/pinned.json` + +### Changed +- Sidebar: added 📊 Analytics action +- Command Palette: added Graph Analytics, Import/Export, Keyboard Shortcuts commands +- Backend: added `export_vault_zip`, `import_folder`, `save_shortcuts`, `load_shortcuts`, `get_pinned`, `set_pinned` + +### Dependencies +- Added `zip` crate (Rust) + +## [0.8.0] — 2026-03-09 + +### Added +- **Outline Sidebar** — Collapsible heading tree (H1–H6) with click-to-scroll and active heading tracking +- **Timeline View** — Chronological note cards grouped by date with 7d/30d/1y filters +- **Document Statistics** — Status bar with word count, characters, lines, reading time, heading count +- **Markdown Table Editor** — Visual table grid with click-to-edit cells, add/remove rows/columns, Tab navigation +- **Random Note** — 🎲 Discover random notes from sidebar or command palette +- **Link Suggestions** — Backend `suggest_links` for wikilink auto-completion + +### Changed +- Sidebar: added 📅 Timeline and 🎲 Random Note actions +- Command Palette: added Timeline, Random Note commands +- Backend: added `suggest_links`, `list_notes_by_date`, `random_note` commands + +### Dependencies +- Added `rand` crate (Rust) + +## [0.7.0] — 2026-03-09 + +### Added +- **Canvas Whiteboard** — Freeform visual thinking surface powered by `@blinksgg/canvas` with card/text nodes, drag, zoom, save/load +- **Database Views** — Notion-style table/gallery/list views from frontmatter properties with sort/filter +- **Backlink Context** — Paragraph-level excerpts around wikilink mentions in backlinks panel +- **Dataview Queries** — Inline ` ```dataview TABLE ... SORT ... ``` ` blocks rendering live query tables +- **Git Sync** — commit/push/pull panel with status indicator, changed file list, repo initialization + +### Changed +- **GraphView rewritten** using `@blinksgg/canvas` (replaces custom HTML5 Canvas force simulation) +- Sidebar: added Database, Whiteboard quick actions +- Command Palette: added Database View, New Whiteboard, Git Sync commands +- Backlinks now use backend `get_backlink_context` for paragraph excerpts + +### Dependencies +- Added `@blinksgg/canvas`, `jotai`, `graphology`, `d3-force` + +## [0.6.0] — 2026-03-09 + +### Added +- **Tabbed Editor** — Multi-note tab bar with drag-reorder, close buttons, active tab highlighting +- **Note Refactoring** — Extract selection to new note (replaces with wikilink), merge notes (appends + updates links) +- **Encrypted Notes** — AES-256-GCM password protection with Argon2 key derivation, lock/unlock button in editor +- **Spaced Repetition Flashcards** — Study mode from `?? question :: answer ??` syntax, SM-2 scheduling, difficulty ratings +- **Heading Folding** — Fold state persistence per note via `.graph-notes/folds.json` +- **Custom CSS Snippets** — Live-preview CSS editor, persisted in `~/.config/graph-notes/custom.css` +- **Workspace Layouts** — Save/restore window arrangements in `.graph-notes/workspaces/` +- **Embeddable Widgets** — `{{progress:N}}` progress bars, `{{counter:N}}` badges, `{{toggle:on/off}}` indicators + +### Changed +- Editor supports right-click context menu for refactoring operations +- Command Palette extended with Flashcards, Custom CSS, and Save Workspace +- Sidebar quick actions include Flashcards +- Custom CSS loaded on mount via `useCustomCssInit` hook + +### Dependencies +- Added `aes-gcm`, `argon2`, `rand`, `base64` for encryption + +## [0.5.0] — 2026-03-08 + +### Added +- **Kanban Board** — Visual task board from `- [ ]` / `- [/]` / `- [x]` items across vault, with drag-and-drop between Todo/In Progress/Done columns +- **Focus / Zen Mode** — Distraction-free writing (`⌘⇧F`): hides sidebar, breadcrumbs, meta, centers content at max 720px +- **Note Version History** — Auto-snapshots every 5 min, timeline sidebar with inline diff viewer (add/remove highlighting) +- **PDF Export** — Print-styled export via browser print dialog with clean typography +- **Global Search & Replace** — Find/replace text across vault with dry-run preview before applying (`⌘H`) +- **Local Backlink Graph** — Mini force-directed canvas in preview showing current note's 1-hop link connections +- **Writing Goals** — Per-note word count targets with gradient progress bar (red→yellow→green) +- **Syntax-Highlighted Code Blocks** — highlight.js with 8 languages, copy-to-clipboard button, dark theme + +### Changed +- Editor supports focus mode (hides chrome, centers content) +- Command Palette extended with Kanban, Focus Mode, Search & Replace, Export as PDF +- Sidebar quick actions include Kanban Board +- Auto-snapshot on save (throttled to 1 per 5 min) + +### Dependencies +- Added `highlight.js` for syntax highlighting + +## [0.4.0] — 2026-03-08 + +### Added +- **Frontmatter & Properties Panel** — YAML `---` fenced metadata with inline key-value editor (collapsible panel below breadcrumbs) +- **Table of Contents** — Auto-generated outline from headings, shown alongside preview mode with active heading highlight +- **Mermaid Diagram Rendering** — Fenced `mermaid` code blocks render as SVG diagrams in preview mode (lazy-loaded) +- **Image & Attachment Support** — Paste images from clipboard, stored in `_attachments/` directory with `![](path)` markdown +- **Slash Commands** — Type `/` at line start to open inline formatting menu (14 commands: headings, lists, code blocks, mermaid, tables) +- **Calendar View** — Visual month grid for daily notes with dot indicators, "Today" button, and click-to-create +- **Theme Picker** — 5 built-in themes (Dark Purple, Dark Emerald, Dark Ocean, Dark Rose, Light) with live preview, persisted +- **Export to HTML** — Export current note as styled standalone HTML file + +### Changed +- Editor now includes PropertiesPanel, TableOfContents sidebar, and SlashMenu +- Command Palette extended with Calendar, Theme, and Export HTML commands +- Sidebar quick actions include Calendar View +- Added `⌘T` keyboard shortcut for Theme Picker + +### Dependencies +- Added `mermaid` for diagram rendering + +## [0.3.0] — 2026-03-08 + +### Added +- **Split Editor** — Open two notes side by side with a draggable divider (right-click → "Open in split") +- **Wikilink Hover Preview** — Hover over `[[wikilinks]]` to see a floating preview card with note content and link count +- **Note Transclusion** — `![[note-name]]` embeds the content of another note inline, with recursive depth limiting (3 levels) +- **Vault Switcher** — Click sidebar brand to switch between recent vaults or open a new folder +- **Drag & Drop File Organization** — Drag notes between folders in the sidebar file tree +- **Breadcrumb Navigation** — Path breadcrumbs shown above the editor for nested notes +- **Note Templates** — Create notes from templates in `_templates/` directory via Command Palette (supports `{{title}}` and `{{date}}` variables) +- **Recent Notes** — Last 5 recently opened notes shown in the sidebar +- **Favorites** — Pin notes as favorites (right-click → "Favorite"), persisted per vault in `.graph-notes/favorites.json` +- **Open in Split Pane** — Right-click context menu option to open a note in a side-by-side view + +### Changed +- Note view now uses `SplitView` component, supporting both single-pane and dual-pane editing +- Context menu expanded with "Favorite" and "Open in split" actions, plus visual divider +- Command Palette shows template commands when available +- `LinkPreview` component renders as a global overlay for all hover previews + +## [0.2.0] — 2026-03-08 + +### Added +- **Full-Text Search** — Vault-wide content search in the sidebar (debounced, with context snippets and result ranking) +- **Command Palette** — `⌘K` / `Ctrl+K` opens a fuzzy search palette for notes, commands, and content +- **Keyboard Shortcuts** — `⌘N` new note, `⌘G` graph view, `⌘D` daily note, `⌘E` toggle edit/preview, `⌘\` toggle sidebar +- **Note Rename** — Right-click context menu on notes in sidebar for inline rename with automatic wikilink updates across vault +- **Note Delete** — Right-click context menu with confirmation dialog; navigates away if active note deleted +- **Tags System** — `#tag` extraction from notes, sidebar tags section with click-to-filter, emerald-colored tag pills in editor +- **Graph Filtering** — Filter bar to highlight matching nodes, focus mode (1-hop neighborhood), orphan node toggle +- **Inline Markdown Styling** — Headings (`# ## ###`) render at proper sizes in edit mode, `**bold**`, `*italic*`, `` `code` `` styled inline +- **List Continuation** — Pressing Enter after `- item` auto-inserts bullet on next line +- **Tab Indent/Outdent** — Tab and Shift+Tab for list item indentation +- **Collapsible Sidebar** — Toggle sidebar visibility with `⌘\` + +### Changed +- Edit/Preview mode is now global (shared via context), toggled with `⌘E` from anywhere +- Search input shows `⌘K` hint for command palette discovery + +## [0.1.0] — 2026-03-07 + +### Added +- **Tauri v2 Desktop App** — Local-first note-taking with full filesystem access via `tauri-plugin-fs` +- **Contenteditable Editor** — Rich inline editing with `[[wikilink]]` token chips (compact pills that unwrap on backspace/delete) +- **Wikilink Autocomplete** — Type `[[` to fuzzy-search and link notes; creates new notes if no match found +- **Force-Directed Graph View** — Canvas-based visualization with semantic zoom (circles → rounded-rect cards with note previews) +- **Graph Interactions** — Single-click animates zoom to node, double-click opens note, drag to reposition nodes +- **shadcn-Inspired Design System** — Zinc-based neutrals, purple accent gradients, focus rings, spring transitions +- **Sidebar** — Recursive file tree with search, collapsible folders, active-state indicators, note count badge +- **Backlinks Panel** — Lists all notes linking to current page with highlighted context snippets +- **Markdown Preview** — Toggle between edit and rendered preview modes with inline wikilink rendering +- **Daily Notes** — Auto-generated daily journal entries accessible from sidebar shortcut +- **Auto-Save** — Debounced 500ms save on every keystroke +- **Custom Scrollbars** — Minimal 5px scrollbars matching the dark theme + +## [0.4.0] - 2026-03-07 (origin) ### Added - **VaultCache** (`src-tauri/src/cache.rs`): In-memory note cache with mtime-based invalidation -- **`init_vault_cache`**: Eagerly scan all `.md` files and populate cache on startup -- **`get_cache_stats`**: Return cache hits/misses/entry count for diagnostics -- **Cache-backed commands**: `read_note`, `read_note_with_meta`, `build_graph`, `search_vault` now read from cache -- **Frontend LRU cache** (`src/lib/noteCache.ts`): Cache last 20 notes with stale-while-revalidate -- **File watcher** (`notify` crate): Foundation for filesystem change detection and cache invalidation +- **`init_vault_cache`/`get_cache_stats`**: Startup scan + diagnostics +- **Cache-backed commands**: `read_note`, `build_graph`, `search_vault` from cache +- **Frontend LRU cache** (`src/lib/noteCache.ts`): 20-note stale-while-revalidate +- **File watcher** (`notify` crate): FS change detection foundation -### Changed -- `build_graph` iterates cached entries instead of walking disk (O(1) vs O(n) on subsequent calls) -- `search_vault` iterates cached entries instead of reading every file from disk - -## [0.3.0] - 2026-03-07 +## [0.3.0] - 2026-03-07 (origin) ### Added -- **Frontmatter Parsing**: Rust backend parses YAML frontmatter (`title`, `tags`, `created`, `modified`) on read -- **`read_note_with_meta`**: New IPC command returns content, parsed metadata, body (without frontmatter), and heading list -- **TypeScript Frontmatter Module** (`src/lib/frontmatter.ts`): Client-side parse/serialize with `extractHeadings()` -- **Outline / TOC Panel**: Right-side panel showing document headings with click-to-scroll and smooth highlight animation -- **Tabbed Right Panel**: Switch between Outline and Backlinks views in the note editor -- **Note Templates**: `_templates/` folder support; Command Palette lists templates and creates notes with `{{title}}`/`{{date}}` replacement -- **Image Attachments**: Drag-and-drop images onto editor; saves to `vault/attachments/` and inserts markdown image link -- **`list_templates`**: Rust command to scan `_templates/` folder for `.md` template files -- **`save_attachment`**: Rust command to save binary data to `vault/attachments/` with deduplication +- **Frontmatter Parsing**: YAML metadata, `read_note_with_meta`, TOC panel, templates, attachments -## [0.2.0] - 2026-03-07 +## [0.2.0] - 2026-03-07 (origin) ### Added -- **Command Palette** (`Ctrl+K`): Fuzzy search notes and run commands with keyboard navigation (↑↓ Enter Esc) -- **Keyboard Shortcuts**: `Ctrl+N` new note, `Ctrl+G` graph view, `Ctrl+D` daily note -- **Full-Text Search**: Rust-powered vault content search with context snippets, displayed in sidebar -- **Note Rename**: Right-click context menu in sidebar for renaming notes with automatic wikilink updates across vault -- **Note Delete**: Context menu delete with confirmation dialog -- **Editor: Heading Scaling**: H1–H3 headings render at proportional sizes in edit mode -- **Editor: Task Lists**: Interactive checkboxes for `- [ ]` / `- [x]` syntax, clickable to toggle -- **Editor: Inline Code**: Backtick-quoted text styled with monospace font and accent color -- **Editor: Markdown Preview**: Styled headings, code blocks, and checkbox rendering in preview mode -- **Graph Filtering**: Filter graph by folder and minimum link count with a dedicated filter bar +- **Command Palette**, keyboard shortcuts, full-text search, rename/delete, graph filtering -### Changed -- Sidebar search upgraded to show both filename matches and content search results -- Graph view header now reflects filtered node/edge counts - -## [0.1.0] - 2025-06-01 +## [0.1.0] - 2025-06-01 (origin) ### Added -- Tauri 2 desktop application with React 19 + Vite 7 -- Contenteditable editor with inline wikilink tokens -- Wikilink autocomplete dropdown (`[[` trigger) -- Force-directed graph view with semantic zoom (circles → cards) -- Sidebar with file tree, search filtering, and quick actions -- Backlinks panel with context snippets -- Daily notes with auto-creation -- Auto-save with debounced writes -- Custom CSS design system (dark theme, glassmorphism, purple accents) +- Tauri 2 + React 19, editor, wikilinks, graph, sidebar, backlinks, daily notes diff --git a/package-lock.json b/package-lock.json index 6e2069f..3ad48b2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,18 +1,24 @@ { "name": "graph-notes", - "version": "0.1.0", + "version": "0.6.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "graph-notes", - "version": "0.1.0", + "version": "0.6.0", "dependencies": { + "@blinksgg/canvas": "file:../blinksgg/gg-antifragile/packages/canvas", "@tauri-apps/api": "^2", "@tauri-apps/plugin-dialog": "^2", "@tauri-apps/plugin-fs": "^2", "@tauri-apps/plugin-opener": "^2", + "d3-force": "^3.0.0", + "graphology": "^0.26.0", + "highlight.js": "^11.11.1", + "jotai": "^2.18.0", "marked": "^15.0.0", + "mermaid": "^11.12.3", "react": "^19.1.0", "react-dom": "^19.1.0", "react-router-dom": "^7.6.0" @@ -28,11 +34,73 @@ "vite": "^7.0.4" } }, + "../blinksgg/gg-antifragile/packages/canvas": { + "name": "@blinksgg/canvas", + "version": "0.13.0", + "dependencies": { + "@supabase/supabase-js": "^2.49.5", + "@use-gesture/react": "^10.3.1", + "debug": "^4.4.3", + "graphology": "^0.26.0", + "graphology-types": "^0.24.8" + }, + "devDependencies": { + "@babel/core": "^7.29.0", + "@babel/preset-react": "^7.28.5", + "@babel/preset-typescript": "^7.28.5", + "@blocknote/core": "^0.45.0", + "@blocknote/react": "^0.45.0", + "@blocknote/shadcn": "^0.45.0", + "@tanstack/react-query": "^5.17.0", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.2", + "@testing-library/user-event": "^14.6.1", + "@types/d3-force": "^3.0.10", + "@types/debug": "^4.1.12", + "@types/node": "^24.5.2", + "@types/react": "^19.1.13", + "@types/react-dom": "^19.1.9", + "babel-plugin-react-compiler": "^1.0.0", + "d3-force": "^3.0.0", + "esbuild-plugin-babel": "^0.2.3", + "jotai": "^2.6.0", + "jsdom": "^26.1.0", + "react": "^19.1.1", + "react-dom": "^19.1.1", + "tsup": "^8.0.0", + "typescript": "^5.3.0", + "vitest": "^3.2.1" + }, + "peerDependencies": { + "@blocknote/core": "^0.45.0", + "@blocknote/react": "^0.45.0", + "@blocknote/shadcn": "^0.45.0", + "@tanstack/react-query": "^5.17.0", + "d3-force": "^3.0.0", + "jotai": "^2.6.0", + "jotai-tanstack-query": "*", + "react": "^19.0.0", + "react-dom": "^19.0.0" + } + }, + "node_modules/@antfu/install-pkg": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@antfu/install-pkg/-/install-pkg-1.1.0.tgz", + "integrity": "sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==", + "license": "MIT", + "dependencies": { + "package-manager-detector": "^1.3.0", + "tinyexec": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, "node_modules/@babel/code-frame": { "version": "7.29.0", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", @@ -47,7 +115,7 @@ "version": "7.29.0", "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -57,7 +125,7 @@ "version": "7.29.0", "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.29.0", @@ -88,7 +156,7 @@ "version": "7.29.1", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/parser": "^7.29.0", @@ -105,7 +173,7 @@ "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/compat-data": "^7.28.6", @@ -122,7 +190,7 @@ "version": "7.28.0", "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -132,7 +200,7 @@ "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/traverse": "^7.28.6", @@ -146,7 +214,7 @@ "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/helper-module-imports": "^7.28.6", @@ -174,7 +242,7 @@ "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -184,7 +252,7 @@ "version": "7.28.5", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -194,7 +262,7 @@ "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -204,7 +272,7 @@ "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/template": "^7.28.6", @@ -218,7 +286,7 @@ "version": "7.29.0", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/types": "^7.29.0" @@ -266,7 +334,7 @@ "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.28.6", @@ -281,7 +349,7 @@ "version": "7.29.0", "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.29.0", @@ -300,7 +368,7 @@ "version": "7.29.0", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.27.1", @@ -310,6 +378,55 @@ "node": ">=6.9.0" } }, + "node_modules/@blinksgg/canvas": { + "resolved": "../blinksgg/gg-antifragile/packages/canvas", + "link": true + }, + "node_modules/@braintree/sanitize-url": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-7.1.2.tgz", + "integrity": "sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA==", + "license": "MIT" + }, + "node_modules/@chevrotain/cst-dts-gen": { + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/@chevrotain/cst-dts-gen/-/cst-dts-gen-11.1.2.tgz", + "integrity": "sha512-XTsjvDVB5nDZBQB8o0o/0ozNelQtn2KrUVteIHSlPd2VAV2utEb6JzyCJaJ8tGxACR4RiBNWy5uYUHX2eji88Q==", + "license": "Apache-2.0", + "dependencies": { + "@chevrotain/gast": "11.1.2", + "@chevrotain/types": "11.1.2", + "lodash-es": "4.17.23" + } + }, + "node_modules/@chevrotain/gast": { + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/@chevrotain/gast/-/gast-11.1.2.tgz", + "integrity": "sha512-Z9zfXR5jNZb1Hlsd/p+4XWeUFugrHirq36bKzPWDSIacV+GPSVXdk+ahVWZTwjhNwofAWg/sZg58fyucKSQx5g==", + "license": "Apache-2.0", + "dependencies": { + "@chevrotain/types": "11.1.2", + "lodash-es": "4.17.23" + } + }, + "node_modules/@chevrotain/regexp-to-ast": { + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/@chevrotain/regexp-to-ast/-/regexp-to-ast-11.1.2.tgz", + "integrity": "sha512-nMU3Uj8naWer7xpZTYJdxbAs6RIv/dxYzkYU8GSwgUtcAAlzjcPfX1w+RKRcYG8POlzMeayOQ/znfwxEGo5ulw==", + "license": "Apache-2.0" + }, + "node_modules/@chevrotain/types": { + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-11.1.2.tgz", + "integrity": "sha512-U+HFai5+zmJCkK86QsaJtoITlboZHBqrVketcO2ROv865xfCMSFpELQoz1GkX5GzME8pTa+3kbKrZHQtI0gdbw==", + "license": "Apache-2.0" + }, + "node_modules/@chevrotain/utils": { + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/@chevrotain/utils/-/utils-11.1.2.tgz", + "integrity": "sha512-4mudFAQ6H+MqBTfqLmU7G1ZwRzCLfJEooL/fsF6rCX5eePMbGhoy5n4g+G4vlh2muDcsCTJtL+uKbOzWxs5LHA==", + "license": "Apache-2.0" + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.27.3", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", @@ -752,11 +869,28 @@ "node": ">=18" } }, + "node_modules/@iconify/types": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz", + "integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==", + "license": "MIT" + }, + "node_modules/@iconify/utils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@iconify/utils/-/utils-3.1.0.tgz", + "integrity": "sha512-Zlzem1ZXhI1iHeeERabLNzBHdOa4VhQbqAcOQaMKuTuyZCpwKbC2R4Dd0Zo3g9EAc+Y4fiarO8HIHRAth7+skw==", + "license": "MIT", + "dependencies": { + "@antfu/install-pkg": "^1.1.0", + "@iconify/types": "^2.0.0", + "mlly": "^1.8.0" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", @@ -767,7 +901,7 @@ "version": "2.3.5", "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", @@ -778,7 +912,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=6.0.0" @@ -788,20 +922,29 @@ "version": "1.5.5", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.31", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@mermaid-js/parser": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@mermaid-js/parser/-/parser-1.0.0.tgz", + "integrity": "sha512-vvK0Hi/VWndxoh03Mmz6wa1KDriSPjS2XMZL/1l19HFwygiObEEoEwSDxOqyLzzAI6J2PU3261JjTMTO7x+BPw==", + "license": "MIT", + "dependencies": { + "langium": "^4.0.0" + } + }, "node_modules/@rolldown/pluginutils": { "version": "1.0.0-beta.27", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", @@ -1730,6 +1873,259 @@ "@babel/types": "^7.28.2" } }, + "node_modules/@types/d3": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz", + "integrity": "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==", + "license": "MIT", + "dependencies": { + "@types/d3-array": "*", + "@types/d3-axis": "*", + "@types/d3-brush": "*", + "@types/d3-chord": "*", + "@types/d3-color": "*", + "@types/d3-contour": "*", + "@types/d3-delaunay": "*", + "@types/d3-dispatch": "*", + "@types/d3-drag": "*", + "@types/d3-dsv": "*", + "@types/d3-ease": "*", + "@types/d3-fetch": "*", + "@types/d3-force": "*", + "@types/d3-format": "*", + "@types/d3-geo": "*", + "@types/d3-hierarchy": "*", + "@types/d3-interpolate": "*", + "@types/d3-path": "*", + "@types/d3-polygon": "*", + "@types/d3-quadtree": "*", + "@types/d3-random": "*", + "@types/d3-scale": "*", + "@types/d3-scale-chromatic": "*", + "@types/d3-selection": "*", + "@types/d3-shape": "*", + "@types/d3-time": "*", + "@types/d3-time-format": "*", + "@types/d3-timer": "*", + "@types/d3-transition": "*", + "@types/d3-zoom": "*" + } + }, + "node_modules/@types/d3-array": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", + "license": "MIT" + }, + "node_modules/@types/d3-axis": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.6.tgz", + "integrity": "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-brush": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.6.tgz", + "integrity": "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-chord": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.6.tgz", + "integrity": "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-contour": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.6.tgz", + "integrity": "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==", + "license": "MIT", + "dependencies": { + "@types/d3-array": "*", + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==", + "license": "MIT" + }, + "node_modules/@types/d3-dispatch": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.7.tgz", + "integrity": "sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==", + "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-dsv": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz", + "integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" + }, + "node_modules/@types/d3-fetch": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.7.tgz", + "integrity": "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==", + "license": "MIT", + "dependencies": { + "@types/d3-dsv": "*" + } + }, + "node_modules/@types/d3-force": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.10.tgz", + "integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==", + "license": "MIT" + }, + "node_modules/@types/d3-format": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz", + "integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==", + "license": "MIT" + }, + "node_modules/@types/d3-geo": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.0.tgz", + "integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==", + "license": "MIT", + "dependencies": { + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-hierarchy": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz", + "integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" + }, + "node_modules/@types/d3-polygon": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.2.tgz", + "integrity": "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==", + "license": "MIT" + }, + "node_modules/@types/d3-quadtree": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz", + "integrity": "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==", + "license": "MIT" + }, + "node_modules/@types/d3-random": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.3.tgz", + "integrity": "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==", + "license": "MIT" + }, + "node_modules/@types/d3-selection": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz", + "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==", + "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", + "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, + "node_modules/@types/d3-time-format": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.3.tgz", + "integrity": "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "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.8", + "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz", + "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==", + "license": "MIT", + "dependencies": { + "@types/d3-interpolate": "*", + "@types/d3-selection": "*" + } + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -1737,11 +2133,17 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/geojson": { + "version": "7946.0.16", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", + "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", + "license": "MIT" + }, "node_modules/@types/react": { "version": "19.2.14", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "csstype": "^3.2.2" @@ -1757,6 +2159,13 @@ "@types/react": "^19.2.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/@vitejs/plugin-react": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", @@ -1778,11 +2187,23 @@ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/baseline-browser-mapping": { "version": "2.10.0", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz", "integrity": "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "bin": { "baseline-browser-mapping": "dist/cli.cjs" @@ -1795,7 +2216,7 @@ "version": "4.28.1", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", - "dev": true, + "devOptional": true, "funding": [ { "type": "opencollective", @@ -1829,7 +2250,7 @@ "version": "1.0.30001777", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001777.tgz", "integrity": "sha512-tmN+fJxroPndC74efCdp12j+0rk0RHwV5Jwa1zWaFVyw2ZxAuPeG8ZgWC3Wz7uSjT3qMRQ5XHZ4COgQmsCMJAQ==", - "dev": true, + "devOptional": true, "funding": [ { "type": "opencollective", @@ -1846,11 +2267,52 @@ ], "license": "CC-BY-4.0" }, + "node_modules/chevrotain": { + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-11.1.2.tgz", + "integrity": "sha512-opLQzEVriiH1uUQ4Kctsd49bRoFDXGGSC4GUqj7pGyxM3RehRhvTlZJc1FL/Flew2p5uwxa1tUDWKzI4wNM8pg==", + "license": "Apache-2.0", + "dependencies": { + "@chevrotain/cst-dts-gen": "11.1.2", + "@chevrotain/gast": "11.1.2", + "@chevrotain/regexp-to-ast": "11.1.2", + "@chevrotain/types": "11.1.2", + "@chevrotain/utils": "11.1.2", + "lodash-es": "4.17.23" + } + }, + "node_modules/chevrotain-allstar": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/chevrotain-allstar/-/chevrotain-allstar-0.3.1.tgz", + "integrity": "sha512-b7g+y9A0v4mxCW1qUhf3BSVPg+/NvGErk/dOkrDaHA0nQIQGAtrOjlX//9OQtRlSCy+x9rfB5N8yC71lH1nvMw==", + "license": "MIT", + "dependencies": { + "lodash-es": "^4.17.21" + }, + "peerDependencies": { + "chevrotain": "^11.0.0" + } + }, + "node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/confbox": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", + "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", + "license": "MIT" + }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/cookie": { @@ -1866,18 +2328,532 @@ "url": "https://opencollective.com/express" } }, + "node_modules/cose-base": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-1.0.3.tgz", + "integrity": "sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==", + "license": "MIT", + "dependencies": { + "layout-base": "^1.0.0" + } + }, "node_modules/csstype": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "dev": true, + "devOptional": true, + "license": "MIT" + }, + "node_modules/cytoscape": { + "version": "3.33.1", + "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.33.1.tgz", + "integrity": "sha512-iJc4TwyANnOGR1OmWhsS9ayRS3s+XQ185FmuHObThD+5AeJCakAAbWv8KimMTt08xCCLNgneQwFp+JRJOr9qGQ==", + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/cytoscape-cose-bilkent": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cytoscape-cose-bilkent/-/cytoscape-cose-bilkent-4.1.0.tgz", + "integrity": "sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ==", + "license": "MIT", + "dependencies": { + "cose-base": "^1.0.0" + }, + "peerDependencies": { + "cytoscape": "^3.2.0" + } + }, + "node_modules/cytoscape-fcose": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cytoscape-fcose/-/cytoscape-fcose-2.2.0.tgz", + "integrity": "sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ==", + "license": "MIT", + "dependencies": { + "cose-base": "^2.2.0" + }, + "peerDependencies": { + "cytoscape": "^3.2.0" + } + }, + "node_modules/cytoscape-fcose/node_modules/cose-base": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-2.2.0.tgz", + "integrity": "sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g==", + "license": "MIT", + "dependencies": { + "layout-base": "^2.0.0" + } + }, + "node_modules/cytoscape-fcose/node_modules/layout-base": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-2.0.1.tgz", + "integrity": "sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg==", + "license": "MIT" + }, + "node_modules/d3": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/d3/-/d3-7.9.0.tgz", + "integrity": "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==", + "license": "ISC", + "dependencies": { + "d3-array": "3", + "d3-axis": "3", + "d3-brush": "3", + "d3-chord": "3", + "d3-color": "3", + "d3-contour": "4", + "d3-delaunay": "6", + "d3-dispatch": "3", + "d3-drag": "3", + "d3-dsv": "3", + "d3-ease": "3", + "d3-fetch": "3", + "d3-force": "3", + "d3-format": "3", + "d3-geo": "3", + "d3-hierarchy": "3", + "d3-interpolate": "3", + "d3-path": "3", + "d3-polygon": "3", + "d3-quadtree": "3", + "d3-random": "3", + "d3-scale": "4", + "d3-scale-chromatic": "3", + "d3-selection": "3", + "d3-shape": "3", + "d3-time": "3", + "d3-time-format": "4", + "d3-timer": "3", + "d3-transition": "3", + "d3-zoom": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-axis": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz", + "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-brush": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz", + "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "3", + "d3-transition": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-chord": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz", + "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==", + "license": "ISC", + "dependencies": { + "d3-path": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-contour": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz", + "integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==", + "license": "ISC", + "dependencies": { + "d3-array": "^3.2.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==", + "license": "ISC", + "dependencies": { + "delaunator": "5" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz", + "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==", + "license": "ISC", + "dependencies": { + "commander": "7", + "iconv-lite": "0.6", + "rw": "1" + }, + "bin": { + "csv2json": "bin/dsv2json.js", + "csv2tsv": "bin/dsv2dsv.js", + "dsv2dsv": "bin/dsv2dsv.js", + "dsv2json": "bin/dsv2json.js", + "json2csv": "bin/json2dsv.js", + "json2dsv": "bin/json2dsv.js", + "json2tsv": "bin/json2dsv.js", + "tsv2csv": "bin/dsv2dsv.js", + "tsv2json": "bin/dsv2json.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-fetch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz", + "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==", + "license": "ISC", + "dependencies": { + "d3-dsv": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-force": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz", + "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-quadtree": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz", + "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-geo": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.1.tgz", + "integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2.5.0 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-hierarchy": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz", + "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-polygon": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz", + "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-quadtree": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz", + "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-random": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz", + "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-sankey": { + "version": "0.12.3", + "resolved": "https://registry.npmjs.org/d3-sankey/-/d3-sankey-0.12.3.tgz", + "integrity": "sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-array": "1 - 2", + "d3-shape": "^1.2.0" + } + }, + "node_modules/d3-sankey/node_modules/d3-array": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.12.1.tgz", + "integrity": "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==", + "license": "BSD-3-Clause", + "dependencies": { + "internmap": "^1.0.0" + } + }, + "node_modules/d3-sankey/node_modules/d3-path": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz", + "integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==", + "license": "BSD-3-Clause" + }, + "node_modules/d3-sankey/node_modules/d3-shape": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz", + "integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-path": "1" + } + }, + "node_modules/d3-sankey/node_modules/internmap": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-1.0.1.tgz", + "integrity": "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==", + "license": "ISC" + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-interpolate": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/dagre-d3-es": { + "version": "7.0.13", + "resolved": "https://registry.npmjs.org/dagre-d3-es/-/dagre-d3-es-7.0.13.tgz", + "integrity": "sha512-efEhnxpSuwpYOKRm/L5KbqoZmNNukHa/Flty4Wp62JRvgH2ojwVgPgdYyr4twpieZnyRDdIH7PY2mopX26+j2Q==", + "license": "MIT", + "dependencies": { + "d3": "^7.9.0", + "lodash-es": "^4.17.21" + } + }, + "node_modules/dayjs": { + "version": "1.11.19", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.19.tgz", + "integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==", "license": "MIT" }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -1891,6 +2867,15 @@ } } }, + "node_modules/delaunator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz", + "integrity": "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==", + "license": "ISC", + "dependencies": { + "robust-predicates": "^3.0.2" + } + }, "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", @@ -1901,11 +2886,23 @@ "node": ">=8" } }, + "node_modules/dompurify": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.2.tgz", + "integrity": "sha512-6obghkliLdmKa56xdbLOpUZ43pAR6xFy1uOrxBaIDjT+yaRuuybLjGS9eVBoSR/UPU5fq3OXClEHLJNGvbxKpQ==", + "license": "(MPL-2.0 OR Apache-2.0)", + "engines": { + "node": ">=20" + }, + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, "node_modules/electron-to-chromium": { "version": "1.5.307", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.307.tgz", "integrity": "sha512-5z3uFKBWjiNR44nFcYdkcXjKMbg5KXNdciu7mhTPo9tB7NbqSNP2sSnGR+fqknZSCwKkBN+oxiiajWs4dT6ORg==", - "dev": true, + "devOptional": true, "license": "ISC" }, "node_modules/enhanced-resolve": { @@ -1968,12 +2965,21 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=6" } }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, "node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -2011,7 +3017,7 @@ "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -2024,6 +3030,61 @@ "dev": true, "license": "ISC" }, + "node_modules/graphology": { + "version": "0.26.0", + "resolved": "https://registry.npmjs.org/graphology/-/graphology-0.26.0.tgz", + "integrity": "sha512-8SSImzgUUYC89Z042s+0r/vMibY7GX/Emz4LDO5e7jYXhuoWfHISPFJYjpRLUSJGq6UQ6xlenvX1p/hJdfXuXg==", + "license": "MIT", + "dependencies": { + "events": "^3.3.0" + }, + "peerDependencies": { + "graphology-types": ">=0.24.0" + } + }, + "node_modules/graphology-types": { + "version": "0.24.8", + "resolved": "https://registry.npmjs.org/graphology-types/-/graphology-types-0.24.8.tgz", + "integrity": "sha512-hDRKYXa8TsoZHjgEaysSRyPdT6uB78Ci8WnjgbStlQysz7xR52PInxNsmnB7IBOM1BhikxkNyCVEFgmPKnpx3Q==", + "license": "MIT", + "peer": true + }, + "node_modules/hachure-fill": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/hachure-fill/-/hachure-fill-0.5.2.tgz", + "integrity": "sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg==", + "license": "MIT" + }, + "node_modules/highlight.js": { + "version": "11.11.1", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.11.1.tgz", + "integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/jiti": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", @@ -2034,18 +3095,47 @@ "jiti": "lib/jiti-cli.mjs" } }, + "node_modules/jotai": { + "version": "2.18.0", + "resolved": "https://registry.npmjs.org/jotai/-/jotai-2.18.0.tgz", + "integrity": "sha512-XI38kGWAvtxAZ+cwHcTgJsd+kJOJGf3OfL4XYaXWZMZ7IIY8e53abpIHvtVn1eAgJ5dlgwlGFnP4psrZ/vZbtA==", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@babel/core": ">=7.0.0", + "@babel/template": ">=7.0.0", + "@types/react": ">=17.0.0", + "react": ">=17.0.0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "@babel/template": { + "optional": true + }, + "@types/react": { + "optional": true + }, + "react": { + "optional": true + } + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", - "dev": true, + "devOptional": true, "license": "MIT", "bin": { "jsesc": "bin/jsesc" @@ -2058,7 +3148,7 @@ "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, + "devOptional": true, "license": "MIT", "bin": { "json5": "lib/cli.js" @@ -2067,6 +3157,59 @@ "node": ">=6" } }, + "node_modules/katex": { + "version": "0.16.38", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.38.tgz", + "integrity": "sha512-cjHooZUmIAUmDsHBN+1n8LaZdpmbj03LtYeYPyuYB7OuloiaeaV6N4LcfjcnHVzGWjVQmKrxxTrpDcmSzEZQwQ==", + "funding": [ + "https://opencollective.com/katex", + "https://github.com/sponsors/katex" + ], + "license": "MIT", + "dependencies": { + "commander": "^8.3.0" + }, + "bin": { + "katex": "cli.js" + } + }, + "node_modules/katex/node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/khroma": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/khroma/-/khroma-2.1.0.tgz", + "integrity": "sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==" + }, + "node_modules/langium": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/langium/-/langium-4.2.1.tgz", + "integrity": "sha512-zu9QWmjpzJcomzdJQAHgDVhLGq5bLosVak1KVa40NzQHXfqr4eAHupvnPOVXEoLkg6Ocefvf/93d//SB7du4YQ==", + "license": "MIT", + "dependencies": { + "chevrotain": "~11.1.1", + "chevrotain-allstar": "~0.3.1", + "vscode-languageserver": "~9.0.1", + "vscode-languageserver-textdocument": "~1.0.11", + "vscode-uri": "~3.1.0" + }, + "engines": { + "node": ">=20.10.0", + "npm": ">=10.2.3" + } + }, + "node_modules/layout-base": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-1.0.2.tgz", + "integrity": "sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==", + "license": "MIT" + }, "node_modules/lightningcss": { "version": "1.31.1", "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.31.1.tgz", @@ -2328,11 +3471,17 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/lodash-es": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.23.tgz", + "integrity": "sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==", + "license": "MIT" + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dev": true, + "devOptional": true, "license": "ISC", "dependencies": { "yallist": "^3.0.2" @@ -2360,11 +3509,63 @@ "node": ">= 18" } }, + "node_modules/mermaid": { + "version": "11.12.3", + "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-11.12.3.tgz", + "integrity": "sha512-wN5ZSgJQIC+CHJut9xaKWsknLxaFBwCPwPkGTSUYrTiHORWvpT8RxGk849HPnpUAQ+/9BPRqYb80jTpearrHzQ==", + "license": "MIT", + "dependencies": { + "@braintree/sanitize-url": "^7.1.1", + "@iconify/utils": "^3.0.1", + "@mermaid-js/parser": "^1.0.0", + "@types/d3": "^7.4.3", + "cytoscape": "^3.29.3", + "cytoscape-cose-bilkent": "^4.1.0", + "cytoscape-fcose": "^2.2.0", + "d3": "^7.9.0", + "d3-sankey": "^0.12.3", + "dagre-d3-es": "7.0.13", + "dayjs": "^1.11.18", + "dompurify": "^3.2.5", + "katex": "^0.16.22", + "khroma": "^2.1.0", + "lodash-es": "^4.17.23", + "marked": "^16.2.1", + "roughjs": "^4.6.6", + "stylis": "^4.3.6", + "ts-dedent": "^2.2.0", + "uuid": "^11.1.0" + } + }, + "node_modules/mermaid/node_modules/marked": { + "version": "16.4.2", + "resolved": "https://registry.npmjs.org/marked/-/marked-16.4.2.tgz", + "integrity": "sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/mlly": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.1.tgz", + "integrity": "sha512-SnL6sNutTwRWWR/vcmCYHSADjiEesp5TGQQ0pXyLhW5IoeibRlF/CbSLailbB3CNqJUk9cVJ9dUDnbD7GrcHBQ==", + "license": "MIT", + "dependencies": { + "acorn": "^8.16.0", + "pathe": "^2.0.3", + "pkg-types": "^1.3.1", + "ufo": "^1.6.3" + } + }, "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, + "devOptional": true, "license": "MIT" }, "node_modules/nanoid": { @@ -2390,14 +3591,32 @@ "version": "2.0.36", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.36.tgz", "integrity": "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==", - "dev": true, + "devOptional": true, + "license": "MIT" + }, + "node_modules/package-manager-detector": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.6.0.tgz", + "integrity": "sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==", + "license": "MIT" + }, + "node_modules/path-data-parser": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/path-data-parser/-/path-data-parser-0.1.0.tgz", + "integrity": "sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w==", + "license": "MIT" + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", "license": "MIT" }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, + "devOptional": true, "license": "ISC" }, "node_modules/picomatch": { @@ -2413,6 +3632,33 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pkg-types": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", + "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", + "license": "MIT", + "dependencies": { + "confbox": "^0.1.8", + "mlly": "^1.7.4", + "pathe": "^2.0.1" + } + }, + "node_modules/points-on-curve": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/points-on-curve/-/points-on-curve-0.2.0.tgz", + "integrity": "sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A==", + "license": "MIT" + }, + "node_modules/points-on-path": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/points-on-path/-/points-on-path-0.2.1.tgz", + "integrity": "sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g==", + "license": "MIT", + "dependencies": { + "path-data-parser": "0.1.0", + "points-on-curve": "0.2.0" + } + }, "node_modules/postcss": { "version": "8.5.8", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", @@ -2511,6 +3757,12 @@ "react-dom": ">=18" } }, + "node_modules/robust-predicates": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz", + "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==", + "license": "Unlicense" + }, "node_modules/rollup": { "version": "4.59.0", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", @@ -2556,6 +3808,30 @@ "fsevents": "~2.3.2" } }, + "node_modules/roughjs": { + "version": "4.6.6", + "resolved": "https://registry.npmjs.org/roughjs/-/roughjs-4.6.6.tgz", + "integrity": "sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ==", + "license": "MIT", + "dependencies": { + "hachure-fill": "^0.5.2", + "path-data-parser": "^0.1.0", + "points-on-curve": "^0.2.0", + "points-on-path": "^0.2.1" + } + }, + "node_modules/rw": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", + "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==", + "license": "BSD-3-Clause" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, "node_modules/scheduler": { "version": "0.27.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", @@ -2566,7 +3842,7 @@ "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, + "devOptional": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -2588,6 +3864,12 @@ "node": ">=0.10.0" } }, + "node_modules/stylis": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.6.tgz", + "integrity": "sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==", + "license": "MIT" + }, "node_modules/tailwindcss": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.1.tgz", @@ -2609,6 +3891,15 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/tinyexec": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", + "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", @@ -2626,6 +3917,15 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "node_modules/ts-dedent": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ts-dedent/-/ts-dedent-2.2.0.tgz", + "integrity": "sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==", + "license": "MIT", + "engines": { + "node": ">=6.10" + } + }, "node_modules/typescript": { "version": "5.8.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", @@ -2640,11 +3940,17 @@ "node": ">=14.17" } }, + "node_modules/ufo": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.3.tgz", + "integrity": "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==", + "license": "MIT" + }, "node_modules/update-browserslist-db": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", - "dev": true, + "devOptional": true, "funding": [ { "type": "opencollective", @@ -2671,6 +3977,19 @@ "browserslist": ">= 4.21.0" } }, + "node_modules/uuid": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, "node_modules/vite": { "version": "7.3.1", "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", @@ -2746,11 +4065,60 @@ } } }, + "node_modules/vscode-jsonrpc": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz", + "integrity": "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/vscode-languageserver": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-9.0.1.tgz", + "integrity": "sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==", + "license": "MIT", + "dependencies": { + "vscode-languageserver-protocol": "3.17.5" + }, + "bin": { + "installServerIntoExtension": "bin/installServerIntoExtension" + } + }, + "node_modules/vscode-languageserver-protocol": { + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz", + "integrity": "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==", + "license": "MIT", + "dependencies": { + "vscode-jsonrpc": "8.2.0", + "vscode-languageserver-types": "3.17.5" + } + }, + "node_modules/vscode-languageserver-textdocument": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.12.tgz", + "integrity": "sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==", + "license": "MIT" + }, + "node_modules/vscode-languageserver-types": { + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz", + "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==", + "license": "MIT" + }, + "node_modules/vscode-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz", + "integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==", + "license": "MIT" + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true, + "devOptional": true, "license": "ISC" } } diff --git a/package.json b/package.json index 1b3fee6..e8cf630 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "graph-notes", "private": true, - "version": "0.1.0", + "version": "0.9.0", "type": "module", "scripts": { "dev": "vite", @@ -10,23 +10,29 @@ "tauri": "tauri" }, "dependencies": { + "@blinksgg/canvas": "file:../blinksgg/gg-antifragile/packages/canvas", + "@tauri-apps/api": "^2", + "@tauri-apps/plugin-dialog": "^2", + "@tauri-apps/plugin-fs": "^2", + "@tauri-apps/plugin-opener": "^2", + "d3-force": "^3.0.0", + "graphology": "^0.26.0", + "highlight.js": "^11.11.1", + "jotai": "^2.18.0", + "marked": "^15.0.0", + "mermaid": "^11.12.3", "react": "^19.1.0", "react-dom": "^19.1.0", - "react-router-dom": "^7.6.0", - "@tauri-apps/api": "^2", - "@tauri-apps/plugin-opener": "^2", - "@tauri-apps/plugin-fs": "^2", - "@tauri-apps/plugin-dialog": "^2", - "marked": "^15.0.0" + "react-router-dom": "^7.6.0" }, "devDependencies": { + "@tailwindcss/vite": "^4.2.1", + "@tauri-apps/cli": "^2", "@types/react": "^19.1.8", "@types/react-dom": "^19.1.6", "@vitejs/plugin-react": "^4.6.0", - "@tailwindcss/vite": "^4.2.1", "tailwindcss": "^4.2.1", "typescript": "~5.8.3", - "vite": "^7.0.4", - "@tauri-apps/cli": "^2" + "vite": "^7.0.4" } } \ No newline at end of file diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 7940c22..001f414 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "graph-notes" -version = "0.1.0" +version = "0.9.0" description = "A graph-based note-taking app" authors = ["you"] edition = "2021" @@ -22,4 +22,8 @@ serde_json = "1" walkdir = "2" regex = "1" chrono = "0.4" -notify = { version = "6", features = ["macos_kqueue"] } +aes-gcm = "0.10" +argon2 = "0.5" +rand = "0.8" +base64 = "0.22" +zip = "2" diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 8666344..37bb4cb 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,15 +1,10 @@ use serde::{Deserialize, Serialize}; use std::fs; use std::path::{Path, PathBuf}; -use std::collections::HashMap; -use std::sync::{Arc, Mutex}; use walkdir::WalkDir; use regex::Regex; use chrono::Local; -mod cache; -use cache::{VaultCache, SharedCache}; - #[derive(Debug, Serialize, Deserialize, Clone)] pub struct NoteEntry { pub path: String, @@ -38,43 +33,11 @@ pub struct GraphEdge { pub target: String, } -#[derive(Debug, Serialize, Deserialize)] -pub struct SearchResult { - pub path: String, - pub name: String, - pub context: String, - pub line_number: usize, -} - -#[derive(Debug, Serialize, Deserialize, Clone, Default)] -pub struct NoteMeta { - pub title: Option, - pub tags: Vec, - pub created: Option, - pub modified: Option, - pub extra: HashMap, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct NoteWithMeta { - pub content: String, - pub meta: NoteMeta, - pub body: String, // content without frontmatter - pub headings: Vec, -} - -#[derive(Debug, Serialize, Deserialize, Clone)] -pub struct HeadingEntry { - pub level: usize, - pub text: String, - pub line: usize, -} - -pub fn normalize_note_name(name: &str) -> String { +fn normalize_note_name(name: &str) -> String { name.trim().to_lowercase() } -pub fn extract_wikilinks(content: &str) -> Vec { +fn extract_wikilinks(content: &str) -> Vec { let re = Regex::new(r"\[\[([^\]|]+)(?:\|[^\]]+)?\]\]").unwrap(); re.captures_iter(content) .map(|cap| cap[1].trim().to_string()) @@ -132,12 +95,7 @@ fn list_notes(vault_path: String) -> Result, String> { } #[tauri::command] -fn read_note(cache_state: tauri::State<'_, SharedCache>, vault_path: String, relative_path: String) -> Result { - let mut cache = cache_state.lock().map_err(|e| e.to_string())?; - if let Some(entry) = cache.get(&relative_path) { - return Ok(entry.content.clone()); - } - // Fallback to direct read +fn read_note(vault_path: String, relative_path: String) -> Result { let full_path = Path::new(&vault_path).join(&relative_path); fs::read_to_string(&full_path).map_err(|e| format!("Failed to read note: {}", e)) } @@ -165,38 +123,56 @@ fn delete_note(vault_path: String, relative_path: String) -> Result<(), String> } #[tauri::command] -fn build_graph(cache_state: tauri::State<'_, SharedCache>, _vault_path: String) -> Result { - let mut cache = cache_state.lock().map_err(|e| e.to_string())?; - - // Ensure cache is populated - if cache.entries.is_empty() { - cache.scan_all(); +fn build_graph(vault_path: String) -> Result { + let vault = Path::new(&vault_path); + if !vault.exists() { + return Err("Vault path does not exist".to_string()); } let mut nodes: Vec = Vec::new(); let mut edges: Vec = Vec::new(); - // Build note name → path mapping from cache - let mut note_map: HashMap = HashMap::new(); - for (rel_path, cached) in cache.entries.iter() { - note_map.insert(normalize_note_name(&cached.name), rel_path.clone()); + // Collect all notes + let mut note_map: std::collections::HashMap = std::collections::HashMap::new(); + + for entry in WalkDir::new(vault) + .into_iter() + .filter_map(|e| e.ok()) + .filter(|e| e.path().extension().map_or(false, |ext| ext == "md")) + { + let rel_path = entry.path().strip_prefix(vault).unwrap_or(entry.path()); + let rel_str = rel_path.to_string_lossy().to_string(); + let name = rel_path + .file_stem() + .unwrap_or_default() + .to_string_lossy() + .to_string(); + + note_map.insert(normalize_note_name(&name), rel_str.clone()); + nodes.push(GraphNode { - id: rel_path.clone(), - label: cached.name.clone(), - path: rel_path.clone(), - link_count: cached.links.len(), + id: rel_str.clone(), + label: name, + path: rel_str, + link_count: 0, }); } - // Build edges from cached links - for (rel_path, cached) in cache.entries.iter() { - for link in &cached.links { - let normalized = normalize_note_name(link); - if let Some(target_path) = note_map.get(&normalized) { - edges.push(GraphEdge { - source: rel_path.clone(), - target: target_path.clone(), - }); + // Parse links and build edges + for node in &mut nodes { + let full_path = vault.join(&node.path); + if let Ok(content) = fs::read_to_string(&full_path) { + let links = extract_wikilinks(&content); + node.link_count = links.len(); + + for link in &links { + let normalized = normalize_note_name(link); + if let Some(target_path) = note_map.get(&normalized) { + edges.push(GraphEdge { + source: node.id.clone(), + target: target_path.clone(), + }); + } } } } @@ -204,233 +180,6 @@ fn build_graph(cache_state: tauri::State<'_, SharedCache>, _vault_path: String) Ok(GraphData { nodes, edges }) } -#[tauri::command] -fn search_vault(cache_state: tauri::State<'_, SharedCache>, _vault_path: String, query: String) -> Result, String> { - let mut cache = cache_state.lock().map_err(|e| e.to_string())?; - - // Ensure cache is populated - if cache.entries.is_empty() { - cache.scan_all(); - } - - let query_lower = query.to_lowercase(); - let mut results: Vec = Vec::new(); - - for (rel_path, cached) in cache.entries.iter() { - let mut file_count = 0; - for (i, line) in cached.content.lines().enumerate() { - if line.to_lowercase().contains(&query_lower) { - let context = line.trim().to_string(); - let context_display = if context.len() > 120 { - format!("{}…", &context[..120]) - } else { - context - }; - - results.push(SearchResult { - path: rel_path.clone(), - name: cached.name.clone(), - context: context_display, - line_number: i + 1, - }); - - file_count += 1; - if file_count >= 3 { - break; - } - } - } - } - - results.truncate(50); - Ok(results) -} - -#[tauri::command] -fn rename_note(vault_path: String, old_path: String, new_name: String) -> Result { - let vault = Path::new(&vault_path); - let old_full = vault.join(&old_path); - if !old_full.is_file() { - return Err("Note not found".to_string()); - } - - // Compute new path (same directory, new name) - let parent = Path::new(&old_path).parent().unwrap_or(Path::new("")); - let new_file = format!("{}.md", new_name.trim()); - let new_rel = if parent == Path::new("") { - new_file.clone() - } else { - format!("{}/{}", parent.to_string_lossy(), new_file) - }; - let new_full = vault.join(&new_rel); - - if new_full.exists() { - return Err("A note with that name already exists".to_string()); - } - - // Rename the file - fs::rename(&old_full, &new_full).map_err(|e| format!("Failed to rename: {}", e))?; - - // Update wikilinks across vault: [[old_name]] → [[new_name]] - let old_stem = Path::new(&old_path) - .file_stem() - .unwrap_or_default() - .to_string_lossy() - .to_string(); - let new_stem = new_name.trim().to_string(); - - if old_stem != new_stem { - let link_re = Regex::new(&format!( - r"\[\[{}(\|[^\]]+)?\]\]", - regex::escape(&old_stem) - )).map_err(|e| e.to_string())?; - - for entry in WalkDir::new(vault) - .into_iter() - .filter_map(|e| e.ok()) - .filter(|e| e.path().extension().map_or(false, |ext| ext == "md")) - { - if let Ok(content) = fs::read_to_string(entry.path()) { - let updated = link_re.replace_all(&content, |caps: ®ex::Captures| { - match caps.get(1) { - Some(alias) => format!("[[{}{}", new_stem, alias.as_str()), - None => format!("[[{}]]", new_stem), - } - }).to_string(); - - if updated != content { - let _ = fs::write(entry.path(), &updated); - } - } - } - } - - Ok(new_rel) -} - -/* ── Frontmatter Parsing ───────────────────────────────────── */ - -pub fn parse_frontmatter(content: &str) -> (NoteMeta, String) { - let trimmed = content.trim_start(); - if !trimmed.starts_with("---") { - return (NoteMeta::default(), content.to_string()); - } - - // Find closing --- - let after_open = &trimmed[3..]; - let close_pos = after_open.find("\n---"); - if close_pos.is_none() { - return (NoteMeta::default(), content.to_string()); - } - let close_pos = close_pos.unwrap(); - let yaml_block = &after_open[..close_pos].trim(); - let body_start = 3 + close_pos + 4; // skip opening --- + yaml + \n--- - let body = trimmed[body_start..].trim_start_matches('\n').to_string(); - - let mut meta = NoteMeta::default(); - - // Simple YAML key: value parser (handles strings and arrays) - for line in yaml_block.lines() { - let line = line.trim(); - if line.is_empty() || line.starts_with('#') { - continue; - } - if let Some(colon_pos) = line.find(':') { - let key = line[..colon_pos].trim().to_lowercase(); - let value = line[colon_pos + 1..].trim().to_string(); - - match key.as_str() { - "title" => meta.title = Some(value.trim_matches('"').trim_matches('\'').to_string()), - "created" => meta.created = Some(value.trim_matches('"').trim_matches('\'').to_string()), - "modified" => meta.modified = Some(value.trim_matches('"').trim_matches('\'').to_string()), - "tags" => { - // Handle [tag1, tag2] or tag1, tag2 - let cleaned = value.trim_start_matches('[').trim_end_matches(']'); - meta.tags = cleaned.split(',') - .map(|t| t.trim().trim_matches('"').trim_matches('\'').to_string()) - .filter(|t| !t.is_empty()) - .collect(); - } - _ => { meta.extra.insert(key, value); } - } - } - } - - (meta, body) -} - -pub fn extract_headings(content: &str) -> Vec { - let heading_re = Regex::new(r"^(#{1,6})\s+(.+)$").unwrap(); - content.lines().enumerate().filter_map(|(i, line)| { - heading_re.captures(line).map(|caps| HeadingEntry { - level: caps[1].len(), - text: caps[2].trim().to_string(), - line: i + 1, - }) - }).collect() -} - -#[tauri::command] -fn read_note_with_meta(cache_state: tauri::State<'_, SharedCache>, _vault_path: String, relative_path: String) -> Result { - let mut cache = cache_state.lock().map_err(|e| e.to_string())?; - if let Some(entry) = cache.get(&relative_path) { - return Ok(NoteWithMeta { - content: entry.content.clone(), - meta: entry.meta.clone(), - body: entry.body.clone(), - headings: entry.headings.clone(), - }); - } - Err(format!("Note not found in cache: {}", relative_path)) -} - -#[tauri::command] -fn list_templates(vault_path: String) -> Result, String> { - let templates_dir = PathBuf::from(&vault_path).join("_templates"); - if !templates_dir.exists() { - return Ok(vec![]); - } - let mut templates = Vec::new(); - for entry in fs::read_dir(&templates_dir).map_err(|e| e.to_string())? { - let entry = entry.map_err(|e| e.to_string())?; - let path = entry.path(); - if path.extension().and_then(|e| e.to_str()) == Some("md") { - if let Some(name) = path.file_stem().and_then(|s| s.to_str()) { - templates.push(name.to_string()); - } - } - } - templates.sort(); - Ok(templates) -} - -#[tauri::command] -fn save_attachment(vault_path: String, filename: String, data: Vec) -> Result { - let attachments = PathBuf::from(&vault_path).join("attachments"); - fs::create_dir_all(&attachments) - .map_err(|e| format!("Failed to create attachments dir: {}", e))?; - - // Deduplicate filename if it already exists - let mut target = attachments.join(&filename); - if target.exists() { - let stem = target.file_stem().and_then(|s| s.to_str()).unwrap_or("file").to_string(); - let ext = target.extension().and_then(|s| s.to_str()).unwrap_or("png").to_string(); - let mut counter = 1u32; - loop { - let new_name = format!("{}_{}.{}", stem, counter, ext); - target = attachments.join(&new_name); - if !target.exists() { break; } - counter += 1; - } - } - - fs::write(&target, &data) - .map_err(|e| format!("Failed to write attachment: {}", e))?; - - let rel = format!("attachments/{}", target.file_name().unwrap().to_str().unwrap()); - Ok(rel) -} - #[tauri::command] fn get_or_create_daily(vault_path: String) -> Result { let today = Local::now().format("%Y-%m-%d").to_string(); @@ -480,6 +229,11 @@ fn dirs_config_path() -> PathBuf { Path::new(&home).join(".config").join("graph-notes").join("vault_path") } +fn dirs_config_dir() -> PathBuf { + let home = std::env::var("HOME").unwrap_or_else(|_| ".".to_string()); + Path::new(&home).join(".config").join("graph-notes") +} + #[tauri::command] fn ensure_vault(vault_path: String) -> Result<(), String> { let vault = Path::new(&vault_path); @@ -489,58 +243,1669 @@ fn ensure_vault(vault_path: String) -> Result<(), String> { Ok(()) } +/* ── Full-Text Search ──────────────────────────────────────── */ + #[derive(Debug, Serialize, Deserialize)] -pub struct CacheStats { - pub entry_count: usize, - pub hits: u64, - pub misses: u64, +pub struct SearchResult { + pub path: String, + pub name: String, + pub line_number: usize, + pub context: String, + pub score: usize, } #[tauri::command] -fn init_vault_cache(cache_state: tauri::State<'_, SharedCache>, vault_path: String) -> Result { - let mut cache = cache_state.lock().map_err(|e| e.to_string())?; - *cache = VaultCache::new(&vault_path); - cache.scan_all(); - Ok(cache.entries.len()) +fn search_vault(vault_path: String, query: String) -> Result, String> { + let vault = Path::new(&vault_path); + if !vault.exists() { + return Err("Vault path does not exist".to_string()); + } + + let query_lower = query.to_lowercase(); + let mut results: Vec = Vec::new(); + + for entry in WalkDir::new(vault) + .into_iter() + .filter_map(|e| e.ok()) + .filter(|e| e.path().extension().map_or(false, |ext| ext == "md")) + { + let rel_path = entry.path().strip_prefix(vault).unwrap_or(entry.path()); + let rel_str = rel_path.to_string_lossy().to_string(); + let name = rel_path + .file_stem() + .unwrap_or_default() + .to_string_lossy() + .to_string(); + + if let Ok(content) = fs::read_to_string(entry.path()) { + let content_lower = content.to_lowercase(); + let match_count = content_lower.matches(&query_lower).count(); + + if match_count > 0 { + // Find the first matching line for context + for (i, line) in content.lines().enumerate() { + if line.to_lowercase().contains(&query_lower) { + results.push(SearchResult { + path: rel_str.clone(), + name: name.clone(), + line_number: i + 1, + context: line.trim().chars().take(200).collect(), + score: match_count, + }); + break; + } + } + } + } + } + + // Sort by score (most matches first), then by name + results.sort_by(|a, b| b.score.cmp(&a.score).then_with(|| a.name.cmp(&b.name))); + Ok(results) +} + +/* ── Rename & Relink ───────────────────────────────────────── */ + +#[tauri::command] +fn rename_note(vault_path: String, old_path: String, new_path: String) -> Result<(), String> { + let vault = Path::new(&vault_path); + let old_full = vault.join(&old_path); + let new_full = vault.join(&new_path); + + if !old_full.exists() { + return Err("Source note does not exist".to_string()); + } + if new_full.exists() { + return Err("A note with that name already exists".to_string()); + } + + // Ensure destination directory exists + if let Some(parent) = new_full.parent() { + fs::create_dir_all(parent).map_err(|e| format!("Failed to create directory: {}", e))?; + } + + fs::rename(&old_full, &new_full).map_err(|e| format!("Failed to rename note: {}", e)) } #[tauri::command] -fn get_cache_stats(cache_state: tauri::State<'_, SharedCache>) -> Result { - let cache = cache_state.lock().map_err(|e| e.to_string())?; - Ok(CacheStats { - entry_count: cache.entries.len(), - hits: cache.hits, - misses: cache.misses, - }) +fn update_wikilinks(vault_path: String, old_name: String, new_name: String) -> Result { + let vault = Path::new(&vault_path); + let mut updated_count = 0; + + for entry in WalkDir::new(vault) + .into_iter() + .filter_map(|e| e.ok()) + .filter(|e| e.path().extension().map_or(false, |ext| ext == "md")) + { + if let Ok(content) = fs::read_to_string(entry.path()) { + // Replace [[old_name]] and [[old_name|display]] patterns + let pattern = format!(r"\[\[{}\]\]", regex::escape(&old_name)); + let pattern_with_alias = format!(r"\[\[{}\|", regex::escape(&old_name)); + + let re1 = Regex::new(&pattern).unwrap(); + let re2 = Regex::new(&pattern_with_alias).unwrap(); + + if re1.is_match(&content) || re2.is_match(&content) { + let new_content = re1.replace_all(&content, format!("[[{}]]", new_name)); + let new_content = re2.replace_all(&new_content, format!("[[{}|", new_name)); + fs::write(entry.path(), new_content.as_ref()) + .map_err(|e| format!("Failed to update links: {}", e))?; + updated_count += 1; + } + } + } + + Ok(updated_count) +} + +/* ── Tags ──────────────────────────────────────────────────── */ + +#[derive(Debug, Serialize, Deserialize)] +pub struct TagInfo { + pub tag: String, + pub count: usize, + pub notes: Vec, +} + +fn extract_tags(content: &str) -> Vec { + let re = Regex::new(r"(?:^|[\s,;(])(#[a-zA-Z][a-zA-Z0-9_/-]*)").unwrap(); + let mut tags: Vec = re + .captures_iter(content) + .map(|cap| cap[1].to_string()) + .collect(); + tags.sort(); + tags.dedup(); + tags +} + +#[tauri::command] +fn list_tags(vault_path: String) -> Result, String> { + let vault = Path::new(&vault_path); + if !vault.exists() { + return Err("Vault path does not exist".to_string()); + } + + let mut tag_map: std::collections::HashMap> = std::collections::HashMap::new(); + + for entry in WalkDir::new(vault) + .into_iter() + .filter_map(|e| e.ok()) + .filter(|e| e.path().extension().map_or(false, |ext| ext == "md")) + { + let rel_path = entry.path().strip_prefix(vault).unwrap_or(entry.path()); + let rel_str = rel_path.to_string_lossy().to_string(); + + if let Ok(content) = fs::read_to_string(entry.path()) { + for tag in extract_tags(&content) { + tag_map.entry(tag).or_default().push(rel_str.clone()); + } + } + } + + let mut tags: Vec = tag_map + .into_iter() + .map(|(tag, notes)| TagInfo { + tag, + count: notes.len(), + notes, + }) + .collect(); + + tags.sort_by(|a, b| b.count.cmp(&a.count).then_with(|| a.tag.cmp(&b.tag))); + Ok(tags) +} + +/* ── Note Preview ────────────────────────────────────────── */ + +#[tauri::command] +fn read_note_preview(vault_path: String, note_path: String, max_chars: Option) -> Result { + let full = Path::new(&vault_path).join(¬e_path); + let content = fs::read_to_string(&full).map_err(|e| format!("Read failed: {}", e))?; + let limit = max_chars.unwrap_or(200); + + // Strip markdown formatting for clean preview + let cleaned: String = content + .lines() + .filter(|l| !l.trim().starts_with('#')) // remove headings + .map(|l| { + let s = l.to_string(); + s + }) + .collect::>() + .join(" "); + + let re_wikilink = Regex::new(r"\[\[([^\]|]+)(?:\|[^\]]+)?\]\]").unwrap(); + let cleaned = re_wikilink.replace_all(&cleaned, "$1").to_string(); + let re_fmt = Regex::new(r"[*_~`]").unwrap(); + let cleaned = re_fmt.replace_all(&cleaned, "").to_string(); + let cleaned = cleaned.trim().to_string(); + + if cleaned.len() > limit { + Ok(format!("{}...", &cleaned[..limit])) + } else { + Ok(cleaned) + } +} + +/* ── Vault Management ────────────────────────────────────── */ + +#[tauri::command] +fn list_recent_vaults() -> Result, String> { + let path = dirs_config_dir().join("recent_vaults.json"); + if !path.exists() { + return Ok(vec![]); + } + let content = fs::read_to_string(&path).map_err(|e| e.to_string())?; + let vaults: Vec = serde_json::from_str(&content).unwrap_or_default(); + Ok(vaults) +} + +#[tauri::command] +fn add_vault(vault_path: String) -> Result<(), String> { + let config_path = dirs_config_dir().join("recent_vaults.json"); + let mut vaults: Vec = if config_path.exists() { + let content = fs::read_to_string(&config_path).unwrap_or_default(); + serde_json::from_str(&content).unwrap_or_default() + } else { + vec![] + }; + + // Remove if already exists, then prepend + vaults.retain(|v| v != &vault_path); + vaults.insert(0, vault_path); + vaults.truncate(10); // Keep last 10 + + if let Some(parent) = config_path.parent() { + fs::create_dir_all(parent).map_err(|e| e.to_string())?; + } + let json = serde_json::to_string_pretty(&vaults).map_err(|e| e.to_string())?; + fs::write(&config_path, json).map_err(|e| e.to_string()) +} + +/* ── Templates ───────────────────────────────────────────── */ + +#[derive(Debug, Serialize, Deserialize)] +pub struct TemplateInfo { + pub name: String, + pub path: String, +} + +#[tauri::command] +fn list_templates(vault_path: String) -> Result, String> { + let templates_dir = Path::new(&vault_path).join("_templates"); + if !templates_dir.exists() { + return Ok(vec![]); + } + + let mut templates: Vec = Vec::new(); + for entry in fs::read_dir(&templates_dir).map_err(|e| e.to_string())? { + let entry = entry.map_err(|e| e.to_string())?; + let path = entry.path(); + if path.extension().map_or(false, |ext| ext == "md") { + let name = path.file_stem() + .unwrap_or_default() + .to_string_lossy() + .to_string(); + let rel = format!("_templates/{}", path.file_name().unwrap().to_string_lossy()); + templates.push(TemplateInfo { name, path: rel }); + } + } + templates.sort_by(|a, b| a.name.cmp(&b.name)); + Ok(templates) +} + +#[tauri::command] +fn create_from_template( + vault_path: String, + template_path: String, + note_name: String, +) -> Result { + let vault = Path::new(&vault_path); + let template_full = vault.join(&template_path); + let template_content = fs::read_to_string(&template_full) + .map_err(|e| format!("Failed to read template: {}", e))?; + + let today = Local::now().format("%Y-%m-%d").to_string(); + let content = template_content + .replace("{{title}}", ¬e_name) + .replace("{{date}}", &today); + + let note_path = format!("{}.md", note_name); + let full_path = vault.join(¬e_path); + if let Some(parent) = full_path.parent() { + fs::create_dir_all(parent).map_err(|e| e.to_string())?; + } + fs::write(&full_path, content).map_err(|e| format!("Failed to create note: {}", e))?; + Ok(note_path) +} + +/* ── Favorites ──────────────────────────────────────────── */ + +#[tauri::command] +fn get_favorites(vault_path: String) -> Result, String> { + let path = Path::new(&vault_path).join(".graph-notes").join("favorites.json"); + if !path.exists() { + return Ok(vec![]); + } + let content = fs::read_to_string(&path).map_err(|e| e.to_string())?; + let favs: Vec = serde_json::from_str(&content).unwrap_or_default(); + Ok(favs) +} + +#[tauri::command] +fn set_favorites(vault_path: String, favorites: Vec) -> Result<(), String> { + let dir = Path::new(&vault_path).join(".graph-notes"); + fs::create_dir_all(&dir).map_err(|e| e.to_string())?; + let json = serde_json::to_string_pretty(&favorites).map_err(|e| e.to_string())?; + fs::write(dir.join("favorites.json"), json).map_err(|e| e.to_string()) +} + +/* ── Frontmatter ────────────────────────────────────────── */ + +#[tauri::command] +fn parse_frontmatter(vault_path: String, note_path: String) -> Result { + let full = Path::new(&vault_path).join(¬e_path); + let content = fs::read_to_string(&full).map_err(|e| e.to_string())?; + + if !content.starts_with("---\n") { + return Ok(serde_json::json!({})); + } + + let end = content[4..].find("\n---"); + match end { + Some(pos) => { + let yaml_str = &content[4..4 + pos]; + // Parse simple key: value pairs + let mut map = serde_json::Map::new(); + for line in yaml_str.lines() { + if let Some(colon_pos) = line.find(':') { + let key = line[..colon_pos].trim().to_string(); + let val = line[colon_pos + 1..].trim().to_string(); + map.insert(key, serde_json::Value::String(val)); + } + } + Ok(serde_json::Value::Object(map)) + } + None => Ok(serde_json::json!({})), + } +} + +#[tauri::command] +fn write_frontmatter( + vault_path: String, + note_path: String, + frontmatter: serde_json::Map, +) -> Result<(), String> { + let full = Path::new(&vault_path).join(¬e_path); + let content = fs::read_to_string(&full).map_err(|e| e.to_string())?; + + // Strip existing frontmatter + let body = if content.starts_with("---\n") { + if let Some(end) = content[4..].find("\n---") { + content[4 + end + 4..].to_string() + } else { + content + } + } else { + content + }; + + // Build new frontmatter + let mut yaml = String::from("---\n"); + for (key, val) in &frontmatter { + let v = match val { + serde_json::Value::String(s) => s.clone(), + other => other.to_string(), + }; + yaml.push_str(&format!("{}: {}\n", key, v)); + } + yaml.push_str("---\n"); + + let new_content = if frontmatter.is_empty() { + body.trim_start().to_string() + } else { + format!("{}{}", yaml, body) + }; + + fs::write(&full, new_content).map_err(|e| e.to_string()) +} + +/* ── Attachments ────────────────────────────────────────── */ + +#[tauri::command] +fn save_attachment( + vault_path: String, + file_name: String, + data: Vec, +) -> Result { + let attach_dir = Path::new(&vault_path).join("_attachments"); + fs::create_dir_all(&attach_dir).map_err(|e| e.to_string())?; + + // Deduplicate filename + let mut target = attach_dir.join(&file_name); + let stem = target.file_stem().unwrap_or_default().to_string_lossy().to_string(); + let ext = target.extension().map(|e| format!(".{}", e.to_string_lossy())).unwrap_or_default(); + let mut counter = 1; + while target.exists() { + target = attach_dir.join(format!("{}-{}{}", stem, counter, ext)); + counter += 1; + } + + fs::write(&target, &data).map_err(|e| e.to_string())?; + let rel = format!("_attachments/{}", target.file_name().unwrap().to_string_lossy()); + Ok(rel) +} + +#[tauri::command] +fn list_attachments(vault_path: String) -> Result, String> { + let attach_dir = Path::new(&vault_path).join("_attachments"); + if !attach_dir.exists() { + return Ok(vec![]); + } + + let mut files: Vec = Vec::new(); + for entry in fs::read_dir(&attach_dir).map_err(|e| e.to_string())? { + let entry = entry.map_err(|e| e.to_string())?; + if entry.path().is_file() { + files.push(entry.file_name().to_string_lossy().to_string()); + } + } + files.sort(); + Ok(files) +} + +/* ── Daily Notes Listing ────────────────────────────────── */ + +#[tauri::command] +fn list_daily_notes(vault_path: String) -> Result, String> { + let daily_dir = Path::new(&vault_path).join("daily"); + if !daily_dir.exists() { + return Ok(vec![]); + } + + let re = Regex::new(r"^\d{4}-\d{2}-\d{2}\.md$").unwrap(); + let mut dates: Vec = Vec::new(); + + for entry in fs::read_dir(&daily_dir).map_err(|e| e.to_string())? { + let entry = entry.map_err(|e| e.to_string())?; + let name = entry.file_name().to_string_lossy().to_string(); + if re.is_match(&name) { + dates.push(name.replace(".md", "")); + } + } + dates.sort(); + Ok(dates) +} + +/* ── Theme ──────────────────────────────────────────────── */ + +#[tauri::command] +fn get_theme() -> Result { + let path = dirs_config_dir().join("theme"); + if path.exists() { + fs::read_to_string(&path).map_err(|e| e.to_string()) + } else { + Ok("dark-purple".to_string()) + } +} + +#[tauri::command] +fn set_theme(theme: String) -> Result<(), String> { + let dir = dirs_config_dir(); + fs::create_dir_all(&dir).map_err(|e| e.to_string())?; + fs::write(dir.join("theme"), &theme).map_err(|e| e.to_string()) +} + +/* ── Export ──────────────────────────────────────────────── */ + +#[tauri::command] +fn export_note_html(vault_path: String, note_path: String) -> Result { + let full = Path::new(&vault_path).join(¬e_path); + let content = fs::read_to_string(&full).map_err(|e| e.to_string())?; + let title = Path::new(¬e_path) + .file_stem() + .unwrap_or_default() + .to_string_lossy() + .to_string(); + + // Basic markdown-to-HTML (headings, bold, italic, links, paragraphs) + let mut html_body = String::new(); + for line in content.lines() { + let trimmed = line.trim(); + if trimmed.starts_with("---") { + continue; // skip frontmatter delimiters + } + if trimmed.starts_with("# ") { + html_body.push_str(&format!("

{}

\n", &trimmed[2..])); + } else if trimmed.starts_with("## ") { + html_body.push_str(&format!("

{}

\n", &trimmed[3..])); + } else if trimmed.starts_with("### ") { + html_body.push_str(&format!("

{}

\n", &trimmed[4..])); + } else if trimmed.starts_with("- ") { + html_body.push_str(&format!("
  • {}
  • \n", &trimmed[2..])); + } else if trimmed.is_empty() { + html_body.push_str("
    \n"); + } else { + html_body.push_str(&format!("

    {}

    \n", trimmed)); + } + } + + // Replace wikilinks + let re_wl = Regex::new(r"\[\[([^\]|]+)(?:\|([^\]]+))?\]\]").unwrap(); + let html_body = re_wl.replace_all(&html_body, |caps: ®ex::Captures| { + let target = caps.get(1).map_or("", |m| m.as_str()).trim(); + let label = caps.get(2).map_or(target, |m| m.as_str()).trim(); + format!("{}", target, label) + }).to_string(); + + let html = format!(r#" + + + + +{title} + + + +{html_body} + +"#); + Ok(html) +} + +/* ── Tasks (Kanban) ─────────────────────────────────────── */ + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct TaskItem { + pub text: String, + pub state: String, // "todo", "in-progress", "done" + pub source_path: String, + pub line_number: usize, +} + +#[tauri::command] +fn list_tasks(vault_path: String) -> Result, String> { + let vault = Path::new(&vault_path); + let re = Regex::new(r"^(\s*)- \[([ x/])\] (.+)$").unwrap(); + let mut tasks: Vec = Vec::new(); + + for entry in WalkDir::new(vault) + .into_iter() + .filter_map(|e| e.ok()) + .filter(|e| e.path().extension().map_or(false, |ext| ext == "md")) + { + let path = entry.path(); + let rel = path.strip_prefix(vault).unwrap_or(path).to_string_lossy().to_string(); + if rel.starts_with(".") || rel.starts_with("_") { + continue; + } + + if let Ok(content) = fs::read_to_string(path) { + for (i, line) in content.lines().enumerate() { + if let Some(caps) = re.captures(line) { + let marker = &caps[2]; + let state = match marker { + "x" => "done", + "/" => "in-progress", + _ => "todo", + }.to_string(); + let text = caps[3].trim().to_string(); + tasks.push(TaskItem { + text, + state, + source_path: rel.clone(), + line_number: i + 1, + }); + } + } + } + } + Ok(tasks) +} + +#[tauri::command] +fn toggle_task(vault_path: String, note_path: String, line_number: usize, new_state: String) -> Result<(), String> { + let full = Path::new(&vault_path).join(¬e_path); + let content = fs::read_to_string(&full).map_err(|e| e.to_string())?; + let mut lines: Vec = content.lines().map(|s| s.to_string()).collect(); + + if line_number == 0 || line_number > lines.len() { + return Err("Invalid line number".to_string()); + } + + let marker = match new_state.as_str() { + "done" => "x", + "in-progress" => "/", + _ => " ", + }; + + let re = Regex::new(r"- \[[ x/]\]").unwrap(); + let line = &lines[line_number - 1]; + if let Some(m) = re.find(line) { + let mut new_line = String::new(); + new_line.push_str(&line[..m.start()]); + new_line.push_str(&format!("- [{}]", marker)); + new_line.push_str(&line[m.end()..]); + lines[line_number - 1] = new_line; + } + + fs::write(&full, lines.join("\n") + "\n").map_err(|e| e.to_string()) +} + +/* ── Snapshots (Version History) ────────────────────────── */ + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct SnapshotInfo { + pub timestamp: String, + pub filename: String, + pub size: u64, +} + +#[tauri::command] +fn save_snapshot(vault_path: String, note_path: String) -> Result { + let full = Path::new(&vault_path).join(¬e_path); + let content = fs::read_to_string(&full).map_err(|e| e.to_string())?; + + let safe_name = note_path.replace('/', "__").replace(".md", ""); + let history_dir = Path::new(&vault_path).join(".graph-notes").join("history").join(&safe_name); + fs::create_dir_all(&history_dir).map_err(|e| e.to_string())?; + + let ts = Local::now().format("%Y%m%d_%H%M%S").to_string(); + let snap_name = format!("{}.md", ts); + fs::write(history_dir.join(&snap_name), &content).map_err(|e| e.to_string())?; + Ok(snap_name) +} + +#[tauri::command] +fn list_snapshots(vault_path: String, note_path: String) -> Result, String> { + let safe_name = note_path.replace('/', "__").replace(".md", ""); + let history_dir = Path::new(&vault_path).join(".graph-notes").join("history").join(&safe_name); + + if !history_dir.exists() { + return Ok(vec![]); + } + + let mut snaps: Vec = Vec::new(); + for entry in fs::read_dir(&history_dir).map_err(|e| e.to_string())? { + let entry = entry.map_err(|e| e.to_string())?; + let meta = entry.metadata().map_err(|e| e.to_string())?; + if meta.is_file() { + let name = entry.file_name().to_string_lossy().to_string(); + let ts = name.replace(".md", ""); + snaps.push(SnapshotInfo { + timestamp: ts, + filename: name, + size: meta.len(), + }); + } + } + snaps.sort_by(|a, b| b.timestamp.cmp(&a.timestamp)); + Ok(snaps) +} + +#[tauri::command] +fn read_snapshot(vault_path: String, note_path: String, snapshot_name: String) -> Result { + let safe_name = note_path.replace('/', "__").replace(".md", ""); + let snap_path = Path::new(&vault_path) + .join(".graph-notes") + .join("history") + .join(&safe_name) + .join(&snapshot_name); + fs::read_to_string(&snap_path).map_err(|e| e.to_string()) +} + +/* ── Search & Replace ───────────────────────────────────── */ + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct ReplaceResult { + pub path: String, + pub count: usize, +} + +#[tauri::command] +fn search_replace_vault( + vault_path: String, + search: String, + replace: String, + dry_run: bool, +) -> Result, String> { + let vault = Path::new(&vault_path); + let mut results: Vec = Vec::new(); + + for entry in WalkDir::new(vault) + .into_iter() + .filter_map(|e| e.ok()) + .filter(|e| e.path().extension().map_or(false, |ext| ext == "md")) + { + let path = entry.path(); + let rel = path.strip_prefix(vault).unwrap_or(path).to_string_lossy().to_string(); + if rel.starts_with(".") || rel.starts_with("_") { + continue; + } + + if let Ok(content) = fs::read_to_string(path) { + let count = content.matches(&search).count(); + if count > 0 { + if !dry_run { + let new_content = content.replace(&search, &replace); + let _ = fs::write(path, new_content); + } + results.push(ReplaceResult { + path: rel, + count, + }); + } + } + } + Ok(results) +} + +/* ── Writing Goals ──────────────────────────────────────── */ + +#[tauri::command] +fn get_writing_goal(vault_path: String, note_path: String) -> Result { + let goals_path = Path::new(&vault_path).join(".graph-notes").join("goals.json"); + if !goals_path.exists() { + return Ok(0); + } + let content = fs::read_to_string(&goals_path).map_err(|e| e.to_string())?; + let goals: serde_json::Map = + serde_json::from_str(&content).unwrap_or_default(); + let goal = goals.get(¬e_path) + .and_then(|v| v.as_u64()) + .unwrap_or(0); + Ok(goal as u32) +} + +#[tauri::command] +fn set_writing_goal(vault_path: String, note_path: String, goal: u32) -> Result<(), String> { + let dir = Path::new(&vault_path).join(".graph-notes"); + fs::create_dir_all(&dir).map_err(|e| e.to_string())?; + let goals_path = dir.join("goals.json"); + + let mut goals: serde_json::Map = if goals_path.exists() { + let content = fs::read_to_string(&goals_path).map_err(|e| e.to_string())?; + serde_json::from_str(&content).unwrap_or_default() + } else { + serde_json::Map::new() + }; + + if goal == 0 { + goals.remove(¬e_path); + } else { + goals.insert(note_path, serde_json::Value::Number(serde_json::Number::from(goal))); + } + + let json = serde_json::to_string_pretty(&goals).map_err(|e| e.to_string())?; + fs::write(&goals_path, json).map_err(|e| e.to_string()) +} + +/* ── Note Refactoring ───────────────────────────────────── */ + +#[tauri::command] +fn extract_to_note( + vault_path: String, + source_path: String, + selected_text: String, + new_note_name: String, +) -> Result { + let vault = Path::new(&vault_path); + let new_path = vault.join(format!("{}.md", &new_note_name)); + if new_path.exists() { + return Err(format!("Note '{}' already exists", new_note_name)); + } + + // Create new note with extracted text + fs::write(&new_path, &selected_text).map_err(|e| e.to_string())?; + + // Replace selected text with wikilink in source + let source_full = vault.join(&source_path); + let content = fs::read_to_string(&source_full).map_err(|e| e.to_string())?; + let new_content = content.replacen(&selected_text, &format!("[[{}]]", new_note_name), 1); + fs::write(&source_full, new_content).map_err(|e| e.to_string())?; + + Ok(format!("{}.md", new_note_name)) +} + +#[tauri::command] +fn merge_notes( + vault_path: String, + source_path: String, + target_path: String, +) -> Result<(), String> { + let vault = Path::new(&vault_path); + let source_full = vault.join(&source_path); + let target_full = vault.join(&target_path); + + let source_content = fs::read_to_string(&source_full).map_err(|e| e.to_string())?; + let target_content = fs::read_to_string(&target_full).map_err(|e| e.to_string())?; + + let source_name = source_path.replace(".md", ""); + let merged = format!("{}\n\n---\n\n## Merged from {}\n\n{}", target_content.trim_end(), source_name, source_content); + fs::write(&target_full, merged).map_err(|e| e.to_string())?; + + // Delete source + fs::remove_file(&source_full).map_err(|e| e.to_string())?; + + // Update wikilinks pointing to source → target + let target_name = target_path.replace(".md", ""); + for entry in WalkDir::new(vault) + .into_iter() + .filter_map(|e| e.ok()) + .filter(|e| e.path().extension().map_or(false, |ext| ext == "md")) + { + let path = entry.path(); + if path == target_full { continue; } + if let Ok(content) = fs::read_to_string(path) { + let updated = content.replace( + &format!("[[{}]]", source_name), + &format!("[[{}]]", target_name), + ); + if updated != content { + let _ = fs::write(path, updated); + } + } + } + Ok(()) +} + +/* ── Encryption ─────────────────────────────────────────── */ + +use aes_gcm::{Aes256Gcm, Key, Nonce}; +use aes_gcm::aead::{Aead, KeyInit}; +use argon2::Argon2; +use base64::{Engine as _, engine::general_purpose::STANDARD as B64}; + +fn derive_key(password: &str, salt: &[u8]) -> [u8; 32] { + let mut key = [0u8; 32]; + Argon2::default() + .hash_password_into(password.as_bytes(), salt, &mut key) + .expect("key derivation failed"); + key +} + +#[tauri::command] +fn encrypt_note(vault_path: String, note_path: String, password: String) -> Result<(), String> { + let full = Path::new(&vault_path).join(¬e_path); + let content = fs::read_to_string(&full).map_err(|e| e.to_string())?; + + let salt: [u8; 16] = rand::random(); + let nonce_bytes: [u8; 12] = rand::random(); + + let key_bytes = derive_key(&password, &salt); + let key = Key::::from_slice(&key_bytes); + let cipher = Aes256Gcm::new(key); + let nonce = Nonce::from_slice(&nonce_bytes); + + let ciphertext = cipher.encrypt(nonce, content.as_bytes()) + .map_err(|_| "Encryption failed".to_string())?; + + // Format: GRAPHNOTES_ENC:v1:{salt_b64}:{nonce_b64}:{ciphertext_b64} + let encoded = format!( + "GRAPHNOTES_ENC:v1:{}:{}:{}", + B64.encode(salt), + B64.encode(nonce_bytes), + B64.encode(&ciphertext), + ); + fs::write(&full, encoded).map_err(|e| e.to_string()) +} + +#[tauri::command] +fn decrypt_note(vault_path: String, note_path: String, password: String) -> Result { + let full = Path::new(&vault_path).join(¬e_path); + let content = fs::read_to_string(&full).map_err(|e| e.to_string())?; + + if !content.starts_with("GRAPHNOTES_ENC:v1:") { + return Err("Note is not encrypted".to_string()); + } + + let parts: Vec<&str> = content.splitn(5, ':').collect(); + if parts.len() != 5 { + return Err("Invalid encrypted format".to_string()); + } + + let salt = B64.decode(parts[2]).map_err(|_| "Invalid salt".to_string())?; + let nonce_bytes = B64.decode(parts[3]).map_err(|_| "Invalid nonce".to_string())?; + let ciphertext = B64.decode(parts[4]).map_err(|_| "Invalid ciphertext".to_string())?; + + let key_bytes = derive_key(&password, &salt); + let key = Key::::from_slice(&key_bytes); + let cipher = Aes256Gcm::new(key); + let nonce = Nonce::from_slice(&nonce_bytes); + + let plaintext = cipher.decrypt(nonce, ciphertext.as_ref()) + .map_err(|_| "Wrong password".to_string())?; + + String::from_utf8(plaintext).map_err(|_| "Invalid UTF-8".to_string()) +} + +#[tauri::command] +fn is_encrypted(vault_path: String, note_path: String) -> Result { + let full = Path::new(&vault_path).join(¬e_path); + let content = fs::read_to_string(&full).map_err(|e| e.to_string())?; + Ok(content.starts_with("GRAPHNOTES_ENC:v1:")) +} + +/* ── Flashcards (SRS) ───────────────────────────────────── */ + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct Flashcard { + pub question: String, + pub answer: String, + pub source_path: String, + pub line_number: usize, + pub due: Option, + pub interval: u32, + pub ease: f32, +} + +#[tauri::command] +fn list_flashcards(vault_path: String) -> Result, String> { + let vault = Path::new(&vault_path); + let re = Regex::new(r"\?\?\s*(.+?)\s*::\s*(.+?)\s*\?\?").unwrap(); + let mut cards: Vec = Vec::new(); + + // Load schedule data + let srs_path = vault.join(".graph-notes").join("srs.json"); + let srs: serde_json::Map = if srs_path.exists() { + let c = fs::read_to_string(&srs_path).unwrap_or_default(); + serde_json::from_str(&c).unwrap_or_default() + } else { + serde_json::Map::new() + }; + + for entry in WalkDir::new(vault) + .into_iter() + .filter_map(|e| e.ok()) + .filter(|e| e.path().extension().map_or(false, |ext| ext == "md")) + { + let path = entry.path(); + let rel = path.strip_prefix(vault).unwrap_or(path).to_string_lossy().to_string(); + if rel.starts_with(".") || rel.starts_with("_") { continue; } + + if let Ok(content) = fs::read_to_string(path) { + for (i, line) in content.lines().enumerate() { + for caps in re.captures_iter(line) { + let q = caps[1].trim().to_string(); + let a = caps[2].trim().to_string(); + let card_id = format!("{}:{}", rel, i + 1); + + let (due, interval, ease) = if let Some(sched) = srs.get(&card_id) { + ( + sched.get("due").and_then(|v| v.as_str()).map(|s| s.to_string()), + sched.get("interval").and_then(|v| v.as_u64()).unwrap_or(1) as u32, + sched.get("ease").and_then(|v| v.as_f64()).unwrap_or(2.5) as f32, + ) + } else { + (None, 1, 2.5) + }; + + cards.push(Flashcard { + question: q, + answer: a, + source_path: rel.clone(), + line_number: i + 1, + due, + interval, + ease, + }); + } + } + } + } + Ok(cards) +} + +#[tauri::command] +fn update_card_schedule( + vault_path: String, + card_id: String, + quality: u32, +) -> Result<(), String> { + let srs_path = Path::new(&vault_path).join(".graph-notes").join("srs.json"); + fs::create_dir_all(Path::new(&vault_path).join(".graph-notes")).map_err(|e| e.to_string())?; + + let mut srs: serde_json::Map = if srs_path.exists() { + let c = fs::read_to_string(&srs_path).unwrap_or_default(); + serde_json::from_str(&c).unwrap_or_default() + } else { + serde_json::Map::new() + }; + + let entry = srs.entry(card_id).or_insert_with(|| serde_json::json!({"interval": 1, "ease": 2.5})); + let obj = entry.as_object_mut().ok_or("Invalid SRS entry")?; + + let mut interval = obj.get("interval").and_then(|v| v.as_u64()).unwrap_or(1) as f64; + let mut ease = obj.get("ease").and_then(|v| v.as_f64()).unwrap_or(2.5); + + // SM-2 algorithm + if quality >= 3 { + if interval <= 1.0 { interval = 1.0; } + else if interval <= 6.0 { interval = 6.0; } + else { interval *= ease; } + ease = ease + (0.1 - (5.0 - quality as f64) * (0.08 + (5.0 - quality as f64) * 0.02)); + if ease < 1.3 { ease = 1.3; } + } else { + interval = 1.0; + } + + let due = Local::now() + chrono::Duration::days(interval as i64); + obj.insert("interval".into(), serde_json::json!(interval as u32)); + obj.insert("ease".into(), serde_json::json!(ease)); + obj.insert("due".into(), serde_json::json!(due.format("%Y-%m-%d").to_string())); + + let json = serde_json::to_string_pretty(&srs).map_err(|e| e.to_string())?; + fs::write(&srs_path, json).map_err(|e| e.to_string()) +} + +/* ── Fold State ─────────────────────────────────────────── */ + +#[tauri::command] +fn save_fold_state(vault_path: String, note_path: String, folds: Vec) -> Result<(), String> { + let dir = Path::new(&vault_path).join(".graph-notes"); + fs::create_dir_all(&dir).map_err(|e| e.to_string())?; + let folds_path = dir.join("folds.json"); + + let mut data: serde_json::Map = if folds_path.exists() { + let c = fs::read_to_string(&folds_path).unwrap_or_default(); + serde_json::from_str(&c).unwrap_or_default() + } else { + serde_json::Map::new() + }; + + data.insert(note_path, serde_json::json!(folds)); + let json = serde_json::to_string_pretty(&data).map_err(|e| e.to_string())?; + fs::write(&folds_path, json).map_err(|e| e.to_string()) +} + +#[tauri::command] +fn load_fold_state(vault_path: String, note_path: String) -> Result, String> { + let folds_path = Path::new(&vault_path).join(".graph-notes").join("folds.json"); + if !folds_path.exists() { return Ok(vec![]); } + let c = fs::read_to_string(&folds_path).map_err(|e| e.to_string())?; + let data: serde_json::Map = serde_json::from_str(&c).unwrap_or_default(); + let folds = data.get(¬e_path) + .and_then(|v| v.as_array()) + .map(|arr| arr.iter().filter_map(|v| v.as_u64().map(|n| n as usize)).collect()) + .unwrap_or_default(); + Ok(folds) +} + +/* ── Custom CSS ─────────────────────────────────────────── */ + +#[tauri::command] +fn get_custom_css() -> Result { + let config_dir = dirs_config_path(); + let css_path = config_dir.join("custom.css"); + if css_path.exists() { + fs::read_to_string(&css_path).map_err(|e| e.to_string()) + } else { + Ok(String::new()) + } +} + +#[tauri::command] +fn set_custom_css(css: String) -> Result<(), String> { + let config_dir = dirs_config_path(); + fs::create_dir_all(&config_dir).map_err(|e| e.to_string())?; + fs::write(config_dir.join("custom.css"), css).map_err(|e| e.to_string()) +} + +fn dirs_config_path() -> PathBuf { + dirs::config_dir().unwrap_or_else(|| PathBuf::from(".")).join("graph-notes") +} + +/* ── Workspace Layouts ──────────────────────────────────── */ + +#[tauri::command] +fn save_workspace(vault_path: String, name: String, state: String) -> Result<(), String> { + let dir = Path::new(&vault_path).join(".graph-notes").join("workspaces"); + fs::create_dir_all(&dir).map_err(|e| e.to_string())?; + fs::write(dir.join(format!("{}.json", name)), state).map_err(|e| e.to_string()) +} + +#[tauri::command] +fn load_workspace(vault_path: String, name: String) -> Result { + let path = Path::new(&vault_path).join(".graph-notes").join("workspaces").join(format!("{}.json", name)); + fs::read_to_string(&path).map_err(|e| e.to_string()) +} + +#[tauri::command] +fn list_workspaces(vault_path: String) -> Result, String> { + let dir = Path::new(&vault_path).join(".graph-notes").join("workspaces"); + if !dir.exists() { return Ok(vec![]); } + let mut names: Vec = Vec::new(); + for entry in fs::read_dir(&dir).map_err(|e| e.to_string())? { + let entry = entry.map_err(|e| e.to_string())?; + let name = entry.file_name().to_string_lossy().replace(".json", ""); + names.push(name); + } + names.sort(); + Ok(names) +} + +/* ── Tab Persistence ────────────────────────────────────── */ + +#[tauri::command] +fn save_tabs(vault_path: String, tabs: String) -> Result<(), String> { + let dir = Path::new(&vault_path).join(".graph-notes"); + fs::create_dir_all(&dir).map_err(|e| e.to_string())?; + fs::write(dir.join("tabs.json"), tabs).map_err(|e| e.to_string()) +} + +#[tauri::command] +fn load_tabs(vault_path: String) -> Result { + let path = Path::new(&vault_path).join(".graph-notes").join("tabs.json"); + if !path.exists() { return Ok("[]".to_string()); } + fs::read_to_string(&path).map_err(|e| e.to_string()) +} + +/* ── Canvas / Whiteboard Persistence ────────────────────── */ + +#[tauri::command] +fn save_canvas(vault_path: String, name: String, data: String) -> Result<(), String> { + let dir = Path::new(&vault_path).join(".graph-notes").join("canvases"); + fs::create_dir_all(&dir).map_err(|e| e.to_string())?; + fs::write(dir.join(format!("{}.json", name)), data).map_err(|e| e.to_string()) +} + +#[tauri::command] +fn load_canvas(vault_path: String, name: String) -> Result { + let path = Path::new(&vault_path).join(".graph-notes").join("canvases").join(format!("{}.json", name)); + if !path.exists() { return Ok("{}".to_string()); } + fs::read_to_string(&path).map_err(|e| e.to_string()) +} + +#[tauri::command] +fn list_canvases(vault_path: String) -> Result, String> { + let dir = Path::new(&vault_path).join(".graph-notes").join("canvases"); + if !dir.exists() { return Ok(vec![]); } + let mut names: Vec = Vec::new(); + for entry in fs::read_dir(&dir).map_err(|e| e.to_string())? { + let entry = entry.map_err(|e| e.to_string())?; + let name = entry.file_name().to_string_lossy().replace(".json", ""); + names.push(name); + } + names.sort(); + Ok(names) +} + +/* ── Frontmatter Query ──────────────────────────────────── */ + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct FrontmatterRow { + pub path: String, + pub title: String, + pub fields: serde_json::Map, +} + +#[tauri::command] +fn query_frontmatter(vault_path: String) -> Result, String> { + let vault = Path::new(&vault_path); + let mut rows: Vec = Vec::new(); + let fm_re = Regex::new(r"(?s)^---\n(.+?)\n---").unwrap(); + + for entry in WalkDir::new(vault) + .into_iter() + .filter_map(|e| e.ok()) + .filter(|e| e.path().extension().map_or(false, |ext| ext == "md")) + { + let path = entry.path(); + let rel = path.strip_prefix(vault).unwrap_or(path).to_string_lossy().to_string(); + if rel.starts_with(".") { continue; } + + if let Ok(content) = fs::read_to_string(path) { + if let Some(caps) = fm_re.captures(&content) { + let yaml_str = &caps[1]; + let mut fields = serde_json::Map::new(); + for line in yaml_str.lines() { + if let Some(idx) = line.find(':') { + let key = line[..idx].trim().to_string(); + let val = line[idx+1..].trim().to_string(); + fields.insert(key, serde_json::Value::String(val)); + } + } + let title = rel.replace(".md", ""); + rows.push(FrontmatterRow { path: rel, title, fields }); + } + } + } + Ok(rows) +} + +/* ── Backlink Context ───────────────────────────────────── */ + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct BacklinkContext { + pub source_path: String, + pub source_name: String, + pub excerpt: String, +} + +#[tauri::command] +fn get_backlink_context(vault_path: String, note_name: String) -> Result, String> { + let vault = Path::new(&vault_path); + let link_pattern = format!("[[{}]]", note_name); + let mut results: Vec = Vec::new(); + + for entry in WalkDir::new(vault) + .into_iter() + .filter_map(|e| e.ok()) + .filter(|e| e.path().extension().map_or(false, |ext| ext == "md")) + { + let path = entry.path(); + let rel = path.strip_prefix(vault).unwrap_or(path).to_string_lossy().to_string(); + if rel.starts_with(".") { continue; } + + if let Ok(content) = fs::read_to_string(path) { + if content.contains(&link_pattern) { + // Find paragraph containing the link + let paragraphs: Vec<&str> = content.split("\n\n").collect(); + for para in paragraphs { + if para.contains(&link_pattern) { + let excerpt = para.trim().chars().take(200).collect::(); + results.push(BacklinkContext { + source_path: rel.clone(), + source_name: rel.replace(".md", ""), + excerpt, + }); + break; + } + } + } + } + } + Ok(results) +} + +/* ── Dataview Query Engine ──────────────────────────────── */ + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct DataviewResult { + pub columns: Vec, + pub rows: Vec>, +} + +#[tauri::command] +fn run_dataview_query(vault_path: String, query: String) -> Result { + let vault = Path::new(&vault_path); + let fm_re = Regex::new(r"(?s)^---\n(.+?)\n---").unwrap(); + + // Parse query: TABLE field1, field2 [FROM ""] [WHERE cond] [SORT field [ASC|DESC]] + let query_upper = query.to_uppercase(); + let is_table = query_upper.starts_with("TABLE"); + if !is_table { return Err("Only TABLE queries supported".into()); } + + // Extract fields + let after_table = query.trim_start_matches(|c: char| c.is_alphabetic() || c == ' ').trim(); + let parts: Vec<&str> = after_table.splitn(2, " FROM").collect(); + let field_str = parts[0].trim(); + let columns: Vec = std::iter::once("File".to_string()) + .chain(field_str.split(',').map(|s| s.trim().to_string())) + .collect(); + + // Parse sort + let sort_field = if query_upper.contains("SORT") { + let sort_part = query.split("SORT").nth(1).unwrap_or("").trim(); + let sf: Vec<&str> = sort_part.split_whitespace().collect(); + sf.first().map(|s| s.to_string()) + } else { None }; + + let mut rows: Vec> = Vec::new(); + + for entry in WalkDir::new(vault) + .into_iter() + .filter_map(|e| e.ok()) + .filter(|e| e.path().extension().map_or(false, |ext| ext == "md")) + { + let path = entry.path(); + let rel = path.strip_prefix(vault).unwrap_or(path).to_string_lossy().to_string(); + if rel.starts_with(".") { continue; } + + if let Ok(content) = fs::read_to_string(path) { + let mut fields_map: std::collections::HashMap = std::collections::HashMap::new(); + fields_map.insert("title".into(), rel.replace(".md", "")); + + if let Some(caps) = fm_re.captures(&content) { + for line in caps[1].lines() { + if let Some(idx) = line.find(':') { + let k = line[..idx].trim().to_lowercase(); + let v = line[idx+1..].trim().to_string(); + fields_map.insert(k, v); + } + } + } + + let mut row = vec![rel.replace(".md", "")]; + for col in &columns[1..] { + let val = fields_map.get(&col.to_lowercase()).cloned().unwrap_or_default(); + row.push(val); + } + rows.push(row); + } + } + + // Sort + if let Some(ref sf) = sort_field { + let sf_lower = sf.to_lowercase(); + if let Some(idx) = columns.iter().position(|c| c.to_lowercase() == sf_lower) { + rows.sort_by(|a, b| a.get(idx).unwrap_or(&String::new()).cmp(b.get(idx).unwrap_or(&String::new()))); + } + } + + Ok(DataviewResult { columns, rows }) +} + +/* ── Git Sync ───────────────────────────────────────────── */ + +use std::process::Command; + +#[tauri::command] +fn git_status(vault_path: String) -> Result { + let output = Command::new("git") + .args(["status", "--porcelain"]) + .current_dir(&vault_path) + .output() + .map_err(|e| e.to_string())?; + Ok(String::from_utf8_lossy(&output.stdout).to_string()) +} + +#[tauri::command] +fn git_commit(vault_path: String, message: String) -> Result { + let _ = Command::new("git") + .args(["add", "."]) + .current_dir(&vault_path) + .output() + .map_err(|e| e.to_string())?; + + let output = Command::new("git") + .args(["commit", "-m", &message]) + .current_dir(&vault_path) + .output() + .map_err(|e| e.to_string())?; + Ok(String::from_utf8_lossy(&output.stdout).to_string()) +} + +#[tauri::command] +fn git_pull(vault_path: String) -> Result { + let output = Command::new("git") + .args(["pull"]) + .current_dir(&vault_path) + .output() + .map_err(|e| e.to_string())?; + if output.status.success() { + Ok(String::from_utf8_lossy(&output.stdout).to_string()) + } else { + Err(String::from_utf8_lossy(&output.stderr).to_string()) + } +} + +#[tauri::command] +fn git_push(vault_path: String) -> Result { + let output = Command::new("git") + .args(["push"]) + .current_dir(&vault_path) + .output() + .map_err(|e| e.to_string())?; + if output.status.success() { + Ok(String::from_utf8_lossy(&output.stdout).to_string()) + } else { + Err(String::from_utf8_lossy(&output.stderr).to_string()) + } +} + +#[tauri::command] +fn git_init(vault_path: String) -> Result { + let output = Command::new("git") + .args(["init"]) + .current_dir(&vault_path) + .output() + .map_err(|e| e.to_string())?; + Ok(String::from_utf8_lossy(&output.stdout).to_string()) +} + +/* ── v0.8 Commands ──────────────────────────────────────── */ + +#[tauri::command] +fn suggest_links(vault_path: String, partial: String) -> Result, String> { + let vault = Path::new(&vault_path); + let partial_lower = partial.to_lowercase(); + let mut matches: Vec = Vec::new(); + + for entry in WalkDir::new(vault) + .into_iter() + .filter_map(|e| e.ok()) + .filter(|e| e.path().extension().map_or(false, |ext| ext == "md")) + { + let path = entry.path(); + let rel = path.strip_prefix(vault).unwrap_or(path).to_string_lossy().to_string(); + if rel.starts_with(".") { continue; } + let name = rel.replace(".md", ""); + if name.to_lowercase().contains(&partial_lower) { + matches.push(name); + } + } + matches.sort_by(|a, b| { + let a_starts = a.to_lowercase().starts_with(&partial_lower); + let b_starts = b.to_lowercase().starts_with(&partial_lower); + b_starts.cmp(&a_starts).then(a.len().cmp(&b.len())) + }); + matches.truncate(15); + Ok(matches) +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct NoteByDate { + pub path: String, + pub name: String, + pub modified: u64, + pub preview: String, +} + +#[tauri::command] +fn list_notes_by_date(vault_path: String) -> Result, String> { + let vault = Path::new(&vault_path); + let mut notes: Vec = Vec::new(); + + for entry in WalkDir::new(vault) + .into_iter() + .filter_map(|e| e.ok()) + .filter(|e| e.path().extension().map_or(false, |ext| ext == "md")) + { + let path = entry.path(); + let rel = path.strip_prefix(vault).unwrap_or(path).to_string_lossy().to_string(); + if rel.starts_with(".") { continue; } + + let modified = path.metadata() + .and_then(|m| m.modified()) + .map(|t| t.duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_secs()) + .unwrap_or(0); + + let preview = fs::read_to_string(path) + .unwrap_or_default() + .lines() + .filter(|l| !l.starts_with("---") && !l.starts_with("#") && !l.trim().is_empty()) + .take(2) + .collect::>() + .join(" ") + .chars() + .take(120) + .collect(); + + notes.push(NoteByDate { + path: rel.clone(), + name: rel.replace(".md", ""), + modified, + preview, + }); + } + notes.sort_by(|a, b| b.modified.cmp(&a.modified)); + Ok(notes) +} + +#[tauri::command] +fn random_note(vault_path: String) -> Result { + let vault = Path::new(&vault_path); + let mut paths: Vec = Vec::new(); + + for entry in WalkDir::new(vault) + .into_iter() + .filter_map(|e| e.ok()) + .filter(|e| e.path().extension().map_or(false, |ext| ext == "md")) + { + let path = entry.path(); + let rel = path.strip_prefix(vault).unwrap_or(path).to_string_lossy().to_string(); + if rel.starts_with(".") { continue; } + paths.push(rel.replace(".md", "")); + } + + if paths.is_empty() { return Err("No notes found".into()); } + use rand::Rng; + let mut rng = rand::thread_rng(); + let idx = rng.gen_range(0..paths.len()); + Ok(paths[idx].clone()) +} + +/* ── v0.9 Commands ──────────────────────────────────────── */ + +#[tauri::command] +fn export_vault_zip(vault_path: String, output_path: String) -> Result { + use std::io::Write; + let vault = Path::new(&vault_path); + let out = Path::new(&output_path); + + let file = fs::File::create(out).map_err(|e| e.to_string())?; + let mut zip = zip::ZipWriter::new(file); + let options = zip::write::SimpleFileOptions::default() + .compression_method(zip::CompressionMethod::Deflated); + + for entry in WalkDir::new(vault) + .into_iter() + .filter_map(|e| e.ok()) + .filter(|e| e.path().is_file()) + { + let path = entry.path(); + let rel = path.strip_prefix(vault).unwrap_or(path).to_string_lossy().to_string(); + if rel.starts_with(".") { continue; } + + let content = fs::read(path).map_err(|e| e.to_string())?; + zip.start_file(&rel, options).map_err(|e| e.to_string())?; + zip.write_all(&content).map_err(|e| e.to_string())?; + } + + zip.finish().map_err(|e| e.to_string())?; + Ok(format!("Exported to {}", output_path)) +} + +#[tauri::command] +fn import_folder(vault_path: String, source_path: String) -> Result { + let vault = Path::new(&vault_path); + let source = Path::new(&source_path); + let mut count: u32 = 0; + + for entry in WalkDir::new(source) + .into_iter() + .filter_map(|e| e.ok()) + .filter(|e| e.path().extension().map_or(false, |ext| ext == "md")) + { + let path = entry.path(); + let rel = path.strip_prefix(source).unwrap_or(path); + let dest = vault.join(rel); + + if let Some(parent) = dest.parent() { + fs::create_dir_all(parent).map_err(|e| e.to_string())?; + } + fs::copy(path, &dest).map_err(|e| e.to_string())?; + count += 1; + } + + Ok(count) +} + +#[tauri::command] +fn save_shortcuts(vault_path: String, shortcuts_json: String) -> Result<(), String> { + let dir = Path::new(&vault_path).join(".graph-notes"); + fs::create_dir_all(&dir).map_err(|e| e.to_string())?; + fs::write(dir.join("shortcuts.json"), shortcuts_json).map_err(|e| e.to_string()) +} + +#[tauri::command] +fn load_shortcuts(vault_path: String) -> Result { + let path = Path::new(&vault_path).join(".graph-notes/shortcuts.json"); + if path.exists() { + fs::read_to_string(path).map_err(|e| e.to_string()) + } else { + Ok("{}".to_string()) + } +} + +#[tauri::command] +fn get_pinned(vault_path: String) -> Result, String> { + let path = Path::new(&vault_path).join(".graph-notes/pinned.json"); + if path.exists() { + let content = fs::read_to_string(path).map_err(|e| e.to_string())?; + serde_json::from_str(&content).map_err(|e| e.to_string()) + } else { + Ok(Vec::new()) + } +} + +#[tauri::command] +fn set_pinned(vault_path: String, pinned: Vec) -> Result<(), String> { + let dir = Path::new(&vault_path).join(".graph-notes"); + fs::create_dir_all(&dir).map_err(|e| e.to_string())?; + let json = serde_json::to_string_pretty(&pinned).map_err(|e| e.to_string())?; + fs::write(dir.join("pinned.json"), json).map_err(|e| e.to_string()) } #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { - let cache: SharedCache = Arc::new(Mutex::new(VaultCache::new(""))); - tauri::Builder::default() .plugin(tauri_plugin_opener::init()) .plugin(tauri_plugin_fs::init()) .plugin(tauri_plugin_dialog::init()) - .manage(cache) .invoke_handler(tauri::generate_handler![ list_notes, read_note, - read_note_with_meta, write_note, delete_note, build_graph, - search_vault, - rename_note, - list_templates, - save_attachment, get_or_create_daily, get_vault_path, set_vault_path, ensure_vault, - init_vault_cache, - get_cache_stats, + search_vault, + rename_note, + update_wikilinks, + list_tags, + read_note_preview, + list_recent_vaults, + add_vault, + list_templates, + create_from_template, + get_favorites, + set_favorites, + parse_frontmatter, + write_frontmatter, + save_attachment, + list_attachments, + list_daily_notes, + get_theme, + set_theme, + export_note_html, + list_tasks, + toggle_task, + save_snapshot, + list_snapshots, + read_snapshot, + search_replace_vault, + get_writing_goal, + set_writing_goal, + extract_to_note, + merge_notes, + encrypt_note, + decrypt_note, + is_encrypted, + list_flashcards, + update_card_schedule, + save_fold_state, + load_fold_state, + get_custom_css, + set_custom_css, + save_workspace, + load_workspace, + list_workspaces, + save_tabs, + load_tabs, + save_canvas, + load_canvas, + list_canvases, + query_frontmatter, + get_backlink_context, + run_dataview_query, + git_status, + git_commit, + git_pull, + git_push, + git_init, + suggest_links, + list_notes_by_date, + random_note, + export_vault_zip, + import_folder, + save_shortcuts, + load_shortcuts, + get_pinned, + set_pinned, ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); } + diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 6fcf7bd..66c196d 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "Graph Notes", - "version": "0.1.0", + "version": "0.9.0", "identifier": "com.graphnotes.app", "build": { "beforeDevCommand": "npm run dev", diff --git a/src/App.tsx b/src/App.tsx index f6bebc5..1fbe928 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -5,7 +5,20 @@ import { Editor } from "./components/Editor"; import { Backlinks } from "./components/Backlinks"; import { GraphView } from "./components/GraphView"; import { CommandPalette } from "./components/CommandPalette"; -import { OutlinePanel } from "./components/OutlinePanel"; +import { SplitView } from "./components/SplitView"; +import { LinkPreview } from "./components/LinkPreview"; +import { CalendarView } from "./components/CalendarView"; +import { ThemePicker, useThemeInit } from "./components/ThemePicker"; +import { KanbanView } from "./components/KanbanView"; +import { SearchReplace } from "./components/SearchReplace"; +import { FlashcardView } from "./components/FlashcardView"; +import { CSSEditor, useCustomCssInit } from "./components/CSSEditor"; +import { TabBar, type Tab } from "./components/TabBar"; +import { WhiteboardView } from "./components/WhiteboardView"; +import { DatabaseView } from "./components/DatabaseView"; +import { GitPanel } from "./components/GitPanel"; +import { TimelineView } from "./components/TimelineView"; +import { GraphAnalytics } from "./components/GraphAnalytics"; import { listNotes, readNote, @@ -13,12 +26,14 @@ import { getVaultPath, setVaultPath, ensureVault, - initVaultCache, getOrCreateDaily, + addVault, + getFavorites, + getBacklinkContext, + setFavorites as setFavoritesCmd, type NoteEntry, } from "./lib/commands"; import { extractWikilinks, type BacklinkEntry } from "./lib/wikilinks"; -import { noteCache } from "./lib/noteCache"; /* ── Vault Context ──────────────────────────────────────────── */ interface VaultContextType { @@ -31,6 +46,18 @@ interface VaultContextType { setNoteContent: (content: string) => void; backlinks: BacklinkEntry[]; navigateToNote: (name: string) => void; + sidebarOpen: boolean; + toggleSidebar: () => void; + editMode: boolean; + toggleEditMode: () => void; + splitNote: string | null; + setSplitNote: (path: string | null) => void; + favorites: string[]; + toggleFavorite: (path: string) => void; + recentNotes: string[]; + switchVault: (path: string) => Promise; + focusMode: boolean; + toggleFocusMode: () => void; } const VaultContext = createContext(null!); @@ -45,9 +72,29 @@ export default function App() { const [backlinks, setBacklinks] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); - const [paletteOpen, setPaletteOpen] = useState(false); + const [sidebarOpen, setSidebarOpen] = useState(true); + const [editMode, setEditMode] = useState(true); + const [cmdPaletteOpen, setCmdPaletteOpen] = useState(false); + const [splitNote, setSplitNote] = useState(null); + const [favorites, setFavorites] = useState([]); + const [recentNotes, setRecentNotes] = useState([]); + const [themePickerOpen, setThemePickerOpen] = useState(false); + const [focusMode, setFocusMode] = useState(false); + const [searchReplaceOpen, setSearchReplaceOpen] = useState(false); + const [cssEditorOpen, setCssEditorOpen] = useState(false); + const [tabs, setTabs] = useState([]); + const [activeTab, setActiveTab] = useState(0); const navigate = useNavigate(); + // Apply saved theme + custom CSS on mount + useThemeInit(); + useCustomCssInit(); + + const toggleSidebar = useCallback(() => setSidebarOpen(v => !v), []); + const toggleEditMode = useCallback(() => setEditMode(v => !v), []); + const toggleFocusMode = useCallback(() => setFocusMode(v => !v), []); + + // Initialize vault useEffect(() => { (async () => { @@ -79,14 +126,6 @@ export default function App() { console.warn("[GraphNotes] ensureVault failed:", e); } - // Initialize the Rust-side vault cache (eagerly scan all notes) - try { - const count = await initVaultCache(path); - console.log(`[GraphNotes] Cache initialized: ${count} notes`); - } catch (e) { - console.warn("[GraphNotes] initVaultCache failed:", e); - } - setVaultPathState(path); console.log("[GraphNotes] Vault ready at:", path); setLoading(false); @@ -107,31 +146,55 @@ export default function App() { useEffect(() => { if (vaultPath) refreshNotes(); + // Load favorites + if (vaultPath) { + getFavorites(vaultPath).then(setFavorites).catch(() => setFavorites([])); + } }, [vaultPath, refreshNotes]); - // Global keyboard shortcuts - useEffect(() => { - const handler = (e: KeyboardEvent) => { - const mod = e.ctrlKey || e.metaKey; - if (mod && e.key === "k") { e.preventDefault(); setPaletteOpen((v) => !v); } - else if (mod && e.key === "n") { - e.preventDefault(); setPaletteOpen(false); - const name = prompt("Note name:"); - if (name?.trim()) { - writeNote(vaultPath, `${name.trim()}.md`, `# ${name.trim()}\n\n`).then(() => { - refreshNotes(); - navigate(`/note/${encodeURIComponent(`${name.trim()}.md`)}`); - }); - } - } - else if (mod && e.key === "g") { e.preventDefault(); setPaletteOpen(false); navigate("/graph"); } - else if (mod && e.key === "d") { e.preventDefault(); setPaletteOpen(false); navigate("/daily"); } - }; - window.addEventListener("keydown", handler); - return () => window.removeEventListener("keydown", handler); - }, [vaultPath, navigate, refreshNotes]); + // Track recent notes + const trackRecent = useCallback((notePath: string) => { + setRecentNotes(prev => { + const next = [notePath, ...prev.filter(p => p !== notePath)].slice(0, 10); + return next; + }); + }, []); - // Build backlinks for current note + // Watch currentNote changes to track recents + useEffect(() => { + if (currentNote) trackRecent(currentNote); + }, [currentNote, trackRecent]); + + // Toggle favorite + const toggleFavorite = useCallback((path: string) => { + setFavorites(prev => { + const next = prev.includes(path) + ? prev.filter(p => p !== path) + : [...prev, path]; + // Persist + if (vaultPath) setFavoritesCmd(vaultPath, next).catch(() => { }); + return next; + }); + }, [vaultPath]); + + // Switch vault + const switchVault = useCallback(async (newPath: string) => { + try { + await ensureVault(newPath); + await setVaultPath(newPath); + await addVault(newPath); + setVaultPathState(newPath); + setCurrentNote(null); + setNoteContent(""); + setSplitNote(null); + setRecentNotes([]); + navigate("/"); + } catch (e) { + console.error("Switch vault failed:", e); + } + }, [navigate, setCurrentNote, setNoteContent]); + + // Build backlinks for current note using backend context useEffect(() => { if (!vaultPath || !currentNote || !notes.length) { setBacklinks([]); @@ -142,37 +205,41 @@ export default function App() { const currentName = currentNote .replace(/\.md$/, "") .split("/") - .pop() - ?.toLowerCase(); + .pop() || ""; if (!currentName) return; - // Read all notes and find backlinks - const allPaths = flattenNotes(notes); - const entries: BacklinkEntry[] = []; - - for (const notePath of allPaths) { - if (notePath === currentNote) continue; - try { - const content = await readNote(vaultPath, notePath); - const links = extractWikilinks(content); - for (const link of links) { - if (link.target.toLowerCase() === currentName) { - const lines = content.split("\n"); - const contextLine = - lines.find((l) => l.includes(link.raw)) || ""; - entries.push({ - sourcePath: notePath, - sourceName: notePath.replace(/\.md$/, "").split("/").pop() || notePath, - context: contextLine.trim().substring(0, 200), - }); + try { + const contexts = await getBacklinkContext(vaultPath, currentName); + const entries: BacklinkEntry[] = contexts.map(c => ({ + sourcePath: c.source_path, + sourceName: c.source_name, + context: c.excerpt, + })); + setBacklinks(entries); + } catch { + // Fallback: simple wikilink scan + const allPaths = flattenNotes(notes); + const entries: BacklinkEntry[] = []; + for (const notePath of allPaths) { + if (notePath === currentNote) continue; + try { + const content = await readNote(vaultPath, notePath); + const links = extractWikilinks(content); + for (const link of links) { + if (link.target.toLowerCase() === currentName.toLowerCase()) { + const lines = content.split("\n"); + const contextLine = lines.find((l) => l.includes(link.raw)) || ""; + entries.push({ + sourcePath: notePath, + sourceName: notePath.replace(/\.md$/, "").split("/").pop() || notePath, + context: contextLine.trim().substring(0, 200), + }); + } } - } - } catch { - // Skip unreadable notes + } catch { } } + setBacklinks(entries); } - - setBacklinks(entries); })(); }, [vaultPath, currentNote, notes]); @@ -202,6 +269,61 @@ export default function App() { [notes, vaultPath, navigate, refreshNotes] ); + // ── Global keyboard shortcuts ── + useEffect(() => { + const handler = (e: KeyboardEvent) => { + const mod = e.metaKey || e.ctrlKey; + if (!mod) return; + + // ⌘⇧F — Focus mode + if (e.shiftKey && e.key.toLowerCase() === "f") { + e.preventDefault(); + setFocusMode(v => !v); + return; + } + + switch (e.key.toLowerCase()) { + case "k": + e.preventDefault(); + setCmdPaletteOpen(v => !v); + break; + case "n": + e.preventDefault(); + { + const name = prompt("Note name:"); + if (name?.trim()) navigateToNote(name.trim()); + } + break; + case "g": + e.preventDefault(); + navigate("/graph"); + break; + case "d": + e.preventDefault(); + navigate("/daily"); + break; + case "e": + e.preventDefault(); + toggleEditMode(); + break; + case "\\": + e.preventDefault(); + toggleSidebar(); + break; + case "t": + e.preventDefault(); + setThemePickerOpen(v => !v); + break; + case "h": + e.preventDefault(); + setSearchReplaceOpen(v => !v); + break; + } + }; + window.addEventListener("keydown", handler); + return () => window.removeEventListener("keydown", handler); + }, [navigate, navigateToNote, toggleEditMode, toggleSidebar]); + if (loading) { return (
    @@ -237,18 +359,41 @@ export default function App() { setNoteContent, backlinks, navigateToNote, + sidebarOpen, + toggleSidebar, + editMode, + toggleEditMode, + splitNote, + setSplitNote, + favorites, + toggleFavorite, + recentNotes, + switchVault, + focusMode, + toggleFocusMode, }} > -
    - +
    + {sidebarOpen && !focusMode && } } /> - } /> + } /> } /> } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> - setPaletteOpen(false)} />
    + setCmdPaletteOpen(false)} /> + + setThemePickerOpen(false)} /> + setSearchReplaceOpen(false)} /> + setCssEditorOpen(false)} /> ); } @@ -258,29 +403,11 @@ function NoteView() { const { path } = useParams<{ path: string }>(); const { vaultPath, setCurrentNote, noteContent, setNoteContent } = useVault(); const decodedPath = decodeURIComponent(path || ""); - const [rightTab, setRightTab] = useState<"backlinks" | "outline">("outline"); useEffect(() => { if (!decodedPath || !vaultPath) return; setCurrentNote(decodedPath); - - // Check frontend LRU cache first - const cached = noteCache.get(decodedPath); - if (cached !== undefined) { - setNoteContent(cached); - // Still re-validate from backend in background - readNote(vaultPath, decodedPath).then((fresh) => { - if (fresh !== cached) { - setNoteContent(fresh); - noteCache.set(decodedPath, fresh); - } - }).catch(() => {}); - } else { - readNote(vaultPath, decodedPath).then((content) => { - setNoteContent(content); - noteCache.set(decodedPath, content); - }).catch(() => setNoteContent("")); - } + readNote(vaultPath, decodedPath).then(setNoteContent).catch(() => setNoteContent("")); }, [decodedPath, vaultPath, setCurrentNote, setNoteContent]); return ( @@ -288,23 +415,7 @@ function NoteView() {
    - +
    ); } diff --git a/src/components/CSSEditor.tsx b/src/components/CSSEditor.tsx new file mode 100644 index 0000000..b6da6b6 --- /dev/null +++ b/src/components/CSSEditor.tsx @@ -0,0 +1,83 @@ +import { useState, useEffect, useRef } from "react"; +import { getCustomCss, setCustomCss } from "../lib/commands"; + +/** + * CSSEditor — Monaco-lite custom CSS editor with live preview. + */ +export function CSSEditor({ open, onClose }: { open: boolean; onClose: () => void }) { + const [css, setCss] = useState(""); + const [saved, setSaved] = useState(false); + const styleRef = useRef(null); + + useEffect(() => { + if (open) { + getCustomCss().then(setCss).catch(() => setCss("")); + } + }, [open]); + + // Live preview + useEffect(() => { + if (!styleRef.current) { + const el = document.createElement("style"); + el.id = "custom-user-css"; + document.head.appendChild(el); + styleRef.current = el; + } + styleRef.current.textContent = css; + }, [css]); + + const handleSave = async () => { + try { + await setCustomCss(css); + setSaved(true); + setTimeout(() => setSaved(false), 2000); + } catch (e) { + console.error("Save CSS failed:", e); + } + }; + + if (!open) return null; + + return ( +
    +
    e.stopPropagation()}> +
    +

    🎨 Custom CSS

    +
    + {saved && ✓ Saved} + + +
    +
    +