]> gerrit.simantics Code Review - simantics/platform.git/blobdiff - bundles/org.simantics.scl.compiler/src/org/simantics/scl/compiler/markdown/inlines/Subject.java
Migrated source code from Simantics SVN
[simantics/platform.git] / bundles / org.simantics.scl.compiler / src / org / simantics / scl / compiler / markdown / inlines / Subject.java
diff --git a/bundles/org.simantics.scl.compiler/src/org/simantics/scl/compiler/markdown/inlines/Subject.java b/bundles/org.simantics.scl.compiler/src/org/simantics/scl/compiler/markdown/inlines/Subject.java
new file mode 100644 (file)
index 0000000..163d707
--- /dev/null
@@ -0,0 +1,621 @@
+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;
+    }
+}