#!/usr/bin/env lua version = {} do local rev = "$Revision: 1.58 $" version.revision = rev:gsub("%$[^:]+: ", ""):sub(1, -3) local date = "$Date: 2010/01/29 03:35:58 $" version.date = date:gsub("%$[^:]+: ", ""):sub(1, -3) end function generateIR(file) local state = "doc" local code = nil local doc = nil local ir = {src = {}, doc = {}, ref = {}} local unit = {} local unitName = "" local threads = nil local function defineUnit(name) if name ~= "..." then unitName = name ir.src[unitName] = ir.src[unitName] or {} unit = ir.src[unitName] end if #unit ~= 0 then unit[#unit + 1] = { type = "break" } end end local function createDoc(name) ir.doc[#ir.doc + 1] = { type = "def", name = unitName } if #unit > 0 and unit[#unit].type == "break" then ir.doc[#ir.doc].start = #unit + 1 end end local function flushDoc() if doc then ir.doc[#ir.doc + 1] = { type = "text", text = doc } if threads and #threads > 0 then ir.doc[#ir.doc].threads = threads end doc = nil end end local function flushCode() if code then unit[#unit + 1] = { type = "code", text = code } ; code = nil end end for line in io.lines(file) do if state == "doc" then local name, kind = line:match("^%<%<(.*)%>%>([=*])$") if name then flushDoc() if kind == "*" then ir.doc[#ir.doc + 1] = { type = "def*", name = name } elseif kind == "=" then defineUnit(name) createDoc(name) state = "code" else error("Internal error: unknown kind " .. kind) end elseif line == "@" or line:match("^@|.+|$") or (g_opts["noweb-compat"] and line:match("^@"))then flushDoc() threads = line:match("^@|.+|") if threads then local threadList = {} for t in threads:gmatch("|[^|]+") do threadList[#threadList + 1] = t:sub(2) end threads = threadList end else doc = (doc and doc .. "\n" or "") .. line end elseif state == "code" then if line == "@" or line:match("^@|.+|$") or (g_opts["noweb-compat"] and line:match("^@")) then flushCode() threads = line:match("^@|.+|$") if threads then local threadList = {} for t in threads:gmatch("|[^|]+") do threadList[#threadList + 1] = t:sub(2) end threads = threadList end state = "doc" else local name = line:match("^%<%<(.*)%>%>=$") or (g_opts["noweb-compat"] and line:match("^%s*%<%<(.*)%>%>=%s*$")) if name then flushCode() defineUnit(name) createDoc(name) else local pre = nil local post = nil local ref = nil if g_opts["noweb-compat"] or g_opts["indented-refs"] then pre, ref, post = line:match("^(.*)%<%<(.*)%>%>(.*)$") else ref = line:match("^%<%<(.*)%>%>$") end if ref then if pre and pre:len() > 0 then code = (code or "") .. pre end flushCode() unit[#unit + 1] = { type = "ref", name = ref } if pre then unit[#unit].indent = pre:len() end if post and post:len() > 0 then unit[#unit].followed = true end local r1 = ir.ref[unitName] r1 = r1 or { fwd = {}, back = {} } r1.fwd[#r1.fwd + 1] = ref ir.ref[unitName] = r1 local r2 = ir.ref[ref] r2 = r2 or { fwd = {}, back = {} } r2.back[#r2.back + 1] = unitName ir.ref[ref] = r2 if post and post:len() > 0 then code = post .. "\n" end else code = (code or "") .. line .. "\n" end end end end end flushCode() flushDoc() return ir end function writeIR(ir, file) local stream = io.open(file, "w") stream:write("return {\n") stream:write(" src = {\n") for k,v in pairs(ir.src) do stream:write(" [" .. string.format("%q", k) .. "] = {\n") for i,v2 in ipairs(v) do stream:write(" {\n") for k3,v3 in pairs(v2) do stream:write(" [\"" .. k3 .. "\"] = ") if type(v3) == "boolean" then stream:write(v3 and "true" or "false") else stream:write(string.format("%q", v3)) end stream:write(",\n") end stream:write(" },\n") end stream:write(" },\n") end stream:write(" },\n") stream:write(" doc = {\n") for i,v in ipairs(ir.doc) do stream:write(" {\n") for k2,v2 in pairs(v) do stream:write(" [" .. string.format("%q", k2) .. "] = ") if type(v2) == "table" then stream:write("{") for i3,v3 in ipairs(v2) do stream:write(string.format("%q", v3)) if i3 < #v2 then stream:write(",") end end stream:write("}") else stream:write(string.format("%q", v2)) end stream:write(",\n") end stream:write(" },\n") end stream:write(" },\n") stream:write(" ref = {\n") for k,v in pairs(ir.ref) do stream:write(" [" .. string.format("%q", k) .. "] = {\n") stream:write(" fwd = {") for i2,v2 in ipairs(ir.ref[k].fwd) do stream:write(string.format("%q", v2) .. ", ") end stream:write("},\n") stream:write(" back = {") for i2,v2 in ipairs(ir.ref[k].back) do stream:write(string.format("%q", v2) .. ", ") end stream:write("}\n") stream:write(" },\n") end stream:write(" }\n") stream:write("}\n") stream:close() end function readIR(file) return dofile(file) end function generateCode(ir, unit, output) if not ir.src[unit] then error("no such unit: " .. unit) end output = output or unit pcall(os.rename, output, output .. ".bak") stream, err = io.open(output, "w") if not stream then io.stderr:write("! Unable to open output file `" .. output .. "' for tangled code from unit `" .. unit .. "'.\n") io.stderr:write("! " .. err .. ". Emergency stop.\n") os.exit(1) end local x, y = pcall(GenerateCode, ir, unit, stream, 0) if not x then pcall(os.rename, output .. ".bak", output) io.stderr:write(y .. "\n") os.exit(1) end stream:write("\n") stream:close() os.remove(output .. ".bak") end function GenerateCode(ir, unit, stream, indent) --print("Indenting " .. unit .. " at " .. indent .. " spaces.") if not ir.src[unit] then error("Program module '" .. unit .. "' was not defined.") end for i,v in ipairs(ir.src[unit]) do if v.type == "code" then local s = v.text -- Insert indentation at every newline. -- Strip off the last indentation (since the code -- invariably ends with a newline). if indent > 0 then local strip = false if s:sub(s:len()) == "\n" then strip = true end s = s:gsub("\n", "\n" .. string.rep(" ", indent)) if strip then s = s:sub(1, s:len() - indent) end end -- Trim off the newline of the last entry. -- Let the referring unit decide if it wants to put something -- after it (v.type == ref && v.followed) or not. if i == #ir.src[unit] then s = s:sub(0, s:len() - 1) end stream:write(s) elseif v.type == "ref" then stream:write(string.rep(" ", indent)) GenerateCode(ir, v.name, stream, indent + (v.indent or 0)) if i < #ir.src[unit] and not v.followed then stream:write("\n") end end end end function generateDoc(ir, output, start) local stream = io.open(output, "w") if stream == nil then io.stderr:write("! Could not write to location `" .. output .. "'.\n") os.exit(1) end local texTrans = { ["{"] = "\\{", ["}"] = "\\}", ["_"] = "\\_", ["-"] = "-{}", ["<"] = "<{}", [">"] = ">{}", ["\\"] = "\\verb+\\+", ["|"] = "\\verb+|+" } local counter = 0 local subcounter = {} local threads = nil local function threadMatch() if g_opts["thread"] == nil then return true elseif threads == nil then return false else for i,v in ipairs(threads) do if g_opts["thread"] == v then return true end end end return false end local function getSubLabel(origName) local sublabel = subcounter[origName] if not sublabel then local file = file:match("([^/.]+)%.[^/.]+$") if not file then error("Could not determine file name.") end file = file:gsub("%s+", "_") local name = origName:gsub("%s+", "_") sublabel = { major = counter, minor = 0 } setmetatable(sublabel, { __tostring = function (e) return string.format("tensile:lbl:%s:%s-%s-%s", file, name, e.major, e.minor) end }) subcounter[origName] = sublabel end return sublabel end local function GenerateDoc(unit, stream, start, ...) options = {...} nonstop = options[1] and options[1].nonstop for i = start or 1, #unit do local v = unit[i] if v.type == "code" then local s = v.text if hook.doc.source.preExpand then s = hook.doc.source.preExpand(s) end if g_opts.expand_unsafe_tex then s = s:gsub("[{}_%-%|\\<>]", texTrans) end if hook.doc.source.postExpand then s = hook.doc.source.postExpand(s) end stream:write(s) elseif v.type == "ref" then stream:write("\\tnslStartUnitName{}" .. v.name) if g_opts["defn-page"] then stream:write("~{\\rm\\subpageref{") local s = tostring(getSubLabel(v.name)) s = s:gsub("-%d+$", "-0") stream:write(s .. "}}") end stream:write("\\tnslEndUnitName{}") if not v.followed then stream:write("\n") end elseif v.type == "break" and not nonstop then break end end end for i,v in ipairs(ir.doc) do if v.type == "text" then threads = v.threads if threadMatch() then if g_opts["source-code"] then stream:write("\\tnslBeginDoc{" .. counter .. "}\\tnslDocPar\n") end if hook.doc.text then stream:write(hook.doc.text(v.text)) else stream:write(v.text) end if g_opts["source-code"] then stream:write("\n\\tnslEndDoc{}\n") end end elseif v.type:match("^def") and g_opts["source-code"] and threadMatch() then local nonstop = false if v.type == "def*" then nonstop = true end stream:write("\\tnslBeginCode{" .. counter .. "}") local sublabel = nil if not nonstop then sublabel = getSubLabel(v.name) if not sublabel.defined then sublabel.defined = true else sublabel.minor = sublabel.minor + 1 end stream:write("\\sublabel{" .. tostring(sublabel) .. "}") if g_opts["margin-tags"] then stream:write("\\tnslMarginTag{{\\subpageref{") stream:write(tostring(sublabel)) stream:write("}}}") end end stream:write("\\tnslBeginUnitDef{" .. v.name) if not nonstop and g_opts["defn-page"] then stream:write("~{\\subpageref{") local s = tostring(sublabel) s = v.start and s:gsub("-%d+$", "-0") or s stream:write(s .. "}}") end stream:write("}\\tnslEndUnitDef" .. (v.start and "Plus" or "")) stream:write("\\tnslBeginDefLine") if g_opts["back-refs"] and ir.ref[v.name] and #ir.ref[v.name].back > 0 then stream:write("\\tnslBackRef{\\\\{") stream:write(tostring(getSubLabel(ir.ref[v.name].back[1]))) stream:write("}}") end stream:write("\\tnslEndDefLine") stream:write("\n") if nonstop then GenerateDoc(ir.src[v.name], stream, v.start, {nonstop=true}) else GenerateDoc(ir.src[v.name], stream, v.start) end if g_opts["back-refs"] and ir.ref[v.name] and #ir.ref[v.name].back > 0 then stream:write("\\tnslUsed{\\\\{") stream:write(tostring(getSubLabel(ir.ref[v.name].back[1]))) stream:write("}}") end stream:write("\\tnslEndCode{}\n") end counter = counter + 1 end end function findTops(ir) local refs = {} local tops = {} for k,v in pairs(ir.src) do for i2,v2 in ipairs(v) do if v2.type == "ref" then refs[v2.name] = true end end end for k,v in pairs(ir.src) do if not refs[k] then tops[#tops + 1] = k end end return tops end -- Program modules to process. local units = {} -- Where to write the TeX output. local weave_output = nil g_opts = { ["extract-all"] = false , ["noweb-compat"] = false , ["indented-refs"] = false , ["show-tops"] = false , ["write-ir"] = false , ["weave"] = true , ["docs"] = true , ["margin-tags"] = true , ["defn-page"] = true , ["back-refs"] = true , ["source-code"] = true , ["expand_unsafe_tex"] = true } local i = 1 while i <= #arg do local v = arg[i] if v == "-h" or v == "-help" then io.stdout:write("This is Tensile, version " .. version.revision .. " (" .. version.date .. " UTC).\n\n") io.stdout:write([[ Tensile (c) 2009-2010 Taylor Venable. All rights reserved. Provided under the terms of the Simplified (2-Clause) BSD license. LaTeX support provided under the LaTeX Project Public License, v1.3c or later. USAGE: tensile OPTIONS: Standard: -h / -help Show this message. File Handling: -indented-refs Allow references to be indented in source. -list-tops Print all toplevel units and quit. -noweb-compat Enable Noweb-compatible parsing. -show-tops Same as "-list-tops". -write-ir Write intermediate form to file. Tangled Output: -extract-all Extract all toplevel units. -tangle-to Write single a single unit's source to . This option will be ignored if > 1 unit is tangled. -unit Tangle unit . Woven Output: -dont-weave Do not produce woven output. -hide-margin-tags Don't display definition tag number in the margin. -hide-defn-page Don't show references to first definition. -hide-back-refs Don't print references to usage location. -hide-source-code Don't output source code in documentation. -no-docs Same as "-dont-weave". -thread Only weave output for doc chunks in thread . -weave-to Write woven output to . Deprecated: -R Same as "-unit ". -o Same as "-weave-to ". Email bug reports to taylor@metasyntax.net. ]]) os.exit(0) end if v:match("^-R") then units[#units + 1] = v:sub(3) elseif v == "-unit" then if i == #arg then error("too few options") end units[#units + 1] = arg[i + 1] i = i + 1 elseif v == "-weave-to" or v == "-o" then if i == #arg then error("too few options") end weave_output = arg[i + 1] i = i + 1 elseif v == "-tangle-to" then if i == #arg then error("too few options") end g_opts["tangle-to"] = arg[i + 1] i = i + 1 elseif v == "-O" then if i == #arg then error("too few options") end local s = arg[i + 1] local k,v = s:match("^([^=]+)=(.*)$") if k and v then if ({FALSE = 1, NO = 1})[v:upper()] then v = false elseif ({TRUE = 1, YES = 1})[v:upper()] then v = true elseif ({NIL = 1, NULL = 1})[v:upper()] then v = nil end g_opts[k] = v end i = i + 1 elseif v == "-thread" then if i == #arg then error("too few options") end g_opts["thread"] = arg[i + 1] i = i + 1 elseif v:match("^%-") then if v:match("^%-hide%-") then g_opts[v:sub(7)] = false elseif v:match("^%-omit%-") then g_opts[v:sub(7)] = false elseif v:match("^%-elide%-") then g_opts[v:sub(8)] = false elseif v:match("^%-no%-") then g_opts[v:sub(5)] = false elseif v:match("^%-dont%-") then g_opts[v:sub(7)] = false else g_opts[v:sub(2)] = true end else file = v end i = i + 1 end if not file then io.stderr:write("! No file given.\n") os.exit(1) end local x, err = io.open(file, "r") if not x then local y = io.open(file .. ".tnsl", "r") if not y then io.stderr:write("! Unable to open input file `" .. file .. "' as literate source.\n") io.stderr:write("! " .. err .. ". Emergency stop.\n") os.exit(1) end y:close() file = file .. ".tnsl" else x:close() end hook = {} hook.doc = {} hook.doc.source = {} hook.src = {} if os.getenv("HOME") then pcall(loadfile(os.getenv("HOME") .. "/.tensilerc")) end local ir if g_opts["write-ir"] then local tangled = file .. ".tensile" writeIR(generateIR(file), tangled) ir = readIR(tangled) else ir = generateIR(file) end local tops = findTops(ir) for i,v in ipairs(tops) do tops[v] = true end if g_opts["list-tops"] or g_opts["show-tops"] then for _,unit in ipairs(tops) do print(unit) end os.exit(0) end if g_opts["extract-all"] then units = tops end if #units > 1 then g_opts["tangle-to"] = nil end for i,v in ipairs(units) do if not tops[v] then print("WARNING: " .. v .. " is not a toplevel module.") end generateCode(ir, v, g_opts["tangle-to"]) end if g_opts.weave and g_opts.docs then weave_output = weave_output or file:gsub("\.[^.]+$", "") .. ".tex" generateDoc(ir, weave_output) end