Render org links in the GUI body
Body text and list items now render org links ([[target]] and [[target][description]]) as clickable hyperlinks via StyledText. Other text is HTML-escaped and newlines become <br> so formatting is preserved. Activating a link opens external URL schemes (http(s), mailto, file) in the system handler; internal links (id:, #custom, *headline) are left for a future navigation pass. The pointer cursor shows over hovered links.
This commit is contained in:
parent
39ab7108ff
commit
b79970b62f
|
|
@ -41,6 +41,45 @@ ListView {
|
||||||
return Math.max(outline.baseFontSize - 1, outline.baseFontSize + 5 - level)
|
return Math.max(outline.baseFontSize - 1, outline.baseFontSize + 5 - level)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Escape HTML special chars for safe StyledText rendering.
|
||||||
|
function escHtml(s) {
|
||||||
|
return String(s)
|
||||||
|
.replace(/&/g, "&")
|
||||||
|
.replace(/</g, "<")
|
||||||
|
.replace(/>/g, ">")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render inline org markup (currently links) to StyledText. Org links are
|
||||||
|
// [[target]] or [[target][description]]; everything else is escaped. Line
|
||||||
|
// breaks are converted to <br> so StyledText preserves them.
|
||||||
|
function renderInline(text) {
|
||||||
|
let re = /\[\[([^\]\[]+?)\](?:\[([^\]]*?)\])?\]/g
|
||||||
|
let out = ""
|
||||||
|
let last = 0
|
||||||
|
let m
|
||||||
|
function escBlock(s) { return escHtml(s).replace(/\n/g, "<br>") }
|
||||||
|
while ((m = re.exec(text)) !== null) {
|
||||||
|
out += escBlock(text.slice(last, m.index))
|
||||||
|
let target = m[1]
|
||||||
|
let desc = (m[2] !== undefined && m[2] !== "") ? m[2] : m[1]
|
||||||
|
let href = escHtml(target).replace(/"/g, """)
|
||||||
|
out += '<a href="' + href + '">' + escHtml(desc) + '</a>'
|
||||||
|
last = re.lastIndex
|
||||||
|
}
|
||||||
|
out += escBlock(text.slice(last))
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open a link target activated from body text. External URL schemes open
|
||||||
|
// in the system handler; internal org links (id:, #custom, *headline) are
|
||||||
|
// left for a future navigation pass.
|
||||||
|
function openLink(target) {
|
||||||
|
if (/^([a-zA-Z][a-zA-Z0-9+.-]*):\/\//.test(target)
|
||||||
|
|| /^(mailto:|file:)/.test(target)) {
|
||||||
|
Qt.openUrlExternally(target)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
delegate: Rectangle {
|
delegate: Rectangle {
|
||||||
id: del
|
id: del
|
||||||
width: outline.width
|
width: outline.width
|
||||||
|
|
@ -427,12 +466,19 @@ ListView {
|
||||||
}
|
}
|
||||||
Label {
|
Label {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
text: modelData.content
|
text: outline.renderInline(modelData.content)
|
||||||
color: theme.placeholderTextColor
|
color: theme.placeholderTextColor
|
||||||
|
linkColor: theme.linkColor
|
||||||
font.pointSize: outline.baseFontSize + 1
|
font.pointSize: outline.baseFontSize + 1
|
||||||
lineHeight: 1.3
|
lineHeight: 1.3
|
||||||
wrapMode: Text.WordWrap
|
wrapMode: Text.WordWrap
|
||||||
textFormat: Text.PlainText
|
textFormat: Text.StyledText
|
||||||
|
onLinkActivated: (link) => outline.openLink(link)
|
||||||
|
|
||||||
|
HoverHandler {
|
||||||
|
enabled: parent.hoveredLink !== ""
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -497,13 +543,20 @@ ListView {
|
||||||
// Item text; checked items are dimmed and struck through.
|
// Item text; checked items are dimmed and struck through.
|
||||||
Label {
|
Label {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
text: modelData.text ?? ""
|
text: outline.renderInline(modelData.text ?? "")
|
||||||
color: (modelData.checkbox === "checked")
|
color: (modelData.checkbox === "checked")
|
||||||
? theme.placeholderTextColor : theme.textColor
|
? theme.placeholderTextColor : theme.textColor
|
||||||
|
linkColor: theme.linkColor
|
||||||
font.pointSize: outline.baseFontSize + 1
|
font.pointSize: outline.baseFontSize + 1
|
||||||
font.strikeout: modelData.checkbox === "checked"
|
font.strikeout: modelData.checkbox === "checked"
|
||||||
wrapMode: Text.WordWrap
|
wrapMode: Text.WordWrap
|
||||||
textFormat: Text.PlainText
|
textFormat: Text.StyledText
|
||||||
|
onLinkActivated: (link) => outline.openLink(link)
|
||||||
|
|
||||||
|
HoverHandler {
|
||||||
|
enabled: parent.hoveredLink !== ""
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue