From a4a98c025a961c403e595c28e8a26d8ba3abe928 Mon Sep 17 00:00:00 2001 From: Christian Bager Bach Houmann Date: Mon, 22 Dec 2025 23:06:22 +0100 Subject: [PATCH 1/5] feat: add per-token multiline VALUE inputs --- docs/docs/Advanced/onePageInputs.md | 1 + docs/docs/FormatSyntax.md | 10 ++ .../Misc/Issue339-Multiline-Input-Context.md | 139 ++++++++++++++++++ docs/docs/Misc/ReleaseNotes.md | 6 + src/constants.ts | 4 +- src/formatters/completeFormatter.ts | 34 +++-- src/formatters/formatter.ts | 60 ++++++-- src/gui/InputPrompt.test.ts | 39 +++++ src/gui/InputPrompt.ts | 6 +- src/preflight/OnePageInputModal.ts | 15 +- src/preflight/RequirementCollector.test.ts | 40 ++++- src/preflight/RequirementCollector.ts | 17 ++- src/utils/valueSyntax.test.ts | 59 +++++++- src/utils/valueSyntax.ts | 83 ++++++++++- 14 files changed, 476 insertions(+), 37 deletions(-) create mode 100644 docs/docs/Misc/Issue339-Multiline-Input-Context.md create mode 100644 docs/docs/Misc/ReleaseNotes.md create mode 100644 src/gui/InputPrompt.test.ts diff --git a/docs/docs/Advanced/onePageInputs.md b/docs/docs/Advanced/onePageInputs.md index 9c0d3f52..27bfb76c 100644 --- a/docs/docs/Advanced/onePageInputs.md +++ b/docs/docs/Advanced/onePageInputs.md @@ -18,6 +18,7 @@ This feature is currently in Beta. - Format variables in filenames, templates, and capture content: - `{{VALUE}}`, `{{VALUE:name}}`, `{{VDATE:name, YYYY-MM-DD}}`, `{{FIELD:name|...}}` - Nested `{{TEMPLATE:path}}` are scanned recursively. +- `{{VALUE|type:multiline}}` and `{{VALUE:name|type:multiline}}` render as textareas in the one-page modal. - Capture target file when capturing to a folder or tag. - Script-declared inputs (from user scripts inside macros), if provided. diff --git a/docs/docs/FormatSyntax.md b/docs/docs/FormatSyntax.md index 2f77b4dd..f594b53f 100644 --- a/docs/docs/FormatSyntax.md +++ b/docs/docs/FormatSyntax.md @@ -13,6 +13,7 @@ title: Format syntax | `{{VALUE:\|label:}}` | Adds helper text to the prompt for a single-value input. The helper appears below the header and is useful for reminders or instructions. For multi-value lists, use the same syntax to label the suggester (e.g., `{{VALUE:Red,Green,Blue\|label:Pick a color}}`). | | `{{VALUE:\|}}` | Same as above, but with a default value. For single-value prompts (e.g., `{{VALUE:name\|Anonymous}}`), the default is pre-populated in the input field - press Enter to accept or clear/edit it. For multi-value suggesters without `\|custom`, you must select one of the provided options (no default applies). If you combine options like `\|label:...`, use `\|default:` instead of the shorthand (mixing option keys with a bare default is not supported). | | `{{VALUE:\|default:}}` | Option-form default value, required when combining with other options like `\|label:`. Example: `{{VALUE:title\|label:Snake case\|default:My_Title}}`. | +| `{{VALUE\|type:multiline}}` / `{{VALUE:\|type:multiline}}` | Forces a multi-line input prompt/textarea for that VALUE token. Only supported for single-value prompts (no comma options / `\|custom`). Overrides the global "Use Multi-line Input Prompt" setting. If `\|type:` is present, shorthand defaults like `\|Some value` are ignored; use `\|default:` instead. | | `{{VALUE:\|custom}}` | Allows you to type custom values in addition to selecting from the provided options. Example: `{{VALUE:Red,Green,Blue\|custom}}` will suggest Red, Green, and Blue, but also allows you to type any other value like "Purple". This is useful when you have common options but want flexibility for edge cases. **Note:** You cannot combine `\|custom` with a shorthand default value - use `\|default:` if you need both. | | `{{LINKCURRENT}}` | A link to the file from which the template or capture was triggered (`[[link]]` format). When the append-link setting is set to **Enabled (skip if no active file)**, this token resolves to an empty string instead of throwing an error if no note is focused. | | `{{FILENAMECURRENT}}` | The basename (without extension) of the file from which the template or capture was triggered. Honors the same **required/optional** behavior as `{{LINKCURRENT}}` - when optional and no active file exists, resolves to an empty string. | @@ -26,3 +27,12 @@ title: Format syntax | `{{CLIPBOARD}}` | The current clipboard content. Will be empty if clipboard access fails due to permissions or security restrictions. | | `{{RANDOM:}}` | Generates a random alphanumeric string of the specified length (1-100). Useful for creating unique identifiers, block references, or temporary codes. Example: `{{RANDOM:6}}` generates something like `3YusT5`. | | `{{TITLE}}` | The final rendered filename (without extension) of the note being created or captured to. | + +### Mixed-mode example + +Use single-line for a title and multi-line for a body: + +```markdown +- {{VALUE:Title|label:Title}} +{{VALUE:Body|type:multiline|label:Body}} +``` diff --git a/docs/docs/Misc/Issue339-Multiline-Input-Context.md b/docs/docs/Misc/Issue339-Multiline-Input-Context.md new file mode 100644 index 00000000..672580db --- /dev/null +++ b/docs/docs/Misc/Issue339-Multiline-Input-Context.md @@ -0,0 +1,139 @@ +--- +title: Issue 339 - Multiline Input Context +--- + +# Context: per-capture or per-token multiline input + +This note summarizes what exists today around VALUE prompts and multiline input, +plus potential directions for Issue #339 ("Toggle multiline input per capture +command"), for PM review. + +## What exists today + +### Global input prompt mode +- There is a global setting: **"Use Multi-line Input Prompt"**. +- It switches all VALUE input prompts between: + - **Single-line** (`GenericInputPrompt`) and + - **Multi-line** (`GenericWideInputPrompt`). + +Behavior differences (code): +- Single-line: Enter submits. +- Multi-line: Ctrl/Cmd+Enter submits, and input backslashes are escaped on submit. +- Drafts are persisted separately for single vs multi (`InputPromptDraftStore` uses + `kind: "single" | "multi"`). + +Relevant code: +- `src/settings.ts` (`inputPrompt` setting) +- `src/quickAddSettingsTab.ts` (toggle UI) +- `src/gui/InputPrompt.ts` (chooses single vs multi) +- `src/gui/GenericInputPrompt/GenericInputPrompt.ts` +- `src/gui/GenericWideInputPrompt/GenericWideInputPrompt.ts` + +### VALUE syntax and options (docs) +Format syntax supports: +- `{{VALUE}}` (and `{{NAME}}`) for a single prompt value. +- `{{VALUE:variable}}` for named prompts. +- Options like: + - `|label:Helper text` + - `|default:Some value` (or shorthand `|Some value` when no other options) + - `|custom` for multi-option suggesters. + +Docs: `docs/docs/FormatSyntax.md`. + +Examples already in docs: +- Capture format: `- {{DATE:HH:mm}} {{VALUE}}` +- Named values: `{{VALUE:title}}`, `{{VALUE:description}}` +- Labeled prompt: `{{VALUE:title|label:Snake case}}` +- Default: `{{VALUE:title|default:My_Title}}` +- Suggester: `{{VALUE:Red,Green,Blue|custom}}` + +### Capture choice has a per-choice *selection-as-value* override +Capture choices already have a per-choice override for whether editor selection +prefills `{{VALUE}}` (follow global, enabled, disabled). + +Docs: `docs/docs/Choices/CaptureChoice.md`. + +This is a good precedent for a per-capture override UI. + +### One-page input supports textarea +The one-page modal already supports field type `textarea`: +- Used in scripts via `quickAddApi.requestInputs(...)`. +- Internally, the preflight scanner chooses `text` vs `textarea` for `{{VALUE}}` + based on the *global* input prompt setting. + +Docs: `docs/docs/Advanced/onePageInputs.md` and `docs/docs/QuickAddAPI.md`. + +### QuickAdd API has both single and wide prompts +Scripts can call either: +- `quickAddApi.inputPrompt(...)` (single-line) +- `quickAddApi.wideInputPrompt(...)` (multi-line) + +Docs: `docs/docs/QuickAddAPI.md`, `docs/docs/UserScripts.md`. + +## What Issue #339 asks for +Problem statement (summary): +- Multiline input is global today. +- Users want **per-capture** (or even per-`{{VALUE}}`) control so that + logging stays single-line, while tasks or notes can be multi-line. + +## Options worth considering + +### Option A: Per-capture setting (UI toggle) +- Add a capture-level override: follow global / single-line / multi-line. +- Mirrors existing "selection-as-value" per-capture override UX. +- Simple mental model for users who want a fixed behavior per Capture choice. + +Pros: +- Easy to explain in UI. +- Minimal new syntax. +- Aligns with existing capture-specific overrides. + +Cons: +- No fine-grained control within a capture format that uses multiple VALUEs. + +### Option B: Per-token modifier (Format Syntax) +Introduce something like: +- `{{VALUE|multi}}` +- `{{VALUE:Description|multi}}` + +Potential variants (to align with existing `|key:value` options): +- `{{VALUE|type:textarea}}` +- `{{VALUE:description|input:multi}}` +- `{{VALUE:description|multiline:true}}` + +Pros: +- Fine-grained control per VALUE token. +- Scales to named variables and templates. + +Cons: +- Adds new syntax rules and parsing. +- Need to define how it interacts with: + - `|label:` and `|default:` + - `|custom` (only applies to option lists) + - unnamed `{{VALUE}}` +- Requires changes in both runtime prompts *and* one-page preflight. + +### Option C: Combine A + B +- Offer per-capture override as the default behavior. +- Allow per-token override for cases that need mixed input types. + +## Notes on implementation impact (for planning) +- `parseValueToken` currently recognizes only `label`, `default`, `custom` as + options. Any new `|multi` or `|type:...` would need to extend parsing. +- `InputPrompt` currently chooses prompt type solely from global settings. A + per-token or per-capture override would need to reach this selection. +- One-page input preflight uses the global setting to decide `text` vs `textarea` + for `{{VALUE}}` requirements; token-level control would need to pass through + to `RequirementCollector`. +- Multi-line prompt behavior differs from single-line (submit keys, backslash + escaping, draft persistence kind), so it is not only a UI width change. + +## Open questions for PM +1. Should multiline control be per-capture only, per-token, or both? +2. If per-token, what option syntax should we standardize on? (short `|multi` + vs explicit `|type:textarea` / `|input:multi`) +3. If per-token, should the modifier be allowed only for single-value prompts, + or also for option lists? +4. How should per-token settings interact with one-page input? (Should they map + to `textarea` in the one-page modal?) + diff --git a/docs/docs/Misc/ReleaseNotes.md b/docs/docs/Misc/ReleaseNotes.md new file mode 100644 index 00000000..df470b0f --- /dev/null +++ b/docs/docs/Misc/ReleaseNotes.md @@ -0,0 +1,6 @@ +--- +title: Release Notes (Draft) +--- + +## Unreleased +- Format syntax: `{{VALUE|type:multiline}}` forces a multi-line prompt/textarea for that VALUE token. Global multi-line setting remains the default when not specified. diff --git a/src/constants.ts b/src/constants.ts index 6b1b4865..f3fc256d 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -77,7 +77,9 @@ export const DATE_REGEX_FORMATTED = new RegExp( ); export const TIME_REGEX = new RegExp(/{{TIME}}/i); export const TIME_REGEX_FORMATTED = new RegExp(/{{TIME:([^}\n\r+]*)}}/i); -export const NAME_VALUE_REGEX = new RegExp(/{{NAME}}|{{VALUE}}/i); +export const NAME_VALUE_REGEX = new RegExp( + /{{(?:NAME|VALUE)(?!:)(?:\|[^\n\r}]*)?}}/i, +); export const VARIABLE_REGEX = new RegExp(/{{VALUE:([^\n\r}]*)}}/i); export const FIELD_VAR_REGEX = new RegExp(/{{FIELD:([^\n\r}]*)}}/i); export const FIELD_VAR_REGEX_WITH_FILTERS = new RegExp( diff --git a/src/formatters/completeFormatter.ts b/src/formatters/completeFormatter.ts index 13f918fa..5217db62 100644 --- a/src/formatters/completeFormatter.ts +++ b/src/formatters/completeFormatter.ts @@ -174,20 +174,28 @@ export class CompleteFormatter extends Formatter { } try { const linkSourcePath = this.getLinkSourcePath(); + const promptFactory = new InputPrompt().factory( + this.valuePromptContext?.inputType, + ); + const defaultValue = this.valuePromptContext?.defaultValue; + const description = this.valuePromptContext?.description; if (linkSourcePath) { - this.value = await new InputPrompt() - .factory() - .PromptWithContext( - this.app, - this.valueHeader ?? `Enter value`, - undefined, - undefined, - linkSourcePath - ); + this.value = await promptFactory.PromptWithContext( + this.app, + this.valueHeader ?? `Enter value`, + undefined, + defaultValue, + linkSourcePath, + description, + ); } else { - this.value = await new InputPrompt() - .factory() - .Prompt(this.app, this.valueHeader ?? `Enter value`); + this.value = await promptFactory.Prompt( + this.app, + this.valueHeader ?? `Enter value`, + undefined, + defaultValue, + description, + ); } } catch (error) { if (isCancellationError(error)) { @@ -217,7 +225,7 @@ export class CompleteFormatter extends Formatter { } // Use default prompt for other variables - return await new InputPrompt().factory().Prompt( + return await new InputPrompt().factory(context?.inputType).Prompt( this.app, header ?? context?.label ?? "Enter value", context?.placeholder ?? diff --git a/src/formatters/formatter.ts b/src/formatters/formatter.ts index 013683c4..414e9c83 100644 --- a/src/formatters/formatter.ts +++ b/src/formatters/formatter.ts @@ -27,8 +27,9 @@ import { TemplatePropertyCollector } from "../utils/TemplatePropertyCollector"; import { settingsStore } from "../settingsStore"; import { normalizeDateInput } from "../utils/dateAliases"; import { + parseAnonymousValueOptions, parseValueToken, - resolveExistingVariableKey, + type ValueInputType, } from "../utils/valueSyntax"; import { parseMacroToken } from "../utils/macroSyntax"; @@ -42,6 +43,7 @@ export interface PromptContext { description?: string; placeholder?: string; variableKey?: string; + inputType?: ValueInputType; } export abstract class Formatter { @@ -50,6 +52,7 @@ export abstract class Formatter { protected dateParser: IDateParser | undefined; private linkToCurrentFileBehavior: LinkToCurrentFileBehavior = "required"; private static readonly FIELD_VARIABLE_PREFIX = "FIELD:"; + protected valuePromptContext?: PromptContext; // Tracks variables collected for YAML property post-processing private readonly propertyCollector: TemplatePropertyCollector; @@ -60,11 +63,12 @@ export abstract class Formatter { protected abstract format(input: string): Promise; - /** Returns true when a variable is present AND its value is not undefined. - * Null and empty string are considered intentional values. */ + /** Returns true when a variable is present AND its value is neither undefined nor null. + * An empty string is considered a valid, intentional value. */ protected hasConcreteVariable(name: string): boolean { if (!this.variables.has(name)) return false; - return this.variables.get(name) !== undefined; + const v = this.variables.get(name); + return v !== undefined && v !== null; } public setTitle(title: string): void { @@ -149,10 +153,11 @@ export abstract class Formatter { // Fast path: nothing to do. if (!NAME_VALUE_REGEX.test(output)) return output; + this.valuePromptContext = this.getValuePromptContext(output); + // Preserve programmatic VALUE injection via reserved variable name `value`. if (this.hasConcreteVariable("value")) { - const existingValue = this.variables.get("value"); - this.value = existingValue === null ? "" : String(existingValue); + this.value = String(this.variables.get("value")); } // Prompt only once per formatter run (empty string is a valid value). @@ -168,6 +173,35 @@ export abstract class Formatter { return output; } + private getValuePromptContext(input: string): PromptContext | undefined { + const regex = new RegExp(NAME_VALUE_REGEX.source, "gi"); + let match: RegExpExecArray | null; + let context: PromptContext | undefined; + + while ((match = regex.exec(input)) !== null) { + const token = match[0]; + const inner = token.slice(2, -2); + const optionsIndex = inner.indexOf("|"); + if (optionsIndex === -1) continue; + const rawOptions = inner.slice(optionsIndex); + + const parsed = parseAnonymousValueOptions(rawOptions); + if (!context) context = {}; + + if (!context.description && parsed.label) { + context.description = parsed.label; + } + if (!context.defaultValue && parsed.defaultValue) { + context.defaultValue = parsed.defaultValue; + } + if (parsed.inputType === "multiline") { + context.inputType = "multiline"; + } + } + + return context; + } + protected async replaceSelectedInString(input: string): Promise { let output: string = input; @@ -290,13 +324,8 @@ export abstract class Formatter { hasOptions, } = parsed; - const resolvedKey = resolveExistingVariableKey( - this.variables, - variableKey, - ); - // Ensure variable is set (prompt if needed) - if (!resolvedKey) { + if (!this.hasConcreteVariable(variableKey)) { let variableValue = ""; const helperText = !hasOptions && label ? label : undefined; @@ -308,6 +337,7 @@ export abstract class Formatter { variableValue = await this.promptForVariable(variableName, { defaultValue, description: helperText, + inputType: parsed.inputType, variableKey, }); } else { @@ -326,10 +356,8 @@ export abstract class Formatter { this.variables.set(variableKey, variableValue); } - const effectiveKey = resolvedKey ?? variableKey; - // Get the raw value from variables - const rawValue = this.variables.get(effectiveKey); + const rawValue = this.variables.get(variableKey); // Offer this variable to the property collector for YAML post-processing this.propertyCollector.maybeCollect({ @@ -342,7 +370,7 @@ export abstract class Formatter { }); // Always use string replacement initially - const replacement = this.getVariableValue(effectiveKey); + const replacement = this.getVariableValue(variableKey); // Replace in output and adjust regex position output = output.slice(0, match.index) + replacement + output.slice(match.index + match[0].length); diff --git a/src/gui/InputPrompt.test.ts b/src/gui/InputPrompt.test.ts new file mode 100644 index 00000000..2f31bc74 --- /dev/null +++ b/src/gui/InputPrompt.test.ts @@ -0,0 +1,39 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +vi.mock("../main", () => ({ + __esModule: true, + default: class QuickAddMock { + static instance = { settings: { inputPrompt: "single-line" } }; + }, +})); + +import InputPrompt from "./InputPrompt"; +import GenericInputPrompt from "./GenericInputPrompt/GenericInputPrompt"; +import GenericWideInputPrompt from "./GenericWideInputPrompt/GenericWideInputPrompt"; +import QuickAdd from "../main"; + +describe("InputPrompt factory", () => { + beforeEach(() => { + QuickAdd.instance = { + settings: { inputPrompt: "single-line" }, + } as any; + }); + + it("prefers multiline override over global single-line", () => { + const prompt = new InputPrompt(); + expect(prompt.factory("multiline")).toBe(GenericWideInputPrompt); + }); + + it("uses global multiline when no override provided", () => { + QuickAdd.instance = { + settings: { inputPrompt: "multi-line" }, + } as any; + const prompt = new InputPrompt(); + expect(prompt.factory()).toBe(GenericWideInputPrompt); + }); + + it("uses single-line when no override and global single-line", () => { + const prompt = new InputPrompt(); + expect(prompt.factory()).toBe(GenericInputPrompt); + }); +}); diff --git a/src/gui/InputPrompt.ts b/src/gui/InputPrompt.ts index 88e0f19a..cb791750 100644 --- a/src/gui/InputPrompt.ts +++ b/src/gui/InputPrompt.ts @@ -1,9 +1,13 @@ import GenericWideInputPrompt from "./GenericWideInputPrompt/GenericWideInputPrompt"; import GenericInputPrompt from "./GenericInputPrompt/GenericInputPrompt"; import QuickAdd from "../main"; +import type { ValueInputType } from "../utils/valueSyntax"; export default class InputPrompt { - public factory() { + public factory(inputType?: ValueInputType) { + if (inputType === "multiline") { + return GenericWideInputPrompt; + } if (QuickAdd.instance.settings.inputPrompt === "multi-line") { return GenericWideInputPrompt; } else { diff --git a/src/preflight/OnePageInputModal.ts b/src/preflight/OnePageInputModal.ts index b0cee937..5e774efb 100644 --- a/src/preflight/OnePageInputModal.ts +++ b/src/preflight/OnePageInputModal.ts @@ -404,11 +404,24 @@ export class OnePageInputModal extends Modal { private submit() { const out: Record = {}; - this.result.forEach((v, k) => (out[k] = v)); + const requirementsById = new Map( + this.requirements.map((req) => [req.id, req]), + ); + this.result.forEach((v, k) => { + const requirement = requirementsById.get(k); + out[k] = + requirement?.type === "textarea" + ? this.escapeBackslashes(v) + : v; + }); this.close(); this.resolvePromise(out); } + private escapeBackslashes(input: string): string { + return input.replace(/\\/g, "\\\\"); + } + private cancel() { this.close(); this.rejectPromise("cancelled"); diff --git a/src/preflight/RequirementCollector.test.ts b/src/preflight/RequirementCollector.test.ts index d1a1e801..f5979aed 100644 --- a/src/preflight/RequirementCollector.test.ts +++ b/src/preflight/RequirementCollector.test.ts @@ -7,7 +7,14 @@ const makeApp = () => ({ workspace: { getActiveFile: () => null }, vault: { getAbstractFileByPath: () => null, cachedRead: async () => "" }, } as any); -const makePlugin = () => ({ } as any); +const makePlugin = (overrides: Record = {}) => + ({ + settings: { + inputPrompt: "single-line", + globalVariables: {}, + ...overrides, + }, + } as any); describe("RequirementCollector", () => { it("collects VALUE with default and options", async () => { @@ -66,4 +73,35 @@ describe("RequirementCollector", () => { expect(rc.templatesToScan.size === 0 || rc.templatesToScan.has("Templates/Note")).toBe(true); }); + + it("uses textarea for VALUE tokens with type:multiline", async () => { + const app = makeApp(); + const plugin = makePlugin({ inputPrompt: "single-line" }); + const rc = new RequirementCollector(app, plugin); + await rc.scanString("{{VALUE:Body|type:multiline}}" ); + + const requirement = rc.requirements.get("Body"); + expect(requirement?.type).toBe("textarea"); + }); + + it("uses textarea for unnamed VALUE with type:multiline", async () => { + const app = makeApp(); + const plugin = makePlugin({ inputPrompt: "single-line" }); + const rc = new RequirementCollector(app, plugin); + await rc.scanString("{{VALUE|type:multiline|label:Notes}}" ); + + const requirement = rc.requirements.get("value"); + expect(requirement?.type).toBe("textarea"); + expect(requirement?.description).toBe("Notes"); + }); + + it("respects global multiline setting for named VALUE tokens", async () => { + const app = makeApp(); + const plugin = makePlugin({ inputPrompt: "multi-line" }); + const rc = new RequirementCollector(app, plugin); + await rc.scanString("{{VALUE:title}}" ); + + const requirement = rc.requirements.get("title"); + expect(requirement?.type).toBe("textarea"); + }); }); diff --git a/src/preflight/RequirementCollector.ts b/src/preflight/RequirementCollector.ts index 585c47cf..5d002fcb 100644 --- a/src/preflight/RequirementCollector.ts +++ b/src/preflight/RequirementCollector.ts @@ -156,6 +156,11 @@ export class RequirementCollector extends Formatter { const requirementId = variableKey; if (!this.requirements.has(requirementId)) { + const baseInputType = + parsed.inputType === "multiline" || + this.plugin.settings.inputPrompt === "multi-line" + ? "textarea" + : "text"; const req: FieldRequirement = { id: requirementId, label: displayLabel, @@ -163,7 +168,7 @@ export class RequirementCollector extends Formatter { ? allowCustomInput ? "suggester" : "dropdown" - : "text", + : baseInputType, description, }; if (hasOptions) { @@ -194,9 +199,12 @@ export class RequirementCollector extends Formatter { id: key, label: header || "Enter value", type: + this.valuePromptContext?.inputType === "multiline" || this.plugin.settings.inputPrompt === "multi-line" ? "textarea" : "text", + description: this.valuePromptContext?.description, + defaultValue: this.valuePromptContext?.defaultValue, source: "collected", }); } @@ -230,10 +238,15 @@ export class RequirementCollector extends Formatter { if (!this.requirements.has(key)) { // Detect simple comma-separated option lists const hasOptions = variableName.includes(","); + const baseInputType = + context?.inputType === "multiline" || + this.plugin.settings.inputPrompt === "multi-line" + ? "textarea" + : "text"; const req: FieldRequirement = { id: key, label: variableName, - type: hasOptions ? "dropdown" : "text", + type: hasOptions ? "dropdown" : baseInputType, description: context?.description, source: "collected", }; diff --git a/src/utils/valueSyntax.test.ts b/src/utils/valueSyntax.test.ts index 6e4ec512..831659ec 100644 --- a/src/utils/valueSyntax.test.ts +++ b/src/utils/valueSyntax.test.ts @@ -1,11 +1,16 @@ -import { describe, expect, it } from "vitest"; +import { describe, expect, it, vi, afterEach } from "vitest"; import { buildValueVariableKey, + parseAnonymousValueOptions, parseValueToken, resolveExistingVariableKey, } from "./valueSyntax"; describe("parseValueToken", () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + it("ignores empty label values", () => { const parsed = parseValueToken("title|label:"); expect(parsed).not.toBeNull(); @@ -42,6 +47,58 @@ describe("parseValueToken", () => { expect(parsed?.allowCustomInput).toBe(true); expect(parsed?.defaultValue).toBe("High"); }); + + it("parses multiline type with label and default", () => { + const parsed = parseValueToken( + "Body|type:multiline|label:Notes|default:Hello", + ); + expect(parsed?.variableName).toBe("Body"); + expect(parsed?.inputType).toBe("multiline"); + expect(parsed?.label).toBe("Notes"); + expect(parsed?.defaultValue).toBe("Hello"); + }); + + it("ignores shorthand default when type is present", () => { + const parsed = parseValueToken("Body|Hello|type:multiline"); + expect(parsed?.defaultValue).toBe(""); + expect(parsed?.inputType).toBe("multiline"); + }); + + it("warns and ignores unknown type values", () => { + const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); + const parsed = parseValueToken("Body|type:wide"); + expect(parsed?.inputType).toBeUndefined(); + expect(warnSpy).toHaveBeenCalled(); + }); + + it("warns and ignores type for option lists", () => { + const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); + const parsed = parseValueToken("Red,Green|type:multiline"); + expect(parsed?.inputType).toBeUndefined(); + expect(warnSpy).toHaveBeenCalled(); + }); +}); + +describe("parseAnonymousValueOptions", () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + it("parses multiline type for unnamed VALUE tokens", () => { + const parsed = parseAnonymousValueOptions( + "|type:multiline|label:Notes|default:Hello", + ); + expect(parsed.inputType).toBe("multiline"); + expect(parsed.label).toBe("Notes"); + expect(parsed.defaultValue).toBe("Hello"); + }); + + it("warns and ignores unknown type for unnamed VALUE tokens", () => { + const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); + const parsed = parseAnonymousValueOptions("|type:wide"); + expect(parsed.inputType).toBeUndefined(); + expect(warnSpy).toHaveBeenCalled(); + }); }); describe("resolveExistingVariableKey", () => { diff --git a/src/utils/valueSyntax.ts b/src/utils/valueSyntax.ts index 0af124c9..18e94d74 100644 --- a/src/utils/valueSyntax.ts +++ b/src/utils/valueSyntax.ts @@ -1,7 +1,9 @@ // Internal-only delimiter for scoping labeled VALUE lists. Unlikely to appear in user input. export const VALUE_LABEL_KEY_DELIMITER = "\u001F"; -const VALUE_OPTION_KEYS = new Set(["label", "default", "custom"]); +export type ValueInputType = "multiline"; + +const VALUE_OPTION_KEYS = new Set(["label", "default", "custom", "type"]); export type ParsedValueToken = { raw: string; @@ -12,6 +14,7 @@ export type ParsedValueToken = { allowCustomInput: boolean; suggestedValues: string[]; hasOptions: boolean; + inputType?: ValueInputType; }; export function buildValueVariableKey( @@ -74,6 +77,7 @@ type ParsedOptions = { defaultValue: string; allowCustomInput: boolean; usesOptions: boolean; + inputType?: string; }; function parseBoolean(value?: string): boolean { @@ -119,6 +123,7 @@ function parseOptions(optionParts: string[], hasOptions: boolean): ParsedOptions let label: string | undefined; let defaultValue = ""; let allowCustomInput = false; + let inputType: string | undefined; for (const part of optionParts) { const trimmed = part.trim(); @@ -145,6 +150,9 @@ function parseOptions(optionParts: string[], hasOptions: boolean): ParsedOptions case "custom": allowCustomInput = parseBoolean(value); break; + case "type": + if (value) inputType = value; + break; default: break; } @@ -155,9 +163,35 @@ function parseOptions(optionParts: string[], hasOptions: boolean): ParsedOptions defaultValue, allowCustomInput, usesOptions: true, + inputType, }; } +function resolveInputType( + rawType: string | undefined, + { + tokenDisplay, + hasOptions, + allowCustomInput, + }: { tokenDisplay: string; hasOptions: boolean; allowCustomInput: boolean }, +): ValueInputType | undefined { + if (!rawType) return undefined; + const normalized = rawType.trim().toLowerCase(); + if (normalized !== "multiline") { + console.warn( + `QuickAdd: Unsupported VALUE type "${rawType}" in token "${tokenDisplay}". Supported types: multiline.`, + ); + return undefined; + } + if (hasOptions || allowCustomInput) { + console.warn( + `QuickAdd: Ignoring type:multiline for option-list VALUE token "${tokenDisplay}".`, + ); + return undefined; + } + return "multiline"; +} + export function parseValueToken(raw: string): ParsedValueToken | null { if (!raw) return null; @@ -180,6 +214,13 @@ export function parseValueToken(raw: string): ParsedValueToken | null { defaultValue = allowCustomInput ? "" : legacyDefault; } + const tokenDisplay = `{{VALUE:${raw}}}`; + const inputType = resolveInputType(options.inputType, { + tokenDisplay, + hasOptions, + allowCustomInput, + }); + const variableKey = buildValueVariableKey(variablePart, label, hasOptions); return { @@ -191,5 +232,45 @@ export function parseValueToken(raw: string): ParsedValueToken | null { allowCustomInput, suggestedValues, hasOptions, + inputType, + }; +} + +export function parseAnonymousValueOptions( + rawOptions: string, +): { + label?: string; + defaultValue: string; + inputType?: ValueInputType; +} { + const normalized = rawOptions.startsWith("|") + ? rawOptions.slice(1) + : rawOptions; + const parts = normalized + .split("|") + .map((part) => part.trim()) + .filter(Boolean); + + if (parts.length === 0) { + return { defaultValue: "" }; + } + + const options = parseOptions(parts, false); + let { label, defaultValue } = options; + if (!options.usesOptions) { + defaultValue = defaultValue.trim(); + } + + const tokenDisplay = `{{VALUE${rawOptions}}}`; + const inputType = resolveInputType(options.inputType, { + tokenDisplay, + hasOptions: false, + allowCustomInput: options.allowCustomInput, + }); + + return { + label, + defaultValue, + inputType, }; } From 8a9c48138346de886c25d8bfaae4cdf89a98bf34 Mon Sep 17 00:00:00 2001 From: Christian Bager Bach Houmann Date: Mon, 22 Dec 2025 23:40:46 +0100 Subject: [PATCH 2/5] chore: drop misc docs from issue 339 work --- .../Misc/Issue339-Multiline-Input-Context.md | 139 ------------------ docs/docs/Misc/ReleaseNotes.md | 6 - 2 files changed, 145 deletions(-) delete mode 100644 docs/docs/Misc/Issue339-Multiline-Input-Context.md delete mode 100644 docs/docs/Misc/ReleaseNotes.md diff --git a/docs/docs/Misc/Issue339-Multiline-Input-Context.md b/docs/docs/Misc/Issue339-Multiline-Input-Context.md deleted file mode 100644 index 672580db..00000000 --- a/docs/docs/Misc/Issue339-Multiline-Input-Context.md +++ /dev/null @@ -1,139 +0,0 @@ ---- -title: Issue 339 - Multiline Input Context ---- - -# Context: per-capture or per-token multiline input - -This note summarizes what exists today around VALUE prompts and multiline input, -plus potential directions for Issue #339 ("Toggle multiline input per capture -command"), for PM review. - -## What exists today - -### Global input prompt mode -- There is a global setting: **"Use Multi-line Input Prompt"**. -- It switches all VALUE input prompts between: - - **Single-line** (`GenericInputPrompt`) and - - **Multi-line** (`GenericWideInputPrompt`). - -Behavior differences (code): -- Single-line: Enter submits. -- Multi-line: Ctrl/Cmd+Enter submits, and input backslashes are escaped on submit. -- Drafts are persisted separately for single vs multi (`InputPromptDraftStore` uses - `kind: "single" | "multi"`). - -Relevant code: -- `src/settings.ts` (`inputPrompt` setting) -- `src/quickAddSettingsTab.ts` (toggle UI) -- `src/gui/InputPrompt.ts` (chooses single vs multi) -- `src/gui/GenericInputPrompt/GenericInputPrompt.ts` -- `src/gui/GenericWideInputPrompt/GenericWideInputPrompt.ts` - -### VALUE syntax and options (docs) -Format syntax supports: -- `{{VALUE}}` (and `{{NAME}}`) for a single prompt value. -- `{{VALUE:variable}}` for named prompts. -- Options like: - - `|label:Helper text` - - `|default:Some value` (or shorthand `|Some value` when no other options) - - `|custom` for multi-option suggesters. - -Docs: `docs/docs/FormatSyntax.md`. - -Examples already in docs: -- Capture format: `- {{DATE:HH:mm}} {{VALUE}}` -- Named values: `{{VALUE:title}}`, `{{VALUE:description}}` -- Labeled prompt: `{{VALUE:title|label:Snake case}}` -- Default: `{{VALUE:title|default:My_Title}}` -- Suggester: `{{VALUE:Red,Green,Blue|custom}}` - -### Capture choice has a per-choice *selection-as-value* override -Capture choices already have a per-choice override for whether editor selection -prefills `{{VALUE}}` (follow global, enabled, disabled). - -Docs: `docs/docs/Choices/CaptureChoice.md`. - -This is a good precedent for a per-capture override UI. - -### One-page input supports textarea -The one-page modal already supports field type `textarea`: -- Used in scripts via `quickAddApi.requestInputs(...)`. -- Internally, the preflight scanner chooses `text` vs `textarea` for `{{VALUE}}` - based on the *global* input prompt setting. - -Docs: `docs/docs/Advanced/onePageInputs.md` and `docs/docs/QuickAddAPI.md`. - -### QuickAdd API has both single and wide prompts -Scripts can call either: -- `quickAddApi.inputPrompt(...)` (single-line) -- `quickAddApi.wideInputPrompt(...)` (multi-line) - -Docs: `docs/docs/QuickAddAPI.md`, `docs/docs/UserScripts.md`. - -## What Issue #339 asks for -Problem statement (summary): -- Multiline input is global today. -- Users want **per-capture** (or even per-`{{VALUE}}`) control so that - logging stays single-line, while tasks or notes can be multi-line. - -## Options worth considering - -### Option A: Per-capture setting (UI toggle) -- Add a capture-level override: follow global / single-line / multi-line. -- Mirrors existing "selection-as-value" per-capture override UX. -- Simple mental model for users who want a fixed behavior per Capture choice. - -Pros: -- Easy to explain in UI. -- Minimal new syntax. -- Aligns with existing capture-specific overrides. - -Cons: -- No fine-grained control within a capture format that uses multiple VALUEs. - -### Option B: Per-token modifier (Format Syntax) -Introduce something like: -- `{{VALUE|multi}}` -- `{{VALUE:Description|multi}}` - -Potential variants (to align with existing `|key:value` options): -- `{{VALUE|type:textarea}}` -- `{{VALUE:description|input:multi}}` -- `{{VALUE:description|multiline:true}}` - -Pros: -- Fine-grained control per VALUE token. -- Scales to named variables and templates. - -Cons: -- Adds new syntax rules and parsing. -- Need to define how it interacts with: - - `|label:` and `|default:` - - `|custom` (only applies to option lists) - - unnamed `{{VALUE}}` -- Requires changes in both runtime prompts *and* one-page preflight. - -### Option C: Combine A + B -- Offer per-capture override as the default behavior. -- Allow per-token override for cases that need mixed input types. - -## Notes on implementation impact (for planning) -- `parseValueToken` currently recognizes only `label`, `default`, `custom` as - options. Any new `|multi` or `|type:...` would need to extend parsing. -- `InputPrompt` currently chooses prompt type solely from global settings. A - per-token or per-capture override would need to reach this selection. -- One-page input preflight uses the global setting to decide `text` vs `textarea` - for `{{VALUE}}` requirements; token-level control would need to pass through - to `RequirementCollector`. -- Multi-line prompt behavior differs from single-line (submit keys, backslash - escaping, draft persistence kind), so it is not only a UI width change. - -## Open questions for PM -1. Should multiline control be per-capture only, per-token, or both? -2. If per-token, what option syntax should we standardize on? (short `|multi` - vs explicit `|type:textarea` / `|input:multi`) -3. If per-token, should the modifier be allowed only for single-value prompts, - or also for option lists? -4. How should per-token settings interact with one-page input? (Should they map - to `textarea` in the one-page modal?) - diff --git a/docs/docs/Misc/ReleaseNotes.md b/docs/docs/Misc/ReleaseNotes.md deleted file mode 100644 index df470b0f..00000000 --- a/docs/docs/Misc/ReleaseNotes.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: Release Notes (Draft) ---- - -## Unreleased -- Format syntax: `{{VALUE|type:multiline}}` forces a multi-line prompt/textarea for that VALUE token. Global multi-line setting remains the default when not specified. From 138a017e768b3feff6e6593da84b0dc9907b352b Mon Sep 17 00:00:00 2001 From: Christian Bager Bach Houmann Date: Mon, 22 Dec 2025 23:46:54 +0100 Subject: [PATCH 3/5] fix: treat null VALUE variables as concrete --- src/formatters/formatter.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/formatters/formatter.ts b/src/formatters/formatter.ts index 414e9c83..ab2615b1 100644 --- a/src/formatters/formatter.ts +++ b/src/formatters/formatter.ts @@ -63,12 +63,11 @@ export abstract class Formatter { protected abstract format(input: string): Promise; - /** Returns true when a variable is present AND its value is neither undefined nor null. - * An empty string is considered a valid, intentional value. */ + /** Returns true when a variable is present AND its value is not undefined. + * Null and empty string are considered intentional values. */ protected hasConcreteVariable(name: string): boolean { if (!this.variables.has(name)) return false; - const v = this.variables.get(name); - return v !== undefined && v !== null; + return this.variables.get(name) !== undefined; } public setTitle(title: string): void { From 1d30a3b39b7d874704b74464359162fe98fe0faa Mon Sep 17 00:00:00 2001 From: Christian Bager Bach Houmann Date: Mon, 22 Dec 2025 23:57:21 +0100 Subject: [PATCH 4/5] fix: preserve scripted VALUE injections --- src/formatters/formatter.ts | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/formatters/formatter.ts b/src/formatters/formatter.ts index ab2615b1..5f79ee49 100644 --- a/src/formatters/formatter.ts +++ b/src/formatters/formatter.ts @@ -29,6 +29,7 @@ import { normalizeDateInput } from "../utils/dateAliases"; import { parseAnonymousValueOptions, parseValueToken, + resolveExistingVariableKey, type ValueInputType, } from "../utils/valueSyntax"; import { parseMacroToken } from "../utils/macroSyntax"; @@ -156,7 +157,8 @@ export abstract class Formatter { // Preserve programmatic VALUE injection via reserved variable name `value`. if (this.hasConcreteVariable("value")) { - this.value = String(this.variables.get("value")); + const existingValue = this.variables.get("value"); + this.value = existingValue === null ? "" : String(existingValue); } // Prompt only once per formatter run (empty string is a valid value). @@ -323,8 +325,13 @@ export abstract class Formatter { hasOptions, } = parsed; + const resolvedKey = resolveExistingVariableKey( + this.variables, + variableKey, + ); + // Ensure variable is set (prompt if needed) - if (!this.hasConcreteVariable(variableKey)) { + if (!resolvedKey) { let variableValue = ""; const helperText = !hasOptions && label ? label : undefined; @@ -355,8 +362,10 @@ export abstract class Formatter { this.variables.set(variableKey, variableValue); } + const effectiveKey = resolvedKey ?? variableKey; + // Get the raw value from variables - const rawValue = this.variables.get(variableKey); + const rawValue = this.variables.get(effectiveKey); // Offer this variable to the property collector for YAML post-processing this.propertyCollector.maybeCollect({ @@ -369,7 +378,7 @@ export abstract class Formatter { }); // Always use string replacement initially - const replacement = this.getVariableValue(variableKey); + const replacement = this.getVariableValue(effectiveKey); // Replace in output and adjust regex position output = output.slice(0, match.index) + replacement + output.slice(match.index + match[0].length); From 7aa127a47a60e44b9b4a5659a68b03558d569fd5 Mon Sep 17 00:00:00 2001 From: Christian Bager Bach Houmann Date: Tue, 23 Dec 2025 17:15:48 +0100 Subject: [PATCH 5/5] refactor: clarify VALUE input type override --- src/formatters/completeFormatter.ts | 4 ++-- src/formatters/formatter.ts | 8 ++++---- src/gui/InputPrompt.ts | 4 ++-- src/preflight/RequirementCollector.ts | 6 +++--- src/utils/valueSyntax.test.ts | 12 ++++++------ src/utils/valueSyntax.ts | 20 ++++++++++---------- 6 files changed, 27 insertions(+), 27 deletions(-) diff --git a/src/formatters/completeFormatter.ts b/src/formatters/completeFormatter.ts index 5217db62..49e4f694 100644 --- a/src/formatters/completeFormatter.ts +++ b/src/formatters/completeFormatter.ts @@ -175,7 +175,7 @@ export class CompleteFormatter extends Formatter { try { const linkSourcePath = this.getLinkSourcePath(); const promptFactory = new InputPrompt().factory( - this.valuePromptContext?.inputType, + this.valuePromptContext?.inputTypeOverride, ); const defaultValue = this.valuePromptContext?.defaultValue; const description = this.valuePromptContext?.description; @@ -225,7 +225,7 @@ export class CompleteFormatter extends Formatter { } // Use default prompt for other variables - return await new InputPrompt().factory(context?.inputType).Prompt( + return await new InputPrompt().factory(context?.inputTypeOverride).Prompt( this.app, header ?? context?.label ?? "Enter value", context?.placeholder ?? diff --git a/src/formatters/formatter.ts b/src/formatters/formatter.ts index 5f79ee49..8df68dba 100644 --- a/src/formatters/formatter.ts +++ b/src/formatters/formatter.ts @@ -44,7 +44,7 @@ export interface PromptContext { description?: string; placeholder?: string; variableKey?: string; - inputType?: ValueInputType; + inputTypeOverride?: ValueInputType; // Undefined means use global input prompt setting. } export abstract class Formatter { @@ -195,8 +195,8 @@ export abstract class Formatter { if (!context.defaultValue && parsed.defaultValue) { context.defaultValue = parsed.defaultValue; } - if (parsed.inputType === "multiline") { - context.inputType = "multiline"; + if (parsed.inputTypeOverride === "multiline") { + context.inputTypeOverride = "multiline"; } } @@ -343,7 +343,7 @@ export abstract class Formatter { variableValue = await this.promptForVariable(variableName, { defaultValue, description: helperText, - inputType: parsed.inputType, + inputTypeOverride: parsed.inputTypeOverride, variableKey, }); } else { diff --git a/src/gui/InputPrompt.ts b/src/gui/InputPrompt.ts index cb791750..a8173a33 100644 --- a/src/gui/InputPrompt.ts +++ b/src/gui/InputPrompt.ts @@ -4,8 +4,8 @@ import QuickAdd from "../main"; import type { ValueInputType } from "../utils/valueSyntax"; export default class InputPrompt { - public factory(inputType?: ValueInputType) { - if (inputType === "multiline") { + public factory(inputTypeOverride?: ValueInputType) { + if (inputTypeOverride === "multiline") { return GenericWideInputPrompt; } if (QuickAdd.instance.settings.inputPrompt === "multi-line") { diff --git a/src/preflight/RequirementCollector.ts b/src/preflight/RequirementCollector.ts index 5d002fcb..5d4a4cfa 100644 --- a/src/preflight/RequirementCollector.ts +++ b/src/preflight/RequirementCollector.ts @@ -157,7 +157,7 @@ export class RequirementCollector extends Formatter { if (!this.requirements.has(requirementId)) { const baseInputType = - parsed.inputType === "multiline" || + parsed.inputTypeOverride === "multiline" || this.plugin.settings.inputPrompt === "multi-line" ? "textarea" : "text"; @@ -199,7 +199,7 @@ export class RequirementCollector extends Formatter { id: key, label: header || "Enter value", type: - this.valuePromptContext?.inputType === "multiline" || + this.valuePromptContext?.inputTypeOverride === "multiline" || this.plugin.settings.inputPrompt === "multi-line" ? "textarea" : "text", @@ -239,7 +239,7 @@ export class RequirementCollector extends Formatter { // Detect simple comma-separated option lists const hasOptions = variableName.includes(","); const baseInputType = - context?.inputType === "multiline" || + context?.inputTypeOverride === "multiline" || this.plugin.settings.inputPrompt === "multi-line" ? "textarea" : "text"; diff --git a/src/utils/valueSyntax.test.ts b/src/utils/valueSyntax.test.ts index 831659ec..9e6aa682 100644 --- a/src/utils/valueSyntax.test.ts +++ b/src/utils/valueSyntax.test.ts @@ -53,7 +53,7 @@ describe("parseValueToken", () => { "Body|type:multiline|label:Notes|default:Hello", ); expect(parsed?.variableName).toBe("Body"); - expect(parsed?.inputType).toBe("multiline"); + expect(parsed?.inputTypeOverride).toBe("multiline"); expect(parsed?.label).toBe("Notes"); expect(parsed?.defaultValue).toBe("Hello"); }); @@ -61,20 +61,20 @@ describe("parseValueToken", () => { it("ignores shorthand default when type is present", () => { const parsed = parseValueToken("Body|Hello|type:multiline"); expect(parsed?.defaultValue).toBe(""); - expect(parsed?.inputType).toBe("multiline"); + expect(parsed?.inputTypeOverride).toBe("multiline"); }); it("warns and ignores unknown type values", () => { const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); const parsed = parseValueToken("Body|type:wide"); - expect(parsed?.inputType).toBeUndefined(); + expect(parsed?.inputTypeOverride).toBeUndefined(); expect(warnSpy).toHaveBeenCalled(); }); it("warns and ignores type for option lists", () => { const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); const parsed = parseValueToken("Red,Green|type:multiline"); - expect(parsed?.inputType).toBeUndefined(); + expect(parsed?.inputTypeOverride).toBeUndefined(); expect(warnSpy).toHaveBeenCalled(); }); }); @@ -88,7 +88,7 @@ describe("parseAnonymousValueOptions", () => { const parsed = parseAnonymousValueOptions( "|type:multiline|label:Notes|default:Hello", ); - expect(parsed.inputType).toBe("multiline"); + expect(parsed.inputTypeOverride).toBe("multiline"); expect(parsed.label).toBe("Notes"); expect(parsed.defaultValue).toBe("Hello"); }); @@ -96,7 +96,7 @@ describe("parseAnonymousValueOptions", () => { it("warns and ignores unknown type for unnamed VALUE tokens", () => { const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); const parsed = parseAnonymousValueOptions("|type:wide"); - expect(parsed.inputType).toBeUndefined(); + expect(parsed.inputTypeOverride).toBeUndefined(); expect(warnSpy).toHaveBeenCalled(); }); }); diff --git a/src/utils/valueSyntax.ts b/src/utils/valueSyntax.ts index 18e94d74..451265be 100644 --- a/src/utils/valueSyntax.ts +++ b/src/utils/valueSyntax.ts @@ -14,7 +14,7 @@ export type ParsedValueToken = { allowCustomInput: boolean; suggestedValues: string[]; hasOptions: boolean; - inputType?: ValueInputType; + inputTypeOverride?: ValueInputType; }; export function buildValueVariableKey( @@ -77,7 +77,7 @@ type ParsedOptions = { defaultValue: string; allowCustomInput: boolean; usesOptions: boolean; - inputType?: string; + inputTypeOverride?: string; }; function parseBoolean(value?: string): boolean { @@ -123,7 +123,7 @@ function parseOptions(optionParts: string[], hasOptions: boolean): ParsedOptions let label: string | undefined; let defaultValue = ""; let allowCustomInput = false; - let inputType: string | undefined; + let inputTypeOverride: string | undefined; for (const part of optionParts) { const trimmed = part.trim(); @@ -151,7 +151,7 @@ function parseOptions(optionParts: string[], hasOptions: boolean): ParsedOptions allowCustomInput = parseBoolean(value); break; case "type": - if (value) inputType = value; + if (value) inputTypeOverride = value; break; default: break; @@ -163,7 +163,7 @@ function parseOptions(optionParts: string[], hasOptions: boolean): ParsedOptions defaultValue, allowCustomInput, usesOptions: true, - inputType, + inputTypeOverride, }; } @@ -215,7 +215,7 @@ export function parseValueToken(raw: string): ParsedValueToken | null { } const tokenDisplay = `{{VALUE:${raw}}}`; - const inputType = resolveInputType(options.inputType, { + const inputTypeOverride = resolveInputType(options.inputTypeOverride, { tokenDisplay, hasOptions, allowCustomInput, @@ -232,7 +232,7 @@ export function parseValueToken(raw: string): ParsedValueToken | null { allowCustomInput, suggestedValues, hasOptions, - inputType, + inputTypeOverride, }; } @@ -241,7 +241,7 @@ export function parseAnonymousValueOptions( ): { label?: string; defaultValue: string; - inputType?: ValueInputType; + inputTypeOverride?: ValueInputType; } { const normalized = rawOptions.startsWith("|") ? rawOptions.slice(1) @@ -262,7 +262,7 @@ export function parseAnonymousValueOptions( } const tokenDisplay = `{{VALUE${rawOptions}}}`; - const inputType = resolveInputType(options.inputType, { + const inputTypeOverride = resolveInputType(options.inputTypeOverride, { tokenDisplay, hasOptions: false, allowCustomInput: options.allowCustomInput, @@ -271,6 +271,6 @@ export function parseAnonymousValueOptions( return { label, defaultValue, - inputType, + inputTypeOverride, }; }