WoWInterface SVN bdGrid

[/] [trunk/] [lib/] [oUF/] [utils/] [docs] - Rev 2

Go to most recent revision | Compare with Previous | Blame | View Log

#!/usr/bin/env lua
-- docs
-- oUF documentation generator
--
-- This is really just a quick and dirty way of generating documentation for
-- oUF[1]. The syntax is inspired by TomDoc[2], but a lot of the non-oUF and
-- non-Lua things aren't implemented.
--
-- Why implement my own documentation generator?
-- It was mainly done because oUF is kind-of special, but also because the
-- available alternatives aren't good enough or have issues I can't workaround.
--
-- Things that need fixing:
--  - No highlighting of Lua code.
--  - Doesn't validate that comments are documentation strings.
--  - Doesn't parse its own documentation header.
--  - Close to zero error handling.
--
-- Usage
--
-- docs [docs path] [file...]
--
-- Links
--
-- [1] https://github.com/haste/oUF
-- [2] http://tomdoc.org/

local out
local lines

local tisf = function(fmt, ...)
        table.insert(out, fmt:format(...))
end

local trim = function(str)
        return str:match('^()%s*$') and '' or str:match('^%s*(.*%S)')
end

local findNextEmpty = function(start, stop)
        for i=start, stop or #lines do
                if(lines[i] == '') then
                        return i
                end
        end
end

local findNextHeader = function(offest)
        for i=offest, #lines do
                local pre, header, post = unpack(lines, i, i + 2)
                -- Single lines without punctuation are headers.
                if(pre == '' and post == '' and not header:match'%.') then
                        return i + 1
                end
        end
end

local findNextArguent = function(start, stop, padding, pattern)
        for i=start, stop do
                local match, pad = lines[i]:match(pattern)
                if(match and pad == padding) then
                        return i
                end
        end
end

local replaceMarkup = function(str)
        return str
        -- `in-line code` to <code>in-line code</code>
        :gsub('`([^`]+)`', '<code>%1</code>')
        -- [Link](http://example.com) to <a href="http://example.com">Link</a>
        :gsub('%[([^%]]+)%]%(([^)]+)%)', '<a href="%2">%1</a>')
end

local handleArguments = function(start, stop, pattern)
        tisf('<dl>')
        repeat
                -- Tear out the argument name and offset of where the description begins.
                local def, padding, offset = lines[start]:match(pattern)
                tisf('<dt>%s</dt>', def)

                -- Insert the first line of the description.
                tisf('<dd>')
                tisf(lines[start]:sub(offset))

                -- Find the next argument in the list or continue to the end of the
                -- current section.
                local nextarg = findNextArguent(start + 1, stop, padding, pattern)
                nextarg = (nextarg or stop + 1) - 1
                for i=start + 1, nextarg do
                        tisf(trim(lines[i]))
                end
                tisf('</dd>')

                start = nextarg + 1
        until start > stop
        tisf('</dl>')
end

local handleExamples = function(start, stop)
        -- An extra line gets added if we don't do this.
        tisf('<pre><code>%s', lines[start]:sub(3))
        for i=start + 1, stop  do
                tisf(lines[i]:sub(3))
        end
        tisf('</code></pre>')
end

local handleParagraph = function(start, stop)
        tisf('<p>')
        for i=start, stop do
                tisf(trim(lines[i]))
        end
        tisf('</p>')
end

local handleSection = function(start, stop)
        while(start) do
                -- Find the next empty line or continue until the end of the section.
                -- findNextEmpty() returns the position of the empty line, so we need to
                -- subtract one from it.
                local nextEmpty = findNextEmpty(start + 1, stop)
                if(nextEmpty) then
                        nextEmpty = nextEmpty - 1
                else
                        nextEmpty = stop
                end

                local line = lines[start]
                if(not line) then
                        return
                elseif(line:match('^%S+%s*%- ')) then
                        handleArguments(start, nextEmpty, '(%S+)%s*()%- ()')
                elseif(line:sub(1, 2) == '  ') then
                        handleExamples(start, nextEmpty)
                else
                        handleParagraph(start, nextEmpty)
                end

                start = findNextEmpty(nextEmpty, stop)
                if(start) then start = start + 1 end
        end
end

local generateDocs = function(str, level)
        lines = {}
        out = {}

        for line in str:gmatch('([^\n]*)\n') do
                table.insert(lines, line:gsub('\t*', ''):sub(2))
        end

        -- The first line is always the main header.
        tisf('<h%d>%s</h%d>', level, lines[1], level)

        -- Then comes the main description.
        local offset = findNextHeader(1)

        -- Continue until two lines before the header or to the end of the comment.
        if(offset) then
                offset = offset - 2
        else
                offset = #lines
        end

        local init = findNextEmpty(1) + 1
        if(init > offset) then
                init = 2
        end

        handleSection(init, offset)

        while(true) do
                offset = findNextHeader(offset)
                if(not offset) then break end

                -- Every section has a header.
                tisf('<h%d>%s</h%d>', level + 1, lines[offset], level + 1)

                -- Find out the size of the section.
                local start = findNextEmpty(offset) + 1
                if(not lines[start]) then
                        -- There's no section here, only a headline.
                        break
                end

                local stop
                local nextHeader = findNextHeader(start)
                if(nextHeader) then
                        stop = nextHeader - 2
                else
                        local nextEmpty = findNextEmpty(start)
                        if(nextEmpty) then
                                stop = nextEmpty - 1
                        else
                                stop = #lines
                        end
                end

                handleSection(start, stop)
                offset = stop + 1
        end

        return table.concat(out, '\n')
end

local handleFile = function(path)
        local file = io.open(path, 'r')
        local content = file:read'*a'
        file:close()

        local out = {}
        local init = 1
        repeat
                local _, comStart, depth = content:find('%-%-%[(=*)%[ ', init)
                if(comStart) then
                        local comEnd = content:find('%]' .. depth .. '%]', comStart)
                        local comment = content:sub(comStart, comEnd - 1)

                        -- Convert markup to html.
                        comment = replaceMarkup(comment)

                        -- The first comment uses h1 and h2, while the subsequent ones uses h3
                        -- and h4.
                        local level = init == 1 and 1 or 3
                        table.insert(out, generateDocs(comment, init == 1 and 1 or 3))

                        init = comEnd
                end
        until not comStart

        return table.concat(out, '\n')
end

local destination = (...)
for i=2, select('#', ...) do
        local file = select(i, ...)
        local path, filename = file:match('(.+)/(.+)$')

        if(path:sub(1,3) == '../') then
                path = path:sub(4)
        end

        if(#path == 0) then path = nil end
        filename = filename:gsub('lua', 'html')

        local doc = handleFile(file)
        if(doc) then
                local dfPath = string.format('%s/%s', destination, path or '')
                os.execute(string.format('mkdir -p %s', dfPath))
                local docFile = io.open(string.format('%s/%s', dfPath, filename), 'w+')
                docFile:write(doc)
                docFile:close()
        end
end

Go to most recent revision | Compare with Previous | Blame