Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,14 @@ object MarkdownPageParser {
parentUuid = parentUuid?.let { BlockUuid(it) },
leftUuid = previousSiblingUuid?.let { BlockUuid(it) },
content = parsedBlock.content,
level = baseLevel,
// parsedBlock.level is the outline nesting depth computed from the source
// Markdown's own indentation (bullet/heading indent, etc. — see
// MarkdownParser.convertBlock / BlockNode.indentLevel). It agrees with
// baseLevel (the tree-recursion depth) for well-formed, contiguously
// indented documents, but baseLevel alone discards the indentLevel this
// parser computes for headings/code-fences/blockquotes/etc., so it must be
// read here for that value to ever reach storage.
level = parsedBlock.level,
position = positionKey,
createdAt = now,
updatedAt = now,
Expand Down Expand Up @@ -216,7 +223,9 @@ object MarkdownPageParser {
pageUuid = pageUuid,
parentUuid = parentUuid?.let { BlockUuid(it) },
content = parsedBlock.content,
level = baseLevel,
// See processParsedBlocks for why parsedBlock.level (not baseLevel) is
// the value that must reach the persisted Block.level.
level = parsedBlock.level,
position = stubPositionKey,
createdAt = now,
updatedAt = now,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,13 @@ class MarkdownParser {
val level = when(block) {
is BulletBlockNode -> block.level
is ParagraphBlockNode -> 0
is HeadingBlockNode -> 0
is CodeFenceBlockNode -> 0
is BlockquoteBlockNode -> 0
is HeadingBlockNode -> block.indentLevel
is CodeFenceBlockNode -> block.indentLevel
is BlockquoteBlockNode -> block.indentLevel
is OrderedListItemBlockNode -> block.level
is ThematicBreakBlockNode -> 0
is TableBlockNode -> 0
is RawHtmlBlockNode -> 0
is ThematicBreakBlockNode -> block.indentLevel
is TableBlockNode -> block.indentLevel
is RawHtmlBlockNode -> block.indentLevel
}

val blockType = when (block) {
Expand Down Expand Up @@ -85,6 +85,7 @@ class MarkdownParser {
is BlockquoteBlockNode -> {
block.children.map { child -> "> ${reconstructContent(child.content)}" }.joinToString("\n")
}
is RawHtmlBlockNode -> block.rawHtml
else -> reconstructContent(block.content)
}

Expand Down
555 changes: 383 additions & 172 deletions kmp/src/commonMain/kotlin/dev/stapler/stelekit/parsing/BlockParser.kt

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,15 @@ data class ParagraphBlockNode(
* and as decoration on bullet blocks (e.g. `- ## TODO My heading`).
*
* [level] is 1–6 (number of leading `#` characters).
* [indentLevel] is the outline nesting depth (mirrors [BulletBlockNode.level]) for headings
* that decorate a bullet; top-level ATX headings default to 0.
*/
data class HeadingBlockNode(
val level: Int,
override val content: List<InlineNode>,
override val children: List<BlockNode> = emptyList(),
override val properties: Map<String, String> = emptyMap()
override val properties: Map<String, String> = emptyMap(),
val indentLevel: Int = 0
) : BlockNode()

/**
Expand All @@ -42,25 +45,32 @@ data class HeadingBlockNode(
* [language] is the info string immediately after the opening fence (e.g. "kotlin", "python").
* [options] are additional words on the opening fence line (Logseq/org-mode extensions).
* [rawContent] is the verbatim body of the block, newlines preserved.
* [indentLevel] is the outline nesting depth (mirrors [BulletBlockNode.level]) for fenced
* code blocks that decorate a bullet; top-level fenced code blocks default to 0.
*/
data class CodeFenceBlockNode(
val language: String?,
val options: List<String> = emptyList(),
val rawContent: String,
override val children: List<BlockNode> = emptyList(),
override val properties: Map<String, String> = emptyMap()
override val properties: Map<String, String> = emptyMap(),
val indentLevel: Int = 0
) : BlockNode() {
override val content: List<InlineNode> = emptyList()
}

/**
* Block-level blockquote: one or more lines prefixed with `>`.
* CommonMark spec §5.1.
*
* [indentLevel] is the outline nesting depth (mirrors [BulletBlockNode.level]) for
* blockquotes that decorate a bullet; top-level blockquotes default to 0.
*/
data class BlockquoteBlockNode(
override val children: List<BlockNode>,
override val content: List<InlineNode> = emptyList(),
override val properties: Map<String, String> = emptyMap()
override val properties: Map<String, String> = emptyMap(),
val indentLevel: Int = 0
) : BlockNode()

/**
Expand All @@ -81,35 +91,47 @@ data class OrderedListItemBlockNode(
/**
* Thematic break: `---`, `***`, `___` (3+ matching characters, optional spaces).
* CommonMark spec §4.1.
*
* [indentLevel] is the outline nesting depth (mirrors [BulletBlockNode.level]) for
* thematic breaks that decorate a bullet; top-level thematic breaks default to 0.
*/
data class ThematicBreakBlockNode(
override val content: List<InlineNode> = emptyList(),
override val children: List<BlockNode> = emptyList(),
override val properties: Map<String, String> = emptyMap()
override val properties: Map<String, String> = emptyMap(),
val indentLevel: Int = 0
) : BlockNode()

/**
* GFM pipe table.
* GFM spec §4.10.
*
* [indentLevel] is the outline nesting depth (mirrors [BulletBlockNode.level]) for tables
* that decorate a bullet; top-level tables default to 0.
*/
data class TableBlockNode(
val headers: List<String>,
val alignments: List<TableAlignment?>,
val rows: List<List<String>>,
override val content: List<InlineNode> = emptyList(),
override val children: List<BlockNode> = emptyList(),
override val properties: Map<String, String> = emptyMap()
override val properties: Map<String, String> = emptyMap(),
val indentLevel: Int = 0
) : BlockNode()

enum class TableAlignment { LEFT, RIGHT, CENTER }

/**
* Raw HTML block — passed through verbatim, rendered as a code block in Compose.
* CommonMark spec §4.6.
*
* [indentLevel] is the outline nesting depth (mirrors [BulletBlockNode.level]) for raw
* HTML blocks that decorate a bullet; top-level raw HTML blocks default to 0.
*/
data class RawHtmlBlockNode(
val rawHtml: String,
override val content: List<InlineNode> = emptyList(),
override val children: List<BlockNode> = emptyList(),
override val properties: Map<String, String> = emptyMap()
override val properties: Map<String, String> = emptyMap(),
val indentLevel: Int = 0
) : BlockNode()
Original file line number Diff line number Diff line change
Expand Up @@ -451,6 +451,18 @@ internal fun BlockItem(
onLongPressSelect = onLongPressSelect,
modifier = Modifier.weight(1f),
)
is BlockType.RawHtml -> CodeFenceBlock(
// Rendered like a code fence (monospace, no inline-markdown parsing)
// per RawHtmlBlockNode's KDoc — raw HTML is passed through verbatim
// and should not be interpreted as Markdown.
content = block.content,
language = "html",
onStartEditing = onStartEditing,
isInSelectionMode = isInSelectionMode,
onToggleSelect = onToggleSelect,
onLongPressSelect = onLongPressSelect,
modifier = Modifier.weight(1f),
)
else -> {
val imageData = remember(block.content) { extractSingleImageNode(block.content) }
if (imageData != null) {
Expand All @@ -465,7 +477,7 @@ internal fun BlockItem(
modifier = Modifier.weight(1f),
)
} else {
BlockViewer( // BULLET, PARAGRAPH, RAW_HTML, unknown
BlockViewer( // BULLET, PARAGRAPH, unknown
content = block.content,
textColor = textColor,
linkColor = linkColor,
Expand Down
Loading
Loading