Skip to content

Commit 569d99d

Browse files
toc: hide and skip option (#1498)
1 parent 8e34693 commit 569d99d

12 files changed

Lines changed: 231 additions & 10 deletions

File tree

znai-core/src/main/java/org/testingisdocumenting/znai/structure/TableOfContents.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,14 +75,14 @@ public void addIndex() {
7575

7676
public TocItem firstNonIndexPage() {
7777
return tocItems.stream()
78-
.filter(tocItem -> !tocItem.isIndex())
78+
.filter(tocItem -> !tocItem.isIndex() && !tocItem.isHidden())
7979
.findFirst()
8080
.orElse(null);
8181
}
8282

8383
public Optional<TocItem> firstPageInChapter(String dirName) {
8484
return tocItems.stream()
85-
.filter(tocItem -> tocItem.getDirName().equals(dirName))
85+
.filter(tocItem -> tocItem.getDirName().equals(dirName) && !tocItem.isHidden())
8686
.findFirst();
8787
}
8888

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/*
2+
* Copyright 2026 znai maintainers
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.testingisdocumenting.znai.structure;
18+
19+
public enum TocBehavior {
20+
DEFAULT,
21+
HIDE,
22+
SKIP;
23+
24+
public static TocBehavior fromString(String value) {
25+
return switch (value.toLowerCase()) {
26+
case "hide" -> HIDE;
27+
case "skip" -> SKIP;
28+
case "default" -> DEFAULT;
29+
default -> throw new IllegalArgumentException(
30+
"unsupported toc value: \"" + value + "\", supported values: \"default\", \"hide\", \"skip\"");
31+
};
32+
}
33+
}

znai-core/src/main/java/org/testingisdocumenting/znai/structure/TocItem.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ public class TocItem {
3333
private final TocNameAndOpts page;
3434
private final String fileNameWithoutExtension;
3535
private final String fileExtension;
36+
private final TocBehavior tocBehavior;
3637
private String pageTitle;
3738
private PageMeta pageMeta;
3839

@@ -55,6 +56,7 @@ public TocItem(TocNameAndOpts chapter, TocNameAndOpts page, String defaultExtens
5556

5657
this.fileNameWithoutExtension = extractName(page.getGivenName());
5758
this.fileExtension = extractExtension(page.getGivenName(), defaultExtension);
59+
this.tocBehavior = page.getTocBehavior();
5860
validateFileName(chapter.getGivenName());
5961
validateFileName(this.fileNameWithoutExtension);
6062

@@ -130,6 +132,18 @@ public void setPageTitleIfNoTocOverridePresent(String pageTitle) {
130132
}
131133
}
132134

135+
public TocBehavior getTocBehavior() {
136+
return tocBehavior;
137+
}
138+
139+
public boolean isHidden() {
140+
return tocBehavior == TocBehavior.HIDE;
141+
}
142+
143+
public boolean isSkip() {
144+
return tocBehavior == TocBehavior.SKIP;
145+
}
146+
133147
public boolean isIndex() {
134148
return chapter.getGivenName().isEmpty() && fileNameWithoutExtension.equals(INDEX);
135149
}
@@ -150,6 +164,10 @@ public boolean match(String dirName, String fileNameWithoutExtension) {
150164
result.put("pageSectionIdTitles",
151165
getPageSectionIdTitles().stream().map(PageSectionIdTitle::toMap).collect(toList()));
152166

167+
if (tocBehavior != TocBehavior.DEFAULT) {
168+
result.put("toc", tocBehavior.name().toLowerCase());
169+
}
170+
153171
return result;
154172
}
155173

znai-core/src/main/java/org/testingisdocumenting/znai/structure/TocNameAndOpts.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,13 @@
2929
public class TocNameAndOpts {
3030
private final String TITLE_KEY = "title";
3131
private final String PATH_KEY = "path";
32+
private final String TOC_BEHAVIOR_KEY = "toc";
3233

3334
private final String givenName;
3435
private final Map<String, ?> opts;
3536
private final boolean hasTitleOverride;
3637
private final boolean hasPath;
38+
private final TocBehavior tocBehavior;
3739

3840
private String humanReadableName;
3941

@@ -45,6 +47,7 @@ public TocNameAndOpts(String trimmed) {
4547
this.opts = hasOpenBracket ? extractOpts(trimmed, openBracketIdx) : Collections.emptyMap();
4648
this.hasTitleOverride = opts.containsKey(TITLE_KEY);
4749
this.hasPath = opts.containsKey(PATH_KEY);
50+
this.tocBehavior = buildTocBehavior();
4851

4952
this.humanReadableName = buildHumanReadableName();
5053
}
@@ -77,6 +80,10 @@ public String getPath() {
7780
return opts.get(PATH_KEY).toString();
7881
}
7982

83+
public TocBehavior getTocBehavior() {
84+
return tocBehavior;
85+
}
86+
8087
private Map<String, ?> extractOpts(String trimmed, int openBracketIdx) {
8188
int closeBracketIdx = trimmed.indexOf('}');
8289
if (closeBracketIdx == -1) {
@@ -86,6 +93,15 @@ public String getPath() {
8693
return JsonUtils.deserializeAsMap(trimmed.substring(openBracketIdx, closeBracketIdx + 1));
8794
}
8895

96+
private TocBehavior buildTocBehavior() {
97+
Object behavior = opts.get(TOC_BEHAVIOR_KEY);
98+
if (behavior == null) {
99+
return TocBehavior.DEFAULT;
100+
}
101+
102+
return TocBehavior.fromString(behavior.toString());
103+
}
104+
89105
private String buildHumanReadableName() {
90106
Object title = opts.get(TITLE_KEY);
91107
if (title != null) {

znai-core/src/test/groovy/org/testingisdocumenting/znai/structure/PlainTextTocGeneratorTest.groovy

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,13 +189,44 @@ api-reference.md {title: "API Docs"}""")
189189
api.getPageTitle().should == "API Docs"
190190
}
191191

192+
@Test
193+
void "should support hide and skip toc property"() {
194+
def toc = new PlainTextTocGenerator("md").generate("""
195+
chapter1
196+
page-a
197+
page-b {toc: "hide"}
198+
page-c {toc: "skip"}""")
199+
200+
def pageA = toc.findTocItem("chapter1", "page-a")
201+
pageA.isHidden().should == false
202+
pageA.getTocBehavior().should == TocBehavior.DEFAULT
203+
204+
def pageB = toc.findTocItem("chapter1", "page-b")
205+
pageB.isHidden().should == true
206+
pageB.getTocBehavior().should == TocBehavior.HIDE
207+
208+
def pageC = toc.findTocItem("chapter1", "page-c")
209+
pageC.isSkip().should == true
210+
pageC.getTocBehavior().should == TocBehavior.SKIP
211+
}
212+
213+
@Test
214+
void "should throw error for unsupported toc value"() {
215+
code {
216+
new PlainTextTocGenerator("md").generate("""
217+
chapter1
218+
page-a {toc: "unknown"}""")
219+
} should throwException(IllegalArgumentException,
220+
~/unsupported toc value: "unknown"/)
221+
}
222+
192223
@Test
193224
void "should throw error for indented page without chapter"() {
194225
code {
195226
new PlainTextTocGenerator("md").generate("""
196227
page-without-chapter
197228
""")
198-
} should throwException(IllegalArgumentException,
229+
} should throwException(IllegalArgumentException,
199230
"chapter is not specified, use a line without indentation to specify a chapter")
200231
}
201232
}

znai-core/src/test/groovy/org/testingisdocumenting/znai/structure/TableOfContentsTest.groovy

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,4 +156,24 @@ class TableOfContentsTest {
156156
toc.findTocItem(pathC).should == [dirName: "chapter2", fileNameWithoutExtension: "page-c"]
157157
toc.findTocItem(pathD).should == null
158158
}
159+
160+
@Test
161+
void "firstNonIndexPage should skip hidden pages"() {
162+
def toc = new TableOfContents("md")
163+
toc.addIndex()
164+
toc.addTocItem(new TocNameAndOpts("chapter1"), new TocNameAndOpts('page-a {toc: "hide"}'))
165+
toc.addTocItem("chapter1", "page-b")
166+
toc.addTocItem("chapter1", "page-c")
167+
168+
toc.firstNonIndexPage().fileNameWithoutExtension.should == "page-b"
169+
}
170+
171+
@Test
172+
void "firstPageInChapter should skip hidden pages"() {
173+
def toc = new TableOfContents("md")
174+
toc.addTocItem(new TocNameAndOpts("chapter1"), new TocNameAndOpts('page-a {toc: "hide"}'))
175+
toc.addTocItem("chapter1", "page-b")
176+
177+
toc.firstPageInChapter("chapter1").get().fileNameWithoutExtension.should == "page-b"
178+
}
159179
}

znai-docs/znai/flow/structure.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,30 @@ optional-chapter
4747
page-four.md
4848
```
4949

50+
# Hide & Skip
51+
52+
You can control how individual pages participate in the documentation by setting `toc` property in the `toc` file.
53+
54+
A hidden page can be navigated to with a direct link, but it won't appear in the TOC panel
55+
and won't participate in next/prev page navigation.
56+
57+
```
58+
chapter-name
59+
page-one
60+
page-two {toc: "hide"}
61+
page-three
62+
```
63+
64+
A skipped page is accessible via direct URL, but it won't participate in next/prev page navigation.
65+
Users navigating sequentially will skip over it.
66+
67+
```
68+
chapter-name
69+
page-one
70+
page-two {toc: "skip"}
71+
page-three
72+
```
73+
5074
# Sub Headings
5175

5276
Only a first level heading is treated as a first class citizen:
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
* Add: `toc` file items support new `toc` property with `skip` and `hide` to control what is displayed in TOC navigation
2+
and what pages are skipped with next/prev page buttons

znai-reactjs/src/layout/TocMenu.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ class Item extends PureComponent {
9797
const Section = ({ section, selected, onTocItemClick, onTocItemPageSectionClick }) => {
9898
const className = "toc-section" + (section.dirName === selected.dirName ? " selected" : "");
9999

100-
const items = section.items.filter((item) => !isTocItemIndex(item));
100+
const items = section.items.filter((item) => !isTocItemIndex(item) && item.toc !== "hide");
101101
if (items.length === 0) {
102102
return null;
103103
}

znai-reactjs/src/structure/TocItem.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,15 @@
1515
* limitations under the License.
1616
*/
1717

18+
export type TocBehavior = "default" | "hide" | "skip";
19+
1820
export interface TocItem {
1921
dirName: string;
2022
fileName: string;
2123
chapterTitle?: string;
2224
pageTitle?: string;
2325
pageMeta?: object;
26+
toc?: TocBehavior;
2427
anchorId?: string;
2528
fileExtension?: string;
2629
items?: TocItem[];

0 commit comments

Comments
 (0)