newo 3.7.2 → 3.7.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -8,7 +8,7 @@
8
8
  * `value_type: json`. The API may return the `value` field as either a
9
9
  * STRING containing JSON or as an already-parsed OBJECT.
10
10
  *
11
- * Without normalization, two bugs leak through:
11
+ * Without normalization, several bugs leak through:
12
12
  *
13
13
  * 1. When the API returns the value as an OBJECT, `yaml.dump` serializes
14
14
  * it as a YAML structure (mappings/sequences). Pushing back then sends
@@ -21,10 +21,25 @@
21
21
  * string vs object representations it triggers spurious pushes that
22
22
  * overwrite the canvas with the wrong shape (Builder shows blank).
23
23
  *
24
- * The fix is conservative: for `value_type: json` only, always coerce the
25
- * value to a STRING when persisting and when pushing, and use canonical
26
- * JSON for comparisons. String-typed values in the wild are left
27
- * untouched, so no churn for the majority of attributes.
24
+ * 3. (Bug 3.7.2-a) Canvas JSON strings with structural newlines (real
25
+ * U+000A between tokens) can be emitted by yaml.dump as double-quoted
26
+ * scalars with `\n` escape sequences. patchYamlToPyyaml then converts
27
+ * those to single-quoted YAML scalars, where `\n` is treated as two
28
+ * literal chars (backslash + n). On push the platform stores those
29
+ * literal chars and the Builder calls JSON.parse, which fails on
30
+ * backslash-n as structural whitespace.
31
+ *
32
+ * 4. (Bug 3.7.2-b) Canvas body text contains Markdown with `\_`
33
+ * (backslash + underscore). `\_` is not a valid JSON escape sequence
34
+ * per RFC 8259 (valid ones: " \ / b f n r t uXXXX). Chrome V8's
35
+ * JSON.parse is strict: it throws SyntaxError on `\_`, silently
36
+ * blanking the Builder.
37
+ *
38
+ * The fix for (3) and (4): for `value_type: json` string values, strip
39
+ * invalid escape sequences then compact via JSON.parse + JSON.stringify.
40
+ * Compaction removes structural newlines and re-serializes all string
41
+ * values with only valid JSON escapes, producing a single-line string
42
+ * that round-trips through YAML without corruption.
28
43
  */
29
44
 
30
45
  /**
@@ -38,22 +53,78 @@ export function isJsonValueType(valueType: unknown): boolean {
38
53
  return lower === 'json' || lower.endsWith('.json');
39
54
  }
40
55
 
56
+ /**
57
+ * Fix invalid JSON escape sequences inside JSON string values.
58
+ *
59
+ * Per RFC 8259, valid escape sequences inside a JSON string are:
60
+ * \" \\ \/ \b \f \n \r \t \uXXXX
61
+ * Anything else (e.g. `\_` `\.` from Markdown) is invalid and causes
62
+ * JSON.parse to throw. Fix: drop the backslash (e.g. `\_` → `_`).
63
+ *
64
+ * Only modifies characters inside JSON string values (tracks quote
65
+ * context). Structural characters outside strings are untouched.
66
+ */
67
+ export function fixInvalidJsonEscapes(s: string): string {
68
+ const VALID_ESCAPES = new Set(['"', '\\', '/', 'b', 'f', 'n', 'r', 't', 'u']);
69
+ const result: string[] = [];
70
+ let inString = false;
71
+ let i = 0;
72
+ while (i < s.length) {
73
+ const c = s[i]!;
74
+ if (inString) {
75
+ if (c === '\\' && i + 1 < s.length) {
76
+ const next = s[i + 1]!;
77
+ if (VALID_ESCAPES.has(next)) {
78
+ result.push(c, next);
79
+ } else {
80
+ result.push(next); // drop the backslash — \_ → _, etc.
81
+ }
82
+ i += 2;
83
+ continue;
84
+ } else if (c === '"') {
85
+ inString = false;
86
+ result.push(c);
87
+ } else {
88
+ result.push(c);
89
+ }
90
+ } else {
91
+ if (c === '"') {
92
+ inString = true;
93
+ result.push(c);
94
+ } else {
95
+ result.push(c);
96
+ }
97
+ }
98
+ i++;
99
+ }
100
+ return result.join('');
101
+ }
102
+
41
103
  /**
42
104
  * Coerce a JSON-typed attribute's value to a STRING suitable for storage
43
105
  * in attributes.yaml and for sending to the platform.
44
106
  *
45
107
  * - `null` / `undefined` → `''`
46
108
  * - object → compact JSON string (`JSON.stringify(value)`)
47
- * - string → returned as-is (we trust the platform's existing format)
109
+ * - string → fix invalid escapes (e.g. `\_` `_`), then compact via
110
+ * JSON.parse + JSON.stringify. If parsing still fails after
111
+ * fixing escapes, return the fixed string as-is.
48
112
  * - other → `String(value)`
49
113
  *
50
- * We deliberately do NOT re-format string values, even when they look
51
- * like JSON. Many existing canvases are stored pretty-printed and
52
- * reformatting would create huge spurious diffs in users' repos.
114
+ * Compacting removes structural newlines and guarantees a single-line
115
+ * string that yaml.dump serializes without escape-sequence corruption in
116
+ * the patchYamlToPyyaml pass. See module-level comment for full context.
53
117
  */
54
118
  export function normalizeJsonValueForStorage(value: unknown): string {
55
119
  if (value == null) return '';
56
- if (typeof value === 'string') return value;
120
+ if (typeof value === 'string') {
121
+ const fixed = fixInvalidJsonEscapes(value);
122
+ try {
123
+ return JSON.stringify(JSON.parse(fixed));
124
+ } catch {
125
+ return fixed;
126
+ }
127
+ }
57
128
  if (typeof value === 'object') {
58
129
  try {
59
130
  return JSON.stringify(value);
@@ -68,18 +139,12 @@ export function normalizeJsonValueForStorage(value: unknown): string {
68
139
  * Canonical comparison for JSON-typed attribute values.
69
140
  *
70
141
  * Returns the canonical form (compact JSON if parseable, otherwise the
71
- * raw string). Use this on both sides of a comparison so that pretty- vs
72
- * compact-printed JSON does not register as a change, and so that an
142
+ * fixed string). Use this on both sides of a comparison so that pretty-
143
+ * vs compact-printed JSON does not register as a change, and so that an
73
144
  * object on one side equals its stringified form on the other side.
74
145
  */
75
146
  export function canonicalJsonValue(value: unknown): string {
76
- const stringified = normalizeJsonValueForStorage(value);
77
- if (stringified === '') return '';
78
- try {
79
- return JSON.stringify(JSON.parse(stringified));
80
- } catch {
81
- return stringified;
82
- }
147
+ return normalizeJsonValueForStorage(value);
83
148
  }
84
149
 
85
150
  /**