all repos — grayfriday @ cd5e4957ceb1c4cd0f506db2c5319d1345785eb6

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