|
| 1 | +/** |
| 2 | + * @typedef {object} MarcRuleSubfieldSpec |
| 3 | + * @property {array<string>} subfields - Array of subfields to match for suppression |
| 4 | + * @property {string} directive - Indicates whether the matching subfields |
| 5 | + * should be "include"d or "exclude"d |
| 6 | + */ |
| 7 | +/** |
| 8 | + * @typedef {object} MarcRule |
| 9 | + * @property {string} fieldTag - Single character tag broadly classifying tag (e.g. 'y') |
| 10 | + * @property {string} marcIndicatorRegExp - Stringified regex for matching a |
| 11 | + * VarField tag joined to 1st and 2nd indicators |
| 12 | + * @property {MarcRuleSubfieldSpec} subfieldSpec - How to match subfields |
| 13 | + * @property {string} directive - Whether to include/exclude if matched. |
| 14 | + */ |
| 15 | + |
| 16 | +/** |
| 17 | + * @typedef {object} SubField |
| 18 | + * @property {string} tag - Identifying tag (e.g. '6', 'a') |
| 19 | + * @property {string} content - Value of subfield |
| 20 | + */ |
| 21 | + |
| 22 | +/** |
| 23 | + * @typedef {object} VarField |
| 24 | + * * @property {string} marcTag - Three digit number classifying field (e.g. '100') |
| 25 | + * @property {string} fieldTag - Single character tag broadly classifying tag (e.g. 'y') |
| 26 | + * @property {string} content - Root level content (usually null/ignored) |
| 27 | + * @property {array<SubField>} subfields |
| 28 | + * @property {string|null} ind1 - First indicator character (space if blank) |
| 29 | + * @property {string|null} ind2 - Second indicator character (space if blank) |
| 30 | + */ |
| 31 | + |
| 32 | +/** |
| 33 | + * @typedef {object} SerializedBib |
| 34 | + * @property {string} id - Bib ID |
| 35 | + * @property {string} nyplSource - MARC source |
| 36 | + * @property {array<VarField>} fields - Array of varFields after suppression |
| 37 | + */ |
| 38 | + |
| 39 | +/** |
| 40 | + * @typedef {object} SerializedMarc |
| 41 | + * @property {SerializedBib} bib - The serialized bib object containing varFields |
| 42 | + */ |
| 43 | + |
| 44 | +const { varFieldMatches } = require('./marc-util') |
| 45 | + |
| 46 | +class MarcSerializer {} |
| 47 | + |
| 48 | +// Load rules |
| 49 | +MarcSerializer.mappingRules = require('../data/marc-rules.json') |
| 50 | + .map((rule) => { |
| 51 | + return Object.assign({}, rule, { |
| 52 | + marcIndicatorRegExp: new RegExp(rule.marcIndicatorRegExp) |
| 53 | + }) |
| 54 | + }) |
| 55 | + |
| 56 | +/** |
| 57 | + * Returns true if a field matches a given MARC rule |
| 58 | + * @param {VarField} field - MARC field to test |
| 59 | + * @param {MarcRule} rule - Rule to match against |
| 60 | + * @returns {boolean} |
| 61 | + */ |
| 62 | +MarcSerializer.varFieldMatches = varFieldMatches |
| 63 | + |
| 64 | +MarcSerializer.describeField = function (field) { |
| 65 | + return `${field.marcTag}${field.ind1 || ' '}${field.ind2 || ' '}` |
| 66 | +} |
| 67 | + |
| 68 | +/** |
| 69 | + * Finds linked 880 fields (parallel scripts) for a given field |
| 70 | + * @param {Bib} bib - Bib object containing varFields |
| 71 | + * @param {VarField} sourceField - Field to find parallels for |
| 72 | + * @returns {Array<VarField>} Array of parallel 880 fields |
| 73 | + */ |
| 74 | +MarcSerializer.findParallelFields = function (bib, sourceField) { |
| 75 | + const linkNumbers = extractLinkingNumbers(sourceField) |
| 76 | + if (linkNumbers.length === 0) return [] |
| 77 | + |
| 78 | + return bib.varFields.filter((field) => |
| 79 | + isLinked880Field(field, linkNumbers) |
| 80 | + ) |
| 81 | +} |
| 82 | + |
| 83 | +/** |
| 84 | + * Extracts linking numbers from subfield 6, removing the 880- prefix |
| 85 | + */ |
| 86 | +function extractLinkingNumbers (varField) { |
| 87 | + return (varField.subfields || []) |
| 88 | + // Is a MARC linking subfield ($6)? |
| 89 | + .filter((subfield) => subfield.tag === '6') |
| 90 | + .map((subfield) => subfield.content.replace(/^880-/, '')) |
| 91 | +} |
| 92 | + |
| 93 | +/** |
| 94 | + * Determines whether a field is an 880 field linked to any of the given numbers |
| 95 | + */ |
| 96 | +function isLinked880Field (field, linkNumbers) { |
| 97 | + if (field.marcTag !== '880' || !field.subfields) return false |
| 98 | + |
| 99 | + const fieldLinks = field.subfields |
| 100 | + // Is a MARC linking subfield ($6)? |
| 101 | + .filter((subfield) => subfield.tag === '6') |
| 102 | + .map((subfield) => subfield.content) |
| 103 | + |
| 104 | + return fieldLinks.some((link) => |
| 105 | + linkNumbers.some((linkNumber) => isMatchingLink(link, linkNumber)) |
| 106 | + ) |
| 107 | +} |
| 108 | + |
| 109 | +/** |
| 110 | + * Checks whether a link contains the link number at position 4 |
| 111 | + */ |
| 112 | +function isMatchingLink (link, linkNumber) { |
| 113 | + return link.indexOf(linkNumber) === 4 |
| 114 | +} |
| 115 | + |
| 116 | +/** |
| 117 | + * Serializes a bib with excluded fields |
| 118 | + * @param {Bib} bib - Bib to serialize |
| 119 | + * @returns {SerializedMarc} Serialized bib |
| 120 | + */ |
| 121 | +MarcSerializer.serialize = function (bib) { |
| 122 | + // Keep track of 880 parallels to exclude |
| 123 | + const excludedLinkNumbers = new Set() |
| 124 | + |
| 125 | + const serializedVarFields = bib.varFields.filter((field) => { |
| 126 | + // Check if this 880 field is linked to an excluded source |
| 127 | + if (field.marcTag === '880') { |
| 128 | + const fieldLinks = field.subfields |
| 129 | + .filter(sf => sf.tag === '6') |
| 130 | + .map(sf => sf.content) |
| 131 | + |
| 132 | + const shouldExclude = fieldLinks.some(link => |
| 133 | + Array.from(excludedLinkNumbers).some(ln => |
| 134 | + link.indexOf(ln) === 4 |
| 135 | + ) |
| 136 | + ) |
| 137 | + |
| 138 | + if (shouldExclude) return false |
| 139 | + } |
| 140 | + |
| 141 | + // Find matching rule for this field |
| 142 | + const matchingRule = MarcSerializer.mappingRules.find((rule) => |
| 143 | + MarcSerializer.varFieldMatches(field, rule) |
| 144 | + ) |
| 145 | + |
| 146 | + if (!matchingRule) return true |
| 147 | + |
| 148 | + // If field is excluded, mark its link numbers for excluding 880 parallels |
| 149 | + if (matchingRule.directive === 'exclude') { |
| 150 | + const linkNumbers = extractLinkingNumbers(field) |
| 151 | + linkNumbers.forEach((ln) => excludedLinkNumbers.add(ln)) |
| 152 | + return false |
| 153 | + } |
| 154 | + |
| 155 | + // Otherwise, keep the field |
| 156 | + return true |
| 157 | + }) |
| 158 | + |
| 159 | + return { |
| 160 | + bib: { |
| 161 | + id: bib.id, |
| 162 | + nyplSource: bib.nyplSource, |
| 163 | + fields: serializedVarFields |
| 164 | + } |
| 165 | + } |
| 166 | +} |
| 167 | + |
| 168 | +module.exports = MarcSerializer |
0 commit comments