diff --git a/config.json b/config.json index 6fcb8932ad..98730d2b19 100644 --- a/config.json +++ b/config.json @@ -1736,6 +1736,14 @@ "text_formatting" ] }, + { + "slug": "sgf-parsing", + "name": "SGF Parsing", + "uuid": "6a44570c-f9a0-4cd7-9217-fcf7f39b3de0", + "practices": [], + "prerequisites": [], + "difficulty": 7 + }, { "slug": "diamond", "name": "Diamond", diff --git a/exercises/practice/sgf-parsing/.docs/instructions.md b/exercises/practice/sgf-parsing/.docs/instructions.md new file mode 100644 index 0000000000..4e1f59b0dc --- /dev/null +++ b/exercises/practice/sgf-parsing/.docs/instructions.md @@ -0,0 +1,66 @@ +# Instructions + +Your task is to parse a Smart Game Format (SGF) string and return a tree data structure representing the game. + +An SGF string represents a tree of nodes. The entire tree is enclosed in parentheses `()`. +Each node in the tree is denoted by a semicolon `;`, followed by a sequence of properties. +A property consists of an uppercase identifier (e.g., `AB`), followed by one or more values enclosed in brackets `[]`. + +For example, the string `(;FF[4]C[root])` represents a tree with a single root node. That node has two properties: +- Property `FF` with a value of `4` +- Property `C` with a value of `root` + +**Variations (Trees and Branches)** +SGF supports game variations by nesting trees. A tree can have multiple child trees, which are also enclosed in parentheses. +For example, `(;A[B](;C[D])(;E[F]))` represents a root node with property `A`, which has two child branches: +1. A node with property `C` and value `D` +2. A node with property `E` and value `F` + +**Formatting & Escaping** +Values within brackets can contain escaped characters. A backslash `\` followed by any character represents that literal character (e.g., `\]` should be parsed as a literal `]`). Additionally, if a newline is preceded by a backslash, it acts as a line continuation and should be removed from the value. All other newlines within a value should be preserved. + +**Error Handling** +You must throw an error for invalid SGF strings. The following are examples of invalid SGF: +- A tree that does not start with parentheses `()`. +- A node that does not start with a semicolon `;`. +- Property identifiers that are not entirely uppercase letters. +- Missing brackets or mismatched parentheses. + +### Data Structure + +Your parsing function should return a representation of the tree. A common approach is to represent each tree as an object with two fields: +- `properties`: An object (or Map) where the keys are the property identifiers (strings) and the values are arrays of strings (since a property can have multiple values). +- `children`: An array containing the child trees. + + +Example: + +Input: `(;FF[4]C[root])` +Output: `{ + properties: { + FF: ['4'], + C: ['root'] + }, + children: [] +}` + +Input: `(;A[B](;C[D])(;E[F]))` +Output: `{ + properties: { + A: ['B'] + }, + children: [ + { + properties: { + C: ['D'] + }, + children: [] + }, + { + properties: { + E: ['F'] + }, + children: [] + } + ] +}` diff --git a/exercises/practice/sgf-parsing/.docs/introduction.md b/exercises/practice/sgf-parsing/.docs/introduction.md new file mode 100644 index 0000000000..c1d051e45c --- /dev/null +++ b/exercises/practice/sgf-parsing/.docs/introduction.md @@ -0,0 +1,7 @@ +# Introduction + +SGF (Smart Game Format) is a popular, text-based file format used for storing records of board games. While it can be used for many different games like Chess, Othello, and Backgammon, it is most widely known and used for the game of Go. + +An SGF file is structured as a tree of nodes. This tree structure is particularly useful because it allows game records to branch out, meaning you can store the main sequence of moves as well as alternative variations, analyses, and commentary all in the same file. + +Each node in the tree contains a set of properties, and each property has one or more values. These properties encode everything from the players' names and ranks, to the game result, board size, and individual moves placed on the board. diff --git a/exercises/practice/sgf-parsing/.gitignore b/exercises/practice/sgf-parsing/.gitignore new file mode 100644 index 0000000000..0c88ff6ec3 --- /dev/null +++ b/exercises/practice/sgf-parsing/.gitignore @@ -0,0 +1,5 @@ +/node_modules +/bin/configlet +/bin/configlet.exe +/package-lock.json +/yarn.lock diff --git a/exercises/practice/sgf-parsing/.meta/config.json b/exercises/practice/sgf-parsing/.meta/config.json new file mode 100644 index 0000000000..26416d4677 --- /dev/null +++ b/exercises/practice/sgf-parsing/.meta/config.json @@ -0,0 +1,24 @@ +{ + "authors": [ + "matthewmorgan" + ], + "files": { + "solution": [ + "sgf-parsing.js" + ], + "test": [ + "sgf-parsing.spec.js" + ], + "example": [ + ".meta/proof.ci.js" + ] + }, + "blurb": "Parsing a Smart Game Format string.", + "source_url": "https://en.wikipedia.org/wiki/Smart_Game_Format", + "custom": { + "version.tests.compatibility": "jest-27", + "flag.tests.task-per-describe": false, + "flag.tests.may-run-long": false, + "flag.tests.includes-optional": false + } +} diff --git a/exercises/practice/sgf-parsing/.meta/proof.ci.js b/exercises/practice/sgf-parsing/.meta/proof.ci.js new file mode 100644 index 0000000000..8885132c54 --- /dev/null +++ b/exercises/practice/sgf-parsing/.meta/proof.ci.js @@ -0,0 +1,132 @@ +export function parse(input) { + if (!input.startsWith('(')) { + throw new Error('tree missing'); + } + + const parser = new SgfParser(input); + return parser.parseTree(); +} + +class SgfParser { + constructor(input) { + this.input = input; + this.pos = 0; + } + + peek() { + return this.input[this.pos]; + } + + consume() { + return this.input[this.pos++]; + } + + parseTree() { + if (this.peek() !== '(') { + throw new Error('tree missing'); + } + this.consume(); // consume '(' + + if (this.peek() === ')') { + throw new Error('tree with no nodes'); + } + + const root = this.parseNode(); + + this.consume(); // consume ')' + + return root; + } + + parseNode() { + if (this.peek() !== ';') { + throw new Error('tree missing'); + } + this.consume(); // consume ';' + + const node = { properties: {}, children: [] }; + + // Parse properties of this node + while (this.pos < this.input.length && /[A-Za-z]/.test(this.peek())) { + const { key, values } = this.parseProperty(); + node.properties[key] = values; + } + + // Parse subsequent nodes or child trees + while (this.pos < this.input.length && this.peek() !== ')') { + if (this.peek() === ';') { + // Next node in sequence becomes the single child + const child = this.parseNode(); + node.children.push(child); + break; // parseNode handles its own children recursively + } else if (this.peek() === '(') { + // Variation: a child tree + const child = this.parseTree(); + node.children.push(child); + } else { + break; + } + } + + return node; + } + + parseProperty() { + let key = ''; + + // Read key characters (must be uppercase letters) + while (this.pos < this.input.length && /[A-Za-z]/.test(this.peek())) { + key += this.consume(); + } + + if (!/^[A-Z]+$/.test(key)) { + throw new Error('property must be in uppercase'); + } + + if (this.peek() !== '[') { + throw new Error('properties without delimiter'); + } + + const values = []; + + // A property can have multiple values: KEY[val1][val2]... + while (this.peek() === '[') { + this.consume(); // consume '[' + values.push(this.parseValue()); + this.consume(); // consume ']' + } + + return { key, values }; + } + + parseValue() { + let value = ''; + + while (this.pos < this.input.length && this.peek() !== ']') { + const ch = this.consume(); + + if (ch === '\\') { + const next = this.consume(); + if (next === '\n') { + // escaped newline: remove entirely + } else if (next === '\r') { + // handle \r\n + if (this.peek() === '\n') this.consume(); + } else if (next === '\t') { + // escaped tab -> space + value += ' '; + } else { + // any other escaped char: keep the char, drop the backslash + value += next; + } + } else if (ch === '\t') { + // unescaped tab -> space + value += ' '; + } else { + value += ch; + } + } + + return value; + } +} diff --git a/exercises/practice/sgf-parsing/.meta/tests.toml b/exercises/practice/sgf-parsing/.meta/tests.toml new file mode 100644 index 0000000000..2a9d7d927b --- /dev/null +++ b/exercises/practice/sgf-parsing/.meta/tests.toml @@ -0,0 +1,84 @@ +# This is an auto-generated file. +# +# Regenerating this file via `configlet sync` will: +# - Recreate every `description` key/value pair +# - Recreate every `reimplements` key/value pair, where they exist in problem-specifications +# - Remove any `include = true` key/value pair (an omitted `include` key implies inclusion) +# - Preserve any other key/value pair +# +# As user-added comments (using the # character) will be removed when this file +# is regenerated, comments can be added via a `comment` key. + +[2668d5dc-109f-4f71-b9d5-8d06b1d6f1cd] +description = "empty input" + +[84ded10a-94df-4a30-9457-b50ccbdca813] +description = "tree with no nodes" + +[0a6311b2-c615-4fa7-800e-1b1cbb68833d] +description = "node without tree" + +[8c419ed8-28c4-49f6-8f2d-433e706110ef] +description = "node without properties" + +[8209645f-32da-48fe-8e8f-b9b562c26b49] +description = "single node tree" + +[6c995856-b919-4c75-8fd6-c2c3c31b37dc] +description = "multiple properties" + +[a771f518-ec96-48ca-83c7-f8d39975645f] +description = "properties without delimiter" + +[6c02a24e-6323-4ed5-9962-187d19e36bc8] +description = "all lowercase property" + +[8772d2b1-3c57-405a-93ac-0703b671adc1] +description = "upper and lowercase property" + +[a759b652-240e-42ec-a6d2-3a08d834b9e2] +description = "two nodes" + +[cc7c02bc-6097-42c4-ab88-a07cb1533d00] +description = "two child trees" + +[724eeda6-00db-41b1-8aa9-4d5238ca0130] +description = "multiple property values" + +[28092c06-275f-4b9f-a6be-95663e69d4db] +description = "within property values, whitespace characters such as tab are converted to spaces" + +[deaecb9d-b6df-4658-aa92-dcd70f4d472a] +description = "within property values, newlines remain as newlines" + +[8e4c970e-42d7-440e-bfef-5d7a296868ef] +description = "escaped closing bracket within property value becomes just a closing bracket" + +[cf371fa8-ba4a-45ec-82fb-38668edcb15f] +description = "escaped backslash in property value becomes just a backslash" + +[dc13ca67-fac0-4b65-b3fe-c584d6a2c523] +description = "opening bracket within property value doesn't need to be escaped" + +[a780b97e-8dbb-474e-8f7e-4031902190e8] +description = "semicolon in property value doesn't need to be escaped" + +[0b57a79e-8d89-49e5-82b6-2eaaa6b88ed7] +description = "parentheses in property value don't need to be escaped" + +[c72a33af-9e04-4cc5-9890-1b92262813ac] +description = "escaped tab in property value is converted to space" + +[3a1023d2-7484-4498-8d73-3666bb386e81] +description = "escaped newline in property value is converted to nothing at all" + +[25abf1a4-5205-46f1-8c72-53273b94d009] +description = "escaped t and n in property value are just letters, not whitespace" + +[08e4b8ba-bb07-4431-a3d9-b1f4cdea6dab] +description = "mixing various kinds of whitespace and escaped characters in property value" +reimplements = "11c36323-93fc-495d-bb23-c88ee5844b8c" + +[11c36323-93fc-495d-bb23-c88ee5844b8c] +description = "escaped property" +include = false diff --git a/exercises/practice/sgf-parsing/.npmrc b/exercises/practice/sgf-parsing/.npmrc new file mode 100644 index 0000000000..d26df800bb --- /dev/null +++ b/exercises/practice/sgf-parsing/.npmrc @@ -0,0 +1 @@ +audit=false diff --git a/exercises/practice/sgf-parsing/LICENSE b/exercises/practice/sgf-parsing/LICENSE new file mode 100644 index 0000000000..90e73be03b --- /dev/null +++ b/exercises/practice/sgf-parsing/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Exercism + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/exercises/practice/sgf-parsing/babel.config.js b/exercises/practice/sgf-parsing/babel.config.js new file mode 100644 index 0000000000..a638497df1 --- /dev/null +++ b/exercises/practice/sgf-parsing/babel.config.js @@ -0,0 +1,4 @@ +module.exports = { + presets: [['@exercism/babel-preset-javascript', { corejs: '3.40' }]], + plugins: [], +}; diff --git a/exercises/practice/sgf-parsing/eslint.config.mjs b/exercises/practice/sgf-parsing/eslint.config.mjs new file mode 100644 index 0000000000..ca517111ed --- /dev/null +++ b/exercises/practice/sgf-parsing/eslint.config.mjs @@ -0,0 +1,45 @@ +// @ts-check + +import config from '@exercism/eslint-config-javascript'; +import maintainersConfig from '@exercism/eslint-config-javascript/maintainers.mjs'; + +import globals from 'globals'; + +export default [ + ...config, + ...maintainersConfig, + { + files: maintainersConfig[1].files, + rules: { + 'jest/expect-expect': ['warn', { assertFunctionNames: ['expect*'] }], + }, + }, + { + files: ['scripts/**/*.mjs'], + languageOptions: { + globals: { + ...globals.node, + }, + }, + }, + // <> + { + ignores: [ + // # Protected or generated + '/.appends/**/*', + '/.github/**/*', + '/.vscode/**/*', + + // # Binaries + '/bin/*', + + // # Configuration + '/config', + '/babel.config.js', + + // # Typings + '/exercises/**/global.d.ts', + '/exercises/**/env.d.ts', + ], + }, +]; diff --git a/exercises/practice/sgf-parsing/example.js b/exercises/practice/sgf-parsing/example.js new file mode 100644 index 0000000000..8885132c54 --- /dev/null +++ b/exercises/practice/sgf-parsing/example.js @@ -0,0 +1,132 @@ +export function parse(input) { + if (!input.startsWith('(')) { + throw new Error('tree missing'); + } + + const parser = new SgfParser(input); + return parser.parseTree(); +} + +class SgfParser { + constructor(input) { + this.input = input; + this.pos = 0; + } + + peek() { + return this.input[this.pos]; + } + + consume() { + return this.input[this.pos++]; + } + + parseTree() { + if (this.peek() !== '(') { + throw new Error('tree missing'); + } + this.consume(); // consume '(' + + if (this.peek() === ')') { + throw new Error('tree with no nodes'); + } + + const root = this.parseNode(); + + this.consume(); // consume ')' + + return root; + } + + parseNode() { + if (this.peek() !== ';') { + throw new Error('tree missing'); + } + this.consume(); // consume ';' + + const node = { properties: {}, children: [] }; + + // Parse properties of this node + while (this.pos < this.input.length && /[A-Za-z]/.test(this.peek())) { + const { key, values } = this.parseProperty(); + node.properties[key] = values; + } + + // Parse subsequent nodes or child trees + while (this.pos < this.input.length && this.peek() !== ')') { + if (this.peek() === ';') { + // Next node in sequence becomes the single child + const child = this.parseNode(); + node.children.push(child); + break; // parseNode handles its own children recursively + } else if (this.peek() === '(') { + // Variation: a child tree + const child = this.parseTree(); + node.children.push(child); + } else { + break; + } + } + + return node; + } + + parseProperty() { + let key = ''; + + // Read key characters (must be uppercase letters) + while (this.pos < this.input.length && /[A-Za-z]/.test(this.peek())) { + key += this.consume(); + } + + if (!/^[A-Z]+$/.test(key)) { + throw new Error('property must be in uppercase'); + } + + if (this.peek() !== '[') { + throw new Error('properties without delimiter'); + } + + const values = []; + + // A property can have multiple values: KEY[val1][val2]... + while (this.peek() === '[') { + this.consume(); // consume '[' + values.push(this.parseValue()); + this.consume(); // consume ']' + } + + return { key, values }; + } + + parseValue() { + let value = ''; + + while (this.pos < this.input.length && this.peek() !== ']') { + const ch = this.consume(); + + if (ch === '\\') { + const next = this.consume(); + if (next === '\n') { + // escaped newline: remove entirely + } else if (next === '\r') { + // handle \r\n + if (this.peek() === '\n') this.consume(); + } else if (next === '\t') { + // escaped tab -> space + value += ' '; + } else { + // any other escaped char: keep the char, drop the backslash + value += next; + } + } else if (ch === '\t') { + // unescaped tab -> space + value += ' '; + } else { + value += ch; + } + } + + return value; + } +} diff --git a/exercises/practice/sgf-parsing/jest.config.js b/exercises/practice/sgf-parsing/jest.config.js new file mode 100644 index 0000000000..ec8e908127 --- /dev/null +++ b/exercises/practice/sgf-parsing/jest.config.js @@ -0,0 +1,22 @@ +module.exports = { + verbose: true, + projects: [''], + testMatch: [ + '**/__tests__/**/*.[jt]s?(x)', + '**/test/**/*.[jt]s?(x)', + '**/?(*.)+(spec|test).[jt]s?(x)', + ], + testPathIgnorePatterns: [ + '/(?:production_)?node_modules/', + '.d.ts$', + '/test/fixtures', + '/test/helpers', + '__mocks__', + ], + transform: { + '^.+\\.[jt]sx?$': 'babel-jest', + }, + moduleNameMapper: { + '^(\\.\\/.+)\\.js$': '$1', + }, +}; diff --git a/exercises/practice/sgf-parsing/package.json b/exercises/practice/sgf-parsing/package.json new file mode 100644 index 0000000000..61f58364f4 --- /dev/null +++ b/exercises/practice/sgf-parsing/package.json @@ -0,0 +1,38 @@ +{ + "name": "@exercism/javascript-practice-sgf-parsing", + "description": "Exercism practice exercise on sgf-parsing", + "author": "Katrina Owen", + "contributors": [ + "Derk-Jan Karrenbeld (https://derk-jan.com)", + "Tejas Bubane (https://tejasbubane.github.io/)" + ], + "private": true, + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/exercism/javascript", + "directory": "exercises/practice/sgf-parsing" + }, + "devDependencies": { + "@exercism/babel-preset-javascript": "^0.5.1", + "@exercism/eslint-config-javascript": "^0.8.1", + "@jest/globals": "^30.3.0", + "@types/node": "^24.3.0", + "@types/shelljs": "^0.8.17", + "babel-jest": "^30.3.0", + "core-js": "~3.46.0", + "diff": "^8.0.3", + "eslint": "^9.28.0", + "expect": "^29.7.0", + "globals": "^16.3.0", + "jest": "^30.2.0" + }, + "dependencies": {}, + "scripts": { + "lint": "corepack pnpm eslint .", + "test": "corepack pnpm jest", + "watch": "corepack pnpm jest --watch", + "format": "corepack pnpm prettier -w ." + }, + "packageManager": "pnpm@9.15.2" +} diff --git a/exercises/practice/sgf-parsing/sgf-parsing.js b/exercises/practice/sgf-parsing/sgf-parsing.js new file mode 100644 index 0000000000..c6bb9e6937 --- /dev/null +++ b/exercises/practice/sgf-parsing/sgf-parsing.js @@ -0,0 +1,8 @@ +// +// This is only a SKELETON file for the 'sgf-parsing' exercise. It's been provided as a +// convenience to get you started writing code faster. +// + +export const parse = () => { + throw new Error('Remove this line and implement the function'); +}; diff --git a/exercises/practice/sgf-parsing/sgf-parsing.spec.js b/exercises/practice/sgf-parsing/sgf-parsing.spec.js new file mode 100644 index 0000000000..324bd9661c --- /dev/null +++ b/exercises/practice/sgf-parsing/sgf-parsing.spec.js @@ -0,0 +1,158 @@ +import { describe, expect, test, xtest } from '@jest/globals'; +import { parse } from './sgf-parsing'; + +describe('Sgf Parsing', () => { + test('empty input', () => { + expect(() => parse('')).toThrow('tree missing'); + }); + + xtest('tree with no nodes', () => { + expect(() => parse('()')).toThrow('tree with no nodes'); + }); + + xtest('node without tree', () => { + expect(() => parse(';')).toThrow('tree missing'); + }); + + xtest('node without properties', () => { + expect(parse('(;)')).toEqual({ properties: {}, children: [] }); + }); + + xtest('single node tree', () => { + expect(parse('(;A[B])')).toEqual({ + properties: { A: ['B'] }, + children: [], + }); + }); + + xtest('multiple properties', () => { + expect(parse('(;A[b]C[d])')).toEqual({ + properties: { A: ['b'], C: ['d'] }, + children: [], + }); + }); + + xtest('properties without delimiter', () => { + expect(() => parse('(;A)')).toThrow('properties without delimiter'); + }); + + xtest('all lowercase property', () => { + expect(() => parse('(;a[b])')).toThrow('property must be in uppercase'); + }); + + xtest('upper and lowercase property', () => { + expect(() => parse('(;Aa[b])')).toThrow('property must be in uppercase'); + }); + + xtest('two nodes', () => { + expect(parse('(;A[B];B[C])')).toEqual({ + properties: { A: ['B'] }, + children: [ + { + properties: { B: ['C'] }, + children: [], + }, + ], + }); + }); + + xtest('two child trees', () => { + expect(parse('(;A[B](;B[C])(;C[D]))')).toEqual({ + properties: { A: ['B'] }, + children: [ + { + properties: { B: ['C'] }, + children: [], + }, + { + properties: { C: ['D'] }, + children: [], + }, + ], + }); + }); + + xtest('multiple property values', () => { + expect(parse('(;A[b][c][d])')).toEqual({ + properties: { A: ['b', 'c', 'd'] }, + children: [], + }); + }); + + xtest('within property values, whitespace characters such as tab are converted to spaces', () => { + expect(parse('(;A[hello\t\tworld])')).toEqual({ + properties: { A: ['hello world'] }, + children: [], + }); + }); + + xtest('within property values, newlines remain as newlines', () => { + expect(parse('(;A[hello\n\nworld])')).toEqual({ + properties: { A: ['hello\n\nworld'] }, + children: [], + }); + }); + + xtest('escaped closing bracket within property value becomes just a closing bracket', () => { + expect(parse('(;A[\\]])')).toEqual({ + properties: { A: [']'] }, + children: [], + }); + }); + + xtest('escaped backslash in property value becomes just a backslash', () => { + expect(parse('(;A[\\\\])')).toEqual({ + properties: { A: ['\\'] }, + children: [], + }); + }); + + xtest("opening bracket within property value doesn't need to be escaped", () => { + expect(parse('(;A[b[c])')).toEqual({ + properties: { A: ['b[c'] }, + children: [], + }); + }); + + xtest("semicolon in property value doesn't need to be escaped", () => { + expect(parse('(;A[a;b])')).toEqual({ + properties: { A: ['a;b'] }, + children: [], + }); + }); + + xtest("parentheses in property value don't need to be escaped", () => { + expect(parse('(;A[a(b])')).toEqual({ + properties: { A: ['a(b'] }, + children: [], + }); + }); + + xtest('escaped tab in property value is converted to space', () => { + expect(parse('(;A[\\\t])')).toEqual({ + properties: { A: [' '] }, + children: [], + }); + }); + + xtest('escaped newline in property value is converted to nothing at all', () => { + expect(parse('(;A[\\\n])')).toEqual({ + properties: { A: [''] }, + children: [], + }); + }); + + xtest('escaped t and n in property value are just letters, not whitespace', () => { + expect(parse('(;A[\\t][\\n])')).toEqual({ + properties: { A: ['t', 'n'] }, + children: [], + }); + }); + + xtest('mixing various kinds of whitespace and escaped characters in property value', () => { + expect(parse('(;A[\\]b\nc\\\nd\t\te\\\\ \\\n\\]])')).toEqual({ + properties: { A: [']b\ncd e\\ ]'] }, + children: [], + }); + }); +});