From 17af24f9c0ab502098eada1d7d8bba35dac2a456 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?KARASZI=20Istv=C3=A1n?= Date: Wed, 22 Jul 2026 19:29:00 +0200 Subject: [PATCH 01/11] Remove idle-timer auto-indentation --- ttl-mode.el | 22 +--------------------- 1 file changed, 1 insertion(+), 21 deletions(-) diff --git a/ttl-mode.el b/ttl-mode.el index 984ff5b..1e095f7 100644 --- a/ttl-mode.el +++ b/ttl-mode.el @@ -68,17 +68,6 @@ "If non-nil, `\;' or `\.' will self insert, reindent the line, and do a newline. (To insert while t, do: \\[quoted-insert] \;)." :type 'boolean) -(defcustom ttl-indent-on-idle-timer t - "If non-nil, will automatically indent a line after `ttl-idle-timer-timeout'." - :type 'boolean) - -(defcustom ttl-indent-idle-timer-period 2 - "If `ttl-indent-on-idle-timer' is non-nil, indent after EMACS has been idle for this many seconds." - :type 'integer) - - -(defvar ttl-indent-idle-timer nil "TTL-mode autoindent idle timer if idle auto indentation is used (`ttl-indent-on-idle-timer' is non-nil).") - ;;;###autoload (define-derived-mode ttl-mode prog-mode "N3/Turtle mode" "Major mode for Turtle RDF documents." @@ -104,11 +93,7 @@ (set (make-local-variable 'indent-line-function) 'ttl-indent-line) (set (make-local-variable 'indent-tabs-mode) nil) (set (make-local-variable 'syntax-propertize-function) 'ttl-propertize-comments) - (setq show-trailing-whitespace t) - (if (and ttl-indent-on-idle-timer (not ttl-indent-idle-timer)) - (setq ttl-indent-idle-timer (run-with-idle-timer ttl-indent-idle-timer-period t 'ttl-idle-indent)) - (when ttl-indent-idle-timer - (setq ttl-indent-idle-timer (cancel-timer ttl-indent-idle-timer))))) + (setq show-trailing-whitespace t)) ;; electric punctuation (define-key ttl-mode-map (kbd "\,") 'ttl-electric-comma) @@ -141,11 +126,6 @@ (or (ttl-calculate-indentation) 0))) (move-to-column (max (current-indentation) (current-column)))) -(defun ttl-idle-indent () - "Indent the current line, and check you're in an ttl-mode buffer." - (when (eq major-mode 'ttl-mode) - (ttl-indent-line))) - (defun ttl-calculate-indentation () "Calculate the indentation for the current line." (save-excursion From db1e0e813bbac6c72588193c7f787e18164f816e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?KARASZI=20Istv=C3=A1n?= Date: Wed, 22 Jul 2026 19:34:36 +0200 Subject: [PATCH 02/11] Fix malformed let binding in ttl-insulate --- ttl-mode.el | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ttl-mode.el b/ttl-mode.el index 1e095f7..35db1a0 100644 --- a/ttl-mode.el +++ b/ttl-mode.el @@ -192,7 +192,7 @@ (defun ttl-insulate () "Return non-nil if this location should not be electrified." (or (not ttl-electric-punctuation) - (let '(s (syntax-ppss)) + (let ((s (syntax-ppss))) (or (nth 3 s) (nth 4 s) (ttl-in-resource))))) From cc1c2fa73213419576395e805c71fab59b2acdae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?KARASZI=20Istv=C3=A1n?= Date: Wed, 22 Jul 2026 19:34:36 +0200 Subject: [PATCH 03/11] Enable lexical-binding and fix byte-compile warnings --- ttl-mode.el | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/ttl-mode.el b/ttl-mode.el index 35db1a0..271518e 100644 --- a/ttl-mode.el +++ b/ttl-mode.el @@ -1,4 +1,4 @@ -;;; ttl-mode.el --- mode for Turtle (and Notation 3) +;;; ttl-mode.el --- mode for Turtle (and Notation 3) -*- lexical-binding: t; -*- ;; ttl-mode.el is released under the terms of the two-clause BSD licence: ;; ;; Copyright 2003-2007, Hugo Haas @@ -65,7 +65,8 @@ :type 'integer) (defcustom ttl-electric-punctuation t - "If non-nil, `\;' or `\.' will self insert, reindent the line, and do a newline. (To insert while t, do: \\[quoted-insert] \;)." + "If non-nil, `\;' or `\.' self-insert, reindent, and open a new line. +To insert one literally while enabled, use \\[quoted-insert]." :type 'boolean) ;;;###autoload @@ -106,7 +107,8 @@ ;; Could be replaced with a call to syntax-propertize-rules. See ;; https://emacs.stackexchange.com/questions/36909/how-can-i-make-syntax-propertize-skip-part-of-the-buffer (defun ttl-propertize-comments (start end) - "Set the syntax class to `comment-start` for all hashes that are prepended by a space between START and END." + "Give comment syntax to each `#' preceded by a space or newline. +Operates on the region between START and END." (save-excursion (goto-char start) (save-match-data @@ -162,7 +164,7 @@ (last-character (+ base-indent ttl-indent-level))))))) (defun ttl-adjusted-paren-depth (parenpos) - "Calculate parenthesis depth from PARENPOS, ignoring parentheses on the same line." + "Count parenthesis depth from PARENPOS, ignoring parens on the same line." ;; Just enough common lisp to be dangerous. (length (delete-dups (cl-loop for pos in parenpos collect (line-number-at-pos pos))))) @@ -184,7 +186,7 @@ (string-match (rx (and string-start (* blank) line-end)) (thing-at-point 'line)))) (forward-line -1)) ;; Then, go to last non-comment-character - (if (search-forward " #" (point-at-eol) t) + (if (search-forward " #" (line-end-position) t) (backward-char 2) (end-of-line))) From 4d4ad86b8c9806b2e0195c0236598afa346a0b89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?KARASZI=20Istv=C3=A1n?= Date: Wed, 22 Jul 2026 19:56:49 +0200 Subject: [PATCH 04/11] Fix indentation of blank-node contents and object lists --- ttl-mode.el | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/ttl-mode.el b/ttl-mode.el index 271518e..7161b0e 100644 --- a/ttl-mode.el +++ b/ttl-mode.el @@ -154,13 +154,15 @@ Operates on the region between START and END." (goto-char (nth 1 syntax-info)) (current-indentation))) ((and (not (bobp)) - (or (ttl-first-line-of ?\[ last-character) - (ttl-first-line-of ?\( last-character) + (or (ttl-first-line-of ?\( last-character) (ttl-first-line-of ?\{ last-character))) (+ last-indent ttl-indent-level)) ((eq ?. last-character) base-indent) (after-prefix 0) - ((ttl-in-blank-node) base-indent) + ;; A blank node's predicates and every object after the first in a + ;; comma list sit two levels in, so they clear the predicate column. + ((or (eq ?\, last-character) (ttl-in-blank-node)) + (+ base-indent (* 2 ttl-indent-level))) (last-character (+ base-indent ttl-indent-level))))))) (defun ttl-adjusted-paren-depth (parenpos) From c566d3f797e3c589007bf14572f0e4f31c26aa3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?KARASZI=20Istv=C3=A1n?= Date: Wed, 22 Jul 2026 20:11:51 +0200 Subject: [PATCH 05/11] Add package metadata headers --- ttl-mode.el | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/ttl-mode.el b/ttl-mode.el index 7161b0e..c2b7611 100644 --- a/ttl-mode.el +++ b/ttl-mode.el @@ -1,4 +1,12 @@ -;;; ttl-mode.el --- mode for Turtle (and Notation 3) -*- lexical-binding: t; -*- +;;; ttl-mode.el --- Major mode for Turtle (and Notation 3) -*- lexical-binding: t; -*- + +;; Author: Hugo Haas, Norman Gray, Daniel Gerber, Peter Vasil +;; Maintainer: Jan Seeger +;; URL: https://github.com/jeeger/ttl-mode +;; Version: 0.2.0 +;; Package-Requires: ((emacs "26.1")) +;; Keywords: languages, data + ;; ttl-mode.el is released under the terms of the two-clause BSD licence: ;; ;; Copyright 2003-2007, Hugo Haas From 306c496ea2e12be7ede6914519cd378126ecd758 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?KARASZI=20Istv=C3=A1n?= Date: Wed, 22 Jul 2026 20:11:51 +0200 Subject: [PATCH 06/11] Add Eask build tooling, Buttercup tests, and CI --- .github/workflows/ci.yml | 25 +++++++++ .gitignore | 6 +++ Easkfile | 17 +++++++ Makefile | 19 +++++++ test/ttl-mode-test.el | 107 +++++++++++++++++++++++++++++++++++++++ 5 files changed, 174 insertions(+) create mode 100644 .github/workflows/ci.yml create mode 100644 .gitignore create mode 100644 Easkfile create mode 100644 Makefile create mode 100644 test/ttl-mode-test.el diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..15f5047 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,25 @@ +name: CI +on: + pull_request: + push: + branches: [master] + +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + emacs_version: ['26.1', '28.2', '29.4', '30.2'] + steps: + - uses: actions/checkout@v4 + - uses: jcs090218/setup-emacs@master + with: + version: ${{ matrix.emacs_version }} + - uses: emacs-eask/setup-eask@master + with: + version: 'snapshot' + - run: eask package + - run: eask install-deps + - run: eask compile + - run: eask lint package + - run: eask test buttercup diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..271496d --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +*.elc +.deps +.compile +.eask/ +.cask/ +dist/ diff --git a/Easkfile b/Easkfile new file mode 100644 index 0000000..05cfbef --- /dev/null +++ b/Easkfile @@ -0,0 +1,17 @@ +(package "ttl-mode" + "0.2.0" + "Major mode for Turtle (and Notation 3)") + +(website-url "https://github.com/raszi/ttl-mode") +(keywords "languages" "data") + +(package-file "ttl-mode.el") + +(source "gnu") +(source "melpa") + +(depends-on "emacs" "26.1") + +(development + (depends-on "package-lint") + (depends-on "buttercup")) diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..31bc09e --- /dev/null +++ b/Makefile @@ -0,0 +1,19 @@ +.PHONY: lint test clean + +.deps: Easkfile + eask install-deps + @touch .deps + +.compile: .deps ttl-mode.el + eask compile + @touch .compile + +lint: .deps + eask lint package + +test: .compile + eask test buttercup + +clean: + eask clean all + @rm -f .deps .compile diff --git a/test/ttl-mode-test.el b/test/ttl-mode-test.el new file mode 100644 index 0000000..419ffd2 --- /dev/null +++ b/test/ttl-mode-test.el @@ -0,0 +1,107 @@ +;;; ttl-mode-test.el --- Tests for ttl-mode -*- lexical-binding: t; -*- + +;;; Commentary: + +;; Buttercup tests for ttl-mode. + +;;; Code: + +(require 'buttercup) +(require 'ttl-mode) + +(defun ttl-test-reindent (text) + "Return TEXT after re-indenting every line in `ttl-mode'." + (with-temp-buffer + (insert text) + (ttl-mode) + (indent-region (point-min) (point-max)) + (buffer-string))) + +(describe "ttl-mode indentation is idempotent" + (dolist (case '((:desc "prefixes and a simple statement" + :text "@prefix ex: . +@prefix sh: . + +ex:s a ex:Thing ; + ex:p ex:o ; + ex:q 1 . +") + (:desc "blank nodes written inline with the predicate" + :text ":Shape a sh:NodeShape ; + sh:property [ sh:datatype xsd:string ; + sh:maxCount 1 ; + sh:path rdfs:label ], + [ sh:datatype xsd:double ; + sh:path :rate ] ; + sh:targetClass :X . +") + (:desc "a blank node opened at end of line" + :text "ex:s ex:p [ + ex:a 1 ; + ex:b 2 ] . +") + (:desc "a TriG named graph" + :text "ex:g { + ex:s ex:p ex:o ; + ex:q ex:r . +} +"))) + (it (plist-get case :desc) + (let ((text (plist-get case :text))) + (expect (ttl-test-reindent text) :to-equal text))))) + +(describe "ttl-mode indentation converges" + (it "restores nesting a flattened blank-node list lost" + (expect + (ttl-test-reindent ":Shape a sh:NodeShape ; + sh:property [ sh:datatype xsd:string ; + sh:maxCount 1 ; + sh:path rdfs:label ], + [ sh:datatype xsd:double ; + sh:path :rate ] ; + sh:targetClass :X . +") + :to-equal ":Shape a sh:NodeShape ; + sh:property [ sh:datatype xsd:string ; + sh:maxCount 1 ; + sh:path rdfs:label ], + [ sh:datatype xsd:double ; + sh:path :rate ] ; + sh:targetClass :X . +"))) + +(describe "ttl-mode comment syntax" + (it "treats a hash after whitespace as a comment" + (with-temp-buffer + (insert "ex:s ex:p ex:o . # a comment\n") + (ttl-mode) + (goto-char (point-min)) + (search-forward "a comment") + (expect (ttl-in-comment) :to-be-truthy))) + + (it "leaves a hash inside a resource alone" + (with-temp-buffer + (insert "ex:s ex:p .\n") + (ttl-mode) + (goto-char (point-min)) + (search-forward "#frag") + (expect (ttl-in-comment) :to-be nil)))) + +(describe "ttl-in-blank-node" + (it "is truthy inside square brackets" + (with-temp-buffer + (insert "ex:s ex:p [ ex:a 1 ] .\n") + (ttl-mode) + (goto-char (point-min)) + (search-forward "ex:a") + (expect (ttl-in-blank-node) :to-be-truthy))) + + (it "is nil in an ordinary statement" + (with-temp-buffer + (insert "ex:s ex:p ex:o .\n") + (ttl-mode) + (goto-char (point-min)) + (search-forward "ex:o") + (expect (ttl-in-blank-node) :to-be nil)))) + +;;; ttl-mode-test.el ends here From bb5245aab053bb2185a61a92552fbaa0007d27d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?KARASZI=20Istv=C3=A1n?= Date: Wed, 22 Jul 2026 20:47:48 +0200 Subject: [PATCH 07/11] Add regression tests for upstream closed-issue behaviors --- test/ttl-mode-test.el | 87 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 86 insertions(+), 1 deletion(-) diff --git a/test/ttl-mode-test.el b/test/ttl-mode-test.el index 419ffd2..b2a6afb 100644 --- a/test/ttl-mode-test.el +++ b/test/ttl-mode-test.el @@ -17,6 +17,16 @@ (indent-region (point-min) (point-max)) (buffer-string))) +(defun ttl-test-face-at (text search) + "Fontify TEXT in `ttl-mode' and return the face where SEARCH begins." + (with-temp-buffer + (insert text) + (ttl-mode) + (font-lock-ensure) + (goto-char (point-min)) + (search-forward search) + (get-text-property (match-beginning 0) 'face))) + (describe "ttl-mode indentation is idempotent" (dolist (case '((:desc "prefixes and a simple statement" :text "@prefix ex: . @@ -45,6 +55,25 @@ ex:s a ex:Thing ; ex:s ex:p ex:o ; ex:q ex:r . } +") + (:desc "a comment line between statements" + :text "ex:s ex:p ex:o ; + # a comment + ex:q 1 . +") + (:desc "a hash inside a resource is not a comment" + :text "ex:s ex:p ; + ex:q 1 . +") + (:desc "a closing bracket on its own line aligns under its opener" + :text "ex:s ex:p [ + ex:a 1 ; + ex:b 2 +] . +") + (:desc "several brackets opened on one line count as one step" + :text "ex:s ex:p [ ex:q [ ex:a 1 ; + ex:b 2 ] ] . "))) (it (plist-get case :desc) (let ((text (plist-get case :text))) @@ -68,7 +97,15 @@ ex:s a ex:Thing ; [ sh:datatype xsd:double ; sh:path :rate ] ; sh:targetClass :X . -"))) +")) + + (it "moves a mis-indented first line to column 0" + (expect (ttl-test-reindent " ex:s ex:p ex:o .\n") + :to-equal "ex:s ex:p ex:o .\n")) + + (it "snaps a mis-indented @prefix to column 0" + (expect (ttl-test-reindent " @prefix ex: .\n") + :to-equal "@prefix ex: .\n"))) (describe "ttl-mode comment syntax" (it "treats a hash after whitespace as a comment" @@ -104,4 +141,52 @@ ex:s a ex:Thing ; (search-forward "ex:o") (expect (ttl-in-blank-node) :to-be nil)))) +(describe "ttl-in-string" + (it "treats single-quoted text as a string" + (with-temp-buffer + (insert "ex:s ex:p 'hello world' .\n") + (ttl-mode) + (goto-char (point-min)) + (search-forward "hello") + (expect (ttl-in-string) :to-be-truthy))) + + (it "is nil outside strings" + (with-temp-buffer + (insert "ex:s ex:p ex:o .\n") + (ttl-mode) + (goto-char (point-min)) + (search-forward "ex:o") + (expect (ttl-in-string) :to-be nil)))) + +(describe "ttl-electric-dot" + (it "refuses to insert a period inside a blank node" + (with-temp-buffer + (insert "ex:s ex:p [ ex:a 1 ") + (ttl-mode) + (goto-char (point-max)) + (let ((before (buffer-string))) + (ttl-electric-dot) + (expect (buffer-string) :to-equal before)))) + + ;; jeeger/ttl-mode issue #3: a dot belongs in a string even in a blank node. + (it "allows a period inside a string within a blank node" + (with-temp-buffer + (insert "ex:s ex:p [ ex:a 'text") + (ttl-mode) + (goto-char (point-max)) + (let ((before (buffer-string))) + (ttl-electric-dot) + (expect (buffer-string) :to-equal (concat before ".")))))) + +(describe "ttl-mode highlighting" + ;; jeeger/ttl-mode issue #5: a dash is a valid prefix character. + (it "highlights a prefix containing a dash" + (expect (ttl-test-face-at "my-prefix:thing a ex:Y ." "my-prefix") + :to-be 'font-lock-type-face)) + + ;; jeeger/ttl-mode issue #4: quoted literals are strings. + (it "highlights a single-quoted literal as a string" + (expect (ttl-test-face-at "ex:s ex:p 'a literal' ." "literal") + :to-be 'font-lock-string-face))) + ;;; ttl-mode-test.el ends here From 8c38af1b712b1da63d8ea37ca1c9580caa3155c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?KARASZI=20Istv=C3=A1n?= Date: Thu, 23 Jul 2026 09:04:55 +0200 Subject: [PATCH 08/11] Fix formatting --- ttl-mode.el | 77 ++++++++++++++++++++++++++--------------------------- 1 file changed, 38 insertions(+), 39 deletions(-) diff --git a/ttl-mode.el b/ttl-mode.el index c2b7611..d5ff569 100644 --- a/ttl-mode.el +++ b/ttl-mode.el @@ -66,7 +66,7 @@ (require 'cl-lib) -(defgroup ttl nil "Customization for ttl-mode" :group 'text) +(defgroup ttl nil "Customization for ttl-mode." :group 'text) (defcustom ttl-indent-level 4 "Number of spaces for each indentation step in `ttl-mode'." @@ -89,12 +89,12 @@ To insert one literally while enabled, use \\[quoted-insert]." (setq font-lock-defaults `((,(regexp-opt '("@prefix" "@base" "@keywords" "PREFIX" "BASE" "@forAll" "@forSome" "true" "false" "a") 'symbols) ;keywords ("\\^\\^[^,;.]+" 0 font-lock-preprocessor-face t) ;literal types - ("\\?[[:word:]_]+" 0 font-lock-variable-name-face) ;Existentially quantified variables + ("\\?[[:word:]_]+" 0 font-lock-variable-name-face) ;Existentially quantified variables ("@[[:word:]_]+" . font-lock-preprocessor-face) ;languages - ; Does not work with iris containing multiple colons (one:two:three is apparently allowed.) + ;; Does not work with iris containing multiple colons (one:two:three is apparently allowed.) ("\\(:?[-[:alnum:]]+\\|_\\)?:" . font-lock-type-face) ;prefix (":\\([[:word:]_-]+\\)\\>" 1 font-lock-constant-face nil) ;suffix - ;; TODO: This incorrectly highlights resources in strings. + ;; TODO: This incorrectly highlights resources in strings. ("<.*?>" 0 font-lock-function-name-face t) ;resources ("[,;.]" 0 font-lock-keyword-face)))) @@ -121,12 +121,12 @@ Operates on the region between START and END." (goto-char start) (save-match-data (while (search-forward "#" end t) - (let ((char-before (buffer-substring-no-properties - (max (point-min) (- (point) 2)) - (min (point-max) (- (point) 1))))) - (when (or (equal char-before " ") - (equal char-before "\n")) - (put-text-property (match-beginning 0) (match-end 0) 'syntax-table '(11)))))))) + (let ((char-before (buffer-substring-no-properties + (max (point-min) (- (point) 2)) + (min (point-max) (- (point) 1))))) + (when (or (equal char-before " ") + (equal char-before "\n")) + (put-text-property (match-beginning 0) (match-end 0) 'syntax-table '(11)))))))) (defun ttl-indent-line () "Indent current line." @@ -141,37 +141,37 @@ Operates on the region between START and END." (save-excursion (backward-to-indentation 0) (cl-destructuring-bind - (last-indent last-character after-prefix) - (save-excursion (ttl-skip-uninteresting-lines) (list (current-indentation) (char-before) (ttl-in-prefix-line))) + (last-indent last-character after-prefix) + (save-excursion (ttl-skip-uninteresting-lines) (list (current-indentation) (char-before) (ttl-in-prefix-line))) (let* ((syntax-info (syntax-ppss)) - (base-indent (* ttl-indent-level (ttl-adjusted-paren-depth (nth 9 syntax-info))))) - (cond - ;; in multiline string - ((nth 3 syntax-info) (current-indentation)) - ;; First line in buffer. - ((= (line-number-at-pos (point) t) 1) 0) - ;; beginning of stanza - ((and (or (looking-at "@") ; @prefix, @base, @keywords. - (looking-at "PREFIX") - (looking-at "BASE")) - (not (looking-at "\\(@forSome\\)\\|\\(@forAll\\)"))) ; @forAll and @forSome should be indented normally. - 0) - ;; ((looking-at "#") base-indent) - ((looking-at "[])}]") ; Indent to level of matching parenthesis. - (save-excursion - (goto-char (nth 1 syntax-info)) - (current-indentation))) - ((and (not (bobp)) + (base-indent (* ttl-indent-level (ttl-adjusted-paren-depth (nth 9 syntax-info))))) + (cond + ;; in multiline string + ((nth 3 syntax-info) (current-indentation)) + ;; First line in buffer. + ((= (line-number-at-pos (point) t) 1) 0) + ;; beginning of stanza + ((and (or (looking-at "@") ; @prefix, @base, @keywords. + (looking-at "PREFIX") + (looking-at "BASE")) + (not (looking-at "\\(@forSome\\)\\|\\(@forAll\\)"))) ; @forAll and @forSome should be indented normally. + 0) + ;; ((looking-at "#") base-indent) + ((looking-at "[])}]") ; Indent to level of matching parenthesis. + (save-excursion + (goto-char (nth 1 syntax-info)) + (current-indentation))) + ((and (not (bobp)) (or (ttl-first-line-of ?\( last-character) (ttl-first-line-of ?\{ last-character))) - (+ last-indent ttl-indent-level)) - ((eq ?. last-character) base-indent) + (+ last-indent ttl-indent-level)) + ((eq ?. last-character) base-indent) (after-prefix 0) ;; A blank node's predicates and every object after the first in a ;; comma list sit two levels in, so they clear the predicate column. ((or (eq ?\, last-character) (ttl-in-blank-node)) (+ base-indent (* 2 ttl-indent-level))) - (last-character (+ base-indent ttl-indent-level))))))) + (last-character (+ base-indent ttl-indent-level))))))) (defun ttl-adjusted-paren-depth (parenpos) "Count parenthesis depth from PARENPOS, ignoring parens on the same line." @@ -199,7 +199,6 @@ Operates on the region between START and END." (if (search-forward " #" (line-end-position) t) (backward-char 2) (end-of-line))) - (defun ttl-insulate () "Return non-nil if this location should not be electrified." @@ -212,8 +211,8 @@ Operates on the region between START and END." (defun ttl-last-bracket-is (brack) "Is the last bracket equal to BRACK?" (let ((list-start (nth 1 (syntax-ppss)))) - (and list-start - (equal (char-after list-start) brack)))) + (and list-start + (equal (char-after list-start) brack)))) (defun ttl-first-line-of (brack lastchar) "Whether we are in the first line of a node introduced by BRACK. @@ -225,7 +224,7 @@ LASTCHAR is the last character of the preceding line." (defun ttl-in-blank-node () "Is point within a blank node, marked by [...]?" (ttl-last-bracket-is ?\[)) - + (defun ttl-in-graph () "Is point within a graph?" (ttl-last-bracket-is ?\{)) @@ -269,8 +268,8 @@ LASTCHAR is the last character of the preceding line." "Insert spaced dot, insert newline, indent." (interactive) (if (and (ttl-in-blank-node) - (not (ttl-in-comment)) - (not (ttl-in-string))) + (not (ttl-in-comment)) + (not (ttl-in-string))) (message "No period in blank nodes.") (if (ttl-insulate) (insert ".") (if (not (looking-back " " 1)) (insert " ")) From 7f3db044d4fd834442de8e1d40429e3d823f74e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?KARASZI=20Istv=C3=A1n?= Date: Thu, 23 Jul 2026 09:21:24 +0200 Subject: [PATCH 09/11] Recognize a comment at the very start of a file --- test/ttl-mode-test.el | 10 +++++++++- ttl-mode.el | 14 ++++++-------- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/test/ttl-mode-test.el b/test/ttl-mode-test.el index b2a6afb..a4cf6ea 100644 --- a/test/ttl-mode-test.el +++ b/test/ttl-mode-test.el @@ -122,7 +122,15 @@ ex:s a ex:Thing ; (ttl-mode) (goto-char (point-min)) (search-forward "#frag") - (expect (ttl-in-comment) :to-be nil)))) + (expect (ttl-in-comment) :to-be nil))) + + (it "treats a hash at the very start of the buffer as a comment" + (with-temp-buffer + (insert "# leading comment\nex:s ex:p ex:o .\n") + (ttl-mode) + (goto-char (point-min)) + (search-forward "leading") + (expect (ttl-in-comment) :to-be-truthy)))) (describe "ttl-in-blank-node" (it "is truthy inside square brackets" diff --git a/ttl-mode.el b/ttl-mode.el index d5ff569..c0f2a8b 100644 --- a/ttl-mode.el +++ b/ttl-mode.el @@ -115,18 +115,16 @@ To insert one literally while enabled, use \\[quoted-insert]." ;; Could be replaced with a call to syntax-propertize-rules. See ;; https://emacs.stackexchange.com/questions/36909/how-can-i-make-syntax-propertize-skip-part-of-the-buffer (defun ttl-propertize-comments (start end) - "Give comment syntax to each `#' preceded by a space or newline. -Operates on the region between START and END." + "Give comment syntax to each `#' that begins a comment. +A `#' begins a comment when preceded by a space, a newline, or the +buffer start. Operates on the region between START and END." (save-excursion (goto-char start) (save-match-data (while (search-forward "#" end t) - (let ((char-before (buffer-substring-no-properties - (max (point-min) (- (point) 2)) - (min (point-max) (- (point) 1))))) - (when (or (equal char-before " ") - (equal char-before "\n")) - (put-text-property (match-beginning 0) (match-end 0) 'syntax-table '(11)))))))) + (when (memq (char-before (match-beginning 0)) '(?\s ?\n nil)) + (put-text-property (match-beginning 0) (match-end 0) + 'syntax-table '(11))))))) (defun ttl-indent-line () "Indent current line." From 457aea8bddec1971d6ad9576a5eb60bfdbfebf69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?KARASZI=20Istv=C3=A1n?= Date: Thu, 23 Jul 2026 09:31:27 +0200 Subject: [PATCH 10/11] Fix idempotency specs testing only the last fixture; add ttl-lines helper --- test/ttl-mode-test.el | 125 ++++++++++++++++++++++-------------------- 1 file changed, 67 insertions(+), 58 deletions(-) diff --git a/test/ttl-mode-test.el b/test/ttl-mode-test.el index a4cf6ea..c052ff3 100644 --- a/test/ttl-mode-test.el +++ b/test/ttl-mode-test.el @@ -9,6 +9,12 @@ (require 'buttercup) (require 'ttl-mode) +(defun ttl-lines (&rest lines) + "Join LINES with newlines into one buffer-ready string. +The result ends with a newline, like a text file, so fixtures can be +written one line per string instead of as a flush-left literal." + (concat (mapconcat #'identity lines "\n") "\n")) + (defun ttl-test-reindent (text) "Return TEXT after re-indenting every line in `ttl-mode'." (with-temp-buffer @@ -28,76 +34,79 @@ (get-text-property (match-beginning 0) 'face))) (describe "ttl-mode indentation is idempotent" - (dolist (case '((:desc "prefixes and a simple statement" - :text "@prefix ex: . -@prefix sh: . - -ex:s a ex:Thing ; - ex:p ex:o ; - ex:q 1 . -") + (dolist (case `((:desc "prefixes and a simple statement" + :text ,(ttl-lines + "@prefix ex: ." + "@prefix sh: ." + "" + "ex:s a ex:Thing ;" + " ex:p ex:o ;" + " ex:q 1 .")) (:desc "blank nodes written inline with the predicate" - :text ":Shape a sh:NodeShape ; - sh:property [ sh:datatype xsd:string ; - sh:maxCount 1 ; - sh:path rdfs:label ], - [ sh:datatype xsd:double ; - sh:path :rate ] ; - sh:targetClass :X . -") + :text ,(ttl-lines + ":Shape a sh:NodeShape ;" + " sh:property [ sh:datatype xsd:string ;" + " sh:maxCount 1 ;" + " sh:path rdfs:label ]," + " [ sh:datatype xsd:double ;" + " sh:path :rate ] ;" + " sh:targetClass :X .")) (:desc "a blank node opened at end of line" - :text "ex:s ex:p [ - ex:a 1 ; - ex:b 2 ] . -") + :text ,(ttl-lines + "ex:s ex:p [" + " ex:a 1 ;" + " ex:b 2 ] .")) (:desc "a TriG named graph" - :text "ex:g { - ex:s ex:p ex:o ; - ex:q ex:r . -} -") + :text ,(ttl-lines + "ex:g {" + " ex:s ex:p ex:o ;" + " ex:q ex:r ." + "}")) (:desc "a comment line between statements" - :text "ex:s ex:p ex:o ; - # a comment - ex:q 1 . -") + :text ,(ttl-lines + "ex:s ex:p ex:o ;" + " # a comment" + " ex:q 1 .")) (:desc "a hash inside a resource is not a comment" - :text "ex:s ex:p ; - ex:q 1 . -") + :text ,(ttl-lines + "ex:s ex:p ;" + " ex:q 1 .")) (:desc "a closing bracket on its own line aligns under its opener" - :text "ex:s ex:p [ - ex:a 1 ; - ex:b 2 -] . -") + :text ,(ttl-lines + "ex:s ex:p [" + " ex:a 1 ;" + " ex:b 2" + "] .")) (:desc "several brackets opened on one line count as one step" - :text "ex:s ex:p [ ex:q [ ex:a 1 ; - ex:b 2 ] ] . -"))) - (it (plist-get case :desc) - (let ((text (plist-get case :text))) + :text ,(ttl-lines + "ex:s ex:p [ ex:q [ ex:a 1 ;" + " ex:b 2 ] ] .")))) + (let ((desc (plist-get case :desc)) + (text (plist-get case :text))) + (it desc (expect (ttl-test-reindent text) :to-equal text))))) (describe "ttl-mode indentation converges" (it "restores nesting a flattened blank-node list lost" (expect - (ttl-test-reindent ":Shape a sh:NodeShape ; - sh:property [ sh:datatype xsd:string ; - sh:maxCount 1 ; - sh:path rdfs:label ], - [ sh:datatype xsd:double ; - sh:path :rate ] ; - sh:targetClass :X . -") - :to-equal ":Shape a sh:NodeShape ; - sh:property [ sh:datatype xsd:string ; - sh:maxCount 1 ; - sh:path rdfs:label ], - [ sh:datatype xsd:double ; - sh:path :rate ] ; - sh:targetClass :X . -")) + (ttl-test-reindent + (ttl-lines + ":Shape a sh:NodeShape ;" + " sh:property [ sh:datatype xsd:string ;" + " sh:maxCount 1 ;" + " sh:path rdfs:label ]," + " [ sh:datatype xsd:double ;" + " sh:path :rate ] ;" + " sh:targetClass :X .")) + :to-equal + (ttl-lines + ":Shape a sh:NodeShape ;" + " sh:property [ sh:datatype xsd:string ;" + " sh:maxCount 1 ;" + " sh:path rdfs:label ]," + " [ sh:datatype xsd:double ;" + " sh:path :rate ] ;" + " sh:targetClass :X ."))) (it "moves a mis-indented first line to column 0" (expect (ttl-test-reindent " ex:s ex:p ex:o .\n") From 518ecfaa3c5a1ca3659b6e81992ac12a7c119229 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?KARASZI=20Istv=C3=A1n?= Date: Thu, 23 Jul 2026 09:40:01 +0200 Subject: [PATCH 11/11] Fix duplicate key binding --- ttl-mode.el | 1 - 1 file changed, 1 deletion(-) diff --git a/ttl-mode.el b/ttl-mode.el index c0f2a8b..d08a4da 100644 --- a/ttl-mode.el +++ b/ttl-mode.el @@ -107,7 +107,6 @@ To insert one literally while enabled, use \\[quoted-insert]." ;; electric punctuation (define-key ttl-mode-map (kbd "\,") 'ttl-electric-comma) (define-key ttl-mode-map (kbd "\;") 'ttl-electric-semicolon) -(define-key ttl-mode-map (kbd "\,") 'ttl-electric-comma) (define-key ttl-mode-map (kbd "\.") 'ttl-electric-dot) (define-key ttl-mode-map [backspace] 'ttl-hungry-delete-backwards)