Skip to content
quarry Artifact [2cf5f2bdd7]
Login

Artifact [2cf5f2bdd7]

Artifact 2cf5f2bdd795fb3ce628565b7c9996a822deeeb4f99cd85131597eb89c5be179:


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
}