all repos — grayfriday @ 55697351d0212e427d9066c27cc1770735ea5d34

blackfriday fork with a few changes

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)
 19
 20// Functions to parse text within a block
 21// Each function returns the number of chars taken care of
 22// data is the complete block being rendered
 23// offset is the number of valid chars before the current cursor
 24
 25func (parser *Parser) parseInline(out *bytes.Buffer, data []byte) {
 26	// this is called recursively: enforce a maximum depth
 27	if parser.nesting >= parser.maxNesting {
 28		return
 29	}
 30	parser.nesting++
 31
 32	i, end := 0, 0
 33	for i < len(data) {
 34		// copy inactive chars into the output
 35		for end < len(data) && parser.inline[data[end]] == nil {
 36			end++
 37		}
 38
 39		if parser.r.NormalText != nil {
 40			parser.r.NormalText(out, data[i:end], parser.r.Opaque)
 41		} else {
 42			out.Write(data[i:end])
 43		}
 44
 45		if end >= len(data) {
 46			break
 47		}
 48		i = end
 49
 50		// call the trigger
 51		handler := parser.inline[data[end]]
 52		if consumed := handler(out, parser, data, i); consumed == 0 {
 53			// no action from the callback; buffer the byte for later
 54			end = i + 1
 55		} else {
 56			// skip past whatever the callback used
 57			i += consumed
 58			end = i
 59		}
 60	}
 61
 62	parser.nesting--
 63}
 64
 65// single and double emphasis parsing
 66func inlineEmphasis(out *bytes.Buffer, parser *Parser, data []byte, offset int) int {
 67	data = data[offset:]
 68	c := data[0]
 69	ret := 0
 70
 71	if len(data) > 2 && data[1] != c {
 72		// whitespace cannot follow an opening emphasis;
 73		// strikethrough only takes two characters '~~'
 74		if c == '~' || isspace(data[1]) {
 75			return 0
 76		}
 77		if ret = inlineHelperEmph1(out, parser, data[1:], c); ret == 0 {
 78			return 0
 79		}
 80
 81		return ret + 1
 82	}
 83
 84	if len(data) > 3 && data[1] == c && data[2] != c {
 85		if isspace(data[2]) {
 86			return 0
 87		}
 88		if ret = inlineHelperEmph2(out, parser, data[2:], c); ret == 0 {
 89			return 0
 90		}
 91
 92		return ret + 2
 93	}
 94
 95	if len(data) > 4 && data[1] == c && data[2] == c && data[3] != c {
 96		if c == '~' || isspace(data[3]) {
 97			return 0
 98		}
 99		if ret = inlineHelperEmph3(out, parser, data, 3, c); ret == 0 {
100			return 0
101		}
102
103		return ret + 3
104	}
105
106	return 0
107}
108
109func inlineCodeSpan(out *bytes.Buffer, parser *Parser, data []byte, offset int) int {
110	data = data[offset:]
111
112	nb := 0
113
114	// count the number of backticks in the delimiter
115	for nb < len(data) && data[nb] == '`' {
116		nb++
117	}
118
119	// find the next delimiter
120	i, end := 0, 0
121	for end = nb; end < len(data) && i < nb; end++ {
122		if data[end] == '`' {
123			i++
124		} else {
125			i = 0
126		}
127	}
128
129	// no matching delimiter?
130	if i < nb && end >= len(data) {
131		return 0
132	}
133
134	// trim outside whitespace
135	fBegin := nb
136	for fBegin < end && (data[fBegin] == ' ' || data[fBegin] == '\t') {
137		fBegin++
138	}
139
140	fEnd := end - nb
141	for fEnd > fBegin && (data[fEnd-1] == ' ' || data[fEnd-1] == '\t') {
142		fEnd--
143	}
144
145	// render the code span
146	if parser.r.CodeSpan == nil {
147		return 0
148	}
149	if !parser.r.CodeSpan(out, data[fBegin:fEnd], parser.r.Opaque) {
150		end = 0
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 inlineLineBreak(out *bytes.Buffer, parser *Parser, 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] == ' ' || outBytes[eol-1] == '\t') {
165		eol--
166	}
167	out.Truncate(eol)
168
169	// should there be a hard line break here?
170	if parser.flags&EXTENSION_HARD_LINE_BREAK == 0 && end-eol < 2 {
171		return 0
172	}
173
174	if parser.r.LineBreak == nil {
175		return 0
176	}
177	if parser.r.LineBreak(out, parser.r.Opaque) {
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, parser *Parser, data []byte, offset int) int {
188	// no links allowed inside other links
189	if parser.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	textHasNl := false
200
201	// check whether the correct renderer exists
202	if (isImg && parser.r.Image == nil) || (!isImg && parser.r.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			textHasNl = 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	txtE := 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		linkB := 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		linkE := i
267
268		// look for title end if present
269		titleB, titleE := 0, 0
270		if data[i] == '\'' || data[i] == '"' {
271			i++
272			titleB = 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			titleE = i - 1
291			for titleE > titleB && isspace(data[titleE]) {
292				titleE--
293			}
294
295			// check for closing quote presence
296			if data[titleE] != '\'' && data[titleE] != '"' {
297				titleB, titleE = 0, 0
298				linkE = i
299			}
300		}
301
302		// remove whitespace at the end of the link
303		for linkE > linkB && isspace(data[linkE-1]) {
304			linkE--
305		}
306
307		// remove optional angle brackets around the link
308		if data[linkB] == '<' {
309			linkB++
310		}
311		if data[linkE-1] == '>' {
312			linkE--
313		}
314
315		// build escaped link and title
316		if linkE > linkB {
317			link = data[linkB:linkE]
318		}
319
320		if titleE > titleB {
321			title = data[titleB:titleE]
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		linkB := i
333		for i < len(data) && data[i] != ']' {
334			i++
335		}
336		if i >= len(data) {
337			return 0
338		}
339		linkE := i
340
341		// find the reference
342		if linkB == linkE {
343			if textHasNl {
344				var b bytes.Buffer
345
346				for j := 1; j < txtE; 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:txtE]
358			}
359		} else {
360			id = data[linkB:linkE]
361		}
362
363		// find the reference with matching id (ids are case-insensitive)
364		key := string(bytes.ToLower(id))
365		lr, ok := parser.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 textHasNl {
381			var b bytes.Buffer
382
383			for j := 1; j < txtE; 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:txtE]
395		}
396
397		// find the reference with matching id
398		key := string(bytes.ToLower(id))
399		lr, ok := parser.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 = txtE + 1
410	}
411
412	// build content: img alt is escaped, link content is parsed
413	var content bytes.Buffer
414	if txtE > 1 {
415		if isImg {
416			content.Write(data[1:txtE])
417		} else {
418			// links cannot contain other links, so turn off link parsing temporarily
419			insideLink := parser.insideLink
420			parser.insideLink = true
421			parser.parseInline(&content, data[1:txtE])
422			parser.insideLink = insideLink
423		}
424	}
425
426	var uLink []byte
427	if len(link) > 0 {
428		var uLinkBuf bytes.Buffer
429		unescapeText(&uLinkBuf, link)
430		uLink = uLinkBuf.Bytes()
431	}
432
433	// call the relevant rendering function
434	ret := false
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 = parser.r.Image(out, uLink, title, content.Bytes(), parser.r.Opaque)
443	} else {
444		ret = parser.r.Link(out, uLink, title, content.Bytes(), parser.r.Opaque)
445	}
446
447	if ret {
448		return i
449	}
450	return 0
451}
452
453// '<' when tags or autolinks are allowed
454func inlineLAngle(out *bytes.Buffer, parser *Parser, data []byte, offset int) int {
455	data = data[offset:]
456	altype := LINK_TYPE_NOT_AUTOLINK
457	end := tagLength(data, &altype)
458	ret := false
459
460	if end > 2 {
461		switch {
462		case parser.r.AutoLink != nil && altype != LINK_TYPE_NOT_AUTOLINK:
463			var uLink bytes.Buffer
464			unescapeText(&uLink, data[1:end+1-2])
465			ret = parser.r.AutoLink(out, uLink.Bytes(), altype, parser.r.Opaque)
466		case parser.r.RawHtmlTag != nil:
467			ret = parser.r.RawHtmlTag(out, data[:end], parser.r.Opaque)
468		}
469	}
470
471	if !ret {
472		return 0
473	}
474	return end
475}
476
477// '\\' backslash escape
478var escapeChars = []byte("\\`*_{}[]()#+-.!:|&<>")
479
480func inlineEscape(out *bytes.Buffer, parser *Parser, 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 parser.r.NormalText != nil {
489			parser.r.NormalText(out, data[1:2], parser.r.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, parser *Parser, 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 parser.r.Entity != nil {
541		parser.r.Entity(out, data[:end], parser.r.Opaque)
542	} else {
543		out.Write(data[:end])
544	}
545
546	return end
547}
548
549func inlineAutoLink(out *bytes.Buffer, parser *Parser, data []byte, offset int) int {
550	// quick check to rule out most false hits on ':'
551	if parser.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	origData := data
565	data = data[offset-rewind:]
566
567	if !isSafeLink(data) {
568		return 0
569	}
570
571	linkEnd := 0
572	for linkEnd < len(data) && !isspace(data[linkEnd]) {
573		linkEnd++
574	}
575
576	// Skip punctuation at the end of the link
577	if (data[linkEnd-1] == '.' || data[linkEnd-1] == ',' || data[linkEnd-1] == ';') && data[linkEnd-2] != '\\' {
578		linkEnd--
579	}
580
581	// See if the link finishes with a punctuation sign that can be closed.
582	var copen byte
583	switch data[linkEnd-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		bufEnd := offset - rewind + linkEnd - 2
600
601		openDelim := 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 bufEnd >= 0 && origData[bufEnd] != '\n' && openDelim != 0 {
624			if origData[bufEnd] == data[linkEnd-1] {
625				openDelim++
626			}
627
628			if origData[bufEnd] == copen {
629				openDelim--
630			}
631
632			bufEnd--
633		}
634
635		if openDelim == 0 {
636			linkEnd--
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 parser.r.AutoLink != nil {
646		var uLink bytes.Buffer
647		unescapeText(&uLink, data[:linkEnd])
648
649		parser.r.AutoLink(out, uLink.Bytes(), LINK_TYPE_NORMAL, parser.r.Opaque)
650	}
651
652	return linkEnd - 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			tmpI := 0
809			i++
810			for i < len(data) && data[i] != '`' {
811				if tmpI == 0 && data[i] == c {
812					tmpI = i
813				}
814				i++
815			}
816			if i >= len(data) {
817				return tmpI
818			}
819			i++
820		} else {
821			if data[i] == '[' {
822				// skip a link
823				tmpI := 0
824				i++
825				for i < len(data) && data[i] != ']' {
826					if tmpI == 0 && data[i] == c {
827						tmpI = 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 tmpI
837				}
838				if data[i] != '[' && data[i] != '(' { // not a link
839					if tmpI > 0 {
840						return tmpI
841					} else {
842						continue
843					}
844				}
845				cc := data[i]
846				i++
847				for i < len(data) && data[i] != cc {
848					if tmpI == 0 && data[i] == c {
849						tmpI = i
850					}
851					i++
852				}
853				if i >= len(data) {
854					return tmpI
855				}
856				i++
857			}
858		}
859	}
860	return 0
861}
862
863func inlineHelperEmph1(out *bytes.Buffer, parser *Parser, data []byte, c byte) int {
864	i := 0
865
866	if parser.r.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 parser.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			parser.parseInline(&work, data[:i])
900			if parser.r.Emphasis(out, work.Bytes(), parser.r.Opaque) {
901				return i + 1
902			} else {
903				return 0
904			}
905		}
906	}
907
908	return 0
909}
910
911func inlineHelperEmph2(out *bytes.Buffer, parser *Parser, data []byte, c byte) int {
912	renderMethod := parser.r.DoubleEmphasis
913	if c == '~' {
914		renderMethod = parser.r.StrikeThrough
915	}
916
917	if renderMethod == nil {
918		return 0
919	}
920
921	i := 0
922
923	for i < len(data) {
924		length := inlineHelperFindEmphChar(data[i:], c)
925		if length == 0 {
926			return 0
927		}
928		i += length
929
930		if i+1 < len(data) && data[i] == c && data[i+1] == c && i > 0 && !isspace(data[i-1]) {
931			var work bytes.Buffer
932			parser.parseInline(&work, data[:i])
933			if renderMethod(out, work.Bytes(), parser.r.Opaque) {
934				return i + 2
935			} else {
936				return 0
937			}
938		}
939		i++
940	}
941	return 0
942}
943
944func inlineHelperEmph3(out *bytes.Buffer, parser *Parser, data []byte, offset int, c byte) int {
945	i := 0
946	origData := data
947	data = data[offset:]
948
949	for i < len(data) {
950		length := inlineHelperFindEmphChar(data[i:], c)
951		if length == 0 {
952			return 0
953		}
954		i += length
955
956		// skip whitespace preceded symbols
957		if data[i] != c || isspace(data[i-1]) {
958			continue
959		}
960
961		switch {
962		case (i+2 < len(data) && data[i+1] == c && data[i+2] == c && parser.r.TripleEmphasis != nil):
963			// triple symbol found
964			var work bytes.Buffer
965
966			parser.parseInline(&work, data[:i])
967			if parser.r.TripleEmphasis(out, work.Bytes(), parser.r.Opaque) {
968				return i + 3
969			} else {
970				return 0
971			}
972		case (i+1 < len(data) && data[i+1] == c):
973			// double symbol found, hand over to emph1
974			length = inlineHelperEmph1(out, parser, origData[offset-2:], c)
975			if length == 0 {
976				return 0
977			} else {
978				return length - 2
979			}
980		default:
981			// single symbol found, hand over to emph2
982			length = inlineHelperEmph2(out, parser, origData[offset-1:], c)
983			if length == 0 {
984				return 0
985			} else {
986				return length - 1
987			}
988		}
989	}
990	return 0
991}