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