Jackson Morgan 5 months ago
parent 0518c5c748
commit db87958cb6
  1. 285
      packages/connected-nextgraph/README.md
  2. 2
      packages/connected-nextgraph/src/resources/NextGraphResource.ts
  3. 4
      packages/connected-solid/src/requester/results/success/SolidReadSuccess.ts
  4. 3
      packages/connected-solid/typedoc.json
  5. 177
      packages/connected/README.md
  6. 1
      packages/connected/typedoc.json

@ -1,204 +1,117 @@
# @ldo/solid # `@ldo/connected-nextgraph`
@ldo/solid is a client that implements the Solid specification with the use of Linked Data Objects. The `@ldo/connected-nextgraph` library allows you to integrate [NextGraph](https://nextgraph.org) with the [LDO](https://ldo.js.org) ecosystem. It provides a `ConnectedLdoDataset` that manages RDF data across decentralized NextGraph resources with real-time synchronization and read/write capabilities.
## Installation ## Installation
Navigate into your project's root folder and run the following command: First, install the required libraries:
```
cd my_project/ ```bash
npx run @ldo/cli init npm install nextgraph @ldo/connected-nextgraph
``` ```
Now install the @ldo/solid library ## Usage:
### 1. Setup: Create a ConnectedLdoDataset
```ts
import { createNextGraphLdoDataset } from "@ldo/connected-nextgraph";
// Create the dataset
const ldoDataset = createNextGraphLdoDataset();
``` ```
npm i @ldo/solid
### 2. Connect to a NextGraph Wallet Session
Before you can create or access resources, you need an active session:
```ts
import ng from "nextgraph";
// Open your nextgraph wallet
const openedWallet = await ng.wallet_open_with_mnemonic_words(
wallet.wallet,
mnemonic,
[1, 2, 1, 2]
);
// Start a session
const session = await ng.session_in_memory_start(
openedWallet.V0.wallet_id,
openedWallet.V0.personal_site
);
``` ```
<details> ---
<summary>
Manual Installation
</summary>
If you already have generated ShapeTypes, you may install the `@ldo/ldo` and `@ldo/solid` libraries independently. ### 3. Link Your Dataset to the NextGraph Session
```ts
ldoDataset.setContext("nextgraph", {
sessionId: session.session_id
});
``` ```
npm i @ldo/ldo @ldo/solid
### 4. Create a Resource
To create a new resource in your store:
```ts
const resource = await ldoDataset.createResource("nextgraph");
if (!resource.isError) {
console.log("Created resource:", resource.uri);
}
``` ```
</details>
### 5. Read and Monitor a Resource**
## Simple Examples
#### Read Existing Resource
Below is a simple example of @ldo/solid. Assume that a ShapeType was previously generated and placed at `./.ldo/foafProfile.shapeTypes`. Also assume we have a shape type for social media at `./.ldo/socialMediaPost.shapeTypes`
```ts
```typescript const resource = ldoDataset.getResource(existingUri);
import { changeData, commitData, createSolidLdoDataset } from "@ldo/solid"; const readResult = await resource.read();
import { fetch, getDefaultSession } from "@inrupt/solid-client-authn-browser";
import { FoafProfileShapeType } from "./.ldo/foafProfile.shapeTypes"; if (!readResult.isError) {
import { SocialMediaPostShapeType } from "./.ldo/socialMediaPost.shapeTypes"; console.log("Resource loaded!", readResult.type);
async function main() {
/**
* ===========================================================================
* READING DATA FROM A POD
* ===========================================================================
*/
// Before we begin using @ldo/solid. Let's get the WebId of the current user
const webIdUri = getDefaultSession().info.webId;
if (!webIdUri) throw new Error("User is not logged in");
// Now let's proceed with @ldo/solid. Our first step is setting up a
// SolidLdoDataset. You can think of this dataset as a local store for all the
// information in the Solidverse. Don't forget to pass the authenticated fetch
// function to do your queries!
const solidLdoDataset = createSolidLdoDataset({ fetch });
// We'll start with getting a representation of our WebId's resource
const webIdResource = solidLdoDataset.getResource(webIdUri);
// This resource is currently unfetched
console.log(webIdResource.isUnfetched()); // Logs true
// So let's fetch it! Running the `read` command will make a request to get
// the WebId.
const readResult = await webIdResource.read();
// @ldo/solid will never throw an error. Instead, it will return errors. This
// design decision was made to force you to handle any errors. It may seem a
// bit annoying at first, but it will result in more resiliant code. You can
// easily follow intellisense tooltips to see what kinds of errors each action
// can throw.
if (readResult.isError) {
switch (readResult.type) {
case "serverError":
console.error("The solid server had an error:", readResult.message);
return;
case "noncompliantPodError":
console.error("The Pod responded in a way not compliant with the spec");
return;
default:
console.error("Some other error was detected:", readResult.message);
}
}
// When fetching a data resource, read triples will automatically be added to
// the solidLdoDataset. You can access them using Linked Data Objects. In
// the following example we're using a Profile Linked Data Object that was
// generated with the init step.
const profile = solidLdoDataset
.usingType(FoafProfileShapeType)
.fromSubject(webIdUri);
// Now you can read "profile" like any JSON.
console.log(profile.name);
/**
* ===========================================================================
* MODIFYING DATA
* ===========================================================================
*/
// When we want to modify data the first step is to use the `changeData`
// function. We pass in an object that we want to change (in this case,
// "profile") as well an a list of any resources to which we want those
// changes to be applied (in this case, just the webIdResource). This gives
// us a new variable (conventionally named with a c for "changed") that we can
// write changes to.
const cProfile = changeData(profile, webIdResource);
// We can make changes just like it's regular JSON
cProfile.name = "Captain Cool Dude";
// Committing data is as easy as running the "commitData" function.
const commitResult = await commitData(cProfile);
// Remember to check for and handle errors! We'll keep it short this time.
if (commitResult.isError) throw commitResult;
/**
* ===========================================================================
* CREATING NEW RESOURCES
* ===========================================================================
*/
// Let's create some social media posts to be stored on the Solid Pod!
// Our first step is going to be finding where to place these posts. In the
// future, there will be advanced ways to determine the location of resources
// but for now, let's throw it in the root folder.
// But, first, let's find out where the root folder is. We can take our WebId
// resource and call `getRootContainer`. Let's assume the root container has
// a URI "https://example.com/"
const rootContainer = await webIdResource.getRootContainer();
if (rootContainer.isError) throw rootContainer;
// Now, let's create a container for our posts
const createPostContainerResult =
await rootContainer.createChildIfAbsent("social-posts/");
if (createPostContainerResult.isError) throw createPostContainerResult;
// Most results store the affected resource in the "resource" field. This
// container has the URI "https://example.com/social-posts/"
const postContainer = createPostContainerResult.resource;
// Now that we have our container, let's make a Post resource! This is a data
// resource, which means we can put raw Solid Data (RDF) into it.
const postResourceResult =
await postContainer.createChildAndOverwrite("post1.ttl");
if (postResourceResult.isError) throw postResourceResult;
const postResource = postResourceResult.resource;
// We can also create binary resources with things like images
const imageResourceResult = await postContainer.uploadChildAndOverwrite(
// name of the binary
"image1.svg",
// A blob for the binary
new Blob([`<svg><circle r="9" /></svg>`]),
// mime type of the binary
"image/svg+xml",
);
if (imageResourceResult.isError) throw imageResourceResult;
const imageResource = imageResourceResult.resource;
/**
* ===========================================================================
* CREATING NEW DATA
* ===========================================================================
*/
// We create data in a similar way to the way we modify data. We can use the
// "createData" method.
const cPost = solidLdoDataset.createData(
// An LDO ShapeType saying that this is a social media psot
SocialMediaPostShapeType,
// The URI of the post (in this case we'll make it the same as the resource)
postResource.uri,
// The resource we should write it to
postResource,
);
// We can add new data
cPost.text = "Check out this bad svg:";
cPost.image = { "@id": imageResource.uri };
// And now we commit data
const newDataResult = await commitData(cPost);
if (newDataResult.isError) throw newDataResult;
/**
* ===========================================================================
* DELETING RESOURCES
* ===========================================================================
*/
// Deleting resources can be done with a single method call. In this case,
// the container will be deleted along with all its contained resources
const deleteResult = await postContainer.delete();
if (deleteResult.isError) throw deleteResult;
} }
main(); ```
#### Read Only If Unfetched
Avoid redundant fetches:
```ts
const readResult = await resource.readIfUnfetched();
```
#### Subscribe to Notifications
```ts
const unsubscribeId = await resource.subscribeToNotifications();
await resource.unsubscribeFromNotification(unsubscribeId);
await resource.unsubscribeFromAllNotifications();
```
---
### 6. Write Data to a Resource
You can write RDF data to a resource using `update()`:
```ts
import { parseRdf } from "@ldo/ldo";
const ttlData = `
@prefix foaf: <http://xmlns.com/foaf/0.1/> .
<#spiderman> a foaf:Person ; foaf:name "Spiderman" .
`;
const triples = await parseRdf(ttlData);
await resource.update({
added: triples,
removed: undefined
});
``` ```
## API Details ## API Details

@ -21,7 +21,7 @@ import { namedNode, quad as createQuad } from "@rdfjs/data-model";
import { NextGraphReadSuccess } from "../results/NextGraphReadSuccess"; import { NextGraphReadSuccess } from "../results/NextGraphReadSuccess";
import { NextGraphNotificationSubscription } from "../notifications/NextGraphNotificationSubscription"; import { NextGraphNotificationSubscription } from "../notifications/NextGraphNotificationSubscription";
import { parseRdf } from "@ldo/ldo"; import { parseRdf } from "@ldo/ldo";
import type { LdoDataset } from "packages/ldo/dist/LdoDataset"; import type { LdoDataset } from "@ldo/ldo";
import { createDataset } from "@ldo/dataset"; import { createDataset } from "@ldo/dataset";
export class NextGraphResource export class NextGraphResource

@ -1,7 +1,7 @@
import type { ConnectedResult } from "@ldo/connected";
import { ReadSuccess, type Resource } from "@ldo/connected"; import { ReadSuccess, type Resource } from "@ldo/connected";
import type { SolidLeaf } from "../../../resources/SolidLeaf"; import type { SolidLeaf } from "../../../resources/SolidLeaf";
import type { SolidContainer } from "../../../resources/SolidContainer"; import type { SolidContainer } from "../../../resources/SolidContainer";
import type { ResourceResult } from "packages/connected/dist/results/ResourceResult";
/** /**
* Indicates that the read request was successful and that the resource * Indicates that the read request was successful and that the resource
@ -66,7 +66,7 @@ export class ContainerReadSuccess extends ReadSuccess<SolidContainer> {
* @returns true if the result is a ReadSuccessResult result * @returns true if the result is a ReadSuccessResult result
*/ */
export function isReadSuccess<ResourceType extends Resource>( export function isReadSuccess<ResourceType extends Resource>(
result: ResourceResult<Resource>, result: ConnectedResult,
): result is ReadSuccess<ResourceType> { ): result is ReadSuccess<ResourceType> {
return ( return (
result.type === "binaryReadSuccess" || result.type === "binaryReadSuccess" ||

@ -4,4 +4,5 @@
"allReflectionsHaveOwnDocument": true, "allReflectionsHaveOwnDocument": true,
"hideInPageTOC": true, "hideInPageTOC": true,
"hideBreadcrumbs": true, "hideBreadcrumbs": true,
} "excludeExternals": true,
}

@ -1,6 +1,6 @@
# @ldo/solid # @ldo/connected
@ldo/solid is a client that implements the Solid specification with the use of Linked Data Objects. @ldo/connected provides tools for LDO to connect to a remote datasource. It requires plugins for that datasource.
## Installation ## Installation
@ -13,7 +13,13 @@ npx run @ldo/cli init
Now install the @ldo/solid library Now install the @ldo/solid library
``` ```
npm i @ldo/solid npm i @ldo/connected
```
You may also install a connected plugin, for example `@ldo/connected-solid` and `@ldo/connected-nextgraph`.
```
npm i @ldo/connected-nextgraph
``` ```
<details> <details>
@ -33,53 +39,84 @@ npm i @ldo/ldo @ldo/solid
Below is a simple example of @ldo/solid. Assume that a ShapeType was previously generated and placed at `./.ldo/foafProfile.shapeTypes`. Also assume we have a shape type for social media at `./.ldo/socialMediaPost.shapeTypes` Below is a simple example of @ldo/solid. Assume that a ShapeType was previously generated and placed at `./.ldo/foafProfile.shapeTypes`. Also assume we have a shape type for social media at `./.ldo/socialMediaPost.shapeTypes`
```typescript ```typescript
import { changeData, commitData, createSolidLdoDataset } from "@ldo/solid"; import {
import { fetch, getDefaultSession } from "@inrupt/solid-client-authn-browser"; changeData,
commitData,
createConnectedLdoDataset
} from "@ldo/connected";
import { solidConnectedPlugin } from "@ldo/connected-solid";
import { nextGraphConnectedPlugin } from "@ldo/connected-nextgraph";
// Shape Types
import { FoafProfileShapeType } from "./.ldo/foafProfile.shapeTypes"; import { FoafProfileShapeType } from "./.ldo/foafProfile.shapeTypes";
import { SocialMediaPostShapeType } from "./.ldo/socialMediaPost.shapeTypes"; import { SocialMediaPostShapeType } from "./.ldo/socialMediaPost.shapeTypes";
// These are tools for Solid and NextGraph outside of the LDO ecosystem
import { fetch, getDefaultSession } from "@inrupt/solid-client-authn-browser";
import ng from "nextgraph";
async function main() { async function main() {
/** /**
* =========================================================================== * ===========================================================================
* READING DATA FROM A POD * SETTING UP A CONNECTED LDO DATASTORE WITH 2 PLUGINS
* =========================================================================== * ===========================================================================
*/ */
const connectedLdoDataset = createConnectedLdoDataset([
solidConncetedPlugin,
nextGraphConnectedPlugin
]);
// Set context to be able to make authenticated requests
connectedLdoDataset.setContext("solid", { fetch });
const session = await ng.session_in_memory_start(
openedWallet.V0.wallet_id,
openedWallet.V0.personal_site
);
connectedLdoDataset.setContext("nextGraph", { sessionId: session.sessionId });
// Before we begin using @ldo/solid. Let's get the WebId of the current user /**
const webIdUri = getDefaultSession().info.webId; * ===========================================================================
if (!webIdUri) throw new Error("User is not logged in"); * READING DATA FROM REMOTE
* ===========================================================================
*/
// Now let's proceed with @ldo/solid. Our first step is setting up a // We can get a Solid resource by including a Solid-Compatible URL
// SolidLdoDataset. You can think of this dataset as a local store for all the const solidResource = solidLdoDataset.getResource(
// information in the Solidverse. Don't forget to pass the authenticated fetch "https://pod.example.com/profile.ttl"
// function to do your queries! );
const solidLdoDataset = createSolidLdoDataset({ fetch }); // Similarly, we can get a NextGraph resource by including a
// NextGraph-Compatible URL
const nextGraphResource = solidLdoDataset.getResource(
"did:ng:o:W6GCQRfQkNTLtSS_2-QhKPJPkhEtLVh-B5lzpWMjGNEA:v:h8ViqyhCYMS2I6IKwPrY6UZi4ougUm1gpM4QnxlmNMQA"
);
// Optionally, you can provide the name of the specific plugin you want to use
const anotherSolidResource = solidLdoDataset.getResource("", "solid");
// We'll start with getting a representation of our WebId's resource
const webIdResource = solidLdoDataset.getResource(webIdUri);
// This resource is currently unfetched // This resource is currently unfetched
console.log(webIdResource.isUnfetched()); // Logs true console.log(solidResource.isUnfetched()); // Logs true
console.log(nextGraphResource.isUnfetched()); // Logs true
// So let's fetch it! Running the `read` command will make a request to get // So let's fetch it! Running the `read` command will make a request to get
// the WebId. // the WebId.
const readResult = await webIdResource.read(); const solidReadResult = await solidResource.read();
const ngReadResult = await nextGraphREsource.read();
// @ldo/solid will never throw an error. Instead, it will return errors. This
// design decision was made to force you to handle any errors. It may seem a // @ldo/connected will never throw an error. Instead, it will return errors.
// bit annoying at first, but it will result in more resiliant code. You can // This design decision was made to force you to handle any errors. It may
// easily follow intellisense tooltips to see what kinds of errors each action // seem a bit annoying at first, but it will result in more resiliant code.
// can throw. // You can easily follow intellisense tooltips to see what kinds of errors
if (readResult.isError) { // each action can throw.
switch (readResult.type) { if (solidReadResult.isError) {
switch (solidReadResult.type) {
case "serverError": case "serverError":
console.error("The solid server had an error:", readResult.message); console.error("The solid server had an error:", solidReadResult.message);
return; return;
case "noncompliantPodError": case "noncompliantPodError":
console.error("The Pod responded in a way not compliant with the spec"); console.error("The Pod responded in a way not compliant with the spec");
return; return;
default: default:
console.error("Some other error was detected:", readResult.message); console.error("Some other error was detected:", solidReadResult.message);
} }
} }
@ -87,9 +124,9 @@ async function main() {
// the solidLdoDataset. You can access them using Linked Data Objects. In // the solidLdoDataset. You can access them using Linked Data Objects. In
// the following example we're using a Profile Linked Data Object that was // the following example we're using a Profile Linked Data Object that was
// generated with the init step. // generated with the init step.
const profile = solidLdoDataset const profile = connectedLdoDataset
.usingType(FoafProfileShapeType) .usingType(FoafProfileShapeType)
.fromSubject(webIdUri); .fromSubject("https://pod.example.com/profile#me");
// Now you can read "profile" like any JSON. // Now you can read "profile" like any JSON.
console.log(profile.name); console.log(profile.name);
@ -106,7 +143,7 @@ async function main() {
// changes to be applied (in this case, just the webIdResource). This gives // changes to be applied (in this case, just the webIdResource). This gives
// us a new variable (conventionally named with a c for "changed") that we can // us a new variable (conventionally named with a c for "changed") that we can
// write changes to. // write changes to.
const cProfile = changeData(profile, webIdResource); const cProfile = changeData(profile, solidResource);
// We can make changes just like it's regular JSON // We can make changes just like it's regular JSON
cProfile.name = "Captain Cool Dude"; cProfile.name = "Captain Cool Dude";
@ -123,28 +160,17 @@ async function main() {
* =========================================================================== * ===========================================================================
*/ */
// Let's create some social media posts to be stored on the Solid Pod! // Let's create some social media posts to be stored on the Solid Pod and in
// Our first step is going to be finding where to place these posts. In the // NextGraph! We can create new resources using the "createResource" method.
// future, there will be advanced ways to determine the location of resources const newSolidResource = await connectedLdoDataset.createResource("solid");
// but for now, let's throw it in the root folder. const newNgResource = await connectedLdoDataset.createResource("nextGraph");
// But, first, let's find out where the root folder is. We can take our WebId
// resource and call `getRootContainer`. Let's assume the root container has
// a URI "https://example.com/"
const rootContainer = await webIdResource.getRootContainer();
if (rootContainer.isError) throw rootContainer;
// Now, let's create a container for our posts // For Solid, you can also create resources at a predefined location
const postContainer = connectedLdoDataset
.getResource("https://pod.example.com/socialPosts/");
const createPostContainerResult = const createPostContainerResult =
await rootContainer.createChildIfAbsent("social-posts/"); await solidSocialPostsContainer.createIfAbsent();
if (createPostContainerResult.isError) throw createPostContainerResult; if (createPostContainerResult.isError) throw createPostContainerResult;
// Most results store the affected resource in the "resource" field. This
// container has the URI "https://example.com/social-posts/"
const postContainer = createPostContainerResult.resource;
// Now that we have our container, let's make a Post resource! This is a data
// resource, which means we can put raw Solid Data (RDF) into it.
const postResourceResult = const postResourceResult =
await postContainer.createChildAndOverwrite("post1.ttl"); await postContainer.createChildAndOverwrite("post1.ttl");
if (postResourceResult.isError) throw postResourceResult; if (postResourceResult.isError) throw postResourceResult;
@ -203,30 +229,47 @@ main();
## API Details ## API Details
SolidLdoDataset ConnectedLdoDataset
- [createSolidLdoDataset](https://ldo.js.org/latest/api/solid/functions/createSolidLdoDataset/) - [createConnectedLdoDataset](https://ldo.js.org/latest/api/connected/functions/createConnectedLdoDataset.md)
- [SolidLdoDataset](https://ldo.js.org/latest/api/solid/classes/SolidLdoDataset/) - [ConnectedLdoDataset](https://ldo.js.org/latest/api/connected/classes/ConnectedLdoDataset.md)
- [ConnectedLdoTransactionDataset](https://ldo.js.org/latest/api/connected/classes/ConnectedLdoTransactionDataset.md)
- [IConnectedLdoDataset](https://ldo.js.org/latest/api/connected/interfaces/IConnectedLdoDataset.md)
Resources (Manage batching requests) ConnectedPlugins
- [LeafUri](https://ldo.js.org/latest/api/solid/types/LeafUri/) - [ConnectedPlugin](https://ldo.js.org/latest/api/connected/interfaces/ConnectedPlugin.md)
- [ContainerUri](https://ldo.js.org/latest/api/solid/types/ContainerUri/)
- [Leaf](https://ldo.js.org/latest/api/solid/classes/Leaf/)
- [Container](https://ldo.js.org/latest/api/solid/classes/Container/)
Standalone Functions Resources (Manage batching requests)
- [checkRootContainter](https://ldo.js.org/latest/api/solid/functions/checkRootContainer/) - [Resource](https://ldo.js.org/latest/api/connected/interfaces/Resource.md)
- [createDataResource](https://ldo.js.org/latest/api/solid/functions/createDataResource/)
- [deleteResource](https://ldo.js.org/latest/api/solid/functions/deleteResource/)
- [readResource](https://ldo.js.org/latest/api/solid/functions/readResource/)
- [updateResource](https://ldo.js.org/latest/api/solid/functions/updateResource/)
- [uploadResource](https://ldo.js.org/latest/api/solid/functions/uploadResource/)
Data Functions Data Functions
- [changeData](https://ldo.js.org/latest/api/solid/functions/changeData/)
- [commitData](https://ldo.js.org/latest/api/solid/functions/commitData/) - [changeData](https://ldo.js.org/latest/api/connected/functions/changeData/)
- [commitData](https://ldo.js.org/latest/api/connected/functions/commitData/)
SuccessResult
- [SuccessResult](https://ldo.js.org/latest/api/connected/classes/SuccessResult.md)
- [AbsentReadSuccess](https://ldo.js.org/latest/api/connected/classes/AbsentReadSuccess.md)
- [AggregateSuccess](https://ldo.js.org/latest/api/connected/classes/AggregateSuccess.md)
- [IgnoredInvalidUpdateSuccess](https://ldo.js.org/latest/api/connected/classes/IgnoredInvalidUpdateSuccess.md)
- [ReadSuccess](https://ldo.js.org/latest/api/connected/classes/ReadSuccess.md)
- [ResourceSuccess](https://ldo.js.org/latest/api/connected/classes/ResourceSuccess.md)
- [Unfetched](https://ldo.js.org/latest/api/connected/classes/Unfetched.md)
- [UpdateDefaultGraphSuccess](https://ldo.js.org/latest/api/connected/classes/UpdateDefaultGraphSuccess.md)
- [UpdateSuccess](https://ldo.js.org/latest/api/connected/classes/UpdateSuccess.md
ErrorResult
- [ErrorResult](https://ldo.js.org/latest/api/connected/classes/ErrorResult.md)
- [AggregateError](https://ldo.js.org/latest/api/connected/classes/AggregateError.md)
- [DisconnectedAttemptingReconnectError](https://ldo.js.org/latest/api/connected/classes/DisconnectedAttemptingReconnectError.md)
- [InvalidUriError](https://ldo.js.org/latest/api/connected/classes/InvalidUriError.md)
- [ResourceError](https://ldo.js.org/latest/api/connected/classes/ResourceError.md)
- [UnexpectedResourceError](https://ldo.js.org/latest/api/connected/classes/UnexpectedResourceError.md)
- [UnsupportedNotificationError](https://ldo.js.org/latest/api/connected/classes/UnsupportedNotificationError.md)
## Sponsorship ## Sponsorship
This project was made possible by a grant from NGI Zero Entrust via nlnet. Learn more on the [NLnet project page](https://nlnet.nl/project/SolidUsableApps/). This project was made possible by a grant from NGI Zero Entrust via nlnet. Learn more on the [NLnet project page](https://nlnet.nl/project/SolidUsableApps/).

@ -1,5 +1,6 @@
{ {
"entryPoints": ["src/index.ts"], "entryPoints": ["src/index.ts"],
"tsconfig": "tsconfig.build.json",
"out": "docs", "out": "docs",
"allReflectionsHaveOwnDocument": true, "allReflectionsHaveOwnDocument": true,
"hideInPageTOC": true, "hideInPageTOC": true,

Loading…
Cancel
Save