all repos — grayfriday @ eff64c563f75c50f8af0ec650e719846efff659b

blackfriday fork with a few changes

block.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 block-level elements.
   9//
  10
  11package blackfriday
  12
  13import (
  14	"bytes"
  15)
  16
  17// parse block-level data
  18func parseBlock(out *bytes.Buffer, rndr *render, data []byte) {
  19	// this is called recursively: enforce a maximum depth
  20	if rndr.nesting >= rndr.maxNesting {
  21		return
  22	}
  23	rndr.nesting++
  24
  25	// parse out one block-level construct at a time
  26	for len(data) > 0 {
  27		// prefixed header:
  28		//
  29		// # Header 1
  30		// ## Header 2
  31		// ...
  32		// ###### Header 6
  33		if isPrefixHeader(rndr, data) {
  34			data = data[blockPrefixHeader(out, rndr, data):]
  35			continue
  36		}
  37
  38		// block of preformatted HTML:
  39		//
  40		// <div>
  41		//     ...
  42		// </div>
  43		if data[0] == '<' && rndr.mk.BlockHtml != nil {
  44			if i := blockHtml(out, rndr, data, true); i > 0 {
  45				data = data[i:]
  46				continue
  47			}
  48		}
  49
  50		// blank lines.  note: returns the # of bytes to skip
  51		if i := isEmpty(data); i > 0 {
  52			data = data[i:]
  53			continue
  54		}
  55
  56		// horizontal rule:
  57		//
  58		// ------
  59		// or
  60		// ******
  61		// or
  62		// ______
  63		if isHRule(data) {
  64			if rndr.mk.HRule != nil {
  65				rndr.mk.HRule(out, rndr.mk.Opaque)
  66			}
  67			var i int
  68			for i = 0; i < len(data) && data[i] != '\n'; i++ {
  69			}
  70			data = data[i:]
  71			continue
  72		}
  73
  74		// fenced code block:
  75		//
  76		// ``` go
  77		// func fact(n int) int {
  78		//     if n <= 1 {
  79		//         return n
  80		//     }
  81		//     return n * fact(n-1)
  82		// }
  83		// ```
  84		if rndr.flags&EXTENSION_FENCED_CODE != 0 {
  85			if i := blockFencedCode(out, rndr, data); i > 0 {
  86				data = data[i:]
  87				continue
  88			}
  89		}
  90
  91		// table:
  92		//
  93		// Name  | Age | Phone
  94		// ------|-----|---------
  95		// Bob   | 31  | 555-1234
  96		// Alice | 27  | 555-4321
  97		if rndr.flags&EXTENSION_TABLES != 0 {
  98			if i := blockTable(out, rndr, data); i > 0 {
  99				data = data[i:]
 100				continue
 101			}
 102		}
 103
 104		// block quote:
 105		//
 106		// > A big quote I found somewhere
 107		// > on the web
 108		if blockQuotePrefix(data) > 0 {
 109			data = data[blockQuote(out, rndr, data):]
 110			continue
 111		}
 112
 113		// indented code block:
 114		//
 115		//     func max(a, b int) int {
 116		//         if a > b {
 117		//             return a
 118		//         }
 119		//         return b
 120		//      }
 121		if blockCodePrefix(data) > 0 {
 122			data = data[blockCode(out, rndr, data):]
 123			continue
 124		}
 125
 126		// an itemized/unordered list:
 127		//
 128		// * Item 1
 129		// * Item 2
 130		//
 131		// also works with + or -
 132		if blockUliPrefix(data) > 0 {
 133			data = data[blockList(out, rndr, data, 0):]
 134			continue
 135		}
 136
 137		// a numbered/ordered list:
 138		//
 139		// 1. Item 1
 140		// 2. Item 2
 141		if blockOliPrefix(data) > 0 {
 142			data = data[blockList(out, rndr, data, LIST_TYPE_ORDERED):]
 143			continue
 144		}
 145
 146		// anything else must look like a normal paragraph
 147		// note: this finds underlined headers, too
 148		data = data[blockParagraph(out, rndr, data):]
 149	}
 150
 151	rndr.nesting--
 152}
 153
 154func isPrefixHeader(rndr *render, data []byte) bool {
 155	if data[0] != '#' {
 156		return false
 157	}
 158
 159	if rndr.flags&EXTENSION_SPACE_HEADERS != 0 {
 160		level := 0
 161		for level < len(data) && level < 6 && data[level] == '#' {
 162			level++
 163		}
 164		if level < len(data) && data[level] != ' ' && data[level] != '\t' {
 165			return false
 166		}
 167	}
 168	return true
 169}
 170
 171func blockPrefixHeader(out *bytes.Buffer, rndr *render, data []byte) int {
 172	level := 0
 173	for level < len(data) && level < 6 && data[level] == '#' {
 174		level++
 175	}
 176	i, end := 0, 0
 177	for i = level; i < len(data) && (data[i] == ' ' || data[i] == '\t'); i++ {
 178	}
 179	for end = i; end < len(data) && data[end] != '\n'; end++ {
 180	}
 181	skip := end
 182	for end > 0 && data[end-1] == '#' {
 183		end--
 184	}
 185	for end > 0 && (data[end-1] == ' ' || data[end-1] == '\t') {
 186		end--
 187	}
 188	if end > i {
 189		if rndr.mk.Header != nil {
 190			work := func() bool {
 191				parseInline(out, rndr, data[i:end])
 192				return true
 193			}
 194			rndr.mk.Header(out, work, level, rndr.mk.Opaque)
 195		}
 196	}
 197	return skip
 198}
 199
 200func isUnderlinedHeader(data []byte) int {
 201	i := 0
 202
 203	// test of level 1 header
 204	if data[i] == '=' {
 205		for i = 1; i < len(data) && data[i] == '='; i++ {
 206		}
 207		for i < len(data) && (data[i] == ' ' || data[i] == '\t') {
 208			i++
 209		}
 210		if i >= len(data) || data[i] == '\n' {
 211			return 1
 212		} else {
 213			return 0
 214		}
 215	}
 216
 217	// test of level 2 header
 218	if data[i] == '-' {
 219		for i = 1; i < len(data) && data[i] == '-'; i++ {
 220		}
 221		for i < len(data) && (data[i] == ' ' || data[i] == '\t') {
 222			i++
 223		}
 224		if i >= len(data) || data[i] == '\n' {
 225			return 2
 226		} else {
 227			return 0
 228		}
 229	}
 230
 231	return 0
 232}
 233
 234func blockHtml(out *bytes.Buffer, rndr *render, data []byte, do_render bool) int {
 235	var i, j int
 236
 237	// identify the opening tag
 238	if len(data) < 2 || data[0] != '<' {
 239		return 0
 240	}
 241	curtag, tagfound := blockHtmlFindTag(data[1:])
 242
 243	// handle special cases
 244	if !tagfound {
 245
 246		// HTML comment, lax form
 247		if len(data) > 5 && data[1] == '!' && data[2] == '-' && data[3] == '-' {
 248			i = 5
 249
 250			for i < len(data) && !(data[i-2] == '-' && data[i-1] == '-' && data[i] == '>') {
 251				i++
 252			}
 253			i++
 254
 255			if i < len(data) {
 256				j = isEmpty(data[i:])
 257			}
 258
 259			if j > 0 {
 260				size := i + j
 261				if do_render && rndr.mk.BlockHtml != nil {
 262					rndr.mk.BlockHtml(out, data[:size], rndr.mk.Opaque)
 263				}
 264				return size
 265			}
 266		}
 267
 268		// HR, which is the only self-closing block tag considered
 269		if len(data) > 4 &&
 270			(data[1] == 'h' || data[1] == 'H') &&
 271			(data[2] == 'r' || data[2] == 'R') {
 272
 273			i = 3
 274			for i < len(data) && data[i] != '>' {
 275				i++
 276			}
 277
 278			if i+1 < len(data) {
 279				i++
 280				j = isEmpty(data[i:])
 281				if j > 0 {
 282					size := i + j
 283					if do_render && rndr.mk.BlockHtml != nil {
 284						rndr.mk.BlockHtml(out, data[:size], rndr.mk.Opaque)
 285					}
 286					return size
 287				}
 288			}
 289		}
 290
 291		// no special case recognized
 292		return 0
 293	}
 294
 295	// look for an unindented matching closing tag
 296	//      followed by a blank line
 297	i = 1
 298	found := false
 299
 300	// if not found, try a second pass looking for indented match
 301	// but not if tag is "ins" or "del" (following original Markdown.pl)
 302	if curtag != "ins" && curtag != "del" {
 303		i = 1
 304		for i < len(data) {
 305			i++
 306			for i < len(data) && !(data[i-1] == '<' && data[i] == '/') {
 307				i++
 308			}
 309
 310			if i+2+len(curtag) >= len(data) {
 311				break
 312			}
 313
 314			j = blockHtmlFindEnd(curtag, rndr, data[i-1:])
 315
 316			if j > 0 {
 317				i += j - 1
 318				found = true
 319				break
 320			}
 321		}
 322	}
 323
 324	if !found {
 325		return 0
 326	}
 327
 328	// the end of the block has been found
 329	if do_render && rndr.mk.BlockHtml != nil {
 330		rndr.mk.BlockHtml(out, data[:i], rndr.mk.Opaque)
 331	}
 332
 333	return i
 334}
 335
 336func blockHtmlFindTag(data []byte) (string, bool) {
 337	i := 0
 338	for i < len(data) && ((data[i] >= '0' && data[i] <= '9') || (data[i] >= 'A' && data[i] <= 'Z') || (data[i] >= 'a' && data[i] <= 'z')) {
 339		i++
 340	}
 341	if i >= len(data) {
 342		return "", false
 343	}
 344	key := string(data[:i])
 345	if block_tags[key] {
 346		return key, true
 347	}
 348	return "", false
 349}
 350
 351func blockHtmlFindEnd(tag string, rndr *render, data []byte) int {
 352	// assume data[0] == '<' && data[1] == '/' already tested
 353
 354	// check if tag is a match
 355	if len(tag)+3 >= len(data) || bytes.Compare(data[2:2+len(tag)], []byte(tag)) != 0 || data[len(tag)+2] != '>' {
 356		return 0
 357	}
 358
 359	// check white lines
 360	i := len(tag) + 3
 361	w := 0
 362	if i < len(data) {
 363		if w = isEmpty(data[i:]); w == 0 {
 364			return 0 // non-blank after tag
 365		}
 366	}
 367	i += w
 368	w = 0
 369
 370	if rndr.flags&EXTENSION_LAX_HTML_BLOCKS != 0 {
 371		if i < len(data) {
 372			w = isEmpty(data[i:])
 373		}
 374	} else {
 375		if i < len(data) {
 376			if w = isEmpty(data[i:]); w == 0 {
 377				return 0 // non-blank line after tag line
 378			}
 379		}
 380	}
 381
 382	return i + w
 383}
 384
 385func isEmpty(data []byte) int {
 386	var i int
 387	for i = 0; i < len(data) && data[i] != '\n'; i++ {
 388		if data[i] != ' ' && data[i] != '\t' {
 389			return 0
 390		}
 391	}
 392	return i + 1
 393}
 394
 395func isHRule(data []byte) bool {
 396	// skip initial spaces
 397	if len(data) < 3 {
 398		return false
 399	}
 400	i := 0
 401
 402	// skip up to three spaces
 403	for i < 3 && data[i] == ' ' {
 404		i++
 405	}
 406
 407	// look at the hrule char
 408	if i+2 >= len(data) || (data[i] != '*' && data[i] != '-' && data[i] != '_') {
 409		return false
 410	}
 411	c := data[i]
 412
 413	// the whole line must be the char or whitespace
 414	n := 0
 415	for i < len(data) && data[i] != '\n' {
 416		switch {
 417		case data[i] == c:
 418			n++
 419		case data[i] != ' ' && data[i] != '\t':
 420			return false
 421		}
 422		i++
 423	}
 424
 425	return n >= 3
 426}
 427
 428func isFencedCode(data []byte, syntax **string) int {
 429	i, n := 0, 0
 430
 431	// skip initial spaces
 432	if len(data) < 3 {
 433		return 0
 434	}
 435	if data[0] == ' ' {
 436		i++
 437		if data[1] == ' ' {
 438			i++
 439			if data[2] == ' ' {
 440				i++
 441			}
 442		}
 443	}
 444
 445	// look at the hrule char
 446	if i+2 >= len(data) || !(data[i] == '~' || data[i] == '`') {
 447		return 0
 448	}
 449
 450	c := data[i]
 451
 452	// the whole line must be the char or whitespace
 453	for i < len(data) && data[i] == c {
 454		n++
 455		i++
 456	}
 457
 458	if n < 3 {
 459		return 0
 460	}
 461
 462	if syntax != nil {
 463		syn := 0
 464
 465		for i < len(data) && (data[i] == ' ' || data[i] == '\t') {
 466			i++
 467		}
 468
 469		syntax_start := i
 470
 471		if i < len(data) && data[i] == '{' {
 472			i++
 473			syntax_start++
 474
 475			for i < len(data) && data[i] != '}' && data[i] != '\n' {
 476				syn++
 477				i++
 478			}
 479
 480			if i == len(data) || data[i] != '}' {
 481				return 0
 482			}
 483
 484			// string all whitespace at the beginning and the end
 485			// of the {} block
 486			for syn > 0 && isspace(data[syntax_start]) {
 487				syntax_start++
 488				syn--
 489			}
 490
 491			for syn > 0 && isspace(data[syntax_start+syn-1]) {
 492				syn--
 493			}
 494
 495			i++
 496		} else {
 497			for i < len(data) && !isspace(data[i]) {
 498				syn++
 499				i++
 500			}
 501		}
 502
 503		language := string(data[syntax_start : syntax_start+syn])
 504		*syntax = &language
 505	}
 506
 507	for i < len(data) && data[i] != '\n' {
 508		if !isspace(data[i]) {
 509			return 0
 510		}
 511		i++
 512	}
 513
 514	return i + 1
 515}
 516
 517func blockFencedCode(out *bytes.Buffer, rndr *render, data []byte) int {
 518	var lang *string
 519	beg := isFencedCode(data, &lang)
 520	if beg == 0 {
 521		return 0
 522	}
 523
 524	var work bytes.Buffer
 525
 526	for beg < len(data) {
 527		fence_end := isFencedCode(data[beg:], nil)
 528		if fence_end != 0 {
 529			beg += fence_end
 530			break
 531		}
 532
 533		var end int
 534		for end = beg + 1; end < len(data) && data[end-1] != '\n'; end++ {
 535		}
 536
 537		if beg < end {
 538			// verbatim copy to the working buffer, escaping entities
 539			if isEmpty(data[beg:]) > 0 {
 540				work.WriteByte('\n')
 541			} else {
 542				work.Write(data[beg:end])
 543			}
 544		}
 545		beg = end
 546	}
 547
 548	if work.Len() > 0 && work.Bytes()[work.Len()-1] != '\n' {
 549		work.WriteByte('\n')
 550	}
 551
 552	if rndr.mk.BlockCode != nil {
 553		syntax := ""
 554		if lang != nil {
 555			syntax = *lang
 556		}
 557
 558		rndr.mk.BlockCode(out, work.Bytes(), syntax, rndr.mk.Opaque)
 559	}
 560
 561	return beg
 562}
 563
 564func blockTable(out *bytes.Buffer, rndr *render, data []byte) int {
 565	var header_work bytes.Buffer
 566	i, columns, col_data := blockTableHeader(&header_work, rndr, data)
 567	if i == 0 {
 568		return 0
 569	}
 570
 571	var body_work bytes.Buffer
 572
 573	for i < len(data) {
 574		pipes, row_start := 0, i
 575		for ; i < len(data) && data[i] != '\n'; i++ {
 576			if data[i] == '|' {
 577				pipes++
 578			}
 579		}
 580
 581		if pipes == 0 || i == len(data) {
 582			i = row_start
 583			break
 584		}
 585
 586		blockTableRow(&body_work, rndr, data[row_start:i], columns, col_data)
 587		i++
 588	}
 589
 590	if rndr.mk.Table != nil {
 591		rndr.mk.Table(out, header_work.Bytes(), body_work.Bytes(), col_data, rndr.mk.Opaque)
 592	}
 593
 594	return i
 595}
 596
 597func blockTableHeader(out *bytes.Buffer, rndr *render, data []byte) (size int, columns int, column_data []int) {
 598	i, pipes := 0, 0
 599	column_data = []int{}
 600	for i = 0; i < len(data) && data[i] != '\n'; i++ {
 601		if data[i] == '|' {
 602			pipes++
 603		}
 604	}
 605
 606	if i == len(data) || pipes == 0 {
 607		return 0, 0, column_data
 608	}
 609
 610	header_end := i
 611
 612	if data[0] == '|' {
 613		pipes--
 614	}
 615
 616	if i > 2 && data[i-1] == '|' {
 617		pipes--
 618	}
 619
 620	columns = pipes + 1
 621	column_data = make([]int, columns)
 622
 623	// parse the header underline
 624	i++
 625	if i < len(data) && data[i] == '|' {
 626		i++
 627	}
 628
 629	under_end := i
 630	for under_end < len(data) && data[under_end] != '\n' {
 631		under_end++
 632	}
 633
 634	col := 0
 635	for ; col < columns && i < under_end; col++ {
 636		dashes := 0
 637
 638		for i < under_end && (data[i] == ' ' || data[i] == '\t') {
 639			i++
 640		}
 641
 642		if data[i] == ':' {
 643			i++
 644			column_data[col] |= TABLE_ALIGNMENT_LEFT
 645			dashes++
 646		}
 647
 648		for i < under_end && data[i] == '-' {
 649			i++
 650			dashes++
 651		}
 652
 653		if i < under_end && data[i] == ':' {
 654			i++
 655			column_data[col] |= TABLE_ALIGNMENT_RIGHT
 656			dashes++
 657		}
 658
 659		for i < under_end && (data[i] == ' ' || data[i] == '\t') {
 660			i++
 661		}
 662
 663		if i < under_end && data[i] != '|' {
 664			break
 665		}
 666
 667		if dashes < 3 {
 668			break
 669		}
 670
 671		i++
 672	}
 673
 674	if col < columns {
 675		return 0, 0, column_data
 676	}
 677
 678	blockTableRow(out, rndr, data[:header_end], columns, column_data)
 679	size = under_end + 1
 680	return
 681}
 682
 683func blockTableRow(out *bytes.Buffer, rndr *render, data []byte, columns int, col_data []int) {
 684	i, col := 0, 0
 685	var row_work bytes.Buffer
 686
 687	if i < len(data) && data[i] == '|' {
 688		i++
 689	}
 690
 691	for col = 0; col < columns && i < len(data); col++ {
 692		for i < len(data) && isspace(data[i]) {
 693			i++
 694		}
 695
 696		cell_start := i
 697
 698		for i < len(data) && data[i] != '|' {
 699			i++
 700		}
 701
 702		cell_end := i - 1
 703
 704		for cell_end > cell_start && isspace(data[cell_end]) {
 705			cell_end--
 706		}
 707
 708		var cell_work bytes.Buffer
 709		parseInline(&cell_work, rndr, data[cell_start:cell_end+1])
 710
 711		if rndr.mk.TableCell != nil {
 712			cdata := 0
 713			if col < len(col_data) {
 714				cdata = col_data[col]
 715			}
 716			rndr.mk.TableCell(&row_work, cell_work.Bytes(), cdata, rndr.mk.Opaque)
 717		}
 718
 719		i++
 720	}
 721
 722	for ; col < columns; col++ {
 723		empty_cell := []byte{}
 724		if rndr.mk.TableCell != nil {
 725			cdata := 0
 726			if col < len(col_data) {
 727				cdata = col_data[col]
 728			}
 729			rndr.mk.TableCell(&row_work, empty_cell, cdata, rndr.mk.Opaque)
 730		}
 731	}
 732
 733	if rndr.mk.TableRow != nil {
 734		rndr.mk.TableRow(out, row_work.Bytes(), rndr.mk.Opaque)
 735	}
 736}
 737
 738// returns blockquote prefix length
 739func blockQuotePrefix(data []byte) int {
 740	i := 0
 741	for i < len(data) && i < 3 && data[i] == ' ' {
 742		i++
 743	}
 744	if i < len(data) && data[i] == '>' {
 745		if i+1 < len(data) && (data[i+1] == ' ' || data[i+1] == '\t') {
 746			return i + 2
 747		}
 748		return i + 1
 749	}
 750	return 0
 751}
 752
 753// parse a blockquote fragment
 754func blockQuote(out *bytes.Buffer, rndr *render, data []byte) int {
 755	var block bytes.Buffer
 756	var work bytes.Buffer
 757	beg, end := 0, 0
 758	for beg < len(data) {
 759		for end = beg + 1; end < len(data) && data[end-1] != '\n'; end++ {
 760		}
 761
 762		if pre := blockQuotePrefix(data[beg:]); pre > 0 {
 763			beg += pre // skip prefix
 764		} else {
 765			// empty line followed by non-quote line
 766			if isEmpty(data[beg:]) > 0 && (end >= len(data) || (blockQuotePrefix(data[end:]) == 0 && isEmpty(data[end:]) == 0)) {
 767				break
 768			}
 769		}
 770
 771		if beg < end { // copy into the in-place working buffer
 772			work.Write(data[beg:end])
 773		}
 774		beg = end
 775	}
 776
 777	parseBlock(&block, rndr, work.Bytes())
 778	if rndr.mk.BlockQuote != nil {
 779		rndr.mk.BlockQuote(out, block.Bytes(), rndr.mk.Opaque)
 780	}
 781	return end
 782}
 783
 784// returns prefix length for block code
 785func blockCodePrefix(data []byte) int {
 786	if len(data) > 0 && data[0] == '\t' {
 787		return 1
 788	}
 789	if len(data) > 3 && data[0] == ' ' && data[1] == ' ' && data[2] == ' ' && data[3] == ' ' {
 790		return 4
 791	}
 792	return 0
 793}
 794
 795func blockCode(out *bytes.Buffer, rndr *render, data []byte) int {
 796	var work bytes.Buffer
 797
 798	beg, end := 0, 0
 799	for beg < len(data) {
 800		for end = beg + 1; end < len(data) && data[end-1] != '\n'; end++ {
 801		}
 802
 803		if pre := blockCodePrefix(data[beg:end]); pre > 0 {
 804			beg += pre
 805		} else {
 806			if isEmpty(data[beg:end]) == 0 {
 807				// non-empty non-prefixed line breaks the pre
 808				break
 809			}
 810		}
 811
 812		if beg < end {
 813			// verbatim copy to the working buffer, escaping entities
 814			if isEmpty(data[beg:end]) > 0 {
 815				work.WriteByte('\n')
 816			} else {
 817				work.Write(data[beg:end])
 818			}
 819		}
 820		beg = end
 821	}
 822
 823	// trim all the \n off the end of work
 824	workbytes := work.Bytes()
 825	n := 0
 826	for len(workbytes) > n && workbytes[len(workbytes)-n-1] == '\n' {
 827		n++
 828	}
 829	if n > 0 {
 830		work.Truncate(len(workbytes) - n)
 831	}
 832
 833	work.WriteByte('\n')
 834
 835	if rndr.mk.BlockCode != nil {
 836		rndr.mk.BlockCode(out, work.Bytes(), "", rndr.mk.Opaque)
 837	}
 838
 839	return beg
 840}
 841
 842// returns unordered list item prefix
 843func blockUliPrefix(data []byte) int {
 844	i := 0
 845
 846	// start with up to 3 spaces
 847	for i < len(data) && i < 3 && data[i] == ' ' {
 848		i++
 849	}
 850
 851	// need a *, +, or - followed by a space/tab
 852	if i+1 >= len(data) ||
 853		(data[i] != '*' && data[i] != '+' && data[i] != '-') ||
 854		(data[i+1] != ' ' && data[i+1] != '\t') {
 855		return 0
 856	}
 857	return i + 2
 858}
 859
 860// returns ordered list item prefix
 861func blockOliPrefix(data []byte) int {
 862	i := 0
 863
 864	// start with up to 3 spaces
 865	for i < len(data) && i < 3 && data[i] == ' ' {
 866		i++
 867	}
 868
 869	// count the digits
 870	start := i
 871	for i < len(data) && data[i] >= '0' && data[i] <= '9' {
 872		i++
 873	}
 874
 875	// we need >= 1 digits followed by a dot and a space/tab
 876	if start == i || data[i] != '.' || i+1 >= len(data) ||
 877		(data[i+1] != ' ' && data[i+1] != '\t') {
 878		return 0
 879	}
 880	return i + 2
 881}
 882
 883// parse ordered or unordered list block
 884func blockList(out *bytes.Buffer, rndr *render, data []byte, flags int) int {
 885	i := 0
 886	work := func() bool {
 887		j := 0
 888		for i < len(data) {
 889			j = blockListItem(out, rndr, data[i:], &flags)
 890			i += j
 891
 892			if j == 0 || flags&LIST_ITEM_END_OF_LIST != 0 {
 893				break
 894			}
 895		}
 896		return true
 897	}
 898
 899	if rndr.mk.List != nil {
 900		rndr.mk.List(out, work, flags, rndr.mk.Opaque)
 901	}
 902	return i
 903}
 904
 905// parse a single list item
 906// assumes initial prefix is already removed
 907func blockListItem(out *bytes.Buffer, rndr *render, data []byte, flags *int) int {
 908	// keep track of the first indentation prefix
 909	beg, end, pre, sublist, orgpre, i := 0, 0, 0, 0, 0, 0
 910
 911	for orgpre < 3 && orgpre < len(data) && data[orgpre] == ' ' {
 912		orgpre++
 913	}
 914
 915	beg = blockUliPrefix(data)
 916	if beg == 0 {
 917		beg = blockOliPrefix(data)
 918	}
 919	if beg == 0 {
 920		return 0
 921	}
 922
 923	// skip leading whitespace on first line
 924	for beg < len(data) && data[beg] == ' ' {
 925		beg++
 926	}
 927
 928	// skip to the beginning of the following line
 929	end = beg
 930	for end < len(data) && data[end-1] != '\n' {
 931		end++
 932	}
 933
 934	// get working buffers
 935	var work bytes.Buffer
 936	var inter bytes.Buffer
 937
 938	// put the first line into the working buffer
 939	work.Write(data[beg:end])
 940	beg = end
 941
 942	// process the following lines
 943	in_empty, has_inside_empty := false, false
 944	for beg < len(data) {
 945		end++
 946
 947		for end < len(data) && data[end-1] != '\n' {
 948			end++
 949		}
 950
 951		// process an empty line
 952		if isEmpty(data[beg:end]) > 0 {
 953			in_empty = true
 954			beg = end
 955			continue
 956		}
 957
 958		// calculate the indentation
 959		i = 0
 960		for i < 4 && beg+i < end && data[beg+i] == ' ' {
 961			i++
 962		}
 963
 964		pre = i
 965		if data[beg] == '\t' {
 966			i = 1
 967			pre = 8
 968		}
 969
 970		// check for a new item
 971		chunk := data[beg+i : end]
 972		if (blockUliPrefix(chunk) > 0 && !isHRule(chunk)) || blockOliPrefix(chunk) > 0 {
 973			if in_empty {
 974				has_inside_empty = true
 975			}
 976
 977			if pre == orgpre { // the following item must have the same indentation
 978				break
 979			}
 980
 981			if sublist == 0 {
 982				sublist = work.Len()
 983			}
 984		} else {
 985			// only join indented stuff after empty lines
 986			if in_empty && i < 4 && data[beg] != '\t' {
 987				*flags |= LIST_ITEM_END_OF_LIST
 988				break
 989			} else {
 990				if in_empty {
 991					work.WriteByte('\n')
 992					has_inside_empty = true
 993				}
 994			}
 995		}
 996
 997		in_empty = false
 998
 999		// add the line into the working buffer without prefix
1000		work.Write(data[beg+i : end])
1001		beg = end
1002	}
1003
1004	// render li contents
1005	if has_inside_empty {
1006		*flags |= LIST_ITEM_CONTAINS_BLOCK
1007	}
1008
1009	workbytes := work.Bytes()
1010	if *flags&LIST_ITEM_CONTAINS_BLOCK != 0 {
1011		// intermediate render of block li
1012		if sublist > 0 && sublist < len(workbytes) {
1013			parseBlock(&inter, rndr, workbytes[:sublist])
1014			parseBlock(&inter, rndr, workbytes[sublist:])
1015		} else {
1016			parseBlock(&inter, rndr, workbytes)
1017		}
1018	} else {
1019		// intermediate render of inline li
1020		if sublist > 0 && sublist < len(workbytes) {
1021			parseInline(&inter, rndr, workbytes[:sublist])
1022			parseBlock(&inter, rndr, workbytes[sublist:])
1023		} else {
1024			parseInline(&inter, rndr, workbytes)
1025		}
1026	}
1027
1028	// render li itself
1029	if rndr.mk.ListItem != nil {
1030		rndr.mk.ListItem(out, inter.Bytes(), *flags, rndr.mk.Opaque)
1031	}
1032
1033	return beg
1034}
1035
1036// render a single paragraph that has already been parsed out
1037func renderParagraph(out *bytes.Buffer, rndr *render, data []byte) {
1038	// trim trailing newlines
1039	end := len(data)
1040	for end > 0 && data[end-1] == '\n' {
1041		end--
1042	}
1043	if end == 0 || rndr.mk.Paragraph == nil {
1044		return
1045	}
1046
1047	var work bytes.Buffer
1048	parseInline(&work, rndr, data[:end])
1049	rndr.mk.Paragraph(out, work.Bytes(), rndr.mk.Opaque)
1050}
1051
1052func blockParagraph(out *bytes.Buffer, rndr *render, data []byte) int {
1053	// prev: index of 1st char of previous line
1054	// line: index of 1st char of current line
1055	// i: index of cursor/end of current line
1056	var prev, line, i int
1057
1058	// keep going until we find something to mark the end of the paragraph
1059	for i < len(data) {
1060		// mark the beginning of the current line
1061		prev = line
1062		current := data[i:]
1063		line = i
1064
1065		// did we find a blank line marking the end of the paragraph?
1066		if n := isEmpty(current); n > 0 {
1067			renderParagraph(out, rndr, data[:i])
1068			return i + n
1069		}
1070
1071		// an underline under some text marks a header, so our paragraph ended on prev line
1072		if i > 0 && rndr.mk.Header != nil {
1073			if level := isUnderlinedHeader(current); level > 0 {
1074				// render the paragraph
1075				renderParagraph(out, rndr, data[:prev])
1076
1077				// render the header
1078				// this ugly, convoluted closure avoids forcing variables onto the heap
1079				work := func(o *bytes.Buffer, r *render, d []byte) func() bool {
1080					return func() bool {
1081						parseInline(o, r, d)
1082						return true
1083					}
1084				}(out, rndr, data[prev:i-1])
1085				rndr.mk.Header(out, work, level, rndr.mk.Opaque)
1086
1087				// find the end of the underline
1088				for ; i < len(data) && data[i] != '\n'; i++ {
1089				}
1090				return i
1091			}
1092		}
1093
1094		// if the next line starts a block of HTML, then the paragraph ends here
1095		if rndr.flags&EXTENSION_LAX_HTML_BLOCKS != 0 {
1096			if data[i] == '<' && rndr.mk.BlockHtml != nil && blockHtml(out, rndr, current, false) > 0 {
1097				// rewind to before the HTML block
1098				renderParagraph(out, rndr, data[:i])
1099				return i
1100			}
1101		}
1102
1103		// if there's a prefixed header or a horizontal rule after this, paragraph is over
1104		if isPrefixHeader(rndr, current) || isHRule(current) {
1105			renderParagraph(out, rndr, data[:i])
1106			return i
1107		}
1108
1109		// otherwise, scan to the beginning of the next line
1110		i++
1111		for i < len(data) && data[i-1] != '\n' {
1112			i++
1113		}
1114	}
1115
1116	renderParagraph(out, rndr, data[:i])
1117	return i
1118}