all repos — grayfriday @ 9a0217f7aa435282b4ba9139d9141bccca28ab05

blackfriday fork with a few changes

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}