WebAppManifestIconParser.kt

/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

package mozilla.components.concept.engine.manifest.parser

import mozilla.components.concept.engine.manifest.Size
import mozilla.components.concept.engine.manifest.WebAppManifest
import mozilla.components.support.ktx.android.org.json.asSequence
import mozilla.components.support.ktx.android.org.json.tryGet
import mozilla.components.support.ktx.android.org.json.tryGetString
import org.json.JSONArray
import org.json.JSONObject
import java.util.Locale

private val whitespace = "\\s+".toRegex()

/**
 * Parses the icons array from a web app manifest.
 */
internal fun parseIcons(json: JSONObject): List<WebAppManifest.Icon> {
    val array = json.optJSONArray("icons") ?: return emptyList()

    return array
        .asSequence { i -> getJSONObject(i) }
        .mapNotNull { obj ->
            val purpose = parsePurposes(obj).ifEmpty {
                return@mapNotNull null
            }
            WebAppManifest.Icon(
                src = obj.getString("src"),
                sizes = parseIconSizes(obj),
                type = obj.tryGetString("type"),
                purpose = purpose,
            )
        }
        .toList()
}

/**
 * Parses a string set, which is expressed as either a space-delimited string or JSONArray of strings.
 *
 * Gecko returns a JSONArray to represent the intermediate infra type for some properties.
 */
private fun parseStringSet(set: Any?): Sequence<String>? = when (set) {
    is String -> set.split(whitespace).asSequence()
    is JSONArray -> set.asSequence { i -> getString(i) }
    else -> null
}

private fun parseIconSizes(json: JSONObject): List<Size> {
    val sizes = parseStringSet(json.tryGet("sizes"))
        ?: return emptyList()

    return sizes.mapNotNull { Size.parse(it) }.toList()
}

private fun parsePurposes(json: JSONObject): Set<WebAppManifest.Icon.Purpose> {
    val purpose = parseStringSet(json.tryGet("purpose"))
        ?: return setOf(WebAppManifest.Icon.Purpose.ANY)

    return purpose
        .mapNotNull {
            when (it.lowercase(Locale.ROOT)) {
                "monochrome" -> WebAppManifest.Icon.Purpose.MONOCHROME
                "maskable" -> WebAppManifest.Icon.Purpose.MASKABLE
                "any" -> WebAppManifest.Icon.Purpose.ANY
                else -> null
            }
        }
        .toSet()
}

internal fun serializeEnumName(name: String) = name.lowercase(Locale.ROOT).replace('_', '-')

internal fun serializeIcons(icons: List<WebAppManifest.Icon>): JSONArray {
    val list = icons.map { icon ->
        JSONObject().apply {
            put("src", icon.src)
            put("sizes", icon.sizes.joinToString(" ") { it.toString() })
            putOpt("type", icon.type)
            put("purpose", icon.purpose.joinToString(" ") { serializeEnumName(it.name) })
        }
    }
    return JSONArray(list)
}