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/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. diff --git a/src/fetcher.ts b/src/fetcher.ts index 91792b3cd..e40bbabe6 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' @@ -155,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 /** @@ -592,20 +593,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) @@ -939,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`). @@ -964,6 +972,12 @@ 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 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 + } 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/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..15e388b07 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' @@ -60,8 +59,22 @@ 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 { 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/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/literal.ts b/src/literal.ts index b67862b5d..f1c9cde2d 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,32 @@ 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; 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 new file mode 100644 index 000000000..20283d43e --- /dev/null +++ b/src/n3-adapter.ts @@ -0,0 +1,398 @@ +/** + * 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 N3.js; 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. + */ +// 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' +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', +} + +/** 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. + * + * @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} + * @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, 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, options) + } catch (e) { + // 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) + } + throw e + } +} + +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() + const foldLists = (n3Mode || format === 'text/turtle') && + !!(rdfFactory && rdfFactory.supports && rdfFactory.supports['COLLECTIONS']) + + // --- 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 + // `.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, 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[] }> = [] + 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': + 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': + 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 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 + 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 +} + +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 + 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..0115205a0 100644 --- a/src/parse.ts +++ b/src/parse.ts @@ -1,19 +1,37 @@ -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 +/** 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: 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 +} + /** * Parse a string and put the result into the graph kb. * Normal method is sync. @@ -23,21 +41,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 (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, options) executeCallback() } else if (contentType === RDFXMLContentType) { var parser = new RDFParser(kb) @@ -59,10 +85,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 +97,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,12 +107,14 @@ 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 () { - if (callback) { - callback(null, kb) + if (cb) { + cb(null, kb) } else { return } @@ -103,8 +129,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 @@ -113,33 +139,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..efe6cb46f 100644 --- a/src/patch-parser.js +++ b/src/patch-parser.js @@ -1,94 +1,192 @@ -// 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 = {} + // 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. - 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) + } + // 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 - 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, with no trailing dot (#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..c6e67375b 100644 --- a/src/serializer.js +++ b/src/serializer.js @@ -577,7 +577,9 @@ 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'} and the + // 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/src/utils/literalValue.ts b/src/utils/literalValue.ts new file mode 100644 index 000000000..6fce129af --- /dev/null +++ b/src/utils/literalValue.ts @@ -0,0 +1,156 @@ +/** + * Value-space helpers for reading typed literals. + * + * 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 + * 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 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/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/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/fetcher-test.js b/tests/unit/fetcher-test.js index 3700d29a7..d5af6a7f0 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,75 @@ describe('Fetcher', () => { expect(match.object.value).to.equal('http://example.com/foo/vocab#building4') }) }) + + 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)', () => { + 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..d3a636b38 100644 --- a/tests/unit/indexed-formula-test.js +++ b/tests/unit/indexed-formula-test.js @@ -355,7 +355,12 @@ describe('IndexedFormula', () => { }) }) describe('removeDocument', () => { - const store = new IndexedFormula() + // 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 : <#>. @prefix http: . @@ -425,5 +430,20 @@ 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; 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')) + 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/literal-value-test.js b/tests/unit/literal-value-test.js new file mode 100644 index 000000000..08d1f81e1 --- /dev/null +++ b/tests/unit/literal-value-test.js @@ -0,0 +1,155 @@ +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 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', () => { + 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 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(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/n3-import-hygiene-test.js b/tests/unit/n3-import-hygiene-test.js new file mode 100644 index 000000000..eef55b6ee --- /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([]) + }) +}) 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/parse-canonicalize-test.js b/tests/unit/parse-canonicalize-test.js new file mode 100644 index 000000000..82b0d8bdc --- /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 }) + }) + }) +}) diff --git a/tests/unit/parse-turtle-family-test.js b/tests/unit/parse-turtle-family-test.js new file mode 100644 index 000000000..29b4747f9 --- /dev/null +++ b/tests/unit/parse-turtle-family-test.js @@ -0,0 +1,313 @@ +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): spec-conformance cases plus the Notation3 model + * mapping (formulae, variables, quantifiers). + */ +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..27cda60de 100644 --- a/tests/unit/patch-parser-test.js +++ b/tests/unit/patch-parser-test.js @@ -42,6 +42,107 @@ 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('resolves the clauses when the base URI carries a fragment', () => { + // 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". } + 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')) + .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