all repos — searchix @ 93b800b4862d5c7e0e621647d78b79416a9f3101

Search engine for NixOS, nix-darwin, home-manager and NUR users

feat: create option link parser Fixes: https://todo.sr.ht/~alanpearce/searchix/22

Alan Pearce
commit

93b800b4862d5c7e0e621647d78b79416a9f3101

parent

ad670006a58faa22a183212f3b68189b6ab8d5fa

M internal/nixdocs/nixdocs.gointernal/nixdocs/nixdocs.go
@@ -3,10 +3,13 @@
import ( "github.com/yuin/goldmark" "github.com/yuin/goldmark/extension" + + "go.alanpearce.eu/searchix/internal/nixdocs/optlink" ) func WithNixDocsExtensions() goldmark.Option { return goldmark.WithExtensions( extension.NewLinkify(), + optlink.New(), ) }
A internal/nixdocs/optlink/example/main.go
@@ -0,0 +1,43 @@
+package main + +import ( + "bytes" + "fmt" + "os" + + "github.com/yuin/goldmark" + "go.alanpearce.eu/searchix/internal/nixdocs/optlink" +) + +func main() { + // Create a new Goldmark instance with our optlink extension + markdown := goldmark.New( + goldmark.WithExtensions( + optlink.New(), + ), + ) + + // Sample markdown content with option links + src := []byte(` +# Example using the OptLink Extension + +This is a regular paragraph with an empty link that points to an option: [](#opt-systemd.sysusers.enable) + +You can use multiple option links in the same document: +- [](#opt-networking.firewall.enable) +- [](#opt-services.openssh.enable) +- [](#opt-users.users.<n>.hashedPassword) + +Regular links still work normally: [NixOS](https://nixos.org) +`) + + // Convert the markdown to HTML + var buf bytes.Buffer + if err := markdown.Convert(src, &buf); err != nil { + fmt.Fprintf(os.Stderr, "Error converting markdown: %v\n", err) + os.Exit(1) + } + + // Print the resulting HTML + fmt.Println(buf.String()) +}
A internal/nixdocs/optlink/optlink.go
@@ -0,0 +1,174 @@
+//nolint:wrapcheck +package optlink + +import ( + "html" + "net/url" + "regexp" + + "github.com/yuin/goldmark" + "github.com/yuin/goldmark/ast" + "github.com/yuin/goldmark/parser" + "github.com/yuin/goldmark/renderer" + ghtml "github.com/yuin/goldmark/renderer/html" + "github.com/yuin/goldmark/text" + "github.com/yuin/goldmark/util" +) + +// Node represents an option link node in markdown AST. +type Node struct { + ast.BaseInline + Option []byte +} + +// Inline implements ast.Inline interface. +func (n *Node) Inline() bool { + return true +} + +// Kind implements ast.Node.Kind interface. +func (n *Node) Kind() ast.NodeKind { + return KindOptLink +} + +// Dump implements ast.Node.Dump interface. +func (n *Node) Dump(source []byte, level int) { + ast.DumpHelper(n, source, level, map[string]string{ + "Option": string(n.Option), + }, nil) +} + +// KindOptLink is a NodeKind for option link nodes. +var KindOptLink = ast.NewNodeKind("OptLink") + +// Parser is a Goldmark inline parser for parsing option link nodes. +// +// Option links have the format [](#opt-option.name) which will be rendered as +// a link to the option with the name. +type Parser struct{} + +var _ parser.InlineParser = (*Parser)(nil) + +var openBracket = byte('[') + +// Trigger reports characters that trigger this parser. +func (*Parser) Trigger() []byte { + return []byte{openBracket} +} + +// optLinkPattern matches [](#opt-option.name) +var optLinkPattern = regexp.MustCompile(`^\[\]\(#opt-([^)]+)\)`) + +// Parse parses an option link node. +func (p *Parser) Parse(_ ast.Node, block text.Reader, _ parser.Context) ast.Node { + line, segment := block.PeekLine() + + // Check if we have enough characters for [](#opt- + if len(line) < 8 || line[0] != openBracket { + return nil + } + + // Use regex to match the pattern [](#opt-option.name) + matches := optLinkPattern.FindSubmatch(line) + if matches == nil { + return nil + } + + // Extract the option name from the match + optionName := matches[1] + if len(optionName) == 0 { + return nil + } + + // Create the node + n := &Node{ + Option: optionName, + } + + // Set the text segment to include the entire [](#opt-option.name) text + matchLength := len(matches[0]) + textSegment := text.NewSegment(segment.Start, segment.Start+matchLength) + n.AppendChild(n, ast.NewTextSegment(textSegment)) + + // Advance the reader past this node + block.Advance(matchLength) + + return n +} + +// HTMLRenderer is a renderer for option link nodes. +type HTMLRenderer struct { + ghtml.Config +} + +// NewHTMLRenderer returns a new HTMLRenderer. +func NewHTMLRenderer(opts ...ghtml.Option) renderer.NodeRenderer { + r := &HTMLRenderer{ + Config: ghtml.NewConfig(), + } + for _, opt := range opts { + opt.SetHTMLOption(&r.Config) + } + + return r +} + +// RegisterFuncs implements renderer.NodeRenderer.RegisterFuncs. +func (r *HTMLRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) { + reg.Register(KindOptLink, r.renderOptLink) +} + +func (r *HTMLRenderer) renderOptLink( + w util.BufWriter, + source []byte, //nolint:revive + node ast.Node, + entering bool, +) (ast.WalkStatus, error) { + if !entering { + return ast.WalkContinue, nil + } + + n := node.(*Node) + optionName := string(n.Option) + + if _, err := w.WriteString( + `<a class="option" href="/?query=` + url.QueryEscape(optionName) + `">`, + ); err != nil { + return ast.WalkStop, err + } + + if _, err := w.WriteString(html.EscapeString(optionName)); err != nil { + return ast.WalkStop, err + } + + if _, err := w.WriteString("</a>"); err != nil { + return ast.WalkStop, err + } + + return ast.WalkSkipChildren, nil +} + +// Extension is a goldmark extension for option links. +type Extension struct{} + +// Extend implements goldmark.Extender.Extend. +func (e *Extension) Extend(m goldmark.Markdown) { + // Register the parser + m.Parser().AddOptions( + parser.WithInlineParsers( + util.Prioritized(new(Parser), 100), + ), + ) + + // Register the renderer + m.Renderer().AddOptions( + renderer.WithNodeRenderers( + util.Prioritized(NewHTMLRenderer(), 500), + ), + ) +} + +// New returns a new option link extension. +func New() goldmark.Extender { + return &Extension{} +}