Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -571,6 +571,9 @@ class MarkdownAccessibilityHelper(
.maxByOrNull { it.depth } ?: return null

if (requireStart) {
// An item split around a code block starts only where its marker is drawn: the segments after
// the code block continue it.
if (!deepest.drawsMarker) return null
val spanStart = spanned.getSpanStart(deepest)
val firstChar =
(spanStart until minOf(spanStart + 10, spanned.length))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ class BlockStyleContext {
var listItemMarkerExtra = 0f
var listItemMarkerWidth: Float? = null
var taskItemCount = 0
var accumulatedIndent = 0

private val orderedListItemNumbers = ArrayDeque<Int>()

Expand Down Expand Up @@ -158,6 +159,7 @@ class BlockStyleContext {
listItemMarkerExtra = 0f
listItemMarkerWidth = null
taskItemCount = 0
accumulatedIndent = 0
orderedListItemNumbers.clear()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ class BlockquoteRenderer(
onLinkLongPress: ((String) -> Unit)?,
factory: RendererFactory,
) {
if (builder.isNotEmpty() && builder.last() != '\n') {
builder.append("\n")
}

val start = builder.length
val style = config.style.blockquoteStyle
val context = factory.blockStyleContext
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ class CodeBlockRenderer(
onLinkLongPress: ((String) -> Unit)?,
factory: RendererFactory,
) {
if (builder.isNotEmpty() && builder.last() != '\n') {
builder.append("\n")
}

val start = builder.length
val style = config.style.codeBlockStyle
val context = factory.blockStyleContext
Expand All @@ -49,7 +53,7 @@ class CodeBlockRenderer(

// Apply background, borders, and horizontal padding to content only
builder.setSpan(
CodeBlockSpan(style, factory.context, factory.styleCache),
CodeBlockSpan(style, factory.context, factory.styleCache, context.accumulatedIndent),
contentStart,
end,
SPAN_FLAGS_EXCLUSIVE_EXCLUSIVE,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ class HeadingRenderer(
onLinkLongPress: ((String) -> Unit)?,
factory: RendererFactory,
) {
if (builder.isNotEmpty() && builder.last() != '\n') {
builder.append("\n")
}

val level = node.getAttribute("level")?.toIntOrNull() ?: 1
val start = builder.length

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@ class ListItemRenderer(

val depth = styleContext.listDepth - 1
val listStyle = config.style.listStyle
val span =

fun makeSpan(drawsMarker: Boolean): BaseListSpan =
if (isTask) {
TaskListSpan(
taskStyle = config.style.taskListStyle,
Expand All @@ -49,6 +50,7 @@ class ListItemRenderer(
styleCache = factory.styleCache,
taskIndex = taskIndex,
isChecked = isChecked,
drawsMarker = drawsMarker,
ancestorMarkerOffset = styleContext.ancestorMarkerOffset,
markerExtraWidth = styleContext.listMarkerExtra,
parentMarkerWidth = styleContext.listItemMarkerWidth,
Expand All @@ -61,6 +63,7 @@ class ListItemRenderer(
depth,
factory.context,
factory.styleCache,
drawsMarker,
ancestorMarkerOffset = styleContext.ancestorMarkerOffset,
parentMarkerWidth = styleContext.listItemMarkerWidth,
)
Expand All @@ -72,6 +75,7 @@ class ListItemRenderer(
depth,
factory.context,
factory.styleCache,
drawsMarker,
markerExtraWidth = styleContext.listMarkerExtra,
ancestorMarkerOffset = styleContext.ancestorMarkerOffset,
parentMarkerWidth = styleContext.listItemMarkerWidth,
Expand All @@ -82,27 +86,66 @@ class ListItemRenderer(
}
}

val markerSpan = makeSpan(drawsMarker = true)

// Nested lists inside this item shift by its extra marker width and measure their indent from its column.
// Code blocks inside it start at its text column, since the item's spans leave their lines (see below).
val previousMarkerExtra = styleContext.listItemMarkerExtra
styleContext.listItemMarkerExtra = span.markerExtraWidth
styleContext.listItemMarkerExtra = markerSpan.markerExtraWidth
val previousMarkerWidth = styleContext.listItemMarkerWidth
styleContext.listItemMarkerWidth = span.baselineMarkerWidth
styleContext.listItemMarkerWidth = markerSpan.baselineMarkerWidth
val previousIndent = styleContext.accumulatedIndent
styleContext.accumulatedIndent = previousIndent + markerSpan.getLeadingMargin(false)
try {
factory.renderChildren(node, builder, onLinkPress, onLinkLongPress)
} finally {
styleContext.accumulatedIndent = previousIndent
styleContext.listItemMarkerExtra = previousMarkerExtra
styleContext.listItemMarkerWidth = previousMarkerWidth
}

if (builder.length == start || builder.substring(start).isBlank()) return

while (builder.length > start && builder.last() == '\n') {
builder.delete(builder.length - 1, builder.length)
if (builder.last() != '\n') {
builder.append("\n")
}
builder.append("\n")

val itemEnd = builder.length
builder.setSpan(span, start, itemEnd, SPAN_FLAGS_LIST_ITEM)

val codeBlocks = builder.getSpans(start, itemEnd, CodeBlockSpan::class.java)
// Nested items render first, so a code block keeps the innermost item that holds it.
for (block in codeBlocks) {
if (block.listItem == null) block.listItem = markerSpan
}

val codeBlockRanges =
codeBlocks
.map { builder.getSpanStart(it) to builder.getSpanEnd(it) }
.filter { it.first < it.second }
.sortedBy { it.first }

var segmentStart = start
var isFirstSegment = true
for ((codeStart, codeEnd) in codeBlockRanges) {
if (segmentStart < codeStart) {
builder.setSpan(
if (isFirstSegment) markerSpan else makeSpan(drawsMarker = false),
segmentStart,
codeStart,
SPAN_FLAGS_LIST_ITEM,
)
isFirstSegment = false
}
segmentStart = maxOf(segmentStart, codeEnd)
}
if (segmentStart < itemEnd) {
builder.setSpan(
if (isFirstSegment) markerSpan else makeSpan(drawsMarker = false),
segmentStart,
Comment on lines +141 to +144

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed by #1066

itemEnd,
SPAN_FLAGS_LIST_ITEM,
)
}

if (isChecked) {
applyCheckedDecorations(builder, start, itemEnd, depth)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ abstract class BaseListSpan(
protected val blockStyle: BlockStyle,
protected val marginLeft: Float,
protected val gapWidth: Float,
internal val drawsMarker: Boolean = true,
internal val ancestorMarkerOffset: Float = 0f,
internal val parentMarkerWidth: Float? = null,
) : MetricAffectingSpan(),
Expand Down Expand Up @@ -76,6 +77,7 @@ abstract class BaseListSpan(
first: Boolean,
layout: Layout?,
) {
if (!drawsMarker) return
if (!first || shouldSkipDrawing(text, start) || !hasContent(text, start, end)) return

// Only draw the marker on the true first line of this span.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,13 @@ class CodeBlockSpan(
private val codeBlockStyle: CodeBlockStyle,
private val context: Context,
private val styleCache: SpanStyleCache,
private val leadingIndent: Int = 0,
) : MetricAffectingSpan(),
LineBackgroundSpan,
LeadingMarginSpan {
/** The innermost list item holding this block, whose marker span this is; copy-as-markdown nests the block under it. */
internal var listItem: BaseListSpan? = null

private val blockStyle =
BlockStyle(
fontSize = codeBlockStyle.fontSize,
Expand Down Expand Up @@ -59,7 +63,7 @@ class CodeBlockSpan(
color = codeBlockStyle.borderColor
}

override fun getLeadingMargin(first: Boolean): Int = codeBlockStyle.padding.toInt()
override fun getLeadingMargin(first: Boolean): Int = codeBlockStyle.padding.toInt() + leadingIndent

override fun drawLeadingMargin(
c: Canvas?,
Expand Down Expand Up @@ -105,11 +109,12 @@ class CodeBlockSpan(
val inset = codeBlockStyle.borderWidth / 2f

rect.set(
left.toFloat() + inset,
left.toFloat() + leadingIndent + inset,

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It will be handled in the follow-up

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

top.toFloat() + (if (isFirstLine) inset else 0f),
right.toFloat() - inset,
bottom.toFloat() - (if (isLastLine) inset else 0f),
)
if (rect.left >= rect.right) return

val radius = codeBlockStyle.borderRadius
val adjRadius = if (radius > inset) radius - inset else radius
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ class OrderedListSpan(
depth: Int,
context: Context,
styleCache: SpanStyleCache,
drawsMarker: Boolean = true,
override val markerExtraWidth: Float = 0f,
ancestorMarkerOffset: Float = 0f,
parentMarkerWidth: Float? = null,
Expand All @@ -33,6 +34,7 @@ class OrderedListSpan(
),
marginLeft = listStyle.marginLeft,
gapWidth = listStyle.gapWidth,
drawsMarker = drawsMarker,
ancestorMarkerOffset = ancestorMarkerOffset,
parentMarkerWidth = parentMarkerWidth,
) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ class TaskListSpan(
* moves it to the end of that order and shifts where its marker lands.
*/
var isChecked: Boolean,
drawsMarker: Boolean = true,
ancestorMarkerOffset: Float = 0f,
override val markerExtraWidth: Float = 0f,
parentMarkerWidth: Float? = null,
Expand All @@ -42,6 +43,7 @@ class TaskListSpan(
),
marginLeft = listStyle.marginLeft,
gapWidth = listStyle.gapWidth,
drawsMarker = drawsMarker,
ancestorMarkerOffset = ancestorMarkerOffset,
parentMarkerWidth = parentMarkerWidth,
) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ class UnorderedListSpan(
depth: Int,
context: Context,
styleCache: SpanStyleCache,
drawsMarker: Boolean = true,
ancestorMarkerOffset: Float = 0f,
parentMarkerWidth: Float? = null,
) : BaseListSpan(
Expand All @@ -28,6 +29,7 @@ class UnorderedListSpan(
),
marginLeft = listStyle.marginLeft,
gapWidth = listStyle.gapWidth,
drawsMarker = drawsMarker,
ancestorMarkerOffset = ancestorMarkerOffset,
parentMarkerWidth = parentMarkerWidth,
) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import com.swmansion.enriched.markdown.spans.HeadingSpan
import com.swmansion.enriched.markdown.spans.HighlightSpan
import com.swmansion.enriched.markdown.spans.ImageSpan
import com.swmansion.enriched.markdown.spans.LinkSpan
import com.swmansion.enriched.markdown.spans.MarginBottomSpan
import com.swmansion.enriched.markdown.spans.OrderedListSpan
import com.swmansion.enriched.markdown.spans.StrongSpan
import com.swmansion.enriched.markdown.spans.TaskListSpan
Expand Down Expand Up @@ -334,6 +335,9 @@ object HTMLGenerator {
return
}

// A block's bottom-margin line inside a list item or a quote holds no content of its own.
if (paraText.isEmpty() && text.getSpans(para.start, para.start + 1, MarginBottomSpan::class.java).isNotEmpty()) return

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It will be handled in follow-up PR

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


// Get content range (trim trailing newline)
val contentEnd = if (para.end > para.start && text[para.end - 1] == '\n') para.end - 1 else para.end
val isCodeBlock = para.type == TYPE_CODE_BLOCK
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,8 @@ object MarkdownExtractor {

val codeBlockSpans = spannable.getSpans(segmentStart, segmentEnd, CodeBlockSpan::class.java)
if (codeBlockSpans.isNotEmpty()) {
appendCodeBlock(segmentText, result, state)
val opensBlock = codeBlockSpans.any { spannable.getSpanStart(it) == segmentStart }
appendCodeBlock(spannable, codeBlockSpans[0], segmentText, opensBlock, result, state)
return true
}

Expand Down Expand Up @@ -291,7 +292,10 @@ object MarkdownExtractor {
}

private fun appendCodeBlock(
spannable: Spannable,
block: CodeBlockSpan,
text: String,
opensBlock: Boolean,
result: StringBuilder,
state: ExtractionState,
) {
Expand All @@ -300,14 +304,42 @@ object MarkdownExtractor {
state.needsBlankLine = false
}

val needsFence = result.isEmpty() || result.endsWith("\n\n")
if (needsFence) result.append("```\n")
// A code block inside a list item lines up under the item's text.
val item = block.listItem

// When the block opens its item, the item's marker span is not attached at all or sits after the block.
val itemStart = item?.let { spannable.getSpanStart(it) } ?: -1
val markerSpanDetached = itemStart < 0
val markerSpanAfterBlock = itemStart >= spannable.getSpanEnd(block)
val blockOpensItem = item != null && (markerSpanDetached || markerSpanAfterBlock)

// The fence takes the item's marker only when the selection includes the block's first line.
val opensItem = opensBlock && blockOpensItem
val nestedItem = item?.takeIf { opensItem || result.isNotEmpty() }
val indent = nestedItem?.let { contentIndent(it) } ?: ""
if (nestedItem != null) state.listDepth = nestedItem.depth

val needsFence = result.isEmpty() || result.endsWith("\n\n") || opensBlock
if (needsFence) {
if (!result.isAtLineStart()) result.append("\n")
if (opensItem) {
result.append(listMarker(item))
state.itemOpenedByCode = item
} else {
result.append(indent)
}
result.append("```\n")
}

result.append(text)
text.split('\n').forEachIndexed { index, line ->
if (index > 0) result.append('\n')
if (line.isNotEmpty() && result.isAtLineStart()) result.append(indent)
result.append(line)
}

if (text.endsWith("\n")) {
result.append("```\n")
state.needsBlankLine = true
result.append(indent).append("```\n")
state.needsBlankLine = nestedItem == null
}
}

Expand Down Expand Up @@ -346,6 +378,13 @@ object MarkdownExtractor {
}

state.listDepth = listSpan.depth

// Text after a code block continues its item rather than starting a new one.
val continuesItem = !listSpan.drawsMarker || listSpan === state.itemOpenedByCode
return if (continuesItem) contentIndent(listSpan) else listMarker(listSpan)
}

private fun listMarker(listSpan: BaseListSpan): String {
val indent = " ".repeat(listSpan.depth)

return when (listSpan) {
Expand All @@ -355,6 +394,12 @@ object MarkdownExtractor {
}
}

/** The column an item's text starts at; a task item's checkbox is part of its text. */
private fun contentIndent(listSpan: BaseListSpan): String {
val markerWidth = if (listSpan is OrderedListSpan) "${listSpan.itemNumber}. ".length else 2
return " ".repeat(2 * listSpan.depth + markerWidth)
}

private fun applyInlineFormatting(
spannable: Spannable,
text: String,
Expand Down Expand Up @@ -424,6 +469,7 @@ object MarkdownExtractor {
var blockquoteDepth: Int = -1,
var listDepth: Int = -1,
var needsBlankLine: Boolean = false,
var itemOpenedByCode: BaseListSpan? = null,
)

private class HeadingAccumulator {
Expand Down
Loading
Loading