Skip to content

Commit 1bd5653

Browse files
committed
fix: deduplicate internetMediaType elements
1 parent b009cf6 commit 1bd5653

2 files changed

Lines changed: 106 additions & 0 deletions

File tree

collection-export/strict-mods.js

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -795,6 +795,53 @@ export function convertSpeakerReleaseDetail(doc) {
795795
return doc
796796
}
797797

798+
/**
799+
* Deduplicate internetMediaType elements within physicalDescription
800+
* Items with multiple attachments often have duplicate MIME types listed.
801+
* Keep only the first occurrence of each unique value, preserving order.
802+
*
803+
* @param {Document} doc - XML DOM document
804+
* @returns {Document} Modified document
805+
*/
806+
export function deduplicateInternetMediaType(doc) {
807+
if (!doc) {
808+
return doc
809+
}
810+
811+
const physicalDescriptions = safeSelect(XPATH_CONTEXTS.PHYSICAL_DESCRIPTION, doc)
812+
const select = xpath.useNamespaces({})
813+
814+
for (let physDesc of physicalDescriptions) {
815+
const mediaTypes = select('internetMediaType', physDesc)
816+
817+
if (mediaTypes.length <= 1) {
818+
continue
819+
}
820+
821+
const seen = new Set()
822+
const toRemove = []
823+
824+
for (let mediaType of mediaTypes) {
825+
const value = mediaType.textContent.trim()
826+
827+
if (seen.has(value)) {
828+
// Duplicate, mark for removal
829+
toRemove.push(mediaType)
830+
} else {
831+
// First occurrence, keep it
832+
seen.add(value)
833+
}
834+
}
835+
836+
// Remove duplicates
837+
for (let element of toRemove) {
838+
physDesc.removeChild(element)
839+
}
840+
}
841+
842+
return doc
843+
}
844+
798845
/**
799846
* Wrap copyInformation in holdingSimple element
800847
* MODS standard requires: location/holdingSimple/copyInformation
@@ -1270,6 +1317,9 @@ export function toStrictMODS(xmlString) {
12701317
// Wrap recordInfo/languageOfCataloging with languageTerm and move authority attribute
12711318
wrapTextWithChild(doc, `${XPATH_CONTEXTS.RECORD_INFO}/${ELEMENT_NAMES.LANGUAGE_OF_CATALOGING}`, ELEMENT_NAMES.LANGUAGE_TERM, [ATTRIBUTES.AUTHORITY])
12721319

1320+
// Deduplicate internetMediaType in physicalDescription
1321+
deduplicateInternetMediaType(doc)
1322+
12731323
// Remove all empty elements (no text, no children with text)
12741324
removeEmptyElements(doc)
12751325

collection-export/strict-mods.test.js

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3218,6 +3218,62 @@ describe('Strict MODS Conversion', () => {
32183218
'Should not have direct title under relatedItem')
32193219
})
32203220

3221+
it('should deduplicate internetMediaType in physicalDescription', () => {
3222+
const input = `<xml><mods>
3223+
<titleInfo><title>Test</title></titleInfo>
3224+
<physicalDescription>
3225+
<internetMediaType>image/tiff</internetMediaType>
3226+
<internetMediaType>image/jpeg</internetMediaType>
3227+
<internetMediaType>image/tiff</internetMediaType>
3228+
<internetMediaType>image/jpeg</internetMediaType>
3229+
<digitalOrigin>reformatted digital</digitalOrigin>
3230+
</physicalDescription>
3231+
</mods></xml>`
3232+
const result = toStrictMODS(input)
3233+
3234+
// Should only have one of each type
3235+
const tiffMatches = result.match(/<internetMediaType>image\/tiff<\/internetMediaType>/g) || []
3236+
const jpegMatches = result.match(/<internetMediaType>image\/jpeg<\/internetMediaType>/g) || []
3237+
3238+
assert.strictEqual(tiffMatches.length, 1, 'Should have exactly one image/tiff')
3239+
assert.strictEqual(jpegMatches.length, 1, 'Should have exactly one image/jpeg')
3240+
})
3241+
3242+
it('should deduplicate internetMediaType while preserving order', () => {
3243+
const input = `<xml><mods>
3244+
<titleInfo><title>Test</title></titleInfo>
3245+
<physicalDescription>
3246+
<internetMediaType>image/tiff</internetMediaType>
3247+
<internetMediaType>image/jpeg</internetMediaType>
3248+
<internetMediaType>application/pdf</internetMediaType>
3249+
<internetMediaType>image/tiff</internetMediaType>
3250+
<internetMediaType>image/jpeg</internetMediaType>
3251+
</physicalDescription>
3252+
</mods></xml>`
3253+
const result = toStrictMODS(input)
3254+
3255+
// Should have each type once, in order of first appearance
3256+
const types = [...result.matchAll(/<internetMediaType>(.*?)<\/internetMediaType>/g)].map(m => m[1])
3257+
3258+
assert.deepStrictEqual(types, ['image/tiff', 'image/jpeg', 'application/pdf'],
3259+
'Should preserve order of first appearance')
3260+
})
3261+
3262+
it('should handle physicalDescription with no internetMediaType', () => {
3263+
const input = `<xml><mods>
3264+
<titleInfo><title>Test</title></titleInfo>
3265+
<physicalDescription>
3266+
<digitalOrigin>born digital</digitalOrigin>
3267+
<extent>10 pages</extent>
3268+
</physicalDescription>
3269+
</mods></xml>`
3270+
const result = toStrictMODS(input)
3271+
3272+
assert.ok(result.includes('<digitalOrigin>born digital</digitalOrigin>'), 'Should preserve digitalOrigin')
3273+
assert.ok(result.includes('<extent>10 pages</extent>'), 'Should preserve extent')
3274+
})
3275+
3276+
32213277
it('should move formBroad and formSpecific to genre', () => {
32223278
const input = `<xml><mods>
32233279
<titleInfo><title>Test Item</title></titleInfo>

0 commit comments

Comments
 (0)