From 0fd4b99a8fb364fffcecbc3064b4279d5bd8bfe0 Mon Sep 17 00:00:00 2001 From: EC2 Default User Date: Sat, 4 Jul 2026 15:34:10 +0000 Subject: [PATCH 1/6] feat: replace the hand-rolled N3 parser with N3.js across the Turtle/N3 family Delete src/n3parser.js (1,610 lines) and src/lists.ts (121 lines) and route every Turtle-family content type through the N3.js parser via a single adapter (src/n3-adapter.ts): - text/turtle, application/x-turtle strict Turtle - text/n3, application/n3 full Notation3 - application/n-triples (new) - application/n-quads (+nquads alias) unified - application/trig (new) The adapter maps N3.js's flat quad stream onto rdflib's model: ( ... ) collections are folded into Collection terms with N3.js's own Store#extractLists machinery; N3 formulae { ... } are rebuilt into Formula sub-stores; @forAll/@forSome (reified by N3.js under explicitQuantifiers) are registered via newUniversal/declareExistential; ?x becomes a Variable; =, =>, <= map natively to owl:sameAs / (reversed) log:implies. The Fetcher's N3Handler now delegates to parse() instead of driving the legacy parser, and sparqlUpdateParser builds its clause formulae by rewriting the patch into N3 and running it through the same adapter (gaining SPARQL-style PREFIX support, #651). Legacy leniencies preserved: the empty prefix ":" still defaults to (retry-once on N3.js's specific "Undefined prefix" error), and lone rdf:nil / reified first-rest chains still fold into collections. Dropped, now loud parse errors (see PR body): @keywords, bareword date/dateTime literals, `has`/`is ... of`, string-literal subjects, and lexical normalisation of numeric/boolean literals. Also fixes the Turtle serializer to accept the spec lexical forms "true"/"false" for xsd:boolean, and repairs an N3.js n3-mode label-scoping bug for `_:x` mentioned inside [ ... ] property lists. Breaking: the N3Parser export is gone; n-quads default-graph statements are now attributed to the document graph; text/turtle is now strict Turtle (full N3 under a turtle content type no longer parses). Co-Authored-By: Claude Fable 5 --- src/fetcher.ts | 17 +- src/formula.ts | 3 +- src/index.ts | 2 - src/lists.ts | 121 -- src/n3-adapter.ts | 329 +++++ src/n3parser.js | 1610 ------------------------ src/parse.ts | 54 +- src/patch-parser.js | 221 +++- src/serializer.js | 5 +- src/types.ts | 3 + tests/serialize/structures.n3 | 27 +- tests/serialize/t1-ref.xml | 4 +- tests/serialize/t1.ttl | 2 +- tests/serialize/t11-ref.xml | 30 +- tests/serialize/t12-ref.ttl | 12 +- tests/serialize/t13-ref.ttl | 12 +- tests/serialize/t17-ref.xml | 134 +- tests/serialize/t2-ref.xml | 4 +- tests/serialize/t2.ttl | 2 +- tests/serialize/t7-ref.nt | 2 +- tests/serialize/t7.n3 | 2 +- tests/unit/fetcher-test.js | 30 +- tests/unit/indexed-formula-test.js | 26 +- tests/unit/lists-test.js | 7 - tests/unit/parse-turtle-family-test.js | 316 +++++ tests/unit/patch-parser-test.js | 66 + tests/unit/serialize-test.js | 4 +- 27 files changed, 1069 insertions(+), 1976 deletions(-) delete mode 100644 src/lists.ts create mode 100644 src/n3-adapter.ts delete mode 100644 src/n3parser.js create mode 100644 tests/unit/parse-turtle-family-test.js diff --git a/src/fetcher.ts b/src/fetcher.ts index 91792b3cd..30017c073 100644 --- a/src/fetcher.ts +++ b/src/fetcher.ts @@ -27,7 +27,6 @@ */ import IndexedFormula from './store' import log from './log' -import N3Parser from './n3parser' import RDFlibNamedNode from './named-node' import Namespace from './namespace' import rdfParse from './parse' @@ -592,20 +591,24 @@ class N3Handler extends Handler { } & Options, response: ExtendedResponse ): ExtendedResponse | Promise { - // Parse the text of this N3 file + // Parse the text of this Turtle or N3 file, through the same parse() + // entry point (and N3.js-based parser) as everything else. let kb = fetcher.store - let p = N3Parser(kb, kb, options.original.value, options.original.value, - null, null, '', null) - // p.loadBuf(xhr.responseText) + const normalized = (fetcher.normalizedContentType(options as AutoInitOptions, response.headers) || '').split(';')[0] + // The handler's pattern also matches legacy aliases such as + // application/rdf+n3 or text/x-turtle; normalize them to the canonical + // content type of the same syntax. + const contentType = /n3/.test(normalized) ? 'text/n3' : 'text/turtle' try { - p.loadBuf(responseText) + rdfParse(responseText, kb, options.original.value, contentType) } catch (err) { let msg = 'Error trying to parse ' + options.resource + ' as Notation3:\n' + err // not err.stack -- irrelevant return fetcher.failFetch(options, msg, 'parse_error', response) } - fetcher.addStatus(options.req, 'N3 parsed: ' + p.statementCount + ' triples in ' + p.lines + ' lines.') + const statementCount = kb.statementsMatching(null, null, null, options.original).length + fetcher.addStatus(options.req, 'N3 parsed: ' + statementCount + ' triples in ' + responseText.split('\n').length + ' lines.') fetcher.store.add(options.original, ns.rdf('type'), ns.link('RDFDocument'), fetcher.appNode) return fetcher.doneFetch(options, this.response) diff --git a/src/formula.ts b/src/formula.ts index 2a73eb825..099db9e1f 100644 --- a/src/formula.ts +++ b/src/formula.ts @@ -677,7 +677,8 @@ export default class Formula extends Node { } /** - * Used by the n3parser to generate list elements + * Creates a collection (or an rdf:first/rest chain when the factory has no + * collection support) from a list of values * @param values - The values of the collection * @param context - The store * @return {BlankNode|Collection} - The term for the statement diff --git a/src/index.ts b/src/index.ts index 93c79c2a6..52a4b206b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,7 +9,6 @@ import Store from './store' import jsonParser from './jsonparser' import Literal from './literal' import log from './log' -import N3Parser from './n3parser' import NamedNode from './named-node' import Namespace from './namespace' import Node from './node' @@ -73,7 +72,6 @@ export { jsonParser, Literal, log, - N3Parser, NamedNode, Namespace, Node, diff --git a/src/lists.ts b/src/lists.ts deleted file mode 100644 index cf4c067a3..000000000 --- a/src/lists.ts +++ /dev/null @@ -1,121 +0,0 @@ -/* Lists form conversion -*/ - - -// import DataFactory from './factories/extended-term-factory' -// import jsonldParser from './jsonldparser' -// @ts-ignore is this injected? -import { Parser as N3jsParser } from 'n3' // @@ Goal: remove this dependency -// import N3Parser from './n3parser' -// import { parseRDFaDOM } from './rdfaparser' -// import RDFParser from './rdfxmlparser' -// import sparqlUpdateParser from './patch-parser' -// import * as Util from './utils-js' -import Node from './node-internal' -// import BlankNode from './blank-node' -// import NamedNode from './named-node' -import Collection from './collection' -import Statement from './statement' -// import Formula from './formula' -import Store from './store' -// import { ContentType, TurtleContentType, N3ContentType, RDFXMLContentType, XHTMLContentType, HTMLContentType, SPARQLUpdateContentType, SPARQLUpdateSingleMatchContentType, JSONLDContentType, NQuadsContentType, NQuadsAltContentType } from './types' - -// import { Quad } from './tf-types' - -import {BlankNode, NamedNode, Quad, Term,} from './tf-types' -import Namespace from './namespace' - -const RDF = Namespace('http://www.w3.org/1999/02/22-rdf-syntax-ns#') - -/* Replace a given node with another node throughout a given document -* -* we do the predicate as well for complenesss though we don't expect Collections to use it -*/ -export function substituteInDoc (store:Store, x:Term, y:Term, doc?: NamedNode ) { - // console.log(`substituteInDoc put ${x} for ${y} in ${doc}}`) - for (const quad of store.statementsMatching(y as any, null, null, doc as any)) { - const newStatement = new Statement(x as any, quad.predicate, quad.object, doc as any) - store.remove(quad) - store.add(newStatement) - } - for (const quad of store.statementsMatching(null, y as any, null, doc) as any) { - store.remove(quad) - // console.log(` substituteInDoc predicate ${x} in ${quad}}`) - store.add(new Statement(quad.subject, x as any, quad.object, doc as any)) - } - for (const quad of store.statementsMatching(null, null, y as any, doc) as any) { - store.remove(quad) - store.add(new Statement(quad.subject, quad.predicate, x as any, doc as any)) - } -} - -/* Change all lone rdf:nil nodes into empty Collections -*/ -export function substituteNillsInDoc (store:Store, doc?: NamedNode) { - const x = RDF('nil') - for (const quad of store.statementsMatching(x as any, null, null, doc as any)) { - store.remove(quad) - const y = new Collection() - store.add(new Statement(y as any, quad.predicate, quad.object, doc as any)) - } - for (const quad of store.statementsMatching(null, null, x as any, doc) as any) { - if (!quad.predicate.sameTerm(RDF('rest'))) { // If not a tail - store.remove(quad) - const y = new Collection() - store.add(new Statement(quad.subject, quad.predicate, y as any, doc as any)) - } - } -} -/** - * Convert lists reified as rdf:first, rest - * Normal method is sync. - * Unfortunately jsdonld is currently written to need to be called async. - * Hence the mess below with executeCallback. - * @param store - The quadstore - * @param doc - The document in which the conversion is done - */ - - -export function convertFirstRestNil ( - store: Store, - doc: NamedNode | undefined, // Do whole store? -) { - - function preceding (ele:BlankNode, listSoFar: Node[], trash: Quad[]): undefined { - - const rests = store.statementsMatching(ele, RDF('rest'), null, doc) - if (rests.length !== 1) throw new Error(`Bad list structure: no rest at ${ele}`) - - const firsts = store.statementsMatching(ele, RDF('first'), null, doc) - if (firsts.length !== 1) throw new Error(`Bad list structure: rest but ${firsts.length} firsts at ${ele}`) - const value = firsts[0].object - const total = [value].concat(listSoFar as any) - // console.log(' List now is: ', total) - const totalTrash = trash.concat(rests).concat(firsts) - - const pres = store.statementsMatching(null, RDF('rest'), ele, doc) - if (pres.length === 0) { // Head of the list - const newList = new Collection(total) - store.remove(totalTrash) - // Replace old list with new list: - substituteInDoc(store, newList, ele, doc) - return - } - if (pres.length !== 1) throw new Error(`Bad list structure: ${pres.length} pres at ${ele}`) - const pre = pres[0].subject - if (pre.termType !== 'BlankNode') throw new Error(`Bad list element node ${pre} type: ${pre.termType} `) - - preceding(pre, total, totalTrash) - return - } - - substituteNillsInDoc(store, doc) // lone ones only - - const tails = store.statementsMatching(null, RDF('rest'), RDF('nil'), doc) - tails.forEach(tail => { - if (tail.subject.termType !== 'BlankNode') - throw new Error(`Bad list element node ${tail.subject} type: ${tail.subject.termType} `) - preceding(tail.subject, [], []) - }) - -} diff --git a/src/n3-adapter.ts b/src/n3-adapter.ts new file mode 100644 index 000000000..76119af82 --- /dev/null +++ b/src/n3-adapter.ts @@ -0,0 +1,329 @@ +/** + * Adapter between the N3.js parser and rdflib's data model. + * + * All Turtle-family content types (Turtle, TriG, N-Triples, N-Quads and full + * Notation3) are parsed by the spec-compliant, optimised N3.js parser; this + * module maps its flat quad stream back onto rdflib's richer model: + * + * - `( … )` collections are folded into rdflib `Collection` terms using + * N3.js's own list machinery (`Store#extractLists`); + * - N3 formulae `{ … }` (which N3.js emits as quads whose graph is a fresh + * blank node) are rebuilt into rdflib `Formula` sub-stores; + * - `@forAll` / `@forSome` declarations (reified by N3.js under + * `explicitQuantifiers`) are registered through `newUniversal` / + * `declareExistential`, as the legacy parser did; + * - `?x` becomes a `Variable`, `=` becomes `owl:sameAs`, `=>`/`<=` become + * (reversed) `log:implies` — all handled natively by N3.js's n3 mode. + * + * Statements in the default graph are attributed to the document graph + * `kb.sym(base)` — rdflib's provenance convention — while explicit named + * graphs (TriG / N-Quads) are kept as-is. + */ +import { Parser as N3jsParser, Store as N3jsStore, DataFactory as N3jsDataFactory } from 'n3' +import Collection from './collection' +import Formula from './formula' +import Variable from './variable' +import { + TurtleContentType, + TurtleLegacyContentType, + N3ContentType, + N3LegacyContentType, + NTriplesContentType, + NQuadsContentType, + NQuadsAltContentType, + TrigContentType, +} from './types' + +const RDF_NS = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#' +const RDF_FIRST = RDF_NS + 'first' +const RDF_REST = RDF_NS + 'rest' +const RDF_NIL = RDF_NS + 'nil' + +/** The graph N3.js reifies quantifier declarations into (explicitQuantifiers). */ +const QUANTIFIERS_GRAPH = 'urn:n3:quantifiers' +const REIFY_FOR_ALL = 'http://www.w3.org/2000/10/swap/reify#forAll' +const REIFY_FOR_SOME = 'http://www.w3.org/2000/10/swap/reify#forSome' + +/** Distinct document-label prefix per n3-mode parse (see label repair below). */ +let docLabelCounter = 0 + +/** + * The rdflib content types parsed by N3.js, mapped to the N3.js `format` + * string that enforces the right grammar for each. + */ +export const N3JS_FORMATS: { [contentType: string]: string } = { + [TurtleContentType]: 'text/turtle', + [TurtleLegacyContentType]: 'text/turtle', + [N3ContentType]: 'text/n3', + [N3LegacyContentType]: 'text/n3', + [NTriplesContentType]: 'application/n-triples', + [NQuadsContentType]: 'application/n-quads', + [NQuadsAltContentType]: 'application/n-quads', + [TrigContentType]: 'application/trig', +} + +/** + * Parse a Turtle-family document with N3.js and load it into the given store. + * + * @param str - The document body + * @param kb - The store (or plain Formula) to load the statements into + * @param base - The base IRI for relative-IRI resolution; also names the document graph + * @param contentType - One of the content types in {@link N3JS_FORMATS} + * @returns The number of statements loaded + */ +export default function parseN3js (str: string, kb: Formula, base: string, contentType: string): number { + const format = N3JS_FORMATS[contentType] + if (!format) { + throw new Error('parseN3js: unsupported content type ' + contentType) + } + const sugared = format === 'text/n3' || format === 'text/turtle' + try { + return runParse(str, kb, base, format, false) + } catch (e) { + // rdflib's legacy parser implicitly bound the empty prefix `:` to + // ``, so documents using `:name` without declaring `@prefix :` + // parsed anyway. N3.js is strict, so when (and only when) it reports an + // undefined empty prefix in one of the syntaxes the legacy parser + // accepted, re-parse once with a synthetic `@prefix : .` seeded. + // Because the declaration is prepended, a later in-document declaration + // still overrides it from that point on, exactly like the sequential + // semantics of the legacy parser. + if (sugared && base && /Undefined prefix ":"/.test(String(e && (e as Error).message))) { + return runParse(str, kb, base, format, true) + } + throw e + } +} + +function runParse (str: string, kb: Formula, base: string, format: string, seedEmptyPrefix: boolean): number { + const n3Mode = format === 'text/n3' + const rdfFactory = kb.rdfFactory + const docGraph = base ? kb.sym(base) : rdfFactory.defaultGraph() + const foldLists = (n3Mode || format === 'text/turtle') && + !!(rdfFactory && rdfFactory.supports && rdfFactory.supports['COLLECTIONS']) + + // --- 1. Parse (synchronously — N3.js throws on error in this mode) ------- + const prefixes: { [prefix: string]: string } = {} + let sawSyntheticEmptyPrefix = false + const input = seedEmptyPrefix ? '@prefix : <' + escapeIri(base) + '#>.\n' + str : str + // In n3 mode, N3.js mis-scopes a document-labelled blank node (`_:c`) + // mentioned inside a `[ … ]` property list: it gets the label + // `.c` instead of the document-wide label. At the top + // level the graph label is empty, so such labels start with "." — we pass an + // explicit per-parse prefix so they can be renamed back to the document + // label (inside a formula the formula label is used, which is the correct + // formula-wide scoping, so those are left alone). + const docLabelPrefix = 'dl' + docLabelCounter++ + '_' + const parser = new N3jsParser({ + baseIRI: base || undefined, + format, + explicitQuantifiers: n3Mode, + blankNodePrefix: n3Mode ? docLabelPrefix : undefined, + } as any) + const parsed: any[] = (parser as any).parse(input, { + onPrefix: (prefix: string, node: any) => { + if (seedEmptyPrefix && prefix === '' && !sawSyntheticEmptyPrefix) { + sawSyntheticEmptyPrefix = true // the synthetic binding is not the document's + return + } + prefixes[prefix] = node && node.value !== undefined ? node.value : String(node) + } + }) + + // --- 2. Set aside the empty-formula markers ------------------------------ + // N3.js signals an empty formula `{}` with a degenerate marker quad that has + // no predicate; its graph is the empty formula's blank node. + const emptyFormulaLabels: string[] = [] + const quads: any[] = [] + for (const q of parsed) { + if (!q.predicate) { + emptyFormulaLabels.push(q.graph.value) + } else { + quads.push(q) + } + } + + // --- 3. List and quantifier work, via N3.js's own store machinery -------- + // Only build the intermediate N3.Store when there is work for it: full N3 + // (formulae/quantifiers possible), or a document that actually mentions + // rdf:nil (every complete list ends in one — same gate the legacy parser + // used for its list folding). + const listItems: { [head: string]: any[] } = {} + const quantifiers: Array<{ scopeLabel: string | null, forAll: boolean, vars: string[] }> = [] + const mentionsNil = (q: any): boolean => + (q.object.termType === 'NamedNode' && q.object.value === RDF_NIL) || + (q.subject.termType === 'NamedNode' && q.subject.value === RDF_NIL) + + // Quads consumed by the list/quantifier machinery are dropped from the + // stream; the survivors are still loaded in original document order (which + // matters: a formula must be populated before an outer statement that + // mentions it is checked against the store's duplicate suppression). + let isLive: (q: any) => boolean = () => true + + if (n3Mode || (foldLists && quads.some(mentionsNil))) { + const n3Store = new N3jsStore() + n3Store.addQuads(quads) + isLive = (q: any): boolean => n3Store.has(q) + + if (n3Mode) { + // Under explicitQuantifiers, each `@forAll x, y` / `@forSome …` becomes + // reify:forAll ( x y ) in graph + // where is the default graph or the enclosing formula's label. + const quantifierQuads = n3Store.getQuads(null, null, null, N3jsDataFactory.namedNode(QUANTIFIERS_GRAPH)) + if (quantifierQuads.length) { + const firsts: { [id: string]: any } = {} + const rests: { [id: string]: any } = {} + const declarations: any[] = [] + for (const q of quantifierQuads) { + if (q.predicate.value === REIFY_FOR_ALL || q.predicate.value === REIFY_FOR_SOME) { + declarations.push(q) + } else if (q.predicate.value === RDF_FIRST) { + firsts[q.subject.value] = q.object + } else if (q.predicate.value === RDF_REST) { + rests[q.subject.value] = q.object + } + } + for (const decl of declarations) { + const vars: string[] = [] + let node: any = decl.object + while (node && node.value !== RDF_NIL && firsts[node.value] !== undefined) { + vars.push(firsts[node.value].value) + node = rests[node.value] + } + quantifiers.push({ + scopeLabel: decl.subject.termType === 'BlankNode' ? decl.subject.value : null, + forAll: decl.predicate.value === REIFY_FOR_ALL, + vars, + }) + } + n3Store.removeQuads(quantifierQuads) + } + } + + if (foldLists) { + // N3.js finds, validates and removes rdf:first/rest/nil chains for us. + // (Throwing mode: a malformed chain is a document error, as it was for + // the legacy parser's own folding.) + Object.assign(listItems, n3Store.extractLists({ remove: true })) + } + } + + // --- 4. Formula registry (n3 mode only) ---------------------------------- + // A blank node is a formula iff it occurs as a graph label (or is an + // empty-formula marker, or scopes a quantifier declaration). + const formulas: { [label: string]: Formula } = {} + const getFormula = (label: string): Formula => { + if (formulas[label] === undefined) { + formulas[label] = (kb as any).formula() + } + return formulas[label] + } + if (n3Mode) { + for (const label of emptyFormulaLabels) getFormula(label) + for (const q of quads) { + if (isLive(q) && q.graph && q.graph.termType === 'BlankNode') getFormula(q.graph.value) + } + for (const decl of quantifiers) { + if (decl.scopeLabel !== null) getFormula(decl.scopeLabel) + } + // Register the quantified variables on their scope, as the legacy parser + // did (the terms themselves stay NamedNodes in the statements). + for (const decl of quantifiers) { + const scope: any = decl.scopeLabel === null ? kb : getFormula(decl.scopeLabel) + for (const v of decl.vars) { + if (decl.forAll) { + if (typeof scope.newUniversal === 'function') scope.newUniversal(v) + } else if (typeof scope.declareExistential === 'function') { + scope.declareExistential(rdfFactory.namedNode(v)) + } + } + } + } + + // --- 5. Term conversion --------------------------------------------------- + const collections: { [head: string]: Collection } = {} + const collectionFor = (label: string): Collection => { + let c = collections[label] + if (c === undefined) { + c = rdfFactory.collection() + collections[label] = c // registered before resolving elements: cycle guard + for (const item of listItems[label]) { + c.append(convertTerm(item)) + } + } + return c + } + + const convertTerm = (t: any, isPredicate?: boolean): any => { + switch (t.termType) { + case 'NamedNode': + if (foldLists && !isPredicate) { + // `()` (and any other lone rdf:nil mention) becomes an empty + // Collection, matching the legacy parser. + if (t.value === RDF_NIL) return rdfFactory.collection() + if (listItems[t.value] !== undefined) return collectionFor(t.value) + } + return rdfFactory.namedNode(t.value) + case 'BlankNode': + if (n3Mode && formulas[t.value] !== undefined) return formulas[t.value] + if (foldLists && !isPredicate && listItems[t.value] !== undefined) return collectionFor(t.value) + // Repair N3.js's mis-scoped top-level document labels (see above). + if (n3Mode && t.value.charAt(0) === '.') return rdfFactory.blankNode(docLabelPrefix + t.value.slice(1)) + return rdfFactory.blankNode(t.value) + case 'Literal': + return rdfFactory.literal(t.value, t.language || rdfFactory.namedNode(t.datatype.value)) + case 'Variable': + return rdfFactory.variable ? rdfFactory.variable(t.value) : new Variable(t.value) + case 'DefaultGraph': + return docGraph + default: + // e.g. RDF-star quoted triples, which N3.js can parse but rdflib's + // store cannot represent. + throw new Error('rdflib cannot represent ' + t.termType + ' terms parsed from ' + format + ' input') + } + } + + // --- 6. Load the statements ---------------------------------------------- + // Statements whose graph is a formula's blank node go into that Formula; + // everything else goes into kb (default graph → the document graph, + // TriG/N-Quads named graphs kept as-is). Inner formula statements carry the + // document as their `why`, exactly like the legacy parser. + let count = 0 + for (const q of quads) { + if (!isLive(q)) continue + let target: any = kb + let why: any = docGraph + const g = q.graph + if (n3Mode && g && g.termType === 'BlankNode') { + target = getFormula(g.value) + } else if (g && g.termType !== 'DefaultGraph') { + why = convertTerm(g) + } + target.add(convertTerm(q.subject), convertTerm(q.predicate, true), convertTerm(q.object), why) + count++ + } + + for (const label in formulas) { + const f: any = formulas[label] + if (typeof f.close === 'function') f.close() + } + + // Register the document's prefix declarations so serialisation round-trips + // them (only stores implement setPrefixForURI; a plain Formula does not). + if (typeof (kb as any).setPrefixForURI === 'function') { + for (const prefix in prefixes) { + (kb as any).setPrefixForURI(prefix, prefixes[prefix]) + } + } + + return count +} + +/** Percent-escape the characters that cannot appear in a Turtle IRIREF. */ +function escapeIri (iri: string): string { + // eslint-disable-next-line no-control-regex + return iri.replace(/[<>"{}|^`\\\u0000-\u0020]/g, (ch) => { + const hex = ch.charCodeAt(0).toString(16).toUpperCase() + return '%' + (hex.length < 2 ? '0' + hex : hex) + }) +} diff --git a/src/n3parser.js b/src/n3parser.js deleted file mode 100644 index 60cb124fd..000000000 --- a/src/n3parser.js +++ /dev/null @@ -1,1610 +0,0 @@ -/** -* -* UTF-8 data encode / decode -* http://www.webtoolkit.info/ -* -**/ -import * as Uri from './uri' -import { ArrayIndexOf } from './utils' -import { convertFirstRestNil } from './lists' - -function hexify (str) { // also used in parser - return encodeURI(str) -} - -var Utf8 = { - // public method for url encoding - encode : function (string) { - string = string.replace(/\r\n/g,"\n") - var utftext = "" - - for (var n = 0; n < string.length; n++) { - - var c = string.charCodeAt(n) - - if (c < 128) { - utftext += String.fromCharCode(c) - } - else if((c > 127) && (c < 2048)) { - utftext += String.fromCharCode((c >> 6) | 192) - utftext += String.fromCharCode((c & 63) | 128) - } - else { - utftext += String.fromCharCode((c >> 12) | 224) - utftext += String.fromCharCode(((c >> 6) & 63) | 128) - utftext += String.fromCharCode((c & 63) | 128) - } - - } - - return utftext - }, - // public method for url decoding - decode : function (utftext) { - var string = "" - var i = 0 - - while ( i < utftext.length ) { - - var c = utftext.charCodeAt(i) - if (c < 128) { - string += String.fromCharCode(c) - i++ - } - else if((c > 191) && (c < 224)) { - string += String.fromCharCode(((c & 31) << 6) - | (utftext.charCodeAt(i+1) & 63)) - i += 2 - } - else { - string += String.fromCharCode(((c & 15) << 12) - | ((utftext.charCodeAt(i+1) & 63) << 6) - | (utftext.charCodeAt(i+2) & 63)) - i += 3 - } - } - return string - } -}// Things we need to define to make converted pythn code work in js -// environment of $rdf - -var RDFSink_forSomeSym = "http://www.w3.org/2000/10/swap/log#forSome" -var RDFSink_forAllSym = "http://www.w3.org/2000/10/swap/log#forAll" -var Logic_NS = "http://www.w3.org/2000/10/swap/log#" - -// pyjs seems to reference runtime library which I didn't find - -var pyjslib_Tuple = function(theList) { return theList } - -var pyjslib_List = function(theList) { return theList } - -var pyjslib_Dict = function(listOfPairs) { - if (listOfPairs.length > 0) - throw "missing.js: oops nnonempty dict not imp" - return [] -} - -var pyjslib_len = function(s) { return s.length } - -var pyjslib_slice = function(str, i, j) { - if (typeof str.slice == 'undefined') - throw '@@ mising.js: No .slice function for '+str+' of type '+(typeof str) - if ((typeof j == 'undefined') || (j ==null)) return str.slice(i) - return str.slice(i, j) // @ exactly the same spec? -} -var StopIteration = Error('dummy error stop iteration') - -var pyjslib_Iterator = function(theList) { - this.last = 0 - this.li = theList - this.next = function() { - if (this.last == this.li.length) throw StopIteration - return this.li[this.last++] - } - return this -} - -var ord = function(str) { - return str.charCodeAt(0) -} - -var string_find = function(str, s) { - return str.indexOf(s) -} - -var assertFudge = function(condition, desc) { - if (condition) return - if (desc) throw "python Assertion failed: "+desc - throw "(python) Assertion failed." -} - - -var stringFromCharCode = function(uesc) { - return String.fromCharCode(uesc) -} - - -String.prototype.encode = function(encoding) { - if (encoding != 'utf-8') throw "UTF8_converter: can only do utf-8" - return Utf8.encode(this) -} -String.prototype.decode = function(encoding) { - if (encoding != 'utf-8') throw "UTF8_converter: can only do utf-8" - //return Utf8.decode(this); - return this -} - - - -var uripath_join = function(base, given) { - return Uri.join(given, base) // sad but true -} - -var becauseSubexpression = null // No reason needed -var diag_tracking = 0 -var diag_chatty_flag = 0 -var diag_progress = function(str) { /*$rdf.log.debug(str);*/ } - -// why_BecauseOfData = function(doc, reason) { return doc }; - - -var RDF_type_URI = "http://www.w3.org/1999/02/22-rdf-syntax-ns#type" -var RDF_nil_URI = "http://www.w3.org/1999/02/22-rdf-syntax-ns#nil" -var DAML_sameAs_URI = "http://www.w3.org/2002/07/owl#sameAs" - -/* -function SyntaxError(details) { - return new __SyntaxError(details); -} -*/ - -function __SyntaxError(details) { - this.details = details -} - -/* - -$Id: n3parser.js 14561 2008-02-23 06:37:26Z kennyluck $ - -HAND EDITED FOR CONVERSION TO JAVASCRIPT - -This module implements a Nptation3 parser, and the final -part of a notation3 serializer. - -See also: - -Notation 3 -http://www.w3.org/DesignIssues/Notation3 - -Closed World Machine - and RDF Processor -http://www.w3.org/2000/10/swap/cwm - -To DO: See also "@@" in comments - -- Clean up interfaces -______________________________________________ - -Module originally by Dan Connolly, includeing notation3 -parser and RDF generator. TimBL added RDF stream model -and N3 generation, replaced stream model with use -of common store/formula API. Yosi Scharf developped -the module, including tests and test harness. - -*/ - -var ADDED_HASH = "#" -var LOG_implies_URI = "http://www.w3.org/2000/10/swap/log#implies" -var INTEGER_DATATYPE = "http://www.w3.org/2001/XMLSchema#integer" -var FLOAT_DATATYPE = "http://www.w3.org/2001/XMLSchema#double" -var DECIMAL_DATATYPE = "http://www.w3.org/2001/XMLSchema#decimal" -var DATE_DATATYPE = "http://www.w3.org/2001/XMLSchema#date" -var DATETIME_DATATYPE = "http://www.w3.org/2001/XMLSchema#dateTime" -var BOOLEAN_DATATYPE = "http://www.w3.org/2001/XMLSchema#boolean" -var option_noregen = 0 -var _notQNameChars = "\t\r\n !\"#$%&'()*.,+/;<=>?@[\\]^`{|}~" -var _notNameChars = ( _notQNameChars + ":" ) -var _rdfns = "http://www.w3.org/1999/02/22-rdf-syntax-ns#" -var N3CommentCharacter = "#" -var eol = new RegExp("^[ \\t]*(#[^\\n]*)?\\r?\\n", 'g') -var eof = new RegExp("^[ \\t]*(#[^\\n]*)?$", 'g') -var ws = new RegExp("^[ \\t]*", 'g') -var signed_integer = new RegExp("^[-+]?[0-9]+", 'g') -var number_syntax = new RegExp("^([-+]?[0-9]+)(\\.[0-9]+)?([eE][-+]?[0-9]+)?", 'g') -var datetime_syntax = new RegExp('^[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9](T[0-9][0-9]:[0-9][0-9](:[0-9][0-9](\\.[0-9]*)?)?)?Z?') - -// Reused in tight loops to detect whitespace or comment after a dot -var wsOrHash = new RegExp("[\\s#]") - -var digitstring = new RegExp("^[0-9]+", 'g') -var interesting = new RegExp("[\\\\\\r\\n\\\"]", 'g') -var langcode = new RegExp("^[a-zA-Z0-9]+(-[a-zA-Z0-9]+)*", 'g') - -// Returns true when a dot at position i should terminate a name, -// i.e., when the next character is whitespace, a comment start, or EOF -function dotTerminatesName(str, i) { - var next = str.charAt(i + 1) - return next === '' || wsOrHash.test(next) -} - -function createSinkParser(store, openFormula, thisDoc, baseURI, genPrefix, metaURI, flags, why) { - return new SinkParser(store, openFormula, thisDoc, baseURI, genPrefix, metaURI, flags, why) -} - -export default createSinkParser - -export class SinkParser { - constructor(store, openFormula, thisDoc, baseURI, genPrefix, metaURI, flags, why) { - if (typeof openFormula == 'undefined') openFormula=null - if (typeof thisDoc == 'undefined') thisDoc="" - if (typeof baseURI == 'undefined') baseURI=null - if (typeof genPrefix == 'undefined') genPrefix="" - if (typeof metaURI == 'undefined') metaURI=null - if (typeof flags == 'undefined') flags="" - if (typeof why == 'undefined') why=null - /* - note: namespace names should *not* end in #; - the # will get added during qname processing */ - - this._bindings = new pyjslib_Dict([]) - this._flags = flags - if ((thisDoc != "")) { - assertFudge((thisDoc.indexOf(":") >= 0), ( "Document URI not absolute: " + thisDoc ) ) - this._bindings[""] = ( ( thisDoc + "#" ) ) - } - this._store = store - if (genPrefix) { - store.setGenPrefix(genPrefix) - } - this._thisDoc = thisDoc - this.source = store.sym(thisDoc) - this.lines = 0 - this.statementCount = 0 - this.hasNil = false - this.startOfLine = 0 - this.previousLine = 0 - this._genPrefix = genPrefix - this.keywords = new pyjslib_List(["a", "this", "bind", "has", "is", "of", "true", "false"]) - this.keywordsSet = 0 - this._anonymousNodes = new pyjslib_Dict([]) - this._variables = new pyjslib_Dict([]) - this._parentVariables = new pyjslib_Dict([]) - this._reason = why - this._reason2 = null - if (diag_tracking) { - this._reason2 = why_BecauseOfData(store.sym(thisDoc), this._reason) - } - if (baseURI) { - this._baseURI = baseURI - } - else { - if (thisDoc) { - this._baseURI = thisDoc - } - else { - this._baseURI = null - } - } - assertFudge(!(this._baseURI) || (this._baseURI.indexOf(":") >= 0)) - if (!(this._genPrefix)) { - if (this._thisDoc) { - this._genPrefix = ( this._thisDoc + "#_g" ) - } - else { - this._genPrefix = RDFSink_uniqueURI() - } - } - if ((openFormula == null)) { - if (this._thisDoc) { - this._formula = store.formula( ( thisDoc + "#_formula" ) ) - } - else { - this._formula = store.formula() - } - } - else { - this._formula = openFormula - } - this._context = this._formula - this._parentContext = null - } - - - here(i) { - return ( ( ( ( this._genPrefix + "_L" ) + this.lines ) + "C" ) + ( ( i - this.startOfLine ) + 1 ) ) - }; - formula() { - return this._formula - }; - loadStream(stream) { - return this.loadBuf(stream.read()) - }; - loadBuf(buf) { - /* - Parses a buffer and returns its top level formula*/ - - this.startDoc() - this.feed(buf) - return this.endDoc() - }; - feed(octets) { - /* - Feed an octet stream tothe parser - - if BadSyntax is raised, the string - passed in the exception object is the - remainder after any statements have been parsed. - So if there is more data to feed to the - parser, it should be straightforward to recover.*/ - - var str = octets.decode("utf-8") - var i = 0 - while ((i >= 0)) { - var j = this.skipSpace(str, i) - if ((j < 0)) { - return - } - var i = this.directiveOrStatement(str, j) - if ((i < 0)) { - throw BadSyntax(this._thisDoc, this.lines, str, j, "expected directive or statement") - } - } - }; - directiveOrStatement(str, h) { - var i = this.skipSpace(str, h) - if ((i < 0)) { - return i - } - var j = this.directive(str, i) - if ((j >= 0)) { - return this.checkDot(str, j) - } - var j = this.statement(str, i) - if ((j >= 0)) { - return this.checkDot(str, j) - } - return j - }; - tok(tok, str, i) { - /* - Check for keyword. Space must have been stripped on entry and - we must not be at end of file.*/ - var whitespace = "\t\n\v\f\r " - if ((str.slice( i, ( i + 1 ) ) == "@")) { - var i = ( i + 1 ) - } - else { - if ((ArrayIndexOf(this.keywords,tok) < 0)) { - return -1 - } - } - var k = ( i + pyjslib_len(tok) ) - if ((str.slice( i, k) == tok) && (_notQNameChars.indexOf(str.charAt(k)) >= 0)) { - return k - } - else { - return -1 - } - }; - directive(str, i) { - var j = this.skipSpace(str, i) - if ((j < 0)) { - return j - } - var res = new pyjslib_List([]) - var j = this.tok("bind", str, i) - if ((j > 0)) { - throw BadSyntax(this._thisDoc, this.lines, str, i, "keyword bind is obsolete: use @prefix") - } - var j = this.tok("keywords", str, i) - if ((j > 0)) { - var i = this.commaSeparatedList(str, j, res, false) - if ((i < 0)) { - throw BadSyntax(this._thisDoc, this.lines, str, i, "'@keywords' needs comma separated list of words") - } - this.setKeywords(pyjslib_slice(res, null, null)) - if ((diag_chatty_flag > 80)) { - diag_progress("Keywords ", this.keywords) - } - return i - } - var j = this.tok("forAll", str, i) - if ((j > 0)) { - var i = this.commaSeparatedList(str, j, res, true) - if ((i < 0)) { - throw BadSyntax(this._thisDoc, this.lines, str, i, "Bad variable list after @forAll") - } - - var __x = new pyjslib_Iterator(res) - try { - while (true) { - var x = __x.next() - - - if (ArrayIndexOf(this._variables,x) < 0 || (ArrayIndexOf(this._parentVariables,x) >= 0)) { - this._variables[x] = ( this._context.newUniversal(x)) - } - - } - } catch (e) { - if (e != StopIteration) { - throw e - } - } - - return i - } - var j = this.tok("forSome", str, i) - if ((j > 0)) { - var i = this.commaSeparatedList(str, j, res, this.uri_ref2) - if ((i < 0)) { - throw BadSyntax(this._thisDoc, this.lines, str, i, "Bad variable list after @forSome") - } - - var __x = new pyjslib_Iterator(res) - try { - while (true) { - var x = __x.next() - - - this._context.declareExistential(x) - - } - } catch (e) { - if (e != StopIteration) { - throw e - } - } - - return i - } - var j = this.tok("prefix", str, i) - if ((j >= 0)) { - var t = new pyjslib_List([]) - var i = this.qname(str, j, t) - if ((i < 0)) { - throw BadSyntax(this._thisDoc, this.lines, str, j, "expected qname after @prefix") - } - var j = this.uri_ref2(str, i, t) - if ((j < 0)) { - throw BadSyntax(this._thisDoc, this.lines, str, i, "expected after @prefix _qname_") - } - var ns = t[1].uri - if (this._baseURI) { - var ns = uripath_join(this._baseURI, ns) - } - else { - assertFudge((ns.indexOf(":") >= 0), "With no base URI, cannot handle relative URI for NS") - } - assertFudge((ns.indexOf(":") >= 0)) - this._bindings[t[0][0]] = ( ns) - - this.bind(t[0][0], hexify(ns)) - return j - } - var j = this.tok("base", str, i) - if ((j >= 0)) { - var t = new pyjslib_List([]) - var i = this.uri_ref2(str, j, t) - if ((i < 0)) { - throw BadSyntax(this._thisDoc, this.lines, str, j, "expected after @base ") - } - var ns = t[0].uri - if (this._baseURI) { - var ns = uripath_join(this._baseURI, ns) - } - else { - throw BadSyntax(this._thisDoc, this.lines, str, j, ( ( "With no previous base URI, cannot use relative URI in @base <" + ns ) + ">" ) ) - } - assertFudge((ns.indexOf(":") >= 0)) - this._baseURI = ns - return i - } - return -1 - }; - bind(qn, uri) { - if ((qn == "")) { - } - else { - this._store.setPrefixForURI(qn, uri) - } - }; - setKeywords(k) { - /* - Takes a list of strings*/ - - if ((k == null)) { - this.keywordsSet = 0 - } - else { - this.keywords = k - this.keywordsSet = 1 - } - }; - startDoc() { - }; - - /* Signal end of document and stop parsing. returns formula */ - endDoc() { - if (this.hasNil && this._store.rdfFactory.supports["COLLECTIONS"]) { - convertFirstRestNil(this._store, this.source) - } - return this._formula - }; - makeStatement(quad) { - quad[0].add(quad[2], quad[1], quad[3], this.source) - if ((quad[2].uri && quad[2].uri === RDF_nil_URI) - || (quad[3].uri && quad[3].uri === RDF_nil_URI)) { - this.hasNil = true - } - this.statementCount += 1 - }; - statement(str, i) { - var r = new pyjslib_List([]) - var i = this.object(str, i, r) - if ((i < 0)) { - return i - } - var j = this.property_list(str, i, r[0]) - if ((j < 0)) { - throw BadSyntax(this._thisDoc, this.lines, str, i, "expected propertylist") - } - return j - }; - subject(str, i, res) { - return this.item(str, i, res) - }; - verb(str, i, res) { - /* - has _prop_ - is _prop_ of - a - = - _prop_ - >- prop -> - <- prop -< - _operator_*/ - - var j = this.skipSpace(str, i) - if ((j < 0)) { - return j - } - var r = new pyjslib_List([]) - var j = this.tok("has", str, i) - if ((j >= 0)) { - var i = this.prop(str, j, r) - if ((i < 0)) { - throw BadSyntax(this._thisDoc, this.lines, str, j, "expected property after 'has'") - } - res.push(new pyjslib_Tuple(["->", r[0]])) - return i - } - var j = this.tok("is", str, i) - if ((j >= 0)) { - var i = this.prop(str, j, r) - if ((i < 0)) { - throw BadSyntax(this._thisDoc, this.lines, str, j, "expected after 'is'") - } - var j = this.skipSpace(str, i) - if ((j < 0)) { - throw BadSyntax(this._thisDoc, this.lines, str, i, "End of file found, expected property after 'is'") - return j - } - var i = j - var j = this.tok("of", str, i) - if ((j < 0)) { - throw BadSyntax(this._thisDoc, this.lines, str, i, "expected 'of' after 'is' ") - } - res.push(new pyjslib_Tuple(["<-", r[0]])) - return j - } - var j = this.tok("a", str, i) - if ((j >= 0)) { - res.push(new pyjslib_Tuple(["->", this._store.sym(RDF_type_URI)])) - return j - } - if ((str.slice( i, ( i + 2 ) ) == "<=")) { - res.push(new pyjslib_Tuple(["<-", this._store.sym( ( Logic_NS + "implies" ) )])) - return ( i + 2 ) - } - if ((str.slice( i, ( i + 1 ) ) == "=")) { - if ((str.slice( ( i + 1 ) , ( i + 2 ) ) == ">")) { - res.push(new pyjslib_Tuple(["->", this._store.sym( ( Logic_NS + "implies" ) )])) - return ( i + 2 ) - } - res.push(new pyjslib_Tuple(["->", this._store.sym(DAML_sameAs_URI)])) - return ( i + 1 ) - } - if ((str.slice( i, ( i + 2 ) ) == ":=")) { - res.push(new pyjslib_Tuple(["->", ( Logic_NS + "becomes" ) ])) - return ( i + 2 ) - } - var j = this.prop(str, i, r) - if ((j >= 0)) { - res.push(new pyjslib_Tuple(["->", r[0]])) - return j - } - if ((str.slice( i, ( i + 2 ) ) == ">-") || (str.slice( i, ( i + 2 ) ) == "<-")) { - throw BadSyntax(this._thisDoc, this.lines, str, j, ">- ... -> syntax is obsolete.") - } - return -1 - }; - prop(str, i, res) { - return this.item(str, i, res) - }; - item(str, i, res) { - return this.path(str, i, res) - }; - blankNode(uri) { - return this._context.bnode(uri, this._reason2) - }; - path(str, i, res) { - /* - Parse the path production. - */ - - var j = this.nodeOrLiteral(str, i, res) - if ((j < 0)) { - return j - } - while (("!^.".indexOf(str.slice( j, ( j + 1 ) )) >= 0)) { - var ch = str.slice( j, ( j + 1 ) ) - if ((ch == ".")) { - if (dotTerminatesName(str, j)) { - break - } - } - var subj = res.pop() - var obj = this.blankNode(this.here(j)) - var j = this.node(str, ( j + 1 ) , res) - if ((j < 0)) { - throw BadSyntax(this._thisDoc, this.lines, str, j, "EOF found in middle of path syntax") - } - var pred = res.pop() - if ((ch == "^")) { - this.makeStatement(new pyjslib_Tuple([this._context, pred, obj, subj])) - } - else { - this.makeStatement(new pyjslib_Tuple([this._context, pred, subj, obj])) - } - res.push(obj) - } - return j - }; - anonymousNode(ln) { - /* - Remember or generate a term for one of these _: anonymous nodes*/ - - var term = this._anonymousNodes[ln] - if (term) { - return term - } - var term = this._store.bnode(ln) - // var term = this._store.bnode(this._context, this._reason2); eh? - this._anonymousNodes[ln] = ( term) - return term - }; - node(str, i, res, subjectAlready) { - if (typeof subjectAlready == 'undefined') subjectAlready=null - /* - Parse the production. - Space is now skipped once at the beginning - instead of in multipe calls to self.skipSpace(). - */ - - var subj = subjectAlready - var j = this.skipSpace(str, i) - if ((j < 0)) { - return j - } - var i = j - var ch = str.slice( i, ( i + 1 ) ) - if ((ch == "[")) { - var bnodeID = this.here(i) - var j = this.skipSpace(str, ( i + 1 ) ) - if ((j < 0)) { - throw BadSyntax(this._thisDoc, this.lines, str, i, "EOF after '['") - } - if ((str.slice( j, ( j + 1 ) ) == "=")) { - var i = ( j + 1 ) - var objs = new pyjslib_List([]) - var j = this.objectList(str, i, objs) - - if ((j >= 0)) { - var subj = objs[0] - if ((pyjslib_len(objs) > 1)) { - var __obj = new pyjslib_Iterator(objs) - try { - while (true) { - var obj = __obj.next() - this.makeStatement(new pyjslib_Tuple([this._context, this._store.sym(DAML_sameAs_URI), subj, obj])) - } - } catch (e) { - if (e != StopIteration) { - throw e - } - } - } - var j = this.skipSpace(str, j) - if ((j < 0)) { - throw BadSyntax(this._thisDoc, this.lines, str, i, "EOF when objectList expected after [ = ") - } - if ((str.slice( j, ( j + 1 ) ) == ";")) { - var j = ( j + 1 ) - } - } - else { - throw BadSyntax(this._thisDoc, this.lines, str, i, "objectList expected after [= ") - } - } - if ((subj == null)) { - var subj = this.blankNode(bnodeID) - } - var i = this.property_list(str, j, subj) - if ((i < 0)) { - throw BadSyntax(this._thisDoc, this.lines, str, j, "property_list expected") - } - var j = this.skipSpace(str, i) - if ((j < 0)) { - throw BadSyntax(this._thisDoc, this.lines, str, i, "EOF when ']' expected after [ ") - } - if ((str.slice( j, ( j + 1 ) ) == ".")) { - // If a dot is found after a blank node, treat it as a statement terminator. - // Do NOT consume the '.' here: statement terminators are handled centrally by - // checkDot() (called by directiveOrStatement after statement()). Consuming the dot - // locally would bypass that unified logic and could cause inconsistencies. - // We do consume ']' below because it is a structural closer of the blank node, - // not a statement terminator. - res.push(subj) - return j // leave '.' for checkDot() - } - if ((str.slice( j, ( j + 1 ) ) != "]")) { - throw BadSyntax(this._thisDoc, this.lines, str, j, "']' expected") - } - res.push(subj) - return ( j + 1 ) - } - if ((ch == "{")) { - var ch2 = str.slice( ( i + 1 ) , ( i + 2 ) ) - if ((ch2 == "$")) { - i += 1 - var j = ( i + 1 ) - var mylist = new pyjslib_List([]) - var first_run = true - while (1) { - var i = this.skipSpace(str, j) - if ((i < 0)) { - throw BadSyntax(this._thisDoc, this.lines, str, i, "needed '$}', found end.") - } - if ((str.slice( i, ( i + 2 ) ) == "$}")) { - var j = ( i + 2 ) - break - } - if (!(first_run)) { - if ((str.slice( i, ( i + 1 ) ) == ",")) { - i += 1 - } - else { - throw BadSyntax(this._thisDoc, this.lines, str, i, "expected: ','") - } - } - else { - var first_run = false - } - var item = new pyjslib_List([]) - var j = this.item(str, i, item) - if ((j < 0)) { - throw BadSyntax(this._thisDoc, this.lines, str, i, "expected item in set or '$}'") - } - mylist.push(item[0]) - } - res.push(this._store.newSet(mylist, this._context)) - return j - } - else { - var j = ( i + 1 ) - var oldParentContext = this._parentContext - this._parentContext = this._context - var parentAnonymousNodes = this._anonymousNodes - var grandParentVariables = this._parentVariables - this._parentVariables = this._variables - this._anonymousNodes = new pyjslib_Dict([]) - this._variables = this._variables.slice() - var reason2 = this._reason2 - this._reason2 = becauseSubexpression - if ((subj == null)) { - var subj = this._store.formula() - } - this._context = subj - while (1) { - var i = this.skipSpace(str, j) - if ((i < 0)) { - throw BadSyntax(this._thisDoc, this.lines, str, i, "needed '}', found end.") - } - if ((str.slice( i, ( i + 1 ) ) == "}")) { - var j = ( i + 1 ) - break - } - var j = this.directiveOrStatement(str, i) - if ((j < 0)) { - throw BadSyntax(this._thisDoc, this.lines, str, i, "expected statement or '}'") - } - } - this._anonymousNodes = parentAnonymousNodes - this._variables = this._parentVariables - this._parentVariables = grandParentVariables - this._context = this._parentContext - this._reason2 = reason2 - this._parentContext = oldParentContext - res.push(subj.close()) - return j - } - } - if ((ch == "(")) { - var thing_type = this._store.list - var ch2 = str.slice( ( i + 1 ) , ( i + 2 ) ) - if ((ch2 == "$")) { - var thing_type = this._store.newSet - i += 1 - } - var j = ( i + 1 ) - var mylist = new pyjslib_List([]) - while (1) { - var i = this.skipSpace(str, j) - if ((i < 0)) { - throw BadSyntax(this._thisDoc, this.lines, str, i, "needed ')', found end.") - } - if ((str.slice( i, ( i + 1 ) ) == ")")) { - var j = ( i + 1 ) - break - } - var item = new pyjslib_List([]) - var j = this.item(str, i, item) - if ((j < 0)) { - throw BadSyntax(this._thisDoc, this.lines, str, i, "expected item in list or ')'") - } - mylist.push(item[0]) - } - res.push(thing_type(mylist, this._context)) - return j - } - var j = this.tok("this", str, i) - if ((j >= 0)) { - throw BadSyntax(this._thisDoc, this.lines, str, i, "Keyword 'this' was ancient N3. Now use @forSome and @forAll keywords.") - res.push(this._context) - return j - } - var j = this.tok("true", str, i) - if ((j >= 0)) { - res.push(true) - return j - } - var j = this.tok("false", str, i) - if ((j >= 0)) { - res.push(false) - return j - } - if ((subj == null)) { - var j = this.uri_ref2(str, i, res) - if ((j >= 0)) { - return j - } - } - return -1 - }; - property_list(str, i, subj) { - /* - Parse property list - Leaves the terminating punctuation in the buffer - */ - - while (1) { - var j = this.skipSpace(str, i) - if ((j < 0)) { - throw BadSyntax(this._thisDoc, this.lines, str, i, "EOF found when expected verb in property list") - return j - } - if ((str.slice( j, ( j + 2 ) ) == ":-")) { - var i = ( j + 2 ) - var res = new pyjslib_List([]) - var j = this.node(str, i, res, subj) - if ((j < 0)) { - throw BadSyntax(this._thisDoc, this.lines, str, i, "bad {} or () or [] node after :- ") - } - var i = j - continue - } - var i = j - var v = new pyjslib_List([]) - var j = this.verb(str, i, v) - if ((j <= 0)) { - return i - } - var objs = new pyjslib_List([]) - var i = this.objectList(str, j, objs) - if ((i < 0)) { - throw BadSyntax(this._thisDoc, this.lines, str, j, "objectList expected") - } - - var __obj = new pyjslib_Iterator(objs) - try { - while (true) { - var obj = __obj.next() - - - var pairFudge = v[0] - var dir = pairFudge[0] - var sym = pairFudge[1] - if ((dir == "->")) { - this.makeStatement(new pyjslib_Tuple([this._context, sym, subj, obj])) - } - else { - this.makeStatement(new pyjslib_Tuple([this._context, sym, obj, subj])) - } - - } - } catch (e) { - if (e != StopIteration) { - throw e - } - } - - var j = this.skipSpace(str, i) - if ((j < 0)) { - throw BadSyntax(this._thisDoc, this.lines, str, j, "EOF found in list of objects") - return j - } - if ((str.slice( i, ( i + 1 ) ) != ";")) { - return i - } - var i = ( i + 1 ) - } - }; - commaSeparatedList(str, j, res, ofUris) { - /* - return value: -1 bad syntax; >1 new position in str - res has things found appended - - Used to use a final value of the function to be called, e.g. this.bareWord - but passing the function didn't work fo js converion pyjs - */ - - var i = this.skipSpace(str, j) - if ((i < 0)) { - throw BadSyntax(this._thisDoc, this.lines, str, i, "EOF found expecting comma sep list") - return i - } - if ((str.charAt(i) == ".")) { - return j - } - if (ofUris) { - var i = this.uri_ref2(str, i, res) - } - else { - var i = this.bareWord(str, i, res) - } - if ((i < 0)) { - return -1 - } - while (1) { - var j = this.skipSpace(str, i) - if ((j < 0)) { - return j - } - var ch = str.slice( j, ( j + 1 ) ) - if ((ch != ",")) { - if ((ch != ".")) { - return -1 - } - return j - } - if (ofUris) { - var i = this.uri_ref2(str, ( j + 1 ) , res) - } - else { - var i = this.bareWord(str, ( j + 1 ) , res) - } - if ((i < 0)) { - throw BadSyntax(this._thisDoc, this.lines, str, i, "bad list content") - return i - } - } - }; - objectList(str, i, res) { - var i = this.object(str, i, res) - if ((i < 0)) { - return -1 - } - while (1) { - var j = this.skipSpace(str, i) - if ((j < 0)) { - throw BadSyntax(this._thisDoc, this.lines, str, j, "EOF found after object") - return j - } - if ((str.slice( j, ( j + 1 ) ) != ",")) { - return j - } - var i = this.object(str, ( j + 1 ) , res) - if ((i < 0)) { - return i - } - } - }; - checkDot(str, i) { - var j = this.skipSpace(str, i) - if ((j < 0)) { - return j - } - if ((str.slice( j, ( j + 1 ) ) == ".")) { - return ( j + 1 ) - } - if ((str.slice( j, ( j + 1 ) ) == "}")) { - return j - } - if ((str.slice( j, ( j + 1 ) ) == "]")) { - return j - } - throw BadSyntax(this._thisDoc, this.lines, str, j, "expected '.' or '}' or ']' at end of statement") - return i - }; - uri_ref2(str, i, res) { - /* - Generate uri from n3 representation. - - Note that the RDF convention of directly concatenating - NS and local name is now used though I prefer inserting a '#' - to make the namesapces look more like what XML folks expect. - */ - - var qn = new pyjslib_List([]) - var j = this.qname(str, i, qn) - if ((j >= 0)) { - var pairFudge = qn[0] - var pfx = pairFudge[0] - var ln = pairFudge[1] - if ((pfx == null)) { - assertFudge(0, "not used?") - var ns = ( this._baseURI + ADDED_HASH ) - } - else { - var ns = this._bindings[pfx] - if (!(ns)) { - if ((pfx == "_")) { - res.push(this.anonymousNode(ln)) - return j - } - throw BadSyntax(this._thisDoc, this.lines, str, i, ( ( "Prefix " + pfx ) + " not bound." ) ) - } - } - var symb = this._store.sym( ( ns + ln ) ) - if ((ArrayIndexOf(this._variables, symb) >= 0)) { - res.push(this._variables[symb]) - } - else { - res.push(symb) - } - return j - } - var i = this.skipSpace(str, i) - if ((i < 0)) { - return -1 - } - if ((str.charAt(i) == "?")) { - var v = new pyjslib_List([]) - var j = this.variable(str, i, v) - if ((j > 0)) { - res.push(v[0]) - return j - } - return -1 - } - else if ((str.charAt(i) == "<")) { - var i = ( i + 1 ) - var st = i - while ((i < pyjslib_len(str))) { - if ((str.charAt(i) == ">")) { - var uref = str.slice( st, i) - if (this._baseURI) { - var uref = uripath_join(this._baseURI, uref) - } - else { - assertFudge((uref.indexOf(":") >= 0), "With no base URI, cannot deal with relative URIs") - } - if ((str.slice( ( i - 1 ) , i) == "#") && !((pyjslib_slice(uref, -1, null) == "#"))) { - var uref = ( uref + "#" ) - } - var symb = this._store.sym(uref) - if ((ArrayIndexOf(this._variables,symb) >= 0)) { - res.push(this._variables[symb]) - } - else { - res.push(symb) - } - return ( i + 1 ) - } - var i = ( i + 1 ) - } - throw BadSyntax(this._thisDoc, this.lines, str, j, "unterminated URI reference") - } - else if (this.keywordsSet) { - var v = new pyjslib_List([]) - var j = this.bareWord(str, i, v) - if ((j < 0)) { - return -1 - } - if ((ArrayIndexOf(this.keywords, v[0]) >= 0)) { - throw BadSyntax(this._thisDoc, this.lines, str, i, ( ( "Keyword \"" + v[0] ) + "\" not allowed here." ) ) - } - res.push(this._store.sym( ( this._bindings[""] + v[0] ) )) - return j - } - else { - return -1 - } - }; - skipSpace(str, i) { - /* - Skip white space, newlines and comments. - return -1 if EOF, else position of first non-ws character*/ - - var whitespace = ' \n\r\t\f\x0b\xa0\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u200b\u2028\u2029\u3000' - for (var j = (i ? i : 0); j < str.length; j++) { - var ch = str.charAt(j) - // console.log(" skipspace j= "+j + " i= " + i + " n= " + str.length); - // console.log(" skipspace ch <" + ch + ">"); - if (whitespace.indexOf(ch) < 0 ) { //not ws - // console.log(" skipspace 2 ch <" + ch + ">"); - if( str.charAt(j)==='#' ) { - for (;; j++) { - // console.log(" skipspace2 j= "+j + " i= " + i + " n= " + str.length); - if (j === str.length) { - return -1 // EOF - } - if (str.charAt(j) === '\n') { - this.lines = this.lines + 1 - break - } - }; - } else { // Not hash - something interesting - // console.log(" skipspace 3 ch <" + ch + ">"); - return j - } - } else { // Whitespace - // console.log(" skipspace 5 ch <" + ch + ">"); - if (str.charAt(j) === '\n') { - this.lines = this.lines + 1 - } - } - } // next j - return -1 // EOF - }; - - variable(str, i, res) { - /* - ?abc -> variable(:abc) - */ - - var j = this.skipSpace(str, i) - if ((j < 0)) { - return -1 - } - if ((str.slice( j, ( j + 1 ) ) != "?")) { - return -1 - } - var j = ( j + 1 ) - var i = j - if (("0123456789-".indexOf(str.charAt(j)) >= 0)) { - throw BadSyntax(this._thisDoc, this.lines, str, j, ( ( "Varible name can't start with '" + str.charAt(j) ) + "s'" ) ) - return -1 - } - while ((i < pyjslib_len(str)) && (_notNameChars.indexOf(str.charAt(i)) < 0)) { - var i = ( i + 1 ) - } - if ((this._parentContext == null)) { - throw BadSyntax(this._thisDoc, this.lines, str, j, ( "Can't use ?xxx syntax for variable in outermost level: " + str.slice( ( j - 1 ) , i) ) ) - } - res.push(this._store.variable(str.slice( j, i))) - return i - }; - bareWord(str, i, res) { - /* - abc -> :abc - */ - - var j = this.skipSpace(str, i) - if ((j < 0)) { - return -1 - } - var ch = str.charAt(j) - if (("0123456789-".indexOf(ch) >= 0)) { - return -1 - } - if ((_notNameChars.indexOf(ch) >= 0)) { - return -1 - } - var i = j - while ((i < pyjslib_len(str))) { - var c = str.charAt(i) - if (c === '.') { - if (dotTerminatesName(str, i)) { - break // treat as statement terminator, not part of name - } - // else: accept '.' as part of name - } else if (_notNameChars.indexOf(c) >= 0) { - // Other invalid characters terminate the name - break - } - var i = ( i + 1 ) - } - res.push(str.slice( j, i)) - return i - }; - qname(str, i, res) { - /* - - xyz:def -> ('xyz', 'def') - If not in keywords and keywordsSet: def -> ('', 'def') - :def -> ('', 'def') - */ - - var i = this.skipSpace(str, i) - if ((i < 0)) { - return -1 - } - var c = str.charAt(i) - if (("0123456789-+".indexOf(c) >= 0)) { - return -1 - } - if ((_notNameChars.indexOf(c) < 0)) { - var ln = c - var i = ( i + 1 ) - while ((i < pyjslib_len(str))) { - var c = str.charAt(i) - if (c === '.') { - if (dotTerminatesName(str, i)) { - break // dot ends the name here - } - } else if (_notNameChars.indexOf(c) >= 0) { - break - } - var ln = ( ln + c ) - var i = ( i + 1 ) - } - } - else { - var ln = "" - } - if ((i < pyjslib_len(str)) && (str.charAt(i) == ":")) { - var pfx = ln - var i = ( i + 1 ) - var ln = "" - while ((i < pyjslib_len(str))) { - var c = str.charAt(i) - if (c === '.') { - if (dotTerminatesName(str, i)) { - break // dot ends the name here - } - } else if (_notNameChars.indexOf(c) >= 0) { - break - } - var ln = ( ln + c ) - var i = ( i + 1 ) - } - res.push(new pyjslib_Tuple([pfx, ln])) - return i - } - else { - if (ln && this.keywordsSet && (ArrayIndexOf(this.keywords, ln) < 0)) { - res.push(new pyjslib_Tuple(["", ln])) - return i - } - return -1 - } - }; - object(str, i, res) { - var j = this.subject(str, i, res) - if ((j >= 0)) { - return j - } - else { - var j = this.skipSpace(str, i) - if ((j < 0)) { - return -1 - } - else { - var i = j - } - var delim = null - let ch = str.charAt(i) - if ((ch == "\"" || ch == "'")) { - if (str.slice(i, ( i + 3 ) == ch + ch)) { - delim = ch + ch + ch - } - else { - delim = ch - } - var i = ( i + pyjslib_len(delim) ) - var pairFudge = this.strconst(str, i, delim) - var j = pairFudge[0] - var s = pairFudge[1] - res.push(this._store.literal(s)) - diag_progress("New string const ", s, j) - return j - } - else { - return -1 - } - } - }; - nodeOrLiteral(str, i, res) { - var j = this.node(str, i, res) - if ((j >= 0)) { - return j - } - else { - var j = this.skipSpace(str, i) - if ((j < 0)) { - return -1 - } - else { - var i = j - } - var ch = str.charAt(i) - if (("-+0987654321".indexOf(ch) >= 0)) { - - datetime_syntax.lastIndex = 0 - var m = datetime_syntax.exec(str.slice(i)) - if ((m != null)) { - // j = ( i + datetime_syntax.lastIndex ) ; - var val = m[0] - j = i + val.length - if ((val.indexOf("T") >= 0)) { - res.push(this._store.literal(val, this._store.sym(DATETIME_DATATYPE))) - } else { - res.push(this._store.literal(val, this._store.sym(DATE_DATATYPE))) - } - - } else { - number_syntax.lastIndex = 0 - var m = number_syntax.exec(str.slice(i)) - if ((m == null)) { - throw BadSyntax(this._thisDoc, this.lines, str, i, "Bad number or date syntax") - } - j = ( i + number_syntax.lastIndex ) - var val = str.slice( i, j) - if ((val.indexOf("e") >= 0)) { - res.push(this._store.literal(parseFloat(val), this._store.sym(FLOAT_DATATYPE))) - } - else if ((str.slice( i, j).indexOf(".") >= 0)) { - res.push(this._store.literal(parseFloat(val), this._store.sym(DECIMAL_DATATYPE))) - } - else { - res.push(this._store.literal(parseInt(val), this._store.sym(INTEGER_DATATYPE))) - } - }; - return j // Where we have got up to - } - if ((str.charAt(i) == "\"")) { - if ((str.slice( i, ( i + 3 ) ) == "\"\"\"")) { - var delim = "\"\"\"" - } - else { - var delim = "\"" - } - var i = ( i + pyjslib_len(delim) ) - var dt = null - var pairFudge = this.strconst(str, i, delim) - var j = pairFudge[0] - var s = pairFudge[1] - var lang = null - if ((str.slice( j, ( j + 1 ) ) == "@")) { - langcode.lastIndex = 0 - - var m = langcode.exec(str.slice( ( j + 1 ) )) - if ((m == null)) { - throw BadSyntax(this._thisDoc, startline, str, i, "Bad language code syntax on string literal, after @") - } - var i = ( ( langcode.lastIndex + j ) + 1 ) - - var lang = str.slice( ( j + 1 ) , i) - var j = i - } - if ((str.slice( j, ( j + 2 ) ) == "^^")) { - var res2 = new pyjslib_List([]) - var j = this.uri_ref2(str, ( j + 2 ) , res2) - var dt = res2[0] - } - res.push(this._store.literal(s, lang || dt)) - return j - } - else { - return -1 - } - } - }; - strconst(str, i, delim) { - /* - parse an N3 string constant delimited by delim. - return index, val - */ - - var j = i - var ustr = "" - var startline = this.lines - while ((j < pyjslib_len(str))) { - var i = ( j + pyjslib_len(delim) ) - if ((str.slice( j, i) == delim)) { - return new pyjslib_Tuple([i, ustr]) - } - if ((str.charAt(j) == "\"")) { - var ustr = ( ustr + "\"" ) - var j = ( j + 1 ) - continue - } - interesting.lastIndex = 0 - var m = interesting.exec(str.slice(j)) - if (!(m)) { - throw BadSyntax(this._thisDoc, startline, str, j, ( ( ( "Closing quote missing in string at ^ in " + str.slice( ( j - 20 ) , j) ) + "^" ) + str.slice( j, ( j + 20 ) ) ) ) - } - var i = ( ( j + interesting.lastIndex ) - 1 ) - var ustr = ( ustr + str.slice( j, i) ) - var ch = str.charAt(i) - if ((ch == "\"")) { - var j = i - continue - } - else if ((ch == "\r")) { - var j = ( i + 1 ) - continue - } - else if ((ch == "\n")) { - if ((delim == "\"")) { - throw BadSyntax(this._thisDoc, startline, str, i, "newline found in string literal") - } - this.lines = ( this.lines + 1 ) - var ustr = ( ustr + ch ) - var j = ( i + 1 ) - this.previousLine = this.startOfLine - this.startOfLine = j - } - else if ((ch == "\\")) { - var j = ( i + 1 ) - var ch = str.slice( j, ( j + 1 ) ) - if (!(ch)) { - throw BadSyntax(this._thisDoc, startline, str, i, "unterminated string literal (2)") - } - var k = string_find("abfrtvn\\\"", ch) - if ((k >= 0)) { - var uch = "\a\b\f\r\t\v\n\\\"".charAt(k) - var ustr = ( ustr + uch ) - var j = ( j + 1 ) - } - else if ((ch == "u")) { - var pairFudge = this.uEscape(str, ( j + 1 ) , startline) - var j = pairFudge[0] - var ch = pairFudge[1] - var ustr = ( ustr + ch ) - } - else if ((ch == "U")) { - var pairFudge = this.UEscape(str, ( j + 1 ) , startline) - var j = pairFudge[0] - var ch = pairFudge[1] - var ustr = ( ustr + ch ) - } - else { - throw BadSyntax(this._thisDoc, this.lines, str, i, "bad escape") - } - } - } - throw BadSyntax(this._thisDoc, this.lines, str, i, "unterminated string literal") - }; - uEscape(str, i, startline) { - var j = i - var count = 0 - var value = 0 - while ((count < 4)) { - var chFudge = str.slice( j, ( j + 1 ) ) - var ch = chFudge.toLowerCase() - var j = ( j + 1 ) - if ((ch == "")) { - throw BadSyntax(this._thisDoc, startline, str, i, "unterminated string literal(3)") - } - var k = string_find("0123456789abcdef", ch) - if ((k < 0)) { - throw BadSyntax(this._thisDoc, startline, str, i, "bad string literal hex escape") - } - var value = ( ( value * 16 ) + k ) - var count = ( count + 1 ) - } - var uch = String.fromCharCode(value) - return new pyjslib_Tuple([j, uch]) - }; - UEscape(str, i, startline) { - var j = i - var count = 0 - var value = "\\U" - while ((count < 8)) { - var chFudge = str.slice( j, ( j + 1 ) ) - var ch = chFudge.toLowerCase() - var j = ( j + 1 ) - if ((ch == "")) { - throw BadSyntax(this._thisDoc, startline, str, i, "unterminated string literal(3)") - } - var k = string_find("0123456789abcdef", ch) - if ((k < 0)) { - throw BadSyntax(this._thisDoc, startline, str, i, "bad string literal hex escape") - } - var value = ( value + ch ) - var count = ( count + 1 ) - } - var uch = stringFromCharCode( ( ( "0x" + pyjslib_slice(value, 2, 10) ) - 0 ) ) - return new pyjslib_Tuple([j, uch]) - }; -} - -function OLD_BadSyntax(uri, lines, str, i, why) { - return new __OLD_BadSyntax(uri, lines, str, i, why) -} -function __OLD_BadSyntax(uri, lines, str, i, why) { - this._str = str.encode("utf-8") - this._str = str - this._i = i - this._why = why - this.lines = lines - this._uri = uri -} -__OLD_BadSyntax.prototype.toString = function() { - var str = this._str - var i = this._i - var st = 0 - if ((i > 60)) { - var pre = "..." - var st = ( i - 60 ) - } - else { - var pre = "" - } - if (( ( pyjslib_len(str) - i ) > 60)) { - var post = "..." - } - else { - var post = "" - } - return "Line %i of <%s>: Bad syntax (%s) at ^ in:\n\"%s%s^%s%s\"" % new pyjslib_Tuple([ ( this.lines + 1 ) , this._uri, this._why, pre, str.slice( st, i), str.slice( i, ( i + 60 ) ), post]) -} - -function BadSyntax(uri, lines, str, i, why) { - let lineNo = lines + 1 - let msg = ( ( ( ( ( ( ( ( "Line " + ( lineNo ) ) + " of <" ) + uri ) + ">: Bad syntax: " ) + why ) + "\nat: \"" ) + str.slice( i, ( i + 30 ) ) ) + "\"" ) - let e = new SyntaxError(msg, uri , lineNo) - e.lineNo = lineNo - e.characterInFile = i - e.syntaxProblem = why - return e -} - -function stripCR(str) { - var res = "" - - var __ch = new pyjslib_Iterator(str) - try { - while (true) { - var ch = __ch.next() - - - if ((ch != "\r")) { - var res = ( res + ch ) - } - - } - } catch (e) { - if (e != StopIteration) { - throw e - } - } - - return res -} - - -function dummyWrite(x) { -} diff --git a/src/parse.ts b/src/parse.ts index 337c54b8b..3ec345612 100644 --- a/src/parse.ts +++ b/src/parse.ts @@ -1,15 +1,11 @@ -import DataFactory from './factories/extended-term-factory' import jsonldParser from './jsonldparser' -// @ts-ignore is this injected? -import { Parser as N3jsParser } from 'n3' // @@ Goal: remove this dependency -import N3Parser from './n3parser' +import parseN3js, { N3JS_FORMATS } from './n3-adapter' import { parseRDFaDOM } from './rdfaparser' import RDFParser from './rdfxmlparser' import sparqlUpdateParser from './patch-parser' import * as Util from './utils-js' import Formula from './formula' -import { ContentType, TurtleContentType, N3ContentType, RDFXMLContentType, XHTMLContentType, HTMLContentType, SPARQLUpdateContentType, SPARQLUpdateSingleMatchContentType, JSONLDContentType, NQuadsContentType, NQuadsAltContentType } from './types' -import { Quad } from './tf-types' +import { ContentType, TurtleContentType, RDFXMLContentType, XHTMLContentType, HTMLContentType, SPARQLUpdateContentType, SPARQLUpdateSingleMatchContentType, JSONLDContentType, NQuadsContentType, NQuadsAltContentType } from './types' import type { Document as XmldomDocument } from '@xmldom/xmldom' type CallbackFunc = (error: any, kb: Formula | null) => void @@ -35,9 +31,10 @@ export default function parse ( contentType = contentType || TurtleContentType contentType = contentType.split(';')[0] as ContentType try { - if (contentType === N3ContentType || contentType === TurtleContentType) { - var p = N3Parser(kb, kb, base, base, null, null, '', null) - p.loadBuf(str) + if (Object.prototype.hasOwnProperty.call(N3JS_FORMATS, contentType)) { + // The Turtle family — Turtle, N3, TriG, N-Triples and N-Quads — is + // parsed by the N3.js parser, adapted onto rdflib's model. + parseN3js(str, kb, base, contentType) executeCallback() } else if (contentType === RDFXMLContentType) { var parser = new RDFParser(kb) @@ -59,10 +56,6 @@ export default function parse ( jsonldParser(str, kb, base) .then(executeCallback) .catch(executeErrorCallback) - } else if (contentType === NQuadsContentType || - contentType === NQuadsAltContentType) { - var n3Parser = new N3jsParser({ factory: DataFactory }) - nquadCallback(null, str) } else if (contentType === undefined) { throw new Error("contentType is undefined") } else { @@ -75,7 +68,9 @@ export default function parse ( (parse as any).handled= { 'text/n3': true, + 'application/n3': true, 'text/turtle': true, + 'application/x-turtle': true, 'application/rdf+xml': true, 'application/xhtml+xml': true, 'text/html': true, @@ -83,7 +78,9 @@ export default function parse ( 'application/sparql-update-single-match': true, 'application/ld+json': true, 'application/nquads' : true, - 'application/n-quads' : true + 'application/n-quads' : true, + 'application/n-triples' : true, + 'application/trig' : true } function executeCallback () { @@ -113,33 +110,4 @@ export default function parse ( } } } -/* - function setJsonLdBase (doc, base) { - if (doc instanceof Array) { - return - } - if (!('@context' in doc)) { - doc['@context'] = {} - } - doc['@context']['@base'] = base - } -*/ - function nquadCallback (err?: Error | null, nquads?: string): void { - if (err) { - (callback as CallbackFunc)(err, kb) - } - try { - n3Parser.parse(nquads, tripleCallback) - } catch (err) { - (callback as CallbackFunc)(err, kb) - } - } - - function tripleCallback (err: Error, triple: Quad) { - if (triple) { - kb.add(triple.subject, triple.predicate, triple.object, triple.graph) - } else { - (callback as CallbackFunc)(err, kb) - } - } } diff --git a/src/patch-parser.js b/src/patch-parser.js index 880a1713e..90c817084 100644 --- a/src/patch-parser.js +++ b/src/patch-parser.js @@ -1,94 +1,185 @@ -// Parse a simple SPARL-Update subset syntax for patches. +// Parse a simple SPARQL-Update subset syntax for patches. // // This parses // WHERE {xxx} DELETE {yyy} INSERT DATA {zzz} // (not necessarily in that order) -// as though it were the n3 +// by rewriting it to the equivalent N3 // <#query> patch:where {xxx}; patch:delete {yyy}; patch:insert {zzz}. -import N3Parser from './n3parser' +// and handing that to the N3.js-based Notation3 parser, so that each clause +// becomes an rdflib Formula, exactly as the legacy parser produced. +import parseN3js from './n3-adapter' import Namespace from './namespace' +const keywords = ['INSERT', 'DELETE', 'WHERE'] +const SQNS = Namespace('http://www.w3.org/ns/pim/patch#') + export default function sparqlUpdateParser (str, kb, base) { - var i, j, k - var keywords = [ 'INSERT', 'DELETE', 'WHERE' ] - var SQNS = Namespace('http://www.w3.org/ns/pim/patch#') - var p = N3Parser(kb, kb, base, base, null, null, '', null) - var clauses = {} + const clauses = {} + const query = kb.sym(base + '#query') // Invent a URI for the query + clauses['query'] = query // A way of accessing it in its N3 model. - var badSyntax = function (uri, lines, str, i, why) { + const badSyntax = function (uri, str, i, why) { + const lines = str.slice(0, i < 0 ? str.length : i).split('\n').length - 1 return ('Line ' + (lines + 1) + ' of <' + uri + '>: Bad syntax:\n ' + - why + '\n at: "' + str.slice(i, (i + 30)) + '"') + why + '\n at: "' + str.slice(i < 0 ? 0 : i, (i < 0 ? 0 : i) + 30) + '"') } - // var check = function (next, last, message) { - // if (next < 0) { - // throw badSyntax(p._thisDoc, p.lines, str, j, last, message) - // } - // return next - // } - i = 0 - var query = kb.sym(base + '#query') // Invent a URI for the query - clauses['query'] = query // A way of accessing it in its N3 model. - + // Scan the top level of the document: keywords with their {...} clauses, + // @prefix directives, and optional ';' separators. Everything inside the + // braces is left for the N3 parser. + let n3doc = '' + const order = [] + let i = 0 while (true) { - // console.log("A Now at i = " + i) - j = p.skipSpace(str, i) - if (j < 0) { - return clauses + i = skipSpace(str, i) + if (i < 0) break // Normal end of input + if (str[i] === ';') { // Allow a separator (and a trailing one) + i++ + continue } - // console.log("B After space at j= " + j) - if (str[j] === ';') { - i = p.skipSpace(str, j + 1) - if (i < 0) { - return clauses // Allow end in a - } - j = i - } - var found = false - for (k = 0; k < keywords.length; k++) { - var key = keywords[k] - if (str.slice(j, j + key.length) === key) { - i = p.skipSpace(str, j + key.length) - if (i < 0) { - throw badSyntax(p._thisDoc, p.lines, str, j + key.length, 'found EOF, needed {...} after ' + key) + let found = false + for (let k = 0; k < keywords.length; k++) { + const key = keywords[k] + if (str.slice(i, i + key.length) === key) { + let j = skipSpace(str, i + key.length) + if (j < 0) { + throw badSyntax(base, str, i + key.length, 'found EOF, needed {...} after ' + key) } - if (((key === 'INSERT') || (key === 'DELETE')) && str.slice(i, i + 4) === 'DATA') { // Some wanted 'DATA'. Whatever - j = p.skipSpace(str, i + 4) + if (((key === 'INSERT') || (key === 'DELETE')) && str.slice(j, j + 4) === 'DATA') { // Some wanted 'DATA'. Whatever + j = skipSpace(str, j + 4) if (j < 0) { - throw badSyntax(p._thisDoc, p.lines, str, i + 4, 'needed {...} after INSERT DATA ' + key) + throw badSyntax(base, str, j, 'needed {...} after INSERT DATA ' + key) } - i = j } - var res2 = [] - j = p.node(str, i, res2) // Parse all the complexity of the clause - - if (j < 0) { - throw badSyntax(p._thisDoc, p.lines, str, i, - 'bad syntax or EOF in {...} after ' + key) + if (str[j] !== '{') { + throw badSyntax(base, str, j, 'needed {...} after ' + key) } - clauses[key.toLowerCase()] = res2[0] - kb.add(query, SQNS(key.toLowerCase()), res2[0]) // , kb.sym(base) - // key is the keyword and res2 has the contents + const end = skipClause(str, j) + if (end < 0) { + throw badSyntax(base, str, j, 'bad syntax or EOF in {...} after ' + key) + } + n3doc += '<#query> <' + SQNS(key.toLowerCase()).value + '> ' + str.slice(j, end) + ' .\n' + order.push(key.toLowerCase()) + i = end found = true - i = j + break + } + } + if (!found && str.slice(i, i + 7) === '@prefix') { + // Pass @prefix directives through to the N3 parser, in source order. + let j = i + 7 + while (j < str.length && str[j] !== '.') { + if (str[j] === '<') { + j = str.indexOf('>', j) + if (j < 0) throw badSyntax(base, str, i, 'bad syntax or EOF after @prefix ') + } + j++ } + if (j >= str.length) { + throw badSyntax(base, str, i, 'bad syntax or EOF after @prefix ') + } + n3doc += str.slice(i, j + 1) + '\n' + i = j + 1 + found = true } - if (!found && str.slice(j, j + 7) === '@prefix') { - i = p.directive(str, j) - if (i < 0) { - throw badSyntax(p._thisDoc, p.lines, str, i, - 'bad syntax or EOF after @prefix ') + if (!found && str.slice(i, i + 7) === 'PREFIX ') { + // SPARQL-style PREFIX declaration (no trailing dot) — see #651. + let j = str.indexOf('<', i) + if (j >= 0) j = str.indexOf('>', j) + if (j < 0) { + throw badSyntax(base, str, i, 'bad syntax or EOF after PREFIX ') } - // console.log("P before dot i= " + i) - i = p.checkDot(str, i) - // console.log("Q after dot i= " + i) + n3doc += str.slice(i, j + 1) + '\n' + i = j + 1 found = true } if (!found) { - // console.log("Bad syntax " + j) - throw badSyntax(p._thisDoc, p.lines, str, j, - "Unknown syntax at start of statememt: '" + str.slice(j).slice(0, 20) + "'") + throw badSyntax(base, str, i, + "Unknown syntax at start of statememt: '" + str.slice(i).slice(0, 20) + "'") } } // while -// return clauses + + parseN3js(n3doc, kb, base, 'text/n3') + + // Pull the clause Formulae back out of the store (the last one wins if a + // keyword appears twice, as before). + for (const key of order) { + const sts = kb.statementsMatching(query, SQNS(key), null) + if (sts.length) { + clauses[key] = sts[sts.length - 1].object + } + } + return clauses +} + +/** Skip whitespace and comments; returns -1 at end of input. */ +function skipSpace (str, i) { + while (i < str.length) { + const ch = str[i] + if (ch === '#') { + while (i < str.length && str[i] !== '\n') i++ + } else if (ch === ' ' || ch === '\t' || ch === '\r' || ch === '\n' || ch === '\f') { + i++ + } else { + return i + } + } + return -1 +} + +/** + * Skip a balanced `{ … }` clause starting at the opening brace, ignoring + * braces inside string literals, IRIs and comments. + * Returns the index just past the matching `}`, or -1 on EOF. + */ +function skipClause (str, i) { + let depth = 0 + while (i < str.length) { + const ch = str[i] + if (ch === '{') { + depth++ + i++ + } else if (ch === '}') { + depth-- + i++ + if (depth === 0) return i + } else if (ch === '"' || ch === "'") { + i = skipStringLiteral(str, i) + if (i < 0) return -1 + } else if (ch === '<') { + i = skipIriOrOperator(str, i) + } else if (ch === '#') { + while (i < str.length && str[i] !== '\n') i++ + } else { + i++ + } + } + return -1 +} + +/** Skip a short or long ("""…""") string literal; -1 on EOF. */ +function skipStringLiteral (str, i) { + const quote = str[i] + const delim = str.slice(i, i + 3) === quote + quote + quote ? quote + quote + quote : quote + i += delim.length + while (i < str.length) { + if (str[i] === '\\') { + i += 2 + } else if (str.slice(i, i + delim.length) === delim) { + return i + delim.length + } else { + i++ + } + } + return -1 +} + +/** + * At a `<`: skip a `` reference, or just the `<` itself when it is part + * of an operator such as `<=`. + */ +function skipIriOrOperator (str, i) { + let j = i + 1 + while (j < str.length && !/[>\s"'{}]/.test(str[j])) j++ + return (j < str.length && str[j] === '>') ? j + 1 : i + 1 } diff --git a/src/serializer.js b/src/serializer.js index ac5599891..6d19e7308 100644 --- a/src/serializer.js +++ b/src/serializer.js @@ -577,7 +577,10 @@ export class Serializer { } case 'http://www.w3.org/2001/XMLSchema#boolean': - return expr.value === '1' ? 'true' : 'false' + // The XSD lexical space is {'true', 'false', '1', '0'}; the + // legacy parser normalised to '1'/'0', the N3.js parser + // preserves the source form. + return (expr.value === '1' || expr.value === 'true') ? 'true' : 'false' } } var str = this.stringToN3(expr.value, this.flags) diff --git a/src/types.ts b/src/types.ts index 7abcbc91e..ccc90044c 100644 --- a/src/types.ts +++ b/src/types.ts @@ -41,6 +41,7 @@ export const NTriplesContentType = "application/n-triples" as const export const RDFXMLContentType = "application/rdf+xml" as const export const SPARQLUpdateContentType = "application/sparql-update" as const export const SPARQLUpdateSingleMatchContentType = "application/sparql-update-single-match" as const +export const TrigContentType = "application/trig" as const export const TurtleContentType = "text/turtle" as const export const TurtleLegacyContentType = "application/x-turtle" as const export const XHTMLContentType = "application/xhtml+xml" as const @@ -55,8 +56,10 @@ export type ContentType = typeof RDFXMLContentType | typeof N3LegacyContentType | typeof NQuadsAltContentType | typeof NQuadsContentType + | typeof NTriplesContentType | typeof SPARQLUpdateContentType | typeof SPARQLUpdateSingleMatchContentType + | typeof TrigContentType | typeof TurtleContentType | typeof TurtleLegacyContentType | typeof XHTMLContentType diff --git a/tests/serialize/structures.n3 b/tests/serialize/structures.n3 index 6e2bca53a..753f37fff 100644 --- a/tests/serialize/structures.n3 +++ b/tests/serialize/structures.n3 @@ -1,18 +1,19 @@ - +@prefix xsd: . :MLK :name "Martin Luther King, Jr"; - :born 1929-01-17; :died 1968-01-15; - :spouse [ - :name "Coretta Scott King"; - is :spouse of [ - a :Marriage; - :spouse :MLK; - :offspring ( :MLK3 :DSK :YK [:name "Bernice King"]); - ] - ]; + :born "1929-01-17"^^xsd:date; :died "1968-01-15"^^xsd:date; + :spouse _:coretta; :wikipediapage . +_:coretta :name "Coretta Scott King". + +[ + a :Marriage; + :spouse _:coretta, :MLK; + :offspring ( :MLK3 :DSK :YK [:name "Bernice King"]); +]. + :MLK3 :name "Martin Luther King III". :DSK :name "Dexter Scott King". :YK :name "Yolanda King". @@ -22,7 +23,7 @@ [] :loves []. # A floating arc between two bnodes -:MLK is :author of [a :Speech ; :date 1963-08-23; :title "I have a dream"] . +[ a :Speech ; :author :MLK; :date "1963-08-23"^^xsd:date; :title "I have a dream"] . # Datatypes :ZooReading1 @@ -43,5 +44,5 @@ :floatOne 1e0; :floatBig 3.14159e100; - :time 2015-03-16T17:53Z; - :date 2015-02-28 . + :time "2015-03-16T17:53Z"^^xsd:dateTime; + :date "2015-02-28"^^xsd:date . diff --git a/tests/serialize/t1-ref.xml b/tests/serialize/t1-ref.xml index fac38e2c0..511f46762 100644 --- a/tests/serialize/t1-ref.xml +++ b/tests/serialize/t1-ref.xml @@ -6,8 +6,8 @@ kasdfjsahdkfhkjhdkjsfhjkasdfkhjkajkdsajkhadsfkhjhjkdfajsdsafhjkdfhjksa 2012-12-10 2012-12-25T23:59 - 12 - 3.141 + 12.0 + 3.141e0 0 diff --git a/tests/serialize/t1.ttl b/tests/serialize/t1.ttl index 3c6e3a33d..d3306bab4 100644 --- a/tests/serialize/t1.ttl +++ b/tests/serialize/t1.ttl @@ -10,7 +10,7 @@ n:content "kasdfjsahdkfhkjhdkjsfhjkasdfkhjkajkdsajkhadsfkhjhjkdfajsdsafhjkdfhjksa"; p:integer - 0; p:decimal 12.0; p:float 3.141e0; p:date 2012-12-10; p:dateTime 2012-12-25T23:59; + 0; p:decimal 12.0; p:float 3.141e0; p:date "2012-12-10"^^XML:date; p:dateTime "2012-12-25T23:59"^^XML:dateTime; p:next <#id1443100912627>. diff --git a/tests/serialize/t11-ref.xml b/tests/serialize/t11-ref.xml index 1d228d864..3458a01a0 100644 --- a/tests/serialize/t11-ref.xml +++ b/tests/serialize/t11-ref.xml @@ -7,13 +7,13 @@ Martin Luther King, Jr - + - + Coretta Scott King - + Martin Luther King III @@ -28,8 +28,8 @@ Bernice King + - Martin Luther King III @@ -40,29 +40,29 @@ Yolanda King - + - + - + 1963-08-23 I have a dream - 0 - 1 + false + true 2015-02-28 1234567890.99 -1234567890.99 - 10 - -10 - 3.14159e+100 - 1 - -0.0000016507 - 10 + 10.0 + -10.0 + 3.14159e100 + 1e0 + -1.6507e-6 + 10.0e0 1234567890 16 -123 diff --git a/tests/serialize/t12-ref.ttl b/tests/serialize/t12-ref.ttl index 2b95d83eb..cb5dcfd66 100644 --- a/tests/serialize/t12-ref.ttl +++ b/tests/serialize/t12-ref.ttl @@ -8,7 +8,7 @@ :died "1968-01-15"^^xsd:date; :friend []; :name "Martin Luther King, Jr"; - :spouse _:_g_L4C88; + :spouse _:dl1_coretta; :wikipediapage . :MLK3 :name "Martin Luther King III". @@ -22,15 +22,15 @@ :decBigNegative -1234567890.99; :decWithPoint 10.0; :decWithPointNegative -10.0; - :floatBig 3.14159e+100; - :floatOne 1.0e0; - :floatSmallNegative -0.0000016507e0; + :floatBig 3.14159e100; + :floatOne 1e0; + :floatSmallNegative -1.6507e-6; :floatTen 10.0e0; :intBig 1234567890; :intCount 16; :intNegative -123; :time "2015-03-16T17:53Z"^^xsd:dateTime. -_:_g_L4C88 :name "Coretta Scott King". +_:dl1_coretta :name "Coretta Scott King". [ a :Speech; :author :MLK; :date "1963-08-23"^^xsd:date; :title "I have a dream" ]. @@ -41,5 +41,5 @@ _:_g_L4C88 :name "Coretta Scott King". [ a :Marriage; :offspring ( :MLK3 :DSK :YK [ :name "Bernice King" ] ); - :spouse :MLK, _:_g_L4C88 + :spouse :MLK, _:dl1_coretta ]. diff --git a/tests/serialize/t13-ref.ttl b/tests/serialize/t13-ref.ttl index aeec04506..b0b81f766 100644 --- a/tests/serialize/t13-ref.ttl +++ b/tests/serialize/t13-ref.ttl @@ -9,7 +9,7 @@ str:MLK str:died "1968-01-15"^^xsd:date; str:friend []; str:name "Martin Luther King, Jr"; - str:spouse _:_g_L4C88; + str:spouse _:dl0_dl1_coretta; str:wikipediapage . str:MLK3 str:name "Martin Luther King III". @@ -23,15 +23,15 @@ str:ZooReading1 str:decBigNegative -1234567890.99; str:decWithPoint 10.0; str:decWithPointNegative -10.0; - str:floatBig 3.14159e+100; - str:floatOne 1.0e0; - str:floatSmallNegative -0.0000016507e0; + str:floatBig 3.14159e100; + str:floatOne 1e0; + str:floatSmallNegative -1.6507e-6; str:floatTen 10.0e0; str:intBig 1234567890; str:intCount 16; str:intNegative -123; str:time "2015-03-16T17:53Z"^^xsd:dateTime. -_:_g_L4C88 str:name "Coretta Scott King". +_:dl0_dl1_coretta str:name "Coretta Scott King". [ a str:Speech; @@ -46,5 +46,5 @@ _:_g_L4C88 str:name "Coretta Scott King". [ a str:Marriage; str:offspring ( str:MLK3 str:DSK str:YK [ str:name "Bernice King" ] ); - str:spouse str:MLK, _:_g_L4C88 + str:spouse str:MLK, _:dl0_dl1_coretta ]. diff --git a/tests/serialize/t17-ref.xml b/tests/serialize/t17-ref.xml index dc323a930..34dabf81e 100644 --- a/tests/serialize/t17-ref.xml +++ b/tests/serialize/t17-ref.xml @@ -1,67 +1,67 @@ - - - - - - - - - 14400000 - - - - - - - - - - - - 0 - _ByGZwRSkEfCQ7aMNxFFsPQ - - - - 0 - 0 - 0 - - - - 0 - 0 - New - - - - 0 - - - - - 257 - Defect 257 - - - 2025-04-07T04:42:00.128Z - - org.eclipse.core.runtime.AssertionFailedException: null argument:<br/><br/> - Tue Apr 15 2025 08:39:38 GMT-0400 (Eastern Daylight Time) - Tue Apr 22 2025 11:24:55 GMT-0400 (Eastern Daylight Time) - Tue Apr 22 2025 15:44:44 GMT-0400 (Eastern Daylight Time) - Fri May 23 2025 09:38:49 GMT-0400 (Eastern Daylight Time) - Fri May 23 2025 10:04:21 GMT-0400 (Eastern Daylight Time) - Fri May 23 2025 13:17:19 GMT-0400 (Eastern Daylight Time) - Mon Dec 08 2025 13:21:50 GMT-0500 (Eastern Standard Time) - 257 - 2025-05-23T17:17:19.734Z - - SWT Exception - Defect - - + + + + + + + + + 14400000 + + + + + + + + + + + + false + _ByGZwRSkEfCQ7aMNxFFsPQ + + + + false + false + false + + + + false + false + New + + + + false + + + + + 257 + Defect 257 + + + 2025-04-07T04:42:00.128Z + + org.eclipse.core.runtime.AssertionFailedException: null argument:<br/><br/> - Tue Apr 15 2025 08:39:38 GMT-0400 (Eastern Daylight Time) - Tue Apr 22 2025 11:24:55 GMT-0400 (Eastern Daylight Time) - Tue Apr 22 2025 15:44:44 GMT-0400 (Eastern Daylight Time) - Fri May 23 2025 09:38:49 GMT-0400 (Eastern Daylight Time) - Fri May 23 2025 10:04:21 GMT-0400 (Eastern Daylight Time) - Fri May 23 2025 13:17:19 GMT-0400 (Eastern Daylight Time) - Mon Dec 08 2025 13:21:50 GMT-0500 (Eastern Standard Time) + 257 + 2025-05-23T17:17:19.734Z + + SWT Exception + Defect + + diff --git a/tests/serialize/t2-ref.xml b/tests/serialize/t2-ref.xml index 9ac5e9e60..58859a408 100644 --- a/tests/serialize/t2-ref.xml +++ b/tests/serialize/t2-ref.xml @@ -7,7 +7,7 @@ 3 rrrrr 2014-09-21 - 2 - 3.14159 + 2.0 + 3.14159e0 diff --git a/tests/serialize/t2.ttl b/tests/serialize/t2.ttl index 5e9fa52dc..d8da24cb6 100644 --- a/tests/serialize/t2.ttl +++ b/tests/serialize/t2.ttl @@ -5,6 +5,6 @@ :foo :p 1,2,3; # :q ( 1 2 3); :r "rrrrr"; - :s 2014-09-21; # extension to TTL + :s "2014-09-21"^^; :t 2.0; :u 3.14159e0 . diff --git a/tests/serialize/t7-ref.nt b/tests/serialize/t7-ref.nt index 2cd02463f..0d941ff06 100644 --- a/tests/serialize/t7-ref.nt +++ b/tests/serialize/t7-ref.nt @@ -2,4 +2,4 @@ "78768"^^ . . "2012-03-12"^^ . - "145000"^^ . + "1.45e5"^^ . diff --git a/tests/serialize/t7.n3 b/tests/serialize/t7.n3 index 0f51a8e8e..754fba549 100644 --- a/tests/serialize/t7.n3 +++ b/tests/serialize/t7.n3 @@ -3,7 +3,7 @@ :building0 :bar 123, 78768. :building1 :length 1.45e5 ; - :created 2012-03-12 . + :created "2012-03-12"^^ . :building0 :connectsTo :building4 . diff --git a/tests/unit/fetcher-test.js b/tests/unit/fetcher-test.js index 3700d29a7..c5666a4ac 100644 --- a/tests/unit/fetcher-test.js +++ b/tests/unit/fetcher-test.js @@ -538,10 +538,13 @@ describe('Fetcher', () => { }) it('should load and parse N3', () => { + // (bareword dates were a nonstandard cwm-era extension; the N3.js-based + // parser requires the standard quoted xsd:date form) let testN3 = `@prefix : . +@prefix xsd: . :building0 :bar 123, 78768. :building1 :length 1.45e5 ; - :created 2012-03-12 . + :created "2012-03-12"^^xsd:date . :building0 :connectsTo :building4 .` nock('https://example.com').get('/test.n3') @@ -560,6 +563,31 @@ describe('Fetcher', () => { expect(match.object.value).to.equal('http://example.com/foo/vocab#building4') }) }) + + it('should load and parse N3 with formula subjects (#567)', () => { + // Loading a document like https://drive.verborgh.org/tmp/2022/cha-58-direct.n3 + // used to fail with "Subject is not a subject type". + let testN3 = `@prefix : . +{ :a :b :c } => { :d :e :f }.` + + nock('https://example.com').get('/rules.n3') + .reply(200, testN3, { 'Content-Type': 'text/n3' }) + + return fetcher.load('https://example.com/rules.n3') + .then(res => { + expect(res.status).to.equal(200) + let kb = fetcher.store + + let match = kb.anyStatementMatching( + null, + kb.sym('http://www.w3.org/2000/10/swap/log#implies') + ) + + expect(match.subject.termType).to.equal('Graph') + expect(match.subject.statements).to.have.length(1) + expect(match.object.termType).to.equal('Graph') + }) + }) }) describe('createContainer', () => { diff --git a/tests/unit/indexed-formula-test.js b/tests/unit/indexed-formula-test.js index b8a1adadf..9bb7c209e 100644 --- a/tests/unit/indexed-formula-test.js +++ b/tests/unit/indexed-formula-test.js @@ -355,7 +355,14 @@ describe('IndexedFormula', () => { }) }) describe('removeDocument', () => { - const store = new IndexedFormula() + // Use a collection-supporting store (the default in normal rdflib usage via + // DataFactory.graph()). The N3.js-based parser correctly attributes the + // status list's rdf:first/rest triples to the document graph, so — unlike a + // bare IndexedFormula() whose CanonicalDataFactory cannot fold them into a + // Collection — the list is stored as a single Collection term that + // removeMetadata/removeDocument clean up. (The legacy parser only "passed" + // here by misfiling those list triples into the default graph.) + const store = DataFactory.graph() const meta = store.sym('chrome://TheCurrentSession') const prefixes = `@prefix : <#>. @prefix http: . @@ -425,5 +432,22 @@ describe('IndexedFormula', () => { expect(serialize(meta, store, null)).to.eql(voidDoc) expect(serialize(doc, store, doc.uri)).to.eql(voidDoc) }) + it ('removeMetadata on a store without collection support leaves the raw status-list triples (pre-existing #631 gap, now visible)', () => { + // With a CanonicalDataFactory store the status list cannot be folded + // into a Collection, so it stays as raw rdf:first/rest triples that are + // now correctly attributed to the metadata graph (the legacy parser + // misfiled them into the default graph, which made the old version of + // this test pass vacuously). removeMetadata only knows how to remove a + // status Collection, so those triples survive — documented here until + // #631 is fixed. + const rawStore = new IndexedFormula() + parse(metaContent, rawStore, meta.value, 'text/turtle') + rawStore.removeMetadata(rawStore.sym('https://bob.localhost:8443/profile/card')) + const leftovers = rawStore.statementsMatching(null, null, null, meta) + expect(leftovers.length).to.be.greaterThan(0) + leftovers.forEach(st => { + expect(st.predicate.value).to.match(/rdf-syntax-ns#(first|rest)$/) + }) + }) }) }) diff --git a/tests/unit/lists-test.js b/tests/unit/lists-test.js index 256eb8e05..a7c45458d 100644 --- a/tests/unit/lists-test.js +++ b/tests/unit/lists-test.js @@ -1,6 +1,5 @@ import {expect} from 'chai' -import { convertFirstRestNil } from '../../src/lists' import parse from '../../src/parse' import CanonicalDataFactory from '../../src/factories/canonical-data-factory' import defaultXSD from '../../src/xsd' @@ -43,7 +42,6 @@ describe('Lists', () => { let content = prefixes + ' <#test> <#value> [ rdf:first 1; rdf:rest [ rdf:first 2; rdf:rest [ rdf:first 3; rdf:rest rdf:nil ]]] .' parse(content, store, base, mimeType) - convertFirstRestNil(store, doc) expect(store.statements[0].object.termType).to.eql('Collection') expect(showDoc(store, doc)).to.eql(`@prefix : <#>. @@ -60,7 +58,6 @@ describe('Lists', () => { let content = prefixes + '[ rdf:first 1; rdf:rest [ rdf:first 2; rdf:rest [ rdf:first 3; rdf:rest rdf:nil ]]] <#value> <#test> .' parse(content, store, base, mimeType) - convertFirstRestNil(store, doc) // console.log('@@@ CCC ' + dumpStore(store)) expect(store.statements[0].subject.termType).to.eql('Collection') expect(store.statements[0].subject.elements.length).to.eql(3) @@ -78,7 +75,6 @@ describe('Lists', () => { parse(content, store, base, mimeType) // console.log('@@@ AAA ' + showDoc(store, doc)) // expect(store.statements[0].object.termType).to.eql('BlankNode') - convertFirstRestNil(store, doc) // console.log('@@@ CCC ' + dumpStore(store)) // expect(store.statements[0].object.termType).to.eql('Collection') expect(showDoc(store, doc)).to.eql(`@prefix : <#>. @@ -101,7 +97,6 @@ describe('Lists', () => { parse(content, store, base, mimeType) // console.log('@@@ AAA ' + showDoc(store, doc)) // expect(store.statements[0].object.termType).to.eql('BlankNode') - convertFirstRestNil(store, doc) // console.log('@@@ BBB ' + showDoc(store, doc)) // expect(store.statements[0].object.termType).to.eql('Collection') expect(showDoc(store, doc)).to.eql(`@prefix : <#>. @@ -119,7 +114,6 @@ describe('Lists', () => { let content = prefixes + ' <#test> <#value> rdf:nil .' parse(content, store, base, mimeType) - convertFirstRestNil(store, doc) expect(showDoc(store, doc)).to.eql(`@prefix : <#>. :test :value ( ). @@ -138,7 +132,6 @@ describe('Lists', () => { :zap [ rdf:first 2; rdf:rest [ rdf:first rdf:nil; rdf:rest [ rdf:first 4; rdf:rest rdf:nil]]] . ` parse(content, store, base, mimeType) - convertFirstRestNil(store, doc) expect(showDoc(store, doc)).to.eql(`@prefix : <#>. :test :value ( ). diff --git a/tests/unit/parse-turtle-family-test.js b/tests/unit/parse-turtle-family-test.js new file mode 100644 index 000000000..b8a5cdb7e --- /dev/null +++ b/tests/unit/parse-turtle-family-test.js @@ -0,0 +1,316 @@ +import { expect } from 'chai' + +import parse from '../../src/parse' +import DataFactory from '../../src/factories/rdflib-data-factory' +import CanonicalDataFactory from '../../src/factories/canonical-data-factory' + +const RDF = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#' +const XSD = 'http://www.w3.org/2001/XMLSchema#' + +/** + * Tests for the Turtle-family formats parsed by the N3.js parser + * (text/turtle, text/n3, application/n-triples, application/n-quads, + * application/trig). + * + * These cover spec-conformance cases that rdflib's historical hand-rolled + * parser did not handle, plus the Notation3 model mapping (formulae, + * variables, quantifiers). See the "Closes #…" list on the migration PR. + */ +describe('Turtle-family parsing via N3.js', () => { + const base = 'https://example.org/doc' + + function parseTtl(content, contentType = 'text/turtle', rdfFactory = undefined) { + const store = rdfFactory ? DataFactory.graph(undefined, { rdfFactory }) : DataFactory.graph() + parse(content, store, base, contentType) + return store + } + + describe('#214 — single-quoted long (multi-line) literals', () => { + it("parses '''…''' the same as \"\"\"…\"\"\"", () => { + const store = parseTtl(`<${base}#s> <${base}#p> '''line one\nline two''' .`) + expect(store.statements).to.have.length(1) + expect(store.statements[0].object.termType).to.equal('Literal') + expect(store.statements[0].object.value).to.equal('line one\nline two') + }) + }) + + describe('#494 — 8-bit / astral \\U unicode escapes', () => { + it('decodes \\U0001F60A to the correct astral code point (not \\uF60A)', () => { + const store = parseTtl(`<${base}#s> <${base}#p> "emoji \\U0001F60A end" .`) + const value = store.statements[0].object.value + expect(value).to.equal('emoji \u{1F60A} end') + expect(value.codePointAt(6).toString(16)).to.equal('1f60a') + }) + + it('decodes \\U escapes inside IRIs', () => { + const store = parseTtl(`<${base}#s> <${base}#p> .`) + expect(store.statements[0].object.value).to.equal('http://example.org/\u{1F4A9}') + }) + }) + + describe('#626 — SPARQL-style PREFIX / BASE directives', () => { + it('accepts case-insensitive PREFIX', () => { + const store = parseTtl(`PREFIX ex: \nex:s ex:p ex:o .`) + expect(store.statements[0].subject.value).to.equal('http://ex/s') + expect(store.statements[0].object.value).to.equal('http://ex/o') + }) + + it('accepts SPARQL-style BASE', () => { + const store = parseTtl(`BASE \n

.`) + expect(store.statements[0].subject.value).to.equal('http://ex/s') + }) + }) + + describe('#329 — Turtle 1.1 conformance samples', () => { + it('parses bareword booleans as xsd:boolean', () => { + const store = parseTtl(`<${base}#s> <${base}#p> true .`) + expect(store.statements[0].object.value).to.equal('true') + expect(store.statements[0].object.datatype.value).to.equal(`${XSD}boolean`) + }) + + it('preserves numeric lexical forms (decimal/double)', () => { + const store = parseTtl(`<${base}#s> <${base}#d> 12.0 ; <${base}#f> 3.141e0 .`, 'text/turtle', CanonicalDataFactory) + const byPred = p => store.statements.find(s => s.predicate.value === `${base}#${p}`).object + expect(byPred('d').value).to.equal('12.0') + expect(byPred('d').datatype.value).to.equal(`${XSD}decimal`) + expect(byPred('f').value).to.equal('3.141e0') + expect(byPred('f').datatype.value).to.equal(`${XSD}double`) + }) + + it('decodes numeric IRI escapes', () => { + const store = parseTtl(`<${base}#s> <${base}#p> .`) + expect(store.statements[0].object.value).to.equal('http://example.org/é') + }) + }) + + describe('empty-prefix (:) auto-binding preserved (rdflib leniency)', () => { + it('resolves `:name` against even without an @prefix : declaration', () => { + const store = parseTtl(`<${base}#s> :p :o .`) + expect(store.statements[0].predicate.value).to.equal(`${base}#p`) + expect(store.statements[0].object.value).to.equal(`${base}#o`) + }) + + it('does not leak a synthetic `:` prefix onto the store when undeclared', () => { + const store = parseTtl(`<${base}#s> :p :o .`) + expect(store.namespaces['']).to.equal(undefined) + }) + + it('an explicit @prefix : declaration still wins and is registered', () => { + const store = parseTtl(`@prefix : .\n:s :p :o .`) + expect(store.statements[0].subject.value).to.equal('http://other/s') + expect(store.namespaces['']).to.equal('http://other/') + }) + }) + + describe('collections still reconstructed into Collection terms', () => { + it('folds ( … ) into a Collection with a collection-supporting factory', () => { + const store = parseTtl(`<${base}#s> <${base}#p> ( "a" "b" "c" ) .`) + const first = store.statements.find(s => s.predicate.value === `${base}#p`) + expect(first.object.termType).to.equal('Collection') + expect(first.object.elements.map(e => e.value)).to.eql(['a', 'b', 'c']) + }) + + it('leaves first/rest triples when the factory lacks collection support', () => { + const store = parseTtl(`<${base}#s> <${base}#p> ( "a" "b" ) .`, 'text/turtle', CanonicalDataFactory) + expect(store.statementsMatching(null, DataFactory.namedNode(`${RDF}rest`), DataFactory.namedNode(`${RDF}nil`)).length).to.equal(1) + }) + }) + + describe('application/n-triples', () => { + it('parses and attributes triples to the document graph', () => { + const store = parseTtl(` "x" .`, 'application/n-triples') + expect(store.statements).to.have.length(1) + expect(store.statements[0].object.value).to.equal('x') + expect(store.statements[0].why.value).to.equal(base) + }) + }) + + describe('application/n-quads', () => { + it('keeps the named graph and files default-graph triples under the document', () => { + const store = parseTtl( + ` .\n .`, + 'application/n-quads' + ) + const named = store.statements.find(s => s.subject.value === 'http://a/s') + const dflt = store.statements.find(s => s.subject.value === 'http://a/s2') + expect(named.why.value).to.equal('http://a/g') + expect(dflt.why.value).to.equal(base) + }) + }) + + describe('application/trig', () => { + it('parses named graphs and the default graph', () => { + const store = parseTtl( + `@prefix : .\n:g { :s :p :o }\n:s2 :p2 :o2 .`, + 'application/trig' + ) + const named = store.statements.find(s => s.subject.value === 'http://ex/s') + const dflt = store.statements.find(s => s.subject.value === 'http://ex/s2') + expect(named.why.value).to.equal('http://ex/g') + expect(dflt.why.value).to.equal(base) + }) + }) + + describe('text/n3 — Notation3 via N3.js, mapped onto rdflib\'s model', () => { + const IMPLIES = 'http://www.w3.org/2000/10/swap/log#implies' + const SAME_AS = 'http://www.w3.org/2002/07/owl#sameAs' + + function parseN3(content) { + const store = DataFactory.graph() + parse(content, store, base, 'text/n3') + return store + } + + it('builds Formula terms for { … } => { … }', () => { + const store = parseN3(`@prefix : <#>. { :a :b :c } => { :d :e :f }.`) + expect(store.statements).to.have.length(1) + const st = store.statements[0] + expect(st.predicate.value).to.equal(IMPLIES) + expect(st.subject.termType).to.equal('Graph') + expect(st.object.termType).to.equal('Graph') + expect(st.subject.statements).to.have.length(1) + expect(st.subject.statements[0].subject.value).to.equal(`${base}#a`) + expect(st.subject.statements[0].why.value).to.equal(base) + expect(st.object.statements[0].object.value).to.equal(`${base}#f`) + }) + + it('reverses <= into log:implies, like the legacy parser', () => { + const store = parseN3(`@prefix : <#>. { :p :q :r } <= { :s :t :u }.`) + const st = store.statements[0] + expect(st.predicate.value).to.equal(IMPLIES) + // the right-hand side of <= is the antecedent + expect(st.subject.statements[0].subject.value).to.equal(`${base}#s`) + expect(st.object.statements[0].subject.value).to.equal(`${base}#p`) + }) + + it('maps = to owl:sameAs', () => { + const store = parseN3(`@prefix : <#>. :x = :y.`) + expect(store.statements[0].predicate.value).to.equal(SAME_AS) + }) + + it('keeps distinct formula pairs distinct (no duplicate-suppression mixups)', () => { + const store = parseN3(`@prefix : <#>. { :a :b :c } => { :d :e :f }. { :g :h :i } => { :j :k :l }.`) + expect(store.statements).to.have.length(2) + }) + + it('parses an empty formula {} as an empty Formula term', () => { + const store = parseN3(`@prefix : <#>. :a :b {}.`) + expect(store.statements[0].object.termType).to.equal('Graph') + expect(store.statements[0].object.statements).to.have.length(0) + }) + + it('handles nested formulae with lists and variables inside', () => { + const store = parseN3( + `@prefix : <#>. { :v :in (1 2). ?w :q :u. } => { :done :is :true }.`) + expect(store.statements).to.have.length(1) + const antecedent = store.statements[0].subject + expect(antecedent.termType).to.equal('Graph') + const list = antecedent.statements.find(s => s.predicate.value === `${base}#in`).object + expect(list.termType).to.equal('Collection') + expect(list.elements.map(e => e.value)).to.eql(['1', '2']) + const varSt = antecedent.statements.find(s => s.predicate.value === `${base}#q`) + expect(varSt.subject.termType).to.equal('Variable') + expect(varSt.subject.value).to.equal('w') + }) + + it('registers @forAll as universals and @forSome as existentials, keeping the terms', () => { + const store = parseN3(`@prefix : <#>. @forAll :u. @forSome :v. :u :knows :v.`) + expect(store.statements).to.have.length(1) + expect(store.statements[0].subject.termType).to.equal('NamedNode') + expect((store._universalVariables || []).map(v => v.value)).to.eql([`${base}#u`]) + expect((store._existentialVariables || []).map(v => v.value)).to.eql([`${base}#v`]) + }) + + it('scopes @forSome declared inside a formula to that formula', () => { + const store = parseN3(`@prefix : <#>. { @forSome :v. :v :p :o. } => { :x :y :z }.`) + const antecedent = store.statements[0].subject + expect((antecedent._existentialVariables || []).map(v => v.value)).to.eql([`${base}#v`]) + expect(store._existentialVariables || []).to.have.length(0) + }) + + it('keeps document-labelled blank nodes whole across [ … ] property lists', () => { + // N3.js's n3 mode mis-scopes `_:c` when it is mentioned inside a + // [ … ] property list (it applies the graph label as a prefix); the + // adapter repairs the label so both mentions are the same node. + const store = parseN3(`@prefix : <#>. _:c :p 1. [ :q _:c ] :r 2.`) + const direct = store.statements.find(s => s.predicate.value === `${base}#p`).subject + const nested = store.statements.find(s => s.predicate.value === `${base}#q`).object + expect(direct.termType).to.equal('BlankNode') + expect(nested.termType).to.equal('BlankNode') + expect(nested.value).to.equal(direct.value) + }) + + it('rejects the retired cwm-era `is … of` syntax with a loud error (documented gap)', () => { + // The legacy parser expanded `:a is :spouse of :b` to `:b :spouse :a`. + // That syntax was dropped from the N3 CG spec and N3.js rejects it; + // rewrite such documents as inverse triples. + const store = DataFactory.graph() + expect(() => parse(`@prefix : .\n:a is :spouse of :b .`, store, base, 'text/n3')) + .to.throw(/Unexpected "is"/) + }) + + it('rejects the retired bareword date syntax with a loud error (documented gap)', () => { + // The legacy parser turned bareword `2026-07-01` into an xsd:date + // literal; that was never standard Turtle/N3. Quote the literal instead. + const store = DataFactory.graph() + expect(() => parse(`@prefix : .\n:a :b 2026-07-01 .`, store, base, 'text/n3')) + .to.throw() + }) + + it('rejects @keywords with a loud error (documented gap)', () => { + const store = DataFactory.graph() + expect(() => parse(`@keywords a.\n<#x> a <#Type>.`, store, base, 'text/n3')) + .to.throw() + }) + }) + + describe('RDF-star quoted triples', () => { + it('reports a descriptive error (rdflib has no term type for them)', () => { + const store = DataFactory.graph() + expect(() => parse(`<< >> .`, store, base, 'text/turtle')) + .to.throw(/cannot represent Quad terms/) + }) + }) + + describe('#352 — boolean barewords inside collections', () => { + it('produces proper Literal terms with termType inside Collection elements', () => { + const store = parseTtl(`<${base}#s> <${base}#p> ( true false ) .`) + const col = store.statements[0].object + expect(col.termType).to.equal('Collection') + col.elements.forEach(el => { + expect(el.termType).to.equal('Literal') + expect(el.datatype.value).to.equal(`${XSD}boolean`) + }) + expect(col.elements.map(e => e.value)).to.eql(['true', 'false']) + }) + }) + + describe('nested collections', () => { + it('folds ( 1 ( 2 3 ) () ) into nested Collection terms', () => { + const store = parseTtl(`<${base}#s> <${base}#p> ( 1 ( 2 3 ) ( ) ) .`) + const col = store.statements[0].object + expect(col.termType).to.equal('Collection') + expect(col.elements).to.have.length(3) + expect(col.elements[0].value).to.equal('1') + expect(col.elements[1].termType).to.equal('Collection') + expect(col.elements[1].elements.map(e => e.value)).to.eql(['2', '3']) + expect(col.elements[2].termType).to.equal('Collection') + expect(col.elements[2].elements).to.have.length(0) + }) + + it('folds explicitly-reified first/rest chains, as the legacy parser did', () => { + const store = parseTtl( + `@prefix rdf: <${RDF}>. <#t> <#v> [ rdf:first 1; rdf:rest [ rdf:first 2; rdf:rest rdf:nil ]].`) + const st = store.statements.find(s => s.predicate.value === `${base}#v`) + expect(st.object.termType).to.equal('Collection') + expect(st.object.elements.map(e => e.value)).to.eql(['1', '2']) + }) + + it('turns a lone rdf:nil into an empty Collection, as the legacy parser did', () => { + const store = parseTtl(`@prefix rdf: <${RDF}>. <#t> <#v> rdf:nil .`) + const st = store.statements.find(s => s.predicate.value === `${base}#v`) + expect(st.object.termType).to.equal('Collection') + expect(st.object.elements).to.have.length(0) + }) + }) +}) diff --git a/tests/unit/patch-parser-test.js b/tests/unit/patch-parser-test.js index 602d7d7cb..23458e7a5 100644 --- a/tests/unit/patch-parser-test.js +++ b/tests/unit/patch-parser-test.js @@ -42,6 +42,72 @@ describe('sparqlUpdateParser', () => { }, ]) }) + + it('binds ?variables in clauses to Variable terms', () => { + const store = new IndexedFormula() + const result = sparqlUpdateParser( + 'WHERE { <#me> ?name. }', + store, 'https://example.org/profile/') + expect(result.where.statements[0].object.termType).to.eql('Variable') + expect(result.where.statements[0].object.value).to.eql('name') + }) + + it('accepts @prefix directives between clauses', () => { + const store = new IndexedFormula() + const result = sparqlUpdateParser( + `@prefix foaf: . + INSERT DATA { <#me> foaf:nick "jw". }`, + store, 'https://example.org/profile/') + expect(result.insert.statements.map(termValues)).to.eql([ + { + subject: 'https://example.org/profile/#me', + predicate: 'http://xmlns.com/foaf/0.1/nick', + object: 'jw', + }, + ]) + }) + + it('accepts SPARQL-style PREFIX declarations without a trailing dot (#651)', () => { + const store = new IndexedFormula() + const result = sparqlUpdateParser( + `PREFIX foaf: + DELETE DATA { <#me> foaf:nick "jw". }`, + store, 'https://example.org/profile/') + expect(result.delete.statements.map(termValues)).to.eql([ + { + subject: 'https://example.org/profile/#me', + predicate: 'http://xmlns.com/foaf/0.1/nick', + object: 'jw', + }, + ]) + }) + + it('allows semicolons between clauses and braces inside string literals', () => { + const store = new IndexedFormula() + const result = sparqlUpdateParser( + 'INSERT { <#s> <#p> "curly } brace". } ; WHERE { <#s> <#q> "x". }', + store, 'https://example.org/doc') + expect(result.insert.statements[0].object.value).to.eql('curly } brace') + expect(result.where.statements[0].object.value).to.eql('x') + }) + + it('records the clauses on the target store under the patch vocabulary', () => { + const store = new IndexedFormula() + const base = 'https://example.org/doc' + const result = sparqlUpdateParser('WHERE { <#s> <#q> "x". }', store, base) + const st = store.statementsMatching( + store.sym(base + '#query'), + store.sym('http://www.w3.org/ns/pim/patch#where'), + null) + expect(st).to.have.length(1) + expect(st[0].object).to.equal(result.where) + }) + + it('throws a descriptive error on unknown top-level syntax', () => { + const store = new IndexedFormula() + expect(() => sparqlUpdateParser('FROBNICATE { <#a> <#b> <#c>. }', store, 'https://example.org/doc')) + .to.throw(/Unknown syntax/) + }) }) function termValues({ subject, predicate, object }) { diff --git a/tests/unit/serialize-test.js b/tests/unit/serialize-test.js index 9643139bd..495105a97 100644 --- a/tests/unit/serialize-test.js +++ b/tests/unit/serialize-test.js @@ -443,8 +443,8 @@ vocab:building1 vocab:created "2012-03-12"^^xsd:date; vocab:length 145000.0e0 . kasdfjsahdkfhkjhdkjsfhjkasdfkhjkajkdsajkhadsfkhjhjkdfajsdsafhjkdfhjksa 2012-12-10 2012-12-25T23:59 - 12 - 3.141 + 12.0 + 3.141e0 0 From 0ccb5d7b4532a9146c86269764f8bcbf39b117ac Mon Sep 17 00:00:00 2001 From: EC2 Default User Date: Sat, 4 Jul 2026 17:10:59 +0000 Subject: [PATCH 2/6] fix: resolve patch clauses under fragment bases, clear on forced reload, stub N3Parser Follow-up to the breaking-change assessment on this PR, landing its cheap mitigations (M1/M2/M4/M8): - sparqlUpdateParser: build the <#query> sym from the fragment-stripped base and write it as an absolute IRI in the generated N3, so insertion and lookup can never diverge. Previously a fragment-bearing base URI (e.g. .../card#me) made the clause lookup miss and the returned patch silently lost its insert/delete/where clauses (regression introduced by this branch; U7 in the assessment). Regression test added. - Fetcher.load: force: true now implies clearPreviousData: true unless the caller explicitly sets it to false. Parsed blank-node labels are no longer stable across parses, so re-parsing without clearing duplicated bnode subgraphs on every forced reload (U3/M1); this restores the net behaviour 2.x callers of load(uri, {force: true}) actually observed. Tests: statement count stable across a forced reload; explicit clearPreviousData: false still accumulates. - index: restore a deprecated N3Parser export as a stub that always throws 'N3Parser was removed in rdflib 3.0; use parse(text, store, base, contentType) or the n3 package directly', converting the silent 'undefined is not a constructor' failure into an actionable message (D4/M5). Throw message tested for both new and plain-call forms. - Documentation/turtle-intro.html: mark the naked date/dateTime and is...of extensions as removed in rdflib 3.0 (they now raise parse errors under every content type) with their standard replacements; note the empty-prefix default is still supported (M8). Co-Authored-By: Claude Fable 5 --- Documentation/turtle-intro.html | 19 +++++++++++++ src/fetcher.ts | 16 +++++++++-- src/index.ts | 13 +++++++++ src/patch-parser.js | 11 ++++++-- tests/unit/fetcher-test.js | 46 ++++++++++++++++++++++++++++++++ tests/unit/n3parser-stub-test.js | 22 +++++++++++++++ tests/unit/patch-parser-test.js | 37 +++++++++++++++++++++++++ 7 files changed, 160 insertions(+), 4 deletions(-) create mode 100644 tests/unit/n3parser-stub-test.js diff --git a/Documentation/turtle-intro.html b/Documentation/turtle-intro.html index 9cbcb4c4c..3a455edb4 100644 --- a/Documentation/turtle-intro.html +++ b/Documentation/turtle-intro.html @@ -302,7 +302,21 @@

Extensions in rdflib

generally, but are shortcts for test data and quick scripts. rdflib.js will understand these sytatax but never generate them

+

Update for rdflib 3.0: the parser was replaced by + N3.js, and the first two + extensions below — naked dates/date-times and + isof — were removed: they now + raise a parse error under every content type, including + text/n3. They are kept here for historical reference, each with + its standard replacement. The local empty-prefix default (the third + extension) is still supported. +

Naked Dates

+

Removed in rdflib 3.0. Write the standard typed literals instead: + "2018-02-31"^^xsd:date and + "2018-02-31T08:24:00.0Z"^^xsd:dateTime + (with @prefix xsd: <http://www.w3.org/2001/XMLSchema#>.). +

@@ -340,6 +354,11 @@ 

Reverse properties

The is ... of syntax was in the original N3 language which Turtle was derived from but unfortunately left out of the turtle standard.

+

Removed in rdflib 3.0 (the modern N3 Community Group grammar + dropped it too). Write the inverse triple instead, e.g. + :grampa fam:child :alice . rather than + :alice is fam:child of :grampa . +

Local document prefix

It is handy to define the empty string prefix : as being for the local document, so it can be used for local identifiers. diff --git a/src/fetcher.ts b/src/fetcher.ts index 30017c073..e21ac51d3 100644 --- a/src/fetcher.ts +++ b/src/fetcher.ts @@ -154,7 +154,9 @@ export interface AutoInitOptions extends RequestInit{ forceContentType?: ContentType /** * Load the data even if loaded before. - * Also sets the `Cache-Control:` header to `no-cache` + * Also sets the `Cache-Control:` header to `no-cache`, and (in + * `Fetcher.load`) implies `clearPreviousData: true` unless that option is + * explicitly set to `false`. */ force?: boolean /** @@ -942,7 +944,10 @@ export default class Fetcher implements CallbackifyInterface { * force the data to be treated as this content-type (for reads) * * @param [options.force] {boolean} Load the data even if loaded before. - * Also sets the `Cache-Control:` header to `no-cache` + * Also sets the `Cache-Control:` header to `no-cache`, and implies + * `clearPreviousData: true` unless that option is explicitly set to + * `false` (re-parsing without clearing would duplicate blank-node + * subgraphs, as parsed blank-node labels are not stable across parses) * * @param [options.baseURI=docuri] {Node|string} Original uri to preserve * through proxying etc (`xhr.original`). @@ -967,6 +972,13 @@ export default class Fetcher implements CallbackifyInterface { options: Options = {} ): T extends Array ? Promise : Promise { options = Object.assign({}, options) // Take a copy as we add stuff to the options!! + // `force` implies `clearPreviousData` (unless the caller explicitly opts + // out): parsed blank-node labels are not stable across parses, so + // re-parsing a previously loaded document without clearing it first would + // duplicate its blank-node subgraphs on every forced reload. + if (options.force && options.clearPreviousData === undefined) { + options.clearPreviousData = true + } if (uri instanceof Array) { return Promise.all(uri.map((x) => { return this.load(x, Object.assign({}, options)) as unknown as Promise diff --git a/src/index.ts b/src/index.ts index 52a4b206b..951e43e13 100644 --- a/src/index.ts +++ b/src/index.ts @@ -59,6 +59,18 @@ const term = Node.fromValue // it exports the _current_ value of nextId, which is always 0 const NextId = BlankNode.nextId +/** + * @deprecated The hand-rolled N3 parser was removed in rdflib 3.0 in favour + * of N3.js. This stub only exists to fail loudly with a pointer: use + * `parse(text, store, base, contentType)` (or the `n3` package directly) + * instead. Calling or constructing it always throws. + */ +function N3Parser (): never { + throw new Error( + 'N3Parser was removed in rdflib 3.0; use parse(text, store, base, contentType) or the n3 package directly' + ) +} + export * from './utils/terms' export type { AutoInitOptions, ExtendedResponse, FetchError } from './fetcher' export { @@ -72,6 +84,7 @@ export { jsonParser, Literal, log, + N3Parser, NamedNode, Namespace, Node, diff --git a/src/patch-parser.js b/src/patch-parser.js index 90c817084..cf9f586ae 100644 --- a/src/patch-parser.js +++ b/src/patch-parser.js @@ -15,7 +15,12 @@ const SQNS = Namespace('http://www.w3.org/ns/pim/patch#') export default function sparqlUpdateParser (str, kb, base) { const clauses = {} - const query = kb.sym(base + '#query') // Invent a URI for the query + // Invent a URI for the query. Resolve it against the *fragment-stripped* + // base: the N3 rewrite below names this node <#query>, which RFC 3986 + // resolves by replacing any fragment on the base, so building the sym from + // `base + '#query'` would silently mismatch (and lose every clause) when + // the caller's base URI carries a fragment. + const query = kb.sym(base.split('#')[0] + '#query') clauses['query'] = query // A way of accessing it in its N3 model. const badSyntax = function (uri, str, i, why) { @@ -58,7 +63,9 @@ export default function sparqlUpdateParser (str, kb, base) { if (end < 0) { throw badSyntax(base, str, j, 'bad syntax or EOF in {...} after ' + key) } - n3doc += '<#query> <' + SQNS(key.toLowerCase()).value + '> ' + str.slice(j, end) + ' .\n' + // Write the query node as an absolute IRI so that the sym used for + // insertion is byte-identical to the one used for lookup below. + n3doc += '<' + query.value + '> <' + SQNS(key.toLowerCase()).value + '> ' + str.slice(j, end) + ' .\n' order.push(key.toLowerCase()) i = end found = true diff --git a/tests/unit/fetcher-test.js b/tests/unit/fetcher-test.js index c5666a4ac..67326e04c 100644 --- a/tests/unit/fetcher-test.js +++ b/tests/unit/fetcher-test.js @@ -564,6 +564,52 @@ describe('Fetcher', () => { }) }) + it('does not accumulate blank-node subgraphs on force-reload (force implies clearPreviousData)', () => { + // Parsed blank-node labels are not stable across parses, so re-parsing + // a document without clearing it first duplicates its blank-node + // subgraphs. `force: true` therefore implies `clearPreviousData: true`. + const testTurtle = `@prefix foaf: . +<#me> foaf:knows [ foaf:name "Amy" ], [ foaf:name "Bob" ].` + const doc = 'https://example.com/bnodes.ttl' + + nock('https://example.com').get('/bnodes.ttl').twice() + .reply(200, testTurtle, { 'Content-Type': 'text/turtle' }) + + const kb = fetcher.store + return fetcher.load(doc) + .then(() => { + const before = kb.statementsMatching(null, null, null, kb.sym(doc)).length + expect(before).to.equal(4) + + return fetcher.load(doc, { force: true }) + .then(() => { + const after = kb.statementsMatching(null, null, null, kb.sym(doc)).length + expect(after).to.equal(before) + }) + }) + }) + + it('accumulates on force-reload when clearPreviousData is explicitly false', () => { + const testTurtle = `@prefix foaf: . +<#me> foaf:knows [ foaf:name "Amy" ].` + const doc = 'https://example.com/bnodes-keep.ttl' + + nock('https://example.com').get('/bnodes-keep.ttl').twice() + .reply(200, testTurtle, { 'Content-Type': 'text/turtle' }) + + const kb = fetcher.store + return fetcher.load(doc) + .then(() => { + const before = kb.statementsMatching(null, null, null, kb.sym(doc)).length + + return fetcher.load(doc, { force: true, clearPreviousData: false }) + .then(() => { + const after = kb.statementsMatching(null, null, null, kb.sym(doc)).length + expect(after).to.be.above(before) + }) + }) + }) + it('should load and parse N3 with formula subjects (#567)', () => { // Loading a document like https://drive.verborgh.org/tmp/2022/cha-58-direct.n3 // used to fail with "Subject is not a subject type". diff --git a/tests/unit/n3parser-stub-test.js b/tests/unit/n3parser-stub-test.js new file mode 100644 index 000000000..a3fb55420 --- /dev/null +++ b/tests/unit/n3parser-stub-test.js @@ -0,0 +1,22 @@ +import { expect } from 'chai' + +import * as rdf from '../../src/index' + +// The N3Parser class itself was removed in rdflib 3.0 (replaced by N3.js); +// a deprecated stub export remains so that legacy call sites fail with an +// actionable message instead of "undefined is not a constructor". +describe('N3Parser deprecation stub', () => { + it('is still exported', () => { + expect(rdf.N3Parser).to.be.an.instanceOf(Function) + }) + + it('throws a descriptive pointer to parse() when constructed', () => { + expect(() => new rdf.N3Parser()) + .to.throw('N3Parser was removed in rdflib 3.0; use parse(text, store, base, contentType) or the n3 package directly') + }) + + it('throws the same pointer when called as a plain function', () => { + expect(() => rdf.N3Parser()) + .to.throw('N3Parser was removed in rdflib 3.0; use parse(text, store, base, contentType) or the n3 package directly') + }) +}) diff --git a/tests/unit/patch-parser-test.js b/tests/unit/patch-parser-test.js index 23458e7a5..3146852cc 100644 --- a/tests/unit/patch-parser-test.js +++ b/tests/unit/patch-parser-test.js @@ -103,6 +103,43 @@ describe('sparqlUpdateParser', () => { expect(st[0].object).to.equal(result.where) }) + it('resolves the clauses when the base URI carries a fragment', () => { + // Regression: the query node is written as <#query> in the generated N3, + // which RFC 3986 resolves by *replacing* any fragment on the base — but + // the lookup sym used to be built as `base + '#query'`. With a + // fragment-bearing base the two silently diverged and the returned patch + // lost its insert/delete/where clauses. + const store = new IndexedFormula() + const result = sparqlUpdateParser( + `DELETE { <#me> "old". } + INSERT { <#me> "new". } + WHERE { <#me> "old". }`, + store, 'https://example.org/profile/card#me') + + expect(result.query.value).to.eql('https://example.org/profile/card#query') + expect(result.delete.statements.map(termValues)).to.eql([ + { + subject: 'https://example.org/profile/card#me', + predicate: 'http://xmlns.com/foaf/0.1/nick', + object: 'old', + }, + ]) + expect(result.insert.statements.map(termValues)).to.eql([ + { + subject: 'https://example.org/profile/card#me', + predicate: 'http://xmlns.com/foaf/0.1/nick', + object: 'new', + }, + ]) + expect(result.where.statements.map(termValues)).to.eql([ + { + subject: 'https://example.org/profile/card#me', + predicate: 'http://xmlns.com/foaf/0.1/nick', + object: 'old', + }, + ]) + }) + it('throws a descriptive error on unknown top-level syntax', () => { const store = new IndexedFormula() expect(() => sparqlUpdateParser('FROBNICATE { <#a> <#b> <#c>. }', store, 'https://example.org/doc')) From c9ec6a6172b8cf8f3b7fd343476dce0dd9c20a09 Mon Sep 17 00:00:00 2001 From: EC2 Default User Date: Sat, 4 Jul 2026 19:25:58 +0000 Subject: [PATCH 3/6] feat: Add value-space literal helpers and an opt-in canonicalize parse flag MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The N3.js-based parsers preserve literal lexical forms, so stored `true` / `12.0` literals no longer compare equal to canonically-constructed ones (`"1"^^xsd:boolean`, `"12"^^xsd:decimal`). This ships the two transition aids from the breaking-change assessment (mitigation M3): - Value-space helpers `isTrue(term)`, `literalToBoolean(term)` and `literalToNumber(term)` (also exposed as `Literal.toBoolean` / `Literal.toNumber`, the inverses of `fromBoolean` / `fromNumber`). They accept every lexical form the datatype allows, giving ^2.x consumers a one-line fix for patterns like `term.value === '1'`. - An opt-in `canonicalize` parse option — `parse(str, kb, base, type, { canonicalize: true })` or as a sixth argument after a callback — that restores rdflib <=2's parse-time normalisation of xsd:boolean / integer / decimal / double / float lexical forms (`true` -> "1", `12.0` -> "12", `3.141e0` -> "3.141") across the Turtle family. Off by default; lexical preservation remains the shipped behaviour. Integer canonicalisation is done lexically so arbitrary-precision values keep their exact digits, and ill-typed forms are left untouched. Covered by a helper truth table, canonicalize on/off store-comparison tests and a tests/types compile check of the new exports. Co-Authored-By: Claude Fable 5 --- src/index.ts | 2 + src/literal.ts | 28 +++++ src/n3-adapter.ts | 75 +++++++++++- src/parse.ts | 44 +++++-- src/utils/literalValue.ts | 161 ++++++++++++++++++++++++++ tests/types/value-space.ts | 31 +++++ tests/unit/literal-value-test.js | 156 +++++++++++++++++++++++++ tests/unit/parse-canonicalize-test.js | 120 +++++++++++++++++++ 8 files changed, 605 insertions(+), 12 deletions(-) create mode 100644 src/utils/literalValue.ts create mode 100644 tests/types/value-space.ts create mode 100644 tests/unit/literal-value-test.js create mode 100644 tests/unit/parse-canonicalize-test.js diff --git a/src/index.ts b/src/index.ts index 951e43e13..15e388b07 100644 --- a/src/index.ts +++ b/src/index.ts @@ -72,7 +72,9 @@ function N3Parser (): never { } export * from './utils/terms' +export { isTrue, literalToBoolean, literalToNumber } from './utils/literalValue' export type { AutoInitOptions, ExtendedResponse, FetchError } from './fetcher' +export type { ParseOptions } from './parse' export { BlankNode, Collection, diff --git a/src/literal.ts b/src/literal.ts index b67862b5d..6425fcbc8 100644 --- a/src/literal.ts +++ b/src/literal.ts @@ -7,6 +7,7 @@ import { ValueType } from './types' import { isLiteral } from './utils/terms' +import { literalToBoolean, literalToNumber } from './utils/literalValue' import XSD from './xsd-internal' import { Literal as TFLiteral, Term } from './tf-types' @@ -163,6 +164,33 @@ export default class Literal extends Node implements TFLiteral { return new Literal(strValue, null, datatype) } + /** + * Reads an `xsd:boolean` literal in value space — the inverse of + * {@link fromBoolean}, accepting every valid lexical form (`"true"`, + * `"1"`, `"false"`, `"0"`). Returns `undefined` for non-literals, + * other datatypes and ill-typed lexical forms. + * + * The parser preserves source lexical forms, so a stored `true` no longer + * reads back as `"1"`; compare booleans with this (or {@link isTrue}) + * rather than against one spelling of `term.value`. + * @param term - The term to read; may be `null`/`undefined` + */ + static toBoolean (term: Term | null | undefined): boolean | undefined { + return literalToBoolean(term) + } + + /** + * Reads a numeric (`xsd:decimal`/integer-family/floating-point) literal in + * value space — the inverse of {@link fromNumber}, accepting every valid + * lexical form (`"12"`, `"12.0"`, `"1.2e1"` all read as `12`). Returns + * `undefined` for non-literals, non-numeric datatypes and ill-typed + * lexical forms. + * @param term - The term to read; may be `null`/`undefined` + */ + static toNumber (term: Term | null | undefined): number | undefined { + return literalToNumber(term) + } + /** * Builds a literal node from an input value * @param value - The input value diff --git a/src/n3-adapter.ts b/src/n3-adapter.ts index 76119af82..1794d2c64 100644 --- a/src/n3-adapter.ts +++ b/src/n3-adapter.ts @@ -62,6 +62,15 @@ export const N3JS_FORMATS: { [contentType: string]: string } = { [TrigContentType]: 'application/trig', } +/** Options accepted by {@link parseN3js} (a subset of `parse()`'s options). */ +export type ParseN3jsOptions = { + /** + * Canonicalize the lexical forms of boolean and numeric literals at parse + * time, as rdflib ≤2 did — see the `canonicalize` option of `parse()`. + */ + canonicalize?: boolean +} + /** * Parse a Turtle-family document with N3.js and load it into the given store. * @@ -69,16 +78,17 @@ export const N3JS_FORMATS: { [contentType: string]: string } = { * @param kb - The store (or plain Formula) to load the statements into * @param base - The base IRI for relative-IRI resolution; also names the document graph * @param contentType - One of the content types in {@link N3JS_FORMATS} + * @param [options] - Parse options; see {@link ParseN3jsOptions} * @returns The number of statements loaded */ -export default function parseN3js (str: string, kb: Formula, base: string, contentType: string): number { +export default function parseN3js (str: string, kb: Formula, base: string, contentType: string, options?: ParseN3jsOptions): number { const format = N3JS_FORMATS[contentType] if (!format) { throw new Error('parseN3js: unsupported content type ' + contentType) } const sugared = format === 'text/n3' || format === 'text/turtle' try { - return runParse(str, kb, base, format, false) + return runParse(str, kb, base, format, false, options) } catch (e) { // rdflib's legacy parser implicitly bound the empty prefix `:` to // ``, so documents using `:name` without declaring `@prefix :` @@ -89,13 +99,14 @@ export default function parseN3js (str: string, kb: Formula, base: string, conte // still overrides it from that point on, exactly like the sequential // semantics of the legacy parser. if (sugared && base && /Undefined prefix ":"/.test(String(e && (e as Error).message))) { - return runParse(str, kb, base, format, true) + return runParse(str, kb, base, format, true, options) } throw e } } -function runParse (str: string, kb: Formula, base: string, format: string, seedEmptyPrefix: boolean): number { +function runParse (str: string, kb: Formula, base: string, format: string, seedEmptyPrefix: boolean, options?: ParseN3jsOptions): number { + const canonicalize = !!(options && options.canonicalize) const n3Mode = format === 'text/n3' const rdfFactory = kb.rdfFactory const docGraph = base ? kb.sym(base) : rdfFactory.defaultGraph() @@ -271,7 +282,11 @@ function runParse (str: string, kb: Formula, base: string, format: string, seedE if (n3Mode && t.value.charAt(0) === '.') return rdfFactory.blankNode(docLabelPrefix + t.value.slice(1)) return rdfFactory.blankNode(t.value) case 'Literal': - return rdfFactory.literal(t.value, t.language || rdfFactory.namedNode(t.datatype.value)) + if (t.language) return rdfFactory.literal(t.value, t.language) + return rdfFactory.literal( + canonicalize ? canonicalLexicalForm(t.value, t.datatype.value) : t.value, + rdfFactory.namedNode(t.datatype.value) + ) case 'Variable': return rdfFactory.variable ? rdfFactory.variable(t.value) : new Variable(t.value) case 'DefaultGraph': @@ -319,6 +334,56 @@ function runParse (str: string, kb: Formula, base: string, format: string, seedE return count } +const XSD_NS = 'http://www.w3.org/2001/XMLSchema#' +const XSD_BOOLEAN = XSD_NS + 'boolean' +const XSD_INTEGER = XSD_NS + 'integer' +const XSD_DECIMAL = XSD_NS + 'decimal' +const XSD_DOUBLE = XSD_NS + 'double' +const XSD_FLOAT = XSD_NS + 'float' + +const INTEGER_LEXICAL = /^([+-]?)0*([0-9]+)$/ +const DECIMAL_LEXICAL = /^[+-]?(?:[0-9]+(?:\.[0-9]*)?|\.[0-9]+)$/ +const FLOATING_LEXICAL = /^[+-]?(?:[0-9]+(?:\.[0-9]*)?|\.[0-9]+)(?:[eE][+-]?[0-9]+)?$/ + +/** + * Map a boolean or numeric lexical form to the canonical form rdflib ≤2 + * produced at parse time (the `canonicalize: true` compatibility mode): + * booleans become `"1"`/`"0"` (matching `Literal.fromBoolean`), integers + * lose their sign/leading-zero decoration, and decimals/doubles/floats are + * rewritten as JavaScript stringifies their numeric value (`12.0` → `"12"`, + * `3.141e0` → `"3.141"`), exactly as the legacy parsers' number round-trip + * did. Only valid lexical forms are rewritten; anything ill-typed (and any + * other datatype) is preserved as-is. + */ +function canonicalLexicalForm (value: string, datatype: string): string { + switch (datatype) { + case XSD_BOOLEAN: + if (value === 'true') return '1' + if (value === 'false') return '0' + return value + case XSD_INTEGER: { + // Rewritten lexically (not via Number) so arbitrary-precision integers + // keep their exact value. + const m = INTEGER_LEXICAL.exec(value) + if (!m) return value + return (m[1] === '-' && m[2] !== '0' ? '-' : '') + m[2] + } + case XSD_DECIMAL: { + if (!DECIMAL_LEXICAL.test(value)) return value + const canonical = String(Number(value)) + // Guard: keep the source form when JS would stringify with an exponent + // (e.g. 0.0000001 → "1e-7"), which is outside xsd:decimal's lexical space. + return DECIMAL_LEXICAL.test(canonical) ? canonical : value + } + case XSD_DOUBLE: + case XSD_FLOAT: + // INF/-INF/NaN don't match and are preserved as-is. + return FLOATING_LEXICAL.test(value) ? String(Number(value)) : value + default: + return value + } +} + /** Percent-escape the characters that cannot appear in a Turtle IRIREF. */ function escapeIri (iri: string): string { // eslint-disable-next-line no-control-regex diff --git a/src/parse.ts b/src/parse.ts index 3ec345612..fa35467e9 100644 --- a/src/parse.ts +++ b/src/parse.ts @@ -10,6 +10,29 @@ import type { Document as XmldomDocument } from '@xmldom/xmldom' type CallbackFunc = (error: any, kb: Formula | null) => void +/** Options accepted by {@link parse}. */ +export type ParseOptions = { + /** + * Canonicalize the lexical forms of boolean and numeric literals at parse + * time, the way rdflib ≤2's own parsers did: `true`/`false` become + * `"1"`/`"0"` (matching `Literal.fromBoolean`), `12.0` becomes `"12"`, + * `3.141e0` becomes `"3.141"`, `+05` becomes `"5"`. Covers `xsd:boolean`, + * `xsd:integer`, `xsd:decimal`, `xsd:double` and `xsd:float`; ill-typed + * lexical forms and all other datatypes are preserved as-is. Applies to + * the Turtle-family content types (Turtle, N3, TriG, N-Triples, N-Quads). + * + * Off by default: since the migration to the N3.js parser, literals keep + * the exact lexical form found in the document, as the RDF specs + * prescribe. This flag is a transition aid for code that still compares + * literals by one canonical spelling (`term.value === '1'`, + * `kb.holds(s, p, Literal.fromBoolean(true))`); new code should keep the + * default and compare in value space instead — see `isTrue`, + * `literalToBoolean` and `literalToNumber` (also available as + * `Literal.toBoolean` / `Literal.toNumber`). + */ + canonicalize?: boolean +} + /** * Parse a string and put the result into the graph kb. * Normal method is sync. @@ -19,22 +42,29 @@ type CallbackFunc = (error: any, kb: Formula | null) => void * @param kb - The store to use * @param base - The base URI to use * @param contentType - The MIME content type string for the input - defaults to text/turtle - * @param [callback] - The callback to call when the data has been loaded + * @param [callback] - The callback to call when the data has been loaded. + * May be omitted: an options object may be passed in this position instead. + * @param [options] - Parse options; see {@link ParseOptions} */ export default function parse ( str: string, kb: Formula, base: string, contentType: string | ContentType = 'text/turtle', - callback?: CallbackFunc + callback?: CallbackFunc | ParseOptions | null, + options?: ParseOptions ) { + if (callback && typeof callback === 'object') { + options = callback // parse(str, kb, base, contentType, { canonicalize: true }) + } + const cb: CallbackFunc | undefined = typeof callback === 'function' ? callback : undefined contentType = contentType || TurtleContentType contentType = contentType.split(';')[0] as ContentType try { if (Object.prototype.hasOwnProperty.call(N3JS_FORMATS, contentType)) { // The Turtle family — Turtle, N3, TriG, N-Triples and N-Quads — is // parsed by the N3.js parser, adapted onto rdflib's model. - parseN3js(str, kb, base, contentType) + parseN3js(str, kb, base, contentType, options) executeCallback() } else if (contentType === RDFXMLContentType) { var parser = new RDFParser(kb) @@ -84,8 +114,8 @@ export default function parse ( } function executeCallback () { - if (callback) { - callback(null, kb) + if (cb) { + cb(null, kb) } else { return } @@ -100,8 +130,8 @@ export default function parse ( // @ts-ignore always true? contentType !== NQuadsAltContentType ) { - if (callback) { - callback(e, kb) + if (cb) { + cb(e, kb) } else { let e2 = new Error('' + e + ' while trying to parse <' + base + '> as ' + contentType) //@ts-ignore .cause is not a default error property diff --git a/src/utils/literalValue.ts b/src/utils/literalValue.ts new file mode 100644 index 000000000..7c92b92e6 --- /dev/null +++ b/src/utils/literalValue.ts @@ -0,0 +1,161 @@ +/** + * Value-space helpers for reading typed literals. + * + * Since the migration to the N3.js parser, the Turtle-family parsers preserve + * the source lexical form of literals: `true` is kept as + * `"true"^^xsd:boolean` (it is no longer normalised to `"1"`), `12.0` stays + * `"12.0"^^xsd:decimal` (no longer `"12"`), `3.141e0` stays `"3.141e0"`. + * Code that compares `term.value` against a single canonical spelling — + * `term.value === '1'`, `kb.holds(s, p, Literal.fromBoolean(true))` — breaks + * silently on data whose author spelled the same value differently (and + * rdflib's own serializer emits `true`, so stored data usually has the + * non-`"1"` spelling). + * + * These helpers compare in *value space* instead: they accept every lexical + * form the datatype's lexical space allows and return the datatype's value, + * so `"true"`, `"1"`, `" true "` all read as `true`, and `"12"`, `"12.0"`, + * `"1.2e1"` all read as `12`. + * + * See also the `canonicalize` option of `parse()` for a transitional + * alternative that restores the old parse-time normalisation instead. + */ +import { Term, Literal as TFLiteral } from '../tf-types' + +const XSD_NS = 'http://www.w3.org/2001/XMLSchema#' +const XSD_BOOLEAN = XSD_NS + 'boolean' + +/** + * The XSD numeric datatypes read by {@link literalToNumber}: xsd:decimal, + * the xsd:integer hierarchy derived from it, and the floating-point types. + */ +const NUMERIC_DATATYPES: { [iri: string]: 'integer' | 'decimal' | 'floating' } = { + [XSD_NS + 'decimal']: 'decimal', + [XSD_NS + 'double']: 'floating', + [XSD_NS + 'float']: 'floating', + [XSD_NS + 'integer']: 'integer', + [XSD_NS + 'long']: 'integer', + [XSD_NS + 'int']: 'integer', + [XSD_NS + 'short']: 'integer', + [XSD_NS + 'byte']: 'integer', + [XSD_NS + 'nonNegativeInteger']: 'integer', + [XSD_NS + 'nonPositiveInteger']: 'integer', + [XSD_NS + 'negativeInteger']: 'integer', + [XSD_NS + 'positiveInteger']: 'integer', + [XSD_NS + 'unsignedLong']: 'integer', + [XSD_NS + 'unsignedInt']: 'integer', + [XSD_NS + 'unsignedShort']: 'integer', + [XSD_NS + 'unsignedByte']: 'integer', +} + +const INTEGER_LEXICAL = /^[+-]?[0-9]+$/ +const DECIMAL_LEXICAL = /^[+-]?(?:[0-9]+(?:\.[0-9]*)?|\.[0-9]+)$/ +const FLOATING_LEXICAL = /^[+-]?(?:[0-9]+(?:\.[0-9]*)?|\.[0-9]+)(?:[eE][+-]?[0-9]+)?$/ +const FLOATING_SPECIALS: { [lexical: string]: number } = { + 'INF': Infinity, + '+INF': Infinity, + '-INF': -Infinity, + 'NaN': NaN, +} + +/** Structural literal check that works for terms from any RDF/JS factory. */ +function asLiteral (term: Term | null | undefined): TFLiteral | null { + if (!term || typeof term !== 'object' || term.termType !== 'Literal') { + return null + } + return term as TFLiteral +} + +/** + * Reads an `xsd:boolean` literal in value space. + * + * Returns `true` for the lexical forms `"true"` and `"1"`, `false` for + * `"false"` and `"0"` (leading/trailing whitespace is ignored, per XSD's + * whitespace collapse), and `undefined` for everything else — a missing + * term, a non-literal, a literal of another datatype, or an ill-typed + * lexical form. + * + * The distinction between `false` and `undefined` lets callers tell an + * explicitly-false setting apart from an unset one: + * ```js + * const setting = literalToBoolean(kb.any(me, ns.dct('audienceEnabled'), null, doc)) + * if (setting === undefined) applyDefault() + * ``` + * For a plain "is it on?" read, use {@link isTrue}. + * + * @param term - The term to read; `null`/`undefined` are accepted (and map + * to `undefined`) so `kb.any(...)` results can be passed directly + */ +export function literalToBoolean (term: Term | null | undefined): boolean | undefined { + const literal = asLiteral(term) + if (!literal || !literal.datatype || literal.datatype.value !== XSD_BOOLEAN) { + return undefined + } + switch (literal.value.trim()) { + case 'true': + case '1': + return true + case 'false': + case '0': + return false + default: + return undefined + } +} + +/** + * Whether the given term is an `xsd:boolean` literal that is true in value + * space — i.e. its lexical form is `"true"` or `"1"`. + * + * This is the drop-in, value-space replacement for the pre-3.0 pattern + * `kb.anyValue(s, p) === '1'`, which relied on the old parsers normalising + * every true boolean to `"1"`: + * ```js + * const enabled = isTrue(kb.any(subject, predicate, null, doc)) + * ``` + * Anything that is not a true boolean literal — a false one, an ill-typed + * one, a non-literal, `null`, `undefined` — yields `false`. + */ +export function isTrue (term: Term | null | undefined): boolean { + return literalToBoolean(term) === true +} + +/** + * Reads a numeric literal in value space, so that `"12"^^xsd:integer`, + * `"12.0"^^xsd:decimal` and `"1.2e1"^^xsd:double` all read as the number + * `12` no matter how the source document spelled them. + * + * Handles `xsd:decimal`, the derived integer types (`xsd:integer`, + * `xsd:long`, `xsd:int`, …) and the floating-point types (`xsd:double`, + * `xsd:float`, including their `INF`/`-INF`/`NaN` specials). Returns + * `undefined` for a missing term, a non-literal, a literal of a + * non-numeric datatype, or a lexical form outside the datatype's lexical + * space. Values beyond IEEE-754 double precision are rounded to the nearest + * representable number, as with JavaScript's `Number`. + * + * @param term - The term to read; `null`/`undefined` are accepted (and map + * to `undefined`) so `kb.any(...)` results can be passed directly + */ +export function literalToNumber (term: Term | null | undefined): number | undefined { + const literal = asLiteral(term) + if (!literal || !literal.datatype) { + return undefined + } + const family = NUMERIC_DATATYPES[literal.datatype.value] + if (family === undefined) { + return undefined + } + const lexical = literal.value.trim() + switch (family) { + case 'integer': + return INTEGER_LEXICAL.test(lexical) ? Number(lexical) : undefined + case 'decimal': + return DECIMAL_LEXICAL.test(lexical) ? Number(lexical) : undefined + case 'floating': + if (Object.prototype.hasOwnProperty.call(FLOATING_SPECIALS, lexical)) { + return FLOATING_SPECIALS[lexical] + } + return FLOATING_LEXICAL.test(lexical) ? Number(lexical) : undefined + default: + return undefined + } +} diff --git a/tests/types/value-space.ts b/tests/types/value-space.ts new file mode 100644 index 000000000..d3356d988 --- /dev/null +++ b/tests/types/value-space.ts @@ -0,0 +1,31 @@ +import { + graph, + isTrue, + literalToBoolean, + literalToNumber, + Literal, + parse, + sym, +} from '../../src/index'; +import type { ParseOptions } from '../../src/index'; + +// The value-space literal helpers accept any RDF/JS term as well as the +// null/undefined that kb.any() can produce, and are exposed both standalone +// and as Literal statics. parse() takes the `canonicalize` option either in +// the callback slot or as a sixth argument after a callback. +const kb = graph(); +const doc = 'https://example.net/doc'; + +parse('<#s> <#p> true .', kb, doc, 'text/turtle'); +parse('<#s> <#p> 12.0 .', kb, doc, 'text/turtle', { canonicalize: true }); +parse('<#s> <#p> 3.141e0 .', kb, doc, 'text/turtle', (_error, _kb) => {}, { canonicalize: false }); +const options: ParseOptions = { canonicalize: true }; +parse('<#s> <#p> false .', kb, doc, 'text/turtle', null, options); + +const term = kb.any(sym(`${doc}#s`), sym(`${doc}#p`)); +const asBoolean: boolean | undefined = literalToBoolean(term); +const asBooleanStatic: boolean | undefined = Literal.toBoolean(undefined); +const truthy: boolean = isTrue(term); +const truthyOfMissing: boolean = isTrue(null); +const asNumber: number | undefined = literalToNumber(term); +const asNumberStatic: number | undefined = Literal.toNumber(new Literal('12')); diff --git a/tests/unit/literal-value-test.js b/tests/unit/literal-value-test.js new file mode 100644 index 000000000..4b00e8fad --- /dev/null +++ b/tests/unit/literal-value-test.js @@ -0,0 +1,156 @@ +import { expect } from 'chai' + +import { isTrue, literalToBoolean, literalToNumber } from '../../src/utils/literalValue' +import Literal from '../../src/literal' +import NamedNode from '../../src/named-node' +import XSD from '../../src/xsd' +import parse from '../../src/parse' +import DataFactory from '../../src/factories/rdflib-data-factory' + +const XSDNS = 'http://www.w3.org/2001/XMLSchema#' +const base = 'https://example.org/doc' + +const boolLit = (lexical) => new Literal(lexical, null, XSD.boolean) +const typedLit = (lexical, localName) => new Literal(lexical, null, new NamedNode(XSDNS + localName)) + +/** + * Value-space reads of typed literals. The N3.js-based parsers preserve the + * source lexical form (a stored `true` is no longer normalised to `"1"`), so + * consumers must compare boolean/numeric literals in value space — these + * helpers are the supported way to do that. + */ +describe('value-space literal helpers', () => { + describe('literalToBoolean', () => { + it('maps every valid xsd:boolean lexical form (truth table)', () => { + expect(literalToBoolean(boolLit('true'))).to.equal(true) + expect(literalToBoolean(boolLit('1'))).to.equal(true) + expect(literalToBoolean(boolLit('false'))).to.equal(false) + expect(literalToBoolean(boolLit('0'))).to.equal(false) + }) + + it('collapses surrounding whitespace, per the XSD whitespace facet', () => { + expect(literalToBoolean(boolLit(' true '))).to.equal(true) + expect(literalToBoolean(boolLit('\tfalse\n'))).to.equal(false) + }) + + it('returns undefined for ill-typed lexical forms', () => { + expect(literalToBoolean(boolLit('yes'))).to.equal(undefined) + expect(literalToBoolean(boolLit('TRUE'))).to.equal(undefined) + expect(literalToBoolean(boolLit(''))).to.equal(undefined) + }) + + it('returns undefined for literals of other datatypes', () => { + expect(literalToBoolean(new Literal('true'))).to.equal(undefined) // xsd:string + expect(literalToBoolean(new Literal('true', 'en'))).to.equal(undefined) // langString + expect(literalToBoolean(typedLit('1', 'integer'))).to.equal(undefined) + }) + + it('returns undefined for non-literals and missing terms', () => { + expect(literalToBoolean(new NamedNode('https://example.org/true'))).to.equal(undefined) + expect(literalToBoolean(null)).to.equal(undefined) + expect(literalToBoolean(undefined)).to.equal(undefined) + }) + + it('is the value-space inverse of Literal.fromBoolean', () => { + expect(literalToBoolean(Literal.fromBoolean(true))).to.equal(true) + expect(literalToBoolean(Literal.fromBoolean(false))).to.equal(false) + }) + }) + + describe('isTrue', () => { + it('is true exactly for true boolean literals', () => { + expect(isTrue(boolLit('true'))).to.equal(true) + expect(isTrue(boolLit('1'))).to.equal(true) + expect(isTrue(boolLit('false'))).to.equal(false) + expect(isTrue(boolLit('0'))).to.equal(false) + }) + + it('is false for anything that is not a true boolean literal', () => { + expect(isTrue(boolLit('yes'))).to.equal(false) + expect(isTrue(new Literal('true'))).to.equal(false) + expect(isTrue(new NamedNode('https://example.org/x'))).to.equal(false) + expect(isTrue(null)).to.equal(false) + expect(isTrue(undefined)).to.equal(false) + }) + + it('reads a stored `true` back from a parsed document (the solid-ui pattern)', () => { + // Serialized pods hold `true` (rdflib's own serializer emits it), which + // no longer parses back to the `"1"` that `value === '1'` checks expect. + const kb = DataFactory.graph() + parse(`<#s> <#p> true . <#s> <#q> false .`, kb, base, 'text/turtle') + const s = kb.sym(`${base}#s`) + expect(kb.anyValue(s, kb.sym(`${base}#p`))).to.equal('true') // lexical form preserved… + expect(isTrue(kb.any(s, kb.sym(`${base}#p`)))).to.equal(true) // …but value space reads fine + expect(isTrue(kb.any(s, kb.sym(`${base}#q`)))).to.equal(false) + expect(isTrue(kb.any(s, kb.sym(`${base}#missing`)))).to.equal(false) + }) + }) + + describe('literalToNumber', () => { + it('reads integers', () => { + expect(literalToNumber(typedLit('12', 'integer'))).to.equal(12) + expect(literalToNumber(typedLit('+5', 'integer'))).to.equal(5) + expect(literalToNumber(typedLit('-7', 'integer'))).to.equal(-7) + expect(literalToNumber(typedLit('012', 'integer'))).to.equal(12) + }) + + it('reads decimals', () => { + expect(literalToNumber(typedLit('12.0', 'decimal'))).to.equal(12) + expect(literalToNumber(typedLit('3.14', 'decimal'))).to.equal(3.14) + expect(literalToNumber(typedLit('.5', 'decimal'))).to.equal(0.5) + expect(literalToNumber(typedLit('-0.25', 'decimal'))).to.equal(-0.25) + }) + + it('reads doubles and floats, including the specials', () => { + expect(literalToNumber(typedLit('3.141e0', 'double'))).to.equal(3.141) + expect(literalToNumber(typedLit('1E3', 'double'))).to.equal(1000) + expect(literalToNumber(typedLit('2.5', 'float'))).to.equal(2.5) + expect(literalToNumber(typedLit('INF', 'double'))).to.equal(Infinity) + expect(literalToNumber(typedLit('-INF', 'double'))).to.equal(-Infinity) + expect(Number.isNaN(literalToNumber(typedLit('NaN', 'double')))).to.equal(true) + }) + + it('reads the derived integer types', () => { + expect(literalToNumber(typedLit('42', 'int'))).to.equal(42) + expect(literalToNumber(typedLit('42', 'long'))).to.equal(42) + expect(literalToNumber(typedLit('42', 'nonNegativeInteger'))).to.equal(42) + }) + + it('returns undefined for lexical forms outside the datatype', () => { + expect(literalToNumber(typedLit('abc', 'integer'))).to.equal(undefined) + expect(literalToNumber(typedLit('1.5', 'integer'))).to.equal(undefined) + expect(literalToNumber(typedLit('1e3', 'decimal'))).to.equal(undefined) // no exponent in xsd:decimal + expect(literalToNumber(typedLit('INF', 'decimal'))).to.equal(undefined) + expect(literalToNumber(typedLit('', 'integer'))).to.equal(undefined) + expect(literalToNumber(typedLit('0x10', 'integer'))).to.equal(undefined) + }) + + it('returns undefined for non-numeric datatypes, non-literals and missing terms', () => { + expect(literalToNumber(new Literal('12'))).to.equal(undefined) // xsd:string + expect(literalToNumber(boolLit('1'))).to.equal(undefined) + expect(literalToNumber(new NamedNode('https://example.org/12'))).to.equal(undefined) + expect(literalToNumber(null)).to.equal(undefined) + expect(literalToNumber(undefined)).to.equal(undefined) + }) + + it('compares parsed numerics in value space regardless of source spelling', () => { + const kb = DataFactory.graph() + parse(`<#s> <#a> 12 . <#s> <#b> 12.0 . <#s> <#c> 1.2e1 .`, kb, base, 'text/turtle') + const s = kb.sym(`${base}#s`) + for (const p of ['a', 'b', 'c']) { + expect(literalToNumber(kb.any(s, kb.sym(`${base}#${p}`)))).to.equal(12) + } + }) + }) + + describe('Literal.toBoolean / Literal.toNumber', () => { + it('expose the helpers as statics, mirroring fromBoolean/fromNumber', () => { + expect(Literal.toBoolean(boolLit('true'))).to.equal(true) + expect(Literal.toBoolean(boolLit('0'))).to.equal(false) + expect(Literal.toBoolean(null)).to.equal(undefined) + expect(Literal.toNumber(typedLit('12.0', 'decimal'))).to.equal(12) + expect(Literal.toNumber(Literal.fromNumber(3.5))).to.equal(3.5) + expect(Literal.toNumber(undefined)).to.equal(undefined) + }) + }) +}) diff --git a/tests/unit/parse-canonicalize-test.js b/tests/unit/parse-canonicalize-test.js new file mode 100644 index 000000000..249fa4467 --- /dev/null +++ b/tests/unit/parse-canonicalize-test.js @@ -0,0 +1,120 @@ +import { expect } from 'chai' + +import parse from '../../src/parse' +import Literal from '../../src/literal' +import DataFactory from '../../src/factories/rdflib-data-factory' + +const XSD = 'http://www.w3.org/2001/XMLSchema#' +const base = 'https://example.org/doc' + +/** + * The opt-in `canonicalize` parse option: a transition aid that restores the + * parse-time lexical normalisation rdflib ≤2 applied to boolean and numeric + * literals, for consumers that still compare literals by one canonical + * spelling. Off by default — lexical forms are preserved as found. + */ +describe('parse() `canonicalize` option', () => { + function parseDoc (content, options, contentType = 'text/turtle') { + const kb = DataFactory.graph() + parse(content, kb, base, contentType, options) + return kb + } + + function objectByDatatype (kb, localDatatype) { + const matches = kb.statements + .map((st) => st.object) + .filter((o) => o.termType === 'Literal' && o.datatype.value === XSD + localDatatype) + expect(matches).to.have.length(1) + return matches[0] + } + + const doc = `<#s> <#p> true, 12.0, 3.141e0 .` + + describe('off (the default): lexical forms are preserved', () => { + for (const options of [undefined, { canonicalize: false }]) { + it(`keeps the source spelling with options ${JSON.stringify(options)}`, () => { + const kb = parseDoc(doc, options) + expect(objectByDatatype(kb, 'boolean').value).to.equal('true') + expect(objectByDatatype(kb, 'decimal').value).to.equal('12.0') + expect(objectByDatatype(kb, 'double').value).to.equal('3.141e0') + }) + } + }) + + describe('on: booleans and numerics get the rdflib ≤2 canonical forms', () => { + it('canonicalizes `true`/`12.0`/`3.141e0` to `1`/`12`/`3.141`', () => { + const kb = parseDoc(doc, { canonicalize: true }) + expect(objectByDatatype(kb, 'boolean').value).to.equal('1') + expect(objectByDatatype(kb, 'decimal').value).to.equal('12') + expect(objectByDatatype(kb, 'double').value).to.equal('3.141') + }) + + it('makes canonically-constructed literals match parsed data again (store comparison)', () => { + const s = DataFactory.namedNode(`${base}#s`) + const p = DataFactory.namedNode(`${base}#p`) + + // Without the flag the 2.x-idiom lookups miss… + const plain = parseDoc(doc) + expect(plain.holds(s, p, Literal.fromBoolean(true))).to.equal(false) + expect(plain.holds(s, p, new Literal('12', null, DataFactory.namedNode(XSD + 'decimal')))).to.equal(false) + + // …with it they hit, as they did on rdflib ≤2 parses. + const canonical = parseDoc(doc, { canonicalize: true }) + expect(canonical.holds(s, p, Literal.fromBoolean(true))).to.equal(true) + expect(canonical.holds(s, p, new Literal('12', null, DataFactory.namedNode(XSD + 'decimal')))).to.equal(true) + expect(canonical.holds(s, p, new Literal('3.141', null, DataFactory.namedNode(XSD + 'double')))).to.equal(true) + }) + + it('normalises integer sign and leading-zero decoration exactly (no precision loss)', () => { + const kb = parseDoc(`<#s> <#a> +05 . <#s> <#b> -0 . <#s> <#c> 042 . + <#s> <#d> 12345678901234567890123 .`, { canonicalize: true }) + const value = (local) => kb.any(kb.sym(`${base}#s`), kb.sym(`${base}#${local}`)).value + expect(value('a')).to.equal('5') + expect(value('b')).to.equal('0') + expect(value('c')).to.equal('42') + expect(value('d')).to.equal('12345678901234567890123') // beyond 2^53: digits kept exact + }) + + it('also canonicalizes quoted typed literals', () => { + const kb = parseDoc(`<#s> <#p> "true"^^<${XSD}boolean>, "012"^^<${XSD}integer> .`, + { canonicalize: true }) + expect(objectByDatatype(kb, 'boolean').value).to.equal('1') + expect(objectByDatatype(kb, 'integer').value).to.equal('12') + }) + + it('leaves ill-typed lexical forms, the double specials and other datatypes alone', () => { + const kb = parseDoc(`<#s> <#p> "maybe"^^<${XSD}boolean>, "abc"^^<${XSD}integer>, + "INF"^^<${XSD}double>, "0.0000001"^^<${XSD}decimal>, "12.0", "true"@en, + "2020-01-01"^^<${XSD}date> .`, { canonicalize: true }) + const values = kb.statements.map((st) => st.object.value).sort() + expect(values).to.eql([ + '0.0000001', // String(Number) would be "1e-7", outside xsd:decimal's lexical space + '12.0', // plain string, not numeric + '2020-01-01', + 'INF', + 'abc', + 'maybe', + 'true', + ]) + }) + + it('applies across the Turtle family (text/n3)', () => { + const kb = parseDoc(`<#s> <#p> true, 12.0 .`, { canonicalize: true }, 'text/n3') + expect(objectByDatatype(kb, 'boolean').value).to.equal('1') + expect(objectByDatatype(kb, 'decimal').value).to.equal('12') + }) + + it('can be passed alongside a callback (6th positional argument)', (done) => { + const kb = DataFactory.graph() + parse(doc, kb, base, 'text/turtle', (error, resultKb) => { + try { + expect(error).to.equal(null) + expect(objectByDatatype(resultKb, 'boolean').value).to.equal('1') + done() + } catch (e) { + done(e) + } + }, { canonicalize: true }) + }) + }) +}) From 2b10b5be18492179c8879b11cb8fe6c371041c1c Mon Sep 17 00:00:00 2001 From: Jesse Wright <63333554+jeswr@users.noreply.github.com> Date: Sun, 5 Jul 2026 01:23:14 +0000 Subject: [PATCH 4/6] docs: annotate reference/ relics that build on the removed N3Parser Completes the M8 documentation mitigation for the N3.js migration: reference/{dumpParser,ldpatchParser,fetcher-classes}.js document the legacy hand-rolled parser's internals, which no longer exist. A README now marks the directory as archived reference material and points at parse()/N3.js. Co-Authored-By: Claude Fable 5 --- reference/README.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 reference/README.md diff --git a/reference/README.md b/reference/README.md new file mode 100644 index 000000000..bd87a503f --- /dev/null +++ b/reference/README.md @@ -0,0 +1,17 @@ +# reference/ — archived pre-TypeScript relics + +The files in this directory are **historical reference copies only**. They are +not part of the build, are not exported by the package, and are not expected +to run. + +In particular, `dumpParser.js`, `ldpatchParser.js` and `fetcher-classes.js` +build on the legacy hand-rolled `N3Parser`, which was **removed in rdflib 3.0** +(replaced by [N3.js](https://github.com/rdfjs/N3.js); see PR #831). The +`N3Parser` export is now a stub that throws with a pointer to `parse()`. +Anything here that constructs `$rdf.N3Parser(...)` or requires +`./n3parser` documents an API that no longer exists — use +`parse(text, store, baseURI, contentType)` or the +[`n3`](https://www.npmjs.com/package/n3) package directly instead. + +The `.coffee` files predate the 2015-era JavaScript port and are kept for +archaeology only. From 5a6bce9ed28d1f4a36c4124aa0890dd2da2e87a7 Mon Sep 17 00:00:00 2001 From: Jesse Wright <63333554+jeswr@users.noreply.github.com> Date: Sun, 5 Jul 2026 13:05:45 +0000 Subject: [PATCH 5/6] fix: deep-import n3 classes in the adapter to preserve bundle hygiene A root `import ... from 'n3'` in n3-adapter.ts pulled n3's package index into downstream browser bundles, dragging N3StreamWriter and its readable-stream/Node polyfill chain back in and undoing the #449 deep-import discipline. The adapter now deep-imports exactly what it uses (n3/lib/{N3Parser,N3Store,N3DataFactory}.js), and a source-level hygiene test fails any future root 'n3' import under src/. Verified on the built browser bundle: zero occurrences of N3StreamWriter/N3StreamParser/N3Writer/readable-stream in dist/rdflib.min.js; full suite green (382 passing). Co-Authored-By: Claude Fable 5 --- src/n3-adapter.ts | 10 +++++++- tests/unit/n3-import-hygiene-test.js | 38 ++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) create mode 100644 tests/unit/n3-import-hygiene-test.js diff --git a/src/n3-adapter.ts b/src/n3-adapter.ts index 1794d2c64..6b25cfa38 100644 --- a/src/n3-adapter.ts +++ b/src/n3-adapter.ts @@ -19,7 +19,15 @@ * `kb.sym(base)` — rdflib's provenance convention — while explicit named * graphs (TriG / N-Quads) are kept as-is. */ -import { Parser as N3jsParser, Store as N3jsStore, DataFactory as N3jsDataFactory } from 'n3' +// Deep imports of just the classes this adapter needs: a root `import 'n3'` +// drags N3StreamWriter (and its readable-stream/Node polyfill chain) into +// downstream browser bundles (#449). +// @ts-ignore no type declarations for the deep import +import N3jsParser from 'n3/lib/N3Parser.js' +// @ts-ignore no type declarations for the deep import +import N3jsStore from 'n3/lib/N3Store.js' +// @ts-ignore no type declarations for the deep import +import N3jsDataFactory from 'n3/lib/N3DataFactory.js' import Collection from './collection' import Formula from './formula' import Variable from './variable' diff --git a/tests/unit/n3-import-hygiene-test.js b/tests/unit/n3-import-hygiene-test.js new file mode 100644 index 000000000..f818e2f7d --- /dev/null +++ b/tests/unit/n3-import-hygiene-test.js @@ -0,0 +1,38 @@ +import { expect } from 'chai' +import * as fs from 'fs' +import * as path from 'path' + +// Guards the deep-import discipline for the n3 dependency (#449): a root +// `import ... from 'n3'` pulls in n3's package index, which drags +// N3StreamWriter (and its readable-stream/Node polyfill chain) into +// downstream browser bundles. Runtime code must deep-import the specific +// `n3/lib/.js` module it needs instead. +// +// This is the source-level guard; the bundle-level assertion (grepping the +// webpack output for N3StreamWriter/readable-stream) lives with the browser +// e2e job. + +const SRC_DIR = path.join(__dirname, '..', '..', 'src') +const ROOT_N3_IMPORT = /(?:from\s+(['"])n3\1|require\((['"])n3\2\))/ + +function sourceFiles (dir) { + return fs.readdirSync(dir, { withFileTypes: true }).flatMap(entry => { + const full = path.join(dir, entry.name) + if (entry.isDirectory()) return sourceFiles(full) + return /\.(ts|js)$/.test(entry.name) ? [full] : [] + }) +} + +describe('n3 import hygiene (#449)', () => { + it('no module under src/ imports the n3 package root', () => { + const offenders = sourceFiles(SRC_DIR) + .filter(file => ROOT_N3_IMPORT.test(fs.readFileSync(file, 'utf8'))) + .map(file => path.relative(SRC_DIR, file)) + + expect( + offenders, + `root 'n3' import found in: ${offenders.join(', ')} — ` + + "deep-import the class instead (e.g. `import N3jsParser from 'n3/lib/N3Parser.js'`)" + ).to.deep.equal([]) + }) +}) From 0d1b7c1400885676500b116339146e527e6edda1 Mon Sep 17 00:00:00 2001 From: Jesse Wright <63333554+jeswr@users.noreply.github.com> Date: Sun, 5 Jul 2026 22:43:49 +0000 Subject: [PATCH 6/6] style: shorten comments and normalize punctuation to repo conventions Co-Authored-By: Claude Fable 5 --- src/fetcher.ts | 7 ++-- src/literal.ts | 9 +++-- src/n3-adapter.ts | 46 ++++++++++++-------------- src/parse.ts | 19 +++++------ src/patch-parser.js | 6 ++-- src/serializer.js | 5 ++- src/utils/literalValue.ts | 27 ++++++--------- tests/unit/fetcher-test.js | 2 -- tests/unit/indexed-formula-test.js | 22 +++++------- tests/unit/literal-value-test.js | 15 ++++----- tests/unit/n3-import-hygiene-test.js | 2 +- tests/unit/parse-canonicalize-test.js | 10 +++--- tests/unit/parse-turtle-family-test.js | 9 ++--- tests/unit/patch-parser-test.js | 8 ++--- 14 files changed, 81 insertions(+), 106 deletions(-) diff --git a/src/fetcher.ts b/src/fetcher.ts index e21ac51d3..e40bbabe6 100644 --- a/src/fetcher.ts +++ b/src/fetcher.ts @@ -972,10 +972,9 @@ export default class Fetcher implements CallbackifyInterface { options: Options = {} ): T extends Array ? Promise : Promise { options = Object.assign({}, options) // Take a copy as we add stuff to the options!! - // `force` implies `clearPreviousData` (unless the caller explicitly opts - // out): parsed blank-node labels are not stable across parses, so - // re-parsing a previously loaded document without clearing it first would - // duplicate its blank-node subgraphs on every forced reload. + // `force` implies `clearPreviousData` unless the caller opts out: + // blank-node labels are not stable across parses, so re-parsing without + // clearing would duplicate blank-node subgraphs if (options.force && options.clearPreviousData === undefined) { options.clearPreviousData = true } diff --git a/src/literal.ts b/src/literal.ts index 6425fcbc8..f1c9cde2d 100644 --- a/src/literal.ts +++ b/src/literal.ts @@ -165,14 +165,13 @@ export default class Literal extends Node implements TFLiteral { } /** - * Reads an `xsd:boolean` literal in value space — the inverse of + * Reads an `xsd:boolean` literal in value space, the inverse of * {@link fromBoolean}, accepting every valid lexical form (`"true"`, * `"1"`, `"false"`, `"0"`). Returns `undefined` for non-literals, * other datatypes and ill-typed lexical forms. * - * The parser preserves source lexical forms, so a stored `true` no longer - * reads back as `"1"`; compare booleans with this (or {@link isTrue}) - * rather than against one spelling of `term.value`. + * The parser preserves source lexical forms; compare booleans with this + * (or {@link isTrue}) rather than against one spelling of `term.value`. * @param term - The term to read; may be `null`/`undefined` */ static toBoolean (term: Term | null | undefined): boolean | undefined { @@ -181,7 +180,7 @@ export default class Literal extends Node implements TFLiteral { /** * Reads a numeric (`xsd:decimal`/integer-family/floating-point) literal in - * value space — the inverse of {@link fromNumber}, accepting every valid + * value space, the inverse of {@link fromNumber}, accepting every valid * lexical form (`"12"`, `"12.0"`, `"1.2e1"` all read as `12`). Returns * `undefined` for non-literals, non-numeric datatypes and ill-typed * lexical forms. diff --git a/src/n3-adapter.ts b/src/n3-adapter.ts index 6b25cfa38..20283d43e 100644 --- a/src/n3-adapter.ts +++ b/src/n3-adapter.ts @@ -2,21 +2,21 @@ * Adapter between the N3.js parser and rdflib's data model. * * All Turtle-family content types (Turtle, TriG, N-Triples, N-Quads and full - * Notation3) are parsed by the spec-compliant, optimised N3.js parser; this - * module maps its flat quad stream back onto rdflib's richer model: + * Notation3) are parsed by N3.js; this module maps its flat quad stream back + * onto rdflib's richer model: * - * - `( … )` collections are folded into rdflib `Collection` terms using + * - `( ... )` collections are folded into rdflib `Collection` terms using * N3.js's own list machinery (`Store#extractLists`); - * - N3 formulae `{ … }` (which N3.js emits as quads whose graph is a fresh + * - N3 formulae `{ ... }` (which N3.js emits as quads whose graph is a fresh * blank node) are rebuilt into rdflib `Formula` sub-stores; * - `@forAll` / `@forSome` declarations (reified by N3.js under * `explicitQuantifiers`) are registered through `newUniversal` / * `declareExistential`, as the legacy parser did; * - `?x` becomes a `Variable`, `=` becomes `owl:sameAs`, `=>`/`<=` become - * (reversed) `log:implies` — all handled natively by N3.js's n3 mode. + * (reversed) `log:implies`, all handled natively by N3.js's n3 mode. * * Statements in the default graph are attributed to the document graph - * `kb.sym(base)` — rdflib's provenance convention — while explicit named + * `kb.sym(base)` (rdflib's provenance convention) while explicit named * graphs (TriG / N-Quads) are kept as-is. */ // Deep imports of just the classes this adapter needs: a root `import 'n3'` @@ -74,7 +74,7 @@ export const N3JS_FORMATS: { [contentType: string]: string } = { export type ParseN3jsOptions = { /** * Canonicalize the lexical forms of boolean and numeric literals at parse - * time, as rdflib ≤2 did — see the `canonicalize` option of `parse()`. + * time, as rdflib <= 2 did; see the `canonicalize` option of `parse()`. */ canonicalize?: boolean } @@ -98,14 +98,10 @@ export default function parseN3js (str: string, kb: Formula, base: string, conte try { return runParse(str, kb, base, format, false, options) } catch (e) { - // rdflib's legacy parser implicitly bound the empty prefix `:` to - // ``, so documents using `:name` without declaring `@prefix :` - // parsed anyway. N3.js is strict, so when (and only when) it reports an - // undefined empty prefix in one of the syntaxes the legacy parser - // accepted, re-parse once with a synthetic `@prefix : .` seeded. - // Because the declaration is prepended, a later in-document declaration - // still overrides it from that point on, exactly like the sequential - // semantics of the legacy parser. + // rdflib implicitly binds the empty prefix `:` to ``; N3.js is + // strict, so on (and only on) an undefined-empty-prefix error re-parse + // once with a synthetic `@prefix : .` seeded. Being prepended, a + // later in-document declaration still overrides it from that point on. if (sugared && base && /Undefined prefix ":"/.test(String(e && (e as Error).message))) { return runParse(str, kb, base, format, true, options) } @@ -121,14 +117,14 @@ function runParse (str: string, kb: Formula, base: string, format: string, seedE const foldLists = (n3Mode || format === 'text/turtle') && !!(rdfFactory && rdfFactory.supports && rdfFactory.supports['COLLECTIONS']) - // --- 1. Parse (synchronously — N3.js throws on error in this mode) ------- + // --- 1. Parse (synchronous: N3.js throws on error in this mode) ------- const prefixes: { [prefix: string]: string } = {} let sawSyntheticEmptyPrefix = false const input = seedEmptyPrefix ? '@prefix : <' + escapeIri(base) + '#>.\n' + str : str // In n3 mode, N3.js mis-scopes a document-labelled blank node (`_:c`) - // mentioned inside a `[ … ]` property list: it gets the label + // mentioned inside a `[ ... ]` property list: it gets the label // `.c` instead of the document-wide label. At the top - // level the graph label is empty, so such labels start with "." — we pass an + // level the graph label is empty, so such labels start with "."; we pass an // explicit per-parse prefix so they can be renamed back to the document // label (inside a formula the formula label is used, which is the correct // formula-wide scoping, so those are left alone). @@ -165,7 +161,7 @@ function runParse (str: string, kb: Formula, base: string, format: string, seedE // --- 3. List and quantifier work, via N3.js's own store machinery -------- // Only build the intermediate N3.Store when there is work for it: full N3 // (formulae/quantifiers possible), or a document that actually mentions - // rdf:nil (every complete list ends in one — same gate the legacy parser + // rdf:nil (every complete list ends in one, the same gate the legacy parser // used for its list folding). const listItems: { [head: string]: any[] } = {} const quantifiers: Array<{ scopeLabel: string | null, forAll: boolean, vars: string[] }> = [] @@ -185,7 +181,7 @@ function runParse (str: string, kb: Formula, base: string, format: string, seedE isLive = (q: any): boolean => n3Store.has(q) if (n3Mode) { - // Under explicitQuantifiers, each `@forAll x, y` / `@forSome …` becomes + // Under explicitQuantifiers, each `@forAll x, y` / `@forSome ...` becomes // reify:forAll ( x y ) in graph // where is the default graph or the enclosing formula's label. const quantifierQuads = n3Store.getQuads(null, null, null, N3jsDataFactory.namedNode(QUANTIFIERS_GRAPH)) @@ -308,7 +304,7 @@ function runParse (str: string, kb: Formula, base: string, format: string, seedE // --- 6. Load the statements ---------------------------------------------- // Statements whose graph is a formula's blank node go into that Formula; - // everything else goes into kb (default graph → the document graph, + // everything else goes into kb (default graph mapped to the document graph, // TriG/N-Quads named graphs kept as-is). Inner formula statements carry the // document as their `why`, exactly like the legacy parser. let count = 0 @@ -354,12 +350,12 @@ const DECIMAL_LEXICAL = /^[+-]?(?:[0-9]+(?:\.[0-9]*)?|\.[0-9]+)$/ const FLOATING_LEXICAL = /^[+-]?(?:[0-9]+(?:\.[0-9]*)?|\.[0-9]+)(?:[eE][+-]?[0-9]+)?$/ /** - * Map a boolean or numeric lexical form to the canonical form rdflib ≤2 + * Map a boolean or numeric lexical form to the canonical form rdflib <= 2 * produced at parse time (the `canonicalize: true` compatibility mode): * booleans become `"1"`/`"0"` (matching `Literal.fromBoolean`), integers * lose their sign/leading-zero decoration, and decimals/doubles/floats are - * rewritten as JavaScript stringifies their numeric value (`12.0` → `"12"`, - * `3.141e0` → `"3.141"`), exactly as the legacy parsers' number round-trip + * rewritten as JavaScript stringifies their numeric value (`12.0` -> `"12"`, + * `3.141e0` -> `"3.141"`), exactly as the legacy parsers' number round-trip * did. Only valid lexical forms are rewritten; anything ill-typed (and any * other datatype) is preserved as-is. */ @@ -380,7 +376,7 @@ function canonicalLexicalForm (value: string, datatype: string): string { if (!DECIMAL_LEXICAL.test(value)) return value const canonical = String(Number(value)) // Guard: keep the source form when JS would stringify with an exponent - // (e.g. 0.0000001 → "1e-7"), which is outside xsd:decimal's lexical space. + // (e.g. 0.0000001 -> "1e-7"), which is outside xsd:decimal's lexical space. return DECIMAL_LEXICAL.test(canonical) ? canonical : value } case XSD_DOUBLE: diff --git a/src/parse.ts b/src/parse.ts index fa35467e9..0115205a0 100644 --- a/src/parse.ts +++ b/src/parse.ts @@ -14,21 +14,20 @@ type CallbackFunc = (error: any, kb: Formula | null) => void export type ParseOptions = { /** * Canonicalize the lexical forms of boolean and numeric literals at parse - * time, the way rdflib ≤2's own parsers did: `true`/`false` become + * time, the way rdflib <= 2's own parsers did: `true`/`false` become * `"1"`/`"0"` (matching `Literal.fromBoolean`), `12.0` becomes `"12"`, * `3.141e0` becomes `"3.141"`, `+05` becomes `"5"`. Covers `xsd:boolean`, * `xsd:integer`, `xsd:decimal`, `xsd:double` and `xsd:float`; ill-typed * lexical forms and all other datatypes are preserved as-is. Applies to * the Turtle-family content types (Turtle, N3, TriG, N-Triples, N-Quads). * - * Off by default: since the migration to the N3.js parser, literals keep - * the exact lexical form found in the document, as the RDF specs - * prescribe. This flag is a transition aid for code that still compares - * literals by one canonical spelling (`term.value === '1'`, - * `kb.holds(s, p, Literal.fromBoolean(true))`); new code should keep the - * default and compare in value space instead — see `isTrue`, - * `literalToBoolean` and `literalToNumber` (also available as - * `Literal.toBoolean` / `Literal.toNumber`). + * Off by default: literals keep the exact lexical form found in the + * document, as the RDF specs prescribe. This flag is a transition aid for + * code that still compares literals by one canonical spelling + * (`term.value === '1'`); new code should keep the default and compare in + * value space instead, via `isTrue`, `literalToBoolean` and + * `literalToNumber` (also available as `Literal.toBoolean` / + * `Literal.toNumber`). */ canonicalize?: boolean } @@ -62,7 +61,7 @@ export default function parse ( contentType = contentType.split(';')[0] as ContentType try { if (Object.prototype.hasOwnProperty.call(N3JS_FORMATS, contentType)) { - // The Turtle family — Turtle, N3, TriG, N-Triples and N-Quads — is + // The Turtle family (Turtle, N3, TriG, N-Triples and N-Quads) is // parsed by the N3.js parser, adapted onto rdflib's model. parseN3js(str, kb, base, contentType, options) executeCallback() diff --git a/src/patch-parser.js b/src/patch-parser.js index cf9f586ae..efe6cb46f 100644 --- a/src/patch-parser.js +++ b/src/patch-parser.js @@ -90,7 +90,7 @@ export default function sparqlUpdateParser (str, kb, base) { found = true } if (!found && str.slice(i, i + 7) === 'PREFIX ') { - // SPARQL-style PREFIX declaration (no trailing dot) — see #651. + // SPARQL-style PREFIX declaration, with no trailing dot (#651) let j = str.indexOf('<', i) if (j >= 0) j = str.indexOf('>', j) if (j < 0) { @@ -135,7 +135,7 @@ function skipSpace (str, i) { } /** - * Skip a balanced `{ … }` clause starting at the opening brace, ignoring + * Skip a balanced `{ ... }` clause starting at the opening brace, ignoring * braces inside string literals, IRIs and comments. * Returns the index just past the matching `}`, or -1 on EOF. */ @@ -164,7 +164,7 @@ function skipClause (str, i) { return -1 } -/** Skip a short or long ("""…""") string literal; -1 on EOF. */ +/** Skip a short or long ("""...""") string literal; -1 on EOF. */ function skipStringLiteral (str, i) { const quote = str[i] const delim = str.slice(i, i + 3) === quote + quote + quote ? quote + quote + quote : quote diff --git a/src/serializer.js b/src/serializer.js index 6d19e7308..c6e67375b 100644 --- a/src/serializer.js +++ b/src/serializer.js @@ -577,9 +577,8 @@ export class Serializer { } case 'http://www.w3.org/2001/XMLSchema#boolean': - // The XSD lexical space is {'true', 'false', '1', '0'}; the - // legacy parser normalised to '1'/'0', the N3.js parser - // preserves the source form. + // The XSD lexical space is {'true', 'false', '1', '0'} and the + // parser preserves the source form return (expr.value === '1' || expr.value === 'true') ? 'true' : 'false' } } diff --git a/src/utils/literalValue.ts b/src/utils/literalValue.ts index 7c92b92e6..6fce129af 100644 --- a/src/utils/literalValue.ts +++ b/src/utils/literalValue.ts @@ -1,23 +1,18 @@ /** * Value-space helpers for reading typed literals. * - * Since the migration to the N3.js parser, the Turtle-family parsers preserve - * the source lexical form of literals: `true` is kept as - * `"true"^^xsd:boolean` (it is no longer normalised to `"1"`), `12.0` stays - * `"12.0"^^xsd:decimal` (no longer `"12"`), `3.141e0` stays `"3.141e0"`. - * Code that compares `term.value` against a single canonical spelling — - * `term.value === '1'`, `kb.holds(s, p, Literal.fromBoolean(true))` — breaks - * silently on data whose author spelled the same value differently (and - * rdflib's own serializer emits `true`, so stored data usually has the - * non-`"1"` spelling). + * The Turtle-family parsers preserve the source lexical form of literals, + * so code comparing `term.value` against a single canonical spelling + * (`term.value === '1'`, `kb.holds(s, p, Literal.fromBoolean(true))`) + * breaks silently on data that spells the same value differently. * - * These helpers compare in *value space* instead: they accept every lexical + * These helpers compare in value space instead: they accept every lexical * form the datatype's lexical space allows and return the datatype's value, * so `"true"`, `"1"`, `" true "` all read as `true`, and `"12"`, `"12.0"`, * `"1.2e1"` all read as `12`. * * See also the `canonicalize` option of `parse()` for a transitional - * alternative that restores the old parse-time normalisation instead. + * alternative that restores parse-time normalisation instead. */ import { Term, Literal as TFLiteral } from '../tf-types' @@ -70,7 +65,7 @@ function asLiteral (term: Term | null | undefined): TFLiteral | null { * * Returns `true` for the lexical forms `"true"` and `"1"`, `false` for * `"false"` and `"0"` (leading/trailing whitespace is ignored, per XSD's - * whitespace collapse), and `undefined` for everything else — a missing + * whitespace collapse), and `undefined` for everything else: a missing * term, a non-literal, a literal of another datatype, or an ill-typed * lexical form. * @@ -104,7 +99,7 @@ export function literalToBoolean (term: Term | null | undefined): boolean | unde /** * Whether the given term is an `xsd:boolean` literal that is true in value - * space — i.e. its lexical form is `"true"` or `"1"`. + * space, i.e. its lexical form is `"true"` or `"1"`. * * This is the drop-in, value-space replacement for the pre-3.0 pattern * `kb.anyValue(s, p) === '1'`, which relied on the old parsers normalising @@ -112,8 +107,8 @@ export function literalToBoolean (term: Term | null | undefined): boolean | unde * ```js * const enabled = isTrue(kb.any(subject, predicate, null, doc)) * ``` - * Anything that is not a true boolean literal — a false one, an ill-typed - * one, a non-literal, `null`, `undefined` — yields `false`. + * Anything that is not a true boolean literal (a false one, an ill-typed + * one, a non-literal, `null`, `undefined`) yields `false`. */ export function isTrue (term: Term | null | undefined): boolean { return literalToBoolean(term) === true @@ -125,7 +120,7 @@ export function isTrue (term: Term | null | undefined): boolean { * `12` no matter how the source document spelled them. * * Handles `xsd:decimal`, the derived integer types (`xsd:integer`, - * `xsd:long`, `xsd:int`, …) and the floating-point types (`xsd:double`, + * `xsd:long`, `xsd:int`, ...) and the floating-point types (`xsd:double`, * `xsd:float`, including their `INF`/`-INF`/`NaN` specials). Returns * `undefined` for a missing term, a non-literal, a literal of a * non-numeric datatype, or a lexical form outside the datatype's lexical diff --git a/tests/unit/fetcher-test.js b/tests/unit/fetcher-test.js index 67326e04c..d5af6a7f0 100644 --- a/tests/unit/fetcher-test.js +++ b/tests/unit/fetcher-test.js @@ -611,8 +611,6 @@ describe('Fetcher', () => { }) it('should load and parse N3 with formula subjects (#567)', () => { - // Loading a document like https://drive.verborgh.org/tmp/2022/cha-58-direct.n3 - // used to fail with "Subject is not a subject type". let testN3 = `@prefix : . { :a :b :c } => { :d :e :f }.` diff --git a/tests/unit/indexed-formula-test.js b/tests/unit/indexed-formula-test.js index 9bb7c209e..d3a636b38 100644 --- a/tests/unit/indexed-formula-test.js +++ b/tests/unit/indexed-formula-test.js @@ -355,13 +355,11 @@ describe('IndexedFormula', () => { }) }) describe('removeDocument', () => { - // Use a collection-supporting store (the default in normal rdflib usage via - // DataFactory.graph()). The N3.js-based parser correctly attributes the - // status list's rdf:first/rest triples to the document graph, so — unlike a - // bare IndexedFormula() whose CanonicalDataFactory cannot fold them into a - // Collection — the list is stored as a single Collection term that - // removeMetadata/removeDocument clean up. (The legacy parser only "passed" - // here by misfiling those list triples into the default graph.) + // Use a collection-supporting store (the default in normal rdflib usage + // via DataFactory.graph()): the status list's rdf:first/rest triples are + // folded into a single Collection term that removeMetadata/removeDocument + // clean up, unlike a bare IndexedFormula() whose CanonicalDataFactory + // cannot fold them. const store = DataFactory.graph() const meta = store.sym('chrome://TheCurrentSession') const prefixes = `@prefix : <#>. @@ -434,12 +432,10 @@ describe('IndexedFormula', () => { }) it ('removeMetadata on a store without collection support leaves the raw status-list triples (pre-existing #631 gap, now visible)', () => { // With a CanonicalDataFactory store the status list cannot be folded - // into a Collection, so it stays as raw rdf:first/rest triples that are - // now correctly attributed to the metadata graph (the legacy parser - // misfiled them into the default graph, which made the old version of - // this test pass vacuously). removeMetadata only knows how to remove a - // status Collection, so those triples survive — documented here until - // #631 is fixed. + // into a Collection; it stays as raw rdf:first/rest triples in the + // metadata graph, and removeMetadata only knows how to remove a status + // Collection, so those triples survive. Documented here until #631 is + // fixed. const rawStore = new IndexedFormula() parse(metaContent, rawStore, meta.value, 'text/turtle') rawStore.removeMetadata(rawStore.sym('https://bob.localhost:8443/profile/card')) diff --git a/tests/unit/literal-value-test.js b/tests/unit/literal-value-test.js index 4b00e8fad..08d1f81e1 100644 --- a/tests/unit/literal-value-test.js +++ b/tests/unit/literal-value-test.js @@ -14,10 +14,9 @@ const boolLit = (lexical) => new Literal(lexical, null, XSD.boolean) const typedLit = (lexical, localName) => new Literal(lexical, null, new NamedNode(XSDNS + localName)) /** - * Value-space reads of typed literals. The N3.js-based parsers preserve the - * source lexical form (a stored `true` is no longer normalised to `"1"`), so - * consumers must compare boolean/numeric literals in value space — these - * helpers are the supported way to do that. + * Value-space reads of typed literals. The parsers preserve the source + * lexical form, so consumers must compare boolean/numeric literals in + * value space; these helpers are the supported way to do that. */ describe('value-space literal helpers', () => { describe('literalToBoolean', () => { @@ -74,13 +73,13 @@ describe('value-space literal helpers', () => { }) it('reads a stored `true` back from a parsed document (the solid-ui pattern)', () => { - // Serialized pods hold `true` (rdflib's own serializer emits it), which - // no longer parses back to the `"1"` that `value === '1'` checks expect. + // Serialized pods hold `true` (rdflib's own serializer emits it), + // which parses back as `"true"`, not the `"1"` old checks expect const kb = DataFactory.graph() parse(`<#s> <#p> true . <#s> <#q> false .`, kb, base, 'text/turtle') const s = kb.sym(`${base}#s`) - expect(kb.anyValue(s, kb.sym(`${base}#p`))).to.equal('true') // lexical form preserved… - expect(isTrue(kb.any(s, kb.sym(`${base}#p`)))).to.equal(true) // …but value space reads fine + expect(kb.anyValue(s, kb.sym(`${base}#p`))).to.equal('true') // lexical form preserved... + expect(isTrue(kb.any(s, kb.sym(`${base}#p`)))).to.equal(true) // ...but value space reads fine expect(isTrue(kb.any(s, kb.sym(`${base}#q`)))).to.equal(false) expect(isTrue(kb.any(s, kb.sym(`${base}#missing`)))).to.equal(false) }) diff --git a/tests/unit/n3-import-hygiene-test.js b/tests/unit/n3-import-hygiene-test.js index f818e2f7d..eef55b6ee 100644 --- a/tests/unit/n3-import-hygiene-test.js +++ b/tests/unit/n3-import-hygiene-test.js @@ -31,7 +31,7 @@ describe('n3 import hygiene (#449)', () => { expect( offenders, - `root 'n3' import found in: ${offenders.join(', ')} — ` + + `root 'n3' import found in: ${offenders.join(', ')}; ` + "deep-import the class instead (e.g. `import N3jsParser from 'n3/lib/N3Parser.js'`)" ).to.deep.equal([]) }) diff --git a/tests/unit/parse-canonicalize-test.js b/tests/unit/parse-canonicalize-test.js index 249fa4467..82b0d8bdc 100644 --- a/tests/unit/parse-canonicalize-test.js +++ b/tests/unit/parse-canonicalize-test.js @@ -9,9 +9,9 @@ const base = 'https://example.org/doc' /** * The opt-in `canonicalize` parse option: a transition aid that restores the - * parse-time lexical normalisation rdflib ≤2 applied to boolean and numeric + * parse-time lexical normalisation rdflib <= 2 applied to boolean and numeric * literals, for consumers that still compare literals by one canonical - * spelling. Off by default — lexical forms are preserved as found. + * spelling. Off by default: lexical forms are preserved as found. */ describe('parse() `canonicalize` option', () => { function parseDoc (content, options, contentType = 'text/turtle') { @@ -41,7 +41,7 @@ describe('parse() `canonicalize` option', () => { } }) - describe('on: booleans and numerics get the rdflib ≤2 canonical forms', () => { + describe('on: booleans and numerics get the rdflib <= 2 canonical forms', () => { it('canonicalizes `true`/`12.0`/`3.141e0` to `1`/`12`/`3.141`', () => { const kb = parseDoc(doc, { canonicalize: true }) expect(objectByDatatype(kb, 'boolean').value).to.equal('1') @@ -53,12 +53,12 @@ describe('parse() `canonicalize` option', () => { const s = DataFactory.namedNode(`${base}#s`) const p = DataFactory.namedNode(`${base}#p`) - // Without the flag the 2.x-idiom lookups miss… + // Without the flag the 2.x-idiom lookups miss... const plain = parseDoc(doc) expect(plain.holds(s, p, Literal.fromBoolean(true))).to.equal(false) expect(plain.holds(s, p, new Literal('12', null, DataFactory.namedNode(XSD + 'decimal')))).to.equal(false) - // …with it they hit, as they did on rdflib ≤2 parses. + // ...with it they hit, as they did on rdflib <= 2 parses. const canonical = parseDoc(doc, { canonicalize: true }) expect(canonical.holds(s, p, Literal.fromBoolean(true))).to.equal(true) expect(canonical.holds(s, p, new Literal('12', null, DataFactory.namedNode(XSD + 'decimal')))).to.equal(true) diff --git a/tests/unit/parse-turtle-family-test.js b/tests/unit/parse-turtle-family-test.js index b8a5cdb7e..29b4747f9 100644 --- a/tests/unit/parse-turtle-family-test.js +++ b/tests/unit/parse-turtle-family-test.js @@ -10,11 +10,8 @@ const XSD = 'http://www.w3.org/2001/XMLSchema#' /** * Tests for the Turtle-family formats parsed by the N3.js parser * (text/turtle, text/n3, application/n-triples, application/n-quads, - * application/trig). - * - * These cover spec-conformance cases that rdflib's historical hand-rolled - * parser did not handle, plus the Notation3 model mapping (formulae, - * variables, quantifiers). See the "Closes #…" list on the migration PR. + * application/trig): spec-conformance cases plus the Notation3 model + * mapping (formulae, variables, quantifiers). */ describe('Turtle-family parsing via N3.js', () => { const base = 'https://example.org/doc' @@ -230,7 +227,7 @@ describe('Turtle-family parsing via N3.js', () => { it('keeps document-labelled blank nodes whole across [ … ] property lists', () => { // N3.js's n3 mode mis-scopes `_:c` when it is mentioned inside a - // [ … ] property list (it applies the graph label as a prefix); the + // [ ... ] property list (it applies the graph label as a prefix); the // adapter repairs the label so both mentions are the same node. const store = parseN3(`@prefix : <#>. _:c :p 1. [ :q _:c ] :r 2.`) const direct = store.statements.find(s => s.predicate.value === `${base}#p`).subject diff --git a/tests/unit/patch-parser-test.js b/tests/unit/patch-parser-test.js index 3146852cc..27cda60de 100644 --- a/tests/unit/patch-parser-test.js +++ b/tests/unit/patch-parser-test.js @@ -104,11 +104,9 @@ describe('sparqlUpdateParser', () => { }) it('resolves the clauses when the base URI carries a fragment', () => { - // Regression: the query node is written as <#query> in the generated N3, - // which RFC 3986 resolves by *replacing* any fragment on the base — but - // the lookup sym used to be built as `base + '#query'`. With a - // fragment-bearing base the two silently diverged and the returned patch - // lost its insert/delete/where clauses. + // The query node is written as <#query> in the generated N3, which + // RFC 3986 resolves by replacing any fragment on the base; the lookup + // sym must resolve the same way or the clauses are silently lost const store = new IndexedFormula() const result = sparqlUpdateParser( `DELETE { <#me> "old". }