@dogsbay/format-obsidian 0.2.0-beta.14 → 0.2.0-beta.141

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.
@@ -0,0 +1,25 @@
1
+ /**
2
+ * What the Obsidian exporter can and cannot represent — a CONTRACT (the
3
+ * format-docusaurus pattern): `validateCapabilities` rejects any non-full
4
+ * entry without a note, and a test asserts every `MATRIX_ROWS` type has
5
+ * exactly one declaration here. Each entry states what a READER sees in the
6
+ * vault, measured by running `treeToObsidian` on nodes parsed from real
7
+ * Dogsbay MD — not what the serializer intends.
8
+ *
9
+ * Several rich blocks are emitted as `dogsbay-*` fenced code blocks
10
+ * (`dogsbay-tabs`, `dogsbay-steps`, `dogsbay-deflist`, `dogsbay-card`,
11
+ * `dogsbay-code`) that the `dogsbay-docs` Obsidian plugin renders. The vault
12
+ * export bundles that plugin by default; WITHOUT it a reader sees those
13
+ * blocks as literal code. "full" for them means "full with the plugin".
14
+ *
15
+ * IMPORTANT: this exporter keeps NO ledger. `walkInline` is called without a
16
+ * `report` callback, so even the losses the shared inline walk does report
17
+ * (unsafe link/image targets, inline types with no emitter) go nowhere. Every
18
+ * partial/none below is therefore, today, a loss nothing records at export
19
+ * time — this declaration is the only place it is stated.
20
+ *
21
+ * Support levels: full — faithful · partial — survives with reduced fidelity
22
+ * (note says how) · none — dropped or flattened (note says what is lost).
23
+ */
24
+ import type { CapabilityDeclaration } from "@dogsbay/serialize-core";
25
+ export declare const OBSIDIAN_CAPABILITIES: CapabilityDeclaration[];
@@ -0,0 +1,171 @@
1
+ export const OBSIDIAN_CAPABILITIES = [
2
+ // ── Blocks ───────────────────────────────────────────────
3
+ { feature: "heading", support: "full", rendersAs: "`#`–`######` + a sanitized, per-page-unique `^block-id` from the slug" },
4
+ { feature: "paragraph", support: "full" },
5
+ { feature: "blockquote", support: "full", rendersAs: "`> …`" },
6
+ { feature: "unordered-list", support: "full", rendersAs: "`- item`" },
7
+ { feature: "ordered-list", support: "full", rendersAs: "`1. item` (honours `start`)" },
8
+ { feature: "hr", support: "full", rendersAs: "`---`" },
9
+ {
10
+ feature: "table",
11
+ support: "partial",
12
+ rendersAs: "GFM pipe table",
13
+ note: "Cells are single-line: a block inside a cell (list, code, paragraphs) is flattened to one line of text (`- a - b`). A table `caption` is dropped.",
14
+ },
15
+ {
16
+ feature: "code",
17
+ support: "partial",
18
+ rendersAs: "plain fence; `dogsbay-code` block (lang/file/highlight/annotations header) when it carries a title, highlights or annotations",
19
+ note: "Faithful at top level and one container deep. Nested two or more containers deep (tabs/steps/deflist) it becomes a 4-space indented code block: language, title, highlights and annotations are lost.",
20
+ },
21
+ { feature: "math-block", support: "full", rendersAs: "`$$ … $$` (native Obsidian MathJax)" },
22
+ { feature: "deflist", support: "full", rendersAs: "`dogsbay-deflist` block (plugin renders a `<dl>`; definition bodies are full block markdown)" },
23
+ {
24
+ feature: "figure",
25
+ support: "partial",
26
+ rendersAs: "the image, then the caption as an italic paragraph",
27
+ note: "No figure/figcaption semantics; the caption is a separate paragraph.",
28
+ },
29
+ { feature: "footnote-list", support: "full", rendersAs: "`[^label]: …` definitions (native Obsidian footnotes)" },
30
+ {
31
+ feature: "html",
32
+ support: "partial",
33
+ rendersAs: "raw HTML passed through (`/_assets/` src/href made vault-relative)",
34
+ note: "Obsidian's reading-view sanitizer strips scripts, `style`, `class`, `data-*` and most attributes; layout-only markup does nothing. Markdown inside the HTML block is not rendered.",
35
+ },
36
+ {
37
+ feature: "html-container",
38
+ support: "partial",
39
+ rendersAs: "its HTML (Obsidian sanitizes it), or its children in place",
40
+ note: "A children-only container's own markup (tag, class) is dropped; the content is kept.",
41
+ },
42
+ { feature: "diagram", support: "full", rendersAs: "```mermaid fence (native Obsidian Mermaid)" },
43
+ {
44
+ feature: "youtube",
45
+ support: "partial",
46
+ rendersAs: "thumbnail image linking to youtube.com/watch",
47
+ note: "Not an embedded player: the video opens in a browser.",
48
+ },
49
+ // ── Components ───────────────────────────────────────────
50
+ {
51
+ feature: "callout",
52
+ support: "partial",
53
+ rendersAs: "Obsidian callout `> [!variant] Title`",
54
+ note: "Variant names pass through verbatim; one Obsidian does not know renders as a note. A custom `icon` is dropped.",
55
+ },
56
+ { feature: "details", support: "full", rendersAs: "foldable callout `> [!variant]- Title` (`+` when open)" },
57
+ { feature: "details-sheet", support: "partial", rendersAs: "a normal collapsible block", note: "Obsidian has no side-sheet component: a `sheet` details is written as an ordinary collapsible block, content intact." },
58
+ { feature: "theme", support: "partial", rendersAs: "the content, unthemed", note: "Obsidian has no scoped themes: the wrapper is dropped and the content kept, in the page's own theme." },
59
+ { feature: "tabs-variant", support: "partial", rendersAs: "the default tabs", note: "Obsidian draws one tab style: a pills or underline variant shows as the default." },
60
+ { feature: "tone", support: "partial", rendersAs: "untinted cards", note: "Obsidian cards take no tone: the colour is lost, the content kept." },
61
+ { feature: "tabs", support: "full", rendersAs: "`dogsbay-tabs` block (plugin renders tabs; fence characters alternate by nesting depth)" },
62
+ { feature: "steps", support: "full", rendersAs: "`dogsbay-steps` block (plugin renders numbered steps with titles)" },
63
+ {
64
+ feature: "accordion",
65
+ support: "partial",
66
+ rendersAs: "one collapsed foldable callout per item, `> [!note]- Label`",
67
+ note: "Obsidian has no accordion: items fold independently, so opening one does not close the others, and a `defaultValue` is not applied.",
68
+ },
69
+ {
70
+ feature: "cards",
71
+ support: "partial",
72
+ rendersAs: "each card as its own `dogsbay-card` block, stacked",
73
+ note: "No grid container: the column layout is lost.",
74
+ },
75
+ {
76
+ feature: "card",
77
+ support: "partial",
78
+ rendersAs: "`dogsbay-card` block (title, href with `.md`, icon, body)",
79
+ note: "An icon that is a registry NAME (`open-book`, `discord`) is dropped — the vault has no icon registry; image-path and emoji icons are kept.",
80
+ },
81
+ {
82
+ feature: "link-card",
83
+ support: "partial",
84
+ rendersAs: "`dogsbay-card` block (title, href, description as the body)",
85
+ note: "Rendered as a card by the dogsbay-docs plugin; the link-card's compact styling is not distinct from a card's.",
86
+ },
87
+ {
88
+ feature: "files",
89
+ support: "partial",
90
+ rendersAs: "the nested list",
91
+ note: "No file-tree view: folders and files are a nested bulleted list.",
92
+ },
93
+ {
94
+ feature: "grid",
95
+ support: "partial",
96
+ rendersAs: "its children stacked (cards as `dogsbay-card` blocks)",
97
+ note: "No grid container: the column layout is lost.",
98
+ },
99
+ {
100
+ feature: "link-button",
101
+ support: "partial",
102
+ rendersAs: "an ordinary Markdown link",
103
+ note: "Button styling and variant are lost; an unsafe href leaves the label as plain text.",
104
+ },
105
+ { feature: "related", support: "full", rendersAs: "bold label + the link list" },
106
+ {
107
+ feature: "example",
108
+ support: "partial",
109
+ rendersAs: "the rendered example content",
110
+ note: "No case: the fallthrough renders children only. The example's frame and its `source` (the Markdown shown alongside the result) are dropped.",
111
+ },
112
+ {
113
+ feature: "container",
114
+ support: "partial",
115
+ rendersAs: "its children in place",
116
+ note: "No case: the container's name and title (`props.title` / `meta.title`) are dropped; only the body survives.",
117
+ },
118
+ { feature: "annotation-list", support: "full", rendersAs: "its children; the annotations also ride in the `dogsbay-code` header" },
119
+ // ── API reference ────────────────────────────────────────
120
+ {
121
+ feature: "endpoint",
122
+ support: "partial",
123
+ rendersAs: "`### METHOD path` heading, summary, description, Markdown tables for parameters / request body / responses, folded `[!example]` callouts for samples and examples",
124
+ note: "Static: no try-it, no response-tab switching. Spec descriptions are neutralized (their Markdown headings become bold lines); nested schema properties flatten to dotted rows.",
125
+ },
126
+ {
127
+ feature: "api-doc",
128
+ support: "partial",
129
+ rendersAs: "the doc HTML converted to Markdown",
130
+ note: "Inline tags become Markdown and the rest is stripped (flatten mode): block structure inside the docstring HTML (lists, tables) is flattened.",
131
+ },
132
+ { feature: "api-class", support: "full", rendersAs: "`### kind \\`name\\``, `Bases:` line, signature in a python fence, children" },
133
+ { feature: "api-member", support: "full", rendersAs: "`#### kind \\`name\\``, signature in a python fence, children" },
134
+ { feature: "api-params", support: "full", rendersAs: "HTML table (name, description, TYPE / DEFAULT / REQUIRED meta)" },
135
+ { feature: "api-source", support: "full", rendersAs: "`*Source: file:line*` + the source in a fence" },
136
+ // ── Inline ───────────────────────────────────────────────
137
+ {
138
+ feature: "link",
139
+ support: "full",
140
+ rendersAs: "`[text](vault/path#^block-id)` — site paths made vault-relative, anchors as block-id refs",
141
+ note: "An unsafe scheme keeps the text and drops the target (not recorded).",
142
+ },
143
+ {
144
+ feature: "image",
145
+ support: "partial",
146
+ rendersAs: "`![alt](_assets/…)` (site root stripped)",
147
+ note: "The image `title` is dropped; an unsafe source leaves only the alt text.",
148
+ },
149
+ { feature: "kbd", support: "full", rendersAs: "`<kbd>Ctrl</kbd>+<kbd>S</kbd>` (Obsidian keeps `<kbd>`)" },
150
+ { feature: "math", support: "full", rendersAs: "`$…$`" },
151
+ { feature: "highlight", support: "full", rendersAs: "`==text==`" },
152
+ {
153
+ feature: "badge",
154
+ support: "partial",
155
+ rendersAs: "the badge text as plain text",
156
+ note: "No inline emitter: the shared walk's fallback writes the text and its report goes nowhere. Variant/styling lost — `Beta` reads as a word in the sentence.",
157
+ },
158
+ {
159
+ feature: "icon",
160
+ support: "partial",
161
+ rendersAs: "`:name:` shortcode text",
162
+ note: "Obsidian does not render emoji/icon shortcodes: the reader sees the literal `:name:`.",
163
+ },
164
+ { feature: "footnote-ref", support: "full", rendersAs: "`[^label]`" },
165
+ {
166
+ feature: "html-inline",
167
+ support: "partial",
168
+ rendersAs: "raw inline HTML passed through",
169
+ note: "Obsidian's sanitizer strips attributes and most tags in reading view; `<kbd>`, `<b>`, `<i>` and similar survive.",
170
+ },
171
+ ];
package/dist/cli.d.ts CHANGED
@@ -1,3 +1,2 @@
1
1
  import type { FormatPlugin } from "@dogsbay/types";
2
2
  export declare const plugin: FormatPlugin;
3
- //# sourceMappingURL=cli.d.ts.map
package/dist/cli.js CHANGED
@@ -1,17 +1,20 @@
1
- import { existsSync, mkdirSync, writeFileSync, cpSync, readdirSync, statSync } from "node:fs";
1
+ import { existsSync, mkdirSync, writeFileSync, readFileSync, cpSync, readdirSync, statSync } from "node:fs";
2
2
  import { join, dirname, resolve, relative } from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
4
  import { exportObsidianVault } from "./project.js";
5
+ import { OBSIDIAN_CAPABILITIES } from "./capabilities.js";
5
6
  const PLUGIN_ID = "dogsbay-docs";
6
7
  export const plugin = {
7
8
  name: "obsidian",
8
9
  canImport: false,
9
10
  canExport: true,
11
+ capabilities: OBSIDIAN_CAPABILITIES,
10
12
  detectSource: (path) => existsSync(join(path, ".obsidian")),
11
13
  importOptions: [],
12
14
  exportOptions: [
13
15
  { flags: "--tabs <mode>", description: "Tab handling: dogsbay-tabs (default) or flatten", default: "dogsbay-tabs" },
14
16
  { flags: "--plugin <mode>", description: "Plugin setup: bundled (copy into vault), registry (list only), none", default: "bundled" },
17
+ { flags: "--graph-groups <path>", description: "JSON file of graph-view colour groups: [{ \"query\": \"path:guides\", \"color\": \"#F2B36F\" }]. Replaces the default hub group. Ignored if the vault already has a graph.json" },
15
18
  ],
16
19
  async export(pages, nav, output, opts) {
17
20
  const pluginMode = opts.plugin ?? "bundled";
@@ -26,7 +29,34 @@ export const plugin = {
26
29
  writeFileSync(fullPath, content);
27
30
  }
28
31
  console.log(`Generated ${result.files.size} vault files`);
29
- // Copy images from source docs directory into vault
32
+ // Mounted asset dirs (a Starlight `src/assets`, a Docusaurus `static/`),
33
+ // copied to `_assets/` to match the `/_assets/…` refs importers rewrite
34
+ // image paths to. Without this the vault carried the reference and not the
35
+ // file — the note renders with a broken image and nothing reports it.
36
+ const mounts = opts.assetMounts;
37
+ for (const mount of mounts ?? []) {
38
+ if (!existsSync(mount.dir))
39
+ continue;
40
+ // The mount carries the root its refs were rewritten to — `_assets`
41
+ // for an imported corpus, `images`/`assets` for hand-authored content.
42
+ // Hardcoding `_assets` here would copy the file somewhere the
43
+ // reference does not point, which is the broken-image-and-no-report
44
+ // failure this copy exists to prevent.
45
+ const root = mount.root ?? "_assets";
46
+ const dest = mount.into ? join(output, root, mount.into) : join(output, root);
47
+ mkdirSync(dest, { recursive: true });
48
+ // `mediaOnly` mounts are whole docs trees (the Docusaurus and VitePress
49
+ // importers mount their entire docs dir this way) — copying them wholesale
50
+ // buries hundreds of duplicate .md/.mdx notes under _assets/. Take only
51
+ // the media, matching format-dogsbay-md's copyMediaTree.
52
+ if (mount.mediaOnly) {
53
+ copyMediaTree(mount.dir, dest);
54
+ continue;
55
+ }
56
+ cpSync(mount.dir, dest, { recursive: true });
57
+ }
58
+ // Co-located images from the source docs directory (the mkdocs shape,
59
+ // where media sits next to the markdown rather than in its own tree).
30
60
  const sourceDir = opts.sourceDir;
31
61
  if (sourceDir) {
32
62
  copyImages(sourceDir, output);
@@ -38,8 +68,8 @@ export const plugin = {
38
68
  else if (pluginMode === "registry") {
39
69
  installRegistryRef(output);
40
70
  }
41
- // Pre-configure Front Matter Title plugin (graph view shows title, not filename)
42
- installFrontMatterTitleConfig(output);
71
+ // Pre-configure graph view: colour groups + forces tuned for a docs vault
72
+ installGraphConfig(output, opts.graphGroups);
43
73
  },
44
74
  };
45
75
  /**
@@ -72,12 +102,17 @@ function installBundledPlugin(vaultDir) {
72
102
  * Write .obsidian/community-plugins.json listing the plugin.
73
103
  * Obsidian will prompt the user to install it from the registry.
74
104
  */
75
- /** Recommended community plugins — listed so Obsidian prompts to install them */
76
- const FRONT_MATTER_TITLE_ID = "obsidian-front-matter-title-plugin";
77
- const RECOMMENDED_PLUGINS = [
78
- PLUGIN_ID,
79
- FRONT_MATTER_TITLE_ID, // Shows frontmatter title in graph view instead of filename
80
- ];
105
+ /**
106
+ * Plugins listed in community-plugins.json.
107
+ *
108
+ * Only plugins the vault actually SHIPS belong here. Listing one we don't
109
+ * bundle made Obsidian report it as enabled while nothing loaded — the vault
110
+ * carried a data.json for Front Matter Title and no plugin, so graph labels
111
+ * silently fell back to filenames. Section notes are named `_<folder>.md` for
112
+ * exactly that reason, and readers who want frontmatter titles everywhere can
113
+ * install Front Matter Title themselves (see docs/obsidian-vaults.md).
114
+ */
115
+ const RECOMMENDED_PLUGINS = [PLUGIN_ID];
81
116
  function installRegistryRef(vaultDir) {
82
117
  const obsidianDir = join(vaultDir, ".obsidian");
83
118
  mkdirSync(obsidianDir, { recursive: true });
@@ -85,29 +120,85 @@ function installRegistryRef(vaultDir) {
85
120
  console.log(`Listed plugins in community-plugins.json: ${RECOMMENDED_PLUGINS.join(", ")}`);
86
121
  }
87
122
  /**
88
- * Pre-configure Front Matter Title plugin so graph view shows titles.
89
- * User still needs to install the plugin from community browser,
90
- * but the settings will be ready when they do.
123
+ * Read `--graph-groups <path>`: `[{ "query": "path:guides", "color": "#F2B36F" }]`.
124
+ *
125
+ * Colours are accepted as `#RRGGBB`, `RRGGBB`, or a raw number, and converted
126
+ * to the decimal int Obsidian stores — asking an author to write `15905647`
127
+ * would be a needless papercut.
128
+ *
129
+ * Throws on a malformed file rather than silently shipping the default: a
130
+ * caller who passed the flag wants THEIR groups, and a quiet fallback would
131
+ * look like the flag being ignored.
91
132
  */
92
- function installFrontMatterTitleConfig(vaultDir) {
93
- const configDir = join(vaultDir, ".obsidian", "plugins", FRONT_MATTER_TITLE_ID);
94
- mkdirSync(configDir, { recursive: true });
95
- const settings = {
96
- templates: {
97
- common: { main: "title", fallback: "fallback_title" },
98
- },
99
- features: {
100
- graph: { enabled: true, templates: { main: "", fallback: "" } },
101
- explorer: { enabled: true, templates: { main: "", fallback: "" } },
102
- tab: { enabled: true, templates: { main: "", fallback: "" } },
103
- },
104
- };
105
- writeFileSync(join(configDir, "data.json"), JSON.stringify(settings, null, 2) + "\n");
133
+ function readGraphGroups(path) {
134
+ let raw;
135
+ try {
136
+ raw = JSON.parse(readFileSync(path, "utf8"));
137
+ }
138
+ catch (err) {
139
+ throw new Error(`--graph-groups: could not parse ${path} as JSON: ${err.message}`);
140
+ }
141
+ if (!Array.isArray(raw)) {
142
+ throw new Error(`--graph-groups: ${path} must contain a JSON array of { query, color }`);
143
+ }
144
+ return raw.map((entry, i) => {
145
+ const g = entry;
146
+ if (!g || typeof g.query !== "string" || !g.query) {
147
+ throw new Error(`--graph-groups: entry ${i} needs a non-empty "query" string`);
148
+ }
149
+ let rgb;
150
+ if (typeof g.color === "number") {
151
+ rgb = g.color;
152
+ }
153
+ else if (typeof g.color === "string") {
154
+ const hex = g.color.replace(/^#/, "");
155
+ if (!/^[0-9a-fA-F]{6}$/.test(hex)) {
156
+ throw new Error(`--graph-groups: entry ${i} colour "${g.color}" is not #RRGGBB`);
157
+ }
158
+ rgb = parseInt(hex, 16);
159
+ }
160
+ else {
161
+ throw new Error(`--graph-groups: entry ${i} needs a "color" (#RRGGBB or a number)`);
162
+ }
163
+ return { query: g.query, color: { a: 1, rgb } };
164
+ });
165
+ }
166
+ function installGraphConfig(vaultDir, groupsPath) {
167
+ const obsidianDir = join(vaultDir, ".obsidian");
168
+ const graphPath = join(obsidianDir, "graph.json");
169
+ if (existsSync(graphPath))
170
+ return; // the reader's own view state — leave it
171
+ const colorGroups = groupsPath
172
+ ? readGraphGroups(groupsPath)
173
+ : // Default: only the group that is true of EVERY Dogsbay vault. Anything
174
+ // corpus-specific (a genre axis, a per-book split) belongs to whoever
175
+ // knows the corpus — pass --graph-groups for those.
176
+ [{ query: "file:/^_/", color: { a: 1, rgb: 0xf2b3_6f } }];
177
+ mkdirSync(obsidianDir, { recursive: true });
178
+ writeFileSync(graphPath, JSON.stringify({
179
+ "collapse-filter": false,
180
+ search: "",
181
+ showTags: false,
182
+ showAttachments: false,
183
+ hideUnresolved: true, // unresolved links are noise, not nodes
184
+ showOrphans: true,
185
+ "collapse-color-groups": false,
186
+ colorGroups,
187
+ "collapse-display": true,
188
+ showArrow: true, // direction matters when hubs point outward
189
+ textFadeMultiplier: -1.5, // labels only when zoomed in; a docs vault is dense
190
+ nodeSizeMultiplier: 0.9,
191
+ lineSizeMultiplier: 0.7,
192
+ "collapse-forces": true,
193
+ centerStrength: 0.5,
194
+ repelStrength: 7, // below the default 10 — let clusters tighten
195
+ linkStrength: 1,
196
+ linkDistance: 180,
197
+ scale: 1,
198
+ close: false,
199
+ }, null, 2) + "\n");
200
+ console.log("Pre-configured graph view (.obsidian/graph.json)");
106
201
  }
107
- /**
108
- * Copy images from source docs directory into the vault root.
109
- * Image paths in markdown (e.g. /img/foo.png) become relative vault paths.
110
- */
111
202
  function copyImages(sourceDir, vaultDir) {
112
203
  const imageExts = new Set([".png", ".jpg", ".jpeg", ".gif", ".svg", ".webp"]);
113
204
  let count = 0;
@@ -158,4 +249,29 @@ function resolvePluginDist() {
158
249
  }
159
250
  return null;
160
251
  }
161
- //# sourceMappingURL=cli.js.map
252
+ /** Media extensions carried across for a `mediaOnly` mount — mirrors format-dogsbay-md. */
253
+ const MEDIA_EXTS = new Set([
254
+ ".png", ".jpg", ".jpeg", ".gif", ".webp", ".avif",
255
+ ".svg", ".ico", ".pdf", ".mp4", ".webm",
256
+ ]);
257
+ /**
258
+ * Copy only media files out of a mount, preserving its subtree shape.
259
+ *
260
+ * A `mediaOnly` mount is a whole docs directory (Docusaurus and VitePress both
261
+ * mount theirs that way), so a recursive copy would bury every `.md`/`.mdx` in
262
+ * the source under `_assets/` as duplicate notes.
263
+ */
264
+ function copyMediaTree(srcDir, destDir) {
265
+ for (const entry of readdirSync(srcDir, { withFileTypes: true })) {
266
+ const full = join(srcDir, entry.name);
267
+ if (entry.isDirectory()) {
268
+ copyMediaTree(full, join(destDir, entry.name));
269
+ continue;
270
+ }
271
+ const dot = entry.name.lastIndexOf(".");
272
+ if (dot === -1 || !MEDIA_EXTS.has(entry.name.slice(dot).toLowerCase()))
273
+ continue;
274
+ mkdirSync(destDir, { recursive: true });
275
+ cpSync(full, join(destDir, entry.name));
276
+ }
277
+ }
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export { treeToObsidian } from "./serialize.js";
2
2
  export { exportObsidianVault } from "./project.js";
3
+ export { OBSIDIAN_CAPABILITIES } from "./capabilities.js";
3
4
  export type { ObsidianVaultOptions, ObsidianVaultExport } from "./project.js";
4
5
  export type { TreeNode, InlineNode, ExportPage, NavItem } from "@dogsbay/types";
5
- //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
1
  // Obsidian format plugin — serialize (tree → Obsidian markdown) + vault export
2
2
  export { treeToObsidian } from "./serialize.js";
3
3
  export { exportObsidianVault } from "./project.js";
4
- //# sourceMappingURL=index.js.map
4
+ export { OBSIDIAN_CAPABILITIES } from "./capabilities.js";
package/dist/project.d.ts CHANGED
@@ -2,21 +2,67 @@
2
2
  * Obsidian vault exporter.
3
3
  *
4
4
  * Takes ExportPage[] + NavItem[] and generates a complete Obsidian vault
5
- * with folder structure from nav, sidebar_position frontmatter, _index.md
5
+ * with folder structure from nav, sidebar_position frontmatter, folder notes
6
6
  * per section, and _nav.yml for lossless round-trip.
7
7
  */
8
8
  import type { ExportPage, NavItem } from "@dogsbay/types";
9
9
  export interface ObsidianVaultOptions {
10
10
  vaultName?: string;
11
- assetsDir?: string;
12
11
  /** Section name — pages go under this subfolder, nav gets a top-level entry */
13
12
  section?: string;
13
+ /**
14
+ * Emit a `_<folder>.md` folder note per nav section (default `true`).
15
+ *
16
+ * These are **invented by this exporter** — no source format supplies them,
17
+ * and they do not appear in `_nav.yml` (a section is a label-only nav entry
18
+ * with no path), so the folder note is the only thing that makes a section
19
+ * addressable at all.
20
+ *
21
+ * Measured on the OpenShift 4.22 vault (2,105 notes), they are load-bearing
22
+ * once populated — they carry the nav as real links, and are the sole
23
+ * inbound link for ~470 content pages that no prose links to:
24
+ *
25
+ * | | notes | edges | no inbound | isolated |
26
+ * |---|---|---|---|---|
27
+ * | with, populated | 2,105 | 6,954 | 44 | 0 |
28
+ * | without | 1,796 | 4,893 | 523 | 278 |
29
+ *
30
+ * Set `false` for a vault that mirrors the source exactly — at the cost of
31
+ * the folder-click affordance and the nav-as-links layer.
32
+ */
33
+ sectionIndexes?: boolean;
34
+ /**
35
+ * Give every folder note a body listing its children (default `true`).
36
+ * Ignored when `sectionIndexes` is `false`.
37
+ *
38
+ * Without it a section index is frontmatter and nothing else: clicking a
39
+ * folder in Obsidian yields a title and white space, and the note has no
40
+ * outbound links, so it sits outside the graph. On the OpenShift 4.22 export
41
+ * that was 309 of 309 section indexes with zero links — the entire outer
42
+ * ring of orphans in the graph view.
43
+ *
44
+ * The listing is ordinary markdown links, so it also feeds backlinks, the
45
+ * local graph, and any consumer that reads links rather than a nav file.
46
+ *
47
+ * Set `false` to keep the frontmatter-only stubs (previous behaviour).
48
+ */
49
+ sectionIndexLinks?: boolean;
14
50
  }
15
51
  export interface ObsidianVaultExport {
16
52
  files: Map<string, string>;
17
53
  }
54
+ /**
55
+ * Path of a section's folder note.
56
+ *
57
+ * Named `_<folder>.md`, not `_index.md`: Obsidian labels graph nodes and tabs
58
+ * by FILENAME, so a vault of 309 sections showed 309 identical `_index` nodes.
59
+ * The leading underscore keeps the note sorted to the top of its folder and
60
+ * distinguishes it from a content page that shares the folder's name — 20
61
+ * sections in the OpenShift corpus contain `<folder>/<folder>.md`, so the bare
62
+ * folder name would collide. No page in that corpus starts with `_`.
63
+ */
64
+ export declare function sectionNotePath(folderPath: string): string;
18
65
  /**
19
66
  * Export pages + nav to an Obsidian vault file map.
20
67
  */
21
68
  export declare function exportObsidianVault(pages: ExportPage[], nav: NavItem[], options?: ObsidianVaultOptions): ObsidianVaultExport;
22
- //# sourceMappingURL=project.d.ts.map