Fill YAML-defined templates from a form and put the result in the prompt box


A Claude Code plugin inspired by convey.
Prompts to an agent often carry values like a branch name, a commit hash or a cluster name. They have to be exact, yet they're really just one item out of a list you could pick from. Looking them up and typing them in every time is tedious, and typos creep in.
With this plugin, you pick those values in a form to fill in the prompt.
.claude/convey//convey to open a form inside Claude Codegit or kubectlclaude plugin marketplace add ynqa/claude-convey
claude plugin install convey@claude-convey
Inside Claude Code, you can also run:
/plugin marketplace add ynqa/claude-convey
/plugin install convey@claude-convey
Restart the session after installing to load the plugin.
Put templates in .claude/convey/ of the project you want to use them in.
Run /convey in Claude Code to open the form.
/convey: loads the templates in .claude/convey//convey path/to/dir: loads the templates in that directory/convey path/to/template.yaml: loads only that fileA path picked with @ in the prompt box, such as /convey @examples, works as well.
Files that can't be loaded are listed in the reply as skipped <path>: <reason>.
The form opens in two tabs: convey and preview: <template name>.
✓, an empty one ○. A yellow * marks a required inputwaiting for …, and loading… while fetching▸ expands the full text)Required: … is shown next to it‹name›| Key | Action |
|---|---|
| Tab / Shift+Tab | Next / previous element |
| ↑ ↓, Enter | Pick in a select |
| Enter (text input) | Confirm the value and move to the next input |
| ctrl+x tab | Switch tabs (convey ↔ preview) |
| Esc | Close the form (the preview tab closes with it) |
name: git-commit-review # shown in the template select and the preview tab
inputs: # the form's inputs, in the order written
branch:
type: select # pick from choices
candidates:
command: # each line of output is one candidate
program: git
args: [branch, --format=%(refname:short)]
commit:
type: select
allow_empty: false # required (default true = may be empty)
depends_on: [branch] # waits for branch, and clears when branch is picked again
candidates:
command:
program: git
args: [log, --format=%h %s, -n, "20", "{{ inputs.branch }}"]
request:
type: textarea # free text
description: What to look at # shown after the input's name, not put in the prompt
output:
template: | # Mustache; values go in as {{ inputs.<name> }}
# Commit review
- Branch: `{{ inputs.branch }}`
- Commit: {{ inputs.commit }}
{{ inputs.request }}
| Field | Description |
|---|---|
name | The template's name (required) |
inputs | Input definitions. Keys are input names; at least one is required |
inputs.<name>.type | select (pick) or textarea (write) |
inputs.<name>.allow_empty | false makes it required. Defaults to true |
inputs.<name>.description | A note shown after the input's name in the form. It isn't put in the prompt |
inputs.<name>.placeholder | Dim hint text shown in an empty textarea. It isn't a value and doesn't go into the prompt |
inputs.<name>.candidates.values | Fixed choices for a select (a list of strings) |
inputs.<name>.candidates.default | The choice picked when the form opens; one of values. Fill inserts it if left unchanged |
inputs.<name>.candidates.command | A command that produces a select's candidates (program and args) |
inputs.<name>.depends_on | Names of the selects whose values this one waits for |
output.template | The shape of the prompt (required) |
A select's candidates takes exactly one of values or command. default can only go beside values: a command's candidates aren't known until it runs. Any field not listed here is an error.
program is started directly, and each entry of args is passed as one argument. No shell is involved, so to use pipes (|), &&, $VAR, ~ and the like, set program to sh and pass the script with -c. Don't embed input values in the script; pass them as arguments and read them as "$1", "$2" command:
program: sh
args:
- -c
- kubectl --context "$1" get pods -o json | jq -r '.items[].metadata.name'
- sh # goes into $0 (any name works)
- "{{ inputs.context }}" # $1
{{ inputs.<name> }} can be used in program and args. Arguments that render empty are droppedargs must be listed in depends_on. depends_on can only name selects that come before this inputWritten in Mustache. Values aren't escaped; they go in exactly as entered.
{{ inputs.<name> }}: the input's value. Nothing is inserted when it's empty{{#inputs.<name>}}…{{/inputs.<name>}}: shows the content only when there's a value{{^inputs.<name>}}…{{/inputs.<name>}}: shows the content only when there's no valueoutput:
template: |
{{#inputs.namespace}}
- Namespace: `{{ inputs.namespace }}`
{{/inputs.namespace}}
The plugin includes a template skill for writing templates. Run /convey:template followed by what the template should do, and it asks what you want to pick each time, tries the commands that produce the candidates, and then writes the template to .claude/convey/. Asking for a template without /convey:template uses the skill too.
Example requests:
/convey:template pick a git branch and commit, and ask for a review of that commit
/convey:template pick a kubectl context, namespace and Pod in turn, and ask to investigate
why the Pod is failing. I want to write what to look into freely each time
When it's done, check that /convey .claude/convey/<name>.yaml opens it. If it shows skipped, tell Claude what it says and it will fix the template.
hooks/register.tsx 70 lines1import type { Register } from 'claude-code'
2
3import {
4 DEFAULT_DIR,
5 PANE,
6 PREVIEW_PANE,
7 loadTemplates,
8 message,
9 previewTitle,
10 showTemplates,
11 targetOf,
12} from './actions'
13import { Form } from './form'
14import { Preview } from './preview'
15
16export const register: Register = on => {
17 on('session.start', async ($, e, next) => {
18 await $.command.register({
19 name: 'convey',
20 description: `Fill a template (default: ${DEFAULT_DIR})`,
21 })
22 return next(e)
23 })
24
25 on('command.run', { command: 'convey' }, async ($, e) => {
26 const target = targetOf(e.args)
27 let loaded: Awaited<ReturnType<typeof loadTemplates>>
28 try {
29 loaded = await loadTemplates($, target)
30 } catch (error) {
31 return { text: `cannot read ${target}: ${message(error)}` }
32 }
33 const { entries, skipped } = loaded
34 const notes = skipped.map(reason => `\n skipped ${reason}`).join('')
35 const [first] = entries
36 if (!first) return { text: `no templates in ${target}${notes}` }
37
38 await showTemplates($, entries)
39 // Tabs in the order convey, preview; only the form asks for the focus.
40 await $.ui.open({ id: PANE, title: 'convey', focus: true, closeOnEscape: true })
41 await $.ui.open({ id: PREVIEW_PANE, title: previewTitle(first.template.name) })
42 // The engine heads a command's reply with the plugin's name, so it is not repeated here.
43 return { text: `${entries.length} template(s) from ${target}${notes}` }
44 })
45
46 // When the person closes the form (Esc, the close mark), take the preview down with it.
47 // Whatever goes wrong here, the close itself goes ahead: the preview's close is
48 // caught, and should the hook still fail, its catch passes the close on (next is
49 // replay-safe there, so a close already made is not made again).
50 on('ui.close', async ($, e, next) => {
51 const closed = await next(e)
52 if (e.id === PANE) await $.ui.close({ id: PREVIEW_PANE }).catch(() => {})
53 return closed
54 }).catch(($, e, next) => next(e))
55
56 on('ui.render', { component: 'Pane', requestId: PREVIEW_PANE }, async ($, e) => Preview($, $.ui.resolve(e)))
57
58 on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
59 const elements = $.ui.resolve(e)
60 // A surface without text fields and selects (today only mobile; terminal, desktop
61 // and vscode carry both) cannot draw the form, so the pane says so in one dim line
62 // instead of failing. The check also narrows elements to FormElements for Form.
63 if (!('Input' in elements) || !('Select' in elements)) {
64 const { Text } = elements
65 return <Text dimColor>convey needs a surface with text fields.</Text>
66 }
67 return Form($, elements, e.props.bodyColumns)
68 })
69}
70hooks/actions.ts 242 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface } from 'claude-code'
3
4import type { ConveyCandidates as Candidates, ConveyCommand as Command, ConveyEntry as Entry } from '../types'
5import {
6 commandArgv,
7 decodeCandidates,
8 dependenciesOf,
9 dependentsOf,
10 initialValues,
11 missingRequired,
12 parseTemplate,
13 render,
14 renderPreview,
15 waitingFor,
16} from './template'
17
18export const PANE = 'convey'
19export const PREVIEW_PANE = 'convey-preview'
20export const DEFAULT_DIR = '.claude/convey'
21
22const templates = atom({ plugin: 'convey', key: 'templates' } as const, [])
23const selected = atom({ plugin: 'convey', key: 'selected' } as const, null)
24const values = atom({ plugin: 'convey', key: 'values' } as const, {})
25const candidates = atom({ plugin: 'convey', key: 'candidates' } as const, {})
26const submits = atom({ plugin: 'convey', key: 'submits' } as const, {})
27
28export const message = (error: unknown) => (error instanceof Error ? error.message : String(error))
29
30/**
31 * The path `/convey`'s argument names, or the default folder when it names none.
32 * Picking a path from the prompt box's `@` typeahead leaves it as `@examples`,
33 * or `@"my templates"` when it has a space, so that mark and its quotes are dropped.
34 */
35export function targetOf(args: string): string {
36 const path = args.trim().replace(/^@/, '').replace(/^"(.*)"$/, '$1')
37 return path || DEFAULT_DIR
38}
39
40/**
41 * Reads `target` (a file, or a directory whose files are each tried) and keeps
42 * what parses as a template; the rest is reported as skipped.
43 */
44export async function loadTemplates($: EngineInterface, target: string) {
45 const paths: string[] = []
46 if ((await $.fs.stat(target)).kind === 'dir') {
47 const dir = target.replace(/\/+$/, '')
48 for (const entry of await $.fs.list(dir)) {
49 if (entry.kind === 'file' && !entry.name.startsWith('.')) paths.push(`${dir}/${entry.name}`)
50 }
51 paths.sort()
52 } else {
53 paths.push(target)
54 }
55
56 const entries: Entry[] = []
57 const skipped: string[] = []
58 for (const path of paths) {
59 try {
60 entries.push({ path, template: parseTemplate(await $.fs.read(path)) })
61 } catch (error) {
62 skipped.push(`${path}: ${message(error)}`)
63 }
64 }
65 return { entries, skipped }
66}
67
68/** Runs a candidate command; a non-zero exit is an error carrying its stderr. */
69async function runCommand($: EngineInterface, argv: string[]): Promise<Candidates> {
70 try {
71 const { exitCode, stdout, stderr } = await $.process.run(argv, { timeoutMs: 30_000 })
72 if (exitCode !== 0) return { status: 'error', error: `${argv[0]} exited with ${exitCode}: ${stderr.trim()}` }
73 return { status: 'ready', values: decodeCandidates(stdout) }
74 } catch (error) {
75 return { status: 'error', error: message(error) }
76 }
77}
78
79// A function declaration, not a const: $ is followed past an import only into these.
80export async function selectedEntry($: EngineInterface) {
81 const path = await read($, selected)
82 return (await read($, templates)).find(entry => entry.path === path)
83}
84
85/**
86 * Loads one command-backed select with the current values. The result is kept
87 * only while the same template is shown and the values the command ran with
88 * are still the current ones, so a slow command cannot overwrite a newer form.
89 */
90async function loadCandidates($: EngineInterface, path: string, name: string, command: Command, dependsOn: string[]) {
91 const current = await read($, values)
92 const ranWith = JSON.stringify(dependsOn.map(dependency => current[dependency]))
93 const loading: Candidates = { status: 'loading' }
94 await update($, candidates, loaded => ({ ...loaded, [name]: loading }))
95
96 let result: Candidates
97 try {
98 result = await runCommand($, commandArgv(command, current))
99 } catch (error) {
100 result = { status: 'error', error: message(error) }
101 }
102
103 if ((await read($, selected)) !== path) return
104 const now = await read($, values)
105 if (JSON.stringify(dependsOn.map(dependency => now[dependency])) !== ranWith) return
106 await update($, candidates, loaded => (loaded[name]?.status === 'loading' ? { ...loaded, [name]: result } : loaded))
107}
108
109/** Starts every command-backed select whose dependencies have values and whose candidates are not loaded. */
110async function loadReadyCandidates($: EngineInterface) {
111 const entry = await selectedEntry($)
112 if (!entry) return
113 const current = await read($, values)
114 const loaded = await read($, candidates)
115 for (const [name, input] of Object.entries(entry.template.inputs)) {
116 if (input.type !== 'select' || !('command' in input.candidates) || loaded[name]) continue
117 if (waitingFor(input, current).length > 0) continue
118 void loadCandidates($, entry.path, name, input.candidates.command, dependenciesOf(input))
119 }
120}
121
122/** Keeps the templates `/convey` found and shows the first one's form. */
123export async function showTemplates($: EngineInterface, entries: Entry[]) {
124 await update($, templates, () => entries)
125 if (entries[0]) await selectTemplate($, entries[0].path)
126}
127
128/** What the form pane draws from. The state is read here, in the file that declares it. */
129export async function readForm($: EngineInterface) {
130 return {
131 entries: await read($, templates),
132 path: await read($, selected),
133 current: await read($, values),
134 loaded: await read($, candidates),
135 submitted: await read($, submits),
136 }
137}
138
139/**
140 * The preview of the chosen template with the current values, as runs whose
141 * inputs still to fill are marked `‹name›` (see renderPreview), or null when none is chosen.
142 */
143export async function previewText($: EngineInterface) {
144 const entry = await selectedEntry($)
145 return entry ? renderPreview(entry.template, await read($, values)) : null
146}
147
148/** Shows a template's form, starting at its defaults, and loads its command candidates. */
149export async function selectTemplate($: EngineInterface, path: string) {
150 await update($, selected, () => path)
151 const entry = await selectedEntry($)
152 await update($, values, () => (entry ? initialValues(entry.template) : {}))
153 await update($, candidates, () => ({}))
154 await update($, submits, () => ({}))
155 await loadReadyCandidates($)
156}
157
158/** The preview tab's title, which names the template it shows. */
159export const previewTitle = (name: string) => `preview: ${name}`
160
161/**
162 * Switches the form to another template from the chooser. An open preview
163 * tab is retitled (opening an open id only retitles it); a closed one stays closed.
164 */
165export async function chooseTemplate($: EngineInterface, path: string) {
166 await selectTemplate($, path)
167 const entry = await selectedEntry($)
168 if (!entry) return
169 if ((await $.ui.panes()).some(pane => pane.id === PREVIEW_PANE)) {
170 await $.ui.open({ id: PREVIEW_PANE, title: previewTitle(entry.template.name) })
171 }
172}
173
174/** Saves what is typed into a text field. */
175export async function enter($: EngineInterface, name: string, text: string) {
176 await update($, values, entered => ({ ...entered, [name]: text }))
177}
178
179/**
180 * Saves a text field's value on Enter and counts the press. The surface empties
181 * a field on Enter, and an unchanged value would not draw it again, so the count
182 * goes into the field's key (see form.tsx), drawing a new field with the value.
183 */
184export async function submit($: EngineInterface, name: string, text: string) {
185 await enter($, name, text)
186 await update($, submits, counted => ({ ...counted, [name]: (counted[name] ?? 0) + 1 }))
187}
188
189/** Saves a select's value, clears every input that depends on it, and loads what can now load. */
190export async function pick($: EngineInterface, name: string, value: string) {
191 const entry = await selectedEntry($)
192 if (!entry) return
193 const dependents = dependentsOf(entry.template, name)
194 await update($, values, entered => {
195 const next = { ...entered, [name]: value }
196 for (const dependent of dependents) delete next[dependent]
197 return next
198 })
199 await update($, candidates, loaded => {
200 const next = { ...loaded }
201 for (const dependent of dependents) delete next[dependent]
202 return next
203 })
204 await loadReadyCandidates($)
205}
206
207/** Forgets a select's candidates and loads them again. */
208export async function retry($: EngineInterface, name: string) {
209 await update($, candidates, loaded => {
210 const next = { ...loaded }
211 delete next[name]
212 return next
213 })
214 await loadReadyCandidates($)
215}
216
217/** Shows a failed select's whole error, or only its first line again. */
218export async function toggleError($: EngineInterface, name: string) {
219 await update($, candidates, loaded => {
220 const state = loaded[name]
221 if (state?.status !== 'error') return loaded
222 return { ...loaded, [name]: { ...state, isExpanded: !state.isExpanded } }
223 })
224}
225
226export async function fill($: EngineInterface) {
227 const entry = await selectedEntry($)
228 if (!entry) return
229 const current = await read($, values)
230 const missing = missingRequired(entry.template, current)
231 if (missing.length > 0) {
232 $.ui.toast(`Required: ${missing.join(', ')}`)
233 return
234 }
235 await $.prompt.fill({ text: render(entry.template, current), mode: 'replace' })
236 await update($, values, () => ({}))
237 await $.ui.close({ id: PANE })
238 // The plugin's own $.ui.close skips its own ui.close hook, so the preview goes here
239 // too; caught, as in that hook, since the prompt is already filled by now.
240 await $.ui.close({ id: PREVIEW_PANE }).catch(() => {})
241}
242hooks/form.tsx 323 lines1import type { Elements, EngineInterface, RenderElement } from 'claude-code'
2
3import type { ConveyCandidates as Candidates, ConveyEntry as Entry, ConveyInput, ConveyTemplate } from '../types'
4import { PANE, chooseTemplate, enter, fill, pick, readForm, retry, submit, toggleError } from './actions'
5import { allowsEmpty, missingRequired, waitingFor } from './template'
6
7/** The elements of a surface that has text fields and selects. */
8export type FormElements = Extract<Elements[keyof Elements], { Input: unknown; Select: unknown }>
9
10/**
11 * What every part of the form draws from. $ is not in it: a mod passes $ only as
12 * itself, so the parts that act take it as their first argument.
13 *
14 * `el` and `columns` come from the render event; the rest is read from the
15 * state by `readForm`. The examples are kubernetes-investigation right after
16 * `kind-kind` is picked as the context, while its namespaces still load.
17 */
18type Ctx = {
19 /**
20 * The elements this surface draws with, as `$.ui.resolve(e)` gave them
21 * (terminal, desktop or vscode; mobile has no text fields and never gets here).
22 * Each is a function that makes an element: `const { Box, Text } = ctx.el`.
23 */
24 el: FormElements
25 /**
26 * How many columns the pane's body has, `e.props.bodyColumns`; `76`, say.
27 * Decides whether a failed select's first error line fits or gets ▸ / ▾.
28 */
29 columns: number
30 /**
31 * Every file `/convey` parsed as a template, in file name order; files
32 * that did not parse are not here. Two or more draw the chooser, one its name.
33 *
34 * @example
35 * [
36 * { path: '.claude/convey/git-commit-review.yaml', template: { name: 'git-commit-review', … } },
37 * { path: '.claude/convey/kubernetes-investigation.yaml', template: { name: 'kubernetes-investigation', … } },
38 * ]
39 */
40 entries: Entry[]
41 /**
42 * The path of the chosen entry, the chooser's value:
43 * `'.claude/convey/kubernetes-investigation.yaml'`.
44 */
45 path: string
46 /**
47 * The chosen entry's template. Its inputs keep the file's order, which
48 * is the order of the fields; `{{ … }}` in a command's args is still unrendered
49 * here, `commandArgv` fills it in just before the command runs.
50 *
51 * @example
52 * {
53 * name: 'kubernetes-investigation',
54 * inputs: {
55 * context: { type: 'select', candidates: { command: { program: 'kubectl', args: ['config', 'get-contexts', '-o', 'name'] } } },
56 * namespace: { type: 'select', depends_on: ['context'], candidates: { command: { program: 'kubectl', args: ['--context', '{{ inputs.context }}', …] } } },
57 * resource_kind: { type: 'select', candidates: { values: ['pod', 'deployment', …] } },
58 * resource: { type: 'select', depends_on: ['context', 'namespace', 'resource_kind'], candidates: { command: … } },
59 * request: { type: 'textarea', allow_empty: false },
60 * },
61 * output: { template: '# Kubernetes investigation\n\n## Target\n…' },
62 * }
63 */
64 template: ConveyTemplate
65 /**
66 * What the person has typed or picked so far, by input name: `{ context: 'kind-kind' }`.
67 * An input not given a value has no key (a cleared text field may stay as `''`),
68 * and picking a select again drops the keys of the inputs that depend on it.
69 * The ✓ / ○ marks, the controls' values and "waiting for …" all read it.
70 */
71 current: Record<string, string>
72 /**
73 * The candidates of each command-backed select, by input name. A select with
74 * fixed values (resource_kind) is never here; one still waiting for its
75 * dependencies (resource) or not yet started has no key.
76 *
77 * @example
78 * {
79 * context: { status: 'ready', values: ['kind-kind', 'kind-kube-inference-sandbox'] },
80 * namespace: { status: 'loading' },
81 * }
82 */
83 loaded: Record<string, Candidates>
84 /**
85 * How many times Enter was pressed in each text field: `{ request: 1 }`. It
86 * goes into the field's key, so a field emptied by Enter is drawn anew.
87 */
88 submitted: Record<string, number>
89}
90
91/** The keys the form's elements are drawn under, which the focus moves between. */
92const keys = {
93 /** An input's field; a text field's also counts its Enter presses, after a `~`. */
94 field: (name: string, submitted = 0) => (submitted === 0 ? name : `${name}~${submitted}`),
95 retry: (name: string) => `retry:${name}`,
96 error: (name: string) => `error:${name}`,
97 chooser: 'template',
98 fill: 'fill',
99}
100
101/**
102 * A field: its title on one line, in the accent color so it reads apart
103 * from what is entered, and the control (or its loading state) indented below.
104 * An input's title carries a mark, ✓ once it has a value and ○ until then,
105 * which also lines the title up with the control under it. A description follows
106 * the title on its line, after a colon and in the plain text color: the dim color
107 * is the placeholder's, so a dim line between title and control would read as one.
108 */
109function Field(
110 { el: { Box, Text } }: Ctx,
111 title: string,
112 isRequired: boolean,
113 control: RenderElement,
114 isFilled?: boolean,
115 description?: string,
116) {
117 return (
118 <Box flexDirection="column">
119 <Box>
120 {isFilled !== undefined &&
121 (isFilled ? <Text color="success">✓ </Text> : <Text color="inactive">○ </Text>)}
122 <Text weight="bold" color="claude">
123 {title}
124 </Text>
125 {description !== undefined && (
126 <Box flexShrink={1} minWidth={0}>
127 <Text>: {description}</Text>
128 </Box>
129 )}
130 {isRequired && <Text color="warning"> *</Text>}
131 </Box>
132 <Box paddingLeft={2}>{control}</Box>
133 </Box>
134 )
135}
136
137/**
138 * The key of the element that takes the focus for an input, as drawn below:
139 * its field, its Retry button, or none while it waits or loads.
140 */
141function focusKey({ current, loaded, submitted }: Ctx, name: string, input: ConveyInput): string | null {
142 if (input.type === 'textarea') return keys.field(name, submitted[name])
143 if ('values' in input.candidates) return keys.field(name)
144 if (waitingFor(input, current).length > 0) return null
145 const state = loaded[name]
146 if (!state || state.status === 'loading') return null
147 return state.status === 'error' || state.values.length === 0 ? keys.retry(name) : keys.field(name)
148}
149
150/** The next element that can take the focus after an input, or the Fill button. */
151function nextKey(ctx: Ctx, name: string) {
152 const inputs = Object.entries(ctx.template.inputs)
153 const after = inputs.slice(inputs.findIndex(([inputName]) => inputName === name) + 1)
154 for (const [inputName, input] of after) {
155 const key = focusKey(ctx, inputName, input)
156 if (key) return key
157 }
158 return keys.fill
159}
160
161/**
162 * Why a command-backed select has nothing to pick, with a Retry button. A
163 * command's stderr runs to many lines: one truncated line keeps Retry in view,
164 * and ▸ / ▾ shows the whole of it or folds it again.
165 */
166function CandidateError($: EngineInterface, ctx: Ctx, name: string, state: Extract<Candidates, { status: 'error' | 'ready' }>) {
167 const { Box, Text, Button } = ctx.el
168 const retryButton = <Button key={keys.retry(name)} label="Retry" onPress={() => void retry($, name)} />
169 if (state.status !== 'error') {
170 return (
171 <Box gap={1}>
172 <Text color="error">no candidates</Text>
173 {retryButton}
174 </Box>
175 )
176 }
177 const [firstLine = '', ...restLines] = state.error.trim().split('\n')
178 const hasMore = restLines.length > 0 || firstLine.length > ctx.columns - 16
179 return (
180 <Box flexDirection="column">
181 <Box gap={1}>
182 {hasMore && (
183 <Button
184 key={keys.error(name)}
185 label={state.isExpanded ? '▾' : '▸'}
186 plain
187 onPress={() => void toggleError($, name)}
188 />
189 )}
190 <Box flexShrink={1} minWidth={0}>
191 <Text color="error" wrap={state.isExpanded ? 'wrap' : 'truncate-end'}>
192 {firstLine}
193 </Text>
194 </Box>
195 {retryButton}
196 </Box>
197 {state.isExpanded && restLines.length > 0 && (
198 <Box paddingLeft={hasMore ? 2 : 0}>
199 <Text color="error">{restLines.join('\n')}</Text>
200 </Box>
201 )}
202 </Box>
203 )
204}
205
206/** The control of one input: a text field, a select, or why its select cannot show yet. */
207function Control($: EngineInterface, ctx: Ctx, name: string, input: ConveyInput) {
208 const { current, loaded, el: { Text, Input, Select } } = ctx
209 if (input.type === 'textarea') {
210 // The placeholder gives an empty field something to click, so one is always set, falling
211 // back to `Enter <name>…` when the template has none; Enter moves on to the next field.
212 return (
213 <Input
214 key={keys.field(name, ctx.submitted[name])}
215 placeholder={input.placeholder ?? `Enter ${name}…`}
216 value={current[name] ?? ''}
217 submitLabel="next"
218 onInput={text => void enter($, name, text)}
219 onSubmit={text => {
220 void submit($, name, text)
221 // A refused move leaves the focus where it is, which is fine here.
222 $.ui.focus({ requestId: PANE, key: nextKey(ctx, name) }).catch(() => {})
223 }}
224 />
225 )
226 }
227
228 let options: string[]
229 if ('values' in input.candidates) {
230 options = input.candidates.values
231 } else {
232 const waiting = waitingFor(input, current)
233 const state = loaded[name]
234 if (waiting.length > 0) return <Text dimColor>waiting for {waiting.join(', ')}</Text>
235 if (!state || state.status === 'loading') return <Text dimColor>loading…</Text>
236 if (state.status === 'error' || state.values.length === 0) return CandidateError($, ctx, name, state)
237 options = state.values
238 }
239 return (
240 <Select
241 key={keys.field(name)}
242 options={options.map(value => ({ value }))}
243 value={current[name]}
244 onSelect={value => void pick($, name, value)}
245 />
246 )
247}
248
249/**
250 * Which template is chosen, above the rounded card of the inputs it asks for.
251 * A header rather than a field: a small dim label beside the template, with no
252 * accent title or mark, so it does not read as one of the inputs. The terminal
253 * has one text size and ignores `caption`; the dim color sets it apart there.
254 */
255function Chooser($: EngineInterface, ctx: Ctx) {
256 const { entries, path, template, el: { Box, Text, Select } } = ctx
257 return (
258 <Box gap={1}>
259 <Text dimColor size="caption">
260 template
261 </Text>
262 {entries.length <= 1 ? (
263 <Text weight="bold">{template.name}</Text>
264 ) : (
265 <Select
266 key={keys.chooser}
267 options={entries.map(entry => ({ value: entry.path, label: entry.template.name }))}
268 value={path}
269 onSelect={choice => void chooseTemplate($, choice)}
270 />
271 )}
272 </Box>
273 )
274}
275
276/**
277 * The Fill button, with the required inputs still empty beside it in the
278 * warning color (the color of their ` *`), so what Fill would refuse shows
279 * before it is pressed. Fill's own toast stays for a press made anyway.
280 */
281function FillRow($: EngineInterface, ctx: Ctx) {
282 const { Box, Text, Button } = ctx.el
283 const missing = missingRequired(ctx.template, ctx.current)
284 return (
285 <Box gap={2}>
286 <Button key={keys.fill} label="Fill" variant="primary" onPress={() => void fill($)} />
287 {missing.length > 0 && <Text color="warning">Required: {missing.join(', ')}</Text>}
288 </Box>
289 )
290}
291
292/** The form pane: the chooser, the card of inputs, and the Fill button. */
293export async function Form($: EngineInterface, el: FormElements, columns: number) {
294 const { Box, Text, Button } = el
295 const { entries, path, current, loaded, submitted } = await readForm($)
296 const template = entries.find(entry => entry.path === path)?.template
297 if (!path || !template) return <Text dimColor>No template selected. Run /convey.</Text>
298
299 const ctx: Ctx = { el, columns, entries, path, template, current, loaded, submitted }
300 // A rule across the pane under its tab header (which the engine draws, so no
301 // line goes between the tabs themselves), then a blank row between the rule,
302 // the template chooser, the card of inputs and the Fill button.
303 return (
304 <Box flexDirection="column" gap={1}>
305 <Text dimColor>{'─'.repeat(Math.max(1, columns))}</Text>
306 {Chooser($, ctx)}
307 <Box flexDirection="column" gap={1} borderStyle="round" paddingX={1}>
308 {Object.entries(template.inputs).map(([name, input]) =>
309 Field(
310 ctx,
311 name,
312 !allowsEmpty(input),
313 Control($, ctx, name, input),
314 Boolean(ctx.current[name]?.trim()),
315 input.description,
316 ),
317 )}
318 </Box>
319 {FillRow($, ctx)}
320 </Box>
321 )
322}
323hooks/preview.tsx 22 lines1import type { Elements, EngineInterface } from 'claude-code'
2
3import { previewText } from './actions'
4
5/**
6 * The preview tab: the chosen template rendered with the current values, as the
7 * plain text Fill puts in the prompt box (not Markdown, which cannot color a
8 * part). Entered values are in the success color, as the form's ✓ marks, and
9 * each input still to fill is `‹name›` in the warning color.
10 */
11export async function Preview($: EngineInterface, { Text }: Elements[keyof Elements]) {
12 const parts = await previewText($)
13 if (parts === null) return <Text dimColor>No template selected.</Text>
14 return (
15 <Text>
16 {parts.map(part =>
17 part.kind === 'text' ? part.text : <Text color={part.kind === 'filled' ? 'success' : 'warning'}>{part.text}</Text>,
18 )}
19 </Text>
20 )
21}
22hooks/template.ts 224 lines1import type { ConveyCommand as Command, ConveyInput as Input, ConveyTemplate as Template } from '../types'
2import { load } from './vendor/js-yaml/js-yaml.mjs'
3import Mustache from './vendor/mustache/mustache.mjs'
4
5type YamlMap = Record<string, unknown>
6
7const isMap = (value: unknown): value is YamlMap =>
8 typeof value === 'object' && value !== null && !Array.isArray(value)
9
10const isStringList = (value: unknown): value is string[] =>
11 Array.isArray(value) && value.every(item => typeof item === 'string')
12
13/** Rejects fields this version does not understand, as convey does. */
14function onlyFields(map: YamlMap, fields: string[], where: string) {
15 for (const key of Object.keys(map)) {
16 if (!fields.includes(key)) throw new Error(`unknown field in ${where}: ${key}`)
17 }
18}
19
20/** `earlierSelects` are the select inputs listed before this one. */
21function checkInput(name: string, input: unknown, earlierSelects: string[]): asserts input is Input {
22 if (!isMap(input)) throw new Error(`input ${name} must be a mapping`)
23 onlyFields(input, ['type', 'allow_empty', 'description', 'depends_on', 'candidates', 'placeholder'], `input ${name}`)
24 if (input.allow_empty !== undefined && typeof input.allow_empty !== 'boolean') {
25 throw new Error(`allow_empty of ${name} must be true or false`)
26 }
27 if (input.description !== undefined && (typeof input.description !== 'string' || input.description.trim() === '')) {
28 throw new Error(`description of ${name} must be a non-empty string`)
29 }
30
31 if (input.type === 'textarea') {
32 if (input.candidates !== undefined) throw new Error(`textarea input ${name} cannot define candidates`)
33 if (input.depends_on !== undefined) throw new Error(`textarea input ${name} cannot define depends_on`)
34 if (input.placeholder !== undefined && (typeof input.placeholder !== 'string' || input.placeholder === '')) {
35 throw new Error(`placeholder of ${name} must be a non-empty string`)
36 }
37 return
38 }
39 if (input.type !== 'select') throw new Error(`input ${name} must have type select or textarea`)
40 if (input.placeholder !== undefined) throw new Error(`select input ${name} cannot define placeholder`)
41
42 if (input.depends_on !== undefined) {
43 if (!isStringList(input.depends_on)) throw new Error(`depends_on of ${name} must be a list of input names`)
44 for (const dependency of input.depends_on) {
45 // Textareas are saved on every keystroke, so depending on one would rerun the command per key.
46 if (!earlierSelects.includes(dependency)) {
47 throw new Error(`input ${name} can depend only on an earlier select input, not ${dependency}`)
48 }
49 }
50 }
51
52 const candidates = input.candidates
53 if (!isMap(candidates)) throw new Error(`select input ${name} requires candidates`)
54 onlyFields(candidates, ['values', 'default', 'command'], `candidates of ${name}`)
55 if ((candidates.values === undefined) === (candidates.command === undefined)) {
56 throw new Error(`candidates of ${name} need exactly one of values or command`)
57 }
58
59 if (candidates.values !== undefined) {
60 if (!isStringList(candidates.values) || candidates.values.length === 0) {
61 throw new Error(`candidates.values of ${name} must be a non-empty list of strings`)
62 }
63 const fallback = candidates.default
64 if (fallback !== undefined && (typeof fallback !== 'string' || !candidates.values.includes(fallback))) {
65 throw new Error(`candidates.default of ${name} must be one of its values`)
66 }
67 return
68 }
69 // A command's candidates are known only once it has run, and the person can pick an
70 // earlier input again meanwhile, so a default there could be picked from stale candidates.
71 if (candidates.default !== undefined) throw new Error(`candidates.default of ${name} needs values, not command`)
72
73 const command = candidates.command
74 if (!isMap(command)) throw new Error(`candidates.command of ${name} must be a mapping`)
75 onlyFields(command, ['program', 'args'], `candidates.command of ${name}`)
76 if (typeof command.program !== 'string' || command.program.trim() === '') {
77 throw new Error(`candidates.command.program of ${name} must not be empty`)
78 }
79 if (command.args !== undefined && !isStringList(command.args)) {
80 throw new Error(`candidates.command.args of ${name} must be a list of strings`)
81 }
82}
83
84/** Each non-empty line of a command's stdout, trimmed, is one candidate. */
85export function decodeCandidates(stdout: string): string[] {
86 return stdout
87 .split('\n')
88 .map(line => line.trim())
89 .filter(line => line !== '')
90}
91
92/** Parses a template YAML file and checks it against convey's rules. */
93export function parseTemplate(source: string): Template {
94 const template = load(source)
95 if (!isMap(template)) throw new Error('a template must be a mapping')
96 onlyFields(template, ['name', 'inputs', 'output'], 'the template')
97
98 if (typeof template.name !== 'string' || template.name.trim() === '') throw new Error('name must not be empty')
99
100 const inputs = template.inputs
101 if (!isMap(inputs) || Object.keys(inputs).length === 0) {
102 throw new Error('inputs must be a mapping with at least one input')
103 }
104 const selects: string[] = []
105 for (const [name, input] of Object.entries(inputs)) {
106 checkInput(name, input, selects)
107 if (input.type === 'select') selects.push(name)
108 }
109
110 const output = template.output
111 if (!isMap(output)) throw new Error('output must be a mapping')
112 onlyFields(output, ['template'], 'output')
113 if (typeof output.template !== 'string' || output.template.trim() === '') {
114 throw new Error('output.template must not be empty')
115 }
116
117 return template as Template
118}
119
120export const allowsEmpty = (input: Input) => input.allow_empty ?? true
121
122export const dependenciesOf = (input: Input) => (input.type === 'select' ? (input.depends_on ?? []) : [])
123
124/**
125 * Renders a Mustache template with the values under `inputs`
126 * (`{{ inputs.<name> }}`). Unset inputs render as empty strings and nothing
127 * is HTML-escaped: the output is Markdown for a prompt, or command arguments.
128 */
129function renderText(source: string, values: Record<string, string>): string {
130 return Mustache.render(source, { inputs: values }, {}, { escape: text => text })
131}
132
133/** Renders `output.template`. */
134export function render(template: Template, values: Record<string, string>): string {
135 return renderText(template.output.template, values)
136}
137
138/**
139 * A run of the preview: the template's own text, an entered value (`filled`), or
140 * the `‹name›` mark of an input still to fill (`missing`).
141 */
142export type PreviewPart =
143 | { kind: 'text'; text: string }
144 | { kind: 'filled' | 'missing'; name: string; text: string }
145
146// Private use characters wrap each input while rendering (START name SEP value END,
147// the value empty when missing), so every input's run is found again exactly, even
148// when an entered value itself holds ‹ or ›.
149const START = '\uE000'
150const SEP = '\uE001'
151const END = '\uE002'
152const WRAPPED = new RegExp(`${START}([^${SEP}]*)${SEP}([^${END}]*)${END}`, 'g')
153
154/**
155 * Renders `output.template` for the preview, as runs: each input is a part of its
156 * own, its value when filled and `‹name›` when unset or blank, so the preview can
157 * color both. Trailing whitespace is dropped. Only the preview uses it; Fill puts
158 * `render`'s text, without marks, in the prompt box.
159 */
160export function renderPreview(template: Template, values: Record<string, string>): PreviewPart[] {
161 const shown = { ...values }
162 for (const name of Object.keys(template.inputs)) {
163 const value = values[name] ?? ''
164 shown[name] = `${START}${name}${SEP}${value.trim() === '' ? '' : value}${END}`
165 }
166 const rendered = renderText(template.output.template, shown).trimEnd()
167
168 const parts: PreviewPart[] = []
169 let at = 0
170 for (const match of rendered.matchAll(WRAPPED)) {
171 const [whole, name = '', value = ''] = match
172 if (match.index > at) parts.push({ kind: 'text', text: rendered.slice(at, match.index) })
173 parts.push(value === '' ? { kind: 'missing', name, text: `‹${name}›` } : { kind: 'filled', name, text: value })
174 at = match.index + whole.length
175 }
176 if (at < rendered.length) parts.push({ kind: 'text', text: rendered.slice(at) })
177 return parts
178}
179
180/**
181 * The argument vector of a candidate command, each part rendered with the
182 * values. Arguments that render empty are dropped, so a Mustache section can
183 * leave an option out (`{{#inputs.ns}}--namespace={{ inputs.ns }}{{/inputs.ns}}`).
184 */
185export function commandArgv(command: Command, values: Record<string, string>): string[] {
186 const program = renderText(command.program, values)
187 if (program.trim() === '') throw new Error(`program ${JSON.stringify(command.program)} rendered empty`)
188 const args = (command.args ?? []).map(arg => renderText(arg, values)).filter(arg => arg !== '')
189 return [program, ...args]
190}
191
192/** The dependencies of `input` that have no value yet. */
193export function waitingFor(input: Input, values: Record<string, string>): string[] {
194 return dependenciesOf(input).filter(dependency => !values[dependency])
195}
196
197/** The values a template's form starts with: the `candidates.default` of each select that has one. */
198export function initialValues(template: Template): Record<string, string> {
199 const initial: Record<string, string> = {}
200 for (const [name, input] of Object.entries(template.inputs)) {
201 if (input.type === 'select' && 'values' in input.candidates && input.candidates.default !== undefined) {
202 initial[name] = input.candidates.default
203 }
204 }
205 return initial
206}
207
208/** The inputs that depend on `changed`, directly or through another input, in file order. */
209export function dependentsOf(template: Template, changed: string): string[] {
210 const affected = new Set([changed])
211 for (const [name, input] of Object.entries(template.inputs)) {
212 if (dependenciesOf(input).some(dependency => affected.has(dependency))) affected.add(name)
213 }
214 affected.delete(changed)
215 return [...affected]
216}
217
218/** The names of inputs with `allow_empty: false` that have no value. */
219export function missingRequired(template: Template, values: Record<string, string>): string[] {
220 return Object.entries(template.inputs)
221 .filter(([name, input]) => !allowsEmpty(input) && (values[name] ?? '').trim() === '')
222 .map(([name]) => name)
223}
224hooks/vendor/js-yaml/js-yaml.mjs 3656 lines1/*! js-yaml 5.5.0 https://github.com/nodeca/js-yaml @license MIT */
2//#region src/tag.ts
3/**
4* Returned by a scalar resolver when the source does not match its tag.
5*
6* @category Tags
7*/
8var NOT_RESOLVED = Symbol("NOT_RESOLVED");
9/**
10* Create a normalized scalar tag definition.
11*
12* @category Tags
13*/
14function defineScalarTag(tagName, options) {
15 return {
16 tagName,
17 nodeKind: "scalar",
18 implicit: options.implicit ?? false,
19 matchByTagPrefix: options.matchByTagPrefix ?? false,
20 implicitFirstChars: options.implicitFirstChars ?? null,
21 resolve: options.resolve,
22 identify: options.identify,
23 represent: options.represent ?? ((data) => String(data)),
24 representTagName: options.representTagName ?? (() => tagName)
25 };
26}
27/**
28* Create a normalized sequence tag definition.
29*
30* @category Tags
31*/
32function defineSequenceTag(tagName, options) {
33 const carrierIsResult = options.finalize === void 0;
34 return {
35 tagName,
36 nodeKind: "sequence",
37 implicit: false,
38 matchByTagPrefix: options.matchByTagPrefix ?? false,
39 create: options.create,
40 addItem: options.addItem,
41 finalize: options.finalize ?? ((carrier) => carrier),
42 carrierIsResult,
43 identify: options.identify,
44 represent: options.represent ?? ((data) => data),
45 representTagName: options.representTagName ?? (() => tagName)
46 };
47}
48/**
49* Create a normalized mapping tag definition.
50*
51* @category Tags
52*/
53function defineMappingTag(tagName, options) {
54 const carrierIsResult = options.finalize === void 0;
55 return {
56 tagName,
57 nodeKind: "mapping",
58 implicit: false,
59 matchByTagPrefix: options.matchByTagPrefix ?? false,
60 create: options.create,
61 addPair: options.addPair,
62 has: options.has,
63 keys: options.keys,
64 get: options.get,
65 finalize: options.finalize ?? ((carrier) => carrier),
66 carrierIsResult,
67 identify: options.identify,
68 represent: options.represent ?? ((data) => data),
69 representTagName: options.representTagName ?? (() => tagName)
70 };
71}
72//#endregion
73//#region src/tag/scalar/str.ts
74/** @category Tags */
75var strTag = defineScalarTag("tag:yaml.org,2002:str", {
76 resolve: (source) => source,
77 identify: (data) => typeof data === "string"
78});
79//#endregion
80//#region src/tag/scalar/null_core.ts
81var NULL_VALUES$1 = [
82 "",
83 "~",
84 "null",
85 "Null",
86 "NULL"
87];
88/** @category Tags */
89var nullCoreTag = defineScalarTag("tag:yaml.org,2002:null", {
90 implicit: true,
91 implicitFirstChars: [
92 "",
93 "~",
94 "n",
95 "N"
96 ],
97 resolve: (source) => {
98 if (NULL_VALUES$1.indexOf(source) !== -1) return null;
99 return NOT_RESOLVED;
100 },
101 identify: (object) => object === null,
102 represent: () => "null"
103});
104//#endregion
105//#region src/tag/scalar/null_json.ts
106/** @category Tags */
107var nullJsonTag = defineScalarTag("tag:yaml.org,2002:null", {
108 implicit: true,
109 implicitFirstChars: ["n"],
110 resolve: (source, isExplicit) => {
111 if (source === "null" || isExplicit && source === "") return null;
112 return NOT_RESOLVED;
113 },
114 identify: (object) => object === null,
115 represent: () => "null"
116});
117//#endregion
118//#region src/tag/scalar/null_yaml11.ts
119var NULL_VALUES = [
120 "",
121 "~",
122 "null",
123 "Null",
124 "NULL"
125];
126/** @category Tags */
127var nullYaml11Tag = defineScalarTag("tag:yaml.org,2002:null", {
128 implicit: true,
129 implicitFirstChars: [
130 "",
131 "~",
132 "n",
133 "N"
134 ],
135 resolve: (source) => {
136 if (NULL_VALUES.indexOf(source) !== -1) return null;
137 return NOT_RESOLVED;
138 },
139 identify: (object) => object === null,
140 represent: () => "null"
141});
142//#endregion
143//#region src/tag/scalar/bool_core.ts
144var TRUE_VALUES$2 = [
145 "true",
146 "True",
147 "TRUE"
148];
149var FALSE_VALUES$2 = [
150 "false",
151 "False",
152 "FALSE"
153];
154/** @category Tags */
155var boolCoreTag = defineScalarTag("tag:yaml.org,2002:bool", {
156 implicit: true,
157 implicitFirstChars: [
158 "t",
159 "T",
160 "f",
161 "F"
162 ],
163 resolve: (source) => {
164 if (TRUE_VALUES$2.indexOf(source) !== -1) return true;
165 if (FALSE_VALUES$2.indexOf(source) !== -1) return false;
166 return NOT_RESOLVED;
167 },
168 identify: (object) => Object.prototype.toString.call(object) === "[object Boolean]",
169 represent: (object) => object ? "true" : "false"
170});
171//#endregion
172//#region src/tag/scalar/bool_json.ts
173var TRUE_VALUES$1 = ["true"];
174var FALSE_VALUES$1 = ["false"];
175/** @category Tags */
176var boolJsonTag = defineScalarTag("tag:yaml.org,2002:bool", {
177 implicit: true,
178 implicitFirstChars: ["t", "f"],
179 resolve: (source) => {
180 if (TRUE_VALUES$1.indexOf(source) !== -1) return true;
181 if (FALSE_VALUES$1.indexOf(source) !== -1) return false;
182 return NOT_RESOLVED;
183 },
184 identify: (object) => Object.prototype.toString.call(object) === "[object Boolean]",
185 represent: (object) => object ? "true" : "false"
186});
187//#endregion
188//#region src/tag/scalar/bool_yaml11.ts
189var TRUE_VALUES = [
190 "true",
191 "True",
192 "TRUE",
193 "y",
194 "Y",
195 "yes",
196 "Yes",
197 "YES",
198 "on",
199 "On",
200 "ON"
201];
202var FALSE_VALUES = [
203 "false",
204 "False",
205 "FALSE",
206 "n",
207 "N",
208 "no",
209 "No",
210 "NO",
211 "off",
212 "Off",
213 "OFF"
214];
215/** @category Tags */
216var boolYaml11Tag = defineScalarTag("tag:yaml.org,2002:bool", {
217 implicit: true,
218 implicitFirstChars: [
219 "y",
220 "Y",
221 "n",
222 "N",
223 "t",
224 "T",
225 "f",
226 "F",
227 "o",
228 "O"
229 ],
230 resolve: (source) => {
231 if (TRUE_VALUES.indexOf(source) !== -1) return true;
232 if (FALSE_VALUES.indexOf(source) !== -1) return false;
233 return NOT_RESOLVED;
234 },
235 identify: (object) => Object.prototype.toString.call(object) === "[object Boolean]",
236 represent: (object) => object ? "true" : "false"
237});
238//#endregion
239//#region src/tag/scalar/int_core.ts
240var YAML_INTEGER_IMPLICIT_PATTERN$1 = /* @__PURE__ */ new RegExp("^(?:0o[0-7]+|0x[0-9a-fA-F]+|[-+]?[0-9]+)$");
241var YAML_INTEGER_EXPLICIT_PATTERN$1 = /* @__PURE__ */ new RegExp("^(?:[-+]?0b[0-1]+|[-+]?0o[0-7]+|[-+]?0x[0-9a-fA-F]+|[-+]?[0-9]+)$");
242function parseYamlInteger$2(source) {
243 let value = source;
244 let sign = 1;
245 if (value[0] === "-" || value[0] === "+") {
246 if (value[0] === "-") sign = -1;
247 value = value.slice(1);
248 }
249 if (value.startsWith("0b")) return sign * parseInt(value.slice(2), 2);
250 if (value.startsWith("0o")) return sign * parseInt(value.slice(2), 8);
251 if (value.startsWith("0x")) return sign * parseInt(value.slice(2), 16);
252 return sign * parseInt(value, 10);
253}
254function resolveYamlInteger$2(source, isExplicit) {
255 if (isExplicit) {
256 if (!YAML_INTEGER_EXPLICIT_PATTERN$1.test(source)) return NOT_RESOLVED;
257 } else if (!YAML_INTEGER_IMPLICIT_PATTERN$1.test(source)) return NOT_RESOLVED;
258 const result = parseYamlInteger$2(source);
259 return Number.isFinite(result) ? result : NOT_RESOLVED;
260}
261/** @category Tags */
262var intCoreTag = defineScalarTag("tag:yaml.org,2002:int", {
263 implicit: true,
264 implicitFirstChars: [
265 "-",
266 "+",
267 ..."0123456789"
268 ],
269 resolve: resolveYamlInteger$2,
270 identify: (object) => Number.isInteger(object) && !Object.is(object, -0) && object.toString(10).indexOf("e") < 0,
271 represent: (object) => object.toString(10)
272});
273//#endregion
274//#region src/tag/scalar/int_json.ts
275var YAML_INTEGER_IMPLICIT_PATTERN = /* @__PURE__ */ new RegExp("^-?(?:0|[1-9][0-9]*)$");
276var YAML_INTEGER_EXPLICIT_PATTERN = /* @__PURE__ */ new RegExp("^(?:[-+]?0b[0-1]+|[-+]?0o[0-7]+|[-+]?0x[0-9a-fA-F]+|[-+]?[0-9]+)$");
277function parseYamlInteger$1(source) {
278 let value = source;
279 let sign = 1;
280 if (value[0] === "-" || value[0] === "+") {
281 if (value[0] === "-") sign = -1;
282 value = value.slice(1);
283 }
284 if (value.startsWith("0b")) return sign * parseInt(value.slice(2), 2);
285 if (value.startsWith("0o")) return sign * parseInt(value.slice(2), 8);
286 if (value.startsWith("0x")) return sign * parseInt(value.slice(2), 16);
287 return sign * parseInt(value, 10);
288}
289function resolveYamlInteger$1(source, isExplicit) {
290 if (isExplicit) {
291 if (!YAML_INTEGER_EXPLICIT_PATTERN.test(source)) return NOT_RESOLVED;
292 } else if (!YAML_INTEGER_IMPLICIT_PATTERN.test(source)) return NOT_RESOLVED;
293 const result = parseYamlInteger$1(source);
294 return Number.isFinite(result) ? result : NOT_RESOLVED;
295}
296/** @category Tags */
297var intJsonTag = defineScalarTag("tag:yaml.org,2002:int", {
298 implicit: true,
299 implicitFirstChars: ["-", ..."0123456789"],
300 resolve: resolveYamlInteger$1,
301 identify: (object) => Number.isInteger(object) && !Object.is(object, -0) && object.toString(10).indexOf("e") < 0,
302 represent: (object) => object.toString(10)
303});
304//#endregion
305//#region src/tag/scalar/int_yaml11.ts
306var YAML_INTEGER_PATTERN = /* @__PURE__ */ new RegExp("^(?:[-+]?0b[0-1_]+|[-+]?0[0-7_]+|[-+]?0x[0-9a-fA-F_]+|[-+]?[0-9][0-9_]*(?::[0-5]?[0-9])+|[-+]?(?:0|[1-9][0-9_]*))$");
307function parseYamlInteger(source) {
308 let value = source.replace(/_/g, "");
309 let sign = 1;
310 if (value[0] === "-" || value[0] === "+") {
311 if (value[0] === "-") sign = -1;
312 value = value.slice(1);
313 }
314 if (value.startsWith("0b")) return sign * parseInt(value.slice(2), 2);
315 if (value.startsWith("0x")) return sign * parseInt(value.slice(2), 16);
316 if (value.includes(":")) {
317 let result = 0;
318 for (const part of value.split(":")) result = result * 60 + Number(part);
319 return sign * result;
320 }
321 if (value !== "0" && value[0] === "0") return sign * parseInt(value, 8);
322 return sign * parseInt(value, 10);
323}
324function resolveYamlInteger(source) {
325 if (!YAML_INTEGER_PATTERN.test(source)) return NOT_RESOLVED;
326 const result = parseYamlInteger(source);
327 return Number.isFinite(result) ? result : NOT_RESOLVED;
328}
329/** @category Tags */
330var intYaml11Tag = defineScalarTag("tag:yaml.org,2002:int", {
331 implicit: true,
332 implicitFirstChars: [
333 "-",
334 "+",
335 ..."0123456789"
336 ],
337 resolve: resolveYamlInteger,
338 identify: (object) => Number.isInteger(object) && !Object.is(object, -0) && object.toString(10).indexOf("e") < 0,
339 represent: (object) => object.toString(10)
340});
341//#endregion
342//#region src/tag/scalar/float_core.ts
343var YAML_FLOAT_PATTERN$1 = /* @__PURE__ */ new RegExp("^(?:[-+]?[0-9]+(?:\\.[0-9]*)?(?:[eE][-+]?[0-9]+)?|[-+]?\\.[0-9]+(?:[eE][-+]?[0-9]+)?|[-+]?\\.(?:inf|Inf|INF)|\\.(?:nan|NaN|NAN))$");
344var YAML_FLOAT_SPECIAL_PATTERN$1 = /* @__PURE__ */ new RegExp("^(?:[-+]?\\.(?:inf|Inf|INF)|\\.(?:nan|NaN|NAN))$");
345function resolveYamlFloat$2(source) {
346 if (!YAML_FLOAT_PATTERN$1.test(source)) return NOT_RESOLVED;
347 let value = source.toLowerCase();
348 const sign = value[0] === "-" ? -1 : 1;
349 if ("+-".includes(value[0])) value = value.slice(1);
350 if (value === ".inf") return sign === 1 ? Number.POSITIVE_INFINITY : Number.NEGATIVE_INFINITY;
351 if (value === ".nan") return NaN;
352 const result = sign * parseFloat(value);
353 if (Number.isFinite(result) || YAML_FLOAT_SPECIAL_PATTERN$1.test(source)) return result;
354 return NOT_RESOLVED;
355}
356function representYamlFloat$2(object) {
357 if (isNaN(object)) return ".nan";
358 if (object === Number.POSITIVE_INFINITY) return ".inf";
359 if (object === Number.NEGATIVE_INFINITY) return "-.inf";
360 if (Object.is(object, -0)) return "-0.0";
361 const result = object.toString(10);
362 return /^[-+]?[0-9]+e/.test(result) ? result.replace("e", ".e") : result;
363}
364/** @category Tags */
365var floatCoreTag = defineScalarTag("tag:yaml.org,2002:float", {
366 implicit: true,
367 implicitFirstChars: [
368 "-",
369 "+",
370 ".",
371 ..."0123456789"
372 ],
373 resolve: resolveYamlFloat$2,
374 identify: (object) => typeof object === "number" && (!Number.isInteger(object) || Object.is(object, -0) || object.toString(10).indexOf("e") >= 0),
375 represent: representYamlFloat$2
376});
377//#endregion
378//#region src/tag/scalar/float_json.ts
379var YAML_FLOAT_IMPLICIT_PATTERN = /* @__PURE__ */ new RegExp("^-?(?:0|[1-9][0-9]*)(?:\\.[0-9]*)?(?:[eE][-+]?[0-9]+)?$");
380var YAML_FLOAT_EXPLICIT_PATTERN = /* @__PURE__ */ new RegExp("^(?:[-+]?[0-9]+(?:\\.[0-9]*)?(?:[eE][-+]?[0-9]+)?|[-+]?\\.[0-9]+(?:[eE][-+]?[0-9]+)?|[-+]?\\.(?:inf|Inf|INF)|\\.(?:nan|NaN|NAN))$");
381function resolveYamlFloat$1(source, isExplicit) {
382 if (isExplicit) {
383 if (!YAML_FLOAT_EXPLICIT_PATTERN.test(source)) return NOT_RESOLVED;
384 let value = source.toLowerCase();
385 const sign = value[0] === "-" ? -1 : 1;
386 if ("+-".includes(value[0])) value = value.slice(1);
387 if (value === ".inf") return sign === 1 ? Number.POSITIVE_INFINITY : Number.NEGATIVE_INFINITY;
388 if (value === ".nan") return NaN;
389 const result = sign * parseFloat(value);
390 return Number.isFinite(result) ? result : NOT_RESOLVED;
391 }
392 if (!YAML_FLOAT_IMPLICIT_PATTERN.test(source)) return NOT_RESOLVED;
393 const result = Number(source);
394 if (Number.isFinite(result)) return result;
395 return NOT_RESOLVED;
396}
397function representYamlFloat$1(object) {
398 if (isNaN(object)) return ".nan";
399 if (object === Number.POSITIVE_INFINITY) return ".inf";
400 if (object === Number.NEGATIVE_INFINITY) return "-.inf";
401 if (Object.is(object, -0)) return "-0.0";
402 const result = object.toString(10);
403 return /^[-+]?[0-9]+e/.test(result) ? result.replace("e", ".e") : result;
404}
405/** @category Tags */
406var floatJsonTag = defineScalarTag("tag:yaml.org,2002:float", {
407 implicit: true,
408 implicitFirstChars: ["-", ..."0123456789"],
409 resolve: resolveYamlFloat$1,
410 identify: (object) => typeof object === "number" && (!Number.isInteger(object) || Object.is(object, -0) || object.toString(10).indexOf("e") >= 0),
411 represent: representYamlFloat$1
412});
413//#endregion
414//#region src/tag/scalar/float_yaml11.ts
415var YAML_FLOAT_PATTERN = /* @__PURE__ */ new RegExp("^(?:[-+]?(?:(?:[0-9][0-9_]*)?\\.[0-9_]*)(?:[eE][-+][0-9]+)?|[-+]?[0-9][0-9_]*(?::[0-5]?[0-9])+\\.[0-9_]*|[-+]?\\.(?:inf|Inf|INF)|\\.(?:nan|NaN|NAN))$");
416var YAML_FLOAT_SPECIAL_PATTERN = /* @__PURE__ */ new RegExp("^(?:[-+]?\\.(?:inf|Inf|INF)|\\.(?:nan|NaN|NAN))$");
417function resolveYamlFloat(source) {
418 if (!YAML_FLOAT_PATTERN.test(source)) return NOT_RESOLVED;
419 let value = source.toLowerCase().replace(/_/g, "");
420 const sign = value[0] === "-" ? -1 : 1;
421 if ("+-".includes(value[0])) value = value.slice(1);
422 if (value === ".inf") return sign === 1 ? Number.POSITIVE_INFINITY : Number.NEGATIVE_INFINITY;
423 if (value === ".nan") return NaN;
424 let result = 0;
425 if (value.includes(":")) {
426 for (const part of value.split(":")) result = result * 60 + Number(part);
427 result *= sign;
428 } else result = sign * parseFloat(value);
429 if (Number.isFinite(result) || YAML_FLOAT_SPECIAL_PATTERN.test(source)) return result;
430 return NOT_RESOLVED;
431}
432function representYamlFloat(object) {
433 if (isNaN(object)) return ".nan";
434 if (object === Number.POSITIVE_INFINITY) return ".inf";
435 if (object === Number.NEGATIVE_INFINITY) return "-.inf";
436 if (Object.is(object, -0)) return "-0.0";
437 const result = object.toString(10);
438 return /^[-+]?[0-9]+e/.test(result) ? result.replace("e", ".e") : result;
439}
440/** @category Tags */
441var floatYaml11Tag = defineScalarTag("tag:yaml.org,2002:float", {
442 implicit: true,
443 implicitFirstChars: [
444 "-",
445 "+",
446 ".",
447 ..."0123456789"
448 ],
449 resolve: resolveYamlFloat,
450 identify: (object) => typeof object === "number" && (!Number.isInteger(object) || Object.is(object, -0) || object.toString(10).indexOf("e") >= 0),
451 represent: representYamlFloat
452});
453//#endregion
454//#region src/tag/scalar/merge.ts
455/**
456* Enables merge keys in {@link CORE_SCHEMA} when added with
457* {@link Schema.withTags}.
458*
459* @category Tags
460*/
461var mergeTag = defineScalarTag("tag:yaml.org,2002:merge", {
462 implicit: true,
463 implicitFirstChars: ["<"],
464 resolve: (source, isExplicit) => {
465 if (source === "<<" || isExplicit && source === "") return "<<";
466 return NOT_RESOLVED;
467 },
468 identify: () => false
469});
470//#endregion
471//#region src/tag/scalar/binary.ts
472var BASE64_PATTERN = /^[A-Za-z0-9+/]*={0,2}$/;
473function resolveYamlBinary(source) {
474 const input = source.replace(/\s/g, "");
475 if (input.length % 4 !== 0 || !BASE64_PATTERN.test(input)) return NOT_RESOLVED;
476 const binary = atob(input);
477 const result = new Uint8Array(binary.length);
478 for (let index = 0; index < binary.length; index++) result[index] = binary.charCodeAt(index);
479 return result;
480}
481function representYamlBinary(object) {
482 let binary = "";
483 for (let index = 0; index < object.length; index++) binary += String.fromCharCode(object[index]);
484 return btoa(binary);
485}
486/**
487* The `!!binary` tag, represented as a `Uint8Array`.
488*
489* @category Tags
490*/
491var binaryTag = defineScalarTag("tag:yaml.org,2002:binary", {
492 resolve: resolveYamlBinary,
493 identify: (object) => Object.prototype.toString.call(object) === "[object Uint8Array]",
494 represent: representYamlBinary
495});
496//#endregion
497//#region src/tag/scalar/timestamp.ts
498var YAML_DATE_REGEXP = /* @__PURE__ */ new RegExp("^([0-9][0-9][0-9][0-9])-([0-9][0-9])-([0-9][0-9])$");
499var YAML_TIMESTAMP_REGEXP = /* @__PURE__ */ new RegExp("^([0-9][0-9][0-9][0-9])-([0-9][0-9]?)-([0-9][0-9]?)(?:[Tt]|[ \\t]+)([0-9][0-9]?):([0-9][0-9]):([0-9][0-9])(?:\\.([0-9]*))?(?:[ \\t]*(Z|([-+])([0-9][0-9]?)(?::([0-9][0-9]))?))?$");
500function makeUtcDate(year, month, day, hour = 0, minute = 0, second = 0, fraction = 0) {
501 const date = new Date(Date.UTC(year, month, day, hour, minute, second, fraction));
502 date.setUTCFullYear(year, month, day);
503 return date;
504}
505function resolveYamlTimestamp(source) {
506 let match = YAML_DATE_REGEXP.exec(source);
507 if (match === null) match = YAML_TIMESTAMP_REGEXP.exec(source);
508 if (match === null) return NOT_RESOLVED;
509 const year = +match[1];
510 const month = +match[2] - 1;
511 const day = +match[3];
512 if (!match[4]) {
513 const date = makeUtcDate(year, month, day);
514 if (date.getUTCFullYear() !== year || date.getUTCMonth() !== month || date.getUTCDate() !== day) return NOT_RESOLVED;
515 return date;
516 }
517 const hour = +match[4];
518 const minute = +match[5];
519 const second = +match[6];
520 let fraction = 0;
521 if (hour > 23 || minute > 59 || second > 59) return NOT_RESOLVED;
522 if (match[7]) {
523 let value = match[7].slice(0, 3);
524 while (value.length < 3) value += "0";
525 fraction = +value;
526 }
527 const date = makeUtcDate(year, month, day, hour, minute, second, fraction);
528 if (date.getUTCFullYear() !== year || date.getUTCMonth() !== month || date.getUTCDate() !== day) return NOT_RESOLVED;
529 if (match[9]) {
530 const offsetHour = +match[10];
531 const offsetMinute = +(match[11] || 0);
532 if (offsetHour > 23 || offsetMinute > 59) return NOT_RESOLVED;
533 const offset = (offsetHour * 60 + offsetMinute) * 6e4;
534 date.setTime(date.getTime() - (match[9] === "-" ? -offset : offset));
535 }
536 return date;
537}
538/**
539* The YAML 1.1 `!!timestamp` tag, represented as a JavaScript `Date`.
540*
541* @category Tags
542*/
543var timestampTag = defineScalarTag("tag:yaml.org,2002:timestamp", {
544 implicit: true,
545 implicitFirstChars: [..."0123456789"],
546 resolve: resolveYamlTimestamp,
547 identify: (object) => object instanceof Date,
548 represent: (object) => object.toISOString()
549});
550//#endregion
551//#region src/tag/sequence/seq.ts
552/** @category Tags */
553var seqTag = defineSequenceTag("tag:yaml.org,2002:seq", {
554 create: () => [],
555 addItem: (container, item) => {
556 container.push(item);
557 },
558 identify: Array.isArray
559});
560//#endregion
561//#region src/common/object.ts
562function isPlainObject(data) {
563 if (data === null || typeof data !== "object" || Array.isArray(data)) return false;
564 const prototype = Object.getPrototypeOf(data);
565 return prototype === null || prototype === Object.prototype;
566}
567function pick(object, keys) {
568 const result = {};
569 for (const key of keys) if (object[key] !== void 0) result[key] = object[key];
570 return result;
571}
572//#endregion
573//#region src/tag/sequence/omap.ts
574/**
575* Provided only for YAML 1.1 compatibility and supported by the loader only.
576* JavaScript has no dedicated class to represent this type, so it cannot be
577* identified and dumped.
578*
579* ```yaml
580* !!omap
581* - one: 1
582* - two: 2
583* ```
584*
585* is loaded as
586*
587* ```javascript
588* [
589* { one: 1 },
590* { two: 2 }
591* ]
592* ```
593*
594* @category Tags
595*/
596var omapTag = defineSequenceTag("tag:yaml.org,2002:omap", {
597 create: () => ({
598 list: [],
599 seen: /* @__PURE__ */ new Set()
600 }),
601 addItem: (carrier, item) => {
602 let key;
603 if (item instanceof Map) {
604 if (item.size !== 1) return "cannot resolve an ordered map item";
605 key = item.keys().next().value;
606 } else if (isPlainObject(item)) {
607 const itemKeys = Object.keys(item);
608 if (itemKeys.length !== 1) return "cannot resolve an ordered map item";
609 key = itemKeys[0];
610 } else return "cannot resolve an ordered map item";
611 if (carrier.seen.has(key)) return "duplicate key in ordered map";
612 carrier.seen.add(key);
613 carrier.list.push(item);
614 return "";
615 },
616 finalize: (carrier) => carrier.list,
617 identify: () => false
618});
619//#endregion
620//#region src/tag/sequence/pairs.ts
621/**
622* Provided only for YAML 1.1 compatibility and supported by the loader only.
623* JavaScript has no dedicated class to represent this type, so it cannot be
624* identified and dumped.
625*
626* ```yaml
627* !!pairs
628* - one: 1
629* - two: 2
630* ```
631*
632* is loaded as
633*
634* ```javascript
635* [
636* ['one', 1],
637* ['two', 2]
638* ]
639* ```
640*
641* @category Tags
642*/
643var pairsTag = defineSequenceTag("tag:yaml.org,2002:pairs", {
644 create: () => [],
645 addItem: (container, item) => {
646 if (item instanceof Map) {
647 if (item.size !== 1) return "cannot resolve a pairs item";
648 container.push(item.entries().next().value);
649 return "";
650 }
651 if (Object.prototype.toString.call(item) !== "[object Object]") return "cannot resolve a pairs item";
652 const object = item;
653 const keys = Object.keys(object);
654 if (keys.length !== 1) return "cannot resolve a pairs item";
655 container.push([keys[0], object[keys[0]]]);
656 return "";
657 },
658 identify: () => false
659});
660//#endregion
661//#region src/tag/mapping/map.ts
662/**
663* This is the default mapping implementation. It uses `{}` objects and has only
664* partial functionality due to language limitations. This choice was made
665* because users expect to get JavaScript objects, and it was left unchanged to
666* avoid too many breaking changes in the v5 release.
667*
668* Side effects:
669*
670* - `Object.hasOwn()` checks or `for...of` loops are required for safe use (to
671* avoid falling through to prototypes).
672* - Only scalar string keys are supported properly.
673* - Other scalar keys, such as `null` and numbers, are converted to strings.
674* This is historical behaviour, and it can cause side effects such as
675* problems with `!!merge`.
676*
677* Note that non-string scalar keys may be deprecated in future versions.
678*
679* Ideally, use {@link realMapTag} instead.
680*
681* @category Tags
682*/
683var mapTag = defineMappingTag("tag:yaml.org,2002:map", {
684 create: () => ({}),
685 identify: isPlainObject,
686 represent: (o) => {
687 const map = /* @__PURE__ */ new Map();
688 for (const key of Object.keys(o)) map.set(key, o[key]);
689 return map;
690 },
691 addPair: (container, key, value) => {
692 if (key !== null && typeof key === "object") return "object-based map does not support complex keys";
693 const normalizedKey = String(key);
694 if (normalizedKey === "__proto__") Object.defineProperty(container, normalizedKey, {
695 value,
696 enumerable: true,
697 configurable: true,
698 writable: true
699 });
700 else container[normalizedKey] = value;
701 return "";
702 },
703 has: (container, key) => {
704 if (key !== null && typeof key === "object") return false;
705 return Object.prototype.hasOwnProperty.call(container, String(key));
706 },
707 keys: (container) => Object.keys(container),
708 get: (container, key) => {
709 const normalizedKey = String(key);
710 if (!Object.prototype.hasOwnProperty.call(container, normalizedKey)) return null;
711 return container[normalizedKey];
712 }
713});
714//#endregion
715//#region src/tag/mapping/set.ts
716/**
717* The YAML 1.1 `!!set` tag, represented as a JavaScript `Set`.
718*
719* @category Tags
720*/
721var setTag = defineMappingTag("tag:yaml.org,2002:set", {
722 create: () => /* @__PURE__ */ new Set(),
723 identify: (data) => data instanceof Set,
724 represent: (data) => {
725 const map = /* @__PURE__ */ new Map();
726 for (const key of data) map.set(key, null);
727 return map;
728 },
729 addPair: (container, key, value) => {
730 if (value !== null) return "cannot resolve a set item";
731 container.add(key);
732 return "";
733 },
734 has: (container, key) => container.has(key),
735 keys: (container) => container.keys(),
736 get: () => null
737});
738//#endregion
739//#region src/schema.ts
740function createTagDefinitionMap() {
741 return {
742 scalar: Object.create(null),
743 sequence: Object.create(null),
744 mapping: Object.create(null)
745 };
746}
747function createTagDefinitionListMap() {
748 return {
749 scalar: [],
750 sequence: [],
751 mapping: []
752 };
753}
754function compileTags(tags) {
755 const result = [];
756 for (const tag of tags) {
757 let index = result.length;
758 for (let previousIndex = 0; previousIndex < result.length; previousIndex++) {
759 const previous = result[previousIndex];
760 if (previous.nodeKind === tag.nodeKind && previous.tagName === tag.tagName && previous.matchByTagPrefix === tag.matchByTagPrefix) {
761 index = previousIndex;
762 break;
763 }
764 }
765 result[index] = tag;
766 }
767 return result;
768}
769/**
770* Controls tag resolution when loading and type selection when dumping.
771*
772* @category Schemas
773*/
774var Schema = class Schema {
775 tags;
776 /** @internal */
777 implicitScalarTags;
778 /**
779 * Dispatch implicit scalar resolvers by `source.charAt(0)`. Each bucket holds
780 * the resolvers that may match that key, in schema order; a key absent from
781 * the map uses
782 * {@link Schema.implicitScalarAnyFirstChar}
783 * (resolvers that declared no first-char constraint, so they apply to any
784 * first character).
785 */
786 implicitScalarByFirstChar;
787 implicitScalarAnyFirstChar;
788 /**
789 * The default scalar tag (`!!str`), resolved once so the composer's fallback
790 * for unresolved plain scalars avoids a keyed lookup per scalar.
791 *
792 * @internal
793 */
794 defaultScalarTag;
795 /**
796 * The default container tags (`!!seq` / `!!map`), used by the dumper: when a
797 * value is identified by its default tag, the tag is implicit and not
798 * printed. Undefined if the schema does not define them (then such values
799 * can't be dumped).
800 *
801 * @internal
802 */
803 defaultSequenceTag;
804 /** @internal */
805 defaultMappingTag;
806 exact;
807 prefix;
808 constructor(tags) {
809 const compiledTags = compileTags(tags);
810 const implicitScalarTags = [];
811 const exact = createTagDefinitionMap();
812 const prefix = createTagDefinitionListMap();
813 for (const tag of compiledTags) {
814 if (tag.nodeKind === "scalar" && tag.implicit) {
815 if (tag.matchByTagPrefix) throw new Error("Implicit scalar tags cannot match by tag prefix");
816 implicitScalarTags.push(tag);
817 }
818 switch (tag.nodeKind) {
819 case "scalar":
820 if (tag.matchByTagPrefix) prefix.scalar.push(tag);
821 else exact.scalar[tag.tagName] = tag;
822 break;
823 case "sequence":
824 if (tag.matchByTagPrefix) prefix.sequence.push(tag);
825 else exact.sequence[tag.tagName] = tag;
826 break;
827 case "mapping": if (tag.matchByTagPrefix) prefix.mapping.push(tag);
828 else exact.mapping[tag.tagName] = tag;
829 }
830 }
831 const implicitScalarAnyFirstChar = implicitScalarTags.filter((tag) => tag.implicitFirstChars === null);
832 const keys = /* @__PURE__ */ new Set();
833 for (const tag of implicitScalarTags) if (tag.implicitFirstChars !== null) for (const key of tag.implicitFirstChars) keys.add(key);
834 const implicitScalarByFirstChar = /* @__PURE__ */ new Map();
835 for (const key of keys) implicitScalarByFirstChar.set(key, implicitScalarTags.filter((tag) => tag.implicitFirstChars === null || tag.implicitFirstChars.indexOf(key) !== -1));
836 const defaultScalarTag = exact.scalar["tag:yaml.org,2002:str"];
837 if (!defaultScalarTag) throw new Error("schema does not define the default scalar tag (tag:yaml.org,2002:str)");
838 this.tags = compiledTags;
839 this.implicitScalarTags = implicitScalarTags;
840 this.implicitScalarByFirstChar = implicitScalarByFirstChar;
841 this.implicitScalarAnyFirstChar = implicitScalarAnyFirstChar;
842 this.defaultScalarTag = defaultScalarTag;
843 this.defaultSequenceTag = exact.sequence["tag:yaml.org,2002:seq"];
844 this.defaultMappingTag = exact.mapping["tag:yaml.org,2002:map"];
845 this.exact = exact;
846 this.prefix = prefix;
847 }
848 /** @internal */
849 lookupScalarTag(tagName) {
850 const exactTag = this.exact.scalar[tagName];
851 if (exactTag) return exactTag;
852 for (const tag of this.prefix.scalar) if (tagName.startsWith(tag.tagName)) return tag;
853 }
854 /** @internal */
855 lookupSequenceTag(tagName) {
856 const exactTag = this.exact.sequence[tagName];
857 if (exactTag) return exactTag;
858 for (const tag of this.prefix.sequence) if (tagName.startsWith(tag.tagName)) return tag;
859 }
860 /** @internal */
861 lookupMappingTag(tagName) {
862 const exactTag = this.exact.mapping[tagName];
863 if (exactTag) return exactTag;
864 for (const tag of this.prefix.mapping) if (tagName.startsWith(tag.tagName)) return tag;
865 }
866 /** @internal */
867 resolveImplicitScalarTag(source) {
868 const candidates = this.implicitScalarByFirstChar.get(source.charAt(0)) ?? this.implicitScalarAnyFirstChar;
869 for (const tag of candidates) {
870 const value = tag.resolve(source, false, tag.tagName);
871 if (value !== NOT_RESOLVED) return {
872 value,
873 tag
874 };
875 }
876 const tag = this.defaultScalarTag;
877 return {
878 value: tag.resolve(source, false, tag.tagName),
879 tag
880 };
881 }
882 /**
883 * Creates a new schema with the specified tags added. If a tag already
884 * exists, it is replaced by the specified tag.
885 *
886 * @example
887 *
888 * ```javascript
889 * import { CORE_SCHEMA, mergeTag, realMapTag } from 'js-yaml'
890 *
891 * const schema = CORE_SCHEMA.withTags(mergeTag, realMapTag)
892 * ```
893 */
894 withTags(...tags) {
895 let flatTags = [];
896 for (const tag of tags) flatTags = flatTags.concat(tag);
897 return new Schema([...this.tags, ...flatTags]);
898 }
899};
900/**
901* The YAML 1.2 Failsafe Schema: strings, sequences, and mappings.
902*
903* @category Schemas
904*/
905var FAILSAFE_SCHEMA = new Schema([
906 strTag,
907 seqTag,
908 mapTag
909]);
910/**
911* The YAML 1.2 JSON Schema. It uses JSON scalar forms while retaining YAML
912* collection syntax.
913*
914* @category Schemas
915*/
916var JSON_SCHEMA = new Schema([
917 ...FAILSAFE_SCHEMA.tags,
918 nullJsonTag,
919 boolJsonTag,
920 intJsonTag,
921 floatJsonTag
922]);
923/**
924* The default schema for the loaders. Note, {@link CORE_SCHEMA} comes
925* without the `!!merge` tag. You can easily enable it if needed.
926*
927* @example
928* Enable {@link mergeTag}:
929*
930* ```javascript
931* import { load, CORE_SCHEMA, mergeTag } from 'js-yaml'
932*
933* try {
934* load(data, { schema: CORE_SCHEMA.withTags(mergeTag) })
935* } catch (e) {
936* console.error(e)
937* }
938* ```
939*
940* @category Schemas
941*/
942var CORE_SCHEMA = new Schema([
943 ...FAILSAFE_SCHEMA.tags,
944 nullCoreTag,
945 boolCoreTag,
946 intCoreTag,
947 floatCoreTag
948]);
949/**
950* YAML 1.1-compatible schema.
951*
952* @category Schemas
953*/
954var YAML11_SCHEMA = new Schema([
955 ...FAILSAFE_SCHEMA.tags,
956 nullYaml11Tag,
957 boolYaml11Tag,
958 intYaml11Tag,
959 floatYaml11Tag,
960 timestampTag,
961 mergeTag,
962 binaryTag,
963 omapTag,
964 pairsTag,
965 setTag
966]);
967/**
968* The dumper schema for maximum compatibility. It combines all supported type
969* variants from YAML 1.1 and YAML 1.2 so strings matching any of them are
970* quoted. This makes the generated YAML more compatible with other parsers.
971*
972* The schema is based on YAML 1.1, but extends `!!int` and `!!float` to accept
973* both YAML 1.1 and Core Schema forms, since Core Schema supports some forms
974* that YAML 1.1 does not.
975*
976* @category Schemas
977*/
978var DUMP_SCHEMA = YAML11_SCHEMA.withTags({
979 ...intYaml11Tag,
980 resolve: (source, isExplicit, tagName) => {
981 const result = intYaml11Tag.resolve(source, isExplicit, tagName);
982 return result === NOT_RESOLVED ? intCoreTag.resolve(source, isExplicit, tagName) : result;
983 }
984}, {
985 ...floatYaml11Tag,
986 resolve: (source, isExplicit, tagName) => {
987 const result = floatYaml11Tag.resolve(source, isExplicit, tagName);
988 return result === NOT_RESOLVED ? floatCoreTag.resolve(source, isExplicit, tagName) : result;
989 }
990});
991//#endregion
992//#region src/tag/mapping/real_map.ts
993/**
994* Recommended when non-string keys are actually needed. It uses native
995* JavaScript `Map` objects, so keys keep their constructed types instead of
996* being converted to strings.
997*
998* It is not the default to avoid widespread breaking changes in existing
999* projects. `Map` has a different access API and does not pass deep equality
1000* checks against `{}`-based fixtures. Alongside the other changes in v5,
1001* making it the default was considered too disruptive.
1002*
1003* If these differences are acceptable for your project, we recommend using
1004* {@link realMapTag} to guarantee the absence of problems and side effects.
1005*
1006* @example
1007* Enable {@link realMapTag}:
1008*
1009* ```javascript
1010* import { load, CORE_SCHEMA, realMapTag } from 'js-yaml'
1011*
1012* try {
1013* load(data, { schema: CORE_SCHEMA.withTags(realMapTag) })
1014* } catch (e) {
1015* console.error(e)
1016* }
1017* ```
1018*
1019* @category Tags
1020*/
1021var realMapTag = defineMappingTag("tag:yaml.org,2002:map", {
1022 create: () => /* @__PURE__ */ new Map(),
1023 addPair: (container, key, value) => {
1024 container.set(key, value);
1025 return "";
1026 },
1027 has: (container, key) => container.has(key),
1028 keys: (container) => container.keys(),
1029 get: (container, key) => container.get(key),
1030 identify: (data) => data instanceof Map || isPlainObject(data),
1031 represent: (data) => {
1032 if (data instanceof Map) return data;
1033 const map = /* @__PURE__ */ new Map();
1034 const obj = data;
1035 for (const key of Object.keys(obj)) map.set(key, obj[key]);
1036 return map;
1037 }
1038});
1039//#endregion
1040//#region src/tag/mapping/legacy_map.ts
1041function normalizeKey(key) {
1042 if (Array.isArray(key)) {
1043 const array = Array.prototype.slice.call(key);
1044 for (let index = 0; index < array.length; index++) {
1045 if (Array.isArray(array[index])) return null;
1046 if (typeof array[index] === "object" && Object.prototype.toString.call(array[index]) === "[object Object]") array[index] = "[object Object]";
1047 }
1048 return String(array);
1049 }
1050 if (typeof key === "object" && Object.prototype.toString.call(key) === "[object Object]") return "[object Object]";
1051 return String(key);
1052}
1053/**
1054* This implementation exists solely to reproduce v4 behavior exactly. Its use
1055* is strongly discouraged. If complex or non-string keys are needed, use
1056* {@link realMapTag} instead.
1057*
1058* @category Tags
1059*/
1060var legacyMapTag = defineMappingTag("tag:yaml.org,2002:map", {
1061 create: () => ({}),
1062 identify: isPlainObject,
1063 represent: (o) => {
1064 const map = /* @__PURE__ */ new Map();
1065 for (const key of Object.keys(o)) map.set(key, o[key]);
1066 return map;
1067 },
1068 addPair: (container, key, value) => {
1069 const normalizedKey = normalizeKey(key);
1070 if (normalizedKey === null) return "nested arrays are not supported inside keys";
1071 if (normalizedKey === "__proto__") Object.defineProperty(container, normalizedKey, {
1072 value,
1073 enumerable: true,
1074 configurable: true,
1075 writable: true
1076 });
1077 else container[normalizedKey] = value;
1078 return "";
1079 },
1080 has: (container, key) => {
1081 const normalizedKey = normalizeKey(key);
1082 return normalizedKey !== null && Object.prototype.hasOwnProperty.call(container, normalizedKey);
1083 },
1084 keys: (container) => Object.keys(container),
1085 get: (container, key) => {
1086 const normalizedKey = String(key);
1087 if (!Object.prototype.hasOwnProperty.call(container, normalizedKey)) return null;
1088 return container[normalizedKey];
1089 }
1090});
1091//#endregion
1092//#region src/common/snippet.ts
1093var DEFAULT_SNIPPET_OPTIONS = {
1094 maxLength: 79,
1095 indent: 1,
1096 linesBefore: 3,
1097 linesAfter: 2
1098};
1099function getLine(buffer, lineStart, lineEnd, position, maxLineLength) {
1100 let head = "";
1101 let tail = "";
1102 const maxHalfLength = Math.floor(maxLineLength / 2) - 1;
1103 if (position - lineStart > maxHalfLength) {
1104 head = " ... ";
1105 lineStart = position - maxHalfLength + head.length;
1106 }
1107 if (lineEnd - position > maxHalfLength) {
1108 tail = " ...";
1109 lineEnd = position + maxHalfLength - tail.length;
1110 }
1111 return {
1112 str: head + buffer.slice(lineStart, lineEnd).replace(/\t/g, "→") + tail,
1113 pos: position - lineStart + head.length
1114 };
1115}
1116function padStart(string, max) {
1117 return " ".repeat(Math.max(max - string.length, 0)) + string;
1118}
1119function makeSnippet(mark, options) {
1120 if (!mark.buffer) return null;
1121 const opts = {
1122 ...DEFAULT_SNIPPET_OPTIONS,
1123 ...options
1124 };
1125 const re = /\r?\n|\r|\0/g;
1126 const lineStarts = [0];
1127 const lineEnds = [];
1128 let match;
1129 let foundLineNo = -1;
1130 while (match = re.exec(mark.buffer)) {
1131 lineEnds.push(match.index);
1132 lineStarts.push(match.index + match[0].length);
1133 if (mark.position <= match.index && foundLineNo < 0) foundLineNo = lineStarts.length - 2;
1134 }
1135 if (foundLineNo < 0) foundLineNo = lineStarts.length - 1;
1136 let result = "";
1137 const lineNoLength = Math.min(mark.line + opts.linesAfter, lineEnds.length).toString().length;
1138 const maxLineLength = opts.maxLength - (opts.indent + lineNoLength + 3);
1139 for (let i = 1; i <= opts.linesBefore; i++) {
1140 if (foundLineNo - i < 0) break;
1141 const line = getLine(mark.buffer, lineStarts[foundLineNo - i], lineEnds[foundLineNo - i], mark.position - (lineStarts[foundLineNo] - lineStarts[foundLineNo - i]), maxLineLength);
1142 result = `${" ".repeat(opts.indent)}${padStart((mark.line - i + 1).toString(), lineNoLength)} | ${line.str}\n${result}`;
1143 }
1144 const line = getLine(mark.buffer, lineStarts[foundLineNo], lineEnds[foundLineNo], mark.position, maxLineLength);
1145 result += `${" ".repeat(opts.indent)}${padStart((mark.line + 1).toString(), lineNoLength)} | ${line.str}\n`;
1146 result += `${"-".repeat(opts.indent + lineNoLength + 3 + line.pos)}^\n`;
1147 for (let i = 1; i <= opts.linesAfter; i++) {
1148 if (foundLineNo + i >= lineEnds.length) break;
1149 const line = getLine(mark.buffer, lineStarts[foundLineNo + i], lineEnds[foundLineNo + i], mark.position - (lineStarts[foundLineNo] - lineStarts[foundLineNo + i]), maxLineLength);
1150 result += `${" ".repeat(opts.indent)}${padStart((mark.line + i + 1).toString(), lineNoLength)} | ${line.str}\n`;
1151 }
1152 return result.replace(/\n$/, "");
1153}
1154//#endregion
1155//#region src/common/exception.ts
1156function formatError(exception, compact) {
1157 let where = "";
1158 if (!exception.mark) return exception.reason;
1159 if (exception.mark.name) where += `in "${exception.mark.name}" `;
1160 where += `(${exception.mark.line + 1}:${exception.mark.column + 1})`;
1161 if (!compact && exception.mark.snippet) where += `\n\n${exception.mark.snippet}`;
1162 return `${exception.reason} ${where}`;
1163}
1164/**
1165* A YAML error. Unlike an ordinary `Error`, it adds a source snippet showing
1166* the location of the problem to the error message, when available.
1167*
1168* @category Main
1169*/
1170var YAMLException = class YAMLException extends Error {
1171 reason;
1172 mark;
1173 /**
1174 * Optional `mark` contains source snippet data. Usually, use
1175 * {@link YAMLException.throwAt} instead of passing it directly.
1176 */
1177 constructor(reason, mark) {
1178 super();
1179 this.name = "YAMLException";
1180 this.reason = reason;
1181 this.mark = mark;
1182 this.message = formatError(this, false);
1183 if (Error.captureStackTrace) Error.captureStackTrace(this, this.constructor);
1184 }
1185 /**
1186 * Returns the formatted error, omitting the source snippet in compact mode.
1187 */
1188 toString(compact) {
1189 return `${this.name}: ${formatError(this, compact)}`;
1190 }
1191 /**
1192 * Builds a YAMLException with a source snippet and throws it. `source` is
1193 * the raw input text; `position` is an offset into it.
1194 */
1195 static throwAt(source, position, message, filename = "") {
1196 let line = 0;
1197 let lineStart = 0;
1198 for (let index = 0; index < position; index++) {
1199 const ch = source.charCodeAt(index);
1200 if (ch === 10) {hooks/vendor/mustache/mustache.mjs 765 lines1/*!
2 * mustache.js - Logic-less {{mustache}} templates with JavaScript
3 * http://github.com/janl/mustache.js
4 */
5
6var objectToString = Object.prototype.toString;
7var isArray = Array.isArray || function isArrayPolyfill (object) {
8 return objectToString.call(object) === '[object Array]';
9};
10
11function isFunction (object) {
12 return typeof object === 'function';
13}
14
15/**
16 * More correct typeof string handling array
17 * which normally returns typeof 'object'
18 */
19function typeStr (obj) {
20 return isArray(obj) ? 'array' : typeof obj;
21}
22
23function escapeRegExp (string) {
24 return string.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g, '\\$&');
25}
26
27/**
28 * Null safe way of checking whether or not an object,
29 * including its prototype, has a given property
30 */
31function hasProperty (obj, propName) {
32 return obj != null && typeof obj === 'object' && (propName in obj);
33}
34
35/**
36 * Safe way of detecting whether or not the given thing is a primitive and
37 * whether it has the given property
38 */
39function primitiveHasOwnProperty (primitive, propName) {
40 return (
41 primitive != null
42 && typeof primitive !== 'object'
43 && primitive.hasOwnProperty
44 && primitive.hasOwnProperty(propName)
45 );
46}
47
48// Workaround for https://issues.apache.org/jira/browse/COUCHDB-577
49// See https://github.com/janl/mustache.js/issues/189
50var regExpTest = RegExp.prototype.test;
51function testRegExp (re, string) {
52 return regExpTest.call(re, string);
53}
54
55var nonSpaceRe = /\S/;
56function isWhitespace (string) {
57 return !testRegExp(nonSpaceRe, string);
58}
59
60var entityMap = {
61 '&': '&',
62 '<': '<',
63 '>': '>',
64 '"': '"',
65 "'": ''',
66 '/': '/',
67 '`': '`',
68 '=': '='
69};
70
71function escapeHtml (string) {
72 return String(string).replace(/[&<>"'`=\/]/g, function fromEntityMap (s) {
73 return entityMap[s];
74 });
75}
76
77var whiteRe = /\s*/;
78var spaceRe = /\s+/;
79var equalsRe = /\s*=/;
80var curlyRe = /\s*\}/;
81var tagRe = /#|\^|\/|>|\{|&|=|!/;
82
83/**
84 * Breaks up the given `template` string into a tree of tokens. If the `tags`
85 * argument is given here it must be an array with two string values: the
86 * opening and closing tags used in the template (e.g. [ "<%", "%>" ]). Of
87 * course, the default is to use mustaches (i.e. mustache.tags).
88 *
89 * A token is an array with at least 4 elements. The first element is the
90 * mustache symbol that was used inside the tag, e.g. "#" or "&". If the tag
91 * did not contain a symbol (i.e. {{myValue}}) this element is "name". For
92 * all text that appears outside a symbol this element is "text".
93 *
94 * The second element of a token is its "value". For mustache tags this is
95 * whatever else was inside the tag besides the opening symbol. For text tokens
96 * this is the text itself.
97 *
98 * The third and fourth elements of the token are the start and end indices,
99 * respectively, of the token in the original template.
100 *
101 * Tokens that are the root node of a subtree contain two more elements: 1) an
102 * array of tokens in the subtree and 2) the index in the original template at
103 * which the closing tag for that section begins.
104 *
105 * Tokens for partials also contain two more elements: 1) a string value of
106 * indendation prior to that tag and 2) the index of that tag on that line -
107 * eg a value of 2 indicates the partial is the third tag on this line.
108 */
109function parseTemplate (template, tags) {
110 if (!template)
111 return [];
112 var lineHasNonSpace = false;
113 var sections = []; // Stack to hold section tokens
114 var tokens = []; // Buffer to hold the tokens
115 var spaces = []; // Indices of whitespace tokens on the current line
116 var hasTag = false; // Is there a {{tag}} on the current line?
117 var nonSpace = false; // Is there a non-space char on the current line?
118 var indentation = ''; // Tracks indentation for tags that use it
119 var tagIndex = 0; // Stores a count of number of tags encountered on a line
120
121 // Strips all whitespace tokens array for the current line
122 // if there was a {{#tag}} on it and otherwise only space.
123 function stripSpace () {
124 if (hasTag && !nonSpace) {
125 while (spaces.length)
126 delete tokens[spaces.pop()];
127 } else {
128 spaces = [];
129 }
130
131 hasTag = false;
132 nonSpace = false;
133 }
134
135 var openingTagRe, closingTagRe, closingCurlyRe;
136 function compileTags (tagsToCompile) {
137 if (typeof tagsToCompile === 'string')
138 tagsToCompile = tagsToCompile.split(spaceRe, 2);
139
140 if (!isArray(tagsToCompile) || tagsToCompile.length !== 2)
141 throw new Error('Invalid tags: ' + tagsToCompile);
142
143 openingTagRe = new RegExp(escapeRegExp(tagsToCompile[0]) + '\\s*');
144 closingTagRe = new RegExp('\\s*' + escapeRegExp(tagsToCompile[1]));
145 closingCurlyRe = new RegExp('\\s*' + escapeRegExp('}' + tagsToCompile[1]));
146 }
147
148 compileTags(tags || mustache.tags);
149
150 var scanner = new Scanner(template);
151
152 var start, type, value, chr, token, openSection;
153 while (!scanner.eos()) {
154 start = scanner.pos;
155
156 // Match any text between tags.
157 value = scanner.scanUntil(openingTagRe);
158
159 if (value) {
160 for (var i = 0, valueLength = value.length; i < valueLength; ++i) {
161 chr = value.charAt(i);
162
163 if (isWhitespace(chr)) {
164 spaces.push(tokens.length);
165 indentation += chr;
166 } else {
167 nonSpace = true;
168 lineHasNonSpace = true;
169 indentation += ' ';
170 }
171
172 tokens.push([ 'text', chr, start, start + 1 ]);
173 start += 1;
174
175 // Check for whitespace on the current line.
176 if (chr === '\n') {
177 stripSpace();
178 indentation = '';
179 tagIndex = 0;
180 lineHasNonSpace = false;
181 }
182 }
183 }
184
185 // Match the opening tag.
186 if (!scanner.scan(openingTagRe))
187 break;
188
189 hasTag = true;
190
191 // Get the tag type.
192 type = scanner.scan(tagRe) || 'name';
193 scanner.scan(whiteRe);
194
195 // Get the tag value.
196 if (type === '=') {
197 value = scanner.scanUntil(equalsRe);
198 scanner.scan(equalsRe);
199 scanner.scanUntil(closingTagRe);
200 } else if (type === '{') {
201 value = scanner.scanUntil(closingCurlyRe);
202 scanner.scan(curlyRe);
203 scanner.scanUntil(closingTagRe);
204 type = '&';
205 } else {
206 value = scanner.scanUntil(closingTagRe);
207 }
208
209 // Match the closing tag.
210 if (!scanner.scan(closingTagRe))
211 throw new Error('Unclosed tag at ' + scanner.pos);
212
213 if (type == '>') {
214 token = [ type, value, start, scanner.pos, indentation, tagIndex, lineHasNonSpace ];
215 } else {
216 token = [ type, value, start, scanner.pos ];
217 }
218 tagIndex++;
219 tokens.push(token);
220
221 if (type === '#' || type === '^') {
222 sections.push(token);
223 } else if (type === '/') {
224 // Check section nesting.
225 openSection = sections.pop();
226
227 if (!openSection)
228 throw new Error('Unopened section "' + value + '" at ' + start);
229
230 if (openSection[1] !== value)
231 throw new Error('Unclosed section "' + openSection[1] + '" at ' + start);
232 } else if (type === 'name' || type === '{' || type === '&') {
233 nonSpace = true;
234 } else if (type === '=') {
235 // Set the tags for the next time around.
236 compileTags(value);
237 }
238 }
239
240 stripSpace();
241
242 // Make sure there are no open sections when we're done.
243 openSection = sections.pop();
244
245 if (openSection)
246 throw new Error('Unclosed section "' + openSection[1] + '" at ' + scanner.pos);
247
248 return nestTokens(squashTokens(tokens));
249}
250
251/**
252 * Combines the values of consecutive text tokens in the given `tokens` array
253 * to a single token.
254 */
255function squashTokens (tokens) {
256 var squashedTokens = [];
257
258 var token, lastToken;
259 for (var i = 0, numTokens = tokens.length; i < numTokens; ++i) {
260 token = tokens[i];
261
262 if (token) {
263 if (token[0] === 'text' && lastToken && lastToken[0] === 'text') {
264 lastToken[1] += token[1];
265 lastToken[3] = token[3];
266 } else {
267 squashedTokens.push(token);
268 lastToken = token;
269 }
270 }
271 }
272
273 return squashedTokens;
274}
275
276/**
277 * Forms the given array of `tokens` into a nested tree structure where
278 * tokens that represent a section have two additional items: 1) an array of
279 * all tokens that appear in that section and 2) the index in the original
280 * template that represents the end of that section.
281 */
282function nestTokens (tokens) {
283 var nestedTokens = [];
284 var collector = nestedTokens;
285 var sections = [];
286
287 var token, section;
288 for (var i = 0, numTokens = tokens.length; i < numTokens; ++i) {
289 token = tokens[i];
290
291 switch (token[0]) {
292 case '#':
293 case '^':
294 collector.push(token);
295 sections.push(token);
296 collector = token[4] = [];
297 break;
298 case '/':
299 section = sections.pop();
300 section[5] = token[2];
301 collector = sections.length > 0 ? sections[sections.length - 1][4] : nestedTokens;
302 break;
303 default:
304 collector.push(token);
305 }
306 }
307
308 return nestedTokens;
309}
310
311/**
312 * A simple string scanner that is used by the template parser to find
313 * tokens in template strings.
314 */
315function Scanner (string) {
316 this.string = string;
317 this.tail = string;
318 this.pos = 0;
319}
320
321/**
322 * Returns `true` if the tail is empty (end of string).
323 */
324Scanner.prototype.eos = function eos () {
325 return this.tail === '';
326};
327
328/**
329 * Tries to match the given regular expression at the current position.
330 * Returns the matched text if it can match, the empty string otherwise.
331 */
332Scanner.prototype.scan = function scan (re) {
333 var match = this.tail.match(re);
334
335 if (!match || match.index !== 0)
336 return '';
337
338 var string = match[0];
339
340 this.tail = this.tail.substring(string.length);
341 this.pos += string.length;
342
343 return string;
344};
345
346/**
347 * Skips all text until the given regular expression can be matched. Returns
348 * the skipped string, which is the entire tail if no match can be made.
349 */
350Scanner.prototype.scanUntil = function scanUntil (re) {
351 var index = this.tail.search(re), match;
352
353 switch (index) {
354 case -1:
355 match = this.tail;
356 this.tail = '';
357 break;
358 case 0:
359 match = '';
360 break;
361 default:
362 match = this.tail.substring(0, index);
363 this.tail = this.tail.substring(index);
364 }
365
366 this.pos += match.length;
367
368 return match;
369};
370
371/**
372 * Represents a rendering context by wrapping a view object and
373 * maintaining a reference to the parent context.
374 */
375function Context (view, parentContext) {
376 this.view = view;
377 this.cache = { '.': this.view };
378 this.parent = parentContext;
379}
380
381/**
382 * Creates a new context using the given view with this context
383 * as the parent.
384 */
385Context.prototype.push = function push (view) {
386 return new Context(view, this);
387};
388
389/**
390 * Returns the value of the given name in this context, traversing
391 * up the context hierarchy if the value is absent in this context's view.
392 */
393Context.prototype.lookup = function lookup (name) {
394 var cache = this.cache;
395
396 var value;
397 if (cache.hasOwnProperty(name)) {
398 value = cache[name];
399 } else {
400 var context = this, intermediateValue, names, index, lookupHit = false;
401
402 while (context) {
403 if (name.indexOf('.') > 0) {
404 intermediateValue = context.view;
405 names = name.split('.');
406 index = 0;
407
408 /**
409 * Using the dot notion path in `name`, we descend through the
410 * nested objects.
411 *
412 * To be certain that the lookup has been successful, we have to
413 * check if the last object in the path actually has the property
414 * we are looking for. We store the result in `lookupHit`.
415 *
416 * This is specially necessary for when the value has been set to
417 * `undefined` and we want to avoid looking up parent contexts.
418 *
419 * In the case where dot notation is used, we consider the lookup
420 * to be successful even if the last "object" in the path is
421 * not actually an object but a primitive (e.g., a string, or an
422 * integer), because it is sometimes useful to access a property
423 * of an autoboxed primitive, such as the length of a string.
424 **/
425 while (intermediateValue != null && index < names.length) {
426 if (index === names.length - 1)
427 lookupHit = (
428 hasProperty(intermediateValue, names[index])
429 || primitiveHasOwnProperty(intermediateValue, names[index])
430 );
431
432 intermediateValue = intermediateValue[names[index++]];
433 }
434 } else {
435 intermediateValue = context.view[name];
436
437 /**
438 * Only checking against `hasProperty`, which always returns `false` if
439 * `context.view` is not an object. Deliberately omitting the check
440 * against `primitiveHasOwnProperty` if dot notation is not used.
441 *
442 * Consider this example:
443 * ```
444 * Mustache.render("The length of a football field is {{#length}}{{length}}{{/length}}.", {length: "100 yards"})
445 * ```
446 *
447 * If we were to check also against `primitiveHasOwnProperty`, as we do
448 * in the dot notation case, then render call would return:
449 *
450 * "The length of a football field is 9."
451 *
452 * rather than the expected:
453 *
454 * "The length of a football field is 100 yards."
455 **/
456 lookupHit = hasProperty(context.view, name);
457 }
458
459 if (lookupHit) {
460 value = intermediateValue;
461 break;
462 }
463
464 context = context.parent;
465 }
466
467 cache[name] = value;
468 }
469
470 if (isFunction(value))
471 value = value.call(this.view);
472
473 return value;
474};
475
476/**
477 * A Writer knows how to take a stream of tokens and render them to a
478 * string, given a context. It also maintains a cache of templates to
479 * avoid the need to parse the same template twice.
480 */
481function Writer () {
482 this.templateCache = {
483 _cache: {},
484 set: function set (key, value) {
485 this._cache[key] = value;
486 },
487 get: function get (key) {
488 return this._cache[key];
489 },
490 clear: function clear () {
491 this._cache = {};
492 }
493 };
494}
495
496/**
497 * Clears all cached templates in this writer.
498 */
499Writer.prototype.clearCache = function clearCache () {
500 if (typeof this.templateCache !== 'undefined') {
501 this.templateCache.clear();
502 }
503};
504
505/**
506 * Parses and caches the given `template` according to the given `tags` or
507 * `mustache.tags` if `tags` is omitted, and returns the array of tokens
508 * that is generated from the parse.
509 */
510Writer.prototype.parse = function parse (template, tags) {
511 var cache = this.templateCache;
512 var cacheKey = template + ':' + (tags || mustache.tags).join(':');
513 var isCacheEnabled = typeof cache !== 'undefined';
514 var tokens = isCacheEnabled ? cache.get(cacheKey) : undefined;
515
516 if (tokens == undefined) {
517 tokens = parseTemplate(template, tags);
518 isCacheEnabled && cache.set(cacheKey, tokens);
519 }
520 return tokens;
521};
522
523/**
524 * High-level method that is used to render the given `template` with
525 * the given `view`.
526 *
527 * The optional `partials` argument may be an object that contains the
528 * names and templates of partials that are used in the template. It may
529 * also be a function that is used to load partial templates on the fly
530 * that takes a single argument: the name of the partial.
531 *
532 * If the optional `config` argument is given here, then it should be an
533 * object with a `tags` attribute or an `escape` attribute or both.
534 * If an array is passed, then it will be interpreted the same way as
535 * a `tags` attribute on a `config` object.
536 *
537 * The `tags` attribute of a `config` object must be an array with two
538 * string values: the opening and closing tags used in the template (e.g.
539 * [ "<%", "%>" ]). The default is to mustache.tags.
540 *
541 * The `escape` attribute of a `config` object must be a function which
542 * accepts a string as input and outputs a safely escaped string.
543 * If an `escape` function is not provided, then an HTML-safe string
544 * escaping function is used as the default.
545 */
546Writer.prototype.render = function render (template, view, partials, config) {
547 var tags = this.getConfigTags(config);
548 var tokens = this.parse(template, tags);
549 var context = (view instanceof Context) ? view : new Context(view, undefined);
550 return this.renderTokens(tokens, context, partials, template, config);
551};
552
553/**
554 * Low-level method that renders the given array of `tokens` using
555 * the given `context` and `partials`.
556 *
557 * Note: The `originalTemplate` is only ever used to extract the portion
558 * of the original template that was contained in a higher-order section.
559 * If the template doesn't use higher-order sections, this argument may
560 * be omitted.
561 */
562Writer.prototype.renderTokens = function renderTokens (tokens, context, partials, originalTemplate, config) {
563 var buffer = '';
564
565 var token, symbol, value;
566 for (var i = 0, numTokens = tokens.length; i < numTokens; ++i) {
567 value = undefined;
568 token = tokens[i];
569 symbol = token[0];
570
571 if (symbol === '#') value = this.renderSection(token, context, partials, originalTemplate, config);
572 else if (symbol === '^') value = this.renderInverted(token, context, partials, originalTemplate, config);
573 else if (symbol === '>') value = this.renderPartial(token, context, partials, config);
574 else if (symbol === '&') value = this.unescapedValue(token, context);
575 else if (symbol === 'name') value = this.escapedValue(token, context, config);
576 else if (symbol === 'text') value = this.rawValue(token);
577
578 if (value !== undefined)
579 buffer += value;
580 }
581
582 return buffer;
583};
584
585Writer.prototype.renderSection = function renderSection (token, context, partials, originalTemplate, config) {
586 var self = this;
587 var buffer = '';
588 var value = context.lookup(token[1]);
589
590 // This function is used to render an arbitrary template
591 // in the current context by higher-order sections.
592 function subRender (template) {
593 return self.render(template, context, partials, config);
594 }
595
596 if (!value) return;
597
598 if (isArray(value)) {
599 for (var j = 0, valueLength = value.length; j < valueLength; ++j) {
600 buffer += this.renderTokens(token[4], context.push(value[j]), partials, originalTemplate, config);
601 }
602 } else if (typeof value === 'object' || typeof value === 'string' || typeof value === 'number') {
603 buffer += this.renderTokens(token[4], context.push(value), partials, originalTemplate, config);
604 } else if (isFunction(value)) {
605 if (typeof originalTemplate !== 'string')
606 throw new Error('Cannot use higher-order sections without the original template');
607
608 // Extract the portion of the original template that the section contains.
609 value = value.call(context.view, originalTemplate.slice(token[3], token[5]), subRender);
610
611 if (value != null)
612 buffer += value;
613 } else {
614 buffer += this.renderTokens(token[4], context, partials, originalTemplate, config);
615 }
616 return buffer;
617};
618
619Writer.prototype.renderInverted = function renderInverted (token, context, partials, originalTemplate, config) {
620 var value = context.lookup(token[1]);
621
622 // Use JavaScript's definition of falsy. Include empty arrays.
623 // See https://github.com/janl/mustache.js/issues/186
624 if (!value || (isArray(value) && value.length === 0))
625 return this.renderTokens(token[4], context, partials, originalTemplate, config);
626};
627
628Writer.prototype.indentPartial = function indentPartial (partial, indentation, lineHasNonSpace) {
629 var filteredIndentation = indentation.replace(/[^ \t]/g, '');
630 var partialByNl = partial.split('\n');
631 for (var i = 0; i < partialByNl.length; i++) {
632 if (partialByNl[i].length && (i > 0 || !lineHasNonSpace)) {
633 partialByNl[i] = filteredIndentation + partialByNl[i];
634 }
635 }
636 return partialByNl.join('\n');
637};
638
639Writer.prototype.renderPartial = function renderPartial (token, context, partials, config) {
640 if (!partials) return;
641 var tags = this.getConfigTags(config);
642
643 var value = isFunction(partials) ? partials(token[1]) : partials[token[1]];
644 if (value != null) {
645 var lineHasNonSpace = token[6];
646 var tagIndex = token[5];
647 var indentation = token[4];
648 var indentedValue = value;
649 if (tagIndex == 0 && indentation) {
650 indentedValue = this.indentPartial(value, indentation, lineHasNonSpace);
651 }
652 var tokens = this.parse(indentedValue, tags);
653 return this.renderTokens(tokens, context, partials, indentedValue, config);
654 }
655};
656
657Writer.prototype.unescapedValue = function unescapedValue (token, context) {
658 var value = context.lookup(token[1]);
659 if (value != null)
660 return value;
661};
662
663Writer.prototype.escapedValue = function escapedValue (token, context, config) {
664 var escape = this.getConfigEscape(config) || mustache.escape;
665 var value = context.lookup(token[1]);
666 if (value != null)
667 return (typeof value === 'number' && escape === mustache.escape) ? String(value) : escape(value);
668};
669
670Writer.prototype.rawValue = function rawValue (token) {
671 return token[1];
672};
673
674Writer.prototype.getConfigTags = function getConfigTags (config) {
675 if (isArray(config)) {
676 return config;
677 }
678 else if (config && typeof config === 'object') {
679 return config.tags;
680 }
681 else {
682 return undefined;
683 }
684};
685
686Writer.prototype.getConfigEscape = function getConfigEscape (config) {
687 if (config && typeof config === 'object' && !isArray(config)) {
688 return config.escape;
689 }
690 else {
691 return undefined;
692 }
693};
694
695var mustache = {
696 name: 'mustache.js',
697 version: '4.2.0',
698 tags: [ '{{', '}}' ],
699 clearCache: undefined,
700 escape: undefined,
701 parse: undefined,
702 render: undefined,
703 Scanner: undefined,
704 Context: undefined,
705 Writer: undefined,
706 /**
707 * Allows a user to override the default caching strategy, by providing an
708 * object with set, get and clear methods. This can also be used to disable
709 * the cache by setting it to the literal `undefined`.
710 */
711 set templateCache (cache) {
712 defaultWriter.templateCache = cache;
713 },
714 /**
715 * Gets the default or overridden caching object from the default writer.
716 */
717 get templateCache () {
718 return defaultWriter.templateCache;
719 }
720};
721
722// All high-level mustache.* functions use this writer.
723var defaultWriter = new Writer();
724
725/**
726 * Clears all cached templates in the default writer.
727 */
728mustache.clearCache = function clearCache () {
729 return defaultWriter.clearCache();
730};
731
732/**
733 * Parses and caches the given template in the default writer and returns the
734 * array of tokens it contains. Doing this ahead of time avoids the need to
735 * parse templates on the fly as they are rendered.
736 */
737mustache.parse = function parse (template, tags) {
738 return defaultWriter.parse(template, tags);
739};
740
741/**
742 * Renders the `template` with the given `view`, `partials`, and `config`
743 * using the default writer.
744 */
745mustache.render = function render (template, view, partials, config) {
746 if (typeof template !== 'string') {
747 throw new TypeError('Invalid template! Template should be a "string" ' +
748 'but "' + typeStr(template) + '" was given as the first ' +
749 'argument for mustache#render(template, view, partials)');
750 }
751
752 return defaultWriter.render(template, view, partials, config);
753};
754
755// Export the escaping function so that the user may override it.
756// See https://github.com/janl/mustache.js/issues/244
757mustache.escape = escapeHtml;
758
759// Export these mainly for testing, but also for advanced usage.
760mustache.Scanner = Scanner;
761mustache.Context = Context;
762mustache.Writer = Writer;
763
764export default mustache;
765types/index.d.ts 67 lines1// Names carry the Convey prefix: inside `declare module 'claude-code'` a bare
2// name resolves to Claude Code's own types first (it has a `Selection`, for one).
3
4/**
5 * A local program whose stdout lines become the candidates. Run without a
6 * shell; `program` and each of `args` are Mustache templates over `inputs`.
7 */
8export type ConveyCommand = { program: string; args?: string[] }
9
10/** One entry of `inputs` in a template YAML. `allow_empty` defaults to true. */
11export type ConveyInput =
12 | {
13 type: 'select'
14 allow_empty?: boolean
15 /** Shown under the input's title to explain it; never put in the prompt. */
16 description?: string
17 /** Earlier select inputs whose values the command's templates use. */
18 depends_on?: string[]
19 /**
20 * Fixed `values`, with an optional `default` among them that the form starts
21 * with, or a `command` whose output lists them (which takes no default).
22 */
23 candidates: { values: string[]; default?: string } | { command: ConveyCommand }
24 }
25 | {
26 type: 'textarea'
27 allow_empty?: boolean
28 /** Shown under the input's title to explain it; never put in the prompt. */
29 description?: string
30 /** Dim hint text shown while the field is empty. Never a value; it does not reach the prompt. */
31 placeholder?: string
32 }
33
34/** A template YAML file, in convey's format. */
35export type ConveyTemplate = {
36 name: string
37 /** Input definitions by name, in the order the file lists them. */
38 inputs: Record<string, ConveyInput>
39 output: { template: string }
40}
41
42/** A file that parsed as a template. */
43export type ConveyEntry = { path: string; template: ConveyTemplate }
44
45/** The candidates a command-backed select has loaded so far. */
46export type ConveyCandidates =
47 | { status: 'loading' }
48 | { status: 'ready'; values: string[] }
49 | { status: 'error'; error: string; isExpanded?: boolean }
50
51declare module 'claude-code' {
52 interface PluginState {
53 convey: {
54 /** The templates found by the last `/convey`. */
55 templates: ConveyEntry[]
56 /** The path of the template the form shows. */
57 selected: string | null
58 /** Values entered in the form, by input name. */
59 values: Record<string, string>
60 /** Candidates of command-backed selects, by input name. */
61 candidates: Record<string, ConveyCandidates>
62 /** How many times Enter was pressed in each text field, by input name. */
63 submits: Record<string, number>
64 }
65 }
66}
67