├── .github └── workflows │ └── ci.yml ├── .gitignore ├── LICENSE ├── README.md ├── _test.deps.ts ├── deno.json ├── deno.lock ├── dprint.json ├── mod.test.ts ├── mod.ts ├── scripts └── build_npm.ts └── utils ├── string_utils.test.ts └── string_utils.ts /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: push 3 | jobs: 4 | test-library: 5 | name: Deno test 6 | permissions: 7 | contents: read 8 | id-token: write 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - uses: actions/checkout@v4 13 | - uses: denoland/setup-deno@v1 14 | - name: Run tests 15 | run: deno test 16 | 17 | - name: Get tag version 18 | if: startsWith(github.ref, 'refs/tags/') 19 | id: get_tag_version 20 | run: echo ::set-output name=TAG_VERSION::${GITHUB_REF/refs\/tags\//} 21 | - uses: actions/setup-node@v2 22 | with: 23 | node-version: '18.x' 24 | registry-url: 'https://registry.npmjs.org' 25 | - name: npm build 26 | run: deno task build:npm ${{steps.get_tag_version.outputs.TAG_VERSION}} 27 | - name: jsr publish 28 | run: deno run -A jsr:@david/publish-on-tag@0.1.3 29 | - name: npm publish 30 | if: startsWith(github.ref, 'refs/tags/') 31 | env: 32 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 33 | run: | 34 | cd npm 35 | npm publish 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | *.js.map 3 | npm 4 | .vscode 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-2024 David Sherret 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 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # code-block-writer 2 | 3 | [![npm version](https://badge.fury.io/js/code-block-writer.svg)](https://badge.fury.io/js/code-block-writer) 4 | [![CI](https://github.com/dsherret/code-block-writer/workflows/CI/badge.svg)](https://github.com/dsherret/code-block-writer/actions?query=workflow%3ACI) 5 | [![JSR](https://jsr.io/badges/@david/code-block-writer)](https://jsr.io/@david/code-block-writer) 6 | [![stable](http://badges.github.io/stability-badges/dist/stable.svg)](http://github.com/badges/stability-badges) 7 | 8 | Code writer for JavaScript and TypeScript code. 9 | 10 | With Deno: 11 | 12 | ```ts 13 | deno add jsr:@david/code-block-writer 14 | ``` 15 | 16 | Or with Node: 17 | 18 | ``` 19 | npm install --save code-block-writer 20 | ``` 21 | 22 | ## Example 23 | 24 | 25 | 26 | ```typescript 27 | // import CodeBlockWriter from "code-block-writer"; // for npm 28 | import CodeBlockWriter from "@david/code-block-writer"; 29 | 30 | const writer = new CodeBlockWriter({ 31 | // optional options 32 | newLine: "\r\n", // default: "\n" 33 | indentNumberOfSpaces: 2, // default: 4 34 | useTabs: false, // default: false 35 | useSingleQuote: true // default: false 36 | }); 37 | 38 | writer.write("class MyClass extends OtherClass").block(() => { 39 | writer.writeLine(`@MyDecorator(1, 2)`); 40 | writer.write(`myMethod(myParam: any)`).block(() => { 41 | writer.write("return this.post(").quote("myArgument").write(");"); 42 | }); 43 | }); 44 | 45 | console.log(writer.toString()); 46 | ``` 47 | 48 | Outputs (using "\r\n" for newlines): 49 | 50 | 51 | 52 | ```js 53 | class MyClass extends OtherClass { 54 | @MyDecorator(1, 2) 55 | myMethod(myParam: any) { 56 | return this.post('myArgument'); 57 | } 58 | } 59 | ``` 60 | 61 | ## Methods 62 | 63 | - `block(block?: () => void)` - Indents all the code written within and surrounds it in braces. 64 | - `inlineBlock(block?: () => void)` - Same as block, but doesn't add a space before the first brace and doesn't add a newline at the end. 65 | - `getLength()` - Get the current number of characters. 66 | - `writeLine(text: string)` - Writes some text and adds a newline. 67 | - `newLine()` - Writes a newline. 68 | - `newLineIfLastNot()` - Writes a newline if what was written last wasn't a newline. 69 | - `blankLine()` - Writes a blank line. Does not allow consecutive blank lines. 70 | - `blankLineIfLastNot()` - Writes a blank line if what was written last wasn't a blank line. 71 | - `quote()` - Writes a quote character. 72 | - `quote(text: string)` - Writes text surrounded in quotes. 73 | - `indent(times?: number)` - Indents the current line. Optionally indents multiple times when providing a number. 74 | - `indent(block?: () => void)` - Indents a block of code. 75 | - `space(times?: number)` - Writes a space. Optionally writes multiple spaces when providing a number. 76 | - `spaceIfLastNot()` - Writes a space if the last was not a space. 77 | - `tab(times?: number)` - Writes a tab. Optionally writes multiple tabs when providing a number. 78 | - `tabIfLastNot()` - Writes a tab if the last was not a tab. 79 | - `write(text: string)` - Writes some text. 80 | - `conditionalNewLine(condition: boolean)` - Writes a newline if the condition is matched. 81 | - `conditionalBlankLine(condition: boolean)` - Writes a blank line if the condition is matched. 82 | - `conditionalWrite(condition: boolean, text: string)` - Writes if the condition is matched. 83 | - `conditionalWrite(condition: boolean, textFunc: () => string)` - Writes if the condition is matched. 84 | - `conditionalWriteLine(condition: boolean, text: string)` - Writes some text and adds a newline if the condition is matched. 85 | - `conditionalWriteLine(condition: boolean, textFunc: () => string)` - Writes some text and adds a newline if the condition is matched. 86 | - `setIndentationLevel(indentationLevel: number)` - Sets the current indentation level. 87 | - `setIndentationLevel(whitespaceText: string)` - Sets the current indentation level based on the provided whitespace text. 88 | - `withIndentationLevel(indentationLevel: number, action: () => void)` - Sets the indentation level within the provided action. 89 | - `withIndentationLevel(whitespaceText: string, action: () => void)` - Sets the indentation level based on the provided whitespace text within the action. 90 | - `getIndentationLevel()` - Gets the current indentation level. 91 | - `queueIndentationLevel(indentationLevel: number)` - Queues an indentation level to be used once a new line is written. 92 | - `queueIndentationLevel(whitespaceText: string)` - Queues an indentation level to be used once a new line is written based on the provided whitespace text. 93 | - `hangingIndent(action: () => void)` - Writes the code within the action with hanging indentation. 94 | - `hangingIndentUnlessBlock(action: () => void)` - Writes the code within the action with hanging indentation unless a block is written going from the first line to the second. 95 | - `closeComment()` - Writes text to exit a comment if in a comment. 96 | - `unsafeInsert(pos: number, text: string)` - Inserts text into the writer. This will not update the writer's state. Read more in its jsdoc. 97 | - `isInComment()` - Gets if the writer is currently in a comment. 98 | - `isAtStartOfFirstLineOfBlock()` - Gets if the writer is currently at the start of the first line of the text, block, or indentation block. 99 | - `isOnFirstLineOfBlock()` - Gets if the writer is currently on the first line of the text, block, or indentation block. 100 | - `isInString()` - Gets if the writer is currently in a string. 101 | - `isLastNewLine()` - Gets if the writer last wrote a newline. 102 | - `isLastBlankLine()` - Gets if the writer last wrote a blank line. 103 | - `isLastSpace()` - Gets if the writer last wrote a space. 104 | - `isLastTab()` - Gets if the writer last wrote a tab. 105 | - `getLastChar()` - Gets the last character written. 106 | - `endsWith(text: string)` - Gets if the writer ends with the provided text. 107 | - `iterateLastChars(action: (char: string, index: number) => T | undefined): T | undefined` - Iterates over the writer's characters in reverse order, stopping once a non-null or undefined value is returned and returns that value. 108 | - `iterateLastCharCodes(action: (charCode: number, index: number) => T | undefined): T | undefined` - A slightly faster version of `iterateLastChars` that doesn't allocate a string per character. 109 | - `getOptions()` - Gets the writer options. 110 | - `toString()` - Gets the string. 111 | 112 | ## Other Features 113 | 114 | - Does not indent within strings. 115 | - Escapes newlines within double and single quotes created with `.quote(text)`. 116 | 117 | ## C# Version 118 | 119 | See [CodeBlockWriterSharp](https://github.com/dsherret/CodeBlockWriterSharp). 120 | -------------------------------------------------------------------------------- /_test.deps.ts: -------------------------------------------------------------------------------- 1 | export { describe, it } from "https://deno.land/std@0.193.0/testing/bdd.ts"; 2 | // @deno-types="npm:@types/chai@4.3" 3 | export { expect } from "npm:chai@4.3.7"; 4 | -------------------------------------------------------------------------------- /deno.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@david/code-block-writer", 3 | "version": "0.0.0", 4 | "exports": "./mod.ts", 5 | "tasks": { 6 | "build:npm": "deno run -A ./scripts/build_npm.ts" 7 | }, 8 | "exclude": [ 9 | "npm" 10 | ], 11 | "imports": { 12 | "@deno/dnt": "jsr:@deno/dnt@^0.41.1" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /deno.lock: -------------------------------------------------------------------------------- 1 | { 2 | "version": "3", 3 | "packages": { 4 | "specifiers": { 5 | "jsr:@deno/cache-dir@^0.8.0": "jsr:@deno/cache-dir@0.8.0", 6 | "jsr:@deno/dnt@^0.41.1": "jsr:@deno/dnt@0.41.1", 7 | "jsr:@std/assert@^0.218.2": "jsr:@std/assert@0.218.2", 8 | "jsr:@std/bytes@^0.218.2": "jsr:@std/bytes@0.218.2", 9 | "jsr:@std/fmt@^0.218.2": "jsr:@std/fmt@0.218.2", 10 | "jsr:@std/fs@^0.218.2": "jsr:@std/fs@0.218.2", 11 | "jsr:@std/io@^0.218.2": "jsr:@std/io@0.218.2", 12 | "jsr:@std/path@^0.218.2": "jsr:@std/path@0.218.2", 13 | "npm:@ts-morph/bootstrap@0.22": "npm:@ts-morph/bootstrap@0.22.0", 14 | "npm:@types/chai@4.3": "npm:@types/chai@4.3.14", 15 | "npm:chai@4.3.7": "npm:chai@4.3.7", 16 | "npm:code-block-writer@^13.0.1": "npm:code-block-writer@13.0.1" 17 | }, 18 | "jsr": { 19 | "@deno/cache-dir@0.8.0": { 20 | "integrity": "e87e80a404958f6350d903e6238b72afb92468378b0b32111f7a1e4916ac7fe7", 21 | "dependencies": [ 22 | "jsr:@std/fmt@^0.218.2", 23 | "jsr:@std/fs@^0.218.2", 24 | "jsr:@std/io@^0.218.2", 25 | "jsr:@std/path@^0.218.2" 26 | ] 27 | }, 28 | "@deno/dnt@0.41.1": { 29 | "integrity": "8746a773e031ae19ef43d0eece850217b76cf1d0118fdd8e059652d7023d4aff", 30 | "dependencies": [ 31 | "jsr:@deno/cache-dir@^0.8.0", 32 | "jsr:@std/fmt@^0.218.2", 33 | "jsr:@std/fs@^0.218.2", 34 | "jsr:@std/path@^0.218.2", 35 | "npm:@ts-morph/bootstrap@0.22", 36 | "npm:code-block-writer@^13.0.1" 37 | ] 38 | }, 39 | "@std/assert@0.218.2": { 40 | "integrity": "7f0a5a1a8cf86607cd6c2c030584096e1ffad27fc9271429a8cb48cfbdee5eaf" 41 | }, 42 | "@std/bytes@0.218.2": { 43 | "integrity": "91fe54b232dcca73856b79a817247f4a651dbb60d51baafafb6408c137241670" 44 | }, 45 | "@std/fmt@0.218.2": { 46 | "integrity": "99526449d2505aa758b6cbef81e7dd471d8b28ec0dcb1491d122b284c548788a" 47 | }, 48 | "@std/fs@0.218.2": { 49 | "integrity": "dd9431453f7282e8c577cc22c9e6d036055a9a980b5549f887d6012969fabcca", 50 | "dependencies": [ 51 | "jsr:@std/assert@^0.218.2", 52 | "jsr:@std/path@^0.218.2" 53 | ] 54 | }, 55 | "@std/io@0.218.2": { 56 | "integrity": "c64fbfa087b7c9d4d386c5672f291f607d88cb7d44fc299c20c713e345f2785f", 57 | "dependencies": [ 58 | "jsr:@std/assert@^0.218.2", 59 | "jsr:@std/bytes@^0.218.2" 60 | ] 61 | }, 62 | "@std/path@0.218.2": { 63 | "integrity": "b568fd923d9e53ad76d17c513e7310bda8e755a3e825e6289a0ce536404e2662", 64 | "dependencies": [ 65 | "jsr:@std/assert@^0.218.2" 66 | ] 67 | } 68 | }, 69 | "npm": { 70 | "@nodelib/fs.scandir@2.1.5": { 71 | "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", 72 | "dependencies": { 73 | "@nodelib/fs.stat": "@nodelib/fs.stat@2.0.5", 74 | "run-parallel": "run-parallel@1.2.0" 75 | } 76 | }, 77 | "@nodelib/fs.stat@2.0.5": { 78 | "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", 79 | "dependencies": {} 80 | }, 81 | "@nodelib/fs.walk@1.2.8": { 82 | "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", 83 | "dependencies": { 84 | "@nodelib/fs.scandir": "@nodelib/fs.scandir@2.1.5", 85 | "fastq": "fastq@1.17.1" 86 | } 87 | }, 88 | "@ts-morph/bootstrap@0.22.0": { 89 | "integrity": "sha512-MI5q7pid4swAlE2lcHwHRa6rcjoIMyT6fy8uuZm8BGg7DHGi/H5bQ0GMZzbk3N0r/LfStMdOYPkl+3IwvfIQ2g==", 90 | "dependencies": { 91 | "@ts-morph/common": "@ts-morph/common@0.22.0" 92 | } 93 | }, 94 | "@ts-morph/common@0.22.0": { 95 | "integrity": "sha512-HqNBuV/oIlMKdkLshXd1zKBqNQCsuPEsgQOkfFQ/eUKjRlwndXW1AjN9LVkBEIukm00gGXSRmfkl0Wv5VXLnlw==", 96 | "dependencies": { 97 | "fast-glob": "fast-glob@3.3.2", 98 | "minimatch": "minimatch@9.0.3", 99 | "mkdirp": "mkdirp@3.0.1", 100 | "path-browserify": "path-browserify@1.0.1" 101 | } 102 | }, 103 | "@types/chai@4.3.14": { 104 | "integrity": "sha512-Wj71sXE4Q4AkGdG9Tvq1u/fquNz9EdG4LIJMwVVII7ashjD/8cf8fyIfJAjRr6YcsXnSE8cOGQPq1gqeR8z+3w==", 105 | "dependencies": {} 106 | }, 107 | "assertion-error@1.1.0": { 108 | "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", 109 | "dependencies": {} 110 | }, 111 | "balanced-match@1.0.2": { 112 | "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", 113 | "dependencies": {} 114 | }, 115 | "brace-expansion@2.0.1": { 116 | "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", 117 | "dependencies": { 118 | "balanced-match": "balanced-match@1.0.2" 119 | } 120 | }, 121 | "braces@3.0.2": { 122 | "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", 123 | "dependencies": { 124 | "fill-range": "fill-range@7.0.1" 125 | } 126 | }, 127 | "chai@4.3.7": { 128 | "integrity": "sha512-HLnAzZ2iupm25PlN0xFreAlBA5zaBSv3og0DdeGA4Ar6h6rJ3A0rolRUKJhSF2V10GZKDgWF/VmAEsNWjCRB+A==", 129 | "dependencies": { 130 | "assertion-error": "assertion-error@1.1.0", 131 | "check-error": "check-error@1.0.3", 132 | "deep-eql": "deep-eql@4.1.3", 133 | "get-func-name": "get-func-name@2.0.2", 134 | "loupe": "loupe@2.3.7", 135 | "pathval": "pathval@1.1.1", 136 | "type-detect": "type-detect@4.0.8" 137 | } 138 | }, 139 | "check-error@1.0.3": { 140 | "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", 141 | "dependencies": { 142 | "get-func-name": "get-func-name@2.0.2" 143 | } 144 | }, 145 | "code-block-writer@13.0.1": { 146 | "integrity": "sha512-c5or4P6erEA69TxaxTNcHUNcIn+oyxSRTOWV+pSYF+z4epXqNvwvJ70XPGjPNgue83oAFAPBRQYwpAJ/Hpe/Sg==", 147 | "dependencies": {} 148 | }, 149 | "deep-eql@4.1.3": { 150 | "integrity": "sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==", 151 | "dependencies": { 152 | "type-detect": "type-detect@4.0.8" 153 | } 154 | }, 155 | "fast-glob@3.3.2": { 156 | "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", 157 | "dependencies": { 158 | "@nodelib/fs.stat": "@nodelib/fs.stat@2.0.5", 159 | "@nodelib/fs.walk": "@nodelib/fs.walk@1.2.8", 160 | "glob-parent": "glob-parent@5.1.2", 161 | "merge2": "merge2@1.4.1", 162 | "micromatch": "micromatch@4.0.5" 163 | } 164 | }, 165 | "fastq@1.17.1": { 166 | "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", 167 | "dependencies": { 168 | "reusify": "reusify@1.0.4" 169 | } 170 | }, 171 | "fill-range@7.0.1": { 172 | "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", 173 | "dependencies": { 174 | "to-regex-range": "to-regex-range@5.0.1" 175 | } 176 | }, 177 | "get-func-name@2.0.2": { 178 | "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", 179 | "dependencies": {} 180 | }, 181 | "glob-parent@5.1.2": { 182 | "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", 183 | "dependencies": { 184 | "is-glob": "is-glob@4.0.3" 185 | } 186 | }, 187 | "is-extglob@2.1.1": { 188 | "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", 189 | "dependencies": {} 190 | }, 191 | "is-glob@4.0.3": { 192 | "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", 193 | "dependencies": { 194 | "is-extglob": "is-extglob@2.1.1" 195 | } 196 | }, 197 | "is-number@7.0.0": { 198 | "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", 199 | "dependencies": {} 200 | }, 201 | "loupe@2.3.7": { 202 | "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", 203 | "dependencies": { 204 | "get-func-name": "get-func-name@2.0.2" 205 | } 206 | }, 207 | "merge2@1.4.1": { 208 | "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", 209 | "dependencies": {} 210 | }, 211 | "micromatch@4.0.5": { 212 | "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", 213 | "dependencies": { 214 | "braces": "braces@3.0.2", 215 | "picomatch": "picomatch@2.3.1" 216 | } 217 | }, 218 | "minimatch@9.0.3": { 219 | "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", 220 | "dependencies": { 221 | "brace-expansion": "brace-expansion@2.0.1" 222 | } 223 | }, 224 | "mkdirp@3.0.1": { 225 | "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", 226 | "dependencies": {} 227 | }, 228 | "path-browserify@1.0.1": { 229 | "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", 230 | "dependencies": {} 231 | }, 232 | "pathval@1.1.1": { 233 | "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", 234 | "dependencies": {} 235 | }, 236 | "picomatch@2.3.1": { 237 | "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", 238 | "dependencies": {} 239 | }, 240 | "queue-microtask@1.2.3": { 241 | "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", 242 | "dependencies": {} 243 | }, 244 | "reusify@1.0.4": { 245 | "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", 246 | "dependencies": {} 247 | }, 248 | "run-parallel@1.2.0": { 249 | "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", 250 | "dependencies": { 251 | "queue-microtask": "queue-microtask@1.2.3" 252 | } 253 | }, 254 | "to-regex-range@5.0.1": { 255 | "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", 256 | "dependencies": { 257 | "is-number": "is-number@7.0.0" 258 | } 259 | }, 260 | "type-detect@4.0.8": { 261 | "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", 262 | "dependencies": {} 263 | } 264 | } 265 | }, 266 | "remote": { 267 | "https://deno.land/std@0.193.0/testing/_test_suite.ts": "30f018feeb3835f12ab198d8a518f9089b1bcb2e8c838a8b615ab10d5005465c", 268 | "https://deno.land/std@0.193.0/testing/bdd.ts": "0c760348442712ca3ae6102dbb3aa76b49a95ee08fee86c8a0932de53a00e308" 269 | }, 270 | "workspace": { 271 | "dependencies": [ 272 | "jsr:@deno/dnt@^0.41.1" 273 | ] 274 | } 275 | } 276 | -------------------------------------------------------------------------------- /dprint.json: -------------------------------------------------------------------------------- 1 | { 2 | "lineWidth": 180, 3 | "excludes": [ 4 | "npm/" 5 | ], 6 | "plugins": [ 7 | "https://plugins.dprint.dev/typescript-0.89.3.wasm", 8 | "https://plugins.dprint.dev/json-0.19.2.wasm", 9 | "https://plugins.dprint.dev/markdown-0.16.4.wasm" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /mod.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "./_test.deps.ts"; 2 | import CodeBlockWriter from "./mod.ts"; 3 | 4 | describe("CodeBlockWriter", () => { 5 | describe("default opts", () => { 6 | it("should use a \n newline if none is specified", () => { 7 | const writer = new CodeBlockWriter(); 8 | writer.writeLine("test"); 9 | expect(writer.toString()).to.equal("test\n"); 10 | }); 11 | }); 12 | 13 | describe("tests for \\n", () => { 14 | runTestsForNewLineChar({ newLine: "\n" }); 15 | }); 16 | 17 | describe("tests for \\r\\n", () => { 18 | runTestsForNewLineChar({ newLine: "\r\n" }); 19 | }); 20 | }); 21 | 22 | function runTestsForNewLineChar(opts: { newLine: "\r\n" | "\n" }) { 23 | function getWriter(additionalOpts: { useSingleQuote?: boolean } = {}) { 24 | return new CodeBlockWriter({ 25 | newLine: opts.newLine, 26 | useSingleQuote: additionalOpts.useSingleQuote, 27 | }); 28 | } 29 | 30 | function doTest( 31 | expected: string, 32 | writerCallback: (writer: CodeBlockWriter) => void, 33 | additionalOpts: { useSingleQuote?: boolean } = {}, 34 | ) { 35 | const writer = getWriter(additionalOpts); 36 | writerCallback(writer); 37 | expect(writer.toString()).to.equal(expected.replace(/\r?\n/g, opts.newLine)); 38 | } 39 | 40 | describe("#write()", () => { 41 | it("should write a single letter", () => { 42 | const expected = `a`; 43 | 44 | doTest(expected, writer => { 45 | writer.write("a"); 46 | }); 47 | }); 48 | 49 | it("should write the text", () => { 50 | const expected = `test`; 51 | 52 | doTest(expected, writer => { 53 | writer.write("test"); 54 | }); 55 | }); 56 | 57 | it("should do nothing if providing a null string", () => { 58 | const expected = ""; 59 | 60 | doTest(expected, writer => { 61 | writer.write(null as unknown as string); 62 | }); 63 | }); 64 | 65 | it("should indent if it's passed a newline character inside a block", () => { 66 | const expected = `test { 67 | inside 68 | inside 69 | }`; 70 | doTest(expected, writer => { 71 | writer.write("test ").block(() => { 72 | writer.write(`inside${opts.newLine}inside`); 73 | }); 74 | }); 75 | }); 76 | 77 | it("should write all requested newlines", () => { 78 | const expected = "\n\ntest\n\n"; 79 | 80 | doTest(expected, writer => { 81 | writer.write("\n\ntest\n\n"); 82 | }); 83 | }); 84 | 85 | it("should not write indentation between newlines", () => { 86 | const expected = " test\n\n test"; 87 | 88 | doTest(expected, writer => { 89 | writer.setIndentationLevel(1); 90 | writer.write("test\n\ntest"); 91 | }); 92 | }); 93 | 94 | it("should indent if passed an empty string at the start of a newline within a block", () => { 95 | const expected = `test {\n inside\n \n}`; 96 | doTest(expected, writer => { 97 | writer.write("test ").block(() => { 98 | writer.writeLine(`inside`); 99 | writer.write(""); 100 | }); 101 | }); 102 | }); 103 | }); 104 | 105 | describe("#block()", () => { 106 | it("should allow an empty block", () => { 107 | const expected = `test { 108 | }`; 109 | doTest(expected, writer => { 110 | writer.write("test").block(); 111 | }); 112 | }); 113 | 114 | it("should write text inside a block", () => { 115 | const expected = `test { 116 | inside 117 | }`; 118 | doTest(expected, writer => { 119 | writer.write("test").block(() => { 120 | writer.write("inside"); 121 | }); 122 | }); 123 | }); 124 | 125 | it("should write text inside a block inside a block", () => { 126 | const expected = `test { 127 | inside { 128 | inside again 129 | } 130 | }`; 131 | 132 | doTest(expected, writer => { 133 | writer.write("test").block(() => { 134 | writer.write("inside").block(() => { 135 | writer.write("inside again"); 136 | }); 137 | }); 138 | }); 139 | }); 140 | 141 | it("should not do an extra space if there was a space added before the block", () => { 142 | const expected = `test { 143 | inside 144 | }`; 145 | doTest(expected, writer => { 146 | writer.write("test ").block(() => { 147 | writer.write("inside"); 148 | }); 149 | }); 150 | }); 151 | 152 | it("should put the brace on the next line if there is a newline before it", () => { 153 | const expected = `test 154 | { 155 | inside 156 | }`; 157 | doTest(expected, writer => { 158 | writer.writeLine("test").block(() => { 159 | writer.writeLine("inside"); 160 | }); 161 | }); 162 | }); 163 | 164 | it("should not add an extra newline if the last character written in the block was a newline", () => { 165 | const expected = `test { 166 | inside 167 | }`; 168 | doTest(expected, writer => { 169 | writer.write("test ").block(() => { 170 | writer.writeLine("inside"); 171 | }); 172 | }); 173 | }); 174 | 175 | it("should add a newline after the block when writing afterwards", () => { 176 | const expected = `{ 177 | t; 178 | } 179 | `; 180 | doTest(expected, writer => { 181 | writer.block(() => writer.write("t;")).write(" "); 182 | }); 183 | }); 184 | 185 | it("should not add a newline after the block when doing a condition call and the conditions are false", () => { 186 | const expected = `{ 187 | t; 188 | }`; 189 | doTest(expected, writer => { 190 | writer.block(() => writer.write("t;")) 191 | .conditionalWrite(false, " ") 192 | .conditionalWriteLine(false, " ") 193 | .conditionalNewLine(false) 194 | .conditionalBlankLine(false); 195 | }); 196 | }); 197 | 198 | it("should not indent when in a string", () => { 199 | const expected = "block {\n const t = `\nt`;\n const u = 1;\n}"; 200 | 201 | doTest(expected, writer => { 202 | writer.write("block").block(() => { 203 | writer.write("const t = `\nt`;\nconst u = 1;"); 204 | }); 205 | }); 206 | }); 207 | 208 | it("should indent when in a comment", () => { 209 | const expected = "block {\n const t = /*\n const u = 1;*/\n}"; 210 | 211 | doTest(expected, writer => { 212 | writer.write("block").block(() => { 213 | writer.write("const t = /*\nconst u = 1;*/"); 214 | }); 215 | }); 216 | }); 217 | }); 218 | 219 | describe("#inlineBlock()", () => { 220 | it("should allow an empty inline block", () => { 221 | const expected = `someCall({\n});`; 222 | 223 | doTest(expected, writer => { 224 | writer.write("someCall(").inlineBlock().write(");"); 225 | }); 226 | }); 227 | 228 | it("should do an inline block correctly", () => { 229 | const expected = `someCall({\n console.log();\n});`; 230 | 231 | doTest(expected, writer => { 232 | writer.write("someCall(").inlineBlock(() => { 233 | writer.write("console.log();"); 234 | }).write(");"); 235 | }); 236 | }); 237 | }); 238 | 239 | describe("#indent()", () => { 240 | describe("number argument", () => { 241 | it("should indent as necessary", () => { 242 | const expected = `test\n test`; 243 | 244 | doTest(expected, writer => { 245 | writer.writeLine("test").indent().write("test"); 246 | }); 247 | }); 248 | 249 | it("should indent multiple times when specifying an argument", () => { 250 | const expected = `test\n test`; 251 | 252 | doTest(expected, writer => { 253 | writer.writeLine("test").indent(2).write("test"); 254 | }); 255 | }); 256 | }); 257 | 258 | describe("block argument", () => { 259 | it("should indent text inside a block", () => { 260 | const expected = `test\n inside`; 261 | doTest(expected, writer => { 262 | writer.write("test").indent(() => { 263 | writer.write("inside"); 264 | }); 265 | }); 266 | }); 267 | 268 | it("should not do a newline on the first line", () => { 269 | const expected = ` inside`; 270 | doTest(expected, writer => { 271 | writer.indent(() => { 272 | writer.write("inside"); 273 | }); 274 | }); 275 | }); 276 | 277 | it("should not do a newline at the start if the last was a new line", () => { 278 | const expected = `test\n inside`; 279 | doTest(expected, writer => { 280 | writer.writeLine("test").indent(() => { 281 | writer.write("inside"); 282 | }); 283 | }); 284 | }); 285 | 286 | it("should not do a newline at the end if the last was a new line", () => { 287 | const expected = ` inside\ntest`; 288 | doTest(expected, writer => { 289 | writer.indent(() => { 290 | writer.writeLine("inside"); 291 | }).write("test"); 292 | }); 293 | }); 294 | 295 | it("should indent text inside a block inside a block", () => { 296 | const expected = `test 297 | inside 298 | inside again 299 | test`; 300 | 301 | doTest(expected, writer => { 302 | writer.write("test").indent(() => { 303 | writer.write("inside").indent(() => { 304 | writer.write("inside again"); 305 | }); 306 | }); 307 | writer.write("test"); 308 | }); 309 | }); 310 | 311 | it("should not indent when in a string", () => { 312 | const expected = "block\n const t = `\nt`;\n const u = 1;"; 313 | 314 | doTest(expected, writer => { 315 | writer.write("block").indent(() => { 316 | writer.write("const t = `\nt`;\nconst u = 1;"); 317 | }); 318 | }); 319 | }); 320 | 321 | it("should indent when in a comment", () => { 322 | const expected = "block\n const t = /*\n const u = 1;*/"; 323 | 324 | doTest(expected, writer => { 325 | writer.write("block").indent(() => { 326 | writer.write("const t = /*\nconst u = 1;*/"); 327 | }); 328 | }); 329 | }); 330 | }); 331 | }); 332 | 333 | describe("#writeLine()", () => { 334 | it("should write some text on a line", () => { 335 | const expected = `test\n`; 336 | 337 | doTest(expected, writer => { 338 | writer.writeLine("test"); 339 | }); 340 | }); 341 | 342 | it("should start writing on a newline if the last one was just writing", () => { 343 | const expected = `test\ntest\ntest\n`; 344 | 345 | doTest(expected, writer => { 346 | writer.writeLine("test").write("test").writeLine("test"); 347 | }); 348 | }); 349 | 350 | it("should not create a newline between two writeLines", () => { 351 | const expected = `test\ntest\n`; 352 | 353 | doTest(expected, writer => { 354 | writer.writeLine("test").writeLine("test"); 355 | }); 356 | }); 357 | 358 | it("should indent if passed an empty string at the start of a newline within a block", () => { 359 | const expected = `test {\n inside\n \n}`; 360 | doTest(expected, writer => { 361 | writer.write("test ").block(() => { 362 | writer.writeLine(`inside`); 363 | writer.writeLine(""); 364 | }); 365 | }); 366 | }); 367 | }); 368 | 369 | describe("#blankLineIfLastNot()", () => { 370 | it("should do a blank line if the last text was not a newline", () => { 371 | const expected = `test\n\n`; 372 | 373 | doTest(expected, writer => { 374 | writer.write("test").blankLineIfLastNot(); 375 | }); 376 | }); 377 | 378 | it("should do a blank line if the last text was a newline", () => { 379 | const expected = `test\n\n`; 380 | 381 | doTest(expected, writer => { 382 | writer.writeLine("test").blankLineIfLastNot(); 383 | }); 384 | }); 385 | 386 | it("should not do a blank line if the last text was a blank line", () => { 387 | const expected = `test\n\n`; 388 | 389 | doTest(expected, writer => { 390 | writer.write("test").blankLine().blankLineIfLastNot(); 391 | }); 392 | }); 393 | }); 394 | 395 | describe("#blankLine()", () => { 396 | it("should do a blank line if the last text was not a new line", () => { 397 | const expected = `test\n\ntest`; 398 | 399 | doTest(expected, writer => { 400 | writer.write("test").blankLine().write("test"); 401 | }); 402 | }); 403 | 404 | it("should do a blank line if the last text was a newline", () => { 405 | const expected = `test\n\ntest`; 406 | 407 | doTest(expected, writer => { 408 | writer.writeLine("test").blankLine().write("test"); 409 | }); 410 | }); 411 | 412 | it("should do a blank line if the last was a blank line", () => { 413 | const expected = `test\n\n\ntest`; 414 | 415 | doTest(expected, writer => { 416 | writer.writeLine("test").blankLine().blankLine().write("test"); 417 | }); 418 | }); 419 | }); 420 | 421 | describe("#conditionalBlankLine()", () => { 422 | it("should write when the condition is true", () => { 423 | doTest("t\n\n", writer => { 424 | writer.write("t").conditionalBlankLine(true); 425 | }); 426 | }); 427 | 428 | it("should not write when the condition is false", () => { 429 | doTest("t", writer => { 430 | writer.write("t").conditionalBlankLine(false); 431 | }); 432 | }); 433 | 434 | it("should not write when the condition is undefined", () => { 435 | doTest("t", writer => { 436 | writer.write("t").conditionalBlankLine(undefined); 437 | }); 438 | }); 439 | }); 440 | 441 | describe("#newLineIfLastNot()", () => { 442 | it("should do a newline if the last text was not a newline", () => { 443 | const expected = `test\n`; 444 | 445 | doTest(expected, writer => { 446 | writer.write("test").newLineIfLastNot(); 447 | }); 448 | }); 449 | 450 | it("should not do a newline if the last text was a newline", () => { 451 | const expected = `test\n`; 452 | 453 | doTest(expected, writer => { 454 | writer.writeLine("test").newLineIfLastNot(); 455 | }); 456 | }); 457 | }); 458 | 459 | describe("#newLine()", () => { 460 | it("should do a newline when writing", () => { 461 | const expected = `test\n`; 462 | 463 | doTest(expected, writer => { 464 | writer.write("test").newLine(); 465 | }); 466 | }); 467 | 468 | it("should do a newline after doing a newline", () => { 469 | const expected = `test\n\ntext`; 470 | 471 | doTest(expected, writer => { 472 | writer.write("test").newLine().newLine().write("text"); 473 | }); 474 | }); 475 | 476 | it("should allow doing a newline at the start", () => { 477 | const expected = `\n`; 478 | 479 | doTest(expected, writer => { 480 | writer.newLine(); 481 | }); 482 | }); 483 | 484 | it("should allow doing a newline after doing a block", () => { 485 | const expected = `test {\n\n test\n}`; 486 | 487 | doTest(expected, writer => { 488 | writer.write("test").block(() => { 489 | writer.newLine().writeLine("test"); 490 | }); 491 | }); 492 | }); 493 | 494 | it("should allow doing a newline if the last line was a blank line (allow consecutive blank lines)", () => { 495 | const expected = `test\n\n\ntext`; 496 | 497 | doTest(expected, writer => { 498 | writer.write("test").newLine().newLine().newLine().write("text"); 499 | }); 500 | }); 501 | 502 | it("should do a newline if a string causes it to not be a consecutive blank line", () => { 503 | const expected = `test\na\n`; 504 | 505 | doTest(expected, writer => { 506 | writer.write("test").newLine().write("a").newLine(); 507 | }); 508 | }); 509 | 510 | it("should be allowed to have two newlines at the end of a file", () => { 511 | const expected = `text\n\n`; 512 | 513 | doTest(expected, writer => { 514 | writer.write("text").newLine().newLine(); 515 | }); 516 | }); 517 | 518 | it("should indent if it's passed a newline character inside a block", () => { 519 | const expected = `test { 520 | inside 521 | inside 522 | }`; 523 | doTest(expected, writer => { 524 | writer.write("test ").block(() => { 525 | writer.writeLine(`inside${opts.newLine}inside`); 526 | }); 527 | }); 528 | }); 529 | }); 530 | 531 | describe("#quote()", () => { 532 | it("should write out a double quote character", () => { 533 | const expected = `"`; 534 | doTest(expected, writer => { 535 | writer.quote(); 536 | }); 537 | }); 538 | 539 | it("should write out a single quote character", () => { 540 | const expected = `'`; 541 | doTest(expected, writer => { 542 | writer.quote(); 543 | }, { useSingleQuote: true }); 544 | }); 545 | 546 | it("should write out text surrounded by quotes", () => { 547 | const expected = `"test"`; 548 | doTest(expected, writer => { 549 | writer.quote("test"); 550 | }); 551 | }); 552 | 553 | it("should write out text surrounded by quotes and escape quotes and new lines", () => { 554 | const expected = `"te\\"\\r\\n\\\r\nst"`; 555 | doTest(expected, writer => { 556 | writer.quote("te\"\r\nst"); 557 | }); 558 | }); 559 | }); 560 | 561 | describe("#spaceIfLastNot()", () => { 562 | it("should do a space at the beginning of the file", () => { 563 | const expected = ` `; 564 | 565 | doTest(expected, writer => { 566 | writer.spaceIfLastNot(); 567 | }); 568 | }); 569 | 570 | it("should do a space if the last character wasn't a space", () => { 571 | const expected = `test `; 572 | 573 | doTest(expected, writer => { 574 | writer.write("test").spaceIfLastNot(); 575 | }); 576 | }); 577 | 578 | it("should not do a space if the last character was a space", () => { 579 | const expected = `test `; 580 | 581 | doTest(expected, writer => { 582 | writer.write("test").spaceIfLastNot().spaceIfLastNot(); 583 | }); 584 | }); 585 | 586 | it("should do a space if the last character was a newline", () => { 587 | const expected = `test\n `; 588 | 589 | doTest(expected, writer => { 590 | writer.write("test").newLine().spaceIfLastNot(); 591 | }); 592 | }); 593 | }); 594 | 595 | describe("#space()", () => { 596 | it("should do a space when saying to", () => { 597 | const expected = ` `; 598 | doTest(expected, writer => { 599 | writer.space().space().space(); 600 | }); 601 | }); 602 | 603 | it("should do a space when saying to do multiple", () => { 604 | const expected = ` `; 605 | doTest(expected, writer => { 606 | writer.space(5); 607 | }); 608 | }); 609 | 610 | it("should throw if providing a negative number", () => { 611 | const writer = new CodeBlockWriter(); 612 | expect(() => writer.space(-1)).to.throw(); 613 | }); 614 | }); 615 | 616 | describe("#tabIfLastNot()", () => { 617 | it("should do a tab at the beginning of the file", () => { 618 | const expected = `\t`; 619 | 620 | doTest(expected, writer => { 621 | writer.tabIfLastNot(); 622 | }); 623 | }); 624 | 625 | it("should do a tab if the last character wasn't a tab", () => { 626 | const expected = `test\t`; 627 | 628 | doTest(expected, writer => { 629 | writer.write("test").tabIfLastNot(); 630 | }); 631 | }); 632 | 633 | it("should not do a tab if the last character was a tab", () => { 634 | const expected = `test\t`; 635 | 636 | doTest(expected, writer => { 637 | writer.write("test").tabIfLastNot().tabIfLastNot(); 638 | }); 639 | }); 640 | 641 | it("should do a tab if the last character was a newline", () => { 642 | const expected = `test\n\t`; 643 | 644 | doTest(expected, writer => { 645 | writer.write("test").newLine().tabIfLastNot(); 646 | }); 647 | }); 648 | }); 649 | 650 | describe("#tab()", () => { 651 | it("should do a tab when saying to", () => { 652 | const expected = `\t\t\t`; 653 | doTest(expected, writer => { 654 | writer.tab().tab().tab(); 655 | }); 656 | }); 657 | 658 | it("should do a tab when saying to do multiple", () => { 659 | const expected = `\t\t\t\t\t`; 660 | doTest(expected, writer => { 661 | writer.tab(5); 662 | }); 663 | }); 664 | 665 | it("should throw if providing a negative number", () => { 666 | const writer = new CodeBlockWriter(); 667 | expect(() => writer.tab(-1)).to.throw(); 668 | }); 669 | }); 670 | 671 | describe("#getLength()", () => { 672 | it("should return the length", () => { 673 | const writer = getWriter(); 674 | writer.write("1234"); 675 | expect(writer.getLength()).to.equal(4); 676 | }); 677 | 678 | it("should get the correct length after various kinds of writes", () => { 679 | const writer = getWriter(); 680 | writer.write("1").writeLine("2").write("3").block(() => writer.write("4")).tab(5).quote("testing").blankLine(); 681 | expect(writer.getLength()).to.equal(writer.toString().length); 682 | }); 683 | }); 684 | 685 | describe("#conditionalNewLine()", () => { 686 | it("should write when the condition is true", () => { 687 | doTest("t\n", writer => { 688 | writer.write("t").conditionalNewLine(true); 689 | }); 690 | }); 691 | 692 | it("should not write when the condition is false", () => { 693 | doTest("t", writer => { 694 | writer.write("t").conditionalNewLine(false); 695 | }); 696 | }); 697 | 698 | it("should not write when the condition is undefined", () => { 699 | doTest("t", writer => { 700 | writer.write("t").conditionalNewLine(undefined); 701 | }); 702 | }); 703 | }); 704 | 705 | describe("#conditionalWrite()", () => { 706 | it("should write the given string when the condition is true", () => { 707 | doTest("test", writer => { 708 | writer.conditionalWrite(true, "test"); 709 | }); 710 | }); 711 | 712 | it("should not write the given string when the condition is false", () => { 713 | doTest("", writer => { 714 | writer.conditionalWrite(false, "test"); 715 | }); 716 | }); 717 | 718 | it("should not write the given string when the condition is undefined", () => { 719 | doTest("", writer => { 720 | writer.conditionalWrite(undefined, "test"); 721 | }); 722 | }); 723 | 724 | it("should write the result of the given function when the condition is true", () => { 725 | doTest("test", writer => { 726 | writer.conditionalWrite(true, () => "test"); 727 | }); 728 | }); 729 | 730 | it("should not write the result of the given function when the condition is false", () => { 731 | doTest("", writer => { 732 | writer.conditionalWrite(false, () => "test"); 733 | }); 734 | }); 735 | 736 | it("should not evaluate the given function when the condition is false", () => { 737 | // deno-lint-ignore no-explicit-any 738 | const test: any = null; 739 | doTest("", writer => { 740 | writer.conditionalWrite(false, () => test.test); 741 | }); 742 | }); 743 | }); 744 | 745 | describe("#conditionalWriteLine()", () => { 746 | it("should write the given string when the condition is true", () => { 747 | doTest("test\n", writer => { 748 | writer.conditionalWriteLine(true, "test"); 749 | }); 750 | }); 751 | 752 | it("should not write the given string when the condition is false", () => { 753 | doTest("", writer => { 754 | writer.conditionalWriteLine(false, "test"); 755 | }); 756 | }); 757 | 758 | it("should not write the given string when the condition is undefined", () => { 759 | doTest("", writer => { 760 | writer.conditionalWriteLine(undefined, "test"); 761 | }); 762 | }); 763 | 764 | it("should write the result of the given function when the condition is true", () => { 765 | doTest("test\n", writer => { 766 | writer.conditionalWriteLine(true, () => "test"); 767 | }); 768 | }); 769 | 770 | it("should not write the result of the given function when the condition is false", () => { 771 | doTest("", writer => { 772 | writer.conditionalWriteLine(false, () => "test"); 773 | }); 774 | }); 775 | 776 | it("should not evaluate the given function when the condition is false", () => { 777 | // deno-lint-ignore no-explicit-any 778 | const test: any = null; 779 | doTest("", writer => { 780 | writer.conditionalWriteLine(false, () => test.test); 781 | }); 782 | }); 783 | }); 784 | 785 | describe("#closeComment()", () => { 786 | it("should not do anything if not in a comment", () => { 787 | doTest("test", writer => { 788 | writer.write("test").closeComment(); 789 | }); 790 | }); 791 | 792 | it("should not do anything if in a string", () => { 793 | doTest('"/* test"', (writer) => { 794 | writer.write('"/* test"').closeComment(); 795 | }); 796 | 797 | doTest('"// test"', (writer) => { 798 | writer.write('"// test"').closeComment(); 799 | }); 800 | }); 801 | 802 | it("should do a newline if in a slash slash comment", () => { 803 | doTest("// test\n", writer => { 804 | writer.write("// test").closeComment(); 805 | }); 806 | }); 807 | 808 | it("should add a space at the end if on same line in a star slash comment", () => { 809 | doTest("/* test */", writer => { 810 | writer.write("/* test").closeComment(); 811 | }); 812 | }); 813 | 814 | it("should not add a space at the end if on same line and a space exists in a star slash comment", () => { 815 | doTest("/* test */", writer => { 816 | writer.write("/* test ").closeComment(); 817 | }); 818 | }); 819 | 820 | it("should not add a space at the start of the line in a star slash comment", () => { 821 | doTest("/* test\n*/", writer => { 822 | writer.writeLine("/* test").closeComment(); 823 | }); 824 | }); 825 | }); 826 | 827 | describe("#unsafeInsert()", () => { 828 | it("should throw if providing a negative number", () => { 829 | const writer = new CodeBlockWriter(); 830 | expect(() => writer.unsafeInsert(-1, "t")) 831 | .to.throw("Provided position of '-1' was less than zero."); 832 | }); 833 | 834 | it("should throw if providing a number greater than the length", () => { 835 | const writer = new CodeBlockWriter(); 836 | expect(() => writer.unsafeInsert(1, "t")) 837 | .to.throw("Provided position of '1' was greater than the text length of '0'."); 838 | }); 839 | 840 | it("should support inserting at the beginning", () => { 841 | doTest("01234", writer => { 842 | writer.write("1234").unsafeInsert(0, "0"); 843 | expect(writer.getLength()).to.equal(5); 844 | }); 845 | }); 846 | 847 | it("should support inserting at the end", () => { 848 | doTest("01234", writer => { 849 | writer.write("0123").unsafeInsert(4, "4"); 850 | expect(writer.getLength()).to.equal(5); 851 | }); 852 | }); 853 | 854 | it("should support inserting in the first half at the beginning of an existing string", () => { 855 | doTest("0x123456", writer => { 856 | writer.write("0").write("12").write("3").write("45").write("6"); 857 | writer.unsafeInsert(1, "x"); 858 | expect(writer.getLength()).to.equal(8); 859 | }); 860 | }); 861 | 862 | it("should support inserting in the first half in the middle of an existing string", () => { 863 | doTest("01x23456", writer => { 864 | writer.write("0").write("12").write("3").write("45").write("6"); 865 | writer.unsafeInsert(2, "x"); 866 | expect(writer.getLength()).to.equal(8); 867 | }); 868 | }); 869 | 870 | it("should support inserting in the first half at the end of an existing string", () => { 871 | doTest("012x3456", writer => { 872 | writer.write("0").write("12").write("3").write("45").write("6"); 873 | writer.unsafeInsert(3, "x"); 874 | expect(writer.getLength()).to.equal(8); 875 | }); 876 | }); 877 | 878 | it("should support inserting in the second half at the beginning of an existing string", () => { 879 | doTest("0123x456", writer => { 880 | writer.write("0").write("12").write("3").write("45").write("6"); 881 | writer.unsafeInsert(4, "x"); 882 | expect(writer.getLength()).to.equal(8); 883 | }); 884 | }); 885 | 886 | it("should support inserting in the second half in the middle of an existing string", () => { 887 | doTest("01234x56", writer => { 888 | writer.write("0").write("12").write("3").write("45").write("6"); 889 | writer.unsafeInsert(5, "x"); 890 | expect(writer.getLength()).to.equal(8); 891 | }); 892 | }); 893 | 894 | it("should support inserting in the second half at the end of an existing string", () => { 895 | doTest("012345x6", writer => { 896 | writer.write("0").write("12").write("3").write("45").write("6"); 897 | writer.unsafeInsert(6, "x"); 898 | expect(writer.getLength()).to.equal(8); 899 | }); 900 | }); 901 | }); 902 | } 903 | 904 | describe("#setIndentationLevel", () => { 905 | it("should throw when providing a negative number", () => { 906 | const writer = new CodeBlockWriter(); 907 | expect(() => writer.setIndentationLevel(-1)).to.throw(); 908 | }); 909 | 910 | it("should throw when not providing a number or string", () => { 911 | const writer = new CodeBlockWriter(); 912 | // deno-lint-ignore no-explicit-any 913 | expect(() => writer.setIndentationLevel({} as any)).to.throw(); 914 | }); 915 | 916 | it("should not throw when providing an empty string", () => { 917 | const writer = new CodeBlockWriter(); 918 | expect(() => writer.setIndentationLevel("")).to.not.throw(); 919 | }); 920 | 921 | it("should throw when providing a string that doesn't contain only spaces and tabs", () => { 922 | const writer = new CodeBlockWriter(); 923 | expect(() => writer.setIndentationLevel(" \ta")).to.throw(); 924 | }); 925 | 926 | it("should be able to set the indentation level and it maintains it over newlines", () => { 927 | const writer = new CodeBlockWriter(); 928 | writer.setIndentationLevel(2); 929 | writer.writeLine("t"); 930 | writer.writeLine("t"); 931 | 932 | expect(writer.toString()).to.equal(" t\n t\n"); 933 | expect(writer.getIndentationLevel()).to.equal(2); 934 | }); 935 | 936 | it("should be able to set the indentation level to 0 within a block", () => { 937 | const writer = new CodeBlockWriter(); 938 | writer.write("t").block(() => { 939 | writer.setIndentationLevel(0); 940 | writer.writeLine("t"); 941 | writer.writeLine("t"); 942 | }).write("t").block(() => { 943 | writer.write("t"); 944 | }); 945 | 946 | expect(writer.toString()).to.equal("t {\nt\nt\n}\nt {\n t\n}"); 947 | }); 948 | 949 | function doSpacesTest(numberSpaces: number) { 950 | const writer = new CodeBlockWriter({ indentNumberOfSpaces: numberSpaces }); 951 | const indent = Array(numberSpaces + 1).join(" "); 952 | writer.setIndentationLevel(indent + indent); 953 | writer.write("t").block(() => writer.write("t")); 954 | 955 | expect(writer.toString()).to.equal(`${indent + indent}t {\n${indent + indent + indent}t\n${indent + indent}}`); 956 | expect(writer.getIndentationLevel()).to.equal(2); 957 | } 958 | 959 | it("should be able to set the indentation level using a string with two spaces", () => { 960 | doSpacesTest(2); 961 | }); 962 | 963 | it("should be able to set the indentation level using a string with four spaces", () => { 964 | doSpacesTest(4); 965 | }); 966 | 967 | it("should be able to set the indentation level using a string with eight spaces", () => { 968 | doSpacesTest(8); 969 | }); 970 | 971 | it("should indent by the provided number of tabs", () => { 972 | const writer = new CodeBlockWriter({ useTabs: true }); 973 | writer.setIndentationLevel("\t\t"); 974 | writer.write("s"); 975 | 976 | expect(writer.toString()).to.equal(`\t\ts`); 977 | }); 978 | 979 | it("should indent to the nearest indent when mixing tabs and spaces (round down)", () => { 980 | const writer = new CodeBlockWriter({ useTabs: true }); 981 | writer.setIndentationLevel("\t \t "); 982 | writer.write("s"); 983 | 984 | expect(writer.toString()).to.equal(`\t\ts`); 985 | }); 986 | 987 | it("should indent to the nearest indent when mixing tabs and spaces (round down, 2 spaces)", () => { 988 | const writer = new CodeBlockWriter({ useTabs: true, indentNumberOfSpaces: 2 }); 989 | writer.setIndentationLevel("\t \t"); 990 | writer.write("s"); 991 | 992 | expect(writer.toString()).to.equal(`\t\ts`); 993 | }); 994 | 995 | it("should indent to the nearest indent when mixing tabs and spaces (round up)", () => { 996 | const writer = new CodeBlockWriter({ useTabs: true }); 997 | writer.setIndentationLevel("\t \t "); 998 | writer.write("s"); 999 | 1000 | expect(writer.toString()).to.equal(`\t\t\ts`); 1001 | }); 1002 | 1003 | it("should indent to the nearest indent when mixing tabs and spaces (2 spaces)", () => { 1004 | const writer = new CodeBlockWriter({ useTabs: true, indentNumberOfSpaces: 2 }); 1005 | writer.setIndentationLevel("\t \t "); 1006 | writer.write("s"); 1007 | 1008 | expect(writer.toString()).to.equal(`\t\t\t\t\ts`); 1009 | }); 1010 | 1011 | function doPortionTest(level: number, expected: string) { 1012 | const writer = new CodeBlockWriter({ useTabs: false, indentNumberOfSpaces: 4 }); 1013 | writer.setIndentationLevel(level); 1014 | writer.write("s"); 1015 | expect(writer.toString()).to.equal(expected + `s`); 1016 | } 1017 | 1018 | it("should indent a quarter of an indent when using spaces and specifying a quarter", () => { 1019 | doPortionTest(0.25, " "); 1020 | }); 1021 | 1022 | it("should indent half an indent when using spaces and specifying halfway", () => { 1023 | doPortionTest(0.5, " "); 1024 | }); 1025 | 1026 | it("should indent a third of an indent when using spaces and specifying a third", () => { 1027 | doPortionTest(0.75, " "); 1028 | }); 1029 | 1030 | it("should round the indent when specifying a position between two indexes", () => { 1031 | doPortionTest(0.125, " "); 1032 | }); 1033 | }); 1034 | 1035 | describe("#withIndentationLevel", () => { 1036 | it("should use the provided indentation level within the block", () => { 1037 | const writer = new CodeBlockWriter(); 1038 | writer.withIndentationLevel(2, () => { 1039 | expect(writer.getIndentationLevel()).to.equal(2); 1040 | }); 1041 | expect(writer.getIndentationLevel()).to.equal(0); 1042 | }); 1043 | 1044 | it("should use the provided indentation level within the block when providing a string", () => { 1045 | const writer = new CodeBlockWriter(); 1046 | writer.withIndentationLevel(" ", () => { 1047 | expect(writer.getIndentationLevel()).to.equal(1); 1048 | }); 1049 | expect(writer.getIndentationLevel()).to.equal(0); 1050 | }); 1051 | }); 1052 | 1053 | describe("#queueIndentationLevel", () => { 1054 | it("should throw when providing a negative number", () => { 1055 | const writer = new CodeBlockWriter(); 1056 | expect(() => writer.queueIndentationLevel(-1)).to.throw(); 1057 | }); 1058 | 1059 | it("should throw when not providing a number or string", () => { 1060 | const writer = new CodeBlockWriter(); 1061 | // deno-lint-ignore no-explicit-any 1062 | expect(() => writer.queueIndentationLevel({} as any)).to.throw(); 1063 | }); 1064 | 1065 | it("should not throw when providing an empty string", () => { 1066 | const writer = new CodeBlockWriter(); 1067 | expect(() => writer.queueIndentationLevel("")).to.not.throw(); 1068 | }); 1069 | 1070 | it("should throw when providing a string that doesn't contain only spaces and tabs", () => { 1071 | const writer = new CodeBlockWriter(); 1072 | expect(() => writer.queueIndentationLevel(" \ta")).to.throw(); 1073 | }); 1074 | 1075 | it("should write with indentation when queuing and immediately doing a newline", () => { 1076 | const writer = new CodeBlockWriter(); 1077 | writer.queueIndentationLevel(1); 1078 | writer.newLine().write("t"); 1079 | 1080 | expect(writer.toString()).to.equal("\n t"); 1081 | }); 1082 | 1083 | it("should be able to queue the indentation level", () => { 1084 | const writer = new CodeBlockWriter(); 1085 | writer.queueIndentationLevel(1); 1086 | writer.writeLine("t"); 1087 | writer.writeLine("t"); 1088 | 1089 | expect(writer.toString()).to.equal("t\n t\n"); 1090 | }); 1091 | 1092 | it("should be able to queue the indentation mid line and it will write the next line with indentation", () => { 1093 | const writer = new CodeBlockWriter(); 1094 | writer.write("t"); 1095 | writer.queueIndentationLevel(1); 1096 | writer.write("t"); 1097 | writer.writeLine("t"); 1098 | 1099 | expect(writer.toString()).to.equal("tt\n t\n"); 1100 | }); 1101 | 1102 | it("should be able to set and queue an indentation", () => { 1103 | const writer = new CodeBlockWriter(); 1104 | writer.setIndentationLevel(1); 1105 | writer.queueIndentationLevel(2); 1106 | writer.writeLine("t"); 1107 | writer.writeLine("t"); 1108 | 1109 | expect(writer.toString()).to.equal(" t\n t\n"); 1110 | }); 1111 | 1112 | it("should be able to set after queueng an indentation", () => { 1113 | const writer = new CodeBlockWriter(); 1114 | writer.queueIndentationLevel(1); 1115 | writer.writeLine("t"); 1116 | writer.writeLine("t"); 1117 | writer.setIndentationLevel(2); 1118 | writer.writeLine("t"); 1119 | writer.writeLine("t"); 1120 | 1121 | expect(writer.toString()).to.equal("t\n t\n t\n t\n"); 1122 | }); 1123 | }); 1124 | 1125 | describe("#hangingIndent", () => { 1126 | it("should queue an indent +1 when using newLine() and writing text", () => { 1127 | function doTest(action: (writer: CodeBlockWriter) => void) { 1128 | const writer = new CodeBlockWriter(); 1129 | writer.setIndentationLevel(2); 1130 | writer.hangingIndent(() => { 1131 | expect(writer.getIndentationLevel()).to.equal(2); 1132 | action(writer); 1133 | expect(writer.getIndentationLevel()).to.equal(3); 1134 | }); 1135 | expect(writer.getIndentationLevel()).to.equal(2); 1136 | } 1137 | 1138 | doTest(writer => writer.newLine()); 1139 | doTest(writer => writer.write("testing\nthis")); 1140 | }); 1141 | 1142 | it("should handle nested indentations", () => { 1143 | const writer = new CodeBlockWriter(); 1144 | writer.write("("); 1145 | writer.hangingIndent(() => { 1146 | writer.write("p"); 1147 | writer.hangingIndent(() => { 1148 | writer.write(": string\n| number"); 1149 | }); 1150 | }); 1151 | writer.write(")"); 1152 | expect(writer.toString()).to.equal("(p: string\n | number)"); 1153 | }); 1154 | 1155 | it("should handle if a block occurs within a hanging indent", () => { 1156 | const writer = new CodeBlockWriter(); 1157 | writer.hangingIndent(() => { 1158 | writer.block(); 1159 | }); 1160 | expect(writer.toString()).to.equal("{\n }"); 1161 | }); 1162 | 1163 | it("should not indent if within a string", () => { 1164 | const writer = new CodeBlockWriter(); 1165 | writer.hangingIndent(() => { 1166 | writer.quote("t\nu").newLine().write("t"); 1167 | }); 1168 | expect(writer.toString()).to.equal(`"t\\n\\\nu"\n t`); 1169 | }); 1170 | }); 1171 | 1172 | describe("#hangingIndentUnlessBlock", () => { 1173 | it("should write with hanging indentation when not a brace", () => { 1174 | const writer = new CodeBlockWriter(); 1175 | writer.setIndentationLevel(2); 1176 | writer.hangingIndentUnlessBlock(() => { 1177 | expect(writer.getIndentationLevel()).to.equal(2); 1178 | writer.write("t").newLine(); 1179 | expect(writer.getIndentationLevel()).to.equal(3); 1180 | }); 1181 | expect(writer.getIndentationLevel()).to.equal(2); 1182 | }); 1183 | 1184 | it("should not write with hanging indentation when it's a block and using \n newlines", () => { 1185 | const writer = new CodeBlockWriter(); 1186 | writer.hangingIndentUnlessBlock(() => { 1187 | writer.write("t").block(() => { 1188 | writer.write("f"); 1189 | }); 1190 | }); 1191 | expect(writer.toString()).to.equal(`t {\n f\n}`); 1192 | }); 1193 | 1194 | it("should not write with hanging indentation when it's a block and using \r\n newlines", () => { 1195 | const writer = new CodeBlockWriter({ newLine: "\r\n" }); 1196 | writer.hangingIndentUnlessBlock(() => { 1197 | writer.write("t").block(() => { 1198 | writer.write("f"); 1199 | }); 1200 | }); 1201 | expect(writer.toString()).to.equal(`t {\r\n f\r\n}`); 1202 | }); 1203 | 1204 | it("should write blocks at the same hanging indentation once past the first line with hanging indenation", () => { 1205 | const writer = new CodeBlockWriter(); 1206 | writer.hangingIndentUnlessBlock(() => { 1207 | writer.writeLine("t"); 1208 | writer.write("u").block(() => { 1209 | writer.write("f"); 1210 | }); 1211 | writer.write("v"); 1212 | }); 1213 | expect(writer.toString()).to.equal(`t\n u {\n f\n }\n v`); 1214 | }); 1215 | 1216 | it("should ignore blocks written in a string", () => { 1217 | // this would be strange to happen... but this behaviour seems ok 1218 | const writer = new CodeBlockWriter(); 1219 | writer.hangingIndentUnlessBlock(() => { 1220 | writer.writeLine("`t{"); 1221 | writer.write("v`"); 1222 | writer.block(() => writer.write("u")); 1223 | }); 1224 | expect(writer.toString()).to.equal("`t{\nv` {\n u\n}"); 1225 | }); 1226 | }); 1227 | 1228 | describe("#endsWith", () => { 1229 | function doTest(str: string, text: string, expectedValue: boolean) { 1230 | const writer = new CodeBlockWriter(); 1231 | writer.write(str); 1232 | expect(writer.endsWith(text)).to.equal(expectedValue); 1233 | } 1234 | 1235 | it("should be true when equal", () => { 1236 | doTest("test", "test", true); 1237 | }); 1238 | 1239 | it("should be true when it ends with", () => { 1240 | doTest("test", "st", true); 1241 | }); 1242 | 1243 | it("should be false when the provided text is greater than the length", () => { 1244 | doTest("test", "test1", false); 1245 | }); 1246 | 1247 | it("should be false when the provided text does not end with", () => { 1248 | doTest("test", "rt", false); 1249 | }); 1250 | }); 1251 | 1252 | describe("#iterateLastChars", () => { 1253 | it("should iterate over the past characters until the end", () => { 1254 | const writer = new CodeBlockWriter(); 1255 | writer.write("test\n"); 1256 | const expected: [string, number][] = [ 1257 | ["\n", 4], 1258 | ["t", 3], 1259 | ["s", 2], 1260 | ["e", 1], 1261 | ["t", 0], 1262 | ]; 1263 | const result: typeof expected = []; 1264 | const returnValue = writer.iterateLastChars((char, index) => { 1265 | result.push([char, index]); 1266 | }); 1267 | expect(result).to.deep.equal(expected); 1268 | expect(returnValue).to.equal(undefined); 1269 | }); 1270 | 1271 | it("should stop and return the value when returning a non-null value", () => { 1272 | const writer = new CodeBlockWriter(); 1273 | writer.write("test"); 1274 | const returnValue = writer.iterateLastChars(char => { 1275 | if (char !== "t") { 1276 | throw new Error("It didn't stop for some reason."); 1277 | } 1278 | return false; 1279 | }); 1280 | expect(returnValue).to.equal(false); 1281 | }); 1282 | }); 1283 | 1284 | describe("#getIndentationLevel", () => { 1285 | it("should get the indentation level", () => { 1286 | const writer = new CodeBlockWriter(); 1287 | writer.setIndentationLevel(5); 1288 | expect(writer.getIndentationLevel()).to.equal(5); 1289 | }); 1290 | }); 1291 | 1292 | describe("#isInString", () => { 1293 | function doTest(str: string, expectedValues: boolean[]) { 1294 | expect(str.length + 1).to.equal(expectedValues.length); 1295 | const writer = new CodeBlockWriter(); 1296 | expect(writer.isInString()).to.equal(expectedValues[0]); 1297 | for (let i = 0; i < str.length; i++) { 1298 | writer.write(str[i]); 1299 | expect(writer.isInString()).to.equal(expectedValues[i + 1], `at expected position ${i + 1}`); 1300 | } 1301 | } 1302 | 1303 | it("should be in a string while in double quotes", () => { 1304 | doTest(`s"y"`, [false, false, true, true, false]); 1305 | }); 1306 | 1307 | it("should be in a string while in single quotes", () => { 1308 | doTest(`s'y'`, [false, false, true, true, false]); 1309 | }); 1310 | 1311 | it("should be in a string while in backticks", () => { 1312 | doTest("s`y`", [false, false, true, true, false]); 1313 | }); 1314 | 1315 | it("should not be in a string after a new line and not closing the double quote", () => { 1316 | doTest(`s"y\nt`, [false, false, true, true, false, false]); 1317 | }); 1318 | 1319 | it("should be in a string after a new line and not closing the double quote, but escaping the new line", () => { 1320 | doTest(`s"y\\\nt`, [false, false, true, true, true, true, true]); 1321 | }); 1322 | 1323 | it("should not be in a string after a new line and not closing the single quote", () => { 1324 | doTest(`s'y\nt`, [false, false, true, true, false, false]); 1325 | }); 1326 | 1327 | it("should be in a string after a new line and not closing the single quote, but escaping the new line", () => { 1328 | doTest(`s'y\\\nt`, [false, false, true, true, true, true, true]); 1329 | }); 1330 | 1331 | it("should be in a string after a new line and not closing the back tick", () => { 1332 | doTest("s\`y\nt", [false, false, true, true, true, true]); 1333 | }); 1334 | 1335 | it("should not be affected by other string quotes while in double quotes", () => { 1336 | doTest(`"'\`\${}"`, [false, true, true, true, true, true, true, false]); 1337 | }); 1338 | 1339 | it("should not be affected by other string quotes while in single quotes", () => { 1340 | doTest(`'"\`\${}'`, [false, true, true, true, true, true, true, false]); 1341 | }); 1342 | 1343 | it("should not be affected by other string quotes while in back ticks", () => { 1344 | doTest(`\`'"\``, [false, true, true, true, false]); 1345 | }); 1346 | 1347 | it("should not be in a string while in backticks within braces", () => { 1348 | doTest("`y${t}`", [false, true, true, true, false, false, true, false]); 1349 | }); 1350 | 1351 | it("should be in a string while in backticks within braces within a single quote string", () => { 1352 | doTest("`${'t'}`", [false, true, true, false, true, true, false, true, false]); 1353 | }); 1354 | 1355 | it("should be in a string while in backticks within braces within a double quote string", () => { 1356 | doTest("`${\"t\"}`", [false, true, true, false, true, true, false, true, false]); 1357 | }); 1358 | 1359 | it("should be in a string while in backticks within braces within back ticks", () => { 1360 | doTest("`${`t`}`", [false, true, true, false, true, true, false, true, false]); 1361 | }); 1362 | 1363 | it("should not be in a string while in backticks within braces within back ticks within braces", () => { 1364 | doTest("`${`${t}`}`", [false, true, true, false, true, true, false, false, true, false, true, false]); 1365 | }); 1366 | 1367 | it("should not be in a string while comments", () => { 1368 | doTest("//'t'", [false, false, false, false, false, false]); 1369 | }); 1370 | 1371 | it("should be in a string while the previous line was a comment and now this is a string", () => { 1372 | doTest("//t\n't'", [false, false, false, false, false, true, true, false]); 1373 | }); 1374 | 1375 | it("should not be in a string for star comments", () => { 1376 | doTest("/*\n't'\n*/'t'", [false, false, false, false, false, false, false, false, false, false, true, true, false]); 1377 | }); 1378 | 1379 | it("should not be in a string for regex using a single quote", () => { 1380 | doTest("/'test/", [false, false, false, false, false, false, false, false]); 1381 | }); 1382 | 1383 | it("should not be in a string for regex using a double quote", () => { 1384 | doTest("/\"test/", [false, false, false, false, false, false, false, false]); 1385 | }); 1386 | 1387 | it("should not be in a string for regex using a back tick", () => { 1388 | doTest("/`test/", [false, false, false, false, false, false, false, false]); 1389 | }); 1390 | 1391 | it("should be in a string for a string after a regex", () => { 1392 | doTest("/`/'t'", [false, false, false, false, true, true, false]); 1393 | }); 1394 | 1395 | it("should handle escaped single quotes", () => { 1396 | doTest(`'\\''`, [false, true, true, true, false]); 1397 | }); 1398 | 1399 | it("should handle escaped double quotes", () => { 1400 | doTest(`"\\""`, [false, true, true, true, false]); 1401 | }); 1402 | 1403 | it("should handle escaped back ticks", () => { 1404 | doTest("`\\``", [false, true, true, true, false]); 1405 | }); 1406 | 1407 | it("should handle escaped template spans", () => { 1408 | doTest("`\\${t}`", [false, true, true, true, true, true, true, false]); 1409 | }); 1410 | }); 1411 | 1412 | function runSequentialCheck( 1413 | str: string, 1414 | expectedValues: T[], 1415 | func: (writer: CodeBlockWriter) => T, 1416 | writer = new CodeBlockWriter(), 1417 | ) { 1418 | expect(str.length + 1).to.equal(expectedValues.length); 1419 | expect(func(writer)).to.equal(expectedValues[0]); 1420 | for (let i = 0; i < str.length; i++) { 1421 | writer.write(str[i]); 1422 | expect(func(writer)).to.equal(expectedValues[i + 1], `For char: ${JSON.stringify(str[i])} (${i})`); 1423 | } 1424 | } 1425 | 1426 | describe("#isInComment", () => { 1427 | function doTest(str: string, expectedValues: boolean[]) { 1428 | runSequentialCheck(str, expectedValues, writer => writer.isInComment()); 1429 | } 1430 | 1431 | it("should be in a comment for star comments", () => { 1432 | doTest("/*\nt\n*/", [false, false, true, true, true, true, true, false]); 1433 | }); 1434 | 1435 | it("should be in a comment for line comments", () => { 1436 | doTest("// t\nt", [false, false, true, true, true, false, false]); 1437 | }); 1438 | }); 1439 | 1440 | describe("#isLastSpace", () => { 1441 | function doTest(str: string, expectedValues: boolean[]) { 1442 | runSequentialCheck(str, expectedValues, writer => writer.isLastSpace()); 1443 | } 1444 | 1445 | it("should be true when a space", () => { 1446 | doTest("t t\t\r\n", [false, false, true, false, false, false, false]); 1447 | }); 1448 | }); 1449 | 1450 | describe("#isLastTab", () => { 1451 | function doTest(str: string, expectedValues: boolean[]) { 1452 | runSequentialCheck(str, expectedValues, writer => writer.isLastTab()); 1453 | } 1454 | 1455 | it("should be true when a tab", () => { 1456 | doTest("t t\t\r\n", [false, false, false, false, true, false, false]); 1457 | }); 1458 | }); 1459 | 1460 | describe("#isOnFirstLineOfBlock", () => { 1461 | function doTest(str: string, expectedValues: boolean[]) { 1462 | runSequentialCheck(str, expectedValues, writer => writer.isOnFirstLineOfBlock()); 1463 | } 1464 | 1465 | it("should be true up until the new line", () => { 1466 | doTest("t \t\n", [true, true, true, true, false]); 1467 | }); 1468 | 1469 | it("should be true when on a new block", () => { 1470 | const writer = new CodeBlockWriter(); 1471 | assertState(true); 1472 | writer.writeLine("testing"); 1473 | assertState(false); 1474 | writer.inlineBlock(() => { 1475 | assertState(true); 1476 | writer.newLine(); 1477 | assertState(false); 1478 | writer.indent(() => { 1479 | assertState(true); 1480 | writer.write("testing\n"); 1481 | assertState(false); 1482 | writer.write("more\n"); 1483 | assertState(false); 1484 | }); 1485 | assertState(false); 1486 | writer.block(() => { 1487 | assertState(true); 1488 | }); 1489 | assertState(false); 1490 | }); 1491 | assertState(false); 1492 | 1493 | function assertState(state: boolean) { 1494 | expect(writer.isOnFirstLineOfBlock()).to.equal(state); 1495 | } 1496 | }); 1497 | }); 1498 | 1499 | describe("#isAtStartOfFirstLineOfBlock", () => { 1500 | function doTest(str: string, expectedValues: boolean[]) { 1501 | runSequentialCheck(str, expectedValues, writer => writer.isAtStartOfFirstLineOfBlock()); 1502 | } 1503 | 1504 | it("should be true only for the first", () => { 1505 | doTest("t \t\n", [true, false, false, false, false]); 1506 | }); 1507 | 1508 | it("should be true when on a new block at the start", () => { 1509 | const writer = new CodeBlockWriter(); 1510 | assertState(true); 1511 | writer.writeLine("testing"); 1512 | assertState(false); 1513 | writer.inlineBlock(() => { 1514 | assertState(true); 1515 | writer.write(" "); 1516 | assertState(false); 1517 | writer.newLine(); 1518 | assertState(false); 1519 | writer.indent(() => { 1520 | assertState(true); 1521 | writer.write("testing\n"); 1522 | assertState(false); 1523 | writer.write("more\n"); 1524 | assertState(false); 1525 | }); 1526 | assertState(false); 1527 | writer.block(() => { 1528 | assertState(true); 1529 | }); 1530 | assertState(false); 1531 | }); 1532 | assertState(false); 1533 | 1534 | function assertState(state: boolean) { 1535 | expect(writer.isAtStartOfFirstLineOfBlock()).to.equal(state); 1536 | } 1537 | }); 1538 | }); 1539 | 1540 | describe("#isLastNewLine", () => { 1541 | function doTest(str: string, expectedValues: boolean[], customWriter?: CodeBlockWriter) { 1542 | runSequentialCheck(str, expectedValues, writer => writer.isLastNewLine(), customWriter); 1543 | } 1544 | 1545 | it("should be true when a new line", () => { 1546 | doTest(" \nt", [false, false, true, false]); 1547 | }); 1548 | 1549 | it("should be true for \\n when specifying \\r\\n", () => { 1550 | const writer = new CodeBlockWriter({ newLine: "\r\n" }); 1551 | doTest(" \nt", [false, false, true, false], writer); 1552 | }); 1553 | }); 1554 | 1555 | describe("#isLastBlankLine", () => { 1556 | function doTest(str: string, expectedValues: boolean[], customWriter?: CodeBlockWriter) { 1557 | runSequentialCheck(str, expectedValues, writer => writer.isLastBlankLine(), customWriter); 1558 | } 1559 | 1560 | it("should be true when a blank line", () => { 1561 | doTest(" \n\nt", [false, false, false, true, false]); 1562 | }); 1563 | 1564 | it("should be true when using \\r\\n", () => { 1565 | doTest(" \n\r\nt", [false, false, false, false, true, false]); 1566 | }); 1567 | }); 1568 | 1569 | describe("#getLastChar", () => { 1570 | function doTest(str: string, expectedValues: (string | undefined)[]) { 1571 | runSequentialCheck(str, expectedValues, writer => writer.getLastChar()); 1572 | } 1573 | 1574 | it("should get the last char", () => { 1575 | doTest(" \nt", [undefined, " ", "\n", "t"]); 1576 | }); 1577 | }); 1578 | 1579 | describe("#_getLastCharWithOffset", () => { 1580 | function doTest(strs: string[], offset: number, expectedValue: string | undefined) { 1581 | const writer = new CodeBlockWriter(); 1582 | for (const str of strs) { 1583 | writer.write(str); 1584 | } 1585 | expect(writer._getLastCharCodeWithOffset(offset)).to.equal(expectedValue?.charCodeAt(0)); 1586 | } 1587 | 1588 | it("should return undefined for a negative number", () => { 1589 | doTest(["values"], -1, undefined); 1590 | }); 1591 | 1592 | it("should return undefined for the text length", () => { 1593 | doTest(["1", "2", "3"], 3, undefined); 1594 | }); 1595 | 1596 | it("should get when getting the first index", () => { 1597 | doTest(["2", "1", "0"], 0, "0"); 1598 | }); 1599 | 1600 | it("should get when getting the last index", () => { 1601 | doTest(["3", "21", "0"], 3, "3"); 1602 | }); 1603 | 1604 | it("should get when getting the last index of a write", () => { 1605 | doTest(["3", "21", "0"], 2, "2"); 1606 | }); 1607 | 1608 | it("should get when getting the first index of a write", () => { 1609 | doTest(["3", "21", "0"], 1, "1"); 1610 | }); 1611 | }); 1612 | 1613 | describe("indentNumberOfSpaces", () => { 1614 | it("should indent 2 spaces", () => { 1615 | const writer = new CodeBlockWriter({ indentNumberOfSpaces: 2 }); 1616 | writer.write("do").block(() => { 1617 | writer.write("something"); 1618 | }); 1619 | 1620 | const expected = `do { 1621 | something 1622 | }`; 1623 | 1624 | expect(writer.toString()).to.equal(expected); 1625 | }); 1626 | }); 1627 | 1628 | describe("useTabs", () => { 1629 | it("should use tabs", () => { 1630 | const writer = new CodeBlockWriter({ useTabs: true }); 1631 | writer.write("do").block(() => { 1632 | writer.write("do").block(() => { 1633 | writer.write("something"); 1634 | }); 1635 | }); 1636 | 1637 | const expected = `do { 1638 | \tdo { 1639 | \t\tsomething 1640 | \t} 1641 | }`; 1642 | 1643 | expect(writer.toString()).to.equal(expected); 1644 | }); 1645 | }); 1646 | 1647 | describe("#getOptions", () => { 1648 | it("should have the options that were passed in", () => { 1649 | const writer = new CodeBlockWriter({ 1650 | indentNumberOfSpaces: 8, 1651 | newLine: "\r\n", 1652 | useTabs: true, 1653 | useSingleQuote: false, 1654 | }); 1655 | expect(writer.getOptions()).to.deep.equal({ 1656 | indentNumberOfSpaces: 8, 1657 | newLine: "\r\n", 1658 | useTabs: true, 1659 | useSingleQuote: false, 1660 | }); 1661 | }); 1662 | }); 1663 | -------------------------------------------------------------------------------- /mod.ts: -------------------------------------------------------------------------------- 1 | import { escapeForWithinString, getStringFromStrOrFunc } from "./utils/string_utils.ts"; 2 | 3 | /** @internal */ 4 | enum CommentChar { 5 | Line, 6 | Star, 7 | } 8 | 9 | /** 10 | * Options for the writer. 11 | */ 12 | export interface Options { 13 | /** 14 | * Newline character. 15 | * @remarks Defaults to \n. 16 | */ 17 | newLine: "\n" | "\r\n"; 18 | /** 19 | * Number of spaces to indent when `useTabs` is false. 20 | * @remarks Defaults to 4. 21 | */ 22 | indentNumberOfSpaces: number; 23 | /** 24 | * Whether to use tabs (true) or spaces (false). 25 | * @remarks Defaults to false. 26 | */ 27 | useTabs: boolean; 28 | /** 29 | * Whether to use a single quote (true) or double quote (false). 30 | * @remarks Defaults to false. 31 | */ 32 | useSingleQuote: boolean; 33 | } 34 | 35 | // Using the char codes is a performance improvement (about 5.5% faster when writing because it eliminates additional string allocations). 36 | const CHARS = { 37 | BACK_SLASH: "\\".charCodeAt(0), 38 | FORWARD_SLASH: "/".charCodeAt(0), 39 | NEW_LINE: "\n".charCodeAt(0), 40 | CARRIAGE_RETURN: "\r".charCodeAt(0), 41 | ASTERISK: "*".charCodeAt(0), 42 | DOUBLE_QUOTE: "\"".charCodeAt(0), 43 | SINGLE_QUOTE: "'".charCodeAt(0), 44 | BACK_TICK: "`".charCodeAt(0), 45 | OPEN_BRACE: "{".charCodeAt(0), 46 | CLOSE_BRACE: "}".charCodeAt(0), 47 | DOLLAR_SIGN: "$".charCodeAt(0), 48 | SPACE: " ".charCodeAt(0), 49 | TAB: "\t".charCodeAt(0), 50 | }; 51 | const isCharToHandle = new Set([ 52 | CHARS.BACK_SLASH, 53 | CHARS.FORWARD_SLASH, 54 | CHARS.NEW_LINE, 55 | CHARS.CARRIAGE_RETURN, 56 | CHARS.ASTERISK, 57 | CHARS.DOUBLE_QUOTE, 58 | CHARS.SINGLE_QUOTE, 59 | CHARS.BACK_TICK, 60 | CHARS.OPEN_BRACE, 61 | CHARS.CLOSE_BRACE, 62 | ]); 63 | 64 | /** 65 | * Code writer that assists with formatting and visualizing blocks of JavaScript or TypeScript code. 66 | */ 67 | export default class CodeBlockWriter { 68 | /** @internal */ 69 | private readonly _indentationText: string; 70 | /** @internal */ 71 | private readonly _newLine: "\n" | "\r\n"; 72 | /** @internal */ 73 | private readonly _useTabs: boolean; 74 | /** @internal */ 75 | private readonly _quoteChar: string; 76 | /** @internal */ 77 | private readonly _indentNumberOfSpaces: number; 78 | /** @internal */ 79 | private _currentIndentation = 0; 80 | /** @internal */ 81 | private _queuedIndentation: number | undefined; 82 | /** @internal */ 83 | private _queuedOnlyIfNotBlock: true | undefined; 84 | /** @internal */ 85 | private _length = 0; 86 | /** @internal */ 87 | private _newLineOnNextWrite = false; 88 | /** @internal */ 89 | private _currentCommentChar: CommentChar | undefined = undefined; 90 | /** @internal */ 91 | private _stringCharStack: number[] = []; 92 | /** @internal */ 93 | private _isInRegEx = false; 94 | /** @internal */ 95 | private _isOnFirstLineOfBlock = true; 96 | // An array of strings is used rather than a single string because it was 97 | // found to be ~11x faster when printing a 10K line file (~11s to ~1s). 98 | /** @internal */ 99 | private _texts: string[] = []; 100 | 101 | /** 102 | * Constructor. 103 | * @param opts - Options for the writer. 104 | */ 105 | constructor(opts: Partial = {}) { 106 | this._newLine = opts.newLine || "\n"; 107 | this._useTabs = opts.useTabs || false; 108 | this._indentNumberOfSpaces = opts.indentNumberOfSpaces || 4; 109 | this._indentationText = getIndentationText(this._useTabs, this._indentNumberOfSpaces); 110 | this._quoteChar = opts.useSingleQuote ? "'" : `"`; 111 | } 112 | 113 | /** 114 | * Gets the options. 115 | */ 116 | getOptions(): Options { 117 | return { 118 | indentNumberOfSpaces: this._indentNumberOfSpaces, 119 | newLine: this._newLine, 120 | useTabs: this._useTabs, 121 | useSingleQuote: this._quoteChar === "'", 122 | }; 123 | } 124 | 125 | /** 126 | * Queues the indentation level for the next lines written. 127 | * @param indentationLevel - Indentation level to queue. 128 | */ 129 | queueIndentationLevel(indentationLevel: number): this; 130 | /** 131 | * Queues the indentation level for the next lines written using the provided indentation text. 132 | * @param whitespaceText - Gets the indentation level from the indentation text. 133 | */ 134 | queueIndentationLevel(whitespaceText: string): this; 135 | /** @internal */ 136 | queueIndentationLevel(countOrText: string | number): this; 137 | queueIndentationLevel(countOrText: string | number) { 138 | this._queuedIndentation = this._getIndentationLevelFromArg(countOrText); 139 | this._queuedOnlyIfNotBlock = undefined; 140 | return this; 141 | } 142 | 143 | /** 144 | * Writes the text within the provided action with hanging indentation. 145 | * @param action - Action to perform with hanging indentation. 146 | */ 147 | hangingIndent(action: () => void): this { 148 | return this._withResetIndentation(() => this.queueIndentationLevel(this.getIndentationLevel() + 1), action); 149 | } 150 | 151 | /** 152 | * Writes the text within the provided action with hanging indentation unless writing a block. 153 | * @param action - Action to perform with hanging indentation unless a block is written. 154 | */ 155 | hangingIndentUnlessBlock(action: () => void): this { 156 | return this._withResetIndentation(() => { 157 | this.queueIndentationLevel(this.getIndentationLevel() + 1); 158 | this._queuedOnlyIfNotBlock = true; 159 | }, action); 160 | } 161 | 162 | /** 163 | * Sets the current indentation level. 164 | * @param indentationLevel - Indentation level to be at. 165 | */ 166 | setIndentationLevel(indentationLevel: number): this; 167 | /** 168 | * Sets the current indentation using the provided indentation text. 169 | * @param whitespaceText - Gets the indentation level from the indentation text. 170 | */ 171 | setIndentationLevel(whitespaceText: string): this; 172 | /** @internal */ 173 | setIndentationLevel(countOrText: string | number): this; 174 | setIndentationLevel(countOrText: string | number) { 175 | this._currentIndentation = this._getIndentationLevelFromArg(countOrText); 176 | return this; 177 | } 178 | 179 | /** 180 | * Sets the indentation level within the provided action and restores the writer's indentation 181 | * state afterwards. 182 | * @remarks Restores the writer's state after the action. 183 | * @param indentationLevel - Indentation level to set. 184 | * @param action - Action to perform with the indentation. 185 | */ 186 | withIndentationLevel(indentationLevel: number, action: () => void): this; 187 | /** 188 | * Sets the indentation level with the provided indentation text within the provided action 189 | * and restores the writer's indentation state afterwards. 190 | * @param whitespaceText - Gets the indentation level from the indentation text. 191 | * @param action - Action to perform with the indentation. 192 | */ 193 | withIndentationLevel(whitespaceText: string, action: () => void): this; 194 | withIndentationLevel(countOrText: string | number, action: () => void) { 195 | return this._withResetIndentation(() => this.setIndentationLevel(countOrText), action); 196 | } 197 | 198 | /** @internal */ 199 | private _withResetIndentation(setStateAction: () => void, writeAction: () => void) { 200 | const previousState = this._getIndentationState(); 201 | setStateAction(); 202 | try { 203 | writeAction(); 204 | } finally { 205 | this._setIndentationState(previousState); 206 | } 207 | return this; 208 | } 209 | 210 | /** 211 | * Gets the current indentation level. 212 | */ 213 | getIndentationLevel(): number { 214 | return this._currentIndentation; 215 | } 216 | 217 | /** 218 | * Writes a block using braces. 219 | * @param block - Write using the writer within this block. 220 | */ 221 | block(block?: () => void): this { 222 | this._newLineIfNewLineOnNextWrite(); 223 | if (this.getLength() > 0 && !this.isLastNewLine()) { 224 | this.spaceIfLastNot(); 225 | } 226 | this.inlineBlock(block); 227 | this._newLineOnNextWrite = true; 228 | return this; 229 | } 230 | 231 | /** 232 | * Writes an inline block with braces. 233 | * @param block - Write using the writer within this block. 234 | */ 235 | inlineBlock(block?: () => void): this { 236 | this._newLineIfNewLineOnNextWrite(); 237 | this.write("{"); 238 | this._indentBlockInternal(block); 239 | this.newLineIfLastNot().write("}"); 240 | 241 | return this; 242 | } 243 | 244 | /** 245 | * Indents the code one level for the current line. 246 | */ 247 | indent(times?: number): this; 248 | /** 249 | * Indents a block of code. 250 | * @param block - Block to indent. 251 | */ 252 | indent(block: () => void): this; 253 | indent(timesOrBlock: number | (() => void) = 1) { 254 | if (typeof timesOrBlock === "number") { 255 | this._newLineIfNewLineOnNextWrite(); 256 | return this.write(this._indentationText.repeat(timesOrBlock)); 257 | } else { 258 | this._indentBlockInternal(timesOrBlock); 259 | if (!this.isLastNewLine()) { 260 | this._newLineOnNextWrite = true; 261 | } 262 | return this; 263 | } 264 | } 265 | 266 | /** @internal */ 267 | private _indentBlockInternal(block?: () => void) { 268 | if (this.getLastChar() != null) { 269 | this.newLineIfLastNot(); 270 | } 271 | this._currentIndentation++; 272 | this._isOnFirstLineOfBlock = true; 273 | if (block != null) { 274 | block(); 275 | } 276 | this._isOnFirstLineOfBlock = false; 277 | this._currentIndentation = Math.max(0, this._currentIndentation - 1); 278 | } 279 | 280 | /** 281 | * Conditionally writes a line of text. 282 | * @param condition - Condition to evaluate. 283 | * @param textFunc - A function that returns a string to write if the condition is true. 284 | */ 285 | conditionalWriteLine(condition: boolean | undefined, textFunc: () => string): this; 286 | /** 287 | * Conditionally writes a line of text. 288 | * @param condition - Condition to evaluate. 289 | * @param text - Text to write if the condition is true. 290 | */ 291 | conditionalWriteLine(condition: boolean | undefined, text: string): this; 292 | conditionalWriteLine(condition: boolean | undefined, strOrFunc: string | (() => string)) { 293 | if (condition) { 294 | this.writeLine(getStringFromStrOrFunc(strOrFunc)); 295 | } 296 | 297 | return this; 298 | } 299 | 300 | /** 301 | * Writes a line of text. 302 | * @param text - String to write. 303 | */ 304 | writeLine(text: string): this { 305 | this._newLineIfNewLineOnNextWrite(); 306 | if (this.getLastChar() != null) { 307 | this.newLineIfLastNot(); 308 | } 309 | this._writeIndentingNewLines(text); 310 | this.newLine(); 311 | 312 | return this; 313 | } 314 | 315 | /** 316 | * Writes a newline if the last line was not a newline. 317 | */ 318 | newLineIfLastNot(): this { 319 | this._newLineIfNewLineOnNextWrite(); 320 | 321 | if (!this.isLastNewLine()) { 322 | this.newLine(); 323 | } 324 | 325 | return this; 326 | } 327 | 328 | /** 329 | * Writes a blank line if the last written text was not a blank line. 330 | */ 331 | blankLineIfLastNot(): this { 332 | if (!this.isLastBlankLine()) { 333 | this.blankLine(); 334 | } 335 | return this; 336 | } 337 | 338 | /** 339 | * Writes a blank line if the condition is true. 340 | * @param condition - Condition to evaluate. 341 | */ 342 | conditionalBlankLine(condition: boolean | undefined): this { 343 | if (condition) { 344 | this.blankLine(); 345 | } 346 | return this; 347 | } 348 | 349 | /** 350 | * Writes a blank line. 351 | */ 352 | blankLine(): this { 353 | return this.newLineIfLastNot().newLine(); 354 | } 355 | 356 | /** 357 | * Writes a newline if the condition is true. 358 | * @param condition - Condition to evaluate. 359 | */ 360 | conditionalNewLine(condition: boolean | undefined): this { 361 | if (condition) { 362 | this.newLine(); 363 | } 364 | return this; 365 | } 366 | 367 | /** 368 | * Writes a newline. 369 | */ 370 | newLine(): this { 371 | this._newLineOnNextWrite = false; 372 | this._baseWriteNewline(); 373 | return this; 374 | } 375 | 376 | /** 377 | * Writes a quote character. 378 | */ 379 | quote(): this; 380 | /** 381 | * Writes text surrounded in quotes. 382 | * @param text - Text to write. 383 | */ 384 | quote(text: string): this; 385 | quote(text?: string) { 386 | this._newLineIfNewLineOnNextWrite(); 387 | this._writeIndentingNewLines(text == null ? this._quoteChar : this._quoteChar + escapeForWithinString(text, this._quoteChar) + this._quoteChar); 388 | return this; 389 | } 390 | 391 | /** 392 | * Writes a space if the last character was not a space. 393 | */ 394 | spaceIfLastNot(): this { 395 | this._newLineIfNewLineOnNextWrite(); 396 | 397 | if (!this.isLastSpace()) { 398 | this._writeIndentingNewLines(" "); 399 | } 400 | 401 | return this; 402 | } 403 | 404 | /** 405 | * Writes a space. 406 | * @param times - Number of times to write a space. 407 | */ 408 | space(times = 1): this { 409 | this._newLineIfNewLineOnNextWrite(); 410 | this._writeIndentingNewLines(" ".repeat(times)); 411 | return this; 412 | } 413 | 414 | /** 415 | * Writes a tab if the last character was not a tab. 416 | */ 417 | tabIfLastNot(): this { 418 | this._newLineIfNewLineOnNextWrite(); 419 | 420 | if (!this.isLastTab()) { 421 | this._writeIndentingNewLines("\t"); 422 | } 423 | 424 | return this; 425 | } 426 | 427 | /** 428 | * Writes a tab. 429 | * @param times - Number of times to write a tab. 430 | */ 431 | tab(times = 1): this { 432 | this._newLineIfNewLineOnNextWrite(); 433 | this._writeIndentingNewLines("\t".repeat(times)); 434 | return this; 435 | } 436 | 437 | /** 438 | * Conditionally writes text. 439 | * @param condition - Condition to evaluate. 440 | * @param textFunc - A function that returns a string to write if the condition is true. 441 | */ 442 | conditionalWrite(condition: boolean | undefined, textFunc: () => string): this; 443 | /** 444 | * Conditionally writes text. 445 | * @param condition - Condition to evaluate. 446 | * @param text - Text to write if the condition is true. 447 | */ 448 | conditionalWrite(condition: boolean | undefined, text: string): this; 449 | conditionalWrite(condition: boolean | undefined, textOrFunc: string | (() => string)) { 450 | if (condition) { 451 | this.write(getStringFromStrOrFunc(textOrFunc)); 452 | } 453 | 454 | return this; 455 | } 456 | 457 | /** 458 | * Writes the provided text. 459 | * @param text - Text to write. 460 | */ 461 | write(text: string): this { 462 | this._newLineIfNewLineOnNextWrite(); 463 | this._writeIndentingNewLines(text); 464 | return this; 465 | } 466 | 467 | /** 468 | * Writes text to exit a comment if in a comment. 469 | */ 470 | closeComment(): this { 471 | const commentChar = this._currentCommentChar; 472 | 473 | switch (commentChar) { 474 | case CommentChar.Line: 475 | this.newLine(); 476 | break; 477 | case CommentChar.Star: 478 | if (!this.isLastNewLine()) { 479 | this.spaceIfLastNot(); 480 | } 481 | this.write("*/"); 482 | break; 483 | default: { 484 | const _assertUndefined: undefined = commentChar; 485 | break; 486 | } 487 | } 488 | 489 | return this; 490 | } 491 | 492 | /** 493 | * Inserts text at the provided position. 494 | * 495 | * This method is "unsafe" because it won't update the state of the writer unless 496 | * inserting at the end position. It is biased towards being fast at inserting closer 497 | * to the start or end, but slower to insert in the middle. Only use this if 498 | * absolutely necessary. 499 | * @param pos - Position to insert at. 500 | * @param text - Text to insert. 501 | */ 502 | unsafeInsert(pos: number, text: string): this { 503 | const textLength = this._length; 504 | const texts = this._texts; 505 | verifyInput(); 506 | 507 | if (pos === textLength) { 508 | return this.write(text); 509 | } 510 | 511 | updateInternalArray(); 512 | this._length += text.length; 513 | 514 | return this; 515 | 516 | function verifyInput() { 517 | if (pos < 0) { 518 | throw new Error(`Provided position of '${pos}' was less than zero.`); 519 | } 520 | if (pos > textLength) { 521 | throw new Error(`Provided position of '${pos}' was greater than the text length of '${textLength}'.`); 522 | } 523 | } 524 | 525 | function updateInternalArray() { 526 | const { index, localIndex } = getArrayIndexAndLocalIndex(); 527 | 528 | if (localIndex === 0) { 529 | texts.splice(index, 0, text); 530 | } else if (localIndex === texts[index].length) { 531 | texts.splice(index + 1, 0, text); 532 | } else { 533 | const textItem = texts[index]; 534 | const startText = textItem.substring(0, localIndex); 535 | const endText = textItem.substring(localIndex); 536 | texts.splice(index, 1, startText, text, endText); 537 | } 538 | } 539 | 540 | function getArrayIndexAndLocalIndex() { 541 | if (pos < textLength / 2) { 542 | // start searching from the front 543 | let endPos = 0; 544 | for (let i = 0; i < texts.length; i++) { 545 | const textItem = texts[i]; 546 | const startPos = endPos; 547 | endPos += textItem.length; 548 | if (endPos >= pos) { 549 | return { index: i, localIndex: pos - startPos }; 550 | } 551 | } 552 | } else { 553 | // start searching from the back 554 | let startPos = textLength; 555 | for (let i = texts.length - 1; i >= 0; i--) { 556 | const textItem = texts[i]; 557 | startPos -= textItem.length; 558 | if (startPos <= pos) { 559 | return { index: i, localIndex: pos - startPos }; 560 | } 561 | } 562 | } 563 | 564 | throw new Error("Unhandled situation inserting. This should never happen."); 565 | } 566 | } 567 | 568 | /** 569 | * Gets the length of the string in the writer. 570 | */ 571 | getLength(): number { 572 | return this._length; 573 | } 574 | 575 | /** 576 | * Gets if the writer is currently in a comment. 577 | */ 578 | isInComment(): boolean { 579 | return this._currentCommentChar !== undefined; 580 | } 581 | 582 | /** 583 | * Gets if the writer is currently at the start of the first line of the text, block, or indentation block. 584 | */ 585 | isAtStartOfFirstLineOfBlock(): boolean { 586 | return this.isOnFirstLineOfBlock() && (this.isLastNewLine() || this.getLastChar() == null); 587 | } 588 | 589 | /** 590 | * Gets if the writer is currently on the first line of the text, block, or indentation block. 591 | */ 592 | isOnFirstLineOfBlock(): boolean { 593 | return this._isOnFirstLineOfBlock; 594 | } 595 | 596 | /** 597 | * Gets if the writer is currently in a string. 598 | */ 599 | isInString(): boolean { 600 | return this._stringCharStack.length > 0 && this._stringCharStack[this._stringCharStack.length - 1] !== CHARS.OPEN_BRACE; 601 | } 602 | 603 | /** 604 | * Gets if the last chars written were for a newline. 605 | */ 606 | isLastNewLine(): boolean { 607 | const lastChar = this.getLastChar(); 608 | return lastChar === "\n" || lastChar === "\r"; 609 | } 610 | 611 | /** 612 | * Gets if the last chars written were for a blank line. 613 | */ 614 | isLastBlankLine(): boolean { 615 | let foundCount = 0; 616 | 617 | // todo: consider extracting out iterating over past characters, but don't use 618 | // an iterator because it will be slow. 619 | for (let i = this._texts.length - 1; i >= 0; i--) { 620 | const currentText = this._texts[i]; 621 | for (let j = currentText.length - 1; j >= 0; j--) { 622 | const currentChar = currentText.charCodeAt(j); 623 | if (currentChar === CHARS.NEW_LINE) { 624 | foundCount++; 625 | if (foundCount === 2) { 626 | return true; 627 | } 628 | } else if (currentChar !== CHARS.CARRIAGE_RETURN) { 629 | return false; 630 | } 631 | } 632 | } 633 | 634 | return false; 635 | } 636 | 637 | /** 638 | * Gets if the last char written was a space. 639 | */ 640 | isLastSpace(): boolean { 641 | return this.getLastChar() === " "; 642 | } 643 | 644 | /** 645 | * Gets if the last char written was a tab. 646 | */ 647 | isLastTab(): boolean { 648 | return this.getLastChar() === "\t"; 649 | } 650 | 651 | /** 652 | * Gets the last char written. 653 | */ 654 | getLastChar(): string | undefined { 655 | const charCode = this._getLastCharCodeWithOffset(0); 656 | return charCode == null ? undefined : String.fromCharCode(charCode); 657 | } 658 | 659 | /** 660 | * Gets if the writer ends with the provided text. 661 | * @param text - Text to check if the writer ends with the provided text. 662 | */ 663 | endsWith(text: string): boolean { 664 | const length = this._length; 665 | return this.iterateLastCharCodes((charCode, index) => { 666 | const offset = length - index; 667 | const textIndex = text.length - offset; 668 | if (text.charCodeAt(textIndex) !== charCode) { 669 | return false; 670 | } 671 | return textIndex === 0 ? true : undefined; 672 | }) || false; 673 | } 674 | 675 | /** 676 | * Iterates over the writer characters in reverse order. The iteration stops when a non-null or 677 | * undefined value is returned from the action. The returned value is then returned by the method. 678 | * 679 | * @remarks It is much more efficient to use this method rather than `#toString()` since `#toString()` 680 | * will combine the internal array into a string. 681 | */ 682 | iterateLastChars(action: (char: string, index: number) => T | undefined): T | undefined { 683 | return this.iterateLastCharCodes((charCode, index) => action(String.fromCharCode(charCode), index)); 684 | } 685 | 686 | /** 687 | * Iterates over the writer character char codes in reverse order. The iteration stops when a non-null or 688 | * undefined value is returned from the action. The returned value is then returned by the method. 689 | * 690 | * @remarks It is much more efficient to use this method rather than `#toString()` since `#toString()` 691 | * will combine the internal array into a string. Additionally, this is slightly more efficient that 692 | * `iterateLastChars` as this won't allocate a string per character. 693 | */ 694 | iterateLastCharCodes(action: (charCode: number, index: number) => T | undefined): T | undefined { 695 | let index = this._length; 696 | for (let i = this._texts.length - 1; i >= 0; i--) { 697 | const currentText = this._texts[i]; 698 | for (let j = currentText.length - 1; j >= 0; j--) { 699 | index--; 700 | const result = action(currentText.charCodeAt(j), index); 701 | if (result != null) { 702 | return result; 703 | } 704 | } 705 | } 706 | return undefined; 707 | } 708 | 709 | /** 710 | * Gets the writer's text. 711 | */ 712 | toString(): string { 713 | if (this._texts.length > 1) { 714 | const text = this._texts.join(""); 715 | this._texts.length = 0; 716 | this._texts.push(text); 717 | } 718 | 719 | return this._texts[0] || ""; 720 | } 721 | 722 | /** @internal */ 723 | private static readonly _newLineRegEx = /\r?\n/; 724 | /** @internal */ 725 | private _writeIndentingNewLines(text: string) { 726 | text = text || ""; 727 | if (text.length === 0) { 728 | writeIndividual(this, ""); 729 | return; 730 | } 731 | 732 | const items = text.split(CodeBlockWriter._newLineRegEx); 733 | items.forEach((s, i) => { 734 | if (i > 0) { 735 | this._baseWriteNewline(); 736 | } 737 | 738 | if (s.length === 0) { 739 | return; 740 | } 741 | 742 | writeIndividual(this, s); 743 | }); 744 | 745 | function writeIndividual(writer: CodeBlockWriter, s: string) { 746 | if (!writer.isInString()) { 747 | const isAtStartOfLine = writer.isLastNewLine() || writer.getLastChar() == null; 748 | if (isAtStartOfLine) { 749 | writer._writeIndentation(); 750 | } 751 | } 752 | 753 | writer._updateInternalState(s); 754 | writer._internalWrite(s); 755 | } 756 | } 757 | 758 | /** @internal */ 759 | private _baseWriteNewline() { 760 | if (this._currentCommentChar === CommentChar.Line) { 761 | this._currentCommentChar = undefined; 762 | } 763 | 764 | const lastStringCharOnStack = this._stringCharStack[this._stringCharStack.length - 1]; 765 | if ((lastStringCharOnStack === CHARS.DOUBLE_QUOTE || lastStringCharOnStack === CHARS.SINGLE_QUOTE) && this._getLastCharCodeWithOffset(0) !== CHARS.BACK_SLASH) { 766 | this._stringCharStack.pop(); 767 | } 768 | 769 | this._internalWrite(this._newLine); 770 | this._isOnFirstLineOfBlock = false; 771 | this._dequeueQueuedIndentation(); 772 | } 773 | 774 | /** @internal */ 775 | private _dequeueQueuedIndentation() { 776 | if (this._queuedIndentation == null) { 777 | return; 778 | } 779 | 780 | if (this._queuedOnlyIfNotBlock && wasLastBlock(this)) { 781 | this._queuedIndentation = undefined; 782 | this._queuedOnlyIfNotBlock = undefined; 783 | } else { 784 | this._currentIndentation = this._queuedIndentation; 785 | this._queuedIndentation = undefined; 786 | } 787 | 788 | function wasLastBlock(writer: CodeBlockWriter) { 789 | let foundNewLine = false; 790 | return writer.iterateLastCharCodes(charCode => { 791 | switch (charCode) { 792 | case CHARS.NEW_LINE: 793 | if (foundNewLine) { 794 | return false; 795 | } else { 796 | foundNewLine = true; 797 | } 798 | break; 799 | case CHARS.CARRIAGE_RETURN: 800 | return undefined; 801 | case CHARS.OPEN_BRACE: 802 | return true; 803 | default: 804 | return false; 805 | } 806 | }); 807 | } 808 | } 809 | 810 | /** @internal */ 811 | private _updateInternalState(str: string) { 812 | for (let i = 0; i < str.length; i++) { 813 | const currentChar = str.charCodeAt(i); 814 | 815 | // This is a performance optimization to short circuit all the checks below. If the current char 816 | // is not in this set then it won't change any internal state so no need to continue and do 817 | // so many other checks (this made it 3x faster in one scenario I tested). 818 | if (!isCharToHandle.has(currentChar)) { 819 | continue; 820 | } 821 | 822 | const pastChar = i === 0 ? this._getLastCharCodeWithOffset(0) : str.charCodeAt(i - 1); 823 | const pastPastChar = i === 0 ? this._getLastCharCodeWithOffset(1) : i === 1 ? this._getLastCharCodeWithOffset(0) : str.charCodeAt(i - 2); 824 | 825 | // handle regex 826 | if (this._isInRegEx) { 827 | if (pastChar === CHARS.FORWARD_SLASH && pastPastChar !== CHARS.BACK_SLASH || pastChar === CHARS.NEW_LINE) { 828 | this._isInRegEx = false; 829 | } else { 830 | continue; 831 | } 832 | } else if (!this.isInString() && !this.isInComment() && isRegExStart(currentChar, pastChar, pastPastChar)) { 833 | this._isInRegEx = true; 834 | continue; 835 | } 836 | 837 | // handle comments 838 | if (!this.isInString()) { 839 | if (this._currentCommentChar == null && pastChar === CHARS.FORWARD_SLASH && currentChar === CHARS.FORWARD_SLASH) { 840 | this._currentCommentChar = CommentChar.Line; 841 | } else if (this._currentCommentChar == null && pastChar === CHARS.FORWARD_SLASH && currentChar === CHARS.ASTERISK) { 842 | this._currentCommentChar = CommentChar.Star; 843 | } else if (this._currentCommentChar === CommentChar.Star && pastChar === CHARS.ASTERISK && currentChar === CHARS.FORWARD_SLASH) { 844 | this._currentCommentChar = undefined; 845 | } 846 | } 847 | if (this.isInComment()) { 848 | continue; 849 | } 850 | 851 | // handle strings 852 | const lastStringCharOnStack = this._stringCharStack.length === 0 ? undefined : this._stringCharStack[this._stringCharStack.length - 1]; 853 | if (pastChar !== CHARS.BACK_SLASH && (currentChar === CHARS.DOUBLE_QUOTE || currentChar === CHARS.SINGLE_QUOTE || currentChar === CHARS.BACK_TICK)) { 854 | if (lastStringCharOnStack === currentChar) { 855 | this._stringCharStack.pop(); 856 | } else if (lastStringCharOnStack === CHARS.OPEN_BRACE || lastStringCharOnStack === undefined) { 857 | this._stringCharStack.push(currentChar); 858 | } 859 | } else if (pastPastChar !== CHARS.BACK_SLASH && pastChar === CHARS.DOLLAR_SIGN && currentChar === CHARS.OPEN_BRACE && lastStringCharOnStack === CHARS.BACK_TICK) { 860 | this._stringCharStack.push(currentChar); 861 | } else if (currentChar === CHARS.CLOSE_BRACE && lastStringCharOnStack === CHARS.OPEN_BRACE) { 862 | this._stringCharStack.pop(); 863 | } 864 | } 865 | } 866 | 867 | /** @internal - This is private, but exposed for testing. */ 868 | _getLastCharCodeWithOffset(offset: number): number | undefined { 869 | if (offset >= this._length || offset < 0) { 870 | return undefined; 871 | } 872 | 873 | for (let i = this._texts.length - 1; i >= 0; i--) { 874 | const currentText = this._texts[i]; 875 | if (offset >= currentText.length) { 876 | offset -= currentText.length; 877 | } else { 878 | return currentText.charCodeAt(currentText.length - 1 - offset); 879 | } 880 | } 881 | return undefined; 882 | } 883 | 884 | /** @internal */ 885 | private _writeIndentation() { 886 | const flooredIndentation = Math.floor(this._currentIndentation); 887 | this._internalWrite(this._indentationText.repeat(flooredIndentation)); 888 | 889 | const overflow = this._currentIndentation - flooredIndentation; 890 | if (this._useTabs) { 891 | if (overflow > 0.5) { 892 | this._internalWrite(this._indentationText); 893 | } 894 | } else { 895 | const portion = Math.round(this._indentationText.length * overflow); 896 | 897 | // build up the string first, then append it for performance reasons 898 | let text = ""; 899 | for (let i = 0; i < portion; i++) { 900 | text += this._indentationText[i]; 901 | } 902 | this._internalWrite(text); 903 | } 904 | } 905 | 906 | /** @internal */ 907 | private _newLineIfNewLineOnNextWrite() { 908 | if (!this._newLineOnNextWrite) { 909 | return; 910 | } 911 | this._newLineOnNextWrite = false; 912 | this.newLine(); 913 | } 914 | 915 | /** @internal */ 916 | private _internalWrite(text: string) { 917 | if (text.length === 0) { 918 | return; 919 | } 920 | 921 | this._texts.push(text); 922 | this._length += text.length; 923 | } 924 | 925 | /** @internal */ 926 | private static readonly _spacesOrTabsRegEx = /^[ \t]*$/; 927 | /** @internal */ 928 | private _getIndentationLevelFromArg(countOrText: string | number) { 929 | if (typeof countOrText === "number") { 930 | if (countOrText < 0) { 931 | throw new Error("Passed in indentation level should be greater than or equal to 0."); 932 | } 933 | return countOrText; 934 | } else if (typeof countOrText === "string") { 935 | if (!CodeBlockWriter._spacesOrTabsRegEx.test(countOrText)) { 936 | throw new Error("Provided string must be empty or only contain spaces or tabs."); 937 | } 938 | 939 | const { spacesCount, tabsCount } = getSpacesAndTabsCount(countOrText); 940 | return tabsCount + spacesCount / this._indentNumberOfSpaces; 941 | } else { 942 | throw new Error("Argument provided must be a string or number."); 943 | } 944 | } 945 | 946 | /** @internal */ 947 | private _setIndentationState(state: IndentationLevelState) { 948 | this._currentIndentation = state.current; 949 | this._queuedIndentation = state.queued; 950 | this._queuedOnlyIfNotBlock = state.queuedOnlyIfNotBlock; 951 | } 952 | 953 | /** @internal */ 954 | private _getIndentationState(): IndentationLevelState { 955 | return { 956 | current: this._currentIndentation, 957 | queued: this._queuedIndentation, 958 | queuedOnlyIfNotBlock: this._queuedOnlyIfNotBlock, 959 | }; 960 | } 961 | } 962 | 963 | interface IndentationLevelState { 964 | current: number; 965 | queued: number | undefined; 966 | queuedOnlyIfNotBlock: true | undefined; 967 | } 968 | 969 | function isRegExStart(currentChar: number, pastChar: number | undefined, pastPastChar: number | undefined) { 970 | return pastChar === CHARS.FORWARD_SLASH 971 | && currentChar !== CHARS.FORWARD_SLASH 972 | && currentChar !== CHARS.ASTERISK 973 | && pastPastChar !== CHARS.ASTERISK 974 | && pastPastChar !== CHARS.FORWARD_SLASH; 975 | } 976 | 977 | function getIndentationText(useTabs: boolean, numberSpaces: number) { 978 | if (useTabs) { 979 | return "\t"; 980 | } 981 | return Array(numberSpaces + 1).join(" "); 982 | } 983 | 984 | function getSpacesAndTabsCount(str: string) { 985 | let spacesCount = 0; 986 | let tabsCount = 0; 987 | 988 | for (let i = 0; i < str.length; i++) { 989 | const charCode = str.charCodeAt(i); 990 | if (charCode === CHARS.SPACE) { 991 | spacesCount++; 992 | } else if (charCode === CHARS.TAB) { 993 | tabsCount++; 994 | } 995 | } 996 | 997 | return { spacesCount, tabsCount }; 998 | } 999 | -------------------------------------------------------------------------------- /scripts/build_npm.ts: -------------------------------------------------------------------------------- 1 | import { build, emptyDir } from "@deno/dnt"; 2 | 3 | await emptyDir("./npm"); 4 | 5 | await build({ 6 | entryPoints: ["mod.ts"], 7 | test: true, 8 | outDir: "./npm", 9 | shims: { 10 | deno: "dev", 11 | }, 12 | compilerOptions: { 13 | "stripInternal": true 14 | }, 15 | package: { 16 | name: "code-block-writer", 17 | version: Deno.args[0], 18 | description: "A simple code writer that assists with formatting and visualizing blocks of code.", 19 | repository: { 20 | type: "git", 21 | url: "git+https://github.com/dsherret/code-block-writer.git", 22 | }, 23 | keywords: [ 24 | "code generation", 25 | "typescript", 26 | "writer", 27 | "printer", 28 | ], 29 | author: "David Sherret", 30 | license: "MIT", 31 | bugs: { 32 | url: "https://github.com/dsherret/code-block-writer/issues", 33 | }, 34 | homepage: "https://github.com/dsherret/code-block-writer#readme", 35 | }, 36 | }); 37 | 38 | Deno.copyFileSync("LICENSE", "npm/LICENSE"); 39 | Deno.copyFileSync("README.md", "npm/README.md"); 40 | -------------------------------------------------------------------------------- /utils/string_utils.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "../_test.deps.ts"; 2 | import { escapeForWithinString, getStringFromStrOrFunc } from "./string_utils.ts"; 3 | 4 | describe("escapeForWithinString", () => { 5 | function doTest(input: string, expected: string) { 6 | expect(escapeForWithinString(input, "\"")).to.equal(expected); 7 | } 8 | 9 | it("should escape the quotes and newline", () => { 10 | doTest(`"testing\n this out"`, `\\"testing\\n\\\n this out\\"`); 11 | }); 12 | 13 | function doQuoteTest(input: string, quote: string, expected: string) { 14 | expect(escapeForWithinString(input, quote)).to.equal(expected); 15 | } 16 | 17 | it("should escape the single quotes when specified", () => { 18 | doQuoteTest(`'testing "this" out'`, `'`, `\\'testing "this" out\\'`); 19 | }); 20 | 21 | it("should escape regardless of if the character is already escaped", () => { 22 | doQuoteTest(`"testing \\"this\\" out"`, `"`, `\\"testing \\\\\\"this\\\\\\" out\\"`); 23 | }); 24 | 25 | it("should escape unicode escape sequences", () => { 26 | doQuoteTest(`\\u0009`, `"`, `\\\\u0009`); 27 | }); 28 | }); 29 | 30 | describe("getStringFromStrOrFunc", () => { 31 | it("should return a string when given a string", () => { 32 | expect(getStringFromStrOrFunc("test")).to.equal("test"); 33 | }); 34 | 35 | it("should return a string when given a function", () => { 36 | expect(getStringFromStrOrFunc(() => "test")).to.equal("test"); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /utils/string_utils.ts: -------------------------------------------------------------------------------- 1 | /** @internal */ 2 | export function escapeForWithinString(str: string, quoteKind: string) { 3 | let result = ""; 4 | // todo: reduce appends (don't go char by char) 5 | for (let i = 0; i < str.length; i++) { 6 | if (str[i] === quoteKind) { 7 | result += "\\"; 8 | } else if (str[i] === "\r" && str[i + 1] === "\n") { 9 | result += "\\r\\n\\"; 10 | i++; // skip the \r 11 | } else if (str[i] === "\n") { 12 | result += "\\n\\"; 13 | } else if (str[i] === "\\") { 14 | result += "\\"; 15 | } 16 | result += str[i]; 17 | } 18 | return result; 19 | } 20 | 21 | /** @internal */ 22 | export function getStringFromStrOrFunc(strOrFunc: string | (() => string)) { 23 | return strOrFunc instanceof Function ? strOrFunc() : strOrFunc; 24 | } 25 | --------------------------------------------------------------------------------