Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 16 additions & 5 deletions docs/data-model.md
Original file line number Diff line number Diff line change
Expand Up @@ -1736,11 +1736,15 @@ by one.
an IRI with a control character, a space or one of `` <>"{}|^`\ ``. So
is a blank node label or a language tag outside Turtle's grammar, a
language subtag over 8 characters, which the pod's parser refuses, text
typed `rdf:langString` without a language, a base direction, or a
quoted triple. JSON-LD can hold such a term, and the writers don't escape it,
so a `>` would end an IRI and the rest be read as statements no check
saw. A PATCH, a PUT, an insert-only PATCH, a release's Turtle and an
export are refused with `unwritableTerm` before anything is sent
typed `rdf:langString` or `rdf:dirLangString` without a language, or a
quoted triple. So is text typed `rdf:dirLangString` without a base
direction or with one other than `ltr` or `rtl`, and a direction on
any other literal. JSON-LD can hold such a term, and the writers
don't escape it, so a `>` would end an IRI and the rest be read as
statements no check saw. A JSON-LD value with a `@direction` but no
`@language` is such a term too. A PATCH, a PUT, an insert-only PATCH,
a release's Turtle and an export are refused with `unwritableTerm`
before anything is sent
([writableTerms.ts](../packages/solid/src/writableTerms.ts)). An
imported deck leaves such triples out; a release read from a file is
refused, as a draft keeps its triples as they are.
Expand All @@ -1764,6 +1768,13 @@ by one.
`sm:fromRelease`. A draft that states one is refused as one that
states a value otherwise than the Studio reads it
(`draftHiddenValue`), so it is never published with it.
A literal with a base direction (RDF 1.2, `"…"@ar--rtl`) that another
app wrote is kept with its language and direction, and written back
as it was read, in a PATCH and in a PUT alike (`graphsOf`). A Studio
draft keeps it too, as it is edited and published, and so does a
deck's export, in Turtle and in JSON-LD (`@direction`).
The app writes no direction of its own and reads no such literal as
text in a language.
- Mutations always follow read → modify → save
([datasets.ts](../packages/solid/src/datasets.ts): `readDataset` or
`getSolidDatasetOrNull`, then `saveDataset`). The save sends a PATCH
Expand Down
5 changes: 4 additions & 1 deletion docs/vendor-code.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,10 @@ type crosses upward past a mapper.
can name one. A subject made by name resolves against the
URL its dataset was read from. `getDatetime` throws on some literals
and gives an invalid date past the JavaScript date range. Language
tags read back in lower case. `getDecimal` reads as `parseFloat`
tags read back in lower case. A literal with a base direction
(`"…"@ar--rtl`) keeps its language and direction, is no text in a
language to `getStringByLocaleAll`, and is written back as read.
`getDecimal` reads as `parseFloat`
does, and a decimal is written as JavaScript spells it (`1`, not
`1.0`). The change log (`changesOf`) and the source (`sourceOf`)
behave as their comments in `rdf.ts` say. The resource module's pin
Expand Down
6 changes: 6 additions & 0 deletions packages/domain/src/release/releaseModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,12 @@ export interface ReleaseText {
/** The language tag, "" when untagged. */
language: string;
datatype: string;
/**
* Its base direction (RDF 1.2, `"…"@ar--rtl`), on text typed
* rdf:dirLangString that another app wrote; absent for any other. The
* app writes none of its own, and keeps one it read.
*/
direction?: string;
}

/** What every subject of the outline has: its IRI and whether it is retired (owl:deprecated true). */
Expand Down
11 changes: 11 additions & 0 deletions packages/shacl/node/quadsToReleaseModel.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -222,3 +222,14 @@ it("reads each kind of term", () => {
const [q] = parseTurtle(`<a> <b> _:x .`, URL);
expect(termOf(q.object)).toEqual({ kind: "blank", value: q.object.value });
});

it("reads a literal's base direction with it", () => {
const [q] = parseTurtle(`<a> <b> "x"@ar--rtl .`, URL);
expect(termOf(q.object)).toEqual({
kind: "literal",
value: "x",
language: "ar",
direction: "rtl",
datatype: "http://www.w3.org/1999/02/22-rdf-syntax-ns#dirLangString",
});
});
6 changes: 5 additions & 1 deletion packages/shacl/node/quadsToReleaseModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,17 @@ const OWL_DEPRECATED = "http://www.w3.org/2002/07/owl#deprecated";
/** A term as the release rules read it. */
export function termOf(object: Quad_Object): ReleaseTerm {
switch (object.termType) {
case "Literal":
case "Literal": {
// A base direction (RDF 1.2), which n3 reads but this package's RDF/JS types do not name.
const { direction } = object as { direction?: string };
return {
kind: "literal",
value: object.value,
language: object.language,
datatype: object.datatype.value,
...(direction ? { direction } : {}),
};
}
case "BlankNode":
return { kind: "blank", value: object.value };
default:
Expand Down
8 changes: 5 additions & 3 deletions packages/solid-memo-client/src/dataset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,18 +137,20 @@ function subjectId(term: Quad_Subject): string {
return term.termType === "BlankNode" ? `_:${term.value}` : term.value;
}

/** A value with its language tag in lower case, as every value read is held. */
/** A value with its language tag in lower case, as every value read is held, its base direction kept. */
function lowerCased(value: Value): Value {
if (value.termType !== "Literal" || value.language === value.language.toLowerCase()) {
return value;
}
return literal(value.value, value.language.toLowerCase());
// n3's factory reads a tag with a direction after `--` (`ar--rtl`) as the two apart.
const direction = value.direction ? `--${value.direction}` : "";
return literal(value.value, `${value.language.toLowerCase()}${direction}`);
}

/** A value's parts, each a step of the path that keys its quad (pathKeys): two values are the same when their parts are. */
function valueParts(value: Value): string[] {
if (value.termType !== "Literal") return [value.termType, value.value];
return ["Literal", value.datatype.value, value.language, value.value];
return ["Literal", value.datatype.value, value.language, value.direction ?? "", value.value];
}

/**
Expand Down
26 changes: 26 additions & 0 deletions packages/solid/src/client/linearDataset.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,32 @@ describe("graphsOf", () => {
expect(getThing(fromRdfJsDataset([] as never), victim)).toBeNull();
});

it("keeps a literal with a base direction under its language and direction, one with no language or no direction by its datatype", async () => {
const [first] = await quadsOf(`<#a> <https://example.com/ns#p> "x" .`);
const DIR_LANG_STRING = "http://www.w3.org/1999/02/22-rdf-syntax-ns#dirLangString";
const withObject = (language: string, direction: string) =>
({
subject: first!.subject,
predicate: first!.predicate,
graph: first!.graph,
object: {
termType: "Literal",
value: `${language}${direction}`,
language,
direction,
datatype: { termType: "NamedNode", value: DIR_LANG_STRING },
},
}) as Quad;
const objects = graphsOf([
withObject("AR", "RTL"),
withObject("ar", "ltr"),
withObject("", "rtl"),
withObject("ar", ""),
]).default[`${URL_}#a`]!.predicates["https://example.com/ns#p"]!;
expect(objects.langStrings).toEqual({ "ar--rtl": ["ARRTL"], "ar--ltr": ["arltr"] });
expect(objects.literals).toEqual({ [DIR_LANG_STRING]: ["rtl", "ar"] });
});

it("refuses an object solid-client cannot hold, as solid-client does", async () => {
const [first] = await quadsOf(`<#a> <https://example.com/ns#p> "x" .`);
const { subject, predicate, graph } = first!;
Expand Down
38 changes: 33 additions & 5 deletions packages/solid/src/client/linearDataset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
type Parser,
type SolidDataset,
} from "@inrupt/solid-client";
import type { Quad } from "@rdfjs/types";
import type { Literal, Quad } from "@rdfjs/types";
import { Parser as N3Parser, type ParserOptions } from "n3";
import { ReadLimitError, type ReadLimits } from "./interface";
import { withBoundedHeaders } from "./linkHeader";
Expand All @@ -31,6 +31,7 @@ type Objects = Graph[string]["predicates"][string];
type Dataset = Awaited<ReturnType<typeof getSolidDataset>>;

const LANG_STRING = "http://www.w3.org/1999/02/22-rdf-syntax-ns#langString";
const DIR_LANG_STRING = "http://www.w3.org/1999/02/22-rdf-syntax-ns#dirLangString";

/** Where the library names a subject that has no URL yet: `<#name>` in the document it is saved to. */
export const LOCAL_NODE = "https://inrupt.com/.well-known/sdk-local-node/";
Expand Down Expand Up @@ -85,6 +86,8 @@ export async function getSolidDatasetLinear(
* parse fails at the first triple that holds one all the same, as an
* annotation (`{| ... |}`) or a reifier (`~`) gives, which the option
* does not keep n3 from reading; after its error it reports nothing more.
* An RDF 1.2 literal with a base direction (`"…"@ar--rtl`) is read, and
* kept with its language and direction (graphsOf).
*/
export function turtleParser(): Parser {
const onQuad: ((quad: Quad) => void)[] = [];
Expand Down Expand Up @@ -186,7 +189,9 @@ function collectingTurtleParser(collected: Collected, url: string): Parser {
* a way to write onto Object.prototype (GHSA-m4hj-7c28-fq4h). A quad
* is told from the others by the path of its terms (pathKeys), each a
* key of its own, as no string of them joined would be held to the
* bound on a term (MAX_TERM_CHARACTERS).
* bound on a term (MAX_TERM_CHARACTERS). A literal with a base direction
* keeps its language and direction (localeOf), so that what is read can
* be written back as it is.
*/
export function graphsOf(quads: Iterable<Quad>): Graphs {
const graphs =
Expand Down Expand Up @@ -215,8 +220,9 @@ export function graphsOf(quads: Iterable<Quad>): Graphs {
} else if (object.termType === "BlankNode") {
(objects.blankNodes ??= []).push(`_:${object.value}`);
} else if (object.termType === "Literal") {
if (object.datatype.value === LANG_STRING) {
((objects.langStrings ??= map())[object.language.toLowerCase()] ??= []).push(object.value);
const locale = localeOf(object);
if (locale !== undefined) {
((objects.langStrings ??= map())[locale] ??= []).push(object.value);
} else {
((objects.literals ??= map())[object.datatype.value] ??= []).push(object.value);
}
Expand All @@ -227,6 +233,27 @@ export function graphsOf(quads: Iterable<Quad>): Graphs {
return deepFreeze(graphs) as Graphs;
}

/**
* Where a literal in a language is kept among a subject's language
* strings: under its language in lower case, and one with a base
* direction (RDF 1.2) under its language and direction as Turtle writes
* them (`ar--rtl`), from which the library's data factory (n3's) makes
* the same literal again, its language and direction apart. No language
* tag has `--` in it, so no other is kept there; the getters of language
* text leave such a one out (rdf.ts getStringByLocaleAll). Undefined for
* a literal in no language, or one with a direction but no language,
* which is kept by its datatype, as Turtle cannot write it (writableTerms.ts).
*/
function localeOf(literal: Literal): string | undefined {
const language = literal.language.toLowerCase();
if (literal.datatype.value === LANG_STRING) return language;
const direction = literal.direction ?? "";
if (literal.datatype.value !== DIR_LANG_STRING || language === "" || direction === "") {
return undefined;
}
return `${language}--${direction.toLowerCase()}`;
}

/** An empty map with no prototype, so that no key reaches Object.prototype. */
function map<T>(): Record<string, T> {
return Object.create(null) as Record<string, T>;
Expand All @@ -235,7 +262,8 @@ function map<T>(): Record<string, T> {
/** An object's parts, each a step of the path that tells its quad from others (graphsOf). */
function termParts(term: Quad["object"]): string[] {
if (term.termType !== "Literal") return [term.termType, term.value];
return ["Literal", term.datatype.value, term.language.toLowerCase(), term.value];
const { datatype, language, direction, value } = term;
return ["Literal", datatype.value, language.toLowerCase(), direction ?? "", value];
}

function deepFreeze<T extends object>(value: T): T {
Expand Down
58 changes: 48 additions & 10 deletions packages/solid/src/client/rdf.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import {
asUrl as libraryAsUrl,
createSolidDataset as libraryCreateSolidDataset,
createThing as libraryCreateThing,
fromRdfJsDataset as libraryFromRdfJsDataset,
getBoolean as libraryGetBoolean,
getDatetime as libraryGetDatetime,
getDecimal as libraryGetDecimal,
Expand Down Expand Up @@ -87,6 +86,11 @@ import { pathKeys } from "./readLimits";
* (getStringByLocaleAll, getLiteralAll), however the document spells it.
* - getDecimal reads a decimal as parseFloat does (`2.50` is 2.5, a value
* that is no number null).
* - A literal with a base direction (RDF 1.2, `"…"@ar--rtl`) is kept with
* its language and direction, and written back as it was read. It is
* no language text: getStringByLocaleAll and setStringAll leave it be,
* and propertiesOf gives it as a literal of its datatype
* (rdf:dirLangString); getLiteralAll and getTermAll give it whole.
*/

export type {
Expand Down Expand Up @@ -160,6 +164,18 @@ export function propertiesOf(subject: Subject): { predicate: string; values: Pro
}));
}

const DIR_LANG_STRING = "http://www.w3.org/1999/02/22-rdf-syntax-ns#dirLangString";

/**
* Whether a subject's language strings under `locale` have a base
* direction: kept under their language and direction (`ar--rtl`,
* linearDataset.ts graphsOf), never under a language tag alone, which
* has no `--` in it.
*/
function isDirectional(locale: string): boolean {
return locale.includes("--");
}

function valuesOf(objects: Thing["predicates"][string]): PropertyValue[] {
const values: PropertyValue[] = [];
for (const iri of objects.namedNodes ?? []) {
Expand All @@ -170,7 +186,13 @@ function valuesOf(objects: Thing["predicates"][string]): PropertyValue[] {
values.push({ type: "literal", value: literal, dataType });
}
}
for (const [language, strings] of Object.entries(objects.langStrings ?? {})) {
const langStrings = Object.entries(objects.langStrings ?? {});
for (const [, strings] of langStrings.filter(([locale]) => isDirectional(locale))) {
for (const value of strings) {
values.push({ type: "literal", value, dataType: DIR_LANG_STRING });
}
}
for (const [language, strings] of langStrings.filter(([locale]) => !isDirectional(locale))) {
for (const value of strings) {
values.push({ type: "langString", value, language });
}
Expand Down Expand Up @@ -347,7 +369,11 @@ function editOf(data: SolidDataset) {
function quadKeys(): (quad: Quad) => number {
const keyOf = pathKeys();
return ({ subject, predicate, object }) => {
const literal = object.termType === "Literal" ? [object.datatype.value, object.language] : [];
// Every quad keyed is made by the library's data factory (n3's), whose literals all name a direction.
const literal =
object.termType === "Literal"
? [object.datatype.value, object.language, object.direction as string]
: [];
return keyOf([
subject.termType,
subject.value,
Expand Down Expand Up @@ -507,9 +533,11 @@ class Builder implements SubjectBuilder {
const literals = { ...previous.literals };
delete literals[XSD_STRING];
if (strings.size > 0) literals[XSD_STRING] = [...strings];
const langStrings = Object.fromEntries(
[...byLanguage].map(([tag, texts]) => [tag, [...texts]]),
);
// Text with a base direction is no language text: kept as it is.
const langStrings = Object.fromEntries([
...Object.entries(previous.langStrings ?? {}).filter(([locale]) => isDirectional(locale)),
...[...byLanguage].map(([tag, texts]) => [tag, [...texts]]),
]);
return { ...previous, literals, langStrings };
});
}
Expand Down Expand Up @@ -570,9 +598,15 @@ export function getStringNoLocaleAll(subject: Subject, property: string): string
return libraryGetStringNoLocaleAll(thing(subject), property);
}

/** The subject's strings with a language for the property, by their language in lower case. */
/**
* The subject's strings with a language for the property, by their
* language in lower case; none with a base direction, which is no
* language text (isDirectional).
*/
export function getStringByLocaleAll(subject: Subject, property: string): Map<string, string[]> {
return libraryGetStringByLocaleAll(thing(subject), property);
const strings = libraryGetStringByLocaleAll(thing(subject), property);
for (const locale of strings.keys()) if (isDirectional(locale)) strings.delete(locale);
return strings;
}

/** An integer the subject has for the property; null when it has none. */
Expand Down Expand Up @@ -615,7 +649,11 @@ export function toRdfJsDataset(dataset: Dataset): DatasetCore {
return libraryToRdfJsDataset(solidDataset(dataset));
}

/** A dataset of an RDF/JS dataset's triples. */
/**
* A dataset of an RDF/JS dataset's triples, built in one pass as a read
* builds one (datasetFromQuads), a literal with a base direction kept
* whole.
*/
export function fromRdfJsDataset(dataset: DatasetCore): Dataset {
return libraryFromRdfJsDataset(dataset);
return datasetFromQuads(dataset);
}
11 changes: 10 additions & 1 deletion packages/solid/src/client/sparqlUpdate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,15 @@ function literal(term: Literal, decimalsAsNumbers: boolean): string {
? String(Number.parseFloat(term.value))
: term.value;
const text = `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\r/g, "\\r")}"`;
if (term.language !== "") return `${text}@${term.language}`;
if (term.language !== "") return `${text}@${term.language}${directionOf(term)}`;
return term.datatype.value === XSD_STRING ? text : `${text}^^<${term.datatype.value}>`;
}

/**
* A literal's base direction as Turtle, N-Triples and SPARQL write it
* after its language (`"…"@ar--rtl`, RDF 1.2); empty for one with none.
* Whether it can be written so is writableTerms.ts's to check.
*/
export function directionOf(term: { direction?: string | null | undefined }): string {
return term.direction ? `--${term.direction}` : "";
}
Loading
Loading