X-Git-Url: https://gerrit.simantics.org/r/gitweb?a=blobdiff_plain;f=bundles%2Forg.simantics.scl.compiler%2Fsrc%2Forg%2Fsimantics%2Fscl%2Fcompiler%2Fmarkdown%2Finlines%2FSubject.java;fp=bundles%2Forg.simantics.scl.compiler%2Fsrc%2Forg%2Fsimantics%2Fscl%2Fcompiler%2Fmarkdown%2Finlines%2FSubject.java;h=163d70708ce53ee5957bb27a24ec16be6b3cab80;hb=969bd23cab98a79ca9101af33334000879fb60c5;hp=0000000000000000000000000000000000000000;hpb=866dba5cd5a3929bbeae85991796acb212338a08;p=simantics%2Fplatform.git 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 index 000000000..163d70708 --- /dev/null +++ b/bundles/org.simantics.scl.compiler/src/org/simantics/scl/compiler/markdown/inlines/Subject.java @@ -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 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; + } +}