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
45 changes: 44 additions & 1 deletion src/lib/helpers/string.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import { singular, camelize, capitalize } from '$lib/helpers/string';
import {
singular,
camelize,
capitalize,
normalizeSmartQuotes,
repairSmartQuotedJson
} from '$lib/helpers/string';
import { expect, test } from 'vitest';

/*
Expand Down Expand Up @@ -97,3 +103,40 @@ test('capitalize should handle strings with no lowercase letters', () => {
test('capitalize should handle strings with only one character', () => {
expect(capitalize('a')).toBe('A');
});

/*
NORMALIZE SMART QUOTES
*/

test('normalizeSmartQuotes replaces curly double quotes only', () => {
const curly = '{ \u201Ctest\u201D: \u2018value\u2019 }';
expect(normalizeSmartQuotes(curly)).toBe('{ "test": \u2018value\u2019 }');
});

test('normalizeSmartQuotes leaves ASCII quotes unchanged', () => {
expect(normalizeSmartQuotes('{ "test": \'value\' }')).toBe('{ "test": \'value\' }');
});

test('normalizeSmartQuotes handles empty input', () => {
expect(normalizeSmartQuotes('')).toBe('');
});

test('repairSmartQuotedJson fixes curly structural quotes', () => {
const curly = '{ \u201Ctest\u201D: \u201Cvalue\u201D }';
expect(repairSmartQuotedJson(curly)).toBe('{ "test": "value" }');
});

test('repairSmartQuotedJson leaves valid JSON with curly quotes in values', () => {
const valid = '{ "quote": "He said \u201Chello\u201D" }';
expect(repairSmartQuotedJson(valid)).toBe(valid);
});

test('repairSmartQuotedJson preserves apostrophes when repairing delimiters', () => {
const curly = '{ \u201Cname\u201D: \u201CO\u2019Brien\u201D }';
expect(repairSmartQuotedJson(curly)).toBe('{ "name": "O\u2019Brien" }');
});

test('repairSmartQuotedJson leaves non-JSON bodies unchanged', () => {
const plain = 'Hello \u201Cworld\u201D';
expect(repairSmartQuotedJson(plain)).toBe(plain);
});
33 changes: 33 additions & 0 deletions src/lib/helpers/string.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,3 +127,36 @@ export function hash(input: string | string[], delimiter: string = ','): string
}
return Math.abs(hash).toString(36);
}

/**
* Replace typographic / “smart” double quotes with ASCII `"`.
* Safari (and some other editors) substitute curly quotes while typing, which
* breaks JSON request bodies. Single quotes / apostrophes are left alone —
* JSON string delimiters are always double quotes.
*/
export function normalizeSmartQuotes(str: string): string {
if (!str) return str;
return str.replace(/[\u201C\u201D\u201E\u201F\u2033\u2036]/g, '"');
}

/**
* Fix Safari-style smart double quotes when they make an otherwise-JSON body
* invalid. Leaves already-valid JSON and non-JSON bodies unchanged so
* intentional typographic characters in string values or plain text are
* preserved. Only double quotes are rewritten (JSON delimiters).
*/
export function repairSmartQuotedJson(str: string): string {
if (!str) return str;
try {
JSON.parse(str);
return str;
} catch {
Comment thread
greptile-apps[bot] marked this conversation as resolved.
const normalized = normalizeSmartQuotes(str);
try {
JSON.parse(normalized);
return normalized;
} catch {
return str;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
toLocaleTimeISO
} from '$lib/helpers/date';
import { last } from '$lib/helpers/array';
import { repairSmartQuotedJson } from '$lib/helpers/string';
import {
Accordion,
Alert,
Expand Down Expand Up @@ -100,7 +101,9 @@
.forProject(page.params.region, page.params.project)
.functions.createExecution({
functionId: func.$id,
body,
// Safari may substitute curly/smart quotes while typing JSON.
// Only rewrite when that made the body invalid JSON.
body: repairSmartQuotedJson(body),
async: true,
xpath: path,
method,
Expand Down Expand Up @@ -179,6 +182,7 @@
<InputTextarea
placeholder="Enter request body here..."
id="body"
spellcheck={false}
bind:value={body} />
</Layout.Stack>
</Accordion>
Expand Down Expand Up @@ -294,6 +298,7 @@
<InputTextarea
placeholder="Enter request body here..."
id="body"
spellcheck={false}
bind:value={body} />
</Layout.Stack>
</Accordion>
Expand Down