summaryrefslogtreecommitdiff
path: root/Data/Libraries/LDoc/ldoc/markup.lua
diff options
context:
space:
mode:
Diffstat (limited to 'Data/Libraries/LDoc/ldoc/markup.lua')
-rw-r--r--Data/Libraries/LDoc/ldoc/markup.lua420
1 files changed, 420 insertions, 0 deletions
diff --git a/Data/Libraries/LDoc/ldoc/markup.lua b/Data/Libraries/LDoc/ldoc/markup.lua
new file mode 100644
index 0000000..425fb07
--- /dev/null
+++ b/Data/Libraries/LDoc/ldoc/markup.lua
@@ -0,0 +1,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