144 lines
4.3 KiB
Go
144 lines
4.3 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"net/url"
|
|
"regexp"
|
|
"strings"
|
|
|
|
"github.com/yuin/goldmark"
|
|
"github.com/yuin/goldmark/ast"
|
|
"github.com/yuin/goldmark/parser"
|
|
"github.com/yuin/goldmark/renderer"
|
|
"github.com/yuin/goldmark/text"
|
|
"github.com/yuin/goldmark/util"
|
|
)
|
|
|
|
// wikiLinkRe matches [[query]] and [[query::display]] anchored at the start of
|
|
// the current inline reader. Query and display forbid newlines and brackets;
|
|
// the query is non-greedy so the first `::` separates query from display when
|
|
// both are present.
|
|
var wikiLinkRe = regexp.MustCompile(`^\[\[([^\[\]\n]+?)(?:::([^\[\]\n]+))?\]\]`)
|
|
|
|
// wikiLinkNode is the AST node produced by wikiLinkParser. Query is already
|
|
// normalized by wikiSearchQuery.
|
|
type wikiLinkNode struct {
|
|
ast.BaseInline
|
|
Query []byte
|
|
Display []byte
|
|
}
|
|
|
|
var kindWikiLink = ast.NewNodeKind("WikiLink")
|
|
|
|
func (n *wikiLinkNode) Kind() ast.NodeKind { return kindWikiLink }
|
|
|
|
func (n *wikiLinkNode) Dump(source []byte, level int) {
|
|
ast.DumpHelper(n, source, level, map[string]string{
|
|
"Query": string(n.Query),
|
|
"Display": string(n.Display),
|
|
}, nil)
|
|
}
|
|
|
|
// wikiSearchQuery normalizes the raw text inside [[...]] into a search query.
|
|
// A leading `/` marks a link written under the old path-based syntax; those
|
|
// collapse to their last segment ("/Trips/Japan 2024" -> "Japan 2024") so
|
|
// existing content keeps resolving without a rewrite. Everything else is free
|
|
// text and passes through with only surrounding space trimmed.
|
|
func wikiSearchQuery(raw string) string {
|
|
q := strings.TrimSpace(raw)
|
|
if !strings.HasPrefix(q, "/") {
|
|
return q
|
|
}
|
|
trimmed := strings.Trim(q, "/")
|
|
if i := strings.LastIndex(trimmed, "/"); i >= 0 {
|
|
return strings.TrimSpace(trimmed[i+1:])
|
|
}
|
|
return trimmed
|
|
}
|
|
|
|
// wikiSearchHref builds the search-page URL for a query. Escaping leaves only
|
|
// URL-safe bytes, so the result needs no further HTML escaping in an href.
|
|
func wikiSearchHref(query string) string {
|
|
return "/?q=" + url.QueryEscape(query)
|
|
}
|
|
|
|
type wikiLinkParser struct{}
|
|
|
|
func (p *wikiLinkParser) Trigger() []byte { return []byte{'['} }
|
|
|
|
func (p *wikiLinkParser) Parse(parent ast.Node, block text.Reader, pc parser.Context) ast.Node {
|
|
line, _ := block.PeekLine()
|
|
if len(line) < 4 || line[0] != '[' || line[1] != '[' {
|
|
return nil
|
|
}
|
|
m := wikiLinkRe.FindSubmatchIndex(line)
|
|
if m == nil {
|
|
return nil
|
|
}
|
|
// A query that normalizes to nothing (e.g. a legacy "[[/]]" root link) is
|
|
// not a link; leave the source text alone.
|
|
query := wikiSearchQuery(string(line[m[2]:m[3]]))
|
|
if query == "" {
|
|
return nil
|
|
}
|
|
var display []byte
|
|
if m[4] != -1 {
|
|
display = bytes.TrimSpace(line[m[4]:m[5]])
|
|
}
|
|
block.Advance(m[1])
|
|
return &wikiLinkNode{
|
|
Query: []byte(query),
|
|
Display: append([]byte(nil), display...),
|
|
}
|
|
}
|
|
|
|
type wikiLinkRenderer struct{}
|
|
|
|
func (r *wikiLinkRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
|
|
reg.Register(kindWikiLink, r.render)
|
|
}
|
|
|
|
func (r *wikiLinkRenderer) render(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
|
|
if !entering {
|
|
return ast.WalkContinue, nil
|
|
}
|
|
n := node.(*wikiLinkNode)
|
|
writeWikiSearchLink(w, string(n.Query), string(n.Display))
|
|
return ast.WalkContinue, nil
|
|
}
|
|
|
|
// writeWikiSearchLink renders the anchor for a [[query]] link: an href into the
|
|
// search page, labelled with the display override or the query itself. Nothing
|
|
// is resolved at render time — whether the query matches anything is the search
|
|
// page's business, so there is no broken state here.
|
|
func writeWikiSearchLink(w util.BufWriter, query, display string) {
|
|
if display == "" {
|
|
display = query
|
|
}
|
|
w.WriteString(`<a href="`)
|
|
w.WriteString(wikiSearchHref(query))
|
|
w.WriteString(`">`)
|
|
w.Write(util.EscapeHTML([]byte(display)))
|
|
w.WriteString(`</a>`)
|
|
}
|
|
|
|
type wikiLinkExt struct{}
|
|
|
|
// newWikiLinkExt returns a goldmark extension that turns [[...]] tokens into
|
|
// links to the search page. It needs no wiki root — a query is never resolved
|
|
// against the filesystem.
|
|
func newWikiLinkExt() goldmark.Extender {
|
|
return &wikiLinkExt{}
|
|
}
|
|
|
|
func (e *wikiLinkExt) Extend(m goldmark.Markdown) {
|
|
// Priority 199 — one higher than the default link parser (200) so
|
|
// [[...]] is consumed before the default parser sees the outer `[`.
|
|
m.Parser().AddOptions(parser.WithInlineParsers(
|
|
util.Prioritized(&wikiLinkParser{}, 199),
|
|
))
|
|
m.Renderer().AddOptions(renderer.WithNodeRenderers(
|
|
util.Prioritized(&wikiLinkRenderer{}, 500),
|
|
))
|
|
}
|