|
| 1 | +import { toMarkdown } from "./pageToMarkdown"; |
| 2 | +import { type RoamDiscourseNodeData } from "./getAllDiscourseNodesSince"; |
| 3 | +import { type DiscourseNode } from "./getDiscourseNodes"; |
| 4 | +import getFullTreeByParentUid from "roamjs-components/queries/getFullTreeByParentUid"; |
| 5 | +import getPageViewType from "roamjs-components/queries/getPageViewType"; |
| 6 | +import type { TreeNode, ViewType } from "roamjs-components/types"; |
| 7 | +import type { LocalContentDataInput } from "@repo/database/inputTypes"; |
| 8 | + |
| 9 | +/** |
| 10 | + * Builds the `full` cross-app content variant for Roam discourse nodes. |
| 11 | + * |
| 12 | + * Per the shared cross-app node contract (ENG-1847, |
| 13 | + * `@repo/database/crossAppNodeContract`), every shared node must persist a |
| 14 | + * `full` variant: a self-sufficient markdown body the destination app can |
| 15 | + * materialize without querying Roam. Roam previously emitted only the `direct` |
| 16 | + * title content; this fills that gap (ENG-1848, F2/F3). The body reuses the |
| 17 | + * existing `toMarkdown` page serializer with block-refs and embeds inlined for |
| 18 | + * self-sufficiency, prefixed with the node title as an H1 — matching the |
| 19 | + * contract's Roam fixture. Known MVP0 markdown-fidelity limits live on F3. |
| 20 | + */ |
| 21 | + |
| 22 | +const FULL_MARKDOWN_OPTS = { |
| 23 | + refs: true, |
| 24 | + embeds: true, |
| 25 | + simplifiedFilename: false, |
| 26 | + removeSpecialCharacters: false, |
| 27 | + maxFilenameLength: 64, |
| 28 | + linkType: "alias", |
| 29 | + allNodes: [] as DiscourseNode[], |
| 30 | +}; |
| 31 | + |
| 32 | +export const buildFullMarkdown = ({ |
| 33 | + title, |
| 34 | + blocks, |
| 35 | + viewType = "bullet", |
| 36 | +}: { |
| 37 | + title: string; |
| 38 | + blocks: TreeNode[]; |
| 39 | + viewType?: ViewType; |
| 40 | +}): string => { |
| 41 | + const body = blocks |
| 42 | + .filter((block) => !!block.text || !!block.children?.length) |
| 43 | + .map((block) => |
| 44 | + toMarkdown({ c: block, v: viewType, i: 0, opts: FULL_MARKDOWN_OPTS }), |
| 45 | + ) |
| 46 | + .join("\n") |
| 47 | + .trim(); |
| 48 | + return body ? `# ${title}\n\n${body}\n` : `# ${title}\n`; |
| 49 | +}; |
| 50 | + |
| 51 | +export const convertRoamNodeToFullContent = ({ |
| 52 | + nodes, |
| 53 | +}: { |
| 54 | + nodes: RoamDiscourseNodeData[]; |
| 55 | +}): LocalContentDataInput[] => |
| 56 | + nodes.flatMap((node) => { |
| 57 | + try { |
| 58 | + const title = node.node_title ?? node.text; |
| 59 | + const blocks = getFullTreeByParentUid(node.source_local_id).children; |
| 60 | + const viewType = getPageViewType(title) || "bullet"; |
| 61 | + return [ |
| 62 | + { |
| 63 | + author_local_id: node.author_local_id, |
| 64 | + source_local_id: node.source_local_id, |
| 65 | + created: new Date(node.created || Date.now()).toISOString(), |
| 66 | + last_modified: new Date( |
| 67 | + node.last_modified || Date.now(), |
| 68 | + ).toISOString(), |
| 69 | + text: buildFullMarkdown({ title, blocks, viewType }), |
| 70 | + variant: "full", |
| 71 | + scale: "document", |
| 72 | + }, |
| 73 | + ]; |
| 74 | + } catch (error) { |
| 75 | + console.error( |
| 76 | + `convertRoamNodeToFullContent: failed to build full markdown for ${node.source_local_id}:`, |
| 77 | + error, |
| 78 | + ); |
| 79 | + return []; |
| 80 | + } |
| 81 | + }); |
0 commit comments