AST now provides access to document catalog footnotes
MODIFY
CHANGELOG.adoc
CHANGELOG.adoc
@@ -13,6 +13,10 @@ For a detailed view of what has changed, refer to the {uri-repo}/commits/master[
== Unreleased
+Improvement::
+
+* AST now provides access to footnotes (@lread) (TBD)
+
== 2.4.1 (2020-09-10)
Build::

ADD
asciidoctorj-api/src/main/java/org/asciidoctor/ast/Catalog.java
+package org.asciidoctor.ast;
+
+import java.util.List;
+
+/**
+ * The asciidoctor catalog contains data collected by asciidoctor that is useful to a converter.
+ * The catalog is considered an internal structure by asciidoctor, and subject to change.
+ */
+public interface Catalog {
+
+ /**
+ * Note that asciidoctor does not populate footnotes until after the
+ * Document's content has been converted.
+ *
+ * @return footnotes occurring in document.
+ */
+ List<Footnote> getFootnotes();
+}
\ No newline at end of file

MODIFY
asciidoctorj-api/src/main/java/org/asciidoctor/ast/Document.java
asciidoctorj-api/src/main/java/org/asciidoctor/ast/Document.java
@@ -75,4 +75,7 @@ public interface Document extends StructuralNode {
* @param state The state in which to put the sourcemap (true = on, false = off).
*/
void setSourcemap(boolean state);
+
+
+ Catalog getCatalog();
}

ADD
asciidoctorj-api/src/main/java/org/asciidoctor/ast/Footnote.java
+package org.asciidoctor.ast;
+
+public interface Footnote {
+
+ Long getIndex();
+
+ String getId();
+
+ String getText();
+}

ADD
asciidoctorj-core/src/main/java/org/asciidoctor/jruby/ast/impl/CatalogImpl.java
+package org.asciidoctor.jruby.ast.impl;
+
+import org.asciidoctor.ast.Catalog;
+import org.asciidoctor.ast.Footnote;
+import org.jruby.RubyArray;
+import org.jruby.RubyStruct;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+public class CatalogImpl implements Catalog {
+
+ private final Map<String, Object> catalog;
+
+ public CatalogImpl(Map<String, Object> catalog) {
+ this.catalog = catalog;
+ }
+
+ @Override
+ public List<Footnote> getFootnotes() {
+ RubyArray<?> rubyFootnotes = (RubyArray<?>) catalog.get("footnotes");
+ List<Footnote> footnotes = new ArrayList<>();
+ for (Object f : rubyFootnotes) {
+ footnotes.add(FootnoteImpl.getInstance((RubyStruct) f));
+ }
+ return footnotes;
+ }
+}

MODIFY
asciidoctorj-core/src/main/java/org/asciidoctor/jruby/ast/impl/DocumentImpl.java
asciidoctorj-core/src/main/java/org/asciidoctor/jruby/ast/impl/DocumentImpl.java
@@ -2,8 +2,10 @@ package org.asciidoctor.jruby.ast.impl;
import java.util.Map;
+import org.asciidoctor.ast.Catalog;
import org.asciidoctor.ast.Document;
import org.asciidoctor.ast.Title;
+import org.asciidoctor.jruby.internal.RubyHashMapDecorator;
import org.asciidoctor.jruby.internal.RubyHashUtil;
import org.jruby.Ruby;
import org.jruby.RubyBoolean;
@@ -76,4 +78,9 @@ public class DocumentImpl extends StructuralNodeImpl implements Document {
public void setSourcemap(boolean state) {
setBoolean("sourcemap", state);
}
+
+ @Override
+ public Catalog getCatalog() {
+ return new CatalogImpl(new RubyHashMapDecorator((RubyHash) getProperty("catalog")));
+ }
}

ADD
asciidoctorj-core/src/main/java/org/asciidoctor/jruby/ast/impl/FootnoteImpl.java
+package org.asciidoctor.jruby.ast.impl;
+
+import org.jruby.RubyStruct;
+
+import org.asciidoctor.ast.Footnote;
+import org.jruby.javasupport.JavaEmbedUtils;
+
+public class FootnoteImpl implements Footnote {
+
+ private static final String INDEX_KEY_NAME = "index";
+ private static final String ID_KEY_NAME = "id";
+ private static final String TEXT_KEY_NAME = "text";
+
+ private Long index;
+ private String id;
+ private String text;
+
+ private static Object aref(RubyStruct s, String key) {
+ return JavaEmbedUtils.rubyToJava(s.aref(JavaEmbedUtils.javaToRuby(s.getRuntime(), key)));
+ }
+
+ public static Footnote getInstance(Long index, String id, String text) {
+ FootnoteImpl footnote = new FootnoteImpl();
+ footnote.index = index;
+ footnote.id = id;
+ footnote.text = text;
+ return footnote;
+ }
+
+ public static Footnote getInstance(RubyStruct rubyFootnote) {
+ return FootnoteImpl.getInstance(
+ (Long) aref(rubyFootnote, INDEX_KEY_NAME),
+ (String) aref(rubyFootnote, ID_KEY_NAME),
+ (String) aref(rubyFootnote, TEXT_KEY_NAME));
+ }
+
+ @Override
+ public Long getIndex() { return index; }
+
+ @Override
+ public String getId() { return id; }
+
+ @Override
+ public String getText() { return text; }
+}

ADD
asciidoctorj-core/src/test/groovy/org/asciidoctor/converter/WhenFootnotesAreUsed.groovy
+package org.asciidoctor.converter
+
+import org.asciidoctor.Asciidoctor
+import org.asciidoctor.OptionsBuilder
+import org.asciidoctor.ast.ContentNode
+import org.asciidoctor.ast.Document
+import org.asciidoctor.ast.Footnote
+import org.asciidoctor.ast.StructuralNode
+import org.asciidoctor.jruby.ast.impl.FootnoteImpl
+import org.jboss.arquillian.spock.ArquillianSputnik
+import org.jboss.arquillian.test.api.ArquillianResource
+import org.junit.runner.RunWith
+import spock.lang.Specification
+
+import static org.hamcrest.Matchers.contains
+import static org.hamcrest.Matchers.empty
+import static org.hamcrest.Matchers.samePropertyValuesAs
+import static org.junit.Assert.assertThat
+
+/**
+ * Tests that footnotes can be accessed from converter.
+ *
+ * Note that it is a current limitation of asciidoctor that footnotes are not available until
+ * after they have been converted for the document.
+ */
+@RunWith(ArquillianSputnik)
+class WhenFootnotesAreUsed extends Specification {
+
+ @ArquillianResource
+ private Asciidoctor asciidoctor
+ private static List<Footnote> footnotesBeforeConvert
+ private static List<Footnote> footnotesAfterConvert
+
+ static class Converter extends StringConverter {
+ Converter(String backend, Map<String, Object> opts) {
+ super(backend, opts)
+ }
+
+ /*
+ * For this conversion test we do not care about the conversion result,
+ * we simply want to force the conversion to verify that footnotes
+ * are populated.
+ */
+ @Override
+ String convert(ContentNode node, String transform, Map<Object, Object> opts) {
+ if (node instanceof Document) {
+ def doc = (Document) node
+ footnotesBeforeConvert = doc.catalog.footnotes.collect()
+ doc.content
+ footnotesAfterConvert = doc.catalog.footnotes.collect()
+ }
+ else if (node instanceof StructuralNode) {
+ ((StructuralNode) node).content
+ }
+ }
+ }
+
+ def setup() {
+ footnotesBeforeConvert = null
+ footnotesAfterConvert = null
+ asciidoctor.javaConverterRegistry().register(Converter, 'footnote')
+ }
+
+ def convert(String document) {
+ asciidoctor.convert(document, OptionsBuilder.options().backend("footnote"))
+ }
+
+ def footnote(Long index, String id, String text) {
+ FootnoteImpl.getInstance(index, id, text)
+ }
+
+ def 'when there are no footnotes in source doc, footnotes should be empty'() {
+ given:
+ String document = 'no footnotes here'
+
+ when:
+ convert(document)
+
+ then:
+ assertThat(footnotesBeforeConvert, empty())
+ assertThat(footnotesAfterConvert, empty())
+ }
+
+ def 'when a footnote is is in source doc, it should be accessible from converter'() {
+ given:
+ String document = 'Do footnotes work?footnote:fid[we shall find out!]'
+
+ when:
+ convert(document)
+
+ then:
+ assertThat(footnotesBeforeConvert, empty())
+ assertThat(footnotesAfterConvert,
+ contains(samePropertyValuesAs(footnote(1,"fid","we shall find out!"))))
+ }
+
+ def 'when footnotes are in source doc, they should be accessible from the converter'() {
+ given:
+ String document = '''
+This document has a variety of footnotes.
+
+Ids can be assigned to footnotes.footnote:myid1[first footnote]
+Footnotes can also be id-less.footnote:[second footnote]
+An existing footnote can be referenced.footnote:myid1[]
+'''
+ when:
+ convert(document)
+
+ then:
+ assertThat(footnotesBeforeConvert, empty())
+ assertThat(footnotesAfterConvert,
+ contains(samePropertyValuesAs(footnote(1,"myid1","first footnote")),
+ samePropertyValuesAs(footnote(2, null, "second footnote"))))
+ }
+}

AST now provides access to document catalog footnotes