Add @arg tag support for documenting excmd flags

Extracts @arg -flag description tags from excmd JSDoc into a flags map
in the generated metadata, and renders them as one bullet list in
`:help`.

A docstring can drop a `{{tridactyl-arg-list}}` marker anywhere in its
prose to control where that list renders, since the @arg tags
themselves must stay at the end of the comment (TSDoc runs a block
tag's content until the next tag).

Annotates `hint` as the first user of this new format.

Related to #5397 (which-key), split out per review discussion since
which-key depends on this.
This commit is contained in:
Robert Hill 2026-08-26 23:43:18 -04:00
parent 62d4cb89e3
commit a7d6115688
3 changed files with 125 additions and 23 deletions

View file

@ -32,6 +32,17 @@ function convertMetadata(project) {
.map(part => part.text || "")
.join("")
.replace(/\n+$/, "")
// Extract @arg tags: `@arg -flag description` -> flags["-flag"] = "description"
const argFlags = comment => {
const flags = {}
for (const tag of comment?.blockTags || []) {
if (tag.tag !== "@arg") continue
const text = (tag.content || []).map(part => part.text || "").join("")
const m = /^(-\S+)\s+(.+)$/.exec(text.trim())
if (m) flags[m[1]] = m[2]
}
return flags
}
const normalizeParameter = (parameter, resolving) => ({
name: parameter.name,
@ -152,15 +163,16 @@ function convertMetadata(project) {
.filter(node => node.kind === KIND.Function)
.map(node => {
const signature = node.signatures?.[0]
const comment = signature?.comment || node.comment
const flags = argFlags(comment)
return [
node.name,
{
doc:
commentText(signature?.comment) ||
commentText(node.comment),
doc: commentText(comment),
params: (signature?.parameters || []).map(parameter =>
normalizeParameter(parameter),
),
...(Object.keys(flags).length ? { flags } : {}),
},
]
}),

View file

@ -252,7 +252,82 @@ class TridactylRouter extends KindRouter {
}
}
function parseArgTag(tag) {
const text = (tag.content || []).map(part => part.text || "").join("")
const m = /^(-\S+)\s+(.+)$/.exec(text.trim())
return m ? [m[1], m[2]] : undefined
}
function renderArgList(anchor, parsed) {
if (parsed.length === 0) return null
return h(
"div",
{ class: "tsd-tag-arg tsd-comment tsd-typography" },
h(
"h4",
{ class: "tsd-anchor-link", id: anchor },
"Arguments",
h(
"a",
{ href: `#${anchor}`, "aria-label": "Permalink", class: "tsd-anchor-icon" },
h(
"svg",
{ viewBox: "0 0 24 24", "aria-hidden": "true" },
h("use", { href: "../assets/icons.svg#icon-anchor" }),
),
),
),
h(
"ul",
null,
parsed.map(([flag, desc]) => h("li", null, h("code", null, flag), " ", desc)),
),
)
}
const ARG_MARKER = "{{tridactyl-arg-list}}"
class TridactylTheme extends DefaultTheme {
getRenderContext(page) {
const context = super.getRenderContext(page)
const defaultCommentSummary = context.commentSummary
context.commentSummary = props => {
const owner = props.isParameter?.() ? props.parent : props
const argTags = (owner?.comment?.blockTags || []).filter(
tag => tag.tag === "@arg",
)
const summaryParts = props.comment?.summary || []
const markerIndex = summaryParts.findIndex(
part => part.kind === "text" && part.text.includes(ARG_MARKER),
)
if (argTags.length === 0 || markerIndex === -1)
return defaultCommentSummary(props)
argTags.forEach(tag => (tag.skipRendering = true))
const parsed = argTags.map(parseArgTag).filter(Boolean)
const anchor = `${String(owner.name || "arguments").toLowerCase()}-arguments`
const markerPart = summaryParts[markerIndex]
const [beforeText, afterText] = markerPart.text.split(ARG_MARKER)
const before = [
...summaryParts.slice(0, markerIndex),
...(beforeText ? [{ kind: "text", text: beforeText }] : []),
]
const after = [
...(afterText ? [{ kind: "text", text: afterText }] : []),
...summaryParts.slice(markerIndex + 1),
]
return h(
JSX.Fragment,
null,
before.length > 0 && context.displayParts(before),
renderArgList(anchor, parsed),
after.length > 0 && context.displayParts(after),
)
}
return context
}
getReflectionClasses(reflection) {
const kind = ReflectionKind.classString(reflection.kind)
const parent =

View file

@ -5532,26 +5532,8 @@ const KILL_STACK: Element[] = []
*
* Hinting action flags (only one can be specified):
*
* - -t open in a new foreground tab
* - -b open in background
* - -y copy (yank) link's target to clipboard
* - -p copy an element's text to the clipboard
* - -h select an element (as if you click-n-dragged over it)
* - -P copy an element's title/alt text to the clipboard
* - -r read an element's text with text-to-speech
* - -i view an image
* - -I view an image in a new tab
* - -k irreversibly deletes an element from the page (until reload)
* - -K hides an element on the page; hidden elements can be restored using [[elementunhide]].
* - -s save (download) the linked resource
* - -S save the linked image
* - -a save-as the linked resource
* - -A save-as the linked image
* - -; focus an element and set it as the element or the child of the element to scroll
* - -# yank an element's anchor URL to clipboard
* - -w open in new window
* - -wp open in new private window
* - -z scroll an element to the top of the viewport
* {{tridactyl-arg-list}}
*
* - `-pipe selector key` e.g, `-pipe a href` returns the URL of the chosen link on a page. Only makes sense with `composite`, e.g, `composite hint -pipe .some-class>a textContent | yank`. If you don't select a hint (i.e. press `<Esc>`), will return an empty string. Most useful when used like `-c` to do things other than opening links. NB: the query selector cannot contain any spaces.
* - `-W excmd...` pass hint href as the final argument to excmd and execute, e.g, `hint -W mpvsafe` to open YouTube videos. NB: passing it to bare [[exclaim]] is dangerous - see `get exaliases.mpvsafe` for an example of how to do it safely. The usual [[composite]] caveats for `;` and `|` in URLs apply. If you need to use a query selector, use `-pipe` instead.
* - -F [callback] - run a custom callback on the selected hint, e.g. `hint -JF e => {tri.excmds.tabopen("-b",e.href); e.remove()}`.
@ -5620,6 +5602,39 @@ const KILL_STACK: Element[] = []
* - `;d` and `;gd` - open links in discarded background tabs (defer loading until tab is switched to)
*
* NB: by default, hinting respects whether links say they should be opened in new tabs (i.e. `target=_blank`). If you wish to override this you can use `:hint -JW open` to force the hints to open in the current tab. JavaScript hints (grey ones) will always open wherever they want, but if you want to include these anyway you can use `:hint -W open`.
*
* @arg -t open in a new foreground tab
* @arg -b open in background
* @arg -y copy (yank) link's target to clipboard
* @arg -p copy an element's text to the clipboard
* @arg -h select an element (as if you click-n-dragged over it)
* @arg -P copy an element's title/alt text to the clipboard
* @arg -r read an element's text with text-to-speech
* @arg -i view an image
* @arg -I view an image in a new tab
* @arg -k irreversibly deletes an element from the page (until reload)
* @arg -K hides an element on the page; hidden elements can be restored using elementunhide
* @arg -s save (download) the linked resource
* @arg -S save the linked image
* @arg -a save-as the linked resource
* @arg -A save-as the linked image
* @arg -; focus an element and set it as the element or the child of the element to scroll
* @arg -# yank an element's anchor URL to clipboard
* @arg -w open in new window
* @arg -wp open in new private window
* @arg -z scroll an element to the top of the viewport
* @arg -pipe pipe attribute to clipboard
* @arg -W run excmd with hint href
* @arg -F run JS callback
* @arg -c hint CSS selector only
* @arg -C hint CSS selector and defaults
* @arg -x exclude CSS selector
* @arg -f filter hints by text
* @arg -fr filter hints by regex
* @arg -J disable JS hints
* @arg -V include invisible elements
* @arg -q rapid (stay in hint mode)
* @arg -! execute all hints immediately
*/
//#content
export async function hint(...args: string[]): Promise<any> {