inline.go (view raw)
1//
2// Black Friday Markdown Processor
3// Originally based on http://github.com/tanoku/upskirt
4// by Russ Ross <russ@russross.com>
5//
6
7//
8// Functions to parse inline elements.
9//
10
11package blackfriday
12
13import (
14 "bytes"
15)
16
17// Functions to parse text within a block
18// Each function returns the number of chars taken care of
19// data is the complete block being rendered
20// offset is the number of valid chars before the current cursor
21
22func parseInline(out *bytes.Buffer, rndr *render, data []byte) {
23 // this is called recursively: enforce a maximum depth
24 if rndr.nesting >= rndr.maxNesting {
25 return
26 }
27 rndr.nesting++
28
29 i, end := 0, 0
30 for i < len(data) {
31 // copy inactive chars into the output
32 for end < len(data) && rndr.inline[data[end]] == nil {
33 end++
34 }
35
36 if rndr.mk.NormalText != nil {
37 rndr.mk.NormalText(out, data[i:end], rndr.mk.Opaque)
38 } else {
39 out.Write(data[i:end])
40 }
41
42 if end >= len(data) {
43 break
44 }
45 i = end
46
47 // call the trigger
48 parser := rndr.inline[data[end]]
49 if consumed := parser(out, rndr, data, i); consumed == 0 {
50 // no action from the callback; buffer the byte for later
51 end = i + 1
52 } else {
53 // skip past whatever the callback used
54 i += consumed
55 end = i
56 }
57 }
58
59 rndr.nesting--
60}
61
62// single and double emphasis parsing
63func inlineEmphasis(out *bytes.Buffer, rndr *render, data []byte, offset int) int {
64 data = data[offset:]
65 c := data[0]
66 ret := 0
67
68 if len(data) > 2 && data[1] != c {
69 // whitespace cannot follow an opening emphasis;
70 // strikethrough only takes two characters '~~'
71 if c == '~' || isspace(data[1]) {
72 return 0
73 }
74 if ret = inlineHelperEmph1(out, rndr, data[1:], c); ret == 0 {
75 return 0
76 }
77
78 return ret + 1
79 }
80
81 if len(data) > 3 && data[1] == c && data[2] != c {
82 if isspace(data[2]) {
83 return 0
84 }
85 if ret = inlineHelperEmph2(out, rndr, data[2:], c); ret == 0 {
86 return 0
87 }
88
89 return ret + 2
90 }
91
92 if len(data) > 4 && data[1] == c && data[2] == c && data[3] != c {
93 if c == '~' || isspace(data[3]) {
94 return 0
95 }
96 if ret = inlineHelperEmph3(out, rndr, data, 3, c); ret == 0 {
97 return 0
98 }
99
100 return ret + 3
101 }
102
103 return 0
104}
105
106func inlineCodeSpan(out *bytes.Buffer, rndr *render, data []byte, offset int) int {
107 data = data[offset:]
108
109 nb := 0
110
111 // count the number of backticks in the delimiter
112 for nb < len(data) && data[nb] == '`' {
113 nb++
114 }
115
116 // find the next delimiter
117 i, end := 0, 0
118 for end = nb; end < len(data) && i < nb; end++ {
119 if data[end] == '`' {
120 i++
121 } else {
122 i = 0
123 }
124 }
125
126 if i < nb && end >= len(data) {
127 out.WriteByte('`')
128 return 0 // no matching delimiter
129 }
130
131 // trim outside whitespace
132 f_begin := nb
133 for f_begin < end && (data[f_begin] == ' ' || data[f_begin] == '\t') {
134 f_begin++
135 }
136
137 f_end := end - nb
138 for f_end > nb && (data[f_end-1] == ' ' || data[f_end-1] == '\t') {
139 f_end--
140 }
141
142 // real code span
143 if rndr.mk.CodeSpan == nil {
144 return 0
145 }
146 if f_begin < f_end {
147 if rndr.mk.CodeSpan(out, data[f_begin:f_end], rndr.mk.Opaque) == 0 {
148 end = 0
149 }
150 } else {
151 if rndr.mk.CodeSpan(out, nil, rndr.mk.Opaque) == 0 {
152 end = 0
153 }
154 }
155
156 return end
157
158}
159
160// newline preceded by two spaces becomes <br>
161// newline without two spaces works when EXTENSION_HARD_LINE_BREAK is enabled
162func inlineLineBreak(out *bytes.Buffer, rndr *render, data []byte, offset int) int {
163 if rndr.flags&EXTENSION_HARD_LINE_BREAK == 0 &&
164 (offset < 2 || data[offset-1] != ' ' || data[offset-2] != ' ') {
165 return 0
166 }
167
168 // remove trailing spaces from out and render
169 outBytes := out.Bytes()
170 end := len(outBytes)
171 for end > 0 && outBytes[end-1] == ' ' {
172 end--
173 }
174 out.Truncate(end)
175
176 if rndr.mk.LineBreak == nil {
177 return 0
178 }
179 if rndr.mk.LineBreak(out, rndr.mk.Opaque) > 0 {
180 return 1
181 } else {
182 return 0
183 }
184
185 return 0
186}
187
188// '[': parse a link or an image
189func inlineLink(out *bytes.Buffer, rndr *render, data []byte, offset int) int {
190 // no links allowed inside other links
191 if rndr.insideLink {
192 return 0
193 }
194
195 isImg := offset > 0 && data[offset-1] == '!'
196
197 data = data[offset:]
198
199 i := 1
200 var title, link []byte
201 text_has_nl := false
202
203 // check whether the correct renderer exists
204 if (isImg && rndr.mk.Image == nil) || (!isImg && rndr.mk.Link == nil) {
205 return 0
206 }
207
208 // look for the matching closing bracket
209 for level := 1; level > 0 && i < len(data); i++ {
210 switch {
211 case data[i] == '\n':
212 text_has_nl = true
213
214 case data[i-1] == '\\':
215 continue
216
217 case data[i] == '[':
218 level++
219
220 case data[i] == ']':
221 level--
222 if level <= 0 {
223 i-- // compensate for extra i++ in for loop
224 }
225 }
226 }
227
228 if i >= len(data) {
229 return 0
230 }
231
232 txt_e := i
233 i++
234
235 // skip any amount of whitespace or newline
236 // (this is much more lax than original markdown syntax)
237 for i < len(data) && isspace(data[i]) {
238 i++
239 }
240
241 // inline style link
242 switch {
243 case i < len(data) && data[i] == '(':
244 // skip initial whitespace
245 i++
246
247 for i < len(data) && isspace(data[i]) {
248 i++
249 }
250
251 link_b := i
252
253 // look for link end: ' " )
254 for i < len(data) {
255 if data[i] == '\\' {
256 i += 2
257 } else {
258 if data[i] == ')' || data[i] == '\'' || data[i] == '"' {
259 break
260 }
261 i++
262 }
263 }
264
265 if i >= len(data) {
266 return 0
267 }
268 link_e := i
269
270 // look for title end if present
271 title_b, title_e := 0, 0
272 if data[i] == '\'' || data[i] == '"' {
273 i++
274 title_b = i
275
276 for i < len(data) {
277 if data[i] == '\\' {
278 i += 2
279 } else {
280 if data[i] == ')' {
281 break
282 }
283 i++
284 }
285 }
286
287 if i >= len(data) {
288 return 0
289 }
290
291 // skip whitespace after title
292 title_e = i - 1
293 for title_e > title_b && isspace(data[title_e]) {
294 title_e--
295 }
296
297 // check for closing quote presence
298 if data[title_e] != '\'' && data[title_e] != '"' {
299 title_b, title_e = 0, 0
300 link_e = i
301 }
302 }
303
304 // remove whitespace at the end of the link
305 for link_e > link_b && isspace(data[link_e-1]) {
306 link_e--
307 }
308
309 // remove optional angle brackets around the link
310 if data[link_b] == '<' {
311 link_b++
312 }
313 if data[link_e-1] == '>' {
314 link_e--
315 }
316
317 // build escaped link and title
318 if link_e > link_b {
319 link = data[link_b:link_e]
320 }
321
322 if title_e > title_b {
323 title = data[title_b:title_e]
324 }
325
326 i++
327
328 // reference style link
329 case i < len(data) && data[i] == '[':
330 var id []byte
331
332 // look for the id
333 i++
334 link_b := i
335 for i < len(data) && data[i] != ']' {
336 i++
337 }
338 if i >= len(data) {
339 return 0
340 }
341 link_e := i
342
343 // find the reference
344 if link_b == link_e {
345 if text_has_nl {
346 var b bytes.Buffer
347
348 for j := 1; j < txt_e; j++ {
349 switch {
350 case data[j] != '\n':
351 b.WriteByte(data[j])
352 case data[j-1] != ' ':
353 b.WriteByte(' ')
354 }
355 }
356
357 id = b.Bytes()
358 } else {
359 id = data[1:txt_e]
360 }
361 } else {
362 id = data[link_b:link_e]
363 }
364
365 // find the reference with matching id (ids are case-insensitive)
366 key := string(bytes.ToLower(id))
367 lr, ok := rndr.refs[key]
368 if !ok {
369 return 0
370 }
371
372 // keep link and title from reference
373 link = lr.link
374 title = lr.title
375 i++
376
377 // shortcut reference style link
378 default:
379 var id []byte
380
381 // craft the id
382 if text_has_nl {
383 var b bytes.Buffer
384
385 for j := 1; j < txt_e; j++ {
386 switch {
387 case data[j] != '\n':
388 b.WriteByte(data[j])
389 case data[j-1] != ' ':
390 b.WriteByte(' ')
391 }
392 }
393
394 id = b.Bytes()
395 } else {
396 id = data[1:txt_e]
397 }
398
399 // find the reference with matching id
400 key := string(bytes.ToLower(id))
401 lr, ok := rndr.refs[key]
402 if !ok {
403 return 0
404 }
405
406 // keep link and title from reference
407 link = lr.link
408 title = lr.title
409
410 // rewind the whitespace
411 i = txt_e + 1
412 }
413
414 // build content: img alt is escaped, link content is parsed
415 var content bytes.Buffer
416 if txt_e > 1 {
417 if isImg {
418 content.Write(data[1:txt_e])
419 } else {
420 // links cannot contain other links, so turn off link parsing temporarily
421 insideLink := rndr.insideLink
422 rndr.insideLink = true
423 parseInline(&content, rndr, data[1:txt_e])
424 rndr.insideLink = insideLink
425 }
426 }
427
428 var u_link []byte
429 if len(link) > 0 {
430 var u_link_buf bytes.Buffer
431 unescapeText(&u_link_buf, link)
432 u_link = u_link_buf.Bytes()
433 }
434
435 // call the relevant rendering function
436 ret := 0
437 if isImg {
438 outSize := out.Len()
439 outBytes := out.Bytes()
440 if outSize > 0 && outBytes[outSize-1] == '!' {
441 out.Truncate(outSize - 1)
442 }
443
444 ret = rndr.mk.Image(out, u_link, title, content.Bytes(), rndr.mk.Opaque)
445 } else {
446 ret = rndr.mk.Link(out, u_link, title, content.Bytes(), rndr.mk.Opaque)
447 }
448
449 if ret > 0 {
450 return i
451 }
452 return 0
453}
454
455// '<' when tags or autolinks are allowed
456func inlineLAngle(out *bytes.Buffer, rndr *render, data []byte, offset int) int {
457 data = data[offset:]
458 altype := LINK_TYPE_NOT_AUTOLINK
459 end := tagLength(data, &altype)
460 ret := 0
461
462 if end > 2 {
463 switch {
464 case rndr.mk.AutoLink != nil && altype != LINK_TYPE_NOT_AUTOLINK:
465 var u_link bytes.Buffer
466 unescapeText(&u_link, data[1:end+1-2])
467 ret = rndr.mk.AutoLink(out, u_link.Bytes(), altype, rndr.mk.Opaque)
468 case rndr.mk.RawHtmlTag != nil:
469 ret = rndr.mk.RawHtmlTag(out, data[:end], rndr.mk.Opaque)
470 }
471 }
472
473 if ret == 0 {
474 return 0
475 }
476 return end
477}
478
479// '\\' backslash escape
480var escapeChars = []byte("\\`*_{}[]()#+-.!:|&<>")
481
482func inlineEscape(out *bytes.Buffer, rndr *render, data []byte, offset int) int {
483 data = data[offset:]
484
485 if len(data) > 1 {
486 if bytes.IndexByte(escapeChars, data[1]) < 0 {
487 return 0
488 }
489
490 if rndr.mk.NormalText != nil {
491 rndr.mk.NormalText(out, data[1:2], rndr.mk.Opaque)
492 } else {
493 out.WriteByte(data[1])
494 }
495 }
496
497 return 2
498}
499
500func unescapeText(ob *bytes.Buffer, src []byte) {
501 i := 0
502 for i < len(src) {
503 org := i
504 for i < len(src) && src[i] != '\\' {
505 i++
506 }
507
508 if i > org {
509 ob.Write(src[org:i])
510 }
511
512 if i+1 >= len(src) {
513 break
514 }
515
516 ob.WriteByte(src[i+1])
517 i += 2
518 }
519}
520
521// '&' escaped when it doesn't belong to an entity
522// valid entities are assumed to be anything matching &#?[A-Za-z0-9]+;
523func inlineEntity(out *bytes.Buffer, rndr *render, data []byte, offset int) int {
524 data = data[offset:]
525
526 end := 1
527
528 if end < len(data) && data[end] == '#' {
529 end++
530 }
531
532 for end < len(data) && isalnum(data[end]) {
533 end++
534 }
535
536 if end < len(data) && data[end] == ';' {
537 end++ // real entity
538 } else {
539 return 0 // lone '&'
540 }
541
542 if rndr.mk.Entity != nil {
543 rndr.mk.Entity(out, data[:end], rndr.mk.Opaque)
544 } else {
545 out.Write(data[:end])
546 }
547
548 return end
549}
550
551func inlineAutoLink(out *bytes.Buffer, rndr *render, data []byte, offset int) int {
552 // quick check to rule out most false hits on ':'
553 if rndr.insideLink || len(data) < offset+3 || data[offset+1] != '/' || data[offset+2] != '/' {
554 return 0
555 }
556
557 // scan backward for a word boundary
558 rewind := 0
559 for offset-rewind > 0 && rewind <= 7 && !isspace(data[offset-rewind-1]) && !isspace(data[offset-rewind-1]) {
560 rewind++
561 }
562 if rewind > 6 { // longest supported protocol is "mailto" which has 6 letters
563 return 0
564 }
565
566 orig_data := data
567 data = data[offset-rewind:]
568
569 if !isSafeLink(data) {
570 return 0
571 }
572
573 link_end := 0
574 for link_end < len(data) && !isspace(data[link_end]) {
575 link_end++
576 }
577
578 // Skip punctuation at the end of the link
579 if (data[link_end-1] == '.' || data[link_end-1] == ',' || data[link_end-1] == ';') && data[link_end-2] != '\\' {
580 link_end--
581 }
582
583 // See if the link finishes with a punctuation sign that can be closed.
584 var copen byte
585 switch data[link_end-1] {
586 case '"':
587 copen = '"'
588 case '\'':
589 copen = '\''
590 case ')':
591 copen = '('
592 case ']':
593 copen = '['
594 case '}':
595 copen = '{'
596 default:
597 copen = 0
598 }
599
600 if copen != 0 {
601 buf_end := offset - rewind + link_end - 2
602
603 open_delim := 1
604
605 /* Try to close the final punctuation sign in this same line;
606 * if we managed to close it outside of the URL, that means that it's
607 * not part of the URL. If it closes inside the URL, that means it
608 * is part of the URL.
609 *
610 * Examples:
611 *
612 * foo http://www.pokemon.com/Pikachu_(Electric) bar
613 * => http://www.pokemon.com/Pikachu_(Electric)
614 *
615 * foo (http://www.pokemon.com/Pikachu_(Electric)) bar
616 * => http://www.pokemon.com/Pikachu_(Electric)
617 *
618 * foo http://www.pokemon.com/Pikachu_(Electric)) bar
619 * => http://www.pokemon.com/Pikachu_(Electric))
620 *
621 * (foo http://www.pokemon.com/Pikachu_(Electric)) bar
622 * => foo http://www.pokemon.com/Pikachu_(Electric)
623 */
624
625 for buf_end >= 0 && orig_data[buf_end] != '\n' && open_delim != 0 {
626 if orig_data[buf_end] == data[link_end-1] {
627 open_delim++
628 }
629
630 if orig_data[buf_end] == copen {
631 open_delim--
632 }
633
634 buf_end--
635 }
636
637 if open_delim == 0 {
638 link_end--
639 }
640 }
641
642 // we were triggered on the ':', so we need to rewind the output a bit
643 if out.Len() >= rewind {
644 out.Truncate(len(out.Bytes()) - rewind)
645 }
646
647 if rndr.mk.AutoLink != nil {
648 var u_link bytes.Buffer
649 unescapeText(&u_link, data[:link_end])
650
651 rndr.mk.AutoLink(out, u_link.Bytes(), LINK_TYPE_NORMAL, rndr.mk.Opaque)
652 }
653
654 return link_end - rewind
655}
656
657var validUris = [][]byte{[]byte("http://"), []byte("https://"), []byte("ftp://"), []byte("mailto://")}
658
659func isSafeLink(link []byte) bool {
660 for _, prefix := range validUris {
661 // TODO: handle unicode here
662 // case-insensitive prefix test
663 if len(link) > len(prefix) && bytes.Equal(bytes.ToLower(link[:len(prefix)]), prefix) && isalnum(link[len(prefix)]) {
664 return true
665 }
666 }
667
668 return false
669}
670
671// return the length of the given tag, or 0 is it's not valid
672func tagLength(data []byte, autolink *int) int {
673 var i, j int
674
675 // a valid tag can't be shorter than 3 chars
676 if len(data) < 3 {
677 return 0
678 }
679
680 // begins with a '<' optionally followed by '/', followed by letter or number
681 if data[0] != '<' {
682 return 0
683 }
684 if data[1] == '/' {
685 i = 2
686 } else {
687 i = 1
688 }
689
690 if !isalnum(data[i]) {
691 return 0
692 }
693
694 // scheme test
695 *autolink = LINK_TYPE_NOT_AUTOLINK
696
697 // try to find the beginning of an URI
698 for i < len(data) && (isalnum(data[i]) || data[i] == '.' || data[i] == '+' || data[i] == '-') {
699 i++
700 }
701
702 if i > 1 && data[i] == '@' {
703 if j = isMailtoAutoLink(data[i:]); j != 0 {
704 *autolink = LINK_TYPE_EMAIL
705 return i + j
706 }
707 }
708
709 if i > 2 && data[i] == ':' {
710 *autolink = LINK_TYPE_NORMAL
711 i++
712 }
713
714 // complete autolink test: no whitespace or ' or "
715 switch {
716 case i >= len(data):
717 *autolink = LINK_TYPE_NOT_AUTOLINK
718 case *autolink != 0:
719 j = i
720
721 for i < len(data) {
722 if data[i] == '\\' {
723 i += 2
724 } else {
725 if data[i] == '>' || data[i] == '\'' || data[i] == '"' || isspace(data[i]) {
726 break
727 } else {
728 i++
729 }
730 }
731
732 }
733
734 if i >= len(data) {
735 return 0
736 }
737 if i > j && data[i] == '>' {
738 return i + 1
739 }
740
741 // one of the forbidden chars has been found
742 *autolink = LINK_TYPE_NOT_AUTOLINK
743 }
744
745 // look for something looking like a tag end
746 for i < len(data) && data[i] != '>' {
747 i++
748 }
749 if i >= len(data) {
750 return 0
751 }
752 return i + 1
753}
754
755// look for the address part of a mail autolink and '>'
756// this is less strict than the original markdown e-mail address matching
757func isMailtoAutoLink(data []byte) int {
758 nb := 0
759
760 // address is assumed to be: [-@._a-zA-Z0-9]+ with exactly one '@'
761 for i := 0; i < len(data); i++ {
762 if isalnum(data[i]) {
763 continue
764 }
765
766 switch data[i] {
767 case '@':
768 nb++
769
770 case '-', '.', '_':
771 break
772
773 case '>':
774 if nb == 1 {
775 return i + 1
776 } else {
777 return 0
778 }
779 default:
780 return 0
781 }
782 }
783
784 return 0
785}
786
787// look for the next emph char, skipping other constructs
788func inlineHelperFindEmphChar(data []byte, c byte) int {
789 i := 1
790
791 for i < len(data) {
792 for i < len(data) && data[i] != c && data[i] != '`' && data[i] != '[' {
793 i++
794 }
795 if i >= len(data) {
796 return 0
797 }
798 if data[i] == c {
799 return i
800 }
801
802 // do not count escaped chars
803 if i != 0 && data[i-1] == '\\' {
804 i++
805 continue
806 }
807
808 if data[i] == '`' {
809 // skip a code span
810 tmp_i := 0
811 i++
812 for i < len(data) && data[i] != '`' {
813 if tmp_i == 0 && data[i] == c {
814 tmp_i = i
815 }
816 i++
817 }
818 if i >= len(data) {
819 return tmp_i
820 }
821 i++
822 } else {
823 if data[i] == '[' {
824 // skip a link
825 tmp_i := 0
826 i++
827 for i < len(data) && data[i] != ']' {
828 if tmp_i == 0 && data[i] == c {
829 tmp_i = i
830 }
831 i++
832 }
833 i++
834 for i < len(data) && (data[i] == ' ' || data[i] == '\t' || data[i] == '\n') {
835 i++
836 }
837 if i >= len(data) {
838 return tmp_i
839 }
840 if data[i] != '[' && data[i] != '(' { // not a link
841 if tmp_i > 0 {
842 return tmp_i
843 } else {
844 continue
845 }
846 }
847 cc := data[i]
848 i++
849 for i < len(data) && data[i] != cc {
850 if tmp_i == 0 && data[i] == c {
851 tmp_i = i
852 }
853 i++
854 }
855 if i >= len(data) {
856 return tmp_i
857 }
858 i++
859 }
860 }
861 }
862 return 0
863}
864
865func inlineHelperEmph1(out *bytes.Buffer, rndr *render, data []byte, c byte) int {
866 i := 0
867
868 if rndr.mk.Emphasis == nil {
869 return 0
870 }
871
872 // skip one symbol if coming from emph3
873 if len(data) > 1 && data[0] == c && data[1] == c {
874 i = 1
875 }
876
877 for i < len(data) {
878 length := inlineHelperFindEmphChar(data[i:], c)
879 if length == 0 {
880 return 0
881 }
882 i += length
883 if i >= len(data) {
884 return 0
885 }
886
887 if i+1 < len(data) && data[i+1] == c {
888 i++
889 continue
890 }
891
892 if data[i] == c && !isspace(data[i-1]) {
893
894 if rndr.flags&EXTENSION_NO_INTRA_EMPHASIS != 0 {
895 if !(i+1 == len(data) || isspace(data[i+1]) || ispunct(data[i+1])) {
896 continue
897 }
898 }
899
900 var work bytes.Buffer
901 parseInline(&work, rndr, data[:i])
902 r := rndr.mk.Emphasis(out, work.Bytes(), rndr.mk.Opaque)
903 if r > 0 {
904 return i + 1
905 } else {
906 return 0
907 }
908 }
909 }
910
911 return 0
912}
913
914func inlineHelperEmph2(out *bytes.Buffer, rndr *render, data []byte, c byte) int {
915 render_method := rndr.mk.DoubleEmphasis
916 if c == '~' {
917 render_method = rndr.mk.StrikeThrough
918 }
919
920 if render_method == nil {
921 return 0
922 }
923
924 i := 0
925
926 for i < len(data) {
927 length := inlineHelperFindEmphChar(data[i:], c)
928 if length == 0 {
929 return 0
930 }
931 i += length
932
933 if i+1 < len(data) && data[i] == c && data[i+1] == c && i > 0 && !isspace(data[i-1]) {
934 var work bytes.Buffer
935 parseInline(&work, rndr, data[:i])
936 r := render_method(out, work.Bytes(), rndr.mk.Opaque)
937 if r > 0 {
938 return i + 2
939 } else {
940 return 0
941 }
942 }
943 i++
944 }
945 return 0
946}
947
948func inlineHelperEmph3(out *bytes.Buffer, rndr *render, data []byte, offset int, c byte) int {
949 i := 0
950 orig_data := data
951 data = data[offset:]
952
953 for i < len(data) {
954 length := inlineHelperFindEmphChar(data[i:], c)
955 if length == 0 {
956 return 0
957 }
958 i += length
959
960 // skip whitespace preceded symbols
961 if data[i] != c || isspace(data[i-1]) {
962 continue
963 }
964
965 switch {
966 case (i+2 < len(data) && data[i+1] == c && data[i+2] == c && rndr.mk.TripleEmphasis != nil):
967 // triple symbol found
968 var work bytes.Buffer
969
970 parseInline(&work, rndr, data[:i])
971 r := rndr.mk.TripleEmphasis(out, work.Bytes(), rndr.mk.Opaque)
972 if r > 0 {
973 return i + 3
974 } else {
975 return 0
976 }
977 case (i+1 < len(data) && data[i+1] == c):
978 // double symbol found, hand over to emph1
979 length = inlineHelperEmph1(out, rndr, orig_data[offset-2:], c)
980 if length == 0 {
981 return 0
982 } else {
983 return length - 2
984 }
985 default:
986 // single symbol found, hand over to emph2
987 length = inlineHelperEmph2(out, rndr, orig_data[offset-1:], c)
988 if length == 0 {
989 return 0
990 } else {
991 return length - 1
992 }
993 }
994 }
995 return 0
996}