Skip to content

Commit c93d142

Browse files
authored
[DOXIA-740] IndexingSink: fix incomplete dispatching of Sink events (#228)
IndexingSink was always expecting a sectionTitle below each section which doesn't hold. Also close bufferingSink when the section is ended or a new one started (without a preceding sectionTitle) Skip irrelevant index entries (non section type or invalid id) in TOC
1 parent b64430d commit c93d142

8 files changed

Lines changed: 195 additions & 22 deletions

File tree

doxia-core/src/main/java/org/apache/maven/doxia/index/IndexEntry.java

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,11 @@ static Type fromSectionLevel(int level) {
9595
.findAny()
9696
.orElseThrow(() -> new IllegalStateException("Could not find enum for sectionLevel " + level));
9797
}
98-
};
98+
99+
public boolean isSection() {
100+
return sectionLevel >= 1;
101+
}
102+
}
99103

100104
/**
101105
* The type of the entry, one of the types defined by {@link IndexingSink}
@@ -156,6 +160,14 @@ public String getId() {
156160
return id;
157161
}
158162

163+
/**
164+
* Returns if the entry has an id.
165+
* @return {@code true} if the entry has a valid id, otherwise it can be considered invalid/empty.
166+
*/
167+
public boolean hasId() {
168+
return id != null;
169+
}
170+
159171
/**
160172
* Set the id.
161173
*
@@ -197,7 +209,7 @@ public boolean hasAnchor() {
197209
/**
198210
* Returns the title.
199211
*
200-
* @return the title.
212+
* @return the title (may be {@code null}).
201213
*/
202214
public String getTitle() {
203215
return title;

doxia-core/src/main/java/org/apache/maven/doxia/index/IndexingSink.java

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,14 @@ public class IndexingSink extends org.apache.maven.doxia.sink.impl.SinkWrapper {
5454

5555
private final IndexEntry rootEntry;
5656

57+
/** Is {@code true} once the sink has been closed. */
5758
private boolean isComplete;
59+
5860
private boolean isTitle;
61+
62+
/** Is {@code true} if the sink is currently populating entry data (i.e. metadata about the current entry is not completely captured yet) */
63+
private boolean hasOpenEntry;
64+
5965
/**
6066
* @deprecated legacy constructor, use {@link #IndexingSink(Sink)} with {@link SinkAdapter} as argument and call {@link #getRootEntry()} to retrieve the index tree afterwards.
6167
*/
@@ -123,12 +129,14 @@ public void title_() {
123129
@Override
124130
public void section(int level, SinkEventAttributes attributes) {
125131
super.section(level, attributes);
132+
indexEntryComplete(); // make sure the previous entry is complete
126133
this.type = IndexEntry.Type.fromSectionLevel(level);
127134
pushNewEntry(type);
128135
}
129136

130137
@Override
131138
public void section_(int level) {
139+
indexEntryComplete(); // make sure the previous entry is complete
132140
pop();
133141
super.section_(level);
134142
}
@@ -150,14 +158,20 @@ public void text(String text, SinkEventAttributes attributes) {
150158
case SECTION_3:
151159
case SECTION_4:
152160
case SECTION_5:
161+
case SECTION_6:
153162
// -----------------------------------------------------------------------
154163
// Sanitize the id. The most important step is to remove any blanks
155164
// -----------------------------------------------------------------------
156165

157166
// append text to current entry
158167
IndexEntry entry = stack.lastElement();
159168

160-
String title = entry.getTitle() + text;
169+
String title = entry.getTitle();
170+
if (title != null) {
171+
title += text;
172+
} else {
173+
title = text;
174+
}
161175
title = title.replaceAll("[\\r\\n]+", "");
162176
entry.setTitle(title);
163177

@@ -220,6 +234,9 @@ String getUniqueId(String id) {
220234
}
221235

222236
void indexEntryComplete() {
237+
if (!hasOpenEntry) {
238+
return;
239+
}
223240
this.type = Type.UNKNOWN;
224241
// remove buffering sink from pipeline
225242
BufferingSink bufferingSink = BufferingSinkProxyFactory.castAsBufferingSink(getWrappedSink());
@@ -229,6 +246,7 @@ void indexEntryComplete() {
229246

230247
// flush the buffer afterwards
231248
bufferingSink.flush();
249+
hasOpenEntry = false;
232250
}
233251

234252
/**
@@ -242,11 +260,11 @@ protected void onIndexEntry(IndexEntry entry) {}
242260
* Creates and pushes a new IndexEntry onto the top of this stack.
243261
*/
244262
private void pushNewEntry(Type type) {
245-
IndexEntry entry = new IndexEntry(peek(), "", type);
246-
entry.setTitle("");
263+
IndexEntry entry = new IndexEntry(peek(), null, type);
247264
stack.push(entry);
248265
// now buffer everything till the next index metadata is complete
249266
setWrappedSink(new BufferingSinkProxyFactory().createWrapper(getWrappedSink()));
267+
hasOpenEntry = true;
250268
}
251269

252270
/**

doxia-core/src/main/java/org/apache/maven/doxia/macro/toc/TocMacro.java

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import org.apache.maven.doxia.parser.ParseException;
3232
import org.apache.maven.doxia.parser.Parser;
3333
import org.apache.maven.doxia.sink.Sink;
34+
import org.apache.maven.doxia.sink.SinkEventAttributes;
3435
import org.apache.maven.doxia.sink.impl.SinkAdapter;
3536
import org.apache.maven.doxia.util.DoxiaUtils;
3637

@@ -85,7 +86,7 @@ public class TocMacro extends AbstractMacro {
8586
private int fromDepth;
8687

8788
/** End depth. */
88-
private int toDepth;
89+
private int toDepth = DEFAULT_DEPTH;
8990

9091
/** The default end depth. */
9192
private static final int DEFAULT_DEPTH = 5;
@@ -112,9 +113,13 @@ public void execute(Sink sink, MacroRequest request) throws MacroExecutionExcept
112113
tocSink.close();
113114
}
114115

115-
IndexEntry index = tocSink.getRootEntry();
116+
writeTocForIndexEntry(sink, getAttributesFromMap(request.getParameters()), tocSink.getRootEntry());
117+
}
118+
119+
void writeTocForIndexEntry(Sink sink, SinkEventAttributes listAttributes, IndexEntry rootEntry) {
120+
IndexEntry index = rootEntry;
116121
if (index.getChildEntries().size() > 0) {
117-
sink.list(getAttributesFromMap(request.getParameters()));
122+
sink.list(listAttributes);
118123

119124
int i = 1;
120125

@@ -131,12 +136,14 @@ public void execute(Sink sink, MacroRequest request) throws MacroExecutionExcept
131136
}
132137

133138
/**
139+
* This recursive method just skips index entries that are not sections (but still evaluates their children).
134140
* @param sink The sink to write to.
135141
* @param sectionIndex The section index.
136142
* @param n The toc depth.
137143
*/
138144
private void writeSubSectionN(Sink sink, IndexEntry sectionIndex, int n) {
139-
if (fromDepth <= n) {
145+
boolean isRelevantIndex = isRelevantIndexEntry(sectionIndex);
146+
if (fromDepth <= n && isRelevantIndex) {
140147
sink.listItem();
141148
sink.link("#" + DoxiaUtils.encodeId(sectionIndex.getId()));
142149
sink.text(sectionIndex.getTitle());
@@ -145,12 +152,12 @@ private void writeSubSectionN(Sink sink, IndexEntry sectionIndex, int n) {
145152

146153
if (toDepth > n) {
147154
if (sectionIndex.getChildEntries().size() > 0) {
148-
if (fromDepth <= n) {
155+
if (fromDepth <= n && isRelevantIndex) {
149156
sink.list();
150157
}
151158

152159
for (IndexEntry subsectionIndex : sectionIndex.getChildEntries()) {
153-
if (n == toDepth - 1) {
160+
if (n == toDepth - 1 && isRelevantIndex) {
154161
sink.listItem();
155162
sink.link("#" + DoxiaUtils.encodeId(subsectionIndex.getId()));
156163
sink.text(subsectionIndex.getTitle());
@@ -161,17 +168,21 @@ private void writeSubSectionN(Sink sink, IndexEntry sectionIndex, int n) {
161168
}
162169
}
163170

164-
if (fromDepth <= n) {
171+
if (fromDepth <= n && isRelevantIndex) {
165172
sink.list_();
166173
}
167174
}
168175
}
169176

170-
if (fromDepth <= n) {
177+
if (fromDepth <= n && isRelevantIndex) {
171178
sink.listItem_();
172179
}
173180
}
174181

182+
static boolean isRelevantIndexEntry(IndexEntry indexEntry) {
183+
return indexEntry.hasId() && indexEntry.getType().isSection();
184+
}
185+
175186
/**
176187
* @param request The MacroRequest.
177188
* @param parameter The parameter.

doxia-core/src/main/java/org/apache/maven/doxia/sink/impl/CreateAnchorsForIndexEntries.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ public CreateAnchorsForIndexEntries(Sink delegate) {
3636

3737
@Override
3838
protected void onIndexEntry(IndexEntry entry) {
39-
if (!entry.hasAnchor()) {
39+
if (!entry.hasAnchor() && entry.hasId()) {
4040
getWrappedSink().anchor(entry.getId());
4141
getWrappedSink().anchor_();
4242
}

doxia-core/src/test/java/org/apache/maven/doxia/index/IndexingSinkTest.java

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,16 +18,71 @@
1818
*/
1919
package org.apache.maven.doxia.index;
2020

21+
import org.apache.maven.doxia.parser.AbstractParserTest;
22+
import org.apache.maven.doxia.sink.impl.SinkEventTestingSink;
2123
import org.junit.jupiter.api.Test;
2224

2325
import static org.junit.jupiter.api.Assertions.assertEquals;
26+
import static org.junit.jupiter.api.Assertions.assertNotNull;
2427

2528
class IndexingSinkTest {
29+
2630
@Test
2731
void testGetUniqueId() {
2832
IndexingSink sink = new IndexingSink(new IndexEntry("root"));
2933
assertEquals("root_1", sink.getUniqueId("root"));
3034
assertEquals("root_2", sink.getUniqueId("root"));
3135
assertEquals("newid", sink.getUniqueId("newid"));
3236
}
37+
38+
@Test
39+
void testIndexingSinkWithComplexSink() {
40+
SinkEventTestingSink resultSink = new SinkEventTestingSink();
41+
IndexingSink sink = new IndexingSink(resultSink);
42+
sink.section1();
43+
sink.sectionTitle1();
44+
sink.text("title1");
45+
sink.sectionTitle1_();
46+
sink.section2();
47+
sink.section3();
48+
sink.sectionTitle3();
49+
sink.text("title3");
50+
sink.sectionTitle3_();
51+
sink.section3_();
52+
sink.section2_();
53+
sink.section1_();
54+
sink.close();
55+
56+
// make sure that all events are emitted downstream
57+
AbstractParserTest.assertSinkEquals(
58+
resultSink.getEventList().iterator(),
59+
"section1",
60+
"sectionTitle1",
61+
"text",
62+
"sectionTitle1_",
63+
"section2",
64+
"section3",
65+
"sectionTitle3",
66+
"text",
67+
"sectionTitle3_",
68+
"section3_",
69+
"section2_",
70+
"section1_",
71+
"close");
72+
73+
// evaluate captured index data
74+
IndexEntry entry = sink.getRootEntry();
75+
assertIndexEntry("index", null, 1, entry);
76+
assertIndexEntry("title1", "title1", 1, entry.getFirstEntry());
77+
assertIndexEntry(null, null, 1, entry.getFirstEntry().getFirstEntry());
78+
assertIndexEntry(
79+
"title3", "title3", 0, entry.getFirstEntry().getFirstEntry().getFirstEntry());
80+
}
81+
82+
private void assertIndexEntry(String id, String title, int numChildren, IndexEntry entry) {
83+
assertNotNull(entry);
84+
assertEquals(id, entry.getId());
85+
assertEquals(title, entry.getTitle());
86+
assertEquals(numChildren, entry.getChildEntries().size());
87+
}
3388
}

doxia-core/src/test/java/org/apache/maven/doxia/macro/toc/TocMacroTest.java

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424
import java.util.Iterator;
2525
import java.util.Map;
2626

27+
import org.apache.maven.doxia.index.IndexEntry;
28+
import org.apache.maven.doxia.index.IndexEntry.Type;
2729
import org.apache.maven.doxia.macro.MacroExecutionException;
2830
import org.apache.maven.doxia.macro.MacroRequest;
2931
import org.apache.maven.doxia.markup.Markup;
@@ -199,4 +201,39 @@ public void testGenerateAnchors() throws ParseException, MacroExecutionException
199201
"<section><a id=\"" + actualLinkTarget.substring(1) + "\"></a>" + Markup.EOL + "<h1>1 Headline</h1>",
200202
out.toString());
201203
}
204+
205+
@Test
206+
void testWriteTocWithEmptyAndNotApplicableIndexEntries() {
207+
TocMacro macro = new TocMacro();
208+
SinkEventTestingSink sink = new SinkEventTestingSink();
209+
final SinkEventAttributeSet atts = new SinkEventAttributeSet();
210+
IndexEntry rootEntry = new IndexEntry("root");
211+
new IndexEntry(rootEntry, null, Type.SECTION_1);
212+
// toc item on level 1
213+
IndexEntry entry = new IndexEntry(rootEntry, "id2", Type.SECTION_1);
214+
entry.setTitle("title 1");
215+
// toc item on level 2 (below toc item 1)
216+
new IndexEntry(entry, "id2-1", Type.SECTION_2).setTitle("title 1 - subtitle1");
217+
// item not relevant for toc (should be skipped)
218+
entry = new IndexEntry(rootEntry, "id3", Type.FIGURE);
219+
// toc item below skipped item
220+
new IndexEntry(entry, "id3-1", Type.SECTION_1).setTitle("title 4");
221+
macro.writeTocForIndexEntry(sink, atts, rootEntry);
222+
223+
Iterator<SinkEventElement> it = sink.getEventList().iterator();
224+
AbstractParserTest.assertSinkStartsWith(it, "list");
225+
assertListItem(it, "#id2", "title 1");
226+
AbstractParserTest.assertSinkStartsWith(it, "list");
227+
assertListItem(it, "#id2-1", "title 1 - subtitle1");
228+
AbstractParserTest.assertSinkStartsWith(it, "listItem_", "list_", "listItem_");
229+
assertListItem(it, "#id3-1", "title 4");
230+
AbstractParserTest.assertSinkEquals(it, "listItem_", "list_");
231+
}
232+
233+
void assertListItem(Iterator<SinkEventElement> it, String link, String text) {
234+
AbstractParserTest.assertSinkStartsWith(it, "listItem");
235+
AbstractParserTest.assertSinkEquals(it.next(), "link", link, null);
236+
AbstractParserTest.assertSinkEquals(it.next(), "text", text, null);
237+
AbstractParserTest.assertSinkStartsWith(it, "link_");
238+
}
202239
}

0 commit comments

Comments
 (0)