| 1 #!/usr/bin/env lua |
1 #!/usr/bin/env lua |
| 2 |
2 |
| 3 --[[ |
|
| 4 # markdown.lua -- version 0.32 |
|
| 5 |
|
| 6 <http://www.frykholm.se/files/markdown.lua> |
|
| 7 |
|
| 8 **Author:** Niklas Frykholm, <niklas@frykholm.se> |
|
| 9 **Date:** 31 May 2008 |
|
| 10 |
|
| 11 This is an implementation of the popular text markup language Markdown in pure Lua. |
|
| 12 Markdown can convert documents written in a simple and easy to read text format |
|
| 13 to well-formatted HTML. For a more thourough description of Markdown and the Markdown |
|
| 14 syntax, see <http://daringfireball.net/projects/markdown>. |
|
| 15 |
|
| 16 The original Markdown source is written in Perl and makes heavy use of advanced |
|
| 17 regular expression techniques (such as negative look-ahead, etc) which are not available |
|
| 18 in Lua's simple regex engine. Therefore this Lua port has been rewritten from the ground |
|
| 19 up. It is probably not completely bug free. If you notice any bugs, please report them to |
|
| 20 me. A unit test that exposes the error is helpful. |
|
| 21 |
|
| 22 ## Usage |
|
| 23 |
|
| 24 require "markdown" |
|
| 25 markdown(source) |
|
| 26 |
|
| 27 ``markdown.lua`` exposes a single global function named ``markdown(s)`` which applies the |
|
| 28 Markdown transformation to the specified string. |
|
| 29 |
|
| 30 ``markdown.lua`` can also be used directly from the command line: |
|
| 31 |
|
| 32 lua markdown.lua test.md |
|
| 33 |
|
| 34 Creates a file ``test.html`` with the converted content of ``test.md``. Run: |
|
| 35 |
|
| 36 lua markdown.lua -h |
|
| 37 |
|
| 38 For a description of the command-line options. |
|
| 39 |
|
| 40 ``markdown.lua`` uses the same license as Lua, the MIT license. |
|
| 41 |
|
| 42 ## License |
|
| 43 |
|
| 44 Copyright © 2008 Niklas Frykholm. |
|
| 45 |
|
| 46 Permission is hereby granted, free of charge, to any person obtaining a copy of this |
|
| 47 software and associated documentation files (the "Software"), to deal in the Software |
|
| 48 without restriction, including without limitation the rights to use, copy, modify, merge, |
|
| 49 publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons |
|
| 50 to whom the Software is furnished to do so, subject to the following conditions: |
|
| 51 |
|
| 52 The above copyright notice and this permission notice shall be included in all copies |
|
| 53 or substantial portions of the Software. |
|
| 54 |
|
| 55 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
|
| 56 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
|
| 57 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
|
| 58 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
|
| 59 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
|
| 60 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN |
|
| 61 THE SOFTWARE. |
|
| 62 |
|
| 63 ## Version history |
|
| 64 |
|
| 65 - **0.32** -- 31 May 2008 |
|
| 66 - Fix for links containing brackets |
|
| 67 - **0.31** -- 1 Mar 2008 |
|
| 68 - Fix for link definitions followed by spaces |
|
| 69 - **0.30** -- 25 Feb 2008 |
|
| 70 - Consistent behavior with Markdown when the same link reference is reused |
|
| 71 - **0.29** -- 24 Feb 2008 |
|
| 72 - Fix for <pre> blocks with spaces in them |
|
| 73 - **0.28** -- 18 Feb 2008 |
|
| 74 - Fix for link encoding |
|
| 75 - **0.27** -- 14 Feb 2008 |
|
| 76 - Fix for link database links with () |
|
| 77 - **0.26** -- 06 Feb 2008 |
|
| 78 - Fix for nested italic and bold markers |
|
| 79 - **0.25** -- 24 Jan 2008 |
|
| 80 - Fix for encoding of naked < |
|
| 81 - **0.24** -- 21 Jan 2008 |
|
| 82 - Fix for link behavior. |
|
| 83 - **0.23** -- 10 Jan 2008 |
|
| 84 - Fix for a regression bug in longer expressions in italic or bold. |
|
| 85 - **0.22** -- 27 Dec 2007 |
|
| 86 - Fix for crash when processing blocks with a percent sign in them. |
|
| 87 - **0.21** -- 27 Dec 2007 |
|
| 88 - Fix for combined strong and emphasis tags |
|
| 89 - **0.20** -- 13 Oct 2007 |
|
| 90 - Fix for < as well in image titles, now matches Dingus behavior |
|
| 91 - **0.19** -- 28 Sep 2007 |
|
| 92 - Fix for quotation marks " and ampersands & in link and image titles. |
|
| 93 - **0.18** -- 28 Jul 2007 |
|
| 94 - Does not crash on unmatched tags (behaves like standard markdown) |
|
| 95 - **0.17** -- 12 Apr 2007 |
|
| 96 - Fix for links with %20 in them. |
|
| 97 - **0.16** -- 12 Apr 2007 |
|
| 98 - Do not require arg global to exist. |
|
| 99 - **0.15** -- 28 Aug 2006 |
|
| 100 - Better handling of links with underscores in them. |
|
| 101 - **0.14** -- 22 Aug 2006 |
|
| 102 - Bug for *`foo()`* |
|
| 103 - **0.13** -- 12 Aug 2006 |
|
| 104 - Added -l option for including stylesheet inline in document. |
|
| 105 - Fixed bug in -s flag. |
|
| 106 - Fixed emphasis bug. |
|
| 107 - **0.12** -- 15 May 2006 |
|
| 108 - Fixed several bugs to comply with MarkdownTest 1.0 <http://six.pairlist.net/pipermail/markdown-discuss/2004-December/000909.html> |
|
| 109 - **0.11** -- 12 May 2006 |
|
| 110 - Fixed bug for escaping `*` and `_` inside code spans. |
|
| 111 - Added license terms. |
|
| 112 - Changed join() to table.concat(). |
|
| 113 - **0.10** -- 3 May 2006 |
|
| 114 - Initial public release. |
|
| 115 |
|
| 116 // Niklas |
|
| 117 ]] |
|
| 118 |
|
| 119 |
|
| 120 -- Set up a table for holding local functions to avoid polluting the global namespace |
|
| 121 local M = {} |
|
| 122 local MT = {__index = _G} |
|
| 123 setmetatable(M, MT) |
|
| 124 setfenv(1, M) |
|
| 125 |
|
| 126 ---------------------------------------------------------------------- |
3 ---------------------------------------------------------------------- |
| 127 -- Utility functions |
4 -- Utility functions |
| 128 ---------------------------------------------------------------------- |
5 ---------------------------------------------------------------------- |
| 129 |
6 |
| 130 -- Locks table t from changes, writes an error if someone attempts to change the table. |
7 local unpack = table.unpack or unpack |
| 131 -- This is useful for detecting variables that have "accidently" been made global. Something |
|
| 132 -- I tend to do all too much. |
|
| 133 function lock(t) |
|
| 134 function lock_new_index(t, k, v) |
|
| 135 error("module has been locked -- " .. k .. " must be declared local", 2) |
|
| 136 end |
|
| 137 |
|
| 138 local mt = {__newindex = lock_new_index} |
|
| 139 if getmetatable(t) then mt.__index = getmetatable(t).__index end |
|
| 140 setmetatable(t, mt) |
|
| 141 end |
|
| 142 |
8 |
| 143 -- Returns the result of mapping the values in table t through the function f |
9 -- Returns the result of mapping the values in table t through the function f |
| 144 function map(t, f) |
10 local function map(t, f) |
| 145 local out = {} |
11 local out = {} |
| 146 for k,v in pairs(t) do out[k] = f(v,k) end |
12 for k,v in pairs(t) do out[k] = f(v,k) end |
| 147 return out |
13 return out |
| 148 end |
14 end |
| 149 |
|
| 150 -- The identity function, useful as a placeholder. |
|
| 151 function identity(text) return text end |
|
| 152 |
15 |
| 153 -- Functional style if statement. (NOTE: no short circuit evaluation) |
16 -- Functional style if statement. (NOTE: no short circuit evaluation) |
| 154 function iff(t, a, b) if t then return a else return b end end |
17 local function iff(t, a, b) if t then return a else return b end end |
| 155 |
18 |
| 156 -- Splits the text into an array of separate lines. |
19 -- Splits the text into an array of separate lines. |
| 157 function split(text, sep) |
20 local function split(text, sep) |
| 158 sep = sep or "\n" |
21 sep = sep or "\n" |
| 159 local lines = {} |
22 local lines = {} |
| 160 local pos = 1 |
23 local pos = 1 |
| 161 while true do |
24 while true do |
| 162 local b,e = text:find(sep, pos) |
25 local b,e = text:find(sep, pos) |
| 163 if not b then table.insert(lines, text:sub(pos)) break end |
26 if not b then table.insert(lines, text:sub(pos)) break end |
| 164 table.insert(lines, text:sub(pos, b-1)) |
27 table.insert(lines, text:sub(pos, b-1)) |
| 165 pos = e + 1 |
28 pos = e + 1 |
| 166 end |
29 end |
| 167 return lines |
30 return lines |
| 168 end |
31 end |
| 169 |
32 |
| 170 -- Converts tabs to spaces |
33 -- Converts tabs to spaces |
| 171 function detab(text) |
34 local function detab(text) |
| 172 local tab_width = 4 |
35 local tab_width = 4 |
| 173 local function rep(match) |
36 local function rep(match) |
| 174 local spaces = -match:len() |
37 local spaces = -match:len() |
| 175 while spaces<1 do spaces = spaces + tab_width end |
38 while spaces<1 do spaces = spaces + tab_width end |
| 176 return match .. string.rep(" ", spaces) |
39 return match .. string.rep(" ", spaces) |
| 177 end |
40 end |
| 178 text = text:gsub("([^\n]-)\t", rep) |
41 text = text:gsub("([^\n]-)\t", rep) |
| 179 return text |
42 return text |
| 180 end |
43 end |
| 181 |
44 |
| 182 -- Applies string.find for every pattern in the list and returns the first match |
45 -- Applies string.find for every pattern in the list and returns the first match |
| 183 function find_first(s, patterns, index) |
46 local function find_first(s, patterns, index) |
| 184 local res = {} |
47 local res = {} |
| 185 for _,p in ipairs(patterns) do |
48 for _,p in ipairs(patterns) do |
| 186 local match = {s:find(p, index)} |
49 local match = {s:find(p, index)} |
| 187 if #match>0 and (#res==0 or match[1] < res[1]) then res = match end |
50 if #match>0 and (#res==0 or match[1] < res[1]) then res = match end |
| 188 end |
51 end |
| 189 return table.unpack(res) |
52 return unpack(res) |
| 190 end |
53 end |
| 191 |
54 |
| 192 -- If a replacement array is specified, the range [start, stop] in the array is replaced |
55 -- If a replacement array is specified, the range [start, stop] in the array is replaced |
| 193 -- with the replacement array and the resulting array is returned. Without a replacement |
56 -- with the replacement array and the resulting array is returned. Without a replacement |
| 194 -- array the section of the array between start and stop is returned. |
57 -- array the section of the array between start and stop is returned. |
| 195 function splice(array, start, stop, replacement) |
58 local function splice(array, start, stop, replacement) |
| 196 if replacement then |
59 if replacement then |
| 197 local n = stop - start + 1 |
60 local n = stop - start + 1 |
| 198 while n > 0 do |
61 while n > 0 do |
| 199 table.remove(array, start) |
62 table.remove(array, start) |
| 200 n = n - 1 |
63 n = n - 1 |
| 201 end |
64 end |
| 202 for i,v in ipairs(replacement) do |
65 for _,v in ipairs(replacement) do |
| 203 table.insert(array, start, v) |
66 table.insert(array, start, v) |
| 204 end |
67 end |
| 205 return array |
68 return array |
| 206 else |
69 else |
| 207 local res = {} |
70 local res = {} |
| 208 for i = start,stop do |
71 for i = start,stop do |
| 209 table.insert(res, array[i]) |
72 table.insert(res, array[i]) |
| 210 end |
73 end |
| 211 return res |
74 return res |
| 212 end |
75 end |
| 213 end |
76 end |
| 214 |
77 |
| 215 -- Outdents the text one step. |
78 -- Outdents the text one step. |
| 216 function outdent(text) |
79 local function outdent(text) |
| 217 text = "\n" .. text |
80 text = "\n" .. text |
| 218 text = text:gsub("\n ? ? ?", "\n") |
81 text = text:gsub("\n ? ? ?", "\n") |
| 219 text = text:sub(2) |
82 text = text:sub(2) |
| 220 return text |
83 return text |
| 221 end |
84 end |
| 222 |
85 |
| 223 -- Indents the text one step. |
86 -- Indents the text one step. |
| 224 function indent(text) |
87 local function indent(text) |
| 225 text = text:gsub("\n", "\n ") |
88 text = text:gsub("\n", "\n ") |
| 226 return text |
89 return text |
| 227 end |
90 end |
| 228 |
91 |
| 229 -- Does a simple tokenization of html data. Returns the data as a list of tokens. |
92 -- Does a simple tokenization of html data. Returns the data as a list of tokens. |
| 230 -- Each token is a table with a type field (which is either "tag" or "text") and |
93 -- Each token is a table with a type field (which is either "tag" or "text") and |
| 231 -- a text field (which contains the original token data). |
94 -- a text field (which contains the original token data). |
| 232 function tokenize_html(html) |
95 local function tokenize_html(html) |
| 233 local tokens = {} |
96 local tokens = {} |
| 234 local pos = 1 |
97 local pos = 1 |
| 235 while true do |
98 while true do |
| 236 local start = find_first(html, {"<!%-%-", "<[a-z/!$]", "<%?"}, pos) |
99 local start = find_first(html, {"<!%-%-", "<[a-z/!$]", "<%?"}, pos) |
| 237 if not start then |
100 if not start then |
| 238 table.insert(tokens, {type="text", text=html:sub(pos)}) |
101 table.insert(tokens, {type="text", text=html:sub(pos)}) |
| 239 break |
102 break |
| 240 end |
103 end |
| 241 if start ~= pos then table.insert(tokens, {type="text", text = html:sub(pos, start-1)}) end |
104 if start ~= pos then table.insert(tokens, {type="text", text = html:sub(pos, start-1)}) end |
| 242 |
105 |
| 243 local _, stop |
106 local _, stop |
| 244 if html:match("^<!%-%-", start) then |
107 if html:match("^<!%-%-", start) then |
| 245 _,stop = html:find("%-%->", start) |
108 _,stop = html:find("%-%->", start) |
| 246 elseif html:match("^<%?", start) then |
109 elseif html:match("^<%?", start) then |
| 247 _,stop = html:find("?>", start) |
110 _,stop = html:find("?>", start) |
| 248 else |
111 else |
| 249 _,stop = html:find("%b<>", start) |
112 _,stop = html:find("%b<>", start) |
| 250 end |
113 end |
| 251 if not stop then |
114 if not stop then |
| 252 -- error("Could not match html tag " .. html:sub(start,start+30)) |
115 -- error("Could not match html tag " .. html:sub(start,start+30)) |
| 253 table.insert(tokens, {type="text", text=html:sub(start, start)}) |
116 table.insert(tokens, {type="text", text=html:sub(start, start)}) |
| 254 pos = start + 1 |
117 pos = start + 1 |
| 255 else |
118 else |
| 256 table.insert(tokens, {type="tag", text=html:sub(start, stop)}) |
119 table.insert(tokens, {type="tag", text=html:sub(start, stop)}) |
| 257 pos = stop + 1 |
120 pos = stop + 1 |
| 258 end |
121 end |
| 259 end |
122 end |
| 260 return tokens |
123 return tokens |
| 261 end |
124 end |
| 262 |
125 |
| 263 ---------------------------------------------------------------------- |
126 ---------------------------------------------------------------------- |
| 264 -- Hash |
127 -- Hash |
| 265 ---------------------------------------------------------------------- |
128 ---------------------------------------------------------------------- |
| 408 -- characters. |
271 -- characters. |
| 409 |
272 |
| 410 -- Returns true if the line is a ruler of (char) characters. |
273 -- Returns true if the line is a ruler of (char) characters. |
| 411 -- The line must contain at least three char characters and contain only spaces and |
274 -- The line must contain at least three char characters and contain only spaces and |
| 412 -- char characters. |
275 -- char characters. |
| 413 function is_ruler_of(line, char) |
276 local function is_ruler_of(line, char) |
| 414 if not line:match("^[ %" .. char .. "]*$") then return false end |
277 if not line:match("^[ %" .. char .. "]*$") then return false end |
| 415 if not line:match("%" .. char .. ".*%" .. char .. ".*%" .. char) then return false end |
278 if not line:match("%" .. char .. ".*%" .. char .. ".*%" .. char) then return false end |
| 416 return true |
279 return true |
| 417 end |
280 end |
| 418 |
281 |
| 419 -- Identifies the block level formatting present in the line |
282 -- Identifies the block level formatting present in the line |
| 420 function classify(line) |
283 local function classify(line) |
| 421 local info = {line = line, text = line} |
284 local info = {line = line, text = line} |
| 422 |
285 |
| 423 if line:match("^ ") then |
286 if line:match("^ ") then |
| 424 info.type = "indented" |
287 info.type = "indented" |
| 425 info.outdented = line:sub(5) |
288 info.outdented = line:sub(5) |
| 426 return info |
289 return info |
| 427 end |
290 end |
| 428 |
291 |
| 429 for _,c in ipairs({'*', '-', '_', '='}) do |
292 for _,c in ipairs({'*', '-', '_', '='}) do |
| 430 if is_ruler_of(line, c) then |
293 if is_ruler_of(line, c) then |
| 431 info.type = "ruler" |
294 info.type = "ruler" |
| 432 info.ruler_char = c |
295 info.ruler_char = c |
| 433 return info |
296 return info |
| 434 end |
297 end |
| 435 end |
298 end |
| 436 |
299 |
| 437 if line == "" then |
300 if line == "" then |
| 438 info.type = "blank" |
301 info.type = "blank" |
| 439 return info |
302 return info |
| 440 end |
303 end |
| 441 |
304 |
| 442 if line:match("^(#+)[ \t]*(.-)[ \t]*#*[ \t]*$") then |
305 if line:match("^(#+)[ \t]*(.-)[ \t]*#*[ \t]*$") then |
| 443 local m1, m2 = line:match("^(#+)[ \t]*(.-)[ \t]*#*[ \t]*$") |
306 local m1, m2 = line:match("^(#+)[ \t]*(.-)[ \t]*#*[ \t]*$") |
| 444 info.type = "header" |
307 info.type = "header" |
| 445 info.level = m1:len() |
308 info.level = m1:len() |
| 446 info.text = m2 |
309 info.text = m2 |
| 447 return info |
310 return info |
| 448 end |
311 end |
| 449 |
312 |
| 450 if line:match("^ ? ? ?(%d+)%.[ \t]+(.+)") then |
313 if line:match("^ ? ? ?(%d+)%.[ \t]+(.+)") then |
| 451 local number, text = line:match("^ ? ? ?(%d+)%.[ \t]+(.+)") |
314 local number, text = line:match("^ ? ? ?(%d+)%.[ \t]+(.+)") |
| 452 info.type = "list_item" |
315 info.type = "list_item" |
| 453 info.list_type = "numeric" |
316 info.list_type = "numeric" |
| 454 info.number = 0 + number |
317 info.number = 0 + number |
| 455 info.text = text |
318 info.text = text |
| 456 return info |
319 return info |
| 457 end |
320 end |
| 458 |
321 |
| 459 if line:match("^ ? ? ?([%*%+%-])[ \t]+(.+)") then |
322 if line:match("^ ? ? ?([%*%+%-])[ \t]+(.+)") then |
| 460 local bullet, text = line:match("^ ? ? ?([%*%+%-])[ \t]+(.+)") |
323 local bullet, text = line:match("^ ? ? ?([%*%+%-])[ \t]+(.+)") |
| 461 info.type = "list_item" |
324 info.type = "list_item" |
| 462 info.list_type = "bullet" |
325 info.list_type = "bullet" |
| 463 info.bullet = bullet |
326 info.bullet = bullet |
| 464 info.text= text |
327 info.text= text |
| 465 return info |
328 return info |
| 466 end |
329 end |
| 467 |
330 |
| 468 if line:match("^>[ \t]?(.*)") then |
331 if line:match("^>[ \t]?(.*)") then |
| 469 info.type = "blockquote" |
332 info.type = "blockquote" |
| 470 info.text = line:match("^>[ \t]?(.*)") |
333 info.text = line:match("^>[ \t]?(.*)") |
| 471 return info |
334 return info |
| 472 end |
335 end |
| 473 |
336 |
| 474 if is_protected(line) then |
337 if is_protected(line) then |
| 475 info.type = "raw" |
338 info.type = "raw" |
| 476 info.html = unprotect(line) |
339 info.html = unprotect(line) |
| 477 return info |
340 return info |
| 478 end |
341 end |
| 479 |
342 |
| 480 info.type = "normal" |
343 info.type = "normal" |
| 481 return info |
344 return info |
| 482 end |
345 end |
| 483 |
346 |
| 484 -- Find headers constisting of a normal line followed by a ruler and converts them to |
347 -- Find headers constisting of a normal line followed by a ruler and converts them to |
| 485 -- header entries. |
348 -- header entries. |
| 486 function headers(array) |
349 local function headers(array) |
| 487 local i = 1 |
350 local i = 1 |
| 488 while i <= #array - 1 do |
351 while i <= #array - 1 do |
| 489 if array[i].type == "normal" and array[i+1].type == "ruler" and |
352 if array[i].type == "normal" and array[i+1].type == "ruler" and |
| 490 (array[i+1].ruler_char == "-" or array[i+1].ruler_char == "=") then |
353 (array[i+1].ruler_char == "-" or array[i+1].ruler_char == "=") then |
| 491 local info = {line = array[i].line} |
354 local info = {line = array[i].line} |
| 492 info.text = info.line |
355 info.text = info.line |
| 493 info.type = "header" |
356 info.type = "header" |
| 494 info.level = iff(array[i+1].ruler_char == "=", 1, 2) |
357 info.level = iff(array[i+1].ruler_char == "=", 1, 2) |
| 495 table.remove(array, i+1) |
358 table.remove(array, i+1) |
| 496 array[i] = info |
359 array[i] = info |
| 497 end |
360 end |
| 498 i = i + 1 |
361 i = i + 1 |
| 499 end |
362 end |
| 500 return array |
363 return array |
| |
364 end |
| |
365 |
| |
366 -- Forward declarations |
| |
367 local block_transform, span_transform, encode_code |
| |
368 |
| |
369 -- Convert lines to html code |
| |
370 local function blocks_to_html(lines, no_paragraphs) |
| |
371 local out = {} |
| |
372 local i = 1 |
| |
373 while i <= #lines do |
| |
374 local line = lines[i] |
| |
375 if line.type == "ruler" then |
| |
376 table.insert(out, "<hr/>") |
| |
377 elseif line.type == "raw" then |
| |
378 table.insert(out, line.html) |
| |
379 elseif line.type == "normal" then |
| |
380 local s = line.line |
| |
381 |
| |
382 while i+1 <= #lines and lines[i+1].type == "normal" do |
| |
383 i = i + 1 |
| |
384 s = s .. "\n" .. lines[i].line |
| |
385 end |
| |
386 |
| |
387 if no_paragraphs then |
| |
388 table.insert(out, span_transform(s)) |
| |
389 else |
| |
390 table.insert(out, "<p>" .. span_transform(s) .. "</p>") |
| |
391 end |
| |
392 elseif line.type == "header" then |
| |
393 local s = "<h" .. line.level .. ">" .. span_transform(line.text) .. "</h" .. line.level .. ">" |
| |
394 table.insert(out, s) |
| |
395 else |
| |
396 table.insert(out, line.line) |
| |
397 end |
| |
398 i = i + 1 |
| |
399 end |
| |
400 return out |
| 501 end |
401 end |
| 502 |
402 |
| 503 -- Find list blocks and convert them to protected data blocks |
403 -- Find list blocks and convert them to protected data blocks |
| 504 function lists(array, sublist) |
404 local function lists(array, sublist) |
| 505 local function process_list(arr) |
405 local function process_list(arr) |
| 506 local function any_blanks(arr) |
406 local function any_blanks(arr) |
| 507 for i = 1, #arr do |
407 for i = 1, #arr do |
| 508 if arr[i].type == "blank" then return true end |
408 if arr[i].type == "blank" then return true end |
| 509 end |
409 end |
| 510 return false |
410 return false |
| 511 end |
411 end |
| 512 |
412 |
| 513 local function split_list_items(arr) |
413 local function split_list_items(arr) |
| 514 local acc = {arr[1]} |
414 local acc = {arr[1]} |
| 515 local res = {} |
415 local res = {} |
| 516 for i=2,#arr do |
416 for i=2,#arr do |
| 517 if arr[i].type == "list_item" then |
417 if arr[i].type == "list_item" then |
| 518 table.insert(res, acc) |
418 table.insert(res, acc) |
| 519 acc = {arr[i]} |
419 acc = {arr[i]} |
| 520 else |
420 else |
| 521 table.insert(acc, arr[i]) |
421 table.insert(acc, arr[i]) |
| 522 end |
422 end |
| 523 end |
423 end |
| 524 table.insert(res, acc) |
424 table.insert(res, acc) |
| 525 return res |
425 return res |
| 526 end |
426 end |
| 527 |
427 |
| 528 local function process_list_item(lines, block) |
428 local function process_list_item(lines, block) |
| 529 while lines[#lines].type == "blank" do |
429 while lines[#lines].type == "blank" do |
| 530 table.remove(lines) |
430 table.remove(lines) |
| 531 end |
431 end |
| 532 |
432 |
| 533 local itemtext = lines[1].text |
433 local itemtext = lines[1].text |
| 534 for i=2,#lines do |
434 for i=2,#lines do |
| 535 itemtext = itemtext .. "\n" .. outdent(lines[i].line) |
435 itemtext = itemtext .. "\n" .. outdent(lines[i].line) |
| 536 end |
436 end |
| 537 if block then |
437 if block then |
| 538 itemtext = block_transform(itemtext, true) |
438 itemtext = block_transform(itemtext, true) |
| 539 if not itemtext:find("<pre>") then itemtext = indent(itemtext) end |
439 if not itemtext:find("<pre>") then itemtext = indent(itemtext) end |
| 540 return " <li>" .. itemtext .. "</li>" |
440 return " <li>" .. itemtext .. "</li>" |
| 541 else |
441 else |
| 542 local lines = split(itemtext) |
442 local lines = split(itemtext) |
| 543 lines = map(lines, classify) |
443 lines = map(lines, classify) |
| 544 lines = lists(lines, true) |
444 lines = lists(lines, true) |
| 545 lines = blocks_to_html(lines, true) |
445 lines = blocks_to_html(lines, true) |
| 546 itemtext = table.concat(lines, "\n") |
446 itemtext = table.concat(lines, "\n") |
| 547 if not itemtext:find("<pre>") then itemtext = indent(itemtext) end |
447 if not itemtext:find("<pre>") then itemtext = indent(itemtext) end |
| 548 return " <li>" .. itemtext .. "</li>" |
448 return " <li>" .. itemtext .. "</li>" |
| 549 end |
449 end |
| 550 end |
450 end |
| 551 |
451 |
| 552 local block_list = any_blanks(arr) |
452 local block_list = any_blanks(arr) |
| 553 local items = split_list_items(arr) |
453 local items = split_list_items(arr) |
| 554 local out = "" |
454 local out = "" |
| 555 for _, item in ipairs(items) do |
455 for _, item in ipairs(items) do |
| 556 out = out .. process_list_item(item, block_list) .. "\n" |
456 out = out .. process_list_item(item, block_list) .. "\n" |
| 557 end |
457 end |
| 558 if arr[1].list_type == "numeric" then |
458 if arr[1].list_type == "numeric" then |
| 559 return "<ol>\n" .. out .. "</ol>" |
459 return "<ol>\n" .. out .. "</ol>" |
| 560 else |
460 else |
| 561 return "<ul>\n" .. out .. "</ul>" |
461 return "<ul>\n" .. out .. "</ul>" |
| 562 end |
462 end |
| 563 end |
463 end |
| 564 |
464 |
| 565 -- Finds the range of lines composing the first list in the array. A list |
465 -- Finds the range of lines composing the first list in the array. A list |
| 566 -- starts with (^ list_item) or (blank list_item) and ends with |
466 -- starts with (^ list_item) or (blank list_item) and ends with |
| 567 -- (blank* $) or (blank normal). |
467 -- (blank* $) or (blank normal). |
| 568 -- |
468 -- |
| 569 -- A sublist can start with just (list_item) does not need a blank... |
469 -- A sublist can start with just (list_item) does not need a blank... |
| 570 local function find_list(array, sublist) |
470 local function find_list(array, sublist) |
| 571 local function find_list_start(array, sublist) |
471 local function find_list_start(array, sublist) |
| 572 if array[1].type == "list_item" then return 1 end |
472 if array[1].type == "list_item" then return 1 end |
| 573 if sublist then |
473 if sublist then |
| 574 for i = 1,#array do |
474 for i = 1,#array do |
| 575 if array[i].type == "list_item" then return i end |
475 if array[i].type == "list_item" then return i end |
| 576 end |
476 end |
| 577 else |
477 else |
| 578 for i = 1, #array-1 do |
478 for i = 1, #array-1 do |
| 579 if array[i].type == "blank" and array[i+1].type == "list_item" then |
479 if array[i].type == "blank" and array[i+1].type == "list_item" then |
| 580 return i+1 |
480 return i+1 |
| 581 end |
481 end |
| 582 end |
482 end |
| 583 end |
483 end |
| 584 return nil |
484 return nil |
| 585 end |
485 end |
| 586 local function find_list_end(array, start) |
486 local function find_list_end(array, start) |
| 587 local pos = #array |
487 local pos = #array |
| 588 for i = start, #array-1 do |
488 for i = start, #array-1 do |
| 589 if array[i].type == "blank" and array[i+1].type ~= "list_item" |
489 if array[i].type == "blank" and array[i+1].type ~= "list_item" |
| 590 and array[i+1].type ~= "indented" and array[i+1].type ~= "blank" then |
490 and array[i+1].type ~= "indented" and array[i+1].type ~= "blank" then |
| 591 pos = i-1 |
491 pos = i-1 |
| 592 break |
492 break |
| 593 end |
493 end |
| 594 end |
494 end |
| 595 while pos > start and array[pos].type == "blank" do |
495 while pos > start and array[pos].type == "blank" do |
| 596 pos = pos - 1 |
496 pos = pos - 1 |
| 597 end |
497 end |
| 598 return pos |
498 return pos |
| 599 end |
499 end |
| 600 |
500 |
| 601 local start = find_list_start(array, sublist) |
501 local start = find_list_start(array, sublist) |
| 602 if not start then return nil end |
502 if not start then return nil end |
| 603 return start, find_list_end(array, start) |
503 return start, find_list_end(array, start) |
| 604 end |
504 end |
| 605 |
505 |
| 606 while true do |
506 while true do |
| 607 local start, stop = find_list(array, sublist) |
507 local start, stop = find_list(array, sublist) |
| 608 if not start then break end |
508 if not start then break end |
| 609 local text = process_list(splice(array, start, stop)) |
509 local text = process_list(splice(array, start, stop)) |
| 610 local info = { |
510 local info = { |
| 611 line = text, |
511 line = text, |
| 612 type = "raw", |
512 type = "raw", |
| 613 html = text |
513 html = text |
| 614 } |
514 } |
| 615 array = splice(array, start, stop, {info}) |
515 array = splice(array, start, stop, {info}) |
| 616 end |
516 end |
| 617 |
517 |
| 618 -- Convert any remaining list items to normal |
518 -- Convert any remaining list items to normal |
| 619 for _,line in ipairs(array) do |
519 for _,line in ipairs(array) do |
| 620 if line.type == "list_item" then line.type = "normal" end |
520 if line.type == "list_item" then line.type = "normal" end |
| 621 end |
521 end |
| 622 |
522 |
| 623 return array |
523 return array |
| 624 end |
524 end |
| 625 |
525 |
| 626 -- Find and convert blockquote markers. |
526 -- Find and convert blockquote markers. |
| 627 function blockquotes(lines) |
527 local function blockquotes(lines) |
| 628 local function find_blockquote(lines) |
528 local function find_blockquote(lines) |
| 629 local start |
529 local start |
| 630 for i,line in ipairs(lines) do |
530 for i,line in ipairs(lines) do |
| 631 if line.type == "blockquote" then |
531 if line.type == "blockquote" then |
| 632 start = i |
532 start = i |
| 633 break |
533 break |
| 634 end |
534 end |
| 635 end |
535 end |
| 636 if not start then return nil end |
536 if not start then return nil end |
| 637 |
537 |
| 638 local stop = #lines |
538 local stop = #lines |
| 639 for i = start+1, #lines do |
539 for i = start+1, #lines do |
| 640 if lines[i].type == "blank" or lines[i].type == "blockquote" then |
540 if lines[i].type == "blank" or lines[i].type == "blockquote" then |
| 641 elseif lines[i].type == "normal" then |
541 elseif lines[i].type == "normal" then |
| 642 if lines[i-1].type == "blank" then stop = i-1 break end |
542 if lines[i-1].type == "blank" then stop = i-1 break end |
| 643 else |
543 else |
| 644 stop = i-1 break |
544 stop = i-1 break |
| 645 end |
545 end |
| 646 end |
546 end |
| 647 while lines[stop].type == "blank" do stop = stop - 1 end |
547 while lines[stop].type == "blank" do stop = stop - 1 end |
| 648 return start, stop |
548 return start, stop |
| 649 end |
549 end |
| 650 |
550 |
| 651 local function process_blockquote(lines) |
551 local function process_blockquote(lines) |
| 652 local raw = lines[1].text |
552 local raw = lines[1].text |
| 653 for i = 2,#lines do |
553 for i = 2,#lines do |
| 654 raw = raw .. "\n" .. lines[i].text |
554 raw = raw .. "\n" .. lines[i].text |
| 655 end |
555 end |
| 656 local bt = block_transform(raw) |
556 local bt = block_transform(raw) |
| 657 if not bt:find("<pre>") then bt = indent(bt) end |
557 if not bt:find("<pre>") then bt = indent(bt) end |
| 658 return "<blockquote>\n " .. bt .. |
558 return "<blockquote>\n " .. bt .. |
| 659 "\n</blockquote>" |
559 "\n</blockquote>" |
| 660 end |
560 end |
| 661 |
561 |
| 662 while true do |
562 while true do |
| 663 local start, stop = find_blockquote(lines) |
563 local start, stop = find_blockquote(lines) |
| 664 if not start then break end |
564 if not start then break end |
| 665 local text = process_blockquote(splice(lines, start, stop)) |
565 local text = process_blockquote(splice(lines, start, stop)) |
| 666 local info = { |
566 local info = { |
| 667 line = text, |
567 line = text, |
| 668 type = "raw", |
568 type = "raw", |
| 669 html = text |
569 html = text |
| 670 } |
570 } |
| 671 lines = splice(lines, start, stop, {info}) |
571 lines = splice(lines, start, stop, {info}) |
| 672 end |
572 end |
| 673 return lines |
573 return lines |
| 674 end |
574 end |
| 675 |
575 |
| 676 -- Find and convert codeblocks. |
576 -- Find and convert codeblocks. |
| 677 function codeblocks(lines) |
577 local function codeblocks(lines) |
| 678 local function find_codeblock(lines) |
578 local function find_codeblock(lines) |
| 679 local start |
579 local start |
| 680 for i,line in ipairs(lines) do |
580 for i,line in ipairs(lines) do |
| 681 if line.type == "indented" then start = i break end |
581 if line.type == "indented" then start = i break end |
| 682 end |
582 end |
| 683 if not start then return nil end |
583 if not start then return nil end |
| 684 |
584 |
| 685 local stop = #lines |
585 local stop = #lines |
| 686 for i = start+1, #lines do |
586 for i = start+1, #lines do |
| 687 if lines[i].type ~= "indented" and lines[i].type ~= "blank" then |
587 if lines[i].type ~= "indented" and lines[i].type ~= "blank" then |
| 688 stop = i-1 |
588 stop = i-1 |
| 689 break |
589 break |
| 690 end |
590 end |
| 691 end |
591 end |
| 692 while lines[stop].type == "blank" do stop = stop - 1 end |
592 while lines[stop].type == "blank" do stop = stop - 1 end |
| 693 return start, stop |
593 return start, stop |
| 694 end |
594 end |
| 695 |
595 |
| 696 local function process_codeblock(lines) |
596 local function process_codeblock(lines) |
| 697 local raw = detab(encode_code(outdent(lines[1].line))) |
597 local raw = detab(encode_code(outdent(lines[1].line))) |
| 698 for i = 2,#lines do |
598 for i = 2,#lines do |
| 699 raw = raw .. "\n" .. detab(encode_code(outdent(lines[i].line))) |
599 raw = raw .. "\n" .. detab(encode_code(outdent(lines[i].line))) |
| 700 end |
600 end |
| 701 return "<pre><code>" .. raw .. "\n</code></pre>" |
601 return "<pre><code>" .. raw .. "\n</code></pre>" |
| 702 end |
602 end |
| 703 |
603 |
| 704 while true do |
604 while true do |
| 705 local start, stop = find_codeblock(lines) |
605 local start, stop = find_codeblock(lines) |
| 706 if not start then break end |
606 if not start then break end |
| 707 local text = process_codeblock(splice(lines, start, stop)) |
607 local text = process_codeblock(splice(lines, start, stop)) |
| 708 local info = { |
608 local info = { |
| 709 line = text, |
609 line = text, |
| 710 type = "raw", |
610 type = "raw", |
| 711 html = text |
611 html = text |
| 712 } |
612 } |
| 713 lines = splice(lines, start, stop, {info}) |
613 lines = splice(lines, start, stop, {info}) |
| 714 end |
614 end |
| 715 return lines |
615 return lines |
| 716 end |
|
| 717 |
|
| 718 -- Convert lines to html code |
|
| 719 function blocks_to_html(lines, no_paragraphs) |
|
| 720 local out = {} |
|
| 721 local i = 1 |
|
| 722 while i <= #lines do |
|
| 723 local line = lines[i] |
|
| 724 if line.type == "ruler" then |
|
| 725 table.insert(out, "<hr/>") |
|
| 726 elseif line.type == "raw" then |
|
| 727 table.insert(out, line.html) |
|
| 728 elseif line.type == "normal" then |
|
| 729 local s = line.line |
|
| 730 |
|
| 731 while i+1 <= #lines and lines[i+1].type == "normal" do |
|
| 732 i = i + 1 |
|
| 733 s = s .. "\n" .. lines[i].line |
|
| 734 end |
|
| 735 |
|
| 736 if no_paragraphs then |
|
| 737 table.insert(out, span_transform(s)) |
|
| 738 else |
|
| 739 table.insert(out, "<p>" .. span_transform(s) .. "</p>") |
|
| 740 end |
|
| 741 elseif line.type == "header" then |
|
| 742 local s = "<h" .. line.level .. ">" .. span_transform(line.text) .. "</h" .. line.level .. ">" |
|
| 743 table.insert(out, s) |
|
| 744 else |
|
| 745 table.insert(out, line.line) |
|
| 746 end |
|
| 747 i = i + 1 |
|
| 748 end |
|
| 749 return out |
|
| 750 end |
616 end |
| 751 |
617 |
| 752 -- Perform all the block level transforms |
618 -- Perform all the block level transforms |
| 753 function block_transform(text, sublist) |
619 function block_transform(text, sublist) |
| 754 local lines = split(text) |
620 local lines = split(text) |
| 755 lines = map(lines, classify) |
621 lines = map(lines, classify) |
| 756 lines = headers(lines) |
622 lines = headers(lines) |
| 757 lines = lists(lines, sublist) |
623 lines = lists(lines, sublist) |
| 758 lines = codeblocks(lines) |
624 lines = codeblocks(lines) |
| 759 lines = blockquotes(lines) |
625 lines = blockquotes(lines) |
| 760 lines = blocks_to_html(lines) |
626 lines = blocks_to_html(lines) |
| 761 local text = table.concat(lines, "\n") |
627 local text = table.concat(lines, "\n") |
| 762 return text |
628 return text |
| 763 end |
|
| 764 |
|
| 765 -- Debug function for printing a line array to see the result |
|
| 766 -- of partial transforms. |
|
| 767 function print_lines(lines) |
|
| 768 for i, line in ipairs(lines) do |
|
| 769 print(i, line.type, line.text or line.line) |
|
| 770 end |
|
| 771 end |
629 end |
| 772 |
630 |
| 773 ---------------------------------------------------------------------- |
631 ---------------------------------------------------------------------- |
| 774 -- Span transform |
632 -- Span transform |
| 775 ---------------------------------------------------------------------- |
633 ---------------------------------------------------------------------- |
| 776 |
634 |
| 777 -- Functions for transforming the text at the span level. |
635 -- Functions for transforming the text at the span level. |
| 778 |
636 |
| 779 -- These characters may need to be escaped because they have a special |
637 -- These characters may need to be escaped because they have a special |
| 780 -- meaning in markdown. |
638 -- meaning in markdown. |
| 781 escape_chars = "'\\`*_{}[]()>#+-.!'" |
639 local escape_chars = "'\\`*_{}[]()>#+-.!'" |
| 782 escape_table = {} |
640 local escape_table = {} |
| 783 |
641 |
| 784 function init_escape_table() |
642 local function init_escape_table() |
| 785 escape_table = {} |
643 escape_table = {} |
| 786 for i = 1,#escape_chars do |
644 for i = 1,#escape_chars do |
| 787 local c = escape_chars:sub(i,i) |
645 local c = escape_chars:sub(i,i) |
| 788 escape_table[c] = hash(c) |
646 escape_table[c] = hash(c) |
| 789 end |
647 end |
| 790 end |
648 end |
| 791 |
649 |
| 792 -- Adds a new escape to the escape table. |
650 -- Adds a new escape to the escape table. |
| 793 function add_escape(text) |
651 local function add_escape(text) |
| 794 if not escape_table[text] then |
652 if not escape_table[text] then |
| 795 escape_table[text] = hash(text) |
653 escape_table[text] = hash(text) |
| 796 end |
654 end |
| 797 return escape_table[text] |
655 return escape_table[text] |
| 798 end |
656 end |
| |
657 |
| |
658 -- Encode backspace-escaped characters in the markdown source. |
| |
659 local function encode_backslash_escapes(t) |
| |
660 for i=1,escape_chars:len() do |
| |
661 local c = escape_chars:sub(i,i) |
| |
662 t = t:gsub("\\%" .. c, escape_table[c]) |
| |
663 end |
| |
664 return t |
| |
665 end |
| 799 |
666 |
| 800 -- Escape characters that should not be disturbed by markdown. |
667 -- Escape characters that should not be disturbed by markdown. |
| 801 function escape_special_chars(text) |
668 local function escape_special_chars(text) |
| 802 local tokens = tokenize_html(text) |
669 local tokens = tokenize_html(text) |
| 803 |
670 |
| 804 local out = "" |
671 local out = "" |
| 805 for _, token in ipairs(tokens) do |
672 for _, token in ipairs(tokens) do |
| 806 local t = token.text |
673 local t = token.text |
| 807 if token.type == "tag" then |
674 if token.type == "tag" then |
| 808 -- In tags, encode * and _ so they don't conflict with their use in markdown. |
675 -- In tags, encode * and _ so they don't conflict with their use in markdown. |
| 809 t = t:gsub("%*", escape_table["*"]) |
676 t = t:gsub("%*", escape_table["*"]) |
| 810 t = t:gsub("%_", escape_table["_"]) |
677 t = t:gsub("%_", escape_table["_"]) |
| 811 else |
678 else |
| 812 t = encode_backslash_escapes(t) |
679 t = encode_backslash_escapes(t) |
| 813 end |
680 end |
| 814 out = out .. t |
681 out = out .. t |
| 815 end |
682 end |
| 816 return out |
683 return out |
| 817 end |
|
| 818 |
|
| 819 -- Encode backspace-escaped characters in the markdown source. |
|
| 820 function encode_backslash_escapes(t) |
|
| 821 for i=1,escape_chars:len() do |
|
| 822 local c = escape_chars:sub(i,i) |
|
| 823 t = t:gsub("\\%" .. c, escape_table[c]) |
|
| 824 end |
|
| 825 return t |
|
| 826 end |
684 end |
| 827 |
685 |
| 828 -- Unescape characters that have been encoded. |
686 -- Unescape characters that have been encoded. |
| 829 function unescape_special_chars(t) |
687 local function unescape_special_chars(t) |
| 830 local tin = t |
688 local tin = t |
| 831 for k,v in pairs(escape_table) do |
689 for k,v in pairs(escape_table) do |
| 832 k = k:gsub("%%", "%%%%") |
690 k = k:gsub("%%", "%%%%") |
| 833 t = t:gsub(v,k) |
691 t = t:gsub(v,k) |
| 834 end |
692 end |
| 835 if t ~= tin then t = unescape_special_chars(t) end |
693 if t ~= tin then t = unescape_special_chars(t) end |
| 836 return t |
694 return t |
| 837 end |
695 end |
| 838 |
696 |
| 839 -- Encode/escape certain characters inside Markdown code runs. |
697 -- Encode/escape certain characters inside Markdown code runs. |
| 840 -- The point is that in code, these characters are literals, |
698 -- The point is that in code, these characters are literals, |
| 841 -- and lose their special Markdown meanings. |
699 -- and lose their special Markdown meanings. |
| 842 function encode_code(s) |
700 function encode_code(s) |
| 843 s = s:gsub("%&", "&") |
701 s = s:gsub("%&", "&") |
| 844 s = s:gsub("<", "<") |
702 s = s:gsub("<", "<") |
| 845 s = s:gsub(">", ">") |
703 s = s:gsub(">", ">") |
| 846 for k,v in pairs(escape_table) do |
704 for k,v in pairs(escape_table) do |
| 847 s = s:gsub("%"..k, v) |
705 s = s:gsub("%"..k, v) |
| 848 end |
706 end |
| 849 return s |
707 return s |
| 850 end |
708 end |
| 851 |
709 |
| 852 -- Handle backtick blocks. |
710 -- Handle backtick blocks. |
| 853 function code_spans(s) |
711 local function code_spans(s) |
| 854 s = s:gsub("\\\\", escape_table["\\"]) |
712 s = s:gsub("\\\\", escape_table["\\"]) |
| 855 s = s:gsub("\\`", escape_table["`"]) |
713 s = s:gsub("\\`", escape_table["`"]) |
| 856 |
714 |
| 857 local pos = 1 |
715 local pos = 1 |
| 858 while true do |
716 while true do |
| 859 local start, stop = s:find("`+", pos) |
717 local start, stop = s:find("`+", pos) |
| 860 if not start then return s end |
718 if not start then return s end |
| 861 local count = stop - start + 1 |
719 local count = stop - start + 1 |
| 862 -- Find a matching numbert of backticks |
720 -- Find a matching numbert of backticks |
| 863 local estart, estop = s:find(string.rep("`", count), stop+1) |
721 local estart, estop = s:find(string.rep("`", count), stop+1) |
| 864 local brstart = s:find("\n", stop+1) |
722 local brstart = s:find("\n", stop+1) |
| 865 if estart and (not brstart or estart < brstart) then |
723 if estart and (not brstart or estart < brstart) then |
| 866 local code = s:sub(stop+1, estart-1) |
724 local code = s:sub(stop+1, estart-1) |
| 867 code = code:gsub("^[ \t]+", "") |
725 code = code:gsub("^[ \t]+", "") |
| 868 code = code:gsub("[ \t]+$", "") |
726 code = code:gsub("[ \t]+$", "") |
| 869 code = code:gsub(escape_table["\\"], escape_table["\\"] .. escape_table["\\"]) |
727 code = code:gsub(escape_table["\\"], escape_table["\\"] .. escape_table["\\"]) |
| 870 code = code:gsub(escape_table["`"], escape_table["\\"] .. escape_table["`"]) |
728 code = code:gsub(escape_table["`"], escape_table["\\"] .. escape_table["`"]) |
| 871 code = "<code>" .. encode_code(code) .. "</code>" |
729 code = "<code>" .. encode_code(code) .. "</code>" |
| 872 code = add_escape(code) |
730 code = add_escape(code) |
| 873 s = s:sub(1, start-1) .. code .. s:sub(estop+1) |
731 s = s:sub(1, start-1) .. code .. s:sub(estop+1) |
| 874 pos = start + code:len() |
732 pos = start + code:len() |
| 875 else |
733 else |
| 876 pos = stop + 1 |
734 pos = stop + 1 |
| 877 end |
735 end |
| 878 end |
736 end |
| 879 return s |
737 return s |
| 880 end |
738 end |
| 881 |
739 |
| 882 -- Encode alt text... enodes &, and ". |
740 -- Encode alt text... enodes &, and ". |
| 883 function encode_alt(s) |
741 local function encode_alt(s) |
| 884 if not s then return s end |
742 if not s then return s end |
| 885 s = s:gsub('&', '&') |
743 s = s:gsub('&', '&') |
| 886 s = s:gsub('"', '"') |
744 s = s:gsub('"', '"') |
| 887 s = s:gsub('<', '<') |
745 s = s:gsub('<', '<') |
| 888 return s |
746 return s |
| 889 end |
747 end |
| |
748 |
| |
749 -- Forward declaration for link_db as returned by strip_link_definitions. |
| |
750 local link_database |
| 890 |
751 |
| 891 -- Handle image references |
752 -- Handle image references |
| 892 function images(text) |
753 local function images(text) |
| 893 local function reference_link(alt, id) |
754 local function reference_link(alt, id) |
| 894 alt = encode_alt(alt:match("%b[]"):sub(2,-2)) |
755 alt = encode_alt(alt:match("%b[]"):sub(2,-2)) |
| 895 id = id:match("%[(.*)%]"):lower() |
756 id = id:match("%[(.*)%]"):lower() |
| 896 if id == "" then id = text:lower() end |
757 if id == "" then id = text:lower() end |
| 897 link_database[id] = link_database[id] or {} |
758 link_database[id] = link_database[id] or {} |
| 898 if not link_database[id].url then return nil end |
759 if not link_database[id].url then return nil end |
| 899 local url = link_database[id].url or id |
760 local url = link_database[id].url or id |
| 900 url = encode_alt(url) |
761 url = encode_alt(url) |
| 901 local title = encode_alt(link_database[id].title) |
762 local title = encode_alt(link_database[id].title) |
| 902 if title then title = " title=\"" .. title .. "\"" else title = "" end |
763 if title then title = " title=\"" .. title .. "\"" else title = "" end |
| 903 return add_escape ('<img src="' .. url .. '" alt="' .. alt .. '"' .. title .. "/>") |
764 return add_escape ('<img src="' .. url .. '" alt="' .. alt .. '"' .. title .. "/>") |
| 904 end |
765 end |
| 905 |
766 |
| 906 local function inline_link(alt, link) |
767 local function inline_link(alt, link) |
| 907 alt = encode_alt(alt:match("%b[]"):sub(2,-2)) |
768 alt = encode_alt(alt:match("%b[]"):sub(2,-2)) |
| 908 local url, title = link:match("%(<?(.-)>?[ \t]*['\"](.+)['\"]") |
769 local url, title = link:match("%(<?(.-)>?[ \t]*['\"](.+)['\"]") |
| 909 url = url or link:match("%(<?(.-)>?%)") |
770 url = url or link:match("%(<?(.-)>?%)") |
| 910 url = encode_alt(url) |
771 url = encode_alt(url) |
| 911 title = encode_alt(title) |
772 title = encode_alt(title) |
| 912 if title then |
773 if title then |
| 913 return add_escape('<img src="' .. url .. '" alt="' .. alt .. '" title="' .. title .. '"/>') |
774 return add_escape('<img src="' .. url .. '" alt="' .. alt .. '" title="' .. title .. '"/>') |
| 914 else |
775 else |
| 915 return add_escape('<img src="' .. url .. '" alt="' .. alt .. '"/>') |
776 return add_escape('<img src="' .. url .. '" alt="' .. alt .. '"/>') |
| 916 end |
777 end |
| 917 end |
778 end |
| 918 |
779 |
| 919 text = text:gsub("!(%b[])[ \t]*\n?[ \t]*(%b[])", reference_link) |
780 text = text:gsub("!(%b[])[ \t]*\n?[ \t]*(%b[])", reference_link) |
| 920 text = text:gsub("!(%b[])(%b())", inline_link) |
781 text = text:gsub("!(%b[])(%b())", inline_link) |
| 921 return text |
782 return text |
| 922 end |
783 end |
| 923 |
784 |
| 924 -- Handle anchor references |
785 -- Handle anchor references |
| 925 function anchors(text) |
786 local function anchors(text) |
| 926 local function reference_link(text, id) |
787 local function reference_link(text, id) |
| 927 text = text:match("%b[]"):sub(2,-2) |
788 text = text:match("%b[]"):sub(2,-2) |
| 928 id = id:match("%b[]"):sub(2,-2):lower() |
789 id = id:match("%b[]"):sub(2,-2):lower() |
| 929 if id == "" then id = text:lower() end |
790 if id == "" then id = text:lower() end |
| 930 link_database[id] = link_database[id] or {} |
791 link_database[id] = link_database[id] or {} |
| 931 if not link_database[id].url then return nil end |
792 if not link_database[id].url then return nil end |
| 932 local url = link_database[id].url or id |
793 local url = link_database[id].url or id |
| 933 url = encode_alt(url) |
794 url = encode_alt(url) |
| 934 local title = encode_alt(link_database[id].title) |
795 local title = encode_alt(link_database[id].title) |
| 935 if title then title = " title=\"" .. title .. "\"" else title = "" end |
796 if title then title = " title=\"" .. title .. "\"" else title = "" end |
| 936 return add_escape("<a href=\"" .. url .. "\"" .. title .. ">") .. text .. add_escape("</a>") |
797 return add_escape("<a href=\"" .. url .. "\"" .. title .. ">") .. text .. add_escape("</a>") |
| 937 end |
798 end |
| 938 |
799 |
| 939 local function inline_link(text, link) |
800 local function inline_link(text, link) |
| 940 text = text:match("%b[]"):sub(2,-2) |
801 text = text:match("%b[]"):sub(2,-2) |
| 941 local url, title = link:match("%(<?(.-)>?[ \t]*['\"](.+)['\"]") |
802 local url, title = link:match("%(<?(.-)>?[ \t]*['\"](.+)['\"]") |
| 942 title = encode_alt(title) |
803 title = encode_alt(title) |
| 943 url = url or link:match("%(<?(.-)>?%)") or "" |
804 url = url or link:match("%(<?(.-)>?%)") or "" |
| 944 url = encode_alt(url) |
805 url = encode_alt(url) |
| 945 if title then |
806 if title then |
| 946 return add_escape("<a href=\"" .. url .. "\" title=\"" .. title .. "\">") .. text .. "</a>" |
807 return add_escape("<a href=\"" .. url .. "\" title=\"" .. title .. "\">") .. text .. "</a>" |
| 947 else |
808 else |
| 948 return add_escape("<a href=\"" .. url .. "\">") .. text .. add_escape("</a>") |
809 return add_escape("<a href=\"" .. url .. "\">") .. text .. add_escape("</a>") |
| 949 end |
810 end |
| 950 end |
811 end |
| 951 |
812 |
| 952 text = text:gsub("(%b[])[ \t]*\n?[ \t]*(%b[])", reference_link) |
813 text = text:gsub("(%b[])[ \t]*\n?[ \t]*(%b[])", reference_link) |
| 953 text = text:gsub("(%b[])(%b())", inline_link) |
814 text = text:gsub("(%b[])(%b())", inline_link) |
| 954 return text |
815 return text |
| 955 end |
816 end |
| 956 |
817 |
| 957 -- Handle auto links, i.e. <http://www.google.com/>. |
818 -- Handle auto links, i.e. <http://www.google.com/>. |
| 958 function auto_links(text) |
819 local function auto_links(text) |
| 959 local function link(s) |
820 local function link(s) |
| 960 return add_escape("<a href=\"" .. s .. "\">") .. s .. "</a>" |
821 return add_escape("<a href=\"" .. s .. "\">") .. s .. "</a>" |
| 961 end |
822 end |
| 962 -- Encode chars as a mix of dec and hex entitites to (perhaps) fool |
823 -- Encode chars as a mix of dec and hex entitites to (perhaps) fool |
| 963 -- spambots. |
824 -- spambots. |
| 964 local function encode_email_address(s) |
825 local function encode_email_address(s) |
| 965 -- Use a deterministic encoding to make unit testing possible. |
826 -- Use a deterministic encoding to make unit testing possible. |
| 966 -- Code 45% hex, 45% dec, 10% plain. |
827 -- Code 45% hex, 45% dec, 10% plain. |
| 967 local hex = {code = function(c) return "&#x" .. string.format("%x", c:byte()) .. ";" end, count = 1, rate = 0.45} |
828 local hex = {code = function(c) return "&#x" .. string.format("%x", c:byte()) .. ";" end, count = 1, rate = 0.45} |
| 968 local dec = {code = function(c) return "&#" .. c:byte() .. ";" end, count = 0, rate = 0.45} |
829 local dec = {code = function(c) return "&#" .. c:byte() .. ";" end, count = 0, rate = 0.45} |
| 969 local plain = {code = function(c) return c end, count = 0, rate = 0.1} |
830 local plain = {code = function(c) return c end, count = 0, rate = 0.1} |
| 970 local codes = {hex, dec, plain} |
831 local codes = {hex, dec, plain} |
| 971 local function swap(t,k1,k2) local temp = t[k2] t[k2] = t[k1] t[k1] = temp end |
832 local function swap(t,k1,k2) local temp = t[k2] t[k2] = t[k1] t[k1] = temp end |
| 972 |
833 |
| 973 local out = "" |
834 local out = "" |
| 974 for i = 1,s:len() do |
835 for i = 1,s:len() do |
| 975 for _,code in ipairs(codes) do code.count = code.count + code.rate end |
836 for _,code in ipairs(codes) do code.count = code.count + code.rate end |
| 976 if codes[1].count < codes[2].count then swap(codes,1,2) end |
837 if codes[1].count < codes[2].count then swap(codes,1,2) end |
| 977 if codes[2].count < codes[3].count then swap(codes,2,3) end |
838 if codes[2].count < codes[3].count then swap(codes,2,3) end |
| 978 if codes[1].count < codes[2].count then swap(codes,1,2) end |
839 if codes[1].count < codes[2].count then swap(codes,1,2) end |
| 979 |
840 |
| 980 local code = codes[1] |
841 local code = codes[1] |
| 981 local c = s:sub(i,i) |
842 local c = s:sub(i,i) |
| 982 -- Force encoding of "@" to make email address more invisible. |
843 -- Force encoding of "@" to make email address more invisible. |
| 983 if c == "@" and code == plain then code = codes[2] end |
844 if c == "@" and code == plain then code = codes[2] end |
| 984 out = out .. code.code(c) |
845 out = out .. code.code(c) |
| 985 code.count = code.count - 1 |
846 code.count = code.count - 1 |
| 986 end |
847 end |
| 987 return out |
848 return out |
| 988 end |
849 end |
| 989 local function mail(s) |
850 local function mail(s) |
| 990 s = unescape_special_chars(s) |
851 s = unescape_special_chars(s) |
| 991 local address = encode_email_address("mailto:" .. s) |
852 local address = encode_email_address("mailto:" .. s) |
| 992 local text = encode_email_address(s) |
853 local text = encode_email_address(s) |
| 993 return add_escape("<a href=\"" .. address .. "\">") .. text .. "</a>" |
854 return add_escape("<a href=\"" .. address .. "\">") .. text .. "</a>" |
| 994 end |
855 end |
| 995 -- links |
856 -- links |
| 996 text = text:gsub("<(https?:[^'\">%s]+)>", link) |
857 text = text:gsub("<(https?:[^'\">%s]+)>", link) |
| 997 text = text:gsub("<(ftp:[^'\">%s]+)>", link) |
858 text = text:gsub("<(ftp:[^'\">%s]+)>", link) |
| 998 |
859 |
| 999 -- mail |
860 -- mail |
| 1000 text = text:gsub("<mailto:([^'\">%s]+)>", mail) |
861 text = text:gsub("<mailto:([^'\">%s]+)>", mail) |
| 1001 text = text:gsub("<([-.%w]+%@[-.%w]+)>", mail) |
862 text = text:gsub("<([-.%w]+%@[-.%w]+)>", mail) |
| 1002 return text |
863 return text |
| 1003 end |
864 end |
| 1004 |
865 |
| 1005 -- Encode free standing amps (&) and angles (<)... note that this does not |
866 -- Encode free standing amps (&) and angles (<)... note that this does not |
| 1006 -- encode free >. |
867 -- encode free >. |
| 1007 function amps_and_angles(s) |
868 local function amps_and_angles(s) |
| 1008 -- encode amps not part of &..; expression |
869 -- encode amps not part of &..; expression |
| 1009 local pos = 1 |
870 local pos = 1 |
| 1010 while true do |
871 while true do |
| 1011 local amp = s:find("&", pos) |
872 local amp = s:find("&", pos) |
| 1012 if not amp then break end |
873 if not amp then break end |
| 1013 local semi = s:find(";", amp+1) |
874 local semi = s:find(";", amp+1) |
| 1014 local stop = s:find("[ \t\n&]", amp+1) |
875 local stop = s:find("[ \t\n&]", amp+1) |
| 1015 if not semi or (stop and stop < semi) or (semi - amp) > 15 then |
876 if not semi or (stop and stop < semi) or (semi - amp) > 15 then |
| 1016 s = s:sub(1,amp-1) .. "&" .. s:sub(amp+1) |
877 s = s:sub(1,amp-1) .. "&" .. s:sub(amp+1) |
| 1017 pos = amp+1 |
878 pos = amp+1 |
| 1018 else |
879 else |
| 1019 pos = amp+1 |
880 pos = amp+1 |
| 1020 end |
881 end |
| 1021 end |
882 end |
| 1022 |
883 |
| 1023 -- encode naked <'s |
884 -- encode naked <'s |
| 1024 s = s:gsub("<([^a-zA-Z/?$!])", "<%1") |
885 s = s:gsub("<([^a-zA-Z/?$!])", "<%1") |
| 1025 s = s:gsub("<$", "<") |
886 s = s:gsub("<$", "<") |
| 1026 |
887 |
| 1027 -- what about >, nothing done in the original markdown source to handle them |
888 -- what about >, nothing done in the original markdown source to handle them |
| 1028 return s |
889 return s |
| 1029 end |
890 end |
| 1030 |
891 |
| 1031 -- Handles emphasis markers (* and _) in the text. |
892 -- Handles emphasis markers (* and _) in the text. |
| 1032 function emphasis(text) |
893 local function emphasis(text) |
| 1033 for _, s in ipairs {"%*%*", "%_%_"} do |
894 for _, s in ipairs {"%*%*", "%_%_"} do |
| 1034 text = text:gsub(s .. "([^%s][%*%_]?)" .. s, "<strong>%1</strong>") |
895 text = text:gsub(s .. "([^%s][%*%_]?)" .. s, "<strong>%1</strong>") |
| 1035 text = text:gsub(s .. "([^%s][^<>]-[^%s][%*%_]?)" .. s, "<strong>%1</strong>") |
896 text = text:gsub(s .. "([^%s][^<>]-[^%s][%*%_]?)" .. s, "<strong>%1</strong>") |
| 1036 end |
897 end |
| 1037 for _, s in ipairs {"%*", "%_"} do |
898 for _, s in ipairs {"%*", "%_"} do |
| 1038 text = text:gsub(s .. "([^%s_])" .. s, "<em>%1</em>") |
899 text = text:gsub(s .. "([^%s_])" .. s, "<em>%1</em>") |
| 1039 text = text:gsub(s .. "(<strong>[^%s_]</strong>)" .. s, "<em>%1</em>") |
900 text = text:gsub(s .. "(<strong>[^%s_]</strong>)" .. s, "<em>%1</em>") |
| 1040 text = text:gsub(s .. "([^%s_][^<>_]-[^%s_])" .. s, "<em>%1</em>") |
901 text = text:gsub(s .. "([^%s_][^<>_]-[^%s_])" .. s, "<em>%1</em>") |
| 1041 text = text:gsub(s .. "([^<>_]-<strong>[^<>_]-</strong>[^<>_]-)" .. s, "<em>%1</em>") |
902 text = text:gsub(s .. "([^<>_]-<strong>[^<>_]-</strong>[^<>_]-)" .. s, "<em>%1</em>") |
| 1042 end |
903 end |
| 1043 return text |
904 return text |
| 1044 end |
905 end |
| 1045 |
906 |
| 1046 -- Handles line break markers in the text. |
907 -- Handles line break markers in the text. |
| 1047 function line_breaks(text) |
908 local function line_breaks(text) |
| 1048 return text:gsub(" +\n", " <br/>\n") |
909 return text:gsub(" +\n", " <br/>\n") |
| 1049 end |
910 end |
| 1050 |
911 |
| 1051 -- Perform all span level transforms. |
912 -- Perform all span level transforms. |
| 1052 function span_transform(text) |
913 function span_transform(text) |
| 1053 text = code_spans(text) |
914 text = code_spans(text) |
| 1054 text = escape_special_chars(text) |
915 text = escape_special_chars(text) |
| 1055 text = images(text) |
916 text = images(text) |
| 1056 text = anchors(text) |
917 text = anchors(text) |
| 1057 text = auto_links(text) |
918 text = auto_links(text) |
| 1058 text = amps_and_angles(text) |
919 text = amps_and_angles(text) |
| 1059 text = emphasis(text) |
920 text = emphasis(text) |
| 1060 text = line_breaks(text) |
921 text = line_breaks(text) |
| 1061 return text |
922 return text |
| 1062 end |
923 end |
| 1063 |
924 |
| 1064 ---------------------------------------------------------------------- |
925 ---------------------------------------------------------------------- |
| 1065 -- Markdown |
926 -- Markdown |
| 1066 ---------------------------------------------------------------------- |
927 ---------------------------------------------------------------------- |
| 1067 |
928 |
| 1068 -- Cleanup the text by normalizing some possible variations to make further |
929 -- Cleanup the text by normalizing some possible variations to make further |
| 1069 -- processing easier. |
930 -- processing easier. |
| 1070 function cleanup(text) |
931 local function cleanup(text) |
| 1071 -- Standardize line endings |
932 -- Standardize line endings |
| 1072 text = text:gsub("\r\n", "\n") -- DOS to UNIX |
933 text = text:gsub("\r\n", "\n") -- DOS to UNIX |
| 1073 text = text:gsub("\r", "\n") -- Mac to UNIX |
934 text = text:gsub("\r", "\n") -- Mac to UNIX |
| 1074 |
935 |
| 1075 -- Convert all tabs to spaces |
936 -- Convert all tabs to spaces |
| 1076 text = detab(text) |
937 text = detab(text) |
| 1077 |
938 |
| 1078 -- Strip lines with only spaces and tabs |
939 -- Strip lines with only spaces and tabs |
| 1079 while true do |
940 while true do |
| 1080 local subs |
941 local subs |
| 1081 text, subs = text:gsub("\n[ \t]+\n", "\n\n") |
942 text, subs = text:gsub("\n[ \t]+\n", "\n\n") |
| 1082 if subs == 0 then break end |
943 if subs == 0 then break end |
| 1083 end |
944 end |
| 1084 |
945 |
| 1085 return "\n" .. text .. "\n" |
946 return "\n" .. text .. "\n" |
| 1086 end |
947 end |
| 1087 |
948 |
| 1088 -- Strips link definitions from the text and stores the data in a lookup table. |
949 -- Strips link definitions from the text and stores the data in a lookup table. |
| 1089 function strip_link_definitions(text) |
950 local function strip_link_definitions(text) |
| 1090 local linkdb = {} |
951 local linkdb = {} |
| 1091 |
952 |
| 1092 local function link_def(id, url, title) |
953 local function link_def(id, url, title) |
| 1093 id = id:match("%[(.+)%]"):lower() |
954 id = id:match("%[(.+)%]"):lower() |
| 1094 linkdb[id] = linkdb[id] or {} |
955 linkdb[id] = linkdb[id] or {} |
| 1095 linkdb[id].url = url or linkdb[id].url |
956 linkdb[id].url = url or linkdb[id].url |
| 1096 linkdb[id].title = title or linkdb[id].title |
957 linkdb[id].title = title or linkdb[id].title |
| 1097 return "" |
958 return "" |
| 1098 end |
959 end |
| 1099 |
960 |
| 1100 local def_no_title = "\n ? ? ?(%b[]):[ \t]*\n?[ \t]*<?([^%s>]+)>?[ \t]*" |
961 local def_no_title = "\n ? ? ?(%b[]):[ \t]*\n?[ \t]*<?([^%s>]+)>?[ \t]*" |
| 1101 local def_title1 = def_no_title .. "[ \t]+\n?[ \t]*[\"'(]([^\n]+)[\"')][ \t]*" |
962 local def_title1 = def_no_title .. "[ \t]+\n?[ \t]*[\"'(]([^\n]+)[\"')][ \t]*" |
| 1102 local def_title2 = def_no_title .. "[ \t]*\n[ \t]*[\"'(]([^\n]+)[\"')][ \t]*" |
963 local def_title2 = def_no_title .. "[ \t]*\n[ \t]*[\"'(]([^\n]+)[\"')][ \t]*" |
| 1103 local def_title3 = def_no_title .. "[ \t]*\n?[ \t]+[\"'(]([^\n]+)[\"')][ \t]*" |
964 local def_title3 = def_no_title .. "[ \t]*\n?[ \t]+[\"'(]([^\n]+)[\"')][ \t]*" |
| 1104 |
965 |
| 1105 text = text:gsub(def_title1, link_def) |
966 text = text:gsub(def_title1, link_def) |
| 1106 text = text:gsub(def_title2, link_def) |
967 text = text:gsub(def_title2, link_def) |
| 1107 text = text:gsub(def_title3, link_def) |
968 text = text:gsub(def_title3, link_def) |
| 1108 text = text:gsub(def_no_title, link_def) |
969 text = text:gsub(def_no_title, link_def) |
| 1109 return text, linkdb |
970 return text, linkdb |
| 1110 end |
971 end |
| 1111 |
|
| 1112 link_database = {} |
|
| 1113 |
972 |
| 1114 -- Main markdown processing function |
973 -- Main markdown processing function |
| 1115 function markdown(text) |
974 local function markdown(text) |
| 1116 init_hash(text) |
975 init_hash(text) |
| 1117 init_escape_table() |
976 init_escape_table() |
| 1118 |
977 |
| 1119 text = cleanup(text) |
978 text = cleanup(text) |
| 1120 text = protect(text) |
979 text = protect(text) |
| 1121 text, link_database = strip_link_definitions(text) |
980 text, link_database = strip_link_definitions(text) |
| 1122 text = block_transform(text) |
981 text = block_transform(text) |
| 1123 text = unescape_special_chars(text) |
982 text = unescape_special_chars(text) |
| 1124 return text |
983 return text |
| 1125 end |
984 end |
| 1126 |
985 |
| 1127 ---------------------------------------------------------------------- |
986 ---------------------------------------------------------------------- |
| 1128 -- End of module |
987 -- End of module |
| 1129 ---------------------------------------------------------------------- |
988 ---------------------------------------------------------------------- |
| 1130 |
989 |
| 1131 setfenv(1, _G) |
990 -- For compatibility, set markdown function as a global |
| 1132 M.lock(M) |
991 _G.markdown = markdown |
| 1133 |
|
| 1134 -- Expose markdown function to the world |
|
| 1135 markdown = M.markdown |
|
| 1136 |
992 |
| 1137 -- Class for parsing command-line options |
993 -- Class for parsing command-line options |
| 1138 local OptionParser = {} |
994 local OptionParser = {} |
| 1139 OptionParser.__index = OptionParser |
995 OptionParser.__index = OptionParser |
| 1140 |
996 |
| 1141 -- Creates a new option parser |
997 -- Creates a new option parser |
| 1142 function OptionParser:new() |
998 function OptionParser:new() |
| 1143 local o = {short = {}, long = {}} |
999 local o = {short = {}, long = {}} |
| 1144 setmetatable(o, self) |
1000 setmetatable(o, self) |
| 1145 return o |
1001 return o |
| 1146 end |
1002 end |
| 1147 |
1003 |
| 1148 -- Calls f() whenever a flag with specified short and long name is encountered |
1004 -- Calls f() whenever a flag with specified short and long name is encountered |
| 1149 function OptionParser:flag(short, long, f) |
1005 function OptionParser:flag(short, long, f) |
| 1150 local info = {type = "flag", f = f} |
1006 local info = {type = "flag", f = f} |
| 1151 if short then self.short[short] = info end |
1007 if short then self.short[short] = info end |
| 1152 if long then self.long[long] = info end |
1008 if long then self.long[long] = info end |
| 1153 end |
1009 end |
| 1154 |
1010 |
| 1155 -- Calls f(param) whenever a parameter flag with specified short and long name is encountered |
1011 -- Calls f(param) whenever a parameter flag with specified short and long name is encountered |
| 1156 function OptionParser:param(short, long, f) |
1012 function OptionParser:param(short, long, f) |
| 1157 local info = {type = "param", f = f} |
1013 local info = {type = "param", f = f} |
| 1158 if short then self.short[short] = info end |
1014 if short then self.short[short] = info end |
| 1159 if long then self.long[long] = info end |
1015 if long then self.long[long] = info end |
| 1160 end |
1016 end |
| 1161 |
1017 |
| 1162 -- Calls f(v) for each non-flag argument |
1018 -- Calls f(v) for each non-flag argument |
| 1163 function OptionParser:arg(f) |
1019 function OptionParser:arg(f) |
| 1164 self.arg = f |
1020 self.arg = f |
| 1165 end |
1021 end |
| 1166 |
1022 |
| 1167 -- Runs the option parser for the specified set of arguments. Returns true if all arguments |
1023 -- Runs the option parser for the specified set of arguments. Returns true if all arguments |
| 1168 -- where successfully parsed and false otherwise. |
1024 -- where successfully parsed and false otherwise. |
| 1169 function OptionParser:run(args) |
1025 function OptionParser:run(args) |
| 1170 local pos = 1 |
1026 local pos = 1 |
| 1171 while pos <= #args do |
1027 while pos <= #args do |
| 1172 local arg = args[pos] |
1028 local arg = args[pos] |
| 1173 if arg == "--" then |
1029 if arg == "--" then |
| 1174 for i=pos+1,#args do |
1030 for i=pos+1,#args do |
| 1175 if self.arg then self.arg(args[i]) end |
1031 if self.arg then self.arg(args[i]) end |
| 1176 return true |
1032 return true |
| 1177 end |
1033 end |
| 1178 end |
1034 end |
| 1179 if arg:match("^%-%-") then |
1035 if arg:match("^%-%-") then |
| 1180 local info = self.long[arg:sub(3)] |
1036 local info = self.long[arg:sub(3)] |
| 1181 if not info then print("Unknown flag: " .. arg) return false end |
1037 if not info then print("Unknown flag: " .. arg) return false end |
| 1182 if info.type == "flag" then |
1038 if info.type == "flag" then |
| 1183 info.f() |
1039 info.f() |
| 1184 pos = pos + 1 |
1040 pos = pos + 1 |
| 1185 else |
1041 else |
| 1186 param = args[pos+1] |
1042 local param = args[pos+1] |
| 1187 if not param then print("No parameter for flag: " .. arg) return false end |
1043 if not param then print("No parameter for flag: " .. arg) return false end |
| 1188 info.f(param) |
1044 info.f(param) |
| 1189 pos = pos+2 |
1045 pos = pos+2 |
| 1190 end |
1046 end |
| 1191 elseif arg:match("^%-") then |
1047 elseif arg:match("^%-") then |
| 1192 for i=2,arg:len() do |
1048 for i=2,arg:len() do |
| 1193 local c = arg:sub(i,i) |
1049 local c = arg:sub(i,i) |
| 1194 local info = self.short[c] |
1050 local info = self.short[c] |
| 1195 if not info then print("Unknown flag: -" .. c) return false end |
1051 if not info then print("Unknown flag: -" .. c) return false end |
| 1196 if info.type == "flag" then |
1052 if info.type == "flag" then |
| 1197 info.f() |
1053 info.f() |
| 1198 else |
1054 else |
| 1199 if i == arg:len() then |
1055 if i == arg:len() then |
| 1200 param = args[pos+1] |
1056 local param = args[pos+1] |
| 1201 if not param then print("No parameter for flag: -" .. c) return false end |
1057 if not param then print("No parameter for flag: -" .. c) return false end |
| 1202 info.f(param) |
1058 info.f(param) |
| 1203 pos = pos + 1 |
1059 pos = pos + 1 |
| 1204 else |
1060 else |
| 1205 param = arg:sub(i+1) |
1061 local param = arg:sub(i+1) |
| 1206 info.f(param) |
1062 info.f(param) |
| 1207 end |
1063 end |
| 1208 break |
1064 break |
| 1209 end |
1065 end |
| 1210 end |
1066 end |
| 1211 pos = pos + 1 |
1067 pos = pos + 1 |
| 1212 else |
1068 else |
| 1213 if self.arg then self.arg(arg) end |
1069 if self.arg then self.arg(arg) end |
| 1214 pos = pos + 1 |
1070 pos = pos + 1 |
| 1215 end |
1071 end |
| 1216 end |
1072 end |
| 1217 return true |
1073 return true |
| |
1074 end |
| |
1075 |
| |
1076 local function read_file(path, descr) |
| |
1077 local file = io.open(path) or error("Could not open " .. descr .. " file: " .. path) |
| |
1078 local contents = file:read("*a") or error("Could not read " .. descr .. " from " .. path) |
| |
1079 file:close() |
| |
1080 return contents |
| 1218 end |
1081 end |
| 1219 |
1082 |
| 1220 -- Handles the case when markdown is run from the command line |
1083 -- Handles the case when markdown is run from the command line |
| 1221 local function run_command_line(arg) |
1084 local function run_command_line(arg) |
| 1222 -- Generate output for input s given options |
1085 -- Generate output for input s given options |
| 1223 local function run(s, options) |
1086 local function run(s, options) |
| 1224 s = markdown(s) |
1087 s = markdown(s) |
| 1225 if not options.wrap_header then return s end |
1088 if not options.wrap_header then return s end |
| 1226 local header = "" |
1089 local header |
| 1227 if options.header then |
1090 if options.header then |
| 1228 local f = io.open(options.header) or error("Could not open file: " .. options.header) |
1091 header = read_file(options.header, "header") |
| 1229 header = f:read("*a") |
1092 else |
| 1230 f:close() |
1093 header = [[ |
| 1231 else |
|
| 1232 header = [[ |
|
| 1233 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> |
1094 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> |
| 1234 <html> |
1095 <html> |
| 1235 <head> |
1096 <head> |
| 1236 <meta http-equiv="content-type" content="text/html; charset=CHARSET" /> |
1097 <meta http-equiv="content-type" content="text/html; charset=CHARSET" /> |
| 1237 <title>TITLE</title> |
1098 <title>TITLE</title> |
| 1238 <link rel="stylesheet" type="text/css" href="STYLESHEET" /> |
1099 <link rel="stylesheet" type="text/css" href="STYLESHEET" /> |
| 1239 </head> |
1100 </head> |
| 1240 <body> |
1101 <body> |
| 1241 ]] |
1102 ]] |
| 1242 local title = options.title or s:match("<h1>(.-)</h1>") or s:match("<h2>(.-)</h2>") or |
1103 local title = options.title or s:match("<h1>(.-)</h1>") or s:match("<h2>(.-)</h2>") or |
| 1243 s:match("<h3>(.-)</h3>") or "Untitled" |
1104 s:match("<h3>(.-)</h3>") or "Untitled" |
| 1244 header = header:gsub("TITLE", title) |
1105 header = header:gsub("TITLE", title) |
| 1245 if options.inline_style then |
1106 if options.inline_style then |
| 1246 local style = "" |
1107 local style = read_file(options.stylesheet, "style sheet") |
| 1247 local f = io.open(options.stylesheet) |
1108 header = header:gsub('<link rel="stylesheet" type="text/css" href="STYLESHEET" />', |
| 1248 if f then |
1109 "<style type=\"text/css\"><!--\n" .. style .. "\n--></style>") |
| 1249 style = f:read("*a") f:close() |
1110 else |
| 1250 else |
1111 header = header:gsub("STYLESHEET", options.stylesheet) |
| 1251 error("Could not include style sheet " .. options.stylesheet .. ": File not found") |
1112 end |
| 1252 end |
1113 header = header:gsub("CHARSET", options.charset) |
| 1253 header = header:gsub('<link rel="stylesheet" type="text/css" href="STYLESHEET" />', |
1114 end |
| 1254 "<style type=\"text/css\"><!--\n" .. style .. "\n--></style>") |
1115 local footer = "</body></html>" |
| 1255 else |
1116 if options.footer then |
| 1256 header = header:gsub("STYLESHEET", options.stylesheet) |
1117 footer = read_file(options.footer, "footer") |
| 1257 end |
1118 end |
| 1258 header = header:gsub("CHARSET", options.charset) |
1119 return header .. s .. footer |
| 1259 end |
1120 end |
| 1260 local footer = "</body></html>" |
1121 |
| 1261 if options.footer then |
1122 -- Generate output path name from input path name given options. |
| 1262 local f = io.open(options.footer) or error("Could not open file: " .. options.footer) |
1123 local function outpath(path, options) |
| 1263 footer = f:read("*a") |
1124 if options.append then return path .. ".html" end |
| 1264 f:close() |
1125 local m = path:match("^(.+%.html)[^/\\]+$") if m then return m end |
| 1265 end |
1126 m = path:match("^(.+%.)[^/\\]*$") if m and path ~= m .. "html" then return m .. "html" end |
| 1266 return header .. s .. footer |
1127 return path .. ".html" |
| 1267 end |
1128 end |
| 1268 |
1129 |
| 1269 -- Generate output path name from input path name given options. |
1130 -- Default commandline options |
| 1270 local function outpath(path, options) |
1131 local options = { |
| 1271 if options.append then return path .. ".html" end |
1132 wrap_header = true, |
| 1272 local m = path:match("^(.+%.html)[^/\\]+$") if m then return m end |
1133 header = nil, |
| 1273 m = path:match("^(.+%.)[^/\\]*$") if m and path ~= m .. "html" then return m .. "html" end |
1134 footer = nil, |
| 1274 return path .. ".html" |
1135 charset = "utf-8", |
| 1275 end |
1136 title = nil, |
| 1276 |
1137 stylesheet = "default.css", |
| 1277 -- Default commandline options |
1138 inline_style = false |
| 1278 local options = { |
1139 } |
| 1279 wrap_header = true, |
1140 local help = [[ |
| 1280 header = nil, |
|
| 1281 footer = nil, |
|
| 1282 charset = "utf-8", |
|
| 1283 title = nil, |
|
| 1284 stylesheet = "default.css", |
|
| 1285 inline_style = false |
|
| 1286 } |
|
| 1287 local help = [[ |
|
| 1288 Usage: markdown.lua [OPTION] [FILE] |
1141 Usage: markdown.lua [OPTION] [FILE] |
| 1289 Runs the markdown text markup to HTML converter on each file specified on the |
1142 Runs the markdown text markup to HTML converter on each file specified on the |
| 1290 command line. If no files are specified, runs on standard input. |
1143 command line. If no files are specified, runs on standard input. |
| 1291 |
1144 |
| 1292 No header: |
1145 No header: |