Skip to content
Open
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
4 changes: 4 additions & 0 deletions jbake-core/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ dependencies {
implementation "org.jsoup:jsoup:$jsoupVersion"
implementation "org.yaml:snakeyaml:$snakeYamlVersion", optional

testImplementation("org.asciidoctor:asciidoctorj-diagram:$asciidoctorjDiagramVersion") {
exclude group: 'org.asciidoctor', module: 'asciidoctorj'
}

// cli specific dependencies
implementation "org.eclipse.jetty:jetty-server:$jettyServerVersion", optional
implementation "info.picocli:picocli:$picocli", optional
Expand Down
43 changes: 38 additions & 5 deletions jbake-core/src/main/java/org/jbake/parser/AsciidoctorEngine.java
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,8 @@ private Asciidoctor getEngine(Options options) {
}

if (options.map().containsKey(OPT_REQUIRES)) {
String[] requires = String.valueOf(options.map().get(OPT_REQUIRES)).split(",");
if (requires.length != 0) {
for (String require : requires) {
engine.requireLibrary(require);
}
for (String require : parseRequires(options.map().get(OPT_REQUIRES))) {
engine.requireLibrary(require);
}
}

Expand Down Expand Up @@ -195,6 +192,42 @@ private Options getAsciiDocOptionsAndAttributes(ParserContext context) {
return options;
}

/**
* Parses the value of the {@code asciidoctor.option.requires} configuration entry into
* a list of library names suitable for {@link Asciidoctor#requireLibrary(String)}.
*
* <p>Historically this option was documented as a comma-separated {@code String}, but
* {@code DefaultJBakeConfiguration.getAsciidoctorOption()} returns it as a {@code List<String>}.
* The previous implementation used {@code String.valueOf(list).split(",")}, which produced
* values like {@code "[asciidoctor-diagram]"} (with literal brackets) and made the
* JRuby {@code require} fail silently. Both {@code Collection} and comma-separated
* {@code String} forms are accepted here; {@code null} and empty entries are filtered out.
*/
static List<String> parseRequires(Object raw) {
List<String> result = new ArrayList<>();
if (raw == null) {
return result;
}
if (raw instanceof Collection) {
for (Object r : (Collection<?>) raw) {
if (r != null) {
String trimmed = String.valueOf(r).trim();
if (!trimmed.isEmpty()) {
result.add(trimmed);
}
}
}
} else {
for (String r : String.valueOf(raw).split(",")) {
String trimmed = r.trim();
if (!trimmed.isEmpty()) {
result.add(trimmed);
}
}
}
return result;
}

@SuppressWarnings("unchecked")
private List<String> getAsList(Object asciidoctorOption) {
List<String> values = new ArrayList<>();
Expand Down
5 changes: 5 additions & 0 deletions jbake-core/src/main/resources/default.properties
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,11 @@ example.project.thymeleaf=example_project_thymeleaf.zip
example.project.jade=example_project_jade.zip

# default asciidoctor options
# Individual options are set via asciidoctor.option.<name>=<value>. Notably,
# asciidoctor.option.requires accepts a comma-separated list of Ruby libraries
# to load before rendering, e.g. asciidoctor.option.requires=asciidoctor-diagram
# to enable PlantUML/Graphviz/Ditaa diagram blocks (the gem must be on the
# classpath, and external tools like Graphviz must be installed separately).
asciidoctor.option=
# default asciidoctor attributes
asciidoctor.attributes=source-highlighter=prettify
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package org.jbake.app;

import org.jbake.TestUtils;
import org.jbake.app.configuration.ConfigUtil;
import org.jbake.app.configuration.DefaultJBakeConfiguration;
import org.jbake.model.DocumentModel;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;

import java.io.File;
import java.io.PrintWriter;

import static org.assertj.core.api.Assertions.assertThat;

/**
* Integration test verifying that {@code asciidoctor-diagram} is loaded correctly
* when configured via {@code asciidoctor.option.requires} and that a PlantUML
* sequence diagram in an AsciiDoc file is rendered to an image.
*
* <p>This test exercises the full path from configuration through
* {@link AsciidoctorEngine#parseRequires} to Asciidoctor's {@code requireLibrary}
* call. PlantUML sequence diagrams are used because they do not require an
* external Graphviz installation.
*/
public class AsciidocParserDiagramIntegrationTest {

@Rule
public TemporaryFolder folder = new TemporaryFolder();

private DefaultJBakeConfiguration config;
private Parser parser;
private File asciidocWithDiagram;

private final String validHeader = "title=Diagram Test\nstatus=draft\ntype=post\ndate=2024-01-15\n~~~~~~";

@Before
public void setUp() throws Exception {
File rootPath = TestUtils.getTestResourcesAsSourceFolder();
config = (DefaultJBakeConfiguration) new ConfigUtil().loadConfig(rootPath);
config.setProperty("asciidoctor.option.requires", "asciidoctor-diagram");
parser = new Parser(config);

asciidocWithDiagram = folder.newFile("diagram-test.ad");
try (PrintWriter out = new PrintWriter(asciidocWithDiagram)) {
out.println(validHeader);
out.println("= Diagram Test");
out.println("");
out.println("[plantuml,test-diagram,svg]");
out.println("----");
out.println("@startuml");
out.println("Alice -> Bob: Hello");
out.println("Bob --> Alice: Hi there");
out.println("@enduml");
out.println("----");
}
}

@Test
public void parsesAsciidocFileWithPlantUmlDiagram() {
DocumentModel map = parser.processFile(asciidocWithDiagram);

assertThat(map).isNotNull();
assertThat(map.getStatus()).isEqualTo("draft");
assertThat(map.getType()).isEqualTo("post");
assertThat(map.getBody())
.as("rendered body should contain an image reference for the diagram")
.containsPattern("(?i)<img[^>]+test-diagram[^>]*>");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package org.jbake.parser;

import org.junit.Test;

import java.util.Arrays;
import java.util.Collections;
import java.util.List;

import static org.assertj.core.api.Assertions.assertThat;

/**
* Unit tests for {@link AsciidoctorEngine#parseRequires(Object)}.
*
* <p>These tests pin the behaviour of the {@code asciidoctor.option.requires} option
* parsing. The option value is produced by {@code DefaultJBakeConfiguration.getAsciidoctorOption()}
* as a {@code List<String>}, but the option was historically documented as a
* comma-separated {@code String}. A prior regression caused {@code String.valueOf(list)}
* to be passed through unchanged, producing values like {@code "[asciidoctor-diagram]"}
* that the JRuby {@code require} call could not resolve. Both forms must be supported,
* and no parsed entry may ever contain the stray brackets introduced by
* {@code List.toString()}.
*/
public class AsciidoctorEngineTest {

@Test
public void parsesSingleElementListFromConfiguration() {
List<String> result = AsciidoctorEngine.parseRequires(Collections.singletonList("asciidoctor-diagram"));

assertThat(result).containsExactly("asciidoctor-diagram");
}

@Test
public void parsesMultiElementListFromConfiguration() {
List<String> result = AsciidoctorEngine.parseRequires(Arrays.asList("asciidoctor-diagram", "asciidoctor-mathematical"));

assertThat(result).containsExactly("asciidoctor-diagram", "asciidoctor-mathematical");
}

@Test
public void parsesCommaSeparatedString() {
List<String> result = AsciidoctorEngine.parseRequires("asciidoctor-diagram,asciidoctor-mathematical");

assertThat(result).containsExactly("asciidoctor-diagram", "asciidoctor-mathematical");
}

@Test
public void trimsWhitespaceAroundEntries() {
List<String> result = AsciidoctorEngine.parseRequires(" asciidoctor-diagram , asciidoctor-mathematical ");

assertThat(result).containsExactly("asciidoctor-diagram", "asciidoctor-mathematical");
}

@Test
public void skipsEmptyAndNullEntries() {
List<String> result = AsciidoctorEngine.parseRequires(Arrays.asList("asciidoctor-diagram", "", null, " "));

assertThat(result).containsExactly("asciidoctor-diagram");
}

@Test
public void returnsEmptyListForNullInput() {
assertThat(AsciidoctorEngine.parseRequires(null)).isEmpty();
}

@Test
public void returnsEmptyListForEmptyString() {
assertThat(AsciidoctorEngine.parseRequires("")).isEmpty();
}

/**
* Regression guard for the original bug: when the option value is a {@code List},
* no parsed entry may contain the literal {@code [} or {@code ]} characters that
* {@code List.toString()} would introduce. Had the previous implementation been
* covered by this test, the regression would have been caught immediately.
*/
@Test
public void listInputNeverLeaksToStringBrackets() {
List<String> result = AsciidoctorEngine.parseRequires(Collections.singletonList("asciidoctor-diagram"));

assertThat(result).isNotEmpty();
for (String require : result) {
assertThat(require)
.as("parsed require entry must not contain List.toString() brackets")
.doesNotContain("[")
.doesNotContain("]");
}
}
}