present: fix Markdown bugs

The handling of subsubsections was not completely right,
causing unexpected subsubsubsections (#### inside ##)
to go into an infinite loop. Handle that.

Also, my usage of goldmark's (not completely documented)
SetAttributeString was wrong. Need []byte, not string.

Change-Id: Ib127a72b94b5a46adc9047fdb88dd2a8d03e73fe
Reviewed-on: https://go-review.googlesource.com/c/tools/+/223601
Reviewed-by: Andrew Bonventre <andybons@golang.org>
diff --git a/present/parse.go b/present/parse.go
index 672a6ff..c4c7d94 100644
--- a/present/parse.go
+++ b/present/parse.go
@@ -316,7 +316,7 @@
 		}
 
 		if isSpeakerNote(lines.text[i]) {
-			doc.TitleNotes = append(doc.TitleNotes, lines.text[i][2:])
+			doc.TitleNotes = append(doc.TitleNotes, trimSpeakerNote(lines.text[i]))
 		}
 	}
 
@@ -349,11 +349,14 @@
 }
 
 // isHeading matches any section heading.
-var isHeading = regexp.MustCompile(`^\*+ `)
+var (
+	isHeadingLegacy   = regexp.MustCompile(`^\*+( |$)`)
+	isHeadingMarkdown = regexp.MustCompile(`^\#+( |$)`)
+)
 
 // lesserHeading returns true if text is a heading of a lesser or equal level
 // than that denoted by prefix.
-func lesserHeading(text, prefix string) bool {
+func lesserHeading(isHeading *regexp.Regexp, text, prefix string) bool {
 	return isHeading.MatchString(text) && !strings.HasPrefix(text, prefix+prefix[:1])
 }
 
@@ -361,6 +364,10 @@
 // number (a nil number indicates the top level).
 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.
@@ -392,7 +399,7 @@
 			ID:     id,
 		}
 		text, ok = lines.nextNonEmpty()
-		for ok && !lesserHeading(text, prefix) {
+		for ok && !lesserHeading(isHeading, text, prefix) {
 			var e Elem
 			r, _ := utf8.DecodeRuneInString(text)
 			switch {
@@ -435,8 +442,8 @@
 				lines.back()
 				e = List{Bullet: b}
 			case isSpeakerNote(text):
-				section.Notes = append(section.Notes, text[2:])
-			case strings.HasPrefix(text, prefix+prefix[:1]+" "):
+				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 {
@@ -445,6 +452,8 @@
 				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" {
@@ -466,7 +475,7 @@
 				for ok && strings.TrimSpace(text) != "" {
 					// Command breaks text block.
 					// Section heading breaks text block in markdown.
-					if text[0] == '.' || isMarkdown && text[0] == '#' {
+					if text[0] == '.' || isMarkdown && text[0] == '#' || isSpeakerNote(text) {
 						lines.back()
 						break
 					}
@@ -514,6 +523,10 @@
 		}
 		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
 }
 
@@ -649,7 +662,14 @@
 }
 
 func isSpeakerNote(s string) bool {
-	return strings.HasPrefix(s, ": ")
+	return strings.HasPrefix(s, ": ") || s == ":"
+}
+
+func trimSpeakerNote(s string) string {
+	if s == ":" {
+		return ""
+	}
+	return strings.TrimPrefix(s, ": ")
 }
 
 func renderMarkdown(input []byte) (template.HTML, error) {
@@ -669,7 +689,9 @@
 		if entering {
 			switch n := n.(type) {
 			case *ast.Link:
-				n.SetAttributeString("target", "_blank")
+				n.SetAttributeString("target", []byte("_blank"))
+				// https://developers.google.com/web/tools/lighthouse/audits/noopener
+				n.SetAttributeString("rel", []byte("noopener"))
 			}
 		}
 		return ast.WalkContinue, nil