diff --git a/.classpath b/.classpath
new file mode 100644
index 00000000..56abbbf9
--- /dev/null
+++ b/.classpath
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/.project b/.project
new file mode 100644
index 00000000..11a9856b
--- /dev/null
+++ b/.project
@@ -0,0 +1,23 @@
+
+
+ dotify.formatter.impl
+
+
+
+
+
+ org.eclipse.jdt.core.javabuilder
+
+
+
+
+ bndtools.core.bndbuilder
+
+
+
+
+
+ org.eclipse.jdt.core.javanature
+ bndtools.core.bndnature
+
+
diff --git a/.settings/org.eclipse.jdt.core.prefs b/.settings/org.eclipse.jdt.core.prefs
new file mode 100644
index 00000000..060c5ee3
--- /dev/null
+++ b/.settings/org.eclipse.jdt.core.prefs
@@ -0,0 +1,11 @@
+eclipse.preferences.version=1
+org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
+org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.5
+org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve
+org.eclipse.jdt.core.compiler.compliance=1.5
+org.eclipse.jdt.core.compiler.debug.lineNumber=generate
+org.eclipse.jdt.core.compiler.debug.localVariable=generate
+org.eclipse.jdt.core.compiler.debug.sourceFile=generate
+org.eclipse.jdt.core.compiler.problem.assertIdentifier=error
+org.eclipse.jdt.core.compiler.problem.enumIdentifier=error
+org.eclipse.jdt.core.compiler.source=1.5
diff --git a/bnd.bnd b/bnd.bnd
new file mode 100644
index 00000000..cc865196
--- /dev/null
+++ b/bnd.bnd
@@ -0,0 +1,18 @@
+Private-Package: org.example,\
+ org.daisy.dotify.engine.impl,\
+ org.daisy.dotify.formatter.impl,\
+ org.daisy.dotify.obfl.impl
+Service-Component: *
+Include-Resource: / = src/
+-sources: false
+-buildpath: osgi.core,\
+ osgi.cmpn,\
+ biz.aQute.bnd.annotation,\
+ junit.osgi,\
+ dotify.api;version=latest,\
+ javax.xml.stream,\
+ dotify.common;version=latest,\
+ com.sun.enterprise.stax-osgi
+Export-Package: org.daisy.dotify.obfl,\
+ org.daisy.dotify.tools,\
+ org.daisy.dotify.writer
\ No newline at end of file
diff --git a/build.xml b/build.xml
new file mode 100644
index 00000000..bd7650f8
--- /dev/null
+++ b/build.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/launch.bndrun b/launch.bndrun
new file mode 100644
index 00000000..f0a3454a
--- /dev/null
+++ b/launch.bndrun
@@ -0,0 +1,14 @@
+-runfw: org.apache.felix.framework;version='[4,5)'
+-runee: JavaSE-1.6
+-runsystemcapabilities: ${native_capability}
+
+-resolve.effective: active
+
+-runbundles:\
+ org.apache.felix.gogo.runtime,\
+ org.apache.felix.gogo.shell,\
+ org.apache.felix.gogo.command
+
+-runrequires:\
+ osgi.identity;filter:='(osgi.identity=org.apache.felix.gogo.shell)',\
+ osgi.identity;filter:='(osgi.identity=org.apache.felix.gogo.command)'
diff --git a/src/META-INF/services/org.daisy.dotify.api.engine.FormatterEngineFactoryService b/src/META-INF/services/org.daisy.dotify.api.engine.FormatterEngineFactoryService
new file mode 100644
index 00000000..159454b9
--- /dev/null
+++ b/src/META-INF/services/org.daisy.dotify.api.engine.FormatterEngineFactoryService
@@ -0,0 +1 @@
+org.daisy.dotify.engine.impl.LayoutEngineFactoryImpl
\ No newline at end of file
diff --git a/src/META-INF/services/org.daisy.dotify.api.formatter.FormatterFactory b/src/META-INF/services/org.daisy.dotify.api.formatter.FormatterFactory
new file mode 100644
index 00000000..65021449
--- /dev/null
+++ b/src/META-INF/services/org.daisy.dotify.api.formatter.FormatterFactory
@@ -0,0 +1 @@
+org.daisy.dotify.formatter.impl.FormatterFactoryImpl
\ No newline at end of file
diff --git a/src/META-INF/services/org.daisy.dotify.api.obfl.ExpressionFactory b/src/META-INF/services/org.daisy.dotify.api.obfl.ExpressionFactory
new file mode 100644
index 00000000..c892f163
--- /dev/null
+++ b/src/META-INF/services/org.daisy.dotify.api.obfl.ExpressionFactory
@@ -0,0 +1 @@
+org.daisy.dotify.obfl.impl.ExpressionFactoryImpl
\ No newline at end of file
diff --git a/src/org/daisy/dotify/engine/impl/LayoutEngineFactoryImpl.java b/src/org/daisy/dotify/engine/impl/LayoutEngineFactoryImpl.java
new file mode 100644
index 00000000..29d5a52e
--- /dev/null
+++ b/src/org/daisy/dotify/engine/impl/LayoutEngineFactoryImpl.java
@@ -0,0 +1,95 @@
+package org.daisy.dotify.engine.impl;
+
+import javax.xml.stream.XMLEventFactory;
+import javax.xml.stream.XMLInputFactory;
+import javax.xml.stream.XMLOutputFactory;
+
+import org.daisy.dotify.api.engine.FormatterEngineFactoryService;
+import org.daisy.dotify.api.formatter.FormatterFactory;
+import org.daisy.dotify.api.obfl.ExpressionFactory;
+import org.daisy.dotify.api.translator.MarkerProcessorFactoryMakerService;
+import org.daisy.dotify.api.translator.TextBorderFactoryMakerService;
+import org.daisy.dotify.api.writer.PagedMediaWriter;
+
+import aQute.bnd.annotation.component.Component;
+import aQute.bnd.annotation.component.Reference;
+
+@Component
+public class LayoutEngineFactoryImpl implements FormatterEngineFactoryService {
+ private FormatterFactory ff;
+ private MarkerProcessorFactoryMakerService mpf;
+ private TextBorderFactoryMakerService tbf;
+ private ExpressionFactory ef;
+ private XMLInputFactory in;
+ private XMLEventFactory xef;
+ private XMLOutputFactory of;
+
+ public LayoutEngineImpl newFormatterEngine(String locale, String mode, PagedMediaWriter writer) {
+ return new LayoutEngineImpl(locale, mode, writer, ff, mpf, tbf, ef, in, xef, of);
+ }
+
+ // FIXME: not a service
+ @Reference
+ public void setFormatterFactory(FormatterFactory formatterFactory) {
+ this.ff = formatterFactory;
+ }
+
+ public void unsetFormatterFactory(FormatterFactory formatterFactory) {
+ this.ff = null;
+ }
+
+ @Reference
+ public void setMarkerProcessor(MarkerProcessorFactoryMakerService mp) {
+ this.mpf = mp;
+ }
+
+ public void unsetMarkerProcessor(MarkerProcessorFactoryMakerService mp) {
+ this.mpf = null;
+ }
+
+ @Reference
+ public void setTextBorderFactoryMaker(TextBorderFactoryMakerService tbf) {
+ this.tbf = tbf;
+ }
+
+ public void unsetTextBorderFactoryMaker(TextBorderFactoryMakerService tbf) {
+ this.tbf = null;
+ }
+
+ @Reference
+ public void setExpressionFactory(ExpressionFactory ef) {
+ this.ef = ef;
+ }
+
+ public void unsetExpressionFactory(ExpressionFactory ef) {
+ this.ef = null;
+ }
+
+ @Reference
+ public void setXMLInputFactory(XMLInputFactory in) {
+ this.in = in;
+ }
+
+ public void unsetXMLInputFactory(XMLInputFactory in) {
+ this.in = null;
+ }
+
+ @Reference
+ public void setXMLEventFactory(XMLEventFactory xef) {
+ this.xef = xef;
+ }
+
+ public void unsetXMLEventFactory(XMLEventFactory xef) {
+ this.xef = null;
+ }
+
+ @Reference
+ public void setXMLOutputFactory(XMLOutputFactory of) {
+ this.of = of;
+ }
+
+ public void unsetXMLOutputFactory(XMLOutputFactory of) {
+ this.of = null;
+ }
+
+}
diff --git a/src/org/daisy/dotify/engine/impl/LayoutEngineImpl.java b/src/org/daisy/dotify/engine/impl/LayoutEngineImpl.java
new file mode 100644
index 00000000..c3667b97
--- /dev/null
+++ b/src/org/daisy/dotify/engine/impl/LayoutEngineImpl.java
@@ -0,0 +1,141 @@
+package org.daisy.dotify.engine.impl;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.logging.Logger;
+
+import javax.xml.stream.XMLEventFactory;
+import javax.xml.stream.XMLInputFactory;
+import javax.xml.stream.XMLOutputFactory;
+import javax.xml.stream.XMLStreamException;
+
+import org.daisy.dotify.api.engine.FormatterEngine;
+import org.daisy.dotify.api.engine.LayoutEngineException;
+import org.daisy.dotify.api.formatter.FormatterFactory;
+import org.daisy.dotify.api.obfl.ExpressionFactory;
+import org.daisy.dotify.api.translator.MarkerProcessor;
+import org.daisy.dotify.api.translator.MarkerProcessorConfigurationException;
+import org.daisy.dotify.api.translator.MarkerProcessorFactoryMakerService;
+import org.daisy.dotify.api.translator.TextBorderFactoryMakerService;
+import org.daisy.dotify.api.writer.PagedMediaWriter;
+import org.daisy.dotify.api.writer.PagedMediaWriterException;
+import org.daisy.dotify.obfl.OBFLParserException;
+import org.daisy.dotify.obfl.OBFLWsNormalizer;
+import org.daisy.dotify.obfl.ObflParser;
+
+/**
+ *
+ * The LayoutEngineTask converts an OBFL-file into a file format defined by the
+ * supplied {@link PagedMediaWriter}.
+ *
+ * The LayoutEngineTask is an advanced text-only layout system.
+ * Input file must be of type OBFL.
+ *
+ * @author Joel Håkansson
+ *
+ */
+class LayoutEngineImpl implements FormatterEngine {
+ private final String locale;
+ private final String mode;
+ private final PagedMediaWriter writer;
+ private final Logger logger;
+ private boolean normalize;
+ private final FormatterFactory ff;
+ private final MarkerProcessorFactoryMakerService mpf;
+ private final TextBorderFactoryMakerService tbf;
+ private final ExpressionFactory ef;
+ private final XMLInputFactory in;
+ private final XMLEventFactory xef;
+ private final XMLOutputFactory of;
+
+ /**
+ * Creates a new instance of LayoutEngineTask.
+ * @param name a descriptive name for the task
+ * @param translator the translator to use
+ * @param writer the output writer
+ */
+ public LayoutEngineImpl(String locale, String mode, PagedMediaWriter writer, FormatterFactory ff, MarkerProcessorFactoryMakerService mpf, TextBorderFactoryMakerService tbf, ExpressionFactory ef, XMLInputFactory in, XMLEventFactory xef, XMLOutputFactory of) {
+ this.locale = locale;
+ this.mode = mode;
+ //this.locale = locale;
+ this.writer = writer;
+ this.logger = Logger.getLogger(LayoutEngineImpl.class.getCanonicalName());
+ this.normalize = true;
+ this.ff = ff;
+ this.mpf = mpf;
+ this.tbf = tbf;
+ this.ef = ef;
+ this.of = of;
+ this.xef = xef;
+ this.in = in;
+ }
+
+ public boolean isNormalizing() {
+ return normalize;
+ }
+
+ public void setNormalizing(boolean normalize) {
+ this.normalize = normalize;
+ }
+
+ public void convert(InputStream input, OutputStream output) throws LayoutEngineException {
+ File f = null;
+ try {
+ if (normalize) {
+ logger.info("Normalizing obfl...");
+ try {
+ f = File.createTempFile("temp", ".tmp");
+ f.deleteOnExit();
+ OBFLWsNormalizer normalizer = new OBFLWsNormalizer(in.createXMLEventReader(input), xef, new FileOutputStream(f));
+ normalizer.parse(of);
+ input = new FileInputStream(f);
+ } catch (Exception e) {
+ throw new LayoutEngineException(e);
+ }
+ }
+ try {
+ logger.info("Parsing input...");
+
+ MarkerProcessor mp;
+ try {
+ mp = mpf.newMarkerProcessor(locale, mode);
+ } catch (MarkerProcessorConfigurationException e) {
+ throw new IllegalArgumentException(e);
+ }
+ ObflParser obflParser = new ObflParser(locale, mode, mp, ff, tbf, ef);
+ obflParser.parse(in.createXMLEventReader(input));
+
+ logger.info("Rendering output...");
+ writer.open(output, obflParser.getMetaData());
+
+ WriterHandler wh = new WriterHandler();
+ wh.write(obflParser.getFormattedResult(), writer);
+ writer.close();
+
+ } catch (FileNotFoundException e) {
+ throw new LayoutEngineException("FileNotFoundException while running task. ", e);
+ } catch (IOException e) {
+ throw new LayoutEngineException("IOException while running task. ", e);
+ } catch (PagedMediaWriterException e) {
+ throw new LayoutEngineException("Could not open media writer.", e);
+ } catch (XMLStreamException e) {
+ throw new LayoutEngineException("XMLStreamException while running task.", e);
+ } catch (OBFLParserException e) {
+ throw new LayoutEngineException("FormatterException while running task.", e);
+ }
+ } finally {
+ if (f != null) {
+ if (!f.delete()) {
+ f.deleteOnExit();
+ }
+ }
+ }
+ }
+
+}
+
\ No newline at end of file
diff --git a/src/org/daisy/dotify/engine/impl/WriterHandler.java b/src/org/daisy/dotify/engine/impl/WriterHandler.java
new file mode 100644
index 00000000..6206a102
--- /dev/null
+++ b/src/org/daisy/dotify/engine/impl/WriterHandler.java
@@ -0,0 +1,55 @@
+package org.daisy.dotify.engine.impl;
+
+import java.io.IOException;
+import java.util.List;
+
+import org.daisy.dotify.api.formatter.LayoutMaster;
+import org.daisy.dotify.api.formatter.Page;
+import org.daisy.dotify.api.formatter.PageSequence;
+import org.daisy.dotify.api.formatter.Volume;
+import org.daisy.dotify.api.writer.PagedMediaWriter;
+
+/**
+ * Provides a method for writing pages to a PagedMediaWriter,
+ * adding headers and footers as required by the layout.
+ * @author Joel Håkansson
+ */
+class WriterHandler {
+
+ public WriterHandler() {
+ }
+ /**
+ * Writes this structure to the suppled PagedMediaWriter.
+ * @param writer the PagedMediaWriter to write to
+ * @throws IOException if IO fails
+ */
+ public void write(Iterable volumes, PagedMediaWriter writer) {
+ for (Volume v : volumes) {
+ boolean firstInVolume = true;
+ for (PageSequence s : v.getContents()) {
+ LayoutMaster lm = s.getLayoutMaster();
+ if (firstInVolume) {
+ firstInVolume = false;
+ writer.newVolume(lm);
+ }
+ writer.newSection(lm);
+ for (Page p : s.getPages()) {
+ writePage(writer, p);
+ }
+ }
+ }
+ }
+
+ private void writePage(PagedMediaWriter writer, Page p) {
+ writer.newPage();
+ List rows = p.getRows();
+ for (String r : rows) {
+ if (r.length() > 0) {
+ writer.newRow(r);
+ } else {
+ writer.newRow();
+ }
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/src/org/daisy/dotify/formatter/impl/AnchorSegment.java b/src/org/daisy/dotify/formatter/impl/AnchorSegment.java
new file mode 100644
index 00000000..f6e79119
--- /dev/null
+++ b/src/org/daisy/dotify/formatter/impl/AnchorSegment.java
@@ -0,0 +1,19 @@
+package org.daisy.dotify.formatter.impl;
+
+
+class AnchorSegment implements Segment {
+ private final String referenceID;
+
+ public AnchorSegment(String referenceID) {
+ this.referenceID = referenceID;
+ }
+
+ public SegmentType getSegmentType() {
+ return SegmentType.Anchor;
+ }
+
+ public String getReferenceID() {
+ return referenceID;
+ }
+
+}
diff --git a/src/org/daisy/dotify/formatter/impl/BlockContentManagerImpl.java b/src/org/daisy/dotify/formatter/impl/BlockContentManagerImpl.java
new file mode 100644
index 00000000..3abe63c9
--- /dev/null
+++ b/src/org/daisy/dotify/formatter/impl/BlockContentManagerImpl.java
@@ -0,0 +1,160 @@
+package org.daisy.dotify.formatter.impl;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.Stack;
+
+import org.daisy.dotify.api.formatter.BlockContentManager;
+import org.daisy.dotify.api.formatter.CrossReferences;
+import org.daisy.dotify.api.formatter.Leader;
+import org.daisy.dotify.api.formatter.Marker;
+import org.daisy.dotify.api.formatter.Row;
+import org.daisy.dotify.api.translator.BrailleTranslatorResult;
+import org.daisy.dotify.api.translator.TranslationException;
+
+class BlockContentManagerImpl implements BlockContentManager {
+ private boolean isVolatile;
+ private final ArrayList groupMarkers;
+ private final ArrayList groupAnchors;
+ private final Stack rows;
+ private final CrossReferences refs;
+
+ BlockContentManagerImpl(Stack segments, RowDataProperties rdp, CrossReferences refs) {
+ this.groupMarkers = new ArrayList();
+ this.groupAnchors = new ArrayList();
+ this.refs = refs;
+ this.rows = calculateRows(segments, rdp);
+ }
+
+ /**
+ * Get markers that are not attached to a row, i.e. markers that proceeds any text contents
+ * @return returns markers that proceeds this FlowGroups text contents
+ */
+ public ArrayList getGroupMarkers() {
+ return groupMarkers;
+ }
+
+ public ArrayList getGroupAnchors() {
+ return groupAnchors;
+ }
+
+ private Stack calculateRows(Stack segments, RowDataProperties rdp) {
+ isVolatile = false;
+ Stack ret = new Stack();
+
+ BlockHandler bh = new BlockHandler.Builder(
+ rdp.getTranslator(),
+ rdp.getMaster(),
+ rdp.getMaster().getFlowWidth() - rdp.getRightMargin(),
+ rdp.getRightMargin()).build();
+
+ if (rdp.isList()) {
+ bh.setListItem(rdp.getListLabel(), rdp.getListStyle());
+ }
+ for (Segment s : segments) {
+ switch (s.getSegmentType()) {
+ case NewLine:
+ {
+ //flush
+ layout("", bh, ret, rdp, null);
+ Row r = new Row("");
+ r.setLeftMargin(((NewLineSegment)s).getLeftIndent());
+ r.setRightMargin(rdp.getRightMargin());
+ ret.add(r);
+ break;
+ }
+ case Text:
+ {
+ TextSegment ts = (TextSegment)s;
+ bh.setBlockProperties(ts.getBlockProperties());
+ boolean oldValue = rdp.getTranslator().isHyphenating();
+ rdp.getTranslator().setHyphenating(ts.getTextProperties().isHyphenating());
+ layout(ts.getChars(), bh, ret, rdp, ts.getTextProperties().getLocale());
+ rdp.getTranslator().setHyphenating(oldValue);
+ break;
+ }
+ case Leader:
+ {
+ if (bh.getCurrentLeader()!=null) {
+ layout("", bh, ret, rdp, null);
+ }
+ bh.setCurrentLeader((Leader)s);
+ break;
+ }
+ case Reference:
+ {
+ isVolatile = true;
+ PageNumberReferenceSegment rs = (PageNumberReferenceSegment)s;
+ Integer page = null;
+ if (refs!=null) {
+ page = refs.getPageNumber(rs.getRefId());
+ }
+ //TODO: translate references using custom language?
+ if (page==null) {
+ layout("??", bh, ret, rdp, null);
+ } else {
+ layout("" + rs.getNumeralStyle().format(page), bh, ret, rdp, null);
+ }
+ break;
+ }
+ case Marker:
+ {
+ Marker m = (Marker)s;
+ if (ret.isEmpty()) {
+ groupMarkers.add(m);
+ } else {
+ ret.peek().addMarker(m);
+ }
+ break;
+ }
+ case Anchor:
+ {
+ AnchorSegment as = (AnchorSegment)s;
+ if (segments.isEmpty()) {
+ groupAnchors.add(as.getReferenceID());
+ } else {
+ ret.peek().addAnchor(as.getReferenceID());
+ }
+ break;
+ }
+ }
+ }
+
+ if (bh.getCurrentLeader()!=null || bh.getListItem()!=null) {
+ layout("", bh, ret, rdp, null);
+ }
+ return ret;
+ }
+
+ private void layout(CharSequence c, BlockHandler bh, Stack rows, RowDataProperties rdp, String locale) {
+ BrailleTranslatorResult btr;
+ if (locale!=null) {
+ try {
+ btr = rdp.getTranslator().translate(c.toString(), locale);
+ } catch (TranslationException e) {
+ e.printStackTrace();
+ btr = rdp.getTranslator().translate(c.toString());
+ }
+ } else {
+ btr = rdp.getTranslator().translate(c.toString());
+ }
+ if (rows.size()==0) {
+ rows.addAll(bh.layoutBlock(btr, rdp.getLeftMargin(), rdp.getBlockIndent(), rdp.getBlockIndentParent()));
+ } else {
+ rows.addAll(bh.appendBlock(btr, rdp.getLeftMargin(), rows.pop(), rdp.getBlockIndent(), rdp.getBlockIndentParent()));
+ }
+ }
+
+ public int getRowCount() {
+ return rows.size();
+ }
+
+ public Iterator iterator() {
+ return rows.iterator();
+ }
+
+ public boolean isVolatile() {
+ return isVolatile;
+ }
+
+}
diff --git a/src/org/daisy/dotify/formatter/impl/BlockHandler.java b/src/org/daisy/dotify/formatter/impl/BlockHandler.java
new file mode 100644
index 00000000..c245c598
--- /dev/null
+++ b/src/org/daisy/dotify/formatter/impl/BlockHandler.java
@@ -0,0 +1,299 @@
+package org.daisy.dotify.formatter.impl;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.daisy.dotify.api.formatter.BlockProperties;
+import org.daisy.dotify.api.formatter.FormattingTypes;
+import org.daisy.dotify.api.formatter.LayoutMaster;
+import org.daisy.dotify.api.formatter.Leader;
+import org.daisy.dotify.api.formatter.Marker;
+import org.daisy.dotify.api.formatter.Row;
+import org.daisy.dotify.api.translator.BrailleTranslator;
+import org.daisy.dotify.api.translator.BrailleTranslatorResult;
+import org.daisy.dotify.tools.StringTools;
+
+
+/**
+ * BlockHandler is responsible for breaking blocks of text into rows. BlockProperties
+ * such as list numbers, leaders and margins are resolved in the process. The input
+ * text is filtered using the supplied StringFilter before breaking into rows, since
+ * the length of the text could change.
+ *
+ * @author Joel Håkansson
+ */
+class BlockHandler {
+ private final BrailleTranslator translator;
+ private final String spaceChar;
+ //private int currentListNumber;
+ //private BlockProperties.ListType currentListType;
+ private Leader currentLeader;
+ private ArrayList ret;
+ private BlockProperties p;
+ private final int available;
+ private final int rightMargin;
+ private ListItem item;
+
+ public static class ListItem {
+ private String label;
+ private FormattingTypes.ListStyle type;
+
+ public ListItem(String label, FormattingTypes.ListStyle type) {
+ this.label = label;
+ this.type = type;
+ }
+
+ public String getLabel() {
+ return label;
+ }
+
+ public FormattingTypes.ListStyle getType() {
+ return type;
+ }
+ }
+
+ public static class Builder {
+ private final BrailleTranslator translator;
+ private final int available;
+ private final int rightMargin;
+
+ public Builder(BrailleTranslator translator, LayoutMaster master, int width, int rightMargin) {
+ this.translator = translator;
+ this.available = width;
+ this.rightMargin = rightMargin;
+ }
+
+ public BlockHandler build() {
+ return new BlockHandler(this);
+ }
+ }
+
+ private BlockHandler(Builder builder) {
+ this.translator = builder.translator;
+ this.currentLeader = null;
+ //this.currentListType = BlockProperties.ListType.NONE;
+ //this.currentListNumber = 0;
+ this.ret = new ArrayList();
+ this.p = new BlockProperties.Builder().build();
+ this.available = builder.available;
+ this.rightMargin = builder.rightMargin;
+ this.item = null;
+ this.spaceChar = translator.translate(" ").getTranslatedRemainder();
+
+ }
+ /*
+ public void setCurrentListType(BlockProperties.ListType type) {
+ currentListType = type;
+ }
+
+ public BlockProperties.ListType getCurrentListType() {
+ return currentListType;
+ }
+
+ public void setCurrentListNumber(int value) {
+ currentListNumber = value;
+ }
+
+ public int getCurrentListNumber() {
+ return currentListNumber;
+ }
+ */
+
+ //TODO: if list type is only used to differentiate between pre and other lists, and pre implies that label.equals(""), then type could be removed
+ /**
+ * Sets the list item to use for the following call to layoutBlock. Since
+ * the list item label is resolved prior to this call, the list type
+ * is only used to differentiate between pre formatted list items and other
+ * types of lists.
+ * @param label the resolved list item label, typically a number or a bullet
+ * @param type type of list item
+ */
+ public void setListItem(String label, FormattingTypes.ListStyle type) {
+ item = new ListItem(label, type);
+ }
+
+ /**
+ * Gets the current list item.
+ * @return returns the current list item, or null if there is no current list item
+ */
+ public ListItem getListItem() {
+ return item;
+ }
+
+ public void setBlockProperties(BlockProperties p) {
+ this.p = p;
+ }
+
+ public BlockProperties getBlockProperties() {
+ return p;
+ }
+ /*
+ public void setWidth(int value) {
+ available = value;
+ }*/
+
+ public int getWidth() {
+ return available;
+ }
+
+ public void setCurrentLeader(Leader l) {
+ currentLeader = l;
+ }
+
+ public Leader getCurrentLeader() {
+ return currentLeader;
+ }
+ /*
+ public void setBlockIndent(int value) {
+ this.blockIndent = value;
+ }*/
+
+ /**
+ * Break text into rows.
+ * @param btr the translator result to break into rows
+ * @param leftMargin left margin of the text
+ * @param blockIndent the block indent
+ * @param blockIndentParent the block indent parent
+ * @return returns an ArrayList of Rows
+ */
+ public ArrayList layoutBlock(BrailleTranslatorResult btr, int leftMargin, int blockIndent, int blockIndentParent) {
+ return layoutBlock(btr, leftMargin, null, blockIndent, blockIndentParent);
+ }
+
+ /**
+ * Continue a block of text, starting on the supplied row.
+ * @param btr the translator result to break into rows
+ * @param leftMargin left margin of the text
+ * @param row the row to continue the layout on
+ * @param blockIndent the block indent
+ * @param blockIndentParent the block indent parent
+ * @return returns an ArrayList of Rows. The first row being the supplied row, with zero or more characters
+ * from text
+ */
+ public ArrayList appendBlock(BrailleTranslatorResult btr, int leftMargin, Row row, int blockIndent, int blockIndentParent) {
+ return layoutBlock(btr, leftMargin, row, blockIndent, blockIndentParent);
+ }
+
+ private ArrayList layoutBlock(BrailleTranslatorResult btr, int leftMargin, Row r, int blockIndent, int blockIndentParent) {
+ ret = new ArrayList();
+ // process first row, is it a new block or should we continue the current row?
+ if (r==null) {
+ // add to left margin
+ if (item!=null) { //currentListType!=BlockProperties.ListType.NONE) {
+ String listLabel = translator.translate(item.getLabel()).getTranslatedRemainder();
+ if (item.getType()==FormattingTypes.ListStyle.PL) {
+ int bypassBlockIndent = blockIndent;
+ blockIndent = blockIndentParent;
+ newRow(listLabel, btr, available, leftMargin, 0, p, blockIndent);
+ blockIndent = bypassBlockIndent;
+ } else {
+ newRow(listLabel, btr, available, leftMargin, p.getFirstLineIndent(), p, blockIndent);
+ }
+ item = null;
+ } else {
+ newRow("", btr, available, leftMargin, p.getFirstLineIndent(), p, blockIndent);
+ }
+ } else {
+ newRow(r.getMarkers(), r.getLeftMargin(), "", r.getChars().toString(), btr, available, p, blockIndent);
+ }
+ while (btr.hasNext()) { //LayoutTools.length(chars.toString())>0
+ newRow("", btr, available, leftMargin, p.getTextIndent(), p, blockIndent);
+ }
+ return ret;
+ }
+
+ private void newRow(String contentBefore, BrailleTranslatorResult chars, int available, int margin, int indent, BlockProperties p, int blockIndent) {
+ int thisIndent = indent + blockIndent - StringTools.length(contentBefore);
+ //assert thisIndent >= 0;
+ String preText = contentBefore + StringTools.fill(spaceChar, thisIndent).toString();
+ newRow(null, margin, preText, "", chars, available, p, blockIndent);
+ }
+
+ //TODO: check leader functionality
+ private void newRow(List r, int margin, String preContent, String preTabText, BrailleTranslatorResult btr, int available, BlockProperties p, int blockIndent) {
+
+ // [margin][preContent][preTabText][tab][postTabText]
+ // preContentPos ^
+
+ int preTextIndent = StringTools.length(preContent);
+ int preContentPos = margin+preTextIndent;
+ preTabText = preTabText.replaceAll("\u00ad", "");
+ int preTabPos = preContentPos+StringTools.length(preTabText);
+ int postTabTextLen = btr.countRemaining();
+ int maxLenText = available-(preContentPos);
+ if (maxLenText<1) {
+ throw new RuntimeException("Cannot continue layout: No space left for characters.");
+ }
+
+ int width = available;
+ String tabSpace = "";
+ if (currentLeader!=null) {
+ int leaderPos = currentLeader.getPosition().makeAbsolute(width);
+ int offset = leaderPos-preTabPos;
+ int align = 0;
+ switch (currentLeader.getAlignment()) {
+ case LEFT:
+ align = 0;
+ break;
+ case RIGHT:
+ align = postTabTextLen;
+ break;
+ case CENTER:
+ align = postTabTextLen/2;
+ break;
+ }
+ if (preTabPos>leaderPos || offset - align < 0) { // if tab position has been passed or if text does not fit within row, try on a new row
+ Row row = new Row(preContent + preTabText);
+ row.setLeftMargin(margin);
+ row.setRightMargin(rightMargin);
+ row.setAlignment(p.getAlignment());
+ if (r!=null) {
+ row.addMarkers(r);
+ r = null;
+ }
+ ret.add(row);
+
+ preContent = StringTools.fill(spaceChar, p.getTextIndent()+blockIndent);
+ preTextIndent = StringTools.length(preContent);
+ preTabText = "";
+
+ preContentPos = margin+preTextIndent;
+ preTabPos = preContentPos;
+ maxLenText = available-(preContentPos);
+ offset = leaderPos-preTabPos;
+ }
+ if (offset - align > 0) {
+ String leaderPattern = translator.translate(currentLeader.getPattern()).getTranslatedRemainder();
+ tabSpace = StringTools.fill(leaderPattern, offset - align);
+ } // else: leader position has been passed on an empty row or text does not fit on an empty row, ignore
+ }
+
+ maxLenText -= StringTools.length(tabSpace);
+ maxLenText -= StringTools.length(preTabText);
+
+ boolean force = maxLenText >= available - (preContentPos);
+ String next = btr.nextTranslatedRow(maxLenText, force);
+ Row nr;
+ if ("".equals(next) && "".equals(tabSpace)) {
+ nr = new Row(preContent + preTabText.replaceAll("[\\s\u2800]+\\z", ""));
+ } else {
+ nr = new Row(preContent + preTabText + tabSpace + next);
+ }
+
+ // discard leader
+ currentLeader = null;
+
+ assert nr != null;
+ if (r!=null) {
+ nr.addMarkers(r);
+ }
+ nr.setLeftMargin(margin);
+ nr.setRightMargin(rightMargin);
+ nr.setAlignment(p.getAlignment());
+ /*
+ if (nr.getChars().length()>master.getFlowWidth()) {
+ throw new RuntimeException("Row is too long (" + nr.getChars().length() + "/" + master.getFlowWidth() + ") '" + nr.getChars() + "'");
+ }*/
+ ret.add(nr);
+ }
+}
diff --git a/src/org/daisy/dotify/formatter/impl/BlockImpl.java b/src/org/daisy/dotify/formatter/impl/BlockImpl.java
new file mode 100644
index 00000000..75697687
--- /dev/null
+++ b/src/org/daisy/dotify/formatter/impl/BlockImpl.java
@@ -0,0 +1,169 @@
+package org.daisy.dotify.formatter.impl;
+
+import java.util.Stack;
+
+import org.daisy.dotify.api.formatter.Block;
+import org.daisy.dotify.api.formatter.BlockContentManager;
+import org.daisy.dotify.api.formatter.BlockPosition;
+import org.daisy.dotify.api.formatter.BlockProperties;
+import org.daisy.dotify.api.formatter.CrossReferences;
+import org.daisy.dotify.api.formatter.FormattingTypes;
+import org.daisy.dotify.api.formatter.Leader;
+import org.daisy.dotify.api.formatter.Marker;
+import org.daisy.dotify.api.formatter.TextProperties;
+import org.daisy.dotify.api.formatter.NumeralField.NumeralStyle;
+import org.daisy.dotify.formatter.impl.Segment.SegmentType;
+
+
+class BlockImpl implements Block {
+ private String blockId;
+ private int spaceBefore;
+ private int spaceAfter;
+ private FormattingTypes.BreakBefore breakBefore;
+ private FormattingTypes.Keep keep;
+ private int keepWithNext;
+ private int keepWithPreviousSheets;
+ private int keepWithNextSheets;
+ private String id;
+ private Stack segments;
+ private final RowDataProperties rdp;
+ private BlockContentManager rdm;
+ private BlockPosition verticalPosition;
+
+
+ BlockImpl(String blockId, RowDataProperties rdp) {
+ this.spaceBefore = 0;
+ this.spaceAfter = 0;
+ this.breakBefore = FormattingTypes.BreakBefore.AUTO;
+ this.keep = FormattingTypes.Keep.AUTO;
+ this.keepWithNext = 0;
+ this.keepWithPreviousSheets = 0;
+ this.keepWithNextSheets = 0;
+ this.id = "";
+ this.blockId = blockId;
+ this.segments = new Stack();
+ this.rdp = rdp;
+ this.rdm = null;
+ this.verticalPosition = null;
+ }
+
+ public void addMarker(Marker m) {
+ segments.add(new MarkerSegment(m));
+ }
+
+ public void addAnchor(String ref) {
+ segments.add(new AnchorSegment(ref));
+ }
+
+ public void newLine(int leftIndent) {
+ segments.push(new NewLineSegment(leftIndent));
+ }
+
+ public void addChars(CharSequence c, TextProperties tp, BlockProperties p) {
+ if (segments.size() > 0 && segments.peek().getSegmentType() == SegmentType.Text) {
+ TextSegment ts = ((TextSegment) segments.peek());
+ if (ts.getBlockProperties().equals(p) && ts.getTextProperties().equals(tp)) {
+ // Logger.getLogger(this.getClass().getCanonicalName()).finer("Appending chars to existing text segment.");
+ ts.setChars(ts.getChars() + "" + c);
+ return;
+ }
+ }
+ segments.push(new TextSegment(c, tp, p));
+ }
+
+ public void insertLeader(Leader l) {
+ segments.push(new LeaderSegment(l));
+ }
+
+ public void insertReference(String identifier, NumeralStyle numeralStyle) {
+ segments.push(new PageNumberReferenceSegment(identifier, numeralStyle));
+ }
+
+ public void setListItem(String label, FormattingTypes.ListStyle type) {
+ rdp.setListItem(label, type);
+ }
+
+ public int getSpaceBefore() {
+ return spaceBefore;
+ }
+
+ public int getSpaceAfter() {
+ return spaceAfter;
+ }
+
+ public FormattingTypes.BreakBefore getBreakBeforeType() {
+ return breakBefore;
+ }
+
+ public FormattingTypes.Keep getKeepType() {
+ return keep;
+ }
+
+ public int getKeepWithNext() {
+ return keepWithNext;
+ }
+
+ public int getKeepWithPreviousSheets() {
+ return keepWithPreviousSheets;
+ }
+
+ public int getKeepWithNextSheets() {
+ return keepWithNextSheets;
+ }
+
+ public String getIdentifier() {
+ return id;
+ }
+
+ public BlockPosition getVerticalPosition() {
+ return verticalPosition;
+ }
+
+ public void addSpaceBefore(int spaceBefore) {
+ this.spaceBefore += spaceBefore;
+ }
+
+ public void addSpaceAfter(int spaceAfter) {
+ this.spaceAfter += spaceAfter;
+ }
+
+ public void setBreakBeforeType(FormattingTypes.BreakBefore breakBefore) {
+ this.breakBefore = breakBefore;
+ }
+
+ public void setKeepType(FormattingTypes.Keep keep) {
+ this.keep = keep;
+ }
+
+ public void setKeepWithNext(int keepWithNext) {
+ this.keepWithNext = keepWithNext;
+ }
+
+ public void setKeepWithPreviousSheets(int keepWithPreviousSheets) {
+ this.keepWithPreviousSheets = keepWithPreviousSheets;
+ }
+
+ public void setKeepWithNextSheets(int keepWithNextSheets) {
+ this.keepWithNextSheets = keepWithNextSheets;
+ }
+
+ public void setIdentifier(String id) {
+ this.id = id;
+ }
+
+ public void setVerticalPosition(BlockPosition vertical) {
+ this.verticalPosition = vertical;
+ }
+
+ public String getBlockIdentifier() {
+ return blockId;
+ }
+
+ public BlockContentManager getBlockContentManager(CrossReferences refs) {
+ if (rdm==null || rdm.isVolatile()) {
+ rdm = new BlockContentManagerImpl(segments, rdp, refs);
+ }
+ return rdm;
+ }
+
+}
diff --git a/src/org/daisy/dotify/formatter/impl/BlockSequenceImpl.java b/src/org/daisy/dotify/formatter/impl/BlockSequenceImpl.java
new file mode 100644
index 00000000..e90356aa
--- /dev/null
+++ b/src/org/daisy/dotify/formatter/impl/BlockSequenceImpl.java
@@ -0,0 +1,79 @@
+package org.daisy.dotify.formatter.impl;
+
+import java.util.Stack;
+
+import org.daisy.dotify.api.formatter.Block;
+import org.daisy.dotify.api.formatter.BlockSequence;
+import org.daisy.dotify.api.formatter.CrossReferences;
+import org.daisy.dotify.api.formatter.LayoutMaster;
+import org.daisy.dotify.api.formatter.SequenceProperties;
+
+
+class BlockSequenceImpl extends Stack implements BlockSequence {
+ private final SequenceProperties p;
+ private final LayoutMaster master;
+
+ public BlockSequenceImpl(SequenceProperties p, LayoutMaster master) {
+ this.p = p;
+ this.master = master;
+ }
+ /*
+ public SequenceProperties getSequenceProperties() {
+ return p;
+ }*/
+
+ public BlockImpl newBlock(String blockId, RowDataProperties rdp) {
+ return (BlockImpl)this.push((Block)new BlockImpl(blockId, rdp));
+ }
+
+ public BlockImpl getCurrentBlock() {
+ return (BlockImpl)this.peek();
+ }
+/*
+ public Block[] toArray() {
+ Block[] ret = new Block[this.size()];
+ return super.toArray(ret);
+ }*/
+
+ private static final long serialVersionUID = -6105005856680272131L;
+
+ public LayoutMaster getLayoutMaster() {
+ return master;
+ }
+
+ public Block getBlock(int index) {
+ return this.elementAt(index);
+ }
+
+ public int getBlockCount() {
+ return this.size();
+ }
+
+ public Integer getInitialPageNumber() {
+ return p.getInitialPageNumber();
+ }
+
+ public SequenceProperties getSequenceProperties() {
+ return p;
+ }
+
+ public int getKeepHeight(Block block, CrossReferences refs) {
+ return getKeepHeight(this.indexOf(block), refs);
+ }
+ private int getKeepHeight(int gi, CrossReferences refs) {
+ int keepHeight = getBlock(gi).getSpaceBefore()+getBlock(gi).getBlockContentManager(refs).getRowCount();
+ if (getBlock(gi).getKeepWithNext()>0 && gi+1 masters;
+ private final Stack blocks;
+
+ public BlockStructImpl() {
+ this(new HashMap());
+ }
+
+ public BlockStructImpl(HashMap masters) {
+ this.masters = masters;
+ this.blocks = new Stack();
+ }
+
+ public void newSequence(SequenceProperties p) {
+ blocks.push((BlockSequence)new BlockSequenceImpl(p, masters.get(p.getMasterName())));
+ }
+
+ public BlockSequenceImpl getCurrentSequence() {
+ return (BlockSequenceImpl)blocks.peek();
+ }
+
+ public void addLayoutMaster(String name, LayoutMaster master) {
+ masters.put(name, master);
+ }
+
+ /*public LayoutMaster getLayoutMaster(String name) {
+ return masters.get(name);
+ }*/
+ /*
+ public HashMap getMasters() {
+ return masters;
+ }*/
+
+ public Iterable getBlockSequenceIterable() {
+ return blocks;
+ }
+
+}
diff --git a/src/org/daisy/dotify/formatter/impl/BookStruct.java b/src/org/daisy/dotify/formatter/impl/BookStruct.java
new file mode 100644
index 00000000..a5c56e5a
--- /dev/null
+++ b/src/org/daisy/dotify/formatter/impl/BookStruct.java
@@ -0,0 +1,270 @@
+package org.daisy.dotify.formatter.impl;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.logging.Logger;
+
+import org.daisy.dotify.api.formatter.BlockSequence;
+import org.daisy.dotify.api.formatter.LayoutMaster;
+import org.daisy.dotify.api.formatter.Page;
+import org.daisy.dotify.api.formatter.PageSequence;
+import org.daisy.dotify.api.formatter.PageStruct;
+import org.daisy.dotify.api.formatter.Volume;
+import org.daisy.dotify.api.formatter.VolumeContentFormatter;
+import org.daisy.dotify.api.translator.BrailleTranslator;
+import org.daisy.dotify.text.BreakPoint;
+import org.daisy.dotify.text.BreakPointHandler;
+import org.daisy.dotify.tools.CompoundIterable;
+
+/**
+ * Provides a default implementation of BookStruct
+ *
+ * @author Joel Håkansson
+ */
+class BookStruct {
+ private final static char ZERO_WIDTH_SPACE = '\u200b';
+ private final Logger logger;
+ private final PaginatorImpl contentPaginator;
+
+ private final VolumeContentFormatter volumeFormatter;
+ private final BrailleTranslator translator;
+
+ private final CrossReferenceHandler crh;
+
+ public BookStruct(PaginatorImpl content, VolumeContentFormatter volumeFormatter,
+ BrailleTranslator translator) {
+ this.contentPaginator = content;
+ this.translator = translator;
+ this.volumeFormatter = volumeFormatter;
+
+ this.logger = Logger.getLogger(BookStruct.class.getCanonicalName());
+
+ this.crh = new CrossReferenceHandler();
+ }
+
+ private void reformat(int splitterMax) throws PaginatorException {
+ crh.setContents(contentPaginator.paginate(crh), splitterMax);
+ //paginator.close();
+ }
+
+ private PageStruct getPreVolumeContents(int volumeNumber) {
+ return getVolumeContents(volumeNumber, true);
+ }
+
+ private PageStruct getPostVolumeContents(int volumeNumber) {
+ return getVolumeContents(volumeNumber, false);
+ }
+
+ private PageStruct getVolumeContents(int volumeNumber, boolean pre) {
+ try {
+ List> ib;
+ if (pre) {
+ ib = volumeFormatter.formatPreVolumeContents(volumeNumber, crh.getExpectedVolumeCount(), crh);
+ } else {
+ ib = volumeFormatter.formatPostVolumeContents(volumeNumber, crh.getExpectedVolumeCount(), crh);
+ }
+ PaginatorImpl paginator2 = new PaginatorImpl();
+ paginator2.open(translator, new CompoundIterable(ib));
+ PageStruct ret = paginator2.paginate(crh);
+ paginator2.close();
+ if (pre) {
+ crh.setPreVolData(volumeNumber, ret);
+ } else {
+ crh.setPostVolData(volumeNumber, ret);
+ }
+ return ret;
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ } catch (PaginatorException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private void trimEnd(StringBuilder sb, Page p) {
+ int i = 0;
+ int x = sb.length()-1;
+ while (i0) {
+ if (sb.charAt(x)=='s') {
+ x--;
+ i++;
+ }
+ if (sb.charAt(x)==ZERO_WIDTH_SPACE) {
+ sb.deleteCharAt(x);
+ x--;
+ }
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ public Iterable getVolumes() {
+ try {
+ reformat(50);
+ } catch (PaginatorException e) {
+ throw new RuntimeException("Error while reformatting.");
+ }
+ int j = 1;
+ boolean ok = false;
+ int totalPreCount = 0;
+ int totalPostCount = 0;
+ int prvVolCount = 0;
+ int volumeOffset = 0;
+ int volsMin = Integer.MAX_VALUE;
+ ArrayList ret = new ArrayList();
+ while (!ok) {
+ // make a preliminary calculation based on contents only
+ Iterable extends PageSequence> ps = crh.getContents().getContents();
+ final int contents = PageTools.countSheets(ps);
+ ArrayList pages = new ArrayList();
+ StringBuilder res = new StringBuilder();
+ {
+ boolean volBreakAllowed = true;
+ for (PageSequence seq :ps) {
+ StringBuilder sb = new StringBuilder();
+ LayoutMaster lm = seq.getLayoutMaster();
+ int pageIndex=0;
+ for (Page p : seq.getPages()) {
+ if (!lm.duplex() || pageIndex%2==0) {
+ volBreakAllowed = true;
+ sb.append("s");
+ }
+ volBreakAllowed &= p.allowsVolumeBreak();
+ trimEnd(sb, p);
+ if (!lm.duplex() || pageIndex%2==1) {
+ if (volBreakAllowed) {
+ sb.append(ZERO_WIDTH_SPACE);
+ }
+ }
+ pages.add(p);
+ pageIndex++;
+ }
+ res.append(sb);
+ res.append(ZERO_WIDTH_SPACE);
+ }
+ }
+ logger.fine("Volume break string: " + res.toString().replace(ZERO_WIDTH_SPACE, '-'));
+ BreakPointHandler volBreaks = new BreakPointHandler(res.toString());
+ int splitterMax = volumeFormatter.getVolumeMaxSize(1, crh.getExpectedVolumeCount());
+
+ EvenSizeVolumeSplitterCalculator esc;
+ esc = new EvenSizeVolumeSplitterCalculator(contents + totalPreCount + totalPostCount, splitterMax, volumeOffset);
+ // this fixes a problem where the volume overhead pushes the
+ // volume count up once the volume offset has been set
+ if (volumeOffset == 1 && esc.getVolumeCount() > volsMin + 1) {
+ volumeOffset = 0;
+ esc = new EvenSizeVolumeSplitterCalculator(contents + totalPreCount + totalPostCount, splitterMax, volumeOffset);
+ }
+
+ volsMin = Math.min(esc.getVolumeCount(), volsMin);
+
+ crh.setSDC(esc);
+
+ if (crh.getExpectedVolumeCount()!=prvVolCount) {
+ prvVolCount = crh.getExpectedVolumeCount();
+ }
+ //System.out.println("volcount "+volumeCount() + " sheets " + sheets);
+ boolean ok2 = true;
+ totalPreCount = 0;
+ totalPostCount = 0;
+ ret = new ArrayList();
+ int pageIndex = 0;
+ ArrayList> preV = new ArrayList>();
+ ArrayList> postV = new ArrayList>();
+
+ for (int i=1;i<=crh.getExpectedVolumeCount();i++) {
+ if (splitterMax!=volumeFormatter.getVolumeMaxSize(i, crh.getExpectedVolumeCount())) {
+ logger.warning("Implementation does not support different target volume size. All volumes must have the same target size.");
+ }
+ preV.add((Iterable) getPreVolumeContents(i).getContents());
+ postV.add((Iterable) getPostVolumeContents(i).getContents());
+ }
+ for (int i=1;i<=crh.getExpectedVolumeCount();i++) {
+
+ totalPreCount += crh.getVolData(i).getPreVolSize();
+ totalPostCount += crh.getVolData(i).getPostVolSize();
+
+ int targetSheetsInVolume = crh.sheetsInVolume(i);
+ if (i==crh.getExpectedVolumeCount()) {
+ targetSheetsInVolume = splitterMax;
+ }
+ int contentSheets = targetSheetsInVolume-crh.getVolData(i).getVolOverhead();
+ int offset = -1;
+ BreakPoint bp;
+ do {
+ offset++;
+ bp = volBreaks.tryNextRow(contentSheets+offset);
+ } while (bp.getHead().length()=pages.size()) {
+ break;
+ }
+ if (body.countSheets(pages.get(pageIndex))<=contentSheets) {
+ body.addPage(pages.get(pageIndex));
+ pageIndex++;
+ } else {
+ break;
+ }
+ }
+ int sheetsInVolume = PageTools.countSheets(body) + crh.getVolData(i).getVolOverhead();
+ if (sheetsInVolume>crh.getVolData(i).getTargetVolSize()) {
+ ok2 = false;
+ logger.fine("Error in code. Too many sheets in volume " + i + ": " + sheetsInVolume);
+ }
+ ret.add(new VolumeImpl(preV.get(i-1), body, postV.get(i-1)));
+ }
+ if (volBreaks.hasNext()) {
+ ok2 = false;
+ logger.fine("There is more content... sheets: " + volBreaks.getRemaining() + ", pages: " +(pages.size()-pageIndex));
+ if (!crh.isDirty()) {
+ if (volumeOffset < 1) {
+ //First check to see if the page increase can will be handled automatically without increasing volume offset
+ //in the next iteration (by supplying up-to-date overhead values)
+ EvenSizeVolumeSplitterCalculator esv = new EvenSizeVolumeSplitterCalculator(contents+totalPreCount+totalPostCount, splitterMax, volumeOffset);
+ if (esv.equals(crh.getSdc())) {
+ volumeOffset++;
+ }
+ } else {
+ logger.warning("Could not fit contents even when adding a new volume.");
+ }
+ }
+ }
+ if (!crh.isDirty() && pageIndex==pages.size() && ok2) {
+ //everything fits
+ ok = true;
+ } else if (j>9) {
+ throw new RuntimeException("Failed to complete volume division.");
+ } else {
+ j++;
+ crh.setDirty(false);
+ try {
+ reformat(volumeFormatter.getVolumeMaxSize(1, crh.getExpectedVolumeCount()));
+ } catch (PaginatorException e) {
+ throw new RuntimeException("Error while reformatting.", e);
+ }
+ logger.info("Things didn't add up, running another iteration (" + j + ")");
+ }
+ }
+ return ret;
+ }
+/*
+ class VolumeStructData implements Iterable {
+ private final List ret;
+ VolumeStructData(List ret) {
+ this.ret = ret;
+ }
+ public Iterator iterator() {
+ return ret.iterator();
+ }
+ };
+*/
+}
diff --git a/src/org/daisy/dotify/formatter/impl/CrossReferenceHandler.java b/src/org/daisy/dotify/formatter/impl/CrossReferenceHandler.java
new file mode 100644
index 00000000..17ac41a2
--- /dev/null
+++ b/src/org/daisy/dotify/formatter/impl/CrossReferenceHandler.java
@@ -0,0 +1,278 @@
+package org.daisy.dotify.formatter.impl;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.daisy.dotify.api.formatter.CrossReferences;
+import org.daisy.dotify.api.formatter.LayoutMaster;
+import org.daisy.dotify.api.formatter.Page;
+import org.daisy.dotify.api.formatter.PageSequence;
+import org.daisy.dotify.api.formatter.PageStruct;
+
+class CrossReferenceHandler implements CrossReferences {
+ private final Map volLocations;
+ private final Map pageLocations;
+ private final Map volData;
+
+ private HashMap volSheet;
+ private Map pageSheetMap;
+ private PageStruct ps;
+ private EvenSizeVolumeSplitterCalculator sdc;
+
+ private boolean isDirty;
+ private boolean volumeForContentSheetChanged;
+ //private int maxKey;
+
+ public CrossReferenceHandler() {
+ this.volLocations = new HashMap();
+ this.pageLocations = new HashMap();
+
+ this.volData = new HashMap();
+ this.volSheet = new HashMap();
+ this.isDirty = false;
+ this.volumeForContentSheetChanged = false;
+ //this.maxKey = 0;
+ }
+
+ public PageStruct getContents() {
+ return ps;
+ }
+
+ public void setContents(PageStruct contents, int splitterMax) {
+ this.ps = contents;
+ this.sdc = new EvenSizeVolumeSplitterCalculator(PageTools.countSheets(ps.getContents()), splitterMax);
+ int sheetIndex=0;
+ this.pageSheetMap = new HashMap();
+ for (PageSequence s : ps.getContents()) {
+ LayoutMaster lm = s.getLayoutMaster();
+ int pageIndex=0;
+ for (Page p : s.getPages()) {
+ if (!lm.duplex() || pageIndex%2==0) {
+ sheetIndex++;
+ }
+ pageSheetMap.put(p, sheetIndex);
+ pageIndex++;
+ }
+ }
+ }
+
+ public EvenSizeVolumeSplitterCalculator getSdc() {
+ return sdc;
+ }
+
+ public void setSDC(EvenSizeVolumeSplitterCalculator sdc) {
+ volumeForContentSheetChanged = false;
+ this.sdc = sdc;
+ }
+
+ public int sheetsInVolume(int volIndex) {
+ return sdc.sheetsInVolume(volIndex);
+ }
+
+ private void setVolData(int volumeNumber, VolData d) {
+ //update the highest observed volume number
+ //maxKey = Math.max(maxKey, volumeNumber);
+ volData.put(volumeNumber, d);
+ }
+
+ public void setPreVolData(int volumeNumber, PageStruct preVolData) {
+ VolData d = (VolData)getVolData(volumeNumber);
+ /*if (d.preVolData!=preVolData) {
+ setDirty(true);
+ }*/
+ d.setPreVolData(preVolData);
+ }
+
+ public void setPostVolData(int volumeNumber, PageStruct postVolData) {
+ VolData d = (VolData)getVolData(volumeNumber);
+ /*if (d.postVolData!=postVolData) {
+ setDirty(true);
+ }*/
+ d.setPostVolData(postVolData);
+ }
+
+ public void setTargetVolSize(int volumeNumber, int targetVolSize) {
+ VolData d = (VolData)getVolData(volumeNumber);
+ if (d.getTargetVolSize()!=targetVolSize) {
+ setDirty(true);
+ }
+ d.setTargetVolSize(targetVolSize);
+ }
+
+ public VolDataInterface getVolData(int volumeNumber) {
+ if (volumeNumber<1) {
+ throw new IndexOutOfBoundsException("Volume must be greater than or equal to 1");
+ }
+ if (volData.get(volumeNumber)==null) {
+ setVolData(volumeNumber, new VolData());
+ setDirty(true);
+ }
+ return volData.get(volumeNumber);
+ }
+
+ public boolean isDirty() {
+ return isDirty || volumeForContentSheetChanged;
+ }
+
+ public void setDirty(boolean isDirty) {
+ this.isDirty = isDirty;
+ }
+
+ public int updateVolumeLocation(String refid, int vol) {
+ Integer v = volLocations.get(refid);
+ volLocations.put(refid, vol);
+ if (v!=null && v!=vol) {
+ //this refid has been requested before and it changed location
+ isDirty = true;
+ }
+ return vol;
+ }
+
+ public Page updatePageLocation(String refid, Page page) {
+ Integer p = pageLocations.get(refid);
+ pageLocations.put(refid, page.getPageIndex());
+ if (p!=null && p!=page.getPageIndex()) {
+ //this refid has been requested before and it changed location
+ isDirty = true;
+ }
+ return page;
+ }
+
+ public Integer getVolumeNumber(String refid) {
+ for (int i=1; i<=sdc.getVolumeCount(); i++) {
+ if (volData.get(i)!=null) {
+ if (volData.get(i).getPreVolData()!=null && volData.get(i).getPreVolData().getPage(refid)!=null) {
+ return updateVolumeLocation(refid, i);
+ }
+ if (volData.get(i).getPostVolData()!=null && volData.get(i).getPostVolData().getPage(refid)!=null) {
+ return updateVolumeLocation(refid, i);
+ }
+ }
+ }
+ Integer i = pageSheetMap.get(getPage(refid));
+ if (i!=null) {
+ return updateVolumeLocation(refid, getVolumeForContentSheet(i));
+ }
+ setDirty(true);
+ return null;
+ }
+
+ private Page getPage(String refid) {
+ Page ret;
+ if (ps!=null && (ret=ps.getPage(refid))!=null) {
+ return updatePageLocation(refid, ret);
+ }
+ if (sdc!=null) {
+ for (int i=1; i<=sdc.getVolumeCount(); i++) {
+ if (volData.get(i)!=null) {
+ if (volData.get(i).getPreVolData()!=null && (ret=volData.get(i).getPreVolData().getPage(refid))!=null) {
+ return updatePageLocation(refid, ret);
+ }
+ if (volData.get(i).getPostVolData()!=null && (ret=volData.get(i).getPostVolData().getPage(refid))!=null) {
+ return updatePageLocation(refid, ret);
+ }
+ }
+ }
+ }
+ setDirty(true);
+ return null;
+ }
+
+ public Integer getPageNumber(String refid) {
+ Page p = getPage(refid);
+ if (p==null) {
+ return null;
+ } else {
+ return p.getPageIndex()+1;
+ }
+ }
+
+ private int getVolumeForContentSheet(int sheetIndex) {
+ if (sheetIndex<1) {
+ throw new IndexOutOfBoundsException("Sheet index must be greater than zero: " + sheetIndex);
+ }
+ if (sheetIndex>sdc.getSheetCount()) {
+ throw new IndexOutOfBoundsException("Sheet index must not exceed agreed value.");
+ }
+ int lastSheetInCurrentVolume=0;
+ int retVolume=0;
+ do {
+ retVolume++;
+ int prvVal = lastSheetInCurrentVolume;
+ int volSize = getVolData(retVolume).getTargetVolSize();
+ if (volSize==0) {
+ volSize = sdc.sheetsInVolume(retVolume);
+ }
+ lastSheetInCurrentVolume += volSize;
+ lastSheetInCurrentVolume -= getVolData(retVolume).getVolOverhead();
+ if (prvVal>=lastSheetInCurrentVolume) {
+ throw new RuntimeException("Negative volume size");
+ }
+ } while (sheetIndex>lastSheetInCurrentVolume);
+ Integer cv = volSheet.get(sheetIndex);
+ if (cv==null || cv!=retVolume) {
+ volumeForContentSheetChanged = true;
+ volSheet.put(sheetIndex, retVolume);
+ }
+ return retVolume;
+ }
+
+ public int getExpectedVolumeCount() {
+ return sdc.getVolumeCount();
+ }
+
+ private class VolData implements VolDataInterface {
+ private PageStruct preVolData;
+ private PageStruct postVolData;
+ private int preVolSize;
+ private int postVolSize;
+ private int targetVolSize;
+
+ private VolData() {
+ this.preVolSize = 0;
+ this.postVolSize = 0;
+ this.targetVolSize = 0;
+ }
+
+ public PageStruct getPreVolData() {
+ return preVolData;
+ }
+
+ public void setPreVolData(PageStruct preVolData) {
+ //use the highest value to avoid oscillation
+ preVolSize = Math.max(preVolSize, PageTools.countSheets(preVolData.getContents()));
+ this.preVolData = preVolData;
+ }
+
+ public PageStruct getPostVolData() {
+ return postVolData;
+ }
+
+ public void setPostVolData(PageStruct postVolData) {
+ //use the highest value to avoid oscillation
+ postVolSize = Math.max(postVolSize, PageTools.countSheets(postVolData.getContents()));
+ this.postVolData = postVolData;
+ }
+
+ public int getPreVolSize() {
+ return preVolSize;
+ }
+
+ public int getPostVolSize() {
+ return postVolSize;
+ }
+
+ public int getVolOverhead() {
+ return preVolSize + postVolSize;
+ }
+
+ public int getTargetVolSize() {
+ return targetVolSize;
+ }
+
+ public void setTargetVolSize(int targetVolSize) {
+ this.targetVolSize = targetVolSize;
+ }
+ }
+
+}
diff --git a/src/org/daisy/dotify/formatter/impl/EvenSizeVolumeSplitterCalculator.java b/src/org/daisy/dotify/formatter/impl/EvenSizeVolumeSplitterCalculator.java
new file mode 100644
index 00000000..700b0a7a
--- /dev/null
+++ b/src/org/daisy/dotify/formatter/impl/EvenSizeVolumeSplitterCalculator.java
@@ -0,0 +1,138 @@
+package org.daisy.dotify.formatter.impl;
+
+/**
+ * Provides information needed to split a book into volumes.
+ *
+ * @author Joel Håkansson
+ */
+class EvenSizeVolumeSplitterCalculator {
+ private final int sheets;
+ // breakpoint, in sheets
+ private final int breakpoint;
+ // number of volumes with breakpoint sheets
+ private final int sheetsPerVolumeBreakpoint;
+
+ private final int volsWithBpSheets;
+ // number of volumes
+ private final int volumes;
+
+ /**
+ *
+ * @param sheets total number of sheets
+ * @param splitterMax maximum number of sheets in a volume
+ */
+ public EvenSizeVolumeSplitterCalculator(int sheets, int splitterMax) {
+ this(sheets, splitterMax, 0);
+ }
+ /**
+ * @param sheets
+ * @param splitterMax
+ * @param volumeOffset
+ */
+ public EvenSizeVolumeSplitterCalculator(int sheets, int splitterMax, int volumeOffset) {
+ volumes = (int)Math.ceil(sheets/(double)splitterMax) + volumeOffset;
+ this.sheets = sheets;
+ this.breakpoint = (int)Math.ceil(sheets/(double)volumes);
+ int slv = sheets - (breakpoint * (volumes - 1));
+ this.volsWithBpSheets = volumes - (breakpoint - slv);
+ this.sheetsPerVolumeBreakpoint = breakpoint*volsWithBpSheets;
+ }
+
+ /**
+ * Tests if the supplied sheetIndex is a breakpoint. This sheetIndex counts all sheets,
+ * including sheets inserted in volume splitting.
+ * @param sheetIndex sheet index, one based
+ * @return returns true if the sheet is a breakpoint, false otherwise
+ * @throws IndexOutOfBoundsException if sheetIndex is outside of agreed boundaries
+ */
+ public boolean isBreakpoint(int sheetIndex) {
+ if (sheetIndex<1) {
+ throw new IndexOutOfBoundsException("Sheet index must be greater than zero: " + sheetIndex);
+ }
+ if (sheetIndex>sheets) {
+ throw new IndexOutOfBoundsException("Sheet index must not exceed agreed value.");
+ }
+ if (sheetIndexsheets) {
+ throw new IndexOutOfBoundsException("Sheet index must not exceed agreed value.");
+ }
+ if (sheetIndex context;
+ //private boolean firstRow;
+ private final StateObject state;
+ //private CrossReferences refs;
+ //private StringFilter filter;
+
+ private final BrailleTranslator translator;
+ //private FilterLocale locale;
+ //private BlockHandler bh;
+
+ private int blockIndent;
+ private Stack blockIndentParent;
+ private ListItem listItem;
+
+ // TODO: fix recursive keep problem
+ // TODO: Implement SpanProperites
+ // TODO: Implement floating elements
+ /**
+ * Creates a new formatter
+ */
+ public FormatterImpl(BrailleTranslator translator) {
+ //this.filters = builder.filtersFactory.getDefault();
+ this.context = new Stack();
+ this.leftMargin = 0;
+ this.rightMargin = 0;
+ this.flowStruct = new BlockStructImpl(); //masters
+ this.state = new StateObject();
+ //this.filter = null;
+ //this.refs = null;
+ this.listItem = null;
+ this.translator = translator;
+ }
+
+ /*
+ public void setLocale(FilterLocale locale) {
+ state.assertUnopened();
+ this.locale = locale;
+ filter = null;
+ }*/
+
+ public void open() {
+ state.assertUnopened();
+ //bh = new BlockHandler(getDefaultFilter());
+ this.blockIndent = 0;
+ this.blockIndentParent = new Stack();
+ blockIndentParent.add(0);
+ state.open();
+ }
+
+ public void addLayoutMaster(String name, LayoutMaster master) {
+ flowStruct.addLayoutMaster(name, master);
+ }
+
+ public void addChars(CharSequence c, TextProperties p) {
+ state.assertOpen();
+ assert context.size()!=0;
+ if (context.size()==0) return;
+ BlockImpl bl = flowStruct.getCurrentSequence().getCurrentBlock();
+ if (listItem!=null) {
+ //append to this block
+ bl.setListItem(listItem.getLabel(), listItem.getType());
+ //list item has been used now, discard
+ listItem = null;
+ }
+ bl.addChars(c, p, context.peek());
+ }
+ // END Using BlockHandler
+
+ public void insertMarker(Marker m) {
+ //FIXME: this does not work
+ state.assertOpen();
+ flowStruct.getCurrentSequence().getCurrentBlock().addMarker(m);
+ }
+
+ public void startBlock(BlockProperties p) {
+ startBlock(p, null);
+ }
+
+ public void startBlock(BlockProperties p, String blockId) {
+ state.assertOpen();
+ leftMargin += p.getLeftMargin();
+ rightMargin += p.getRightMargin();
+ if (context.size()>0) {
+ addToBlockIndent(context.peek().getBlockIndent());
+ }
+ RowDataProperties rdp = new RowDataProperties.Builder(
+ getTranslator(), flowStruct.getCurrentSequence().getLayoutMaster()).
+ blockIndent(blockIndent).
+ blockIndentParent(blockIndentParent.peek()).
+ leftMargin(leftMargin).
+ rightMargin(rightMargin).
+ build();
+ BlockImpl c = flowStruct.getCurrentSequence().newBlock(blockId, rdp);
+ if (context.size()>0) {
+ if (context.peek().getListType()!=FormattingTypes.ListStyle.NONE) {
+ String listLabel;
+ switch (context.peek().getListType()) {
+ case OL:
+ listLabel = context.peek().nextListNumber()+""; break;
+ case UL:
+ listLabel = "•";
+ break;
+ case PL: default:
+ listLabel = "";
+ }
+ listItem = new ListItem(listLabel, context.peek().getListType());
+ }
+ }
+ c.addSpaceBefore(p.getTopMargin());
+ c.setBreakBeforeType(p.getBreakBeforeType());
+ c.setKeepType(p.getKeepType());
+ c.setKeepWithNext(p.getKeepWithNext());
+ c.setIdentifier(p.getIdentifier());
+ c.setKeepWithNextSheets(p.getKeepWithNextSheets());
+ c.setVerticalPosition(p.getVerticalPosition());
+ context.push(p);
+ //firstRow = true;
+ }
+
+ public void endBlock() {
+ state.assertOpen();
+ if (listItem!=null) {
+ addChars("", new TextProperties.Builder(null).build());
+ }
+ BlockProperties p = context.pop();
+ flowStruct.getCurrentSequence().getCurrentBlock().addSpaceAfter(p.getBottomMargin());
+ flowStruct.getCurrentSequence().getCurrentBlock().setKeepWithPreviousSheets(p.getKeepWithPreviousSheets());
+ leftMargin -= p.getLeftMargin();
+ rightMargin -= p.getRightMargin();
+ if (context.size()>0) {
+ Keep keep = context.peek().getKeepType();
+ int next = context.peek().getKeepWithNext();
+ subtractFromBlockIndent(context.peek().getBlockIndent());
+ RowDataProperties rdp = new RowDataProperties.Builder(
+ getTranslator(), flowStruct.getCurrentSequence().getLayoutMaster()).
+ blockIndent(blockIndent).
+ blockIndentParent(blockIndentParent.peek()).
+ leftMargin(leftMargin).
+ rightMargin(rightMargin).
+ build();
+ BlockImpl c = flowStruct.getCurrentSequence().newBlock(null, rdp);
+ c.setKeepType(keep);
+ c.setKeepWithNext(next);
+ }
+ //firstRow = true;
+ }
+
+ public void newSequence(SequenceProperties p) {
+ state.assertOpen();
+ flowStruct.newSequence(p);
+ }
+
+ public void insertLeader(Leader leader) {
+ state.assertOpen();
+ flowStruct.getCurrentSequence().getCurrentBlock().insertLeader(leader);
+ }
+
+ public void newLine() {
+ state.assertOpen();
+ flowStruct.getCurrentSequence().getCurrentBlock().newLine(leftMargin + context.peek().getTextIndent());
+ }
+
+ /**
+ * Gets the resulting data structure
+ * @return returns the data structure
+ * @throws IllegalStateException if not closed
+ */
+ public BlockStruct getFlowStruct() {
+ state.assertClosed();
+ return flowStruct;
+ }
+
+ public void close() throws IOException {
+ if (state.isClosed()) {
+ return;
+ }
+ state.assertOpen();
+ state.close();
+ }
+
+ public void endFloat() {
+ state.assertOpen();
+ // TODO implement float
+ throw new UnsupportedOperationException("Not implemented");
+ }
+
+ public void insertAnchor(String ref) {
+ state.assertOpen();
+ // TODO implement anchor
+ throw new UnsupportedOperationException("Not implemented");
+ }
+
+ public void startFloat(String id) {
+ state.assertOpen();
+ // TODO implement float
+ throw new UnsupportedOperationException("Not implemented");
+ }
+
+/*
+ public FilterFactory getFilterFactory() {
+ return filtersFactory;
+ }*/
+
+/*
+ public FilterLocale getFilterLocale() {
+ return locale;
+ }*/
+
+/*
+ public StringFilter getDefaultFilter() {
+ if (filter == null) {
+ filter = filtersFactory.newStringFilter(locale);
+ }
+ return filter;
+ }*/
+
+ private void addToBlockIndent(int value) {
+ blockIndentParent.push(blockIndent);
+ blockIndent += value;
+ }
+
+ private void subtractFromBlockIndent(int value) {
+ int test = blockIndentParent.pop();
+ blockIndent -= value;
+ assert blockIndent==test;
+ }
+
+ public void insertReference(String identifier, NumeralStyle numeralStyle) {
+ flowStruct.getCurrentSequence().getCurrentBlock().insertReference(identifier, numeralStyle);
+ }
+
+
+ public BrailleTranslator getTranslator() {
+ return translator;
+ }
+
+ public Iterable getVolumes(VolumeContentFormatter vcf) {
+ PaginatorImpl paginator = new PaginatorImpl();
+ paginator.open(getTranslator(), getFlowStruct().getBlockSequenceIterable());
+
+ BookStruct bookStruct = new BookStruct(paginator, vcf, getTranslator());
+ return bookStruct.getVolumes();
+ }
+
+}
diff --git a/src/org/daisy/dotify/formatter/impl/LeaderSegment.java b/src/org/daisy/dotify/formatter/impl/LeaderSegment.java
new file mode 100644
index 00000000..c79fdc14
--- /dev/null
+++ b/src/org/daisy/dotify/formatter/impl/LeaderSegment.java
@@ -0,0 +1,19 @@
+package org.daisy.dotify.formatter.impl;
+
+import org.daisy.dotify.api.formatter.Leader;
+
+class LeaderSegment extends Leader implements Segment {
+
+ protected LeaderSegment(Builder builder) {
+ super(builder);
+ }
+
+ LeaderSegment(Leader leader) {
+ super(leader);
+ }
+
+ public SegmentType getSegmentType() {
+ return SegmentType.Leader;
+ }
+
+}
diff --git a/src/org/daisy/dotify/formatter/impl/MarkerSegment.java b/src/org/daisy/dotify/formatter/impl/MarkerSegment.java
new file mode 100644
index 00000000..24c78095
--- /dev/null
+++ b/src/org/daisy/dotify/formatter/impl/MarkerSegment.java
@@ -0,0 +1,15 @@
+package org.daisy.dotify.formatter.impl;
+
+import org.daisy.dotify.api.formatter.Marker;
+
+class MarkerSegment extends Marker implements Segment {
+
+ MarkerSegment(Marker m) {
+ super(m.getName(), m.getValue());
+ }
+
+ public SegmentType getSegmentType() {
+ return SegmentType.Marker;
+ }
+
+}
diff --git a/src/org/daisy/dotify/formatter/impl/NewLineSegment.java b/src/org/daisy/dotify/formatter/impl/NewLineSegment.java
new file mode 100644
index 00000000..89c17426
--- /dev/null
+++ b/src/org/daisy/dotify/formatter/impl/NewLineSegment.java
@@ -0,0 +1,19 @@
+package org.daisy.dotify.formatter.impl;
+
+
+class NewLineSegment implements Segment {
+ private final int leftIndent;
+
+ public NewLineSegment(int leftIndent) {
+ this.leftIndent = leftIndent;
+ }
+
+ public int getLeftIndent() {
+ return leftIndent;
+ }
+
+ public SegmentType getSegmentType() {
+ return SegmentType.NewLine;
+ }
+
+}
\ No newline at end of file
diff --git a/src/org/daisy/dotify/formatter/impl/PageCopy.java b/src/org/daisy/dotify/formatter/impl/PageCopy.java
new file mode 100644
index 00000000..5e8a131f
--- /dev/null
+++ b/src/org/daisy/dotify/formatter/impl/PageCopy.java
@@ -0,0 +1,52 @@
+package org.daisy.dotify.formatter.impl;
+
+import java.util.List;
+
+import org.daisy.dotify.api.formatter.Page;
+import org.daisy.dotify.api.formatter.PageSequence;
+
+
+/**
+ * Provides a method for creating a shallow copy of a page.
+ * The copy can have another parent than the original page.
+ *
+ * @author Joel Håkansson
+ */
+class PageCopy implements Page {
+ private final Page p;
+ private final PageSequence parent;
+
+ PageCopy(Page p, PageSequence parent) {
+ this.p = p;
+ this.parent = parent;
+ }
+/*
+ public List getMarkers() {
+ return p.getMarkers();
+ }
+
+ public List getContentMarkers() {
+ return p.getContentMarkers();
+ }
+*/
+ public List getRows() {
+ return p.getRows();
+ }
+
+ public int getPageIndex() {
+ return p.getPageIndex();
+ }
+
+ public PageSequence getParent() {
+ return parent;
+ }
+
+ public boolean allowsVolumeBreak() {
+ return p.allowsVolumeBreak();
+ }
+
+ public int keepPreviousSheets() {
+ return p.keepPreviousSheets();
+ }
+
+}
diff --git a/src/org/daisy/dotify/formatter/impl/PageImpl.java b/src/org/daisy/dotify/formatter/impl/PageImpl.java
new file mode 100644
index 00000000..fa91c798
--- /dev/null
+++ b/src/org/daisy/dotify/formatter/impl/PageImpl.java
@@ -0,0 +1,304 @@
+package org.daisy.dotify.formatter.impl;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.daisy.dotify.api.formatter.CompoundField;
+import org.daisy.dotify.api.formatter.CurrentPageField;
+import org.daisy.dotify.api.formatter.Field;
+import org.daisy.dotify.api.formatter.LayoutMaster;
+import org.daisy.dotify.api.formatter.Marker;
+import org.daisy.dotify.api.formatter.MarkerReferenceField;
+import org.daisy.dotify.api.formatter.Page;
+import org.daisy.dotify.api.formatter.PageTemplate;
+import org.daisy.dotify.api.formatter.Row;
+import org.daisy.dotify.api.translator.BrailleTranslator;
+import org.daisy.dotify.api.translator.BrailleTranslatorResult;
+import org.daisy.dotify.api.translator.TextBorderStyle;
+import org.daisy.dotify.tools.StringTools;
+
+
+
+/**
+ * Provides a page object.
+ *
+ * @author Joel Håkansson
+ */
+class PageImpl implements Page {
+ private String marginCharacter = null;
+ private PageSequenceImpl parent;
+ private ArrayList rows;
+ private ArrayList markers;
+ private final int pageIndex;
+ private final int flowHeight;
+ private int contentMarkersBegin;
+ private boolean isVolBreak;
+ private boolean isVolBreakAllowed;
+ private int keepPreviousSheets;
+
+ public PageImpl(PageSequenceImpl parent, int pageIndex) {
+ this.rows = new ArrayList();
+ this.markers = new ArrayList();
+ this.pageIndex = pageIndex;
+ contentMarkersBegin = 0;
+ this.parent = parent;
+ PageTemplate template = parent.getLayoutMaster().getTemplate(pageIndex+1);
+ this.flowHeight = parent.getLayoutMaster().getPageHeight() - template.getHeaderHeight() - template.getFooterHeight() - (parent.getLayoutMaster().getFrame() != null ? 2 : 0);
+ this.isVolBreak = false;
+ this.isVolBreakAllowed = true;
+ this.keepPreviousSheets = 0;
+ }
+
+ public void newRow(Row r) {
+ if (rowsOnPage()==0) {
+ contentMarkersBegin = markers.size();
+ }
+ rows.add(r);
+ markers.addAll(r.getMarkers());
+ }
+
+ /**
+ * Gets the number of rows on this page
+ * @return returns the number of rows on this page
+ */
+ public int rowsOnPage() {
+ return rows.size();
+ }
+
+ public void addMarkers(List m) {
+ markers.addAll(m);
+ }
+
+ /**
+ * Get all markers for this page
+ * @return returns a list of all markers on a page
+ */
+ public List getMarkers() {
+ return markers;
+ }
+
+ /**
+ * Get markers for this page excluding markers before text content
+ * @return returns a list of markers on a page
+ */
+ public List getContentMarkers() {
+ return markers.subList(contentMarkersBegin, markers.size());
+ }
+
+ private String getMarginCharacter() {
+ // lazy init
+ if (marginCharacter == null) {
+ marginCharacter = getParent().getTranslator().translate(" ").getTranslatedRemainder();
+ }
+ return marginCharacter;
+ }
+
+ public List getRows() {
+
+ try {
+ TextBorderStyle frame = getParent().getLayoutMaster().getFrame();
+ if (frame == null) {
+ frame = TextBorderStyle.NONE;
+ }
+ ArrayList ret = new ArrayList();
+ {
+ LayoutMaster lm = getParent().getLayoutMaster();
+ int pagenum = getPageIndex() + 1;
+ PageTemplate t = lm.getTemplate(pagenum);
+ BrailleTranslator filter = getParent().getTranslator();
+ ret.addAll(renderFields(lm, t.getHeader(), filter));
+ ret.addAll(rows);
+ if (t.getFooterHeight() > 0 || frame != TextBorderStyle.NONE) {
+ while (ret.size() < getFlowHeight() + t.getHeaderHeight()) {
+ ret.add(new Row());
+ }
+ ret.addAll(renderFields(lm, t.getFooter(), filter));
+ }
+ }
+ ArrayList ret2 = new ArrayList();
+ {
+ final int pagenum = getPageIndex() + 1;
+ LayoutMaster lm = getParent().getLayoutMaster();
+ TextBorder tb = null;
+
+ int fsize = frame.getLeftBorder().length() + frame.getRightBorder().length();
+ final int pageMargin = ((pagenum % 2 == 0) ? lm.getOuterMargin() : lm.getInnerMargin());
+ int w = getParent().getLayoutMaster().getFlowWidth() + fsize + pageMargin;
+
+ tb = new TextBorder.Builder(w, getMarginCharacter())
+ .style(frame)
+ .outerLeftMargin(StringTools.fill(getMarginCharacter(), pageMargin))
+ .build();
+ if (!TextBorderStyle.NONE.equals(frame)) {
+ ret2.add(tb.getTopBorder());
+ }
+ String res;
+
+ for (Row row : ret) {
+ res = "";
+ if (row.getChars().length() > 0) {
+ // remove trailing whitespace
+ String chars = row.getChars().replaceAll("\\s*\\z", "");
+ //if (!TextBorderStyle.NONE.equals(frame)) {
+ res = tb.addBorderToRow(chars,
+ TextBorder.Align.valueOf(row.getAlignment().toString()),
+ StringTools.fill(getMarginCharacter(), row.getLeftMargin()),
+ StringTools.fill(getMarginCharacter(), row.getRightMargin()),
+ TextBorderStyle.NONE.equals(frame));
+ //} else {
+ // res = StringTools.fill(getMarginCharacter(), pageMargin + row.getLeftMargin()) + chars;
+ //}
+ } else {
+ if (!TextBorderStyle.NONE.equals(frame)) {
+ res = tb.addBorderToRow("",
+ TextBorder.Align.valueOf(row.getAlignment().toString()),
+ StringTools.fill(getMarginCharacter(), row.getLeftMargin()),
+ StringTools.fill(getMarginCharacter(), row.getRightMargin()), false);
+ } else {
+ res = "";
+ }
+ }
+ int rowWidth = StringTools.length(res) + pageMargin;
+ String r = res;
+ if (rowWidth > getParent().getLayoutMaster().getPageWidth()) {
+ throw new PaginatorException("Row is too long (" + rowWidth + "/" + getParent().getLayoutMaster().getPageWidth() + ") '" + res + "'");
+ }
+ ret2.add(r);
+ }
+ if (!TextBorderStyle.NONE.equals(frame)) {
+ ret2.add(tb.getBottomBorder());
+ }
+ }
+ return ret2;
+ } catch (PaginatorException e) {
+ throw new RuntimeException("Cannot render header/footer", e);
+ }
+ }
+
+ /**
+ * Get the number for the page
+ * @return returns the page index in the sequence (zero based)
+ */
+ public int getPageIndex() {
+ return pageIndex;
+ }
+
+ public PageSequenceImpl getParent() {
+ return parent;
+ }
+
+ /**
+ * Gets the flow height for this page, i.e. the number of rows available for text flow
+ * @return returns the flow height
+ */
+ public int getFlowHeight() {
+ return flowHeight;
+ }
+
+ public boolean isVolumeBreak() {
+ return isVolBreak;
+ }
+
+ public void setVolumeBreak(boolean value) {
+ isVolBreak = value;
+ }
+
+
+ private List renderFields(LayoutMaster lm, List> fields, BrailleTranslator translator) throws PaginatorException {
+ ArrayList ret = new ArrayList();
+ for (List row : fields) {
+ try {
+ ret.add(new Row(distribute(row, lm.getFlowWidth(), translator.translate(" ").getTranslatedRemainder(), translator)));
+ } catch (PaginatorToolsException e) {
+ throw new PaginatorException("Error while rendering header", e);
+ }
+ }
+ return ret;
+ }
+
+ private String distribute(List chunks, int width, String padding, BrailleTranslator translator) throws PaginatorToolsException {
+ ArrayList chunkF = new ArrayList();
+ for (Field f : chunks) {
+ BrailleTranslatorResult btr = translator.translate(resolveField(f, this).replaceAll("\u00ad", ""));
+ chunkF.add(btr.getTranslatedRemainder());
+ }
+ return PaginatorTools.distribute(chunkF, width, padding, PaginatorTools.DistributeMode.EQUAL_SPACING);
+ }
+
+ private static String resolveField(Field field, PageImpl p) {
+ if (field instanceof CompoundField) {
+ return resolveCompoundField((CompoundField)field, p);
+ } else if (field instanceof MarkerReferenceField) {
+ MarkerReferenceField f2 = (MarkerReferenceField)field;
+ return findMarker(p, f2);
+ } else if (field instanceof CurrentPageField) {
+ return resolveCurrentPageField((CurrentPageField)field, p);
+ } else {
+ return field.toString();
+ }
+ }
+
+ private static String resolveCompoundField(CompoundField f, PageImpl p) {
+ StringBuffer sb = new StringBuffer();
+ for (Field f2 : f) {
+ sb.append(resolveField(f2, p));
+ }
+ return sb.toString();
+ }
+
+ private static String findMarker(PageImpl page, MarkerReferenceField markerRef) {
+ int dir = 1;
+ int index = 0;
+ int count = 0;
+ List m;
+ if (markerRef.getSearchScope() == MarkerReferenceField.MarkerSearchScope.PAGE_CONTENT) {
+ m = page.getContentMarkers();
+ } else {
+ m = page.getMarkers();
+ }
+ if (markerRef.getSearchDirection() == MarkerReferenceField.MarkerSearchDirection.BACKWARD) {
+ dir = -1;
+ index = m.size()-1;
+ }
+ while (count < m.size()) {
+ Marker m2 = m.get(index);
+ if (m2.getName().equals(markerRef.getName())) {
+ return m2.getValue();
+ }
+ index += dir;
+ count++;
+ }
+ if (markerRef.getSearchScope() == MarkerReferenceField.MarkerSearchScope.SEQUENCE) {
+ int nextPage = page.getPageIndex() - page.getParent().getPageNumberOffset() + dir;
+ //System.out.println("Next page: "+page.getPageIndex() + " | " + nextPage);
+ if (nextPage < page.getParent().getPageCount() && nextPage >= 0) {
+ PageImpl next = page.getParent().getPage(nextPage);
+ return findMarker(next, markerRef);
+ }
+ }
+ return "";
+ }
+
+ private static String resolveCurrentPageField(CurrentPageField f, Page p) {
+ //TODO: include page number offset?
+ int pagenum = p.getPageIndex() + 1;
+ return f.getStyle().format(pagenum);
+ }
+
+ void setKeepWithPreviousSheets(int value) {
+ keepPreviousSheets = Math.max(value, keepPreviousSheets);
+ }
+
+ void setAllowsVolumeBreak(boolean value) {
+ this.isVolBreakAllowed = value;
+ }
+
+ public boolean allowsVolumeBreak() {
+ return isVolBreakAllowed;
+ }
+
+ public int keepPreviousSheets() {
+ return keepPreviousSheets;
+ }
+
+}
diff --git a/src/org/daisy/dotify/formatter/impl/PageNumberReferenceSegment.java b/src/org/daisy/dotify/formatter/impl/PageNumberReferenceSegment.java
new file mode 100644
index 00000000..101dbcc8
--- /dev/null
+++ b/src/org/daisy/dotify/formatter/impl/PageNumberReferenceSegment.java
@@ -0,0 +1,38 @@
+package org.daisy.dotify.formatter.impl;
+
+import org.daisy.dotify.api.formatter.NumeralField.NumeralStyle;
+
+class PageNumberReferenceSegment implements Segment {
+ private final String refid;
+ private final NumeralStyle style;
+
+ public PageNumberReferenceSegment(String refid, NumeralStyle style) {
+ this.refid = refid;
+ this.style = style;
+ }
+
+ /**
+ * Gets the identifier to the reference location.
+ * @return returns the reference identifier
+ */
+ public String getRefId() {
+ return refid;
+ }
+
+ /**
+ * Gets the numeral style for this page number reference
+ * @return returns the numeral style
+ */
+ public NumeralStyle getNumeralStyle() {
+ return style;
+ }
+
+ public boolean canContainEventObjects() {
+ return false;
+ }
+
+ public SegmentType getSegmentType() {
+ return SegmentType.Reference;
+ }
+
+}
diff --git a/src/org/daisy/dotify/formatter/impl/PageSequenceCopy.java b/src/org/daisy/dotify/formatter/impl/PageSequenceCopy.java
new file mode 100644
index 00000000..025f5766
--- /dev/null
+++ b/src/org/daisy/dotify/formatter/impl/PageSequenceCopy.java
@@ -0,0 +1,65 @@
+package org.daisy.dotify.formatter.impl;
+
+import java.util.Stack;
+
+import org.daisy.dotify.api.formatter.LayoutMaster;
+import org.daisy.dotify.api.formatter.Page;
+import org.daisy.dotify.api.formatter.PageSequence;
+
+/**
+ * Provides a method for creating a shallow copy of a PageSequence.
+ *
+ * @author Joel Håkansson
+ *
+ */
+class PageSequenceCopy implements PageSequence {
+ private final Stack pages;
+ private final LayoutMaster master;
+ //private final int pageOffset;
+ //private final FormatterFactory formatterFactory;
+ //private Formatter formatter;
+
+ PageSequenceCopy(LayoutMaster master) { //, int pageOffset, FormatterFactory formatterFactory) {
+ this.pages = new Stack();
+ this.master = master;
+ //this.pageOffset = pageOffset;
+ //this.formatterFactory = formatterFactory;
+ //this.formatter = null;
+ }
+
+ void addPage(Page p) {
+ pages.add(new PageCopy(p, this));
+ }
+
+ public LayoutMaster getLayoutMaster() {
+ return master;
+ }
+
+ public int getPageCount() {
+ return pages.size();
+ }
+
+ public Page getPage(int index) {
+ return pages.get(index);
+ }
+/*
+ public int getPageNumberOffset() {
+ return pageOffset;
+ }
+
+ public FormatterFactory getFormatterFactory() {
+ return formatterFactory;
+ }
+
+ public Formatter getFormatter() {
+ if (formatter == null) {
+ formatter = formatterFactory.newFormatter();
+ }
+ return formatter;
+ }
+*/
+ public Iterable extends Page> getPages() {
+ return pages;
+ }
+
+}
diff --git a/src/org/daisy/dotify/formatter/impl/PageSequenceImpl.java b/src/org/daisy/dotify/formatter/impl/PageSequenceImpl.java
new file mode 100644
index 00000000..495fbf48
--- /dev/null
+++ b/src/org/daisy/dotify/formatter/impl/PageSequenceImpl.java
@@ -0,0 +1,125 @@
+package org.daisy.dotify.formatter.impl;
+
+import java.util.HashMap;
+import java.util.Stack;
+
+import org.daisy.dotify.api.formatter.LayoutMaster;
+import org.daisy.dotify.api.formatter.Page;
+import org.daisy.dotify.api.formatter.PageSequence;
+import org.daisy.dotify.api.formatter.Row;
+import org.daisy.dotify.api.translator.BrailleTranslator;
+
+class PageSequenceImpl implements PageSequence {
+ private final Stack pages;
+ private final LayoutMaster master;
+ private final int pagesOffset;
+ private final HashMap pageReferences;
+ private final BrailleTranslator translator;
+ private int keepNextSheets;
+ private PageImpl nextPage;
+
+ PageSequenceImpl(LayoutMaster master, int pagesOffset, HashMap pageReferences, BrailleTranslator translator) {
+ this.pages = new Stack();
+ this.master = master;
+ this.pagesOffset = pagesOffset;
+ this.pageReferences = pageReferences;
+ this.translator = translator;
+ this.keepNextSheets = 0;
+ this.nextPage = null;
+ }
+
+ int rowsOnCurrentPage() {
+ return currentPage().rowsOnPage();
+ }
+
+ void newPage() {
+ if (nextPage!=null) {
+ pages.push(nextPage);
+ nextPage = null;
+ } else {
+ pages.push(new PageImpl(this, pages.size()+pagesOffset));
+ }
+ if (keepNextSheets>0) {
+ currentPage().setAllowsVolumeBreak(false);
+ }
+ if (!getLayoutMaster().duplex() || getPageCount()%2==0) {
+ if (keepNextSheets>0) {
+ keepNextSheets--;
+ }
+ }
+ }
+
+ void newPageOnRow() {
+ if (nextPage!=null) {
+ //if new page is already in buffer, flush it.
+ newPage();
+ }
+ nextPage = new PageImpl(this, pages.size()+pagesOffset);
+ }
+
+ void setKeepWithPreviousSheets(int value) {
+ currentPage().setKeepWithPreviousSheets(value);
+ }
+
+ void setKeepWithNextSheets(int value) {
+ keepNextSheets = Math.max(value, keepNextSheets);
+ if (keepNextSheets>0) {
+ currentPage().setAllowsVolumeBreak(false);
+ }
+ }
+
+ public int getPageNumberOffset() {
+ return pagesOffset;
+ }
+
+ public int getPageCount() {
+ return pages.size();
+ }
+
+ public PageImpl getPage(int index) {
+ return pages.get(index);
+ }
+
+ PageImpl currentPage() {
+ if (nextPage!=null) {
+ return nextPage;
+ } else {
+ return pages.peek();
+ }
+ }
+
+ void newRow(Row row) {
+ if (currentPage().rowsOnPage()>=currentPage().getFlowHeight() || nextPage!=null) {
+ newPage();
+ }
+ currentPage().newRow(row);
+ }
+
+ void newRow(Row row, String id) {
+ newRow(row);
+ insertIdentifier(id);
+ }
+
+ public LayoutMaster getLayoutMaster() {
+ return master;
+ }
+/*
+ public Iterator iterator() {
+ return (Iterator)pages.iterator();
+ }*/
+
+ public Iterable extends Page> getPages() {
+ return pages;
+ }
+
+ void insertIdentifier(String id) {
+ if (pageReferences.put(id, currentPage())!=null) {
+ throw new IllegalArgumentException("Identifier not unique: " + id);
+ }
+ }
+
+ public BrailleTranslator getTranslator() {
+ return translator;
+ }
+
+}
diff --git a/src/org/daisy/dotify/formatter/impl/PageStructCopy.java b/src/org/daisy/dotify/formatter/impl/PageStructCopy.java
new file mode 100644
index 00000000..fe73cd3d
--- /dev/null
+++ b/src/org/daisy/dotify/formatter/impl/PageStructCopy.java
@@ -0,0 +1,56 @@
+package org.daisy.dotify.formatter.impl;
+
+import java.util.Iterator;
+import java.util.Stack;
+
+import org.daisy.dotify.api.formatter.Page;
+import org.daisy.dotify.api.formatter.PageSequence;
+
+
+class PageStructCopy implements Iterable {
+ private final Stack seq;
+ private PageSequence originalSeq;
+ private int sheets;
+ private int pagesInSeq;
+
+ public PageStructCopy() {
+ this.seq = new Stack();
+ this.sheets = 0;
+ this.pagesInSeq = 0;
+ }
+
+ public void addPage(Page p) {
+ if (seq.empty() || originalSeq != p.getParent()) {
+ originalSeq = p.getParent();
+ seq.add(new PageSequenceCopy(originalSeq.getLayoutMaster())); //, originalSeq.getPageNumberOffset(), originalSeq.getFormatterFactory()));
+ pagesInSeq = 0;
+ }
+ ((PageSequenceCopy)seq.peek()).addPage(p);
+ pagesInSeq++;
+ if (!p.getParent().getLayoutMaster().duplex() || pagesInSeq % 2 == 1) {
+ sheets++;
+ }
+ }
+
+ public int countSheets() {
+ return sheets;
+ }
+
+ /**
+ * Counts the total number of sheets if this page were added
+ * @param p
+ * @return
+ */
+ public int countSheets(Page p) {
+ int i = 0;
+ if (originalSeq != p.getParent() || !p.getParent().getLayoutMaster().duplex() || pagesInSeq % 2 == 0) {
+ i = 1;
+ }
+ return sheets + i;
+ }
+
+ public Iterator iterator() {
+ return seq.iterator();
+ }
+
+}
\ No newline at end of file
diff --git a/src/org/daisy/dotify/formatter/impl/PageStructImpl.java b/src/org/daisy/dotify/formatter/impl/PageStructImpl.java
new file mode 100644
index 00000000..d9eb8f1d
--- /dev/null
+++ b/src/org/daisy/dotify/formatter/impl/PageStructImpl.java
@@ -0,0 +1,92 @@
+package org.daisy.dotify.formatter.impl;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Stack;
+
+import org.daisy.dotify.api.formatter.LayoutMaster;
+import org.daisy.dotify.api.formatter.Marker;
+import org.daisy.dotify.api.formatter.Page;
+import org.daisy.dotify.api.formatter.PageSequence;
+import org.daisy.dotify.api.formatter.PageStruct;
+import org.daisy.dotify.api.formatter.Row;
+import org.daisy.dotify.api.translator.BrailleTranslator;
+
+class PageStructImpl extends Stack implements PageStruct {
+ //private final StringFilter filters;
+ HashMap pageReferences;
+
+
+ public PageStructImpl() {
+ //this.filters = filters;
+ this.pageReferences = new HashMap();
+ }
+
+ /*public StringFilter getFilter() {
+ return filters;
+ }*/
+
+ private static final long serialVersionUID = 2591429059130956153L;
+
+
+ public Iterable extends PageSequence> getContents() {
+ return this;
+ }
+
+ public Page getPage(String refid) {
+ return pageReferences.get(refid);
+ }
+
+ void newSequence(LayoutMaster master, int pagesOffset, BrailleTranslator translator) {
+ this.push(new PageSequenceImpl(master, pagesOffset, this.pageReferences, translator));
+ }
+
+ void newSequence(LayoutMaster master, BrailleTranslator translator) {
+ if (this.size()==0) {
+ newSequence(master, 0, translator);
+ } else {
+ int next = currentSequence().currentPage().getPageIndex()+1;
+ if (currentSequence().getLayoutMaster().duplex() && (next % 2)==1) {
+ next++;
+ }
+ newSequence(master, next, translator);
+ }
+ }
+
+ PageSequenceImpl currentSequence() {
+ return this.peek();
+ }
+
+ PageImpl currentPage() {
+ return currentSequence().currentPage();
+ }
+
+ void newPage() {
+ currentSequence().newPage();
+ }
+
+ void newRow(Row row) {
+ currentSequence().newRow(row);
+ }
+
+ void newRow(Row row, String id) {
+ currentSequence().newRow(row, id);
+ }
+
+ void insertMarkers(List m) {
+ currentSequence().currentPage().addMarkers(m);
+ }
+
+ void insertIdentifier(String id) {
+ currentSequence().insertIdentifier(id);
+ }
+
+ int countRows() {
+ return currentPage().rowsOnPage();
+ }
+
+ int getFlowHeight() {
+ return currentPage().getFlowHeight();
+ }
+
+}
\ No newline at end of file
diff --git a/src/org/daisy/dotify/formatter/impl/PageTools.java b/src/org/daisy/dotify/formatter/impl/PageTools.java
new file mode 100644
index 00000000..836b37f6
--- /dev/null
+++ b/src/org/daisy/dotify/formatter/impl/PageTools.java
@@ -0,0 +1,28 @@
+package org.daisy.dotify.formatter.impl;
+
+import org.daisy.dotify.api.formatter.LayoutMaster;
+import org.daisy.dotify.api.formatter.PageSequence;
+
+/**
+ * PageTools is a utility class for simple operations related to pages.
+ *
+ * @author Joel Håkansson
+ */
+class PageTools {
+
+ // Default constructor is private as this class is not intended to be instantiated.
+ private PageTools() { }
+
+ static int countSheets(Iterable extends PageSequence> mf) {
+ int sheets = 0;
+ for (PageSequence seq : mf) {
+ LayoutMaster lm = seq.getLayoutMaster();
+ if (lm.duplex()) {
+ sheets += (int)Math.ceil(seq.getPageCount()/2d);
+ } else {
+ sheets += seq.getPageCount();
+ }
+ }
+ return sheets;
+ }
+}
\ No newline at end of file
diff --git a/src/org/daisy/dotify/formatter/impl/PaginatorException.java b/src/org/daisy/dotify/formatter/impl/PaginatorException.java
new file mode 100644
index 00000000..b2697146
--- /dev/null
+++ b/src/org/daisy/dotify/formatter/impl/PaginatorException.java
@@ -0,0 +1,25 @@
+package org.daisy.dotify.formatter.impl;
+
+class PaginatorException extends Exception {
+
+ /**
+ *
+ */
+ private static final long serialVersionUID = 8015133306865945283L;
+
+ PaginatorException() {
+ }
+
+ PaginatorException(String message) {
+ super(message);
+ }
+
+ PaginatorException(Throwable cause) {
+ super(cause);
+ }
+
+ PaginatorException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+}
diff --git a/src/org/daisy/dotify/formatter/impl/PaginatorImpl.java b/src/org/daisy/dotify/formatter/impl/PaginatorImpl.java
new file mode 100644
index 00000000..4293a3d3
--- /dev/null
+++ b/src/org/daisy/dotify/formatter/impl/PaginatorImpl.java
@@ -0,0 +1,160 @@
+package org.daisy.dotify.formatter.impl;
+
+import java.io.IOException;
+
+import org.daisy.dotify.api.formatter.Block;
+import org.daisy.dotify.api.formatter.BlockContentManager;
+import org.daisy.dotify.api.formatter.BlockSequence;
+import org.daisy.dotify.api.formatter.CrossReferences;
+import org.daisy.dotify.api.formatter.PageStruct;
+import org.daisy.dotify.api.formatter.Row;
+import org.daisy.dotify.api.translator.BrailleTranslator;
+import org.daisy.dotify.tools.StateObject;
+
+/**
+ * Provides an implementation of the paginator interface. This class should
+ * not be used directly, use the corresponding factory methods instead.
+ *
+ * @author Joel Håkansson
+ */
+class PaginatorImpl {
+ private StateObject state;
+ private BrailleTranslator translator;
+ private Iterable fs;
+ //private HashMap templates;
+
+ public PaginatorImpl() { //HashMap templates
+
+ this.state = new StateObject();
+ //this.templates = templates;
+ }
+
+ public void open(BrailleTranslator translator, Iterable fs) {
+ state.assertUnopened();
+ this.translator = translator;
+ this.fs = fs;
+ state.open();
+ }
+
+/*
+ public LayoutMaster getCurrentLayoutMaster() {
+ return currentSequence().getLayoutMaster();
+ }*/
+
+
+ // End CurrentPageInfo
+
+ public void close() throws IOException {
+ if (state.isClosed()) {
+ return;
+ }
+ state.assertOpen();
+ state.close();
+ }
+
+ /**
+ * Paginates the supplied block sequence
+ * @param refs the cross references to use
+ * @throws IOException if IO fails
+ */
+ public PageStruct paginate(CrossReferences refs) throws PaginatorException {
+ PageStructImpl pageStruct = new PageStructImpl();
+ for (BlockSequence seq : fs) {
+ if (seq.getInitialPageNumber()==null) {
+ pageStruct.newSequence(seq.getLayoutMaster(), translator);
+ } else {
+ pageStruct.newSequence(seq.getLayoutMaster(), seq.getInitialPageNumber() - 1, translator);
+ }
+ pageStruct.newPage();
+ //ArrayList tmp = new ArrayList();
+ //Block[] groupA = new Block[tmp.size()];
+ //groupA = tmp.toArray(groupA);
+ //int gi = 0;
+ for (Block g : seq) {
+ //int height = ps.getCurrentLayoutMaster().getFlowHeight();
+ switch (g.getBreakBeforeType()) {
+ case PAGE:
+ if (pageStruct.countRows()>0) {
+ pageStruct.newPage();
+ }
+ break;
+ case AUTO:default:;
+ }
+ //FIXME: se över recursiv hämtning
+ switch (g.getKeepType()) {
+ case ALL:
+ int keepHeight = seq.getKeepHeight(g, refs);
+ if (pageStruct.countRows()>0 && keepHeight>pageStruct.getFlowHeight()-pageStruct.countRows() && keepHeight<=pageStruct.getFlowHeight()) {
+ pageStruct.newPage();
+ }
+ break;
+ case AUTO:
+ break;
+ default:;
+ }
+ if (g.getSpaceBefore()+g.getSpaceAfter()>=pageStruct.getFlowHeight()) {
+ throw new PaginatorException("Group margins too large to fit on an empty page.");
+ } else if (g.getSpaceBefore()+1>pageStruct.getFlowHeight()-pageStruct.countRows()) {
+ pageStruct.currentSequence().newPageOnRow();
+ }
+ BlockContentManager rdm = g.getBlockContentManager(refs);
+ if (g.getVerticalPosition() != null) {
+ int blockSpace = rdm.getRowCount() + g.getSpaceBefore() + g.getSpaceAfter();
+ int pos = g.getVerticalPosition().getPosition().makeAbsolute(pageStruct.currentPage().getFlowHeight());
+ int t = pos - pageStruct.currentPage().rowsOnPage();
+ if (t > 0) {
+ int advance = 0;
+ switch (g.getVerticalPosition().getAlignment()) {
+ case BEFORE:
+ advance = t - blockSpace;
+ break;
+ case CENTER:
+ advance = t - blockSpace / 2;
+ break;
+ case AFTER:
+ advance = t;
+ break;
+ }
+ for (int i = 0; i < advance; i++) {
+ pageStruct.newRow(new Row(""));
+ }
+ }
+ }
+ for (int i=0; i=pageStruct.getFlowHeight()-pageStruct.countRows()) {
+ pageStruct.currentSequence().newPageOnRow();
+ } else {
+ for (int i=0; i units, int width, String padding) {
+ if (units.size()==1) {
+ return units.get(0);
+ }
+ int chunksLength = 0;
+ for (String s : units) {
+ chunksLength += s.codePointCount(0, s.length());
+ }
+ int totalSpace = (width-chunksLength);
+ int parts = units.size()-1;
+ double target = totalSpace/(double)parts;
+ int used = 0;
+ StringBuffer sb = new StringBuffer();
+ for (int i=0; i0) {
+ int spacing = (int)Math.round(i * target) - used;
+ used += spacing;
+ sb.append(StringTools.fill(padding, spacing));
+ }
+ sb.append(units.get(i));
+ }
+ assert sb.length()==width;
+ return sb.toString();
+ }
+
+ private static String distributeTable(ArrayList units, int width, String padding) throws PaginatorToolsException {
+ double target = width/(double)units.size();
+ StringBuffer sb = new StringBuffer();
+ int used = 0;
+ for (int i=0; iunits of text over width chars, separated by padding pattern
+ * using distribution mode mode.
+ * @param units the units of text to distribute
+ * @param width the width of the resulting string
+ * @param padding the padding pattern to use as separator
+ * @param mode the distribution mode to use
+ * @return returns a string of width chars
+ */
+ public static String distribute(ArrayList units, int width, String padding, DistributeMode mode) throws PaginatorToolsException {
+ switch (mode) {
+ case EQUAL_SPACING:
+ return distributeEqualSpacing(units, width, padding);
+ case UNISIZE_TABLE_CELL:
+ return distributeTable(units, width, padding);
+ }
+ // Cannot happen
+ return null;
+ }
+
+ public static String distribute(Collection units) {
+ TreeSet sortedUnits = new TreeSet();
+ sortedUnits.addAll(units);
+ StringBuffer sb = new StringBuffer();
+ int used = 0;
+ for (TabStopString t : sortedUnits) {
+ used = sb.codePointCount(0, sb.length());
+ if (used > t.getPosition()) {
+ throw new RuntimeException("Cannot layout cell.");
+ }
+ int amount = t.getPosition()-used;
+ switch (t.getAlignment()) {
+ case LEFT:
+ //ok
+ break;
+ case CENTER:
+ amount -= t.length() / 2;
+ break;
+ case RIGHT:
+ amount -= t.length();
+ break;
+ }
+ sb.append(StringTools.fill(t.getPattern(), amount));
+ sb.append(t.getText());
+ }
+ return sb.toString();
+ }
+}
diff --git a/src/org/daisy/dotify/formatter/impl/PaginatorToolsException.java b/src/org/daisy/dotify/formatter/impl/PaginatorToolsException.java
new file mode 100644
index 00000000..ad8a0059
--- /dev/null
+++ b/src/org/daisy/dotify/formatter/impl/PaginatorToolsException.java
@@ -0,0 +1,37 @@
+package org.daisy.dotify.formatter.impl;
+
+/**
+ * A LayoutToolsException is an exception that indicates
+ * conditions in a LayoutTools process that a reasonable
+ * application might want to catch.
+ * @author Joel Håkansson
+ */
+class PaginatorToolsException extends Exception {
+
+ static final long serialVersionUID = 2207586942417976960L;
+
+ /**
+ * Constructs a new exception with null as its detail message.
+ */
+ public PaginatorToolsException() { super(); }
+
+ /**
+ * Constructs a new exception with the specified detail message.
+ * @param message the detail message
+ */
+ public PaginatorToolsException(String message) { super(message); }
+
+ /**
+ * Constructs a new exception with the specified cause
+ * @param cause the cause
+ */
+ public PaginatorToolsException(Throwable cause) { super(cause); }
+
+ /**
+ * Constructs a new exception with the specified detail message and cause.
+ * @param message the detail message
+ * @param cause the cause
+ */
+ public PaginatorToolsException(String message, Throwable cause) { super(message, cause); }
+
+}
diff --git a/src/org/daisy/dotify/formatter/impl/RowDataProperties.java b/src/org/daisy/dotify/formatter/impl/RowDataProperties.java
new file mode 100644
index 00000000..8029e4c5
--- /dev/null
+++ b/src/org/daisy/dotify/formatter/impl/RowDataProperties.java
@@ -0,0 +1,114 @@
+package org.daisy.dotify.formatter.impl;
+
+import org.daisy.dotify.api.formatter.FormattingTypes;
+import org.daisy.dotify.api.formatter.LayoutMaster;
+import org.daisy.dotify.api.translator.BrailleTranslator;
+
+class RowDataProperties {
+ private final int blockIndent, blockIndentParent;
+ private final BrailleTranslator translator;
+ private final LayoutMaster master;
+ private final int leftMargin, rightMargin;
+ private LIDao listProps;
+
+ static class Builder {
+ private final BrailleTranslator translator;
+ private final LayoutMaster master;
+ private int blockIndent = 0;
+ private int blockIndentParent = 0;
+ private int leftMargin = 0;
+ private int rightMargin = 0;
+
+ public Builder(BrailleTranslator translator, LayoutMaster master) {
+ this.translator = translator;
+ this.master = master;
+ }
+
+ public Builder blockIndent(int value) {
+ blockIndent = value;
+ return this;
+ }
+
+ public Builder blockIndentParent(int value) {
+ blockIndentParent = value;
+ return this;
+ }
+ public Builder leftMargin(int value) {
+ leftMargin = value;
+ return this;
+ }
+
+ public Builder rightMargin(int value) {
+ rightMargin = value;
+ return this;
+ }
+ public RowDataProperties build() {
+ return new RowDataProperties(this);
+ }
+ }
+
+ private RowDataProperties(Builder builder) {
+ this.blockIndent = builder.blockIndent;
+ this.blockIndentParent = builder.blockIndentParent;
+ this.translator = builder.translator;
+ this.master = builder.master;
+ this.leftMargin = builder.leftMargin;
+ this.rightMargin = builder.rightMargin;
+ this.listProps = null;
+ }
+
+ public int getBlockIndent() {
+ return blockIndent;
+ }
+
+ public int getBlockIndentParent() {
+ return blockIndentParent;
+ }
+
+ /*
+ public StringFilter getFilter() {
+ return filter;
+ }*/
+
+ public BrailleTranslator getTranslator() {
+ return translator;
+ }
+
+ public LayoutMaster getMaster() {
+ return master;
+ }
+
+ public int getLeftMargin() {
+ return leftMargin;
+ }
+
+ public int getRightMargin() {
+ return rightMargin;
+ }
+
+ public void setListItem(String label, FormattingTypes.ListStyle type) {
+ listProps = new LIDao(label, type);
+ }
+
+ public boolean isList() {
+ return listProps!=null;
+ }
+
+ public String getListLabel() {
+ return listProps.listLabel;
+ }
+
+ public FormattingTypes.ListStyle getListStyle() {
+ return listProps.listType;
+ }
+
+ private static class LIDao {
+ private final String listLabel;
+ private final FormattingTypes.ListStyle listType;
+ private LIDao(String label, FormattingTypes.ListStyle type) {
+ this.listLabel = label;
+ this.listType = type;
+ }
+ }
+
+}
diff --git a/src/org/daisy/dotify/formatter/impl/Segment.java b/src/org/daisy/dotify/formatter/impl/Segment.java
new file mode 100644
index 00000000..adc9b740
--- /dev/null
+++ b/src/org/daisy/dotify/formatter/impl/Segment.java
@@ -0,0 +1,8 @@
+package org.daisy.dotify.formatter.impl;
+
+interface Segment {
+ enum SegmentType {Text, NewLine, Leader, Reference, Marker, Anchor};
+
+ public SegmentType getSegmentType();
+
+}
\ No newline at end of file
diff --git a/src/org/daisy/dotify/formatter/impl/TabStopString.java b/src/org/daisy/dotify/formatter/impl/TabStopString.java
new file mode 100644
index 00000000..bf3ce86e
--- /dev/null
+++ b/src/org/daisy/dotify/formatter/impl/TabStopString.java
@@ -0,0 +1,139 @@
+package org.daisy.dotify.formatter.impl;
+
+
+/**
+ * Provides a tab stop string. A tab stop string is a data object
+ * containg a string, a position, an alignment and a fill pattern.
+ * This information can be used to place the string at the appropriate
+ * position on a row with the specified formatting.
+ *
+ * @author Joel Håkansson
+ */
+class TabStopString implements Comparable {
+ /**
+ * Provides alignment options for a tab stop
+ */
+ enum Alignment {
+ /**
+ * Text run to the right of the specified position
+ */
+ LEFT,
+ /**
+ * Text is centered around the specified position
+ */
+ CENTER,
+ /**
+ * Text run to the left of the specified position
+ */
+ RIGHT
+ };
+
+ private final String text;
+ private final int position;
+ private final int length;
+ private final Alignment align;
+ private final String pattern;
+
+ TabStopString(String text) {
+ this(text, 0, Alignment.LEFT, " ");
+ }
+
+ TabStopString(String text, int stop) {
+ this(text, stop, Alignment.LEFT, " ");
+ }
+
+ TabStopString(String text, int stop, Alignment align) {
+ this(text, stop, align, " ");
+ }
+
+ TabStopString(String text, int stop, Alignment align, String pattern) {
+ this.text = text;
+ this.length = text.codePointCount(0, text.length());
+ this.position = stop;
+ this.align = align;
+ if (pattern.length() == 0) {
+ throw new IllegalArgumentException("Pattern cannot be empty string");
+ }
+ this.pattern = pattern;
+ }
+
+ String getText() {
+ return text;
+ }
+
+ int getPosition() {
+ return position;
+ }
+
+ Alignment getAlignment() {
+ return align;
+ }
+
+ String getPattern() {
+ return this.pattern;
+ }
+
+ int length() {
+ return length;
+ }
+
+ public String toString() {
+ return "{\"" + getText() + "\", " + getPosition() + ", " + getAlignment() + ", \"" + getPattern() + "\"}";
+ }
+
+ public int compareTo(TabStopString o) {
+ if (getPosition()o.getPosition()) {
+ return 1;
+ } else {
+ if (getText().equals(o.getText())) {
+ if (getAlignment().equals(o.getAlignment())) {
+ return getPattern().compareTo(o.getPattern());
+ } else {
+ return getAlignment().compareTo(o.getAlignment());
+ }
+ } else {
+ return getText().compareTo(o.getText());
+ }
+ }
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + ((align == null) ? 0 : align.hashCode());
+ result = prime * result + ((pattern == null) ? 0 : pattern.hashCode());
+ result = prime * result + position;
+ result = prime * result + ((text == null) ? 0 : text.hashCode());
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (obj == null)
+ return false;
+ if (getClass() != obj.getClass())
+ return false;
+ TabStopString other = (TabStopString) obj;
+ if (align != other.align)
+ return false;
+ if (pattern == null) {
+ if (other.pattern != null)
+ return false;
+ } else if (!pattern.equals(other.pattern))
+ return false;
+ if (position != other.position)
+ return false;
+ if (text == null) {
+ if (other.text != null)
+ return false;
+ } else if (!text.equals(other.text))
+ return false;
+ return true;
+ }
+
+}
diff --git a/src/org/daisy/dotify/formatter/impl/TextBorder.java b/src/org/daisy/dotify/formatter/impl/TextBorder.java
new file mode 100644
index 00000000..085a71f5
--- /dev/null
+++ b/src/org/daisy/dotify/formatter/impl/TextBorder.java
@@ -0,0 +1,253 @@
+package org.daisy.dotify.formatter.impl;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.daisy.dotify.api.formatter.Row;
+import org.daisy.dotify.api.translator.BrailleTranslatorResult;
+import org.daisy.dotify.api.translator.TextBorderStyle;
+import org.daisy.dotify.tools.StringTools;
+
+/**
+ * Provides a way to add a border to a set of paragraphs.
+ * @author Joel Håkansson
+ */
+class TextBorder {
+ /**
+ * Text alignment within the bordered box
+ */
+ public enum Align {
+ /**
+ * Align text to the left
+ */
+ LEFT,
+ /**
+ * Center text
+ */
+ CENTER,
+ /**
+ * Align text to the right
+ */
+ RIGHT};
+ private final int topFill, rowFill, bottomFill;
+ private final String topLeftCorner, topBorder, topRightCorner, leftBorder,
+ rightBorder, bottomLeftCorner, bottomBorder, bottomRightCorner,
+ fillCharacter;
+
+ private Align align;
+
+ private final List ret;
+
+ /**
+ * The Builder is used when creating a TextBorder instance.
+ * @author Joel Håkansson
+ */
+ public static class Builder {
+ final int width;
+ final String fillCharacter;
+ Align align;
+ TextBorderStyle style;
+ String outerLeftMargin, innerLeftMargin, innerRightMargin;
+
+ /**
+ * Creates a new Builder
+ * @param width the width of the block including borders
+ */
+ public Builder(int width, String fillCharacter) {
+ this.width = width;
+ this.fillCharacter = fillCharacter;
+ this.align = Align.LEFT;
+ this.style = TextBorderStyle.NONE;
+ this.outerLeftMargin = "";
+ this.innerLeftMargin = "";
+ this.innerRightMargin = "";
+ }
+
+ /**
+ * Sets the text border style
+ *
+ * @param style
+ * the style
+ * @return returns this Builder
+ */
+ public Builder style(TextBorderStyle style) {
+ this.style = style;
+ return this;
+ }
+
+ /**
+ * Sets the text alignment
+ * @param align the text alignment
+ * @return returns this Builder
+ */
+ public Builder alignment(Align align) {
+ this.align = align;
+ return this;
+ }
+
+ public Builder outerLeftMargin(String margin) {
+ this.outerLeftMargin = margin;
+ return this;
+ }
+
+ public Builder innerLeftMargin(String margin) {
+ this.innerLeftMargin = margin;
+ return this;
+ }
+
+ public Builder innerRightMargin(String margin) {
+ this.innerRightMargin = margin;
+ return this;
+ }
+
+ /**
+ * Build TextBorder using the current state of the Builder
+ * @return returns a new TextBorder instance
+ */
+ public TextBorder build() {
+ return new TextBorder(this);
+ }
+ }
+
+ private TextBorder(Builder builder) {
+ this.align = builder.align;
+
+ this.topLeftCorner = builder.outerLeftMargin + builder.style.getTopLeftCorner();
+ this.topBorder = builder.style.getTopBorder();
+ this.topRightCorner = builder.style.getTopRightCorner();
+ this.leftBorder = builder.outerLeftMargin + builder.style.getLeftBorder() + builder.innerLeftMargin;
+ this.rightBorder = builder.innerRightMargin + builder.style.getRightBorder();
+ this.bottomLeftCorner = builder.outerLeftMargin + builder.style.getBottomLeftCorner();
+ this.bottomBorder = builder.style.getBottomBorder();
+ this.bottomRightCorner = builder.style.getBottomRightCorner();
+
+ this.topFill = builder.width - (topLeftCorner.length() + topRightCorner.length());
+ this.rowFill = builder.width - (leftBorder.length() + rightBorder.length());
+ this.bottomFill = builder.width - (bottomLeftCorner.length() + bottomRightCorner.length());
+ this.fillCharacter = builder.fillCharacter;
+ this.ret = new ArrayList();
+ }
+
+ /**
+ * Gets the rendered top border
+ * @return returns the rendered top border
+ */
+ public String getTopBorder() {
+ return topLeftCorner + StringTools.fill(topBorder, topFill) + topRightCorner;
+ }
+
+ /**
+ * Gets the rendered bottom border
+ * @return returns the rendered bottom border
+ */
+ public String getBottomBorder() {
+ return bottomLeftCorner + StringTools.fill(bottomBorder, bottomFill) + bottomRightCorner;
+ }
+
+ /**
+ * Adds borders to a paragraph of text. Each row is padded to
+ * fill up unused space and surrounded by the left and right border patterns.
+ * If the text does not fit within a row, the text is broken and the process
+ * is continued on a new row.
+ * @param bph the translator result to add borders to
+ * @return returns an ArrayList of String where each String is a row in the block.
+ */
+ public void addParagraph(BrailleTranslatorResult bph) {
+ for (String s : addBorderToParagraph(bph)) {
+ ret.add(new Row(s));
+ }
+ }
+
+ public void addParagraph(BrailleTranslatorResult bph, int fill) {
+ ArrayList vol = addBorderToParagraph(bph);
+ while (ret.size() <= fill - vol.size() - 3) {
+ addRow("");
+ }
+ for (String s : vol) {
+ ret.add(new Row(s));
+ }
+ }
+
+ private ArrayList addBorderToParagraph(BrailleTranslatorResult bph) {
+ ArrayList ret = new ArrayList();
+ while (bph.hasNext()) {
+ // .replaceAll("\\s*\\z", "") is probably not needed, as
+ // nextTranslatedRow must not exceed the row length
+ ret.add(addBorderToRow(bph.nextTranslatedRow(rowFill, true).replaceAll("\\s*\\z", ""), align, "", "", false));
+ }
+ return ret;
+ }
+
+ /**
+ * Adds borders to a line of text.
+ * @param text the text to add borders to
+ * @return returns the text padded with space and surrounded with the left and right border patterns.
+ * @throws IllegalArgumentException if the String does not fit within a single row.
+ */
+ public Row addRow(String text) {
+ Row row = new Row(addBorderToRow(text, align, "", "", false));
+ ret.add(row);
+ return row;
+ }
+
+ // Note: this is a transitional implementation (supporting both old code and
+ // the replacement code), therefore
+ // it might look a bit odd. It should be cleaned up, once the old code has
+ // been removed.
+ public String addBorderToRow(String text, Align align, String innerLeftBorder, String innerRightBorder, boolean bypass) {
+ int tRowFill = rowFill - innerLeftBorder.length() - innerRightBorder.length();
+ if (text.length() > tRowFill) {
+ throw new IllegalArgumentException("String (" + text + ") length (" + text.length() + ") must be <= width (" + tRowFill + ")");
+ }
+ StringBuffer sb = new StringBuffer();
+ sb.append(leftBorder);
+ sb.append(innerLeftBorder);
+ switch (align) {
+ case LEFT: break;
+ case CENTER:
+ sb.append(StringTools.fill(fillCharacter, (int) Math.floor((tRowFill - text.length()) / 2d)));
+ break;
+ case RIGHT:
+ sb.append(StringTools.fill(fillCharacter, tRowFill - text.length()));
+ break;
+ }
+ sb.append(text);
+ if (!bypass) {
+ switch (align) {
+ case LEFT:
+ sb.append(StringTools.fill(fillCharacter, tRowFill - text.length()));
+ break;
+ case CENTER:
+ sb.append(StringTools.fill(fillCharacter, (int) Math.ceil((tRowFill - text.length()) / 2d)));
+ break;
+ case RIGHT:
+ break;
+ }
+ sb.append(innerRightBorder);
+ sb.append(rightBorder);
+ }
+ return sb.toString();
+ }
+
+ public List getResult() {
+ ArrayList result = new ArrayList();
+ result.add(new Row(getTopBorder()));
+ result.addAll(ret);
+ ret.clear();
+ result.add(new Row(getBottomBorder()));
+ return result;
+ }
+
+ public List getStringResult() {
+ ArrayList result = new ArrayList();
+ result.add(getTopBorder());
+ // TODO: remove row from this class, unless really needed...
+ for (Row r : ret) {
+ result.add(r.getChars());
+ }
+ ret.clear();
+ result.add(getBottomBorder());
+ return result;
+ }
+
+}
diff --git a/src/org/daisy/dotify/formatter/impl/TextSegment.java b/src/org/daisy/dotify/formatter/impl/TextSegment.java
new file mode 100644
index 00000000..457207ee
--- /dev/null
+++ b/src/org/daisy/dotify/formatter/impl/TextSegment.java
@@ -0,0 +1,39 @@
+package org.daisy.dotify.formatter.impl;
+
+import org.daisy.dotify.api.formatter.BlockProperties;
+import org.daisy.dotify.api.formatter.TextProperties;
+
+
+
+class TextSegment implements Segment {
+ private CharSequence chars;
+ private final TextProperties tp;
+ private final BlockProperties p;
+
+ public TextSegment(CharSequence chars, TextProperties tp, BlockProperties p) {
+ this.chars = chars;
+ this.tp = tp;
+ this.p = p;
+ }
+
+ public CharSequence getChars() {
+ return chars;
+ }
+
+ public void setChars(CharSequence chars) {
+ this.chars = chars;
+ }
+
+ public TextProperties getTextProperties() {
+ return tp;
+ }
+
+ public SegmentType getSegmentType() {
+ return SegmentType.Text;
+ }
+
+ public BlockProperties getBlockProperties() {
+ return p;
+ }
+
+}
diff --git a/src/org/daisy/dotify/formatter/impl/VolDataInterface.java b/src/org/daisy/dotify/formatter/impl/VolDataInterface.java
new file mode 100644
index 00000000..8587643b
--- /dev/null
+++ b/src/org/daisy/dotify/formatter/impl/VolDataInterface.java
@@ -0,0 +1,14 @@
+package org.daisy.dotify.formatter.impl;
+
+import org.daisy.dotify.api.formatter.PageStruct;
+
+interface VolDataInterface {
+
+ public PageStruct getPreVolData();
+ public PageStruct getPostVolData();
+ public int getPreVolSize();
+ public int getPostVolSize();
+ public int getVolOverhead();
+ public int getTargetVolSize();
+
+}
diff --git a/src/org/daisy/dotify/formatter/impl/VolumeImpl.java b/src/org/daisy/dotify/formatter/impl/VolumeImpl.java
new file mode 100644
index 00000000..8249ee07
--- /dev/null
+++ b/src/org/daisy/dotify/formatter/impl/VolumeImpl.java
@@ -0,0 +1,25 @@
+package org.daisy.dotify.formatter.impl;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.daisy.dotify.api.formatter.PageSequence;
+import org.daisy.dotify.api.formatter.Volume;
+import org.daisy.dotify.tools.CompoundIterable;
+
+class VolumeImpl implements Volume {
+ private final CompoundIterable ret;
+
+ public VolumeImpl(Iterable preVolume, Iterable body, Iterable postVolume) {
+ List> contents = new ArrayList>();
+ contents.add(preVolume);
+ contents.add(body);
+ contents.add(postVolume);
+ this.ret = new CompoundIterable(contents);
+ }
+
+ public Iterable getContents() {
+ return ret;
+ }
+
+}
\ No newline at end of file
diff --git a/src/org/daisy/dotify/formatter/impl/package-info.java b/src/org/daisy/dotify/formatter/impl/package-info.java
new file mode 100644
index 00000000..46c270a4
--- /dev/null
+++ b/src/org/daisy/dotify/formatter/impl/package-info.java
@@ -0,0 +1,11 @@
+/**
+ * Provides a formatter implementation.
+ *
+ * IMPORTANT: This package contains implementations that should only be
+ * accessed using the Java Services API. Additional classes in this package
+ * should only be used by these implementations. This package is not part of the
+ * public API.
+ *
+ * @author Joel Håkansson
+ */
+package org.daisy.dotify.formatter.impl;
\ No newline at end of file
diff --git a/src/org/daisy/dotify/obfl/BlockContents.java b/src/org/daisy/dotify/obfl/BlockContents.java
new file mode 100644
index 00000000..b70b7a77
--- /dev/null
+++ b/src/org/daisy/dotify/obfl/BlockContents.java
@@ -0,0 +1,20 @@
+package org.daisy.dotify.obfl;
+
+import java.util.Map;
+
+
+
+/**
+ * Provides an interface for block contents.
+ * @author Joel Håkansson
+ *
+ */
+interface BlockContents extends IterableEventContents {
+
+ /**
+ * Sets the evaluate context using the supplied map where key
+ * is a variable name and value is the variables value.
+ * @param vars a map containing variables and their value
+ */
+ public void setEvaluateContext(Map vars);
+}
diff --git a/src/org/daisy/dotify/obfl/BlockEvent.java b/src/org/daisy/dotify/obfl/BlockEvent.java
new file mode 100644
index 00000000..26f5308e
--- /dev/null
+++ b/src/org/daisy/dotify/obfl/BlockEvent.java
@@ -0,0 +1,18 @@
+package org.daisy.dotify.obfl;
+
+import org.daisy.dotify.api.formatter.BlockProperties;
+
+/**
+ * Provides an interface for block events.
+ *
+ * @author Joel Håkansson
+ */
+interface BlockEvent extends BlockContents {
+
+ /**
+ * Gets properties of this block.
+ * @return returns the properties
+ */
+ public BlockProperties getProperties();
+
+}
diff --git a/src/org/daisy/dotify/obfl/BlockEventHandler.java b/src/org/daisy/dotify/obfl/BlockEventHandler.java
new file mode 100644
index 00000000..46e64c33
--- /dev/null
+++ b/src/org/daisy/dotify/obfl/BlockEventHandler.java
@@ -0,0 +1,112 @@
+package org.daisy.dotify.obfl;
+
+import java.io.IOException;
+import java.util.Map;
+
+import org.daisy.dotify.api.formatter.BlockStruct;
+import org.daisy.dotify.api.formatter.Formatter;
+import org.daisy.dotify.api.formatter.FormatterFactory;
+import org.daisy.dotify.api.formatter.LayoutMaster;
+import org.daisy.dotify.api.formatter.Leader;
+import org.daisy.dotify.api.formatter.Marker;
+import org.daisy.dotify.api.obfl.ExpressionFactory;
+import org.daisy.dotify.obfl.EventContents.ContentType;
+
+/**
+ * Provides a method to send events to a formatter.
+ *
+ * @author Joel Håkansson
+ */
+class BlockEventHandler {
+ private final Formatter formatter;
+ private final ExpressionFactory ef;
+
+ public BlockEventHandler(String locale, String mode, Map masters, FormatterFactory ff, ExpressionFactory ef) {
+ this.formatter = ff.newFormatter(locale, mode);
+ this.formatter.open();
+ for (String name : masters.keySet()) {
+ this.formatter.addLayoutMaster(name, masters.get(name));
+ }
+ this.ef = ef;
+ }
+
+ public BlockEventHandler(Formatter formatter, ExpressionFactory ef) {
+ this.formatter = formatter;
+ this.ef = ef;
+ }
+
+ public void insertEventContents(IterableEventContents b) {
+ for (EventContents bc : b) {
+ switch (bc.getContentType()) {
+ case PCDATA: {
+ TextContents tc = (TextContents)bc;
+ formatter.addChars(tc.getText(), tc.getSpanProperties());
+ break; }
+ case LEADER: {
+ formatter.insertLeader(((Leader)bc));
+ break; }
+ case PAGE_NUMBER: {
+ formatter.insertReference(((PageNumberReference)bc).getRefId(), ((PageNumberReference)bc).getNumeralStyle());
+ break; }
+ case BLOCK: {
+ BlockEvent ev = (BlockEvent)bc;
+ formatter.startBlock(ev.getProperties());
+ insertEventContents(ev);
+ formatter.endBlock();
+ break; }
+ case TOC_ENTRY: {
+ TocBlockEvent ev = (TocBlockEvent)bc;
+ formatter.startBlock(ev.getProperties(), ev.getTocId());
+ insertEventContents(ev);
+ formatter.endBlock();
+ break; }
+ case BR: {
+ formatter.newLine();
+ break; }
+ case EVALUATE: {
+ Evaluate e = ((Evaluate)bc);
+ formatter.addChars((ef.newExpression().evaluate(e.getExpression(), e.getVariables())).toString(), e.getTextProperties());
+ break; }
+ case MARKER: {
+ Marker m = ((Marker)bc);
+ formatter.insertMarker(m);
+ break;
+ }
+ case STYLE: {
+ StyleEvent ev = (StyleEvent) bc;
+ insertEventContents(ev);
+ break;
+ }
+ default:
+ throw new RuntimeException("Unknown contents: " + bc.getContentType());
+ }
+ }
+ }
+
+ public void formatSequences(Iterable sequences) {
+ for (SequenceEvent events : sequences) {
+ formatSequence(events);
+ }
+ }
+
+ public void formatSequence(SequenceEvent events) {
+ formatter.newSequence(events.getSequenceProperties());
+ for (BlockEvent e : events) {
+ if (e.getContentType()==ContentType.TOC_ENTRY) {
+ formatter.startBlock(e.getProperties(), ((TocBlockEvent)e).getTocId());
+ } else if (e.getContentType()==ContentType.BLOCK) {
+ formatter.startBlock(e.getProperties());
+ } else {
+ throw new RuntimeException("Coding error");
+ }
+ insertEventContents(e);
+ formatter.endBlock();
+ }
+ }
+
+ public BlockStruct close() throws IOException {
+ formatter.close();
+ return formatter.getFlowStruct();
+ }
+
+}
diff --git a/src/org/daisy/dotify/obfl/BlockEventHandlerRunner.java b/src/org/daisy/dotify/obfl/BlockEventHandlerRunner.java
new file mode 100644
index 00000000..d60776fa
--- /dev/null
+++ b/src/org/daisy/dotify/obfl/BlockEventHandlerRunner.java
@@ -0,0 +1,196 @@
+package org.daisy.dotify.obfl;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import org.daisy.dotify.api.formatter.Block;
+import org.daisy.dotify.api.formatter.BlockSequence;
+import org.daisy.dotify.api.formatter.CrossReferences;
+import org.daisy.dotify.api.formatter.FormatterFactory;
+import org.daisy.dotify.api.formatter.LayoutMaster;
+import org.daisy.dotify.api.formatter.VolumeContentFormatter;
+import org.daisy.dotify.api.obfl.ExpressionFactory;
+import org.daisy.dotify.obfl.TocSequenceEvent.TocRange;
+
+class BlockEventHandlerRunner implements VolumeContentFormatter {
+ private final Iterable volumeTemplates;
+ private final String locale;
+ private final String mode;
+ private final Map masters;
+ private final Map tocs;
+ private final Logger logger;
+ private final FormatterFactory ff;
+ private final ExpressionFactory ef;
+
+ BlockEventHandlerRunner(String locale, String mode, Map masters, Map tocs, Iterable volumeTemplates, FormatterFactory ff, ExpressionFactory ef) {
+ this.volumeTemplates = volumeTemplates;
+ this.locale = locale;
+ this.mode = mode;
+ this.masters = masters;
+ this.tocs = tocs;
+ this.logger = Logger.getLogger(this.getClass().getCanonicalName());
+ this.ff = ff;
+ this.ef = ef;
+ }
+
+ private void appendToc(VolumeSequenceEvent seq, CrossReferences crh, int volumeNumber, int volumeCount, List> ib) throws IOException {
+ TocSequenceEvent toc = (TocSequenceEvent)seq;
+ if (toc.appliesTo(volumeNumber, volumeCount)) {
+ BlockEventHandler beh = new BlockEventHandler(locale, mode, masters, ff, ef);
+ TableOfContents data = tocs.get(toc.getTocName());
+ TocEvents events = toc.getTocEvents(volumeNumber, volumeCount);
+ StaticSequenceEventImpl evs = new StaticSequenceEventImpl(toc.getSequenceProperties());
+ for (BlockEvent e : events.getTocStartEvents()) {
+ evs.push(e);
+ }
+ for (BlockEvent e : data) {
+ evs.push(e);
+ }
+ if (toc.getRange()==TocRange.DOCUMENT) {
+ for (BlockEvent e : events.getVolumeEndEvents(volumeCount)) {
+ evs.push(e);
+ }
+ }
+ for (BlockEvent e : events.getTocEndEvents()) {
+ evs.push(e);
+ }
+ if (toc.getRange()==TocRange.VOLUME) {
+ beh.formatSequence(evs);
+ BlockSequenceManipulator fsm = new BlockSequenceManipulator(beh.close());
+ String start = null;
+ String stop = null;
+ //assumes toc is in sequential order
+ for (String id : data.getTocIdList()) {
+ String ref = data.getRefForID(id);
+ int vol = crh.getVolumeNumber(ref);
+ if (vol r = new ArrayList();
+ fsm.removeRange(data.getTocIdList().iterator().next(), start);
+ fsm.removeTail(stop);
+ BlockEventHandler beh2 = new BlockEventHandler(locale, mode, masters, ff, ef);
+ StaticSequenceEventImpl evs2 = new StaticSequenceEventImpl(toc.getSequenceProperties());
+ for (BlockEvent e : events.getTocEndEvents()) {
+ evs2.add(e);
+ }
+ beh2.formatSequence(evs2);
+ fsm.appendGroup(beh2.close().getBlockSequenceIterable().iterator().next());
+ r.add(fsm.newSequence());
+ ib.add(r);
+ } catch (Exception e) {
+ logger.log(Level.SEVERE, "TOC failed for: volume " + volumeNumber + " of " + volumeCount, e);
+ }
+ }
+ } else if (toc.getRange()==TocRange.DOCUMENT) {
+ beh.formatSequence(evs);
+ BlockSequenceManipulator fsm = new BlockSequenceManipulator(beh.close());
+ int nv=0;
+ HashMap statics = new HashMap();
+ for (Block b : fsm.getBlocks()) {
+ if (b.getBlockIdentifier()!=null) {
+ String ref = data.getRefForID(b.getBlockIdentifier());
+ Integer vol = crh.getVolumeNumber(ref);
+ if (vol!=null) {
+ if (nv!=vol) {
+ BlockEventHandler beh2 = new BlockEventHandler(locale, mode, masters, ff, ef);
+ StaticSequenceEventImpl evs2 = new StaticSequenceEventImpl(toc.getSequenceProperties());
+ if (nv>0) {
+ for (BlockEvent e : events.getVolumeEndEvents(nv)) {
+ evs2.add(e);
+ }
+ }
+ nv = vol;
+ for (BlockEvent e : events.getVolumeStartEvents(vol)) {
+ evs2.add(e);
+ }
+ beh2.formatSequence(evs2);
+ statics.put(b.getBlockIdentifier(), beh2.close().getBlockSequenceIterable().iterator().next());
+ }
+ }
+ }
+ }
+ for (String key : statics.keySet()) {
+ fsm.insertGroup(statics.get(key), key);
+ }
+ ArrayList r = new ArrayList();
+ r.add(fsm.newSequence());
+ ib.add(r);
+ } else {
+ throw new RuntimeException("Coding error");
+ }
+ }
+ }
+
+ public int getVolumeMaxSize(int volumeNumber, int volumeCount) {
+ for (VolumeTemplate t : volumeTemplates) {
+ if (t==null) {
+ System.out.println("VOLDATA NULL");
+ }
+ if (t.appliesTo(volumeNumber, volumeCount)) {
+ return t.getVolumeMaxSize();
+ }
+ }
+ //TODO: don't return a fixed value
+ return 50;
+ }
+
+ public List> formatPreVolumeContents(int volumeNumber, int volumeCount, CrossReferences crh) {
+ try {
+ return formatVolumeContents(volumeNumber, volumeCount, crh, true);
+ } catch (IOException e) {
+ return null;
+ }
+ }
+
+ public List> formatPostVolumeContents(int volumeNumber, int volumeCount, CrossReferences crh) {
+ try {
+ return formatVolumeContents(volumeNumber, volumeCount, crh, false);
+ } catch (IOException e) {
+ return null;
+ }
+ }
+
+ private List> formatVolumeContents(int volumeNumber, int volumeCount, CrossReferences crh, boolean pre) throws IOException {
+ ArrayList> ib = new ArrayList>();
+ for (VolumeTemplate t : volumeTemplates) {
+ if (t.appliesTo(volumeNumber, volumeCount)) {
+ for (VolumeSequenceEvent seq : (pre?t.getPreVolumeContent():t.getPostVolumeContent())) {
+ if (seq instanceof TocSequenceEvent) {
+ appendToc(seq, crh, volumeNumber, volumeCount, ib);
+ } else if (seq instanceof SequenceEvent) {
+ BlockEventHandler beh = new BlockEventHandler(locale, mode, masters, ff, ef);
+ SequenceEvent seqEv = ((SequenceEvent)seq);
+ HashMap vars = new HashMap();
+ vars.put(t.getVolumeCountVariableName(), volumeCount+"");
+ vars.put(t.getVolumeNumberVariableName(), volumeNumber+"");
+ seqEv.setEvaluateContext(vars);
+ beh.formatSequence(seqEv);
+ ib.add(beh.close().getBlockSequenceIterable());
+ } else {
+ throw new RuntimeException("Unexpected error");
+ }
+ }
+ break;
+ }
+ }
+ return ib;
+ }
+
+}
diff --git a/src/org/daisy/dotify/obfl/BlockEventImpl.java b/src/org/daisy/dotify/obfl/BlockEventImpl.java
new file mode 100644
index 00000000..9da22087
--- /dev/null
+++ b/src/org/daisy/dotify/obfl/BlockEventImpl.java
@@ -0,0 +1,46 @@
+package org.daisy.dotify.obfl;
+
+import java.util.Map;
+import java.util.Stack;
+
+import org.daisy.dotify.api.formatter.BlockProperties;
+
+
+
+class BlockEventImpl extends Stack implements BlockEvent {
+ private final BlockProperties props;
+
+ public BlockEventImpl(BlockProperties props) {
+ this.props = props;
+ }
+ /**
+ *
+ */
+ private static final long serialVersionUID = 9098524584205247145L;
+
+ public ContentType getContentType() {
+ return ContentType.BLOCK;
+ }
+
+ public BlockProperties getProperties() {
+ return props;
+ }
+
+ public void setEvaluateContext(Map vars) {
+ for (int i=0; i taggedEntries;
+ private final Stack sequence;
+ //private final SequenceProperties props;
+ private int initialPagenum;
+ private final LayoutMaster master;
+
+ public BlockSequenceManipulator(BlockStruct struct) {
+ this.sequence = new Stack();
+ //SequenceProperties tmp = null;
+ LayoutMaster tMaster = null;
+ for (BlockSequence b : struct.getBlockSequenceIterable()) {
+ //tmp = b.getSequenceProperties();
+ initialPagenum = b.getInitialPageNumber();
+ tMaster = b.getLayoutMaster();
+ for (Block bb : b) {
+ this.sequence.add(bb);
+ }
+ }
+ //this.props = tmp;
+ this.master = tMaster;
+ this.taggedEntries = tagSequence(this.sequence);
+ }
+
+ private BlockSequence newSequence(List c) {
+ BlockSeqImpl ret = new BlockSeqImpl(initialPagenum, master);
+ ret.addAll(c);
+ return ret;
+ }
+
+ public BlockSequence newSequence() {
+ return newSequence(sequence);
+ }
+
+ /*
+ public BlockSequence newSubSequence(String fromId) {
+ Integer fromIndex = taggedEntries.get(fromId);
+ if (fromIndex==null) {
+ throw new IllegalArgumentException("Cannot find identifier " + fromId);
+ }
+ return newSequence(sequence.subList(fromIndex, sequence.size()));
+ }*/
+
+ public void insertGroup(Iterable blocks, String beforeId) {
+ ArrayList call = new ArrayList();
+ for (Block b : blocks) {
+ call.add(b);
+ }
+ insertGroup(call, beforeId);
+ }
+ public void appendGroup(Iterable blocks) {
+ ArrayList call = new ArrayList();
+ for (Block b : blocks) {
+ call.add(b);
+ }
+ sequence.addAll(call);
+ taggedEntries = tagSequence(sequence);
+ }
+
+ public void insertGroup(Collection seq, String beforeId) {
+ Integer beforeIndex = taggedEntries.get(beforeId);
+ if (beforeIndex==null) {
+ throw new IllegalArgumentException("Cannot find identifier " + beforeId);
+ }
+ sequence.addAll(beforeIndex, seq);
+ taggedEntries = tagSequence(sequence);
+ }
+
+ public void removeGroup(String id) {
+ Integer index = taggedEntries.get(id);
+ if (index==null) {
+ throw new IllegalArgumentException("Cannot find identifier " + id);
+ }
+ sequence.removeElementAt(index);
+ taggedEntries = tagSequence(sequence);
+ }
+
+ public void removeRange(String fromId, String toId) {
+ Integer fromIndex = taggedEntries.get(fromId);
+ Integer toIndex = taggedEntries.get(toId);
+ if (fromIndex==null || toIndex==null) {
+ throw new IllegalArgumentException("Cannot find identifier " + fromId + "/" + toId);
+ }
+ for (int i=0; itoIndex
+ return newSequence(sequence.subList(fromIndex, toIndex));
+ }*/
+ /*
+ public BlockSequence newFromItem(String id) {
+ return newSubSequence(id, id);
+ }
+ */
+ private static HashMap tagSequence(List seq) {
+ HashMap entries = new HashMap();
+ int i = 0;
+ for (Block group : seq) {
+ if (group.getBlockIdentifier()!=null && !group.getBlockIdentifier().equals("")) {
+ if (entries.put(group.getBlockIdentifier(), i)!=null) {
+ throw new IllegalArgumentException("Duplicate id " + group.getBlockIdentifier());
+ }
+ //System.out.println("GROUP! " + fg.getIdentifier());
+ }
+ i++;
+ }
+ return entries;
+ }
+
+ public List getBlocks() {
+ return sequence;
+ }
+
+ private static class BlockSeqImpl extends Stack implements BlockSequence {
+ /**
+ *
+ */
+ private static final long serialVersionUID = -7098716884005865317L;
+ //private final SequenceProperties p;
+ private final LayoutMaster master;
+ private final int initialPagenum;
+
+ private BlockSeqImpl(int initialPagenum, LayoutMaster master) {
+ //this.p = p;
+ this.initialPagenum = initialPagenum;
+ this.master = master;
+ }
+
+ public LayoutMaster getLayoutMaster() {
+ return master;
+ }
+
+ public Integer getInitialPageNumber() {
+ return initialPagenum;
+ }
+
+ public int getBlockCount() {
+ return this.size();
+ }
+
+ public Block getBlock(int index) {
+ return this.elementAt(index);
+ }
+
+ public int getKeepHeight(Block block, CrossReferences refs) {
+ return getKeepHeight(this.indexOf(block), refs);
+ }
+ private int getKeepHeight(int gi, CrossReferences refs) {
+ int keepHeight = getBlock(gi).getSpaceBefore()+getBlock(gi).getBlockContentManager(refs).getRowCount();
+ if (getBlock(gi).getKeepWithNext()>0 && gi+1 events;
+ private final ExpressionFactory ef;
+
+ public ConditionalEvents(Iterable events, String condition, ExpressionFactory ef) {
+ this.events = events;
+ this.condition = condition;
+ this.ef = ef;
+ }
+
+ public Iterable getEvents() {
+ return events;
+ }
+
+ public boolean appliesTo(Map variables) {
+ if (condition==null) {
+ return true;
+ }
+ return ef.newExpression().evaluate(condition, variables).equals(true);
+ }
+
+}
\ No newline at end of file
diff --git a/src/org/daisy/dotify/obfl/Evaluate.java b/src/org/daisy/dotify/obfl/Evaluate.java
new file mode 100644
index 00000000..a5291288
--- /dev/null
+++ b/src/org/daisy/dotify/obfl/Evaluate.java
@@ -0,0 +1,50 @@
+package org.daisy.dotify.obfl;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.daisy.dotify.api.formatter.TextProperties;
+
+
+/**
+ * Provides an evaluate event object.
+ *
+ * @author Joel Håkansson
+ *
+ */
+class Evaluate implements EventContents {
+ private final String expression;
+ private final Map vars;
+ private final TextProperties props;
+
+ public Evaluate(String expression, Map vars, TextProperties props) {
+ this.expression = expression;
+ this.vars = vars;
+ this.props = props;
+ }
+
+ public Evaluate(String expression, TextProperties props) {
+ this(expression, new HashMap(), props);
+ }
+
+ public String getExpression() {
+ return expression;
+ }
+
+ public Map getVariables() {
+ return vars;
+ }
+
+ public ContentType getContentType() {
+ return ContentType.EVALUATE;
+ }
+
+ public boolean canContainEventObjects() {
+ return false;
+ }
+
+ public TextProperties getTextProperties() {
+ return props;
+ }
+
+}
diff --git a/src/org/daisy/dotify/obfl/EventContents.java b/src/org/daisy/dotify/obfl/EventContents.java
new file mode 100644
index 00000000..eb418079
--- /dev/null
+++ b/src/org/daisy/dotify/obfl/EventContents.java
@@ -0,0 +1,9 @@
+package org.daisy.dotify.obfl;
+
+
+interface EventContents {
+ public enum ContentType {PCDATA, LEADER, MARKER, ANCHOR, BR, EVALUATE, BLOCK, STYLE, TOC_ENTRY, PAGE_NUMBER};
+ public ContentType getContentType();
+ public boolean canContainEventObjects();
+
+}
diff --git a/src/org/daisy/dotify/obfl/IterableEventContents.java b/src/org/daisy/dotify/obfl/IterableEventContents.java
new file mode 100644
index 00000000..c77e7d98
--- /dev/null
+++ b/src/org/daisy/dotify/obfl/IterableEventContents.java
@@ -0,0 +1,5 @@
+package org.daisy.dotify.obfl;
+
+public interface IterableEventContents extends Iterable, EventContents {
+
+}
diff --git a/src/org/daisy/dotify/obfl/LayoutMasterImpl.java b/src/org/daisy/dotify/obfl/LayoutMasterImpl.java
new file mode 100644
index 00000000..06e375bf
--- /dev/null
+++ b/src/org/daisy/dotify/obfl/LayoutMasterImpl.java
@@ -0,0 +1,176 @@
+package org.daisy.dotify.obfl;
+
+import java.util.ArrayList;
+
+import org.daisy.dotify.api.formatter.LayoutMaster;
+import org.daisy.dotify.api.formatter.PageTemplate;
+import org.daisy.dotify.api.obfl.ExpressionFactory;
+import org.daisy.dotify.api.translator.TextBorderStyle;
+
+/**
+ * ConfigurableLayoutMaster will ensure that the LayoutMaster measurements adds up.
+ * @author Joel Håkansson
+ */
+class LayoutMasterImpl implements LayoutMaster {
+// protected final int headerHeight;
+// protected final int footerHeight;
+ protected final int flowWidth;
+ // protected final int flowHeight;
+ protected final int pageWidth;
+ protected final int pageHeight;
+ protected final ExpressionFactory ef;
+ protected final int innerMargin;
+ protected final int outerMargin;
+ protected final float rowSpacing;
+ protected final boolean duplex;
+ protected final ArrayList templates;
+ protected final TextBorderStyle frame;
+
+ /**
+ * Configuration class for a ConfigurableLayoutMaster
+ * @author Joel Håkansson
+ *
+ */
+ public static class Builder {
+ final int pageWidth;
+ final int pageHeight;
+ final ExpressionFactory ef;
+ // optional
+// int headerHeight = 0;
+// int footerHeight = 0;
+ int innerMargin = 0;
+ int outerMargin = 0;
+ float rowSpacing = 1;
+ boolean duplex = true;
+ ArrayList templates;
+ TextBorderStyle frame;
+
+
+ public Builder(int pageWidth, int pageHeight, ExpressionFactory ef) {
+ this.pageWidth = pageWidth;
+ this.pageHeight = pageHeight;
+ this.templates = new ArrayList();
+ this.ef = ef;
+ frame = null;
+ }
+ /*
+ public Builder headerHeight(int value) {
+ this.headerHeight = value;
+ return this;
+ }
+
+ public Builder footerHeight(int value) {
+ this.footerHeight = value;
+ return this;
+ }*/
+
+ public Builder innerMargin(int value) {
+ this.innerMargin = value;
+ return this;
+ }
+
+ public Builder outerMargin(int value) {
+ this.outerMargin = value;
+ return this;
+ }
+
+ public Builder rowSpacing(float value) {
+ this.rowSpacing = value;
+ return this;
+ }
+
+ public Builder duplex(boolean value) {
+ this.duplex = value;
+ return this;
+ }
+
+ public Builder frame(TextBorderStyle frame) {
+ this.frame = frame;
+ return this;
+ }
+
+ public Builder addTemplate(PageTemplate value) {
+ this.templates.add(value);
+ return this;
+ }
+
+ public LayoutMasterImpl build() {
+ return new LayoutMasterImpl(this);
+ }
+ }
+
+ private LayoutMasterImpl(Builder config) {
+ // int flowWidth, int flowHeight, int headerHeight, int footerHeight, int innerMargin, int outerMargin, float rowSpacing
+// this.headerHeight = config.headerHeight;
+// this.footerHeight = config.footerHeight;
+ int fsize = 0;
+ if (config.frame != null) {
+ fsize = config.frame.getLeftBorder().length() + config.frame.getRightBorder().length();
+ }
+ this.flowWidth = config.pageWidth - config.innerMargin - config.outerMargin - fsize;
+ //this.flowHeight = config.pageHeight-config.headerHeight-config.footerHeight;
+ this.pageWidth = config.pageWidth;
+ this.pageHeight = config.pageHeight;
+ this.innerMargin = config.innerMargin;
+ this.outerMargin = config.outerMargin;
+ this.rowSpacing = config.rowSpacing;
+ this.duplex = config.duplex;
+ this.templates = config.templates;
+ this.frame = config.frame;
+ this.ef = config.ef;
+ }
+
+ public int getPageWidth() {
+ return pageWidth;
+ }
+
+ public int getPageHeight() {
+ return pageHeight;
+ }
+
+ public int getFlowWidth() {
+ return flowWidth;
+ }
+/*
+ public int getFlowHeight() {
+ return flowHeight;
+ }*/
+/*
+ public int getHeaderHeight() {
+ return headerHeight;
+ }
+
+ public int getFooterHeight() {
+ return footerHeight;
+ }*/
+
+ public int getInnerMargin() {
+ return innerMargin;
+ }
+
+ public int getOuterMargin() {
+ return outerMargin;
+ }
+
+ public float getRowSpacing() {
+ return rowSpacing;
+ }
+
+ public boolean duplex() {
+ return duplex;
+ }
+
+ public TextBorderStyle getFrame() {
+ return frame;
+ }
+
+ public PageTemplate getTemplate(int pagenum) {
+ for (PageTemplate t : templates) {
+ if (t.appliesTo(pagenum)) { return t; }
+ }
+ // if no template applies, an empty template should be returned
+ // since adding templates is optional in Builder
+ return new PageTemplateImpl(ef);
+ }
+
+}
diff --git a/src/org/daisy/dotify/obfl/LeaderEventContents.java b/src/org/daisy/dotify/obfl/LeaderEventContents.java
new file mode 100644
index 00000000..7315a1c4
--- /dev/null
+++ b/src/org/daisy/dotify/obfl/LeaderEventContents.java
@@ -0,0 +1,14 @@
+package org.daisy.dotify.obfl;
+
+import org.daisy.dotify.api.formatter.Leader;
+
+class LeaderEventContents extends Leader implements EventContents {
+
+ public LeaderEventContents(Builder builder) {
+ super(builder);
+ }
+
+ public ContentType getContentType() {
+ return ContentType.LEADER;
+ }
+}
diff --git a/src/org/daisy/dotify/obfl/LineBreak.java b/src/org/daisy/dotify/obfl/LineBreak.java
new file mode 100644
index 00000000..558779bc
--- /dev/null
+++ b/src/org/daisy/dotify/obfl/LineBreak.java
@@ -0,0 +1,20 @@
+package org.daisy.dotify.obfl;
+
+
+
+/**
+ * Provides a line break event object.
+ * @author Joel Håkansson
+ *
+ */
+class LineBreak implements EventContents {
+
+ public ContentType getContentType() {
+ return ContentType.BR;
+ }
+
+ public boolean canContainEventObjects() {
+ return false;
+ }
+
+}
diff --git a/src/org/daisy/dotify/obfl/MarkerEventContents.java b/src/org/daisy/dotify/obfl/MarkerEventContents.java
new file mode 100644
index 00000000..0662d07c
--- /dev/null
+++ b/src/org/daisy/dotify/obfl/MarkerEventContents.java
@@ -0,0 +1,14 @@
+package org.daisy.dotify.obfl;
+
+import org.daisy.dotify.api.formatter.Marker;
+
+class MarkerEventContents extends Marker implements EventContents {
+
+ public MarkerEventContents(String name, String value) {
+ super(name, value);
+ }
+
+ public ContentType getContentType() {
+ return ContentType.MARKER;
+ }
+}
diff --git a/src/org/daisy/dotify/obfl/OBFLParserException.java b/src/org/daisy/dotify/obfl/OBFLParserException.java
new file mode 100644
index 00000000..db31b888
--- /dev/null
+++ b/src/org/daisy/dotify/obfl/OBFLParserException.java
@@ -0,0 +1,25 @@
+package org.daisy.dotify.obfl;
+
+public class OBFLParserException extends Exception {
+
+ /**
+ *
+ */
+ private static final long serialVersionUID = -5063158789318366739L;
+
+ public OBFLParserException() {
+ }
+
+ public OBFLParserException(String message) {
+ super(message);
+ }
+
+ public OBFLParserException(Throwable cause) {
+ super(cause);
+ }
+
+ public OBFLParserException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+}
diff --git a/src/org/daisy/dotify/obfl/OBFLWsNormalizer.java b/src/org/daisy/dotify/obfl/OBFLWsNormalizer.java
new file mode 100644
index 00000000..177a7ee3
--- /dev/null
+++ b/src/org/daisy/dotify/obfl/OBFLWsNormalizer.java
@@ -0,0 +1,288 @@
+package org.daisy.dotify.obfl;
+
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.regex.Pattern;
+
+import javax.xml.namespace.QName;
+import javax.xml.stream.XMLEventFactory;
+import javax.xml.stream.XMLEventReader;
+import javax.xml.stream.XMLEventWriter;
+import javax.xml.stream.XMLInputFactory;
+import javax.xml.stream.XMLOutputFactory;
+import javax.xml.stream.XMLStreamConstants;
+import javax.xml.stream.XMLStreamException;
+import javax.xml.stream.events.StartDocument;
+import javax.xml.stream.events.XMLEvent;
+
+public class OBFLWsNormalizer {
+ private final XMLEventReader input;
+ private final OutputStream out;
+ private final XMLEventFactory eventFactory;
+ private XMLEventWriter writer;
+ private final Pattern beginWS;
+ private final Pattern endWS;
+
+ public OBFLWsNormalizer(XMLEventReader input, XMLEventFactory eventFactory, OutputStream out) throws XMLStreamException {
+ this.input = input;
+ this.writer = null;
+ this.out = out;
+ this.eventFactory = eventFactory;
+ beginWS = Pattern.compile("\\A\\s+");
+ endWS = Pattern.compile("\\s+\\z");
+ }
+
+ public void parse(XMLOutputFactory outputFactory) {
+ XMLEvent event;
+ while (input.hasNext()) {
+ try {
+ event = input.nextEvent();
+ if (event.getEventType() == XMLStreamConstants.START_DOCUMENT) {
+ StartDocument sd = (StartDocument) event;
+ if (sd.encodingSet()) {
+ writer = outputFactory.createXMLEventWriter(out, sd.getCharacterEncodingScheme());
+ writer.add(event);
+ } else {
+ writer = outputFactory.createXMLEventWriter(out, "utf-8");
+ writer.add(eventFactory.createStartDocument("utf-8", "1.0"));
+ }
+ } else if (event.getEventType() == XMLStreamConstants.CHARACTERS) {
+ writer.add(eventFactory.createCharacters(normalizeSpace(event.asCharacters().getData())));
+ } else if (equalsStart(event, ObflQName.BLOCK, ObflQName.TOC_ENTRY)) {
+ parseBlock(event);
+ } else {
+ writer.add(event);
+ }
+ } catch (XMLStreamException e) {
+ e.printStackTrace();
+ }
+ }
+ try {
+ input.close();
+ } catch (XMLStreamException e) {
+ e.printStackTrace();
+ }
+ try {
+ writer.close();
+ } catch (XMLStreamException e) {
+ e.printStackTrace();
+ }
+ }
+
+ private void parseBlock(XMLEvent event) throws XMLStreamException {
+ QName end = event.asStartElement().getName();
+ List events = new ArrayList();
+ events.add(event);
+ while (input.hasNext()) {
+ event = input.nextEvent();
+ if (equalsStart(event, ObflQName.BLOCK, ObflQName.TOC_ENTRY)) {
+ processList(events);
+ events.clear();
+ parseBlock(event);
+ } else if (equalsEnd(event, end)) {
+ events.add(event);
+ processList(events);
+ break;
+ } else {
+ events.add(event);
+ }
+ }
+ }
+
+ private void processList(List events) throws XMLStreamException {
+ // System.out.println(events.size());
+ List modified = new ArrayList();
+ // process
+ for (int i = 0; i < events.size(); i++) {
+ XMLEvent event = events.get(i);
+
+ if (event.getEventType() == XMLStreamConstants.CHARACTERS) {
+ String data = event.asCharacters().getData();
+
+ String pre = "";
+ String post = "";
+
+ if (normalizeSpace(data).equals("") && ((i == events.size() - 2 && equalsEnd(events.get(i + 1), ObflQName.BLOCK, ObflQName.TOC_ENTRY)) || i == events.size() - 1)) {
+ // this is the last element in the block, ignore
+ } else if (i > 0) {
+ XMLEvent preceedingEvent = events.get(i - 1);
+ if (preceedingEvent.isEndElement() && beginWS.matcher(data).find() && isPreserveElement(preceedingEvent.asEndElement().getName())) {
+ pre = " ";
+ } else if (equalsEnd(preceedingEvent, ObflQName.SPAN, ObflQName.STYLE) && beginWS.matcher(data).find()) {
+ pre = " ";
+ } else if (equalsEnd(preceedingEvent, ObflQName.MARKER, ObflQName.ANCHOR)) {
+ if (beginWS.matcher(data).find()) {
+ pre = " ";
+ } else {
+ int j = untilEventIsNotBackward(events, i - 1, ObflQName.MARKER, ObflQName.ANCHOR);
+ if (j > -1) {
+ XMLEvent upstream = events.get(j);
+ if (upstream.isCharacters() && endWS.matcher(upstream.asCharacters().getData()).find()) {
+ pre = " ";
+ }
+ }
+ }
+ } else if (preceedingEvent.isEndElement()) {
+ int j = untilEventIsNotBackward(events, i - 1, XMLStreamConstants.END_ELEMENT);
+ if (j > -1) {
+ XMLEvent upstream = events.get(j);
+ if (upstream.isCharacters() && endWS.matcher(upstream.asCharacters().getData()).find()) {
+ pre = " ";
+ }
+ }
+ }
+
+ }
+ if (i < events.size() - 1) {
+ XMLEvent followingEvent = events.get(i + 1);
+ if (normalizeSpace(data).equals("")) {
+ // don't output post
+ if (equalsStart(followingEvent, ObflQName.MARKER)) {
+ pre = "";
+ }
+ } else if (followingEvent.isStartElement() && endWS.matcher(data).find() && isPreserveElement(followingEvent.asStartElement().getName())) {
+ post = " ";
+ } else if ((equalsStart(followingEvent, ObflQName.SPAN, ObflQName.STYLE)) && endWS.matcher(data).find()) {
+ post = " ";
+ } else if (followingEvent.isStartElement()) {
+ int j = untilEventIsNotForward(events, i + 1, XMLStreamConstants.START_ELEMENT);
+ if (j > -1) {
+ XMLEvent downstream = events.get(j);
+ if (downstream.isCharacters() && beginWS.matcher(downstream.asCharacters().getData()).find()) {
+ post = " ";
+ }
+ }
+ }
+ }
+ // System.out.println("'" + pre + "'" + normalizeSpace(data) +
+ // "'" + post + "'");
+ modified.add(eventFactory.createCharacters(pre + normalizeSpace(data) + post));
+ } else if (equalsStart(event, ObflQName.SPAN, ObflQName.STYLE)) {
+
+ if (i > 0) {
+ int j = untilEventIsNotBackward(events, i - 1, ObflQName.MARKER, ObflQName.ANCHOR);
+ if (!(j > -1 && j < i - 1)) {
+ j = untilEventIsNotBackward(events, i - 1, XMLStreamConstants.END_ELEMENT);
+ }
+ if (j > -1 && j < i - 1) {
+ XMLEvent upstream = events.get(j);
+ if (upstream.isCharacters() && endWS.matcher(upstream.asCharacters().getData()).find()) {
+ modified.add(eventFactory.createCharacters(" "));
+ }
+ }
+ }
+
+ modified.add(event);
+
+ } else if (equalsEnd(event, ObflQName.SPAN, ObflQName.STYLE)) {
+ modified.add(event);
+ if (i < events.size() - 1) {
+ int j = untilEventIsNotForward(events, i + 1, XMLStreamConstants.START_ELEMENT);
+ if (j > -1 && j > i + 1) {
+ XMLEvent downstream = events.get(j);
+ if (downstream.isCharacters() && beginWS.matcher(downstream.asCharacters().getData()).find()) {
+ modified.add(eventFactory.createCharacters(" "));
+ }
+ }
+ }
+ } else {
+ modified.add(event);
+ }
+ }
+
+ // write result
+ for (XMLEvent event : modified) {
+ writer.add(event);
+ // System.out.print(event);
+ }
+ // System.out.println();
+ }
+
+ private boolean isPreserveElement(QName name) {
+ return name.equals(ObflQName.PAGE_NUMBER) || name.equals(ObflQName.LEADER) || name.equals(ObflQName.EVALUATE);
+ }
+
+ private int untilEventIsNotForward(List events, final int i, final int eventType) {
+ for (int j = i; j < events.size(); j++) {
+ if (events.get(j).getEventType() != eventType) {
+ return j;
+ }
+ }
+ return -1;
+ }
+
+ private int untilEventIsNotBackward(List events, final int i, final int eventType) {
+ for (int j = 0; j < i; j++) {
+ if (events.get(i - j).getEventType() != eventType) {
+ return i - j;
+ }
+ }
+ return -1;
+ }
+
+ private int untilEventIsNotBackward(List events, final int i, QName... name) {
+ for (int j = 0; j < i; j++) {
+ XMLEvent event = events.get(i - j);
+ boolean found = false;
+ for (QName n : name) {
+ if (equalsStart(event, n) || equalsEnd(event, n)) {
+ found = true;
+ break;
+ }
+ }
+ if (found) {
+ // continue
+ } else {
+ return i - j;
+ }
+ }
+ return -1;
+ }
+
+ private String normalizeSpace(String input) {
+ return input.replaceAll("\\s+", " ").trim();
+ }
+
+ private boolean equalsStart(XMLEvent event, QName... element) {
+ for (QName n : element) {
+ if (event.getEventType() == XMLStreamConstants.START_ELEMENT && event.asStartElement().getName().equals(n)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private boolean equalsEnd(XMLEvent event, QName... element) {
+ for (QName n : element) {
+ if (event.getEventType() == XMLStreamConstants.END_ELEMENT && event.asEndElement().getName().equals(n)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * @param args
+ */
+ public static void main(String[] args) {
+ try {
+ XMLInputFactory inFactory = XMLInputFactory.newInstance();
+ inFactory.setProperty(XMLInputFactory.IS_COALESCING, Boolean.TRUE);
+ inFactory.setProperty(XMLInputFactory.IS_NAMESPACE_AWARE, Boolean.TRUE);
+ inFactory.setProperty(XMLInputFactory.SUPPORT_DTD, Boolean.FALSE);
+ inFactory.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, Boolean.FALSE);
+ OBFLWsNormalizer p = new OBFLWsNormalizer(inFactory.createXMLEventReader(new FileInputStream("ws-test-input.xml")), XMLEventFactory.newInstance(), new FileOutputStream("out.xml"));
+ p.parse(XMLOutputFactory.newInstance());
+ } catch (FileNotFoundException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ } catch (XMLStreamException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+ }
+}
diff --git a/src/org/daisy/dotify/obfl/ObflParser.java b/src/org/daisy/dotify/obfl/ObflParser.java
new file mode 100644
index 00000000..46eae417
--- /dev/null
+++ b/src/org/daisy/dotify/obfl/ObflParser.java
@@ -0,0 +1,866 @@
+package org.daisy.dotify.obfl;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Stack;
+import java.util.logging.Logger;
+
+import javax.xml.namespace.QName;
+import javax.xml.stream.Location;
+import javax.xml.stream.XMLEventReader;
+import javax.xml.stream.XMLStreamConstants;
+import javax.xml.stream.XMLStreamException;
+import javax.xml.stream.events.Attribute;
+import javax.xml.stream.events.XMLEvent;
+
+import org.daisy.dotify.api.formatter.BlockPosition.VerticalAlignment;
+import org.daisy.dotify.api.formatter.BlockProperties;
+import org.daisy.dotify.api.formatter.CompoundField;
+import org.daisy.dotify.api.formatter.CurrentPageField;
+import org.daisy.dotify.api.formatter.Field;
+import org.daisy.dotify.api.formatter.Formatter;
+import org.daisy.dotify.api.formatter.FormatterFactory;
+import org.daisy.dotify.api.formatter.FormattingTypes;
+import org.daisy.dotify.api.formatter.LayoutMaster;
+import org.daisy.dotify.api.formatter.Leader;
+import org.daisy.dotify.api.formatter.MarkerReferenceField;
+import org.daisy.dotify.api.formatter.MarkerReferenceField.MarkerSearchDirection;
+import org.daisy.dotify.api.formatter.MarkerReferenceField.MarkerSearchScope;
+import org.daisy.dotify.api.formatter.NumeralField.NumeralStyle;
+import org.daisy.dotify.api.formatter.PageTemplate;
+import org.daisy.dotify.api.formatter.Position;
+import org.daisy.dotify.api.formatter.SequenceProperties;
+import org.daisy.dotify.api.formatter.StringField;
+import org.daisy.dotify.api.formatter.TextProperties;
+import org.daisy.dotify.api.formatter.Volume;
+import org.daisy.dotify.api.formatter.VolumeContentFormatter;
+import org.daisy.dotify.api.obfl.ExpressionFactory;
+import org.daisy.dotify.api.translator.MarkerProcessor;
+import org.daisy.dotify.api.translator.TextAttribute;
+import org.daisy.dotify.api.translator.TextBorderConfigurationException;
+import org.daisy.dotify.api.translator.TextBorderFactory;
+import org.daisy.dotify.api.translator.TextBorderFactoryMakerService;
+import org.daisy.dotify.api.translator.TextBorderStyle;
+import org.daisy.dotify.api.writer.MetaDataItem;
+import org.daisy.dotify.obfl.EventContents.ContentType;
+import org.daisy.dotify.obfl.TocSequenceEvent.TocRange;
+import org.daisy.dotify.text.FilterLocale;
+import org.daisy.dotify.translator.DefaultTextAttribute;
+
+/**
+ * Provides a parser for OBFL. The parser accepts OBFL input, either
+ * as an InputStream or as an XMLEventReader.
+ *
+ * @author Joel Håkansson
+ *
+ */
+public class ObflParser {
+
+ private HashMap tocs;
+ private HashMap masters;
+ private Stack volumeTemplates;
+ private List meta;
+
+ private Formatter formatter;
+ private final FilterLocale locale;
+ private final String mode;
+ private final FormatterFactory formatterFactory;
+ private final MarkerProcessor mp;
+ private final TextBorderFactoryMakerService maker;
+ private final ExpressionFactory ef;
+
+ public ObflParser(String locale, String mode, MarkerProcessor mp, FormatterFactory formatterFactory, TextBorderFactoryMakerService maker, ExpressionFactory ef) {
+ this.locale = FilterLocale.parse(locale);
+ this.mode = mode;
+ this.formatterFactory = formatterFactory;
+ this.mp = mp;
+ this.maker = maker;
+ this.ef = ef;
+ }
+
+ public void parse(XMLEventReader input) throws XMLStreamException, OBFLParserException {
+ this.formatter = formatterFactory.newFormatter(locale.toString(), mode);
+ this.tocs = new HashMap();
+ this.masters = new HashMap();
+ this.volumeTemplates = new Stack();
+ this.meta = new ArrayList();
+ formatter.open();
+ XMLEvent event;
+ FilterLocale locale = null;
+ boolean hyphenate = true;
+ while (input.hasNext()) {
+ event = input.nextEvent();
+ if (equalsStart(event, ObflQName.OBFL)) {
+ String loc = getAttr(event, ObflQName.ATTR_XML_LANG);
+ if (loc==null) {
+ throw new OBFLParserException("Missing xml:lang on root element");
+ } else {
+ locale = FilterLocale.parse(loc);
+ }
+ hyphenate = getHyphenate(event, hyphenate);
+ } else if (equalsStart(event, ObflQName.META)) {
+ parseMeta(event, input);
+ } else if (equalsStart(event, ObflQName.LAYOUT_MASTER)) {
+ parseLayoutMaster(event, input);
+ } else if (equalsStart(event, ObflQName.SEQUENCE)) {
+ parseSequence(event, input, locale, hyphenate);
+ } else if (equalsStart(event, ObflQName.TABLE_OF_CONTENTS)) {
+ parseTableOfContents(event, input, locale, hyphenate);
+ } else if (equalsStart(event, ObflQName.VOLUME_TEMPLATE)) {
+ parseVolumeTemplate(event, input, locale, hyphenate);
+ } else {
+ report(event);
+ }
+ }
+ try {
+ input.close();
+ formatter.close();
+ } catch (IOException e) {
+ throw new OBFLParserException(e);
+ }
+ }
+
+ private void parseMeta(XMLEvent event, XMLEventReader input) throws XMLStreamException {
+ int level = 0;
+ while (input.hasNext()) {
+ event = input.nextEvent();
+ if (event.getEventType() == XMLStreamConstants.START_ELEMENT) {
+ level++;
+ if (level == 1) {
+ StringBuilder sb = new StringBuilder();
+ QName name = event.asStartElement().getName();
+ while (input.hasNext()) {
+ event = input.nextEvent();
+ if (event.getEventType() == XMLStreamConstants.START_ELEMENT) {
+ level++;
+ warning(event, "Nested meta data not supported.");
+ } else if (event.getEventType() == XMLStreamConstants.END_ELEMENT) {
+ level--;
+ } else if (event.getEventType() == XMLStreamConstants.CHARACTERS) {
+ sb.append(event.asCharacters().getData());
+ } else {
+ report(event);
+ }
+ if (level < 2) {
+ break;
+ }
+ }
+ meta.add(new MetaDataItem(name, sb.toString()));
+ } else {
+ warning(event, "Nested meta data not supported.");
+ }
+ } else if (equalsEnd(event, ObflQName.META)) {
+ break;
+ } else if (event.getEventType() == XMLStreamConstants.END_ELEMENT) {
+ level--;
+ } else {
+ report(event);
+ }
+ }
+ }
+
+ private void report(XMLEvent event) {
+ if (event.isEndElement()) {
+ // ok
+ } else if (event.isStartElement()) {
+ String msg = "Unsupported context for element: " + event.asStartElement().getName() + buildLocationMsg(event.getLocation());
+ //throw new UnsupportedOperationException(msg);
+ Logger.getLogger(this.getClass().getCanonicalName()).warning(msg);
+ } else if (event.isStartDocument() || event.isEndDocument()) {
+ // ok
+ } else {
+ Logger.getLogger(this.getClass().getCanonicalName()).warning(event.toString());
+ }
+ }
+
+ private void warning(XMLEvent event, String msg) {
+ Logger.getLogger(this.getClass().getCanonicalName()).warning(msg + buildLocationMsg(event.getLocation()));
+ }
+
+ public String buildLocationMsg(Location location) {
+ int line = -1;
+ int col = -1;
+ if (location != null) {
+ line = location.getLineNumber();
+ col = location.getColumnNumber();
+ }
+ return (line > -1 ? " (at line: " + line + (col > -1 ? ", column: " + col : "") + ") " : "");
+ }
+
+ //TODO: parse page-number-variable
+ private void parseLayoutMaster(XMLEvent event, XMLEventReader input) throws XMLStreamException {
+ @SuppressWarnings("unchecked")
+ Iterator i = event.asStartElement().getAttributes();
+ int width = Integer.parseInt(getAttr(event, ObflQName.ATTR_PAGE_WIDTH));
+ int height = Integer.parseInt(getAttr(event, ObflQName.ATTR_PAGE_HEIGHT));
+ String masterName = getAttr(event, ObflQName.ATTR_NAME);
+ LayoutMasterImpl.Builder masterConfig = new LayoutMasterImpl.Builder(width, height, ef);
+ while (i.hasNext()) {
+ Attribute atts = i.next();
+ String name = atts.getName().getLocalPart();
+ String value = atts.getValue();
+ if (name.equals("inner-margin")) {
+ masterConfig.innerMargin(Integer.parseInt(value));
+ } else if (name.equals("outer-margin")) {
+ masterConfig.outerMargin(Integer.parseInt(value));
+ } else if (name.equals("row-spacing")) {
+ masterConfig.rowSpacing(Float.parseFloat(value));
+ } else if (name.equals("duplex")) {
+ masterConfig.duplex(value.equals("true"));
+ } else if (name.equals("frame")) {
+ HashSet set = new HashSet(Arrays.asList(value.split(" ")));
+ HashMap features = new HashMap();
+ features.put(TextBorderFactory.FEATURE_MODE, formatter.getTranslator().getTranslatorMode());
+ features.put(TextBorderFactory.FEATURE_STYLE, set);
+ TextBorderStyle style = null;
+ try {
+ style = maker.newTextBorderStyle(features);
+ } catch (TextBorderConfigurationException e) {
+ }
+ masterConfig.frame(style);
+ }
+ }
+ while (input.hasNext()) {
+ event=input.nextEvent();
+ if (equalsStart(event, ObflQName.TEMPLATE)) {
+ masterConfig.addTemplate(parseTemplate(event, input));
+ } else if (equalsStart(event, ObflQName.DEFAULT_TEMPLATE)) {
+ masterConfig.addTemplate(parseTemplate(event, input));
+ } else if (equalsEnd(event, ObflQName.LAYOUT_MASTER)) {
+ break;
+ } else {
+ report(event);
+ }
+ }
+ formatter.addLayoutMaster(masterName, masterConfig.build());
+ masters.put(masterName, masterConfig.build());
+ }
+
+ private PageTemplate parseTemplate(XMLEvent event, XMLEventReader input) throws XMLStreamException {
+ PageTemplateImpl template;
+ if (equalsStart(event, ObflQName.TEMPLATE)) {
+ template = new PageTemplateImpl(getAttr(event, ObflQName.ATTR_USE_WHEN), ef);
+ } else {
+ template = new PageTemplateImpl(ef);
+ }
+ while (input.hasNext()) {
+ event=input.nextEvent();
+ if (equalsStart(event, ObflQName.HEADER)) {
+ ArrayList fields = parseHeaderFooter(event, input);
+ if (fields.size()>0) {
+ template.addToHeader(fields);
+ }
+ } else if (equalsStart(event, ObflQName.FOOTER)) {
+ ArrayList fields = parseHeaderFooter(event, input);
+ if (fields.size()>0) {
+ template.addToFooter(fields);
+ }
+ } else if (equalsEnd(event, ObflQName.TEMPLATE) || equalsEnd(event, ObflQName.DEFAULT_TEMPLATE)) {
+ break;
+ } else {
+ report(event);
+ }
+ }
+ return template;
+ }
+
+ private ArrayList parseHeaderFooter(XMLEvent event, XMLEventReader input) throws XMLStreamException {
+ ArrayList fields = new ArrayList();
+ while (input.hasNext()) {
+ event=input.nextEvent();
+ if (equalsStart(event, ObflQName.FIELD)) {
+ ArrayList compound = parseField(event, input);
+ if (compound.size()==1) {
+ fields.add(compound.get(0));
+ } else {
+ CompoundField f = new CompoundField();
+ f.addAll(compound);
+ fields.add(f);
+ }
+ } else if (equalsEnd(event, ObflQName.HEADER) || equalsEnd(event, ObflQName.FOOTER)) {
+ break;
+ } else {
+ report(event);
+ }
+ }
+ return fields;
+ }
+
+ private ArrayList parseField(XMLEvent event, XMLEventReader input) throws XMLStreamException {
+ ArrayList compound = new ArrayList();
+ while (input.hasNext()) {
+ event=input.nextEvent();
+ if (equalsStart(event, ObflQName.STRING)) {
+ compound.add(new StringField(getAttr(event, "value")));
+ } else if (equalsStart(event, ObflQName.EVALUATE)) {
+ //FIXME: add variables...
+ compound.add(new StringField(ef.newExpression().evaluate(getAttr(event, "expression"))));
+ } else if (equalsStart(event, ObflQName.CURRENT_PAGE)) {
+ compound.add(new CurrentPageField(NumeralStyle.valueOf(getAttr(event, "style").replace('-', '_').toUpperCase())));
+ } else if (equalsStart(event, ObflQName.MARKER_REFERENCE)) {
+ compound.add(
+ new MarkerReferenceField(
+ getAttr(event, "marker"),
+ MarkerSearchDirection.valueOf(getAttr(event, "direction").toUpperCase()),
+ MarkerSearchScope.valueOf(getAttr(event, "scope").toUpperCase())
+ )
+ );
+ } else if (equalsEnd(event, ObflQName.FIELD)) {
+ break;
+ } else {
+ report(event);
+ }
+ }
+ return compound;
+ }
+
+ private void parseSequence(XMLEvent event, XMLEventReader input, FilterLocale locale, boolean hyph) throws XMLStreamException {
+ String masterName = getAttr(event, "master");
+ locale = getLang(event, locale);
+ hyph = getHyphenate(event, hyph);
+ SequenceProperties.Builder builder = new SequenceProperties.Builder(masterName);
+ String initialPageNumber = getAttr(event, "initial-page-number");
+ if (initialPageNumber!=null) {
+ builder.initialPageNumber(Integer.parseInt(initialPageNumber));
+ }
+ formatter.newSequence(builder.build());
+ while (input.hasNext()) {
+ event=input.nextEvent();
+ if (equalsStart(event, ObflQName.BLOCK)) {
+ parseBlock(event, input, locale, hyph);
+ }/* else if (equalsStart(event, LEADER)) {
+ parseLeader(event, input);
+ }*/
+ else if (equalsEnd(event, ObflQName.SEQUENCE)) {
+ break;
+ } else {
+ report(event);
+ }
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ private void parseBlock(XMLEvent event, XMLEventReader input, FilterLocale locale, boolean hyph) throws XMLStreamException {
+ formatter.startBlock(blockBuilder(event.asStartElement().getAttributes()));
+ locale = getLang(event, locale);
+ hyph = getHyphenate(event, hyph);
+ while (input.hasNext()) {
+ event=input.nextEvent();
+ if (event.isCharacters()) {
+ formatter.addChars(event.asCharacters().getData(), new TextProperties.Builder(locale.toString()).hyphenate(hyph).build());
+ } else if (equalsStart(event, ObflQName.BLOCK)) {
+ parseBlock(event, input, locale, hyph);
+ } else if (equalsStart(event, ObflQName.SPAN)) {
+ parseSpan(event, input, locale, hyph);
+ } else if (equalsStart(event, ObflQName.STYLE)) {
+ parseStyle(event, input, locale, hyph);
+ } else if (equalsStart(event, ObflQName.LEADER)) {
+ formatter.insertLeader(parseLeader(event, input));
+ } else if (equalsStart(event, ObflQName.MARKER)) {
+ formatter.insertMarker(parseMarker(event, input));
+ } else if (equalsStart(event, ObflQName.BR)) {
+ formatter.newLine();
+ scanEmptyElement(input, ObflQName.BR);
+ }
+ // TODO:anchor, evaluate, page-number
+ else if (equalsEnd(event, ObflQName.BLOCK)) {
+ break;
+ } else {
+ report(event);
+ }
+ }
+ formatter.endBlock();
+ }
+
+ private void parseSpan(XMLEvent event, XMLEventReader input, FilterLocale locale, boolean hyph) throws XMLStreamException {
+ locale = getLang(event, locale);
+ hyph = getHyphenate(event, hyph);
+ while (input.hasNext()) {
+ event=input.nextEvent();
+ if (event.isCharacters()) {
+ formatter.addChars(event.asCharacters().getData(), new TextProperties.Builder(locale.toString()).hyphenate(hyph).build());
+ } else if (equalsStart(event, ObflQName.STYLE)) {
+ parseStyle(event, input, locale, hyph);
+ } else if (equalsStart(event, ObflQName.LEADER)) {
+ formatter.insertLeader(parseLeader(event, input));
+ } else if (equalsStart(event, ObflQName.MARKER)) {
+ formatter.insertMarker(parseMarker(event, input));
+ } else if (equalsStart(event, ObflQName.BR)) {
+ formatter.newLine();
+ scanEmptyElement(input, ObflQName.BR);
+ }
+ else if (equalsEnd(event, ObflQName.SPAN)) {
+ break;
+ } else {
+ report(event);
+ }
+ }
+ }
+
+ private void parseStyle(XMLEvent event, XMLEventReader input, FilterLocale locale, boolean hyph) throws XMLStreamException {
+ TextProperties tp = new TextProperties.Builder(locale.toString()).hyphenate(hyph).build();
+
+ BlockEventHandler eh = new BlockEventHandler(formatter, ef);
+ eh.insertEventContents(parseStyleEvent(event, input, tp));
+ }
+
+ private StyleEvent parseStyleEvent(XMLEvent event, XMLEventReader input, TextProperties tp) throws XMLStreamException {
+
+ StyleEvent ev = parseStyleEventInner(event, input, tp);
+ {
+ TextAttribute t = processTextAttributes(ev);
+ List chunks = TextContents.getTextSegments(ev);
+ TextContents.updateTextContents(ev, mp.processAttributesRetain(t, chunks.toArray(new String[] {})));
+ }
+ return ev;
+ }
+
+ /**
+ * Builds a DOM over the style sub tree.
+ *
+ * @param event
+ * @param input
+ * @param evr
+ * @param tp
+ * @return
+ * @throws XMLStreamException
+ */
+ private StyleEvent parseStyleEventInner(XMLEvent event, XMLEventReader input, TextProperties tp) throws XMLStreamException {
+ StyleEvent ev = new StyleEvent(getAttr(event, "name"));
+ while (input.hasNext()) {
+ event = input.nextEvent();
+ if (event.isCharacters()) {
+ String sr = event.asCharacters().getData();
+ ev.add(new TextContents(sr, tp));
+ } else if (equalsStart(event, ObflQName.STYLE)) {
+ ev.add(parseStyleEventInner(event, input, tp));
+ } else if (equalsStart(event, ObflQName.MARKER)) {
+ ev.add(parseMarker(event, input));
+ } else if (equalsStart(event, ObflQName.BR)) {
+ ev.add(new LineBreak());
+ } else if (equalsEnd(event, ObflQName.STYLE)) {
+ return ev;
+ } else {
+ report(event);
+ }
+ }
+ return null;
+ }
+
+ private TextAttribute processTextAttributes(StyleEvent ev) throws XMLStreamException {
+ // StringBuilder sb = new StringBuilder();
+ int len = 0;
+ DefaultTextAttribute.Builder ret = new DefaultTextAttribute.Builder(ev.getName());
+ if (ev.size() == 1 && ev.get(0).getContentType() == ContentType.PCDATA) {
+ return ret.build(((TextContents) ev.get(0)).getText().length());
+ } else {
+ for (EventContents c : ev) {
+ switch (c.getContentType()) {
+ case PCDATA:
+ String sr = ((TextContents) c).getText();
+ ret.add(new DefaultTextAttribute.Builder().build(sr.length()));
+ len += sr.length();
+ // sb.append(sr);
+ break;
+ case STYLE:
+ TextAttribute t = processTextAttributes((StyleEvent) c);
+ ret.add(t);
+ len += t.getWidth();
+ break;
+ case MARKER:
+ case BR:
+ // ignore
+ break;
+ default:
+ throw new UnsupportedOperationException("Unknown element: " + ev);
+ }
+ }
+ return ret.build(len);
+ }
+ }
+
+ private BlockProperties blockBuilder(Iterator atts) {
+ BlockProperties.Builder builder = new BlockProperties.Builder();
+ while (atts.hasNext()) {
+ Attribute att = atts.next();
+ String name = att.getName().getLocalPart();
+ if (name.equals("margin-left")) {
+ builder.leftMargin(Integer.parseInt(att.getValue()));
+ } else if (name.equals("margin-right")) {
+ builder.rightMargin(Integer.parseInt(att.getValue()));
+ } else if (name.equals("margin-top")) {
+ builder.topMargin(Integer.parseInt(att.getValue()));
+ } else if (name.equals("margin-bottom")) {
+ builder.bottomMargin(Integer.parseInt(att.getValue()));
+ } else if (name.equals("text-indent")) {
+ builder.textIndent(Integer.parseInt(att.getValue()));
+ } else if (name.equals("first-line-indent")) {
+ builder.firstLineIndent(Integer.parseInt(att.getValue()));
+ } else if (name.equals("list-type")) {
+ builder.listType(FormattingTypes.ListStyle.valueOf(att.getValue().toUpperCase()));
+ } else if (name.equals("break-before")) {
+ builder.breakBefore(FormattingTypes.BreakBefore.valueOf(att.getValue().toUpperCase()));
+ } else if (name.equals("keep")) {
+ builder.keep(FormattingTypes.Keep.valueOf(att.getValue().toUpperCase()));
+ } else if (name.equals("keep-with-next")) {
+ builder.keepWithNext(Integer.parseInt(att.getValue()));
+ } else if (name.equals("keep-with-previous-sheets")) {
+ builder.keepWithPreviousSheets(Integer.parseInt(att.getValue()));
+ } else if (name.equals("keep-with-next-sheets")) {
+ builder.keepWithNextSheets(Integer.parseInt(att.getValue()));
+ } else if (name.equals("block-indent")) {
+ builder.blockIndent(Integer.parseInt(att.getValue()));
+ } else if (name.equals("id")) {
+ builder.identifier(att.getValue());
+ } else if (name.equals("align")) {
+ builder.align(FormattingTypes.Alignment.valueOf(att.getValue().toUpperCase()));
+ } else if (name.equals("vertical-position")) {
+ builder.verticalPosition(Position.parsePosition(att.getValue()));
+ } else if (name.equals("vertical-align")) {
+ builder.verticalAlignment(VerticalAlignment.valueOf(att.getValue().toUpperCase()));
+ }
+ }
+ return builder.build();
+ }
+
+ private LeaderEventContents parseLeader(XMLEvent event, XMLEventReader input) throws XMLStreamException {
+ LeaderEventContents.Builder builder = new LeaderEventContents.Builder();
+ @SuppressWarnings("unchecked")
+ Iterator atts = event.asStartElement().getAttributes();
+ while (atts.hasNext()) {
+ Attribute att = atts.next();
+ String name = att.getName().getLocalPart();
+ if (name.equals("align")) {
+ builder.align(Leader.Alignment.valueOf(att.getValue().toUpperCase()));
+ } else if (name.equals("position")) {
+ builder.position(Position.parsePosition(att.getValue()));
+ } else if (name.equals("pattern")) {
+ builder.pattern(att.getValue());
+ } else {
+ report(event);
+ }
+ }
+ scanEmptyElement(input, ObflQName.LEADER);
+ return new LeaderEventContents(builder);
+ }
+
+ private MarkerEventContents parseMarker(XMLEvent event, XMLEventReader input) throws XMLStreamException {
+ String markerName = getAttr(event, "class");
+ String markerValue = getAttr(event, "value");
+ return new MarkerEventContents(markerName, markerValue);
+ }
+
+ private void parseTableOfContents(XMLEvent event, XMLEventReader input, FilterLocale locale, boolean hyph) throws XMLStreamException {
+ String tocName = getAttr(event, ObflQName.ATTR_NAME);
+ locale = getLang(event, locale);
+ hyph = getHyphenate(event, hyph);
+ TableOfContentsImpl toc = new TableOfContentsImpl();
+ while (input.hasNext()) {
+ event=input.nextEvent();
+ if (equalsStart(event, ObflQName.TOC_ENTRY)) {
+ toc.add(parseTocEntry(event, input, toc, locale, hyph));
+ } else if (equalsEnd(event, ObflQName.TABLE_OF_CONTENTS)) {
+ break;
+ } else {
+ report(event);
+ }
+ }
+ tocs.put(tocName, toc);
+ }
+
+ @SuppressWarnings("unchecked")
+ private BlockEvent parseTocEntry(XMLEvent event, XMLEventReader input, TableOfContentsImpl toc, FilterLocale locale, boolean hyph) throws XMLStreamException {
+ String refId = getAttr(event, "ref-id");
+ locale = getLang(event, locale);
+ hyph = getHyphenate(event, hyph);
+ String tocId;
+ do {
+ tocId = ""+((int)Math.round((99999999*Math.random())));
+ } while (toc.containsTocID(tocId));
+ TocBlockEvent ret = new TocBlockEvent(refId, tocId, blockBuilder(event.asStartElement().getAttributes()));
+ while (input.hasNext()) {
+ event=input.nextEvent();
+ if (event.isCharacters()) {
+ ret.add(new TextContents(event.asCharacters().getData(), new TextProperties.Builder(locale.toString()).hyphenate(hyph).build()));
+ } else if (equalsStart(event, ObflQName.TOC_ENTRY)) {
+ ret.add(parseTocEntry(event, input, toc, locale, hyph));
+ } else if (equalsStart(event, ObflQName.LEADER)) {
+ ret.add(parseLeader(event, input));
+ } else if (equalsStart(event, ObflQName.MARKER)) {
+ ret.add(parseMarker(event, input));
+ } else if (equalsStart(event, ObflQName.BR)) {
+ ret.add(new LineBreak());
+ scanEmptyElement(input, ObflQName.BR);
+ } else if (equalsStart(event, ObflQName.PAGE_NUMBER)) {
+ ret.add(parsePageNumber(event, input));
+ } else if (equalsStart(event, ObflQName.ANCHOR)) {
+ //TODO: implement
+ throw new UnsupportedOperationException("Not implemented");
+ // TODO: span, style
+ } else if (equalsStart(event, ObflQName.EVALUATE)) {
+ ret.add(parseEvaluate(event, input, locale, hyph));
+ }
+ else if (equalsEnd(event, ObflQName.TOC_ENTRY)) {
+ break;
+ } else {
+ report(event);
+ }
+ }
+ return ret;
+ }
+
+ private PageNumberReferenceEventContents parsePageNumber(XMLEvent event, XMLEventReader input) throws XMLStreamException {
+ String refId = getAttr(event, "ref-id");
+ NumeralStyle style = NumeralStyle.DEFAULT;
+ String styleStr = getAttr(event, "style");
+ if (styleStr!=null) {
+ try {
+ style = NumeralStyle.valueOf(styleStr.replace('-', '_').toUpperCase());
+ } catch (Exception e) { }
+ }
+ scanEmptyElement(input, ObflQName.PAGE_NUMBER);
+ return new PageNumberReferenceEventContents(refId, style);
+ }
+
+ private Evaluate parseEvaluate(XMLEvent event, XMLEventReader input, FilterLocale locale, boolean hyph) throws XMLStreamException {
+ String expr = getAttr(event, "expression");
+ scanEmptyElement(input, ObflQName.EVALUATE);
+ return new Evaluate(expr, new TextProperties.Builder(locale.toString()).hyphenate(hyph).build());
+ }
+
+ private void parseVolumeTemplate(XMLEvent event, XMLEventReader input, FilterLocale locale, boolean hyph) throws XMLStreamException {
+ String volumeVar = getAttr(event, "volume-number-variable");
+ String volumeCountVar = getAttr(event, "volume-count-variable");
+ String useWhen = getAttr(event, ObflQName.ATTR_USE_WHEN);
+ String splitterMax = getAttr(event, "sheets-in-volume-max");
+ VolumeTemplateImpl template = new VolumeTemplateImpl(volumeVar, volumeCountVar, useWhen, Integer.parseInt(splitterMax), ef);
+ while (input.hasNext()) {
+ event=input.nextEvent();
+ if (equalsStart(event, ObflQName.PRE_CONTENT)) {
+ template.setPreVolumeContent(parsePreVolumeContent(event, input, template, locale, hyph));
+ } else if (equalsStart(event, ObflQName.POST_CONTENT)) {
+ template.setPostVolumeContent(parsePostVolumeContent(event, input, locale, hyph));
+ } else if (equalsEnd(event, ObflQName.VOLUME_TEMPLATE)) {
+ break;
+ } else {
+ report(event);
+ }
+ }
+ volumeTemplates.push(template);
+ }
+
+ private Iterable parsePreVolumeContent(XMLEvent event, XMLEventReader input, VolumeTemplate template, FilterLocale locale, boolean hyph) throws XMLStreamException {
+ ArrayList ret = new ArrayList();
+ while (input.hasNext()) {
+ event=input.nextEvent();
+ if (equalsStart(event, ObflQName.SEQUENCE)) {
+ ret.add(parseVolumeSequence(event, input, locale, hyph));
+ } else if (equalsStart(event, ObflQName.TOC_SEQUENCE)) {
+ ret.add(parseTocSequence(event, input, template, locale, hyph));
+ } else if (equalsEnd(event, ObflQName.PRE_CONTENT)) {
+ break;
+ } else {
+ report(event);
+ }
+ }
+ return ret;
+ }
+
+ private Iterable parsePostVolumeContent(XMLEvent event, XMLEventReader input, FilterLocale locale, boolean hyph) throws XMLStreamException {
+ ArrayList ret = new ArrayList();
+ while (input.hasNext()) {
+ event=input.nextEvent();
+ if (equalsStart(event, ObflQName.SEQUENCE)) {
+ ret.add(parseVolumeSequence(event, input, locale, hyph));
+ } else if (equalsEnd(event, ObflQName.POST_CONTENT)) {
+ break;
+ } else {
+ report(event);
+ }
+ }
+ return ret;
+ }
+
+ private VolumeSequenceEvent parseVolumeSequence(XMLEvent event, XMLEventReader input, FilterLocale locale, boolean hyph) throws XMLStreamException {
+ String masterName = getAttr(event, "master");
+ locale = getLang(event, locale);
+ hyph = getHyphenate(event, hyph);
+ SequenceProperties.Builder builder = new SequenceProperties.Builder(masterName);
+ String initialPageNumber = getAttr(event, "initial-page-number");
+ if (initialPageNumber!=null) {
+ builder.initialPageNumber(Integer.parseInt(initialPageNumber));
+ }
+ StaticSequenceEventImpl volSeq = new StaticSequenceEventImpl(builder.build());
+ while (input.hasNext()) {
+ event=input.nextEvent();
+ if (equalsStart(event, ObflQName.BLOCK)) {
+ volSeq.add(parseBlockEvent(event, input, locale, hyph));
+ } else if (equalsEnd(event, ObflQName.SEQUENCE)) {
+ break;
+ } else {
+ report(event);
+ }
+ }
+ return volSeq;
+ }
+
+ private VolumeSequenceEvent parseTocSequence(XMLEvent event, XMLEventReader input, VolumeTemplate template, FilterLocale locale, boolean hyph) throws XMLStreamException {
+ String masterName = getAttr(event, "master");
+ String tocName = getAttr(event, "toc");
+ locale = getLang(event, locale);
+ hyph = getHyphenate(event, hyph);
+ SequenceProperties.Builder builder = new SequenceProperties.Builder(masterName);
+ String initialPageNumber = getAttr(event, "initial-page-number");
+ if (initialPageNumber!=null) {
+ builder.initialPageNumber(Integer.parseInt(initialPageNumber));
+ }
+ TocRange range = TocRange.valueOf(getAttr(event, "range").toUpperCase());
+ String condition = getAttr(event, ObflQName.ATTR_USE_WHEN);
+ String volEventVar = getAttr(event, "toc-event-volume-number-variable");
+ TocSequenceEventImpl tocSequence = new TocSequenceEventImpl(builder.build(), tocName, range, condition, volEventVar, template, ef);
+ while (input.hasNext()) {
+ event=input.nextEvent();
+ if (equalsStart(event, ObflQName.ON_TOC_START)) {
+ String tmp = getAttr(event, ObflQName.ATTR_USE_WHEN);
+ tocSequence.addTocStartEvents(parseOnEvent(event, input, ObflQName.ON_TOC_START, locale, hyph), tmp);
+ } else if (equalsStart(event, ObflQName.ON_VOLUME_START)) {
+ String tmp = getAttr(event, ObflQName.ATTR_USE_WHEN);
+ tocSequence.addVolumeStartEvents(parseOnEvent(event, input, ObflQName.ON_VOLUME_START, locale, hyph), tmp);
+ } else if (equalsStart(event, ObflQName.ON_VOLUME_END)) {
+ String tmp = getAttr(event, ObflQName.ATTR_USE_WHEN);
+ tocSequence.addVolumeEndEvents(parseOnEvent(event, input, ObflQName.ON_VOLUME_END, locale, hyph), tmp);
+ } else if (equalsStart(event, ObflQName.ON_TOC_END)) {
+ String tmp = getAttr(event, ObflQName.ATTR_USE_WHEN);
+ tocSequence.addTocEndEvents(parseOnEvent(event, input, ObflQName.ON_TOC_END, locale, hyph), tmp);
+ }
+ else if (equalsEnd(event, ObflQName.TOC_SEQUENCE)) {
+ break;
+ } else {
+ report(event);
+ }
+ }
+ return tocSequence;
+ }
+
+ private Iterable parseOnEvent(XMLEvent event, XMLEventReader input, QName end, FilterLocale locale, boolean hyph) throws XMLStreamException {
+ ArrayList ret = new ArrayList();
+ while (input.hasNext()) {
+ event=input.nextEvent();
+ if (equalsStart(event, ObflQName.BLOCK)) {
+ ret.add(parseBlockEvent(event, input, locale, hyph));
+ } else if (equalsEnd(event, end)) {
+ break;
+ } else {
+ report(event);
+ }
+ }
+ return ret;
+ }
+
+ @SuppressWarnings("unchecked")
+ private BlockEvent parseBlockEvent(XMLEvent event, XMLEventReader input, FilterLocale locale, boolean hyph) throws XMLStreamException {
+ BlockEventImpl ret = new BlockEventImpl(blockBuilder(event.asStartElement().getAttributes()));
+ locale = getLang(event, locale);
+ hyph = getHyphenate(event, hyph);
+ while (input.hasNext()) {
+ event=input.nextEvent();
+ if (event.isCharacters()) {
+ ret.add(new TextContents(event.asCharacters().getData(), new TextProperties.Builder(locale.toString()).hyphenate(hyph).build()));
+ } else if (equalsStart(event, ObflQName.BLOCK)) {
+ ret.add(parseBlockEvent(event, input, locale, hyph));
+ } else if (equalsStart(event, ObflQName.LEADER)) {
+ ret.add(parseLeader(event, input));
+ } else if (equalsStart(event, ObflQName.MARKER)) {
+ ret.add(parseMarker(event, input));
+ } else if (equalsStart(event, ObflQName.BR)) {
+ ret.add(new LineBreak());
+ scanEmptyElement(input, ObflQName.BR);
+ } else if (equalsStart(event, ObflQName.EVALUATE)) {
+ ret.add(parseEvaluate(event, input, locale, hyph));
+ } else if (equalsStart(event, ObflQName.STYLE)) {
+ ret.add(parseStyleEvent(event, input, new TextProperties.Builder(locale.toString()).hyphenate(hyph).build()));
+ } else if (equalsStart(event, ObflQName.SPAN)) {
+ // FIXME: implement span support. See DTB05532
+ }
+ else if (equalsEnd(event, ObflQName.BLOCK)) {
+ break;
+ } else {
+ report(event);
+ }
+ }
+ return ret;
+ }
+
+ private void scanEmptyElement(XMLEventReader input, QName element) throws XMLStreamException {
+ XMLEvent event;
+ while (input.hasNext()) {
+ event=input.nextEvent();
+ if (event.getEventType()!=XMLStreamConstants.END_ELEMENT) {
+ throw new RuntimeException("Unexpected input");
+ } else if (equalsEnd(event, element)) {
+ break;
+ }
+ }
+ }
+
+ private String getAttr(XMLEvent event, String attr) {
+ return getAttr(event, new QName(attr));
+ }
+
+ private String getAttr(XMLEvent event, QName attr) {
+ Attribute ret = event.asStartElement().getAttributeByName(attr);
+ if (ret==null) {
+ return null;
+ } else {
+ return ret.getValue();
+ }
+ }
+
+ private FilterLocale getLang(XMLEvent event, FilterLocale locale) {
+ String lang = getAttr(event, ObflQName.ATTR_XML_LANG);
+ if (lang!=null) {
+ if (lang.equals("")) {
+ return null;
+ } else {
+ return FilterLocale.parse(lang);
+ }
+ }
+ return locale;
+ }
+
+ private boolean getHyphenate(XMLEvent event, boolean hyphenate) {
+ String hyph = getAttr(event, ObflQName.ATTR_HYPHENATE);
+ if (hyph!=null) {
+ return hyph.equals("true");
+ }
+ return hyphenate;
+ }
+
+ private boolean equalsStart(XMLEvent event, QName element) {
+ return event.getEventType()==XMLStreamConstants.START_ELEMENT
+ && event.asStartElement().getName().equals(element);
+ }
+
+ private boolean equalsEnd(XMLEvent event, QName element) {
+ return event.getEventType()==XMLStreamConstants.END_ELEMENT
+ && event.asEndElement().getName().equals(element);
+ }
+
+ public Iterable getFormattedResult() {
+ return formatter.getVolumes(getVolumeContentFormatter());
+ }
+
+ public VolumeContentFormatter getVolumeContentFormatter() {
+ return new BlockEventHandlerRunner(locale.toString(), mode, masters, tocs, volumeTemplates, formatterFactory, ef);
+ }
+
+ public List getMetaData() {
+ return meta;
+ }
+
+}
diff --git a/src/org/daisy/dotify/obfl/ObflQName.java b/src/org/daisy/dotify/obfl/ObflQName.java
new file mode 100644
index 00000000..8ba2332c
--- /dev/null
+++ b/src/org/daisy/dotify/obfl/ObflQName.java
@@ -0,0 +1,46 @@
+package org.daisy.dotify.obfl;
+
+import javax.xml.namespace.QName;
+
+interface ObflQName {
+ final static QName OBFL = new QName("obfl");
+ final static QName META = new QName("meta");
+ final static QName LAYOUT_MASTER = new QName("layout-master");
+ final static QName TEMPLATE = new QName("template");
+ final static QName DEFAULT_TEMPLATE = new QName("default-template");
+ final static QName HEADER = new QName("header");
+ final static QName FOOTER = new QName("footer");
+ final static QName FIELD = new QName("field");
+ final static QName STRING = new QName("string");
+ final static QName EVALUATE = new QName("evaluate");
+ final static QName CURRENT_PAGE = new QName("current-page");
+ final static QName MARKER_REFERENCE = new QName("marker-reference");
+ final static QName BLOCK = new QName("block");
+ final static QName SPAN = new QName("span");
+ final static QName STYLE = new QName("style");
+ final static QName TOC_ENTRY = new QName("toc-entry");
+ final static QName LEADER = new QName("leader");
+ final static QName MARKER = new QName("marker");
+ final static QName ANCHOR = new QName("anchor");
+ final static QName BR = new QName("br");
+ final static QName PAGE_NUMBER = new QName("page-number");
+
+ final static QName SEQUENCE = new QName("sequence");
+ final static QName VOLUME_TEMPLATE = new QName("volume-template");
+ final static QName PRE_CONTENT = new QName("pre-content");
+ final static QName POST_CONTENT = new QName("post-content");
+ final static QName TOC_SEQUENCE = new QName("toc-sequence");
+ final static QName ON_TOC_START = new QName("on-toc-start");
+ final static QName ON_VOLUME_START = new QName("on-volume-start");
+ final static QName ON_VOLUME_END = new QName("on-volume-end");
+ final static QName ON_TOC_END = new QName("on-toc-end");
+
+ final static QName TABLE_OF_CONTENTS = new QName("table-of-contents");
+
+ final static QName ATTR_XML_LANG = new QName("http://www.w3.org/XML/1998/namespace", "lang", "xml");
+ final static QName ATTR_HYPHENATE = new QName("hyphenate");
+ final static QName ATTR_PAGE_WIDTH = new QName("page-width");
+ final static QName ATTR_PAGE_HEIGHT = new QName("page-height");
+ final static QName ATTR_NAME = new QName("name");
+ final static QName ATTR_USE_WHEN = new QName("use-when");
+}
diff --git a/src/org/daisy/dotify/obfl/PageNumberReference.java b/src/org/daisy/dotify/obfl/PageNumberReference.java
new file mode 100644
index 00000000..67c7892c
--- /dev/null
+++ b/src/org/daisy/dotify/obfl/PageNumberReference.java
@@ -0,0 +1,40 @@
+package org.daisy.dotify.obfl;
+
+import org.daisy.dotify.api.formatter.NumeralField.NumeralStyle;
+
+
+/**
+ * Provides a page number reference event object.
+ *
+ * @author Joel Håkansson
+ */
+class PageNumberReference {
+ private final String refid;
+ private final NumeralStyle style;
+
+ PageNumberReference(String refid, NumeralStyle style) {
+ this.refid = refid;
+ this.style = style;
+ }
+
+ /**
+ * Gets the identifier to the reference location.
+ * @return returns the reference identifier
+ */
+ public String getRefId() {
+ return refid;
+ }
+
+ /**
+ * Gets the numeral style for this page number reference
+ * @return returns the numeral style
+ */
+ public NumeralStyle getNumeralStyle() {
+ return style;
+ }
+
+ public boolean canContainEventObjects() {
+ return false;
+ }
+
+}
diff --git a/src/org/daisy/dotify/obfl/PageNumberReferenceEventContents.java b/src/org/daisy/dotify/obfl/PageNumberReferenceEventContents.java
new file mode 100644
index 00000000..aa603534
--- /dev/null
+++ b/src/org/daisy/dotify/obfl/PageNumberReferenceEventContents.java
@@ -0,0 +1,16 @@
+package org.daisy.dotify.obfl;
+
+import org.daisy.dotify.api.formatter.NumeralField.NumeralStyle;
+
+class PageNumberReferenceEventContents extends PageNumberReference
+ implements EventContents {
+
+
+ public PageNumberReferenceEventContents(String refid, NumeralStyle style) {
+ super(refid, style);
+ }
+
+ public ContentType getContentType() {
+ return ContentType.PAGE_NUMBER;
+ }
+}
diff --git a/src/org/daisy/dotify/obfl/PageTemplateImpl.java b/src/org/daisy/dotify/obfl/PageTemplateImpl.java
new file mode 100644
index 00000000..f6312b34
--- /dev/null
+++ b/src/org/daisy/dotify/obfl/PageTemplateImpl.java
@@ -0,0 +1,77 @@
+package org.daisy.dotify.obfl;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+
+import org.daisy.dotify.api.formatter.Field;
+import org.daisy.dotify.api.formatter.PageTemplate;
+import org.daisy.dotify.api.obfl.Expression;
+import org.daisy.dotify.api.obfl.ExpressionFactory;
+
+
+class PageTemplateImpl implements PageTemplate {
+ private final String condition;
+ private final List> header;
+ private final List> footer;
+ private final HashMap appliesTo;
+ private final ExpressionFactory ef;
+
+ public PageTemplateImpl(ExpressionFactory ef) {
+ this(null, ef);
+ }
+
+ /**
+ * Create a new SimpleTemplate.
+ * @param useWhen string to evaluate. In addition to the syntax of {@link Expression}, the value $page can be
+ * used. This will be replaced by the current page number before the expression is evaluated.
+ */
+ public PageTemplateImpl(String useWhen, ExpressionFactory ef) {
+ this.condition = useWhen;
+ this.header = new ArrayList>();
+ this.footer = new ArrayList>();
+ this.appliesTo = new HashMap();
+ this.ef = ef;
+ }
+
+ public void addToHeader(List obj) {
+ header.add(obj);
+ }
+
+ public void addToFooter(List obj) {
+ footer.add(obj);
+ }
+
+ public List