@dogsbay/format-obsidian 0.2.0-beta.15 → 0.2.0-beta.150
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/dist/capabilities.d.ts +25 -0
- package/dist/capabilities.js +171 -0
- package/dist/cli.d.ts +0 -1
- package/dist/cli.js +148 -32
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/project.d.ts +49 -3
- package/dist/project.js +525 -38
- package/dist/serialize.d.ts +19 -1
- package/dist/serialize.js +664 -130
- package/package.json +8 -5
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/project.d.ts.map +0 -1
- package/dist/project.js.map +0 -1
- package/dist/serialize.d.ts.map +0 -1
- package/dist/serialize.js.map +0 -1
|
@@ -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: "`` (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
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
|
-
//
|
|
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
|
|
42
|
-
|
|
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
|
-
/**
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
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
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|