Fixed recognizing Links with spaces
Some checks failed
Node.js build / build (20.x) (push) Has been cancelled
Node.js build / build (22.x) (push) Has been cancelled

This commit is contained in:
Lars 2026-02-11 11:56:26 +01:00
parent 523a850ebb
commit c40a89096f
7 changed files with 44 additions and 30 deletions

View File

@ -362,6 +362,8 @@ export async function buildCandidateNodes(
if (section.sectionType) effectiveType = section.sectionType;
displayHeading = section.heading; // canonical heading from file (e.g. with ^block-id)
}
// Note: We don't skip candidate nodes if section is not found, as the heading might be valid
// but not yet parsed correctly, or it might be a note-level reference
}
candidateNodes.push({

View File

@ -28,7 +28,7 @@ export function getSectionTypeForHeading(content: string, heading: string): stri
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
if (line === undefined) continue;
const match = line.match(/^(#{1,6})\s+(.+?)(?:\s+\^[\w-]+)?\s*$/);
const match = line.match(/^(#{1,6})\s+(.+?)(?:\s*\^[\w-]+)?\s*$/);
if (match && match[2]) {
const lineHeading = match[2].trim();
if (lineHeading.toLowerCase() === headingNorm) {
@ -115,7 +115,7 @@ export async function getHeadingsWithSectionTypes(
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
if (line === undefined) continue;
const match = line.match(/^(#{1,6})\s+(.+?)(?:\s+\^[\w-]+)?\s*$/);
const match = line.match(/^(#{1,6})\s+(.+?)(?:\s*\^[\w-]+)?\s*$/);
if (match && match[1] && match[2]) {
const level = match[1].length;
const heading = match[2].trim();

View File

@ -17,8 +17,8 @@ export interface NoteSection {
/** Match `> [!section] type` callout (type = word characters). */
const SECTION_CALLOUT_REGEX = /^\s*>\s*\[!section\]\s*(\S+)\s*$/i;
/** Match block ID at end of heading text: `... ^block-id`. */
const BLOCK_ID_IN_HEADING_REGEX = /\s+\^([a-zA-Z0-9_-]+)\s*$/;
/** Match block ID at end of heading text: `... ^block-id` or `...^block-id` (with or without space before ^). */
const BLOCK_ID_IN_HEADING_REGEX = /\s*\^([a-zA-Z0-9_-]+)\s*$/;
/**
* Split markdown content into sections by headings.

View File

@ -136,7 +136,8 @@ type: experience
if (!h) return "";
let s = h.trim();
if (!s) return "";
s = s.replace(/\s+\^[a-zA-Z0-9_-]+\s*$/, "").trim();
// Use \s* instead of \s+ to match both " ^block" and "^block" (with or without space before ^)
s = s.replace(/\s*\^[a-zA-Z0-9_-]+\s*$/, "").trim();
s = s.replace(/\s+[a-zA-Z0-9_-]+\s*$/, "").trim();
return s || "";
};

View File

@ -50,12 +50,10 @@ describe("normalizeHeadingForMatch", () => {
expect(normalizeHeadingForMatch("Kontext ^context-block")).toBe("Kontext");
});
it("strips one trailing word (Obsidian UI link form)", () => {
expect(normalizeHeadingForMatch("Überschrift Block")).toBe("Überschrift");
expect(normalizeHeadingForMatch("Kontext")).toBe("Kontext");
});
it("leaves heading without block suffix unchanged", () => {
it("leaves heading without caret unchanged (idempotency)", () => {
// When no ^ is present, return as-is. Input may be canonical form from "Titel ^Block".
// This ensures "Lars ist gut" (from "Lars ist gut ^PersLars") is not incorrectly shortened.
expect(normalizeHeadingForMatch("Lars ist gut")).toBe("Lars ist gut");
expect(normalizeHeadingForMatch("Überschrift")).toBe("Überschrift");
expect(normalizeHeadingForMatch("Kontext")).toBe("Kontext");
});
@ -70,9 +68,10 @@ describe("headingsMatch", () => {
it("matches heading with block-id variants", () => {
expect(headingsMatch("Überschrift", "Überschrift ^Block")).toBe(true);
expect(headingsMatch("Überschrift ^Block", "Überschrift")).toBe(true);
expect(headingsMatch("Überschrift Block", "Überschrift")).toBe(true);
expect(headingsMatch("Überschrift", "Überschrift Block")).toBe(true);
expect(headingsMatch("Überschrift ^Block", "Überschrift Block")).toBe(true);
// Multi-word with ^: "Lars ist gut ^PersLars" <-> "Lars ist gut" (idempotent)
expect(headingsMatch("Lars ist gut ^PersLars", "Lars ist gut")).toBe(true);
expect(headingsMatch("Lars ist gut", "Lars ist gut ^PersLars")).toBe(true);
expect(headingsMatch("Nächster Schritt ^next", "Nächster Schritt")).toBe(true);
});
it("does not match different headings", () => {

View File

@ -6,20 +6,28 @@ import { App, TFile } from "obsidian";
/**
* Normalize heading string for comparison only (Inspect Chains / Chain Workbench).
* Maps "Überschrift", "Überschrift ^Block", "Überschrift Block" to the same canonical form
* so that Obsidian UI links (no ^) and plugin/sectionParser variants match.
* - Strip trailing block-id with caret: \s+\^[a-zA-Z0-9_-]+$
* - Strip one trailing word (Obsidian stores "Überschrift Block" without ^)
* Maps "Überschrift ^Block", "Lars ist gut ^PersLars" etc. to canonical form.
* - Strip trailing block-id with caret: \s*\^[a-zA-Z0-9_-]+$ (with or without space before ^)
* - When no ^ is present, return as-is (idempotency for multi-word headings)
* Use only for equality checks, not for display or stored links.
*/
export function normalizeHeadingForMatch(heading: string | null): string | null {
if (heading === null || heading === undefined) return null;
let s = heading.trim();
if (!s) return null;
// 1) Remove optional block-id suffix with caret: "Überschrift ^Block" -> "Überschrift"
s = s.replace(/\s+\^[a-zA-Z0-9_-]+\s*$/, "").trim();
// 2) Remove one trailing word (Obsidian link form "Überschrift Block" -> "Überschrift")
s = s.replace(/\s+[a-zA-Z0-9_-]+\s*$/, "").trim();
// 1) Remove optional block-id suffix with caret: "Überschrift ^Block" or "Überschrift^Block" -> "Überschrift"
// Use \s* instead of \s+ to match both " ^block" and "^block" (with or without space before ^)
const hadCaretBlockId = /\s*\^[a-zA-Z0-9_-]+\s*$/.test(s);
if (hadCaretBlockId) {
s = s.replace(/\s*\^[a-zA-Z0-9_-]+\s*$/, "").trim();
// Do NOT apply step 2: the ^BlockID was the block reference; the rest is the title.
// This ensures idempotency: norm(norm(x)) === norm(x) when x is already normalized.
return s || null;
}
// 2) When no ^ was present: return as-is for idempotency. The input may be the canonical
// form from a previous normalization (e.g. key "Lars ist gut" from "Lars ist gut ^PersLars").
// Applying step 2 would incorrectly remove words, breaking headingsMatch.
// Obsidian format "Titel BlockID" (without ^) is not handled; use ^ format for reliability.
return s || null;
}

View File

@ -231,18 +231,22 @@ async function insertEdgeIntoFile(
zoneContent.includes(`> [!${wrapperCalloutType}]`) && zoneContent.includes(wrapperTitle);
let insertLine = zone.endLine - 1;
const edgeLinesToInsert = [
EDGE_GROUP_SEPARATOR_LINE,
...newEdgeLines.split("\n").filter((line) => line.trim().length > 0),
];
if (hasWrapper) {
const wrapperEnd = findWrapperBlockEnd(lines, zone.startLine, wrapperCalloutType, wrapperTitle);
if (wrapperEnd != null) {
const afterSameType = findInsertLineAfterSameEdgeType(lines, zone.startLine, wrapperEnd, edgeType);
const lastContentLine = findLastContentLineInWrapper(lines, zone.startLine, wrapperEnd);
insertLine = afterSameType ?? (lastContentLine + 1);
const edgeContentLines = newEdgeLines.split("\n").filter((line) => line.trim().length > 0);
const edgeLinesToInsert = afterSameType !== null
? edgeContentLines
: [EDGE_GROUP_SEPARATOR_LINE, ...edgeContentLines];
removeBlanksAndInsert(lines, insertLine, wrapperEnd, edgeLinesToInsert);
} else {
const edgeLinesToInsert = [
EDGE_GROUP_SEPARATOR_LINE,
...newEdgeLines.split("\n").filter((line) => line.trim().length > 0),
];
lines.splice(insertLine, 0, ...edgeLinesToInsert);
}
} else {
@ -315,10 +319,10 @@ async function insertEdgeIntoSection(
const afterSameType = findInsertLineAfterSameEdgeType(lines, sectionStart, wrapperEnd, edgeType);
const lastContentLine = findLastContentLineInWrapper(lines, sectionStart, wrapperEnd);
const insertLine = afterSameType ?? (lastContentLine + 1);
const edgeLines = [
EDGE_GROUP_SEPARATOR_LINE,
...newEdgeLines.split("\n").filter((line) => line.trim().length > 0),
];
const edgeContentLines = newEdgeLines.split("\n").filter((line) => line.trim().length > 0);
const edgeLines = afterSameType !== null
? edgeContentLines
: [EDGE_GROUP_SEPARATOR_LINE, ...edgeContentLines];
removeBlanksAndInsert(lines, insertLine, wrapperEnd, edgeLines);
} else {
const wrapper = `\n> [!${wrapperCalloutType}]${foldMarker}${wrapperTitle ? ` ${wrapperTitle}` : ""}\n${newEdgeLines.trimEnd()}`;