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