Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/docs/content/docs/cli/caddy/add.mdx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
title: stack caddy add
description: Link this project's generated Caddy site(s) into the global proxy
description: "Link this project's generated Caddy site(s) into the global proxy"
icon: Terminal
---

Expand Down
2 changes: 1 addition & 1 deletion apps/docs/content/docs/cli/caddy/remove.mdx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
title: stack caddy remove
description: Unlink this project's Caddy site(s) from the global proxy
description: "Unlink this project's Caddy site(s) from the global proxy"
icon: Terminal
---

Expand Down
2 changes: 1 addition & 1 deletion apps/docs/content/docs/cli/config/get.mdx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
title: stack config get
description: Get a configuration value by dot-path
description: "Get a configuration value by dot-path"
icon: Terminal
---

Expand Down
2 changes: 1 addition & 1 deletion apps/docs/content/docs/cli/config/set.mdx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
title: stack config set
description: Set a configuration value by dot-path
description: "Set a configuration value by dot-path"
icon: Terminal
---

Expand Down
2 changes: 1 addition & 1 deletion apps/docs/content/docs/cli/debug/index.mdx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
title: stack debug
description: Run stackpanel from source via 'go run'
description: "Run stackpanel from source via 'go run'"
icon: Terminal
---

Expand Down
2 changes: 1 addition & 1 deletion apps/docs/content/docs/cli/setup.mdx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
title: stack setup
description: Reconcile the project - report, confirm, apply
description: "Reconcile the project: report, confirm, apply"
icon: Terminal
---

Expand Down
2 changes: 1 addition & 1 deletion apps/docs/content/docs/reference/caddy.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ Stable port for Caddy. If null, computed from project-name.

Top-level domain for virtual hosts.

Virtual hosts use the format: .. For example, with project "myapp" and tld "localhost":
Virtual hosts use the format: \<app\>.\<project\>.\<tld\> For example, with project "myapp" and tld "localhost":

Change to "lan" or "local" for custom DNS setups.

Expand Down
2 changes: 1 addition & 1 deletion apps/docs/content/docs/reference/modules.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -312,7 +312,7 @@ Flake URL (e.g., "github:author/my-module")

## `modules.<name>.healthcheckModule`

Name of the doctor module that provides health checks for this module. This links to stackpanel.doctor.<name>.
Name of the doctor module that provides health checks for this module. This links to stackpanel.doctor.\<name\>.

| Property | Value |
|----------|-------|
Expand Down
2 changes: 1 addition & 1 deletion apps/docs/content/docs/reference/outputs.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ Output derivations to expose.

These will be available as:

Example: stackpanel.outputs = \{ my-docs = pkgs.runCommand "docs" \{\} "..."; my-script = pkgs.writeShellScriptBin "foo" "..."; # Grouped packages (available via nix run .#scripts.) scripts = \{ foo = pkgs.writeShellScriptBin "foo" "..."; \}; \};
Example: stackpanel.outputs = \{ my-docs = pkgs.runCommand "docs" \{\} "..."; my-script = pkgs.writeShellScriptBin "foo" "..."; # Grouped packages (available via nix run .#scripts.\<name\>) scripts = \{ foo = pkgs.writeShellScriptBin "foo" "..."; \}; \};

| Property | Value |
|----------|-------|
Expand Down
2 changes: 1 addition & 1 deletion apps/docs/content/docs/reference/panels.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ Help text shown below the field

## `panels.<name>.fields.*.editPath`

Dot-separated path within the app's config for writes. E.g., "go.mainPackage" tells the agent to patch apps..go.mainPackage in the data file.
Dot-separated path within the app's config for writes. E.g., "go.mainPackage" tells the agent to patch apps.\<appId\>.go.mainPackage in the data file.

| Property | Value |
|----------|-------|
Expand Down
2 changes: 1 addition & 1 deletion apps/docs/content/docs/reference/variables-backend.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ Secret storage backend. This is the single source of truth that controls how sec

Chamber service prefix. The full chamber service path is: \{service-prefix\}/\{env\}

For example, with prefix "darkmatter/stackpanel" and a variable /dev/DATABASE_URL: chamber write darkmatter/stackpanel/dev DATABASE_URL chamber exec darkmatter/stackpanel/dev --
For example, with prefix "darkmatter/stackpanel" and a variable /dev/DATABASE_URL: chamber write darkmatter/stackpanel/dev DATABASE_URL \<value\> chamber exec darkmatter/stackpanel/dev -- \<command\>

Defaults to "\{owner\}/\{repo\}" when project owner/repo are configured, otherwise falls back to the project name.

Expand Down
16 changes: 13 additions & 3 deletions apps/stackpanel-go/internal/docgen/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import (
// Cobra command data.
type CLICommandView struct {
Title string
Description string
Description string // YAML-escaped, frontmatter only
Long string
Usage string
Aliases []string
Expand Down Expand Up @@ -140,11 +140,21 @@ func GenerateCLIDocs(rootCmd *cobra.Command, outputDir string) error {
// escapeMDX escapes characters that MDX would interpret as JSX ({, }, <, >).
// Content inside fenced code blocks and inline code spans is left untouched
// since MDX treats those as literal text. Already-escaped sequences (preceded
// by backslash) are also preserved to avoid double-escaping.
// by backslash) are also preserved to avoid double-escaping. Leading YAML
// frontmatter is copied verbatim: it is parsed as YAML, not MDX, and `\{` or
// `\<` inside a double-quoted YAML string is an invalid escape sequence.
func escapeMDX(text string) string {
var result strings.Builder
result.Grow(len(text))

if rest, ok := strings.CutPrefix(text, "---\n"); ok {
if end := strings.Index(rest, "\n---\n"); end != -1 {
frontmatterLen := len("---\n") + end + len("\n---\n")
result.WriteString(text[:frontmatterLen])
text = text[frontmatterLen:]
}
}

lines := strings.Split(text, "\n")
inFencedBlock := false

Expand Down Expand Up @@ -275,7 +285,7 @@ func generateCommandDocs(
// Create view
view := CLICommandView{
Title: fmt.Sprintf("%s %s", cliName, pathPrefix),
Description: cmd.Short,
Description: escapeYAMLString(cmd.Short),
Long: long,
Usage: usage,
Aliases: cmd.Aliases,
Expand Down
18 changes: 13 additions & 5 deletions apps/stackpanel-go/internal/docgen/convert.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,14 @@ func mdNodeText(n ast.Node, source []byte) string {
buf.Write(t.Segment.Value(source))
}
}
case *ast.RawHTML:
// Keep inline HTML as literal text. Option descriptions are written
// for renderers with HTML disabled (nixos-render-docs), so `<name>`
// is a placeholder, not a tag. escapeMDX escapes it on write.
for i := 0; i < v.Segments.Len(); i++ {
seg := v.Segments.At(i)
buf.Write(seg.Value(source))
}
default:
// Recurse for other inline nodes (emphasis, strong, links, etc.)
buf.WriteString(mdNodeText(c, source))
Expand Down Expand Up @@ -314,16 +322,16 @@ func concatenateDocsToMdx(docs []ParsedDoc) string {
func escapeYAMLString(s string) string {
needsQuotes := strings.ContainsAny(s, `:{}[]&*#?|-<>=!%@\'"`)
if needsQuotes {
escaped := strings.ReplaceAll(s, `"`, `\"`)
escaped := strings.NewReplacer(`\`, `\\`, `"`, `\"`).Replace(s)
return `"` + escaped + `"`
}
return s
}

// formatDescription sanitizes an option description that may contain docbook/XML
// remnants from nixosOptionsDoc. Parses as markdown via goldmark and extracts
// clean text, which transparently drops any inline HTML tags while preserving
// code blocks and list structure.
// formatDescription flattens an option description from nixosOptionsDoc
// (CommonMark) into text for a reference page by parsing it with goldmark.
// Angle-bracket placeholders such as `<name>` survive as literal text (see
// mdNodeText); the page-level escapeMDX pass makes them safe for MDX.
func formatDescription(desc string) string {
if desc == "" {
return "_No description provided._"
Expand Down
73 changes: 71 additions & 2 deletions apps/stackpanel-go/internal/docgen/docgen_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"testing"

"github.com/spf13/cobra"
"gopkg.in/yaml.v3"
)

func TestModuleFromDeclaration(t *testing.T) {
Expand Down Expand Up @@ -307,9 +308,9 @@ func TestFormatDescription(t *testing.T) {
expected: "Enable the service",
},
{
name: "with xml tags",
name: "tag-like text is kept literally, not stripped",
input: "Enable <literal>foo</literal> service",
expected: "Enable foo service",
expected: "Enable <literal>foo</literal> service",
},
{
name: "with whitespace",
Expand Down Expand Up @@ -875,6 +876,74 @@ Example:
}
}

// TestGenerateOptionsDocsEscapesPlaceholders covers stackpanel-thq.23: an option
// description containing `<name>` must reach the written page escaped, not
// dropped (the old output) and not bare (which breaks the MDX build).
func TestGenerateOptionsDocsEscapesPlaceholders(t *testing.T) {
dir := t.TempDir()
optionsPath := filepath.Join(dir, "options.json")
optionsJSON := `{
"stackpanel.modules.<name>.healthcheckModule": {
"declarations": [],
"description": "Name of the doctor module that provides health checks for this module.\nThis links to stackpanel.doctor.<name>.\n",
"loc": ["stackpanel", "modules", "<name>", "healthcheckModule"],
"readOnly": false,
"type": "null or string"
}
}`
if err := os.WriteFile(optionsPath, []byte(optionsJSON), 0o644); err != nil {
t.Fatal(err)
}

outDir := filepath.Join(dir, "reference")
if err := generateOptionsDocs(optionsPath, outDir, filepath.Join(dir, "internal")); err != nil {
t.Fatalf("generateOptionsDocs failed: %v", err)
}

content, err := os.ReadFile(filepath.Join(outDir, "modules.mdx"))
if err != nil {
t.Fatalf("failed to read modules.mdx: %v", err)
}
s := string(content)

if !strings.Contains(s, `This links to stackpanel.doctor.\<name\>.`) {
t.Errorf("expected the <name> placeholder escaped in the description, got:\n%s", s)
}
// Inline code is literal in MDX, so the heading must stay unescaped.
if !strings.Contains(s, "## `modules.<name>.healthcheckModule`") {
t.Errorf("expected the option heading unchanged, got:\n%s", s)
}
}

// TestGenerateCLIDocsFrontmatterIsValidYAML: a command's Short lands in the
// page frontmatter, which the docs build parses as YAML, not MDX.
func TestGenerateCLIDocsFrontmatterIsValidYAML(t *testing.T) {
outputDir := t.TempDir()
short := `Reconcile the project: apply {changes} under C:\work`
rootCmd := &cobra.Command{Use: "testcli", Short: "Test CLI"}
rootCmd.AddCommand(&cobra.Command{Use: "setup", Short: short})

if err := GenerateCLIDocs(rootCmd, outputDir); err != nil {
t.Fatalf("GenerateCLIDocs failed: %v", err)
}
content, err := os.ReadFile(filepath.Join(outputDir, "setup.mdx"))
if err != nil {
t.Fatalf("failed to read setup.mdx: %v", err)
}

parts := strings.SplitN(string(content), "---\n", 3)
if len(parts) != 3 {
t.Fatalf("expected a frontmatter block, got:\n%s", content)
}
var fm struct{ Description string }
if err := yaml.Unmarshal([]byte(parts[1]), &fm); err != nil {
t.Fatalf("frontmatter is not valid YAML: %v\n%s", err, parts[1])
}
if fm.Description != short {
t.Errorf("description = %q, want %q", fm.Description, short)
}
}

func TestIsSeparatorLine(t *testing.T) {
tests := []struct {
input string
Expand Down
Loading