Browse Source

Use own IDs for attribute & type names.

scossu 2 weeks ago
parent
commit
bf74a0c770

+ 1 - 0
README.md

@@ -255,6 +255,7 @@ functional and available for use by the intended audience.
   - Relatioships inference
   - Relatioships inference
 - htmlgen option for local file or webserver URL generation
 - htmlgen option for local file or webserver URL generation
 - Improve search indexing
 - Improve search indexing
+- Category browsing
 - CLI
 - CLI
   - Generate LL (multi)
   - Generate LL (multi)
   - Generate RDF (multi)
   - Generate RDF (multi)

+ 4 - 5
config/app.lua

@@ -14,11 +14,10 @@ return {
     md = {
     md = {
         -- Single-valued fields. TODO rely on content model cardinality.
         -- Single-valued fields. TODO rely on content model cardinality.
         single_values = {
         single_values = {
-            ["dc:identifier"] = true,
-            ["pas:contentType"] = true,
-            ["dc:title"] = true,
-            ["path"] = true,
-            ["pas:sourcePath"] = true,
+            ["content_type"] = true,
+            ["label"] = true,
+            ["archive_path"] = true,
+            ["source_path"] = true,
         },
         },
 
 
         -- Map of data types in prop definitions to RDF literal types.
         -- Map of data types in prop definitions to RDF literal types.

+ 13 - 0
config/model/generation.lua

@@ -0,0 +1,13 @@
+return {
+    still_image_file = {
+        presentation_type = "image",
+
+        -- Transformers for site generation.
+        transformers = {
+            -- Main deliverable asset.
+            pres = {fn = "img_resize", ext = ".jpg", 1024},
+            -- Thumbnail derivative.
+            thumbnail = {fn = "img_resize", ext = ".jpg", 256},
+        },
+    },
+}

+ 2 - 1
config/model/typedef/agent.lua

@@ -1,4 +1,5 @@
 return {
 return {
   label = "Agent",
   label = "Agent",
-  broader = "pas:Anything"
+  uri = "pas:Agent",
+  broader = "anything"
 }
 }

+ 35 - 18
config/model/typedef/anything.lua

@@ -1,71 +1,88 @@
 return {
 return {
-    id = "pas:Anything",
+    uri = "pas:Anything",
     label = "Anything",
     label = "Anything",
     description = "Superclass of every resource type in the system.",
     description = "Superclass of every resource type in the system.",
     abstract = "true",
     abstract = "true",
 
 
     properties = {
     properties = {
-        ["pas:sourcePath"] = {
+        source_path = {
+            uri = "pas:sourcePath",
             label = "Source path",
             label = "Source path",
-            description = [[
-                  File or folder path relative to the SIP root as it was
-                  originally submitted.]],
+            description =
+                [[Path of the resource at deposit time, relative to the SIP
+                root.]],
             type = "string",
             type = "string",
             min_cardinality = 1,
             min_cardinality = 1,
             max_cardinality = 1,
             max_cardinality = 1,
         },
         },
-        ["pas:contentType"] = {
+        content_type = {
+            uri = "pas:contentType",
             label = "Content type",
             label = "Content type",
             type = "resource",
             type = "resource",
             min_cardinality = 1,
             min_cardinality = 1,
             max_cardinality = 1,
             max_cardinality = 1,
         },
         },
         --[[
         --[[
-        ["pas:id"] = {
+        id = {
+            uri = "pas:id",
             label = "Primary ID",
             label = "Primary ID",
             type = "string",
             type = "string",
             min_cardinality = 1,
             min_cardinality = 1,
             max_cardinality = 1,
             max_cardinality = 1,
         },
         },
         --]]
         --]]
-        ["dc:identifier"] = {
+        ext_id = {
+            uri = "dc:identifier",
             label = "External system ID",
             label = "External system ID",
             type = "string",
             type = "string",
         },
         },
-        ["dc:title"] = {
+        label = {
+            uri = "dc:title",
             label = "Title",
             label = "Title",
             type = "string",
             type = "string",
             max_cardinality = 1,
             max_cardinality = 1,
         },
         },
-        ["dc:alternative"] = {
+        alt_label = {
+            uri = "dc:alternative",
             label = "Alternative Label",
             label = "Alternative Label",
             type = "string",
             type = "string",
         },
         },
-        ["dc:description"] = {
+        description = {
+            uri = "dc:description",
             label = "Description",
             label = "Description",
             type = "string",
             type = "string",
         },
         },
-        ["dc:created"] = {
-            label = "Created On",
+        submitted = {
+            uri = "dc:dateSubmitted",
+            label = "Submitted On",
             type = "datetime",
             type = "datetime",
             min_cardinality = 1,
             min_cardinality = 1,
             max_cardinality = 1,
             max_cardinality = 1,
         },
         },
-        ["dc:modified"] = {
+        last_modified = {
+            uri = "dc:modified",
             label = "Last Updated On",
             label = "Last Updated On",
             type = "datetime",
             type = "datetime",
             min_cardinality = 1,
             min_cardinality = 1,
             max_cardinality = 1,
             max_cardinality = 1,
         },
         },
-        ["dc:creator"] = {
+        submitted_by = {
+            uri = "dc:creator",
+            label = "Submitted By",
+            type = "rel",
+            range = {agent = true},
+        },
+        created_by = {
+            uri = "dc:creator",
             label = "Created By",
             label = "Created By",
             type = "rel",
             type = "rel",
-            range = {["pas:Agent"] = true},
+            range = {agent = true},
         },
         },
-        ["dc:contributor"] = {
+        last_modified_by = {
+            uri = "dc:contributor",
             label = "Last Updated By",
             label = "Last Updated By",
             type = "rel",
             type = "rel",
-            range = {["pas:Agent"] = true},
+            range = {agent = true},
         },
         },
     },
     },
 }
 }

+ 25 - 7
config/model/typedef/artifact.lua

@@ -1,19 +1,37 @@
 return {
 return {
-    id = "pas:Artifact",
+    uri = "pas:Artifact",
     label = "Artifact",
     label = "Artifact",
     description = "Intellectual work.",
     description = "Intellectual work.",
-    broader = "pas:Anything",
+    broader = "anything",
 
 
     properties = {
     properties = {
-        ["pas:first"] = {
+        first = {
+            uri = "pas:first",
             label = "First child",
             label = "First child",
             type = "resource",
             type = "resource",
-            range = {["pas:Part"] = true},
+            range = {part = true},
         },
         },
-        ["pas:hasFile"] = {
-            label = "Has file",
+        has_representation = {
+            uri = "pas:hasRepresentation",
+            label = "Representation",
+            description =
+                [[Preferred representation. Used to generate
+                a thumbnail (for a visual item) or sample (for non-visual
+                materials such as audio).]],
             type = "resource",
             type = "resource",
-            range = {["pas:File"] = true},
+            range = {file = true},
+        },
+        has_file = {
+            uri = "pas:hasFile",
+            label = "Related file",
+            type = "resource",
+            range = {file = true},
+        },
+        location = {
+            uri = "pas:location",
+            label = "Location",
+            description = "Location related to the artifact.",
+            type = "string",
         },
         },
     },
     },
 }
 }

+ 6 - 4
config/model/typedef/brick.lua

@@ -16,17 +16,19 @@ In a more complex hierarchy, any given Part may have both "first" and
 --]]
 --]]
 
 
 return {
 return {
-    id = "pas:Brick",
+    uri = "pas:Brick",
     label = "Brick",
     label = "Brick",
 
 
-    broader = "pas:Anything",
+    broader = "anything",
 
 
     properties = {
     properties = {
-        ["pas:first"] = {
+        first = {
+            uri = "pas:first",
             label = "First child",
             label = "First child",
             type = "resource",
             type = "resource",
         },
         },
-        ["pas:next"] = {
+        next = {
+            uri = "pas:next",
             label = "Next sibling",
             label = "Next sibling",
             type = "resource",
             type = "resource",
             max_cardinality = 1,
             max_cardinality = 1,

+ 6 - 6
config/model/typedef/collection.lua

@@ -1,15 +1,15 @@
 return {
 return {
-    id = "pas:Collection",
+    uri = "pas:Collection",
     label = "Collection",
     label = "Collection",
 
 
-    broader = "pas:Brick",
+    broader = "brick",
 
 
     properties = {
     properties = {
-        ["pas:first"] = {
-            range = {["pas:Collection"] = true, ["pas:Artifact"] = true},
+        first = {
+            range = {collection = true, artifact = true},
         },
         },
-        ["pas:next"] = {
-            range = {["pas:Collection"] = true, ["pas:Artifact"] = true},
+        next = {
+            range = {collection = true, artifact = true},
         }
         }
     }
     }
 }
 }

+ 2 - 2
config/model/typedef/document.lua

@@ -1,6 +1,6 @@
 return {
 return {
-    id = "pas:Document",
+    uri = "pas:Document",
     label = "Document",
     label = "Document",
-    broader = "pas:Artifact",
+    broader = "artifact",
 }
 }
 
 

+ 17 - 21
config/model/typedef/file.lua

@@ -1,51 +1,47 @@
 return {
 return {
-    id = "pas:File",
+    uri = "pas:File",
     label = "File",
     label = "File",
-    broader = "pas:Anything",
+    broader = "anything",
 
 
     properties = {
     properties = {
-        --[[
-        ["pas:location"] = {
-            label = "Location",
-            type = "string",
-            min_cardinality = 1,
-            max_cardinality = 1,
-        },
-        --]]
-        ["pas:path"] = {
+        archive_path = {
+            uri = "pas:archivePath",
             label = "Archival path",
             label = "Archival path",
-            description = [[
-              Path of the preserved file, relative to the archival root. ]],
+            description =
+              [[Path of the preserved resource, relative to the archival
+              root.]],
             type = "string",
             type = "string",
             min_cardinality = 1,
             min_cardinality = 1,
             max_cardinality = 1,
             max_cardinality = 1,
         },
         },
-        ["pas:next"] = {
-            label = "Next sibling",
-            type = "resource",
-            range = {["pas:Part"] = true},
+        next = {
+            range = {Part = true, File = true},
             max_cardinality = 1,
             max_cardinality = 1,
         },
         },
-        ["pas:thumbnail"] = {
+        thumbnail = {
+            uri = "pas:thumbnail",
             label = "Thumbnail",
             label = "Thumbnail",
             type = "string",
             type = "string",
             --min_cardinality = 1,
             --min_cardinality = 1,
             max_cardinality = 1,
             max_cardinality = 1,
         },
         },
-        ["dc:format"] = {
+        format = {
+            uri = "dc:format",
             label = "MIME type",
             label = "MIME type",
             type = "string",
             type = "string",
             min_cardinality = 1,
             min_cardinality = 1,
             max_cardinality = 1,
             max_cardinality = 1,
         },
         },
-        ["dc:extent"] = {
+        size = {
+            uri = "dc:extent",
             label = "File size",
             label = "File size",
             description = "File size in bytes.",
             description = "File size in bytes.",
             type = "integer",
             type = "integer",
             min_cardinality = 1,
             min_cardinality = 1,
             max_cardinality = 1,
             max_cardinality = 1,
         },
         },
-        ["premis:hasMessageDigest"] = {
+        checksum = {
+            uri = "premis:hasMessageDigest",
             label = "Checksum",
             label = "Checksum",
             description = [[
             description = [[
                 File checksum formatted as an URN:
                 File checksum formatted as an URN:

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

@@ -1,15 +1,15 @@
 return {
 return {
-    id = "pas:Part",
+    uri = "pas:Part",
     label = "Part",
     label = "Part",
 
 
-    broader = "pas:Brick",
+    broader = "brick",
 
 
     properties = {
     properties = {
-        ["pas:first"] = {
-            range = {["pas:Part"] = true, ["pas:File"] = true},
+        first = {
+            range = {part = true, file = true},
         },
         },
-        ["pas:next"] = {
-            range = {["pas:Part"] = true},
+        next = {
+            range = {part = true},
         }
         }
     }
     }
 }
 }

+ 8 - 6
config/model/typedef/postcard.lua

@@ -1,17 +1,19 @@
 return {
 return {
-    id = "pas:Postcard",
+    uri = "pas:Postcard",
     label = "Postcard",
     label = "Postcard",
-    broader = "pas:Document",
+    broader = "document",
     properties = {
     properties = {
-        ["pas:recto"] = {
+        recto = {
+            uri = "pas:recto",
             label = "Recto",
             label = "Recto",
             type = "resource",
             type = "resource",
-            range = {["pas:Part"] = true},
+            range = {part = true},
         },
         },
-        ["pas:verso"] = {
+        verso = {
+            uri = "pas:verso",
             label = "Verso",
             label = "Verso",
             type = "resource",
             type = "resource",
-            range = {["pas:Part"] = true},
+            range = {part = true},
         },
         },
     }
     }
 }
 }

+ 2 - 2
config/model/typedef/still_image.lua

@@ -1,5 +1,5 @@
 return {
 return {
-    id = "pas:StillImage",
+    uri = "pas:StillImage",
     label = "Still Image",
     label = "Still Image",
-    broader = "pas:Artifact",
+    broader = "artifact",
 }
 }

+ 2 - 12
config/model/typedef/still_image_file.lua

@@ -1,15 +1,5 @@
 return {
 return {
-    id = "pas:StillImageFile",
+    uri = "pas:StillImageFile",
     label = "Still Image File",
     label = "Still Image File",
-    broader = "pas:File",
-
-    presentation_type = "image",
-
-    -- Transformers for site generation.
-    transformers = {
-        -- Main deliverable asset.
-        deliverable = {fn = "img_resize", ext = ".jpg", 1024},
-        -- Thumbnail derivative.
-        thumbnail = {fn = "img_resize", ext = ".jpg", 256},
-    },
+    broader = "file",
 }
 }

+ 0 - 0
out/html/.keep


+ 28 - 2
src/core.lua

@@ -16,7 +16,7 @@ local PROTECTED = {
 local fpath = debug.getinfo(1, "S").source:sub(2)
 local fpath = debug.getinfo(1, "S").source:sub(2)
 local root_path = path.dirname(path.dirname(fpath))
 local root_path = path.dirname(path.dirname(fpath))
 local config_path = os.getenv("PKAR_CONFIG_DIR") or
 local config_path = os.getenv("PKAR_CONFIG_DIR") or
-        path.join(root_path .. "/config")
+        path.join(root_path, "/config")
 
 
 local config = dofile(path.join(config_path, "app.lua"))
 local config = dofile(path.join(config_path, "app.lua"))
 local store_id = "file://" .. (os.getenv("PKAR_BASE") or config.fs.dres_path)
 local store_id = "file://" .. (os.getenv("PKAR_BASE") or config.fs.dres_path)
@@ -28,6 +28,7 @@ for pfx, ns in pairs(config.namespace) do nsm.add(pfx, ns) end
 local M = {
 local M = {
     -- Project root path.
     -- Project root path.
     root = root_path,
     root = root_path,
+    config_path = config_path,
     config = config,
     config = config,
 
 
     default_title = "Pocket Archive",
     default_title = "Pocket Archive",
@@ -49,7 +50,7 @@ local M = {
     RDF_TYPE = term.new_iriref_ns("rdf:type"),
     RDF_TYPE = term.new_iriref_ns("rdf:type"),
 
 
     DC_TITLE_P = term.new_iriref_ns("dc:title"),
     DC_TITLE_P = term.new_iriref_ns("dc:title"),
-    DC_CREATED_P = term.new_iriref_ns("dc:created"),
+    SUBMITTED_P = term.new_iriref_ns("dc:dateSubmitted"),
     TN_P = term.new_iriref_ns("pas:thumbnail"),
     TN_P = term.new_iriref_ns("pas:thumbnail"),
     FIRST_P = term.new_iriref_ns("pas:first"),
     FIRST_P = term.new_iriref_ns("pas:first"),
     NEXT_P = term.new_iriref_ns("pas:next"),
     NEXT_P = term.new_iriref_ns("pas:next"),
@@ -73,6 +74,28 @@ local M = {
 
 
 }
 }
 
 
+-- Adapted from lua-núcleo
+M.escape_pattern = function(s)
+    local matches = {
+        ["^"] = "%^";
+        ["$"] = "%$";
+        ["("] = "%(";
+        [")"] = "%)";
+        ["%"] = "%%";
+        ["."] = "%.";
+        ["["] = "%[";
+        ["]"] = "%]";
+        ["*"] = "%*";
+        ["+"] = "%+";
+        ["-"] = "%-";
+        ["?"] = "%?";
+        ["\0"] = "%z";
+    }
+
+    return (s:gsub(".", matches))
+end
+
+
 --[[
 --[[
   Generate pairtree directory and file path from an ID string and prefix.
   Generate pairtree directory and file path from an ID string and prefix.
 
 
@@ -130,6 +153,9 @@ setmetatable (M, mt)
 
 
 
 
 -- Initialize random ID generator.
 -- Initialize random ID generator.
+-- TODO This must be set to completely random or use another random generator
+-- so that resource IDs are not repeated for each process. For testing it's
+-- convenient that resource IDs have always the same pattern.
 math.randomseed(M.config.id.seed[1], M.config.id.seed[2])
 math.randomseed(M.config.id.seed[1], M.config.id.seed[2])
 
 
 
 

+ 95 - 68
src/generator.lua

@@ -3,7 +3,7 @@ local datafile = require "datafile"
 local dir = require "pl.dir"
 local dir = require "pl.dir"
 local etlua = require "etlua"
 local etlua = require "etlua"
 local json = require "cjson"
 local json = require "cjson"
-local plpath = require "pl.path"
+local path = require "pl.path"
 local pp = require "pl.pretty"
 local pp = require "pl.pretty"
 
 
 local nsm = require "volksdata.namespace"
 local nsm = require "volksdata.namespace"
@@ -27,9 +27,9 @@ local NT = {}
 local subjects
 local subjects
 
 
 local asset_dir = pkar.config.htmlgen.out_dir
 local asset_dir = pkar.config.htmlgen.out_dir
-local index_path = plpath.join(asset_dir, "js", "fuse_index.json")
-local keys_path = plpath.join(asset_dir, "js", "fuse_keys.json")
-local idx_ignore = {["pas_first"] = true, ["pas:next"] = true,}
+local index_path = path.join(asset_dir, "js", "fuse_index.json")
+local keys_path = path.join(asset_dir, "js", "fuse_keys.json")
+local idx_ignore = {first = true, next = true,}
 -- Collector for all search term keys.
 -- Collector for all search term keys.
 local idx_keys = {}
 local idx_keys = {}
 
 
@@ -50,12 +50,15 @@ end
 
 
 -- HTML generator module.
 -- HTML generator module.
 local M = {
 local M = {
-    res_dir = plpath.join(pkar.config.htmlgen.out_dir, "res"),
+    res_dir = path.join(pkar.config.htmlgen.out_dir, "res"),
     asset_dir = asset_dir,
     asset_dir = asset_dir,
-    media_dir = plpath.join(pkar.config.htmlgen.out_dir, "media"),
+    media_dir = path.join(pkar.config.htmlgen.out_dir, "media"),
     webroot = "",  -- TODO switch depending on local FS or webserver generation.
     webroot = "",  -- TODO switch depending on local FS or webserver generation.
 }
 }
 
 
+local TN_FS_PATH = path.join(M.media_dir, "thumbnail")
+local TN_WEB_PATH = TN_FS_PATH:gsub(pkar.config.htmlgen.out_dir, "")
+
 
 
 local function get_breadcrumbs(mconf)
 local function get_breadcrumbs(mconf)
     -- Breadcrumbs, from top class to current class.
     -- Breadcrumbs, from top class to current class.
@@ -64,7 +67,7 @@ local function get_breadcrumbs(mconf)
     for i = 1, #mconf.lineage do
     for i = 1, #mconf.lineage do
         breadcrumbs[i] = {
         breadcrumbs[i] = {
             mconf.lineage[i],
             mconf.lineage[i],
-            model.models[mconf.lineage[i]].label
+            model.types[mconf.lineage[i]].label
         }
         }
     end
     end
 
 
@@ -72,19 +75,31 @@ local function get_breadcrumbs(mconf)
 end
 end
 
 
 
 
-local function get_tn_url(s)
+--[[ Infer thumbnail web path from the resource subject URI.
+
+    If the resource is not a file and as such does not have a thumbnail of
+    its own, traverse the list of first children and use the first one found
+    with a thumbnail.
+
+    @TODO This should actually use the `pas:hasRepresentation` property
+    instead of the first child.
+
+    @param[in] s Subject (resource) URI.
+
+    @param[in] ext Optional extension to add, including the extension separator
+    (`.`). If not provided, `.jpg` is used.
+--]]
+local function get_tn_url(s, ext)  -- TODO caller needs to pass correct ext
     if repo.gr:attr(s, pkar.RDF_TYPE)[pkar.FILE_T.hash] then
     if repo.gr:attr(s, pkar.RDF_TYPE)[pkar.FILE_T.hash] then
         -- The subject is a file.
         -- The subject is a file.
-        tn_fname = (s.data:gsub(pkar.PAR_NS, "") .. ".jpg")  -- FIXME do not hardcode.
-        return plpath.join(
-                M.media_dir, tn_fname:sub(1, 2), tn_fname:sub(3, 4), tn_fname)
+        return pkar.gen_pairtree(TN_WEB_PATH, s.data, ext or ".jpg", true)
     end
     end
 
 
     -- Recurse through all first children until one with a thumbnail, or a
     -- Recurse through all first children until one with a thumbnail, or a
     -- leaf without children, is found.
     -- leaf without children, is found.
     local first_child
     local first_child
     _, first_child = next(repo.gr:attr(s, pkar.FIRST_P))
     _, first_child = next(repo.gr:attr(s, pkar.FIRST_P))
-    if first_child then return get_tn_url(first_child) end
+    if first_child then return get_tn_url(first_child, ext) end
 end
 end
 
 
 
 
@@ -96,15 +111,20 @@ local function generate_dres(s, mconf)
     -- Metadata
     -- Metadata
     local attrs = repo.gr:connections(s, term.LINK_OUTBOUND)
     local attrs = repo.gr:connections(s, term.LINK_OUTBOUND)
     for p, ots in pairs(attrs) do
     for p, ots in pairs(attrs) do
-        local pname = nsm.denormalize_uri(p.data)
-        p_label = ((mconf.properties or NT)[pname] or NT).label
+        local pname = model.uri_to_id[nsm.denormalize_uri(p.data)] or p.data
+        logger:debug("DRES pname: " .. pname)
+        local pconf = ((mconf.properties or NT)[pname] or NT)
         -- RDF types are shown in in breadcrumbs.
         -- RDF types are shown in in breadcrumbs.
-        if pname == "rdf:type" then goto skip
+        if pname == pkar.RDF_TYPE.data then goto skip
         elseif ((mconf.properties or NT)[pname] or NT).type == "rel" then
         elseif ((mconf.properties or NT)[pname] or NT).type == "rel" then
             -- Relationship.
             -- Relationship.
-            rel[pname] = {label = p_label, uri = pname}
+            rel[pname] = {
+                label = pconf.label,
+                description = pconf.description,
+                uri = pconf.uri
+            }
             for _, o in pairs(ots) do table.insert(dmd[pname], o.data) end
             for _, o in pairs(ots) do table.insert(dmd[pname], o.data) end
-        elseif pname == "pas:first" then
+        elseif pname == "first" then
             -- Build a linked list for every first found.
             -- Build a linked list for every first found.
             for _, o in pairs(ots) do
             for _, o in pairs(ots) do
                 -- Loop through all first children.
                 -- Loop through all first children.
@@ -118,18 +138,17 @@ local function generate_dres(s, mconf)
                 if label then label = label.data
                 if label then label = label.data
                 else
                 else
                     _, label = next(repo.gr:attr(child_s, pkar.PATH_P))
                     _, label = next(repo.gr:attr(child_s, pkar.PATH_P))
-                    if label then label = plpath.basename(label.data)
+                    if label then label = path.basename(label.data)
                         else label = child_s.data end
                         else label = child_s.data end
                 end
                 end
 
 
                 while child_s do
                 while child_s do
                     -- Loop trough all next nodes for each first child.
                     -- Loop trough all next nodes for each first child.
-                    --require "debugger".assert(get_tn_url(child_s))
                     table.insert(ll, {
                     table.insert(ll, {
                         href = pkar.gen_pairtree(
                         href = pkar.gen_pairtree(
                                 "/res", child_s.data, ".html", true),
                                 "/res", child_s.data, ".html", true),
                         label = label,
                         label = label,
-                        tn = get_tn_url(child_s):gsub(M.media_dir, "/media/tn"),
+                        tn = get_tn_url(child_s),
                     })
                     })
                     logger:debug("Child label for ", child_s.data, ": ", ll[#ll].label or "nil")
                     logger:debug("Child label for ", child_s.data, ": ", ll[#ll].label or "nil")
                     -- There can only be one "next"
                     -- There can only be one "next"
@@ -137,12 +156,16 @@ local function generate_dres(s, mconf)
                 end
                 end
                 table.insert(children, ll)
                 table.insert(children, ll)
             end
             end
-        elseif pname == "pas:next" then
+        elseif pname == "next" then
             -- Sibling.
             -- Sibling.
             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.
-            local attr = {label = p_label, uri = pname}
+            local attr = {
+                label = pconf.label or pname,
+                description = pconf.description,
+                uri = pconf.uri,
+            }
             -- TODO differentiate term types
             -- TODO differentiate term types
             for _, o in pairs(ots) do table.insert(attr, o.data) end
             for _, o in pairs(ots) do table.insert(attr, o.data) end
             table.sort(attr)
             table.sort(attr)
@@ -195,20 +218,28 @@ local function generate_ores(s, mconf)
     -- Metadata
     -- Metadata
     local attrs = repo.gr:connections(s, term.LINK_OUTBOUND)
     local attrs = repo.gr:connections(s, term.LINK_OUTBOUND)
     for p, ots in pairs(attrs) do
     for p, ots in pairs(attrs) do
-        local pname = nsm.denormalize_uri(p.data)
-        p_label = ((mconf.properties or NT)[pname] or NT).label
+        local pname = model.uri_to_id[nsm.denormalize_uri(p.data)] or p.data
+        local pconf = ((mconf.properties or NT)[pname] or NT)
         -- RDF types are shown in in breadcrumbs.
         -- RDF types are shown in in breadcrumbs.
-        if pname == "rdf:type" then goto skip
-        elseif ((mconf.properties or NT)[pname] or NT).type == "rel" then
+        if pname == pkar.RDF_TYPE.data then goto skip
+        elseif pconf.type == "rel" then
             -- Relationship.
             -- Relationship.
-            rel[pname] = {label = p_label, uri = pname}
+            rel[pname] = {
+                label = pconf.label,
+                description = pconf.description,
+                uri = pconf.uri
+            }
             for _, o in pairs(ots) do table.insert(techmd[pname], o.data) end
             for _, o in pairs(ots) do table.insert(techmd[pname], o.data) end
-        elseif pname == "pas:next" then
+        elseif pname == "next" then
             -- Sibling.
             -- Sibling.
             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.
-            techmd[pname] = {label = p_label, uri = pname}
+            techmd[pname] = {
+                label = pconf.label,
+                description = pconf.description,
+                uri = pconf.uri,
+            }
             -- TODO differentiate term types
             -- TODO differentiate term types
             for _, o in pairs(ots) do table.insert(techmd[pname], o.data) end
             for _, o in pairs(ots) do table.insert(techmd[pname], o.data) end
             table.sort(techmd[pname])
             table.sort(techmd[pname])
@@ -225,43 +256,40 @@ local function generate_ores(s, mconf)
     -- Transform and move media assets.
     -- Transform and move media assets.
     local dest_fname, dest_dir, dest  -- Reused for thumbnail.
     local dest_fname, dest_dir, dest  -- Reused for thumbnail.
     logger:info("Transforming resource file.")
     logger:info("Transforming resource file.")
-    local res_path = techmd["pas:path"]
+    local res_path = techmd.archive_path
     if not res_path then error("No file path for File resource!") end
     if not res_path then error("No file path for File resource!") end
-    local txconf = (mconf.transformers or NT).deliverable or {fn = "copy"}
+    local txconf = (mconf.gen or NT).transformers or {}
+
+    local pres_conf = txconf.pres or {fn = "copy"}
     -- Set file name to resource ID + source extension.
     -- Set file name to resource ID + source extension.
     dest_fname = (
     dest_fname = (
             s.data:gsub(pkar.PAR_NS, "") ..
             s.data:gsub(pkar.PAR_NS, "") ..
-            (txconf.ext or plpath.extension(res_path[1])))
-    dest_dir = plpath.join(
+            (pres_conf.ext or path.extension(res_path[1])))
+    dest_dir = path.join(
             M.media_dir, dest_fname:sub(1, 2), dest_fname:sub(3, 4))
             M.media_dir, dest_fname:sub(1, 2), dest_fname:sub(3, 4))
-    dir.makepath(dest_dir)
-    dest = plpath.join(dest_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, "")
-    logger:info("Access file: ", deliverable)
+    dest = path.join(dest_dir, dest_fname)
+    assert(transformers[pres_conf.fn](
+            res_path[1], dest, table.unpack(pres_conf or NT)))
+    local pres = dest:gsub(pkar.config.htmlgen.out_dir, "")
+    logger:info("Presentation file: ", pres)
 
 
     -- Thumbnail.
     -- Thumbnail.
     local tn
     local tn
-    txconf = (mconf.transformers or NT).thumbnail
-    if txconf then
-        if txconf.ext then
-            dest_fname = plpath.splitext(dest_fname) .. txconf.ext
+    if txconf.thumbnail then
+        if txconf.thumbnail.ext then
+            dest_fname = path.splitext(dest_fname) .. txconf.thumbnail.ext
         end
         end
-        dest_dir = plpath.join(
-                M.media_dir, "tn", dest_fname:sub(1, 2), dest_fname:sub(3, 4))
-        dir.makepath(dest_dir)
-        dest = plpath.join(dest_dir, dest_fname)
-        assert(transformers[txconf.fn](
-                res_path[1], dest, table.unpack(txconf or NT)))
-        tn = dest:gsub(M.media_dir, "/media/tn")
+        dest = pkar.gen_pairtree(TN_FS_PATH, dest_fname)
+        assert(transformers[txconf.thumbnail.fn](
+                res_path[1], dest, table.unpack(txconf.thumbnail or NT)))
+        tn = dest:gsub(M.media_dir, TN_WEB_PATH)
         logger:info("Thumbnail: ", tn)
         logger:info("Thumbnail: ", tn)
     end
     end
 
 
     out_html = templates.ores.data({
     out_html = templates.ores.data({
         --webroot = M.webroot,
         --webroot = M.webroot,
         site_title = pkar.config.site.title or pkar.default_title,
         site_title = pkar.config.site.title or pkar.default_title,
-        pname = plpath.basename(techmd["pas:sourcePath"][1]),
+        fname = path.basename(techmd.source_path[1]),
         head_tpl = templates.head.data,
         head_tpl = templates.head.data,
         header_tpl = templates.header.data,
         header_tpl = templates.header.data,
         mconf = mconf,
         mconf = mconf,
@@ -270,7 +298,7 @@ local function generate_ores(s, mconf)
         rel = rel,
         rel = rel,
         ls_next = ls_next,
         ls_next = ls_next,
         breadcrumbs = get_breadcrumbs(mconf),
         breadcrumbs = get_breadcrumbs(mconf),
-        deliverable = deliverable,
+        pres = pres,
         thumbnail = tn,
         thumbnail = tn,
         rdf_href = pkar.gen_pairtree("/res", s.data, ".ttl", true),
         rdf_href = pkar.gen_pairtree("/res", s.data, ".ttl", true),
     })
     })
@@ -287,7 +315,7 @@ end
 M.generate_res_idx = function(s, mconf)
 M.generate_res_idx = function(s, mconf)
     local rrep = {
     local rrep = {
         id = nsm.denormalize_uri(s.data),
         id = nsm.denormalize_uri(s.data),
-        tn = get_tn_url(s):gsub(M.media_dir, "/media/tn"),
+        tn = get_tn_url(s),
         href = pkar.gen_pairtree("/res", s.data, ".html", true),
         href = pkar.gen_pairtree("/res", s.data, ".html", true),
     }
     }
 
 
@@ -296,7 +324,7 @@ M.generate_res_idx = function(s, mconf)
     local function format_value(pname, o)
     local function format_value(pname, o)
         logger:debug("Adding value to " .. pname .. ": " .. ((o or NT).data or "nil"))
         logger:debug("Adding value to " .. pname .. ": " .. ((o or NT).data or "nil"))
         local v
         local v
-        if pname == "rdf:type" or pname == "pas:contentType" then
+        if pname == "type" or pname == "content_type" then
             v = nsm.denormalize_uri(o.data)
             v = nsm.denormalize_uri(o.data)
         else v = o.data
         else v = o.data
         end
         end
@@ -357,16 +385,17 @@ end
 M.generate_resource = function(s)
 M.generate_resource = function(s)
     local res_type
     local res_type
     _, res_type = next(repo.gr:attr(s, pkar.CONTENT_TYPE_P))
     _, res_type = next(repo.gr:attr(s, pkar.CONTENT_TYPE_P))
-    local mconf = model.models[res_type.data]
+    local mconf = model.from_uri(res_type)
 
 
     -- Generate RDF/Turtle doc.
     -- Generate RDF/Turtle doc.
     local res_path = pkar.gen_pairtree(M.res_dir, s.data, ".ttl")
     local res_path = pkar.gen_pairtree(M.res_dir, s.data, ".ttl")
+    dir.makepath(path.dirname(res_path))
     local ofh = assert(io.open(res_path, "w"))
     local ofh = assert(io.open(res_path, "w"))
-    ofh:write(repo.serialze_rsrc(s, "ttl"))
+    ofh:write(repo.serialize_rsrc(s, "ttl"))
     ofh:close()
     ofh:close()
 
 
     -- Generate HTML doc.
     -- Generate HTML doc.
-    if mconf.types["pas:File"] then assert(generate_ores(s, mconf))
+    if mconf.types.file then assert(generate_ores(s, mconf))
     else assert(generate_dres(s, mconf)) end
     else assert(generate_dres(s, mconf)) end
 
 
     -- Generate JSON rep and append to search index.
     -- Generate JSON rep and append to search index.
@@ -418,19 +447,19 @@ M.generate_idx = function()
         term.new_iriref_ns("pas:Artifact"), triple.POS_O
         term.new_iriref_ns("pas:Artifact"), triple.POS_O
     )
     )
     for _, s in pairs(s_ts) do
     for _, s in pairs(s_ts) do
-        local title, created
+        local title, submitted
         _, title = next(repo.gr:attr(s, pkar.DC_TITLE_P))
         _, title = next(repo.gr:attr(s, pkar.DC_TITLE_P))
-        _, created = next(repo.gr:attr(s, pkar.DC_CREATED_P))
+        _, submitted = next(repo.gr:attr(s, pkar.SUBMITTED_P))
 
 
         local obj = {
         local obj = {
             href = pkar.gen_pairtree("/res", s.data, ".html", true),
             href = pkar.gen_pairtree("/res", s.data, ".html", true),
             title = title,
             title = title,
-            created = created.data,
-            tn = get_tn_url(s):gsub(M.media_dir, "/media/tn"),
+            submitted = submitted.data,
+            tn = get_tn_url(s),
         }
         }
         table.insert(obj_idx, obj)
         table.insert(obj_idx, obj)
     end
     end
-    table.sort(obj_idx, function(a, b) return a.created < b.created end)
+    table.sort(obj_idx, function(a, b) return a.submitted < b.submitted end)
 
 
     logger:debug(pp.write(obj_idx))
     logger:debug(pp.write(obj_idx))
     out_html = templates.idx.data({
     out_html = templates.idx.data({
@@ -443,7 +472,7 @@ M.generate_idx = function()
         obj_idx = obj_idx,
         obj_idx = obj_idx,
     })
     })
 
 
-    local idx_path = plpath.join(pkar.config.htmlgen.out_dir, "index.html")
+    local idx_path = path.join(pkar.config.htmlgen.out_dir, "index.html")
     local ofh = assert(io.open(idx_path, "w"))
     local ofh = assert(io.open(idx_path, "w"))
 
 
     logger:debug("Writing info at ", idx_path)
     logger:debug("Writing info at ", idx_path)
@@ -458,14 +487,12 @@ M.generate_site = function()
     -- Reset target folders.
     -- Reset target folders.
     -- TODO for larger sites, a selective update should be implemented by
     -- TODO for larger sites, a selective update should be implemented by
     -- comparing RDF resource timestamps with HTML page timestamps. Post-MVP.
     -- comparing RDF resource timestamps with HTML page timestamps. Post-MVP.
-    if plpath.isdir(M.res_dir) then dir.rmtree(M.res_dir) end
-    dir.makepath(M.res_dir)
+    if path.isdir(pkar.config.htmlgen.out_dir) then
+        dir.rmtree(pkar.config.htmlgen.out_dir) end
     --[[
     --[[
-    if plpath.isdir(M.asset_dir) then dir.rmtree(M.asset_dir) end
+    if path.isdir(M.asset_dir) then dir.rmtree(M.asset_dir) end
     dir.makepath(M.asset_dir)
     dir.makepath(M.asset_dir)
     --]]
     --]]
-    if plpath.isdir(M.media_dir) then dir.rmtree(M.media_dir) end
-    dir.makepath(plpath.join(M.media_dir, "tn"))
 
 
     -- Copy static assets.
     -- Copy static assets.
     dir.clonetree("templates/assets", M.asset_dir, dir.copyfile)
     dir.clonetree("templates/assets", M.asset_dir, dir.copyfile)

+ 57 - 16
src/model.lua

@@ -1,41 +1,81 @@
-local string = string
-local table = table
-local io = io
+local dir = require "pl.dir"
+local path = require "pl.path"
+
+local term = require "volksdata.term"
+local nsm = require "volksdata.namespace"
 
 
 local pkar = require "pocket_archive"
 local pkar = require "pocket_archive"
 
 
+local dbg = require "debugger"
+
 
 
 -- Escape magic characters.
 -- Escape magic characters.
 local PAS_NS_PTN = pkar.escape_ptn(pkar.PAS_NS)
 local PAS_NS_PTN = pkar.escape_ptn(pkar.PAS_NS)
 
 
-local M = {models = {}}
+local M = {
+    -- Parsed typedef configurations.
+    types = {},
+
+    -- Term-to-URI map.
+    id_to_uri = {},
+
+    -- URI-to-term map.
+    uri_to_id = {},
+}
+
+M.from_uri = function(type_uri)
+    return M.types[M.uri_to_id[nsm.denormalize_uri(type_uri.data)]]
+end
+
 
 
 -- Parameters that do not get inherited.
 -- Parameters that do not get inherited.
 local NO_INHERIT = {abstract = true}
 local NO_INHERIT = {abstract = true}
-local MODEL_PATH = "./config/model/typedef/"
+local MODEL_PATH = path.join(pkar.config_path, "model")
+
+local gen_config = dofile(path.join(MODEL_PATH, "generation.lua"))
 
 
 
 
+--[[
 local function camel2snake(src)
 local function camel2snake(src)
     return src
     return src
         :gsub("^pas:", "")  -- Strip namespace.
         :gsub("^pas:", "")  -- Strip namespace.
         :gsub("([^^])(%u)", "%1_%2")  -- Uppercase (except initial) to _.
         :gsub("([^^])(%u)", "%1_%2")  -- Uppercase (except initial) to _.
         :lower()
         :lower()
 end
 end
+--]]
 
 
 
 
-M.parse_model = function(mod_id)
+local function add_term(id, uri_str)
+    --if not uri then error(("Term %s has not a URI!"):format(term), 2) end
+    if not uri_str then return end
+    local uri = term.new_iriref_ns(uri_str)
+    if not M.id_to_uri[id] then M.id_to_uri[id] = uri end
+    if not M.uri_to_id[uri_str] then M.uri_to_id[uri_str] = id end
+end
+
+
+local function parse_model(mod_id)
     mod_id = mod_id:gsub(PAS_NS_PTN, ""):gsub("par:", "")
     mod_id = mod_id:gsub(PAS_NS_PTN, ""):gsub("par:", "")
     local hierarchy = {}
     local hierarchy = {}
 
 
     local function traverse(mod_id)
     local function traverse(mod_id)
         print("traversing:", mod_id)
         print("traversing:", mod_id)
-        local fname = camel2snake (mod_id)
-        local model = dofile(MODEL_PATH .. fname .. ".lua")
+        local model = dofile(path.join(
+                MODEL_PATH, "typedef", mod_id .. ".lua"))
+        -- Merge separate generator config
+        model.gen = gen_config[mod_id]
         --model.id = mod_id
         --model.id = mod_id
         --print("Model: ")
         --print("Model: ")
         --for k, v in pairs(model) do print (k, v) end
         --for k, v in pairs(model) do print (k, v) end
 
 
         -- Prepend to hierarchy.
         -- Prepend to hierarchy.
+        model.id = mod_id
+
+        -- Store term-to-URI and URI-to-term mappings.
+        add_term(model.id, model.uri)
+        for prop, pdata in pairs(model.properties or {}) do
+            add_term(prop, pdata.uri) end
+
         table.insert(hierarchy, 1, model)
         table.insert(hierarchy, 1, model)
 
 
         if model.broader then traverse(model.broader) end
         if model.broader then traverse(model.broader) end
@@ -72,14 +112,15 @@ M.parse_model = function(mod_id)
 end
 end
 
 
 
 
--- Create a cache for model configurations.
-local models_mt = {
-    ["__index"] = function (t, model)
-        t[model] = M.parse_model(model)
-        return t[model]
-    end
-}
-setmetatable(M.models, models_mt)
+-- Collect all type names from config file names.
+for _, fpath in ipairs(dir.getfiles(
+            path.join(MODEL_PATH, "typedef"), "*.lua")) do
+    local mname = path.basename(fpath):gsub(".lua$", "")
+    local typedef = parse_model(mname)
+
+    -- Store parsed typedef configurations.
+    M.types[mname] = typedef
+end
 
 
 
 
 return M
 return M

+ 45 - 91
src/submission.lua

@@ -25,56 +25,9 @@ local NT = {}
 
 
 local M = {}  -- Submission module
 local M = {}  -- Submission module
 
 
--- Adapted from lua-núcleo
-local function escape_pattern(s)
-    local matches = {
-        ["^"] = "%^";
-        ["$"] = "%$";
-        ["("] = "%(";
-        [")"] = "%)";
-        ["%"] = "%%";
-        ["."] = "%.";
-        ["["] = "%[";
-        ["]"] = "%]";
-        ["*"] = "%*";
-        ["+"] = "%+";
-        ["-"] = "%-";
-        ["?"] = "%?";
-        ["\0"] = "%z";
-    }
-
-    return (s:gsub(".", matches))
-end
-
-
---[[
-    Only generate a thumbnail for pas:File types.
-
-    Non-file resources may be assigned a thumbnail from a contained file
-    or from a stock type icon in the metadata population phase.
---]]
---[=[
-local function generate_thumbnail(rsrc, sip_root, tn_dir)
-    local mconf = model.models[rsrc["pas:contentType"]]
-    if not mconf.types["pas:File"] then return end
-
-    local txconf = (mconf.transformers or NT).thumbnail or {fn = "type_icon"}
-    local src = plpath.join(sip_root, rsrc["pas:sourcePath"])
-    local dest_fname = rsrc.id:gsub("^par:", "")
-    local ext = txconf.ext or plpath.extension(src)
-    local dest = plpath.join(tn_dir, dest_fname .. ext)
-    assert(transformers[txconf.fn](
-            src, dest, table.unpack(txconf or NT)))
-    local deliverable = dest:gsub(pkar.config.htmlgen.out_dir, "..")
-    logger:debug("thumbnail: ", dest)
-
-    return dest
-end
---]=]
-
 
 
 -- Initialize libmagic database.
 -- Initialize libmagic database.
-local magic = libmagic.open( libmagic.MIME_TYPE, libmagic.NO_CHECK_COMPRESS )
+local magic = libmagic.open(libmagic.MIME_TYPE, libmagic.NO_CHECK_COMPRESS )
 assert(magic:load())
 assert(magic:load())
 
 
 
 
@@ -88,7 +41,7 @@ for i = 97, 122 do table.insert(chpool, i) end  -- a-z
 Generate a random, reader-friendly ID.
 Generate a random, reader-friendly ID.
 
 
 A 16-character ID with the above defined #chpool of 60 smybols has an entropy
 A 16-character ID with the above defined #chpool of 60 smybols has an entropy
-of 94.5 bits, which should be plenty for a small repository.
+of 94.5 bits, which should be plenty for a medium-sized repository.
 ]]
 ]]
 M.idgen = function(len)
 M.idgen = function(len)
     local charlist = {}
     local charlist = {}
@@ -110,22 +63,16 @@ M.generate_sip = function(path)
 
 
     local i = 0
     local i = 0
     for row_n, row in csv.parseLine(path) do
     for row_n, row in csv.parseLine(path) do
-        logger:debug("Row path: ", row["pas:sourcePath"])
+        logger:debug("Row path: ", row.source_path)
         logger:debug("Parsing row:", pp.write(row))
         logger:debug("Parsing row:", pp.write(row))
-        if #row["pas:sourcePath"] > 0 then
+        if #row.source_path > 0 then
             i = i + 1
             i = i + 1
             logger:info(
             logger:info(
                     ("Processing LL resource #%d at row #%d.")
                     ("Processing LL resource #%d at row #%d.")
                     :format(i, row_n))
                     :format(i, row_n))
-            prev_path = row["pas:sourcePath"]
+            prev_path = row.source_path
             -- New row.
             -- New row.
-            local id
-            if #row.id > 0 then
-                id = "par:" .. row.id
-                row.id = nil
-            else id = "par:" .. M.idgen() end
-
-            sip[i] = {id = id}
+            sip[i] = {id = "par:" .. M.idgen()}
             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
@@ -136,10 +83,10 @@ M.generate_sip = function(path)
             --[[
             --[[
             -- Generate thumbnail for files.
             -- Generate thumbnail for files.
             local rsrc_path = plpath.join(
             local rsrc_path = plpath.join(
-                    sip.root_path, sip[i]["pas:sourcePath"])
+                    sip.root_path, sip[i].source_path)
             if plpath.isfile(rsrc_path) then
             if plpath.isfile(rsrc_path) then
                 --require "debugger"()
                 --require "debugger"()
-                sip[i]["pas:thumbnail"] = generate_thumbnail(
+                sip[i].thumbnail = generate_thumbnail(
                         sip[i], sip.root_path, tn_dir)
                         sip[i], sip.root_path, tn_dir)
             end
             end
             --]]
             --]]
@@ -164,30 +111,31 @@ M.generate_sip = function(path)
                     end
                     end
                     ::cont2::
                     ::cont2::
                 end
                 end
-                row["pas:sourcePath"] = prev_path
+                row.source_path = prev_path
             end
             end
         end
         end
         row_n = row_n + 1
         row_n = row_n + 1
     end
     end
     -- Infer structure from paths and row ordering.
     -- Infer structure from paths and row ordering.
     for i, v in ipairs(sip) do
     for i, v in ipairs(sip) do
-        local rmod = model.parse_model(v["pas:contentType"])
-        if rmod.properties["pas:next"] then
+        local rmod = model.types[v.content_type]
+        --dbg.assert(rmod)
+        if rmod.properties.next then
             for j = i + 1, #sip do
             for j = i + 1, #sip do
-                if not v["pas:next"] and
-                        sip[j]["pas:sourcePath"]:match("(.*/)") ==
-                                v["pas:sourcePath"]:match("(.*/)") then
-                    v["pas:next"] = sip[j].id
+                if not v.next and
+                        sip[j].source_path:match("(.*/)") ==
+                                v.source_path:match("(.*/)") then
+                    v.next = sip[j].id
                 end
                 end
             end
             end
         end
         end
-        if rmod.properties["pas:first"] then
+        if rmod.properties.first then
             for j = i + 1, #sip do
             for j = i + 1, #sip do
-                if not v["pas:first"] and
-                    sip[j]["pas:sourcePath"]:match(
-                            "^" .. escape_pattern(v["pas:sourcePath"])
+                if not v.first and
+                    sip[j].source_path:match(
+                            "^" .. pkar.escape_pattern(v.source_path)
                 ) then
                 ) then
-                    v["pas:first"] = sip[j].id
+                    v.first = sip[j].id
                 end
                 end
             end
             end
         end
         end
@@ -202,7 +150,7 @@ end
 
 
 --]]
 --]]
 M.rsrc_to_graph = function(rsrc)
 M.rsrc_to_graph = function(rsrc)
-    local rmod = model.parse_model(rsrc["pas:contentType"])
+    local rmod = model.types[rsrc.content_type]
     logger:info("Updating resource md: ", pp.write(rsrc))
     logger:info("Updating resource md: ", pp.write(rsrc))
 
 
     local s = term.new_iriref_ns(rsrc.id)
     local s = term.new_iriref_ns(rsrc.id)
@@ -210,12 +158,15 @@ M.rsrc_to_graph = function(rsrc)
 
 
     it = gr:add_init()
     it = gr:add_init()
     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
         if k == "id" then goto skip end
-
         logger:debug(("Adding attribute: %s = %s"):format(k, pp.write(v)))
         logger:debug(("Adding attribute: %s = %s"):format(k, pp.write(v)))
-        local p = term.new_iriref_ns(k)
-        local o
+        local p = model.id_to_uri[k]
+        if not p then
+            logger:warn(
+                ("Term %s has no URI mapped. Assigning `pas:%s`.")
+                :format(k, k))
+            p = term.new_iriref_ns("pas:" .. k)
+        end
         local datatype = ((rmod.properties or NT)[k] or NT).type
         local datatype = ((rmod.properties or NT)[k] or NT).type
         local rdf_type_str = pkar.config.md.datatypes[datatype]
         local rdf_type_str = pkar.config.md.datatypes[datatype]
         local rdf_type
         local rdf_type
@@ -224,9 +175,10 @@ M.rsrc_to_graph = function(rsrc)
         end
         end
         -- Force all fields to be multi-valued.
         -- Force all fields to be multi-valued.
         if type(v) ~= "table" then v = {[v] = true} end
         if type(v) ~= "table" then v = {[v] = true} end
+        local o
         for vv in pairs(v) do
         for vv in pairs(v) do
-            if k == "pas:contentType" then
-                vv = "pas:" .. vv
+            if k == "content_type" then
+                vv = rmod.uri
             end
             end
             if datatype == "resource" then
             if datatype == "resource" then
                 o = term.new_iriref_ns(vv)
                 o = term.new_iriref_ns(vv)
@@ -238,7 +190,9 @@ M.rsrc_to_graph = function(rsrc)
         ::skip::
         ::skip::
     end
     end
     for i, m in ipairs(rmod.lineage) do
     for i, m in ipairs(rmod.lineage) do
-        it:add_iter(triple.new(s, pkar.RDF_TYPE, term.new_iriref_ns(m)))
+        it:add_iter(triple.new(
+            s, pkar.RDF_TYPE,
+            term.new_iriref_ns(model.types[m].uri)))
     end
     end
     it:add_done()
     it:add_done()
 
 
@@ -252,7 +206,7 @@ M.deposit = function(sip)
         logger:debug(("Processing resource #%d of %d: %s"):format(
         logger:debug(("Processing resource #%d of %d: %s"):format(
                 i, #sip, rsrc.id))
                 i, #sip, rsrc.id))
 
 
-        local in_path = sip.root_path .. rsrc["pas:sourcePath"]
+        local in_path = sip.root_path .. rsrc.source_path
         local fext = plpath.extension(in_path)
         local fext = plpath.extension(in_path)
         -- If it's a directory, skip file processing.
         -- If it's a directory, skip file processing.
         if not plpath.isfile(in_path) then goto continue end
         if not plpath.isfile(in_path) then goto continue end
@@ -266,7 +220,7 @@ M.deposit = function(sip)
 
 
             local ifh = assert(io.open(in_path, "r"))
             local ifh = assert(io.open(in_path, "r"))
 
 
-            rsrc["dc:format"] = {[magic:filehandle(ifh)] = true}
+            rsrc.format = {[magic:filehandle(ifh)] = true}
             local hash_it = mc.new_blake2b()
             local hash_it = mc.new_blake2b()
             local fsize = 0
             local fsize = 0
             logger:debug("Hashing ", in_path)
             logger:debug("Hashing ", in_path)
@@ -279,9 +233,9 @@ M.deposit = function(sip)
                 fsize = fsize + #chunk
                 fsize = fsize + #chunk
             end
             end
             local checksum = hash_it:final(true)
             local checksum = hash_it:final(true)
-            rsrc["premis:hasMessageDigest"] = {
+            rsrc.checksum = {
                     ["urn:blake2:" .. checksum] = true}
                     ["urn:blake2:" .. checksum] = true}
-            rsrc["dc:extent"] = fsize
+            rsrc.size = fsize
 
 
             ofh:close()
             ofh:close()
             ifh:close()
             ifh:close()
@@ -296,24 +250,24 @@ M.deposit = function(sip)
             dir.makepath(out_dir)
             dir.makepath(out_dir)
             logger:debug(("Moving file %s to %s"):format(tmp_path, out_path))
             logger:debug(("Moving file %s to %s"):format(tmp_path, out_path))
             dir.movefile(tmp_path, out_path)
             dir.movefile(tmp_path, out_path)
-            rsrc["pas:path"] = out_path
+            rsrc.archive_path = out_path
 
 
             -- Copy thumbnail if existing.
             -- Copy thumbnail if existing.
-            if rsrc["pas:thumbnail"] then
-                src_path = rsrc["pas:thumbnail"]
+            if rsrc.thumbnail then
+                src_path = rsrc.thumbnail
                 out_path = plpath.join(
                 out_path = plpath.join(
                         out_dir, plpath.basename(src_path))
                         out_dir, plpath.basename(src_path))
                 logger:debug(("Moving file %s to %s"):format(src_path, out_path))
                 logger:debug(("Moving file %s to %s"):format(src_path, out_path))
                 dir.movefile(src_path, out_path)
                 dir.movefile(src_path, out_path)
-                rsrc["pas:thumbnail"] = out_path
+                rsrc.thumbnail = out_path
             end
             end
         end
         end
 
 
         ::continue::
         ::continue::
 
 
         tstamp = os.date("!%Y-%m-%dT%TZ")
         tstamp = os.date("!%Y-%m-%dT%TZ")
-        rsrc["dc:created"] = tstamp
-        rsrc["dc:modified"] = tstamp
+        rsrc.submitted = tstamp
+        rsrc.last_modified = tstamp
         repo.store_updates(M.rsrc_to_graph(rsrc))
         repo.store_updates(M.rsrc_to_graph(rsrc))
     end
     end
 
 

+ 5 - 10
src/transformers.lua

@@ -1,3 +1,5 @@
+local dir = require "pl.dir"
+local path = require "pl.path"
 local vips = require "vips"
 local vips = require "vips"
 
 
 local pkar = require "pocket_archive"
 local pkar = require "pocket_archive"
@@ -11,6 +13,7 @@ M = {}
 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.
     -- TODO Make streaming if possible.
+    dir.makepath(path.dirname(dest))
     local img = vips.Image.thumbnail(src, size)
     local img = vips.Image.thumbnail(src, size)
 
 
     img:write_to_file(dest)
     img:write_to_file(dest)
@@ -28,16 +31,8 @@ end
 
 
 -- Straight copy with no transformation.
 -- Straight copy with no transformation.
 M.copy = function(src, dest)
 M.copy = function(src, dest)
-    local ifh = assert(io.open(src, "r"))
-    local ofh = assert(io.open(dest, "w"))
-
-    while true do
-        chunk = ifh:read(pkar.config.fs.stream_chunk_size)
-        if not chunk then break end
-        ofh:write(chunk)
-    end
-    ifh:close()
-    ofh:close()
+    dir.makepath(path.dirname(dest))
+    dir.copyfile(src, dest)
 
 
     return true
     return true
 end
 end

+ 4 - 3
src/validator.lua

@@ -17,8 +17,9 @@ local M = {}
 
 
 M.validate = function(gr, s)
 M.validate = function(gr, s)
     _, ctype = next(gr:attr(s, pkar.CONTENT_TYPE_P))
     _, ctype = next(gr:attr(s, pkar.CONTENT_TYPE_P))
-    local rmod = model.parse_model(ctype.data)
-    if not rmod then error("No type definition for ", ctype.data) end
+    local rmod = model.from_type_uri(ctype)
+    dbg.assert(rmod)
+    if not rmod then error("No type definition for " .. ctype.data) end
 
 
     local report = {
     local report = {
         id = s.data, ctype = ctype.data,
         id = s.data, ctype = ctype.data,
@@ -27,7 +28,7 @@ M.validate = function(gr, s)
 
 
     for fname, rules in pairs(rmod.properties or NT) do
     for fname, rules in pairs(rmod.properties or NT) do
         local values
         local values
-        values = gr:attr(s, term.new_iriref_ns(fname))
+        values = gr:attr(s, model.id_to_uri[fname])
 
 
         -- Cardinality
         -- Cardinality
         local card = 0
         local card = 0

+ 9 - 0
templates/assets/css/pkar.css

@@ -9,3 +9,12 @@
 }
 }
 
 
 .hidden {display: none;}
 .hidden {display: none;}
+
+.breadcrumb {
+    font-style: italic;
+}
+
+.breadcrumb.current {
+    font-style: normal;
+    font-weight: bold;
+}

+ 5 - 1
templates/dres.html

@@ -22,15 +22,19 @@
                     </a>
                     </a>
                 <% end %>
                 <% end %>
                 </p>
                 </p>
-                <a href="<%= rdf_href %>">RDF document</a>
             </section>
             </section>
             <section id="res_dmd">
             <section id="res_dmd">
                 <h2>Metadata</h2>
                 <h2>Metadata</h2>
+                <p><a href="<%= rdf_href %>">Download RDF document</a></p>
                 <dl class="res_md">
                 <dl class="res_md">
                 <% for _, ol in ipairs(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 %>
+                        <% if ol.description then %><span
+                            title="<%= ol.description -%>"
+                            >&nbsp;&#x1F6C8;</span>
+                        <% end %>
                     </dt>
                     </dt>
                     <% for _, o in ipairs(ol) do %>
                     <% for _, o in ipairs(ol) do %>
                       <dd><%= o %></dd>
                       <dd><%= o %></dd>

+ 9 - 5
templates/ores.html

@@ -22,15 +22,19 @@
                     </a>
                     </a>
                 <% end %>
                 <% end %>
                 </p>
                 </p>
-                <a href="<%= rdf_href %>">RDF document</a>
             </section>
             </section>
             <section class="res_md" id="res_techmd">
             <section class="res_md" id="res_techmd">
                 <h2>Metadata</h2>
                 <h2>Metadata</h2>
+                <p><a href="<%= rdf_href %>">Download RDF document</a></p>
                 <dl>
                 <dl>
                 <% for p, ol in pairs(techmd) do %>
                 <% for p, ol in pairs(techmd) 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 %>
+                        <% if ol.description then %><span
+                            title="<%= ol.description -%>"
+                            >&nbsp;&#x1F6C8;</span>
+                        <% end %>
                     </dt>
                     </dt>
                     <% for _, o in ipairs(ol) do %>
                     <% for _, o in ipairs(ol) do %>
                       <dd><%= o %></dd>
                       <dd><%= o %></dd>
@@ -54,12 +58,12 @@
                 </dl>
                 </dl>
             </section>
             </section>
             <% end %>
             <% end %>
-            <% if deliverable then %>
+            <% if pres then %>
             <section id="res_pres">
             <section id="res_pres">
-            <% if mconf.presentation_type == "image" then %>
-                <img src="<%= deliverable -%>" />
+            <% if mconf.gen.presentation_type == "image" then %>
+                <img src="<%= pres -%>" />
             <%else %>
             <%else %>
-                <a href="<%= deliverable -%>" download="<%= fname -%>">
+                <a href="<%= pres -%>" download="<%= fname -%>">
                     Download file
                     Download file
                 </a>
                 </a>
             <% end %>
             <% end %>

+ 9 - 9
test/sample_submission/demo01/pkar_submission.csv

@@ -1,15 +1,15 @@
-"pas:sourcePath","id","pas:contentType","dc:title","dc:alternative","dc:description","dc:coverage","dc:date"
-"demo_collection",,"Collection","My Demo Collection","My Beautiful  Collection","Some random stuff from my hard drive.",,2025-07-28
+"source_path","ext_id","content_type","label","alt_label","description","location","date"
+"demo_collection",,"collection","My Demo Collection","My Beautiful  Collection","Some random stuff from my hard drive.",,2025-07-28
 ,,,,"My Aunt's Beautiful Collection","Old B/W photos.",,
 ,,,,"My Aunt's Beautiful Collection","Old B/W photos.",,
 ,,,,,"More description to demonstrate how multi-valued fields are filled.",,
 ,,,,,"More description to demonstrate how multi-valued fields are filled.",,
 ,,,,,"""id"" fields have been left blank to let the system auto-generate them.",,
 ,,,,,"""id"" fields have been left blank to let the system auto-generate them.",,
-"demo_collection/demo_postcard",,"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.",,2025-06-10
-"demo_collection/demo_postcard/front",,"Part","Recto",,"An idle, windy day in Capo Falcone, Sardinia","Capo Falcone (SS) Italy",2004-04-12
+"demo_collection/demo_postcard",,"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.",,2025-06-10
+"demo_collection/demo_postcard/front",,"part","Recto",,"An idle, windy day in Capo Falcone, Sardinia","Capo Falcone (SS) Italy",2004-04-12
 ,,,,,,"https://www.openstreetmap.org/#map=18/40.9696884/8.2020324",
 ,,,,,,"https://www.openstreetmap.org/#map=18/40.9696884/8.2020324",
-"demo_collection/demo_postcard/front/54321.jpg",,"StillImageFile",,,,,
-"demo_collection/demo_postcard/back",,"Part","Verso",,"Wandering around somewhere in Tirana, 2006.","Tirana, Albania",2006-05-05
+"demo_collection/demo_postcard/front/54321.jpg",,"still_image_file",,,,,
+"demo_collection/demo_postcard/back",,"part","Verso",,"Wandering around somewhere in Tirana, 2006.","Tirana, Albania",2006-05-05
 ,,,,,,"https://geohack.toolforge.org/geohack.php?params=41.32888888888889_N_19.817777777777778_E_globe:earth&language=en",
 ,,,,,,"https://geohack.toolforge.org/geohack.php?params=41.32888888888889_N_19.817777777777778_E_globe:earth&language=en",
-"demo_collection/demo_postcard/back/567890.jpg",,"StillImageFile",,,,,
-"demo_collection/single_image",,"StillImage","Preparing kebab at Aqil's during curfew.",,"Nothing much to do under curfew but cooking, eating, singing, dancing, playing cards, smoking water pipe, and occasionally playing soccer in the street when the Merkava didn't get in the way.","Nablus, Palestine",2002-08-16
+"demo_collection/demo_postcard/back/567890.jpg",,"still_image_file",,,,,
+"demo_collection/single_image",,"still_image","Preparing kebab at Aqil's during curfew.",,"Nothing much to do under curfew but cooking, eating, singing, dancing, playing cards, smoking water pipe, and occasionally playing soccer in the street when the Merkava didn't get in the way.","Nablus, Palestine",2002-08-16
 ,,,,,,"https://www.openstreetmap.org/#map=19/32.221597/35.260929",
 ,,,,,,"https://www.openstreetmap.org/#map=19/32.221597/35.260929",
-"demo_collection/single_image/0685_04.jpg",,"StillImageFile",,,,,
+"demo_collection/single_image/0685_04.jpg",,"still_image_file",,,,,