Fixed recognizing Links with spaces
This commit is contained in:
parent
523a850ebb
commit
c40a89096f
|
|
@ -362,6 +362,8 @@ export async function buildCandidateNodes(
|
||||||
if (section.sectionType) effectiveType = section.sectionType;
|
if (section.sectionType) effectiveType = section.sectionType;
|
||||||
displayHeading = section.heading; // canonical heading from file (e.g. with ^block-id)
|
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({
|
candidateNodes.push({
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ export function getSectionTypeForHeading(content: string, heading: string): stri
|
||||||
for (let i = 0; i < lines.length; i++) {
|
for (let i = 0; i < lines.length; i++) {
|
||||||
const line = lines[i];
|
const line = lines[i];
|
||||||
if (line === undefined) continue;
|
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]) {
|
if (match && match[2]) {
|
||||||
const lineHeading = match[2].trim();
|
const lineHeading = match[2].trim();
|
||||||
if (lineHeading.toLowerCase() === headingNorm) {
|
if (lineHeading.toLowerCase() === headingNorm) {
|
||||||
|
|
@ -115,7 +115,7 @@ export async function getHeadingsWithSectionTypes(
|
||||||
for (let i = 0; i < lines.length; i++) {
|
for (let i = 0; i < lines.length; i++) {
|
||||||
const line = lines[i];
|
const line = lines[i];
|
||||||
if (line === undefined) continue;
|
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]) {
|
if (match && match[1] && match[2]) {
|
||||||
const level = match[1].length;
|
const level = match[1].length;
|
||||||
const heading = match[2].trim();
|
const heading = match[2].trim();
|
||||||
|
|
|
||||||
|
|
@ -17,8 +17,8 @@ export interface NoteSection {
|
||||||
|
|
||||||
/** Match `> [!section] type` callout (type = word characters). */
|
/** Match `> [!section] type` callout (type = word characters). */
|
||||||
const SECTION_CALLOUT_REGEX = /^\s*>\s*\[!section\]\s*(\S+)\s*$/i;
|
const SECTION_CALLOUT_REGEX = /^\s*>\s*\[!section\]\s*(\S+)\s*$/i;
|
||||||
/** Match block ID at end of heading text: `... ^block-id`. */
|
/** 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*$/;
|
const BLOCK_ID_IN_HEADING_REGEX = /\s*\^([a-zA-Z0-9_-]+)\s*$/;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Split markdown content into sections by headings.
|
* Split markdown content into sections by headings.
|
||||||
|
|
|
||||||
|
|
@ -136,7 +136,8 @@ type: experience
|
||||||
if (!h) return "";
|
if (!h) return "";
|
||||||
let s = h.trim();
|
let s = h.trim();
|
||||||
if (!s) return "";
|
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();
|
s = s.replace(/\s+[a-zA-Z0-9_-]+\s*$/, "").trim();
|
||||||
return s || "";
|
return s || "";
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -50,12 +50,10 @@ describe("normalizeHeadingForMatch", () => {
|
||||||
expect(normalizeHeadingForMatch("Kontext ^context-block")).toBe("Kontext");
|
expect(normalizeHeadingForMatch("Kontext ^context-block")).toBe("Kontext");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("strips one trailing word (Obsidian UI link form)", () => {
|
it("leaves heading without caret unchanged (idempotency)", () => {
|
||||||
expect(normalizeHeadingForMatch("Überschrift Block")).toBe("Überschrift");
|
// When no ^ is present, return as-is. Input may be canonical form from "Titel ^Block".
|
||||||
expect(normalizeHeadingForMatch("Kontext")).toBe("Kontext");
|
// This ensures "Lars ist gut" (from "Lars ist gut ^PersLars") is not incorrectly shortened.
|
||||||
});
|
expect(normalizeHeadingForMatch("Lars ist gut")).toBe("Lars ist gut");
|
||||||
|
|
||||||
it("leaves heading without block suffix unchanged", () => {
|
|
||||||
expect(normalizeHeadingForMatch("Überschrift")).toBe("Überschrift");
|
expect(normalizeHeadingForMatch("Überschrift")).toBe("Überschrift");
|
||||||
expect(normalizeHeadingForMatch("Kontext")).toBe("Kontext");
|
expect(normalizeHeadingForMatch("Kontext")).toBe("Kontext");
|
||||||
});
|
});
|
||||||
|
|
@ -70,9 +68,10 @@ describe("headingsMatch", () => {
|
||||||
it("matches heading with block-id variants", () => {
|
it("matches heading with block-id variants", () => {
|
||||||
expect(headingsMatch("Überschrift", "Überschrift ^Block")).toBe(true);
|
expect(headingsMatch("Überschrift", "Überschrift ^Block")).toBe(true);
|
||||||
expect(headingsMatch("Überschrift ^Block", "Überschrift")).toBe(true);
|
expect(headingsMatch("Überschrift ^Block", "Überschrift")).toBe(true);
|
||||||
expect(headingsMatch("Überschrift Block", "Überschrift")).toBe(true);
|
// Multi-word with ^: "Lars ist gut ^PersLars" <-> "Lars ist gut" (idempotent)
|
||||||
expect(headingsMatch("Überschrift", "Überschrift Block")).toBe(true);
|
expect(headingsMatch("Lars ist gut ^PersLars", "Lars ist gut")).toBe(true);
|
||||||
expect(headingsMatch("Überschrift ^Block", "Überschrift Block")).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", () => {
|
it("does not match different headings", () => {
|
||||||
|
|
|
||||||
|
|
@ -6,20 +6,28 @@ import { App, TFile } from "obsidian";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Normalize heading string for comparison only (Inspect Chains / Chain Workbench).
|
* Normalize heading string for comparison only (Inspect Chains / Chain Workbench).
|
||||||
* Maps "Überschrift", "Überschrift ^Block", "Überschrift Block" to the same canonical form
|
* Maps "Überschrift ^Block", "Lars ist gut ^PersLars" etc. to canonical form.
|
||||||
* so that Obsidian UI links (no ^) and plugin/sectionParser variants match.
|
* - Strip trailing block-id with caret: \s*\^[a-zA-Z0-9_-]+$ (with or without space before ^)
|
||||||
* - Strip trailing block-id with caret: \s+\^[a-zA-Z0-9_-]+$
|
* - When no ^ is present, return as-is (idempotency for multi-word headings)
|
||||||
* - Strip one trailing word (Obsidian stores "Überschrift Block" without ^)
|
|
||||||
* Use only for equality checks, not for display or stored links.
|
* Use only for equality checks, not for display or stored links.
|
||||||
*/
|
*/
|
||||||
export function normalizeHeadingForMatch(heading: string | null): string | null {
|
export function normalizeHeadingForMatch(heading: string | null): string | null {
|
||||||
if (heading === null || heading === undefined) return null;
|
if (heading === null || heading === undefined) return null;
|
||||||
let s = heading.trim();
|
let s = heading.trim();
|
||||||
if (!s) return null;
|
if (!s) return null;
|
||||||
// 1) Remove optional block-id suffix with caret: "Überschrift ^Block" -> "Überschrift"
|
// 1) Remove optional block-id suffix with caret: "Überschrift ^Block" or "Überschrift^Block" -> "Überschrift"
|
||||||
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 ^)
|
||||||
// 2) Remove one trailing word (Obsidian link form "Überschrift Block" -> "Überschrift")
|
const hadCaretBlockId = /\s*\^[a-zA-Z0-9_-]+\s*$/.test(s);
|
||||||
s = s.replace(/\s+[a-zA-Z0-9_-]+\s*$/, "").trim();
|
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;
|
return s || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -231,18 +231,22 @@ async function insertEdgeIntoFile(
|
||||||
zoneContent.includes(`> [!${wrapperCalloutType}]`) && zoneContent.includes(wrapperTitle);
|
zoneContent.includes(`> [!${wrapperCalloutType}]`) && zoneContent.includes(wrapperTitle);
|
||||||
|
|
||||||
let insertLine = zone.endLine - 1;
|
let insertLine = zone.endLine - 1;
|
||||||
const edgeLinesToInsert = [
|
|
||||||
EDGE_GROUP_SEPARATOR_LINE,
|
|
||||||
...newEdgeLines.split("\n").filter((line) => line.trim().length > 0),
|
|
||||||
];
|
|
||||||
if (hasWrapper) {
|
if (hasWrapper) {
|
||||||
const wrapperEnd = findWrapperBlockEnd(lines, zone.startLine, wrapperCalloutType, wrapperTitle);
|
const wrapperEnd = findWrapperBlockEnd(lines, zone.startLine, wrapperCalloutType, wrapperTitle);
|
||||||
if (wrapperEnd != null) {
|
if (wrapperEnd != null) {
|
||||||
const afterSameType = findInsertLineAfterSameEdgeType(lines, zone.startLine, wrapperEnd, edgeType);
|
const afterSameType = findInsertLineAfterSameEdgeType(lines, zone.startLine, wrapperEnd, edgeType);
|
||||||
const lastContentLine = findLastContentLineInWrapper(lines, zone.startLine, wrapperEnd);
|
const lastContentLine = findLastContentLineInWrapper(lines, zone.startLine, wrapperEnd);
|
||||||
insertLine = afterSameType ?? (lastContentLine + 1);
|
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);
|
removeBlanksAndInsert(lines, insertLine, wrapperEnd, edgeLinesToInsert);
|
||||||
} else {
|
} else {
|
||||||
|
const edgeLinesToInsert = [
|
||||||
|
EDGE_GROUP_SEPARATOR_LINE,
|
||||||
|
...newEdgeLines.split("\n").filter((line) => line.trim().length > 0),
|
||||||
|
];
|
||||||
lines.splice(insertLine, 0, ...edgeLinesToInsert);
|
lines.splice(insertLine, 0, ...edgeLinesToInsert);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -315,10 +319,10 @@ async function insertEdgeIntoSection(
|
||||||
const afterSameType = findInsertLineAfterSameEdgeType(lines, sectionStart, wrapperEnd, edgeType);
|
const afterSameType = findInsertLineAfterSameEdgeType(lines, sectionStart, wrapperEnd, edgeType);
|
||||||
const lastContentLine = findLastContentLineInWrapper(lines, sectionStart, wrapperEnd);
|
const lastContentLine = findLastContentLineInWrapper(lines, sectionStart, wrapperEnd);
|
||||||
const insertLine = afterSameType ?? (lastContentLine + 1);
|
const insertLine = afterSameType ?? (lastContentLine + 1);
|
||||||
const edgeLines = [
|
const edgeContentLines = newEdgeLines.split("\n").filter((line) => line.trim().length > 0);
|
||||||
EDGE_GROUP_SEPARATOR_LINE,
|
const edgeLines = afterSameType !== null
|
||||||
...newEdgeLines.split("\n").filter((line) => line.trim().length > 0),
|
? edgeContentLines
|
||||||
];
|
: [EDGE_GROUP_SEPARATOR_LINE, ...edgeContentLines];
|
||||||
removeBlanksAndInsert(lines, insertLine, wrapperEnd, edgeLines);
|
removeBlanksAndInsert(lines, insertLine, wrapperEnd, edgeLines);
|
||||||
} else {
|
} else {
|
||||||
const wrapper = `\n> [!${wrapperCalloutType}]${foldMarker}${wrapperTitle ? ` ${wrapperTitle}` : ""}\n${newEdgeLines.trimEnd()}`;
|
const wrapper = `\n> [!${wrapperCalloutType}]${foldMarker}${wrapperTitle ? ` ${wrapperTitle}` : ""}\n${newEdgeLines.trimEnd()}`;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user