diff --git a/lib/core/theme/parser/jsonc_preprocessor.dart b/lib/core/theme/parser/jsonc_preprocessor.dart index 8497172b..9f6e92c0 100644 --- a/lib/core/theme/parser/jsonc_preprocessor.dart +++ b/lib/core/theme/parser/jsonc_preprocessor.dart @@ -9,8 +9,10 @@ String stripJsonc(String input) { final next = i + 1 < len ? input[i + 1] : ''; if (ch == '"') { - out.write(_copyStringLiteral(input, i)); - i = _skipStringLiteral(input, i); + // One scan finds the closing quote; the literal is copied as a slice. + final end = _stringLiteralEnd(input, i); + out.write(input.substring(i, end)); + i = end; continue; } @@ -54,37 +56,20 @@ String stripJsonc(String input) { bool _isWhitespace(String c) => c == ' ' || c == '\t' || c == '\n' || c == '\r'; -String _copyStringLiteral(String s, int start) { - final buf = StringBuffer(); - var i = start; - buf.write(s[i]); - i++; - while (i < s.length) { - final ch = s[i]; - buf.write(ch); - if (ch == '\\' && i + 1 < s.length) { - i++; - buf.write(s[i]); - } else if (ch == '"') { - i++; - break; - } - i++; - } - return buf.toString(); -} - -int _skipStringLiteral(String s, int start) { +/// Offset after the string literal that opens at [start] (a `"`): past its +/// closing quote, or the end of [s] when it is not terminated. A backslash +/// escapes the next character. +int _stringLiteralEnd(String s, int start) { + final len = s.length; var i = start + 1; - while (i < s.length) { - if (s[i] == '\\') { + while (i < len) { + final c = s.codeUnitAt(i); + if (c == 0x5C) { i += 2; continue; } - if (s[i] == '"') { - return i + 1; - } + if (c == 0x22) return i + 1; i++; } - return s.length; + return len; } diff --git a/test/core/theme/parser/jsonc_preprocessor_test.dart b/test/core/theme/parser/jsonc_preprocessor_test.dart index 77c94752..39196cec 100644 --- a/test/core/theme/parser/jsonc_preprocessor_test.dart +++ b/test/core/theme/parser/jsonc_preprocessor_test.dart @@ -42,5 +42,35 @@ void main() { expect(cleaned.contains(',}'), isFalse); expect(cleaned, contains('"editor.background"')); }); + + test('keeps string literals byte for byte, escapes included', () { + const input = r'{"a": "q\"uote // not a comment", "b": "back\\"}'; + expect(stripJsonc(input), input); + }); + + test('comment markers and trailing commas inside strings survive', () { + const input = r'{"a": "/* x */", "b": "[1,]", "c": "x,}"}'; + expect(stripJsonc(input), input); + }); + + test('an unterminated string is copied to the end', () { + expect(stripJsonc('{"a": "oops'), '{"a": "oops'); + }); + + test('a trailing backslash at the end of input does not throw', () { + expect(stripJsonc(r'{"a": "x\'), r'{"a": "x\'); + }); + + test('comments and trailing commas around many strings', () { + final input = StringBuffer('{\n'); + for (var i = 0; i < 200; i++) { + input.write(' // token $i\n "k$i": "#${i.toRadixString(16).padLeft(6, '0')}",\n'); + } + input.write('}'); + final cleaned = stripJsonc(input.toString()); + expect(cleaned.contains('//'), isFalse); + expect(cleaned.contains(',\n}'), isFalse); + expect(cleaned, contains('"k199": "#0000c7"')); + }); }); }