├── .github └── stale.yml ├── .gitignore ├── .prettierrc.yaml ├── .travis.yml ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── keymaps └── markdown-table-formatter.cson ├── lib ├── config.js ├── formatTable.js ├── markdown-table-formatter.js ├── regex.js └── table-formatter.js ├── menus └── markdown-table-formatter.cson ├── package.json ├── settings.png ├── spec ├── atom.d.ts ├── duplicate-tests.spec.ts ├── editor.spec.ts ├── fixtures │ ├── empty.md │ ├── empty.text │ ├── test-tables-pref-line-len.md │ └── test-tables.md ├── format.spec.ts ├── global.d.ts ├── markdown-table-formatter.spec.ts ├── regex.spec.ts ├── tables │ ├── non-tables.ts │ └── test-tables-default.ts ├── tsconfig.json ├── tslint.json └── utils.ts ├── src ├── atom.d.ts ├── config.ts ├── formatTable.ts ├── markdown-table-formatter.ts ├── regex.ts └── table-formatter.ts ├── tsconfig.json └── tslint.json /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 90 3 | # Number of days of inactivity before a stale issue is closed 4 | daysUntilClose: 7 5 | # Issues with these labels will never be considered stale 6 | exemptLabels: 7 | - pinned 8 | - security 9 | - enhancement 10 | # Label to use when marking an issue as stale 11 | staleLabel: stale 12 | # Comment to post when marking an issue as stale. Set to `false` to disable 13 | markComment: > 14 | This issue has been automatically marked as stale because it has not had 15 | recent activity. It will be closed if no further activity occurs. Thank you 16 | for your contributions. 17 | # Comment to post when closing a stale issue. Set to `false` to disable 18 | closeComment: false 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | npm-debug.log 3 | node_modules 4 | -------------------------------------------------------------------------------- /.prettierrc.yaml: -------------------------------------------------------------------------------- 1 | # .prettierrc 2 | semi: false 3 | singleQuote: true 4 | trailingComma: 'all' 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | ### Project specific config ### 2 | language: generic 3 | 4 | env: 5 | global: 6 | - APM_TEST_PACKAGES="" 7 | - ATOM_LINT_WITH_BUNDLED_NODE="true" 8 | 9 | matrix: 10 | - ATOM_CHANNEL=stable 11 | # - ATOM_CHANNEL=beta 12 | 13 | os: 14 | - linux 15 | # - osx 16 | 17 | ### Generic setup follows ### 18 | script: 19 | - curl -s -O https://raw.githubusercontent.com/atom/ci/master/build-package.sh 20 | - chmod u+x build-package.sh 21 | - ./build-package.sh 22 | - npm test 23 | 24 | notifications: 25 | email: 26 | on_success: never 27 | on_failure: change 28 | 29 | branches: 30 | only: 31 | - master 32 | 33 | git: 34 | depth: 10 35 | 36 | sudo: false 37 | 38 | dist: trusty 39 | 40 | addons: 41 | apt: 42 | packages: 43 | - build-essential 44 | - fakeroot 45 | - git 46 | - libsecret-1-dev 47 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 2.9.2 2 | * Fix #26 (CRLF breaks whole-document formatting) 3 | * Add tests for CRLF (#26) 4 | 5 | ## 2.9.1 6 | * [Fix] #44 - Events not triggering 7 | 8 | ## 2.9.0 9 | * [Fix] Do not EVER contract selection ranges 10 | * [Change] Format selection for non-Markdown scopes 11 | * [New] Command to add current grammar to markdownGrammarScopes 12 | 13 | ## 2.8.4 14 | * Added limitLastColumnPadding setting to generate less padding for tables where the last column runs long 15 | 16 | ## 2.8.3 17 | * [Fix] #30 regression 18 | 19 | ## 2.8.2 20 | * [Fix] \_this.getModel is not a function 21 | 22 | ## 2.8.1 23 | * [Change] Safeguard in cases that the editor was not instantiated 24 | 25 | ## 2.8.0 26 | * [New] Added the option to choose in which grammars the plugin run 27 | * [Fix] Changed if test to the check scope names 28 | * [Change] Updated settings image 29 | 30 | ## 2.7.2 31 | * Bugfixes (closes #23, #24) 32 | 33 | ## 2.7.1 34 | * Bugfix for misinterpretation of default table Justification 35 | 36 | ## 2.6.0 37 | * Added the option to set the default justification for tables that have only a \'-\' on the formatting line 38 | 39 | ## 2.5.5 40 | * Added some usage information to the README 41 | 42 | ## 2.5.4 43 | * Tests code cleanup 44 | * Raise minimal compatible Atom version to 1.0.0 45 | * Wrap all changes into a single undo transaction 46 | 47 | ## 2.5.3 48 | * Ignore common zero-width unicode characters for cell width (#17) 49 | 50 | ## 2.5.2 51 | * Bogus release (no changes) 52 | 53 | ## 2.5.1 54 | * Fix #16 55 | 56 | ## 2.5.0 57 | 58 | * Removed unnecessary comments 59 | * Code format 60 | * Add possible EOF at table body end will allow for capturing tables that are at end of file w/o trailing newline 61 | * Extra newline removed 62 | * Fix typo 63 | * Add testcases for Chinese and Russian tables 64 | * Use wcwidth 65 | * Small regex format update 66 | * Extend selections to whole tables 67 | * Only auto-select document if all ranges are empty 68 | * Only extend ranges when not empty or not ASED (auto-select entire document 69 | 70 | 71 | ## 2.4.2 72 | * Update specs to check more cases 73 | * Put all extra content into last cell 74 | * Correct CJK character width counting 75 | 76 | 77 | ## 2.4.1 78 | 79 | * Fix #11 80 | * Settings are now handled via fields to facilitate easier testing 81 | * Update specs 82 | 83 | 84 | ## 2.4.0 85 | 86 | * Fixed stray `if` from #7 87 | 88 | I'm very sorry for this bug. I didn't tested because today was my first day at work after my vacation. Things were a little crazy. I promisse I'll be more careful from now on. 89 | 90 | 91 | ## 2.3.0 92 | 93 | * Merged changes from #7 and #8 94 | * **Now the package is ready for Atom API 1.0** 95 | 96 | 97 | ## 2.2.0 98 | 99 | * Merge branch 'lierdakil-newregex' lierdakil@08374da6c9ec2bb35d35bc666c0408929448fe37 100 | 101 | 102 | ## 2.1.1 103 | 104 | * Fix for bug #3 "Insertion Point Lost On Save" 105 | 106 | 107 | ## 2.1.0 108 | 109 | * Fix for keeping document auto selected 110 | 111 | 112 | ## 2.0.1 113 | 114 | * Fix a regression where a table wouldn't be recognized 115 | 116 | 117 | ## 2.0.0 118 | 119 | * Added option to choose if first and last pipes should be kept 120 | * Added option to choose the padding of the cells 121 | * Added support for latest config api 122 | * Added specs to package 123 | * Changed regex to identify pipeless tables 124 | * Changed requirements to atom >= 0.135.0 125 | * Removed deprecated APIs to observe text editors 126 | 127 | 128 | ## 1.0.2 129 | 130 | * Sorry to anyone using my package, had some troubles with git and remote tags. Hope this fixes everything. 131 | 132 | 133 | ## 1.0.1 134 | 135 | * Added option to restore selections (default: false) 136 | * Fixed incorrect trimming of selected tables 137 | * Auto-select entire document if selection is empty (default: true) 138 | * Added a fix in the regex to consider spaces at the end of each line 139 | 140 | 141 | ## 0.1.2 142 | 143 | * Bug fixes 144 | 145 | 146 | ## 0.1.1 147 | 148 | * Bug fixes 149 | 150 | 151 | ## 0.1.0 152 | 153 | * Every feature added 154 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Fernando Xavier de Freitas Crespo 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Markdown Table Formatter 2 | 3 | A simple markdown plugin to format tables. 4 | 5 | Based on the awesome [Improved Markdown table commands for TextMate](http://www.leancrew.com/all-this/2012/03/improved-markdown-table-commands-for-textmate/) work from [Dr. Drang (@drdrang)](https://twitter.com/drdrang) 6 | 7 | [Changelog](https://github.com/fcrespo82/atom-markdown-table-formatter/blob/master/CHANGELOG.md) 8 | 9 | 10 | ## Usage 11 | 12 | There are two basic ways of using this plugin. 13 | 14 | 1. Select the table you want to format and then hit `alt-shift-T` to format it. 15 | 2. If you didn't select any table the entire document (default) will be scanned, by the plugin, for tables and format all of them. 16 | 17 | ### Settings & Keybindings 18 | 19 | ![Settings image](https://github.com/fcrespo82/atom-markdown-table-formatter/raw/master/settings.png) 20 | 21 | ## Tips 22 | 23 | ### Using with `language-markdown` package 24 | 25 | Since `language-markdown` is a community package, auto-formatting entire document is not supported out-of-the box, but that is easy to fix, see next tip. 26 | 27 | ### Enable Markdown Table Formatter for the current file type 28 | 29 | To enable Markdown Table Formatter for your current file type: put your cursor in the file, open the Command Palette ⌘ (CMD)+⇧ (SHIFT)+P, and run the `Markdown Table Formatter: Enable For Current Scope` command. This will add grammar scope from current editor to the list of scopes in the settings for the Markdown Table Formatter package. You can edit this setting manually later if you want to. 30 | 31 | Formatting selection should work regardless though. 32 | 33 | ### How to style the tables correctly if you use non-monospace fonts 34 | 35 | Pre Atom-1.13: 36 | 37 | ```less 38 | atom-text-editor::shadow .table.gfm { 39 | font-family: monospace; 40 | } 41 | ``` 42 | 43 | Post Atom-1.13: 44 | 45 | ```less 46 | atom-text-editor .syntax--table.syntax--gfm { 47 | font-family: monospace; 48 | } 49 | ``` 50 | -------------------------------------------------------------------------------- /keymaps/markdown-table-formatter.cson: -------------------------------------------------------------------------------- 1 | # Keybindings require three things to be fully defined: A selector that is 2 | # matched against the focused element, the keystroke and the command to 3 | # execute. 4 | # 5 | # Below is a basic keybinding which registers on all platforms by applying to 6 | # the root workspace element. 7 | 8 | # For more detailed documentation see 9 | # https://atom.io/docs/latest/advanced/keymaps 10 | 'atom-text-editor': 11 | 'alt-shift-t': 'markdown-table-formatter:format' 12 | -------------------------------------------------------------------------------- /lib/config.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.config = { 4 | formatOnSave: { 5 | type: 'boolean', 6 | default: true, 7 | description: 'Format tables when document is saved?', 8 | }, 9 | autoSelectEntireDocument: { 10 | type: 'boolean', 11 | default: true, 12 | description: 'Select entire document if selection is empty', 13 | }, 14 | spacePadding: { 15 | type: 'integer', 16 | default: 1, 17 | description: 'How many spaces between left and right of each column content', 18 | }, 19 | keepFirstAndLastPipes: { 20 | type: 'boolean', 21 | default: true, 22 | description: `Keep first and last pipes "|" in table formatting. \ 23 | Tables are easier to format when pipes are kept`, 24 | }, 25 | defaultTableJustification: { 26 | type: 'string', 27 | default: 'Left', 28 | enum: ['Left', 'Center', 'Right'], 29 | description: `Defines the default justification for tables that have only a \ 30 | \'-\' on the formatting line`, 31 | }, 32 | markdownGrammarScopes: { 33 | type: 'array', 34 | default: ['source.gfm'], 35 | description: `File grammar scopes that will be considered Markdown by this package (comma-separated). \ 36 | Run \'Markdown Table Formatter: Enable For Current Scope\' command to \ 37 | add current editor grammar to this setting.`, 38 | items: { 39 | type: 'string', 40 | }, 41 | }, 42 | limitLastColumnPadding: { 43 | type: 'boolean', 44 | default: false, 45 | description: `Do not pad the last column to more than your editor\'s \ 46 | preferredLineLength setting.`, 47 | }, 48 | }; 49 | //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY29uZmlnLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vc3JjL2NvbmZpZy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOztBQUFhLFFBQUEsTUFBTSxHQUFHO0lBQ3BCLFlBQVksRUFBRTtRQUNaLElBQUksRUFBRSxTQUFTO1FBQ2YsT0FBTyxFQUFFLElBQUk7UUFDYixXQUFXLEVBQUUsdUNBQXVDO0tBQ3JEO0lBQ0Qsd0JBQXdCLEVBQUU7UUFDeEIsSUFBSSxFQUFFLFNBQVM7UUFDZixPQUFPLEVBQUUsSUFBSTtRQUNiLFdBQVcsRUFBRSw4Q0FBOEM7S0FDNUQ7SUFDRCxZQUFZLEVBQUU7UUFDWixJQUFJLEVBQUUsU0FBUztRQUNmLE9BQU8sRUFBRSxDQUFDO1FBQ1YsV0FBVyxFQUNULCtEQUErRDtLQUNsRTtJQUNELHFCQUFxQixFQUFFO1FBQ3JCLElBQUksRUFBRSxTQUFTO1FBQ2YsT0FBTyxFQUFFLElBQUk7UUFDYixXQUFXLEVBQUU7Z0RBQytCO0tBQzdDO0lBQ0QseUJBQXlCLEVBQUU7UUFDekIsSUFBSSxFQUFFLFFBQVE7UUFDZCxPQUFPLEVBQUUsTUFBTTtRQUNmLElBQUksRUFBRSxDQUFDLE1BQU0sRUFBRSxRQUFRLEVBQUUsT0FBTyxDQUFDO1FBQ2pDLFdBQVcsRUFBRTs2QkFDWTtLQUMxQjtJQUNELHFCQUFxQixFQUFFO1FBQ3JCLElBQUksRUFBRSxPQUFPO1FBQ2IsT0FBTyxFQUFFLENBQUMsWUFBWSxDQUFDO1FBQ3ZCLFdBQVcsRUFBRTs7NENBRTJCO1FBQ3hDLEtBQUssRUFBRTtZQUNMLElBQUksRUFBRSxRQUFRO1NBQ2Y7S0FDRjtJQUNELHNCQUFzQixFQUFFO1FBQ3RCLElBQUksRUFBRSxTQUFTO1FBQ2YsT0FBTyxFQUFFLEtBQUs7UUFDZCxXQUFXLEVBQUU7NkJBQ1k7S0FDMUI7Q0FDRixDQUFBIiwic291cmNlc0NvbnRlbnQiOlsiZXhwb3J0IGNvbnN0IGNvbmZpZyA9IHtcbiAgZm9ybWF0T25TYXZlOiB7XG4gICAgdHlwZTogJ2Jvb2xlYW4nLFxuICAgIGRlZmF1bHQ6IHRydWUsXG4gICAgZGVzY3JpcHRpb246ICdGb3JtYXQgdGFibGVzIHdoZW4gZG9jdW1lbnQgaXMgc2F2ZWQ/JyxcbiAgfSxcbiAgYXV0b1NlbGVjdEVudGlyZURvY3VtZW50OiB7XG4gICAgdHlwZTogJ2Jvb2xlYW4nLFxuICAgIGRlZmF1bHQ6IHRydWUsXG4gICAgZGVzY3JpcHRpb246ICdTZWxlY3QgZW50aXJlIGRvY3VtZW50IGlmIHNlbGVjdGlvbiBpcyBlbXB0eScsXG4gIH0sXG4gIHNwYWNlUGFkZGluZzoge1xuICAgIHR5cGU6ICdpbnRlZ2VyJyxcbiAgICBkZWZhdWx0OiAxLFxuICAgIGRlc2NyaXB0aW9uOlxuICAgICAgJ0hvdyBtYW55IHNwYWNlcyBiZXR3ZWVuIGxlZnQgYW5kIHJpZ2h0IG9mIGVhY2ggY29sdW1uIGNvbnRlbnQnLFxuICB9LFxuICBrZWVwRmlyc3RBbmRMYXN0UGlwZXM6IHtcbiAgICB0eXBlOiAnYm9vbGVhbicsXG4gICAgZGVmYXVsdDogdHJ1ZSxcbiAgICBkZXNjcmlwdGlvbjogYEtlZXAgZmlyc3QgYW5kIGxhc3QgcGlwZXMgXCJ8XCIgaW4gdGFibGUgZm9ybWF0dGluZy4gXFxcblRhYmxlcyBhcmUgZWFzaWVyIHRvIGZvcm1hdCB3aGVuIHBpcGVzIGFyZSBrZXB0YCxcbiAgfSxcbiAgZGVmYXVsdFRhYmxlSnVzdGlmaWNhdGlvbjoge1xuICAgIHR5cGU6ICdzdHJpbmcnLFxuICAgIGRlZmF1bHQ6ICdMZWZ0JyxcbiAgICBlbnVtOiBbJ0xlZnQnLCAnQ2VudGVyJywgJ1JpZ2h0J10sXG4gICAgZGVzY3JpcHRpb246IGBEZWZpbmVzIHRoZSBkZWZhdWx0IGp1c3RpZmljYXRpb24gZm9yIHRhYmxlcyB0aGF0IGhhdmUgb25seSBhIFxcXG5cXCctXFwnIG9uIHRoZSBmb3JtYXR0aW5nIGxpbmVgLFxuICB9LFxuICBtYXJrZG93bkdyYW1tYXJTY29wZXM6IHtcbiAgICB0eXBlOiAnYXJyYXknLFxuICAgIGRlZmF1bHQ6IFsnc291cmNlLmdmbSddLFxuICAgIGRlc2NyaXB0aW9uOiBgRmlsZSBncmFtbWFyIHNjb3BlcyB0aGF0IHdpbGwgYmUgY29uc2lkZXJlZCBNYXJrZG93biBieSB0aGlzIHBhY2thZ2UgKGNvbW1hLXNlcGFyYXRlZCkuIFxcXG5SdW4gXFwnTWFya2Rvd24gVGFibGUgRm9ybWF0dGVyOiBFbmFibGUgRm9yIEN1cnJlbnQgU2NvcGVcXCcgY29tbWFuZCB0byBcXFxuYWRkIGN1cnJlbnQgZWRpdG9yIGdyYW1tYXIgdG8gdGhpcyBzZXR0aW5nLmAsXG4gICAgaXRlbXM6IHtcbiAgICAgIHR5cGU6ICdzdHJpbmcnLFxuICAgIH0sXG4gIH0sXG4gIGxpbWl0TGFzdENvbHVtblBhZGRpbmc6IHtcbiAgICB0eXBlOiAnYm9vbGVhbicsXG4gICAgZGVmYXVsdDogZmFsc2UsXG4gICAgZGVzY3JpcHRpb246IGBEbyBub3QgcGFkIHRoZSBsYXN0IGNvbHVtbiB0byBtb3JlIHRoYW4geW91ciBlZGl0b3JcXCdzIFxcXG5wcmVmZXJyZWRMaW5lTGVuZ3RoIHNldHRpbmcuYCxcbiAgfSxcbn1cbiJdfQ== -------------------------------------------------------------------------------- /lib/formatTable.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | const table_formatter_1 = require("./table-formatter"); 4 | const wcswidth = require("wcwidth"); 5 | function swidth(str) { 6 | const zwcrx = /[\u200B-\u200D\uFEFF\u00AD]/g; 7 | const match = str.match(zwcrx); 8 | return wcswidth(str) - (match ? match.length : 0); 9 | } 10 | function padding(len, str = ' ') { 11 | return str.repeat(len); 12 | } 13 | const stripTailPipes = (str) => str.trim().replace(/(^\||\|$)/g, ''); 14 | const splitCells = (str) => str.split('|'); 15 | const addTailPipes = (str) => `|${str}|`; 16 | const joinCells = (arr) => arr.join('|'); 17 | const tableJustMap = { 18 | Left: ':-', 19 | Center: '::', 20 | Right: '-:', 21 | }; 22 | function formatTable(text, settings = table_formatter_1.getAllSettings()) { 23 | const addTailPipesIfNeeded = settings.keepFirstAndLastPipes 24 | ? addTailPipes 25 | : (x) => x; 26 | let formatline = text[2].trim(); 27 | const headerline = text[1].trim(); 28 | let formatrow; 29 | let data; 30 | if (headerline.length === 0) { 31 | formatrow = 0; 32 | data = text[3]; 33 | } 34 | else { 35 | formatrow = 1; 36 | data = text[1] + text[3]; 37 | } 38 | const lines = data.trim().split(/\r?\n/); 39 | const justify = splitCells(stripTailPipes(formatline)).map(cell => { 40 | const trimmed = cell.trim(); 41 | const first = trimmed[0]; 42 | const last = trimmed[trimmed.length - 1]; 43 | const ends = (first || ':') + (last || '-'); 44 | if (ends === '--') 45 | return tableJustMap[settings.defaultTableJustification]; 46 | else 47 | return ends; 48 | }); 49 | const columns = justify.length; 50 | const colArr = Array.from(Array(columns)); 51 | const cellPadding = padding(settings.spacePadding); 52 | const content = lines.map(line => { 53 | const cells = splitCells(stripTailPipes(line)); 54 | if (columns - cells.length > 0) { 55 | cells.push(...Array(columns - cells.length).fill('')); 56 | } 57 | else if (columns - cells.length < 0) { 58 | cells[columns - 1] = joinCells(cells.slice(columns - 1)); 59 | } 60 | return cells.map(cell => `${cellPadding}${cell.trim()}${cellPadding}`); 61 | }); 62 | const widths = colArr.map((_x, i) => Math.max(2, ...content.map(cells => swidth(cells[i])))); 63 | if (settings.limitLastColumnPadding) { 64 | const preferredLineLength = atom.config.get('editor.preferredLineLength'); 65 | const sum = (arr) => arr.reduce((x, y) => x + y, 0); 66 | const wsum = sum(widths); 67 | if (wsum > preferredLineLength) { 68 | const prewsum = sum(widths.slice(0, -1)); 69 | widths[widths.length - 1] = Math.max(preferredLineLength - prewsum - widths.length - 1, 3); 70 | } 71 | } 72 | const just = function (str, col) { 73 | const length = Math.max(widths[col] - swidth(str), 0); 74 | switch (justify[col]) { 75 | case '::': 76 | return padding(length / 2) + str + padding((length + 1) / 2); 77 | case '-:': 78 | return padding(length) + str; 79 | case ':-': 80 | return str + padding(length); 81 | default: 82 | throw new Error(`Unknown column justification ${justify[col]}`); 83 | } 84 | }; 85 | const formatted = content.map(cells => addTailPipesIfNeeded(joinCells(colArr.map((_x, i) => just(cells[i], i))))); 86 | formatline = addTailPipesIfNeeded(joinCells(colArr.map((_x, i) => { 87 | const [front, back] = justify[i]; 88 | return front + padding(widths[i] - 2, '-') + back; 89 | }))); 90 | formatted.splice(formatrow, 0, formatline); 91 | return ((formatrow === 0 && text[1] !== '' ? '\n' : '') + 92 | formatted.join('\n') + 93 | '\n'); 94 | } 95 | exports.formatTable = formatTable; 96 | //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZm9ybWF0VGFibGUuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi9zcmMvZm9ybWF0VGFibGUudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7QUFBQSx1REFBa0Q7QUFDbEQsb0NBQW9DO0FBRXBDLGdCQUFnQixHQUFXO0lBR3pCLE1BQU0sS0FBSyxHQUFHLDhCQUE4QixDQUFBO0lBQzVDLE1BQU0sS0FBSyxHQUFHLEdBQUcsQ0FBQyxLQUFLLENBQUMsS0FBSyxDQUFDLENBQUE7SUFDOUIsTUFBTSxDQUFDLFFBQVEsQ0FBQyxHQUFHLENBQUMsR0FBRyxDQUFDLEtBQUssQ0FBQyxDQUFDLENBQUMsS0FBSyxDQUFDLE1BQU0sQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUE7QUFDbkQsQ0FBQztBQUVELGlCQUFpQixHQUFXLEVBQUUsTUFBYyxHQUFHO0lBQzdDLE1BQU0sQ0FBQyxHQUFHLENBQUMsTUFBTSxDQUFDLEdBQUcsQ0FBQyxDQUFBO0FBQ3hCLENBQUM7QUFDRCxNQUFNLGNBQWMsR0FBRyxDQUFDLEdBQVcsRUFBRSxFQUFFLENBQUMsR0FBRyxDQUFDLElBQUksRUFBRSxDQUFDLE9BQU8sQ0FBQyxZQUFZLEVBQUUsRUFBRSxDQUFDLENBQUE7QUFDNUUsTUFBTSxVQUFVLEdBQUcsQ0FBQyxHQUFXLEVBQUUsRUFBRSxDQUFDLEdBQUcsQ0FBQyxLQUFLLENBQUMsR0FBRyxDQUFDLENBQUE7QUFDbEQsTUFBTSxZQUFZLEdBQUcsQ0FBQyxHQUFXLEVBQUUsRUFBRSxDQUFDLElBQUksR0FBRyxHQUFHLENBQUE7QUFDaEQsTUFBTSxTQUFTLEdBQUcsQ0FBQyxHQUFhLEVBQUUsRUFBRSxDQUFDLEdBQUcsQ0FBQyxJQUFJLENBQUMsR0FBRyxDQUFDLENBQUE7QUFFbEQsTUFBTSxZQUFZLEdBQUc7SUFDbkIsSUFBSSxFQUFFLElBQUk7SUFDVixNQUFNLEVBQUUsSUFBSTtJQUNaLEtBQUssRUFBRSxJQUFJO0NBQ1osQ0FBQTtBQUVELHFCQUNFLElB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QUMsS0FBSyxFQUFFLElBQUksQ0FBQyxHQUFHLE9BQU8sQ0FBQyxDQUFDLENBQUMsQ0FBQTtRQUNoQyxNQUFNLENBQUMsS0FBSyxHQUFHLE9BQU8sQ0FBQyxNQUFNLENBQUMsQ0FBQyxDQUFDLEdBQUcsQ0FBQyxFQUFFLEdBQUcsQ0FBQyxHQUFHLElBQUksQ0FBQTtJQUNuRCxDQUFDLENBQUMsQ0FDSCxDQUNGLENBQUE7SUFFRCxTQUFTLENBQUMsTUFBTSxDQUFDLFNBQVMsRUFBRSxDQUFDLEVBQUUsVUFBVSxDQUFDLENBQUE7SUFFMUMsTUFBTSxDQUFDLENBQ0wsQ0FBQyxTQUFTLEtBQUssQ0FBQyxJQUFJLElBQUksQ0FBQyxDQUFDLENBQUMsS0FBSyxFQUFFLENBQUMsQ0FBQyxDQUFDLElBQUksQ0FBQyxDQUFDLENBQUMsRUFBRSxDQUFDO1FBQy9DLFNBQVMsQ0FBQyxJQUFJLENBQUMsSUFBSSxDQUFDO1FBQ3BCLElBQUksQ0FDTCxDQUFBO0FBQ0gsQ0FBQztBQXBHRCxrQ0FvR0MiLCJzb3VyY2VzQ29udGVudCI6WyJpbXBvcnQgeyBnZXRBbGxTZXR0aW5ncyB9IGZyb20gJy4vdGFibGUtZm9ybWF0dGVyJ1xuaW1wb3J0IHdjc3dpZHRoID0gcmVxdWlyZSgnd2N3aWR0aCcpXG5cbmZ1bmN0aW9uIHN3aWR0aChzdHI6IHN0cmluZykge1xuICAvLyB6ZXJvLXdpZHRoIFVuaWNvZGUgY2hhcmFjdGVycyB0aGF0IHdlIHNob3VsZCBpZ25vcmUgZm9yXG4gIC8vIHB1cnBvc2VzIG9mIGNvbXB1dGluZyBzdHJpbmcgXCJkaXNwbGF5XCIgd2lkdGhcbiAgY29uc3QgendjcnggPSAvW1xcdTIwMEItXFx1MjAwRFxcdUZFRkZcXHUwMEFEXS9nXG4gIGNvbnN0IG1hdGNoID0gc3RyLm1hdGNoKHp3Y3J4KVxuICByZXR1cm4gd2Nzd2lkdGgoc3RyKSAtIChtYXRjaCA/IG1hdGNoLmxlbmd0aCA6IDApXG59XG5cbmZ1bmN0aW9uIHBhZGRpbmcobGVuOiBudW1iZXIsIHN0cjogc3RyaW5nID0gJyAnKSB7XG4gIHJldHVybiBzdHIucmVwZWF0KGxlbilcbn1cbmNvbnN0IHN0cmlwVGFpbFBpcGVzID0gKHN0cjogc3RyaW5nKSA9PiBzdHIudHJpbSgpLnJlcGxhY2UoLyheXFx8fFxcfCQpL2csICcnKVxuY29uc3Qgc3BsaXRDZWxscyA9IChzdHI6IHN0cmluZykgPT4gc3RyLnNwbGl0KCd8JylcbmNvbnN0IGFkZFRhaWxQaXBlcyA9IChzdHI6IHN0cmluZykgPT4gYHwke3N0cn18YFxuY29uc3Qgam9pbkNlbGxzID0gKGFycjogc3RyaW5nW10pID0+IGFyci5qb2luKCd8JylcblxuY29uc3QgdGFibGVKdXN0TWFwID0ge1xuICBMZWZ0OiAnOi0nLFxuICBDZW50ZXI6ICc6OicsXG4gIFJpZ2h0OiAnLTonLFxufVxuXG5leHBvcnQgZnVuY3Rpb24gZm9ybWF0VGFibGUoXG4gIHRleHQ6IFJlZ0V4cEV4ZWNBcnJheSxcbiAgc2V0dGluZ3MgPSBnZXRBbGxTZXR0aW5ncygpLFxuKSB7XG4gIGNvbnN0IGFkZFRhaWxQaXBlc0lmTmVlZGVkID0gc2V0dGluZ3Mua2VlcEZpcnN0QW5kTGFzdFBpcGVzXG4gICAgPyBhZGRUYWlsUGlwZXNcbiAgICA6ICh4OiBzdHJpbmcpID0+IHhcblxuICBsZXQgZm9ybWF0bGluZSA9IHRleHRbMl0udHJpbSgpXG4gIGNvbnN0IGhlYWRlcmxpbmUgPSB0ZXh0WzFdLnRyaW0oKVxuXG4gIGxldCBmb3JtYXRyb3c6IG51bWJlclxuICBsZXQgZGF0YTogc3RyaW5nXG4gIGlmIChoZWFkZXJsaW5lLmxlbmd0aCA9PT0gMCkge1xuICAgIGZvcm1hdHJvdyA9IDBcbiAgICBkYXRhID0gdGV4dFszXVxuICB9IGVsc2Uge1xuICAgIGZvcm1hdHJvdyA9IDFcbiAgICBkYXRhID0gdGV4dFsxXSArIHRleHRbM11cbiAgfVxuICBjb25zdCBsaW5lcyA9IGRhdGEudHJpbSgpLnNwbGl0KC9cXHI/XFxuLylcblxuICBjb25zdCBqdXN0aWZ5ID0gc3BsaXRDZWxscyhzdHJpcFRhaWxQaXBlcyhmb3JtYXRsaW5lKSkubWFwKGNlbGwgPT4ge1xuICAgIGNvbnN0IHRyaW1tZWQgPSBjZWxsLnRyaW0oKVxuICAgIGNvbnN0IGZpcnN0ID0gdHJpbW1lZFswXVxuICAgIGNvbnN0IGxhc3QgPSB0cmltbWVkW3RyaW1tZWQubGVuZ3RoIC0gMV1cbiAgICBjb25zdCBlbmRzID0gKGZpcnN0IHx8ICc6JykgKyAobGFzdCB8fCAnLScpXG4gICAgaWYgKGVuZHMgPT09ICctLScpIHJldHVybiB0YWJsZUp1c3RNYXBbc2V0dGluZ3MuZGVmYXVsdFRhYmxlSnVzdGlmaWNhdGlvbl1cbiAgICBlbHNlIHJldHVybiBlbmRzXG4gIH0pXG5cbiAgY29uc3QgY29sdW1ucyA9IGp1c3RpZnkubGVuZ3RoXG4gIGNvbnN0IGNvbEFycjogdW5kZWZpbmVkW10gPSBBcnJheS5mcm9tKEFycmF5KGNvbHVtbnMpKVxuXG4gIGNvbnN0IGNlbGxQYWRkaW5nID0gcGFkZGluZyhzZXR0aW5ncy5zcGFjZVBhZGRpbmcpXG5cbiAgY29uc3QgY29udGVudCA9IGxpbmVzLm1hcChsaW5lID0+IHtcbiAgICBjb25zdCBjZWxscyA9IHNwbGl0Q2VsbHMoc3RyaXBUYWlsUGlwZXMobGluZSkpXG4gICAgaWYgKGNvbHVtbnMgLSBjZWxscy5sZW5ndGggPiAwKSB7XG4gICAgICAvLyBwYWQgcm93cyB0byBoYXZlIGBjb2x1bW5zYCBjZWxsc1xuICAgICAgY2VsbHMucHVzaCguLi5BcnJheShjb2x1bW5zIC0gY2VsbHMubGVuZ3RoKS5maWxsKCcnKSlcbiAgICB9IGVsc2UgaWYgKGNvbHVtbnMgLSBjZWxscy5sZW5ndGggPCAwKSB7XG4gICAgICAvLyBwdXQgYWxsIGV4dHJhIGNvbnRlbnQgaW50byBsYXN0IGNlbGxcbiAgICAgIGNlbGxzW2NvbHVtbnMgLSAxXSA9IGpvaW5DZWxscyhjZWxscy5zbGljZShjb2x1bW5zIC0gMSkpXG4gICAgfVxuICAgIHJldHVybiBjZWxscy5tYXAoY2VsbCA9PiBgJHtjZWxsUGFkZGluZ30ke2NlbGwudHJpbSgpfSR7Y2VsbFBhZGRpbmd9YClcbiAgfSlcblxuICBjb25zdCB3aWR0aHMgPSBjb2xBcnIubWFwKChfeCwgaSkgPT5cbiAgICBNYXRoLm1heCgyLCAuLi5jb250ZW50Lm1hcChjZWxscyA9PiBzd2lkdGgoY2VsbHNbaV0pKSksXG4gIClcblxuICBpZiAoc2V0dGluZ3MubGltaXRMYXN0Q29sdW1uUGFkZGluZykge1xuICAgIGNvbnN0IHByZWZlcnJlZExpbmVMZW5ndGggPSBhdG9tLmNvbmZpZy5nZXQoJ2VkaXRvci5wcmVmZXJyZWRMaW5lTGVuZ3RoJylcbiAgICBjb25zdCBzdW0gPSAoYXJyOiBudW1iZXJbXSkgPT4gYXJyLnJlZHVjZSgoeCwgeSkgPT4geCArIHksIDApXG4gICAgY29uc3Qgd3N1bSA9IHN1bSh3aWR0aHMpXG4gICAgaWYgKHdzdW0gPiBwcmVmZXJyZWRMaW5lTGVuZ3RoKSB7XG4gICAgICBjb25zdCBwcmV3c3VtID0gc3VtKHdpZHRocy5zbGljZSgwLCAtMSkpXG4gICAgICB3aWR0aHNbd2lkdGhzLmxlbmd0aCAtIDFdID0gTWF0aC5tYXgoXG4gICAgICAgIHByZWZlcnJlZExpbmVMZW5ndGggLSBwcmV3c3VtIC0gd2lkdGhzLmxlbmd0aCAtIDEsXG4gICAgICAgIDMsXG4gICAgICApXG4gICAgICAvLyBOZWVkIGF0IGxlYXN0IDotLSBmb3IgZ2l0aHViIHRvIHJlY29nbml6ZSBhIGNvbHVtblxuICAgIH1cbiAgfVxuXG4gIGNvbnN0IGp1c3QgPSBmdW5jdGlvbihzdHI6IHN0cmluZywgY29sOiBudW1iZXIpIHtcbiAgICBjb25zdCBsZW5ndGggPSBNYXRoLm1heCh3aWR0aHNbY29sXSAtIHN3aWR0aChzdHIpLCAwKVxuICAgIHN3aXRjaCAoanVzdGlmeVtjb2xdKSB7XG4gICAgICBjYXNlICc6Oic6XG4gICAgICAgIHJldHVybiBwYWRkaW5nKGxlbmd0aCAvIDIpICsgc3RyICsgcGFkZGluZygobGVuZ3RoICsgMSkgLyAyKVxuICAgICAgY2FzZSAnLTonOlxuICAgICAgICByZXR1cm4gcGFkZGluZyhsZW5ndGgpICsgc3RyXG4gICAgICBjYXNlICc6LSc6XG4gICAgICAgIHJldHVybiBzdHIgKyBwYWRkaW5nKGxlbmd0aClcbiAgICAgIGRlZmF1bHQ6XG4gICAgICAgIHRocm93IG5ldyBFcnJvcihgVW5rbm93biBjb2x1bW4ganVzdGlmaWNhdGlvbiAke2p1c3RpZnlbY29sXX1gKVxuICAgIH1cbiAgfVxuXG4gIGNvbnN0IGZvcm1hdHRlZCA9IGNvbnRlbnQubWFwKGNlbGxzID0+XG4gICAgYWRkVGFpbFBpcGVzSWZOZWVkZWQoam9pbkNlbGxzKGNvbEFyci5tYXAoKF94LCBpKSA9PiBqdXN0KGNlbGxzW2ldLCBpKSkpKSxcbiAgKVxuXG4gIGZvcm1hdGxpbmUgPSBhZGRUYWlsUGlwZXNJZk5lZWRlZChcbiAgICBqb2luQ2VsbHMoXG4gICAgICBjb2xBcnIubWFwKChfeCwgaSkgPT4ge1xuICAgICAgICBjb25zdCBbZnJvbnQsIGJhY2tdID0ganVzdGlmeVtpXVxuICAgICAgICByZXR1cm4gZnJvbnQgKyBwYWRkaW5nKHdpZHRoc1tpXSAtIDIsICctJykgKyBiYWNrXG4gICAgICB9KSxcbiAgICApLFxuICApXG5cbiAgZm9ybWF0dGVkLnNwbGljZShmb3JtYXRyb3csIDAsIGZvcm1hdGxpbmUpXG5cbiAgcmV0dXJuIChcbiAgICAoZm9ybWF0cm93ID09PSAwICYmIHRleHRbMV0gIT09ICcnID8gJ1xcbicgOiAnJykgK1xuICAgIGZvcm1hdHRlZC5qb2luKCdcXG4nKSArXG4gICAgJ1xcbidcbiAgKVxufVxuIl19 -------------------------------------------------------------------------------- /lib/markdown-table-formatter.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | const table_formatter_1 = require("./table-formatter"); 4 | var config_1 = require("./config"); 5 | exports.config = config_1.config; 6 | let tableFormatter; 7 | let command; 8 | function activate() { 9 | tableFormatter = new table_formatter_1.TableFormatter(); 10 | command = atom.commands.add('atom-text-editor', { 11 | 'markdown-table-formatter:format': event => { 12 | const editor = event.currentTarget.getModel(); 13 | tableFormatter.format(editor); 14 | }, 15 | 'markdown-table-formatter:enable-for-current-scope'(event) { 16 | const editor = event.currentTarget.getModel(); 17 | const scope = editor.getGrammar().scopeName; 18 | const key = 'markdown-table-formatter.markdownGrammarScopes'; 19 | const current = atom.config.get(key); 20 | if (!scope) { 21 | atom.notifications.addError('Could not determine editor grammar scope'); 22 | } 23 | else if (current.includes(scope)) { 24 | atom.notifications.addWarning(`${scope} already considered Markdown`); 25 | } 26 | else { 27 | atom.config.set(key, [...current, scope]); 28 | atom.notifications.addSuccess(`Successfully added ${scope} to Markdown scopes`); 29 | } 30 | }, 31 | }); 32 | } 33 | exports.activate = activate; 34 | function deactivate() { 35 | command.dispose(); 36 | tableFormatter.destroy(); 37 | } 38 | exports.deactivate = deactivate; 39 | //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoibWFya2Rvd24tdGFibGUtZm9ybWF0dGVyLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vc3JjL21hcmtkb3duLXRhYmxlLWZvcm1hdHRlci50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOztBQUFBLHVEQUFrRDtBQUVsRCxtQ0FBaUM7QUFBeEIsMEJBQUEsTUFBTSxDQUFBO0FBRWYsSUFBSSxjQUE4QixDQUFBO0FBQ2xDLElBQUksT0FBbUIsQ0FBQTtBQUV2QjtJQUNFLGNBQWMsR0FBRyxJQUFJLGdDQUFjLEVBQUUsQ0FBQTtJQUVyQyxPQUFPLEdBQUcsSUFBSSxDQUFDLFFBQVEsQ0FBQyxHQUFHLENBQUMsa0JBQWtCLEVBQUU7UUFDOUMsaUNBQWlDLEVBQUUsS0FBSyxDQUFDLEVBQUU7WUFDekMsTUFBTSxNQUFNLEdBQUcsS0FBSyxDQUFDLGFBQWEsQ0FBQyxRQUFRLEVBQUUsQ0FBQTtZQUM3QyxjQUFjLENBQUMsTUFBTSxDQUFDLE1BQU0sQ0FBQyxDQUFBO1FBQy9CLENBQUM7UUFDRCxtREFBbUQsQ0FBQyxLQUFLO1lBQ3ZELE1BQU0sTUFBTSxHQUFHLEtBQUssQ0FBQyxhQUFhLENBQUMsUUFBUSxFQUFFLENBQUE7WUFDN0MsTUFBTSxLQUFLLEdBQUcsTUFBTSxDQUFDLFVBQVUsRUFBRSxDQUFDLFNBQVMsQ0FBQTtZQUMzQyxNQUFNLEdBQUcsR0FBRyxnREFBZ0QsQ0FBQTtZQUM1RCxNQUFNLE9BQU8sR0FBRyxJQUFJLENBQUMsTUFBTSxDQUFDLEdBQUcsQ0FBQyxHQUFHLENBQUMsQ0FBQTtZQUNwQyxFQUFFLENBQUMsQ0FBQyxDQUFDLEtBQUssQ0FBQyxDQUFDLENBQUM7Z0JBQ1gsSUFBSSxDQUFDLGFBQWEsQ0FBQyxRQUFRLENBQUMsMENBQTBDLENBQUMsQ0FBQTtZQUN6RSxDQUFDO1lBQUMsSUFBSSxDQUFDLEVBQUUsQ0FBQyxDQUFDLE9BQU8sQ0FBQyxRQUFRLENBQUMsS0FBSyxDQUFDLENBQUMsQ0FBQyxDQUFDO2dCQUNuQyxJQUFJLENBQUMsYUFBYSxDQUFDLFVBQVUsQ0FBQyxHQUFHLEtBQUssOEJBQThCLENBQUMsQ0FBQTtZQUN2RSxDQUFDO1lBQUMsSUFBSSxDQUFDLENBQUM7Z0JBQ04sSUFBSSxDQUFDLE1BQU0sQ0FBQyxHQUFHLENBQUMsR0FBRyxFQUFFLENBQUMsR0FBRyxPQUFPLEVBQUUsS0FBSyxDQUFDLENBQUMsQ0FBQTtnQkFDekMsSUFBSSxDQUFDLGFBQWEsQ0FBQyxVQUFVLENBQzNCLHNCQUFzQixLQUFLLHFCQUFxQixDQUNqRCxDQUFBO1lBQ0gsQ0FBQztRQUNILENBQUM7S0FDRixDQUFDLENBQUE7QUFDSixDQUFDO0FBekJELDRCQXlCQztBQUVEO0lBQ0UsT0FBTyxDQUFDLE9BQU8sRUFBRSxDQUFBO0lBQ2pCLGNBQWMsQ0FBQyxPQUFPLEVBQUUsQ0FBQTtBQUMxQixDQUFDO0FBSEQsZ0NBR0MiLCJzb3VyY2VzQ29udGVudCI6WyJpbXBvcnQgeyBUYWJsZUZvcm1hdHRlciB9IGZyb20gJy4vdGFibGUtZm9ybWF0dGVyJ1xuaW1wb3J0IHsgRGlzcG9zYWJsZSB9IGZyb20gJ2F0b20nXG5leHBvcnQgeyBjb25maWcgfSBmcm9tICcuL2NvbmZpZydcblxubGV0IHRhYmxlRm9ybWF0dGVyOiBUYWJsZUZvcm1hdHRlclxubGV0IGNvbW1hbmQ6IERpc3Bvc2FibGVcblxuZXhwb3J0IGZ1bmN0aW9uIGFjdGl2YXRlKCkge1xuICB0YWJsZUZvcm1hdHRlciA9IG5ldyBUYWJsZUZvcm1hdHRlcigpXG4gIC8vIFJlZ2lzdGVyIGNvbW1hbmQgdG8gd29ya3NwYWNlXG4gIGNvbW1hbmQgPSBhdG9tLmNvbW1hbmRzLmFkZCgnYXRvbS10ZXh0LWVkaXRvcicsIHtcbiAgICAnbWFya2Rvd24tdGFibGUtZm9ybWF0dGVyOmZvcm1hdCc6IGV2ZW50ID0+IHtcbiAgICAgIGNvbnN0IGVkaXRvciA9IGV2ZW50LmN1cnJlbnRUYXJnZXQuZ2V0TW9kZWwoKVxuICAgICAgdGFibGVGb3JtYXR0ZXIuZm9ybWF0KGVkaXRvcilcbiAgICB9LFxuICAgICdtYXJrZG93bi10YWJsZS1mb3JtYXR0ZXI6ZW5hYmxlLWZvci1jdXJyZW50LXNjb3BlJyhldmVudCkge1xuICAgICAgY29uc3QgZWRpdG9yID0gZXZlbnQuY3VycmVudFRhcmdldC5nZXRNb2RlbCgpXG4gICAgICBjb25zdCBzY29wZSA9IGVkaXRvci5nZXRHcmFtbWFyKCkuc2NvcGVOYW1lXG4gICAgICBjb25zdCBrZXkgPSAnbWFya2Rvd24tdGFibGUtZm9ybWF0dGVyLm1hcmtkb3duR3JhbW1hclNjb3BlcydcbiAgICAgIGNvbnN0IGN1cnJlbnQgPSBhdG9tLmNvbmZpZy5nZXQoa2V5KVxuICAgICAgaWYgKCFzY29wZSkge1xuICAgICAgICBhdG9tLm5vdGlmaWNhdGlvbnMuYWRkRXJyb3IoJ0NvdWxkIG5vdCBkZXRlcm1pbmUgZWRpdG9yIGdyYW1tYXIgc2NvcGUnKVxuICAgICAgfSBlbHNlIGlmIChjdXJyZW50LmluY2x1ZGVzKHNjb3BlKSkge1xuICAgICAgICBhdG9tLm5vdGlmaWNhdGlvbnMuYWRkV2FybmluZyhgJHtzY29wZX0gYWxyZWFkeSBjb25zaWRlcmVkIE1hcmtkb3duYClcbiAgICAgIH0gZWxzZSB7XG4gICAgICAgIGF0b20uY29uZmlnLnNldChrZXksIFsuLi5jdXJyZW50LCBzY29wZV0pXG4gICAgICAgIGF0b20ubm90aWZpY2F0aW9ucy5hZGRTdWNjZXNzKFxuICAgICAgICAgIGBTdWNjZXNzZnVsbHkgYWRkZWQgJHtzY29wZX0gdG8gTWFya2Rvd24gc2NvcGVzYCxcbiAgICAgICAgKVxuICAgICAgfVxuICAgIH0sXG4gIH0pXG59XG5cbmV4cG9ydCBmdW5jdGlvbiBkZWFjdGl2YXRlKCkge1xuICBjb21tYW5kLmRpc3Bvc2UoKVxuICB0YWJsZUZvcm1hdHRlci5kZXN0cm95KClcbn1cbiJdfQ== -------------------------------------------------------------------------------- /lib/regex.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | const XRegExp = require("xregexp"); 4 | exports.regex = XRegExp(`\ 5 | ( # header capture 6 | (?: 7 | (?:[^\\r\\n]*?\\|[^\\r\\n]*) # line w/ at least one pipe 8 | \\ * # maybe trailing whitespace 9 | )? # maybe header 10 | (?:\\r?\\n|^) # newline 11 | ) 12 | ( # format capture 13 | (?: 14 | \\|\\ *(?::?-+:?|::)\\ * # format starting w/pipe 15 | |\\|?(?:\\ *(?::?-+:?|::)\\ *\\|)+ # or separated by pipe 16 | ) 17 | (?:\\ *(?::?-+:?|::)\\ *)? # maybe w/o trailing pipe 18 | \\ * # maybe trailing whitespace 19 | \\r?\\n # newline 20 | ) 21 | ( # body capture 22 | (?: 23 | (?:[^\\r\\n]*?\\|[^\\r\\n]*) # line w/ at least one pipe 24 | \\ * # maybe trailing whitespace 25 | (?:\\r?\\n|$) # newline 26 | )+ # at least one 27 | ) 28 | `, 'gx'); 29 | //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicmVnZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi9zcmMvcmVnZXgudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7QUFBQSxtQ0FBbUM7QUFFdEIsUUFBQSxLQUFLLEdBQUcsT0FBTyxDQUMxQjs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7O0NBd0JELEVBQ0MsSUFBSSxDQUNMLENBQUEiLCJzb3VyY2VzQ29udGVudCI6WyJpbXBvcnQgWFJlZ0V4cCA9IHJlcXVpcmUoJ3hyZWdleHAnKVxuXG5leHBvcnQgY29uc3QgcmVnZXggPSBYUmVnRXhwKFxuICBgXFxcbiggIyBoZWFkZXIgY2FwdHVyZVxuICAoPzpcbiAgICAoPzpbXlxcXFxyXFxcXG5dKj9cXFxcfFteXFxcXHJcXFxcbl0qKSAgICAgICAjIGxpbmUgdy8gYXQgbGVhc3Qgb25lIHBpcGVcbiAgICBcXFxcICogICAgICAgICAgICAgICAgICAgICAgICMgbWF5YmUgdHJhaWxpbmcgd2hpdGVzcGFjZVxuICApPyAgICAgICAgICAgICAgICAgICAgICAgICAgIyBtYXliZSBoZWFkZXJcbiAgKD86XFxcXHI/XFxcXG58XikgICAgICAgICAgICAgICAgICMgbmV3bGluZVxuKVxuKCAjIGZvcm1hdCBjYXB0dXJlXG4gICg/OlxuICAgIFxcXFx8XFxcXCAqKD86Oj8tKzo/fDo6KVxcXFwgKiAgICAgICAgICAgICMgZm9ybWF0IHN0YXJ0aW5nIHcvcGlwZVxuICAgIHxcXFxcfD8oPzpcXFxcICooPzo6Py0rOj98OjopXFxcXCAqXFxcXHwpKyAgICMgb3Igc2VwYXJhdGVkIGJ5IHBpcGVcbiAgKVxuICAoPzpcXFxcICooPzo6Py0rOj98OjopXFxcXCAqKT8gICAgICAgICAgICMgbWF5YmUgdy9vIHRyYWlsaW5nIHBpcGVcbiAgXFxcXCAqICAgICAgICAgICAgICAgICAgICAgICAgICMgbWF5YmUgdHJhaWxpbmcgd2hpdGVzcGFjZVxuICBcXFxccj9cXFxcbiAgICAgICAgICAgICAgICAgICAgICAgIyBuZXdsaW5lXG4pXG4oICMgYm9keSBjYXB0dXJlXG4gICg/OlxuICAgICg/OlteXFxcXHJcXFxcbl0qP1xcXFx8W15cXFxcclxcXFxuXSopICAgICAgICMgbGluZSB3LyBhdCBsZWFzdCBvbmUgcGlwZVxuICAgIFxcXFwgKiAgICAgICAgICAgICAgICAgICAgICAgIyBtYXliZSB0cmFpbGluZyB3aGl0ZXNwYWNlXG4gICAgKD86XFxcXHI/XFxcXG58JCkgICAgICAgICAgICAgICAjIG5ld2xpbmVcbiAgKSsgIyBhdCBsZWFzdCBvbmVcbilcbmAsXG4gICdneCcsXG4pXG4iXX0= -------------------------------------------------------------------------------- /lib/table-formatter.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | const atom_1 = require("atom"); 4 | const regex_1 = require("./regex"); 5 | const formatTable_1 = require("./formatTable"); 6 | function getAllSettings() { 7 | return atom.config.get('markdown-table-formatter'); 8 | } 9 | exports.getAllSettings = getAllSettings; 10 | class TableFormatter { 11 | constructor() { 12 | this.subscriptions = new atom_1.CompositeDisposable(); 13 | atom.workspace.observeTextEditors(editor => this.subscriptions.add(editor.getBuffer().onWillSave(() => { 14 | if (atom.config.get('markdown-table-formatter.formatOnSave')) { 15 | this.format(editor, true); 16 | } 17 | }))); 18 | } 19 | destroy() { 20 | this.subscriptions.dispose(); 21 | } 22 | format(editor, force = false) { 23 | let selectionsRanges = editor.getSelectedBufferRanges(); 24 | const settings = getAllSettings(); 25 | const bufferRange = editor.getBuffer().getRange(); 26 | const selectionsRangesEmpty = selectionsRanges.every(i => i.isEmpty()); 27 | if (!settings.markdownGrammarScopes.includes(editor.getGrammar().scopeName) && 28 | selectionsRangesEmpty) { 29 | return undefined; 30 | } 31 | if (force || (selectionsRangesEmpty && settings.autoSelectEntireDocument)) { 32 | selectionsRanges = [bufferRange]; 33 | } 34 | else { 35 | selectionsRanges = selectionsRanges 36 | .filter(srange => !(srange.isEmpty() && settings.autoSelectEntireDocument)) 37 | .map(srange => { 38 | let { start } = bufferRange; 39 | let { end } = bufferRange; 40 | editor.scanInBufferRange(/^$/m, new atom_1.Range(srange.start, bufferRange.end), ({ range }) => (end = range.start)); 41 | editor.backwardsScanInBufferRange(/^$/m, new atom_1.Range(bufferRange.start, srange.end), ({ range }) => (start = range.start)); 42 | if (end.isLessThan(srange.end)) 43 | end = srange.end; 44 | if (start.isGreaterThan(srange.start)) 45 | start = srange.start; 46 | return new atom_1.Range(start, end); 47 | }); 48 | } 49 | return editor.getBuffer().transact(() => selectionsRanges.map(range => editor.backwardsScanInBufferRange(regex_1.regex, range, function (obj) { 50 | return editor.setTextInBufferRange(obj.range, formatTable_1.formatTable(obj.match, settings)); 51 | }))); 52 | } 53 | } 54 | exports.TableFormatter = TableFormatter; 55 | //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidGFibGUtZm9ybWF0dGVyLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vc3JjL3RhYmxlLWZvcm1hdHRlci50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOztBQUFBLCtCQUErRTtBQUMvRSxtQ0FBK0I7QUFDL0IsK0NBQTJDO0FBRTNDO0lBQ0UsTUFBTSxDQUFDLElBQUksQ0FBQyxNQUFNLENBQUMsR0FBRyxDQUFDLDBCQUEwQixDQUFDLENBQUE7QUFDcEQsQ0FBQztBQUZELHdDQUVDO0FBRUQ7SUFFRTtRQURpQixrQkFBYSxHQUFHLElBQUksMEJBQW1CLEVBQUUsQ0FBQTtRQUV4RCxJQUFJLENBQUMsU0FBUyxDQUFDLGtCQUFrQixDQUFDLE1BQU0sQ0FBQyxFQUFFLENBQ3pDLElBQUksQ0FBQyxhQUFhLENBQUMsR0FBRyxDQUNwQixNQUFNLENBQUMsU0FBUyxFQUFFLENBQUMsVUFBVSxDQUFDLEdBQUcsRUFBRTtZQUNqQyxFQUFFLENBQUMsQ0FBQyxJQUFJLENBQUMsTUFBTSxDQUFDLEdBQUcsQ0FBQyx1Q0FBdUMsQ0FBQyxDQUFDLENBQUMsQ0FBQztnQkFDN0QsSUFBSSxDQUFDLE1BQU0sQ0FBQyxNQUFNLEVBQUUsSUFBSSxDQUFDLENBQUE7WUFDM0IsQ0FBQztRQUNILENBQUMsQ0FBQyxDQUNILENBQ0YsQ0FBQTtJQUNILENBQUM7SUFFTSxPQUFPO1FBQ1osSUFBSSxDQUFDLGFBQWEsQ0FBQyxPQUFPLEVBQUUsQ0FBQTtJQUM5QixDQUFDO0lBRU0sTUFBTSxDQUFDLE1BQWtCLEVBQUUsUUFBaUIsS0FBSztRQUN0RCxJQUFJLGdCQUFnQixHQUFHLE1BQU0sQ0FBQyx1QkFBdUIsRUFBRSxDQUFBO1FBQ3ZELE1BQU0sUUFBUSxHQUFHLGNBQWMsRUFBRSxDQUFBO1FBRWpDLE1BQU0sV0FBVyxHQUFHLE1BQU0sQ0FBQyxTQUFTLEVBQUUsQ0FBQyxRQUFRLEVBQUUsQ0FBQTtRQUNqRCxNQUFNLHFCQUFxQixHQUFHLGdCQUFnQixDQUFDLEtBQUssQ0FBQyxDQUFDLENBQUMsRUFBRSxDQUFDLENBQUMsQ0FBQyxPQUFPLEVBQUUsQ0FBQyxDQUFBO1FBQ3RFLEVBQUUsQ0FBQyxDQUNELENBQUMsUUFBUSxDQUFDLHFCQUFxQixDQUFDLFFBQVEsQ0FBQyxNQUFNLENBQUMsVUFBVSxFQUFFLENBQUMsU0FBUyxDQUFDO1lBQ3ZFLHFCQUNGLENBQUMsQ0FBQyxDQUFDO1lBQ0QsTUFBTSxDQUFDLFNBQVMsQ0FBQTtRQUNsQixDQUFDO1FBQ0QsRUFBRSxDQUFDLENBQUMsS0FBSyxJQUFJLENBQUMscUJBQXFCLElBQUksUUFBUSxDQUFDLHdCQUF3QixDQUFDLENBQUMsQ0FBQyxDQUFDO1lBQzFFLGdCQUFnQixHQUFHLENBQUMsV0FBVyxDQUFDLENBQUE7UUFDbEMsQ0FBQztRQUFDLElBQUksQ0FBQyxDQUFDO1lBQ04sZ0JBQWdCLEdBQUcsZ0JBQWdCO2lCQUNoQyxNQUFNLENBQ0wsTUFBTSxDQUFDLEVBQUUsQ0FBQyxDQUFDLENBQUMsTUFBTSxDQUFDLE9BQU8sRUFBRSxJQUFJLFFBQVEsQ0FBQyx3QkFBd0IsQ0FBQyxDQUNuRTtpQkFDQSxHQUFHLENBQUMsTUFBTSxDQUFDLEVBQUU7Z0JBQ1osSUFBSSxFQUFFLEtBQUssRUFBRSxHQUFHLFdBQVcsQ0FBQTtnQkFDM0IsSUFBSSxFQUFFLEdBQUcsRUFBRSxHQUFHLFdBQVcsQ0FBQTtnQkFDekIsTUFBTSxDQUFDLGlCQUFpQixDQUN0QixLQUFLLEVBQ0wsSUFBSSxZQUFLLENBQUMsTUFBTSxDQUFDLEtBQUssRUFBRSxXQUFXLENBQUMsR0FBRyxDQUFDLEVBQ3hDLENBQUMsRUFBRSxLQUFLLEVBQUUsRUFBRSxFQUFFLENBQUMsQ0FBQyxHQUFHLEdBQUcsS0FBSyxDQUFDLEtBQUssQ0FBQyxDQUNuQyxDQUFBO2dCQUNELE1BQU0sQ0FBQywwQkFBMEIsQ0FDL0IsS0FBSyxFQUNMLElBQUksWUFBSyxDQUFDLFdBQVcsQ0FBQyxLQUFLLEVBQUUsTUFBTSxDQUFDLEdBQUcsQ0FBQyxFQUN4QyxDQUFDLEVBQUUsS0FBSyxFQUFFLEVBQUUsRUFBRSxDQUFDLENBQUMsS0FBSyxHQUFHLEtBQUssQ0FBQyxLQUFLLENBQUMsQ0FDckMsQ0FBQTtnQkFDRCxFQUFFLENBQUMsQ0FBQyxHQUFHLENBQUMsVUFBVSxDQUFDLE1BQU0sQ0FBQyxHQUFHLENBQUMsQ0FBQztvQkFBQyxHQUFHLEdBQUcsTUFBTSxDQUFDLEdBQUcsQ0FBQTtnQkFDaEQsRUFBRSxDQUFDLENBQUMsS0FBSyxDQUFDLGFBQWEsQ0FBQyxNQUFNLENBQUMsS0FBSyxDQUFDLENBQUM7b0JBQUMsS0FBSyxHQUFHLE1BQU0sQ0FBQyxLQUFLLENBQUE7Z0JBQzNELE1BQU0sQ0FBQyxJQUFJLFlBQUssQ0FBQyxLQUFLLEVBQUUsR0FBRyxDQUFDLENBQUE7WUFDOUIsQ0FBQyxDQUFDLENBQUE7UUFDTixDQUFDO1FBRUQsTUFBTSxDQUFDLE1BQU0sQ0FBQyxTQUFTLEVBQUUsQ0FBQyxRQUFRLENBQUMsR0FBRyxFQUFFLENBQ3RDLGdCQUFnQixDQUFDLEdBQUcsQ0FBQyxLQUFLLENBQUMsRUFBRSxDQUMzQixNQUFNLENBQUMsMEJBQTBCLENBQUMsYUFBSyxFQUFFLEtBQUssRUFBRSxVQUM5QyxHQUFxQjtZQUVyQixNQUFNLENBQUMsTUFBTSxDQUFDLG9CQUFvQixDQUNoQyxHQUFHLENBQUMsS0FBSyxFQUNULHlCQUFXLENBQUMsR0FBRyxDQUFDLEtBQUssRUFBRSxRQUFRLENBQUMsQ0FDakMsQ0FBQTtRQUNILENBQUMsQ0FBQyxDQUNILENBQ0YsQ0FBQTtJQUNILENBQUM7Q0FDRjtBQXJFRCx3Q0FxRUMiLCJzb3VyY2VzQ29udGVudCI6WyJpbXBvcnQgeyBDb21wb3NpdGVEaXNwb3NhYmxlLCBSYW5nZSwgVGV4dEVkaXRvciwgQnVmZmVyU2NhblJlc3VsdCB9IGZyb20gJ2F0b20nXG5pbXBvcnQgeyByZWdleCB9IGZyb20gJy4vcmVnZXgnXG5pbXBvcnQgeyBmb3JtYXRUYWJsZSB9IGZyb20gJy4vZm9ybWF0VGFibGUnXG5cbmV4cG9ydCBmdW5jdGlvbiBnZXRBbGxTZXR0aW5ncygpIHtcbiAgcmV0dXJuIGF0b20uY29uZmlnLmdldCgnbWFya2Rvd24tdGFibGUtZm9ybWF0dGVyJylcbn1cblxuZXhwb3J0IGNsYXNzIFRhYmxlRm9ybWF0dGVyIHtcbiAgcHJpdmF0ZSByZWFkb25seSBzdWJzY3JpcHRpb25zID0gbmV3IENvbXBvc2l0ZURpc3Bvc2FibGUoKVxuICBjb25zdHJ1Y3RvcigpIHtcbiAgICBhdG9tLndvcmtzcGFjZS5vYnNlcnZlVGV4dEVkaXRvcnMoZWRpdG9yID0+XG4gICAgICB0aGlzLnN1YnNjcmlwdGlvbnMuYWRkKFxuICAgICAgICBlZGl0b3IuZ2V0QnVmZmVyKCkub25XaWxsU2F2ZSgoKSA9PiB7XG4gICAgICAgICAgaWYgKGF0b20uY29uZmlnLmdldCgnbWFya2Rvd24tdGFibGUtZm9ybWF0dGVyLmZvcm1hdE9uU2F2ZScpKSB7XG4gICAgICAgICAgICB0aGlzLmZvcm1hdChlZGl0b3IsIHRydWUpXG4gICAgICAgICAgfVxuICAgICAgICB9KSxcbiAgICAgICksXG4gICAgKVxuICB9XG5cbiAgcHVibGljIGRlc3Ryb3koKSB7XG4gICAgdGhpcy5zdWJzY3JpcHRpb25zLmRpc3Bvc2UoKVxuICB9XG5cbiAgcHVibGljIGZvcm1hdChlZGl0b3I6IFRleHRFZGl0b3IsIGZvcmNlOiBib29sZWFuID0gZmFsc2UpIHtcbiAgICBsZXQgc2VsZWN0aW9uc1JhbmdlcyA9IGVkaXRvci5nZXRTZWxlY3RlZEJ1ZmZlclJhbmdlcygpXG4gICAgY29uc3Qgc2V0dGluZ3MgPSBnZXRBbGxTZXR0aW5ncygpXG5cbiAgICBjb25zdCBidWZmZXJSYW5nZSA9IGVkaXRvci5nZXRCdWZmZXIoKS5nZXRSYW5nZSgpXG4gICAgY29uc3Qgc2VsZWN0aW9uc1Jhbmdlc0VtcHR5ID0gc2VsZWN0aW9uc1Jhbmdlcy5ldmVyeShpID0+IGkuaXNFbXB0eSgpKVxuICAgIGlmIChcbiAgICAgICFzZXR0aW5ncy5tYXJrZG93bkdyYW1tYXJTY29wZXMuaW5jbHVkZXMoZWRpdG9yLmdldEdyYW1tYXIoKS5zY29wZU5hbWUpICYmXG4gICAgICBzZWxlY3Rpb25zUmFuZ2VzRW1wdHlcbiAgICApIHtcbiAgICAgIHJldHVybiB1bmRlZmluZWRcbiAgICB9XG4gICAgaWYgKGZvcmNlIHx8IChzZWxlY3Rpb25zUmFuZ2VzRW1wdHkgJiYgc2V0dGluZ3MuYXV0b1NlbGVjdEVudGlyZURvY3VtZW50KSkge1xuICAgICAgc2VsZWN0aW9uc1JhbmdlcyA9IFtidWZmZXJSYW5nZV1cbiAgICB9IGVsc2Uge1xuICAgICAgc2VsZWN0aW9uc1JhbmdlcyA9IHNlbGVjdGlvbnNSYW5nZXNcbiAgICAgICAgLmZpbHRlcihcbiAgICAgICAgICBzcmFuZ2UgPT4gIShzcmFuZ2UuaXNFbXB0eSgpICYmIHNldHRpbmdzLmF1dG9TZWxlY3RFbnRpcmVEb2N1bWVudCksXG4gICAgICAgIClcbiAgICAgICAgLm1hcChzcmFuZ2UgPT4ge1xuICAgICAgICAgIGxldCB7IHN0YXJ0IH0gPSBidWZmZXJSYW5nZVxuICAgICAgICAgIGxldCB7IGVuZCB9ID0gYnVmZmVyUmFuZ2VcbiAgICAgICAgICBlZGl0b3Iuc2NhbkluQnVmZmVyUmFuZ2UoXG4gICAgICAgICAgICAvXiQvbSxcbiAgICAgICAgICAgIG5ldyBSYW5nZShzcmFuZ2Uuc3RhcnQsIGJ1ZmZlclJhbmdlLmVuZCksXG4gICAgICAgICAgICAoeyByYW5nZSB9KSA9PiAoZW5kID0gcmFuZ2Uuc3RhcnQpLFxuICAgICAgICAgIClcbiAgICAgICAgICBlZGl0b3IuYmFja3dhcmRzU2NhbkluQnVmZmVyUmFuZ2UoXG4gICAgICAgICAgICAvXiQvbSxcbiAgICAgICAgICAgIG5ldyBSYW5nZShidWZmZXJSYW5nZS5zdGFydCwgc3JhbmdlLmVuZCksXG4gICAgICAgICAgICAoeyByYW5nZSB9KSA9PiAoc3RhcnQgPSByYW5nZS5zdGFydCksXG4gICAgICAgICAgKVxuICAgICAgICAgIGlmIChlbmQuaXNMZXNzVGhhbihzcmFuZ2UuZW5kKSkgZW5kID0gc3JhbmdlLmVuZFxuICAgICAgICAgIGlmIChzdGFydC5pc0dyZWF0ZXJUaGFuKHNyYW5nZS5zdGFydCkpIHN0YXJ0ID0gc3JhbmdlLnN0YXJ0XG4gICAgICAgICAgcmV0dXJuIG5ldyBSYW5nZShzdGFydCwgZW5kKVxuICAgICAgICB9KVxuICAgIH1cblxuICAgIHJldHVybiBlZGl0b3IuZ2V0QnVmZmVyKCkudHJhbnNhY3QoKCkgPT5cbiAgICAgIHNlbGVjdGlvbnNSYW5nZXMubWFwKHJhbmdlID0+XG4gICAgICAgIGVkaXRvci5iYWNrd2FyZHNTY2FuSW5CdWZmZXJSYW5nZShyZWdleCwgcmFuZ2UsIGZ1bmN0aW9uKFxuICAgICAgICAgIG9iajogQnVmZmVyU2NhblJlc3VsdCxcbiAgICAgICAgKSB7XG4gICAgICAgICAgcmV0dXJuIGVkaXRvci5zZXRUZXh0SW5CdWZmZXJSYW5nZShcbiAgICAgICAgICAgIG9iai5yYW5nZSxcbiAgICAgICAgICAgIGZvcm1hdFRhYmxlKG9iai5tYXRjaCwgc2V0dGluZ3MpLFxuICAgICAgICAgIClcbiAgICAgICAgfSksXG4gICAgICApLFxuICAgIClcbiAgfVxufVxuIl19 -------------------------------------------------------------------------------- /menus/markdown-table-formatter.cson: -------------------------------------------------------------------------------- 1 | # See https://atom.io/docs/latest/creating-a-package#menus for more details 2 | 'menu': [ 3 | { 4 | 'label': 'Packages' 5 | 'submenu': [ 6 | 'label': 'Markdown Table Formatter' 7 | 'submenu': [ 8 | { 'label': 'Format', 'command': 'markdown-table-formatter:format' } 9 | ] 10 | ] 11 | } 12 | ] 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "markdown-table-formatter", 3 | "main": "./lib/markdown-table-formatter", 4 | "version": "2.9.2", 5 | "description": "A simple markdown plugin to format tables.", 6 | "repository": "https://github.com/fcrespo82/atom-markdown-table-formatter", 7 | "license": "MIT", 8 | "engines": { 9 | "atom": ">=1.25.0 <2.0.0" 10 | }, 11 | "atomTestRunner": "./node_modules/atom-ts-spec-runner/runner.js", 12 | "scripts": { 13 | "build": "tsc -p .", 14 | "prettier": "prettier --write '**/*.ts'", 15 | "prettier-check": "prettier -l '**/*.ts'", 16 | "typecheck": "tsc --noEmit -p . && tsc --noEmit -p ./spec", 17 | "lint": "tslint --project . && tslint --project ./spec", 18 | "test": "npm run typecheck && npm run lint && npm run prettier-check" 19 | }, 20 | "dependencies": { 21 | "wcwidth": "1.*", 22 | "xregexp": "^4.1.1" 23 | }, 24 | "devDependencies": { 25 | "@types/atom": "~1.25.0", 26 | "@types/chai": "^4.1.2", 27 | "@types/mocha": "^2.2.48", 28 | "@types/wcwidth": "^1.0.0", 29 | "@types/xregexp": "^3.0.29", 30 | "atom-haskell-tslint-rules": "^0.2.2", 31 | "atom-ts-spec-runner": "^1.0.0", 32 | "chai": "^4.1.2", 33 | "mocha": "^5.0.4", 34 | "prettier": "^1.11.1", 35 | "ts-node": "^5.0.1", 36 | "tslint": "^5.9.1", 37 | "typescript": "^2.7.2", 38 | "xregexp": "^4.1.1" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fcrespo82/atom-markdown-table-formatter/9a8d7cdac38129176eab56eb8ac3b0045731fcb4/settings.png -------------------------------------------------------------------------------- /spec/atom.d.ts: -------------------------------------------------------------------------------- 1 | export {} 2 | declare module 'atom' { 3 | interface TextBuffer { 4 | setPreferredLineEnding(e: string): void 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /spec/duplicate-tests.spec.ts: -------------------------------------------------------------------------------- 1 | import { loadFixture } from './utils' 2 | import { expect } from 'chai' 3 | 4 | describe('Duplicate tests', () => { 5 | function test(name: string) { 6 | it(`do not exist on ${name}`, () => { 7 | const res = loadFixture(name) 8 | res.forEach((x, i) => { 9 | expect(res.findIndex(y => y.input === x.input)).to.be.equal( 10 | i, 11 | `duplicate test in ${name}[${i}]: "${x.input}"`, 12 | ) 13 | }) 14 | }) 15 | } 16 | test('test-tables') 17 | test('test-tables-pref-line-len') 18 | }) 19 | -------------------------------------------------------------------------------- /spec/editor.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai' 2 | import { TextEditor } from 'atom' 3 | import { 4 | testFormat, 5 | testSuite, 6 | loadPackage, 7 | settings, 8 | loadFixture, 9 | } from './utils' 10 | import nonTables = require('./tables/non-tables') 11 | import { sep } from 'path' 12 | 13 | const testTables = loadFixture('test-tables') 14 | 15 | describe('editor tests', function() { 16 | beforeEach(async () => { 17 | await loadPackage() 18 | }) 19 | 20 | const runEditorTests = function( 21 | grammar: string, 22 | fixture: string, 23 | scope: string, 24 | inopts: { 25 | addGrammar?: boolean 26 | selectBeforeTest?: boolean 27 | crlf?: '\r\n' | '\n' 28 | } = {}, 29 | ) { 30 | const defopts = { 31 | addGrammar: true, 32 | selectBeforeTest: false, 33 | crlf: '\n', 34 | } 35 | const opts = { ...inopts, ...defopts } 36 | 37 | return describe(`editor tests for ${grammar} with ${JSON.stringify( 38 | opts.crlf, 39 | )}`, function() { 40 | let editor: TextEditor 41 | 42 | function editorFormat(input: string) { 43 | editor.setText(input) 44 | if (opts.selectBeforeTest) { 45 | editor.setSelectedBufferRange(editor.getBuffer().getRange()) 46 | } 47 | atom.commands.dispatch( 48 | atom.views.getView(editor), 49 | 'markdown-table-formatter:format', 50 | ) 51 | return editor.getText() 52 | } 53 | 54 | function modCrLf(x: string) { 55 | return x.replace(/\n/g, opts.crlf) 56 | } 57 | 58 | const test = testFormat(editorFormat, modCrLf) 59 | 60 | beforeEach(async function() { 61 | await atom.packages.activatePackage(grammar) 62 | editor = (await atom.workspace.open( 63 | `${__dirname}${sep}fixtures${sep}${fixture}`, 64 | )) as TextEditor 65 | expect(editor).to.exist 66 | editor.getBuffer().setPreferredLineEnding(opts.crlf) 67 | if (opts.addGrammar) { 68 | expect( 69 | atom.commands.dispatch( 70 | atom.views.getView(editor), 71 | 'markdown-table-formatter:enable-for-current-scope', 72 | ), 73 | ).to.be.ok 74 | } 75 | }) 76 | 77 | it(`should ${ 78 | opts.addGrammar ? '' : 'NOT ' 79 | }have ${scope} in grammarScopes`, function() { 80 | if (opts.addGrammar) { 81 | return expect(settings('markdownGrammarScopes')).to.contain(scope) 82 | } else { 83 | return expect(settings('markdownGrammarScopes')).not.to.contain(scope) 84 | } 85 | }) 86 | 87 | testSuite(test) 88 | 89 | it("shouldn't try to format non-tables", () => 90 | nonTables.map((nonTable: string) => 91 | test({ input: nonTable, expected: nonTable }), 92 | )) 93 | 94 | describe('Tables at the end of document', function() { 95 | const modTest = testFormat(editorFormat, (text, rand) => 96 | modCrLf( 97 | nonTables[Math.floor(rand * nonTables.length)] + opts.crlf + text, 98 | ), 99 | ) 100 | testSuite(modTest) 101 | }) 102 | 103 | describe('Tables at the beginning of document', function() { 104 | const modTest = testFormat(editorFormat, (text, rand) => 105 | modCrLf( 106 | text + opts.crlf + nonTables[Math.floor(rand * nonTables.length)], 107 | ), 108 | ) 109 | testSuite(modTest) 110 | }) 111 | 112 | it('should properly format large text', function() { 113 | let edtext = '' 114 | let expected = '' 115 | for (const table of testTables) { 116 | const text = 117 | nonTables[Math.floor(Math.random() * nonTables.length + 1)] || '' 118 | edtext += opts.crlf + text + opts.crlf + table.input 119 | expected += opts.crlf + text + opts.crlf + table.expected 120 | } 121 | test({ input: edtext, expected }) 122 | }) 123 | }) 124 | } 125 | 126 | runEditorTests('language-gfm', 'empty.md', 'source.gfm') 127 | runEditorTests('language-text', 'empty.text', 'text.plain.null-grammar') 128 | runEditorTests('language-gfm', 'empty.md', 'source.gfm', { crlf: '\r\n' }) 129 | runEditorTests('language-text', 'empty.text', 'text.plain.null-grammar', { 130 | crlf: '\r\n', 131 | }) 132 | runEditorTests('language-text', 'empty.text', 'text.plain.null-grammar', { 133 | addGrammar: false, 134 | selectBeforeTest: true, 135 | }) 136 | }) 137 | -------------------------------------------------------------------------------- /spec/fixtures/empty.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fcrespo82/atom-markdown-table-formatter/9a8d7cdac38129176eab56eb8ac3b0045731fcb4/spec/fixtures/empty.md -------------------------------------------------------------------------------- /spec/fixtures/empty.text: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fcrespo82/atom-markdown-table-formatter/9a8d7cdac38129176eab56eb8ac3b0045731fcb4/spec/fixtures/empty.text -------------------------------------------------------------------------------- /spec/fixtures/test-tables-pref-line-len.md: -------------------------------------------------------------------------------- 1 | 2 | # Input 3 | | aaa | aaa | aaa | aaa | aaa | aaaaaaa | 4 | | --- | --- | --- | --- | --- | --------------------------------------------------------- | 5 | | bbb | bbb | bbb | bbb | bbb | bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb | 6 | 7 | # Expected 8 | | aaa | aaa | aaa | aaa | aaa | aaaaaaa | 9 | |:----|:----|:----|:----|:----|:-----------------------------------------------| 10 | | bbb | bbb | bbb | bbb | bbb | bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb | 11 | 12 | # Input 13 | |Coffee|| 14 | |:---|:---| 15 | |Origin/Name|[prompt:Origin/Name]| 16 | |Brew method|[list:Brew Method|AeroPress|Chemex|Drip|Espresso|French Press|Pour Over|Other]| 17 | |Brewer|[prompt:Who brewed it?]| 18 | |Rating|[list:Rating|★☆☆|★★☆|★★★]| 19 | |Notes|[prompt:Notes]| 20 | 21 | # Expected 22 | | Coffee | | 23 | |:------------|:---------------------------------------------------------------| 24 | | Origin/Name | [prompt:Origin/Name] | 25 | | Brew method | [list:Brew Method|AeroPress|Chemex|Drip|Espresso|French Press|Pour Over|Other] | 26 | | Brewer | [prompt:Who brewed it?] | 27 | | Rating | [list:Rating|★☆☆|★★☆|★★★] | 28 | | Notes | [prompt:Notes] | 29 | -------------------------------------------------------------------------------- /spec/fixtures/test-tables.md: -------------------------------------------------------------------------------- 1 | 2 | # Input 3 | |Left|Center|Right| 4 | |:-|:-:|-:| 5 | |1|2|3| 6 | 7 | # Expected 8 | | Left | Center | Right | 9 | |:-----|:------:|------:| 10 | | 1 | 2 | 3 | 11 | 12 | # Input 13 | | h1 h1| h2 | h3 | 14 | |-|-|-| 15 | | data1 | data2 | data3 | 16 | 17 | # Expected 18 | | h1 h1 | h2 | h3 | 19 | |:------|:------|:------| 20 | | data1 | data2 | data3 | 21 | 22 | # Input 23 | h1 | h2 | h3 24 | -|-|- 25 | data-1 | data-2 | data-3 26 | 27 | # Expected 28 | | h1 | h2 | h3 | 29 | |:-------|:-------|:-------| 30 | | data-1 | data-2 | data-3 | 31 | 32 | # Input 33 | -|-|- 34 | a|b|c 35 | 36 | # Expected 37 | |:--|:--|:--| 38 | | a | b | c | 39 | 40 | # Input 41 | | Header 1 | Header 2 | Header 3 | 42 | |----|---|-| 43 | | data1a | Data is longer than header | 1 | 44 | | d1b | add a cell| 45 | |lorem|ipsum|3| 46 | | | empty outside cells 47 | | skip| | 5 | 48 | | six | Morbi purus | 6 | 49 | 50 | # Expected 51 | | Header 1 | Header 2 | Header 3 | 52 | |:---------|:---------------------------|:---------| 53 | | data1a | Data is longer than header | 1 | 54 | | d1b | add a cell | | 55 | | lorem | ipsum | 3 | 56 | | | empty outside cells | | 57 | | skip | | 5 | 58 | | six | Morbi purus | 6 | 59 | 60 | # Input 61 | |teste-3| 62 | |---| 63 | |other| 64 | 65 | # Expected 66 | | teste-3 | 67 | |:--------| 68 | | other | 69 | 70 | # Input 71 | |outro 72 | |- 73 | |teste 74 | 75 | # Expected 76 | | outro | 77 | |:------| 78 | | teste | 79 | 80 | # Input 81 | outro| 82 | :-:| 83 | teste| 84 | 85 | # Expected 86 | | outro | 87 | |:-----:| 88 | | teste | 89 | 90 | # Input 91 | First Header | Second Header 92 | ------------- | ------------- 93 | Content Cell | Content Cell 94 | Content Cell | Content Cell 95 | 96 | # Expected 97 | | First Header | Second Header | 98 | |:-------------|:--------------| 99 | | Content Cell | Content Cell | 100 | | Content Cell | Content Cell | 101 | 102 | # Input 103 | | First Header | Second Header | 104 | | ------------- | ------------- | 105 | | Content Cell | Content Cell | 106 | | Content Cell | Content Cell | 107 | 108 | # Expected 109 | | First Header | Second Header | 110 | |:-------------|:--------------| 111 | | Content Cell | Content Cell | 112 | | Content Cell | Content Cell | 113 | 114 | # Input 115 | | Item | Value | 116 | | --------- | -----:| 117 | | Computer | $1600 | 118 | | Phone | $12 | 119 | | Pipe | $1 | 120 | 121 | # Expected 122 | | Item | Value | 123 | |:---------|------:| 124 | | Computer | $1600 | 125 | | Phone | $12 | 126 | | Pipe | $1 | 127 | 128 | # Input 129 | | Function name | Description | 130 | | ------------- | ------------------------------ | 131 | | `help()` | Display the help window. | 132 | | `destroy()` | **Destroy your computer!** | 133 | 134 | # Expected 135 | | Function name | Description | 136 | |:--------------|:---------------------------| 137 | | `help()` | Display the help window. | 138 | | `destroy()` | **Destroy your computer!** | 139 | 140 | # Input 141 | | Left align | Right align | Center align | 142 | |:-----------|------------:|:------------:| 143 | | This | This | This | 144 | | column | column | column | 145 | | will | will | will | 146 | | be | be | be | 147 | | left | right | center | 148 | | aligned | aligned | aligned | 149 | 150 | # Expected 151 | | Left align | Right align | Center align | 152 | |:-----------|------------:|:------------:| 153 | | This | This | This | 154 | | column | column | column | 155 | | will | will | will | 156 | | be | be | be | 157 | | left | right | center | 158 | | aligned | aligned | aligned | 159 | 160 | # Input 161 | |Left align|Right align|Center align| 162 | |:---------|----------:|:----------:| 163 | |This|This|This| 164 | |column|column|column| 165 | |will|will|will| 166 | |be|be|be| 167 | |left|right|center| 168 | |aligned|aligned|aligned| 169 | 170 | # Expected 171 | | Left align | Right align | Center align | 172 | |:-----------|------------:|:------------:| 173 | | This | This | This | 174 | | column | column | column | 175 | | will | will | will | 176 | | be | be | be | 177 | | left | right | center | 178 | | aligned | aligned | aligned | 179 | 180 | # Input 181 | | Name | Description | 182 | | ------------- | ----------- | 183 | | Help | Display the help window.| 184 | | Close | Closes a window | 185 | 186 | # Expected 187 | | Name | Description | 188 | |:------|:-------------------------| 189 | | Help | Display the help window. | 190 | | Close | Closes a window | 191 | 192 | # Input 193 | | Name | Description | 194 | | ------------- | ----------- | 195 | | Help | ~~Display the~~ help window.| 196 | | Close | _Closes_ a window | 197 | 198 | # Expected 199 | | Name | Description | 200 | |:------|:-----------------------------| 201 | | Help | ~~Display the~~ help window. | 202 | | Close | _Closes_ a window | 203 | 204 | # Input 205 | | Left-Aligned | Center Aligned | Right Aligned | 206 | | :------------ |:---------------:| -----:| 207 | | col 3 is | some wordy text | $1600 | 208 | | col 2 is | centered | $12 | 209 | | zebra stripes | are neat | $1 | 210 | 211 | # Expected 212 | | Left-Aligned | Center Aligned | Right Aligned | 213 | |:--------------|:---------------:|--------------:| 214 | | col 3 is | some wordy text | $1600 | 215 | | col 2 is | centered | $12 | 216 | | zebra stripes | are neat | $1 | 217 | 218 | # Input 219 | | First Header | Second Header | Third Header | 220 | | :----------- | :-----------: | -------------------: | 221 | | First row | Data | Very long data entry | 222 | | Second row | **Cell** | *Cell* | 223 | 224 | # Expected 225 | | First Header | Second Header | Third Header | 226 | |:-------------|:-------------:|---------------------:| 227 | | First row | Data | Very long data entry | 228 | | Second row | **Cell** | *Cell* | 229 | 230 | # Input 231 | |Coffee|| 232 | |:---|:---| 233 | |Origin/Name|[prompt:Origin/Name]| 234 | |Brew method|[list:Brew Method|AeroPress|Chemex|Drip|Espresso|French Press|Pour Over|Other]| 235 | |Brewer|[prompt:Who brewed it?]| 236 | |Rating|[list:Rating|★☆☆|★★☆|★★★]| 237 | |Notes|[prompt:Notes]| 238 | 239 | # Expected 240 | | Coffee | | 241 | |:------------|:-------------------------------------------------------------------------------| 242 | | Origin/Name | [prompt:Origin/Name] | 243 | | Brew method | [list:Brew Method|AeroPress|Chemex|Drip|Espresso|French Press|Pour Over|Other] | 244 | | Brewer | [prompt:Who brewed it?] | 245 | | Rating | [list:Rating|★☆☆|★★☆|★★★] | 246 | | Notes | [prompt:Notes] | 247 | 248 | # Input 249 | code|描述|详细解释 250 | :-|:-|:- 251 | 200|成功|成功 252 | 400|错误请求|该请求是无效的,详细的错误信息会说明原因 253 | 401|验证错误|验证失败,详细的错误信息会说明原因 254 | 403|被拒绝|被拒绝调用,详细的错误信息会说明原因 255 | 404|无法找到|资源不存在 256 | 429|过多的请求|超出了调用频率限制,详细的错误信息会说明原因 257 | 500|内部服务错误|服务器内部出错了,请联系我们尽快解决问题 258 | 504|内部服务响应超时|服务器在运行,本次请求响应超时,请稍后重试 259 | 260 | # Expected 261 | | code | 描述 | 详细解释 | 262 | |:-----|:-----------------|:---------------------------------------------| 263 | | 200 | 成功 | 成功 | 264 | | 400 | 错误请求 | 该请求是无效的,详细的错误信息会说明原因 | 265 | | 401 | 验证错误 | 验证失败,详细的错误信息会说明原因 | 266 | | 403 | 被拒绝 | 被拒绝调用,详细的错误信息会说明原因 | 267 | | 404 | 无法找到 | 资源不存在 | 268 | | 429 | 过多的请求 | 超出了调用频率限制,详细的错误信息会说明原因 | 269 | | 500 | 内部服务错误 | 服务器内部出错了,请联系我们尽快解决问题 | 270 | | 504 | 内部服务响应超时 | 服务器在运行,本次请求响应超时,请稍后重试 | 271 | 272 | # Input 273 | заголовок|таблицы 274 | -|- 275 | тело|таблицы 276 | продолжение|тела 277 | 278 | # Expected 279 | | заголовок | таблицы | 280 | |:------------|:--------| 281 | | тело | таблицы | 282 | | продолжение | тела | 283 | 284 | # Input (regression test for #16) 285 | | test | table | with| many | columns | 286 | |-|-|-|-|- 287 | |asd 288 | |dsa 289 | 290 | # Expected 291 | | test | table | with | many | columns | 292 | |:-----|:------|:-----|:-----|:--------| 293 | | asd | | | | | 294 | | dsa | | | | | 295 | 296 | # Input (regression test for #17) 297 | | **Name** | **Unicode** | **Unicode Name** | **ASCII Dec** | 298 | |:----------------|:--------------------------------------|:------------------------------------------------------|:---------------| 299 | | Digits | The code points U+0030 through U+0039 | DIGIT ZERO through DIGIT NINE | 48 through 57 | 300 | | CA­PI­TAL-LETTERS | The code points U+0041 through U+005A | LATIN CA­PI­TAL LET­TER A through LATIN CA­PI­TAL LET­TER Z | 65 through 90 | 301 | | SMALL-LETTERS | The code points U+0061 through U+007A | LATIN SMALL LET­TER A through LATIN SMALL LET­TER Z | 97 through 122 | 302 | 303 | # Expected 304 | | **Name** | **Unicode** | **Unicode Name** | **ASCII Dec** | 305 | |:----------------|:--------------------------------------|:------------------------------------------------------|:---------------| 306 | | Digits | The code points U+0030 through U+0039 | DIGIT ZERO through DIGIT NINE | 48 through 57 | 307 | | CA­PI­TAL-LETTERS | The code points U+0041 through U+005A | LATIN CA­PI­TAL LET­TER A through LATIN CA­PI­TAL LET­TER Z | 65 through 90 | 308 | | SMALL-LETTERS | The code points U+0061 through U+007A | LATIN SMALL LET­TER A through LATIN SMALL LET­TER Z | 97 through 122 | 309 | 310 | # Input (regression test for #24) 311 | | 312 | |-|-|-|-| 313 | |table | with |empty |header| 314 | 315 | # Expected 316 | | | | | | 317 | |:------|:-----|:------|:-------| 318 | | table | with | empty | header | 319 | -------------------------------------------------------------------------------- /spec/format.spec.ts: -------------------------------------------------------------------------------- 1 | import { formatTable } from '../src/formatTable' 2 | import { testFormat, testSuite, loadPackage } from './utils' 3 | import { regex } from '../src/regex' 4 | 5 | describe('format tests', function() { 6 | beforeEach(async () => { 7 | await loadPackage() 8 | }) 9 | 10 | const test = testFormat(function(input: string) { 11 | regex.lastIndex = 0 12 | const r = regex.exec(input) 13 | if (r) return formatTable(r) 14 | else return 'No regex match' 15 | }) 16 | 17 | describe('default options', function() { 18 | beforeEach(function() { 19 | atom.config.set('markdown-table-formatter.spacePadding', 1) 20 | atom.config.set('markdown-table-formatter.keepFirstAndLastPipes', true) 21 | atom.config.set( 22 | 'markdown-table-formatter.defaultTableJustification', 23 | 'Left', 24 | ) 25 | }) 26 | 27 | testSuite(test) 28 | }) 29 | }) 30 | -------------------------------------------------------------------------------- /spec/global.d.ts: -------------------------------------------------------------------------------- 1 | interface ObjectConstructor { 2 | keys(o: { [Key in K]: any }): K[] 3 | entries(o: { [Key in K]: T }): [K, T][] 4 | } 5 | -------------------------------------------------------------------------------- /spec/markdown-table-formatter.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai' 2 | import { loadPackage } from './utils' 3 | 4 | describe('package', function() { 5 | it('should activate', async () => { 6 | await loadPackage() 7 | expect(atom.packages.isPackageActive('markdown-table-formatter')).to.equal( 8 | true, 9 | ) 10 | }) 11 | }) 12 | -------------------------------------------------------------------------------- /spec/regex.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai' 2 | import nonTables = require('./tables/non-tables') 3 | import { regex } from '../src/regex' 4 | import { loadFixture } from './utils' 5 | 6 | const testTables = loadFixture('test-tables') 7 | 8 | describe('regex tests', function() { 9 | it('should match the regex', function() { 10 | for (const table of testTables) { 11 | table.input.match(regex) 12 | expect(table.input).to.match(regex) 13 | } 14 | }) 15 | 16 | it('should NOT match the regex', function() { 17 | for (const table of nonTables) { 18 | table.match(regex) 19 | expect(table).not.to.match(regex) 20 | } 21 | }) 22 | }) 23 | -------------------------------------------------------------------------------- /spec/tables/non-tables.ts: -------------------------------------------------------------------------------- 1 | export = [ 2 | `\ 3 | aaa|dafdas|adfas 4 | ----\t| ----\t| --- 5 | asdf|asadf|sdfas 6 | \ 7 | `, 8 | 9 | `\ 10 | Here’s the original Coffee Log after decoding: 11 | dayone://post?entry=#coffee #log 12 | If you wish, you can add a leading and tailing pipe to each line of the table. 13 | Use the form that you like. As an illustration, this will give the same result as above: 14 | Note: A table need at least one pipe on each line for Markdown Extra to parse 15 | it correctly. This means that the only way to create a one-column table is to 16 | add a leading or a tailing pipe, or both of them, to each line. 17 | The align HTML attribute is applied to each cell of the concerned column. 18 | You can apply span-level formatting to the content of each cell using regular 19 | Markdown syntax: 20 | First, let’s talk about what Markdown tables look like. The full explanation 21 | can be found at either the PHP Markdown Extra or MultiMarkdown sites, but the 22 | nutshell version is this: 23 | The pipe (|) characters separate the columns. The alignment of each column is 24 | determined by the placement of the colons in the separator line. A colon at the 25 | left end only (or no colon at all) means left aligned; a colon at the right end 26 | only means right aligned; a colon at each end means centered. 27 | It’s nice to have the text version of the table spaced as I have it above with 28 | the pipes stacked on top of one another and the text aligned the way the separator 29 | line says. It’s very time consuming to type it in that way, though, so my tables 30 | tend to look more like this: 31 | which is easy to type, but hard to read and edit. So the first command I made 32 | is a Python script that takes an ugly text table and makes it pretty. Here’s the code.\ 33 | `, 34 | 35 | `\ 36 | 1 #!/usr/bin/python 37 | 2 38 | 3 import sys 39 | 4 40 | 5 def just(string, type, n): 41 | 6 "Justify a string to length n according to type." 42 | 7 43 | 8 if type == '::': 44 | 9 return string.center(n) 45 | 10 elif type == '-:': 46 | 11 return string.rjust(n) 47 | 12 elif type == ':-': 48 | 13 return string.ljust(n) 49 | 14 else: 50 | 15 return string 51 | 16 52 | 17 53 | 18 def normtable(text): 54 | 19 "Aligns the vertical bars in a text table." 55 | 20 56 | 21 # Start by turning the text into a list of lines. 57 | 22 lines = text.splitlines() 58 | 23 rows = len(lines) 59 | 24 60 | 25 # Figure out the cell formatting. 61 | 26 # First, find the separator line. 62 | 27 for i in range(rows): 63 | 28 if set(lines[i]).issubset('|:.-'): 64 | 29 formatline = lines[i] 65 | 30 formatrow = i 66 | 31 break 67 | 32 68 | 33 # Delete the separator line from the content. 69 | 34 del lines[formatrow] 70 | 35 71 | 36 # Determine how each column is to be justified. 72 | 37 formatline = formatline.strip('| ') 73 | 38 fstrings = formatline.split('|') 74 | 39 justify = [] 75 | 40 for cell in fstrings: 76 | 41 ends = cell[0] + cell[-1] 77 | 42 if ends == '::': 78 | 43 justify.append('::') 79 | 44 elif ends == '-:': 80 | 45 justify.append('-:') 81 | 46 else: 82 | 47 justify.append(':-') 83 | 48 84 | 49 # Assume the number of columns in the separator line is the number 85 | 50 # for the entire table. 86 | 51 columns = len(justify) 87 | 52 88 | 53 # Extract the content into a matrix. 89 | 54 content = [] 90 | 55 for line in lines: 91 | 56 line = line.strip('| ') 92 | 57 cells = line.split('|') 93 | 58 # Put exactly one space at each end as "bumpers." 94 | 59 linecontent = [ ' ' + x.strip() + ' ' for x in cells ] 95 | 60 content.append(linecontent) 96 | 61 97 | 62 # Append cells to rows that don't have enough. 98 | 63 rows = len(content) 99 | 64 for i in range(rows): 100 | 65 while len(content[i]) < columns: 101 | 66 content[i].append('') 102 | 67 103 | 68 # Get the width of the content in each column. The minimum width will 104 | 69 # be 2, because that's the shortest length of a formatting string and 105 | 70 # because that matches an empty column with "bumper" spaces. 106 | 71 widths = [2] * columns 107 | 72 for row in content: 108 | 73 for i in range(columns): 109 | 74 widths[i] = max(len(row[i]), widths[i]) 110 | 75 111 | 76 # Add whitespace to make all the columns the same width and 112 | 77 formatted = [] 113 | 78 for row in content: 114 | 79 formatted.append('|' + '|'.join([ just(s, t, n) for (s, t, n) in zip(row, justify, widths) ]) + '|') 115 | 80 116 | 81 # Recreate the format line with the appropriate column widths. 117 | 82 formatline = '|' + '|'.join([ s[0] + '-'*(n-2) + s[-1] for (s, n) in zip(justify, widths) ]) + '|' 118 | 83 119 | 84 # Insert the formatline back into the table. 120 | 85 formatted.insert(formatrow, formatline) 121 | 86 122 | 87 # Return the formatted table. 123 | 88 return '\\n'.join(formatted) 124 | 89 125 | 90 126 | 91 # Read the input, process, and print. 127 | 92 unformatted = sys.stdin.read() 128 | 93 print normtable(unformatted)\ 129 | `, 130 | ] 131 | -------------------------------------------------------------------------------- /spec/tables/test-tables-default.ts: -------------------------------------------------------------------------------- 1 | export = { 2 | Left: [ 3 | { 4 | input: `\ 5 | |First Header|Second Header|Third Header| 6 | |-|-:|:-:| 7 | |Content|Content|Content| 8 | |Content|Content|Content| 9 | \ 10 | `, 11 | expected: `\ 12 | | First Header | Second Header | Third Header | 13 | |:-------------|--------------:|:------------:| 14 | | Content | Content | Content | 15 | | Content | Content | Content | 16 | \ 17 | `, 18 | }, 19 | ], 20 | Center: [ 21 | { 22 | input: `\ 23 | |First Header|Second Header|Third Header| 24 | |-|:-|::| 25 | |Content|Content|Content| 26 | |Content|Content|Content| 27 | \ 28 | `, 29 | expected: `\ 30 | | First Header | Second Header | Third Header | 31 | |:------------:|:--------------|:------------:| 32 | | Content | Content | Content | 33 | | Content | Content | Content | 34 | \ 35 | `, 36 | }, 37 | ], 38 | Right: [ 39 | { 40 | input: `\ 41 | |First Header|Second Header| 42 | |-|:-| 43 | |Content|Content| 44 | |Content|Content| 45 | \ 46 | `, 47 | expected: `\ 48 | | First Header | Second Header | 49 | |-------------:|:--------------| 50 | | Content | Content | 51 | | Content | Content | 52 | \ 53 | `, 54 | }, 55 | ], 56 | } 57 | -------------------------------------------------------------------------------- /spec/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "extends": "../tsconfig.json", 4 | "include": ["./**.ts", "../src/**.ts", "../src/atom.d.ts"], 5 | "exclude": ["../node_modules"] 6 | } 7 | -------------------------------------------------------------------------------- /spec/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tslint.json", 3 | "rules": { 4 | "no-unused-expression": false 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /spec/utils.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai' 2 | import testTablesDefault = require('./tables/test-tables-default') 3 | import { join } from 'path' 4 | import { readFileSync } from 'fs' 5 | 6 | const pkg = join(__dirname, '..') 7 | const testTables = loadFixture('test-tables') 8 | const testTablesPrefLineLen = loadFixture('test-tables-pref-line-len') 9 | 10 | export interface TestData { 11 | input: string 12 | expected: string 13 | } 14 | 15 | export function testFormat( 16 | formatter: (x: string) => string, 17 | modifier?: (x: string, rand: number) => string, 18 | ) { 19 | return function({ input, expected }: TestData) { 20 | if (modifier === undefined) { 21 | modifier = x => x 22 | } 23 | const rand = Math.random() 24 | expect(formatter(modifier(input, rand))).to.equal(modifier(expected, rand)) 25 | } 26 | } 27 | 28 | function testSuiteSingle(test: (arg: TestData) => void, tbls: TestData[]) { 29 | it('should properly format these tables', () => tbls.forEach(test)) 30 | } 31 | 32 | export function testSuite(test: (arg: TestData) => void) { 33 | testSuiteSingle(test, testTables) 34 | 35 | for (const just of Object.keys(testTablesDefault)) { 36 | const tbls = testTablesDefault[just] 37 | describe(`Default ${just} justification tests`, function() { 38 | beforeEach(() => 39 | atom.config.set( 40 | 'markdown-table-formatter.defaultTableJustification', 41 | just, 42 | ), 43 | ) 44 | testSuiteSingle(test, tbls) 45 | afterEach(() => { 46 | atom.config.unset('markdown-table-formatter.defaultTableJustification') 47 | }) 48 | }) 49 | } 50 | 51 | describe('limitLastColumnPadding tests', function() { 52 | beforeEach(function() { 53 | atom.config.set('markdown-table-formatter.limitLastColumnPadding', true) 54 | }) 55 | 56 | testSuiteSingle(test, testTablesPrefLineLen) 57 | 58 | afterEach(function() { 59 | atom.config.set('markdown-table-formatter.limitLastColumnPadding', false) 60 | }) 61 | }) 62 | } 63 | 64 | export async function loadPackage() { 65 | await atom.packages.activatePackage(pkg) 66 | } 67 | 68 | export function settings(name: string) { 69 | return atom.config.get(`markdown-table-formatter.${name}`) 70 | } 71 | 72 | export function loadFixture(name: string) { 73 | const fixture = join(__dirname, `fixtures/${name}.md`) 74 | const data = readFileSync(fixture, { encoding: 'utf8' }) 75 | 76 | return data 77 | .split(/\n# Input*[^\n]*\n/g) 78 | .slice(1) 79 | .map(t => { 80 | const [input, expected] = t.split(/\n# Expected*[^\n]*\n/g) 81 | expect(input).to.exist 82 | expect(expected).to.exist 83 | return { input, expected } 84 | }) 85 | } 86 | -------------------------------------------------------------------------------- /src/atom.d.ts: -------------------------------------------------------------------------------- 1 | export {} 2 | declare module 'atom' { 3 | interface ConfigValues { 4 | 'markdown-table-formatter': { 5 | autoSelectEntireDocument: boolean 6 | spacePadding: number 7 | keepFirstAndLastPipes: boolean 8 | formatOnSave: boolean 9 | defaultTableJustification: 'Left' | 'Right' | 'Center' 10 | markdownGrammarScopes: string[] 11 | limitLastColumnPadding: boolean 12 | } 13 | 'markdown-table-formatter.autoSelectEntireDocument': boolean 14 | 'markdown-table-formatter.spacePadding': number 15 | 'markdown-table-formatter.keepFirstAndLastPipes': boolean 16 | 'markdown-table-formatter.formatOnSave': boolean 17 | 'markdown-table-formatter.defaultTableJustification': 18 | | 'Left' 19 | | 'Right' 20 | | 'Center' 21 | 'markdown-table-formatter.markdownGrammarScopes': string[] 22 | 'markdown-table-formatter.limitLastColumnPadding': boolean 23 | } 24 | interface Config { 25 | get( 26 | keyPath: T, 27 | options?: { 28 | sources?: string[] 29 | excludeSources?: string[] 30 | scope?: string[] | ScopeDescriptor 31 | }, 32 | ): ConfigValues[T] 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | export const config = { 2 | formatOnSave: { 3 | type: 'boolean', 4 | default: true, 5 | description: 'Format tables when document is saved?', 6 | }, 7 | autoSelectEntireDocument: { 8 | type: 'boolean', 9 | default: true, 10 | description: 'Select entire document if selection is empty', 11 | }, 12 | spacePadding: { 13 | type: 'integer', 14 | default: 1, 15 | description: 16 | 'How many spaces between left and right of each column content', 17 | }, 18 | keepFirstAndLastPipes: { 19 | type: 'boolean', 20 | default: true, 21 | description: `Keep first and last pipes "|" in table formatting. \ 22 | Tables are easier to format when pipes are kept`, 23 | }, 24 | defaultTableJustification: { 25 | type: 'string', 26 | default: 'Left', 27 | enum: ['Left', 'Center', 'Right'], 28 | description: `Defines the default justification for tables that have only a \ 29 | \'-\' on the formatting line`, 30 | }, 31 | markdownGrammarScopes: { 32 | type: 'array', 33 | default: ['source.gfm'], 34 | description: `File grammar scopes that will be considered Markdown by this package (comma-separated). \ 35 | Run \'Markdown Table Formatter: Enable For Current Scope\' command to \ 36 | add current editor grammar to this setting.`, 37 | items: { 38 | type: 'string', 39 | }, 40 | }, 41 | limitLastColumnPadding: { 42 | type: 'boolean', 43 | default: false, 44 | description: `Do not pad the last column to more than your editor\'s \ 45 | preferredLineLength setting.`, 46 | }, 47 | } 48 | -------------------------------------------------------------------------------- /src/formatTable.ts: -------------------------------------------------------------------------------- 1 | import { getAllSettings } from './table-formatter' 2 | import wcswidth = require('wcwidth') 3 | 4 | function swidth(str: string) { 5 | // zero-width Unicode characters that we should ignore for 6 | // purposes of computing string "display" width 7 | const zwcrx = /[\u200B-\u200D\uFEFF\u00AD]/g 8 | const match = str.match(zwcrx) 9 | return wcswidth(str) - (match ? match.length : 0) 10 | } 11 | 12 | function padding(len: number, str: string = ' ') { 13 | return str.repeat(len) 14 | } 15 | const stripTailPipes = (str: string) => str.trim().replace(/(^\||\|$)/g, '') 16 | const splitCells = (str: string) => str.split('|') 17 | const addTailPipes = (str: string) => `|${str}|` 18 | const joinCells = (arr: string[]) => arr.join('|') 19 | 20 | const tableJustMap = { 21 | Left: ':-', 22 | Center: '::', 23 | Right: '-:', 24 | } 25 | 26 | export function formatTable( 27 | text: RegExpExecArray, 28 | settings = getAllSettings(), 29 | ) { 30 | const addTailPipesIfNeeded = settings.keepFirstAndLastPipes 31 | ? addTailPipes 32 | : (x: string) => x 33 | 34 | let formatline = text[2].trim() 35 | const headerline = text[1].trim() 36 | 37 | let formatrow: number 38 | let data: string 39 | if (headerline.length === 0) { 40 | formatrow = 0 41 | data = text[3] 42 | } else { 43 | formatrow = 1 44 | data = text[1] + text[3] 45 | } 46 | const lines = data.trim().split(/\r?\n/) 47 | 48 | const justify = splitCells(stripTailPipes(formatline)).map(cell => { 49 | const trimmed = cell.trim() 50 | const first = trimmed[0] 51 | const last = trimmed[trimmed.length - 1] 52 | const ends = (first || ':') + (last || '-') 53 | if (ends === '--') return tableJustMap[settings.defaultTableJustification] 54 | else return ends 55 | }) 56 | 57 | const columns = justify.length 58 | const colArr: undefined[] = Array.from(Array(columns)) 59 | 60 | const cellPadding = padding(settings.spacePadding) 61 | 62 | const content = lines.map(line => { 63 | const cells = splitCells(stripTailPipes(line)) 64 | if (columns - cells.length > 0) { 65 | // pad rows to have `columns` cells 66 | cells.push(...Array(columns - cells.length).fill('')) 67 | } else if (columns - cells.length < 0) { 68 | // put all extra content into last cell 69 | cells[columns - 1] = joinCells(cells.slice(columns - 1)) 70 | } 71 | return cells.map(cell => `${cellPadding}${cell.trim()}${cellPadding}`) 72 | }) 73 | 74 | const widths = colArr.map((_x, i) => 75 | Math.max(2, ...content.map(cells => swidth(cells[i]))), 76 | ) 77 | 78 | if (settings.limitLastColumnPadding) { 79 | const preferredLineLength = atom.config.get('editor.preferredLineLength') 80 | const sum = (arr: number[]) => arr.reduce((x, y) => x + y, 0) 81 | const wsum = sum(widths) 82 | if (wsum > preferredLineLength) { 83 | const prewsum = sum(widths.slice(0, -1)) 84 | widths[widths.length - 1] = Math.max( 85 | preferredLineLength - prewsum - widths.length - 1, 86 | 3, 87 | ) 88 | // Need at least :-- for github to recognize a column 89 | } 90 | } 91 | 92 | const just = function(str: string, col: number) { 93 | const length = Math.max(widths[col] - swidth(str), 0) 94 | switch (justify[col]) { 95 | case '::': 96 | return padding(length / 2) + str + padding((length + 1) / 2) 97 | case '-:': 98 | return padding(length) + str 99 | case ':-': 100 | return str + padding(length) 101 | default: 102 | throw new Error(`Unknown column justification ${justify[col]}`) 103 | } 104 | } 105 | 106 | const formatted = content.map(cells => 107 | addTailPipesIfNeeded(joinCells(colArr.map((_x, i) => just(cells[i], i)))), 108 | ) 109 | 110 | formatline = addTailPipesIfNeeded( 111 | joinCells( 112 | colArr.map((_x, i) => { 113 | const [front, back] = justify[i] 114 | return front + padding(widths[i] - 2, '-') + back 115 | }), 116 | ), 117 | ) 118 | 119 | formatted.splice(formatrow, 0, formatline) 120 | 121 | return ( 122 | (formatrow === 0 && text[1] !== '' ? '\n' : '') + 123 | formatted.join('\n') + 124 | '\n' 125 | ) 126 | } 127 | -------------------------------------------------------------------------------- /src/markdown-table-formatter.ts: -------------------------------------------------------------------------------- 1 | import { TableFormatter } from './table-formatter' 2 | import { Disposable } from 'atom' 3 | export { config } from './config' 4 | 5 | let tableFormatter: TableFormatter 6 | let command: Disposable 7 | 8 | export function activate() { 9 | tableFormatter = new TableFormatter() 10 | // Register command to workspace 11 | command = atom.commands.add('atom-text-editor', { 12 | 'markdown-table-formatter:format': event => { 13 | const editor = event.currentTarget.getModel() 14 | tableFormatter.format(editor) 15 | }, 16 | 'markdown-table-formatter:enable-for-current-scope'(event) { 17 | const editor = event.currentTarget.getModel() 18 | const scope = editor.getGrammar().scopeName 19 | const key = 'markdown-table-formatter.markdownGrammarScopes' 20 | const current = atom.config.get(key) 21 | if (!scope) { 22 | atom.notifications.addError('Could not determine editor grammar scope') 23 | } else if (current.includes(scope)) { 24 | atom.notifications.addWarning(`${scope} already considered Markdown`) 25 | } else { 26 | atom.config.set(key, [...current, scope]) 27 | atom.notifications.addSuccess( 28 | `Successfully added ${scope} to Markdown scopes`, 29 | ) 30 | } 31 | }, 32 | }) 33 | } 34 | 35 | export function deactivate() { 36 | command.dispose() 37 | tableFormatter.destroy() 38 | } 39 | -------------------------------------------------------------------------------- /src/regex.ts: -------------------------------------------------------------------------------- 1 | import XRegExp = require('xregexp') 2 | 3 | export const regex = XRegExp( 4 | `\ 5 | ( # header capture 6 | (?: 7 | (?:[^\\r\\n]*?\\|[^\\r\\n]*) # line w/ at least one pipe 8 | \\ * # maybe trailing whitespace 9 | )? # maybe header 10 | (?:\\r?\\n|^) # newline 11 | ) 12 | ( # format capture 13 | (?: 14 | \\|\\ *(?::?-+:?|::)\\ * # format starting w/pipe 15 | |\\|?(?:\\ *(?::?-+:?|::)\\ *\\|)+ # or separated by pipe 16 | ) 17 | (?:\\ *(?::?-+:?|::)\\ *)? # maybe w/o trailing pipe 18 | \\ * # maybe trailing whitespace 19 | \\r?\\n # newline 20 | ) 21 | ( # body capture 22 | (?: 23 | (?:[^\\r\\n]*?\\|[^\\r\\n]*) # line w/ at least one pipe 24 | \\ * # maybe trailing whitespace 25 | (?:\\r?\\n|$) # newline 26 | )+ # at least one 27 | ) 28 | `, 29 | 'gx', 30 | ) 31 | -------------------------------------------------------------------------------- /src/table-formatter.ts: -------------------------------------------------------------------------------- 1 | import { CompositeDisposable, Range, TextEditor, BufferScanResult } from 'atom' 2 | import { regex } from './regex' 3 | import { formatTable } from './formatTable' 4 | 5 | export function getAllSettings() { 6 | return atom.config.get('markdown-table-formatter') 7 | } 8 | 9 | export class TableFormatter { 10 | private readonly subscriptions = new CompositeDisposable() 11 | constructor() { 12 | atom.workspace.observeTextEditors(editor => 13 | this.subscriptions.add( 14 | editor.getBuffer().onWillSave(() => { 15 | if (atom.config.get('markdown-table-formatter.formatOnSave')) { 16 | this.format(editor, true) 17 | } 18 | }), 19 | ), 20 | ) 21 | } 22 | 23 | public destroy() { 24 | this.subscriptions.dispose() 25 | } 26 | 27 | public format(editor: TextEditor, force: boolean = false) { 28 | let selectionsRanges = editor.getSelectedBufferRanges() 29 | const settings = getAllSettings() 30 | 31 | const bufferRange = editor.getBuffer().getRange() 32 | const selectionsRangesEmpty = selectionsRanges.every(i => i.isEmpty()) 33 | if ( 34 | !settings.markdownGrammarScopes.includes(editor.getGrammar().scopeName) && 35 | selectionsRangesEmpty 36 | ) { 37 | return undefined 38 | } 39 | if (force || (selectionsRangesEmpty && settings.autoSelectEntireDocument)) { 40 | selectionsRanges = [bufferRange] 41 | } else { 42 | selectionsRanges = selectionsRanges 43 | .filter( 44 | srange => !(srange.isEmpty() && settings.autoSelectEntireDocument), 45 | ) 46 | .map(srange => { 47 | let { start } = bufferRange 48 | let { end } = bufferRange 49 | editor.scanInBufferRange( 50 | /^$/m, 51 | new Range(srange.start, bufferRange.end), 52 | ({ range }) => (end = range.start), 53 | ) 54 | editor.backwardsScanInBufferRange( 55 | /^$/m, 56 | new Range(bufferRange.start, srange.end), 57 | ({ range }) => (start = range.start), 58 | ) 59 | if (end.isLessThan(srange.end)) end = srange.end 60 | if (start.isGreaterThan(srange.start)) start = srange.start 61 | return new Range(start, end) 62 | }) 63 | } 64 | 65 | return editor.getBuffer().transact(() => 66 | selectionsRanges.map(range => 67 | editor.backwardsScanInBufferRange(regex, range, function( 68 | obj: BufferScanResult, 69 | ) { 70 | return editor.setTextInBufferRange( 71 | obj.range, 72 | formatTable(obj.match, settings), 73 | ) 74 | }), 75 | ), 76 | ) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2017", 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "isolatedModules": false, 7 | "jsx": "react", 8 | "experimentalDecorators": true, 9 | "emitDecoratorMetadata": true, 10 | "declaration": false, 11 | "noImplicitAny": true, 12 | "noImplicitUseStrict": false, 13 | "removeComments": true, 14 | "noLib": false, 15 | "jsxFactory": "etch.dom", 16 | "preserveConstEnums": true, 17 | "suppressImplicitAnyIndexErrors": true, 18 | "outDir": "lib", 19 | "inlineSources": true, 20 | "inlineSourceMap": true, 21 | "strictNullChecks": true, 22 | "allowJs": false, 23 | "lib": ["dom", "es2017"], 24 | "strict": true, 25 | "importHelpers": false, 26 | "noFallthroughCasesInSwitch": true, 27 | "noImplicitReturns": true, 28 | "noUnusedLocals": true, 29 | "noUnusedParameters": true 30 | }, 31 | "exclude": ["node_modules", "spec"], 32 | "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.js"], 33 | "compileOnSave": false, 34 | "experimentalDecorators": true 35 | } 36 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "atom-haskell-tslint-rules", 3 | "rules": { 4 | "no-unsafe-any": true, 5 | "space-before-function-paren": [true, "asyncArrow"], 6 | "align": false 7 | } 8 | } 9 | --------------------------------------------------------------------------------