diff --git a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/blog/HtmlText.kt b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/blog/HtmlText.kt index 44babcd9dffaeb14f9718e54bfea08fbac03e73a..a98a41862abb34d1790102ee68b18cac1f121108 100644 --- a/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/blog/HtmlText.kt +++ b/briar-desktop/src/main/kotlin/org/briarproject/briar/desktop/blog/HtmlText.kt @@ -44,6 +44,7 @@ import androidx.compose.ui.text.style.BaselineShift import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.text.style.TextIndent import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.TextUnit import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import org.briarproject.briar.desktop.utils.PreviewUtils.preview @@ -69,15 +70,19 @@ fun main() = preview { contains a <a href="https://google.com">link to Google</a> </p> <blockquote>This is a block quote<br/>with multiple lines.</blockquote> -<ul> +<p><ul> <li>foo</li> - <li>bar</li> -</ul> + <li>direct children<ul><li>child1</li><li>child2</li></ul></li> + <ul> + <li>bar1</li> + <li>bar2</li> + </ul> +</ul></p> <p> -<ol> +<ul> <li>foo</li> <li>bar</li> -</ol> +</ul> </p> """.trimIndent() @@ -98,6 +103,14 @@ fun HtmlText( handleLink: (String) -> Unit, ) { + data class IndentInfo(val indent: TextUnit, val start: Int) + + val indentStack = mutableListOf<IndentInfo>() + + var listNesting = -1 + + var lastCharWasNewline = true + // Elements we support: // "h1", "h2", "h3", "h4", "h5", "h6", // "a", "b"/"strong", "i"/"em"/"cite", "u", "strike", "sub", "sup", "q", @@ -127,8 +140,6 @@ fun HtmlText( // todo: trim newlines / whitespaces?! // todo: nested paragraphs not possible, but in HTML it is? - val blockquote = ParagraphStyle(textIndent = TextIndent(20.sp, 20.sp)) - val paragraph = ParagraphStyle() val formattedString = remember(html) { buildAnnotatedString { @@ -136,15 +147,61 @@ fun HtmlText( fun appendAndUpdateCursor(str: String) { append(str) cursorPosition += str.length + lastCharWasNewline = str.last() == '\n' + } + + fun ensureNewline() { + if (!lastCharWasNewline) appendAndUpdateCursor("\n") + } + + fun pushIndent(indent: TextUnit) { + check(indent.isSp) { "only TextUnit.sp allowed" } + + var combinedIndent = indent + if (indentStack.isNotEmpty()) { + val prev = indentStack.last() + if (prev.start < cursorPosition) { + println("pushIndent: ${prev.start}-$cursorPosition") + addStyle( + style = ParagraphStyle(textIndent = TextIndent(prev.indent, prev.indent)), + start = prev.start, + end = cursorPosition + ) + // ensureNewline() + } + combinedIndent = (prev.indent.value + indent.value).sp + } + + indentStack.add(IndentInfo(combinedIndent, cursorPosition)) } - fun addParagraph() { - pushStyle(paragraph) - if (cursorPosition > 0) { - appendAndUpdateCursor("\n") + fun popIndent() { + check(indentStack.isNotEmpty()) { "nothing to pop from" } + val prev = indentStack.removeLast() + if (prev.start < cursorPosition) { + println("popIndent: ${prev.start}-$cursorPosition") + addStyle( + style = ParagraphStyle(textIndent = TextIndent(prev.indent, prev.indent)), + start = prev.start, + end = cursorPosition + ) + ensureNewline() + } + + if (indentStack.isNotEmpty()) { + val next = indentStack.removeLast() + indentStack.add(next.copy(start = cursorPosition)) } } + fun startParagraph() { + pushIndent(0.sp) + } + + fun endParagraph() { + popIndent() + } + fun addLink(node: Element) { val start = cursorPosition val end = start + node.text().length @@ -160,16 +217,16 @@ fun HtmlText( } fun startBlockQuote() { - pushStyle(blockquote) + pushIndent(20.sp) pushStringAnnotation("quote", "") } fun endBlockQuote() { pop() - pop() + popIndent() } - // todo: this should be properly localized + // todo: quotation marks should be properly localized fun startInlineQuote() { appendAndUpdateCursor("\"") } @@ -179,23 +236,29 @@ fun HtmlText( } fun startUnorderedList() { - pushStyle(blockquote) + pushIndent(20.sp) + listNesting++ } fun endUnorderedList() { - pop() + popIndent() + listNesting-- } fun startBullet() { - pushStringAnnotation("bullet", "") + check(listNesting >= 0) { "<li> outside of list" } + pushStringAnnotation("bullet", listNesting.toString()) } fun endBullet() { pop() - appendAndUpdateCursor("\n") + ensureNewline() } - val doc = Jsoup.parse(html) + // replace multiple newlines/whitespaces to single whitespace + // todo: also trim text (at least) inside <p> tags + val cleanHtml = html.replace("\\s+".toRegex(), " ") + val doc = Jsoup.parse(cleanHtml) doc.traverse(object : NodeVisitor { override fun head(node: Node, depth: Int) { @@ -227,13 +290,14 @@ fun HtmlText( "a" -> addLink(node) // lists - "ul" -> startUnorderedList() + // todo: properly support ordered list + "ul", "ol" -> startUnorderedList() "li" -> startBullet() // misc "br" -> appendAndUpdateCursor("\n") "blockquote" -> startBlockQuote() - "p" -> addParagraph() + "p" -> startParagraph() // else -> throw Exception("Unsupported tag '${node.tagName()}'") } } @@ -249,14 +313,16 @@ fun HtmlText( when (node.tagName()) { "h1", "h2", "h3", "h4", "h5", "h6", "b", "strong", "i", "em", "cite", "u", "strike", "sub", "sup", - "a", "p", + "a", -> pop() "q" -> endInlineQuote() - "ul" -> endUnorderedList() + // todo: properly support ordered list + "ul", "ol" -> endUnorderedList() "li" -> endBullet() + "p" -> endParagraph() "blockquote" -> endBlockQuote() } } @@ -267,7 +333,13 @@ fun HtmlText( val textMeasurer = rememberTextMeasurer() // todo: doesn't respect actual text size, but also cannot be changed currently - val textLayoutResult = remember { textMeasurer.measure("\u2022") } + val listBullets = remember { + listOf( + textMeasurer.measure("\u2022"), + textMeasurer.measure("\u25e6"), + textMeasurer.measure("\u25aa"), + ) + } val color = MaterialTheme.colors.onSurface var onDraw: DrawScope.() -> Unit by remember { mutableStateOf({}) } @@ -297,13 +369,19 @@ fun HtmlText( right = 0f ) } + + data class BulletInfo(val rect: Rect, val nestingLevel: Int) + val bullets = formattedString.getStringAnnotations("bullet").map { val line = layoutResult.getLineForOffset(it.start) - Rect( - top = layoutResult.getLineTop(line), - bottom = layoutResult.getLineBottom(line), - left = 0f, - right = layoutResult.getLineLeft(line) + BulletInfo( + rect = Rect( + top = layoutResult.getLineTop(line), + bottom = layoutResult.getLineBottom(line), + left = layoutResult.getLineLeft(line), + right = layoutResult.getLineLeft(line) + ), + nestingLevel = it.item.toInt() ) } onDraw = { @@ -316,12 +394,13 @@ fun HtmlText( ) } bullets.forEach { + val bullet = listBullets[it.nestingLevel % listBullets.size] drawText( - textLayoutResult = textLayoutResult, + textLayoutResult = bullet, color = color, topLeft = Offset( - it.center.x - textLayoutResult.size.width / 2, - it.center.y - textLayoutResult.size.height / 2, + it.rect.left - 10.dp.toPx() - bullet.size.width / 2, + it.rect.center.y - bullet.size.height / 2, ), ) }