local datafile = require "datafile" local dir = require "pl.dir" local etlua = require "etlua" local plpath = require "pl.path" local pp = require "pl.pretty" local nsm = require "volksdata.namespace" local term = require "volksdata.term" local triple = require "volksdata.triple" local graph = require "volksdata.graph" local pkar = require "pocket_archive" local logger = pkar.logger local model = require "pocket_archive.model" local transformers = require "pocket_archive.transformers" local dbg = require "debugger" -- "nil" table - for missing key fallback in chaining. local NT = {} -- Default store graph to search all triples. local gr -- HTML templates. Compile them only once. -- TODO Add override for user-maintained templates. local fh, idx_tpl, dres_tpl, ores_tpl fh = datafile.open("templates/index.html") idx_tpl = assert(etlua.compile(fh:read("a"))) fh:close() fh = datafile.open("templates/dres.html") dres_tpl = assert(etlua.compile(fh:read("a"))) fh:close() fh = datafile.open("templates/ores.html") ores_tpl = assert(etlua.compile(fh:read("a"))) fh:close() fh = datafile.open("templates/header.html") header_tpl = assert(etlua.compile(fh:read("a"))) fh:close() -- HTML generator module. local M = { res_dir = plpath.join(pkar.config.htmlgen.out_dir, "res"), asset_dir = plpath.join(pkar.config.htmlgen.out_dir, "assets"), media_dir = plpath.join(pkar.config.htmlgen.out_dir, "media"), } local function get_breadcrumbs(mconf) -- Breadcrumbs, from top class to current class. -- Also verify if it's a File subclass. local breadcrumbs = {} for i = 1, #mconf.lineage do breadcrumbs[i] = { mconf.lineage[i], model.models[mconf.lineage[i]].label } end return breadcrumbs end local function get_tn_url(s) if gr:attr(s, pkar.RDF_TYPE)[pkar.FILE_T.hash] then -- The subject is a file. tn_fname = (s.data:gsub(pkar.PAR_NS, "") .. ".jpg") -- FIXME do not hardcode. return plpath.join( M.media_dir, tn_fname:sub(1, 2), tn_fname:sub(3, 4), tn_fname) end -- Recurse through all first children until one with a thumbnail, or a -- leaf without children, is found. local first_child _, first_child = next(gr:attr(s, pkar.FIRST_P)) if first_child then return get_tn_url(first_child) end end local function generate_dres(s, mconf) local dmd = {} local rel = {} local children = {} local title -- Metadata local attrs = gr:connections(s, term.LINK_OUTBOUND) for p, ots in pairs(attrs) do local fname = nsm.denormalize_uri(p.data) p_label = ((mconf.properties or NT)[fname] or NT).label -- RDF types are shown in in breadcrumbs. if fname == "rdf:type" then goto skip elseif ((mconf.properties or NT)[fname] or NT).type == "rel" then -- Relationship. rel[fname] = {label = p_label, uri = fname} for _, o in pairs(ots) do table.insert(dmd[fname], o.data) end elseif fname == "pas:first" then -- Build a linked list for every first found. for _, o in pairs(ots) do -- Loop through all first children. local child_s = o logger:debug("local child_s: ", child_s.data) local ll = {} -- Fallback labels. local label _, label = next(gr:attr(child_s, pkar.DC_TITLE_P)) if label then label = label.data else _, label = next(gr:attr(child_s, pkar.PATH_P)) if label then label = plpath.basename(label.data) else label = child_s.data end end while child_s do -- Loop trough all next nodes for each first child. require "debugger".assert(get_tn_url(child_s)) table.insert(ll, { href = pkar.gen_pairtree( "/res", child_s.data, ".html", true), label = label, tn = get_tn_url(child_s):gsub(M.media_dir, "/media/tn"), }) logger:debug("Child label for ", child_s.data, ": ", ll[#ll].label or "nil") -- There can only be one "next" _, child_s = next(gr:attr(child_s, pkar.NEXT_P)) end table.insert(children, ll) end elseif fname == "pas:next" then -- Sibling. for _, o in pairs(ots) do ls_next = o.data break end else -- Descriptive metadata. local attr = {label = p_label, uri = fname} -- TODO differentiate term types for _, o in pairs(ots) do table.insert(attr, o.data) end table.sort(attr) if p == pkar.DC_TITLE_P then title = attr[1] end table.insert(dmd, attr) end ::skip:: end table.sort( dmd, function(a, b) return ((a.label or a.uri) < (b.label or b.uri)) end ) table.sort(rel) table.sort(children) logger:debug("Lineage:", pp.write(mconf.lineage)) logger:debug("DMD:", pp.write(dmd)) logger:debug("REL:", pp.write(rel)) logger:debug("Children:", pp.write(children)) logger:debug("Breadcrumbs:", pp.write(get_breadcrumbs(mconf))) out_html = dres_tpl({ site_title = pkar.config.site.title or pkar.default_title, title = title or s.data, header_tpl = header_tpl, mconf = mconf, uri = s, dmd = dmd, rel = rel, children = children, ls_next = ls_next, breadcrumbs = get_breadcrumbs(mconf), rdf_href = pkar.gen_pairtree("/res", s.data, ".ttl", true), }) local res_path = pkar.gen_pairtree(M.res_dir, s.data, ".html") local ofh = assert(io.open(res_path, "w")) ofh:write(out_html) ofh:close() return true end local function generate_ores(s, mconf) local techmd = {} local rel = {} -- Metadata local attrs = gr:connections(s, term.LINK_OUTBOUND) for p, ots in pairs(attrs) do local fname = nsm.denormalize_uri(p.data) p_label = ((mconf.properties or NT)[fname] or NT).label -- RDF types are shown in in breadcrumbs. if fname == "rdf:type" then goto skip elseif ((mconf.properties or NT)[fname] or NT).type == "rel" then -- Relationship. rel[fname] = {label = p_label, uri = fname} for _, o in pairs(ots) do table.insert(techmd[fname], o.data) end elseif fname == "pas:next" then -- Sibling. for _, o in pairs(ots) do ls_next = o.data break end else -- Descriptive metadata. techmd[fname] = {label = p_label, uri = fname} -- TODO differentiate term types for _, o in pairs(ots) do table.insert(techmd[fname], o.data) end table.sort(techmd[fname]) end ::skip:: end table.sort(techmd) table.sort(rel) logger:debug("Lineage:", pp.write(mconf.lineage)) logger:debug("Breadcrumbs:", pp.write(get_breadcrumbs(mconf))) logger:debug("techmd:", pp.write(techmd)) logger:debug("REL:", pp.write(rel)) -- Transform and move media assets. local dest_fname, dest_dir, dest -- Reused for thumbnail. logger:info("Transforming resource file.") local res_path = techmd["pas:path"] if not res_path then error("No file path for File resource!") end local txconf = (mconf.transformers or NT).deliverable or {fn = "copy"} -- Set file name to resource ID + source extension. dest_fname = ( s.data:gsub(pkar.PAR_NS, "") .. (txconf.ext or plpath.extension(res_path[1]))) dest_dir = plpath.join( M.media_dir, dest_fname:sub(1, 2), dest_fname:sub(3, 4)) dir.makepath(dest_dir) dest = plpath.join(dest_dir, dest_fname) assert(transformers[txconf.fn]( res_path[1], dest, table.unpack(txconf or NT))) local deliverable = dest:gsub(pkar.config.htmlgen.out_dir, "") logger:info("Access file: ", deliverable) -- Thumbnail. local tn txconf = (mconf.transformers or NT).thumbnail if txconf then if txconf.ext then dest_fname = plpath.splitext(dest_fname) .. txconf.ext end dest_dir = plpath.join( M.media_dir, "tn", dest_fname:sub(1, 2), dest_fname:sub(3, 4)) dir.makepath(dest_dir) dest = plpath.join(dest_dir, dest_fname) assert(transformers[txconf.fn]( res_path[1], dest, table.unpack(txconf or NT))) tn = dest:gsub(M.media_dir, "/media/tn") logger:info("Thumbnail: ", tn) end out_html = ores_tpl({ site_title = pkar.config.site.title or pkar.default_title, fname = plpath.basename(techmd["pas:sourcePath"][1]), header_tpl = header_tpl, mconf = mconf, uri = s, techmd = techmd, rel = rel, ls_next = ls_next, breadcrumbs = get_breadcrumbs(mconf), deliverable = deliverable, thumbnail = tn, rdf_href = pkar.gen_pairtree("/res", s.data, ".ttl", true), }) local res_path = pkar.gen_pairtree(M.res_dir, s.data, ".html") local ofh = assert(io.open(res_path, "w")) ofh:write(out_html) ofh:close() return true end M.get_graph = function(s) out_gr = graph.new(nil, s.data) gr:copy(out_gr, s) return out_gr end M.generate_resource = function(s) local res_type _, res_type = next(gr:attr(s, pkar.CONTENT_TYPE_P)) local mconf = model.models[res_type.data] -- Generate RDF/Turtle doc. local res_gr = M.get_graph(s) logger:debug("Serializing graph: ", s.data) local res_path = pkar.gen_pairtree(M.res_dir, s.data, ".ttl") local ofh = assert(io.open(res_path, "w")) ofh:write(res_gr:encode("ttl")) ofh:close() -- Generate HTML doc. if mconf.types["pas:File"] then return generate_ores(s, mconf) else return generate_dres(s, mconf) end end M.generate_resources = function() local subjects = gr:unique_terms(triple.POS_S) -- TODO parallelize for _, s in pairs(subjects) do assert(M.generate_resource(s)) end return true end M.generate_idx = function() local obj_idx = {} -- Get all subject of type: Artifact. s_ts = gr:term_set( pkar.RDF_TYPE, triple.POS_P, term.new_iriref_ns("pas:Artifact"), triple.POS_O ) for _, s in pairs(s_ts) do local title, created _, title = next(gr:attr(s, pkar.DC_TITLE_P)) _, created = next(gr:attr(s, pkar.DC_CREATED_P)) local obj = { href = pkar.gen_pairtree("/res", s.data, ".html", true), title = title, created = created.data, tn = get_tn_url(s):gsub(M.media_dir, "/media/tn"), } table.insert(obj_idx, obj) end table.sort(obj_idx, function(a, b) return a.created < b.created end) logger:debug(pp.write(obj_idx)) out_html = idx_tpl({ title = pkar.config.site.title or pkar.default_title, header_tpl = header_tpl, nsm = nsm, obj_idx = obj_idx, }) local idx_path = plpath.join(pkar.config.htmlgen.out_dir, "index.html") local ofh = assert(io.open(idx_path, "w")) logger:debug("Writing info at ", idx_path) ofh:write(out_html) ofh:close() return true end M.generate_site = function() if plpath.isdir(M.res_dir) then dir.rmtree(M.res_dir) end dir.makepath(M.res_dir) if plpath.isdir(M.asset_dir) then dir.rmtree(M.asset_dir) end dir.makepath(M.asset_dir) if plpath.isdir(M.media_dir) then dir.rmtree(M.media_dir) end dir.makepath(plpath.join(M.media_dir, "tn")) gr = graph.new(pkar.store, term.DEFAULT_CTX) assert(M.generate_resources()) assert(M.generate_idx()) dir.clonetree("templates/assets", plpath.dirname(M.asset_dir), dir.copyfile) end return M