package org.ebookdroid.droids.fb2.codec.handlers; import org.ebookdroid.droids.fb2.codec.FB2Page; import org.ebookdroid.droids.fb2.codec.ParsedContent; import org.ebookdroid.droids.fb2.codec.tags.FB2Tag; import org.ebookdroid.droids.fb2.codec.tags.FB2TagId; import java.util.ArrayList; import org.emdev.common.fonts.data.FontStyle; import org.emdev.common.lang.StrBuilder; import org.emdev.common.textmarkup.JustificationMode; import org.emdev.common.textmarkup.MarkupElement; import org.emdev.common.textmarkup.MarkupEndDocument; import org.emdev.common.textmarkup.MarkupEndPage; import org.emdev.common.textmarkup.MarkupExtraSpace; import org.emdev.common.textmarkup.MarkupImageRef; import org.emdev.common.textmarkup.MarkupNoLineBreak; import org.emdev.common.textmarkup.MarkupNoSpace; import org.emdev.common.textmarkup.MarkupNote; import org.emdev.common.textmarkup.MarkupParagraphEnd; import org.emdev.common.textmarkup.MarkupTable; import org.emdev.common.textmarkup.MarkupTable.Cell; import org.emdev.common.textmarkup.MarkupTitle; import org.emdev.common.textmarkup.RenderingStyle; import org.emdev.common.textmarkup.RenderingStyle.Script; import org.emdev.common.textmarkup.TextStyle; import org.emdev.common.textmarkup.line.LineFixedWhiteSpace; import org.emdev.common.textmarkup.line.LineWhiteSpace; import org.emdev.common.textmarkup.line.TextElement; import org.emdev.common.textmarkup.line.TextPreElement; import org.emdev.common.xml.IContentHandler; import org.emdev.common.xml.TextProvider; import org.emdev.common.xml.tags.XmlTag; import org.emdev.utils.StringUtils; public class StandardHandler extends BaseHandler implements IContentHandler, FB2TagId { protected boolean documentStarted = false, documentEnded = false; protected boolean paragraphParsing = false; protected int ulLevel = 0; protected boolean cover = false; protected String tmpBinaryName = null; protected boolean parsingNotes = false; protected boolean parsingBinary = false; protected boolean inTitle = false; protected boolean inEpigraph = false; protected boolean inCite = false; protected boolean noteFirstWord = true; protected boolean parseNotesInParagraphs = false; protected boolean spaceNeeded = true; protected StrBuilder tmpBinaryContent = null; protected char[] tmpBinary = null; protected int tmpBinaryStart = 0; protected int tmpBinaryLength = 0; protected final StringBuilder title = new StringBuilder(); protected final StrBuilder tmpTagContent = new StrBuilder(16 * 1024); protected int sectionLevel = -1; protected boolean skipContent = true; protected MarkupTable currentTable; private boolean parsingPreformatted = false; private int parsingPreformattedLevel = -1; private int parsingPreformattedLines = 0; protected int tagLevel = 0; public StandardHandler(final ParsedContent content) { super(content); } @Override public boolean parseAttributes(final XmlTag tag) { if (tag == FB2Tag.P.tag) { return parseNotesInParagraphs && tag.attributes.length > 0; } return tag.attributes.length > 0; } @Override public void startElement(final XmlTag tag, final String... attributes) { tagLevel++; final ArrayList<MarkupElement> markupStream = parsedContent.getMarkupStream(currentStream); if (tmpTagContent.length() > 0) { processTagContent(); } switch (tag.tag) { case P: paragraphParsing = true; if (!parsingNotes) { if (!inTitle) { markupStream.add(crs.paint.pOffset); } else { if (title.length() > 0) { title.append(" "); } } } else if (parseNotesInParagraphs) { currentStream = attributes[0]; if (currentStream != null) { final String n = getNoteId(currentStream, true); parsedContent.getMarkupStream(currentStream).add(text(new TextProvider(n), 0, n.length(), crs)); parsedContent.getMarkupStream(currentStream).add(crs.paint.fixedSpace); noteFirstWord = true; } } break; case UL: markupStream.add(emptyLine(crs.textSize)); markupStream.add(MarkupParagraphEnd.E); ulLevel++; break; case LI: paragraphParsing = true; markupStream.add(new MarkupExtraSpace((int) (crs.paint.pOffset.width * ulLevel))); markupStream.add(crs.bullet); markupStream.add(MarkupNoSpace.E); break; case V: paragraphParsing = true; markupStream.add(new MarkupExtraSpace((int) (crs.paint.pOffset.width + crs.paint.vOffset.width))); break; case BINARY: tmpBinaryName = attributes[0]; tmpBinary = null; tmpBinaryContent = null; parsingBinary = true; break; case BODY: if (!documentStarted && !documentEnded) { documentStarted = true; skipContent = false; currentStream = null; } if ("notes".equals(attributes[0])) { if (documentStarted) { documentEnded = true; parsedContent.getMarkupStream(null).add(new MarkupEndDocument()); } parsingNotes = true; crs = new RenderingStyle(parsedContent, TextStyle.FOOTNOTE); } if ("footnotes".equals(attributes[0])) { if (documentStarted) { documentEnded = true; parsedContent.getMarkupStream(null).add(new MarkupEndDocument()); } parsingNotes = true; parseNotesInParagraphs = true; crs = new RenderingStyle(parsedContent, TextStyle.FOOTNOTE); } break; case SECTION: if (parsingNotes) { if (!parseNotesInParagraphs) { currentStream = attributes[0]; if (currentStream != null) { final String n = getNoteId(currentStream, true); parsedContent.getMarkupStream(currentStream).add( text(new TextProvider(n), 0, n.length(), crs)); parsedContent.getMarkupStream(currentStream).add(crs.paint.fixedSpace); noteFirstWord = true; } } } else { sectionLevel++; } break; case TITLE: if (!parsingNotes) { setTitleStyle(!isInSection() ? TextStyle.MAIN_TITLE : TextStyle.SECTION_TITLE); markupStream.add(crs.jm); markupStream.add(emptyLine(crs.textSize)); markupStream.add(MarkupParagraphEnd.E); title.setLength(0); } else { skipContent = true; } inTitle = true; break; case CITE: inCite = true; markupStream.add(emptyLine(crs.textSize)); markupStream.add(MarkupParagraphEnd.E); markupStream.add(new MarkupExtraSpace(FB2Page.PAGE_WIDTH / 6)); break; case SUBTITLE: paragraphParsing = true; markupStream.add(setSubtitleStyle().jm); markupStream.add(emptyLine(crs.textSize)); markupStream.add(MarkupParagraphEnd.E); break; case TEXT_AUTHOR: paragraphParsing = true; markupStream.add(setTextAuthorStyle(inCite).jm); markupStream.add(crs.paint.pOffset); break; case DATE: if (documentStarted && !documentEnded || parsingNotes) { paragraphParsing = true; markupStream.add(setTextAuthorStyle(inCite).jm); markupStream.add(crs.paint.pOffset); } break; case A: if (paragraphParsing) { if ("note".equalsIgnoreCase(attributes[1])) { if (markupStream.get(markupStream.size() - 1) instanceof LineWhiteSpace) { markupStream.remove(markupStream.size() - 1); } final String note = attributes[0]; final String prettyNote = " " + getNoteId(note, false); markupStream.add(MarkupNoSpace.E); markupStream.add(MarkupNoLineBreak.E); markupStream.add(new TextElement(prettyNote.toCharArray(), 0, prettyNote.length(), new RenderingStyle(parsedContent, crs, Script.SUPER))); markupStream.add(new MarkupNote(note)); skipContent = true; } } break; case EMPTY_LINE: case BR: markupStream.add(emptyLine(crs.textSize)); markupStream.add(MarkupParagraphEnd.E); break; case POEM: markupStream.add(MarkupParagraphEnd.E); markupStream.add(emptyLine(crs.textSize)); markupStream.add(setPoemStyle().jm); break; case STRONG: setBoldStyle(); break; case SUP: setSupStyle(); spaceNeeded = false; break; case SUB: setSubStyle(); spaceNeeded = false; break; case STRIKETHROUGH: setStrikeThrough(); break; case EMPHASIS: setEmphasisStyle(); break; case EPIGRAPH: inEpigraph = true; markupStream.add(MarkupParagraphEnd.E); markupStream.add(setEpigraphStyle().jm); break; case IMAGE: final String ref = attributes[0]; if (cover) { parsedContent.setCover(ref); } else { if (!paragraphParsing) { markupStream.add(emptyLine(crs.textSize)); markupStream.add(MarkupParagraphEnd.E); } markupStream.add(new MarkupImageRef(ref, paragraphParsing)); if (!paragraphParsing) { markupStream.add(emptyLine(crs.textSize)); markupStream.add(MarkupParagraphEnd.E); } } break; case COVERPAGE: cover = true; break; case ANNOTATION: skipContent = false; break; case TABLE: currentTable = new MarkupTable(); markupStream.add(currentTable); break; case TR: if (currentTable != null) { currentTable.addRow(); } break; case TD: case TH: if (currentTable != null) { paragraphParsing = true; final Cell c = currentTable.addCol(); c.hasBackground = tag == FB2Tag.TH.tag; final String align = attributes[0]; if ("right".equals(align)) { c.align = JustificationMode.Right; } if ("center".equals(align)) { c.align = JustificationMode.Center; } oldStream = currentStream; currentStream = c.stream; } break; case CODE: parsingPreformatted = true; parsingPreformattedLevel = tagLevel; parsingPreformattedLines = 0; setPreformatted(); if (markupStream.get(markupStream.size() - 1) instanceof LineFixedWhiteSpace) { markupStream.remove(markupStream.size() - 1); } default: break; } tmpTagContent.setLength(0); } @Override public void endElement(final XmlTag tag) { tagLevel--; if (tmpTagContent.length() > 0) { processTagContent(); } final ArrayList<MarkupElement> markupStream = parsedContent.getMarkupStream(currentStream); switch (tag.tag) { case LI: markupStream.add(new MarkupExtraSpace(-(int) (crs.paint.pOffset.width * ulLevel))); case P: if (!skipContent) { if (crs.face.style != FontStyle.REGULAR && !inEpigraph) { crs = new RenderingStyle(parsedContent, crs, FontStyle.REGULAR); } markupStream.add(MarkupParagraphEnd.E); } paragraphParsing = false; break; case V: markupStream.add(new MarkupExtraSpace(-(int) (crs.paint.pOffset.width + crs.paint.vOffset.width))); if (!skipContent) { markupStream.add(MarkupParagraphEnd.E); } paragraphParsing = false; break; case UL: ulLevel--; markupStream.add(emptyLine(crs.textSize)); markupStream.add(MarkupParagraphEnd.E); break; case BINARY: if (tmpBinary != null) { parsedContent.addImage(tmpBinaryName, tmpBinary, tmpBinaryStart, tmpBinaryLength); tmpBinaryName = null; tmpBinary = null; } else if (tmpBinaryContent != null) { parsedContent.addImage(tmpBinaryName, tmpBinaryContent.shareValue(), 0, tmpBinaryContent.length()); tmpBinaryName = null; tmpBinaryContent = null; } parsingBinary = false; break; case BODY: parsingNotes = false; currentStream = null; parseNotesInParagraphs = false; break; case SECTION: if (parsingNotes) { noteId = -1; } else { if (isInSection()) { markupStream.add(MarkupEndPage.E); sectionLevel--; } } break; case TITLE: inTitle = false; skipContent = false; if (!parsingNotes) { markupStream.add(new MarkupTitle(title.toString(), sectionLevel)); markupStream.add(emptyLine(crs.textSize)); markupStream.add(MarkupParagraphEnd.E); markupStream.add(setPrevStyle().jm); } break; case CITE: inCite = false; markupStream.add(emptyLine(crs.textSize)); markupStream.add(MarkupParagraphEnd.E); markupStream.add(new MarkupExtraSpace(-FB2Page.PAGE_WIDTH / 6)); break; case SUBTITLE: markupStream.add(MarkupParagraphEnd.E); markupStream.add(emptyLine(crs.textSize)); markupStream.add(MarkupParagraphEnd.E); markupStream.add(setPrevStyle().jm); paragraphParsing = false; break; case TEXT_AUTHOR: case DATE: markupStream.add(MarkupParagraphEnd.E); markupStream.add(setPrevStyle().jm); paragraphParsing = false; break; case STANZA: markupStream.add(emptyLine(crs.textSize)); markupStream.add(MarkupParagraphEnd.E); break; case POEM: markupStream.add(emptyLine(crs.textSize)); markupStream.add(MarkupParagraphEnd.E); markupStream.add(setPrevStyle().jm); break; case STRONG: setPrevStyle(); spaceNeeded = false; break; case STRIKETHROUGH: setPrevStyle(); break; case SUP: setPrevStyle(); if (markupStream.get(markupStream.size() - 1) instanceof MarkupNoSpace) { markupStream.remove(markupStream.size() - 1); } break; case SUB: setPrevStyle(); if (markupStream.get(markupStream.size() - 1) instanceof MarkupNoSpace) { markupStream.remove(markupStream.size() - 1); } break; case EMPHASIS: setPrevStyle(); spaceNeeded = false; break; case EPIGRAPH: markupStream.add(setPrevStyle().jm); inEpigraph = false; break; case COVERPAGE: cover = false; break; case A: if (paragraphParsing) { skipContent = false; } break; case ANNOTATION: skipContent = true; parsedContent.getMarkupStream(null).add(MarkupEndPage.E); break; case TABLE: currentTable = null; break; case TD: case TH: paragraphParsing = false; currentStream = oldStream; break; case CODE: setPrevStyle(); parsingPreformatted = false; parsingPreformattedLevel = -1; if (!paragraphParsing) { markupStream.add(MarkupParagraphEnd.E); } default: break; } } protected boolean isInSection() { return sectionLevel >= 0; } @Override public boolean skipCharacters() { return skipContent || (!(documentStarted && !documentEnded) && !paragraphParsing && !parsingBinary && !parsingNotes); } @Override public void characters(final TextProvider text, final int start, final int length) { if (parsingBinary) { if (tmpBinary == null) { tmpBinary = text.chars; tmpBinaryStart = start; tmpBinaryLength = length; } else { tmpBinaryLength += length; } } else { processText(text, start, length); } } protected void processTagContent() { final int length = tmpTagContent.length(); final int start = 0; final char[] ch = tmpTagContent.getValue(); processText(new TextProvider(ch), start, length); tmpTagContent.setLength(0); } protected void processText(final TextProvider text, final int start, final int length) { if (inTitle) { title.append(text.chars, start, length); } if (!parsingPreformatted || tagLevel > parsingPreformattedLevel) { final int count = StringUtils.split(text.chars, start, length, starts, lengths, false); if (count > 0) { final ArrayList<MarkupElement> markupStream = parsedContent.getMarkupStream(currentStream); if (!spaceNeeded && !Character.isWhitespace(text.chars[start])) { markupStream.add(MarkupNoSpace.E); } spaceNeeded = true; for (int i = 0; i < count; i++) { final int st = starts[i]; final int len = lengths[i]; if (parsingNotes) { if (noteFirstWord) { noteFirstWord = false; final int id = getNoteId(text.chars, st, len); if (id == noteId) { continue; } } } markupStream.add(text(text, st, len, crs)); if (crs.script != null) { markupStream.add(MarkupNoSpace.E); } } if (Character.isWhitespace(text.chars[start + length - 1])) { markupStream.add(crs.paint.space); } spaceNeeded = false; } } else { final int count = StringUtils.split(text.chars, start, length, starts, lengths, true); if (count > 0) { final ArrayList<MarkupElement> markupStream = parsedContent.getMarkupStream(currentStream); for (int i = 0; i < count; i++) { final int st = starts[i]; final int len = lengths[i]; if (!paragraphParsing || (parsingPreformattedLines++) > 0) { markupStream.add(MarkupParagraphEnd.E); } markupStream.add(textPre(text, st, len, crs)); } } } } protected TextElement text(final TextProvider text, final int st, final int len, final RenderingStyle style) { return new TextElement(text.chars, st, len, style); } protected TextElement textPre(final TextProvider text, final int st, final int len, final RenderingStyle style) { return new TextPreElement(text.chars, st, len, style); } }