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}