├── .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("", 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('', 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 | ![](http://images.clipartpanda.com/rainbow-clip-art-rainbow.gif) 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 | --------------------------------------------------------------------------------