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