├── .eslintrc.json ├── .github ├── assignment.yml └── workflows │ └── tests.yml ├── .gitignore ├── .mocharc.json ├── .npmignore ├── .vscode ├── launch.json ├── settings.json └── tasks.json ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── SECURITY.md ├── build ├── pipeline.yml └── remove-sourcemap-refs.js ├── package-lock.json ├── package.json └── src ├── impl ├── edit.ts ├── format.ts ├── parser.ts ├── scanner.ts └── string-intern.ts ├── main.ts ├── test ├── edit.test.ts ├── format.test.ts ├── json.test.ts └── string-intern.test.ts ├── tsconfig.esm.json └── tsconfig.json /.eslintrc.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "root": true, 4 | "parser": "@typescript-eslint/parser", 5 | "parserOptions": { 6 | "ecmaVersion": 6, 7 | "sourceType": "module" 8 | }, 9 | "plugins": [ 10 | "@typescript-eslint" 11 | ], 12 | "rules": { 13 | "@typescript-eslint/naming-convention": [ 14 | "warn", 15 | { 16 | "selector": "typeLike", 17 | "format": [ 18 | "PascalCase" 19 | ] 20 | } 21 | ], 22 | "@typescript-eslint/semi": "warn", 23 | "curly": "warn", 24 | "eqeqeq": "warn", 25 | "no-throw-literal": "warn", 26 | "semi": "off", 27 | "no-unused-expressions": "warn", 28 | "no-duplicate-imports": "warn", 29 | "new-parens": "warn" 30 | } 31 | } -------------------------------------------------------------------------------- /.github/assignment.yml: -------------------------------------------------------------------------------- 1 | { 2 | perform: true, 3 | assignees: [ aeschli ] 4 | } 5 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | on: [push] 2 | 3 | name: Tests 4 | 5 | jobs: 6 | build: 7 | strategy: 8 | matrix: 9 | os: [macos-latest, ubuntu-latest, windows-latest] 10 | runs-on: ${{ matrix.os }} 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v3 14 | - name: Install Node.js 15 | uses: actions/setup-node@v3 16 | with: 17 | node-version: 18.x 18 | - name: Install root project dependencies 19 | run: npm install 20 | - name: Build and run tests 21 | run: npm test -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | lib/ 2 | node_modules/ 3 | yarn.lock -------------------------------------------------------------------------------- /.mocharc.json: -------------------------------------------------------------------------------- 1 | { 2 | "ui": "tdd", 3 | "color": true 4 | } -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .vscode/ 2 | build/ 3 | .github/ 4 | lib/**/test/ 5 | lib/**/*.map 6 | lib/*/*/*.d.ts 7 | src/ 8 | test/ 9 | .travis.yml 10 | .eslintrc 11 | .gitignore 12 | .mocharc.json 13 | .eslintrc.json 14 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Unit Tests", 6 | "type": "node", 7 | "request": "launch", 8 | "program": "${workspaceRoot}/node_modules/mocha/bin/_mocha", 9 | "stopOnEntry": false, 10 | "args": [ 11 | "./lib/umd/test", 12 | "--timeout", 13 | "999999", 14 | "--colors" 15 | ], 16 | "cwd": "${workspaceRoot}", 17 | "runtimeExecutable": null, 18 | "runtimeArgs": [], 19 | "env": {}, 20 | "sourceMaps": true, 21 | "outFiles": ["${workspaceRoot}/lib/umd/**/*.js"], 22 | "preLaunchTask": "npm: watch" 23 | } 24 | ] 25 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "git.branchProtection": [ 3 | "main" 4 | ], 5 | "git.branchProtectionPrompt": "alwaysCommitToNewBranch", 6 | "git.branchRandomName.enable": true, 7 | "githubPullRequests.assignCreated": "${user}", 8 | "githubPullRequests.defaultMergeMethod": "squash" 9 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // See https://go.microsoft.com/fwlink/?LinkId=733558 2 | // for the documentation about the tasks.json format 3 | { 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "type": "npm", 8 | "script": "watch", 9 | "problemMatcher": "$tsc-watch", 10 | "isBackground": true, 11 | "presentation": { 12 | "reveal": "never" 13 | }, 14 | "group": { 15 | "kind": "build", 16 | "isDefault": true 17 | } 18 | } 19 | ] 20 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 3.3.0 2022-06-24 2 | ================= 3 | - `JSONVisitor.onObjectBegin` and `JSONVisitor.onArrayBegin` can now return `false` to instruct the visitor that no children should be visited. 4 | 5 | 6 | 3.2.0 2022-08-30 7 | ================= 8 | - update the version of the bundled Javascript files to `es2020`. 9 | - include all `const enum` values in the bundled JavaScript files (`ScanError`, `SyntaxKind`, `ParseErrorCode`). 10 | 11 | 3.1.0 2022-07-07 12 | ================== 13 | * added new API `FormattingOptions.keepLines` : It leaves the initial line positions in the formatting. 14 | 15 | 3.0.0 2020-11-13 16 | ================== 17 | * fixed API spec for `parseTree`. Can return `undefine` for empty input. 18 | * added new API `FormattingOptions.insertFinalNewline`. 19 | 20 | 21 | 2.3.0 2020-07-03 22 | ================== 23 | * new API `ModificationOptions.isArrayInsertion`: If `JSONPath` refers to an index of an array and `isArrayInsertion` is `true`, then `modify` will insert a new item at that location instead of overwriting its contents. 24 | * `ModificationOptions.formattingOptions` is now optional. If not set, newly inserted content will not be formatted. 25 | 26 | 27 | 2.2.0 2019-10-25 28 | ================== 29 | * added `ParseOptions.allowEmptyContent`. Default is `false`. 30 | * new API `getNodeType`: Returns the type of a value returned by parse. 31 | * `parse`: Fix issue with empty property name 32 | 33 | 2.1.0 2019-03-29 34 | ================== 35 | * `JSONScanner` and `JSONVisitor` return lineNumber / character. 36 | 37 | 2.0.0 2018-04-12 38 | ================== 39 | * renamed `Node.columnOffset` to `Node.colonOffset` 40 | * new API `getNodePath`: Gets the JSON path of the given JSON DOM node 41 | * new API `findNodeAtOffset`: Finds the most inner node at the given offset. If `includeRightBound` is set, also finds nodes that end at the given offset. 42 | 43 | 1.0.3 2018-03-07 44 | ================== 45 | * provide ems modules 46 | 47 | 1.0.2 2018-03-05 48 | ================== 49 | * added the `visit.onComment` API, reported when comments are allowed. 50 | * added the `ParseErrorCode.InvalidCommentToken` enum value, reported when comments are disallowed. 51 | 52 | 1.0.1 53 | ================== 54 | * added the `format` API: computes edits to format a JSON document. 55 | * added the `modify` API: computes edits to insert, remove or replace a property or value in a JSON document. 56 | * added the `allyEdits` API: applies edits to a document 57 | 58 | 1.0.0 59 | ================== 60 | * remove nls dependency (remove `getParseErrorMessage`) 61 | 62 | 0.4.2 / 2017-05-05 63 | ================== 64 | * added `ParseError.offset` & `ParseError.length` 65 | 66 | 0.4.1 / 2017-04-02 67 | ================== 68 | * added `ParseOptions.allowTrailingComma` 69 | 70 | 0.4.0 / 2017-02-23 71 | ================== 72 | * fix for `getLocation`. Now `getLocation` inside an object will always return a property from inside that property. Can be empty string if the object has no properties or if the offset is before a actual property `{ "a": { | }} will return location ['a', ' ']` 73 | 74 | 0.3.0 / 2017-01-17 75 | ================== 76 | * Updating to typescript 2.0 -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Microsoft 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # jsonc-parser 2 | Scanner and parser for JSON with comments. 3 | 4 | [![npm Package](https://img.shields.io/npm/v/jsonc-parser.svg?style=flat-square)](https://www.npmjs.org/package/jsonc-parser) 5 | [![NPM Downloads](https://img.shields.io/npm/dm/jsonc-parser.svg)](https://npmjs.org/package/jsonc-parser) 6 | [![Build Status](https://github.com/microsoft/node-jsonc-parser/workflows/Tests/badge.svg)](https://github.com/microsoft/node-jsonc-parser/workflows/Tests) 7 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 8 | 9 | Why? 10 | ---- 11 | JSONC is JSON with JavaScript style comments. This node module provides a scanner and fault tolerant parser that can process JSONC but is also useful for standard JSON. 12 | - the *scanner* tokenizes the input string into tokens and token offsets 13 | - the *visit* function implements a 'SAX' style parser with callbacks for the encountered properties and values. 14 | - the *parseTree* function computes a hierarchical DOM with offsets representing the encountered properties and values. 15 | - the *parse* function evaluates the JavaScript object represented by JSON string in a fault tolerant fashion. 16 | - the *getLocation* API returns a location object that describes the property or value located at a given offset in a JSON document. 17 | - the *findNodeAtLocation* API finds the node at a given location path in a JSON DOM. 18 | - the *format* API computes edits to format a JSON document. 19 | - the *modify* API computes edits to insert, remove or replace a property or value in a JSON document. 20 | - the *applyEdits* API applies edits to a document. 21 | 22 | Installation 23 | ------------ 24 | 25 | ``` 26 | npm install --save jsonc-parser 27 | ``` 28 | 29 | API 30 | --- 31 | 32 | ### Scanner: 33 | ```typescript 34 | 35 | /** 36 | * Creates a JSON scanner on the given text. 37 | * If ignoreTrivia is set, whitespaces or comments are ignored. 38 | */ 39 | export function createScanner(text: string, ignoreTrivia: boolean = false): JSONScanner; 40 | 41 | /** 42 | * The scanner object, representing a JSON scanner at a position in the input string. 43 | */ 44 | export interface JSONScanner { 45 | /** 46 | * Sets the scan position to a new offset. A call to 'scan' is needed to get the first token. 47 | */ 48 | setPosition(pos: number): any; 49 | /** 50 | * Read the next token. Returns the token code. 51 | */ 52 | scan(): SyntaxKind; 53 | /** 54 | * Returns the zero-based current scan position, which is after the last read token. 55 | */ 56 | getPosition(): number; 57 | /** 58 | * Returns the last read token. 59 | */ 60 | getToken(): SyntaxKind; 61 | /** 62 | * Returns the last read token value. The value for strings is the decoded string content. For numbers it's of type number, for boolean it's true or false. 63 | */ 64 | getTokenValue(): string; 65 | /** 66 | * The zero-based start offset of the last read token. 67 | */ 68 | getTokenOffset(): number; 69 | /** 70 | * The length of the last read token. 71 | */ 72 | getTokenLength(): number; 73 | /** 74 | * The zero-based start line number of the last read token. 75 | */ 76 | getTokenStartLine(): number; 77 | /** 78 | * The zero-based start character (column) of the last read token. 79 | */ 80 | getTokenStartCharacter(): number; 81 | /** 82 | * An error code of the last scan. 83 | */ 84 | getTokenError(): ScanError; 85 | } 86 | ``` 87 | 88 | ### Parser: 89 | ```typescript 90 | 91 | export interface ParseOptions { 92 | disallowComments?: boolean; 93 | allowTrailingComma?: boolean; 94 | allowEmptyContent?: boolean; 95 | } 96 | /** 97 | * Parses the given text and returns the object the JSON content represents. On invalid input, the parser tries to be as fault tolerant as possible, but still return a result. 98 | * Therefore always check the errors list to find out if the input was valid. 99 | */ 100 | export declare function parse(text: string, errors?: {error: ParseErrorCode;}[], options?: ParseOptions): any; 101 | 102 | /** 103 | * Parses the given text and invokes the visitor functions for each object, array and literal reached. 104 | */ 105 | export declare function visit(text: string, visitor: JSONVisitor, options?: ParseOptions): any; 106 | 107 | /** 108 | * Visitor called by {@linkcode visit} when parsing JSON. 109 | * 110 | * The visitor functions have the following common parameters: 111 | * - `offset`: Global offset within the JSON document, starting at 0 112 | * - `startLine`: Line number, starting at 0 113 | * - `startCharacter`: Start character (column) within the current line, starting at 0 114 | * 115 | * Additionally some functions have a `pathSupplier` parameter which can be used to obtain the 116 | * current `JSONPath` within the document. 117 | */ 118 | export interface JSONVisitor { 119 | /** 120 | * Invoked when an open brace is encountered and an object is started. The offset and length represent the location of the open brace. 121 | * When `false` is returned, the array items will not be visited. 122 | */ 123 | onObjectBegin?: (offset: number, length: number, startLine: number, startCharacter: number, pathSupplier: () => JSONPath) => void | boolean; 124 | 125 | /** 126 | * Invoked when a property is encountered. The offset and length represent the location of the property name. 127 | * The `JSONPath` created by the `pathSupplier` refers to the enclosing JSON object, it does not include the 128 | * property name yet. 129 | */ 130 | onObjectProperty?: (property: string, offset: number, length: number, startLine: number, startCharacter: number, pathSupplier: () => JSONPath) => void; 131 | /** 132 | * Invoked when a closing brace is encountered and an object is completed. The offset and length represent the location of the closing brace. 133 | */ 134 | onObjectEnd?: (offset: number, length: number, startLine: number, startCharacter: number) => void; 135 | /** 136 | * Invoked when an open bracket is encountered. The offset and length represent the location of the open bracket. 137 | * When `false` is returned, the array items will not be visited.* 138 | */ 139 | onArrayBegin?: (offset: number, length: number, startLine: number, startCharacter: number, pathSupplier: () => JSONPath) => void | boolean; 140 | /** 141 | * Invoked when a closing bracket is encountered. The offset and length represent the location of the closing bracket. 142 | */ 143 | onArrayEnd?: (offset: number, length: number, startLine: number, startCharacter: number) => void; 144 | /** 145 | * Invoked when a literal value is encountered. The offset and length represent the location of the literal value. 146 | */ 147 | onLiteralValue?: (value: any, offset: number, length: number, startLine: number, startCharacter: number, pathSupplier: () => JSONPath) => void; 148 | /** 149 | * Invoked when a comma or colon separator is encountered. The offset and length represent the location of the separator. 150 | */ 151 | onSeparator?: (character: string, offset: number, length: number, startLine: number, startCharacter: number) => void; 152 | /** 153 | * When comments are allowed, invoked when a line or block comment is encountered. The offset and length represent the location of the comment. 154 | */ 155 | onComment?: (offset: number, length: number, startLine: number, startCharacter: number) => void; 156 | /** 157 | * Invoked on an error. 158 | */ 159 | onError?: (error: ParseErrorCode, offset: number, length: number, startLine: number, startCharacter: number) => void; 160 | } 161 | 162 | /** 163 | * Parses the given text and returns a tree representation the JSON content. On invalid input, the parser tries to be as fault tolerant as possible, but still return a result. 164 | */ 165 | export declare function parseTree(text: string, errors?: ParseError[], options?: ParseOptions): Node | undefined; 166 | 167 | export declare type NodeType = "object" | "array" | "property" | "string" | "number" | "boolean" | "null"; 168 | export interface Node { 169 | type: NodeType; 170 | value?: any; 171 | offset: number; 172 | length: number; 173 | colonOffset?: number; 174 | parent?: Node; 175 | children?: Node[]; 176 | } 177 | 178 | ``` 179 | 180 | ### Utilities: 181 | ```typescript 182 | /** 183 | * Takes JSON with JavaScript-style comments and remove 184 | * them. Optionally replaces every none-newline character 185 | * of comments with a replaceCharacter 186 | */ 187 | export declare function stripComments(text: string, replaceCh?: string): string; 188 | 189 | /** 190 | * For a given offset, evaluate the location in the JSON document. Each segment in the location path is either a property name or an array index. 191 | */ 192 | export declare function getLocation(text: string, position: number): Location; 193 | 194 | /** 195 | * A {@linkcode JSONPath} segment. Either a string representing an object property name 196 | * or a number (starting at 0) for array indices. 197 | */ 198 | export declare type Segment = string | number; 199 | export declare type JSONPath = Segment[]; 200 | export interface Location { 201 | /** 202 | * The previous property key or literal value (string, number, boolean or null) or undefined. 203 | */ 204 | previousNode?: Node; 205 | /** 206 | * The path describing the location in the JSON document. The path consists of a sequence strings 207 | * representing an object property or numbers for array indices. 208 | */ 209 | path: JSONPath; 210 | /** 211 | * Matches the locations path against a pattern consisting of strings (for properties) and numbers (for array indices). 212 | * '*' will match a single segment, of any property name or index. 213 | * '**' will match a sequence of segments or no segment, of any property name or index. 214 | */ 215 | matches: (patterns: JSONPath) => boolean; 216 | /** 217 | * If set, the location's offset is at a property key. 218 | */ 219 | isAtPropertyKey: boolean; 220 | } 221 | 222 | /** 223 | * Finds the node at the given path in a JSON DOM. 224 | */ 225 | export function findNodeAtLocation(root: Node, path: JSONPath): Node | undefined; 226 | 227 | /** 228 | * Finds the most inner node at the given offset. If includeRightBound is set, also finds nodes that end at the given offset. 229 | */ 230 | export function findNodeAtOffset(root: Node, offset: number, includeRightBound?: boolean) : Node | undefined; 231 | 232 | /** 233 | * Gets the JSON path of the given JSON DOM node 234 | */ 235 | export function getNodePath(node: Node): JSONPath; 236 | 237 | /** 238 | * Evaluates the JavaScript object of the given JSON DOM node 239 | */ 240 | export function getNodeValue(node: Node): any; 241 | 242 | /** 243 | * Computes the edit operations needed to format a JSON document. 244 | * 245 | * @param documentText The input text 246 | * @param range The range to format or `undefined` to format the full content 247 | * @param options The formatting options 248 | * @returns The edit operations describing the formatting changes to the original document following the format described in {@linkcode EditResult}. 249 | * To apply the edit operations to the input, use {@linkcode applyEdits}. 250 | */ 251 | export function format(documentText: string, range: Range, options: FormattingOptions): EditResult; 252 | 253 | /** 254 | * Computes the edit operations needed to modify a value in the JSON document. 255 | * 256 | * @param documentText The input text 257 | * @param path The path of the value to change. The path represents either to the document root, a property or an array item. 258 | * If the path points to an non-existing property or item, it will be created. 259 | * @param value The new value for the specified property or item. If the value is undefined, 260 | * the property or item will be removed. 261 | * @param options Options 262 | * @returns The edit operations describing the changes to the original document, following the format described in {@linkcode EditResult}. 263 | * To apply the edit operations to the input, use {@linkcode applyEdits}. 264 | */ 265 | export function modify(text: string, path: JSONPath, value: any, options: ModificationOptions): EditResult; 266 | 267 | /** 268 | * Applies edits to an input string. 269 | * @param text The input text 270 | * @param edits Edit operations following the format described in {@linkcode EditResult}. 271 | * @returns The text with the applied edits. 272 | * @throws An error if the edit operations are not well-formed as described in {@linkcode EditResult}. 273 | */ 274 | export function applyEdits(text: string, edits: EditResult): string; 275 | 276 | /** 277 | * An edit result describes a textual edit operation. It is the result of a {@linkcode format} and {@linkcode modify} operation. 278 | * It consist of one or more edits describing insertions, replacements or removals of text segments. 279 | * * The offsets of the edits refer to the original state of the document. 280 | * * No two edits change or remove the same range of text in the original document. 281 | * * Multiple edits can have the same offset if they are multiple inserts, or an insert followed by a remove or replace. 282 | * * The order in the array defines which edit is applied first. 283 | * To apply an edit result use {@linkcode applyEdits}. 284 | * In general multiple EditResults must not be concatenated because they might impact each other, producing incorrect or malformed JSON data. 285 | */ 286 | export type EditResult = Edit[]; 287 | 288 | /** 289 | * Represents a text modification 290 | */ 291 | export interface Edit { 292 | /** 293 | * The start offset of the modification. 294 | */ 295 | offset: number; 296 | /** 297 | * The length of the modification. Must not be negative. Empty length represents an *insert*. 298 | */ 299 | length: number; 300 | /** 301 | * The new content. Empty content represents a *remove*. 302 | */ 303 | content: string; 304 | } 305 | 306 | /** 307 | * A text range in the document 308 | */ 309 | export interface Range { 310 | /** 311 | * The start offset of the range. 312 | */ 313 | offset: number; 314 | /** 315 | * The length of the range. Must not be negative. 316 | */ 317 | length: number; 318 | } 319 | 320 | /** 321 | * Options used by {@linkcode format} when computing the formatting edit operations 322 | */ 323 | export interface FormattingOptions { 324 | /** 325 | * If indentation is based on spaces (`insertSpaces` = true), then what is the number of spaces that make an indent? 326 | */ 327 | tabSize: number; 328 | /** 329 | * Is indentation based on spaces? 330 | */ 331 | insertSpaces: boolean; 332 | /** 333 | * The default 'end of line' character 334 | */ 335 | eol: string; 336 | } 337 | 338 | /** 339 | * Options used by {@linkcode modify} when computing the modification edit operations 340 | */ 341 | export interface ModificationOptions { 342 | /** 343 | * Formatting options. If undefined, the newly inserted code will be inserted unformatted. 344 | */ 345 | formattingOptions?: FormattingOptions; 346 | /** 347 | * Default false. If `JSONPath` refers to an index of an array and `isArrayInsertion` is `true`, then 348 | * {@linkcode modify} will insert a new item at that location instead of overwriting its contents. 349 | */ 350 | isArrayInsertion?: boolean; 351 | /** 352 | * Optional function to define the insertion index given an existing list of properties. 353 | */ 354 | getInsertionIndex?: (properties: string[]) => number; 355 | } 356 | ``` 357 | 358 | 359 | License 360 | ------- 361 | 362 | (MIT License) 363 | 364 | Copyright 2018, Microsoft 365 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Security 4 | 5 | Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/). 6 | 7 | If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://docs.microsoft.com/en-us/previous-versions/tn-archive/cc751383(v=technet.10)), please report it to us as described below. 8 | 9 | ## Reporting Security Issues 10 | 11 | **Please do not report security vulnerabilities through public GitHub issues.** 12 | 13 | Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://msrc.microsoft.com/create-report). 14 | 15 | If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://www.microsoft.com/en-us/msrc/pgp-key-msrc). 16 | 17 | You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://www.microsoft.com/msrc). 18 | 19 | Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: 20 | 21 | * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) 22 | * Full paths of source file(s) related to the manifestation of the issue 23 | * The location of the affected source code (tag/branch/commit or direct URL) 24 | * Any special configuration required to reproduce the issue 25 | * Step-by-step instructions to reproduce the issue 26 | * Proof-of-concept or exploit code (if possible) 27 | * Impact of the issue, including how an attacker might exploit the issue 28 | 29 | This information will help us triage your report more quickly. 30 | 31 | If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://microsoft.com/msrc/bounty) page for more details about our active programs. 32 | 33 | ## Preferred Languages 34 | 35 | We prefer all communications to be in English. 36 | 37 | ## Policy 38 | 39 | Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://www.microsoft.com/en-us/msrc/cvd). 40 | 41 | -------------------------------------------------------------------------------- /build/pipeline.yml: -------------------------------------------------------------------------------- 1 | name: $(Date:yyyyMMdd)$(Rev:.r) 2 | 3 | trigger: 4 | batch: true 5 | branches: 6 | include: 7 | - main 8 | pr: none 9 | 10 | resources: 11 | repositories: 12 | - repository: templates 13 | type: github 14 | name: microsoft/vscode-engineering 15 | ref: main 16 | endpoint: Monaco 17 | 18 | parameters: 19 | - name: publishPackage 20 | displayName: 🚀 Publish jsonc-parser 21 | type: boolean 22 | default: false 23 | 24 | extends: 25 | template: azure-pipelines/npm-package/pipeline.yml@templates 26 | parameters: 27 | npmPackages: 28 | - name: jsonc-parser 29 | 30 | testPlatforms: 31 | - name: Linux 32 | nodeVersions: 33 | - 18.x 34 | - name: MacOS 35 | nodeVersions: 36 | - 18.x 37 | - name: Windows 38 | nodeVersions: 39 | - 18.x 40 | 41 | buildSteps: 42 | - script: npm ci 43 | displayName: Install dependencies 44 | 45 | testSteps: 46 | - script: npm ci 47 | displayName: Install dependencies 48 | - script: npm test 49 | displayName: Test npm package 50 | 51 | publishPackage: ${{ parameters.publishPackage }} 52 | -------------------------------------------------------------------------------- /build/remove-sourcemap-refs.js: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | const fs = require('fs'); 7 | const path = require('path'); 8 | 9 | function deleteRefs(dir) { 10 | const files = fs.readdirSync(dir); 11 | for (let file of files) { 12 | const filePath = path.join(dir, file); 13 | const stat = fs.statSync(filePath); 14 | if (stat.isDirectory()) { 15 | deleteRefs(filePath); 16 | } else if (path.extname(file) === '.js') { 17 | const content = fs.readFileSync(filePath, 'utf8'); 18 | const newContent = content.replace(/\/\/\# sourceMappingURL=[^]+.js.map/, '') 19 | if (content.length !== newContent.length) { 20 | console.log('remove sourceMappingURL in ' + filePath); 21 | fs.writeFileSync(filePath, newContent); 22 | } 23 | } else if (path.extname(file) === '.map') { 24 | fs.unlinkSync(filePath) 25 | console.log('remove ' + filePath); 26 | } 27 | } 28 | } 29 | 30 | let location = path.join(__dirname, '..', 'lib'); 31 | console.log('process ' + location); 32 | deleteRefs(location); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jsonc-parser", 3 | "version": "3.3.1", 4 | "description": "Scanner and parser for JSON with comments.", 5 | "main": "./lib/umd/main.js", 6 | "typings": "./lib/umd/main.d.ts", 7 | "module": "./lib/esm/main.js", 8 | "author": "Microsoft Corporation", 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/microsoft/node-jsonc-parser" 12 | }, 13 | "license": "MIT", 14 | "bugs": { 15 | "url": "https://github.com/microsoft/node-jsonc-parser/issues" 16 | }, 17 | "devDependencies": { 18 | "@types/mocha": "^10.0.7", 19 | "@types/node": "^18.x", 20 | "@typescript-eslint/eslint-plugin": "^7.13.1", 21 | "@typescript-eslint/parser": "^7.13.1", 22 | "eslint": "^8.57.0", 23 | "mocha": "^10.4.0", 24 | "rimraf": "^5.0.7", 25 | "typescript": "^5.4.2" 26 | }, 27 | "scripts": { 28 | "prepack": "npm run clean && npm run compile-esm && npm run test && npm run remove-sourcemap-refs", 29 | "compile": "tsc -p ./src && npm run lint", 30 | "compile-esm": "tsc -p ./src/tsconfig.esm.json", 31 | "remove-sourcemap-refs": "node ./build/remove-sourcemap-refs.js", 32 | "clean": "rimraf lib", 33 | "watch": "tsc -w -p ./src", 34 | "test": "npm run compile && mocha ./lib/umd/test", 35 | "lint": "eslint src/**/*.ts" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/impl/edit.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 'use strict'; 6 | 7 | import { Edit, ParseError, Node, JSONPath, Segment, ModificationOptions } from '../main'; 8 | import { format, isEOL } from './format'; 9 | import { parseTree, findNodeAtLocation } from './parser'; 10 | 11 | export function removeProperty(text: string, path: JSONPath, options: ModificationOptions): Edit[] { 12 | return setProperty(text, path, void 0, options); 13 | } 14 | 15 | export function setProperty(text: string, originalPath: JSONPath, value: any, options: ModificationOptions): Edit[] { 16 | const path = originalPath.slice(); 17 | const errors: ParseError[] = []; 18 | const root = parseTree(text, errors); 19 | let parent: Node | undefined = void 0; 20 | 21 | let lastSegment: Segment | undefined = void 0; 22 | while (path.length > 0) { 23 | lastSegment = path.pop(); 24 | parent = findNodeAtLocation(root, path); 25 | if (parent === void 0 && value !== void 0) { 26 | if (typeof lastSegment === 'string') { 27 | value = { [lastSegment]: value }; 28 | } else { 29 | value = [value]; 30 | } 31 | } else { 32 | break; 33 | } 34 | } 35 | 36 | if (!parent) { 37 | // empty document 38 | if (value === void 0) { // delete 39 | throw new Error('Can not delete in empty document'); 40 | } 41 | return withFormatting(text, { offset: root ? root.offset : 0, length: root ? root.length : 0, content: JSON.stringify(value) }, options); 42 | } else if (parent.type === 'object' && typeof lastSegment === 'string' && Array.isArray(parent.children)) { 43 | const existing = findNodeAtLocation(parent, [lastSegment]); 44 | if (existing !== void 0) { 45 | if (value === void 0) { // delete 46 | if (!existing.parent) { 47 | throw new Error('Malformed AST'); 48 | } 49 | const propertyIndex = parent.children.indexOf(existing.parent); 50 | let removeBegin: number; 51 | let removeEnd = existing.parent.offset + existing.parent.length; 52 | if (propertyIndex > 0) { 53 | // remove the comma of the previous node 54 | let previous = parent.children[propertyIndex - 1]; 55 | removeBegin = previous.offset + previous.length; 56 | } else { 57 | removeBegin = parent.offset + 1; 58 | if (parent.children.length > 1) { 59 | // remove the comma of the next node 60 | let next = parent.children[1]; 61 | removeEnd = next.offset; 62 | } 63 | } 64 | return withFormatting(text, { offset: removeBegin, length: removeEnd - removeBegin, content: '' }, options); 65 | } else { 66 | // set value of existing property 67 | return withFormatting(text, { offset: existing.offset, length: existing.length, content: JSON.stringify(value) }, options); 68 | } 69 | } else { 70 | if (value === void 0) { // delete 71 | return []; // property does not exist, nothing to do 72 | } 73 | const newProperty = `${JSON.stringify(lastSegment)}: ${JSON.stringify(value)}`; 74 | const index = options.getInsertionIndex ? options.getInsertionIndex(parent.children.map(p => p.children![0].value)) : parent.children.length; 75 | let edit: Edit; 76 | if (index > 0) { 77 | let previous = parent.children[index - 1]; 78 | edit = { offset: previous.offset + previous.length, length: 0, content: ',' + newProperty }; 79 | } else if (parent.children.length === 0) { 80 | edit = { offset: parent.offset + 1, length: 0, content: newProperty }; 81 | } else { 82 | edit = { offset: parent.offset + 1, length: 0, content: newProperty + ',' }; 83 | } 84 | return withFormatting(text, edit, options); 85 | } 86 | } else if (parent.type === 'array' && typeof lastSegment === 'number' && Array.isArray(parent.children)) { 87 | const insertIndex = lastSegment; 88 | if (insertIndex === -1) { 89 | // Insert 90 | const newProperty = `${JSON.stringify(value)}`; 91 | let edit: Edit; 92 | if (parent.children.length === 0) { 93 | edit = { offset: parent.offset + 1, length: 0, content: newProperty }; 94 | } else { 95 | const previous = parent.children[parent.children.length - 1]; 96 | edit = { offset: previous.offset + previous.length, length: 0, content: ',' + newProperty }; 97 | } 98 | return withFormatting(text, edit, options); 99 | } else if (value === void 0 && parent.children.length >= 0) { 100 | // Removal 101 | const removalIndex = lastSegment; 102 | const toRemove = parent.children[removalIndex]; 103 | let edit: Edit; 104 | if (parent.children.length === 1) { 105 | // only item 106 | edit = { offset: parent.offset + 1, length: parent.length - 2, content: '' }; 107 | } else if (parent.children.length - 1 === removalIndex) { 108 | // last item 109 | let previous = parent.children[removalIndex - 1]; 110 | let offset = previous.offset + previous.length; 111 | let parentEndOffset = parent.offset + parent.length; 112 | edit = { offset, length: parentEndOffset - 2 - offset, content: '' }; 113 | } else { 114 | edit = { offset: toRemove.offset, length: parent.children[removalIndex + 1].offset - toRemove.offset, content: '' }; 115 | } 116 | return withFormatting(text, edit, options); 117 | } else if (value !== void 0) { 118 | let edit: Edit; 119 | const newProperty = `${JSON.stringify(value)}`; 120 | 121 | if (!options.isArrayInsertion && parent.children.length > lastSegment) { 122 | const toModify = parent.children[lastSegment]; 123 | 124 | edit = { offset: toModify.offset, length: toModify.length, content: newProperty }; 125 | } else if (parent.children.length === 0 || lastSegment === 0) { 126 | edit = { offset: parent.offset + 1, length: 0, content: parent.children.length === 0 ? newProperty : newProperty + ',' }; 127 | } else { 128 | const index = lastSegment > parent.children.length ? parent.children.length : lastSegment; 129 | const previous = parent.children[index - 1]; 130 | edit = { offset: previous.offset + previous.length, length: 0, content: ',' + newProperty }; 131 | } 132 | 133 | return withFormatting(text, edit, options); 134 | } else { 135 | throw new Error(`Can not ${value === void 0 ? 'remove' : (options.isArrayInsertion ? 'insert' : 'modify')} Array index ${insertIndex} as length is not sufficient`); 136 | } 137 | } else { 138 | throw new Error(`Can not add ${typeof lastSegment !== 'number' ? 'index' : 'property'} to parent of type ${parent.type}`); 139 | } 140 | } 141 | 142 | function withFormatting(text: string, edit: Edit, options: ModificationOptions): Edit[] { 143 | if (!options.formattingOptions) { 144 | return [edit]; 145 | } 146 | // apply the edit 147 | let newText = applyEdit(text, edit); 148 | 149 | // format the new text 150 | let begin = edit.offset; 151 | let end = edit.offset + edit.content.length; 152 | if (edit.length === 0 || edit.content.length === 0) { // insert or remove 153 | while (begin > 0 && !isEOL(newText, begin - 1)) { 154 | begin--; 155 | } 156 | while (end < newText.length && !isEOL(newText, end)) { 157 | end++; 158 | } 159 | } 160 | 161 | const edits = format(newText, { offset: begin, length: end - begin }, { ...options.formattingOptions, keepLines: false }); 162 | 163 | // apply the formatting edits and track the begin and end offsets of the changes 164 | for (let i = edits.length - 1; i >= 0; i--) { 165 | const edit = edits[i]; 166 | newText = applyEdit(newText, edit); 167 | begin = Math.min(begin, edit.offset); 168 | end = Math.max(end, edit.offset + edit.length); 169 | end += edit.content.length - edit.length; 170 | } 171 | // create a single edit with all changes 172 | const editLength = text.length - (newText.length - end) - begin; 173 | return [{ offset: begin, length: editLength, content: newText.substring(begin, end) }]; 174 | } 175 | 176 | export function applyEdit(text: string, edit: Edit): string { 177 | return text.substring(0, edit.offset) + edit.content + text.substring(edit.offset + edit.length); 178 | } 179 | 180 | export function isWS(text: string, offset: number) { 181 | return '\r\n \t'.indexOf(text.charAt(offset)) !== -1; 182 | } 183 | -------------------------------------------------------------------------------- /src/impl/format.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 'use strict'; 6 | 7 | import { Range, FormattingOptions, Edit, SyntaxKind, ScanError } from '../main'; 8 | import { createScanner } from './scanner'; 9 | import { cachedSpaces, cachedBreakLinesWithSpaces, supportedEols, SupportedEOL } from './string-intern'; 10 | 11 | export function format(documentText: string, range: Range | undefined, options: FormattingOptions): Edit[] { 12 | let initialIndentLevel: number; 13 | let formatText: string; 14 | let formatTextStart: number; 15 | let rangeStart: number; 16 | let rangeEnd: number; 17 | if (range) { 18 | rangeStart = range.offset; 19 | rangeEnd = rangeStart + range.length; 20 | 21 | formatTextStart = rangeStart; 22 | while (formatTextStart > 0 && !isEOL(documentText, formatTextStart - 1)) { 23 | formatTextStart--; 24 | } 25 | let endOffset = rangeEnd; 26 | while (endOffset < documentText.length && !isEOL(documentText, endOffset)) { 27 | endOffset++; 28 | } 29 | formatText = documentText.substring(formatTextStart, endOffset); 30 | initialIndentLevel = computeIndentLevel(formatText, options); 31 | } else { 32 | formatText = documentText; 33 | initialIndentLevel = 0; 34 | formatTextStart = 0; 35 | rangeStart = 0; 36 | rangeEnd = documentText.length; 37 | } 38 | const eol = getEOL(options, documentText); 39 | const eolFastPathSupported = supportedEols.includes(eol as any); 40 | 41 | let numberLineBreaks = 0; 42 | 43 | let indentLevel = 0; 44 | let indentValue: string; 45 | if (options.insertSpaces) { 46 | indentValue = cachedSpaces[options.tabSize || 4] ?? repeat(cachedSpaces[1], options.tabSize || 4); 47 | } else { 48 | indentValue = '\t'; 49 | } 50 | const indentType = indentValue === '\t' ? '\t' : ' '; 51 | 52 | let scanner = createScanner(formatText, false); 53 | let hasError = false; 54 | 55 | function newLinesAndIndent(): string { 56 | if (numberLineBreaks > 1) { 57 | return repeat(eol, numberLineBreaks) + repeat(indentValue, initialIndentLevel + indentLevel); 58 | } 59 | 60 | const amountOfSpaces = indentValue.length * (initialIndentLevel + indentLevel); 61 | 62 | if (!eolFastPathSupported || amountOfSpaces > cachedBreakLinesWithSpaces[indentType][eol as SupportedEOL].length) { 63 | return eol + repeat(indentValue, initialIndentLevel + indentLevel); 64 | } 65 | 66 | if (amountOfSpaces <= 0) { 67 | return eol; 68 | } 69 | 70 | return cachedBreakLinesWithSpaces[indentType][eol as SupportedEOL][amountOfSpaces]; 71 | } 72 | 73 | function scanNext(): SyntaxKind { 74 | let token = scanner.scan(); 75 | numberLineBreaks = 0; 76 | 77 | while (token === SyntaxKind.Trivia || token === SyntaxKind.LineBreakTrivia) { 78 | if (token === SyntaxKind.LineBreakTrivia && options.keepLines) { 79 | numberLineBreaks += 1; 80 | } else if (token === SyntaxKind.LineBreakTrivia) { 81 | numberLineBreaks = 1; 82 | } 83 | token = scanner.scan(); 84 | } 85 | hasError = token === SyntaxKind.Unknown || scanner.getTokenError() !== ScanError.None; 86 | return token; 87 | } 88 | const editOperations: Edit[] = []; 89 | function addEdit(text: string, startOffset: number, endOffset: number) { 90 | if (!hasError && (!range || (startOffset < rangeEnd && endOffset > rangeStart)) && documentText.substring(startOffset, endOffset) !== text) { 91 | editOperations.push({ offset: startOffset, length: endOffset - startOffset, content: text }); 92 | } 93 | } 94 | 95 | let firstToken = scanNext(); 96 | if (options.keepLines && numberLineBreaks > 0) { 97 | addEdit(repeat(eol, numberLineBreaks), 0, 0); 98 | } 99 | 100 | if (firstToken !== SyntaxKind.EOF) { 101 | let firstTokenStart = scanner.getTokenOffset() + formatTextStart; 102 | let initialIndent = (indentValue.length * initialIndentLevel < 20) && options.insertSpaces 103 | ? cachedSpaces[indentValue.length * initialIndentLevel] 104 | : repeat(indentValue, initialIndentLevel); 105 | addEdit(initialIndent, formatTextStart, firstTokenStart); 106 | } 107 | 108 | while (firstToken !== SyntaxKind.EOF) { 109 | 110 | let firstTokenEnd = scanner.getTokenOffset() + scanner.getTokenLength() + formatTextStart; 111 | let secondToken = scanNext(); 112 | let replaceContent = ''; 113 | let needsLineBreak = false; 114 | 115 | while (numberLineBreaks === 0 && (secondToken === SyntaxKind.LineCommentTrivia || secondToken === SyntaxKind.BlockCommentTrivia)) { 116 | let commentTokenStart = scanner.getTokenOffset() + formatTextStart; 117 | addEdit(cachedSpaces[1], firstTokenEnd, commentTokenStart); 118 | firstTokenEnd = scanner.getTokenOffset() + scanner.getTokenLength() + formatTextStart; 119 | needsLineBreak = secondToken === SyntaxKind.LineCommentTrivia; 120 | replaceContent = needsLineBreak ? newLinesAndIndent() : ''; 121 | secondToken = scanNext(); 122 | } 123 | 124 | if (secondToken === SyntaxKind.CloseBraceToken) { 125 | if (firstToken !== SyntaxKind.OpenBraceToken) { indentLevel--; }; 126 | 127 | if (options.keepLines && numberLineBreaks > 0 || !options.keepLines && firstToken !== SyntaxKind.OpenBraceToken) { 128 | replaceContent = newLinesAndIndent(); 129 | } else if (options.keepLines) { 130 | replaceContent = cachedSpaces[1]; 131 | } 132 | } else if (secondToken === SyntaxKind.CloseBracketToken) { 133 | if (firstToken !== SyntaxKind.OpenBracketToken) { indentLevel--; }; 134 | 135 | if (options.keepLines && numberLineBreaks > 0 || !options.keepLines && firstToken !== SyntaxKind.OpenBracketToken) { 136 | replaceContent = newLinesAndIndent(); 137 | } else if (options.keepLines) { 138 | replaceContent = cachedSpaces[1]; 139 | } 140 | } else { 141 | switch (firstToken) { 142 | case SyntaxKind.OpenBracketToken: 143 | case SyntaxKind.OpenBraceToken: 144 | indentLevel++; 145 | if (options.keepLines && numberLineBreaks > 0 || !options.keepLines) { 146 | replaceContent = newLinesAndIndent(); 147 | } else { 148 | replaceContent = cachedSpaces[1]; 149 | } 150 | break; 151 | case SyntaxKind.CommaToken: 152 | if (options.keepLines && numberLineBreaks > 0 || !options.keepLines) { 153 | replaceContent = newLinesAndIndent(); 154 | } else { 155 | replaceContent = cachedSpaces[1]; 156 | } 157 | break; 158 | case SyntaxKind.LineCommentTrivia: 159 | replaceContent = newLinesAndIndent(); 160 | break; 161 | case SyntaxKind.BlockCommentTrivia: 162 | if (numberLineBreaks > 0) { 163 | replaceContent = newLinesAndIndent(); 164 | } else if (!needsLineBreak) { 165 | replaceContent = cachedSpaces[1]; 166 | } 167 | break; 168 | case SyntaxKind.ColonToken: 169 | if (options.keepLines && numberLineBreaks > 0) { 170 | replaceContent = newLinesAndIndent(); 171 | } else if (!needsLineBreak) { 172 | replaceContent = cachedSpaces[1]; 173 | } 174 | break; 175 | case SyntaxKind.StringLiteral: 176 | if (options.keepLines && numberLineBreaks > 0) { 177 | replaceContent = newLinesAndIndent(); 178 | } else if (secondToken === SyntaxKind.ColonToken && !needsLineBreak) { 179 | replaceContent = ''; 180 | } 181 | break; 182 | case SyntaxKind.NullKeyword: 183 | case SyntaxKind.TrueKeyword: 184 | case SyntaxKind.FalseKeyword: 185 | case SyntaxKind.NumericLiteral: 186 | case SyntaxKind.CloseBraceToken: 187 | case SyntaxKind.CloseBracketToken: 188 | if (options.keepLines && numberLineBreaks > 0) { 189 | replaceContent = newLinesAndIndent(); 190 | } else { 191 | if ((secondToken === SyntaxKind.LineCommentTrivia || secondToken === SyntaxKind.BlockCommentTrivia) && !needsLineBreak) { 192 | replaceContent = cachedSpaces[1]; 193 | } else if (secondToken !== SyntaxKind.CommaToken && secondToken !== SyntaxKind.EOF) { 194 | hasError = true; 195 | } 196 | } 197 | break; 198 | case SyntaxKind.Unknown: 199 | hasError = true; 200 | break; 201 | } 202 | if (numberLineBreaks > 0 && (secondToken === SyntaxKind.LineCommentTrivia || secondToken === SyntaxKind.BlockCommentTrivia)) { 203 | replaceContent = newLinesAndIndent(); 204 | } 205 | } 206 | if (secondToken === SyntaxKind.EOF) { 207 | if (options.keepLines && numberLineBreaks > 0) { 208 | replaceContent = newLinesAndIndent(); 209 | } else { 210 | replaceContent = options.insertFinalNewline ? eol : ''; 211 | } 212 | } 213 | const secondTokenStart = scanner.getTokenOffset() + formatTextStart; 214 | addEdit(replaceContent, firstTokenEnd, secondTokenStart); 215 | firstToken = secondToken; 216 | } 217 | return editOperations; 218 | } 219 | 220 | function repeat(s: string, count: number): string { 221 | let result = ''; 222 | for (let i = 0; i < count; i++) { 223 | result += s; 224 | } 225 | return result; 226 | } 227 | 228 | function computeIndentLevel(content: string, options: FormattingOptions): number { 229 | let i = 0; 230 | let nChars = 0; 231 | const tabSize = options.tabSize || 4; 232 | while (i < content.length) { 233 | let ch = content.charAt(i); 234 | if (ch === cachedSpaces[1]) { 235 | nChars++; 236 | } else if (ch === '\t') { 237 | nChars += tabSize; 238 | } else { 239 | break; 240 | } 241 | i++; 242 | } 243 | return Math.floor(nChars / tabSize); 244 | } 245 | 246 | function getEOL(options: FormattingOptions, text: string): string { 247 | for (let i = 0; i < text.length; i++) { 248 | const ch = text.charAt(i); 249 | if (ch === '\r') { 250 | if (i + 1 < text.length && text.charAt(i + 1) === '\n') { 251 | return '\r\n'; 252 | } 253 | return '\r'; 254 | } else if (ch === '\n') { 255 | return '\n'; 256 | } 257 | } 258 | return (options && options.eol) || '\n'; 259 | } 260 | 261 | export function isEOL(text: string, offset: number) { 262 | return '\r\n'.indexOf(text.charAt(offset)) !== -1; 263 | } 264 | -------------------------------------------------------------------------------- /src/impl/parser.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 'use strict'; 6 | 7 | import { createScanner } from './scanner'; 8 | import { 9 | JSONPath, 10 | JSONVisitor, 11 | Location, 12 | Node, 13 | NodeType, 14 | ParseError, 15 | ParseErrorCode, 16 | ParseOptions, 17 | ScanError, 18 | Segment, 19 | SyntaxKind 20 | } from '../main'; 21 | 22 | namespace ParseOptions { 23 | export const DEFAULT = { 24 | allowTrailingComma: false 25 | }; 26 | } 27 | 28 | interface NodeImpl extends Node { 29 | type: NodeType; 30 | value?: any; 31 | offset: number; 32 | length: number; 33 | colonOffset?: number; 34 | parent?: NodeImpl; 35 | children?: NodeImpl[]; 36 | } 37 | 38 | /** 39 | * For a given offset, evaluate the location in the JSON document. Each segment in the location path is either a property name or an array index. 40 | */ 41 | export function getLocation(text: string, position: number): Location { 42 | const segments: Segment[] = []; // strings or numbers 43 | const earlyReturnException = new Object(); 44 | let previousNode: NodeImpl | undefined = undefined; 45 | const previousNodeInst: NodeImpl = { 46 | value: {}, 47 | offset: 0, 48 | length: 0, 49 | type: 'object', 50 | parent: undefined 51 | }; 52 | let isAtPropertyKey = false; 53 | function setPreviousNode(value: string, offset: number, length: number, type: NodeType) { 54 | previousNodeInst.value = value; 55 | previousNodeInst.offset = offset; 56 | previousNodeInst.length = length; 57 | previousNodeInst.type = type; 58 | previousNodeInst.colonOffset = undefined; 59 | previousNode = previousNodeInst; 60 | } 61 | try { 62 | 63 | visit(text, { 64 | onObjectBegin: (offset: number, length: number) => { 65 | if (position <= offset) { 66 | throw earlyReturnException; 67 | } 68 | previousNode = undefined; 69 | isAtPropertyKey = position > offset; 70 | segments.push(''); // push a placeholder (will be replaced) 71 | }, 72 | onObjectProperty: (name: string, offset: number, length: number) => { 73 | if (position < offset) { 74 | throw earlyReturnException; 75 | } 76 | setPreviousNode(name, offset, length, 'property'); 77 | segments[segments.length - 1] = name; 78 | if (position <= offset + length) { 79 | throw earlyReturnException; 80 | } 81 | }, 82 | onObjectEnd: (offset: number, length: number) => { 83 | if (position <= offset) { 84 | throw earlyReturnException; 85 | } 86 | previousNode = undefined; 87 | segments.pop(); 88 | }, 89 | onArrayBegin: (offset: number, length: number) => { 90 | if (position <= offset) { 91 | throw earlyReturnException; 92 | } 93 | previousNode = undefined; 94 | segments.push(0); 95 | }, 96 | onArrayEnd: (offset: number, length: number) => { 97 | if (position <= offset) { 98 | throw earlyReturnException; 99 | } 100 | previousNode = undefined; 101 | segments.pop(); 102 | }, 103 | onLiteralValue: (value: any, offset: number, length: number) => { 104 | if (position < offset) { 105 | throw earlyReturnException; 106 | } 107 | setPreviousNode(value, offset, length, getNodeType(value)); 108 | 109 | if (position <= offset + length) { 110 | throw earlyReturnException; 111 | } 112 | }, 113 | onSeparator: (sep: string, offset: number, length: number) => { 114 | if (position <= offset) { 115 | throw earlyReturnException; 116 | } 117 | if (sep === ':' && previousNode && previousNode.type === 'property') { 118 | previousNode.colonOffset = offset; 119 | isAtPropertyKey = false; 120 | previousNode = undefined; 121 | } else if (sep === ',') { 122 | const last = segments[segments.length - 1]; 123 | if (typeof last === 'number') { 124 | segments[segments.length - 1] = last + 1; 125 | } else { 126 | isAtPropertyKey = true; 127 | segments[segments.length - 1] = ''; 128 | } 129 | previousNode = undefined; 130 | } 131 | } 132 | }); 133 | } catch (e) { 134 | if (e !== earlyReturnException) { 135 | throw e; 136 | } 137 | } 138 | 139 | return { 140 | path: segments, 141 | previousNode, 142 | isAtPropertyKey, 143 | matches: (pattern: Segment[]) => { 144 | let k = 0; 145 | for (let i = 0; k < pattern.length && i < segments.length; i++) { 146 | if (pattern[k] === segments[i] || pattern[k] === '*') { 147 | k++; 148 | } else if (pattern[k] !== '**') { 149 | return false; 150 | } 151 | } 152 | return k === pattern.length; 153 | } 154 | }; 155 | } 156 | 157 | 158 | /** 159 | * Parses the given text and returns the object the JSON content represents. On invalid input, the parser tries to be as fault tolerant as possible, but still return a result. 160 | * Therefore always check the errors list to find out if the input was valid. 161 | */ 162 | export function parse(text: string, errors: ParseError[] = [], options: ParseOptions = ParseOptions.DEFAULT): any { 163 | let currentProperty: string | null = null; 164 | let currentParent: any = []; 165 | const previousParents: any[] = []; 166 | 167 | function onValue(value: any) { 168 | if (Array.isArray(currentParent)) { 169 | (currentParent).push(value); 170 | } else if (currentProperty !== null) { 171 | currentParent[currentProperty] = value; 172 | } 173 | } 174 | 175 | const visitor: JSONVisitor = { 176 | onObjectBegin: () => { 177 | const object = {}; 178 | onValue(object); 179 | previousParents.push(currentParent); 180 | currentParent = object; 181 | currentProperty = null; 182 | }, 183 | onObjectProperty: (name: string) => { 184 | currentProperty = name; 185 | }, 186 | onObjectEnd: () => { 187 | currentParent = previousParents.pop(); 188 | }, 189 | onArrayBegin: () => { 190 | const array: any[] = []; 191 | onValue(array); 192 | previousParents.push(currentParent); 193 | currentParent = array; 194 | currentProperty = null; 195 | }, 196 | onArrayEnd: () => { 197 | currentParent = previousParents.pop(); 198 | }, 199 | onLiteralValue: onValue, 200 | onError: (error: ParseErrorCode, offset: number, length: number) => { 201 | errors.push({ error, offset, length }); 202 | } 203 | }; 204 | visit(text, visitor, options); 205 | return currentParent[0]; 206 | } 207 | 208 | 209 | /** 210 | * Parses the given text and returns a tree representation the JSON content. On invalid input, the parser tries to be as fault tolerant as possible, but still return a result. 211 | */ 212 | export function parseTree(text: string, errors: ParseError[] = [], options: ParseOptions = ParseOptions.DEFAULT): Node | undefined { 213 | let currentParent: NodeImpl = { type: 'array', offset: -1, length: -1, children: [], parent: undefined }; // artificial root 214 | 215 | function ensurePropertyComplete(endOffset: number) { 216 | if (currentParent.type === 'property') { 217 | currentParent.length = endOffset - currentParent.offset; 218 | currentParent = currentParent.parent!; 219 | } 220 | } 221 | 222 | function onValue(valueNode: Node): Node { 223 | currentParent.children!.push(valueNode); 224 | return valueNode; 225 | } 226 | 227 | const visitor: JSONVisitor = { 228 | onObjectBegin: (offset: number) => { 229 | currentParent = onValue({ type: 'object', offset, length: -1, parent: currentParent, children: [] }); 230 | }, 231 | onObjectProperty: (name: string, offset: number, length: number) => { 232 | currentParent = onValue({ type: 'property', offset, length: -1, parent: currentParent, children: [] }); 233 | currentParent.children!.push({ type: 'string', value: name, offset, length, parent: currentParent }); 234 | }, 235 | onObjectEnd: (offset: number, length: number) => { 236 | ensurePropertyComplete(offset + length); // in case of a missing value for a property: make sure property is complete 237 | 238 | currentParent.length = offset + length - currentParent.offset; 239 | currentParent = currentParent.parent!; 240 | ensurePropertyComplete(offset + length); 241 | }, 242 | onArrayBegin: (offset: number, length: number) => { 243 | currentParent = onValue({ type: 'array', offset, length: -1, parent: currentParent, children: [] }); 244 | }, 245 | onArrayEnd: (offset: number, length: number) => { 246 | currentParent.length = offset + length - currentParent.offset; 247 | currentParent = currentParent.parent!; 248 | ensurePropertyComplete(offset + length); 249 | }, 250 | onLiteralValue: (value: any, offset: number, length: number) => { 251 | onValue({ type: getNodeType(value), offset, length, parent: currentParent, value }); 252 | ensurePropertyComplete(offset + length); 253 | }, 254 | onSeparator: (sep: string, offset: number, length: number) => { 255 | if (currentParent.type === 'property') { 256 | if (sep === ':') { 257 | currentParent.colonOffset = offset; 258 | } else if (sep === ',') { 259 | ensurePropertyComplete(offset); 260 | } 261 | } 262 | }, 263 | onError: (error: ParseErrorCode, offset: number, length: number) => { 264 | errors.push({ error, offset, length }); 265 | } 266 | }; 267 | visit(text, visitor, options); 268 | 269 | const result = currentParent.children![0]; 270 | if (result) { 271 | delete result.parent; 272 | } 273 | return result; 274 | } 275 | 276 | /** 277 | * Finds the node at the given path in a JSON DOM. 278 | */ 279 | export function findNodeAtLocation(root: Node | undefined, path: JSONPath): Node | undefined { 280 | if (!root) { 281 | return undefined; 282 | } 283 | let node = root; 284 | for (let segment of path) { 285 | if (typeof segment === 'string') { 286 | if (node.type !== 'object' || !Array.isArray(node.children)) { 287 | return undefined; 288 | } 289 | let found = false; 290 | for (const propertyNode of node.children) { 291 | if (Array.isArray(propertyNode.children) && propertyNode.children[0].value === segment && propertyNode.children.length === 2) { 292 | node = propertyNode.children[1]; 293 | found = true; 294 | break; 295 | } 296 | } 297 | if (!found) { 298 | return undefined; 299 | } 300 | } else { 301 | const index = segment; 302 | if (node.type !== 'array' || index < 0 || !Array.isArray(node.children) || index >= node.children.length) { 303 | return undefined; 304 | } 305 | node = node.children[index]; 306 | } 307 | } 308 | return node; 309 | } 310 | 311 | /** 312 | * Gets the JSON path of the given JSON DOM node 313 | */ 314 | export function getNodePath(node: Node): JSONPath { 315 | if (!node.parent || !node.parent.children) { 316 | return []; 317 | } 318 | const path = getNodePath(node.parent); 319 | if (node.parent.type === 'property') { 320 | const key = node.parent.children[0].value; 321 | path.push(key); 322 | } else if (node.parent.type === 'array') { 323 | const index = node.parent.children.indexOf(node); 324 | if (index !== -1) { 325 | path.push(index); 326 | } 327 | } 328 | return path; 329 | } 330 | 331 | /** 332 | * Evaluates the JavaScript object of the given JSON DOM node 333 | */ 334 | export function getNodeValue(node: Node): any { 335 | switch (node.type) { 336 | case 'array': 337 | return node.children!.map(getNodeValue); 338 | case 'object': 339 | const obj = Object.create(null); 340 | for (let prop of node.children!) { 341 | const valueNode = prop.children![1]; 342 | if (valueNode) { 343 | obj[prop.children![0].value] = getNodeValue(valueNode); 344 | } 345 | } 346 | return obj; 347 | case 'null': 348 | case 'string': 349 | case 'number': 350 | case 'boolean': 351 | return node.value; 352 | default: 353 | return undefined; 354 | } 355 | 356 | } 357 | 358 | export function contains(node: Node, offset: number, includeRightBound = false): boolean { 359 | return (offset >= node.offset && offset < (node.offset + node.length)) || includeRightBound && (offset === (node.offset + node.length)); 360 | } 361 | 362 | /** 363 | * Finds the most inner node at the given offset. If includeRightBound is set, also finds nodes that end at the given offset. 364 | */ 365 | export function findNodeAtOffset(node: Node, offset: number, includeRightBound = false): Node | undefined { 366 | if (contains(node, offset, includeRightBound)) { 367 | const children = node.children; 368 | if (Array.isArray(children)) { 369 | for (let i = 0; i < children.length && children[i].offset <= offset; i++) { 370 | const item = findNodeAtOffset(children[i], offset, includeRightBound); 371 | if (item) { 372 | return item; 373 | } 374 | } 375 | 376 | } 377 | return node; 378 | } 379 | return undefined; 380 | } 381 | 382 | 383 | /** 384 | * Parses the given text and invokes the visitor functions for each object, array and literal reached. 385 | */ 386 | export function visit(text: string, visitor: JSONVisitor, options: ParseOptions = ParseOptions.DEFAULT): any { 387 | 388 | const _scanner = createScanner(text, false); 389 | // Important: Only pass copies of this to visitor functions to prevent accidental modification, and 390 | // to not affect visitor functions which stored a reference to a previous JSONPath 391 | const _jsonPath: JSONPath = []; 392 | 393 | // Depth of onXXXBegin() callbacks suppressed. onXXXEnd() decrements this if it isn't 0 already. 394 | // Callbacks are only called when this value is 0. 395 | let suppressedCallbacks = 0; 396 | 397 | function toNoArgVisit(visitFunction?: (offset: number, length: number, startLine: number, startCharacter: number) => void): () => void { 398 | return visitFunction ? () => suppressedCallbacks === 0 && visitFunction(_scanner.getTokenOffset(), _scanner.getTokenLength(), _scanner.getTokenStartLine(), _scanner.getTokenStartCharacter()) : () => true; 399 | } 400 | function toOneArgVisit(visitFunction?: (arg: T, offset: number, length: number, startLine: number, startCharacter: number) => void): (arg: T) => void { 401 | return visitFunction ? (arg: T) => suppressedCallbacks === 0 && visitFunction(arg, _scanner.getTokenOffset(), _scanner.getTokenLength(), _scanner.getTokenStartLine(), _scanner.getTokenStartCharacter()) : () => true; 402 | } 403 | function toOneArgVisitWithPath(visitFunction?: (arg: T, offset: number, length: number, startLine: number, startCharacter: number, pathSupplier: () => JSONPath) => void): (arg: T) => void { 404 | return visitFunction ? (arg: T) => suppressedCallbacks === 0 && visitFunction(arg, _scanner.getTokenOffset(), _scanner.getTokenLength(), _scanner.getTokenStartLine(), _scanner.getTokenStartCharacter(), () => _jsonPath.slice()) : () => true; 405 | } 406 | function toBeginVisit(visitFunction?: (offset: number, length: number, startLine: number, startCharacter: number, pathSupplier: () => JSONPath) => boolean | void): () => void { 407 | return visitFunction ? 408 | () => { 409 | if (suppressedCallbacks > 0) { suppressedCallbacks++; } 410 | else { 411 | let cbReturn = visitFunction(_scanner.getTokenOffset(), _scanner.getTokenLength(), _scanner.getTokenStartLine(), _scanner.getTokenStartCharacter(), () => _jsonPath.slice()); 412 | if (cbReturn === false) { suppressedCallbacks = 1; } 413 | } 414 | } 415 | : () => true; 416 | } 417 | function toEndVisit(visitFunction?: (offset: number, length: number, startLine: number, startCharacter: number) => void): () => void { 418 | return visitFunction ? 419 | () => { 420 | if (suppressedCallbacks > 0) { suppressedCallbacks--; } 421 | if (suppressedCallbacks === 0) { visitFunction(_scanner.getTokenOffset(), _scanner.getTokenLength(), _scanner.getTokenStartLine(), _scanner.getTokenStartCharacter()); } 422 | } 423 | : () => true; 424 | } 425 | 426 | const onObjectBegin = toBeginVisit(visitor.onObjectBegin), 427 | onObjectProperty = toOneArgVisitWithPath(visitor.onObjectProperty), 428 | onObjectEnd = toEndVisit(visitor.onObjectEnd), 429 | onArrayBegin = toBeginVisit(visitor.onArrayBegin), 430 | onArrayEnd = toEndVisit(visitor.onArrayEnd), 431 | onLiteralValue = toOneArgVisitWithPath(visitor.onLiteralValue), 432 | onSeparator = toOneArgVisit(visitor.onSeparator), 433 | onComment = toNoArgVisit(visitor.onComment), 434 | onError = toOneArgVisit(visitor.onError); 435 | 436 | const disallowComments = options && options.disallowComments; 437 | const allowTrailingComma = options && options.allowTrailingComma; 438 | function scanNext(): SyntaxKind { 439 | while (true) { 440 | const token = _scanner.scan(); 441 | switch (_scanner.getTokenError()) { 442 | case ScanError.InvalidUnicode: 443 | handleError(ParseErrorCode.InvalidUnicode); 444 | break; 445 | case ScanError.InvalidEscapeCharacter: 446 | handleError(ParseErrorCode.InvalidEscapeCharacter); 447 | break; 448 | case ScanError.UnexpectedEndOfNumber: 449 | handleError(ParseErrorCode.UnexpectedEndOfNumber); 450 | break; 451 | case ScanError.UnexpectedEndOfComment: 452 | if (!disallowComments) { 453 | handleError(ParseErrorCode.UnexpectedEndOfComment); 454 | } 455 | break; 456 | case ScanError.UnexpectedEndOfString: 457 | handleError(ParseErrorCode.UnexpectedEndOfString); 458 | break; 459 | case ScanError.InvalidCharacter: 460 | handleError(ParseErrorCode.InvalidCharacter); 461 | break; 462 | } 463 | switch (token) { 464 | case SyntaxKind.LineCommentTrivia: 465 | case SyntaxKind.BlockCommentTrivia: 466 | if (disallowComments) { 467 | handleError(ParseErrorCode.InvalidCommentToken); 468 | } else { 469 | onComment(); 470 | } 471 | break; 472 | case SyntaxKind.Unknown: 473 | handleError(ParseErrorCode.InvalidSymbol); 474 | break; 475 | case SyntaxKind.Trivia: 476 | case SyntaxKind.LineBreakTrivia: 477 | break; 478 | default: 479 | return token; 480 | } 481 | } 482 | } 483 | 484 | function handleError(error: ParseErrorCode, skipUntilAfter: SyntaxKind[] = [], skipUntil: SyntaxKind[] = []): void { 485 | onError(error); 486 | if (skipUntilAfter.length + skipUntil.length > 0) { 487 | let token = _scanner.getToken(); 488 | while (token !== SyntaxKind.EOF) { 489 | if (skipUntilAfter.indexOf(token) !== -1) { 490 | scanNext(); 491 | break; 492 | } else if (skipUntil.indexOf(token) !== -1) { 493 | break; 494 | } 495 | token = scanNext(); 496 | } 497 | } 498 | } 499 | 500 | function parseString(isValue: boolean): boolean { 501 | const value = _scanner.getTokenValue(); 502 | if (isValue) { 503 | onLiteralValue(value); 504 | } else { 505 | onObjectProperty(value); 506 | // add property name afterwards 507 | _jsonPath.push(value); 508 | } 509 | scanNext(); 510 | return true; 511 | } 512 | 513 | function parseLiteral(): boolean { 514 | switch (_scanner.getToken()) { 515 | case SyntaxKind.NumericLiteral: 516 | const tokenValue = _scanner.getTokenValue(); 517 | let value = Number(tokenValue); 518 | 519 | if (isNaN(value)) { 520 | handleError(ParseErrorCode.InvalidNumberFormat); 521 | value = 0; 522 | } 523 | 524 | onLiteralValue(value); 525 | break; 526 | case SyntaxKind.NullKeyword: 527 | onLiteralValue(null); 528 | break; 529 | case SyntaxKind.TrueKeyword: 530 | onLiteralValue(true); 531 | break; 532 | case SyntaxKind.FalseKeyword: 533 | onLiteralValue(false); 534 | break; 535 | default: 536 | return false; 537 | } 538 | scanNext(); 539 | return true; 540 | } 541 | 542 | function parseProperty(): boolean { 543 | if (_scanner.getToken() !== SyntaxKind.StringLiteral) { 544 | handleError(ParseErrorCode.PropertyNameExpected, [], [SyntaxKind.CloseBraceToken, SyntaxKind.CommaToken]); 545 | return false; 546 | } 547 | parseString(false); 548 | if (_scanner.getToken() === SyntaxKind.ColonToken) { 549 | onSeparator(':'); 550 | scanNext(); // consume colon 551 | 552 | if (!parseValue()) { 553 | handleError(ParseErrorCode.ValueExpected, [], [SyntaxKind.CloseBraceToken, SyntaxKind.CommaToken]); 554 | } 555 | } else { 556 | handleError(ParseErrorCode.ColonExpected, [], [SyntaxKind.CloseBraceToken, SyntaxKind.CommaToken]); 557 | } 558 | _jsonPath.pop(); // remove processed property name 559 | return true; 560 | } 561 | 562 | function parseObject(): boolean { 563 | onObjectBegin(); 564 | scanNext(); // consume open brace 565 | 566 | let needsComma = false; 567 | while (_scanner.getToken() !== SyntaxKind.CloseBraceToken && _scanner.getToken() !== SyntaxKind.EOF) { 568 | if (_scanner.getToken() === SyntaxKind.CommaToken) { 569 | if (!needsComma) { 570 | handleError(ParseErrorCode.ValueExpected, [], []); 571 | } 572 | onSeparator(','); 573 | scanNext(); // consume comma 574 | if (_scanner.getToken() === SyntaxKind.CloseBraceToken && allowTrailingComma) { 575 | break; 576 | } 577 | } else if (needsComma) { 578 | handleError(ParseErrorCode.CommaExpected, [], []); 579 | } 580 | if (!parseProperty()) { 581 | handleError(ParseErrorCode.ValueExpected, [], [SyntaxKind.CloseBraceToken, SyntaxKind.CommaToken]); 582 | } 583 | needsComma = true; 584 | } 585 | onObjectEnd(); 586 | if (_scanner.getToken() !== SyntaxKind.CloseBraceToken) { 587 | handleError(ParseErrorCode.CloseBraceExpected, [SyntaxKind.CloseBraceToken], []); 588 | } else { 589 | scanNext(); // consume close brace 590 | } 591 | return true; 592 | } 593 | 594 | function parseArray(): boolean { 595 | onArrayBegin(); 596 | scanNext(); // consume open bracket 597 | let isFirstElement = true; 598 | 599 | let needsComma = false; 600 | while (_scanner.getToken() !== SyntaxKind.CloseBracketToken && _scanner.getToken() !== SyntaxKind.EOF) { 601 | if (_scanner.getToken() === SyntaxKind.CommaToken) { 602 | if (!needsComma) { 603 | handleError(ParseErrorCode.ValueExpected, [], []); 604 | } 605 | onSeparator(','); 606 | scanNext(); // consume comma 607 | if (_scanner.getToken() === SyntaxKind.CloseBracketToken && allowTrailingComma) { 608 | break; 609 | } 610 | } else if (needsComma) { 611 | handleError(ParseErrorCode.CommaExpected, [], []); 612 | } 613 | if (isFirstElement) { 614 | _jsonPath.push(0); 615 | isFirstElement = false; 616 | } else { 617 | (_jsonPath[_jsonPath.length - 1] as number)++; 618 | } 619 | if (!parseValue()) { 620 | handleError(ParseErrorCode.ValueExpected, [], [SyntaxKind.CloseBracketToken, SyntaxKind.CommaToken]); 621 | } 622 | needsComma = true; 623 | } 624 | onArrayEnd(); 625 | if (!isFirstElement) { 626 | _jsonPath.pop(); // remove array index 627 | } 628 | if (_scanner.getToken() !== SyntaxKind.CloseBracketToken) { 629 | handleError(ParseErrorCode.CloseBracketExpected, [SyntaxKind.CloseBracketToken], []); 630 | } else { 631 | scanNext(); // consume close bracket 632 | } 633 | return true; 634 | } 635 | 636 | function parseValue(): boolean { 637 | switch (_scanner.getToken()) { 638 | case SyntaxKind.OpenBracketToken: 639 | return parseArray(); 640 | case SyntaxKind.OpenBraceToken: 641 | return parseObject(); 642 | case SyntaxKind.StringLiteral: 643 | return parseString(true); 644 | default: 645 | return parseLiteral(); 646 | } 647 | } 648 | 649 | scanNext(); 650 | if (_scanner.getToken() === SyntaxKind.EOF) { 651 | if (options.allowEmptyContent) { 652 | return true; 653 | } 654 | handleError(ParseErrorCode.ValueExpected, [], []); 655 | return false; 656 | } 657 | if (!parseValue()) { 658 | handleError(ParseErrorCode.ValueExpected, [], []); 659 | return false; 660 | } 661 | if (_scanner.getToken() !== SyntaxKind.EOF) { 662 | handleError(ParseErrorCode.EndOfFileExpected, [], []); 663 | } 664 | return true; 665 | } 666 | 667 | /** 668 | * Takes JSON with JavaScript-style comments and remove 669 | * them. Optionally replaces every none-newline character 670 | * of comments with a replaceCharacter 671 | */ 672 | export function stripComments(text: string, replaceCh?: string): string { 673 | 674 | let _scanner = createScanner(text), 675 | parts: string[] = [], 676 | kind: SyntaxKind, 677 | offset = 0, 678 | pos: number; 679 | 680 | do { 681 | pos = _scanner.getPosition(); 682 | kind = _scanner.scan(); 683 | switch (kind) { 684 | case SyntaxKind.LineCommentTrivia: 685 | case SyntaxKind.BlockCommentTrivia: 686 | case SyntaxKind.EOF: 687 | if (offset !== pos) { 688 | parts.push(text.substring(offset, pos)); 689 | } 690 | if (replaceCh !== undefined) { 691 | parts.push(_scanner.getTokenValue().replace(/[^\r\n]/g, replaceCh)); 692 | } 693 | offset = _scanner.getPosition(); 694 | break; 695 | } 696 | } while (kind !== SyntaxKind.EOF); 697 | 698 | return parts.join(''); 699 | } 700 | 701 | export function getNodeType(value: any): NodeType { 702 | switch (typeof value) { 703 | case 'boolean': return 'boolean'; 704 | case 'number': return 'number'; 705 | case 'string': return 'string'; 706 | case 'object': { 707 | if (!value) { 708 | return 'null'; 709 | } else if (Array.isArray(value)) { 710 | return 'array'; 711 | } 712 | return 'object'; 713 | } 714 | default: return 'null'; 715 | } 716 | } 717 | -------------------------------------------------------------------------------- /src/impl/scanner.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 'use strict'; 6 | 7 | import { ScanError, SyntaxKind, JSONScanner } from '../main'; 8 | 9 | /** 10 | * Creates a JSON scanner on the given text. 11 | * If ignoreTrivia is set, whitespaces or comments are ignored. 12 | */ 13 | export function createScanner(text: string, ignoreTrivia: boolean = false): JSONScanner { 14 | 15 | const len = text.length; 16 | let pos = 0, 17 | value: string = '', 18 | tokenOffset = 0, 19 | token: SyntaxKind = SyntaxKind.Unknown, 20 | lineNumber = 0, 21 | lineStartOffset = 0, 22 | tokenLineStartOffset = 0, 23 | prevTokenLineStartOffset = 0, 24 | scanError: ScanError = ScanError.None; 25 | 26 | function scanHexDigits(count: number, exact?: boolean): number { 27 | let digits = 0; 28 | let value = 0; 29 | while (digits < count || !exact) { 30 | let ch = text.charCodeAt(pos); 31 | if (ch >= CharacterCodes._0 && ch <= CharacterCodes._9) { 32 | value = value * 16 + ch - CharacterCodes._0; 33 | } 34 | else if (ch >= CharacterCodes.A && ch <= CharacterCodes.F) { 35 | value = value * 16 + ch - CharacterCodes.A + 10; 36 | } 37 | else if (ch >= CharacterCodes.a && ch <= CharacterCodes.f) { 38 | value = value * 16 + ch - CharacterCodes.a + 10; 39 | } 40 | else { 41 | break; 42 | } 43 | pos++; 44 | digits++; 45 | } 46 | if (digits < count) { 47 | value = -1; 48 | } 49 | return value; 50 | } 51 | 52 | function setPosition(newPosition: number) { 53 | pos = newPosition; 54 | value = ''; 55 | tokenOffset = 0; 56 | token = SyntaxKind.Unknown; 57 | scanError = ScanError.None; 58 | } 59 | 60 | function scanNumber(): string { 61 | let start = pos; 62 | if (text.charCodeAt(pos) === CharacterCodes._0) { 63 | pos++; 64 | } else { 65 | pos++; 66 | while (pos < text.length && isDigit(text.charCodeAt(pos))) { 67 | pos++; 68 | } 69 | } 70 | if (pos < text.length && text.charCodeAt(pos) === CharacterCodes.dot) { 71 | pos++; 72 | if (pos < text.length && isDigit(text.charCodeAt(pos))) { 73 | pos++; 74 | while (pos < text.length && isDigit(text.charCodeAt(pos))) { 75 | pos++; 76 | } 77 | } else { 78 | scanError = ScanError.UnexpectedEndOfNumber; 79 | return text.substring(start, pos); 80 | } 81 | } 82 | let end = pos; 83 | if (pos < text.length && (text.charCodeAt(pos) === CharacterCodes.E || text.charCodeAt(pos) === CharacterCodes.e)) { 84 | pos++; 85 | if (pos < text.length && text.charCodeAt(pos) === CharacterCodes.plus || text.charCodeAt(pos) === CharacterCodes.minus) { 86 | pos++; 87 | } 88 | if (pos < text.length && isDigit(text.charCodeAt(pos))) { 89 | pos++; 90 | while (pos < text.length && isDigit(text.charCodeAt(pos))) { 91 | pos++; 92 | } 93 | end = pos; 94 | } else { 95 | scanError = ScanError.UnexpectedEndOfNumber; 96 | } 97 | } 98 | return text.substring(start, end); 99 | } 100 | 101 | function scanString(): string { 102 | 103 | let result = '', 104 | start = pos; 105 | 106 | while (true) { 107 | if (pos >= len) { 108 | result += text.substring(start, pos); 109 | scanError = ScanError.UnexpectedEndOfString; 110 | break; 111 | } 112 | const ch = text.charCodeAt(pos); 113 | if (ch === CharacterCodes.doubleQuote) { 114 | result += text.substring(start, pos); 115 | pos++; 116 | break; 117 | } 118 | if (ch === CharacterCodes.backslash) { 119 | result += text.substring(start, pos); 120 | pos++; 121 | if (pos >= len) { 122 | scanError = ScanError.UnexpectedEndOfString; 123 | break; 124 | } 125 | const ch2 = text.charCodeAt(pos++); 126 | switch (ch2) { 127 | case CharacterCodes.doubleQuote: 128 | result += '\"'; 129 | break; 130 | case CharacterCodes.backslash: 131 | result += '\\'; 132 | break; 133 | case CharacterCodes.slash: 134 | result += '/'; 135 | break; 136 | case CharacterCodes.b: 137 | result += '\b'; 138 | break; 139 | case CharacterCodes.f: 140 | result += '\f'; 141 | break; 142 | case CharacterCodes.n: 143 | result += '\n'; 144 | break; 145 | case CharacterCodes.r: 146 | result += '\r'; 147 | break; 148 | case CharacterCodes.t: 149 | result += '\t'; 150 | break; 151 | case CharacterCodes.u: 152 | const ch3 = scanHexDigits(4, true); 153 | if (ch3 >= 0) { 154 | result += String.fromCharCode(ch3); 155 | } else { 156 | scanError = ScanError.InvalidUnicode; 157 | } 158 | break; 159 | default: 160 | scanError = ScanError.InvalidEscapeCharacter; 161 | } 162 | start = pos; 163 | continue; 164 | } 165 | if (ch >= 0 && ch <= 0x1f) { 166 | if (isLineBreak(ch)) { 167 | result += text.substring(start, pos); 168 | scanError = ScanError.UnexpectedEndOfString; 169 | break; 170 | } else { 171 | scanError = ScanError.InvalidCharacter; 172 | // mark as error but continue with string 173 | } 174 | } 175 | pos++; 176 | } 177 | return result; 178 | } 179 | 180 | function scanNext(): SyntaxKind { 181 | 182 | value = ''; 183 | scanError = ScanError.None; 184 | 185 | tokenOffset = pos; 186 | lineStartOffset = lineNumber; 187 | prevTokenLineStartOffset = tokenLineStartOffset; 188 | 189 | if (pos >= len) { 190 | // at the end 191 | tokenOffset = len; 192 | return token = SyntaxKind.EOF; 193 | } 194 | 195 | let code = text.charCodeAt(pos); 196 | // trivia: whitespace 197 | if (isWhiteSpace(code)) { 198 | do { 199 | pos++; 200 | value += String.fromCharCode(code); 201 | code = text.charCodeAt(pos); 202 | } while (isWhiteSpace(code)); 203 | 204 | return token = SyntaxKind.Trivia; 205 | } 206 | 207 | // trivia: newlines 208 | if (isLineBreak(code)) { 209 | pos++; 210 | value += String.fromCharCode(code); 211 | if (code === CharacterCodes.carriageReturn && text.charCodeAt(pos) === CharacterCodes.lineFeed) { 212 | pos++; 213 | value += '\n'; 214 | } 215 | lineNumber++; 216 | tokenLineStartOffset = pos; 217 | return token = SyntaxKind.LineBreakTrivia; 218 | } 219 | 220 | switch (code) { 221 | // tokens: []{}:, 222 | case CharacterCodes.openBrace: 223 | pos++; 224 | return token = SyntaxKind.OpenBraceToken; 225 | case CharacterCodes.closeBrace: 226 | pos++; 227 | return token = SyntaxKind.CloseBraceToken; 228 | case CharacterCodes.openBracket: 229 | pos++; 230 | return token = SyntaxKind.OpenBracketToken; 231 | case CharacterCodes.closeBracket: 232 | pos++; 233 | return token = SyntaxKind.CloseBracketToken; 234 | case CharacterCodes.colon: 235 | pos++; 236 | return token = SyntaxKind.ColonToken; 237 | case CharacterCodes.comma: 238 | pos++; 239 | return token = SyntaxKind.CommaToken; 240 | 241 | // strings 242 | case CharacterCodes.doubleQuote: 243 | pos++; 244 | value = scanString(); 245 | return token = SyntaxKind.StringLiteral; 246 | 247 | // comments 248 | case CharacterCodes.slash: 249 | const start = pos - 1; 250 | // Single-line comment 251 | if (text.charCodeAt(pos + 1) === CharacterCodes.slash) { 252 | pos += 2; 253 | 254 | while (pos < len) { 255 | if (isLineBreak(text.charCodeAt(pos))) { 256 | break; 257 | } 258 | pos++; 259 | 260 | } 261 | value = text.substring(start, pos); 262 | return token = SyntaxKind.LineCommentTrivia; 263 | } 264 | 265 | // Multi-line comment 266 | if (text.charCodeAt(pos + 1) === CharacterCodes.asterisk) { 267 | pos += 2; 268 | 269 | const safeLength = len - 1; // For lookahead. 270 | let commentClosed = false; 271 | while (pos < safeLength) { 272 | const ch = text.charCodeAt(pos); 273 | 274 | if (ch === CharacterCodes.asterisk && text.charCodeAt(pos + 1) === CharacterCodes.slash) { 275 | pos += 2; 276 | commentClosed = true; 277 | break; 278 | } 279 | 280 | pos++; 281 | 282 | if (isLineBreak(ch)) { 283 | if (ch === CharacterCodes.carriageReturn && text.charCodeAt(pos) === CharacterCodes.lineFeed) { 284 | pos++; 285 | } 286 | 287 | lineNumber++; 288 | tokenLineStartOffset = pos; 289 | } 290 | } 291 | 292 | if (!commentClosed) { 293 | pos++; 294 | scanError = ScanError.UnexpectedEndOfComment; 295 | } 296 | 297 | value = text.substring(start, pos); 298 | return token = SyntaxKind.BlockCommentTrivia; 299 | } 300 | // just a single slash 301 | value += String.fromCharCode(code); 302 | pos++; 303 | return token = SyntaxKind.Unknown; 304 | 305 | // numbers 306 | case CharacterCodes.minus: 307 | value += String.fromCharCode(code); 308 | pos++; 309 | if (pos === len || !isDigit(text.charCodeAt(pos))) { 310 | return token = SyntaxKind.Unknown; 311 | } 312 | // found a minus, followed by a number so 313 | // we fall through to proceed with scanning 314 | // numbers 315 | case CharacterCodes._0: 316 | case CharacterCodes._1: 317 | case CharacterCodes._2: 318 | case CharacterCodes._3: 319 | case CharacterCodes._4: 320 | case CharacterCodes._5: 321 | case CharacterCodes._6: 322 | case CharacterCodes._7: 323 | case CharacterCodes._8: 324 | case CharacterCodes._9: 325 | value += scanNumber(); 326 | return token = SyntaxKind.NumericLiteral; 327 | // literals and unknown symbols 328 | default: 329 | // is a literal? Read the full word. 330 | while (pos < len && isUnknownContentCharacter(code)) { 331 | pos++; 332 | code = text.charCodeAt(pos); 333 | } 334 | if (tokenOffset !== pos) { 335 | value = text.substring(tokenOffset, pos); 336 | // keywords: true, false, null 337 | switch (value) { 338 | case 'true': return token = SyntaxKind.TrueKeyword; 339 | case 'false': return token = SyntaxKind.FalseKeyword; 340 | case 'null': return token = SyntaxKind.NullKeyword; 341 | } 342 | return token = SyntaxKind.Unknown; 343 | } 344 | // some 345 | value += String.fromCharCode(code); 346 | pos++; 347 | return token = SyntaxKind.Unknown; 348 | } 349 | } 350 | 351 | function isUnknownContentCharacter(code: CharacterCodes) { 352 | if (isWhiteSpace(code) || isLineBreak(code)) { 353 | return false; 354 | } 355 | switch (code) { 356 | case CharacterCodes.closeBrace: 357 | case CharacterCodes.closeBracket: 358 | case CharacterCodes.openBrace: 359 | case CharacterCodes.openBracket: 360 | case CharacterCodes.doubleQuote: 361 | case CharacterCodes.colon: 362 | case CharacterCodes.comma: 363 | case CharacterCodes.slash: 364 | return false; 365 | } 366 | return true; 367 | } 368 | 369 | 370 | function scanNextNonTrivia(): SyntaxKind { 371 | let result: SyntaxKind; 372 | do { 373 | result = scanNext(); 374 | } while (result >= SyntaxKind.LineCommentTrivia && result <= SyntaxKind.Trivia); 375 | return result; 376 | } 377 | 378 | return { 379 | setPosition: setPosition, 380 | getPosition: () => pos, 381 | scan: ignoreTrivia ? scanNextNonTrivia : scanNext, 382 | getToken: () => token, 383 | getTokenValue: () => value, 384 | getTokenOffset: () => tokenOffset, 385 | getTokenLength: () => pos - tokenOffset, 386 | getTokenStartLine: () => lineStartOffset, 387 | getTokenStartCharacter: () => tokenOffset - prevTokenLineStartOffset, 388 | getTokenError: () => scanError, 389 | }; 390 | } 391 | 392 | function isWhiteSpace(ch: number): boolean { 393 | return ch === CharacterCodes.space || ch === CharacterCodes.tab; 394 | } 395 | 396 | function isLineBreak(ch: number): boolean { 397 | return ch === CharacterCodes.lineFeed || ch === CharacterCodes.carriageReturn; 398 | } 399 | 400 | function isDigit(ch: number): boolean { 401 | return ch >= CharacterCodes._0 && ch <= CharacterCodes._9; 402 | } 403 | 404 | const enum CharacterCodes { 405 | lineFeed = 0x0A, // \n 406 | carriageReturn = 0x0D, // \r 407 | 408 | space = 0x0020, // " " 409 | 410 | _0 = 0x30, 411 | _1 = 0x31, 412 | _2 = 0x32, 413 | _3 = 0x33, 414 | _4 = 0x34, 415 | _5 = 0x35, 416 | _6 = 0x36, 417 | _7 = 0x37, 418 | _8 = 0x38, 419 | _9 = 0x39, 420 | 421 | a = 0x61, 422 | b = 0x62, 423 | c = 0x63, 424 | d = 0x64, 425 | e = 0x65, 426 | f = 0x66, 427 | g = 0x67, 428 | h = 0x68, 429 | i = 0x69, 430 | j = 0x6A, 431 | k = 0x6B, 432 | l = 0x6C, 433 | m = 0x6D, 434 | n = 0x6E, 435 | o = 0x6F, 436 | p = 0x70, 437 | q = 0x71, 438 | r = 0x72, 439 | s = 0x73, 440 | t = 0x74, 441 | u = 0x75, 442 | v = 0x76, 443 | w = 0x77, 444 | x = 0x78, 445 | y = 0x79, 446 | z = 0x7A, 447 | 448 | A = 0x41, 449 | B = 0x42, 450 | C = 0x43, 451 | D = 0x44, 452 | E = 0x45, 453 | F = 0x46, 454 | G = 0x47, 455 | H = 0x48, 456 | I = 0x49, 457 | J = 0x4A, 458 | K = 0x4B, 459 | L = 0x4C, 460 | M = 0x4D, 461 | N = 0x4E, 462 | O = 0x4F, 463 | P = 0x50, 464 | Q = 0x51, 465 | R = 0x52, 466 | S = 0x53, 467 | T = 0x54, 468 | U = 0x55, 469 | V = 0x56, 470 | W = 0x57, 471 | X = 0x58, 472 | Y = 0x59, 473 | Z = 0x5a, 474 | 475 | asterisk = 0x2A, // * 476 | backslash = 0x5C, // \ 477 | closeBrace = 0x7D, // } 478 | closeBracket = 0x5D, // ] 479 | colon = 0x3A, // : 480 | comma = 0x2C, // , 481 | dot = 0x2E, // . 482 | doubleQuote = 0x22, // " 483 | minus = 0x2D, // - 484 | openBrace = 0x7B, // { 485 | openBracket = 0x5B, // [ 486 | plus = 0x2B, // + 487 | slash = 0x2F, // / 488 | 489 | formFeed = 0x0C, // \f 490 | tab = 0x09, // \t 491 | } 492 | -------------------------------------------------------------------------------- /src/impl/string-intern.ts: -------------------------------------------------------------------------------- 1 | export const cachedSpaces = new Array(20).fill(0).map((_, index) => { 2 | return ' '.repeat(index); 3 | }); 4 | 5 | const maxCachedValues = 200; 6 | 7 | export const cachedBreakLinesWithSpaces = { 8 | ' ': { 9 | '\n': new Array(maxCachedValues).fill(0).map((_, index) => { 10 | return '\n' + ' '.repeat(index); 11 | }), 12 | '\r': new Array(maxCachedValues).fill(0).map((_, index) => { 13 | return '\r' + ' '.repeat(index); 14 | }), 15 | '\r\n': new Array(maxCachedValues).fill(0).map((_, index) => { 16 | return '\r\n' + ' '.repeat(index); 17 | }), 18 | }, 19 | '\t': { 20 | '\n': new Array(maxCachedValues).fill(0).map((_, index) => { 21 | return '\n' + '\t'.repeat(index); 22 | }), 23 | '\r': new Array(maxCachedValues).fill(0).map((_, index) => { 24 | return '\r' + '\t'.repeat(index); 25 | }), 26 | '\r\n': new Array(maxCachedValues).fill(0).map((_, index) => { 27 | return '\r\n' + '\t'.repeat(index); 28 | }), 29 | } 30 | }; 31 | 32 | export const supportedEols = ['\n', '\r', '\r\n'] as const; 33 | export type SupportedEOL = '\n' | '\r' | '\r\n'; 34 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 'use strict'; 6 | 7 | import * as formatter from './impl/format'; 8 | import * as edit from './impl/edit'; 9 | import * as scanner from './impl/scanner'; 10 | import * as parser from './impl/parser'; 11 | 12 | /** 13 | * Creates a JSON scanner on the given text. 14 | * If ignoreTrivia is set, whitespaces or comments are ignored. 15 | */ 16 | export const createScanner: (text: string, ignoreTrivia?: boolean) => JSONScanner = scanner.createScanner; 17 | 18 | export const enum ScanError { 19 | None = 0, 20 | UnexpectedEndOfComment = 1, 21 | UnexpectedEndOfString = 2, 22 | UnexpectedEndOfNumber = 3, 23 | InvalidUnicode = 4, 24 | InvalidEscapeCharacter = 5, 25 | InvalidCharacter = 6 26 | } 27 | 28 | export const enum SyntaxKind { 29 | OpenBraceToken = 1, 30 | CloseBraceToken = 2, 31 | OpenBracketToken = 3, 32 | CloseBracketToken = 4, 33 | CommaToken = 5, 34 | ColonToken = 6, 35 | NullKeyword = 7, 36 | TrueKeyword = 8, 37 | FalseKeyword = 9, 38 | StringLiteral = 10, 39 | NumericLiteral = 11, 40 | LineCommentTrivia = 12, 41 | BlockCommentTrivia = 13, 42 | LineBreakTrivia = 14, 43 | Trivia = 15, 44 | Unknown = 16, 45 | EOF = 17 46 | } 47 | 48 | /** 49 | * The scanner object, representing a JSON scanner at a position in the input string. 50 | */ 51 | export interface JSONScanner { 52 | /** 53 | * Sets the scan position to a new offset. A call to 'scan' is needed to get the first token. 54 | */ 55 | setPosition(pos: number): void; 56 | /** 57 | * Read the next token. Returns the token code. 58 | */ 59 | scan(): SyntaxKind; 60 | /** 61 | * Returns the zero-based current scan position, which is after the last read token. 62 | */ 63 | getPosition(): number; 64 | /** 65 | * Returns the last read token. 66 | */ 67 | getToken(): SyntaxKind; 68 | /** 69 | * Returns the last read token value. The value for strings is the decoded string content. For numbers it's of type number, for boolean it's true or false. 70 | */ 71 | getTokenValue(): string; 72 | /** 73 | * The zero-based start offset of the last read token. 74 | */ 75 | getTokenOffset(): number; 76 | /** 77 | * The length of the last read token. 78 | */ 79 | getTokenLength(): number; 80 | /** 81 | * The zero-based start line number of the last read token. 82 | */ 83 | getTokenStartLine(): number; 84 | /** 85 | * The zero-based start character (column) of the last read token. 86 | */ 87 | getTokenStartCharacter(): number; 88 | /** 89 | * An error code of the last scan. 90 | */ 91 | getTokenError(): ScanError; 92 | } 93 | 94 | 95 | /** 96 | * For a given offset, evaluate the location in the JSON document. Each segment in the location path is either a property name or an array index. 97 | */ 98 | export const getLocation: (text: string, position: number) => Location = parser.getLocation; 99 | 100 | /** 101 | * Parses the given text and returns the object the JSON content represents. On invalid input, the parser tries to be as fault tolerant as possible, but still return a result. 102 | * Therefore, always check the errors list to find out if the input was valid. 103 | */ 104 | export const parse: (text: string, errors?: ParseError[], options?: ParseOptions) => any = parser.parse; 105 | 106 | /** 107 | * Parses the given text and returns a tree representation the JSON content. On invalid input, the parser tries to be as fault tolerant as possible, but still return a result. 108 | */ 109 | export const parseTree: (text: string, errors?: ParseError[], options?: ParseOptions) => Node | undefined = parser.parseTree; 110 | 111 | /** 112 | * Finds the node at the given path in a JSON DOM. 113 | */ 114 | export const findNodeAtLocation: (root: Node, path: JSONPath) => Node | undefined = parser.findNodeAtLocation; 115 | 116 | /** 117 | * Finds the innermost node at the given offset. If includeRightBound is set, also finds nodes that end at the given offset. 118 | */ 119 | export const findNodeAtOffset: (root: Node, offset: number, includeRightBound?: boolean) => Node | undefined = parser.findNodeAtOffset; 120 | 121 | /** 122 | * Gets the JSON path of the given JSON DOM node 123 | */ 124 | export const getNodePath: (node: Node) => JSONPath = parser.getNodePath; 125 | 126 | /** 127 | * Evaluates the JavaScript object of the given JSON DOM node 128 | */ 129 | export const getNodeValue: (node: Node) => any = parser.getNodeValue; 130 | 131 | /** 132 | * Parses the given text and invokes the visitor functions for each object, array and literal reached. 133 | */ 134 | export const visit: (text: string, visitor: JSONVisitor, options?: ParseOptions) => any = parser.visit; 135 | 136 | /** 137 | * Takes JSON with JavaScript-style comments and remove 138 | * them. Optionally replaces every none-newline character 139 | * of comments with a replaceCharacter 140 | */ 141 | export const stripComments: (text: string, replaceCh?: string) => string = parser.stripComments; 142 | 143 | export interface ParseError { 144 | error: ParseErrorCode; 145 | offset: number; 146 | length: number; 147 | } 148 | 149 | export const enum ParseErrorCode { 150 | InvalidSymbol = 1, 151 | InvalidNumberFormat = 2, 152 | PropertyNameExpected = 3, 153 | ValueExpected = 4, 154 | ColonExpected = 5, 155 | CommaExpected = 6, 156 | CloseBraceExpected = 7, 157 | CloseBracketExpected = 8, 158 | EndOfFileExpected = 9, 159 | InvalidCommentToken = 10, 160 | UnexpectedEndOfComment = 11, 161 | UnexpectedEndOfString = 12, 162 | UnexpectedEndOfNumber = 13, 163 | InvalidUnicode = 14, 164 | InvalidEscapeCharacter = 15, 165 | InvalidCharacter = 16 166 | } 167 | 168 | export function printParseErrorCode(code: ParseErrorCode) { 169 | switch (code) { 170 | case ParseErrorCode.InvalidSymbol: return 'InvalidSymbol'; case ParseErrorCode.InvalidNumberFormat: return 'InvalidNumberFormat'; 171 | case ParseErrorCode.PropertyNameExpected: return 'PropertyNameExpected'; 172 | case ParseErrorCode.ValueExpected: return 'ValueExpected'; 173 | case ParseErrorCode.ColonExpected: return 'ColonExpected'; 174 | case ParseErrorCode.CommaExpected: return 'CommaExpected'; 175 | case ParseErrorCode.CloseBraceExpected: return 'CloseBraceExpected'; 176 | case ParseErrorCode.CloseBracketExpected: return 'CloseBracketExpected'; 177 | case ParseErrorCode.EndOfFileExpected: return 'EndOfFileExpected'; 178 | case ParseErrorCode.InvalidCommentToken: return 'InvalidCommentToken'; 179 | case ParseErrorCode.UnexpectedEndOfComment: return 'UnexpectedEndOfComment'; 180 | case ParseErrorCode.UnexpectedEndOfString: return 'UnexpectedEndOfString'; 181 | case ParseErrorCode.UnexpectedEndOfNumber: return 'UnexpectedEndOfNumber'; 182 | case ParseErrorCode.InvalidUnicode: return 'InvalidUnicode'; 183 | case ParseErrorCode.InvalidEscapeCharacter: return 'InvalidEscapeCharacter'; 184 | case ParseErrorCode.InvalidCharacter: return 'InvalidCharacter'; 185 | } 186 | return ''; 187 | } 188 | 189 | export type NodeType = 'object' | 'array' | 'property' | 'string' | 'number' | 'boolean' | 'null'; 190 | 191 | export interface Node { 192 | readonly type: NodeType; 193 | readonly value?: any; 194 | readonly offset: number; 195 | readonly length: number; 196 | readonly colonOffset?: number; 197 | readonly parent?: Node; 198 | readonly children?: Node[]; 199 | } 200 | 201 | /** 202 | * A {@linkcode JSONPath} segment. Either a string representing an object property name 203 | * or a number (starting at 0) for array indices. 204 | */ 205 | export type Segment = string | number; 206 | export type JSONPath = Segment[]; 207 | 208 | export interface Location { 209 | /** 210 | * The previous property key or literal value (string, number, boolean or null) or undefined. 211 | */ 212 | previousNode?: Node; 213 | /** 214 | * The path describing the location in the JSON document. The path consists of a sequence of strings 215 | * representing an object property or numbers for array indices. 216 | */ 217 | path: JSONPath; 218 | /** 219 | * Matches the locations path against a pattern consisting of strings (for properties) and numbers (for array indices). 220 | * '*' will match a single segment of any property name or index. 221 | * '**' will match a sequence of segments of any property name or index, or no segment. 222 | */ 223 | matches: (patterns: JSONPath) => boolean; 224 | /** 225 | * If set, the location's offset is at a property key. 226 | */ 227 | isAtPropertyKey: boolean; 228 | } 229 | 230 | export interface ParseOptions { 231 | disallowComments?: boolean; 232 | allowTrailingComma?: boolean; 233 | allowEmptyContent?: boolean; 234 | } 235 | 236 | /** 237 | * Visitor called by {@linkcode visit} when parsing JSON. 238 | * 239 | * The visitor functions have the following common parameters: 240 | * - `offset`: Global offset within the JSON document, starting at 0 241 | * - `startLine`: Line number, starting at 0 242 | * - `startCharacter`: Start character (column) within the current line, starting at 0 243 | * 244 | * Additionally some functions have a `pathSupplier` parameter which can be used to obtain the 245 | * current `JSONPath` within the document. 246 | */ 247 | export interface JSONVisitor { 248 | /** 249 | * Invoked when an open brace is encountered and an object is started. The offset and length represent the location of the open brace. 250 | * When `false` is returned, the object properties will not be visited. 251 | */ 252 | onObjectBegin?: (offset: number, length: number, startLine: number, startCharacter: number, pathSupplier: () => JSONPath) => boolean | void; 253 | 254 | /** 255 | * Invoked when a property is encountered. The offset and length represent the location of the property name. 256 | * The `JSONPath` created by the `pathSupplier` refers to the enclosing JSON object, it does not include the 257 | * property name yet. 258 | */ 259 | onObjectProperty?: (property: string, offset: number, length: number, startLine: number, startCharacter: number, pathSupplier: () => JSONPath) => void; 260 | 261 | /** 262 | * Invoked when a closing brace is encountered and an object is completed. The offset and length represent the location of the closing brace. 263 | */ 264 | onObjectEnd?: (offset: number, length: number, startLine: number, startCharacter: number) => void; 265 | 266 | /** 267 | * Invoked when an open bracket is encountered. The offset and length represent the location of the open bracket. 268 | * When `false` is returned, the array items will not be visited. 269 | */ 270 | onArrayBegin?: (offset: number, length: number, startLine: number, startCharacter: number, pathSupplier: () => JSONPath) => boolean | void; 271 | 272 | /** 273 | * Invoked when a closing bracket is encountered. The offset and length represent the location of the closing bracket. 274 | */ 275 | onArrayEnd?: (offset: number, length: number, startLine: number, startCharacter: number) => void; 276 | 277 | /** 278 | * Invoked when a literal value is encountered. The offset and length represent the location of the literal value. 279 | */ 280 | onLiteralValue?: (value: any, offset: number, length: number, startLine: number, startCharacter: number, pathSupplier: () => JSONPath) => void; 281 | 282 | /** 283 | * Invoked when a comma or colon separator is encountered. The offset and length represent the location of the separator. 284 | */ 285 | onSeparator?: (character: string, offset: number, length: number, startLine: number, startCharacter: number) => void; 286 | 287 | /** 288 | * When comments are allowed, invoked when a line or block comment is encountered. The offset and length represent the location of the comment. 289 | */ 290 | onComment?: (offset: number, length: number, startLine: number, startCharacter: number) => void; 291 | 292 | /** 293 | * Invoked on an error. 294 | */ 295 | onError?: (error: ParseErrorCode, offset: number, length: number, startLine: number, startCharacter: number) => void; 296 | } 297 | 298 | /** 299 | * An edit result describes a textual edit operation. It is the result of a {@linkcode format} and {@linkcode modify} operation. 300 | * It consist of one or more edits describing insertions, replacements or removals of text segments. 301 | * * The offsets of the edits refer to the original state of the document. 302 | * * No two edits change or remove the same range of text in the original document. 303 | * * Multiple edits can have the same offset if they are multiple inserts, or an insert followed by a remove or replace. 304 | * * The order in the array defines which edit is applied first. 305 | * To apply an edit result use {@linkcode applyEdits}. 306 | * In general multiple EditResults must not be concatenated because they might impact each other, producing incorrect or malformed JSON data. 307 | */ 308 | export type EditResult = Edit[]; 309 | 310 | /** 311 | * Represents a text modification 312 | */ 313 | export interface Edit { 314 | /** 315 | * The start offset of the modification. 316 | */ 317 | offset: number; 318 | /** 319 | * The length of the modification. Must not be negative. Empty length represents an *insert*. 320 | */ 321 | length: number; 322 | /** 323 | * The new content. Empty content represents a *remove*. 324 | */ 325 | content: string; 326 | } 327 | 328 | /** 329 | * A text range in the document 330 | */ 331 | export interface Range { 332 | /** 333 | * The start offset of the range. 334 | */ 335 | offset: number; 336 | /** 337 | * The length of the range. Must not be negative. 338 | */ 339 | length: number; 340 | } 341 | 342 | /** 343 | * Options used by {@linkcode format} when computing the formatting edit operations 344 | */ 345 | export interface FormattingOptions { 346 | /** 347 | * If indentation is based on spaces (`insertSpaces` = true), the number of spaces that make an indent. 348 | */ 349 | tabSize?: number; 350 | /** 351 | * Is indentation based on spaces? 352 | */ 353 | insertSpaces?: boolean; 354 | /** 355 | * The default 'end of line' character. If not set, '\n' is used as default. 356 | */ 357 | eol?: string; 358 | /** 359 | * If set, will add a new line at the end of the document. 360 | */ 361 | insertFinalNewline?: boolean; 362 | /** 363 | * If true, will keep line positions as is in the formatting 364 | */ 365 | keepLines?: boolean; 366 | } 367 | 368 | /** 369 | * Computes the edit operations needed to format a JSON document. 370 | * 371 | * @param documentText The input text 372 | * @param range The range to format or `undefined` to format the full content 373 | * @param options The formatting options 374 | * @returns The edit operations describing the formatting changes to the original document following the format described in {@linkcode EditResult}. 375 | * To apply the edit operations to the input, use {@linkcode applyEdits}. 376 | */ 377 | export function format(documentText: string, range: Range | undefined, options: FormattingOptions): EditResult { 378 | return formatter.format(documentText, range, options); 379 | } 380 | 381 | /** 382 | * Options used by {@linkcode modify} when computing the modification edit operations 383 | */ 384 | export interface ModificationOptions { 385 | /** 386 | * Formatting options. If undefined, the newly inserted code will be inserted unformatted. 387 | */ 388 | formattingOptions?: FormattingOptions; 389 | /** 390 | * Default false. If `JSONPath` refers to an index of an array and `isArrayInsertion` is `true`, then 391 | * {@linkcode modify} will insert a new item at that location instead of overwriting its contents. 392 | */ 393 | isArrayInsertion?: boolean; 394 | /** 395 | * Optional function to define the insertion index given an existing list of properties. 396 | */ 397 | getInsertionIndex?: (properties: string[]) => number; 398 | } 399 | 400 | /** 401 | * Computes the edit operations needed to modify a value in the JSON document. 402 | * 403 | * @param documentText The input text 404 | * @param path The path of the value to change. The path represents either to the document root, a property or an array item. 405 | * If the path points to an non-existing property or item, it will be created. 406 | * @param value The new value for the specified property or item. If the value is undefined, 407 | * the property or item will be removed. 408 | * @param options Options 409 | * @returns The edit operations describing the changes to the original document, following the format described in {@linkcode EditResult}. 410 | * To apply the edit operations to the input, use {@linkcode applyEdits}. 411 | */ 412 | export function modify(text: string, path: JSONPath, value: any, options: ModificationOptions): EditResult { 413 | return edit.setProperty(text, path, value, options); 414 | } 415 | 416 | /** 417 | * Applies edits to an input string. 418 | * @param text The input text 419 | * @param edits Edit operations following the format described in {@linkcode EditResult}. 420 | * @returns The text with the applied edits. 421 | * @throws An error if the edit operations are not well-formed as described in {@linkcode EditResult}. 422 | */ 423 | export function applyEdits(text: string, edits: EditResult): string { 424 | let sortedEdits = edits.slice(0).sort((a, b) => { 425 | const diff = a.offset - b.offset; 426 | if (diff === 0) { 427 | return a.length - b.length; 428 | } 429 | return diff; 430 | }); 431 | let lastModifiedOffset = text.length; 432 | for (let i = sortedEdits.length - 1; i >= 0; i--) { 433 | let e = sortedEdits[i]; 434 | if (e.offset + e.length <= lastModifiedOffset) { 435 | text = edit.applyEdit(text, e); 436 | } else { 437 | throw new Error('Overlapping edit'); 438 | } 439 | lastModifiedOffset = e.offset; 440 | } 441 | return text; 442 | } 443 | -------------------------------------------------------------------------------- /src/test/edit.test.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 'use strict'; 6 | 7 | import * as assert from 'assert'; 8 | import { FormattingOptions, Edit, ModificationOptions, modify } from '../main'; 9 | 10 | suite('JSON - edits', () => { 11 | 12 | function assertEdit(content: string, edits: Edit[], expected: string) { 13 | assert(edits); 14 | let lastEditOffset = content.length; 15 | for (let i = edits.length - 1; i >= 0; i--) { 16 | let edit = edits[i]; 17 | assert(edit.offset >= 0 && edit.length >= 0 && edit.offset + edit.length <= content.length); 18 | assert(typeof edit.content === 'string'); 19 | assert(lastEditOffset >= edit.offset + edit.length); // make sure all edits are ordered 20 | lastEditOffset = edit.offset; 21 | content = content.substring(0, edit.offset) + edit.content + content.substring(edit.offset + edit.length); 22 | } 23 | assert.strictEqual(content, expected); 24 | } 25 | 26 | let formattingOptions: FormattingOptions = { 27 | insertSpaces: true, 28 | tabSize: 2, 29 | eol: '\n', 30 | keepLines: false 31 | }; 32 | 33 | let formattingOptionsKeepLines: FormattingOptions = { 34 | insertSpaces: true, 35 | tabSize: 2, 36 | eol: '\n', 37 | keepLines: true 38 | }; 39 | 40 | let options: ModificationOptions = { 41 | formattingOptions 42 | }; 43 | 44 | let optionsKeepLines: ModificationOptions = { 45 | formattingOptions : formattingOptionsKeepLines 46 | }; 47 | 48 | test('set property', () => { 49 | let content = '{\n "x": "y"\n}'; 50 | let edits = modify(content, ['x'], 'bar', options); 51 | assertEdit(content, edits, '{\n "x": "bar"\n}'); 52 | 53 | content = 'true'; 54 | edits = modify(content, [], 'bar', options); 55 | assertEdit(content, edits, '"bar"'); 56 | 57 | content = '{\n "x": "y"\n}'; 58 | edits = modify(content, ['x'], { key: true }, options); 59 | assertEdit(content, edits, '{\n "x": {\n "key": true\n }\n}'); 60 | 61 | content = '{\n "a": "b", "x": "y"\n}'; 62 | edits = modify(content, ['a'], null, options); 63 | assertEdit(content, edits, '{\n "a": null, "x": "y"\n}'); 64 | }); 65 | 66 | test('insert property', () => { 67 | let content = '{}'; 68 | let edits = modify(content, ['foo'], 'bar', options); 69 | assertEdit(content, edits, '{\n "foo": "bar"\n}'); 70 | 71 | edits = modify(content, ['foo', 'foo2'], 'bar', options); 72 | assertEdit(content, edits, '{\n "foo": {\n "foo2": "bar"\n }\n}'); 73 | 74 | content = '{\n}'; 75 | edits = modify(content, ['foo'], 'bar', options); 76 | assertEdit(content, edits, '{\n "foo": "bar"\n}'); 77 | 78 | content = ' {\n }'; 79 | edits = modify(content, ['foo'], 'bar', options); 80 | assertEdit(content, edits, ' {\n "foo": "bar"\n }'); 81 | 82 | content = '{\n "x": "y"\n}'; 83 | edits = modify(content, ['foo'], 'bar', options); 84 | assertEdit(content, edits, '{\n "x": "y",\n "foo": "bar"\n}'); 85 | 86 | content = '{\n "x": "y"\n}'; 87 | edits = modify(content, ['e'], 'null', options); 88 | assertEdit(content, edits, '{\n "x": "y",\n "e": "null"\n}'); 89 | 90 | edits = modify(content, ['x'], 'bar', options); 91 | assertEdit(content, edits, '{\n "x": "bar"\n}'); 92 | 93 | content = '{\n "x": {\n "a": 1,\n "b": true\n }\n}\n'; 94 | edits = modify(content, ['x'], 'bar', options); 95 | assertEdit(content, edits, '{\n "x": "bar"\n}\n'); 96 | 97 | edits = modify(content, ['x', 'b'], 'bar', options); 98 | assertEdit(content, edits, '{\n "x": {\n "a": 1,\n "b": "bar"\n }\n}\n'); 99 | 100 | edits = modify(content, ['x', 'c'], 'bar', { formattingOptions, getInsertionIndex: () => 0 }); 101 | assertEdit(content, edits, '{\n "x": {\n "c": "bar",\n "a": 1,\n "b": true\n }\n}\n'); 102 | 103 | edits = modify(content, ['x', 'c'], 'bar', { formattingOptions, getInsertionIndex: () => 1 }); 104 | assertEdit(content, edits, '{\n "x": {\n "a": 1,\n "c": "bar",\n "b": true\n }\n}\n'); 105 | 106 | edits = modify(content, ['x', 'c'], 'bar', { formattingOptions, getInsertionIndex: () => 2 }); 107 | assertEdit(content, edits, '{\n "x": {\n "a": 1,\n "b": true,\n "c": "bar"\n }\n}\n'); 108 | 109 | edits = modify(content, ['c'], 'bar', options); 110 | assertEdit(content, edits, '{\n "x": {\n "a": 1,\n "b": true\n },\n "c": "bar"\n}\n'); 111 | 112 | content = '{\n "a": [\n {\n } \n ] \n}'; 113 | edits = modify(content, ['foo'], 'bar', options); 114 | assertEdit(content, edits, '{\n "a": [\n {\n } \n ],\n "foo": "bar"\n}'); 115 | 116 | content = ''; 117 | edits = modify(content, ['foo', 0], 'bar', options); 118 | assertEdit(content, edits, '{\n "foo": [\n "bar"\n ]\n}'); 119 | 120 | content = '//comment'; 121 | edits = modify(content, ['foo', 0], 'bar', options); 122 | assertEdit(content, edits, '{\n "foo": [\n "bar"\n ]\n} //comment'); 123 | }); 124 | 125 | test('remove property', () => { 126 | let content = '{\n "x": "y"\n}'; 127 | let edits = modify(content, ['x'], undefined, options); 128 | assertEdit(content, edits, '{\n}'); 129 | 130 | content = '{\n "x": "y", "a": []\n}'; 131 | edits = modify(content, ['x'], undefined, options); 132 | assertEdit(content, edits, '{\n "a": []\n}'); 133 | 134 | content = '{\n "x": "y", "a": []\n}'; 135 | edits = modify(content, ['a'], undefined, options); 136 | assertEdit(content, edits, '{\n "x": "y"\n}'); 137 | }); 138 | 139 | test('set item', () => { 140 | let content = '{\n "x": [1, 2, 3],\n "y": 0\n}'; 141 | 142 | let edits = modify(content, ['x', 0], 6, options); 143 | assertEdit(content, edits, '{\n "x": [6, 2, 3],\n "y": 0\n}'); 144 | 145 | edits = modify(content, ['x', 1], 5, options); 146 | assertEdit(content, edits, '{\n "x": [1, 5, 3],\n "y": 0\n}'); 147 | 148 | edits = modify(content, ['x', 2], 4, options); 149 | assertEdit(content, edits, '{\n "x": [1, 2, 4],\n "y": 0\n}'); 150 | 151 | edits = modify(content, ['x', 3], 3, options); 152 | assertEdit(content, edits, '{\n "x": [\n 1,\n 2,\n 3,\n 3\n ],\n "y": 0\n}'); 153 | }); 154 | 155 | test('insert item at 0; isArrayInsertion = true', () => { 156 | let content = '[\n 2,\n 3\n]'; 157 | let edits = modify(content, [0], 1, { formattingOptions, isArrayInsertion: true }); 158 | assertEdit(content, edits, '[\n 1,\n 2,\n 3\n]'); 159 | }); 160 | 161 | test('insert item at 0 in empty array', () => { 162 | let content = '[\n]'; 163 | let edits = modify(content, [0], 1, options); 164 | assertEdit(content, edits, '[\n 1\n]'); 165 | }); 166 | 167 | test('insert item at an index; isArrayInsertion = true', () => { 168 | let content = '[\n 1,\n 3\n]'; 169 | let edits = modify(content, [1], 2, { formattingOptions, isArrayInsertion: true }); 170 | assertEdit(content, edits, '[\n 1,\n 2,\n 3\n]'); 171 | }); 172 | 173 | test('insert item at an index in empty array', () => { 174 | let content = '[\n]'; 175 | let edits = modify(content, [1], 1, options); 176 | assertEdit(content, edits, '[\n 1\n]'); 177 | }); 178 | 179 | test('insert item at end index', () => { 180 | let content = '[\n 1,\n 2\n]'; 181 | let edits = modify(content, [2], 3, options); 182 | assertEdit(content, edits, '[\n 1,\n 2,\n 3\n]'); 183 | }); 184 | 185 | test('insert item at end to empty array', () => { 186 | let content = '[\n]'; 187 | let edits = modify(content, [-1], 'bar', options); 188 | assertEdit(content, edits, '[\n "bar"\n]'); 189 | }); 190 | 191 | test('insert item at end', () => { 192 | let content = '[\n 1,\n 2\n]'; 193 | let edits = modify(content, [-1], 'bar', options); 194 | assertEdit(content, edits, '[\n 1,\n 2,\n "bar"\n]'); 195 | }); 196 | 197 | test('remove item in array with one item', () => { 198 | let content = '[\n 1\n]'; 199 | let edits = modify(content, [0], void 0, options); 200 | assertEdit(content, edits, '[]'); 201 | }); 202 | 203 | test('remove item in the middle of the array', () => { 204 | let content = '[\n 1,\n 2,\n 3\n]'; 205 | let edits = modify(content, [1], void 0, options); 206 | assertEdit(content, edits, '[\n 1,\n 3\n]'); 207 | }); 208 | 209 | test('remove last item in the array', () => { 210 | let content = '[\n 1,\n 2,\n "bar"\n]'; 211 | let edits = modify(content, [2], void 0, options); 212 | assertEdit(content, edits, '[\n 1,\n 2\n]'); 213 | }); 214 | 215 | test('remove last item in the array if ends with comma', () => { 216 | let content = '[\n 1,\n "foo",\n "bar",\n]'; 217 | let edits = modify(content, [2], void 0, options); 218 | assertEdit(content, edits, '[\n 1,\n "foo"\n]'); 219 | }); 220 | 221 | test('remove last item in the array if there is a comment in the beginning', () => { 222 | let content = '// This is a comment\n[\n 1,\n "foo",\n "bar"\n]'; 223 | let edits = modify(content, [2], void 0, options); 224 | assertEdit(content, edits, '// This is a comment\n[\n 1,\n "foo"\n]'); 225 | }); 226 | 227 | test('set property without formatting', () => { 228 | let content = '{\n "x": [1, 2, 3],\n "y": 0\n}'; 229 | 230 | let edits = modify(content, ['x', 0], { a: 1, b: 2 }, { formattingOptions }); 231 | assertEdit(content, edits, '{\n "x": [{\n "a": 1,\n "b": 2\n }, 2, 3],\n "y": 0\n}'); 232 | 233 | edits = modify(content, ['x', 0], { a: 1, b: 2 }, { formattingOptions: undefined }); 234 | assertEdit(content, edits, '{\n "x": [{"a":1,"b":2}, 2, 3],\n "y": 0\n}'); 235 | }); 236 | 237 | // test added for the keepLines feature 238 | test('insert property when keepLines is true', () => { 239 | 240 | let content = '{}'; 241 | let edits = modify(content, ['foo', 'foo2'], 'bar', options); 242 | assertEdit(content, edits, '{\n "foo": {\n "foo2": "bar"\n }\n}'); 243 | }); 244 | }); -------------------------------------------------------------------------------- /src/test/format.test.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 'use strict'; 6 | 7 | import * as assert from 'assert'; 8 | import * as Formatter from '../impl/format'; 9 | import { Range } from '../main'; 10 | 11 | suite('JSON - formatter', () => { 12 | 13 | function format(content: string, expected: string, insertSpaces = true, insertFinalNewline = false, keepLines = false) { 14 | let range: Range | undefined = void 0; 15 | const rangeStart = content.indexOf('|'); 16 | const rangeEnd = content.lastIndexOf('|'); 17 | if (rangeStart !== -1 && rangeEnd !== -1) { 18 | content = content.substring(0, rangeStart) + content.substring(rangeStart + 1, rangeEnd) + content.substring(rangeEnd + 1); 19 | range = { offset: rangeStart, length: rangeEnd - rangeStart }; 20 | } 21 | 22 | const edits = Formatter.format(content, range, { tabSize: 2, insertSpaces, insertFinalNewline, eol: '\n', keepLines }); 23 | 24 | let lastEditOffset = content.length; 25 | 26 | for (let i = edits.length - 1; i >= 0; i--) { 27 | const edit = edits[i]; 28 | // assert(edit.offset >= 0 && edit.length >= 0 && edit.offset + edit.length <= content.length); 29 | // assert(typeof edit.content === 'string'); 30 | // assert(lastEditOffset >= edit.offset + edit.length); // make sure all edits are ordered 31 | lastEditOffset = edit.offset; 32 | content = content.substring(0, edit.offset) + edit.content + content.substring(edit.offset + edit.length); 33 | } 34 | 35 | assert.strictEqual(content, expected); 36 | } 37 | 38 | test('object - single property', () => { 39 | const content = [ 40 | '{"x" : 1}' 41 | ].join('\n'); 42 | 43 | const expected = [ 44 | '{', 45 | ' "x": 1', 46 | '}' 47 | ].join('\n'); 48 | 49 | format(content, expected); 50 | }); 51 | test('object - multiple properties', () => { 52 | const content = [ 53 | '{"x" : 1, "y" : "foo", "z" : true}' 54 | ].join('\n'); 55 | 56 | const expected = [ 57 | '{', 58 | ' "x": 1,', 59 | ' "y": "foo",', 60 | ' "z": true', 61 | '}' 62 | ].join('\n'); 63 | 64 | format(content, expected); 65 | }); 66 | test('object - no properties ', () => { 67 | const content = [ 68 | '{"x" : { }, "y" : {}}' 69 | ].join('\n'); 70 | 71 | const expected = [ 72 | '{', 73 | ' "x": {},', 74 | ' "y": {}', 75 | '}' 76 | ].join('\n'); 77 | 78 | format(content, expected); 79 | }); 80 | test('object - nesting', () => { 81 | const content = [ 82 | '{"x" : { "y" : { "z" : { }}, "a": true}}' 83 | ].join('\n'); 84 | 85 | const expected = [ 86 | '{', 87 | ' "x": {', 88 | ' "y": {', 89 | ' "z": {}', 90 | ' },', 91 | ' "a": true', 92 | ' }', 93 | '}' 94 | ].join('\n'); 95 | 96 | format(content, expected); 97 | }); 98 | 99 | test('array - single items', () => { 100 | const content = [ 101 | '["[]"]' 102 | ].join('\n'); 103 | 104 | const expected = [ 105 | '[', 106 | ' "[]"', 107 | ']' 108 | ].join('\n'); 109 | 110 | format(content, expected); 111 | }); 112 | 113 | test('array - multiple items', () => { 114 | const content = [ 115 | '[true,null,1.2]' 116 | ].join('\n'); 117 | 118 | const expected = [ 119 | '[', 120 | ' true,', 121 | ' null,', 122 | ' 1.2', 123 | ']' 124 | ].join('\n'); 125 | 126 | format(content, expected); 127 | }); 128 | 129 | test('array - no items', () => { 130 | const content = [ 131 | '[ ]' 132 | ].join('\n'); 133 | 134 | const expected = [ 135 | '[]' 136 | ].join('\n'); 137 | 138 | format(content, expected); 139 | }); 140 | 141 | test('array - nesting', () => { 142 | const content = [ 143 | '[ [], [ [ {} ], "a" ] ]' 144 | ].join('\n'); 145 | 146 | const expected = [ 147 | '[', 148 | ' [],', 149 | ' [', 150 | ' [', 151 | ' {}', 152 | ' ],', 153 | ' "a"', 154 | ' ]', 155 | ']', 156 | ].join('\n'); 157 | 158 | format(content, expected); 159 | }); 160 | 161 | test('syntax errors', () => { 162 | const content = [ 163 | '[ null 1.2 "Hello" ]' 164 | ].join('\n'); 165 | 166 | const expected = [ 167 | '[', 168 | ' null 1.2 "Hello"', 169 | ']', 170 | ].join('\n'); 171 | 172 | format(content, expected); 173 | }); 174 | 175 | test('syntax errors 2', () => { 176 | const content = [ 177 | '{"a":"b""c":"d" }' 178 | ].join('\n'); 179 | 180 | const expected = [ 181 | '{', 182 | ' "a": "b""c": "d"', 183 | '}', 184 | ].join('\n'); 185 | 186 | format(content, expected); 187 | }); 188 | 189 | test('empty lines', () => { 190 | const content = [ 191 | '{', 192 | '"a": true,', 193 | '', 194 | '"b": true', 195 | '}', 196 | ].join('\n'); 197 | 198 | const expected = [ 199 | '{', 200 | '\t"a": true,', 201 | '\t"b": true', 202 | '}', 203 | ].join('\n'); 204 | 205 | format(content, expected, false); 206 | }); 207 | test('single line comment', () => { 208 | const content = [ 209 | '[ ', 210 | '//comment', 211 | '"foo", "bar"', 212 | '] ' 213 | ].join('\n'); 214 | 215 | const expected = [ 216 | '[', 217 | ' //comment', 218 | ' "foo",', 219 | ' "bar"', 220 | ']', 221 | ].join('\n'); 222 | 223 | format(content, expected); 224 | }); 225 | test('block line comment', () => { 226 | const content = [ 227 | '[{', 228 | ' /*comment*/ ', 229 | '"foo" : true', 230 | '}] ' 231 | ].join('\n'); 232 | 233 | const expected = [ 234 | '[', 235 | ' {', 236 | ' /*comment*/', 237 | ' "foo": true', 238 | ' }', 239 | ']', 240 | ].join('\n'); 241 | 242 | format(content, expected); 243 | }); 244 | test('single line comment on same line', () => { 245 | const content = [ 246 | ' { ', 247 | ' "a": {}// comment ', 248 | ' } ' 249 | ].join('\n'); 250 | 251 | const expected = [ 252 | '{', 253 | ' "a": {} // comment ', 254 | '}', 255 | ].join('\n'); 256 | 257 | format(content, expected); 258 | }); 259 | test('single line comment on same line 2', () => { 260 | const content = [ 261 | '{ //comment', 262 | '}' 263 | ].join('\n'); 264 | 265 | const expected = [ 266 | '{ //comment', 267 | '}' 268 | ].join('\n'); 269 | 270 | format(content, expected); 271 | }); 272 | test('block comment on same line', () => { 273 | const content = [ 274 | '{ "a": {}, /*comment*/ ', 275 | ' /*comment*/ "b": {}, ', 276 | ' "c": {/*comment*/} } ', 277 | ].join('\n'); 278 | 279 | const expected = [ 280 | '{', 281 | ' "a": {}, /*comment*/', 282 | ' /*comment*/ "b": {},', 283 | ' "c": { /*comment*/}', 284 | '}', 285 | ].join('\n'); 286 | 287 | format(content, expected); 288 | }); 289 | 290 | test('block comment on same line advanced', () => { 291 | const content = [ 292 | ' { "d": [', 293 | ' null', 294 | ' ] /*comment*/', 295 | ' ,"e": /*comment*/ [null] }', 296 | ].join('\n'); 297 | 298 | const expected = [ 299 | '{', 300 | ' "d": [', 301 | ' null', 302 | ' ] /*comment*/,', 303 | ' "e": /*comment*/ [', 304 | ' null', 305 | ' ]', 306 | '}', 307 | ].join('\n'); 308 | 309 | format(content, expected); 310 | }); 311 | 312 | test('multiple block comments on same line', () => { 313 | const content = [ 314 | '{ "a": {} /*comment*/, /*comment*/ ', 315 | ' /*comment*/ "b": {} /*comment*/ } ' 316 | ].join('\n'); 317 | 318 | const expected = [ 319 | '{', 320 | ' "a": {} /*comment*/, /*comment*/', 321 | ' /*comment*/ "b": {} /*comment*/', 322 | '}', 323 | ].join('\n'); 324 | 325 | format(content, expected); 326 | }); 327 | 328 | test('multiple mixed comments on same line', () => { 329 | const content = [ 330 | '[ /*comment*/ /*comment*/ // comment ', 331 | ']' 332 | ].join('\n'); 333 | 334 | const expected = [ 335 | '[ /*comment*/ /*comment*/ // comment ', 336 | ']' 337 | ].join('\n'); 338 | 339 | format(content, expected); 340 | }); 341 | 342 | test('range', () => { 343 | const content = [ 344 | '{ "a": {},', 345 | '|"b": [null, null]|', 346 | '} ' 347 | ].join('\n'); 348 | 349 | const expected = [ 350 | '{ "a": {},', 351 | '"b": [', 352 | ' null,', 353 | ' null', 354 | ']', 355 | '} ', 356 | ].join('\n'); 357 | 358 | format(content, expected); 359 | }); 360 | 361 | test('range with existing indent', () => { 362 | const content = [ 363 | '{ "a": {},', 364 | ' |"b": [null],', 365 | '"c": {}', 366 | '}|' 367 | ].join('\n'); 368 | 369 | const expected = [ 370 | '{ "a": {},', 371 | ' "b": [', 372 | ' null', 373 | ' ],', 374 | ' "c": {}', 375 | '}', 376 | ].join('\n'); 377 | 378 | format(content, expected); 379 | }); 380 | 381 | 382 | test('range with existing indent - tabs', () => { 383 | const content = [ 384 | '{ "a": {},', 385 | '| "b": [null], ', 386 | '"c": {}', 387 | '}| ' 388 | ].join('\n'); 389 | 390 | const expected = [ 391 | '{ "a": {},', 392 | '\t"b": [', 393 | '\t\tnull', 394 | '\t],', 395 | '\t"c": {}', 396 | '}', 397 | ].join('\n'); 398 | 399 | format(content, expected, false); 400 | }); 401 | 402 | test('property range - issue 14623', () => { 403 | const content = [ 404 | '{ |"a" :| 1,', 405 | ' "b": 1', 406 | '}' 407 | ].join('\n'); 408 | 409 | const expected = [ 410 | '{ "a": 1,', 411 | ' "b": 1', 412 | '}' 413 | ].join('\n'); 414 | 415 | format(content, expected, false); 416 | }); 417 | test('block comment none-line breaking symbols', () => { 418 | const content = [ 419 | '{ "a": [ 1', 420 | '/* comment */', 421 | ', 2', 422 | '/* comment */', 423 | ']', 424 | '/* comment */', 425 | ',', 426 | ' "b": true', 427 | '/* comment */', 428 | '}' 429 | ].join('\n'); 430 | 431 | const expected = [ 432 | '{', 433 | ' "a": [', 434 | ' 1', 435 | ' /* comment */', 436 | ' ,', 437 | ' 2', 438 | ' /* comment */', 439 | ' ]', 440 | ' /* comment */', 441 | ' ,', 442 | ' "b": true', 443 | ' /* comment */', 444 | '}', 445 | ].join('\n'); 446 | 447 | format(content, expected); 448 | }); 449 | test('line comment after none-line breaking symbols', () => { 450 | const content = [ 451 | '{ "a":', 452 | '// comment', 453 | 'null,', 454 | ' "b"', 455 | '// comment', 456 | ': null', 457 | '// comment', 458 | '}' 459 | ].join('\n'); 460 | 461 | const expected = [ 462 | '{', 463 | ' "a":', 464 | ' // comment', 465 | ' null,', 466 | ' "b"', 467 | ' // comment', 468 | ' : null', 469 | ' // comment', 470 | '}', 471 | ].join('\n'); 472 | 473 | format(content, expected); 474 | }); 475 | 476 | test('line comment, enforce line comment ', () => { 477 | const content = [ 478 | '{"settings": // This is some text', 479 | '{', 480 | '"foo": 1', 481 | '}', 482 | '}' 483 | ].join('\n'); 484 | 485 | const expected = [ 486 | '{', 487 | ' "settings": // This is some text', 488 | ' {', 489 | ' "foo": 1', 490 | ' }', 491 | '}' 492 | ].join('\n'); 493 | 494 | format(content, expected); 495 | }); 496 | 497 | test('random content', () => { 498 | const content = [ 499 | 'a 1 b 1 3 true' 500 | ].join('\n'); 501 | 502 | const expected = [ 503 | 'a 1 b 1 3 true', 504 | ].join('\n'); 505 | 506 | format(content, expected); 507 | }); 508 | 509 | test('insertFinalNewline', () => { 510 | const content = [ 511 | '{', 512 | '}' 513 | ].join('\n'); 514 | 515 | const expected = [ 516 | '{}', 517 | '' 518 | ].join('\n'); 519 | 520 | format(content, expected, undefined, true); 521 | }); 522 | 523 | 524 | // tests added for the keepLines feature 525 | 526 | test('adjust the indentation of a one-line array', () => { 527 | const content = [ 528 | '{ "array": [1,2,3]', 529 | '}' 530 | ].join('\n'); 531 | 532 | const expected = [ 533 | '{ "array": [ 1, 2, 3 ]', 534 | '}' 535 | ].join('\n'); 536 | 537 | format(content, expected, true, false, true); 538 | }); 539 | 540 | test('adjust the indentation of a multi-line array', () => { 541 | const content = [ 542 | '{"array":', 543 | ' [1,2,', 544 | ' 3]', 545 | '}' 546 | ].join('\n'); 547 | 548 | const expected = [ 549 | '{ "array":', 550 | ' [ 1, 2,', 551 | ' 3 ]', 552 | '}' 553 | ].join('\n'); 554 | 555 | format(content, expected, true, false, true); 556 | }); 557 | 558 | test('adjust the identation of a one-line object', () => { 559 | const content = [ 560 | '{"settings": // This is some text', 561 | '{"foo": 1}', 562 | '}' 563 | ].join('\n'); 564 | 565 | const expected = [ 566 | '{ "settings": // This is some text', 567 | ' { "foo": 1 }', 568 | '}' 569 | ].join('\n'); 570 | 571 | format(content, expected, true, false, true); 572 | }); 573 | 574 | test('multiple line breaks are kept', () => { 575 | const content = [ 576 | '{"settings":', 577 | '', 578 | '', 579 | '', 580 | '{"foo": 1}', 581 | '}' 582 | ].join('\n'); 583 | 584 | const expected = [ 585 | '{ "settings":', 586 | '', 587 | '', 588 | '', 589 | ' { "foo": 1 }', 590 | '}' 591 | ].join('\n'); 592 | 593 | format(content, expected, true, false, true); 594 | }); 595 | 596 | test('adjusting multiple line breaks and a block comment, line breaks are kept', () => { 597 | const content = [ 598 | '{"settings":', 599 | '', 600 | '', 601 | '{"foo": 1} /* this is a multiline', 602 | 'comment */', 603 | '}' 604 | ].join('\n'); 605 | 606 | const expected = [ 607 | '{ "settings":', 608 | '', 609 | '', 610 | ' { "foo": 1 } /* this is a multiline', 611 | 'comment */', 612 | '}' 613 | ].join('\n'); 614 | 615 | format(content, expected, true, false, true); 616 | }); 617 | 618 | test('colon is kept on its own line', () => { 619 | const content = [ 620 | '{"settings"', 621 | ':', 622 | '{"foo"', 623 | ':', 624 | '1}', 625 | '}' 626 | ].join('\n'); 627 | 628 | const expected = [ 629 | '{ "settings"', 630 | ' :', 631 | ' { "foo"', 632 | ' :', 633 | ' 1 }', 634 | '}' 635 | ].join('\n'); 636 | 637 | format(content, expected, true, false, true); 638 | }); 639 | 640 | test('adjusting the indentation of a nested multi-line array', () => { 641 | const content = [ 642 | '{', 643 | '', 644 | '{', 645 | '', 646 | '"array" : [1, 2', 647 | '3, 4]', 648 | '}', 649 | '}' 650 | ].join('\n'); 651 | 652 | const expected = [ 653 | '{', 654 | '', 655 | ' {', 656 | '', 657 | ' "array": [ 1, 2', 658 | ' 3, 4 ]', 659 | ' }', 660 | '}' 661 | ].join('\n'); 662 | 663 | format(content, expected, true, false, true); 664 | }); 665 | 666 | test('adjusting the indentation for a series of empty arrays or objects', () => { 667 | const content = [ 668 | '{', 669 | '', 670 | '}', 671 | '', 672 | '{', 673 | '[', 674 | ']', 675 | '}' 676 | ].join('\n'); 677 | 678 | const expected = [ 679 | '{', 680 | '', 681 | '}', 682 | '', 683 | '{', 684 | ' [', 685 | ' ]', 686 | '}' 687 | ].join('\n'); 688 | 689 | format(content, expected, true, false, true); 690 | }); 691 | 692 | test('adjusting the indentation for a series of multiple empty lines at the end', () => { 693 | const content = [ 694 | '{', 695 | '}', 696 | '', 697 | '', 698 | '' 699 | ].join('\n'); 700 | 701 | const expected = [ 702 | '{', 703 | '}', 704 | '', 705 | '', 706 | '' 707 | ].join('\n'); 708 | 709 | format(content, expected, true, false, true); 710 | }); 711 | 712 | test('adjusting the indentation for comments on separate lines', () => { 713 | const content = [ 714 | '', 715 | '', 716 | '', 717 | ' // comment 1', 718 | '', 719 | '', 720 | '', 721 | ' /* comment 2 */', 722 | 'const' 723 | ].join('\n'); 724 | 725 | const expected = [ 726 | 727 | '', 728 | '', 729 | '', 730 | '// comment 1', 731 | '', 732 | '', 733 | '', 734 | '/* comment 2 */', 735 | 'const' 736 | ].join('\n'); 737 | 738 | format(content, expected, true, false, true); 739 | }); 740 | }); 741 | -------------------------------------------------------------------------------- /src/test/json.test.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 'use strict'; 6 | 7 | import * as assert from 'assert'; 8 | import { 9 | SyntaxKind, createScanner, parse, getLocation, Node, ParseError, parseTree, ParseErrorCode, 10 | ParseOptions, Segment, findNodeAtLocation, getNodeValue, getNodePath, ScanError, visit, JSONVisitor, JSONPath 11 | } from '../main'; 12 | 13 | function assertKinds(text: string, ...kinds: SyntaxKind[]): void { 14 | var scanner = createScanner(text); 15 | var kind: SyntaxKind; 16 | while ((kind = scanner.scan()) !== SyntaxKind.EOF) { 17 | assert.strictEqual(kind, kinds.shift()); 18 | assert.strictEqual(scanner.getTokenError(), ScanError.None, text); 19 | } 20 | assert.strictEqual(kinds.length, 0); 21 | } 22 | function assertScanError(text: string, scanError: ScanError, ...kinds: SyntaxKind[]): void { 23 | var scanner = createScanner(text); 24 | scanner.scan(); 25 | assert.strictEqual(scanner.getToken(), kinds.shift()); 26 | assert.strictEqual(scanner.getTokenError(), scanError); 27 | var kind: SyntaxKind; 28 | while ((kind = scanner.scan()) !== SyntaxKind.EOF) { 29 | assert.strictEqual(kind, kinds.shift()); 30 | } 31 | assert.strictEqual(kinds.length, 0); 32 | } 33 | 34 | function assertValidParse(input: string, expected: any, options?: ParseOptions): void { 35 | var errors: ParseError[] = []; 36 | var actual = parse(input, errors, options); 37 | 38 | assert.deepStrictEqual([], errors); 39 | assert.deepStrictEqual(actual, expected); 40 | } 41 | 42 | function assertInvalidParse(input: string, expected: any, options?: ParseOptions): void { 43 | var errors: ParseError[] = []; 44 | var actual = parse(input, errors, options); 45 | 46 | assert(errors.length > 0); 47 | assert.deepStrictEqual(actual, expected); 48 | } 49 | 50 | function assertTree(input: string, expected: any, expectedErrors: ParseError[] = []): void { 51 | var errors: ParseError[] = []; 52 | var actual = parseTree(input, errors); 53 | 54 | assert.deepStrictEqual(errors, expectedErrors); 55 | let checkParent = (node: Node | undefined) => { 56 | if (node?.children) { 57 | for (let child of node.children) { 58 | assert.strictEqual(node, child.parent); 59 | delete (child).parent; // delete to avoid recursion in deep equal 60 | checkParent(child); 61 | } 62 | } 63 | }; 64 | checkParent(actual); 65 | 66 | assert.deepStrictEqual(actual, expected, JSON.stringify(actual)); 67 | } 68 | 69 | interface VisitorCallback { 70 | id: keyof JSONVisitor, 71 | text: string; 72 | startLine: number; 73 | startCharacter: number; 74 | arg?: any; 75 | path?: JSONPath; 76 | } 77 | interface VisitorError extends ParseError { 78 | startLine: number; 79 | startCharacter: number; 80 | } 81 | 82 | function assertVisit(input: string, expected: VisitorCallback[], expectedErrors: VisitorError[] = [], disallowComments = false, stopOffsets?: number[]): void { 83 | let errors: VisitorError[] = []; 84 | let actuals: VisitorCallback[] = []; 85 | let noArgHandler = (id: keyof JSONVisitor) => (offset: number, length: number, startLine: number, startCharacter: number) => actuals.push({ id, text: input.substr(offset, length), startLine, startCharacter }); 86 | let oneArgHandler = (id: keyof JSONVisitor) => (arg: any, offset: number, length: number, startLine: number, startCharacter: number) => actuals.push({ id, text: input.substr(offset, length), startLine, startCharacter, arg }); 87 | let oneArgHandlerWithPath = (id: keyof JSONVisitor) => (arg: any, offset: number, length: number, startLine: number, startCharacter: number, pathSupplier: () => JSONPath) => actuals.push({ id, text: input.substr(offset, length), startLine, startCharacter, arg, path: pathSupplier() }); 88 | let beginHandler = (id: keyof JSONVisitor) => (offset: number, length: number, startLine: number, startCharacter: number, pathSupplier: () => JSONPath) => { actuals.push({ id, text: input.substr(offset, length), startLine, startCharacter, path: pathSupplier() }); return !stopOffsets || (stopOffsets.indexOf(offset) === -1); }; 89 | visit(input, { 90 | onObjectBegin: beginHandler('onObjectBegin'), 91 | onObjectProperty: oneArgHandlerWithPath('onObjectProperty'), 92 | onObjectEnd: noArgHandler('onObjectEnd'), 93 | onArrayBegin: beginHandler('onArrayBegin'), 94 | onArrayEnd: noArgHandler('onArrayEnd'), 95 | onLiteralValue: oneArgHandlerWithPath('onLiteralValue'), 96 | onSeparator: oneArgHandler('onSeparator'), 97 | onComment: noArgHandler('onComment'), 98 | onError: (error: ParseErrorCode, offset: number, length: number, startLine: number, startCharacter: number) => { 99 | errors.push({ error, offset, length, startLine, startCharacter }); 100 | } 101 | }, { 102 | disallowComments 103 | }); 104 | assert.deepStrictEqual(errors, expectedErrors); 105 | assert.deepStrictEqual(actuals, expected, JSON.stringify(actuals)); 106 | } 107 | 108 | function assertNodeAtLocation(input: Node | undefined, segments: Segment[], expected: any) { 109 | let actual = input && findNodeAtLocation(input, segments); 110 | assert.deepEqual(actual ? getNodeValue(actual) : void 0, expected); 111 | if (actual) { 112 | assert.deepStrictEqual(getNodePath(actual), segments); 113 | } 114 | } 115 | 116 | function assertLocation(input: string, expectedSegments: Segment[], expectedNodeType: string | undefined, expectedCompleteProperty: boolean): void { 117 | var offset = input.indexOf('|'); 118 | input = input.substring(0, offset) + input.substring(offset + 1, input.length); 119 | var actual = getLocation(input, offset); 120 | assert(actual); 121 | assert.deepStrictEqual(actual.path, expectedSegments, input); 122 | assert.strictEqual(actual.previousNode && actual.previousNode.type, expectedNodeType, input); 123 | assert.strictEqual(actual.isAtPropertyKey, expectedCompleteProperty, input); 124 | } 125 | 126 | function assertMatchesLocation(input: string, matchingSegments: Segment[], expectedResult = true): void { 127 | var offset = input.indexOf('|'); 128 | input = input.substring(0, offset) + input.substring(offset + 1, input.length); 129 | var actual = getLocation(input, offset); 130 | assert(actual); 131 | assert.strictEqual(actual.matches(matchingSegments), expectedResult); 132 | } 133 | 134 | suite('JSON', () => { 135 | test('tokens', () => { 136 | assertKinds('{', SyntaxKind.OpenBraceToken); 137 | assertKinds('}', SyntaxKind.CloseBraceToken); 138 | assertKinds('[', SyntaxKind.OpenBracketToken); 139 | assertKinds(']', SyntaxKind.CloseBracketToken); 140 | assertKinds(':', SyntaxKind.ColonToken); 141 | assertKinds(',', SyntaxKind.CommaToken); 142 | }); 143 | 144 | test('comments', () => { 145 | assertKinds('// this is a comment', SyntaxKind.LineCommentTrivia); 146 | assertKinds('// this is a comment\n', SyntaxKind.LineCommentTrivia, SyntaxKind.LineBreakTrivia); 147 | assertKinds('/* this is a comment*/', SyntaxKind.BlockCommentTrivia); 148 | assertKinds('/* this is a \r\ncomment*/', SyntaxKind.BlockCommentTrivia); 149 | assertKinds('/* this is a \ncomment*/', SyntaxKind.BlockCommentTrivia); 150 | 151 | // unexpected end 152 | assertScanError('/* this is a', ScanError.UnexpectedEndOfComment, SyntaxKind.BlockCommentTrivia); 153 | assertScanError('/* this is a \ncomment', ScanError.UnexpectedEndOfComment, SyntaxKind.BlockCommentTrivia); 154 | 155 | // broken comment 156 | assertKinds('/ ttt', SyntaxKind.Unknown, SyntaxKind.Trivia, SyntaxKind.Unknown); 157 | }); 158 | 159 | test('strings', () => { 160 | assertKinds('"test"', SyntaxKind.StringLiteral); 161 | assertKinds('"\\""', SyntaxKind.StringLiteral); 162 | assertKinds('"\\/"', SyntaxKind.StringLiteral); 163 | assertKinds('"\\b"', SyntaxKind.StringLiteral); 164 | assertKinds('"\\f"', SyntaxKind.StringLiteral); 165 | assertKinds('"\\n"', SyntaxKind.StringLiteral); 166 | assertKinds('"\\r"', SyntaxKind.StringLiteral); 167 | assertKinds('"\\t"', SyntaxKind.StringLiteral); 168 | assertKinds('"\u88ff"', SyntaxKind.StringLiteral); 169 | assertKinds('"​\u2028"', SyntaxKind.StringLiteral); 170 | assertScanError('"\\v"', ScanError.InvalidEscapeCharacter, SyntaxKind.StringLiteral); 171 | 172 | // unexpected end 173 | assertScanError('"test', ScanError.UnexpectedEndOfString, SyntaxKind.StringLiteral); 174 | assertScanError('"test\n"', ScanError.UnexpectedEndOfString, SyntaxKind.StringLiteral, SyntaxKind.LineBreakTrivia, SyntaxKind.StringLiteral); 175 | 176 | // invalid characters 177 | assertScanError('"\t"', ScanError.InvalidCharacter, SyntaxKind.StringLiteral); 178 | assertScanError('"\t "', ScanError.InvalidCharacter, SyntaxKind.StringLiteral); 179 | assertScanError('"\0 "', ScanError.InvalidCharacter, SyntaxKind.StringLiteral); 180 | }); 181 | 182 | test('numbers', () => { 183 | assertKinds('0', SyntaxKind.NumericLiteral); 184 | assertKinds('0.1', SyntaxKind.NumericLiteral); 185 | assertKinds('-0.1', SyntaxKind.NumericLiteral); 186 | assertKinds('-1', SyntaxKind.NumericLiteral); 187 | assertKinds('1', SyntaxKind.NumericLiteral); 188 | assertKinds('123456789', SyntaxKind.NumericLiteral); 189 | assertKinds('10', SyntaxKind.NumericLiteral); 190 | assertKinds('90', SyntaxKind.NumericLiteral); 191 | assertKinds('90E+123', SyntaxKind.NumericLiteral); 192 | assertKinds('90e+123', SyntaxKind.NumericLiteral); 193 | assertKinds('90e-123', SyntaxKind.NumericLiteral); 194 | assertKinds('90E-123', SyntaxKind.NumericLiteral); 195 | assertKinds('90E123', SyntaxKind.NumericLiteral); 196 | assertKinds('90e123', SyntaxKind.NumericLiteral); 197 | 198 | // zero handling 199 | assertKinds('01', SyntaxKind.NumericLiteral, SyntaxKind.NumericLiteral); 200 | assertKinds('-01', SyntaxKind.NumericLiteral, SyntaxKind.NumericLiteral); 201 | 202 | // unexpected end 203 | assertKinds('-', SyntaxKind.Unknown); 204 | assertKinds('.0', SyntaxKind.Unknown); 205 | }); 206 | 207 | test('keywords: true, false, null', () => { 208 | assertKinds('true', SyntaxKind.TrueKeyword); 209 | assertKinds('false', SyntaxKind.FalseKeyword); 210 | assertKinds('null', SyntaxKind.NullKeyword); 211 | 212 | 213 | assertKinds('true false null', 214 | SyntaxKind.TrueKeyword, 215 | SyntaxKind.Trivia, 216 | SyntaxKind.FalseKeyword, 217 | SyntaxKind.Trivia, 218 | SyntaxKind.NullKeyword); 219 | 220 | // invalid words 221 | assertKinds('nulllll', SyntaxKind.Unknown); 222 | assertKinds('True', SyntaxKind.Unknown); 223 | assertKinds('foo-bar', SyntaxKind.Unknown); 224 | assertKinds('foo bar', SyntaxKind.Unknown, SyntaxKind.Trivia, SyntaxKind.Unknown); 225 | 226 | assertKinds('false//hello', SyntaxKind.FalseKeyword, SyntaxKind.LineCommentTrivia); 227 | }); 228 | 229 | test('trivia', () => { 230 | assertKinds(' ', SyntaxKind.Trivia); 231 | assertKinds(' \t ', SyntaxKind.Trivia); 232 | assertKinds(' \t \n \t ', SyntaxKind.Trivia, SyntaxKind.LineBreakTrivia, SyntaxKind.Trivia); 233 | assertKinds('\r\n', SyntaxKind.LineBreakTrivia); 234 | assertKinds('\r', SyntaxKind.LineBreakTrivia); 235 | assertKinds('\n', SyntaxKind.LineBreakTrivia); 236 | assertKinds('\n\r', SyntaxKind.LineBreakTrivia, SyntaxKind.LineBreakTrivia); 237 | assertKinds('\n \n', SyntaxKind.LineBreakTrivia, SyntaxKind.Trivia, SyntaxKind.LineBreakTrivia); 238 | }); 239 | 240 | test('parse: literals', () => { 241 | 242 | assertValidParse('true', true); 243 | assertValidParse('false', false); 244 | assertValidParse('null', null); 245 | assertValidParse('"foo"', 'foo'); 246 | assertValidParse('"\\"-\\\\-\\/-\\b-\\f-\\n-\\r-\\t"', '"-\\-/-\b-\f-\n-\r-\t'); 247 | assertValidParse('"\\u00DC"', 'Ü'); 248 | assertValidParse('9', 9); 249 | assertValidParse('-9', -9); 250 | assertValidParse('0.129', 0.129); 251 | assertValidParse('23e3', 23e3); 252 | assertValidParse('1.2E+3', 1.2E+3); 253 | assertValidParse('1.2E-3', 1.2E-3); 254 | assertValidParse('1.2E-3 // comment', 1.2E-3); 255 | }); 256 | 257 | test('parse: objects', () => { 258 | assertValidParse('{}', {}); 259 | assertValidParse('{ "foo": true }', { foo: true }); 260 | assertValidParse('{ "bar": 8, "xoo": "foo" }', { bar: 8, xoo: 'foo' }); 261 | assertValidParse('{ "hello": [], "world": {} }', { hello: [], world: {} }); 262 | assertValidParse('{ "a": false, "b": true, "c": [ 7.4 ] }', { a: false, b: true, c: [7.4] }); 263 | assertValidParse('{ "lineComment": "//", "blockComment": ["/*", "*/"], "brackets": [ ["{", "}"], ["[", "]"], ["(", ")"] ] }', { lineComment: '//', blockComment: ['/*', '*/'], brackets: [['{', '}'], ['[', ']'], ['(', ')']] }); 264 | assertValidParse('{ "hello": [], "world": {} }', { hello: [], world: {} }); 265 | assertValidParse('{ "hello": { "again": { "inside": 5 }, "world": 1 }}', { hello: { again: { inside: 5 }, world: 1 } }); 266 | assertValidParse('{ "foo": /*hello*/true }', { foo: true }); 267 | assertValidParse('{ "": true }', { '': true }); 268 | }); 269 | 270 | test('parse: arrays', () => { 271 | assertValidParse('[]', []); 272 | assertValidParse('[ [], [ [] ]]', [[], [[]]]); 273 | assertValidParse('[ 1, 2, 3 ]', [1, 2, 3]); 274 | assertValidParse('[ { "a": null } ]', [{ a: null }]); 275 | }); 276 | 277 | test('parse: objects with errors', () => { 278 | assertInvalidParse('{,}', {}); 279 | assertInvalidParse('{ "foo": true, }', { foo: true }); 280 | assertInvalidParse('{ "bar": 8 "xoo": "foo" }', { bar: 8, xoo: 'foo' }); 281 | assertInvalidParse('{ ,"bar": 8 }', { bar: 8 }); 282 | assertInvalidParse('{ ,"bar": 8, "foo" }', { bar: 8 }); 283 | assertInvalidParse('{ "bar": 8, "foo": }', { bar: 8 }); 284 | assertInvalidParse('{ 8, "foo": 9 }', { foo: 9 }); 285 | }); 286 | 287 | test('parse: array with errors', () => { 288 | assertInvalidParse('[,]', []); 289 | assertInvalidParse('[ 1 2, 3 ]', [1, 2, 3]); 290 | assertInvalidParse('[ ,1, 2, 3 ]', [1, 2, 3]); 291 | assertInvalidParse('[ ,1, 2, 3, ]', [1, 2, 3]); 292 | }); 293 | 294 | test('parse: errors', () => { 295 | assertInvalidParse('', undefined); 296 | assertInvalidParse('1,1', 1); 297 | }); 298 | 299 | test('parse: disallow comments', () => { 300 | let options = { disallowComments: true }; 301 | 302 | assertValidParse('[ 1, 2, null, "foo" ]', [1, 2, null, 'foo'], options); 303 | assertValidParse('{ "hello": [], "world": {} }', { hello: [], world: {} }, options); 304 | 305 | assertInvalidParse('{ "foo": /*comment*/ true }', { foo: true }, options); 306 | }); 307 | 308 | test('parse: trailing comma', () => { 309 | let options = { allowTrailingComma: true }; 310 | assertValidParse('{ "hello": [], }', { hello: [] }, options); 311 | assertValidParse('{ "hello": [] }', { hello: [] }, options); 312 | assertValidParse('{ "hello": [], "world": {}, }', { hello: [], world: {} }, options); 313 | assertValidParse('{ "hello": [], "world": {} }', { hello: [], world: {} }, options); 314 | assertValidParse('[ 1, 2, ]', [1, 2], options); 315 | assertValidParse('[ 1, 2 ]', [1, 2], options); 316 | 317 | assertInvalidParse('{ "hello": [], }', { hello: [] }); 318 | assertInvalidParse('{ "hello": [], "world": {}, }', { hello: [], world: {} }); 319 | assertInvalidParse('[ 1, 2, ]', [1, 2]); 320 | }); 321 | test('location: properties', () => { 322 | assertLocation('|{ "foo": "bar" }', [], void 0, false); 323 | assertLocation('{| "foo": "bar" }', [''], void 0, true); 324 | assertLocation('{ |"foo": "bar" }', ['foo'], 'property', true); 325 | assertLocation('{ "foo|": "bar" }', ['foo'], 'property', true); 326 | assertLocation('{ "foo"|: "bar" }', ['foo'], 'property', true); 327 | assertLocation('{ "foo": "bar"| }', ['foo'], 'string', false); 328 | assertLocation('{ "foo":| "bar" }', ['foo'], void 0, false); 329 | assertLocation('{ "foo": {"bar|": 1, "car": 2 } }', ['foo', 'bar'], 'property', true); 330 | assertLocation('{ "foo": {"bar": 1|, "car": 3 } }', ['foo', 'bar'], 'number', false); 331 | assertLocation('{ "foo": {"bar": 1,| "car": 4 } }', ['foo', ''], void 0, true); 332 | assertLocation('{ "foo": {"bar": 1, "ca|r": 5 } }', ['foo', 'car'], 'property', true); 333 | assertLocation('{ "foo": {"bar": 1, "car": 6| } }', ['foo', 'car'], 'number', false); 334 | assertLocation('{ "foo": {"bar": 1, "car": 7 }| }', ['foo'], void 0, false); 335 | assertLocation('{ "foo": {"bar": 1, "car": 8 },| "goo": {} }', [''], void 0, true); 336 | assertLocation('{ "foo": {"bar": 1, "car": 9 }, "go|o": {} }', ['goo'], 'property', true); 337 | assertLocation('{ "dep": {"bar": 1, "car": |', ['dep', 'car'], void 0, false); 338 | assertLocation('{ "dep": {"bar": 1,, "car": |', ['dep', 'car'], void 0, false); 339 | assertLocation('{ "dep": {"bar": "na", "dar": "ma", "car": | } }', ['dep', 'car'], void 0, false); 340 | }); 341 | 342 | test('location: arrays', () => { 343 | assertLocation('|["foo", null ]', [], void 0, false); 344 | assertLocation('[|"foo", null ]', [0], 'string', false); 345 | assertLocation('["foo"|, null ]', [0], 'string', false); 346 | assertLocation('["foo",| null ]', [1], void 0, false); 347 | assertLocation('["foo", |null ]', [1], 'null', false); 348 | assertLocation('["foo", null,| ]', [2], void 0, false); 349 | assertLocation('["foo", null,,| ]', [3], void 0, false); 350 | assertLocation('[["foo", null,, ],|', [1], void 0, false); 351 | }); 352 | 353 | test('tree: literals', () => { 354 | assertTree('true', { type: 'boolean', offset: 0, length: 4, value: true }); 355 | assertTree('false', { type: 'boolean', offset: 0, length: 5, value: false }); 356 | assertTree('null', { type: 'null', offset: 0, length: 4, value: null }); 357 | assertTree('23', { type: 'number', offset: 0, length: 2, value: 23 }); 358 | assertTree('-1.93e-19', { type: 'number', offset: 0, length: 9, value: -1.93e-19 }); 359 | assertTree('"hello"', { type: 'string', offset: 0, length: 7, value: 'hello' }); 360 | }); 361 | 362 | test('tree: arrays', () => { 363 | assertTree('[]', { type: 'array', offset: 0, length: 2, children: [] }); 364 | assertTree('[ 1 ]', { type: 'array', offset: 0, length: 5, children: [{ type: 'number', offset: 2, length: 1, value: 1 }] }); 365 | assertTree('[ 1,"x"]', { 366 | type: 'array', offset: 0, length: 8, children: [ 367 | { type: 'number', offset: 2, length: 1, value: 1 }, 368 | { type: 'string', offset: 4, length: 3, value: 'x' } 369 | ] 370 | }); 371 | assertTree('[[]]', { 372 | type: 'array', offset: 0, length: 4, children: [ 373 | { type: 'array', offset: 1, length: 2, children: [] } 374 | ] 375 | }); 376 | }); 377 | 378 | test('tree: objects', () => { 379 | assertTree('{ }', { type: 'object', offset: 0, length: 3, children: [] }); 380 | assertTree('{ "val": 1 }', { 381 | type: 'object', offset: 0, length: 12, children: [ 382 | { 383 | type: 'property', offset: 2, length: 8, colonOffset: 7, children: [ 384 | { type: 'string', offset: 2, length: 5, value: 'val' }, 385 | { type: 'number', offset: 9, length: 1, value: 1 } 386 | ] 387 | } 388 | ] 389 | }); 390 | assertTree('{"id": "$", "v": [ null, null] }', 391 | { 392 | type: 'object', offset: 0, length: 32, children: [ 393 | { 394 | type: 'property', offset: 1, length: 9, colonOffset: 5, children: [ 395 | { type: 'string', offset: 1, length: 4, value: 'id' }, 396 | { type: 'string', offset: 7, length: 3, value: '$' } 397 | ] 398 | }, 399 | { 400 | type: 'property', offset: 12, length: 18, colonOffset: 15, children: [ 401 | { type: 'string', offset: 12, length: 3, value: 'v' }, 402 | { 403 | type: 'array', offset: 17, length: 13, children: [ 404 | { type: 'null', offset: 19, length: 4, value: null }, 405 | { type: 'null', offset: 25, length: 4, value: null } 406 | ] 407 | } 408 | ] 409 | } 410 | ] 411 | } 412 | ); 413 | assertTree('{ "id": { "foo": { } } , }', 414 | { 415 | type: 'object', offset: 0, length: 27, children: [ 416 | { 417 | type: 'property', offset: 3, length: 20, colonOffset: 7, children: [ 418 | { type: 'string', offset: 3, length: 4, value: 'id' }, 419 | { 420 | type: 'object', offset: 9, length: 14, children: [ 421 | { 422 | type: 'property', offset: 11, length: 10, colonOffset: 16, children: [ 423 | { type: 'string', offset: 11, length: 5, value: 'foo' }, 424 | { type: 'object', offset: 18, length: 3, children: [] } 425 | ] 426 | } 427 | ] 428 | } 429 | ] 430 | } 431 | ] 432 | }, [ 433 | { error: ParseErrorCode.PropertyNameExpected, offset: 26, length: 1 }, 434 | { error: ParseErrorCode.ValueExpected, offset: 26, length: 1 } 435 | ]); 436 | }); 437 | 438 | test('visit: object', () => { 439 | assertVisit('{ }', [ 440 | { id: 'onObjectBegin', text: '{', startLine: 0, startCharacter: 0, path: [] }, 441 | { id: 'onObjectEnd', text: '}', startLine: 0, startCharacter: 2 }, 442 | ]); 443 | assertVisit('{ "foo": "bar" }', [ 444 | { id: 'onObjectBegin', text: '{', startLine: 0, startCharacter: 0, path: [] }, 445 | { id: 'onObjectProperty', text: '"foo"', startLine: 0, startCharacter: 2, arg: 'foo', path: [] }, 446 | { id: 'onSeparator', text: ':', startLine: 0, startCharacter: 7, arg: ':' }, 447 | { id: 'onLiteralValue', text: '"bar"', startLine: 0, startCharacter: 9, arg: 'bar', path: ['foo'] }, 448 | { id: 'onObjectEnd', text: '}', startLine: 0, startCharacter: 15 }, 449 | ]); 450 | assertVisit('{ "foo": { "goo": 3 } }', [ 451 | { id: 'onObjectBegin', text: '{', startLine: 0, startCharacter: 0, path: [] }, 452 | { id: 'onObjectProperty', text: '"foo"', startLine: 0, startCharacter: 2, arg: 'foo', path: [] }, 453 | { id: 'onSeparator', text: ':', startLine: 0, startCharacter: 7, arg: ':' }, 454 | { id: 'onObjectBegin', text: '{', startLine: 0, startCharacter: 9, path: ['foo'] }, 455 | { id: 'onObjectProperty', text: '"goo"', startLine: 0, startCharacter: 11, arg: 'goo', path: ['foo'] }, 456 | { id: 'onSeparator', text: ':', startLine: 0, startCharacter: 16, arg: ':' }, 457 | { id: 'onLiteralValue', text: '3', startLine: 0, startCharacter: 18, arg: 3, path: ['foo', 'goo'] }, 458 | { id: 'onObjectEnd', text: '}', startLine: 0, startCharacter: 20 }, 459 | { id: 'onObjectEnd', text: '}', startLine: 0, startCharacter: 22 }, 460 | ]); 461 | assertVisit('{ "foo": "bar", "a": {"b": "c"} }', [ 462 | { id: 'onObjectBegin', text: '{', startLine: 0, startCharacter: 0, path: [] }, 463 | { id: 'onObjectEnd', text: '}', startLine: 0, startCharacter: 32 }, 464 | ], [], false, [0]); 465 | assertVisit('{ "a": { "b": "c", "d": { "e": "f" } } }', [ 466 | { id: 'onObjectBegin', text: '{', startLine: 0, startCharacter: 0, path: [] }, 467 | { id: 'onObjectProperty', text: '"a"', startLine: 0, startCharacter: 2, arg: 'a', path: [] }, 468 | { id: 'onSeparator', text: ':', startLine: 0, startCharacter: 5, arg: ':' }, 469 | { id: 'onObjectBegin', text: '{', startLine: 0, startCharacter: 7, path: ['a'] }, 470 | { id: 'onObjectEnd', text: '}', startLine: 0, startCharacter: 37 }, 471 | { id: 'onObjectEnd', text: '}', startLine: 0, startCharacter: 39 } 472 | ], [], true, [7]); 473 | }); 474 | 475 | test('visit: array', () => { 476 | assertVisit('[]', [ 477 | { id: 'onArrayBegin', text: '[', startLine: 0, startCharacter: 0, path: [] }, 478 | { id: 'onArrayEnd', text: ']', startLine: 0, startCharacter: 1 }, 479 | ]); 480 | assertVisit('[ true, null, [] ]', [ 481 | { id: 'onArrayBegin', text: '[', startLine: 0, startCharacter: 0, path: [] }, 482 | { id: 'onLiteralValue', text: 'true', startLine: 0, startCharacter: 2, arg: true, path: [0] }, 483 | { id: 'onSeparator', text: ',', startLine: 0, startCharacter: 6, arg: ',' }, 484 | { id: 'onLiteralValue', text: 'null', startLine: 0, startCharacter: 8, arg: null, path: [1] }, 485 | { id: 'onSeparator', text: ',', startLine: 0, startCharacter: 12, arg: ',' }, 486 | { id: 'onArrayBegin', text: '[', startLine: 0, startCharacter: 14, path: [2] }, 487 | { id: 'onArrayEnd', text: ']', startLine: 0, startCharacter: 15 }, 488 | { id: 'onArrayEnd', text: ']', startLine: 0, startCharacter: 17 }, 489 | ]); 490 | assertVisit('[\r\n0,\r\n1,\r\n2\r\n]', [ 491 | { id: 'onArrayBegin', text: '[', startLine: 0, startCharacter: 0, path: [] }, 492 | { id: 'onLiteralValue', text: '0', startLine: 1, startCharacter: 0, arg: 0, path: [0] }, 493 | { id: 'onSeparator', text: ',', startLine: 1, startCharacter: 1, arg: ',' }, 494 | { id: 'onLiteralValue', text: '1', startLine: 2, startCharacter: 0, arg: 1, path: [1] }, 495 | { id: 'onSeparator', text: ',', startLine: 2, startCharacter: 1, arg: ',' }, 496 | { id: 'onLiteralValue', text: '2', startLine: 3, startCharacter: 0, arg: 2, path: [2] }, 497 | { id: 'onArrayEnd', text: ']', startLine: 4, startCharacter: 0 }, 498 | ]); 499 | }); 500 | 501 | test('visit: object & array', () => { 502 | assertVisit('[ { "p1": [ { "p11": 1, "p12": [ true, [ false, 2 ] ] } ] } ]', [ 503 | { id: 'onArrayBegin', text: '[', startLine: 0, startCharacter: 0, path: [] }, 504 | { id: 'onObjectBegin', text: '{', startLine: 0, startCharacter: 2, path: [0] }, 505 | { id: 'onObjectProperty', text: '"p1"', startLine: 0, startCharacter: 4, arg: 'p1', path: [0] }, 506 | { id: 'onSeparator', text: ':', startLine: 0, startCharacter: 8, arg: ':' }, 507 | { id: 'onArrayBegin', text: '[', startLine: 0, startCharacter: 10, path: [0, 'p1'] }, 508 | { id: 'onObjectBegin', text: '{', startLine: 0, startCharacter: 12, path: [0, 'p1', 0] }, 509 | { id: 'onObjectProperty', text: '"p11"', startLine: 0, startCharacter: 14, arg: 'p11', path: [0, 'p1', 0] }, 510 | { id: 'onSeparator', text: ':', startLine: 0, startCharacter: 19, arg: ':' }, 511 | { id: 'onLiteralValue', text: '1', startLine: 0, startCharacter: 21, arg: 1, path: [0, 'p1', 0, 'p11'] }, 512 | { id: 'onSeparator', text: ',', startLine: 0, startCharacter: 22, arg: ',' }, 513 | { id: 'onObjectProperty', text: '"p12"', startLine: 0, startCharacter: 24, arg: 'p12', path: [0, 'p1', 0] }, 514 | { id: 'onSeparator', text: ':', startLine: 0, startCharacter: 29, arg: ':' }, 515 | { id: 'onArrayBegin', text: '[', startLine: 0, startCharacter: 31, path: [0, 'p1', 0, 'p12'] }, 516 | { id: 'onLiteralValue', text: 'true', startLine: 0, startCharacter: 33, arg: true, path: [0, 'p1', 0, 'p12', 0] }, 517 | { id: 'onSeparator', text: ',', startLine: 0, startCharacter: 37, arg: ',' }, 518 | { id: 'onArrayBegin', text: '[', startLine: 0, startCharacter: 39, path: [0, 'p1', 0, 'p12', 1] }, 519 | { id: 'onLiteralValue', text: 'false', startLine: 0, startCharacter: 41, arg: false, path: [0, 'p1', 0, 'p12', 1, 0] }, 520 | { id: 'onSeparator', text: ',', startLine: 0, startCharacter: 46, arg: ',' }, 521 | { id: 'onLiteralValue', text: '2', startLine: 0, startCharacter: 48, arg: 2, path: [0, 'p1', 0, 'p12', 1, 1] }, 522 | { id: 'onArrayEnd', text: ']', startLine: 0, startCharacter: 50 }, 523 | { id: 'onArrayEnd', text: ']', startLine: 0, startCharacter: 52 }, 524 | { id: 'onObjectEnd', text: '}', startLine: 0, startCharacter: 54 }, 525 | { id: 'onArrayEnd', text: ']', startLine: 0, startCharacter: 56 }, 526 | { id: 'onObjectEnd', text: '}', startLine: 0, startCharacter: 58 }, 527 | { id: 'onArrayEnd', text: ']', startLine: 0, startCharacter: 60 }, 528 | ]); 529 | assertVisit('{ "foo": [ { "a": "b", "c:": "d", "d": { "e": "f" } } ] }', [ 530 | { id: 'onObjectBegin', text: '{', startLine: 0, startCharacter: 0, path: [] }, 531 | { id: 'onObjectProperty', text: '"foo"', startLine: 0, startCharacter: 2, arg: 'foo', path: [] }, 532 | { id: 'onSeparator', text: ':', startLine: 0, startCharacter: 7, arg: ':' }, 533 | { id: 'onArrayBegin', text: '[', startLine: 0, startCharacter: 9, path: ['foo'] }, 534 | { id: 'onArrayEnd', text: ']', startLine: 0, startCharacter: 54 }, 535 | { id: 'onObjectEnd', text: '}', startLine: 0, startCharacter: 56 } 536 | ], [], true, [9]); 537 | }); 538 | 539 | test('visit: comment', () => { 540 | assertVisit('/* g */ { "foo": //f\n"bar" }', [ 541 | { id: 'onComment', text: '/* g */', startLine: 0, startCharacter: 0 }, 542 | { id: 'onObjectBegin', text: '{', startLine: 0, startCharacter: 8, path: [] }, 543 | { id: 'onObjectProperty', text: '"foo"', startLine: 0, startCharacter: 10, arg: 'foo', path: [] }, 544 | { id: 'onSeparator', text: ':', startLine: 0, startCharacter: 15, arg: ':' }, 545 | { id: 'onComment', text: '//f', startLine: 0, startCharacter: 17 }, 546 | { id: 'onLiteralValue', text: '"bar"', startLine: 1, startCharacter: 0, arg: 'bar', path: ['foo'] }, 547 | { id: 'onObjectEnd', text: '}', startLine: 1, startCharacter: 6 }, 548 | ]); 549 | assertVisit('/* g\r\n */ { "foo": //f\n"bar" }', [ 550 | { id: 'onComment', text: '/* g\r\n */', startLine: 0, startCharacter: 0 }, 551 | { id: 'onObjectBegin', text: '{', startLine: 1, startCharacter: 4, path: [] }, 552 | { id: 'onObjectProperty', text: '"foo"', startLine: 1, startCharacter: 6, arg: 'foo', path: [] }, 553 | { id: 'onSeparator', text: ':', startLine: 1, startCharacter: 11, arg: ':' }, 554 | { id: 'onComment', text: '//f', startLine: 1, startCharacter: 13 }, 555 | { id: 'onLiteralValue', text: '"bar"', startLine: 2, startCharacter: 0, arg: 'bar', path: ['foo'] }, 556 | { id: 'onObjectEnd', text: '}', startLine: 2, startCharacter: 6 }, 557 | ]); 558 | assertVisit('/* g\n */ { "foo": //f\n"bar"\n}', 559 | [ 560 | { id: 'onObjectBegin', text: '{', startLine: 1, startCharacter: 4, path: [] }, 561 | { id: 'onObjectProperty', text: '"foo"', startLine: 1, startCharacter: 6, arg: 'foo', path: [] }, 562 | { id: 'onSeparator', text: ':', startLine: 1, startCharacter: 11, arg: ':' }, 563 | { id: 'onLiteralValue', text: '"bar"', startLine: 2, startCharacter: 0, arg: 'bar', path: ['foo'] }, 564 | { id: 'onObjectEnd', text: '}', startLine: 3, startCharacter: 0 }, 565 | ], 566 | [ 567 | { error: ParseErrorCode.InvalidCommentToken, offset: 0, length: 8, startLine: 0, startCharacter: 0 }, 568 | { error: ParseErrorCode.InvalidCommentToken, offset: 18, length: 3, startLine: 1, startCharacter: 13 }, 569 | ], 570 | true 571 | ); 572 | }); 573 | 574 | test('visit: malformed', () => { 575 | // Note: The expected visitor calls below heavily depend on implementation details; they don't 576 | // dictate how exactly malformed JSON should be parsed 577 | assertVisit('[ { "a", "b": [] 1, "c" [ 1 2 ], true: "d":, "e": }, 2, 3 4 ]', 578 | [ 579 | { id: 'onArrayBegin', text: '[', startLine: 0, startCharacter: 0, path: [] }, 580 | { id: 'onObjectBegin', text: "{", startLine: 0, startCharacter: 2, path: [0] }, 581 | { id: 'onObjectProperty', text: '"a"', startLine: 0, startCharacter: 4, arg: 'a', path: [0] }, 582 | { id: 'onSeparator', text: ',', startLine: 0, startCharacter: 7, arg: ',' }, 583 | { id: 'onObjectProperty', text: '"b"', startLine: 0, startCharacter: 9, arg: 'b', path: [0] }, 584 | { id: 'onSeparator', text: ':', startLine: 0, startCharacter: 12, arg: ':' }, 585 | { id: 'onArrayBegin', text: '[', startLine: 0, startCharacter: 14, path: [0, 'b'] }, 586 | { id: 'onArrayEnd', text: ']', startLine: 0, startCharacter: 15 }, 587 | { id: 'onSeparator', text: ',', startLine: 0, startCharacter: 18, arg: ',' }, 588 | { id: 'onObjectProperty', text: '"c"', startLine: 0, startCharacter: 20, arg: 'c', path: [0] }, 589 | { id: 'onSeparator', text: ',', startLine: 0, startCharacter: 31, arg: ',' }, 590 | { id: 'onSeparator', text: ',', startLine: 0, startCharacter: 43, arg: ',' }, 591 | { id: 'onObjectProperty', text: '"e"', startLine: 0, startCharacter: 45, arg: 'e', path: [0] }, 592 | { id: 'onSeparator', text: ':', startLine: 0, startCharacter: 48, arg: ':' }, 593 | { id: 'onObjectEnd', text: '}', startLine: 0, startCharacter: 50 }, 594 | { id: 'onSeparator', text: ',', startLine: 0, startCharacter: 51, arg: ',' }, 595 | { id: 'onLiteralValue', text: '2', startLine: 0, startCharacter: 53, arg: 2, path: [1] }, 596 | { id: 'onSeparator', text: ',', startLine: 0, startCharacter: 54, arg: ',' }, 597 | { id: 'onLiteralValue', text: '3', startLine: 0, startCharacter: 56, arg: 3, path: [2] }, 598 | { id: 'onLiteralValue', text: '4', startLine: 0, startCharacter: 58, arg: 4, path: [3] }, 599 | { id: 'onArrayEnd', text: ']', startLine: 0, startCharacter: 60 }, 600 | ], 601 | [ 602 | { error: ParseErrorCode.ColonExpected, offset: 7, length: 1, startLine: 0, startCharacter: 7 }, 603 | { error: ParseErrorCode.CommaExpected, offset: 17, length: 1, startLine: 0, startCharacter: 17 }, 604 | { error: ParseErrorCode.PropertyNameExpected, offset: 17, length: 1, startLine: 0, startCharacter: 17 }, 605 | { error: ParseErrorCode.ValueExpected, offset: 18, length: 1, startLine: 0, startCharacter: 18 }, 606 | { error: ParseErrorCode.ColonExpected, offset: 24, length: 1, startLine: 0, startCharacter: 24 }, 607 | { error: ParseErrorCode.PropertyNameExpected, offset: 33, length: 4, startLine: 0, startCharacter: 33 }, 608 | { error: ParseErrorCode.ValueExpected, offset: 43, length: 1, startLine: 0, startCharacter: 43 }, 609 | { error: ParseErrorCode.ValueExpected, offset: 50, length: 1, startLine: 0, startCharacter: 50 }, 610 | { error: ParseErrorCode.CommaExpected, offset: 58, length: 1, startLine: 0, startCharacter: 58 }, 611 | ] 612 | ); 613 | }); 614 | 615 | test('visit: incomplete', () => { 616 | assertVisit('{"prop1":"foo","prop2":"foo2","prop3":{"prp1":{""}}}', 617 | [ 618 | { id: 'onObjectBegin', text: "{", startLine: 0, startCharacter: 0, path: [] }, 619 | { id: 'onObjectProperty', text: '"prop1"', startLine: 0, startCharacter: 1, arg: 'prop1', path: [] }, 620 | { id: 'onSeparator', text: ":", startLine: 0, startCharacter: 8, arg: ":" }, 621 | { id: 'onLiteralValue', text: '"foo"', startLine: 0, startCharacter: 9, arg: 'foo', path: ['prop1'] }, 622 | { id: 'onSeparator', text: ",", startLine: 0, startCharacter: 14, arg: "," }, 623 | { id: 'onObjectProperty', text: '"prop2"', startLine: 0, startCharacter: 15, arg: 'prop2', path: [] }, 624 | { id: 'onSeparator', text: ":", startLine: 0, startCharacter: 22, arg: ":" }, 625 | { id: 'onLiteralValue', text: '"foo2"', startLine: 0, startCharacter: 23, arg: 'foo2', path: ['prop2'] }, 626 | { id: 'onSeparator', text: ",", startLine: 0, startCharacter: 29, arg: "," }, 627 | { id: 'onObjectProperty', text: '"prop3"', startLine: 0, startCharacter: 30, arg: 'prop3', path: [] }, 628 | { id: 'onSeparator', text: ":", startLine: 0, startCharacter: 37, arg: ":" }, 629 | { id: 'onObjectBegin', text: "{", startLine: 0, startCharacter: 38, path: ['prop3'] }, 630 | { id: 'onObjectProperty', text: '"prp1"', startLine: 0, startCharacter: 39, arg: 'prp1', path: ['prop3'] }, 631 | { id: 'onSeparator', text: ":", startLine: 0, startCharacter: 45, arg: ':' }, 632 | { id: 'onObjectBegin', text: "{", startLine: 0, startCharacter: 46, path: ['prop3', 'prp1'] }, 633 | { id: 'onObjectProperty', text: '""', startLine: 0, startCharacter: 47, arg: '', path: ['prop3', 'prp1'] }, 634 | { id: 'onObjectEnd', text: "}", startLine: 0, startCharacter: 49 }, 635 | { id: 'onObjectEnd', text: "}", startLine: 0, startCharacter: 50 }, 636 | { id: 'onObjectEnd', text: "}", startLine: 0, startCharacter: 51 }, 637 | ], 638 | [ 639 | { error: ParseErrorCode.ColonExpected, offset: 49, length: 1, startLine: 0, startCharacter: 49 }, 640 | ] 641 | ); 642 | 643 | assertTree('{"prop1":"foo","prop2":"foo2","prop3":{"prp1":{""}}}', { 644 | type: 'object', offset: 0, length: 52, children: [ 645 | { 646 | type: 'property', offset: 1, length: 13, children: [ 647 | { type: 'string', value: 'prop1', offset: 1, length: 7 }, 648 | { type: 'string', offset: 9, length: 5, value: 'foo' } 649 | ], colonOffset: 8 650 | }, { 651 | type: 'property', offset: 15, length: 14, children: [ 652 | { type: 'string', value: 'prop2', offset: 15, length: 7 }, 653 | { type: 'string', offset: 23, length: 6, value: 'foo2' } 654 | ], colonOffset: 22 655 | }, 656 | { 657 | type: 'property', offset: 30, length: 21, children: [ 658 | { type: 'string', value: 'prop3', offset: 30, length: 7 }, 659 | { 660 | type: 'object', offset: 38, length: 13, children: [ 661 | { 662 | type: 'property', offset: 39, length: 11, children: [ 663 | { type: 'string', value: 'prp1', offset: 39, length: 6 }, 664 | { 665 | type: 'object', offset: 46, length: 4, children: [ 666 | { 667 | type: 'property', offset: 47, length: 3, children: [ 668 | { type: 'string', value: '', offset: 47, length: 2 }, 669 | ] 670 | } 671 | ] 672 | } 673 | ], colonOffset: 45 674 | } 675 | ] 676 | } 677 | ], colonOffset: 37 678 | } 679 | ] 680 | }, [{ error: ParseErrorCode.ColonExpected, offset: 49, length: 1 }]); 681 | }); 682 | 683 | test('tree: find location', () => { 684 | let root = parseTree('{ "key1": { "key11": [ "val111", "val112" ] }, "key2": [ { "key21": false, "key22": 221 }, null, [{}] ], "key3": { "key31":, "key32": 32 } }'); 685 | assertNodeAtLocation(root, ['key1'], { key11: ['val111', 'val112'] }); 686 | assertNodeAtLocation(root, ['key1', 'key11'], ['val111', 'val112']); 687 | assertNodeAtLocation(root, ['key1', 'key11', 0], 'val111'); 688 | assertNodeAtLocation(root, ['key1', 'key11', 1], 'val112'); 689 | assertNodeAtLocation(root, ['key1', 'key11', 2], void 0); 690 | assertNodeAtLocation(root, ['key2', 0, 'key21'], false); 691 | assertNodeAtLocation(root, ['key2', 0, 'key22'], 221); 692 | assertNodeAtLocation(root, ['key2', 1], null); 693 | assertNodeAtLocation(root, ['key2', 2], [{}]); 694 | assertNodeAtLocation(root, ['key2', 2, 0], {}); 695 | assertNodeAtLocation(root, ['key3', 'key31', 'key311'], undefined); 696 | assertNodeAtLocation(root, ['key3', 'key32'], 32); 697 | }); 698 | 699 | test('location: matches', () => { 700 | assertMatchesLocation('{ "dependencies": { | } }', ['dependencies']); 701 | assertMatchesLocation('{ "dependencies": { "fo| } }', ['dependencies']); 702 | assertMatchesLocation('{ "dependencies": { "fo|" } }', ['dependencies']); 703 | assertMatchesLocation('{ "dependencies": { "fo|": 1 } }', ['dependencies']); 704 | assertMatchesLocation('{ "dependencies": { "fo|": 1 } }', ['dependencies']); 705 | assertMatchesLocation('{ "dependencies": { "fo": | } }', ['dependencies', '*']); 706 | }); 707 | 708 | 709 | }); 710 | -------------------------------------------------------------------------------- /src/test/string-intern.test.ts: -------------------------------------------------------------------------------- 1 | import { cachedBreakLinesWithSpaces, cachedSpaces, supportedEols } from '../impl/string-intern'; 2 | import * as assert from 'assert'; 3 | 4 | suite('string intern', () => { 5 | test('should correctly define spaces intern', () => { 6 | for (let i = 0; i < cachedSpaces.length; i++) { 7 | assert.strictEqual(cachedSpaces[i], ' '.repeat(i)); 8 | } 9 | }); 10 | 11 | test('should correctly define break lines with spaces intern', () => { 12 | for (const indentType of [' ', '\t'] as const) { 13 | for (const eol of supportedEols) { 14 | for (let i = 0; i < cachedBreakLinesWithSpaces[indentType][eol].length; i++) { 15 | assert.strictEqual(cachedBreakLinesWithSpaces[indentType][eol][i], eol + indentType.repeat(i)); 16 | } 17 | } 18 | } 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /src/tsconfig.esm.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2020", 4 | "module": "es6", 5 | "moduleResolution": "node", 6 | "sourceMap": true, 7 | "declaration": true, 8 | "stripInternal": true, 9 | "outDir": "../lib/esm", 10 | "strict": true, 11 | "preserveConstEnums": true, 12 | "lib": [ 13 | "es2020" 14 | ] 15 | } 16 | } -------------------------------------------------------------------------------- /src/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2020", 4 | "module": "umd", 5 | "moduleResolution": "node", 6 | "sourceMap": true, 7 | "declaration": true, 8 | "stripInternal": true, 9 | "outDir": "../lib/umd", 10 | "strict": true, 11 | "preserveConstEnums": true, 12 | "lib": [ 13 | "es2020" 14 | ] 15 | } 16 | } --------------------------------------------------------------------------------