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;
|
||||
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({
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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 || "";
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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", () => {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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()}`;
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user