├── .gitignore
├── LICENSE.md
├── README.md
├── luamd
├── md.lua
├── mddev
├── rockspecs
├── md-0.0-1.rockspec
└── md-0.0-2.rockspec
├── test_documents
└── 1.md
└── testrender.lua
/.gitignore:
--------------------------------------------------------------------------------
1 | CommonMark
2 | test_documents/*.html
3 |
4 | # Created by https://www.gitignore.io/api/lua
5 |
6 | ### Lua ###
7 | # Compiled Lua sources
8 | luac.out
9 |
10 | # luarocks build files
11 | *.src.rock
12 | *.zip
13 | *.tar.gz
14 |
15 | # Object files
16 | *.o
17 | *.os
18 | *.ko
19 | *.obj
20 | *.elf
21 |
22 | # Precompiled Headers
23 | *.gch
24 | *.pch
25 |
26 | # Libraries
27 | *.lib
28 | *.a
29 | *.la
30 | *.lo
31 | *.def
32 | *.exp
33 |
34 | # Shared objects (inc. Windows DLLs)
35 | *.dll
36 | *.so
37 | *.so.*
38 | *.dylib
39 |
40 | # Executables
41 | *.exe
42 | *.out
43 | *.app
44 | *.i*86
45 | *.x86_64
46 | *.hex
47 |
48 |
49 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | Copyright (c) 2016 Calvin Rose
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of
4 | this software and associated documentation files (the "Software"), to deal in
5 | the Software without restriction, including without limitation the rights to
6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
7 | the Software, and to permit persons to whom the Software is furnished to do so,
8 | subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in all
11 | copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
15 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
16 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
17 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
19 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # luamd
2 | luamd is a Markdown to HTML renderer written in portable, pure Lua. It's also really easy to use.
3 |
4 | ```lua
5 | local md = require "md"
6 | local htmlFragment = md[[
7 | # This is Some Markdown
8 | Write whatever you want.
9 | * Supports Lists
10 | * And other features
11 | ]]
12 | ```
13 |
14 | ## Install
15 | Copy `md.lua` to your project in whatever directory you want.
16 |
17 | ## Use it
18 | Render markdown from a string. On bad input, retuns nil and an error message.
19 | ```lua
20 | local html, err = md.renderString(str, options)
21 | ```
22 |
23 | Render markdown from a line iterator. An iterator is a function the returns successive lines
24 | when called repeatedly, and nil when there are no lines left.
25 | ```lua
26 | local html, err = md.renderLineIterator(iter, options)
27 | ```
28 |
29 | Render markdown from a list like table of lines.
30 | ```lua
31 | local html, err = md.renderTable(t, options)
32 | ```
33 |
34 | Renders strings, iterators, and tables.
35 | ```lua
36 | local html, err = md.render(object, options)
37 | ```
38 |
39 | Calling the module as a function will invoke `md.render`. This is the easiest way to use the module.
40 |
41 | The options table is an optional table of options. The currently supported options are below.
42 | * `tag` - Surrounding HTML tag for HTML fragment.
43 | * `attributes` - A table attributes for the surround HTML node. For example, `{ style = "padding: 10px;" }`.
44 | * `insertHead` - An HTML fragment to insert before the main body of HTML. (Inserted after the wrapping tag, if present.)
45 | * `insertTail` - An HTML fragment to insert after the main body of HTML. (Inserted before the closing tag, if present.)
46 | * `prependHead` - An HTML fragment to insert before the main body of HTML. (Inserted before the opening tag, if present.)
47 | * `appendTail` - An HTML fragment to insert after the main body of HTML. (Inserted after the closing tag, if present.)
48 |
49 | Here is a little diagram for where the optional fragments go.
50 | ```
51 | ** prependHead **
52 |
53 | ** insertHead **
54 |
55 | ... rendered markdown ...
56 |
57 | ** insertTail **
58 |
59 | ** appendTail **
60 | ```
61 |
62 | ## Testing
63 |
64 | There is no unit-testing yet, but testing can be done by running the testrender.lua script. This
65 | builds HTML files in the test_documents directory that correspond to the markdown source files.
66 | Open these with a web browser and assure that they look fine. To add more test documents, place
67 | a markdown file in the test_documents folder and add it to the documents list in testrender.lua.
68 |
69 | ## Todo
70 |
71 | Needs some good unit testing. :).
72 |
73 | Supports most of basic Markdown syntax, but there are some features that need to be implemented.
74 | I haven't implemented them because I don't need them - yet.
75 |
76 | * HTML and code escapes - Probably the most important one on the list.
77 | * Some alternative syntax for numbered Lists (using `#.`)
78 | * Indent style code - I prefer backtick quoted code
79 | * Tables - GitHub style tables would be cool
80 | * Footnotes - Might need them, but not yet.
81 |
82 | ## Bugs
83 |
84 | If anyone wants to use this and finds bugs and issues, let me know! I usually can fix things pretty quickly,
85 | and I appreciate help.
86 |
--------------------------------------------------------------------------------
/luamd:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env lua
2 |
3 | local function showusage()
4 | print("usage: luamd [OPTION] ... file.md")
5 | print("Converts Markdown to HTML")
6 | print("")
7 | print(" -f, --fragment convert to HTML fragment only")
8 | print(" -p, --print-only just print, don't write to file")
9 | print(" -v, --verbose show some useful info while working")
10 | print(" -h, --help show this usage help")
11 | print("")
12 | print("This software is licensed under the MIT License.")
13 | print("See https://github.com/bakpakin/luamd for more info.")
14 | end
15 |
16 | -- option parsing:
17 | local longopts = {
18 | fragment = "f",
19 | help = "h",
20 | ["print-only"] = "p",
21 | verbose = "v",
22 | }
23 |
24 | local fragment, print_only, verbose
25 |
26 | local ARGV = arg
27 | local argidx = 1
28 | while argidx <= #ARGV do
29 | local arg = ARGV[argidx]
30 | argidx = argidx + 1
31 | if arg == "--" then break end
32 | -- parse longopts
33 | if arg:sub(1,2) == "--" then
34 | local opt = longopts[arg:sub(3)]
35 | if opt ~= nil then arg = "-"..opt end
36 | end
37 | -- code for each option
38 | if arg == "-h" then
39 | return showusage()
40 | elseif arg == "-f" then
41 | fragment = true
42 | elseif arg == "-p" then
43 | print_only = true
44 | elseif arg == "-v" then
45 | verbose = true
46 | else
47 | -- not a recognized option, should be a filename
48 | argidx = argidx - 1
49 | break
50 | end
51 | end
52 |
53 | if verbose then
54 | print("Running", _VERSION)
55 | print("Options", ...)
56 | end
57 |
58 | local function file_exists(name)
59 | local f=io.open(name,"r")
60 | if f~=nil then io.close(f) return true else return false end
61 | end
62 |
63 | local stdin = io.stdin
64 | local md_options = {
65 | prependHead = "",
66 | appendTail = "",
67 | tag = "body",
68 | insertHead = string.format("
%s", file),
69 | }
70 | if ARGV[argidx] and ARGV[argidx] ~= "" then
71 | local file = nil
72 | if file_exists(ARGV[argidx]) then
73 | file = ARGV[argidx]
74 | end
75 | if file then
76 | local md = require("md")
77 | local f = io.open(file, "rb")
78 | local content = f:read("*all")
79 | f:close()
80 | if fragment then md_options = {} end
81 | local html, err = md(content, md_options)
82 | if html then
83 | if print_only then
84 | print(html)
85 | else
86 | f = io.open(file..".html", "w")
87 | f:write(html)
88 | f:close()
89 | end
90 | elseif err then
91 | print("Error: ", err)
92 | end
93 | end
94 | elseif stdin then
95 | local md = require("md")
96 | local f = stdin
97 | local content = f:read("*all")
98 | f:close()
99 | if fragment then md_options = {} end
100 | local html, err = md(content, md_options)
101 | if html then
102 | if print_only then
103 | print(html)
104 | end
105 | elseif err then
106 | print("Error: ", err)
107 | end
108 | else
109 | return showusage()
110 | end
111 |
--------------------------------------------------------------------------------
/md.lua:
--------------------------------------------------------------------------------
1 | --[[
2 | Copyright (c) 2016 Calvin Rose
3 |
4 | Permission is hereby granted, free of charge, to any person obtaining a copy of
5 | this software and associated documentation files (the "Software"), to deal in
6 | the Software without restriction, including without limitation the rights to
7 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
8 | the Software, and to permit persons to whom the Software is furnished to do so,
9 | subject to the following conditions:
10 |
11 | The above copyright notice and this permission notice shall be included in all
12 | copies or substantial portions of the Software.
13 |
14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
16 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
17 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
18 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
19 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
20 | ]]
21 |
22 | local concat = table.concat
23 | local sub = string.sub
24 | local match = string.match
25 | local format = string.format
26 | local gmatch = string.gmatch
27 | local byte = string.byte
28 | local find = string.find
29 | local lower = string.lower
30 | local tonumber = tonumber -- luacheck: no unused
31 | local type = type
32 | local pcall = pcall
33 |
34 | --------------------------------------------------------------------------------
35 | -- Stream Utils
36 | --------------------------------------------------------------------------------
37 |
38 | local function stringLineStream(str)
39 | return gmatch(str, "([^\n\r]*)\r?\n?")
40 | end
41 |
42 | local function tableLineStream(t)
43 | local index = 0
44 | return function()
45 | index = index + 1
46 | return t[index]
47 | end
48 | end
49 |
50 | local function bufferStream(linestream)
51 | local bufferedLine = linestream()
52 | return function()
53 | bufferedLine = linestream()
54 | return bufferedLine
55 | end, function()
56 | return bufferedLine
57 | end
58 | end
59 |
60 | --------------------------------------------------------------------------------
61 | -- Line Level Operations
62 | --------------------------------------------------------------------------------
63 |
64 | local lineDelimiters = {'`', '__', '**', '_', '*', '~~'}
65 | local function findDelim(str, start, max)
66 | local delim = nil
67 | local min = 1/0
68 | local finish = 1/0
69 | max = max or #str
70 | for i = 1, #lineDelimiters do
71 | local pos, fin = find(str, lineDelimiters[i], start, true)
72 | if pos and pos < min and pos <= max then
73 | min = pos
74 | finish = fin
75 | delim = lineDelimiters[i]
76 | end
77 | end
78 | return delim, min, finish
79 | end
80 |
81 | local function externalLinkEscape(str, t)
82 | local nomatches = true
83 | for m1, m2, m3 in gmatch(str, '(.*)%[(.*)%](.*)') do
84 | if nomatches then t[#t + 1] = match(m1, '^(.-)!?$'); nomatches = false end
85 | if byte(m1, #m1) == byte '!' then
86 | t[#t + 1] = {type = 'img', attributes = {alt = m2}}
87 | else
88 | t[#t + 1] = {m2, type = 'a'}
89 | end
90 | t[#t + 1] = m3
91 | end
92 | if nomatches then t[#t + 1] = str end
93 | end
94 |
95 | local function linkEscape(str, t)
96 | local nomatches = true
97 | for m1, m2, m3, m4 in gmatch(str, '(.*)%[(.*)%]%((.*)%)(.*)') do
98 | if nomatches then externalLinkEscape(match(m1, '^(.-)!?$'), t); nomatches = false end
99 | if byte(m1, #m1) == byte '!' then
100 | t[#t + 1] = {type = 'img', attributes = {
101 | src = m3,
102 | alt = m2
103 | }, noclose = true}
104 | else
105 | t[#t + 1] = {m2, type = 'a', attributes = {href = m3}}
106 | end
107 | externalLinkEscape(m4, t)
108 | end
109 | if nomatches then externalLinkEscape(str, t) end
110 | end
111 |
112 | local lineDeimiterNames = {['`'] = 'code', ['__'] = 'strong', ['**'] = 'strong', ['_'] = 'em', ['*'] = 'em', ['~~'] = 'strike' }
113 | local function lineRead(str, start, finish)
114 | start, finish = start or 1, finish or #str
115 | local searchIndex = start
116 | local tree = {}
117 | while true do
118 | local delim, dstart, dfinish = findDelim(str, searchIndex, finish)
119 | if not delim then
120 | linkEscape(sub(str, searchIndex, finish), tree)
121 | break
122 | end
123 | if dstart > searchIndex then
124 | linkEscape(sub(str, searchIndex, dstart - 1), tree)
125 | end
126 | local nextdstart, nextdfinish = find(str, delim, dfinish + 1, true)
127 | if nextdstart then
128 | if delim == '`' then
129 | tree[#tree + 1] = {
130 | sub(str, dfinish + 1, nextdstart - 1),
131 | type = 'code'
132 | }
133 | else
134 | local subtree = lineRead(str, dfinish + 1, nextdstart - 1)
135 | subtree.type = lineDeimiterNames[delim]
136 | tree[#tree + 1] = subtree
137 | end
138 | searchIndex = nextdfinish + 1
139 | else
140 | tree[#tree + 1] = {
141 | delim,
142 | }
143 | searchIndex = dfinish + 1
144 | end
145 | end
146 | return tree
147 | end
148 |
149 | local function getIndentLevel(line)
150 | local level = 0
151 | for i = 1, #line do
152 | local b = byte(line, i)
153 | if b == byte(' ') or b == byte('>') then
154 | level = level + 1
155 | elseif b == byte('\t') then
156 | level = level + 4
157 | else
158 | break
159 | end
160 | end
161 | return level
162 | end
163 |
164 | local function stripIndent(line, level, ignorepattern) -- luacheck: no unused args
165 | local currentLevel = -1
166 | for i = 1, #line do
167 | if byte(line, i) == byte("\t") then
168 | currentLevel = currentLevel + 4
169 | elseif byte(line, i) == byte(" ") or byte(line, i) == byte(">") then
170 | currentLevel = currentLevel + 1
171 | else
172 | return sub(line, i, -1)
173 | end
174 | if currentLevel == level then
175 | return sub(line, i, -1)
176 | elseif currentLevel > level then
177 | local front = ""
178 | for j = 1, currentLevel - level do front = front .. " " end -- luacheck: no unused args
179 | return front .. sub(line, i, -1)
180 | end
181 | end
182 | end
183 |
184 | --------------------------------------------------------------------------------
185 | -- Useful variables
186 | --------------------------------------------------------------------------------
187 | local NEWLINE = '\n'
188 |
189 | --------------------------------------------------------------------------------
190 | -- Patterns
191 | --------------------------------------------------------------------------------
192 |
193 | local PATTERN_EMPTY = "^%s*$"
194 | local PATTERN_COMMENT = "^%s*<>"
195 | local PATTERN_HEADER = "^%s*(%#+)%s*(.*)%#*$"
196 | local PATTERN_RULE1 = "^%s?%s?%s?(-%s*-%s*-[%s-]*)$"
197 | local PATTERN_RULE2 = "^%s?%s?%s?(*%s**%s**[%s*]*)$"
198 | local PATTERN_RULE3 = "^%s?%s?%s?(_%s*_%s*_[%s_]*)$"
199 | local PATTERN_CODEBLOCK = "^%s*%`%`%`(.*)"
200 | local PATTERN_BLOCKQUOTE = "^%s*> (.*)$"
201 | local PATTERN_ULIST = "^%s*[%*%-] (.+)$"
202 | local PATTERN_OLIST = "^%s*%d+%. (.+)$"
203 | local PATTERN_LINKDEF = "^%s*%[(.*)%]%s*%:%s*(.*)"
204 |
205 | -- List of patterns
206 | local PATTERNS = {
207 | PATTERN_EMPTY,
208 | PATTERN_COMMENT,
209 | PATTERN_HEADER,
210 | PATTERN_RULE1,
211 | PATTERN_RULE2,
212 | PATTERN_RULE3,
213 | PATTERN_CODEBLOCK,
214 | PATTERN_BLOCKQUOTE,
215 | PATTERN_ULIST,
216 | PATTERN_OLIST,
217 | PATTERN_LINKDEF
218 | }
219 |
220 | local function isSpecialLine(line)
221 | for i = 1, #PATTERNS do
222 | if match(line, PATTERNS[i]) then return PATTERNS[i] end
223 | end
224 | end
225 |
226 | --------------------------------------------------------------------------------
227 | -- Simple Reading - Non Recursive
228 | --------------------------------------------------------------------------------
229 |
230 | local function readSimple(pop, peek, tree, links)
231 |
232 | local line = peek()
233 | if not line then return end
234 |
235 | -- Test for Empty or Comment
236 | if match(line, PATTERN_EMPTY) or match(line, PATTERN_COMMENT) then
237 | return pop()
238 | end
239 |
240 | -- Test for Header
241 | local m, rest = match(line, PATTERN_HEADER)
242 | if m then
243 | tree[#tree + 1] = {
244 | lineRead(rest),
245 | type = "h" .. #m
246 | }
247 | tree[#tree + 1] = NEWLINE
248 | return pop()
249 | end
250 |
251 | -- Test for Horizontal Rule
252 | if match(line, PATTERN_RULE1) or
253 | match(line, PATTERN_RULE2) or
254 | match(line, PATTERN_RULE3) then
255 | tree[#tree + 1] = { type = "hr", noclose = true }
256 | tree[#tree + 1] = NEWLINE
257 | return pop()
258 | end
259 |
260 | -- Test for Code Block
261 | local syntax = match(line, PATTERN_CODEBLOCK)
262 | if syntax then
263 | local indent = getIndentLevel(line)
264 | local code = {
265 | type = "code"
266 | }
267 | if #syntax > 0 then
268 | code.attributes = {
269 | class = format('language-%s', lower(syntax))
270 | }
271 | end
272 | local pre = {
273 | type = "pre",
274 | [1] = code
275 | }
276 | tree[#tree + 1] = pre
277 | while not (match(pop(), PATTERN_CODEBLOCK) and getIndentLevel(peek()) == indent) do
278 | code[#code + 1] = peek()
279 | code[#code + 1] = '\r\n'
280 | end
281 | return pop()
282 | end
283 |
284 | -- Test for link definition
285 | local linkname, location = match(line, PATTERN_LINKDEF)
286 | if linkname then
287 | links[lower(linkname)] = location
288 | return pop()
289 | end
290 |
291 | -- Test for header type two
292 | local nextLine = pop()
293 | if nextLine and match(nextLine, "^%s*%=+$") then
294 | tree[#tree + 1] = { lineRead(line), type = "h1" }
295 | return pop()
296 | elseif nextLine and match(nextLine, "^%s*%-+$") then
297 | tree[#tree + 1] = { lineRead(line), type = "h2" }
298 | return pop()
299 | end
300 |
301 | -- Do Paragraph
302 | local p = {
303 | lineRead(line), NEWLINE,
304 | type = "p"
305 | }
306 | tree[#tree + 1] = p
307 | while nextLine and not isSpecialLine(nextLine) do
308 | p[#p + 1] = lineRead(nextLine)
309 | p[#p + 1] = NEWLINE
310 | nextLine = pop()
311 | end
312 | p[#p] = nil
313 | tree[#tree + 1] = NEWLINE
314 | return peek()
315 |
316 | end
317 |
318 | --------------------------------------------------------------------------------
319 | -- Main Reading - Potentially Recursive
320 | --------------------------------------------------------------------------------
321 |
322 | local readLineStream
323 |
324 | local function readFragment(pop, peek, links, stop, ...)
325 | local accum2 = {}
326 | local line = peek()
327 | local indent = getIndentLevel(line)
328 | while true do
329 | accum2[#accum2 + 1] = stripIndent(line, indent)
330 | line = pop()
331 | if not line then break end
332 | if stop(line, ...) then break end
333 | end
334 | local tree = {}
335 | readLineStream(tableLineStream(accum2), tree, links)
336 | return tree
337 | end
338 |
339 | local function readBlockQuote(pop, peek, tree, links)
340 | local line = peek()
341 | if match(line, PATTERN_BLOCKQUOTE) then
342 | local bq = readFragment(pop, peek, links, function(l)
343 | local tp = isSpecialLine(l)
344 | return tp and tp ~= PATTERN_BLOCKQUOTE
345 | end)
346 | bq.type = 'blockquote'
347 | tree[#tree + 1] = bq
348 | return peek()
349 | end
350 | end
351 |
352 | local function readList(pop, peek, tree, links, expectedIndent)
353 | if not peek() then return end
354 | if expectedIndent and getIndentLevel(peek()) ~= expectedIndent then return end
355 | local listPattern = (match(peek(), PATTERN_ULIST) and PATTERN_ULIST) or
356 | (match(peek(), PATTERN_OLIST) and PATTERN_OLIST)
357 | if not listPattern then return end
358 | local lineType = listPattern
359 | local line = peek()
360 | local indent = getIndentLevel(line)
361 | local list = {
362 | type = (listPattern == PATTERN_ULIST and "ul" or "ol")
363 | }
364 | tree[#tree + 1] = list
365 | list[1] = NEWLINE
366 | while lineType == listPattern do
367 | list[#list + 1] = {
368 | lineRead(match(line, lineType)),
369 | type = "li"
370 | }
371 | line = pop()
372 | if not line then break end
373 | lineType = isSpecialLine(line)
374 | if lineType ~= PATTERN_EMPTY then
375 | list[#list + 1] = NEWLINE
376 | local i = getIndentLevel(line)
377 | if i < indent then break end
378 | if i > indent then
379 | local subtree = readFragment(pop, peek, links, function(l)
380 | if not l then return true end
381 | local tp = isSpecialLine(l)
382 | return tp ~= PATTERN_EMPTY and getIndentLevel(l) < i
383 | end)
384 | list[#list + 1] = subtree
385 | line = peek()
386 | if not line then break end
387 | lineType = isSpecialLine(line)
388 | end
389 | end
390 | end
391 | list[#list + 1] = NEWLINE
392 | tree[#tree + 1] = NEWLINE
393 | return peek()
394 | end
395 |
396 | function readLineStream(stream, tree, links)
397 | local pop, peek = bufferStream(stream)
398 | tree = tree or {}
399 | links = links or {}
400 | while peek() do
401 | if not readBlockQuote(pop, peek, tree, links) then
402 | if not readList(pop, peek, tree, links) then
403 | readSimple(pop, peek, tree, links)
404 | end
405 | end
406 | end
407 | return tree, links
408 | end
409 |
410 | local function read(str) -- luacheck: no unused
411 | return readLineStream(stringLineStream(str))
412 | end
413 |
414 | --------------------------------------------------------------------------------
415 | -- Rendering
416 | --------------------------------------------------------------------------------
417 |
418 | local function renderAttributes(attributes)
419 | local accum = {}
420 | for k, v in pairs(attributes) do
421 | accum[#accum + 1] = format("%s=\"%s\"", k, v)
422 | end
423 | return concat(accum, ' ')
424 | end
425 |
426 | local function renderTree(tree, links, accum)
427 | if tree.type then
428 | local attribs = tree.attributes or {}
429 | if tree.type == 'a' and not attribs.href then attribs.href = links[lower(tree[1] or '')] or '' end
430 | if tree.type == 'img' and not attribs.src then attribs.src = links[lower(attribs.alt or '')] or '' end
431 | local attribstr = renderAttributes(attribs)
432 | if #attribstr > 0 then
433 | accum[#accum + 1] = format("<%s %s>", tree.type, attribstr)
434 | else
435 | accum[#accum + 1] = format("<%s>", tree.type)
436 | end
437 | end
438 | for i = 1, #tree do
439 | local line = tree[i]
440 | if type(line) == "string" then
441 | accum[#accum + 1] = line
442 | elseif type(line) == "table" then
443 | renderTree(line, links, accum)
444 | else
445 | error "Unexpected node while rendering tree."
446 | end
447 | end
448 | if not tree.noclose and tree.type then
449 | accum[#accum + 1] = format("%s>", tree.type)
450 | end
451 | end
452 |
453 | local function renderLinesRaw(stream, options)
454 | local tree, links = readLineStream(stream)
455 | local accum = {}
456 | local head, tail, insertHead, insertTail, prependHead, appendTail = nil, nil, nil, nil, nil, nil
457 | if options then
458 | assert(type(options) == 'table', "Options argument should be a table.")
459 | if options.tag then
460 | tail = format('%s>', options.tag)
461 | if options.attributes then
462 | head = format('<%s %s>', options.tag, renderAttributes(options.attributes))
463 | else
464 | head = format('<%s>', options.tag)
465 | end
466 | end
467 | insertHead = options.insertHead
468 | insertTail = options.insertTail
469 | prependHead = options.prependHead
470 | appendTail = options.appendTail
471 | end
472 | accum[#accum + 1] = prependHead
473 | accum[#accum + 1] = head
474 | accum[#accum + 1] = insertHead
475 | renderTree(tree, links, accum)
476 | if accum[#accum] == NEWLINE then accum[#accum] = nil end
477 | accum[#accum + 1] = insertTail
478 | accum[#accum + 1] = tail
479 | accum[#accum + 1] = appendTail
480 | return concat(accum)
481 | end
482 |
483 | --------------------------------------------------------------------------------
484 | -- Module
485 | --------------------------------------------------------------------------------
486 |
487 | local function pwrap(...)
488 | local status, value = pcall(...)
489 | if status then
490 | return value
491 | else
492 | return nil, value
493 | end
494 | end
495 |
496 | local function renderLineIterator(stream, options)
497 | return pwrap(renderLinesRaw, stream, options)
498 | end
499 |
500 | local function renderTable(t, options)
501 | return pwrap(renderLinesRaw, tableLineStream(t), options)
502 | end
503 |
504 | local function renderString(str, options)
505 | return pwrap(renderLinesRaw, stringLineStream(str), options)
506 | end
507 |
508 | local renderers = {
509 | ['string'] = renderString,
510 | ['table'] = renderTable,
511 | ['function'] = renderLineIterator
512 | }
513 |
514 | local function render(source, options)
515 | local renderer = renderers[type(source)]
516 | if not renderer then return nil, "Source must be a string, table, or function." end
517 | return renderer(source, options)
518 | end
519 |
520 | return setmetatable({
521 | render = render,
522 | renderString = renderString,
523 | renderLineIterator = renderLineIterator,
524 | renderTable = renderTable
525 | }, {
526 | __call = function(self, ...) -- luacheck: no unused args
527 | return render(...)
528 | end
529 | })
530 |
--------------------------------------------------------------------------------
/mddev:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | CURDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
4 | PROG="$CURDIR/luamd -f -p"
5 | PATTERN=""
6 |
7 | if [ ! -d "CommonMark" ]; then
8 | git clone https://github.com/jgm/CommonMark.git
9 | fi
10 |
11 | python3 CommonMark/test/spec_tests.py --program "$PROG" --spec "CommonMark/spec.txt" --pattern "$PATTERN"
12 |
--------------------------------------------------------------------------------
/rockspecs/md-0.0-1.rockspec:
--------------------------------------------------------------------------------
1 | package = "md"
2 | version = "0.0-1"
3 | source = {
4 | url = "git://github.com/bakpakin/luamd",
5 | tag = "0.0-1"
6 | }
7 | description = {
8 | summary = "Markdown to HTML in pure Lua.",
9 | detailed = [[
10 | md is a Markdown to HTML renderer written in portable, pure Lua. It's also really easy to use.
11 | ]],
12 | homepage = "https://github.com/bakpakin/luamd",
13 | license = "MIT"
14 | }
15 | dependencies = {
16 | "lua >= 5.1"
17 | }
18 | build = {
19 | type = "builtin",
20 | modules = {
21 | tiny = "md.lua"
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/rockspecs/md-0.0-2.rockspec:
--------------------------------------------------------------------------------
1 | package = "md"
2 | version = "0.0-2"
3 | source = {
4 | url = "git://github.com/bakpakin/luamd",
5 | tag = "0.0"
6 | }
7 | description = {
8 | summary = "Markdown to HTML in pure Lua.",
9 | detailed = [[
10 | md is a Markdown to HTML renderer written in portable, pure Lua. It's also really easy to use.
11 | ]],
12 | homepage = "https://github.com/bakpakin/luamd",
13 | license = "MIT"
14 | }
15 | dependencies = {
16 | "lua >= 5.1"
17 | }
18 | build = {
19 | type = "builtin",
20 | modules = {
21 | md = "md.lua"
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/test_documents/1.md:
--------------------------------------------------------------------------------
1 | # Test Number 1
2 |
3 | md.lua is pretty cool.
4 |
5 | * It outputs html
6 | * It's pretty fast
7 | * It's pure Lua
8 | * It's smaller than other solutions
9 | * It has these tests
10 | * It's made with rainbows.
11 |
12 | 
13 |
--------------------------------------------------------------------------------
/testrender.lua:
--------------------------------------------------------------------------------
1 | --[[
2 | This test module renders all of the markdown files in the test documents directory
3 | and outputs html files of the same name (minus the extension). Open these in a browser
4 | to ensure they look alright.
5 | ]]
6 |
7 | local md = require 'md'
8 |
9 | local documents = {
10 | "1"
11 | }
12 |
13 | for _, name in ipairs(documents) do
14 | local fullpath = './test_documents/' .. name .. '.md'
15 | local source, err = md(io.lines(fullpath))
16 | if err then
17 | print(("Error in %s: %s"):format(fullpath, err))
18 | else
19 | local file = io.open('./test_documents/' .. name .. '.html', 'w')
20 | file:write(source)
21 | print(("Rendered %s."):format(fullpath))
22 | end
23 | end
24 |
--------------------------------------------------------------------------------