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