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
4 changes: 4 additions & 0 deletions apps/roam/src/utils/canonicalRoamUrl.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
const ROAM_URL_PREFIX = "https://roamresearch.com/#/app/";
const canonicalRoamUrl = (graphName = window.roamAlphaAPI.graph.name) =>
ROAM_URL_PREFIX + graphName;
export default canonicalRoamUrl;
24 changes: 24 additions & 0 deletions apps/roam/src/utils/getExportTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { getRelationDataUtil } from "./getRelationData";
import { ExportTypes } from "./types";
import { getExportSettings } from "./getExportSettings";
import { pageToMarkdown, toMarkdown } from "./pageToMarkdown";
import { getJsonLdData } from "./jsonld";
import {
uniqJsonArray,
getPageData,
Expand Down Expand Up @@ -144,6 +145,29 @@ const getExportTypes = ({
];
},
},
{
name: "JSON-LD (Experimental)",
callback: async ({ filename }) => {
if (!results) return [];
const data = await getJsonLdData({
results,
allNodes,
allRelations,
nodeLabelByType,
updateExportProgress: async (progress: number) => {
updateExportProgress({ progress, id: exportId });
// skip a beat to let progress render
await new Promise((resolve) => setTimeout(resolve));
},
});
return [
{
title: `${filename.replace(/\.json$/, "")}.json`,
content: JSON.stringify(data, undefined, " "),
},
];
},
},
{
name: "Neo4j",
callback: async ({ filename }) => {
Expand Down
14 changes: 9 additions & 5 deletions apps/roam/src/utils/getPageMetadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,21 +14,25 @@ const getDisplayName = (s: string) => {

const getPageMetadata = (title: string, cacheKey?: string) => {
const results = window.roamAlphaAPI.q(
`[:find (pull ?p [:create/time :block/uid]) (pull ?cu [:user/uid]) :where [?p :node/title "${normalizePageTitle(
`[:find (pull ?p [:block/uid :create/time [:edit/time :as "modified"]]) (pull ?cu [:user/uid]) :where [?p :node/title "${normalizePageTitle(
title,
)}"] [?p :create/user ?cu]]`,
) as [[{ time: number; uid: string }, { uid: string }]];
) as [[{ uid: string; time: number; modified: number }, { uid: string }]];
if (results.length) {
const [[{ time: createdTime, uid: id }, { uid }]] = results;
const [[{ uid: id, time: createdTime, modified: modifiedTime }, { uid }]] =
results;

const displayName = getDisplayName(uid);
const date = new Date(createdTime);
return { displayName, date, id };
const modified = new Date(modifiedTime);
return { displayName, date, id, modified };
}
const date = new Date();
return {
displayName: "Unknown",
date: new Date(),
date,
id: "",
modified: date,
};
};

Expand Down
188 changes: 188 additions & 0 deletions apps/roam/src/utils/jsonld.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
import type { Result } from "roamjs-components/types/query-builder";
import type { DiscourseRelation } from "./getDiscourseRelations";
import type { DiscourseNode } from "./getDiscourseNodes";
import getPageMetadata from "./getPageMetadata";
import { pageToMarkdown } from "./pageToMarkdown";
import { getRelationDataUtil } from "./getRelationData";
import { uniqJsonArray, getPageData } from "./exportUtils";
import { getExportSettings } from "./getExportSettings";
import canonicalRoamUrl from "./canonicalRoamUrl";
import internalError from "./internalError";

export const jsonLdContext = (baseUrl: string): Record<string, string> => ({
rdf: "http://www.w3.org/1999/02/22-rdf-syntax-ns#",
rdfs: "http://www.w3.org/2000/01/rdf-schema#",
owl: "http://www.w3.org/2002/07/owl#",
dc: "http://purl.org/dc/elements/1.1/",
prov: "http://www.w3.org/ns/prov#",
sioc: "http://rdfs.org/sioc/ns#",
dgb: "https://discoursegraphs.com/schema/dg_base",
subClassOf: "rdfs:subClassOf",
title: "dc:title",
label: "rdfs:label",
modified: "dc:modified",
created: "dc:date",
creator: "dc:creator",
content: "sioc:content",
source: "dgb:source",
destination: "dgb:destination",
textRefersToNode: "dgb:textRefersToNode",
predicate: "rdf:predicate",
nodeSchema: "dgb:NodeSchema",
relationDef: "dgb:RelationDef",
relationInstance: "dgb:RelationInstance",
inverseOf: "owl:inverseOf",
pages: `${baseUrl}/page/`,
});

export const getJsonLdSchema = async ({
allNodes,
allRelations,
updateExportProgress,
}: {
allNodes: DiscourseNode[];
allRelations: DiscourseRelation[];
updateExportProgress: (progress: number) => Promise<void>;
}): Promise<Record<string, string>[]> => {
let numTreatedPages = 0;
const settings = {
...getExportSettings(),
includeDiscourseContext: false,
};
// TODO : Identify existing CURIES in the node definition
const nodeSchemaData = await Promise.all(
allNodes.map(async (node: DiscourseNode) => {
const { date, displayName, modified } = getPageMetadata(node.text);
const r = await pageToMarkdown(
{
text: node.text,
uid: node.type,
},
{ ...settings, allNodes },
);
numTreatedPages += 1;
await updateExportProgress(numTreatedPages);
return {
"@id": `pages:${node.type}`, // eslint-disable-line @typescript-eslint/naming-convention
"@type": "nodeSchema", // eslint-disable-line @typescript-eslint/naming-convention
label: node.text,
content: r.content,
modified: modified?.toJSON(),
created: date.toJSON(),
creator: displayName,
};
}),
);
const relSchemaData = allRelations.map((r: DiscourseRelation) => ({
"@id": `pages:${r.id}`, // eslint-disable-line @typescript-eslint/naming-convention
"@type": "relationDef", // eslint-disable-line @typescript-eslint/naming-convention
domain: `pages:${r.source}`,
range: `pages:${r.destination}`,
label: r.label,
}));
const inverseRelSchemaData = allRelations.map((r: DiscourseRelation) => ({
"@id": `pages:${r.id}-inverse`, // eslint-disable-line @typescript-eslint/naming-convention
"@type": "relationDef", // eslint-disable-line @typescript-eslint/naming-convention
domain: `pages:${r.destination}`,
range: `pages:${r.source}`,
label: r.complement,
inverseOf: `pages:${r.id}`,
}));
/* eslint-enable @typescript-eslint/naming-convention */
return [...nodeSchemaData, ...relSchemaData, ...inverseRelSchemaData];
};

export const getJsonLdData = async ({
results,
allNodes,
allRelations,
nodeLabelByType,
updateExportProgress,
}: {
results: Result[];
allNodes: DiscourseNode[];
allRelations: DiscourseRelation[];
nodeLabelByType: Record<string, string>;
updateExportProgress: (progress: number) => Promise<void>;
}): Promise<
Record<string, string | Record<string, string> | Record<string, string>[]>
> => {
const roamUrl = canonicalRoamUrl();
const getRelationData = () =>
getRelationDataUtil({ allRelations, nodeLabelByType });
await updateExportProgress(0);
const pageData = getPageData({ results, allNodes });
const numPages = pageData.length + allNodes.length;
let numTreatedPages = 0;
const settings = {
...getExportSettings(),
includeDiscourseContext: false,
};
const schemaData = await getJsonLdSchema({
allNodes,
allRelations,
updateExportProgress: async (numTreatedPages: number) => {
await updateExportProgress(0.1 + (numTreatedPages / numPages) * 0.75);
},
});

const nodeSchemaUriByName = Object.fromEntries(
schemaData
.filter((s) => s.content !== undefined)
.map((node) => [node.label, node["@id"]]),
);

await Promise.all(
pageData.map(async (page: Result) => {
const r = await pageToMarkdown(page, {
...settings,
allNodes,
linkType: "roam url",
});
page.content = r.content;
numTreatedPages += 1;
await updateExportProgress(0.1 + (numTreatedPages / numPages) * 0.75);
}),
);

const nodes = pageData.map(({ text, uid, content, type }) => {
const { date, displayName, modified } = getPageMetadata(text);
const nodeType = nodeSchemaUriByName[type];
if (!nodeType) {
internalError({
error: `Unknown node type "${type}" for page "${text}"`,
});
}
const r = {
"@id": `pages:${uid}`, // eslint-disable-line @typescript-eslint/naming-convention
"@type": nodeType ?? "nodeSchema", // eslint-disable-line @typescript-eslint/naming-convention
title: text,
content: content as string,
modified: modified?.toJSON(),
created: date.toJSON(),
creator: displayName,
};
return r;
});
const nodeSet = new Set(pageData.map((n) => n.uid));
const rels = await getRelationData();
await updateExportProgress(1);
const relations = uniqJsonArray(
rels.filter((r) => nodeSet.has(r.source) && nodeSet.has(r.target)),
);
const relData = relations.map(({ relUid, source, target }) => ({
// no id yet, just a blank node
"@type": "relationInstance", // eslint-disable-line @typescript-eslint/naming-convention
predicate: `pages:${relUid}`,
source: `pages:${source}`,
destination: `pages:${target}`,
}));
return {
/* eslint-disable @typescript-eslint/naming-convention */
"@context": jsonLdContext(roamUrl),
"@id": roamUrl,
"prov:generatedAtTime": new Date().toISOString(),
"@graph": [...schemaData, ...nodes, ...relData],
/* eslint-enable @typescript-eslint/naming-convention */
};
};
6 changes: 2 additions & 4 deletions apps/roam/src/utils/supabaseContext.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import getCurrentUserEmail from "roamjs-components/queries/getCurrentUserEmail";
import getCurrentUserDisplayName from "roamjs-components/queries/getCurrentUserDisplayName";
import getPageUidByPageTitle from "roamjs-components/queries/getPageUidByPageTitle";
import getRoamUrl from "roamjs-components/dom/getRoamUrl";

import canonicalRoamUrl from "./canonicalRoamUrl";
import { DISCOURSE_CONFIG_PAGE_TITLE } from "~/utils/renderNodeConfigPage";
import getBlockProps from "~/utils/getBlockProps";
import setBlockProps from "~/utils/setBlockProps";
Expand All @@ -27,8 +27,6 @@ export type SupabaseContext = {

let _contextCache: SupabaseContext | null = null;

const ROAM_URL_PREFIX = "https://roamresearch.com/#/app/";

const getOrCreateSpacePassword = () => {
const settingsConfigPageUid = getPageUidByPageTitle(
DISCOURSE_CONFIG_PAGE_TITLE,
Expand Down Expand Up @@ -57,7 +55,7 @@ export const getSupabaseContext = async (): Promise<SupabaseContext | null> => {
const personEmail = getCurrentUserEmail();
const personName = getCurrentUserDisplayName();
const spaceName = window.roamAlphaAPI.graph.name;
const url = ROAM_URL_PREFIX + spaceName;
const url = canonicalRoamUrl(spaceName);
const platform: Platform = "Roam";
const spaceResult = await fetchOrCreateSpaceDirect({
password: spacePassword,
Expand Down
87 changes: 87 additions & 0 deletions apps/website/public/schema/dg_base.ttl
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix : <http://www.w3.org/2000/01/rdf-schema#> .
@prefix dc: <http://purl.org/dc/elements/1.1/> .
@prefix owl: <http://www.w3.org/2002/07/owl#> .
@prefix dgb: <https://discoursegraphs.com/schema/dg_base#> .

<https://discoursegraphs.com/schema/dg_base#>
dc:date "2025-12-22" ;
rdfs:comment "DiscourseGraph foundation vocabulary"@en ;
rdfs:label "DiscourseGraph foundation vocabulary"@en ;
owl:versionInfo "0 (tentative)" ;
a owl:Ontology.

# This is inspired by https://hyperknowledge.org/schemas/hyperknowledge_frames.ttl
# and topic mapping

dgb:NodeSchema
rdfs:subClassOf owl:Class;
rdfs:comment "Subclasses of DiscourseGraph nodes"@en .

dgb:Role
rdfs:subClassOf owl:ObjectProperty,
[a owl:Restriction; owl:onProperty rdfs:domain ; owl:allValuesFrom dgb:NodeSchema ],
[a owl:Restriction; owl:onProperty rdfs:range ; owl:allValuesFrom dgb:NodeSchema ];
rdfs:comment "A role within a node schema"@en .

dgb:RelationDef rdfs:subClassOf owl:ObjectProperty;
rdfs:comment "DiscourseGraph relations"@en.

dgb:RelationInstance rdfs:subClassOf rdf:Statement, dgb:NodeSchema,
[a owl:Restriction; owl:onProperty rdfs:predicate ; owl:allValuesFrom dgb:RelationDef ].

dgb:source a dgb:Role ;
rdfs:subPropertyOf rdf:subject ;
rdfs:domain dgb:RelationInstance ;
rdfs:range dgb:NodeSchema ;
rdfs:comment "The source of a binary relation"@en .

dgb:destination a dgb:Role ;
rdfs:subPropertyOf rdf:object ;
rdfs:domain dgb:RelationInstance ;
rdfs:range dgb:NodeSchema ;
rdfs:comment "The destination of a binary relation"@en .

dgb:textRefersToNode a owl:ObjectProperty;
rdfs:domain dgb:NodeSchema;
rdfs:range dgb:NodeSchema;
rdfs:comment "The text of a node refers to another node"@en .


# examples

# :x a dgb:NodeSchema .
# :y a dgb:NodeSchema .
# :x0 a :x.
# :y0 a :y.
# :r a dgb:NodeSchema.
# :x_r a dgb:Role ;
# rdfs:domain :r ;
# rdfs:range :x .

# :r0 a :r;
# :x_r :x0.

# :br a dgb:RelationDef;
# rdfs:domain :x;
# rdfs:range :y;

# :br0
# a dgb:RelationInstance;
# rdf:predicate :br ;
# dgb:source :x0 ;
# dgb:destination :y0 ;

# # This is "about" :x0 :br :y0;

# Note: we could also use punning, and define
# :br rdfs:subClassOf dgb:RelationInstance,
# [a owl:Restriction;
# owl:onProperty rdf:predicate ;
# owl:hasValue :br ].
# Then we can more simply state
# :br0
# a :br;
# dgb:source :x0 ;
# dgb:destination :y0 ;
Loading