Skip to content

Commit

Permalink
HTML Linearizer but no images yet
Browse files Browse the repository at this point in the history
Implemented figure and image

Actually renders linear content

WIP

Liking this

Great previews

Layouts look good but html parsing is doing something wrong

Not sure why some text is small but pretty good. Images could be larger in tables

Fixed some bugs

Fixed row colors for table

Fixed so empty lists aren't added

Added support for ArsTechnica stupid image styles

No longer assumes full screen for dimensions

Fixed empty space at end of blockquotes

Fixed nullability assumptinos

Added spans to table

Added better handling of ending whitespace

Added support for <span style> for bold/italic

Fixed test

Build apk for branch

Fixed incorrect parsing of nested tables

Optimized out single row tables

Optimized single col tables
  • Loading branch information
spacecowboy committed Jun 1, 2024
1 parent 0402559 commit 4fe4813
Show file tree
Hide file tree
Showing 18 changed files with 4,393 additions and 1,809 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/branch_apk.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ name: Signed APKs
on:
push:
branches:
- upgrades
- table-layout

jobs:
signed_apk:
Expand Down

Large diffs are not rendered by default.

293 changes: 293 additions & 0 deletions app/src/main/java/com/nononsenseapps/feeder/model/html/LinearStuff.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,293 @@
package com.nononsenseapps.feeder.model.html

import androidx.collection.ArrayMap

data class LinearArticle(
val elements: List<LinearElement>,
)

/**
* A linear element can contain other linear elements
*/
sealed interface LinearElement

/**
* Represents a list of items, ordered or unordered
*/
data class LinearList(
val ordered: Boolean,
val items: List<LinearListItem>,
) : LinearElement {
fun isEmpty(): Boolean {
return items.isEmpty()
}

fun isNotEmpty(): Boolean {
return items.isNotEmpty()
}

class Builder(private val ordered: Boolean) {
private val items: MutableList<LinearListItem> = mutableListOf()

fun add(item: LinearListItem) {
items.add(item)
}

fun build(): LinearList {
return LinearList(ordered, items)
}
}

companion object {
fun build(
ordered: Boolean,
block: Builder.() -> Unit,
): LinearList {
return Builder(ordered).apply(block).build()
}
}
}

/**
* Represents a single item in a list
*/
data class LinearListItem(
val content: List<LinearElement>,
) {
constructor(block: ListBuilderScope<LinearElement>.() -> Unit) : this(content = ListBuilderScope(block).items)

constructor(vararg elements: LinearElement) : this(content = elements.toList())

fun isEmpty(): Boolean {
return content.isEmpty()
}

fun isNotEmpty(): Boolean {
return content.isNotEmpty()
}

class Builder {
private val content: MutableList<LinearElement> = mutableListOf()

fun add(element: LinearElement) {
content.add(element)
}

fun build(): LinearListItem {
return LinearListItem(content)
}
}

companion object {
fun build(block: Builder.() -> Unit): LinearListItem {
return Builder().apply(block).build()
}
}
}

/**
* Represents a table
*/
data class LinearTable(
val rowCount: Int,
val colCount: Int,
private val cellsReal: ArrayMap<Coordinate, LinearTableCellItem>,
) : LinearElement {
val cells: Map<Coordinate, LinearTableCellItem>
get() = cellsReal

constructor(
rowCount: Int,
colCount: Int,
cells: List<LinearTableCellItem>,
) : this(
rowCount,
colCount,
ArrayMap<Coordinate, LinearTableCellItem>().apply {
cells.forEachIndexed { index, item ->
put(Coordinate(row = index / colCount, col = index % colCount), item)
}
},
)

fun cellAt(
row: Int,
col: Int,
): LinearTableCellItem? {
return cells[Coordinate(row = row, col = col)]
}

class Builder {
private val cells: ArrayMap<Coordinate, LinearTableCellItem> = ArrayMap()
private var rowCount: Int = 0
private var colCount: Int = 0
private var currentRowColCount = 0
private var currentRow = 0

fun add(element: LinearTableCellItem) {
check(rowCount > 0) { "Must add a row before adding cells" }

// First find the first empty cell in this row
var cellCoord = Coordinate(row = currentRow, col = currentRowColCount)
while (cells[cellCoord] != null) {
currentRowColCount++
cellCoord = cellCoord.copy(col = currentRowColCount)
}

currentRowColCount += element.colSpan
if (currentRowColCount > colCount) {
colCount = currentRowColCount
}

cells[cellCoord] = element

// Insert filler elements for spanned cells
for (r in 0 until element.rowSpan) {
for (c in 0 until element.colSpan) {
// Skip first since this is the cell itself
if (r == 0 && c == 0) {
continue
}

val fillerCoord = Coordinate(row = currentRow + r, col = currentRowColCount - element.colSpan + c)
check(cells[fillerCoord] == null) { "Cell at filler $fillerCoord already exists" }
cells[fillerCoord] = LinearTableCellItem.filler
}
}
}

fun newRow() {
if (rowCount > 0) {
currentRow++
}
rowCount++
currentRowColCount = 0
}

fun build(): LinearTable {
return LinearTable(rowCount, colCount, cells)
}
}

companion object {
fun build(block: Builder.() -> Unit): LinearTable {
return Builder().apply(block).build()
}
}
}

data class Coordinate(
val row: Int,
val col: Int,
)

/**
* Represents a single cell in a table
*/
data class LinearTableCellItem(
val type: LinearTableCellItemType,
val colSpan: Int,
val rowSpan: Int,
val content: List<LinearElement>,
) {
constructor(
colSpan: Int,
rowSpan: Int,
type: LinearTableCellItemType,
block: ListBuilderScope<LinearElement>.() -> Unit,
) : this(colSpan = colSpan, rowSpan = rowSpan, type = type, content = ListBuilderScope(block).items)

val isFiller
get() = colSpan == filler.colSpan && rowSpan == filler.rowSpan

class Builder(
private val colSpan: Int,
private val rowSpan: Int,
private val type: LinearTableCellItemType,
) {
private val content: MutableList<LinearElement> = mutableListOf()

fun add(element: LinearElement) {
content.add(element)
}

fun build(): LinearTableCellItem {
return LinearTableCellItem(colSpan = colSpan, rowSpan = rowSpan, type = type, content = content)
}
}

companion object {
fun build(
colSpan: Int,
rowSpan: Int,
type: LinearTableCellItemType,
block: Builder.() -> Unit,
): LinearTableCellItem {
return Builder(colSpan = colSpan, rowSpan = rowSpan, type = type).apply(block).build()
}

val filler =
LinearTableCellItem(
type = LinearTableCellItemType.DATA,
colSpan = -1,
rowSpan = -1,
content = emptyList(),
)
}
}

enum class LinearTableCellItemType {
HEADER,
DATA,
}

data class LinearBlockQuote(
val cite: String?,
val content: List<LinearElement>,
) : LinearElement {
constructor(cite: String?, block: ListBuilderScope<LinearElement>.() -> Unit) : this(cite = cite, content = ListBuilderScope(block).items)

constructor(cite: String?, vararg elements: LinearElement) : this(cite = cite, content = elements.toList())
}

/**
* Primitives can not contain other elements
*/
sealed interface LinearPrimitive : LinearElement

/**
* Represents a text element. For example a paragraph, or a header.
*/
data class LinearText(
val text: String,
val annotations: List<LinearTextAnnotation>,
val blockStyle: LinearTextBlockStyle,
) : LinearPrimitive {
constructor(text: String, blockStyle: LinearTextBlockStyle, vararg annotations: LinearTextAnnotation) : this(text = text, blockStyle = blockStyle, annotations = annotations.toList())
}

enum class LinearTextBlockStyle {
TEXT,
PRE_FORMATTED,
CODE_BLOCK,
}

val LinearTextBlockStyle.shouldSoftWrap: Boolean
get() = this == LinearTextBlockStyle.TEXT

/**
* Represents an image element
*/
data class LinearImage(
val candidates: List<LinearImageCandidate>,
val caption: LinearText?,
val link: String?,
) : LinearElement

data class LinearImageCandidate(
val imgUri: String,
val widthPx: Int?,
val heightPx: Int?,
val pixelDensity: Float?,
val screenWidth: Int?,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package com.nononsenseapps.feeder.model.html

data class LinearTextAnnotation(
val data: LinearTextAnnotationData,
/**
* Inclusive start index
*/
val start: Int,
/**
* Inclusive end index
*/
var end: Int,
) {
val endExclusive
get() = end + 1
}

sealed interface LinearTextAnnotationData

data object LinearTextAnnotationH1 : LinearTextAnnotationData

data object LinearTextAnnotationH2 : LinearTextAnnotationData

data object LinearTextAnnotationH3 : LinearTextAnnotationData

data object LinearTextAnnotationH4 : LinearTextAnnotationData

data object LinearTextAnnotationH5 : LinearTextAnnotationData

data object LinearTextAnnotationH6 : LinearTextAnnotationData

data object LinearTextAnnotationBold : LinearTextAnnotationData

data object LinearTextAnnotationItalic : LinearTextAnnotationData

data object LinearTextAnnotationMonospace : LinearTextAnnotationData

data object LinearTextAnnotationUnderline : LinearTextAnnotationData

data object LinearTextAnnotationStrikethrough : LinearTextAnnotationData

data object LinearTextAnnotationSuperscript : LinearTextAnnotationData

data object LinearTextAnnotationSubscript : LinearTextAnnotationData

data class LinearTextAnnotationFont(
val face: String,
) : LinearTextAnnotationData

data object LinearTextAnnotationCode : LinearTextAnnotationData

data class LinearTextAnnotationLink(
val href: String,
) : LinearTextAnnotationData
Loading

0 comments on commit 4fe4813

Please sign in to comment.