diff --git a/README.md b/README.md
index 8fa2ef93..bd175a44 100644
--- a/README.md
+++ b/README.md
@@ -506,6 +506,69 @@ can obtain a list of all keybindings by pressing C-c C-h.
to the end of the current or following defun, and C-M-h will
put the region around the entire defun.
+ * Table Editing:
+
+ Markdown Mode includes support for editing tables, which
+ have the following basic format:
+
+ | Right | Left | Center | Default |
+ |------:|:-----|:------:|---------|
+ | 12 | 12 | 12 | 12 |
+ | 123 | 123 | 123 | 123 |
+ | 1 | 1 | 1 | 1 |
+
+ The first line contains column headers. The second line
+ contains a separator line between the headers and the content.
+ Each following line is a row in the table. Columns are always
+ separated by the pipe character. The colons indicate column
+ alignment.
+
+ A table is re-aligned automatically each time you press TAB
+ or RET inside the table. TAB also moves to the next
+ field (RET to the next row) and creates new table rows at
+ the end of the table or before horizontal separator lines. The
+ indentation of the table is set by the first line. Column
+ centering inside Emacs is not supported.
+
+ Beginning pipe characters are required for proper detection of
+ table borders inside Emacs. Any line starting with `|-` or `|:`
+ is considered as a horizontal separator line and will be
+ expanded on the next re-align to span the whole table width. No
+ padding is allowed between the beginning pipe character and
+ header separator symbol. So, to create the above table, you
+ would only type
+
+ |Right|Left|Center|Default|
+ |-
+
+ and then press TAB to align the table and start filling in
+ cells.
+
+ Then you can jump with TAB from one cell to the next or with
+ S-TAB to the previous one. RET will jump to the to the
+ next cell in the same column, and create a new row if there is
+ no such cell or if the next row is beyond a separator line.
+
+ You can also convert selected region to a table. Basic editing
+ capabilities include inserting, deleting, and moving of columns
+ and rows, and table re-alignment, sorting, transposition:
+
+ - C-c UP or C-c DOWN - Move the current row up or down.
+ - C-c LEFT or C-c RIGHT - Move the current column left or right.
+ - C-c S-UP - Kill the current row.
+ - C-c S-DOWN - Insert a row above the current row. With a
+ prefix argument, row line is created below the current one.
+ - C-c S-LEFT - Kill the current column.
+ - C-c S-RIGHT - Insert a new column to the left of the current one.
+ - C-c C-d - Re-align the current table (`markdown-do`).
+ - C-c C-c ^ - Sort table lines alpabetically or numerically.
+ - C-c C-c | - Convert selected region to a table.
+ - C-c C-c t - Transpose table at point.
+
+ The table editing functions try to handle markup hiding
+ correctly when calculating column widths, however, columns
+ containig hidden markup may not always be aligned properly.
+
* Miscellaneous Commands:
When the [`edit-indirect`][ei] package is installed, C-c `
diff --git a/markdown-mode.el b/markdown-mode.el
index 9b657bba..891b1d69 100644
--- a/markdown-mode.el
+++ b/markdown-mode.el
@@ -522,6 +522,69 @@
;; to the end of the current or following defun, and `C-M-h` will
;; put the region around the entire defun.
;;
+;; * Table Editing:
+;;
+;; Markdown Mode includes support for editing tables, which
+;; have the following basic format:
+;;
+;; | Right | Left | Center | Default |
+;; |------:|:-----|:------:|---------|
+;; | 12 | 12 | 12 | 12 |
+;; | 123 | 123 | 123 | 123 |
+;; | 1 | 1 | 1 | 1 |
+;;
+;; The first line contains column headers. The second line
+;; contains a separator line between the headers and the content.
+;; Each following line is a row in the table. Columns are always
+;; separated by the pipe character. The colons indicate column
+;; alignment.
+;;
+;; A table is re-aligned automatically each time you press `TAB`
+;; or `RET` inside the table. `TAB` also moves to the next
+;; field (`RET` to the next row) and creates new table rows at
+;; the end of the table or before horizontal separator lines. The
+;; indentation of the table is set by the first line. Column
+;; centering inside Emacs is not supported.
+;;
+;; Beginning pipe characters are required for proper detection of
+;; table borders inside Emacs. Any line starting with `|-` or `|:`
+;; is considered as a horizontal separator line and will be
+;; expanded on the next re-align to span the whole table width. No
+;; padding is allowed between the beginning pipe character and
+;; header separator symbol. So, to create the above table, you
+;; would only type
+;;
+;; |Right|Left|Center|Default|
+;; |-
+;;
+;; and then press `TAB` to align the table and start filling in
+;; cells.
+;;
+;; Then you can jump with `TAB` from one cell to the next or with
+;; `S-TAB` to the previous one. `RET` will jump to the to the
+;; next cell in the same column, and create a new row if there is
+;; no such cell or if the next row is beyond a separator line.
+;;
+;; You can also convert selected region to a table. Basic editing
+;; capabilities include inserting, deleting, and moving of columns
+;; and rows, and table re-alignment, sorting, transposition:
+;;
+;; - `C-c UP` or `C-c DOWN` - Move the current row up or down.
+;; - `C-c LEFT` or `C-c RIGHT` - Move the current column left or right.
+;; - `C-c S-UP` - Kill the current row.
+;; - `C-c S-DOWN` - Insert a row above the current row. With a
+;; prefix argument, row line is created below the current one.
+;; - `C-c S-LEFT` - Kill the current column.
+;; - `C-c S-RIGHT` - Insert a new column to the left of the current one.
+;; - `C-c C-d` - Re-align the current table (`markdown-do`).
+;; - `C-c C-c ^` - Sort table lines alpabetically or numerically.
+;; - `C-c C-c |` - Convert selected region to a table.
+;; - `C-c C-c t` - Transpose table at point.
+;;
+;; The table editing functions try to handle markup hiding
+;; correctly when calculating column widths, however, columns
+;; containig hidden markup may not always be aligned properly.
+;;
;; * Miscellaneous Commands:
;;
;; When the [`edit-indirect'][ei] package is installed, `C-c '`
@@ -5445,7 +5508,9 @@ duplicate positions, which are handled up by calling functions."
(reverse positions)))
(defun markdown-enter-key ()
- "Handle RET according to value of `markdown-indent-on-enter'.
+ "Handle RET depending on the context.
+If the point is at a table, move to the next row. Otherwise,
+indent according to value of `markdown-indent-on-enter'.
When it is nil, simply call `newline'. Otherwise, indent the next line
following RET using `markdown-indent-line'. Furthermore, when it
is set to 'indent-and-new-item and the point is in a list item,
@@ -5453,8 +5518,12 @@ start a new item with the same indentation. If the point is in an
empty list item, remove it (so that pressing RET twice when in a
list simply adds a blank line)."
(interactive)
- (if (not markdown-indent-on-enter)
- (newline)
+ (cond
+ ;; Table
+ ((markdown-table-at-point-p)
+ (call-interactively #'markdown-table-next-row))
+ ;; Indent non-table text
+ (markdown-indent-on-enter
(let (bounds)
(if (and (memq markdown-indent-on-enter '(indent-and-new-item))
(setq bounds (markdown-cur-list-item-bounds)))
@@ -5471,7 +5540,9 @@ list simply adds a blank line)."
(call-interactively #'markdown-insert-list-item)))
;; Point is not in a list
(newline)
- (markdown-indent-line)))))
+ (markdown-indent-line))))
+ ;; Insert a raw newline
+ (t (newline))))
(define-obsolete-function-alias 'markdown-exdent-or-delete
'markdown-outdent-or-delete "v2.3")
@@ -5836,6 +5907,9 @@ Assumes match data is available for `markdown-regex-italic'."
(define-key map (kbd "c") 'markdown-check-refs)
(define-key map (kbd "n") 'markdown-cleanup-list-numbers)
(define-key map (kbd "]") 'markdown-complete-buffer)
+ (define-key map (kbd "^") 'markdown-table-sort-lines)
+ (define-key map (kbd "|") 'markdown-table-convert-region)
+ (define-key map (kbd "t") 'markdown-table-transpose)
map)
"Keymap for Markdown buffer-wide commands.")
@@ -5859,7 +5933,7 @@ Assumes match data is available for `markdown-regex-italic'."
(define-key map (kbd "C-c >") 'markdown-indent-region)
(define-key map (kbd "C-c <") 'markdown-outdent-region)
;; Visibility cycling
- (define-key map (kbd "TAB") 'markdown-cycle)
+ (define-key map (kbd "TAB") 'markdown-tab)
(define-key map (kbd "") 'markdown-shifttab)
(define-key map (kbd "") 'markdown-shifttab)
(define-key map (kbd "") 'markdown-shifttab)
@@ -5871,11 +5945,15 @@ Assumes match data is available for `markdown-regex-italic'."
(define-key map (kbd "C-c C-u") 'markdown-outline-up)
;; Buffer-wide commands
(define-key map (kbd "C-c C-c") markdown-mode-command-map)
- ;; Subtree and list editing
+ ;; Subtree, list, and table editing
(define-key map (kbd "C-c ") 'markdown-move-up)
(define-key map (kbd "C-c ") 'markdown-move-down)
(define-key map (kbd "C-c ") 'markdown-promote)
(define-key map (kbd "C-c ") 'markdown-demote)
+ (define-key map (kbd "C-c S-") 'markdown-table-delete-row)
+ (define-key map (kbd "C-c S-") 'markdown-table-insert-row)
+ (define-key map (kbd "C-c S-") 'markdown-table-delete-column)
+ (define-key map (kbd "C-c S-") 'markdown-table-insert-column)
(define-key map (kbd "C-c C-M-h") 'markdown-mark-subtree)
(define-key map (kbd "C-x n s") 'markdown-narrow-to-subtree)
(define-key map (kbd "M-") 'markdown-insert-list-item)
@@ -5950,6 +6028,14 @@ Assumes match data is available for `markdown-regex-italic'."
"Keymap for `gfm-mode'.
See also `markdown-mode-map'.")
+(defun markdown-tab ()
+ "Handle TAB key based on context."
+ (interactive)
+ (cond
+ ((markdown-table-at-point-p)
+ (call-interactively #'markdown-table-forward-cell))
+ (t (call-interactively #'markdown-cycle))))
+
;;; Menu ==================================================================
@@ -7239,10 +7325,14 @@ as appropriate, by calling `indent-for-tab-command'."
(indent-for-tab-command))))
(defun markdown-shifttab ()
- "Global visibility cycling.
-Calls `markdown-cycle' with argument t."
+ "Handle S-TAB keybinding based on context.
+When in a table, move backward one cell.
+Otherwise, cycle global heading visibility by calling
+`markdown-cycle' with argument t."
(interactive)
- (markdown-cycle t))
+ (cond ((markdown-table-at-point-p)
+ (call-interactively #'markdown-table-backward-cell))
+ (t (markdown-cycle t))))
(defun markdown-outline-level ()
"Return the depth to which a statement is nested in the outline."
@@ -7459,14 +7549,17 @@ This puts point at the start of the current subtree, and mark at the end."
(defun markdown-move-up ()
"Move thing at point up.
When in a list item, call `markdown-move-list-item-up'.
+When in a table, call `markdown-table-move-row-up'.
Otherwise, move the current heading subtree up with
`markdown-move-subtree-up'."
(interactive)
(cond
((markdown-list-item-at-point-p)
- (markdown-move-list-item-up))
+ (call-interactively #'markdown-move-list-item-up))
+ ((markdown-table-at-point-p)
+ (call-interactively #'markdown-table-move-row-up))
(t
- (markdown-move-subtree-up))))
+ (call-interactively #'markdown-move-subtree-up))))
(defun markdown-move-down ()
"Move thing at point down.
@@ -7476,14 +7569,17 @@ Otherwise, move the current heading subtree up with
(interactive)
(cond
((markdown-list-item-at-point-p)
- (markdown-move-list-item-down))
+ (call-interactively #'markdown-move-list-item-down))
+ ((markdown-table-at-point-p)
+ (call-interactively #'markdown-table-move-row-down))
(t
- (markdown-move-subtree-down))))
+ (call-interactively #'markdown-move-subtree-down))))
(defun markdown-promote ()
- "Either promote header or list item at point or cycle markup.
-See `markdown-cycle-atx', `markdown-cycle-setext', and
-`markdown-promote-list-item'."
+ "Promote or move element at point to the left.
+Depending on the context, this function will promote a heading or
+list item at the point, move a table column to the left, or cycle
+markup."
(interactive)
(let (bounds)
(cond
@@ -7499,6 +7595,9 @@ See `markdown-cycle-atx', `markdown-cycle-setext', and
;; Promote list item
((setq bounds (markdown-cur-list-item-bounds))
(markdown-promote-list-item bounds))
+ ;; Move table column to the left
+ ((markdown-table-at-point-p)
+ (call-interactively #'markdown-table-move-column-left))
;; Promote bold
((thing-at-point-looking-at markdown-regex-bold)
(markdown-cycle-bold))
@@ -7509,9 +7608,10 @@ See `markdown-cycle-atx', `markdown-cycle-setext', and
(user-error "Nothing to promote at point")))))
(defun markdown-demote ()
- "Either demote header or list item at point or cycle or remove markup.
-See `markdown-cycle-atx', `markdown-cycle-setext', and
-`markdown-demote-list-item'."
+ "Demote or move element at point to the right.
+Depending on the context, this function will demote a heading or
+list item at the point, move a table column to the right, or cycle
+or remove markup."
(interactive)
(let (bounds)
(cond
@@ -7527,6 +7627,9 @@ See `markdown-cycle-atx', `markdown-cycle-setext', and
;; Demote list item
((setq bounds (markdown-cur-list-item-bounds))
(markdown-demote-list-item bounds))
+ ;; Move table column to the right
+ ((markdown-table-at-point-p)
+ (call-interactively #'markdown-table-move-column-right))
;; Demote bold
((thing-at-point-looking-at markdown-regex-bold)
(markdown-cycle-bold))
@@ -8356,6 +8459,9 @@ markers and footnote text."
;; GFM task list item
((markdown-gfm-task-list-item-at-point)
(markdown-toggle-gfm-checkbox))
+ ;; Align table
+ ((markdown-table-at-point-p)
+ (call-interactively #'markdown-table-align))
;; Otherwise
(t
(markdown-insert-gfm-checkbox))))
@@ -8927,6 +9033,643 @@ position."
(package-install 'edit-indirect)
(markdown-edit-code-block))))))
+
+;;; Table Editing
+
+;; These functions were originally adapted from `org-table.el'.
+
+;; General helper functions
+
+(defmacro markdown--with-gensyms (symbols &rest body)
+ (declare (debug (sexp body)) (indent 1))
+ `(let ,(mapcar (lambda (s)
+ `(,s (make-symbol (concat "--" (symbol-name ',s)))))
+ symbols)
+ ,@body))
+
+(defun markdown--split-string (string &optional separators)
+ "Splits STRING into substrings at SEPARATORS.
+SEPARATORS is a regular expression. If nil it defaults to
+`split-string-default-separators'. This version returns no empty
+strings if there are matches at the beginning and end of string."
+ (let ((start 0) notfirst list)
+ (while (and (string-match
+ (or separators split-string-default-separators)
+ string
+ (if (and notfirst
+ (= start (match-beginning 0))
+ (< start (length string)))
+ (1+ start) start))
+ (< (match-beginning 0) (length string)))
+ (setq notfirst t)
+ (or (eq (match-beginning 0) 0)
+ (and (eq (match-beginning 0) (match-end 0))
+ (eq (match-beginning 0) start))
+ (push (substring string start (match-beginning 0)) list))
+ (setq start (match-end 0)))
+ (or (eq start (length string))
+ (push (substring string start) list))
+ (nreverse list)))
+
+(defun markdown--string-width (s)
+ "Return width of string S.
+This version ignores characters with invisibility property
+`markdown-markup'."
+ (let (b)
+ (when (or (eq t buffer-invisibility-spec)
+ (member 'markdown-markup buffer-invisibility-spec))
+ (while (setq b (text-property-any
+ 0 (length s)
+ 'invisible 'markdown-markup s))
+ (setq s (concat
+ (substring s 0 b)
+ (substring s (or (next-single-property-change
+ b 'invisible s)
+ (length s))))))))
+ (string-width s))
+
+(defun markdown--remove-invisible-markup (s)
+ "Remove Markdown markup from string S.
+This version removes characters with invisibility property
+`markdown-markup'."
+ (let (b)
+ (while (setq b (text-property-any
+ 0 (length s)
+ 'invisible 'markdown-markup s))
+ (setq s (concat
+ (substring s 0 b)
+ (substring s (or (next-single-property-change
+ b 'invisible s)
+ (length s)))))))
+ s)
+
+;; Functions for maintaining tables
+
+(defconst markdown-table-line-regexp "^[ \t]*|"
+ "Regexp matching any line inside a table.")
+
+(defconst markdown-table-hline-regexp "^[ \t]*|[-:]"
+ "Regexp matching hline inside a table.")
+
+(defconst markdown-table-dline-regexp "^[ \t]*|[^-:]"
+ "Regexp matching dline inside a table.")
+
+(defconst markdown-table-border-regexp "^[ \t]*[^| \t]"
+ "Regexp matching any line outside a table.")
+
+(defun markdown-table-at-point-p ()
+ "Return non-nil when point is inside a table."
+ (save-excursion
+ (beginning-of-line)
+ (and (looking-at-p markdown-table-line-regexp)
+ (not (markdown-code-block-at-point-p)))))
+
+(defun markdown-table-hline-at-point-p ()
+ "Return non-nil when point is on a hline in a table.
+This function assumes point is on a table."
+ (save-excursion
+ (beginning-of-line)
+ (looking-at-p markdown-table-hline-regexp)))
+
+(defun markdown-table-begin ()
+ "Find the beginning of the table and return its position.
+This function assumes point is on a table."
+ (cond
+ ((save-excursion
+ (and (re-search-backward markdown-table-border-regexp nil t)
+ (line-beginning-position 2))))
+ (t (point-min))))
+
+(defun markdown-table-end ()
+ "Find the end of the table and return its position.
+This function assumes point is on a table."
+ (save-excursion
+ (cond
+ ((re-search-forward markdown-table-border-regexp nil t)
+ (match-beginning 0))
+ (t (goto-char (point-max))
+ (skip-chars-backward " \t")
+ (if (bolp) (point) (line-end-position))))))
+
+(defun markdown-table-get-dline ()
+ "Return index of the table data line at point.
+This function assumes point is on a table."
+ (let ((pos (point)) (end (markdown-table-end)) (cnt 0))
+ (save-excursion
+ (goto-char (markdown-table-begin))
+ (while (and (re-search-forward
+ markdown-table-dline-regexp end t)
+ (setq cnt (1+ cnt))
+ (< (point-at-eol) pos))))
+ cnt))
+
+(defun markdown-table-get-column ()
+ "Return table column at point.
+This function assumes point is on a table."
+ (let ((pos (point)) (cnt 0))
+ (save-excursion
+ (beginning-of-line)
+ (while (search-forward "|" pos t) (setq cnt (1+ cnt))))
+ cnt))
+
+(defun markdown-table-get-cell (&optional n)
+ "Return the content of the cell in column N of current row.
+N defaults to column at point. This function assumes point is on
+a table."
+ (and n (markdown-table-goto-column n))
+ (skip-chars-backward "^|\n") (backward-char 1)
+ (if (looking-at "|[^|\r\n]*")
+ (let* ((pos (match-beginning 0))
+ (val (buffer-substring (1+ pos) (match-end 0))))
+ (goto-char (min (point-at-eol) (+ 2 pos)))
+ ;; Trim whitespaces
+ (setq val (replace-regexp-in-string "\\`[ \t]+" "" val)
+ val (replace-regexp-in-string "[ \t]+\\'" "" val)))
+ (forward-char 1) ""))
+
+(defun markdown-table-goto-dline (N)
+ "Go to the Nth data line in the table at point.
+Return t when the line exists, nil otherwise. This function
+assumes point is on a table."
+ (goto-char (markdown-table-begin))
+ (let ((end (markdown-table-end)) (cnt 0))
+ (while (and (re-search-forward
+ markdown-table-dline-regexp end t)
+ (< (setq cnt (1+ cnt)) N)))
+ (= cnt N)))
+
+(defun markdown-table-goto-column (n &optional on-delim)
+ "Go to the Nth column in the table line at point.
+With optional argument ON-DELIM, stop with point before the left
+delimiter of the cell. If there are less than N cells, just go
+beyond the last delimiter. This function assumes point is on a
+table."
+ (beginning-of-line 1)
+ (when (> n 0)
+ (while (and (> (setq n (1- n)) -1)
+ (search-forward "|" (point-at-eol) t)))
+ (if on-delim
+ (backward-char 1)
+ (when (looking-at " ") (forward-char 1)))))
+
+(defmacro markdown-table-save-cell (&rest body)
+ "Save cell at point, execute BODY and restore cell.
+This function assumes point is on a table."
+ (declare (debug (body)))
+ (markdown--with-gensyms (line column)
+ `(let ((,line (copy-marker (line-beginning-position)))
+ (,column (markdown-table-get-column)))
+ (unwind-protect
+ (progn ,@body)
+ (goto-char ,line)
+ (markdown-table-goto-column ,column)
+ (set-marker ,line nil)))))
+
+(defun markdown-table-blank-line (s)
+ "Convert a table line S into a line with blank cells."
+ (if (string-match "^[ \t]*|-" s)
+ (setq s (mapconcat
+ (lambda (x) (if (member x '(?| ?+)) "|" " "))
+ s ""))
+ (while (string-match "|\\([ \t]*?[^ \t\r\n|][^\r\n|]*\\)|" s)
+ (setq s (replace-match
+ (concat "|" (make-string (length (match-string 1 s)) ?\ ) "|")
+ t t s)))
+ s))
+
+(defun markdown-table-colfmt (fmtspec)
+ "Process column alignment specifier FMTSPEC for tables."
+ (when (stringp fmtspec)
+ (mapcar (lambda (x)
+ (cond ((string-match-p "^:.*:$" x) 'c)
+ ((string-match-p "^:" x) 'l)
+ ((string-match-p ":$" x) 'r)
+ (t 'd)))
+ (markdown--split-string fmtspec "\\s-*|\\s-*"))))
+
+(defun markdown-table-align ()
+ "Align table at point.
+This function assumes point is on a table."
+ (interactive)
+ (let ((begin (markdown-table-begin))
+ (end (copy-marker (markdown-table-end))))
+ (markdown-table-save-cell
+ (goto-char begin)
+ (let* (fmtspec
+ ;; Store table indent
+ (indent (progn (looking-at "[ \t]*") (match-string 0)))
+ ;; Split table in lines and save column format specifier
+ (lines (mapcar (lambda (l)
+ (if (string-match-p "\\`[ \t]*|[-:]" l)
+ (progn (setq fmtspec (or fmtspec l)) nil) l))
+ (markdown--split-string (buffer-substring begin end) "\n")))
+ ;; Split lines in cells
+ (cells (mapcar (lambda (l) (markdown--split-string l "\\s-*|\\s-*"))
+ (remq nil lines)))
+ ;; Calculate maximum number of cells in a line
+ (maxcells (if cells
+ (apply #'max (mapcar #'length cells))
+ (user-error "Empty table")))
+ ;; Empty cells to fill short lines
+ (emptycells (make-list maxcells "")) maxwidths)
+ ;; Calculate maximum width for each column
+ (dotimes (i maxcells)
+ (let ((column (mapcar (lambda (x) (or (nth i x) "")) cells)))
+ (push (apply #'max 1 (mapcar #'markdown--string-width column))
+ maxwidths)))
+ (setq maxwidths (nreverse maxwidths))
+ ;; Process column format specifier
+ (setq fmtspec (markdown-table-colfmt fmtspec))
+ ;; Compute formats needed for output of table lines
+ (let ((hfmt (concat indent "|"))
+ (rfmt (concat indent "|"))
+ hfmt1 rfmt1 fmt)
+ (dolist (width maxwidths (setq hfmt (concat (substring hfmt 0 -1) "|")))
+ (setq fmt (pop fmtspec))
+ (cond ((equal fmt 'l) (setq hfmt1 ":%s-|" rfmt1 " %%-%ds |"))
+ ((equal fmt 'r) (setq hfmt1 "-%s:|" rfmt1 " %%%ds |"))
+ ((equal fmt 'c) (setq hfmt1 ":%s:|" rfmt1 " %%-%ds |"))
+ (t (setq hfmt1 "-%s-|" rfmt1 " %%-%ds |")))
+ (setq rfmt (concat rfmt (format rfmt1 width)))
+ (setq hfmt (concat hfmt (format hfmt1 (make-string width ?-)))))
+ ;; Replace modified lines only
+ (dolist (line lines)
+ (let ((line (if line
+ (apply #'format rfmt (append (pop cells) emptycells))
+ hfmt))
+ (previous (buffer-substring (point) (line-end-position))))
+ (if (equal previous line)
+ (forward-line)
+ (insert line "\n")
+ (delete-region (point) (line-beginning-position 2))))))
+ (set-marker end nil)))))
+
+(defun markdown-table-insert-row (&optional arg)
+ "Insert a new row above the row at point into the table.
+With optional argument ARG, insert below the current row."
+ (interactive "P")
+ (unless (markdown-table-at-point-p)
+ (user-error "Not at a table"))
+ (let* ((line (buffer-substring
+ (line-beginning-position) (line-end-position)))
+ (new (markdown-table-blank-line line)))
+ (beginning-of-line (if arg 2 1))
+ (unless (bolp) (insert "\n"))
+ (insert-before-markers new "\n")
+ (beginning-of-line 0)
+ (re-search-forward "| ?" (line-end-position) t)))
+
+(defun markdown-table-delete-row ()
+ "Delete row or horizontal line at point from the table."
+ (interactive)
+ (unless (markdown-table-at-point-p)
+ (user-error "Not at a table"))
+ (let ((col (current-column)))
+ (kill-region (point-at-bol)
+ (min (1+ (point-at-eol)) (point-max)))
+ (unless (markdown-table-at-point-p) (beginning-of-line 0))
+ (move-to-column col)))
+
+(defun markdown-table-move-row (&optional up)
+ "Move table line at point down.
+With optional argument UP, move it up."
+ (interactive "P")
+ (unless (markdown-table-at-point-p)
+ (user-error "Not at a table"))
+ (let* ((col (current-column)) (pos (point))
+ (tonew (if up 0 2)) txt)
+ (beginning-of-line tonew)
+ (unless (markdown-table-at-point-p)
+ (goto-char pos) (user-error "Cannot move row further"))
+ (goto-char pos) (beginning-of-line 1) (setq pos (point))
+ (setq txt (buffer-substring (point) (1+ (point-at-eol))))
+ (delete-region (point) (1+ (point-at-eol)))
+ (beginning-of-line tonew)
+ (insert txt) (beginning-of-line 0)
+ (move-to-column col)))
+
+(defun markdown-table-move-row-up ()
+ "Move table row at point up."
+ (interactive)
+ (markdown-table-move-row 'up))
+
+(defun markdown-table-move-row-down ()
+ "Move table row at point down."
+ (interactive)
+ (markdown-table-move-row nil))
+
+(defun markdown-table-insert-column ()
+ "Insert a new table column."
+ (interactive)
+ (unless (markdown-table-at-point-p)
+ (user-error "Not at a table"))
+ (let* ((col (max 1 (markdown-table-get-column)))
+ (begin (markdown-table-begin))
+ (end (copy-marker (markdown-table-end))))
+ (markdown-table-save-cell
+ (goto-char begin)
+ (while (< (point) end)
+ (markdown-table-goto-column col t)
+ (if (markdown-table-hline-at-point-p)
+ (insert "|---")
+ (insert "| "))
+ (forward-line)))
+ (set-marker end nil)
+ (markdown-table-align)))
+
+(defun markdown-table-delete-column ()
+ "Delete column at point from table."
+ (interactive)
+ (unless (markdown-table-at-point-p)
+ (user-error "Not at a table"))
+ (let ((col (markdown-table-get-column))
+ (begin (markdown-table-begin))
+ (end (copy-marker (markdown-table-end))))
+ (markdown-table-save-cell
+ (goto-char begin)
+ (while (< (point) end)
+ (markdown-table-goto-column col t)
+ (and (looking-at "|[^|\n]+|")
+ (replace-match "|"))
+ (forward-line)))
+ (set-marker end nil)
+ (markdown-table-goto-column (max 1 (1- col)))
+ (markdown-table-align)))
+
+(defun markdown-table-move-column (&optional left)
+ "Move table column at point to the right.
+With optional argument LEFT, move it to the left."
+ (interactive "P")
+ (unless (markdown-table-at-point-p)
+ (user-error "Not at a table"))
+ (let* ((col (markdown-table-get-column))
+ (col1 (if left (1- col) col))
+ (colpos (if left (1- col) (1+ col)))
+ (begin (markdown-table-begin))
+ (end (copy-marker (markdown-table-end))))
+ (when (and left (= col 1))
+ (user-error "Cannot move column further left"))
+ (when (and (not left) (looking-at "[^|\n]*|[^|\n]*$"))
+ (user-error "Cannot move column further right"))
+ (markdown-table-save-cell
+ (goto-char begin)
+ (while (< (point) end)
+ (markdown-table-goto-column col1 t)
+ (when (looking-at "|\\([^|\n]+\\)|\\([^|\n]+\\)|")
+ (replace-match "|\\2|\\1|"))
+ (forward-line)))
+ (set-marker end nil)
+ (markdown-table-goto-column colpos)
+ (markdown-table-align)))
+
+(defun markdown-table-move-column-left ()
+ "Move table column at point to the left."
+ (interactive)
+ (markdown-table-move-column 'left))
+
+(defun markdown-table-move-column-right ()
+ "Move table column at point to the right."
+ (interactive)
+ (markdown-table-move-column nil))
+
+(defun markdown-table-next-row ()
+ "Go to the next row (same column) in the table.
+Create new table lines if required."
+ (interactive)
+ (unless (markdown-table-at-point-p)
+ (user-error "Not at a table"))
+ (if (or (looking-at "[ \t]*$")
+ (save-excursion (skip-chars-backward " \t") (bolp)))
+ (newline)
+ (markdown-table-align)
+ (let ((col (markdown-table-get-column)))
+ (beginning-of-line 2)
+ (if (or (not (markdown-table-at-point-p))
+ (markdown-table-hline-at-point-p))
+ (progn
+ (beginning-of-line 0)
+ (markdown-table-insert-row 'below)))
+ (markdown-table-goto-column col)
+ (skip-chars-backward "^|\n\r")
+ (when (looking-at " ") (forward-char 1)))))
+
+(defun markdown-table-forward-cell ()
+ "Go to the next cell in the table.
+Create new table lines if required."
+ (interactive)
+ (unless (markdown-table-at-point-p)
+ (user-error "Not at a table"))
+ (markdown-table-align)
+ (let ((end (markdown-table-end)))
+ (when (markdown-table-hline-at-point-p) (end-of-line 1))
+ (condition-case nil
+ (progn
+ (re-search-forward "|" end)
+ (if (looking-at "[ \t]*$")
+ (re-search-forward "|" end))
+ (if (and (looking-at "[-:]")
+ (re-search-forward "^[ \t]*|\\([^-:]\\)" end t))
+ (goto-char (match-beginning 1)))
+ (if (looking-at "[-:]")
+ (progn
+ (beginning-of-line 0)
+ (markdown-table-insert-row 'below))
+ (when (looking-at " ") (forward-char 1))))
+ (error (markdown-table-insert-row 'below)))))
+
+(defun markdown-table-backward-cell ()
+ "Go to the previous cell in the table."
+ (interactive)
+ (unless (markdown-table-at-point-p)
+ (user-error "Not at a table"))
+ (markdown-table-align)
+ (when (markdown-table-hline-at-point-p) (end-of-line 1))
+ (condition-case nil
+ (progn
+ (re-search-backward "|" (markdown-table-begin))
+ (re-search-backward "|" (markdown-table-begin)))
+ (error (user-error "Cannot move to previous table cell")))
+ (while (looking-at "|\\([-:]\\|[ \t]*$\\)")
+ (re-search-backward "|" (markdown-table-begin)))
+ (when (looking-at "| ?") (goto-char (match-end 0))))
+
+(defun markdown-table-transpose ()
+ "Transpose table at point.
+Horizontal separator lines will be eliminated."
+ (interactive)
+ (unless (markdown-table-at-point-p)
+ (user-error "Not at a table"))
+ (let* ((table (buffer-substring-no-properties
+ (markdown-table-begin) (markdown-table-end)))
+ ;; Convert table to a Lisp structure
+ (table (delq nil
+ (mapcar
+ (lambda (x)
+ (unless (string-match-p
+ markdown-table-hline-regexp x)
+ (markdown--split-string x "\\s-*|\\s-*")))
+ (markdown--split-string table "[ \t]*\n[ \t]*"))))
+ (dline_old (markdown-table-get-dline))
+ (col_old (markdown-table-get-column))
+ (contents (mapcar (lambda (_)
+ (let ((tp table))
+ (mapcar
+ (lambda (_)
+ (prog1
+ (pop (car tp))
+ (setq tp (cdr tp))))
+ table)))
+ (car table))))
+ (goto-char (markdown-table-begin))
+ (re-search-forward "|") (backward-char)
+ (delete-region (point) (markdown-table-end))
+ (insert (mapconcat
+ (lambda(x)
+ (concat "| " (mapconcat 'identity x " | " ) " |\n"))
+ contents ""))
+ (markdown-table-goto-dline col_old)
+ (markdown-table-goto-column dline_old))
+ (markdown-table-align))
+
+(defun markdown-table-sort-lines (&optional sorting-type)
+ "Sort table lines according to the column at point.
+
+The position of point indicates the column to be used for
+sorting, and the range of lines is the range between the nearest
+horizontal separator lines, or the entire table of no such lines
+exist. If point is before the first column, user will be prompted
+for the sorting column. If there is an active region, the mark
+specifies the first line and the sorting column, while point
+should be in the last line to be included into the sorting.
+
+The command then prompts for the sorting type which can be
+alphabetically or numerically. Sorting in reverse order is also
+possible.
+
+If SORTING-TYPE is specified when this function is called from a
+Lisp program, no prompting will take place. SORTING-TYPE must be
+a character, any of (?a ?A ?n ?N) where the capital letters
+indicate that sorting should be done in reverse order."
+ (interactive)
+ (unless (markdown-table-at-point-p)
+ (user-error "Not at a table"))
+ ;; Set sorting type and column used for sorting
+ (let ((column (let ((c (markdown-table-get-column)))
+ (cond ((> c 0) c)
+ ((called-interactively-p 'any)
+ (read-number "Use column N for sorting: "))
+ (t 1))))
+ (sorting-type
+ (or sorting-type
+ (read-char-exclusive
+ "Sort type: [a]lpha [n]umeric (A/N means reversed): "))))
+ (save-restriction
+ ;; Narrow buffer to appropriate sorting area
+ (if (region-active-p)
+ (narrow-to-region
+ (save-excursion
+ (progn
+ (goto-char (region-beginning)) (line-beginning-position)))
+ (save-excursion
+ (progn
+ (goto-char (region-end)) (line-end-position))))
+ (let ((start (markdown-table-begin))
+ (end (markdown-table-end)))
+ (narrow-to-region
+ (save-excursion
+ (if (re-search-backward
+ markdown-table-hline-regexp start t)
+ (line-beginning-position 2)
+ start))
+ (if (save-excursion (re-search-forward
+ markdown-table-hline-regexp end t))
+ (match-beginning 0)
+ end))))
+ ;; Determine arguments for `sort-subr'
+ (let* ((extract-key-from-cell
+ (cl-case sorting-type
+ ((?a ?A) #'markdown--remove-invisible-markup) ;; #'identity)
+ ((?n ?N) #'string-to-number)
+ (t (user-error "Invalid sorting type: %c" sorting-type))))
+ (predicate
+ (cl-case sorting-type
+ ((?n ?N) #'<)
+ ((?a ?A) #'string<))))
+ ;; Sort selected area
+ (goto-char (point-min))
+ (sort-subr (memq sorting-type '(?A ?N))
+ (lambda ()
+ (forward-line)
+ (while (and (not (eobp))
+ (not (looking-at
+ markdown-table-dline-regexp)))
+ (forward-line)))
+ #'end-of-line
+ (lambda ()
+ (funcall extract-key-from-cell
+ (markdown-table-get-cell column)))
+ nil
+ predicate)
+ (goto-char (point-min))))))
+
+(defun markdown-table-convert-region (begin end &optional separator)
+ "Convert region from BEGIN to END to table with SEPARATOR.
+
+If every line contains at least one TAB character, the function
+assumes that the material is tab separated (TSV). If every line
+contains a comma, comma-separated values (CSV) are assumed. If
+not, lines are split at whitespace into cells.
+
+You can use a prefix argument to force a specific separator:
+\\[universal-argument] once forces CSV, \\[universal-argument]
+twice forces TAB, and \\[universal-argument] three times will
+prompt for a regular expression to match the separator, and a
+numeric argument N indicates that at least N consecutive
+spaces, or alternatively a TAB should be used as the separator."
+
+ (interactive "r\nP")
+ (let* ((begin (min begin end)) (end (max begin end)) re)
+ (goto-char begin) (beginning-of-line 1)
+ (setq begin (point-marker))
+ (goto-char end)
+ (if (bolp) (backward-char 1) (end-of-line 1))
+ (setq end (point-marker))
+ (when (equal separator '(64))
+ (setq separator (read-regexp "Regexp for cell separator: ")))
+ (unless separator
+ ;; Get the right cell separator
+ (goto-char begin)
+ (setq separator
+ (cond
+ ((not (re-search-forward "^[^\n\t]+$" end t)) '(16))
+ ((not (re-search-forward "^[^\n,]+$" end t)) '(4))
+ (t 1))))
+ (goto-char begin)
+ (if (equal separator '(4))
+ ;; Parse CSV
+ (while (< (point) end)
+ (cond
+ ((looking-at "^") (insert "| "))
+ ((looking-at "[ \t]*$") (replace-match " |") (beginning-of-line 2))
+ ((looking-at "[ \t]*\"\\([^\"\n]*\\)\"")
+ (replace-match "\\1") (if (looking-at "\"") (insert "\"")))
+ ((looking-at "[^,\n]+") (goto-char (match-end 0)))
+ ((looking-at "[ \t]*,") (replace-match " | "))
+ (t (beginning-of-line 2))))
+ (setq re
+ (cond
+ ((equal separator '(4)) "^\\|\"?[ \t]*,[ \t]*\"?")
+ ((equal separator '(16)) "^\\|\t")
+ ((integerp separator)
+ (if (< separator 1)
+ (user-error "Cell separator must contain one or more spaces")
+ (format "^ *\\| *\t *\\| \\{%d,\\}" separator)))
+ ((stringp separator) (format "^ *\\|%s" separator))
+ (t (error "Invalid cell separator"))))
+ (while (re-search-forward re end t) (replace-match "| " t t)))
+ (goto-char begin)
+ (markdown-table-align)))
+
;;; ElDoc Support