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 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 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 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 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 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> getHeader() { + //ArrayList> ret = new ArrayList>(); + //ret.add(header); + return header; + } + + public List> getFooter() { + //ArrayList> ret = new ArrayList>(); + //ret.add(footer); + return footer; + } + + public boolean appliesTo(int pagenum) { + if (condition==null) { + return true; + } + // keep a HashMap with calculated results + if (appliesTo.containsKey(pagenum)) { + return appliesTo.get(pagenum); + } + boolean applies = ef.newExpression().evaluate(condition.replaceAll("\\$page(?=\\W)", "" + pagenum)).equals(true); + appliesTo.put(pagenum, applies); + return applies; + } + + public int getFooterHeight() { + return footer.size(); + } + + public int getHeaderHeight() { + return header.size(); + } +} diff --git a/src/org/daisy/dotify/obfl/SequenceEvent.java b/src/org/daisy/dotify/obfl/SequenceEvent.java new file mode 100644 index 00000000..cfb197da --- /dev/null +++ b/src/org/daisy/dotify/obfl/SequenceEvent.java @@ -0,0 +1,11 @@ +package org.daisy.dotify.obfl; + +import java.util.Map; + + + +interface SequenceEvent extends Iterable, VolumeSequenceEvent { + + public void setEvaluateContext(Map vars); + +} diff --git a/src/org/daisy/dotify/obfl/StaticSequenceEvent.java b/src/org/daisy/dotify/obfl/StaticSequenceEvent.java new file mode 100644 index 00000000..7c0fe5fd --- /dev/null +++ b/src/org/daisy/dotify/obfl/StaticSequenceEvent.java @@ -0,0 +1,13 @@ +package org.daisy.dotify.obfl; + + + + +/** + * Provides a static sequence event object. + * + * @author Joel Håkansson + */ +interface StaticSequenceEvent extends SequenceEvent { + +} diff --git a/src/org/daisy/dotify/obfl/StaticSequenceEventImpl.java b/src/org/daisy/dotify/obfl/StaticSequenceEventImpl.java new file mode 100644 index 00000000..f9be08b9 --- /dev/null +++ b/src/org/daisy/dotify/obfl/StaticSequenceEventImpl.java @@ -0,0 +1,37 @@ +package org.daisy.dotify.obfl; + +import java.util.Map; +import java.util.Stack; + +import org.daisy.dotify.api.formatter.SequenceProperties; + +class StaticSequenceEventImpl extends Stack implements StaticSequenceEvent { + private final SequenceProperties props; + + /** + * Creates a new sequence event + * @param props + */ + public StaticSequenceEventImpl(SequenceProperties props) { + this.props = props; + } + + /** + * + */ + private static final long serialVersionUID = 4646831324973203983L; + + public SequenceProperties getSequenceProperties() { + return props; + } + + public VolumeSequenceType getVolumeSequenceType() { + return VolumeSequenceType.STATIC; + } + + public void setEvaluateContext(Map vars) { + for (BlockEvent e : this) { + e.setEvaluateContext(vars); + } + } +} diff --git a/src/org/daisy/dotify/obfl/StyleEvent.java b/src/org/daisy/dotify/obfl/StyleEvent.java new file mode 100644 index 00000000..22d0901c --- /dev/null +++ b/src/org/daisy/dotify/obfl/StyleEvent.java @@ -0,0 +1,30 @@ +package org.daisy.dotify.obfl; + +import java.util.Stack; + +class StyleEvent extends Stack implements IterableEventContents { + + /** + * + */ + private static final long serialVersionUID = -7263147868028801228L; + + private final String name; + + public StyleEvent(String name) { + this.name = name; + } + + public ContentType getContentType() { + return ContentType.STYLE; + } + + public String getName() { + return name; + } + + public boolean canContainEventObjects() { + return true; + } + +} diff --git a/src/org/daisy/dotify/obfl/TableOfContents.java b/src/org/daisy/dotify/obfl/TableOfContents.java new file mode 100644 index 00000000..575b079c --- /dev/null +++ b/src/org/daisy/dotify/obfl/TableOfContents.java @@ -0,0 +1,11 @@ +package org.daisy.dotify.obfl; + +import java.util.Set; + + +interface TableOfContents extends Iterable { + + public Set getTocIdList(); + + public String getRefForID(String id); +} diff --git a/src/org/daisy/dotify/obfl/TableOfContentsImpl.java b/src/org/daisy/dotify/obfl/TableOfContentsImpl.java new file mode 100644 index 00000000..483c44cc --- /dev/null +++ b/src/org/daisy/dotify/obfl/TableOfContentsImpl.java @@ -0,0 +1,71 @@ +package org.daisy.dotify.obfl; + +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.Set; +import java.util.Stack; + +import org.daisy.dotify.obfl.EventContents.ContentType; + + +/** + * Provides table of contents entries to be used when building a Table of Contents + * @author Joel Håkansson + */ +class TableOfContentsImpl implements TableOfContents { + private final Stack data; + private final LinkedHashMap refs; + + public TableOfContentsImpl() { + this.data = new Stack(); + this.refs = new LinkedHashMap(); + + } + + private void collectIds(BlockEvent e) { + String tocId = ((TocBlockEvent)e).getTocId(); + if (tocId!=null) { + if (refs.put(tocId, ((TocBlockEvent)e).getRefId())!=null) { + throw new RuntimeException("Identifier is not unique: " + tocId); + } + } + for (EventContents c : e) { + switch (c.getContentType()) { + case TOC_ENTRY: + collectIds((TocBlockEvent)c); + break; + default: + break; + } + } + } + + public boolean add(BlockEvent e) { + if (e.getContentType()!=ContentType.TOC_ENTRY) { + throw new IllegalArgumentException("Can only add toc entries to a TOC"); + } + collectIds(e); + return data.add(e); + } + + public boolean containsTocID(String id) { + return refs.containsKey(id); + } + + public Set getTocIdList() { + return refs.keySet(); + } + + public String getRefForID(String id) { + return refs.get(id); + } +/* + public LinkedHashMap getIdIdRefMap() { + return refs; + }*/ + + public Iterator iterator() { + return data.iterator(); + } + +} diff --git a/src/org/daisy/dotify/obfl/TextContents.java b/src/org/daisy/dotify/obfl/TextContents.java new file mode 100644 index 00000000..3ee04c11 --- /dev/null +++ b/src/org/daisy/dotify/obfl/TextContents.java @@ -0,0 +1,73 @@ +package org.daisy.dotify.obfl; + +import java.util.ArrayList; +import java.util.List; + +import org.daisy.dotify.api.formatter.TextProperties; + +/** + * Provides a text event object. + * @author Joel Håkansson + * + */ +class TextContents implements EventContents { + private String text; + private final TextProperties p; + + public TextContents(String text, TextProperties p) { + this.text = text; + this.p = p; + } + + public ContentType getContentType() { + return ContentType.PCDATA; + } + + public String getText() { + return text; + } + + public TextProperties getSpanProperties() { + return p; + } + + public boolean canContainEventObjects() { + return false; + } + + public static List getTextSegments(IterableEventContents ev) { + List chunks = new ArrayList(); + for (EventContents c : ev) { + switch (c.getContentType()) { + case PCDATA: + chunks.add(((TextContents) c).getText()); + break; + default: + if (c instanceof IterableEventContents) { + chunks.addAll(getTextSegments((IterableEventContents) c)); + } + } + } + return chunks; + } + + public static void updateTextContents(IterableEventContents ev, String[] chunks) { + updateTextContents(chunks, ev, 0); + } + + private static int updateTextContents(String[] chunks, IterableEventContents ev, int i) { + for (EventContents c : ev) { + switch (c.getContentType()) { + case PCDATA: + ((TextContents) c).text = chunks[i]; + i++; + break; + default: + if (c instanceof IterableEventContents) { + i = updateTextContents(chunks, (IterableEventContents) c, i); + } + } + } + return i; + } +} diff --git a/src/org/daisy/dotify/obfl/TocBlockEvent.java b/src/org/daisy/dotify/obfl/TocBlockEvent.java new file mode 100644 index 00000000..eb989f7a --- /dev/null +++ b/src/org/daisy/dotify/obfl/TocBlockEvent.java @@ -0,0 +1,36 @@ +package org.daisy.dotify.obfl; + +import org.daisy.dotify.api.formatter.BlockProperties; + +/** + * Provides an interface for TOC block event. + * + * @author Joel Håkansson + */ +class TocBlockEvent extends BlockEventImpl { + /** + * + */ + private static final long serialVersionUID = 1378970818712629309L; + private final String refId; + private final String tocId; + + public TocBlockEvent(String refId, String tocId, BlockProperties props) { + super(props); + this.refId = refId; + this.tocId = tocId; + } + + public ContentType getContentType() { + return ContentType.TOC_ENTRY; + } + + public String getRefId() { + return refId; + } + + public String getTocId() { + return tocId; + } + +} \ No newline at end of file diff --git a/src/org/daisy/dotify/obfl/TocEvents.java b/src/org/daisy/dotify/obfl/TocEvents.java new file mode 100644 index 00000000..7054588e --- /dev/null +++ b/src/org/daisy/dotify/obfl/TocEvents.java @@ -0,0 +1,38 @@ +package org.daisy.dotify.obfl; + + + +/** + * Provides the toc events for a specified context + * @author Joel Håkansson + * + */ +interface TocEvents { + + /** + * Gets the events that should precede the TOC + * @return returns the events that should precede the TOC + */ + public Iterable getTocStartEvents(); + + /** + * Gets the events that should precede TOC entries from the specified volume + * @param forVolume the number of the volume that is to be started, one based + * @return returns the events that should precede the TOC entries from the specified volume + */ + public Iterable getVolumeStartEvents(int forVolume); + + /** + * Gets the events that should follow TOC entries from the specified volume + * @param forVolume the number of the volume that has just ended, one based + * @return returns the events that should follow the TOC entries from the specified volume + */ + public Iterable getVolumeEndEvents(int forVolume); + + /** + * Gets the events that should follow the TOC + * @return returns the events that should follow the TOC + */ + public Iterable getTocEndEvents(); + +} diff --git a/src/org/daisy/dotify/obfl/TocSequenceEvent.java b/src/org/daisy/dotify/obfl/TocSequenceEvent.java new file mode 100644 index 00000000..b9085814 --- /dev/null +++ b/src/org/daisy/dotify/obfl/TocSequenceEvent.java @@ -0,0 +1,43 @@ +package org.daisy.dotify.obfl; + + +/** + * Provides a TOC sequence event object. + * + * @author Joel Håkansson + */ +interface TocSequenceEvent extends VolumeSequenceEvent { + /** + * Defines TOC ranges. + */ + enum TocRange { + /** + * Defines the TOC range to include the entire document + */ + DOCUMENT, + /** + * Defines the TOC range to include entries within the volume + */ + VOLUME}; + + public String getTocName(); + + public TocRange getRange(); + + /** + * Returns true if this toc sequence applies to the supplied context + * @param volume + * @param volumeCount + * @return returns true if this toc sequence applies to the supplied context, false otherwise + */ + public boolean appliesTo(int volume, int volumeCount); + + /** + * Gets the TOC events + * @param volume + * @param volumeCount + * @return returns the TOC events + */ + public TocEvents getTocEvents(int volume, int volumeCount); + +} \ No newline at end of file diff --git a/src/org/daisy/dotify/obfl/TocSequenceEventImpl.java b/src/org/daisy/dotify/obfl/TocSequenceEventImpl.java new file mode 100644 index 00000000..b2509119 --- /dev/null +++ b/src/org/daisy/dotify/obfl/TocSequenceEventImpl.java @@ -0,0 +1,144 @@ +package org.daisy.dotify.obfl; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; + +import org.daisy.dotify.api.formatter.SequenceProperties; +import org.daisy.dotify.api.obfl.ExpressionFactory; +import org.daisy.dotify.tools.CompoundIterable; + +class TocSequenceEventImpl implements TocSequenceEvent { + + public final static String DEFAULT_EVENT_VOLUME_NUMBER = "started-volume-number"; + private final SequenceProperties props; + private final String tocName; + private final TocRange range; + private final String condition; + private final ArrayList tocStartEvents; + private final ArrayList volumeStartEvents; + private final ArrayList volumeEndEvents; + private final ArrayList tocEndEvents; + private final VolumeTemplate template; + private final String volEventVariable; + private final ExpressionFactory ef; + + public TocSequenceEventImpl(SequenceProperties props, String tocName, TocRange range, String condition, String volEventVar, VolumeTemplate template, ExpressionFactory ef) { + this.props = props; + this.tocName = tocName; + this.range = range; + this.condition = condition; + this.tocStartEvents = new ArrayList(); + this.volumeStartEvents = new ArrayList(); + this.volumeEndEvents = new ArrayList(); + this.tocEndEvents = new ArrayList(); + this.template = template; + this.volEventVariable = (volEventVar!=null?volEventVar:DEFAULT_EVENT_VOLUME_NUMBER); + this.ef = ef; + } + + void addTocStartEvents(Iterable events, String condition) { + tocStartEvents.add(new ConditionalEvents(events, condition, ef)); + } + + void addVolumeStartEvents(Iterable events, String condition) { + volumeStartEvents.add(new ConditionalEvents(events, condition, ef)); + } + + void addVolumeEndEvents(Iterable events, String condition) { + volumeEndEvents.add(new ConditionalEvents(events, condition, ef)); + } + + void addTocEndEvents(Iterable events, String condition) { + tocEndEvents.add(new ConditionalEvents(events, condition, ef)); + } + + public VolumeSequenceType getVolumeSequenceType() { + return VolumeSequenceType.TABLE_OF_CONTENTS; + } + + public String getTocName() { + return tocName; + } + + public TocRange getRange() { + return range; + } + + /** + * Returns true if this toc sequence applies to the supplied context + * @param volume + * @param volumeCount + * @return returns true if this toc sequence applies to the supplied context, false otherwise + */ + public boolean appliesTo(int volume, int volumeCount) { + if (condition==null) { + return true; + } + String[] vars = new String[] { + template.getVolumeNumberVariableName()+"="+volume, + template.getVolumeCountVariableName()+"="+volumeCount + }; + return ef.newExpression().evaluate(condition, vars).equals(true); + } + + /** + * Gets the TOC events + * @param volume + * @param volumeCount + * @return returns the TOC events + */ + public TocEvents getTocEvents(int volume, int volumeCount) { + return new TocEventsImpl(volume, volumeCount); + } + + private static Iterable getCompoundIterable(Iterable events, Map vars) { + ArrayList> it = new ArrayList>(); + for (ConditionalEvents ev : events) { + if (ev.appliesTo(vars)) { + Iterable tmp = ev.getEvents(); + for (BlockEvent e : tmp) { + e.setEvaluateContext(vars); + } + it.add(tmp); + } + } + return new CompoundIterable(it); + } + + private class TocEventsImpl implements TocEvents { + private final HashMap vars; + + public TocEventsImpl(int volume, int volumeCount) { + vars = new HashMap(); + vars.put(template.getVolumeNumberVariableName(), volume+""); + vars.put(template.getVolumeCountVariableName(), volumeCount+""); + } + + public Iterable getTocStartEvents() { + return getCompoundIterable(tocStartEvents, vars); + } + + public Iterable getVolumeStartEvents(int forVolume) { + HashMap v2 = new HashMap(); + v2.putAll(vars); + v2.put(volEventVariable, forVolume+""); + return getCompoundIterable(volumeStartEvents, v2); + } + + public Iterable getVolumeEndEvents(int forVolume) { + HashMap v2 = new HashMap(); + v2.putAll(vars); + v2.put(volEventVariable, forVolume+""); + return getCompoundIterable(volumeEndEvents, v2); + } + + public Iterable getTocEndEvents() { + return getCompoundIterable(tocEndEvents, vars); + } + } + + public SequenceProperties getSequenceProperties() { + return props; + } +} diff --git a/src/org/daisy/dotify/obfl/VolumeSequenceEvent.java b/src/org/daisy/dotify/obfl/VolumeSequenceEvent.java new file mode 100644 index 00000000..9bf347cf --- /dev/null +++ b/src/org/daisy/dotify/obfl/VolumeSequenceEvent.java @@ -0,0 +1,26 @@ +package org.daisy.dotify.obfl; + +import org.daisy.dotify.api.formatter.SequenceProperties; + +/** + * Provides a volume sequence event object. A volume sequence is a chunk of contents + * that is to be placed before or after the contents of a volume. + * + * @author Joel Håkansson + */ +interface VolumeSequenceEvent { + /** + * Defines types of volume sequences + */ + enum VolumeSequenceType {STATIC, TABLE_OF_CONTENTS}; + /** + * Gets the volume sequence event type. + * @return returns the volume sequence event type + */ + //public VolumeSequenceType getVolumeSequenceType(); + /** + * Gets the volume sequence event properties. + * @return returns the volume sequence event properties + */ + public SequenceProperties getSequenceProperties(); +} diff --git a/src/org/daisy/dotify/obfl/VolumeTemplate.java b/src/org/daisy/dotify/obfl/VolumeTemplate.java new file mode 100644 index 00000000..841838d8 --- /dev/null +++ b/src/org/daisy/dotify/obfl/VolumeTemplate.java @@ -0,0 +1,26 @@ +package org.daisy.dotify.obfl; + + + +interface VolumeTemplate { + + /** + * Test if this Template applies to this combination of volume and volume count. + * @param volume the volume to test + * @return returns true if the Template should be applied to the volume + */ + public boolean appliesTo(int volume, int volumeCount); + + public Iterable getPreVolumeContent(); + + public Iterable getPostVolumeContent(); + + public String getVolumeNumberVariableName(); + public String getVolumeCountVariableName(); + + /** + * Gets the maximum number of sheets allowed. + * @return returns the number of sheets allowed + */ + public int getVolumeMaxSize(); +} diff --git a/src/org/daisy/dotify/obfl/VolumeTemplateImpl.java b/src/org/daisy/dotify/obfl/VolumeTemplateImpl.java new file mode 100644 index 00000000..7274678b --- /dev/null +++ b/src/org/daisy/dotify/obfl/VolumeTemplateImpl.java @@ -0,0 +1,60 @@ +package org.daisy.dotify.obfl; + +import org.daisy.dotify.api.obfl.ExpressionFactory; + + +class VolumeTemplateImpl implements VolumeTemplate { + public final static String DEFAULT_VOLUME_NUMBER_VARIABLE_NAME = "volume"; + public final static String DEFAULT_VOLUME_COUNT_VARIABLE_NAME = "volumes"; + private final String volumeNumberVar, volumeCountVar, condition; + private final int splitterMax; + private final ExpressionFactory ef; + private Iterable preVolumeContent; + private Iterable postVolumeContent; + + public VolumeTemplateImpl(String volumeVar, String volumeCountVar, String condition, Integer splitterMax, ExpressionFactory ef) { + this.volumeNumberVar = (volumeVar!=null?volumeVar:DEFAULT_VOLUME_NUMBER_VARIABLE_NAME); + this.volumeCountVar = (volumeCountVar!=null?volumeCountVar:DEFAULT_VOLUME_COUNT_VARIABLE_NAME); + this.condition = condition; + this.splitterMax = splitterMax; + this.ef = ef; + } + + public boolean appliesTo(int volume, int volumeCount) { + if (condition==null) { + return true; + } + return ef.newExpression().evaluate( + condition.replaceAll("\\$"+volumeNumberVar+"(?=\\W)", ""+volume).replaceAll("\\$"+volumeCountVar+"(?=\\W)", ""+volumeCount) + ).equals(true); + } + + public void setPreVolumeContent(Iterable preVolumeContent) { + this.preVolumeContent = preVolumeContent; + } + + public void setPostVolumeContent(Iterable postVolumeContent) { + this.postVolumeContent = postVolumeContent; + } + + public Iterable getPreVolumeContent() { + return preVolumeContent; + } + + public Iterable getPostVolumeContent() { + return postVolumeContent; + } + + public String getVolumeNumberVariableName() { + return volumeNumberVar; + } + + public String getVolumeCountVariableName() { + return volumeCountVar; + } + + public int getVolumeMaxSize() { + return splitterMax; + } + +} diff --git a/src/org/daisy/dotify/obfl/impl/ExpressionFactoryImpl.java b/src/org/daisy/dotify/obfl/impl/ExpressionFactoryImpl.java new file mode 100644 index 00000000..c0a6646c --- /dev/null +++ b/src/org/daisy/dotify/obfl/impl/ExpressionFactoryImpl.java @@ -0,0 +1,27 @@ +package org.daisy.dotify.obfl.impl; + +import org.daisy.dotify.api.obfl.Expression; +import org.daisy.dotify.api.obfl.ExpressionFactory; +import org.daisy.dotify.api.text.Integer2TextFactoryMakerService; + +import aQute.bnd.annotation.component.Component; +import aQute.bnd.annotation.component.Reference; + +@Component +public class ExpressionFactoryImpl implements ExpressionFactory { + private Integer2TextFactoryMakerService itf; + + public Expression newExpression() { + return new ExpressionImpl(itf); + } + + @Reference + public void setInteger2TextFactory(Integer2TextFactoryMakerService itf) { + this.itf = itf; + } + + public void unsetInteger2TextFactory(Integer2TextFactoryMakerService itf) { + this.itf = null; + } + +} diff --git a/src/org/daisy/dotify/obfl/impl/ExpressionImpl.java b/src/org/daisy/dotify/obfl/impl/ExpressionImpl.java new file mode 100644 index 00000000..2757fa09 --- /dev/null +++ b/src/org/daisy/dotify/obfl/impl/ExpressionImpl.java @@ -0,0 +1,382 @@ +package org.daisy.dotify.obfl.impl; + +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +import org.daisy.dotify.api.obfl.Expression; +import org.daisy.dotify.api.text.Integer2Text; +import org.daisy.dotify.api.text.Integer2TextConfigurationException; +import org.daisy.dotify.api.text.Integer2TextFactoryMakerService; +import org.daisy.dotify.api.text.IntegerOutOfRange; + +/** + *

+ * Expression is a small expressions language interpreter. The language uses + * prefix notation with arguments separated by whitespace. The entire expression + * must be surrounded with parentheses. + *

+ *

+ * The following operators are defined: +, -, *, /, %, =, <, <=, >, >=, + * &, | + *

+ *

+ * All operators require at least two arguments. E.g. (+ 5 7 9) evaluates to 21. + *

+ *

+ * Special keywords: + *

+ *
    + *
  • if: (if (boolean_expression) value_when_true value_when_false)
  • + *
  • now: (now date_format) where date_format is as defined by + * {@link SimpleDateFormat}
  • + *
  • round: (round value)
  • + *
  • set: (set key value) where key is the key that will be replaced by value + * in any subsequent expressions (within the same evaluation).
  • + *
  • int2text: (int2text number language-code) where number is an integer + * number to be converted into text using the language specified by + * language-code.
  • + *
  • concat: (concat ...) all arguments are concatenated to a single string
  • + *
+ *

+ * Quotes must surround arguments containing whitespace. + *

+ * + * @author Joel Håkansson + */ +class ExpressionImpl implements Expression { + private HashMap vars; + private final Integer2TextFactoryMakerService integer2textFactoryMaker; + + public ExpressionImpl(Integer2TextFactoryMakerService integer2textFactoryMaker) { + // = Integer2TextFactoryMaker.newInstance(); + this.integer2textFactoryMaker = integer2textFactoryMaker; + } + + public Object evaluate(String expr) { + // init + vars = new HashMap(); + // return value + String[] exprs = getArgs(expr); + for (int i=0; i variables) { + if (variables==null) { + return evaluate(expr); + } + for (String varName : variables.keySet()) { + expr = expr.replaceAll("\\$"+varName+"(?=\\W)", variables.get(varName)); + } + return evaluate(expr); + } + + public Object evaluate(String expr, String ... vars) { + for (String var : vars) { + String[] v = var.split("=", 2); + expr = expr.replaceAll("\\$"+v[0]+"(?=\\W)", v[1]); + } + return evaluate(expr); + } + + private Object doEval1(String expr) { + if (expr.startsWith("\"") && expr.endsWith("\"")) { + return expr.substring(1, expr.length()-1); + } + if (vars.containsKey(expr)) { + return vars.get(expr); + } + try { + return toNumber(expr); + } catch (NumberFormatException e) { + return expr; + } + } + + private Object doEval2(String[] args1) { + String operator = args1[0].trim(); + Object[] args = new Object[args1.length-1]; + for (int i=0; i".equals(operator)) { + return greaterThan(args); + } else if (">=".equals(operator)) { + return greaterThanOrEqualTo(args); + } else if ("&".equals(operator)) { + return and(args); + } else if ("|".equals(operator)) { + return or(args); + } else if ("if".equals(operator)) { + return ifOp(args); + } else if ("now".equals(operator)) { + return now(args); + } else if ("round".equals(operator)) { + return round(args); + } else if ("set".equals(operator)) { + return set(args); + } else if ("int2text".equals(operator)) { + return int2text(args); + } else if ("concat".equals(operator)) { + return concat(args); + } + else { + throw new IllegalArgumentException("Unknown operator: '" + operator + "'"); + } + } + + private Object doEvaluate(String expr) { + + expr = expr.trim(); + expr = expr.replaceAll("\\s+", " "); + int leftPar = expr.indexOf('('); + int rightPar = expr.lastIndexOf(')'); + if (leftPar==-1 && rightPar==-1) { + return doEval1(expr); + } else if (leftPar>-1 && rightPar>-1) { + return doEval2( getArgs(expr.substring(leftPar+1, rightPar))); + } else { + throw new IllegalArgumentException("Unmatched parenthesis"); + } + } + + private static double toNumber(Object input) { + return Double.parseDouble(input.toString()); + } + + private static double add(Object[] input) { + double ret = toNumber(input[0]); + for (int i=1; itoNumber((input[i])))) { + return false; + } + } + return true; + } + + private static boolean greaterThanOrEqualTo(Object[] input) { + for (int i=1; i=toNumber((input[i])))) { + return false; + } + } + return true; + } + + private static boolean and(Object[] input) { + for (int i=1; i1) { + throw new IllegalArgumentException("Wrong number of arguments: (now format)"); + } + SimpleDateFormat sdf = new SimpleDateFormat(input[0].toString()); + return sdf.format(new Date()); + } + + private static int round(Object[] input) { + if (input.length>1) { + throw new IllegalArgumentException("Wrong number of arguments: (round value)"); + } + return (int)Math.round(toNumber(input[0])); + } + + private Object set(Object[] input) { + if (input.length>2) { + throw new IllegalArgumentException("Wrong number of arguments: (set key value)"); + } + vars.put("$"+input[0].toString(), input[1]); + return input[1]; + } + + private String int2text(Object[] input) { + if (input.length > 2) { + throw new IllegalArgumentException("Wrong number of arguments: (int2text integer language-code)"); + } + if (integer2textFactoryMaker == null) { + throw new UnsupportedOperationException("Operation not supported in the current configuration."); + } + Integer2Text t; + try { + t = integer2textFactoryMaker.newInteger2Text(input[1].toString()); + } catch (Integer2TextConfigurationException e) { + throw new IllegalArgumentException("Unsupported locale: " + input[1], e); + } + try { + if (input[0] instanceof Integer) { + return t.intToText((Integer) input[0]); + } else { + double d = toNumber(input[0]); + if (Math.round(d) == d) { + return t.intToText((int) Math.round(d)); + } else { + throw new IllegalArgumentException("First argument must be an integer: " + input[0]); + } + } + } catch (NumberFormatException e) { + throw new IllegalArgumentException("First argument must be an integer: " + input[0], e); + } catch (IntegerOutOfRange e) { + throw new IllegalArgumentException("Integer out of range: " + input[0], e); + } + } + + private Object concat(Object[] input) { + StringBuilder sb = new StringBuilder(); + for (Object o : input) { + sb.append(o); + } + return sb.toString(); + } + + private static String[] getArgs(String expr) { + expr = expr.trim(); + ArrayList ret = new ArrayList(); + int ci = 0; + int level = 0; + boolean str = false; + for (int i=0; i + * Provides parsing of OBFL-files (Open Braille Formatting Language). + *

+ * @author Joel Håkansson + */ +package org.daisy.dotify.obfl; \ No newline at end of file diff --git a/src/org/daisy/dotify/obfl/packageinfo b/src/org/daisy/dotify/obfl/packageinfo new file mode 100644 index 00000000..a4f15462 --- /dev/null +++ b/src/org/daisy/dotify/obfl/packageinfo @@ -0,0 +1 @@ +version 1.0 \ No newline at end of file diff --git a/src/org/daisy/dotify/obfl/resource-files/obfl-ws-normalizer-2.xsl b/src/org/daisy/dotify/obfl/resource-files/obfl-ws-normalizer-2.xsl new file mode 100644 index 00000000..e00d0b09 --- /dev/null +++ b/src/org/daisy/dotify/obfl/resource-files/obfl-ws-normalizer-2.xsl @@ -0,0 +1,98 @@ + + + + + + + + ^\s+ + \s+$ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/org/daisy/dotify/obfl/resource-files/obfl-ws-normalizer.xsl b/src/org/daisy/dotify/obfl/resource-files/obfl-ws-normalizer.xsl new file mode 100644 index 00000000..8f7da90d --- /dev/null +++ b/src/org/daisy/dotify/obfl/resource-files/obfl-ws-normalizer.xsl @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + + ^\s+ + \s+$ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/org/daisy/dotify/tools/CompoundIterable.java b/src/org/daisy/dotify/tools/CompoundIterable.java new file mode 100644 index 00000000..50d06abd --- /dev/null +++ b/src/org/daisy/dotify/tools/CompoundIterable.java @@ -0,0 +1,22 @@ +package org.daisy.dotify.tools; + +import java.util.Iterator; + +/** + * Provides a method to iterate over several iterables of the same type + * as if the items were part of the same iterable. + * @author Joel Håkansson + * + * @param the type of iterable + */ +public class CompoundIterable implements Iterable { + private final Iterable> iterables; + + public CompoundIterable(Iterable> iterables) { + this.iterables = iterables; + } + + public Iterator iterator() { + return new CompoundIterator(iterables); + } +} \ No newline at end of file diff --git a/src/org/daisy/dotify/tools/CompoundIterator.java b/src/org/daisy/dotify/tools/CompoundIterator.java new file mode 100644 index 00000000..50ba2cac --- /dev/null +++ b/src/org/daisy/dotify/tools/CompoundIterator.java @@ -0,0 +1,45 @@ +package org.daisy.dotify.tools; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.NoSuchElementException; + +/** + * Provides an iterator for a collection of iterables + * @author Joel Håkansson + * + * @param the type of iterator + */ +public class CompoundIterator implements Iterator { + ArrayList> iterators; + + public CompoundIterator(Iterable> iterables) { + iterators = new ArrayList>(); + for (Iterable e : iterables) { + iterators.add(e.iterator()); + } + } + + public boolean hasNext() { + for (Iterator e : iterators) { + if (e.hasNext()) { + return true; + } + } + return false; + } + + public T next() { + for (Iterator e : iterators) { + if (e.hasNext()) { + return e.next(); + } + } + throw new NoSuchElementException(); + } + + public void remove() { + throw new UnsupportedOperationException(); + + } +} \ No newline at end of file diff --git a/src/org/daisy/dotify/tools/StateObject.java b/src/org/daisy/dotify/tools/StateObject.java new file mode 100644 index 00000000..7874695b --- /dev/null +++ b/src/org/daisy/dotify/tools/StateObject.java @@ -0,0 +1,119 @@ +package org.daisy.dotify.tools; + +/** + * The StateObject is a convenience object that can be used + * to avoid certain programming errors. By setting this object + * when the state changes and asserting that it has the correct + * state before engaging in state dependent activity, + * the programmer can get proper feedback whenever the object + * is in the wrong state for a particular request. + * + * @author Joel Håkansson + */ +public class StateObject { + /** + * Possible states for a StateObject + */ + public enum State { + /** + * Indicates that the StateObject has not yet been opened + */ + UNOPENED, + /** + * Indicates that the StateObject is open + */ + OPEN, + /** + * Indicates that the StateObject has been open, but is now closed + */ + CLOSED + } + private State state; + private String type; + + /** + * Create a new StateObject with the specified type. + * @param type the type name of this StateObject, e.g. a class name + */ + public StateObject(String type) { + this.type = type; + state = State.UNOPENED; + } + + /** + * Create a new StateObject with the default type, which is "Object" + */ + public StateObject() { + this("Object"); + } + + /** + * Open the StateObject + */ + public void open() { + state = State.OPEN; + } + + /** + * Close the StateObject + */ + public void close() { + state = State.CLOSED; + } + + /** + * Check if the StateObject has been closed + * @return returns true if the object is closed + */ + public boolean isClosed() { + return state == State.CLOSED; + } + + /** + * Check if the StateObject has been opened + * @return returns true if the object is opened + */ + public boolean isOpen() { + return state == State.OPEN; + } + + /** + * Assert that the object is open + * @throws throws IllegalStateException if the object is not open + */ + public void assertOpen() throws IllegalStateException { + if (state != State.OPEN) { + throw new IllegalStateException(type + " is not open."); + } + } + + /** + * Assert that the object is not open + * @throws throws IllegalStateException if the object is open + */ + public void assertNotOpen() throws IllegalStateException { + if (state == State.OPEN) { + throw new IllegalStateException(type + " is already open."); + } + } + + /** + * Assert that the object has been closed + * @throws throws IllegalStateException if the object is not closed + */ + public void assertClosed() throws IllegalStateException { + if (state != State.CLOSED) { + throw new IllegalStateException(type + " is not closed."); + } + } + + /** + * Assert that the object has never been opened + * @throws throws IllegalStateException if the object is not unopened + */ + public void assertUnopened() throws IllegalStateException { + if (state != State.UNOPENED) { + throw new IllegalStateException(type + " has already been opened."); + } + } +} \ No newline at end of file diff --git a/src/org/daisy/dotify/tools/StringTools.java b/src/org/daisy/dotify/tools/StringTools.java new file mode 100644 index 00000000..47536cc0 --- /dev/null +++ b/src/org/daisy/dotify/tools/StringTools.java @@ -0,0 +1,68 @@ +package org.daisy.dotify.tools; + +import java.util.Arrays; + +/** + * StringTools is a utility class for simple static operations related + * to strings. + * + * @author Joel Håkansson + */ +public class StringTools { + + // Default constructor is private as this class is not intended to be instantiated. + private StringTools() { } + + /** + * Count the number of code points in a String. This is equivalent + * to calling codePointCount on the entire String (beginIndex=0 + * and endIndex=string.length()). + * @param str the String to count length on + * @return returns the number of code points in the entire String + */ + public static int length(String str) { + return str.codePointCount(0, str.length()); + } + + /** + * Fill a String with a single character + * @param c the character to fill with + * @param length the length of the resulting String + * @return returns a String filled with character c + */ + public static String fill(char c, int length) { + /* + StringBuilder sb = new StringBuilder(); + for (int i=0; i meta) throws PagedMediaWriterException { + state.assertUnopened(); + state.open(); + try { + pst = new PrintStream(os, true, "UTF-8"); + } catch (UnsupportedEncodingException e) { + // should never happen + throw new PagedMediaWriterException("Cannot open PrintStream with UTF-8.", e); + } + hasOpenVolume = false; + hasOpenSection = false; + hasOpenPage = false; + pst.println(""); + pst.println(""); + pst.println(""); + pst.println(""); + pst.println("application/x-pef+xml"); + // these could be moved to OBFL-input + pst.println("" + p.getProperty(PROPERTY_IDENTIFIER, "identifier?") + ""); + pst.println("" + p.getProperty(PROPERTY_DATE, "date?") + ""); + + if (meta!=null) { + for (MetaDataItem item : meta) { + if (item.getKey().getNamespaceURI().equals(DC_NAMESPACE_URI)) { + if (!(item.getKey().getLocalPart().equals("format") && item.getKey().getLocalPart().equals("identifier") && item.getKey().getLocalPart().equals("date"))) { + Logger.getLogger(this.getClass().getCanonicalName()).fine("adding metadata " + item.getKey() + " " + item.getValue()); + pst.println("" + escape(item.getValue()) + ""); + } + } + } + } + for (Object key : p.keySet()) { + pst.println("" + p.get(key) + "" ); + } + pst.println(""); + pst.println(""); + pst.println(""); + } + + public void newPage() { + state.assertOpen(); + closeOpenPage(); + if (!hasOpenSection) { + throw new IllegalStateException("No open section."); + } + pst.println(""); + hasOpenPage = true; + } + + public void newRow(CharSequence row) { + state.assertOpen(); + if (nonBraillePattern.matcher(row).matches()) { + if (errorCount<10) { + Logger.getLogger(this.getClass().getCanonicalName()).fine( + "Non-braille characters in output"+ + (errorCount==9?" (supressing additional messages of this kind)":"") + ": " + row + ); + errorCount++; + } + } + pst.println(""+row+""); + } + + public void newRow() { + state.assertOpen(); + pst.println(""); + } + + public void newVolume(SectionProperties master) { + state.assertOpen(); + closeOpenVolume(); + cCols = master.getPageWidth(); + cRows = master.getPageHeight(); + cRowgap = Math.round((master.getRowSpacing()-1)*4); + cDuplex = master.duplex(); + pst.println(""); + hasOpenVolume = true; + } + + public void newSection(SectionProperties master) { + state.assertOpen(); + if (!hasOpenVolume) { + newVolume(master); + } + closeOpenSection(); + pst.print(""); + hasOpenSection = true; + } + + private String escape(String text) { + if (text == null) { + return ""; + } + return text.replaceAll("&", "&").replaceAll("<", "<").replaceAll("\"", """); + } + + private void closeOpenVolume() { + state.assertOpen(); + closeOpenSection(); + if (hasOpenVolume) { + pst.println(""); + hasOpenVolume = false; + } + } + + private void closeOpenSection() { + state.assertOpen(); + closeOpenPage(); + if (hasOpenSection) { + pst.println(""); + hasOpenSection = false; + } + } + + private void closeOpenPage() { + state.assertOpen(); + if (hasOpenPage) { + pst.println(""); + hasOpenPage = false; + } + } + + public void close() { + if (state.isClosed()) { + return; + } + state.assertOpen(); + closeOpenVolume(); + pst.println(""); + pst.println(""); + pst.close(); + state.close(); + } + +} diff --git a/src/org/daisy/dotify/writer/TextMediaWriter.java b/src/org/daisy/dotify/writer/TextMediaWriter.java new file mode 100644 index 00000000..90790910 --- /dev/null +++ b/src/org/daisy/dotify/writer/TextMediaWriter.java @@ -0,0 +1,132 @@ +package org.daisy.dotify.writer; + +import java.io.OutputStream; +import java.io.PrintStream; +import java.io.UnsupportedEncodingException; +import java.util.List; + +import org.daisy.dotify.api.formatter.SectionProperties; +import org.daisy.dotify.api.writer.MetaDataItem; +import org.daisy.dotify.api.writer.PagedMediaWriter; +import org.daisy.dotify.api.writer.PagedMediaWriterException; +import org.daisy.dotify.tools.StateObject; + + +/** + * PagedMediaWriter implementation that outputs plain text. + * @author Joel Håkansson + * + */ +public class TextMediaWriter implements PagedMediaWriter { + private PrintStream pst; + //private Properties p; + private boolean hasOpenVolume; + private boolean hasOpenSection; + private boolean hasOpenPage; + /* + private int cCols; + private int cRows; + private int cRowgap; + private boolean cDuplex;*/ + private String encoding; + private StateObject state; + + /** + * Creates a new text media writer using with the specified encoding. + * @param encoding the encoding to use. + */ + public TextMediaWriter(String encoding) { + //this.p = p; + hasOpenVolume = false; + hasOpenSection = false; + hasOpenPage = false; + /*cCols = 0; + cRows = 0; + cRowgap = 0; + cDuplex = true;*/ + this.encoding = encoding; + this.state = new StateObject("Writer"); + } + + public void open(OutputStream os, List meta) throws PagedMediaWriterException { + state.assertUnopened(); + state.open(); + try { + pst = new PrintStream(os, true, encoding); + } catch (UnsupportedEncodingException e) { + throw new PagedMediaWriterException("Cannot open PrintStream with " + encoding, e); + } + hasOpenVolume = false; + hasOpenSection = false; + hasOpenPage = false; + } + + public void newPage() { + state.assertOpen(); + closeOpenPage(); + hasOpenPage = true; + } + + public void newRow(CharSequence row) { + state.assertOpen(); + pst.println(row); + } + + public void newRow() { + state.assertOpen(); + pst.println(); + } + + + public void newVolume(SectionProperties master) { + state.assertOpen(); + closeOpenVolume(); + /* + cCols = master.getPageWidth(); + cRows = master.getPageHeight(); + cRowgap = Math.round((master.getRowSpacing()-1)*4); + cDuplex = master.duplex();*/ + hasOpenVolume = true; + } + + public void newSection(SectionProperties master) { + state.assertOpen(); + if (!hasOpenVolume) { + newVolume(master); + } + closeOpenSection(); + hasOpenSection = true; + } + + private void closeOpenVolume() { + closeOpenSection(); + if (hasOpenVolume) { + hasOpenVolume = false; + } + } + + private void closeOpenSection() { + closeOpenPage(); + if (hasOpenSection) { + hasOpenSection = false; + } + } + + private void closeOpenPage() { + if (hasOpenPage) { + hasOpenPage = false; + } + } + + public void close() { + if (state.isClosed()) { + return; + } + state.assertOpen(); + closeOpenVolume(); + pst.close(); + state.close(); + } + + +} diff --git a/src/org/daisy/dotify/writer/package-info.java b/src/org/daisy/dotify/writer/package-info.java new file mode 100644 index 00000000..e98f7cbc --- /dev/null +++ b/src/org/daisy/dotify/writer/package-info.java @@ -0,0 +1,17 @@ +/** + *

+ * Provides PagedMediaWriter implementations. + *

+ * + *

+ * Note on adding PagedMediaWriter implementations: First consider if your needs + * could be met by converting a PEF file to your desired format using (or + * extending) Braille + * Utils. If not, plase add a class to this package, implementing + * "PagedMediaWriter". If your output format requirements cannot be met by + * implementing PagedMediaWriter, please consider submitting a feature request. + *

+ * @author Joel Håkansson + */ +package org.daisy.dotify.writer; \ No newline at end of file diff --git a/src/org/daisy/dotify/writer/packageinfo b/src/org/daisy/dotify/writer/packageinfo new file mode 100644 index 00000000..a4f15462 --- /dev/null +++ b/src/org/daisy/dotify/writer/packageinfo @@ -0,0 +1 @@ +version 1.0 \ No newline at end of file diff --git a/test/org/daisy/dotify/formatter/impl/EvenSizeVolumeSplitterCalculatorTest.java b/test/org/daisy/dotify/formatter/impl/EvenSizeVolumeSplitterCalculatorTest.java new file mode 100644 index 00000000..f04442e4 --- /dev/null +++ b/test/org/daisy/dotify/formatter/impl/EvenSizeVolumeSplitterCalculatorTest.java @@ -0,0 +1,121 @@ +package org.daisy.dotify.formatter.impl; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import org.daisy.dotify.formatter.impl.EvenSizeVolumeSplitterCalculator; +import org.junit.Test; + +public class EvenSizeVolumeSplitterCalculatorTest { + + @Test + public void breakpoints() { + int i = 479; + EvenSizeVolumeSplitterCalculator ssd = new EvenSizeVolumeSplitterCalculator(i, 49); + for (int j=1; j ts = new TreeSet(); + ts.add(new TabStopString("Text1", 15)); + ts.add(new TabStopString("Text2", 13)); + ts.add(new TabStopString("Text3", 3)); + ts.add(new TabStopString("Text4", 27)); + ts.add(new TabStopString("Text", 11, Alignment.CENTER)); + ts.add(new TabStopString("Text", 11)); + ts.add(new TabStopString("Text", 11, Alignment.LEFT, "1")); + + TabStopString[] tss = ts.toArray(new TabStopString[]{}); + + //Test + assertEquals("Assert order and defaults", new TabStopString("Text3", 3, Alignment.LEFT, " "), tss[0]); + assertEquals("Assert order and defaults", new TabStopString("Text", 11, Alignment.LEFT, " "), tss[1]); + assertEquals("Assert order and defaults", new TabStopString("Text", 11, Alignment.LEFT, "1"), tss[2]); + assertEquals("Assert order and defaults", new TabStopString("Text", 11, Alignment.CENTER, " "), tss[3]); + assertEquals("Assert order and defaults", new TabStopString("Text2", 13, Alignment.LEFT, " "), tss[4]); + assertEquals("Assert order and defaults", new TabStopString("Text1", 15, Alignment.LEFT, " "), tss[5]); + assertEquals("Assert order and defaults", new TabStopString("Text4", 27, Alignment.LEFT, " "), tss[6]); + + /* + {"Text3", 3, LEFT, " "} + {"Text", 11, LEFT, " "} + {"Text", 11, LEFT, "1"} + {"Text", 11, CENTER, " "} + {"Text2", 13, LEFT, " "} + {"Text1", 15, LEFT, " "} + {"Text4", 27, LEFT, " "} + + for (TabStopString tss : ts) { + System.out.println(tss.toString()); + }*/ + } + + public void testTabStopStringEquals() { + //Setup + TabStopString tss1 = new TabStopString("Text", 1); + TabStopString tss2 = new TabStopString("Text", 1); + + //Test + assertEquals("Assert equal", tss1, tss2); + } + +} diff --git a/test/org/daisy/dotify/obfl/ObflWsXsltTest.java b/test/org/daisy/dotify/obfl/ObflWsXsltTest.java new file mode 100644 index 00000000..dd86201d --- /dev/null +++ b/test/org/daisy/dotify/obfl/ObflWsXsltTest.java @@ -0,0 +1,147 @@ +package org.daisy.dotify.obfl; + +import static org.junit.Assert.assertTrue; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +import javax.xml.stream.XMLEventFactory; +import javax.xml.stream.XMLInputFactory; +import javax.xml.stream.XMLOutputFactory; +import javax.xml.stream.XMLStreamException; + +import org.junit.Test; +public class ObflWsXsltTest { + + @Test + public void testWsNormalizer_01() throws IOException, XMLStreamException { + int ret = testWsNormalizer("resource-files/ws-test-input-01.xml", "resource-files/ws-test-expected-01.xml"); + assertTrue("Compare (Toc) failed at byte: " + ret, ret == -1); + } + + @Test + public void testWsNormalizer_02() throws IOException, XMLStreamException { + int ret = testWsNormalizer("resource-files/ws-test-input-02.xml", "resource-files/ws-test-expected-02.xml"); + assertTrue("Compare (Block) failed at byte: " + ret, ret == -1); + } + + @Test + public void testWsNormalizer_03() throws IOException, XMLStreamException { + int ret = testWsNormalizer("resource-files/ws-test-input-03.xml", "resource-files/ws-test-expected-03.xml"); + assertTrue("Compare (Span) failed at byte: " + ret, ret == -1); + } + + @Test + public void testWsNormalizer_04() throws IOException, XMLStreamException { + int ret = testWsNormalizer("resource-files/ws-test-input-04.xml", "resource-files/ws-test-expected-04.xml"); + assertTrue("Compare (Line breaks) failed at byte: " + ret, ret == -1); + } + + @Test + public void testWsNormalizer_05() throws IOException, XMLStreamException { + int ret = testWsNormalizer("resource-files/ws-test-input-05.xml", "resource-files/ws-test-expected-05.xml"); + assertTrue("Compare (Leader) failed at byte: " + ret, ret == -1); + } + + @Test + public void testWsNormalizer_06() throws IOException, XMLStreamException { + int ret = testWsNormalizer("resource-files/ws-test-input-06.xml", "resource-files/ws-test-expected-06.xml"); + assertTrue("Compare (Evaluate) failed at byte: " + ret, ret == -1); + } + + @Test + public void testWsNormalizer_07() throws IOException, XMLStreamException { + int ret = testWsNormalizer("resource-files/ws-test-input-07.xml", "resource-files/ws-test-expected-07.xml"); + assertTrue("Compare (Page number) failed at byte: " + ret, ret == -1); + } + + @Test + public void testWsNormalizer_08() throws IOException, XMLStreamException { + int ret = testWsNormalizer("resource-files/ws-test-input-08.xml", "resource-files/ws-test-expected-08.xml"); + assertTrue("Compare (Marker) failed at byte: " + ret, ret == -1); + } + + @Test + public void testWsNormalizer_09() throws IOException, XMLStreamException { + int ret = testWsNormalizer("resource-files/ws-test-input-09.xml", "resource-files/ws-test-expected-09.xml"); + assertTrue("Compare (Anchor) failed at byte: " + ret, ret == -1); + } + + @Test + public void testWsNormalizer_10() throws IOException, XMLStreamException { + int ret = testWsNormalizer("resource-files/ws-test-input-10.xml", "resource-files/ws-test-expected-10.xml"); + assertTrue("Compare (Style) failed at byte: " + ret, ret == -1); + } + + @Test + public void testWsNormalizer_11() throws IOException, XMLStreamException { + int ret = testWsNormalizer("resource-files/ws-test-input-11.xml", "resource-files/ws-test-expected-11.xml"); + assertTrue("Compare (NBSP) failed at byte: " + ret, ret == -1); + } + + // Helpers + public int testWsNormalizer(String input, String expected) throws IOException, XMLStreamException { + + File in = File.createTempFile("TestInput", ".tmp"); + copy(this.getClass().getResourceAsStream(input), new FileOutputStream(in)); + + File normalizedFile = File.createTempFile("TestResult", ".tmp"); + 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 t = new OBFLWsNormalizer(inFactory.createXMLEventReader(new FileInputStream(in)), XMLEventFactory.newInstance(), new FileOutputStream(normalizedFile)); + t.parse(XMLOutputFactory.newInstance()); + int ret = compareBinary(new FileInputStream(normalizedFile), this.getClass().getResourceAsStream(expected)); + + if (!normalizedFile.delete()) { + normalizedFile.deleteOnExit(); + } + if (!in.delete()) { + in.deleteOnExit(); + } + + return ret; + } + + public static void copy(InputStream is, OutputStream os) throws IOException { + InputStream bis = new BufferedInputStream(is); + OutputStream bos = new BufferedOutputStream(os); + int b; + while ((b = bis.read())!=-1) { + bos.write(b); + } + bos.flush(); + bos.close(); + bis.close(); + } + + public int compareBinary(InputStream f1, InputStream f2) throws IOException { + InputStream bf1 = new BufferedInputStream(f1); + InputStream bf2 = new BufferedInputStream(f2); + int pos = 0; + try { + int b1; + int b2; + while ((b1 = bf1.read())!=-1 & b1 == (b2 = bf2.read())) { + pos++; + //continue + } + if (b1!=-1 || b2!=-1) { + return pos; + } + return -1; + } finally { + bf1.close(); + bf2.close(); + } + } + +} diff --git a/test/org/daisy/dotify/obfl/resource-files/ws-test-expected-01.xml b/test/org/daisy/dotify/obfl/resource-files/ws-test-expected-01.xml new file mode 100644 index 00000000..63d0d3b6 --- /dev/null +++ b/test/org/daisy/dotify/obfl/resource-files/ws-test-expected-01.xml @@ -0,0 +1 @@ +