@biblioteksentralen/marc 0.0.2 → 0.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -4
- package/dist/index.cjs +130 -52
- package/dist/index.d.cts +25 -5
- package/dist/index.d.ts +25 -5
- package/dist/index.js +129 -54
- package/package.json +8 -8
package/README.md
CHANGED
|
@@ -2,14 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
Package for representating MARC records in TypeScript and serialize to/from MARC XML and JSON.
|
|
4
4
|
|
|
5
|
-
The JSON serialization is compatible with schema defined by <https://www.npmjs.com/package/@natlibfi/marc-record>
|
|
5
|
+
The JSON serialization is compatible with the schema defined by <https://www.npmjs.com/package/@natlibfi/marc-record>
|
|
6
6
|
|
|
7
7
|
## Usage
|
|
8
8
|
|
|
9
|
-
### Parsing XML
|
|
9
|
+
### Parsing and serializing XML
|
|
10
10
|
|
|
11
11
|
```ts
|
|
12
|
-
import { parseMarcXml } from "@biblioteksentralen/marc";
|
|
12
|
+
import { parseMarcXml, serializeMarcXml } from "@biblioteksentralen/marc";
|
|
13
13
|
|
|
14
14
|
const xmlRecord = `
|
|
15
15
|
<record xmlns="http://www.loc.gov/MARC21/slim">
|
|
@@ -38,9 +38,32 @@ const title = record
|
|
|
38
38
|
.getSubfieldValues("245", /a|b|n|p/)
|
|
39
39
|
.join(" ")
|
|
40
40
|
?.trim();
|
|
41
|
+
|
|
42
|
+
const xml = serializeMarcXml(record);
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Serializing Line MARC
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
import {
|
|
49
|
+
createControlField,
|
|
50
|
+
createDataField,
|
|
51
|
+
createSubfield,
|
|
52
|
+
serializeLineMarc
|
|
53
|
+
} from "@biblioteksentralen/marc";
|
|
54
|
+
|
|
55
|
+
const record = new MarcRecord({
|
|
56
|
+
leader: "...",
|
|
57
|
+
fields: [
|
|
58
|
+
createControlField(...),
|
|
59
|
+
createDataField(...),
|
|
60
|
+
]
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const lineMarc = serializeLineMarc(record);
|
|
41
64
|
```
|
|
42
65
|
|
|
43
|
-
### Serializing
|
|
66
|
+
### Serializing and deserializing as JSON
|
|
44
67
|
|
|
45
68
|
The MarcRecord class can be JSON serialized:
|
|
46
69
|
|
package/dist/index.cjs
CHANGED
|
@@ -199,9 +199,10 @@ var MarcRecord = class _MarcRecord {
|
|
|
199
199
|
this.leader = leader;
|
|
200
200
|
this.fields = fields;
|
|
201
201
|
}
|
|
202
|
-
static fromJSON(data) {
|
|
202
|
+
static fromJSON(data, log) {
|
|
203
203
|
const { format, leader, fields } = this.validateJSON(
|
|
204
|
-
fixInvalidMarcRecordSerialization(data)
|
|
204
|
+
fixInvalidMarcRecordSerialization(data),
|
|
205
|
+
log
|
|
205
206
|
);
|
|
206
207
|
return new _MarcRecord({
|
|
207
208
|
leader,
|
|
@@ -211,10 +212,16 @@ var MarcRecord = class _MarcRecord {
|
|
|
211
212
|
)
|
|
212
213
|
});
|
|
213
214
|
}
|
|
214
|
-
static validateJSON(data) {
|
|
215
|
+
static validateJSON(data, log) {
|
|
215
216
|
if (validator(data)) {
|
|
216
217
|
return data;
|
|
217
218
|
}
|
|
219
|
+
log.error(
|
|
220
|
+
`MarcRecord validation failed:
|
|
221
|
+
${validator.errors ? JSON.stringify(validator.errors) : "Unknown error"}.
|
|
222
|
+
Data:
|
|
223
|
+
${JSON.stringify(data)}`
|
|
224
|
+
);
|
|
218
225
|
throw new ValidationFailed(validator.errors ?? []);
|
|
219
226
|
}
|
|
220
227
|
getControlFields() {
|
|
@@ -248,73 +255,79 @@ var MarcRecord = class _MarcRecord {
|
|
|
248
255
|
fields: this.fields.map((field) => field.toJSON())
|
|
249
256
|
};
|
|
250
257
|
}
|
|
251
|
-
toString() {
|
|
252
|
-
return this.fields.map((field) => field.toString()).join("\n");
|
|
253
|
-
}
|
|
254
258
|
};
|
|
255
259
|
|
|
256
|
-
// src/marc-record/
|
|
257
|
-
var
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
// the same schema as the original MARC 21 XML schema, but weakens restrictions to support
|
|
262
|
-
// other dialects than MARC 21 (not excluding the most esoteric ones).
|
|
263
|
-
"info:lc/xmlns/marcxchange-v1",
|
|
264
|
-
// Version 2 of MarcXchange adds support of embedded data, one of the many
|
|
265
|
-
// advanced XML features that a poor developer hopes not to encounter in the wild.
|
|
266
|
-
// Also weakens restrictions even further so that even a completely empty record is valid.
|
|
267
|
-
"info:lc/xmlns/marcxchange-v2"
|
|
268
|
-
];
|
|
269
|
-
function detectNamespace(input) {
|
|
270
|
-
for (const possibleNamespace of marcXmlNamespaces) {
|
|
271
|
-
if (input.indexOf(possibleNamespace) !== -1) {
|
|
272
|
-
return possibleNamespace;
|
|
273
|
-
}
|
|
260
|
+
// src/marc-record/MarcParseError.ts
|
|
261
|
+
var MarcParseError = class extends Error {
|
|
262
|
+
constructor(message, record) {
|
|
263
|
+
super(message);
|
|
264
|
+
this.record = record;
|
|
274
265
|
}
|
|
275
|
-
|
|
276
|
-
}
|
|
266
|
+
};
|
|
277
267
|
|
|
278
268
|
// src/marc-record/parseMarcXml.ts
|
|
279
|
-
function parseMarcXml(input, options = {}) {
|
|
280
|
-
const
|
|
281
|
-
const xmlRecord = xmlUtils.parseXml(input, {
|
|
282
|
-
namespaces: {
|
|
283
|
-
marc: namespace
|
|
284
|
-
}
|
|
285
|
-
});
|
|
269
|
+
async function parseMarcXml(input, options = {}) {
|
|
270
|
+
const xmlRecord = typeof input === "string" ? await xmlUtils.parseXml(input) : input;
|
|
286
271
|
const processControlField = options.processControlField ?? ((field) => field);
|
|
287
272
|
const processDataField = options.processDataField ?? ((field) => field);
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
273
|
+
const { requireFields = true } = options;
|
|
274
|
+
const records = getRecords(xmlRecord);
|
|
275
|
+
return records.map((marcRecord) => {
|
|
276
|
+
const parseError = (message) => new MarcParseError(message, xmlUtils.serializeXml(marcRecord));
|
|
277
|
+
const leader = marcRecord.text("leader");
|
|
278
|
+
if (!leader) {
|
|
279
|
+
throw parseError("MARC record is missing leader");
|
|
280
|
+
}
|
|
281
|
+
const fields = marcRecord.children(/controlfield|datafield/).reduce((fields2, field) => {
|
|
291
282
|
const tag = field.attr("tag");
|
|
292
|
-
if (!tag)
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
283
|
+
if (!tag) return fields2;
|
|
284
|
+
if (field.name === "controlfield") {
|
|
285
|
+
const value = field.text();
|
|
286
|
+
if (!value && options.strict) {
|
|
287
|
+
throw parseError("MARC record control fields cannot be empty");
|
|
288
|
+
}
|
|
289
|
+
const newField = value ? processControlField(new ControlField(tag, value)) : void 0;
|
|
298
290
|
return newField ? [...fields2, newField] : fields2;
|
|
299
291
|
} else {
|
|
300
|
-
const subfields = field.
|
|
292
|
+
const subfields = field.children("subfield").reduce((subfields2, subfield) => {
|
|
301
293
|
const code = subfield.attr("code");
|
|
302
|
-
|
|
294
|
+
const value = subfield.text();
|
|
295
|
+
return code && value ? [...subfields2, new Subfield(code, value)] : subfields2;
|
|
303
296
|
}, []);
|
|
304
|
-
const
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
297
|
+
const ind1 = field.attr("ind1");
|
|
298
|
+
const ind2 = field.attr("ind2");
|
|
299
|
+
if (options.strict && (ind1 === void 0 || ind2 === void 0)) {
|
|
300
|
+
throw parseError("MARC record data fields must have indicators");
|
|
301
|
+
}
|
|
302
|
+
if (subfields.length === 0 && options.strict) {
|
|
303
|
+
throw parseError(
|
|
304
|
+
"MARC record data fields must have at least one subfield"
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
const newField = subfields.length ? processDataField(
|
|
308
|
+
new DataField(tag, ind1 ?? " ", ind2 ?? " ", subfields)
|
|
309
|
+
) : void 0;
|
|
312
310
|
return newField ? [...fields2, newField] : fields2;
|
|
313
311
|
}
|
|
314
312
|
}, []);
|
|
313
|
+
if (fields.length === 0 && requireFields) {
|
|
314
|
+
throw parseError("MARC record must have at least one field");
|
|
315
|
+
}
|
|
315
316
|
return new MarcRecord({ leader, fields, format: options.format });
|
|
316
317
|
});
|
|
317
318
|
}
|
|
319
|
+
function getRecords(node) {
|
|
320
|
+
switch (node.name) {
|
|
321
|
+
case "record":
|
|
322
|
+
return [node];
|
|
323
|
+
case "collection":
|
|
324
|
+
// MarcXchange
|
|
325
|
+
case "metadata":
|
|
326
|
+
return node.children("record");
|
|
327
|
+
default:
|
|
328
|
+
return [];
|
|
329
|
+
}
|
|
330
|
+
}
|
|
318
331
|
var controlFieldSchema = zod.z.object({
|
|
319
332
|
tag: zod.z.string().length(3, "MARC tag must be three characters long"),
|
|
320
333
|
value: zod.z.string()
|
|
@@ -331,11 +344,76 @@ var marcRecordZodSchema = zod.z.object({
|
|
|
331
344
|
fields: zod.z.array(zod.z.union([controlFieldSchema, dataFieldSchema]))
|
|
332
345
|
});
|
|
333
346
|
|
|
347
|
+
// src/marc-record/serializeLineMarc.ts
|
|
348
|
+
function serializeLineMarc(input) {
|
|
349
|
+
const leader = serializer.leader(input.leader);
|
|
350
|
+
const control = input.getControlFields().map(serializer.controlfield).join("");
|
|
351
|
+
const data = input.getDataFields().map(serializer.datafield).join("");
|
|
352
|
+
return `${leader}${control}${data}^
|
|
353
|
+
`;
|
|
354
|
+
}
|
|
355
|
+
var serializer = {
|
|
356
|
+
leader: (leader) => `*LDR${leader}
|
|
357
|
+
`,
|
|
358
|
+
controlfield: (field) => `*${field.tag}${field.value}
|
|
359
|
+
`,
|
|
360
|
+
datafield: (field) => {
|
|
361
|
+
const ind1 = field.ind1 ?? " ";
|
|
362
|
+
const ind2 = field.ind2 ?? " ";
|
|
363
|
+
const subfields = field.subfields.map(
|
|
364
|
+
(subfield) => `$${subfield.code}${escapeSubfieldValue(subfield.value)}`
|
|
365
|
+
).join("");
|
|
366
|
+
return `*${field.tag}${ind1}${ind2}${subfields}
|
|
367
|
+
`;
|
|
368
|
+
}
|
|
369
|
+
};
|
|
370
|
+
var escapeSubfieldValue = (value) => {
|
|
371
|
+
return value.replace(/\n/g, " ").replace(/\$/g, "@{2}");
|
|
372
|
+
};
|
|
373
|
+
function serializeMarcXml(input, pretty = false) {
|
|
374
|
+
const fields = [
|
|
375
|
+
xmlUtils.createXmlElement("leader", { text: input.leader }),
|
|
376
|
+
...input.getControlFields().map(
|
|
377
|
+
(field) => xmlUtils.createXmlElement("controlfield", {
|
|
378
|
+
attributes: { tag: field.tag },
|
|
379
|
+
text: field.value
|
|
380
|
+
})
|
|
381
|
+
),
|
|
382
|
+
...input.getDataFields().map(
|
|
383
|
+
(field) => xmlUtils.createXmlElement("datafield", {
|
|
384
|
+
attributes: withoutEmptyValues({
|
|
385
|
+
tag: field.tag,
|
|
386
|
+
ind1: field.ind1,
|
|
387
|
+
ind2: field.ind2
|
|
388
|
+
}),
|
|
389
|
+
children: field.subfields.map(
|
|
390
|
+
(subfield) => xmlUtils.createXmlElement("subfield", {
|
|
391
|
+
attributes: { code: subfield.code },
|
|
392
|
+
text: subfield.value
|
|
393
|
+
})
|
|
394
|
+
)
|
|
395
|
+
})
|
|
396
|
+
)
|
|
397
|
+
];
|
|
398
|
+
const recordNode = xmlUtils.createXmlElement("record", {
|
|
399
|
+
attributes: { xmlns: "http://www.loc.gov/MARC21/slim" },
|
|
400
|
+
children: fields
|
|
401
|
+
});
|
|
402
|
+
return xmlUtils.serializeXml(recordNode, pretty);
|
|
403
|
+
}
|
|
404
|
+
var withoutEmptyValues = (obj) => Object.keys(obj).reduce(
|
|
405
|
+
(acc, key) => obj[key] === void 0 ? { ...acc } : { ...acc, [key]: obj[key] },
|
|
406
|
+
{}
|
|
407
|
+
);
|
|
408
|
+
|
|
334
409
|
exports.ControlField = ControlField;
|
|
335
410
|
exports.DataField = DataField;
|
|
411
|
+
exports.MarcParseError = MarcParseError;
|
|
336
412
|
exports.MarcRecord = MarcRecord;
|
|
337
413
|
exports.Subfield = Subfield;
|
|
338
414
|
exports.createControlField = createControlField;
|
|
339
415
|
exports.createDataField = createDataField;
|
|
340
416
|
exports.marcRecordZodSchema = marcRecordZodSchema;
|
|
341
417
|
exports.parseMarcXml = parseMarcXml;
|
|
418
|
+
exports.serializeLineMarc = serializeLineMarc;
|
|
419
|
+
exports.serializeMarcXml = serializeMarcXml;
|
package/dist/index.d.cts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { XmlElement } from '@biblioteksentralen/xml-utils';
|
|
2
|
+
import { Logger } from 'ts-log';
|
|
1
3
|
import { z } from 'zod';
|
|
2
4
|
|
|
3
5
|
type SerializedMarcField = SerializedDataField | SerializedControlField;
|
|
@@ -72,8 +74,8 @@ declare class MarcRecord {
|
|
|
72
74
|
fields: MarcField[];
|
|
73
75
|
format?: string;
|
|
74
76
|
});
|
|
75
|
-
static fromJSON(data: unknown): MarcRecord;
|
|
76
|
-
static validateJSON(data: unknown): SerializedMarcRecord;
|
|
77
|
+
static fromJSON(data: unknown, log: Logger): MarcRecord;
|
|
78
|
+
static validateJSON(data: unknown, log: Logger): SerializedMarcRecord;
|
|
77
79
|
getControlFields(): ControlField[];
|
|
78
80
|
getControlField(tag: string): ControlField | undefined;
|
|
79
81
|
getDataFields(tag?: string | RegExp, indicators?: Indicators): DataField[];
|
|
@@ -82,7 +84,6 @@ declare class MarcRecord {
|
|
|
82
84
|
getSubfieldValues(tag: string | RegExp, code: string | RegExp, indicators?: Indicators): string[];
|
|
83
85
|
getFirstSubfieldValue(tag: string | RegExp, code: string | RegExp, indicators?: Indicators): string | undefined;
|
|
84
86
|
toJSON(): SerializedMarcRecord;
|
|
85
|
-
toString(): string;
|
|
86
87
|
}
|
|
87
88
|
|
|
88
89
|
interface MarcXmlOptions {
|
|
@@ -99,8 +100,18 @@ interface MarcXmlOptions {
|
|
|
99
100
|
* Free-form string that specifies the MARC record flavour.
|
|
100
101
|
*/
|
|
101
102
|
format?: string;
|
|
103
|
+
/**
|
|
104
|
+
* Whether to require at least one field to be present. Defaults to true.
|
|
105
|
+
*/
|
|
106
|
+
requireFields?: boolean;
|
|
107
|
+
/**
|
|
108
|
+
* If set to true, the parser throws an error if a record is missing indicators or have empty
|
|
109
|
+
* fields or subfields. If set to false, it will set default values for missing indicators and
|
|
110
|
+
* skip empty fields and subfields. Defaults to false.
|
|
111
|
+
*/
|
|
112
|
+
strict?: boolean;
|
|
102
113
|
}
|
|
103
|
-
declare function parseMarcXml(input: string, options?: MarcXmlOptions): MarcRecord[]
|
|
114
|
+
declare function parseMarcXml(input: string | XmlElement, options?: MarcXmlOptions): Promise<MarcRecord[]>;
|
|
104
115
|
|
|
105
116
|
declare const marcRecordZodSchema: z.ZodObject<{
|
|
106
117
|
format: z.ZodOptional<z.ZodString>;
|
|
@@ -177,4 +188,13 @@ declare const marcRecordZodSchema: z.ZodObject<{
|
|
|
177
188
|
format?: string | undefined;
|
|
178
189
|
}>;
|
|
179
190
|
|
|
180
|
-
|
|
191
|
+
declare function serializeLineMarc(input: MarcRecord): string;
|
|
192
|
+
|
|
193
|
+
declare function serializeMarcXml(input: MarcRecord, pretty?: boolean): string;
|
|
194
|
+
|
|
195
|
+
declare class MarcParseError extends Error {
|
|
196
|
+
readonly record: string;
|
|
197
|
+
constructor(message: string, record: string);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export { ControlField, DataField, type MarcField, MarcParseError, MarcRecord, type SerializedControlField, type SerializedDataField, type SerializedMarcField, type SerializedMarcRecord, Subfield, createControlField, createDataField, marcRecordZodSchema, parseMarcXml, serializeLineMarc, serializeMarcXml };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { XmlElement } from '@biblioteksentralen/xml-utils';
|
|
2
|
+
import { Logger } from 'ts-log';
|
|
1
3
|
import { z } from 'zod';
|
|
2
4
|
|
|
3
5
|
type SerializedMarcField = SerializedDataField | SerializedControlField;
|
|
@@ -72,8 +74,8 @@ declare class MarcRecord {
|
|
|
72
74
|
fields: MarcField[];
|
|
73
75
|
format?: string;
|
|
74
76
|
});
|
|
75
|
-
static fromJSON(data: unknown): MarcRecord;
|
|
76
|
-
static validateJSON(data: unknown): SerializedMarcRecord;
|
|
77
|
+
static fromJSON(data: unknown, log: Logger): MarcRecord;
|
|
78
|
+
static validateJSON(data: unknown, log: Logger): SerializedMarcRecord;
|
|
77
79
|
getControlFields(): ControlField[];
|
|
78
80
|
getControlField(tag: string): ControlField | undefined;
|
|
79
81
|
getDataFields(tag?: string | RegExp, indicators?: Indicators): DataField[];
|
|
@@ -82,7 +84,6 @@ declare class MarcRecord {
|
|
|
82
84
|
getSubfieldValues(tag: string | RegExp, code: string | RegExp, indicators?: Indicators): string[];
|
|
83
85
|
getFirstSubfieldValue(tag: string | RegExp, code: string | RegExp, indicators?: Indicators): string | undefined;
|
|
84
86
|
toJSON(): SerializedMarcRecord;
|
|
85
|
-
toString(): string;
|
|
86
87
|
}
|
|
87
88
|
|
|
88
89
|
interface MarcXmlOptions {
|
|
@@ -99,8 +100,18 @@ interface MarcXmlOptions {
|
|
|
99
100
|
* Free-form string that specifies the MARC record flavour.
|
|
100
101
|
*/
|
|
101
102
|
format?: string;
|
|
103
|
+
/**
|
|
104
|
+
* Whether to require at least one field to be present. Defaults to true.
|
|
105
|
+
*/
|
|
106
|
+
requireFields?: boolean;
|
|
107
|
+
/**
|
|
108
|
+
* If set to true, the parser throws an error if a record is missing indicators or have empty
|
|
109
|
+
* fields or subfields. If set to false, it will set default values for missing indicators and
|
|
110
|
+
* skip empty fields and subfields. Defaults to false.
|
|
111
|
+
*/
|
|
112
|
+
strict?: boolean;
|
|
102
113
|
}
|
|
103
|
-
declare function parseMarcXml(input: string, options?: MarcXmlOptions): MarcRecord[]
|
|
114
|
+
declare function parseMarcXml(input: string | XmlElement, options?: MarcXmlOptions): Promise<MarcRecord[]>;
|
|
104
115
|
|
|
105
116
|
declare const marcRecordZodSchema: z.ZodObject<{
|
|
106
117
|
format: z.ZodOptional<z.ZodString>;
|
|
@@ -177,4 +188,13 @@ declare const marcRecordZodSchema: z.ZodObject<{
|
|
|
177
188
|
format?: string | undefined;
|
|
178
189
|
}>;
|
|
179
190
|
|
|
180
|
-
|
|
191
|
+
declare function serializeLineMarc(input: MarcRecord): string;
|
|
192
|
+
|
|
193
|
+
declare function serializeMarcXml(input: MarcRecord, pretty?: boolean): string;
|
|
194
|
+
|
|
195
|
+
declare class MarcParseError extends Error {
|
|
196
|
+
readonly record: string;
|
|
197
|
+
constructor(message: string, record: string);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export { ControlField, DataField, type MarcField, MarcParseError, MarcRecord, type SerializedControlField, type SerializedDataField, type SerializedMarcField, type SerializedMarcRecord, Subfield, createControlField, createDataField, marcRecordZodSchema, parseMarcXml, serializeLineMarc, serializeMarcXml };
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { parseXml } from '@biblioteksentralen/xml-utils';
|
|
1
|
+
import { parseXml, createXmlElement, serializeXml } from '@biblioteksentralen/xml-utils';
|
|
2
2
|
import { Ajv } from 'ajv';
|
|
3
3
|
import { z } from 'zod';
|
|
4
4
|
|
|
@@ -197,9 +197,10 @@ var MarcRecord = class _MarcRecord {
|
|
|
197
197
|
this.leader = leader;
|
|
198
198
|
this.fields = fields;
|
|
199
199
|
}
|
|
200
|
-
static fromJSON(data) {
|
|
200
|
+
static fromJSON(data, log) {
|
|
201
201
|
const { format, leader, fields } = this.validateJSON(
|
|
202
|
-
fixInvalidMarcRecordSerialization(data)
|
|
202
|
+
fixInvalidMarcRecordSerialization(data),
|
|
203
|
+
log
|
|
203
204
|
);
|
|
204
205
|
return new _MarcRecord({
|
|
205
206
|
leader,
|
|
@@ -209,10 +210,16 @@ var MarcRecord = class _MarcRecord {
|
|
|
209
210
|
)
|
|
210
211
|
});
|
|
211
212
|
}
|
|
212
|
-
static validateJSON(data) {
|
|
213
|
+
static validateJSON(data, log) {
|
|
213
214
|
if (validator(data)) {
|
|
214
215
|
return data;
|
|
215
216
|
}
|
|
217
|
+
log.error(
|
|
218
|
+
`MarcRecord validation failed:
|
|
219
|
+
${validator.errors ? JSON.stringify(validator.errors) : "Unknown error"}.
|
|
220
|
+
Data:
|
|
221
|
+
${JSON.stringify(data)}`
|
|
222
|
+
);
|
|
216
223
|
throw new ValidationFailed(validator.errors ?? []);
|
|
217
224
|
}
|
|
218
225
|
getControlFields() {
|
|
@@ -246,73 +253,79 @@ var MarcRecord = class _MarcRecord {
|
|
|
246
253
|
fields: this.fields.map((field) => field.toJSON())
|
|
247
254
|
};
|
|
248
255
|
}
|
|
249
|
-
toString() {
|
|
250
|
-
return this.fields.map((field) => field.toString()).join("\n");
|
|
251
|
-
}
|
|
252
256
|
};
|
|
253
257
|
|
|
254
|
-
// src/marc-record/
|
|
255
|
-
var
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
// the same schema as the original MARC 21 XML schema, but weakens restrictions to support
|
|
260
|
-
// other dialects than MARC 21 (not excluding the most esoteric ones).
|
|
261
|
-
"info:lc/xmlns/marcxchange-v1",
|
|
262
|
-
// Version 2 of MarcXchange adds support of embedded data, one of the many
|
|
263
|
-
// advanced XML features that a poor developer hopes not to encounter in the wild.
|
|
264
|
-
// Also weakens restrictions even further so that even a completely empty record is valid.
|
|
265
|
-
"info:lc/xmlns/marcxchange-v2"
|
|
266
|
-
];
|
|
267
|
-
function detectNamespace(input) {
|
|
268
|
-
for (const possibleNamespace of marcXmlNamespaces) {
|
|
269
|
-
if (input.indexOf(possibleNamespace) !== -1) {
|
|
270
|
-
return possibleNamespace;
|
|
271
|
-
}
|
|
258
|
+
// src/marc-record/MarcParseError.ts
|
|
259
|
+
var MarcParseError = class extends Error {
|
|
260
|
+
constructor(message, record) {
|
|
261
|
+
super(message);
|
|
262
|
+
this.record = record;
|
|
272
263
|
}
|
|
273
|
-
|
|
274
|
-
}
|
|
264
|
+
};
|
|
275
265
|
|
|
276
266
|
// src/marc-record/parseMarcXml.ts
|
|
277
|
-
function parseMarcXml(input, options = {}) {
|
|
278
|
-
const
|
|
279
|
-
const xmlRecord = parseXml(input, {
|
|
280
|
-
namespaces: {
|
|
281
|
-
marc: namespace
|
|
282
|
-
}
|
|
283
|
-
});
|
|
267
|
+
async function parseMarcXml(input, options = {}) {
|
|
268
|
+
const xmlRecord = typeof input === "string" ? await parseXml(input) : input;
|
|
284
269
|
const processControlField = options.processControlField ?? ((field) => field);
|
|
285
270
|
const processDataField = options.processDataField ?? ((field) => field);
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
271
|
+
const { requireFields = true } = options;
|
|
272
|
+
const records = getRecords(xmlRecord);
|
|
273
|
+
return records.map((marcRecord) => {
|
|
274
|
+
const parseError = (message) => new MarcParseError(message, serializeXml(marcRecord));
|
|
275
|
+
const leader = marcRecord.text("leader");
|
|
276
|
+
if (!leader) {
|
|
277
|
+
throw parseError("MARC record is missing leader");
|
|
278
|
+
}
|
|
279
|
+
const fields = marcRecord.children(/controlfield|datafield/).reduce((fields2, field) => {
|
|
289
280
|
const tag = field.attr("tag");
|
|
290
|
-
if (!tag)
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
281
|
+
if (!tag) return fields2;
|
|
282
|
+
if (field.name === "controlfield") {
|
|
283
|
+
const value = field.text();
|
|
284
|
+
if (!value && options.strict) {
|
|
285
|
+
throw parseError("MARC record control fields cannot be empty");
|
|
286
|
+
}
|
|
287
|
+
const newField = value ? processControlField(new ControlField(tag, value)) : void 0;
|
|
296
288
|
return newField ? [...fields2, newField] : fields2;
|
|
297
289
|
} else {
|
|
298
|
-
const subfields = field.
|
|
290
|
+
const subfields = field.children("subfield").reduce((subfields2, subfield) => {
|
|
299
291
|
const code = subfield.attr("code");
|
|
300
|
-
|
|
292
|
+
const value = subfield.text();
|
|
293
|
+
return code && value ? [...subfields2, new Subfield(code, value)] : subfields2;
|
|
301
294
|
}, []);
|
|
302
|
-
const
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
295
|
+
const ind1 = field.attr("ind1");
|
|
296
|
+
const ind2 = field.attr("ind2");
|
|
297
|
+
if (options.strict && (ind1 === void 0 || ind2 === void 0)) {
|
|
298
|
+
throw parseError("MARC record data fields must have indicators");
|
|
299
|
+
}
|
|
300
|
+
if (subfields.length === 0 && options.strict) {
|
|
301
|
+
throw parseError(
|
|
302
|
+
"MARC record data fields must have at least one subfield"
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
const newField = subfields.length ? processDataField(
|
|
306
|
+
new DataField(tag, ind1 ?? " ", ind2 ?? " ", subfields)
|
|
307
|
+
) : void 0;
|
|
310
308
|
return newField ? [...fields2, newField] : fields2;
|
|
311
309
|
}
|
|
312
310
|
}, []);
|
|
311
|
+
if (fields.length === 0 && requireFields) {
|
|
312
|
+
throw parseError("MARC record must have at least one field");
|
|
313
|
+
}
|
|
313
314
|
return new MarcRecord({ leader, fields, format: options.format });
|
|
314
315
|
});
|
|
315
316
|
}
|
|
317
|
+
function getRecords(node) {
|
|
318
|
+
switch (node.name) {
|
|
319
|
+
case "record":
|
|
320
|
+
return [node];
|
|
321
|
+
case "collection":
|
|
322
|
+
// MarcXchange
|
|
323
|
+
case "metadata":
|
|
324
|
+
return node.children("record");
|
|
325
|
+
default:
|
|
326
|
+
return [];
|
|
327
|
+
}
|
|
328
|
+
}
|
|
316
329
|
var controlFieldSchema = z.object({
|
|
317
330
|
tag: z.string().length(3, "MARC tag must be three characters long"),
|
|
318
331
|
value: z.string()
|
|
@@ -329,4 +342,66 @@ var marcRecordZodSchema = z.object({
|
|
|
329
342
|
fields: z.array(z.union([controlFieldSchema, dataFieldSchema]))
|
|
330
343
|
});
|
|
331
344
|
|
|
332
|
-
|
|
345
|
+
// src/marc-record/serializeLineMarc.ts
|
|
346
|
+
function serializeLineMarc(input) {
|
|
347
|
+
const leader = serializer.leader(input.leader);
|
|
348
|
+
const control = input.getControlFields().map(serializer.controlfield).join("");
|
|
349
|
+
const data = input.getDataFields().map(serializer.datafield).join("");
|
|
350
|
+
return `${leader}${control}${data}^
|
|
351
|
+
`;
|
|
352
|
+
}
|
|
353
|
+
var serializer = {
|
|
354
|
+
leader: (leader) => `*LDR${leader}
|
|
355
|
+
`,
|
|
356
|
+
controlfield: (field) => `*${field.tag}${field.value}
|
|
357
|
+
`,
|
|
358
|
+
datafield: (field) => {
|
|
359
|
+
const ind1 = field.ind1 ?? " ";
|
|
360
|
+
const ind2 = field.ind2 ?? " ";
|
|
361
|
+
const subfields = field.subfields.map(
|
|
362
|
+
(subfield) => `$${subfield.code}${escapeSubfieldValue(subfield.value)}`
|
|
363
|
+
).join("");
|
|
364
|
+
return `*${field.tag}${ind1}${ind2}${subfields}
|
|
365
|
+
`;
|
|
366
|
+
}
|
|
367
|
+
};
|
|
368
|
+
var escapeSubfieldValue = (value) => {
|
|
369
|
+
return value.replace(/\n/g, " ").replace(/\$/g, "@{2}");
|
|
370
|
+
};
|
|
371
|
+
function serializeMarcXml(input, pretty = false) {
|
|
372
|
+
const fields = [
|
|
373
|
+
createXmlElement("leader", { text: input.leader }),
|
|
374
|
+
...input.getControlFields().map(
|
|
375
|
+
(field) => createXmlElement("controlfield", {
|
|
376
|
+
attributes: { tag: field.tag },
|
|
377
|
+
text: field.value
|
|
378
|
+
})
|
|
379
|
+
),
|
|
380
|
+
...input.getDataFields().map(
|
|
381
|
+
(field) => createXmlElement("datafield", {
|
|
382
|
+
attributes: withoutEmptyValues({
|
|
383
|
+
tag: field.tag,
|
|
384
|
+
ind1: field.ind1,
|
|
385
|
+
ind2: field.ind2
|
|
386
|
+
}),
|
|
387
|
+
children: field.subfields.map(
|
|
388
|
+
(subfield) => createXmlElement("subfield", {
|
|
389
|
+
attributes: { code: subfield.code },
|
|
390
|
+
text: subfield.value
|
|
391
|
+
})
|
|
392
|
+
)
|
|
393
|
+
})
|
|
394
|
+
)
|
|
395
|
+
];
|
|
396
|
+
const recordNode = createXmlElement("record", {
|
|
397
|
+
attributes: { xmlns: "http://www.loc.gov/MARC21/slim" },
|
|
398
|
+
children: fields
|
|
399
|
+
});
|
|
400
|
+
return serializeXml(recordNode, pretty);
|
|
401
|
+
}
|
|
402
|
+
var withoutEmptyValues = (obj) => Object.keys(obj).reduce(
|
|
403
|
+
(acc, key) => obj[key] === void 0 ? { ...acc } : { ...acc, [key]: obj[key] },
|
|
404
|
+
{}
|
|
405
|
+
);
|
|
406
|
+
|
|
407
|
+
export { ControlField, DataField, MarcParseError, MarcRecord, Subfield, createControlField, createDataField, marcRecordZodSchema, parseMarcXml, serializeLineMarc, serializeMarcXml };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@biblioteksentralen/marc",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.4",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "MARC record parser and serializer",
|
|
@@ -28,23 +28,23 @@
|
|
|
28
28
|
"ajv": "^8.17.1",
|
|
29
29
|
"ts-log": "^2.2.5",
|
|
30
30
|
"zod": "^3.23.8",
|
|
31
|
-
"@biblioteksentralen/xml-utils": "^0.0.
|
|
31
|
+
"@biblioteksentralen/xml-utils": "^0.0.2"
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|
|
34
34
|
"@arethetypeswrong/cli": "^0.15.4",
|
|
35
35
|
"@types/json-schema": "^7.0.15",
|
|
36
|
-
"@types/node": "^
|
|
37
|
-
"
|
|
36
|
+
"@types/node": "^22.15.3",
|
|
37
|
+
"eslint": "^9.26.0",
|
|
38
38
|
"rimraf": "^5.0.5",
|
|
39
39
|
"tsup": "^8.0.2",
|
|
40
|
-
"typescript": "^5.
|
|
41
|
-
"vitest": "^1.
|
|
40
|
+
"typescript": "^5.6.2",
|
|
41
|
+
"vitest": "^3.1.2",
|
|
42
42
|
"@dataplattform/eslint-config": "1.0.0"
|
|
43
43
|
},
|
|
44
44
|
"scripts": {
|
|
45
|
-
"dev": "
|
|
45
|
+
"dev": "tsup src/index.ts --format cjs,esm --dts --treeshake --watch",
|
|
46
46
|
"build": "tsup src/index.ts --format cjs,esm --dts --treeshake",
|
|
47
|
-
"test": "vitest run --poolOptions.threads.singleThread --reporter=verbose
|
|
47
|
+
"test": "vitest run --poolOptions.threads.singleThread --reporter=verbose",
|
|
48
48
|
"test:watch": "vitest",
|
|
49
49
|
"clean": "rimraf dist",
|
|
50
50
|
"lint": "eslint .",
|