/* eslint-disable @typescript-eslint/consistent-type-assertions */ /* --------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ // Some code copied and modified from https://github.com/microsoft/vscode/blob/1.44.0/src/vs/workbench/services/themes/common/plistParser.ts const enum ChCode { BOM = 65279, SPACE = 32, TAB = 9, CARRIAGE_RETURN = 13, LINE_FEED = 10, SLASH = 47, LESS_THAN = 60, QUESTION_MARK = 63, EXCLAMATION_MARK = 33, } const enum State { ROOT_STATE = 0, DICT_STATE = 1, ARR_STATE = 2, } export function parseWithLocation(content: string, filename: string, locationKeyName: string): any { return _parse(content, filename, locationKeyName); } /** * A very fast plist parser */ export function parse(content: string): any { return _parse(content, null, null); } function _parse(content: string, filename: string | null, locationKeyName: string | null): any { const len = content.length; let pos = 0; let line = 1; let char = 0; // Skip UTF8 BOM if (len > 0 && content.charCodeAt(0) === ChCode.BOM) { pos = 1; } function advancePosBy(by: number): void { if (locationKeyName === null) { pos = pos + by; } else { while (by > 0) { const chCode = content.charCodeAt(pos); if (chCode === ChCode.LINE_FEED) { pos++; line++; char = 0; } else { pos++; char++; } by--; } } } function advancePosTo(to: number): void { if (locationKeyName === null) { pos = to; } else { advancePosBy(to - pos); } } function skipWhitespace(): void { while (pos < len) { const chCode = content.charCodeAt(pos); if ( chCode !== ChCode.SPACE && chCode !== ChCode.TAB && chCode !== ChCode.CARRIAGE_RETURN && chCode !== ChCode.LINE_FEED ) { break; } advancePosBy(1); } } function advanceIfStartsWith(str: string): boolean { if (content.substr(pos, str.length) === str) { advancePosBy(str.length); return true; } return false; } function advanceUntil(str: string): void { const nextOccurence = content.indexOf(str, pos); if (nextOccurence !== -1) { advancePosTo(nextOccurence + str.length); } else { // EOF advancePosTo(len); } } function captureUntil(str: string): string { const nextOccurence = content.indexOf(str, pos); if (nextOccurence !== -1) { const r = content.substring(pos, nextOccurence); advancePosTo(nextOccurence + str.length); return r; } else { // EOF const r = content.substr(pos); advancePosTo(len); return r; } } let state = State.ROOT_STATE; let cur: any = null; const stateStack: State[] = []; const objStack: any[] = []; let curKey: string | null = null; function pushState(newState: State, newCur: any): void { stateStack.push(state); objStack.push(cur); state = newState; cur = newCur; } function popState(): void { if (stateStack.length === 0) { return fail('illegal state stack'); } state = stateStack.pop()!; cur = objStack.pop(); } function fail(msg: string): void { throw new Error('Near offset ' + pos + ': ' + msg + ' ~~~' + content.substr(pos, 50) + '~~~'); } const dictState = { enterDict() { if (curKey === null) { return fail('missing '); } const newDict = {}; if (locationKeyName !== null) { newDict[locationKeyName] = { filename, line, char, }; } cur[curKey] = newDict; curKey = null; pushState(State.DICT_STATE, newDict); }, enterArray() { if (curKey === null) { return fail('missing '); } const newArr: any[] = []; cur[curKey] = newArr; curKey = null; pushState(State.ARR_STATE, newArr); }, }; const arrState = { enterDict() { const newDict = {}; if (locationKeyName !== null) { newDict[locationKeyName] = { filename, line, char, }; } cur.push(newDict); pushState(State.DICT_STATE, newDict); }, enterArray() { const newArr: any[] = []; cur.push(newArr); pushState(State.ARR_STATE, newArr); }, }; function enterDict() { if (state === State.DICT_STATE) { dictState.enterDict(); } else if (state === State.ARR_STATE) { arrState.enterDict(); } else { // ROOT_STATE cur = {}; if (locationKeyName !== null) { cur[locationKeyName] = { filename, line, char, }; } pushState(State.DICT_STATE, cur); } } function leaveDict() { if (state === State.DICT_STATE) { popState(); } else if (state === State.ARR_STATE) { return fail('unexpected '); } else { // ROOT_STATE return fail('unexpected '); } } function enterArray() { if (state === State.DICT_STATE) { dictState.enterArray(); } else if (state === State.ARR_STATE) { arrState.enterArray(); } else { // ROOT_STATE cur = []; pushState(State.ARR_STATE, cur); } } function leaveArray() { if (state === State.DICT_STATE) { return fail('unexpected '); } else if (state === State.ARR_STATE) { popState(); } else { // ROOT_STATE return fail('unexpected '); } } function acceptKey(val: string) { if (state === State.DICT_STATE) { if (curKey !== null) { return fail('too many '); } curKey = val; } else if (state === State.ARR_STATE) { return fail('unexpected '); } else { // ROOT_STATE return fail('unexpected '); } } function acceptString(val: string) { if (state === State.DICT_STATE) { if (curKey === null) { return fail('missing '); } cur[curKey] = val; curKey = null; } else if (state === State.ARR_STATE) { cur.push(val); } else { // ROOT_STATE cur = val; } } function acceptReal(val: number) { if (isNaN(val)) { return fail('cannot parse float'); } if (state === State.DICT_STATE) { if (curKey === null) { return fail('missing '); } cur[curKey] = val; curKey = null; } else if (state === State.ARR_STATE) { cur.push(val); } else { // ROOT_STATE cur = val; } } function acceptInteger(val: number) { if (isNaN(val)) { return fail('cannot parse integer'); } if (state === State.DICT_STATE) { if (curKey === null) { return fail('missing '); } cur[curKey] = val; curKey = null; } else if (state === State.ARR_STATE) { cur.push(val); } else { // ROOT_STATE cur = val; } } function acceptDate(val: Date) { if (state === State.DICT_STATE) { if (curKey === null) { return fail('missing '); } cur[curKey] = val; curKey = null; } else if (state === State.ARR_STATE) { cur.push(val); } else { // ROOT_STATE cur = val; } } function acceptData(val: string) { if (state === State.DICT_STATE) { if (curKey === null) { return fail('missing '); } cur[curKey] = val; curKey = null; } else if (state === State.ARR_STATE) { cur.push(val); } else { // ROOT_STATE cur = val; } } function acceptBool(val: boolean) { if (state === State.DICT_STATE) { if (curKey === null) { return fail('missing '); } cur[curKey] = val; curKey = null; } else if (state === State.ARR_STATE) { cur.push(val); } else { // ROOT_STATE cur = val; } } function escapeVal(str: string): string { return str .replace(/&#([0-9]+);/g, function (_: string, m0: string) { return (String).fromCodePoint(parseInt(m0, 10)); }) .replace(/&#x([0-9a-f]+);/g, function (_: string, m0: string) { return (String).fromCodePoint(parseInt(m0, 16)); }) .replace(/&|<|>|"|'/g, function (_: string) { switch (_) { case '&': return '&'; case '<': return '<'; case '>': return '>'; case '"': return '"'; case ''': return "'"; } return _; }); } interface IParsedTag { name: string; isClosed: boolean; } function parseOpenTag(): IParsedTag { let r = captureUntil('>'); let isClosed = false; if (r.charCodeAt(r.length - 1) === ChCode.SLASH) { isClosed = true; r = r.substring(0, r.length - 1); } return { name: r.trim(), isClosed, }; } function parseTagValue(tag: IParsedTag): string { if (tag.isClosed) { return ''; } const val = captureUntil(''); return escapeVal(val); } while (pos < len) { skipWhitespace(); if (pos >= len) { break; } const chCode = content.charCodeAt(pos); advancePosBy(1); if (chCode !== ChCode.LESS_THAN) { return fail('expected <'); } if (pos >= len) { return fail('unexpected end of input'); } const peekChCode = content.charCodeAt(pos); if (peekChCode === ChCode.QUESTION_MARK) { advancePosBy(1); advanceUntil('?>'); continue; } if (peekChCode === ChCode.EXCLAMATION_MARK) { advancePosBy(1); if (advanceIfStartsWith('--')) { advanceUntil('-->'); continue; } advanceUntil('>'); continue; } if (peekChCode === ChCode.SLASH) { advancePosBy(1); skipWhitespace(); if (advanceIfStartsWith('plist')) { advanceUntil('>'); continue; } if (advanceIfStartsWith('dict')) { advanceUntil('>'); leaveDict(); continue; } if (advanceIfStartsWith('array')) { advanceUntil('>'); leaveArray(); continue; } return fail('unexpected closed tag'); } const tag = parseOpenTag(); switch (tag.name) { case 'dict': enterDict(); if (tag.isClosed) { leaveDict(); } continue; case 'array': enterArray(); if (tag.isClosed) { leaveArray(); } continue; case 'key': acceptKey(parseTagValue(tag)); continue; case 'string': acceptString(parseTagValue(tag)); continue; case 'real': acceptReal(parseFloat(parseTagValue(tag))); continue; case 'integer': acceptInteger(parseInt(parseTagValue(tag), 10)); continue; case 'date': acceptDate(new Date(parseTagValue(tag))); continue; case 'data': acceptData(parseTagValue(tag)); continue; case 'true': parseTagValue(tag); acceptBool(true); continue; case 'false': parseTagValue(tag); acceptBool(false); continue; } if (/^plist/.test(tag.name)) { continue; } return fail('unexpected opened tag ' + tag.name); } return cur; }