Add toy-graph dataset captions to rendered Gremlin examples Render an italic caption beneath each Gremlin example naming the toy graph it runs against, linking to that graph's reference section in the HTML output and emitting a machine-parseable `graph-dataset:` line in the Markdown mirror. Resolve the dataset from the AST walk rather than execution state. Introduce GraphCatalog as the single source of truth for toy-graph tokens, replacing the duplicated GRAPH_INIT and GRAPH_DOC maps so adding a dataset is a one-line change. Also fix tab labels wrapping below the tab box: flex-center the labels so a long label like "console (groovy)" wraps within the tab instead of spilling into the gray content area.
diff --git a/docs/sass/tabs.scss b/docs/sass/tabs.scss index 1541fd1..5d009bd 100644 --- a/docs/sass/tabs.scss +++ b/docs/sass/tabs.scss
@@ -65,13 +65,15 @@ background: $inactive; color: $black; font-size: 15px; - line-height: $tabHeight; + line-height: 1.15; height: $tabHeight + 10; position: relative; top: 0; padding: 0 20px; float: left; - display: block; + display: flex; + align-items: center; + justify-content: center; letter-spacing: 1px; text-transform: uppercase; font-weight: bold; @@ -114,6 +116,14 @@ } } +// "console (groovy)" is the only multi-word label; at higher tab counts it now wraps to two lines +// that fit inside the tab box (the label is flex-centered vertically). Trim horizontal padding for +// the tightest counts so single long words (e.g. "javascript") don't overflow the narrow tab. +.tabs-6 label, +.tabs-7 label { + padding: 0 8px; +} + .tabs label:first-of-type { z-index: 4; } @@ -176,3 +186,22 @@ padding-top: 30px; } } + +// Graph-dataset caption for Gremlin examples: an italic line just below the content box naming the +// toy graph the example runs against, with a link to its reference section. Cleared so it drops +// below the floated .tabcontent instead of overlapping it. The negative top margin offsets the +// .tabcontent `top: -10px` shift above, keeping the caption snug against the content box. +.tabs .graph-dataset-note { + clear: both; + margin-top: -8px; + padding: 2px 2px 0; + font-size: 13px; + font-style: italic; + color: #555; + text-align: right; + + a { + color: $active; + text-decoration: underline; + } +}
diff --git a/docs/stylesheets/tinkerpop.css b/docs/stylesheets/tinkerpop.css index 71cc47e..b9090f3 100644 --- a/docs/stylesheets/tinkerpop.css +++ b/docs/stylesheets/tinkerpop.css
@@ -692,4 +692,8 @@ #footer { background-color: #465158; padding: 2em; } #footer-text { color: #eee; font-size: 0.8em; text-align: center; } -.tabs{position:relative;margin:40px auto;width:1024px;max-width:100%;overflow:hidden;padding-top:10px;margin-bottom:60px}.tabs input{position:absolute;z-index:1000;height:50px;left:0;top:0;opacity:0;-ms-filter:"progid:DXImageTransform.Microsoft.Alpha(Opacity=0)";filter:alpha(opacity=0);cursor:pointer;margin:0}.tabs input:hover+label{background:#e08f24}.tabs label{background:#e9ffe9;color:#1a1a1a;font-size:15px;line-height:50px;height:60px;position:relative;top:0;padding:0 20px;float:left;display:block;letter-spacing:1px;text-transform:uppercase;font-weight:bold;text-align:center;box-shadow:2px 0 2px rgba(0,0,0,0.1),-2px 0 2px rgba(0,0,0,0.1);box-sizing:border-box;-webkit-transition:all 150ms ease 0s;transition:all 150ms ease 0s}.tabs label:hover{cursor:pointer}.tabs label:after{content:'';background:#609060;position:absolute;bottom:-2px;left:0;width:100%;height:2px;display:block}.tabs-2 input{width:50%}.tabs-2 input.tab-selector-1{left:0%}.tabs-2 input.tab-selector-2{left:50%}.tabs-2 label{width:50%}.tabs-3 input{width:33.3333333333%}.tabs-3 input.tab-selector-1{left:0%}.tabs-3 input.tab-selector-2{left:33.3333333333%}.tabs-3 input.tab-selector-3{left:66.6666666667%}.tabs-3 label{width:33.3333333333%}.tabs-4 input{width:25%}.tabs-4 input.tab-selector-1{left:0%}.tabs-4 input.tab-selector-2{left:25%}.tabs-4 input.tab-selector-3{left:50%}.tabs-4 input.tab-selector-4{left:75%}.tabs-4 label{width:25%}.tabs-5 input{width:20%}.tabs-5 input.tab-selector-1{left:0%}.tabs-5 input.tab-selector-2{left:20%}.tabs-5 input.tab-selector-3{left:40%}.tabs-5 input.tab-selector-4{left:60%}.tabs-5 input.tab-selector-5{left:80%}.tabs-5 label{width:20%}.tabs-6 input{width:16.6666666667%}.tabs-6 input.tab-selector-1{left:0%}.tabs-6 input.tab-selector-2{left:16.6666666667%}.tabs-6 input.tab-selector-3{left:33.3333333333%}.tabs-6 input.tab-selector-4{left:50%}.tabs-6 input.tab-selector-5{left:66.6666666667%}.tabs-6 input.tab-selector-6{left:83.3333333333%}.tabs-6 label{width:16.6666666667%}.tabs-7 input{width:14.2857142857%}.tabs-7 input.tab-selector-1{left:0%}.tabs-7 input.tab-selector-2{left:14.2857142857%}.tabs-7 input.tab-selector-3{left:28.5714285714%}.tabs-7 input.tab-selector-4{left:42.8571428571%}.tabs-7 input.tab-selector-5{left:57.1428571429%}.tabs-7 input.tab-selector-6{left:71.4285714286%}.tabs-7 input.tab-selector-7{left:85.7142857143%}.tabs-7 label{width:14.2857142857%}.tabs label:first-of-type{z-index:4}.tab-label-2{z-index:4}.tab-label-3{z-index:3}.tab-label-4{z-index:2}.tabs input:checked+label{background:#609060;color:#fefefe;z-index:6}.clear-shadow{clear:both}.tabcontent{height:auto;width:100%;float:left;position:relative;z-index:5;background:#eee;top:-10px;box-sizing:border-box}.tabcontent>div{position:relative;float:left;width:0;height:0;box-sizing:border-box;top:0;left:0;z-index:1;opacity:0;background:#eee}.tabcontent .CodeRay{background-color:#fefefe}.tabs .tab-selector-1:checked ~ .tabcontent .tabcontent-1{z-index:100;-ms-filter:"progid:DXImageTransform.Microsoft.Alpha(Opacity=100)";filter:alpha(opacity=100);opacity:1;width:100%;height:auto;width:100%;height:auto;padding-top:30px}.tabs .tab-selector-2:checked ~ .tabcontent .tabcontent-2{z-index:100;-ms-filter:"progid:DXImageTransform.Microsoft.Alpha(Opacity=100)";filter:alpha(opacity=100);opacity:1;width:100%;height:auto;width:100%;height:auto;padding-top:30px}.tabs .tab-selector-3:checked ~ .tabcontent .tabcontent-3{z-index:100;-ms-filter:"progid:DXImageTransform.Microsoft.Alpha(Opacity=100)";filter:alpha(opacity=100);opacity:1;width:100%;height:auto;width:100%;height:auto;padding-top:30px}.tabs .tab-selector-4:checked ~ .tabcontent .tabcontent-4{z-index:100;-ms-filter:"progid:DXImageTransform.Microsoft.Alpha(Opacity=100)";filter:alpha(opacity=100);opacity:1;width:100%;height:auto;width:100%;height:auto;padding-top:30px}.tabs .tab-selector-5:checked ~ .tabcontent .tabcontent-5{z-index:100;-ms-filter:"progid:DXImageTransform.Microsoft.Alpha(Opacity=100)";filter:alpha(opacity=100);opacity:1;width:100%;height:auto;width:100%;height:auto;padding-top:30px}.tabs .tab-selector-6:checked ~ .tabcontent .tabcontent-6{z-index:100;-ms-filter:"progid:DXImageTransform.Microsoft.Alpha(Opacity=100)";filter:alpha(opacity=100);opacity:1;width:100%;height:auto;width:100%;height:auto;padding-top:30px}.tabs .tab-selector-7:checked ~ .tabcontent .tabcontent-7{z-index:100;-ms-filter:"progid:DXImageTransform.Microsoft.Alpha(Opacity=100)";filter:alpha(opacity=100);opacity:1;width:100%;height:auto;width:100%;height:auto;padding-top:30px} +.tabs{position:relative;margin:40px auto;width:1024px;max-width:100%;overflow:hidden;padding-top:10px;margin-bottom:60px}.tabs input{position:absolute;z-index:1000;height:50px;left:0;top:0;opacity:0;-ms-filter:"progid:DXImageTransform.Microsoft.Alpha(Opacity=0)";filter:alpha(opacity=0);cursor:pointer;margin:0}.tabs input:hover+label{background:#e08f24}.tabs label{background:#e9ffe9;color:#1a1a1a;font-size:15px;line-height:1.15;height:60px;position:relative;top:0;padding:0 20px;float:left;display:flex;align-items:center;justify-content:center;letter-spacing:1px;text-transform:uppercase;font-weight:bold;text-align:center;box-shadow:2px 0 2px rgba(0,0,0,0.1),-2px 0 2px rgba(0,0,0,0.1);box-sizing:border-box;-webkit-transition:all 150ms ease 0s;transition:all 150ms ease 0s}.tabs label:hover{cursor:pointer}.tabs label:after{content:'';background:#609060;position:absolute;bottom:-2px;left:0;width:100%;height:2px;display:block}.tabs-2 input{width:50%}.tabs-2 input.tab-selector-1{left:0%}.tabs-2 input.tab-selector-2{left:50%}.tabs-2 label{width:50%}.tabs-3 input{width:33.3333333333%}.tabs-3 input.tab-selector-1{left:0%}.tabs-3 input.tab-selector-2{left:33.3333333333%}.tabs-3 input.tab-selector-3{left:66.6666666667%}.tabs-3 label{width:33.3333333333%}.tabs-4 input{width:25%}.tabs-4 input.tab-selector-1{left:0%}.tabs-4 input.tab-selector-2{left:25%}.tabs-4 input.tab-selector-3{left:50%}.tabs-4 input.tab-selector-4{left:75%}.tabs-4 label{width:25%}.tabs-5 input{width:20%}.tabs-5 input.tab-selector-1{left:0%}.tabs-5 input.tab-selector-2{left:20%}.tabs-5 input.tab-selector-3{left:40%}.tabs-5 input.tab-selector-4{left:60%}.tabs-5 input.tab-selector-5{left:80%}.tabs-5 label{width:20%}.tabs-6 input{width:16.6666666667%}.tabs-6 input.tab-selector-1{left:0%}.tabs-6 input.tab-selector-2{left:16.6666666667%}.tabs-6 input.tab-selector-3{left:33.3333333333%}.tabs-6 input.tab-selector-4{left:50%}.tabs-6 input.tab-selector-5{left:66.6666666667%}.tabs-6 input.tab-selector-6{left:83.3333333333%}.tabs-6 label{width:16.6666666667%}.tabs-7 input{width:14.2857142857%}.tabs-7 input.tab-selector-1{left:0%}.tabs-7 input.tab-selector-2{left:14.2857142857%}.tabs-7 input.tab-selector-3{left:28.5714285714%}.tabs-7 input.tab-selector-4{left:42.8571428571%}.tabs-7 input.tab-selector-5{left:57.1428571429%}.tabs-7 input.tab-selector-6{left:71.4285714286%}.tabs-7 input.tab-selector-7{left:85.7142857143%}.tabs-7 label{width:14.2857142857%}.tabs label:first-of-type{z-index:4}.tab-label-2{z-index:4}.tab-label-3{z-index:3}.tab-label-4{z-index:2}.tabs input:checked+label{background:#609060;color:#fefefe;z-index:6}.clear-shadow{clear:both}.tabcontent{height:auto;width:100%;float:left;position:relative;z-index:5;background:#eee;top:-10px;box-sizing:border-box}.tabcontent>div{position:relative;float:left;width:0;height:0;box-sizing:border-box;top:0;left:0;z-index:1;opacity:0;background:#eee}.tabcontent .CodeRay{background-color:#fefefe}.tabs .tab-selector-1:checked ~ .tabcontent .tabcontent-1{z-index:100;-ms-filter:"progid:DXImageTransform.Microsoft.Alpha(Opacity=100)";filter:alpha(opacity=100);opacity:1;width:100%;height:auto;width:100%;height:auto;padding-top:30px}.tabs .tab-selector-2:checked ~ .tabcontent .tabcontent-2{z-index:100;-ms-filter:"progid:DXImageTransform.Microsoft.Alpha(Opacity=100)";filter:alpha(opacity=100);opacity:1;width:100%;height:auto;width:100%;height:auto;padding-top:30px}.tabs .tab-selector-3:checked ~ .tabcontent .tabcontent-3{z-index:100;-ms-filter:"progid:DXImageTransform.Microsoft.Alpha(Opacity=100)";filter:alpha(opacity=100);opacity:1;width:100%;height:auto;width:100%;height:auto;padding-top:30px}.tabs .tab-selector-4:checked ~ .tabcontent .tabcontent-4{z-index:100;-ms-filter:"progid:DXImageTransform.Microsoft.Alpha(Opacity=100)";filter:alpha(opacity=100);opacity:1;width:100%;height:auto;width:100%;height:auto;padding-top:30px}.tabs .tab-selector-5:checked ~ .tabcontent .tabcontent-5{z-index:100;-ms-filter:"progid:DXImageTransform.Microsoft.Alpha(Opacity=100)";filter:alpha(opacity=100);opacity:1;width:100%;height:auto;width:100%;height:auto;padding-top:30px}.tabs .tab-selector-6:checked ~ .tabcontent .tabcontent-6{z-index:100;-ms-filter:"progid:DXImageTransform.Microsoft.Alpha(Opacity=100)";filter:alpha(opacity=100);opacity:1;width:100%;height:auto;width:100%;height:auto;padding-top:30px}.tabs .tab-selector-7:checked ~ .tabcontent .tabcontent-7{z-index:100;-ms-filter:"progid:DXImageTransform.Microsoft.Alpha(Opacity=100)";filter:alpha(opacity=100);opacity:1;width:100%;height:auto;width:100%;height:auto;padding-top:30px} + +.tabs .graph-dataset-note{clear:both;margin-top:-8px;padding:2px 2px 0;font-size:13px;font-style:italic;color:#555;text-align:right}.tabs .graph-dataset-note a{color:#4a704a;text-decoration:underline} + +.tabs-6 label,.tabs-7 label{padding:0 8px}
diff --git a/docs/tinkeradoc-extension/src/main/java/org/apache/tinkerpop/tinkeradoc/GraphCatalog.java b/docs/tinkeradoc-extension/src/main/java/org/apache/tinkerpop/tinkeradoc/GraphCatalog.java new file mode 100644 index 0000000..dd2a975 --- /dev/null +++ b/docs/tinkeradoc-extension/src/main/java/org/apache/tinkerpop/tinkeradoc/GraphCatalog.java
@@ -0,0 +1,94 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.tinkerpop.tinkeradoc; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +/** + * Canonical registry of the toy graph datasets referenced by Gremlin documentation examples. + * <p> + * This is the single source of truth shared by two concerns that must never disagree about the set + * of datasets: + * <ul> + * <li><b>execution</b> — the Gremlin statement run to build the graph before an example + * ({@link GremlinTreeprocessor}); and</li> + * <li><b>rendering</b> — the dataset caption shown beneath an example and its link to the + * reference section ({@link TabbedHtmlBuilder}).</li> + * </ul> + * Adding a graph is therefore a one-line change here that feeds both, so a new dataset can never be + * executable-but-unlabeled (or vice versa). + */ +final class GraphCatalog { + + /** A dataset entry: how to construct it, how to name it, and where its reference section lives. */ + static final class Entry { + /** Gremlin statement that constructs the graph into the {@code graph} binding. */ + final String initStatement; + /** Human-readable name used in the rendered dataset caption. */ + final String displayName; + /** Reference-book section anchor, or {@code null} if the graph has no dedicated section. */ + final String docAnchor; + + Entry(final String initStatement, final String displayName, final String docAnchor) { + this.initStatement = initStatement; + this.displayName = displayName; + this.docAnchor = docAnchor; + } + } + + /** Init statement for a bare block or any unrecognized token: a fresh, empty graph. */ + static final String DEFAULT_INIT = "graph = TinkerGraph.open()"; + + private static final Map<String, Entry> BY_TOKEN; + + static { + final Map<String, Entry> m = new HashMap<>(); + // "crew" and "theCrew" are aliases for the same dataset. + final Entry crew = new Entry("graph = TinkerFactory.createTheCrew()", "crew", "the-crew-toy-graph"); + m.put("modern", new Entry("graph = TinkerFactory.createModern()", "modern", "tinkerpop-modern")); + m.put("classic", new Entry("graph = TinkerFactory.createClassic()", "classic", null)); + m.put("crew", crew); + m.put("theCrew", crew); + m.put("grateful", new Entry("graph = TinkerFactory.createGratefulDead()", "Grateful Dead", "grateful-dead")); + m.put("sink", new Entry("graph = TinkerFactory.createKitchenSink()", "kitchen sink", null)); + BY_TOKEN = Collections.unmodifiableMap(m); + } + + private GraphCatalog() { + } + + /** + * Returns the catalog entry for a dataset token, or {@code null} if the token is not a known + * dataset (including {@code null}, a bare block, or a misspelling). + */ + static Entry entry(final String token) { + return token == null ? null : BY_TOKEN.get(token); + } + + /** + * Returns the Gremlin init statement for a token, falling back to {@link #DEFAULT_INIT} (a fresh + * empty graph) for {@code null} or unrecognized tokens. + */ + static String initStatement(final String token) { + final Entry e = entry(token); + return e != null ? e.initStatement : DEFAULT_INIT; + } +}
diff --git a/docs/tinkeradoc-extension/src/main/java/org/apache/tinkerpop/tinkeradoc/GremlinTreeprocessor.java b/docs/tinkeradoc-extension/src/main/java/org/apache/tinkerpop/tinkeradoc/GremlinTreeprocessor.java index 0106f33..a39d2f4 100644 --- a/docs/tinkeradoc-extension/src/main/java/org/apache/tinkerpop/tinkeradoc/GremlinTreeprocessor.java +++ b/docs/tinkeradoc-extension/src/main/java/org/apache/tinkerpop/tinkeradoc/GremlinTreeprocessor.java
@@ -55,19 +55,6 @@ static final Set<String> SUPPORTED_LANGUAGES = Collections.unmodifiableSet( new HashSet<>(Arrays.asList("groovy", "java", "csharp", "javascript", "python", "go"))); - static final Map<String, String> GRAPH_INIT; - - static { - final Map<String, String> m = new HashMap<>(); - m.put("modern", "graph = TinkerFactory.createModern()"); - m.put("classic", "graph = TinkerFactory.createClassic()"); - m.put("crew", "graph = TinkerFactory.createTheCrew()"); - m.put("theCrew", "graph = TinkerFactory.createTheCrew()"); - m.put("grateful", "graph = TinkerFactory.createGratefulDead()"); - m.put("sink", "graph = TinkerFactory.createKitchenSink()"); - GRAPH_INIT = Collections.unmodifiableMap(m); - } - private int gremlinBlockCount; private final StatementExecutor executor; private final TabbedHtmlBuilder tabBuilder; @@ -78,6 +65,11 @@ private final ConsoleRestartHandler restartHandler; private ConsoleRestartHandler activeRestartHandler; private String currentGraph; + // Tracks the most recently declared graph dataset from the AST walk (independent of execution), + // used only to resolve the label for "existing" blocks. Unlike currentGraph, this is maintained + // on every backend pass — including passes that reuse the execution cache and skip graph init — + // so the rendered dataset label is identical across the HTML and Markdown backends. + private String labelGraphLineage; private String documentId; private List<String> currentExcludedPlugins; @@ -152,6 +144,7 @@ public Document process(final Document document) { gremlinBlockCount = 0; currentGraph = null; + labelGraphLineage = null; documentId = resolveDocumentId(document); markdownMode = isMarkdownBackend(document); final Object dryRunAttr = document.getAttribute("gremlin-docs-dryrun"); @@ -317,6 +310,20 @@ final List<StructuralNode> blocks = parent.getBlocks(); final Block gremlinBlock = (Block) blocks.get(startIndex); + // Resolve the graph dataset name for the label from the AST walk (not execution state), so + // it is identical across backend passes even when a later pass reuses the execution cache and + // skips graph initialization. An "existing" block carries forward the last declared dataset; + // any other block (including a bare block with no dataset) sets the lineage, mirroring how + // initGraphIfNeeded() re-initializes the graph for every non-"existing" block. + final String rawGraphName = extractGraphName(gremlinBlock); + final String resolvedGraphName; + if (EXISTING.equals(rawGraphName)) { + resolvedGraphName = labelGraphLineage; + } else { + resolvedGraphName = rawGraphName; + labelGraphLineage = rawGraphName; + } + // Walk consecutive [source,<lang>] sibling blocks first (FR-5). This is pure AST work with // no execution: it fixes the block range to replace and captures each sibling's language // and source, which are needed whether the console output comes fresh or from the cache. @@ -352,7 +359,7 @@ executionCache.put(cacheKey, tabs); } - emitNeutralTabGroup(parent, startIndex, lastIndex, tabs); + emitNeutralTabGroup(parent, startIndex, lastIndex, tabs, resolvedGraphName); return startIndex; } @@ -377,7 +384,7 @@ } } - emitNeutralTabGroup(parent, startIndex, lastIndex, tabs); + emitNeutralTabGroup(parent, startIndex, lastIndex, tabs, null); return startIndex; } @@ -389,12 +396,12 @@ * into a neutral custom block for a separate render pass to consume. */ private void emitNeutralTabGroup(final StructuralNode parent, final int startIndex, - final int endIndex, final List<NeutralTab> tabs) { + final int endIndex, final List<NeutralTab> tabs, final String graphName) { final String json = NeutralTabCodec.serialize(tabs); final List<NeutralTab> resolvedTabs = NeutralTabCodec.parse(json); final String rendered = markdownMode - ? markdownTabRenderer.render(resolvedTabs) - : htmlTabRenderer.render(parent, resolvedTabs); + ? markdownTabRenderer.render(resolvedTabs, graphName) + : htmlTabRenderer.render(parent, resolvedTabs, graphName); replaceWithPassBlock(parent, startIndex, endIndex, rendered); } @@ -684,12 +691,7 @@ executeSafely("['/tmp/neo4j', '/tmp/tinkergraph.kryo'].each { p -> " + "def f = new File(p); if (f.exists()) f.deleteDir() }"); - final String initStatement; - if (graphName == null) { - initStatement = "graph = TinkerGraph.open()"; - } else { - initStatement = GRAPH_INIT.getOrDefault(graphName, "graph = TinkerGraph.open()"); - } + final String initStatement = GraphCatalog.initStatement(graphName); executeSafely(initStatement); executeSafely("g = graph.traversal()");
diff --git a/docs/tinkeradoc-extension/src/main/java/org/apache/tinkerpop/tinkeradoc/HtmlTabRenderer.java b/docs/tinkeradoc-extension/src/main/java/org/apache/tinkerpop/tinkeradoc/HtmlTabRenderer.java index 3f9f667..1373b87 100644 --- a/docs/tinkeradoc-extension/src/main/java/org/apache/tinkerpop/tinkeradoc/HtmlTabRenderer.java +++ b/docs/tinkeradoc-extension/src/main/java/org/apache/tinkerpop/tinkeradoc/HtmlTabRenderer.java
@@ -45,11 +45,12 @@ /** * Highlights and renders the given neutral tabs into a complete tab-group HTML string. * - * @param parent the AST node used to locate the JRuby runtime for highlighting - * @param tabs the neutral tabs to render + * @param parent the AST node used to locate the JRuby runtime for highlighting + * @param tabs the neutral tabs to render + * @param graphName the resolved graph dataset name (e.g. "modern"), or null if not applicable * @return the tab-group HTML */ - String render(final StructuralNode parent, final List<NeutralTab> tabs) { + String render(final StructuralNode parent, final List<NeutralTab> tabs, final String graphName) { final List<TabbedHtmlBuilder.Tab> htmlTabs = new ArrayList<>(tabs.size()); for (final NeutralTab tab : tabs) { // The existing pipeline highlighted every tab (console and source) as groovy-flavored @@ -62,6 +63,13 @@ htmlTabs.add(TabbedHtmlBuilder.codeTabHighlighted(tab.getLanguage(), highlighted)); } } - return tabBuilder.build(htmlTabs); + return tabBuilder.build(htmlTabs, graphName); + } + + /** + * Highlights and renders the given neutral tabs into a complete tab-group HTML string (no graph label). + */ + String render(final StructuralNode parent, final List<NeutralTab> tabs) { + return render(parent, tabs, null); } }
diff --git a/docs/tinkeradoc-extension/src/main/java/org/apache/tinkerpop/tinkeradoc/MarkdownTabRenderer.java b/docs/tinkeradoc-extension/src/main/java/org/apache/tinkerpop/tinkeradoc/MarkdownTabRenderer.java index bb1685b..cf58352 100644 --- a/docs/tinkeradoc-extension/src/main/java/org/apache/tinkerpop/tinkeradoc/MarkdownTabRenderer.java +++ b/docs/tinkeradoc-extension/src/main/java/org/apache/tinkerpop/tinkeradoc/MarkdownTabRenderer.java
@@ -45,7 +45,22 @@ * @return the Markdown for the tab group (no trailing newline) */ String render(final List<NeutralTab> tabs) { + return render(tabs, null); + } + + /** + * Renders the given neutral tabs as a sequence of labeled fenced code blocks with an optional + * graph-dataset label. + * + * @param tabs the neutral tabs to render + * @param graphName the resolved graph dataset name (e.g. "modern"), or null to omit + * @return the Markdown for the tab group (no trailing newline) + */ + String render(final List<NeutralTab> tabs, final String graphName) { final StringBuilder sb = new StringBuilder(); + if (graphName != null && !graphName.isEmpty()) { + sb.append("*graph-dataset: ").append(graphName).append("*\n\n"); + } for (int i = 0; i < tabs.size(); i++) { if (i > 0) sb.append("\n\n"); final NeutralTab tab = tabs.get(i);
diff --git a/docs/tinkeradoc-extension/src/main/java/org/apache/tinkerpop/tinkeradoc/TabbedHtmlBuilder.java b/docs/tinkeradoc-extension/src/main/java/org/apache/tinkerpop/tinkeradoc/TabbedHtmlBuilder.java index 5098ecf..7cf29d9 100644 --- a/docs/tinkeradoc-extension/src/main/java/org/apache/tinkerpop/tinkeradoc/TabbedHtmlBuilder.java +++ b/docs/tinkeradoc-extension/src/main/java/org/apache/tinkerpop/tinkeradoc/TabbedHtmlBuilder.java
@@ -46,6 +46,28 @@ private int groupCounter = 0; /** + * Builds the italic caption line naming the toy graph an example runs against, linking the graph + * name to its reference section when one exists. Unknown tokens render as plain (unlinked) text. + * The display name and anchor come from the shared {@link GraphCatalog}. The link uses the + * {@code x.y.z} version placeholder, which {@code GremlinPostprocessor} replaces with the real + * version in the final HTML (the same mechanism used by every other cross-reference in the docs). + * + * @param token the resolved graph token (e.g. "modern", "theCrew") + * @return the caption HTML (a {@code <div class="graph-dataset-note">} element) + */ + static String graphNoteHtml(final String token) { + final GraphCatalog.Entry entry = GraphCatalog.entry(token); + final String display = (entry != null) ? entry.displayName : token; + final String anchor = (entry != null) ? entry.docAnchor : null; + final String name = (anchor != null) + ? "<a href=\"https://tinkerpop.apache.org/docs/x.y.z/reference/#" + anchor + "\">" + + escapeHtml(display) + "</a>" + : escapeHtml(display); + return "<div class=\"graph-dataset-note\"><em>This example runs against the " + + name + " graph.</em></div>"; + } + + /** * A single tab entry with a label, language, and source code content. */ static class Tab { @@ -103,6 +125,17 @@ * @return the complete HTML string for the tab group */ String build(final List<Tab> tabs) { + return build(tabs, null); + } + + /** + * Builds tabbed HTML for a list of tabs with an optional graph-dataset caption. + * + * @param tabs the tabs to render + * @param graphName the resolved graph dataset name (e.g. "modern"), or null to omit the caption + * @return the complete HTML string for the tab group + */ + String build(final List<Tab> tabs, final String graphName) { if (tabs == null || tabs.isEmpty()) { return ""; } @@ -145,8 +178,15 @@ } html.append(" </div>\n"); - html.append("</section>"); + // Graph-dataset caption: an italic line rendered just below the content box (as a child of + // the tab section, after and cleared of the floated .tabcontent). It links to the toy + // graph's reference section. Placed here so it never overlaps the code panel or the page. + if (graphName != null && !graphName.isEmpty()) { + html.append(" ").append(graphNoteHtml(graphName)).append("\n"); + } + + html.append("</section>"); return html.toString(); }
diff --git a/docs/tinkeradoc-extension/src/test/java/org/apache/tinkerpop/tinkeradoc/GraphCatalogTest.java b/docs/tinkeradoc-extension/src/test/java/org/apache/tinkerpop/tinkeradoc/GraphCatalogTest.java new file mode 100644 index 0000000..5486078 --- /dev/null +++ b/docs/tinkeradoc-extension/src/test/java/org/apache/tinkerpop/tinkeradoc/GraphCatalogTest.java
@@ -0,0 +1,65 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.tinkerpop.tinkeradoc; + +import org.junit.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.nullValue; + +public class GraphCatalogTest { + + @Test + public void shouldExposeInitStatementDisplayAndAnchorForKnownGraph() { + final GraphCatalog.Entry modern = GraphCatalog.entry("modern"); + assertThat(modern.initStatement, is("graph = TinkerFactory.createModern()")); + assertThat(modern.displayName, is("modern")); + assertThat(modern.docAnchor, is("tinkerpop-modern")); + } + + @Test + public void shouldTreatCrewAndTheCrewAsTheSameDataset() { + final GraphCatalog.Entry crew = GraphCatalog.entry("crew"); + final GraphCatalog.Entry theCrew = GraphCatalog.entry("theCrew"); + assertThat(crew.initStatement, is("graph = TinkerFactory.createTheCrew()")); + assertThat(theCrew.initStatement, is(crew.initStatement)); + assertThat(theCrew.displayName, is(crew.displayName)); + assertThat(theCrew.docAnchor, is(crew.docAnchor)); + } + + @Test + public void shouldReportNullAnchorForGraphWithoutReferenceSection() { + assertThat(GraphCatalog.entry("classic").docAnchor, nullValue()); + assertThat(GraphCatalog.entry("sink").docAnchor, nullValue()); + } + + @Test + public void shouldReturnNoEntryForNullOrUnknownToken() { + assertThat(GraphCatalog.entry(null), nullValue()); + assertThat(GraphCatalog.entry("bogus"), nullValue()); + } + + @Test + public void shouldFallBackToEmptyGraphInitForNullOrUnknownToken() { + assertThat(GraphCatalog.initStatement(null), is(GraphCatalog.DEFAULT_INIT)); + assertThat(GraphCatalog.initStatement("bogus"), is(GraphCatalog.DEFAULT_INIT)); + assertThat(GraphCatalog.DEFAULT_INIT, is("graph = TinkerGraph.open()")); + } +}
diff --git a/docs/tinkeradoc-extension/src/test/java/org/apache/tinkerpop/tinkeradoc/GremlinTreeprocessorTest.java b/docs/tinkeradoc-extension/src/test/java/org/apache/tinkerpop/tinkeradoc/GremlinTreeprocessorTest.java index 209e73f..bd0ec6a 100644 --- a/docs/tinkeradoc-extension/src/test/java/org/apache/tinkerpop/tinkeradoc/GremlinTreeprocessorTest.java +++ b/docs/tinkeradoc-extension/src/test/java/org/apache/tinkerpop/tinkeradoc/GremlinTreeprocessorTest.java
@@ -88,6 +88,68 @@ } @Test + public void shouldRenderGraphBadgeForNamedGraph() { + final RecordingExecutor executor = new RecordingExecutor("==>v[1]"); + final GremlinTreeprocessor processor = new GremlinTreeprocessor(executor); + try (final Asciidoctor asciidoctor = Asciidoctor.Factory.create()) { + asciidoctor.unregisterAllExtensions(); + asciidoctor.javaExtensionRegistry().treeprocessor(processor); + final String input = "= Test\n\n[gremlin-groovy,modern]\n----\ng.V(1)\n----\n"; + final String result = asciidoctor.convert(input, Options.builder().build()); + assertThat(result, containsString("class=\"graph-dataset-note\"")); + assertThat(result, containsString("/reference/#tinkerpop-modern\">modern</a> graph.")); + } + } + + @Test + public void shouldCarryForwardGraphLabelForExisting() { + final RecordingExecutor executor = new RecordingExecutor("==>result"); + final GremlinTreeprocessor processor = new GremlinTreeprocessor(executor); + try (final Asciidoctor asciidoctor = Asciidoctor.Factory.create()) { + asciidoctor.unregisterAllExtensions(); + asciidoctor.javaExtensionRegistry().treeprocessor(processor); + final String input = "= Test\n\n" + + "[gremlin-groovy,modern]\n----\ng.V(1)\n----\n\n" + + "[gremlin-groovy,existing]\n----\ng.E()\n----\n"; + final String result = asciidoctor.convert(input, Options.builder().build()); + // Both blocks show the "modern" note; the existing block carries it forward. + final int badgeCount = result.split("/reference/#tinkerpop-modern\">modern</a>", -1).length - 1; + assertThat(badgeCount, is(2)); + } + } + + @Test + public void shouldLabelExistingConsistentlyAcrossCachedSecondPass() { + // Regression: the "existing" label must be resolved from the AST walk, not from + // execution-set state. A second backend pass reuses the shared execution cache and skips + // graph initialization, so a label derived from execution state would go missing on that + // pass. Both passes must produce the same "modern" label for the existing block. + final RecordingExecutor executor = new RecordingExecutor("==>result"); + final GremlinExecutionCache cache = new GremlinExecutionCache(); + final String input = "= Test\n\n" + + "[gremlin-groovy,modern]\n----\ng.V(1)\n----\n\n" + + "[gremlin-groovy,existing]\n----\ng.E()\n----\n"; + try (final Asciidoctor asciidoctor = Asciidoctor.Factory.create()) { + asciidoctor.javaConverterRegistry().register(MarkdownConverter.class); + + asciidoctor.unregisterAllExtensions(); + asciidoctor.javaExtensionRegistry().treeprocessor(new GremlinTreeprocessor(executor, null, cache)); + final String html = asciidoctor.convert(input, Options.builder().backend("html5").build()); + + asciidoctor.unregisterAllExtensions(); + asciidoctor.javaExtensionRegistry().treeprocessor(new GremlinTreeprocessor(executor, null, cache)); + final String markdown = asciidoctor.convert(input, Options.builder().backend("tpmarkdown").build()); + + // HTML pass: two "modern" notes (named + existing carried forward). + final int htmlBadges = html.split("/reference/#tinkerpop-modern\">modern</a>", -1).length - 1; + assertThat(htmlBadges, is(2)); + // Markdown pass (cache reused, no execution): two "modern" dataset labels. + final int mdLabels = markdown.split("\\*graph-dataset: modern\\*", -1).length - 1; + assertThat(mdLabels, is(2)); + } + } + + @Test public void shouldReuseGraphStateForExisting() { final RecordingExecutor executor = new RecordingExecutor("==>result"); final GremlinTreeprocessor processor = new GremlinTreeprocessor(executor);
diff --git a/docs/tinkeradoc-extension/src/test/java/org/apache/tinkerpop/tinkeradoc/MarkdownTabRendererTest.java b/docs/tinkeradoc-extension/src/test/java/org/apache/tinkerpop/tinkeradoc/MarkdownTabRendererTest.java index 34d8e61..39b8ff9 100644 --- a/docs/tinkeradoc-extension/src/test/java/org/apache/tinkerpop/tinkeradoc/MarkdownTabRendererTest.java +++ b/docs/tinkeradoc-extension/src/test/java/org/apache/tinkerpop/tinkeradoc/MarkdownTabRendererTest.java
@@ -110,4 +110,18 @@ NeutralTab.source("groovy", "a() <1>\nb()\nc() <2>"))); assertThat(md, containsString("a() // (1)\nb()\nc() // (2)")); } + + @Test + public void shouldIncludeGraphLabelWhenProvided() { + final String md = renderer.render(Collections.singletonList( + NeutralTab.source("groovy", "g.V()")), "modern"); + assertThat(md, containsString("*graph-dataset: modern*")); + } + + @Test + public void shouldOmitGraphLabelWhenNull() { + final String md = renderer.render(Collections.singletonList( + NeutralTab.source("groovy", "g.V()")), null); + assertThat(md, not(containsString("graph-dataset"))); + } }
diff --git a/docs/tinkeradoc-extension/src/test/java/org/apache/tinkerpop/tinkeradoc/TabbedHtmlBuilderTest.java b/docs/tinkeradoc-extension/src/test/java/org/apache/tinkerpop/tinkeradoc/TabbedHtmlBuilderTest.java index 998c410..edea44a 100644 --- a/docs/tinkeradoc-extension/src/test/java/org/apache/tinkerpop/tinkeradoc/TabbedHtmlBuilderTest.java +++ b/docs/tinkeradoc-extension/src/test/java/org/apache/tinkerpop/tinkeradoc/TabbedHtmlBuilderTest.java
@@ -283,4 +283,31 @@ assertThat(html2, containsString("id=\"tab-2-1\"")); assertThat(html3, containsString("id=\"tab-3-1\"")); } + + @Test + public void shouldIncludeGraphBadgeWhenGraphNameProvided() { + final List<TabbedHtmlBuilder.Tab> tabs = Collections.singletonList( + TabbedHtmlBuilder.codeTab("groovy", "g.V()")); + final String html = builder.build(tabs, "modern"); + assertThat(html, containsString("class=\"graph-dataset-note\"")); + assertThat(html, containsString("This example runs against the")); + assertThat(html, containsString("/reference/#tinkerpop-modern\">modern</a> graph.")); + } + + @Test + public void shouldOmitGraphBadgeWhenGraphNameIsNull() { + final List<TabbedHtmlBuilder.Tab> tabs = Collections.singletonList( + TabbedHtmlBuilder.codeTab("groovy", "g.V()")); + final String html = builder.build(tabs, null); + assertThat(html, not(containsString("graph-dataset-note"))); + } + + @Test + public void shouldEscapeHtmlInGraphBadge() { + final List<TabbedHtmlBuilder.Tab> tabs = Collections.singletonList( + TabbedHtmlBuilder.codeTab("groovy", "g.V()")); + final String html = builder.build(tabs, "<script>alert(1)</script>"); + assertThat(html, containsString("<script>alert(1)</script>")); + assertThat(html, not(containsString("<script>"))); + } }