package folioxml.folio; import folioxml.core.InvalidMarkupException; import folioxml.core.TokenInfo; import folioxml.core.TokenUtils; import java.io.StringWriter; import java.util.ArrayList; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * Opening angle brackets must be paired. Closing angle brackets cannot be present at all! (</example>) * * @author nathanael */ public class FolioToken { //style attributes //arbitrary attributes public FolioToken(TokenType type) { this.type = type; } public FolioToken(String text) throws InvalidMarkupException { this.text = text; if (pTag.matcher(text).find()) { this.type = TokenType.Tag; parseTag(); } else if (pComment.matcher(text).find()) { this.type = TokenType.Comment; } else if (pText.matcher(text).find()) { this.type = TokenType.Text; } else { throw new InvalidMarkupException("Invalid token; neither tag, comment, or plain text:" + text); } } public enum TokenType { None, Comment, Text, Tag } /** * Comment, text, or tag */ public TokenType type = TokenType.None; public String text = null; public String tagName = null; public String tagOptions = null; public TokenInfo info = null; protected boolean _isClosing = false; /** * Returns true if it is a closing tag (if it has a forwardslash after the angle braket) * * @return */ public boolean isClosing() { return _isClosing; } /** * Sets whether or not the tag has a forwardslash after the angle bracket. * * @param value * @return */ public void isClosing(boolean value) { _isClosing = value; } /** * Matches a comment tag and any intermediate comments. Lazy, of course. */ private static Pattern pComment = Pattern.compile("^" + FolioTokenReader.CommentRegex + "$", Pattern.DOTALL | Pattern.CASE_INSENSITIVE); /** * Matches text that doesn't contain any open brackets that are directly followed by a letter or a closing slash. */ private static Pattern pText = Pattern.compile("^" + FolioTokenReader.TextRegex + "$"); //non <, expect doubles /** * Matches any two-letter tag (and +/-), and captures (optional) options. group 1 and 2, respectively. Adjacent delimiters handled for TA:; bug in export. */ private static Pattern pTag = Pattern.compile("^" + FolioTokenReader.TagRegex + "$"); protected void parseTag() { assert (tagOptions == null && tagName == null); Matcher m = pTag.matcher(text); if (!m.find()) { assert (false); //Tag has invalid syntax. } parseTagFromMatcher(m); // this.stackID = this.tagName; } protected void parseTagFromMatcher(Matcher m) { //Tag type isClosing((m.group(1) != null)); //Tag name this.tagName = m.group(2); //Parse attributes this.tagOptions = m.group(3); } /** * Returns true if the tag name (case-insensitive) matches the regex. * * @param regex * @return */ public boolean matches(String regex) { return TokenUtils.fastMatches(regex, tagName); } /** * Throws an exception if this tag doesn't have the specified number of options. Always returns true * * @param count * @return */ public boolean assertCount(int count) throws InvalidMarkupException { if (count() != count) throw new InvalidMarkupException("This tag " + text + " is required to have " + count + " options."); return true; } /** * Alphanumeric or quoted string. Quoted string can contain paired quotes. Adjacent delimiters handled for TA:; bug in export. */ private static Pattern pString = Pattern.compile("^\\s*([^;,:\"]+|\"(?:[^\"]|(?:\"\"))*\")(?:\\s*[:;,]+\\s*|$)"); /** * Numeric with optional decimal. Allowed units: p t c */ private static Pattern pUnit = Pattern.compile("^\\s*(-?[0-9]+(?:\\.[0-9]*)?[ptc]?)(?:\\s*[:;,]\\s*|$)"); private String _cachedOptionsText = null; private List<String> _cachedOptionsList = null; private List<Boolean> _cachedOptionsQuoted = null; /** * Returns an array of the options specified on this tag. unquotes text options automatically and inserts entities for " and << * Cached - don't modify the List<> or you'll mess everybody up. * * @return */ public List<String> getOptionsArray() throws InvalidMarkupException { //Jan 21, 2009 - profiled this method - Was taking 20% of overall execution time. //75% of method time in ReplaceAll if (tagOptions != null) { if (_cachedOptionsText != null) if (_cachedOptionsText.equals(tagOptions)) return _cachedOptionsList; ArrayList<String> attrs = new ArrayList<String>(6); ArrayList<Boolean> attrsQuoted = new ArrayList<Boolean>(6); Matcher mStr = pString.matcher(tagOptions); Matcher mUnit = pUnit.matcher(tagOptions); int index = 0; while (index < tagOptions.length()) { Boolean quoted = false; //Set the region to parse mUnit.region(index, tagOptions.length()); mStr.region(index, tagOptions.length()); if (mUnit.find()) { attrs.add(mUnit.group(1)); index = mUnit.end(); } else if (mStr.find()) { //This section was taking 75% in replaceAll calls. /* Old code: took 13% of entire library CPU * String fixed = mStr.group(1).replaceAll("\"\"", """).replaceAll("^\"|\"$", "").replaceAll("<<","<").replaceAll("<","<").replaceAll(">",">"); if (mStr.group(1).replaceAll("\"\"","").startsWith("\"")) quoted = true; */ String val = mStr.group(1); //If the first char is a quote, but not the second, and more than 2 chars if (val.length() > 2 && val.charAt(0) == '"' && val.charAt(1) != '"') quoted = true; //or if both chars are quotes, then value is quoted. if (val.length() == 2 && val.contentEquals("\"\"")) quoted = true; attrs.add(convertString(val, quoted)); index = mStr.end(); } else { throw new InvalidMarkupException("Invalid syntax for a tag option:" + tagOptions.substring(index)); //assert false : "Invalid token : " + tagOptions.substring(index); } attrsQuoted.add(quoted); } _cachedOptionsQuoted = attrsQuoted; _cachedOptionsText = tagOptions; _cachedOptionsList = attrs; return attrs; } return new ArrayList<String>(); } /** * Converts a folio-style attribute (with "", <<,<,> escaping methods) to XML style entities. * * @param s * @param removeQuotes * @return */ private String convertString(String s, boolean removeQuotes) { //This section was taking 75% in replaceAll calls. /* Old code: took 13% of entire library CPU * String fixed = mStr.group(1).replaceAll("\"\"", """).replaceAll("^\"|\"$", "").replaceAll("<<","<").replaceAll("<","<").replaceAll(">",">"); if (mStr.group(1).replaceAll("\"\"","").startsWith("\"")) quoted = true; */ //New version takes 8% instead of 75% of getOptionsArray(); StringBuilder sb = new StringBuilder(s.length()); //Turn remaining quote pairs and angle bracket pairs in " and < respectively. //Convert single angle braket pairs int < and > for compatibility. //Convert single quote pairs into " boolean lastCharQuote = false; boolean lastCharLtBracket = false; for (int i = 0; i < s.length(); i++) { //If removeQuotes, remove first and last character if they are quotes. if (removeQuotes && (i == 0 || i == s.length() - 1)) continue; char c = s.charAt(i); //Quotes, doubled and single if (lastCharQuote) { lastCharQuote = false; //Flush the last char regardless. sb.append("""); //Skip this char also if a quote. if (c == '\"') continue; } else if (c == '\"') { lastCharQuote = true; continue; } //Less than, doubled and single if (lastCharLtBracket) { lastCharLtBracket = false; //Flush the last char regardless. sb.append("<"); //Skip this char also if a quote. if (c == '<') continue; } else if (c == '<') { lastCharLtBracket = true; continue; } //> greater than if (c == '>') { sb.append(">"); continue; } sb.append(c); } //if (fixed.indexOf("\"") > -1) throw new InvalidMarkupException("") //assert(fixed.indexOf("\"") < 0);//No single quote marks should be present within the string. //assert(fixed.indexOf("<") < 0); //No single opening brackets should be present return sb.toString(); } public List<String> getOptionsArrayWithTagName() throws InvalidMarkupException { ArrayList<String> opts = new ArrayList<String>(); opts.add(tagName); opts.addAll(getOptionsArray()); return opts; } public boolean wasQuoted(int index) throws InvalidMarkupException { getOptionsArray(); if (_cachedOptionsQuoted != null) return _cachedOptionsQuoted.get(index); return false; } /** * Returns the option at the specfied index. Returns null if there is no option at that index * * @param optionIndex * @return */ public String get(int optionIndex) throws InvalidMarkupException { List<String> opts = getOptionsArray(); if (opts.size() > optionIndex) return opts.get(optionIndex); else return null; } public FolioToken remove(int optionIndex) throws InvalidMarkupException { List<String> opts = getOptionsArray(); opts.remove(optionIndex); StringWriter sw = new StringWriter(); for (int i = 0; i < opts.size(); i++) { String s = opts.get(i); if (i > 0) sw.append(","); int oldIndex = i; if (i >= optionIndex) oldIndex++; if (!wasQuoted(oldIndex))//if (FolioCssUtils.matchesCaseInsensitive("^[A-Za-z0-9\\.]+$", s)) sw.append(s); else sw.append("\"" + s + "\""); } this.tagOptions = sw.toString(); if (tagOptions.length() == 0) tagOptions = null;//round down return this; } /** * Returns the number of options specified in the tag * * @return */ public int count() throws InvalidMarkupException { return getOptionsArray().size(); } /** * Returns the option directly following the specified option. Case-insensitive comparison. Returns null if not found. * * @param precedingOption * @return */ public String getOptionAfter(String precedingOption) throws InvalidMarkupException { List<String> opts = getOptionsArray(); for (int i = 1; i < opts.size(); i++) { if (opts.get((i - 1)).equalsIgnoreCase(precedingOption)) { return opts.get(i); } } return null; } public boolean hasOption(String option) throws InvalidMarkupException { List<String> opts = getOptionsArray(); for (int i = 0; i < opts.size(); i++) { if (opts.get(i).equalsIgnoreCase(option)) { return true; } } return false; } }