package markup
import (
"bytes"
"sort"
"strings"
)
// continueQuotes gives markdown quotes fossil's extent, which is wider
// than CommonMark's. Fossil's parse_blockquote (src/markdown.c) keeps a
// quote open over every following line, marked or not, blank lines
// included, until a blank line followed by a line that is neither blank
// nor a quote line. When the quote's first line opens a fence, blank
// lines before the fence's closing run do not end it either. CommonMark
// continues a quote lazily only while it holds a paragraph, so a fence
// opened on a quote line whose body carries no ">" — how SQLite's
// tickets quote their test scripts — became an empty quote, with the
// script spilling out after it as a paragraph.
//
// An alert is the exception: GitHub starts a new block at each alert
// marker and ends an alert at the next quote, so a blank line before a
// marker, or after an alert, closes the quote.
//
// Every line fossil counts as part of the quote gets the marker it was
// written without, at the quote's own indent so a quote inside a list
// item stays there. Lines of a fence opened outside any quote are code
// and are left alone.
func continueQuotes(src []byte) []byte {
if bytes.IndexByte(src, '>') < 0 {
return src
}
var out bytes.Buffer
out.Grow(len(src) + 64)
var fence []byte // the open top-level fence's marker run
quote := -1 // the open quote's indent; -1 when none is open
endFence := 0 // offset past the open quote's closing fence run
alert := false // the open quote opens with an alert marker
var runs fenceRuns
for pos := 0; pos < len(src); {
line := nextLine(src[pos:])
pos += len(line)
switch {
case fence != nil:
if closesFence(line, fence) {
fence = nil
}
case quoteIndent(line) >= 0:
if quote < 0 {
quote = quoteIndent(line)
endFence = quotedFenceEnd(src, pos-len(line), &runs)
alert = alertMarker(line)
}
case quote >= 0 && isBlank(line):
next := nextLine(src[pos:])
if pos >= len(src) || (pos > endFence && quoteIndent(next) < 0 && !isBlank(next)) ||
(pos > endFence && quoteIndent(next) >= 0 && (alert || alertMarker(next))) {
quote = -1
break
}
out.Write(bytes.Repeat([]byte{' '}, quote))
out.WriteByte('>')
out.Write(line[len(bytes.TrimRight(line, " \t\r\n")):])
continue
case quote >= 0:
n := 0
for n < quote && n < len(line) && line[n] == ' ' {
n++
}
out.Write(bytes.Repeat([]byte{' '}, quote))
out.WriteString("> ")
out.Write(line[n:])
continue
default:
fence = opensFence(line)
}
out.Write(line)
}
return out.Bytes()
}
// alertMarker answers whether a quote line is a GitHub alert marker,
// [!KIND] alone after the ">".
func alertMarker(line []byte) bool {
i := quoteIndent(line)
if i < 0 {
return false
}
m := strings.ToLower(string(bytes.TrimSpace(line[i+1:])))
if !strings.HasPrefix(m, "[!") || !strings.HasSuffix(m, "]") {
return false
}
_, ok := alertKinds[m[2:len(m)-1]]
return ok
}
// nextLine is b's first line, newline included.
func nextLine(b []byte) []byte {
if i := bytes.IndexByte(b, '\n'); i >= 0 {
return b[:i+1]
}
return b
}
func isBlank(line []byte) bool { return len(bytes.TrimSpace(line)) == 0 }
// quoteIndent is the indent of a quote marker opening line, or -1 when
// the line does not open with one (a marker needs at most three spaces
// before it).
func quoteIndent(line []byte) int {
n := 0
for n < 3 && n < len(line) && line[n] == ' ' {
n++
}
if n < len(line) && line[n] == '>' {
return n
}
return -1
}
// quotedFenceEnd mirrors the fence check at the top of fossil's
// parse_blockquote: when the quote opening at src[at:] starts with a
// fence (on its first line, or on the next when the first is otherwise
// empty), it is the offset just past the first later run of as many
// fence characters, wherever that run sits; 0 when there is none.
func quotedFenceEnd(src []byte, at int, runs *fenceRuns) int {
line := nextLine(src[at:])
i := at + quoteIndent(line) + 1
if i < len(src) && (src[i] == ' ' || src[i] == '\t') {
i++
}
if rest := nextLine(src[i:]); isBlank(rest) && bytes.HasSuffix(rest, []byte("\n")) {
i += len(rest)
}
if i >= len(src) || (src[i] != '`' && src[i] != '~') {
return 0
}
c, nb := src[i], 0
for i+nb < len(src) && src[i+nb] == c {
nb++
}
if nb < 3 {
return 0
}
return runs.closing(src, c, i+nb, nb)
}
// fenceRuns indexes the runs of three or more backticks and of tildes
// in a document, so finding a quoted fence's closing run is a lookup,
// not a scan to the end of the input for every quote that opens one.
// Built on first use; most documents never open a fence in a quote.
type fenceRuns struct {
built bool
of [2]runIndex // backticks, tildes
}
// runIndex holds one character's runs in order, with a max tree over
// their lengths for "first run from here at least this long".
type runIndex struct {
starts, lens []int
tree []int // tree[1] is the root; leaves from len(tree)/2
}
// closing is the offset just past the first nb characters of the first
// run of c of at least nb starting at or after from; 0 when none does.
// from always follows a maximal run of c, so no run straddles it.
func (r *fenceRuns) closing(src []byte, c byte, from, nb int) int {
if !r.built {
r.build(src)
}
x := &r.of[0]
if c == '~' {
x = &r.of[1]
}
k := sort.SearchInts(x.starts, from)
if j := x.first(1, 0, len(x.tree)/2, k, nb); j >= 0 {
return x.starts[j] + nb
}
return 0
}
func (r *fenceRuns) build(src []byte) {
r.built = true
for i := 0; i < len(src); {
c := src[i]
if c != '`' && c != '~' {
i++
continue
}
n := 1
for i+n < len(src) && src[i+n] == c {
n++
}
if n >= 3 {
x := &r.of[0]
if c == '~' {
x = &r.of[1]
}
x.starts = append(x.starts, i)
x.lens = append(x.lens, n)
}
i += n
}
for k := range r.of {
x := &r.of[k]
size := 1
for size < len(x.lens) {
size *= 2
}
x.tree = make([]int, 2*size)
copy(x.tree[size:], x.lens)
for i := size - 1; i >= 1; i-- {
x.tree[i] = max(x.tree[2*i], x.tree[2*i+1])
}
}
}
// first is the index of the first run at or after k at least nb long in
// the subtree node spanning runs [lo, hi), or -1.
func (x *runIndex) first(node, lo, hi, k, nb int) int {
if hi <= k || x.tree[node] < nb {
return -1
}
if hi-lo == 1 {
return lo
}
mid := (lo + hi) / 2
if j := x.first(2*node, lo, mid, k, nb); j >= 0 {
return j
}
return x.first(2*node+1, mid, hi, k, nb)
}
// opensFence returns the marker run of a fence opening line, nil when
// the line is not one.
func opensFence(line []byte) []byte {
s := bytes.TrimLeft(line, " ")
if len(line)-len(s) > 3 || len(s) < 3 || (s[0] != '`' && s[0] != '~') {
return nil
}
n := 0
for n < len(s) && s[n] == s[0] {
n++
}
if n < 3 || (s[0] == '`' && bytes.IndexByte(s[n:], '`') >= 0) {
return nil
}
return s[:n]
}
// closesFence answers whether line closes the fence opened by marker:
// the same character, at least as many, and nothing after but space.
func closesFence(line, marker []byte) bool {
s := bytes.TrimLeft(line, " ")
if len(line)-len(s) > 3 {
return false
}
n := 0
for n < len(s) && s[n] == marker[0] {
n++
}
return n >= len(marker) && len(bytes.TrimSpace(s[n:])) == 0
}