package markup
import "bytes"
// 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.
//
// 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
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))
}
case quote >= 0 && isBlank(line):
next := nextLine(src[pos:])
if pos >= len(src) || (pos > endFence && quoteIndent(next) < 0 && !isBlank(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()
}
// 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) 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
}
run := 0
for j := i + nb; j < len(src); j++ {
if src[j] != c {
run = 0
continue
}
if run++; run == nb {
return j + 1
}
}
return 0
}
// 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
}