in present/parse.go [365:557]
func parseSections(ctx *Context, name, prefix string, lines *Lines, number []int) ([]Section, error) {
isMarkdown := prefix[0] == '#'
isHeading := isHeadingLegacy
if isMarkdown {
isHeading = isHeadingMarkdown
}
var sections []Section
for i := 1; ; i++ {
// Next non-empty line is title.
text, ok := lines.nextNonEmpty()
for ok && text == "" {
text, ok = lines.next()
}
if !ok {
break
}
if text != prefix && !strings.HasPrefix(text, prefix+" ") {
lines.back()
break
}
// Markdown sections can end in {#id} to set the HTML anchor for the section.
// This is nicer than the default #TOC_1_2-style anchor.
title := strings.TrimSpace(text[len(prefix):])
id := ""
if isMarkdown && strings.HasSuffix(title, "}") {
j := strings.LastIndex(title, "{#")
if j >= 0 {
id = title[j+2 : len(title)-1]
title = strings.TrimSpace(title[:j])
}
}
section := Section{
Number: append(append([]int{}, number...), i),
Title: title,
ID: id,
}
text, ok = lines.nextNonEmpty()
for ok && !lesserHeading(isHeading, text, prefix) {
var e Elem
r, _ := utf8.DecodeRuneInString(text)
switch {
case !isMarkdown && unicode.IsSpace(r):
i := strings.IndexFunc(text, func(r rune) bool {
return !unicode.IsSpace(r)
})
if i < 0 {
break
}
indent := text[:i]
var s []string
for ok && (strings.HasPrefix(text, indent) || text == "") {
if text != "" {
text = text[i:]
}
s = append(s, text)
text, ok = lines.next()
}
lines.back()
pre := strings.Join(s, "\n")
raw := pre
pre = strings.Replace(pre, "\t", " ", -1) // browsers treat tabs badly
pre = strings.TrimRightFunc(pre, unicode.IsSpace)
e = Text{Lines: []string{pre}, Pre: true, Raw: raw}
case !isMarkdown && strings.HasPrefix(text, "- "):
var b []string
for {
if strings.HasPrefix(text, "- ") {
b = append(b, text[2:])
} else if len(b) > 0 && strings.HasPrefix(text, " ") {
b[len(b)-1] += "\n" + strings.TrimSpace(text)
} else {
break
}
if text, ok = lines.next(); !ok {
break
}
}
lines.back()
e = List{Bullet: b}
case isSpeakerNote(text):
section.Notes = append(section.Notes, trimSpeakerNote(text))
case strings.HasPrefix(text, prefix+prefix[:1]+" ") || text == prefix+prefix[:1]:
lines.back()
subsecs, err := parseSections(ctx, name, prefix+prefix[:1], lines, section.Number)
if err != nil {
return nil, err
}
for _, ss := range subsecs {
section.Elem = append(section.Elem, ss)
}
case strings.HasPrefix(text, prefix+prefix[:1]):
return nil, fmt.Errorf("%s:%d: badly nested section inside %s: %s", name, lines.line, prefix, text)
case strings.HasPrefix(text, "."):
args := strings.Fields(text)
if args[0] == ".background" {
section.Classes = append(section.Classes, "background")
section.Styles = append(section.Styles, "background-image: url('"+args[1]+"')")
break
}
parser := parsers[args[0]]
if parser == nil {
return nil, fmt.Errorf("%s:%d: unknown command %q", name, lines.line, text)
}
t, err := parser(ctx, name, lines.line, text)
if err != nil {
return nil, err
}
e = t
case isMarkdown:
// Collect Markdown lines, including blank lines and indented text.
var block []string
endLine, endBlock := lines.line-1, -1 // end is last non-empty line
for ok {
trim := strings.TrimSpace(text)
if trim != "" {
// Command breaks text block.
// Section heading breaks text block in markdown.
if text[0] == '.' || text[0] == '#' || isSpeakerNote(text) {
break
}
if strings.HasPrefix(text, `\.`) { // Backslash escapes initial period.
text = text[1:]
}
endLine, endBlock = lines.line, len(block)
}
block = append(block, text)
text, ok = lines.next()
}
block = block[:endBlock+1]
lines.line = endLine + 1
if len(block) == 0 {
break
}
// Replace all leading tabs with 4 spaces,
// which render better in code blocks.
// CommonMark defines that for parsing the structure of the file
// a tab is equivalent to 4 spaces, so this change won't
// affect the later parsing at all.
// An alternative would be to apply this to code blocks after parsing,
// at the same time that we update <a> targets, but that turns out
// to be quite difficult to modify in the AST.
for i, line := range block {
if len(line) > 0 && line[0] == '\t' {
short := strings.TrimLeft(line, "\t")
line = strings.Repeat(" ", len(line)-len(short)) + short
block[i] = line
}
}
html, err := renderMarkdown([]byte(strings.Join(block, "\n")))
if err != nil {
return nil, err
}
e = HTML{HTML: html}
default:
// Collect text lines.
var block []string
for ok && strings.TrimSpace(text) != "" {
// Command breaks text block.
// Section heading breaks text block in markdown.
if text[0] == '.' || isSpeakerNote(text) {
lines.back()
break
}
if strings.HasPrefix(text, `\.`) { // Backslash escapes initial period.
text = text[1:]
}
block = append(block, text)
text, ok = lines.next()
}
if len(block) == 0 {
break
}
e = Text{Lines: block}
}
if e != nil {
section.Elem = append(section.Elem, e)
}
text, ok = lines.nextNonEmpty()
}
if isHeading.MatchString(text) {
lines.back()
}
sections = append(sections, section)
}
if len(sections) == 0 {
return nil, fmt.Errorf("%s:%d: unexpected line: %s", name, lines.line+1, lines.text[lines.line])
}
return sections, nil
}