├── LICENSE ├── README.md ├── reader.lua ├── stylua.toml ├── test_files ├── reader_problematic_cases.typ ├── reader_test_file.typ └── writer_test_file.md └── writer.lua /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Louis Vignoli 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [pandoc 3.1.2](https://github.com/jgm/pandoc/releases/tag/3.1.2) now provides a Typst writer to convert **to** Typst. 2 | A reader is also [planned](https://github.com/jgm/pandoc/issues/8740). 3 | 4 | Hence, because of upcoming native support for Typst, this extension will **not** be maintained. 5 | 6 | --- 7 | 8 | # Typst custom reader and writer for Pandoc 9 | 10 | [Typst](https://typst.app) is a modern markup-based typesetting system with powerful typesetting and scripting capabilities. 11 | _It's really great._ 12 | 13 | [Pandoc](https://pandoc.org) is a universal document converter that handles **lots** of formats. 14 | 15 | The custom `reader.lua` and `writer.lua` allow converting to and from Typst using Pandoc embedded Lua engine. 16 | 17 | ## Usage 18 | 19 | ```terminal 20 | pandoc -t writer.lua input.tex -o output.typ 21 | pandoc -f reader.lua input.typ -o output.html 22 | ``` 23 | 24 | > Hint: 25 | > Try converting this README to Typst! 26 | 27 | See [Pandoc User's guide](https://pandoc.org/MANUAL.html) for more advanced usage. 28 | 29 | ## TODO 30 | 31 | Some planned improvements to be done. 32 | 33 | - [X] Better formatting 34 | - [X] Sort imports 35 | - [ ] Support tight and sparse lists properly 36 | - [ ] Improve standalone template with prettier default 37 | - [ ] Parse the language of code blocks in Typst markup \[reader\] 38 | - [ ] Parse Typst nested lists (that's a bit tricky here, use a recursive pattern or an LPeg dedicated grammar) \[reader\] 39 | - [ ] Parse escaped Typst markup characters \[reader\] 40 | - [ ] Work towards feature completeness 41 | - [ ] Handle most informative attributes in the writer 42 | - [ ] … 43 | 44 | --- 45 | 46 | ## Features completeness 47 | 48 | Checked items are minimally supported. 49 | Unchecked items are not supported. 50 | 51 | ### Writer 52 | 53 | #### Blocks 54 | 55 | - [X] Plain 56 | - [X] Para 57 | - [ ] LineBlock 58 | - [X] CodeBlock 59 | - [X] RawBlock 60 | - [X] BlockQuote 61 | - [X] OrderedList 62 | - [X] BulletList 63 | - [X] DefinitionList 64 | - [X] Header 65 | - [X] HorizontalRule 66 | - [ ] Table 67 | - [ ] Figure 68 | - [X] Div 69 | 70 | #### Inlines 71 | 72 | - [X] Str 73 | - [X] Emph 74 | - [X] Underline 75 | - [X] Strong 76 | - [X] Strikeout 77 | - [X] Superscript 78 | - [X] Subscript 79 | - [X] SmallCaps 80 | - [X] Quoted 81 | - [ ] Cite 82 | - [X] Code 83 | - [X] Space 84 | - [X] SoftBreak 85 | - [X] LineBreak 86 | - [ ] Math (needs to convert TeX syntax to Typst) 87 | - [ ] RawInline 88 | - [X] Link 89 | - [X] Image 90 | - [ ] Note 91 | - [X] Span 92 | 93 | ### Reader 94 | 95 | Some Pandoc AST items do not have a dedicated Typst markup. 96 | The result is usually obtained by a generic and expected function call, which could be parsed, such as `#strike[redacted]` or `#underline[important]`. 97 | 98 | #### Blocks 99 | 100 | - [X] Plain 101 | - [X] Para 102 | - [ ] LineBlock 103 | - [X] CodeBlock 104 | - [ ] RawBlock 105 | - [ ] BlockQuote (no markup) 106 | - [X] OrderedList 107 | - [X] BulletList 108 | - [ ] DefinitionList 109 | - [X] Header 110 | - [X] HorizontalRule 111 | - [ ] Table 112 | - [ ] Figure 113 | - [ ] Div (probably corresponds to a content block) 114 | 115 | #### Inlines 116 | 117 | - [X] Str 118 | - [X] Emph 119 | - [ ] Underline (no markup) 120 | - [X] Strong 121 | - [ ] Strikeout (no markup) 122 | - [X] Superscript 123 | - [X] Subscript 124 | - [X] SmallCaps 125 | - [ ] Quoted 126 | - [ ] Cite 127 | - [X] Code 128 | - [X] Space 129 | - [X] SoftBreak 130 | - [X] LineBreak 131 | - [ ] Math (needs to convert TeX syntax to Typst, not planned) 132 | - [ ] RawInline 133 | - [X] Link 134 | - [ ] Image 135 | - [ ] Note 136 | - [ ] Span 137 | -------------------------------------------------------------------------------- /reader.lua: -------------------------------------------------------------------------------- 1 | -- A custom Pandoc Typst reader. 2 | -- Louis Vignoli 3 | 4 | -- Imports 5 | -- These modules are provided in Pandoc embedded Lua runtime. 6 | 7 | local lpeg = require "lpeg" 8 | local pandoc = require "pandoc" 9 | 10 | -- For better performance we put these functions in local variables: 11 | 12 | local B = lpeg.B 13 | local C = lpeg.C 14 | local Cb = lpeg.Cb 15 | local Cc = lpeg.Cc 16 | local Cf = lpeg.Cf 17 | local Cg = lpeg.Cg 18 | local Cmt = lpeg.Cmt 19 | local Cs = lpeg.Cs 20 | local Ct = lpeg.Ct 21 | local P = lpeg.P 22 | local R = lpeg.R 23 | local S = lpeg.S 24 | local V = lpeg.V 25 | 26 | -- Common patterns 27 | 28 | local whitespacechar = S " \t\r\n" 29 | local specialchar = S "/*_~[]\\{}|`" 30 | local wordchar = (1 - (whitespacechar + specialchar)) 31 | local spacechar = S " \t" 32 | local newline = P "\r" ^ -1 * P "\n" 33 | local blankline = spacechar ^ 0 * newline 34 | local endline = newline * #-blankline 35 | local endequals = spacechar ^ 0 * P "=" ^ 0 * spacechar ^ 0 * newline 36 | local cellsep = spacechar ^ 0 * P "|" 37 | 38 | -- trim trims a string from its surrounding whitespaces. 39 | local function trim(s) 40 | return (s:gsub("^%s*(.-)%s*$", "%1")) 41 | end 42 | 43 | -- ListItem is a helper function to parse bullet and numbered lists. 44 | -- It is almost copy pasted form the pandoc Creole reader example. 45 | local function ListItem(lev, ch) 46 | local start 47 | if ch == nil then 48 | start = S "-+" 49 | else 50 | start = P(ch) 51 | end 52 | local subitem = function(c) 53 | if lev < 6 then 54 | return ListItem(lev + 1, c) 55 | else 56 | return (1 - 1) -- fails 57 | end 58 | end 59 | local parser = spacechar ^ 0 60 | * start ^ lev -- NOTE: This patterns is completely wrong regarding Typst grammar, but flat list are well-parsed this way. 61 | * #-start 62 | * spacechar ^ 0 63 | * Ct((V "Inline" - (newline * spacechar ^ 0 * S "-+")) ^ 0) 64 | * newline 65 | * (Ct(subitem "-" ^ 1) / pandoc.BulletList + Ct(subitem "+" ^ 1) / pandoc.OrderedList + Cc(nil)) 66 | / function(ils, sublist) 67 | return { pandoc.Plain(ils), sublist } 68 | end 69 | return parser 70 | end 71 | 72 | -- Typst (uncomplete) grammar. 73 | G = P { 74 | "Pandoc", 75 | Pandoc = Ct(V "Block" ^ 0) / pandoc.Pandoc, 76 | 77 | Block = blankline ^ 0 * (V "Header" + V "CodeBlock" + V "List" + V "Para"), 78 | 79 | Para = Ct(V "Inline" ^ 1) * newline / pandoc.Para, 80 | Header = P "=" ^ 1 / string.len * spacechar ^ 1 * Ct(V "Inline" ^ 1) / pandoc.Header, 81 | CodeBlock = P "```" * blankline * C((1 - P "```") ^ 0) * P "```" / trim / pandoc.CodeBlock, 82 | List = V "BulletList" + V "OrderedList", 83 | BulletList = Ct(ListItem(1, "-") ^ 1) / pandoc.BulletList, 84 | OrderedList = Ct(ListItem(1, "+") ^ 1) / pandoc.OrderedList, 85 | 86 | Inline = V "Strong" + V "Emph" + V "LineBreak" + V "Str" + V "Space" + V "SoftBreak" + V "Code", 87 | 88 | Strong = P "*" * Ct((V "Inline" - P "*") ^ 1) * P "*" / pandoc.Strong, 89 | Emph = P "_" * Ct((V "Inline" - P "_") ^ 1) * P "_" / pandoc.Emph, 90 | LineBreak = P "\\" / pandoc.LineBreak, 91 | Str = wordchar ^ 1 / pandoc.Str, 92 | Space = spacechar ^ 1 / pandoc.Space, 93 | SoftBreak = endline / pandoc.SoftBreak, 94 | Code = P "`" * C((1 - P "`") ^ 1) * P "`" / pandoc.Code, 95 | } 96 | 97 | function Reader(input, reader_options) 98 | return lpeg.match(G, tostring(input)) 99 | end 100 | -------------------------------------------------------------------------------- /stylua.toml: -------------------------------------------------------------------------------- 1 | call_parentheses = "None" -------------------------------------------------------------------------------- /test_files/reader_problematic_cases.typ: -------------------------------------------------------------------------------- 1 | Escaped markup\_ \#character should be \*parsed correctly\$. 2 | 3 | Nested lists: 4 | - 1 5 | - 1.1 6 | - 1.2 7 | - 2 8 | -------------------------------------------------------------------------------- /test_files/reader_test_file.typ: -------------------------------------------------------------------------------- 1 | = Title 2 | 3 | == First section 4 | 5 | Hello *World*, how are _you_? 6 | 7 | A sentence with long spaces. They should collapse. 8 | 9 | == Second section 10 | 11 | I am `fine`. \ 12 | You? 13 | 14 | Some code 15 | 16 | ``` 17 | func main(){ 18 | fmt.Println("Hello World!") 19 | } 20 | ``` 21 | 22 | - One 23 | - Two 24 | - Three 25 | 26 | Another list 27 | 28 | + One 29 | + Two 30 | + Three 31 | -------------------------------------------------------------------------------- /test_files/writer_test_file.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Writer test file 3 | author: Louis Vignoli 4 | date: 2022-02-09 5 | --- 6 | 7 | # Title 8 | 9 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. 10 | 11 | ## Subtitle 12 | 13 | This is a _test_ file. It is **important** that it renders well, not ~~bad~~. 14 | 15 | A line with a 16 | line break (check the double space ending in the markdown source). 17 | 18 | It # should #escapes special $ #\` $characters` correctly*, right?_*. 19 | 20 | Look at the math now: for $a$, $b$ and $c$ are real numbers with $a\neq 0$, the quadratic formula is 21 | 22 | $$ 23 | x_\pm = \frac{-b\pm\sqrt{b^2-4ac}}{2a}. 24 | $$ 25 | 26 | Of course, this latex math syntax is not compilable in Typst, so for now we ouput a dummy symbol. 27 | 28 | ## Recipe 29 | 30 | - Olive 31 | - Garlic 32 | - Feta 33 | 34 | 1. Cut the garlic and make 35 | 2. Cut the feta in cube 36 | 3. I have no idea what to do now 37 | 38 | Banana 39 | : A fruit. 40 | 41 | Apple 42 | : Another fruit. 43 | : It's more juicy than banana. 44 | 45 | Some nested lists 46 | 47 | - 1 48 | - 1.1 49 | - 1.2 50 | - 2 51 | - 2.1 52 | - 2.1.1 53 | - 2.1.2 54 | - 2.2 55 | - 3 56 | 57 | ## Stuff 58 | 59 | Click [here](https://example.com). It points to `example.com`. 60 | 61 | Here is some `Go` code: 62 | 63 | ```go 64 | func main() { 65 | fmt.Println("Hello World!") 66 | } 67 | ``` 68 | 69 | and now some `rust` code as well 🦀 70 | 71 | ```rust 72 | fn main() { 73 | println!("Hello World!"); 74 | } 75 | ``` 76 | 77 | Now some famous quotes: 78 | 79 | > Life is what happens when you're busy making other plans. — John Lennon 80 | 81 | > Whoever is happy will make others happy too. — Anne Frank 82 | 83 | Another citation but multiline in the markdown source 84 | 85 | > Nous avons certains souvenirs qui sont comme la peinture hollandaise de notre mémoire, tableaux de genre où les personnages sont souvent de condition médiocre, pris à un moment bien simple de leur existence, sans événements solennels, parfois sans événements du tout, dans un cadre nullement extraordinaire et sans grandeur. 86 | > Le naturel des caractères et l'innocence de la scène en font l'agrément, l'éloignement met entre elle et nous une lumière douce qui la baigne de beauté. 87 | > 88 | > — _Le plaisir des jours_ (1896), Marcel Proust 89 | 90 | --- 91 | 92 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. 93 | 94 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. 95 | Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. 96 | Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. 97 | Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. 98 | -------------------------------------------------------------------------------- /writer.lua: -------------------------------------------------------------------------------- 1 | -- A custom Pandoc Typst writer. 2 | -- Louis Vignoli 3 | 4 | -- Imports 5 | -- This module is provided in Pandoc embedded Lua runtime. 6 | 7 | local pandoc = require "pandoc" 8 | 9 | -- For better performance we put these functions in local variables: 10 | 11 | local layout = pandoc.layout 12 | 13 | local blankline = layout.blankline 14 | local concat = layout.concat 15 | local cr = layout.cr 16 | local double_quotes = layout.double_quotes 17 | local empty = layout.empty 18 | local hang = layout.hang 19 | local inside = layout.inside 20 | local literal = layout.literal 21 | local nest = layout.nest 22 | local parens = layout.parens 23 | local prefixed = layout.prefixed 24 | local space = layout.space 25 | local to_roman = pandoc.utils.to_roman_numeral 26 | local stringify = pandoc.utils.stringify 27 | 28 | -- Default indent size for Typst generated code. 29 | local TAB_SIZE = 2 30 | local BLOCKQUOTE_BOX_RELATIVE_WIDTH = "97%" 31 | 32 | -- Writer is the custom writer that Pandoc will use. 33 | -- We scaffold it from Pandoc. 34 | Writer = pandoc.scaffolding.Writer 35 | 36 | local inlines = Writer.Inlines 37 | local blocks = Writer.Blocks 38 | 39 | -- escape escapes reserved Typst markup character with a backslash. 40 | local escape = function(s) 41 | return (s:gsub("[#$\\'\"`_*]", function(x) 42 | return "\\" .. x 43 | end)) 44 | end 45 | 46 | -- inline_wrap wraps a doc in a call to cmd using Typst content syntax. 47 | -- Namely 48 | -- cmd[doc] 49 | -- Any additional # in cmd is the caller responsability. 50 | local inline_wrap = function(doc, cmd) 51 | cmd = cmd or "#" 52 | return concat { cmd .. "[", doc, "]" } 53 | end 54 | 55 | -- wrap wraps a doc in a call to cmd using Typst content blocks syntax. 56 | -- Namely 57 | -- cmd[ 58 | -- doc 59 | -- ] 60 | -- Any additional # in cmd is the caller responsability. 61 | local wrap = function(doc, cmd) 62 | cmd = cmd or "#" 63 | return concat { cmd .. "[", cr, nest(doc, TAB_SIZE), cr, "]" } 64 | end 65 | 66 | Writer.Block.Null = function(e) 67 | return empty 68 | end 69 | 70 | Writer.Block.Plain = function(el) 71 | return inlines(el.content) 72 | end 73 | 74 | Writer.Block.Para = function(para) 75 | return { Writer.Inlines(para.content), blankline } 76 | end 77 | 78 | Writer.Block.Header = function(header) 79 | return { 80 | string.rep("=", header.level), 81 | space, 82 | inlines(header.content), 83 | } 84 | end 85 | 86 | Writer.Block.BulletList = function(e) 87 | local function render_item(item) 88 | return concat { hang(blocks(item), TAB_SIZE, "- "), cr } -- hang allows to indent nested list properly 89 | end 90 | return e.content:map(render_item) 91 | end 92 | 93 | Writer.Block.OrderedList = function(e) 94 | local function render_item(item) 95 | return hang(blocks(item, blankline), TAB_SIZE, "+ ") 96 | end 97 | 98 | local sep = cr 99 | return concat(e.content:map(render_item), sep) 100 | end 101 | 102 | Writer.Block.DefinitionList = function(e) 103 | -- To simplify their treatment, blocks after the first one in definitions 104 | -- are put on the next line and indented by a single space. It is not very 105 | -- pretty (better would be to indent them by TAB_SIZE or up to the colon), 106 | -- but valid Typst syntax. 107 | local function render_term(term) 108 | return concat { concat { "/", space, inlines(term), ":" } } 109 | end 110 | local function render_definition(def) 111 | return concat { " ", blocks(def), cr } 112 | end 113 | local function render_item(item) 114 | local term, definitions = table.unpack(item) 115 | local inner = concat(definitions:map(render_definition)) 116 | return concat { render_term(term), inner } 117 | end 118 | 119 | local sep = cr 120 | return concat(e.content:map(render_item), sep) 121 | end 122 | 123 | Writer.Block.CodeBlock = function(e) 124 | return { "```", e.classes[1], cr, nest(e.text, TAB_SIZE), cr, "```" } 125 | end 126 | 127 | Writer.Block.BlockQuote = function(e) 128 | -- Since there is no dedicated Typst markup, we retain the popular 129 | -- blockquote rendering of having an indented block in dimmed font with a 130 | -- light vertical ruler on the left. 131 | 132 | local indent = "#h(1fr)" -- filler space that pushes the box to the right 133 | local style = '#set text(style: "italic", fill: gray.darken(10%))' 134 | local citation = concat { style, cr, blocks(e.content), blankline } 135 | 136 | -- The choice of having a relative indent rather than an absolute one is debatable. 137 | -- It works well with current Typst default page layout. 138 | local box_cmd = string.format("#box(width: %s)", BLOCKQUOTE_BOX_RELATIVE_WIDTH) 139 | local box = concat { indent, cr, wrap(citation, box_cmd) } 140 | 141 | local stacked_content = wrap(box, "#stack(dir: ltr)") 142 | 143 | local rect_cmd = "#rect(stroke: (left:2pt+silver))" 144 | local result = wrap(stacked_content, rect_cmd) 145 | 146 | return result 147 | end 148 | 149 | Writer.Block.Div = function(e) 150 | return concat { blankline, wrap(blocks(e.content)), blankline } 151 | end 152 | 153 | Writer.Block.HorizontalRule = function(e) 154 | return { blankline, "#line(length: 100%)", blankline } 155 | end 156 | 157 | Writer.Inline.Str = function(e) 158 | return escape(e.text) 159 | end 160 | 161 | Writer.Inline.Space = space 162 | 163 | Writer.Inline.SoftBreak = function(_, opts) 164 | return opts.wrap_text == "wrap-preserve" and cr or space 165 | end 166 | 167 | Writer.Inline.LineBreak = { space, "\\", cr } 168 | 169 | Writer.Inline.Emph = function(el) 170 | return { "_", inlines(el.content), "_" } 171 | end 172 | 173 | Writer.Inline.Strong = function(el) 174 | return { "*", inlines(el.content), "*" } 175 | end 176 | 177 | Writer.Inline.Strikeout = function(el) 178 | return { inline_wrap(inlines(el.content), "#strike") } 179 | end 180 | 181 | Writer.Inline.Subscript = function(el) 182 | return { inline_wrap(inlines(el.content), "#sub") } 183 | end 184 | 185 | Writer.Inline.Superscript = function(el) 186 | return { inline_wrap(inlines(el.content), "#super") } 187 | end 188 | 189 | Writer.Inline.Underline = function(el) 190 | return { inline_wrap(inlines(el.content), "#underline") } 191 | end 192 | 193 | Writer.Inline.SmallCaps = function(el) 194 | return { inline_wrap(inlines(el.content), "#smallcaps") } 195 | end 196 | 197 | Writer.Inline.Link = function(link) 198 | local cmd = "#link" .. parens(double_quotes(link.target)) 199 | return inline_wrap(inlines(link.content), cmd) 200 | end 201 | 202 | Writer.Inline.Span = function(el) 203 | return inlines(el.content) 204 | end 205 | 206 | Writer.Inline.Quoted = function(el) 207 | if el.quotetype == "DoubleQuote" then 208 | return concat { '"', inlines(el.content), '"' } 209 | else 210 | return concat { "'", inlines(el.content), "'" } 211 | end 212 | end 213 | 214 | Writer.Inline.Code = function(code) 215 | return { "`", code.text, "`" } 216 | end 217 | 218 | Writer.Inline.Math = function(math) 219 | -- Conversion between LaTeX math and Typst math is out of the scope of this 220 | -- writer. We return a dummy filler symbol. 221 | if math.mathtype == "DisplayMath" then 222 | return literal "$ quest.excl $" 223 | else 224 | return literal "$quest.excl$" 225 | end 226 | end 227 | 228 | Writer.Inline.Image = function(img) 229 | return { inline_wrap(img.src, "#image") } 230 | end 231 | 232 | -- template_string is a default template for this reader, applied when pandoc 233 | -- CLI is called with the -s/--standalone. 234 | -- BUG: the numbersections if branch is not entered when pandoc CLI is called 235 | -- with the -n/--number-sections options, but works when the numbersections 236 | -- field is set to true in a pandoc YAML header. Probably a Pandoc bug? 237 | local template_string = [[ 238 | #set page("a4") 239 | #set text(lang: "en") 240 | $if(numbersections)$ 241 | #set headings(numbering: "1.1") 242 | $endif$ 243 | $if(toc)$ 244 | // Future table of contents call here. 245 | $endif$ 246 | $if(date)$ 247 | $date$ 248 | $endif$ 249 | 250 | $body$ 251 | ]] 252 | 253 | Template = function() 254 | return template_string 255 | end 256 | --------------------------------------------------------------------------------