├── .gitignore ├── .travis.yml ├── Commands.sublime-commands ├── Default (Linux).sublime-keymap ├── Default (OSX).sublime-keymap ├── Default (Windows).sublime-keymap ├── LICENSE ├── Main.sublime-menu ├── MarkdownTableFormatter.sublime-settings ├── README.md ├── markdown_table_formatter.py ├── mtf_show_off_small.gif ├── simple_markdown ├── __init__.py └── table.py └── tests ├── test.md ├── test.py └── testpath.py /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "3.3" 4 | script: 5 | - cd tests && python test.py 6 | -------------------------------------------------------------------------------- /Commands.sublime-commands: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "caption": "Markdown Table Formatter: Format Table", 4 | "command": "markdown_table_format" 5 | } 6 | ] 7 | -------------------------------------------------------------------------------- /Default (Linux).sublime-keymap: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "keys": ["ctrl+alt+shift+t"], 4 | "command": "markdown_table_format", 5 | "context": [ 6 | {"key": "selector", "operator": "equal", "operand": "text.html.markdown"} 7 | ] 8 | } 9 | ] 10 | -------------------------------------------------------------------------------- /Default (OSX).sublime-keymap: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "keys": ["ctrl+alt+shift+t"], 4 | "command": "markdown_table_format", 5 | "context": [ 6 | {"key": "selector", "operator": "equal", "operand": "text.html.markdown"} 7 | ] 8 | } 9 | ] 10 | -------------------------------------------------------------------------------- /Default (Windows).sublime-keymap: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "keys": ["ctrl+alt+shift+t"], 4 | "command": "markdown_table_format", 5 | "context": [ 6 | {"key": "selector", "operator": "equal", "operand": "text.html.markdown"} 7 | ] 8 | } 9 | ] 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2016 Bitwiser73 - c37d42607a51f68a2aa46b07a3983ab8506cf6d42235c6f2b4a0d8102fce3483 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 9 | -------------------------------------------------------------------------------- /Main.sublime-menu: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "Caption": "Edit", 4 | "mnemonic": "E", 5 | "id": "edit", 6 | "children": 7 | [ 8 | { 9 | "caption": "Markdown Table Formatter", 10 | "command": "markdown_table_format" 11 | } 12 | ] 13 | }, 14 | { 15 | "caption": "Preferences", 16 | "mnemonic": "n", 17 | "id": "preferences", 18 | "children": 19 | [ 20 | { 21 | "caption": "Package Settings", 22 | "mnemonic": "P", 23 | "id": "package-settings", 24 | "children": 25 | [ 26 | { 27 | "caption": "Markdown Table Formatter", 28 | "children": 29 | [ 30 | { 31 | "command": "edit_settings", 32 | "args": { "base_file": "${packages}/Markdown Table Formatter/MarkdownTableFormatter.sublime-settings" }, 33 | "caption": "Settings" 34 | }, 35 | { "caption": "-" }, 36 | { 37 | "command": "open_file", 38 | "args": { 39 | "file": "${packages}/Markdown Table Formatter/Default (OSX).sublime-keymap", 40 | "platform": "OSX" 41 | }, 42 | "caption": "Key Bindings – Default" 43 | }, 44 | { 45 | "command": "open_file", 46 | "args": { 47 | "file": "${packages}/Markdown Table Formatter/Default (Linux).sublime-keymap", 48 | "platform": "Linux" 49 | }, 50 | "caption": "Key Bindings – Default" 51 | }, 52 | { 53 | "command": "open_file", 54 | "args": { 55 | "file": "${packages}/Markdown Table Formatter/Default (Windows).sublime-keymap", 56 | "platform": "Windows" 57 | }, 58 | "caption": "Key Bindings – Default" 59 | }, 60 | { 61 | "command": "open_file", 62 | "args": { 63 | "file": "${packages}/User/Default (OSX).sublime-keymap", 64 | "platform": "OSX" 65 | }, 66 | "caption": "Key Bindings – User" 67 | }, 68 | { 69 | "command": "open_file", 70 | "args": { 71 | "file": "${packages}/User/Default (Linux).sublime-keymap", 72 | "platform": "Linux" 73 | }, 74 | "caption": "Key Bindings – User" 75 | }, 76 | { 77 | "command": "open_file", 78 | "args": { 79 | "file": "${packages}/User/Default (Windows).sublime-keymap", 80 | "platform": "Windows" 81 | }, 82 | "caption": "Key Bindings – User" 83 | }, 84 | { "caption": "-" } 85 | ] 86 | } 87 | ] 88 | } 89 | ] 90 | } 91 | ] -------------------------------------------------------------------------------- /MarkdownTableFormatter.sublime-settings: -------------------------------------------------------------------------------- 1 | { 2 | // make plugin verbose in debug console 3 | "verbose": false, 4 | 5 | // scan document to format tables when saving 6 | "autoformat_on_save": false, 7 | 8 | // spaces between "|" and cell's text 9 | "margin": 1, 10 | 11 | // additional spaces before/after cell's text (depending on justification) 12 | "padding": 0, 13 | 14 | // how text should be justified when not specified [LEFT, RIGHT, CENTER] 15 | "default_justification": "LEFT" 16 | } 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Markdown Table Formatter 2 | 3 | [![travis][img-travis]](https://travis-ci.org/bitwiser73/MarkdownTableFormatter) [![MIT licensed][img-mit]](./LICENSE) [![Donate][img-paypal]][donate-paypal] 4 | 5 | Sublime Text 3 markdown plugin that offers table formatting. 6 | 7 | Inspired by the [Atom's version](https://atom.io/packages/markdown-table-formatter) from fcrespo82 (Fernando). 8 | 9 | markdowntableformatter[at]gmail[.]com 10 | 11 | ![Example](mtf_show_off_small.gif) 12 | 13 | ## Usage 14 | 15 | There are two basic ways of using this plugin. Select what you want to format and hit **Ctrl+Alt+Shift+T** or use the same shortcut without selection to format the entire document. 16 | 17 | ## Configuration 18 | 19 | ``` 20 | { 21 | // make plugin verbose in debug console 22 | "verbose": false, 23 | 24 | // scan document to format tables when saving 25 | "autoformat_on_save": false, 26 | 27 | // spaces between "|" and cell's text 28 | "margin": 1, 29 | 30 | // additional spaces before/after cell's text (depending on justification) 31 | "padding": 0, 32 | 33 | // how text should be justified when not specified [LEFT, RIGHT, CENTER] 34 | "default_justification": "LEFT" 35 | } 36 | ``` 37 | 38 | ## Proper formatting 39 | 40 | To be recognised properly by the plugin, Markdown tables must not contain white space in the line that separate the table heading from the other table rows. 41 | 42 | For example, the following works: 43 | 44 | ```md 45 | |:------------------|:---------------------|:-----------------| 46 | ``` 47 | 48 | This example will not work: 49 | 50 | ```md 51 | |:------------------| ---------------------|:-----------------| 52 | ``` 53 | 54 | ## Support it! 55 | 56 | [![Donate][img-paypal]][donate-paypal] 57 | 58 | [donate-paypal]: https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=WAQUTBM9K8246 59 | [img-travis]: https://travis-ci.org/bitwiser73/MarkdownTableFormatter.svg?branch=master 60 | [img-mit]: https://img.shields.io/badge/license-MIT-blue.svg 61 | [img-paypal]: https://img.shields.io/badge/Donate-PayPal-blue.svg 62 | -------------------------------------------------------------------------------- /markdown_table_formatter.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import sublime 4 | import sublime_plugin 5 | 6 | import logging 7 | 8 | from . import simple_markdown as markdown 9 | from .simple_markdown import table 10 | 11 | log = logging.getLogger(__name__) 12 | 13 | 14 | class MarkdownTableFormatCommand(sublime_plugin.TextCommand): 15 | def run(self, edit, format_all=False): 16 | logging.basicConfig(level=logging.DEBUG) 17 | settings = \ 18 | sublime.load_settings("MarkdownTableFormatter.sublime-settings") 19 | verbose = settings.get("verbose") 20 | margin = settings.get("margin") 21 | padding = settings.get("padding") 22 | justify = settings.get("default_justification") 23 | justify = markdown.table.Justify.from_string[justify] 24 | 25 | if verbose: 26 | log.setLevel(logging.DEBUG) 27 | else: 28 | log.setLevel(logging.INFO) 29 | 30 | # format selected regions or all file with no selection 31 | has_selection = len(self.view.sel()) and self.view.sel()[0].size() 32 | if not format_all and has_selection: 33 | regions = self.view.sel() 34 | else: 35 | regions = [sublime.Region(0, self.view.size())] 36 | 37 | table_new_regions = [] 38 | for region in regions: 39 | text = self.view.substr(region) 40 | # get all tables positions as (start,end) list 41 | positions = markdown.table.find_all(text) 42 | offset = 0 43 | for start, end in positions: 44 | prev_table = text[start:end] 45 | log.debug("table found:\n" + prev_table) 46 | new_table = markdown.table.format(prev_table, margin, padding, 47 | justify) 48 | log.debug("formatted output:\n" + new_table) 49 | 50 | # absolute original table position after some insertion/removal 51 | table_prev_begin = region.begin() + start + offset 52 | table_prev_end = region.begin() + end + offset 53 | table_prev_region = \ 54 | sublime.Region(table_prev_begin, table_prev_end) 55 | 56 | # future table position after some insertion/removal 57 | table_new_begin = table_prev_begin 58 | table_new_end = \ 59 | region.begin() + start + offset + len(new_table) 60 | table_new_region = \ 61 | sublime.Region(table_new_begin, table_new_end) 62 | 63 | self.view.replace(edit, table_prev_region, new_table) 64 | # stack new regions to update selection 65 | table_new_regions.append(table_new_region) 66 | 67 | # as table length will likely change after being formatted an 68 | # offset is required to keep positions consistent 69 | offset = offset + len(new_table) - len(prev_table) 70 | 71 | if table_new_regions and not format_all: 72 | had_multiple_regions = has_selection and len(self.view.sel()) > 1 73 | self.view.sel().clear() 74 | 75 | # I don't like having to hit 'esc' to get only one cursor back 76 | # after having formatted more than one table using one selection 77 | if had_multiple_regions or len(table_new_regions) == 1: 78 | log.debug("MULTIPLE REGION") 79 | self.view.sel().add_all(table_new_regions) 80 | else: 81 | cursor = sublime.Region(table_new_regions[0].a) 82 | self.view.sel().add(cursor) 83 | 84 | 85 | class MarkdownTableFormatterListener(sublime_plugin.EventListener): 86 | def on_pre_save(self, view): 87 | # restrict to markdown files 88 | if view.score_selector(0, "text.html.markdown") == 0: 89 | return 90 | 91 | settings = \ 92 | sublime.load_settings("MarkdownTableFormatter.sublime-settings") 93 | if not settings.get("autoformat_on_save"): 94 | return 95 | view.run_command("markdown_table_format", {"format_all": True}) 96 | -------------------------------------------------------------------------------- /mtf_show_off_small.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitwiser73/MarkdownTableFormatter/77359eed12abf5ac649bbadcfc7bb0ded72278c6/mtf_show_off_small.gif -------------------------------------------------------------------------------- /simple_markdown/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitwiser73/MarkdownTableFormatter/77359eed12abf5ac649bbadcfc7bb0ded72278c6/simple_markdown/__init__.py -------------------------------------------------------------------------------- /simple_markdown/table.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | 4 | def enum(*sequential, **named): 5 | enums = dict(zip(sequential, range(len(sequential))), **named) 6 | from_string = dict((key, value) for key, value in enums.items()) 7 | from_int = dict((value, key) for key, value in enums.items()) 8 | enums['from_string'] = from_string 9 | enums['from_int'] = from_int 10 | return type('Enum', (), enums) 11 | 12 | Justify = enum("LEFT", "CENTER", "RIGHT") 13 | 14 | 15 | def find_all(text): 16 | tables = [] 17 | offset = 0 18 | while True: 19 | pattern = ".*\|.*\r?\n[\s\t]*\|?(?::?-+:?\|:?-+:?\|?)+(?:\r?\n.*\|.*)+" 20 | grp = re.search(pattern, text[offset:], re.MULTILINE) 21 | if grp is None: 22 | return tables 23 | tables.append((grp.start() + offset, grp.end() + offset)) 24 | offset = offset + grp.end() 25 | return tables 26 | 27 | 28 | def format(raw_table, margin=1, padding=0, default_justify=Justify.LEFT): 29 | rows = raw_table.splitlines() 30 | # normalize markdown table, add missing leading/trailing '|' 31 | for idx, row in enumerate(rows): 32 | if re.match("^[\s\t]*\|", row) is None: 33 | rows[idx] = "|" + rows[idx] 34 | if re.match(".*\|[\s\t]*\r?\n?$", row) is None: 35 | rows[idx] = rows[idx] + "|" 36 | 37 | matrix = [[col.strip() for col in row.split("|")] for row in rows] 38 | 39 | # remove first and last empties column 40 | matrix[:] = [row[1:] for row in matrix] 41 | matrix[:] = [row[:-1] for row in matrix] 42 | 43 | # ensure there's same column number for each row or add missings 44 | col_cnt = max([len(row) for row in matrix]) 45 | matrix[:] = \ 46 | [r if len(r) == col_cnt else r + [""]*(col_cnt-len(r)) for r in matrix] 47 | 48 | # merge the multiple "-" of the 2nd line 49 | matrix[1] = [re.sub("[-. ]+","-", col) for col in matrix[1]] 50 | 51 | # determine each cell text size 52 | text_width = [[len(col) for col in row] for row in matrix] 53 | # determine column width (including space padding/margin) 54 | col_width = [max(size) + margin*2 + padding for size in zip(*text_width)] 55 | 56 | # get each column justification or apply default 57 | justify = [] 58 | for col_idx, col in enumerate(matrix[1]): 59 | if col.startswith(":") and col.endswith(":"): 60 | justify.append(Justify.CENTER) 61 | elif col.endswith(":"): 62 | justify.append(Justify.RIGHT) 63 | elif col.startswith(":"): 64 | justify.append(Justify.LEFT) 65 | else: 66 | justify.append(default_justify) 67 | 68 | # construct a clean markdown table without separation row 69 | table = [] 70 | for row_idx, row in enumerate(matrix): 71 | line = ["|"] 72 | # separation row is processed after 73 | if row_idx == 1: 74 | continue 75 | for col_idx, col in enumerate(row): 76 | if justify[col_idx] == Justify.CENTER: 77 | div, mod = divmod(col_width[col_idx] - len(col), 2) 78 | text = " "*div + col + " "*(div+mod) 79 | line.append(text + "|") 80 | continue 81 | if justify[col_idx] == Justify.RIGHT: 82 | text = col.rjust(col_width[col_idx] - margin*2) 83 | elif justify[col_idx] == Justify.LEFT: 84 | text = col.ljust(col_width[col_idx] - margin*2) 85 | line.append(" "*margin + text + " "*margin + "|") 86 | table.append("".join(line)) 87 | 88 | # construct separation row 89 | sep_row = [] 90 | for col_idx, col in enumerate(matrix[1]): 91 | line = list("-" * (col_width[col_idx])) 92 | if justify[col_idx] == Justify.LEFT: 93 | line[0] = ":" 94 | elif justify[col_idx] == Justify.CENTER: 95 | line[0] = ":" 96 | line[-1] = ":" 97 | elif justify[col_idx] == Justify.RIGHT: 98 | line[-1] = ":" 99 | sep_row.append("".join(line)) 100 | 101 | table.insert(1, "|" + "|".join(sep_row) + "|") 102 | return "\n".join(table) 103 | -------------------------------------------------------------------------------- /tests/test.md: -------------------------------------------------------------------------------- 1 | Hello! 2 | 3 | | Tables | Are | Cool | 4 | |:-------------|:-----------:|-----:| 5 | | col 3 is | right-aligned | $1600 | 6 | col 2 is | centered| $12 7 | | zebrà stripes | are neat | $1 | 8 | ||| $2 | 9 | 10 | This is some text that won't be modified when formatting those nice tables. 11 | 12 | | Tables | Are | Cool | 13 | |:-------------|------------:|:-----| 14 | | some pipe are... | missing | $1600 | 15 | but von Kármán | say it's working| $12 16 | | z€bra stripes | are neat | $1 | 17 | ||| $2 | 18 | 19 | >"Emacs is a great operating system, lacking only a decent editor". 20 | 21 | | Tables | Are | Cool | 22 | |:-------------|:-------------|:-----| 23 | | more **table** | more fun | $1600 | 24 | with empty cell || $12 25 | | zebra stripes | are still neat | $1 | 26 | ||| $2 | 27 | 28 | | it's | a trap | (i'm not a table!) 29 | Select 1 | Select 2 | Select ALL 30 | 31 | 32 | foo|bar 33 | ---|--- 34 | test1-|-test2 35 | 36 | 37 | foo|bar 38 | --|:--- 39 | test1-|-test2 40 | 41 | 42 | -------------------------------------------------------------------------------- /tests/test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*_ 3 | 4 | import unittest 5 | 6 | import testpath 7 | import simple_markdown.table 8 | import simple_markdown.table as Table 9 | 10 | 11 | class test_markdown_table(unittest.TestCase): 12 | def setUp(self): 13 | pass 14 | 15 | def tearDown(self): 16 | pass 17 | 18 | def test_format(self): 19 | raw_table = """\ 20 | | Tables | Are | Cool | 21 | |:-------------|-------------|-----:| 22 | | col 1 is | left-aligned | $1600 | 23 | col 2 is | centered| $12 24 | | zebra stripes | | are neat $1 | 25 | || |$hello 26 | | $2 |""" 27 | 28 | expected_table_2_2 = """\ 29 | | Tables | Are | Cool | 30 | |:------------------|:----------------:|------------------:| 31 | | col 1 is | left-aligned | $1600 | 32 | | col 2 is | centered | $12 | 33 | | zebra stripes | | are neat $1 | 34 | | | | $hello | 35 | | $2 | | |""" 36 | 37 | table = Table.format(raw_table, margin=2, padding=2, 38 | default_justify=Table.Justify.CENTER) 39 | self.assertEqual(table, expected_table_2_2) 40 | 41 | expected_table_0_0 = """\ 42 | |Tables |Are | Cool| 43 | |:------------|:-----------|------------:| 44 | |col 1 is |left-aligned| $1600| 45 | |col 2 is |centered | $12| 46 | |zebra stripes| |are neat $1| 47 | | | | $hello| 48 | |$2 | | |""" 49 | 50 | table = Table.format(raw_table, margin=0, padding=0) 51 | self.assertEqual(table, expected_table_0_0) 52 | 53 | # test table with minimal form (#7) 54 | small = """\ 55 | foo|bar 56 | --------|:--- 57 | 123|4567777789 58 | a|""" 59 | 60 | expected_small = """\ 61 | | foo | bar | 62 | |:----|:-----------| 63 | | 123 | 4567777789 | 64 | | a | |""" 65 | 66 | table = Table.format(small, margin=1, padding=0) 67 | self.assertEqual(table, expected_small) 68 | 69 | def test_find_all(self): 70 | junk_tables = """ 71 | | Tables | Are | Cool #1 | 72 | |-------------|:-------------:|:-----| 73 | | col 3 is | right-aligned | $1600 | 74 | col 2 is || $12 75 | | zebra stripes|are neat| $1 | 76 | || |$hello 77 | | $2 | 78 | 79 | hellobar 80 | fooworld 81 | and junk 82 | 83 | | Tables | Are | Cool #2 | 84 | |-------------|:-------------:|:-----| 85 | | col 3 is | right-aligned | $1600 | 86 | col 2 is || $12 87 | | zebra stripes | are neat | $1 | 88 | || |$hello 89 | | $2 | 90 | 91 | | ok | more | col | and more | col 92 | |----|-----|--|----|---|--|-|-|-| 93 | |ok | still good 94 | 95 | junk junk junk 96 | and some | to test | 97 | if it's still working |||||| 98 | is it? 99 | """ 100 | offsets = simple_markdown.table.find_all(junk_tables) 101 | self.assertEqual(len(offsets), 3) 102 | 103 | if __name__ == '__main__': 104 | unittest.main() 105 | -------------------------------------------------------------------------------- /tests/testpath.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | sys.path.append(os.path.realpath('..')) 5 | --------------------------------------------------------------------------------