Skip to content

Commit 1c82087

Browse files
committed
chore: wrap CLI flags in backticks in Mintlify doc output
Mintlify's smart-typography renderer converts bare `--` to an em dash inside Markdown table cells, garbling flag names in the generated client_reference pages. Wrap every flag mention (--flag, -x) in backticks so the renderer treats them as inline code.
1 parent a2ee562 commit 1c82087

2 files changed

Lines changed: 151 additions & 0 deletions

File tree

internal/docgen/mintlify.go

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,10 @@ func (MintlifyFormatter) Synopsis(meta CommandMeta) string {
5858
func (MintlifyFormatter) FlagsSection(flags, inherited string) string {
5959
flags = linkifyKosliDocsURLs(flags)
6060
flags = escapeMintlifyProse(flags)
61+
flags = backtickFlags(flags)
6162
inherited = linkifyKosliDocsURLs(inherited)
6263
inherited = escapeMintlifyProse(inherited)
64+
inherited = backtickFlags(inherited)
6365
var b strings.Builder
6466
if flags != "" {
6567
b.WriteString("## Flags\n")
@@ -170,6 +172,71 @@ var htmlTags = map[string]bool{
170172
// single quotes are reserved for example/placeholder URLs.
171173
var singleQuotedURLPattern = regexp.MustCompile(`'(https?://[^\s']+)'`)
172174

175+
// flagPattern matches the body of a CLI flag like --name or -x. The caller
176+
// must check the preceding character to ensure it is a real flag boundary
177+
// (start-of-string or a non-word, non-slash, non-dash character).
178+
var flagPattern = regexp.MustCompile(`^-{1,2}[a-zA-Z][\w-]*`)
179+
180+
// backtickFlags wraps every CLI flag mention (--flag, -x) in backticks so
181+
// Mintlify's smart-typography renderer does not turn `--` into an em dash
182+
// inside Markdown table cells. Idempotent: flags already inside inline-code
183+
// spans or fenced code blocks are left alone.
184+
func backtickFlags(s string) string {
185+
parts := strings.Split(s, "```")
186+
for i := 0; i < len(parts); i += 2 {
187+
parts[i] = backtickFlagsFragment(parts[i])
188+
}
189+
return strings.Join(parts, "```")
190+
}
191+
192+
func isFlagBoundaryChar(c byte) bool {
193+
// A flag must be preceded by start-of-string or a character that is not
194+
// a word char, slash, dash, or backtick.
195+
if c == '/' || c == '-' || c == '`' {
196+
return false
197+
}
198+
if c == '_' {
199+
return false
200+
}
201+
if (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') {
202+
return false
203+
}
204+
return true
205+
}
206+
207+
func backtickFlagsFragment(s string) string {
208+
var b strings.Builder
209+
i := 0
210+
for i < len(s) {
211+
if s[i] == '`' {
212+
b.WriteByte(s[i])
213+
i++
214+
for i < len(s) && s[i] != '`' {
215+
b.WriteByte(s[i])
216+
i++
217+
}
218+
if i < len(s) {
219+
b.WriteByte(s[i])
220+
i++
221+
}
222+
continue
223+
}
224+
boundary := i == 0 || isFlagBoundaryChar(s[i-1])
225+
if boundary && (s[i] == '-') {
226+
if loc := flagPattern.FindStringIndex(s[i:]); loc != nil {
227+
b.WriteByte('`')
228+
b.WriteString(s[i : i+loc[1]])
229+
b.WriteByte('`')
230+
i += loc[1]
231+
continue
232+
}
233+
}
234+
b.WriteByte(s[i])
235+
i++
236+
}
237+
return b.String()
238+
}
239+
173240
func escapeProseFragment(s string) string {
174241
// Escape curly braces: {expr} -> \{expr\}
175242
s = strings.ReplaceAll(s, "{", "\\{")

internal/docgen/mintlify_test.go

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,90 @@ func TestEscapeMintlifyProse(t *testing.T) {
217217
}
218218
}
219219

220+
func TestBacktickFlags(t *testing.T) {
221+
tests := []struct {
222+
name string
223+
input string
224+
want string
225+
}{
226+
{
227+
name: "double-dash flag at start",
228+
input: "--api-token",
229+
want: "`--api-token`",
230+
},
231+
{
232+
name: "single-dash flag at start",
233+
input: "-x",
234+
want: "`-x`",
235+
},
236+
{
237+
name: "flag in middle of sentence",
238+
input: "use --api-token to authenticate",
239+
want: "use `--api-token` to authenticate",
240+
},
241+
{
242+
name: "flag in table cell",
243+
input: "| --api-token | API token |",
244+
want: "| `--api-token` | API token |",
245+
},
246+
{
247+
name: "two adjacent flags",
248+
input: "--foo --bar",
249+
want: "`--foo` `--bar`",
250+
},
251+
{
252+
name: "comma-separated flags",
253+
input: "--foo,--bar",
254+
want: "`--foo`,`--bar`",
255+
},
256+
{
257+
name: "already-backticked flag is left alone",
258+
input: "use `--api-token` here",
259+
want: "use `--api-token` here",
260+
},
261+
{
262+
name: "hyphenated word is not a flag",
263+
input: "this is built-in behaviour",
264+
want: "this is built-in behaviour",
265+
},
266+
{
267+
name: "code fence content is untouched",
268+
input: "see ```\nkosli foo --bar\n``` for example",
269+
want: "see ```\nkosli foo --bar\n``` for example",
270+
},
271+
{
272+
name: "flag with hyphen in name",
273+
input: "use --jira-base-url here",
274+
want: "use `--jira-base-url` here",
275+
},
276+
{
277+
name: "short flag in middle",
278+
input: "use -h for help",
279+
want: "use `-h` for help",
280+
},
281+
}
282+
for _, tt := range tests {
283+
t.Run(tt.name, func(t *testing.T) {
284+
got := backtickFlags(tt.input)
285+
if got != tt.want {
286+
t.Errorf("backtickFlags(%q):\ngot: %q\nwant: %q", tt.input, got, tt.want)
287+
}
288+
})
289+
}
290+
}
291+
292+
func TestMintlifyFlagsSectionBackticksFlags(t *testing.T) {
293+
f := MintlifyFormatter{}
294+
flags := "| --api-token string | required (default $KOSLI_API_TOKEN) |\n"
295+
got := f.FlagsSection(flags, "")
296+
if !strings.Contains(got, "`--api-token`") {
297+
t.Errorf("expected --api-token to be backticked, got:\n%s", got)
298+
}
299+
if strings.Contains(got, "| --api-token string |") {
300+
t.Errorf("expected bare --api-token to be replaced, got:\n%s", got)
301+
}
302+
}
303+
220304
func TestSanitizeDescription(t *testing.T) {
221305
tests := []struct {
222306
name string

0 commit comments

Comments
 (0)