├── .github └── workflows │ └── ci.yml ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── bin └── lcmark ├── filters ├── count_links.lua ├── highlight.lua └── include.lua ├── lcmark.1 ├── lcmark.1.md ├── lcmark.lua ├── rockspec.in ├── standalone ├── Makefile └── main.c ├── templates ├── default.html ├── default.man └── simple.latex ├── test.t └── tests ├── bad_filter.lua ├── bad_filter2.lua ├── bad_filter3.lua └── spec-tests.lua /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI tests 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | linux: 7 | 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v1 11 | - uses: leafo/gh-actions-lua@v9 12 | - uses: leafo/gh-actions-luarocks@v4 13 | - name: Build and test 14 | run: make 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.rockspec 2 | 3 | # Compiled Lua sources 4 | luac.out 5 | 6 | # luarocks build files 7 | *.src.rock 8 | *.zip 9 | *.tar.gz 10 | 11 | # Object files 12 | *.o 13 | *.os 14 | *.ko 15 | *.obj 16 | *.elf 17 | 18 | # Precompiled Headers 19 | *.gch 20 | *.pch 21 | 22 | # Libraries 23 | *.lib 24 | *.a 25 | *.la 26 | *.lo 27 | *.def 28 | *.exp 29 | 30 | # Shared objects (inc. Windows DLLs) 31 | *.dll 32 | *.so 33 | *.so.* 34 | *.dylib 35 | 36 | # Executables 37 | *.exe 38 | *.out 39 | *.app 40 | *.i*86 41 | *.x86_64 42 | *.hex 43 | 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014-2015, John MacFarlane 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 15 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 16 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 18 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 20 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 21 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 22 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | 25 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | VERSION=0.31.1 2 | REVISION=1 3 | ROCKSPEC=lcmark-$(VERSION)-$(REVISION).rockspec 4 | TESTS=tests 5 | CMARK_DIR=../cmark 6 | 7 | .PHONY: clean, test, all, rock, update, check, checkversion 8 | 9 | all: rock lcmark.1 10 | 11 | checkversion: 12 | @grep -q 'lcmark.version = "$(VERSION)"' lcmark.lua || \ 13 | (echo "lcmark.version needs updating in lcmark.lua" && exit 1) 14 | 15 | lcmark.1: lcmark.1.md templates/default.man 16 | bin/lcmark -t man --template templates/default.man -o $@ $< 17 | 18 | $(ROCKSPEC): rockspec.in 19 | sed -e "s/_VERSION/$(VERSION)/g; s/_REVISION/$(REVISION)/g" $< > $@ 20 | 21 | rock: checkversion $(ROCKSPEC) 22 | luarocks --local make $(ROCKSPEC) 23 | 24 | upload: rock 25 | luarocks upload --api-key=$(LUAROCKS_API_KEY) $(ROCKSPEC) 26 | 27 | update: $(TESTS)/spec-tests.lua 28 | 29 | $(TESTS)/spec-tests.lua: $(CMARK_DIR)/test/spec.txt 30 | python3 $(CMARK_DIR)/test/spec_tests.py -d --spec $< | sed -e 's/^\([ \t]*\)"\([^"]*\)":/\1\2 = /' | sed -e 's/^\[/return {/' | sed -e 's/^\]/}/' > $@ 31 | 32 | check: 33 | luacheck bin/lcmark lcmark.lua 34 | 35 | test: check 36 | prove test.t 37 | 38 | clean: 39 | rm -rf *.o $(CBITS)/*.o $(ROCKSPEC) 40 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # lcmark 2 | 3 | `lcmark` is a high-level [CommonMark](https://commonmark.org/) converter 4 | built upon [`cmark`](https://github.com/jgm/cmark). It comes as both a 5 | command-line program and a Lua module. It supports: 6 | 7 | - **YAML metadata** at the top of the document. 8 | 9 | - **Filters**, which allow the document to be transformed between parsing and 10 | rendering, making a large number of customizations possible. 11 | 12 | - **Templates**, which allow the body and metadata values to be embedded into a 13 | pre-defined structure. 14 | 15 | Also see the [documentation](#module). 16 | 17 | 18 | # Installation 19 | 20 | To install: `luarocks install lcmark` 21 | 22 | (This installs both the library and the program). 23 | 24 | Additionally, you'll also need a YAML parsing library. `lcmark` will 25 | automatically attempt to load and use one of 26 | [yaml](https://github.com/lubyk/yaml), 27 | [lua-yaml](https://github.com/exosite/lua-yaml) or 28 | [lyaml](https://github.com/gvvaughan/lyaml), so make sure you have one of those 29 | installed. Alternatively, a custom parser can be used (see the `yaml_parser` 30 | option below). 31 | 32 | 33 | # Features 34 | 35 | ## YAML Metadata 36 | 37 | The YAML metadata section (if present) must occur at the beginning of the 38 | document. It begins with a line containing `---` and ends with a line 39 | containing `...` or `---`. Between these, a YAML key/value map is expected. 40 | 41 | String values found in the metadata will be parsed and rendered as 42 | CommonMark. If a string value contains only a single paragraph, it will be 43 | rendered as an inline string. 44 | 45 | If the `yaml_parser` option (a function) is provided, `lcmark` will use it to 46 | parse YAML. The function should take a string as input and should return a 47 | table. In case of failure, it should either throw an error or return a 48 | `nil, err` tuple; other returns will be discarded silently. 49 | 50 | Example: 51 | 52 | ``` 53 | --- 54 | # This is a comment! 55 | # Note that the quotes below are needed because of the 56 | # colon in the title: 57 | title: 'This is my *article*: subtitle here' 58 | author: 59 | - name: Sam Smith 60 | institute: U of X 61 | - name: Sasha Xi 62 | institute: NXQ 63 | abstract: | 64 | Here is a multiline abstract. 65 | 66 | - It can even 67 | - contain 68 | - lists and other block elements 69 | ... 70 | 71 | Document body starts here... 72 | ``` 73 | 74 | ## Filters 75 | 76 | Filters modify the parsed document prior to rendering. 77 | 78 | A filter is a function that takes three arguments (`doc`, `meta`, `to`), where 79 | `doc` is a cmark node, `meta` is the YAML metadata as a (potentially nested) Lua 80 | table with all strings replaced with cmark nodes, and `to` is a string 81 | specifying the output format (the same string as passed to `lcmark.convert`). 82 | The filter may destructively modify `doc` and `meta`. 83 | 84 | Some sample filters are provided in 85 | [`filters/`](https://github.com/jgm/lcmark/tree/master/filters). 86 | 87 | ## Templates 88 | 89 | Templates are used to build standalone documents from the parsed document body 90 | and metadata. 91 | 92 | `lcmark` supports a small subset of the templating language used by 93 | [pandoc](http://pandoc.org), and `lcmark` templates can be used with pandoc 94 | (with the caveat that pandoc sets many variables automatically that `lcmark` 95 | does not). 96 | 97 | `nil`, `false` and `{}` (an empty table) are considered to be "falsy" values. 98 | Any other value is considered "truthy". 99 | 100 | A quick guide: 101 | 102 | - The only special character in templates is `$`. To get 103 | a literal `$` character, use `$$`. 104 | 105 | - `$name$` will be replaced with the value of the `name` 106 | metadata field. Variable names can contain alphanumerics, 107 | `-`, and `_`. 108 | 109 | - `$name.subname$` will be replaced with the value of the 110 | `subname` field of the `name` metadata field (assumed to 111 | be a map). Multiple indexes can be chained together this 112 | way. 113 | 114 | - `$if(name)$...$endif$` will be replaced by the content 115 | in `...` if the value of the `name` metadata field is 116 | "truthy", otherwise by nothing. `...` may contain 117 | nested templating directives. 118 | 119 | - `$if(name)$...$else$,,,$endif$` will be 120 | replaced by the content in `...` if `name` has a truthy 121 | value, and by the content in `,,,` otherwise. Both 122 | `...` and `,,,` may contain nested templating directives. 123 | 124 | - `$for(name)$...$endfor$` is a loop, producing 125 | successive concatenated copies of `...`. If the value 126 | of `name` is a non-empty table, then in each occurrence 127 | of `...`, the value of `name` will be replaced by a 128 | different element from the table (in order). For example, 129 | `$for(authors)$$authors$$endfor$` will concatenate 130 | all the values of the `authors` table. 131 | 132 | Otherwise, if the value of `name` isn't a table, the loop 133 | behaves like an `if`. 134 | 135 | - `$for(name)$...$sep$,,,$endfor$` behaves like the above, 136 | except that the content in `,,,` is inserted between each 137 | copy of `...`. `,,,` supports nested templating directives. 138 | 139 | Additionally, if newlines occurs directly after **both** `$for()$` and 140 | `$endfor$` (or `$if()$` and `$endif$`), they will be ignored. This is to 141 | prevent spurious blank lines in the rendered document if the template contains 142 | many directives that span multiple lines and evaluate to false. 143 | 144 | Some sample templates are provided in 145 | [`templates/`](https://github.com/jgm/lcmark/tree/master/templates). 146 | 147 | 148 | # Documentation 149 | 150 | ## Program 151 | 152 | `lcmark --help` will print a short list of options. 153 | 154 | For a more detailed description, see the [`lcmark(1)` man page](lcmark.1.md). 155 | 156 | ## Module 157 | 158 | Basic usage: 159 | 160 | ```lua 161 | local lcmark = require("lcmark") 162 | local body, metadata = lcmark.convert("Hello *world*", 163 | "latex", {smart = true, columns = 40}) 164 | ``` 165 | 166 | The module exports the following fields: 167 | 168 | - `lcmark.version`: a string with the version number. 169 | 170 | - `lcmark.yaml_parser_name`: a string holding the name of the 171 | automatically-loaded module and function used to parse YAML. 172 | Possible values are: 173 | - `"lyaml.load"` (lyaml) 174 | - `"yaml.load"` (yaml) 175 | - `"yaml.eval"` (lua-yaml) 176 | - `nil` (none) 177 | 178 | - `lcmark.convert(str, to, options)`: 179 | Converts `str` (a CommonMark formatted string) to the output 180 | format specified by `to` (a string; one of `html`, `commonmark`, 181 | `latex`, `man`, or `xml`). `options` is a table with the 182 | following fields (all optional): 183 | 184 | - `smart` - enable "smart punctuation" 185 | - `hardbreaks` - treat newlines as hard breaks 186 | - `safe` - filter out potentially unsafe HTML and links 187 | - `sourcepos` - include source position in HTML and XML output 188 | - `filters` - an array of filters to run (see `load_filter` below) 189 | - `columns` - column width, or 0 to preserve wrapping in input 190 | - `yaml_metadata` - whether to parse initial YAML metadata block 191 | - `yaml_parser` - a function to parse YAML with (see 192 | [YAML Metadata](#yaml-metadata)) 193 | 194 | Returns `body`, `meta` on success, where `body` is the rendered 195 | document body and `meta` is the YAML metadata as a table. If the 196 | `yaml_metadata` option is false or if the document contains no 197 | YAML metadata, `meta` will be an empty table. In case of an 198 | error, the function returns `nil, nil, msg`. 199 | 200 | - `lcmark.load_filter(filename)`: 201 | Loads a filter from a Lua file (see [Filters](#filters)) 202 | and populates the loaded function's environment with all the 203 | fields from [`cmark-lua`](https://github.com/jgm/cmark-lua). 204 | Returns the filter function on success, or `nil, msg` on failure. 205 | 206 | - `lcmark.compile_template(str)`: 207 | Compiles a template string `str` (see [Templates](#templates)) 208 | into an arbitrary template object which can then be passed to 209 | `lcmark.apply_template()`. Returns the template object on 210 | success, or `nil, msg` on failure. 211 | 212 | - `lcmark.apply_template(template, context)`: 213 | Renders a compiled template object into a string using a 214 | context table (typically the document's metadata). 215 | 216 | - `lcmark.render_template(str, context)`: 217 | Compiles and applies a template string to a context table. 218 | Returns the resulting document string on success, or 219 | `nil, msg` on failure. 220 | 221 | - `lcmark.writers`: a table with strings as keys (`html`, `latex`, 222 | `man`, `xml`, `commonmark`) and renderers as values. A renderer 223 | is a function that takes three arguments (a cmark node, cmark 224 | options (a number), and a column width (a number). It returns 225 | the rendered output as a string. 226 | 227 | 228 | # Development 229 | 230 | `make` builds the rock and installs it locally. 231 | 232 | `make test` runs some tests. These are in `test.t` and `tests/`. 233 | You'll need the [`prove`](https://perldoc.perl.org/prove) executable, 234 | as well as [luacheck](https://github.com/mpeterv/luacheck) and 235 | [lua-TestMore](https://fperrad.frama.io/lua-TestMore/). 236 | 237 | `make update` will update the spec tests from the 238 | `../cmark` directory. 239 | 240 | `make -C standalone` will create a fully self-contained version 241 | of `lcmark` which embeds the lua interpreter and all required 242 | libraries, resulting in no external dependencies. 243 | -------------------------------------------------------------------------------- /bin/lcmark: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env lua 2 | 3 | -- package.path='./?.lua;' .. package.path -- UNCOMMENT FOR TESTING 4 | local optparse = require("optparse") 5 | local lcmark = require("lcmark") 6 | 7 | local function err(msg, exit_code) 8 | io.stderr:write("lcmark: " .. msg .. "\n") 9 | os.exit(exit_code or 1) 10 | end 11 | 12 | local function ensure_one_of(optval,s,ary) 13 | for i=1,#ary do 14 | if ary[i]==s then return true end 15 | end 16 | err("Illegal value for " .. optval .. 17 | "\nLegal values are: " .. table.concat(ary,", ")) 18 | end 19 | 20 | --- Find a template and return its contents (or `false` if 21 | -- not found). The template is sought first in the 22 | -- working directory, then in `templates`, then in 23 | -- `$HOME/.lunamark/templates`, then in the Windows 24 | -- `APPDATA` directory. 25 | local find_template = function(name, to) 26 | if not name then 27 | return false, "Missing template name" 28 | end 29 | local base, ext = name:match("([^%.]*)(.*)") 30 | if (not ext or ext == "") and to then 31 | ext = "." .. to 32 | end 33 | local fname = base .. ext 34 | local file = io.open(fname, "r") 35 | if not file then 36 | file = io.open("templates/" .. fname, "r") 37 | end 38 | if not file then 39 | local home = os.getenv("HOME") 40 | if home then 41 | file = io.open(home .. "/.lcmark/templates/" .. fname, "r") 42 | end 43 | end 44 | if not file then 45 | local appdata = os.getenv("APPDATA") 46 | if appdata then 47 | file = io.open(appdata .. "/lcmark/templates/" .. fname, "r") 48 | end 49 | end 50 | if file then 51 | return file:read("*all") 52 | else 53 | return false, "Could not find template '" .. fname .. "'" 54 | end 55 | end 56 | 57 | local spec = [[ 58 | lcmark ]] .. lcmark.version .. [[ 59 | 60 | https://github.com/jgm/lcmark 61 | 62 | Usage: lcmark [options] [file..] 63 | 64 | Convert markdown to other formats. 65 | 66 | Options: 67 | 68 | -t, --to=FORMAT Target format 69 | -o, --output=FILE Output file 70 | -F, --filter=FILE Lua script to filter AST 71 | -T, --template=FILE Insert output into template 72 | -c, --columns=NUMBER Number of columns to wrap text (or 0 for no wrap) 73 | -S, --smart Smart punctuation 74 | -u, --unsafe Suppress HTML and potentially unsafe attributes 75 | -h, --hardbreaks Treat softbreaks as hard line breaks 76 | -p, --sourcepos Include source position information 77 | -V, --version Version information 78 | -h, --help This message 79 | 80 | FORMAT can be html, latex, man, commonmark, or xml. 81 | ]] 82 | 83 | local optparser = optparse(spec) 84 | 85 | local opts 86 | _G.arg, opts = optparser:parse(_G.arg) 87 | 88 | local to = opts.to or "html" 89 | ensure_one_of("--to,-t", to, 90 | {"commonmark","html","latex","man","xml"}) 91 | 92 | local output = opts.output 93 | local ok, msg 94 | ok, msg = pcall(function() io.output(output) end) 95 | if not ok then 96 | err("Could not open '" .. output .. "' for writing.\n" .. msg, 9) 97 | end 98 | 99 | local options = { 100 | smart = opts.smart, 101 | hardbreaks = opts.hardbreaks, 102 | safe = not opts.unsafe, 103 | sourcepos = opts.sourcepos, 104 | columns = opts.columns or 0, 105 | yaml_metadata = true, 106 | filters = {} 107 | } 108 | 109 | if opts.filter then 110 | for filter in string.gmatch(opts.filter, "([^,]+)") do 111 | local f 112 | f, msg = lcmark.load_filter(filter) 113 | if f then 114 | table.insert(options.filters, f) 115 | else 116 | err("Error loading filter " .. filter .. "\n" .. msg) 117 | end 118 | end 119 | end 120 | 121 | local inp 122 | if #arg == 0 then 123 | inp = io.read("*all") 124 | else 125 | local inpt = {} 126 | for _,f in ipairs(arg) do 127 | ok, msg = pcall(function() io.input(f) end) 128 | if ok then 129 | table.insert(inpt, io.read("*all")) 130 | else 131 | err("Could not open file '" .. f .. "': " .. msg, 7) 132 | end 133 | end 134 | inp = table.concat(inpt, "\n") 135 | end 136 | 137 | local body, data 138 | body, data, msg = lcmark.convert(inp, to, options) 139 | if body then 140 | local template = nil 141 | if opts.template then 142 | template = find_template(opts.template, to) 143 | if not template then 144 | err("Could not find template " .. opts.template .. " for output format " .. 145 | to) 146 | end 147 | end 148 | if template then 149 | data.body = body 150 | local rendered 151 | rendered, msg = lcmark.render_template(template, data) 152 | if rendered then 153 | io.write(rendered) 154 | else 155 | err("Could not render template " .. opts.template .. ":\n" .. msg) 156 | end 157 | else 158 | io.write(body) 159 | end 160 | else 161 | err(msg) 162 | end 163 | 164 | -------------------------------------------------------------------------------- /filters/count_links.lua: -------------------------------------------------------------------------------- 1 | local b = require('cmark.builder') 2 | 3 | -- This is a sample filter using the cmark API in lua. 4 | -- It adds a parenthetical message after each link, 5 | -- numbering the link. 6 | -- It also adds a paragraph to the end of the document that 7 | -- states how many links the document contains, and a 8 | -- metadata field number_of_links. 9 | 10 | -- A filter is a lua program that returns a function 11 | -- whose arguments are a cmark node, a metadata tree 12 | -- of cmark nodes, and a string giving the target format. 13 | return function(doc, meta, format) 14 | local cur, node_type, entering 15 | local links = 0 16 | 17 | -- cmark-lua has a built-in iterator to walk over 18 | -- all the node of the document. 19 | for cur, entering, node_type in walk(doc) do 20 | -- Increment links if we're entering a link node: 21 | if node_type == NODE_LINK and not entering then 22 | links = links + 1 23 | -- insert " (link #n)" after the link: 24 | local t = b.text(string.format(" (link #%d)", links)) 25 | node_insert_after(cur, t) 26 | end 27 | end 28 | 29 | -- Now we need to add a paragraph at the end of the 30 | -- document with a message about the number of links 31 | -- found. We'll need to create a paragraph node, 32 | -- and a text node to go in it, and we'll add the 33 | -- text as the literal content of the text node. 34 | node_append_child(doc, b.paragraph{ 35 | b.text(string.format("%d links found in this %s document.", 36 | links, format))}) 37 | 38 | -- For good measure, let's add a number_of_links metadata 39 | -- field: 40 | meta.number_of_links = b.text(links) 41 | 42 | end 43 | 44 | -------------------------------------------------------------------------------- /filters/highlight.lua: -------------------------------------------------------------------------------- 1 | -- highlights code using pygmentize 2 | -- assumes first word of info string is the syntax 3 | -- styles needed for the output format are put in 4 | -- the metadata field highlighting_styles 5 | 6 | local b = require('cmark.builder') 7 | 8 | local highlight = function(code, syntax, format) 9 | local tmpname = os.tmpname() 10 | io.output(tmpname) 11 | io.write(code) 12 | io.close() 13 | io.output(io.stdout) 14 | local handle = io.popen('pygmentize -l ' .. syntax .. ' -f ' .. format .. ' ' .. tmpname, 'r') 15 | local result = handle:read("*a") 16 | io.close(handle) 17 | os.remove(tmpname) 18 | return result 19 | end 20 | 21 | local style_defs = function(syntax, format) 22 | local handle = io.popen('pygmentize -S default -f ' .. format, 'r') 23 | local result = handle:read("*a") 24 | io.close(handle) 25 | return result 26 | end 27 | 28 | return function(doc, meta, to) 29 | for cur, entering, node_type in walk(doc) do 30 | if entering and node_type == NODE_CODE_BLOCK then 31 | local syntax = string.match(node_get_fence_info(cur), '^([^=%s]+)') 32 | if syntax then 33 | local contents = node_get_literal(cur) 34 | local highlighted = highlight(contents, syntax, to) 35 | 36 | if not meta.highlighting_styles then 37 | meta.highlighting_styles = b.custom_block{ 38 | on_enter = style_defs(syntax, to) 39 | } 40 | end 41 | 42 | -- now run this through pygmentize and insert a raw_block with output 43 | node_replace(cur, b.custom_block{ on_enter = highlighted }) 44 | node_free(cur) 45 | end 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /filters/include.lua: -------------------------------------------------------------------------------- 1 | -- filter to include files in code blocks 2 | -- fenced code block marked with include="path-to-file" 3 | -- will have its contents replaced with those of "path-to-file" 4 | 5 | return function(doc, meta, to) 6 | for cur, entering, node_type in walk(doc) do 7 | if entering and node_type == NODE_CODE_BLOCK then 8 | local includefile = string.match(node_get_fence_info(cur), 9 | 'include="([^"]*)"') 10 | if includefile then 11 | local f = io.open(includefile, 'r') 12 | assert(f, "Could not open file " .. includefile) 13 | local contents = f:read("*all") 14 | -- set contents of code block: 15 | node_set_literal(cur, contents) 16 | end 17 | end 18 | end 19 | end 20 | 21 | -------------------------------------------------------------------------------- /lcmark.1: -------------------------------------------------------------------------------- 1 | .TH "lcmark" "1" "January 1, 2016" "" "" 2 | .SH 3 | NAME 4 | .PP 5 | lcmark \- flexible command line CommonMark parser and renderer 6 | .SH 7 | SYNOPSIS 8 | .PP 9 | lcmark [options] [file..] 10 | .SH 11 | DESCRIPTION 12 | .PP 13 | \f[C]lcmark\f[] does what the \f[C]cmark\f[] (https://github.com/jgm/cmark) 14 | program does, with the following enhancements: 15 | .IP \[bu] 2 16 | Support for \f[B]YAML metadata\f[] at the top of the document. 17 | .IP \[bu] 2 18 | Support for \f[B]filters\f[], which allow the document to be transformed between 19 | parsing and rendering, making a large number of customizations possible. 20 | .IP \[bu] 2 21 | Support for \f[B]templates\f[], which allow the body and metadata values to be 22 | embedded into a pre\-defined structure. 23 | .SH 24 | OPTIONS 25 | .PP 26 | \f[C]\-\-to,\-t\f[] \f[I]format\f[] 27 | .PP 28 | Specify format for output. 29 | \f[I]format\f[] can be \f[C]commonmark\f[], \f[C]html\f[], \f[C]man\f[], \f[C]xml\f[], or \f[C]latex\f[]. 30 | .PP 31 | \f[C]\-\-output,\-o\f[] \f[I]file\f[] 32 | .PP 33 | Write output to \f[I]file\f[]. 34 | .PP 35 | \f[C]\-\-columns,\-c\f[] \f[I]NUMBER\f[] 36 | .PP 37 | Specify number of columns for text wrapping in supported 38 | formats. The default is 0 = no wrapping. 39 | .PP 40 | \f[C]\-\-filter,\-F\f[] \f[I]file[,file]\f[] 41 | .PP 42 | Filter the parsed AST using a Lua script. See FILTERS 43 | below for details. 44 | .PP 45 | \f[C]\-\-template,\-T\f[] \f[I]file\f[] 46 | .PP 47 | Insert converted text and metadata into a template. See TEMPLATES, 48 | below, for template format. 49 | .PP 50 | \f[C]\-\-smart\f[] 51 | .PP 52 | Enable smart typography (straight quotes are turned into 53 | curly quotes, \f[C]\-\-\f[] into en dashes, \f[C]\-\-\-\f[] into em dashes, 54 | \f[C]...\f[] into ellipses). 55 | .PP 56 | \f[C]\-\-safe\f[] 57 | .PP 58 | Suppress raw HTML and unsafe links (\f[C]javascript:\f[], \f[C]file:\f[], 59 | \f[C]vbscript:\f[], and \f[C]data:\f[], except for \f[C]image/png\f[], \f[C]image/gif\f[], 60 | \f[C]image/jpeg\f[], or \f[C]image/webp\f[] mime types). 61 | .PP 62 | \f[C]\-\-hardbreaks\f[] 63 | .PP 64 | Render softbreak elements (newlines in paragraphs) as hard 65 | line breaks. 66 | .PP 67 | \f[C]\-\-sourcepos\f[] 68 | .PP 69 | Include source position information in html/xml attributes. 70 | .PP 71 | \f[C]\-\-version,\-V\f[] 72 | .PP 73 | Print version information. 74 | .PP 75 | \f[C]\-\-help,\-h\f[] 76 | .PP 77 | This message 78 | .SH 79 | METADATA 80 | .PP 81 | The YAML metadata section (if present) must occur at the beginning of the 82 | document. It begins with a line containing \f[C]\-\-\-\f[] and ends with a line 83 | containing \f[C]...\f[] or \f[C]\-\-\-\f[]. Between these, a YAML key/value map is expected. 84 | .PP 85 | String values found in the metadata will be parsed and rendered as 86 | CommonMark. If a string value contains only a single paragraph, it will be 87 | rendered as an inline string. 88 | .SH 89 | TEMPLATES 90 | .PP 91 | By default, \f[C]lcmark\f[] will produce a fragment. If the \f[C]\-\-template\f[] 92 | option is specified, it will insert this fragment into a 93 | template, producing a standalone document with appropriate 94 | header and footer. The template is sought first in the working 95 | directory, then in \f[C]templates\f[], and finally in 96 | \f[C]$HOME/lcmark/templates\f[]. If no extension is given, the name of 97 | the writer will be used as an extension. So, for example, one 98 | can put the template \f[C]letter.html\f[] in the 99 | \f[C]$HOME/lcmark/templates\f[] directory, and use it anywhere with 100 | \f[C]lcmark \-\-template letter\f[]. 101 | .PP 102 | Variables are taken from YAML metadata; string fields are interpreted 103 | as CommonMark and rendered appropriately for the output format. 104 | The following additional variables are set automatically: 105 | .IP \[bu] 2 106 | \f[C]body\f[]: the document body 107 | .PP 108 | \f[C]lcmark\f[] supports a small subset of the templating language used by 109 | pandoc (http://pandoc.org), and \f[C]lcmark\f[] templates can be used with pandoc 110 | (with the caveat that pandoc sets many variables automatically that \f[C]lcmark\f[] 111 | does not). 112 | .PP 113 | \f[C]nil\f[], \f[C]false\f[] and empty tables are considered to be "falsy" values. 114 | Any other value is considered to be "truthy". 115 | .PP 116 | A quick guide: 117 | .IP \[bu] 2 118 | The only special character in templates is \f[C]$\f[]. To get 119 | a literal \f[C]$\f[] character, use \f[C]$$\f[]. 120 | .IP \[bu] 2 121 | \f[C]$name$\f[] will be replaced with the value of the \f[C]name\f[] 122 | metadata field. Variable names can contain alphanumerics, 123 | \f[C]\-\f[], and \f[C]_\f[]. 124 | .IP \[bu] 2 125 | \f[C]$name.subname$\f[] will be replaced with the value of the 126 | \f[C]subname\f[] field of the \f[C]name\f[] metadata field (assumed to 127 | be a map). More indexes can be changed together this way. 128 | .IP \[bu] 2 129 | \f[C]$if(name)$...$endif$\f[] will be replaced by the content 130 | in \f[C]...\f[] if the value of the \f[C]name\f[] metadata field is 131 | "truthy", otherwise by nothing. \f[C]...\f[] may contain 132 | nested templating directives. 133 | .IP \[bu] 2 134 | \f[C]$if(name)$...$else$,,,$endif$\f[] will be 135 | replaced by the content in \f[C]...\f[] if \f[C]name\f[] has a truthy 136 | value, and by the content in \f[C],,,\f[] otherwise. Both 137 | \f[C]...\f[] and \f[C],,,\f[] may contain nested templating directives. 138 | .IP \[bu] 2 139 | \f[C]$for(name)$...$endfor$\f[] is a loop, producing 140 | successive concatenated copies of \f[C]...\f[]. If the value 141 | of \f[C]name\f[] is a non\-empty table, then in each occurrence 142 | of \f[C]...\f[], the value of \f[C]name\f[] will be replaced by a 143 | different element from the table (in order). For example, 144 | \f[C]$for(authors)$$authors$$endfor$\f[] will concatenate 145 | all the values of the \f[C]authors\f[] table. 146 | .PP 147 | Otherwise, if the value of \f[C]name\f[] isn't a table, the loop 148 | behaves like an \f[C]if\f[]. 149 | .IP \[bu] 2 150 | \f[C]$for(name)$...$sep$,,,$endfor$\f[] behaves like the above, 151 | except that the content in \f[C],,,\f[] is inserted between each 152 | copy of \f[C]...\f[]. \f[C],,,\f[] supports nested templating directives. 153 | .PP 154 | Additionally, if newlines occurs directly after \f[B]both\f[] \f[C]$for()$\f[] and 155 | \f[C]$endfor$\f[] (or \f[C]$if()$\f[] and \f[C]$endif$\f[]), they will be ignored. This is to 156 | prevent spurious blank lines in the rendered document if the template contains 157 | many directives that span multiple lines and evaluate to false. 158 | .PP 159 | For examples, see the \f[C]templates/\f[] directory in the source 160 | repository. 161 | .SH 162 | FILTERS 163 | .PP 164 | Filters modify the parsed document prior to rendering. 165 | .PP 166 | A filter is a function that takes three arguments (\f[C]doc\f[], \f[C]meta\f[], \f[C]to\f[]), where 167 | \f[C]doc\f[] is a cmark node, \f[C]meta\f[] is the YAML metadata as a (potentially nested) Lua 168 | table with all strings replaced with cmark nodes, and \f[C]to\f[] is a string 169 | specifying the output format. The filter may destructively modify \f[C]doc\f[] and 170 | \f[C]meta\f[]. 171 | .PP 172 | When loading filters, \f[C]lcmark\f[] automatically populates the filter function's 173 | environment with the functions and values provided by 174 | \f[C]cmark\-lua\f[] (https://github.com/jgm/cmark\-lua) so that any \f[C]cmark\f[] functions do 175 | not have to be qualified with \f[C]cmark.\f[]. 176 | .PP 177 | For examples, see the \f[C]filters/\f[] directory in the source 178 | repository. 179 | .PP 180 | The arguments to \f[C]\-\-filter\f[] should be Lua scripts that \f[C]return\f[] 181 | a filter function, as defined above. Filters will be run in the 182 | order listed. Filters are applied to the root document node, 183 | not to metadata (although a filter can operate on metadata if 184 | desired). 185 | .SH 186 | EXAMPLES 187 | .IP 188 | .nf 189 | \f[C] 190 | lcmark 191 | \f[] 192 | .fi 193 | .PP 194 | acts as a filter, reading markdown from stdin and writing 195 | HTML to stdout. 196 | .IP 197 | .nf 198 | \f[C] 199 | lcmark \-\-smart \-t latex 200 | \f[] 201 | .fi 202 | .PP 203 | acts as a filter, reading markdown with smart typography 204 | and definition list extensions from stdin, and writing 205 | LaTeX to stdout. 206 | .IP 207 | .nf 208 | \f[C] 209 | lcmark \-t latex \-o mybook.tex ch{1,2,3}.txt references.txt 210 | \f[] 211 | .fi 212 | .PP 213 | reads \f[C]ch1.txt\f[], \f[C]ch2.txt\f[], \f[C]ch3.txt\f[], and \f[C]references.txt\f[], 214 | concatenates them, and converts the result from markdown to LaTeX. 215 | .IP 216 | .nf 217 | \f[C] 218 | lcmark \-\-template letter \-t latex \-o myletter.tex myletter.txt 219 | \f[] 220 | .fi 221 | .PP 222 | produces a LaTeX file using the template \f[C]letter.latex\f[]. 223 | 224 | .SH AUTHORS 225 | John MacFarlane 226 | -------------------------------------------------------------------------------- /lcmark.1.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: lcmark 3 | section: 1 4 | date: January 1, 2016 5 | author: 6 | - John MacFarlane 7 | ... 8 | 9 | # NAME 10 | 11 | lcmark - flexible command line CommonMark parser and renderer 12 | 13 | # SYNOPSIS 14 | 15 | lcmark [options] [file..] 16 | 17 | # DESCRIPTION 18 | 19 | `lcmark` does what the [`cmark`](https://github.com/jgm/cmark) 20 | program does, with the following enhancements: 21 | 22 | - Support for **YAML metadata** at the top of the document. 23 | 24 | - Support for **filters**, which allow the document to be transformed between 25 | parsing and rendering, making a large number of customizations possible. 26 | 27 | - Support for **templates**, which allow the body and metadata values to be 28 | embedded into a pre-defined structure. 29 | 30 | # OPTIONS 31 | 32 | `--to,-t` *format* 33 | 34 | Specify format for output. 35 | *format* can be `commonmark`, `html`, `man`, `xml`, or `latex`. 36 | 37 | `--output,-o` *file* 38 | 39 | Write output to *file*. 40 | 41 | `--columns,-c` *NUMBER* 42 | 43 | Specify number of columns for text wrapping in supported 44 | formats. The default is 0 = no wrapping. 45 | 46 | `--filter,-F` *file[,file]* 47 | 48 | Filter the parsed AST using a Lua script. See FILTERS 49 | below for details. 50 | 51 | `--template,-T` *file* 52 | 53 | Insert converted text and metadata into a template. See TEMPLATES, 54 | below, for template format. 55 | 56 | `--smart` 57 | 58 | Enable smart typography (straight quotes are turned into 59 | curly quotes, `--` into en dashes, `---` into em dashes, 60 | `...` into ellipses). 61 | 62 | `--safe` 63 | 64 | Suppress raw HTML and unsafe links (`javascript:`, `file:`, 65 | `vbscript:`, and `data:`, except for `image/png`, `image/gif`, 66 | `image/jpeg`, or `image/webp` mime types). 67 | 68 | `--hardbreaks` 69 | 70 | Render softbreak elements (newlines in paragraphs) as hard 71 | line breaks. 72 | 73 | `--sourcepos` 74 | 75 | Include source position information in html/xml attributes. 76 | 77 | `--version,-V` 78 | 79 | Print version information. 80 | 81 | `--help,-h` 82 | 83 | This message 84 | 85 | # METADATA 86 | 87 | The YAML metadata section (if present) must occur at the beginning of the 88 | document. It begins with a line containing `---` and ends with a line 89 | containing `...` or `---`. Between these, a YAML key/value map is expected. 90 | 91 | String values found in the metadata will be parsed and rendered as 92 | CommonMark. If a string value contains only a single paragraph, it will be 93 | rendered as an inline string. 94 | 95 | # TEMPLATES 96 | 97 | By default, `lcmark` will produce a fragment. If the `--template` 98 | option is specified, it will insert this fragment into a 99 | template, producing a standalone document with appropriate 100 | header and footer. The template is sought first in the working 101 | directory, then in `templates`, and finally in 102 | `$HOME/lcmark/templates`. If no extension is given, the name of 103 | the writer will be used as an extension. So, for example, one 104 | can put the template `letter.html` in the 105 | `$HOME/lcmark/templates` directory, and use it anywhere with 106 | `lcmark --template letter`. 107 | 108 | Variables are taken from YAML metadata; string fields are interpreted 109 | as CommonMark and rendered appropriately for the output format. 110 | The following additional variables are set automatically: 111 | 112 | * `body`: the document body 113 | 114 | `lcmark` supports a small subset of the templating language used by 115 | [pandoc](http://pandoc.org), and `lcmark` templates can be used with pandoc 116 | (with the caveat that pandoc sets many variables automatically that `lcmark` 117 | does not). 118 | 119 | `nil`, `false` and empty tables are considered to be "falsy" values. 120 | Any other value is considered to be "truthy". 121 | 122 | A quick guide: 123 | 124 | - The only special character in templates is `$`. To get 125 | a literal `$` character, use `$$`. 126 | 127 | - `$name$` will be replaced with the value of the `name` 128 | metadata field. Variable names can contain alphanumerics, 129 | `-`, and `_`. 130 | 131 | - `$name.subname$` will be replaced with the value of the 132 | `subname` field of the `name` metadata field (assumed to 133 | be a map). More indexes can be changed together this way. 134 | 135 | - `$if(name)$...$endif$` will be replaced by the content 136 | in `...` if the value of the `name` metadata field is 137 | "truthy", otherwise by nothing. `...` may contain 138 | nested templating directives. 139 | 140 | - `$if(name)$...$else$,,,$endif$` will be 141 | replaced by the content in `...` if `name` has a truthy 142 | value, and by the content in `,,,` otherwise. Both 143 | `...` and `,,,` may contain nested templating directives. 144 | 145 | - `$for(name)$...$endfor$` is a loop, producing 146 | successive concatenated copies of `...`. If the value 147 | of `name` is a non-empty table, then in each occurrence 148 | of `...`, the value of `name` will be replaced by a 149 | different element from the table (in order). For example, 150 | `$for(authors)$$authors$$endfor$` will concatenate 151 | all the values of the `authors` table. 152 | 153 | Otherwise, if the value of `name` isn't a table, the loop 154 | behaves like an `if`. 155 | 156 | - `$for(name)$...$sep$,,,$endfor$` behaves like the above, 157 | except that the content in `,,,` is inserted between each 158 | copy of `...`. `,,,` supports nested templating directives. 159 | 160 | Additionally, if newlines occurs directly after **both** `$for()$` and 161 | `$endfor$` (or `$if()$` and `$endif$`), they will be ignored. This is to 162 | prevent spurious blank lines in the rendered document if the template contains 163 | many directives that span multiple lines and evaluate to false. 164 | 165 | For examples, see the `templates/` directory in the source 166 | repository. 167 | 168 | # FILTERS 169 | 170 | Filters modify the parsed document prior to rendering. 171 | 172 | A filter is a function that takes three arguments (`doc`, `meta`, `to`), where 173 | `doc` is a cmark node, `meta` is the YAML metadata as a (potentially nested) Lua 174 | table with all strings replaced with cmark nodes, and `to` is a string 175 | specifying the output format. The filter may destructively modify `doc` and 176 | `meta`. 177 | 178 | When loading filters, `lcmark` automatically populates the filter function's 179 | environment with the functions and values provided by 180 | [`cmark-lua`](https://github.com/jgm/cmark-lua) so that any `cmark` functions do 181 | not have to be qualified with `cmark.`. 182 | 183 | For examples, see the `filters/` directory in the source 184 | repository. 185 | 186 | The arguments to `--filter` should be Lua scripts that `return` 187 | a filter function, as defined above. Filters will be run in the 188 | order listed. Filters are applied to the root document node, 189 | not to metadata (although a filter can operate on metadata if 190 | desired). 191 | 192 | # EXAMPLES 193 | 194 | lcmark 195 | 196 | acts as a filter, reading markdown from stdin and writing 197 | HTML to stdout. 198 | 199 | lcmark --smart -t latex 200 | 201 | acts as a filter, reading markdown with smart typography 202 | and definition list extensions from stdin, and writing 203 | LaTeX to stdout. 204 | 205 | lcmark -t latex -o mybook.tex ch{1,2,3}.txt references.txt 206 | 207 | reads `ch1.txt`, `ch2.txt`, `ch3.txt`, and `references.txt`, 208 | concatenates them, and converts the result from markdown to LaTeX. 209 | 210 | lcmark --template letter -t latex -o myletter.tex myletter.txt 211 | 212 | produces a LaTeX file using the template `letter.latex`. 213 | 214 | -------------------------------------------------------------------------------- /lcmark.lua: -------------------------------------------------------------------------------- 1 | local cmark = require("cmark") 2 | local lpeg = require("lpeg") 3 | 4 | local S, C, P, R, V, Ct = 5 | lpeg.S, lpeg.C, lpeg.P, lpeg.R, lpeg.V, lpeg.Ct 6 | local nl = P"\r\n" + P"\r" + P"\n" 7 | local sp = S" \t"^0 8 | 9 | local lcmark = {} 10 | 11 | lcmark.version = "0.31.1" 12 | 13 | lcmark.writers = { 14 | html = function(d, opts, _) return cmark.render_html(d, opts) end, 15 | man = cmark.render_man, 16 | xml = function(d, opts, _) return cmark.render_xml(d, opts) end, 17 | latex = cmark.render_latex, 18 | commonmark = cmark.render_commonmark 19 | } 20 | 21 | local default_yaml_parser = nil 22 | 23 | local function try_load(module_name, func_name) 24 | if default_yaml_parser then -- already loaded; skip 25 | return 26 | end 27 | 28 | local success, loaded = pcall(require, module_name) 29 | 30 | if not success then 31 | return 32 | end 33 | if type(loaded) ~= "table" or type(loaded[func_name]) ~= "function" then 34 | return 35 | end 36 | 37 | default_yaml_parser = loaded[func_name] 38 | lcmark.yaml_parser_name = module_name .. "." .. func_name 39 | end 40 | 41 | try_load("lyaml", "load") 42 | try_load("yaml", "load") -- must come before yaml.eval 43 | try_load("yaml", "eval") 44 | 45 | -- the reason yaml.load must come before yaml.eval is that the 'yaml' library 46 | -- prints error messages if you try to index non-existent fields such as 'eval' 47 | 48 | 49 | local toOptions = function(opts) 50 | if type(opts) == 'table' then 51 | return (cmark.OPT_VALIDATE_UTF8 + cmark.OPT_NORMALIZE + 52 | (opts.smart and cmark.OPT_SMART or 0) + 53 | (opts.safe and 0 or cmark.OPT_UNSAFE) + 54 | (opts.hardbreaks and cmark.OPT_HARDBREAKS or 0) + 55 | (opts.sourcepos and cmark.OPT_SOURCEPOS or 0) 56 | ) 57 | else 58 | return opts 59 | end 60 | end 61 | 62 | -- walk nodes of table, applying a callback to each 63 | local function walk_table(table, callback, inplace) 64 | assert(type(table) == 'table') 65 | local new = {} 66 | local res 67 | for k, v in pairs(table) do 68 | if type(v) == 'table' then 69 | res = walk_table(v, callback, inplace) 70 | else 71 | res = callback(v) 72 | end 73 | if not inplace then 74 | new[k] = res 75 | end 76 | end 77 | if not inplace then 78 | return new 79 | end 80 | end 81 | 82 | -- We inject cmark into environment where filters are 83 | -- run, so users don't need to qualify each function with 'cmark.'. 84 | local defaultEnv = setmetatable({}, { __index = _G }) 85 | for k,v in pairs(cmark) do 86 | defaultEnv[k] = v 87 | end 88 | 89 | local loadfile_with_env 90 | if setfenv then 91 | -- Lua 5.1/LuaJIT 92 | loadfile_with_env = function(filename) 93 | local result, msg = loadfile(filename) 94 | if result then 95 | return setfenv(result, defaultEnv) 96 | else 97 | return result, msg 98 | end 99 | end 100 | else 101 | -- Lua 5.2+ 102 | loadfile_with_env = function(filename) 103 | return loadfile(filename, 't', defaultEnv) 104 | end 105 | end 106 | 107 | -- Loads a filter from a Lua file and populates the loaded function's 108 | -- environment with all the fields from `cmark-lua`. 109 | -- Returns the filter function on success, or `nil, msg` on failure. 110 | function lcmark.load_filter(filename) 111 | local result, msg = loadfile_with_env(filename) 112 | if result then 113 | local evaluated = result() 114 | if type(evaluated) == 'function' then 115 | return evaluated 116 | else 117 | return nil, ("Filter " .. filename .. " returns a " .. 118 | type(evaluated) .. ", not a function") 119 | end 120 | else 121 | return nil, msg 122 | end 123 | end 124 | 125 | -- Render a metadata node in the target format. 126 | local render_metadata = function(node, writer, options, columns) 127 | local firstblock = cmark.node_first_child(node) 128 | if cmark.node_get_type(firstblock) == cmark.NODE_PARAGRAPH and 129 | not cmark.node_next(firstblock) then 130 | -- render as inlines 131 | local ils = cmark.node_new(cmark.NODE_CUSTOM_INLINE) 132 | local b = cmark.node_first_child(firstblock) 133 | while b do 134 | local nextb = cmark.node_next(b) 135 | cmark.node_append_child(ils, b) 136 | b = nextb 137 | end 138 | local result = string.gsub(writer(ils, options, columns), "%s*$", "") 139 | cmark.node_free(ils) 140 | return result 141 | else -- render as blocks 142 | return writer(node, options, columns) 143 | end 144 | end 145 | 146 | -- Iterate over the metadata, converting to cmark nodes. 147 | -- Returns a new table. 148 | local convert_metadata = function(table, options) 149 | return walk_table(table, 150 | function(s) 151 | if type(s) == "string" then 152 | return cmark.parse_string(s, options) 153 | elseif type(s) == "userdata" then 154 | return tostring(s) 155 | else 156 | return s 157 | end 158 | end, false) 159 | end 160 | 161 | local yaml_begin_line = P"---" * sp * nl 162 | local yaml_end_line = (P"---" + P"...") * sp * nl 163 | local yaml_content_line = -yaml_end_line * P(1 - S"\r\n")^0 * nl 164 | local yaml_block = yaml_begin_line * (yaml_content_line^1 + sp) * yaml_end_line 165 | 166 | -- Parses document with optional front YAML metadata; returns document, 167 | -- metadata. 168 | local parse_document_with_metadata = function(inp, parser, options) 169 | local metadata = {} 170 | local meta_end = lpeg.match(yaml_block, inp) 171 | if meta_end then 172 | if meta_end then 173 | local ok, yaml_meta, err = pcall(parser, string.sub(inp, 1, meta_end)) 174 | if not ok then 175 | return nil, yaml_meta -- the error message 176 | elseif not yaml_meta then -- parser may return nil, err instead of error 177 | return nil, tostring(err) 178 | end 179 | if type(yaml_meta) == 'table' then 180 | metadata = convert_metadata(yaml_meta, options) 181 | if type(metadata) ~= 'table' then 182 | metadata = {} 183 | end 184 | -- We insert blank lines where the header was, so sourcepos is accurate: 185 | inp = string.gsub(string.sub(inp, 1, meta_end), '[^\n\r]+', '') .. 186 | string.sub(inp, meta_end) 187 | end 188 | end 189 | end 190 | local doc = cmark.parse_string(inp, options) 191 | return doc, metadata 192 | end 193 | 194 | -- Apply a compiled template to a context (a dictionary-like 195 | -- table). 196 | function lcmark.apply_template(m, ctx) 197 | if type(m) == 'function' then 198 | return m(ctx) 199 | elseif type(m) == 'table' then 200 | local buffer = {} 201 | for i,v in ipairs(m) do 202 | buffer[i] = lcmark.apply_template(v, ctx) 203 | end 204 | return table.concat(buffer) 205 | else 206 | return tostring(m) 207 | end 208 | end 209 | 210 | local get_value = function(var, ctx) 211 | local result = ctx 212 | assert(type(var) == 'table') 213 | for _,varpart in ipairs(var) do 214 | if type(result) ~= 'table' then 215 | return nil 216 | end 217 | result = result[varpart] 218 | if result == nil then 219 | return nil 220 | end 221 | end 222 | return result 223 | end 224 | 225 | local set_value = function(var, newval, ctx) 226 | local result = ctx 227 | assert(type(var) == 'table') 228 | for i,varpart in ipairs(var) do 229 | if i == #var then 230 | -- last one 231 | result[varpart] = newval 232 | else 233 | result = result[varpart] 234 | if result == nil then 235 | return nil 236 | end 237 | end 238 | end 239 | return true 240 | end 241 | 242 | local is_truthy = function(val) 243 | local is_empty_tbl = type(val) == "table" and #val == 0 244 | return val and not is_empty_tbl 245 | end 246 | 247 | -- if s starts with newline, remove initial and final newline 248 | local trim = function(s) 249 | if s:match("^[\r\n]") then 250 | return s:gsub("^[\r]?[\n]?", ""):gsub("[\r]?[\n]?$", "") 251 | else 252 | return s 253 | end 254 | end 255 | 256 | local conditional = function(var, ifpart, elsepart) 257 | return function(ctx) 258 | local result 259 | if is_truthy(get_value(var, ctx)) then 260 | result = lcmark.apply_template(ifpart, ctx) 261 | elseif elsepart then 262 | result = lcmark.apply_template(elsepart, ctx) 263 | else 264 | result = "" 265 | end 266 | return trim(result) 267 | end 268 | end 269 | 270 | local forloop = function(var, inner, sep) 271 | return function(ctx) 272 | local val = get_value(var, ctx) 273 | local vs 274 | if not is_truthy(val) then 275 | return "" 276 | end 277 | if type(val) == 'table' then 278 | vs = val 279 | else 280 | -- if not a table, just iterate once 281 | vs = {val} 282 | end 283 | local buffer = {} 284 | for i,v in ipairs(vs) do 285 | set_value(var, v, ctx) -- set temporary context 286 | buffer[#buffer + 1] = lcmark.apply_template(inner, ctx) 287 | if sep and i < #vs then 288 | buffer[#buffer + 1] = lcmark.apply_template(sep, ctx) 289 | end 290 | set_value(var, val, ctx) -- restore original context 291 | end 292 | local result = lcmark.apply_template(buffer, ctx) 293 | return trim(result) 294 | end 295 | end 296 | 297 | -- Template syntax. 298 | local TemplateGrammar = Ct{"Main", 299 | Main = V"Template" * (-1 + lpeg.Cp()), 300 | Template = Ct((V"Text" + 301 | V"EscapedDollar" + 302 | V"ConditionalNl" + 303 | V"Conditional" + 304 | V"ForLoopNl" + 305 | V"ForLoop" + 306 | V"Var")^0), 307 | EscapedDollar = P"$$" / "$", 308 | -- the Nl forms eat an extra newline after the end, if the 309 | -- opening if() or for() ends with a newline. This is to avoid 310 | -- excess blank space when a document contains many ifs or fors 311 | -- that evaluate to false. 312 | ConditionalNl = P"$if(" * Ct(V"Variable") * P")$" * nl * Ct(V"Template") * 313 | (P"$else$" * Ct(V"Template"))^-1 * P"$endif$" * nl / conditional, 314 | ForLoopNl = P"$for(" * Ct(V"Variable") * P")$" * nl * Ct(V"Template") * 315 | (P"$sep$" * Ct(V"Template"))^-1 * P"$endfor$" * nl / forloop, 316 | Conditional = P"$if(" * Ct(V"Variable") * P")$" * Ct(V"Template") * 317 | (P"$else$" * Ct(V"Template"))^-1 * P"$endif$" / conditional, 318 | ForLoop = P"$for(" * Ct(V"Variable") * P")$" * Ct(V"Template") * 319 | (P"$sep$" * Ct(V"Template"))^-1 * P"$endfor$" / forloop, 320 | Text = C((1 - P"$")^1), 321 | Reserved = P"if$" + P"endif$" + P"else$" + P"for$" + P"endfor$" + P"sep$", 322 | VarPart = (R"az" + R"AZ" + R"09" + S"_-")^1, 323 | Variable = C(V"VarPart") * (P"." * C(V"VarPart"))^0, 324 | Var = P"$" * - V"Reserved" * Ct(V"Variable") * P"$" / 325 | function(var) 326 | return function(ctx) 327 | local val = get_value(var, ctx) 328 | if is_truthy(val) then 329 | return tostring(val) 330 | else 331 | return "" 332 | end 333 | end 334 | end, 335 | } 336 | 337 | -- Compiles a template string into an arbitrary template object 338 | -- which can then be passed to `lcmark.apply_template()`. 339 | -- Returns the template object on success, or `nil, msg` on failure. 340 | lcmark.compile_template = function(tpl) 341 | local matches = lpeg.match(TemplateGrammar, tpl, nil) 342 | if matches[2] == nil then 343 | if matches[1] == nil then 344 | return nil, "parse failed at the end of the template" 345 | else 346 | return matches[1] 347 | end 348 | else 349 | local line_num = 1 350 | local parse_failure_pos = matches[2] 351 | tpl:sub(1,parse_failure_pos):gsub('[^\n]*[\n]', 352 | function() line_num = line_num + 1 end) 353 | return nil, ("parse failure at line " .. line_num .. 354 | ": '" .. string.sub(tpl, parse_failure_pos, 355 | parse_failure_pos + 40) .. "'") 356 | end 357 | end 358 | 359 | -- Compiles and applies a template string to a context table. 360 | -- Returns the resulting document string on success, or 361 | -- `nil, msg` on failure. 362 | function lcmark.render_template(tpl, ctx) 363 | local compiled_template, msg = lcmark.compile_template(tpl) 364 | if not compiled_template then 365 | return nil, msg 366 | end 367 | return lcmark.apply_template(compiled_template, ctx) 368 | end 369 | 370 | -- Converts `inp` (a CommonMark formatted string) to the output 371 | -- format specified by `to` (a string; one of `html`, `commonmark`, 372 | -- `latex`, `man`, or `xml`). `options` is a table with the 373 | -- following fields (all optional): 374 | -- * `smart` - enable "smart punctuation" 375 | -- * `hardbreaks` - treat newlines as hard breaks 376 | -- * `safe` - filter out potentially unsafe HTML and links 377 | -- * `sourcepos` - include source position in HTML and XML output 378 | -- * `filters` - an array of filters to run (see `load_filter` above) 379 | -- * `columns` - column width, or 0 to preserve wrapping in input 380 | -- * `yaml_metadata` - whether to parse initial YAML metadata block 381 | -- * `yaml_parser` - a function to parse YAML with (see 382 | -- [YAML Metadata](#yaml-metadata)) 383 | -- Returns `body`, `meta` on success, where `body` is the rendered 384 | -- document body and `meta` is the YAML metadata as a table. If the 385 | -- `yaml_metadata` option is false or if the document contains no 386 | -- YAML metadata, `meta` will be an empty table. In case of an 387 | -- error, the function returns `nil, nil, msg`. 388 | function lcmark.convert(inp, to, options) 389 | local writer = lcmark.writers[to] 390 | if not writer then 391 | return nil, nil, ("Unknown output format " .. tostring(to)) 392 | end 393 | local opts, columns, filters, yaml_metadata, yaml_parser 394 | if options then 395 | opts = toOptions(options) 396 | columns = options.columns or 0 397 | filters = options.filters or {} 398 | yaml_metadata = options.yaml_metadata 399 | yaml_parser = options.yaml_parser or default_yaml_parser 400 | else 401 | opts = cmark.OPT_DEFAULT 402 | columns = 0 403 | filters = {} 404 | yaml_metadata = false 405 | yaml_parser = default_yaml_parser 406 | end 407 | if not yaml_parser then 408 | error("no YAML libraries were found and no yaml_parser was specified") 409 | end 410 | local doc, meta 411 | if yaml_metadata then 412 | doc, meta = parse_document_with_metadata(inp, yaml_parser, opts) 413 | if not doc then 414 | return nil, nil, ("YAML parsing error: " .. meta) 415 | end 416 | else 417 | doc = cmark.parse_string(inp, opts) 418 | meta = {} 419 | end 420 | if not doc then 421 | return nil, nil, "Unable to parse document" 422 | end 423 | for _, f in ipairs(filters) do 424 | -- do we want filters to apply automatically to metadata? 425 | -- better to let users do this manually when they want to. 426 | -- walk_table(meta, function(node) f(node, meta, to) end, true) 427 | local ok, msg = pcall(function() f(doc, meta, to) end) 428 | if not ok then 429 | return nil, nil, ("Error running filter:\n" .. msg) 430 | end 431 | end 432 | local body = writer(doc, opts, columns) 433 | local data = walk_table(meta, 434 | function(node) 435 | if type(node) == "userdata" then 436 | return render_metadata(node, writer, opts, columns) 437 | else 438 | return node 439 | end 440 | end, false) 441 | -- free memory allocated by libcmark 442 | cmark.node_free(doc) 443 | walk_table(meta, 444 | function(node) 445 | if type(node) == "userdata" then 446 | cmark.node_free(node) 447 | end 448 | end, true) 449 | return body, data 450 | end 451 | 452 | return lcmark 453 | -------------------------------------------------------------------------------- /rockspec.in: -------------------------------------------------------------------------------- 1 | package = "lcmark" 2 | version = "_VERSION-_REVISION" 3 | source = { 4 | url = "git://github.com/jgm/lcmark", 5 | tag = "_VERSION-_REVISION" 6 | } 7 | description = { 8 | summary = [[A command-line CommonMark converter with flexible 9 | features, and a lua module that exposes these features.]], 10 | detailed = [[lcmark does what the cmark program does, 11 | with the following enhancements: 12 | (1) Support for YAML metadata at the top of the document. 13 | The metadata is parsed as CommonMark and returned in 14 | a table (dictionary) that will set template variables. 15 | (2) Support for templates, which add headers 16 | and footers around the body of the document, and can 17 | include variables defined in the metadata. 18 | (3) Support for filters, which allow the document to be 19 | transformed between parsing and rendering, making possible 20 | a large number of customizations.]], 21 | homepage = "https://github.com/jgm/lcmark", 22 | license = "BSD2", 23 | maintainer = "John MacFarlane ", 24 | } 25 | dependencies = { 26 | "lua >= 5.1", 27 | "cmark >= 0.31.1", 28 | "lpeg >= 0.12", 29 | "optparse >= 1.4", 30 | } 31 | build = { 32 | type = "builtin", 33 | modules = { 34 | lcmark = "lcmark.lua" 35 | }, 36 | install = { 37 | bin = { lcmark = "bin/lcmark" } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /standalone/Makefile: -------------------------------------------------------------------------------- 1 | # Change PLATFORM for your platform 2 | 3 | # Note that this currently only works with lua 5.2. 4 | # Lua 5.3 doesn't like the use of 'module' in alt_getopt.lua. 5 | # We could easily include a slightly customized version of 6 | # alt_getopt.lua to work around this limitation. 7 | 8 | # Note also that the produced binary can't be stripped! 9 | 10 | PLATFORM?=macosx 11 | LUATARBALL=http://www.lua.org/ftp/lua-5.2.4.tar.gz 12 | COPTS=-O2 -DNDEBUG -Wall -Wextra -pedantic 13 | LUADIR=$(shell echo $(LUATARBALL) | sed -e 's/.*\(lua-.*\)\.tar\.gz/\1/') 14 | LUASTATIC=$(LUADIR)/src/liblua.a 15 | AMALGAMATED=lcmark_amalgamated.lua 16 | PROG ?= ./lcmark 17 | LCMARK_DIR = .. 18 | 19 | .PHONY : all test distclean clean 20 | 21 | $(PROG): main.c $(AMALGAMATED).embed $(LUASTATIC) 22 | $(CC) $(COPTS) -I$(LUADIR)/src -L$(LUADIR)/src -o $@ $< $(LUASTATIC) 23 | 24 | %.embed: % 25 | xxd -i $< > $@ 26 | 27 | $(LUASTATIC): $(LUADIR) 28 | make -C $(LUADIR)/src $(PLATFORM) MYCFLAGS=-DLUA_USE_LINUX 29 | # note: LUA_USE_LINUX is recommended for linux, osx, freebsd 30 | 31 | $(AMALGAMATED): $(LCMARK_DIR)/bin/lcmark 32 | amalg.lua || (echo "You need to 'luarocks install amalg'" && exit 1) 33 | echo "amalgamating..." | lua -lamalg $< 34 | amalg.lua -s $< -c -x | sed -e '1,2d' > $@ 35 | 36 | $(LUADIR): 37 | curl $(LUATARBALL) | tar xvz 38 | 39 | clean: 40 | make -C $(LUADIR) clean 41 | -rm $(AMALGAMATED) $(AMALGAMATED).embed amalg.cache 42 | 43 | distclean: 44 | -rm -r $(PROG) $(LUADIR) 45 | -------------------------------------------------------------------------------- /standalone/main.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | /* Include the Lua API header files. */ 5 | #include 6 | #include 7 | #include 8 | 9 | #include "lcmark_amalgamated.lua.embed" 10 | 11 | int main( int argc, char *argv[] ) 12 | { 13 | lua_State *L = luaL_newstate(); 14 | 15 | /* command line args */ 16 | lua_newtable(L); 17 | if (argc > 0) { 18 | int i; 19 | for (i = 1; i < argc; i++) { 20 | lua_pushnumber(L, i); 21 | lua_pushstring(L, argv[i]); 22 | lua_rawset(L, -3); 23 | } 24 | } 25 | lua_setglobal(L, "arg"); 26 | 27 | /* load the libs */ 28 | luaL_openlibs(L); 29 | 30 | luaL_loadbuffer(L, (const char*)lcmark_amalgamated_lua, 31 | lcmark_amalgamated_lua_len, 32 | "lcmark_amalgamated_lua_len"); 33 | 34 | if (lua_pcall(L, 0, 0, 0) != 0) { 35 | lua_error(L); 36 | } 37 | 38 | lua_close(L); 39 | 40 | return 0; 41 | } 42 | -------------------------------------------------------------------------------- /templates/default.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | $title$ 8 | 9 | $if(highlighting_styles)$$endif$ 10 | 13 | 14 | 15 | $body$ 16 | 17 | 18 | -------------------------------------------------------------------------------- /templates/default.man: -------------------------------------------------------------------------------- 1 | .TH "$title$" "$section$" "$date$" "$footer$" "$header$" 2 | $body$ 3 | .SH AUTHORS 4 | $for(author)$$author$$sep$, $endfor$ 5 | -------------------------------------------------------------------------------- /templates/simple.latex: -------------------------------------------------------------------------------- 1 | \documentclass{article} 2 | \title{$title$} 3 | $if(author)$ 4 | \author{$for(author)$$author$$sep$\and$endfor$} 5 | $endif$ 6 | \begin{document} 7 | \maketitle 8 | $body$ 9 | \end{document} 10 | -------------------------------------------------------------------------------- /test.t: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env lua 2 | require 'Test.More' 3 | 4 | package.path = "./?.lua;" .. package.path 5 | package.cpath = "./?.so;" .. package.cpath 6 | 7 | local lcmark = require 'lcmark' 8 | local tests = require 'tests/spec-tests' 9 | 10 | subtest("spec tests (lcmark)", function() 11 | for _,test in ipairs(tests) do 12 | local html = lcmark.convert(test.markdown, 'html', {}) 13 | is(html, test.html, "example " .. tostring(test.example) .. 14 | " (lines " .. tostring(test.start_line) .. " - " .. 15 | tostring(test.end_line) .. ")") 16 | end 17 | end) 18 | 19 | local render_template = lcmark.render_template 20 | subtest("template tests", function() 21 | local res, msg = render_template("$", {}) 22 | is(res, nil, "unescaped $") 23 | is(msg, "parse failure at line 1: '$'", "error message for parse failure") 24 | is(render_template("foo$$bar$$baz", {}), 25 | "foo$bar$baz", "escaped $") 26 | is(render_template("foo $bar$", {bar = "bim"}), 27 | "foo bim", "variable") 28 | is(render_template("foo $bim$", {bar = "bim"}), 29 | "foo ", "missing variable") 30 | is(render_template("foo $bar$", {bar = false}), 31 | "foo ", "variable with false") 32 | is(render_template("foo $bar$", {bar = {}}), 33 | "foo ", "variable with empty table") 34 | is(render_template("foo $bar.baz$", {bar = { baz = "bim" }}), 35 | "foo bim", "variable with field") 36 | is(render_template("foo $bar.baz.bar$", {bar = { baz = "bim" }}), 37 | "foo ", "variable with missing field") 38 | is(render_template("$if(foo)$hello$endif$", {foo = true}), 39 | "hello", "simple if") 40 | is(render_template("$if(foo)$hello$endif$", {}), 41 | "", "simple if with missing variable") 42 | is(render_template("$if(foo)$hello$endif$", {foo = {}}), 43 | "", "simple if with empty table") 44 | is(render_template("$if(foo)$hello $foo$$endif$", {foo = true}), 45 | "hello true", "if with variable") 46 | is(render_template("$if(foo)$hello$else$goodbye$endif$", {foo = true}), 47 | "hello", "true if with else") 48 | is(render_template("$if(foo)$hello$else$goodbye$endif$", {}), 49 | "goodbye", "false if with else") 50 | is(render_template("$for(foo)${$foo$}$endfor$", {foo = {1,2,3}}), 51 | "{1}{2}{3}", "simple for") 52 | is(render_template("$for(foo)$$foo$$sep$, $endfor$", {foo = {1,2,3}}), 53 | "1, 2, 3", "simple for with sep") 54 | is(render_template("$for(foo)$$foo$$sep$, $endfor$", {foo = {}}), 55 | "", "for with empty list") 56 | is(render_template("$for(foo)$$foo$$sep$, $endfor$", {}), 57 | "", "for with nonexistent variable") 58 | is(render_template("$if(foo)$\nhello\n$endif$", {foo = "hi"}), 59 | render_template("$if(foo)$hello$endif$", {foo = "hi"}), 60 | "ignore newline after if and before endif") 61 | is(render_template("$if(foo)$\nhello\n$endif$\n", {foo = false}), 62 | "", "no extra blank line for conditional") 63 | end) 64 | 65 | local body, meta, msg = lcmark.convert("Hello *world*", "latex", {}) 66 | is(body, "Hello \\emph{world}\n", "simple latex body") 67 | eq_array(meta, {}, "simple latex meta") 68 | 69 | local body, meta, msg = lcmark.convert("dog's", "man", {smart = true}) 70 | is(body, ".PP\ndog\\[cq]s\n", "smart apostrophe") 71 | 72 | local body, meta, msg = lcmark.convert("foo\nbar", "html", 73 | {hardbreaks = true}) 74 | is(body, "

foo
\nbar

\n", "hardbreaks option") 75 | 76 | local body, meta, msg = lcmark.convert("", "html", 77 | {safe = true}) 78 | is(body, "\n", "safe option") 79 | 80 | local lorem = "Lorem ipsum dolor sit amet, consectetur adipiscing elit.\n" 81 | 82 | local body, meta, msg = lcmark.convert(lorem, "latex", {columns = 20}) 83 | is(body, "Lorem ipsum dolor\nsit amet,\nconsectetur\nadipiscing elit.\n", 84 | "columns option") 85 | 86 | local body, meta, msg = lcmark.convert("---\ntitle: My *title*\nauthor:\n- name: JJ\n institute: U of H\n...\n\nHello *world*", "latex", {yaml_metadata = true}) 87 | is(body, "Hello \\emph{world}\n", "latex body") 88 | eq_array(meta, {title = "My \\emph{title}", author = { {name = "JJ", institute = "U of H"}} }, "latex meta") 89 | 90 | local body, meta, msg = lcmark.convert("---\ntitle: My *title*\nauthor:\n- name: JJ\n institute: U of H\n...\n\nHello *world*", "latex", {yaml_metadata = false}) 91 | isnt(body, "Hello \\emph{world}\n", "latex body with yaml_metadata=false") 92 | eq_array(meta, {}, "latex meta with yaml_metadata=false") 93 | 94 | local body, meta, msg = lcmark.convert("---\ntitle: 1: 2\n...\n\nHello *world*", "latex", {yaml_metadata = true}) 95 | nok(body, "latex body nil with bad YAML") 96 | nok(meta, "meta nil with bad YAML") 97 | like(msg, "YAML parsing error:.*mapping values are not allowed in this context", "error message with bad YAML") 98 | 99 | local custom_parser = function(s) 100 | if s:find("title: 1: 2") then 101 | error("bad yaml") -- Simulate error 102 | elseif s:find("title: 3: 4") then 103 | return nil, "bad yaml" -- Simulate safe error 104 | else 105 | return {foo = "bar"} -- Simulate success 106 | end 107 | end 108 | 109 | local body, meta, msg = lcmark.convert("---\nfoo: bar\n...\n\nHello *world*", "latex", {yaml_metadata = true, yaml_parser = custom_parser}) 110 | is(body, "Hello \\emph{world}\n", "latex body") 111 | eq_array(meta, {foo = "bar"}, "meta with custom YAML parser") 112 | 113 | local body, meta, msg = lcmark.convert("---\ntitle: 1: 2\n...\n\nHello *world*", "latex", {yaml_metadata = true, yaml_parser = custom_parser}) 114 | nok(body, "latex body nil with bad YAML and custom YAML parser") 115 | nok(meta, "meta nil with bad YAML and custom YAML parser") 116 | like(msg, "YAML parsing error:.*bad yaml", "error message with bad YAML and custom YAML parser") 117 | 118 | local body, meta, msg = lcmark.convert("---\ntitle: 3: 4\n...\n\nHello *world*", "latex", {yaml_metadata = true, yaml_parser = custom_parser}) 119 | nok(body, "latex body nil with bad YAML and safe custom YAML parser") 120 | nok(meta, "meta nil with bad YAML and safe custom YAML parser") 121 | like(msg, "YAML parsing error:.*bad yaml", "error message with bad YAML and safe custom YAML parser") 122 | 123 | local nonexistent, msg = lcmark.load_filter("nonexistent.lua") 124 | nok(nonexistent, "load_filter fails on nonexistent filter") 125 | is(msg, "cannot open nonexistent.lua: No such file or directory", "message on nonexistent filter") 126 | 127 | local badfilter, msg = lcmark.load_filter("tests/bad_filter.lua") 128 | nok(badfilter, "load_filter fails on bad filter") 129 | like(msg, "tests/bad_filter%.lua:2: '?'? expected near '%('", "error message on bad filter") 130 | 131 | local badfilter, msg = lcmark.load_filter("tests/bad_filter2.lua") 132 | nok(badfilter, "load_filter fails when script doesn't return a function") 133 | is(msg, "Filter tests/bad_filter2.lua returns a table, not a function", "error message on filter not returning a function") 134 | 135 | local badfilter, msg = lcmark.load_filter("tests/bad_filter3.lua") 136 | local doc, meta, msg = lcmark.convert("test", "html", {filters = {badfilter}}) 137 | nok(doc, "trap runtime error raised by filter") 138 | like(msg, "Error running filter:\ntests/bad_filter3%.lua:2: attempt to perform arithmetic on .*local 'doc'", "error message on runtime error from filter") 139 | 140 | local count_links = lcmark.load_filter("filters/count_links.lua") 141 | ok(count_links, "loaded filter count_links.lua") 142 | 143 | local body, meta, msg = lcmark.convert("[link](u) and ", "html", {filters = {count_links}}) 144 | is(body, "

link (link #1) and http://example.com (link #2)

\n

2 links found in this html document.

\n", "added link numbers and count") 145 | is(meta.number_of_links, "2", "added metadata field number_of_links") 146 | 147 | local include = lcmark.load_filter("filters/include.lua") 148 | ok(count_links, "loaded filter include.lua") 149 | local body, meta, msg = lcmark.convert('``` make include="Makefile"\ncontents to ignore\n```', 'html', {filters = {include}}) 150 | like(body, '
') 151 | 152 | 153 | done_testing() 154 | -------------------------------------------------------------------------------- /tests/bad_filter.lua: -------------------------------------------------------------------------------- 1 | -- This has errors! 2 | function(doc) 3 | return doc2 4 | end 5 | -------------------------------------------------------------------------------- /tests/bad_filter2.lua: -------------------------------------------------------------------------------- 1 | -- does not return a function 2 | return {5} 3 | -------------------------------------------------------------------------------- /tests/bad_filter3.lua: -------------------------------------------------------------------------------- 1 | return function(doc, meta, to) 2 | return doc + 3 3 | end 4 | --------------------------------------------------------------------------------