743 lines
18 KiB
Go
743 lines
18 KiB
Go
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
|||
|
|
// SPDX-License-Identifier: MIT
|
||
|
|
|
||
|
|
package markdown
|
||
|
|
|
||
|
|
import (
|
||
|
|
"bytes"
|
||
|
|
"unicode"
|
||
|
|
"unicode/utf8"
|
||
|
|
)
|
||
|
|
|
||
|
|
type inlineKind uint8
|
||
|
|
|
||
|
|
const (
|
||
|
|
inlText inlineKind = iota
|
||
|
|
inlCode
|
||
|
|
inlRawHTML
|
||
|
|
inlEmph
|
||
|
|
inlStrong
|
||
|
|
inlStrikethrough
|
||
|
|
inlLink
|
||
|
|
inlImage
|
||
|
|
inlBreak
|
||
|
|
inlFootnoteRef
|
||
|
|
)
|
||
|
|
|
||
|
|
// inline is one node of the inline tree. While parsing, the top level
|
||
|
|
// nodes form a doubly-linked chain; when emphasis or a link takes a range,
|
||
|
|
// the range becomes the children slice of the wrapping node.
|
||
|
|
type inline struct {
|
||
|
|
kind inlineKind
|
||
|
|
literal string // text, code content, raw HTML, soft break
|
||
|
|
dest string
|
||
|
|
title string
|
||
|
|
hasTitle bool
|
||
|
|
num int // footnote reference: ordinal
|
||
|
|
occurrence int // footnote reference: which reference to that ordinal
|
||
|
|
children []*inline
|
||
|
|
prev *inline
|
||
|
|
next *inline
|
||
|
|
}
|
||
|
|
|
||
|
|
// footnoteTracker numbers footnote references in document order: a
|
||
|
|
// definition receives its ordinal at its first reference, and later
|
||
|
|
// references to the same definition count their occurrences.
|
||
|
|
type footnoteTracker struct {
|
||
|
|
defs map[string]*Node
|
||
|
|
ordinals map[string]int
|
||
|
|
seen map[string]int
|
||
|
|
order []*Node
|
||
|
|
}
|
||
|
|
|
||
|
|
func newFootnoteTracker(defs []*Node) *footnoteTracker {
|
||
|
|
m := make(map[string]*Node, len(defs))
|
||
|
|
for _, d := range defs {
|
||
|
|
if _, ok := m[d.label]; !ok {
|
||
|
|
m[d.label] = d
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return &footnoteTracker{defs: m, ordinals: map[string]int{}, seen: map[string]int{}}
|
||
|
|
}
|
||
|
|
|
||
|
|
// reference records one reference to the labelled footnote.
|
||
|
|
func (t *footnoteTracker) reference(label string) (ordinal, occurrence int, ok bool) {
|
||
|
|
if _, defined := t.defs[label]; !defined {
|
||
|
|
return 0, 0, false
|
||
|
|
}
|
||
|
|
t.seen[label]++
|
||
|
|
if t.seen[label] == 1 {
|
||
|
|
t.ordinals[label] = len(t.order) + 1
|
||
|
|
t.order = append(t.order, t.defs[label])
|
||
|
|
}
|
||
|
|
return t.ordinals[label], t.seen[label], true
|
||
|
|
}
|
||
|
|
|
||
|
|
// delimiter is one entry of the delimiter stack: a run of asterisks or
|
||
|
|
// underscores waiting to be matched, or an open link or image bracket.
|
||
|
|
type delimiter struct {
|
||
|
|
node *inline
|
||
|
|
char byte
|
||
|
|
isBracket bool
|
||
|
|
image bool
|
||
|
|
active bool
|
||
|
|
length int
|
||
|
|
origLen int
|
||
|
|
canOpen bool
|
||
|
|
canClose bool
|
||
|
|
srcPos int // bracket: index just after the opening literal
|
||
|
|
prev *delimiter
|
||
|
|
next *delimiter
|
||
|
|
}
|
||
|
|
|
||
|
|
// parseInlines parses the inline content of a paragraph or heading against
|
||
|
|
// the document's link reference definitions and footnotes.
|
||
|
|
func parseInlines(content []byte, refs map[string]reference, footnotes *footnoteTracker) []*inline {
|
||
|
|
p := &inlineParser{src: bytes.TrimRight(content, " \t"), refs: refs, footnotes: footnotes}
|
||
|
|
p.parse()
|
||
|
|
return p.chain()
|
||
|
|
}
|
||
|
|
|
||
|
|
type inlineParser struct {
|
||
|
|
src []byte
|
||
|
|
pos int
|
||
|
|
refs map[string]reference
|
||
|
|
footnotes *footnoteTracker
|
||
|
|
|
||
|
|
first *inline
|
||
|
|
last *inline
|
||
|
|
firstDelim *delimiter
|
||
|
|
lastDelim *delimiter
|
||
|
|
}
|
||
|
|
|
||
|
|
func (p *inlineParser) chain() []*inline {
|
||
|
|
var nodes []*inline
|
||
|
|
for n := p.first; n != nil; n = n.next {
|
||
|
|
nodes = append(nodes, n)
|
||
|
|
}
|
||
|
|
return nodes
|
||
|
|
}
|
||
|
|
|
||
|
|
func (p *inlineParser) push(n *inline) *inline {
|
||
|
|
n.prev = p.last
|
||
|
|
n.next = nil
|
||
|
|
if p.last != nil {
|
||
|
|
p.last.next = n
|
||
|
|
} else {
|
||
|
|
p.first = n
|
||
|
|
}
|
||
|
|
p.last = n
|
||
|
|
return n
|
||
|
|
}
|
||
|
|
|
||
|
|
func (p *inlineParser) removeNode(n *inline) {
|
||
|
|
if n.prev != nil {
|
||
|
|
n.prev.next = n.next
|
||
|
|
} else {
|
||
|
|
p.first = n.next
|
||
|
|
}
|
||
|
|
if n.next != nil {
|
||
|
|
n.next.prev = n.prev
|
||
|
|
} else {
|
||
|
|
p.last = n.prev
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func (p *inlineParser) pushDelim(d *delimiter) {
|
||
|
|
d.prev = p.lastDelim
|
||
|
|
d.next = nil
|
||
|
|
if p.lastDelim != nil {
|
||
|
|
p.lastDelim.next = d
|
||
|
|
} else {
|
||
|
|
p.firstDelim = d
|
||
|
|
}
|
||
|
|
p.lastDelim = d
|
||
|
|
}
|
||
|
|
|
||
|
|
func (p *inlineParser) removeDelim(d *delimiter) {
|
||
|
|
if d.prev != nil {
|
||
|
|
d.prev.next = d.next
|
||
|
|
} else {
|
||
|
|
p.firstDelim = d.next
|
||
|
|
}
|
||
|
|
if d.next != nil {
|
||
|
|
d.next.prev = d.prev
|
||
|
|
} else {
|
||
|
|
p.lastDelim = d.prev
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// lastBracket returns the most recent open bracket on the stack.
|
||
|
|
func (p *inlineParser) lastBracket() *delimiter {
|
||
|
|
for d := p.lastDelim; d != nil; d = d.prev {
|
||
|
|
if d.isBracket {
|
||
|
|
return d
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func (p *inlineParser) parse() {
|
||
|
|
for p.pos < len(p.src) {
|
||
|
|
switch c := p.src[p.pos]; c {
|
||
|
|
case '\n':
|
||
|
|
p.handleNewline()
|
||
|
|
case '\\':
|
||
|
|
p.handleBackslash()
|
||
|
|
case '`':
|
||
|
|
p.handleBackticks()
|
||
|
|
case '<':
|
||
|
|
p.handleLessThan()
|
||
|
|
case '&':
|
||
|
|
p.handleAmpersand()
|
||
|
|
case '*', '_', '~':
|
||
|
|
p.handleDelimiterRun(c)
|
||
|
|
case '[':
|
||
|
|
if p.tryFootnoteRef() {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
p.pushBracket(false)
|
||
|
|
case '!':
|
||
|
|
if p.pos+1 < len(p.src) && p.src[p.pos+1] == '[' {
|
||
|
|
p.pushBracket(true)
|
||
|
|
} else {
|
||
|
|
p.push(&inline{kind: inlText, literal: "!"})
|
||
|
|
p.pos++
|
||
|
|
}
|
||
|
|
case ']':
|
||
|
|
p.handleCloseBracket()
|
||
|
|
default:
|
||
|
|
p.textRun()
|
||
|
|
}
|
||
|
|
}
|
||
|
|
p.processEmphasis(nil)
|
||
|
|
}
|
||
|
|
|
||
|
|
// handleNewline ends the line: the whitespace before the line ending is
|
||
|
|
// stripped, and two or more spaces make the break hard.
|
||
|
|
func (p *inlineParser) handleNewline() {
|
||
|
|
spaces := 0
|
||
|
|
if p.last != nil && p.last.kind == inlText {
|
||
|
|
lit := p.last.literal
|
||
|
|
end := len(lit)
|
||
|
|
for end > 0 && isSpaceTab(lit[end-1]) {
|
||
|
|
if lit[end-1] == ' ' {
|
||
|
|
spaces++
|
||
|
|
}
|
||
|
|
end--
|
||
|
|
}
|
||
|
|
if end == 0 {
|
||
|
|
p.removeNode(p.last)
|
||
|
|
} else {
|
||
|
|
p.last.literal = lit[:end]
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if spaces >= 2 {
|
||
|
|
p.push(&inline{kind: inlBreak})
|
||
|
|
} else {
|
||
|
|
p.push(&inline{kind: inlText, literal: "\n"})
|
||
|
|
}
|
||
|
|
p.pos++
|
||
|
|
}
|
||
|
|
|
||
|
|
func (p *inlineParser) handleBackslash() {
|
||
|
|
if p.pos+1 < len(p.src) {
|
||
|
|
next := p.src[p.pos+1]
|
||
|
|
switch {
|
||
|
|
case next == '\n':
|
||
|
|
p.push(&inline{kind: inlBreak})
|
||
|
|
p.pos += 2
|
||
|
|
return
|
||
|
|
case isASCIIPunct(next):
|
||
|
|
p.push(&inline{kind: inlText, literal: string(next)})
|
||
|
|
p.pos += 2
|
||
|
|
return
|
||
|
|
}
|
||
|
|
}
|
||
|
|
p.push(&inline{kind: inlText, literal: `\`})
|
||
|
|
p.pos++
|
||
|
|
}
|
||
|
|
|
||
|
|
// handleBackticks scans for a closing backtick string of equal length and
|
||
|
|
// emits the code span between them, or the literal opening run.
|
||
|
|
func (p *inlineParser) handleBackticks() {
|
||
|
|
openLen := fenceRun(p.src[p.pos:], '`')
|
||
|
|
i := p.pos + openLen
|
||
|
|
for i < len(p.src) {
|
||
|
|
if p.src[i] == '`' {
|
||
|
|
l := fenceRun(p.src[i:], '`')
|
||
|
|
if l == openLen {
|
||
|
|
p.push(&inline{kind: inlCode, literal: codeSpanContent(p.src[p.pos+openLen : i])})
|
||
|
|
p.pos = i + l
|
||
|
|
return
|
||
|
|
}
|
||
|
|
i += l
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
i++
|
||
|
|
}
|
||
|
|
p.push(&inline{kind: inlText, literal: string(p.src[p.pos : p.pos+openLen])})
|
||
|
|
p.pos += openLen
|
||
|
|
}
|
||
|
|
|
||
|
|
// codeSpanContent converts line endings to spaces and strips the one-space
|
||
|
|
// margin a span carries at both ends when it is not all spaces.
|
||
|
|
func codeSpanContent(c []byte) string {
|
||
|
|
c = bytes.ReplaceAll(c, []byte("\n"), []byte(" "))
|
||
|
|
if len(c) >= 2 && c[0] == ' ' && c[len(c)-1] == ' ' {
|
||
|
|
allSpaces := true
|
||
|
|
for _, b := range c {
|
||
|
|
if b != ' ' {
|
||
|
|
allSpaces = false
|
||
|
|
break
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if !allSpaces {
|
||
|
|
c = c[1 : len(c)-1]
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return string(c)
|
||
|
|
}
|
||
|
|
|
||
|
|
func (p *inlineParser) handleLessThan() {
|
||
|
|
if text, dest, n, ok := scanAutolink(p.src[p.pos:]); ok {
|
||
|
|
p.push(&inline{kind: inlLink, dest: dest, children: []*inline{{kind: inlText, literal: text}}})
|
||
|
|
p.pos += n
|
||
|
|
return
|
||
|
|
}
|
||
|
|
if n := scanRawHTML(p.src[p.pos:]); n > 0 {
|
||
|
|
p.push(&inline{kind: inlRawHTML, literal: string(p.src[p.pos : p.pos+n])})
|
||
|
|
p.pos += n
|
||
|
|
return
|
||
|
|
}
|
||
|
|
p.push(&inline{kind: inlText, literal: "<"})
|
||
|
|
p.pos++
|
||
|
|
}
|
||
|
|
|
||
|
|
func (p *inlineParser) handleAmpersand() {
|
||
|
|
if s, n, ok := scanEntity(p.src, p.pos); ok {
|
||
|
|
p.push(&inline{kind: inlText, literal: s})
|
||
|
|
p.pos += n
|
||
|
|
return
|
||
|
|
}
|
||
|
|
p.push(&inline{kind: inlText, literal: "&"})
|
||
|
|
p.pos++
|
||
|
|
}
|
||
|
|
|
||
|
|
// handleDelimiterRun records a run of asterisks or underscores and whether
|
||
|
|
// it may open or close emphasis under the flanking rules.
|
||
|
|
func (p *inlineParser) handleDelimiterRun(char byte) {
|
||
|
|
start := p.pos
|
||
|
|
end := start + fenceRun(p.src[start:], char)
|
||
|
|
beforeWS, beforePunct := classifyRune(runeBefore(p.src, start))
|
||
|
|
afterWS, afterPunct := classifyRune(runeAfter(p.src, end))
|
||
|
|
left := !afterWS && (!afterPunct || beforeWS || beforePunct)
|
||
|
|
right := !beforeWS && (!beforePunct || afterWS || afterPunct)
|
||
|
|
d := &delimiter{char: char, length: end - start, origLen: end - start}
|
||
|
|
if char == '_' {
|
||
|
|
d.canOpen = left && (!right || beforePunct)
|
||
|
|
d.canClose = right && (!left || afterPunct)
|
||
|
|
} else {
|
||
|
|
// asterisks and tildes flank the same way
|
||
|
|
d.canOpen, d.canClose = left, right
|
||
|
|
}
|
||
|
|
d.node = p.push(&inline{kind: inlText, literal: string(p.src[start:end])})
|
||
|
|
p.pushDelim(d)
|
||
|
|
p.pos = end
|
||
|
|
}
|
||
|
|
|
||
|
|
// tryFootnoteRef consumes a reference to a defined footnote and emits its
|
||
|
|
// marker. A reference to an undefined footnote stays bracket text.
|
||
|
|
func (p *inlineParser) tryFootnoteRef() bool {
|
||
|
|
if p.footnotes == nil {
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
label, n, ok := scanFootnoteLabel(p.src[p.pos:])
|
||
|
|
if !ok {
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
num, occurrence, ok := p.footnotes.reference(normaliseLabel(label))
|
||
|
|
if !ok {
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
p.push(&inline{kind: inlFootnoteRef, num: num, occurrence: occurrence})
|
||
|
|
p.pos += n
|
||
|
|
return true
|
||
|
|
}
|
||
|
|
|
||
|
|
func (p *inlineParser) pushBracket(image bool) {
|
||
|
|
lit := "["
|
||
|
|
if image {
|
||
|
|
lit = "!["
|
||
|
|
}
|
||
|
|
n := p.push(&inline{kind: inlText, literal: lit})
|
||
|
|
p.pushDelim(&delimiter{
|
||
|
|
node: n, char: '[', isBracket: true, image: image, active: true,
|
||
|
|
srcPos: p.pos + len(lit),
|
||
|
|
})
|
||
|
|
p.pos += len(lit)
|
||
|
|
}
|
||
|
|
|
||
|
|
// textRun consumes the run of ordinary characters up to the next special
|
||
|
|
// one, emitting extended autolinks and plain text in the order they come.
|
||
|
|
func (p *inlineParser) textRun() {
|
||
|
|
start := p.pos
|
||
|
|
for p.pos < len(p.src) {
|
||
|
|
c := p.src[p.pos]
|
||
|
|
if isInlineSpecial(c) {
|
||
|
|
break
|
||
|
|
}
|
||
|
|
var prev byte
|
||
|
|
if p.pos > 0 {
|
||
|
|
prev = p.src[p.pos-1]
|
||
|
|
}
|
||
|
|
if c == 'w' || c == 'h' || c == 'f' ||
|
||
|
|
(!isEmailPrevByte(prev) && isEmailLocalByte(c)) {
|
||
|
|
if n, text, dest, ok := scanExtendedAutolink(p.src, p.pos, prev); ok {
|
||
|
|
if p.pos > start {
|
||
|
|
p.push(&inline{kind: inlText, literal: string(p.src[start:p.pos])})
|
||
|
|
}
|
||
|
|
p.push(&inline{
|
||
|
|
kind: inlLink,
|
||
|
|
dest: dest,
|
||
|
|
children: []*inline{{kind: inlText, literal: text}},
|
||
|
|
})
|
||
|
|
p.pos += n
|
||
|
|
start = p.pos
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
}
|
||
|
|
p.pos++
|
||
|
|
}
|
||
|
|
if p.pos > start {
|
||
|
|
p.push(&inline{kind: inlText, literal: string(p.src[start:p.pos])})
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func isInlineSpecial(c byte) bool {
|
||
|
|
switch c {
|
||
|
|
case '\n', '\\', '`', '<', '&', '*', '_', '~', '[', ']', '!':
|
||
|
|
return true
|
||
|
|
}
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
|
||
|
|
// processEmphasis matches delimiter runs between the stack bottom and the
|
||
|
|
// top into emphasis and strong nodes, following the reference algorithm:
|
||
|
|
// closers walk forward, openers are searched backwards, a matching pair
|
||
|
|
// may not both be intraword delimiters whose run lengths add up against
|
||
|
|
// the rule of three, and the matched run lengths shrink from the inner
|
||
|
|
// sides.
|
||
|
|
func (p *inlineParser) processEmphasis(bottom *delimiter) {
|
||
|
|
var closer *delimiter
|
||
|
|
if bottom == nil {
|
||
|
|
closer = p.firstDelim
|
||
|
|
} else {
|
||
|
|
closer = bottom.next
|
||
|
|
}
|
||
|
|
for closer != nil {
|
||
|
|
if closer.isBracket || !closer.canClose {
|
||
|
|
closer = closer.next
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
opener, found := p.findOpener(closer, bottom)
|
||
|
|
if !found {
|
||
|
|
if !closer.canOpen {
|
||
|
|
p.removeDelim(closer)
|
||
|
|
}
|
||
|
|
closer = closer.next
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
use := 1
|
||
|
|
kind := inlEmph
|
||
|
|
switch closer.char {
|
||
|
|
case '~':
|
||
|
|
if closer.length >= 2 && opener.length >= 2 {
|
||
|
|
use = 2
|
||
|
|
}
|
||
|
|
kind = inlStrikethrough
|
||
|
|
default:
|
||
|
|
if closer.length >= 2 && opener.length >= 2 {
|
||
|
|
use = 2
|
||
|
|
kind = inlStrong
|
||
|
|
}
|
||
|
|
}
|
||
|
|
var children []*inline
|
||
|
|
for n := opener.node.next; n != closer.node; n = n.next {
|
||
|
|
children = append(children, n)
|
||
|
|
}
|
||
|
|
node := &inline{kind: kind, children: children}
|
||
|
|
opener.node.next = node
|
||
|
|
node.prev = opener.node
|
||
|
|
node.next = closer.node
|
||
|
|
closer.node.prev = node
|
||
|
|
opener.node.literal = opener.node.literal[:len(opener.node.literal)-use]
|
||
|
|
closer.node.literal = closer.node.literal[use:]
|
||
|
|
opener.length -= use
|
||
|
|
closer.length -= use
|
||
|
|
for d := closer.prev; d != nil && d != opener; {
|
||
|
|
prev := d.prev
|
||
|
|
p.removeDelim(d)
|
||
|
|
d = prev
|
||
|
|
}
|
||
|
|
if opener.length == 0 {
|
||
|
|
p.removeNode(opener.node)
|
||
|
|
p.removeDelim(opener)
|
||
|
|
}
|
||
|
|
if closer.length == 0 {
|
||
|
|
next := closer.next
|
||
|
|
p.removeNode(closer.node)
|
||
|
|
p.removeDelim(closer)
|
||
|
|
closer = next
|
||
|
|
}
|
||
|
|
}
|
||
|
|
for p.lastDelim != nil && p.lastDelim != bottom {
|
||
|
|
p.removeDelim(p.lastDelim)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// findOpener searches backwards from the closer for a run of the same
|
||
|
|
// character that may open, honouring the rule of three.
|
||
|
|
func (p *inlineParser) findOpener(closer, bottom *delimiter) (*delimiter, bool) {
|
||
|
|
for opener := closer.prev; opener != nil && opener != bottom; opener = opener.prev {
|
||
|
|
if opener.isBracket || opener.char != closer.char || !opener.canOpen {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
oddMatch := closer.char != '~' &&
|
||
|
|
(closer.canOpen || opener.canClose) &&
|
||
|
|
(opener.origLen+closer.origLen)%3 == 0 &&
|
||
|
|
!(opener.origLen%3 == 0 && closer.origLen%3 == 0)
|
||
|
|
if !oddMatch {
|
||
|
|
return opener, true
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return nil, false
|
||
|
|
}
|
||
|
|
|
||
|
|
// handleCloseBracket tries to close the most recent open bracket as an
|
||
|
|
// inline link or image, as a reference link with an explicit, collapsed or
|
||
|
|
// empty label, and leaves the bracket as literal text otherwise.
|
||
|
|
func (p *inlineParser) handleCloseBracket() {
|
||
|
|
opener := p.lastBracket()
|
||
|
|
if opener == nil {
|
||
|
|
p.push(&inline{kind: inlText, literal: "]"})
|
||
|
|
p.pos++
|
||
|
|
return
|
||
|
|
}
|
||
|
|
if !opener.active {
|
||
|
|
p.removeDelim(opener)
|
||
|
|
p.push(&inline{kind: inlText, literal: "]"})
|
||
|
|
p.pos++
|
||
|
|
return
|
||
|
|
}
|
||
|
|
closerIdx := p.pos
|
||
|
|
p.pos++
|
||
|
|
|
||
|
|
var dest, title string
|
||
|
|
var hasTitle bool
|
||
|
|
matched := false
|
||
|
|
|
||
|
|
if p.pos < len(p.src) && p.src[p.pos] == '(' {
|
||
|
|
save := p.pos
|
||
|
|
p.pos++
|
||
|
|
if d, t, ht, ok := p.scanInlineSpec(); ok {
|
||
|
|
dest, title, hasTitle, matched = d, t, ht, true
|
||
|
|
} else {
|
||
|
|
p.pos = save
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if !matched {
|
||
|
|
save := p.pos
|
||
|
|
label, ok := p.referenceLabel(opener, closerIdx)
|
||
|
|
if ok && len(bytes.TrimSpace(label)) > 0 && validLabel(label) {
|
||
|
|
if ref, exists := p.refs[normaliseLabel(string(label))]; exists {
|
||
|
|
dest, title, hasTitle, matched = ref.destination, ref.title, ref.hasTitle, true
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if !matched {
|
||
|
|
p.pos = save
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if !matched {
|
||
|
|
p.removeDelim(opener)
|
||
|
|
p.push(&inline{kind: inlText, literal: "]"})
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
p.processEmphasis(opener)
|
||
|
|
var children []*inline
|
||
|
|
for n := opener.node.next; n != nil; n = n.next {
|
||
|
|
children = append(children, n)
|
||
|
|
}
|
||
|
|
kind := inlLink
|
||
|
|
if opener.image {
|
||
|
|
kind = inlImage
|
||
|
|
}
|
||
|
|
node := &inline{kind: kind, dest: dest, title: title, hasTitle: hasTitle, children: children}
|
||
|
|
if opener.node.prev != nil {
|
||
|
|
opener.node.prev.next = node
|
||
|
|
} else {
|
||
|
|
p.first = node
|
||
|
|
}
|
||
|
|
node.prev = opener.node.prev
|
||
|
|
node.next = nil
|
||
|
|
p.last = node
|
||
|
|
p.removeDelim(opener)
|
||
|
|
if !opener.image {
|
||
|
|
// Links may not nest in links; image brackets stay open.
|
||
|
|
for d := opener.prev; d != nil; d = d.prev {
|
||
|
|
if d.isBracket && !d.image {
|
||
|
|
d.active = false
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// referenceLabel reads the label of a reference link after the closing
|
||
|
|
// bracket: an explicit label in brackets, the collapsed empty brackets, or
|
||
|
|
// the shortcut label taken from the link text itself.
|
||
|
|
func (p *inlineParser) referenceLabel(opener *delimiter, closerIdx int) ([]byte, bool) {
|
||
|
|
if p.pos < len(p.src) && p.src[p.pos] == '[' {
|
||
|
|
if p.pos+1 < len(p.src) && p.src[p.pos+1] == ']' {
|
||
|
|
p.pos += 2
|
||
|
|
return p.src[opener.srcPos:closerIdx], true
|
||
|
|
}
|
||
|
|
j := p.pos + 1
|
||
|
|
for j < len(p.src) {
|
||
|
|
if p.src[j] == '\\' && j+1 < len(p.src) {
|
||
|
|
j += 2
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
if p.src[j] == ']' {
|
||
|
|
break
|
||
|
|
}
|
||
|
|
j++
|
||
|
|
}
|
||
|
|
if j < len(p.src) {
|
||
|
|
label := p.src[p.pos+1 : j]
|
||
|
|
p.pos = j + 1
|
||
|
|
return label, true
|
||
|
|
}
|
||
|
|
return nil, false
|
||
|
|
}
|
||
|
|
return p.src[opener.srcPos:closerIdx], true
|
||
|
|
}
|
||
|
|
|
||
|
|
// scanInlineSpec parses the destination and optional title of an inline
|
||
|
|
// link, starting after the opening parenthesis.
|
||
|
|
func (p *inlineParser) scanInlineSpec() (string, string, bool, bool) {
|
||
|
|
p.skipWhitespace()
|
||
|
|
if p.pos < len(p.src) && p.src[p.pos] == ')' {
|
||
|
|
p.pos++
|
||
|
|
return "", "", false, true
|
||
|
|
}
|
||
|
|
d, n, ok := scanDestination(p.src[p.pos:])
|
||
|
|
if !ok {
|
||
|
|
return "", "", false, false
|
||
|
|
}
|
||
|
|
dest := unescapeText(string(d))
|
||
|
|
p.pos += n
|
||
|
|
p.skipWhitespace()
|
||
|
|
if p.pos < len(p.src) {
|
||
|
|
switch c := p.src[p.pos]; c {
|
||
|
|
case '"', '\'', '(':
|
||
|
|
title, ok := p.scanInlineTitle(c)
|
||
|
|
if !ok {
|
||
|
|
return "", "", false, false
|
||
|
|
}
|
||
|
|
p.skipWhitespace()
|
||
|
|
if p.pos < len(p.src) && p.src[p.pos] == ')' {
|
||
|
|
p.pos++
|
||
|
|
return dest, unescapeText(title), true, true
|
||
|
|
}
|
||
|
|
return "", "", false, false
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if p.pos < len(p.src) && p.src[p.pos] == ')' {
|
||
|
|
p.pos++
|
||
|
|
return dest, "", false, true
|
||
|
|
}
|
||
|
|
return "", "", false, false
|
||
|
|
}
|
||
|
|
|
||
|
|
// scanInlineTitle scans a title through its closing quote, line endings
|
||
|
|
// included, leaving the position past the closing quote.
|
||
|
|
func (p *inlineParser) scanInlineTitle(open byte) (string, bool) {
|
||
|
|
closer := open
|
||
|
|
if open == '(' {
|
||
|
|
closer = ')'
|
||
|
|
}
|
||
|
|
i := p.pos + 1
|
||
|
|
for i < len(p.src) {
|
||
|
|
c := p.src[i]
|
||
|
|
if c == '\\' && i+1 < len(p.src) {
|
||
|
|
i += 2
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
if c == closer {
|
||
|
|
title := string(p.src[p.pos+1 : i])
|
||
|
|
p.pos = i + 1
|
||
|
|
return title, true
|
||
|
|
}
|
||
|
|
i++
|
||
|
|
}
|
||
|
|
return "", false
|
||
|
|
}
|
||
|
|
|
||
|
|
func (p *inlineParser) skipWhitespace() {
|
||
|
|
for p.pos < len(p.src) {
|
||
|
|
switch p.src[p.pos] {
|
||
|
|
case ' ', '\t', '\n':
|
||
|
|
p.pos++
|
||
|
|
default:
|
||
|
|
return
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// runeBefore returns the rune that ends just before the position, or a
|
||
|
|
// null rune at the start, which counts as whitespace.
|
||
|
|
func runeBefore(src []byte, pos int) rune {
|
||
|
|
if pos == 0 {
|
||
|
|
return 0
|
||
|
|
}
|
||
|
|
r, _ := utf8.DecodeLastRune(src[:pos])
|
||
|
|
return r
|
||
|
|
}
|
||
|
|
|
||
|
|
// runeAfter returns the rune that starts at the position, or a null rune
|
||
|
|
// at the end, which counts as whitespace.
|
||
|
|
func runeAfter(src []byte, pos int) rune {
|
||
|
|
if pos >= len(src) {
|
||
|
|
return 0
|
||
|
|
}
|
||
|
|
r, _ := utf8.DecodeRune(src[pos:])
|
||
|
|
return r
|
||
|
|
}
|
||
|
|
|
||
|
|
// classifyRune reports whether the rune is whitespace and whether it is
|
||
|
|
// punctuation, under the CommonMark definitions: Unicode whitespace, and
|
||
|
|
// ASCII or Unicode punctuation or symbol characters.
|
||
|
|
func classifyRune(r rune) (space, punct bool) {
|
||
|
|
if r == 0 {
|
||
|
|
return true, false
|
||
|
|
}
|
||
|
|
if r < utf8.RuneSelf {
|
||
|
|
space = r == ' ' || r == '\t' || r == '\n' || r == '\v' || r == '\f' || r == '\r'
|
||
|
|
punct = isASCIIPunct(byte(r))
|
||
|
|
return space, punct
|
||
|
|
}
|
||
|
|
return unicode.IsSpace(r), unicode.IsPunct(r) || unicode.IsSymbol(r)
|
||
|
|
}
|
||
|
|
|
||
|
|
// isASCIIPunct reports whether c is an ASCII punctuation character.
|
||
|
|
func isASCIIPunct(c byte) bool {
|
||
|
|
switch c {
|
||
|
|
case '!', '"', '#', '$', '%', '&', '\'', '(', ')', '*', '+', ',', '-',
|
||
|
|
'.', '/', ':', ';', '<', '=', '>', '?', '@', '[', '\\', ']', '^',
|
||
|
|
'_', '`', '{', '|', '}', '~':
|
||
|
|
return true
|
||
|
|
}
|
||
|
|
return false
|
||
|
|
}
|