npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

marc-ts

v0.2.0

Published

TypeScript MARC21 library for Node.js and browsers

Downloads

294

Readme

marc-ts

TypeScript MARC21 library for Node.js and browsers

npm version License: MIT

Features

  • Four formats — ISO2709 binary, MARCXML, MARC-in-JSON, MARCBreaker/marctxt
  • Consistent API — every format uses parse*(input) → MarcRecord[] and serialize*(records[]) → <native>
  • Immutable — all operations return new objects, never mutate
  • Zero dependencies — works in Node.js and modern browsers
  • Fully typed — strict TypeScript throughout

Installation

npm install marc-ts

Quick Start

import { parseMarcBinary, serializeMarcBinary, title, author } from 'marc-ts';
import { parseMarcXml, serializeMarcXml } from 'marc-ts/xml';
import { parseMarcJson, serializeMarcJsonString } from 'marc-ts/json';
import { parseMarcTxt, serializeMarcTxt } from 'marc-ts/txt';

// Binary (ISO2709) — splits on 0x1D, returns all records
const records = parseMarcBinary(buffer);
console.log(title(records[0]));
console.log(author(records[0]));
const binary = serializeMarcBinary(records);

// MARCXML — parses <collection> or bare <record> elements
const xmlRecords = parseMarcXml(xmlString);
const xml = serializeMarcXml(xmlRecords);

// MARC-in-JSON — accepts array, single object, or JSON string
const jsonRecords = parseMarcJson(jsonString);
const json = serializeMarcJsonString(jsonRecords);

// MARCBreaker — records separated by blank lines
const txtRecords = parseMarcTxt(txtString);
const txt = serializeMarcTxt(txtRecords);

Formats

ISO2709 Binary (marc-ts)

import { parseMarcBinary, serializeMarcBinary } from 'marc-ts';
import type { ParseOptions, SerializeOptions } from 'marc-ts';

parseMarcBinary(buffer, options?): MarcRecord[]

Parse a concatenated ISO2709 binary stream. Splits on 0x1D (RECORD_TERMINATOR) and parses each record. Records that fail to parse are silently skipped in lenient mode; with strict: true the first error throws.

const records = parseMarcBinary(buffer);
const strict = parseMarcBinary(buffer, { strict: true });

ParseOptions

| Option | Type | Default | Description | |--------|------|---------|-------------| | strict | boolean | false | Throw on fatal parse errors instead of skipping | | maxWarnings | number | 100 | Stop collecting warnings after this many |

Character encoding. Leader byte 9 controls decoding: 'a' = UTF-8, ' ' (space) = MARC-8. MARC-8 decoding handles ANSEL Latin, Greek, Hebrew, Cyrillic, Arabic, and subscript/superscript scripts via escape-designated sequences. EACC/CJK coverage is limited (~33 of ~16,000 official triples); records with substantial CJK content will mostly decode to U+FFFD — prefer UTF-8 sources for CJK catalogs.

serializeMarcBinary(records, options?): Uint8Array

Serialize an array of records to a concatenated ISO2709 binary stream. Each record is individually serialized with its own 0x1D terminator.

const buffer = serializeMarcBinary(records);
const marc8Buffer = serializeMarcBinary(records, { encoding: 'marc8' });

SerializeOptions

| Option | Type | Default | Description | |--------|------|---------|-------------| | encoding | 'utf8' \| 'marc8' | 'utf8' | Character encoding; 'marc8' replaces unsupported Unicode with ? |


MARCXML (marc-ts/xml)

import { parseMarcXml, serializeMarcXml } from 'marc-ts/xml';

parseMarcXml(xml): MarcRecord[]

Parse a MARCXML string. Accepts a <collection> document, bare <record> elements, or namespace-prefixed variants (e.g. marc:record). Returns all records found; returns [] for empty or record-free input.

const records = parseMarcXml(xmlString);

serializeMarcXml(records): string

Serialize records to a full MARCXML <collection> document with XML declaration and MARC21 namespace.

const xml = serializeMarcXml(records);

MARC-in-JSON (marc-ts/json)

import { parseMarcJson, serializeMarcJson, serializeMarcJsonString } from 'marc-ts/json';
import type { MarcJsonObject } from 'marc-ts/json';

The MARC-in-JSON format represents each field as a single-key object:

{
  "leader": "01142cam a2200301 a 4500",
  "fields": [
    { "001": "5490" },
    { "245": { "subfields": [{ "a": "The Hobbit" }], "ind1": "1", "ind2": "0" } }
  ]
}

parseMarcJson(json): MarcRecord[]

Parse MARC-in-JSON into records. Accepts:

  • A JSON string whose top-level value is an array or a single object
  • A MarcJsonObject[] array
  • A single MarcJsonObject

Always returns MarcRecord[]. Throws on structural errors.

const records = parseMarcJson(jsonString);       // JSON string (array or single)
const records = parseMarcJson([obj1, obj2]);     // plain object array
const records = parseMarcJson(singleObj);        // single object → one-element array

serializeMarcJson(records): MarcJsonObject[]

Serialize records to an array of MARC-in-JSON plain objects.

const objs = serializeMarcJson(records);

serializeMarcJsonString(records): string

Serialize records directly to a JSON string (a JSON array).

const json = serializeMarcJsonString(records);

MARCBreaker / marctxt (marc-ts/txt)

import { parseMarcTxt, serializeMarcTxt } from 'marc-ts/txt';

MARCBreaker is a human-readable line-oriented format. Each field occupies one line; blank indicators are written as \; subfields use $ followed by a single-character code. Records are separated by blank lines:

=LDR  00706cam a2200217 a 4500
=001  5490
=003  OCoLC
=245  14$aThe Hobbit /$cJ.R.R. Tolkien.
=650  \1$aHobbits (Fictitious characters)$vFiction.

Value escaping — reserved characters in field values are escaped as follows:

| Character | Escaped form | |-----------|-------------| | $ | {dollar} | | { | {lcub} | | } | {rcub} | | \ | {bsol} |

Embedded newlines in values are replaced with a space on serialize.

parseMarcTxt(text): MarcRecord[]

Parse a marctxt string. Accepts \n and \r\n line endings. Records are separated by blank lines. Returns all records found.

const records = parseMarcTxt(txtString);

serializeMarcTxt(records): string

Serialize records to a marctxt string, with records separated by blank lines.

const txt = serializeMarcTxt(records);

Convenience Accessors

Extract common bibliographic metadata from any MarcRecord:

import { title, titleProper, author, edition, publisher, publicationDate,
         isbn, issn, lccn, subjects, seriesStatement } from 'marc-ts';

| Function | Source field | Returns | |----------|-------------|---------| | title(record) | 245 $a$b | Full title with subtitle | | titleProper(record) | 245 $a | Main title only | | author(record) | 100/110 $a | Main author/creator | | edition(record) | 250 $a | Edition statement | | publisher(record) | 260/264 $b | Publisher name | | publicationDate(record) | 260/264 $c | Publication date | | isbn(record) | 020 $a | string[] of ISBNs | | issn(record) | 022 $a | ISSN | | lccn(record) | 010 $a | Library of Congress Control Number | | subjects(record) | 6XX $a | string[] of subject headings | | seriesStatement(record) | 490 $a | Series statement |


Field Access

import { getField, getFields, getSubfield, getSubfields, getAllSubfields } from 'marc-ts';
import { isControlField, isDataField } from 'marc-ts';

getField(record, tag) / getFields(record, tag)

const field = getField(record, '245');        // first match or undefined
const fields = getFields(record, '650');      // all matches

getSubfield(field, code) / getSubfields(field, code)

if (field && isDataField(field)) {
  const a = getSubfield(field, 'a');          // first $a or undefined
  const xs = getSubfields(field, 'x');        // all $x values
}

getAllSubfields(field)

const all = getAllSubfields(field);           // [{ code, value }, ...]

Wildcard Querying

import { getFieldsByPattern, getFirstFieldByPattern } from 'marc-ts';

const subjects = getFieldsByPattern(record, '6..');   // all 6XX fields
const first7xx = getFirstFieldByPattern(record, '7XX');

. and X each match any single digit.


Field Operations (Immutable)

All operations return a new MarcRecord without modifying the original.

import {
  appendField, insertFieldBefore, insertFieldAfter, insertGroupedField,
  removeFields, removeField,
  addSubfield, removeSubfield, replaceSubfield,
} from 'marc-ts';

const r1 = appendField(record, newField);
const r2 = insertFieldBefore(record, '700', newField);
const r3 = insertFieldAfter(record, '245', newField);
const r4 = insertGroupedField(record, newField);  // maintains MARC block order
const r5 = removeFields(record, '650');
const r6 = removeField(record, specificField);    // reference equality

// Subfield operations — return a new DataField
const f1 = addSubfield(field, 'b', 'Subtitle');
const f2 = removeSubfield(field, 'x');
const f3 = replaceSubfield(field, 'a', 'New value');

Clone and Equality

import { cloneRecord, recordsEqual, fieldsEqual } from 'marc-ts';

const copy = cloneRecord(record);
recordsEqual(a, b);              // strict field order
recordsEqual(a, b, true);        // ignore field order
fieldsEqual(field1, field2);

Types

import type { MarcRecord, ControlField, DataField, Subfield,
              ParseOptions, SerializeOptions, MarcWarning, MarcWarningType } from 'marc-ts';

Development

Requires Node.js 20.19 or 22.12+ (driven by Vite 8). The compiled output targets modern browsers and any actively-supported Node release.

npm test            # run tests
npm run build       # compile to dist/
npm run type-check  # TypeScript check without emit