Skip to content

Commit a55a8f3

Browse files
Merge branch '1.12-compat' of https://github.com/michaelpporter/breadcrumbs into 1.12-compat
2 parents 62d1001 + 9c2da8b commit a55a8f3

6 files changed

Lines changed: 121 additions & 12 deletions

File tree

CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,18 @@ All notable changes to this project will be documented in this file. See [standa
44

55
## 4.X
66

7+
### [4.14.3-beta.2](https://github.com/michaelpporter/breadcrumbs/compare/4.14.3-beta.1...4.14.3-beta.2) (2026-07-06)
8+
9+
### Bug Fixes
10+
11+
* **typed_link (1.12-compat)** — recognize inline Dataview fields inside blockquotes (`> [up:: [[Note]]]`) and wrapped fields appearing mid-sentence rather than only at the start of a line (`... is a child of (up:: [[Note]])`). Continues the 4.14.3-beta.1 backport (#724).
12+
13+
### [4.14.3-beta.1](https://github.com/michaelpporter/breadcrumbs/compare/4.14.2...4.14.3-beta.1) (2026-07-05)
14+
15+
### Bug Fixes
16+
17+
* **typed_link (1.12-compat)** — inline Dataview properties wrapped in parentheses or brackets (e.g. `(down:: [[Note]])`, `[down:: [[Note]]]`) are recognized again. Backport of the 4.15.1 fix for users still on Obsidian < 1.13 (#724).
18+
719
### [4.14.2](https://github.com/michaelpporter/breadcrumbs/compare/4.14.1...4.14.2) (2026-06-05)
820

921
Internal maintenance release — no user-facing behavior changes.

manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"id": "breadcrumbs",
33
"name": "Breadcrumbs",
4-
"version": "4.14.2",
4+
"version": "4.14.3-beta.2",
55
"minAppVersion": "1.12.3",
66
"description": "Add structured hierarchies to your notes.",
77
"author": "MichaelPPorter",

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "breadcrumbs",
3-
"version": "4.14.2",
3+
"version": "4.14.3-beta.2",
44
"description": "Add typed-links to your Obsidian notes",
55
"main": "main.js",
66
"scripts": {

src/graph/builders/explicit/typed_link.ts

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,16 @@ import { resolve_relative_target_path } from "src/utils/obsidian";
66
import { GCEdgeData, GCNodeData } from "wasm/pkg/breadcrumbs_graph_wasm";
77

88
// Matches Dataview-style inline fields at the start of a line:
9-
// [optional list marker] field-name:: rest-of-line
10-
// Supports: "same:: ...", "- same:: ...", "up :: ..." etc.
11-
const INLINE_FIELD_REGEX = /^(?:\s*[-*+\d.]+\s+)?([\w][\w\s-]*)\s*::\s*/;
9+
// [optional blockquote marker] [optional list marker] [optional ( or [ wrapper] field-name:: rest-of-line
10+
// Supports: "same:: ...", "- same:: ...", "up :: ...", "> same:: ...",
11+
// and Dataview's bracket/paren wrappers "(down:: ...)" / "[down:: ...]".
12+
const LINE_START_FIELD_REGEX =
13+
/^(?:\s*>+\s*)?(?:\s*[-*+\d.]+\s+)?[([]?\s*([\w][\w\s-]*)\s*::\s*/;
14+
15+
// Matches Dataview's bracket/paren-wrapped inline fields anywhere within a
16+
// line of prose, e.g. "This note is a child of (up:: [[Note]])." — Dataview
17+
// only recognizes these mid-line when wrapped, unlike the line-start form.
18+
const WRAPPED_FIELD_REGEX = /[([]\s*([\w][\w\s-]*)\s*::\s*/g;
1219

1320
/**
1421
* **typed_link** — the primary edge builder.
@@ -91,10 +98,29 @@ export const _add_explicit_edges_typed_link: ExplicitEdgeBuilder = async (
9198
for (const link_cache of cache.links) {
9299
const line_num = link_cache.position.start.line;
93100
const line_text = lines[line_num] ?? "";
94-
const match = INLINE_FIELD_REGEX.exec(line_text);
95-
if (!match) continue;
96101

97-
const field = match[1].trim();
102+
const start_match = LINE_START_FIELD_REGEX.exec(line_text);
103+
let field: string | undefined = start_match?.[1]?.trim();
104+
105+
if (!field) {
106+
// Fall back to the nearest wrapped field preceding the
107+
// link on this line (mid-sentence inline annotations).
108+
const before_link = line_text.slice(
109+
0,
110+
link_cache.position.start.col,
111+
);
112+
WRAPPED_FIELD_REGEX.lastIndex = 0;
113+
let wrapped_match: RegExpExecArray | null;
114+
let last: RegExpExecArray | undefined;
115+
while (
116+
(wrapped_match = WRAPPED_FIELD_REGEX.exec(before_link))
117+
) {
118+
last = wrapped_match;
119+
}
120+
field = last?.[1]?.trim();
121+
}
122+
123+
if (!field) continue;
98124
if (!field_labels.has(field)) continue;
99125
if (fm_covered.has(`${field}\0${link_cache.link}`)) continue;
100126

tests/graph/builders/helpers.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ export function mock_file(
2323
/** Markdown list items (list_note builder): one per line */
2424
listItems?: { line: number; col: number; parent: number }[];
2525
/** Body wikilinks keyed by line (list_note builder) */
26-
links?: { line: number; link: string }[];
26+
links?: { line: number; col?: number; link: string }[];
2727
} = {},
2828
) {
2929
const slash_path = path.replace(/\\/g, "/");
@@ -66,8 +66,8 @@ export function mock_file(
6666
links: opts.links?.map((l) => ({
6767
link: l.link,
6868
position: {
69-
start: { line: l.line, col: 0, offset: 0 },
70-
end: { line: l.line, col: 0, offset: 0 },
69+
start: { line: l.line, col: l.col ?? 0, offset: 0 },
70+
end: { line: l.line, col: l.col ?? 0, offset: 0 },
7171
},
7272
})),
7373
}

tests/graph/builders/typed_link.test.ts

Lines changed: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { _add_explicit_edges_typed_link } from "src/graph/builders/explicit/typed_link";
22
import { TFile } from "obsidian";
3-
import { describe, test } from "vitest";
3+
import { describe, expect, test } from "vitest";
44
import { make_all_files, make_plugin, mock_file } from "./helpers";
55

66
function mock_tfile(path: string): TFile {
@@ -111,3 +111,74 @@ describe("typed_link builder — Obsidian branch", () => {
111111
});
112112
});
113113

114+
// ---- Body inline-field branch ----
115+
116+
/** Build a plugin whose cachedRead returns `body` for any file. */
117+
function inline_plugin(body: string) {
118+
return make_plugin(
119+
{ edge_fields: ["up", "down", "same"].map((l) => ({ label: l })) },
120+
[],
121+
undefined,
122+
{ cachedRead: async () => body },
123+
);
124+
}
125+
126+
describe("typed_link builder — body inline fields", () => {
127+
test.each([
128+
["plain", "down:: [[Convolutions]]"],
129+
["list marker", "- down:: [[Convolutions]]"],
130+
["space before ::", "down :: [[Convolutions]]"],
131+
["paren wrapper", "- (down:: [[Convolutions]])"],
132+
["paren wrapper, no space", "- (down::[[Convolutions]])"],
133+
["bracket wrapper", "- [down:: [[Convolutions]]]"],
134+
])("detects inline field — %s", async (_label, line) => {
135+
const files = [mock_file("a.md", { links: [{ line: 0, link: "Convolutions" }] })];
136+
const r = await _add_explicit_edges_typed_link(
137+
inline_plugin(line),
138+
make_all_files(files),
139+
);
140+
expect(r.edges, `no edge for: ${line}`).toHaveLength(1);
141+
expect(r.edges[0]!.edge_type, line).toBe("down");
142+
expect(r.edges[0]!.target).toBe("Convolutions.md");
143+
});
144+
145+
test("field not in edge_fields → no edge", async (t) => {
146+
const files = [mock_file("a.md", { links: [{ line: 0, link: "X" }] })];
147+
const r = await _add_explicit_edges_typed_link(
148+
inline_plugin("(unknown:: [[X]])"),
149+
make_all_files(files),
150+
);
151+
t.expect(r.edges).toHaveLength(0);
152+
});
153+
154+
test("blockquote-wrapped field at line start", async (t) => {
155+
const line = "> [up:: [[Note]]]";
156+
const files = [
157+
mock_file("a.md", {
158+
links: [{ line: 0, col: line.indexOf("[[Note]]"), link: "Note" }],
159+
}),
160+
];
161+
const r = await _add_explicit_edges_typed_link(
162+
inline_plugin(line),
163+
make_all_files(files),
164+
);
165+
expect(r.edges).toHaveLength(1);
166+
expect(r.edges[0]!.edge_type).toBe("up");
167+
});
168+
169+
test("wrapped field mid-sentence (not at line start)", async (t) => {
170+
const line = "This other note is a child of (up:: [[Note]]).";
171+
const files = [
172+
mock_file("a.md", {
173+
links: [{ line: 0, col: line.indexOf("[[Note]]"), link: "Note" }],
174+
}),
175+
];
176+
const r = await _add_explicit_edges_typed_link(
177+
inline_plugin(line),
178+
make_all_files(files),
179+
);
180+
expect(r.edges).toHaveLength(1);
181+
expect(r.edges[0]!.edge_type).toBe("up");
182+
});
183+
});
184+

0 commit comments

Comments
 (0)