├── .gitignore ├── docs └── assets │ ├── screenshot.gif │ └── icon-128x128.png ├── .editorconfig ├── .vscodeignore ├── tsconfig.json ├── src ├── utils │ ├── extract-tables.ts │ └── format-table.ts ├── test │ ├── extension.test.ts │ ├── runTest.ts │ ├── index.ts │ └── utils │ │ ├── extract-table.test.ts │ │ └── format-table.test.ts └── extension.ts ├── CHANGELOG.md ├── .vscode ├── settings.json ├── launch.json └── tasks.json ├── README.md ├── LICENSE.md └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | out 2 | node_modules 3 | 4 | npm-debug.log* 5 | .vscode-test 6 | -------------------------------------------------------------------------------- /docs/assets/screenshot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/josa42/vscode-markdown-table-formatter/HEAD/docs/assets/screenshot.gif -------------------------------------------------------------------------------- /docs/assets/icon-128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/josa42/vscode-markdown-table-formatter/HEAD/docs/assets/icon-128x128.png -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | indent_style = space 7 | charset = utf-8 8 | indent_size = 2 9 | -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | .vscode-test/** 3 | typings/** 4 | out/test/** 5 | test/** 6 | src/** 7 | **/*.map 8 | .editorconfig 9 | .gitignore 10 | tsconfig.json 11 | *.vsix 12 | doc 13 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es6", 5 | "outDir": "out", 6 | "sourceMap": true, 7 | "rootDir": "src", 8 | "strict": true 9 | }, 10 | "exclude": ["node_modules", ".vscode-test"] 11 | 12 | } 13 | -------------------------------------------------------------------------------- /src/utils/extract-tables.ts: -------------------------------------------------------------------------------- 1 | const TABLE_EXP = /((?:(?:[^\n]*?\|[^\n]*)\ *)?(?:\r?\n|^))((?:\|\ *(?::?-+:?|::)\ *|\|?(?:\ *(?::?-+:?|::)\ *\|)+)(?:\ *(?::?-+:?|::)\ *)?\ *\r?\n)((?:(?:[^\n]*?\|[^\n]*)\ *(?:\r?\n|$))+)/g; 2 | 3 | export default function extractTables(text: string): string[] { 4 | return (text.match(TABLE_EXP) || []).map(s => s.replace(/\n+$/, '')); 5 | } 6 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | 4 | ## 0.2.0 5 | 6 | - Add configuration option to disable the formatter 7 | 8 | ## 0.1.0 9 | 10 | - Support multiple table formatting 11 | 12 | ## 0.0.4 13 | 14 | - Fix readme screenshot 15 | 16 | ## 0.0.3 17 | 18 | - Fix formatting if no table is found 19 | 20 | ## 0.0.2 21 | 22 | - Fix dependencies 23 | 24 | ## 0.0.1 25 | 26 | - Initial release 27 | -------------------------------------------------------------------------------- /src/test/extension.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import { before } from 'mocha'; 3 | 4 | import * as vscode from 'vscode'; 5 | 6 | describe('Extension', () => { 7 | 8 | before(() => { 9 | vscode.window.showInformationMessage('Start all tests.'); 10 | }); 11 | 12 | it('should do somethign', () => { 13 | assert.equal([1, 2, 3].indexOf(5), -1); 14 | assert.equal([1, 2, 3].indexOf(0), -1); 15 | }); 16 | 17 | }); 18 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "files.exclude": { 4 | "out": true, 5 | "node_modules": true 6 | }, 7 | "search.exclude": { 8 | "out": true, 9 | "node_modules": true 10 | }, 11 | "typescript.tsdk": "./node_modules/typescript/lib", 12 | "eslint.enable": false, 13 | "jshint.enable": false, 14 | "jscs.enable": false, 15 | "tslint.enable": false // we want to use the TS server from our node_modules folder to control its version 16 | } 17 | -------------------------------------------------------------------------------- /src/test/runTest.ts: -------------------------------------------------------------------------------- 1 | 2 | import * as path from 'path'; 3 | 4 | import { runTests } from 'vscode-test'; 5 | 6 | async function main() { 7 | try { 8 | // The folder containing the Extension Manifest package.json 9 | // Passed to `--extensionDevelopmentPath` 10 | const extensionDevelopmentPath = path.resolve(__dirname, '../../'); 11 | 12 | // The path to the extension test script 13 | // Passed to --extensionTestsPath 14 | const extensionTestsPath = path.resolve(__dirname, './index'); 15 | 16 | // Download VS Code, unzip it and run the integration test 17 | await runTests({ extensionDevelopmentPath, extensionTestsPath }); 18 | } catch (err) { 19 | console.error('Failed to run tests'); 20 | process.exit(1); 21 | } 22 | } 23 | 24 | main(); 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # markdown-table-formatter 2 | 3 | Format markdown tables. 4 | 5 | ![Project: Not maintained](https://img.shields.io/badge/Project-Not_maintained-red.svg) 6 | 7 | --- 8 | 9 | **Note: the repository is not maintained. If you would like to take over, please open an issue!** 10 | 11 | --- 12 | 13 | ![](https://rawgithub.com/josa42/vscode-markdown-table-formatter/master/docs/assets/screenshot.gif) 14 | 15 | ## Usage 16 | 17 | Run the command `Format Document` or press `alt+shift+f`. 18 | 19 | ## Configuration 20 | 21 | Enable/disable Markdown Table Formatter. 22 | 23 | ``` 24 | { 25 | "markdownTableFormatter.enable": true 26 | } 27 | ``` 28 | 29 | ## License 30 | 31 | See: [LICENSE.md](https://github.com/josa42/vscode-markdown-table-formatter/blob/master/LICENSE.md) 32 | -------------------------------------------------------------------------------- /src/test/index.ts: -------------------------------------------------------------------------------- 1 | 2 | import * as path from 'path'; 3 | import * as Mocha from 'mocha'; 4 | import * as glob from 'glob'; 5 | 6 | export function run(): Promise { 7 | // Create the mocha test 8 | const mocha = new Mocha(); 9 | mocha.useColors(true); 10 | 11 | const testsRoot = path.resolve(__dirname, '..'); 12 | 13 | return new Promise((c, e) => { 14 | glob('**/**.test.js', { cwd: testsRoot }, (err, files) => { 15 | if (err) { 16 | return e(err); 17 | } 18 | 19 | // Add files to the test suite 20 | files.forEach(f => mocha.addFile(path.resolve(testsRoot, f))); 21 | 22 | try { 23 | // Run the mocha test 24 | mocha.run(failures => { 25 | if (failures > 0) { 26 | e(new Error(`${failures} tests failed.`)); 27 | } else { 28 | c(); 29 | } 30 | }); 31 | } catch (err) { 32 | e(err); 33 | } 34 | }); 35 | }); 36 | } 37 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that compiles the extension and then opens it inside a new window 2 | { 3 | "version": "0.1.0", 4 | "configurations": [{ 5 | "name": "Launch Extension", 6 | "type": "extensionHost", 7 | "request": "launch", 8 | "runtimeExecutable": "${execPath}", 9 | "args": ["--extensionDevelopmentPath=${workspaceRoot}"], 10 | "stopOnEntry": false, 11 | "sourceMaps": true, 12 | "outFiles": ["${workspaceRoot}/out/src/**/*.js"], 13 | "preLaunchTask": "npm" 14 | }, { 15 | "name": "Launch Tests", 16 | "type": "extensionHost", 17 | "request": "launch", 18 | "runtimeExecutable": "${execPath}", 19 | "args": ["--extensionDevelopmentPath=${workspaceRoot}", "--extensionTestsPath=${workspaceRoot}/out/test"], 20 | "stopOnEntry": false, 21 | "sourceMaps": true, 22 | "outFiles": ["${workspaceRoot}/out/test/**/*.js"], 23 | "preLaunchTask": "npm" 24 | }] 25 | } 26 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 Josa Gesell 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // Available variables which can be used inside of strings. 2 | // ${workspaceRoot}: the root folder of the team 3 | // ${file}: the current opened file 4 | // ${fileBasename}: the current opened file's basename 5 | // ${fileDirname}: the current opened file's dirname 6 | // ${fileExtname}: the current opened file's extension 7 | // ${cwd}: the current working directory of the spawned process 8 | 9 | // A task runner that calls a custom npm script that compiles the extension. 10 | { 11 | "version": "0.1.0", 12 | 13 | // we want to run npm 14 | "command": "npm", 15 | 16 | // the command is a shell script 17 | "isShellCommand": true, 18 | 19 | // show the output window only if unrecognized errors occur. 20 | "showOutput": "silent", 21 | 22 | // we run the custom script "compile" as defined in package.json 23 | "args": ["run", "compile", "--loglevel", "silent"], 24 | 25 | // The tsc compiler is started in watching mode 26 | "isBackground": true, 27 | 28 | // use the standard tsc in watch mode problem matcher to find compile problems in the output. 29 | "problemMatcher": "$tsc-watch" 30 | } 31 | -------------------------------------------------------------------------------- /src/test/utils/extract-table.test.ts: -------------------------------------------------------------------------------- 1 | 2 | import * as assert from 'assert'; 3 | import extractTables from '../../utils/extract-tables'; 4 | 5 | 6 | describe('extractTables()', () => { 7 | 8 | it('should extract a markdown table', () => { 9 | const table = [ 10 | '| Header 1 | Header 2 | Header 3|H|', 11 | '| --- | --- | :---: | :---: |', 12 | '| aaa |bbb| cccc | ddddd |', 13 | ' | eee |fff', 14 | '| | | eee |fff', 15 | '| | | |' 16 | ].join('\n') 17 | 18 | assert.deepEqual(extractTables(table), [table]) 19 | }); 20 | 21 | it('should extract multiple large markdown tables', () => { 22 | const rows = 10_000 23 | const columns = 200 24 | 25 | const table = [ 26 | '| Header '.repeat(columns) + '|', 27 | '|:-:'.repeat(columns) + '|', 28 | ...Array(rows).fill('| Foo '.repeat(columns) + '|'), 29 | ].join('\n') 30 | 31 | const input = [ 32 | '# Header', 33 | '', 34 | table, 35 | '', 36 | table 37 | ].join("\n") 38 | 39 | assert.deepEqual(extractTables(input), [table, table]) 40 | }); 41 | 42 | }); 43 | -------------------------------------------------------------------------------- /src/extension.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | // The module 'vscode' contains the VS Code extensibility API 3 | // Import the module and reference it with the alias vscode in your code below 4 | import * as vscode from 'vscode'; 5 | import {workspace} from 'vscode'; 6 | import extractTables from './utils/extract-tables' 7 | 8 | import formatTable from './utils/format-table' 9 | import * as escapeStringRegexp from 'escape-string-regexp' 10 | 11 | let config = workspace.getConfiguration('markdownTableFormatter'); 12 | let enable: boolean = config.get('enable', true); 13 | 14 | workspace.onDidChangeConfiguration(e => { 15 | config = workspace.getConfiguration('markdownTableFormatter'); 16 | enable = config.get('enable', true); 17 | }); 18 | 19 | export function activate(context: vscode.ExtensionContext) { 20 | 21 | context.subscriptions.push(vscode.languages.registerDocumentFormattingEditProvider('markdown', { 22 | provideDocumentFormattingEdits(document, options, token) { 23 | 24 | if (!enable) { 25 | return 26 | } 27 | 28 | const result: vscode.TextEdit[] = []; 29 | 30 | const start = new vscode.Position(0, 0); 31 | const end = new vscode.Position(document.lineCount - 1, document.lineAt(document.lineCount - 1).text.length); 32 | const range = new vscode.Range(start, end); 33 | 34 | let text = document.getText(range) 35 | 36 | const tables = extractTables(text) 37 | if (tables) { 38 | tables.forEach((table) => { 39 | var re = new RegExp(escapeStringRegexp(String(table)), 'g') 40 | text = text.replace(re, (substring: string) => formatTable(table)) 41 | }) 42 | result.push(new vscode.TextEdit(range, text)); 43 | } 44 | 45 | return result; 46 | } 47 | })) 48 | } 49 | 50 | export function deactivate() {} 51 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "markdown-table-formatter", 3 | "displayName": "Markdown Table Formatter", 4 | "description": "", 5 | "version": "0.3.0", 6 | "publisher": "josa", 7 | "license": "MIT", 8 | "homepage": "https://github.com/josa42/vscode-markdown-table-formatter", 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/josa42/vscode-markdown-table-formatter.git" 12 | }, 13 | "bugs": { 14 | "url": "https://github.com/josa42/vscode-markdown-table-formatter/issues" 15 | }, 16 | "engines": { 17 | "vscode": "^1.5.0" 18 | }, 19 | "categories": [ 20 | "Other", 21 | "Formatters" 22 | ], 23 | "icon": "docs/assets/icon-128x128.png", 24 | "galleryBanner": { 25 | "color": "#FFFFFF", 26 | "theme": "light" 27 | }, 28 | "activationEvents": [ 29 | "onLanguage:markdown" 30 | ], 31 | "main": "./out/extension.js", 32 | "contributes": { 33 | "configuration": { 34 | "type": "object", 35 | "title": "Markdown Table Formatter", 36 | "properties": { 37 | "markdownTableFormatter.enable": { 38 | "type": "boolean", 39 | "default": true, 40 | "description": "Enable/disable Markdown Table Formatter." 41 | } 42 | } 43 | } 44 | }, 45 | "scripts": { 46 | "vscode:prepublish": "npm run compile", 47 | "compile": "tsc -p ./", 48 | "lint": "tslint -p ./", 49 | "watch": "tsc -watch -p ./", 50 | "pretest": "npm run compile", 51 | "test": "node ./out/test/runTest.js" 52 | }, 53 | "devDependencies": { 54 | "@types/glob": "^7.1.1", 55 | "@types/mocha": "^5.2.7", 56 | "@types/node": "^12.6.9", 57 | "glob": "^7.1.4", 58 | "mocha": "^6.2.0", 59 | "typescript": "^3.5.3", 60 | "vscode": "^1.1.0", 61 | "vscode-test": "^1.1.0" 62 | }, 63 | "dependencies": { 64 | "escape-string-regexp": "^2.0.0" 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/test/utils/format-table.test.ts: -------------------------------------------------------------------------------- 1 | 2 | import * as assert from 'assert'; 3 | import formatTable from '../../utils/format-table'; 4 | 5 | 6 | describe('format-table', () => { 7 | 8 | it('should format a markdown table', () => { 9 | const input = [ 10 | '| Header 1 | Header 2 | Header 3|H|', 11 | '| --- | --- | :---: | :---: |', 12 | '| aaa |bbb| cccc | ddddd |', 13 | ' | eee |fff', 14 | '| | | eee |fff', 15 | '| | | |' 16 | ].join('\n') 17 | 18 | const output = [ 19 | '| Header 1 | Header 2 | Header 3 | H |', 20 | '|----------|----------|:--------:|:-----:|', 21 | '| aaa | bbb | cccc | ddddd |', 22 | '| eee | fff | | |', 23 | '| | | eee | fff |', 24 | '| | | | |' 25 | ].join('\n'); 26 | 27 | assert.deepEqual(formatTable(input), output) 28 | }); 29 | 30 | it('should format a large markdown table', () => { 31 | const rows = 10_000 32 | const colums = 200 33 | 34 | const input = [ 35 | '| Header '.repeat(colums) + '|', 36 | '|:-:'.repeat(colums) + '|', 37 | ...Array(rows).fill('| Foo '.repeat(colums) + '|') 38 | ].join('\n') 39 | 40 | const output = [ 41 | '| Header '.repeat(colums) + '|', 42 | '|:------:'.repeat(colums) + '|', 43 | ...Array(rows).fill('| Foo '.repeat(colums) + '|') 44 | ].join('\n') 45 | 46 | assert.deepEqual(formatTable(input), output) 47 | }); 48 | 49 | it('should format tables correct with empty first column (issue: #6)', () => { 50 | const input = [ 51 | '| Supported in following Version | Minimum Version Supported |', 52 | '|------------------------------------------|---------------------------|', 53 | '| | Win32/64 (inc. Windows 7, Windows 8 Pro) |', 54 | '| | Android 4.1 |' 55 | ].join('\n') 56 | 57 | const output = [ 58 | '| Supported in following Version | Minimum Version Supported |', 59 | '|--------------------------------|------------------------------------------|', 60 | '| | Win32/64 (inc. Windows 7, Windows 8 Pro) |', 61 | '| | Android 4.1 |' 62 | ].join('\n') 63 | 64 | 65 | assert.deepEqual(formatTable(input), output) 66 | }); 67 | 68 | it('should format tables correct with empty first column (issue: #7)', () => { 69 | const input = [ 70 | '| Parameter | Type | Default | Required | Description |', 71 | '|-----------|----------------------------------------|-------------|----------|----------------------------------------------------------------|', 72 | '| title | `string | null | void` | | Yes | Array of strings that are offered as autocomplete suggestions. |', 73 | '| content | `string | null | void` | | Yes | A callback that gets |' 74 | ].join('\n') 75 | 76 | const output = [ 77 | '| Parameter | Type | Default | Required | Description |', 78 | '|-----------|------------------------|---------|----------|----------------------------------------------------------------|', 79 | '| title | `string | null | void` | | Yes | Array of strings that are offered as autocomplete suggestions. |', 80 | '| content | `string | null | void` | | Yes | A callback that gets |' 81 | ].join('\n') 82 | 83 | assert.deepEqual(formatTable(input), output) 84 | }); 85 | }); 86 | 87 | -------------------------------------------------------------------------------- /src/utils/format-table.ts: -------------------------------------------------------------------------------- 1 | // Taken from: https://github.com/dbrockman/reformat-markdown-table 2 | 3 | const ROW_IDX_HEADER = 0 4 | const ROW_IDX_ALIGNMENT = 1 5 | 6 | export default function formatTable(str: string): string { 7 | let table = splitStringToTable(str) 8 | 9 | table = fillInMissingColumns(table); 10 | 11 | table[ROW_IDX_ALIGNMENT] = table[ROW_IDX_ALIGNMENT].map((cell) => { 12 | return padHeaderSeparatorString(cell, 0); 13 | }); 14 | 15 | const alignments = table[ROW_IDX_ALIGNMENT].map(getAlignment); 16 | const max_length_per_column = getMaxLengthPerColumn(table); 17 | 18 | return table.map((row: string[], row_index: number) => { 19 | return '|' + row.map((cell, column_index) => { 20 | var column_length = max_length_per_column[column_index]; 21 | if (row_index === 1) { 22 | // Alignment, e.g.: ":------:" 23 | return padHeaderSeparatorString(cell, column_length + 2); 24 | } 25 | 26 | return ' ' + padStringWithAlignment(cell, column_length, alignments[column_index]) + ' '; 27 | }).join('|') + '|'; 28 | }).join('\n'); 29 | } 30 | 31 | function splitStringToTable(str: string): string[][] { 32 | return str.trim().split('\n') 33 | // trim space and "|", but respect empty first column 34 | // E.g. "| | Test a | Test b |" 35 | // => "| Test a | Test b" 36 | .map((row) => row.replace(/^(\s*\|\s*|\s+)/, '').replace(/[\|\s]+$/, '')) 37 | // Split rows into columns 38 | .map((row) => { 39 | 40 | let inCode = false 41 | 42 | return row.split('') 43 | // Split by "|", but only if not inside inline-code 44 | // E.g. "| Command | `ls | grep foo` |" 45 | // => [ "Command","`ls | grep foo`" ] 46 | .reduce((columns, c): string[] => { 47 | if (c === '`') { 48 | // Switch mode 49 | inCode = !inCode 50 | } 51 | 52 | if (c === '|' && !inCode) { 53 | // Add new Column 54 | columns.push('') 55 | 56 | } else { 57 | // Append char to current column 58 | columns[columns.length - 1] += c 59 | } 60 | 61 | return columns 62 | }, ['']) 63 | // Trim space in columns 64 | .map(column => column.trim()) 65 | }) 66 | } 67 | 68 | function getMaxLengthPerColumn(table: string[][]): number[] { 69 | return table[ROW_IDX_HEADER].map((_: string, column_index: number) => { 70 | return getColumn(table, column_index).reduce((max, item) => { 71 | return Math.max(max, item.length); 72 | }, 0) 73 | }); 74 | } 75 | 76 | function padHeaderSeparatorString(str: string, len: number): string { 77 | switch (getAlignment(str)) { 78 | case 'c': return ':' + '-'.repeat(Math.max(1, len - 2)) + ':'; 79 | case 'r': return '-'.repeat(Math.max(1, len - 1)) + ':'; 80 | case 'l': 81 | default: return '-'.repeat(Math.max(1, len)); 82 | } 83 | } 84 | 85 | function getAlignment(str: string): string { 86 | if (str.endsWith(':')) { 87 | if (str.startsWith(':')) { 88 | return 'c' 89 | } 90 | 91 | return 'r'; 92 | } 93 | 94 | return 'l'; 95 | 96 | } 97 | 98 | function fillInMissingColumns(table: string[][]): string[][] { 99 | var max = table.reduce((max, item) => Math.max(max, item.length), 0); 100 | 101 | return table.map((row) => row.concat(Array(max - row.length).fill(''))); 102 | } 103 | 104 | function getColumn(table: string[][], column_index: number): string[] { 105 | return table.map((row) => row[column_index]); 106 | } 107 | 108 | function padStringWithAlignment(str: string, len: number, alignment: string): string { 109 | switch (alignment) { 110 | case 'c': return padLeftAndRight(str, len); 111 | case 'r': return str.padStart(len); 112 | case 'l': 113 | default: return str.padEnd(len); 114 | } 115 | } 116 | 117 | function padLeftAndRight(str: string, len: number): string { 118 | const l = (len - str.length) / 2; 119 | return ' '.repeat(Math.ceil(l)) + str + ' '.repeat(Math.floor(l)); 120 | } 121 | 122 | --------------------------------------------------------------------------------