@zoldia/omnigraph 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -8,10 +8,11 @@ OmniGraph is a free, local developer tool that statically analyzes full-stack mo
8
8
 
9
9
  | Language | Extensions | Framework Detection |
10
10
  |----------|-----------|-------------------|
11
- | **TypeScript** | `.ts`, `.tsx` | NestJS (`@Controller`, `@Injectable`, `@Module`) |
11
+ | **TypeScript** | `.ts`, `.tsx` | NestJS (`@Controller`, `@Injectable`, `@Module`), Next.js (App Router, Pages Router) |
12
12
  | **JavaScript** | `.js`, `.jsx` | CommonJS and ES module imports |
13
13
  | **Python** | `.py` | FastAPI (`@router.get`, `@app.post`), Flask (`@app.route`), Django (Views, Models) |
14
14
  | **PHP** | `.php` | Laravel (Controllers, Models, Middleware, Route definitions) |
15
+ | **Markdown** | `.md`, `.mdx` | Obsidian wiki-links (`[[Page]]`), embeds (`![[Page]]`), frontmatter tags/aliases |
15
16
 
16
17
  ## Quick Start
17
18
 
@@ -44,28 +45,38 @@ omnigraph --path <repo-path> --port 4000 # Optional: custom port (default 3000)
44
45
  ## Features
45
46
 
46
47
  ### Multi-Language Dependency Graph
47
- Point OmniGraph at any project containing TypeScript, JavaScript, Python, or PHP files. It recursively walks the directory, respects `.gitignore`, and builds a dependency graph from import/require statements.
48
+ Point OmniGraph at any project containing TypeScript, JavaScript, Python, PHP, or Markdown files. It recursively walks the directory, respects `.gitignore`, and builds a dependency graph from import/require statements and wiki-links.
48
49
 
49
50
  ### Framework-Aware Parsing
50
51
  OmniGraph doesn't just find imports — it understands framework patterns:
51
52
  - **NestJS**: Detects `@Controller`, `@Injectable`, `@Module` decorators with route metadata
53
+ - **Next.js**: Detects App Router (`route.ts`, `page.tsx`, `layout.tsx`) and Pages Router (`pages/api/`)
52
54
  - **FastAPI/Flask**: Detects route decorators (`@router.get("/users")`) with HTTP methods and paths
53
55
  - **Django**: Detects class-based views (`APIView`, `ViewSet`) and models
54
56
  - **Laravel**: Detects controllers, models, middleware, and `Route::get()` definitions
57
+ - **Obsidian/Markdown**: Detects wiki-links (`[[Page]]`), embeds (`![[Page]]`), YAML frontmatter (tags, aliases), and classifies MOC/daily/readme note types
55
58
 
56
59
  ### Interactive Visualization
57
60
  - **5 Layout Presets**: Directory (grouped by folder), Hierarchical, Force-Directed, Grid, Mind Map (LR/RL)
58
61
  - **Live Force Simulation**: In force-directed mode, dragging a node causes nearby nodes to push and pull reactively via d3-force physics
59
- - **Search & Filter**: Search nodes by name, filter by type with color-coded toggle chips
62
+ - **Search & Filter with BFS Expansion**: Search nodes by name, filter by type with color-coded toggle chips. Hide or dim non-matching nodes. Connected nodes expand via BFS depth slider to reveal full data flow paths.
63
+ - **Hub-Centric Compaction**: After filtering, compact visible nodes around the most-connected hub node(s) using d3-force. Single hub stays pinned; multiple hubs meet at their average position.
60
64
  - **Node Inspector**: Click any node to see its file path, type, route metadata, and ID in the sidebar
61
- - **Color-Coded Types**: Each node type has a distinct color — controllers (red), injectables (blue), modules (orange), Python files (blue), FastAPI routes (teal), Laravel controllers (red), and more
62
-
63
- ### Sidebar Controls
64
- All controls live in a clean right sidebar:
65
- - Layout preset selector with mind map direction toggle
66
- - Real-time search with match count
67
- - Type filter chips with color legend
68
- - Node inspector panel below a divider
65
+ - **Color-Coded Types**: Each node type has a distinct color — controllers (red), injectables (blue), modules (orange), Python files (blue), FastAPI routes (teal), Laravel controllers (red), markdown (purple), and more
66
+ - **Clickable Minimap**: Zoom and pan directly on the minimap for faster navigation
67
+
68
+ ### Export
69
+ - **PNG** 2x resolution raster image
70
+ - **SVG** Scalable vector graphic
71
+ - **JSON** Raw OmniGraph data
72
+ - **GIF** 1-second animated GIF (30fps) showing edge flow direction with a progress overlay
73
+
74
+ ### Sidebar Tabs
75
+ The right sidebar has four tabs:
76
+ - **Graph** — Layout selector, search/filter with depth slider, type chips, node inspector, export dropdown, compact button
77
+ - **API** — Postman-style API debugger (auto-fills from cross-network edges)
78
+ - **Trace** — Step-through flow tracer with Back/Next navigation and animated highlighting
79
+ - **Settings** — Configurable edge labels (show/hide per type, color, font size), graph options (minimap, animations), search defaults, with per-category reset and localStorage persistence
69
80
 
70
81
  ## Technology Stack
71
82
 
@@ -77,9 +88,11 @@ All controls live in a clean right sidebar:
77
88
  | TypeScript/JS Parser | `@typescript-eslint/typescript-estree` |
78
89
  | Python Parser | Regex-based AST extraction |
79
90
  | PHP Parser | Regex-based AST extraction |
91
+ | Markdown Parser | Regex-based wiki-link/embed/frontmatter extraction |
80
92
  | Frontend | React 18 + Vite |
81
93
  | Graph Engine | React Flow |
82
- | Layout Engines | dagre (hierarchical/mind map), d3-force (force-directed) |
94
+ | Layout Engines | dagre (hierarchical/mind map), d3-force (force-directed, compaction) |
95
+ | GIF Export | gif.js (web worker encoding) |
83
96
  | Testing | Vitest |
84
97
 
85
98
  ## Architecture
@@ -152,7 +165,7 @@ All parsers produce a standardized graph format regardless of source language:
152
165
  ## Running Tests
153
166
 
154
167
  ```bash
155
- npx vitest run # Run all tests (56 tests across 6 files)
168
+ npx vitest run # Run all tests (132 tests across 9 files)
156
169
  npx vitest --watch # Watch mode
157
170
  ```
158
171
 
@@ -169,9 +182,9 @@ Contributions are welcome! Here's how to get started:
169
182
  ### Good First Issues
170
183
 
171
184
  - Add a new language parser (Go, Rust, Java, C#, Ruby)
172
- - Add export functionality (SVG, PNG, JSON)
173
185
  - Improve import resolution for edge cases (barrel exports, dynamic imports)
174
186
  - Add dark/light theme toggle
187
+ - WebSocket connection tracing
175
188
 
176
189
  ## Project Documentation
177
190
 
@@ -181,6 +194,7 @@ Contributions are welcome! Here's how to get started:
181
194
  | [SAD](docs/SAD.md) | Software architecture, data flow, and design decisions |
182
195
  | [ADR-001](docs/adr/ADR-001-parsing-engine.md) | Why typescript-estree for Phase 1 |
183
196
  | [ADR-002](docs/adr/ADR-002-phase2-multi-language-parsing.md) | Why regex-based parsing for Phase 2 Python/PHP |
197
+ | [ADR-003](docs/adr/ADR-003-markdown-obsidian-parser.md) | Markdown/Obsidian wiki-link parser design |
184
198
  | [API Spec](docs/API-SPEC.md) | HTTP endpoint and CLI interface documentation |
185
199
 
186
200
  ## License
package/dist/cli.js CHANGED
@@ -259851,6 +259851,89 @@ var require_typescript_parser = __commonJS({
259851
259851
  }
259852
259852
  return null;
259853
259853
  }
259854
+ var NEXTJS_HTTP_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"];
259855
+ function detectNextJSAppRoute(filePath, exportedNames) {
259856
+ const normalized = filePath.replace(/\\/g, "/");
259857
+ const basename = path2.basename(normalized, path2.extname(normalized));
259858
+ if (basename !== "route")
259859
+ return null;
259860
+ const appMatch = normalized.match(/(?:^|\/)(src\/)?app\/(.*?)\/route\.\w+$/);
259861
+ if (!appMatch)
259862
+ return null;
259863
+ const routeDir = appMatch[2];
259864
+ const routePath = "/" + routeDir.split("/").map((segment) => {
259865
+ if (/^\[\[?\.\.\.\w+\]?\]$/.test(segment)) {
259866
+ const name = segment.replace(/[\[\]\.]/g, "");
259867
+ return `:${name}*`;
259868
+ }
259869
+ if (/^\[\w+\]$/.test(segment)) {
259870
+ return ":" + segment.slice(1, -1);
259871
+ }
259872
+ return segment;
259873
+ }).join("/");
259874
+ const methods = exportedNames.filter((n) => NEXTJS_HTTP_METHODS.includes(n));
259875
+ if (methods.length === 0) {
259876
+ return { nodeType: "nextjs-api-route", route: routePath };
259877
+ }
259878
+ const routeEntries = methods.map((m) => `${m} ${routePath}`).join(", ");
259879
+ return { nodeType: "nextjs-api-route", route: routeEntries };
259880
+ }
259881
+ function detectNextJSPagesApiRoute(filePath, hasDefaultExport) {
259882
+ if (!hasDefaultExport)
259883
+ return null;
259884
+ const normalized = filePath.replace(/\\/g, "/");
259885
+ const basename = path2.basename(normalized, path2.extname(normalized));
259886
+ const pagesMatch = normalized.match(/(?:^|\/)(src\/)?pages\/api\/(.*?)(\.\w+)$/);
259887
+ if (!pagesMatch)
259888
+ return null;
259889
+ let routeSegments = pagesMatch[2];
259890
+ if (basename === "index") {
259891
+ routeSegments = routeSegments.replace(/\/?index$/, "");
259892
+ }
259893
+ const segments = routeSegments.split("/").filter(Boolean).map((segment) => {
259894
+ if (/^\[\[?\.\.\.\w+\]?\]$/.test(segment)) {
259895
+ const name = segment.replace(/[\[\]\.]/g, "");
259896
+ return `:${name}*`;
259897
+ }
259898
+ if (/^\[\w+\]$/.test(segment)) {
259899
+ return ":" + segment.slice(1, -1);
259900
+ }
259901
+ return segment;
259902
+ });
259903
+ const routePath = segments.length > 0 ? "/api/" + segments.join("/") : "/api";
259904
+ return { nodeType: "nextjs-api-route", route: routePath };
259905
+ }
259906
+ function detectNextJSPage(filePath) {
259907
+ const normalized = filePath.replace(/\\/g, "/");
259908
+ const basename = path2.basename(normalized, path2.extname(normalized));
259909
+ if (basename !== "page")
259910
+ return null;
259911
+ const appMatch = normalized.match(/(?:^|\/)(src\/)?app\/(.*?)\/page\.\w+$/);
259912
+ if (!appMatch)
259913
+ return null;
259914
+ const routeDir = appMatch[2];
259915
+ const routePath = "/" + routeDir.split("/").map((segment) => {
259916
+ if (/^\[\[?\.\.\.\w+\]?\]$/.test(segment)) {
259917
+ const name = segment.replace(/[\[\]\.]/g, "");
259918
+ return `:${name}*`;
259919
+ }
259920
+ if (/^\[\w+\]$/.test(segment)) {
259921
+ return ":" + segment.slice(1, -1);
259922
+ }
259923
+ return segment;
259924
+ }).join("/");
259925
+ return { nodeType: "nextjs-page", route: routePath };
259926
+ }
259927
+ function detectNextJSLayout(filePath) {
259928
+ const normalized = filePath.replace(/\\/g, "/");
259929
+ const basename = path2.basename(normalized, path2.extname(normalized));
259930
+ if (basename !== "layout")
259931
+ return null;
259932
+ const appMatch = normalized.match(/(?:^|\/)(src\/)?app\/(.+\/)?layout\.\w+$/);
259933
+ if (!appMatch)
259934
+ return null;
259935
+ return { nodeType: "nextjs-layout", route: "" };
259936
+ }
259854
259937
  var RESOLVE_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx"];
259855
259938
  function resolveImport(fromFile, importPath) {
259856
259939
  const base = path2.normalize(path2.join(path2.dirname(fromFile), importPath));
@@ -259888,6 +259971,8 @@ var require_typescript_parser = __commonJS({
259888
259971
  const isJS = /\.(js|jsx)$/.test(filePath);
259889
259972
  let nodeType = isJS ? "javascript-file" : "typescript-file";
259890
259973
  let route = "";
259974
+ const exportedNames = [];
259975
+ let hasDefaultExport = false;
259891
259976
  for (const stmt of ast.body) {
259892
259977
  if (stmt.type === "ImportDeclaration") {
259893
259978
  const src = stmt.source.value;
@@ -259904,6 +259989,31 @@ var require_typescript_parser = __commonJS({
259904
259989
  }
259905
259990
  }
259906
259991
  }
259992
+ if (stmt.type === "ExportNamedDeclaration") {
259993
+ if (stmt.declaration) {
259994
+ const decl = stmt.declaration;
259995
+ if (decl.type === "FunctionDeclaration" && decl.id && typeof decl.id.name === "string") {
259996
+ exportedNames.push(decl.id.name);
259997
+ }
259998
+ if (decl.type === "VariableDeclaration" && Array.isArray(decl.declarations)) {
259999
+ for (const d of decl.declarations) {
260000
+ if (d.id && typeof d.id.name === "string") {
260001
+ exportedNames.push(d.id.name);
260002
+ }
260003
+ }
260004
+ }
260005
+ }
260006
+ if (stmt.specifiers) {
260007
+ for (const spec of stmt.specifiers) {
260008
+ if (spec.exported && typeof spec.exported.name === "string") {
260009
+ exportedNames.push(spec.exported.name);
260010
+ }
260011
+ }
260012
+ }
260013
+ }
260014
+ if (stmt.type === "ExportDefaultDeclaration") {
260015
+ hasDefaultExport = true;
260016
+ }
259907
260017
  let classNode = null;
259908
260018
  if (stmt.type === "ExportNamedDeclaration" && stmt.declaration?.type === "ClassDeclaration") {
259909
260019
  classNode = stmt.declaration;
@@ -259919,6 +260029,13 @@ var require_typescript_parser = __commonJS({
259919
260029
  }
259920
260030
  }
259921
260031
  }
260032
+ if (nodeType === "typescript-file" || nodeType === "javascript-file") {
260033
+ const nextResult = detectNextJSAppRoute(filePath, exportedNames) ?? detectNextJSPagesApiRoute(filePath, hasDefaultExport) ?? detectNextJSPage(filePath) ?? detectNextJSLayout(filePath);
260034
+ if (nextResult) {
260035
+ nodeType = nextResult.nodeType;
260036
+ route = nextResult.route;
260037
+ }
260038
+ }
259922
260039
  const node = {
259923
260040
  id: fileId,
259924
260041
  type: nodeType,
@@ -260433,6 +260550,281 @@ var require_php_parser = __commonJS({
260433
260550
  }
260434
260551
  });
260435
260552
 
260553
+ // packages/parsers/dist/markdown/markdown-parser.js
260554
+ var require_markdown_parser = __commonJS({
260555
+ "packages/parsers/dist/markdown/markdown-parser.js"(exports2) {
260556
+ "use strict";
260557
+ var __createBinding = exports2 && exports2.__createBinding || (Object.create ? (function(o, m, k, k2) {
260558
+ if (k2 === void 0) k2 = k;
260559
+ var desc = Object.getOwnPropertyDescriptor(m, k);
260560
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
260561
+ desc = { enumerable: true, get: function() {
260562
+ return m[k];
260563
+ } };
260564
+ }
260565
+ Object.defineProperty(o, k2, desc);
260566
+ }) : (function(o, m, k, k2) {
260567
+ if (k2 === void 0) k2 = k;
260568
+ o[k2] = m[k];
260569
+ }));
260570
+ var __setModuleDefault = exports2 && exports2.__setModuleDefault || (Object.create ? (function(o, v) {
260571
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
260572
+ }) : function(o, v) {
260573
+ o["default"] = v;
260574
+ });
260575
+ var __importStar = exports2 && exports2.__importStar || /* @__PURE__ */ (function() {
260576
+ var ownKeys = function(o) {
260577
+ ownKeys = Object.getOwnPropertyNames || function(o2) {
260578
+ var ar = [];
260579
+ for (var k in o2) if (Object.prototype.hasOwnProperty.call(o2, k)) ar[ar.length] = k;
260580
+ return ar;
260581
+ };
260582
+ return ownKeys(o);
260583
+ };
260584
+ return function(mod) {
260585
+ if (mod && mod.__esModule) return mod;
260586
+ var result = {};
260587
+ if (mod != null) {
260588
+ for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
260589
+ }
260590
+ __setModuleDefault(result, mod);
260591
+ return result;
260592
+ };
260593
+ })();
260594
+ Object.defineProperty(exports2, "__esModule", { value: true });
260595
+ exports2.MarkdownParser = void 0;
260596
+ var path2 = __importStar(require("path"));
260597
+ var fs = __importStar(require("fs"));
260598
+ var FRONTMATTER = /^---\r?\n([\s\S]*?)\r?\n---/;
260599
+ var YAML_TAGS_INLINE = /^tags:\s*\[([^\]]*)\]/m;
260600
+ var YAML_TAGS_KEY = /^tags:\s*$/m;
260601
+ var YAML_ALIASES = /^aliases:\s*\[([^\]]*)\]/m;
260602
+ var IMAGE_EXTENSIONS = /* @__PURE__ */ new Set([".png", ".jpg", ".jpeg", ".gif", ".svg", ".webp", ".bmp"]);
260603
+ var MarkdownParser = class {
260604
+ constructor() {
260605
+ this.rootDir = "";
260606
+ }
260607
+ setRootDir(dir) {
260608
+ this.rootDir = dir;
260609
+ }
260610
+ canHandle(filePath) {
260611
+ const ext = path2.extname(filePath).toLowerCase();
260612
+ return ext === ".md" || ext === ".mdx";
260613
+ }
260614
+ parse(filePath, source) {
260615
+ const nodes = [];
260616
+ const edges = [];
260617
+ const fileId = filePath.replace(/\\/g, "/");
260618
+ const fileName = path2.basename(filePath, path2.extname(filePath));
260619
+ const frontmatter = this.parseFrontmatter(source);
260620
+ const headings = [];
260621
+ let headingMatch;
260622
+ const headingRegex = /^(#{1,6})\s+(.+)$/gm;
260623
+ while ((headingMatch = headingRegex.exec(source)) !== null) {
260624
+ headings.push(headingMatch[2].trim());
260625
+ }
260626
+ let nodeType = "markdown-file";
260627
+ if (frontmatter.tags.some((t) => t.toLowerCase() === "moc" || t.toLowerCase() === "index")) {
260628
+ nodeType = "markdown-moc";
260629
+ } else if (frontmatter.tags.some((t) => t.toLowerCase() === "daily" || t.toLowerCase() === "journal")) {
260630
+ nodeType = "markdown-daily";
260631
+ } else if (filePath.toLowerCase().includes("readme")) {
260632
+ nodeType = "markdown-readme";
260633
+ }
260634
+ const metadata = {
260635
+ filePath,
260636
+ language: "markdown"
260637
+ };
260638
+ if (headings.length > 0) {
260639
+ metadata.headings = headings.slice(0, 8).join(", ");
260640
+ }
260641
+ if (frontmatter.tags.length > 0) {
260642
+ metadata.tags = frontmatter.tags.join(", ");
260643
+ }
260644
+ if (frontmatter.aliases.length > 0) {
260645
+ metadata.aliases = frontmatter.aliases.join(", ");
260646
+ }
260647
+ nodes.push({
260648
+ id: fileId,
260649
+ type: nodeType,
260650
+ label: fileName,
260651
+ metadata
260652
+ });
260653
+ const body = this.stripFrontmatterAndCode(source);
260654
+ const linkedTargets = /* @__PURE__ */ new Set();
260655
+ let match;
260656
+ const wikiRegex = /\[\[([^\]|#]+)(?:#[^\]|]*)?\|?[^\]]*\]\]/g;
260657
+ while ((match = wikiRegex.exec(body)) !== null) {
260658
+ const target = match[1].trim();
260659
+ if (!target)
260660
+ continue;
260661
+ const ext = path2.extname(target).toLowerCase();
260662
+ if (IMAGE_EXTENSIONS.has(ext))
260663
+ continue;
260664
+ const resolved = this.resolveWikiLink(filePath, target);
260665
+ if (resolved && !linkedTargets.has(resolved)) {
260666
+ linkedTargets.add(resolved);
260667
+ edges.push({
260668
+ id: `e-${fileId}->${resolved}`,
260669
+ source: fileId,
260670
+ target: resolved,
260671
+ label: "links to"
260672
+ });
260673
+ }
260674
+ }
260675
+ const embedRegex = /!\[\[([^\]|#]+)(?:#[^\]|]*)?\|?[^\]]*\]\]/g;
260676
+ while ((match = embedRegex.exec(body)) !== null) {
260677
+ const target = match[1].trim();
260678
+ if (!target)
260679
+ continue;
260680
+ const ext = path2.extname(target).toLowerCase();
260681
+ if (IMAGE_EXTENSIONS.has(ext))
260682
+ continue;
260683
+ const resolved = this.resolveWikiLink(filePath, target);
260684
+ if (resolved && !linkedTargets.has(resolved)) {
260685
+ linkedTargets.add(resolved);
260686
+ edges.push({
260687
+ id: `e-${fileId}->embed-${resolved}`,
260688
+ source: fileId,
260689
+ target: resolved,
260690
+ label: "embeds"
260691
+ });
260692
+ }
260693
+ }
260694
+ const mdLinkRegex = /\[(?:[^\]]*)\]\((?!https?:\/\/|mailto:|#)([^)]+\.(?:md|mdx))\)/g;
260695
+ while ((match = mdLinkRegex.exec(body)) !== null) {
260696
+ const linkPath = match[1].trim();
260697
+ if (!linkPath)
260698
+ continue;
260699
+ const resolved = this.resolveRelativeLink(filePath, linkPath);
260700
+ if (resolved && !linkedTargets.has(resolved)) {
260701
+ linkedTargets.add(resolved);
260702
+ edges.push({
260703
+ id: `e-${fileId}->${resolved}`,
260704
+ source: fileId,
260705
+ target: resolved,
260706
+ label: "links to"
260707
+ });
260708
+ }
260709
+ }
260710
+ return { nodes, edges };
260711
+ }
260712
+ // ─── Frontmatter Parsing ─────────────────────────────────────────
260713
+ parseFrontmatter(source) {
260714
+ const tags = [];
260715
+ const aliases = [];
260716
+ const fmMatch = FRONTMATTER.exec(source);
260717
+ if (!fmMatch)
260718
+ return { tags, aliases };
260719
+ const fmBlock = fmMatch[1];
260720
+ const tagsInline = YAML_TAGS_INLINE.exec(fmBlock);
260721
+ if (tagsInline) {
260722
+ tags.push(...tagsInline[1].split(",").map((t) => t.trim().replace(/^['"]|['"]$/g, "")).filter(Boolean));
260723
+ } else if (YAML_TAGS_KEY.test(fmBlock)) {
260724
+ let itemMatch;
260725
+ const itemRegex = /^\s+-\s+(.+)$/gm;
260726
+ const tagsSection = fmBlock.slice(fmBlock.indexOf("tags:"));
260727
+ while ((itemMatch = itemRegex.exec(tagsSection)) !== null) {
260728
+ tags.push(itemMatch[1].trim().replace(/^['"]|['"]$/g, ""));
260729
+ }
260730
+ }
260731
+ const aliasMatch = YAML_ALIASES.exec(fmBlock);
260732
+ if (aliasMatch) {
260733
+ aliases.push(...aliasMatch[1].split(",").map((a) => a.trim().replace(/^['"]|['"]$/g, "")).filter(Boolean));
260734
+ }
260735
+ return { tags, aliases };
260736
+ }
260737
+ // ─── Link Resolution ─────────────────────────────────────────────
260738
+ /**
260739
+ * Resolve an Obsidian wiki-link target to a file path.
260740
+ * Obsidian uses "shortest path when possible" — [[Page]] matches
260741
+ * any file named Page.md anywhere in the vault.
260742
+ */
260743
+ resolveWikiLink(fromFile, target) {
260744
+ const dir = path2.dirname(fromFile);
260745
+ if (path2.extname(target)) {
260746
+ return this.tryResolve(dir, target);
260747
+ }
260748
+ const resolved = this.tryResolve(dir, target + ".md") ?? this.tryResolve(dir, target + ".mdx");
260749
+ if (resolved)
260750
+ return resolved;
260751
+ if (this.rootDir) {
260752
+ const found = this.findFileInVault(target);
260753
+ if (found)
260754
+ return found;
260755
+ }
260756
+ return null;
260757
+ }
260758
+ /** Resolve a standard relative markdown link */
260759
+ resolveRelativeLink(fromFile, linkPath) {
260760
+ const dir = path2.dirname(fromFile);
260761
+ const candidate = path2.resolve(dir, linkPath);
260762
+ if (fs.existsSync(candidate)) {
260763
+ return candidate.replace(/\\/g, "/");
260764
+ }
260765
+ return null;
260766
+ }
260767
+ /** Try resolving a path relative to the current directory or as absolute */
260768
+ tryResolve(dir, target) {
260769
+ const relative = path2.resolve(dir, target);
260770
+ if (fs.existsSync(relative)) {
260771
+ return relative.replace(/\\/g, "/");
260772
+ }
260773
+ if (this.rootDir) {
260774
+ const fromRoot = path2.resolve(this.rootDir, target);
260775
+ if (fs.existsSync(fromRoot)) {
260776
+ return fromRoot.replace(/\\/g, "/");
260777
+ }
260778
+ }
260779
+ return null;
260780
+ }
260781
+ /**
260782
+ * Search the vault (rootDir) recursively for a file matching the target name.
260783
+ * This implements Obsidian's "shortest path" resolution for [[Page]] links.
260784
+ */
260785
+ findFileInVault(target) {
260786
+ if (!this.rootDir)
260787
+ return null;
260788
+ const targetLower = target.toLowerCase();
260789
+ const extensions = [".md", ".mdx"];
260790
+ const queue = [this.rootDir];
260791
+ const skip = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", ".next", "build", ".obsidian"]);
260792
+ while (queue.length > 0) {
260793
+ const dir = queue.shift();
260794
+ let entries;
260795
+ try {
260796
+ entries = fs.readdirSync(dir, { withFileTypes: true });
260797
+ } catch {
260798
+ continue;
260799
+ }
260800
+ for (const entry of entries) {
260801
+ if (entry.isDirectory()) {
260802
+ if (!skip.has(entry.name)) {
260803
+ queue.push(path2.join(dir, entry.name));
260804
+ }
260805
+ } else if (entry.isFile()) {
260806
+ const name = path2.basename(entry.name, path2.extname(entry.name));
260807
+ const ext = path2.extname(entry.name).toLowerCase();
260808
+ if (name.toLowerCase() === targetLower && extensions.includes(ext)) {
260809
+ return path2.join(dir, entry.name).replace(/\\/g, "/");
260810
+ }
260811
+ }
260812
+ }
260813
+ }
260814
+ return null;
260815
+ }
260816
+ /** Strip frontmatter and fenced code blocks so link regexes don't match inside them */
260817
+ stripFrontmatterAndCode(source) {
260818
+ let body = source.replace(FRONTMATTER, "");
260819
+ body = body.replace(/```[\s\S]*?```/g, "");
260820
+ body = body.replace(/`[^`]*`/g, "");
260821
+ return body;
260822
+ }
260823
+ };
260824
+ exports2.MarkdownParser = MarkdownParser;
260825
+ }
260826
+ });
260827
+
260436
260828
  // packages/parsers/dist/cross-network/http-call-detector.js
260437
260829
  var require_http_call_detector = __commonJS({
260438
260830
  "packages/parsers/dist/cross-network/http-call-detector.js"(exports2) {
@@ -260795,13 +261187,15 @@ var require_parser_registry = __commonJS({
260795
261187
  var typescript_parser_1 = require_typescript_parser();
260796
261188
  var python_parser_1 = require_python_parser();
260797
261189
  var php_parser_1 = require_php_parser();
261190
+ var markdown_parser_1 = require_markdown_parser();
260798
261191
  var cross_network_1 = require_cross_network();
260799
261192
  var fs = __importStar(require("fs"));
260800
261193
  var path2 = __importStar(require("path"));
260801
261194
  var ignore_1 = __importDefault(require_ignore());
260802
261195
  var pythonParser = new python_parser_1.PythonParser();
260803
261196
  var phpParser = new php_parser_1.PhpParser();
260804
- var parsers = [new typescript_parser_1.TypeScriptParser(), pythonParser, phpParser];
261197
+ var markdownParser = new markdown_parser_1.MarkdownParser();
261198
+ var parsers = [new typescript_parser_1.TypeScriptParser(), pythonParser, phpParser, markdownParser];
260805
261199
  var ALWAYS_SKIP = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", ".next", "build"]);
260806
261200
  function loadGitignore(rootDir) {
260807
261201
  const ig = (0, ignore_1.default)();
@@ -260820,6 +261214,7 @@ var require_parser_registry = __commonJS({
260820
261214
  const sourceByFileId = /* @__PURE__ */ new Map();
260821
261215
  pythonParser.setRootDir(dirPath);
260822
261216
  phpParser.setRootDir(dirPath);
261217
+ markdownParser.setRootDir(dirPath);
260823
261218
  function walk(dir) {
260824
261219
  const entries = fs.readdirSync(dir, { withFileTypes: true });
260825
261220
  for (const entry of entries) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@zoldia/omnigraph",
3
- "version": "1.0.0",
4
- "description": "A multi-language, AST-driven dependency visualizer for complex codebases. Parses TypeScript/NestJS, Python/FastAPI/Django, and PHP/Laravel and renders an interactive dependency graph.",
3
+ "version": "1.1.0",
4
+ "description": "A multi-language, AST-driven dependency visualizer for complex codebases. Parses TypeScript/NestJS/Next.js, Python/FastAPI/Django, PHP/Laravel, and Markdown/Obsidian and renders an interactive dependency graph.",
5
5
  "bin": {
6
6
  "omnigraph": "dist/cli.js"
7
7
  },
@@ -19,12 +19,16 @@
19
19
  "typescript",
20
20
  "python",
21
21
  "php",
22
+ "markdown",
22
23
  "nestjs",
24
+ "nextjs",
23
25
  "fastapi",
24
26
  "django",
25
27
  "laravel",
28
+ "obsidian",
29
+ "wiki-links",
26
30
  "react-flow",
27
- "obsidian"
31
+ "gif-export"
28
32
  ],
29
33
  "repository": {
30
34
  "type": "git",