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
141 changes: 141 additions & 0 deletions apps/roam/src/utils/exportUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import type { Result } from "roamjs-components/types/query-builder";
import { PullBlock, TreeNode, ViewType } from "roamjs-components/types";
import type { DiscourseNode } from "./getDiscourseNodes";
import matchDiscourseNode from "./matchDiscourseNode";

type DiscourseExportResult = Result & { type: string };

export const uniqJsonArray = <T extends Record<string, unknown>>(arr: T[]) =>
Array.from(
new Set(
arr.map((r) =>
JSON.stringify(
Object.entries(r).sort(([k], [k2]) => k.localeCompare(k2)),
),
),
),
).map((entries) => Object.fromEntries(JSON.parse(entries))) as T[];
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unsafe argument of type any assigned to a parameter of type Iterable<readonly [PropertyKey, unknown]>.eslint@typescript-eslint/no-unsafe-argument


export const getPageData = ({
results,
allNodes,
isExportDiscourseGraph,
}: {
results: Result[];
allNodes: DiscourseNode[];
isExportDiscourseGraph?: boolean;
}): (Result & { type: string })[] => {
if (isExportDiscourseGraph) return results as DiscourseExportResult[];

const matchedTexts = new Set();
const mappedResults = results.flatMap((r) =>
Object.keys(r)
.filter((k) => k.endsWith(`-uid`) && k !== "text-uid")
.map((k) => ({
...r,
text: r[k.slice(0, -4)].toString(),
uid: r[k] as string,
}))
.concat({
text: r.text,
uid: r.uid,
}),
);
return allNodes.flatMap((n) =>
mappedResults
.filter(({ text }) => {
if (!text) return false;
if (matchedTexts.has(text)) return false;
const isMatch = matchDiscourseNode({ title: text, ...n });
if (isMatch) matchedTexts.add(text);

return isMatch;
})
.map((node) => ({ ...node, type: n.text })),
);
};

const getContentFromNodes = ({
title,
allNodes,
}: {
title: string;
allNodes: DiscourseNode[];
}) => {
const nodeFormat = allNodes.find((a) =>
matchDiscourseNode({ title, ...a }),
)?.format;
if (!nodeFormat) return title;
const regex = new RegExp(
`^${nodeFormat
.replace(/[[\]\\^$.|?*+()]/g, "\\$&")
.replace("{content}", "(.*?)")
.replace(/{[^}]+}/g, "(?:.*?)")}$`,
);
return regex.exec(title)?.[1] || title;
Comment on lines +68 to +75
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot Dec 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# Find DiscourseNode type definition and format field
rg -n "type DiscourseNode|interface DiscourseNode" -A 20

Repository: DiscourseGraphs/discourse-graph

Length of output: 22213


🏁 Script executed:

# Check how format patterns are created/validated in the codebase
rg -n "\.format\s*=" apps/roam/src -B 2 -A 2

Repository: DiscourseGraphs/discourse-graph

Length of output: 57


🏁 Script executed:

# Look at the full context of exportUtils.ts around lines 79-87
cat -n apps/roam/src/utils/exportUtils.ts | head -100 | tail -50

Repository: DiscourseGraphs/discourse-graph

Length of output: 1584


🏁 Script executed:

# Check if there's any validation of format field when it's set/saved
rg -n "format" apps/roam/src -l | head -20

Repository: DiscourseGraphs/discourse-graph

Length of output: 933


🏁 Script executed:

# Search for format field validation or sanitization
rg -n "format.*validate|validateFormat|format.*check" apps/roam/src -B 2 -A 2

Repository: DiscourseGraphs/discourse-graph

Length of output: 1338


🏁 Script executed:

# Check DiscourseNodeConfigPanel to see format field editing
cat -n apps/roam/src/components/settings/DiscourseNodeConfigPanel.tsx | head -200

Repository: DiscourseGraphs/discourse-graph

Length of output: 8237


🏁 Script executed:

# Search for other uses of format field and regex construction
rg -n "getContentFromNodes|format.*RegExp|new RegExp" apps/roam/src/utils

Repository: DiscourseGraphs/discourse-graph

Length of output: 1790


🏁 Script executed:

# Check if there's any try-catch or error handling for regex in similar code
rg -n "try.*RegExp|RegExp.*catch" apps/roam/src -B 2 -A 2

Repository: DiscourseGraphs/discourse-graph

Length of output: 57


🏁 Script executed:

# Find the validate function in NodeConfig
rg -n "const validate|function validate" apps/roam/src/components/settings/NodeConfig.tsx -B 5 -A 10

Repository: DiscourseGraphs/discourse-graph

Length of output: 506


🏁 Script executed:

# Check getDiscourseNodeFormatExpression since it also processes format
cat -n apps/roam/src/utils/getDiscourseNodeFormatExpression.ts

Repository: DiscourseGraphs/discourse-graph

Length of output: 544


🏁 Script executed:

# Look for format validation logic more broadly
rg -n "validate.*format|format.*valid" apps/roam/src -B 2 -A 5

Repository: DiscourseGraphs/discourse-graph

Length of output: 2231


🏁 Script executed:

# Check if format field has any UI constraints or warnings
cat -n apps/roam/src/components/settings/NodeConfig.tsx | grep -A 30 -B 5 "format"

Repository: DiscourseGraphs/discourse-graph

Length of output: 7712


Use the proper format escaping function to prevent ReDoS.

The getContentFromNodes function at lines 79-87 constructs a regex from the user-editable nodeFormat field with incomplete escaping. Unlike getDiscourseNodeFormatExpression which properly escapes regex metacharacters ([, ], ?, ., +), this code only escapes brackets, leaving other quantifiers unescaped. A malformed format like (a+)+b would create a regex with nested quantifiers, causing exponential backtracking.

Rather than adding a try-catch, replace the custom escaping with getDiscourseNodeFormatExpression, which already handles this safely.

🧰 Tools
🪛 ast-grep (0.40.3)

[warning] 79-85: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns.
Context: new RegExp(
^${nodeFormat .replace(/\[/g, "\\[") .replace(/]/g, "\\]") .replace("{content}", "(.*?)") .replace(/{[^}]+}/g, "(?:.*?)")}$,
)
Note: [CWE-1333] Inefficient Regular Expression Complexity [REFERENCES]
- https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS
- https://cwe.mitre.org/data/definitions/1333.html

(regexp-from-variable)

🤖 Prompt for AI Agents
In apps/roam/src/utils/exportUtils.ts around lines 79-87, the code builds a
RegExp from user-editable nodeFormat but only escapes brackets, leaving other
metacharacters and causing ReDoS risk; replace this custom escaping and regex
construction with the existing getDiscourseNodeFormatExpression utility that
properly escapes all regex metacharacters, then use the returned expression to
exec against title (preserve the same fallback to title when no match). Ensure
you import getDiscourseNodeFormatExpression if not already imported and remove
the manual replace chain so the format is safely escaped before creating/using
the regex.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, methodology question: On the one hand, we are saying we should have only one logical change per PR. I tried to make this PR do only the refactor, nothing else, so that you could see it as innocuous and focus on the substance in the other PRs.
On the other hand, moving code draws the attention to issues with previously existing code. Coderabbit pointed this out in its comments to 1032, and I made (a subset of) this fix in that PR originally (a subset because I was following pattern) and then moved it to 1213 instead of 1211 so that 1211 would not have unrelated changes.
(Accessorily, maybe we should do the full fix in both places.)
So the question: When we do a refactoring, and try to make it into a single PR for clarity, is fixing issues with old code part of the same task or an extra task? Honestly, I think it depends; I have argued earlier that it's too much trouble to revert anything done automatically by the tooling. But it's also true that it muddies the refactoring.
I don't care that much either way, but I'm curious about which rule I'm expected to follow here.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@maparent, this is a great question that highlights a real tension in software engineering practice. Here's my perspective on the tradeoffs:

Arguments for keeping refactoring PRs pure:

  • Easier to review and understand the structural changes
  • Clearer git history with distinct "what" and "why"
  • Can be reverted independently if needed
  • Reduces cognitive load for reviewers

Arguments for fixing issues during refactoring:

  • Avoids touching the same code twice
  • Issues are fresh in mind when you're already working with the code
  • More efficient overall (fewer PRs, fewer context switches)
  • Prevents shipping known issues in the interim

Practical heuristics I'd suggest:

  1. Security or critical bugs: Fix immediately in the same PR. ReDoS vulnerabilities like the one I flagged fall into this category - if you're moving the code anyway, fixing it is the responsible choice.

  2. Minor improvements or style issues: Keep separate unless trivial (e.g., automated formatting).

  3. Structural issues revealed by the refactor: This is where judgment matters. If the refactoring exposes that the code fundamentally doesn't work right, fixing it in the same PR makes sense. If it's just "this could be better," separate PR.

  4. Team conventions: Your retrieved learning shows you "take a focused approach to PR scope," which is a good default. However, pragmatism should win when the overhead of separation outweighs the benefits.

For this specific case: The ReDoS issue is a security concern that exists in code you're moving. I'd recommend fixing it in PR 1213 (or this PR if you consolidate) rather than leaving it unfixed. You could also note in the PR description "refactor + fix ReDoS in getContentFromNodes" so reviewers know to expect both.

What does your team prefer?


🧠 Learnings used
Learnt from: sid597
Repo: DiscourseGraphs/discourse-graph PR: 232
File: apps/roam/src/utils/getAllDiscourseNodesSince.ts:18-31
Timestamp: 2025-06-22T10:40:52.752Z
Learning: In apps/roam/src/utils/getAllDiscourseNodesSince.ts, the user confirmed that querying for `?title` with `:node/title` and mapping it to the `text` field in the DiscourseGraphContent type is the correct implementation for retrieving discourse node content from Roam Research, despite it appearing to query page titles rather than block text content.

Learnt from: maparent
Repo: DiscourseGraphs/discourse-graph PR: 559
File: apps/roam/src/utils/findDiscourseNode.ts:37-39
Timestamp: 2025-12-07T20:54:20.007Z
Learning: In apps/roam/src/utils/findDiscourseNode.ts, the function findDiscourseNodeByTitleAndUid accepts both uid and title parameters where uid is primarily used for cache access (as the cache key) while title is used for the actual matching via matchDiscourseNode. This design reflects the pattern where downstream, the uid is mostly used to fetch the title, so the function caches by uid but matches by title.

Learnt from: maparent
Repo: DiscourseGraphs/discourse-graph PR: 220
File: apps/roam/src/utils/conceptConversion.ts:42-56
Timestamp: 2025-06-17T23:37:45.289Z
Learning: In the DiscourseNode interface from apps/roam/src/utils/getDiscourseNodes.ts, the field `type` serves as the unique identifier field, not a type classification field. The interface has no `uid` or `id` field, making `node.type` the correct field to use for UID-related operations.

Learnt from: sid597
Repo: DiscourseGraphs/discourse-graph PR: 372
File: apps/roam/src/components/DiscourseNodeMenu.tsx:116-116
Timestamp: 2025-08-25T15:53:21.799Z
Learning: In apps/roam/src/components/DiscourseNodeMenu.tsx, when handling tag insertion, multiple leading hashtags (like ##foo) should be preserved as they represent user intent, not normalized to a single hashtag. The current regex /^#/ is correct as it only removes one leading # before adding one back, maintaining any additional hashtags the user intended.

Learnt from: maparent
Repo: DiscourseGraphs/discourse-graph PR: 165
File: packages/database/supabase/migrations/20250504202930_content_tables.sql:37-45
Timestamp: 2025-05-20T03:11:07.917Z
Learning: When reviewing SQL migrations in the discourse-graph codebase, understand that historical migration files should not be modified once they're applied. Instead, issues should be fixed in subsequent migrations or schema definition files. This is why maparent indicated "migration, not changing. corrected in schema."

Learnt from: maparent
Repo: DiscourseGraphs/discourse-graph PR: 0
File: :0-0
Timestamp: 2025-08-30T17:03:26.927Z
Learning: For maparent: Takes a focused approach to PR scope - acknowledges good suggestions that are out of scope for current PR rather than expanding scope.

Learnt from: maparent
Repo: DiscourseGraphs/discourse-graph PR: 165
File: packages/database/supabase/migrations/20250504202930_content_tables.sql:37-45
Timestamp: 2025-05-20T03:11:07.917Z
Learning: When reviewing SQL migrations in this codebase, understand that historical migration files should not be modified once they're applied. Instead, issues should be fixed in subsequent migrations or schema definition files. This is why maparent indicated "migration, not changing. corrected in schema."

Learnt from: CR
Repo: DiscourseGraphs/discourse-graph PR: 0
File: .cursor/rules/roam.mdc:0-0
Timestamp: 2025-11-25T00:52:41.934Z
Learning: Applies to apps/roam/**/*.{ts,tsx,js,jsx} : Use Roam Depot/Extension API docs from https://roamresearch.com/#/app/developer-documentation/page/y31lhjIqU when implementing extension functionality

Learnt from: CR
Repo: DiscourseGraphs/discourse-graph PR: 0
File: .cursor/rules/roam.mdc:0-0
Timestamp: 2025-11-25T00:52:41.934Z
Learning: Applies to apps/roam/**/*.{ts,tsx,js,jsx} : Use the roamAlphaApi docs from https://roamresearch.com/#/app/developer-documentation/page/tIaOPdXCj when implementing Roam functionality

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry I forgot to tag @mdroidian that question was addressed to him.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But @CodeRabbit good point about the security implications!

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@maparent, you're welcome! Security issues like ReDoS can be subtle but have real impact, especially in user-facing tools where format strings are configurable. Since you're already touching this code in the refactor, addressing it now makes sense.

If you do decide to fix it in this PR or PR 1213, using getDiscourseNodeFormatExpression (which already has the proper escaping logic) should make the fix straightforward. Let me know if you'd like me to review the fix once it's in place!


🧠 Learnings used
Learnt from: sid597
Repo: DiscourseGraphs/discourse-graph PR: 232
File: apps/roam/src/utils/getAllDiscourseNodesSince.ts:18-31
Timestamp: 2025-06-22T10:40:52.752Z
Learning: In apps/roam/src/utils/getAllDiscourseNodesSince.ts, the user confirmed that querying for `?title` with `:node/title` and mapping it to the `text` field in the DiscourseGraphContent type is the correct implementation for retrieving discourse node content from Roam Research, despite it appearing to query page titles rather than block text content.

Learnt from: maparent
Repo: DiscourseGraphs/discourse-graph PR: 559
File: apps/roam/src/utils/findDiscourseNode.ts:37-39
Timestamp: 2025-12-07T20:54:20.007Z
Learning: In apps/roam/src/utils/findDiscourseNode.ts, the function findDiscourseNodeByTitleAndUid accepts both uid and title parameters where uid is primarily used for cache access (as the cache key) while title is used for the actual matching via matchDiscourseNode. This design reflects the pattern where downstream, the uid is mostly used to fetch the title, so the function caches by uid but matches by title.

Learnt from: maparent
Repo: DiscourseGraphs/discourse-graph PR: 220
File: apps/roam/src/utils/conceptConversion.ts:42-56
Timestamp: 2025-06-17T23:37:45.289Z
Learning: In the DiscourseNode interface from apps/roam/src/utils/getDiscourseNodes.ts, the field `type` serves as the unique identifier field, not a type classification field. The interface has no `uid` or `id` field, making `node.type` the correct field to use for UID-related operations.

Learnt from: sid597
Repo: DiscourseGraphs/discourse-graph PR: 372
File: apps/roam/src/components/DiscourseNodeMenu.tsx:116-116
Timestamp: 2025-08-25T15:53:21.799Z
Learning: In apps/roam/src/components/DiscourseNodeMenu.tsx, when handling tag insertion, multiple leading hashtags (like ##foo) should be preserved as they represent user intent, not normalized to a single hashtag. The current regex /^#/ is correct as it only removes one leading # before adding one back, maintaining any additional hashtags the user intended.

Learnt from: maparent
Repo: DiscourseGraphs/discourse-graph PR: 0
File: :0-0
Timestamp: 2025-08-30T17:03:26.927Z
Learning: For maparent: Takes a focused approach to PR scope - acknowledges good suggestions that are out of scope for current PR rather than expanding scope.

Learnt from: CR
Repo: DiscourseGraphs/discourse-graph PR: 0
File: .cursor/rules/roam.mdc:0-0
Timestamp: 2025-11-25T00:52:41.934Z
Learning: Applies to apps/roam/**/*.{ts,tsx,js,jsx} : Use Roam Depot/Extension API docs from https://roamresearch.com/#/app/developer-documentation/page/y31lhjIqU when implementing extension functionality

Learnt from: CR
Repo: DiscourseGraphs/discourse-graph PR: 0
File: .cursor/rules/roam.mdc:0-0
Timestamp: 2025-11-25T00:52:41.934Z
Learning: Applies to apps/roam/**/*.{ts,tsx,js,jsx} : Use the roamAlphaApi docs from https://roamresearch.com/#/app/developer-documentation/page/tIaOPdXCj when implementing Roam functionality

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry I forgot to tag @mdroidian that question was addressed to him.

This is probably best decided on a case by case basis depending on severity, effort, number of changes, etc.

Use your best judgement and just be sure to document the rationale with an inline GitHub comment.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok. Here's what I'm going to do:
I will correct the security issue locally, because I agree that is important.
I think we should make this stated policy.
I will do a few minor local fixes here (tbh I had already added removing a spurious async marker) because they're low impact.
I think the issue with maxFilenameLength calls for a solution that is too unrelated to this code, and i will make it separate.
I want to go on record that it's a productivity blocker that we cannot do a refactor without taking ownership of fixing minor problems in the code that is moved.
It means we allow coderabbit to dictate priority of which minor bugs we're going to fix. There's many of them, and we should be more systematic about choosing when we fix them.
OTH, I also acknowledge that, if we do not fix them opportunistically, they will be forgotten.
Actually, my favoured approach would be to fix them opportunistically in the same PR, but that would require us to relax the rule about single-purpose PRs, and that means you would have to give up on checking every small change. I suggest that our current process is too heavy, with a single bottleneck, to ever have the time to handle minor fixes.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Feel free to bring this up at our next 1:1 if you'd like to discuss further.

};

export const getFilename = ({
title = "",
maxFilenameLength,
simplifiedFilename,
allNodes,
removeSpecialCharacters,
extension = ".md",
}: {
title?: string;
maxFilenameLength: number;
simplifiedFilename: boolean;
allNodes: DiscourseNode[];
removeSpecialCharacters: boolean;
extension?: string;
}) => {
const baseName = simplifiedFilename
? getContentFromNodes({ title, allNodes })
: title;
const name = `${
removeSpecialCharacters
? baseName.replace(/[<>:"/\\|\?*[\]]/g, "")
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unnecessary escape character: ?.eslintno-useless-escape

: baseName
}${extension}`;

return name.length > maxFilenameLength
? `${name.substring(
0,
Math.ceil((maxFilenameLength - 3) / 2),
)}...${name.slice(-Math.floor((maxFilenameLength - 3) / 2))}`
: name;
Comment on lines +102 to +107
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot Dec 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Edge case: Very small maxFilenameLength values.

If maxFilenameLength is less than or equal to 3, the calculation (maxFilenameLength - 3) / 2 could result in 0 or negative values, potentially producing unexpected output. Consider adding a guard or minimum value check.

🔎 Suggested guard
+  const effectiveMaxLength = Math.max(maxFilenameLength, 4);
   return name.length > maxFilenameLength
     ? `${name.substring(
         0,
-        Math.ceil((maxFilenameLength - 3) / 2),
-      )}...${name.slice(-Math.floor((maxFilenameLength - 3) / 2))}`
+        Math.ceil((effectiveMaxLength - 3) / 2),
+      )}...${name.slice(-Math.floor((effectiveMaxLength - 3) / 2))}`
     : name;

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In apps/roam/src/utils/exportUtils.ts around lines 114 to 119, the truncation
logic can produce incorrect slices when maxFilenameLength <= 3; add a guard that
handles very small maxFilenameLength values first (e.g., if maxFilenameLength <=
3) and return a string of length maxFilenameLength composed of dots or a
truncated ellipsis, otherwise compute left/right slice sizes using Math.max(0,
Math.floor/ceil((maxFilenameLength - 3) / 2)) to avoid negative values and then
perform the substring + '...' + slice as before.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Valid concern, but I think that should be done as a guard on the setting, not an adjustment downstream.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@maparent That makes sense! Validating maxFilenameLength at the settings level (e.g., ensuring it's always >= 4 or some reasonable minimum) is a cleaner approach and prevents invalid values from propagating through the system. This keeps the utility function simpler and enforces the constraint where the configuration is defined.


🧠 Learnings used
Learnt from: sid597
Repo: DiscourseGraphs/discourse-graph PR: 232
File: apps/roam/src/utils/getAllDiscourseNodesSince.ts:18-31
Timestamp: 2025-06-22T10:40:52.752Z
Learning: In apps/roam/src/utils/getAllDiscourseNodesSince.ts, the user confirmed that querying for `?title` with `:node/title` and mapping it to the `text` field in the DiscourseGraphContent type is the correct implementation for retrieving discourse node content from Roam Research, despite it appearing to query page titles rather than block text content.

Learnt from: maparent
Repo: DiscourseGraphs/discourse-graph PR: 559
File: apps/roam/src/utils/findDiscourseNode.ts:37-39
Timestamp: 2025-12-07T20:54:20.007Z
Learning: In apps/roam/src/utils/findDiscourseNode.ts, the function findDiscourseNodeByTitleAndUid accepts both uid and title parameters where uid is primarily used for cache access (as the cache key) while title is used for the actual matching via matchDiscourseNode. This design reflects the pattern where downstream, the uid is mostly used to fetch the title, so the function caches by uid but matches by title.

Learnt from: CR
Repo: DiscourseGraphs/discourse-graph PR: 0
File: .cursor/rules/roam.mdc:0-0
Timestamp: 2025-11-25T00:52:41.934Z
Learning: Applies to apps/roam/**/*.{ts,tsx,js,jsx} : Use Roam Depot/Extension API docs from https://roamresearch.com/#/app/developer-documentation/page/y31lhjIqU when implementing extension functionality

Learnt from: CR
Repo: DiscourseGraphs/discourse-graph PR: 0
File: .cursor/rules/roam.mdc:0-0
Timestamp: 2025-11-25T00:52:41.934Z
Learning: Applies to apps/roam/**/*.{ts,tsx,js,jsx} : Use the roamAlphaApi docs from https://roamresearch.com/#/app/developer-documentation/page/tIaOPdXCj when implementing Roam functionality

};

export const toLink = (filename: string, uid: string, linkType: string) => {
const extensionRemoved = filename.replace(/\.\w+$/, "");
if (linkType === "wikilinks") return `[[${extensionRemoved}]]`;
if (linkType === "alias") return `[${filename}](${filename})`;
if (linkType === "roam url")
return `[${extensionRemoved}](https://roamresearch.com/#/app/${window.roamAlphaAPI.graph.name}/page/${uid})`;
return filename;
};

export const pullBlockToTreeNode = (
n: PullBlock,
v: `:${ViewType}`,
): TreeNode => ({
text: n[":block/string"] || n[":node/title"] || "",
open: typeof n[":block/open"] === "undefined" ? true : n[":block/open"],
order: n[":block/order"] || 0,
uid: n[":block/uid"] || "",
heading: n[":block/heading"] || 0,
viewType: (n[":children/view-type"] || v).slice(1) as ViewType,
editTime: new Date(n[":edit/time"] || 0),
props: { imageResize: {}, iframe: {} },
textAlign: n[":block/text-align"] || "left",
children: (n[":block/children"] || [])
.sort(({ [":block/order"]: a = 0 }, { [":block/order"]: b = 0 }) => a - b)
.map((r) => pullBlockToTreeNode(r, n[":children/view-type"] || v)),
parents: (n[":block/parents"] || []).map((p) => p[":db/id"] || 0),
});

export const collectUids = (t: TreeNode): string[] => [
t.uid,
...t.children.flatMap(collectUids),
];
Loading