@gmickel/gno 2.7.1 → 2.8.1
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 +3 -2
- package/assets/skill/SKILL.md +11 -1
- package/assets/skill/cli-reference.md +8 -1
- package/assets/skill/examples.md +2 -1
- package/assets/skill/mcp-reference.md +3 -1
- package/assets/skill/recipes/memory-scoped-recall.md +10 -5
- package/assets/spa-production.json.gz +0 -0
- package/browser-extension/artifacts/{gno-browser-clipper-v2.7.1.zip → gno-browser-clipper-v2.8.1.zip} +0 -0
- package/browser-extension/artifacts/gno-browser-clipper-v2.8.1.zip.sha256 +1 -0
- package/browser-extension/dist/manifest.json +1 -1
- package/package.json +1 -1
- package/spec/cli.md +86 -9
- package/spec/db/schema.sql +0 -1
- package/spec/mcp.md +26 -7
- package/spec/output-schemas/audit-report.schema.json +18 -4
- package/spec/output-schemas/backlinks.schema.json +4 -0
- package/spec/output-schemas/collection-list.schema.json +13 -0
- package/spec/output-schemas/graph.schema.json +2 -0
- package/spec/output-schemas/links-list.schema.json +4 -0
- package/spec/output-schemas/memory-recall.schema.json +1 -1
- package/spec/output-schemas/status.schema.json +18 -3
- package/src/cli/commands/audit.ts +23 -4
- package/src/cli/commands/collection/list.ts +39 -5
- package/src/cli/commands/embed.ts +3 -3
- package/src/cli/commands/graph.ts +3 -1
- package/src/cli/commands/links.ts +61 -180
- package/src/cli/commands/shared.ts +7 -0
- package/src/cli/commands/status.ts +6 -0
- package/src/cli/program.ts +12 -2
- package/src/config/loader.ts +43 -0
- package/src/config/types.ts +8 -0
- package/src/core/audit-contract.ts +16 -4
- package/src/core/audit-freshness.ts +11 -1
- package/src/core/audit-links.ts +197 -25
- package/src/core/audit-outside-index.ts +215 -0
- package/src/core/audit-provenance.ts +11 -4
- package/src/core/audit-workspace.ts +30 -9
- package/src/core/audit.ts +76 -16
- package/src/core/context-compiler.ts +3 -0
- package/src/core/context-evidence.ts +11 -0
- package/src/core/graph-edge-confidence.ts +23 -1
- package/src/core/host-paths.ts +1 -0
- package/src/core/knowledge-impact.ts +28 -0
- package/src/core/link-inventory-markdown.ts +2 -3
- package/src/core/link-workspace.ts +324 -0
- package/src/core/links.ts +40 -17
- package/src/core/memory-recall.ts +254 -15
- package/src/core/memory-types.ts +12 -0
- package/src/core/memory.ts +2 -0
- package/src/core/retrieval-replay-candidate.ts +6 -0
- package/src/core/retrieval-trace-request.ts +3 -0
- package/src/index.ts +14 -1
- package/src/ingestion/graph-reconciliation.ts +77 -15
- package/src/ingestion/source-availability/darwin-path.ts +9 -3
- package/src/ingestion/sync.ts +27 -4
- package/src/ingestion/types.ts +14 -0
- package/src/llm/inference-scope.ts +4 -3
- package/src/mcp/http-egress.ts +42 -3
- package/src/mcp/tools/audit.ts +11 -2
- package/src/mcp/tools/changes.ts +1 -0
- package/src/mcp/tools/links.ts +74 -93
- package/src/mcp/tools/sessions.ts +33 -4
- package/src/mcp/tools/status.ts +4 -0
- package/src/pipeline/expansion.ts +19 -31
- package/src/pipeline/graph-retrieval.ts +22 -2
- package/src/pipeline/hybrid.ts +1 -1
- package/src/pipeline/search.ts +2 -0
- package/src/pipeline/types.ts +10 -3
- package/src/sdk/client.ts +1 -0
- package/src/serve/findings-pass.ts +1 -1
- package/src/serve/public/components/editor/MarkdownPreview.tsx +5 -3
- package/src/serve/public/pages/GraphView.tsx +2 -0
- package/src/serve/routes/changes.ts +6 -1
- package/src/serve/routes/graph.ts +3 -1
- package/src/serve/routes/links.ts +45 -50
- package/src/serve/routes/sessions.ts +41 -53
- package/src/serve/server.ts +2 -1
- package/src/serve/status.ts +1 -0
- package/src/sessions/config-refresh.ts +111 -0
- package/src/store/migrations/033-drop-documents-active-index.ts +30 -0
- package/src/store/migrations/034-collection-link-workspace.ts +47 -0
- package/src/store/migrations/index.ts +4 -0
- package/src/store/sqlite/adapter.ts +477 -337
- package/src/store/sqlite/eligibility.ts +8 -2
- package/src/store/sqlite/graph-link-resolver.ts +259 -5
- package/src/store/sqlite/graph-neighbors.ts +147 -40
- package/src/store/sqlite/graph-reference-state.ts +13 -2
- package/src/store/sqlite/graph-similarity.ts +96 -0
- package/src/store/sqlite/workspace-link-resolver.ts +742 -0
- package/src/store/types.ts +64 -5
- package/src/store/vector/stats.ts +1 -1
- package/src/store/vector/status.ts +27 -0
- package/src/store/vector/stored-vectors.ts +158 -0
- package/src/store/vector/types.ts +6 -0
- package/src/store/vector/variant-search.ts +30 -14
- package/browser-extension/artifacts/gno-browser-clipper-v2.7.1.zip.sha256 +0 -1
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Link workspaces: collections whose roots share one workspace root resolve
|
|
3
|
+
* plain wiki links across each other (Obsidian vault semantics).
|
|
4
|
+
*
|
|
5
|
+
* Membership is derived from the filesystem, never stored in content: the
|
|
6
|
+
* nearest ancestor-or-self directory containing `.obsidian/`, or an explicit
|
|
7
|
+
* per-collection `workspaceRoot` (absolute path, or `false` to opt out).
|
|
8
|
+
* Ownership is finally decided per document: a nested vault (`.obsidian/`
|
|
9
|
+
* below the collection root) forms its own workspace.
|
|
10
|
+
*
|
|
11
|
+
* @module src/core/link-workspace
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
// node:fs has no Bun equivalent for synchronous realpath/stat of directories.
|
|
15
|
+
import { realpathSync, statSync } from "node:fs";
|
|
16
|
+
// node:path provides platform-correct path algebra; Bun has no path utilities.
|
|
17
|
+
import { dirname, isAbsolute, relative, sep } from "node:path";
|
|
18
|
+
|
|
19
|
+
/** Bump when resolution semantics change; enters projection fingerprints. */
|
|
20
|
+
export const LINK_RESOLVER_VERSION = 2;
|
|
21
|
+
|
|
22
|
+
export const WORKSPACE_MARKER_DIR = ".obsidian";
|
|
23
|
+
|
|
24
|
+
export type LinkWorkspaceSource =
|
|
25
|
+
/** No workspace root: collection-scoped resolution (today's behaviour). */
|
|
26
|
+
| "none"
|
|
27
|
+
/** Nearest `.obsidian/` ancestor-or-self of the collection root. */
|
|
28
|
+
| "detected"
|
|
29
|
+
/** Explicit absolute `workspaceRoot`. */
|
|
30
|
+
| "configured"
|
|
31
|
+
/** Explicit `workspaceRoot: false` opt-out. */
|
|
32
|
+
| "disabled"
|
|
33
|
+
/** Root or ancestors could not be inspected; fails closed to collection scope. */
|
|
34
|
+
| "unavailable";
|
|
35
|
+
|
|
36
|
+
export interface CollectionWorkspaceInfo {
|
|
37
|
+
/** Canonical (real) collection root, or null when it cannot be resolved. */
|
|
38
|
+
realPath: string | null;
|
|
39
|
+
/** Canonical workspace root, or null for collection-scoped resolution. */
|
|
40
|
+
root: string | null;
|
|
41
|
+
source: LinkWorkspaceSource;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Stored membership of one collection, as read back from the index. */
|
|
45
|
+
export interface CollectionWorkspaceMembership extends CollectionWorkspaceInfo {
|
|
46
|
+
collection: string;
|
|
47
|
+
/** Collection-relative POSIX prefixes that hold their own `.obsidian/`. */
|
|
48
|
+
nested: string[];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
type DirProbe = "present" | "absent" | "error";
|
|
52
|
+
|
|
53
|
+
const probeMarker = (directory: string): DirProbe => {
|
|
54
|
+
try {
|
|
55
|
+
return statSync(`${directory}${sep}${WORKSPACE_MARKER_DIR}`).isDirectory()
|
|
56
|
+
? "present"
|
|
57
|
+
: "absent";
|
|
58
|
+
} catch (cause) {
|
|
59
|
+
const code = (cause as NodeJS.ErrnoException | undefined)?.code;
|
|
60
|
+
return code === "ENOENT" || code === "ENOTDIR" ? "absent" : "error";
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const canonical = (path: string): string | null => {
|
|
65
|
+
try {
|
|
66
|
+
return realpathSync(path).normalize("NFC");
|
|
67
|
+
} catch {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
/** Join a POSIX relative path onto a native absolute root. */
|
|
73
|
+
const nativeJoin = (root: string, posixPath: string): string =>
|
|
74
|
+
`${root}${sep}${posixPath.split("/").join(sep)}`;
|
|
75
|
+
|
|
76
|
+
/** True when `child` equals `parent` or sits inside it (component-wise). */
|
|
77
|
+
export const pathContains = (parent: string, child: string): boolean => {
|
|
78
|
+
const rel = relative(parent, child);
|
|
79
|
+
return rel === "" || (!rel.startsWith("..") && !isAbsolute(rel));
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
/** POSIX path of `child` relative to `parent` ('' when equal). */
|
|
83
|
+
export const posixRelative = (parent: string, child: string): string =>
|
|
84
|
+
relative(parent, child).split(sep).join("/").normalize("NFC");
|
|
85
|
+
|
|
86
|
+
export type WorkspaceRootSettingError =
|
|
87
|
+
| "not_absolute"
|
|
88
|
+
| "not_found"
|
|
89
|
+
| "not_containing";
|
|
90
|
+
|
|
91
|
+
/** Validate an explicit `workspaceRoot` against the collection root. */
|
|
92
|
+
export const validateWorkspaceRootSetting = (
|
|
93
|
+
collectionPath: string,
|
|
94
|
+
workspaceRoot: string
|
|
95
|
+
): WorkspaceRootSettingError | null => {
|
|
96
|
+
if (!isAbsolute(workspaceRoot)) return "not_absolute";
|
|
97
|
+
const root = canonical(workspaceRoot);
|
|
98
|
+
if (root === null) return "not_found";
|
|
99
|
+
try {
|
|
100
|
+
if (!statSync(root).isDirectory()) return "not_found";
|
|
101
|
+
} catch {
|
|
102
|
+
return "not_found";
|
|
103
|
+
}
|
|
104
|
+
const collectionRoot = canonical(collectionPath) ?? collectionPath;
|
|
105
|
+
return pathContains(root, collectionRoot) ? null : "not_containing";
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
export const workspaceRootSettingMessage = (
|
|
109
|
+
collection: string,
|
|
110
|
+
error: WorkspaceRootSettingError
|
|
111
|
+
): string => {
|
|
112
|
+
switch (error) {
|
|
113
|
+
case "not_absolute":
|
|
114
|
+
return `Collection "${collection}": workspaceRoot must be an absolute path`;
|
|
115
|
+
case "not_found":
|
|
116
|
+
return `Collection "${collection}": workspaceRoot does not exist or is not a directory`;
|
|
117
|
+
case "not_containing":
|
|
118
|
+
return `Collection "${collection}": workspaceRoot must contain the collection root`;
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Resolve one collection's workspace from its root. Real paths are used
|
|
124
|
+
* throughout; an unreadable root or ancestor never joins a broader workspace.
|
|
125
|
+
*/
|
|
126
|
+
export const detectCollectionWorkspace = (collection: {
|
|
127
|
+
path: string;
|
|
128
|
+
workspaceRoot?: string | false;
|
|
129
|
+
}): CollectionWorkspaceInfo => {
|
|
130
|
+
const realPath = canonical(collection.path);
|
|
131
|
+
if (collection.workspaceRoot === false) {
|
|
132
|
+
return { realPath, root: null, source: "disabled" };
|
|
133
|
+
}
|
|
134
|
+
if (realPath === null) {
|
|
135
|
+
return { realPath: null, root: null, source: "unavailable" };
|
|
136
|
+
}
|
|
137
|
+
if (typeof collection.workspaceRoot === "string") {
|
|
138
|
+
const invalid = validateWorkspaceRootSetting(
|
|
139
|
+
collection.path,
|
|
140
|
+
collection.workspaceRoot
|
|
141
|
+
);
|
|
142
|
+
const root = invalid ? null : canonical(collection.workspaceRoot);
|
|
143
|
+
return root === null
|
|
144
|
+
? { realPath, root: null, source: "unavailable" }
|
|
145
|
+
: { realPath, root, source: "configured" };
|
|
146
|
+
}
|
|
147
|
+
let current = realPath;
|
|
148
|
+
for (;;) {
|
|
149
|
+
const probe = probeMarker(current);
|
|
150
|
+
if (probe === "present") {
|
|
151
|
+
return { realPath, root: current, source: "detected" };
|
|
152
|
+
}
|
|
153
|
+
if (probe === "error") {
|
|
154
|
+
return { realPath, root: null, source: "unavailable" };
|
|
155
|
+
}
|
|
156
|
+
const parent = dirname(current);
|
|
157
|
+
if (parent === current) {
|
|
158
|
+
return { realPath, root: null, source: "none" };
|
|
159
|
+
}
|
|
160
|
+
current = parent;
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Find nested vaults inside a collection: every ancestor directory of an
|
|
166
|
+
* indexed document (below the collection root) that holds `.obsidian/`.
|
|
167
|
+
* Bounded by distinct directories, never by link count.
|
|
168
|
+
*/
|
|
169
|
+
export const detectNestedWorkspacePrefixes = (
|
|
170
|
+
realRoot: string,
|
|
171
|
+
relPaths: Iterable<string>
|
|
172
|
+
): string[] => {
|
|
173
|
+
const directories = new Set<string>();
|
|
174
|
+
for (const relPath of relPaths) {
|
|
175
|
+
let directory = relPath.includes("/")
|
|
176
|
+
? relPath.slice(0, relPath.lastIndexOf("/"))
|
|
177
|
+
: "";
|
|
178
|
+
while (directory && !directories.has(directory)) {
|
|
179
|
+
directories.add(directory);
|
|
180
|
+
directory = directory.includes("/")
|
|
181
|
+
? directory.slice(0, directory.lastIndexOf("/"))
|
|
182
|
+
: "";
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
const nested: string[] = [];
|
|
186
|
+
for (const directory of directories) {
|
|
187
|
+
if (probeMarker(nativeJoin(realRoot, directory)) === "present") {
|
|
188
|
+
nested.push(directory.normalize("NFC"));
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return nested.sort();
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Fingerprint input for effective link resolution: resolver version plus the
|
|
196
|
+
* membership of every collection that belongs to (or contains) a workspace.
|
|
197
|
+
* Undefined when no collection is in a workspace, so collection-scoped
|
|
198
|
+
* indexes keep their existing fingerprints.
|
|
199
|
+
*/
|
|
200
|
+
export const linkResolutionFingerprintInput = (
|
|
201
|
+
rows: ReadonlyArray<{
|
|
202
|
+
name: string;
|
|
203
|
+
realPath?: string | null;
|
|
204
|
+
workspaceRoot?: string | null;
|
|
205
|
+
workspaceSource?: LinkWorkspaceSource;
|
|
206
|
+
workspaceNested?: string[];
|
|
207
|
+
}>
|
|
208
|
+
):
|
|
209
|
+
| {
|
|
210
|
+
version: number;
|
|
211
|
+
workspaces: Array<{
|
|
212
|
+
collection: string;
|
|
213
|
+
realPath: string | null;
|
|
214
|
+
root: string | null;
|
|
215
|
+
source: LinkWorkspaceSource;
|
|
216
|
+
nested: string[];
|
|
217
|
+
}>;
|
|
218
|
+
}
|
|
219
|
+
| undefined => {
|
|
220
|
+
const workspaces = rows
|
|
221
|
+
.filter(
|
|
222
|
+
(row) =>
|
|
223
|
+
(row.workspaceRoot ?? null) !== null ||
|
|
224
|
+
(row.workspaceNested?.length ?? 0) > 0
|
|
225
|
+
)
|
|
226
|
+
.map((row) => ({
|
|
227
|
+
collection: row.name,
|
|
228
|
+
realPath: row.realPath ?? null,
|
|
229
|
+
root: row.workspaceRoot ?? null,
|
|
230
|
+
source: row.workspaceSource ?? "none",
|
|
231
|
+
nested: [...(row.workspaceNested ?? [])].sort(),
|
|
232
|
+
}))
|
|
233
|
+
.sort((left, right) =>
|
|
234
|
+
left.collection < right.collection
|
|
235
|
+
? -1
|
|
236
|
+
: left.collection > right.collection
|
|
237
|
+
? 1
|
|
238
|
+
: 0
|
|
239
|
+
);
|
|
240
|
+
return workspaces.length > 0
|
|
241
|
+
? { version: LINK_RESOLVER_VERSION, workspaces }
|
|
242
|
+
: undefined;
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* One-line link workspace description for status output, or null for a
|
|
247
|
+
* collection that is not in a workspace. A redacted root prints no path.
|
|
248
|
+
*/
|
|
249
|
+
export const formatLinkWorkspace = (collection: {
|
|
250
|
+
workspaceRoot?: string | null;
|
|
251
|
+
workspaceSource?: string;
|
|
252
|
+
}): string | null => {
|
|
253
|
+
switch (collection.workspaceSource) {
|
|
254
|
+
case "detected":
|
|
255
|
+
case "configured":
|
|
256
|
+
return collection.workspaceRoot
|
|
257
|
+
? `${collection.workspaceRoot} (${collection.workspaceSource})`
|
|
258
|
+
: collection.workspaceSource;
|
|
259
|
+
case "disabled":
|
|
260
|
+
return "off (links stay inside this collection)";
|
|
261
|
+
case "unavailable":
|
|
262
|
+
return "unavailable (links stay inside this collection)";
|
|
263
|
+
default:
|
|
264
|
+
return null;
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
/** Workspace identity and workspace-relative path of one document. */
|
|
269
|
+
export interface DocumentWorkspacePlacement {
|
|
270
|
+
/** Canonical workspace root (the workspace identity), or null. */
|
|
271
|
+
key: string | null;
|
|
272
|
+
/** Document path relative to the workspace root (POSIX, NFC). */
|
|
273
|
+
path: string;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/** Collection root relative to its workspace root, per membership object. */
|
|
277
|
+
const workspacePrefixes = new WeakMap<CollectionWorkspaceMembership, string>();
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Place a document into its workspace: the deepest nested vault containing
|
|
281
|
+
* it, else its collection's workspace. Collection-scoped collections (none,
|
|
282
|
+
* disabled, unavailable) never place documents into a workspace, except that
|
|
283
|
+
* nested vaults still own their documents when the collection root is known
|
|
284
|
+
* and not opted out.
|
|
285
|
+
*/
|
|
286
|
+
export const placeDocument = (
|
|
287
|
+
membership: CollectionWorkspaceMembership | undefined,
|
|
288
|
+
relPath: string
|
|
289
|
+
): DocumentWorkspacePlacement => {
|
|
290
|
+
const normalized = relPath.normalize("NFC");
|
|
291
|
+
if (
|
|
292
|
+
!membership ||
|
|
293
|
+
membership.realPath === null ||
|
|
294
|
+
membership.source === "disabled" ||
|
|
295
|
+
membership.source === "unavailable"
|
|
296
|
+
) {
|
|
297
|
+
return { key: null, path: normalized };
|
|
298
|
+
}
|
|
299
|
+
let nestedPrefix: string | null = null;
|
|
300
|
+
for (const prefix of membership.nested) {
|
|
301
|
+
if (
|
|
302
|
+
normalized.startsWith(`${prefix}/`) &&
|
|
303
|
+
(nestedPrefix === null || prefix.length > nestedPrefix.length)
|
|
304
|
+
) {
|
|
305
|
+
nestedPrefix = prefix;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
if (nestedPrefix !== null) {
|
|
309
|
+
return {
|
|
310
|
+
key: nativeJoin(membership.realPath, nestedPrefix),
|
|
311
|
+
path: normalized.slice(nestedPrefix.length + 1),
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
if (membership.root === null) return { key: null, path: normalized };
|
|
315
|
+
let prefix = workspacePrefixes.get(membership);
|
|
316
|
+
if (prefix === undefined) {
|
|
317
|
+
prefix = posixRelative(membership.root, membership.realPath);
|
|
318
|
+
workspacePrefixes.set(membership, prefix);
|
|
319
|
+
}
|
|
320
|
+
return {
|
|
321
|
+
key: membership.root,
|
|
322
|
+
path: prefix ? `${prefix}/${normalized}` : normalized,
|
|
323
|
+
};
|
|
324
|
+
};
|
package/src/core/links.ts
CHANGED
|
@@ -88,6 +88,8 @@ const LOGSEQ_EMBED_REGEX =
|
|
|
88
88
|
* Markdown inline link: [text](url)
|
|
89
89
|
* Captures: 1=text, 2=url (path and optional anchor)
|
|
90
90
|
* Negative lookbehind to avoid image links ![]()
|
|
91
|
+
* Link text may contain balanced square brackets one level deep
|
|
92
|
+
* (`[see [1]](note.md)`), as CommonMark allows.
|
|
91
93
|
*
|
|
92
94
|
* SCOPE LIMITATIONS:
|
|
93
95
|
* - Only matches simple inline links [text](url)
|
|
@@ -95,7 +97,10 @@ const LOGSEQ_EMBED_REGEX =
|
|
|
95
97
|
* - Does NOT match autolinks <url> or bare URLs
|
|
96
98
|
* - Parens in URLs not supported (use %28 %29 encoding)
|
|
97
99
|
*/
|
|
98
|
-
const MARKDOWN_LINK_REGEX = /(?<!!)\[([
|
|
100
|
+
const MARKDOWN_LINK_REGEX = /(?<!!)\[((?:[^[\]]|\[[^[\]]*\])*)\]\(([^)]+)\)/g;
|
|
101
|
+
|
|
102
|
+
/** Square brackets in a destination mean the text was split, not a path. */
|
|
103
|
+
const BRACKET_IN_DESTINATION_REGEX = /[[\]]/;
|
|
99
104
|
|
|
100
105
|
/** External URL pattern (http:// https:// mailto: etc.) */
|
|
101
106
|
const EXTERNAL_URL_REGEX = /^[a-z][a-z0-9+.-]*:/i;
|
|
@@ -144,6 +149,26 @@ export function extractWikiBasename(ref: string): string {
|
|
|
144
149
|
return stripWikiMdExt(base);
|
|
145
150
|
}
|
|
146
151
|
|
|
152
|
+
/**
|
|
153
|
+
* Split the content of a wiki link into target and alias. Obsidian also reads
|
|
154
|
+
* `\|` (the pipe escaped inside a Markdown table) as the alias separator,
|
|
155
|
+
* so `[[Note\|Alias]]` targets `Note`, not `Note\`.
|
|
156
|
+
*/
|
|
157
|
+
export function splitWikiLinkContent(content: string): {
|
|
158
|
+
target: string;
|
|
159
|
+
alias?: string;
|
|
160
|
+
} {
|
|
161
|
+
const pipeIndex = content.indexOf("|");
|
|
162
|
+
if (pipeIndex < 0) {
|
|
163
|
+
return { target: content };
|
|
164
|
+
}
|
|
165
|
+
const escaped = pipeIndex > 0 && content[pipeIndex - 1] === "\\";
|
|
166
|
+
return {
|
|
167
|
+
target: content.slice(0, escaped ? pipeIndex - 1 : pipeIndex),
|
|
168
|
+
alias: content.slice(pipeIndex + 1),
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
147
172
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
148
173
|
// Path Normalization
|
|
149
174
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -338,22 +363,14 @@ export function parseLinks(
|
|
|
338
363
|
const content = match[1];
|
|
339
364
|
if (!content) continue;
|
|
340
365
|
|
|
341
|
-
// Parse [[target|alias]] format
|
|
342
|
-
const
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
// Only set displayText if different from target
|
|
350
|
-
displayText =
|
|
351
|
-
aliasText !== targetPart
|
|
352
|
-
? truncateText(aliasText, MAX_DISPLAY_TEXT_GRAPHEMES)
|
|
353
|
-
: undefined;
|
|
354
|
-
} else {
|
|
355
|
-
targetPart = content;
|
|
356
|
-
}
|
|
366
|
+
// Parse [[target|alias]] (and table-escaped [[target\|alias]]) format
|
|
367
|
+
const { target: targetPart, alias: aliasText } =
|
|
368
|
+
splitWikiLinkContent(content);
|
|
369
|
+
// Only set displayText if different from target
|
|
370
|
+
const displayText =
|
|
371
|
+
aliasText !== undefined && aliasText !== targetPart
|
|
372
|
+
? truncateText(aliasText, MAX_DISPLAY_TEXT_GRAPHEMES)
|
|
373
|
+
: undefined;
|
|
357
374
|
|
|
358
375
|
const trimmedTarget = targetPart.trim();
|
|
359
376
|
if (!trimmedTarget) {
|
|
@@ -427,6 +444,12 @@ export function parseLinks(
|
|
|
427
444
|
continue;
|
|
428
445
|
}
|
|
429
446
|
|
|
447
|
+
// Brackets in the destination come from link text split at an
|
|
448
|
+
// unbalanced bracket (`[a [b](c](d)`): unparseable, not a missing target.
|
|
449
|
+
if (BRACKET_IN_DESTINATION_REGEX.test(url)) {
|
|
450
|
+
continue;
|
|
451
|
+
}
|
|
452
|
+
|
|
430
453
|
// Skip URLs that look like protocol-relative (//example.com)
|
|
431
454
|
if (url.startsWith("//")) {
|
|
432
455
|
continue;
|