From 17906c06f908a19253e3af379f0232acebf903b6 Mon Sep 17 00:00:00 2001 From: Levi Neely Date: Thu, 1 Oct 2026 10:16:27 +0200 Subject: [PATCH] Render org src blocks as styled code blocks - Parse body text into segments (text vs code) - Src blocks get: monospace font, alternate background, rounded corners - Language label shown above code (python, bash, etc.) - Regular text keeps left rule indicator Handles #+BEGIN_SRC lang ... #+END_SRC blocks. --- ui/qml/OutlineView.qml | 136 +++++++++++++++++++++++++++++++++++------ 1 file changed, 119 insertions(+), 17 deletions(-) diff --git a/ui/qml/OutlineView.qml b/ui/qml/OutlineView.qml index 2235815..4af0d5a 100644 --- a/ui/qml/OutlineView.qml +++ b/ui/qml/OutlineView.qml @@ -57,6 +57,57 @@ ListView { readonly property int sectionLevel: model.level ?? 1 readonly property int indent: (sectionLevel - 1) * outline.indentWidth + // Parse body into text and code block segments + readonly property var bodyParts: { + let parts = []; + let body = sectionBody; + if (!body) return parts; + + let lines = body.split('\n'); + let currentText = []; + let inCode = false; + let codeLang = ""; + let codeLines = []; + + for (let i = 0; i < lines.length; i++) { + let line = lines[i]; + let trimmed = line.trim(); + + if (!inCode && trimmed.toUpperCase().startsWith("#+BEGIN_SRC")) { + // Flush pending text + if (currentText.length > 0) { + let text = currentText.join('\n').trim(); + if (text) parts.push({type: "text", content: text}); + currentText = []; + } + // Start code block + inCode = true; + codeLang = trimmed.substring(11).trim().split(/\s+/)[0] || ""; + codeLines = []; + } else if (inCode && trimmed.toUpperCase() === "#+END_SRC") { + // End code block + parts.push({type: "code", language: codeLang, content: codeLines.join('\n')}); + inCode = false; + codeLang = ""; + codeLines = []; + } else if (inCode) { + codeLines.push(line); + } else { + currentText.push(line); + } + } + + // Flush remaining + if (inCode && codeLines.length > 0) { + parts.push({type: "code", language: codeLang, content: codeLines.join('\n')}); + } else if (currentText.length > 0) { + let text = currentText.join('\n').trim(); + if (text) parts.push({type: "text", content: text}); + } + + return parts; + } + MouseArea { anchors.fill: parent onClicked: outline.currentIndex = del.index @@ -230,27 +281,78 @@ ListView { } } - // Body text with left rule - RowLayout { + // Body content (text and code blocks) + ColumnLayout { visible: del.sectionBody !== "" Layout.fillWidth: true - spacing: 8 + spacing: 4 - Rectangle { - width: 2 - Layout.fillHeight: true - Layout.leftMargin: 2 - color: theme.midColor - } + Repeater { + model: del.bodyParts - Label { - Layout.fillWidth: true - text: del.sectionBody - color: theme.placeholderTextColor - font.pointSize: outline.baseFontSize + 1 - lineHeight: 1.3 - wrapMode: Text.WordWrap - textFormat: Text.PlainText + delegate: Loader { + required property var modelData + Layout.fillWidth: true + sourceComponent: modelData.type === "code" ? codeBlockComponent : textBlockComponent + + Component { + id: textBlockComponent + RowLayout { + spacing: 8 + Rectangle { + width: 2 + Layout.fillHeight: true + Layout.leftMargin: 2 + color: theme.midColor + } + Label { + Layout.fillWidth: true + text: modelData.content + color: theme.placeholderTextColor + font.pointSize: outline.baseFontSize + 1 + lineHeight: 1.3 + wrapMode: Text.WordWrap + textFormat: Text.PlainText + } + } + } + + Component { + id: codeBlockComponent + Rectangle { + color: theme.alternateBaseColor + radius: 4 + implicitHeight: codeColumn.implicitHeight + 12 + + ColumnLayout { + id: codeColumn + anchors.fill: parent + anchors.margins: 6 + spacing: 4 + + // Language label + Label { + visible: (modelData.language ?? "") !== "" + text: modelData.language + color: theme.linkColor + font.pointSize: outline.baseFontSize - 2 + font.bold: true + } + + // Code content + Label { + Layout.fillWidth: true + text: modelData.content + color: theme.textColor + font.family: "monospace" + font.pointSize: outline.baseFontSize + wrapMode: Text.WrapAnywhere + textFormat: Text.PlainText + } + } + } + } + } } } }