Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[DOXIA-731] Simplify HTML markup emitted from Sink.verbatim #202

Merged
merged 2 commits into from
Mar 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
import org.apache.maven.doxia.markup.Markup;
import org.apache.maven.doxia.sink.Sink;
import org.apache.maven.doxia.sink.SinkEventAttributes;
import org.apache.maven.doxia.sink.impl.Xhtml5BaseSink.VerbatimMode;
import org.apache.maven.doxia.util.DoxiaUtils;
import org.apache.maven.doxia.util.HtmlTools;
import org.codehaus.plexus.util.xml.PrettyPrintXMLWriter;
Expand Down Expand Up @@ -77,8 +78,16 @@ public class Xhtml5BaseSink extends AbstractXmlSink implements HtmlMarkup {
/** An indication on if we're inside a paragraph flag. */
private boolean paragraphFlag;

/** An indication on if we're in verbatim mode. */
private boolean verbatimFlag;
protected enum VerbatimMode {
/** not in verbatim mode */
OFF,
/** Inside {@code <pre>} */
ON,
/** Inside {@code <pre><code>} */
ON_WITH_CODE
}
/** An indication on if we're in verbatim mode and if so, surrounded by which tags. */
private VerbatimMode verbatimMode;

/** Stack of alignment int[] of table cells. */
private final LinkedList<int[]> cellJustifStack;
Expand Down Expand Up @@ -160,21 +169,28 @@ protected boolean isHeadFlag() {
}

/**
* <p>Setter for the field <code>verbatimFlag</code>.</p>
*
* @param verb a verbatim flag.
* @return the current verbatim mode.
*/
protected VerbatimMode getVerbatimMode() {
return this.verbatimMode;
}

/**
* <p>Setter for the field <code>verbatimMode</code>.</p>
*
* @param mode a verbatim mode.
*/
protected void setVerbatimFlag(boolean verb) {
this.verbatimFlag = verb;
protected void setVerbatimMode(VerbatimMode mode) {
this.verbatimMode = mode;
}

/**
* <p>isVerbatimFlag.</p>
*
* @return the current verbatim flag.
* @return {@code true} if inside verbatim section, {@code false} otherwise
*/
protected boolean isVerbatimFlag() {
return this.verbatimFlag;
protected boolean isVerbatim() {
return this.verbatimMode != VerbatimMode.OFF;
}

/**
Expand Down Expand Up @@ -232,7 +248,7 @@ protected void init() {

this.headFlag = false;
this.paragraphFlag = false;
this.verbatimFlag = false;
this.verbatimMode = VerbatimMode.OFF;

this.evenTableRow = true;
this.tableAttributes = null;
Expand Down Expand Up @@ -825,11 +841,13 @@ public void division_() {
}

/**
* The default class style is <code>verbatim</code>, for source is {@code verbatim source}.
* Depending on whether the decoration attribute is "source" or not, this leads
* to either emitting {@code <pre><code>} or just {@code <pre>}.
* No default classes are emitted but the given attributes are always added to the {@code pre} element only.
*
* {@inheritDoc}
* @see javax.swing.text.html.HTML.Tag#DIV
* @see javax.swing.text.html.HTML.Tag#PRE
* @see javax.swing.text.html.HTML.Tag#CODE
*/
@Override
public void verbatim(SinkEventAttributes attributes) {
Expand All @@ -840,47 +858,41 @@ public void verbatim(SinkEventAttributes attributes) {
paragraph_();
}

verbatimFlag = true;

MutableAttributeSet atts = SinkUtils.filterAttributes(attributes, SinkUtils.SINK_VERBATIM_ATTRIBUTES);

if (atts == null) {
atts = new SinkEventAttributeSet();
}

boolean source = false;

verbatimMode = VerbatimMode.ON;
if (atts.isDefined(SinkEventAttributes.DECORATION)) {
source = "source"
.equals(atts.getAttribute(SinkEventAttributes.DECORATION).toString());
}

SinkEventAttributes divAtts = null;
String divClass = "verbatim";

if (source) {
divClass += " source";
if ("source"
.equals(atts.getAttribute(SinkEventAttributes.DECORATION).toString())) {
verbatimMode = VerbatimMode.ON_WITH_CODE;
}
}

divAtts = new SinkEventAttributeSet(SinkEventAttributes.CLASS.toString(), divClass);

atts.removeAttribute(SinkEventAttributes.DECORATION);

writeStartTag(HtmlMarkup.DIV, divAtts);
writeStartTag(HtmlMarkup.PRE, atts);
if (verbatimMode == VerbatimMode.ON_WITH_CODE) {
writeStartTag(HtmlMarkup.CODE);
}
}

/**
* {@inheritDoc}
* @see javax.swing.text.html.HTML.Tag#DIV
* @see javax.swing.text.html.HTML.Tag#CODE
* @see javax.swing.text.html.HTML.Tag#PRE
*/
@Override
public void verbatim_() {
if (verbatimMode == VerbatimMode.ON_WITH_CODE) {
writeEndTag(HtmlMarkup.CODE);
}
writeEndTag(HtmlMarkup.PRE);
writeEndTag(HtmlMarkup.DIV);

verbatimFlag = false;
verbatimMode = VerbatimMode.OFF;
}

/**
Expand Down Expand Up @@ -1369,7 +1381,7 @@ public void monospaced_() {
*/
@Override
public void lineBreak(SinkEventAttributes attributes) {
if (headFlag || isVerbatimFlag()) {
if (headFlag || isVerbatim()) {
getTextBuffer().append(EOL);
} else {
MutableAttributeSet atts = SinkUtils.filterAttributes(attributes, SinkUtils.SINK_BR_ATTRIBUTES);
Expand All @@ -1381,7 +1393,7 @@ public void lineBreak(SinkEventAttributes attributes) {
/** {@inheritDoc} */
@Override
public void lineBreakOpportunity(SinkEventAttributes attributes) {
if (!headFlag && !isVerbatimFlag()) {
if (!headFlag && !isVerbatim()) {
MutableAttributeSet atts = SinkUtils.filterAttributes(attributes, SinkUtils.SINK_BR_ATTRIBUTES);

writeSimpleTag(HtmlMarkup.WBR, atts);
Expand Down Expand Up @@ -1412,7 +1424,7 @@ public void text(String text, SinkEventAttributes attributes) {
}
if (headFlag) {
getTextBuffer().append(text);
} else if (verbatimFlag) {
} else if (isVerbatim()) {
verbatimContent(text);
} else {
content(text);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,20 +23,25 @@
import java.io.StringWriter;
import java.io.Writer;
import java.util.Iterator;
import java.util.ListIterator;

import org.apache.maven.doxia.AbstractModuleTest;
import org.apache.maven.doxia.sink.Sink;
import org.apache.maven.doxia.sink.SinkEventAttributes;
import org.apache.maven.doxia.sink.impl.SinkEventAttributeSet;
import org.apache.maven.doxia.sink.impl.SinkEventElement;
import org.apache.maven.doxia.sink.impl.SinkEventTestingSink;
import org.apache.maven.doxia.sink.impl.SinkWrapper;
import org.apache.maven.doxia.sink.impl.SinkWrapperFactory;
import org.apache.maven.doxia.sink.impl.TextSink;
import org.apache.maven.doxia.sink.impl.WellformednessCheckingSink;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assumptions.assumeTrue;

/**
* Test the parsing of sample input files.
Expand All @@ -46,12 +51,17 @@
* @since 1.0
*/
public abstract class AbstractParserTest extends AbstractModuleTest {
/**
* Example text which usually requires escaping in different markup languages as they have special meaning there
*/
public static final String TEXT_WITH_SPECIAL_CHARS = "<>{}=#*";

/**
* Create a new instance of the parser to test.
*
* @return the parser to test.
*/
protected abstract Parser createParser();
protected abstract AbstractParser createParser();

/**
* Returns the directory where all parser test output will go.
Expand Down Expand Up @@ -146,6 +156,91 @@ public final void testSinkWrapper() throws ParseException, IOException {
}
}

/**
* Override this method if the parser always emits some static prefix for incomplete documents
* and consume the prefix related events from the given {@link Iterator}.
* @param eventIterator the iterator
*/
protected void assertEventPrefix(Iterator<SinkEventElement> eventIterator) {
// do nothing by default, i.e. assume no prefix
}

/**
* Override this method if the parser always emits some static suffix for incomplete documents
* and consume the suffix related events from the given {@link Iterator}.
* @param eventIterator the iterator
*/
protected void assertEventSuffix(Iterator<SinkEventElement> eventIterator) {
assertFalse(eventIterator.hasNext(), "didn't expect any further events but got at least one");
}

/**
* @return markup representing the verbatim text {@value #TEXT_WITH_SPECIAL_CHARS} (needs to be properly escaped).
* {@code null} can be returned to skip the test for a particular parser.
*/
protected abstract String getVerbatimSource();

/**
* Test a verbatim block (no code) given through {@link #getVerbatimSource()}
* @throws ParseException
*/
@Test
public void testVerbatim() throws ParseException {
String source = getVerbatimSource();
assumeTrue(source != null, "parser does not support simple verbatim text");
michael-o marked this conversation as resolved.
Show resolved Hide resolved
AbstractParser parser = createParser();
SinkEventTestingSink sink = new SinkEventTestingSink();

parser.parse(source, sink);
ListIterator<SinkEventElement> it = sink.getEventList().listIterator();
assertEventPrefix(it);
assertEquals("verbatim", it.next().getName());
assertConcatenatedTextEquals(it, TEXT_WITH_SPECIAL_CHARS, true);
assertEquals("verbatim_", it.next().getName());
assertEventSuffix(it);
}

/**
* @return markup representing the verbatim code block {@value #TEXT_WITH_SPECIAL_CHARS} (needs to be properly escaped).
* {@code null} can be returned to skip the test for a particular parser.
*/
protected abstract String getVerbatimCodeSource();

/**
* Test a verbatim code block given through {@link #getVerbatimCodeSource()}
* @throws ParseException
*/
@Test
public void testVerbatimCode() throws ParseException {
String source = getVerbatimCodeSource();
assumeTrue(source != null, "parser does not support verbatim code");
AbstractParser parser = createParser();
SinkEventTestingSink sink = new SinkEventTestingSink();

parser.parse(source, sink);
ListIterator<SinkEventElement> it = sink.getEventList().listIterator();
assertEventPrefix(it);
SinkEventElement verbatimEvt = it.next();
assertEquals("verbatim", verbatimEvt.getName());
SinkEventAttributeSet atts = (SinkEventAttributeSet) verbatimEvt.getArgs()[0];

// either verbatim event has attribute "source" or additional "inline" event
boolean isInlineCode;
if (atts.isEmpty()) {
isInlineCode = true;
assertSinkAttributesEqual(it.next(), "inline", SinkEventAttributeSet.Semantics.CODE);
} else {
isInlineCode = false;
assertEquals(SinkEventAttributeSet.SOURCE, atts);
}
assertConcatenatedTextEquals(it, TEXT_WITH_SPECIAL_CHARS, true);
if (isInlineCode) {
assertEquals("inline_", it.next().getName());
}
assertEquals("verbatim_", it.next().getName());
assertEventSuffix(it);
}

public static void assertSinkEquals(SinkEventElement element, String name, Object... args) {
Assertions.assertEquals(name, element.getName(), "Name of element doesn't match");
Assertions.assertArrayEquals(args, element.getArgs(), "Arguments don't match");
Expand All @@ -157,6 +252,36 @@ public static void assertSinkAttributeEquals(SinkEventElement element, String na
Assertions.assertEquals(value, atts.getAttribute(attr));
}

public static void assertSinkAttributesEqual(
SinkEventElement element, String name, SinkEventAttributes expectedAttributes) {
Assertions.assertEquals(name, element.getName());
SinkEventAttributeSet atts = (SinkEventAttributeSet) element.getArgs()[0];
Assertions.assertEquals(expectedAttributes, atts);
}

/**
* Consumes all consecutive text events from the given {@link ListIterator} and compares the concatenated text with the expected text
* @param it the iterator to traverse, is positioned to the last text event once this method finishes
* @param expectedText the expected text which is compared with the concatenated text of all text events
* @param trimText {@code true} to trim the actual text before comparing with the expected one, otherwise compare without trimming
*/
void assertConcatenatedTextEquals(ListIterator<SinkEventElement> it, String expectedText, boolean trimText) {
StringBuilder builder = new StringBuilder();
while (it.hasNext()) {
SinkEventElement currentEvent = it.next();
if (currentEvent.getName() != "text") {
it.previous();
break;
}
builder.append(currentEvent.getArgs()[0]);
}
String actualValue = builder.toString();
if (trimText) {
actualValue = actualValue.trim();
}
assertEquals(expectedText, actualValue);
}

public static void assertSinkEquals(Iterator<SinkEventElement> it, String... names) {
StringBuilder expected = new StringBuilder();
StringBuilder actual = new StringBuilder();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ public class Xhtml5BaseParserTest extends AbstractParserTest {
private final SinkEventTestingSink sink = new SinkEventTestingSink();

@Override
protected Parser createParser() {
protected AbstractParser createParser() {
parser = new Xhtml5BaseParser();
return parser;
}
Expand Down Expand Up @@ -931,4 +931,14 @@ public void testUnbalancedDefinitionListItem() throws Exception {
"definitionListItem_",
"definitionList_");
}

@Override
protected String getVerbatimSource() {
return "<pre>&lt;&gt;{}=#*</pre>";
}

@Override
protected String getVerbatimCodeSource() {
return "<pre><code>&lt;&gt;{}=#*</code></pre>";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,10 @@ public static void generate(Sink sink) {

generateList(sink);

sink.verbatim(SinkEventAttributeSet.SOURCE);
sink.verbatim();
sink.inline(SinkEventAttributeSet.Semantics.CODE);
sink.text("Verbatim source text not contained in list item 3");
sink.inline_();
sink.verbatim_();

generateNumberedList(sink);
Expand Down Expand Up @@ -258,8 +260,10 @@ public static void generateDefinitionList(Sink sink) {
sink.definedTerm_();
sink.definition();
sink.text("of definition list.");
sink.verbatim(SinkEventAttributeSet.SOURCE);
sink.verbatim();
sink.inline(SinkEventAttributeSet.Semantics.CODE);
sink.text("Verbatim source text" + eol + " in a box ");
sink.inline_();
sink.verbatim_();
sink.definition_();
sink.definitionListItem_();
Expand Down
Loading