├── 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 | "
some link
" 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 | --------------------------------------------------------------------------------