diff --git a/apps/roam/src/utils/jsonld.ts b/apps/roam/src/utils/jsonld.ts index 9ce326880..3fa4bd102 100644 --- a/apps/roam/src/utils/jsonld.ts +++ b/apps/roam/src/utils/jsonld.ts @@ -108,10 +108,12 @@ export const getJsonLdData = async ({ Record | Record[]> > => { const roamUrl = canonicalRoamUrl(); + const roamUrlRe = new RegExp(roamUrl + "/page/([\\w\\d-]{9,10})\\b", "g"); const getRelationData = () => getRelationDataUtil({ allRelations, nodeLabelByType }); await updateExportProgress(0); const pageData = getPageData({ results, allNodes }); + const nodeUids = new Set(pageData.map((p) => p.uid)); const numPages = pageData.length + allNodes.length; let numTreatedPages = 0; const settings = { @@ -138,6 +140,8 @@ export const getJsonLdData = async ({ ...settings, allNodes, linkType: "roam url", + blockAnchors: true, + blockRefsAsLinks: true, }); page.content = r.content; numTreatedPages += 1; @@ -153,6 +157,11 @@ export const getJsonLdData = async ({ error: `Unknown node type "${type}" for page "${text}"`, }); } + const textRefersToNode = [ + ...new Set([...(content as string).matchAll(roamUrlRe)].map((r) => r[1])), + ] + .filter((r) => nodeUids.has(r)) + .map((r) => `pages:${r}`); const r = { "@id": `pages:${uid}`, // eslint-disable-line @typescript-eslint/naming-convention "@type": nodeType ?? "nodeSchema", // eslint-disable-line @typescript-eslint/naming-convention @@ -162,6 +171,7 @@ export const getJsonLdData = async ({ created: date.toJSON(), creator: displayName, }; + if (textRefersToNode.length > 0) return { ...r, textRefersToNode }; return r; }); const nodeSet = new Set(pageData.map((n) => n.uid)); diff --git a/apps/roam/src/utils/pageToMarkdown.ts b/apps/roam/src/utils/pageToMarkdown.ts index 4dc760631..06fcb11f4 100644 --- a/apps/roam/src/utils/pageToMarkdown.ts +++ b/apps/roam/src/utils/pageToMarkdown.ts @@ -1,5 +1,6 @@ import { BLOCK_REF_REGEX } from "roamjs-components/dom/constants"; import normalizePageTitle from "roamjs-components/queries/normalizePageTitle"; +import getPageUidByBlockUid from "roamjs-components/queries/getPageUidByBlockUid"; import getTextByBlockUid from "roamjs-components/queries/getTextByBlockUid"; import getPageMetadata from "./getPageMetadata"; import getPageViewType from "roamjs-components/queries/getPageViewType"; @@ -20,6 +21,7 @@ import { pullBlockToTreeNode, collectUids, } from "./exportUtils"; +import getPageUidByPageTitle from "roamjs-components/queries/getPageUidByPageTitle"; const MATCHES_NONE = /$.+^/; @@ -127,6 +129,8 @@ export const toMarkdown = ({ removeSpecialCharacters: boolean; linkType: string; flatten?: boolean; + blockRefsAsLinks?: boolean; + blockAnchors?: boolean; }; }): string => { const { @@ -138,21 +142,37 @@ export const toMarkdown = ({ removeSpecialCharacters, linkType, flatten = false, + blockRefsAsLinks = false, + blockAnchors = false, } = opts; const processedText = c.text - .replace(embeds ? EMBED_REGEX : MATCHES_NONE, (_, blockUid) => { + .replace(embeds ? EMBED_REGEX : MATCHES_NONE, (_, blockUid: string) => { const reference = getFullTreeByParentUid(blockUid); return toMarkdown({ c: reference, i, v, opts }); }) - .replace(embeds ? EMBED_CHILDREN_REGEX : MATCHES_NONE, (_, blockUid) => { - const reference = getFullTreeByParentUid(blockUid); - return reference.children - .map((child) => toMarkdown({ c: child, i, v, opts })) - .join("\n"); - }) - .replace(refs ? BLOCK_REF_REGEX : MATCHES_NONE, (_, blockUid) => { - const reference = getTextByBlockUid(blockUid); - return reference || blockUid; + .replace( + embeds ? EMBED_CHILDREN_REGEX : MATCHES_NONE, + (_, blockUid: string) => { + const reference = getFullTreeByParentUid(blockUid); + return reference.children + .map((child) => toMarkdown({ c: child, i, v, opts })) + .join("\n"); + }, + ) + .replace(refs ? BLOCK_REF_REGEX : MATCHES_NONE, (_, blockUid: string) => { + const reference = getTextByBlockUid(blockUid) || blockUid; + + if (blockRefsAsLinks) { + const pageUid = blockAnchors + ? getPageUidByBlockUid(blockUid) + : blockUid; + if (pageUid === blockUid || pageUid === "") + return toLink(reference, blockUid, linkType); + else + // Note that the roam anchor follows a more complex pattern + // Here using the block anchor as target for internal export consistency + return toLink(reference, `${pageUid}#block-${blockUid}`, linkType); + } else return reference; }) .replace(/{{\[\[TODO\]\]}}/g, v === "bullet" ? "[ ]" : "- [ ]") .replace(/{{\[\[DONE\]\]}}/g, v === "bullet" ? "[x]" : "- [x]") @@ -160,13 +180,15 @@ export const toMarkdown = ({ .replace(/(? { if (s.name === "match") { + const pageUid = getPageUidByPageTitle(s.value); + if (pageUid.length > 0) return toLink(s.value, pageUid, linkType); const name = getFilename({ title: s.value, allNodes, @@ -205,8 +227,8 @@ export const toMarkdown = ({ }) .join(""); const lineBreak = v === "document" ? "\n" : ""; - - return `${indentation}${viewTypePrefix}${headingPrefix}${finalProcessedText}${lineBreak}${childrenMarkdown}`; + const blockAnchor = blockAnchors ? `{#block-${c.uid}}` : ""; + return `${indentation}${viewTypePrefix}${headingPrefix}${blockAnchor}${finalProcessedText}${lineBreak}${childrenMarkdown}`; }; export const pageToMarkdown = async ( @@ -222,6 +244,8 @@ export const pageToMarkdown = async ( maxFilenameLength, removeSpecialCharacters, linkType, + blockRefsAsLinks = false, + blockAnchors = false, }: { includeDiscourseContext: boolean; appendRefNodeContext: boolean; @@ -233,6 +257,8 @@ export const pageToMarkdown = async ( maxFilenameLength: number; removeSpecialCharacters: boolean; linkType: string; + blockRefsAsLinks?: boolean; + blockAnchors?: boolean; }, ): Promise<{ title: string; content: string; uids: Set }> => { const v = getPageViewType(text) || "bullet"; @@ -287,6 +313,8 @@ export const pageToMarkdown = async ( maxFilenameLength, removeSpecialCharacters, linkType, + blockAnchors, + blockRefsAsLinks, }, }), )