all repos — searchix @ d2962720cfd4b4f022ccd564f39189db1ccfa0b7

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

feat: parse {var} in markdown documentation

Alan Pearce
commit

d2962720cfd4b4f022ccd564f39189db1ccfa0b7

parent

7ab69fb0411fc260e3f3ea519443abd87ace3a7e

M frontend/static/style.cssfrontend/static/style.css
@@ -208,6 +208,11 @@ cursor: pointer;
text-decoration: underline; } +var { + font-style: normal; + font-family: var(--mono-font); +} + @media only screen and (max-width: 600px) { header > nav { display: unset;
M internal/nixdocs/nixdocs.gointernal/nixdocs/nixdocs.go
@@ -9,6 +9,7 @@ "go.alanpearce.eu/searchix/internal/nixdocs/command"
"go.alanpearce.eu/searchix/internal/nixdocs/envvar" "go.alanpearce.eu/searchix/internal/nixdocs/option" "go.alanpearce.eu/searchix/internal/nixdocs/optlink" + "go.alanpearce.eu/searchix/internal/nixdocs/variable" ) func WithNixDocsExtensions() goldmark.Option {
@@ -19,5 +20,6 @@ optlink.New(),
option.New(), command.New(), envvar.New(), + variable.New(), ) }
A internal/nixdocs/variable/example/main.go
@@ -0,0 +1,44 @@
+package main + +import ( + "bytes" + "fmt" + "os" + + "github.com/yuin/goldmark" + "go.alanpearce.eu/searchix/internal/nixdocs/variable" +) + +func main() { + // Create a new Goldmark instance with our variable extension + md := goldmark.New( + goldmark.WithExtensions( + variable.New(), + ), + ) + + // Some example Markdown content with variable references + src := []byte(` +# Variable References Example + +Nix packages like {var}` + "`pkgs.virtualbox`" + ` and {var}` + "`pkgs.firefox`" + ` +can be installed in your system. + +The {var}` + "`services.postgresql.enable`" + ` option enables the PostgreSQL service. + +Configuration variables are often found in these contexts: +- {var}` + "`system.stateVersion`" + ` for system configuration +- {var}` + "`networking.hostName`" + ` for network settings +- {var}` + "`users.users.alice`" + ` for user accounts +`) + + // Convert the markdown to HTML + var buf bytes.Buffer + if err := md.Convert(src, &buf); err != nil { + fmt.Println("Conversion error:", err) + os.Exit(1) + } + + // Print the HTML output + fmt.Println(buf.String()) +}
A internal/nixdocs/variable/var.go
@@ -0,0 +1,172 @@
+//nolint:wrapcheck +package variable + +import ( + "bytes" + + "github.com/yuin/goldmark" + "github.com/yuin/goldmark/ast" + "github.com/yuin/goldmark/parser" + "github.com/yuin/goldmark/renderer" + "github.com/yuin/goldmark/renderer/html" + "github.com/yuin/goldmark/text" + "github.com/yuin/goldmark/util" +) + +// Node represents a variable reference node in markdown AST. +type Node struct { + ast.BaseInline + VarName []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 KindVar +} + +// Dump implements ast.Node.Dump interface. +func (n *Node) Dump(source []byte, level int) { + ast.DumpHelper(n, source, level, map[string]string{ + "VarName": string(n.VarName), + }, nil) +} + +// KindVar is a NodeKind for variable nodes. +var KindVar = ast.NewNodeKind("Var") + +// Parser is a Goldmark inline parser for parsing variable nodes. +// +// Variable references have the format {var}`pkgs.virtualbox` which will be rendered as +// a var HTML element with the variable name. +type Parser struct{} + +var _ parser.InlineParser = (*Parser)(nil) + +// Trigger reports characters that trigger this parser. +func (*Parser) Trigger() []byte { + return []byte{'{'} +} + +// Parse parses a variable 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 {var}` + if len(line) < 6 || line[0] != '{' { + return nil + } + + // Check for {var}` prefix + if !bytes.HasPrefix(line, []byte("{var}`")) { + return nil + } + + // Skip the {var}` prefix + line = line[6:] + start := segment.Start + 6 + + // Find the closing backtick + endPos := bytes.IndexByte(line, '`') + if endPos < 0 { + return nil + } + + // Extract the variable name + varName := line[:endPos] + if len(varName) == 0 { + return nil + } + + // Create the node + n := &Node{ + VarName: varName, + } + + // Set the text segment to include the entire {var}`name` text + textSegment := text.NewSegment(segment.Start, start+endPos+1) + n.AppendChild(n, ast.NewTextSegment(textSegment)) + + // Advance the reader past this node + block.Advance(6 + endPos + 1) // {var}` + name + ` + + return n +} + +// HTMLRenderer is a renderer for variable nodes. +type HTMLRenderer struct { + html.Config +} + +// NewHTMLRenderer returns a new HTMLRenderer. +func NewHTMLRenderer(opts ...html.Option) renderer.NodeRenderer { + r := &HTMLRenderer{ + Config: html.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(KindVar, r.renderVar) +} + +func (r *HTMLRenderer) renderVar( + w util.BufWriter, + source []byte, //nolint:revive + node ast.Node, + entering bool, +) (ast.WalkStatus, error) { + if !entering { + return ast.WalkContinue, nil + } + + n := node.(*Node) + + if _, err := w.WriteString("<var>"); err != nil { + return ast.WalkStop, err + } + + if _, err := w.Write(n.VarName); err != nil { + return ast.WalkStop, err + } + + if _, err := w.WriteString("</var>"); err != nil { + return ast.WalkStop, err + } + + return ast.WalkSkipChildren, nil +} + +// Extension is a goldmark extension for variable references. +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 variable extension. +func New() goldmark.Extender { + return &Extension{} +}
A internal/nixdocs/variable/variable_test.go
@@ -0,0 +1,78 @@
+package variable + +import ( + "bytes" + "testing" + + "github.com/yuin/goldmark" +) + +func TestVarExtension(t *testing.T) { + markdown := goldmark.New( + goldmark.WithExtensions( + New(), + ), + ) + + tests := []struct { + name string + input string + expected string + }{ + { + name: "basic variable reference", + input: "{var}`pkgs.virtualbox`", + expected: "<p><var>pkgs.virtualbox</var></p>\n", + }, + { + name: "variable reference in sentence", + input: "Use {var}`nixpkgs.lib.attrsets` to manipulate attribute sets.", + expected: "<p>Use <var>nixpkgs.lib.attrsets</var> to manipulate attribute sets.</p>\n", + }, + { + name: "multiple variable references", + input: "Both {var}`config.services.nginx` and {var}`config.services.postgresql` define service configurations.", + expected: "<p>Both <var>config.services.nginx</var> and <var>" + + "config.services.postgresql</var> define service configurations.</p>\n", + }, + { + name: "incomplete variable reference - no closing backtick", + input: "{var}`MISSING_BACKTICK", + expected: "<p>{var}`MISSING_BACKTICK</p>\n", + }, + { + name: "incomplete variable reference - empty variable name", + input: "{var}``", + expected: "<p>{var}``</p>\n", + }, + { + name: "variable reference with code block", + input: "Set {var}`pkgs.hello` in your `configuration.nix` file.", + expected: "<p>Set <var>pkgs.hello</var> " + + "in your <code>configuration.nix</code> file.</p>\n", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + var buf bytes.Buffer + if err := markdown.Convert([]byte(test.input), &buf); err != nil { + t.Fatalf("Failed to convert markdown: %v", err) + } + + if got := buf.String(); got != test.expected { + t.Errorf("Expected:\n%q\nGot:\n%q", test.expected, got) + } + }) + } +} + +func TestVarNodeKind(t *testing.T) { + n := &Node{} + if kind := n.Kind(); kind != KindVar { + t.Errorf("Expected node kind %v, got %v", KindVar, kind) + } + if !n.Inline() { + t.Error("Expected node to be inline") + } +}