This is an automated email from the ASF dual-hosted git repository.

spmallette pushed a commit to branch afd
in repository https://gitbox.apache.org/repos/asf/tinkerpop.git


The following commit(s) were added to refs/heads/afd by this push:
     new bbedd343ba Reference spun-off child pages from their parent in split 
Markdown
bbedd343ba is described below

commit bbedd343ba1624a424f459c3bd7cf43940acdafd
Author: Stephen Mallette <[email protected]>
AuthorDate: Mon Jul 27 15:40:44 2026 -0400

    Reference spun-off child pages from their parent in split Markdown
    
    When a summarized section breaks off to its own .md page, render a
    table-of-contents entry in its place on the parent page -- the child's
    heading (at its original level), its llms-summary as prose, and a link to
    the page -- instead of silently dropping the content. The parent stayed
    readable only for the sections that happened not to break off, so a book
    like reference/index.md jumped from its intro straight to the conclusion.
    
    The reference cascades at every level (index -> chapter -> subsection), so
    the whole book is followable page to page.
    
    Assisted-by: Claude Code:claude-opus-4-8
---
 .../tinkerpop/tinkeradoc/MarkdownSplitter.java     | 80 +++++++++++++++++++---
 .../tinkerpop/tinkeradoc/MarkdownSplitterTest.java | 44 ++++++++++++
 2 files changed, 113 insertions(+), 11 deletions(-)

diff --git 
a/docs/tinkeradoc-extension/src/main/java/org/apache/tinkerpop/tinkeradoc/MarkdownSplitter.java
 
b/docs/tinkeradoc-extension/src/main/java/org/apache/tinkerpop/tinkeradoc/MarkdownSplitter.java
index 8b37863088..6eabea91d0 100644
--- 
a/docs/tinkeradoc-extension/src/main/java/org/apache/tinkerpop/tinkeradoc/MarkdownSplitter.java
+++ 
b/docs/tinkeradoc-extension/src/main/java/org/apache/tinkerpop/tinkeradoc/MarkdownSplitter.java
@@ -23,6 +23,7 @@ import java.nio.charset.StandardCharsets;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.util.ArrayList;
+import java.util.IdentityHashMap;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
@@ -62,6 +63,8 @@ class MarkdownSplitter {
     private static final Pattern INTRA_LINK = 
Pattern.compile("\\]\\(#([^)]+)\\)");
     // The sole page-break signal: the hidden marker MarkdownConverter emits 
from [llms-summary="..."].
     private static final Pattern SUMMARY_MARKER = Pattern.compile("^<!-- 
llms-summary: .* -->$");
+    // The same marker, capturing the summary text for rendering into parent 
table-of-contents entries.
+    private static final Pattern SUMMARY_TEXT = Pattern.compile("^<!-- 
llms-summary: (.*) -->$");
     // Marker MarkdownConverter emits from allow-oversize="true": this page 
may exceed the budget.
     private static final String ALLOW_OVERSIZE_MARKER = "<!-- 
llms-allow-oversize -->";
 
@@ -234,16 +237,18 @@ class MarkdownSplitter {
         }
 
         final Map<String, String> anchorToFile = new LinkedHashMap<>();
+        final Map<Node, String> nodeToFile = new IdentityHashMap<>();
         final List<PagePlan> plans = new ArrayList<>();
         final PagePlan index = new PagePlan(indexFileName, root);
         plans.add(index);
+        nodeToFile.put(root, index.fileName);
         recordAnchorsUntilBreak(root, index.fileName, anchorToFile);
-        planBreaks(root, plans, anchorToFile);
+        planBreaks(root, plans, anchorToFile, nodeToFile);
 
         final List<Page> pages = new ArrayList<>();
         for (final PagePlan plan : plans) {
             final StringBuilder body = new StringBuilder();
-            renderPage(plan.owner, body, plan == index);
+            renderPage(plan.owner, body, plan == index, nodeToFile);
             final String rewritten = rewriteLinks(body.toString(), 
plan.fileName, anchorToFile);
             pages.add(new Page(plan.fileName, LLMS_POINTER + rewritten, 
isAllowOversize(plan.owner)));
         }
@@ -274,14 +279,16 @@ class MarkdownSplitter {
      * sections each get a page.
      */
     private void planBreaks(final Node node, final List<PagePlan> plans,
-                            final Map<String, String> anchorToFile) {
+                            final Map<String, String> anchorToFile,
+                            final Map<Node, String> nodeToFile) {
         for (final Node child : node.children) {
             if (hasSummary(child)) {
                 final PagePlan page = new PagePlan(fileNameFor(child, plans), 
child);
                 plans.add(page);
+                nodeToFile.put(child, page.fileName);
                 recordAnchorsUntilBreak(child, page.fileName, anchorToFile);
             }
-            planBreaks(child, plans, anchorToFile);
+            planBreaks(child, plans, anchorToFile, nodeToFile);
         }
     }
 
@@ -304,22 +311,73 @@ class MarkdownSplitter {
 
     /**
      * Renders one page: {@code owner}'s own lines, then each descendant up to 
the next summarized
-     * section. Summarized descendants are omitted here (they render on their 
own page).
+     * section. A summarized descendant does not render its content here (that 
lives on its own page);
+     * in its place a table-of-contents reference to that page is emitted, so 
the parent stays a
+     * coherent, navigable outline rather than silently dropping the child's 
content.
      */
-    private void renderPage(final Node owner, final StringBuilder sb, final 
boolean isIndex) {
+    private void renderPage(final Node owner, final StringBuilder sb, final 
boolean isIndex,
+                            final Map<Node, String> nodeToFile) {
         for (final String l : owner.lines) sb.append(l).append('\n');
         for (final Node child : owner.children) {
-            if (hasSummary(child)) continue;
-            renderSubtree(child, sb);
+            if (hasSummary(child)) {
+                appendChildReference(child, sb, nodeToFile);
+                continue;
+            }
+            renderSubtree(child, sb, nodeToFile);
         }
     }
 
-    private void renderSubtree(final Node node, final StringBuilder sb) {
+    private void renderSubtree(final Node node, final StringBuilder sb,
+                               final Map<Node, String> nodeToFile) {
         for (final String l : node.lines) sb.append(l).append('\n');
         for (final Node child : node.children) {
-            if (hasSummary(child)) continue;
-            renderSubtree(child, sb);
+            if (hasSummary(child)) {
+                appendChildReference(child, sb, nodeToFile);
+                continue;
+            }
+            renderSubtree(child, sb, nodeToFile);
+        }
+    }
+
+    /**
+     * Emits a table-of-contents reference to a child section that has broken 
off to its own page:
+     * the child's heading (kept at its original level), its llms-summary as 
prose, and a link to the
+     * page. This keeps the parent a coherent outline that points at every 
spun-off child, the same
+     * way {@code llms.txt} references pages.
+     */
+    private void appendChildReference(final Node child, final StringBuilder sb,
+                                      final Map<Node, String> nodeToFile) {
+        sb.append('\n');
+        final String heading = headingText(child);
+        if (heading != null) {
+            sb.append("#".repeat(Math.max(1, child.level))).append(' 
').append(heading).append("\n\n");
+        }
+        final String summary = summaryText(child);
+        if (summary != null) {
+            sb.append(summary).append("\n\n");
+        }
+        final String file = nodeToFile.get(child);
+        if (file != null) {
+            sb.append("[Read more](").append(file).append(")\n\n");
+        }
+    }
+
+    /** The text of a node's heading line (the part after the {@code #}s), or 
{@code null} if none. */
+    private static String headingText(final Node node) {
+        for (final String line : node.lines) {
+            final Matcher hm = HEADING.matcher(line);
+            if (hm.matches()) return hm.group(2).trim();
+        }
+        return null;
+    }
+
+    /** The llms-summary text carried by a node, or {@code null} if it has 
none. */
+    private static String summaryText(final Node node) {
+        for (final String line : node.lines) {
+            final Matcher m = SUMMARY_TEXT.matcher(line.trim());
+            if (m.matches()) return m.group(1).trim();
         }
+        return null;
     }
 
     /**
diff --git 
a/docs/tinkeradoc-extension/src/test/java/org/apache/tinkerpop/tinkeradoc/MarkdownSplitterTest.java
 
b/docs/tinkeradoc-extension/src/test/java/org/apache/tinkerpop/tinkeradoc/MarkdownSplitterTest.java
index 67b2810523..f6587e3d1a 100644
--- 
a/docs/tinkeradoc-extension/src/test/java/org/apache/tinkerpop/tinkeradoc/MarkdownSplitterTest.java
+++ 
b/docs/tinkeradoc-extension/src/test/java/org/apache/tinkerpop/tinkeradoc/MarkdownSplitterTest.java
@@ -97,6 +97,50 @@ public class MarkdownSplitterTest {
         assertThat(page(pages, "index.md").getContent(), 
not(containsString("content a")));
     }
 
+    @Test
+    public void parentPageReferencesEachSpunOffChild() {
+        // 2b: when a summarized child breaks off, the parent renders a 
table-of-contents entry in its
+        // place -- the child's heading (kept at its level), its summary as 
prose, and a link to the
+        // page -- so the parent stays a coherent outline instead of jumping 
over the missing content.
+        final String md = "preamble\n\n" + heading("io", 1, "IO Reference") + 
"\nintro\n\n"
+                + summarized("graphml", 2, "GraphML", "The GraphML format.") + 
"\ncontent graphml\n\n"
+                + heading("wrapup", 1, "Wrap Up") + "\nclosing words\n";
+        final List<MarkdownSplitter.Page> pages = new 
MarkdownSplitter().split(md, "index.md");
+        final String index = page(pages, "index.md").getContent();
+        // Heading kept at the child's own level, summary rendered as prose, 
and a link to its page.
+        assertThat(index, containsString("## GraphML"));
+        assertThat(index, containsString("The GraphML format."));
+        assertThat(index, containsString("[Read more](graphml.md)"));
+        // ...but not the child's body, which lives on its own page.
+        assertThat(index, not(containsString("content graphml")));
+        // The reference sits in the child's original position: after the 
intro, before the wrap-up.
+        assertThat(index.indexOf("intro") < index.indexOf("## GraphML"), 
is(true));
+        assertThat(index.indexOf("## GraphML") < index.indexOf("Wrap Up"), 
is(true));
+    }
+
+    @Test
+    public void referenceCascadesThroughEveryLevel() {
+        // The table-of-contents reference applies at every depth: a chapter 
that is itself its own
+        // page must reference its own summarized subsections, so the whole 
book stays followable top
+        // to bottom (index -> chapter -> subsection), not just from the 
landing page.
+        final String md = "preamble\n\n" + heading("io", 1, "IO Reference") + 
"\nintro\n\n"
+                + summarized("traversal", 1, "The Traversal", "The step 
catalog.") + "\ncatalog intro\n\n"
+                + summarized("fold-step", 2, "Fold Step", "fold() 
aggregates.") + "\nfold body\n\n"
+                + summarized("group-step", 2, "Group Step", "group() 
organizes.") + "\ngroup body\n";
+        final List<MarkdownSplitter.Page> pages = new 
MarkdownSplitter().split(md, "index.md");
+        // The landing page references the chapter...
+        assertThat(page(pages, "index.md").getContent(), containsString("[Read 
more](traversal.md)"));
+        // ...and the chapter page in turn references each of its own spun-off 
steps.
+        final String traversal = page(pages, "traversal.md").getContent();
+        assertThat(traversal, containsString("## Fold Step"));
+        assertThat(traversal, containsString("[Read more](fold-step.md)"));
+        assertThat(traversal, containsString("## Group Step"));
+        assertThat(traversal, containsString("[Read more](group-step.md)"));
+        // ...without pulling in the steps' bodies, which live on their own 
pages.
+        assertThat(traversal, not(containsString("fold body")));
+        assertThat(traversal, not(containsString("group body")));
+    }
+
     @Test
     public void unsummarizedChildAttachesToSummarizedAncestorPage() {
         // Chapter is summarized (own page); its child A1 is not (stays on 
chapter page); its child A2

Reply via email to