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