all repos — grayfriday @ 2a18706ca4952462e699c51b746e188970933d8d

blackfriday fork with a few changes

inline.go (view raw)

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