npm.io
0.3.2 • Published 1 week agoCLI

rdf-parser-ts

Licence
MIT
Version
0.3.2
Deps
2
Size
390 kB
Vulns
0
Weekly
0
Stars
1

RDF Parser for TypeScript

W3C RDF1.2 spec compliance

Fast RDF/JS parsing for Turtle, TriG, N-Triples, N-Quads, RDF 1.2 triple terms, and RDF Message Logs in Node.js and browsers.

Try rdf-parser-ts in the ldfetch playground, which uses this parser library in the browser.

This implementation has been built with a clear scope in mind: RDF1.2 compliance for parsing these 4 formats with RDF Messages support using the RDF/JS data model. We will explicitly never support storing data or reasoning.

We have a sibling package for writing data called rdf-writer-ts.

I built this as an agentic coding experiment for myself. I’m happy to see spec compliance, RDF Messages support, and a significant performance improvement over N3.js on the generated benchmarks, but integration tests with other software will need to show whether this work is as maintainable and useful as other libraries. This project would not have been possible without Blake Regalia’s work on Graphy and Ruben Verborgh’s work on N3.js.

Install

npm install rdf-parser-ts

Package layout

  • src/index.ts contains the RDF-JS data model, parser, stream parser, incremental parser, and parser helpers.
  • src/bin/rdf-parser-ts provides the rdf-parser-ts CLI.
  • test/ contains Vitest unit tests.
  • spec/ contains the rdf-test-suite adapter and EARL metadata, matching the N3.js spec-test setup.
  • perf/ contains synthetic performance benchmarks against N3.js and Graphy.
  • dist/ is generated by npm run build, including Node.js builds and browser bundles.

Build and validation scripts

npm run build          # Build CommonJS, ESM, declarations, CLI, and browser bundles
npm run build:browser  # Build dist/browser/index.mjs and dist/browser/index.global.js
npm run lint           # Type-check with tsc --noEmit
npm test               # Run unit tests
npm run check          # Type-check, build, then test
npm run ci             # Run check plus the performance regression warning check
npm run spec           # Run RDF 1.1 and RDF 1.2 spec suites
npm run perf           # Benchmark 10^4, 10^5, and 10^6 generated statements
npm run perf:quick     # Smaller benchmark for local iteration
npm run perf:regression # Compare current build to a git baseline and warn on >20% throughput drops
npm run perf:graphy    # Graphy-compatible benchmark without RDF 1.2 triple terms

The package exports CommonJS, ESM, and browser builds:

{
	"main": "./dist/index.js",
	"module": "./dist/index.mjs",
	"types": "./dist/index.d.ts",
	"browser": "./dist/browser/index.mjs"
}

Browser usage

With a browser-aware bundler, import the browser entry explicitly:

import { Parser, StreamParser, quadToString } from 'rdf-parser-ts/browser';

const quads = new Parser({ baseIRI: 'https://example.org/' }).parse('<s> <p> <o>.') ?? [];
console.log(quadToString(quads[0]!));

For direct browser usage through a CDN, use the ESM bundle:

<script type="module">
	import { Parser, quadToString } from 'https://cdn.jsdelivr.net/npm/rdf-parser-ts/dist/browser/index.mjs';

	const quads = new Parser({ baseIRI: 'https://example.org/' }).parse('<s> <p> <o>.') ?? [];
	console.log(quadToString(quads[0]));
</script>

Or use the global bundle, which exposes RDFParserTS:

<script src="https://unpkg.com/rdf-parser-ts/dist/browser/index.global.js"></script>
<script>
	const { Parser, quadToString } = RDFParserTS;
	const quads = new Parser({ baseIRI: 'https://example.org/' }).parse('<s> <p> <o>.') || [];
	console.log(quadToString(quads[0]));
</script>

For streaming in browsers, StreamParser works with Web Streams. It can be passed to pipeThrough() or used through its import() convenience method:

import { StreamParser, quadToString } from 'rdf-parser-ts/browser';

const parser = new StreamParser({ baseIRI: 'https://example.org/' });
const rdfStream = new Blob(['<s> <p>', ' <o>.']).stream();

for await (const quad of rdfStream.pipeThrough(parser)) {
	console.log(quadToString(quad));
}

Current minified browser bundle sizes after npm run build, measured with gzip -9 for the compressed column:

Bundle Minified gzip compressed
dist/browser/index.mjs 42,908 bytes (41.9 KiB) 11,032 bytes (10.8 KiB)
dist/browser/index.global.js 43,393 bytes (42.4 KiB) 11,223 bytes (11.0 KiB)

Parsing strings

import { Parser, quadToString } from 'rdf-parser-ts';

const parser = new Parser({ baseIRI: 'http://example.org/' });
const quads = parser.parse(`
	@prefix ex: <http://example.com/>.
	ex:s ex:p "hello"@en;
			 ex:n 42;
			 a ex:Thing.
`);

for (const quad of quads ?? []) {
	console.log(quad.subject.termType, quad.predicate.value, quad.object.value);
	console.log(quadToString(quad));
}

Parser#parse() returns RDF-JS quads when no callback is provided. With a callback, it follows the N3.js-style callback flow and calls the callback once per quad, then once with quad === null and the prefix map.

const parser = new Parser();

parser.parse('<s> <p> <o>.', (error, quad, prefixes) => {
	if (error) throw error;
	if (quad) console.log(quad);
	else console.log('done', prefixes);
});
Parser options
  • baseIRI / baseIRIPath: resolve relative IRIs.
  • format: hint the input format, such as text/turtle, application/n-triples, application/n-quads, or application/trig.
  • factory: custom RDF-JS data factory.
  • comments: emit comment events in streaming mode.
  • relax: enable the faster relaxed line-format path for generated input.
  • rdfMessages / messages: force RDF Messages mode.
  • version: set the RDF version label; messages versions such as 1.2-messages enable RDF Messages mode.
  • parseUnsupportedVersions: accept unsupported version labels for compatibility testing.

RDF Messages

RDF Messages mode is enabled automatically when the input contains a messages version label, such as VERSION "1.2-messages" or @version "1.2-messages" .. It can also be enabled explicitly with rdfMessages: true or messages: true.

When RDF Messages mode is active, Parser#parse() returns entries that contain both the parsed quad and the message counter. Counters start at 0 and increase at each MESSAGE or @message . delimiter.

import { Parser, isMessageQuad, quadToString } from 'rdf-parser-ts';

const output = new Parser().parse(`
	VERSION "1.2-messages"
	<http://example.org/s1> <http://example.org/p> <http://example.org/o1> .
	MESSAGE
	<http://example.org/s2> <http://example.org/p> <http://example.org/o2> .
`);

for (const entry of output ?? []) {
	if (isMessageQuad(entry)) {
		console.log(entry.messageCounter, quadToString(entry.quad));
	}
}

The callback form still emits quads, with an additional optional message-counter argument when RDF Messages mode is active:

new Parser().parse(input, (error, quad, prefixes, messageCounter) => {
	if (error) throw error;
	if (quad) console.log(messageCounter, quadToString(quad));
});

Use toMessages() to group parser output into Message instances. Message extends Array and contains the quads belonging to one RDF Message. Empty messages are preserved when the input contains delimiters before the first quad or between two delimiters.

import { Parser, toMessages } from 'rdf-parser-ts';

const output = new Parser({ rdfMessages: true }).parse(`
	MESSAGE
	<http://example.org/s> <http://example.org/p> <http://example.org/o> .
`);

const messages = toMessages(output ?? []);
console.log(messages[0]?.length); // 0
console.log(messages[1]?.length); // 1

For direct message-level parsing, use parseMessages():

const messages = new Parser({ baseIRI: 'http://example.org/' }).parseMessages(`
	VERSION "1.2-messages"
	<s1> <p> <o1> .
	MESSAGE
	<s2> <p> <o2> .
`);

Blank node labels are scoped per message in RDF Messages mode, so the same blank node label in two messages produces distinct blank node terms.

Streaming and incremental parsing

StreamParser is a Node.js Transform stream in object mode. It accepts string or Buffer chunks and emits RDF-JS quads. In RDF Messages mode, it emits { quad, messageCounter } entries and a messageCounter event for each parsed quad.

import { createReadStream } from 'node:fs';
import { StreamParser } from 'rdf-parser-ts';

const parser = new StreamParser({
	baseIRI: 'http://example.org/',
	format: 'application/n-quads',
});

createReadStream('data.nq')
	.pipe(parser)
	.on('data', quad => {
		console.log(quad.subject.value, quad.predicate.value, quad.object.value);
	})
	.on('prefix', (prefix, iri) => {
		console.log('prefix', prefix, iri.value);
	})
	.on('comment', comment => {
		console.log('comment', comment);
	});

The import() convenience method mirrors N3.js:

const parser = new StreamParser();
parser.import(createReadStream('data.ttl')).on('data', quad => console.log(quad));

IncrementalParser exposes the same chunking logic without Node streams. Call write(chunk) for each partial input chunk and end(optionalFinalChunk) when the input is complete; both methods return any complete RDF-JS output items parsed from the accumulated text.

RDF-JS data model

The default DataFactory creates RDF-JS-compatible terms:

import { DataFactory } from 'rdf-parser-ts';

const s = DataFactory.namedNode('http://example.org/s');
const p = DataFactory.namedNode('http://example.org/p');
const o = DataFactory.literal('hello', 'en');
const q = DataFactory.quad(s, p, o);

console.log(q.termType);        // Quad
console.log(q.object.termType); // Literal
console.log(q.equals(q));       // true

Public exports include:

  • Parser: string parser with parse() and parseMessages().
  • StreamParser: Node/browser stream parser with import().
  • IncrementalParser: chunked parser with write() and end().
  • DataFactory: RDF-JS factory for namedNode(), blankNode(), literal(), variable(), defaultGraph(), and quad().
  • NamedNode, BlankNode, Literal, Variable, DefaultGraph, Quad, and Message: lightweight RDF-JS term classes.
  • Factory aliases: namedNode, blankNode, literal, variable, defaultGraph, and quad.
  • termToString(): serialize a term for diagnostics and IDs.
  • quadToString(): serialize a quad in line syntax.
  • termToId() / termFromId(): convert terms to and from stable string IDs.
  • isMessageQuad(): type guard for { quad, messageCounter } entries.
  • toMessages(): group parser output into Message[].

Custom RDF-JS factories

Pass a custom factory to produce terms owned by another RDF-JS implementation, such as Comunica’s data factory.

import { StreamParser } from 'rdf-parser-ts';

const parser = new StreamParser({
	factory: dataFactory,
	baseIRI: action.metadata?.baseIRI,
	format: mediaType,
	parseUnsupportedVersions: true,
	version: action.metadata?.version,
});

This option shape matches the usage pattern in Comunica's ActorRdfParseN3: a consumer can replace import { StreamParser } from 'n3' with import { StreamParser } from 'rdf-parser-ts' for evaluation.

CLI

After building or installing the package, the rdf-parser-ts binary reads RDF from a file or stdin and writes N-Quads-style output.

rdf-parser-ts --base http://example.org/ data.ttl
cat data.nq | rdf-parser-ts --format application/n-quads

Options:

  • --format, -f: format hint, such as text/turtle or application/n-quads.
  • --base, -b: base IRI for relative IRIs.
  • --help, -h: print usage.

RDF Working Group test suites

The spec/ setup mirrors N3.js:

  • spec/parser.cjs implements the rdf-test-suite parser interface by piping streamify-string(data) into new StreamParser(...) and collecting with arrayify-stream.
  • spec/earl-meta.json contains metadata for EARL report generation.
  • .rdf-test-suite-cache/ is used for downloaded manifests.
  • Library-specific RDF Messages tests cover VERSION and @version, MESSAGE and @message ., message counters, empty messages, final delimiters, repeated prefixes, named graphs, blank-node scoping, and delimiter errors.

Run all configured compliance suites with:

npm run spec

Run focused suites when iterating:

npm run spec-1-1-ntriples
npm run spec-1-1-nquads
npm run spec-1-1-turtle
npm run spec-1-1-trig
npm run spec-1-2-ntriples
npm run spec-1-2-nquads
npm run spec-1-2-turtle
npm run spec-1-2-trig

Generate EARL reports with npm run spec-1-1-earl or npm run spec-1-2-earl, and use npm run spec-clean to remove the manifest cache.

The N-Triples and N-Quads RDF 1.1/RDF 1.2 scripts run without skips. The Turtle and TriG scripts run the same official manifests with explicit --skip patterns for currently unsupported edge cases such as full PN_CHARS Unicode coverage, escaped prefixed names, some IRI-resolution cases, RDF 1.2 annotation/reifier syntax, and Turtle/TriG version directives. This keeps npm run spec reproducible and green while making remaining conformance work visible in package.json.

Performance benchmarks

The benchmark generates synthetic RDF 1.2 N-Quads-like input with a mix of default-graph triples, named-graph quads, IRI objects, string literals, language-tagged literals, numeric/boolean literals, and triple terms as objects. Default sizes are 10,000, 100,000, and 1,000,000 statements.

npm run perf
npm run perf:quick
node perf/bench.js --sizes 10000,50000 --no-n3
node perf/bench.js --sizes 10000,50000 --no-triple-terms

Graphy 4.x's N-Quads reader does not parse RDF 1.2 triple terms, so the default triple-term benchmark prints a skipped Graphy row. Use --no-triple-terms or npm run perf:graphy for direct rdf-parser-ts, N3.js, Graphy, and Graphy relaxed-mode numbers on the same generated line-format input.

Quick benchmark snapshot

The following results were captured with Node.js v25.9.0 on Linux x64 using the quick benchmark commands. They are intended as a local performance snapshot, not as stable release guarantees; larger runs with npm run perf and node --expose-gc are more representative.

Default RDF 1.2 triple-term input, from npm run perf:quick:

Statements Parser Time Throughput Input RSS delta
1,000 rdf-parser-ts 0.002s 439,540 q/s 0.1 MiB 2.1 MiB
1,000 rdf-parser-ts/relax 0.001s 782,497 q/s 0.1 MiB 0.4 MiB
1,000 N3.js 0.008s 127,099 q/s 0.1 MiB 1.9 MiB
10,000 rdf-parser-ts 0.023s 435,162 q/s 1.1 MiB 5.6 MiB
10,000 rdf-parser-ts/relax 0.012s 839,620 q/s 1.1 MiB 5.0 MiB
10,000 N3.js 0.047s 214,567 q/s 1.1 MiB 6.6 MiB

Line-format input without RDF 1.2 triple terms, from node perf/bench.js --sizes 1000,10000 --no-triple-terms:

Statements Parser Time Throughput Input RSS delta
1,000 rdf-parser-ts 0.001s 773,045 q/s 0.1 MiB 0.5 MiB
1,000 rdf-parser-ts/relax 0.002s 525,116 q/s 0.1 MiB 0.3 MiB
1,000 N3.js 0.006s 166,228 q/s 0.1 MiB 1.9 MiB
1,000 Graphy 0.003s 317,648 q/s 0.1 MiB 1.9 MiB
1,000 Graphy/relax 0.002s 594,226 q/s 0.1 MiB -1.0 MiB
10,000 rdf-parser-ts 0.010s 959,069 q/s 0.9 MiB 1.4 MiB
10,000 rdf-parser-ts/relax 0.010s 982,829 q/s 0.9 MiB 4.7 MiB
10,000 N3.js 0.026s 386,462 q/s 0.9 MiB 3.3 MiB
10,000 Graphy 0.011s 881,601 q/s 0.9 MiB 7.5 MiB
10,000 Graphy/relax 0.005s 1,857,713 q/s 0.9 MiB 2.4 MiB

On these generated inputs, the strict parser is ahead of N3.js, and relax: true improves the RDF 1.2 triple-term case by reducing validation overhead on hot line-format paths. The no-triple-term run shows the intended fast-path shape most clearly: common escapeless N-Quads statements are parsed with direct index scanning, bounded named-node caching, and fallback only when the specialized parser cannot handle a line. Graphy remains a strong baseline for ordinary N-Quads and benefits from its own relaxed mode, but the current Graphy reader is skipped for the default RDF 1.2 triple-term workload. Memory deltas in the quick run are noisy because the process is short-lived and includes JIT, parser warmup, and garbage-collection timing.

For cleaner memory measurements, run Node with explicit garbage collection:

npm run build
node --expose-gc perf/bench.js

Performance-oriented implementation notes

  • The parser uses a single pass over the input string.
  • It tracks positions with numeric indexes and avoids token objects.
  • It emits quads directly from parse routines.
  • For strict N-Triples/N-Quads input, it uses a Graphy-inspired fast path for common escapeless statements before falling back to the general parser.
  • The fast path caches recurring predicate, datatype, and graph named nodes in a bounded cache.
  • A relax: true option mirrors Graphy's relaxed mode by skipping part of the validation cost on hot line-format paths and by fast-parsing common RDF 1.2 triple-term objects; spec tests use strict validation by default.
  • The default data model has small classes with simple equals() implementations.
  • StreamParser incrementally parses complete statement prefixes and only retains incomplete trailing input between chunks; very large single statements still need to be held until their terminating boundary arrives.

License

Ghent University - IMEC

MIT Licensed

Keywords