all repos — grayfriday @ fffbd3ed1a3469e71a3a8c9b9d7d77808cdeaeb8

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