Browse Source

Bunch of fixes:

* Add CSS.
* Add tiny static webserver (darkhttpd).
* Fix sequences.
* Use a single function (generate_site) to generate HTML.
* Many other fixes but it's late night.
scossu 1 week ago
parent
commit
f4e60f450a

+ 3 - 0
.gitmodules

@@ -0,0 +1,3 @@
+[submodule "ext/darkhttpd"]
+	path = ext/darkhttpd
+	url = https://github.com/emikulic/darkhttpd.git

+ 29 - 1
config/model/typedef/file.lua

@@ -9,6 +9,34 @@ return {
             type = "string",
             type = "string",
             min_cardinality = 1,
             min_cardinality = 1,
             max_cardinality = 1,
             max_cardinality = 1,
-        }
+        },
+        ["pas:path"] = {
+            label = "Archival path",
+            description = [[
+              Path of the preserved file, relative to the archival root. ]],
+            type = "string",
+            min_cardinality = 1,
+            max_cardinality = 1,
+        },
+        ["dc:format"] = {
+            label = "MIME type",
+            type = "string",
+            min_cardinality = 1,
+            max_cardinality = 1,
+        },
+        ["dc:extent"] = {
+            label = "File size",
+            description = "File size in bytes.",
+            type = "integer",
+            min_cardinality = 1,
+            max_cardinality = 1,
+        },
+        ["premis:hasMessageDigest"] = {
+            label = "Checksum",
+            description = [[
+                File checksum formatted as: <algorithm>:<hex digest>]],
+            type = "string",
+            min_cardinality = 1,
+        },
     }
     }
 }
 }

+ 1 - 1
config/model/typedef/part.lua

@@ -24,7 +24,7 @@ return {
         ["pas:first"] = {
         ["pas:first"] = {
             label = "First child",
             label = "First child",
             type = "resource",
             type = "resource",
-            range = {["pas:Part"] = true},
+            range = {["pas:Part"] = true, ["pas:File"] = true},
         },
         },
         ["pas:next"] = {
         ["pas:next"] = {
             label = "Next sibling",
             label = "Next sibling",

+ 1 - 0
ext/darkhttpd

@@ -0,0 +1 @@
+Subproject commit 84bce93d66c5551f955b21f11e984f382125e10c

+ 1 - 5
scratch.lua

@@ -16,8 +16,4 @@ sip = sub.generate_sip_v2(
 sub.deposit(sip)
 sub.deposit(sip)
 --]]
 --]]
 
 
-idx = graph.list(pkar.store)
---assert(#idx == 5)
-
-html = hgen.generate_idx()
-res_html = hgen.generate_resources()
+html = hgen.generate_site()

+ 47 - 21
src/html_generator.lua

@@ -32,6 +32,10 @@ fh = datafile.open("templates/ores.html")
 ores_tpl = assert(etlua.compile(fh:read("a")))
 ores_tpl = assert(etlua.compile(fh:read("a")))
 fh:close()
 fh:close()
 
 
+fh = datafile.open("templates/header.html")
+header_tpl = assert(etlua.compile(fh:read("a")))
+fh:close()
+
 
 
 -- HTML generator module.
 -- HTML generator module.
 local M = {
 local M = {
@@ -62,9 +66,9 @@ local function generate_dres(s, mconf)
     local dmd = {}
     local dmd = {}
     local rel = {}
     local rel = {}
     local children = {}
     local children = {}
+    local title
     -- Metadata
     -- Metadata
     local attrs = gr:connections(s, term.LINK_OUTBOUND)
     local attrs = gr:connections(s, term.LINK_OUTBOUND)
-    --require "debugger"()
     for p, ots in pairs(attrs) do
     for p, ots in pairs(attrs) do
         local fname = nsm.denormalize_uri(p.data)
         local fname = nsm.denormalize_uri(p.data)
         p_label = ((mconf.properties or NT)[fname] or NT).label
         p_label = ((mconf.properties or NT)[fname] or NT).label
@@ -76,23 +80,24 @@ local function generate_dres(s, mconf)
             for o in pairs(ots) do table.insert(dmd[fname], o.data) end
             for o in pairs(ots) do table.insert(dmd[fname], o.data) end
         elseif fname == "pas:first" then
         elseif fname == "pas:first" then
             -- Build a linked list for every first found.
             -- Build a linked list for every first found.
-            local p = term.new_iriref_ns("pas:next")
             local dc_title = term.new_iriref_ns("dc:title")
             local dc_title = term.new_iriref_ns("dc:title")
             local tn_p = term.new_iriref_ns("pas:thumbnail")
             local tn_p = term.new_iriref_ns("pas:thumbnail")
             for o in pairs(ots) do
             for o in pairs(ots) do
                 -- Loop through all first children.
                 -- Loop through all first children.
                 local node_uri = o
                 local node_uri = o
-                logger:debug("local node_uri", node_uri.data)
+                logger:debug("local node_uri: ", node_uri.data)
                 local ll = {}
                 local ll = {}
+                --require "debugger"()
                 while node_uri do
                 while node_uri do
                     -- Loop trough all next nodes for each first child.
                     -- Loop trough all next nodes for each first child.
-                    local el_gr = graph.get(node_uri, pkar.store)
                     table.insert(ll, {
                     table.insert(ll, {
-                        uri = node_uri,
-                        label = gr:attr(node_uri, dc_title)[1],
-                        tn = gr:attr(node_uri, tn_p)[1],
+                        href = node_uri.data:gsub(
+                                nsm.get_ns("par"), "/res/") .. ".html",
+                        label = (next(gr:attr(node_uri, dc_title)) or NT).data,
+                        tn = next(gr:attr(node_uri, tn_p)),
                     })
                     })
-                    local next_attr = gr:attr(node_uri, p)
+                    local next_attr = gr:attr(
+                            node_uri, term.new_iriref_ns("pas:next"))
                     node_uri = next(next_attr)  -- There can only be one "next"
                     node_uri = next(next_attr)  -- There can only be one "next"
                 end
                 end
                 table.insert(children, ll)
                 table.insert(children, ll)
@@ -102,14 +107,16 @@ local function generate_dres(s, mconf)
             for o in pairs(ots) do ls_next = o.data break end
             for o in pairs(ots) do ls_next = o.data break end
         else
         else
             -- Descriptive metadata.
             -- Descriptive metadata.
-            dmd[fname] = {label = p_label, uri = fname}
+            local attr = {label = p_label, uri = fname}
             -- TODO differentiate term types
             -- TODO differentiate term types
-            for o in pairs(ots) do table.insert(dmd[fname], o.data) end
-            table.sort(dmd[fname])
+            for o in pairs(ots) do table.insert(attr, o.data) end
+            table.sort(attr)
+            if p == term.new_iriref_ns("dc:title") then title = attr[1] end
+            table.insert(dmd, attr)
         end
         end
         ::skip::
         ::skip::
     end
     end
-    table.sort(dmd)
+    table.sort(dmd, function(a, b) return (a.label < b.label) end)
     table.sort(rel)
     table.sort(rel)
     table.sort(children)
     table.sort(children)
     logger:debug("Lineage:", pp.write(mconf.lineage))
     logger:debug("Lineage:", pp.write(mconf.lineage))
@@ -120,7 +127,8 @@ local function generate_dres(s, mconf)
 
 
     out_html = dres_tpl({
     out_html = dres_tpl({
         site_title = pkar.config.site.title or pkar.default_title,
         site_title = pkar.config.site.title or pkar.default_title,
-        title = ((dmd["dc:title"] or NT)[1] or NT) or s.data,
+        title = title or s.data,
+        header_tpl = header_tpl,
         mconf = mconf,
         mconf = mconf,
         uri = s,
         uri = s,
         dmd = dmd,
         dmd = dmd,
@@ -136,6 +144,8 @@ local function generate_dres(s, mconf)
             "%s/%s.html", M.res_dir, res_id), "w"))
             "%s/%s.html", M.res_dir, res_id), "w"))
     ofh:write(out_html)
     ofh:write(out_html)
     ofh:close()
     ofh:close()
+
+   return true
 end
 end
 
 
 
 
@@ -184,7 +194,7 @@ local function generate_ores(s, mconf)
     if txconf.ext then
     if txconf.ext then
         dest_fname = plpath.splitext(dest_fname) .. txconf.ext
         dest_fname = plpath.splitext(dest_fname) .. txconf.ext
     end
     end
-    dest = M.asset_dir .. "/" .. dest_fname
+    dest = M.media_dir .. "/" .. dest_fname
     assert(transformers[txconf.fn](
     assert(transformers[txconf.fn](
             res_path[1], dest, table.unpack(txconf or NT)))
             res_path[1], dest, table.unpack(txconf or NT)))
     local deliverable = dest:gsub(pkar.config.htmlgen.out_dir, "..")
     local deliverable = dest:gsub(pkar.config.htmlgen.out_dir, "..")
@@ -193,6 +203,7 @@ local function generate_ores(s, mconf)
     out_html = ores_tpl({
     out_html = ores_tpl({
         site_title = pkar.config.site.title or pkar.default_title,
         site_title = pkar.config.site.title or pkar.default_title,
         fname = plpath.basename(techmd["pas:sourcePath"][1]),
         fname = plpath.basename(techmd["pas:sourcePath"][1]),
+        header_tpl = header_tpl,
         mconf = mconf,
         mconf = mconf,
         uri = s,
         uri = s,
         techmd = techmd,
         techmd = techmd,
@@ -207,6 +218,8 @@ local function generate_ores(s, mconf)
             "%s/%s.html", M.res_dir, res_id), "w"))
             "%s/%s.html", M.res_dir, res_id), "w"))
     ofh:write(out_html)
     ofh:write(out_html)
     ofh:close()
     ofh:close()
+
+   return true
 end
 end
 
 
 
 
@@ -225,17 +238,13 @@ end
 
 
 
 
 M.generate_resources = function()
 M.generate_resources = function()
-    dir.rmtree(M.res_dir)
-    dir.makepath(M.res_dir)
-    dir.rmtree(M.asset_dir)
-    dir.makepath(M.asset_dir)
-    dir.rmtree(M.media_dir)
-    dir.makepath(M.media_dir)
     local gr = graph.new(pkar.store, term.DEFAULT_CTX)
     local gr = graph.new(pkar.store, term.DEFAULT_CTX)
     local subjects = gr:unique_terms(triple.POS_S)
     local subjects = gr:unique_terms(triple.POS_S)
 
 
     -- TODO parallelize
     -- TODO parallelize
-    for s in pairs(subjects) do M.generate_resource(s) end
+    for s in pairs(subjects) do assert(M.generate_resource(s)) end
+
+    return true
 end
 end
 
 
 
 
@@ -257,6 +266,7 @@ M.generate_idx = function()
     logger:debug(pp.write(obj_idx))
     logger:debug(pp.write(obj_idx))
     out_html = idx_tpl({
     out_html = idx_tpl({
         title = pkar.config.site.title or pkar.default_title,
         title = pkar.config.site.title or pkar.default_title,
+        header_tpl = header_tpl,
         nsm = nsm,
         nsm = nsm,
         obj_idx = obj_idx,
         obj_idx = obj_idx,
     })
     })
@@ -267,6 +277,22 @@ M.generate_idx = function()
     logger:debug("Writing info at ", idx_path)
     logger:debug("Writing info at ", idx_path)
     ofh:write(out_html)
     ofh:write(out_html)
     ofh:close()
     ofh:close()
+
+    return true
+end
+
+
+M.generate_site = function()
+    dir.rmtree(M.res_dir)
+    dir.makepath(M.res_dir)
+    dir.rmtree(M.asset_dir)
+    dir.makepath(M.asset_dir)
+    dir.rmtree(M.media_dir)
+    dir.makepath(M.media_dir)
+
+    assert(M.generate_idx())
+    assert(M.generate_resources())
+    dir.clonetree("templates/assets", plpath.dirname(M.asset_dir), dir.copyfile)
 end
 end
 
 
 
 

+ 16 - 8
src/submission.lua

@@ -184,7 +184,12 @@ M.generate_sip_v2 = function(path)
         if row["pas:sourcePath"] ~= "" then
         if row["pas:sourcePath"] ~= "" then
             prev_path = row["pas:sourcePath"]
             prev_path = row["pas:sourcePath"]
             -- New row.
             -- New row.
-            sip[i] = {id = M.idgen()}
+            local id
+            if row.id then
+                id = "par:" .. row.id
+                row.id = nil
+            else id = "par:" .. M.idgen() end
+            sip[i] = {id = id}
             for k, v in pairs(row) do
             for k, v in pairs(row) do
                 if v == "" then goto cont1 end  -- skip empty strings.
                 if v == "" then goto cont1 end  -- skip empty strings.
                 if pkar.config.md.single_values[k] then sip[i][k] = v
                 if pkar.config.md.single_values[k] then sip[i][k] = v
@@ -223,17 +228,17 @@ M.generate_sip_v2 = function(path)
                     sip[j]["pas:sourcePath"]:match("(.*/)") ==
                     sip[j]["pas:sourcePath"]:match("(.*/)") ==
                             v["pas:sourcePath"]:match("(.*/)") then
                             v["pas:sourcePath"]:match("(.*/)") then
                 --print("next match.")
                 --print("next match.")
-                v["pas:next"] = "pas:" .. sip[j].id
+                v["pas:next"] = sip[j].id
             end
             end
             if not v["pas:first"] and
             if not v["pas:first"] and
                     sip[j]["pas:sourcePath"]:match("^" .. escape_pattern(v["pas:sourcePath"])) then
                     sip[j]["pas:sourcePath"]:match("^" .. escape_pattern(v["pas:sourcePath"])) then
                 --print("First child match.")
                 --print("First child match.")
-                v["pas:first"] = "pas:" .. sip[j].id
+                v["pas:first"] = sip[j].id
             end
             end
         end
         end
         v._sort = nil
         v._sort = nil
     end
     end
-
+    --require "debugger"()
 
 
     return sip
     return sip
 end
 end
@@ -252,25 +257,28 @@ M.update_rsrc_md = function(rsrc)
     triples = {}
     triples = {}
 
 
     gr = graph.new(pkar.store, term.DEFAULT_CTX)
     gr = graph.new(pkar.store, term.DEFAULT_CTX)
-    local s = term.new_iriref_ns("par:" .. rsrc.id)
-    rsrc.id = nil  -- Exclude from metadata scan.
+    local s = term.new_iriref_ns(rsrc.id)
     for k, v in pairs(rsrc) do
     for k, v in pairs(rsrc) do
+        -- id is the subject, it won't be an attribute.
+        if k == "id" then goto skip end
+
         print("Adding attribute:", k, v)
         print("Adding attribute:", k, v)
         local p = term.new_iriref_ns(k)
         local p = term.new_iriref_ns(k)
         local o
         local o
         if type(v) == "table" then
         if type(v) == "table" then
             for vv, _ in pairs(v) do
             for vv, _ in pairs(v) do
-                if ((rmod.properties or NT)[k] or NT).type == "rel" then
+                if ((rmod.properties or NT)[k] or NT).type == "resource" then
                     o = term.new_iriref_ns(vv)
                     o = term.new_iriref_ns(vv)
                 else o = term.new_lit(vv) end
                 else o = term.new_lit(vv) end
                 table.insert(triples, triple.new(s, p, o))
                 table.insert(triples, triple.new(s, p, o))
             end
             end
         else
         else
-            if ((rmod.properties or NT)[k] or NT).type == "rel" then
+            if ((rmod.properties or NT)[k] or NT).type == "resource" then
                 o = term.new_iriref_ns(v)
                 o = term.new_iriref_ns(v)
             else o = term.new_lit(v) end
             else o = term.new_lit(v) end
             table.insert(triples, triple.new(s, p, o))
             table.insert(triples, triple.new(s, p, o))
         end
         end
+        ::skip::
     end
     end
     for i, m in ipairs(rmod.lineage) do
     for i, m in ipairs(rmod.lineage) do
         table.insert(
         table.insert(

+ 1 - 0
src/transformers.lua

@@ -7,6 +7,7 @@ M = {}
 -- Resize an image to a maximum size on either dimension.
 -- Resize an image to a maximum size on either dimension.
 M.img_resize = function(src, dest, size)
 M.img_resize = function(src, dest, size)
     print(("Resizing image %s with size %d to %s"):format(src, size, dest))
     print(("Resizing image %s with size %d to %s"):format(src, size, dest))
+    -- TODO Make streaming if possible.
     local img = vips.Image.thumbnail(src, size)
     local img = vips.Image.thumbnail(src, size)
 
 
     img:write_to_file(dest)
     img:write_to_file(dest)

+ 635 - 0
templates/assets/css/milligram.css

@@ -0,0 +1,635 @@
+/*!
+  * Milligram v1.4.1
+  * https://milligram.io
+  *
+  * Copyright (c) 2020 CJ Patoilo
+  * Licensed under the MIT license
+ */
+
+*,
+*:after,
+*:before {
+  box-sizing: inherit;
+}
+
+html {
+  box-sizing: border-box;
+  font-size: 62.5%;
+}
+
+body {
+  color: #606c76;
+  font-family: 'Roboto', 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif;
+  font-size: 1.6em;
+  font-weight: 300;
+  letter-spacing: .01em;
+  line-height: 1.6;
+}
+
+blockquote {
+  border-left: 0.3rem solid #d1d1d1;
+  margin-left: 0;
+  margin-right: 0;
+  padding: 1rem 1.5rem;
+}
+
+blockquote *:last-child {
+  margin-bottom: 0;
+}
+
+.button,
+button,
+input[type='button'],
+input[type='reset'],
+input[type='submit'] {
+  background-color: #9b4dca;
+  border: 0.1rem solid #9b4dca;
+  border-radius: .4rem;
+  color: #fff;
+  cursor: pointer;
+  display: inline-block;
+  font-size: 1.1rem;
+  font-weight: 700;
+  height: 3.8rem;
+  letter-spacing: .1rem;
+  line-height: 3.8rem;
+  padding: 0 3.0rem;
+  text-align: center;
+  text-decoration: none;
+  text-transform: uppercase;
+  white-space: nowrap;
+}
+
+.button:focus, .button:hover,
+button:focus,
+button:hover,
+input[type='button']:focus,
+input[type='button']:hover,
+input[type='reset']:focus,
+input[type='reset']:hover,
+input[type='submit']:focus,
+input[type='submit']:hover {
+  background-color: #606c76;
+  border-color: #606c76;
+  color: #fff;
+  outline: 0;
+}
+
+.button[disabled],
+button[disabled],
+input[type='button'][disabled],
+input[type='reset'][disabled],
+input[type='submit'][disabled] {
+  cursor: default;
+  opacity: .5;
+}
+
+.button[disabled]:focus, .button[disabled]:hover,
+button[disabled]:focus,
+button[disabled]:hover,
+input[type='button'][disabled]:focus,
+input[type='button'][disabled]:hover,
+input[type='reset'][disabled]:focus,
+input[type='reset'][disabled]:hover,
+input[type='submit'][disabled]:focus,
+input[type='submit'][disabled]:hover {
+  background-color: #9b4dca;
+  border-color: #9b4dca;
+}
+
+.button.button-outline,
+button.button-outline,
+input[type='button'].button-outline,
+input[type='reset'].button-outline,
+input[type='submit'].button-outline {
+  background-color: transparent;
+  color: #9b4dca;
+}
+
+.button.button-outline:focus, .button.button-outline:hover,
+button.button-outline:focus,
+button.button-outline:hover,
+input[type='button'].button-outline:focus,
+input[type='button'].button-outline:hover,
+input[type='reset'].button-outline:focus,
+input[type='reset'].button-outline:hover,
+input[type='submit'].button-outline:focus,
+input[type='submit'].button-outline:hover {
+  background-color: transparent;
+  border-color: #606c76;
+  color: #606c76;
+}
+
+.button.button-outline[disabled]:focus, .button.button-outline[disabled]:hover,
+button.button-outline[disabled]:focus,
+button.button-outline[disabled]:hover,
+input[type='button'].button-outline[disabled]:focus,
+input[type='button'].button-outline[disabled]:hover,
+input[type='reset'].button-outline[disabled]:focus,
+input[type='reset'].button-outline[disabled]:hover,
+input[type='submit'].button-outline[disabled]:focus,
+input[type='submit'].button-outline[disabled]:hover {
+  border-color: inherit;
+  color: #9b4dca;
+}
+
+.button.button-clear,
+button.button-clear,
+input[type='button'].button-clear,
+input[type='reset'].button-clear,
+input[type='submit'].button-clear {
+  background-color: transparent;
+  border-color: transparent;
+  color: #9b4dca;
+}
+
+.button.button-clear:focus, .button.button-clear:hover,
+button.button-clear:focus,
+button.button-clear:hover,
+input[type='button'].button-clear:focus,
+input[type='button'].button-clear:hover,
+input[type='reset'].button-clear:focus,
+input[type='reset'].button-clear:hover,
+input[type='submit'].button-clear:focus,
+input[type='submit'].button-clear:hover {
+  background-color: transparent;
+  border-color: transparent;
+  color: #606c76;
+}
+
+.button.button-clear[disabled]:focus, .button.button-clear[disabled]:hover,
+button.button-clear[disabled]:focus,
+button.button-clear[disabled]:hover,
+input[type='button'].button-clear[disabled]:focus,
+input[type='button'].button-clear[disabled]:hover,
+input[type='reset'].button-clear[disabled]:focus,
+input[type='reset'].button-clear[disabled]:hover,
+input[type='submit'].button-clear[disabled]:focus,
+input[type='submit'].button-clear[disabled]:hover {
+  color: #9b4dca;
+}
+
+code {
+  background: #f4f5f6;
+  border-radius: .4rem;
+  font-size: 86%;
+  margin: 0 .2rem;
+  padding: .2rem .5rem;
+  white-space: nowrap;
+}
+
+pre {
+  background: #f4f5f6;
+  border-left: 0.3rem solid #9b4dca;
+  overflow-y: hidden;
+}
+
+pre > code {
+  border-radius: 0;
+  display: block;
+  padding: 1rem 1.5rem;
+  white-space: pre;
+}
+
+hr {
+  border: 0;
+  border-top: 0.1rem solid #f4f5f6;
+  margin: 3.0rem 0;
+}
+
+input[type='color'],
+input[type='date'],
+input[type='datetime'],
+input[type='datetime-local'],
+input[type='email'],
+input[type='month'],
+input[type='number'],
+input[type='password'],
+input[type='search'],
+input[type='tel'],
+input[type='text'],
+input[type='url'],
+input[type='week'],
+input:not([type]),
+textarea,
+select {
+  -webkit-appearance: none;
+  background-color: transparent;
+  border: 0.1rem solid #d1d1d1;
+  border-radius: .4rem;
+  box-shadow: none;
+  box-sizing: inherit;
+  height: 3.8rem;
+  padding: .6rem 1.0rem .7rem;
+  width: 100%;
+}
+
+input[type='color']:focus,
+input[type='date']:focus,
+input[type='datetime']:focus,
+input[type='datetime-local']:focus,
+input[type='email']:focus,
+input[type='month']:focus,
+input[type='number']:focus,
+input[type='password']:focus,
+input[type='search']:focus,
+input[type='tel']:focus,
+input[type='text']:focus,
+input[type='url']:focus,
+input[type='week']:focus,
+input:not([type]):focus,
+textarea:focus,
+select:focus {
+  border-color: #9b4dca;
+  outline: 0;
+}
+
+select {
+  background: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 30 8" width="30"><path fill="%23d1d1d1" d="M0,0l6,8l6-8"/></svg>') center right no-repeat;
+  padding-right: 3.0rem;
+}
+
+select:focus {
+  background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 30 8" width="30"><path fill="%239b4dca" d="M0,0l6,8l6-8"/></svg>');
+}
+
+select[multiple] {
+  background: none;
+  height: auto;
+}
+
+textarea {
+  min-height: 6.5rem;
+}
+
+label,
+legend {
+  display: block;
+  font-size: 1.6rem;
+  font-weight: 700;
+  margin-bottom: .5rem;
+}
+
+fieldset {
+  border-width: 0;
+  padding: 0;
+}
+
+input[type='checkbox'],
+input[type='radio'] {
+  display: inline;
+}
+
+.label-inline {
+  display: inline-block;
+  font-weight: normal;
+  margin-left: .5rem;
+}
+
+.container {
+  margin: 0 auto;
+  max-width: 112.0rem;
+  padding: 0 2.0rem;
+  position: relative;
+  width: 100%;
+}
+
+.row {
+  display: flex;
+  flex-direction: column;
+  padding: 0;
+  width: 100%;
+}
+
+.row.row-no-padding {
+  padding: 0;
+}
+
+.row.row-no-padding > .column {
+  padding: 0;
+}
+
+.row.row-wrap {
+  flex-wrap: wrap;
+}
+
+.row.row-top {
+  align-items: flex-start;
+}
+
+.row.row-bottom {
+  align-items: flex-end;
+}
+
+.row.row-center {
+  align-items: center;
+}
+
+.row.row-stretch {
+  align-items: stretch;
+}
+
+.row.row-baseline {
+  align-items: baseline;
+}
+
+.row .column {
+  display: block;
+  flex: 1 1 auto;
+  margin-left: 0;
+  max-width: 100%;
+  width: 100%;
+}
+
+.row .column.column-offset-10 {
+  margin-left: 10%;
+}
+
+.row .column.column-offset-20 {
+  margin-left: 20%;
+}
+
+.row .column.column-offset-25 {
+  margin-left: 25%;
+}
+
+.row .column.column-offset-33, .row .column.column-offset-34 {
+  margin-left: 33.3333%;
+}
+
+.row .column.column-offset-40 {
+  margin-left: 40%;
+}
+
+.row .column.column-offset-50 {
+  margin-left: 50%;
+}
+
+.row .column.column-offset-60 {
+  margin-left: 60%;
+}
+
+.row .column.column-offset-66, .row .column.column-offset-67 {
+  margin-left: 66.6666%;
+}
+
+.row .column.column-offset-75 {
+  margin-left: 75%;
+}
+
+.row .column.column-offset-80 {
+  margin-left: 80%;
+}
+
+.row .column.column-offset-90 {
+  margin-left: 90%;
+}
+
+.row .column.column-10 {
+  flex: 0 0 10%;
+  max-width: 10%;
+}
+
+.row .column.column-20 {
+  flex: 0 0 20%;
+  max-width: 20%;
+}
+
+.row .column.column-25 {
+  flex: 0 0 25%;
+  max-width: 25%;
+}
+
+.row .column.column-33, .row .column.column-34 {
+  flex: 0 0 33.3333%;
+  max-width: 33.3333%;
+}
+
+.row .column.column-40 {
+  flex: 0 0 40%;
+  max-width: 40%;
+}
+
+.row .column.column-50 {
+  flex: 0 0 50%;
+  max-width: 50%;
+}
+
+.row .column.column-60 {
+  flex: 0 0 60%;
+  max-width: 60%;
+}
+
+.row .column.column-66, .row .column.column-67 {
+  flex: 0 0 66.6666%;
+  max-width: 66.6666%;
+}
+
+.row .column.column-75 {
+  flex: 0 0 75%;
+  max-width: 75%;
+}
+
+.row .column.column-80 {
+  flex: 0 0 80%;
+  max-width: 80%;
+}
+
+.row .column.column-90 {
+  flex: 0 0 90%;
+  max-width: 90%;
+}
+
+.row .column .column-top {
+  align-self: flex-start;
+}
+
+.row .column .column-bottom {
+  align-self: flex-end;
+}
+
+.row .column .column-center {
+  align-self: center;
+}
+
+@media (min-width: 40rem) {
+  .row {
+    flex-direction: row;
+    margin-left: -1.0rem;
+    width: calc(100% + 2.0rem);
+  }
+  .row .column {
+    margin-bottom: inherit;
+    padding: 0 1.0rem;
+  }
+}
+
+a {
+  color: #9b4dca;
+  text-decoration: none;
+}
+
+a:focus, a:hover {
+  color: #606c76;
+}
+
+dl,
+ol,
+ul {
+  list-style: none;
+  margin-top: 0;
+  padding-left: 0;
+}
+
+dl dl,
+dl ol,
+dl ul,
+ol dl,
+ol ol,
+ol ul,
+ul dl,
+ul ol,
+ul ul {
+  font-size: 90%;
+  margin: 1.5rem 0 1.5rem 3.0rem;
+}
+
+ol {
+  list-style: decimal inside;
+}
+
+ul {
+  list-style: circle inside;
+}
+
+.button,
+button,
+dd,
+dt,
+li {
+  margin-bottom: 1.0rem;
+}
+
+fieldset,
+input,
+select,
+textarea {
+  margin-bottom: 1.5rem;
+}
+
+blockquote,
+dl,
+figure,
+form,
+ol,
+p,
+pre,
+table,
+ul {
+  margin-bottom: 2.5rem;
+}
+
+table {
+  border-spacing: 0;
+  display: block;
+  overflow-x: auto;
+  text-align: left;
+  width: 100%;
+}
+
+td,
+th {
+  border-bottom: 0.1rem solid #e1e1e1;
+  padding: 1.2rem 1.5rem;
+}
+
+td:first-child,
+th:first-child {
+  padding-left: 0;
+}
+
+td:last-child,
+th:last-child {
+  padding-right: 0;
+}
+
+@media (min-width: 40rem) {
+  table {
+    display: table;
+    overflow-x: initial;
+  }
+}
+
+b,
+strong {
+  font-weight: bold;
+}
+
+p {
+  margin-top: 0;
+}
+
+h1,
+h2,
+h3,
+h4,
+h5,
+h6 {
+  font-weight: 300;
+  letter-spacing: -.1rem;
+  margin-bottom: 2.0rem;
+  margin-top: 0;
+}
+
+h1 {
+  font-size: 4.6rem;
+  line-height: 1.2;
+}
+
+h2 {
+  font-size: 3.6rem;
+  line-height: 1.25;
+}
+
+h3 {
+  font-size: 2.8rem;
+  line-height: 1.3;
+}
+
+h4 {
+  font-size: 2.2rem;
+  letter-spacing: -.08rem;
+  line-height: 1.35;
+}
+
+h5 {
+  font-size: 1.8rem;
+  letter-spacing: -.05rem;
+  line-height: 1.5;
+}
+
+h6 {
+  font-size: 1.6rem;
+  letter-spacing: 0;
+  line-height: 1.4;
+}
+
+img {
+  max-width: 100%;
+}
+
+.clearfix:after {
+  clear: both;
+  content: ' ';
+  display: table;
+}
+
+.float-left {
+  float: left;
+}
+
+.float-right {
+  float: right;
+}
+
+/*# sourceMappingURL=milligram.css.map */

+ 349 - 0
templates/assets/css/normalize.css

@@ -0,0 +1,349 @@
+/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */
+
+/* Document
+   ========================================================================== */
+
+/**
+ * 1. Correct the line height in all browsers.
+ * 2. Prevent adjustments of font size after orientation changes in iOS.
+ */
+
+html {
+  line-height: 1.15; /* 1 */
+  -webkit-text-size-adjust: 100%; /* 2 */
+}
+
+/* Sections
+   ========================================================================== */
+
+/**
+ * Remove the margin in all browsers.
+ */
+
+body {
+  margin: 0;
+}
+
+/**
+ * Render the `main` element consistently in IE.
+ */
+
+main {
+  display: block;
+}
+
+/**
+ * Correct the font size and margin on `h1` elements within `section` and
+ * `article` contexts in Chrome, Firefox, and Safari.
+ */
+
+h1 {
+  font-size: 2em;
+  margin: 0.67em 0;
+}
+
+/* Grouping content
+   ========================================================================== */
+
+/**
+ * 1. Add the correct box sizing in Firefox.
+ * 2. Show the overflow in Edge and IE.
+ */
+
+hr {
+  box-sizing: content-box; /* 1 */
+  height: 0; /* 1 */
+  overflow: visible; /* 2 */
+}
+
+/**
+ * 1. Correct the inheritance and scaling of font size in all browsers.
+ * 2. Correct the odd `em` font sizing in all browsers.
+ */
+
+pre {
+  font-family: monospace, monospace; /* 1 */
+  font-size: 1em; /* 2 */
+}
+
+/* Text-level semantics
+   ========================================================================== */
+
+/**
+ * Remove the gray background on active links in IE 10.
+ */
+
+a {
+  background-color: transparent;
+}
+
+/**
+ * 1. Remove the bottom border in Chrome 57-
+ * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.
+ */
+
+abbr[title] {
+  border-bottom: none; /* 1 */
+  text-decoration: underline; /* 2 */
+  text-decoration: underline dotted; /* 2 */
+}
+
+/**
+ * Add the correct font weight in Chrome, Edge, and Safari.
+ */
+
+b,
+strong {
+  font-weight: bolder;
+}
+
+/**
+ * 1. Correct the inheritance and scaling of font size in all browsers.
+ * 2. Correct the odd `em` font sizing in all browsers.
+ */
+
+code,
+kbd,
+samp {
+  font-family: monospace, monospace; /* 1 */
+  font-size: 1em; /* 2 */
+}
+
+/**
+ * Add the correct font size in all browsers.
+ */
+
+small {
+  font-size: 80%;
+}
+
+/**
+ * Prevent `sub` and `sup` elements from affecting the line height in
+ * all browsers.
+ */
+
+sub,
+sup {
+  font-size: 75%;
+  line-height: 0;
+  position: relative;
+  vertical-align: baseline;
+}
+
+sub {
+  bottom: -0.25em;
+}
+
+sup {
+  top: -0.5em;
+}
+
+/* Embedded content
+   ========================================================================== */
+
+/**
+ * Remove the border on images inside links in IE 10.
+ */
+
+img {
+  border-style: none;
+}
+
+/* Forms
+   ========================================================================== */
+
+/**
+ * 1. Change the font styles in all browsers.
+ * 2. Remove the margin in Firefox and Safari.
+ */
+
+button,
+input,
+optgroup,
+select,
+textarea {
+  font-family: inherit; /* 1 */
+  font-size: 100%; /* 1 */
+  line-height: 1.15; /* 1 */
+  margin: 0; /* 2 */
+}
+
+/**
+ * Show the overflow in IE.
+ * 1. Show the overflow in Edge.
+ */
+
+button,
+input { /* 1 */
+  overflow: visible;
+}
+
+/**
+ * Remove the inheritance of text transform in Edge, Firefox, and IE.
+ * 1. Remove the inheritance of text transform in Firefox.
+ */
+
+button,
+select { /* 1 */
+  text-transform: none;
+}
+
+/**
+ * Correct the inability to style clickable types in iOS and Safari.
+ */
+
+button,
+[type="button"],
+[type="reset"],
+[type="submit"] {
+  -webkit-appearance: button;
+}
+
+/**
+ * Remove the inner border and padding in Firefox.
+ */
+
+button::-moz-focus-inner,
+[type="button"]::-moz-focus-inner,
+[type="reset"]::-moz-focus-inner,
+[type="submit"]::-moz-focus-inner {
+  border-style: none;
+  padding: 0;
+}
+
+/**
+ * Restore the focus styles unset by the previous rule.
+ */
+
+button:-moz-focusring,
+[type="button"]:-moz-focusring,
+[type="reset"]:-moz-focusring,
+[type="submit"]:-moz-focusring {
+  outline: 1px dotted ButtonText;
+}
+
+/**
+ * Correct the padding in Firefox.
+ */
+
+fieldset {
+  padding: 0.35em 0.75em 0.625em;
+}
+
+/**
+ * 1. Correct the text wrapping in Edge and IE.
+ * 2. Correct the color inheritance from `fieldset` elements in IE.
+ * 3. Remove the padding so developers are not caught out when they zero out
+ *    `fieldset` elements in all browsers.
+ */
+
+legend {
+  box-sizing: border-box; /* 1 */
+  color: inherit; /* 2 */
+  display: table; /* 1 */
+  max-width: 100%; /* 1 */
+  padding: 0; /* 3 */
+  white-space: normal; /* 1 */
+}
+
+/**
+ * Add the correct vertical alignment in Chrome, Firefox, and Opera.
+ */
+
+progress {
+  vertical-align: baseline;
+}
+
+/**
+ * Remove the default vertical scrollbar in IE 10+.
+ */
+
+textarea {
+  overflow: auto;
+}
+
+/**
+ * 1. Add the correct box sizing in IE 10.
+ * 2. Remove the padding in IE 10.
+ */
+
+[type="checkbox"],
+[type="radio"] {
+  box-sizing: border-box; /* 1 */
+  padding: 0; /* 2 */
+}
+
+/**
+ * Correct the cursor style of increment and decrement buttons in Chrome.
+ */
+
+[type="number"]::-webkit-inner-spin-button,
+[type="number"]::-webkit-outer-spin-button {
+  height: auto;
+}
+
+/**
+ * 1. Correct the odd appearance in Chrome and Safari.
+ * 2. Correct the outline style in Safari.
+ */
+
+[type="search"] {
+  -webkit-appearance: textfield; /* 1 */
+  outline-offset: -2px; /* 2 */
+}
+
+/**
+ * Remove the inner padding in Chrome and Safari on macOS.
+ */
+
+[type="search"]::-webkit-search-decoration {
+  -webkit-appearance: none;
+}
+
+/**
+ * 1. Correct the inability to style clickable types in iOS and Safari.
+ * 2. Change font properties to `inherit` in Safari.
+ */
+
+::-webkit-file-upload-button {
+  -webkit-appearance: button; /* 1 */
+  font: inherit; /* 2 */
+}
+
+/* Interactive
+   ========================================================================== */
+
+/*
+ * Add the correct display in Edge, IE 10+, and Firefox.
+ */
+
+details {
+  display: block;
+}
+
+/*
+ * Add the correct display in all browsers.
+ */
+
+summary {
+  display: list-item;
+}
+
+/* Misc
+   ========================================================================== */
+
+/**
+ * Add the correct display in IE 10+.
+ */
+
+template {
+  display: none;
+}
+
+/**
+ * Add the correct display in IE 10.
+ */
+
+[hidden] {
+  display: none;
+}

+ 6 - 15
templates/dres.html

@@ -1,13 +1,11 @@
 <!DOCTYPE html>
 <!DOCTYPE html>
 <html>
 <html>
     <head>
     <head>
-        <title>
-            <%= title %>&emsp;&#x2741;&emsp;<%= site_title %>
-        </title>
+        <%- header_tpl({site_title = site_title, title = title}) %>
     </head>
     </head>
     <body>
     <body>
         <header>
         <header>
-            <h1><%= breadcrumbs[#breadcrumbs][2] %>&ensp;:&ensp;<%= title %></h1>
+            <h1><%= breadcrumbs[#breadcrumbs][2] %>:&ensp;<%= title %></h1>
         </header>
         </header>
         <main>
         <main>
             <section id="res_lineage">
             <section id="res_lineage">
@@ -29,7 +27,7 @@
             <section id="res_dmd">
             <section id="res_dmd">
                 <h2>Metadata</h2>
                 <h2>Metadata</h2>
                 <dl class="res_md">
                 <dl class="res_md">
-                <% for p, ol in pairs(dmd) do %>
+                <% for _, ol in ipairs(dmd) do %>
                     <dt>
                     <dt>
                         <% if ol.label then %><%= ol.label %>
                         <% if ol.label then %><%= ol.label %>
                         <% else %><code><%= ol.uri %></code><% end %>
                         <% else %><code><%= ol.uri %></code><% end %>
@@ -50,7 +48,7 @@
                         <% else %><code><%= ol.uri %></code><% end %>
                         <% else %><code><%= ol.uri %></code><% end %>
                     </dt>
                     </dt>
                     <% for _, o in ipairs(ol) do %>
                     <% for _, o in ipairs(ol) do %>
-                        <dd><a href="res/<%= o.data %>.html"><%= o.data %></a></dd>
+                        <dd><a href="/res/<%= o.data %>.html"><%= o.data %></a></dd>
                     <% end %>
                     <% end %>
                 <% end %>
                 <% end %>
                 </dl>
                 </dl>
@@ -61,22 +59,15 @@
                 <h2>Contains</h2>
                 <h2>Contains</h2>
                 <ul>
                 <ul>
                 <% for _, ll in ipairs(children) do %>
                 <% for _, ll in ipairs(children) do %>
-                    <li><ol>
+                <li>Sequence (<%= #ll %> items):<ol>
                     <% for i, el in ipairs(ll) do %>
                     <% for i, el in ipairs(ll) do %>
-                        <li><a href="<%= el.uri %>"><%= el.label %></a></li>
+                        <li><a href="<%= el.href %>"><%= el.label %></a></li>
                     <%end %>
                     <%end %>
                     </ol></li>
                     </ol></li>
                 <% end %>
                 <% end %>
                 </ul>
                 </ul>
             </section>
             </section>
             <% end %>
             <% end %>
-            <% if deliverable then %>
-            <section id="res_pres">
-            <% if mconf.presentation_type == "image" then %>
-                <img src="<%= deliverable -%>" />
-            <% end %>
-            </section>
-            <% end %>
         </main>
         </main>
         <footer></footer>
         <footer></footer>
     </body>
     </body>

+ 5 - 0
templates/header.html

@@ -0,0 +1,5 @@
+<!-- Milligram CSS -->
+<link rel="stylesheet" href="/css/normalize.css">
+<link rel="stylesheet" href="/css/milligram.css">
+<title><%= title %>&emsp;&#x2741;&emsp;<%= site_title %></title>
+

+ 2 - 2
templates/index.html

@@ -1,7 +1,7 @@
 <!DOCTYPE html>
 <!DOCTYPE html>
 <html>
 <html>
     <head>
     <head>
-        <title><%= title %></title>
+        <%- header_tpl({site_title = site_title, title = "Index"}) %>
     </head>
     </head>
     <body>
     <body>
         <header>
         <header>
@@ -13,7 +13,7 @@
                 <ul>
                 <ul>
                 <% for uri, data in pairs(obj_idx) do %>
                 <% for uri, data in pairs(obj_idx) do %>
                     <li class="obj_link">
                     <li class="obj_link">
-                        <a href="<%= uri:gsub('par:', 'res/') %>.html">
+                        <a href="<%= uri:gsub('par:', '/res/') %>.html">
                             <%= data.title.data %>
                             <%= data.title.data %>
                             <%if data.title.lang then %>
                             <%if data.title.lang then %>
                                 <span class="langtag"><%= data.title.lang %></span>
                                 <span class="langtag"><%= data.title.lang %></span>

+ 7 - 3
templates/ores.html

@@ -1,11 +1,11 @@
 <!DOCTYPE html>
 <!DOCTYPE html>
 <html>
 <html>
     <head>
     <head>
-        <title><%= title %>&emsp;&#x2741;&emsp;<%= fname %></title>
+        <%- header_tpl({site_title = site_title, title = fname}) %>
     </head>
     </head>
     <body>
     <body>
         <header>
         <header>
-            <h1><%= breadcrumbs[#breadcrumbs][2] -%>:&nbsp;<%= fname %></h1>
+            <h1><%= breadcrumbs[#breadcrumbs][2] -%>:&ensp;<%= fname %></h1>
         </header>
         </header>
         <main>
         <main>
             <section id="res_lineage">
             <section id="res_lineage">
@@ -48,7 +48,7 @@
                         <% else %><code><%= ol.uri %></code><% end %>
                         <% else %><code><%= ol.uri %></code><% end %>
                     </dt>
                     </dt>
                     <% for _, o in ipairs(ol) do %>
                     <% for _, o in ipairs(ol) do %>
-                        <dd><a href="res/<%= o.data %>.html"><%= o.data %></a></dd>
+                        <dd><a href="/res/<%= o.data %>.html"><%= o.data %></a></dd>
                     <% end %>
                     <% end %>
                 <% end %>
                 <% end %>
                 </dl>
                 </dl>
@@ -58,6 +58,10 @@
             <section id="res_pres">
             <section id="res_pres">
             <% if mconf.presentation_type == "image" then %>
             <% if mconf.presentation_type == "image" then %>
                 <img src="<%= deliverable -%>" />
                 <img src="<%= deliverable -%>" />
+            <%else %>
+                <a href="<%= deliverable -%>" download="<%= fname -%>">
+                    Download file
+                </a>
             <% end %>
             <% end %>
             </section>
             </section>
             <% end %>
             <% end %>

+ 1 - 1
test/sample_submission/postcard-bag/data/submission-v2.csv

@@ -1,4 +1,4 @@
-"pas:sourcePath","dc:identifier","pas:contentType","dc:title","dc:alternative","dc:description"
+"pas:sourcePath","id","pas:contentType","dc:title","dc:alternative","dc:description"
 0001,0001,"Postcard","Example Postcard","This is an alternative label","Note that recto and verso representations have been named front and back, to emphasize that the ordering is not alphabetical."
 0001,0001,"Postcard","Example Postcard","This is an alternative label","Note that recto and verso representations have been named front and back, to emphasize that the ordering is not alphabetical."
 ,,,,"And this is another alternative label","Second description."
 ,,,,"And this is another alternative label","Second description."
 ,,,,"Yet another alt label.",
 ,,,,"Yet another alt label.",