Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions docs/src/content/docs/docs/FormatSyntax.md
Original file line number Diff line number Diff line change
Expand Up @@ -401,8 +401,10 @@ from where the placeholder appears:
- `|format:markdown` writes a vertical Markdown bullet list. Put the placeholder
on its own line: `{{VALUE:Alpha,Beta|multi|format:markdown}}` becomes `- Alpha`
followed by `- Beta`.
- `|format:inline` always writes the existing comma-separated text form:
- `|format:inline` always writes the compact comma-separated text form:
`Alpha,Beta`.
- `|format:spaced` writes the same text with a space after each comma:
`{{VALUE:option a, option b|multi|format:spaced}}` becomes `option a, option b`.
- `|format:auto` is the default and preserves the context-sensitive behavior
described below.

Expand Down Expand Up @@ -752,7 +754,8 @@ defaults as single-value FIELD prompts:
`{{FIELD:topic|multi|folder:Projects|tag:active|default:Inbox}}`.

FIELD multi-selects support the same explicit output formats as VALUE:
`|format:yaml`, `|format:markdown`, `|format:inline`, and `|format:auto`.
`|format:yaml`, `|format:markdown`, `|format:inline`, `|format:spaced`,
and `|format:auto`.
For example, `topics: {{FIELD:topic|multi|format:yaml}}` always writes a native
YAML list, including in template-backed captures.

Expand Down Expand Up @@ -890,7 +893,7 @@ Good to know:
- In a one-page input form, single and multi FILE pickers appear inline. Search matches the friendly title, file name, and full path. Selected files remain exact path-backed values internally, so commas in file names or labels are safe.

FILE multi-selects support `|format:yaml`, `|format:markdown`,
`|format:inline`, and `|format:auto`. The format composes with `|link` and
`|format:inline`, `|format:spaced`, and `|format:auto`. The format composes with `|link` and
`|path`, so `{{FILE:People|multi|link|format:yaml}}` writes a native YAML list
of links without relying on the capture context.

Expand Down
4 changes: 2 additions & 2 deletions src/engine/CaptureChoiceEngine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ function isCaptureContentEmpty(content: string): boolean {
const MULTI_SELECT_TOKEN_REGEX =
/\{\{(?:VALUE|FILE|FIELD):[^}]*\|\s*multi\s*(?=[:}|]|$)[^}]*\}\}/gi;
const EXPLICIT_MULTI_FORMAT_REGEX =
/\|\s*format\s*:\s*(?:inline|yaml|markdown)\s*(?=\||}})/i;
/\|\s*format\s*:\s*(?:inline|spaced|yaml|markdown)\s*(?=\||}})/i;

function hasContextualMultiSelectToken(input: string): boolean {
return Array.from(input.matchAll(MULTI_SELECT_TOKEN_REGEX)).some(
Expand Down Expand Up @@ -509,7 +509,7 @@ export class CaptureChoiceEngine extends QuickAddChoiceEngine {
hasContextualMultiSelectToken(this.choice?.format?.format ?? "")
) {
log.logWarning(
"QuickAdd: {{VALUE:…|multi}}, {{FILE:…|multi}} and {{FIELD:…|multi}} in this capture write comma-separated strings by default. Add |format:yaml, |format:markdown or |format:inline to choose the output explicitly.",
"QuickAdd: {{VALUE:…|multi}}, {{FILE:…|multi}} and {{FIELD:…|multi}} in this capture write comma-separated strings by default. Add |format:yaml, |format:markdown, |format:inline or |format:spaced to choose the output explicitly.",
);
}

Expand Down
28 changes: 28 additions & 0 deletions src/formatters/completeFormatter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1819,6 +1819,7 @@ describe("CompleteFormatter - remote prompt provider routing", () => {
["yaml", '["a", "c"]'],
["markdown", "- a\n- c"],
["inline", "a,c"],
["spaced", "a, c"],
] as const)("renders VALUE multi-selects with |format:%s", async (format, expected) => {
const suggesterMulti = vi.fn(
async (_display: string[], _actual: string[]) => ["a", "c"],
Expand All @@ -1830,6 +1831,33 @@ describe("CompleteFormatter - remote prompt provider routing", () => {
).resolves.toBe(expected);
});

it("keeps format:inline compact when the option list has spaces", async () => {
const suggesterMulti = vi.fn(
async (_display: string[], _actual: string[]) => ["a", "b"],
);
const f = providerFormatter({ suggesterMulti });

await expect(
f.formatFileContent("{{VALUE:a, b|multi|format:inline}}"),
).resolves.toBe("a,b");
});

it("joins format:spaced with a comma and a space, including spaced option names", async () => {
const suggesterMulti = vi.fn(
async (_display: string[], _actual: string[]) => [
"option a",
"option b",
],
);
const f = providerFormatter({ suggesterMulti });

await expect(
f.formatFileContent(
"{{VALUE:option a, option b|multi|format:spaced}}",
),
).resolves.toBe("option a, option b");
});

it("routes anonymous {{VALUE|type:checkbox}} to the provider's suggester, not the Obsidian modal", async () => {
mocks.genericSuggesterSuggest.mockResolvedValue("false"); // modal answer (should be unused)
const suggester = vi.fn(async () => "true");
Expand Down
14 changes: 14 additions & 0 deletions src/formatters/formatter-field-title-regression.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,20 @@ describe("Formatter FIELD and TITLE namespace handling", () => {
expect(formatter.getAndClearTemplatePropertyVars().size).toBe(0);
});

it("writes format:spaced as comma-space text inside a collection scope", async () => {
formatter.setMockFieldResponse("topic|multi|format:spaced", [
"Alpha",
"Beta",
]);

await expect(
formatter.runFormatWithPropertyCollection(
"---\ntopics: {{FIELD:topic|multi|format:spaced}}\n---\n",
),
).resolves.toBe("---\ntopics: Alpha, Beta\n---\n");
expect(formatter.getAndClearTemplatePropertyVars().size).toBe(0);
});

it("collects FIELD multi arrays from YAML list item token positions", async () => {
formatter.setMockFieldResponse("topic|multi", ["Alpha", "Beta"]);

Expand Down
8 changes: 8 additions & 0 deletions src/utils/FieldSuggestionParser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,14 @@ describe("FieldSuggestionParser", () => {
expect(result.multiFormat).toBe("markdown");
});

it("parses |format:spaced", () => {
const result = FieldSuggestionParser.parse(
"topics|multi|format:spaced",
);
expect(result.multiSelect).toBe(true);
expect(result.multiFormat).toBe("spaced");
});

it("warns on |format: without |multi, even |format:auto", () => {
const warnings: string[] = [];
const result = FieldSuggestionParser.parse("topics|format:auto", {
Expand Down
6 changes: 6 additions & 0 deletions src/utils/fileSyntax.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,12 @@ describe("parseFileToken", () => {
expect(parsed?.multiFormat).toBe("yaml");
});

it("parses |format:spaced", () => {
const parsed = parseFileToken("People|multi|format:spaced");
expect(parsed?.multiSelect).toBe(true);
expect(parsed?.multiFormat).toBe("spaced");
});

it("warns on |format: without |multi, even |format:auto", () => {
const warnings: string[] = [];
const parsed = parseFileToken("People|format:auto", {
Expand Down
17 changes: 14 additions & 3 deletions src/utils/multiValueFormat.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
} from "./multiValueFormat";

describe("multi-select output formatting", () => {
it.each(["auto", "inline", "yaml", "markdown"] as const)(
it.each(["auto", "inline", "spaced", "yaml", "markdown"] as const)(
"parses %s",
(format) => {
expect(parseMultiValueFormat(format, "token")).toBe(format);
Expand All @@ -16,7 +16,7 @@ describe("multi-select output formatting", () => {
const warn = vi.fn();
expect(parseMultiValueFormat("table", "token", warn)).toBeUndefined();
expect(warn).toHaveBeenCalledWith(
expect.stringContaining("auto, inline, yaml, markdown"),
expect.stringContaining("auto, inline, spaced, yaml, markdown"),
);
});

Expand All @@ -31,7 +31,7 @@ describe("multi-select output formatting", () => {
).toBeUndefined();
});

it("renders inline output with the legacy comma separator", () => {
it("renders inline output with the compact comma separator", () => {
expect(
renderExplicitMultiValue({
input: "value",
Expand All @@ -42,6 +42,17 @@ describe("multi-select output formatting", () => {
).toBe("Alpha,Beta");
});

it("renders spaced output with a comma and a space", () => {
expect(
renderExplicitMultiValue({
input: "value",
matchStart: 0,
values: ["Alpha", "Beta"],
format: "spaced",
}),
).toBe("Alpha, Beta");
});

it("renders a quoted YAML flow sequence that preserves string values", () => {
expect(
renderExplicitMultiValue({
Expand Down
33 changes: 22 additions & 11 deletions src/utils/multiValueFormat.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { quoteYamlDouble } from "./yamlScalarQuoting";
import type { WarnSink } from "./warnSink";

export type MultiValueFormat = "auto" | "inline" | "yaml" | "markdown";
export type MultiValueFormat = "auto" | "inline" | "spaced" | "yaml" | "markdown";

const MULTI_VALUE_FORMATS = new Set<MultiValueFormat>([
"auto",
"inline",
"spaced",
"yaml",
"markdown",
]);
Expand All @@ -21,7 +22,7 @@ export function parseMultiValueFormat(
}

warn?.(
`QuickAdd: Unsupported multi-select format "${raw}" in "${tokenDisplay}". Supported formats: auto, inline, yaml, markdown.`,
`QuickAdd: Unsupported multi-select format "${raw}" in "${tokenDisplay}". Supported formats: auto, inline, spaced, yaml, markdown.`,
);
return undefined;
}
Expand All @@ -46,15 +47,25 @@ export function renderExplicitMultiValue(args: {
format: MultiValueFormat;
}): string | undefined {
const { input, matchStart, values, format } = args;
if (format === "auto") return undefined;

const strings = values.map((value) => String(value));
if (format === "inline") return strings.join(",");
if (format === "yaml") {
return `[${strings.map(quoteYamlDouble).join(", ")}]`;
}

if (strings.length === 0) return "";
const indent = currentLineIndent(input, matchStart);
return strings.map(renderMarkdownItem).join(`\n${indent}`);
switch (format) {
case "auto":
return undefined;
case "inline":
return strings.join(",");
Comment thread
coderabbitai[bot] marked this conversation as resolved.
case "spaced":
return strings.join(", ");
case "yaml":
return `[${strings.map(quoteYamlDouble).join(", ")}]`;
case "markdown": {
if (strings.length === 0) return "";
const indent = currentLineIndent(input, matchStart);
return strings.map(renderMarkdownItem).join(`\n${indent}`);
}
default: {
const _exhaustive: never = format;
return _exhaustive;
}
}
}
2 changes: 1 addition & 1 deletion src/utils/valueSyntax.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,7 @@ describe("parseValueToken", () => {
expect(parsed?.multiEmit).toBe("linklist");
});

it.each(["yaml", "markdown", "inline"] as const)(
it.each(["yaml", "markdown", "inline", "spaced"] as const)(
"parses |format:%s separately from |multi item emission",
(format) => {
const parsed = parseValueToken(
Expand Down
Loading