]> gerrit.simantics Code Review - simantics/platform.git/blob - bundles/org.simantics.scl.compiler/src/org/simantics/scl/compiler/markdown/inlines/Subject.java
migrated to svn revision 33108
[simantics/platform.git] / bundles / org.simantics.scl.compiler / src / org / simantics / scl / compiler / markdown / inlines / Subject.java
1 package org.simantics.scl.compiler.markdown.inlines;
2
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;
14
15 import gnu.trove.map.hash.THashMap;
16
17 public class Subject {
18     THashMap<String, Reference> referenceMap;
19     StringBuilder input;
20     int pos;
21     Delimiter lastDelim;
22     
23     public Subject(THashMap<String, Reference> referenceMap, StringBuilder input) {
24         this.referenceMap = referenceMap;
25         this.input = input;
26         this.pos = 0;
27     }
28
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;
34     }
35
36     private void processEmphasis(Delimiter begin) {
37         if(lastDelim == begin)
38             return;
39         
40         // Find first delimiter
41         Delimiter closer = lastDelim;
42         while(closer.previous != begin)
43             closer = closer.previous;
44         
45         // Loop all delimeters
46         closer = closer.next;
47         while(closer != null) {
48             if(closer.canClose) {
49                 // Find opener
50                 for(Delimiter opener = closer.previous; opener != begin; opener = opener.previous) {
51                     if(opener.canOpen && opener.delimChar == closer.delimChar) {
52                         closer = insertEmph(opener, closer);
53                         break;
54                     }
55                 }
56             }
57             closer = closer.next;
58         }
59     }
60
61     private Delimiter insertEmph(Delimiter opener, Delimiter closer) {
62         // Remove all delimiters between opener and closer
63         opener.next = closer;
64         closer.previous = opener;
65         
66         // Length
67         int openerLength = opener.inlText.stringContent.length();
68         int closerLength = closer.inlText.stringContent.length();
69         int commonLength = Math.min(openerLength, closerLength);
70         if(commonLength > 2)
71             commonLength = 2 - (closerLength % 2);
72         
73         // Add emph
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)
85             node.parent = emph;
86         
87         // Remove
88         if(openerLength == commonLength) {
89             removeDelim(opener);
90             opener.inlText.remove();
91         }
92         else
93             opener.inlText.stringContent.delete(openerLength-commonLength, openerLength);
94         if(closerLength == commonLength) {
95             removeDelim(closer);
96             closer.inlText.remove();
97             return closer;
98         }
99         else {
100             closer.inlText.stringContent.delete(closerLength-commonLength, closerLength);
101             if(closer.previous != null)
102                 return closer.previous;
103             else
104                 return closer;
105         }
106     }
107
108     private boolean parseInline(Node parent) {
109         Node newInl = null;
110         char c = peekChar();
111         if(c == 0)
112             return false;
113         switch(c) {
114         case '\\':
115             newInl = handleBackslash();
116             break;
117             
118         case '`':
119             newInl = handleBackticks();
120             break;
121             
122         case '<':
123             newInl = handlePointyBrace();
124             break;
125             
126         case '\n':
127             newInl = handleNewline();
128             break;
129             
130         case '[':
131             newInl = new TextNode(new StringBuilder("["));
132             lastDelim = new Delimiter(lastDelim, newInl, pos, '[', false, false);
133             ++pos;
134             break;
135             
136         case '!':
137             ++pos;
138             if(peekChar() == '[') {
139                 newInl = new TextNode(new StringBuilder("!["));
140                 lastDelim = new Delimiter(lastDelim, newInl, pos-1, '!', false, false);
141                 ++pos;
142             }
143             else
144                 newInl = new TextNode(new StringBuilder("!"));
145             break;
146             
147         case ']':
148             newInl = handleCloseBracket();
149             if(newInl == null)
150                 newInl = new TextNode(new StringBuilder("]"));
151             break;
152             
153         case '&':
154             newInl = handleEntity();
155             if(newInl == null)
156                 newInl = new TextNode(new StringBuilder("&"));
157             break;
158             
159         case '*':
160         case '_':
161         case '\'':
162         case '"':
163             newInl = handleDelim(c);
164             break;
165         default: {
166             int startPos = pos;
167             ++pos;
168             while(pos < input.length() && !isSpecialChar(input.charAt(pos)))
169                 ++pos;
170             char nc = peekChar();
171             int tEnd = pos;
172             if(nc == '\n' || nc == 0) {
173                 while(tEnd > startPos && input.charAt(tEnd-1) == ' ')
174                     --tEnd;
175             }
176             newInl = new TextNode(new StringBuilder(input.subSequence(startPos, tEnd)));
177         }
178         }
179         if(newInl != null)
180             addChild(parent, newInl);
181         return true;
182     }
183     
184     private Node handleEntity() {
185         ++pos;
186         if(peekChar() == '#') {
187             int p = pos+1;
188             if(p == input.length())
189                 return null;
190             char c = input.charAt(p);
191             if(c == 'x' || c == 'X') {
192                 int code = 0;
193                 for(int i=0;i<8;++i) {
194                     ++p;
195                     c = input.charAt(p);
196                     if(c == ';') {
197                         if(p == pos+2)
198                             return null;
199                         pos = p+1;
200                         if(!Character.isValidCodePoint(code))
201                             code = 0xFFFD;
202                         return new TextNode(new StringBuilder(new String(new int[] {code}, 0, 1))); 
203                     }
204                     else if(c >= '0' && c <= '9') {
205                         code *= 16;
206                         code += (int)(c - '0');
207                     }
208                     else if(c >= 'a' && c <= 'f') {
209                         code *= 16;
210                         code += (int)(c - 'a') + 10;
211                     }
212                     else if(c >= 'A' && c <= 'F') {
213                         code *= 16;
214                         code += (int)(c - 'A') + 10;
215                     }
216                 }
217                 return null;
218             }
219             else if(c >= '0' && c <= '9'){
220                 int code = (int)(c - '0');
221                 for(int i=0;i<8;++i) {
222                     ++p;
223                     c = input.charAt(p);
224                     if(c == ';') {
225                         if(p == pos+1)
226                             return null;
227                         pos = p+1;
228                         if(!Character.isValidCodePoint(code))
229                             code = 0xFFFD;
230                         return new TextNode(new StringBuilder(new String(new int[] {code}, 0, 1))); 
231                     }
232                     else if(c >= '0' && c <= '9') {
233                         code *= 10;
234                         code += (int)(c - '0');
235                     }
236                 }
237                 return null;
238             }
239             else
240                 return null;
241         }
242         else {
243             int maxPos = Math.min(input.length(), pos+Entities.MAX_ENTITY_LENGTH+1);
244             int p = pos;
245             while(p < maxPos) {
246                 char c = input.charAt(p++);
247                 if(c == ';') {
248                     String entity = input.substring(pos, p-1);
249                     String character = Entities.ENTITY_MAP.get(entity);
250                     if(character == null)
251                         return null;
252                     else {
253                         pos = p;
254                         return new TextNode(new StringBuilder(character));
255                     }
256                 }
257             }
258             return null;
259         }
260     }
261
262     private Node handleCloseBracket() {
263         ++pos;
264         Delimiter opener = lastDelim;
265         while(opener != null) {
266             if(opener.delimChar == '[' || opener.delimChar == '!')
267                 break;
268             opener = opener.previous;
269         }
270         if(opener == null)
271             return null;
272         remove(opener);
273         if(!opener.active)
274             return null;
275         
276         String label = input.substring(opener.position+(opener.delimChar == '[' ? 1 : 2), pos-1);
277         
278         String url, title;
279         
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);
285             if(titleStart == -1)
286                 return null;
287             int titleEnd = titleStart == urlEnd ? titleStart : Scanner.scanLinkTitle(input, titleStart);
288             if(titleEnd == -1)
289                 return null;
290             int endAll = Scanner.scanWhitespace(input, titleEnd);
291             if(endAll == -1 || input.charAt(endAll) != ')')
292                 return null;
293             pos = endAll + 1;
294             
295             if(input.charAt(urlStart) == '<')
296                 url = input.substring(urlStart+1, urlEnd-1);
297             else
298                 url = input.substring(urlStart, urlEnd);
299             url = Reference.cleanUrl(url);
300             title = titleStart==titleEnd ? "" : Reference.cleanTitle(input.substring(titleStart+1, titleEnd-1));
301         }
302         else {
303             int originalPos = pos;
304             String normalizedLabel = null;
305             tryLink: {
306                 int linkStart = Scanner.scanWhitespace(input, pos);
307                 if(linkStart == -1 || input.charAt(linkStart) != '[')
308                     break tryLink;
309                 int linkEnd = Scanner.scanLinkLabel(input, linkStart);
310                 if(linkEnd == -1)
311                     break tryLink;
312                 if(linkStart+2 < linkEnd)
313                     normalizedLabel = Reference.normalizeLabel(input.substring(linkStart+1, linkEnd-1));
314                 pos = linkEnd;
315             }
316             
317             if(normalizedLabel == null)
318                 normalizedLabel = Reference.normalizeLabel(label);
319             Reference reference = referenceMap.get(normalizedLabel);
320             if(reference == null) {
321                 pos = originalPos;
322                 return null;
323             }
324             url = reference.url;
325             title = reference.title;
326         }
327         
328         Node newLast = opener.inlText.prev;
329         Node parent = opener.inlText.parent;
330         Node newNode;
331         processEmphasis(opener.previous);
332         if(opener.delimChar == '[') {
333             newNode = new LinkNode(label, url, title);
334         }
335         else {
336             newNode = new ImageNode(label, url, title);
337         }
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();
344
345         parent.lastChild = newLast;
346         if(newLast != null)
347             newLast.next = null;
348         else
349             parent.firstChild = null;
350
351         lastDelim = opener.previous;
352         if(lastDelim != null)
353             lastDelim.next = null;
354         
355         if(opener.delimChar == '[')
356             for(Delimiter cur = lastDelim;cur != null && cur.active; cur = cur.previous)
357                 if(cur.delimChar == '[')
358                     cur.active = false;
359
360         return newNode;
361     }
362
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;
368         else
369             lastDelim = delimiter.previous;
370     }
371
372     private Node handleNewline() {
373         int nlPos = pos;
374         ++pos;
375         while(peekChar() == ' ')
376             ++pos;
377         if(nlPos > 1 && input.charAt(nlPos-1) == ' ' && input.charAt(nlPos-2) == ' ')
378             return new HardLineBreakNode();
379         else
380             return new TextNode(new StringBuilder("\n"));
381     }
382
383     private Node handlePointyBrace() {
384         ++pos;
385         
386         // URL
387         int p = Scanner.scanUri(input, pos);
388         if(p >= 0) {
389             Node result = new AutolinkNode(new StringBuilder(input.substring(pos, p-1)), false);
390             pos = p;
391             return result;
392         }
393         
394         p = Scanner.scanEmail(input, pos);
395         if(p >= 0) {
396             Node result = new AutolinkNode(new StringBuilder(input.substring(pos, p-1)), true);
397             pos = p;
398             return result;
399         }
400                 
401         // HTML tag
402         p = Scanner.scanHtmlTag(input, pos);
403         if(p >= 0) {
404             Node result = new HtmlTagNode(new StringBuilder(input.substring(pos-1, p)));
405             pos = p;
406             return result;
407         }
408         return new TextNode(new StringBuilder("<"));
409     }
410
411     private Node handleBackslash() {
412         ++pos;
413         char c = peekChar();
414         ++pos;
415         if(c == 0) {
416             StringBuilder b = new StringBuilder(2);
417             b.append('\\');
418             return new TextNode(b); 
419         }
420         if(getCharType(c)==2) {
421             StringBuilder b = new StringBuilder(1);
422             b.append(c);
423             return new TextNode(b);
424         }
425         else if(c == '\n')
426             return new HardLineBreakNode();
427         else {
428             StringBuilder b = new StringBuilder(2);
429             b.append('\\');
430             b.append(c);
431             return new TextNode(b); 
432         }
433     }
434     
435     private Node handleBackticks() {
436         int startPos = pos;
437         while(peekChar() == '`')
438             ++pos;
439         int tickCount = pos-startPos;
440         char c;
441         int endTickCount;
442         do {
443             while((c = peekChar()) != '`' && c != 0)
444                 ++pos;
445             if(c == 0) {
446                 pos = startPos+tickCount;
447                 StringBuilder b = new StringBuilder(tickCount);
448                 for(int i=0;i<tickCount;++i)
449                     b.append('`');
450                 return new TextNode(b);
451             }
452             endTickCount = 0;
453             while(peekChar() == '`') {
454                 ++pos;
455                 ++endTickCount;
456             }
457         } while(endTickCount != tickCount);
458         return new CodeNode(normalizeWhitespace(startPos+tickCount, pos-tickCount));
459     }
460     
461     private StringBuilder normalizeWhitespace(int begin, int end) {
462         while(begin < end && isWhitespace(input.charAt(begin)))
463             ++begin;
464         while(begin < end && isWhitespace(input.charAt(end-1)))
465             --end;
466         StringBuilder b = new StringBuilder(end-begin);
467         boolean lastCharWasWhitespace = false;
468         while(begin < end) {
469             char c = input.charAt(begin++);
470             if(isWhitespace(c)) {
471                 if(!lastCharWasWhitespace) {
472                     lastCharWasWhitespace = true;
473                     b.append(' ');
474                 }
475             }
476             else {
477                 lastCharWasWhitespace = false;
478                 b.append(c);
479             }
480         }
481         return b;
482     }
483     
484     private static boolean isWhitespace(char c) {
485         return c == ' ' || c == '\n';
486     }
487
488     private Node handleDelim(char c) {
489         char beforeChar;
490         char afterChar;
491         int startPos = pos;
492         
493         if(pos == 0)
494             beforeChar = '\n';
495         else
496             beforeChar = input.charAt(pos-1);
497         
498         ++pos;
499         while(pos < input.length() && input.charAt(pos) == c)
500             ++pos;
501         
502         if(pos == input.length())
503             afterChar = '\n';
504         else
505             afterChar = input.charAt(pos);
506         
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);
513         
514         boolean canOpen;
515         boolean canClose;
516         if(c == '_') {
517             canOpen = leftFlanking && (rightFlanking ? beforeCharType == CHAR_TYPE_PUNCTUATION : true);
518             canClose = rightFlanking && (leftFlanking ? afterCharType == CHAR_TYPE_PUNCTUATION : true);
519         }
520         else {
521             canOpen = leftFlanking;
522             canClose = rightFlanking;
523         }
524         
525         Node inlText = new TextNode(new StringBuilder(input.subSequence(startPos, pos)));
526         lastDelim = new Delimiter(lastDelim, inlText, pos, c, canOpen, canClose);
527         return inlText;
528     }
529     
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;
533     
534     public static int getCharType(char c) {
535         switch(c) {
536         case ' ':
537         case '\n':
538             return CHAR_TYPE_WHITESPACE;
539         case '!':
540         case '"':
541         case '#':
542         case '$':
543         case '%':
544         case '&':
545         case '\'':
546         case '(':
547         case ')':
548         case '*':
549         case '+':
550         case ',':
551         case '-':
552         case '.':
553         case '/':
554         case ':':
555         case ';':
556         case '<':
557         case '=':
558         case '>':
559         case '?':
560         case '@':
561         case '[':
562         case '\\':
563         case ']':
564         case '^':
565         case '_':
566         case '`':
567         case '{':
568         case '|':
569         case '}':
570         case '~':
571             return CHAR_TYPE_PUNCTUATION;
572         default:
573             return CHAR_TYPE_OTHER;
574         }
575     }
576
577     private char peekChar() {
578         if(pos < input.length())
579             return input.charAt(pos);
580         else
581             return 0;
582     }
583
584     private boolean isEof() {
585         return pos >= input.length();
586     }
587     
588     static final boolean[] SPECIAL_CHARS = new boolean[128];
589     static final String SPECIALS_STRING = "\n\\`&_*[]<!";
590     static {
591         for(int i=0;i<SPECIALS_STRING.length();++i)
592             SPECIAL_CHARS[(int)SPECIALS_STRING.charAt(i)] = true;
593     }
594     
595     static private boolean isSpecialChar(char c) {
596         return c >= 0 && c < 128 && SPECIAL_CHARS[(int)c];
597     }
598     
599     private void addChild(Node parent, Node child) {
600         child.parent = parent;
601         if(parent.lastChild == null)
602             parent.firstChild = child; 
603         else {
604             Node oldLast = parent.lastChild;
605             oldLast.next = child;
606             child.prev = oldLast;
607         }
608         parent.lastChild = child;
609     }
610     
611     private void removeDelim(Delimiter delim) {
612         Delimiter previous = delim.previous;
613         Delimiter next = delim.next;
614         if(delim == lastDelim)
615             lastDelim = previous;
616         else
617             next.previous = previous;
618         if(previous != null)
619             previous.next = next;
620     }
621 }