Pārlūkot izejas kodu

Use own IDs for attribute & type names.

scossu 2 nedēļas atpakaļ
vecāks
revīzija
bf74a0c770

+ 1 - 0
README.md

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

+ 4 - 5
config/app.lua

@@ -14,11 +14,10 @@ return {
     md = {
         -- Single-valued fields. TODO rely on content model cardinality.
         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.

+ 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 {
   label = "Agent",
-  broader = "pas:Anything"
+  uri = "pas:Agent",
+  broader = "anything"
 }

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

@@ -1,71 +1,88 @@
 return {
-    id = "pas:Anything",
+    uri = "pas:Anything",
     label = "Anything",
     description = "Superclass of every resource type in the system.",
     abstract = "true",
 
     properties = {
-        ["pas:sourcePath"] = {
+        source_path = {
+            uri = "pas:sourcePath",
             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",
             min_cardinality = 1,
             max_cardinality = 1,
         },
-        ["pas:contentType"] = {
+        content_type = {
+            uri = "pas:contentType",
             label = "Content type",
             type = "resource",
             min_cardinality = 1,
             max_cardinality = 1,
         },
         --[[
-        ["pas:id"] = {
+        id = {
+            uri = "pas:id",
             label = "Primary ID",
             type = "string",
             min_cardinality = 1,
             max_cardinality = 1,
         },
         --]]
-        ["dc:identifier"] = {
+        ext_id = {
+            uri = "dc:identifier",
             label = "External system ID",
             type = "string",
         },
-        ["dc:title"] = {
+        label = {
+            uri = "dc:title",
             label = "Title",
             type = "string",
             max_cardinality = 1,
         },
-        ["dc:alternative"] = {
+        alt_label = {
+            uri = "dc:alternative",
             label = "Alternative Label",
             type = "string",
         },
-        ["dc:description"] = {
+        description = {
+            uri = "dc:description",
             label = "Description",
             type = "string",
         },
-        ["dc:created"] = {
-            label = "Created On",
+        submitted = {
+            uri = "dc:dateSubmitted",
+            label = "Submitted On",
             type = "datetime",
             min_cardinality = 1,
             max_cardinality = 1,
         },
-        ["dc:modified"] = {
+        last_modified = {
+            uri = "dc:modified",
             label = "Last Updated On",
             type = "datetime",
             min_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",
             type = "rel",
-            range = {["pas:Agent"] = true},
+            range = {agent = true},
         },
-        ["dc:contributor"] = {
+        last_modified_by = {
+            uri = "dc:contributor",
             label = "Last Updated By",
             type = "rel",
-            range = {["pas:Agent"] = true},
+            range = {agent = true},
         },
     },
 }

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

@@ -1,19 +1,37 @@
 return {
-    id = "pas:Artifact",
+    uri = "pas:Artifact",
     label = "Artifact",
     description = "Intellectual work.",
-    broader = "pas:Anything",
+    broader = "anything",
 
     properties = {
-        ["pas:first"] = {
+        first = {
+            uri = "pas:first",
             label = "First child",
             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",
-            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 {
-    id = "pas:Brick",
+    uri = "pas:Brick",
     label = "Brick",
 
-    broader = "pas:Anything",
+    broader = "anything",
 
     properties = {
-        ["pas:first"] = {
+        first = {
+            uri = "pas:first",
             label = "First child",
             type = "resource",
         },
-        ["pas:next"] = {
+        next = {
+            uri = "pas:next",
             label = "Next sibling",
             type = "resource",
             max_cardinality = 1,

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

@@ -1,15 +1,15 @@
 return {
-    id = "pas:Collection",
+    uri = "pas:Collection",
     label = "Collection",
 
-    broader = "pas:Brick",
+    broader = "brick",
 
     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 {
-    id = "pas:Document",
+    uri = "pas:Document",
     label = "Document",
-    broader = "pas:Artifact",
+    broader = "artifact",
 }
 

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

@@ -1,51 +1,47 @@
 return {
-    id = "pas:File",
+    uri = "pas:File",
     label = "File",
-    broader = "pas:Anything",
+    broader = "anything",
 
     properties = {
-        --[[
-        ["pas:location"] = {
-            label = "Location",
-            type = "string",
-            min_cardinality = 1,
-            max_cardinality = 1,
-        },
-        --]]
-        ["pas:path"] = {
+        archive_path = {
+            uri = "pas:archivePath",
             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",
             min_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,
         },
-        ["pas:thumbnail"] = {
+        thumbnail = {
+            uri = "pas:thumbnail",
             label = "Thumbnail",
             type = "string",
             --min_cardinality = 1,
             max_cardinality = 1,
         },
-        ["dc:format"] = {
+        format = {
+            uri = "dc:format",
             label = "MIME type",
             type = "string",
             min_cardinality = 1,
             max_cardinality = 1,
         },
-        ["dc:extent"] = {
+        size = {
+            uri = "dc:extent",
             label = "File size",
             description = "File size in bytes.",
             type = "integer",
             min_cardinality = 1,
             max_cardinality = 1,
         },
-        ["premis:hasMessageDigest"] = {
+        checksum = {
+            uri = "premis:hasMessageDigest",
             label = "Checksum",
             description = [[
                 File checksum formatted as an URN:

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

@@ -1,15 +1,15 @@
 return {
-    id = "pas:Part",
+    uri = "pas:Part",
     label = "Part",
 
-    broader = "pas:Brick",
+    broader = "brick",
 
     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 {
-    id = "pas:Postcard",
+    uri = "pas:Postcard",
     label = "Postcard",
-    broader = "pas:Document",
+    broader = "document",
     properties = {
-        ["pas:recto"] = {
+        recto = {
+            uri = "pas:recto",
             label = "Recto",
             type = "resource",
-            range = {["pas:Part"] = true},
+            range = {part = true},
         },
-        ["pas:verso"] = {
+        verso = {
+            uri = "pas:verso",
             label = "Verso",
             type = "resource",
-            range = {["pas:Part"] = true},
+            range = {part = true},
         },
     }
 }

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

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

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

@@ -1,15 +1,5 @@
 return {
-    id = "pas:StillImageFile",
+    uri = "pas:StillImageFile",
     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 root_path = path.dirname(path.dirname(fpath))
 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 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 = {
     -- Project root path.
     root = root_path,
+    config_path = config_path,
     config = config,
 
     default_title = "Pocket Archive",
@@ -49,7 +50,7 @@ local M = {
     RDF_TYPE = term.new_iriref_ns("rdf:type"),
 
     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"),
     FIRST_P = term.new_iriref_ns("pas:first"),
     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.
 
@@ -130,6 +153,9 @@ setmetatable (M, mt)
 
 
 -- 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])
 
 

+ 95 - 68
src/generator.lua

@@ -3,7 +3,7 @@ local datafile = require "datafile"
 local dir = require "pl.dir"
 local etlua = require "etlua"
 local json = require "cjson"
-local plpath = require "pl.path"
+local path = require "pl.path"
 local pp = require "pl.pretty"
 
 local nsm = require "volksdata.namespace"
@@ -27,9 +27,9 @@ local NT = {}
 local subjects
 
 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.
 local idx_keys = {}
 
@@ -50,12 +50,15 @@ end
 
 -- HTML generator module.
 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,
-    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.
 }
 
+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)
     -- Breadcrumbs, from top class to current class.
@@ -64,7 +67,7 @@ local function get_breadcrumbs(mconf)
     for i = 1, #mconf.lineage do
         breadcrumbs[i] = {
             mconf.lineage[i],
-            model.models[mconf.lineage[i]].label
+            model.types[mconf.lineage[i]].label
         }
     end
 
@@ -72,19 +75,31 @@ local function get_breadcrumbs(mconf)
 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
         -- 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
 
     -- Recurse through all first children until one with a thumbnail, or a
     -- leaf without children, is found.
     local first_child
     _, 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
 
 
@@ -96,15 +111,20 @@ local function generate_dres(s, mconf)
     -- Metadata
     local attrs = repo.gr:connections(s, term.LINK_OUTBOUND)
     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.
-        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
             -- 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
-        elseif pname == "pas:first" then
+        elseif pname == "first" then
             -- Build a linked list for every first found.
             for _, o in pairs(ots) do
                 -- Loop through all first children.
@@ -118,18 +138,17 @@ local function generate_dres(s, mconf)
                 if label then label = label.data
                 else
                     _, 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
                 end
 
                 while child_s do
                     -- Loop trough all next nodes for each first child.
-                    --require "debugger".assert(get_tn_url(child_s))
                     table.insert(ll, {
                         href = pkar.gen_pairtree(
                                 "/res", child_s.data, ".html", true),
                         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")
                     -- There can only be one "next"
@@ -137,12 +156,16 @@ local function generate_dres(s, mconf)
                 end
                 table.insert(children, ll)
             end
-        elseif pname == "pas:next" then
+        elseif pname == "next" then
             -- Sibling.
             for _, o in pairs(ots) do ls_next = o.data break end
         else
             -- 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
             for _, o in pairs(ots) do table.insert(attr, o.data) end
             table.sort(attr)
@@ -195,20 +218,28 @@ local function generate_ores(s, mconf)
     -- Metadata
     local attrs = repo.gr:connections(s, term.LINK_OUTBOUND)
     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.
-        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.
-            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
-        elseif pname == "pas:next" then
+        elseif pname == "next" then
             -- Sibling.
             for _, o in pairs(ots) do ls_next = o.data break end
         else
             -- Descriptive metadata.
-            techmd[pname] = {label = p_label, uri = pname}
+            techmd[pname] = {
+                label = pconf.label,
+                description = pconf.description,
+                uri = pconf.uri,
+            }
             -- TODO differentiate term types
             for _, o in pairs(ots) do table.insert(techmd[pname], o.data) end
             table.sort(techmd[pname])
@@ -225,43 +256,40 @@ local function generate_ores(s, mconf)
     -- Transform and move media assets.
     local dest_fname, dest_dir, dest  -- Reused for thumbnail.
     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
-    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.
     dest_fname = (
             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))
-    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.
     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
-        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)
     end
 
     out_html = templates.ores.data({
         --webroot = M.webroot,
         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,
         header_tpl = templates.header.data,
         mconf = mconf,
@@ -270,7 +298,7 @@ local function generate_ores(s, mconf)
         rel = rel,
         ls_next = ls_next,
         breadcrumbs = get_breadcrumbs(mconf),
-        deliverable = deliverable,
+        pres = pres,
         thumbnail = tn,
         rdf_href = pkar.gen_pairtree("/res", s.data, ".ttl", true),
     })
@@ -287,7 +315,7 @@ end
 M.generate_res_idx = function(s, mconf)
     local rrep = {
         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),
     }
 
@@ -296,7 +324,7 @@ M.generate_res_idx = function(s, mconf)
     local function format_value(pname, o)
         logger:debug("Adding value to " .. pname .. ": " .. ((o or NT).data or "nil"))
         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)
         else v = o.data
         end
@@ -357,16 +385,17 @@ end
 M.generate_resource = function(s)
     local res_type
     _, 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.
     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"))
-    ofh:write(repo.serialze_rsrc(s, "ttl"))
+    ofh:write(repo.serialize_rsrc(s, "ttl"))
     ofh:close()
 
     -- 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
 
     -- 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
     )
     for _, s in pairs(s_ts) do
-        local title, created
+        local title, submitted
         _, 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 = {
             href = pkar.gen_pairtree("/res", s.data, ".html", true),
             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)
     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))
     out_html = templates.idx.data({
@@ -443,7 +472,7 @@ M.generate_idx = function()
         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"))
 
     logger:debug("Writing info at ", idx_path)
@@ -458,14 +487,12 @@ M.generate_site = function()
     -- Reset target folders.
     -- TODO for larger sites, a selective update should be implemented by
     -- 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)
     --]]
-    if plpath.isdir(M.media_dir) then dir.rmtree(M.media_dir) end
-    dir.makepath(plpath.join(M.media_dir, "tn"))
 
     -- Copy static assets.
     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 dbg = require "debugger"
+
 
 -- Escape magic characters.
 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.
 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)
     return src
         :gsub("^pas:", "")  -- Strip namespace.
         :gsub("([^^])(%u)", "%1_%2")  -- Uppercase (except initial) to _.
         :lower()
 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:", "")
     local hierarchy = {}
 
     local function traverse(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
         --print("Model: ")
         --for k, v in pairs(model) do print (k, v) end
 
         -- 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)
 
         if model.broader then traverse(model.broader) end
@@ -72,14 +112,15 @@ M.parse_model = function(mod_id)
 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

+ 45 - 91
src/submission.lua

@@ -25,56 +25,9 @@ local NT = {}
 
 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.
-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())
 
 
@@ -88,7 +41,7 @@ for i = 97, 122 do table.insert(chpool, i) end  -- a-z
 Generate a random, reader-friendly ID.
 
 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)
     local charlist = {}
@@ -110,22 +63,16 @@ M.generate_sip = function(path)
 
     local i = 0
     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))
-        if #row["pas:sourcePath"] > 0 then
+        if #row.source_path > 0 then
             i = i + 1
             logger:info(
                     ("Processing LL resource #%d at row #%d.")
                     :format(i, row_n))
-            prev_path = row["pas:sourcePath"]
+            prev_path = row.source_path
             -- 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
                 if v == "" then goto cont1 end  -- skip empty strings.
                 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.
             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
                 --require "debugger"()
-                sip[i]["pas:thumbnail"] = generate_thumbnail(
+                sip[i].thumbnail = generate_thumbnail(
                         sip[i], sip.root_path, tn_dir)
             end
             --]]
@@ -164,30 +111,31 @@ M.generate_sip = function(path)
                     end
                     ::cont2::
                 end
-                row["pas:sourcePath"] = prev_path
+                row.source_path = prev_path
             end
         end
         row_n = row_n + 1
     end
     -- Infer structure from paths and row ordering.
     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
-                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
-        if rmod.properties["pas:first"] then
+        if rmod.properties.first then
             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
-                    v["pas:first"] = sip[j].id
+                    v.first = sip[j].id
                 end
             end
         end
@@ -202,7 +150,7 @@ end
 
 --]]
 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))
 
     local s = term.new_iriref_ns(rsrc.id)
@@ -210,12 +158,15 @@ M.rsrc_to_graph = function(rsrc)
 
     it = gr:add_init()
     for k, v in pairs(rsrc) do
-        -- id is the subject, it won't be an attribute.
         if k == "id" then goto skip end
-
         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 rdf_type_str = pkar.config.md.datatypes[datatype]
         local rdf_type
@@ -224,9 +175,10 @@ M.rsrc_to_graph = function(rsrc)
         end
         -- Force all fields to be multi-valued.
         if type(v) ~= "table" then v = {[v] = true} end
+        local o
         for vv in pairs(v) do
-            if k == "pas:contentType" then
-                vv = "pas:" .. vv
+            if k == "content_type" then
+                vv = rmod.uri
             end
             if datatype == "resource" then
                 o = term.new_iriref_ns(vv)
@@ -238,7 +190,9 @@ M.rsrc_to_graph = function(rsrc)
         ::skip::
     end
     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
     it:add_done()
 
@@ -252,7 +206,7 @@ M.deposit = function(sip)
         logger:debug(("Processing resource #%d of %d: %s"):format(
                 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)
         -- If it's a directory, skip file processing.
         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"))
 
-            rsrc["dc:format"] = {[magic:filehandle(ifh)] = true}
+            rsrc.format = {[magic:filehandle(ifh)] = true}
             local hash_it = mc.new_blake2b()
             local fsize = 0
             logger:debug("Hashing ", in_path)
@@ -279,9 +233,9 @@ M.deposit = function(sip)
                 fsize = fsize + #chunk
             end
             local checksum = hash_it:final(true)
-            rsrc["premis:hasMessageDigest"] = {
+            rsrc.checksum = {
                     ["urn:blake2:" .. checksum] = true}
-            rsrc["dc:extent"] = fsize
+            rsrc.size = fsize
 
             ofh:close()
             ifh:close()
@@ -296,24 +250,24 @@ M.deposit = function(sip)
             dir.makepath(out_dir)
             logger:debug(("Moving file %s to %s"):format(tmp_path, out_path))
             dir.movefile(tmp_path, out_path)
-            rsrc["pas:path"] = out_path
+            rsrc.archive_path = out_path
 
             -- 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_dir, plpath.basename(src_path))
                 logger:debug(("Moving file %s to %s"):format(src_path, out_path))
                 dir.movefile(src_path, out_path)
-                rsrc["pas:thumbnail"] = out_path
+                rsrc.thumbnail = out_path
             end
         end
 
         ::continue::
 
         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))
     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 pkar = require "pocket_archive"
@@ -11,6 +13,7 @@ M = {}
 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.
+    dir.makepath(path.dirname(dest))
     local img = vips.Image.thumbnail(src, size)
 
     img:write_to_file(dest)
@@ -28,16 +31,8 @@ end
 
 -- Straight copy with no transformation.
 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
 end

+ 4 - 3
src/validator.lua

@@ -17,8 +17,9 @@ local M = {}
 
 M.validate = function(gr, s)
     _, 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 = {
         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
         local values
-        values = gr:attr(s, term.new_iriref_ns(fname))
+        values = gr:attr(s, model.id_to_uri[fname])
 
         -- Cardinality
         local card = 0

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

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

+ 9 - 5
templates/ores.html

@@ -22,15 +22,19 @@
                     </a>
                 <% end %>
                 </p>
-                <a href="<%= rdf_href %>">RDF document</a>
             </section>
             <section class="res_md" id="res_techmd">
                 <h2>Metadata</h2>
+                <p><a href="<%= rdf_href %>">Download RDF document</a></p>
                 <dl>
                 <% for p, ol in pairs(techmd) do %>
                     <dt>
                         <% if ol.label then %><%= ol.label %>
                         <% else %><code><%= ol.uri %></code><% end %>
+                        <% if ol.description then %><span
+                            title="<%= ol.description -%>"
+                            >&nbsp;&#x1F6C8;</span>
+                        <% end %>
                     </dt>
                     <% for _, o in ipairs(ol) do %>
                       <dd><%= o %></dd>
@@ -54,12 +58,12 @@
                 </dl>
             </section>
             <% end %>
-            <% if deliverable then %>
+            <% if pres then %>
             <section id="res_pres">
-            <% if mconf.presentation_type == "image" then %>
-                <img src="<%= deliverable -%>" />
+            <% if mconf.gen.presentation_type == "image" then %>
+                <img src="<%= pres -%>" />
             <%else %>
-                <a href="<%= deliverable -%>" download="<%= fname -%>">
+                <a href="<%= pres -%>" download="<%= fname -%>">
                     Download file
                 </a>
             <% 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.",,
 ,,,,,"More description to demonstrate how multi-valued fields are filled.",,
 ,,,,,"""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",
-"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",
-"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",
-"demo_collection/single_image/0685_04.jpg",,"StillImageFile",,,,,
+"demo_collection/single_image/0685_04.jpg",,"still_image_file",,,,,