Skip to content

Commit a3df070

Browse files
committed
feat: personal name elements for syllabus, ref #115
1 parent 3612689 commit a3df070

4 files changed

Lines changed: 133 additions & 3 deletions

File tree

collection-export/fixtures/syllabus-one-faculty.mods.xml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,12 @@
77
<partNumber>Fall 2026</partNumber>
88
</titleInfo>
99

10-
<genre authority="aat">syllabi</genre>
10+
<genre authority="aat" authorityURI="http://vocab.getty.edu/aat/" valueURI="http://vocab.getty.edu/aat/300028026">syllabi</genre>
1111

1212
<name type="personal">
1313
<namePart>Eric Phetteplace</namePart>
1414
<role>
15-
<roleTerm type="text" authority="marcrelator">instructor</roleTerm>
15+
<roleTerm authority="marcrelator" authorityURI="http://id.loc.gov/vocabulary/relators" valueURI="http://id.loc.gov/vocabulary/relators/tch">teacher</roleTerm>
1616
</role>
1717
</name>
1818

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<xml>
2+
<local>
3+
<courseInfo>
4+
<semester>Fall 2026</semester>
5+
<department>FASHN</department>
6+
<course>Media History</course>
7+
<faculty>Eric Phetteplace, Annemarie Haar</faculty>
8+
<section>FASHN-3600-1</section>
9+
<courseName>FASHN-360</courseName>
10+
<facultyID>ephetteplace, ahaar</facultyID>
11+
<courseinfo>Fall 2026\FASHN\Media History\Melissa Leventon\FASHN-3600-1</courseinfo>
12+
</courseInfo>
13+
<department>Fashion Design (BFA)</department>
14+
<division>Design Division</division>
15+
</local>
16+
<mods>
17+
<part>
18+
<number>87e7c816-35cd-4ae5-b443-b55eaa7f9527</number>
19+
</part>
20+
<titleInfo>
21+
<title>Fall 2026 | FASHN-360 | Media History</title>
22+
</titleInfo>
23+
</mods>
24+
</xml>

collection-export/syllabus.js

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,57 @@ import {convertPartNumbers, moveAndRenameElement, removeEmptyElements} from './s
1919
// TODO originInfo/dateIssued with month mapping for semester
2020
// TODO note with the full course info hierarchy written out
2121

22+
/**
23+
* Add a role/roleTerm child to a parent element assuming marcrelator authority.
24+
* Used by personalNames and corporateName functions.
25+
* @param {Element} parent Parent (name) element to which the roleTerm is added
26+
* @param {string} roleTerm Text content of the roleTerm element
27+
* @param {string} valueURI URI for the roleTerm (optional)
28+
* @return {Element|null} The created roleTerm element, or null if parent or roleTerm is not provided
29+
*/
30+
export function addRoleTerm(parent, roleTerm, valueURI = 'marcrelator') {
31+
if (!parent || !roleTerm) return null
32+
33+
const roleElement = createElement(parent.ownerDocument, 'role')
34+
parent.appendChild(roleElement)
35+
const roleTermElement = createElement(parent.ownerDocument, 'roleTerm')
36+
roleTermElement.textContent = roleTerm
37+
38+
// Set attributes
39+
roleTermElement.setAttribute('authority', 'marcrelator')
40+
roleTermElement.setAttribute('authorityURI', 'http://id.loc.gov/vocabulary/relators')
41+
if (valueURI) roleTermElement.setAttribute('valueURI', valueURI)
42+
43+
roleElement.appendChild(roleTermElement)
44+
45+
return roleTermElement
46+
}
47+
48+
/**
49+
* Adds personal name elements for all faculty
50+
* @param {Document} doc XML document
51+
* @returns {Document} transformed document
52+
*/
53+
export function personalNames(doc) {
54+
const facultyElement = safeSelectFirst("//local/courseInfo/faculty", doc)
55+
const mods = safeSelectFirst("//mods", doc)
56+
57+
if (hasDirectTextContent(facultyElement)) {
58+
const names = facultyElement.textContent.split(', ').map(name => name.trim()).filter(name => name.length > 0)
59+
names.forEach(name => {
60+
let nameElement = createElement(doc, 'name')
61+
nameElement.setAttribute('type', 'personal')
62+
let namePart = createElement(doc, 'namePart')
63+
namePart.textContent = name
64+
nameElement.appendChild(namePart)
65+
mods.appendChild(nameElement)
66+
addRoleTerm(nameElement, 'teacher', 'http://id.loc.gov/vocabulary/relators/tch')
67+
})
68+
}
69+
70+
return doc
71+
}
72+
2273
/**
2374
* Adds a static mods/genre = 'syllabi' element
2475
*
@@ -200,6 +251,8 @@ export function convertSyllabusXMLtoMODS(xmlString) {
200251
addSubjects(doc)
201252
addIdentifiers(doc)
202253

254+
personalNames(doc)
255+
203256
// Remove empty elements last, after all transformations are complete
204257
removeEmptyElements(doc)
205258

collection-export/syllabus.test.js

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,64 @@
11
import assert from 'node:assert'
22
import {describe, it} from 'mocha'
33
import xpath from 'xpath'
4-
import {convertSyllabusXMLtoMODS} from './syllabus.js'
4+
import {addRoleTerm, convertSyllabusXMLtoMODS} from './syllabus.js'
5+
import {DOMParser} from '@xmldom/xmldom'
56

67
// helper function to wrap XML in a root </xml> element
78
const x = (xml) => `<xml>${xml}</xml>`
89

10+
describe('addRoleTerm', () => {
11+
const authority = 'marcrelator'
12+
const authorityURI = 'http://id.loc.gov/vocabulary/relators'
13+
14+
it('should add a role/roleTerm element with authority and type attributes', async () => {
15+
const doc = new DOMParser().parseFromString('<mods></mods>', 'text/xml')
16+
const valueURI = 'http://id.loc.gov/vocabulary/relators/tch'
17+
addRoleTerm(doc.documentElement, 'teacher', valueURI)
18+
const roleTerm = xpath.select1('//mods/role/roleTerm', doc)
19+
assert.ok(roleTerm)
20+
assert.strictEqual(roleTerm.textContent, 'teacher')
21+
assert.strictEqual(roleTerm.getAttribute('authority'), authority)
22+
assert.strictEqual(roleTerm.getAttribute('authorityURI'), authorityURI)
23+
assert.strictEqual(roleTerm.getAttribute('valueURI'), valueURI)
24+
})
25+
26+
it('should work without a valueURI', async () => {
27+
const doc = new DOMParser().parseFromString('<mods></mods>', 'text/xml')
28+
addRoleTerm(doc.documentElement, 'teacher')
29+
const roleTerm = xpath.select1('//mods/role/roleTerm', doc)
30+
assert.ok(roleTerm)
31+
assert.strictEqual(roleTerm.textContent, 'teacher')
32+
assert.strictEqual(roleTerm.getAttribute('authority'), authority)
33+
assert.strictEqual(roleTerm.getAttribute('authorityURI'), authorityURI)
34+
})
35+
})
36+
37+
describe('personalNames', () => {
38+
it('should add a single personal name from local/faculty elements', async () => {
39+
const inputXML = x(`<local><courseInfo><faculty>John Doe</faculty></courseInfo></local>`)
40+
const result = convertSyllabusXMLtoMODS(inputXML)
41+
const nameElement = xpath.select1('//mods/name[@type="personal"]', result)
42+
assert.ok(nameElement)
43+
const namePart = xpath.select1('namePart', nameElement)
44+
assert.ok(namePart)
45+
assert.strictEqual(namePart.textContent, 'John Doe')
46+
})
47+
48+
it('should add multiple personal names from local/faculty elements', async () => {
49+
const inputXML = x(`<local><courseInfo><faculty>John Doe, Jane Smith</faculty></courseInfo></local>`)
50+
const result = convertSyllabusXMLtoMODS(inputXML)
51+
const nameElements = xpath.select('//mods/name[@type="personal"]', result)
52+
assert.strictEqual(nameElements.length, 2)
53+
const namePart1 = xpath.select1('namePart', nameElements[0])
54+
assert.ok(namePart1)
55+
assert.strictEqual(namePart1.textContent, 'John Doe')
56+
const namePart2 = xpath.select1('namePart', nameElements[1])
57+
assert.ok(namePart2)
58+
assert.strictEqual(namePart2.textContent, 'Jane Smith')
59+
})
60+
})
61+
962
describe('addIdentifiers', () => {
1063
it('should add course number and section identifiers', async () => {
1164
const courseNumber = 'ARTED-101'

0 commit comments

Comments
 (0)