package folioxml.slx;
import folioxml.core.InvalidMarkupException;
import folioxml.core.TokenUtils;
import java.util.ArrayList;
import java.util.List;
import java.util.Stack;
import java.util.UUID;
public class SlxContextStack {
private Stack<SlxToken> stack = new Stack<SlxToken>();
/**
* @param xmlMode in XML mode, ghost tags are prohibited and will throw an InvalidMarkupException.
* @param tagGhostPairs If true, ghost tags are prepped for conversion to XML (pairs are tagged with a UUID)
*/
public SlxContextStack(boolean xmlMode, boolean tagGhostPairs) {
this.inXmlMode = xmlMode;
this.tagGhostPairs = tagGhostPairs;
}
protected boolean inXmlMode = false;
/**
* If true, XML mode is enforced (no ghost tags allowed)
*
* @return
*/
public boolean inXmlMode() {
return inXmlMode;
}
protected boolean tagGhostPairs = false;
/**
* If true, then matching pairs of ghost tags are being branded with a UUID so that the token stream is prepared for
* the conversion to XML.
*
* @return
*/
public boolean taggingGhostPairs() {
return tagGhostPairs;
}
/**
* Adds the token to the top of the stack. Can be any type: ghost, context, normal
*
* @param t
* @throws InvalidMarkupException
*/
public void add(SlxToken t) throws InvalidMarkupException {
if (t.isGhost && inXmlMode)
throw new InvalidMarkupException("SlxContextStack prohibits ghost tags when in XML mode", t);
stack.push(t);
}
/**
* TODO: this method is not needed since doing anything should consider removing it
*/
public int size() {
return stack.size();
}
/**
* returns the innermost parent with startsNewContext == true
*
* @return
*/
public SlxToken getTopContext() {
for (int i = stack.size() - 1; i >= 0; i--) {
if (stack.get(i).startsNewContext) return stack.get(i);
}
return null;
}
/**
* The top item, even if it is a ghost. Returns null if stack is empty.
*
* @return
*/
public SlxToken topItem() {
if (stack.size() == 0) return null;
return (stack.peek());
}
/**
* The top (non-ghost) item. Use topItem() to get the actual top item, including ghosts.
* Same as topItem() in xmlMode;
*
* @return
*/
public SlxToken top() {
if (inXmlMode) return topItem();
for (int i = stack.size() - 1; i >= 0; i--) {
if (!stack.get(i).isGhost) return stack.get(i);
}
return null;
}
/**
* How many non-ghost items are currently on the stack.
* Returns stack.size() in xmlMode
*
* @return
*/
public int nonGhostCount() {
if (inXmlMode) return size();
int count = 0;
for (int i = 0; i < stack.size(); i++) {
if (!stack.get(i).isGhost) count++;
}
return count;
}
/**
* Pops the top (non-ghost) item off the stack. Returns null if the stack is empty of non-ghosts.
* Calls popItem() in xmlMode
*
* @return
*/
public SlxToken pop() {
if (inXmlMode) return popItem();
int index = -1;
SlxToken s = null;
for (int i = stack.size() - 1; i >= 0; i--) {
s = stack.get(i);
if (!s.isGhost) {
index = i;
break;
}
}
if (index > -1) {
stack.remove(index);
}
return s;
}
/**
* Returns the topmost item off the stack, but only if it is a ghost. Returns null if the top item isn't a ghost, or the stack is empty.
*
* @return
*/
public SlxToken topGhost() {
if (stack.size() > 0 && stack.peek().isGhost) return stack.peek();
return null;
}
/**
* Pops the topmost item off the stack, even if it is a ghost. Returns null if the stack is empty.
*
* @return
*/
public SlxToken popItem() {
if (stack.size() == 0) return null;
return stack.pop();
}
/**
* Pops the topmost item off the stack, but only if it is a ghost. Returns null if the top item isn't a ghost, or the stack is empty.
*
* @return
*/
public SlxToken popGhost() {
if (stack.size() > 0 && stack.peek().isGhost) return stack.pop();
return null;
}
/**
* Removes the specified ghost item from the stack - can be at any level *within* the current context. Throws an exception if the item is not a ghost.
* Throws exception in xmlMode
*
* @param t
* @return
* @throws InvalidMarkupException
*/
public boolean pullGhost(SlxToken t) throws InvalidMarkupException {
if (t.isGhost && inXmlMode)
throw new InvalidMarkupException("SlxContextStack prohibits ghost tags when in XML mode", t);
if (!t.isGhost) throw new InvalidMarkupException("pullGhost only accepts ghost tags as arguments", t);
SlxToken s;
int index = -1;
for (int i = stack.size() - 1; i >= 0; i--) {
s = stack.get(i);
if (t.equals(s)) {
index = i;
break;
} //Record the index if we find the match
if (s.startsNewContext)
break; //don't cross context bounds, but allow the innermost context to be removed by reference
}
if (index > -1) {
stack.remove(index);
return true;
}
return false;
}
/**
* Returns true if there is a tag that matches the specified tag name (or tag name regex) within the innermost context. use has(string,true) to bypass the context boundaries.
*
* @param string
* @return
*/
public boolean has(String string) throws InvalidMarkupException {
return has(string, false);
}
/**
* Returns true if there is a tag that matches the specified tag name (or tag name regex) within the innermost context. use has(string,true) to bypass the context boundaries.
*
* @param string
* @return
*/
public boolean has(String string, boolean bypassContext) throws InvalidMarkupException {
SlxToken s;
for (int i = stack.size() - 1; i >= 0; i--) {
s = stack.get(i);
if (s.matches(string)) return true;
if (!bypassContext && s.startsNewContext) return false; //don't cross context bounds
}
return false;
}
/**
* Returns a collection of the ghost tags currently open.
*
* @param name
* @param bypassContext
* @return
* @throws InvalidMarkupException
*/
public List<SlxToken> getGhostTags(String name, boolean bypassContext) throws InvalidMarkupException {
return getOpenTags(name, true, bypassContext);
}
/**
* Returns a collection of the ghost tags currently open, topmost first.
*
* @param name
* @param bypassContext
* @return
* @throws InvalidMarkupException
*/
public List<SlxToken> getOpenTags(String name, boolean ghostsOnly, boolean bypassContext) throws InvalidMarkupException {
List<SlxToken> ghosts = new ArrayList<SlxToken>();
SlxToken s;
for (int i = stack.size() - 1; i >= 0; i--) {
s = stack.get(i);
if ((!ghostsOnly || s.isGhost) && (name == null || s.matches(name))) ghosts.add(s);
if (!bypassContext && s.startsNewContext) return ghosts; //don't cross context bounds
}
return ghosts;
}
/**
* Returns the innermost tag that matches the specified tag name (or tag name regex) *within the innermost context!*
*
* @param string
* @return
*/
public SlxToken get(String string) throws InvalidMarkupException {
return get(string, false);
}
/**
* Returns the innermost tag that matches the specified tag name (or tag name regex) *within the innermost context!*
*
* @param string
* @return
*/
public SlxToken get(String string, boolean bypassContext) throws InvalidMarkupException {
SlxToken s;
for (int i = stack.size() - 1; i >= 0; i--) {
s = stack.get(i);
if (s.matches(string)) return s;
if (!bypassContext && s.startsNewContext) return null; //don't cross context bounds
}
return null;
}
/**
* Returns the innermost tag that matches the specified tag name and type value. Searches ghosts also. Tag name and value can be a regex. if typeValue == null, find() will return null
* if typeValue is null, then types will not be filtered.
*
* @param name
* @param typeValue
* @param bypassContext
* @return
*/
public SlxToken find(String name, String typeValue, boolean bypassContext) throws InvalidMarkupException {
SlxToken s;
for (int i = stack.size() - 1; i >= 0; i--) {
s = stack.get(i);
if (s.matches(name) && (typeValue == null || TokenUtils.fastMatches(typeValue, s.get("type")))) return s;
if (!bypassContext && s.startsNewContext) return null; //don't cross context bounds
}
return null;
}
/**
* Performs the appropriate .add() , .pop(), or .pullGhost() needed for the specified tag.
* Compares tag name and the 'type' attribute to determine equivalence.
*
* @param t
* @return
* @throws InvalidMarkupException
*/
public void process(SlxToken t) throws InvalidMarkupException {
boolean strict = true;
if (!t.isTag()) return; //Only tags are proccessed
if (t.isGhost && inXmlMode)
throw new InvalidMarkupException("SlxContextStack prohibits ghost tags when in XML mode", t);
//If it's an opening tag, add it to the stack. Ghosts
if (t.isOpening()) this.add(t);
//(Only for non-ghosts): Make sure closing tags match with what's on the top of the stack. Ghost elements span & link aren't counted.
if (t.isClosing() && !t.isGhost) {
//See if there are open ghost tags
SlxToken topGhost = this.topGhost();
SlxToken opener = this.pop();
//Compare tag names. If the closing tag has a type attribute, compare that as well.
boolean isMatch = t.matches(opener.getTagName()) && (t.get("type") == null || t.get("type").equalsIgnoreCase(opener.get("type")));
//Verify that this closing tag matches the topmost open tag (that's not a ghost)
if (!isMatch) {
boolean useContext = !t.startsNewContext;
boolean matchExistsInContext = (this.find(t.getTagName(), t.get("type"), useContext) != null);
if (matchExistsInContext)
throw new InvalidMarkupException("Closing tag for " + opener.markup + " expected.", t);
else throw new InvalidMarkupException("Unexpected closing tag found.", t);
} else if (strict && t.startsNewContext) {
if (topGhost != null)
throw new InvalidMarkupException("Expected closing ghost tag before context ended.", topGhost);
}
}
//For ghosts: make sure there is an opening tag in the stack (current context) that matches.
if (t.isClosing() && t.isGhost) {
SlxToken opener = this.find(t.getTagName(), t.get("type"), false);
if (opener == null) {
if (strict) throw new InvalidMarkupException("Unexpected closing ghost tag encountered.", t);
} else {
if (tagGhostPairs) {
opener.ghostPair = t.ghostPair = UUID.randomUUID(); //For later use
}
if (!this.pullGhost(opener)) throw new InvalidMarkupException("Failed to remove ghost item from stack");
}
}
}
/**
* Returns the opening tag for t. The opening tag must be at the top of the stack (or in the context, in the case of ghost tags)
*
* @param t
* @return
* @throws InvalidMarkupException
*/
public SlxToken getOpeningTag(SlxToken t) throws InvalidMarkupException {
if (t.isGhost && inXmlMode)
throw new InvalidMarkupException("SlxContextStack prohibits ghost tags when in XML mode", t);
if (!t.isClosing()) throw new InvalidMarkupException("Only closing tags can be arguments", t);
//(Only for non-ghosts): Make sure closing tags match with what's on the top of the stack. Ghost elements span & link aren't counted.
//For ghosts: make sure there is an opening tag in the stack (current context) that matches.
if (t.isGhost) {
return this.find(t.getTagName(), t.get("type"), false);
} else {//See if there are open ghost tags
//Dont'Remove tag from stack
SlxToken opener = this.top();
//Compare tag names. If the closing tag has a type attribute, compare that as well.
boolean isMatch = t.matches(opener.getTagName()) && (t.get("type") == null || t.get("type").equalsIgnoreCase(opener.get("type")));
//Verify that this closing tag matches the topmost open tag (that's not a ghost)
if (!isMatch) {
return null;
}
return opener;
}
}
/**
* Returns true if there is a matching opening tag within the current context.
*
* @param t
* @return
* @throws InvalidMarkupException
*/
public boolean matchingOpeningTagExists(SlxToken t) throws InvalidMarkupException {
return (find(t.getTagName(), t.get("type"), false) != null);
}
/**
* Returns the topmost item that doesn't match the specified regex. Doesn't cross context bounds
* @param exclude
* @return
*
public SlxToken top(String exclude){
SlxToken s;
for (int i = stack.size() - 1; i >= 0; i--){
s = stack.get(i);
if (!s.matches(exclude)) return s;
if (s.startsNewContext) return null; //don't cross context bounds
}
return null;
}
/**
* Returns the innermost match for 'string' that occurs before a match for 'boundingElement'. If 'string' matches 'boundingElement' it will be returned.
* *within the innermost context*
* @param string
* @param boundingElement
* @return
*
public SlxToken getInside(String string, String boundingElement) {
SlxToken s;
for (int i = stack.size() - 1; i >= 0; i--){
s = stack.get(i);
if (s.matches(string)) return s;
if (s.matches(boundingElement)) return null; //don't cross bounding bounds
if (s.startsNewContext) return null; //don't cross context bounds
}
return null;
}
/**
* Removes the specified element from the array, but only if it is in the innermost conext (or *is* the innermost context)
* @param t
* @return
*
public SlxContextStack remove(String string){
//Get index
SlxToken s;
int index = -1;
for (int i = stack.size() - 1; i >= 0; i--){
s = stack.get(i);
if (s.matches(string)) {index = i; break;} //Record the index if we find the match
if (s.startsNewContext) break; //don't cross context bounds, but allow the innermost context to be removed by reference
}
if (index > -1) stack.remove(index);
return this;
}
/**
* Removes the specified element from the array, but only if it is in the innermost conext (or *is* the innermost context)
* @param t
* @return
*
public SlxContextStack remove(SlxToken t){
//Get index
SlxToken s;
int index = -1;
for (int i = stack.size() - 1; i >= 0; i--){
s = stack.get(i);
if (t.equals(s)) {index = i; break;} //Record the index if we find the match
if (s.startsNewContext) break; //don't cross context bounds, but allow the innermost context to be removed by reference
}
if (index > -1) stack.remove(index);
return this;
}
* */
}