From fbccdbc8e861c9a98fe876beb7330adecf35d2fa Mon Sep 17 00:00:00 2001 From: Lenny Chen Date: Tue, 2 Jun 2026 12:16:13 -0700 Subject: [PATCH 1/5] Escape MDX-incompatible patterns in gen-docs output The generated MDX docs contain patterns that break Docusaurus MDX compilation: heading IDs ({#id}), bare angle-bracket placeholders (), and curly braces in JSON examples within descriptions. Add escapeMDXDescription() to handle these cases in command descriptions, complementing the existing encodeJSONExample() that already handles option descriptions. Also fix the one heading ID instance in commands.yaml. Co-Authored-By: Claude Opus 4.6 (1M context) --- internal/commandsgen/docs.go | 52 ++++++++++++++++++++++++++- internal/commandsgen/docs_test.go | 56 ++++++++++++++++++++++++++++++ internal/temporalcli/commands.yaml | 4 +-- 3 files changed, 109 insertions(+), 3 deletions(-) create mode 100644 internal/commandsgen/docs_test.go diff --git a/internal/commandsgen/docs.go b/internal/commandsgen/docs.go index b3857503e..b493be526 100644 --- a/internal/commandsgen/docs.go +++ b/internal/commandsgen/docs.go @@ -92,7 +92,7 @@ func (w *docWriter) writeSubcommand(c *Command) { fileName := c.fileName() prefix := strings.Repeat("#", c.depth()) w.fileMap[fileName].WriteString(prefix + " " + c.leafName() + "\n\n") - w.fileMap[fileName].WriteString(c.Description + "\n\n") + w.fileMap[fileName].WriteString(escapeMDXDescription(c.Description) + "\n\n") if w.isLeafCommand(c) { // gather options from command and all options available from parent commands @@ -262,3 +262,53 @@ func encodeJSONExample(v string) string { v = re.ReplaceAllString(v, "`$1`") return v } + +var ( + reAngleBracketPlaceholder = regexp.MustCompile(`<([a-z][a-z0-9_:-]+)>`) + reHeadingID = regexp.MustCompile(`^(#{1,6}\s+.+?)\s+\{#[\w-]+\}\s*$`) + reJSONInSingleQuotes = regexp.MustCompile(`'([^']*\{[^}]*\}[^']*)'`) +) + +// escapeMDXDescription escapes patterns in command descriptions that are +// valid Markdown but break MDX compilation: heading IDs ({#id}), bare +// angle-bracket placeholders (), and curly braces in JSON examples. +// Code fences are left untouched. +func escapeMDXDescription(desc string) string { + lines := strings.Split(desc, "\n") + var result []string + inCodeBlock := false + + for _, line := range lines { + trimmed := strings.TrimSpace(line) + if strings.HasPrefix(trimmed, "```") { + inCodeBlock = !inCodeBlock + result = append(result, line) + continue + } + if inCodeBlock { + result = append(result, line) + continue + } + + // Strip {#custom-id} from headings; Docusaurus generates IDs from heading text. + line = reHeadingID.ReplaceAllString(line, "$1") + + // Escape patterns that MDX would parse as HTML tags. + // Split on backtick spans to avoid modifying inline code. + parts := strings.Split(line, "`") + for i := range parts { + if i%2 == 0 { // outside backticks + parts[i] = reAngleBracketPlaceholder.ReplaceAllString(parts[i], `\<$1\>`) + } + } + line = strings.Join(parts, "`") + + // Escape curly braces inside single-quoted JSON examples. + line = reJSONInSingleQuotes.ReplaceAllStringFunc(line, func(match string) string { + return strings.NewReplacer("{", `\{`, "}", `\}`).Replace(match) + }) + + result = append(result, line) + } + return strings.Join(result, "\n") +} diff --git a/internal/commandsgen/docs_test.go b/internal/commandsgen/docs_test.go new file mode 100644 index 000000000..f39a3e311 --- /dev/null +++ b/internal/commandsgen/docs_test.go @@ -0,0 +1,56 @@ +package commandsgen + +import "testing" + +func TestEscapeMDXDescription(t *testing.T) { + tests := []struct { + name string + input string + expected string + }{ + { + name: "heading ID stripped", + input: "### Resetting activities that heartbeat {#reset-heartbeats}", + expected: "### Resetting activities that heartbeat", + }, + { + name: "angle bracket placeholder escaped", + input: "Use --ca-certificate to set the cert.", + expected: `Use --ca-certificate \ to set the cert.`, + }, + { + name: "angle brackets inside backticks left alone", + input: "Use `--flag ` to set.", + expected: "Use `--flag ` to set.", + }, + { + name: "JSON in single quotes escaped", + input: `For example: 'YourKey={"your": "value"}'.`, + expected: `For example: 'YourKey=\{"your": "value"\}'.`, + }, + { + name: "code fence content untouched", + input: "text\n```\n--flag \n{#id}\n```\nmore text", + expected: "text\n```\n--flag \n{#id}\n```\nmore text", + }, + { + name: "plain text unchanged", + input: "This is a normal description with no special characters.", + expected: "This is a normal description with no special characters.", + }, + { + name: "multiple placeholders on one line", + input: "--limit --reason ", + expected: `--limit \ --reason \`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := escapeMDXDescription(tt.input) + if got != tt.expected { + t.Errorf("escapeMDXDescription(%q)\n got: %q\n want: %q", tt.input, got, tt.expected) + } + }) + } +} diff --git a/internal/temporalcli/commands.yaml b/internal/temporalcli/commands.yaml index 1d383d179..b0ebea8b9 100644 --- a/internal/temporalcli/commands.yaml +++ b/internal/temporalcli/commands.yaml @@ -560,7 +560,7 @@ commands: This restarts the activity as if it were first being scheduled. That is, it will reset both the number of attempts and the activity timeout, as well as, optionally, the - [heartbeat details](#reset-heartbeats). + [heartbeat details](#resetting-activities-that-heartbeat). If the activity may be executing (i.e. it has not yet timed out), the reset will take effect the next time it fails, heartbeats, or times out. @@ -576,7 +576,7 @@ commands: Either `--activity-id` (with `--workflow-id`) or `--query` must be specified. - ### Resetting activities that heartbeat {#reset-heartbeats} + ### Resetting activities that heartbeat Activities that heartbeat will receive a [Canceled failure](/references/failures#cancelled-failure) the next time they heartbeat after a reset. From 862ab5251cb97b3c791bcb191128d641cd7fa52e Mon Sep 17 00:00:00 2001 From: Lenny Chen Date: Tue, 2 Jun 2026 12:36:37 -0700 Subject: [PATCH 2/5] Remove test file for escapeMDXDescription Co-Authored-By: Claude Opus 4.6 (1M context) --- internal/commandsgen/docs_test.go | 56 ------------------------------- 1 file changed, 56 deletions(-) delete mode 100644 internal/commandsgen/docs_test.go diff --git a/internal/commandsgen/docs_test.go b/internal/commandsgen/docs_test.go deleted file mode 100644 index f39a3e311..000000000 --- a/internal/commandsgen/docs_test.go +++ /dev/null @@ -1,56 +0,0 @@ -package commandsgen - -import "testing" - -func TestEscapeMDXDescription(t *testing.T) { - tests := []struct { - name string - input string - expected string - }{ - { - name: "heading ID stripped", - input: "### Resetting activities that heartbeat {#reset-heartbeats}", - expected: "### Resetting activities that heartbeat", - }, - { - name: "angle bracket placeholder escaped", - input: "Use --ca-certificate to set the cert.", - expected: `Use --ca-certificate \ to set the cert.`, - }, - { - name: "angle brackets inside backticks left alone", - input: "Use `--flag ` to set.", - expected: "Use `--flag ` to set.", - }, - { - name: "JSON in single quotes escaped", - input: `For example: 'YourKey={"your": "value"}'.`, - expected: `For example: 'YourKey=\{"your": "value"\}'.`, - }, - { - name: "code fence content untouched", - input: "text\n```\n--flag \n{#id}\n```\nmore text", - expected: "text\n```\n--flag \n{#id}\n```\nmore text", - }, - { - name: "plain text unchanged", - input: "This is a normal description with no special characters.", - expected: "This is a normal description with no special characters.", - }, - { - name: "multiple placeholders on one line", - input: "--limit --reason ", - expected: `--limit \ --reason \`, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got := escapeMDXDescription(tt.input) - if got != tt.expected { - t.Errorf("escapeMDXDescription(%q)\n got: %q\n want: %q", tt.input, got, tt.expected) - } - }) - } -} From a6e15dfd8db997d27be1847ebf3d861765075fb3 Mon Sep 17 00:00:00 2001 From: Lenny Chen Date: Tue, 2 Jun 2026 12:42:36 -0700 Subject: [PATCH 3/5] Convert heading IDs to MDX comment syntax instead of stripping Docusaurus supports {/* #id */} for custom heading IDs in MDX. Convert {#id} to this format so custom anchors are preserved. Revert commands.yaml changes since the YAML can keep the standard {#id} syntax. Co-Authored-By: Claude Opus 4.6 (1M context) --- internal/commandsgen/docs.go | 6 +++--- internal/temporalcli/commands.yaml | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/internal/commandsgen/docs.go b/internal/commandsgen/docs.go index b493be526..caa6768ad 100644 --- a/internal/commandsgen/docs.go +++ b/internal/commandsgen/docs.go @@ -265,7 +265,7 @@ func encodeJSONExample(v string) string { var ( reAngleBracketPlaceholder = regexp.MustCompile(`<([a-z][a-z0-9_:-]+)>`) - reHeadingID = regexp.MustCompile(`^(#{1,6}\s+.+?)\s+\{#[\w-]+\}\s*$`) + reHeadingID = regexp.MustCompile(`^(#{1,6}\s+.+?)\s+\{#([\w-]+)\}\s*$`) reJSONInSingleQuotes = regexp.MustCompile(`'([^']*\{[^}]*\}[^']*)'`) ) @@ -290,8 +290,8 @@ func escapeMDXDescription(desc string) string { continue } - // Strip {#custom-id} from headings; Docusaurus generates IDs from heading text. - line = reHeadingID.ReplaceAllString(line, "$1") + // Convert {#custom-id} to MDX-compatible {/* #custom-id */} comment syntax. + line = reHeadingID.ReplaceAllString(line, "$1 {/* #$2 */}") // Escape patterns that MDX would parse as HTML tags. // Split on backtick spans to avoid modifying inline code. diff --git a/internal/temporalcli/commands.yaml b/internal/temporalcli/commands.yaml index b0ebea8b9..1d383d179 100644 --- a/internal/temporalcli/commands.yaml +++ b/internal/temporalcli/commands.yaml @@ -560,7 +560,7 @@ commands: This restarts the activity as if it were first being scheduled. That is, it will reset both the number of attempts and the activity timeout, as well as, optionally, the - [heartbeat details](#resetting-activities-that-heartbeat). + [heartbeat details](#reset-heartbeats). If the activity may be executing (i.e. it has not yet timed out), the reset will take effect the next time it fails, heartbeats, or times out. @@ -576,7 +576,7 @@ commands: Either `--activity-id` (with `--workflow-id`) or `--query` must be specified. - ### Resetting activities that heartbeat + ### Resetting activities that heartbeat {#reset-heartbeats} Activities that heartbeat will receive a [Canceled failure](/references/failures#cancelled-failure) the next time they heartbeat after a reset. From 37512d9ebe20948a326738937d30d6d0e1ca82f3 Mon Sep 17 00:00:00 2001 From: Lenny Chen Date: Tue, 2 Jun 2026 12:59:50 -0700 Subject: [PATCH 4/5] Fix encodeJSONExample regex and revert commands.yaml The encodeJSONExample regex was not matching patterns like 'YourKey={"your": "value"}' where the closing brace isn't at the end. Use a broader pattern that matches any single-quoted string containing curly braces. Revert commands.yaml since the heading ID conversion in escapeMDXDescription handles {#id} automatically. Co-Authored-By: Claude Opus 4.6 (1M context) --- internal/commandsgen/docs.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/commandsgen/docs.go b/internal/commandsgen/docs.go index caa6768ad..04eddf15a 100644 --- a/internal/commandsgen/docs.go +++ b/internal/commandsgen/docs.go @@ -258,7 +258,7 @@ func encodeJSONExample(v string) string { // example: 'YourKey={"your": "value"}' // results in an mdx acorn rendering error // and wrapping in backticks lets it render - re := regexp.MustCompile(`('[a-zA-Z0-9]*={.*}')`) + re := regexp.MustCompile(`('[^']*\{[^']*\}[^']*')`) v = re.ReplaceAllString(v, "`$1`") return v } From 258d2d6dfa43d29ee96867eb8255ff446b192be4 Mon Sep 17 00:00:00 2001 From: Lenny Chen Date: Thu, 4 Jun 2026 11:32:02 -0700 Subject: [PATCH 5/5] Align JSON escaping regex and add cross-reference comments Align reJSONInSingleQuotes with encodeJSONExample to use [^']* inside braces instead of [^}]*, so nested JSON like '{"a": {"b": "c"}}' is handled correctly. Add cross-reference comments between encodeJSONExample and escapeMDXDescription so maintainers understand why there are two escaping functions for the same class of issues. Co-Authored-By: Claude Opus 4.6 (1M context) --- internal/commandsgen/docs.go | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/internal/commandsgen/docs.go b/internal/commandsgen/docs.go index 04eddf15a..377f28681 100644 --- a/internal/commandsgen/docs.go +++ b/internal/commandsgen/docs.go @@ -254,10 +254,11 @@ func (w *docWriter) isLeafCommand(c *Command) bool { return true } +// encodeJSONExample wraps single-quoted JSON examples in backticks so MDX +// does not parse curly braces as JSX expressions. This handles option +// description table cells. See also escapeMDXDescription which handles +// the same class of issues for command description body text. func encodeJSONExample(v string) string { - // example: 'YourKey={"your": "value"}' - // results in an mdx acorn rendering error - // and wrapping in backticks lets it render re := regexp.MustCompile(`('[^']*\{[^']*\}[^']*')`) v = re.ReplaceAllString(v, "`$1`") return v @@ -266,13 +267,15 @@ func encodeJSONExample(v string) string { var ( reAngleBracketPlaceholder = regexp.MustCompile(`<([a-z][a-z0-9_:-]+)>`) reHeadingID = regexp.MustCompile(`^(#{1,6}\s+.+?)\s+\{#([\w-]+)\}\s*$`) - reJSONInSingleQuotes = regexp.MustCompile(`'([^']*\{[^}]*\}[^']*)'`) + reJSONInSingleQuotes = regexp.MustCompile(`'([^']*\{[^']*\}[^']*)'`) ) // escapeMDXDescription escapes patterns in command descriptions that are // valid Markdown but break MDX compilation: heading IDs ({#id}), bare // angle-bracket placeholders (), and curly braces in JSON examples. -// Code fences are left untouched. +// Code fences are left untouched. Inline backtick spans are also preserved, +// assuming balanced backticks. See also encodeJSONExample which handles +// the same class of issues for option description table cells. func escapeMDXDescription(desc string) string { lines := strings.Split(desc, "\n") var result []string