diff --git a/README.md b/README.md index 3717278..7270fdd 100644 --- a/README.md +++ b/README.md @@ -83,7 +83,7 @@ Two are provided: a [`MemDB`](./docs/db.md#memdb) who is basically an "in-memory - reading from a file - maintaining the files when changes are brought -The `FileDB` uses a human-accessible (using [json5](https://json5.org/) for custom types) and based on `\t` indentation file format only proper for this usage. +The `FileDB` uses a human-accessible js-like format for custom types) and based on `\t` indentation file format only proper for this usage. Having the translators managing translations in the UI while the devs have to access the file to add/remove keys, change their zone, ... and all this to go through git commits (so, to have local changes that will be integrated in the project after push/merge) can be done with `FileDB` - for this, just interface a `PUT` to a call on `InteractiveServer::modify` (while that server has a `FileDB` as a source) then the new file will be saved soon with the modified values. diff --git a/docs/db.md b/docs/db.md index 113994e..d77c76f 100644 --- a/docs/db.md +++ b/docs/db.md @@ -142,7 +142,7 @@ For each key, the callback calls will be `onKey - onText* - endKey` for each key The serialization file-format is specific for regexp-ability _and_ human interactions; grouping is done by indentation (made with tabulations - `\t`). -`KeyInfos` and `TextInfos` are stored in [`json5`](https://json5.org/) format +`KeyInfos` and `TextInfos` are stored in human-accessible js-like format ##### 0-tabs @@ -153,7 +153,7 @@ A line beginning with no tabs is a key specification ``` ``` -[text-key][{ SomeKeyInfos: 'json5 format' }]:[zone] +[text-key][{ SomeKeyInfos: 'jju format' }]:[zone] ``` > Note: the zone can and will often be `""` @@ -167,7 +167,7 @@ A line beginning with one tab is a locale specification for the key "en cours" ``` ``` - [locale][{ SomeTextInfos: 'json5 format' }]:Some fancy translation + [locale][{ SomeTextInfos: 'value' }]:Some fancy translation ``` ##### 2-tabs diff --git a/docs/umd.md b/docs/umd.md index aae34d2..afb8780 100644 --- a/docs/umd.md +++ b/docs/umd.md @@ -4,8 +4,6 @@ Static websites will have to expose static translations files, import a library For now, the file `lib/omni18n.js` is the file to be copied and referred to in the html script tag. -Note: until now (1.1.8), the compressed library is 40k - it might be an overkill as the library is designed for fullstack optimization. The big thing that can still be profitable even in static websites is the [interpolation engine](./interpolation.md), that it translates on-the-fly (without reloading the page) and that it works out of the box. - ## Script UMD (static websites) use no zones (only the main one). Therefore, a global `T` variable is the only [Translator](./translator.md). An event is raised when it is loaded/changed diff --git a/package-lock.json b/package-lock.json index 6cb593e..75d9d22 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,9 +8,6 @@ "name": "omni18n", "version": "1.1.11", "license": "ISC", - "dependencies": { - "json5": "^2.2.3" - }, "bin": { "extractLocales": "bin/extractLocales.js" }, @@ -641,262 +638,6 @@ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz", - "integrity": "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.12.tgz", - "integrity": "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.12.tgz", - "integrity": "sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.12.tgz", - "integrity": "sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.12.tgz", - "integrity": "sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.12.tgz", - "integrity": "sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.12.tgz", - "integrity": "sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.12.tgz", - "integrity": "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.12.tgz", - "integrity": "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.12.tgz", - "integrity": "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.12.tgz", - "integrity": "sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.12.tgz", - "integrity": "sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==", - "cpu": [ - "loong64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.12.tgz", - "integrity": "sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==", - "cpu": [ - "mips64el" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.12.tgz", - "integrity": "sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.12.tgz", - "integrity": "sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==", - "cpu": [ - "riscv64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.12.tgz", - "integrity": "sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==", - "cpu": [ - "s390x" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, "node_modules/@esbuild/linux-x64": { "version": "0.19.12", "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.12.tgz", @@ -913,102 +654,6 @@ "node": ">=12" } }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.12.tgz", - "integrity": "sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.12.tgz", - "integrity": "sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.12.tgz", - "integrity": "sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.12.tgz", - "integrity": "sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.12.tgz", - "integrity": "sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz", - "integrity": "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", @@ -3312,20 +2957,6 @@ "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", "dev": true }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -4488,6 +4119,7 @@ "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, "bin": { "json5": "lib/cli.js" }, diff --git a/package.json b/package.json index e97b349..9dcd66b 100644 --- a/package.json +++ b/package.json @@ -73,7 +73,5 @@ "tsx": "^4.7.3", "typescript": "^5.4.5" }, - "dependencies": { - "json5": "^2.2.3" - } + "dependencies": {} } diff --git a/rollup.config.js b/rollup.config.js index 0af6494..2b84020 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -19,7 +19,6 @@ export default [ banner: '/** https://www.npmjs.com/package/omni18n */', dir: 'lib' }, - external: ['json5'], plugins: [ resolve(), commonjs(), @@ -60,7 +59,6 @@ export default [ sourcemap: true, format: 'esm' }, - external: ['json5'], plugins: [ resolve(), commonjs(), diff --git a/src/cgpt-js.ts b/src/cgpt-js.ts new file mode 100644 index 0000000..9fe2c46 --- /dev/null +++ b/src/cgpt-js.ts @@ -0,0 +1,344 @@ +/** + * "Library" for stringifying and parsing human-readable JSON + * Made in 1/2h with chatGPT + */ +type JSONValue = string | number | boolean | null | JSONObject | JSONArray +interface JSONObject { + [key: string]: JSONValue +} +interface JSONArray extends Array {} + +/** + * Stringify a human-js value + * @param {JSONValue} value + * @param {number} maxLength Length of an object/array after which it is broken in several lines + * @param {number | '\t'} indentation Number of spaces per indentation level / tab + */ +export function stringify( + value: JSONValue, + maxLength: number = 80, + indentation: number | '\t' = '\t' +): string { + function innerStringify(value: JSONValue, currentIndent: number, currentLength: number): string { + if (typeof value === 'object' && value !== null) { + if (Array.isArray(value)) { + return stringifyArray(value, currentIndent, currentLength) + } else { + return stringifyObject(value, currentIndent, currentLength) + } + } else if (typeof value === 'string') { + return '"' + value.replace(/"/g, '\\"') + '"' + } else { + return String(value) + } + } + + function stringifyKey(key: string): string { + if (/^[a-zA-Z_]\w*$/.test(key)) { + return key + } else { + return '"' + key.replace(/"/g, '\\"') + '"' + } + } + + function stringifyObject(obj: JSONObject, currentIndent: number, currentLength: number): string { + const entries = Object.entries(obj) + if (entries.length === 0) { + return '{}' + } + let result = '{' + for (const [key, val] of entries) { + const strValue = innerStringify( + val, + currentIndent + getIndentSize(indentation), + currentLength + key.length + 4 + ) + const newLength = currentLength + key.length + 4 + strValue.length + if (newLength <= maxLength) { + result += stringifyKey(key) + ': ' + strValue + ', ' + } else { + return stringifyObjectMultiline(obj, currentIndent) + } + } + return result.slice(0, -2) + '}' + } + + function stringifyObjectMultiline(obj: JSONObject, currentIndent: number): string { + const entries = Object.entries(obj) + if (entries.length === 0) { + return '{}' + } + const indent = getIndentString(currentIndent) + let result = '{\n' + for (const [key, val] of entries) { + const strValue = innerStringify(val, currentIndent + getIndentSize(indentation), 0) + result += + indent + + getIndentString(getIndentSize(indentation)) + + stringifyKey(key) + + ': ' + + strValue + + ',\n' + } + result = result.slice(0, -2) + '\n' + getIndentString(currentIndent) + '}' + return result + } + + function stringifyArray(arr: JSONArray, currentIndent: number, currentLength: number): string { + if (arr.length === 0) { + return '[]' + } + let result = '[' + for (const val of arr) { + const strValue = innerStringify( + val, + currentIndent + getIndentSize(indentation), + currentLength + 2 + ) + const newLength = currentLength + 2 + strValue.length + if (newLength <= maxLength) { + result += strValue + ', ' + } else { + return stringifyArrayMultiline(arr, currentIndent) + } + } + return result.slice(0, -2) + ']' + } + + function stringifyArrayMultiline(arr: JSONArray, currentIndent: number): string { + if (arr.length === 0) { + return '[]' + } + const indent = getIndentString(currentIndent) + let result = '[\n' + for (const val of arr) { + const strValue = innerStringify(val, currentIndent + getIndentSize(indentation), 0) + result += indent + getIndentString(getIndentSize(indentation)) + strValue + ',\n' + } + result = result.slice(0, -2) + '\n' + getIndentString(currentIndent) + ']' + return result + } + + function getIndentString(indentSize: number): string { + return indentation === '\t' + ? '\t'.repeat(Math.max(indentSize / 4, 0)) + : getIndentString(Math.max(indentSize, 0)) + } + + function getIndentSize(indentation: number | string): number { + return typeof indentation === 'number' ? Math.max(indentation, 0) : 4 + } + + return innerStringify(value, 0, 0) +} + +export function parse(jsonString: string): JSONValue { + function syntaxErrorDtl() { + return `\nat line ${lineNumber} column ${columnNumber}: ` + jsonString.slice(index, index + 20) + } + let index = 0 + let lineNumber = 1 + let columnNumber = 1 + + function skipWhitespace() { + while (index < jsonString.length && /\s/.test(jsonString[index])) { + if (jsonString[index] === '\n') { + lineNumber++ + columnNumber = 1 + } else { + columnNumber++ + } + index++ + } + } + + function skipComment() { + if (jsonString[index] === '/' && jsonString[index + 1] === '/') { + // Single-line comment, skip until the end of the line + while (index < jsonString.length && jsonString[index] !== '\n') { + index++ + } + lineNumber++ + columnNumber = 1 + } else if (jsonString[index] === '/' && jsonString[index + 1] === '*') { + // Multi-line comment, skip until '*/' is encountered + index += 2 // Skip the opening '/*' + while ( + index < jsonString.length && + !(jsonString[index] === '*' && jsonString[index + 1] === '/') + ) { + if (jsonString[index] === '\n') { + lineNumber++ + columnNumber = 1 + } else { + columnNumber++ + } + index++ + } + if (index >= jsonString.length) { + throw new SyntaxError( + `Unexpected end of input while parsing multiline comment ${syntaxErrorDtl()}` + ) + } + index += 2 // Skip the closing '*/' + columnNumber += 2 + } + } + + function parseString(): string { + const quote = jsonString[index] + if (quote !== '"' && quote !== "'") { + throw new SyntaxError(`Unexpected character '${quote}' ${syntaxErrorDtl()}`) + } + let value = '' + index++ // Skip opening quote + columnNumber++ + while (index < jsonString.length && jsonString[index] !== quote) { + if (jsonString[index] === '\\' && jsonString[index + 1] === quote) { + // Handle escaped quotes + value += quote + index += 2 + } else if (jsonString[index] === '\n') { + // Multiline string + value += jsonString[index++] + lineNumber++ + columnNumber = 1 + } else { + value += jsonString[index++] + columnNumber++ + } + } + if (index >= jsonString.length) { + throw new SyntaxError(`Unexpected end of input while parsing string ${syntaxErrorDtl()}`) + } + index++ // Skip closing quote + columnNumber++ + return value + } + + function parseKey(): string { + skipWhitespace() + const quote = jsonString[index] + if (quote === '"' || quote === "'") { + // If the key starts with a quote, parse it as a string + return parseString() + } else { + // If the key does not start with a quote, parse it as an unquoted key + let key = '' + // Check if the key starts with a valid character + if (/[a-zA-Z_$]/.test(jsonString[index])) { + key += jsonString[index++] + columnNumber++ + } else { + throw new SyntaxError(`Unexpected character ${syntaxErrorDtl()}`) + } + // Continue adding valid characters to the key + while (index < jsonString.length && /[a-zA-Z0-9_$]/.test(jsonString[index])) { + key += jsonString[index++] + columnNumber++ + } + return key + } + } + + function parseValue(): JSONValue { + skipWhitespace() + if (jsonString[index] === '{') { + return parseObject() + } else if (jsonString[index] === '[') { + return parseArray() + } else if (jsonString[index] === '"' || jsonString[index] === "'") { + return parseString() + } else { + return parseLiteral() + } + } + + function parseLiteral(): JSONValue { + let literal = '' + while (index < jsonString.length && /\w/.test(jsonString[index])) { + literal += jsonString[index++] + columnNumber++ + } + if (literal === 'true') { + return true + } else if (literal === 'false') { + return false + } else if (literal === 'null') { + return null + } else if (!isNaN(parseFloat(literal))) { + return parseFloat(literal) + } + throw new SyntaxError(`Unexpected character ${syntaxErrorDtl()}`) + } + + function parseArray(): JSONArray { + let arr: JSONArray = [] + index++ // Skip opening bracket + columnNumber++ + while (jsonString[index] !== ']') { + arr.push(parseValue()) + skipWhitespace() + if (jsonString[index] === ',') { + index++ // Skip comma + columnNumber++ + } else if (jsonString[index] !== ']') { + throw new SyntaxError(`Unexpected character ${syntaxErrorDtl()}`) + } + skipWhitespace() + } + if (index >= jsonString.length) { + throw new SyntaxError(`Unexpected end of input while parsing array ${syntaxErrorDtl()}`) + } + index++ // Skip closing bracket + columnNumber++ + return arr + } + + function parseObject(): JSONObject { + let obj: JSONObject = {} + index++ // Skip opening brace + columnNumber++ + skipAllIgnored() + while (jsonString[index] !== '}') { + const key = parseKey() + skipAllIgnored() + if (jsonString[index++] !== ':') { + throw new SyntaxError(`Expected ':' ${syntaxErrorDtl()}`) + } + skipAllIgnored() + const value = parseValue() + obj[key] = value + skipAllIgnored() + if (jsonString[index] === ',') { + index++ // Skip comma + columnNumber++ + skipAllIgnored() + } else if (jsonString[index] !== '}') { + throw new SyntaxError(`Unexpected character ${syntaxErrorDtl()}`) + } + } + if (index >= jsonString.length) { + throw new SyntaxError(`Unexpected end of input while parsing object ${syntaxErrorDtl()}`) + } + index++ // Skip closing brace + columnNumber++ + return obj + } + + function skipAllIgnored(): void { + while (index < jsonString.length) { + skipWhitespace() + if ( + jsonString[index] === '/' && + (jsonString[index + 1] === '/' || jsonString[index + 1] === '*') + ) { + skipComment() + } else { + break + } + } + } + + return parseValue() +} diff --git a/src/client.ts b/src/client.ts index 9998e97..681c471 100644 --- a/src/client.ts +++ b/src/client.ts @@ -1,6 +1,7 @@ export * from './types' export * from './client/index' export * from './flags' +export * from './cgpt-js' declare global { interface Set { diff --git a/src/client/helpers.ts b/src/client/helpers.ts index 1f8f69b..96af523 100644 --- a/src/client/helpers.ts +++ b/src/client/helpers.ts @@ -1,6 +1,5 @@ -import json5 from 'json5' -const { parse } = json5 -import { type CondensedDictionary, type TextKey, type Translation, type Zone } from '../types' +import { parse } from '../cgpt-js' +import { type CondensedDictionary, type TextKey, type Translation } from '../types' import { ClientDictionary, TContext, @@ -119,7 +118,7 @@ export function translator(context: TContext): Translator { export function parseInternals(dictionary: ClientDictionary | string) { if (!dictionary) return {} if (typeof dictionary === 'string') return parse(dictionary) - const result = text in dictionary ? parse(dictionary[text]!) : {} + const result = text in dictionary ? parse(dictionary[text]!) : {} for (const key in dictionary) if (key !== '') result[key] = parseInternals(dictionary[key]) return result } diff --git a/src/db/serialization.ts b/src/db/serialization.ts index bfda856..d18cbcf 100644 --- a/src/db/serialization.ts +++ b/src/db/serialization.ts @@ -1,5 +1,4 @@ -import json5 from 'json5' -const { parse, stringify } = json5 +import { parse, stringify } from '../cgpt-js' import { Locale, TextKey, Translation, Zone } from 'src/types' import { MemDBDictionary, MemDBDictionaryEntry } from './memDb' @@ -117,7 +116,7 @@ const serialization = { key, localeFetch[1] as Locale, localeFetch[3] && localeFetch[3].replace(/\u0000\t\t/g, '\n'), - localeFetch[2] ? parse(localeFetch[2].replace(/\u0000/g, '\n')) : undefined + localeFetch[2] ? parse(localeFetch[2].replace(/\u0000/g, '\n')) : undefined ) } endKey?.(key) diff --git a/src/umd/client.ts b/src/umd/client.ts index 5b13d69..d194ab9 100644 --- a/src/umd/client.ts +++ b/src/umd/client.ts @@ -9,8 +9,7 @@ import { reports, TContext } from '../client' -import json5 from 'json5' -const { parse } = json5 +import { parse } from '../cgpt-js' declare global { var T: Translator @@ -171,4 +170,4 @@ try { let textContent = document.currentScript?.textContent if (textContent) hcArgs = parse('[' + textContent + ']') } catch (e) {} -if (hcArgs) init.apply(null, hcArgs) +if (hcArgs) init.apply(null, hcArgs) diff --git a/src/umd/extractLocales.ts b/src/umd/extractLocales.ts index 34e31b8..e71d306 100644 --- a/src/umd/extractLocales.ts +++ b/src/umd/extractLocales.ts @@ -2,8 +2,7 @@ import { mkdir, writeFile, watch } from 'fs/promises' import { dirname, join, basename } from 'path' import commandLineArgs from 'command-line-args' import { FileDB, I18nServer } from 'src' -import json5 from 'json5' -const { stringify } = json5 +import { stringify } from '../cgpt-js' const options = commandLineArgs( [ diff --git a/test/specifics.test.ts b/test/specifics.test.ts index d4e3a25..7876b2d 100644 --- a/test/specifics.test.ts +++ b/test/specifics.test.ts @@ -11,7 +11,9 @@ import { reports, localeFlags, flagCodeExceptions, - FileDB + FileDB, + parse, + stringify } from '../src/index' import { localStack } from './utils' @@ -159,9 +161,9 @@ test.multiline: await server.modify('fld.name', 'hu', 'Név') await db.save() const content = await readFile('./db.test', 'utf16le') - expect(content).toBe(`fld.name{note:'the name of the person'}: + expect(content).toBe(`fld.name{note: "the name of the person"}: en:Name - fr{obvious:true}:Nom + fr{obvious: true}:Nom hu:Név test.multiline: :Line 1 @@ -170,3 +172,80 @@ test.multiline: await unlink('./db.test') }) }) + +describe('cgpt-js', () => { + describe('stringify function', () => { + test('should stringify a simple object', () => { + const obj = { name: 'John', age: 30 } + const expected = '{name: "John", age: 30}' + expect(stringify(obj)).toBe(expected) + }) + + test('should stringify an object with indentation', () => { + const obj = { name: 'John', 'age-': 30 } + const expected = `{ +\tname: "John", +\t"age-": 30 +}` + expect(stringify(obj, 10, '\t')).toBe(expected) + }) + + test('complex stringify', () => { + const obj = { + name: 'John', + age: 30, + isAdmin: true, + hobbies: ['reading', 'coding', 'swimming'], + address: { + city: 'New\nYork', + country: 'USA' + } + } + const expected = `{ + name: "John", + age: 30, + isAdmin: true, + hobbies: ["reading", "coding", "swimming"], + address: {city: "New +York", country: "USA"} +}` + expect(stringify(obj, 40, '\t')).toBe(expected) + }) + }) + describe('parse function', () => { + test('should parse a simple JSON string', () => { + const jsonString = '{"name":"John","age":30}' + const expected = { name: 'John', age: 30 } + expect(parse(jsonString)).toEqual(expected) + }) + + test('should throw SyntaxError when parsing invalid JSON', () => { + const invalidJsonString = '{"name":"John","age":}' + expect(() => parse(invalidJsonString)).toThrow(SyntaxError) + }) + + test('complex parse', () => { + const jsonString = `{ + // This is a comment + name: "John",// Another comment + age: 30/* multi +line*/, + isAdmin: true, + hobbies: ["reading", "coding", "swimming"], + address: {city: "New +York", country: "USA"/* -- */} +}` + const expected = { + name: 'John', + age: 30, + isAdmin: true, + hobbies: ['reading', 'coding', 'swimming'], + address: { + city: 'New\nYork', + country: 'USA' + } + } + expect(parse(jsonString)).toEqual(expected) + }) + }) +})