mirror of
https://github.com/MinimalBible/MinimalBible
synced 2025-07-04 15:25:14 -04:00
Refactor the tag parsing system
Much cleaner, I like this a whole lot more.
This commit is contained in:
@ -11,21 +11,17 @@ import org.crosswire.jsword.versification.getBooks
|
||||
import org.crosswire.jsword.versification.BibleBook
|
||||
import org.bspeice.minimalbible.activity.viewer.BookAdapter.ChapterInfo
|
||||
import rx.subjects.PublishSubject
|
||||
import org.bspeice.minimalbible.service.lookup.VerseLookup
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.text.style.StyleSpan
|
||||
import android.graphics.Typeface
|
||||
import android.text.style.SuperscriptSpan
|
||||
import android.text.style.RelativeSizeSpan
|
||||
import android.util.TypedValue
|
||||
import org.bspeice.minimalbible.service.format.osisparser.OsisParser
|
||||
import android.util.Log
|
||||
|
||||
/**
|
||||
* Adapter used for displaying a book
|
||||
* Displays one chapter at a time,
|
||||
* as each TextView widget is it's own line break
|
||||
*/
|
||||
class BookAdapter(val b: Book, val lookup: VerseLookup,
|
||||
val prefs: BibleViewerPreferences)
|
||||
class BookAdapter(val b: Book, val prefs: BibleViewerPreferences)
|
||||
: RecyclerView.Adapter<PassageView>() {
|
||||
|
||||
val versification = b.getVersification()
|
||||
@ -81,7 +77,7 @@ class BookAdapter(val b: Book, val lookup: VerseLookup,
|
||||
// TODO: Prefs object for handling this?
|
||||
emptyView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 16f)
|
||||
|
||||
val passage = PassageView(emptyView, b, lookup)
|
||||
val passage = PassageView(emptyView, b)
|
||||
return passage
|
||||
}
|
||||
|
||||
@ -114,85 +110,20 @@ class BookAdapter(val b: Book, val lookup: VerseLookup,
|
||||
}
|
||||
}
|
||||
|
||||
class PassageView(val v: TextView, val b: Book, val lookup: VerseLookup)
|
||||
class PassageView(val v: TextView, val b: Book)
|
||||
: RecyclerView.ViewHolder(v) {
|
||||
|
||||
// Span to be applied to an individual verse - doesn't know about the sizes
|
||||
// of other verses so that's why start and end are relative
|
||||
/**
|
||||
* A holder object that knows how apply itself to a SpannableStringBuilder
|
||||
* Since we don't know ahead of time where this verse will end up relative to the
|
||||
* entire TextView (since there is one chapter per TextView) we use a start and end
|
||||
* relative to the verse text itself. That is, rStart of 0 indicates verse text start,
|
||||
* and rEnd of (text.length - 1) indicates the end of verse text
|
||||
* @param span The span object we should apply
|
||||
* @param rStart When the span should begin, relative to the verse
|
||||
* @param rEnd When the span should end, relative to the verse
|
||||
*/
|
||||
data class SpanHolder(val span: Any?, val rStart: Int, val rEnd: Int) {
|
||||
// TODO: Is there a more case-class like way of doing this?
|
||||
class object {
|
||||
val EMPTY = SpanHolder(null, 0, 0)
|
||||
}
|
||||
/**
|
||||
* Apply this span object to the specified builder
|
||||
* Tries to be as close to immutable as possible. The offset is used to calculate
|
||||
* the absolute position of when this span should start and end, since
|
||||
* rStart and rEnd are relative to the verse text, and know nothing about the
|
||||
* rest of the text in the TextView
|
||||
*/
|
||||
fun apply(builder: SpannableStringBuilder, offset: Int): SpannableStringBuilder {
|
||||
if (span != null)
|
||||
builder.setSpan(span, rStart + offset, rEnd + offset, 0)
|
||||
return builder
|
||||
}
|
||||
fun buildOrdinal(verse: Int, info: ChapterInfo) =
|
||||
b.getVersification().decodeOrdinal(verse + info.vOffset)
|
||||
|
||||
fun getAllVerses(verses: Progression<Int>, info: ChapterInfo): SpannableStringBuilder {
|
||||
val builder = SpannableStringBuilder()
|
||||
verses.forEach { OsisParser(builder).appendVerse(b, buildOrdinal(it, info)) }
|
||||
return builder
|
||||
}
|
||||
|
||||
// TODO: getRawVerse shouldn't know how to decode ints
|
||||
fun getRawVerse(verse: Int): String =
|
||||
lookup.getText(b.getVersification().decodeOrdinal(verse))
|
||||
|
||||
// TODO: This code is nasty, not sure how to refactor, but it needs doing
|
||||
fun getProcessedVerse(verseOrdinal: Int, info: ChapterInfo): Pair<String, List<SpanHolder>> {
|
||||
val rawText = getRawVerse(verseOrdinal)
|
||||
// To be honest, I have no idea why I need to subtract one. But I do.
|
||||
val relativeVerse = verseOrdinal - info.vOffset - 1
|
||||
val processedText = when (relativeVerse) {
|
||||
0 -> ""
|
||||
1 -> "${info.chapter} $rawText"
|
||||
else -> "$relativeVerse$rawText"
|
||||
}
|
||||
val spans: List<SpanHolder> = listOf(
|
||||
when (relativeVerse) {
|
||||
0 -> SpanHolder.EMPTY
|
||||
1 -> SpanHolder(StyleSpan(Typeface.BOLD), 0, info.chapter.toString().length)
|
||||
else -> SpanHolder(SuperscriptSpan(), 0, relativeVerse.toString().length)
|
||||
}
|
||||
)
|
||||
val secondSpan =
|
||||
if (relativeVerse > 1)
|
||||
// TODO: No magic numbers!
|
||||
spans plus SpanHolder(RelativeSizeSpan(0.6f), 0, relativeVerse.toString().length)
|
||||
else
|
||||
spans
|
||||
|
||||
return Pair(processedText, secondSpan)
|
||||
}
|
||||
|
||||
fun getAllVerses(verses: Progression<Int>, info: ChapterInfo) =
|
||||
verses.map { getProcessedVerse(it + info.vOffset, info) }
|
||||
// For each verse, get the text
|
||||
.fold(SpannableStringBuilder(), { initialBuilder, versePair ->
|
||||
val offset = initialBuilder.length()
|
||||
val builderWithText = initialBuilder append versePair.first
|
||||
|
||||
// And apply all spans
|
||||
versePair.second.fold(builderWithText, { postBuilder, span ->
|
||||
span.apply(postBuilder, offset)
|
||||
})
|
||||
})
|
||||
|
||||
fun bind(info: ChapterInfo) {
|
||||
Log.d("PassageView", "Binding chapter ${info.chapter}")
|
||||
v setText getAllVerses(info.vStart..info.vEnd, info)
|
||||
}
|
||||
}
|
||||
|
@ -8,21 +8,26 @@ import org.crosswire.jsword.book.OSISUtil
|
||||
import org.crosswire.jsword.book.BookData
|
||||
import org.crosswire.jsword.book.Book
|
||||
import kotlin.properties.Delegates
|
||||
import org.bspeice.minimalbible.service.format.osisparser.handler.TagHandler
|
||||
import org.bspeice.minimalbible.service.format.osisparser.handler.VerseHandler
|
||||
import org.bspeice.minimalbible.service.format.osisparser.handler.UnknownHandler
|
||||
import android.text.SpannableStringBuilder
|
||||
import org.bspeice.minimalbible.service.format.osisparser.handler.DivineHandler
|
||||
|
||||
/**
|
||||
* Parse out the OSIS XML into whatever we want!
|
||||
* TODO: Speed up parsing. This is the single most expensive repeated operation
|
||||
*/
|
||||
class OsisParser() : DefaultHandler() {
|
||||
class OsisParser(val builder: SpannableStringBuilder) : DefaultHandler() {
|
||||
|
||||
// Don't pass a verse as part of the constructor, but still guarantee
|
||||
// that it will exist
|
||||
var verseContent: VerseContent by Delegates.notNull()
|
||||
|
||||
// TODO: Implement a stack to keep min API 8
|
||||
val doWrite = ArrayDeque<Boolean>()
|
||||
val handlerStack = ArrayDeque<TagHandler>()
|
||||
|
||||
fun getVerse(b: Book, v: Verse): VerseContent {
|
||||
fun appendVerse(b: Book, v: Verse): VerseContent {
|
||||
verseContent = VerseContent(v)
|
||||
BookData(b, v).getSAXEventProvider() provideSAXEvents this
|
||||
return verseContent
|
||||
@ -31,18 +36,17 @@ class OsisParser() : DefaultHandler() {
|
||||
override fun startElement(uri: String, localName: String,
|
||||
qName: String, attributes: Attributes) {
|
||||
when (localName) {
|
||||
OSISUtil.OSIS_ELEMENT_VERSE -> doWrite.push(true)
|
||||
"divineName" -> doWrite.push(true)
|
||||
else -> doWrite.push(false)
|
||||
OSISUtil.OSIS_ELEMENT_VERSE -> handlerStack push VerseHandler()
|
||||
"divineName" -> handlerStack push DivineHandler()
|
||||
else -> handlerStack push UnknownHandler(localName)
|
||||
}
|
||||
}
|
||||
|
||||
override fun endElement(uri: String, localName: String, qName: String) {
|
||||
doWrite.pop()
|
||||
handlerStack.pop()
|
||||
}
|
||||
|
||||
override fun characters(ch: CharArray, start: Int, length: Int) {
|
||||
if (doWrite.peek())
|
||||
verseContent = verseContent appendContent String(ch)
|
||||
handlerStack.peek().render(builder, verseContent, String(ch))
|
||||
}
|
||||
}
|
@ -7,7 +7,7 @@ import com.google.gson.Gson
|
||||
import org.crosswire.jsword.passage.Verse
|
||||
import java.util.ArrayList
|
||||
|
||||
//TODO: JSON Streaming parsing? http://instagram-engineering.tumblr.com/post/97147584853/json-parsing
|
||||
// TODO: Refactor to a VerseInfo class, not the actual content to support "streaming" parsing
|
||||
data class VerseContent(val v: Verse,
|
||||
val id: Int = v.getOrdinal(),
|
||||
val bookName: String = v.getName(),
|
||||
|
@ -0,0 +1,19 @@
|
||||
package org.bspeice.minimalbible.service.format.osisparser.handler
|
||||
|
||||
import android.text.SpannableStringBuilder
|
||||
import org.bspeice.minimalbible.service.format.osisparser.VerseContent
|
||||
import android.text.style.RelativeSizeSpan
|
||||
|
||||
/**
|
||||
* Created by bspeice on 12/1/14.
|
||||
*/
|
||||
class DivineHandler() : TagHandler {
|
||||
override fun render(builder: SpannableStringBuilder, info: VerseContent, chars: String) {
|
||||
this buildDivineName chars forEach { it apply builder }
|
||||
}
|
||||
|
||||
fun buildDivineName(chars: String) =
|
||||
listOf(AppendArgs(chars take 1, null),
|
||||
AppendArgs(chars drop 1, RelativeSizeSpan(.9f))
|
||||
)
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
package org.bspeice.minimalbible.service.format.osisparser.handler
|
||||
|
||||
import android.text.SpannableStringBuilder
|
||||
import org.bspeice.minimalbible.service.format.osisparser.VerseContent
|
||||
import android.text.style.CharacterStyle
|
||||
|
||||
/**
|
||||
* Created by bspeice on 12/1/14.
|
||||
*/
|
||||
|
||||
trait TagHandler {
|
||||
fun render(builder: SpannableStringBuilder, info: VerseContent, chars: String)
|
||||
|
||||
|
||||
}
|
||||
|
||||
data class AppendArgs(val text: String, val span: Any?) {
|
||||
fun apply(builder: SpannableStringBuilder) {
|
||||
val offset = builder.length()
|
||||
builder.append(text)
|
||||
when (span) {
|
||||
is List<*> -> span.forEach { builder.setSpan(it, offset, offset + text.length, 0) }
|
||||
is CharacterStyle -> builder.setSpan(span, offset, offset + text.length, 0)
|
||||
}
|
||||
builder.setSpan(span, offset, offset + text.length, 0)
|
||||
}
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
package org.bspeice.minimalbible.service.format.osisparser.handler
|
||||
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.util.Log
|
||||
import org.bspeice.minimalbible.service.format.osisparser.VerseContent
|
||||
|
||||
/**
|
||||
* Created by bspeice on 12/1/14.
|
||||
*/
|
||||
class UnknownHandler(val tagName: String) : TagHandler {
|
||||
override fun render(builder: SpannableStringBuilder, info: VerseContent, chars: String) {
|
||||
Log.d("UnknownHandler", "Unknown tag $tagName received text: $chars")
|
||||
}
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
package org.bspeice.minimalbible.service.format.osisparser.handler
|
||||
|
||||
import android.text.SpannableStringBuilder
|
||||
import org.bspeice.minimalbible.service.format.osisparser.VerseContent
|
||||
import android.text.style.StyleSpan
|
||||
import android.graphics.Typeface
|
||||
import android.text.style.SuperscriptSpan
|
||||
import android.text.style.RelativeSizeSpan
|
||||
|
||||
/**
|
||||
* Created by bspeice on 12/1/14.
|
||||
*/
|
||||
class VerseHandler() : TagHandler {
|
||||
var isVerseStart = true
|
||||
|
||||
override fun render(builder: SpannableStringBuilder, info: VerseContent, chars: String) {
|
||||
buildVerseHeader(info.chapter, info.verseNum, isVerseStart) apply builder
|
||||
builder append chars
|
||||
isVerseStart = false
|
||||
}
|
||||
|
||||
fun buildVerseHeader(chapter: Int, verseNum: Int, verseStart: Boolean): AppendArgs =
|
||||
when {
|
||||
!verseStart -> AppendArgs("", null)
|
||||
verseNum == 1 -> AppendArgs("$chapter", StyleSpan(Typeface.BOLD))
|
||||
else -> AppendArgs("${verseNum}", listOf(SuperscriptSpan(), RelativeSizeSpan(.75f)))
|
||||
}
|
||||
}
|
@ -1,86 +0,0 @@
|
||||
package org.bspeice.minimalbible.service.lookup
|
||||
|
||||
import org.crosswire.jsword.book.Book
|
||||
import android.support.v4.util.LruCache
|
||||
import rx.functions.Action1
|
||||
import org.crosswire.jsword.passage.Verse
|
||||
import rx.subjects.PublishSubject
|
||||
import rx.schedulers.Schedulers
|
||||
import org.bspeice.minimalbible.service.format.osisparser.OsisParser
|
||||
import org.crosswire.jsword.book.getVersification
|
||||
import org.bspeice.minimalbible.service.format.osisparser.VerseContent
|
||||
|
||||
/**
|
||||
* Do the low-level work of getting a verse's content
|
||||
* This class is currently impossible to test because I can't mock Verse objects
|
||||
*/
|
||||
open class VerseLookup(val b: Book) : Action1<Verse> {
|
||||
|
||||
val cache = VerseCache()
|
||||
/**
|
||||
* The listener servers to let other objects notify us we should pre-cache verses
|
||||
*/
|
||||
val listener: PublishSubject<Verse> = PublishSubject.create();
|
||||
|
||||
{
|
||||
listener.observeOn(Schedulers.io())
|
||||
.subscribe(this)
|
||||
}
|
||||
|
||||
fun getVerseId(v: Verse) = v.getOrdinal()
|
||||
|
||||
fun getText(v: Verse): String =
|
||||
if (cache contains v)
|
||||
cache[getVerseId(v)]
|
||||
else {
|
||||
val content = doLookup(v).content
|
||||
notify(v)
|
||||
content
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform the ugly work of getting the actual data for a verse
|
||||
* Note that we build the verse object, JS should be left to determine how
|
||||
* it is displayed.
|
||||
*
|
||||
* @param v The verse to look up
|
||||
* @return The string content of this verse
|
||||
*/
|
||||
fun doLookup(v: Verse): VerseContent = OsisParser().getVerse(b, v)
|
||||
fun doLookup(ordinal: Int): VerseContent = OsisParser()
|
||||
.getVerse(b, b.getVersification() decodeOrdinal ordinal)
|
||||
|
||||
/**
|
||||
* Not necessary, but helpful if you let us know ahead of time we should pre-cache a verse.
|
||||
* For example, if something showed up in search results, it'd be helpful to start
|
||||
* looking up some of the results.
|
||||
*
|
||||
* @param v The verse we should pre-cache
|
||||
*/
|
||||
fun notify(v: Verse) = listener onNext v
|
||||
|
||||
/**
|
||||
* Let someone know if the cache contains a verse we want
|
||||
* Also provides a nice wrapper if the underlying cache isn't working properly.
|
||||
*
|
||||
* @param v The verse to check
|
||||
* @return Whether we can retrieve the verse from our cache
|
||||
*/
|
||||
open fun contains(v: Verse) = cache[v.getOrdinal()] != null
|
||||
|
||||
// IO Thread operations begin here
|
||||
|
||||
/**
|
||||
* Someone was nice enough to let us know that a verse was recently called,
|
||||
* we should probably cache its neighbors!
|
||||
*/
|
||||
override fun call(t1: Verse?) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
open class VerseCache : LruCache<Int, String>(1000000) {
|
||||
|
||||
fun getId(v: Verse) = v.getOrdinal()
|
||||
fun contains(v: Verse) = (this get getId(v)) != null
|
||||
}
|
Reference in New Issue
Block a user