all repos — grayfriday @ c5ece173ad8a086a3ea27a54b0a2ee82028fb274

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