Refactor the tag parsing system

Much cleaner, I like this a whole lot more.
This commit is contained in:
Bradlee Speice
2014-12-01 11:51:13 -05:00
parent 7f221ed863
commit caf2227555
37 changed files with 117 additions and 2604 deletions

View File

@ -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))
}
}

View File

@ -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(),

View File

@ -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))
)
}

View File

@ -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)
}
}

View File

@ -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")
}
}

View File

@ -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)))
}
}

View File

@ -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
}