1 package org.simantics.scl.compiler.markdown.inlines;
3 import org.simantics.scl.compiler.markdown.internal.Scanner;
4 import org.simantics.scl.compiler.markdown.nodes.AutolinkNode;
5 import org.simantics.scl.compiler.markdown.nodes.CodeNode;
6 import org.simantics.scl.compiler.markdown.nodes.EmphNode;
7 import org.simantics.scl.compiler.markdown.nodes.HardLineBreakNode;
8 import org.simantics.scl.compiler.markdown.nodes.HtmlTagNode;
9 import org.simantics.scl.compiler.markdown.nodes.ImageNode;
10 import org.simantics.scl.compiler.markdown.nodes.LinkNode;
11 import org.simantics.scl.compiler.markdown.nodes.Node;
12 import org.simantics.scl.compiler.markdown.nodes.Reference;
13 import org.simantics.scl.compiler.markdown.nodes.TextNode;
15 import gnu.trove.map.hash.THashMap;
17 public class Subject {
18 THashMap<String, Reference> referenceMap;
23 public Subject(THashMap<String, Reference> referenceMap, StringBuilder input) {
24 this.referenceMap = referenceMap;
29 public static void parseInlines(THashMap<String, Reference> referenceMap, Node parent) {
30 Subject subject = new Subject(referenceMap, parent.stringContent);
31 while(!subject.isEof() && subject.parseInline(parent));
32 subject.processEmphasis(null);
33 parent.stringContent = null;
36 private void processEmphasis(Delimiter begin) {
37 if(lastDelim == begin)
40 // Find first delimiter
41 Delimiter closer = lastDelim;
42 while(closer.previous != begin)
43 closer = closer.previous;
45 // Loop all delimeters
47 while(closer != null) {
50 for(Delimiter opener = closer.previous; opener != begin; opener = opener.previous) {
51 if(opener.canOpen && opener.delimChar == closer.delimChar) {
52 closer = insertEmph(opener, closer);
61 private Delimiter insertEmph(Delimiter opener, Delimiter closer) {
62 // Remove all delimiters between opener and closer
64 closer.previous = opener;
67 int openerLength = opener.inlText.stringContent.length();
68 int closerLength = closer.inlText.stringContent.length();
69 int commonLength = Math.min(openerLength, closerLength);
71 commonLength = 2 - (closerLength % 2);
74 EmphNode emph = new EmphNode(commonLength==2);
75 emph.firstChild = opener.inlText.next;
76 emph.lastChild = closer.inlText.prev;
77 emph.firstChild.prev = null;
78 emph.lastChild.next = null;
79 opener.inlText.next = emph;
80 closer.inlText.prev = emph;
81 emph.next = closer.inlText;
82 emph.prev = opener.inlText;
83 emph.parent = opener.inlText.parent;
84 for(Node node = emph.firstChild;node != null;node = node.next)
88 if(openerLength == commonLength) {
90 opener.inlText.remove();
93 opener.inlText.stringContent.delete(openerLength-commonLength, openerLength);
94 if(closerLength == commonLength) {
96 closer.inlText.remove();
100 closer.inlText.stringContent.delete(closerLength-commonLength, closerLength);
101 if(closer.previous != null)
102 return closer.previous;
108 private boolean parseInline(Node parent) {
115 newInl = handleBackslash();
119 newInl = handleBackticks();
123 newInl = handlePointyBrace();
127 newInl = handleNewline();
131 newInl = new TextNode(new StringBuilder("["));
132 lastDelim = new Delimiter(lastDelim, newInl, pos, '[', false, false);
138 if(peekChar() == '[') {
139 newInl = new TextNode(new StringBuilder("!["));
140 lastDelim = new Delimiter(lastDelim, newInl, pos-1, '!', false, false);
144 newInl = new TextNode(new StringBuilder("!"));
148 newInl = handleCloseBracket();
150 newInl = new TextNode(new StringBuilder("]"));
154 newInl = handleEntity();
156 newInl = new TextNode(new StringBuilder("&"));
163 newInl = handleDelim(c);
168 while(pos < input.length() && !isSpecialChar(input.charAt(pos)))
170 char nc = peekChar();
172 if(nc == '\n' || nc == 0) {
173 while(tEnd > startPos && input.charAt(tEnd-1) == ' ')
176 newInl = new TextNode(new StringBuilder(input.subSequence(startPos, tEnd)));
180 addChild(parent, newInl);
184 private Node handleEntity() {
186 if(peekChar() == '#') {
188 if(p == input.length())
190 char c = input.charAt(p);
191 if(c == 'x' || c == 'X') {
193 for(int i=0;i<8;++i) {
200 if(!Character.isValidCodePoint(code))
202 return new TextNode(new StringBuilder(new String(new int[] {code}, 0, 1)));
204 else if(c >= '0' && c <= '9') {
206 code += (int)(c - '0');
208 else if(c >= 'a' && c <= 'f') {
210 code += (int)(c - 'a') + 10;
212 else if(c >= 'A' && c <= 'F') {
214 code += (int)(c - 'A') + 10;
219 else if(c >= '0' && c <= '9'){
220 int code = (int)(c - '0');
221 for(int i=0;i<8;++i) {
228 if(!Character.isValidCodePoint(code))
230 return new TextNode(new StringBuilder(new String(new int[] {code}, 0, 1)));
232 else if(c >= '0' && c <= '9') {
234 code += (int)(c - '0');
243 int maxPos = Math.min(input.length(), pos+Entities.MAX_ENTITY_LENGTH+1);
246 char c = input.charAt(p++);
248 String entity = input.substring(pos, p-1);
249 String character = Entities.ENTITY_MAP.get(entity);
250 if(character == null)
254 return new TextNode(new StringBuilder(character));
262 private Node handleCloseBracket() {
264 Delimiter opener = lastDelim;
265 while(opener != null) {
266 if(opener.delimChar == '[' || opener.delimChar == '!')
268 opener = opener.previous;
276 String label = input.substring(opener.position+(opener.delimChar == '[' ? 1 : 2), pos-1);
280 int urlStart, urlEnd;
281 if(pos < input.length() && input.charAt(pos) == '('
282 && (urlStart = Scanner.scanWhitespace(input, pos+1)) >= 0
283 && (urlEnd = Scanner.scanLinkUrl(input, urlStart)) >= 0) {
284 int titleStart = Scanner.scanWhitespace(input, urlEnd);
287 int titleEnd = titleStart == urlEnd ? titleStart : Scanner.scanLinkTitle(input, titleStart);
290 int endAll = Scanner.scanWhitespace(input, titleEnd);
291 if(endAll == -1 || input.charAt(endAll) != ')')
295 if(input.charAt(urlStart) == '<')
296 url = input.substring(urlStart+1, urlEnd-1);
298 url = input.substring(urlStart, urlEnd);
299 url = Reference.cleanUrl(url);
300 title = titleStart==titleEnd ? "" : Reference.cleanTitle(input.substring(titleStart+1, titleEnd-1));
303 int originalPos = pos;
304 String normalizedLabel = null;
306 int linkStart = Scanner.scanWhitespace(input, pos);
307 if(linkStart == -1 || input.charAt(linkStart) != '[')
309 int linkEnd = Scanner.scanLinkLabel(input, linkStart);
312 if(linkStart+2 < linkEnd)
313 normalizedLabel = Reference.normalizeLabel(input.substring(linkStart+1, linkEnd-1));
317 if(normalizedLabel == null)
318 normalizedLabel = Reference.normalizeLabel(label);
319 Reference reference = referenceMap.get(normalizedLabel);
320 if(reference == null) {
325 title = reference.title;
328 Node newLast = opener.inlText.prev;
329 Node parent = opener.inlText.parent;
331 processEmphasis(opener.previous);
332 if(opener.delimChar == '[') {
333 newNode = new LinkNode(label, url, title);
336 newNode = new ImageNode(label, url, title);
338 opener.inlText.prev = null;
339 newNode.firstChild = opener.inlText;
340 newNode.lastChild = parent.lastChild;
341 for(Node node = newNode.firstChild;node != null;node = node.next)
342 node.parent = newNode;
343 opener.inlText.remove();
345 parent.lastChild = newLast;
349 parent.firstChild = null;
351 lastDelim = opener.previous;
352 if(lastDelim != null)
353 lastDelim.next = null;
355 if(opener.delimChar == '[')
356 for(Delimiter cur = lastDelim;cur != null && cur.active; cur = cur.previous)
357 if(cur.delimChar == '[')
363 private void remove(Delimiter delimiter) {
364 if(delimiter.previous != null)
365 delimiter.previous.next = delimiter.next;
366 if(delimiter.next != null)
367 delimiter.next.previous = delimiter.previous;
369 lastDelim = delimiter.previous;
372 private Node handleNewline() {
375 while(peekChar() == ' ')
377 if(nlPos > 1 && input.charAt(nlPos-1) == ' ' && input.charAt(nlPos-2) == ' ')
378 return new HardLineBreakNode();
380 return new TextNode(new StringBuilder("\n"));
383 private Node handlePointyBrace() {
387 int p = Scanner.scanUri(input, pos);
389 Node result = new AutolinkNode(new StringBuilder(input.substring(pos, p-1)), false);
394 p = Scanner.scanEmail(input, pos);
396 Node result = new AutolinkNode(new StringBuilder(input.substring(pos, p-1)), true);
402 p = Scanner.scanHtmlTag(input, pos);
404 Node result = new HtmlTagNode(new StringBuilder(input.substring(pos-1, p)));
408 return new TextNode(new StringBuilder("<"));
411 private Node handleBackslash() {
416 StringBuilder b = new StringBuilder(2);
418 return new TextNode(b);
420 if(getCharType(c)==2) {
421 StringBuilder b = new StringBuilder(1);
423 return new TextNode(b);
426 return new HardLineBreakNode();
428 StringBuilder b = new StringBuilder(2);
431 return new TextNode(b);
435 private Node handleBackticks() {
437 while(peekChar() == '`')
439 int tickCount = pos-startPos;
443 while((c = peekChar()) != '`' && c != 0)
446 pos = startPos+tickCount;
447 StringBuilder b = new StringBuilder(tickCount);
448 for(int i=0;i<tickCount;++i)
450 return new TextNode(b);
453 while(peekChar() == '`') {
457 } while(endTickCount != tickCount);
458 return new CodeNode(normalizeWhitespace(startPos+tickCount, pos-tickCount));
461 private StringBuilder normalizeWhitespace(int begin, int end) {
462 while(begin < end && isWhitespace(input.charAt(begin)))
464 while(begin < end && isWhitespace(input.charAt(end-1)))
466 StringBuilder b = new StringBuilder(end-begin);
467 boolean lastCharWasWhitespace = false;
469 char c = input.charAt(begin++);
470 if(isWhitespace(c)) {
471 if(!lastCharWasWhitespace) {
472 lastCharWasWhitespace = true;
477 lastCharWasWhitespace = false;
484 private static boolean isWhitespace(char c) {
485 return c == ' ' || c == '\n';
488 private Node handleDelim(char c) {
496 beforeChar = input.charAt(pos-1);
499 while(pos < input.length() && input.charAt(pos) == c)
502 if(pos == input.length())
505 afterChar = input.charAt(pos);
507 int beforeCharType = getCharType(beforeChar);
508 int afterCharType = getCharType(afterChar);
509 boolean leftFlanking = afterCharType != CHAR_TYPE_WHITESPACE &&
510 (afterCharType != CHAR_TYPE_PUNCTUATION || beforeCharType != CHAR_TYPE_OTHER);
511 boolean rightFlanking = beforeCharType != CHAR_TYPE_WHITESPACE &&
512 (beforeCharType != CHAR_TYPE_PUNCTUATION || afterCharType != CHAR_TYPE_OTHER);
517 canOpen = leftFlanking && (rightFlanking ? beforeCharType == CHAR_TYPE_PUNCTUATION : true);
518 canClose = rightFlanking && (leftFlanking ? afterCharType == CHAR_TYPE_PUNCTUATION : true);
521 canOpen = leftFlanking;
522 canClose = rightFlanking;
525 Node inlText = new TextNode(new StringBuilder(input.subSequence(startPos, pos)));
526 lastDelim = new Delimiter(lastDelim, inlText, pos, c, canOpen, canClose);
530 public static final int CHAR_TYPE_WHITESPACE = 1;
531 public static final int CHAR_TYPE_PUNCTUATION = 2;
532 public static final int CHAR_TYPE_OTHER = 0;
534 public static int getCharType(char c) {
538 return CHAR_TYPE_WHITESPACE;
571 return CHAR_TYPE_PUNCTUATION;
573 return CHAR_TYPE_OTHER;
577 private char peekChar() {
578 if(pos < input.length())
579 return input.charAt(pos);
584 private boolean isEof() {
585 return pos >= input.length();
588 static final boolean[] SPECIAL_CHARS = new boolean[128];
589 static final String SPECIALS_STRING = "\n\\`&_*[]<!";
591 for(int i=0;i<SPECIALS_STRING.length();++i)
592 SPECIAL_CHARS[(int)SPECIALS_STRING.charAt(i)] = true;
595 static private boolean isSpecialChar(char c) {
596 return c >= 0 && c < 128 && SPECIAL_CHARS[(int)c];
599 private void addChild(Node parent, Node child) {
600 child.parent = parent;
601 if(parent.lastChild == null)
602 parent.firstChild = child;
604 Node oldLast = parent.lastChild;
605 oldLast.next = child;
606 child.prev = oldLast;
608 parent.lastChild = child;
611 private void removeDelim(Delimiter delim) {
612 Delimiter previous = delim.previous;
613 Delimiter next = delim.next;
614 if(delim == lastDelim)
615 lastDelim = previous;
617 next.previous = previous;
619 previous.next = next;