package parser
import (
"fmt"
"strings"
"github.com/yuin/goldmark/ast"
"github.com/yuin/goldmark/text"
"github.com/yuin/goldmark/util"
)
var linkLabelStateKey = NewContextKey ()
type linkLabelState struct {
ast .BaseInline
Segment text .Segment
IsImage bool
Prev *linkLabelState
Next *linkLabelState
First *linkLabelState
Last *linkLabelState
}
func newLinkLabelState(segment text .Segment , isImage bool ) *linkLabelState {
return &linkLabelState {
Segment : segment ,
IsImage : isImage ,
}
}
func (s *linkLabelState ) Text (source []byte ) []byte {
return s .Segment .Value (source )
}
func (s *linkLabelState ) Dump (source []byte , level int ) {
fmt .Printf ("%slinkLabelState: \"%s\"\n" , strings .Repeat (" " , level ), s .Text (source ))
}
var kindLinkLabelState = ast .NewNodeKind ("LinkLabelState" )
func (s *linkLabelState ) Kind () ast .NodeKind {
return kindLinkLabelState
}
func linkLabelStateLength(v *linkLabelState ) int {
if v == nil || v .Last == nil || v .First == nil {
return 0
}
return v .Last .Segment .Stop - v .First .Segment .Start
}
func pushLinkLabelState(pc Context , v *linkLabelState ) {
tlist := pc .Get (linkLabelStateKey )
var list *linkLabelState
if tlist == nil {
list = v
v .First = v
v .Last = v
pc .Set (linkLabelStateKey , list )
} else {
list = tlist .(*linkLabelState )
l := list .Last
list .Last = v
l .Next = v
v .Prev = l
}
}
func removeLinkLabelState(pc Context , d *linkLabelState ) {
tlist := pc .Get (linkLabelStateKey )
var list *linkLabelState
if tlist == nil {
return
}
list = tlist .(*linkLabelState )
if d .Prev == nil {
list = d .Next
if list != nil {
list .First = d
list .Last = d .Last
list .Prev = nil
pc .Set (linkLabelStateKey , list )
} else {
pc .Set (linkLabelStateKey , nil )
}
} else {
d .Prev .Next = d .Next
if d .Next != nil {
d .Next .Prev = d .Prev
}
}
if list != nil && d .Next == nil {
list .Last = d .Prev
}
d .Next = nil
d .Prev = nil
d .First = nil
d .Last = nil
}
type linkParser struct {
}
var defaultLinkParser = &linkParser {}
func NewLinkParser () InlineParser {
return defaultLinkParser
}
func (s *linkParser ) Trigger () []byte {
return []byte {'!' , '[' , ']' }
}
var linkBottom = NewContextKey ()
func (s *linkParser ) Parse (parent ast .Node , block text .Reader , pc Context ) ast .Node {
line , segment := block .PeekLine ()
if line [0 ] == '!' {
if len (line ) > 1 && line [1 ] == '[' {
block .Advance (1 )
pushLinkBottom (pc )
return processLinkLabelOpen (block , segment .Start +1 , true , pc )
}
return nil
}
if line [0 ] == '[' {
pushLinkBottom (pc )
return processLinkLabelOpen (block , segment .Start , false , pc )
}
tlist := pc .Get (linkLabelStateKey )
if tlist == nil {
return nil
}
last := tlist .(*linkLabelState ).Last
if last == nil {
_ = popLinkBottom (pc )
return nil
}
block .Advance (1 )
removeLinkLabelState (pc , last )
if linkLabelStateLength (tlist .(*linkLabelState )) > 998 {
ast .MergeOrReplaceTextSegment (last .Parent (), last , last .Segment )
_ = popLinkBottom (pc )
return nil
}
if !last .IsImage && s .containsLink (last ) {
ast .MergeOrReplaceTextSegment (last .Parent (), last , last .Segment )
_ = popLinkBottom (pc )
return nil
}
c := block .Peek ()
l , pos := block .Position ()
var link *ast .Link
var hasValue bool
if c == '(' {
link = s .parseLink (parent , last , block , pc )
} else if c == '[' {
link , hasValue = s .parseReferenceLink (parent , last , block , pc )
if link == nil && hasValue {
ast .MergeOrReplaceTextSegment (last .Parent (), last , last .Segment )
_ = popLinkBottom (pc )
return nil
}
}
if link == nil {
block .SetPosition (l , pos )
ssegment := text .NewSegment (last .Segment .Stop , segment .Start )
maybeReference := block .Value (ssegment )
if len (maybeReference ) > 999 {
ast .MergeOrReplaceTextSegment (last .Parent (), last , last .Segment )
_ = popLinkBottom (pc )
return nil
}
ref , ok := pc .Reference (util .ToLinkReference (maybeReference ))
if !ok {
ast .MergeOrReplaceTextSegment (last .Parent (), last , last .Segment )
_ = popLinkBottom (pc )
return nil
}
link = ast .NewLink ()
s .processLinkLabel (parent , link , last , pc )
link .Title = ref .Title ()
link .Destination = ref .Destination ()
}
if last .IsImage {
last .Parent ().RemoveChild (last .Parent (), last )
return ast .NewImage (link )
}
last .Parent ().RemoveChild (last .Parent (), last )
return link
}
func (s *linkParser ) containsLink (n ast .Node ) bool {
if n == nil {
return false
}
for c := n ; c != nil ; c = c .NextSibling () {
if _ , ok := c .(*ast .Link ); ok {
return true
}
if s .containsLink (c .FirstChild ()) {
return true
}
}
return false
}
func processLinkLabelOpen(block text .Reader , pos int , isImage bool , pc Context ) *linkLabelState {
start := pos
if isImage {
start --
}
state := newLinkLabelState (text .NewSegment (start , pos +1 ), isImage )
pushLinkLabelState (pc , state )
block .Advance (1 )
return state
}
func (s *linkParser ) processLinkLabel (parent ast .Node , link *ast .Link , last *linkLabelState , pc Context ) {
bottom := popLinkBottom (pc )
ProcessDelimiters (bottom , pc )
for c := last .NextSibling (); c != nil ; {
next := c .NextSibling ()
parent .RemoveChild (parent , c )
link .AppendChild (link , c )
c = next
}
}
var linkFindClosureOptions text .FindClosureOptions = text .FindClosureOptions {
Nesting : false ,
Newline : true ,
Advance : true ,
}
func (s *linkParser ) parseReferenceLink (parent ast .Node , last *linkLabelState ,
block text .Reader , pc Context ) (*ast .Link , bool ) {
_ , orgpos := block .Position ()
block .Advance (1 )
segments , found := block .FindClosure ('[' , ']' , linkFindClosureOptions )
if !found {
return nil , false
}
var maybeReference []byte
if segments .Len () == 1 {
maybeReference = block .Value (segments .At (0 ))
} else {
maybeReference = []byte {}
for i := 0 ; i < segments .Len (); i ++ {
s := segments .At (i )
maybeReference = append (maybeReference , block .Value (s )...)
}
}
if util .IsBlank (maybeReference ) {
s := text .NewSegment (last .Segment .Stop , orgpos .Start -1 )
maybeReference = block .Value (s )
}
if len (maybeReference ) > 999 {
return nil , true
}
ref , ok := pc .Reference (util .ToLinkReference (maybeReference ))
if !ok {
return nil , true
}
link := ast .NewLink ()
s .processLinkLabel (parent , link , last , pc )
link .Title = ref .Title ()
link .Destination = ref .Destination ()
return link , true
}
func (s *linkParser ) parseLink (parent ast .Node , last *linkLabelState , block text .Reader , pc Context ) *ast .Link {
block .Advance (1 )
block .SkipSpaces ()
var title []byte
var destination []byte
var ok bool
if block .Peek () == ')' {
block .Advance (1 )
} else {
destination , ok = parseLinkDestination (block )
if !ok {
return nil
}
block .SkipSpaces ()
if block .Peek () == ')' {
block .Advance (1 )
} else {
title , ok = parseLinkTitle (block )
if !ok {
return nil
}
block .SkipSpaces ()
if block .Peek () == ')' {
block .Advance (1 )
} else {
return nil
}
}
}
link := ast .NewLink ()
s .processLinkLabel (parent , link , last , pc )
link .Destination = destination
link .Title = title
return link
}
func parseLinkDestination(block text .Reader ) ([]byte , bool ) {
block .SkipSpaces ()
line , _ := block .PeekLine ()
if block .Peek () == '<' {
i := 1
for i < len (line ) {
c := line [i ]
if c == '\\' && i < len (line )-1 && util .IsPunct (line [i +1 ]) {
i += 2
continue
} else if c == '>' {
block .Advance (i + 1 )
return line [1 :i ], true
}
i ++
}
return nil , false
}
opened := 0
i := 0
for i < len (line ) {
c := line [i ]
if c == '\\' && i < len (line )-1 && util .IsPunct (line [i +1 ]) {
i += 2
continue
} else if c == '(' {
opened ++
} else if c == ')' {
opened --
if opened < 0 {
break
}
} else if util .IsSpace (c ) {
break
}
i ++
}
block .Advance (i )
return line [:i ], len (line [:i ]) != 0
}
func parseLinkTitle(block text .Reader ) ([]byte , bool ) {
block .SkipSpaces ()
opener := block .Peek ()
if opener != '"' && opener != '\'' && opener != '(' {
return nil , false
}
closer := opener
if opener == '(' {
closer = ')'
}
block .Advance (1 )
segments , found := block .FindClosure (opener , closer , linkFindClosureOptions )
if found {
if segments .Len () == 1 {
return block .Value (segments .At (0 )), true
}
var title []byte
for i := 0 ; i < segments .Len (); i ++ {
s := segments .At (i )
title = append (title , block .Value (s )...)
}
return title , true
}
return nil , false
}
func pushLinkBottom(pc Context ) {
bottoms := pc .Get (linkBottom )
b := pc .LastDelimiter ()
if bottoms == nil {
pc .Set (linkBottom , b )
return
}
if s , ok := bottoms .([]ast .Node ); ok {
pc .Set (linkBottom , append (s , b ))
return
}
pc .Set (linkBottom , []ast .Node {bottoms .(ast .Node ), b })
}
func popLinkBottom(pc Context ) ast .Node {
bottoms := pc .Get (linkBottom )
if bottoms == nil {
return nil
}
if v , ok := bottoms .(ast .Node ); ok {
pc .Set (linkBottom , nil )
return v
}
s := bottoms .([]ast .Node )
v := s [len (s )-1 ]
n := s [0 : len (s )-1 ]
switch len (n ) {
case 0 :
pc .Set (linkBottom , nil )
case 1 :
pc .Set (linkBottom , n [0 ])
default :
pc .Set (linkBottom , s [0 :len (s )-1 ])
}
return v
}
func (s *linkParser ) CloseBlock (parent ast .Node , block text .Reader , pc Context ) {
pc .Set (linkBottom , nil )
tlist := pc .Get (linkLabelStateKey )
if tlist == nil {
return
}
for s := tlist .(*linkLabelState ); s != nil ; {
next := s .Next
removeLinkLabelState (pc , s )
s .Parent ().ReplaceChild (s .Parent (), s , ast .NewTextSegment (s .Segment ))
s = next
}
}
The pages are generated with Golds v0.8.2 . (GOOS=linux GOARCH=amd64)
Golds is a Go 101 project developed by Tapir Liu .
PR and bug reports are welcome and can be submitted to the issue list .
Please follow @zigo_101 (reachable from the left QR code) to get the latest news of Golds .