all repos — grayfriday @ 9d23b68fa51d84855bbfaff1d12a449a53a25268

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