# Mysword Format Parser

> MySword + MyBible Format Parser — Pipeline Kotlin Completo

- Skill: `welitonevoc/mysword-format-parser` (Agent Skill)
- Install (CLI): `npx skillmds@latest add welitonevoc/mysword-format-parser`
- Raw SKILL.md: https://api.skillmd.com/api/skills/welitonevoc/mysword-format-parser/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: DevOps & Infra
- Author: welitonevoc (https://skillmd.com/u/welitonevoc)
- Updated: 2026-09-22
- Page: https://skillmd.com/skills/welitonevoc/mysword-format-parser

---


# MySword + MyBible Format Parser — Pipeline Kotlin Completo

> **Dois formatos:** MySword usa GBF tags; MyBible usa tags HTML simples.  
> **Regra:** Camadas 1–3 são Kotlin puro. Apenas camada 4 toca Compose/Material.

---

## 🏛️ Arquitetura de 5 Camadas

```
Scripture (raw String do SQLite)
    │
    ▼ [1] VerseRulesProcessor
    │     Aplica Details.VerseRules (Regex, sep por \t)
    │     ← Kotlin puro, zero Android
    │
    ▼ [2] Tokenizer (format-specific)
    │     MySword → GbfTagTokenizer
    │     MyBible → HtmlTagTokenizer
    │     ← Kotlin puro, zero Android
    │
    ▼ [3] Parser (format-specific)
    │     MySword → GbfTagParser (máquina de estados)
    │     MyBible → MyBibleTagParser
    │     → List<VerseSegment>
    │     ← Kotlin puro, zero Android
    │
    ▼ [4] BibleAnnotatedStringBuilder
    │     List<VerseSegment> → AnnotatedString
    │     ← Depende de Compose / Material3
    │
    ▼ [5] Compose Text()
```

---

## 📦 Modelos de Dados Compartilhados (Pure Kotlin)

```kotlin
data class ParseOptions(
    val showStrongsNumbers: Boolean     = false,
    val showWordsOfJesusInRed: Boolean  = true,   // <FR> MySword, <J> MyBible
    val showAddedWordsInItalic: Boolean = true,   // <FI> MySword, <i> MyBible
    val showTranslatorsNotes: Boolean   = true,   // <RF> MySword, <n> MyBible
    val showCrossReferences: Boolean    = true,   // <RX> MySword, <f> MyBible
    val showMorphologyCodes: Boolean    = false,
    val showTitles: Boolean             = true,
    val isRightToLeft: Boolean          = false,
    val isInterlinear: Boolean          = false,
    val isMySwordFormat: Boolean        = true    // false = MyBible HTML tags
)

sealed class VerseSegment {
    data class PlainText(val text: String) : VerseSegment()
    data class ItalicText(val text: String) : VerseSegment()
    data class RedLetterText(val text: String) : VerseSegment()     // <FR>/<J>
    data class OTQuoteText(val text: String) : VerseSegment()       // <FO>/<t>
    data class UnderlineText(val text: String) : VerseSegment()     // <FU>
    data class EmphasisText(val text: String) : VerseSegment()      // <e> MyBible
    data class HeadingText(val text: String, val level: Int = 1) : VerseSegment()
    object ParagraphBreak : VerseSegment()                          // <CM>/<pb/>
    object LineBreak : VerseSegment()                               // <br/> MyBible
    data class PoetryLine(val pi: Int, val pf: Int) : VerseSegment()
    data class StrongsHebrew(val number: Int) : VerseSegment()
    data class StrongsGreek(val number: Int) : VerseSegment()
    data class MorphologyCode(val code: String) : VerseSegment()
    data class TranslatorsNote(
        val noteText: String, val linkLabel: String? = null
    ) : VerseSegment()
    data class CrossReference(
        val book: Int, val chapter: Int,
        val fromVerse: Int, val toVerse: Int? = null
    ) : VerseSegment()
    data class FootnoteMarker(val marker: String) : VerseSegment()  // <f> MyBible
    data class InterlinearBlock(
        val originalText: String,
        val transliteration: String?,
        val translation: String?,
        val strongsNumber: Int?,
        val isHebrew: Boolean
    ) : VerseSegment()
}

data class ParsedVerse(
    val verseNumber: Int,
    val segments: List<VerseSegment>,
    val isChapterHeader: Boolean = false   // Verse=0 do MySword
)
```

---

## ⚙️ Camada 1: VerseRulesProcessor

```kotlin
/**
 * CRÍTICO: separador entre RegEx e Replacement é TAB (\t), NUNCA espaço!
 * Precompilar TODAS as regras na criação — nunca dentro de process()
 */
class VerseRulesProcessor(verseRulesText: String?) {

    val rules: List<Pair<Regex, String>> = verseRulesText
        ?.lines()?.filter { it.isNotBlank() }
        ?.mapNotNull { line ->
            val parts = line.split("\t")  // ← TAB obrigatório, não espaço
            if (parts.size >= 2)
                try { Regex(parts[0]) to parts[1] }
                catch (e: PatternSyntaxException) { null }
            else null
        } ?: emptyList()

    fun process(scripture: String): String =
        rules.fold(scripture) { acc, (rx, rep) -> acc.replace(rx, rep) }

    companion object { val EMPTY = VerseRulesProcessor(null) }
}
```

---

## 🔍 Camada 2a: GbfTagTokenizer (MySword)

```kotlin
sealed class GbfToken {
    data class Text(val content: String) : GbfToken()
    data class Tag(
        val rawName: String, val upperName: String,
        val isClosing: Boolean, val attributes: String
    ) : GbfToken()
}

object GbfTagTokenizer {
    // ⚠️ PRECOMPILADO como object — nunca recriar dentro de tokenize()
    private val TAG_REGEX = Regex("<(/?)([A-Za-z][A-Za-z0-9]*)([^>]*)>")

    fun tokenize(scripture: String): List<GbfToken> {
        val tokens = mutableListOf<GbfToken>(); var lastEnd = 0
        for (match in TAG_REGEX.findAll(scripture)) {
            if (match.range.first > lastEnd) {
                val t = scripture.substring(lastEnd, match.range.first)
                if (t.isNotEmpty()) tokens += GbfToken.Text(t)
            }
            tokens += GbfToken.Tag(
                rawName = match.groupValues[2],
                upperName = match.groupValues[2].uppercase(),
                isClosing = match.groupValues[1] == "/",
                attributes = match.groupValues[3].trim()
            )
            lastEnd = match.range.last + 1
        }
        if (lastEnd < scripture.length)
            scripture.substring(lastEnd).takeIf { it.isNotEmpty() }
                ?.let { tokens += GbfToken.Text(it) }
        return tokens
    }
}
```

---

## 🏗️ Camada 3a: GbfTagParser — Máquina de Estados Completa (MySword)

```kotlin
object GbfTagParser {
    // ⚠️ TODOS precompilados — nunca dentro de parse()
    private val STRONG_H   = Regex("WH(\\d+)")
    private val STRONG_G   = Regex("WG(\\d+)")
    private val MORPH      = Regex("WT(\\S+)")
    private val CROSS_REF  = Regex("RX(\\d+)\\.(\\d+)\\.(\\d+)(?:-(\\d+))?")
    private val NOTE_LINK  = Regex("q=([^\\s>]+)")
    private val PI         = Regex("PI([0-7])")
    private val PF         = Regex("PF([0-7])")
    private val TS_LEVEL   = Regex("TS([0-9]?)")

    fun parse(tokens: List<GbfToken>, opts: ParseOptions): List<VerseSegment> {
        val out = mutableListOf<VerseSegment>()
        var red = false; var italic = false; var ot = false; var ul = false
        var title = false; var titleLvl = 1
        var note = false; val noteText = StringBuilder(); var noteLbl: String? = null
        var pi = 0; var pf = 0
        // Interlinear state
        var inQ = false
        val iOrig = StringBuilder(); val iTrans = StringBuilder()
        val iXlit = StringBuilder()
        var iStrong: Int? = null; var iHebrew = false
        var inH = false; var inG = false; var inE = false; var inX = false

        for (t in tokens) when (t) {
            is GbfToken.Text -> {
                val s = t.content
                when {
                    note           -> noteText.append(s)
                    inQ && inH     -> iOrig.append(s)
                    inQ && inG     -> iOrig.append(s)
                    inQ && inX     -> iXlit.append(s)
                    inQ && inE     -> iTrans.append(s)
                    title && opts.showTitles -> out += VerseSegment.HeadingText(s, titleLvl)
                    red && opts.showWordsOfJesusInRed -> out += VerseSegment.RedLetterText(s)
                    italic && opts.showAddedWordsInItalic -> out += VerseSegment.ItalicText(s)
                    ot             -> out += VerseSegment.OTQuoteText(s)
                    ul             -> out += VerseSegment.UnderlineText(s)
                    else           -> out += VerseSegment.PlainText(s)
                }
            }
            is GbfToken.Tag -> {
                val n = t.upperName; val c = t.isClosing
                when {
                    n == "FR"             -> red    = !c
                    n == "FI"             -> italic = !c
                    n == "FO"             -> ot     = !c
                    n == "FU"             -> ul     = !c
                    TS_LEVEL.matches(n) && !c -> { title = true; titleLvl = TS_LEVEL.find(n)!!.groupValues[1].toIntOrNull() ?: 1 }
                    TS_LEVEL.matches(n) && c  -> title = false
                    n == "CM"             -> out += VerseSegment.ParagraphBreak
                    n == "CI"             -> out += VerseSegment.PlainText(" ")
                    PI.matches(n)         -> pi = PI.find(n)!!.groupValues[1].toIntOrNull() ?: 0
                    PF.matches(n)         -> { pf = PF.find(n)!!.groupValues[1].toIntOrNull() ?: 0; out += VerseSegment.PoetryLine(pi, pf) }
                    STRONG_H.matches(n) && opts.showStrongsNumbers ->
                        out += VerseSegment.StrongsHebrew(STRONG_H.find(n)!!.groupValues[1].toIntOrNull() ?: 0)
                    STRONG_G.matches(n) && opts.showStrongsNumbers ->
                        out += VerseSegment.StrongsGreek(STRONG_G.find(n)!!.groupValues[1].toIntOrNull() ?: 0)
                    MORPH.matches(n) && opts.showMorphologyCodes ->
                        out += VerseSegment.MorphologyCode(MORPH.find(n)!!.groupValues[1])
                    n == "RF" && !c && opts.showTranslatorsNotes -> {
                        note = true; noteText.clear()
                        noteLbl = NOTE_LINK.find(t.attributes)?.groupValues?.get(1)
                    }
                    n == "RF" && c -> if (note) {
                        out += VerseSegment.TranslatorsNote(noteText.toString(), noteLbl)
                        note = false; noteText.clear(); noteLbl = null
                    }
                    CROSS_REF.matches(n) && opts.showCrossReferences -> {
                        val m = CROSS_REF.find(n)!!
                        out += VerseSegment.CrossReference(m.groupValues[1].toInt(), m.groupValues[2].toInt(), m.groupValues[3].toInt(), m.groupValues[4].toIntOrNull())
                    }
                    // Interlinear
                    n == "Q" && !c && opts.isInterlinear -> {
                        inQ = true; iOrig.clear(); iTrans.clear(); iXlit.clear()
                        iStrong = null; iHebrew = false; inH = false; inG = false; inE = false; inX = false
                    }
                    n == "Q" && c && inQ -> {
                        out += VerseSegment.InterlinearBlock(iOrig.toString(), iXlit.toString().ifBlank { null }, iTrans.toString().ifBlank { null }, iStrong, iHebrew)
                        inQ = false
                    }
                    n == "H" -> { if (!c) { iHebrew = true; inH = true } else inH = false }
                    n == "G" -> { if (!c) { iHebrew = false; inG = true } else inG = false }
                    n == "X" -> { if (!c) inX = true else inX = false }
                    n == "E" || n == "T" -> { if (!c) inE = true else inE = false }
                    inQ && STRONG_H.matches(n) -> { iStrong = STRONG_H.find(n)!!.groupValues[1].toIntOrNull(); iHebrew = true }
                    inQ && STRONG_G.matches(n) -> { iStrong = STRONG_G.find(n)!!.groupValues[1].toIntOrNull(); iHebrew = false }
                }
            }
        }
        return out
    }
}
```

---

## 🏗️ Camada 3b: MyBibleTagParser (tags HTML simples)

```kotlin
/**
 * Parser para o formato MyBible. Tags: <S>, <m>, <i>, <J>, <n>, <e>, <t>, <br/>, <pb/>, <f>
 * Diferente do GBF: Strong's sem prefixo (livro determina H ou G)
 */
object MyBibleTagParser {
    // Precompilados
    private val TAG_REGEX = Regex("<(/?)(S|m|i|J|n|e|t|br|pb|f)([^>]*)/?\\s*>", RegexOption.IGNORE_CASE)
    private val STRONG_NUM = Regex("(\\d+)")

    /**
     * @param isNewTestament true → Strong's são Gregos (G); false → Hebreus (H)
     */
    fun parse(text: String, opts: ParseOptions, isNewTestament: Boolean): List<VerseSegment> {
        val out = mutableListOf<VerseSegment>()
        var lastEnd = 0
        var inJ = false; var inI = false; var inT = false; var inE = false
        var inN = false; val noteText = StringBuilder()

        fun flushText(raw: String) {
            if (raw.isEmpty()) return
            out += when {
                inJ && opts.showWordsOfJesusInRed  -> VerseSegment.RedLetterText(raw)
                inI && opts.showAddedWordsInItalic -> VerseSegment.ItalicText(raw)
                inT  -> VerseSegment.OTQuoteText(raw)
                inE  -> VerseSegment.EmphasisText(raw)
                inN  -> { noteText.append(raw); null } ?: return
                else -> VerseSegment.PlainText(raw)
            }
        }

        for (match in TAG_REGEX.findAll(text)) {
            flushText(text.substring(lastEnd, match.range.first))
            val tag = match.groupValues[2].lowercase()
            val closing = match.groupValues[1] == "/"
            val selfClose = match.value.endsWith("/>") || tag == "br" || tag == "pb"
            val content = match.groupValues[3].trim()

            when (tag) {
                "j"  -> if (closing) inJ = false else inJ = true
                "i"  -> if (closing) inI = false else inI = true
                "t"  -> if (closing) inT = false else inT = true
                "e"  -> if (closing) inE = false else inE = true
                "n"  -> {
                    if (closing && inN) {
                        if (opts.showTranslatorsNotes)
                            out += VerseSegment.TranslatorsNote(noteText.toString())
                        inN = false; noteText.clear()
                    } else inN = true
                }
                "s"  -> {
                    // Strong's sem prefixo — livro determina H ou G
                    val num = STRONG_NUM.find(content)?.groupValues?.get(1)?.toIntOrNull() ?: 0
                    if (opts.showStrongsNumbers) {
                        out += if (isNewTestament) VerseSegment.StrongsGreek(num)
                               else VerseSegment.StrongsHebrew(num)
                    }
                }
                "m"  -> if (opts.showMorphologyCodes && content.isNotBlank())
                             out += VerseSegment.MorphologyCode(content)
                "br" -> out += VerseSegment.LineBreak
                "pb" -> out += VerseSegment.ParagraphBreak
                "f"  -> {
                    if (!closing && opts.showCrossReferences)
                        out += VerseSegment.FootnoteMarker(content)
                }
            }
            lastEnd = match.range.last + 1
        }
        flushText(text.substring(lastEnd))
        return out
    }
}
```

---

## 🎨 Camada 4: BibleAnnotatedStringBuilder (Compose)

```kotlin
data class BibleColors(
    val verseNumber: Color,
    val wordsOfJesus: Color   = Color.Red,
    val otQuote: Color,
    val emphasis: Color,
    val strongsNumber: Color,
    val noteLink: Color,
    val crossRefLink: Color
)

object BibleAnnotatedStringBuilder {
    fun build(verses: List<ParsedVerse>, colors: BibleColors): AnnotatedString =
        buildAnnotatedString {
            verses.forEach { v ->
                if (v.isChapterHeader) {
                    v.segments.filterIsInstance<VerseSegment.HeadingText>().forEach {
                        append("\n")
                        withStyle(SpanStyle(fontWeight = FontWeight.Bold, fontSize = 18.sp)) { append(it.text) }
                        append("\n")
                    }
                    return@forEach
                }
                // Número do versículo
                withStyle(SpanStyle(color = colors.verseNumber, fontSize = 10.sp,
                    baselineShift = BaselineShift.Superscript, fontWeight = FontWeight.Bold)) {
                    append("${v.verseNumber} ")
                }
                v.segments.forEach { seg ->
                    when (seg) {
                        is VerseSegment.PlainText     -> append(seg.text)
                        is VerseSegment.ItalicText    -> withStyle(SpanStyle(fontStyle = FontStyle.Italic)) { append(seg.text) }
                        is VerseSegment.RedLetterText -> withStyle(SpanStyle(color = colors.wordsOfJesus)) { append(seg.text) }
                        is VerseSegment.OTQuoteText   -> withStyle(SpanStyle(color = colors.otQuote)) { append(seg.text) }
                        is VerseSegment.UnderlineText -> withStyle(SpanStyle(textDecoration = TextDecoration.Underline)) { append(seg.text) }
                        is VerseSegment.EmphasisText  -> withStyle(SpanStyle(color = colors.emphasis)) { append(seg.text) }
                        is VerseSegment.HeadingText   -> {
                            append("\n")
                            withStyle(SpanStyle(fontWeight = FontWeight.Bold,
                                fontSize = when (seg.level) { 1 -> 18.sp; 2 -> 16.sp; else -> 14.sp })) { append(seg.text) }
                            append("\n")
                        }
                        is VerseSegment.ParagraphBreak -> append("\n\n")
                        is VerseSegment.LineBreak      -> append("\n")
                        is VerseSegment.PoetryLine     -> append("\n")
                        is VerseSegment.StrongsHebrew  -> {
                            pushStringAnnotation("STRONG_H", seg.number.toString())
                            withStyle(SpanStyle(color = colors.strongsNumber, fontSize = 9.sp, baselineShift = BaselineShift.Superscript)) {
                                append("H${seg.number}")
                            }
                            pop()
                        }
                        is VerseSegment.StrongsGreek   -> {
                            pushStringAnnotation("STRONG_G", seg.number.toString())
                            withStyle(SpanStyle(color = colors.strongsNumber, fontSize = 9.sp, baselineShift = BaselineShift.Superscript)) {
                                append("G${seg.number}")
                            }
                            pop()
                        }
                        is VerseSegment.TranslatorsNote -> {
                            pushStringAnnotation("NOTE", seg.noteText)
                            withStyle(SpanStyle(color = colors.noteLink, fontSize = 9.sp)) { append("[${seg.linkLabel ?: "†"}]") }
                            pop()
                        }
                        is VerseSegment.CrossReference -> {
                            pushStringAnnotation("CROSS_REF", "${seg.book}.${seg.chapter}.${seg.fromVerse}")
                            withStyle(SpanStyle(color = colors.crossRefLink, fontSize = 9.sp)) { append("✝") }
                            pop()
                        }
                        is VerseSegment.FootnoteMarker -> {
                            pushStringAnnotation("FOOTNOTE", seg.marker)
                            withStyle(SpanStyle(color = colors.crossRefLink, fontSize = 9.sp)) { append("[${seg.marker}]") }
                            pop()
                        }
                        is VerseSegment.InterlinearBlock -> {
                            if (seg.originalText.isNotBlank()) {
                                withStyle(SpanStyle(fontSize = 12.sp)) { append(seg.originalText) }
                                seg.translation?.let { append(" ($it) ") }
                            }
                        }
                        else -> { /* MorphologyCode: ignorar se showMorphologyCodes=false */ }
                    }
                }
                append(" ")
            }
        }
}
```

---

## 🔋 Cache e Performance

```kotlin
class BibleRenderPipeline(
    private val rules: VerseRulesProcessor,
    private val opts: ParseOptions
) {
    // ~12.5% da heap para cache de capítulos
    private val cache = LruCache<String, List<ParsedVerse>>(
        maxSize = (Runtime.getRuntime().maxMemory() / 1024 / 8).toInt()
    )

    fun getOrParse(
        key: String,
        rawVerses: List<Pair<Int, String>>,
        isNewTestament: Boolean = true   // para MyBible: determina H vs G em <S>
    ): List<ParsedVerse> = cache.get(key) ?: run {
        val parsed = rawVerses.map { (n, scripture) ->
            val processed = rules.process(scripture)
            val segments = if (opts.isMySwordFormat) {
                val tokens = GbfTagTokenizer.tokenize(processed)
                GbfTagParser.parse(tokens, opts)
            } else {
                MyBibleTagParser.parse(processed, opts, isNewTestament)
            }
            ParsedVerse(verseNumber = n, segments = segments, isChapterHeader = n == 0)
        }
        cache.put(key, parsed)
        parsed
    }

    fun invalidate(key: String) = cache.remove(key)
}

// Strip para FTS — sem criar AnnotatedString
object BibleTextStripper {
    private val ALL_TAGS    = Regex("<[^>]+>")
    private val MULTI_SPACE = Regex("\\s+")

    fun strip(scripture: String): String =
        scripture.replace(ALL_TAGS, " ").replace(MULTI_SPACE, " ").trim()
}
```

---

## ✅ Checklist de Validação (20 itens)

**MySword GBF:**
- [ ] `<FR>...<Fr>` → **VERMELHO** obrigatório
- [ ] `<FI>...<Fi>` → itálico
- [ ] `<FO>...<Fo>` → estilo distinto de texto comum
- [ ] `<FU>...<Fu>` → sublinhado
- [ ] `<TS>...<Ts>`, `<TS1>...<Ts1>` → heading com tamanho maior
- [ ] `<WG>`, `<WH>` → `pushStringAnnotation` clicável
- [ ] `<RF>` e `<RF q=a>` → nota com label customizado
- [ ] `<RX43.3.16>` e `<RX43.3.16-18>` → cross-ref clicável
- [ ] `<CM>` → ParagraphBreak (não texto)
- [ ] Verse=0 → `isChapterHeader=true`
- [ ] VerseRules aplicado **antes** do tokenizer

**MyBible HTML:**
- [ ] `<J>...<J>` → vermelho (palavras de Jesus)
- [ ] `<i>...<i>` → itálico (palavras adicionadas)
- [ ] `<S>N</S>` → Strong's sem prefixo; livro ≤39 = Hebraico, ≥40 = Grego
- [ ] `<n>...<n>` → nota clicável
- [ ] `<pb/>` → ParagraphBreak
- [ ] `<br/>` → LineBreak
- [ ] `<f>marker</f>` → FootnoteMarker clicável

**Ambos:**
- [ ] Regex do tokenizer compilado como `object` (nunca inline)
- [ ] Cache invalidado ao trocar de módulo/tradução

