marc-ts 0.1.0 → 0.4.2

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 CHANGED
@@ -7,12 +7,11 @@
7
7
 
8
8
  ## Features
9
9
 
10
- - **Immutable API** - All operations return new objects, never mutate existing records
11
- - **Zero runtime dependencies** - Works in browsers and Node.js (≥14) without any dependencies
12
- - **Type-safe** - Full TypeScript type definitions with strict typing
13
- - **Well-tested** - >90% code coverage with comprehensive test suite
14
- - **Universal** - Runs in Node.js and modern browsers (Chrome, Firefox, Safari, Edge)
15
- - **Functional design** - Pure functions for composability and predictability
10
+ - **Four formats** ISO2709 binary, MARCXML, MARC-in-JSON, MARCBreaker/marctxt
11
+ - **Consistent API** every format uses `parse*(input) → MarcRecord[]` and `serialize*(records) output`
12
+ - **Functional-style** all operations return new objects; originals are never mutated
13
+ - **Zero dependencies** no runtime deps, including no XML parser. MARCXML has only 5 element types with no arbitrary nesting, so it's parsed with a lightweight hand-rolled tokenizer instead of a full DOM/SAX library. Works in Node.js and modern browsers.
14
+ - **Fully typed** strict TypeScript throughout
16
15
 
17
16
  ## Installation
18
17
 
@@ -23,620 +22,216 @@ npm install marc-ts
23
22
  ## Quick Start
24
23
 
25
24
  ```typescript
26
- import { parseMarcRecord, serializeMarcRecord, title, author, isbn, subjects } from 'marc-ts';
25
+ import { parseMarcBinary, serializeMarcBinary, title, author } from 'marc-ts';
27
26
  import { parseMarcXml, serializeMarcXml } from 'marc-ts/xml';
28
27
  import { parseMarcJson, serializeMarcJsonString } from 'marc-ts/json';
29
28
  import { parseMarcTxt, serializeMarcTxt } from 'marc-ts/txt';
30
29
 
31
- // --- ISO 2709 binary (MARC21) ---
32
- const buffer = new Uint8Array([...]); // Your MARC21 binary data
33
- const result = parseMarcRecord(buffer);
30
+ const records = parseMarcBinary(buffer);
31
+ console.log(title(records[0]));
34
32
 
35
- if (result.record) {
36
- console.log('Title:', title(result.record));
37
- console.log('Author:', author(result.record));
38
- console.log('ISBNs:', isbn(result.record));
39
- console.log('Subjects:', subjects(result.record));
40
- }
41
- if (result.warnings.length > 0) {
42
- console.warn('Parsing warnings:', result.warnings);
43
- }
44
-
45
- // Serialize back to binary (UTF-8 by default; pass { encoding: 'marc8' } for MARC-8 output)
46
- const binary = serializeMarcRecord(result.record!, { encoding: 'utf8' });
47
-
48
- // --- MARCXML ---
49
- const xmlString = `<?xml version="1.0"?>
50
- <collection xmlns="http://www.loc.gov/MARC21/slim">
51
- <record>
52
- <leader>00000nam a2200000 4500</leader>
53
- <datafield tag="245" ind1="1" ind2="0">
54
- <subfield code="a">The Hobbit</subfield>
55
- </datafield>
56
- </record>
57
- </collection>`;
58
-
59
- const [xmlRecord] = parseMarcXml(xmlString);
60
- console.log('Title from XML:', title(xmlRecord));
61
- const roundtripXml = serializeMarcXml([xmlRecord]); // back to a <collection> document
62
-
63
- // --- MARC-in-JSON ---
64
- const jsonString = JSON.stringify({
65
- leader: '00000nam a2200000 4500',
66
- fields: [
67
- { '245': { subfields: [{ a: 'The Hobbit' }], ind1: '1', ind2: '0' } },
68
- ],
69
- });
70
-
71
- const jsonRecord = parseMarcJson(jsonString);
72
- console.log('Title from JSON:', title(jsonRecord));
73
- const roundtripJson = serializeMarcJsonString(jsonRecord); // back to a JSON string
74
-
75
- // --- MARCBreaker (marctxt) ---
76
- const txtString = `=LDR 00000nam a2200000 4500
77
- =001 5490
78
- =245 10$aThe Hobbit /$cJ.R.R. Tolkien.
79
- `;
80
-
81
- const [txtRecord] = parseMarcTxt(txtString);
82
- console.log('Title from marctxt:', title(txtRecord));
83
- const roundtripTxt = serializeMarcTxt([txtRecord]); // back to marctxt string
84
-
85
- // --- MARC-8 binary ---
86
- // parseMarcRecord detects MARC-8 automatically from leader byte 9 (' ');
87
- // records are decoded to Unicode transparently — no special handling needed.
88
- const marc8Buffer = new Uint8Array([...]); // MARC-8 encoded binary
89
- const marc8Result = parseMarcRecord(marc8Buffer); // decoded to Unicode automatically
90
- ```
91
-
92
- ## Why marc-ts?
93
-
94
- Existing JavaScript/TypeScript MARC libraries are often:
95
- - Node.js-only (using streams, fs, Buffer APIs)
96
- - Class-based OOP patterns that don't leverage TypeScript's strengths
97
- - Mutable APIs that can lead to unexpected bugs
98
- - Lacking comprehensive type definitions
99
-
100
- **marc-ts** addresses these limitations:
101
- - Universal browser and Node.js compatibility
102
- - TypeScript-native with full type safety and functional patterns
103
- - Immutable operations for safer, more predictable code
104
- - Zero runtime dependencies for minimal bundle size
105
-
106
- ## Core Concepts
107
-
108
- ### Immutability
109
-
110
- Mutation-style operations in **marc-ts** return new records or fields rather than modifying existing ones:
111
-
112
- ```typescript
113
- const updated = appendField(record, field); // record remains unchanged
114
- ```
115
-
116
- This approach prevents accidental mutations and makes code easier to reason about, especially in reactive frameworks like React or Vue.
117
-
118
- ### Functional API
33
+ const xmlRecords = parseMarcXml(xmlString);
34
+ const xml = serializeMarcXml(xmlRecords);
119
35
 
120
- **marc-ts** uses pure functions for maximum composability:
36
+ const jsonRecords = parseMarcJson(jsonString);
37
+ const json = serializeMarcJsonString(jsonRecords);
121
38
 
122
- ```typescript
123
- // Extract metadata using pure functions
124
- const bookTitle = title(record);
125
- const bookAuthor = author(record);
126
-
127
- // Access fields functionally
128
- const titleField = getField(record, '245');
39
+ const txtRecords = parseMarcTxt(txtString);
40
+ const txt = serializeMarcTxt(txtRecords);
129
41
  ```
130
42
 
131
- ### Type Safety
43
+ ## Formats
132
44
 
133
- Full TypeScript types ensure compile-time correctness:
45
+ ### ISO2709 Binary (`marc-ts`)
134
46
 
135
47
  ```typescript
136
- import type { MarcRecord, DataField } from 'marc-ts';
137
- import { isDataField } from 'marc-ts';
138
-
139
- const field = getField(record, '245');
140
- if (field && isDataField(field)) {
141
- // TypeScript knows field is a DataField
142
- const titleValue = getSubfield(field, 'a');
143
- }
48
+ import { parseMarcBinary, serializeMarcBinary } from 'marc-ts';
144
49
  ```
145
50
 
146
- ## API Reference
51
+ #### `parseMarcBinary(buffer, options?): MarcRecord[]`
147
52
 
148
- ### Parsing and Serialization
53
+ Splits on `0x1D` record terminators and parses each record. Failed records are skipped in lenient mode; `strict: true` throws on the first error.
149
54
 
150
- #### `parseMarcRecord(buffer, options?): ParseResult`
55
+ | Option | Type | Default | Description |
56
+ |--------|------|---------|-------------|
57
+ | `strict` | `boolean` | `false` | Throw on fatal parse errors instead of skipping |
58
+ | `maxWarnings` | `number` | `100` | Stop collecting warnings after this many (per record) |
151
59
 
152
- Parse ISO2709 binary data into a MARC record.
60
+ Leader byte 9 controls character decoding: `'a'` = UTF-8, `' '` = MARC-8. MARC-8 handles ANSEL Latin, Greek, Hebrew, Cyrillic, Arabic, and sub/superscript scripts. EACC/CJK coverage is minimal (~33 of ~16k triples) — prefer UTF-8 sources for CJK catalogs.
153
61
 
154
- ```typescript
155
- const result = parseMarcRecord(buffer, {
156
- strict: false, // If true, throw on fatal parse errors
157
- maxWarnings: 100, // Maximum warnings to collect
158
- });
159
-
160
- if (result.record) {
161
- // Successfully parsed
162
- } else {
163
- // Parsing failed, check result.warnings
164
- }
165
- ```
62
+ #### `parseMarcBinaryWithWarnings(buffer, options?): ParseBatchResult`
166
63
 
167
- Recoverable issues may still be returned in `warnings`, such as MARC leader compatibility warnings.
64
+ Same as `parseMarcBinary`, but returns per-record results with warnings. Failed records appear with `record: null`.
168
65
 
169
- #### `parseMarcRecordStrict(buffer): MarcRecord`
66
+ #### `serializeMarcBinary(records, options?): Uint8Array`
170
67
 
171
- Convenience wrapper for strict parsing (throws on fatal parse errors).
68
+ | Option | Type | Default | Description |
69
+ |--------|------|---------|-------------|
70
+ | `encoding` | `'utf8' \| 'marc8'` | `'utf8'` | Character encoding; `'marc8'` replaces unsupported Unicode with `?` |
71
+ | `maxWarnings` | `number` | `100` | Stop collecting warnings after this many (per record) |
172
72
 
173
- ```typescript
174
- try {
175
- const record = parseMarcRecordStrict(buffer);
176
- } catch (error) {
177
- console.error('Parsing failed:', error);
178
- }
179
- ```
180
-
181
- #### `serializeMarcRecord(record): Uint8Array`
182
-
183
- Serialize a MARC record to ISO2709 binary format.
184
-
185
- ```typescript
186
- const buffer = serializeMarcRecord(record);
187
- // Can be written to file or transmitted over network
188
- ```
189
-
190
- `parseMarcRecord` decodes UTF-8 records and MARC-8 records signaled by leader
191
- byte 9. MARC-8 decoding handles escape-designated scripts such as ANSEL Latin,
192
- Greek, Hebrew, Cyrillic, Arabic, subscript/superscript, and mapped EACC/CJK
193
- triples. MARC-8 serialization is intentionally conservative: `encoding:
194
- 'marc8'` writes ASCII plus ANSEL Latin/combining characters and replaces
195
- unsupported Unicode characters with `?`.
196
-
197
- **EACC coverage caveat:** the bundled EACC table maps only ~33 of the ~16,000
198
- official triples. Records with substantial Chinese/Japanese/Korean content
199
- will mostly decode to U+FFFD. For CJK catalogs, prefer UTF-8 sources
200
- (`leader[9] === 'a'`).
201
-
202
- **Surfacing lossy MARC-8 encoding:** because `serializeMarcRecord` returns a
203
- plain `Uint8Array`, lossy substitutions are invisible to callers. Use
204
- `serializeMarcRecordWithWarnings(record, { encoding: 'marc8' })` to get
205
- `{ bytes, warnings }` — any character that could not be encoded surfaces as
206
- an `encoding_error` warning. For just-the-encoder visibility, use
207
- `unicodeToMarc8WithStats(text)` to get `{ bytes, lossyCount }`.
208
-
209
- ### Convenience Accessors
210
-
211
- Extract common bibliographic metadata:
212
-
213
- | Function | Field | Description | Example |
214
- |----------|-------|-------------|---------|
215
- | `title(record)` | 245 $a$b | Full title with subtitle | `"The Catcher in the Rye"` |
216
- | `titleProper(record)` | 245 $a | Main title only | `"The Catcher in the Rye"` |
217
- | `author(record)` | 100/110 $a | Main author/creator | `"Salinger, J. D."` |
218
- | `edition(record)` | 250 $a | Edition statement | `"1st ed."` |
219
- | `publisher(record)` | 260/264 $b | Publisher name | `"Little, Brown,"` |
220
- | `publicationDate(record)` | 260/264 $c | Publication date | `"1951."` |
221
- | `isbn(record)` | 020 $a | ISBN(s) - array | `["978-0-316-76948-0"]` |
222
- | `issn(record)` | 022 $a | ISSN | `"0028-0836"` |
223
- | `lccn(record)` | 010 $a | Library of Congress Control Number | `"50011915"` |
224
- | `subjects(record)` | 6XX $a | All subject headings - array | `["Fiction", "History"]` |
225
- | `seriesStatement(record)` | 490 $a | Series statement | `"Penguin classics"` |
226
-
227
- ### Field Access
228
-
229
- #### `getField(record, tag): ControlField | DataField | undefined`
230
-
231
- Get the first field with a specific tag.
232
-
233
- ```typescript
234
- const titleField = getField(record, '245');
235
- ```
236
-
237
- #### `getFields(record, tag): (ControlField | DataField)[]`
238
-
239
- Get all fields with a specific tag.
240
-
241
- ```typescript
242
- const subjectFields = getFields(record, '650');
243
- ```
244
-
245
- #### `getSubfield(field, code): string | undefined`
246
-
247
- Get the first subfield value from a data field.
248
-
249
- ```typescript
250
- const field = getField(record, '245');
251
- if (field && isDataField(field)) {
252
- const titleValue = getSubfield(field, 'a');
253
- }
254
- ```
73
+ #### `serializeMarcBinaryWithWarnings(records, options?): SerializeBatchResult`
255
74
 
256
- #### `getSubfields(field, code): string[]`
75
+ Same as `serializeMarcBinary`, but returns per-record serialization warnings alongside the bytes.
257
76
 
258
- Get all subfield values with a specific code (for repeatable subfields).
259
-
260
- ```typescript
261
- const field = getField(record, '650');
262
- if (field && isDataField(field)) {
263
- const subdivisions = getSubfields(field, 'x');
264
- }
265
- ```
266
-
267
- #### `getAllSubfields(field): Array<{ code: string; value: string }>`
268
-
269
- Get all subfields from a data field.
270
-
271
- ```typescript
272
- const field = getField(record, '245');
273
- if (field && isDataField(field)) {
274
- const allSubfields = getAllSubfields(field);
275
- }
276
- ```
277
-
278
- ### Wildcard Querying
279
-
280
- #### `getFieldsByPattern(record, pattern): (ControlField | DataField)[]`
77
+ ---
281
78
 
282
- Match fields using wildcard patterns (`.` or `X` = any digit).
79
+ ### MARCXML (`marc-ts/xml`)
283
80
 
284
81
  ```typescript
285
- // Get all 6XX subject fields
286
- const subjects = getFieldsByPattern(record, '6..');
287
-
288
- // Get all 7XX added entry fields
289
- const addedEntries = getFieldsByPattern(record, '7XX');
290
-
291
- // Get all X00 fields (100, 200, ..., 900)
292
- const x00Fields = getFieldsByPattern(record, 'X00');
82
+ import { parseMarcXml, serializeMarcXml } from 'marc-ts/xml';
293
83
  ```
294
84
 
295
- #### `getFirstFieldByPattern(record, pattern): ControlField | DataField | undefined`
296
-
297
- Get the first field matching a wildcard pattern.
85
+ #### `parseMarcXml(xml): MarcRecord[]`
298
86
 
299
- ```typescript
300
- const firstSubject = getFirstFieldByPattern(record, '6..');
301
- ```
87
+ Accepts `<collection>`, bare `<record>` elements, or namespace-prefixed variants. Returns `[]` for empty input.
302
88
 
303
- ### Field Operations (Immutable)
89
+ #### `serializeMarcXml(records): string`
304
90
 
305
- All operations return new records/fields without mutating the original.
91
+ Produces a full MARCXML `<collection>` document with XML declaration and MARC21 namespace.
306
92
 
307
- #### `appendField(record, field): MarcRecord`
93
+ ---
308
94
 
309
- Append a field to the end of a record.
95
+ ### MARC-in-JSON (`marc-ts/json`)
310
96
 
311
97
  ```typescript
312
- const newField: DataField = {
313
- tag: '650',
314
- indicator1: ' ',
315
- indicator2: '0',
316
- subfields: [{ code: 'a', value: 'New subject' }],
317
- };
318
-
319
- const updated = appendField(record, newField);
320
- // record is unchanged, updated has the new field
98
+ import { parseMarcJson, serializeMarcJson, serializeMarcJsonString } from 'marc-ts/json';
321
99
  ```
322
100
 
323
- #### `insertFieldBefore(record, tag, field): MarcRecord`
101
+ Implements the [MARC-in-JSON](https://wiki.code4lib.org/MARCJSONification) spec.
324
102
 
325
- Insert a field before the first occurrence of a tag.
326
-
327
- ```typescript
328
- const updated = insertFieldBefore(record, '700', newField);
329
- ```
103
+ #### `parseMarcJson(json): MarcRecord[]`
330
104
 
331
- #### `insertFieldAfter(record, tag, field): MarcRecord`
105
+ Accepts a JSON string (array or single object), a `MarcJsonObject[]`, or a single `MarcJsonObject`.
332
106
 
333
- Insert a field after the first occurrence of a tag.
107
+ #### `serializeMarcJson(records): MarcJsonObject[]`
334
108
 
335
- ```typescript
336
- const updated = insertFieldAfter(record, '245', newField);
337
- ```
109
+ #### `serializeMarcJsonString(records): string`
338
110
 
339
- #### `insertGroupedField(record, field): MarcRecord`
111
+ ---
340
112
 
341
- Insert a field maintaining MARC block order (00X → 0XX → 1XX → ... → 9XX).
113
+ ### MARCBreaker / marctxt (`marc-ts/txt`)
342
114
 
343
115
  ```typescript
344
- const updated = insertGroupedField(record, field);
345
- // Field is inserted in proper MARC order
116
+ import { parseMarcTxt, serializeMarcTxt } from 'marc-ts/txt';
346
117
  ```
347
118
 
348
- #### `removeFields(record, tag): MarcRecord`
119
+ Line-oriented format: one field per line, blank lines between records, `$` for subfield delimiters, `\` for blank indicators.
349
120
 
350
- Remove all fields with a specific tag.
121
+ Reserved characters are escaped: `$` `{dollar}`, `{` → `{lcub}`, `}` → `{rcub}`, `\` → `{bsol}`.
351
122
 
352
- ```typescript
353
- const updated = removeFields(record, '650');
354
- ```
355
-
356
- #### `removeField(record, field): MarcRecord`
123
+ #### `parseMarcTxt(text): MarcRecord[]`
357
124
 
358
- Remove a specific field instance using reference equality.
125
+ #### `serializeMarcTxt(records): string`
359
126
 
360
- ```typescript
361
- const field = getField(record, '650');
362
- const updated = field ? removeField(record, field) : record;
363
- ```
127
+ ---
364
128
 
365
- #### Subfield Operations
129
+ ## Convenience Accessors
366
130
 
367
131
  ```typescript
368
- // Add subfield to a field
369
- const updated = addSubfield(field, 'b', 'Subtitle');
370
-
371
- // Remove all subfields with code
372
- const updated = removeSubfield(field, 'x');
373
-
374
- // Replace first subfield with code
375
- const updated = replaceSubfield(field, 'a', 'New value');
132
+ import { title, titleProper, author, edition, publisher, publicationDate,
133
+ isbn, issn, lccn, subjects, seriesStatement } from 'marc-ts';
376
134
  ```
377
135
 
378
- ### Clone and Equality
136
+ | Function | Source field | Returns |
137
+ |----------|-------------|---------|
138
+ | `title(record)` | 245 $a$b | Full title with subtitle |
139
+ | `titleProper(record)` | 245 $a | Main title only |
140
+ | `author(record)` | 100/110 $a | Main author/creator |
141
+ | `edition(record)` | 250 $a | Edition statement |
142
+ | `publisher(record)` | 260/264 $b | Publisher name |
143
+ | `publicationDate(record)` | 260/264 $c | Publication date |
144
+ | `isbn(record)` | 020 $a | `string[]` of ISBNs |
145
+ | `issn(record)` | 022 $a | ISSN |
146
+ | `lccn(record)` | 010 $a | Library of Congress Control Number |
147
+ | `subjects(record)` | 6XX $a | `string[]` of subject headings |
148
+ | `seriesStatement(record)` | 490 $a | Series statement |
379
149
 
380
- #### `cloneRecord(record): MarcRecord`
150
+ ---
381
151
 
382
- Create a deep copy of a record.
152
+ ## Field Access
383
153
 
384
154
  ```typescript
385
- const copy = cloneRecord(record);
386
- // Modifying copy will not affect record
155
+ import { getField, getFields, getSubfield, getSubfields, getAllSubfields } from 'marc-ts';
156
+ import { isControlField, isDataField } from 'marc-ts';
387
157
  ```
388
158
 
389
- #### `recordsEqual(a, b, ignoreFieldOrder?): boolean`
390
-
391
- Check if two records are equal.
392
-
393
159
  ```typescript
394
- if (recordsEqual(record1, record2)) {
395
- console.log('Records are identical');
396
- }
397
-
398
- // Ignore field order
399
- if (recordsEqual(record1, record2, true)) {
400
- console.log('Records have same content');
401
- }
402
- ```
160
+ const field = getField(record, '245'); // first match or undefined
161
+ const fields = getFields(record, '650'); // all matches
403
162
 
404
- #### `fieldsEqual(a, b): boolean`
405
-
406
- Check if two fields are equal.
407
-
408
- ```typescript
409
- if (fieldsEqual(field1, field2)) {
410
- console.log('Fields are identical');
163
+ if (field && isDataField(field)) {
164
+ const a = getSubfield(field, 'a');
165
+ const xs = getSubfields(field, 'x');
166
+ const all = getAllSubfields(field); // [{ code, value }, ...]
411
167
  }
412
168
  ```
413
169
 
414
- ### Warnings
415
-
416
- #### `createWarning(type, message, position?, tag?): MarcWarning`
417
-
418
- Create a parsing warning object.
170
+ ## Wildcard Querying
419
171
 
420
172
  ```typescript
421
- const warning = createWarning('invalid_field', 'Field is out of bounds', 42, '245');
422
- ```
423
-
424
- ## Additional Formats
425
-
426
- ### MARCXML (`marc-ts/xml`)
427
-
428
- Import from the `marc-ts/xml` subpath for MARCXML support (Library of Congress schema).
173
+ import { getFieldsByPattern, getFirstFieldByPattern } from 'marc-ts';
429
174
 
430
- ```typescript
431
- import {
432
- parseMarcXml,
433
- parseMarcXmlRecord,
434
- serializeMarcXml,
435
- serializeMarcXmlRecord,
436
- } from 'marc-ts/xml';
175
+ const subjects = getFieldsByPattern(record, '6..'); // all 6XX fields
437
176
  ```
438
177
 
439
- #### `parseMarcXml(xml): MarcRecord[]`
440
-
441
- Parse a MARCXML string containing a `<collection>` or one or more bare `<record>` elements.
442
-
443
- ```typescript
444
- const records = parseMarcXml(xmlString);
445
- // Returns all records found in the document
446
- ```
447
-
448
- #### `parseMarcXmlRecord(xml): MarcRecord`
449
-
450
- Parse a MARCXML string expected to contain exactly one `<record>`. Throws if none is found.
451
-
452
- ```typescript
453
- const record = parseMarcXmlRecord(xmlString);
454
- ```
455
-
456
- #### `serializeMarcXml(records): string`
457
-
458
- Serialize one or more records into a full MARCXML `<collection>` document (with XML declaration).
459
-
460
- ```typescript
461
- const xml = serializeMarcXml([record1, record2]);
462
- ```
463
-
464
- #### `serializeMarcXmlRecord(record): string`
465
-
466
- Serialize a single record to a `<record>` XML element string (no collection wrapper or XML declaration).
467
-
468
- ```typescript
469
- const recordXml = serializeMarcXmlRecord(record);
470
- ```
178
+ `.` and `X` each match any single digit.
471
179
 
472
180
  ---
473
181
 
474
- ### MARC-in-JSON (`marc-ts/json`)
182
+ ## Field Operations
475
183
 
476
- Import from the `marc-ts/json` subpath for [MARC-in-JSON](https://wiki.code4lib.org/MARCJSONification) support (used by Open Library and many REST APIs).
184
+ All operations return new objects originals are never mutated.
477
185
 
478
186
  ```typescript
479
187
  import {
480
- parseMarcJson,
481
- serializeMarcJson,
482
- serializeMarcJsonString,
483
- } from 'marc-ts/json';
484
- import type { MarcJsonObject } from 'marc-ts/json';
485
- ```
188
+ appendField, insertFieldBefore, insertFieldAfter, insertGroupedField,
189
+ removeFields, removeField,
190
+ addSubfield, removeSubfield, replaceSubfield,
191
+ } from 'marc-ts';
486
192
 
487
- The format represents each field as a single-key object in an array:
193
+ const r1 = appendField(record, newField);
194
+ const r2 = insertFieldBefore(record, '700', newField);
195
+ const r3 = insertFieldAfter(record, '245', newField);
196
+ const r4 = insertGroupedField(record, newField); // maintains MARC tag order
197
+ const r5 = removeFields(record, '650');
198
+ const r6 = removeField(record, specificField); // reference equality
488
199
 
489
- ```json
490
- {
491
- "leader": "01142cam a2200301 a 4500",
492
- "fields": [
493
- { "001": "5490" },
494
- { "245": { "subfields": [{ "a": "The Hobbit" }], "ind1": "1", "ind2": "0" } }
495
- ]
496
- }
497
- ```
498
-
499
- #### `parseMarcJson(json): MarcRecord`
500
-
501
- Parse a MARC-in-JSON object or JSON string into a `MarcRecord`. Throws on structural errors.
502
-
503
- ```typescript
504
- const record = parseMarcJson(jsonString); // from a JSON string
505
- const record = parseMarcJson(jsonObject); // from a plain object
506
- ```
507
-
508
- #### `serializeMarcJson(record): MarcJsonObject`
509
-
510
- Serialize a `MarcRecord` to a MARC-in-JSON plain object.
511
-
512
- ```typescript
513
- const obj = serializeMarcJson(record);
514
- // obj.leader, obj.fields — ready for JSON.stringify or further processing
515
- ```
516
-
517
- #### `serializeMarcJsonString(record): string`
518
-
519
- Serialize a `MarcRecord` directly to a JSON string.
520
-
521
- ```typescript
522
- const json = serializeMarcJsonString(record);
200
+ const f1 = addSubfield(field, 'b', 'Subtitle');
201
+ const f2 = removeSubfield(field, 'x');
202
+ const f3 = replaceSubfield(field, 'a', 'New value');
523
203
  ```
524
204
 
525
205
  ---
526
206
 
527
- ### MARCBreaker / marctxt (`marc-ts/txt`)
528
-
529
- Import from the `marc-ts/txt` subpath for MARCBreaker support. This format (also called MARCMaker or marctxt) is a human-readable line-oriented representation originated by the Library of Congress MARCMaker/MARCBreaker tools and widely used for editing MARC data in plain text.
530
-
531
- ```typescript
532
- import {
533
- parseMarcTxt,
534
- parseMarcTxtRecord,
535
- serializeMarcTxt,
536
- serializeMarcTxtRecord,
537
- } from 'marc-ts/txt';
538
- ```
539
-
540
- Each field occupies one line. Blank indicators are written as `\`. Subfields use `$` followed by a single-character code. Records are separated by blank lines:
541
-
542
- ```
543
- =LDR 00706cam a2200217 a 4500
544
- =001 5490
545
- =003 OCoLC
546
- =245 14$aThe Hobbit /$cJ.R.R. Tolkien.
547
- =650 \1$aHobbits (Fictitious characters)$vFiction.
548
- ```
549
-
550
- **Value escaping.** Standard MARCBreaker has no way to represent a literal `$`
551
- in a value, and `marc-ts` follows the same convention as other tools for the
552
- remaining reserved characters:
553
-
554
- - `$` → `{dollar}`
555
- - `{` → `{lcub}`, `}` → `{rcub}`
556
- - `\` → `{bsol}`
557
-
558
- Embedded newlines (`\n`) in field values are replaced with a space on
559
- serialize, matching the behavior of other MARCBreaker tools. Source values that
560
- do not contain any of these characters are emitted verbatim.
561
-
562
- #### `parseMarcTxt(text): MarcRecord[]`
563
-
564
- Parse a marctxt string containing one or more records separated by blank lines. Accepts both `\n` and `\r\n` line endings.
565
-
566
- ```typescript
567
- const records = parseMarcTxt(txtString);
568
- // Returns all records found
569
- ```
570
-
571
- #### `parseMarcTxtRecord(text): MarcRecord`
572
-
573
- Parse a marctxt string expected to contain exactly one record. Throws if none is found.
207
+ ## Clone and Equality
574
208
 
575
209
  ```typescript
576
- const record = parseMarcTxtRecord(txtString);
577
- ```
578
-
579
- #### `serializeMarcTxt(records): string`
210
+ import { cloneRecord, recordsEqual, fieldsEqual } from 'marc-ts';
580
211
 
581
- Serialize one or more records into a marctxt string, with records separated by blank lines.
582
-
583
- ```typescript
584
- const txt = serializeMarcTxt([record1, record2]);
212
+ const copy = cloneRecord(record);
213
+ recordsEqual(a, b); // strict field order
214
+ recordsEqual(a, b, true); // ignore field order
215
+ fieldsEqual(field1, field2);
585
216
  ```
586
217
 
587
- #### `serializeMarcTxtRecord(record): string`
218
+ ---
588
219
 
589
- Serialize a single record to marctxt (no surrounding blank line).
220
+ ## Types
590
221
 
591
222
  ```typescript
592
- const txt = serializeMarcTxtRecord(record);
223
+ import type { MarcRecord, ControlField, DataField, Subfield,
224
+ ParseOptions, SerializeOptions, MarcWarning, MarcWarningType } from 'marc-ts';
593
225
  ```
594
226
 
595
227
  ---
596
228
 
597
- ## Browser Usage
598
-
599
- **marc-ts** works in modern browsers without any bundler configuration:
600
-
601
- ```html
602
- <!DOCTYPE html>
603
- <html>
604
- <head>
605
- <title>marc-ts Browser Example</title>
606
- </head>
607
- <body>
608
- <input type="file" id="fileInput" accept=".mrc" />
609
- <pre id="output"></pre>
610
-
611
- <script type="module">
612
- import { parseMarcRecord, title, author } from 'https://cdn.skypack.dev/marc-ts';
613
-
614
- document.getElementById('fileInput').addEventListener('change', async (e) => {
615
- const file = e.target.files[0];
616
- const arrayBuffer = await file.arrayBuffer();
617
- const buffer = new Uint8Array(arrayBuffer);
618
-
619
- const result = parseMarcRecord(buffer);
620
- if (result.record) {
621
- document.getElementById('output').textContent = `
622
- Title: ${title(result.record) || 'N/A'}
623
- Author: ${author(result.record) || 'N/A'}
624
- `;
625
- }
626
- });
627
- </script>
628
- </body>
629
- </html>
630
- ```
631
-
632
- ## Examples
633
-
634
- See the [examples/](./examples/) directory for more examples:
635
- - [basic-usage.ts](./examples/basic-usage.ts) - Common usage patterns
636
- - [browser.html](./examples/browser.html) - Browser integration
637
-
638
229
  ## Development
639
230
 
640
- Requires Node.js **20.19** or **22.12+** (driven by Vite 8). Older Node versions
641
- are EOL and will fail to install the dev toolchain. The compiled output is
642
- compatible with modern browsers and any actively-supported Node release.
231
+ Requires Node.js **20.19** or **22.12+** (driven by Vite 8).
232
+
233
+ ```bash
234
+ npm test # run tests
235
+ npm run build # compile to dist/
236
+ npm run type-check # TypeScript check without emit
237
+ ```