Recently Written · git

tmpl

Insanely simple templates

git clone https://github.com/equwal/tmpl

Log | Files | Refs


commit 806f4eb1ecf13204140ca0a35c5ad62e10d46681
map588 <map588@pitt.edu>
2026-09-27 02:16:25 -0400

Add summary comments and named placeholders

Every template now starts with a one-line summary comment. The
converter reads it as the snippet description. <+name+> is a named
placeholder; the converter emits it as ${n:name}. The Vim mapping
pattern matches both forms. git-config-branch and cl/class use
named placeholders as examples.

 README.md                              | 23 +++++++++++++++++------
 c/main.c                               |  1 +
 cl/class                               |  9 +++++----
 cl/defmethod-specialized               |  1 +
 cl/defpackage                          |  1 +
 cl/defstruct                           |  1 +
 cl/defsystem                           |  1 +
 cl/do                                  |  2 +-
 cl/dolist                              |  1 +
 cl/dotimes                             |  1 +
 cl/handler-case                        |  1 +
 cl/restart-case                        |  1 +
 cl/slot                                |  1 +
 cl/tagbody-go                          |  1 +
 cl/with-open-file                      |  1 +
 cl/with-simple-restart                 |  1 +
 convert_to_luasnip.py                  | 28 ++++++++++++++++++++++------
 git-config-branch                      |  5 +++--
 html/template.html                     |  1 +
 make/conditionals/conditional-variable |  1 +
 make/conditionals/ifeq-endif           |  1 +
 make/functions/foreach-simple          |  2 +-
 make/golf/if-pkgconfig-exists          |  2 +-
 make/mapping/map-fn                    |  1 +
 make/pedantic-phony                    |  1 +
 make/shell                             |  1 +
 make/templates/config.mk               |  1 +
 make/templates/dist                    |  1 +
 sed/append-after-match                 |  1 +
 sed/d                                  |  2 +-
 sh/for-array                           |  2 +-
 sh/str-fn                              |  1 +
 test_convert_to_luasnip.py             | 34 ++++++++++++++++++++++++++++++++++
 33 files changed, 109 insertions(+), 23 deletions(-)
diff --git a/README.md b/README.md
index 4cf5530..47f4fa4 100644
--- a/README.md
+++ b/README.md
@@ -38,23 +38,33 @@ Two kinds of file live here:
   most of `sh/`). They are notes to read or `:read` in whole. The snippet
   converter skips them.
 
+# Conventions
+
+- The first line of a template is a one-line summary in the language's
+  comment syntax (`#`, `;;`, `//` or `<!-- -->`). Tools read it as the
+  description. A `#!` line may come before it.
+- `<++>` marks a spot to fill. `<+name+>` does the same and names the spot,
+  for example `<+remote+>`. The snippet converter turns the name into the
+  placeholder text. Use the plain form unless a name helps.
+
 # Using with Vim
 
 ```vim
 :read /path/to/tmpl/thing
 ```
 
-Add a shortcut on your leader key that jumps to the next `<++>`, deletes it,
-and leaves you in insert mode.
+Add a shortcut on your leader key that jumps to the next placeholder, deletes
+it, and leaves you in insert mode. The pattern matches both `<++>` and
+`<+name+>`.
 
 Vimscript:
 ```vim
-map <leader><Space> /<++><CR>dt>a<BS>
+map <leader><Space> /<+.\{-}+><CR>dt>a<BS>
 ```
 
 Lua:
 ```lua
-map("n", "<leader><Space>", "/<++><CR>dt>a<BS>", { desc = "Jump to next template placeholder" })
+map("n", "<leader><Space>", "/<+.\\{-}+><CR>dt>a<BS>", { desc = "Jump to next template placeholder" })
 ```
 
 # Using with Neovim and LuaSnip
@@ -77,8 +87,9 @@ require("luasnip.loaders.from_vscode").lazy_load({
 ```
 
 The snippet prefix is the path with dashes: `c/main.c` is `c-main`,
-`sh/trap/cleanup-temp` is `sh-trap-cleanup-temp`. Type the prefix, press Tab,
-and Tab again to move between the placeholders.
+`sh/trap/cleanup-temp` is `sh-trap-cleanup-temp`. The description is the
+template's summary comment. Type the prefix, press Tab, and Tab again to move
+between the placeholders.
 
 Tests:
 ```sh
diff --git a/c/main.c b/c/main.c
index b4b8e1a..59415c9 100644
--- a/c/main.c
+++ b/c/main.c
@@ -1,3 +1,4 @@
+// main with argc and argv
 #include <stdio.h>
 
 int main(int argc, char **argv) {
diff --git a/cl/class b/cl/class
index 71c90f9..35dcca6 100644
--- a/cl/class
+++ b/cl/class
@@ -1,4 +1,5 @@
-(defclass <++> (<++>) ; name (inherits)
-  ((<++> :initarg :<++>
-         :accessor <++>))
-  (:documentation "<++>"))
+;; defclass with one slot
+(defclass <+name+> (<+superclass+>)
+  ((<+slot+> :initarg :<+slot+>
+         :accessor <+slot+>))
+  (:documentation "<+doc+>"))
diff --git a/cl/defmethod-specialized b/cl/defmethod-specialized
index d5b2e8d..51e7863 100644
--- a/cl/defmethod-specialized
+++ b/cl/defmethod-specialized
@@ -1,3 +1,4 @@
+;; defmethod specialized on two arguments
 (defmethod <++> ((<++> <++>) (<++> <++>))
   "<++>"
   <++>)
\ No newline at end of file
diff --git a/cl/defpackage b/cl/defpackage
index 7ff1983..6d3510d 100644
--- a/cl/defpackage
+++ b/cl/defpackage
@@ -1,3 +1,4 @@
+;; defpackage with every common option
 (defpackage #:<++>
   (:nicknames #:<++>)
   (:shadow #:<++>)
diff --git a/cl/defstruct b/cl/defstruct
index 20342da..105d462 100644
--- a/cl/defstruct
+++ b/cl/defstruct
@@ -1,3 +1,4 @@
+;; defstruct with options and one slot
 (defstruct (<++> :<++> <++>) ; name and options*
   "<++>"
   (<++> <++> :<++> <++>) ; name initform options*
diff --git a/cl/defsystem b/cl/defsystem
index ae20e81..48d13b8 100644
--- a/cl/defsystem
+++ b/cl/defsystem
@@ -1,3 +1,4 @@
+;; ASDF defsystem with its own package
 (defpackage :<++>-system (:use :cl :asdf))
 (in-package :<++>-system)
 (defsystem <++>
diff --git a/cl/do b/cl/do
index bb033b2..3ca4103 100644
--- a/cl/do
+++ b/cl/do
@@ -1,4 +1,4 @@
-;; * or no *?
+;; do or do* loop with two variables
 (do<++> ((<++> <++> <++>) ; var initform stepform
      (<++> <++> <++>)) ; more vars
     (<++> <++>) ; condition, return
diff --git a/cl/dolist b/cl/dolist
index bc66d2a..3da77ba 100644
--- a/cl/dolist
+++ b/cl/dolist
@@ -1,2 +1,3 @@
+;; dolist over a list
 (dolist (<++> <++>) ; var listform
         <++>) ; body
diff --git a/cl/dotimes b/cl/dotimes
index 6b37e14..9ac7e75 100644
--- a/cl/dotimes
+++ b/cl/dotimes
@@ -1,2 +1,3 @@
+;; dotimes counted loop
 (dotimes (<++> <++> <++>) ; var count optional-return
          <++>) ; optional-body
diff --git a/cl/handler-case b/cl/handler-case
index 5564929..d80660d 100644
--- a/cl/handler-case
+++ b/cl/handler-case
@@ -1,3 +1,4 @@
+;; handler-case with a :no-error clause
 (handler-case (progn
                 <++>)
   (<++> (condition)
diff --git a/cl/restart-case b/cl/restart-case
index fb2649a..be9229d 100644
--- a/cl/restart-case
+++ b/cl/restart-case
@@ -1,3 +1,4 @@
+;; restart-case with a report and an interactive function
 (restart-case <++>
   (<++> (<++>)
     :report "<++>"
diff --git a/cl/slot b/cl/slot
index fbe218b..367ed57 100644
--- a/cl/slot
+++ b/cl/slot
@@ -1,3 +1,4 @@
+;; defclass slot with every option
 (<++> :initarg :<++>
  :initform <++>
  ;; :accessor <++>
diff --git a/cl/tagbody-go b/cl/tagbody-go
index 9355692..7bcea5a 100644
--- a/cl/tagbody-go
+++ b/cl/tagbody-go
@@ -1,3 +1,4 @@
+;; tagbody with go labels
 (tagbody
    place-1
    (go place-1)
diff --git a/cl/with-open-file b/cl/with-open-file
index 6eac378..f0f0d01 100644
--- a/cl/with-open-file
+++ b/cl/with-open-file
@@ -1,3 +1,4 @@
+;; with-open-file with direction and if-exists options
 (with-open-file (<++> "<++>"
                  :direction <++> ; :input :output :io
                  :if-exists <++> ; :supersede :append :overwrite
diff --git a/cl/with-simple-restart b/cl/with-simple-restart
index 7e84cc3..6014756 100644
--- a/cl/with-simple-restart
+++ b/cl/with-simple-restart
@@ -1,2 +1,3 @@
+;; with-simple-restart around a body
 (with-simple-restart (<++> "<++>")
   <++>)
\ No newline at end of file
diff --git a/convert_to_luasnip.py b/convert_to_luasnip.py
index f7e0e56..15f49bf 100644
--- a/convert_to_luasnip.py
+++ b/convert_to_luasnip.py
@@ -30,7 +30,11 @@ ROOT_FILE_MAP = {
     "uad.json": "json",
 }
 
-PLACEHOLDER_RE = re.compile(r"<\+\+>")
+# <++> is an unnamed placeholder. <+name+> is a named placeholder.
+PLACEHOLDER_RE = re.compile(r"<\+([^+>]*)\+>")
+
+# A summary comment on the first line of a template, in any supported comment syntax.
+HEAD_COMMENT_RE = re.compile(r"^\s*(?:#+|;+|//|<!--)\s*(.*?)\s*(?:-->)?\s*$")
 
 
 def escape_snippet_text(text: str) -> str:
@@ -39,12 +43,18 @@ def escape_snippet_text(text: str) -> str:
 
 
 def convert_placeholders(content: str) -> Tuple[str, int]:
-    """Replace each <++> with a numbered tab stop. Escape all other text."""
+    """Replace each placeholder with a numbered tab stop. Escape all other text.
+
+    <++> becomes ${n}. <+name+> becomes ${n:name}.
+    """
     parts = PLACEHOLDER_RE.split(content)
     body = escape_snippet_text(parts[0])
-    for index, part in enumerate(parts[1:], start=1):
-        body += "${" + str(index) + "}" + escape_snippet_text(part)
-    return body, len(parts) - 1
+    count = 0
+    for name, text in zip(parts[1::2], parts[2::2]):
+        count += 1
+        stop = f"${{{count}:{escape_snippet_text(name)}}}" if name else f"${{{count}}}"
+        body += stop + escape_snippet_text(text)
+    return body, count
 
 
 def generate_snippet_name(filepath: Path, template_dir: Path) -> str:
@@ -54,7 +64,13 @@ def generate_snippet_name(filepath: Path, template_dir: Path) -> str:
 
 
 def get_description(filepath: Path, template_dir: Path) -> str:
-    """Describe the snippet by its path."""
+    """Use the summary comment on the first line. Fall back to the path."""
+    lines = filepath.read_text(encoding="utf-8").splitlines()
+    if lines and lines[0].startswith("#!"):
+        lines = lines[1:]
+    match = HEAD_COMMENT_RE.match(lines[0]) if lines else None
+    if match and match.group(1):
+        return match.group(1)
     rel_path = filepath.relative_to(template_dir)
     return f"{rel_path} template"
 
diff --git a/git-config-branch b/git-config-branch
index aaba2ca..73db71c 100644
--- a/git-config-branch
+++ b/git-config-branch
@@ -1,3 +1,4 @@
-[remote "<++>"]
-	url = git@github.com:equwal/<++>
+# git remote section for a GitHub repository
+[remote "<+remote+>"]
+	url = git@github.com:equwal/<+repository+>
 	fetch = +refs/heads/*:refs/remotes/origin/*
diff --git a/html/template.html b/html/template.html
index 117a4e0..64e9a95 100644
--- a/html/template.html
+++ b/html/template.html
@@ -1,3 +1,4 @@
+<!-- HTML5 page skeleton -->
 <!DOCTYPE html>
 <html>
 <head>
diff --git a/make/conditionals/conditional-variable b/make/conditionals/conditional-variable
index 61fc1bf..2b12b7f 100644
--- a/make/conditionals/conditional-variable
+++ b/make/conditionals/conditional-variable
@@ -1 +1,2 @@
+# Set a variable only when it is not set already
 <++> ?= <++>
diff --git a/make/conditionals/ifeq-endif b/make/conditionals/ifeq-endif
index 16a9851..14ebbf6 100644
--- a/make/conditionals/ifeq-endif
+++ b/make/conditionals/ifeq-endif
@@ -1,3 +1,4 @@
+# ifeq/ifneq conditional block
 if<++>eq (<++>,<++>) #space before the condition is important
     <++>
 endif
diff --git a/make/functions/foreach-simple b/make/functions/foreach-simple
index 099f9f5..daef786 100644
--- a/make/functions/foreach-simple
+++ b/make/functions/foreach-simple
@@ -1,2 +1,2 @@
-# see complex for more info
+# foreach over a list (see foreach-complex for more)
 $(foreach <++>, <++>, <++>)
diff --git a/make/golf/if-pkgconfig-exists b/make/golf/if-pkgconfig-exists
index 09b6ddb..7aabfa8 100644
--- a/make/golf/if-pkgconfig-exists
+++ b/make/golf/if-pkgconfig-exists
@@ -1,4 +1,4 @@
-# example with ncursesw like mtm or dvtm
+# Use pkg-config for a library when it is installed (example: ncursesw)
 CURSESLIB ?= <++>
 LIBS      ?= -l$(CURSESLIB) -lutil
 
diff --git a/make/mapping/map-fn b/make/mapping/map-fn
index 8994df9..706fbaa 100644
--- a/make/mapping/map-fn
+++ b/make/mapping/map-fn
@@ -1,3 +1,4 @@
+# Define a map function from a one-argument function
 var := a
 map = $(foreach $(var), $(2), $(call $(1),$(var)))
 # <new-mapped-fn> = $(call map,<old-onearg-fn>,<new-mapped-fn> map MAKE
diff --git a/make/pedantic-phony b/make/pedantic-phony
index 45212a3..a779912 100644
--- a/make/pedantic-phony
+++ b/make/pedantic-phony
@@ -1 +1,2 @@
+# Declare the standard phony targets
 .PHONY: install uninstall clean <++> # etc.
diff --git a/make/shell b/make/shell
index 3639c50..599bba4 100644
--- a/make/shell
+++ b/make/shell
@@ -1 +1,2 @@
+# Run a shell command and capture its output
 $(shell <++>)
diff --git a/make/templates/config.mk b/make/templates/config.mk
index 424ac8e..2b7847e 100644
--- a/make/templates/config.mk
+++ b/make/templates/config.mk
@@ -1,3 +1,4 @@
+# config.mk with version, shell and install prefixes
 VERSION=0.1
 SHELL=/bin/sh
 PROJECT=<++>
diff --git a/make/templates/dist b/make/templates/dist
index 1988968..156e158 100644
--- a/make/templates/dist
+++ b/make/templates/dist
@@ -1,3 +1,4 @@
+# Minimal makefile that builds a release tarball
 include config.mk
 
 # A super minimal makefile for deployment of a tarball.
diff --git a/sed/append-after-match b/sed/append-after-match
index 566870f..2ee3ccd 100644
--- a/sed/append-after-match
+++ b/sed/append-after-match
@@ -1 +1,2 @@
+# Append a line after the first line that matches
 sed "/# Use a comment for example to mark where you want to append/a\\$1" (FILE)<++>
diff --git a/sed/d b/sed/d
index 482895c..bc50e96 100644
--- a/sed/d
+++ b/sed/d
@@ -1,4 +1,4 @@
-# Use this command to try an example (in this case, the D sed command).
+# Delete matching lines with D (keeps the newline) or d
 # the line's contents -- but not the newline at the end -- will be deleted with
 # same as sed 's/^pattern.*//'
 sed '/^<++>/D' 
diff --git a/sh/for-array b/sh/for-array
index ffabaef..a5d8ba4 100644
--- a/sh/for-array
+++ b/sh/for-array
@@ -1,4 +1,4 @@
-#
+# Iterate over a saved argument list in POSIX sh
 # Yes, it really is this hard to do
 # and it is broken on GNU non-POSIX non-+ support.
 save () {
diff --git a/sh/str-fn b/sh/str-fn
index e5e15bf..badc4ff 100644
--- a/sh/str-fn
+++ b/sh/str-fn
@@ -1,3 +1,4 @@
+# Shell function that returns a string by reference
 fn () {
     # argument: the name of variable to call-by-reference, output string into.
     <++>
diff --git a/test_convert_to_luasnip.py b/test_convert_to_luasnip.py
index 7490353..913ff7e 100644
--- a/test_convert_to_luasnip.py
+++ b/test_convert_to_luasnip.py
@@ -90,3 +90,37 @@ def test_write_creates_package_json(tmp_path):
     package = json.loads((out / "package.json").read_text())
     assert package["contributes"]["snippets"] == [{"language": "sh", "path": "./sh.json"}]
     assert json.loads((out / "sh.json").read_text())["sh-x"]["prefix"] == "sh-x"
+
+
+def test_named_placeholder_becomes_named_tab_stop():
+    body, count = conv.convert_placeholders("[remote \"<+name+>\"] <++>")
+    assert body == '[remote "${1:name}"] ${2}'
+    assert count == 2
+
+
+@given(st.text(alphabet=st.characters(blacklist_characters="+>"), min_size=1, max_size=8))
+def test_named_placeholder_name_is_escaped(name):
+    body, _ = conv.convert_placeholders(f"<+{name}+>")
+    assert body == "${1:" + conv.escape_snippet_text(name) + "}"
+
+
+def test_description_comes_from_head_comment(tmp_path):
+    root = tmp_path / "tmpl"
+    (root / "sh").mkdir(parents=True)
+    f = root / "sh" / "thing"
+    f.write_text("#!/bin/sh\n# Print the thing\necho <++>\n")
+    assert conv.get_description(f, root) == "Print the thing"
+    g = root / "sh" / "lisp"
+    g.write_text(";; Lisp style\n(<++>)\n")
+    assert conv.get_description(g, root) == "Lisp style"
+    h = root / "sh" / "page"
+    h.write_text("<!-- Html style -->\n<++>\n")
+    assert conv.get_description(h, root) == "Html style"
+
+
+def test_description_falls_back_to_path(tmp_path):
+    root = tmp_path / "tmpl"
+    (root / "sh").mkdir(parents=True)
+    f = root / "sh" / "bare"
+    f.write_text("echo <++>\n")
+    assert conv.get_description(f, root) == "sh/bare template"