stricter checks for json layouts (and fix an old bug in hebrew layout)

This commit is contained in:
Helium314 2025-02-09 15:17:06 +01:00
parent 2700c3428a
commit ecf7aabdb1
4 changed files with 56 additions and 8 deletions

View file

@ -2,12 +2,12 @@
[ [
{ "$": "variation_selector", { "$": "variation_selector",
"email": { "label": "-" }, "email": { "label": "-" },
"url": { "label": "-" }, "uri": { "label": "-" },
"default": { "label": "'", "popup": { "relevant": [{ "label": "׳" }, { "label": "״" }, { "label": "\"" }] } } "default": { "label": "'", "popup": { "relevant": [{ "label": "׳" }, { "label": "״" }, { "label": "\"" }] } }
}, },
{ "$": "variation_selector", { "$": "variation_selector",
"email": { "label": "_" }, "email": { "label": "_" },
"url": { "label": "_" }, "uri": { "label": "_" },
"default": { "label": "-", "popup": { "relevant": [{ "label": "־" }, { "label": "_" }] } } "default": { "label": "-", "popup": { "relevant": [{ "label": "־" }, { "label": "_" }] } }
}, },
{ "label": "ק", "popup": { { "label": "ק", "popup": {

View file

@ -541,7 +541,7 @@ public final class KeyboardSwitcher implements KeyboardState.SwitchActions {
} }
// Displays a toast-like message with the provided text for a specified duration. // Displays a toast-like message with the provided text for a specified duration.
public void showFakeToast(final String text, final int timeMillis) { private void showFakeToast(final String text, final int timeMillis) {
if (mFakeToastView.getVisibility() == View.VISIBLE) return; if (mFakeToastView.getVisibility() == View.VISIBLE) return;
final Drawable appIcon = mFakeToastView.getCompoundDrawables()[0]; final Drawable appIcon = mFakeToastView.getCompoundDrawables()[0];

View file

@ -27,6 +27,7 @@ import helium314.keyboard.latin.utils.ScriptUtils.script
import helium314.keyboard.latin.utils.getCustomFunctionalLayoutName import helium314.keyboard.latin.utils.getCustomFunctionalLayoutName
import helium314.keyboard.latin.utils.getCustomLayoutFile import helium314.keyboard.latin.utils.getCustomLayoutFile
import helium314.keyboard.latin.utils.getCustomLayoutFiles import helium314.keyboard.latin.utils.getCustomLayoutFiles
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.modules.SerializersModule import kotlinx.serialization.modules.SerializersModule
import kotlinx.serialization.modules.polymorphic import kotlinx.serialization.modules.polymorphic
@ -69,7 +70,9 @@ object RawKeyboardParser {
* codes of multi_text_key not used, only the label * codes of multi_text_key not used, only the label
* (currently) popups is always read to [number, main, relevant] layoutPopupKeys, no choice of which to use or which hint is provided * (currently) popups is always read to [number, main, relevant] layoutPopupKeys, no choice of which to use or which hint is provided
*/ */
fun parseJsonString(layoutText: String): List<List<AbstractKeyData>> = florisJsonConfig.decodeFromString(layoutText.stripCommentLines()) fun parseJsonString(layoutText: String, strict: Boolean = true): List<List<AbstractKeyData>> =
if (strict) checkJsonConfig.decodeFromString(layoutText.stripCommentLines())
else florisJsonConfig.decodeFromString(layoutText.stripCommentLines())
/** Parse simple layouts, defined only as rows of (normal) keys with popup keys. */ /** Parse simple layouts, defined only as rows of (normal) keys with popup keys. */
fun parseSimpleString(layoutText: String): List<List<KeyData>> { fun parseSimpleString(layoutText: String): List<List<KeyData>> {
@ -90,7 +93,7 @@ object RawKeyboardParser {
val layoutFileName = getLayoutFileName(layoutName, context) val layoutFileName = getLayoutFileName(layoutName, context)
val layoutText = if (layoutFileName.startsWith(CUSTOM_LAYOUT_PREFIX)) { val layoutText = if (layoutFileName.startsWith(CUSTOM_LAYOUT_PREFIX)) {
try { try {
getCustomLayoutFile(layoutFileName, context).readText() getCustomLayoutFile(layoutFileName, context).readText().trimStart()
} catch (e: Exception) { // fall back to defaults if for some reason file is broken } catch (e: Exception) { // fall back to defaults if for some reason file is broken
val name = when { val name = when {
layoutName.contains("functional") -> "functional_keys.json" layoutName.contains("functional") -> "functional_keys.json"
@ -102,15 +105,17 @@ object RawKeyboardParser {
context.assets.open("layouts${File.separator}$name").reader().use { it.readText() } context.assets.open("layouts${File.separator}$name").reader().use { it.readText() }
} }
} else context.assets.open("layouts${File.separator}$layoutFileName").reader().use { it.readText() } } else context.assets.open("layouts${File.separator}$layoutFileName").reader().use { it.readText() }
if (layoutFileName.endsWith(".json") || layoutFileName.startsWith(CUSTOM_LAYOUT_PREFIX)) { if (layoutFileName.endsWith(".json") || (layoutFileName.startsWith(CUSTOM_LAYOUT_PREFIX) && layoutText.startsWith("["))) {
try { try {
val florisKeyData = parseJsonString(layoutText) val florisKeyData = parseJsonString(layoutText, false)
return { params -> return { params ->
florisKeyData.mapTo(mutableListOf()) { row -> florisKeyData.mapTo(mutableListOf()) { row ->
row.mapNotNullTo(mutableListOf()) { it.compute(params) } row.mapNotNullTo(mutableListOf()) { it.compute(params) }
} }
} }
} catch (_: Exception) { } } catch (e: Exception) {
Log.w(TAG, "could not parse json layout for $layoutName, falling back to simple layout parsing", e)
}
} }
// not a json, or invalid json // not a json, or invalid json
val simpleKeyData = parseSimpleString(layoutText) val simpleKeyData = parseSimpleString(layoutText)
@ -178,7 +183,9 @@ object RawKeyboardParser {
* modified * modified
* SPDX-License-Identifier: Apache-2.0 * SPDX-License-Identifier: Apache-2.0
*/ */
@OptIn(ExperimentalSerializationApi::class)
private val florisJsonConfig = Json { private val florisJsonConfig = Json {
allowTrailingComma = true
classDiscriminator = "$" classDiscriminator = "$"
encodeDefaults = true encodeDefaults = true
ignoreUnknownKeys = true ignoreUnknownKeys = true
@ -205,4 +212,36 @@ object RawKeyboardParser {
} }
} }
} }
// copy of florisJsonConfig, but with ignoreUnknownKeys = false so users get warned
// this is not default because users may have old layouts that should not stop working on app upgrade
@OptIn(ExperimentalSerializationApi::class)
private val checkJsonConfig = Json {
allowTrailingComma = true
classDiscriminator = "$"
encodeDefaults = true
ignoreUnknownKeys = false
isLenient = true
serializersModule = SerializersModule {
polymorphic(AbstractKeyData::class) {
subclass(TextKeyData::class, TextKeyData.serializer())
subclass(AutoTextKeyData::class, AutoTextKeyData.serializer())
subclass(MultiTextKeyData::class, MultiTextKeyData.serializer())
subclass(CaseSelector::class, CaseSelector.serializer())
subclass(ShiftStateSelector::class, ShiftStateSelector.serializer())
subclass(VariationSelector::class, VariationSelector.serializer())
subclass(KeyboardStateSelector::class, KeyboardStateSelector.serializer())
subclass(LayoutDirectionSelector::class, LayoutDirectionSelector.serializer())
subclass(CharWidthSelector::class, CharWidthSelector.serializer())
subclass(KanaSelector::class, KanaSelector.serializer())
defaultDeserializer { TextKeyData.serializer() }
}
polymorphic(KeyData::class) {
subclass(TextKeyData::class, TextKeyData.serializer())
subclass(AutoTextKeyData::class, AutoTextKeyData.serializer())
subclass(MultiTextKeyData::class, MultiTextKeyData.serializer())
defaultDeserializer { TextKeyData.serializer() }
}
}
}
} }

View file

@ -458,6 +458,15 @@ f""", // no newline at the end
} }
} }
@Test fun parseExistingLayouts() {
latinIME.assets.list("layouts")?.forEach {
val content = latinIME.assets.open("layouts/$it").reader().readText()
if (it.endsWith(".json"))
RawKeyboardParser.parseJsonString(content)
else RawKeyboardParser.parseSimpleString(content)
}
}
private data class Expected(val code: Int, val label: String? = null, val icon: String? = null, val text: String? = null, val popups: List<Pair<String?, Int>>? = null) private data class Expected(val code: Int, val label: String? = null, val icon: String? = null, val text: String? = null, val popups: List<Pair<String?, Int>>? = null)
private fun assertIsExpected(json: String, expected: Expected) { private fun assertIsExpected(json: String, expected: Expected) {