···66 * @see {deserialize_plist_xml_to_plist_object}
77 * @link https://code.google.com/archive/p/networkpx/wikis/PlistSpec.wiki
88 * @description A single-file zero dependency parser for serializing and deserializing Apple Info.plist files. Supprts only XML formatted plists.
99- * Only supports plists with a single root element, that contain only string, number, boolean, array(also nested), and dictionary(also nested) values.
99+ * Only supports plists with a single root element, that contain only string, number, boolean, reals, dates, array(also nested), and dictionary(also nested) values.
1010 * Attempts to conform to Apple's formatting and specifications for plists in the parts of the spec that are supported.
1111 *
1212 * Notes for things that are not supported:
···1414 * - Large files probably won't work due to the use of using regex to extract the <plist> data. Changing deserialization to use a streaming parser would address this if it is an issue.
1515 * - Comments are ignored and will no-op/be removed when deserializing.
1616 * - Binary data is ignored, and is untested.
1717- * - Dates are ignored, and is untested. At the moment I believe it would resolve to a regular string.
1817 * - All unknown key dictionaries are not supported, and may be treated as a regular JSON object, which may mess up the structure of the plist.
1918 */
2019···2221 * Error types that plist_parser can throw.
2322 */
2423export type plist_parser_error_types = 'unsupported_value_type' | 'unsupported_tag' | 'invalid_xml' | 'info_plist_not_found' | 'infinite_loop'
2525-export type plist_value = string | number | boolean | plist_value[] | { [key: string]: plist_value }
2424+export type plist_value = string | Date | number | boolean | plist_value[] | { [key: string]: plist_value }
26252726/**
2827 * Serialize JS object to plist XML
···7069 case 'string': return serialize_string(value, indent)
7170 case 'boolean': return serialize_boolean(value, indent)
7271 case 'number':
7373- if (!Number.isSafeInteger(value)) break
7272+ if (!Number.isSafeInteger(value))
7373+ if (Number.isFinite(value))
7474+ return serialize_real(value, indent)
7575+ else
7676+ throw new Error('unsupported_value_type' as plist_parser_error_types, {
7777+ cause: { value, type: typeof value },
7878+ })
7479 return serialize_number(value, indent)
7580 case 'object':
7681 if (Array.isArray(value)) return serialize_array(value, indent, depth)
7777- else if (value !== null && value !== undefined) return serialize_object(value as Record<string, plist_value>, indent, depth)
8282+ else if (value !== null && value !== undefined)
8383+ if (value instanceof Date)
8484+ return serialize_date(value as Date, indent)
8585+ else
8686+ return serialize_object(value as Record<string, plist_value>, indent, depth)
7887 }
79888089 throw new Error('unsupported_value_type' as plist_parser_error_types, {
···8291 })
8392}
84939494+function serialize_date(value: Date, indent: string) {
9595+ return `${indent}<date>${value.toISOString()}</date>`
9696+}
9797+8598function serialize_string(value: string, indent: string) {
8699 return `${indent}<string>${escape_xml(value)}</string>`
87100}
···89102// Supporting real values is not supported by this parser, we only support numbers.
90103function serialize_number(value: number, indent: string) {
91104 return `${indent}<integer>${value}</integer>`
105105+}
106106+107107+function serialize_real(value: number, indent: string) {
108108+ return `${indent}<real>${value}</real>`
92109}
9311094111function serialize_boolean(value: boolean, indent: string) {
···130147}
131148132149/* Deserialization */
133133-const plist_parser_regex = /<(dict|array|string|integer)>([\s\S]*?)<\/\1>/
150150+const plist_parser_regex = /<(dict|array|string|integer|date|real)>([\s\S]*?)<\/\1>/
134151function naive_ends_with_closing_tag(xml_fragment: string, _element_name: string) {
135152 const open = xml_fragment.lastIndexOf('<')
136153 return open !== -1 && xml_fragment[open + 1] === '/' && xml_fragment[xml_fragment.length - 1] === '>'
137154}
138155156156+const plist_date_iso8601_regex = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.(\d{1,3}))?Z$/
157157+function deserialize_plist_date_to_date_object(xml_fragment: string) {
158158+ const match = xml_fragment.match(plist_date_iso8601_regex)
159159+ if (!match) throw new Error('invalid_xml' as plist_parser_error_types, { cause: { xml: xml_fragment } })
160160+161161+ const [, year, month, day, hour, minute, second] = match
162162+ return new Date(Date.UTC(
163163+ Number(year),
164164+ Number(month) - 1,
165165+ Number(day),
166166+ Number(hour),
167167+ Number(minute),
168168+ Number(second),
169169+ ))
170170+}
171171+139172function deserialize_xml_fragment_to_plist_value_object(xml_fragment: string): plist_value {
140173 if (xml_fragment === '<true/>' || xml_fragment === '<false/>') return xml_fragment === '<true/>'
141174 else if (xml_fragment === '<array/>') return []
142175 else if (xml_fragment === '<dict/>') return {}
176176+ else if (xml_fragment === '<date/>') return new Date(0)
143177 else if (!naive_ends_with_closing_tag(xml_fragment, '')) throw new Error('invalid_xml' as plist_parser_error_types, { cause: { xml: xml_fragment } })
144178145179 const match = xml_fragment.match(plist_parser_regex)
···152186 if (tag === 'string') return unescape_xml(content!)
153187 else if (tag === 'dict') return deserialize_plist_dict_to_object(content!)
154188 else if (tag === 'array') return deserialize_plist_array_to_object(content!)
189189+ else if (tag === 'date') return deserialize_plist_date_to_date_object(content!)
155190 else if (tag === 'true' || tag === 'false') return tag === 'true'
156191 else if (tag === 'integer') {
157192 const number_value = Number(content!)
158193 if (Number.isSafeInteger(number_value))
159159- return parseInt(content!) // Reals probably could be supported if we added a check for if the value is a fixed int or not.
194194+ return parseInt(content!)
195195+ }
196196+ else if (tag === 'real') {
197197+ const number_value = Number(content!)
198198+ if (Number.isFinite(number_value))
199199+ return number_value
160200 }
161201162202 throw new Error('invalid_xml' as plist_parser_error_types, {