require "lfs" require "eluadoc" require "md5" -- Uncomment this when generating offline docs local is_offline = true -- Languages in the system -- NOTE: "en" must ALWAYS be the first entry in this array! -- NOTE: all languages must be given in lowercase only! -- ## Obs: PT going offline in July 2010 for lack of support. -- We hope to offer it again and I'll keep maintaining (offline) what I can. --languages = { "en", "pt" } languages = { "en" } -- Reverse lookup (language to idx) dictionary local langidx = {} for k, v in ipairs( languages ) do langidx[ v ] = k end local sf = string.format local cache_invalid = false ------------------------------------------------------------------------------- -- Indexes into our menu table (defined in docdata.lua) name_idx, link_idx, submenu_idx , title_idx = 1, 2, 3, 4 ------------------------------------------------------------------------------- -- "getstr" support (return strings in different languages) -- If defaults to english (but gives a warning) if the string isn't found in the given language -- This table keeps the strings we already emitted warnings for -- After all, we don't want to drive the user crazy local warned = {} function getstr( str, lang ) -- Get the language index from langidx local idx = langidx[ lang ] if not idx then error( string.format( "Invalid language %s", lang ) ) end -- Look from the string in the "translations" table local where for _, v in ipairs( translations ) do if v[ 1 ] == str then where = v break end end if not where then error( string.format( "String %s not found in translations" , str ) ) end -- Try to return the value in the specified language -- If not possible, return the value in english, but issue a warning first local res = where[ idx ] if not res then res = where[ 1 ] if not warned[ str ] then print( string.format( "*** WARNING: translation for '%s' in language '%s' not found!", str, lang ) ) warned[ str ] = true end end return res end ------------------------------------------------------------------------------- -- Generic helper functions -- Remove anchor from a link of the form a/b.../baselink.html#anchor local function get_base_link( name ) return ( name:gsub( "#.*", "" ) ) end -- Get the menu field for a given item and language -- Returns the english name is the field for the specified language can't be found local function get_menu_field( menuitem, lang, fieldidx ) if not menuitem[ fieldidx ] then return nil else if type( menuitem[ fieldidx ] ) == "string" then return menuitem[ fieldidx ] else local lidx = langidx[ lang ] return menuitem[ fieldidx ][ lidx ] or menuitem[ fieldidx ][ 1 ] end end end -- Get the menu name for a given menu item and a language -- Returns the english name if the name for the specified language can't be found local function get_menu_name( menuitem, lang ) return get_menu_field( menuitem, lang, name_idx ) end -- Get the link for a given menu item and a language -- Returns the english name if the name for the specified language can't be found -- If the link field doesn't exists, the name is returned instead local function get_menu_title( menuitem, lang ) return "eLua - " .. ( get_menu_field( menuitem, lang, title_idx ) or get_menu_field( menuitem, lang, name_idx ) ) end -- Set "print" to print indented (with 2 spaces) local oldprint local function indent_print() oldprint = print print = function( ... ) io.write( " " ); oldprint( ... ) end end -- Restore the "regular" print function local function regular_print() print = oldprint end ------------------------------------------------------------------------------- -- File/directory operations helpers -- Copy the given file to the 'dest' directory -- Doesn't do error checking local function copy_file( fname, dst ) local destname = fname if fname:find( "/" ) then -- Get only the filename from the path local sidx for f = #fname, 1, -1 do if fname:sub( f, f ) == "/" then sidx = f break end end destname = fname:sub( sidx + 1 ) end local fsrc = io.open( fname, "rb" ) local fdst = io.open( string.format( "%s/%s", dst, destname ), "wb" ) local data = fsrc:read( "*a" ) fdst:write( data ) fsrc:close() fdst:close() end -- Copy the 'src' directory to the 'dst' directory, going recursively through -- its content. Doesn't do error checking. local function copy_dir_rec( src, dst ) for f in lfs.dir( src ) do local oldf = string.format( "%s/%s", src, f ) local attrs = lfs.attributes( oldf ) if attrs.mode == 'directory' and f ~= "." and f ~= ".." and f ~= ".svn" and f ~= ".git" then local newdir = string.format( "%s/%s", dst, f ) lfs.mkdir( newdir ) copy_dir_rec( oldf, newdir ) elseif attrs.mode == 'file' then copy_file( oldf, dst ) end end end -- Remove a directory recusively -- USE WITH CARE!! Doesn't do much checks :) local function rm_dir_rec( dirname ) for f in lfs.dir( dirname ) do local ename = string.format( "%s/%s", dirname, f ) local attrs = lfs.attributes( ename ) if attrs.mode == 'directory' and f ~= '.' and f ~= '..' then rm_dir_rec( ename ) elseif attrs.mode == 'file' or attrs.mode == 'named pipe' or attrs.mode == 'link' then os.remove( ename ) end end lfs.rmdir( dirname ) end -- Copy a directory to another directory local function copy_dir( src, dst ) local newdir = string.format( "%s/%s", dst, src ) lfs.mkdir( newdir ) copy_dir_rec( src, newdir ) end ------------------------------------------------------------------------------- -- Cache helpers local function read_md5( filename ) local fullname = string.format( "cache/%s.cache", filename ) local f = io.open( fullname, "rb" ) if not f then return "" end local d = f:read( "*a" ) f:close() return d end local function write_md5( filename, d ) local fullname = string.format( "cache/%s.cache", filename ) local f = io.open( fullname, "wb" ) if not f then return false end f:write( d ) f:close() return true end local function file_md5( filename ) local f = io.open( filename, "rb" ) if not f then return "" end local d = f:read( "*a" ) f:close() return md5.sumhexa( d ) end ------------------------------------------------------------------------------- -- Table utils (from http://lua-users.org/wiki/TableUtils) function table.val_to_str( v ) if "string" == type( v ) then v = string.gsub( v, "\n", "\\n" ) if string.match( string.gsub(v,"[^'\"]",""), '^"+$' ) then return "'" .. v .. "'" end return '"' .. string.gsub(v,'"', '\\"' ) .. '"' else return "table" == type( v ) and table.tostring( v ) or tostring( v ) end end function table.key_to_str ( k ) if "string" == type( k ) and string.match( k, "^[_%a][_%a%d]*$" ) then return k else return "[" .. table.val_to_str( k ) .. "]" end end function table.tostring( tbl ) local result, done = {}, {} for k, v in ipairs( tbl ) do table.insert( result, table.val_to_str( v ) ) done[ k ] = true end for k, v in pairs( tbl ) do if not done[ k ] then table.insert( result, table.key_to_str( k ) .. "=" .. table.val_to_str( v ) ) end end return "{" .. table.concat( result, "," ) .. "}" end ------------------------------------------------------------------------------- -- Build the list of files that must be processed starting from the menu data -- Traverse a second (or higher) level menu and add relevant information to flist local function traverse_list( item, parentid, flist ) if not item[ link_idx ] then return end local base = get_base_link( item[ link_idx ] ) if base ~= "" and not flist[ base ] then flist[ base ] = { parentid = parentid, item = item } end if item[ submenu_idx ] then for i = 1, #item[ submenu_idx ] do traverse_list( item[ submenu_idx ][ i ], parentid, flist ) end end end -- Iterate over the menu list, building the list of files that must be -- processed by the doc generator. Returns a dictionary with list, parent_id -- pairs where parent_id is the parent menu of link in themenu local function get_file_list() local flist = {} for i = 1, #themenu do traverse_list( themenu[ i ], i, flist ) end return flist end -- Returns true if the given string begins with the given substring, false otherwise -- The comparation is case-insensitive local function beginswith( str, prefix ) return str:sub( 1, #prefix ):lower() == prefix:lower() end ------------------------------------------------------------------------------- -- Build the navigation data for a given page -- Helper function: format a link starting from language and link -- Links marked as "#" ("null" links) are left alone -- Links that begin with "http(s)://" are unchanged local function get_link( lang, link ) if link == "#" then return "#" elseif link:find( "https?://" ) == 1 then return link else return string.format( "%s_%s", lang, link ) end end -- Helper for gen_html_nav: generate the submenu(s) for a given top level menu item local function gen_submenus( item, lang, level ) level = level or 1 local data = '' local lidx = langidx[ lang ] local arrptr = 'right arrow' for i = 1, #item do local l = item[ i ] if l[ submenu_idx ] then data = data .. string.rep( " ", level * 2 + 8 ) .. string.format( '
  • %s%s\n', get_link( lang, l[ link_idx ] ), arrptr, get_menu_name( l, lang ) ) data = data .. string.rep( " ", level * 2 + 8 ) .. "
  • \n" else if get_menu_name( l, lang ) then data = data .. string.rep( " ", level * 2 + 8 ) .. string.format( '
  • %s
  • \n', get_link( lang, l[ link_idx ] ), get_menu_name( l, lang ) ) end end end return data end -- Generate the HTML menu structure for the given language and parentid -- If "is_offline" is true, don't generate links to the counter and the BerliOS logo local function gen_html_nav( parentid, lang ) local htmlstr = [[ ]], offline_data ) return htmlstr end -- Helper function: replace local links with links prefixed by language local function language_for_links( lang, orig ) -- Iterate through all the links in the document and change the local ones with -- the correct language option orig = orig:gsub( [==[]==], function( link ) if beginswith( link, "#" ) or beginswith( link, "http://" ) or beginswith( link, "https://" ) or beginswith( link, "ftp://" ) then return string.format( '', link ) else return string.format( '', lang, link ) end end ) return orig end ------------------------------------------------------------------------------- -- Build the logo for a given language local function gen_logo( fname, lang ) local numl = #languages local langdata = '' for i = 1, numl do local crtlang = languages[ i ] local hlang = crtlang:sub( 1, 1 ):upper() .. crtlang:sub( 2, -1 ) if lang:lower() == crtlang:lower() then langdata = langdata .. string.format('
    %s
    \n', hlang, crtlang ) else langdata = langdata .. string.format('
    %s
    \n', crtlang:lower(), fname, hlang, crtlang ) end end return string.format( [[
    ]], getstr( "eLua - Embedded Lua", lang ), numl + 1, getstr( "Search", lang ), getstr( "Language", lang ), langdata:sub( 1, -2 ) ) end ------------------------------------------------------------------------------- -- Generate an actual HTML page starting from a template -- Replace the $$HEADER$$ and $$FOOTER$$ with proper data local function gen_html_page( fname, lang ) local entry = flist[ fname ] local parentid = entry.parentid local item = entry.item -- Open and read file local fullname = string.format( "%s/%s", lang, fname ) local f = io.open( fullname, "rb" ) if not f then fullname = fullname:gsub( "%.html", "%.txt" ) f = io.open( fullname, "rb" ) if not f then return nil, string.format( "Error opening %s", fullname ) end end local orig = f:read( "*a" ) f:close() -- Check cache local cfilename = string.format( "%s_%s", lang, fname ) local oldsum = read_md5( cfilename ) local crtsum = md5.sumhexa( orig ) if oldsum == crtsum then if not cache_invalid then return nil, "#cached#" end else write_md5( cfilename, crtsum ) end local asciimode = fullname:find( "%.txt" ) -- Check the presence of $$HEADER$$ and $$FOOTER$$ if not orig:find( "%$%$HEADER%$%$" ) or not orig:find( "%$%$FOOTER%$%$" ) then return nil, string.format( "%s not formated properly ($$HEADER$$ or $$FOOTER$$ not found)", fullname ) end if not asciimode then print "" -- Anticipate some common errors and fix them directly orig = orig:gsub( "
    ", "
    " ) orig = orig:gsub( '()([^\n]-)%s-\n', function( anchor, data ) return anchor:gsub( ">", " />" ) .. data .. "\n" end ) orig = orig:gsub( '

    (.-)

    ', "
    %1
    " ) orig = orig:gsub( 'target="_blank"', "" ) else print( "(AsciiDoc mode)" ) -- Call "asciidoc" to generate the actual HTML local tempname = fullname .. '.temp' os.execute( sf( "asciidoc -s -a icons -a 'newline=\\n' -b xhtml11 -o %s %s", tempname, fullname ) ) local resfile = io.open( tempname, "rb" ) if not resfile then return nil, sf( "Unable to find the AsciiDoc generated file %s", tempname ) end orig = resfile:read( "*a" ) resfile:close() orig = "$$HEADER$$\n" .. orig .. "$$FOOTER$$\n" os.remove( tempname ) end -- Replace local links with language-dependent links orig = language_for_links( lang, orig ) -- Generate actual data local header = string.format( [=[ %s ]=], get_menu_title( item, lang ) ) header = header .. gen_logo( fname, lang ) .. "\n" local menuitems = gen_html_nav( parentid, lang ) header = header .. menuitems .. '
    \n' .. ( asciimode and "" or '
    ' ) local footer = ( asciimode and "" or '
    ' ) .. [[
    ]] orig = orig:gsub( "%$%$HEADER%$%$", header ) orig = orig:gsub( "%$%$FOOTER%$%$", footer ) return orig end ------------------------------------------------------------------------------- -- Documentation generator -- Helper function: iterate through the menu and replace automatically generated content local function replace_auto_content( automenus, item ) if type( item[ submenu_idx ] ) == "string" then local r = automenus[ item[ submenu_idx ] ] if not r then return string.format( "Autogenerated menu '%s' not found", item[ submenu_idx ] ) else print( string.format( "Replaced autogenerated menu '%s'", item[ submenu_idx ] ) ) item[ submenu_idx ] = r end elseif type( item[ submenu_idx ] ) == "table" then for i = 1, #item[ submenu_idx ] do replace_auto_content( automenus, item[ submenu_idx ][ i ] ) end end end -- Argument check local args = { ... } local destdir = "dist" local destdiridx = 1 if #args > 2 then print "Usage: buildall.lua [destdir] [-online] [-clean]" print "Use -online to generate online documentation (includes BerliOS logo and counter)" print "Use -clean to clear the cache and generate clean documentation" return end local cleancache = false for i = 1, #args do if args[ i ] == "-online" then is_offline = false elseif args[ i ] == "-clean" then cleancache = true else destdir = args[ i ] end end print( sf( "Using '%s' as the destination directory", destdir ) ); -- Read the documentation data themenu, translations, fixed = dofile( "docdata.lua" ) if not themenu or not translations or not fixed then print "docdata.lua doesn't return the proper data, aborting." return end -- Add the content generated from eluadoc to our menu(s) print "Generating HTML documentation..." indent_print() local automenus, genfiles = eluadoc.gen_html_doc() if not automenus then return end regular_print() -- Replace content generated by gen_html_doc in the menu for i = 1, #themenu do local replerr = replace_auto_content( automenus, themenu[ i ] ) if replerr then print( replerr ) return end end print( "done" ) -- If the destination directory doesn't exist, create it -- If it exists, remove it local attr = lfs.attributes( destdir ) if not attr then if not lfs.mkdir( destdir ) then print( string.format( "Unable to create directory %s", destdir ) ) return end else if attr.mode ~= "directory" then print( string.format( "%s is not a directory", destdir ) ) return end rm_dir_rec( destdir ) lfs.mkdir( destdir ) end -- If the cache must be cleared, do it now if cleancache then local attr = lfs.attributes( 'cache' ) if attr then if attr.mode ~= "directory" then print( "'cache' is not a directory" ) return end rm_dir_rec( 'cache' ) lfs.mkdir( 'cache' ) end end -- Create the cache directory if it doesn't exist local attr = lfs.attributes( 'cache' ) if not attr then if not lfs.mkdir( 'cache' ) then print( "Unable to create cache directory" ) return end end -- Set the global "cache invalid" flag -- It is set to 'true' if the content of docdata.lua changes local crtdocsum = md5.sumhexa( table.tostring( themenu ) ) local oldsum = read_md5( "docdata" ) cache_invalid = crtdocsum ~= oldsum if cache_invalid then write_md5( "docdata", crtdocsum ) print "Cache invalidated" end print "\nProcessing HTML templates..." indent_print() flist = get_file_list() for _, lang in ipairs( languages ) do for fname, entry in pairs( flist ) do if fname:find( "https?://" ) ~= 1 then -- not a filename but a direct link io.write( string.format( "Processing %s %s...", fname, entry.item[ name_idx ] and "" or "(hidden entry)" ) ) local res, err = gen_html_page( fname, lang ) if err == "#cached#" then -- This file is already in the cache print( " (cached)" ) elseif not res then print( "***" .. err ) else local g = io.open( string.format( "cache/%s_%s", lang, fname ), "wb" ) if not g then print( string.format( "Unable to open %s for writing", fname ) ) else g:write( res ) g:close() end end -- Copy file from cache to destination directory local srcf = io.open( string.format( "cache/%s_%s", lang, fname ), "rb" ) local destf = io.open( string.format( "%s/%s_%s", destdir, lang, fname ), "wb" ) if not srcf or not destf then print "Unable to copy file from cache to dist" return end local content = srcf:read( "*a" ) destf:write( content ) srcf:close() destf:close() end end end regular_print() print "done" -- Now copy the fixed content in the documentation directory print "\nCopying fixed content ..." indent_print() for _, v in ipairs( fixed ) do print( string.format( "Copying %s", v ) ) if v:sub( -1 ) == "/" then copy_dir( v, destdir ) else copy_file( v, destdir ) end end regular_print() print "done" -- And delete the files generated by eluadoc print "\nCleaning up files generated by eluadoc..." indent_print() for _, v in pairs( genfiles ) do print( string.format( "Deleting %s...", v ) ) os.remove( v ) end regular_print() print "done" print( string.format( "\nEnjoy your documentation in %s :)", destdir ) )