package org.simantics.scl.compiler.markdown.inlines; import org.simantics.scl.compiler.markdown.internal.Scanner; import org.simantics.scl.compiler.markdown.nodes.AutolinkNode; import org.simantics.scl.compiler.markdown.nodes.CodeNode; import org.simantics.scl.compiler.markdown.nodes.EmphNode; import org.simantics.scl.compiler.markdown.nodes.HardLineBreakNode; import org.simantics.scl.compiler.markdown.nodes.HtmlTagNode; import org.simantics.scl.compiler.markdown.nodes.ImageNode; import org.simantics.scl.compiler.markdown.nodes.LinkNode; import org.simantics.scl.compiler.markdown.nodes.Node; import org.simantics.scl.compiler.markdown.nodes.Reference; import org.simantics.scl.compiler.markdown.nodes.TextNode; import gnu.trove.map.hash.THashMap; public class Subject { THashMap referenceMap; StringBuilder input; int pos; Delimiter lastDelim; public Subject(THashMap referenceMap, StringBuilder input) { this.referenceMap = referenceMap; this.input = input; this.pos = 0; } public static void parseInlines(THashMap referenceMap, Node parent) { Subject subject = new Subject(referenceMap, parent.stringContent); while(!subject.isEof() && subject.parseInline(parent)); subject.processEmphasis(null); parent.stringContent = null; } private void processEmphasis(Delimiter begin) { if(lastDelim == begin) return; // Find first delimiter Delimiter closer = lastDelim; while(closer.previous != begin) closer = closer.previous; // Loop all delimeters closer = closer.next; while(closer != null) { if(closer.canClose) { // Find opener for(Delimiter opener = closer.previous; opener != begin; opener = opener.previous) { if(opener.canOpen && opener.delimChar == closer.delimChar) { closer = insertEmph(opener, closer); break; } } } closer = closer.next; } } private Delimiter insertEmph(Delimiter opener, Delimiter closer) { // Remove all delimiters between opener and closer opener.next = closer; closer.previous = opener; // Length int openerLength = opener.inlText.stringContent.length(); int closerLength = closer.inlText.stringContent.length(); int commonLength = Math.min(openerLength, closerLength); if(commonLength > 2) commonLength = 2 - (closerLength % 2); // Add emph EmphNode emph = new EmphNode(commonLength==2); emph.firstChild = opener.inlText.next; emph.lastChild = closer.inlText.prev; emph.firstChild.prev = null; emph.lastChild.next = null; opener.inlText.next = emph; closer.inlText.prev = emph; emph.next = closer.inlText; emph.prev = opener.inlText; emph.parent = opener.inlText.parent; for(Node node = emph.firstChild;node != null;node = node.next) node.parent = emph; // Remove if(openerLength == commonLength) { removeDelim(opener); opener.inlText.remove(); } else opener.inlText.stringContent.delete(openerLength-commonLength, openerLength); if(closerLength == commonLength) { removeDelim(closer); closer.inlText.remove(); return closer; } else { closer.inlText.stringContent.delete(closerLength-commonLength, closerLength); if(closer.previous != null) return closer.previous; else return closer; } } private boolean parseInline(Node parent) { Node newInl = null; char c = peekChar(); if(c == 0) return false; switch(c) { case '\\': newInl = handleBackslash(); break; case '`': newInl = handleBackticks(); break; case '<': newInl = handlePointyBrace(); break; case '\n': newInl = handleNewline(); break; case '[': newInl = new TextNode(new StringBuilder("[")); lastDelim = new Delimiter(lastDelim, newInl, pos, '[', false, false); ++pos; break; case '!': ++pos; if(peekChar() == '[') { newInl = new TextNode(new StringBuilder("![")); lastDelim = new Delimiter(lastDelim, newInl, pos-1, '!', false, false); ++pos; } else newInl = new TextNode(new StringBuilder("!")); break; case ']': newInl = handleCloseBracket(); if(newInl == null) newInl = new TextNode(new StringBuilder("]")); break; case '&': newInl = handleEntity(); if(newInl == null) newInl = new TextNode(new StringBuilder("&")); break; case '*': case '_': case '\'': case '"': newInl = handleDelim(c); break; default: { int startPos = pos; ++pos; while(pos < input.length() && !isSpecialChar(input.charAt(pos))) ++pos; char nc = peekChar(); int tEnd = pos; if(nc == '\n' || nc == 0) { while(tEnd > startPos && input.charAt(tEnd-1) == ' ') --tEnd; } newInl = new TextNode(new StringBuilder(input.subSequence(startPos, tEnd))); } } if(newInl != null) addChild(parent, newInl); return true; } private Node handleEntity() { ++pos; if(peekChar() == '#') { int p = pos+1; if(p == input.length()) return null; char c = input.charAt(p); if(c == 'x' || c == 'X') { int code = 0; for(int i=0;i<8;++i) { ++p; c = input.charAt(p); if(c == ';') { if(p == pos+2) return null; pos = p+1; if(!Character.isValidCodePoint(code)) code = 0xFFFD; return new TextNode(new StringBuilder(new String(new int[] {code}, 0, 1))); } else if(c >= '0' && c <= '9') { code *= 16; code += (int)(c - '0'); } else if(c >= 'a' && c <= 'f') { code *= 16; code += (int)(c - 'a') + 10; } else if(c >= 'A' && c <= 'F') { code *= 16; code += (int)(c - 'A') + 10; } } return null; } else if(c >= '0' && c <= '9'){ int code = (int)(c - '0'); for(int i=0;i<8;++i) { ++p; c = input.charAt(p); if(c == ';') { if(p == pos+1) return null; pos = p+1; if(!Character.isValidCodePoint(code)) code = 0xFFFD; return new TextNode(new StringBuilder(new String(new int[] {code}, 0, 1))); } else if(c >= '0' && c <= '9') { code *= 10; code += (int)(c - '0'); } } return null; } else return null; } else { int maxPos = Math.min(input.length(), pos+Entities.MAX_ENTITY_LENGTH+1); int p = pos; while(p < maxPos) { char c = input.charAt(p++); if(c == ';') { String entity = input.substring(pos, p-1); String character = Entities.ENTITY_MAP.get(entity); if(character == null) return null; else { pos = p; return new TextNode(new StringBuilder(character)); } } } return null; } } private Node handleCloseBracket() { ++pos; Delimiter opener = lastDelim; while(opener != null) { if(opener.delimChar == '[' || opener.delimChar == '!') break; opener = opener.previous; } if(opener == null) return null; remove(opener); if(!opener.active) return null; String label = input.substring(opener.position+(opener.delimChar == '[' ? 1 : 2), pos-1); String url, title; int urlStart, urlEnd; if(pos < input.length() && input.charAt(pos) == '(' && (urlStart = Scanner.scanWhitespace(input, pos+1)) >= 0 && (urlEnd = Scanner.scanLinkUrl(input, urlStart)) >= 0) { int titleStart = Scanner.scanWhitespace(input, urlEnd); if(titleStart == -1) return null; int titleEnd = titleStart == urlEnd ? titleStart : Scanner.scanLinkTitle(input, titleStart); if(titleEnd == -1) return null; int endAll = Scanner.scanWhitespace(input, titleEnd); if(endAll == -1 || input.charAt(endAll) != ')') return null; pos = endAll + 1; if(input.charAt(urlStart) == '<') url = input.substring(urlStart+1, urlEnd-1); else url = input.substring(urlStart, urlEnd); url = Reference.cleanUrl(url); title = titleStart==titleEnd ? "" : Reference.cleanTitle(input.substring(titleStart+1, titleEnd-1)); } else { int originalPos = pos; String normalizedLabel = null; tryLink: { int linkStart = Scanner.scanWhitespace(input, pos); if(linkStart == -1 || input.charAt(linkStart) != '[') break tryLink; int linkEnd = Scanner.scanLinkLabel(input, linkStart); if(linkEnd == -1) break tryLink; if(linkStart+2 < linkEnd) normalizedLabel = Reference.normalizeLabel(input.substring(linkStart+1, linkEnd-1)); pos = linkEnd; } if(normalizedLabel == null) normalizedLabel = Reference.normalizeLabel(label); Reference reference = referenceMap.get(normalizedLabel); if(reference == null) { pos = originalPos; return null; } url = reference.url; title = reference.title; } Node newLast = opener.inlText.prev; Node parent = opener.inlText.parent; Node newNode; processEmphasis(opener.previous); if(opener.delimChar == '[') { newNode = new LinkNode(label, url, title); } else { newNode = new ImageNode(label, url, title); } opener.inlText.prev = null; newNode.firstChild = opener.inlText; newNode.lastChild = parent.lastChild; for(Node node = newNode.firstChild;node != null;node = node.next) node.parent = newNode; opener.inlText.remove(); parent.lastChild = newLast; if(newLast != null) newLast.next = null; else parent.firstChild = null; lastDelim = opener.previous; if(lastDelim != null) lastDelim.next = null; if(opener.delimChar == '[') for(Delimiter cur = lastDelim;cur != null && cur.active; cur = cur.previous) if(cur.delimChar == '[') cur.active = false; return newNode; } private void remove(Delimiter delimiter) { if(delimiter.previous != null) delimiter.previous.next = delimiter.next; if(delimiter.next != null) delimiter.next.previous = delimiter.previous; else lastDelim = delimiter.previous; } private Node handleNewline() { int nlPos = pos; ++pos; while(peekChar() == ' ') ++pos; if(nlPos > 1 && input.charAt(nlPos-1) == ' ' && input.charAt(nlPos-2) == ' ') return new HardLineBreakNode(); else return new TextNode(new StringBuilder("\n")); } private Node handlePointyBrace() { ++pos; // URL int p = Scanner.scanUri(input, pos); if(p >= 0) { Node result = new AutolinkNode(new StringBuilder(input.substring(pos, p-1)), false); pos = p; return result; } p = Scanner.scanEmail(input, pos); if(p >= 0) { Node result = new AutolinkNode(new StringBuilder(input.substring(pos, p-1)), true); pos = p; return result; } // HTML tag p = Scanner.scanHtmlTag(input, pos); if(p >= 0) { Node result = new HtmlTagNode(new StringBuilder(input.substring(pos-1, p))); pos = p; return result; } return new TextNode(new StringBuilder("<")); } private Node handleBackslash() { ++pos; char c = peekChar(); ++pos; if(c == 0) { StringBuilder b = new StringBuilder(2); b.append('\\'); return new TextNode(b); } if(getCharType(c)==2) { StringBuilder b = new StringBuilder(1); b.append(c); return new TextNode(b); } else if(c == '\n') return new HardLineBreakNode(); else { StringBuilder b = new StringBuilder(2); b.append('\\'); b.append(c); return new TextNode(b); } } private Node handleBackticks() { int startPos = pos; while(peekChar() == '`') ++pos; int tickCount = pos-startPos; char c; int endTickCount; do { while((c = peekChar()) != '`' && c != 0) ++pos; if(c == 0) { pos = startPos+tickCount; StringBuilder b = new StringBuilder(tickCount); for(int i=0;i': case '?': case '@': case '[': case '\\': case ']': case '^': case '_': case '`': case '{': case '|': case '}': case '~': return CHAR_TYPE_PUNCTUATION; default: return CHAR_TYPE_OTHER; } } private char peekChar() { if(pos < input.length()) return input.charAt(pos); else return 0; } private boolean isEof() { return pos >= input.length(); } static final boolean[] SPECIAL_CHARS = new boolean[128]; static final String SPECIALS_STRING = "\n\\`&_*[]= 0 && c < 128 && SPECIAL_CHARS[(int)c]; } private void addChild(Node parent, Node child) { child.parent = parent; if(parent.lastChild == null) parent.firstChild = child; else { Node oldLast = parent.lastChild; oldLast.next = child; child.prev = oldLast; } parent.lastChild = child; } private void removeDelim(Delimiter delim) { Delimiter previous = delim.previous; Delimiter next = delim.next; if(delim == lastDelim) lastDelim = previous; else next.previous = previous; if(previous != null) previous.next = next; } }