generator.lua 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750
  1. local csv = require "ftcsv"
  2. local datafile = require "datafile"
  3. local dir = require "pl.dir"
  4. local etlua = require "etlua"
  5. local json = require "cjson"
  6. local markdown = require "markdown"
  7. local path = require "pl.path"
  8. local pp = require "pl.pretty"
  9. local nsm = require "volksdata.namespace"
  10. local term = require "volksdata.term"
  11. local triple = require "volksdata.triple"
  12. local graph = require "volksdata.graph"
  13. local pkar = require "pocket_archive"
  14. local logger = pkar.logger
  15. local model = require "pocket_archive.model"
  16. local repo = require "pocket_archive.repo"
  17. local get_single_v = repo.get_single_v
  18. local transformers = require "pocket_archive.transformers"
  19. local dbg = require "debugger"
  20. -- "nil" table - for missing key fallback in chaining.
  21. local NT = {}
  22. -- Extension for type-based icon files.
  23. local ICON_EXT = ".svg"
  24. local asset_dir = pkar.config.htmlgen.out_dir
  25. local index_path = path.join(asset_dir, "js", "fuse_index.json")
  26. local keys_path = path.join(asset_dir, "js", "fuse_keys.json")
  27. local idx_ignore = {first = true, next = true}
  28. -- Collector for all search term keys.
  29. local idx_keys
  30. -- HTML templates. Compile them only once.
  31. -- TODO Add override for user-maintained templates.
  32. local templates = {
  33. idx = {file = "index.html"},
  34. coll = {file = "coll.html"},
  35. dres = {file = "dres.html"},
  36. ores = {file = "ores.html"},
  37. head = {file = "head_common.html"},
  38. header = {file = "header.html"},
  39. }
  40. for _, tpl in pairs(templates) do
  41. local fh = datafile.open(path.join("templates", tpl.file))
  42. tpl.data = assert(etlua.compile(fh:read("a")))
  43. end
  44. -- HTML generator module.
  45. local M = {
  46. res_dir = path.join(pkar.config.htmlgen.out_dir, "res"),
  47. asset_dir = asset_dir,
  48. icon_dir = path.join(asset_dir, "icons"),
  49. media_dir = path.join(pkar.config.htmlgen.out_dir, "media"),
  50. webroot = "", -- TODO switch depending on local FS or webserver generation.
  51. }
  52. local MEDIA_WEB_PATH = M.media_dir:gsub(pkar.config.htmlgen.out_dir, "")
  53. local TN_FS_PATH = path.join(M.media_dir, "thumbnail")
  54. local TN_WEB_PATH = TN_FS_PATH:gsub(pkar.config.htmlgen.out_dir, "")
  55. -- Get model configuration from subject URI.
  56. local function get_mconf(s)
  57. local ctype = get_single_v(s, "content_type")
  58. return model.types[model.uri_to_id[nsm.denormalize_uri(ctype.data)]]
  59. end
  60. local function get_breadcrumbs(mconf)
  61. -- Breadcrumbs, from top class to current class.
  62. -- Also verify if it's a File subclass.
  63. local breadcrumbs = {}
  64. for i = 1, #mconf.lineage do
  65. breadcrumbs[i] = {
  66. mconf.lineage[i],
  67. model.types[mconf.lineage[i]].label
  68. }
  69. end
  70. return breadcrumbs
  71. end
  72. --[[ Infer thumbnail web path from the resource subject URI.
  73. If the resource is not a file and as such does not have a thumbnail of
  74. its own, traverse the list of first children and use the first one found
  75. with a thumbnail.
  76. @param[in] s Subject (resource) URI.
  77. @param[in] ext Optional extension to add, including the extension separator
  78. (`.`). If not provided, `.jpg` is used.
  79. --]]
  80. local function get_tn_url(s, ext) -- TODO caller needs to pass correct ext
  81. if repo.gr:contains(triple.new(s, pkar.RDF_TYPE, model.id_to_uri.file))
  82. then
  83. -- The subject is a file.
  84. return pkar.gen_pairtree(TN_WEB_PATH, s.data, ext or ".jpg", true)
  85. end
  86. -- If it's a brick, look for its ref.
  87. local ref = get_single_v(s, "ref")
  88. if ref then return get_tn_url(ref, ext) end
  89. local pref_rep = get_single_v(s, "pref_rep")
  90. if pref_rep then return get_tn_url(pref_rep, ext) end
  91. -- Recurse through all first children until one with a thumbnail, or a
  92. -- leaf without children, is found.
  93. -- Look for preferred rep first.
  94. local t = get_single_v(s, "pref_rep")
  95. if not t then
  96. -- If not found, look for reference of first child.
  97. t = get_single_v(s, "first")
  98. if t then t = get_single_v(t, "ref") end
  99. end
  100. if t then return get_tn_url(t, ext) end
  101. end
  102. --[[ Find closest available type icon to the given type.
  103. --]]
  104. local function get_icon_url(lineage)
  105. for i = #lineage, 1, -1 do
  106. if path.isfile(path.join(M.icon_dir, lineage[i] .. ICON_EXT)) then
  107. return M.webroot .. "/icons/" .. lineage[i] .. ICON_EXT
  108. end
  109. end
  110. end
  111. local function generate_coll(s, mconf)
  112. local pref_rep = get_single_v(s, "pref_rep")
  113. local members = {}
  114. local child_s = get_single_v(s, "first")
  115. --[[ FIXME this should check for the ref attribute of the proxy.
  116. if not repo.gr:contains(triple.new(
  117. s, model.id_to_uri.has_member, first
  118. )) then
  119. error(("first child %s is not a member of %s!")
  120. :format(first.data, s.data)
  121. )
  122. end
  123. --]]
  124. -- First child is the alternative pref representation.
  125. if not pref_rep then pref_rep = child_s end
  126. local pref_rep_url
  127. if pref_rep then
  128. pref_rep_url = pkar.gen_pairtree("/res", pref_rep.data, ".html", true)
  129. -- Collection page uses full size image, shrunk to size if necessary.
  130. pref_rep_file = get_tn_url(pref_rep):gsub(TN_WEB_PATH, MEDIA_WEB_PATH)
  131. end
  132. local child_ref, child_label, child_mconf
  133. while child_s do
  134. child_ref = get_single_v(child_s, "ref")
  135. -- Skip relationship with long description doc.
  136. if repo.gr:contains(
  137. triple.new(s, model.id_to_uri.long_description, child_ref)
  138. ) then goto skip end
  139. child_mconf = get_mconf(child_ref)
  140. --if not child_ref then child_ref = child_s end
  141. child_label = get_single_v(child_s, "label")
  142. if not child_label then
  143. if child_mconf.types.file then
  144. child_label = path.basename(
  145. get_single_v(child_ref, "source_path").data)
  146. else
  147. child_label = get_single_v(child_ref, "label")
  148. end
  149. end
  150. if child_label.data then child_label = child_label.data end
  151. table.insert(members, {
  152. icon = get_icon_url(child_mconf.lineage),
  153. tn = get_tn_url(child_s),
  154. href = pkar.gen_pairtree("/res", child_ref.data, ".html", true),
  155. label = child_label,
  156. ctype_label = child_mconf.label,
  157. })
  158. ::skip::
  159. child_s = get_single_v(child_s, "next")
  160. end
  161. local title = get_single_v(s, "label")
  162. local description = get_single_v(s, "description")
  163. local body_rel = get_single_v(s, "long_description")
  164. local body
  165. if body_rel then
  166. local body_res_path = get_single_v(body_rel, "archive_path").data
  167. local bfh = assert(io.open(body_res_path, "r"))
  168. body = markdown(bfh:read("a"))
  169. bfh:close()
  170. end
  171. out_html = templates.coll.data({
  172. --webroot = M.webroot,
  173. site_title = pkar.config.site.title,
  174. title = title.data,
  175. description = description.data,
  176. body = body,
  177. head_tpl = templates.head.data,
  178. header_tpl = templates.header.data,
  179. mconf = mconf,
  180. uri = s,
  181. members = members,
  182. tn_url = get_tn_url(s),
  183. pref_rep = {
  184. url = pref_rep_url,
  185. file = pref_rep_file,
  186. },
  187. icon_url = get_icon_url(mconf.lineage),
  188. --breadcrumbs = get_breadcrumbs(mconf),
  189. rdf_href = pkar.gen_pairtree("/res", s.data, ".ttl", true),
  190. })
  191. local res_path = pkar.gen_pairtree(M.res_dir, s.data, ".html")
  192. local ofh = assert(io.open(res_path, "w"))
  193. ofh:write(out_html)
  194. ofh:close()
  195. return true
  196. end
  197. local function generate_dres(s, mconf)
  198. local dmd = {}
  199. local rel = {}
  200. local children = {}
  201. local title
  202. -- Metadata
  203. local attrs = repo.gr:connections(s, term.LINK_OUTBOUND)
  204. local pref_rep = get_single_v(s, "pref_rep")
  205. or get_single_v(s, "has_member")
  206. for p, ots_it in attrs:iter() do
  207. local pname = model.uri_to_id[nsm.denormalize_uri(p.data)] or p.data
  208. logger:debug("DRES pname: " .. pname)
  209. local pconf = ((mconf.properties or NT)[pname] or NT)
  210. -- RDF types are shown in in breadcrumbs.
  211. if pname == pkar.RDF_TYPE.data then goto skip end
  212. if pname == "first" then
  213. -- Build a linked list for every first found.
  214. for o in ots_it do
  215. -- Loop through all first children.
  216. local child_s = o
  217. if not pref_rep then pref_rep = child_s end
  218. logger:debug("local child_s: ", child_s.data)
  219. local ll = {}
  220. -- Fallback labels.
  221. local label
  222. local ref = get_single_v(child_s, "ref")
  223. if ref then
  224. label = (get_single_v(ref, "label") or NT).data
  225. else
  226. label = (get_single_v(child_s, "label") or NT).data
  227. end
  228. if not label then
  229. label = (get_single_v(child_s, "source_path") or NT).data
  230. if label then label = path.basename(label)
  231. else label = child_s.data end
  232. end
  233. while child_s do
  234. -- Loop trough all next nodes for each first child.
  235. local ref = get_single_v(child_s, "ref")
  236. table.insert(ll, {
  237. href = pkar.gen_pairtree("/res", ref.data, ".html", true),
  238. label = label,
  239. tn = get_tn_url(ref),
  240. })
  241. child_s = get_single_v(child_s, "next")
  242. end
  243. table.insert(children, ll)
  244. end
  245. elseif pname == "next" then
  246. -- Sibling.
  247. for o in ots_it do ls_next = o.data break end
  248. elseif pconf.type == "resource" then
  249. -- Relationship.
  250. rel[pname] = {
  251. label = pconf.label,
  252. description = pconf.description,
  253. uri = pconf.uri,
  254. }
  255. for o in ots_it do
  256. table.insert(rel[pname], {
  257. href = pkar.gen_pairtree("/res", o.data, ".html", true),
  258. label = nsm.denormalize_uri(o.data),
  259. })
  260. end
  261. else
  262. -- Descriptive metadata.
  263. local attr = {
  264. label = pconf.label or pname,
  265. description = pconf.description,
  266. uri = pconf.uri,
  267. }
  268. -- TODO differentiate term types
  269. for o in ots_it do table.insert(attr, o.data) end
  270. table.sort(attr)
  271. if p == model.id_to_uri.label then title = attr[1] end
  272. table.insert(dmd, attr)
  273. end
  274. ::skip::
  275. end
  276. table.sort(
  277. dmd, function(a, b)
  278. return ((a.label or a.uri) < (b.label or b.uri))
  279. end
  280. )
  281. table.sort(rel)
  282. table.sort(children)
  283. logger:debug("Lineage:", pp.write(mconf.lineage))
  284. logger:debug("DMD:", pp.write(dmd))
  285. logger:debug("REL:", pp.write(rel))
  286. logger:debug("Children:", pp.write(children))
  287. logger:debug("Breadcrumbs:", pp.write(get_breadcrumbs(mconf)))
  288. local pref_rep_url
  289. if pref_rep then
  290. pref_rep_url = pkar.gen_pairtree("/res", pref_rep.data, ".html", true)
  291. pref_rep_file = get_tn_url(pref_rep):gsub(TN_WEB_PATH, MEDIA_WEB_PATH)
  292. end
  293. out_html = templates.dres.data({
  294. --webroot = M.webroot,
  295. site_title = pkar.config.site.title,
  296. title = title or s.data,
  297. head_tpl = templates.head.data,
  298. header_tpl = templates.header.data,
  299. mconf = mconf,
  300. uri = s,
  301. dmd = dmd,
  302. rel = rel,
  303. children = children,
  304. ls_next = ls_next,
  305. tn_url = get_tn_url(s),
  306. pref_rep = {
  307. url = pref_rep_url,
  308. file = pref_rep_file,
  309. },
  310. icon_url = get_icon_url(mconf.lineage),
  311. breadcrumbs = get_breadcrumbs(mconf),
  312. rdf_href = pkar.gen_pairtree("/res", s.data, ".ttl", true),
  313. })
  314. local res_path = pkar.gen_pairtree(M.res_dir, s.data, ".html")
  315. local ofh = assert(io.open(res_path, "w"))
  316. ofh:write(out_html)
  317. ofh:close()
  318. return true
  319. end
  320. local function generate_ores(s, mconf)
  321. local techmd = {}
  322. local rel = {}
  323. -- Metadata
  324. local attrs = repo.gr:connections(s, term.LINK_OUTBOUND)
  325. for p, ots_it in attrs:iter() do
  326. local pname = model.uri_to_id[nsm.denormalize_uri(p.data)] or p.data
  327. local pconf = ((mconf.properties or NT)[pname] or NT)
  328. -- RDF types are shown in in breadcrumbs.
  329. if pname == pkar.RDF_TYPE.data then goto skip end
  330. if pname == "next" then
  331. -- Sibling.
  332. for o in ots_it do ls_next = o.data break end
  333. elseif pconf.type == "resource" then
  334. -- Relationship.
  335. rel[pname] = {
  336. label = pconf.label,
  337. description = pconf.description,
  338. uri = pconf.uri,
  339. }
  340. for o in ots_it do
  341. table.insert(rel[pname], {
  342. href = pkar.gen_pairtree("/res", o.data, ".html", true),
  343. label = nsm.denormalize_uri(o.data),
  344. })
  345. end
  346. else
  347. -- Descriptive metadata.
  348. techmd[pname] = {
  349. label = pconf.label,
  350. description = pconf.description,
  351. uri = pconf.uri,
  352. }
  353. -- TODO differentiate term types
  354. for o in ots_it do table.insert(techmd[pname], o.data) end
  355. table.sort(techmd[pname])
  356. end
  357. ::skip::
  358. end
  359. table.sort(techmd)
  360. table.sort(rel)
  361. logger:debug("Lineage:", pp.write(mconf.lineage))
  362. logger:debug("Breadcrumbs:", pp.write(get_breadcrumbs(mconf)))
  363. logger:debug("techmd:", pp.write(techmd))
  364. logger:debug("REL:", pp.write(rel))
  365. -- Transform and move media assets.
  366. local dest_fname, dest_dir, dest -- Reused for thumbnail.
  367. logger:debug("Transforming resource file.")
  368. local res_path = techmd.archive_path
  369. if not res_path then error("No file path for File resource!") end
  370. local txconf = (mconf.gen or NT).transformers or {}
  371. local pres_conf = txconf.pres or {fn = "copy"}
  372. -- Set file name to resource ID + source extension.
  373. dest_fname = (
  374. s.data:gsub(nsm.get_ns("par"), "") ..
  375. (pres_conf.ext or path.extension(res_path[1])))
  376. dest_dir = path.join(
  377. M.media_dir, dest_fname:sub(1, 2), dest_fname:sub(3, 4))
  378. dest = path.join(dest_dir, dest_fname)
  379. assert(transformers[pres_conf.fn](
  380. res_path[1], dest, table.unpack(pres_conf or NT)))
  381. local pres = dest:gsub(pkar.config.htmlgen.out_dir, "")
  382. logger:debug("Presentation file: ", pres)
  383. -- Thumbnail.
  384. local tn
  385. if txconf.thumbnail then
  386. if txconf.thumbnail.ext then
  387. dest_fname = path.splitext(dest_fname) .. txconf.thumbnail.ext
  388. end
  389. dest = pkar.gen_pairtree(TN_FS_PATH, dest_fname)
  390. assert(transformers[txconf.thumbnail.fn](
  391. res_path[1], dest, table.unpack(txconf.thumbnail or NT)))
  392. tn = dest:gsub(M.media_dir, TN_WEB_PATH)
  393. logger:debug("Thumbnail: ", tn)
  394. end
  395. out_html = templates.ores.data({
  396. --webroot = M.webroot,
  397. site_title = pkar.config.site.title,
  398. fname = path.basename(techmd.source_path[1]),
  399. head_tpl = templates.head.data,
  400. header_tpl = templates.header.data,
  401. mconf = mconf,
  402. uri = s,
  403. techmd = techmd,
  404. rel = rel,
  405. ls_next = ls_next,
  406. icon_url = get_icon_url(mconf.lineage),
  407. breadcrumbs = get_breadcrumbs(mconf),
  408. pres = pres,
  409. thumbnail = tn,
  410. rdf_href = pkar.gen_pairtree("/res", s.data, ".ttl", true),
  411. })
  412. local res_path = pkar.gen_pairtree(M.res_dir, s.data, ".html")
  413. local ofh = assert(io.open(res_path, "w"))
  414. ofh:write(out_html)
  415. ofh:close()
  416. return true
  417. end
  418. M.generate_search_idx = function(s, mconf)
  419. local rrep = {
  420. id = s.data:gsub("^.*/", ""),
  421. tn = get_tn_url(s),
  422. href = pkar.gen_pairtree("/res", s.data, ".html", true),
  423. content_type = mconf.id,
  424. type = mconf.lineage,
  425. icon = get_icon_url(mconf.lineage),
  426. }
  427. local function format_value(pname, o)
  428. logger:debug("Adding value to " .. pname .. ": " .. ((o or NT).data or "nil"))
  429. local v
  430. if pname == "type" or pname == "content_type" then
  431. v = model.uri_to_id[p]
  432. else v = o.data
  433. end
  434. return v
  435. end
  436. local attrs = repo.gr:connections(s, term.LINK_OUTBOUND)
  437. local fpath
  438. for p, ots_it, ots_size in attrs:iter() do
  439. local pname
  440. if p == model.id_to_uri.content_type then goto skip end
  441. if p == model.id_to_uri.source_path then
  442. if mconf.types.file then
  443. fpath = ots_it()
  444. rrep.fname = path.basename(fpath.data)
  445. end
  446. goto skip
  447. end
  448. pname = model.uri_to_id[nsm.denormalize_uri(p.data)]
  449. if not pname then goto skip end
  450. local pconf = (mconf.properties or NT)[pname] or NT
  451. -- TODO dereference & index resource values.
  452. if idx_ignore[pname] or pconf.type == "resource" then goto skip end
  453. local attr
  454. -- Quick check if it's multi-valued
  455. if ots_size > 1 then
  456. attr = {}
  457. for o in ots_it do
  458. table.insert(attr, format_value(pname, o))
  459. end
  460. else
  461. attr = format_value(pname, ots_it())
  462. end
  463. rrep[pname] = attr -- Add to search index.
  464. idx_keys[pname] = true -- Add to search keys.
  465. ::skip::
  466. end
  467. return rrep
  468. end
  469. M.generate_ll = function(s)
  470. local res_gr = repo.get_rsrc(s)
  471. local mconf = get_mconf(s)
  472. local tdata = {
  473. {
  474. id = path.basename(s.data),
  475. content_type = mconf.id,
  476. },
  477. }
  478. for p, ots_it in pairs(res_gr:connections(s, term.LINK_OUTBOUND)) do
  479. local pname = model.uri_to_id[nsm.denormalize_uri(p.data)]
  480. --if p == pkar.RDF_TYPE then goto skip_p end
  481. if not pname then goto skip_p end
  482. if pname == "content_type" then goto skip_p end
  483. for o in ots_it do
  484. -- Find a row where the pname slot has not been occupied.
  485. if (mconf.properties[pname] or {}).type == "resource" then
  486. o = {data = o.data:gsub(nsm.get_ns("par"), "")}
  487. end
  488. for i = 1, math.huge do
  489. if (tdata[i] or NT)[pname] then goto continue
  490. else
  491. if tdata[i] then tdata[i][pname] = o.data
  492. else tdata[i] = {[pname] = o.data} end
  493. break
  494. end
  495. ::continue::
  496. end
  497. end
  498. ::skip_p::
  499. end
  500. -- FIXME ftcsv encodes nil values as `"nil"`. See
  501. -- https://github.com/FourierTransformer/ftcsv/issues/46
  502. return csv.encode(tdata, {encodeNilAs = ""})
  503. end
  504. M.generate_resource = function(s)
  505. local res_type
  506. res_type = get_single_v(s, "content_type")
  507. local mconf = model.from_uri(res_type)
  508. -- Generate RDF/Turtle doc.
  509. local res_path = pkar.gen_pairtree(M.res_dir, s.data, ".ttl")
  510. dir.makepath(path.dirname(res_path))
  511. local ofh = assert(io.open(res_path, "w"))
  512. for chunk in repo.serialize_rsrc(s, "ttl") do ofh:write(chunk) end
  513. ofh:close()
  514. -- Generate HTML doc.
  515. if mconf.types.collection then assert(generate_coll(s, mconf))
  516. elseif mconf.types.file then assert(generate_ores(s, mconf))
  517. else assert(generate_dres(s, mconf)) end
  518. -- Generate JSON rep and append to search index.
  519. idx_rep = M.generate_search_idx(s, mconf)
  520. json_rep = " " .. json.encode(idx_rep)
  521. ofh = assert(io.open(index_path, "a"))
  522. ofh:write(json_rep)
  523. ofh:write(",\n") -- Hack together the JSON objects in a list.
  524. ofh:close()
  525. return s
  526. end
  527. M.generate_resources = function(coll_id)
  528. -- TODO It's more complicated than this. Each member in the collection
  529. -- must be scanned recursively for outbound links and visited links must
  530. -- be noted down to avoid loops.
  531. --[[
  532. if coll_id then
  533. subject_ts = repo.gr:term_set(
  534. term.new_iriref_ns("par:" .. coll_id), triple.POS_S,
  535. model.id_to_uri.has_member, triple.POS_P
  536. )
  537. else subjects_ts = repo.gr:unique_terms(triple.POS_S) end
  538. --]]
  539. subjects_ts = repo.gr:unique_terms(triple.POS_S)
  540. -- Initialize the JSON template with an opening brace.
  541. local ofh = assert(io.open(index_path, "w"))
  542. ofh:write("[\n")
  543. ofh:close()
  544. -- TODO parallelize
  545. for s in subjects_ts:iter() do assert(M.generate_resource(s)) end
  546. -- Close the open list brace in the JSON template after all the resources
  547. -- have been added.
  548. ofh = assert(io.open(index_path, "a"))
  549. ofh:write(" {}\n]") -- Add empty object to validate the last comma
  550. ofh:close()
  551. -- Write index keys.
  552. ofh = assert(io.open(keys_path, "w"))
  553. idx_keys_ls = {}
  554. for k in pairs(idx_keys) do table.insert(idx_keys_ls, k) end
  555. ofh:write(json.encode(idx_keys_ls))
  556. ofh:close()
  557. return true
  558. end
  559. M.generate_homepage = function()
  560. local idx_data = {objects = {}, collections = {}}
  561. -- Get all subject of type: Artifact.
  562. local s_ts = repo.gr:term_set(
  563. pkar.RDF_TYPE, triple.POS_P,
  564. term.new_iriref_ns("pas:Artifact"), triple.POS_O
  565. )
  566. local i = 1
  567. for s in s_ts:iter() do
  568. if i > (pkar.config.htmlgen.max_homepage_items or 10) then break end
  569. table.insert(idx_data.objects, {
  570. href = pkar.gen_pairtree("/res", s.data, ".html", true),
  571. title = get_single_v(s, "label"),
  572. submitted = get_single_v(s, "submitted").data,
  573. tn = get_tn_url(s),
  574. })
  575. i = i + 1
  576. end
  577. table.sort(
  578. idx_data.objects, function(a, b)
  579. return a.submitted < b.submitted end
  580. )
  581. s_ts = repo.gr:term_set(
  582. pkar.RDF_TYPE, triple.POS_P,
  583. term.new_iriref_ns("pas:Collection"), triple.POS_O
  584. )
  585. for s in s_ts:iter() do
  586. table.insert(idx_data.collections, {
  587. href = pkar.gen_pairtree("/res", s.data, ".html", true),
  588. title = get_single_v(s, "label"),
  589. submitted = get_single_v(s, "submitted").data,
  590. tn = get_tn_url(s),
  591. })
  592. end
  593. table.sort(
  594. idx_data.collections, function(a, b)
  595. return a.submitted < b.submitted end
  596. )
  597. logger:debug(pp.write(idx_data))
  598. out_html = templates.idx.data({
  599. webroot = M.webroot,
  600. site_title = pkar.config.site.title,
  601. head_tpl = templates.head.data,
  602. header_tpl = templates.header.data,
  603. nsm = nsm,
  604. idx_data = idx_data,
  605. })
  606. local idx_path = path.join(pkar.config.htmlgen.out_dir, "index.html")
  607. local ofh = assert(io.open(idx_path, "w"))
  608. logger:debug("Writing info at ", idx_path)
  609. ofh:write(out_html)
  610. ofh:close()
  611. return true
  612. end
  613. M.reset_site = function()
  614. -- Reset target folders.
  615. -- TODO for larger sites, a selective update should be implemented by
  616. -- comparing RDF resource timestamps with HTML page timestamps. Post-MVP.
  617. if path.isdir(pkar.config.htmlgen.out_dir) then
  618. logger:warn("Removing existing web site.")
  619. dir.rmtree(pkar.config.htmlgen.out_dir)
  620. end
  621. -- Recreate asset dir.
  622. if path.isdir(M.asset_dir) then
  623. logger:warn("Removing existing web assets.")
  624. dir.rmtree(M.asset_dir)
  625. end
  626. dir.makepath(M.asset_dir)
  627. -- Copy static assets.
  628. logger:info("Copying templates dir " .. datafile.path("templates/assets") .. " to " .. M.asset_dir)
  629. assert(dir.clonetree(
  630. datafile.path("templates/assets"),
  631. M.asset_dir, dir.copyfile)
  632. )
  633. end
  634. M.generate_site = function(keep)
  635. if not keep then M.reset_site() end
  636. -- Clear local search index keys.
  637. idx_keys = {
  638. id = true,
  639. content_type = true,
  640. type = true,
  641. fname = true,
  642. }
  643. -- Generate individual resource pages, RDF, and JSON index.
  644. assert(M.generate_resources())
  645. -- Generate index page.
  646. assert(M.generate_homepage())
  647. end
  648. return M