/* * Copyright (C) 2011 René Jeschke <rene_jeschke@yahoo.de> * * Licensed 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. */ package io.kaif.kmark; import java.io.IOException; import java.io.Reader; import java.io.StringReader; import org.springframework.web.util.HtmlUtils; public class KmarkProcessor { public static String process(final String input) { try { return new KmarkProcessor().process(new StringReader(input)); } catch (IOException ignore) { //never happen, it's string reader return null; } } public static String escapeHtml(String input) { return HtmlUtils.htmlEscape(input); } private KmarkProcessor() { } private String process(final Reader reader) throws IOException { Configuration configuration = new Configuration.Builder().build(); Emitter emitter = new Emitter(configuration); final HtmlEscapeStringBuilder out = new HtmlEscapeStringBuilder(); final Block parent = this.readLines(reader, emitter); parent.removeSurroundingEmptyLines(); this.recurse(parent, false); Block block = parent.blocks; while (block != null) { emitter.emit(out, block); block = block.next; } emitter.emitRefLinks(out); return out.toString(); } private Block readLines(final Reader reader, final Emitter emitter) throws IOException { final Block block = new Block(); final StringBuilder sb = new StringBuilder(200); boolean underCodeBlock = false; int c = reader.read(); LinkRef lastLinkRef = null; while (c != -1) { sb.setLength(0); int pos = 0; boolean eol = false; while (!eol) { switch (c) { case -1: eol = true; break; case '\n': c = reader.read(); if (c == '\r') { c = reader.read(); } eol = true; break; case '\r': c = reader.read(); if (c == '\n') { c = reader.read(); } eol = true; break; case '\t': { final int np = pos + (4 - (pos & 3)); while (pos < np) { sb.append(' '); pos++; } c = reader.read(); break; } default: pos++; sb.append((char) c); c = reader.read(); break; } } final Line line = new Line(); line.value = sb.toString(); line.init(); if (line.getLineType() == LineType.FENCED_CODE) { underCodeBlock = !underCodeBlock; } if (underCodeBlock) { block.appendLine(line); continue; } // Check for link definitions boolean isLinkRef = false; String id = null, link = null, comment = null; if (!line.isEmpty && line.value.charAt(line.leading) == '[') { line.pos = line.leading + 1; // Read ID up to ']' id = line.readUntil(']'); // Is ID valid and are there any more characters? if (id != null && line.pos + 2 < line.value.length()) { // Check for ':' ([...]:...) if (line.value.charAt(line.pos + 1) == ':') { line.pos += 2; line.skipSpaces(); // Check for link syntax if (line.value.charAt(line.pos) == '<') { line.pos++; link = line.readUntil('>'); line.pos++; } else { link = line.readUntil(' ', '\n'); } // Is link valid? if (link != null) { // Any non-whitespace characters following? if (line.skipSpaces()) { final char ch = line.value.charAt(line.pos); // Read comment if (ch == '\"' || ch == '\'' || ch == '(') { line.pos++; comment = line.readUntil(ch == '(' ? ')' : ch); // Valid linkRef only if comment is valid if (comment != null) { isLinkRef = true; } } } else { isLinkRef = true; } } } } } if (isLinkRef) { // Store linkRef and skip line final LinkRef lr = emitter.addLinkRef(id, link, comment); if (comment == null) { lastLinkRef = lr; } } else { comment = null; // Check for multi-line linkRef if (!line.isEmpty && lastLinkRef != null) { line.pos = line.leading; final char ch = line.value.charAt(line.pos); if (ch == '\"' || ch == '\'' || ch == '(') { line.pos++; comment = line.readUntil(ch == '(' ? ')' : ch); } if (comment != null) { lastLinkRef.title = comment; } lastLinkRef = null; } // No multi-line linkRef, store line if (comment == null) { line.pos = 0; block.appendLine(line); } } } return block; } /** * Initializes a list block by separating it into list item blocks. * * @param root * The Block to process. */ private void initListBlock(final Block root) { Line line = root.lines; line = line.next; while (line != null) { final LineType t = line.getLineType(); if ((t == LineType.OLIST || t == LineType.ULIST) || (!line.isEmpty && (line.prevEmpty && line.leading == 0))) { root.split(line.previous).type = BlockType.LIST_ITEM; } line = line.next; } root.split(root.lineTail).type = BlockType.LIST_ITEM; } /** * Recursively process the given Block. * * @param root * The Block to process. * @param listMode * Flag indicating that we're in a list item block. */ private void recurse(final Block root, boolean listMode) { Block block, list; Line line = root.lines; if (listMode) { root.removeListIndent(); } while (line != null && line.isEmpty) { line = line.next; } if (line == null) { return; } while (line != null) { final LineType type = line.getLineType(); switch (type) { case OTHER: { final boolean wasEmpty = line.prevEmpty; while (line != null && !line.isEmpty) { final LineType t = line.getLineType(); if ((listMode) && (t == LineType.OLIST || t == LineType.ULIST)) { break; } if (t == LineType.FENCED_CODE || t == LineType.BQUOTE) { break; } line = line.next; } final BlockType bt; if (line != null && !line.isEmpty) { bt = (!wasEmpty) ? BlockType.NONE : BlockType.PARAGRAPH; root.split(line.previous).type = bt; root.removeLeadingEmptyLines(); } else { bt = (listMode && (line == null || !line.isEmpty) && !wasEmpty) ? BlockType.NONE : BlockType.PARAGRAPH; root.split(line == null ? root.lineTail : line).type = bt; root.removeLeadingEmptyLines(); } line = root.lines; break; } case BQUOTE: while (line != null) { if (!line.isEmpty && (line.prevEmpty && line.leading == 0 && line.getLineType() != LineType.BQUOTE)) { break; } line = line.next; } block = root.split(line != null ? line.previous : root.lineTail); block.type = BlockType.BLOCKQUOTE; block.removeSurroundingEmptyLines(); block.removeBlockQuotePrefix(); this.recurse(block, false); line = root.lines; break; case FENCED_CODE: line = line.next; while (line != null) { if (line.getLineType() == LineType.FENCED_CODE) { break; } // TODO ... is this really necessary? Maybe add a special // flag? line = line.next; } if (line != null) { line = line.next; } block = root.split(line != null ? line.previous : root.lineTail); block.type = BlockType.FENCED_CODE; block.meta = Utils.getMetaFromFence(block.lines.value); block.lines.setEmpty(); if (block.lineTail.getLineType() == LineType.FENCED_CODE) { block.lineTail.setEmpty(); } block.removeSurroundingEmptyLines(); break; case OLIST: case ULIST: while (line != null) { final LineType t = line.getLineType(); if (!line.isEmpty && (line.prevEmpty && line.leading == 0 && !(t == LineType.OLIST || t == LineType.ULIST))) { break; } line = line.next; } list = root.split(line != null ? line.previous : root.lineTail); list.type = type == LineType.OLIST ? BlockType.ORDERED_LIST : BlockType.UNORDERED_LIST; list.lines.prevEmpty = false; list.lineTail.nextEmpty = false; list.removeSurroundingEmptyLines(); list.lines.prevEmpty = list.lineTail.nextEmpty = false; initListBlock(list); block = list.blocks; while (block != null) { this.recurse(block, true); block = block.next; } list.expandListParagraphs(); break; default: line = line.next; break; } } } }