all repos — grayfriday @ f9b03f67fbe685393bf24c1fd18fbc89da420561

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