/* * 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 divconq.web.md; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.Reader; import java.io.StringReader; import java.nio.file.Files; import java.nio.file.Path; import divconq.log.Logger; import divconq.web.WebContext; import divconq.web.dcui.Nodes; import divconq.web.md.process.Block; import divconq.web.md.process.BlockType; import divconq.web.md.process.Emitter; import divconq.web.md.process.Line; import divconq.web.md.process.LineType; import divconq.web.md.process.LinkRef; import divconq.web.md.process.Utils; import divconq.xml.XElement; /* * this whole thing needs to be rewritten to use one string builder with markers pointing to the blocks, etc */ public class Processor { public static Nodes process(WebContext ctx, Reader reader, Configuration configuration) throws IOException { try { Processor p = new Processor(ctx, reader, configuration); return p.process(); } catch (Exception x) { Logger.warn("Inline MD problems: " + x); throw x; } } public static Nodes process(WebContext ctx, String input, Configuration configuration) throws IOException { try (StringReader in = new StringReader(input)) { return process(ctx, in, configuration); } } public static Nodes process(WebContext ctx, Path file, Configuration configuration) throws IOException { try (BufferedReader in = Files.newBufferedReader(file)) { return process(ctx, in, configuration); } } public static Nodes process(WebContext ctx, InputStream input, Configuration configuration) throws IOException { try (BufferedReader in = new BufferedReader(new InputStreamReader(input, "UTF-8"))) { return process(ctx, in, configuration); } } public static XElement parse(ProcessContext ctx, String input) throws IOException { try (StringReader in = new StringReader(input)) { return parse(ctx, in); } } public static XElement parse(ProcessContext ctx, Reader reader) throws IOException { try { Processor p = new Processor(ctx, reader); return p.toXml(); } catch (Exception x) { Logger.warn("Inline MD problems: " + x); throw x; } } // instance code protected Reader reader = null; protected Emitter emitter = null; protected ProcessContext ctx = null; public Processor(ProcessContext ctx, Reader reader) { this.ctx = ctx; this.reader = reader; this.emitter = new Emitter(this.ctx); } public Processor(WebContext ctx, Reader reader, Configuration config) { this.ctx = new ProcessContext(config, ctx); this.reader = reader; this.emitter = new Emitter(this.ctx); } public Nodes process() throws IOException { // root is used as place holder only, it is ignored here XElement root = this.toXml(); return this.ctx.getWeb().getDomain().parseXml(this.ctx.getWeb(), root); } public XElement toXml() throws IOException { XElement root = new XElement("div"); Block parent = this.readLines(); parent.removeSurroundingEmptyLines(); this.recurse(parent, false); Block block = parent.blocks; while (block != null) { this.emitter.emit(root, block); block = block.next; } //System.out.println(" ============================= "); //System.out.println(root.toString(true)); //System.out.println(" ============================= "); return root; } /** * Reads all lines from our reader. * <p> * Takes care of markdown link references. * </p> * * @return A Block containing all lines. * @throws IOException * If an IO error occurred. */ protected Block readLines() throws IOException { Block block = new Block(); StringBuilder sb = new StringBuilder(1000); int c = this.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 = this.reader.read(); if(c == '\r') c = this.reader.read(); eol = true; break; case '\r': c = this.reader.read(); if(c == '\n') c = this.reader.read(); eol = true; break; case '\t': { int np = pos + (4 - (pos & 3)); while(pos < np) { sb.append(' '); pos++; } c = this.reader.read(); break; } default: pos++; sb.append((char)c); c = this.reader.read(); break; } } Line line = new Line(); line.value = sb.toString(); line.init(); // Check for link definitions boolean isLinkRef = false; String id = null, link = null, comment = null; if(!line.isEmpty && line.leading < 4 && 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()) { 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; } } } } // To make compiler happy: add != null checks if(isLinkRef && id != null && link != null) { // Store linkRef and skip line LinkRef lr = new LinkRef(link, comment, comment != null && (link.length() == 1 && link.charAt(0) == '*')); this.emitter.addLinkRef(id, lr); if(comment == null) lastLinkRef = lr; } else { comment = null; // Check for multi-line linkRef if(!line.isEmpty && lastLinkRef != null) { line.pos = line.leading; 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. */ protected void initListBlock(Block root) { Line line = root.lines; line = line.next; while(line != null) { LineType t = line.getLineType(); if((t == LineType.OLIST || t == LineType.ULIST) || (!line.isEmpty && (line.prevEmpty && line.leading == 0 && !(t == LineType.OLIST || t == LineType.ULIST)))) { 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. */ protected void recurse(Block root, boolean listMode) { Block block, list; Line line = root.lines; if(listMode) { root.removeListIndent(); if(root.lines != null && root.lines.getLineType() != LineType.CODE) { root.id = root.lines.stripID(); } } while(line != null && line.isEmpty) line = line.next; if(line == null) return; while(line != null) { LineType type = line.getLineType(); switch(type) { case OTHER: { boolean wasEmpty = line.prevEmpty; while(line != null && !line.isEmpty) { LineType t = line.getLineType(); // removed || t == LineType.XML if (t == LineType.OLIST || t == LineType.ULIST || t == LineType.CODE || t == LineType.FENCED_CODE || t == LineType.PLUGIN || t == LineType.HEADLINE || t == LineType.HEADLINE1 || t == LineType.HEADLINE2 || t == LineType.HR || t == LineType.BQUOTE) break; line = line.next; } BlockType bt; if(line != null && !line.isEmpty) { bt = (listMode && !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 CODE: while(line != null && (line.isEmpty || line.leading > 3)) { line = line.next; } block = root.split(line != null ? line.previous : root.lineTail); block.type = BlockType.CODE; block.removeSurroundingEmptyLines(); break; case XML: if(line.previous != null) { // FIXME ... this looks wrong root.split(line.previous); } root.split(line.xmlEndLine).type = BlockType.XML; 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 HR: if(line.previous != null) { // FIXME ... this looks wrong root.split(line.previous); } root.split(line).type = BlockType.RULER; root.removeLeadingEmptyLines(); 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 PLUGIN: // plugins may end on same line if ((line.value.length() > 3) && !line.value.endsWith("%%%")) { line = line.next; while(line != null) { if(line.getLineType() == LineType.PLUGIN) 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.PLUGIN; block.removeSurroundingEmptyLines(); block.meta = Utils.getMetaFromFence(block.lines.value); // TODO handle if the %%% is at the end block.lines.setEmpty(); if (block.lineTail.getLineType() == LineType.PLUGIN) block.lineTail.setEmpty(); block.removeSurroundingEmptyLines(); break; case HEADLINE: case HEADLINE1: case HEADLINE2: if(line.previous != null) root.split(line.previous); if(type != LineType.HEADLINE) line.next.setEmpty(); block = root.split(line); block.type = BlockType.HEADLINE; if(type != LineType.HEADLINE) block.hlDepth = type == LineType.HEADLINE1 ? 1 : 2; block.id = block.lines.stripID(); block.transfromHeadline(); root.removeLeadingEmptyLines(); line = root.lines; break; case OLIST: case ULIST: while(line != null) { 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; } } } }