package jp.example.android.util.html import android.content.Context import android.graphics.Bitmap import android.support.v4.content.ContextCompat import android.text.SpannableString import android.text.SpannableStringBuilder import android.text.Spanned.SPAN_INCLUSIVE_INCLUSIVE import android.text.style.TextAppearanceSpan import android.view.Gravity.CENTER import android.view.View import android.view.ViewGroup.LayoutParams.MATCH_PARENT import android.view.ViewGroup.LayoutParams.WRAP_CONTENT import android.widget.* import android.widget.LinearLayout import android.widget.LinearLayout.VERTICAL import com.airbnb.paris.Paris import com.bumptech.glide.Glide import com.bumptech.glide.load.model.GlideUrl import com.bumptech.glide.load.model.LazyHeaders import life.medley.android.R import life.medley.android.util.image.GlideLoggingListener import okhttp3.HttpUrl import org.jsoup.Jsoup import org.jsoup.nodes.Element class HTMLConverter( private val context: Context, private val html: String ) { private val resultView = LinearLayout(context).apply { layoutParams = LinearLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT) orientation = VERTICAL } fun parse() { val body = Jsoup.parse(html) .normalise() .body() inspect(body, ElementViewHolder()) } fun build() = resultView private fun inspect(parentElement: Element, parent: ElementViewHolder) { var brotherCount = 1 parentElement.children().forEach { val prevElement = it.previousElementSibling() if (prevElement != null && prevElement.tagName() == it.tagName()) { brotherCount++ } else { brotherCount = 1 } val type = ElementType.valueOf(it) val view = when (type) { ElementType.TABLE, ElementType.TABLE_BODY -> parseTable(it) ElementType.TABLE_ROW -> createTableRow() ElementType.TABLE_DATA -> parseTableData(it) ElementType.IMAGE -> parseImage(it) ElementType.HEADER1, ElementType.HEADER2, ElementType.HEADER3, ElementType.HEADER4, ElementType.HEADER5, ElementType.HEADER6 -> parseTitle(it, type) ElementType.DEFINITION_TERM, ElementType.PARAGRAPH -> parseParagraph(it) ElementType.LINK -> parseLink(it) ElementType.UNORDERED_LIST, ElementType.ORDERED_LIST, ElementType.DEFINITION_LIST -> createListView() ElementType.LIST_ITEM, ElementType.DEFINITION_DESCRIPTION -> parseListItem(it, parent, brotherCount) else -> null } val children = it.children() if (children.isNotEmpty()) { val holder = ElementViewHolder(view, type, parent.depth + 1) if (type == parent.type) { holder.orderString = parent.orderString + "." + brotherCount } else { holder.orderString = brotherCount.toString() } inspect(it, holder) } // View が存在しないか、parent に合成できた場合は view に追加する必用がない if (view == null || compose(ElementViewHolder(view, ElementType.valueOf(it)), parent)) { return@forEach } if (type == ElementType.TABLE || type == ElementType.TABLE_BODY) { val innerView = LinearLayout(context).apply { layoutParams = LinearLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT) orientation = LinearLayout.HORIZONTAL addView(view) } val scrollView = HorizontalScrollView(context).apply { layoutParams = LinearLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT) addView(innerView) } resultView.addView(scrollView) return@forEach } resultView.addView(view, MATCH_PARENT, WRAP_CONTENT) } } private fun parseTable(el: Element): View? { // 自分が table で小要素に tbody が合った場合は tbody で探索を行う必要があるため、探索を打ち切る val child = el.children() .map { ElementType.valueOf(it) } .firstOrNull { it == ElementType.TABLE_BODY } if (child !== null) { return null } return TableLayout(context).apply { layoutParams = LinearLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT) } } private fun createListView(): View? { return TextView(context).apply { Paris.style(this).apply(R.style.article_item) } } private fun createTableRow(): View? { return TableRow(context).apply { gravity = CENTER } } private fun parseImage(el: Element): View? { val url = HttpUrl.parse(el.absUrl("src")) ?: return null return ImageView(context).apply { layoutParams = LinearLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT) Paris.style(this).apply(R.style.article_item) val headers = LazyHeaders.Builder() .addHeader("Content-Type", "image/bmp") .build() val glideUrl = GlideUrl(url.url(), headers) Glide.with(context) .asBitmap() .load(glideUrl) .listener(GlideLoggingListener()) .into(this) } } private fun parseTitle(el: Element, type: ElementType): View? { return TextView(context).apply { layoutParams = LinearLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT) if (type == ElementType.HEADER2) { background = ContextCompat.getDrawable(context, R.drawable.sub_title) } Paris.style(this).apply(R.style.subtitle) text = el.text() } } private fun parseParagraph(el: Element): View? { // 空行の区切りに使用されているものがあるので無視する if (el.text() == " ") { return null } return TextView(context).apply { Paris.style(this).apply(R.style.article_item) text = SpannableString(el.text()) } } private fun parseLink(el: Element): View? { return TextView(context).apply { Paris.style(this).apply(R.style.article_item) text = el.text() } } private fun parseListItem(el: Element, parent: ElementViewHolder, brotherCount: Int): View? { return TextView(context).apply { Paris.style(this).apply(R.style.article_item) val prefix = "\t".repeat(parent.depth - 1) text = when (parent.type) { ElementType.UNORDERED_LIST -> prefix + UNORDERED_LIST_DECORATOR + el.text() ElementType.ORDERED_LIST -> "$prefix$brotherCount. ${el.text()}" ElementType.DEFINITION_LIST -> prefix + el.text() else -> "" } } } private fun parseTableData(el: Element): View? { return TextView(context).apply { Paris.style(this).apply(R.style.table_data) text = el.text() } } private fun compose(self: ElementViewHolder, parent: ElementViewHolder): Boolean { return when (self.type) { ElementType.DEFINITION_TERM, ElementType.PARAGRAPH, ElementType.UNORDERED_LIST, ElementType.ORDERED_LIST, ElementType.DEFINITION_LIST, ElementType.LIST_ITEM, ElementType.DEFINITION_DESCRIPTION, ElementType.TABLE_DATA -> composeText(self.view as TextView, parent) ElementType.LINK -> composeLink(self.view as TextView, parent) ElementType.TABLE_ROW -> composeTableRow(self.view as TableRow, parent) else -> false } } private fun composeText(self: TextView, parent: ElementViewHolder): Boolean { when (parent.type) { ElementType.DEFINITION_TERM, ElementType.PARAGRAPH, ElementType.UNORDERED_LIST, ElementType.ORDERED_LIST, ElementType.DEFINITION_LIST, ElementType.LIST_ITEM, ElementType.DEFINITION_DESCRIPTION -> { val parentTextView = parent.view as TextView parentTextView.apply { text = String.format("%s\n%s", parentTextView.text.toString(), self.text.trim()) } } ElementType.TABLE_DATA -> return true ElementType.TABLE_ROW -> { val tableRow = parent.view as TableRow tableRow.addView(self) } else -> return false } return true } private fun composeLink(self: TextView, parent: ElementViewHolder): Boolean { when (parent.type) { ElementType.DEFINITION_TERM, ElementType.PARAGRAPH, ElementType.UNORDERED_LIST, ElementType.ORDERED_LIST, ElementType.DEFINITION_LIST, ElementType.LIST_ITEM, ElementType.DEFINITION_DESCRIPTION, ElementType.TABLE_DATA -> { val parentTextView = parent.view as TextView val builder = SpannableStringBuilder(parentTextView.text) Regex(self.text.toString()).findAll(parentTextView.text).forEach { val style = TextAppearanceSpan(context, R.color.link) builder.setSpan(style, it.range.start, it.range.last + 1, SPAN_INCLUSIVE_INCLUSIVE) } parentTextView.text = builder } ElementType.TABLE_ROW -> { val castParentView = parent.view as TableRow val builder = SpannableStringBuilder(self.text).apply { val style = TextAppearanceSpan(context, R.color.link) setSpan(style, 0, self.text.length - 1, SPAN_INCLUSIVE_INCLUSIVE) } self.text = builder castParentView.addView(self) } else -> return false } return true } private fun composeTableRow(self: TableRow, parent: ElementViewHolder): Boolean { val table = parent.view as? TableLayout ?: return false table.addView(self) return true } companion object { private const val UNORDERED_LIST_DECORATOR = "・" } private enum class ElementType(val tagName: String, val level: Int = 0) { UNKNOWN(""), TABLE("table"), TABLE_BODY("tbody"), TABLE_ROW("tr"), TABLE_DATA("td"), IMAGE("img"), HEADER1("h1", 1), HEADER2("h2", 2), HEADER3("h3", 3), HEADER4("h4", 4), HEADER5("h5", 5), HEADER6("h6", 6), PARAGRAPH("p"), LINK("a"), UNORDERED_LIST("ul"), ORDERED_LIST("ol"), LIST_ITEM("li"), DEFINITION_LIST("dl"), DEFINITION_TERM("dt"), DEFINITION_DESCRIPTION("dd"); companion object { fun valueOf(element: Element) = values().firstOrNull { it.tagName === element.tagName() } ?: UNKNOWN } } private data class ElementViewHolder( var view: View? = null, var type: ElementType = ElementType.UNKNOWN, var depth: Int = 1, var orderString: String = "" ) }