presentation.lua 25 KB

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