| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You under the Apache License, Version 2.0 |
| * (the "License"); you may not use this file except in compliance with |
| * the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| /* $Id$ */ |
| |
| package org.apache.fop.fo; |
| |
| import java.awt.Color; |
| import java.nio.CharBuffer; |
| import java.util.NoSuchElementException; |
| import java.util.Stack; |
| |
| import org.xml.sax.Locator; |
| |
| import org.apache.fop.accessibility.StructureTreeElement; |
| import org.apache.fop.apps.FOPException; |
| import org.apache.fop.complexscripts.bidi.DelimitedTextRange; |
| import org.apache.fop.datatypes.Length; |
| import org.apache.fop.fo.flow.Block; |
| import org.apache.fop.fo.properties.CommonFont; |
| import org.apache.fop.fo.properties.CommonHyphenation; |
| import org.apache.fop.fo.properties.CommonTextDecoration; |
| import org.apache.fop.fo.properties.KeepProperty; |
| import org.apache.fop.fo.properties.Property; |
| import org.apache.fop.fo.properties.SpaceProperty; |
| import org.apache.fop.fonts.TextFragment; |
| import org.apache.fop.util.CharUtilities; |
| |
| /** |
| * A text node (PCDATA) in the formatting object tree. |
| */ |
| public class FOText extends FONode implements CharSequence, TextFragment { |
| |
| /** the <code>CharBuffer</code> containing the text */ |
| private CharBuffer charBuffer; |
| |
| // The value of FO traits (refined properties) that apply to #PCDATA |
| // (aka implicit sequence of fo:character) |
| private CommonFont commonFont; |
| private CommonHyphenation commonHyphenation; |
| private Color color; |
| private KeepProperty keepTogether; |
| private Property letterSpacing; |
| private SpaceProperty lineHeight; |
| private int whiteSpaceTreatment; |
| private int whiteSpaceCollapse; |
| private int textTransform; |
| private Property wordSpacing; |
| private int wrapOption; |
| private Length baselineShift; |
| private String country; |
| private String language; |
| private String script; |
| // End of trait values |
| |
| /** |
| * Points to the previous FOText object created within the current |
| * block. If this is "null", this is the first such object. |
| */ |
| private FOText prevFOTextThisBlock; |
| |
| /** |
| * Points to the next FOText object created within the current |
| * block. If this is "null", this is the last such object. |
| */ |
| private FOText nextFOTextThisBlock; |
| |
| /** |
| * Points to the ancestor Block object. This is used to keep track of |
| * which FOText nodes are descendants of the same block. |
| */ |
| private Block ancestorBlock; |
| |
| /** Holds the text decoration values. May be null */ |
| private CommonTextDecoration textDecoration; |
| |
| private StructureTreeElement structureTreeElement; |
| |
| /* bidi levels */ |
| private int[] bidiLevels; |
| |
| private static final int IS_WORD_CHAR_FALSE = 0; |
| private static final int IS_WORD_CHAR_TRUE = 1; |
| private static final int IS_WORD_CHAR_MAYBE = 2; |
| |
| /** |
| * Creates a new FO text node. |
| * |
| * @param parent FONode that is the parent of this object |
| */ |
| public FOText(FONode parent) { |
| super(parent); |
| } |
| |
| /** {@inheritDoc} */ |
| protected void characters(char[] data, int start, int length, |
| PropertyList list, Locator locator) throws FOPException { |
| if (charBuffer == null) { |
| // buffer not yet initialized, do so now |
| int newLength = (length < 16) ? 16 : length; |
| charBuffer = CharBuffer.allocate(newLength); |
| } else { |
| // allocate a larger buffer, and transfer contents |
| int requires = charBuffer.position() + length; |
| int capacity = charBuffer.capacity(); |
| if (requires > capacity) { |
| int newCapacity = capacity * 2; |
| if (requires > newCapacity) { |
| newCapacity = requires; |
| } |
| CharBuffer newBuffer = CharBuffer.allocate(newCapacity); |
| charBuffer.rewind(); |
| newBuffer.put(charBuffer); |
| charBuffer = newBuffer; |
| } |
| } |
| // extend limit to capacity |
| charBuffer.limit(charBuffer.capacity()); |
| // append characters |
| charBuffer.put(data, start, length); |
| // shrink limit to position |
| charBuffer.limit(charBuffer.position()); |
| } |
| |
| /** |
| * Return the array of characters for this instance. |
| * |
| * @return a char sequence containing the text |
| */ |
| public CharSequence getCharSequence() { |
| if (this.charBuffer == null) { |
| return null; |
| } |
| this.charBuffer.rewind(); |
| return this.charBuffer.asReadOnlyBuffer().subSequence(0, this.charBuffer.limit()); |
| } |
| |
| /** {@inheritDoc} */ |
| public FONode clone(FONode parent, boolean removeChildren) |
| throws FOPException { |
| FOText ft = (FOText) super.clone(parent, removeChildren); |
| if (removeChildren) { |
| // not really removing, just make sure the char buffer |
| // pointed to is really a different one |
| if (charBuffer != null) { |
| ft.charBuffer = CharBuffer.allocate(charBuffer.limit()); |
| charBuffer.rewind(); |
| ft.charBuffer.put(charBuffer); |
| ft.charBuffer.rewind(); |
| } |
| } |
| ft.prevFOTextThisBlock = null; |
| ft.nextFOTextThisBlock = null; |
| ft.ancestorBlock = null; |
| return ft; |
| } |
| |
| /** {@inheritDoc} */ |
| public void bind(PropertyList pList) throws FOPException { |
| this.commonFont = pList.getFontProps(); |
| this.commonHyphenation = pList.getHyphenationProps(); |
| this.color = pList.get(Constants.PR_COLOR).getColor(getUserAgent()); |
| this.keepTogether = pList.get(Constants.PR_KEEP_TOGETHER).getKeep(); |
| this.lineHeight = pList.get(Constants.PR_LINE_HEIGHT).getSpace(); |
| this.letterSpacing = pList.get(Constants.PR_LETTER_SPACING); |
| this.whiteSpaceCollapse = pList.get(Constants.PR_WHITE_SPACE_COLLAPSE).getEnum(); |
| this.whiteSpaceTreatment = pList.get(Constants.PR_WHITE_SPACE_TREATMENT).getEnum(); |
| this.textTransform = pList.get(Constants.PR_TEXT_TRANSFORM).getEnum(); |
| this.wordSpacing = pList.get(Constants.PR_WORD_SPACING); |
| this.wrapOption = pList.get(Constants.PR_WRAP_OPTION).getEnum(); |
| this.textDecoration = pList.getTextDecorationProps(); |
| this.baselineShift = pList.get(Constants.PR_BASELINE_SHIFT).getLength(); |
| this.country = pList.get(Constants.PR_COUNTRY).getString(); |
| this.language = pList.get(Constants.PR_LANGUAGE).getString(); |
| this.script = pList.get(Constants.PR_SCRIPT).getString(); |
| } |
| |
| /** {@inheritDoc} */ |
| public void endOfNode() throws FOPException { |
| if (charBuffer != null) { |
| charBuffer.rewind(); |
| } |
| super.endOfNode(); |
| getFOEventHandler().characters(this); |
| } |
| |
| /** {@inheritDoc} */ |
| public void finalizeNode() { |
| textTransform(); |
| } |
| |
| /** |
| * Check if this text node will create an area. |
| * This means either there is non-whitespace or it is |
| * preserved whitespace. |
| * Maybe this just needs to check length > 0, since char iterators |
| * handle whitespace. |
| * |
| * @return true if this will create an area in the output |
| */ |
| public boolean willCreateArea() { |
| if (whiteSpaceCollapse == Constants.EN_FALSE |
| && charBuffer.limit() > 0) { |
| return true; |
| } |
| |
| char ch; |
| charBuffer.rewind(); |
| while (charBuffer.hasRemaining()) { |
| ch = charBuffer.get(); |
| if (!((ch == CharUtilities.SPACE) |
| || (ch == CharUtilities.LINEFEED_CHAR) |
| || (ch == CharUtilities.CARRIAGE_RETURN) |
| || (ch == CharUtilities.TAB))) { |
| // not whitespace |
| charBuffer.rewind(); |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| /** |
| * @return a new TextCharIterator |
| */ |
| public CharIterator charIterator() { |
| return new TextCharIterator(); |
| } |
| |
| /** |
| * This method is run as part of the ancestor Block's flushText(), to |
| * create xref pointers to the previous FOText objects within the same Block |
| * @param ancestorBlock the ancestor fo:block |
| */ |
| protected void createBlockPointers(Block ancestorBlock) { |
| this.ancestorBlock = ancestorBlock; |
| // if the last FOText is a sibling, point to it, and have it point here |
| if (ancestorBlock.lastFOTextProcessed != null) { |
| if (ancestorBlock.lastFOTextProcessed.ancestorBlock |
| == this.ancestorBlock) { |
| prevFOTextThisBlock = ancestorBlock.lastFOTextProcessed; |
| prevFOTextThisBlock.nextFOTextThisBlock = this; |
| } else { |
| prevFOTextThisBlock = null; |
| } |
| } |
| } |
| |
| /** |
| * This method is run as part of endOfNode(), to handle the |
| * text-transform property for accumulated FOText |
| */ |
| private void textTransform() { |
| if (getBuilderContext().inMarker() |
| || textTransform == Constants.EN_NONE) { |
| return; |
| } |
| |
| charBuffer.rewind(); |
| CharBuffer tmp = charBuffer.slice(); |
| char c; |
| int lim = charBuffer.limit(); |
| int pos = -1; |
| while (++pos < lim) { |
| c = charBuffer.get(); |
| switch (textTransform) { |
| case Constants.EN_UPPERCASE: |
| tmp.put(Character.toUpperCase(c)); |
| break; |
| case Constants.EN_LOWERCASE: |
| tmp.put(Character.toLowerCase(c)); |
| break; |
| case Constants.EN_CAPITALIZE: |
| if (isStartOfWord(pos)) { |
| /* |
| Use toTitleCase here. Apparently, some languages use |
| a different character to represent a letter when using |
| initial caps than when all of the letters in the word |
| are capitalized. We will try to let Java handle this. |
| */ |
| tmp.put(Character.toTitleCase(c)); |
| } else { |
| tmp.put(c); |
| } |
| break; |
| default: |
| //should never happen as the property subsystem catches that case |
| assert false; |
| //nop |
| } |
| } |
| } |
| |
| /** |
| * Determines whether a particular location in an FOText object's text is |
| * the start of a new "word". The use of "word" here is specifically for |
| * the text-transform property, but may be useful for other things as |
| * well, such as word-spacing. The definition of "word" is somewhat ambiguous |
| * and appears to be definable by the user agent. |
| * |
| * @param i index into charBuffer |
| * |
| * @return True if the character at this location is the start of a new |
| * word. |
| */ |
| private boolean isStartOfWord(int i) { |
| char prevChar = getRelativeCharInBlock(i, -1); |
| /* All we are really concerned about here is of what type prevChar |
| * is. If inputChar is not part of a word, then the Java |
| * conversions will (we hope) simply return inputChar. |
| */ |
| switch (isWordChar(prevChar)) { |
| case IS_WORD_CHAR_TRUE: |
| return false; |
| case IS_WORD_CHAR_FALSE: |
| return true; |
| /* "MAYBE" implies that additional context is needed. An example is a |
| * single-quote, either straight or closing, which might be interpreted |
| * as a possessive or a contraction, or might be a closing quote. |
| */ |
| case IS_WORD_CHAR_MAYBE: |
| char prevPrevChar = getRelativeCharInBlock(i, -2); |
| switch (isWordChar(prevPrevChar)) { |
| case IS_WORD_CHAR_TRUE: |
| return false; |
| case IS_WORD_CHAR_FALSE: |
| return true; |
| case IS_WORD_CHAR_MAYBE: |
| return true; |
| default: |
| return false; |
| } |
| default: |
| return false; |
| } |
| } |
| |
| /** |
| * Finds a character within the current Block that is relative in location |
| * to a character in the current FOText. Treats all FOText objects within a |
| * block as one unit, allowing text in adjoining FOText objects to be |
| * returned if the parameters are outside of the current object. |
| * |
| * @param i index into the CharBuffer |
| * @param offset signed integer with relative position within the |
| * block of the character to return. To return the character immediately |
| * preceding i, pass -1. To return the character immediately after i, |
| * pass 1. |
| * @return the character in the offset position within the block; \u0000 if |
| * the offset points to an area outside of the block. |
| */ |
| private char getRelativeCharInBlock(int i, int offset) { |
| |
| int charIndex = i + offset; |
| // The easy case is where the desired character is in the same FOText |
| if (charIndex >= 0 && charIndex < this.length()) { |
| return this.charAt(i + offset); |
| } |
| |
| // For now, we can't look at following FOText nodes |
| if (offset > 0) { |
| return CharUtilities.NULL_CHAR; |
| } |
| |
| // Remaining case has the text in some previous FOText node |
| boolean foundChar = false; |
| char charToReturn = CharUtilities.NULL_CHAR; |
| FOText nodeToTest = this; |
| int remainingOffset = offset + i; |
| while (!foundChar) { |
| if (nodeToTest.prevFOTextThisBlock == null) { |
| break; |
| } |
| nodeToTest = nodeToTest.prevFOTextThisBlock; |
| int diff = nodeToTest.length() + remainingOffset - 1; |
| if (diff >= 0) { |
| charToReturn = nodeToTest.charAt(diff); |
| foundChar = true; |
| } else { |
| remainingOffset += diff; |
| } |
| } |
| return charToReturn; |
| } |
| |
| /** |
| * @return The previous FOText node in this Block; null, if this is the |
| * first FOText in this Block. |
| */ |
| //public FOText getPrevFOTextThisBlock () { |
| // return prevFOTextThisBlock; |
| //} |
| |
| /** |
| * @return The next FOText node in this Block; null if this is the last |
| * FOText in this Block; null if subsequent FOText nodes have not yet been |
| * processed. |
| */ |
| //public FOText getNextFOTextThisBlock () { |
| // return nextFOTextThisBlock; |
| //} |
| |
| /** |
| * @return The nearest ancestor block object which contains this FOText. |
| */ |
| //public Block getAncestorBlock () { |
| // return ancestorBlock; |
| //} |
| |
| /** |
| * Determines whether the input char should be considered part of a |
| * "word". This is used primarily to determine whether the character |
| * immediately following starts a new word, but may have other uses. |
| * We have not found a definition of "word" in the standard (1.0), so the |
| * logic used here is based on the programmer's best guess. |
| * |
| * @param inputChar the character to be tested. |
| * @return int IS_WORD_CHAR_TRUE, IS_WORD_CHAR_FALSE, or IS_WORD_CHAR_MAYBE, |
| * depending on whether the character should be considered part of a word |
| * or not. |
| */ |
| private static int isWordChar(char inputChar) { |
| switch (Character.getType(inputChar)) { |
| case Character.COMBINING_SPACING_MARK: |
| return IS_WORD_CHAR_TRUE; |
| case Character.CONNECTOR_PUNCTUATION: |
| return IS_WORD_CHAR_TRUE; |
| case Character.CONTROL: |
| return IS_WORD_CHAR_FALSE; |
| case Character.CURRENCY_SYMBOL: |
| return IS_WORD_CHAR_TRUE; |
| case Character.DASH_PUNCTUATION: |
| if (inputChar == '-') { |
| return IS_WORD_CHAR_TRUE; //hyphen |
| } |
| return IS_WORD_CHAR_FALSE; |
| case Character.DECIMAL_DIGIT_NUMBER: |
| return IS_WORD_CHAR_TRUE; |
| case Character.ENCLOSING_MARK: |
| return IS_WORD_CHAR_FALSE; |
| case Character.END_PUNCTUATION: |
| if (inputChar == '\u2019') { |
| return IS_WORD_CHAR_MAYBE; //apostrophe, right single quote |
| } |
| return IS_WORD_CHAR_FALSE; |
| case Character.FORMAT: |
| return IS_WORD_CHAR_FALSE; |
| case Character.LETTER_NUMBER: |
| return IS_WORD_CHAR_TRUE; |
| case Character.LINE_SEPARATOR: |
| return IS_WORD_CHAR_FALSE; |
| case Character.LOWERCASE_LETTER: |
| return IS_WORD_CHAR_TRUE; |
| case Character.MATH_SYMBOL: |
| return IS_WORD_CHAR_FALSE; |
| case Character.MODIFIER_LETTER: |
| return IS_WORD_CHAR_TRUE; |
| case Character.MODIFIER_SYMBOL: |
| return IS_WORD_CHAR_TRUE; |
| case Character.NON_SPACING_MARK: |
| return IS_WORD_CHAR_TRUE; |
| case Character.OTHER_LETTER: |
| return IS_WORD_CHAR_TRUE; |
| case Character.OTHER_NUMBER: |
| return IS_WORD_CHAR_TRUE; |
| case Character.OTHER_PUNCTUATION: |
| if (inputChar == '\'') { |
| return IS_WORD_CHAR_MAYBE; //ASCII apostrophe |
| } |
| return IS_WORD_CHAR_FALSE; |
| case Character.OTHER_SYMBOL: |
| return IS_WORD_CHAR_TRUE; |
| case Character.PARAGRAPH_SEPARATOR: |
| return IS_WORD_CHAR_FALSE; |
| case Character.PRIVATE_USE: |
| return IS_WORD_CHAR_FALSE; |
| case Character.SPACE_SEPARATOR: |
| return IS_WORD_CHAR_FALSE; |
| case Character.START_PUNCTUATION: |
| return IS_WORD_CHAR_FALSE; |
| case Character.SURROGATE: |
| return IS_WORD_CHAR_FALSE; |
| case Character.TITLECASE_LETTER: |
| return IS_WORD_CHAR_TRUE; |
| case Character.UNASSIGNED: |
| return IS_WORD_CHAR_FALSE; |
| case Character.UPPERCASE_LETTER: |
| return IS_WORD_CHAR_TRUE; |
| default: |
| return IS_WORD_CHAR_FALSE; |
| } |
| } |
| |
| private class TextCharIterator extends CharIterator { |
| |
| private int currentPosition; |
| |
| private boolean canRemove; |
| private boolean canReplace; |
| |
| public TextCharIterator() { |
| } |
| |
| /** {@inheritDoc} */ |
| public boolean hasNext() { |
| return (this.currentPosition < charBuffer.limit()); |
| } |
| |
| /** {@inheritDoc} */ |
| public char nextChar() { |
| |
| if (this.currentPosition < charBuffer.limit()) { |
| this.canRemove = true; |
| this.canReplace = true; |
| return charBuffer.get(currentPosition++); |
| } else { |
| throw new NoSuchElementException(); |
| } |
| |
| } |
| |
| /** {@inheritDoc} */ |
| public void remove() { |
| |
| if (this.canRemove) { |
| charBuffer.position(currentPosition); |
| // Slice the buffer at the current position |
| CharBuffer tmp = charBuffer.slice(); |
| // Reset position to before current character |
| charBuffer.position(--currentPosition); |
| if (tmp.hasRemaining()) { |
| // Transfer any remaining characters |
| charBuffer.mark(); |
| charBuffer.put(tmp); |
| charBuffer.reset(); |
| } |
| // Decrease limit |
| charBuffer.limit(charBuffer.limit() - 1); |
| // Make sure following calls fail, unless nextChar() was called |
| this.canRemove = false; |
| } else { |
| throw new IllegalStateException(); |
| } |
| |
| } |
| |
| /** {@inheritDoc} */ |
| public void replaceChar(char c) { |
| |
| if (this.canReplace) { |
| charBuffer.put(currentPosition - 1, c); |
| } else { |
| throw new IllegalStateException(); |
| } |
| |
| } |
| |
| } |
| |
| /** |
| * @return the Common Font Properties. |
| */ |
| public CommonFont getCommonFont() { |
| return commonFont; |
| } |
| |
| /** |
| * @return the Common Hyphenation Properties. |
| */ |
| public CommonHyphenation getCommonHyphenation() { |
| return commonHyphenation; |
| } |
| |
| /** |
| * @return the "color" trait. |
| */ |
| public Color getColor() { |
| return color; |
| } |
| |
| /** |
| * @return the "keep-together" trait. |
| */ |
| public KeepProperty getKeepTogether() { |
| return keepTogether; |
| } |
| |
| /** |
| * @return the "letter-spacing" trait. |
| */ |
| public Property getLetterSpacing() { |
| return letterSpacing; |
| } |
| |
| /** |
| * @return the "line-height" trait. |
| */ |
| public SpaceProperty getLineHeight() { |
| return lineHeight; |
| } |
| |
| /** |
| * @return the "white-space-treatment" trait |
| */ |
| public int getWhitespaceTreatment() { |
| return whiteSpaceTreatment; |
| } |
| |
| /** |
| * @return the "word-spacing" trait. |
| */ |
| public Property getWordSpacing() { |
| return wordSpacing; |
| } |
| |
| /** |
| * @return the "wrap-option" trait. |
| */ |
| public int getWrapOption() { |
| return wrapOption; |
| } |
| |
| /** @return the "text-decoration" trait. */ |
| public CommonTextDecoration getTextDecoration() { |
| return textDecoration; |
| } |
| |
| /** @return the baseline-shift trait */ |
| public Length getBaseLineShift() { |
| return baselineShift; |
| } |
| |
| /** @return the country trait */ |
| public String getCountry() { |
| return country; |
| } |
| |
| /** @return the language trait */ |
| public String getLanguage() { |
| return language; |
| } |
| |
| /** @return the script trait */ |
| public String getScript() { |
| return script; |
| } |
| |
| /** {@inheritDoc} */ |
| public String toString() { |
| if (charBuffer == null) { |
| return ""; |
| } else { |
| CharBuffer cb = charBuffer.duplicate(); |
| cb.rewind(); |
| return cb.toString(); |
| } |
| } |
| |
| /** {@inheritDoc} */ |
| public String getLocalName() { |
| return "#PCDATA"; |
| } |
| |
| /** {@inheritDoc} */ |
| public String getNormalNamespacePrefix() { |
| return null; |
| } |
| |
| /** {@inheritDoc} */ |
| protected String gatherContextInfo() { |
| if (this.locator != null) { |
| return super.gatherContextInfo(); |
| } else { |
| return this.toString(); |
| } |
| } |
| |
| /** {@inheritDoc} */ |
| public char charAt(int position) { |
| return charBuffer.get(position); |
| } |
| |
| /** {@inheritDoc} */ |
| public CharSequence subSequence(int start, int end) { |
| return charBuffer.subSequence(start, end); |
| } |
| |
| /** {@inheritDoc} */ |
| public int length() { |
| return charBuffer.limit(); |
| } |
| |
| /** |
| * Resets the backing <code>java.nio.CharBuffer</code> |
| */ |
| public void resetBuffer() { |
| if (charBuffer != null) { |
| charBuffer.rewind(); |
| } |
| } |
| |
| @Override |
| public boolean isDelimitedTextRangeBoundary(int boundary) { |
| return false; |
| } |
| |
| @Override |
| public void setStructureTreeElement(StructureTreeElement structureTreeElement) { |
| this.structureTreeElement = structureTreeElement; |
| } |
| |
| @Override |
| public StructureTreeElement getStructureTreeElement() { |
| return structureTreeElement; |
| } |
| |
| /** |
| * Set bidirectional level over interval [start,end). |
| * @param level the resolved level |
| * @param start the starting index of interval |
| * @param end the ending index of interval |
| */ |
| public void setBidiLevel(int level, int start, int end) { |
| if (start < end) { |
| if (bidiLevels == null) { |
| bidiLevels = new int [ length() ]; |
| } |
| for (int i = start, n = end; i < n; i++) { |
| bidiLevels [ i ] = level; |
| } |
| if (parent != null) { |
| ((FObj) parent).setBidiLevel(level); |
| } |
| } else { |
| assert start < end; |
| } |
| } |
| |
| /** |
| * Obtain bidirectional level of each character |
| * represented by this FOText. |
| * @return a (possibly empty) array of bidi levels or null |
| * in case no bidi levels have been assigned |
| */ |
| public int[] getBidiLevels() { |
| return bidiLevels; |
| } |
| |
| /** |
| * Obtain bidirectional level of each character over |
| * interval [start,end). |
| * @param start the starting index of interval |
| * @param end the ending index of interval |
| * @return a (possibly empty) array of bidi levels or null |
| * in case no bidi levels have been assigned |
| */ |
| public int[] getBidiLevels(int start, int end) { |
| if (this.bidiLevels != null) { |
| assert start <= end; |
| int n = end - start; |
| int[] bidiLevels = new int [ n ]; |
| for (int i = 0; i < n; i++) { |
| bidiLevels[i] = this.bidiLevels [ start + i ]; |
| } |
| return bidiLevels; |
| } else { |
| return null; |
| } |
| } |
| |
| /** |
| * Obtain bidirectional level of character at |
| * specified position, which must be a non-negative integer |
| * less than the length of this FO. |
| * @param position an offset position into FO's characters |
| * @return a resolved bidi level or -1 if default |
| * @throws IndexOutOfBoundsException if position is not non-negative integer |
| * or is greater than or equal to length |
| */ |
| public int bidiLevelAt(int position) throws IndexOutOfBoundsException { |
| if ((position < 0) || (position >= length())) { |
| throw new IndexOutOfBoundsException(); |
| } else if (bidiLevels != null) { |
| return bidiLevels [ position ]; |
| } else { |
| return -1; |
| } |
| } |
| |
| @Override |
| protected Stack<DelimitedTextRange> collectDelimitedTextRanges(Stack<DelimitedTextRange> ranges, |
| DelimitedTextRange currentRange) { |
| if (currentRange != null) { |
| currentRange.append(charIterator(), this); |
| } |
| return ranges; |
| } |
| |
| private static class MapRange { |
| private int start; |
| private int end; |
| MapRange(int start, int end) { |
| this.start = start; |
| this.end = end; |
| } |
| public int hashCode() { |
| return (start * 31) + end; |
| } |
| public boolean equals(Object o) { |
| if (o instanceof MapRange) { |
| MapRange r = (MapRange) o; |
| return (r.start == start) && (r.end == end); |
| } else { |
| return false; |
| } |
| } |
| } |
| |
| } |