Table alignment for Emacs and Sublime Text (from dev 66442bdd4d5d)
This commit is contained in:
@@ -39,7 +39,7 @@ are regenerated on each release — patches cannot be merged directly.
|
|||||||
Report problems (or send patches) to the author; accepted changes are
|
Report problems (or send patches) to the author; accepted changes are
|
||||||
applied to the development tree and appear in a following snapshot.
|
applied to the development tree and appear in a following snapshot.
|
||||||
|
|
||||||
This snapshot was assembled from development commit `5d35f256476e`.
|
This snapshot was assembled from development commit `66442bdd4d5d`.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|||||||
@@ -13,12 +13,14 @@ and load the mode. Add to `~/.emacs.d/init.el`:
|
|||||||
(add-to-list 'load-path "full-pathname-of-the-emacs-directory")
|
(add-to-list 'load-path "full-pathname-of-the-emacs-directory")
|
||||||
(require 'klammertext-mode)
|
(require 'klammertext-mode)
|
||||||
(require 'klammertext-indent) ; optional, experimental: TAB indentation
|
(require 'klammertext-indent) ; optional, experimental: TAB indentation
|
||||||
|
(require 'klammertext-align) ; optional, experimental: table alignment
|
||||||
```
|
```
|
||||||
|
|
||||||
Replace `full-pathname-of-the-emacs-directory` with the full path to the
|
Replace `full-pathname-of-the-emacs-directory` with the full path to the
|
||||||
directory that contains `klammertext-mode.el`. The second require loads the
|
directory that contains `klammertext-mode.el`. The last two requires load
|
||||||
experimental indentation support (see "Indentation" below); it is a separate
|
the experimental indentation and table-alignment support (see their sections
|
||||||
unit — comment the line out to disable indentation entirely.
|
below); each is a separate unit — comment its line out to disable it
|
||||||
|
entirely.
|
||||||
|
|
||||||
The mode auto-activates for `.kt` and `.k` files. (The `.k` / `.kt` distinction
|
The mode auto-activates for `.kt` and `.k` files. (The `.k` / `.kt` distinction
|
||||||
is a filing convention, not a lexical one — the same mode serves both.) You can
|
is a filing convention, not a lexical one — the same mode serves both.) You can
|
||||||
@@ -196,6 +198,35 @@ indentation happens only when you ask for it (TAB, `indent-region`). The
|
|||||||
offset is `klammertext-indent-offset` (default 2); all three variables are
|
offset is `klammertext-indent-offset` (default 2); all three variables are
|
||||||
customizable in the `klammertext-indent` group.
|
customizable in the `klammertext-indent` group.
|
||||||
|
|
||||||
|
## Table alignment (experimental)
|
||||||
|
|
||||||
|
With `klammertext-align.el` loaded (the optional require above), **`C-c C-a`**
|
||||||
|
with point anywhere inside a `@table` span pads the cells of its rows so the
|
||||||
|
`|` separators line up:
|
||||||
|
|
||||||
|
```
|
||||||
|
@table
|
||||||
|
First item | Second | A third item that's longer ||
|
||||||
|
Row 2 | Text | Not as long ||
|
||||||
|
@
|
||||||
|
```
|
||||||
|
|
||||||
|
A row is one line ending with the row delimiter `||` (the customary trailing
|
||||||
|
delimiter — the parser strips one trailing delimiter, and it keeps every row
|
||||||
|
uniform, which also suits program-generated tables); the last row may omit
|
||||||
|
it. Alignment is for small data items, so a row is left untouched — and
|
||||||
|
contributes nothing to the column widths — when any of its cells is longer
|
||||||
|
than `klammertext-align-cell-max` (30) characters or the row spans lines.
|
||||||
|
If the aligned rows would exceed `klammertext-align-row-max` (100) columns,
|
||||||
|
nothing changes and the reason is reported.
|
||||||
|
|
||||||
|
The padding is semantically free: the SKS strips cell content, and no
|
||||||
|
whitespace is ever inserted inside a bar run (which would turn a `||` row
|
||||||
|
separator into an empty `| |` cell). Bars inside a nested klammer in a cell
|
||||||
|
(`@frac 1 | 2 @`) belong to that klammer, not the table, and are left alone.
|
||||||
|
Aligned rows adopt the leading whitespace of the first aligned row — run TAB
|
||||||
|
first if the rows disagree.
|
||||||
|
|
||||||
## Literal klammers
|
## Literal klammers
|
||||||
|
|
||||||
Inside a `literal` argument — for example the body of `@code ... code@` — `#`
|
Inside a `literal` argument — for example the body of `@code ... code@` — `#`
|
||||||
|
|||||||
455
doc/edit/emacs/klammertext-align.el
Normal file
455
doc/edit/emacs/klammertext-align.el
Normal file
@@ -0,0 +1,455 @@
|
|||||||
|
;;; klammertext-align.el --- Table alignment for Klammertext -*- lexical-binding: t; -*-
|
||||||
|
|
||||||
|
;; EXPERIMENTAL. Aligns the columns of a table klammer: with point anywhere
|
||||||
|
;; inside a @table span, `klammertext-align-table' (bound C-c C-a) pads the
|
||||||
|
;; cells of its rows so the | separators line up vertically:
|
||||||
|
;;
|
||||||
|
;; @table
|
||||||
|
;; First item | Second | A third item that's longer ||
|
||||||
|
;; Row 2 | Text | Not as long ||
|
||||||
|
;; @
|
||||||
|
;;
|
||||||
|
;; This file is a separate unit, loaded from the init file:
|
||||||
|
;;
|
||||||
|
;; (require 'klammertext-align)
|
||||||
|
;;
|
||||||
|
;; Comment that line out to disable alignment entirely. The Sublime Text
|
||||||
|
;; port doc/sublime/Klammertext_align.py implements the same algorithm —
|
||||||
|
;; keep the two in step.
|
||||||
|
;;
|
||||||
|
;; Alignment is for SMALL data items (2026-07-27):
|
||||||
|
;;
|
||||||
|
;; * A row is one line ending with the row delimiter || (the customary
|
||||||
|
;; trailing delimiter; the parser strips one trailing top-level
|
||||||
|
;; delimiter, and it keeps every row uniform). The last row may omit
|
||||||
|
;; the ||.
|
||||||
|
;; * A row is LEFT UNTOUCHED when any of its cells is longer than
|
||||||
|
;; `klammertext-align-cell-max' (30) characters, or when the row spans
|
||||||
|
;; lines (a cell with a newline). Untouched rows do not contribute to
|
||||||
|
;; the column widths.
|
||||||
|
;; * If the aligned rows would exceed `klammertext-align-row-max' (100)
|
||||||
|
;; columns, nothing is changed and the reason is reported — the general
|
||||||
|
;; case of long rows has no good answer, so the command declines rather
|
||||||
|
;; than guessing.
|
||||||
|
;;
|
||||||
|
;; Cell padding is semantically free: the SKS strips cell content, and no
|
||||||
|
;; whitespace is ever inserted inside a bar run (that would turn a || row
|
||||||
|
;; separator into an empty | | cell — the load-bearing-whitespace trap).
|
||||||
|
;; Bars inside a nested klammer (e.g. @frac 1 | 2 @ in a cell) belong to
|
||||||
|
;; that klammer, not the table: only bars at nesting depth 0 within the
|
||||||
|
;; table span count, the same depth rule the Klammermachine itself applies
|
||||||
|
;; to @cond. Aligned rows adopt the leading whitespace of the first
|
||||||
|
;; aligned row; run TAB / `indent-region' first if the rows disagree.
|
||||||
|
;;
|
||||||
|
;; SYNC: `klammertext-align-klammers' / `-cell-max' / `-row-max' are
|
||||||
|
;; mirrored as ALIGN_KLAMMERS / CELL_MAX / ROW_MAX in Klammertext_align.py
|
||||||
|
;; (a Sublime plugin cannot read these defcustoms).
|
||||||
|
|
||||||
|
;;; Code:
|
||||||
|
|
||||||
|
(require 'klammertext-mode)
|
||||||
|
|
||||||
|
(defgroup klammertext-align nil
|
||||||
|
"Table alignment for Klammertext files."
|
||||||
|
:group 'klammertext)
|
||||||
|
|
||||||
|
(defcustom klammertext-align-klammers '("table")
|
||||||
|
"Klammers whose rows `klammertext-align-table' aligns."
|
||||||
|
:type '(repeat string)
|
||||||
|
:group 'klammertext-align)
|
||||||
|
|
||||||
|
(defcustom klammertext-align-cell-max 30
|
||||||
|
"A row with a cell longer than this many characters is left untouched.
|
||||||
|
Alignment is for small data items."
|
||||||
|
:type 'integer
|
||||||
|
:group 'klammertext-align)
|
||||||
|
|
||||||
|
(defcustom klammertext-align-row-max 100
|
||||||
|
"Refuse to align when the aligned rows would exceed this many columns.
|
||||||
|
nil means no limit."
|
||||||
|
:type '(choice integer (const nil))
|
||||||
|
:group 'klammertext-align)
|
||||||
|
|
||||||
|
;; --- string helpers (the mode's helpers are buffer-based) ----------------
|
||||||
|
|
||||||
|
(defun klammertext-align--escaped-p (s pos)
|
||||||
|
"Non-nil if the char at POS in string S is escaped by an odd run of ^."
|
||||||
|
(let ((n 0) (i (1- pos)))
|
||||||
|
(while (and (>= i 0) (eq (aref s i) ?^))
|
||||||
|
(setq n (1+ n) i (1- i)))
|
||||||
|
(= (mod n 2) 1)))
|
||||||
|
|
||||||
|
(defun klammertext-align--block-end (s frm)
|
||||||
|
"Index just after the ]# closing a #[ block opened at FRM in string S.
|
||||||
|
Counts nested #[ ... ]#; (length S) if unclosed."
|
||||||
|
(let ((depth 1) (i frm) (n (length s)))
|
||||||
|
(while (> depth 0)
|
||||||
|
(let ((a (string-search "#[" s i))
|
||||||
|
(b (string-search "]#" s i)))
|
||||||
|
(cond
|
||||||
|
((and (null a) (null b))
|
||||||
|
(setq i n depth 0))
|
||||||
|
((or (null b) (and a (< a b)))
|
||||||
|
(setq depth (1+ depth) i (+ a 2)))
|
||||||
|
(t
|
||||||
|
(setq depth (1- depth) i (+ b 2))))))
|
||||||
|
i))
|
||||||
|
|
||||||
|
(defun klammertext-align--at-run-end (s pos)
|
||||||
|
"Index just after the run of @ that begins at POS in string S."
|
||||||
|
(let ((p pos) (n (length s)))
|
||||||
|
(while (and (< p n) (eq (aref s p) ?@))
|
||||||
|
(setq p (1+ p)))
|
||||||
|
p))
|
||||||
|
|
||||||
|
(defun klammertext-align--name-end (s pos)
|
||||||
|
"Index just past the run of name chars starting at POS in string S."
|
||||||
|
(let ((k pos) (n (length s)))
|
||||||
|
(while (and (< k n) (klammertext--name-char-p (aref s k)))
|
||||||
|
(setq k (1+ k)))
|
||||||
|
k))
|
||||||
|
|
||||||
|
;; --- finding the enclosing table span (buffer-based) ---------------------
|
||||||
|
|
||||||
|
(defun klammertext-align--enclosing-span (pos names)
|
||||||
|
"Innermost span of a klammer named in NAMES that contains POS.
|
||||||
|
Return (NAME CONTENT-START CONTENT-END) with CONTENT-START just after the
|
||||||
|
opening @NAME token and CONTENT-END at the start of the closing delimiter
|
||||||
|
token, or nil. Scans the buffer from the top with a position stack,
|
||||||
|
stepping over removed text, literal spans, escaped characters, and the
|
||||||
|
abbreviated @name-arg form."
|
||||||
|
(save-excursion
|
||||||
|
(goto-char (point-min))
|
||||||
|
(let ((stack nil) (found nil) (go t))
|
||||||
|
(while (and go (not found)
|
||||||
|
(re-search-forward "[@#]" nil t))
|
||||||
|
(let ((hit (1- (point))))
|
||||||
|
(cond
|
||||||
|
((klammertext--escaped-p hit))
|
||||||
|
((eq (char-after hit) ?#)
|
||||||
|
(let ((next (char-after (1+ hit))))
|
||||||
|
(cond
|
||||||
|
((eq next ?#) (setq go nil))
|
||||||
|
((eq next ?\[) (goto-char (klammertext--block-end (+ hit 2))))
|
||||||
|
((memq next '(?+ ?/ ?-)))
|
||||||
|
(t (goto-char (line-end-position))))))
|
||||||
|
(t
|
||||||
|
(let* ((run-end (klammertext--at-run-end hit))
|
||||||
|
(len (- run-end hit)))
|
||||||
|
(if (klammertext--name-char-p (char-after run-end))
|
||||||
|
(progn
|
||||||
|
(goto-char run-end)
|
||||||
|
(skip-chars-forward "A-Za-z0-9_")
|
||||||
|
(let ((name (buffer-substring-no-properties run-end (point))))
|
||||||
|
(cond
|
||||||
|
((and (= len 1)
|
||||||
|
(member name klammertext-literal-klammers))
|
||||||
|
(unless (re-search-forward
|
||||||
|
(concat (regexp-quote name) "@") nil t)
|
||||||
|
(setq go nil)))
|
||||||
|
((and (= len 1) (eq (char-after) ?-)))
|
||||||
|
(t (push (list name hit (point)) stack)))))
|
||||||
|
;; a close: token starts at the preceding name run, if any
|
||||||
|
(let* ((ns (save-excursion
|
||||||
|
(goto-char hit)
|
||||||
|
(skip-chars-backward "A-Za-z0-9_")
|
||||||
|
(point)))
|
||||||
|
(tok-start (if (and (< ns hit)
|
||||||
|
(or (= ns (point-min))
|
||||||
|
(not (eq (char-before ns) ?@))))
|
||||||
|
ns
|
||||||
|
hit))
|
||||||
|
(entry (pop stack)))
|
||||||
|
(when (and entry
|
||||||
|
(member (nth 0 entry) names)
|
||||||
|
(<= (nth 1 entry) pos)
|
||||||
|
(<= pos run-end))
|
||||||
|
(setq found (list (nth 0 entry) (nth 2 entry) tok-start)))
|
||||||
|
(goto-char run-end))))))))
|
||||||
|
found)))
|
||||||
|
|
||||||
|
;; --- scanning the span content, line by line -----------------------------
|
||||||
|
;; A line record is a vector:
|
||||||
|
;; [START END START-DEPTH END-DEPTH BARS BLOCKED COMMENT]
|
||||||
|
;; START/END are offsets into the content string (END excludes the newline);
|
||||||
|
;; BARS is a list of (POS . RUNLEN) for unescaped depth-0 bar runs, in order.
|
||||||
|
|
||||||
|
(defun klammertext-align--line-index (starts p)
|
||||||
|
"Index of the line (in the STARTS vector) containing offset P."
|
||||||
|
(let ((lo 0) (hi (1- (length starts))))
|
||||||
|
(while (< lo hi)
|
||||||
|
(let ((mid (/ (+ lo hi 1) 2)))
|
||||||
|
(if (<= (aref starts mid) p)
|
||||||
|
(setq lo mid)
|
||||||
|
(setq hi (1- mid)))))
|
||||||
|
lo))
|
||||||
|
|
||||||
|
(defun klammertext-align--block-lines (lines starts n a b)
|
||||||
|
"Mark every line record touched by [A, B) as blocked."
|
||||||
|
(let* ((last (max a (1- b)))
|
||||||
|
(k0 (klammertext-align--line-index starts a))
|
||||||
|
(k1 (klammertext-align--line-index starts (min last (max 0 (1- n))))))
|
||||||
|
(let ((k k0))
|
||||||
|
(while (<= k k1)
|
||||||
|
(aset (aref lines k) 5 t)
|
||||||
|
(setq k (1+ k))))))
|
||||||
|
|
||||||
|
(defun klammertext-align--scan-lines (content)
|
||||||
|
"Scan CONTENT (a table span's text). Return the vector of line records."
|
||||||
|
(let* ((n (length content))
|
||||||
|
(starts (let ((acc (list 0)))
|
||||||
|
(dotimes (idx n)
|
||||||
|
(when (eq (aref content idx) ?\n)
|
||||||
|
(push (1+ idx) acc)))
|
||||||
|
(vconcat (nreverse acc))))
|
||||||
|
(nlines (length starts))
|
||||||
|
(lines (make-vector nlines nil)))
|
||||||
|
(dotimes (k nlines)
|
||||||
|
(aset lines k (vector (aref starts k)
|
||||||
|
(if (< (1+ k) nlines)
|
||||||
|
(1- (aref starts (1+ k)))
|
||||||
|
n)
|
||||||
|
nil nil nil nil nil)))
|
||||||
|
(aset (aref lines 0) 2 0)
|
||||||
|
(let ((depth 0) (i 0) (go t))
|
||||||
|
(while (and go (< i n))
|
||||||
|
(let ((j i))
|
||||||
|
(while (and (< j n)
|
||||||
|
(not (memq (aref content j) '(?@ ?# ?| ?\n))))
|
||||||
|
(setq j (1+ j)))
|
||||||
|
(if (>= j n)
|
||||||
|
(setq go nil)
|
||||||
|
(let ((hit j) (c (aref content j)))
|
||||||
|
(setq i (1+ hit))
|
||||||
|
(cond
|
||||||
|
((eq c ?\n)
|
||||||
|
(let ((k (klammertext-align--line-index starts hit)))
|
||||||
|
(aset (aref lines k) 3 depth)
|
||||||
|
(when (< (1+ k) nlines)
|
||||||
|
(aset (aref lines (1+ k)) 2 depth))))
|
||||||
|
((klammertext-align--escaped-p content hit))
|
||||||
|
((eq c ?#)
|
||||||
|
(let ((next (and (< (1+ hit) n) (aref content (1+ hit)))))
|
||||||
|
(cond
|
||||||
|
((eq next ?#)
|
||||||
|
(klammertext-align--block-lines lines starts n hit n)
|
||||||
|
(setq go nil))
|
||||||
|
((eq next ?\[)
|
||||||
|
(let ((e (klammertext-align--block-end content (+ hit 2))))
|
||||||
|
(unless (= (klammertext-align--line-index starts (max hit (1- e)))
|
||||||
|
(klammertext-align--line-index starts hit))
|
||||||
|
(klammertext-align--block-lines lines starts n hit e))
|
||||||
|
(setq i e)))
|
||||||
|
((memq next '(?+ ?/ ?-)))
|
||||||
|
(t
|
||||||
|
(aset (aref lines (klammertext-align--line-index starts hit)) 6 t)
|
||||||
|
(let ((eol (string-search "\n" content hit)))
|
||||||
|
(setq i (or eol n)))))))
|
||||||
|
((eq c ?|)
|
||||||
|
(unless (and (> hit 0) (eq (aref content (1- hit)) ?|))
|
||||||
|
(let ((k hit))
|
||||||
|
(while (and (< k n) (eq (aref content k) ?|))
|
||||||
|
(setq k (1+ k)))
|
||||||
|
(when (= depth 0)
|
||||||
|
(let ((rec (aref lines (klammertext-align--line-index starts hit))))
|
||||||
|
(aset rec 4 (append (aref rec 4)
|
||||||
|
(list (cons hit (- k hit)))))))
|
||||||
|
(setq i k))))
|
||||||
|
(t ; ?@
|
||||||
|
(let* ((run-end (klammertext-align--at-run-end content hit))
|
||||||
|
(run-len (- run-end hit))
|
||||||
|
(after (and (< run-end n) (aref content run-end))))
|
||||||
|
(if (klammertext--name-char-p after)
|
||||||
|
(let* ((k (klammertext-align--name-end content run-end))
|
||||||
|
(name (substring content run-end k)))
|
||||||
|
(setq i k)
|
||||||
|
(cond
|
||||||
|
((and (= run-len 1)
|
||||||
|
(member name klammertext-literal-klammers))
|
||||||
|
(let* ((idx (string-search (concat name "@") content k))
|
||||||
|
(e (if idx (+ idx (length name) 1) n)))
|
||||||
|
(unless (= (klammertext-align--line-index
|
||||||
|
starts (max hit (1- e)))
|
||||||
|
(klammertext-align--line-index starts hit))
|
||||||
|
(klammertext-align--block-lines lines starts n hit e))
|
||||||
|
(setq i e)))
|
||||||
|
((and (= run-len 1) (< k n) (eq (aref content k) ?-)))
|
||||||
|
(t (setq depth (1+ depth)))))
|
||||||
|
(setq depth (max 0 (1- depth)))
|
||||||
|
(setq i run-end)))))))))
|
||||||
|
(dotimes (k nlines)
|
||||||
|
(let ((rec (aref lines k)))
|
||||||
|
(unless (aref rec 2) (aset rec 5 t))
|
||||||
|
(unless (aref rec 3) (aset rec 3 depth)))))
|
||||||
|
lines))
|
||||||
|
|
||||||
|
;; --- the alignment -------------------------------------------------------
|
||||||
|
|
||||||
|
(defun klammertext-align--indent-width (content rec)
|
||||||
|
"Width of the leading whitespace of line REC in CONTENT."
|
||||||
|
(let ((i (aref rec 0)) (end (aref rec 1)))
|
||||||
|
(while (and (< i end) (memq (aref content i) '(?\s ?\t)))
|
||||||
|
(setq i (1+ i)))
|
||||||
|
(- i (aref rec 0))))
|
||||||
|
|
||||||
|
(defun klammertext-align--trailing-rowsep-p (content rec)
|
||||||
|
"Non-nil when line REC's last depth-0 bar run is a || at the end of the
|
||||||
|
line (only whitespace after it)."
|
||||||
|
(let ((bars (aref rec 4)))
|
||||||
|
(and bars
|
||||||
|
(let* ((run (car (last bars)))
|
||||||
|
(pos (car run)))
|
||||||
|
(and (= (cdr run) 2)
|
||||||
|
(string-blank-p (substring content (+ pos 2) (aref rec 1))))))))
|
||||||
|
|
||||||
|
(defun klammertext-align--parse-row (content rec text chain-ok last-content-p)
|
||||||
|
"If line REC is an alignable row, return (REC CELLS TRAILING-P); else nil.
|
||||||
|
TEXT is the line's text; see the file header for the rules."
|
||||||
|
(catch 'no
|
||||||
|
(when (or (aref rec 5) (aref rec 6) (not chain-ok))
|
||||||
|
(throw 'no nil))
|
||||||
|
(unless (and (eql (aref rec 2) 0) (eql (aref rec 3) 0))
|
||||||
|
(throw 'no nil))
|
||||||
|
(unless (aref rec 4)
|
||||||
|
(throw 'no nil))
|
||||||
|
(let* ((trailing (klammertext-align--trailing-rowsep-p content rec))
|
||||||
|
(bars (aref rec 4))
|
||||||
|
(singles (if trailing (butlast bars) bars)))
|
||||||
|
(dolist (run singles)
|
||||||
|
(unless (= (cdr run) 1)
|
||||||
|
(throw 'no nil))) ; a mid-line || or ||| : not one row
|
||||||
|
(unless (or trailing last-content-p)
|
||||||
|
(throw 'no nil)) ; row continues onto the next line
|
||||||
|
(ignore text)
|
||||||
|
(let* ((cell-start (+ (aref rec 0)
|
||||||
|
(klammertext-align--indent-width content rec)))
|
||||||
|
(cell-end (if trailing (car (car (last bars))) (aref rec 1)))
|
||||||
|
(bounds (append (list cell-start)
|
||||||
|
(mapcar #'car singles)
|
||||||
|
(list cell-end)))
|
||||||
|
(cells nil)
|
||||||
|
(b-idx 0))
|
||||||
|
(while (< b-idx (1- (length bounds)))
|
||||||
|
(let* ((a (+ (nth b-idx bounds) (if (> b-idx 0) 1 0)))
|
||||||
|
(cell (string-trim (substring content a (nth (1+ b-idx) bounds)))))
|
||||||
|
(when (> (length cell) klammertext-align-cell-max)
|
||||||
|
(throw 'no nil))
|
||||||
|
(push cell cells)
|
||||||
|
(setq b-idx (1+ b-idx))))
|
||||||
|
(list rec (nreverse cells) trailing)))))
|
||||||
|
|
||||||
|
(defun klammertext-align--pad (cell width)
|
||||||
|
(concat cell (make-string (max 0 (- width (length cell))) ?\s)))
|
||||||
|
|
||||||
|
(defun klammertext-align--edits (content)
|
||||||
|
"Compute the alignment edits for CONTENT (a table span's text).
|
||||||
|
Return (EDITS . MESSAGE): EDITS is a list of (START END NEW) triples
|
||||||
|
relative to CONTENT, ascending; MESSAGE is a status string (a reason when
|
||||||
|
EDITS is nil)."
|
||||||
|
(let* ((lines (klammertext-align--scan-lines content))
|
||||||
|
(nlines (length lines))
|
||||||
|
(last-content nil))
|
||||||
|
(let ((k (1- nlines)))
|
||||||
|
(while (and (>= k 1) (null last-content))
|
||||||
|
(let ((rec (aref lines k)))
|
||||||
|
(unless (string-blank-p (substring content (aref rec 0) (aref rec 1)))
|
||||||
|
(setq last-content k)))
|
||||||
|
(setq k (1- k))))
|
||||||
|
(let ((rows nil) (chain-ok t))
|
||||||
|
(let ((k 1))
|
||||||
|
(while (< k nlines)
|
||||||
|
(let* ((rec (aref lines k))
|
||||||
|
(text (substring content (aref rec 0) (aref rec 1)))
|
||||||
|
(stripped (string-trim text)))
|
||||||
|
(cond
|
||||||
|
((string-empty-p stripped)) ; blank: chain unchanged
|
||||||
|
((and (string-prefix-p ":" stripped) ; option line: unchanged
|
||||||
|
(null (aref rec 4))))
|
||||||
|
(t
|
||||||
|
(let ((row (klammertext-align--parse-row
|
||||||
|
content rec text chain-ok (eql k last-content))))
|
||||||
|
(setq chain-ok (klammertext-align--trailing-rowsep-p content rec))
|
||||||
|
(when row (push row rows))))))
|
||||||
|
(setq k (1+ k))))
|
||||||
|
(setq rows (nreverse rows))
|
||||||
|
(if (null rows)
|
||||||
|
(cons nil "no alignable rows found")
|
||||||
|
(let* ((ncols (apply #'max (mapcar (lambda (r) (length (nth 1 r))) rows)))
|
||||||
|
(widths (make-vector ncols 0)))
|
||||||
|
(dolist (r rows)
|
||||||
|
(let ((c-idx 0))
|
||||||
|
(dolist (cell (nth 1 r))
|
||||||
|
(aset widths c-idx (max (aref widths c-idx) (length cell)))
|
||||||
|
(setq c-idx (1+ c-idx)))))
|
||||||
|
(let* ((indent (make-string (klammertext-align--indent-width
|
||||||
|
content (nth 0 (car rows)))
|
||||||
|
?\s))
|
||||||
|
(longest 0))
|
||||||
|
(dolist (r rows)
|
||||||
|
(let* ((m (length (nth 1 r)))
|
||||||
|
(w (+ (length indent)
|
||||||
|
(let ((sum 0) (c 0))
|
||||||
|
(while (< c m)
|
||||||
|
(setq sum (+ sum (aref widths c)) c (1+ c)))
|
||||||
|
sum)
|
||||||
|
(* 3 (1- m))
|
||||||
|
(if (nth 2 r) 3 0))))
|
||||||
|
(setq longest (max longest w))))
|
||||||
|
(if (and klammertext-align-row-max
|
||||||
|
(> longest klammertext-align-row-max))
|
||||||
|
(cons nil (format "aligned rows would be %d characters (limit %d); not aligning"
|
||||||
|
longest klammertext-align-row-max))
|
||||||
|
(let ((edits nil))
|
||||||
|
(dolist (r rows)
|
||||||
|
(let* ((rec (nth 0 r))
|
||||||
|
(cells (nth 1 r))
|
||||||
|
(trailing (nth 2 r))
|
||||||
|
(m (length cells))
|
||||||
|
(parts nil)
|
||||||
|
(c-idx 0))
|
||||||
|
(dolist (cell cells)
|
||||||
|
(push (if (and (= c-idx (1- m)) (not trailing))
|
||||||
|
cell
|
||||||
|
(klammertext-align--pad cell (aref widths c-idx)))
|
||||||
|
parts)
|
||||||
|
(setq c-idx (1+ c-idx)))
|
||||||
|
(let ((new (concat indent
|
||||||
|
(mapconcat #'identity (nreverse parts) " | ")
|
||||||
|
(if trailing " ||" ""))))
|
||||||
|
(unless (string= new (substring content
|
||||||
|
(aref rec 0) (aref rec 1)))
|
||||||
|
(push (list (aref rec 0) (aref rec 1) new) edits)))))
|
||||||
|
(setq edits (nreverse edits))
|
||||||
|
(cons edits
|
||||||
|
(if edits
|
||||||
|
(format "aligned %d rows" (length rows))
|
||||||
|
"already aligned"))))))))))
|
||||||
|
|
||||||
|
;; --- the command ---------------------------------------------------------
|
||||||
|
|
||||||
|
(defun klammertext-align-table ()
|
||||||
|
"Align the columns of the table klammer containing point.
|
||||||
|
Rows with a cell longer than `klammertext-align-cell-max' characters, or
|
||||||
|
spanning lines, are left untouched; see the file header for the full rules."
|
||||||
|
(interactive)
|
||||||
|
(let ((span (klammertext-align--enclosing-span (point)
|
||||||
|
klammertext-align-klammers)))
|
||||||
|
(unless span
|
||||||
|
(user-error "Point is not inside a table klammer (%s)"
|
||||||
|
(mapconcat (lambda (name) (concat "@" name))
|
||||||
|
klammertext-align-klammers ", ")))
|
||||||
|
(let* ((beg (nth 1 span))
|
||||||
|
(content (buffer-substring-no-properties beg (nth 2 span)))
|
||||||
|
(result (klammertext-align--edits content)))
|
||||||
|
(save-excursion
|
||||||
|
(dolist (e (sort (copy-sequence (car result))
|
||||||
|
(lambda (a b) (> (nth 0 a) (nth 0 b)))))
|
||||||
|
(goto-char (+ beg (nth 0 e)))
|
||||||
|
(delete-region (+ beg (nth 0 e)) (+ beg (nth 1 e)))
|
||||||
|
(insert (nth 2 e))))
|
||||||
|
(message "Klammertext: %s" (cdr result)))))
|
||||||
|
|
||||||
|
(define-key klammertext-mode-map (kbd "C-c C-a") #'klammertext-align-table)
|
||||||
|
|
||||||
|
(provide 'klammertext-align)
|
||||||
|
;;; klammertext-align.el ends here
|
||||||
@@ -28,5 +28,12 @@
|
|||||||
"context": [
|
"context": [
|
||||||
{ "key": "selector", "operator": "equal", "operand": "text.klammertext" }
|
{ "key": "selector", "operator": "equal", "operand": "text.klammertext" }
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"keys": ["ctrl+alt+a"],
|
||||||
|
"command": "klammertext_align_table",
|
||||||
|
"context": [
|
||||||
|
{ "key": "selector", "operator": "equal", "operand": "text.klammertext" }
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
426
doc/edit/sublime/Klammertext_align.py
Normal file
426
doc/edit/sublime/Klammertext_align.py
Normal file
@@ -0,0 +1,426 @@
|
|||||||
|
# Klammertext_align.py
|
||||||
|
#
|
||||||
|
# EXPERIMENTAL. Table alignment for Klammertext files — pads the cells of a
|
||||||
|
# klammer's rows so the | separators line up vertically. Companion to
|
||||||
|
# doc/emacs/klammertext-align.el (the same algorithm; keep the two in step).
|
||||||
|
# This file is a separate unit: delete it (or move it out of the package
|
||||||
|
# folder) to disable alignment entirely.
|
||||||
|
#
|
||||||
|
# Command name (for keymaps / the command palette): klammertext_align_table
|
||||||
|
# Keybinding: Ctrl+Alt+A (in Default.sublime-keymap), scoped to Klammertext
|
||||||
|
# files. With the caret anywhere inside a @table span, the command aligns
|
||||||
|
# that table's rows:
|
||||||
|
#
|
||||||
|
# @table
|
||||||
|
# First item | Second | A third item that's longer ||
|
||||||
|
# Row 2 | Text | Not as long ||
|
||||||
|
# @
|
||||||
|
#
|
||||||
|
# Alignment is for SMALL data items (2026-07-27):
|
||||||
|
#
|
||||||
|
# * A row is one line ending with the row delimiter || (the customary
|
||||||
|
# trailing delimiter; the parser strips one trailing top-level delimiter,
|
||||||
|
# and it keeps every row uniform). The last row may omit the ||.
|
||||||
|
# * A row is LEFT UNTOUCHED when any of its cells is longer than CELL_MAX
|
||||||
|
# (30) characters, or when the row spans lines (a cell with a newline).
|
||||||
|
# Untouched rows do not contribute to the column widths.
|
||||||
|
# * If the aligned rows would exceed ROW_MAX (100) columns, nothing is
|
||||||
|
# changed and the status bar says so — the general case of long rows has
|
||||||
|
# no good answer, so the command declines rather than guessing.
|
||||||
|
#
|
||||||
|
# Cell padding is semantically free: the SKS strips cell content, and no
|
||||||
|
# whitespace is ever inserted inside a bar run (that would turn a || row
|
||||||
|
# separator into an empty | | cell — the load-bearing-whitespace trap).
|
||||||
|
# Bars inside a nested klammer (e.g. @frac 1 | 2 @ in a cell) belong to that
|
||||||
|
# klammer, not the table: only bars at nesting depth 0 within the table span
|
||||||
|
# count, the same depth rule the Klammermachine itself applies to @cond.
|
||||||
|
# Aligned rows adopt the leading whitespace of the first aligned row; run
|
||||||
|
# the reindent command (Ctrl+Alt+I) first if the rows disagree.
|
||||||
|
#
|
||||||
|
# SYNC: ALIGN_KLAMMERS / CELL_MAX / ROW_MAX mirror the Emacs defcustoms
|
||||||
|
# klammertext-align-klammers / -cell-max / -row-max in klammertext-align.el.
|
||||||
|
# LITERAL_KLAMMERS is the same four-way synced list as everywhere else. The
|
||||||
|
# scanning helpers are duplicated from Klammertext.py so this file stays a
|
||||||
|
# deletable unit with no import coupling.
|
||||||
|
|
||||||
|
try:
|
||||||
|
import sublime
|
||||||
|
import sublime_plugin
|
||||||
|
_IN_SUBLIME = True
|
||||||
|
except ImportError: # standalone testing outside Sublime Text
|
||||||
|
_IN_SUBLIME = False
|
||||||
|
|
||||||
|
import bisect
|
||||||
|
|
||||||
|
ALIGN_KLAMMERS = set(["table"])
|
||||||
|
CELL_MAX = 30
|
||||||
|
ROW_MAX = 100
|
||||||
|
LITERAL_KLAMMERS = set(["code"])
|
||||||
|
|
||||||
|
|
||||||
|
# --- pure helpers (duplicated from Klammertext.py; see SYNC note above) -----
|
||||||
|
|
||||||
|
def name_char_p(ch):
|
||||||
|
"""True if CH can be part of a klammer name (letter, digit or _)."""
|
||||||
|
if ch is None:
|
||||||
|
return False
|
||||||
|
return (('a' <= ch <= 'z') or ('A' <= ch <= 'Z')
|
||||||
|
or ('0' <= ch <= '9') or ch == '_')
|
||||||
|
|
||||||
|
|
||||||
|
def escaped_p(s, pos):
|
||||||
|
"""True if the char at POS is escaped by an odd run of ^ before it."""
|
||||||
|
n = 0
|
||||||
|
i = pos - 1
|
||||||
|
while i >= 0 and s[i] == '^':
|
||||||
|
n += 1
|
||||||
|
i -= 1
|
||||||
|
return (n % 2) == 1
|
||||||
|
|
||||||
|
|
||||||
|
def block_end(s, frm):
|
||||||
|
"""Index just after the ]# that closes a #[ block opened at FROM (the index
|
||||||
|
just after the opening #[). Counts nested #[ ... ]#; len(s) if unclosed."""
|
||||||
|
depth = 1
|
||||||
|
i = frm
|
||||||
|
n = len(s)
|
||||||
|
while depth > 0:
|
||||||
|
a = s.find('#[', i)
|
||||||
|
b = s.find(']#', i)
|
||||||
|
if a == -1 and b == -1:
|
||||||
|
return n
|
||||||
|
if b == -1 or (a != -1 and a < b):
|
||||||
|
depth += 1
|
||||||
|
i = a + 2
|
||||||
|
else:
|
||||||
|
depth -= 1
|
||||||
|
i = b + 2
|
||||||
|
return i
|
||||||
|
|
||||||
|
|
||||||
|
def at_run_end(s, pos):
|
||||||
|
"""Index just after the run of @ that begins at POS."""
|
||||||
|
p = pos
|
||||||
|
n = len(s)
|
||||||
|
while p < n and s[p] == '@':
|
||||||
|
p += 1
|
||||||
|
return p
|
||||||
|
|
||||||
|
|
||||||
|
# --- finding the enclosing table span ---------------------------------------
|
||||||
|
|
||||||
|
def enclosing_span(s, pos, names):
|
||||||
|
"""Innermost span of a klammer named in NAMES that contains POS.
|
||||||
|
Return (name, content_start, content_end) with content_start just after
|
||||||
|
the opening @name token and content_end at the start of the closing
|
||||||
|
delimiter token, or None. Scans S from the start with a position stack,
|
||||||
|
stepping over removed text, literal spans, escaped characters, and the
|
||||||
|
abbreviated @name-arg form."""
|
||||||
|
stack = [] # (name, open_token_start, content_start)
|
||||||
|
n = len(s)
|
||||||
|
i = 0
|
||||||
|
while i < n:
|
||||||
|
j = i
|
||||||
|
while j < n and s[j] != '@' and s[j] != '#':
|
||||||
|
j += 1
|
||||||
|
if j >= n:
|
||||||
|
break
|
||||||
|
hit = j
|
||||||
|
i = hit + 1
|
||||||
|
if escaped_p(s, hit):
|
||||||
|
continue
|
||||||
|
nxt = s[hit + 1] if hit + 1 < n else None
|
||||||
|
if s[hit] == '#':
|
||||||
|
if nxt == '#':
|
||||||
|
break # ## removes the rest of the buffer
|
||||||
|
elif nxt == '[':
|
||||||
|
i = block_end(s, hit + 2)
|
||||||
|
elif nxt in ('+', '/', '-'):
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
eol = s.find('\n', hit)
|
||||||
|
i = n if eol == -1 else eol
|
||||||
|
continue
|
||||||
|
run_end = at_run_end(s, hit)
|
||||||
|
run_len = run_end - hit
|
||||||
|
after = s[run_end] if run_end < n else None
|
||||||
|
if name_char_p(after):
|
||||||
|
k = run_end
|
||||||
|
while k < n and name_char_p(s[k]):
|
||||||
|
k += 1
|
||||||
|
name = s[run_end:k]
|
||||||
|
i = k
|
||||||
|
if run_len == 1 and name in LITERAL_KLAMMERS:
|
||||||
|
idx = s.find(name + '@', k)
|
||||||
|
if idx == -1:
|
||||||
|
break
|
||||||
|
i = idx + len(name) + 1
|
||||||
|
elif run_len == 1 and k < n and s[k] == '-':
|
||||||
|
pass # @name-arg : opens no span
|
||||||
|
else:
|
||||||
|
stack.append((name, hit, k))
|
||||||
|
else:
|
||||||
|
# a close: the token starts at the preceding name run, if any
|
||||||
|
ns = hit
|
||||||
|
while ns > 0 and name_char_p(s[ns - 1]):
|
||||||
|
ns -= 1
|
||||||
|
tok_start = ns if (ns < hit and (ns == 0 or s[ns - 1] != '@')) else hit
|
||||||
|
if stack:
|
||||||
|
name, open_start, content_start = stack.pop()
|
||||||
|
if name in names and open_start <= pos <= run_end:
|
||||||
|
return (name, content_start, tok_start)
|
||||||
|
i = run_end
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
# --- scanning the span content, line by line --------------------------------
|
||||||
|
|
||||||
|
def scan_lines(content):
|
||||||
|
"""Scan CONTENT (the text of a table span). Return a list of line
|
||||||
|
records, one per line: dicts with start, end (offsets into CONTENT, end
|
||||||
|
excludes the newline), start_depth, end_depth (klammer nesting relative
|
||||||
|
to the span), bars (list of (pos, runlen) for unescaped depth-0 bar
|
||||||
|
runs), blocked (inside removed/verbatim content), comment (a # removes
|
||||||
|
the rest of the line)."""
|
||||||
|
n = len(content)
|
||||||
|
line_starts = [0]
|
||||||
|
for idx, ch in enumerate(content):
|
||||||
|
if ch == '\n':
|
||||||
|
line_starts.append(idx + 1)
|
||||||
|
nlines = len(line_starts)
|
||||||
|
|
||||||
|
def line_index(p):
|
||||||
|
return bisect.bisect_right(line_starts, p) - 1
|
||||||
|
|
||||||
|
lines = [{'start': line_starts[k],
|
||||||
|
'end': (line_starts[k + 1] - 1 if k + 1 < nlines else n),
|
||||||
|
'bars': [], 'blocked': False, 'comment': False,
|
||||||
|
'start_depth': None, 'end_depth': None}
|
||||||
|
for k in range(nlines)]
|
||||||
|
lines[0]['start_depth'] = 0
|
||||||
|
|
||||||
|
def block_range(a, b):
|
||||||
|
"""Mark every line touched by [a, b) as blocked."""
|
||||||
|
last = max(a, b - 1)
|
||||||
|
for k in range(line_index(a), line_index(min(last, n - 1)) + 1):
|
||||||
|
lines[k]['blocked'] = True
|
||||||
|
|
||||||
|
depth = 0
|
||||||
|
i = 0
|
||||||
|
while i < n:
|
||||||
|
j = i
|
||||||
|
while j < n and content[j] not in '@#|\n':
|
||||||
|
j += 1
|
||||||
|
if j >= n:
|
||||||
|
break
|
||||||
|
hit = j
|
||||||
|
i = hit + 1
|
||||||
|
c = content[hit]
|
||||||
|
if c == '\n':
|
||||||
|
k = line_index(hit)
|
||||||
|
lines[k]['end_depth'] = depth
|
||||||
|
if k + 1 < nlines:
|
||||||
|
lines[k + 1]['start_depth'] = depth
|
||||||
|
continue
|
||||||
|
if escaped_p(content, hit):
|
||||||
|
continue
|
||||||
|
nxt = content[hit + 1] if hit + 1 < n else None
|
||||||
|
if c == '#':
|
||||||
|
if nxt == '#':
|
||||||
|
block_range(hit, n)
|
||||||
|
break
|
||||||
|
elif nxt == '[':
|
||||||
|
e = block_end(content, hit + 2)
|
||||||
|
if line_index(max(hit, e - 1)) != line_index(hit):
|
||||||
|
block_range(hit, e)
|
||||||
|
i = e
|
||||||
|
elif nxt in ('+', '/', '-'):
|
||||||
|
pass
|
||||||
|
else: # # to end of line
|
||||||
|
lines[line_index(hit)]['comment'] = True
|
||||||
|
eol = content.find('\n', hit)
|
||||||
|
i = n if eol == -1 else eol
|
||||||
|
continue
|
||||||
|
if c == '|':
|
||||||
|
if hit > 0 and content[hit - 1] == '|':
|
||||||
|
continue # mid-run (after an escaped ^|)
|
||||||
|
k = hit
|
||||||
|
while k < n and content[k] == '|':
|
||||||
|
k += 1
|
||||||
|
if depth == 0:
|
||||||
|
lines[line_index(hit)]['bars'].append((hit, k - hit))
|
||||||
|
i = k
|
||||||
|
continue
|
||||||
|
# '@'
|
||||||
|
run_end = at_run_end(content, hit)
|
||||||
|
run_len = run_end - hit
|
||||||
|
after = content[run_end] if run_end < n else None
|
||||||
|
if name_char_p(after):
|
||||||
|
k = run_end
|
||||||
|
while k < n and name_char_p(content[k]):
|
||||||
|
k += 1
|
||||||
|
name = content[run_end:k]
|
||||||
|
i = k
|
||||||
|
if run_len == 1 and name in LITERAL_KLAMMERS:
|
||||||
|
idx = content.find(name + '@', k)
|
||||||
|
e = n if idx == -1 else idx + len(name) + 1
|
||||||
|
if line_index(max(hit, e - 1)) != line_index(hit):
|
||||||
|
block_range(hit, e)
|
||||||
|
i = e
|
||||||
|
elif run_len == 1 and k < n and content[k] == '-':
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
depth += 1
|
||||||
|
else:
|
||||||
|
depth = max(0, depth - 1)
|
||||||
|
i = run_end
|
||||||
|
|
||||||
|
for ln in lines:
|
||||||
|
if ln['start_depth'] is None:
|
||||||
|
ln['blocked'] = True
|
||||||
|
if ln['end_depth'] is None:
|
||||||
|
ln['end_depth'] = depth
|
||||||
|
return lines
|
||||||
|
|
||||||
|
|
||||||
|
# --- the alignment ----------------------------------------------------------
|
||||||
|
|
||||||
|
def compute_edits(content):
|
||||||
|
"""Compute the alignment edits for CONTENT (a table span's text).
|
||||||
|
Return (edits, message): edits is a list of (start, end, new_text)
|
||||||
|
triples relative to CONTENT, ascending; message is a status string (a
|
||||||
|
reason when edits is empty)."""
|
||||||
|
lines = scan_lines(content)
|
||||||
|
n = len(content)
|
||||||
|
|
||||||
|
# The last line holding actual content (a row there may omit its ||).
|
||||||
|
last_content = None
|
||||||
|
for k in range(len(lines) - 1, 0, -1):
|
||||||
|
if content[lines[k]['start']:lines[k]['end']].strip():
|
||||||
|
last_content = k
|
||||||
|
break
|
||||||
|
|
||||||
|
rows = [] # (line record, cells, trailing_p)
|
||||||
|
chain_ok = True # a row must START a row: the previous
|
||||||
|
for k in range(1, len(lines)): # content line ended with || (or was the
|
||||||
|
ln = lines[k] # opener / an option line / blank)
|
||||||
|
text = content[ln['start']:ln['end']]
|
||||||
|
stripped = text.strip()
|
||||||
|
if not stripped:
|
||||||
|
continue # blank line: chain unchanged
|
||||||
|
if stripped.startswith(':') and not ln['bars']:
|
||||||
|
continue # option line: chain unchanged
|
||||||
|
row = _parse_row(content, ln, text, chain_ok, k == last_content)
|
||||||
|
trailing = _trailing_rowsep(content, ln)
|
||||||
|
chain_ok = trailing
|
||||||
|
if row is not None:
|
||||||
|
rows.append(row)
|
||||||
|
|
||||||
|
if not rows:
|
||||||
|
return ([], "no alignable rows found")
|
||||||
|
|
||||||
|
widths = []
|
||||||
|
for _ln, cells, _tr in rows:
|
||||||
|
for c_idx, cell in enumerate(cells):
|
||||||
|
if c_idx >= len(widths):
|
||||||
|
widths.append(0)
|
||||||
|
widths[c_idx] = max(widths[c_idx], len(cell))
|
||||||
|
|
||||||
|
indent = ' ' * _indent_width(content, rows[0][0])
|
||||||
|
if ROW_MAX is not None:
|
||||||
|
longest = 0
|
||||||
|
for _ln, cells, trailing in rows:
|
||||||
|
m = len(cells)
|
||||||
|
w = (len(indent) + sum(widths[:m]) + 3 * (m - 1)
|
||||||
|
+ (3 if trailing else 0))
|
||||||
|
longest = max(longest, w)
|
||||||
|
if longest > ROW_MAX:
|
||||||
|
return ([], "aligned rows would be %d characters (limit %d); "
|
||||||
|
"not aligning" % (longest, ROW_MAX))
|
||||||
|
|
||||||
|
edits = []
|
||||||
|
for ln, cells, trailing in rows:
|
||||||
|
parts = [cells[c].ljust(widths[c]) for c in range(len(cells) - 1)]
|
||||||
|
last = cells[-1]
|
||||||
|
if trailing:
|
||||||
|
last = last.ljust(widths[len(cells) - 1])
|
||||||
|
parts.append(last)
|
||||||
|
new = indent + ' | '.join(parts) + (' ||' if trailing else '')
|
||||||
|
if new != content[ln['start']:ln['end']]:
|
||||||
|
edits.append((ln['start'], ln['end'], new))
|
||||||
|
msg = ("aligned %d rows" % len(rows)) if edits else "already aligned"
|
||||||
|
return (edits, msg)
|
||||||
|
|
||||||
|
|
||||||
|
def _indent_width(content, ln):
|
||||||
|
i = ln['start']
|
||||||
|
while i < ln['end'] and content[i] in ' \t':
|
||||||
|
i += 1
|
||||||
|
return i - ln['start']
|
||||||
|
|
||||||
|
|
||||||
|
def _trailing_rowsep(content, ln):
|
||||||
|
"""True when the line's LAST depth-0 bar run is a || sitting at the end of
|
||||||
|
the line (only whitespace after it)."""
|
||||||
|
if not ln['bars']:
|
||||||
|
return False
|
||||||
|
pos, runlen = ln['bars'][-1]
|
||||||
|
return (runlen == 2
|
||||||
|
and content[pos + 2:ln['end']].strip() == '')
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_row(content, ln, text, chain_ok, is_last_content):
|
||||||
|
"""If the line is an alignable row, return (ln, cells, trailing_p);
|
||||||
|
else None. See the file header for the rules."""
|
||||||
|
if ln['blocked'] or ln['comment'] or not chain_ok:
|
||||||
|
return None
|
||||||
|
if ln['start_depth'] != 0 or ln['end_depth'] != 0:
|
||||||
|
return None
|
||||||
|
if not ln['bars']:
|
||||||
|
return None
|
||||||
|
trailing = _trailing_rowsep(content, ln)
|
||||||
|
singles = ln['bars'][:-1] if trailing else ln['bars']
|
||||||
|
for _pos, runlen in singles:
|
||||||
|
if runlen != 1:
|
||||||
|
return None # a mid-line || (or |||): not one row
|
||||||
|
if not trailing and not is_last_content:
|
||||||
|
return None # row continues onto the next line
|
||||||
|
cell_start = ln['start'] + _indent_width(content, ln)
|
||||||
|
cell_end = ln['bars'][-1][0] if trailing else ln['end']
|
||||||
|
bounds = [cell_start] + [p for p, _r in singles] + [cell_end]
|
||||||
|
cells = []
|
||||||
|
for b_idx in range(len(bounds) - 1):
|
||||||
|
a = bounds[b_idx] + (1 if b_idx > 0 else 0) # skip the | itself
|
||||||
|
cell = content[a:bounds[b_idx + 1]].strip()
|
||||||
|
if len(cell) > CELL_MAX:
|
||||||
|
return None
|
||||||
|
cells.append(cell)
|
||||||
|
return (ln, cells, trailing)
|
||||||
|
|
||||||
|
|
||||||
|
# --- the command ------------------------------------------------------------
|
||||||
|
|
||||||
|
if _IN_SUBLIME:
|
||||||
|
|
||||||
|
class KlammertextAlignTableCommand(sublime_plugin.TextCommand):
|
||||||
|
"""Align the columns of the table klammer containing the caret.
|
||||||
|
Bound to Ctrl+Alt+A."""
|
||||||
|
|
||||||
|
def run(self, edit):
|
||||||
|
view = self.view
|
||||||
|
s = view.substr(sublime.Region(0, view.size()))
|
||||||
|
pos = view.sel()[0].b if len(view.sel()) else 0
|
||||||
|
span = enclosing_span(s, pos, ALIGN_KLAMMERS)
|
||||||
|
if span is None:
|
||||||
|
sublime.status_message(
|
||||||
|
"Klammertext: the caret is not inside a table klammer (%s)"
|
||||||
|
% ", ".join("@" + name for name in sorted(ALIGN_KLAMMERS)))
|
||||||
|
return
|
||||||
|
_name, cs, ce = span
|
||||||
|
edits, msg = compute_edits(s[cs:ce])
|
||||||
|
for a, b, new in sorted(edits, reverse=True):
|
||||||
|
view.replace(edit, sublime.Region(cs + a, cs + b), new)
|
||||||
|
sublime.status_message("Klammertext: " + msg)
|
||||||
|
|
||||||
|
def is_enabled(self):
|
||||||
|
return self.view.match_selector(0, "text.klammertext")
|
||||||
@@ -12,7 +12,8 @@ Emacs mode closely; where the two intentionally differ, the file headers say so.
|
|||||||
| `Klammertext.sublime-syntax` | Syntax highlighting. Colors the text-removal constructs (`#`, `##`, `#[...]#`) and the three `@`-tiers — application `@`, definition `@@`, system `@@@` — each as an opening vs. a close, plus `^`-escapes and verbatim `@code ... code@` spans. |
|
| `Klammertext.sublime-syntax` | Syntax highlighting. Colors the text-removal constructs (`#`, `##`, `#[...]#`) and the three `@`-tiers — application `@`, definition `@@`, system `@@@` — each as an opening vs. a close, plus `^`-escapes and verbatim `@code ... code@` spans. |
|
||||||
| `Klammertext.py` | Plugin with two features that share one context-sensitive matcher: jump between an opening and its close, and live highlighting of the matching delimiter as the caret moves (mismatched or unbalanced delimiters flag in red). |
|
| `Klammertext.py` | Plugin with two features that share one context-sensitive matcher: jump between an opening and its close, and live highlighting of the matching delimiter as the caret moves (mismatched or unbalanced delimiters flag in red). |
|
||||||
| `Klammertext_indent.py` | **Experimental.** Reindentation per the Klammertext convention (see below). A separate unit: delete this one file to disable indentation; nothing else is affected. |
|
| `Klammertext_indent.py` | **Experimental.** Reindentation per the Klammertext convention (see below). A separate unit: delete this one file to disable indentation; nothing else is affected. |
|
||||||
| `Default.sublime-keymap` | Binds jump-to-match to **Ctrl+M** and reindent to **Ctrl+Alt+I**, scoped to Klammertext files. |
|
| `Klammertext_align.py` | **Experimental.** Table alignment (see below). Also a separate, deletable unit. |
|
||||||
|
| `Default.sublime-keymap` | Binds jump-to-match to **Ctrl+M**, reindent to **Ctrl+Alt+I**, and table alignment to **Ctrl+Alt+A**, scoped to Klammertext files. |
|
||||||
| `Comments.tmPreferences` | Comment toggling: **Ctrl+/** inserts `# ` (line removal), **Ctrl+Shift+/** wraps in `#[ ... ]#` (block removal). |
|
| `Comments.tmPreferences` | Comment toggling: **Ctrl+/** inserts `# ` (line removal), **Ctrl+Shift+/** wraps in `#[ ... ]#` (block removal). |
|
||||||
| `Breakers` / `Celeste` / `Mariana` / `Monokai` / `Sixteen` `.sublime-color-scheme` | Color overrides for Sublime's five built-in schemes — one hue system, full intensity on the dark schemes, scaled down on the light ones. Additive: they recolor only the Klammertext delimiters and leave the rest of each scheme unchanged. |
|
| `Breakers` / `Celeste` / `Mariana` / `Monokai` / `Sixteen` `.sublime-color-scheme` | Color overrides for Sublime's five built-in schemes — one hue system, full intensity on the dark schemes, scaled down on the light ones. Additive: they recolor only the Klammertext delimiters and leave the rest of each scheme unchanged. |
|
||||||
| `Klammertext_in_Sublime_Text.md` | This file. |
|
| `Klammertext_in_Sublime_Text.md` | This file. |
|
||||||
@@ -50,6 +51,7 @@ adaptive `region.*` scopes, which were added in ST4.
|
|||||||
| **Ctrl+/** | Toggle line comment (`#`) |
|
| **Ctrl+/** | Toggle line comment (`#`) |
|
||||||
| **Ctrl+Shift+/** | Toggle block comment (`#[ ... ]#`) |
|
| **Ctrl+Shift+/** | Toggle block comment (`#[ ... ]#`) |
|
||||||
| **Ctrl+Alt+I** | Reindent the selected lines (the current line when there is just a caret) — experimental, see "Indentation" below |
|
| **Ctrl+Alt+I** | Reindent the selected lines (the current line when there is just a caret) — experimental, see "Indentation" below |
|
||||||
|
| **Ctrl+Alt+A** | Align the columns of the table containing the caret — experimental, see "Table alignment" below |
|
||||||
|
|
||||||
Ctrl+M is Sublime's own "go to matching bracket" key, reused here because the
|
Ctrl+M is Sublime's own "go to matching bracket" key, reused here because the
|
||||||
built-in cannot match Klammertext's context-dependent `@`. macOS users who
|
built-in cannot match Klammertext's context-dependent `@`. macOS users who
|
||||||
@@ -91,6 +93,34 @@ ask for it. To disable the feature, delete `Klammertext_indent.py` and the
|
|||||||
Ctrl+Alt+I entry in `Default.sublime-keymap` (or just the keymap entry, to
|
Ctrl+Alt+I entry in `Default.sublime-keymap` (or just the keymap entry, to
|
||||||
keep the command available from plugins).
|
keep the command available from plugins).
|
||||||
|
|
||||||
|
## Table alignment (experimental)
|
||||||
|
|
||||||
|
`Klammertext_align.py` ports the Emacs mode's table alignment
|
||||||
|
(`doc/emacs/klammertext-align.el`): **Ctrl+Alt+A** with the caret anywhere
|
||||||
|
inside a `@table` span pads the cells of its rows so the `|` separators line
|
||||||
|
up:
|
||||||
|
|
||||||
|
```
|
||||||
|
@table
|
||||||
|
First item | Second | A third item that's longer ||
|
||||||
|
Row 2 | Text | Not as long ||
|
||||||
|
@
|
||||||
|
```
|
||||||
|
|
||||||
|
A row is one line ending with the row delimiter `||` (the customary trailing
|
||||||
|
delimiter; the last row may omit it). Alignment is for small data items, so
|
||||||
|
a row is left untouched — and contributes nothing to the column widths —
|
||||||
|
when any of its cells is longer than `CELL_MAX` (30) characters or the row
|
||||||
|
spans lines. If the aligned rows would exceed `ROW_MAX` (100) columns,
|
||||||
|
nothing changes and the status bar says so. The padding is semantically
|
||||||
|
free: the SKS strips cell content, no whitespace is ever inserted inside a
|
||||||
|
bar run (which would turn a `||` row separator into an empty `| |` cell),
|
||||||
|
and bars inside a nested klammer in a cell (`@frac 1 | 2 @`) belong to that
|
||||||
|
klammer, not the table. Aligned rows adopt the leading whitespace of the
|
||||||
|
first aligned row — run Ctrl+Alt+I first if the rows disagree. The limits
|
||||||
|
sit at the top of `Klammertext_align.py`, mirrored from the Emacs
|
||||||
|
defcustoms.
|
||||||
|
|
||||||
## Colors
|
## Colors
|
||||||
|
|
||||||
Colors are installed automatically for all five of Sublime's built-in schemes.
|
Colors are installed automatically for all five of Sublime's built-in schemes.
|
||||||
|
|||||||
Reference in New Issue
Block a user