/**
* Copyright (C) 2011 JTalks.org Team
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
package org.jtalks.jcommune.service.bb2htmlprocessors;
import java.util.LinkedList;
import java.util.List;
import java.util.Stack;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Close list items tags [*] with the [/*] tag.
* Support nested lists processing.
* Transform the incoming text to the tree-like structure and recreate the text with closed tags
* by this tree.
*
* @author Pavel Vervenko, Mikhail Stryzhonok
*/
class ListItemsProcessor {
private static final String LIST_TAG_OPEN = "[list";
private static final String LIST_TAG_CLOSE = "[/list]";
private static final String LIST_ITEM_TAG_OPEN = "[*]";
private static final String LIST_ITEM_CLOSE = "[/*]";
private RootElement root;
private ListElement lastElement;
private final Pattern listPattern = Pattern.compile(LIST_REGEX, Pattern.DOTALL | Pattern.CASE_INSENSITIVE);
private final Stack<ListElement> listStack = new Stack<>();
/**
* Matches one of the following tags: [list],[/list],[*]
*/
private static final String TAG_PATTERN = "\\[list([^\\]\\[]+)?]|\\[\\*\\]|\\[\\/list\\]";
/**
* TAG + everything before next tag
*/
private static final String LIST_REGEX = "(" + TAG_PATTERN + ")(.*?)?(?=" + TAG_PATTERN + "|$)";
private final String initialText;
ListItemsProcessor(String bbEncodedText) {
this.initialText = bbEncodedText;
}
StringBuilder getTextWithClosedTags() {
root = createRootElement(initialText);
Matcher matcher = listPattern.matcher(initialText);
while (matcher.find()) {
Tag tag = createTag(matcher.group(1));
tag.processMatch(matcher);
}
return root.toBBString();
}
/**
* Create root element of the tags tree.
* @param str String from which root element will be created
* @return root element
*/
private RootElement createRootElement(String str) {
RootElement newRoot = new RootElement();
int firstListPos = str.toLowerCase().indexOf(LIST_TAG_OPEN);
if (firstListPos < 0) {
newRoot.text = str;
} else {
newRoot.text = str.substring(0, firstListPos);
}
return newRoot;
}
/**
* Creates tag depending on needed tag type
* @param tagString String tag representation
* @return Newly created tag
*/
private Tag createTag(String tagString) {
if (tagString.equalsIgnoreCase(LIST_ITEM_TAG_OPEN)) {
return new ItemElement();
}
if (tagString.equalsIgnoreCase(LIST_TAG_CLOSE)) {
return new ClosingList();
}
if (tagString.toLowerCase().startsWith(LIST_TAG_OPEN)) {
return new ListElement();
} else {
throw new IllegalArgumentException("Unknown tag type " + tagString);
}
}
private TreeElement getCurrentElement() {
if (listStack.isEmpty()) {
return root;
}
return listStack.peek().getLastChild();
}
/**
* Represents one element of the tags tree.
*/
private abstract class TreeElement {
String text = "";
List<TreeElement> children = new LinkedList<>();
/**
* @return last element child
*/
TreeElement getLastChild() {
if (children.isEmpty()) {
return this;
}
return children.get(children.size() - 1);
}
/**
* @param newEl new sub-element
*/
void addChild(TreeElement newEl) {
children.add(newEl);
}
/**
* Return string representation of this element with tags and all children.
*
* @return BB-string
*/
StringBuilder toBBString() {
StringBuilder res = new StringBuilder(getOpenTag());
res.append(text);
for (TreeElement e : children) {
res.append(e.toBBString());
}
return res.append(getCloseTag());
}
/**
* @return open tag
*/
protected String getOpenTag() {
return "";
}
/**
* @return close tag
*/
protected String getCloseTag() {
return "";
}
}
/**
* Root of the tags tree. Contains all lists or just text.
*/
private class RootElement extends TreeElement {
@Override
TreeElement getLastChild() {
return this;
}
}
/**
* Root class of all kind of list tags. E.g. ListElement, ListItem, ClosingList
*/
private abstract class Tag extends TreeElement {
/**
* Process matching tag
* @param matcher Matcher object
*/
public abstract void processMatch(Matcher matcher);
}
/**
* List item element,[*]
*/
private class ItemElement extends Tag {
/**
* We need store parent element to check if it closed
*/
private ListElement parent;
@Override
protected String getOpenTag() {
return LIST_ITEM_TAG_OPEN;
}
@Override
protected String getCloseTag() {
if (parent.getClosingList() != null) {
return LIST_ITEM_CLOSE;
} else {
return "";
}
}
/**
* {@inheritDoc}
*/
@Override
public void processMatch(Matcher matcher) {
if (!listStack.isEmpty()) {
this.text = matcher.group(3);
listStack.peek().addChild(this);
this.parent = listStack.peek();
} else if (lastElement != null) {
lastElement.endText += LIST_ITEM_TAG_OPEN + matcher.group(3);
}
}
}
/**
* List tag, [list].
*/
private class ListElement extends Tag {
/**
* Case-sensitive value of tag
*/
String value;
/**
* list params
*/
String params;
/**
* text after list close tag
*/
String endText;
/**
* Closing tag for this elemetn
*/
private ClosingList closingList;
@Override
protected String getOpenTag() {
return value + getParams() + "]";
}
@Override
protected String getCloseTag() {
if (closingList != null) {
return closingList.getValue() + getEndText();
} else {
return "";
}
}
public String getParams() {
return params != null ? params : "";
}
public String getEndText() {
return endText != null ? endText : "";
}
public void setClosingList(ClosingList closingList) {
this.closingList = closingList;
}
public ClosingList getClosingList() {
return closingList;
}
/**
* {@inheritDoc}
*/
@Override
public void processMatch(Matcher matcher) {
this.params = matcher.group(2);
this.text = matcher.group(3);
// We should extract something like "[list"
this.value = matcher.group(1).substring(0, 5);
getCurrentElement().addChild(this);
listStack.push(this);
lastElement = this;
}
}
/**
* Class for closing list tags. Needed for separate matching processing.
*/
private class ClosingList extends Tag {
/**
* Case-sensitive value of the tag
*/
String value;
/**
* {@inheritDoc}
*/
@Override
public void processMatch(Matcher matcher) {
value = matcher.group(1);
if (listStack.isEmpty()) {
if (lastElement != null) {
lastElement.endText += matcher.group(3) + value;
}
return;
}
ListElement listToClose = listStack.pop();
listToClose.endText = matcher.group(3);
listToClose.setClosingList(this);
lastElement = listToClose;
}
public String getValue() {
return value;
}
}
}