1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420
|
--------------
-- Handling markup transformation.
-- Currently just does Markdown, but this is intended to
-- be the general module for managing other formats as well.
local doc = require 'ldoc.doc'
local utils = require 'pl.utils'
local stringx = require 'pl.stringx'
local prettify = require 'ldoc.prettify'
local concat = table.concat
local markup = {}
local backtick_references
-- inline <references> use same lookup as @see
local function resolve_inline_references (ldoc, txt, item, plain)
local do_escape = not plain and not ldoc.dont_escape_underscore
local res = (txt:gsub('@{([^}]-)}',function (name)
if name:match '^\\' then return '@{'..name:sub(2)..'}' end
local qname,label = utils.splitv(name,'%s*|')
if not qname then
qname = name
end
local ref, err
local custom_ref, refname = utils.splitv(qname,':')
if custom_ref and ldoc.custom_references then
custom_ref = ldoc.custom_references[custom_ref]
if custom_ref then
ref,err = custom_ref(refname)
end
end
if not ref then
ref,err = markup.process_reference(qname)
end
if not ref then
err = err .. ' ' .. qname
if item and item.warning then item:warning(err)
else
io.stderr:write('nofile error: ',err,'\n')
end
return '???'
end
if not label then
label = ref.label
end
if label and do_escape then -- a nastiness with markdown.lua and underscores
label = label:gsub('_','\\_')
end
local html = ldoc.href(ref) or '#'
label = ldoc.escape(label or qname)
local res = ('<a href="%s">%s</a>'):format(html,label)
return res
end))
if backtick_references then
res = res:gsub('`([^`]+)`',function(name)
local ref,_ = markup.process_reference(name)
local label = name
if name and do_escape then
label = name:gsub('_', '\\_')
end
label = ldoc.escape(label)
if ref then
return ('<a href="%s">%s</a>'):format(ldoc.href(ref),label)
else
return '<code>'..label..'</code>'
end
end)
end
return res
end
-- for readme text, the idea here is to create module sections at ## so that
-- they can appear in the contents list as a ToC.
function markup.add_sections(F, txt)
local sections, L, first = {}, 1, true
local title_pat
local lstrip = stringx.lstrip
for line in stringx.lines(txt) do
if first then
local level,header = line:match '^(#+)%s*(.+)'
if level then
level = level .. '#'
else
level = '##'
end
title_pat = '^'..level..'([^#]%s*.+)'
title_pat = lstrip(title_pat)
first = false
F.display_name = header
end
local title = line:match (title_pat)
if title then
--- Windows line endings are the cockroaches of text
title = title:gsub('\r$','')
-- Markdown allows trailing '#'...
title = title:gsub('%s*#+$','')
sections[L] = F:add_document_section(lstrip(title))
end
L = L + 1
end
F.sections = sections
return txt
end
local function indent_line (line)
line = line:gsub('\t',' ') -- support for barbarians ;)
local indent = #line:match '^%s*'
return indent,line
end
local function blank (line)
return not line:find '%S'
end
local global_context, local_context
-- before we pass Markdown documents to markdown/discount, we need to do three things:
-- - resolve any @{refs} and (optionally) `refs`
-- - any @lookup directives that set local context for ref lookup
-- - insert any section ids which were generated by add_sections above
-- - prettify any code blocks
local function process_multiline_markdown(ldoc, txt, F, filename, deflang)
local res, L, append = {}, 0, table.insert
local err_item = {
warning = function (self,msg)
io.stderr:write(filename..':'..L..': '..msg,'\n')
end
}
local get = stringx.lines(txt)
local getline = function()
L = L + 1
return get()
end
local function pretty_code (code, lang)
code = concat(code,'\n')
if code ~= '' then
local _
-- If we omit the following '\n', a '--' (or '//') comment on the
-- last line won't be recognized.
code, _ = prettify.code(lang,filename,code..'\n',L,false)
code = resolve_inline_references(ldoc, code, err_item,true)
append(res,'<pre>')
append(res, code)
append(res,'</pre>')
else
append(res,code)
end
end
local indent,start_indent
local_context = nil
local line = getline()
while line do
local name = line:match '^@lookup%s+(%S+)'
if name then
local_context = name .. '.'
line = getline()
end
local fence = line:match '^```(.*)'
if fence then
local plain = fence==''
line = getline()
local code = {}
while not line:match '^```' do
if not plain then
append(code, line)
else
append(res, ' '..line)
end
line = getline()
end
pretty_code (code,fence)
line = getline() -- skip fence
if not line then break end
end
indent, line = indent_line(line)
if indent >= 4 then -- indented code block
local code = {}
local plain
while indent >= 4 or blank(line) do
if not start_indent then
start_indent = indent
if line:match '^%s*@plain%s*$' then
plain = true
line = getline()
end
end
if not plain then
append(code,line:sub(start_indent + 1))
else
append(res,line)
end
line = getline()
if line == nil then break end
indent, line = indent_line(line)
end
start_indent = nil
while #code > 1 and blank(code[#code]) do -- trim blank lines.
table.remove(code)
end
pretty_code (code,deflang)
else
local section = F and F.sections[L]
if section then
append(res,('<a name="%s"></a>'):format(section))
end
line = resolve_inline_references(ldoc, line, err_item)
append(res,line)
line = getline()
end
end
res = concat(res,'\n')
return res
end
-- Handle markdown formatters
-- Try to get the one the user has asked for, but if it's not available,
-- try all the others we know about. If they don't work, fall back to text.
local function generic_formatter(format)
local ok, f = pcall(require, format)
return ok and f
end
local formatters =
{
markdown = function(format)
local ok, markdown = pcall(require, 'markdown')
if not ok then
print('format: using built-in markdown')
ok, markdown = pcall(require, 'ldoc.markdown')
end
return ok and markdown
end,
discount = function(format)
local ok, markdown = pcall(require, 'discount')
if ok then
-- luacheck: push ignore 542
if 'function' == type(markdown) then
-- lua-discount by A.S. Bradbury, https://luarocks.org/modules/luarocks/lua-discount
elseif 'table' == type(markdown) and ('function' == type(markdown.compile) or 'function' == type(markdown.to_html)) then
-- discount by Craig Barnes, https://luarocks.org/modules/craigb/discount
-- result of apt-get install lua-discount (links against libmarkdown2)
local mysterious_debian_variant = markdown.to_html ~= nil
markdown = markdown.compile or markdown.to_html
return function(text)
local result, errmsg = markdown(text)
if result then
if mysterious_debian_variant then
return result
else
return result.body
end
else
io.stderr:write('LDoc discount failed with error ',errmsg)
os.exit(1)
end
end
else
ok = false
end
-- luacheck: pop
end
if not ok then
print('format: using built-in markdown')
ok, markdown = pcall(require, 'ldoc.markdown')
end
return ok and markdown
end,
lunamark = function(format)
local ok, lunamark = pcall(require, format)
if ok then
local writer = lunamark.writer.html.new()
local parse = lunamark.reader.markdown.new(writer,
{ smart = true })
return function(text) return parse(text) end
end
end,
commonmark = function(format)
local ok, cmark = pcall(require, 'cmark')
if ok then
return function(text)
local doc = cmark.parse_document(text, string.len(text), cmark.OPT_DEFAULT)
return cmark.render_html(doc, cmark.OPT_DEFAULT)
end
end
end
}
local function get_formatter(format)
local used_format = format
local formatter = (formatters[format] or generic_formatter)(format)
if not formatter then -- try another equivalent processor
for name, f in pairs(formatters) do
formatter = f(name)
if formatter then
print('format: '..format..' not found, using '..name)
used_format = name
break
end
end
end
return formatter, used_format
end
local function text_processor(ldoc)
return function(txt,item)
if txt == nil then return '' end
-- hack to separate paragraphs with blank lines
txt = txt:gsub('\n\n','\n<p>')
return resolve_inline_references(ldoc, txt, item, true)
end
end
local plain_processor
local function markdown_processor(ldoc, formatter)
return function (txt,item,plain)
if txt == nil then return '' end
if plain then
if not plain_processor then
plain_processor = text_processor(ldoc)
end
return plain_processor(txt,item)
end
local is_file = utils.is_type(item,doc.File)
local is_module = not is_file and item and doc.project_level(item.type)
if is_file or is_module then
local deflang = 'lua'
if ldoc.parse_extra and ldoc.parse_extra.C then
deflang = 'c'
end
if is_module then
txt = process_multiline_markdown(ldoc, txt, nil, item.file.filename, deflang)
else
txt = process_multiline_markdown(ldoc, txt, item, item.filename, deflang)
end
else
txt = resolve_inline_references(ldoc, txt, item)
end
txt = formatter(txt)
-- We will add our own paragraph tags, if needed.
return (txt:gsub('^%s*<p>(.+)</p>%s*$','%1'))
end
end
local function get_processor(ldoc, format)
if format == 'plain' then return text_processor(ldoc) end
local formatter,actual_format = get_formatter(format)
if formatter then
markup.plain = false
-- AFAIK only markdown.lua has underscore-in-identifier problem...
if ldoc.dont_escape_underscore ~= nil then
ldoc.dont_escape_underscore = actual_format ~= 'markdown'
end
return markdown_processor(ldoc, formatter)
end
print('format: '..format..' not found, falling back to text')
return text_processor(ldoc)
end
function markup.create (ldoc, format, pretty, user_keywords)
local processor
markup.plain = true
if format == 'backtick' then
ldoc.backtick_references = true
format = 'plain'
end
backtick_references = ldoc.backtick_references
global_context = ldoc.package and ldoc.package .. '.'
prettify.set_prettifier(pretty)
prettify.set_user_keywords(user_keywords)
markup.process_reference = function(name,istype)
if local_context == 'none.' and not name:match '%.' then
return nil,'not found'
end
local mod = ldoc.single or ldoc.module or ldoc.modules[1]
local ref,err = mod:process_see_reference(name, ldoc.modules, istype)
if ref then return ref end
if global_context then
local qname = global_context .. name
ref = mod:process_see_reference(qname, ldoc.modules, istype)
if ref then return ref end
end
if local_context then
local qname = local_context .. name
ref = mod:process_see_reference(qname, ldoc.modules, istype)
if ref then return ref end
end
-- note that we'll return the original error!
return ref,err
end
markup.href = function(ref)
return ldoc.href(ref)
end
processor = get_processor(ldoc, format)
if not markup.plain and backtick_references == nil then
backtick_references = true
end
markup.resolve_inline_references = function(txt, errfn)
return resolve_inline_references(ldoc, txt, errfn, markup.plain)
end
markup.processor = processor
prettify.resolve_inline_references = function(txt, errfn)
return resolve_inline_references(ldoc, txt, errfn, true)
end
return processor
end
return markup
|