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