import { ensureDir , readFile , writeFile } from '@ionic/utils-fs' ; import Debug from 'debug' ; import et from 'elementtree' ; import pathlib from 'path' ; import util from 'util' ; import { Platform } from '../platform' ; import type { ResolvedColorSource , ResolvedSource , ResourceConfig , ResourceValue , UnknownResource , } from '../resources' ; import { ResourceKey , ResourceType , SourceType } from '../resources' ; import { combinationJoiner } from '../utils/array' ; import { identity } from '../utils/fn' ; const debug = Debug ( 'cordova-res:cordova:config' ) ; export function getConfigPath ( directory ) { return pathlib . resolve ( directory , 'config.xml' ) ; } export async function run ( resourcesDirectory , doc , sources , resources , errstream ) { const colors = sources . filter ( ( source ) : source is ResolvedColorSource => source . type === SourceType . COLOR ) ; if ( colors . length > 0 ) { debug ( 'Color sources found--generating colors document.' ) ; const androidPlatformElement = resolvePlatformElement ( doc . getroot ( ) , Platform . ANDROID ) ; const colorsPath = pathlib . join ( resourcesDirectory , 'values' , 'colors.xml' ) ; await runColorsConfig ( colorsPath , colors ) ; const resourceFileElement = resolveElement ( androidPlatformElement , 'resource-file' , [ `resource-file[@src='${ colorsPath }']` ] ) ; resourceFileElement . set ( 'src' , colorsPath ) ; resourceFileElement . set ( 'target' , '/app/src/main/res/values/colors.xml' ) ; } runConfig ( doc , resources , errstream ) ; } export async function resolveColorsDocument ( colorsPath ) { try { return await read ( colorsPath ) ; } catch ( e ) { if ( e . code !== 'ENOENT' ) { throw e ; } const element = et . Element ( 'resources' ) ; return new et . ElementTree ( element ) ; } } export async function runColorsConfig ( colorsPath , colors ) { await ensureDir ( pathlib . dirname ( colorsPath ) ) ; const colorsDocument = await resolveColorsDocument ( colorsPath ) ; const root = colorsDocument . getroot ( ) ; for ( const color of colors ) { let colorElement = root . find ( `color[@name='${ color . name }']` ) ; if ( ! colorElement ) { debug ( 'Creating node for %o' , color . name ) ; colorElement = et . SubElement ( root , 'color' ) ; } colorElement . set ( 'name' , color . name ) ; colorElement . text = color . color ; } await write ( colorsPath , colorsDocument ) ; } export function runConfig ( doc , resources , errstream ) { const root = doc . getroot ( ) ; const orientationPreference = getPreference ( root , 'Orientation' ) ; debug ( 'Orientation preference: %O' , orientationPreference ) ; const orientation = orientationPreference || 'default' ; if ( orientation !== 'default' ) { errstream ?. write ( util . format ( `WARN:\tOrientation preference set to '%s'. Only configuring %s resources.` , orientation , orientation ) + '\n' ) ; } const platforms = groupImages ( resources ) ; for ( const [ platform , platformResources ] of platforms ) { const rules = getPlatformConfigXmlRules ( platform ) ; const platformElement = resolvePlatformElement ( root , platform ) ; const filteredResources = platformResources . sort ( rules . sort ) . filter ( ( img ) => orientation === 'default' || typeof img . orientation === 'undefined' || img . orientation === orientation ) ; for ( const resource of filteredResources ) { runResource ( platformElement , resource ) ; } } } export function runResource ( container , resource ) { const rules = getResourceConfigXmlRules ( resource ) ; if ( ! rules || ! rules . included ( resource ) ) { return ; } const { nodeName , nodeAttributes } = rules ; const xpaths = getResourceXPaths ( rules , resource ) ; const imgElement = resolveElement ( container , nodeName , xpaths ) ; for ( const attr of nodeAttributes ) { const v = resolveAttribute ( resource , attr ) ; if ( v ) { imgElement . set ( attr , v ) ; } } } export function resolvePlatformElement ( container , platform ) { const platformElement = resolveElement ( container , 'platform' , [ `platform[@name='${ platform }']` , ] ) ; platformElement . set ( 'name' , platform ) ; return platformElement ; } /**
 * Query a container for a subelement and create it if it doesn't exist
 */ export function resolveElement ( container , nodeName , xpaths ) { for ( const xpath of xpaths ) { const imgElement = container . find ( xpath ) ; if ( imgElement ) { return imgElement ; } } debug ( 'Creating %O node (not found by xpaths: %O)' , nodeName , xpaths ) ; return et . SubElement ( container , nodeName ) ; } export function conformPath ( value ) { return value . toString ( ) . replace ( /\\/g , '/' ) ; } export function resolveAttributeValue ( attr , value ) { const type = getAttributeType ( attr ) ; return type === ResourceKeyType . PATH ? conformPath ( value ) : value . toString ( ) ; } export function resolveAttribute ( resource , attr ) { const v = resource [ attr ] ; if ( v ) { return resolveAttributeValue ( attr , v ) ; } } export function groupImages ( images ) { const platforms = new Map < Platform , ResourceConfig [ ] > ( ) ; for ( const image of images ) { let platformImages = platforms . get ( image . platform ) ; if ( ! platformImages ) { platformImages = [ ] ; } platformImages . push ( image ) ; platforms . set ( image . platform , platformImages ) ; } return platforms ; } export async function read ( path ) { const contents = await readFile ( path , 'utf8' ) ; const doc = et . parse ( contents ) ; return doc ; } export async function write ( path , doc ) { const contents = doc . write ( { indent : 4 } ) ; await writeFile ( path , contents , 'utf8' ) ; } export function getPlatforms ( container ) { const platformElements = container . findall ( 'platform' ) ; const platforms = platformElements . map ( el => el . get ( 'name' ) ) ; return platforms . filter ( ( p ) : p is string => typeof p === 'string' ) ; } export function getPreference ( container , name ) { const preferenceElement = container . find ( `preference[@name='${ name }']` ) ; if ( ! preferenceElement ) { return undefined ; } const value = preferenceElement . get ( 'value' ) ; if ( ! value ) { return undefined ; } return value ; } export const enum ResourceKeyType { PATH = 'path' } export function getAttributeType ( attr ) { if ( [ ResourceKey . FOREGROUND , ResourceKey . BACKGROUND , ResourceKey . SRC ] . includes ( attr ) ) { return ResourceKeyType . PATH ; } } export function getResourceXPaths ( rules , resource ) { const { nodeName } = rules ; const indexes = combinationJoiner ( rules . indexAttributes . map ( indexAttribute => getIndexAttributeXPathParts ( rules , indexAttribute , resource [ indexAttribute . key ] ) ) . filter ( index => index . length > 0 ) , parts => parts . join ( '' ) ) ; return indexes . map ( index => `${ nodeName }${ index }` ) ; } export function getIndexAttributeXPathParts ( rules , indexAttribute , value ) { const { nodeAttributes } = rules ; const { key , values } = indexAttribute ; if ( ! nodeAttributes . includes ( key ) ) { return [ ] ; } if ( values ) { if ( typeof value === 'undefined' ) { return [ ] ; } const result = values ( value ) ; if ( Array . isArray ( result ) ) { return result . map ( v => `[@${ key }='${ v }']` ) ; } else { return [ `[@${ key }='${ result }']` ] ; } } return [ `[@${ key }]` ] ; } export function pathValues ( inputValue ) { if ( typeof inputValue !== 'string' ) { return [ ] ; } return [ inputValue , inputValue . replace ( /\//g , '\\' ) ] ; } export interface PlatformConfigXmlRules { /**
     * Sort the resources as per config.xml requirements
     */ readonly sort : ( a : UnknownResource , b : UnknownResource ) => - 1 | 0 | 1 ; } export const RESOURCE_WEIGHTS = { [ ResourceType . ADAPTIVE_ICON ] : 1 , [ ResourceType . ICON ] : 2 , [ ResourceType . SPLASH ] : 3 , } ; export const sortResources = ( a , b ) : - 1 | 0 | 1 => { if ( a . type === b . type ) { return 0 ; } return RESOURCE_WEIGHTS [ a . type ] > RESOURCE_WEIGHTS [ b . type ] ? 1 : - 1 ; } ; export function getPlatformConfigXmlRules ( platform ) { switch ( platform ) { case Platform . ANDROID : return { sort : sortResources } ; case Platform . IOS : return { sort : sortResources } ; case Platform . WINDOWS : return { sort : sortResources } ; } } export interface ResourceConfigXmlIndex { readonly key : ResourceKey ; readonly values ? : ( inputValue : ResourceValue ) => ResourceValue | ResourceValue [ ] ; } export interface ResourceConfigXmlRules { /**
     * XML node name of this resource (e.g. 'icon', 'splash')
     */ readonly nodeName : string ; /**
     * An array of resource keys to copy into the XML node as attributes
     */ readonly nodeAttributes : readonly ResourceKey [ ] ; /**
     * An array of resource keys to use as an index when generating the XPath(s)
     */ readonly indexAttributes : ResourceConfigXmlIndex [ ] ; /**
     * Get whether a resource should be included in the XML or not
     */ readonly included : ( resource : Partial < UnknownResource > ) => boolean ; } export function getResourceConfigXmlRules ( resource ) { switch ( resource . platform ) { case Platform . ANDROID : switch ( resource . type ) { case ResourceType . ADAPTIVE_ICON : return { nodeName : 'icon' , nodeAttributes : [ ResourceKey . FOREGROUND , ResourceKey . DENSITY , ResourceKey . BACKGROUND , ] , indexAttributes : [ { key : ResourceKey . FOREGROUND } , { key : ResourceKey . BACKGROUND } , { key : ResourceKey . DENSITY , values : identity } , ] , included : ( ) => true , } ; case ResourceType . ICON : return { nodeName : 'icon' , nodeAttributes : [ ResourceKey . SRC , ResourceKey . DENSITY ] , indexAttributes : [ { key : ResourceKey . SRC } , { key : ResourceKey . DENSITY , values : identity } , ] , included : ( ) => true , } ; case ResourceType . SPLASH : return { nodeName : 'splash' , nodeAttributes : [ ResourceKey . SRC , ResourceKey . DENSITY ] , indexAttributes : [ { key : ResourceKey . DENSITY , values : identity } ] , included : ( ) => true , } ; } case Platform . IOS : switch ( resource . type ) { case ResourceType . ICON : return { nodeName : 'icon' , nodeAttributes : [ ResourceKey . SRC , ResourceKey . WIDTH , ResourceKey . HEIGHT , ] , indexAttributes : [ { key : ResourceKey . SRC , values : pathValues } ] , included : ( ) => true , } ; case ResourceType . SPLASH : return { nodeName : 'splash' , nodeAttributes : [ ResourceKey . SRC , ResourceKey . WIDTH , ResourceKey . HEIGHT , ] , indexAttributes : [ { key : ResourceKey . SRC , values : pathValues } ] , included : ( ) => true , } ; } case Platform . WINDOWS : switch ( resource . type ) { case ResourceType . ICON : return { nodeName : 'icon' , nodeAttributes : [ ResourceKey . SRC , ResourceKey . TARGET ] , indexAttributes : [ { key : ResourceKey . SRC , values : pathValues } ] , included : resource => ! ! resource . target , } ; case ResourceType . SPLASH : return { nodeName : 'splash' , nodeAttributes : [ ResourceKey . SRC , ResourceKey . TARGET ] , indexAttributes : [ { key : ResourceKey . SRC , values : pathValues } ] , included : resource => ! ! resource . target , } ; } } }