From fd0ca6d4554e3613a6131113286359db38532593 Mon Sep 17 00:00:00 2001 From: Gregory Schier Date: Fri, 26 Jun 2026 21:58:38 -0700 Subject: [PATCH] Fix bulk env var parsing (#482) --- .../components/core/BulkPairEditor.test.ts | 36 +++++++++++++++++++ .../components/core/BulkPairEditor.tsx | 12 +++---- .../core/Editor/pairs/pairs.grammar | 2 +- .../core/Editor/pairs/pairs.test.ts | 26 ++++++++++++++ .../components/core/Editor/pairs/pairs.ts | 2 +- packages/theme/src/yaakColor.ts | 11 +++--- 6 files changed, 77 insertions(+), 12 deletions(-) create mode 100644 apps/yaak-client/components/core/BulkPairEditor.test.ts create mode 100644 apps/yaak-client/components/core/Editor/pairs/pairs.test.ts diff --git a/apps/yaak-client/components/core/BulkPairEditor.test.ts b/apps/yaak-client/components/core/BulkPairEditor.test.ts new file mode 100644 index 00000000..b0599802 --- /dev/null +++ b/apps/yaak-client/components/core/BulkPairEditor.test.ts @@ -0,0 +1,36 @@ +import { describe, expect, test } from "vite-plus/test"; +import { parseBulkPairLine } from "./BulkPairEditor"; + +describe("parseBulkPairLine", () => { + test("parses colon-space pairs as name and value", () => { + expect(parseBulkPairLine("foo: bar")).toMatchObject({ + enabled: true, + name: "foo", + value: "bar", + }); + }); + + test("preserves colon-without-space lines as a name with an empty value", () => { + expect(parseBulkPairLine("foo:bar")).toMatchObject({ + enabled: true, + name: "foo:bar", + value: "", + }); + }); + + test("preserves malformed lines instead of dropping their contents", () => { + expect(parseBulkPairLine("not a pair")).toMatchObject({ + enabled: true, + name: "not a pair", + value: "", + }); + }); + + test("unescapes newlines in parsed values", () => { + expect(parseBulkPairLine("foo: bar\\nbaz")).toMatchObject({ + enabled: true, + name: "foo", + value: "bar\nbaz", + }); + }); +}); diff --git a/apps/yaak-client/components/core/BulkPairEditor.tsx b/apps/yaak-client/components/core/BulkPairEditor.tsx index 0a35eaea..fe3b4bd5 100644 --- a/apps/yaak-client/components/core/BulkPairEditor.tsx +++ b/apps/yaak-client/components/core/BulkPairEditor.tsx @@ -17,7 +17,7 @@ export function BulkPairEditor({ const pairsText = useMemo(() => { return pairs .filter((p) => !(p.name.trim() === "" && p.value.trim() === "")) - .map(pairToLine) + .map(formatBulkPairLine) .join("\n"); }, [pairs]); @@ -26,7 +26,7 @@ export function BulkPairEditor({ const pairs = text .split("\n") .filter((l: string) => l.trim()) - .map(lineToPair); + .map(parseBulkPairLine); onChange(pairs); }, [onChange], @@ -47,16 +47,16 @@ export function BulkPairEditor({ ); } -function pairToLine(pair: Pair) { +export function formatBulkPairLine(pair: Pair) { const value = pair.value.replaceAll("\n", "\\n"); return `${pair.name}: ${value}`; } -function lineToPair(line: string): PairWithId { - const [, name, value] = line.match(/^(:?[^:]+):\s+(.*)$/) ?? []; +export function parseBulkPairLine(line: string): PairWithId { + const [, name, value] = line.match(/^([^:]+):\s+(.*)$/) ?? []; return { enabled: true, - name: (name ?? "").trim(), + name: (name ?? line).trim(), value: (value ?? "").replaceAll("\\n", "\n").trim(), id: generateId(), }; diff --git a/apps/yaak-client/components/core/Editor/pairs/pairs.grammar b/apps/yaak-client/components/core/Editor/pairs/pairs.grammar index ec060c0c..bcb1fca3 100644 --- a/apps/yaak-client/components/core/Editor/pairs/pairs.grammar +++ b/apps/yaak-client/components/core/Editor/pairs/pairs.grammar @@ -1,7 +1,7 @@ @top pairs { (Key Sep Value "\n")* } @tokens { - Sep { ":" } + Sep { ":" $[ \t]+ } Key { ":"? ![:]+ } Value { ![\n]+ } } diff --git a/apps/yaak-client/components/core/Editor/pairs/pairs.test.ts b/apps/yaak-client/components/core/Editor/pairs/pairs.test.ts new file mode 100644 index 00000000..440f7c58 --- /dev/null +++ b/apps/yaak-client/components/core/Editor/pairs/pairs.test.ts @@ -0,0 +1,26 @@ +import { describe, expect, test } from "vite-plus/test"; +import { parser } from "./pairs"; + +function getNodeNames(input: string): string[] { + const tree = parser.parse(input); + const nodes: string[] = []; + const cursor = tree.cursor(); + do { + if (cursor.name !== "pairs") { + nodes.push(cursor.name); + } + } while (cursor.next()); + return nodes; +} + +describe("pairs grammar", () => { + test("parses colon-space pairs with a value", () => { + expect(getNodeNames("foo: bar\n")).toEqual(["Key", "Sep", "Value"]); + }); + + test("does not parse colon-without-space as a value", () => { + const nodes = getNodeNames("foo:bar\n"); + + expect(nodes).not.toContain("Value"); + }); +}); diff --git a/apps/yaak-client/components/core/Editor/pairs/pairs.ts b/apps/yaak-client/components/core/Editor/pairs/pairs.ts index f5cc1403..fdd40d5e 100644 --- a/apps/yaak-client/components/core/Editor/pairs/pairs.ts +++ b/apps/yaak-client/components/core/Editor/pairs/pairs.ts @@ -12,7 +12,7 @@ export const parser = LRParser.deserialize({ skippedNodes: [0], repeatNodeCount: 1, tokenData: - "$]VRVOYhYZ#[Z![h![!]#o!];'Sh;'S;=`#U<%lOhToVQPSSOYhYZ!UZ![h![!]!m!];'Sh;'S;=`#U<%lOhP!ZSQPO![!U!];'S!U;'S;=`!g<%lO!UP!jP;=`<%l!US!rSSSOY!mZ;'S!m;'S;=`#O<%lO!mS#RP;=`<%l!mT#XP;=`<%lhR#cSVQQPO![!U!];'S!U;'S;=`!g<%lO!UV#vVRQSSOYhYZ!UZ![h![!]!m!];'Sh;'S;=`#U<%lOh", + "%]VRVOYhYZ#[Z![h![!]#o!];'Sh;'S;=`#U<%lOhToVQPSSOYhYZ!UZ![h![!]!m!];'Sh;'S;=`#U<%lOhP!ZSQPO![!U!];'S!U;'S;=`!g<%lO!UP!jP;=`<%l!US!rSSSOY!mZ;'S!m;'S;=`#O<%lO!mS#RP;=`<%l!mT#XP;=`<%lhR#cSVQQPO![!U!];'S!U;'S;=`!g<%lO!UV#tYSSOXhXY$dYZ!UZphpq$dq![h![!]!m!];'Sh;'S;=`#U<%lOhV$mYQPRQSSOXhXY$dYZ!UZphpq$dq![h![!]!m!];'Sh;'S;=`#U<%lOh", tokenizers: [0, 1, 2], topRules: { pairs: [0, 1] }, tokenPrec: 0, diff --git a/packages/theme/src/yaakColor.ts b/packages/theme/src/yaakColor.ts index 6c9bb94e..41f6b0b8 100644 --- a/packages/theme/src/yaakColor.ts +++ b/packages/theme/src/yaakColor.ts @@ -247,10 +247,13 @@ function parseOklch( ); if (match == null) return null; - const lightness = parseOklchLightness(match[1]); - const chroma = parseCssNumber(match[2], 1); - const hue = normalizeHue(parseCssNumber(match[3].replace(/deg$/i, ""), 1)); - const alpha = parseCssNumber(match[4] ?? match[5] ?? "1", 1); + const [, lightnessValue, chromaValue, hueValue, slashAlpha, commaAlpha] = match; + if (lightnessValue == null || chromaValue == null || hueValue == null) return null; + + const lightness = parseOklchLightness(lightnessValue); + const chroma = parseCssNumber(chromaValue, 1); + const hue = normalizeHue(parseCssNumber(hueValue.replace(/deg$/i, ""), 1)); + const alpha = parseCssNumber(slashAlpha ?? commaAlpha ?? "1", 1); if ( !Number.isFinite(lightness) ||