From 340fec3cab627fd427b20eaef6159785b3d593f7 Mon Sep 17 00:00:00 2001 From: Nick Beckley Date: Sat, 5 Sep 2026 20:36:26 +0000 Subject: [PATCH] Reuse Word markers before inserting anchors --- PlotLine.Tests/Program.cs | 5 +-- PlotLine/wwwroot/js/word-companion-host.js | 42 +++++++++++++++++++--- 2 files changed, 41 insertions(+), 6 deletions(-) diff --git a/PlotLine.Tests/Program.cs b/PlotLine.Tests/Program.cs index 495ca51..53e620c 100644 --- a/PlotLine.Tests/Program.cs +++ b/PlotLine.Tests/Program.cs @@ -1980,11 +1980,12 @@ static void WordCompanionRuntimeFollowsMultipleImportedSceneAnchors() Assert(script.Contains("if (paragraph.sceneAnchorId)", StringComparison.Ordinal), "Runtime structure should start scenes from PD-SCENE anchors on ordinary paragraphs."); Assert(script.Contains("return chapter.scenes\r\n .filter((item) => item.startParagraphIndex <= paragraphIndex)", StringComparison.Ordinal) || script.Contains("return chapter.scenes\n .filter((item) => item.startParagraphIndex <= paragraphIndex)", StringComparison.Ordinal), "Cursor resolution should select the latest preceding scene boundary."); - Assert(script.Contains("findExactAnchorControl(paragraph, tag)", StringComparison.Ordinal), "Marker insertion should reuse only the exact canonical SceneID tag."); + Assert(script.Contains("findExactAnchorControl(paragraph, tag)", StringComparison.Ordinal), "Marker insertion should prefer the exact canonical SceneID tag."); + Assert(script.Contains("const reusable = existing || findAnchorControl(paragraph, prefix)", StringComparison.Ordinal), "Marker insertion should reuse an existing same-prefix marker on the same paragraph before inserting another control."); Assert(script.Contains("const rangeLocations = [\"Content\", \"Start\", \"Whole\"]", StringComparison.Ordinal), "Marker insertion should try heading content before progressively smaller Word ranges."); Assert(script.Contains("paragraph.getRange(rangeLocation).insertContentControl()", StringComparison.Ordinal), "Marker insertion should retry Word-supported paragraph range locations."); Assert(script.Contains("await ensureAnchorControl(context, paragraphs.items[index], title, tag, prefix)", StringComparison.Ordinal), "Bulk marker sync should await each marker insertion so Word range failures are isolated."); - Assert(!script.Contains("const existing = findAnchorControl(paragraph, prefix);", StringComparison.Ordinal), "Marker insertion must not collapse distinct PD-SCENE tags by prefix."); + Assert(script.Contains("paragraphs.items[index].contentControls.load(\"items/tag,title\")", StringComparison.Ordinal), "Bulk marker sync should inspect existing paragraph markers before deciding to insert content controls."); } static void WordCompanionExistingProjectBookLinkingRemainsAvailable() diff --git a/PlotLine/wwwroot/js/word-companion-host.js b/PlotLine/wwwroot/js/word-companion-host.js index 829ce01..a66cc14 100644 --- a/PlotLine/wwwroot/js/word-companion-host.js +++ b/PlotLine/wwwroot/js/word-companion-host.js @@ -2621,6 +2621,39 @@ paragraphs.load("items/text,items/style,items/styleBuiltIn"); await context.sync(); + const markerParagraphIndexes = new Set(); + if (chapterInstructions.length > 0 || sceneInstructions.length > 0) { + for (const instruction of [...chapterInstructions, ...sceneInstructions]) { + const index = Number.parseInt(instruction.wordParagraphIndex ?? instruction.WordParagraphIndex, 10); + if (Number.isInteger(index) && paragraphs.items[index]) { + markerParagraphIndexes.add(index); + } + } + } else { + for (const chapter of lastOnboardingScanPreview.chapters || []) { + const index = Number.parseInt(chapter.startPosition, 10); + if (Number.isInteger(index) && paragraphs.items[index]) { + markerParagraphIndexes.add(index); + } + } + + for (const scene of lastOnboardingScanPreview.scenes || []) { + const index = Number.parseInt(scene.startPosition, 10); + if (Number.isInteger(index) && paragraphs.items[index]) { + markerParagraphIndexes.add(index); + } + } + } + + try { + for (const index of markerParagraphIndexes) { + paragraphs.items[index].contentControls.load("items/tag,title"); + } + await context.sync(); + } catch (error) { + console.warn("Unable to inspect existing PlotDirector markers before marker sync.", error); + } + if (chapterInstructions.length > 0 || sceneInstructions.length > 0) { for (const instruction of chapterInstructions) { const index = Number.parseInt(instruction.wordParagraphIndex ?? instruction.WordParagraphIndex, 10); @@ -4615,10 +4648,11 @@ return false; } - if (existing) { - existing.title = title; - existing.tag = tag; - existing.appearance = "Hidden"; + const reusable = existing || findAnchorControl(paragraph, prefix); + if (reusable) { + reusable.title = title; + reusable.tag = tag; + reusable.appearance = "Hidden"; await context.sync(); return true; }