Переглянути джерело

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 тиждень тому
батько
коміт
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",
             min_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"] = {
             label = "First child",
             type = "resource",
-            range = {["pas:Part"] = true},
+            range = {["pas:Part"] = true, ["pas:File"] = true},
         },
         ["pas:next"] = {
             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)
 --]]
 
-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")))
 fh:close()
 
+fh = datafile.open("templates/header.html")
+header_tpl = assert(etlua.compile(fh:read("a")))
+fh:close()
+
 
 -- HTML generator module.
 local M = {
@@ -62,9 +66,9 @@ local function generate_dres(s, mconf)
     local dmd = {}
     local rel = {}
     local children = {}
+    local title
     -- Metadata
     local attrs = gr:connections(s, term.LINK_OUTBOUND)
-    --require "debugger"()
     for p, ots in pairs(attrs) do
         local fname = nsm.denormalize_uri(p.data)
         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
         elseif fname == "pas:first" then
             -- 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 tn_p = term.new_iriref_ns("pas:thumbnail")
             for o in pairs(ots) do
                 -- Loop through all first children.
                 local node_uri = o
-                logger:debug("local node_uri", node_uri.data)
+                logger:debug("local node_uri: ", node_uri.data)
                 local ll = {}
+                --require "debugger"()
                 while node_uri do
                     -- Loop trough all next nodes for each first child.
-                    local el_gr = graph.get(node_uri, pkar.store)
                     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"
                 end
                 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
         else
             -- Descriptive metadata.
-            dmd[fname] = {label = p_label, uri = fname}
+            local attr = {label = p_label, uri = fname}
             -- 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
         ::skip::
     end
-    table.sort(dmd)
+    table.sort(dmd, function(a, b) return (a.label < b.label) end)
     table.sort(rel)
     table.sort(children)
     logger:debug("Lineage:", pp.write(mconf.lineage))
@@ -120,7 +127,8 @@ local function generate_dres(s, mconf)
 
     out_html = dres_tpl({
         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,
         uri = s,
         dmd = dmd,
@@ -136,6 +144,8 @@ local function generate_dres(s, mconf)
             "%s/%s.html", M.res_dir, res_id), "w"))
     ofh:write(out_html)
     ofh:close()
+
+   return true
 end
 
 
@@ -184,7 +194,7 @@ local function generate_ores(s, mconf)
     if txconf.ext then
         dest_fname = plpath.splitext(dest_fname) .. txconf.ext
     end
-    dest = M.asset_dir .. "/" .. dest_fname
+    dest = M.media_dir .. "/" .. dest_fname
     assert(transformers[txconf.fn](
             res_path[1], dest, table.unpack(txconf or NT)))
     local deliverable = dest:gsub(pkar.config.htmlgen.out_dir, "..")
@@ -193,6 +203,7 @@ local function generate_ores(s, mconf)
     out_html = ores_tpl({
         site_title = pkar.config.site.title or pkar.default_title,
         fname = plpath.basename(techmd["pas:sourcePath"][1]),
+        header_tpl = header_tpl,
         mconf = mconf,
         uri = s,
         techmd = techmd,
@@ -207,6 +218,8 @@ local function generate_ores(s, mconf)
             "%s/%s.html", M.res_dir, res_id), "w"))
     ofh:write(out_html)
     ofh:close()
+
+   return true
 end
 
 
@@ -225,17 +238,13 @@ end
 
 
 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 subjects = gr:unique_terms(triple.POS_S)
 
     -- 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
 
 
@@ -257,6 +266,7 @@ M.generate_idx = function()
     logger:debug(pp.write(obj_idx))
     out_html = idx_tpl({
         title = pkar.config.site.title or pkar.default_title,
+        header_tpl = header_tpl,
         nsm = nsm,
         obj_idx = obj_idx,
     })
@@ -267,6 +277,22 @@ M.generate_idx = function()
     logger:debug("Writing info at ", idx_path)
     ofh:write(out_html)
     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
 
 

+ 16 - 8
src/submission.lua

@@ -184,7 +184,12 @@ M.generate_sip_v2 = function(path)
         if row["pas:sourcePath"] ~= "" then
             prev_path = row["pas:sourcePath"]
             -- 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
                 if v == "" then goto cont1 end  -- skip empty strings.
                 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("(.*/)") ==
                             v["pas:sourcePath"]:match("(.*/)") then
                 --print("next match.")
-                v["pas:next"] = "pas:" .. sip[j].id
+                v["pas:next"] = sip[j].id
             end
             if not v["pas:first"] and
                     sip[j]["pas:sourcePath"]:match("^" .. escape_pattern(v["pas:sourcePath"])) then
                 --print("First child match.")
-                v["pas:first"] = "pas:" .. sip[j].id
+                v["pas:first"] = sip[j].id
             end
         end
         v._sort = nil
     end
-
+    --require "debugger"()
 
     return sip
 end
@@ -252,25 +257,28 @@ M.update_rsrc_md = function(rsrc)
     triples = {}
 
     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
+        -- id is the subject, it won't be an attribute.
+        if k == "id" then goto skip end
+
         print("Adding attribute:", k, v)
         local p = term.new_iriref_ns(k)
         local o
         if type(v) == "table" then
             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)
                 else o = term.new_lit(vv) end
                 table.insert(triples, triple.new(s, p, o))
             end
         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)
             else o = term.new_lit(v) end
             table.insert(triples, triple.new(s, p, o))
         end
+        ::skip::
     end
     for i, m in ipairs(rmod.lineage) do
         table.insert(

+ 1 - 0
src/transformers.lua

@@ -7,6 +7,7 @@ M = {}
 -- Resize an image to a maximum size on either dimension.
 M.img_resize = function(src, dest, size)
     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)
 
     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>
 <html>
     <head>
-        <title>
-            <%= title %>&emsp;&#x2741;&emsp;<%= site_title %>
-        </title>
+        <%- header_tpl({site_title = site_title, title = title}) %>
     </head>
     <body>
         <header>
-            <h1><%= breadcrumbs[#breadcrumbs][2] %>&ensp;:&ensp;<%= title %></h1>
+            <h1><%= breadcrumbs[#breadcrumbs][2] %>:&ensp;<%= title %></h1>
         </header>
         <main>
             <section id="res_lineage">
@@ -29,7 +27,7 @@
             <section id="res_dmd">
                 <h2>Metadata</h2>
                 <dl class="res_md">
-                <% for p, ol in pairs(dmd) do %>
+                <% for _, ol in ipairs(dmd) do %>
                     <dt>
                         <% if ol.label then %><%= ol.label %>
                         <% else %><code><%= ol.uri %></code><% end %>
@@ -50,7 +48,7 @@
                         <% else %><code><%= ol.uri %></code><% end %>
                     </dt>
                     <% 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 %>
                 </dl>
@@ -61,22 +59,15 @@
                 <h2>Contains</h2>
                 <ul>
                 <% for _, ll in ipairs(children) do %>
-                    <li><ol>
+                <li>Sequence (<%= #ll %> items):<ol>
                     <% 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 %>
                     </ol></li>
                 <% end %>
                 </ul>
             </section>
             <% end %>
-            <% if deliverable then %>
-            <section id="res_pres">
-            <% if mconf.presentation_type == "image" then %>
-                <img src="<%= deliverable -%>" />
-            <% end %>
-            </section>
-            <% end %>
         </main>
         <footer></footer>
     </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>
 <html>
     <head>
-        <title><%= title %></title>
+        <%- header_tpl({site_title = site_title, title = "Index"}) %>
     </head>
     <body>
         <header>
@@ -13,7 +13,7 @@
                 <ul>
                 <% for uri, data in pairs(obj_idx) do %>
                     <li class="obj_link">
-                        <a href="<%= uri:gsub('par:', 'res/') %>.html">
+                        <a href="<%= uri:gsub('par:', '/res/') %>.html">
                             <%= data.title.data %>
                             <%if data.title.lang then %>
                                 <span class="langtag"><%= data.title.lang %></span>

+ 7 - 3
templates/ores.html

@@ -1,11 +1,11 @@
 <!DOCTYPE html>
 <html>
     <head>
-        <title><%= title %>&emsp;&#x2741;&emsp;<%= fname %></title>
+        <%- header_tpl({site_title = site_title, title = fname}) %>
     </head>
     <body>
         <header>
-            <h1><%= breadcrumbs[#breadcrumbs][2] -%>:&nbsp;<%= fname %></h1>
+            <h1><%= breadcrumbs[#breadcrumbs][2] -%>:&ensp;<%= fname %></h1>
         </header>
         <main>
             <section id="res_lineage">
@@ -48,7 +48,7 @@
                         <% else %><code><%= ol.uri %></code><% end %>
                     </dt>
                     <% 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 %>
                 </dl>
@@ -58,6 +58,10 @@
             <section id="res_pres">
             <% if mconf.presentation_type == "image" then %>
                 <img src="<%= deliverable -%>" />
+            <%else %>
+                <a href="<%= deliverable -%>" download="<%= fname -%>">
+                    Download file
+                </a>
             <% end %>
             </section>
             <% 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."
 ,,,,"And this is another alternative label","Second description."
 ,,,,"Yet another alt label.",