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 regular links, footnote, and deferred footnotes
184 if p.insideLink && (offset > 0 && data[offset-1] == '[' || len(data)-1 > offset && data[offset+1] == '^') {
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 t == linkNormal || t == linkImg {
485 if len(link) > 0 {
486 var uLinkBuf bytes.Buffer
487 unescapeText(&uLinkBuf, link)
488 uLink = uLinkBuf.Bytes()
489 }
490
491 // links need something to click on and somewhere to go
492 if len(uLink) == 0 || (t == linkNormal && content.Len() == 0) {
493 return 0
494 }
495 }
496
497 // call the relevant rendering function
498 switch t {
499 case linkNormal:
500 p.r.Link(out, uLink, title, content.Bytes())
501
502 case linkImg:
503 outSize := out.Len()
504 outBytes := out.Bytes()
505 if outSize > 0 && outBytes[outSize-1] == '!' {
506 out.Truncate(outSize - 1)
507 }
508
509 p.r.Image(out, uLink, title, content.Bytes())
510
511 case linkInlineFootnote:
512 outSize := out.Len()
513 outBytes := out.Bytes()
514 if outSize > 0 && outBytes[outSize-1] == '^' {
515 out.Truncate(outSize - 1)
516 }
517
518 p.r.FootnoteRef(out, link, noteId)
519
520 case linkDeferredFootnote:
521 p.r.FootnoteRef(out, link, noteId)
522
523 default:
524 return 0
525 }
526
527 return i
528}
529
530// '<' when tags or autolinks are allowed
531func leftAngle(p *parser, out *bytes.Buffer, data []byte, offset int) int {
532 data = data[offset:]
533 altype := LINK_TYPE_NOT_AUTOLINK
534 end := tagLength(data, &altype)
535
536 if end > 2 {
537 if altype != LINK_TYPE_NOT_AUTOLINK {
538 var uLink bytes.Buffer
539 unescapeText(&uLink, data[1:end+1-2])
540 if uLink.Len() > 0 {
541 p.r.AutoLink(out, uLink.Bytes(), altype)
542 }
543 } else {
544 p.r.RawHtmlTag(out, data[:end])
545 }
546 }
547
548 return end
549}
550
551// '\\' backslash escape
552var escapeChars = []byte("\\`*_{}[]()#+-.!:|&<>")
553
554func escape(p *parser, out *bytes.Buffer, data []byte, offset int) int {
555 data = data[offset:]
556
557 if len(data) > 1 {
558 if bytes.IndexByte(escapeChars, data[1]) < 0 {
559 return 0
560 }
561
562 p.r.NormalText(out, data[1:2])
563 }
564
565 return 2
566}
567
568func unescapeText(ob *bytes.Buffer, src []byte) {
569 i := 0
570 for i < len(src) {
571 org := i
572 for i < len(src) && src[i] != '\\' {
573 i++
574 }
575
576 if i > org {
577 ob.Write(src[org:i])
578 }
579
580 if i+1 >= len(src) {
581 break
582 }
583
584 ob.WriteByte(src[i+1])
585 i += 2
586 }
587}
588
589// '&' escaped when it doesn't belong to an entity
590// valid entities are assumed to be anything matching &#?[A-Za-z0-9]+;
591func entity(p *parser, out *bytes.Buffer, data []byte, offset int) int {
592 data = data[offset:]
593
594 end := 1
595
596 if end < len(data) && data[end] == '#' {
597 end++
598 }
599
600 for end < len(data) && isalnum(data[end]) {
601 end++
602 }
603
604 if end < len(data) && data[end] == ';' {
605 end++ // real entity
606 } else {
607 return 0 // lone '&'
608 }
609
610 p.r.Entity(out, data[:end])
611
612 return end
613}
614
615func autoLink(p *parser, out *bytes.Buffer, data []byte, offset int) int {
616 // quick check to rule out most false hits on ':'
617 if p.insideLink || len(data) < offset+3 || data[offset+1] != '/' || data[offset+2] != '/' {
618 return 0
619 }
620
621 // scan backward for a word boundary
622 rewind := 0
623 for offset-rewind > 0 && rewind <= 7 && isletter(data[offset-rewind-1]) {
624 rewind++
625 }
626 if rewind > 6 { // longest supported protocol is "mailto" which has 6 letters
627 return 0
628 }
629
630 origData := data
631 data = data[offset-rewind:]
632
633 if !isSafeLink(data) {
634 return 0
635 }
636
637 linkEnd := 0
638 for linkEnd < len(data) && !isspace(data[linkEnd]) {
639 linkEnd++
640 }
641
642 // Skip punctuation at the end of the link
643 if (data[linkEnd-1] == '.' || data[linkEnd-1] == ',' || data[linkEnd-1] == ';') && data[linkEnd-2] != '\\' {
644 linkEnd--
645 }
646
647 // See if the link finishes with a punctuation sign that can be closed.
648 var copen byte
649 switch data[linkEnd-1] {
650 case '"':
651 copen = '"'
652 case '\'':
653 copen = '\''
654 case ')':
655 copen = '('
656 case ']':
657 copen = '['
658 case '}':
659 copen = '{'
660 default:
661 copen = 0
662 }
663
664 if copen != 0 {
665 bufEnd := offset - rewind + linkEnd - 2
666
667 openDelim := 1
668
669 /* Try to close the final punctuation sign in this same line;
670 * if we managed to close it outside of the URL, that means that it's
671 * not part of the URL. If it closes inside the URL, that means it
672 * is part of the URL.
673 *
674 * Examples:
675 *
676 * foo http://www.pokemon.com/Pikachu_(Electric) bar
677 * => http://www.pokemon.com/Pikachu_(Electric)
678 *
679 * foo (http://www.pokemon.com/Pikachu_(Electric)) bar
680 * => http://www.pokemon.com/Pikachu_(Electric)
681 *
682 * foo http://www.pokemon.com/Pikachu_(Electric)) bar
683 * => http://www.pokemon.com/Pikachu_(Electric))
684 *
685 * (foo http://www.pokemon.com/Pikachu_(Electric)) bar
686 * => foo http://www.pokemon.com/Pikachu_(Electric)
687 */
688
689 for bufEnd >= 0 && origData[bufEnd] != '\n' && openDelim != 0 {
690 if origData[bufEnd] == data[linkEnd-1] {
691 openDelim++
692 }
693
694 if origData[bufEnd] == copen {
695 openDelim--
696 }
697
698 bufEnd--
699 }
700
701 if openDelim == 0 {
702 linkEnd--
703 }
704 }
705
706 // we were triggered on the ':', so we need to rewind the output a bit
707 if out.Len() >= rewind {
708 out.Truncate(len(out.Bytes()) - rewind)
709 }
710
711 var uLink bytes.Buffer
712 unescapeText(&uLink, data[:linkEnd])
713
714 if uLink.Len() > 0 {
715 p.r.AutoLink(out, uLink.Bytes(), LINK_TYPE_NORMAL)
716 }
717
718 return linkEnd - rewind
719}
720
721var validUris = [][]byte{[]byte("http://"), []byte("https://"), []byte("ftp://"), []byte("mailto://"), []byte("/")}
722
723func isSafeLink(link []byte) bool {
724 for _, prefix := range validUris {
725 // TODO: handle unicode here
726 // case-insensitive prefix test
727 if len(link) > len(prefix) && bytes.Equal(bytes.ToLower(link[:len(prefix)]), prefix) && isalnum(link[len(prefix)]) {
728 return true
729 }
730 }
731
732 return false
733}
734
735// return the length of the given tag, or 0 is it's not valid
736func tagLength(data []byte, autolink *int) int {
737 var i, j int
738
739 // a valid tag can't be shorter than 3 chars
740 if len(data) < 3 {
741 return 0
742 }
743
744 // begins with a '<' optionally followed by '/', followed by letter or number
745 if data[0] != '<' {
746 return 0
747 }
748 if data[1] == '/' {
749 i = 2
750 } else {
751 i = 1
752 }
753
754 if !isalnum(data[i]) {
755 return 0
756 }
757
758 // scheme test
759 *autolink = LINK_TYPE_NOT_AUTOLINK
760
761 // try to find the beginning of an URI
762 for i < len(data) && (isalnum(data[i]) || data[i] == '.' || data[i] == '+' || data[i] == '-') {
763 i++
764 }
765
766 if i > 1 && i < len(data) && data[i] == '@' {
767 if j = isMailtoAutoLink(data[i:]); j != 0 {
768 *autolink = LINK_TYPE_EMAIL
769 return i + j
770 }
771 }
772
773 if i > 2 && i < len(data) && data[i] == ':' {
774 *autolink = LINK_TYPE_NORMAL
775 i++
776 }
777
778 // complete autolink test: no whitespace or ' or "
779 switch {
780 case i >= len(data):
781 *autolink = LINK_TYPE_NOT_AUTOLINK
782 case *autolink != 0:
783 j = i
784
785 for i < len(data) {
786 if data[i] == '\\' {
787 i += 2
788 } else if data[i] == '>' || data[i] == '\'' || data[i] == '"' || isspace(data[i]) {
789 break
790 } else {
791 i++
792 }
793
794 }
795
796 if i >= len(data) {
797 return 0
798 }
799 if i > j && data[i] == '>' {
800 return i + 1
801 }
802
803 // one of the forbidden chars has been found
804 *autolink = LINK_TYPE_NOT_AUTOLINK
805 }
806
807 // look for something looking like a tag end
808 for i < len(data) && data[i] != '>' {
809 i++
810 }
811 if i >= len(data) {
812 return 0
813 }
814 return i + 1
815}
816
817// look for the address part of a mail autolink and '>'
818// this is less strict than the original markdown e-mail address matching
819func isMailtoAutoLink(data []byte) int {
820 nb := 0
821
822 // address is assumed to be: [-@._a-zA-Z0-9]+ with exactly one '@'
823 for i := 0; i < len(data); i++ {
824 if isalnum(data[i]) {
825 continue
826 }
827
828 switch data[i] {
829 case '@':
830 nb++
831
832 case '-', '.', '_':
833 break
834
835 case '>':
836 if nb == 1 {
837 return i + 1
838 } else {
839 return 0
840 }
841 default:
842 return 0
843 }
844 }
845
846 return 0
847}
848
849// look for the next emph char, skipping other constructs
850func helperFindEmphChar(data []byte, c byte) int {
851 i := 1
852
853 for i < len(data) {
854 for i < len(data) && data[i] != c && data[i] != '`' && data[i] != '[' {
855 i++
856 }
857 if i >= len(data) {
858 return 0
859 }
860 if data[i] == c {
861 return i
862 }
863
864 // do not count escaped chars
865 if i != 0 && data[i-1] == '\\' {
866 i++
867 continue
868 }
869
870 if data[i] == '`' {
871 // skip a code span
872 tmpI := 0
873 i++
874 for i < len(data) && data[i] != '`' {
875 if tmpI == 0 && data[i] == c {
876 tmpI = i
877 }
878 i++
879 }
880 if i >= len(data) {
881 return tmpI
882 }
883 i++
884 } else if data[i] == '[' {
885 // skip a link
886 tmpI := 0
887 i++
888 for i < len(data) && data[i] != ']' {
889 if tmpI == 0 && data[i] == c {
890 tmpI = i
891 }
892 i++
893 }
894 i++
895 for i < len(data) && (data[i] == ' ' || data[i] == '\n') {
896 i++
897 }
898 if i >= len(data) {
899 return tmpI
900 }
901 if data[i] != '[' && data[i] != '(' { // not a link
902 if tmpI > 0 {
903 return tmpI
904 } else {
905 continue
906 }
907 }
908 cc := data[i]
909 i++
910 for i < len(data) && data[i] != cc {
911 if tmpI == 0 && data[i] == c {
912 tmpI = i
913 }
914 i++
915 }
916 if i >= len(data) {
917 return tmpI
918 }
919 i++
920 }
921 }
922 return 0
923}
924
925func helperEmphasis(p *parser, out *bytes.Buffer, data []byte, c byte) int {
926 i := 0
927
928 // skip one symbol if coming from emph3
929 if len(data) > 1 && data[0] == c && data[1] == c {
930 i = 1
931 }
932
933 for i < len(data) {
934 length := helperFindEmphChar(data[i:], c)
935 if length == 0 {
936 return 0
937 }
938 i += length
939 if i >= len(data) {
940 return 0
941 }
942
943 if i+1 < len(data) && data[i+1] == c {
944 i++
945 continue
946 }
947
948 if data[i] == c && !isspace(data[i-1]) {
949
950 if p.flags&EXTENSION_NO_INTRA_EMPHASIS != 0 {
951 if !(i+1 == len(data) || isspace(data[i+1]) || ispunct(data[i+1])) {
952 continue
953 }
954 }
955
956 var work bytes.Buffer
957 p.inline(&work, data[:i])
958 p.r.Emphasis(out, work.Bytes())
959 return i + 1
960 }
961 }
962
963 return 0
964}
965
966func helperDoubleEmphasis(p *parser, out *bytes.Buffer, data []byte, c byte) int {
967 i := 0
968
969 for i < len(data) {
970 length := helperFindEmphChar(data[i:], c)
971 if length == 0 {
972 return 0
973 }
974 i += length
975
976 if i+1 < len(data) && data[i] == c && data[i+1] == c && i > 0 && !isspace(data[i-1]) {
977 var work bytes.Buffer
978 p.inline(&work, data[:i])
979
980 if work.Len() > 0 {
981 // pick the right renderer
982 if c == '~' {
983 p.r.StrikeThrough(out, work.Bytes())
984 } else {
985 p.r.DoubleEmphasis(out, work.Bytes())
986 }
987 }
988 return i + 2
989 }
990 i++
991 }
992 return 0
993}
994
995func helperTripleEmphasis(p *parser, out *bytes.Buffer, data []byte, offset int, c byte) int {
996 i := 0
997 origData := data
998 data = data[offset:]
999
1000 for i < len(data) {
1001 length := helperFindEmphChar(data[i:], c)
1002 if length == 0 {
1003 return 0
1004 }
1005 i += length
1006
1007 // skip whitespace preceded symbols
1008 if data[i] != c || isspace(data[i-1]) {
1009 continue
1010 }
1011
1012 switch {
1013 case i+2 < len(data) && data[i+1] == c && data[i+2] == c:
1014 // triple symbol found
1015 var work bytes.Buffer
1016
1017 p.inline(&work, data[:i])
1018 if work.Len() > 0 {
1019 p.r.TripleEmphasis(out, work.Bytes())
1020 }
1021 return i + 3
1022 case (i+1 < len(data) && data[i+1] == c):
1023 // double symbol found, hand over to emph1
1024 length = helperEmphasis(p, out, origData[offset-2:], c)
1025 if length == 0 {
1026 return 0
1027 } else {
1028 return length - 2
1029 }
1030 default:
1031 // single symbol found, hand over to emph2
1032 length = helperDoubleEmphasis(p, out, origData[offset-1:], c)
1033 if length == 0 {
1034 return 0
1035 } else {
1036 return length - 1
1037 }
1038 }
1039 }
1040 return 0
1041}