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// '\n' preceded by two spaces
161func inlineLineBreak(out *bytes.Buffer, rndr *render, data []byte, offset int) int {
162 if offset < 2 || data[offset-1] != ' ' || data[offset-2] != ' ' {
163 return 0
164 }
165
166 // remove trailing spaces from out and render
167 outBytes := out.Bytes()
168 end := len(outBytes)
169 for end > 0 && outBytes[end-1] == ' ' {
170 end--
171 }
172 out.Truncate(end)
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 text_has_nl := 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 text_has_nl = 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 txt_e := 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 link_b := 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 link_e := i
267
268 // look for title end if present
269 title_b, title_e := 0, 0
270 if data[i] == '\'' || data[i] == '"' {
271 i++
272 title_b = 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 title_e = i - 1
291 for title_e > title_b && isspace(data[title_e]) {
292 title_e--
293 }
294
295 // check for closing quote presence
296 if data[title_e] != '\'' && data[title_e] != '"' {
297 title_b, title_e = 0, 0
298 link_e = i
299 }
300 }
301
302 // remove whitespace at the end of the link
303 for link_e > link_b && isspace(data[link_e-1]) {
304 link_e--
305 }
306
307 // remove optional angle brackets around the link
308 if data[link_b] == '<' {
309 link_b++
310 }
311 if data[link_e-1] == '>' {
312 link_e--
313 }
314
315 // build escaped link and title
316 if link_e > link_b {
317 link = data[link_b:link_e]
318 }
319
320 if title_e > title_b {
321 title = data[title_b:title_e]
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 link_b := i
333 for i < len(data) && data[i] != ']' {
334 i++
335 }
336 if i >= len(data) {
337 return 0
338 }
339 link_e := i
340
341 // find the reference
342 if link_b == link_e {
343 if text_has_nl {
344 var b bytes.Buffer
345
346 for j := 1; j < txt_e; 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:txt_e]
358 }
359 } else {
360 id = data[link_b:link_e]
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 text_has_nl {
381 var b bytes.Buffer
382
383 for j := 1; j < txt_e; 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:txt_e]
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 = txt_e + 1
410 }
411
412 // build content: img alt is escaped, link content is parsed
413 var content bytes.Buffer
414 if txt_e > 1 {
415 if isImg {
416 content.Write(data[1:txt_e])
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:txt_e])
422 rndr.insideLink = insideLink
423 }
424 }
425
426 var u_link []byte
427 if len(link) > 0 {
428 var u_link_buf bytes.Buffer
429 unescapeText(&u_link_buf, link)
430 u_link = u_link_buf.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, u_link, title, content.Bytes(), rndr.mk.Opaque)
443 } else {
444 ret = rndr.mk.Link(out, u_link, 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 u_link bytes.Buffer
464 unescapeText(&u_link, data[1:end+1-2])
465 ret = rndr.mk.AutoLink(out, u_link.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 orig_data := data
565 data = data[offset-rewind:]
566
567 if !isSafeLink(data) {
568 return 0
569 }
570
571 link_end := 0
572 for link_end < len(data) && !isspace(data[link_end]) {
573 link_end++
574 }
575
576 // Skip punctuation at the end of the link
577 if (data[link_end-1] == '.' || data[link_end-1] == ',' || data[link_end-1] == ';') && data[link_end-2] != '\\' {
578 link_end--
579 }
580
581 // See if the link finishes with a punctuation sign that can be closed.
582 var copen byte
583 switch data[link_end-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 buf_end := offset - rewind + link_end - 2
600
601 open_delim := 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 buf_end >= 0 && orig_data[buf_end] != '\n' && open_delim != 0 {
624 if orig_data[buf_end] == data[link_end-1] {
625 open_delim++
626 }
627
628 if orig_data[buf_end] == copen {
629 open_delim--
630 }
631
632 buf_end--
633 }
634
635 if open_delim == 0 {
636 link_end--
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 u_link bytes.Buffer
647 unescapeText(&u_link, data[:link_end])
648
649 rndr.mk.AutoLink(out, u_link.Bytes(), LINK_TYPE_NORMAL, rndr.mk.Opaque)
650 }
651
652 return link_end - 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 tmp_i := 0
809 i++
810 for i < len(data) && data[i] != '`' {
811 if tmp_i == 0 && data[i] == c {
812 tmp_i = i
813 }
814 i++
815 }
816 if i >= len(data) {
817 return tmp_i
818 }
819 i++
820 } else {
821 if data[i] == '[' {
822 // skip a link
823 tmp_i := 0
824 i++
825 for i < len(data) && data[i] != ']' {
826 if tmp_i == 0 && data[i] == c {
827 tmp_i = 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 tmp_i
837 }
838 if data[i] != '[' && data[i] != '(' { // not a link
839 if tmp_i > 0 {
840 return tmp_i
841 } else {
842 continue
843 }
844 }
845 cc := data[i]
846 i++
847 for i < len(data) && data[i] != cc {
848 if tmp_i == 0 && data[i] == c {
849 tmp_i = i
850 }
851 i++
852 }
853 if i >= len(data) {
854 return tmp_i
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 render_method := rndr.mk.DoubleEmphasis
914 if c == '~' {
915 render_method = rndr.mk.StrikeThrough
916 }
917
918 if render_method == 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 := render_method(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 orig_data := 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, orig_data[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, orig_data[offset-1:], c)
986 if length == 0 {
987 return 0
988 } else {
989 return length - 1
990 }
991 }
992 }
993 return 0
994}