package divconq.web.md.process; import java.util.ArrayList; import java.util.HashMap; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; import divconq.lang.op.FuncResult; import divconq.util.StringUtil; import divconq.web.md.Plugin; import divconq.web.md.ProcessContext; import divconq.xml.XElement; import divconq.xml.XText; import divconq.xml.XmlReader; public class Emitter { protected ProcessContext ctx = null; protected HashMap<String, LinkRef> linkRefs = new HashMap<String, LinkRef>(); protected Map<String, Plugin> plugins = new HashMap<String, Plugin>(); public Emitter(ProcessContext ctx) { this.ctx = ctx; for(Plugin plugin : ctx.getConfig().getPlugins()) register(plugin); } public void register(Plugin plugin) { plugins.put(plugin.getIdPlugin(), plugin); } public void addLinkRef(String key, LinkRef linkRef) { this.linkRefs.put(key.toLowerCase(), linkRef); } public void emit(XElement parent, Block root) { root.removeSurroundingEmptyLines(); XElement target = null; switch(root.type) { case RULER: parent.add(new XElement("hr")); return; case NONE: case XML: case PLUGIN: target = parent; break; case HEADLINE: { target = new XElement("h" + root.hlDepth); if (root.id != null) target.setAttribute("id", root.id); parent.add(target); break; } case PARAGRAPH: target = new XElement("p"); if (root.id != null) target.setAttribute("id", root.id); parent.add(target); break; case CODE: case FENCED_CODE: XElement targetparent = new XElement("pre"); target = new XElement("code"); if (root.id != null) target.setAttribute("id", root.id); targetparent.add(target); parent.add(targetparent); break; case BLOCKQUOTE: target = new XElement("blockquote"); if (root.id != null) target.setAttribute("id", root.id); parent.add(target); break; case UNORDERED_LIST: target = new XElement("ul"); if (root.id != null) target.setAttribute("id", root.id); parent.add(target); break; case ORDERED_LIST: target = new XElement("ol"); if (root.id != null) target.setAttribute("id", root.id); parent.add(target); break; case LIST_ITEM: target = new XElement("li"); if (root.id != null) target.setAttribute("id", root.id); parent.add(target); break; } if(root.hasLines()) { switch(root.type) { case CODE: this.emitCodeLines(target, root.lines, root.meta, true); break; case FENCED_CODE: this.emitCodeLines(target, root.lines, root.meta, false); break; case PLUGIN: this.emitPluginLines(target, root.lines, root.meta); break; case XML: this.emitRawLines(target, root.lines); break; default: this.emitMarkedLines(target, root.lines); break; } } else { Block block = root.blocks; while (block != null) { this.emit(target, block); block = block.next; } } } /** * Finds the position of the given Token in the given String. * * @param in * The String to search on. * @param start * The starting character position. * @param token * The token to find. * @return The position of the token or -1 if none could be found. */ private int findToken(String in, int start, MarkToken token) { int pos = start; while(pos < in.length()) { if(this.getToken(in, pos) == token) return pos; pos++; } return -1; } /* * Checks if there is a valid markdown link definition. */ protected int emitLink(XElement parent, String in, int start, MarkToken token) { boolean isAbbrev = false; int pos = start + (token == MarkToken.LINK ? 1 : (token == MarkToken.X_IMAGE) ? 3 : 2); StringBuilder temp = new StringBuilder(); temp.setLength(0); pos = Utils.readMdLinkId(temp, in, pos); if (pos < start) return -1; String name = temp.toString(), link = null, comment = null; int oldPos = pos++; pos = Utils.skipSpaces(in, pos); if (pos < start) { LinkRef lr = this.linkRefs.get(name.toLowerCase()); if (lr == null) return -1; isAbbrev = lr.isAbbrev; link = lr.link; comment = lr.title; pos = oldPos; } else if (in.charAt(pos) == '(') { pos++; pos = Utils.skipSpaces(in, pos); if (pos < start) return -1; temp.setLength(0); boolean useLt = in.charAt(pos) == '<'; pos = useLt ? Utils.readUntil(temp, in, pos + 1, '>') : Utils.readMdLink(temp, in, pos); if (pos < start) return -1; if (useLt) pos++; link = temp.toString(); if (in.charAt(pos) == ' ') { pos = Utils.skipSpaces(in, pos); if (pos > start && in.charAt(pos) == '"') { pos++; temp.setLength(0); pos = Utils.readUntil(temp, in, pos, '"'); if (pos < start) return -1; comment = temp.toString(); pos++; pos = Utils.skipSpaces(in, pos); if (pos == -1) return -1; } } // grab the position for X_IMAGE if (pos > start && in.charAt(pos) == '"') { pos++; temp.setLength(0); pos = Utils.readUntil(temp, in, pos, '"'); if(pos < start) return -1; //position = temp.toString(); pos++; pos = Utils.skipSpaces(in, pos); if(pos == -1) return -1; } if (in.charAt(pos) != ')') return -1; } else if (in.charAt(pos) == '[') { pos++; temp.setLength(0); pos = Utils.readRawUntil(temp, in, pos, ']'); if (pos < start) return -1; String id = (temp.length() > 0) ? temp.toString() : name; LinkRef lr = this.linkRefs.get(id.toLowerCase()); if (lr != null) { link = lr.link; comment = lr.title; } } else { LinkRef lr = this.linkRefs.get(name.toLowerCase()); if (lr == null) return -1; isAbbrev = lr.isAbbrev; link = lr.link; comment = lr.title; pos = oldPos; } if (link == null) return -1; if (token == MarkToken.LINK) { if(isAbbrev && comment != null) { XElement anchr = new XElement("abbr") .withAttribute("title", comment); this.recursiveEmitLine(anchr, name, 0, MarkToken.NONE); } else { XElement anchr = new XElement("a") .withAttribute("href", link) .withAttribute("alt", name); parent.add(anchr); if(comment != null) anchr.withAttribute("title", comment); this.recursiveEmitLine(anchr, name, 0, MarkToken.NONE); } } else if (token == MarkToken.IMAGE) { XElement img = new XElement("img") .withAttribute("src", link) .withAttribute("alt", name); if (comment != null) img.withAttribute("title", comment); parent.add(img); } else { // X_IMAGE a captioned image XElement div = new XElement("div") .withAttribute("class", "inline-img"); div.add(new XElement("img") .withAttribute("src", link) .withAttribute("alt", name)); if (comment != null) div.add(new XElement("div").withText(comment)); parent.add(div); } return pos; } /** * Check if there is a valid HTML tag here. This method also transforms auto * links and mailto auto links. * * @param out * The StringBuilder to write to. * @param in * Input String. * @param start * Starting position. * @return The new position or -1 if nothing valid has been found. */ protected int emitHtml(XElement parent, String in, int start) { StringBuilder temp = new StringBuilder(); int pos; // Check for auto links temp.setLength(0); pos = Utils.readUntil(temp, in, start + 1, ':', ' ', '>', '\n'); if (pos != -1 && in.charAt(pos) == ':' && in.length() > (pos + 2) && in.charAt(pos + 1) == '/' && in.charAt(pos + 2) == '/') { pos = Utils.readUntil(temp, in, pos, '>'); if (pos != -1) { String link = temp.toString(); parent.add(new XElement("a").withAttribute("href", link).withText(link)); return pos; } } // Check for mailto or address auto link temp.setLength(0); pos = Utils.readUntil(temp, in, start + 1, '@', ' ', '>', '\n'); if (pos != -1 && in.charAt(pos) == '@') { pos = Utils.readUntil(temp, in, pos, '>'); if (pos != -1) { String link = temp.toString(); XElement xml = new XElement("a"); parent.add(xml); //address auto links if(link.startsWith("@")) { String slink = link.substring(1); String url = "https://maps.google.com/maps?q=" + slink.replace(' ', '+'); xml.withAttribute("href", url).withText(slink); } //mailto auto links else { xml.withAttribute("href", "mailto:" + link).withText(link); } return pos; } } // Check for inline html if (start + 2 < in.length()) { //temp.setLength(0); //pos = Utils.readXML(temp, in, start, this.config.safeMode); pos = Utils.scanHTML(in, start); if (pos > 0) { String xml = in.substring(start, pos + 1); FuncResult<XElement> xres = XmlReader.parse(xml, false); if (xres.isNotEmptyResult()) parent.add(xres.getResult()); } return pos; } return -1; } /** * Check if this is a valid XML/HTML entity. * * @param out * The StringBuilder to write to. * @param in * Input String. * @param start * Starting position * @return The new position or -1 if this entity in invalid. */ protected int emitEntity(XElement parent, String in, int start, MarkToken token) { /* int pos = start; while (pos < in.length()) { if (in.charAt(pos) == ';') break; pos++; } // nothing found if ((pos == in.length()) || (pos - start < 3)) return -1; */ if (in.length() - start < 3) return -1; int pos = -1; if (in.charAt(start + 1) == '#') { if (in.charAt(start + 2) == 'x' || in.charAt(start + 2) == 'X') { if (in.length() - start < 4) return -1; for (int i = start + 3; i < in.length(); i++) { char c = in.charAt(i); if (c == ';') { pos = i; break; } if ((c < '0' || c > '9') && ((c < 'a' || c > 'f') && (c < 'A' || c > 'F'))) return -1; } } else { for (int i = start + 2; i < in.length(); i++) { char c = in.charAt(i); if (c == ';') { pos = i; break; } if (c < '0' || c > '9') return -1; } } } else { for (int i = start + 1; i < in.length(); i++) { char c = in.charAt(i); if (c == ';') { pos = i; break; } if ((c < 'a' || c > 'z') && (c < 'A' || c > 'Z')) return -1; } } parent.appendRaw(in.substring(start, pos + 1)); return pos; } protected int emitCode(XElement parent, String in, int start, MarkToken token) { boolean dub = (token == MarkToken.CODE_DOUBLE); int a = start + (dub ? 2 : 1); int b = this.findToken(in, a, token); if (b < start + 1) return -1; int pos = b + (dub ? 1 : 0); parent.add(new XElement("code").withCData(in.substring(a, b - 1))); return pos; } protected int emitEm(XElement parent, String in, int start, MarkToken token) { int b = this.findToken(in, start + 1, token); if (b > 0) { XElement em = new XElement("em"); parent.add(em); this.recursiveEmitLine(em, in.substring(start + 1, b), 0, token); return b; } return -1; } protected int emitSuper(XElement parent, String in, int start, MarkToken token) { int b = this.findToken(in, start + 1, token); if (b > 0) { XElement em = new XElement("sup"); parent.add(em); this.recursiveEmitLine(em, in.substring(start + 1, b - 1), 0, token); //this.recursiveEmitLine(em, in.substring(1, in.length() - 2), 0, token); return b; } return -1; } protected int emitStrong(XElement parent, String in, int start, MarkToken token) { int b = this.findToken(in, start + 2, token); if (b > 0) { XElement em = new XElement("strong"); parent.add(em); this.recursiveEmitLine(em, in.substring(start + 2, b), 0, token); //this.recursiveEmitLine(em, in.substring(start + 2, b - 4), 0, token); return b + 1; } return -1; } protected int emitStrike(XElement parent, String in, int start, MarkToken token) { int b = this.findToken(in, start + 2, token); if (b > 0) { XElement em = new XElement("s"); parent.add(em); this.recursiveEmitLine(em, in.substring(start + 2, b), 0, token); //this.recursiveEmitLine(em, in.substring(2, in.length() - 4), 0, token); return b + 1; } return -1; } /* * Recursively scans through the given line, taking care of any markdown * stuff. */ protected void recursiveEmitLine(XElement parent, String in, int start, MarkToken token) { int pos = start; int b = 0; while (pos < in.length()) { MarkToken mt = this.getToken(in, pos); if ((token != MarkToken.NONE) && (mt == token || token == MarkToken.EM_STAR && mt == MarkToken.STRONG_STAR || token == MarkToken.EM_UNDERSCORE && mt == MarkToken.STRONG_UNDERSCORE)) return; switch(mt) { case IMAGE: case X_IMAGE: case LINK: b = this.emitLink(parent, in, pos, mt); if(b > 0) pos = b; else parent.append(in.charAt(pos)); break; case X_LINK_OPEN: b = 0; //b = this.recursiveEmitLine(parent, in, pos + 2, MarkToken.X_LINK_CLOSE); //b = this.emitXLink(parent, in, pos, mt); /* TODO temp.setLength(0); b = this.recursiveEmitLine(temp, in, pos + 2, MarkToken.X_LINK_CLOSE); if(b > 0 && this.config.specialLinkEmitter != null) { this.config.specialLinkEmitter.emitSpan(out, temp.toString()); pos = b + 1; } else { out.append(in.charAt(pos)); } */ if (b > 0) pos = b; else parent.append(in.charAt(pos)); break; case EM_STAR: case EM_UNDERSCORE: b = this.emitEm(parent, in, pos, mt); if(b > 0) pos = b; else parent.append(in.charAt(pos)); break; case STRONG_STAR: case STRONG_UNDERSCORE: b = this.emitStrong(parent, in, pos, mt); if(b > 0) pos = b; else parent.append(in.charAt(pos)); break; case STRIKE: b = this.emitStrike(parent, in, pos, mt); if(b > 0) pos = b; else parent.append(in.charAt(pos)); break; case SUPER: b = this.emitSuper(parent, in, pos, mt); if(b > 0) pos = b; else parent.append(in.charAt(pos)); break; case CODE_SINGLE: case CODE_DOUBLE: b = this.emitCode(parent, in, pos, mt); if(b > 0) pos = b; else parent.append(in.charAt(pos)); break; case HTML: b = this.emitHtml(parent, in, pos); if(b > 0) pos = b; else parent.append("<"); break; case ENTITY: b = this.emitEntity(parent, in, pos, mt); if (b > 0) pos = b; else parent.append(in.charAt(pos)); break; case X_COPY: parent.appendRaw("©"); pos += 2; break; case X_REG: parent.appendRaw("®"); pos += 2; break; case X_TRADE: parent.appendRaw("™"); pos += 3; break; case X_NDASH: parent.appendRaw("–"); pos++; break; case X_MDASH: parent.appendRaw("—"); pos += 2; break; case X_HELLIP: parent.appendRaw("…"); pos += 2; break; case X_LAQUO: parent.appendRaw("«"); pos++; break; case X_RAQUO: parent.appendRaw("»"); pos++; break; case X_RDQUO: parent.appendRaw("”"); break; case X_LDQUO: parent.appendRaw("“"); break; case ESCAPE: pos++; //$FALL-THROUGH$ default: char ch = in.charAt(pos); if (ch != '\n') parent.append(ch); break; } pos++; } } /** * Turns every whitespace character into a space character. * * @param c * Character to check * @return 32 is c was a whitespace, c otherwise */ private static char whitespaceToSpace(char c) { return Character.isWhitespace(c) ? ' ' : c; } /** * Check if there is any markdown Token. * * @param in * Input String. * @param pos * Starting position. * @return The Token. */ protected MarkToken getToken(String in, int pos) { char c0 = pos > 0 ? whitespaceToSpace(in.charAt(pos - 1)) : ' '; char c = whitespaceToSpace(in.charAt(pos)); char c1 = pos + 1 < in.length() ? whitespaceToSpace(in.charAt(pos + 1)) : ' '; char c2 = pos + 2 < in.length() ? whitespaceToSpace(in.charAt(pos + 2)) : ' '; char c3 = pos + 3 < in.length() ? whitespaceToSpace(in.charAt(pos + 3)) : ' '; switch(c) { case '*': if(c1 == '*') { return c0 != ' ' || c2 != ' ' ? MarkToken.STRONG_STAR : MarkToken.EM_STAR; } return c0 != ' ' || c1 != ' ' ? MarkToken.EM_STAR : MarkToken.NONE; case '_': if(c1 == '_') { return c0 != ' ' || c2 != ' ' ? MarkToken.STRONG_UNDERSCORE : MarkToken.EM_UNDERSCORE; } return Character.isLetterOrDigit(c0) && c0 != '_' && Character.isLetterOrDigit(c1) ? MarkToken.NONE : MarkToken.EM_UNDERSCORE; case '~': if(c1 == '~') { return MarkToken.STRIKE; } return MarkToken.NONE; case '!': if((c1 == '!') && (c2 == '[')) return MarkToken.X_IMAGE; if(c1 == '[') return MarkToken.IMAGE; return MarkToken.NONE; case '[': if(c1 == '[') return MarkToken.X_LINK_OPEN; return MarkToken.LINK; case ']': if(c1 == ']') return MarkToken.X_LINK_CLOSE; return MarkToken.NONE; case '`': return c1 == '`' ? MarkToken.CODE_DOUBLE : MarkToken.CODE_SINGLE; case '\\': switch(c1) { case '\\': case '[': case ']': case '(': case ')': case '{': case '}': case '#': case '"': case '\'': case '.': case '>': case '<': case '*': case '+': case '-': case '_': case '!': case '`': case '^': return MarkToken.ESCAPE; default: return MarkToken.NONE; } case '<': if(c1 == '<') return MarkToken.X_LAQUO; return MarkToken.HTML; case '&': return MarkToken.ENTITY; case '-': if(c1 == '-') return c2 == '-' ? MarkToken.X_MDASH : MarkToken.X_NDASH; break; case '^': return c0 == '^' || c1 == '^' ? MarkToken.NONE : MarkToken.SUPER; case '>': if(c1 == '>') return MarkToken.X_RAQUO; break; case '.': if(c1 == '.' && c2 == '.') return MarkToken.X_HELLIP; break; case '(': if(c1 == 'C' && c2 == ')') return MarkToken.X_COPY; if(c1 == 'R' && c2 == ')') return MarkToken.X_REG; if(c1 == 'T' & c2 == 'M' & c3 == ')') return MarkToken.X_TRADE; break; case '"': if(!Character.isLetterOrDigit(c0) && c1 != ' ') return MarkToken.X_LDQUO; if(c0 != ' ' && !Character.isLetterOrDigit(c1)) return MarkToken.X_RDQUO; break; default: return MarkToken.NONE; } return MarkToken.NONE; } protected void emitMarkedLines(XElement parent, Line lines) { StringBuilder in = new StringBuilder(); Line line = lines; while(line != null) { if(!line.isEmpty) in.append(line.value.substring(line.leading, line.value.length() - line.trailing)); if(line.next != null) in.append("\n<br />"); line = line.next; } this.recursiveEmitLine(parent, in.toString(), 0, MarkToken.NONE); } protected void emitRawLines(XElement parent, Line lines) { Line line = lines; if (this.ctx.getConfig().getSafeMode()) { StringBuilder sb = new StringBuilder(); while (line != null) { if(!line.isEmpty) sb.append(line.value); sb.append('\n'); line = line.next; } parent.add(new XText(false, sb.toString())); // TODO check that safe really is escaped } else { StringBuilder sb = new StringBuilder(); while (line != null) { if (!line.isEmpty) sb.append(line.value); sb.append("\n"); line = line.next; } FuncResult<XElement> res = XmlReader.parse(sb, false); if (res.isNotEmptyResult()) parent.add(res.getResult()); } } protected void emitCodeLines(XElement parent, Line lines, String meta, boolean removeIndent) { Line line = lines; if (StringUtil.isNotEmpty(meta)) parent.setAttribute("class", meta); StringBuilder sb = new StringBuilder(); while (line != null) { if (!line.isEmpty) sb.append(removeIndent ? line.value.substring(4) : line.value); sb.append("\n"); line = line.next; } parent.add(new XText(true, sb.toString())); } /* * interprets a plugin block into the StringBuilder. */ protected void emitPluginLines(XElement parent, Line lines, String meta) { String idPlugin = meta; String sparams = null; Map<String, String> params = null; int iow = meta.indexOf(' '); if (iow != -1) { idPlugin = meta.substring(0, iow); sparams = meta.substring(iow+1); if(sparams != null) params = parsePluginParams(sparams); } if (params == null) params = new HashMap<String, String>(); ArrayList<String> list = new ArrayList<String>(); Line line = lines; while (line != null) { if (line.isEmpty) list.add(""); else list.add(line.value); line = line.next; } Plugin plugin = plugins.get(idPlugin); if(plugin != null) plugin.emit(this.ctx, parent, list, params); } protected Map<String, String> parsePluginParams(String s) { Map<String, String> params = new HashMap<String, String>(); Pattern p = Pattern.compile("(\\w+)=\"*((?<=\")[^\"]+(?=\")|([^\\s]+))\"*"); Matcher m = p.matcher(s); while(m.find()){ params.put(m.group(1), m.group(2)); } return params; } }