all repos — grayfriday @ 3af64a90ad74a0c4bd13fc63b41b298b8a09ac21

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