Fixes Fix rendering to Java streams
MODIFY
.gitignore
.gitignore
@@ -9,3 +9,6 @@ target/
*.ipr
*.iws
.idea/
+
+# Strange directories that IntelliJ seems to create now
+out/

MODIFY
asciidoctorj-core/src/main/java/org/asciidoctor/converter/ConverterProxy.java
asciidoctorj-core/src/main/java/org/asciidoctor/converter/ConverterProxy.java
@@ -4,6 +4,7 @@ import org.asciidoctor.ast.ContentNode;
import org.asciidoctor.ast.NodeConverter;
import org.asciidoctor.internal.RubyHashMapDecorator;
import org.asciidoctor.internal.RubyHashUtil;
+import org.asciidoctor.internal.RubyOutputStreamWrapper;
import org.jruby.Ruby;
import org.jruby.RubyClass;
import org.jruby.RubyHash;
@@ -162,6 +163,8 @@ public class ConverterProxy<T> extends RubyObject {
} else if (target instanceof RubyString) {
File f = new File(((RubyString) target).asJavaString());
out = new FileOutputStream(f);
+ } else if (target instanceof RubyOutputStreamWrapper) {
+ out = ((RubyOutputStreamWrapper)target).getOut();
} else {
throw new IllegalArgumentException("Can't write to a " + target);
}

MODIFY
asciidoctorj-core/src/main/java/org/asciidoctor/internal/JRubyAsciidoctor.java
asciidoctorj-core/src/main/java/org/asciidoctor/internal/JRubyAsciidoctor.java
@@ -30,6 +30,7 @@ import org.jruby.runtime.builtin.IRubyObject;
import java.io.File;
import java.io.IOException;
+import java.io.OutputStream;
import java.io.Reader;
import java.io.Writer;
import java.util.ArrayList;
@@ -449,6 +450,11 @@ public class JRubyAsciidoctor implements Asciidoctor {
rubyRuntime.setCurrentDirectory((String) options.get(Options.BASEDIR));
}
+ final Object toFileOption = options.get(Options.TO_FILE);
+ if (toFileOption instanceof OutputStream) {
+ options.put(Options.TO_FILE, RubyOutputStreamWrapper.wrap(getRubyRuntime(), (OutputStream) toFileOption));
+ }
+
RubyHash rubyHash = RubyHashUtil.convertMapToRubyHashWithSymbols(rubyRuntime, options);
try {

ADD
asciidoctorj-core/src/main/java/org/asciidoctor/internal/RubyOutputStreamWrapper.java
+package org.asciidoctor.internal;
+
+import org.jruby.Ruby;
+import org.jruby.RubyClass;
+import org.jruby.RubyKernel;
+import org.jruby.RubyModule;
+import org.jruby.RubyNumeric;
+import org.jruby.RubyObject;
+import org.jruby.RubyString;
+import org.jruby.anno.JRubyMethod;
+import org.jruby.runtime.ObjectAllocator;
+import org.jruby.runtime.ThreadContext;
+import org.jruby.runtime.builtin.IRubyObject;
+
+import java.io.IOException;
+import java.io.OutputStream;
+
+public class RubyOutputStreamWrapper extends RubyObject {
+
+ public static final String RUBY_CLASS_NAME = "OutputStreamWrapper";
+
+ private OutputStream out;
+
+ private int bytesWritten = 0;
+
+ public static IRubyObject wrap(final Ruby rubyRuntime, final OutputStream out) {
+
+ final RubyClass rubyClass = getOrCreateOutputStreamWrapperClass(rubyRuntime);
+
+ final IRubyObject wrapper = rubyClass.allocate();
+
+ ((RubyOutputStreamWrapper) wrapper).setOut(out);
+
+ return wrapper;
+ }
+
+ public RubyOutputStreamWrapper(final Ruby rubyRuntime, final RubyClass rubyClass) {
+ super(rubyRuntime, rubyClass);
+ }
+
+ public void setOut(OutputStream out) {
+ this.out = out;
+ }
+
+ public OutputStream getOut() {
+ return out;
+ }
+
+ public static RubyClass getOrCreateOutputStreamWrapperClass(final Ruby rubyRuntime) {
+ RubyModule asciidoctorModule = rubyRuntime.getModule("AsciidoctorJ");
+ RubyClass outputStreamWrapperClass = asciidoctorModule.getClass(RUBY_CLASS_NAME);
+ if (outputStreamWrapperClass != null) {
+ return outputStreamWrapperClass;
+ }
+
+ final RubyClass rubyClass = asciidoctorModule.defineClassUnder(RUBY_CLASS_NAME, rubyRuntime.getObject(), new ObjectAllocator() {
+ @Override
+ public IRubyObject allocate(final Ruby runtime, final RubyClass klazz) {
+ return new RubyOutputStreamWrapper(runtime, klazz);
+ }
+ });
+
+ rubyClass.defineAnnotatedMethods(RubyOutputStreamWrapper.class);
+
+ return rubyClass;
+ }
+
+ @JRubyMethod(name = "write", required = 1)
+ public IRubyObject write(ThreadContext context, IRubyObject arg) throws IOException {
+ writeToStream(arg);
+ return context.getRuntime().getNil();
+ }
+
+ @JRubyMethod(name = "<<", required = 1)
+ public IRubyObject append(ThreadContext context, IRubyObject arg) throws IOException {
+ writeToStream(arg);
+ return this;
+ }
+
+ @JRubyMethod(name = "printf", required = 1, rest = true)
+ public IRubyObject printf(ThreadContext context, IRubyObject[] args) throws IOException {
+ writeToStream(RubyKernel.sprintf(context, null, args));
+ return context.getRuntime().getNil();
+ }
+
+ @JRubyMethod(name = "size")
+ public IRubyObject size(ThreadContext context) throws IOException {
+ return context.getRuntime().newFixnum(bytesWritten);
+ }
+
+ private void writeToStream(IRubyObject arg) throws IOException {
+ final byte[] bytes = convertToBytes(arg);
+ out.write(bytes);
+ bytesWritten += bytes.length;
+ }
+
+ private byte[] convertToBytes(IRubyObject arg) {
+ if (arg instanceof RubyString) {
+ return ((RubyString) arg).getBytes();
+ } else if (arg instanceof RubyNumeric) {
+ return arg.asString().getBytes();
+ } else {
+ throw new IllegalArgumentException("Don't know how to write a " + arg + " " + arg.getClass());
+ }
+ }
+
+
+}

ADD
asciidoctorj-core/src/test/groovy/org/asciidoctor/WhenADocumentIsRenderedToStream.groovy
+package org.asciidoctor
+
+import org.asciidoctor.ast.Block
+import org.asciidoctor.ast.ContentNode
+import org.asciidoctor.ast.Document
+import org.asciidoctor.ast.Section
+import org.asciidoctor.converter.StringConverter
+import org.jboss.arquillian.spock.ArquillianSputnik
+import org.jboss.arquillian.test.api.ArquillianResource
+import org.junit.runner.RunWith
+import spock.lang.Specification
+
+@RunWith(ArquillianSputnik)
+class WhenADocumentIsRenderedToStream extends Specification {
+
+ public static final String HTML5_BACKEND = 'html5'
+ public static final String HELLO_WORLD = 'Hello World'
+ public static final String EMPTY_STRING = ''
+
+ @ArquillianResource
+ private Asciidoctor asciidoctor
+
+ static final String TEST_DOC = '''= Test
+
+A test document
+
+== A Section
+
+Hello World
+'''
+
+ def 'should render HTML to ByteArrayOutputStream'() throws Exception {
+
+ given:
+ def out = new ByteArrayOutputStream()
+
+ when:
+ asciidoctor.convert(TEST_DOC,
+ OptionsBuilder.options()
+ .backend(HTML5_BACKEND)
+ .toStream(out))
+
+ String result = new String(out.toByteArray())
+
+ then:
+ result != null && result != EMPTY_STRING
+ result.contains(HELLO_WORLD)
+ }
+
+ def 'should render twice HTML to ByteArrayOutputStream'() throws Exception {
+
+ given:
+ def out1 = new ByteArrayOutputStream()
+ def out2 = new ByteArrayOutputStream()
+
+ when:
+ asciidoctor.convert(TEST_DOC,
+ OptionsBuilder.options()
+ .backend(HTML5_BACKEND)
+ .toStream(out1))
+ asciidoctor.convert(TEST_DOC,
+ OptionsBuilder.options()
+ .backend(HTML5_BACKEND)
+ .toStream(out2))
+
+ String result1 = new String(out1.toByteArray())
+ String result2 = new String(out2.toByteArray())
+
+ then:
+ result1 != null && result1 != EMPTY_STRING && result1.contains(HELLO_WORLD)
+ result2 != null && result2 != EMPTY_STRING && result2.contains(HELLO_WORLD)
+ }
+
+ def 'should render with custom Java converter to ByteArrayOutputStream'() throws Exception {
+
+ given:
+ def out = new ByteArrayOutputStream()
+ asciidoctor.javaConverterRegistry().register(TestConverter, TestConverter.TEST_BACKEND_NAME)
+ def testdoc = '''== Test
+
+Hello, World!
+'''
+
+ when:
+ asciidoctor.convert(testdoc,
+ OptionsBuilder.options()
+ .backend(TestConverter.TEST_BACKEND_NAME)
+ .toStream(out))
+
+ String result = new String(out.toByteArray())
+
+ then:
+ result != null && result != EMPTY_STRING
+ result.contains('-- Test --')
+ result.contains('Hello, World!')
+ }
+
+
+ static class TestConverter extends StringConverter {
+
+ public static final String TEST_BACKEND_NAME = 'test'
+
+ private static final String NEWLINE = '\n'
+ private static final String NEWLINE_2 = NEWLINE * 2
+
+ TestConverter(String backend, Map<String, Object> opts) {
+ super(backend, opts)
+ }
+ @Override
+ String convert(ContentNode node, String transform, Map<Object, Object> opts) {
+ if (transform == null) {
+ transform = node.nodeName
+ }
+ this."convert${transform.capitalize()}"(node)
+ }
+
+ Object convertEmbedded(Document node) {
+ String result = node.doctitle.toUpperCase()
+ String content = node.content
+ if (content && content.trim().length() > 0) {
+ result += NEWLINE_2 + content
+ }
+ result
+ }
+
+ Object convertSection(Section section) {
+ String result = "-- ${section.title} --"
+ String content = section.content
+ if (content && content.trim().length() > 0) {
+ result += NEWLINE_2 + content
+ }
+ result
+ }
+
+ Object convertParagraph(Block block) {
+ block.getLines().join(NEWLINE)
+ }
+
+ }
+
+}

ADD
asciidoctorj-distribution/src/test/groovy/org/asciidoctor/diagram/WhenAPdfDocumentIsRenderedToStream.groovy
+package org.asciidoctor.diagram
+
+import org.asciidoctor.Asciidoctor
+import org.asciidoctor.AttributesBuilder
+import org.asciidoctor.OptionsBuilder
+import org.asciidoctor.SafeMode
+import org.jboss.arquillian.spock.ArquillianSputnik
+import org.jboss.arquillian.test.api.ArquillianResource
+import org.junit.rules.TemporaryFolder
+import org.junit.runner.RunWith
+import spock.lang.Specification
+
+import java.text.SimpleDateFormat
+
+@RunWith(ArquillianSputnik)
+class WhenAPdfDocumentIsRenderedToStream extends Specification {
+
+ public static final String BACKEND_PDF = 'pdf'
+ @ArquillianResource
+ private Asciidoctor asciidoctor
+
+ @ArquillianResource
+ public TemporaryFolder testFolder
+
+ static final String ASCIIDOCTOR_DIAGRAM = 'asciidoctor-diagram'
+
+
+ def 'should render PDF to ByteArrayOutputStream'() throws Exception {
+
+ given:
+ File referenceFile = new File('build/stream-test-file.pdf')
+ File streamFile = new File('build/stream-test-stream.pdf')
+
+ String imageFileName = UUID.randomUUID()
+
+ String testDoc = """= Test
+
+A test document
+
+== A Section
+
+Hello World
+
+[ditaa,${imageFileName}]
+....
+
++---+
+| A |
++---+
+....
+
+== Another Section
+
+Some other test
+
+a
+
+<<<
+
+b
+
+c
+
+"""
+
+ asciidoctor.requireLibrary(ASCIIDOCTOR_DIAGRAM)
+
+ def out = new ByteArrayOutputStream()
+
+ def dateTimeFormatter = new SimpleDateFormat('yyyy-MM-dd HH:mm:ss z')
+
+ def now = new Date()
+
+ def attrs = AttributesBuilder.attributes()
+ .attribute('docdatetime', dateTimeFormatter.format(now))
+ .attribute('localdatetime', dateTimeFormatter.format(now))
+
+ when:
+ asciidoctor.convert(testDoc,
+ OptionsBuilder.options()
+ .backend(BACKEND_PDF)
+ .headerFooter(true)
+ .attributes(attrs)
+ .safe(SafeMode.UNSAFE)
+ .toStream(out))
+
+ streamFile.bytes = out.toByteArray()
+ def toStreamBytes = out.toByteArray()
+
+ asciidoctor.convert(testDoc,
+ OptionsBuilder.options()
+ .backend(BACKEND_PDF)
+ .headerFooter(true)
+ .attributes(attrs)
+ .safe(SafeMode.UNSAFE)
+ .toFile(referenceFile))
+
+ def toFileBytes = referenceFile.bytes
+
+ then:
+ toStreamBytes.length == toFileBytes.length
+ Arrays.equals(toStreamBytes, toFileBytes)
+ }
+}

Fixes #588. Fix rendering to Java streams