--- /dev/null
+package org.simantics.scl.compiler.markdown.inlines;
+
+import gnu.trove.map.hash.THashMap;
+
+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;
+
+public class Subject {
+ THashMap<String, Reference> referenceMap;
+ StringBuilder input;
+ int pos;
+ Delimiter lastDelim;
+
+ public Subject(THashMap<String, Reference> referenceMap, StringBuilder input) {
+ this.referenceMap = referenceMap;
+ this.input = input;
+ this.pos = 0;
+ }
+
+ public static void parseInlines(THashMap<String, Reference> 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<tickCount;++i)
+ b.append('`');
+ return new TextNode(b);
+ }
+ endTickCount = 0;
+ while(peekChar() == '`') {
+ ++pos;
+ ++endTickCount;
+ }
+ } while(endTickCount != tickCount);
+ return new CodeNode(normalizeWhitespace(startPos+tickCount, pos-tickCount));
+ }
+
+ private StringBuilder normalizeWhitespace(int begin, int end) {
+ while(begin < end && isWhitespace(input.charAt(begin)))
+ ++begin;
+ while(begin < end && isWhitespace(input.charAt(end-1)))
+ --end;
+ StringBuilder b = new StringBuilder(end-begin);
+ boolean lastCharWasWhitespace = false;
+ while(begin < end) {
+ char c = input.charAt(begin++);
+ if(isWhitespace(c)) {
+ if(!lastCharWasWhitespace) {
+ lastCharWasWhitespace = true;
+ b.append(' ');
+ }
+ }
+ else {
+ lastCharWasWhitespace = false;
+ b.append(c);
+ }
+ }
+ return b;
+ }
+
+ private static boolean isWhitespace(char c) {
+ return c == ' ' || c == '\n';
+ }
+
+ private Node handleDelim(char c) {
+ char beforeChar;
+ char afterChar;
+ int startPos = pos;
+
+ if(pos == 0)
+ beforeChar = '\n';
+ else
+ beforeChar = input.charAt(pos-1);
+
+ ++pos;
+ while(pos < input.length() && input.charAt(pos) == c)
+ ++pos;
+
+ if(pos == input.length())
+ afterChar = '\n';
+ else
+ afterChar = input.charAt(pos);
+
+ int beforeCharType = getCharType(beforeChar);
+ int afterCharType = getCharType(afterChar);
+ boolean leftFlanking = afterCharType != CHAR_TYPE_WHITESPACE &&
+ (afterCharType != CHAR_TYPE_PUNCTUATION || beforeCharType != CHAR_TYPE_OTHER);
+ boolean rightFlanking = beforeCharType != CHAR_TYPE_WHITESPACE &&
+ (beforeCharType != CHAR_TYPE_PUNCTUATION || afterCharType != CHAR_TYPE_OTHER);
+
+ boolean canOpen;
+ boolean canClose;
+ if(c == '_') {
+ canOpen = leftFlanking && (rightFlanking ? beforeCharType == CHAR_TYPE_PUNCTUATION : true);
+ canClose = rightFlanking && (leftFlanking ? afterCharType == CHAR_TYPE_PUNCTUATION : true);
+ }
+ else {
+ canOpen = leftFlanking;
+ canClose = rightFlanking;
+ }
+
+ Node inlText = new TextNode(new StringBuilder(input.subSequence(startPos, pos)));
+ lastDelim = new Delimiter(lastDelim, inlText, pos, c, canOpen, canClose);
+ return inlText;
+ }
+
+ public static final int CHAR_TYPE_WHITESPACE = 1;
+ public static final int CHAR_TYPE_PUNCTUATION = 2;
+ public static final int CHAR_TYPE_OTHER = 0;
+
+ public static int getCharType(char c) {
+ switch(c) {
+ case ' ':
+ case '\n':
+ return CHAR_TYPE_WHITESPACE;
+ case '!':
+ case '"':
+ case '#':
+ case '$':
+ case '%':
+ case '&':
+ case '\'':
+ case '(':
+ case ')':
+ case '*':
+ case '+':
+ case ',':
+ case '-':
+ case '.':
+ case '/':
+ case ':':
+ case ';':
+ case '<':
+ case '=':
+ case '>':
+ 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\\`&_*[]<!";
+ static {
+ for(int i=0;i<SPECIALS_STRING.length();++i)
+ SPECIAL_CHARS[(int)SPECIALS_STRING.charAt(i)] = true;
+ }
+
+ static private boolean isSpecialChar(char c) {
+ return c >= 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;
+ }
+}