diff --git a/src/helma/util/MarkdownProcessor.java b/src/helma/util/MarkdownProcessor.java new file mode 100644 index 00000000..a1bc9244 --- /dev/null +++ b/src/helma/util/MarkdownProcessor.java @@ -0,0 +1,1162 @@ +package helma.util; + +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.util.*; + +public class MarkdownProcessor { + + private HashMap links = new HashMap(); + private int state; + private int i; + private int length; + private char[] chars; + private StringBuilder buffer; + private int lineMarker = 0; + private int paragraphStartMarker = 0; + private boolean listParagraphs = false; + private int codeEndMarker = 0; + private boolean strong, em; + private ElementStack stack = new ElementStack(); + private HashMap spanTags; + + private String result = null; + + // private Logger log = Logger.getLogger(MarkdownProcessor.class); + private int line; + + private final int NONE = 0, NEWLINE = 1, LINK_ID = 2, LINK_URL = 3, + // stage 2 states + HEADER = 4, PARAGRAPH = 5, LIST = 6, HTML_BLOCK = 7, CODE = 8, BLOCKQUOTE = 9; + + static final Set blockTags = new HashSet(); + + static { + blockTags.add("p"); + blockTags.add("div"); + blockTags.add("h1"); + blockTags.add("h2"); + blockTags.add("h3"); + blockTags.add("h4"); + blockTags.add("h5"); + blockTags.add("h6"); + blockTags.add("blockquote"); + blockTags.add("pre"); + blockTags.add("table"); + blockTags.add("tr"); // handle as block tag for pragmatical reasons + blockTags.add("dl"); + blockTags.add("ol"); + blockTags.add("ul"); + blockTags.add("script"); + blockTags.add("noscript"); + blockTags.add("form"); + blockTags.add("fieldset"); + blockTags.add("iframe"); + blockTags.add("math"); + } + + public MarkdownProcessor() {} + + public MarkdownProcessor(String text) { + length = text.length(); + chars = new char[length + 2]; + text.getChars(0, length, chars, 0); + chars[length] = chars[length + 1] = '\n'; + } + + public MarkdownProcessor(File file) throws IOException { + length = (int) file.length(); + chars = new char[length + 2]; + FileReader reader = new FileReader(file); + if (reader.read(chars) != length) { + throw new IOException("Couldn't read file"); + } + chars[length] = chars[length + 1] = '\n'; + } + + public synchronized String process(String text) { + length = text.length(); + chars = new char[length + 2]; + text.getChars(0, length, chars, 0); + chars[length] = chars[length + 1] = '\n'; + return process(); + } + + public synchronized String process() { + if (result == null) { + length = chars.length; + firstPass(); + secondPass(); + result = buffer.toString(); + cleanup(); + } + return result; + } + + /** + * Retrieve a link defined in the source text. If the link is not found, we call + * lookupLink(String) to retrieve it from an external source. + * @param linkId the link id + * @return a String array with the url as first element and the link title as second. + */ + protected String[] getLink(String linkId) { + String[] link = (String[]) links.get(linkId); + if (link == null) { + link = lookupLink(linkId); + } + return link; + } + + /** + * Method to override for extended link lookup, e.g. for integration into a wiki + * @param linkId the link id + * @return a String array with the url as first element and the link title as second. + */ + protected String[] lookupLink(String linkId) { + return null; + } + + /** + * Method to override to create custom HTML tags. + * @param tag the html tag to generate + * @param builder the java.lang.StringBuilder to generate the string + */ + protected void openTag(String tag, StringBuilder builder) { + builder.append('<').append(tag).append('>'); + } + + /** + * First pass: extract links definitions and remove them from the source text. + */ + private synchronized void firstPass() { + int state = NEWLINE; + int linestart = 0; + int indentation = 0; + int indentationChars = 0; + String linkId = null; + String[] linkValue = null; + StringBuffer buffer = new StringBuffer(); + for (int i = 0; i < length; i++) { + // convert \r\n and \r newlines to \n + if (chars[i] == '\r') { + if (i < length && chars[i + 1] == '\n') { + System.arraycopy(chars, i + 1, chars, i, (length - i) - 1); + length -= 1; + } else { + chars[i] = '\n'; + } + } + } + + for (int i = 0; i < length; i++) { + char c = chars[i]; + + switch (state) { + case NEWLINE: + if (c == '[' && indentation < 4) { + state = LINK_ID; + } else if (isSpace(c)) { + indentationChars += 1; + indentation += (c == '\t') ? 4 : 1; + } else if (c == '\n' && indentationChars > 0) { + System.arraycopy(chars, i, chars, i - indentationChars, length - i); + i -= indentationChars; + length -= indentationChars; + } else { + state = NONE; + } + break; + case LINK_ID: + if (c == ']') { + if (i < length - 1 && chars[i + 1] == ':') { + linkId = buffer.toString(); + linkValue = new String[2]; + state = LINK_URL; + i++; + } else { + state = NONE; + } + buffer.setLength(0); + } else { + buffer.append(c); + } + break; + case LINK_URL: + if (c == '<' && buffer.length() == 0) { + continue; + } else if ((Character.isWhitespace(c) || c == '>') && buffer.length() > 0) { + linkValue[0] = buffer.toString().trim(); + buffer.setLength(0); + int j = i + 1; + int newlines = 0; + while (j < length && chars[j] != ')' && Character.isWhitespace(chars[j])) { + if (chars[j] == '\n') { + newlines += 1; + if (newlines > 1) { + break; + } else { + i = j; + c = chars[j]; + } + } + j += 1; + } + if ((chars[j] == '"' || chars[j] == '\'' || chars[j] == '(') && newlines <= 1) { + char quoteChar = chars[j] == '(' ? ')' : chars[j]; + int start = j = j + 1; + int len = -1; + while (j < length && chars[j] != '\n') { + if (chars[j] == quoteChar) { + len = j - start; + } else if (len > -1 && !isSpace(chars[j])) { + len = -1; + } + j += 1; + } + if (len > -1) { + linkValue[1] = new String(chars, start, len); + i = j; + c = chars[j]; + } + } + if (c == '\n') { + links.put(linkId.toLowerCase(), linkValue); + System.arraycopy(chars, i, chars, linestart, length - i); + length -= (i - linestart); + i = linestart; + buffer.setLength(0); + linkId = null; + } + } else { + buffer.append(c); + } + } + + if (c == '\n') { + state = NEWLINE; + linestart = i; + indentation = indentationChars = 0; + } + + } + } + + private synchronized void secondPass() { + state = NEWLINE; + stack.add(new BaseElement()); + spanTags = new HashMap(); + buffer = new StringBuilder((int) (length * 1.2)); + line = 1; + boolean escape = false; + + for (i = 0; i < length; ) { + char c; + + if (state == NEWLINE) { + checkBlock(0); + } + + boolean leadingSpaceChars = true; + + while (i < length && chars[i] != '\n') { + + c = chars[i]; + leadingSpaceChars = leadingSpaceChars && isSpace(c); + + if (state == HTML_BLOCK) { + buffer.append(c); + i += 1; + continue; + } + + if (escape) { + buffer.append(c); + escape = false; + i += 1; + continue; + } else if (c == '\\') { + escape = true; + i += 1; + continue; + } + + switch (c) { + case '*': + case '_': + if (checkEmphasis(c)) { + continue; + } + break; + + case '`': + if (checkCodeSpan(c)) { + continue; + } + break; + + case '[': + if (checkLink(c)) { + continue; + } + break; + + case '!': + if (checkImage()) { + continue; + } + break; + + case '<': + if (checkHtmlLink(c)) { + continue; + } + break; + } + + if (state == HEADER) { + if (c == '#') { + ((Element) stack.peek()).m ++; + } else { + ((Element) stack.peek()).m = 0; + } + } + + if (!leadingSpaceChars) { + buffer.append(c); + } + i += 1; + + } + + while (i < length && chars[i] == '\n') { + + c = chars[i]; + line += 1; + + if (state == HTML_BLOCK && + (i >= length - 1 || chars[i + 1] != '\n')) { + buffer.append(c); + i += 1; + continue; + } + if (state == HEADER) { + Element header = (Element) stack.pop(); + if (header.m > 0) { + buffer.setLength(buffer.length() - header.m); + } + header.close(); + } + + int bufLen = buffer.length(); + boolean markParagraph = bufLen > 0 && buffer.charAt(bufLen - 1) == '\n'; + + if (state == LIST && i < length) { + checkParagraph(listParagraphs); + } + if (state == PARAGRAPH && i < length) { + checkParagraph(true); + checkHeader(); + } + + buffer.append(c); + state = NEWLINE; + lineMarker = buffer.length(); + + if (markParagraph) { + paragraphStartMarker = lineMarker; + } + i += 1; + + } + + } + while (!stack.isEmpty()) { + ((Element) stack.pop()).close(); + } + } + + private boolean checkBlock(int blockquoteNesting) { + int indentation = 0; + int j = i; + while (j < length && isSpace(chars[j])) { + indentation += chars[j] == '\t' ? 4 : 1; + j += 1; + } + + if (j < length) { + char c = chars[j]; + + if (checkBlockquote(c, j, indentation, blockquoteNesting)) { + return true; + } + + if (checkCodeBlock(c, j, indentation, blockquoteNesting)) { + return true; + } + + if (checkList(c, j, indentation, blockquoteNesting)) { + return true; + } + + if (checkAtxHeader(c, j)) { + return true; + } + + if (!checkHtmlBlock(c, j)) { + state = stack.search(ListElement.class) != null ? LIST : PARAGRAPH; + } + } + return false; + } + + private boolean checkEmphasis(char c) { + if (c == '*' || c == '_') { + int n = 1; + int j = i + 1; + while(j < length && chars[j] == c && n <= 3) { + n += 1; + j += 1; + } + int found = n; + boolean isStartTag = j < length - 1 && !Character.isWhitespace(chars[j]); + boolean isEndTag = i > 0 && !Character.isWhitespace(chars[i - 1]); + boolean hasStrong = spanTags.get(Integer.valueOf(2)) != null; + boolean hasEmphasis = spanTags.get(Integer.valueOf(1)) != null; + if (isStartTag && (!hasStrong || !hasEmphasis)) { + List matchingEndTags = new ArrayList(); + char lastChar = 0; + int count = 0; + for (int k = j; k < length; k++) { + if (chars[k] == '\n' && lastChar == '\n') { + break; + } + if (chars[k] == c) { + count += 1; + } else { + if (count > 0 && !Character.isWhitespace(chars[k - count - 1])) { + matchingEndTags.add(Integer.valueOf(count)); + } + count = 0; + } + lastChar = chars[k]; + } + + String[] tryElements = { + hasEmphasis ? null : "em", + hasStrong || n < 2 ? null : "strong" + }; + Stack matchedElements = new Stack(); + for (int l = tryElements.length - 1; l >= 0; l--) { + for (int k = 0; k < matchingEndTags.size(); k++) { + // FIXME bogus check + if (matchedElements.size() == tryElements.length) { + break; + } + if (n > l && tryElements[l] != null && ((Integer) matchingEndTags.get(k)).intValue() > l) { + matchedElements.add(tryElements[l]); + n -= l + 1; + matchingEndTags.set(k, Integer.valueOf(((Integer) matchingEndTags.get(k)).intValue() - l + 1)); + tryElements[l] = null; + } + } + } + + while (matchedElements.size() > 0) { + String ctor = (String) matchedElements.pop(); + Element elem = "em".equals(ctor) ? (Element) new Emphasis() : new Strong(); + elem.open(); + spanTags.put(Integer.valueOf("strong".equals(ctor) ? 2 : 1), elem); + } + } + if (isEndTag) { + for (int z = 2; z > 0; z--) { + Element elem = (Element) spanTags.get(Integer.valueOf(z)); + if (elem != null && elem.m <= n) { + spanTags.remove(Integer.valueOf(z)); + elem.close(); + n -= elem.m; + } + } + } + if (n == found) { + return false; + } + for (int m = 0; m < n; m++) { + buffer.append(c); + } + i = j; + return true; + } + return false; + } + + private boolean checkCodeSpan(char c) { + if (c != '`') { + return false; + } + int n = 0; // additional backticks to match + int j = i + 1; + StringBuffer code = new StringBuffer(); + while(j < length && chars[j] == '`') { + n += 1; + j += 1; + } + outer: while(j < length) { + if (chars[j] == '`') { + if (n == 0) { + break; + } else { + if (j + n >= length) { + return false; + } + for (int k = j + 1; k <= j + n; k++) { + if (chars[k] != '`') { + break; + } else if (k == j + n) { + j = k; + break outer; + } + } + + } + } + if (chars[j] == '&') { + code.append("&"); + } else if (chars[j] == '<') { + code.append("<"); + } else if (chars[j] == '>') { + code.append(">"); + } else { + code.append(chars[j]); + } + j += 1; + } + openTag("code", buffer); + buffer.append(code.toString().trim()).append(""); + i = j + 1; + return true; + } + + private boolean checkLink(char c) { + return checkLinkInternal(c, i + 1, false); + } + + private boolean checkImage() { + return checkLinkInternal(chars[i + 1], i + 2, true); + } + + private boolean checkLinkInternal(char c, int j, boolean isImage) { + if (c != '[') { + return false; + } + StringBuffer b = new StringBuffer(); + boolean escape = false; + boolean space = false; + int nesting = 0; + boolean needsEncoding = false; + while (j < length && (escape || chars[j] != ']' || nesting != 0)) { + c = chars[j]; + if (c == '\n' && chars[j - 1] == '\n') { + return false; + } + + if (escape) { + b.append(c); + escape = false; + } else { + escape = c == '\\'; + if (!escape) { + if (c == '[') { + nesting += 1; + } else if (c == ']') { + nesting -= 1; + } + if (c == '*' || c == '_' || c == '`') { + needsEncoding = true; + } + boolean s = Character.isWhitespace(chars[j]); + if (!space || !s) { + b.append(s ? ' ' : c); + } + space = s; + } + } + j += 1; + } + String text = b.toString(); + b.setLength(0); + String[] link; + String linkId; + int k = j; + j += 1; + // this is weird, bug we follow the official markup implementation here: + // only accept space between link text and link target for [][], not for []() + boolean extraSpace = false; + if (j < length && isSpace(chars[j])) { + j += 1; + extraSpace = true; + } + c = chars[j++]; + if (c == '[') { + while (j < length && chars[j] != ']') { + if (chars[j] == '\n') { + return false; + } + b.append(chars[j]); + j += 1; + } + linkId = b.toString().toLowerCase(); + if (linkId.length() > 0) { + link = getLink(linkId); + if (link == null) { + return false; + } + } else { + linkId = text.toLowerCase(); + link = getLink(linkId); + if (link == null) { + return false; + } + } + } else if (c == '(' && !extraSpace) { + link = new String[2]; + while (j < length && chars[j] != ')' && !isSpace(chars[j])) { + if (chars[j] == '\n') { + return false; + } + b.append(chars[j]); + j += 1; + } + link[0] = b.toString(); + if (j < length && chars[j] != ')') { + while (j < length && chars[j] != ')' && Character.isWhitespace(chars[j])) { + j += 1; + } + if (chars[j] == '"') { + int start = j = j + 1; + int len = -1; + while (j < length && chars[j] != '\n') { + if (chars[j] == '"') { + len = j - start; + } else if (len > -1) { + if (chars[j] == ')') { + link[1] = new String(chars, start, len); + break; + } else if (!isSpace(chars[j])) { + len = -1; + } + } + j += 1; + } + } + if (chars[j] != ')') { + return false; + } + } + } else { + j = k; + linkId = text.toLowerCase(); + link = getLink(linkId); + if (link == null) { + return false; + } + } + b.setLength(0); + if (isImage) { + buffer.append("\"").append(escapeHtml(text)).append("\"");"); + + } else { + buffer.append(""); + if (needsEncoding) { + b.append(escapeHtml(text)).append(""); + } else { + buffer.append(escapeHtml(text)).append(""); + } + } + if (b.length() > 0) { + System.arraycopy(chars, j + 1, chars, i + b.length(), length - j - 1); + b.getChars(0, b.length(), chars, i); + length = i + (length - j - 1) + b.length(); + } else { + System.arraycopy(chars, j + 1, chars, i, length - j - 1); + length -= 1 + j - i; + } + return true; + } + + private boolean checkHtmlLink(char c) { + if (c != '<') { + return false; + } + int k = i + 1; + int j = k; + while (j < length && !Character.isWhitespace(chars[j]) && chars[j] != '>') { + j += 1; + } + if (chars[j] == '>') { + String href = new String(chars, k, j - k); + if (href.matches("\\w+:\\S*")) { + String text = href; + if (href.startsWith("mailto:")) { + text = href.substring(7); + href = escapeMailtoUrl(href); + } + buffer.append("") + .append(text).append(""); + i = j + 1; + return true; + } else if (href.matches("^.+@.+\\.[a-zA-Z]+$")) { + buffer.append("") + .append(href).append(""); + i = j + 1; + return true; + } + } + return false; + } + + private boolean checkList(char c, int j, int indentation, int blockquoteNesting) { + int nesting = indentation / 4 + blockquoteNesting; + if (c >= '0' && c <= '9') { + while (j < length && chars[j] >= '0' && chars[j] <= '9' ) { + j += 1; + } + if (j < length && chars[j] == '.') { + checkCloseList("ol", nesting); + checkOpenList("ol", nesting); + i = j + 1; + return true; + } + } else if (c == '*' || c == '+' || c == '-') { + if (c != '+' && checkHorizontalRule(c, j, nesting)) { + return true; + } + j += 1; + if (j < length && isSpace(chars[j])) { + checkCloseList("ul", nesting); + checkOpenList("ul", nesting); + i = j; + return true; + } + } + if (isParagraphStart()) { + // never close list unless there's an empty line + checkCloseList(null, nesting - 1); + } + return false; + } + + private void checkOpenList(String tag, int nesting) { + Element list = stack.search(ListElement.class); + if (list == null || !tag.equals(list.tag) || nesting != list.nesting) { + list = new ListElement(tag, nesting); + stack.push(list); + list.open(); + } else { + stack.closeElementsExclusive(list); + buffer.insert(getBufferEnd(), ""); + } + openTag("li", buffer); + listParagraphs = isParagraphStart(); + lineMarker = paragraphStartMarker = buffer.length(); + state = LIST; + } + + private void checkCloseList(String tag, int nesting) { + Element elem = stack.search(ListElement.class); + while (elem != null && + (elem.nesting > nesting || + (elem.nesting == nesting && tag != null && !elem.tag.equals(tag)))) { + stack.closeElements(elem); + elem = stack.peekNestedElement(); + lineMarker = paragraphStartMarker = buffer.length(); + } + } + + private boolean checkCodeBlock(char c, int j, int indentation, int blockquoteNesting) { + int nesting = indentation / 4; + int nestedLists = stack.countNestedLists(null); + Element code; + if (nesting - nestedLists <= 0) { + code = stack.findNestedElement(CodeElement.class, blockquoteNesting + nestedLists); + if (code != null) { + stack.closeElements(code); + lineMarker = paragraphStartMarker = buffer.length(); + } + return false; + } + code = stack.isEmpty() ? null : (Element) stack.peek(); + if (!(code instanceof CodeElement)) { + code = new CodeElement(blockquoteNesting + nestedLists); + code.open(); + stack.push(code); + } + int sub = 4 + nestedLists * 4; + for (int k = sub; k < indentation; k++) { + buffer.append(' '); + } + while(j < length && chars[j] != '\n') { + if (chars[j] == '&') { + buffer.append("&"); + } else if (chars[j] == '<') { + buffer.append("<"); + } else if (chars[j] == '>') { + buffer.append(">"); + } else if (chars[j] == '\t') { + buffer.append(" "); + } else { + buffer.append(chars[j]); + } + j += 1; + } + codeEndMarker = buffer.length(); + i = j; + state = CODE; + return true; + } + + private boolean checkBlockquote(char c, int j, int indentation, int blockquoteNesting) { + int nesting = indentation / 4; + Element elem = stack.findNestedElement(BlockquoteElement.class, nesting + blockquoteNesting); + if (c != '>' && isParagraphStart() || nesting > stack.countNestedLists(elem)) { + elem = stack.findNestedElement(BlockquoteElement.class, blockquoteNesting); + if (elem != null) { + stack.closeElements(elem); + lineMarker = paragraphStartMarker = buffer.length(); + } + return false; + } + nesting += blockquoteNesting; + elem = stack.findNestedElement(BlockquoteElement.class, nesting); + if (c == '>') { + stack.closeElementsUnlessExists(BlockquoteElement.class, nesting); + if (elem != null && !(elem instanceof BlockquoteElement)) { + stack.closeElements(elem); + elem = null; + } + if (elem == null || elem.nesting < nesting) { + elem = new BlockquoteElement(nesting); + elem.open(); + stack.push(elem); + lineMarker = paragraphStartMarker = buffer.length(); + } else { + lineMarker = buffer.length(); + } + i = isSpace(chars[j+ 1]) ? j + 2 : j + 1; + state = NEWLINE; + checkBlock(nesting + 1); + return true; + } else { + return elem instanceof BlockquoteElement; + } + } + + + private void checkParagraph(boolean paragraphs) { + int paragraphEndMarker = getBufferEnd(); + if (paragraphs && paragraphEndMarker > paragraphStartMarker && + (chars[i + 1] == '\n' || buffer.charAt(buffer.length() - 1) == '\n')) { + buffer.insert(paragraphEndMarker, "

"); + buffer.insert(paragraphStartMarker, "

"); + } + } + + private boolean checkAtxHeader(char c, int j) { + if (c == '#') { + int nesting = 1; + int k = j + 1; + while (k < length && chars[k++] == '#') { + nesting += 1; + } + HeaderElement header = new HeaderElement(nesting); + header.open(); + stack.push(header); + state = HEADER; + i = k - 1; + return true; + } + return false; + } + + private boolean checkHtmlBlock(char c, int j) { + if (c == '<') { + j += 1; + int k = j; + while (k < length && Character.isLetterOrDigit(chars[k])) { + k += 1; + } + String tag = new String(chars, j, k - j).toLowerCase(); + if (blockTags.contains(tag)) { + state = HTML_BLOCK; + return true; + } + } + return false; + } + + private void checkHeader() { + char c = chars[i + 1]; + if (c == '-' || c == '=') { + int j = i + 1; + while (j < length && chars[j] == c) { + j++; + } + if (j < length && chars[j] == '\n') { + if (c == '=') { + buffer.insert(lineMarker, "

"); + buffer.append("

"); + } else { + buffer.insert(lineMarker, "

"); + buffer.append("

"); + } + i = j; + } + } + } + + private boolean checkHorizontalRule(char c, int j, int nesting) { + if (c != '*' && c != '-') { + return false; + } + int count = 1; + int k = j; + while (k < length && (isSpace(chars[k]) || chars[k] == c)) { + k += 1; + if (chars[k] == c) { + count += 1; + } + } + if (count >= 3 && chars[k] == '\n') { + checkCloseList(null, nesting - 1); + buffer.append("
"); + i = k; + return true; + } + return false; + } + + private void cleanup() { + links = null; + chars = null; + buffer = null; + stack = null; + } + + private String escapeHtml(String str) { + if (str.indexOf('"') > -1) { + str = str.replace("\"", """); + } + if (str.indexOf('<') > -1) { + str = str.replace("\"", "<"); + } + if (str.indexOf('>') > -1) { + str = str.replace("\"", ">"); + } + return str; + } + + private String escapeMailtoUrl(String str) { + StringBuffer b = new StringBuffer(); + char[] chars = str.toCharArray(); + for (int i = 0; i < chars.length; i++) { + double random = Math.random(); + if (random < 0.5) { + b.append("&#x").append(Integer.toString(chars[i], 16)).append(";"); + } else if (random < 0.9) { + b.append("&#").append(Integer.toString(chars[i], 10)).append(";"); + } else { + b.append(chars[i]); + } + } + return b.toString(); + } + + boolean isSpace(char c) { + return c == ' ' || c == '\t'; + } + + boolean isParagraphStart() { + return paragraphStartMarker == lineMarker; + } + + int getBufferEnd() { + int l = buffer.length(); + while(l > 0 && buffer.charAt(l - 1) == '\n') { + l -= 1; + } + return l; + } + + class ElementStack extends Stack { + private static final long serialVersionUID = 8514510754511119691L; + + private Element search(Class clazz) { + for (int i = size() - 1; i >= 0; i--) { + Element elem = (Element) get(i); + if (clazz.isInstance(elem)) { + return elem; + } + } + return null; + } + + private int countNestedLists(Element startFromElement) { + int count = 0; + for (int i = size() - 1; i >= 0; i--) { + Element elem = (Element) get(i); + if (startFromElement != null) { + if (startFromElement == elem) { + startFromElement = null; + } + continue; + } + if (elem instanceof ListElement) { + count += 1; + } else if (elem instanceof BlockquoteElement) { + break; + } + } + return count; + } + + private Element peekNestedElement() { + for (int i = size() - 1; i >= 0; i--) { + Element elem = (Element) get(i); + if (elem instanceof ListElement || elem instanceof BlockquoteElement) { + return elem; + } + } + return null; + } + + private Element findNestedElement(Class type, int nesting) { + for (Iterator it = iterator(); it.hasNext();) { + Element elem = (Element) it.next(); + if (nesting == elem.nesting && type.isInstance(elem)) { + return elem; + } + } + return null; + } + + private void closeElements(Element element) { + do { + ((Element) peek()).close(); + } while (pop() != element); + } + + private void closeElementsExclusive(Element element) { + while(peek() != element) { + ((Element) pop()).close(); + } + } + + private void closeElementsUnlessExists(Class type, int nesting) { + Element elem = this.findNestedElement(type, nesting); + if (elem == null) { + while(stack.size() > 0) { + elem = (Element) this.peek(); + if (elem != null && elem.nesting >= nesting) { + ((Element) stack.pop()).close(); + } else { + break; + } + } + } + } + } + + class Element { + String tag; + int nesting, m; + + void open() { + openTag(tag, buffer); + } + + void close() { + buffer.insert(getBufferEnd(), ""); + } + } + + class BaseElement extends Element { + void open() {} + void close() {} + } + + class BlockquoteElement extends Element { + BlockquoteElement(int nesting) { + tag = "blockquote"; + this.nesting = nesting; + } + } + + class CodeElement extends Element { + CodeElement(int nesting) { + this.nesting = nesting; + } + + void open() { + openTag("pre", buffer); + openTag("code", buffer); + } + + void close() { + buffer.insert(codeEndMarker, ""); + } + } + + class HeaderElement extends Element { + HeaderElement(int nesting) { + this.nesting = nesting; + this.tag = "h" + nesting; + } + } + + class ListElement extends Element { + ListElement(String tag, int nesting) { + this.tag = tag; + this.nesting = nesting; + } + + void close() { + buffer.insert(getBufferEnd(), ""); } + } + + class Emphasis extends Element { + Emphasis() { + this.tag = "em"; + this.m = 1; + } + } + + class Strong extends Element { + Strong() { + this.tag = "strong"; + this.m = 2; + } + } + + public static void main(String[] args) throws IOException { + if (args.length != 1) { + System.err.println("Usage: java org.helma.util.MarkdownProcessor FILE"); + return; + } + MarkdownProcessor processor = new MarkdownProcessor(new File(args[0])); + System.out.println(processor.process()); + } + + +} +