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(``) w.Write(util.EscapeHTML([]byte(display))) w.WriteString(``) } 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), )) }