Skip to content
quarry Artifact [455c6ff981]
Login

Artifact [455c6ff981]

Artifact 455c6ff9810eb89ce84d29190760fe59deb4333d7de67e24323393a542dbea76:


package markup

import (
	"bytes"
	"sort"
)

// 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
	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)
			}
		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, 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
}