diff --git a/common/src/commonMain/kotlin/cz/frantisekmasa/wfrp_master/common/compendium/domain/importer/books/ArchivesOfTheEmpire1.kt b/common/src/commonMain/kotlin/cz/frantisekmasa/wfrp_master/common/compendium/domain/importer/books/ArchivesOfTheEmpire1.kt index 5dab03c1f..3219a0675 100644 --- a/common/src/commonMain/kotlin/cz/frantisekmasa/wfrp_master/common/compendium/domain/importer/books/ArchivesOfTheEmpire1.kt +++ b/common/src/commonMain/kotlin/cz/frantisekmasa/wfrp_master/common/compendium/domain/importer/books/ArchivesOfTheEmpire1.kt @@ -51,26 +51,26 @@ object ArchivesOfTheEmpire1 : Book, CareerSource, TrappingSource { override fun resolveToken(textToken: TextToken): Token? { if (textToken.fontName.endsWith("CaslonAntique-Bold-SC700")) { - return Token.BoxHeader(textToken.text) + return Token.BoxHeader(textToken) } if (textToken.fontName.endsWith("CaslonAntique")) { if (textToken.fontSizePt == 11f) { - return Token.TableHeading(textToken.text) + return Token.TableHeading(textToken.text, textToken.metadata) } if (textToken.fontSizePt == 15f) { - return Token.BoxHeader(textToken.text) + return Token.BoxHeader(textToken) } } if (textToken.fontName.endsWith("CaslonAntique-Bold")) { if (textToken.fontSizePt == 19f || textToken.fontSizePt == 22f) { - return Token.Heading1(textToken.text) + return Token.Heading1(textToken.text, textToken.metadata) } if (textToken.fontSizePt == 10f) { - return Token.TableHeadCell(textToken.text) + return Token.TableHeadCell(textToken) } } @@ -79,28 +79,24 @@ object ArchivesOfTheEmpire1 : Book, CareerSource, TrappingSource { } if (textToken.fontSizePt == 12f && textToken.fontName.endsWith("ACaslonPro-Bold")) { - return Token.Heading3(textToken.text) + return Token.Heading3(textToken) } if (textToken.fontSizePt == 8f && textToken.fontName.endsWith("ACaslonPro-Regular")) { - return Token.BodyCellPart( - text = textToken.text, - y = textToken.y, - height = textToken.height, - ) + return Token.BodyCellPart(textToken) } if (textToken.fontSizePt == 9f) { if (textToken.fontName.endsWith("ACaslonPro-Bold")) { - return Token.BoldPart(textToken.text) + return Token.BoldPart(textToken) } if (textToken.fontName.endsWith("ACaslonPro-Italic")) { - return Token.ItalicsPart(textToken.text) + return Token.ItalicsPart(textToken) } if (textToken.fontName.endsWith("ACaslonPro-Regular")) { - return Token.NormalPart(textToken.text) + return Token.NormalPart(textToken) } } diff --git a/common/src/commonMain/kotlin/cz/frantisekmasa/wfrp_master/common/compendium/domain/importer/books/ArchivesOfTheEmpire2.kt b/common/src/commonMain/kotlin/cz/frantisekmasa/wfrp_master/common/compendium/domain/importer/books/ArchivesOfTheEmpire2.kt index 60ef81f79..2b71c6640 100644 --- a/common/src/commonMain/kotlin/cz/frantisekmasa/wfrp_master/common/compendium/domain/importer/books/ArchivesOfTheEmpire2.kt +++ b/common/src/commonMain/kotlin/cz/frantisekmasa/wfrp_master/common/compendium/domain/importer/books/ArchivesOfTheEmpire2.kt @@ -28,8 +28,8 @@ object ArchivesOfTheEmpire2 : Book, CareerSource, SpellSource, TrappingSource { return CareerParser( tokenMapper = { when (it) { - is Token.BodyCellPart -> Token.NormalPart(it.text) - is Token.TableHeadCell -> Token.BoldPart(it.text) + is Token.BodyCellPart -> Token.NormalPart(it.text, it.metadata) + is Token.TableHeadCell -> Token.BoldPart(it.text, it.metadata) else -> it } }, @@ -89,29 +89,29 @@ object ArchivesOfTheEmpire2 : Book, CareerSource, SpellSource, TrappingSource { override fun resolveToken(textToken: TextToken): Token? { if (textToken.fontName.endsWith("CaslonAntique-Bold-SC700")) { if (textToken.fontSizePt == 12f || textToken.fontSizePt == 18f) { - return Token.Heading1(textToken.text) + return Token.Heading1(textToken) } - return Token.BoxHeader(textToken.text) + return Token.BoxHeader(textToken) } if (textToken.fontName.endsWith("CaslonAntique")) { if (textToken.fontSizePt == 11f) { - return Token.TableHeading(textToken.text) + return Token.TableHeading(textToken) } if (textToken.fontSizePt == 15f) { - return Token.BoxHeader(textToken.text) + return Token.BoxHeader(textToken) } } if (textToken.fontName.endsWith("CaslonAntique-Bold")) { if (textToken.fontSizePt == 19f || textToken.fontSizePt == 22f) { - return Token.Heading1(textToken.text) + return Token.Heading1(textToken) } if (textToken.fontSizePt == 10f) { - return Token.TableHeadCell(textToken.text) + return Token.TableHeadCell(textToken) } } @@ -120,43 +120,41 @@ object ArchivesOfTheEmpire2 : Book, CareerSource, SpellSource, TrappingSource { } if (textToken.fontSizePt == 12f && textToken.fontName.endsWith("ACaslonPro-Bold")) { - return Token.Heading3(textToken.text) + return Token.Heading3(textToken) } if (textToken.fontSizePt == 8f) { if (textToken.fontName.endsWith("ACaslonPro-Regular")) { return Token.BodyCellPart( text = textToken.text, - y = textToken.y, - height = textToken.height, + metadata = Token.Metadata( + y = textToken.y, + height = textToken.height + ) ) } if (textToken.fontName.endsWith("ACaslonPro-Italic")) { - return Token.ItalicsPart(textToken.text) + return Token.ItalicsPart(textToken) } } if (textToken.fontSizePt == 9f) { if (textToken.fontName.endsWith("ACaslonPro-Bold")) { - return Token.BoldPart(textToken.text) + return Token.BoldPart(textToken) } if (textToken.fontName.endsWith("ACaslonPro-Italic")) { - return Token.ItalicsPart(textToken.text) + return Token.ItalicsPart(textToken) } if (textToken.fontName.endsWith("ACaslonPro-Regular")) { // These table cells are not formatted as table cells if (textToken.text == "Ironfist" || textToken.text == "Big Ogre Club") { - return Token.BodyCellPart( - text = textToken.text, - y = textToken.y, - height = textToken.height, - ) + return Token.BodyCellPart(textToken) } - return Token.NormalPart(textToken.text) + return Token.NormalPart(textToken) } } diff --git a/common/src/commonMain/kotlin/cz/frantisekmasa/wfrp_master/common/compendium/domain/importer/books/ArchivesOfTheEmpire3.kt b/common/src/commonMain/kotlin/cz/frantisekmasa/wfrp_master/common/compendium/domain/importer/books/ArchivesOfTheEmpire3.kt new file mode 100644 index 000000000..fe5cb1eac --- /dev/null +++ b/common/src/commonMain/kotlin/cz/frantisekmasa/wfrp_master/common/compendium/domain/importer/books/ArchivesOfTheEmpire3.kt @@ -0,0 +1,182 @@ +package cz.frantisekmasa.wfrp_master.common.compendium.domain.importer.books + +import cz.frantisekmasa.wfrp_master.common.compendium.domain.Career +import cz.frantisekmasa.wfrp_master.common.compendium.domain.JournalEntry +import cz.frantisekmasa.wfrp_master.common.compendium.domain.Trapping +import cz.frantisekmasa.wfrp_master.common.compendium.domain.importer.parsers.Document +import cz.frantisekmasa.wfrp_master.common.compendium.domain.importer.parsers.Lexer +import cz.frantisekmasa.wfrp_master.common.compendium.domain.importer.parsers.RulesParser +import cz.frantisekmasa.wfrp_master.common.compendium.domain.importer.parsers.TextPosition +import cz.frantisekmasa.wfrp_master.common.compendium.domain.importer.parsers.TextToken +import cz.frantisekmasa.wfrp_master.common.compendium.domain.importer.parsers.Token +import cz.frantisekmasa.wfrp_master.common.compendium.domain.importer.parsers.TokenStream +import cz.frantisekmasa.wfrp_master.common.compendium.domain.importer.parsers.TwoColumnPdfLexer +import cz.frantisekmasa.wfrp_master.common.compendium.domain.importer.parsers.trappings.ArmourParser +import cz.frantisekmasa.wfrp_master.common.compendium.domain.importer.parsers.trappings.description.HeadingDescriptionParser +import cz.frantisekmasa.wfrp_master.common.compendium.domain.importer.sources.CareerSource +import cz.frantisekmasa.wfrp_master.common.compendium.domain.importer.sources.JournalEntrySource +import cz.frantisekmasa.wfrp_master.common.compendium.domain.importer.sources.TrappingSource +import kotlin.math.max +import kotlin.math.min + +object ArchivesOfTheEmpire3 : Book, JournalEntrySource, TrappingSource, CareerSource { + override val name = "Archives of the Empire — Volume III" + override val tableFootnotesAsNormalText: Boolean = true + + override fun importCareers(document: Document): List { + return emptyList() + } + + override fun importJournalEntries(document: Document): List { + val lexer = TwoColumnPdfLexer(document, this) + return buildList { + fun parseRules( + pages: Iterable, + excludedTables: Set = emptySet(), + excludedBoxes: Set = emptySet(), + commonParents: List, + supportedParents: List>, + tokenMapper: (Token) -> Token = { it }, + ): List { + val entries = + RulesParser( + excludedBoxes = excludedBoxes, + excludedTables = excludedTables, + ).import( + TokenStream( + pages.asSequence() + .flatMap { lexer.getTokens(it).toList() } + .flatten() + .map(tokenMapper), + ), + ) + + return entries.filter { it.parents in supportedParents } + .map { it.copy(parents = commonParents + it.parents) } + .toList() + } + + addAll( + parseRules( + 36..36, + commonParents = listOf("The Consumers’ Guide"), + supportedParents = + listOf( + listOf("Armour Rules", "Armour Qualities"), + listOf("Armour Rules", "Armour Flaws"), + ), + ) + .asSequence() + .map { + it.copy( + parents = listOf("The Consumers’ Guide", "Armour", it.parents.last()), + ) + } + ) + } + } + + override fun importTrappings(document: Document): List { + return ArmourParser( + document, + this, + HeadingDescriptionParser(), + lexerModifier = { lexer -> + object : Lexer { + override fun getTokens(page: Int): Sequence { + return lexer.getTokens(page).map { + when (it) { + is Token.BoldPart -> Token.TableHeadCell(it.text, it.metadata) + is Token.NormalPart -> Token.BodyCellPart( + text = it.text, + metadata = it.metadata, + ) + is Token.Heading2 -> Token.BoxHeader(it.text, it.metadata) + else -> it + } + } + } + } + }, + ).parse(37, 38..38) + } + + override fun areSameStyle(a: TextPosition, b: TextPosition): Boolean { + return super.areSameStyle(a, b) || arePartsOfHeading2(a, b) + } + + private fun arePartsOfHeading2( + a: TextPosition, + b: TextPosition, + ): Boolean { + // Some headings are a mix of 12pt and 18pt font + return a.getFont().getName() == b.getFont().getName() && + a.getFont().getName().endsWith("CaslonAntique-Bold-SC700") && + min(a.getFontSizeInPt(), b.getFontSizeInPt()) == 12f && + max(a.getFontSizeInPt(), b.getFontSizeInPt()) == 18f + } + + override fun resolveToken(textToken: TextToken): Token? { + if (textToken.fontName.endsWith("CaslonAntique-Bold-SC700")) { + if (textToken.fontSizePt == 12f || textToken.fontSizePt == 18f) { + return Token.Heading2(textToken) + } + + return Token.BoxHeader(textToken) + } + + if (textToken.fontName.endsWith("CaslonAntique")) { + if (textToken.fontSizePt == 18f) { + return Token.BoxHeader(textToken) + } + } + + if (textToken.fontName.endsWith("CaslonAntique-Bold")) { + if (textToken.fontSizePt == 14f) { + return Token.TableHeading(textToken) + } + + if (textToken.fontSizePt == 19f || textToken.fontSizePt == 22f) { + return Token.Heading1(textToken) + } + + if (textToken.fontSizePt == 10f) { + return Token.TableHeadCell(textToken) + } + } + + if (textToken.fontName.endsWith("crossbatstfb") && textToken.text == "h") { + return Token.CrossIcon + } + + if (textToken.fontSizePt == 12f && textToken.fontName.endsWith("ACaslonPro-Bold")) { + return Token.Heading3(textToken) + } + + if (textToken.fontSizePt == 8f && textToken.fontName.endsWith("ACaslonPro-Regular")) { + return Token.BodyCellPart( + text = textToken.text, + metadata = Token.Metadata( + y = textToken.y, + height = textToken.height, + ) + ) + } + + if (textToken.fontSizePt == 9f) { + if (textToken.fontName.endsWith("ACaslonPro-Bold")) { + return Token.BoldPart(textToken) + } + + if (textToken.fontName.endsWith("ACaslonPro-Italic")) { + return Token.ItalicsPart(textToken) + } + + if (textToken.fontName.endsWith("ACaslonPro-Regular")) { + return Token.NormalPart(textToken) + } + } + + return null + } +} \ No newline at end of file diff --git a/common/src/commonMain/kotlin/cz/frantisekmasa/wfrp_master/common/compendium/domain/importer/books/CoreRulebook.kt b/common/src/commonMain/kotlin/cz/frantisekmasa/wfrp_master/common/compendium/domain/importer/books/CoreRulebook.kt index 3f8d7e8eb..68c15b76b 100644 --- a/common/src/commonMain/kotlin/cz/frantisekmasa/wfrp_master/common/compendium/domain/importer/books/CoreRulebook.kt +++ b/common/src/commonMain/kotlin/cz/frantisekmasa/wfrp_master/common/compendium/domain/importer/books/CoreRulebook.kt @@ -312,7 +312,7 @@ object CoreRulebook : tokenMapper = { token -> if (token is Token.Heading3 && token.text == "Armour Flaws\n") { // This is incorrectly formatted in the PDF, see the bottom of page 300 - Token.Heading2(token.text) + Token.Heading2(token.text, token.metadata) } else { token } @@ -380,20 +380,20 @@ object CoreRulebook : override fun resolveToken(textToken: TextToken): Token? { if (textToken.fontName.endsWith("CaslonAntique") && textToken.fontSizePt == 15f) { - return Token.BoxHeader(textToken.text) + return Token.BoxHeader(textToken) } if (textToken.fontName.endsWith("CaslonAntique") && textToken.fontSizePt == 10f) { - return Token.BoxContent(textToken.text) + return Token.BoxContent(textToken) } if (textToken.fontName.endsWith("CaslonAntique,Bold")) { if (textToken.fontSizePt == 19f) { - return Token.Heading1(textToken.text) + return Token.Heading1(textToken) } if (textToken.fontSizePt == 10f && textToken.text.isNotBlank()) { - return Token.TableHeadCell(textToken.text) + return Token.TableHeadCell(textToken) } } @@ -403,14 +403,14 @@ object CoreRulebook : if (textToken.fontName.endsWith("CaslonAntique-Bold-SC700")) { if (textToken.fontSizePt == 12f || textToken.fontSizePt == 18f) { - return Token.Heading2(textToken.text) + return Token.Heading2(textToken) } - return Token.BoxHeader(textToken.text) + return Token.BoxHeader(textToken) } if (textToken.fontName.endsWith("ACaslonPro-Bold") && textToken.fontSizePt == 12f) { - return Token.Heading3(textToken.text) + return Token.Heading3(textToken) } if (textToken.text.startsWith("•")) { @@ -419,16 +419,16 @@ object CoreRulebook : if (textToken.fontSizePt == 9.0f) { if (textToken.fontName.endsWith("ACaslonPro-Bold")) { - return Token.BoldPart(textToken.text) + return Token.BoldPart(textToken) } if (textToken.fontName.endsWith("ACaslonPro-Regular")) { if (heightEquals(textToken.height, 6.39f)) { - return Token.ItalicsPart(textToken.text) + return Token.ItalicsPart(textToken) } if (heightEquals(textToken.height, 6.2f)) { - return Token.NormalPart(textToken.text) + return Token.NormalPart(textToken) } } } @@ -436,13 +436,15 @@ object CoreRulebook : if (textToken.fontSizePt == 8f && textToken.fontName.endsWith("ACaslonPro-Regular")) { return Token.BodyCellPart( text = textToken.text, - y = textToken.y, - height = textToken.height, + metadata = Token.Metadata( + y = textToken.y, + height = textToken.height + ) ) } if (textToken.fontSizePt == 11f && textToken.fontName.endsWith("CaslonAntique")) { - return Token.TableHeading(textToken.text) + return Token.TableHeading(textToken) } return null diff --git a/common/src/commonMain/kotlin/cz/frantisekmasa/wfrp_master/common/compendium/domain/importer/books/EnemyInShadowsCompanion.kt b/common/src/commonMain/kotlin/cz/frantisekmasa/wfrp_master/common/compendium/domain/importer/books/EnemyInShadowsCompanion.kt index e78a18ddb..fac73ed1b 100644 --- a/common/src/commonMain/kotlin/cz/frantisekmasa/wfrp_master/common/compendium/domain/importer/books/EnemyInShadowsCompanion.kt +++ b/common/src/commonMain/kotlin/cz/frantisekmasa/wfrp_master/common/compendium/domain/importer/books/EnemyInShadowsCompanion.kt @@ -60,30 +60,30 @@ object EnemyInShadowsCompanion : Book, SpellSource { (textToken.fontSizePt == 12f || textToken.fontSizePt == 18f) && textToken.fontName.endsWith("CaslonAntique-Bold-SC700") ) { - return Token.Heading2(textToken.text) + return Token.Heading2(textToken) } if ( (textToken.fontSizePt == 19f || textToken.fontSizePt == 22f) && textToken.fontName.endsWith("CaslonAntique-Bold") ) { - return Token.Heading2(textToken.text) + return Token.Heading2(textToken) } if (textToken.fontSizePt == 12f && textToken.fontName.endsWith("ACaslonPro-Bold")) { - return Token.Heading3(textToken.text) + return Token.Heading3(textToken) } if (textToken.fontSizePt == 10f || textToken.fontSizePt == 9f) { if (textToken.fontName.endsWith("ACaslonPro-Bold")) { - return Token.BoldPart(textToken.text) + return Token.BoldPart(textToken) } if (textToken.fontName.endsWith("ACaslonPro-Italic")) { - return Token.ItalicsPart(textToken.text) + return Token.ItalicsPart(textToken) } if (textToken.fontName.endsWith("ACaslonPro-Regular")) { - return Token.NormalPart(textToken.text) + return Token.NormalPart(textToken) } } diff --git a/common/src/commonMain/kotlin/cz/frantisekmasa/wfrp_master/common/compendium/domain/importer/books/SeaOfClaws.kt b/common/src/commonMain/kotlin/cz/frantisekmasa/wfrp_master/common/compendium/domain/importer/books/SeaOfClaws.kt index 5c48feb44..746a54c8c 100644 --- a/common/src/commonMain/kotlin/cz/frantisekmasa/wfrp_master/common/compendium/domain/importer/books/SeaOfClaws.kt +++ b/common/src/commonMain/kotlin/cz/frantisekmasa/wfrp_master/common/compendium/domain/importer/books/SeaOfClaws.kt @@ -37,8 +37,8 @@ object SeaOfClaws : Book, SpellSource, TalentSource, CareerSource, MiracleSource override fun importCareers(document: Document): List { val tokenMapper: (Token) -> Token = { when (it) { - is Token.BodyCellPart -> Token.NormalPart(it.text) - is Token.TableHeadCell -> Token.BoldPart(it.text) + is Token.BodyCellPart -> Token.NormalPart(it.text, it.metadata) + is Token.TableHeadCell -> Token.BoldPart(it.text, it.metadata) else -> it } } @@ -90,25 +90,25 @@ object SeaOfClaws : Book, SpellSource, TalentSource, CareerSource, MiracleSource override fun resolveToken(textToken: TextToken): Token? { if (textToken.fontSizePt == 10f) { if (textToken.fontName.endsWith("ACaslonPro-Bold")) { - return Token.BoldPart(textToken.text) + return Token.BoldPart(textToken) } if (textToken.fontName.endsWith("ACaslonPro-Italic")) { - return Token.ItalicsPart(textToken.text) + return Token.ItalicsPart(textToken) } if (textToken.fontName.endsWith("ACaslonPro-Regular")) { - return Token.NormalPart(textToken.text) + return Token.NormalPart(textToken) } } if (textToken.fontName.endsWith("CaslonAntique-Bold")) { if (textToken.fontSizePt == 22f) { - return Token.Heading1(textToken.text) + return Token.Heading1(textToken) } if (textToken.fontSizePt == 10f) { - return Token.TableHeading(textToken.text) + return Token.TableHeading(textToken) } } @@ -117,29 +117,31 @@ object SeaOfClaws : Book, SpellSource, TalentSource, CareerSource, MiracleSource } if (textToken.fontSizePt == 12f && textToken.fontName.endsWith("ACaslonPro-Bold")) { - return Token.Heading3(textToken.text) + return Token.Heading3(textToken) } if (textToken.fontSizePt == 9f) { if (textToken.fontName.endsWith("ACaslonPro-Regular")) { return Token.BodyCellPart( text = textToken.text, - y = textToken.y, - height = textToken.height, + metadata = Token.Metadata( + y = textToken.y, + height = textToken.height, + ) ) } if (textToken.fontName.endsWith("ACaslonPro-Bold")) { - return Token.TableHeadCell(textToken.text) + return Token.TableHeadCell(textToken) } } if (textToken.fontName.endsWith("CaslonAntique-Bold-SC700")) { if (textToken.fontSizePt == 12f || textToken.fontSizePt == 18f) { - return Token.Heading2(textToken.text) + return Token.Heading2(textToken) } - return Token.BoxHeader(textToken.text) + return Token.BoxHeader(textToken) } return null diff --git a/common/src/commonMain/kotlin/cz/frantisekmasa/wfrp_master/common/compendium/domain/importer/books/UpInArms.kt b/common/src/commonMain/kotlin/cz/frantisekmasa/wfrp_master/common/compendium/domain/importer/books/UpInArms.kt index d4c18a42f..d55b3e10e 100644 --- a/common/src/commonMain/kotlin/cz/frantisekmasa/wfrp_master/common/compendium/domain/importer/books/UpInArms.kt +++ b/common/src/commonMain/kotlin/cz/frantisekmasa/wfrp_master/common/compendium/domain/importer/books/UpInArms.kt @@ -37,8 +37,8 @@ object UpInArms : Book, CareerSource, TalentSource, TrappingSource, JournalEntry return CareerParser( tokenMapper = { when (it) { - is Token.BodyCellPart -> Token.NormalPart(it.text) - is Token.TableHeadCell -> Token.BoldPart(it.text) + is Token.BodyCellPart -> Token.NormalPart(it.text, it.metadata) + is Token.TableHeadCell -> Token.BoldPart(it.text, it.metadata) else -> it } }, @@ -108,7 +108,7 @@ object UpInArms : Book, CareerSource, TalentSource, TrappingSource, JournalEntry .flatten() .map { when (it) { - is Token.BoxHeader -> Token.Heading2(it.text) + is Token.BoxHeader -> Token.Heading2(it.text, it.metadata) else -> it } } @@ -182,20 +182,20 @@ object UpInArms : Book, CareerSource, TalentSource, TrappingSource, JournalEntry override fun resolveToken(textToken: TextToken): Token? { if (textToken.fontName.endsWith("CaslonAntique-Bold-SC700")) { - return Token.BoxHeader(textToken.text) + return Token.BoxHeader(textToken) } if (textToken.fontName.endsWith("CaslonAntique-Bold")) { if (textToken.fontSizePt == 19f || textToken.fontSizePt == 22f) { - return Token.Heading1(textToken.text) + return Token.Heading1(textToken) } if (textToken.fontSizePt == 10f) { - return Token.TableHeading(textToken.text) + return Token.TableHeading(textToken) } if (textToken.fontSizePt == 15f) { - return Token.BoxHeader(textToken.text) + return Token.BoxHeader(textToken) } } @@ -204,36 +204,38 @@ object UpInArms : Book, CareerSource, TalentSource, TrappingSource, JournalEntry } if (textToken.fontSizePt == 12f && textToken.fontName.endsWith("ACaslonPro-Bold")) { - return Token.Heading3(textToken.text) + return Token.Heading3(textToken) } if (textToken.fontSizePt == 10f || textToken.fontSizePt == 9f) { if (textToken.fontName.endsWith("ACaslonPro-Bold")) { if (textToken.height == 6.201f) { - return Token.TableHeadCell(textToken.text) + return Token.TableHeadCell(textToken) } - return Token.BoldPart(textToken.text) + return Token.BoldPart(textToken) } if (textToken.fontName.endsWith("ACaslonPro-Italic")) { - return Token.ItalicsPart(textToken.text) + return Token.ItalicsPart(textToken) } if (textToken.fontName.endsWith("ACaslonPro-Regular")) { if (textToken.height == 6.201f) { return Token.BodyCellPart( text = textToken.text, - y = textToken.y, - height = textToken.height, + metadata = Token.Metadata( + y = textToken.y, + height = textToken.height + ) ) } - return Token.NormalPart(textToken.text) + return Token.NormalPart(textToken) } if (textToken.fontName.endsWith("ACaslonPro-BoldItalic")) { - return Token.BoldItalicPart(textToken.text) + return Token.BoldItalicPart(textToken) } } diff --git a/common/src/commonMain/kotlin/cz/frantisekmasa/wfrp_master/common/compendium/domain/importer/books/WindsOfMagic.kt b/common/src/commonMain/kotlin/cz/frantisekmasa/wfrp_master/common/compendium/domain/importer/books/WindsOfMagic.kt index 596e0707f..d14615863 100644 --- a/common/src/commonMain/kotlin/cz/frantisekmasa/wfrp_master/common/compendium/domain/importer/books/WindsOfMagic.kt +++ b/common/src/commonMain/kotlin/cz/frantisekmasa/wfrp_master/common/compendium/domain/importer/books/WindsOfMagic.kt @@ -26,7 +26,7 @@ object WindsOfMagic : Book, CareerSource, SpellSource, TrappingSource { return CareerParser( tokenMapper = { when (it) { - is Token.BodyCellPart -> Token.NormalPart(it.text) + is Token.BodyCellPart -> Token.NormalPart(it.text, it.metadata) else -> it } }, @@ -114,16 +114,16 @@ object WindsOfMagic : Book, CareerSource, SpellSource, TrappingSource { override fun resolveToken(textToken: TextToken): Token? { if (textToken.fontName.endsWith("CaslonAntique-Bold-SC700")) { - return Token.BoxHeader(textToken.text) + return Token.BoxHeader(textToken) } if (textToken.fontName.endsWith("CaslonAntique-Bold")) { if (textToken.fontSizePt == 19f || textToken.fontSizePt == 22f) { - return Token.Heading1(textToken.text) + return Token.Heading1(textToken) } if (textToken.fontSizePt == 10f) { - return Token.TableHeadCell(textToken.text) + return Token.TableHeadCell(textToken) } } @@ -132,28 +132,30 @@ object WindsOfMagic : Book, CareerSource, SpellSource, TrappingSource { } if (textToken.fontSizePt == 12f && textToken.fontName.endsWith("ACaslonPro-Bold")) { - return Token.Heading3(textToken.text) + return Token.Heading3(textToken) } if (textToken.fontSizePt == 9f && textToken.fontName.endsWith("ACaslonPro-Regular")) { return Token.BodyCellPart( text = textToken.text, - y = textToken.y, - height = textToken.height, + metadata = Token.Metadata( + y = textToken.y, + height = textToken.height + ) ) } if (textToken.fontSizePt == 10f || textToken.fontSizePt == 9f) { if (textToken.fontName.endsWith("ACaslonPro-Bold")) { - return Token.BoldPart(textToken.text) + return Token.BoldPart(textToken) } if (textToken.fontName.endsWith("ACaslonPro-Italic")) { - return Token.ItalicsPart(textToken.text) + return Token.ItalicsPart(textToken) } if (textToken.fontName.endsWith("ACaslonPro-Regular")) { - return Token.NormalPart(textToken.text) + return Token.NormalPart(textToken) } } diff --git a/common/src/commonMain/kotlin/cz/frantisekmasa/wfrp_master/common/compendium/domain/importer/parsers/BlessingParser.kt b/common/src/commonMain/kotlin/cz/frantisekmasa/wfrp_master/common/compendium/domain/importer/parsers/BlessingParser.kt index 3f612545e..b1f5901c2 100644 --- a/common/src/commonMain/kotlin/cz/frantisekmasa/wfrp_master/common/compendium/domain/importer/parsers/BlessingParser.kt +++ b/common/src/commonMain/kotlin/cz/frantisekmasa/wfrp_master/common/compendium/domain/importer/parsers/BlessingParser.kt @@ -39,7 +39,7 @@ class BlessingParser { val effect = MarkdownBuilder.buildMarkdown( - listOf(Token.NormalPart(effectStart)) + + listOf(Token.NormalPart(effectStart, Token.Metadata.Empty)) + stream.consumeUntil { it is Token.Heading3 } .filterIsInstance(), ) diff --git a/common/src/commonMain/kotlin/cz/frantisekmasa/wfrp_master/common/compendium/domain/importer/parsers/CareerParser.kt b/common/src/commonMain/kotlin/cz/frantisekmasa/wfrp_master/common/compendium/domain/importer/parsers/CareerParser.kt index 4417e35bb..6c9546277 100644 --- a/common/src/commonMain/kotlin/cz/frantisekmasa/wfrp_master/common/compendium/domain/importer/parsers/CareerParser.kt +++ b/common/src/commonMain/kotlin/cz/frantisekmasa/wfrp_master/common/compendium/domain/importer/parsers/CareerParser.kt @@ -75,7 +75,7 @@ class CareerParser( val descriptionStart = mutableListOf() if (text.size > 1) { - descriptionStart += text.drop(1).map { Token.NormalPart(it) } + descriptionStart += text.drop(1).map { Token.NormalPart(it, Token.Metadata.Empty) } } val species = @@ -99,7 +99,7 @@ class CareerParser( descriptionStart + secondColumn.filterIsInstance().map { if (it is Token.ItalicsPart) { - Token.BlockQuote(it.text) + Token.BlockQuote(it.text, it.metadata) } else { it } diff --git a/common/src/commonMain/kotlin/cz/frantisekmasa/wfrp_master/common/compendium/domain/importer/parsers/DefaultLayoutPdfLexer.kt b/common/src/commonMain/kotlin/cz/frantisekmasa/wfrp_master/common/compendium/domain/importer/parsers/DefaultLayoutPdfLexer.kt index 54c962b43..ddfcdedb6 100644 --- a/common/src/commonMain/kotlin/cz/frantisekmasa/wfrp_master/common/compendium/domain/importer/parsers/DefaultLayoutPdfLexer.kt +++ b/common/src/commonMain/kotlin/cz/frantisekmasa/wfrp_master/common/compendium/domain/importer/parsers/DefaultLayoutPdfLexer.kt @@ -10,8 +10,8 @@ class DefaultLayoutPdfLexer( private val structure: PdfStructure, private val mergeSubsequentTokens: Boolean = true, private val sortTokens: Boolean = false, -) { - fun getTokens(page: Int): Sequence { +): Lexer { + override fun getTokens(page: Int): Sequence { val stripper = TextStripper() stripper.setSortByPosition(sortTokens && !structure.tokensSorted) stripper.setStartPage(page) diff --git a/common/src/commonMain/kotlin/cz/frantisekmasa/wfrp_master/common/compendium/domain/importer/parsers/DiseaseParser.kt b/common/src/commonMain/kotlin/cz/frantisekmasa/wfrp_master/common/compendium/domain/importer/parsers/DiseaseParser.kt index fc1e35f09..e3ffd5d72 100644 --- a/common/src/commonMain/kotlin/cz/frantisekmasa/wfrp_master/common/compendium/domain/importer/parsers/DiseaseParser.kt +++ b/common/src/commonMain/kotlin/cz/frantisekmasa/wfrp_master/common/compendium/domain/importer/parsers/DiseaseParser.kt @@ -25,8 +25,8 @@ class DiseaseParser( tokens .map { when (it) { - is Token.BodyCellPart -> Token.NormalPart(it.text) - is Token.TableHeadCell -> Token.BoldPart(it.text) + is Token.BodyCellPart -> Token.NormalPart(it.text, it.metadata) + is Token.TableHeadCell -> Token.BoldPart(it.text, it.metadata) else -> it } }.toList() diff --git a/common/src/commonMain/kotlin/cz/frantisekmasa/wfrp_master/common/compendium/domain/importer/parsers/Lexer.kt b/common/src/commonMain/kotlin/cz/frantisekmasa/wfrp_master/common/compendium/domain/importer/parsers/Lexer.kt new file mode 100644 index 000000000..4ec13a11e --- /dev/null +++ b/common/src/commonMain/kotlin/cz/frantisekmasa/wfrp_master/common/compendium/domain/importer/parsers/Lexer.kt @@ -0,0 +1,5 @@ +package cz.frantisekmasa.wfrp_master.common.compendium.domain.importer.parsers + +interface Lexer { + fun getTokens(page: Int): Sequence +} \ No newline at end of file diff --git a/common/src/commonMain/kotlin/cz/frantisekmasa/wfrp_master/common/compendium/domain/importer/parsers/MarkdownBuilder.kt b/common/src/commonMain/kotlin/cz/frantisekmasa/wfrp_master/common/compendium/domain/importer/parsers/MarkdownBuilder.kt index 5091dec94..9d6239bbd 100644 --- a/common/src/commonMain/kotlin/cz/frantisekmasa/wfrp_master/common/compendium/domain/importer/parsers/MarkdownBuilder.kt +++ b/common/src/commonMain/kotlin/cz/frantisekmasa/wfrp_master/common/compendium/domain/importer/parsers/MarkdownBuilder.kt @@ -6,6 +6,7 @@ import arrow.core.nonEmptyListOf object MarkdownBuilder { private const val BULLET_POINT_PLACEHOLDER = "•" + // TODO: Use different tokens that parser tokens fun buildMarkdown(tokens: List): String { val flattened = mutableListOf>() diff --git a/common/src/commonMain/kotlin/cz/frantisekmasa/wfrp_master/common/compendium/domain/importer/parsers/MiracleParser.kt b/common/src/commonMain/kotlin/cz/frantisekmasa/wfrp_master/common/compendium/domain/importer/parsers/MiracleParser.kt index ab4cd1c18..3bd14e0d6 100644 --- a/common/src/commonMain/kotlin/cz/frantisekmasa/wfrp_master/common/compendium/domain/importer/parsers/MiracleParser.kt +++ b/common/src/commonMain/kotlin/cz/frantisekmasa/wfrp_master/common/compendium/domain/importer/parsers/MiracleParser.kt @@ -51,7 +51,7 @@ class MiracleParser { val effect = MarkdownBuilder.buildMarkdown( - listOf(Token.NormalPart(effectStart)) + + listOf(Token.NormalPart(effectStart, Token.Metadata.Empty)) + stream.consumeUntil { it is Token.Heading3 || isCultHeading(it) } .filterIsInstance(), ) diff --git a/common/src/commonMain/kotlin/cz/frantisekmasa/wfrp_master/common/compendium/domain/importer/parsers/RulesParser.kt b/common/src/commonMain/kotlin/cz/frantisekmasa/wfrp_master/common/compendium/domain/importer/parsers/RulesParser.kt index d1867e774..a3a1fa165 100644 --- a/common/src/commonMain/kotlin/cz/frantisekmasa/wfrp_master/common/compendium/domain/importer/parsers/RulesParser.kt +++ b/common/src/commonMain/kotlin/cz/frantisekmasa/wfrp_master/common/compendium/domain/importer/parsers/RulesParser.kt @@ -89,12 +89,12 @@ class RulesParser( if (token is Token.BoxHeader) { // Make sure we do not merge two boxes add(Token.BlankLine) - add(Token.BlockQuote("### ${cleanupHeading(token.text)}")) + add(Token.BlockQuote("### ${cleanupHeading(token.text)}", Token.Metadata.Empty)) continue } if (token is Token.BoxContent) { - add(Token.BlockQuote(token.text)) + add(Token.BlockQuote(token.text, token.metadata)) continue } diff --git a/common/src/commonMain/kotlin/cz/frantisekmasa/wfrp_master/common/compendium/domain/importer/parsers/SpellParser.kt b/common/src/commonMain/kotlin/cz/frantisekmasa/wfrp_master/common/compendium/domain/importer/parsers/SpellParser.kt index 9ec345c99..024fe04b8 100644 --- a/common/src/commonMain/kotlin/cz/frantisekmasa/wfrp_master/common/compendium/domain/importer/parsers/SpellParser.kt +++ b/common/src/commonMain/kotlin/cz/frantisekmasa/wfrp_master/common/compendium/domain/importer/parsers/SpellParser.kt @@ -130,7 +130,7 @@ class SpellParser( effect = MarkdownBuilder.buildMarkdown( listOf( - listOf(Token.NormalPart(effectStart)), + listOf(Token.NormalPart(effectStart, Token.Metadata.Empty)), stream.consumeUntil { it is Token.Heading }, ).flatten().filterIsInstance(), ), diff --git a/common/src/commonMain/kotlin/cz/frantisekmasa/wfrp_master/common/compendium/domain/importer/parsers/TableParser.kt b/common/src/commonMain/kotlin/cz/frantisekmasa/wfrp_master/common/compendium/domain/importer/parsers/TableParser.kt index 9a1ac1c19..1214eec6b 100644 --- a/common/src/commonMain/kotlin/cz/frantisekmasa/wfrp_master/common/compendium/domain/importer/parsers/TableParser.kt +++ b/common/src/commonMain/kotlin/cz/frantisekmasa/wfrp_master/common/compendium/domain/importer/parsers/TableParser.kt @@ -1,8 +1,15 @@ package cz.frantisekmasa.wfrp_master.common.compendium.domain.importer.parsers +/** + * Parses table in format: + * + * + *
+ + * ( Token.NormalPart(it.text) + is Token.BodyCellPart -> Token.NormalPart(it.text, it.metadata) else -> it } } .map { when (it) { - is Token.NormalPart -> Token.NormalPart(it.text.replace("*", "")) + is Token.NormalPart -> Token.NormalPart(it.text.replace("*", ""), it.metadata) else -> it } } @@ -148,7 +155,7 @@ class TableParser { if ( isLastColumn && nextToken is Token.BodyCellPart && - nextToken.y > lastToken.y + lastToken.height + nextToken.metadata.y > lastToken.metadata.y + lastToken.metadata.height ) { cells += "" break @@ -163,7 +170,7 @@ class TableParser { val hasAnotherLine = if (isLastColumn) { - (text.endsWith(", ") || text.endsWith(" or ")) // e.g. list of weapon qualities in last column + (text.endsWith(", ") || text.endsWith(" or ") || text.endsWith(" ")) // e.g. list of weapon qualities in last column } else { (text.endsWith(" ") || text.endsWith("’s")) // e.g. "10% \n Perception } diff --git a/common/src/commonMain/kotlin/cz/frantisekmasa/wfrp_master/common/compendium/domain/importer/parsers/TalentParser.kt b/common/src/commonMain/kotlin/cz/frantisekmasa/wfrp_master/common/compendium/domain/importer/parsers/TalentParser.kt index 9c0e8597b..83ad79840 100644 --- a/common/src/commonMain/kotlin/cz/frantisekmasa/wfrp_master/common/compendium/domain/importer/parsers/TalentParser.kt +++ b/common/src/commonMain/kotlin/cz/frantisekmasa/wfrp_master/common/compendium/domain/importer/parsers/TalentParser.kt @@ -34,7 +34,7 @@ class TalentParser { append('\n') }.split('\n', limit = 2) - description += Token.NormalPart(descriptionStart) + description += Token.NormalPart(descriptionStart, Token.Metadata.Empty) var tests = "" @@ -53,7 +53,7 @@ class TalentParser { ) tests = testsLine - description += Token.NormalPart(descriptionStart2) + description += Token.NormalPart(descriptionStart2, Token.Metadata.Empty) } description += diff --git a/common/src/commonMain/kotlin/cz/frantisekmasa/wfrp_master/common/compendium/domain/importer/parsers/TextToken.kt b/common/src/commonMain/kotlin/cz/frantisekmasa/wfrp_master/common/compendium/domain/importer/parsers/TextToken.kt index eb41494e8..be844842f 100644 --- a/common/src/commonMain/kotlin/cz/frantisekmasa/wfrp_master/common/compendium/domain/importer/parsers/TextToken.kt +++ b/common/src/commonMain/kotlin/cz/frantisekmasa/wfrp_master/common/compendium/domain/importer/parsers/TextToken.kt @@ -6,4 +6,9 @@ data class TextToken( val height: Float, val fontSizePt: Float, val y: Float, -) +) { + val metadata get() = Token.Metadata( + y = y, + height = height, + ) +} diff --git a/common/src/commonMain/kotlin/cz/frantisekmasa/wfrp_master/common/compendium/domain/importer/parsers/Token.kt b/common/src/commonMain/kotlin/cz/frantisekmasa/wfrp_master/common/compendium/domain/importer/parsers/Token.kt index 7605c6589..3b62e4d83 100644 --- a/common/src/commonMain/kotlin/cz/frantisekmasa/wfrp_master/common/compendium/domain/importer/parsers/Token.kt +++ b/common/src/commonMain/kotlin/cz/frantisekmasa/wfrp_master/common/compendium/domain/importer/parsers/Token.kt @@ -1,65 +1,110 @@ package cz.frantisekmasa.wfrp_master.common.compendium.domain.importer.parsers +import cz.frantisekmasa.wfrp_master.common.compendium.domain.importer.parsers.TextToken as ParseToken + sealed interface Token { val text: String get() = "" - sealed class TextToken(override val text: String) : Token + data class Metadata( + val y: Float, + val height: Float, + ) { + companion object { + val Empty = Metadata(0f, 0f) + } + } + + sealed class TextToken(override val text: String, val metadata: Metadata) : Token interface Heading : Token { override val text: String } - class Heading1(text: String) : TextToken(text), Heading + class Heading1(text: String, metadata: Metadata) : TextToken(text, metadata), Heading { + constructor(token: ParseToken) : this(token.text, token.metadata) + } - class Heading2(text: String) : TextToken(text), Heading + class Heading2(text: String, metadata: Metadata) : TextToken(text, metadata), Heading { + constructor(token: ParseToken) : this(token.text, token.metadata) + } - class Heading3(text: String) : ParagraphToken(text, "heading3"), Heading + class Heading3(text: String, metadata: Metadata) : ParagraphToken(text, "heading3", metadata), Heading { + constructor(token: ParseToken) : this(token.text, token.metadata) + } - sealed class ParagraphToken(text: String, val type: String) : TextToken(text) { + sealed class ParagraphToken(text: String, val type: String, metadata: Metadata) : TextToken(text, metadata) { override fun toString(): String { - return "ParagraphToken(type=$type, text=$text)" + return "ParagraphToken(type=$type, text=$text, metadata=$metadata)" } } - class NormalPart(text: String) : ParagraphToken(text, "normal") + class NormalPart(text: String, metadata: Metadata) : ParagraphToken(text, "normal", metadata) { + constructor(token: ParseToken) : this(token.text, token.metadata) + } - class ItalicsPart(text: String) : ParagraphToken(text, "italics") + class ItalicsPart(text: String, metadata: Metadata) : ParagraphToken(text, "italics", metadata) { + constructor(token: ParseToken) : this(token.text, token.metadata) + } - class BoldPart(text: String) : ParagraphToken(text, "bold") + class BoldPart(text: String, metadata: Metadata) : ParagraphToken(text, "bold", metadata) { + constructor(token: ParseToken) : this(token.text, token.metadata) + } - class BoldItalicPart(text: String) : ParagraphToken(text, "bold-italic") + class BoldItalicPart(text: String, metadata: Metadata) : ParagraphToken(text, "bold-italic", metadata) { + constructor(token: ParseToken) : this(token.text, token.metadata) + } - object BlankLine : ParagraphToken(text = "\n\n", "blankLine") + object BlankLine : ParagraphToken(text = "\n\n", "blankLine", Metadata(0f, 0f)) - class BlockQuote(text: String) : ParagraphToken(text, "blockQuote") + class BlockQuote(text: String, metadata: Metadata) : ParagraphToken(text, "blockQuote", metadata) - object LineBreak : ParagraphToken("\n", "lineBreak") + object LineBreak : ParagraphToken("\n", "lineBreak", Metadata(0f, 0f)) - sealed class TableValue(override val text: String) : Token + sealed class TableValue(override val text: String, val metadata: Metadata) : Token class BodyCellPart( text: String, - val y: Float, - val height: Float, - ) : TableValue(text) { + metadata: Metadata, + ) : TableValue(text, metadata) { + constructor(token: ParseToken) : this(token.text, token.metadata) + override fun toString(): String { - return "BodyCellPart(text=$text, x=$y, height=$height)" + return "BodyCellPart(text=$text, metadata=$metadata)" } } - class TableHeadCell(text: String) : TableValue(text) { + class TableHeadCell(text: String, metadata: Metadata) : TableValue(text, metadata) { + constructor(token: ParseToken) : this(token.text, token.metadata) override fun toString(): String { - return "TableHeadCell(text=$text)" + return "TableHeadCell(text=$text, metadata=$metadata)" } } - class TableHeading(text: String) : TableValue(text) + class TableHeading(text: String, metadata: Metadata) : TableValue(text, metadata) { + constructor(token: ParseToken) : this(token.text, token.metadata) + override fun toString(): String { + return "TableHeading(text=$text, metadata=$metadata)" + } + } - object BulletPoint : ParagraphToken("\n -", "bulletPoint") + object BulletPoint : ParagraphToken("\n -", "bulletPoint", Metadata(0f, 0f)) object CrossIcon : Token - class BoxHeader(override val text: String) : Token + class BoxHeader(override val text: String, val metadata: Metadata) : Token { + constructor(token: ParseToken) : this(token.text, token.metadata) + + override fun toString(): String { + return "BoxHeader(text=$text, metadata=$metadata)" + } + } + + class BoxContent(override val text: String, val metadata: Metadata) : Token { + constructor(token: ParseToken) : this(token.text, token.metadata) - class BoxContent(override val text: String) : Token + override fun toString(): String { + return "BoxContent(text=$text, metadata=$metadata)" + } + + } } diff --git a/common/src/commonMain/kotlin/cz/frantisekmasa/wfrp_master/common/compendium/domain/importer/parsers/trappings/ArmourParser.kt b/common/src/commonMain/kotlin/cz/frantisekmasa/wfrp_master/common/compendium/domain/importer/parsers/trappings/ArmourParser.kt index 7d9d2e66e..c6c6b4331 100644 --- a/common/src/commonMain/kotlin/cz/frantisekmasa/wfrp_master/common/compendium/domain/importer/parsers/trappings/ArmourParser.kt +++ b/common/src/commonMain/kotlin/cz/frantisekmasa/wfrp_master/common/compendium/domain/importer/parsers/trappings/ArmourParser.kt @@ -5,6 +5,7 @@ import cz.frantisekmasa.wfrp_master.common.compendium.domain.Trapping import cz.frantisekmasa.wfrp_master.common.compendium.domain.TrappingType import cz.frantisekmasa.wfrp_master.common.compendium.domain.importer.parsers.DefaultLayoutPdfLexer import cz.frantisekmasa.wfrp_master.common.compendium.domain.importer.parsers.Document +import cz.frantisekmasa.wfrp_master.common.compendium.domain.importer.parsers.Lexer import cz.frantisekmasa.wfrp_master.common.compendium.domain.importer.parsers.PdfStructure import cz.frantisekmasa.wfrp_master.common.compendium.domain.importer.parsers.TableParser import cz.frantisekmasa.wfrp_master.common.compendium.domain.importer.parsers.trappings.description.TrappingDescriptionParser @@ -19,13 +20,14 @@ class ArmourParser( private val document: Document, private val structure: PdfStructure, private val descriptionParser: TrappingDescriptionParser, + private val lexerModifier: (Lexer) -> Lexer = { it }, ) { fun parse( tablePage: Int, descriptionPages: IntRange, ): List { val parser = TableParser() - val lexer = DefaultLayoutPdfLexer(document, structure, mergeSubsequentTokens = false) + val lexer = lexerModifier(DefaultLayoutPdfLexer(document, structure, mergeSubsequentTokens = false)) val table = parser.findTables(lexer, structure, tablePage, findNames = true) .asSequence() @@ -38,13 +40,13 @@ class ArmourParser( .filter { it.heading != null } .flatMap { section -> val armourType = - matchEnumOrNull(section.heading!!.replace("*", "")) + matchEnumOrNull(normalizeName(section.heading!!.replace("*", ""))) ?: error("Invalid armour type ${section.heading}") section.rows.map { row -> val price = PriceParser.parse(row[1]) val penalty = row[4].trim() - val name = row[0].trim() + val name = normalizeName(row[0].trim()) val comparableName = descriptionParser.comparableName(name) val footnoteNumbers = @@ -65,13 +67,13 @@ class ArmourParser( TrappingType.Armour( type = armourType, locations = locations(row[5]), - points = ArmourPoints(row[6].toInt()), + points = ArmourPoints(optionalValue(row[6])?.toInt() ?: 0), qualities = parseFeatures(row[7]), flaws = parseFeatures(row[7]), ), description = buildString { - if (penalty != "" && penalty != "–") { + if(optionalValue(penalty) != null) { append("**Penalty**: $penalty\n") } @@ -115,4 +117,17 @@ class ArmourParser( .filter { location -> location.name.equals(it, ignoreCase = true) } }.toSet() } + + private fun optionalValue(value: String): String? { + // Archives of the Empire 3 uses "–" for empty cells + return value.takeIf { value.isNotBlank() && value.trim() != "–" && value.trim() != "-" } + } + + private fun normalizeName(name: String): String { + return name + // Archives of the Empire 3 uses "Chainmail" instead of "Mail" + .replace("Chainmail", "Mail", ignoreCase = true) + // Archives of the Empire 3 uses plural in Armour table + .replace("Soft Kits", "Soft Kit", ignoreCase = true) + } } diff --git a/common/src/commonMain/kotlin/cz/frantisekmasa/wfrp_master/common/compendium/domain/importer/parsers/trappings/BasicTrappingsParser.kt b/common/src/commonMain/kotlin/cz/frantisekmasa/wfrp_master/common/compendium/domain/importer/parsers/trappings/BasicTrappingsParser.kt index b29a26c67..93d6eb80c 100644 --- a/common/src/commonMain/kotlin/cz/frantisekmasa/wfrp_master/common/compendium/domain/importer/parsers/trappings/BasicTrappingsParser.kt +++ b/common/src/commonMain/kotlin/cz/frantisekmasa/wfrp_master/common/compendium/domain/importer/parsers/trappings/BasicTrappingsParser.kt @@ -51,7 +51,10 @@ class BasicTrappingsParser( tokens.mapNotNull { when (it) { is Token.BodyCellPart -> it - is Token.NormalPart -> Token.BodyCellPart(it.text, y = 0f, height = Float.MAX_VALUE) + is Token.NormalPart -> Token.BodyCellPart( + text = it.text, + metadata = it.metadata, + ) else -> null } }, diff --git a/common/src/commonMain/kotlin/cz/frantisekmasa/wfrp_master/common/compendium/domain/importer/parsers/trappings/Utils.kt b/common/src/commonMain/kotlin/cz/frantisekmasa/wfrp_master/common/compendium/domain/importer/parsers/trappings/Utils.kt index 0b59fab4a..25818989d 100644 --- a/common/src/commonMain/kotlin/cz/frantisekmasa/wfrp_master/common/compendium/domain/importer/parsers/trappings/Utils.kt +++ b/common/src/commonMain/kotlin/cz/frantisekmasa/wfrp_master/common/compendium/domain/importer/parsers/trappings/Utils.kt @@ -10,6 +10,7 @@ inline fun > matchEnumOrNull( val comparableValue = value + .trim() .replace('-', '_') .replace(' ', '_') diff --git a/common/src/commonMain/kotlin/cz/frantisekmasa/wfrp_master/common/core/domain/trappings/ArmourFlaw.kt b/common/src/commonMain/kotlin/cz/frantisekmasa/wfrp_master/common/core/domain/trappings/ArmourFlaw.kt index c260b2cc0..7d069aa89 100644 --- a/common/src/commonMain/kotlin/cz/frantisekmasa/wfrp_master/common/core/domain/trappings/ArmourFlaw.kt +++ b/common/src/commonMain/kotlin/cz/frantisekmasa/wfrp_master/common/core/domain/trappings/ArmourFlaw.kt @@ -8,12 +8,14 @@ import dev.icerock.moko.parcelize.Parcelize @Immutable enum class ArmourFlaw() : Flaw { PARTIAL, + REQUIRES_KIT, WEAKPOINTS, ; override val translatableName get() = when (this) { PARTIAL -> Str.armour_flaws_partial + REQUIRES_KIT -> Str.armour_flaws_requires_kit WEAKPOINTS -> Str.armour_flaws_weakpoints } diff --git a/common/src/commonMain/kotlin/cz/frantisekmasa/wfrp_master/common/core/domain/trappings/ArmourQuality.kt b/common/src/commonMain/kotlin/cz/frantisekmasa/wfrp_master/common/core/domain/trappings/ArmourQuality.kt index 6851220a4..2ce868a02 100644 --- a/common/src/commonMain/kotlin/cz/frantisekmasa/wfrp_master/common/core/domain/trappings/ArmourQuality.kt +++ b/common/src/commonMain/kotlin/cz/frantisekmasa/wfrp_master/common/core/domain/trappings/ArmourQuality.kt @@ -9,12 +9,18 @@ import dev.icerock.moko.parcelize.Parcelize enum class ArmourQuality : Quality { FLEXIBLE, IMPENETRABLE, + OVERCOAT, + REINFORCED, + VISOR, ; override val translatableName get() = when (this) { FLEXIBLE -> Str.armour_qualities_flexible IMPENETRABLE -> Str.armour_qualities_impenetrable + OVERCOAT -> Str.armour_qualities_overcoat + REINFORCED -> Str.armour_qualities_reinforced + VISOR -> Str.armour_qualities_visor } override val hasRating: Boolean get() = false } diff --git a/common/src/commonMain/kotlin/cz/frantisekmasa/wfrp_master/common/core/domain/trappings/ArmourType.kt b/common/src/commonMain/kotlin/cz/frantisekmasa/wfrp_master/common/core/domain/trappings/ArmourType.kt index 35ddfe64b..4ed303037 100644 --- a/common/src/commonMain/kotlin/cz/frantisekmasa/wfrp_master/common/core/domain/trappings/ArmourType.kt +++ b/common/src/commonMain/kotlin/cz/frantisekmasa/wfrp_master/common/core/domain/trappings/ArmourType.kt @@ -5,22 +5,17 @@ import cz.frantisekmasa.wfrp_master.common.Str import cz.frantisekmasa.wfrp_master.common.core.domain.NamedEnum import dev.icerock.moko.parcelize.Parcelable import dev.icerock.moko.parcelize.Parcelize +import dev.icerock.moko.resources.StringResource @Parcelize @Immutable -enum class ArmourType : NamedEnum, Parcelable { - SOFT_LEATHER, - BOILED_LEATHER, - MAIL, - PLATE, - OTHER, ; +enum class ArmourType(override val translatableName: StringResource) : NamedEnum, Parcelable { + BOILED_LEATHER(Str.armour_types_boiled_leather), + BRIGANDINE(Str.armour_types_brigandine), + SOFT_KIT(Str.armour_types_soft_kit), + SOFT_LEATHER(Str.armour_types_soft_leather), + MAIL(Str.armour_types_mail), + PLATE(Str.armour_types_plate), + OTHER(Str.armour_types_other); - override val translatableName get() = - when (this) { - SOFT_LEATHER -> Str.armour_types_soft_leather - BOILED_LEATHER -> Str.armour_types_boiled_leather - MAIL -> Str.armour_types_mail - PLATE -> Str.armour_types_plate - OTHER -> Str.armour_types_other - } } diff --git a/common/src/commonMain/moko-resources/base/strings.xml b/common/src/commonMain/moko-resources/base/strings.xml index 1654d2892..203c65026 100644 --- a/common/src/commonMain/moko-resources/base/strings.xml +++ b/common/src/commonMain/moko-resources/base/strings.xml @@ -11,6 +11,7 @@ Character ambitions Party ambitions Partial + Requires Kit Weakpoints Armour Points (AP) Armour Flaws @@ -20,13 +21,18 @@ At least one location is required Flexible Impenetrable + Overcoat + Reinforced + Visor Shield Armour is auto-calculated from worn armour trappings. Armour Boiled Leather + Brigandine Mail Other Plate + Soft Kit Soft Leather Sign in Log out diff --git a/common/src/jvmTest/kotlin/CompendiumImportRegressionTest.kt b/common/src/jvmTest/kotlin/CompendiumImportRegressionTest.kt index a08e03857..dbe9b01af 100644 --- a/common/src/jvmTest/kotlin/CompendiumImportRegressionTest.kt +++ b/common/src/jvmTest/kotlin/CompendiumImportRegressionTest.kt @@ -1,6 +1,7 @@ import cz.frantisekmasa.wfrp_master.common.compendium.domain.importer.PdfCompendiumImporter import cz.frantisekmasa.wfrp_master.common.compendium.domain.importer.books.ArchivesOfTheEmpire1 import cz.frantisekmasa.wfrp_master.common.compendium.domain.importer.books.ArchivesOfTheEmpire2 +import cz.frantisekmasa.wfrp_master.common.compendium.domain.importer.books.ArchivesOfTheEmpire3 import cz.frantisekmasa.wfrp_master.common.compendium.domain.importer.books.Book import cz.frantisekmasa.wfrp_master.common.compendium.domain.importer.books.CoreRulebook import cz.frantisekmasa.wfrp_master.common.compendium.domain.importer.books.EnemyInShadowsCompanion @@ -64,6 +65,11 @@ class CompendiumImportRegressionTest { assertMatchesPreviousRuns(ArchivesOfTheEmpire2) } + @Test + fun `Archives of The Empire III`() { + assertMatchesPreviousRuns(ArchivesOfTheEmpire3) + } + @Test fun `Sea of Claws`() { assertMatchesPreviousRuns(SeaOfClaws) diff --git a/common/src/jvmTest/kotlin/ConfigProvider.kt b/common/src/jvmTest/kotlin/ConfigProvider.kt index 21ad684c5..aff039429 100644 --- a/common/src/jvmTest/kotlin/ConfigProvider.kt +++ b/common/src/jvmTest/kotlin/ConfigProvider.kt @@ -1,5 +1,6 @@ import cz.frantisekmasa.wfrp_master.common.compendium.domain.importer.books.ArchivesOfTheEmpire1 import cz.frantisekmasa.wfrp_master.common.compendium.domain.importer.books.ArchivesOfTheEmpire2 +import cz.frantisekmasa.wfrp_master.common.compendium.domain.importer.books.ArchivesOfTheEmpire3 import cz.frantisekmasa.wfrp_master.common.compendium.domain.importer.books.Book import cz.frantisekmasa.wfrp_master.common.compendium.domain.importer.books.CoreRulebook import cz.frantisekmasa.wfrp_master.common.compendium.domain.importer.books.EnemyInShadowsCompanion @@ -34,6 +35,7 @@ object ConfigProvider { EnemyInShadowsCompanion -> "enemy_in_shadows_companion.pdf" ArchivesOfTheEmpire1 -> "archives_of_the_empire_1.pdf" ArchivesOfTheEmpire2 -> "archives_of_the_empire_2.pdf" + ArchivesOfTheEmpire3 -> "archives_of_the_empire_3.pdf" SeaOfClaws -> "sea_of_claws.pdf" else -> null } diff --git a/common/src/jvmTest/kotlin/cz/frantisekmasa/wfrp_master/common/compendium/domain/importer/parsers/RulesParserTest.kt b/common/src/jvmTest/kotlin/cz/frantisekmasa/wfrp_master/common/compendium/domain/importer/parsers/RulesParserTest.kt index 62cff5aa4..5032ae113 100644 --- a/common/src/jvmTest/kotlin/cz/frantisekmasa/wfrp_master/common/compendium/domain/importer/parsers/RulesParserTest.kt +++ b/common/src/jvmTest/kotlin/cz/frantisekmasa/wfrp_master/common/compendium/domain/importer/parsers/RulesParserTest.kt @@ -6,22 +6,23 @@ import kotlin.test.assertEquals class RulesParserTest { @Test fun `correctly parses nested headings`() { + val metadata = Token.Metadata(y = 0f, height = 0f) val tokens = listOf( - Token.Heading1("Heading 1"), - Token.NormalPart("Text 1"), - Token.Heading2("Heading 1.1"), - Token.NormalPart("Text 1.1"), - Token.Heading3("Heading 1.1.1"), - Token.NormalPart("Text 1.1.1"), - Token.Heading2("Heading 1.2"), - Token.NormalPart("Text 1.2"), - Token.Heading2("Heading 1.3"), - Token.NormalPart("Text 1.3"), - Token.Heading3("Text 1.3.1"), - Token.NormalPart("Text 1.3.1"), - Token.Heading1("Heading 2"), - Token.NormalPart("Text 2"), + Token.Heading1("Heading 1", metadata), + Token.NormalPart("Text 1", metadata), + Token.Heading2("Heading 1.1", metadata), + Token.NormalPart("Text 1.1", metadata), + Token.Heading3("Heading 1.1.1", metadata), + Token.NormalPart("Text 1.1.1", metadata), + Token.Heading2("Heading 1.2", metadata), + Token.NormalPart("Text 1.2", metadata), + Token.Heading2("Heading 1.3", metadata), + Token.NormalPart("Text 1.3", metadata), + Token.Heading3("Text 1.3.1", metadata), + Token.NormalPart("Text 1.3.1", metadata), + Token.Heading1("Heading 2", metadata), + Token.NormalPart("Text 2", metadata), ) assertEquals( @@ -42,15 +43,16 @@ class RulesParserTest { @Test fun `flattens entries that have only child sections`() { + val metadata = Token.Metadata(y = 0f, height = 0f) val tokens = listOf( - Token.Heading1("Heading 1"), - Token.Heading1("Heading 2"), - Token.NormalPart("Text 2"), - Token.Heading2("Heading 2.1"), - Token.Heading2("Heading 2.2"), - Token.Heading3("Heading 2.2.1"), - Token.NormalPart("Text 2.2.1"), + Token.Heading1("Heading 1", metadata), + Token.Heading1("Heading 2", metadata), + Token.NormalPart("Text 2", metadata), + Token.Heading2("Heading 2.1", metadata), + Token.Heading2("Heading 2.2", metadata), + Token.Heading3("Heading 2.2.1", metadata), + Token.NormalPart("Text 2.2.1", metadata), ) assertEquals( @@ -66,16 +68,17 @@ class RulesParserTest { @Test fun `unifies casing for heading`() { + val metadata = Token.Metadata(y = 0f, height = 0f) val tokens = listOf( // Words will be title cased - Token.Heading1("headinG 1"), - Token.NormalPart("Text 1"), - Token.Heading2("Heading 1.1"), - Token.NormalPart("Text 1.1"), + Token.Heading1("headinG 1", metadata), + Token.NormalPart("Text 1", metadata), + Token.Heading2("Heading 1.1", metadata), + Token.NormalPart("Text 1.1", metadata), // "of" and "and" are not title cased - Token.Heading2("heading of rules and other"), - Token.NormalPart("Text 1.2"), + Token.Heading2("heading of rules and other", metadata), + Token.NormalPart("Text 1.2", metadata), ) assertEquals(