Skip to content

Commit a353ce8

Browse files
committed
Add markdown table support
2 parents c663aab + 04f09a9 commit a353ce8

35 files changed

Lines changed: 1184 additions & 2 deletions

android-load-example-notes.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
#!/bin/sh -v
22
# To set up the android emulator with some data
33
adb push example-notes /storage/self/primary/Documents/mdnx/
4+
adb push tests/snapshots/ /storage/self/primary/Documents/mdnx/snaps

android/app/src/main/java/co/rustworkshop/markdownneuraxis/ui/screens/FileViewScreen.kt

Lines changed: 81 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ import androidx.compose.ui.text.withStyle
2323
import androidx.compose.ui.unit.dp
2424
import androidx.compose.ui.graphics.Color
2525
import androidx.compose.material3.LocalContentColor
26+
import androidx.compose.foundation.background
27+
import androidx.compose.foundation.border
2628
import androidx.documentfile.provider.DocumentFile
2729
import co.rustworkshop.markdownneuraxis.io.readFileContent
2830
import co.rustworkshop.markdownneuraxis.io.resolveDocumentFile
@@ -131,8 +133,9 @@ private fun RenderBlockTree(
131133
) {
132134
for (block in blocks) {
133135
RenderBlock(block, depth, onWikiLinkClick)
134-
// List blocks handle their own children internally
135-
if (block.kind != "list" && block.children.isNotEmpty()) {
136+
// List and table blocks handle their own children internally
137+
val handlesOwnChildren = block.kind in listOf("list", "table", "table_header_row", "table_row")
138+
if (!handlesOwnChildren && block.children.isNotEmpty()) {
136139
RenderBlockTree(block.children, depth + 1, onWikiLinkClick)
137140
}
138141
}
@@ -299,6 +302,34 @@ private fun RenderBlock(block: Block, depth: Int, onWikiLinkClick: (String) -> U
299302
"thematic_break" -> {
300303
HorizontalDivider(modifier = Modifier.padding(vertical = 8.dp))
301304
}
305+
"table" -> {
306+
// Table container - calculate column count for consistent widths
307+
val columnCount = block.children.firstOrNull()?.children?.size ?: 1
308+
Column(
309+
modifier = Modifier
310+
.padding(vertical = 8.dp)
311+
.border(1.dp, MaterialTheme.colorScheme.outline)
312+
) {
313+
block.children.forEachIndexed { index, row ->
314+
RenderTableRow(row, columnCount, onWikiLinkClick)
315+
// Add divider between rows (not after last)
316+
if (index < block.children.size - 1) {
317+
HorizontalDivider(color = MaterialTheme.colorScheme.outline)
318+
}
319+
}
320+
}
321+
}
322+
"table_header_row", "table_row" -> {
323+
// Rows are rendered via RenderTableRow, this handles standalone case
324+
RenderTableRow(block, block.children.size, onWikiLinkClick)
325+
}
326+
"table_cell" -> {
327+
// Standalone cell (shouldn't happen - rows handle cells)
328+
RenderSegments(
329+
segments = block.segments,
330+
onWikiLinkClick = onWikiLinkClick
331+
)
332+
}
302333
else -> {
303334
Text(
304335
text = segmentsToText(block.segments),
@@ -308,6 +339,54 @@ private fun RenderBlock(block: Block, depth: Int, onWikiLinkClick: (String) -> U
308339
}
309340
}
310341

342+
@Composable
343+
private fun RenderTableRow(
344+
block: Block,
345+
columnCount: Int,
346+
onWikiLinkClick: (String) -> Unit
347+
) {
348+
val isHeader = block.kind == "table_header_row"
349+
val cells = block.children
350+
351+
Row(
352+
modifier = Modifier
353+
.fillMaxWidth()
354+
.height(IntrinsicSize.Min)
355+
.then(
356+
if (isHeader) Modifier.background(MaterialTheme.colorScheme.surfaceVariant)
357+
else Modifier
358+
)
359+
) {
360+
// Render each cell, padding to columnCount if needed
361+
for (i in 0 until columnCount) {
362+
if (i > 0) {
363+
// Vertical divider between cells
364+
VerticalDivider(
365+
modifier = Modifier.fillMaxHeight(),
366+
color = MaterialTheme.colorScheme.outline
367+
)
368+
}
369+
Box(
370+
modifier = Modifier
371+
.weight(1f)
372+
.padding(horizontal = 8.dp, vertical = 6.dp)
373+
) {
374+
if (i < cells.size) {
375+
RenderSegments(
376+
segments = cells[i].segments,
377+
style = if (isHeader) {
378+
MaterialTheme.typography.bodyMedium.copy(fontWeight = FontWeight.Bold)
379+
} else {
380+
MaterialTheme.typography.bodyMedium
381+
},
382+
onWikiLinkClick = onWikiLinkClick
383+
)
384+
}
385+
}
386+
}
387+
}
388+
}
389+
311390
private fun parseDocument(content: String): List<Block>? {
312391
return try {
313392
val doc = DocumentHandle.fromString(content)

crates/markdown-neuraxis-cli/src/main.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,28 @@ impl App {
225225
lines.push("---".to_string());
226226
lines.push(String::new());
227227
}
228+
BlockKind::Table => {
229+
// Render table rows
230+
if let BlockContent::Children(children) = &block.content {
231+
for child in children {
232+
render_block(child, lines);
233+
}
234+
}
235+
lines.push(String::new());
236+
}
237+
BlockKind::TableRow { .. } => {
238+
// Collect cell contents
239+
let mut cells = Vec::new();
240+
if let BlockContent::Children(children) = &block.content {
241+
for child in children {
242+
cells.push(segments_to_plain_text(&child.segments));
243+
}
244+
}
245+
lines.push(format!("| {} |", cells.join(" | ")));
246+
}
247+
BlockKind::TableCell => {
248+
// Cells are rendered by TableRow
249+
}
228250
}
229251
}
230252

crates/markdown-neuraxis-dioxus/src/assets/solarized-light.css

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -899,3 +899,36 @@ hr {
899899
background: var(--base1);
900900
margin: 4px 0;
901901
}
902+
903+
/* Table styles */
904+
.table {
905+
border-collapse: collapse;
906+
width: auto;
907+
}
908+
909+
.table-container {
910+
margin: 16px 0;
911+
}
912+
913+
.table-header-row {
914+
background-color: var(--base2);
915+
}
916+
917+
.table-row {
918+
background-color: var(--base3);
919+
}
920+
921+
.table-row:hover {
922+
background-color: var(--base2);
923+
}
924+
925+
.table-cell {
926+
border: 1px solid var(--base1);
927+
padding: 8px 12px;
928+
text-align: left;
929+
}
930+
931+
.table-header-row .table-cell {
932+
font-weight: 600;
933+
color: var(--base02);
934+
}

crates/markdown-neuraxis-dioxus/src/ui/components/block.rs

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,5 +259,92 @@ pub fn BlockRenderer(
259259
on_command
260260
}
261261
},
262+
BlockKind::Table => {
263+
let block_id = block.id;
264+
if is_focused {
265+
// Edit entire table as raw markdown
266+
let content_text = source
267+
.get(block.node_range.clone())
268+
.unwrap_or("")
269+
.to_string();
270+
let block_clone = block.clone();
271+
rsx! {
272+
div {
273+
class: "table-container clickable-block",
274+
EditorBlock {
275+
block: block_clone,
276+
content_text,
277+
on_command,
278+
on_cancel: {
279+
let mut focused_anchor_id = focused_anchor_id;
280+
move |_| focused_anchor_id.set(None)
281+
}
282+
}
283+
}
284+
}
285+
} else if let BlockContent::Children(children) = &block.content {
286+
rsx! {
287+
table {
288+
class: "table clickable-block",
289+
onclick: {
290+
let mut focused_anchor_id = focused_anchor_id;
291+
move |evt| {
292+
evt.stop_propagation();
293+
focused_anchor_id.set(Some(block_id))
294+
}
295+
},
296+
for (i, child) in children.iter().enumerate() {
297+
BlockRenderer {
298+
key: "{i}",
299+
block: child.clone(),
300+
source: source.clone(),
301+
focused_anchor_id,
302+
collapsed_ids,
303+
on_context_menu,
304+
on_command,
305+
on_wikilink_click
306+
}
307+
}
308+
}
309+
}
310+
} else {
311+
rsx! {}
312+
}
313+
}
314+
BlockKind::TableRow { is_header } => {
315+
if let BlockContent::Children(children) = &block.content {
316+
rsx! {
317+
tr {
318+
class: if *is_header { "table-header-row" } else { "table-row" },
319+
for (i, child) in children.iter().enumerate() {
320+
BlockRenderer {
321+
key: "{i}",
322+
block: child.clone(),
323+
source: source.clone(),
324+
focused_anchor_id,
325+
collapsed_ids,
326+
on_context_menu,
327+
on_command,
328+
on_wikilink_click
329+
}
330+
}
331+
}
332+
}
333+
} else {
334+
rsx! {}
335+
}
336+
}
337+
BlockKind::TableCell => {
338+
let segments = block.segments.clone();
339+
rsx! {
340+
td {
341+
class: "table-cell",
342+
InlineSegments {
343+
segments,
344+
on_wikilink_click
345+
}
346+
}
347+
}
348+
}
262349
}
263350
}

0 commit comments

Comments
 (0)