html_generator.lua 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371
  1. local datafile = require "datafile"
  2. local dir = require "pl.dir"
  3. local etlua = require "etlua"
  4. local plpath = require "pl.path"
  5. local pp = require "pl.pretty"
  6. local nsm = require "volksdata.namespace"
  7. local term = require "volksdata.term"
  8. local triple = require "volksdata.triple"
  9. local graph = require "volksdata.graph"
  10. local pkar = require "pocket_archive"
  11. local logger = pkar.logger
  12. local model = require "pocket_archive.model"
  13. local transformers = require "pocket_archive.transformers"
  14. local dbg = require "debugger"
  15. -- "nil" table - for missing key fallback in chaining.
  16. local NT = {}
  17. -- Default store graph to search all triples.
  18. local gr
  19. -- HTML templates. Compile them only once.
  20. -- TODO Add override for user-maintained templates.
  21. local fh, idx_tpl, dres_tpl, ores_tpl
  22. fh = datafile.open("templates/index.html")
  23. idx_tpl = assert(etlua.compile(fh:read("a")))
  24. fh:close()
  25. fh = datafile.open("templates/dres.html")
  26. dres_tpl = assert(etlua.compile(fh:read("a")))
  27. fh:close()
  28. fh = datafile.open("templates/ores.html")
  29. ores_tpl = assert(etlua.compile(fh:read("a")))
  30. fh:close()
  31. fh = datafile.open("templates/header.html")
  32. header_tpl = assert(etlua.compile(fh:read("a")))
  33. fh:close()
  34. -- HTML generator module.
  35. local M = {
  36. res_dir = plpath.join(pkar.config.htmlgen.out_dir, "res"),
  37. asset_dir = plpath.join(pkar.config.htmlgen.out_dir, "assets"),
  38. media_dir = plpath.join(pkar.config.htmlgen.out_dir, "media"),
  39. }
  40. local function get_breadcrumbs(mconf)
  41. -- Breadcrumbs, from top class to current class.
  42. -- Also verify if it's a File subclass.
  43. local breadcrumbs = {}
  44. for i = 1, #mconf.lineage do
  45. breadcrumbs[i] = {
  46. mconf.lineage[i],
  47. model.models[mconf.lineage[i]].label
  48. }
  49. end
  50. return breadcrumbs
  51. end
  52. local function get_tn_url(s)
  53. if gr:attr(s, pkar.RDF_TYPE)[pkar.FILE_T.hash] then
  54. -- The subject is a file.
  55. tn_fname = (s.data:gsub(pkar.PAR_NS, "") .. ".jpg") -- FIXME do not hardcode.
  56. return plpath.join(
  57. M.media_dir, tn_fname:sub(1, 2), tn_fname:sub(3, 4), tn_fname)
  58. end
  59. -- Recurse through all first children until one with a thumbnail, or a
  60. -- leaf without children, is found.
  61. local first_child
  62. _, first_child = next(gr:attr(s, pkar.FIRST_P))
  63. if first_child then return get_tn_url(first_child) end
  64. end
  65. local function generate_dres(s, mconf)
  66. local dmd = {}
  67. local rel = {}
  68. local children = {}
  69. local title
  70. -- Metadata
  71. local attrs = gr:connections(s, term.LINK_OUTBOUND)
  72. for p, ots in pairs(attrs) do
  73. local fname = nsm.denormalize_uri(p.data)
  74. p_label = ((mconf.properties or NT)[fname] or NT).label
  75. -- RDF types are shown in in breadcrumbs.
  76. if fname == "rdf:type" then goto skip
  77. elseif ((mconf.properties or NT)[fname] or NT).type == "rel" then
  78. -- Relationship.
  79. rel[fname] = {label = p_label, uri = fname}
  80. for _, o in pairs(ots) do table.insert(dmd[fname], o.data) end
  81. elseif fname == "pas:first" then
  82. -- Build a linked list for every first found.
  83. for _, o in pairs(ots) do
  84. -- Loop through all first children.
  85. local child_s = o
  86. logger:debug("local child_s: ", child_s.data)
  87. local ll = {}
  88. -- Fallback labels.
  89. local label
  90. _, label = next(gr:attr(child_s, pkar.DC_TITLE_P))
  91. if label then label = label.data
  92. else
  93. _, label = next(gr:attr(child_s, pkar.PATH_P))
  94. if label then label = plpath.basename(label.data)
  95. else label = child_s.data end
  96. end
  97. while child_s do
  98. -- Loop trough all next nodes for each first child.
  99. require "debugger".assert(get_tn_url(child_s))
  100. table.insert(ll, {
  101. href = pkar.gen_pairtree(
  102. "/res", child_s.data, ".html", true),
  103. label = label,
  104. tn = get_tn_url(child_s):gsub(M.media_dir, "/media/tn"),
  105. })
  106. logger:debug("Child label for ", child_s.data, ": ", ll[#ll].label or "nil")
  107. -- There can only be one "next"
  108. _, child_s = next(gr:attr(child_s, pkar.NEXT_P))
  109. end
  110. table.insert(children, ll)
  111. end
  112. elseif fname == "pas:next" then
  113. -- Sibling.
  114. for _, o in pairs(ots) do ls_next = o.data break end
  115. else
  116. -- Descriptive metadata.
  117. local attr = {label = p_label, uri = fname}
  118. -- TODO differentiate term types
  119. for _, o in pairs(ots) do table.insert(attr, o.data) end
  120. table.sort(attr)
  121. if p == pkar.DC_TITLE_P then title = attr[1] end
  122. table.insert(dmd, attr)
  123. end
  124. ::skip::
  125. end
  126. table.sort(
  127. dmd, function(a, b)
  128. return ((a.label or a.uri) < (b.label or b.uri))
  129. end
  130. )
  131. table.sort(rel)
  132. table.sort(children)
  133. logger:debug("Lineage:", pp.write(mconf.lineage))
  134. logger:debug("DMD:", pp.write(dmd))
  135. logger:debug("REL:", pp.write(rel))
  136. logger:debug("Children:", pp.write(children))
  137. logger:debug("Breadcrumbs:", pp.write(get_breadcrumbs(mconf)))
  138. out_html = dres_tpl({
  139. site_title = pkar.config.site.title or pkar.default_title,
  140. title = title or s.data,
  141. header_tpl = header_tpl,
  142. mconf = mconf,
  143. uri = s,
  144. dmd = dmd,
  145. rel = rel,
  146. children = children,
  147. ls_next = ls_next,
  148. breadcrumbs = get_breadcrumbs(mconf),
  149. rdf_href = pkar.gen_pairtree("/res", s.data, ".ttl", true),
  150. })
  151. local res_path = pkar.gen_pairtree(M.res_dir, s.data, ".html")
  152. local ofh = assert(io.open(res_path, "w"))
  153. ofh:write(out_html)
  154. ofh:close()
  155. return true
  156. end
  157. local function generate_ores(s, mconf)
  158. local techmd = {}
  159. local rel = {}
  160. -- Metadata
  161. local attrs = gr:connections(s, term.LINK_OUTBOUND)
  162. for p, ots in pairs(attrs) do
  163. local fname = nsm.denormalize_uri(p.data)
  164. p_label = ((mconf.properties or NT)[fname] or NT).label
  165. -- RDF types are shown in in breadcrumbs.
  166. if fname == "rdf:type" then goto skip
  167. elseif ((mconf.properties or NT)[fname] or NT).type == "rel" then
  168. -- Relationship.
  169. rel[fname] = {label = p_label, uri = fname}
  170. for _, o in pairs(ots) do table.insert(techmd[fname], o.data) end
  171. elseif fname == "pas:next" then
  172. -- Sibling.
  173. for _, o in pairs(ots) do ls_next = o.data break end
  174. else
  175. -- Descriptive metadata.
  176. techmd[fname] = {label = p_label, uri = fname}
  177. -- TODO differentiate term types
  178. for _, o in pairs(ots) do table.insert(techmd[fname], o.data) end
  179. table.sort(techmd[fname])
  180. end
  181. ::skip::
  182. end
  183. table.sort(techmd)
  184. table.sort(rel)
  185. logger:debug("Lineage:", pp.write(mconf.lineage))
  186. logger:debug("Breadcrumbs:", pp.write(get_breadcrumbs(mconf)))
  187. logger:debug("techmd:", pp.write(techmd))
  188. logger:debug("REL:", pp.write(rel))
  189. -- Transform and move media assets.
  190. local dest_fname, dest_dir, dest -- Reused for thumbnail.
  191. logger:info("Transforming resource file.")
  192. local res_path = techmd["pas:path"]
  193. if not res_path then error("No file path for File resource!") end
  194. local txconf = (mconf.transformers or NT).deliverable or {fn = "copy"}
  195. -- Set file name to resource ID + source extension.
  196. dest_fname = (
  197. s.data:gsub(pkar.PAR_NS, "") ..
  198. (txconf.ext or plpath.extension(res_path[1])))
  199. dest_dir = plpath.join(
  200. M.media_dir, dest_fname:sub(1, 2), dest_fname:sub(3, 4))
  201. dir.makepath(dest_dir)
  202. dest = plpath.join(dest_dir, dest_fname)
  203. assert(transformers[txconf.fn](
  204. res_path[1], dest, table.unpack(txconf or NT)))
  205. local deliverable = dest:gsub(pkar.config.htmlgen.out_dir, "")
  206. logger:info("Access file: ", deliverable)
  207. -- Thumbnail.
  208. local tn
  209. txconf = (mconf.transformers or NT).thumbnail
  210. if txconf then
  211. if txconf.ext then
  212. dest_fname = plpath.splitext(dest_fname) .. txconf.ext
  213. end
  214. dest_dir = plpath.join(
  215. M.media_dir, "tn", dest_fname:sub(1, 2), dest_fname:sub(3, 4))
  216. dir.makepath(dest_dir)
  217. dest = plpath.join(dest_dir, dest_fname)
  218. assert(transformers[txconf.fn](
  219. res_path[1], dest, table.unpack(txconf or NT)))
  220. tn = dest:gsub(M.media_dir, "/media/tn")
  221. logger:info("Thumbnail: ", tn)
  222. end
  223. out_html = ores_tpl({
  224. site_title = pkar.config.site.title or pkar.default_title,
  225. fname = plpath.basename(techmd["pas:sourcePath"][1]),
  226. header_tpl = header_tpl,
  227. mconf = mconf,
  228. uri = s,
  229. techmd = techmd,
  230. rel = rel,
  231. ls_next = ls_next,
  232. breadcrumbs = get_breadcrumbs(mconf),
  233. deliverable = deliverable,
  234. thumbnail = tn,
  235. rdf_href = pkar.gen_pairtree("/res", s.data, ".ttl", true),
  236. })
  237. local res_path = pkar.gen_pairtree(M.res_dir, s.data, ".html")
  238. local ofh = assert(io.open(res_path, "w"))
  239. ofh:write(out_html)
  240. ofh:close()
  241. return true
  242. end
  243. M.get_graph = function(s)
  244. out_gr = graph.new(nil, s.data)
  245. gr:copy(out_gr, s)
  246. return out_gr
  247. end
  248. M.generate_resource = function(s)
  249. local res_type
  250. _, res_type = next(gr:attr(s, pkar.CONTENT_TYPE_P))
  251. local mconf = model.models[res_type.data]
  252. -- Generate RDF/Turtle doc.
  253. local res_gr = M.get_graph(s)
  254. logger:debug("Serializing graph: ", s.data)
  255. local res_path = pkar.gen_pairtree(M.res_dir, s.data, ".ttl")
  256. local ofh = assert(io.open(res_path, "w"))
  257. ofh:write(res_gr:encode("ttl"))
  258. ofh:close()
  259. -- Generate HTML doc.
  260. if mconf.types["pas:File"] then return generate_ores(s, mconf)
  261. else return generate_dres(s, mconf) end
  262. end
  263. M.generate_resources = function()
  264. local subjects = gr:unique_terms(triple.POS_S)
  265. -- TODO parallelize
  266. for _, s in pairs(subjects) do assert(M.generate_resource(s)) end
  267. return true
  268. end
  269. M.generate_idx = function()
  270. local obj_idx = {}
  271. -- Get all subject of type: Artifact.
  272. s_ts = gr:term_set(
  273. pkar.RDF_TYPE, triple.POS_P,
  274. term.new_iriref_ns("pas:Artifact"), triple.POS_O
  275. )
  276. for _, s in pairs(s_ts) do
  277. local title, created
  278. _, title = next(gr:attr(s, pkar.DC_TITLE_P))
  279. _, created = next(gr:attr(s, pkar.DC_CREATED_P))
  280. local obj = {
  281. href = pkar.gen_pairtree("/res", s.data, ".html", true),
  282. title = title,
  283. created = created.data,
  284. tn = get_tn_url(s):gsub(M.media_dir, "/media/tn"),
  285. }
  286. table.insert(obj_idx, obj)
  287. end
  288. table.sort(obj_idx, function(a, b) return a.created < b.created end)
  289. logger:debug(pp.write(obj_idx))
  290. out_html = idx_tpl({
  291. title = pkar.config.site.title or pkar.default_title,
  292. header_tpl = header_tpl,
  293. nsm = nsm,
  294. obj_idx = obj_idx,
  295. })
  296. local idx_path = plpath.join(pkar.config.htmlgen.out_dir, "index.html")
  297. local ofh = assert(io.open(idx_path, "w"))
  298. logger:debug("Writing info at ", idx_path)
  299. ofh:write(out_html)
  300. ofh:close()
  301. return true
  302. end
  303. M.generate_site = function()
  304. if plpath.isdir(M.res_dir) then dir.rmtree(M.res_dir) end
  305. dir.makepath(M.res_dir)
  306. if plpath.isdir(M.asset_dir) then dir.rmtree(M.asset_dir) end
  307. dir.makepath(M.asset_dir)
  308. if plpath.isdir(M.media_dir) then dir.rmtree(M.media_dir) end
  309. dir.makepath(plpath.join(M.media_dir, "tn"))
  310. gr = graph.new(pkar.store, term.DEFAULT_CTX)
  311. assert(M.generate_resources())
  312. assert(M.generate_idx())
  313. dir.clonetree("templates/assets", plpath.dirname(M.asset_dir), dir.copyfile)
  314. end
  315. return M