diff --git a/module/parser/src/commonMain/kotlin/org/cru/godtools/shared/tool/parser/ParserConfig.kt b/module/parser/src/commonMain/kotlin/org/cru/godtools/shared/tool/parser/ParserConfig.kt index 554383fd0..ec374a809 100644 --- a/module/parser/src/commonMain/kotlin/org/cru/godtools/shared/tool/parser/ParserConfig.kt +++ b/module/parser/src/commonMain/kotlin/org/cru/godtools/shared/tool/parser/ParserConfig.kt @@ -31,6 +31,7 @@ data class ParserConfig private constructor( const val FEATURE_CONTENT_CARD = "content_card" const val FEATURE_FLOW = "flow" const val FEATURE_MULTISELECT = "multiselect" + const val FEATURE_PAGE_COLLECTION = "page-collection" internal const val FEATURE_REQUIRED_VERSIONS = "required-versions" } diff --git a/module/parser/src/commonMain/kotlin/org/cru/godtools/shared/tool/parser/model/HasPages.kt b/module/parser/src/commonMain/kotlin/org/cru/godtools/shared/tool/parser/model/HasPages.kt new file mode 100644 index 000000000..e6539ae04 --- /dev/null +++ b/module/parser/src/commonMain/kotlin/org/cru/godtools/shared/tool/parser/model/HasPages.kt @@ -0,0 +1,31 @@ +package org.cru.godtools.shared.tool.parser.model + +import kotlin.experimental.ExperimentalObjCRefinement +import kotlin.js.ExperimentalJsExport +import kotlin.js.JsExport +import kotlin.js.JsName +import kotlin.native.HiddenFromObjC +import kotlin.reflect.KClass +import org.ccci.gto.support.androidx.annotation.RestrictTo +import org.ccci.gto.support.androidx.annotation.RestrictToScope +import org.cru.godtools.shared.tool.parser.model.page.Page + +@JsExport +@OptIn(ExperimentalJsExport::class, ExperimentalObjCRefinement::class) +interface HasPages : Base { + @JsName("_pages") + val pages: List + + fun findPage(id: String?) = id?.let { pages.find { it.id == id } } + + @HiddenFromObjC + @JsExport.Ignore + @RestrictTo(RestrictToScope.LIBRARY) + fun supportsPageType(type: KClass): Boolean + + // region Kotlin/JS interop + @HiddenFromObjC + @JsName("pages") + val jsPages get() = pages.toTypedArray() + // endregion Kotlin/JS interop +} diff --git a/module/parser/src/commonMain/kotlin/org/cru/godtools/shared/tool/parser/model/Manifest.kt b/module/parser/src/commonMain/kotlin/org/cru/godtools/shared/tool/parser/model/Manifest.kt index ae2c282c4..e1acbf815 100644 --- a/module/parser/src/commonMain/kotlin/org/cru/godtools/shared/tool/parser/model/Manifest.kt +++ b/module/parser/src/commonMain/kotlin/org/cru/godtools/shared/tool/parser/model/Manifest.kt @@ -6,6 +6,7 @@ import kotlin.js.ExperimentalJsExport import kotlin.js.JsExport import kotlin.js.JsName import kotlin.native.HiddenFromObjC +import kotlin.reflect.KClass import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.coroutineScope @@ -18,6 +19,7 @@ import org.cru.godtools.shared.common.model.Uri import org.cru.godtools.shared.common.model.isHttpUrl import org.cru.godtools.shared.common.model.toUriOrNull import org.cru.godtools.shared.tool.parser.ParserConfig +import org.cru.godtools.shared.tool.parser.ParserConfig.Companion.FEATURE_PAGE_COLLECTION import org.cru.godtools.shared.tool.parser.internal.AndroidColorInt import org.cru.godtools.shared.tool.parser.internal.DeprecationException import org.cru.godtools.shared.tool.parser.internal.fluidlocale.toLocaleOrNull @@ -27,15 +29,20 @@ import org.cru.godtools.shared.tool.parser.model.Multiselect.Companion.XML_MULTI import org.cru.godtools.shared.tool.parser.model.Multiselect.Companion.XML_MULTISELECT_OPTION_SELECTED_COLOR import org.cru.godtools.shared.tool.parser.model.Styles.Companion.DEFAULT_TEXT_SCALE import org.cru.godtools.shared.tool.parser.model.lesson.DEFAULT_LESSON_NAV_BAR_COLOR +import org.cru.godtools.shared.tool.parser.model.lesson.LessonPage import org.cru.godtools.shared.tool.parser.model.lesson.XMLNS_LESSON +import org.cru.godtools.shared.tool.parser.model.page.CardCollectionPage +import org.cru.godtools.shared.tool.parser.model.page.ContentPage import org.cru.godtools.shared.tool.parser.model.page.DEFAULT_CONTROL_COLOR import org.cru.godtools.shared.tool.parser.model.page.Page +import org.cru.godtools.shared.tool.parser.model.page.PageCollectionPage import org.cru.godtools.shared.tool.parser.model.page.XMLNS_PAGE import org.cru.godtools.shared.tool.parser.model.page.XML_CONTROL_COLOR import org.cru.godtools.shared.tool.parser.model.shareable.Shareable import org.cru.godtools.shared.tool.parser.model.shareable.Shareable.Companion.parseShareableItems import org.cru.godtools.shared.tool.parser.model.shareable.XMLNS_SHAREABLE import org.cru.godtools.shared.tool.parser.model.tips.Tip +import org.cru.godtools.shared.tool.parser.model.tract.TractPage import org.cru.godtools.shared.tool.parser.util.setOnce import org.cru.godtools.shared.tool.parser.xml.XmlPullParser import org.cru.godtools.shared.tool.parser.xml.parseChildren @@ -67,7 +74,7 @@ private const val XML_TIPS_TIP_SRC = "src" @JsExport @OptIn(ExperimentalJsExport::class, ExperimentalObjCRefinement::class) -class Manifest : BaseModel, Styles { +class Manifest : BaseModel, Styles, HasPages { internal companion object { @AndroidColorInt internal val DEFAULT_PRIMARY_COLOR = color(59, 164, 219, 1.0) @@ -94,8 +101,8 @@ class Manifest : BaseModel, Styles { // parse pages if (config.parsePages) { launch { - manifest.pages = manifest.pagesToParse - .map { (fileName, src) -> async { Page.parse(manifest, fileName, parseFile(src)) } } + manifest.pages = manifest.pageXmlFiles + .map { (name, src) -> async { Page.parse(manifest, name, parseFile(src), parseFile) } } .awaitAll().filterNotNull() } } else { @@ -105,8 +112,8 @@ class Manifest : BaseModel, Styles { // parse tips if (config.parseTips) { launch { - manifest.tips = manifest.tipsToParse - .map { (id, src) -> async { Tip(manifest, id, parseFile(src)) } } + manifest.tips = manifest.tipXmlFiles + .map { (id, src) -> async { Tip(manifest, id.orEmpty(), parseFile(src)) } } .awaitAll() .associateBy { it.id } } @@ -182,8 +189,7 @@ class Manifest : BaseModel, Styles { val aemImports: List val categories: List - @JsName("_pages") - var pages: List by setOnce() + override var pages: List by setOnce() private set @VisibleForTesting internal val resources: Map @@ -192,12 +198,12 @@ class Manifest : BaseModel, Styles { internal var tips: Map by setOnce() private set - private val pagesToParse: List> - private val tipsToParse: List> + internal val pageXmlFiles: List + private val tipXmlFiles: List val relatedFiles get() = buildSet { - addAll(pagesToParse.map { it.second }) - addAll(tipsToParse.map { it.second }) + addAll(pageXmlFiles.map { it.src }) + addAll(tipXmlFiles.map { it.src }) addAll(resources.values.mapNotNull { it.localName }) } @@ -249,8 +255,8 @@ class Manifest : BaseModel, Styles { categories = mutableListOf() resources = mutableMapOf() val shareables = mutableListOf() - pagesToParse = mutableListOf() - tipsToParse = mutableListOf() + pageXmlFiles = mutableListOf() + tipXmlFiles = mutableListOf() parser.parseChildren { @Suppress("ktlint:standard:blank-line-between-when-conditions") when (parser.namespace) { @@ -260,10 +266,10 @@ class Manifest : BaseModel, Styles { XML_PAGES -> { val result = parser.parsePages() aemImports += result.aemImports - pagesToParse += result.pages + pageXmlFiles += result.pages } XML_RESOURCES -> resources += parser.parseResources().associateBy { it.name } - XML_TIPS -> tipsToParse += parser.parseTips() + XML_TIPS -> tipXmlFiles += parser.parseTips() } XMLNS_SHAREABLE -> when (parser.name) { @@ -298,7 +304,8 @@ class Manifest : BaseModel, Styles { resources: ((Manifest) -> List)? = null, shareables: ((Manifest) -> List)? = null, tips: ((Manifest) -> List)? = null, - pages: ((Manifest) -> List)? = null + pages: ((Manifest) -> List)? = null, + pageXmlFiles: List = emptyList(), ) { this.config = config @@ -338,22 +345,34 @@ class Manifest : BaseModel, Styles { this.shareables = shareables?.invoke(this).orEmpty() this.tips = tips?.invoke(this)?.associateBy { it.id }.orEmpty() - pagesToParse = emptyList() - tipsToParse = emptyList() + this.pageXmlFiles = pageXmlFiles + tipXmlFiles = emptyList() } override val manifest get() = this - val hasTips get() = tips.isNotEmpty() || (!config.parseTips && tipsToParse.isNotEmpty()) + val hasTips get() = tips.isNotEmpty() || (!config.parseTips && tipXmlFiles.isNotEmpty()) internal fun getResource(name: String?) = name?.let { resources[name] } @JsExport.Ignore fun findCategory(category: String?) = categories.firstOrNull { it.id == category } - fun findPage(id: String?) = id?.let { pages.firstOrNull { it.id == id } } @JsExport.Ignore fun findShareable(id: String?) = id?.let { shareables.firstOrNull { it.id == id } } @JsExport.Ignore fun findTip(id: String?) = tips[id] + override fun supportsPageType(type: KClass) = when (this.type) { + Type.ARTICLE -> false + Type.CYOA -> when (type) { + CardCollectionPage::class, + ContentPage::class -> true + PageCollectionPage::class -> config.supportsFeature(FEATURE_PAGE_COLLECTION) + else -> false + } + Type.LESSON -> type == LessonPage::class + Type.TRACT -> type == TractPage::class + Type.UNKNOWN -> false + } + private fun XmlPullParser.parseCategories() = buildList { require(XmlPullParser.START_TAG, XMLNS_MANIFEST, XML_CATEGORIES) parseChildren { @@ -367,7 +386,7 @@ class Manifest : BaseModel, Styles { private class PagesData { val aemImports = mutableListOf() - val pages = mutableListOf>() + val pages = mutableListOf() } private fun XmlPullParser.parsePages() = PagesData().also { result -> @@ -380,7 +399,7 @@ class Manifest : BaseModel, Styles { XML_PAGES_PAGE -> { val src = getAttributeValue(XML_PAGES_PAGE_SRC) ?: return@parseChildren val fileName = getAttributeValue(XML_PAGES_PAGE_FILENAME) - result.pages += fileName to src + result.pages += XmlFile(fileName, src) } } @@ -411,7 +430,7 @@ class Manifest : BaseModel, Styles { XML_TIPS_TIP -> { val id = getAttributeValue(XML_TIPS_TIP_ID) ?: return@parseChildren val src = getAttributeValue(XML_TIPS_TIP_SRC) ?: return@parseChildren - add(id to src) + add(XmlFile(id, src)) } } } @@ -422,10 +441,6 @@ class Manifest : BaseModel, Styles { @HiddenFromObjC @JsName("dismissListeners") val jsDismissListeners get() = dismissListeners.toTypedArray() - - @HiddenFromObjC - @JsName("pages") - val jsPages get() = pages.toTypedArray() // endregion Kotlin/JS interop enum class Type { @@ -448,6 +463,8 @@ class Manifest : BaseModel, Styles { } } } + + data class XmlFile(internal val name: String?, internal val src: String) } @get:AndroidColorInt diff --git a/module/parser/src/commonMain/kotlin/org/cru/godtools/shared/tool/parser/model/lesson/LessonPage.kt b/module/parser/src/commonMain/kotlin/org/cru/godtools/shared/tool/parser/model/lesson/LessonPage.kt index 5dc77675c..9431b0844 100644 --- a/module/parser/src/commonMain/kotlin/org/cru/godtools/shared/tool/parser/model/lesson/LessonPage.kt +++ b/module/parser/src/commonMain/kotlin/org/cru/godtools/shared/tool/parser/model/lesson/LessonPage.kt @@ -7,6 +7,7 @@ import org.cru.godtools.shared.tool.parser.model.AnalyticsEvent import org.cru.godtools.shared.tool.parser.model.AnalyticsEvent.Companion.parseAnalyticsEvents import org.cru.godtools.shared.tool.parser.model.Content import org.cru.godtools.shared.tool.parser.model.Gravity +import org.cru.godtools.shared.tool.parser.model.HasPages import org.cru.godtools.shared.tool.parser.model.ImageScaleType import org.cru.godtools.shared.tool.parser.model.Manifest import org.cru.godtools.shared.tool.parser.model.Parent @@ -27,10 +28,10 @@ class LessonPage : Page, Parent { override val content: List internal constructor( - manifest: Manifest, + container: HasPages, fileName: String?, parser: XmlPullParser - ) : super(manifest, fileName, parser) { + ) : super(container, fileName, parser) { parser.require(XmlPullParser.START_TAG, XMLNS_LESSON, XML_PAGE) analyticsEvents = mutableListOf() @@ -71,6 +72,4 @@ class LessonPage : Page, Parent { content = emptyList() } - - override fun supports(type: Manifest.Type) = type == Manifest.Type.LESSON } diff --git a/module/parser/src/commonMain/kotlin/org/cru/godtools/shared/tool/parser/model/page/CardCollectionPage.kt b/module/parser/src/commonMain/kotlin/org/cru/godtools/shared/tool/parser/model/page/CardCollectionPage.kt index 293ead54a..14dc5bc64 100644 --- a/module/parser/src/commonMain/kotlin/org/cru/godtools/shared/tool/parser/model/page/CardCollectionPage.kt +++ b/module/parser/src/commonMain/kotlin/org/cru/godtools/shared/tool/parser/model/page/CardCollectionPage.kt @@ -9,6 +9,7 @@ import org.cru.godtools.shared.tool.parser.model.AnalyticsEvent.Trigger import org.cru.godtools.shared.tool.parser.model.BaseModel import org.cru.godtools.shared.tool.parser.model.Content import org.cru.godtools.shared.tool.parser.model.HasAnalyticsEvents +import org.cru.godtools.shared.tool.parser.model.HasPages import org.cru.godtools.shared.tool.parser.model.Manifest import org.cru.godtools.shared.tool.parser.model.Parent import org.cru.godtools.shared.tool.parser.model.PlatformColor @@ -32,10 +33,10 @@ class CardCollectionPage : Page { val cards: List internal constructor( - manifest: Manifest, + container: HasPages, fileName: String?, parser: XmlPullParser - ) : super(manifest, fileName, parser) { + ) : super(container, fileName, parser) { parser.require(XmlPullParser.START_TAG, XMLNS_PAGE, XML_PAGE) parser.requirePageType(TYPE_CARD_COLLECTION) @@ -72,8 +73,6 @@ class CardCollectionPage : Page { cards = emptyList() } - override fun supports(type: Manifest.Type) = type == Manifest.Type.CYOA - class Card : BaseModel, Parent, HasAnalyticsEvents { internal companion object { internal const val XML_CARD = "card" diff --git a/module/parser/src/commonMain/kotlin/org/cru/godtools/shared/tool/parser/model/page/ContentPage.kt b/module/parser/src/commonMain/kotlin/org/cru/godtools/shared/tool/parser/model/page/ContentPage.kt index 7b77eb7d4..6a1898b6d 100644 --- a/module/parser/src/commonMain/kotlin/org/cru/godtools/shared/tool/parser/model/page/ContentPage.kt +++ b/module/parser/src/commonMain/kotlin/org/cru/godtools/shared/tool/parser/model/page/ContentPage.kt @@ -5,7 +5,7 @@ import org.ccci.gto.support.androidx.annotation.RestrictToScope import org.cru.godtools.shared.tool.parser.model.AnalyticsEvent import org.cru.godtools.shared.tool.parser.model.AnalyticsEvent.Companion.parseAnalyticsEvents import org.cru.godtools.shared.tool.parser.model.Content -import org.cru.godtools.shared.tool.parser.model.Manifest +import org.cru.godtools.shared.tool.parser.model.HasPages import org.cru.godtools.shared.tool.parser.model.Parent import org.cru.godtools.shared.tool.parser.model.XMLNS_ANALYTICS import org.cru.godtools.shared.tool.parser.model.parseContent @@ -23,10 +23,10 @@ class ContentPage : Page, Parent { override val content: List internal constructor( - manifest: Manifest, + container: HasPages, fileName: String?, parser: XmlPullParser - ) : super(manifest, fileName, parser) { + ) : super(container, fileName, parser) { parser.require(XmlPullParser.START_TAG, XMLNS_PAGE, XML_PAGE) parser.requirePageType(TYPE_CONTENT) @@ -47,13 +47,11 @@ class ContentPage : Page, Parent { @RestrictTo(RestrictToScope.TESTS) internal constructor( - manifest: Manifest, + container: HasPages, id: String? = null, parentPage: String? = null - ) : super(manifest, id = id, parentPage = parentPage) { + ) : super(container, id = id, parentPage = parentPage) { analyticsEvents = emptyList() content = emptyList() } - - override fun supports(type: Manifest.Type) = type == Manifest.Type.CYOA } diff --git a/module/parser/src/commonMain/kotlin/org/cru/godtools/shared/tool/parser/model/page/Page.kt b/module/parser/src/commonMain/kotlin/org/cru/godtools/shared/tool/parser/model/page/Page.kt index 0642d42fd..dfe69ce10 100644 --- a/module/parser/src/commonMain/kotlin/org/cru/godtools/shared/tool/parser/model/page/Page.kt +++ b/module/parser/src/commonMain/kotlin/org/cru/godtools/shared/tool/parser/model/page/Page.kt @@ -9,6 +9,7 @@ import kotlin.native.HiddenFromObjC import org.ccci.gto.support.androidx.annotation.RestrictTo import org.ccci.gto.support.androidx.annotation.RestrictToScope import org.ccci.gto.support.androidx.annotation.VisibleForTesting +import org.cru.godtools.shared.tool.parser.ParserConfig.Companion.FEATURE_PAGE_COLLECTION import org.cru.godtools.shared.tool.parser.internal.AndroidColorInt import org.cru.godtools.shared.tool.parser.model.AnalyticsEvent import org.cru.godtools.shared.tool.parser.model.AnalyticsEvent.Trigger @@ -18,6 +19,7 @@ import org.cru.godtools.shared.tool.parser.model.EventId import org.cru.godtools.shared.tool.parser.model.Gravity import org.cru.godtools.shared.tool.parser.model.Gravity.Companion.toGravityOrNull import org.cru.godtools.shared.tool.parser.model.HasAnalyticsEvents +import org.cru.godtools.shared.tool.parser.model.HasPages import org.cru.godtools.shared.tool.parser.model.ImageScaleType import org.cru.godtools.shared.tool.parser.model.ImageScaleType.Companion.toImageScaleTypeOrNull import org.cru.godtools.shared.tool.parser.model.Manifest @@ -47,6 +49,7 @@ import org.cru.godtools.shared.tool.parser.model.page.ContentPage.Companion.TYPE import org.cru.godtools.shared.tool.parser.model.page.Page.Companion.DEFAULT_BACKGROUND_COLOR import org.cru.godtools.shared.tool.parser.model.page.Page.Companion.DEFAULT_BACKGROUND_IMAGE_GRAVITY import org.cru.godtools.shared.tool.parser.model.page.Page.Companion.DEFAULT_BACKGROUND_IMAGE_SCALE_TYPE +import org.cru.godtools.shared.tool.parser.model.page.PageCollectionPage.Companion.TYPE_PAGE_COLLECTION import org.cru.godtools.shared.tool.parser.model.primaryColor import org.cru.godtools.shared.tool.parser.model.primaryTextColor import org.cru.godtools.shared.tool.parser.model.stylesParent @@ -70,6 +73,8 @@ abstract class Page : BaseModel, Styles, HasAnalyticsEvents { internal companion object { internal const val XML_PAGE = "page" + private const val XML_PARENT_PAGE_COLLECTION_OVERRIDE = "parent_override_page-collection" + @AndroidColorInt @VisibleForTesting internal val DEFAULT_BACKGROUND_COLOR = color(0, 0, 0, 0.0) @@ -78,16 +83,33 @@ abstract class Page : BaseModel, Styles, HasAnalyticsEvents { @VisibleForTesting internal val DEFAULT_BACKGROUND_IMAGE_SCALE_TYPE = ImageScaleType.FILL_X - fun parse(manifest: Manifest, fileName: String?, parser: XmlPullParser): Page? { + internal suspend fun parse( + container: HasPages, + fileName: String?, + parser: XmlPullParser, + parseFile: suspend (String) -> XmlPullParser, + ): Page? { + parser.require(XmlPullParser.START_TAG, null, XML_PAGE) + + return when (parser.namespace to parser.getAttributeValue(XMLNS_XSI, XML_TYPE)) { + XMLNS_PAGE to TYPE_PAGE_COLLECTION -> { + if (!container.supportsPageType(PageCollectionPage::class)) return null + PageCollectionPage.parse(container, fileName, parser, parseFile) + } + else -> parse(container, fileName, parser) + }?.takeIf { container.supportsPageType(it::class) } + } + + internal fun parse(container: HasPages, fileName: String?, parser: XmlPullParser): Page? { parser.require(XmlPullParser.START_TAG, null, XML_PAGE) @Suppress("ktlint:standard:blank-line-between-when-conditions") return when (parser.namespace) { - XMLNS_LESSON -> LessonPage(manifest, fileName, parser) - XMLNS_TRACT -> TractPage(manifest, fileName, parser) + XMLNS_LESSON -> LessonPage(container, fileName, parser) + XMLNS_TRACT -> TractPage(container, fileName, parser) XMLNS_PAGE -> when (val type = parser.getAttributeValue(XMLNS_XSI, XML_TYPE)) { - TYPE_CARD_COLLECTION -> CardCollectionPage(manifest, fileName, parser) - TYPE_CONTENT -> ContentPage(manifest, fileName, parser) + TYPE_CARD_COLLECTION -> CardCollectionPage(container, fileName, parser) + TYPE_CONTENT -> ContentPage(container, fileName, parser) else -> { val message = "Unrecognized page type: <${parser.namespace}:${parser.name} type=$type>" Logger.e(message, UnsupportedOperationException(message), "Page") @@ -99,7 +121,7 @@ abstract class Page : BaseModel, Styles, HasAnalyticsEvents { Logger.e(message, UnsupportedOperationException(message), "Page") null } - }?.takeIf { it.supports(manifest.type) } + }?.takeIf { container.supportsPageType(it::class) } } internal fun XmlPullParser.requirePageType(type: String) { @@ -108,17 +130,32 @@ abstract class Page : BaseModel, Styles, HasAnalyticsEvents { } } + private val parentPageContainer: HasPages + get() { + var parent = parent + while (parent !is HasPages) parent = parent?.parent ?: return manifest + return parent + } + val id by lazy { _id ?: fileName ?: "${manifest.code}-$position" } - val position by lazy { manifest.pages.indexOf(this) } + val position by lazy { parentPageContainer.pages.indexOf(this) } private val _id: String? @VisibleForTesting internal val fileName: String? private val _parentPage: String? - val parentPage get() = manifest.findPage(_parentPage) - val nextPage get() = manifest.pages.getOrNull(position + 1) - val previousPage get() = manifest.pages.getOrNull(position - 1) + val parentPage get() = parentPageContainer.findPage(_parentPage?.substringBefore("?")) + val parentPageParams get() = when { + parentPage == null -> emptyMap() + else -> _parentPage.orEmpty().substringAfter("?", "") + .split("&") + .mapNotNull { it.split("=", limit = 2).takeIf { it.size == 2 } } + .associate { (key, value) -> key to value } + } + + val nextPage get() = parentPageContainer.pages.getOrNull(position + 1) + val previousPage get() = parentPageContainer.pages.getOrNull(position - 1) val isHidden: Boolean @@ -149,12 +186,13 @@ abstract class Page : BaseModel, Styles, HasAnalyticsEvents { @Suppress("ktlint:standard:property-naming") // https://github.com/pinterest/ktlint/issues/2448 private val _controlColor: PlatformColor? @get:AndroidColorInt - internal val controlColor get() = _controlColor ?: manifest.pageControlColor + internal val controlColor: PlatformColor + get() = _controlColor ?: (parentPageContainer as? Page)?.controlColor ?: manifest.pageControlColor @AndroidColorInt private val _cardBackgroundColor: PlatformColor? @get:AndroidColorInt - override val cardBackgroundColor get() = _cardBackgroundColor ?: manifest.cardBackgroundColor + override val cardBackgroundColor get() = _cardBackgroundColor ?: super.cardBackgroundColor private val _multiselectOptionBackgroundColor: PlatformColor? override val multiselectOptionBackgroundColor @@ -170,12 +208,15 @@ abstract class Page : BaseModel, Styles, HasAnalyticsEvents { private val _textScale: Double override val textScale get() = _textScale * stylesParent.textScale - internal constructor(manifest: Manifest, fileName: String?, parser: XmlPullParser) : super(manifest) { + internal constructor(container: HasPages, fileName: String?, parser: XmlPullParser) : super(container) { parser.require(XmlPullParser.START_TAG, null, XML_PAGE) _id = parser.getAttributeValue(XML_ID) this.fileName = fileName - _parentPage = parser.getAttributeValue(XMLNS_CYOA, XML_PARENT) + _parentPage = + parser.getAttributeValue(XMLNS_CYOA, XML_PARENT_PAGE_COLLECTION_OVERRIDE) + ?.takeIf { manifest.config.supportsFeature(FEATURE_PAGE_COLLECTION) } + ?: parser.getAttributeValue(XMLNS_CYOA, XML_PARENT) isHidden = parser.getAttributeValue(XML_HIDDEN)?.toBoolean() ?: false @@ -208,7 +249,7 @@ abstract class Page : BaseModel, Styles, HasAnalyticsEvents { @RestrictTo(RestrictToScope.SUBCLASSES, RestrictToScope.TESTS) internal constructor( - manifest: Manifest = Manifest(), + container: HasPages = Manifest(), id: String? = null, fileName: String? = null, parentPage: String? = null, @@ -223,7 +264,7 @@ abstract class Page : BaseModel, Styles, HasAnalyticsEvents { multiselectOptionSelectedColor: PlatformColor? = null, textColor: PlatformColor? = null, textScale: Double = DEFAULT_TEXT_SCALE - ) : super(manifest) { + ) : super(container) { _id = id this.fileName = fileName _parentPage = parentPage @@ -252,8 +293,6 @@ abstract class Page : BaseModel, Styles, HasAnalyticsEvents { _textScale = textScale } - internal abstract fun supports(type: Manifest.Type): Boolean - // region HasAnalyticsEvents @VisibleForTesting internal abstract val analyticsEvents: List diff --git a/module/parser/src/commonMain/kotlin/org/cru/godtools/shared/tool/parser/model/page/PageCollectionPage.kt b/module/parser/src/commonMain/kotlin/org/cru/godtools/shared/tool/parser/model/page/PageCollectionPage.kt new file mode 100644 index 000000000..0bb5d747d --- /dev/null +++ b/module/parser/src/commonMain/kotlin/org/cru/godtools/shared/tool/parser/model/page/PageCollectionPage.kt @@ -0,0 +1,101 @@ +package org.cru.godtools.shared.tool.parser.model.page + +import kotlin.reflect.KClass +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.coroutineScope +import org.cru.godtools.shared.tool.parser.model.AnalyticsEvent +import org.cru.godtools.shared.tool.parser.model.AnalyticsEvent.Companion.parseAnalyticsEvents +import org.cru.godtools.shared.tool.parser.model.HasPages +import org.cru.godtools.shared.tool.parser.model.XMLNS_ANALYTICS +import org.cru.godtools.shared.tool.parser.util.setOnce +import org.cru.godtools.shared.tool.parser.xml.XmlPullParser +import org.cru.godtools.shared.tool.parser.xml.parseChildren + +class PageCollectionPage : Page, HasPages { + companion object { + internal const val TYPE_PAGE_COLLECTION = "page-collection" + + private const val XML_PAGES = "pages" + private const val XML_IMPORT = "import" + private const val XML_IMPORT_FILENAME = "filename" + + const val PARENT_PARAM_ACTIVE_PAGE = "active-page" + + internal suspend fun parse( + container: HasPages, + fileName: String?, + parser: XmlPullParser, + parseFile: suspend (String) -> XmlPullParser + ) = PageCollectionPage(container, fileName, parser).apply { + buildPagesFromParsedPages(parseFile) + } + } + + override val analyticsEvents: List + private val parsedPages: List + override var pages: List by setOnce() + private set + + private constructor( + container: HasPages, + fileName: String?, + parser: XmlPullParser + ) : super(container, fileName, parser) { + parser.require(XmlPullParser.START_TAG, XMLNS_PAGE, XML_PAGE) + parser.requirePageType(TYPE_PAGE_COLLECTION) + + analyticsEvents = mutableListOf() + parsedPages = mutableListOf() + parser.parseChildren { + when (parser.namespace) { + XMLNS_ANALYTICS -> when (parser.name) { + AnalyticsEvent.XML_EVENTS -> analyticsEvents += parser.parseAnalyticsEvents(this) + } + + XMLNS_PAGE -> when (parser.name) { + XML_PAGES -> parsedPages += parser.parsePages() + } + } + } + } + + private fun XmlPullParser.parsePages() = buildList { + require(XmlPullParser.START_TAG, XMLNS_PAGE, XML_PAGES) + + // process any child elements + parseChildren { + when (namespace) { + XMLNS_PAGE -> when (name) { + XML_PAGE -> { + parse(this@PageCollectionPage, null, this@parsePages) + ?.let { add(PageOrImport(it)) } + } + XML_IMPORT -> add(PageOrImport(ref = getAttributeValue(XML_IMPORT_FILENAME))) + } + } + } + } + + private suspend fun buildPagesFromParsedPages(parseFile: suspend (String) -> XmlPullParser) { + val pageIndex by lazy { manifest.pageXmlFiles.associate { it.name to it.src } } + + pages = coroutineScope { + parsedPages + .map { + it.page?.let { CompletableDeferred(it) } + ?: async { + val pageSrc = it.ref?.let { pageIndex[it] } ?: return@async null + Page.parse(this@PageCollectionPage, it.ref, parseFile(pageSrc), parseFile) + } + } + .awaitAll() + .filterNotNull() + } + } + + override fun supportsPageType(type: KClass) = type == ContentPage::class + + private class PageOrImport(val page: Page? = null, val ref: String? = null) +} diff --git a/module/parser/src/commonMain/kotlin/org/cru/godtools/shared/tool/parser/model/tract/TractPage.kt b/module/parser/src/commonMain/kotlin/org/cru/godtools/shared/tool/parser/model/tract/TractPage.kt index e2caf6c51..b32c79c7f 100644 --- a/module/parser/src/commonMain/kotlin/org/cru/godtools/shared/tool/parser/model/tract/TractPage.kt +++ b/module/parser/src/commonMain/kotlin/org/cru/godtools/shared/tool/parser/model/tract/TractPage.kt @@ -19,6 +19,7 @@ import org.cru.godtools.shared.tool.parser.model.EventId import org.cru.godtools.shared.tool.parser.model.Gravity import org.cru.godtools.shared.tool.parser.model.Gravity.Companion.toGravityOrNull import org.cru.godtools.shared.tool.parser.model.HasAnalyticsEvents +import org.cru.godtools.shared.tool.parser.model.HasPages import org.cru.godtools.shared.tool.parser.model.ImageScaleType import org.cru.godtools.shared.tool.parser.model.ImageScaleType.Companion.toImageScaleTypeOrNull import org.cru.godtools.shared.tool.parser.model.Manifest @@ -66,10 +67,10 @@ class TractPage : Page { val callToAction: CallToAction internal constructor( - manifest: Manifest, + container: HasPages, fileName: String?, parser: XmlPullParser - ) : super(manifest, fileName, parser) { + ) : super(container, fileName, parser) { parser.require(XmlPullParser.START_TAG, XMLNS_TRACT, XML_PAGE) _cardTextColor = parser.getAttributeValue(XML_CARD_TEXT_COLOR)?.toColorOrNull() @@ -133,8 +134,6 @@ class TractPage : Page { this.callToAction = callToAction?.invoke(this) ?: CallToAction(this) } - override fun supports(type: Manifest.Type) = type == Manifest.Type.TRACT - fun findModal(id: String?) = modals.firstOrNull { it.id.equals(id, ignoreCase = true) } // region Cards diff --git a/module/parser/src/commonTest/kotlin/org/cru/godtools/shared/tool/analytics/ToolAnalyticsScreenNamesTest.kt b/module/parser/src/commonTest/kotlin/org/cru/godtools/shared/tool/analytics/ToolAnalyticsScreenNamesTest.kt index d97a2f53c..253912d9f 100644 --- a/module/parser/src/commonTest/kotlin/org/cru/godtools/shared/tool/analytics/ToolAnalyticsScreenNamesTest.kt +++ b/module/parser/src/commonTest/kotlin/org/cru/godtools/shared/tool/analytics/ToolAnalyticsScreenNamesTest.kt @@ -14,7 +14,7 @@ class ToolAnalyticsScreenNamesTest { @Test fun testForCyoaPage() { val page = ContentPage( - manifest = Manifest(code = "cyoa"), + container = Manifest(code = "cyoa"), id = "page" ) diff --git a/module/parser/src/commonTest/kotlin/org/cru/godtools/shared/tool/parser/internal/UsesResources.kt b/module/parser/src/commonTest/kotlin/org/cru/godtools/shared/tool/parser/internal/UsesResources.kt index e66b24235..f098181d2 100644 --- a/module/parser/src/commonTest/kotlin/org/cru/godtools/shared/tool/parser/internal/UsesResources.kt +++ b/module/parser/src/commonTest/kotlin/org/cru/godtools/shared/tool/parser/internal/UsesResources.kt @@ -1,8 +1,11 @@ package org.cru.godtools.shared.tool.parser.internal +import org.cru.godtools.shared.tool.parser.xml.XmlPullParser import org.cru.godtools.shared.tool.parser.xml.XmlPullParserFactory abstract class UsesResources(internal val resourcesDir: String? = "model") { + internal val parseFile: suspend (String) -> XmlPullParser = { getTestXmlParser(it) } + internal suspend fun getTestXmlParser(name: String) = TEST_XML_PULL_PARSER_FACTORY.getXmlParser(name)!!.apply { nextTag() } } diff --git a/module/parser/src/commonTest/kotlin/org/cru/godtools/shared/tool/parser/model/ManifestTest.kt b/module/parser/src/commonTest/kotlin/org/cru/godtools/shared/tool/parser/model/ManifestTest.kt index 3c803586a..cdd048ac8 100644 --- a/module/parser/src/commonTest/kotlin/org/cru/godtools/shared/tool/parser/model/ManifestTest.kt +++ b/module/parser/src/commonTest/kotlin/org/cru/godtools/shared/tool/parser/model/ManifestTest.kt @@ -13,10 +13,15 @@ import org.ccci.gto.support.androidx.test.junit.runners.AndroidJUnit4 import org.ccci.gto.support.androidx.test.junit.runners.RunOnAndroidWith import org.ccci.gto.support.fluidsonic.locale.toCommon import org.cru.godtools.shared.tool.parser.ParserConfig +import org.cru.godtools.shared.tool.parser.ParserConfig.Companion.FEATURE_PAGE_COLLECTION import org.cru.godtools.shared.tool.parser.internal.UsesResources import org.cru.godtools.shared.tool.parser.model.Styles.Companion.DEFAULT_TEXT_SCALE import org.cru.godtools.shared.tool.parser.model.lesson.DEFAULT_LESSON_NAV_BAR_COLOR +import org.cru.godtools.shared.tool.parser.model.lesson.LessonPage +import org.cru.godtools.shared.tool.parser.model.page.CardCollectionPage +import org.cru.godtools.shared.tool.parser.model.page.ContentPage import org.cru.godtools.shared.tool.parser.model.page.DEFAULT_CONTROL_COLOR +import org.cru.godtools.shared.tool.parser.model.page.PageCollectionPage import org.cru.godtools.shared.tool.parser.model.shareable.ShareableImage import org.cru.godtools.shared.tool.parser.model.tract.TractPage @@ -201,6 +206,7 @@ class ManifestTest : UsesResources() { Manifest.parse(name, config) { getTestXmlParser(it) } // endregion parse Manifest + // region HasPages @Test fun testManifestFindPage() { val manifest = Manifest(code = "tool", pages = { manifest -> List(10) { TractPage(manifest) } }) @@ -210,6 +216,51 @@ class ManifestTest : UsesResources() { } } + // region HasPages.supportsPageType() + @Test + fun testManifestSupportsPageType_Article() { + val manifest = Manifest(type = Manifest.Type.ARTICLE) + assertFalse(manifest.supportsPageType(ContentPage::class)) + assertFalse(manifest.supportsPageType(LessonPage::class)) + assertFalse(manifest.supportsPageType(TractPage::class)) + } + + @Test + fun testManifestSupportsPageType_Cyoa() { + val manifest = Manifest( + config = ParserConfig().withSupportedFeatures(FEATURE_PAGE_COLLECTION), + type = Manifest.Type.CYOA + ) + assertTrue(manifest.supportsPageType(ContentPage::class)) + assertTrue(manifest.supportsPageType(CardCollectionPage::class)) + assertTrue(manifest.supportsPageType(PageCollectionPage::class)) + assertFalse(manifest.supportsPageType(LessonPage::class)) + assertFalse(manifest.supportsPageType(TractPage::class)) + + assertFalse( + Manifest(type = Manifest.Type.CYOA).supportsPageType(PageCollectionPage::class), + "PageCollectionPages are only supported when the feature is enabled", + ) + } + + @Test + fun testManifestSupportsPageType_Lesson() { + val manifest = Manifest(type = Manifest.Type.LESSON) + assertTrue(manifest.supportsPageType(LessonPage::class)) + assertFalse(manifest.supportsPageType(ContentPage::class)) + assertFalse(manifest.supportsPageType(TractPage::class)) + } + + @Test + fun testManifestSupportsPageType_Tract() { + val manifest = Manifest(type = Manifest.Type.TRACT) + assertTrue(manifest.supportsPageType(TractPage::class)) + assertFalse(manifest.supportsPageType(ContentPage::class)) + assertFalse(manifest.supportsPageType(LessonPage::class)) + } + // endregion Manifest.supportsPageType() + // endregion HasPages + @Test fun testManifestFindShareable() { val manifest = Manifest(code = "tool", shareables = { List(5) { ShareableImage(id = "shareable$it") } }) diff --git a/module/parser/src/commonTest/kotlin/org/cru/godtools/shared/tool/parser/model/page/PageCollectionPageTest.kt b/module/parser/src/commonTest/kotlin/org/cru/godtools/shared/tool/parser/model/page/PageCollectionPageTest.kt new file mode 100644 index 000000000..98225d752 --- /dev/null +++ b/module/parser/src/commonTest/kotlin/org/cru/godtools/shared/tool/parser/model/page/PageCollectionPageTest.kt @@ -0,0 +1,62 @@ +package org.cru.godtools.shared.tool.parser.model.page + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertIs +import kotlin.test.assertNotNull +import kotlinx.coroutines.test.runTest +import org.ccci.gto.support.androidx.test.junit.runners.AndroidJUnit4 +import org.ccci.gto.support.androidx.test.junit.runners.RunOnAndroidWith +import org.cru.godtools.shared.tool.parser.ParserConfig +import org.cru.godtools.shared.tool.parser.ParserConfig.Companion.FEATURE_PAGE_COLLECTION +import org.cru.godtools.shared.tool.parser.internal.UsesResources +import org.cru.godtools.shared.tool.parser.model.Manifest +import org.cru.godtools.shared.tool.parser.xml.XmlPullParserException + +@RunOnAndroidWith(AndroidJUnit4::class) +class PageCollectionPageTest : UsesResources("model/page") { + private val manifest = Manifest( + config = ParserConfig().withSupportedFeatures(FEATURE_PAGE_COLLECTION), + type = Manifest.Type.CYOA, + pageXmlFiles = listOf( + Manifest.XmlFile("ref_page_valid", "page_content.xml"), + Manifest.XmlFile("ref_page_invalid", "page_page-collection_imports.xml"), + ), + ) + + // region Parse XML + @Test + fun testParsePageCollectionPage() = runTest { + assertNotNull( + PageCollectionPage.parse(manifest, null, getTestXmlParser("page_page-collection.xml"), parseFile) + ) { + assertEquals(1, it.analyticsEvents.size) + assertEquals(1, it.pages.size) + assertNotNull(it.pages[0]) { page -> + assertIs(page) + assertEquals("content_page", page.id) + } + } + } + + @Test + fun testParsePageCollectionPage_PageImports() = runTest { + assertNotNull( + PageCollectionPage.parse(manifest, null, getTestXmlParser("page_page-collection_imports.xml"), parseFile) + ) { + assertEquals(1, it.analyticsEvents.size) + assertEquals(2, it.pages.size) + assertEquals("embedded_content_page", it.pages[0].id) + assertEquals("content_page", it.pages[1].id) + } + } + + @Test + fun testParsePageCollectionPageInvalidType() = runTest { + assertFailsWith(XmlPullParserException::class) { + PageCollectionPage.parse(manifest, null, getTestXmlParser("page_invalid_type.xml"), parseFile) + } + } + // endregion Parse XML +} diff --git a/module/parser/src/commonTest/kotlin/org/cru/godtools/shared/tool/parser/model/page/PageTest.kt b/module/parser/src/commonTest/kotlin/org/cru/godtools/shared/tool/parser/model/page/PageTest.kt index c5a4ac497..4d5f3318a 100644 --- a/module/parser/src/commonTest/kotlin/org/cru/godtools/shared/tool/parser/model/page/PageTest.kt +++ b/module/parser/src/commonTest/kotlin/org/cru/godtools/shared/tool/parser/model/page/PageTest.kt @@ -1,15 +1,20 @@ package org.cru.godtools.shared.tool.parser.model.page +import kotlin.reflect.KClass import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertIs +import kotlin.test.assertNotNull import kotlin.test.assertNull import kotlin.test.assertSame import kotlinx.coroutines.test.runTest import org.ccci.gto.support.androidx.test.junit.runners.AndroidJUnit4 import org.ccci.gto.support.androidx.test.junit.runners.RunOnAndroidWith +import org.cru.godtools.shared.tool.parser.ParserConfig +import org.cru.godtools.shared.tool.parser.ParserConfig.Companion.FEATURE_PAGE_COLLECTION import org.cru.godtools.shared.tool.parser.internal.UsesResources import org.cru.godtools.shared.tool.parser.model.AnalyticsEvent +import org.cru.godtools.shared.tool.parser.model.HasPages import org.cru.godtools.shared.tool.parser.model.Manifest import org.cru.godtools.shared.tool.parser.model.PlatformColor import org.cru.godtools.shared.tool.parser.model.TestColors @@ -43,6 +48,35 @@ class PageTest : UsesResources("model/page") { assertNull(Page.parse(Manifest(type = Manifest.Type.TRACT), null, getTestXmlParser("../lesson/page.xml"))) } + @Test + fun testParsePageCollectionPage() = runTest { + val config = ParserConfig().withSupportedFeatures(FEATURE_PAGE_COLLECTION) + assertIs( + Page.parse( + Manifest(config = config, type = Manifest.Type.CYOA), + null, + getTestXmlParser("page_page-collection.xml"), + parseFile, + ), + ) + assertNull( + Page.parse( + Manifest(type = Manifest.Type.CYOA), + null, + getTestXmlParser("page_page-collection.xml"), + parseFile, + ), + ) + assertNull( + Page.parse( + Manifest(config = config, type = Manifest.Type.LESSON), + null, + getTestXmlParser("page_page-collection.xml"), + parseFile, + ), + ) + } + @Test fun testParseTractPage() = runTest { assertIs( @@ -64,13 +98,100 @@ class PageTest : UsesResources("model/page") { assertNull(Page.parse(Manifest(type = it), null, getTestXmlParser("page_invalid_namespace.xml"))) } } + + @Test + fun testParseParentPage() = runTest { + val manifest = Manifest( + type = Manifest.Type.CYOA, + pages = { listOf(ContentPage(it, id = "page1"), ContentPage(it, id = "page2")) } + ) + assertNotNull(Page.parse(manifest, null, getTestXmlParser("page_content_parent.xml"))) { + assertSame(manifest.findPage("page1"), it.parentPage) + assertEquals(mapOf("param" to "value"), it.parentPageParams) + } + assertNotNull(Page.parse(manifest, null, getTestXmlParser("page_content_parent_override.xml"))) { + assertSame(manifest.findPage("page1"), it.parentPage) + assertEquals(mapOf("param" to "value"), it.parentPageParams) + } + } + + @Test + fun testParseParentPage_pageCollection() = runTest { + val manifest = Manifest( + config = ParserConfig().withSupportedFeatures(FEATURE_PAGE_COLLECTION), + type = Manifest.Type.CYOA, + pages = { listOf(ContentPage(it, id = "page1"), ContentPage(it, id = "page2")) }, + ) + assertNotNull(Page.parse(manifest, null, getTestXmlParser("page_content_parent.xml"))) { + assertSame(manifest.findPage("page1"), it.parentPage) + assertEquals(mapOf("param" to "value"), it.parentPageParams) + } + assertNotNull(Page.parse(manifest, null, getTestXmlParser("page_content_parent_override.xml"))) { + assertSame(manifest.findPage("page2"), it.parentPage) + assertEquals(mapOf("param" to "value2"), it.parentPageParams) + } + } // endregion Page.parse() + // region Property: position + @Test + fun testPosition_manifest() { + val manifest = Manifest( + pages = { listOf(ContentPage(it, id = "page1"), ContentPage(it, id = "page2")) } + ) + + val page1 = manifest.findPage("page1")!! + val page2 = manifest.findPage("page2")!! + assertEquals(0, page1.position) + assertEquals(1, page2.position) + } + + @Test + fun testPosition_hasPagesParent() { + val parent = TestPage( + pages = { listOf(ContentPage(it, id = "page1"), ContentPage(it, id = "page2")) } + ) + + val page1 = parent.findPage("page1")!! + val page2 = parent.findPage("page2")!! + assertEquals(0, page1.position) + assertEquals(1, page2.position) + } + // endregion Property: position + + // region Property: cardBackgroundColor + @Test + fun testPropertyCardBackgroundColor() { + val manifest = Manifest(cardBackgroundColor = TestColors.GREEN) + val hasPagesParent = TestPage(parent = manifest, cardBackgroundColor = TestColors.BLUE) + + assertEquals(TestColors.RED, TestPage(manifest, cardBackgroundColor = TestColors.RED).cardBackgroundColor) + assertEquals(TestColors.RED, TestPage(hasPagesParent, cardBackgroundColor = TestColors.RED).cardBackgroundColor) + assertEquals(TestColors.GREEN, TestPage(manifest, cardBackgroundColor = null).cardBackgroundColor) + assertEquals(TestColors.GREEN, TestPage(TestPage(manifest, cardBackgroundColor = null)).cardBackgroundColor) + assertEquals(TestColors.BLUE, TestPage(hasPagesParent, cardBackgroundColor = null).cardBackgroundColor) + } + // endregion Property: cardBackgroundColor + + // region Property: controlColor + @Test + fun testPropertyControlColor() { + val manifest = Manifest(pageControlColor = TestColors.GREEN) + val hasPagesParent = TestPage(parent = manifest, controlColor = TestColors.BLUE) + + assertEquals(TestColors.RED, TestPage(manifest, controlColor = TestColors.RED).controlColor) + assertEquals(TestColors.RED, TestPage(hasPagesParent, controlColor = TestColors.RED).controlColor) + assertEquals(TestColors.GREEN, TestPage(manifest, controlColor = null).controlColor) + assertEquals(TestColors.GREEN, TestPage(TestPage(manifest, controlColor = null)).controlColor) + assertEquals(TestColors.BLUE, TestPage(hasPagesParent, controlColor = null).controlColor) + } + // endregion Property: controlColor + // region Property: multiselectOptionBackgroundColor @Test fun testPropertyMultiselectOptionBackgroundColor() { val page = TestPage( - manifest = Manifest(multiselectOptionBackgroundColor = TestColors.RED), + parent = Manifest(multiselectOptionBackgroundColor = TestColors.RED), multiselectOptionBackgroundColor = TestColors.GREEN, ) assertEquals(TestColors.GREEN, page.multiselectOptionBackgroundColor) @@ -79,7 +200,7 @@ class PageTest : UsesResources("model/page") { @Test fun testPropertyMultiselectOptionBackgroundColorFallback() { val page = TestPage( - manifest = Manifest(multiselectOptionBackgroundColor = TestColors.GREEN), + parent = Manifest(multiselectOptionBackgroundColor = TestColors.GREEN), multiselectOptionBackgroundColor = null, ) assertEquals(TestColors.GREEN, page.multiselectOptionBackgroundColor) @@ -90,7 +211,7 @@ class PageTest : UsesResources("model/page") { @Test fun testPropertyMultiselectOptionSelectedColor() { val page = TestPage( - manifest = Manifest(multiselectOptionSelectedColor = TestColors.RED), + parent = Manifest(multiselectOptionSelectedColor = TestColors.RED), multiselectOptionSelectedColor = TestColors.GREEN, ) assertEquals(TestColors.GREEN, page.multiselectOptionSelectedColor) @@ -99,7 +220,7 @@ class PageTest : UsesResources("model/page") { @Test fun testPropertyMultiselectOptionSelectedColorFallback() { val page = TestPage( - manifest = Manifest(multiselectOptionSelectedColor = TestColors.GREEN), + parent = Manifest(multiselectOptionSelectedColor = TestColors.GREEN), multiselectOptionSelectedColor = null, ) assertEquals(TestColors.GREEN, page.multiselectOptionSelectedColor) @@ -108,7 +229,7 @@ class PageTest : UsesResources("model/page") { // region Property: parentPage @Test - fun testParentPage() { + fun testParentPage_manifest() { val manifest = Manifest( pages = { listOf(ContentPage(it, id = "page1"), ContentPage(it, id = "page2", parentPage = "page1")) } ) @@ -117,11 +238,104 @@ class PageTest : UsesResources("model/page") { val page2 = manifest.findPage("page2")!! assertSame(page1, page2.parentPage) } + + @Test + fun testParentPage_hasPagesParent() { + val parent = TestPage( + pages = { listOf(ContentPage(it, id = "page1"), ContentPage(it, id = "page2", parentPage = "page1")) } + ) + + val page1 = parent.findPage("page1")!! + val page2 = parent.findPage("page2")!! + assertSame(page1, page2.parentPage) + } + + @Test + fun testParentPage_restrictToCurrentContainer() { + val manifest = Manifest( + pages = { + listOf( + TestPage(it, id = "page1") { + listOf( + TestPage(it, id = "page1_1"), + + TestPage(it, id = "test1", parentPage = "page1_1"), + TestPage(it, id = "test2", parentPage = "page2"), + ) + }, + TestPage(it, id = "page2"), + + // parentPage tests + TestPage(it, id = "test3", parentPage = "page1"), + TestPage(it, id = "test4", parentPage = "page1_1"), + ) + }, + ) + val page1 = manifest.findPage("page1") as TestPage + val page11 = page1.findPage("page1_1")!! + + assertSame(page11, page1.findPage("test1")!!.parentPage) + assertNull(page1.findPage("test2")!!.parentPage, "page2 is not in the same page container as test2") + assertSame(page1, manifest.findPage("test3")!!.parentPage) + assertNull(manifest.findPage("test4")!!.parentPage, "page1_1 is not in the same page container as test4") + } + + @Test + fun testParentPage_hasParams() { + val manifest = Manifest( + pages = { + listOf( + TestPage(it, id = "page1"), + + // parentPage tests + TestPage(it, id = "test1", parentPage = "page1"), + TestPage(it, id = "test2", parentPage = "page1?active-page=page1_2"), + TestPage(it, id = "test3", parentPage = "page1?active-page=page2"), + TestPage(it, id = "test4", parentPage = "missing?active-page=page2"), + ) + }, + ) + val page1 = manifest.findPage("page1")!! + + assertSame(page1, manifest.findPage("test1")!!.parentPage) + assertSame(page1, manifest.findPage("test2")!!.parentPage) + assertSame(page1, manifest.findPage("test3")!!.parentPage) + assertNull(manifest.findPage("test4")!!.parentPage, "missing doesn't exist") + } // endregion Property: parentPage + // region Property: parentPageParams + @Test + fun testParentPageParams() { + val manifest = Manifest( + pages = { + listOf( + TestPage(it, id = "page1"), + + TestPage(it, id = "test1", parentPage = "page1"), + TestPage(it, id = "test2", parentPage = "page1?active-page=page1_2"), + TestPage(it, id = "test3", parentPage = "page1?a=1&c=3&b=2"), + TestPage(it, id = "test4", parentPage = null), + TestPage(it, id = "test5", parentPage = "missing?active-page=page2"), + ) + }, + ) + + assertEquals(emptyMap(), manifest.findPage("test1")!!.parentPageParams) + assertEquals(mapOf("active-page" to "page1_2"), manifest.findPage("test2")!!.parentPageParams) + assertEquals(mapOf("a" to "1", "b" to "2", "c" to "3"), manifest.findPage("test3")!!.parentPageParams) + assertEquals(emptyMap(), manifest.findPage("test4")!!.parentPageParams) + assertEquals( + emptyMap(), + manifest.findPage("test5")!!.parentPageParams, + "Don't parse parameters if the parentPage doesn't exist", + ) + } + // endregion Property: parentPageParams + // region Property: nextPage @Test - fun testNextPage() { + fun testNextPage_manifest() { val manifest = Manifest( pages = { listOf(ContentPage(it, id = "page1"), ContentPage(it, id = "page2")) } ) @@ -131,11 +345,23 @@ class PageTest : UsesResources("model/page") { assertSame(page2, page1.nextPage) assertNull(page2.nextPage) } + + @Test + fun testNextPage_hasPagesParent() { + val parent = TestPage( + pages = { listOf(ContentPage(it, id = "page1"), ContentPage(it, id = "page2")) } + ) + + val page1 = parent.findPage("page1")!! + val page2 = parent.findPage("page2")!! + assertSame(page2, page1.nextPage) + assertNull(page2.nextPage) + } // endregion Property: nextPage // region Property: previousPage @Test - fun testPreviousPage() { + fun testPreviousPage_manifest() { val manifest = Manifest( pages = { listOf(ContentPage(it, id = "page1"), ContentPage(it, id = "page2")) } ) @@ -145,18 +371,42 @@ class PageTest : UsesResources("model/page") { assertSame(page1, page2.previousPage) assertNull(page1.previousPage) } + + @Test + fun testPreviousPage_hasPagesParent() { + val parent = TestPage( + pages = { listOf(ContentPage(it, id = "page1"), ContentPage(it, id = "page2")) } + ) + + val page1 = parent.findPage("page1")!! + val page2 = parent.findPage("page2")!! + assertSame(page1, page2.previousPage) + assertNull(page1.previousPage) + } // endregion Property: previousPage private class TestPage( - manifest: Manifest = Manifest(), + parent: HasPages = Manifest(), + id: String? = null, + parentPage: String? = null, + cardBackgroundColor: PlatformColor? = null, + controlColor: PlatformColor? = null, multiselectOptionBackgroundColor: PlatformColor? = null, multiselectOptionSelectedColor: PlatformColor? = null, override val analyticsEvents: List = emptyList(), - ) : Page( - manifest = manifest, - multiselectOptionBackgroundColor = multiselectOptionBackgroundColor, - multiselectOptionSelectedColor = multiselectOptionSelectedColor, - ) { - override fun supports(type: Manifest.Type) = true + pages: ((HasPages) -> List) = { listOf() }, + ) : + Page( + container = parent, + id = id, + parentPage = parentPage, + cardBackgroundColor = cardBackgroundColor, + controlColor = controlColor, + multiselectOptionBackgroundColor = multiselectOptionBackgroundColor, + multiselectOptionSelectedColor = multiselectOptionSelectedColor, + ), + HasPages { + override val pages: List = pages(this) + override fun supportsPageType(type: KClass) = true } } diff --git a/module/parser/src/commonTest/kotlin/org/cru/godtools/shared/tool/parser/model/tract/TractPageTest.kt b/module/parser/src/commonTest/kotlin/org/cru/godtools/shared/tool/parser/model/tract/TractPageTest.kt index df7b6dc63..e63ee019c 100644 --- a/module/parser/src/commonTest/kotlin/org/cru/godtools/shared/tool/parser/model/tract/TractPageTest.kt +++ b/module/parser/src/commonTest/kotlin/org/cru/godtools/shared/tool/parser/model/tract/TractPageTest.kt @@ -104,13 +104,6 @@ class TractPageTest : UsesResources("model/tract") { } } - @Test - fun testCardBackgroundColorFallbackBehavior() { - val manifest = Manifest(cardBackgroundColor = TestColors.BLUE) - assertEquals(TestColors.GREEN, TractPage(manifest, cardBackgroundColor = TestColors.GREEN).cardBackgroundColor) - assertEquals(manifest.cardBackgroundColor, TractPage(manifest).cardBackgroundColor) - } - @Test fun testCardTextColorBehavior() { with(TractPage(textColor = TestColors.GREEN)) { diff --git a/module/parser/src/commonTest/resources/org/cru/godtools/shared/tool/parser/model/page/page_content.xml b/module/parser/src/commonTest/resources/org/cru/godtools/shared/tool/parser/model/page/page_content.xml index 6c7c86751..378ac58de 100644 --- a/module/parser/src/commonTest/resources/org/cru/godtools/shared/tool/parser/model/page/page_content.xml +++ b/module/parser/src/commonTest/resources/org/cru/godtools/shared/tool/parser/model/page/page_content.xml @@ -1,7 +1,7 @@ + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="content" id="content_page"> diff --git a/module/parser/src/commonTest/resources/org/cru/godtools/shared/tool/parser/model/page/page_content_parent.xml b/module/parser/src/commonTest/resources/org/cru/godtools/shared/tool/parser/model/page/page_content_parent.xml new file mode 100644 index 000000000..b9b74e2fc --- /dev/null +++ b/module/parser/src/commonTest/resources/org/cru/godtools/shared/tool/parser/model/page/page_content_parent.xml @@ -0,0 +1,6 @@ + + + diff --git a/module/parser/src/commonTest/resources/org/cru/godtools/shared/tool/parser/model/page/page_content_parent_override.xml b/module/parser/src/commonTest/resources/org/cru/godtools/shared/tool/parser/model/page/page_content_parent_override.xml new file mode 100644 index 000000000..2f3a03cac --- /dev/null +++ b/module/parser/src/commonTest/resources/org/cru/godtools/shared/tool/parser/model/page/page_content_parent_override.xml @@ -0,0 +1,6 @@ + + + diff --git a/module/parser/src/commonTest/resources/org/cru/godtools/shared/tool/parser/model/page/page_page-collection.xml b/module/parser/src/commonTest/resources/org/cru/godtools/shared/tool/parser/model/page/page_page-collection.xml new file mode 100644 index 000000000..5231c569a --- /dev/null +++ b/module/parser/src/commonTest/resources/org/cru/godtools/shared/tool/parser/model/page/page_page-collection.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + Text + + + + + + + + + + + + + diff --git a/module/parser/src/commonTest/resources/org/cru/godtools/shared/tool/parser/model/page/page_page-collection_imports.xml b/module/parser/src/commonTest/resources/org/cru/godtools/shared/tool/parser/model/page/page_page-collection_imports.xml new file mode 100644 index 000000000..3831b9f7f --- /dev/null +++ b/module/parser/src/commonTest/resources/org/cru/godtools/shared/tool/parser/model/page/page_page-collection_imports.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + Text + + + + + + + + +