import fs from 'fs-extra' ; import { EOL } from 'os' ; import path from 'path' ; import { Builder , Parser } from 'xml2js' ; export type XMLValue = boolean | number | string | null | XMLArray | XMLObject ; export interface XMLArray extends Array < XMLValue > { } export interface XMLObject { [ key ] : XMLValue | undefined ; } export async function writeXMLAsync ( options ) { const xml = format ( options . xml ) ; await fs . ensureDir ( path . dirname ( options . path ) ) ; await fs . writeFile ( options . path , xml ) ; } export async function readXMLAsync ( options ) { let contents = '' ; try { contents = await fs . readFile ( options . path , { encoding : 'utf8' , flag : 'r' } ) ; } catch { } const parser = new Parser ( ) ; const manifest = await parser . parseStringPromise ( contents || options . fallback || '' ) ; return manifest ; } export async function parseXMLAsync ( contents ) { const xml = await new Parser ( ) . parseStringPromise ( contents ) ; return xml ; } const stringTimesN = ( n , char ) => Array ( n + 1 ) . join ( char ) ; export function format ( manifest , { indentLevel = 2 , newline = EOL } = { } ) { let xmlInput ; if ( typeof manifest === 'string' ) { xmlInput = manifest ; } else if ( manifest . toString ) { const builder = new Builder ( { headless : true } ) ; if ( Array . isArray ( manifest ?. resources ?. string ) ) { for ( const string of manifest ?. resources ?. string ) { if ( string . $ . translatable === 'false' || string . $ . translatable === false ) { continue ; } string . _ = escapeAndroidString ( string . _ ) ; } } xmlInput = builder . buildObject ( manifest ) ; return xmlInput ; } else { throw new Error ( `Invalid XML value passed in: ${ manifest }` ) ; } const indentString = stringTimesN ( indentLevel , ' ' ) ; let formatted = '' ; const regex = /(>)(<)(\/*)/g ; const xml = xmlInput . replace ( regex , `$1${ newline }$2$3` ) ; let pad = 0 ; xml . split ( /\r?\n/ ) . map ( ( line ) => line . trim ( ) ) . forEach ( ( line ) => { let indent = 0 ; if ( line . match ( /.+<\/\w[^>]*>$/ ) ) { indent = 0 ; } else if ( line . match ( /^<\/\w/ ) ) { if ( pad !== 0 ) { pad -= 1 ; } } else if ( line . match ( /^<\w([^>]*[^/])?>.*$/ ) ) { indent = 1 ; } else { indent = 0 ; } const padding = stringTimesN ( pad , indentString ) ; formatted += padding + line + newline ; pad += indent ; } ) ; return formatted . trim ( ) ; } param value export function escapeAndroidString ( value ) { value = value . replace ( /[\n\r\t'"@]/g , m => { switch ( m ) { case '"' : case "'" : case '@' : return '\\' + m ; case '\n' : return '\\n' ; case '\r' : return '\\r' ; case '\t' : return '\\t' ; default : throw new Error ( `Cannot escape unhandled XML character: ${ m }` ) ; } } ) ; if ( value . match ( /(^\s|\s$)/ ) ) { value = '"' + value + '"' ; } return value ; }