rdf-parser-ts
v0.3.1
Published
Fast RDF-JS parser for Turtle, TriG, N-Triples, N-Quads, and RDF Messages for NodeJS and browsers.
Maintainers
Readme
RDF Parser for TypeScript
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 the interactive browser demo at https://www.pieter.pm/rdf-parser.ts/.
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.
[!NOTE] 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-tsPackage layout
src/index.tscontains the RDF-JS data model, parser, stream parser, incremental parser, and parser helpers.src/bin/rdf-parser.tsprovides therdf-parser-tsCLI.test/contains Vitest unit tests.spec/contains therdf-test-suiteadapter and EARL metadata, matching the N3.js spec-test setup.perf/contains synthetic performance benchmarks against N3.js and Graphy.dist/is generated bynpm 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 termsThe 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 | 31,617 bytes (30.9 KiB) | 8,267 bytes (8.1 KiB) |
| dist/browser/index.global.js | 32,099 bytes (31.3 KiB) | 8,462 bytes (8.3 KiB) |
The example/ folder contains a browser-only playground. It parses RDF with this package, reports quads/messages per second, and currently uses N3 in the browser for optional serialization until the standalone rdf-writer.ts package is wired in.
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 astext/turtle,application/n-triples,application/n-quads, orapplication/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 as1.2-messagesenable 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); // 1For 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)); // truePublic exports include:
Parser: string parser withparse()andparseMessages().StreamParser: Node/browser stream parser withimport().IncrementalParser: chunked parser withwrite()andend().DataFactory: RDF-JS factory fornamedNode(),blankNode(),literal(),variable(),defaultGraph(), andquad().NamedNode,BlankNode,Literal,Variable,DefaultGraph,Quad, andMessage: lightweight RDF-JS term classes.- Factory aliases:
namedNode,blankNode,literal,variable,defaultGraph, andquad. 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 intoMessage[].
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-quadsOptions:
--format,-f: format hint, such astext/turtleorapplication/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.cjsimplements therdf-test-suiteparser interface by pipingstreamify-string(data)intonew StreamParser(...)and collecting witharrayify-stream.spec/earl-meta.jsoncontains metadata for EARL report generation..rdf-test-suite-cache/is used for downloaded manifests.- Library-specific RDF Messages tests cover
VERSIONand@version,MESSAGEand@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 specRun 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-trigGenerate 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-termsGraphy 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.jsPerformance-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: trueoption 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. StreamParserincrementally 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
