|
- package parser
-
- import (
- "fmt"
- "regexp"
- "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 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{}
-
- // NewLinkParser return a new InlineParser that parses links.
- func NewLinkParser() InlineParser {
- return defaultLinkParser
- }
-
- func (s *linkParser) Trigger() []byte {
- return []byte{'!', '[', ']'}
- }
-
- var linkDestinationRegexp = regexp.MustCompile(`\s*([^\s].+)`)
- var linkTitleRegexp = regexp.MustCompile(`\s+(\)|["'\(].+)`)
- 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)
- pc.Set(linkBottom, pc.LastDelimiter())
- return processLinkLabelOpen(block, segment.Start+1, true, pc)
- }
- return nil
- }
- if line[0] == '[' {
- pc.Set(linkBottom, pc.LastDelimiter())
- return processLinkLabelOpen(block, segment.Start, false, pc)
- }
-
- // line[0] == ']'
- tlist := pc.Get(linkLabelStateKey)
- if tlist == nil {
- return nil
- }
- last := tlist.(*linkLabelState).Last
- if last == nil {
- return nil
- }
- block.Advance(1)
- removeLinkLabelState(pc, last)
- if s.containsLink(last) { // a link in a link text is not allowed
- ast.MergeOrReplaceTextSegment(last.Parent(), last, last.Segment)
- return nil
- }
-
- c := block.Peek()
- l, pos := block.Position()
- var link *ast.Link
- var hasValue bool
- if c == '(' { // normal link
- link = s.parseLink(parent, last, block, pc)
- } else if c == '[' { // reference link
- link, hasValue = s.parseReferenceLink(parent, last, block, pc)
- if link == nil && hasValue {
- ast.MergeOrReplaceTextSegment(last.Parent(), last, last.Segment)
- return nil
- }
- }
-
- if link == nil {
- // maybe shortcut reference link
- block.SetPosition(l, pos)
- ssegment := text.NewSegment(last.Segment.Stop, segment.Start)
- maybeReference := block.Value(ssegment)
- ref, ok := pc.Reference(util.ToLinkReference(maybeReference))
- if !ok {
- ast.MergeOrReplaceTextSegment(last.Parent(), last, last.Segment)
- 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(last *linkLabelState) bool {
- if last.IsImage {
- return false
- }
- var c ast.Node
- for c = last; c != nil; c = c.NextSibling() {
- if _, ok := c.(*ast.Link); ok {
- 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) {
- var bottom ast.Node
- if v := pc.Get(linkBottom); v != nil {
- bottom = v.(ast.Node)
- }
- pc.Set(linkBottom, nil)
- ProcessDelimiters(bottom, pc)
- for c := last.NextSibling(); c != nil; {
- next := c.NextSibling()
- parent.RemoveChild(parent, c)
- link.AppendChild(link, c)
- c = next
- }
- }
-
- func (s *linkParser) parseReferenceLink(parent ast.Node, last *linkLabelState, block text.Reader, pc Context) (*ast.Link, bool) {
- _, orgpos := block.Position()
- block.Advance(1) // skip '['
- line, segment := block.PeekLine()
- endIndex := util.FindClosure(line, '[', ']', false, true)
- if endIndex < 0 {
- return nil, false
- }
-
- block.Advance(endIndex + 1)
- ssegment := segment.WithStop(segment.Start + endIndex)
- maybeReference := block.Value(ssegment)
- if util.IsBlank(maybeReference) { // collapsed reference link
- ssegment = text.NewSegment(last.Segment.Stop, orgpos.Start-1)
- maybeReference = block.Value(ssegment)
- }
-
- 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) // skip '('
- block.SkipSpaces()
- var title []byte
- var destination []byte
- var ok bool
- if block.Peek() == ')' { // empty link like '[link]()'
- 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()
- buf := []byte{}
- if block.Peek() == '<' {
- i := 1
- for i < len(line) {
- c := line[i]
- if c == '\\' && i < len(line)-1 && util.IsPunct(line[i+1]) {
- buf = append(buf, '\\', line[i+1])
- i += 2
- continue
- } else if c == '>' {
- block.Advance(i + 1)
- return line[1:i], true
- }
- buf = append(buf, c)
- 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]) {
- buf = append(buf, '\\', line[i+1])
- i += 2
- continue
- } else if c == '(' {
- opened++
- } else if c == ')' {
- opened--
- if opened < 0 {
- break
- }
- } else if util.IsSpace(c) {
- break
- }
- buf = append(buf, c)
- 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 = ')'
- }
- savedLine, savedPosition := block.Position()
- var title []byte
- for i := 0; ; i++ {
- line, _ := block.PeekLine()
- if line == nil {
- block.SetPosition(savedLine, savedPosition)
- return nil, false
- }
- offset := 0
- if i == 0 {
- offset = 1
- }
- pos := util.FindClosure(line[offset:], opener, closer, false, true)
- if pos < 0 {
- title = append(title, line[offset:]...)
- block.AdvanceLine()
- continue
- }
- pos += offset + 1 // 1: closer
- block.Advance(pos)
- if i == 0 { // avoid allocating new slice
- return line[offset : pos-1], true
- }
- return append(title, line[offset:pos-1]...), true
- }
- }
-
- func (s *linkParser) CloseBlock(parent ast.Node, block text.Reader, pc Context) {
- 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
- }
- }
|