From 76fffdc428d6bbe025e8c92f66a2cab9edfa24d5 Mon Sep 17 00:00:00 2001 From: Moritz Schmidt Date: Tue, 13 Feb 2018 18:46:11 +0100 Subject: [PATCH] modernize setup * use jest for testing * use webpack for web * add travis config * move src to /src, add /dist and /web for builds --- .gitignore | 3 + .npmignore | 1 + .travis.yml | 5 + class-parser.js | 314 ------------------ dist/.keep | 0 package.json | 37 ++- src/class-parser-web.ts | 5 + src/class-parser.spec.ts | 235 +++++++++++++ src/class-parser.ts | 289 ++++++++++++++++ tests.html | 27 -- tests.js | 227 ------------- tsconfig.json | 13 + .../session-report-parser.html | 2 +- webpack.config.js | 15 + 14 files changed, 597 insertions(+), 576 deletions(-) create mode 100644 .gitignore create mode 100644 .npmignore create mode 100644 .travis.yml delete mode 100644 class-parser.js create mode 100644 dist/.keep create mode 100644 src/class-parser-web.ts create mode 100644 src/class-parser.spec.ts create mode 100644 src/class-parser.ts delete mode 100644 tests.html delete mode 100644 tests.js create mode 100644 tsconfig.json rename session-report-parser.html => web/session-report-parser.html (98%) create mode 100644 webpack.config.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7df8d94 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +dist/*.* +web/arma-class-parser.js +node_modules diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..3c3629e --- /dev/null +++ b/.npmignore @@ -0,0 +1 @@ +node_modules diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..97bb8e8 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,5 @@ +language: node_js +node_js: + - "8" +script: npm install && npm test + diff --git a/class-parser.js b/class-parser.js deleted file mode 100644 index 2072f2d..0000000 --- a/class-parser.js +++ /dev/null @@ -1,314 +0,0 @@ -if (!Array.prototype.last) { - Array.prototype.last = function () { - return this[this.length - 1]; - }; -} - -(function (callback) { - if (typeof define === 'function' && define.amd) { - define(callback); - } else { - callback(); - } -}(function () { - var - chars = { - QUOTE: '"', - SEMICOLON: ';', - EQUALS: '=', - CURLY_OPEN: '{', - CURLY_CLOSE: '}', - SQUARE_OPEN: '[', - SQUARE_CLOSE: ']', - COMMA: ',', - MINUS: '-', - SLASH: '/', - DOLLAR: '$' - }; - - /** - * - * @param {string} raw - * @param {Object} [options] - * @param {Object.} [options.translations] - */ - function parse(raw, options) { - var - currentPosition = 0, - assert = function (bool, msg) { - if (bool) { - return; - } - throw new Error( - msg + ' at position ' + currentPosition + ', ' + - 'before: ' + JSON.stringify(raw.substr(Math.max(0, currentPosition - 40), 40)) + ', ' + - 'after: ' + JSON.stringify(raw.substr(currentPosition, 40)) - ); - }, - detectLineComment = function () { - var indexOfLinefeed; - if (current() === chars.SLASH && raw[currentPosition + 1] === chars.SLASH) { - indexOfLinefeed = raw.indexOf('\n', currentPosition); - currentPosition = indexOfLinefeed === -1 ? raw.length : indexOfLinefeed; - } - }, - next = function () { - currentPosition += 1; - detectLineComment(); - return current(); - }, - nextWithoutCommentDetection = function () { - currentPosition += 1; - return current(); - }, - current = function () { - return raw[currentPosition]; - }, - result = {}, - weHaveADoubleQuote = function () { - return (raw.substr(currentPosition, 2).indexOf('""') === 0); - }, - weHaveAStringLineBreak = function () { - return raw.substr(currentPosition, 6).indexOf('" \\n "') === 0; - }, - forwardToNextQuote = function () { - currentPosition = indexOfOrMaxInt.call(raw, chars.QUOTE, currentPosition + 1); - }, - parseString = function () { - var result = ''; - assert(current() === chars.QUOTE); - nextWithoutCommentDetection(); - while (true) { - if (weHaveADoubleQuote()) { - result += current(); - nextWithoutCommentDetection(); - } else if (weHaveAStringLineBreak()) { - result += '\n'; - next(); - forwardToNextQuote(); - } else if (current() === chars.QUOTE) { - break; - } else { - result += current(); - } - nextWithoutCommentDetection(); - } - assert(current() === chars.QUOTE); - nextWithoutCommentDetection(); - return result; - }, - parseTranslationString = function() { - var result = ''; - assert(current() === chars.DOLLAR); - next(); - assert( - raw.substr(currentPosition, 3).indexOf('STR') === 0, - 'Invalid translation string beginning' - ); - while (true) { - if ( - current() === chars.SEMICOLON - || (current() === chars.COMMA || current() === chars.CURLY_CLOSE) - ) { - break; - } else { - result += current(); - } - nextWithoutCommentDetection(); - } - assert( - current() === chars.SEMICOLON - || (current() === chars.COMMA || current() === chars.CURLY_CLOSE) - ); - - return translateString(result); - }, - translateString = function(string) { - if(typeof options.translations === "object") { - return options.translations.hasOwnProperty(string) ? - options.translations[string] : string; - } - - return string; - }, - parseNumber = function (str) { - str = str.trim(); - if (str.substr(0, 2) === '0x') { - return parseInt(str); - } - if (str.match(/\-?[\d]*(\.\d)?(e\-?\d+)?/)) { - return parseFloat(str); - } - throw new Error('not a number: ' + str); - }, - indexOfOrMaxInt = function (str, fromPos) { - var pos = this.indexOf(str, fromPos); - return pos === -1 ? Infinity : pos; - }, - parseMathExpression = function () { - var - posOfExpressionEnd = Math.min( - indexOfOrMaxInt.call(raw, chars.SEMICOLON, currentPosition), - indexOfOrMaxInt.call(raw, chars.CURLY_CLOSE, currentPosition), - indexOfOrMaxInt.call(raw, chars.COMMA, currentPosition) - ), - expression = raw.substr(currentPosition, posOfExpressionEnd - currentPosition); - assert(posOfExpressionEnd !== -1); - currentPosition = posOfExpressionEnd; - - // DONT LOOK, IT HURTS - return expression.split('+').map(parseNumber).reduce(function (prev, cur) { - return prev + cur; - }, 0); - }, - parsePropertyValue = function () { - var - result; - - if (current() === chars.CURLY_OPEN) { - result = parseArray(); - } else if (current() === chars.QUOTE) { - result = parseString(); - } else if(current() === chars.DOLLAR) { - result = parseTranslationString(); - } else { - result = parseMathExpression(); - } - return result; - - }, - isValidVarnameChar = function (char) { - return (char >= '0' && char <= '9') || - (char >= 'A' && char <= 'Z') || - (char >= 'a' && char <= 'z') || - char === '_'; - }, - parsePropertyName = function () { - var result = current(); - while (isValidVarnameChar(next())) { - result += current(); - } - return result; - }, - parseClassValue = function () { - var result = {}; - - assert(current() === chars.CURLY_OPEN); - next(); - parseWhitespace(); - - while(current() !== chars.CURLY_CLOSE) { - - parseProperty(result); - parseWhitespace(); - } - - next(); - - return result; - }, - parseArray = function () { - var result = []; - assert(current() === chars.CURLY_OPEN); - next(); - parseWhitespace(); - while (current() !== chars.CURLY_CLOSE) { - result.push(parsePropertyValue()); - parseWhitespace(); - if (current() === chars.COMMA) { - next(); - parseWhitespace(); - } else { - break; - } - - } - next(); - return result; - }, - parseProperty = function (context) { - var - name = parsePropertyName(), - value; - - parseWhitespace(); - - if (name === 'class') { - name = parsePropertyName(); - parseWhitespace(); - if (current() === ':') { - next(); - parseWhitespace(); - parsePropertyName(); - parseWhitespace(); - } - } - - switch (current()) { - case chars.SQUARE_OPEN: - assert(next() === chars.SQUARE_CLOSE); - next(); - parseWhitespace(); - assert(current() === chars.EQUALS); - next(); - parseWhitespace(); - value = parseArray(); - break; - case chars.EQUALS: - next(); - parseWhitespace(); - value = parsePropertyValue(); - break; - case chars.CURLY_OPEN: - value = parseClassValue(); - break; - case chars.SLASH: - if (next() === chars.SLASH) { - currentPosition = raw.indexOf('\n', currentPosition); - break; - } - throw new Error('unexpected value at post ' + currentPosition); - default: - throw new Error('unexpected value at pos ' + currentPosition); - } - - context[name] = value; - parseWhitespace(); - assert(current() === chars.SEMICOLON); - next(); - }, - parseWhitespace = function () { - while ( - (' \t\r\n'.indexOf(raw[currentPosition]) !== -1) || - (raw.charCodeAt(currentPosition) < 32) - ) { - next(); - } - }; - - options = options || {}; - - if (typeof raw !== 'string') { - throw new TypeError('expecting string!'); - } - - detectLineComment(); - parseWhitespace(); - while(current()) { - parseProperty(result); - next(); - parseWhitespace(); - } - - return result; - } - - if (this === this.parent) { - this.parse = parse; - } - if (typeof module === 'object' && module) { - module.exports = parse; - } - - return parse; -})); diff --git a/dist/.keep b/dist/.keep new file mode 100644 index 0000000..e69de29 diff --git a/package.json b/package.json index 58f24c5..31fcb81 100644 --- a/package.json +++ b/package.json @@ -1,13 +1,36 @@ { "name": "arma-class-parser", - "version": "0.1.3", + "version": "1.0.0", "description": "Parse Armed Assault classes (like: session log, mission.sqm, ...)", - "keywords": ["ArmA", "Armed Assault", "parser", "Arma3"], - "author": "Moritz Schmidt (http://www.fusselwurm.de/)", - "files": ["class-parser.js"], + "keywords": [ + "ArmA", + "Armed Assault", + "parser", + "Arma3" + ], + "author": "Moritz Schmidt ", + "files": [ + "dist/*.js" + ], "main": "class-parser", - "repository" : { - "type" : "git", - "url" : "http://github.com/fusselwurm/arma-class-parser.git" + "dependencies": {}, + "repository": { + "type": "git", + "url": "http://github.com/fusselwurm/arma-class-parser.git" + }, + "devDependencies": { + "@types/jest": "^22.1.1", + "@types/node": "^8.0.0", + "jest": "^22.3.0", + "ts-loader": "^4.4.1", + "typescript": "^2.7.1", + "webpack": "^4.12.1", + "webpack-cli": "^3.0.8" + }, + "scripts": { + "test": "tsc && jest", + "build:amd": "tsc --module amd --outFile dist/class-parser.amd.js", + "build:web": "tsc && webpack -p", + "build": "tsc" } } diff --git a/src/class-parser-web.ts b/src/class-parser-web.ts new file mode 100644 index 0000000..a9fba31 --- /dev/null +++ b/src/class-parser-web.ts @@ -0,0 +1,5 @@ +import {parse} from './class-parser'; + +declare var window: {parse?: Function}; + +window.parse = parse; diff --git a/src/class-parser.spec.ts b/src/class-parser.spec.ts new file mode 100644 index 0000000..1d2b4dc --- /dev/null +++ b/src/class-parser.spec.ts @@ -0,0 +1,235 @@ +import {parse} from './class-parser'; + +describe('arma-class-parser', () => { + it("is defined", function() { + expect(typeof parse).toBe('function') + }); + + it("parses empty object", function() { + + let expected = {Moo: {}}; + let result = parse('class Moo {};'); + + expect(result).toEqual(expected); + }); + + + it("parses integer property values", function() { + + let expected = { + Moo: { + value: 1 + } + }; + let result = parse('class Moo {\r\nvalue=1; };'); + + expect(result).toEqual(expected); + }); + + it("finds more than one property", function () { + let expected = { + version: 12, + Moo: { + value: 1 + } + }; + + let result = parse('version=12;\n\nclass Moo {\r\n value = 1; };'); + + expect(result).toEqual(expected); + }); + + it("understands scalar array properties", function () { + let expected = { + Moo: { + foo: ['bar', 'baz', 1.5e2] + } + }; + + let result = parse('class Moo {\r\nfoo[]={"bar", "baz",1.5e2}; };'); + + expect(result).toEqual(expected); + }); + + it("knows to parse scientific notation", function () { + expect(parse("x=-1.5e2;")).toEqual({x: -1.5e2}); + }); + + it("can do simple arithmetic", function () { + expect(parse("x=48+0x800;")).toEqual({x: 48 + 0x800}); + }); + + it("ignores symbols", function () { + let testString = "class Moo {\n" + + "\tfoo = xxx;\n" + + "\tclass xxx {};\n" + + "};"; + + expect(parse(testString)).toEqual({Moo: {foo: NaN, xxx: {}}}); + }); + + it("ignores inheritance (?)", function () { + let testString = "class Moo : foo {};"; + expect(parse(testString)).toEqual({Moo: {}}); + }); + + it("ignores line comments", function () { + + expect(parse("// foo comment")).toEqual({}); + expect(parse("// foo comment\nx=2;")).toEqual({x: 2}); + expect(parse("x=2;// foo comment")).toEqual({x: 2}); + expect(parse("class Moo { // foo comment\n};")).toEqual({Moo: {}}); + + }); + + it("can handle escaped quotes", function () { + expect(parse('foo="bar ""haha"";";\n')).toEqual({foo: 'bar "haha";'}); + }); + + it("mission report", function() { + + let expected = { + "Session": { + "Player1": { + "customScore": 0, + "killed": 0, + "killsAir": 0, + "killsArmor": 0, + "killsInfantry": 4, + "killsPlayers": 0, + "killsSoft": 0, + "killsTotal": 4, + "name": "Lord DK" + }, + "Player2": { + "customScore": 0, + "killed": 0, + "killsAir": 0, + "killsArmor": 0, + "killsInfantry": 3, + "killsPlayers": 0, + "killsSoft": 0, + "killsTotal": 3, + "name": "XiviD" + }, + "Player3": { + "customScore": 0, + "killed": 0, + "killsAir": 0, + "killsArmor": 0, + "killsInfantry": 2, + "killsPlayers": 0, + "killsSoft": 0, + "killsTotal": 2, + "name": "40mm2Die" + }, + "Player4": { + "customScore": 0, + "killed": 0, + "killsAir": 0, + "killsArmor": 0, + "killsInfantry": 4, + "killsPlayers": 0, + "killsSoft": 0, + "killsTotal": 4, + "name": "WickerMan" + }, + "Player5": { + "customScore": 0, + "killed": 1, + "killsAir": 0, + "killsArmor": 0, + "killsInfantry": 3, + "killsPlayers": 0, + "killsSoft": -1, + "killsTotal": 1, + "name": "Fusselwurm" + }, + "Player6": { + "customScore": 0, + "killed": 0, + "killsAir": 0, + "killsArmor": 0, + "killsInfantry": 0, + "killsPlayers": 0, + "killsSoft": 0, + "killsTotal": 0, + "name": "Simmax" + }, + "Player7": { + "customScore": 0, + "killed": 2, + "killsAir": 0, + "killsArmor": 0, + "killsInfantry": 0, + "killsPlayers": 0, + "killsSoft": 0, + "killsTotal": 0, + "name": "Andre" + }, + "duration": 5821.1724, + "gameType": "Coop", + "island": "Altis", + "mission": "W-CO@10 StealBoot v03" + } + }; + + let result = parse("\n\tclass Session\n\t{\n\tmission=\"W-CO@10 StealBoot v03\";\n\tisland=\"Altis\";\n\t" + + "gameType=\"Coop\";\n\tduration=5821.1724;\n\tclass Player1\n\t{\n\tname=\"Lord DK\";\n\tkillsInfantry=4;\n\t" + + "killsSoft=0;\n\tkillsArmor=0;\n\tkillsAir=0;\n\tkillsPlayers=0;\n\tcustomScore=0;\n\tkillsTotal=4;\n\tkilled=0;" + + "\n\t};\n\tclass Player2\n\t{\n\tname=\"XiviD\";\n\tkillsInfantry=3;\n\tkillsSoft=0;\n\tkillsArmor=0;\n\tkillsAir=0;" + + "\n\tkillsPlayers=0;\n\tcustomScore=0;\n\tkillsTotal=3;\n\tkilled=0;\n\t};\n\t" + + "class Player3\n\t{\n\tname=\"40mm2Die\";\n\tkillsInfantry=2;\n\tkillsSoft=0;\n\tkillsArmor=0;\n\tkillsAir=0;" + + "\n\tkillsPlayers=0;\n\tcustomScore=0;\n\tkillsTotal=2;\n\tkilled=0;\n\t};\n\t" + + "class Player4\n\t{\n\tname=\"WickerMan\";\n\tkillsInfantry=4;\n\tkillsSoft=0;\n\tkillsArmor=0;\n\tkillsAir=0;" + + "\n\tkillsPlayers=0;\n\tcustomScore=0;\n\tkillsTotal=4;\n\tkilled=0;\n\t};\n\t" + + "class Player5\n\t{\n\tname=\"Fusselwurm\";\n\tkillsInfantry=3;\n\tkillsSoft=-1;\n\tkillsArmor=0;\n\tkillsAir=0;" + + "\n\tkillsPlayers=0;\n\tcustomScore=0;\n\tkillsTotal=1;\n\tkilled=1;\n\t};\n\t" + + "class Player6\n\t{\n\tname=\"Simmax\";\n\tkillsInfantry=0;\n\tkillsSoft=0;\n\tkillsArmor=0;\n\tkillsAir=0;" + + "\n\tkillsPlayers=0;\n\tcustomScore=0;\n\tkillsTotal=0;\n\tkilled=0;\n\t};\n\t" + + "class Player7\n\t{\n\tname=\"Andre\";\n\tkillsInfantry=0;\n\tkillsSoft=0;\n\tkillsArmor=0;\n\tkillsAir=0;" + + "\n\tkillsPlayers=0;\n\tcustomScore=0;\n\tkillsTotal=0;\n\tkilled=2;\n\t};\n\t};\n\n\t"); + + expect(result).toEqual(expected); + }); + + it("supports multiline init expressions created by editor", function () { + const source = `class Item0 { + position[]={1954.6425,5.9796591,5538.1045}; + id=0; + init="[this, ""Platoon""] call FP_fnc_setVehicleName;" \\n "if (isServer) then {" \\n " [this] call FP_fnc_clearVehicle; this addWeaponCargoGlobal [""CUP_launch_M136"", 1];" \\n " this addMagazineCargoGlobal [""1Rnd_HE_Grenade_shell"", 10];" \\n " this addMagazineCargoGlobal [""ATMine_Range_Mag"", 6];" \\n "};"; +};`; + let result = parse(source); + let expected = { + Item0: { + position: [1954.6425, 5.9796591, 5538.1045], + id: 0, + init: '[this, "Platoon"] call FP_fnc_setVehicleName;\nif (isServer) then {\n [this] call FP_fnc_clearVehicle; this addWeaponCargoGlobal ["CUP_launch_M136", 1];\n this addMagazineCargoGlobal ["1Rnd_HE_Grenade_shell", 10];\n this addMagazineCargoGlobal ["ATMine_Range_Mag", 6];\n};' + } + }; + + expect(result).toEqual(expected); + }); + + it("class with translation strings", () => { + const expected = { + testClass: { + title: "Test Class", + values: [0, 1], + texts: ["STR_UNTRANSLATED", "Translated text"], + default: 1 + } + }; + + const testString = "class testClass {\n\ttitle = $STR_CLASS_TITLE;\n\tvalues[] = {0,1};\n\ttexts[] = {$STR_UNTRANSLATED, $STR_TRANSLATED};\n\tdefault = 1;\n};"; + + const result = parse(testString, { + translations: { + STR_CLASS_TITLE: 'Test Class', + STR_TRANSLATED: 'Translated text' + } + }); + + expect(result).toEqual(expected); + }); +}); diff --git a/src/class-parser.ts b/src/class-parser.ts new file mode 100644 index 0000000..60299dd --- /dev/null +++ b/src/class-parser.ts @@ -0,0 +1,289 @@ +const chars = { + QUOTE: '"', + SEMICOLON: ';', + EQUALS: '=', + CURLY_OPEN: '{', + CURLY_CLOSE: '}', + SQUARE_OPEN: '[', + SQUARE_CLOSE: ']', + COMMA: ',', + MINUS: '-', + SLASH: '/', + DOLLAR: '$', +}; + +export interface Options { + translations?: { + [key: string]: string; + } +} + +export const parse = function (raw: string, options?: Options): any { + let currentPosition: number = 0; + let current = function (): string { + return raw[currentPosition] || ''; + }; + let translateString = function(string: string): string { + if(typeof options.translations === "object") { + return options.translations.hasOwnProperty(string) ? + options.translations[string] : string; + } + + return string; + }; + let indexOfOrMaxInt = function (str: string, fromPos: number) { + const pos = this.indexOf(str, fromPos); + return pos === -1 ? Infinity : pos; + }; + let parseArray = function (): any[] { + const result = []; + assert(current() === chars.CURLY_OPEN); + next(); + parseWhitespace(); + while (current() !== chars.CURLY_CLOSE) { + result.push(parsePropertyValue()); + parseWhitespace(); + if (current() === chars.COMMA) { + next(); + parseWhitespace(); + } else { + break; + } + + } + next(); + return result; + }; + let parseProperty = function (context): any { + let name = parsePropertyName(), + value; + + parseWhitespace(); + + if (name === 'class') { + name = parsePropertyName(); + parseWhitespace(); + if (current() === ':') { + next(); + parseWhitespace(); + parsePropertyName(); + parseWhitespace(); + } + } + + switch (current()) { + case chars.SQUARE_OPEN: + assert(next() === chars.SQUARE_CLOSE); + next(); + parseWhitespace(); + assert(current() === chars.EQUALS); + next(); + parseWhitespace(); + value = parseArray(); + break; + case chars.EQUALS: + next(); + parseWhitespace(); + value = parsePropertyValue(); + break; + case chars.CURLY_OPEN: + value = parseClassValue(); + break; + case chars.SLASH: + if (next() === chars.SLASH) { + currentPosition = raw.indexOf('\n', currentPosition); + break; + } + throw new Error('unexpected value at post ' + currentPosition); + case chars.DOLLAR: + result = parseTranslationString(); + break; + default: + throw new Error('unexpected value at pos ' + currentPosition); + } + + context[name] = value; + parseWhitespace(); + assert(current() === chars.SEMICOLON); + next(); + }; + let parseWhitespace = function (): void { + while ( + (' \t\r\n'.indexOf(raw[currentPosition]) !== -1) || + (raw.charCodeAt(currentPosition) < 32) + ) { + next(); + } + }; + let assert = function(bool: boolean, msg: string = ''): void { + if (bool) { + return; + } + throw new Error( + msg + ' at position ' + currentPosition + ', ' + + 'before: ' + JSON.stringify(raw.substr(Math.max(0, currentPosition - 40), 40)) + ', ' + + 'after: ' + JSON.stringify(raw.substr(currentPosition, 40)) + ); + }, + detectLineComment = function(): void { + let indexOfLinefeed; + if (current() === chars.SLASH && raw[currentPosition + 1] === chars.SLASH) { + indexOfLinefeed = raw.indexOf('\n', currentPosition); + currentPosition = indexOfLinefeed === -1 ? raw.length : indexOfLinefeed; + } + }, + next = function(): string { + currentPosition += 1; + detectLineComment(); + return current(); + }, + nextWithoutCommentDetection = function(): string { + currentPosition += 1; + return current(); + }, + result = {}, + weHaveADoubleQuote = function(): boolean { + return (raw.substr(currentPosition, 2).indexOf('""') === 0); + }, + weHaveAStringLineBreak = function(): boolean { + return raw.substr(currentPosition, 6).indexOf('" \\n "') === 0; + }, + forwardToNextQuote = function(): void { + currentPosition = indexOfOrMaxInt.call(raw, chars.QUOTE, currentPosition + 1); + }, + parseString = function(): any { + let result = ''; + assert(current() === chars.QUOTE); + nextWithoutCommentDetection(); + while (true) { + if (weHaveADoubleQuote()) { + result += current(); + nextWithoutCommentDetection(); + } else if (weHaveAStringLineBreak()) { + result += '\n'; + next(); + forwardToNextQuote(); + } else if (current() === chars.QUOTE) { + break; + } else { + result += current(); + } + nextWithoutCommentDetection(); + } + assert(current() === chars.QUOTE); + nextWithoutCommentDetection(); + return result; + }, + parseTranslationString = function(): string { + let result: string = ''; + assert(current() === chars.DOLLAR); + next(); + assert( + raw.substr(currentPosition, 3).indexOf('STR') === 0, + 'Invalid translation string beginning' + ); + while (true) { + if ( + current() === chars.SEMICOLON + || (current() === chars.COMMA || current() === chars.CURLY_CLOSE) + ) { + break; + } else { + result += current(); + } + nextWithoutCommentDetection(); + } + assert( + current() === chars.SEMICOLON + || (current() === chars.COMMA || current() === chars.CURLY_CLOSE) + ); + + return translateString(result); + }, + parseNumber = function(str): number { + str = str.trim(); + if (str.substr(0, 2) === '0x') { + return parseInt(str); + } + if (str.match(/\-?[\d]*(\.\d)?(e\-?\d+)?/)) { + return parseFloat(str); + } + throw new Error('not a number: ' + str); + }, + parseMathExpression = function() { + const posOfExpressionEnd = Math.min( + indexOfOrMaxInt.call(raw, chars.SEMICOLON, currentPosition), + indexOfOrMaxInt.call(raw, chars.CURLY_CLOSE, currentPosition), + indexOfOrMaxInt.call(raw, chars.COMMA, currentPosition) + ), + expression = raw.substr(currentPosition, posOfExpressionEnd - currentPosition); + assert(posOfExpressionEnd !== Infinity); + currentPosition = posOfExpressionEnd; + + // DONT LOOK, IT HURTS + return expression.split('+').map(parseNumber).reduce(function(prev, cur) { + return prev + cur; + }, 0); + }, + parsePropertyValue = function(): any { + let result; + + if (current() === chars.CURLY_OPEN) { + result = parseArray(); + } else if (current() === chars.QUOTE) { + result = parseString(); + } else if (current() === chars.DOLLAR) { + result = parseTranslationString(); + } else { + result = parseMathExpression(); + } + return result; + + }, + isValidVarnameChar = function(char): boolean { + return (char >= '0' && char <= '9') || + (char >= 'A' && char <= 'Z') || + (char >= 'a' && char <= 'z') || + char === '_'; + }, + parsePropertyName = function(): string { + let result = current(); + while (isValidVarnameChar(next())) { + result += current(); + } + return result; + }, + parseClassValue = function(): any { + const result = {}; + + assert(current() === chars.CURLY_OPEN); + next(); + parseWhitespace(); + + while (current() !== chars.CURLY_CLOSE) { + + parseProperty(result); + parseWhitespace(); + } + + next(); + + return result; + }; + + options = options || {}; + + if (typeof raw !== 'string') { + throw new TypeError('expecting string!'); + } + + detectLineComment(); + parseWhitespace(); + while(current()) { + parseProperty(result); + next(); + parseWhitespace(); + } + + return result; +}; diff --git a/tests.html b/tests.html deleted file mode 100644 index 4ba3aa7..0000000 --- a/tests.html +++ /dev/null @@ -1,27 +0,0 @@ - - - - - QUnit Example - - - - -
-class Item0 { - position[]={1954.6425,5.9796591,5538.1045}; - id=0; - init="[this, ""Platoon""] call FP_fnc_setVehicleName;" \n "if (isServer) then {" \n " [this] call FP_fnc_clearVehicle; this addWeaponCargoGlobal [""CUP_launch_M136"", 1];" \n " this addMagazineCargoGlobal [""1Rnd_HE_Grenade_shell"", 10];" \n " this addMagazineCargoGlobal [""ATMine_Range_Mag"", 6];" \n "};"; -}; -
-
-
- - - - - \ No newline at end of file diff --git a/tests.js b/tests.js deleted file mode 100644 index 0362849..0000000 --- a/tests.js +++ /dev/null @@ -1,227 +0,0 @@ -QUnit.test("parser exists", function(assert) { - assert.equal(typeof window.parse, 'function'); -}); - -QUnit.test("empty object", function(assert) { - - var expected = {Moo: {}}; - var result = window.parse('class Moo {};'); - - assert.deepEqual(result, expected); -}); - - -QUnit.test("integer property", function(assert) { - - var expected = { - Moo: { - value: 1 - } - }; - var result = window.parse('class Moo {\r\nvalue=1; };'); - - assert.deepEqual(result, expected); -}); - -QUnit.test("more than one value in file", function (assert) { - var expected = { - version: 12, - Moo: { - value: 1 - } - }; - - var result = window.parse('version=12;\n\nclass Moo {\r\n value = 1; };'); - - assert.deepEqual(result, expected); -}); - -QUnit.test("array of scalars", function (assert) { - var expected = { - Moo: { - foo: ['bar', 'baz', 1.5e2] - } - }; - - var result = window.parse('class Moo {\r\nfoo[]={"bar", "baz",1.5e2}; };'); - - assert.deepEqual(result, expected); -}); - -QUnit.test("scientific notation", function (assert) { - assert.deepEqual(parse("x=-1.5e2;"), {x: -1.5e2}); -}); - -QUnit.test("simple arithmetic", function (assert) { - assert.deepEqual(parse("x=48+0x800;"), {x: 48 + 0x800}); -}); - -QUnit.test("ignore symbols", function (assert) { - var testString = "class Moo {\n" + - "\tfoo = xxx;\n" + - "\tclass xxx {};\n" + - "};"; - - assert.deepEqual(parse(testString), {Moo: {foo: NaN, xxx: {}}}); -}); - -QUnit.test("ignore inheritance (?)", function (assert) { - var testString = "class Moo : foo {};"; - assert.deepEqual(parse(testString), {Moo: {}}); -}); - -QUnit.test("line comments", function (assert) { - - assert.deepEqual(parse("// foo comment"), {}); - assert.deepEqual(parse("// foo comment\nx=2;"), {x: 2}); - assert.deepEqual(parse("x=2;// foo comment"), {x: 2}); - assert.deepEqual(parse("class Moo { // foo comment\n};"), {Moo: {}}); - -}); - -QUnit.test("quote escaping by double quote -.-", function (assert) { - assert.deepEqual(parse('foo="bar ""haha"";";\n'), {foo: 'bar "haha";'}); -}); - -QUnit.test("mission report", function(assert) { - - var expected = { - "Session": { - "Player1": { - "customScore": 0, - "killed": 0, - "killsAir": 0, - "killsArmor": 0, - "killsInfantry": 4, - "killsPlayers": 0, - "killsSoft": 0, - "killsTotal": 4, - "name": "Lord DK" - }, - "Player2": { - "customScore": 0, - "killed": 0, - "killsAir": 0, - "killsArmor": 0, - "killsInfantry": 3, - "killsPlayers": 0, - "killsSoft": 0, - "killsTotal": 3, - "name": "XiviD" - }, - "Player3": { - "customScore": 0, - "killed": 0, - "killsAir": 0, - "killsArmor": 0, - "killsInfantry": 2, - "killsPlayers": 0, - "killsSoft": 0, - "killsTotal": 2, - "name": "40mm2Die" - }, - "Player4": { - "customScore": 0, - "killed": 0, - "killsAir": 0, - "killsArmor": 0, - "killsInfantry": 4, - "killsPlayers": 0, - "killsSoft": 0, - "killsTotal": 4, - "name": "WickerMan" - }, - "Player5": { - "customScore": 0, - "killed": 1, - "killsAir": 0, - "killsArmor": 0, - "killsInfantry": 3, - "killsPlayers": 0, - "killsSoft": -1, - "killsTotal": 1, - "name": "Fusselwurm" - }, - "Player6": { - "customScore": 0, - "killed": 0, - "killsAir": 0, - "killsArmor": 0, - "killsInfantry": 0, - "killsPlayers": 0, - "killsSoft": 0, - "killsTotal": 0, - "name": "Simmax" - }, - "Player7": { - "customScore": 0, - "killed": 2, - "killsAir": 0, - "killsArmor": 0, - "killsInfantry": 0, - "killsPlayers": 0, - "killsSoft": 0, - "killsTotal": 0, - "name": "Andre" - }, - "duration": 5821.1724, - "gameType": "Coop", - "island": "Altis", - "mission": "W-CO@10 StealBoot v03" - } - }; - - var result = window.parse("\n\tclass Session\n\t{\n\tmission=\"W-CO@10 StealBoot v03\";\n\tisland=\"Altis\";\n\t" + - "gameType=\"Coop\";\n\tduration=5821.1724;\n\tclass Player1\n\t{\n\tname=\"Lord DK\";\n\tkillsInfantry=4;\n\t" + - "killsSoft=0;\n\tkillsArmor=0;\n\tkillsAir=0;\n\tkillsPlayers=0;\n\tcustomScore=0;\n\tkillsTotal=4;\n\tkilled=0;" + - "\n\t};\n\tclass Player2\n\t{\n\tname=\"XiviD\";\n\tkillsInfantry=3;\n\tkillsSoft=0;\n\tkillsArmor=0;\n\tkillsAir=0;" + - "\n\tkillsPlayers=0;\n\tcustomScore=0;\n\tkillsTotal=3;\n\tkilled=0;\n\t};\n\t" + - "class Player3\n\t{\n\tname=\"40mm2Die\";\n\tkillsInfantry=2;\n\tkillsSoft=0;\n\tkillsArmor=0;\n\tkillsAir=0;" + - "\n\tkillsPlayers=0;\n\tcustomScore=0;\n\tkillsTotal=2;\n\tkilled=0;\n\t};\n\t" + - "class Player4\n\t{\n\tname=\"WickerMan\";\n\tkillsInfantry=4;\n\tkillsSoft=0;\n\tkillsArmor=0;\n\tkillsAir=0;" + - "\n\tkillsPlayers=0;\n\tcustomScore=0;\n\tkillsTotal=4;\n\tkilled=0;\n\t};\n\t" + - "class Player5\n\t{\n\tname=\"Fusselwurm\";\n\tkillsInfantry=3;\n\tkillsSoft=-1;\n\tkillsArmor=0;\n\tkillsAir=0;" + - "\n\tkillsPlayers=0;\n\tcustomScore=0;\n\tkillsTotal=1;\n\tkilled=1;\n\t};\n\t" + - "class Player6\n\t{\n\tname=\"Simmax\";\n\tkillsInfantry=0;\n\tkillsSoft=0;\n\tkillsArmor=0;\n\tkillsAir=0;" + - "\n\tkillsPlayers=0;\n\tcustomScore=0;\n\tkillsTotal=0;\n\tkilled=0;\n\t};\n\t" + - "class Player7\n\t{\n\tname=\"Andre\";\n\tkillsInfantry=0;\n\tkillsSoft=0;\n\tkillsArmor=0;\n\tkillsAir=0;" + - "\n\tkillsPlayers=0;\n\tcustomScore=0;\n\tkillsTotal=0;\n\tkilled=2;\n\t};\n\t};\n\n\t"); - - assert.deepEqual(result, expected); -}); - -QUnit.test("multiline-init", function (assert) { - var source = document.getElementById('fixture-multiline-init').textContent; - var result = window.parse(source); - var expected = { - Item0: { - position: [1954.6425, 5.9796591, 5538.1045], - id: 0, - init: '[this, "Platoon"] call FP_fnc_setVehicleName;\nif (isServer) then {\n [this] call FP_fnc_clearVehicle; this addWeaponCargoGlobal ["CUP_launch_M136", 1];\n this addMagazineCargoGlobal ["1Rnd_HE_Grenade_shell", 10];\n this addMagazineCargoGlobal ["ATMine_Range_Mag", 6];\n};' - } - }; - - assert.deepEqual(result, expected); -}); - -QUnit.test("class with translation strings", function (assert) { - var expected = { - testClass: { - title: "Test Class", - values: [0, 1], - texts: ["STR_UNTRANSLATED", "Translated text"], - default: 1 - } - }; - - var testString = "class testClass {\n\ttitle = $STR_CLASS_TITLE;\n\tvalues[] = {0,1};\n\ttexts[] = {$STR_UNTRANSLATED, $STR_TRANSLATED};\n\tdefault = 1;\n};" - - var result = window.parse(testString, { - translations: { - STR_CLASS_TITLE: 'Test Class', - STR_TRANSLATED: 'Translated text' - } - }); - - assert.deepEqual(result, expected); -}); \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..52e91de --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "es5", + "lib": [ "es2015"], + "outDir": "dist", + "sourceMap": true, + "declaration": true + }, + "include": [ + "src/**.ts" + ] +} diff --git a/session-report-parser.html b/web/session-report-parser.html similarity index 98% rename from session-report-parser.html rename to web/session-report-parser.html index 65fdf4d..d52681b 100644 --- a/session-report-parser.html +++ b/web/session-report-parser.html @@ -50,7 +50,7 @@ }*/ - +