Skip to content

Roam: Convert {{[[table]]}} blocks to Markdown pipe tables#505

Open
cristip73 wants to merge 1 commit intoobsidianmd:masterfrom
cristip73:roam-table-conversion
Open

Roam: Convert {{[[table]]}} blocks to Markdown pipe tables#505
cristip73 wants to merge 1 commit intoobsidianmd:masterfrom
cristip73:roam-table-conversion

Conversation

@cristip73
Copy link
Copy Markdown

Summary

Roam Research {{[[table]]}} blocks are currently not converted during
import — the table marker text is left as-is (the only handling was a
commented-out replaceAll at line 289), and table children become plain
bullet lists that lose all tabular structure in Obsidian.

This PR adds proper structural conversion of Roam tables into standard
Markdown pipe tables.

Partially addresses #180.

How it works

  • A new regex roamTableRe detects {{[[table]]}} and {{table}}
    markers (case-insensitive)
  • In jsonToMarkdown(), when a table block is detected, processing is
    delegated to a new convertRoamTable() method instead of the normal
    bullet/children recursion
  • convertRoamTable() walks each row's linear first-child chain to
    extract columns (Roam encodes table cells as nested first-children),
    scrubs Roam markup via the existing roamMarkupScrubber(), escapes
    pipe characters, pads uneven rows, and builds a standard pipe table

Edge cases handled

  • No children → empty output (table marker removed cleanly)
  • Uneven row lengths → padded with empty cells
  • Pipes in cell content → escaped as \|
  • Deep nesting (4+ columns) → linear chain walk handles any depth
  • Roam markup in cells → links, TODOs, highlights, dates all converted
  • Table inside nested outline → indent preserved on all table lines

Test plan

  • Build passes (npm run build)
  • Lint passes (npm run lint)
  • Import a Roam JSON export containing {{[[table]]}} blocks and
    verify pipe tables render correctly in Obsidian
  • Verify non-table blocks are unaffected

🤖 Generated with Claude Code

Previously table markers were stripped (commented-out code) and table
children rendered as plain bullet lists. This adds proper structural
conversion: each row's linear first-child chain becomes table columns,
with Roam markup scrubbed, pipes escaped, and uneven rows padded.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@tgrosinger tgrosinger left a comment

Choose a reason for hiding this comment

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

Thank you for submitting this improvement! Overall it looks great, just a few suggestions.

I also noticed when running the tests/roam/small-test-graph.json, the table is still indented under the bullet point, causing it to not render correctly. Can that be fixed here too?

Image

const binaryRegex = /https:\/\/firebasestorage(.*?)\?alt(.*?)/;

const blockRefRegex = /(?<=\(\()\b(.*?)\b(?=\)\))/g;
const roamTableRe = /^\{\{(\[\[)?table(\]\])?\}\}$/i;
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.

The current regex has two independent optional groups:

/^\{\{(\[\[)?table(\]\])?\}\}$/i
       ^^^^        ^^^^
       group 1     group 2

Each ? makes its group independently optional, producing 4 possible matches:

Group 1 Group 2 Matches Valid Roam?
absent absent {{table}} yes
present present {{[[table]]}} yes
present absent {{[[table}} no
absent present {{table]]}} no

The proposed fix ties the brackets together as a single alternation:

/^\{\{(\[\[table\]\]|table)\}\}$/i

This only matches {{[[table]]}} or {{table}} -- brackets are always balanced.

Suggested change
const roamTableRe = /^\{\{(\[\[)?table(\]\])?\}\}$/i;
const roamTableRe = /^\{\{(\[\[table\]\]|table)\}\}$/i;

while (current) {
const scrubbed = await this.roamMarkupScrubber(graphFolder, attachmentsFolder, current.string || '');
cells.push(scrubbed.replace(/\|/g, '\\|'));
current = current.children?.[0];
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.

If a Roam table cell has multiple children (e.g., user added sub-items), all but the first are silently discarded with no warning. This could cause data loss without any indication to the user. Consider logging a warning when current.children.length > 1.

const prefix = json.heading ? '#'.repeat(json.heading) + ' ' : '';
const scrubbed = await this.roamMarkupScrubber(graphFolder, attachmentsFolder, json.string);
markdown.push(`${isChild ? indent + '* ' : indent}${prefix}${scrubbed}`);
if ('string' in json && json.string && roamTableRe.test(json.string.trim()) && json.children) {
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.

  • children: [] (truthy) enters convertRoamTable, returns '' -- table marker vanishes silently
  • children: undefined falls to the else branch and renders raw {{[[table]]}} in output

Consider having the table-detection guard not require json.children, and letting convertRoamTable handle all cases uniformly.

// Build pipe table
const lines: string[] = [];
// Header row
lines.push(indent + '| ' + tableData[0].join(' | ') + ' |');
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.

The formatting of the various table sections could be unified and simplified by inserting the separator row after the header:

const separator = tableData[0].map(() => '---');
tableData.splice(1, 0, separator);

const lines = tableData.map(row => indent + '| ' + row.join(' | ') + ' |');

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants