···55 * @see {serialize_xml_to_plist_object}
66 * @see {deserialize_plist_xml_to_plist_object}
77 * @link https://code.google.com/archive/p/networkpx/wikis/PlistSpec.wiki
88+ * @link https://www.apple.com/DTDs/PropertyList-1.0.dtd
89 * @description A single-file zero dependency parser for serializing and deserializing Apple Info.plist files. Supprts only XML formatted plists.
910 * 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+ * Attempts to conform to Apple's formatting and specifications for plists in the parts of the spec that are supported. Comments are ignored and will no-op/be removed when deserializing.
1112 *
1213 * Notes for things that are not supported:
1314 * - Does not care about UTF-16 encoding support. Untested, but might work fine.
1415 * - 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- * - 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.
1817 */
19182019/**
2120 * Error types that plist_parser can throw.
2221 */
2322export type plist_parser_error_types = 'unsupported_value_type' | 'unsupported_tag' | 'invalid_xml' | 'info_plist_not_found' | 'infinite_loop'
2424-export type plist_value = string | Date | number | boolean | plist_value[] | { [key: string]: plist_value }
2323+/**
2424+ * The plist value type.
2525+ * @see https://www.apple.com/DTDs/PropertyList-1.0.dtd
2626+ * @todo Add support for binary data.
2727+ */
2828+export type plist_value = string | Uint8Array | Date | number | boolean | plist_value[] | { [key: string]: plist_value }
25292630/**
2731 * Serialize JS object to plist XML
···8286 else if (value !== null && value !== undefined)
8387 if (value instanceof Date)
8488 return serialize_date(value as Date, indent)
8989+ else if (value instanceof Uint8Array)
9090+ return serialize_data(value as Uint8Array, indent)
8591 else
8692 return serialize_object(value as Record<string, plist_value>, indent, depth)
8793 }
···110116111117function serialize_boolean(value: boolean, indent: string) {
112118 return `${indent}${value ? '<true/>' : '<false/>'}`
119119+}
120120+121121+function serialize_data(value: Uint8Array, _indent: string): string {
122122+ throw new Error('unsupported_value_type' as plist_parser_error_types, { cause: { value, type: typeof value } })
113123}
114124115125function serialize_array(value: plist_value[], indent: string, depth: number) {
···153163 return open !== -1 && xml_fragment[open + 1] === '/' && xml_fragment[xml_fragment.length - 1] === '>'
154164}
155165156156-const plist_date_iso8601_regex = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.(\d{1,3}))?Z$/
157166function 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 } })
167167+ const timestampe_date = new Date(xml_fragment)
168168+ if (timestampe_date.toString() === 'Invalid Date') throw new Error('invalid_xml' as plist_parser_error_types, { cause: { xml: xml_fragment } })
169169+170170+ return timestampe_date
171171+}
172172+173173+function deserialize_plist_integer_to_number_object(content: string) {
174174+ const number_value = Number(content)
175175+ if (Number.isSafeInteger(number_value))
176176+ return parseInt(content!)
177177+178178+ throw new Error('invalid_xml' as plist_parser_error_types, { cause: { xml: content } })
179179+}
180180+181181+function deserialize_plist_real_to_number_object(content: string) {
182182+ const number_value = Number(content)
183183+ if (Number.isFinite(number_value))
184184+ return number_value
160185161161- 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- ))
186186+ throw new Error('invalid_xml' as plist_parser_error_types, { cause: { xml: content } })
187187+}
188188+189189+function deserialize_plist_data_to_data_object(content: string): plist_value {
190190+ throw new Error('unsupported_tag' as plist_parser_error_types, { cause: { xml: content } })
170191}
171192172193function deserialize_xml_fragment_to_plist_value_object(xml_fragment: string): plist_value {
173194 if (xml_fragment === '<true/>' || xml_fragment === '<false/>') return xml_fragment === '<true/>'
174195 else if (xml_fragment === '<array/>') return []
175196 else if (xml_fragment === '<dict/>') return {}
176176- else if (xml_fragment === '<date/>') return new Date(0)
177197 else if (!naive_ends_with_closing_tag(xml_fragment, '')) throw new Error('invalid_xml' as plist_parser_error_types, { cause: { xml: xml_fragment } })
178198179199 const match = xml_fragment.match(plist_parser_regex)
···188208 else if (tag === 'array') return deserialize_plist_array_to_object(content!)
189209 else if (tag === 'date') return deserialize_plist_date_to_date_object(content!)
190210 else if (tag === 'true' || tag === 'false') return tag === 'true'
191191- else if (tag === 'integer') {
192192- const number_value = Number(content!)
193193- if (Number.isSafeInteger(number_value))
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
200200- }
211211+ else if (tag === 'integer') return deserialize_plist_integer_to_number_object(content!)
212212+ else if (tag === 'real') return deserialize_plist_real_to_number_object(content!)
213213+ else if (tag === 'data') return deserialize_plist_data_to_data_object(content!)
201214202215 throw new Error('invalid_xml' as plist_parser_error_types, {
203203- cause: { xml: xml_fragment },
216216+ cause: { xml: xml_fragment, tag },
204217 })
205218}
206219