From bc522a405517b4c3acc2b123f080d629d3ba35e8 Mon Sep 17 00:00:00 2001 From: dwithana Date: Mon, 9 Dec 2024 17:51:09 -0500 Subject: [PATCH 1/3] Annotation parser setup, with support multiple motivations --- src/context/manifest-context.js | 17 +- src/services/annotation-parser.test.js | 237 +++++++++++++++++++++++++ src/services/annotations-parser.js | 170 ++++++++++++++++++ src/services/utility-helpers.js | 53 +++--- 4 files changed, 454 insertions(+), 23 deletions(-) create mode 100644 src/services/annotation-parser.test.js create mode 100644 src/services/annotations-parser.js diff --git a/src/context/manifest-context.js b/src/context/manifest-context.js index 59a43ab3..e05eadea 100644 --- a/src/context/manifest-context.js +++ b/src/context/manifest-context.js @@ -1,3 +1,4 @@ +import { parseAnnotationSets } from '@Services/annotations-parser'; import { canvasesInManifest, parseAutoAdvance } from '../services/iiif-parser'; import { getAnnotationService, getIsPlaylist, parsePlaylistAnnotations } from '@Services/playlist-parser'; import React, { createContext, useContext, useReducer } from 'react'; @@ -38,7 +39,8 @@ const defaultState = { hasStructure: false, // current Canvas has structure timespans isCollapsed: false, // all sections are expanded by default structItems: [], - } + }, + annotations: [], // [{ canvasIndex: Number, annotationSets: Array }] }; function getHasStructure(canvasSegments, canvasIndex) { @@ -52,6 +54,12 @@ function getHasStructure(canvasSegments, canvasIndex) { return canvasStructures.length > 0; } + +function hasParsedCanvasAnnotations(annotations, canvasIndex) { + const parsedAnnotations = annotations.filter((a) => a.canvasIndex == canvasIndex); + return parsedAnnotations?.length > 0; +} + function manifestReducer(state = defaultState, action) { switch (action.type) { case 'updateManifest': { @@ -74,10 +82,12 @@ function manifestReducer(state = defaultState, action) { annotationServiceId: annotationService, hasAnnotationService: annotationService ? true : false, markers: playlistMarkers, - } + }, + annotations: [parseAnnotationSets(manifest, state.canvasIndex)] }; } case 'switchCanvas': { + const hasAnnotations = hasParsedCanvasAnnotations(state.annotations, action.canvasIndex); return { ...state, canvasIndex: action.canvasIndex, @@ -85,6 +95,9 @@ function manifestReducer(state = defaultState, action) { ...state.structures, hasStructure: getHasStructure(state.canvasSegments, action.canvasIndex), }, + annotations: hasAnnotations + ? state.annotations.filter((a) => a.canvasIndex === action.canvasIndex)[0] + : [...state.annotations, parseAnnotationSets(state.manifest, action.canvasIndex)] }; } case 'switchItem': { diff --git a/src/services/annotation-parser.test.js b/src/services/annotation-parser.test.js new file mode 100644 index 00000000..b91fdcd2 --- /dev/null +++ b/src/services/annotation-parser.test.js @@ -0,0 +1,237 @@ +import * as annotationParser from './annotations-parser'; +import lunchroomManners from '@TestData/lunchroom-manners'; + +const textualBodyAnnotations = { + '@context': 'http://iiif.io/api/presentation/3/context.json', + id: 'https://example.com/avannotate-test/manifest.json', + type: 'Manifest', + label: { en: ['AVAnnotate TextualBody Annotations'] }, + items: [ + { + id: 'https://example.com/avannotate-test/canvas-1/canvas', + type: 'Canvas', + duration: 809.0, + annotations: [ + { + type: 'AnnotationPage', + id: 'https://example.com/avannotate-test/canvas-1/canvas', + label: { none: ['Default'] }, + items: [ + { + type: 'Annotation', + motivation: ['commenting', 'tagging'], + id: 'https://example.com/avannotate-test/canvas-1/canvas/page/1', + body: [ + { type: 'TextualBody', value: '[Inaudible]', format: 'text/plain', motivation: 'commenting' }, + { type: 'TextualBody', value: 'Inaudible', format: 'text/plain', motivation: 'tagging' } + ], + target: 'https://example.com/avannotate-test/canvas-1/canvas#t=52,60' + }, { + type: 'Annotation', + motivation: ['commenting', 'tagging'], + id: 'https://example.com/avannotate-test/canvas-1/canvas/page/1', + body: [ + { type: 'TextualBody', value: 'Alright.', format: 'text/plain', motivation: 'commenting' }, + { type: 'TextualBody', value: 'Herbert Halpert', format: 'text/plain', motivation: 'tagging' } + ], + target: 'https://example.com/avannotate-test/canvas-1/canvas#t=1085,1085' + }, { + type: 'Annotation', + motivation: ['commenting', 'tagging'], + id: 'https://example.com/avannotate-test/canvas-1/canvas/page/1', + body: [ + { type: 'TextualBody', value: 'Alright. Let\'s play.', format: 'text/plain', motivation: 'commenting' }, + { type: 'TextualBody', value: 'Herbert Halpert', format: 'text/plain', motivation: 'tagging' } + ], + target: 'https://example.com/avannotate-test/canvas-1/canvas#t=1231,1232' + }, { + type: 'Annotation', + motivation: ['commenting', 'tagging'], + id: 'https://example.com/avannotate-test/canvas-1/canvas/page/1', + body: [ + { type: 'TextualBody', value: 'Men singing', format: 'text/plain', motivation: 'commenting' }, + { type: 'TextualBody', value: 'Unknown', format: 'text/plain', motivation: 'tagging' } + ], + target: 'https://example.com/avannotate-test/canvas-1/canvas#t=0,39' + }, + ] + } + ], + items: [ + { + id: 'https://example.com/avannotate-test/canvas-1/paintings', + type: 'AnnotationPage', + items: [ + { + id: 'https://example.com/avannotate-test/canvas-1/painting', + type: 'Annotation', + motivation: 'painting', + body: { + id: 'https://example.com/avannotate-test.mp4', + type: 'Video', + format: 'video/mp4', + duration: 809.0 + }, + target: 'https://example.com/avannotate-test/canvas-1/canvas' + } + ] + } + ] + } + ] +}; + +const externalAnnotationPage = { + '@context': 'http://iiif.io/api/presentation/3/context.json', + id: 'https://example.com/avannotate-annotations/manifest.json', + type: 'Manifest', + label: { 'en': ['S1576 , T86-244'] }, + items: [ + { + id: 'https://example.com/avannotate-annotations/canvas-1/canvas', + type: 'Canvas', + duration: 3400.0, + annotations: [ + { + type: 'AnnotationPage', + id: 'https://example.com/annotations/avannotate-annotations-canvas-1-library-of-congress.json', + label: { 'none': ['Library of Congress'] } + }, + { + type: 'AnnotationPage', + id: 'https://example.com/annotations/avannotate-annotations-canvas-1-rolla-southworth.json', + label: { 'none': ['Rolla Southworth'] } + }, + { + type: 'AnnotationPage', + id: 'https://example.com/annotations/avannotate-annotations-canvas-1-zora-neale-hurston.json', + label: { 'none': ['Zora Neale Hurston'] } + }, + { + type: 'AnnotationPage', + id: 'https://example.com/annotations/avannotate-annotations-canvas-1-herbert-halpert.json', + label: { 'none': ['Herbert Halpert'] } + }, + ] + } + ] +}; + +describe('annotation-parser', () => { + describe('parseAnnotationSets', () => { + test('AnnotationPage with TextualBody annotations', () => { + const { canvasIndex, annotationSets } = annotationParser.parseAnnotationSets(textualBodyAnnotations, 0); + expect(canvasIndex).toEqual(0); + expect(annotationSets.length).toEqual(1); + const { items, label } = annotationSets[0]; + expect(items.length).toEqual(4); + expect(label).toEqual('Default'); + }); + + test('AnnotationPage without TextualBody annotations', () => { + const { canvasIndex, annotationSets } = annotationParser.parseAnnotationSets(lunchroomManners, 0); + expect(canvasIndex).toEqual(0); + expect(annotationSets.length).toEqual(1); + + const { items, label } = annotationSets[0]; + expect(items.length).toEqual(1); + expect(label).toEqual(''); + }); + + test('AnnotationPage without items property', () => { + const { canvasIndex, annotationSets } = annotationParser.parseAnnotationSets(externalAnnotationPage, 0); + expect(canvasIndex).toEqual(0); + expect(annotationSets.length).toEqual(4); + + const { items, label, url } = annotationSets[0]; + expect(items).toBeUndefined(); + expect(url).toEqual('https://example.com/annotations/avannotate-annotations-canvas-1-library-of-congress.json'); + expect(label).toEqual('Library of Congress'); + }); + }); + + describe('parseAnnotationItems', () => { + test('Annotation with TextualBody type', () => { + const annotations = [ + { + type: 'Annotation', + motivation: ['commenting', 'tagging'], + id: 'https://example.com/avannotate-test/canvas-1/canvas/page/1', + body: [ + { type: 'TextualBody', value: '[Inaudible]', format: 'text/plain', motivation: 'commenting' }, + { type: 'TextualBody', value: 'Inaudible', format: 'text/plain', motivation: 'tagging' } + ], + target: 'https://example.com/avannotate-test/canvas-1/canvas#t=52,60' + }, { + type: 'Annotation', + motivation: ['commenting', 'tagging'], + id: 'https://example.com/avannotate-test/canvas-1/canvas/page/1', + body: [ + { type: 'TextualBody', value: 'Alright.', format: 'text/plain', motivation: 'commenting' }, + { type: 'TextualBody', value: 'Herbert Halpert', format: 'text/plain', motivation: 'tagging' } + ], + target: 'https://example.com/avannotate-test/canvas-1/canvas#t=1085,1085' + }, { + type: 'Annotation', + motivation: ['commenting', 'tagging'], + id: 'https://example.com/avannotate-test/canvas-1/canvas/page/1', + body: [ + { type: 'TextualBody', value: 'Alright. Let\'s play.', format: 'text/plain', motivation: 'commenting' }, + { type: 'TextualBody', value: 'Herbert Halpert', format: 'text/plain', motivation: 'tagging' } + ], + target: 'https://example.com/avannotate-test/canvas-1/canvas#t=1231,1232' + }, { + type: 'Annotation', + motivation: ['commenting', 'tagging'], + id: 'https://example.com/avannotate-test/canvas-1/canvas/page/1', + body: [ + { type: 'TextualBody', value: 'Men singing', format: 'text/plain', motivation: 'commenting' }, + { type: 'TextualBody', value: 'Unknown', format: 'text/plain', motivation: 'tagging' } + ], + target: 'https://example.com/avannotate-test/canvas-1/canvas#t=0,39' + }, + ]; + const items = annotationParser.parseAnnotationItems(annotations, 809.0); + + expect(items.length).toEqual(4); + expect(items[0].motivation).toEqual(['commenting', 'tagging']); + expect(items[0].id).toEqual('https://example.com/avannotate-test/canvas-1/canvas/page/1'); + expect(items[0].times).toEqual({ start: 0, end: 39 }); + expect(items[0].canvasId).toEqual('https://example.com/avannotate-test/canvas-1/canvas'); + expect(items[0].body).toEqual([ + { format: 'text/plain', purpose: ['commenting'], value: 'Men singing' }, + { format: 'text/plain', purpose: ['tagging'], value: 'Unknown' }]); + }); + + test('Annotation with Text type', () => { + const annotations = [ + { + id: 'https://example.com/manifest/lunchroom_manners/canvas/1/annotation/1', + type: 'Annotation', + motivation: 'supplementing', + body: { + id: 'https://example.com/manifest/lunchroom_manners.vtt', + type: 'Text', + format: 'text/vtt', + label: { + en: ['Captions in WebVTT format'], + }, + language: 'en', + }, + target: 'https://example.com/manifest/lunchroom_manners/canvas/1' + } + ]; + const items = annotationParser.parseAnnotationItems(annotations, 572.34); + expect(items[0].motivation).toEqual(['supplementing']); + expect(items[0].id).toEqual('https://example.com/manifest/lunchroom_manners/canvas/1/annotation/1'); + expect(items[0].times).toBeUndefined(); + expect(items[0].canvasId).toEqual('https://example.com/manifest/lunchroom_manners/canvas/1'); + expect(items[0].body).toEqual([{ + format: 'text/vtt', + value: 'Captions in WebVTT format', + url: 'https://example.com/manifest/lunchroom_manners.vtt', + isExternal: true, + }]); + }); + }); +}); diff --git a/src/services/annotations-parser.js b/src/services/annotations-parser.js new file mode 100644 index 00000000..fac6d242 --- /dev/null +++ b/src/services/annotations-parser.js @@ -0,0 +1,170 @@ +import { getCanvasId } from "./iiif-parser"; +import { getLabelValue, getMediaFragment, parseTimeStrings } from "./utility-helpers"; + +export function parseAnnotationSets(manifest, canvasIndex) { + let canvas = null; + let annotationSets = []; + + // return empty object when canvasIndex is undefined + if (canvasIndex === undefined || canvasIndex < 0) { + return { + ...info, + error: 'Error fetching content' + }; + } + + // return an error when the given Manifest doesn't have any Canvas(es) + const canvases = manifest.items; + if (canvases?.length != 0) { + canvas = canvases[canvasIndex]; + + if (canvas === undefined) { + console.error( + 'iiif-parser -> getMediaInfo() -> canvas undefined -> ', canvasIndex + ); + throw new Error(GENERIC_ERROR_MESSAGE); + } + + const annotations = canvas.annotations; + const duration = Number(canvas.duration); + + if (annotations?.length > 0 && annotations[0].type === 'AnnotationPage') { + annotations.map((annotation) => { + if (annotation.type === 'AnnotationPage') { + let annotationSet = { label: getLabelValue(annotation.label) }; + if (annotation.items?.length > 0) { + annotationSet.items = parseAnnotationItems(annotation.items, duration); + } else { + annotationSet.url = annotation.id; + } + annotationSets.push(annotationSet); + } + }); + } + } + + return { canvasIndex, annotationSets }; +}; + +/** + * Parse each Annotation in a given AnnotationPage resource + * @param {Array} annotations list of annotations from AnnotationPage + * @param {Number} duration Canvas duration + * @returns {Array} array of JSON objects for each Annotation + * [{ + * motivation: Array, + * id: String, + * times: { start: Number, end: Number || undefined }, + * canvasId: URI, + * body: [ return type of parseTextualBody() ] + * }] + */ +export function parseAnnotationItems(annotations, duration) { + if (annotations == undefined || annotations?.length == 0) { + return []; + } + let items = []; + annotations.map((annotation) => { + let canvasId, times; + if (typeof annotation.target === 'string') { + canvasId = getCanvasId(annotation.target); + times = getMediaFragment(annotation.target, duration); + } else { + // Might want to re-visit based on the implementation changes in AVAnnotate manifests + const { source, selector } = annotation.target; + canvasId = source.id; + times = parseSelector(selector, duration); + } + items.push({ + motivation: Array.isArray(annotation.motivation) + ? annotation.motivation : [annotation.motivation], + id: annotation.id, + times: times, + canvasId, + body: parseAnnotationBody(annotation.body) + }); + }); + + // Sort by start time of annotations + items.sort((a, b) => a.times?.start - b.times?.start); + return items; +}; + +/** + * Parse different types of temporal selectors given in an Annotation + * @param {Object} selector Selector object from an Annotation + * @param {Number} duration Canvas duration + * @returns {Object} start, end times of an Annotation + */ +function parseSelector(selector, duration) { + const selectorType = selector.type; + let times; + switch (selectorType) { + case 'FragmentSelector': + times = parseTimeStrings(selector.value.split('t=')[1], duration); + break; + case 'PointSelector': + times = { start: Number(selector.t), end: undefined }; + break; + case 'RangeSelector': + // TODO: https://github.com/IIIF/presentation-validator/issues/172 + times = parseTimeStrings(selector.t, duration); + break; + default: + times = { start: 0, end: 0 }; + break; + } + return times; +}; + +/** + * Parse value of a TextualBody into a JSON object + * @param {Object} textualBody TextualBody type object + * @returns {Object} JSON object for TextualBody value + * { format: String, purpose: Array, value: String } + */ +function parseTextualBody(textualBody) { + const purpose = textualBody.purpose ? textualBody.purpose : textualBody.motivation; + const annotationBody = { + format: textualBody.format, + /** + * Use purpose instead of motivation, as it is specific to 'TextualBody' type. + * 'purpose'/'motaivation' can have 0 or more values. + * Reference: https://www.w3.org/TR/annotation-model/#motivation-and-purpose + */ + purpose: Array.isArray(purpose) ? purpose : [purpose], + value: textualBody.value, + }; + return annotationBody; +} + +/** + * Parse 'body' of an Annotation into a JSON object. + * @param {Array || Object} annotationBody body property of an Annotation + */ +function parseAnnotationBody(annotationBody) { + if (!Array.isArray(annotationBody)) { + annotationBody = [annotationBody]; + } + + let values = []; + annotationBody.map((body) => { + const type = body.type; + switch (type) { + case 'TextualBody': + values.push(parseTextualBody(body)); + break; + case 'Text': + values.push({ + format: body.format, + value: getLabelValue(body.label), + url: body.id, + isExternal: true, + }); + break; + default: + break; + } + }); + return values; +} diff --git a/src/services/utility-helpers.js b/src/services/utility-helpers.js index b4be4aaa..a965ad69 100644 --- a/src/services/utility-helpers.js +++ b/src/services/utility-helpers.js @@ -252,29 +252,40 @@ export function fileDownload(fileUrl, fileName, fileExt = '', machineGenerated = export function getMediaFragment(uri, duration = 0) { if (uri !== undefined) { const fragment = uri.split('#t=')[1]; - if (fragment !== undefined) { - let start, end; - /** - * If the times are in a string format (hh:mm:ss) check for comma seperated decimals. - * Some SRT captions use comma to seperate milliseconds. - */ - const timestampRegex = /([0-9]*:){1,2}([0-9]{2})(?:((\.|\,)[0-9]{2,3})?)/g; - if (fragment.includes(':') && [...fragment.matchAll(/\,/g)]?.length > 1) { - const times = [...fragment.matchAll(timestampRegex)]; - [start, end] = times?.length == 2 ? [times[0][0], times[1][0]] : [0, 0]; - } else { - [start, end] = fragment.split(','); - } - if (end === undefined) { - end = duration.toString(); - } - return { - start: start.match(timestampRegex) ? timeToS(start) : Number(start), - end: end.match(timestampRegex) ? timeToS(end) : Number(end) - }; + return parseTimeStrings(fragment, duration); + } else { + return undefined; + } +} + +/** + * Parse comma seperated media-fragment + * @function Util#parseTimeStrings + * @param {String} fragment media fragment + * @param {Number} duration Canvas duration + * @returns {Object} {start: Number, end: Number } + */ +export function parseTimeStrings(fragment, duration = 0) { + if (fragment !== undefined) { + let start, end; + /** + * If the times are in a string format (hh:mm:ss) check for comma seperated decimals. + * Some SRT captions use comma to seperate milliseconds. + */ + const timestampRegex = /([0-9]*:){1,2}([0-9]{2})(?:((\.|\,)[0-9]{2,3})?)/g; + if (fragment.includes(':') && [...fragment.matchAll(/\,/g)]?.length > 1) { + const times = [...fragment.matchAll(timestampRegex)]; + [start, end] = times?.length == 2 ? [times[0][0], times[1][0]] : [0, 0]; } else { - return undefined; + [start, end] = fragment.split(','); } + if (end === undefined) { + end = duration.toString(); + } + return { + start: start.match(timestampRegex) ? timeToS(start) : Number(start), + end: end.match(timestampRegex) ? timeToS(end) : Number(end) + }; } else { return undefined; } From f1bc45c56d7963d941a3c82c6794ad16b3996cc1 Mon Sep 17 00:00:00 2001 From: dwithana Date: Wed, 11 Dec 2024 14:54:21 -0500 Subject: [PATCH 2/3] Add unit tests for annotation-parser functions --- src/services/annotation-parser.test.js | 104 +++++++++++++++++++++++-- src/services/annotations-parser.js | 19 +---- 2 files changed, 101 insertions(+), 22 deletions(-) diff --git a/src/services/annotation-parser.test.js b/src/services/annotation-parser.test.js index b91fdcd2..adbf004e 100644 --- a/src/services/annotation-parser.test.js +++ b/src/services/annotation-parser.test.js @@ -119,7 +119,11 @@ const externalAnnotationPage = { describe('annotation-parser', () => { describe('parseAnnotationSets', () => { - test('AnnotationPage with TextualBody annotations', () => { + test('returns null when canvasIndex is undefined', () => { + const annotations = annotationParser.parseAnnotationSets(textualBodyAnnotations); + expect(annotations).toBeNull(); + }); + test('returns annotations for AnnotationPage with TextualBody annotations', () => { const { canvasIndex, annotationSets } = annotationParser.parseAnnotationSets(textualBodyAnnotations, 0); expect(canvasIndex).toEqual(0); expect(annotationSets.length).toEqual(1); @@ -128,7 +132,7 @@ describe('annotation-parser', () => { expect(label).toEqual('Default'); }); - test('AnnotationPage without TextualBody annotations', () => { + test('returns annotations for AnnotationPage without TextualBody annotations', () => { const { canvasIndex, annotationSets } = annotationParser.parseAnnotationSets(lunchroomManners, 0); expect(canvasIndex).toEqual(0); expect(annotationSets.length).toEqual(1); @@ -138,7 +142,7 @@ describe('annotation-parser', () => { expect(label).toEqual(''); }); - test('AnnotationPage without items property', () => { + test('returns AnnotationPage info for AnnotationPage without items property', () => { const { canvasIndex, annotationSets } = annotationParser.parseAnnotationSets(externalAnnotationPage, 0); expect(canvasIndex).toEqual(0); expect(annotationSets.length).toEqual(4); @@ -151,7 +155,11 @@ describe('annotation-parser', () => { }); describe('parseAnnotationItems', () => { - test('Annotation with TextualBody type', () => { + test('returns an empty array for empty list of undefined annotaitons', () => { + expect(annotationParser.parseAnnotationItems([], 809.0)).toEqual([]); + expect(annotationParser.parseAnnotationItems()).toEqual([]); + }); + test('parses Annotation with TextualBody type', () => { const annotations = [ { type: 'Annotation', @@ -203,7 +211,7 @@ describe('annotation-parser', () => { { format: 'text/plain', purpose: ['tagging'], value: 'Unknown' }]); }); - test('Annotation with Text type', () => { + test('parses Annotation with Text type', () => { const annotations = [ { id: 'https://example.com/manifest/lunchroom_manners/canvas/1/annotation/1', @@ -233,5 +241,91 @@ describe('annotation-parser', () => { isExternal: true, }]); }); + + describe('parses Annotations with target', () => { + test('defined as a string', () => { + const annotations = [ + { + type: 'Annotation', + motivation: ['commenting', 'tagging'], + id: 'https://example.com/avannotate-test/canvas-1/canvas/page/1', + body: [ + { type: 'TextualBody', value: '[Inaudible]', format: 'text/plain', motivation: 'commenting' }, + { type: 'TextualBody', value: 'Inaudible', format: 'text/plain', motivation: 'tagging' } + ], + target: 'https://example.com/avannotate-test/canvas-1/canvas#t=52,60' + } + ]; + const items = annotationParser.parseAnnotationItems(annotations, 809.0); + + expect(items[0].times).toEqual({ start: 52, end: 60 }); + expect(items[0].canvasId).toEqual('https://example.com/avannotate-test/canvas-1/canvas'); + }); + test('defined as a FragmentSelctor', () => { + const annotations = [ + { + type: 'Annotation', + motivation: ['commenting', 'tagging'], + id: 'https://example.com/avannotate-test/canvas-1/canvas/page/1', + body: [ + { type: 'TextualBody', value: '[Inaudible]', format: 'text/plain', motivation: 'commenting' }, + { type: 'TextualBody', value: 'Inaudible', format: 'text/plain', motivation: 'tagging' } + ], + target: { + type: 'SpecificResource', + source: { + id: 'https://example.com/avannotate-test/canvas-1/canvas', + type: 'Canvas', + partOf: [{ + id: 'https://example.com/avannotate-test/manifest.json', + type: 'Manifest', + }], + }, + selector: { + type: 'FragmentSelector', + conformsTo: 'http://www.w3.org/TR/media-frags', + value: 't=52,60' + } + } + } + ]; + const items = annotationParser.parseAnnotationItems(annotations, 809.0); + + expect(items[0].times).toEqual({ start: 52, end: 60 }); + expect(items[0].canvasId).toEqual('https://example.com/avannotate-test/canvas-1/canvas'); + }); + test('defined as a PointSelector', () => { + const annotations = [ + { + type: 'Annotation', + motivation: ['commenting', 'tagging'], + id: 'https://example.com/avannotate-test/canvas-1/canvas/page/1', + body: [ + { type: 'TextualBody', value: '[Inaudible]', format: 'text/plain', motivation: 'commenting' }, + { type: 'TextualBody', value: 'Inaudible', format: 'text/plain', motivation: 'tagging' } + ], + target: { + type: 'SpecificResource', + source: { + id: 'https://example.com/avannotate-test/canvas-1/canvas', + type: 'Canvas', + partOf: [{ + id: 'https://example.com/avannotate-test/manifest.json', + type: 'Manifest', + }], + }, + selector: { + type: 'PointSelector', + t: 52 + } + } + } + ]; + const items = annotationParser.parseAnnotationItems(annotations, 809.0); + + expect(items[0].times).toEqual({ start: 52, end: undefined }); + expect(items[0].canvasId).toEqual('https://example.com/avannotate-test/canvas-1/canvas'); + }); + }); }); }); diff --git a/src/services/annotations-parser.js b/src/services/annotations-parser.js index fac6d242..50faa266 100644 --- a/src/services/annotations-parser.js +++ b/src/services/annotations-parser.js @@ -7,24 +7,14 @@ export function parseAnnotationSets(manifest, canvasIndex) { // return empty object when canvasIndex is undefined if (canvasIndex === undefined || canvasIndex < 0) { - return { - ...info, - error: 'Error fetching content' - }; + return null; } // return an error when the given Manifest doesn't have any Canvas(es) const canvases = manifest.items; - if (canvases?.length != 0) { + if (canvases?.length != 0 && canvases[canvasIndex] != undefined) { canvas = canvases[canvasIndex]; - if (canvas === undefined) { - console.error( - 'iiif-parser -> getMediaInfo() -> canvas undefined -> ', canvasIndex - ); - throw new Error(GENERIC_ERROR_MESSAGE); - } - const annotations = canvas.annotations; const duration = Number(canvas.duration); @@ -106,12 +96,7 @@ function parseSelector(selector, duration) { case 'PointSelector': times = { start: Number(selector.t), end: undefined }; break; - case 'RangeSelector': - // TODO: https://github.com/IIIF/presentation-validator/issues/172 - times = parseTimeStrings(selector.t, duration); - break; default: - times = { start: 0, end: 0 }; break; } return times; From a6ea42b238f776cf68e8d2c5ef16eac4bb2b7823 Mon Sep 17 00:00:00 2001 From: dwithana Date: Mon, 16 Dec 2024 14:49:05 -0500 Subject: [PATCH 3/3] Code review: remove redundant comment --- src/services/annotations-parser.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/services/annotations-parser.js b/src/services/annotations-parser.js index 50faa266..633e710f 100644 --- a/src/services/annotations-parser.js +++ b/src/services/annotations-parser.js @@ -10,7 +10,6 @@ export function parseAnnotationSets(manifest, canvasIndex) { return null; } - // return an error when the given Manifest doesn't have any Canvas(es) const canvases = manifest.items; if (canvases?.length != 0 && canvases[canvasIndex] != undefined) { canvas = canvases[canvasIndex];