├── file-format-tests.el
├── README.creole
└── file-format.el
/file-format-tests.el:
--------------------------------------------------------------------------------
1 | ;;; file-format-tests.el --- tests for file-templates
2 |
3 |
4 | (require 'fakir)
5 | (require 'ert)
6 | (require 'file-format)
7 |
8 | (ert-deftest file-format-test ()
9 | "Fake a file and test that we can format it."
10 | (fakir-fake-file
11 | (fakir-file :filename "test-page"
12 | :directory "/root/pages"
13 | :content "test that ${name}\nhas ${age} years\n")
14 | (equal
15 | (file-format
16 | "test-page" "/root/pages" 'aget
17 | '(("name" . "mr test")
18 | ("age" . "10")))
19 | "test that mr test\nhas 10 years\n")))
20 |
21 | (ert-deftest file-format-html-test ()
22 | "Fake a file and test that we can HTML format it."
23 | (fakir-fake-file
24 | (fakir-file :filename "test-page"
25 | :directory "/root/pages"
26 | :content "test that ${name}\nhas ${age} years\n")
27 | (equal
28 | (file-format-html
29 | "test-page" "/root/pages" 'aget
30 | '(("name" . "mr test")
31 | ("age" . "<10>")))
32 | "test that mr test\nhas <10> years\n")))
33 |
34 | (provide 'file-format-tests)
35 |
36 | ;;; file-format-tests.el ends here
37 |
--------------------------------------------------------------------------------
/README.creole:
--------------------------------------------------------------------------------
1 | = File formatting from EmacsLisp =
2 |
3 | Emacs has many templating libraries but none of them very useful for
4 | the sort of templating you do in web applications. So this is yet
5 | another //tool needed for Elnode//.
6 |
7 | I originally put this together for elmarmalade and now I need it in
8 | other places.
9 |
10 | === using ===
11 |
12 | It's quite simple to use. Make HTML pages that have template markers
13 | in them:
14 |
15 | {{{
16 |
17 |
18 |
19 |
20 | an HTML page!
21 |
22 |
23 | ${header}
24 | ${package-name}
25 |
${description}
26 | ${author}
27 |
28 |
29 | }}}
30 |
31 | and then call the {{{file-format-html}}} function:
32 |
33 | {{{
34 | (require 'file-format)
35 |
36 | (file-format-html
37 | filename
38 | docroot
39 | replacer-function
40 | extra) ;; => string of the replaced text
41 | }}}
42 |
43 | For example:
44 |
45 | {{{
46 | (file-format-html
47 | "my-page.html"
48 | (expand-file-name "templates-dir" marmalade-root)
49 | 'aget
50 | `(("package-name" . ,package-name)
51 | ("author" . ,(if (or (not author)(equal author "")) "Unknown" author))
52 | ("description" . ,description)
53 | ;; Replace safely later
54 | ("header" . ,(propertize
55 | ""
56 | :file-format-html-safe t))))
57 | }}}
58 |
59 | {{{file-format}}} functions use the {{{s-format}}} convention for the
60 | replacement function, mostly that means you can just use the symbol
61 | {{{aget}}} and use an alist.
62 |
63 | A function can be supplied and that is called with the key and the
64 | {{{extra}}} argument to lookup the value to be replaced.
65 |
66 | === safety and HTML ===
67 |
68 | Everything you insert into the HTML with the templating is passed
69 | through {{{xml-escape-string}}} so it should be safe. If you //want//
70 | to insert HTML you therefore have to propertize it so that
71 | {{{file-format-html}}} knows not to escape it.
72 |
73 | === other functions ===
74 |
75 | There's a non-HTML version: {{{file-format}}} which does no escaping.
76 |
77 |
78 | === how does it work? ===
79 |
80 | {{{file-format}}} just uses the {{{s}}} library. It reads the whole
81 | file into a string and does the replacement there.
82 |
--------------------------------------------------------------------------------
/file-format.el:
--------------------------------------------------------------------------------
1 | ;;; file-format.el --- templates with files as the source -*- lexical-binding: t -*-
2 |
3 | ;; Copyright (C) 2014 Nic Ferrier
4 |
5 | ;; Author: Nic Ferrier
6 | ;; Keywords: lisp
7 | ;; Package-Requires: ((s "1.5.0"))
8 | ;; Version: 0.0.4
9 | ;; Url: http://github.com/nicferrier/emacs-file-format
10 |
11 | ;; This program is free software; you can redistribute it and/or modify
12 | ;; it under the terms of the GNU General Public License as published by
13 | ;; the Free Software Foundation, either version 3 of the License, or
14 | ;; (at your option) any later version.
15 |
16 | ;; This program is distributed in the hope that it will be useful,
17 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
18 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19 | ;; GNU General Public License for more details.
20 |
21 | ;; You should have received a copy of the GNU General Public License
22 | ;; along with this program. If not, see .
23 |
24 | ;;; Commentary:
25 |
26 | ;; File templates let you keep template descriptions in a file and
27 | ;; then fill in the template values.
28 |
29 | ;; This attempts to be efficient, not fetching the file templates
30 | ;; unless they have been changed. They are stored internally, as
31 | ;; strings, in a cache.
32 |
33 | ;; Here's an example, from marmalade:
34 |
35 | ;; (file-format-html
36 | ;; "package-page.html" marmalade-dir 'aget
37 | ;; `(("package-name" . ,package-name)
38 | ;; ("version" . ,(format "%S" version))
39 | ;; ("author" . ,(if (or (not author)
40 | ;; (equal author ""))
41 | ;; "Unknown" author))
42 | ;; ("package-download" . ,package-download)
43 | ;; ("description" . ,description)
44 | ;; ("about" . ,about-text)
45 | ;; ;; Replace safely later
46 | ;; ("header" . ,(propertize
47 | ;; (marmalade/page-header httpcon)
48 | ;; :file-format-html-safe t))))
49 |
50 | ;; Note the use of propertize to mark the HTML as safe so it isn't
51 | ;; escaped.
52 |
53 | ;;; Code:
54 |
55 | (require 'xml)
56 | (require 's)
57 |
58 | (defmacro with-transient-file (file-name &rest code)
59 | "Load FILE-NAME into a buffer and eval CODE.
60 |
61 | Then dispose of the buffer.
62 |
63 | File loading errors may be generated as by any call to visit a
64 | file."
65 | (declare (indent 1)
66 | (debug (sexp &rest form)))
67 | (let ((fvn (make-symbol "fv"))
68 | (bvn (make-symbol "bv")))
69 | `(let* ((,fvn ,file-name)
70 | (,bvn (get-buffer-create
71 | (generate-new-buffer-name "transient"))))
72 | (unwind-protect
73 | (with-current-buffer ,bvn
74 | (insert-file-contents-literally ,fvn)
75 | ,@code)
76 | (progn
77 | (set-buffer-modified-p nil)
78 | (kill-buffer ,bvn))))))
79 |
80 | (defconst file-format/cache (make-hash-table :test 'equal)
81 | "The cache used for the template objects.")
82 |
83 | (defconst file-attributes (list (cons :file-type 0)
84 | (cons :link-count 1)
85 | (cons :uid 2)
86 | (cons :gid 3)
87 | (cons :atime 4)
88 | (cons :mtime 5)
89 | (cons :stime 6)
90 | (cons :size 7)
91 | (cons :mode 8)
92 | (cons :gid-change 9)
93 | (cons :inode 10)
94 | (cons :fsdevice 11))
95 | "The indices for `file-attributes' list items.")
96 |
97 | (defun file-attr (filename symbolic-name)
98 | "Get the SYMBOLIC-NAME attribute from FILENAME, eg: `:mtime'."
99 | ;; We could do with caching this stuff
100 | (elt (file-attributes (expand-file-name filename))
101 | (kva symbolic-name file-attributes)))
102 |
103 | (defun file-format/template-get (file-name root)
104 | "Return the template object for the file FILE-NAME under ROOT."
105 | (let* ((name (file-name-nondirectory file-name))
106 | (template (gethash name file-format/cache))
107 | (file-details (file-attributes (expand-file-name name root))))
108 | (unless file-details
109 | (signal 'file-error (format "file %s at %s not found" name root)))
110 | (let ((mtime (file-attr file-name :mtime)))
111 | (when (or (not template)
112 | (time-less-p (or (plist-get template :time)
113 | (seconds-to-time 0))
114 | (or mtime (current-time))))
115 | (with-transient-file (expand-file-name name root)
116 | (puthash name
117 | (list :name name
118 | :time mtime
119 | :content (buffer-string)) file-format/cache)))
120 | (gethash name file-format/cache))))
121 |
122 | (defun file-format (name root func &rest extra)
123 | "Format the template file specified by NAME under ROOT.
124 |
125 | FUNC specifies how to replace the templated values, possibly with
126 | EXTRA. See `s-format' for more details."
127 | (apply 's-format (plist-get (file-format/template-get name root) :content)
128 | func extra))
129 |
130 | (defun file-format-html (name root replacer &rest extra)
131 | "Format as HTML the template file specified by NAME under ROOT.
132 |
133 | FUNC specifies how to replace the templated values, possibly with
134 | EXTRA. See `s-format' for more details.
135 |
136 | Each value replaced is escaped for HTML-ness with
137 | `xml-escape-string' unless it is marked with the text property
138 | `:file-format-html-safe'."
139 | (apply 's-format
140 | (plist-get (file-format/template-get name root) :content)
141 | (lambda (key &optional extra)
142 | (let ((value
143 | (cond
144 | ((eq replacer 'aget) (s--aget extra key))
145 | ((eq replacer 'elt) (elt extra key))
146 | (t
147 | (if extra
148 | (funcall replacer key extra)
149 | (funcall replacer key))))))
150 | (if (memq :file-format-html-safe (text-properties-at 0 value))
151 | value
152 | (xml-escape-string value))))
153 | extra))
154 |
155 | (provide 'file-format)
156 |
157 | ;;; file-format.el ends here
158 |
--------------------------------------------------------------------------------