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