├── .eslintrc.json ├── .gitattributes ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .npmignore ├── .prettierrc ├── .vscode └── launch.json ├── HISTORY.md ├── LICENSE ├── README.md ├── dist ├── index.d.ts ├── index.js └── index.js.map ├── esm ├── index.d.ts ├── index.js └── index.js.map ├── package-lock.json ├── package.json ├── src ├── __tests__ │ └── index.spec.ts └── index.ts ├── tsconfig.jest.json └── tsconfig.json /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2020": true, 5 | "node": true 6 | }, 7 | "extends": [ 8 | "eslint:recommended", 9 | "plugin:@typescript-eslint/recommended" 10 | ], 11 | "parser": "@typescript-eslint/parser", 12 | "parserOptions": { 13 | "ecmaVersion": 11, 14 | "sourceType": "module" 15 | }, 16 | "plugins": [ 17 | "@typescript-eslint" 18 | ], 19 | "rules": { 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: [master] 4 | pull_request: 5 | branches: [master] 6 | 7 | jobs: 8 | test: 9 | runs-on: ${{ matrix.os }} 10 | strategy: 11 | matrix: 12 | node: [ 13 | lts/*, 14 | 19.x, # EOL 2023-04-01 / 2023-06-01 15 | 18.x, # EOL 2023-10-18 / 2025-04-30 16 | ] 17 | os: [ 18 | macos-latest, 19 | windows-latest, 20 | ubuntu-latest, 21 | ubuntu-22.04, # EOL 2027-04-21 / 2032-04-01 22 | ubuntu-20.04, # EOL 2025-04-02 / 2030-04-01 23 | ] 24 | 25 | steps: 26 | # Configures the node version used on GitHub-hosted runners 27 | - uses: actions/setup-node@v3 28 | with: 29 | # The Node.js version to configure 30 | node-version: ${{ matrix.node }} 31 | - name: Checkout 32 | uses: actions/checkout@v3 33 | - name: Cache node modules 34 | uses: actions/cache@v3 35 | env: 36 | cache-name: cache-node-modules 37 | with: 38 | # npm cache files are stored in `~/.npm` on Linux/macOS 39 | path: ~/.npm 40 | key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }} 41 | restore-keys: | 42 | ${{ runner.os }}-build-${{ env.cache-name }}- 43 | ${{ runner.os }}-build- 44 | ${{ runner.os }}- 45 | - name: Install node modules 46 | run: npm install 47 | - name: Test 48 | run: npm run test 49 | 50 | build: 51 | runs-on: ${{ matrix.os }} 52 | strategy: 53 | matrix: 54 | node: [ 55 | lts/*, 56 | 19.x, # EOL 2023-04-01 / 2023-06-01 57 | 18.x, # EOL 2023-10-18 / 2025-04-30 58 | ] 59 | os: [ 60 | macos-latest, 61 | windows-latest, 62 | ubuntu-latest, 63 | ubuntu-22.04, # EOL 2027-04-21 / 2032-04-01 64 | ubuntu-20.04, # EOL 2025-04-02 / 2030-04-01 65 | ] 66 | 67 | steps: 68 | # Configures the node version used on GitHub-hosted runners 69 | - uses: actions/setup-node@v3 70 | with: 71 | # The Node.js version to configure 72 | node-version: ${{ matrix.node }} 73 | - name: Checkout 74 | uses: actions/checkout@v3 75 | - name: Cache node modules 76 | uses: actions/cache@v3 77 | env: 78 | cache-name: cache-node-modules 79 | with: 80 | # npm cache files are stored in `~/.npm` on Linux/macOS 81 | path: ~/.npm 82 | key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }} 83 | restore-keys: | 84 | ${{ runner.os }}-build-${{ env.cache-name }}- 85 | ${{ runner.os }}-build- 86 | ${{ runner.os }}- 87 | - name: Install node modules 88 | run: npm install 89 | - name: Build 90 | run: npm run compile 91 | 92 | coverage: 93 | runs-on: ubuntu-latest 94 | steps: 95 | # Configures the node version used on GitHub-hosted runners 96 | - uses: actions/setup-node@v3 97 | with: 98 | # The Node.js version to configure 99 | node-version: lts/* 100 | - name: Checkout 101 | uses: actions/checkout@v3 102 | - name: Cache node modules 103 | uses: actions/cache@v3 104 | env: 105 | cache-name: cache-node-modules 106 | with: 107 | # npm cache files are stored in `~/.npm` on Linux/macOS 108 | path: ~/.npm 109 | key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }} 110 | restore-keys: | 111 | ${{ runner.os }}-build-${{ env.cache-name }}- 112 | ${{ runner.os }}-build- 113 | ${{ runner.os }}- 114 | - name: Install node modules 115 | run: npm install 116 | - name: Run Coverage Script 117 | run: npm run ci 118 | - name: Upload coverage to Codecov 119 | uses: codecov/codecov-action@v3 120 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | coverage* 2 | .eslintcache 3 | node_modules 4 | npm-debug.log* 5 | .DS_Store 6 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | * 2 | !dist 3 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "files": "*.ts", 3 | "trailingComma": "all", 4 | "tabWidth": 2, 5 | "semi": true, 6 | "singleQuote": true, 7 | "overrides": [{ 8 | "files": ".prettierrc", 9 | "options": { 10 | "parser": "json" 11 | } 12 | }] 13 | } 14 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "name": "vscode-jest-tests", 10 | "request": "launch", 11 | "args": [ 12 | "--runInBand" 13 | ], 14 | "cwd": "${workspaceFolder}", 15 | "console": "integratedTerminal", 16 | "internalConsoleOptions": "neverOpen", 17 | "program": "${workspaceFolder}/node_modules/jest/bin/jest" 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /HISTORY.md: -------------------------------------------------------------------------------- 1 | # History 2 | 3 | ## vNext 4 | 5 | TBD 6 | 7 | ## v2.2.1 8 | 9 | - Fixed typo in readme 10 | - Updated build tools 11 | - Bumped CI test versions 12 | 13 | ## v2.2.0 14 | 15 | Add indentation to values with multiline strings & added ESM module 16 | 17 | - Updated all dependencies to their latest version 18 | - Updated CI settings (added node 16, multiple os platforms) 19 | - Moved from Travis CI to Github Actions 20 | 21 | ## v2.1.1 22 | 23 | Security update with dependency changes 24 | 25 | - Updated all dependencies to their latest version 26 | - Updated CI settings (added node 15) 27 | 28 | ## v2.1.0 29 | 30 | - Correctly handle escape sequences when used as a tag 31 | - Add test build to CI 32 | - Only run coverage once per change 33 | 34 | ## v2.0.0 35 | 36 | Fixes #4 37 | 38 | - ! Might break/change existing behavior 39 | - If a line does not start with whitespace don't remove the indentation 40 | 41 | ## v1.2.0 42 | 43 | Security update with dependency changes 44 | 45 | - Updated all dependencies to their latest version 46 | - Updated CI settings 47 | - Replaced tslint with typescript-eslint 48 | - Removed unused @types/node 49 | - Added lint to run with the test suite 50 | 51 | ## v1.1.0 52 | 53 | Security update with dependency changes 54 | 55 | - Updated all dependencies to their latest version 56 | 57 | ## v1.0.0 58 | 59 | First release includes following functions 60 | 61 | - `function dedent(TemplateStringsArray | string, ...any[]): string 62 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Tamino Martinius 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TypeScript Dedent 2 | 3 | [![codecov](https://codecov.io/gh/tamino-martinius/node-ts-dedent/branch/master/graph/badge.svg)](https://codecov.io/gh/tamino-martinius/node-ts-dedent) 4 | 5 | TypeScript package which smartly trims and strips indentation from multi-line strings. 6 | 7 | ## Usage Examples 8 | 9 | ```js 10 | import { dedent } from 'ts-dedent'; 11 | 12 | console.log(dedent`A string that gets so long you need to break it over 13 | multiple lines. Luckily dedent is here to keep it 14 | readable without lots of spaces ending up in the string 15 | itself.`); 16 | 17 | console.log(dedent` 18 | A string that gets so long you need to break it over 19 | multiple lines. Luckily dedent is here to keep it 20 | readable without lots of spaces ending up in the string 21 | itself. 22 | `); 23 | ``` 24 | 25 | ```txt 26 | A string that gets so long you need to break it over 27 | multiple lines. Luckily dedent is here to keep it 28 | readable without lots of spaces ending up in the string 29 | itself. 30 | ``` 31 | 32 | --- 33 | 34 | ```js 35 | console.log(dedent` 36 | Leading and trailing lines will be trimmed, so you can write something like 37 | this and have it work as you expect: 38 | 39 | * how convenient it is 40 | * that I can use an indented list 41 | - and still have it do the right thing 42 | 43 | That's all. 44 | `); 45 | ``` 46 | 47 | ```txt 48 | Leading and trailing lines will be trimmed, so you can write something like 49 | this and have it work as you expect: 50 | 51 | * how convenient it is 52 | * that I can use an indented list 53 | - and still have it do the right thing 54 | 55 | That's all. 56 | ``` 57 | 58 | --- 59 | 60 | ```js 61 | console.log(dedent` 62 | Also works fine 63 | 64 | ${1}. With any kind of 65 | ${2}. Placeholders 66 | `); 67 | ``` 68 | 69 | ```txt 70 | Also works fine 71 | 72 | 1. With any kind of 73 | 2. Placeholders 74 | ``` 75 | 76 | --- 77 | 78 | ```js 79 | console.log(dedent(` 80 | Wait! I lied. Dedent can also be used as a function. 81 | `); 82 | ``` 83 | 84 | ```txt 85 | Wait! I lied. Dedent can also be used as a function. 86 | ``` 87 | 88 | ## License 89 | 90 | MIT 91 | 92 | ## Based on 93 | 94 | - [dedent](https://www.npmjs.com/package/dedent) by ~dmnd 95 | - [dedent-js](https://www.npmjs.com/package/dedent-js) by ~martin-kolarik 96 | 97 | ## Changelog 98 | 99 | See [history](HISTORY.md) for more details. 100 | 101 | - `2.2.1` **2021-08-01** Update build dependencies and fixed typos in readme 102 | - `2.2.0` **2021-08-01** Add indentation to values with multiline strings & added ESM module 103 | - `2.1.1` **2021-03-31** Update build dependencies 104 | - `2.1.0` **2021-03-24** Bugfixes 105 | - `2.0.0` **2020-09-28** Bugfixes 106 | - `1.2.0` **2020-09-28** Update build dependencies and a couple of minor improvments 107 | - `1.1.0` **2019-07-26** Update build dependencies and fixed links in readme 108 | - `1.0.0` **2018-06-14** Initial release 109 | -------------------------------------------------------------------------------- /dist/index.d.ts: -------------------------------------------------------------------------------- 1 | export declare function dedent(templ: TemplateStringsArray | string, ...values: unknown[]): string; 2 | export default dedent; 3 | -------------------------------------------------------------------------------- /dist/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.dedent = void 0; 4 | function dedent(templ) { 5 | var values = []; 6 | for (var _i = 1; _i < arguments.length; _i++) { 7 | values[_i - 1] = arguments[_i]; 8 | } 9 | var strings = Array.from(typeof templ === 'string' ? [templ] : templ); 10 | strings[strings.length - 1] = strings[strings.length - 1].replace(/\r?\n([\t ]*)$/, ''); 11 | var indentLengths = strings.reduce(function (arr, str) { 12 | var matches = str.match(/\n([\t ]+|(?!\s).)/g); 13 | if (matches) { 14 | return arr.concat(matches.map(function (match) { var _a, _b; return (_b = (_a = match.match(/[\t ]/g)) === null || _a === void 0 ? void 0 : _a.length) !== null && _b !== void 0 ? _b : 0; })); 15 | } 16 | return arr; 17 | }, []); 18 | if (indentLengths.length) { 19 | var pattern_1 = new RegExp("\n[\t ]{" + Math.min.apply(Math, indentLengths) + "}", 'g'); 20 | strings = strings.map(function (str) { return str.replace(pattern_1, '\n'); }); 21 | } 22 | strings[0] = strings[0].replace(/^\r?\n/, ''); 23 | var string = strings[0]; 24 | values.forEach(function (value, i) { 25 | var endentations = string.match(/(?:^|\n)( *)$/); 26 | var endentation = endentations ? endentations[1] : ''; 27 | var indentedValue = value; 28 | if (typeof value === 'string' && value.includes('\n')) { 29 | indentedValue = String(value) 30 | .split('\n') 31 | .map(function (str, i) { 32 | return i === 0 ? str : "" + endentation + str; 33 | }) 34 | .join('\n'); 35 | } 36 | string += indentedValue + strings[i + 1]; 37 | }); 38 | return string; 39 | } 40 | exports.dedent = dedent; 41 | exports.default = dedent; 42 | //# sourceMappingURL=index.js.map -------------------------------------------------------------------------------- /dist/index.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;AAAA,SAAgB,MAAM,CACpB,KAAoC;IACpC,gBAAoB;SAApB,UAAoB,EAApB,qBAAoB,EAApB,IAAoB;QAApB,+BAAoB;;IAEpB,IAAI,OAAO,GAAG,KAAK,CAAC,IAAI,CAAC,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;IAGtE,OAAO,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,GAAG,OAAO,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,OAAO,CAC/D,gBAAgB,EAChB,EAAE,CACH,CAAC;IAGF,IAAM,aAAa,GAAG,OAAO,CAAC,MAAM,CAAC,UAAC,GAAG,EAAE,GAAG;QAC5C,IAAM,OAAO,GAAG,GAAG,CAAC,KAAK,CAAC,qBAAqB,CAAC,CAAC;QACjD,IAAI,OAAO,EAAE;YACX,OAAO,GAAG,CAAC,MAAM,CACf,OAAO,CAAC,GAAG,CAAC,UAAC,KAAK,gBAAK,OAAA,MAAA,MAAA,KAAK,CAAC,KAAK,CAAC,QAAQ,CAAC,0CAAE,MAAM,mCAAI,CAAC,CAAA,EAAA,CAAC,CAC3D,CAAC;SACH;QACD,OAAO,GAAG,CAAC;IACb,CAAC,EAAY,EAAE,CAAC,CAAC;IAGjB,IAAI,aAAa,CAAC,MAAM,EAAE;QACxB,IAAM,SAAO,GAAG,IAAI,MAAM,CAAC,aAAW,IAAI,CAAC,GAAG,OAAR,IAAI,EAAQ,aAAa,OAAI,EAAE,GAAG,CAAC,CAAC;QAE1E,OAAO,GAAG,OAAO,CAAC,GAAG,CAAC,UAAC,GAAG,IAAK,OAAA,GAAG,CAAC,OAAO,CAAC,SAAO,EAAE,IAAI,CAAC,EAA1B,CAA0B,CAAC,CAAC;KAC5D;IAGD,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC;IAG9C,IAAI,MAAM,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;IAExB,MAAM,CAAC,OAAO,CAAC,UAAC,KAAK,EAAE,CAAC;QAEtB,IAAM,YAAY,GAAG,MAAM,CAAC,KAAK,CAAC,eAAe,CAAC,CAAA;QAClD,IAAM,WAAW,GAAG,YAAY,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAA;QACvD,IAAI,aAAa,GAAG,KAAK,CAAA;QAEzB,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE;YACrD,aAAa,GAAG,MAAM,CAAC,KAAK,CAAC;iBAC1B,KAAK,CAAC,IAAI,CAAC;iBACX,GAAG,CAAC,UAAC,GAAG,EAAE,CAAC;gBACV,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,KAAG,WAAW,GAAG,GAAK,CAAA;YAC/C,CAAC,CAAC;iBACD,IAAI,CAAC,IAAI,CAAC,CAAC;SACf;QAED,MAAM,IAAI,aAAa,GAAG,OAAO,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;IAC3C,CAAC,CAAC,CAAC;IAEH,OAAO,MAAM,CAAC;AAChB,CAAC;AAvDD,wBAuDC;AAED,kBAAe,MAAM,CAAC"} -------------------------------------------------------------------------------- /esm/index.d.ts: -------------------------------------------------------------------------------- 1 | export declare function dedent(templ: TemplateStringsArray | string, ...values: unknown[]): string; 2 | export default dedent; 3 | -------------------------------------------------------------------------------- /esm/index.js: -------------------------------------------------------------------------------- 1 | export function dedent(templ) { 2 | var values = []; 3 | for (var _i = 1; _i < arguments.length; _i++) { 4 | values[_i - 1] = arguments[_i]; 5 | } 6 | var strings = Array.from(typeof templ === 'string' ? [templ] : templ); 7 | strings[strings.length - 1] = strings[strings.length - 1].replace(/\r?\n([\t ]*)$/, ''); 8 | var indentLengths = strings.reduce(function (arr, str) { 9 | var matches = str.match(/\n([\t ]+|(?!\s).)/g); 10 | if (matches) { 11 | return arr.concat(matches.map(function (match) { var _a, _b; return (_b = (_a = match.match(/[\t ]/g)) === null || _a === void 0 ? void 0 : _a.length) !== null && _b !== void 0 ? _b : 0; })); 12 | } 13 | return arr; 14 | }, []); 15 | if (indentLengths.length) { 16 | var pattern_1 = new RegExp("\n[\t ]{" + Math.min.apply(Math, indentLengths) + "}", 'g'); 17 | strings = strings.map(function (str) { return str.replace(pattern_1, '\n'); }); 18 | } 19 | strings[0] = strings[0].replace(/^\r?\n/, ''); 20 | var string = strings[0]; 21 | values.forEach(function (value, i) { 22 | var endentations = string.match(/(?:^|\n)( *)$/); 23 | var endentation = endentations ? endentations[1] : ''; 24 | var indentedValue = value; 25 | if (typeof value === 'string' && value.includes('\n')) { 26 | indentedValue = String(value) 27 | .split('\n') 28 | .map(function (str, i) { 29 | return i === 0 ? str : "" + endentation + str; 30 | }) 31 | .join('\n'); 32 | } 33 | string += indentedValue + strings[i + 1]; 34 | }); 35 | return string; 36 | } 37 | export default dedent; 38 | //# sourceMappingURL=index.js.map -------------------------------------------------------------------------------- /esm/index.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,MAAM,UAAU,MAAM,CACpB,KAAoC;IACpC,gBAAoB;SAApB,UAAoB,EAApB,qBAAoB,EAApB,IAAoB;QAApB,+BAAoB;;IAEpB,IAAI,OAAO,GAAG,KAAK,CAAC,IAAI,CAAC,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;IAGtE,OAAO,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,GAAG,OAAO,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,OAAO,CAC/D,gBAAgB,EAChB,EAAE,CACH,CAAC;IAGF,IAAM,aAAa,GAAG,OAAO,CAAC,MAAM,CAAC,UAAC,GAAG,EAAE,GAAG;QAC5C,IAAM,OAAO,GAAG,GAAG,CAAC,KAAK,CAAC,qBAAqB,CAAC,CAAC;QACjD,IAAI,OAAO,EAAE;YACX,OAAO,GAAG,CAAC,MAAM,CACf,OAAO,CAAC,GAAG,CAAC,UAAC,KAAK,gBAAK,OAAA,MAAA,MAAA,KAAK,CAAC,KAAK,CAAC,QAAQ,CAAC,0CAAE,MAAM,mCAAI,CAAC,CAAA,EAAA,CAAC,CAC3D,CAAC;SACH;QACD,OAAO,GAAG,CAAC;IACb,CAAC,EAAY,EAAE,CAAC,CAAC;IAGjB,IAAI,aAAa,CAAC,MAAM,EAAE;QACxB,IAAM,SAAO,GAAG,IAAI,MAAM,CAAC,aAAW,IAAI,CAAC,GAAG,OAAR,IAAI,EAAQ,aAAa,OAAI,EAAE,GAAG,CAAC,CAAC;QAE1E,OAAO,GAAG,OAAO,CAAC,GAAG,CAAC,UAAC,GAAG,IAAK,OAAA,GAAG,CAAC,OAAO,CAAC,SAAO,EAAE,IAAI,CAAC,EAA1B,CAA0B,CAAC,CAAC;KAC5D;IAGD,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC;IAG9C,IAAI,MAAM,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;IAExB,MAAM,CAAC,OAAO,CAAC,UAAC,KAAK,EAAE,CAAC;QAEtB,IAAM,YAAY,GAAG,MAAM,CAAC,KAAK,CAAC,eAAe,CAAC,CAAA;QAClD,IAAM,WAAW,GAAG,YAAY,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAA;QACvD,IAAI,aAAa,GAAG,KAAK,CAAA;QAEzB,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE;YACrD,aAAa,GAAG,MAAM,CAAC,KAAK,CAAC;iBAC1B,KAAK,CAAC,IAAI,CAAC;iBACX,GAAG,CAAC,UAAC,GAAG,EAAE,CAAC;gBACV,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,KAAG,WAAW,GAAG,GAAK,CAAA;YAC/C,CAAC,CAAC;iBACD,IAAI,CAAC,IAAI,CAAC,CAAC;SACf;QAED,MAAM,IAAI,aAAa,GAAG,OAAO,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;IAC3C,CAAC,CAAC,CAAC;IAEH,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,eAAe,MAAM,CAAC"} -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ts-dedent", 3 | "version": "2.2.0", 4 | "description": "TypeScript package which smartly trims and strips indentation from multi-line strings", 5 | "author": "Tamino Martinius ", 6 | "main": "./dist/index.js", 7 | "module": "./esm/index.js", 8 | "jsnext:main": "./dist/index.js", 9 | "typings": "./dist/index.d.ts", 10 | "license": "MIT", 11 | "files": [ 12 | "dist", 13 | "esm", 14 | "src" 15 | ], 16 | "repository": { 17 | "type": "git", 18 | "url": "https://github.com/tamino-martinius/node-ts-dedent.git" 19 | }, 20 | "keywords": [ 21 | "dedent", 22 | "deindent", 23 | "indentation", 24 | "multi-line string", 25 | "multiline strings", 26 | "template literals", 27 | "template strings", 28 | "ts", 29 | "typescript", 30 | "es6", 31 | "harmony" 32 | ], 33 | "engines": { 34 | "node": ">=6.10" 35 | }, 36 | "scripts": { 37 | "coverage": "rm -rf coverage* && jest --coverage", 38 | "pretest": "npm run lint", 39 | "test": "jest", 40 | "lint": "eslint .", 41 | "watch": "tsc -w", 42 | "ci": "npm run coverage", 43 | "compile": "rm -rf dist/* && rm -rf esm/* && tsc --module commonjs --outdir dist && tsc --module es6 --outdir esm", 44 | "preversion": "npm run compile && git add ." 45 | }, 46 | "devDependencies": { 47 | "@types/jest": "^29.2.2", 48 | "@typescript-eslint/eslint-plugin": "^5.42.1", 49 | "@typescript-eslint/parser": "^5.42.1", 50 | "eslint": "^8.27.0", 51 | "jest": "^29.3.1", 52 | "ts-jest": "^29.0.3", 53 | "typescript": "~4.8.4" 54 | }, 55 | "jest": { 56 | "transform": { 57 | ".ts": "ts-jest" 58 | }, 59 | "testRegex": "\\.(test|spec)\\.ts$", 60 | "moduleFileExtensions": [ 61 | "ts", 62 | "tsx", 63 | "js", 64 | "json" 65 | ] 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/__tests__/index.spec.ts: -------------------------------------------------------------------------------- 1 | import { dedent } from '..'; 2 | 3 | function tag(strings: TemplateStringsArray, ...values: number[]) { 4 | let string = strings[0]; 5 | 6 | values.forEach((value, i) => { 7 | string += 2 * value + strings[i + 1]; 8 | }); 9 | 10 | return string; 11 | } 12 | 13 | describe('dedent tag', () => { 14 | it('should work with empty string', () => { 15 | expect(dedent``).toEqual(''); 16 | }); 17 | 18 | it('should work with tabs', () => { 19 | expect(dedent`Line #1 20 | Line #2 21 | Line #3`).toEqual('Line #1\nLine #2\nLine #3'); 22 | 23 | expect(dedent`Line #${1} 24 | Line #${2} 25 | Line #${3}`).toEqual('Line #1\nLine #2\nLine #3'); 26 | 27 | expect(dedent`${1}. line #${1} 28 | ${2}. line #${2} 29 | ${3}. line`).toEqual('1. line #1\n2. line #2\n3. line'); 30 | }); 31 | 32 | it('should work with spaces', () => { 33 | expect(dedent`Line #1 34 | Line #2 35 | Line #3`).toEqual('Line #1\nLine #2\nLine #3'); 36 | 37 | expect(dedent`Line #${1} 38 | Line #${2} 39 | Line #${3}`).toEqual('Line #1\nLine #2\nLine #3'); 40 | 41 | expect(dedent`${1}. line #${1} 42 | ${2}. line #${2} 43 | ${3}. line`).toEqual('1. line #1\n2. line #2\n3. line'); 44 | }); 45 | 46 | it('should remove leading/trailing line break', () => { 47 | expect( 48 | dedent` 49 | Line #1 50 | Line #2 51 | Line #3 52 | `, 53 | ).toEqual('Line #1\nLine #2\nLine #3'); 54 | 55 | expect( 56 | dedent` 57 | Line #1 58 | Line #2 59 | Line #3 60 | `, 61 | ).toEqual('Line #1\n\tLine #2\n\tLine #3'); 62 | 63 | expect( 64 | dedent` 65 | Line #${1} 66 | Line #${2} 67 | Line #${3} 68 | `, 69 | ).toEqual('Line #1\nLine #2\nLine #3'); 70 | 71 | expect( 72 | dedent` 73 | Line #${1} 74 | Line #${2} 75 | Line #${3} 76 | `, 77 | ).toEqual('Line #1\n\tLine #2\n\tLine #3'); 78 | 79 | expect( 80 | dedent` 81 | ${1}. line #${1} 82 | ${2}. line #${2} 83 | ${3}. line 84 | `, 85 | ).toEqual('1. line #1\n2. line #2\n3. line'); 86 | }); 87 | 88 | it('should not remove more than one leading/trailing line break', () => { 89 | expect( 90 | dedent` 91 | 92 | Line #1 93 | Line #2 94 | Line #3 95 | 96 | `, 97 | ).toEqual('\nLine #1\nLine #2\nLine #3\n'); 98 | 99 | expect( 100 | dedent` 101 | 102 | Line #${1} 103 | Line #${2} 104 | Line #${3} 105 | 106 | `, 107 | ).toEqual('\nLine #1\nLine #2\nLine #3\n'); 108 | 109 | expect( 110 | dedent` 111 | 112 | ${1}. line #${1} 113 | ${2}. line #${2} 114 | ${3}. line 115 | 116 | `, 117 | ).toEqual('\n1. line #1\n2. line #2\n3. line\n'); 118 | }); 119 | 120 | it('should remove the same number of tabs/spaces from each line', () => { 121 | expect( 122 | dedent` 123 | Line #1 124 | Line #2 125 | Line #3 126 | `, 127 | ).toEqual('Line #1\n\tLine #2\n\t\tLine #3'); 128 | 129 | expect( 130 | dedent` 131 | Line #${1} 132 | Line #${2} 133 | Line #${3} 134 | `, 135 | ).toEqual('Line #1\n\tLine #2\n\t\tLine #3'); 136 | 137 | expect( 138 | dedent` 139 | ${1}. line #${1} 140 | ${2}. line #${2} 141 | ${3}. line 142 | `, 143 | ).toEqual('1. line #1\n\t2. line #2\n\t\t3. line'); 144 | }); 145 | 146 | it("should ignore the last line if it doesn't contain anything else than whitespace", () => { 147 | expect( 148 | (() => { 149 | return dedent` 150 | Line #1 151 | Line #2 152 | Line #3 153 | `; 154 | })(), 155 | ).toEqual('Line #1\nLine #2\nLine #3'); 156 | 157 | expect( 158 | (() => { 159 | return dedent` 160 | Line #${1} 161 | Line #${2} 162 | Line #${3} 163 | `; 164 | })(), 165 | ).toEqual('Line #1\nLine #2\nLine #3'); 166 | 167 | expect( 168 | (() => { 169 | return dedent` 170 | ${1}. line #${1} 171 | ${2}. line #${2} 172 | ${3}. line 173 | `; 174 | })(), 175 | ).toEqual('1. line #1\n2. line #2\n3. line'); 176 | }); 177 | 178 | it('should process escape sequences', () => { 179 | expect( 180 | (() => { 181 | return dedent` 182 | \${not interpolated} 183 | \` 184 | `; 185 | })(), 186 | ).toEqual('${not interpolated}\n`'); 187 | }); 188 | 189 | it('should dedent nested dedents correctly', () => { 190 | const fieldDocs = dedent` 191 | * a 192 | * b 193 | * c 194 | `; 195 | 196 | const a = dedent` 197 | /** 198 | ${fieldIntro()} 199 | * 200 | ${fieldDocs} 201 | * 202 | ${fieldExample()} 203 | */ 204 | `; 205 | 206 | function fieldIntro() { 207 | return dedent` 208 | * 0 209 | `; 210 | } 211 | function fieldExample() { 212 | return dedent` 213 | * d 214 | `; 215 | } 216 | 217 | const expected = `/** 218 | * 0 219 | * 220 | * a 221 | * b 222 | * c 223 | * 224 | * d 225 | */`; 226 | 227 | expect(a).toEqual(expected); 228 | }); 229 | }); 230 | 231 | describe('dedent() function', () => { 232 | it('should work with tabs', () => { 233 | expect( 234 | dedent(`Line #1 235 | Line #2 236 | Line #3`), 237 | ).toEqual('Line #1\nLine #2\nLine #3'); 238 | 239 | expect( 240 | dedent(`Line #${1} 241 | Line #${2} 242 | Line #${3}`), 243 | ).toEqual('Line #1\nLine #2\nLine #3'); 244 | 245 | expect( 246 | dedent(`${1}. line #${1} 247 | ${2}. line #${2} 248 | ${3}. line`), 249 | ).toEqual('1. line #1\n2. line #2\n3. line'); 250 | }); 251 | 252 | it('should work with spaces', () => { 253 | expect( 254 | dedent(`Line #1 255 | Line #2 256 | Line #3`), 257 | ).toEqual('Line #1\nLine #2\nLine #3'); 258 | 259 | expect( 260 | dedent(`Line #${1} 261 | Line #${2} 262 | Line #${3}`), 263 | ).toEqual('Line #1\nLine #2\nLine #3'); 264 | 265 | expect( 266 | dedent(`${1}. line #${1} 267 | ${2}. line #${2} 268 | ${3}. line`), 269 | ).toEqual('1. line #1\n2. line #2\n3. line'); 270 | }); 271 | 272 | it('should remove leading/trailing line break', () => { 273 | expect( 274 | dedent(` 275 | Line #1 276 | Line #2 277 | Line #3 278 | `), 279 | ).toEqual('Line #1\nLine #2\nLine #3'); 280 | 281 | expect( 282 | dedent(` 283 | Line #1 284 | Line #2 285 | Line #3 286 | `), 287 | ).toEqual('Line #1\n\tLine #2\n\tLine #3'); 288 | 289 | expect( 290 | dedent(` 291 | Line #${1} 292 | Line #${2} 293 | Line #${3} 294 | `), 295 | ).toEqual('Line #1\nLine #2\nLine #3'); 296 | 297 | expect( 298 | dedent(` 299 | Line #${1} 300 | Line #${2} 301 | Line #${3} 302 | `), 303 | ).toEqual('Line #1\n\tLine #2\n\tLine #3'); 304 | 305 | expect( 306 | dedent(` 307 | ${1}. line #${1} 308 | ${2}. line #${2} 309 | ${3}. line 310 | `), 311 | ).toEqual('1. line #1\n2. line #2\n3. line'); 312 | }); 313 | 314 | it('should not remove more than one leading/trailing line break', () => { 315 | expect( 316 | dedent(` 317 | 318 | Line #1 319 | Line #2 320 | Line #3 321 | 322 | `), 323 | ).toEqual('\nLine #1\nLine #2\nLine #3\n'); 324 | 325 | expect( 326 | dedent(` 327 | 328 | Line #${1} 329 | Line #${2} 330 | Line #${3} 331 | 332 | `), 333 | ).toEqual('\nLine #1\nLine #2\nLine #3\n'); 334 | 335 | expect( 336 | dedent(` 337 | 338 | ${1}. line #${1} 339 | ${2}. line #${2} 340 | ${3}. line 341 | 342 | `), 343 | ).toEqual('\n1. line #1\n2. line #2\n3. line\n'); 344 | }); 345 | 346 | it('should remove the same number of tabs/spaces from each line', () => { 347 | expect( 348 | dedent(` 349 | Line #1 350 | Line #2 351 | Line #3 352 | `), 353 | ).toEqual('Line #1\n\tLine #2\n\t\tLine #3'); 354 | 355 | expect( 356 | dedent(` 357 | Line #${1} 358 | Line #${2} 359 | Line #${3} 360 | `), 361 | ).toEqual('Line #1\n\tLine #2\n\t\tLine #3'); 362 | 363 | expect( 364 | dedent(` 365 | ${1}. line #${1} 366 | ${2}. line #${2} 367 | ${3}. line 368 | `), 369 | ).toEqual('1. line #1\n\t2. line #2\n\t\t3. line'); 370 | }); 371 | 372 | it("should ignore the last line if it doesn't contain anything else than whitespace", () => { 373 | expect( 374 | (() => { 375 | return dedent(` 376 | Line #1 377 | Line #2 378 | Line #3 379 | `); 380 | })(), 381 | ).toEqual('Line #1\nLine #2\nLine #3'); 382 | 383 | expect( 384 | (() => { 385 | return dedent(` 386 | Line #${1} 387 | Line #${2} 388 | Line #${3} 389 | `); 390 | })(), 391 | ).toEqual('Line #1\nLine #2\nLine #3'); 392 | 393 | expect( 394 | (() => { 395 | return dedent(` 396 | ${1}. line #${1} 397 | ${2}. line #${2} 398 | ${3}. line 399 | `); 400 | })(), 401 | ).toEqual('1. line #1\n2. line #2\n3. line'); 402 | }); 403 | 404 | it('should process escape sequences', () => { 405 | expect( 406 | dedent(` 407 | \${not interpolated} 408 | \` 409 | `), 410 | ).toEqual('${not interpolated}\n`'); 411 | }); 412 | }); 413 | 414 | describe('dedent() function with custom tag', () => { 415 | it('should work with tabs', () => { 416 | expect( 417 | dedent(tag`Line #1 418 | Line #2 419 | Line #3`), 420 | ).toEqual('Line #1\nLine #2\nLine #3'); 421 | 422 | expect( 423 | dedent(tag`Line #${1} 424 | Line #${2} 425 | Line #${3}`), 426 | ).toEqual('Line #2\nLine #4\nLine #6'); 427 | 428 | expect( 429 | dedent(tag`${1}. line #${1} 430 | ${2}. line #${2} 431 | ${3}. line`), 432 | ).toEqual('2. line #2\n4. line #4\n6. line'); 433 | }); 434 | 435 | it('should work with spaces', () => { 436 | expect( 437 | dedent(tag`Line #1 438 | Line #2 439 | Line #3`), 440 | ).toEqual('Line #1\nLine #2\nLine #3'); 441 | 442 | expect( 443 | dedent(tag`Line #${1} 444 | Line #${2} 445 | Line #${3}`), 446 | ).toEqual('Line #2\nLine #4\nLine #6'); 447 | 448 | expect( 449 | dedent(tag`${1}. line #${1} 450 | ${2}. line #${2} 451 | ${3}. line`), 452 | ).toEqual('2. line #2\n4. line #4\n6. line'); 453 | }); 454 | 455 | it('should remove leading/trailing line break', () => { 456 | expect( 457 | dedent(tag` 458 | Line #1 459 | Line #2 460 | Line #3 461 | `), 462 | ).toEqual('Line #1\nLine #2\nLine #3'); 463 | 464 | expect( 465 | dedent(tag` 466 | Line #1 467 | Line #2 468 | Line #3 469 | `), 470 | ).toEqual('Line #1\n\tLine #2\n\tLine #3'); 471 | 472 | expect( 473 | dedent(tag` 474 | Line #${1} 475 | Line #${2} 476 | Line #${3} 477 | `), 478 | ).toEqual('Line #2\nLine #4\nLine #6'); 479 | 480 | expect( 481 | dedent(tag` 482 | Line #${1} 483 | Line #${2} 484 | Line #${3} 485 | `), 486 | ).toEqual('Line #2\n\tLine #4\n\tLine #6'); 487 | 488 | expect( 489 | dedent(tag` 490 | ${1}. line #${1} 491 | ${2}. line #${2} 492 | ${3}. line 493 | `), 494 | ).toEqual('2. line #2\n4. line #4\n6. line'); 495 | }); 496 | 497 | it('should not remove more than one leading/trailing line break', () => { 498 | expect( 499 | dedent(tag` 500 | 501 | Line #1 502 | Line #2 503 | Line #3 504 | 505 | `), 506 | ).toEqual('\nLine #1\nLine #2\nLine #3\n'); 507 | 508 | expect( 509 | dedent(tag` 510 | 511 | Line #${1} 512 | Line #${2} 513 | Line #${3} 514 | 515 | `), 516 | ).toEqual('\nLine #2\nLine #4\nLine #6\n'); 517 | 518 | expect( 519 | dedent(tag` 520 | 521 | ${1}. line #${1} 522 | ${2}. line #${2} 523 | ${3}. line 524 | 525 | `), 526 | ).toEqual('\n2. line #2\n4. line #4\n6. line\n'); 527 | }); 528 | 529 | it('should remove the same number of tabs/spaces from each line', () => { 530 | expect( 531 | dedent(tag` 532 | Line #1 533 | Line #2 534 | Line #3 535 | `), 536 | ).toEqual('Line #1\n\tLine #2\n\t\tLine #3'); 537 | 538 | expect( 539 | dedent(tag` 540 | Line #${1} 541 | Line #${2} 542 | Line #${3} 543 | `), 544 | ).toEqual('Line #2\n\tLine #4\n\t\tLine #6'); 545 | 546 | expect( 547 | dedent(tag` 548 | ${1}. line #${1} 549 | ${2}. line #${2} 550 | ${3}. line 551 | `), 552 | ).toEqual('2. line #2\n\t4. line #4\n\t\t6. line'); 553 | }); 554 | 555 | it("should ignore the last line if it doesn't contain anything else than whitespace", () => { 556 | expect( 557 | (() => { 558 | return dedent(tag` 559 | Line #1 560 | Line #2 561 | Line #3 562 | `); 563 | })(), 564 | ).toEqual('Line #1\nLine #2\nLine #3'); 565 | 566 | expect( 567 | (() => { 568 | return dedent(tag` 569 | Line #${1} 570 | Line #${2} 571 | Line #${3} 572 | `); 573 | })(), 574 | ).toEqual('Line #2\nLine #4\nLine #6'); 575 | 576 | expect( 577 | (() => { 578 | return dedent(tag` 579 | ${1}. line #${1} 580 | ${2}. line #${2} 581 | ${3}. line 582 | `); 583 | })(), 584 | ).toEqual('2. line #2\n4. line #4\n6. line'); 585 | }); 586 | 587 | it('should process escape sequences', () => { 588 | expect( 589 | dedent(tag` 590 | \${not interpolated} 591 | \` 592 | `), 593 | ).toEqual('${not interpolated}\n`'); 594 | }); 595 | }); 596 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export function dedent( 2 | templ: TemplateStringsArray | string, 3 | ...values: unknown[] 4 | ): string { 5 | let strings = Array.from(typeof templ === 'string' ? [templ] : templ); 6 | 7 | // 1. Remove trailing whitespace. 8 | strings[strings.length - 1] = strings[strings.length - 1].replace( 9 | /\r?\n([\t ]*)$/, 10 | '', 11 | ); 12 | 13 | // 2. Find all line breaks to determine the highest common indentation level. 14 | const indentLengths = strings.reduce((arr, str) => { 15 | const matches = str.match(/\n([\t ]+|(?!\s).)/g); 16 | if (matches) { 17 | return arr.concat( 18 | matches.map((match) => match.match(/[\t ]/g)?.length ?? 0), 19 | ); 20 | } 21 | return arr; 22 | }, []); 23 | 24 | // 3. Remove the common indentation from all strings. 25 | if (indentLengths.length) { 26 | const pattern = new RegExp(`\n[\t ]{${Math.min(...indentLengths)}}`, 'g'); 27 | 28 | strings = strings.map((str) => str.replace(pattern, '\n')); 29 | } 30 | 31 | // 4. Remove leading whitespace. 32 | strings[0] = strings[0].replace(/^\r?\n/, ''); 33 | 34 | // 5. Perform interpolation. 35 | let string = strings[0]; 36 | 37 | values.forEach((value, i) => { 38 | // 5.1 Read current indentation level 39 | const endentations = string.match(/(?:^|\n)( *)$/) 40 | const endentation = endentations ? endentations[1] : '' 41 | let indentedValue = value 42 | // 5.2 Add indentation to values with multiline strings 43 | if (typeof value === 'string' && value.includes('\n')) { 44 | indentedValue = String(value) 45 | .split('\n') 46 | .map((str, i) => { 47 | return i === 0 ? str : `${endentation}${str}` 48 | }) 49 | .join('\n'); 50 | } 51 | 52 | string += indentedValue + strings[i + 1]; 53 | }); 54 | 55 | return string; 56 | } 57 | 58 | export default dedent; 59 | -------------------------------------------------------------------------------- /tsconfig.jest.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig", 3 | "compilerOptions": { 4 | "module": "commonjs" 5 | }, 6 | "globals": { 7 | "ts-jest": { 8 | "enableTsDiagnostics": true 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "es6", 5 | "moduleResolution": "node", 6 | "sourceMap": true, 7 | "declaration": true, 8 | "rootDir": "src", 9 | "outDir": "esm", 10 | "allowSyntheticDefaultImports": true, 11 | "removeComments": true, 12 | "noImplicitAny": true, 13 | "noImplicitReturns": true, 14 | "noFallthroughCasesInSwitch": true, 15 | "strictNullChecks": true, 16 | "noUnusedLocals": true, 17 | "noUnusedParameters": true, 18 | "experimentalDecorators": true 19 | }, 20 | "files": [ 21 | "node_modules/typescript/lib/lib.es2015.d.ts", 22 | "node_modules/typescript/lib/lib.dom.d.ts", 23 | "src/index.ts" 24 | ], 25 | "include": [ 26 | "*.ts", 27 | "**/*.ts" 28 | ], 29 | "exclude": [ 30 | "node_modules", 31 | "*/__tests__", 32 | "*/__mocks__", 33 | "dist" 34 | ], 35 | "jest": { 36 | "globals": { 37 | "ts-jest": { 38 | "tsConfigFile": "tsconfig.jest.json" 39 | } 40 | } 41 | } 42 | } 43 | --------------------------------------------------------------------------------