diff --git a/.github/workflows/generate-manifests.yml b/.github/workflows/generate-manifests.yml index d33a34f..f827fc9 100644 --- a/.github/workflows/generate-manifests.yml +++ b/.github/workflows/generate-manifests.yml @@ -60,7 +60,7 @@ jobs: if [[ -n "$(git status --porcelain)" ]]; then git config user.name "github-actions[bot]" git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git add projects/**/manifest.json projects/**/README.md projects/**/WARNING.md + git add README.md projects/**/manifest*.json projects/**/README.md projects/**/WARNING.md git commit -m "chore: regenerate manifests" git push else diff --git a/README.md b/README.md index 31b3a41..71c1d04 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Github Manifests -Generate and publish IIIF Presentation 3 manifests for TPEN3 and IIIF viewers such as Mirador. +Generate and publish IIIF Presentation 3 and 4 manifests for TPEN3 and IIIF viewers such as Mirador. ## Purpose @@ -19,7 +19,9 @@ This repository is designed for low-friction manifest creation: │ ├── example-project/ │ │ ├── images/ │ │ ├── info.yml -│ │ ├── manifest.json # generated +│ │ ├── manifest.json # generated (primary version) +│ │ ├── manifest-v3.json # generated (Presentation 3) +│ │ ├── manifest-v4.json # generated (Presentation 4) │ │ ├── README.md # generated │ │ └── WARNING.md # generated ├── scripts/ @@ -45,7 +47,7 @@ Required: Optional: -- info.yml for metadata, ordering, and top-level IIIF fields +- info.yml for metadata, ordering, top-level IIIF fields, and presentationVersion Supported external resource methods: @@ -59,6 +61,29 @@ Ordering priority is: 1. resources entries in info.yml (by order, then natural file/url sort) 2. remaining local images and .lnk files in natural filename order +## IIIF Presentation Versions + +Each generation run writes three manifests per project: + +- manifest.json: the primary manifest, using the project's configured version +- manifest-v3.json: always IIIF Presentation 3 +- manifest-v4.json: always IIIF Presentation 4 + +The primary version defaults to 3 so existing tools keep working. +To switch a project's primary manifest to Presentation 4, set this in info.yml: + +```yaml +presentationVersion: 4 +``` + +The version-pinned manifests are always published, so tools that are not yet upgraded can keep using manifest-v3.json while v4-ready tools use manifest-v4.json. +Canvas, page, and annotation ids are identical across all three serializations, so annotations remain valid whichever version a tool loads. + +Reference guidance: + +- [IIIF Presentation 4.0 (preview)](https://preview.iiif.io/api/prezi-4/presentation/4.0) +- [IIIF Presentation 4.0 Model](https://iiif.io/api/presentation/4.0/model/) + ## Metadata Policy Metadata is optional. @@ -91,6 +116,8 @@ Warnings are written to project WARNING.md. Generated files are always overwritten on each run: - manifest.json +- manifest-v3.json +- manifest-v4.json - README.md - WARNING.md @@ -146,9 +173,11 @@ To reduce unnecessary workflow runs, commit messages can include these markers: GitHub Pages should be configured to publish from the main branch. -Manifest URL pattern: +Manifest URL patterns: - `https://{owner}.github.io/{repo}/projects/{project-name}/manifest.json` +- `https://{owner}.github.io/{repo}/projects/{project-name}/manifest-v3.json` +- `https://{owner}.github.io/{repo}/projects/{project-name}/manifest-v4.json` ## TPEN3 and Viewer Use diff --git a/package.json b/package.json index 63179cb..ad59b3c 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "github-manifests", "version": "0.1.0", "private": true, - "description": "Generate IIIF Presentation 3 manifests for TPEN3/Mirador projects", + "description": "Generate IIIF Presentation 3 and 4 manifests for TPEN3/Mirador projects", "type": "module", "scripts": { "generate": "node scripts/generate.js", diff --git a/projects/example-project/README.md b/projects/example-project/README.md index 920d22d..dd025ad 100644 --- a/projects/example-project/README.md +++ b/projects/example-project/README.md @@ -8,11 +8,20 @@ This project is generated by repository scripts. - Optional advanced configuration in info.yml ## Generated Outputs -- manifest.json +- manifest.json (IIIF Presentation 3) +- manifest-v3.json (IIIF Presentation 3) +- manifest-v4.json (IIIF Presentation 4) - WARNING.md +## IIIF Presentation Versions +manifest.json is the primary manifest and currently uses IIIF Presentation 3. +Set presentationVersion in info.yml to switch the primary version. +Version-pinned manifests are always available for tools that require a specific version. + ## Links - [Manifest.json](https://CenterForDigitalHumanities.github.io/Github-Manifests/projects/example-project/manifest.json) +- [Presentation 3 manifest](https://CenterForDigitalHumanities.github.io/Github-Manifests/projects/example-project/manifest-v3.json) +- [Presentation 4 manifest](https://CenterForDigitalHumanities.github.io/Github-Manifests/projects/example-project/manifest-v4.json) - [Images folder](./images/) - [Mirador Viewer](https://projectmirador.org/embed/?iiif-content=https%3A%2F%2FCenterForDigitalHumanities.github.io%2FGithub-Manifests%2Fprojects%2Fexample-project%2Fmanifest.json) - [Universal Viewer](https://uv-v3.netlify.app/#?manifest=https%3A%2F%2FCenterForDigitalHumanities.github.io%2FGithub-Manifests%2Fprojects%2Fexample-project%2Fmanifest.json) diff --git a/projects/example-project/manifest-v3.json b/projects/example-project/manifest-v3.json new file mode 100644 index 0000000..ce1801f --- /dev/null +++ b/projects/example-project/manifest-v3.json @@ -0,0 +1,119 @@ +{ + "@context": "http://iiif.io/api/presentation/3/context.json", + "id": "https://CenterForDigitalHumanities.github.io/Github-Manifests/projects/example-project/manifest-v3.json", + "type": "Manifest", + "label": { + "none": [ + "Example Project" + ] + }, + "metadata": [ + { + "label": { + "en": [ + "Creator" + ] + }, + "value": { + "en": [ + "Example Archivist" + ] + } + }, + { + "label": { + "en": [ + "Date" + ] + }, + "value": { + "none": [ + "1901" + ] + } + } + ], + "items": [ + { + "id": "https://CenterForDigitalHumanities.github.io/Github-Manifests/projects/example-project/manifest.json/canvas/1", + "type": "Canvas", + "label": { + "en": [ + "External Page 1 (from info.yml)" + ] + }, + "items": [ + { + "id": "https://CenterForDigitalHumanities.github.io/Github-Manifests/projects/example-project/manifest.json/page/1/1", + "type": "AnnotationPage", + "items": [ + { + "id": "https://CenterForDigitalHumanities.github.io/Github-Manifests/projects/example-project/manifest.json/annotation/1/1", + "type": "Annotation", + "motivation": "painting", + "body": { + "id": "https://iiif.io/api/image/3.0/example/reference/15f769d62ca9a3a2deca390efed75d73-5_titlepage2/full/max/0/default.jpg", + "type": "Image", + "format": "image/jpeg" + }, + "target": "https://CenterForDigitalHumanities.github.io/Github-Manifests/projects/example-project/manifest.json/canvas/1" + } + ] + } + ] + }, + { + "id": "https://CenterForDigitalHumanities.github.io/Github-Manifests/projects/example-project/manifest.json/canvas/2", + "type": "Canvas", + "label": { + "en": [ + "External Page 2 (via .lnk reference)" + ] + }, + "items": [ + { + "id": "https://CenterForDigitalHumanities.github.io/Github-Manifests/projects/example-project/manifest.json/page/2/1", + "type": "AnnotationPage", + "items": [ + { + "id": "https://CenterForDigitalHumanities.github.io/Github-Manifests/projects/example-project/manifest.json/annotation/2/1", + "type": "Annotation", + "motivation": "painting", + "body": { + "id": "https://iiif.io/api/image/3.0/example/reference/15f769d62ca9a3a2deca390efed75d73-3_titlepage1/full/max/0/default.jpg", + "type": "Image", + "format": "image/jpeg" + }, + "target": "https://CenterForDigitalHumanities.github.io/Github-Manifests/projects/example-project/manifest.json/canvas/2" + } + ] + } + ] + } + ], + "summary": { + "en": [ + "Example generated manifest for TPEN3 and Mirador testing." + ] + }, + "rights": "http://creativecommons.org/licenses/by/4.0/", + "requiredStatement": { + "label": { + "en": [ + "Attribution" + ] + }, + "value": { + "en": [ + "Provided by Example Institution" + ] + } + }, + "seeAlso": [ + { + "id": "https://iiif.io/", + "type": "Dataset", + "format": "text/html" + } + ] +} diff --git a/projects/example-project/manifest-v4.json b/projects/example-project/manifest-v4.json new file mode 100644 index 0000000..a7a8ece --- /dev/null +++ b/projects/example-project/manifest-v4.json @@ -0,0 +1,119 @@ +{ + "@context": "http://iiif.io/api/presentation/4/context.json", + "id": "https://CenterForDigitalHumanities.github.io/Github-Manifests/projects/example-project/manifest-v4.json", + "type": "Manifest", + "label": { + "none": [ + "Example Project" + ] + }, + "metadata": [ + { + "label": { + "en": [ + "Creator" + ] + }, + "value": { + "en": [ + "Example Archivist" + ] + } + }, + { + "label": { + "en": [ + "Date" + ] + }, + "value": { + "none": [ + "1901" + ] + } + } + ], + "items": [ + { + "id": "https://CenterForDigitalHumanities.github.io/Github-Manifests/projects/example-project/manifest.json/canvas/1", + "type": "Canvas", + "label": { + "en": [ + "External Page 1 (from info.yml)" + ] + }, + "items": [ + { + "id": "https://CenterForDigitalHumanities.github.io/Github-Manifests/projects/example-project/manifest.json/page/1/1", + "type": "AnnotationPage", + "items": [ + { + "id": "https://CenterForDigitalHumanities.github.io/Github-Manifests/projects/example-project/manifest.json/annotation/1/1", + "type": "Annotation", + "motivation": "painting", + "body": { + "id": "https://iiif.io/api/image/3.0/example/reference/15f769d62ca9a3a2deca390efed75d73-5_titlepage2/full/max/0/default.jpg", + "type": "Image", + "format": "image/jpeg" + }, + "target": "https://CenterForDigitalHumanities.github.io/Github-Manifests/projects/example-project/manifest.json/canvas/1" + } + ] + } + ] + }, + { + "id": "https://CenterForDigitalHumanities.github.io/Github-Manifests/projects/example-project/manifest.json/canvas/2", + "type": "Canvas", + "label": { + "en": [ + "External Page 2 (via .lnk reference)" + ] + }, + "items": [ + { + "id": "https://CenterForDigitalHumanities.github.io/Github-Manifests/projects/example-project/manifest.json/page/2/1", + "type": "AnnotationPage", + "items": [ + { + "id": "https://CenterForDigitalHumanities.github.io/Github-Manifests/projects/example-project/manifest.json/annotation/2/1", + "type": "Annotation", + "motivation": "painting", + "body": { + "id": "https://iiif.io/api/image/3.0/example/reference/15f769d62ca9a3a2deca390efed75d73-3_titlepage1/full/max/0/default.jpg", + "type": "Image", + "format": "image/jpeg" + }, + "target": "https://CenterForDigitalHumanities.github.io/Github-Manifests/projects/example-project/manifest.json/canvas/2" + } + ] + } + ] + } + ], + "summary": { + "en": [ + "Example generated manifest for TPEN3 and Mirador testing." + ] + }, + "rights": "http://creativecommons.org/licenses/by/4.0/", + "requiredStatement": { + "label": { + "en": [ + "Attribution" + ] + }, + "value": { + "en": [ + "Provided by Example Institution" + ] + } + }, + "seeAlso": [ + { + "id": "https://iiif.io/", + "type": "Dataset", + "format": "text/html" + } + ] +} diff --git a/scripts/config.js b/scripts/config.js index e336ef2..ac67091 100644 --- a/scripts/config.js +++ b/scripts/config.js @@ -22,7 +22,13 @@ export const MAX_IMAGE_DIMENSION_WARNING = 5000 export const MAX_FILE_SIZE_WARNING_BYTES = 15 * 1024 * 1024 export const FETCH_TIMEOUT_MS = 15000 -export const IIIF_CONTEXT = "http://iiif.io/api/presentation/3/context.json" +export const IIIF_CONTEXTS = { + 3: "http://iiif.io/api/presentation/3/context.json", + 4: "http://iiif.io/api/presentation/4/context.json" +} + +export const SUPPORTED_PRESENTATION_VERSIONS = Object.keys(IIIF_CONTEXTS).map(Number) +export const DEFAULT_PRESENTATION_VERSION = 3 export const TOP_LEVEL_ALLOWED_INFO_KEYS = new Set([ "label", @@ -42,5 +48,6 @@ export const TOP_LEVEL_ALLOWED_INFO_KEYS = new Set([ "start", "thumbnail", "resources", - "ordering" + "ordering", + "presentationVersion" ]) diff --git a/scripts/generate.js b/scripts/generate.js index aaf21ec..93ef9ef 100644 --- a/scripts/generate.js +++ b/scripts/generate.js @@ -4,12 +4,14 @@ import { extname } from "node:path" import sizeOf from "image-size" import mime from "mime-types" import { + DEFAULT_PRESENTATION_VERSION, FETCH_TIMEOUT_MS, - IIIF_CONTEXT, + IIIF_CONTEXTS, MAX_FILE_SIZE_WARNING_BYTES, MAX_IMAGE_DIMENSION_WARNING, SUPPORTED_IMAGE_EXTENSIONS, SUPPORTED_IMAGE_MIME_TYPES, + SUPPORTED_PRESENTATION_VERSIONS, TOP_LEVEL_ALLOWED_INFO_KEYS } from "./config.js" import { @@ -139,6 +141,27 @@ function buildProjectDirectoryUrl(projectName) { return `./projects/${projectName}/` } +function resolvePresentationVersion(info, warnings) { + const raw = info.presentationVersion + if (raw === undefined || raw === null) { + return DEFAULT_PRESENTATION_VERSION + } + + const version = Number(raw) + if (!SUPPORTED_PRESENTATION_VERSIONS.includes(version)) { + warnings.push( + createWarning( + "unsupported-presentation-version", + `info.yml presentationVersion '${raw}' is not supported. Using default version ${DEFAULT_PRESENTATION_VERSION}. Supported versions: ${SUPPORTED_PRESENTATION_VERSIONS.join(", ")}.`, + { value: raw } + ) + ) + return DEFAULT_PRESENTATION_VERSION + } + + return version +} + function normalizeTopLevelFields(info, warnings) { const out = {} @@ -198,10 +221,10 @@ function toManifestMetadata(info) { return metadataFromInfo(info.metadata) } -function makeCanvas(manifestId, index, label, body) { - const canvasId = `${manifestId}/canvas/${index}` - const pageId = `${manifestId}/page/${index}/1` - const annotationId = `${manifestId}/annotation/${index}/1` +function makeCanvas(canvasBaseId, index, label, body) { + const canvasId = `${canvasBaseId}/canvas/${index}` + const pageId = `${canvasBaseId}/page/${index}/1` + const annotationId = `${canvasBaseId}/annotation/${index}/1` const canvas = { id: canvasId, @@ -624,45 +647,37 @@ function mergeAndOrderResources(infoResolved, folderResources) { return ordered } -function buildManifest({ projectName, siteBaseUrl, info, resources, warnings }) { - const manifestId = `${siteBaseUrl}/projects/${projectName}/manifest.json` - const topLevel = normalizeTopLevelFields(info, warnings) - - const manifestLabel = languageMapFrom(info.label ?? projectName) - const metadata = toManifestMetadata(info) - - const manifest = { - "@context": IIIF_CONTEXT, - id: manifestId, - type: "Manifest", - label: manifestLabel, - metadata, - items: resources.map((resource, index) => { - const body = { - id: resource.body.id, - type: resource.body.type ?? "Image" - } +function buildCanvases(canvasBaseId, resources) { + return resources.map((resource, index) => { + const body = { + id: resource.body.id, + type: resource.body.type ?? "Image" + } - if (resource.body.format) { - body.format = resource.body.format - } - if (resource.body.width) { - body.width = resource.body.width - } - if (resource.body.height) { - body.height = resource.body.height - } - if (resource.body.service) { - body.service = Array.isArray(resource.body.service) ? resource.body.service : [resource.body.service] - } + if (resource.body.format) { + body.format = resource.body.format + } + if (resource.body.width) { + body.width = resource.body.width + } + if (resource.body.height) { + body.height = resource.body.height + } + if (resource.body.service) { + body.service = Array.isArray(resource.body.service) ? resource.body.service : [resource.body.service] + } - return makeCanvas(manifestId, index + 1, resource.label, body) - }) - } + return makeCanvas(canvasBaseId, index + 1, resource.label, body) + }) +} - Object.assign(manifest, topLevel) +function buildManifestContent({ projectName, info, resources, canvasBaseId, warnings }) { + const topLevel = normalizeTopLevelFields(info, warnings) + const manifestLabel = languageMapFrom(info.label ?? projectName) + const metadata = toManifestMetadata(info) + const canvases = buildCanvases(canvasBaseId, resources) - if (!manifest.rights) { + if (!topLevel.rights) { warnings.push( createWarning( "missing-rights", @@ -672,16 +687,31 @@ function buildManifest({ projectName, siteBaseUrl, info, resources, warnings }) ) } + return { topLevel, manifestLabel, metadata, canvases } +} + +function serializeManifest({ manifestId, presentationVersion, content }) { + const manifest = { + "@context": IIIF_CONTEXTS[presentationVersion], + id: manifestId, + type: "Manifest", + label: content.manifestLabel, + metadata: content.metadata, + items: content.canvases + } + + Object.assign(manifest, content.topLevel) + return manifest } -function generateProjectReadme(projectName, manifestUrl) { +function generateProjectReadme(projectName, { manifestUrl, manifestV3Url, manifestV4Url, presentationVersion }) { const encodedManifestUrl = encodeURIComponent(manifestUrl) const miradorUrl = `https://projectmirador.org/embed/?iiif-content=${encodedManifestUrl}` const universalViewerUrl = `https://uv-v3.netlify.app/#?manifest=${encodedManifestUrl}` const tpenImportUrl = `https://app.t-pen.org/project/import?manifest=${encodedManifestUrl}` - return `# ${projectName}\n\nThis project is generated by repository scripts.\n\n## Inputs\n- Add local images in images/\n- Add external references using .lnk files in images/ (one HTTP(S) URL per file)\n- Optional advanced configuration in info.yml\n\n## Generated Outputs\n- manifest.json\n- WARNING.md\n\n## Links\n- [Manifest.json](${manifestUrl})\n- [Images folder](./images/)\n- [Mirador Viewer](${miradorUrl})\n- [Universal Viewer](${universalViewerUrl})\n- [TPEN3 Create Project](${tpenImportUrl})\n\n## TPEN3\nUse the TPEN3 import link above to create a new project directly from this manifest.\n\n> This file is regenerated when manifest generation runs. Put durable custom metadata in info.yml.\n` + return `# ${projectName}\n\nThis project is generated by repository scripts.\n\n## Inputs\n- Add local images in images/\n- Add external references using .lnk files in images/ (one HTTP(S) URL per file)\n- Optional advanced configuration in info.yml\n\n## Generated Outputs\n- manifest.json (IIIF Presentation ${presentationVersion})\n- manifest-v3.json (IIIF Presentation 3)\n- manifest-v4.json (IIIF Presentation 4)\n- WARNING.md\n\n## IIIF Presentation Versions\nmanifest.json is the primary manifest and currently uses IIIF Presentation ${presentationVersion}.\nSet presentationVersion in info.yml to switch the primary version.\nVersion-pinned manifests are always available for tools that require a specific version.\n\n## Links\n- [Manifest.json](${manifestUrl})\n- [Presentation 3 manifest](${manifestV3Url})\n- [Presentation 4 manifest](${manifestV4Url})\n- [Images folder](./images/)\n- [Mirador Viewer](${miradorUrl})\n- [Universal Viewer](${universalViewerUrl})\n- [TPEN3 Create Project](${tpenImportUrl})\n\n## TPEN3\nUse the TPEN3 import link above to create a new project directly from this manifest.\n\n> This file is regenerated when manifest generation runs. Put durable custom metadata in info.yml.\n` } function generateRootProjectIndex(projectNames) { @@ -763,18 +793,46 @@ async function generateProject(projectName) { ) } - const manifest = buildManifest({ + const presentationVersion = resolvePresentationVersion(info, warnings) + const projectBaseUrl = `${siteBaseUrl}/projects/${projectName}` + const manifestUrl = `${projectBaseUrl}/manifest.json` + + // Canvas, page, and annotation ids are derived from the primary manifest url + // in every serialization so annotations stay valid across versions. + const content = buildManifestContent({ projectName, - siteBaseUrl, info, resources: orderedResources, + canvasBaseId: manifestUrl, warnings }) - const manifestUrl = `${siteBaseUrl}/projects/${projectName}/manifest.json` await ensureDir(projectDir) - await writeJson(manifestPath, manifest) - await writeText(readmePath, generateProjectReadme(projectName, manifestUrl)) + await writeJson( + manifestPath, + serializeManifest({ manifestId: manifestUrl, presentationVersion, content }) + ) + + const versionedUrls = {} + for (const version of SUPPORTED_PRESENTATION_VERSIONS) { + const versionedFileName = `manifest-v${version}.json` + const versionedUrl = `${projectBaseUrl}/${versionedFileName}` + versionedUrls[version] = versionedUrl + await writeJson( + path.join(projectDir, versionedFileName), + serializeManifest({ manifestId: versionedUrl, presentationVersion: version, content }) + ) + } + + await writeText( + readmePath, + generateProjectReadme(projectName, { + manifestUrl, + manifestV3Url: versionedUrls[3], + manifestV4Url: versionedUrls[4], + presentationVersion + }) + ) await writeText(warningPath, generateWarningsMarkdown(projectName, warnings)) return { diff --git a/scripts/validate.js b/scripts/validate.js index 1334e23..225130a 100644 --- a/scripts/validate.js +++ b/scripts/validate.js @@ -1,7 +1,10 @@ import fs from "node:fs/promises" import path from "node:path" +import { IIIF_CONTEXTS, SUPPORTED_PRESENTATION_VERSIONS } from "./config.js" import { parseArgs, repoRoot } from "./utils.js" +const SUPPORTED_CONTEXTS = new Set(Object.values(IIIF_CONTEXTS)) + function fail(message) { throw new Error(message) } @@ -11,38 +14,62 @@ async function listProjectDirectories(projectsRoot) { return entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name) } -async function validateProject(projectName) { - const projectDir = path.join(repoRoot, "projects", projectName) - const manifestPath = path.join(projectDir, "manifest.json") +async function validateManifestFile(projectName, fileName, expectedVersion) { + const manifestPath = path.join(repoRoot, "projects", projectName, fileName) - const raw = await fs.readFile(manifestPath, "utf8") - const manifest = JSON.parse(raw) + const raw = await fs.readFile(manifestPath, "utf8").catch(() => { + fail(`${projectName}: ${fileName} is missing. Run generation to create it.`) + }) + + let manifest + try { + manifest = JSON.parse(raw) + } catch { + fail(`${projectName}: ${fileName} is not valid JSON.`) + } + + const context = manifest["@context"] + if (expectedVersion !== undefined && context !== IIIF_CONTEXTS[expectedVersion]) { + fail(`${projectName}: ${fileName} @context must be '${IIIF_CONTEXTS[expectedVersion]}'.`) + } + + if (expectedVersion === undefined && !SUPPORTED_CONTEXTS.has(context)) { + fail(`${projectName}: ${fileName} @context '${context}' is not a supported IIIF Presentation context.`) + } if (manifest.type !== "Manifest") { - fail(`${projectName}: manifest type must be 'Manifest'.`) + fail(`${projectName}: ${fileName} type must be 'Manifest'.`) } if (!manifest.id || typeof manifest.id !== "string") { - fail(`${projectName}: manifest id is missing.`) + fail(`${projectName}: ${fileName} id is missing.`) } if (!manifest.label || typeof manifest.label !== "object") { - fail(`${projectName}: manifest label is missing or invalid.`) + fail(`${projectName}: ${fileName} label is missing or invalid.`) } if (!Array.isArray(manifest.items) || manifest.items.length === 0) { - fail(`${projectName}: manifest items must contain at least one Canvas.`) + fail(`${projectName}: ${fileName} items must contain at least one Canvas.`) } for (const [index, canvas] of manifest.items.entries()) { if (canvas.type !== "Canvas") { - fail(`${projectName}: item ${index + 1} is not a Canvas.`) + fail(`${projectName}: ${fileName} item ${index + 1} is not a Canvas.`) } if (!Array.isArray(canvas.items) || canvas.items.length === 0) { - fail(`${projectName}: canvas ${index + 1} is missing AnnotationPage items.`) + fail(`${projectName}: ${fileName} canvas ${index + 1} is missing AnnotationPage items.`) } } +} + +async function validateProject(projectName) { + await validateManifestFile(projectName, "manifest.json", undefined) + + for (const version of SUPPORTED_PRESENTATION_VERSIONS) { + await validateManifestFile(projectName, `manifest-v${version}.json`, version) + } return `${projectName}: OK` }