mirror of
https://github.com/42wim/matterbridge.git
synced 2024-11-24 03:32:02 -08:00
665 lines
16 KiB
Go
665 lines
16 KiB
Go
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
|
// See License.txt for license information.
|
|
|
|
package markdown
|
|
|
|
import (
|
|
"container/list"
|
|
"strings"
|
|
"unicode"
|
|
"unicode/utf8"
|
|
)
|
|
|
|
type Inline interface {
|
|
IsInline() bool
|
|
}
|
|
|
|
type inlineBase struct{}
|
|
|
|
func (inlineBase) IsInline() bool { return true }
|
|
|
|
type Text struct {
|
|
inlineBase
|
|
|
|
Text string
|
|
Range Range
|
|
}
|
|
|
|
type CodeSpan struct {
|
|
inlineBase
|
|
|
|
Code string
|
|
}
|
|
|
|
type HardLineBreak struct {
|
|
inlineBase
|
|
}
|
|
|
|
type SoftLineBreak struct {
|
|
inlineBase
|
|
}
|
|
|
|
type InlineLinkOrImage struct {
|
|
inlineBase
|
|
|
|
Children []Inline
|
|
|
|
RawDestination Range
|
|
|
|
markdown string
|
|
rawTitle string
|
|
}
|
|
|
|
func (i *InlineLinkOrImage) Destination() string {
|
|
return Unescape(i.markdown[i.RawDestination.Position:i.RawDestination.End])
|
|
}
|
|
|
|
func (i *InlineLinkOrImage) Title() string {
|
|
return Unescape(i.rawTitle)
|
|
}
|
|
|
|
type InlineLink struct {
|
|
InlineLinkOrImage
|
|
}
|
|
|
|
type InlineImage struct {
|
|
InlineLinkOrImage
|
|
}
|
|
|
|
type ReferenceLinkOrImage struct {
|
|
inlineBase
|
|
*ReferenceDefinition
|
|
|
|
Children []Inline
|
|
}
|
|
|
|
type ReferenceLink struct {
|
|
ReferenceLinkOrImage
|
|
}
|
|
|
|
type ReferenceImage struct {
|
|
ReferenceLinkOrImage
|
|
}
|
|
|
|
type Autolink struct {
|
|
inlineBase
|
|
|
|
Children []Inline
|
|
|
|
RawDestination Range
|
|
|
|
markdown string
|
|
}
|
|
|
|
func (i *Autolink) Destination() string {
|
|
destination := Unescape(i.markdown[i.RawDestination.Position:i.RawDestination.End])
|
|
|
|
if strings.HasPrefix(destination, "www") {
|
|
destination = "http://" + destination
|
|
}
|
|
|
|
return destination
|
|
}
|
|
|
|
type delimiterType int
|
|
|
|
const (
|
|
linkOpeningDelimiter delimiterType = iota
|
|
imageOpeningDelimiter
|
|
)
|
|
|
|
type delimiter struct {
|
|
Type delimiterType
|
|
IsInactive bool
|
|
TextNode int
|
|
Range Range
|
|
}
|
|
|
|
type inlineParser struct {
|
|
markdown string
|
|
ranges []Range
|
|
referenceDefinitions []*ReferenceDefinition
|
|
|
|
raw string
|
|
position int
|
|
inlines []Inline
|
|
delimiterStack *list.List
|
|
}
|
|
|
|
func newInlineParser(markdown string, ranges []Range, referenceDefinitions []*ReferenceDefinition) *inlineParser {
|
|
return &inlineParser{
|
|
markdown: markdown,
|
|
ranges: ranges,
|
|
referenceDefinitions: referenceDefinitions,
|
|
delimiterStack: list.New(),
|
|
}
|
|
}
|
|
|
|
func (p *inlineParser) parseBackticks() {
|
|
count := 1
|
|
for i := p.position + 1; i < len(p.raw) && p.raw[i] == '`'; i++ {
|
|
count++
|
|
}
|
|
opening := p.raw[p.position : p.position+count]
|
|
search := p.position + count
|
|
for search < len(p.raw) {
|
|
end := strings.Index(p.raw[search:], opening)
|
|
if end == -1 {
|
|
break
|
|
}
|
|
if search+end+count < len(p.raw) && p.raw[search+end+count] == '`' {
|
|
search += end + count
|
|
for search < len(p.raw) && p.raw[search] == '`' {
|
|
search++
|
|
}
|
|
continue
|
|
}
|
|
code := strings.Join(strings.Fields(p.raw[p.position+count:search+end]), " ")
|
|
p.position = search + end + count
|
|
p.inlines = append(p.inlines, &CodeSpan{
|
|
Code: code,
|
|
})
|
|
return
|
|
}
|
|
p.position += len(opening)
|
|
absPos := relativeToAbsolutePosition(p.ranges, p.position-len(opening))
|
|
p.inlines = append(p.inlines, &Text{
|
|
Text: opening,
|
|
Range: Range{absPos, absPos + len(opening)},
|
|
})
|
|
}
|
|
|
|
func (p *inlineParser) parseLineEnding() {
|
|
if p.position >= 1 && p.raw[p.position-1] == '\t' {
|
|
p.inlines = append(p.inlines, &HardLineBreak{})
|
|
} else if p.position >= 2 && p.raw[p.position-1] == ' ' && (p.raw[p.position-2] == '\t' || p.raw[p.position-1] == ' ') {
|
|
p.inlines = append(p.inlines, &HardLineBreak{})
|
|
} else {
|
|
p.inlines = append(p.inlines, &SoftLineBreak{})
|
|
}
|
|
p.position++
|
|
if p.position < len(p.raw) && p.raw[p.position] == '\n' {
|
|
p.position++
|
|
}
|
|
}
|
|
|
|
func (p *inlineParser) parseEscapeCharacter() {
|
|
if p.position+1 < len(p.raw) && isEscapableByte(p.raw[p.position+1]) {
|
|
absPos := relativeToAbsolutePosition(p.ranges, p.position+1)
|
|
p.inlines = append(p.inlines, &Text{
|
|
Text: string(p.raw[p.position+1]),
|
|
Range: Range{absPos, absPos + len(string(p.raw[p.position+1]))},
|
|
})
|
|
p.position += 2
|
|
} else {
|
|
absPos := relativeToAbsolutePosition(p.ranges, p.position)
|
|
p.inlines = append(p.inlines, &Text{
|
|
Text: `\`,
|
|
Range: Range{absPos, absPos + 1},
|
|
})
|
|
p.position++
|
|
}
|
|
}
|
|
|
|
func (p *inlineParser) parseText() {
|
|
if next := strings.IndexAny(p.raw[p.position:], "\r\n\\`&![]wW:"); next == -1 {
|
|
absPos := relativeToAbsolutePosition(p.ranges, p.position)
|
|
p.inlines = append(p.inlines, &Text{
|
|
Text: strings.TrimRightFunc(p.raw[p.position:], isWhitespace),
|
|
Range: Range{absPos, absPos + len(p.raw[p.position:])},
|
|
})
|
|
p.position = len(p.raw)
|
|
} else {
|
|
absPos := relativeToAbsolutePosition(p.ranges, p.position)
|
|
if p.raw[p.position+next] == '\r' || p.raw[p.position+next] == '\n' {
|
|
s := strings.TrimRightFunc(p.raw[p.position:p.position+next], isWhitespace)
|
|
p.inlines = append(p.inlines, &Text{
|
|
Text: s,
|
|
Range: Range{absPos, absPos + len(s)},
|
|
})
|
|
} else {
|
|
if next == 0 {
|
|
// Always read at least one character since 'w', 'W', and ':' may not actually match another
|
|
// type of node
|
|
next = 1
|
|
}
|
|
|
|
p.inlines = append(p.inlines, &Text{
|
|
Text: p.raw[p.position : p.position+next],
|
|
Range: Range{absPos, absPos + next},
|
|
})
|
|
}
|
|
p.position += next
|
|
}
|
|
}
|
|
|
|
func (p *inlineParser) parseLinkOrImageDelimiter() {
|
|
absPos := relativeToAbsolutePosition(p.ranges, p.position)
|
|
if p.raw[p.position] == '[' {
|
|
p.inlines = append(p.inlines, &Text{
|
|
Text: "[",
|
|
Range: Range{absPos, absPos + 1},
|
|
})
|
|
p.delimiterStack.PushBack(&delimiter{
|
|
Type: linkOpeningDelimiter,
|
|
TextNode: len(p.inlines) - 1,
|
|
Range: Range{p.position, p.position + 1},
|
|
})
|
|
p.position++
|
|
} else if p.raw[p.position] == '!' && p.position+1 < len(p.raw) && p.raw[p.position+1] == '[' {
|
|
p.inlines = append(p.inlines, &Text{
|
|
Text: "![",
|
|
Range: Range{absPos, absPos + 2},
|
|
})
|
|
p.delimiterStack.PushBack(&delimiter{
|
|
Type: imageOpeningDelimiter,
|
|
TextNode: len(p.inlines) - 1,
|
|
Range: Range{p.position, p.position + 2},
|
|
})
|
|
p.position += 2
|
|
} else {
|
|
p.inlines = append(p.inlines, &Text{
|
|
Text: "!",
|
|
Range: Range{absPos, absPos + 1},
|
|
})
|
|
p.position++
|
|
}
|
|
}
|
|
|
|
func (p *inlineParser) peekAtInlineLinkDestinationAndTitle(position int, isImage bool) (destination, title Range, end int, ok bool) {
|
|
if position >= len(p.raw) || p.raw[position] != '(' {
|
|
return
|
|
}
|
|
position++
|
|
|
|
destinationStart := nextNonWhitespace(p.raw, position)
|
|
if destinationStart >= len(p.raw) {
|
|
return
|
|
} else if p.raw[destinationStart] == ')' {
|
|
return Range{destinationStart, destinationStart}, Range{destinationStart, destinationStart}, destinationStart + 1, true
|
|
}
|
|
|
|
destination, end, ok = parseLinkDestination(p.raw, destinationStart)
|
|
if !ok {
|
|
return
|
|
}
|
|
position = end
|
|
|
|
if isImage && position < len(p.raw) && isWhitespaceByte(p.raw[position]) {
|
|
dimensionsStart := nextNonWhitespace(p.raw, position)
|
|
if dimensionsStart >= len(p.raw) {
|
|
return
|
|
}
|
|
|
|
if p.raw[dimensionsStart] == '=' {
|
|
// Read optional image dimensions even if we don't use them
|
|
_, end, ok = parseImageDimensions(p.raw, dimensionsStart)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
position = end
|
|
}
|
|
}
|
|
|
|
if position < len(p.raw) && isWhitespaceByte(p.raw[position]) {
|
|
titleStart := nextNonWhitespace(p.raw, position)
|
|
if titleStart >= len(p.raw) {
|
|
return
|
|
} else if p.raw[titleStart] == ')' {
|
|
return destination, Range{titleStart, titleStart}, titleStart + 1, true
|
|
}
|
|
|
|
if p.raw[titleStart] == '"' || p.raw[titleStart] == '\'' || p.raw[titleStart] == '(' {
|
|
title, end, ok = parseLinkTitle(p.raw, titleStart)
|
|
if !ok {
|
|
return
|
|
}
|
|
position = end
|
|
}
|
|
}
|
|
|
|
closingPosition := nextNonWhitespace(p.raw, position)
|
|
if closingPosition >= len(p.raw) || p.raw[closingPosition] != ')' {
|
|
return Range{}, Range{}, 0, false
|
|
}
|
|
|
|
return destination, title, closingPosition + 1, true
|
|
}
|
|
|
|
func (p *inlineParser) referenceDefinition(label string) *ReferenceDefinition {
|
|
clean := strings.Join(strings.Fields(label), " ")
|
|
for _, d := range p.referenceDefinitions {
|
|
if strings.EqualFold(clean, strings.Join(strings.Fields(d.Label()), " ")) {
|
|
return d
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (p *inlineParser) lookForLinkOrImage() {
|
|
for element := p.delimiterStack.Back(); element != nil; element = element.Prev() {
|
|
d := element.Value.(*delimiter)
|
|
if d.Type != imageOpeningDelimiter && d.Type != linkOpeningDelimiter {
|
|
continue
|
|
}
|
|
if d.IsInactive {
|
|
p.delimiterStack.Remove(element)
|
|
break
|
|
}
|
|
|
|
isImage := d.Type == imageOpeningDelimiter
|
|
|
|
var inline Inline
|
|
|
|
if destination, title, next, ok := p.peekAtInlineLinkDestinationAndTitle(p.position+1, isImage); ok {
|
|
destinationMarkdownPosition := relativeToAbsolutePosition(p.ranges, destination.Position)
|
|
linkOrImage := InlineLinkOrImage{
|
|
Children: append([]Inline(nil), p.inlines[d.TextNode+1:]...),
|
|
RawDestination: Range{destinationMarkdownPosition, destinationMarkdownPosition + destination.End - destination.Position},
|
|
markdown: p.markdown,
|
|
rawTitle: p.raw[title.Position:title.End],
|
|
}
|
|
if d.Type == imageOpeningDelimiter {
|
|
inline = &InlineImage{linkOrImage}
|
|
} else {
|
|
inline = &InlineLink{linkOrImage}
|
|
}
|
|
p.position = next
|
|
} else {
|
|
referenceLabel := ""
|
|
label, next, hasLinkLabel := parseLinkLabel(p.raw, p.position+1)
|
|
if hasLinkLabel && label.End > label.Position {
|
|
referenceLabel = p.raw[label.Position:label.End]
|
|
} else {
|
|
referenceLabel = p.raw[d.Range.End:p.position]
|
|
if !hasLinkLabel {
|
|
next = p.position + 1
|
|
}
|
|
}
|
|
if referenceLabel != "" {
|
|
if reference := p.referenceDefinition(referenceLabel); reference != nil {
|
|
linkOrImage := ReferenceLinkOrImage{
|
|
ReferenceDefinition: reference,
|
|
Children: append([]Inline(nil), p.inlines[d.TextNode+1:]...),
|
|
}
|
|
if d.Type == imageOpeningDelimiter {
|
|
inline = &ReferenceImage{linkOrImage}
|
|
} else {
|
|
inline = &ReferenceLink{linkOrImage}
|
|
}
|
|
p.position = next
|
|
}
|
|
}
|
|
}
|
|
|
|
if inline != nil {
|
|
if d.Type == imageOpeningDelimiter {
|
|
p.inlines = append(p.inlines[:d.TextNode], inline)
|
|
} else {
|
|
p.inlines = append(p.inlines[:d.TextNode], inline)
|
|
for element := element.Prev(); element != nil; element = element.Prev() {
|
|
if d := element.Value.(*delimiter); d.Type == linkOpeningDelimiter {
|
|
d.IsInactive = true
|
|
}
|
|
}
|
|
}
|
|
p.delimiterStack.Remove(element)
|
|
return
|
|
} else {
|
|
p.delimiterStack.Remove(element)
|
|
break
|
|
}
|
|
}
|
|
absPos := relativeToAbsolutePosition(p.ranges, p.position)
|
|
p.inlines = append(p.inlines, &Text{
|
|
Text: "]",
|
|
Range: Range{absPos, absPos + 1},
|
|
})
|
|
p.position++
|
|
}
|
|
|
|
func CharacterReference(ref string) string {
|
|
if ref == "" {
|
|
return ""
|
|
}
|
|
if ref[0] == '#' {
|
|
if len(ref) < 2 {
|
|
return ""
|
|
}
|
|
n := 0
|
|
if ref[1] == 'X' || ref[1] == 'x' {
|
|
if len(ref) < 3 {
|
|
return ""
|
|
}
|
|
for i := 2; i < len(ref); i++ {
|
|
if i > 9 {
|
|
return ""
|
|
}
|
|
d := ref[i]
|
|
switch {
|
|
case d >= '0' && d <= '9':
|
|
n = n*16 + int(d-'0')
|
|
case d >= 'a' && d <= 'f':
|
|
n = n*16 + 10 + int(d-'a')
|
|
case d >= 'A' && d <= 'F':
|
|
n = n*16 + 10 + int(d-'A')
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
} else {
|
|
for i := 1; i < len(ref); i++ {
|
|
if i > 8 || ref[i] < '0' || ref[i] > '9' {
|
|
return ""
|
|
}
|
|
n = n*10 + int(ref[i]-'0')
|
|
}
|
|
}
|
|
c := rune(n)
|
|
if c == '\u0000' || !utf8.ValidRune(c) {
|
|
return string(unicode.ReplacementChar)
|
|
}
|
|
return string(c)
|
|
}
|
|
if entity, ok := htmlEntities[ref]; ok {
|
|
return entity
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func (p *inlineParser) parseCharacterReference() {
|
|
absPos := relativeToAbsolutePosition(p.ranges, p.position)
|
|
p.position++
|
|
if semicolon := strings.IndexByte(p.raw[p.position:], ';'); semicolon == -1 {
|
|
p.inlines = append(p.inlines, &Text{
|
|
Text: "&",
|
|
Range: Range{absPos, absPos + 1},
|
|
})
|
|
} else if s := CharacterReference(p.raw[p.position : p.position+semicolon]); s != "" {
|
|
p.position += semicolon + 1
|
|
p.inlines = append(p.inlines, &Text{
|
|
Text: s,
|
|
Range: Range{absPos, absPos + len(s)},
|
|
})
|
|
} else {
|
|
p.inlines = append(p.inlines, &Text{
|
|
Text: "&",
|
|
Range: Range{absPos, absPos + 1},
|
|
})
|
|
}
|
|
}
|
|
|
|
func (p *inlineParser) parseAutolink(c rune) bool {
|
|
for element := p.delimiterStack.Back(); element != nil; element = element.Prev() {
|
|
d := element.Value.(*delimiter)
|
|
if !d.IsInactive {
|
|
return false
|
|
}
|
|
}
|
|
|
|
var link Range
|
|
if c == ':' {
|
|
var ok bool
|
|
link, ok = parseURLAutolink(p.raw, p.position)
|
|
|
|
if !ok {
|
|
return false
|
|
}
|
|
|
|
// Since the current position is at the colon, we have to rewind the parsing slightly so that
|
|
// we don't duplicate the URL scheme
|
|
rewind := strings.Index(p.raw[link.Position:link.End], ":")
|
|
if rewind != -1 {
|
|
lastInline := p.inlines[len(p.inlines)-1]
|
|
lastText, ok := lastInline.(*Text)
|
|
|
|
if !ok {
|
|
// This should never occur since parseURLAutolink will only return a non-empty value
|
|
// when the previous text ends in a valid URL protocol which would mean that the previous
|
|
// node is a Text node
|
|
return false
|
|
}
|
|
|
|
p.inlines = p.inlines[0 : len(p.inlines)-1]
|
|
p.inlines = append(p.inlines, &Text{
|
|
Text: lastText.Text[:len(lastText.Text)-rewind],
|
|
Range: Range{lastText.Range.Position, lastText.Range.End - rewind},
|
|
})
|
|
p.position -= rewind
|
|
}
|
|
} else if c == 'w' || c == 'W' {
|
|
var ok bool
|
|
link, ok = parseWWWAutolink(p.raw, p.position)
|
|
|
|
if !ok {
|
|
return false
|
|
}
|
|
}
|
|
|
|
linkMarkdownPosition := relativeToAbsolutePosition(p.ranges, link.Position)
|
|
linkRange := Range{linkMarkdownPosition, linkMarkdownPosition + link.End - link.Position}
|
|
|
|
p.inlines = append(p.inlines, &Autolink{
|
|
Children: []Inline{
|
|
&Text{
|
|
Text: p.raw[link.Position:link.End],
|
|
Range: linkRange,
|
|
},
|
|
},
|
|
RawDestination: linkRange,
|
|
markdown: p.markdown,
|
|
})
|
|
p.position += (link.End - link.Position)
|
|
|
|
return true
|
|
}
|
|
|
|
func (p *inlineParser) Parse() []Inline {
|
|
for _, r := range p.ranges {
|
|
p.raw += p.markdown[r.Position:r.End]
|
|
}
|
|
|
|
for p.position < len(p.raw) {
|
|
c, _ := utf8.DecodeRuneInString(p.raw[p.position:])
|
|
|
|
switch c {
|
|
case '\r', '\n':
|
|
p.parseLineEnding()
|
|
case '\\':
|
|
p.parseEscapeCharacter()
|
|
case '`':
|
|
p.parseBackticks()
|
|
case '&':
|
|
p.parseCharacterReference()
|
|
case '!', '[':
|
|
p.parseLinkOrImageDelimiter()
|
|
case ']':
|
|
p.lookForLinkOrImage()
|
|
case 'w', 'W', ':':
|
|
matched := p.parseAutolink(c)
|
|
|
|
if !matched {
|
|
p.parseText()
|
|
}
|
|
default:
|
|
p.parseText()
|
|
}
|
|
}
|
|
|
|
return p.inlines
|
|
}
|
|
|
|
func ParseInlines(markdown string, ranges []Range, referenceDefinitions []*ReferenceDefinition) (inlines []Inline) {
|
|
return newInlineParser(markdown, ranges, referenceDefinitions).Parse()
|
|
}
|
|
|
|
func MergeInlineText(inlines []Inline) []Inline {
|
|
var ret []Inline
|
|
for i, v := range inlines {
|
|
// always add first node
|
|
if i == 0 {
|
|
ret = append(ret, v)
|
|
continue
|
|
}
|
|
// not a text node? nothing to merge
|
|
text, ok := v.(*Text)
|
|
if !ok {
|
|
ret = append(ret, v)
|
|
continue
|
|
}
|
|
// previous node is not a text node? nothing to merge
|
|
prevText, ok := ret[len(ret)-1].(*Text)
|
|
if !ok {
|
|
ret = append(ret, v)
|
|
continue
|
|
}
|
|
// previous node is not right before this one
|
|
if prevText.Range.End != text.Range.Position {
|
|
ret = append(ret, v)
|
|
continue
|
|
}
|
|
// we have two consecutive text nodes
|
|
ret[len(ret)-1] = &Text{
|
|
Text: prevText.Text + text.Text,
|
|
Range: Range{prevText.Range.Position, text.Range.End},
|
|
}
|
|
}
|
|
return ret
|
|
}
|
|
|
|
func Unescape(markdown string) string {
|
|
ret := ""
|
|
|
|
position := 0
|
|
for position < len(markdown) {
|
|
c, cSize := utf8.DecodeRuneInString(markdown[position:])
|
|
|
|
switch c {
|
|
case '\\':
|
|
if position+1 < len(markdown) && isEscapableByte(markdown[position+1]) {
|
|
ret += string(markdown[position+1])
|
|
position += 2
|
|
} else {
|
|
ret += `\`
|
|
position++
|
|
}
|
|
case '&':
|
|
position++
|
|
if semicolon := strings.IndexByte(markdown[position:], ';'); semicolon == -1 {
|
|
ret += "&"
|
|
} else if s := CharacterReference(markdown[position : position+semicolon]); s != "" {
|
|
position += semicolon + 1
|
|
ret += s
|
|
} else {
|
|
ret += "&"
|
|
}
|
|
default:
|
|
ret += string(c)
|
|
position += cSize
|
|
}
|
|
}
|
|
|
|
return ret
|
|
}
|