all repos — grayfriday @ fffbd3ed1a3469e71a3a8c9b9d7d77808cdeaeb8

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) && isalnum(data[i]) {
 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(data) < len(tag)+3 || data[len(tag)+2] != '>' ||
 356		bytes.Compare(data[2:2+len(tag)], []byte(tag)) != 0 {
 357		return 0
 358	}
 359
 360	// check for blank line/eof after the closing tag
 361	i := len(tag) + 3
 362	w := 0
 363	if i < len(data) {
 364		if w = isEmpty(data[i:]); w == 0 {
 365			return 0 // non-blank after tag
 366		}
 367	}
 368	i += w
 369	w = 0
 370
 371	if rndr.flags&EXTENSION_LAX_HTML_BLOCKS != 0 {
 372		if i < len(data) {
 373			w = isEmpty(data[i:])
 374		}
 375	} else {
 376		if i < len(data) {
 377			if w = isEmpty(data[i:]); w == 0 {
 378				return 0 // non-blank line after tag line
 379			}
 380		}
 381	}
 382
 383	return i + w
 384}
 385
 386func isEmpty(data []byte) int {
 387	var i int
 388	for i = 0; i < len(data) && data[i] != '\n'; i++ {
 389		if data[i] != ' ' && data[i] != '\t' {
 390			return 0
 391		}
 392	}
 393	return i + 1
 394}
 395
 396func isHRule(data []byte) bool {
 397	// skip initial spaces
 398	if len(data) < 3 {
 399		return false
 400	}
 401	i := 0
 402
 403	// skip up to three spaces
 404	for i < 3 && data[i] == ' ' {
 405		i++
 406	}
 407
 408	// look at the hrule char
 409	if i+2 >= len(data) || (data[i] != '*' && data[i] != '-' && data[i] != '_') {
 410		return false
 411	}
 412	c := data[i]
 413
 414	// the whole line must be the char or whitespace
 415	n := 0
 416	for i < len(data) && data[i] != '\n' {
 417		switch {
 418		case data[i] == c:
 419			n++
 420		case data[i] != ' ' && data[i] != '\t':
 421			return false
 422		}
 423		i++
 424	}
 425
 426	return n >= 3
 427}
 428
 429func isFencedCode(data []byte, syntax **string) int {
 430	i, n := 0, 0
 431
 432	// skip initial spaces
 433	if len(data) < 3 {
 434		return 0
 435	}
 436	if data[0] == ' ' {
 437		i++
 438		if data[1] == ' ' {
 439			i++
 440			if data[2] == ' ' {
 441				i++
 442			}
 443		}
 444	}
 445
 446	// look at the hrule char
 447	if i+2 >= len(data) || !(data[i] == '~' || data[i] == '`') {
 448		return 0
 449	}
 450
 451	c := data[i]
 452
 453	// the whole line must be the char or whitespace
 454	for i < len(data) && data[i] == c {
 455		n++
 456		i++
 457	}
 458
 459	if n < 3 {
 460		return 0
 461	}
 462
 463	if syntax != nil {
 464		syn := 0
 465
 466		for i < len(data) && (data[i] == ' ' || data[i] == '\t') {
 467			i++
 468		}
 469
 470		syntax_start := i
 471
 472		if i < len(data) && data[i] == '{' {
 473			i++
 474			syntax_start++
 475
 476			for i < len(data) && data[i] != '}' && data[i] != '\n' {
 477				syn++
 478				i++
 479			}
 480
 481			if i == len(data) || data[i] != '}' {
 482				return 0
 483			}
 484
 485			// string all whitespace at the beginning and the end
 486			// of the {} block
 487			for syn > 0 && isspace(data[syntax_start]) {
 488				syntax_start++
 489				syn--
 490			}
 491
 492			for syn > 0 && isspace(data[syntax_start+syn-1]) {
 493				syn--
 494			}
 495
 496			i++
 497		} else {
 498			for i < len(data) && !isspace(data[i]) {
 499				syn++
 500				i++
 501			}
 502		}
 503
 504		language := string(data[syntax_start : syntax_start+syn])
 505		*syntax = &language
 506	}
 507
 508	for i < len(data) && data[i] != '\n' {
 509		if !isspace(data[i]) {
 510			return 0
 511		}
 512		i++
 513	}
 514
 515	return i + 1
 516}
 517
 518func blockFencedCode(out *bytes.Buffer, rndr *render, data []byte) int {
 519	var lang *string
 520	beg := isFencedCode(data, &lang)
 521	if beg == 0 {
 522		return 0
 523	}
 524
 525	var work bytes.Buffer
 526
 527	for beg < len(data) {
 528		fence_end := isFencedCode(data[beg:], nil)
 529		if fence_end != 0 {
 530			beg += fence_end
 531			break
 532		}
 533
 534		var end int
 535		for end = beg + 1; end < len(data) && data[end-1] != '\n'; end++ {
 536		}
 537
 538		if beg < end {
 539			// verbatim copy to the working buffer, escaping entities
 540			if isEmpty(data[beg:]) > 0 {
 541				work.WriteByte('\n')
 542			} else {
 543				work.Write(data[beg:end])
 544			}
 545		}
 546		beg = end
 547	}
 548
 549	if work.Len() > 0 && work.Bytes()[work.Len()-1] != '\n' {
 550		work.WriteByte('\n')
 551	}
 552
 553	if rndr.mk.BlockCode != nil {
 554		syntax := ""
 555		if lang != nil {
 556			syntax = *lang
 557		}
 558
 559		rndr.mk.BlockCode(out, work.Bytes(), syntax, rndr.mk.Opaque)
 560	}
 561
 562	return beg
 563}
 564
 565func blockTable(out *bytes.Buffer, rndr *render, data []byte) int {
 566	var header_work bytes.Buffer
 567	i, columns, col_data := blockTableHeader(&header_work, rndr, data)
 568	if i == 0 {
 569		return 0
 570	}
 571
 572	var body_work bytes.Buffer
 573
 574	for i < len(data) {
 575		pipes, row_start := 0, i
 576		for ; i < len(data) && data[i] != '\n'; i++ {
 577			if data[i] == '|' {
 578				pipes++
 579			}
 580		}
 581
 582		if pipes == 0 || i == len(data) {
 583			i = row_start
 584			break
 585		}
 586
 587		blockTableRow(&body_work, rndr, data[row_start:i], columns, col_data)
 588		i++
 589	}
 590
 591	if rndr.mk.Table != nil {
 592		rndr.mk.Table(out, header_work.Bytes(), body_work.Bytes(), col_data, rndr.mk.Opaque)
 593	}
 594
 595	return i
 596}
 597
 598func blockTableHeader(out *bytes.Buffer, rndr *render, data []byte) (size int, columns int, column_data []int) {
 599	i, pipes := 0, 0
 600	column_data = []int{}
 601	for i = 0; i < len(data) && data[i] != '\n'; i++ {
 602		if data[i] == '|' {
 603			pipes++
 604		}
 605	}
 606
 607	if i == len(data) || pipes == 0 {
 608		return 0, 0, column_data
 609	}
 610
 611	header_end := i
 612
 613	if data[0] == '|' {
 614		pipes--
 615	}
 616
 617	if i > 2 && data[i-1] == '|' {
 618		pipes--
 619	}
 620
 621	columns = pipes + 1
 622	column_data = make([]int, columns)
 623
 624	// parse the header underline
 625	i++
 626	if i < len(data) && data[i] == '|' {
 627		i++
 628	}
 629
 630	under_end := i
 631	for under_end < len(data) && data[under_end] != '\n' {
 632		under_end++
 633	}
 634
 635	col := 0
 636	for ; col < columns && i < under_end; col++ {
 637		dashes := 0
 638
 639		for i < under_end && (data[i] == ' ' || data[i] == '\t') {
 640			i++
 641		}
 642
 643		if data[i] == ':' {
 644			i++
 645			column_data[col] |= TABLE_ALIGNMENT_LEFT
 646			dashes++
 647		}
 648
 649		for i < under_end && data[i] == '-' {
 650			i++
 651			dashes++
 652		}
 653
 654		if i < under_end && data[i] == ':' {
 655			i++
 656			column_data[col] |= TABLE_ALIGNMENT_RIGHT
 657			dashes++
 658		}
 659
 660		for i < under_end && (data[i] == ' ' || data[i] == '\t') {
 661			i++
 662		}
 663
 664		if i < under_end && data[i] != '|' {
 665			break
 666		}
 667
 668		if dashes < 3 {
 669			break
 670		}
 671
 672		i++
 673	}
 674
 675	if col < columns {
 676		return 0, 0, column_data
 677	}
 678
 679	blockTableRow(out, rndr, data[:header_end], columns, column_data)
 680	size = under_end + 1
 681	return
 682}
 683
 684func blockTableRow(out *bytes.Buffer, rndr *render, data []byte, columns int, col_data []int) {
 685	i, col := 0, 0
 686	var row_work bytes.Buffer
 687
 688	if i < len(data) && data[i] == '|' {
 689		i++
 690	}
 691
 692	for col = 0; col < columns && i < len(data); col++ {
 693		for i < len(data) && isspace(data[i]) {
 694			i++
 695		}
 696
 697		cell_start := i
 698
 699		for i < len(data) && data[i] != '|' {
 700			i++
 701		}
 702
 703		cell_end := i - 1
 704
 705		for cell_end > cell_start && isspace(data[cell_end]) {
 706			cell_end--
 707		}
 708
 709		var cell_work bytes.Buffer
 710		parseInline(&cell_work, rndr, data[cell_start:cell_end+1])
 711
 712		if rndr.mk.TableCell != nil {
 713			cdata := 0
 714			if col < len(col_data) {
 715				cdata = col_data[col]
 716			}
 717			rndr.mk.TableCell(&row_work, cell_work.Bytes(), cdata, rndr.mk.Opaque)
 718		}
 719
 720		i++
 721	}
 722
 723	for ; col < columns; col++ {
 724		empty_cell := []byte{}
 725		if rndr.mk.TableCell != nil {
 726			cdata := 0
 727			if col < len(col_data) {
 728				cdata = col_data[col]
 729			}
 730			rndr.mk.TableCell(&row_work, empty_cell, cdata, rndr.mk.Opaque)
 731		}
 732	}
 733
 734	if rndr.mk.TableRow != nil {
 735		rndr.mk.TableRow(out, row_work.Bytes(), rndr.mk.Opaque)
 736	}
 737}
 738
 739// returns blockquote prefix length
 740func blockQuotePrefix(data []byte) int {
 741	i := 0
 742	for i < len(data) && i < 3 && data[i] == ' ' {
 743		i++
 744	}
 745	if i < len(data) && data[i] == '>' {
 746		if i+1 < len(data) && (data[i+1] == ' ' || data[i+1] == '\t') {
 747			return i + 2
 748		}
 749		return i + 1
 750	}
 751	return 0
 752}
 753
 754// parse a blockquote fragment
 755func blockQuote(out *bytes.Buffer, rndr *render, data []byte) int {
 756	var block bytes.Buffer
 757	var work bytes.Buffer
 758	beg, end := 0, 0
 759	for beg < len(data) {
 760		for end = beg + 1; end < len(data) && data[end-1] != '\n'; end++ {
 761		}
 762
 763		if pre := blockQuotePrefix(data[beg:]); pre > 0 {
 764			beg += pre // skip prefix
 765		} else {
 766			// empty line followed by non-quote line
 767			if isEmpty(data[beg:]) > 0 && (end >= len(data) || (blockQuotePrefix(data[end:]) == 0 && isEmpty(data[end:]) == 0)) {
 768				break
 769			}
 770		}
 771
 772		if beg < end { // copy into the in-place working buffer
 773			work.Write(data[beg:end])
 774		}
 775		beg = end
 776	}
 777
 778	parseBlock(&block, rndr, work.Bytes())
 779	if rndr.mk.BlockQuote != nil {
 780		rndr.mk.BlockQuote(out, block.Bytes(), rndr.mk.Opaque)
 781	}
 782	return end
 783}
 784
 785// returns prefix length for block code
 786func blockCodePrefix(data []byte) int {
 787	if len(data) > 0 && data[0] == '\t' {
 788		return 1
 789	}
 790	if len(data) > 3 && data[0] == ' ' && data[1] == ' ' && data[2] == ' ' && data[3] == ' ' {
 791		return 4
 792	}
 793	return 0
 794}
 795
 796func blockCode(out *bytes.Buffer, rndr *render, data []byte) int {
 797	var work bytes.Buffer
 798
 799	beg, end := 0, 0
 800	for beg < len(data) {
 801		for end = beg + 1; end < len(data) && data[end-1] != '\n'; end++ {
 802		}
 803
 804		if pre := blockCodePrefix(data[beg:end]); pre > 0 {
 805			beg += pre
 806		} else {
 807			if isEmpty(data[beg:end]) == 0 {
 808				// non-empty non-prefixed line breaks the pre
 809				break
 810			}
 811		}
 812
 813		if beg < end {
 814			// verbatim copy to the working buffer, escaping entities
 815			if isEmpty(data[beg:end]) > 0 {
 816				work.WriteByte('\n')
 817			} else {
 818				work.Write(data[beg:end])
 819			}
 820		}
 821		beg = end
 822	}
 823
 824	// trim all the \n off the end of work
 825	workbytes := work.Bytes()
 826	n := 0
 827	for len(workbytes) > n && workbytes[len(workbytes)-n-1] == '\n' {
 828		n++
 829	}
 830	if n > 0 {
 831		work.Truncate(len(workbytes) - n)
 832	}
 833
 834	work.WriteByte('\n')
 835
 836	if rndr.mk.BlockCode != nil {
 837		rndr.mk.BlockCode(out, work.Bytes(), "", rndr.mk.Opaque)
 838	}
 839
 840	return beg
 841}
 842
 843// returns unordered list item prefix
 844func blockUliPrefix(data []byte) int {
 845	i := 0
 846
 847	// start with up to 3 spaces
 848	for i < len(data) && i < 3 && data[i] == ' ' {
 849		i++
 850	}
 851
 852	// need a *, +, or - followed by a space/tab
 853	if i+1 >= len(data) ||
 854		(data[i] != '*' && data[i] != '+' && data[i] != '-') ||
 855		(data[i+1] != ' ' && data[i+1] != '\t') {
 856		return 0
 857	}
 858	return i + 2
 859}
 860
 861// returns ordered list item prefix
 862func blockOliPrefix(data []byte) int {
 863	i := 0
 864
 865	// start with up to 3 spaces
 866	for i < len(data) && i < 3 && data[i] == ' ' {
 867		i++
 868	}
 869
 870	// count the digits
 871	start := i
 872	for i < len(data) && data[i] >= '0' && data[i] <= '9' {
 873		i++
 874	}
 875
 876	// we need >= 1 digits followed by a dot and a space/tab
 877	if start == i || data[i] != '.' || i+1 >= len(data) ||
 878		(data[i+1] != ' ' && data[i+1] != '\t') {
 879		return 0
 880	}
 881	return i + 2
 882}
 883
 884// parse ordered or unordered list block
 885func blockList(out *bytes.Buffer, rndr *render, data []byte, flags int) int {
 886	i := 0
 887	work := func() bool {
 888		j := 0
 889		for i < len(data) {
 890			j = blockListItem(out, rndr, data[i:], &flags)
 891			i += j
 892
 893			if j == 0 || flags&LIST_ITEM_END_OF_LIST != 0 {
 894				break
 895			}
 896		}
 897		return true
 898	}
 899
 900	if rndr.mk.List != nil {
 901		rndr.mk.List(out, work, flags, rndr.mk.Opaque)
 902	}
 903	return i
 904}
 905
 906// parse a single list item
 907// assumes initial prefix is already removed
 908func blockListItem(out *bytes.Buffer, rndr *render, data []byte, flags *int) int {
 909	// keep track of the first indentation prefix
 910	beg, end, pre, sublist, orgpre, i := 0, 0, 0, 0, 0, 0
 911
 912	for orgpre < 3 && orgpre < len(data) && data[orgpre] == ' ' {
 913		orgpre++
 914	}
 915
 916	beg = blockUliPrefix(data)
 917	if beg == 0 {
 918		beg = blockOliPrefix(data)
 919	}
 920	if beg == 0 {
 921		return 0
 922	}
 923
 924	// skip leading whitespace on first line
 925	for beg < len(data) && (data[beg] == ' ' || data[beg] == '\t') {
 926		beg++
 927	}
 928
 929	// skip to the beginning of the following line
 930	end = beg
 931	for end < len(data) && data[end-1] != '\n' {
 932		end++
 933	}
 934
 935	// get working buffers
 936	var work bytes.Buffer
 937	var inter bytes.Buffer
 938
 939	// put the first line into the working buffer
 940	work.Write(data[beg:end])
 941	beg = end
 942
 943	// process the following lines
 944	contains_blank_line, contains_block := false, false
 945	for beg < len(data) {
 946		end++
 947
 948		for end < len(data) && data[end-1] != '\n' {
 949			end++
 950		}
 951
 952		// process an empty line
 953		if isEmpty(data[beg:end]) > 0 {
 954			contains_blank_line = true
 955			beg = end
 956			continue
 957		}
 958
 959		// calculate the indentation
 960		i = 0
 961		for i < 4 && beg+i < end && data[beg+i] == ' ' {
 962			i++
 963		}
 964
 965		pre = i
 966		if data[beg] == '\t' {
 967			i = 1
 968			pre = TAB_SIZE
 969		}
 970
 971		chunk := data[beg+i : end]
 972
 973		// check for a nested list item
 974		if (blockUliPrefix(chunk) > 0 && !isHRule(chunk)) || blockOliPrefix(chunk) > 0 {
 975			if contains_blank_line {
 976				contains_block = true
 977			}
 978
 979			// the following item must have the same indentation
 980			if pre == orgpre {
 981				break
 982			}
 983
 984			if sublist == 0 {
 985				sublist = work.Len()
 986			}
 987		} else {
 988			// how about a nested prefix header?
 989			if isPrefixHeader(rndr, chunk) {
 990				// only nest headers that are indented
 991				if contains_blank_line && i < 4 && data[beg] != '\t' {
 992					*flags |= LIST_ITEM_END_OF_LIST
 993					break
 994				}
 995				contains_block = true
 996			} else {
 997				// only join stuff after empty lines when indented
 998				if contains_blank_line && i < 4 && data[beg] != '\t' {
 999					*flags |= LIST_ITEM_END_OF_LIST
1000					break
1001				} else {
1002					if contains_blank_line {
1003						work.WriteByte('\n')
1004						contains_block = true
1005					}
1006				}
1007			}
1008		}
1009
1010		contains_blank_line = false
1011
1012		// add the line into the working buffer without prefix
1013		work.Write(data[beg+i : end])
1014		beg = end
1015	}
1016
1017	// render li contents
1018	if contains_block {
1019		*flags |= LIST_ITEM_CONTAINS_BLOCK
1020	}
1021
1022	workbytes := work.Bytes()
1023	if *flags&LIST_ITEM_CONTAINS_BLOCK != 0 {
1024		// intermediate render of block li
1025		if sublist > 0 && sublist < len(workbytes) {
1026			parseBlock(&inter, rndr, workbytes[:sublist])
1027			parseBlock(&inter, rndr, workbytes[sublist:])
1028		} else {
1029			parseBlock(&inter, rndr, workbytes)
1030		}
1031	} else {
1032		// intermediate render of inline li
1033		if sublist > 0 && sublist < len(workbytes) {
1034			parseInline(&inter, rndr, workbytes[:sublist])
1035			parseBlock(&inter, rndr, workbytes[sublist:])
1036		} else {
1037			parseInline(&inter, rndr, workbytes)
1038		}
1039	}
1040
1041	// render li itself
1042	if rndr.mk.ListItem != nil {
1043		rndr.mk.ListItem(out, inter.Bytes(), *flags, rndr.mk.Opaque)
1044	}
1045
1046	return beg
1047}
1048
1049// render a single paragraph that has already been parsed out
1050func renderParagraph(out *bytes.Buffer, rndr *render, data []byte) {
1051	// trim leading whitespace
1052	beg := 0
1053	for beg < len(data) && isspace(data[beg]) {
1054		beg++
1055	}
1056
1057	// trim trailing whitespace
1058	end := len(data)
1059	for end > beg && isspace(data[end-1]) {
1060		end--
1061	}
1062	if end == beg || rndr.mk.Paragraph == nil {
1063		return
1064	}
1065
1066	work := func() bool {
1067		parseInline(out, rndr, data[beg:end])
1068		return true
1069	}
1070	rndr.mk.Paragraph(out, work, rndr.mk.Opaque)
1071}
1072
1073func blockParagraph(out *bytes.Buffer, rndr *render, data []byte) int {
1074	// prev: index of 1st char of previous line
1075	// line: index of 1st char of current line
1076	// i: index of cursor/end of current line
1077	var prev, line, i int
1078
1079	// keep going until we find something to mark the end of the paragraph
1080	for i < len(data) {
1081		// mark the beginning of the current line
1082		prev = line
1083		current := data[i:]
1084		line = i
1085
1086		// did we find a blank line marking the end of the paragraph?
1087		if n := isEmpty(current); n > 0 {
1088			renderParagraph(out, rndr, data[:i])
1089			return i + n
1090		}
1091
1092		// an underline under some text marks a header, so our paragraph ended on prev line
1093		if i > 0 && rndr.mk.Header != nil {
1094			if level := isUnderlinedHeader(current); level > 0 {
1095				// render the paragraph
1096				renderParagraph(out, rndr, data[:prev])
1097
1098				// ignore leading and trailing whitespace
1099				eol := i - 1
1100				for prev < eol && (data[prev] == ' ' || data[prev] == '\t') {
1101					prev++
1102				}
1103				for eol > prev && (data[eol-1] == ' ' || data[eol-1] == '\t') {
1104					eol--
1105				}
1106
1107				// render the header
1108				// this ugly double closure avoids forcing variables onto the heap
1109				work := func(o *bytes.Buffer, r *render, d []byte) func() bool {
1110					return func() bool {
1111						parseInline(o, r, d)
1112						return true
1113					}
1114				}(out, rndr, data[prev:eol])
1115				rndr.mk.Header(out, work, level, rndr.mk.Opaque)
1116
1117				// find the end of the underline
1118				for ; i < len(data) && data[i] != '\n'; i++ {
1119				}
1120				return i
1121			}
1122		}
1123
1124		// if the next line starts a block of HTML, then the paragraph ends here
1125		if rndr.flags&EXTENSION_LAX_HTML_BLOCKS != 0 {
1126			if data[i] == '<' && rndr.mk.BlockHtml != nil && blockHtml(out, rndr, current, false) > 0 {
1127				// rewind to before the HTML block
1128				renderParagraph(out, rndr, data[:i])
1129				return i
1130			}
1131		}
1132
1133		// if there's a prefixed header or a horizontal rule after this, paragraph is over
1134		if isPrefixHeader(rndr, current) || isHRule(current) {
1135			renderParagraph(out, rndr, data[:i])
1136			return i
1137		}
1138
1139		// otherwise, scan to the beginning of the next line
1140		i++
1141		for i < len(data) && data[i-1] != '\n' {
1142			i++
1143		}
1144	}
1145
1146	renderParagraph(out, rndr, data[:i])
1147	return i
1148}