diff --git a/.vscode/launch.json b/.vscode/launch.json index 652070ee1..2ab4b6d46 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -4,6 +4,7 @@ // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ + { "name": "Launch C# Compiler", "type": "node", @@ -25,6 +26,45 @@ "env": { "TS_NODE_PROJECT": "tsconfig.build-csharp.json" } + }, + + { + "name": "Launch TypeScript Generator", + "type": "node", + "request": "launch", + "args": [ + "src.compiler/typescript/AlphaTabGenerator.ts", + "--project", + "tsconfig.build-csharp.json" + ], + "runtimeArgs": [ + "--nolazy", + "-r", + "ts-node/register" + ], + "cwd": "${workspaceRoot}", + "protocol": "inspector", + "smartStep": false, + "internalConsoleOptions": "openOnSessionStart", + "env": { + "TS_NODE_PROJECT": "tsconfig.build-csharp.json" + } + }, + + { + "name": "Launch JavaScript Compiler", + "type": "node", + "request": "launch", + "runtimeExecutable": "npm", + "windows": { + "runtimeExecutable": "npm.cmd" + }, + "runtimeArgs": [ + "run-script", + "build", + "--inspect-brk=5858" + ], + "port": 5858 } ] } \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index e302cd919..78af7f1e9 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,6 +1,7 @@ { "files.exclude": { "dist/lib.csharp/**": true, - "src.csharp/**": true + "src.csharp/**": true, + "node_modules/**": true } } \ No newline at end of file diff --git a/karma.conf.js b/karma.conf.js index f7b7d5e0e..19595f6e8 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -15,6 +15,7 @@ const storage = multer.diskStorage({ const upload = multer({ storage: storage }); const cors = require('cors'); const fs = require('fs'); +const path = require('path'); module.exports = function (config) { config.set({ @@ -54,6 +55,25 @@ module.exports = function (config) { port: 8090, appVisitor: function (app, log) { app.use(cors()); + app.get( + '/list-files', + function (req, res) { + log.info(`loading files from ${req.query.dir}`); + + const directoryPath = path.join(__dirname, req.query.dir); + fs.readdir(directoryPath, (err, files) => { + //handling error + if (err) { + res.status(400); + res.send(JSON.stringify(`Error: ${err.message}`)); + } else { + res.send(JSON.stringify(files.filter(f => + fs.statSync(path.join(directoryPath, f)).isFile() + ))); + } + }); + } + ); app.post( '/save-visual-error', upload.fields([ diff --git a/package-lock.json b/package-lock.json index 343ee2c2e..211bad9b9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -57,9 +57,9 @@ } }, "@rollup/plugin-commonjs": { - "version": "16.0.0", - "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-16.0.0.tgz", - "integrity": "sha512-LuNyypCP3msCGVQJ7ki8PqYdpjfEkE/xtFa5DqlF+7IBD0JsfMZ87C58heSwIMint58sAUZbt3ITqOmdQv/dXw==", + "version": "17.0.0", + "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-17.0.0.tgz", + "integrity": "sha512-/omBIJG1nHQc+bgkYDuLpb/V08QyutP9amOrJRUSlYJZP+b/68gM//D8sxJe3Yry2QnYIr3QjR3x4AlxJEN3GA==", "dev": true, "requires": { "@rollup/pluginutils": "^3.1.0", @@ -72,12 +72,12 @@ }, "dependencies": { "resolve": { - "version": "1.18.1", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.18.1.tgz", - "integrity": "sha512-lDfCPaMKfOJXjy0dPayzPdF1phampNWr3qFCjAu+rw/qbQmr5jWH5xN2hwh9QKfw9E5v4hwV7A+jrCmL8yjjqA==", + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.19.0.tgz", + "integrity": "sha512-rArEXAgsBG4UgRGcynxWIWKFvh/XZCcS8UJdHhwy91zwAvCZIbcs+vAbflgBnNjYMs/i/i+/Ux6IZhML1yPvxg==", "dev": true, "requires": { - "is-core-module": "^2.0.0", + "is-core-module": "^2.1.0", "path-parse": "^1.0.6" } } @@ -135,9 +135,9 @@ } }, "@types/jasmine": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-3.6.1.tgz", - "integrity": "sha512-eeSCVhBsgwHNS1FmaMu4zrLxfykCTWJMLFZv7lmyrZQjw7foUUXoPu4GukSN9v7JvUw7X+/aDH3kCaymirBSTg==", + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-3.6.2.tgz", + "integrity": "sha512-AzfesNFLvOs6Q1mHzIsVJXSeUnqVh4ZHG8ngygKJfbkcSLwzrBVm/LKa+mR8KrOfnWtUL47112gde1MC0IXqpQ==", "dev": true }, "@types/minimatch": { @@ -787,6 +787,12 @@ "vary": "^1" } }, + "create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true + }, "custom-event": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/custom-event/-/custom-event-1.0.1.tgz", @@ -1111,9 +1117,9 @@ "dev": true }, "estree-walker": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.1.tgz", - "integrity": "sha512-tF0hv+Yi2Ot1cwj9eYHtxC0jB9bmjacjQs6ZBTj82H8JwUywFuc+7E83NWfNMwHXZc11mjfFcVXPe9gEP4B8dg==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", "dev": true }, "esutils": { @@ -1931,9 +1937,9 @@ "dev": true }, "is-core-module": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.1.0.tgz", - "integrity": "sha512-YcV7BgVMRFRua2FqQzKtTDMz8iCuLEyGKjr70q8Zm1yy2qKcurbFEd79PAdHV77oL3NrAaOVQIbMmiHQCHB7ZA==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.2.0.tgz", + "integrity": "sha512-XRAfAdyyY5F5cOXn7hYQDqh2Xmii+DEfIcQGxK/uNwMHhIkPWO0g8msXcbzLe+MpGoR951MlqM/2iIlU4vKDdQ==", "dev": true, "requires": { "has": "^1.0.3" @@ -3074,9 +3080,9 @@ } }, "rollup": { - "version": "2.33.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.33.1.tgz", - "integrity": "sha512-uY4O/IoL9oNW8MMcbA5hcOaz6tZTMIh7qJHx/tzIJm+n1wLoY38BLn6fuy7DhR57oNFLMbDQtDeJoFURt5933w==", + "version": "2.35.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.35.1.tgz", + "integrity": "sha512-q5KxEyWpprAIcainhVy6HfRttD9kutQpHbeqDTWnqAFNJotiojetK6uqmcydNMymBEtC4I8bCYR+J3mTMqeaUA==", "dev": true, "requires": { "fsevents": "~2.1.2" @@ -3109,43 +3115,13 @@ } }, "rollup-plugin-dts": { - "version": "1.4.13", - "resolved": "https://registry.npmjs.org/rollup-plugin-dts/-/rollup-plugin-dts-1.4.13.tgz", - "integrity": "sha512-7mxoQ6PcmCkBE5ZhrjGDL4k42XLy8BkSqpiRi1MipwiGs+7lwi4mQkp2afX+OzzLjJp/TGM8llfe8uayIUhPEw==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/rollup-plugin-dts/-/rollup-plugin-dts-2.0.1.tgz", + "integrity": "sha512-y38NSXIY37YExCumbGBTL5dXg7pL7XD+Kbe98iEHWFN9yiKJf7t4kKBOkml5ylUDjQIXBnNClGDeRktc1T5dmA==", "dev": true, "requires": { - "@babel/code-frame": "^7.10.4" - }, - "dependencies": { - "@babel/code-frame": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", - "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", - "dev": true, - "optional": true, - "requires": { - "@babel/highlight": "^7.10.4" - } - }, - "@babel/helper-validator-identifier": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", - "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", - "dev": true, - "optional": true - }, - "@babel/highlight": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz", - "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==", - "dev": true, - "optional": true, - "requires": { - "@babel/helper-validator-identifier": "^7.10.4", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - } - } + "@babel/code-frame": "^7.10.4", + "magic-string": "^0.25.7" } }, "rollup-plugin-license": { @@ -3908,9 +3884,9 @@ } }, "terser": { - "version": "5.3.8", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.3.8.tgz", - "integrity": "sha512-zVotuHoIfnYjtlurOouTazciEfL7V38QMAOhGqpXDEg6yT13cF4+fEP9b0rrCEQTn+tT46uxgFsTZzhygk+CzQ==", + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.5.1.tgz", + "integrity": "sha512-6VGWZNVP2KTUcltUQJ25TtNjx/XgdDsBDKGt8nN0MpydU36LmbPPcMBd2kmtZNNGVVDLg44k7GKeHHj+4zPIBQ==", "dev": true, "requires": { "commander": "^2.20.0", @@ -3995,12 +3971,13 @@ "dev": true }, "ts-node": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-9.0.0.tgz", - "integrity": "sha512-/TqB4SnererCDR/vb4S/QvSZvzQMJN8daAslg7MeaiHvD8rDZsSfXmNeNumyZZzMned72Xoq/isQljYSt8Ynfg==", + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-9.1.1.tgz", + "integrity": "sha512-hPlt7ZACERQGf03M253ytLY3dHbGNGrAq9qIHWUY9XHYl1z7wYngSr3OQ5xmui8o2AaxsONxIzjafLUiWBo1Fg==", "dev": true, "requires": { "arg": "^4.1.0", + "create-require": "^1.1.0", "diff": "^4.0.1", "make-error": "^1.1.1", "source-map-support": "^0.5.17", @@ -4112,9 +4089,9 @@ "dev": true }, "typescript": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.0.5.tgz", - "integrity": "sha512-ywmr/VrTVCmNTJ6iV2LwIrfG1P+lv6luD8sUJs+2eI9NLGigaN+nUQc13iHqisq7bra9lnmUSYqbJvegraBOPQ==", + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.1.3.tgz", + "integrity": "sha512-B3ZIOf1IKeH2ixgHhj6la6xdwR9QrLC5d1VKeCSY4tvkqhF2eqd9O7txNlS0PO3GrBAFIdr3L1ndNwteUbZLYg==", "dev": true }, "ua-parser-js": { diff --git a/package.json b/package.json index 582696341..b3f569802 100644 --- a/package.json +++ b/package.json @@ -28,20 +28,21 @@ "scripts": { "clean": "rimraf dist", "lint": "tslint --project tsconfig.build.json -t codeFrame 'src/**/*.ts' 'test/**/*.ts'", - "build": "ttsc --project tsconfig.build.json && rollup -c rollup.config.js", + "build": "npm run generate-typescript && tsc --project tsconfig.build.json && rollup -c rollup.config.js", "build-ci": "npm run clean && npm run build && npm pack", - "start": "node scripts/setup-playground.js && npm run build && concurrently --kill-others \"ttsc --project tsconfig.build.json --watch\" \"rollup -c rollup.config.js -w\"", - "test": "ttsc --project tsconfig.json && concurrently --kill-others \"ttsc --project tsconfig.json -w\" \"karma start karma.conf.js --browsers Chrome --no-single-run --reporters spec,kjhtml\"", - "test-ci": "ttsc --project tsconfig.json && karma start karma.conf.js --browsers ChromeHeadless --single-run --reporters spec", - "generate-csharp": "ts-node --project tsconfig.build-csharp.json src.compiler/csharp/CSharpTranspiler.ts --project tsconfig.build-csharp.json", + "start": "node scripts/setup-playground.js && npm run build && concurrently --kill-others \"tsc --project tsconfig.build.json --watch\" \"rollup -c rollup.config.js -w\"", + "test": "npm run generate-typescript && tsc --project tsconfig.json && concurrently --kill-others \"tsc --project tsconfig.json -w\" \"karma start karma.conf.js --browsers Chrome --no-single-run --reporters spec,kjhtml\"", + "test-ci": "npm run generate-typescript && tsc --project tsconfig.json && karma start karma.conf.js --browsers ChromeHeadless --single-run --reporters spec", + "generate-typescript": "rimraf src/generated && ts-node --project tsconfig.build-csharp.json src.compiler/typescript/AlphaTabGenerator.ts --project tsconfig.build-csharp.json", + "generate-csharp": "npm run generate-typescript && ts-node --project tsconfig.build-csharp.json src.compiler/csharp/CSharpTranspiler.ts --project tsconfig.build-csharp.json", "build-csharp": "npm run generate-csharp && cd src.csharp && dotnet build -c Release", "build-csharp-ci": "npm run clean && npm run generate-csharp && cd src.csharp && dotnet build -c Release", "test-csharp": "cd src.csharp && dotnet test", "test-csharp-ci": "cd src.csharp && dotnet test" }, "devDependencies": { - "@rollup/plugin-commonjs": "^16.0.0", - "@types/jasmine": "^3.6.1", + "@rollup/plugin-commonjs": "^17.0.0", + "@types/jasmine": "^3.6.2", "concurrently": "^5.3.0", "cors": "^2.8.5", "fs-extra": "^9.0.1", @@ -56,19 +57,19 @@ "lodash": "^4.17.20", "multer": "^1.4.2", "rimraf": "^3.0.2", - "rollup": "^2.33.1", + "rollup": "^2.35.1", "rollup-plugin-copy": "^3.3.0", - "rollup-plugin-dts": "^1.4.13", + "rollup-plugin-dts": "^2.0.1", "rollup-plugin-license": "^2.2.0", "rollup-plugin-serve": "^1.1.0", "rollup-plugin-terser": "^7.0.2", - "terser": "^5.3.8", - "ts-node": "^9.0.0", + "terser": "^5.5.1", + "ts-node": "^9.1.1", "tslint": "^6.1.3", "tslint-config-prettier": "^1.15.0", "tslint-config-standard": "^9.0.0", "ttypescript": "^1.5.12", - "typescript": "^4.0.5" + "typescript": "^4.1.3" }, "files": [ "/dist/alphaTab.js", diff --git a/src.compiler/BuilderHelpers.ts b/src.compiler/BuilderHelpers.ts new file mode 100644 index 000000000..bda0c358d --- /dev/null +++ b/src.compiler/BuilderHelpers.ts @@ -0,0 +1,120 @@ +import { indexOf } from 'lodash'; +import * as ts from 'typescript'; + +export function addNewLines(stmts: ts.Statement[]) { + return stmts.map(stmt => ts.addSyntheticTrailingComment(stmt, ts.SyntaxKind.SingleLineCommentTrivia, '', true)); +} +export function getTypeWithNullableInfo(checker: ts.TypeChecker, node: ts.TypeNode | undefined) { + if(!node) { + return { + isNullable: false, + type: {} as ts.Type + }; + } + + let isNullable = false; + let type: ts.Type | null = null; + if (ts.isUnionTypeNode(node)) { + for (const t of node.types) { + if (t.kind === ts.SyntaxKind.NullKeyword) { + isNullable = true; + } else if (ts.isLiteralTypeNode(t) && t.literal.kind === ts.SyntaxKind.NullKeyword) { + isNullable = true; + } else if (type !== null) { + throw new Error('Multi union types on JSON settings not supported: ' + node.getSourceFile().fileName + ':' + node.getText()); + } else { + type = checker.getTypeAtLocation(t); + } + } + } else { + type = checker.getTypeAtLocation(node); + } + + return { + isNullable, + type: type as ts.Type + }; +} + +export function unwrapArrayItemType(type: ts.Type, typeChecker: ts.TypeChecker): ts.Type | null { + if (type.symbol && type.symbol.name === 'Array') { + return (type as ts.TypeReference).typeArguments![0]; + } + + if (isPrimitiveType(type)) { + return null; + } + + if (type.isUnion()) { + const nonNullable = typeChecker.getNonNullableType(type); + return unwrapArrayItemType(nonNullable, typeChecker); + } + + return null; +} + + +export function isPrimitiveType(type: ts.Type | null) { + if (!type) { + return false; + } + + if (hasFlag(type, ts.TypeFlags.Number)) { + return true; + } + if (hasFlag(type, ts.TypeFlags.String)) { + return true; + } + if (hasFlag(type, ts.TypeFlags.Boolean)) { + return true; + } + if (hasFlag(type, ts.TypeFlags.BigInt)) { + return true; + } + if (hasFlag(type, ts.TypeFlags.Unknown)) { + return true; + } + + return isEnumType(type); +} + +export function isNumberType(type: ts.Type | null) { + if (!type) { + return false; + } + + if (hasFlag(type, ts.TypeFlags.Number)) { + return true; + } + + return false; +} + +export function isEnumType(type: ts.Type) { + // if for some reason this returns true... + if (hasFlag(type, ts.TypeFlags.Enum)) return true; + // it's not an enum type if it's an enum literal type + if (hasFlag(type, ts.TypeFlags.EnumLiteral) && !type.isUnion()) return false; + // get the symbol and check if its value declaration is an enum declaration + const symbol = type.getSymbol(); + if (!symbol) return false; + const { valueDeclaration } = symbol; + + return valueDeclaration && valueDeclaration.kind === ts.SyntaxKind.EnumDeclaration; +} + +export function wrapToNonNull(isNullableType: boolean, expr: ts.Expression, factory: ts.NodeFactory) { + return isNullableType ? expr : factory.createNonNullExpression(expr); +} + +export function isTypedArray(type: ts.Type) { + return !!type.symbol?.members?.has(ts.escapeLeadingUnderscores('slice')); +} + +export function hasFlag(type: ts.Type, flag: ts.TypeFlags): boolean { + return (type.flags & flag) === flag; +} + +export function isMap(type: ts.Type | null): boolean { + return !!(type && type.symbol?.name === 'Map'); +} \ No newline at end of file diff --git a/src.compiler/JsonSerializationBuilder.ts b/src.compiler/JsonSerializationBuilder.ts deleted file mode 100644 index f2e4e86d9..000000000 --- a/src.compiler/JsonSerializationBuilder.ts +++ /dev/null @@ -1,942 +0,0 @@ -import * as ts from 'typescript'; -interface JsonProperty { - property: ts.PropertyDeclaration; - jsonNames: string[]; -} - -function wrapToNonNull(isNullableType: boolean, expr: ts.Expression) { - return isNullableType ? expr : ts.createNonNullExpression(expr); -} - -function getTypeWithNullableInfo(checker: ts.TypeChecker, node: ts.TypeNode) { - let isNullable = false; - let type: ts.Type | null = null; - if (ts.isUnionTypeNode(node)) { - for (const t of node.types) { - if (t.kind === ts.SyntaxKind.NullKeyword) { - isNullable = true; - } else if (ts.isLiteralTypeNode(t) && t.literal.kind === ts.SyntaxKind.NullKeyword) { - isNullable = true; - } else if (type !== null) { - throw new Error('Multi union types on JSON settings not supported: ' + node.getSourceFile().fileName + ':' + node.getText()); - } else { - type = checker.getTypeAtLocation(t); - } - } - } else { - type = checker.getTypeAtLocation(node); - } - - return { - isNullable, - type - }; -} - -function isPrimitiveType(type: ts.Type) { - if (hasFlag(type, ts.TypeFlags.Number)) { - return true; - } - if (hasFlag(type, ts.TypeFlags.String)) { - return true; - } - if (hasFlag(type, ts.TypeFlags.Boolean)) { - return true; - } - if (hasFlag(type, ts.TypeFlags.BigInt)) { - return true; - } - if (hasFlag(type, ts.TypeFlags.Unknown)) { - return true; - } - - return isEnumType(type); -} - -function isEnumType(type: ts.Type) { - // if for some reason this returns true... - if (hasFlag(type, ts.TypeFlags.Enum)) return true; - // it's not an enum type if it's an enum literal type - if (hasFlag(type, ts.TypeFlags.EnumLiteral) && !type.isUnion()) return false; - // get the symbol and check if its value declaration is an enum declaration - const symbol = type.getSymbol(); - if (!symbol) return false; - const { valueDeclaration } = symbol; - - return valueDeclaration && valueDeclaration.kind === ts.SyntaxKind.EnumDeclaration; -} - -function isTypedArray(type: ts.Type) { - return type.symbol.members.has(ts.escapeLeadingUnderscores('slice')); -} - -function hasFlag(type: ts.Type, flag: ts.TypeFlags): boolean { - return (type.flags & flag) === flag; -} - -function isImmutable(type: ts.Type): boolean { - const declaration = type.symbol.valueDeclaration; - if (declaration) { - return !!ts.getJSDocTags(declaration).find(t => t.tagName.text === 'json_immutable'); - } - - return false; -} - -function isMap(type: ts.Type): boolean { - return type.symbol.name === 'Map'; -} - -function generateToJsonBodyForClass( - classDeclaration: ts.ClassDeclaration, - propertiesToSerialize: JsonProperty[] -): ts.Block { - return ts.createBlock([ - // const json:any = {}; - ts.createVariableStatement( - [ts.createModifier(ts.SyntaxKind.ConstKeyword)], - [ - ts.createVariableDeclaration( - 'json', - ts.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword), - ts.createObjectLiteral() - ) - ] - ), - - // obj.fillToJson(json) - ts.createExpressionStatement( - ts.createCall( - ts.createPropertyAccess(ts.createIdentifier('obj'), 'fillToJson'), - [], - [ts.createIdentifier('json')] - ) - ), - - // return json; - ts.createReturn(ts.createIdentifier('json')) - ]); -} -function generateFillToJsonBodyForClass( - program: ts.Program, - classDeclaration: ts.ClassDeclaration, - propertiesToSerialize: JsonProperty[] -): ts.Block { - const statements: ts.Statement[] = []; - - for (let prop of propertiesToSerialize) { - const fieldName = (prop.property.name as ts.Identifier).text; - const jsonName = prop.jsonNames.filter(n => n !== '')[0]; - - const accessJsonName = function (): ts.Expression { - return ts.createPropertyAccess(ts.createIdentifier('json'), jsonName); - }; - const accessField = function (): ts.Expression { - return ts.createPropertyAccess(ts.createThis(), ts.createIdentifier(fieldName)); - }; - - const assignToJsonName = function (value: ts.Expression): ts.Statement { - return ts.createExpressionStatement(ts.createAssignment(accessJsonName(), value)); - }; - - if (jsonName) { - const type = getTypeWithNullableInfo(program.getTypeChecker(), prop.property.type); - if (isPrimitiveType(type.type)) { - // json.jsonName = this.fieldName - statements.push(assignToJsonName(accessField())); - } else if (isTypedArray(type.type)) { - // json.jsonName = this.fieldName ? this.fieldName.slice() : null - if (type.isNullable) { - statements.push( - assignToJsonName( - ts.createConditional( - accessField(), - ts.createToken(ts.SyntaxKind.QuestionToken), - ts.createCall(ts.createPropertyAccess(accessField(), 'slice'), [], []), - ts.createToken(ts.SyntaxKind.ColonToken), - ts.createNull() - ) - ) - ); - } else { - statements.push( - assignToJsonName(ts.createCall(ts.createPropertyAccess(accessField(), 'slice'), [], [])) - ); - } - } else if (isMap(type.type)) { - const mapType = type.type as ts.TypeReference; - if (!isEnumType(mapType.typeArguments[0]) || !isPrimitiveType(mapType.typeArguments[1])) { - throw new Error('only Map maps are supported extend if needed!'); - } - // json.jsonName = { } as any; - // this.fieldName.forEach((val, key) => (json.jsonName as any)[key] = val)) - statements.push( - assignToJsonName( - ts.createAsExpression( - ts.createObjectLiteral(), - ts.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword) - ) - ) - ); - statements.push( - ts.createExpressionStatement( - ts.createCall(ts.createPropertyAccess(accessField(), 'forEach'), undefined, [ - ts.createArrowFunction( - undefined, - undefined, - [ - ts.createParameter(undefined, undefined, undefined, '$mv'), - ts.createParameter(undefined, undefined, undefined, '$mk') - ], - undefined, - ts.createToken(ts.SyntaxKind.EqualsGreaterThanToken), - ts.createBlock([ - ts.createExpressionStatement( - ts.createAssignment( - ts.createElementAccess( - ts.createAsExpression( - accessJsonName(), - ts.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword) - ), - ts.createIdentifier('$mk') - ), - ts.createIdentifier('$mv') - ) - ) - ]) - ) - ]) - ) - ); - } else if (isImmutable(type.type)) { - // json.jsonName = TypeName.toJson(this.fieldName); - statements.push( - assignToJsonName( - wrapToNonNull( - type.isNullable, - ts.createCall( - ts.createPropertyAccess(ts.createIdentifier(type.type.symbol.name), 'toJson'), - [], - [accessField()] - ) - ) - ) - ); - } else { - // not nullable: - // if(json.jsonName) { - // this.fieldName.fillToJson(json.jsonName) - // } else { - // json.jsonName = TypeName.toJson(this.fieldName)!; - // } - - // nullable: - // if(json.jsonName) { - // if(this.fieldName) this.fieldName.fillToJson(json.jsonName) - // } else { - // json.jsonName = TypeName.toJson(this.fieldName); - // } - - // this.field.fillToJson(json.jsonName) - let fillToJsonStatent: ts.Statement = ts.createExpressionStatement( - ts.createCall( - // this.field.fillToJson - ts.createPropertyAccess(accessField(), 'fillToJson'), - [], - [accessJsonName()] - ) - ); - if (type.isNullable) { - fillToJsonStatent = ts.createIf(accessField(), fillToJsonStatent); - } - - statements.push( - ts.createIf( - // if(json.jsonName) - accessJsonName(), - // this.field.fillToJson(json.jsonName) - fillToJsonStatent, - // else json.jsonName = ... - assignToJsonName( - wrapToNonNull( - type.isNullable, - ts.createCall( - // TypeName.toJson - ts.createPropertyAccess(ts.createIdentifier(type.type.symbol.name), 'toJson'), - [], - [accessField()] - ) - ) - ) - ) - ); - } - } - } - - return ts.createBlock(statements); -} -function generateFromJsonBodyForClass( - classDeclaration: ts.ClassDeclaration, - propertiesToSerialize: JsonProperty[] -): ts.Block { - const statements: ts.Statement[] = []; - // if(!json) return null; - statements.push( - ts.createIf( - ts.createPrefix(ts.SyntaxKind.ExclamationToken, ts.createIdentifier('json')), - ts.createReturn(ts.createNull()) - ) - ); - - // const obj = new Type(); - statements.push( - ts.createVariableStatement( - [ts.createModifier(ts.SyntaxKind.ConstKeyword)], - [ - ts.createVariableDeclaration( - 'obj', - undefined, - ts.createNew(ts.createIdentifier(classDeclaration.name.text), [], []) - ) - ] - ) - ); - - // obj.fillFromJson(json); - statements.push( - ts.createExpressionStatement( - ts.createCall( - ts.createPropertyAccess(ts.createIdentifier('obj'), 'fillFromJson'), - [], - [ts.createIdentifier('json')] - ) - ) - ); - - // return obj; - statements.push( - // return json; - ts.createReturn(ts.createIdentifier('obj')) - ); - - return ts.createBlock(statements); -} -function generateFillFromJsonBodyForClass( - classDeclaration: ts.ClassDeclaration, - propertiesToSerialize: JsonProperty[] -): ts.Block { - return ts.createBlock([ - ts.createIf( - // if(json) for($k in json) { this.setProperty($k.toLowerCase(), json[$k]) } - ts.createIdentifier('json'), - ts.createForIn( - ts.createVariableDeclarationList([ts.createVariableDeclaration('$k')], ts.NodeFlags.Const), - ts.createIdentifier('json'), - ts.createExpressionStatement( - ts.createCall( - ts.createPropertyAccess(ts.createThis(), 'setProperty'), - [], - [ - // $k.toLowerCase(), - ts.createCall(ts.createPropertyAccess(ts.createIdentifier('$k'), 'toLowerCase'), [], []), - // json[$k] - ts.createElementAccess(ts.createIdentifier('json'), ts.createIdentifier('$k')) - ] - ) - ) - ) - ) - ]); -} - -function createEnumMapping(value: string, type: ts.Type): ts.Expression { - // isNan(parseInt(value)) ? Enum[Object.keys(Enum).find($k => $k.toLowerCase() === value.toLowerCase()] : parseInt(value) - return ts.createConditional( - ts.createCall(ts.createIdentifier('isNaN'), undefined, [ - ts.createCall(ts.createIdentifier('parseInt'), undefined, [ts.createIdentifier(value)]) - ]), - ts.createToken(ts.SyntaxKind.QuestionToken), - ts.createElementAccess( - ts.createIdentifier(type.symbol.name), - ts.createCall( - // Object.keys(EnumName).find - ts.createPropertyAccess( - // Object.keys(EnumName) - ts.createCall( - ts.createPropertyAccess(ts.createIdentifier('Object'), 'keys'), - [], - [ts.createIdentifier(type.symbol.name)] - ), - 'find' - ), - [], - [ - ts.createArrowFunction( - [], - [], - [ts.createParameter(undefined, undefined, undefined, '$k')], - undefined, - ts.createToken(ts.SyntaxKind.EqualsGreaterThanToken), - ts.createBinary( - ts.createCall( - // $.toLowerCase() - ts.createPropertyAccess(ts.createIdentifier('$k'), 'toLowerCase'), - [], - [] - ), - // === - ts.createToken(ts.SyntaxKind.EqualsEqualsEqualsToken), - // value.toLowerCase() - ts.createCall( - // $.toLowerCase() - ts.createPropertyAccess(ts.createIdentifier(value), 'toLowerCase'), - [], - [] - ) - ) - ) - ] - ) - ), - ts.createToken(ts.SyntaxKind.ColonToken), - ts.createCall(ts.createIdentifier('parseInt'), undefined, [ts.createIdentifier(value)]) - ); -} - -function generateSetPropertyMethodBodyForClass( - program: ts.Program, - classDeclaration: ts.ClassDeclaration, - propertiesToSerialize: JsonProperty[] -): ts.Block { - const statements: ts.Statement[] = []; - const cases: ts.CaseOrDefaultClause[] = []; - - const typeChecker = program.getTypeChecker(); - for (const prop of propertiesToSerialize) { - const jsonNames = prop.jsonNames.map(j => j.toLowerCase()); - const caseValues: string[] = jsonNames.filter(j => j !== ''); - const fieldName = (prop.property.name as ts.Identifier).text; - - const caseStatements: ts.Statement[] = []; - - const type = getTypeWithNullableInfo(typeChecker, prop.property.type); - - const assignField = function (expr: ts.Expression): ts.Statement { - return ts.createExpressionStatement( - ts.createAssignment(ts.createPropertyAccess(ts.createThis(), fieldName), expr) - ); - }; - - if (isEnumType(type.type)) { - // this.fieldName = enummapping - // return true; - caseStatements.push(assignField(createEnumMapping('value', type.type))); - caseStatements.push(ts.createReturn(ts.createTrue())); - } else if (isPrimitiveType(type.type)) { - // this.fieldName = value - // return true; - caseStatements.push(assignField(ts.createIdentifier('value'))); - caseStatements.push(ts.createReturn(ts.createTrue())); - } else if (isTypedArray(type.type)) { - // nullable: - // this.fieldName = value ? value.slice() : null - // return true; - - // not nullable: - // this.fieldName = value.slice() - // return true; - - if (type.isNullable) { - caseStatements.push( - assignField( - ts.createConditional( - ts.createIdentifier('value'), - ts.createToken(ts.SyntaxKind.QuestionToken), - ts.createCall(ts.createPropertyAccess(ts.createIdentifier('value'), 'slice'), [], []), - ts.createToken(ts.SyntaxKind.ColonToken), - ts.createNull() - ) - ) - ); - } else { - caseStatements.push( - assignField(ts.createCall(ts.createPropertyAccess(ts.createIdentifier('value'), 'slice'), [], [])) - ); - } - - caseStatements.push(ts.createReturn(ts.createTrue())); - } else if (isMap(type.type)) { - // this.fieldName = new Map(); - // for(let key in value) { - // if(value.hasOwnProperty(key) this.fieldName.set(, value[key]); - // } - // return true; - - const mapType = type.type as ts.TypeReference; - if (!isEnumType(mapType.typeArguments[0]) || !isPrimitiveType(mapType.typeArguments[1])) { - throw new Error('only Map maps are supported extend if needed!'); - } - - caseStatements.push(assignField(ts.createNew(ts.createIdentifier('Map'), undefined, []))); - caseStatements.push( - ts.createForIn( - ts.createVariableDeclarationList( - [ts.createVariableDeclaration(ts.createIdentifier('$mk'), undefined, undefined)], - ts.NodeFlags.Let - ), - ts.createIdentifier('value'), - ts.createIf( - ts.createCall( - ts.createPropertyAccess(ts.createIdentifier('value'), 'hasOwnProperty'), - undefined, - [ts.createIdentifier('$mk')] - ), - ts.createExpressionStatement( - ts.createCall( - ts.createPropertyAccess( - ts.createPropertyAccess(ts.createThis(), ts.createIdentifier(fieldName)), - ts.createIdentifier('set') - ), - undefined, - [ - createEnumMapping('$mk', mapType.typeArguments![0]), - ts.createElementAccess(ts.createIdentifier('value'), ts.createIdentifier('$mk')) - ] - ) - ) - ) - ) - ); - caseStatements.push(ts.createReturn(ts.createTrue())); - } else if (isImmutable(type.type)) { - // this.fieldName = TypeName.fromJson(value)! - // return true; - caseStatements.push( - assignField( - wrapToNonNull( - type.isNullable, - ts.createCall( - // TypeName.fromJson - ts.createPropertyAccess(ts.createIdentifier(type.type.symbol.name), 'fromJson'), - [], - [ts.createIdentifier('value')] - ) - ) - ) - ); - caseStatements.push(ts.createReturn(ts.createTrue())); - } else { - // for complex types it is a bit more tricky - // if the property matches exactly, we use fromJson - // if the property starts with the field name, we try to set a sub-property - const jsonNameArray = ts.createArrayLiteral(jsonNames.map(n => ts.createStringLiteral(n))); - - statements.push( - ts.createIf( - // if(["", "core"].indexOf(property) >= 0) - ts.createBinary( - ts.createCall( - ts.createPropertyAccess(jsonNameArray, 'indexOf'), - [], - [ts.createIdentifier('property')] - ), - ts.SyntaxKind.GreaterThanEqualsToken, - ts.createNumericLiteral('0') - ), - ts.createBlock([ - // if(this.field) { - // this.field.fillFromJson(value); - // } else { - // this.field = TypeName.fromJson(value); - // } - // return true; - ts.createIf( - ts.createPropertyAccess(ts.createThis(), fieldName), - ts.createExpressionStatement( - ts.createCall( - ts.createPropertyAccess( - ts.createPropertyAccess(ts.createThis(), fieldName), - 'fillFromJson' - ), - [], - [ts.createIdentifier('value')] - ) - ), - assignField( - wrapToNonNull( - type.isNullable, - ts.createCall( - // TypeName.fromJson - ts.createPropertyAccess(ts.createIdentifier(type.type.symbol.name), 'fromJson'), - [], - [ts.createIdentifier('value')] - ) - ) - ) - ), - ts.createReturn(ts.createTrue()) - ]), - ts.createBlock([ - // for(const candidate of ["", "core"]) { - // if(candidate.indexOf(property) === 0) { - // if(!this.field) { this.field = new FieldType(); } - // if(this.field.setProperty(property.substring(candidate.length), value)) return true; - // } - // } - ts.createForOf( - undefined, - ts.createVariableDeclarationList([ts.createVariableDeclaration('$c')], ts.NodeFlags.Const), - jsonNameArray, - ts.createIf( - ts.createBinary( - ts.createCall( - ts.createPropertyAccess(ts.createIdentifier('property'), 'indexOf'), - [], - [ts.createIdentifier('$c')] - ), - ts.SyntaxKind.EqualsEqualsEqualsToken, - ts.createNumericLiteral('0') - ), - ts.createBlock([ - ts.createIf( - ts.createPrefix( - ts.SyntaxKind.ExclamationToken, - ts.createPropertyAccess(ts.createThis(), fieldName) - ), - assignField(ts.createNew(ts.createIdentifier(type.type.symbol.name), [], [])) - ), - ts.createIf( - ts.createCall( - ts.createPropertyAccess( - ts.createPropertyAccess(ts.createThis(), fieldName), - 'setProperty' - ), - [], - [ - ts.createCall( - ts.createPropertyAccess( - ts.createIdentifier('property'), - 'substring' - ), - [], - [ts.createPropertyAccess(ts.createIdentifier('$c'), 'length')] - ), - ts.createIdentifier('value') - ] - ), - ts.createReturn(ts.createTrue()) - ) - ]) - ) - ) - ]) - ) - ); - } - - if (caseStatements.length > 0) { - for (let i = 0; i < caseValues.length; i++) { - cases.push( - ts.createCaseClause( - ts.createStringLiteral(caseValues[i]), - // last case gets the statements, others are fall through - i < caseValues.length - 1 ? [] : caseStatements - ) - ); - } - } - } - - const switchExpr = ts.createSwitch(ts.createIdentifier('property'), ts.createCaseBlock(cases)); - statements.unshift(switchExpr); - statements.push(ts.createReturn(ts.createFalse())); - - return ts.createBlock(statements); -} - -function rewriteClassForJsonSerialization( - program: ts.Program, - classDeclaration: ts.ClassDeclaration, - sourceFile: ts.SourceFile -): ts.ClassDeclaration { - console.debug(`Rewriting ${classDeclaration.name.escapedText} for JSON serialization`); - let toJsonMethod: ts.MethodDeclaration = undefined; - let fromJsonMethod: ts.MethodDeclaration = undefined; - let fillToJsonMethod: ts.MethodDeclaration = undefined; - let fillFromJsonMethod: ts.MethodDeclaration = undefined; - let setPropertyMethod: ts.MethodDeclaration = undefined; - - let propertiesToSerialize: JsonProperty[] = []; - - var newMembers = []; - - // collect class state - classDeclaration.members.forEach(member => { - if (ts.isPropertyDeclaration(member)) { - const propertyDeclaration = member as ts.PropertyDeclaration; - if (!propertyDeclaration.modifiers.find(m => m.kind === ts.SyntaxKind.StaticKeyword)) { - const jsonNames = [member.name.getText(sourceFile)]; - - if (ts.getJSDocTags(member).find(t => t.tagName.text === 'json_on_parent')) { - jsonNames.push(''); - } - - propertiesToSerialize.push({ - property: propertyDeclaration, - jsonNames: jsonNames - }); - } - newMembers.push(member); - } else if (ts.isMethodDeclaration(member)) { - if (ts.isIdentifier(member.name)) { - const methodName = (member.name as ts.Identifier).escapedText; - switch (methodName) { - case 'toJson': - toJsonMethod = member; - break; - case 'fromJson': - fromJsonMethod = member; - break; - case 'fillToJson': - fillToJsonMethod = member; - break; - case 'fillFromJson': - fillFromJsonMethod = member; - break; - case 'setProperty': - setPropertyMethod = member; - break; - default: - newMembers.push(member); - break; - } - } - } else { - newMembers.push(member); - } - }); - - if (!toJsonMethod) { - toJsonMethod = ts.createMethod( - undefined, - [ts.createModifier(ts.SyntaxKind.PublicKeyword), ts.createModifier(ts.SyntaxKind.StaticKeyword)], - undefined, - 'toJson', - undefined, - undefined, - [ - ts.createParameter( - undefined, - undefined, - undefined, - 'obj', - undefined, - ts.createTypeReferenceNode(classDeclaration.name, []) - ) - ], - ts.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword), - undefined - ); - } - - if (!fillToJsonMethod) { - fillToJsonMethod = ts.createMethod( - undefined, - [ts.createModifier(ts.SyntaxKind.PublicKeyword)], - undefined, - 'fillToJson', - undefined, - undefined, - [ - ts.createParameter( - undefined, - undefined, - undefined, - 'json', - undefined, - ts.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword) - ) - ], - ts.createKeywordTypeNode(ts.SyntaxKind.VoidKeyword), - ts.createBlock([ts.createThrow(ts.createStringLiteral('todo'))]) - ); - } - - if (!fromJsonMethod) { - fromJsonMethod = ts.createMethod( - undefined, - [ts.createModifier(ts.SyntaxKind.PublicKeyword), ts.createModifier(ts.SyntaxKind.StaticKeyword)], - undefined, - 'fromJson', - undefined, - undefined, - [ - ts.createParameter( - undefined, - undefined, - undefined, - 'json', - undefined, - ts.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword) - ) - ], - ts.createTypeReferenceNode(classDeclaration.name, []), - ts.createBlock([ts.createThrow(ts.createStringLiteral('todo'))]) - ); - } - - if (!fillFromJsonMethod) { - fillFromJsonMethod = ts.createMethod( - undefined, - [ts.createModifier(ts.SyntaxKind.PublicKeyword)], - undefined, - 'fillFromJson', - undefined, - undefined, - [ - ts.createParameter( - undefined, - undefined, - undefined, - 'json', - undefined, - ts.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword) - ) - ], - ts.createKeywordTypeNode(ts.SyntaxKind.VoidKeyword), - ts.createBlock([ts.createThrow(ts.createStringLiteral('todo'))]) - ); - } - - if (!setPropertyMethod) { - setPropertyMethod = ts.createMethod( - undefined, - [ts.createModifier(ts.SyntaxKind.PublicKeyword)], - undefined, - 'setProperty', - undefined, - undefined, - [ - ts.createParameter( - undefined, - undefined, - undefined, - 'property', - undefined, - ts.createKeywordTypeNode(ts.SyntaxKind.StringKeyword) - ), - ts.createParameter( - undefined, - undefined, - undefined, - 'value', - undefined, - ts.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword) - ) - ], - ts.createKeywordTypeNode(ts.SyntaxKind.BooleanKeyword), - ts.createBlock([ts.createThrow(ts.createStringLiteral('todo'))]) - ); - } - - function updateMethodBody( - method: ts.MethodDeclaration, - paramNames: string[], - body: ts.Block - ): ts.MethodDeclaration { - if (!method) { - return; - } - const parameters = method.parameters.map((v, i) => - ts.createParameter( - method.parameters[i].decorators, - method.parameters[i].modifiers, - method.parameters[i].dotDotDotToken, - i < paramNames.length ? paramNames[i] : method.parameters[0].name, - method.parameters[i].questionToken, - method.parameters[i].type, - method.parameters[i].initializer - ) - ); - - return ts.updateMethod( - method, - method.decorators, - method.modifiers, - method.asteriskToken, - method.name, - method.questionToken, - method.typeParameters, - parameters, - method.type, - body - ); - } - - toJsonMethod = updateMethodBody( - toJsonMethod, - ['obj'], - generateToJsonBodyForClass(classDeclaration, propertiesToSerialize) - ); - - fillToJsonMethod = updateMethodBody( - fillToJsonMethod, - ['json'], - generateFillToJsonBodyForClass(program, classDeclaration, propertiesToSerialize) - ); - - fromJsonMethod = updateMethodBody( - fromJsonMethod, - ['json'], - generateFromJsonBodyForClass(classDeclaration, propertiesToSerialize) - ); - - fillFromJsonMethod = updateMethodBody( - fillFromJsonMethod, - ['json'], - generateFillFromJsonBodyForClass(classDeclaration, propertiesToSerialize) - ); - - setPropertyMethod = updateMethodBody( - setPropertyMethod, - ['property', 'value'], - generateSetPropertyMethodBodyForClass(program, classDeclaration, propertiesToSerialize) - ); - - newMembers.push(toJsonMethod); - newMembers.push(fillToJsonMethod); - newMembers.push(fromJsonMethod); - newMembers.push(fillFromJsonMethod); - newMembers.push(setPropertyMethod); - - console.debug(`Rewriting ${classDeclaration.name.escapedText} done`); - - return ts.updateClassDeclaration( - classDeclaration, - classDeclaration.decorators, - classDeclaration.modifiers, - classDeclaration.name, - classDeclaration.typeParameters, - classDeclaration.heritageClauses, - newMembers - ); -} - -export default function (program: ts.Program) { - return (ctx: ts.TransformationContext) => { - return (sourceFile: ts.SourceFile) => { - function visitor(node: ts.Node): ts.Node { - if (ts.isClassDeclaration(node)) { - if (ts.getJSDocTags(node).find(t => t.tagName.text === 'json')) { - return rewriteClassForJsonSerialization(program, node, sourceFile); - } - } - - return node; - } - - return ts.visitEachChild(sourceFile, visitor, ctx); - }; - }; -} diff --git a/src.compiler/TranspilerBase.ts b/src.compiler/TranspilerBase.ts new file mode 100644 index 000000000..453b52709 --- /dev/null +++ b/src.compiler/TranspilerBase.ts @@ -0,0 +1,102 @@ +import * as ts from 'typescript'; + +function createDiagnosticReporter(pretty?: boolean): ts.DiagnosticReporter { + const host: ts.FormatDiagnosticsHost = { + getCurrentDirectory: () => ts.sys.getCurrentDirectory(), + getNewLine: () => ts.sys.newLine, + getCanonicalFileName: ts.sys.useCaseSensitiveFileNames + ? x => x + : x => x.toLowerCase(), + }; + + if (!pretty) { + return diagnostic => ts.sys.write(ts.formatDiagnostic(diagnostic, host)); + } + + return diagnostic => { + ts.sys.write(ts.formatDiagnosticsWithColorAndContext([diagnostic], host) + host.getNewLine()); + }; +} + +interface Emitter { + name: string, + emit(program: ts.Program, diagnostics: ts.Diagnostic[]): void; +} + +export default function (emitters: Emitter[], handleErrors: boolean = false) { + console.log('Parsing...'); + const commandLine = ts.parseCommandLine(ts.sys.args); + if (!ts.sys.fileExists(commandLine.options.project!)) { + ts.sys.exit(ts.ExitStatus.InvalidProject_OutputsSkipped); + } + + let reportDiagnostic = createDiagnosticReporter(); + + const parseConfigFileHost: ts.ParseConfigFileHost = ts.sys; + parseConfigFileHost.onUnRecoverableConfigFileDiagnostic = diagnostic => { + reportDiagnostic(diagnostic); + ts.sys.exit(ts.ExitStatus.InvalidProject_OutputsSkipped); + }; + + const parsedCommandLine = ts.getParsedCommandLineOfConfigFile(commandLine.options.project!, commandLine.options, parseConfigFileHost, /*extendedConfigCache*/ undefined, commandLine.watchOptions)!; + const pretty = !!ts.sys.writeOutputIsTTY && ts.sys.writeOutputIsTTY(); + if (pretty) { + reportDiagnostic = createDiagnosticReporter(true); + } + + const program = ts.createProgram({ + rootNames: parsedCommandLine.fileNames, + options: parsedCommandLine.options, + projectReferences: parsedCommandLine.projectReferences, + host: ts.createCompilerHost(parsedCommandLine.options), + }); + + const allDiagnostics = program.getConfigFileParsingDiagnostics().slice(); + const configFileParsingDiagnosticsLength = allDiagnostics.length; + allDiagnostics.push(...program.getSyntacticDiagnostics()); + + if (allDiagnostics.length === configFileParsingDiagnosticsLength) { + allDiagnostics.push(...program.getOptionsDiagnostics()); + allDiagnostics.push(...program.getGlobalDiagnostics()); + allDiagnostics.push(...program.getSemanticDiagnostics()); + } + + program.getTypeChecker(); + + for(const emitter of emitters) { + console.log(`[${emitter.name}] Emitting...`); + emitter.emit(program, allDiagnostics); + } + + if (handleErrors) { + let diagnostics = ts.sortAndDeduplicateDiagnostics(allDiagnostics); + let errorCount = 0; + let warningCount = 0; + diagnostics.forEach(d => { + switch (d.category) { + case ts.DiagnosticCategory.Error: errorCount++; break; + case ts.DiagnosticCategory.Warning: warningCount++; break; + } + reportDiagnostic(d); + }); + + if (pretty) { + reportDiagnostic({ + file: undefined, + start: undefined, + length: undefined, + code: 6194, + messageText: `Compilation completed with ${errorCount} errors and ${warningCount} warnings${ts.sys.newLine}`, + category: errorCount > 0 ? ts.DiagnosticCategory.Error : warningCount > 0 ? ts.DiagnosticCategory.Warning : ts.DiagnosticCategory.Message, + }); + } + + if (errorCount > 0) { + ts.sys.exit(ts.ExitStatus.DiagnosticsPresent_OutputsGenerated); + } else { + ts.sys.exit(ts.ExitStatus.Success); + } + } + + console.log('Done'); +} \ No newline at end of file diff --git a/src.compiler/csharp/CSharpAst.ts b/src.compiler/csharp/CSharpAst.ts index 7a2ab3c25..e3719d2e4 100644 --- a/src.compiler/csharp/CSharpAst.ts +++ b/src.compiler/csharp/CSharpAst.ts @@ -75,6 +75,7 @@ export enum SyntaxKind { Identifier, DefaultExpression, ToDoExpression, + TypeOfExpression, Attribute } @@ -380,6 +381,10 @@ export interface NonNullExpression extends Node { expression: Expression; } +export interface TypeOfExpression extends Node { + expression: Expression; +} + export interface NullSafeExpression extends Node { expression: Expression; } diff --git a/src.compiler/csharp/CSharpAstPrinter.ts b/src.compiler/csharp/CSharpAstPrinter.ts index 54b58c598..bee2fed26 100644 --- a/src.compiler/csharp/CSharpAstPrinter.ts +++ b/src.compiler/csharp/CSharpAstPrinter.ts @@ -273,12 +273,12 @@ export default class CSharpAstPrinter { defaultConstructor.parameters = constructorDeclaration.parameters; defaultConstructor.baseConstructorArguments = constructorDeclaration.parameters.map( p => - ({ - parent: defaultConstructor, - nodeType: cs.SyntaxKind.Identifier, - text: p.name, - tsNode: defaultConstructor.tsNode - } as cs.Identifier) + ({ + parent: defaultConstructor, + nodeType: cs.SyntaxKind.Identifier, + text: p.name, + tsNode: defaultConstructor.tsNode + } as cs.Identifier) ); this.writeMember(defaultConstructor); } @@ -413,7 +413,7 @@ export default class CSharpAstPrinter { this.write('where '); this.write(p.name); this.write(' : '); - this.writeType(p.constraint); + this.writeType(p.constraint, false, false, true); } }); this._indent--; @@ -555,35 +555,52 @@ export default class CSharpAstPrinter { this.writeLine(';'); } - private writeType(type: cs.TypeNode, forNew: boolean = false, asNativeArray: boolean = false) { + private writeType(type: cs.TypeNode, forNew: boolean = false, asNativeArray: boolean = false, forTypeConstraint: boolean = false) { if (!type) { console.log('ERR'); } switch (type.nodeType) { case cs.SyntaxKind.PrimitiveTypeNode: - switch ((type as cs.PrimitiveTypeNode).type) { - case cs.PrimitiveType.Bool: - this.write('bool'); - break; - case cs.PrimitiveType.Dynamic: - this.write('dynamic'); - break; - case cs.PrimitiveType.Double: - this.write('double'); - break; - case cs.PrimitiveType.Int: - this.write('int'); - break; - case cs.PrimitiveType.Object: - this.write('object'); - break; - case cs.PrimitiveType.String: - this.write('string'); - break; - case cs.PrimitiveType.Void: - this.write('void'); - break; + if (forTypeConstraint) { + switch ((type as cs.PrimitiveTypeNode).type) { + case cs.PrimitiveType.Bool: + case cs.PrimitiveType.Int: + case cs.PrimitiveType.Double: + this.write('struct'); + break; + case cs.PrimitiveType.Object: + case cs.PrimitiveType.Dynamic: + case cs.PrimitiveType.String: + case cs.PrimitiveType.Void: + this.write('class'); + break; + } + } else { + switch ((type as cs.PrimitiveTypeNode).type) { + case cs.PrimitiveType.Bool: + this.write('bool'); + break; + case cs.PrimitiveType.Dynamic: + this.write('dynamic'); + break; + case cs.PrimitiveType.Double: + this.write('double'); + break; + case cs.PrimitiveType.Int: + this.write('int'); + break; + case cs.PrimitiveType.Object: + this.write('object'); + break; + case cs.PrimitiveType.String: + this.write('string'); + break; + case cs.PrimitiveType.Void: + this.write('void'); + break; + } } + break; case cs.SyntaxKind.ArrayTypeNode: const arrayType = type as cs.ArrayTypeNode; @@ -591,13 +608,20 @@ export default class CSharpAstPrinter { this.writeType(arrayType.elementType); this.write('[]'); } else { - if (forNew) { - this.write('System.Collections.Generic.List<'); + const isDynamicArray = arrayType.elementType.nodeType == cs.SyntaxKind.PrimitiveTypeNode + && (arrayType.elementType as cs.PrimitiveTypeNode).type == cs.PrimitiveType.Dynamic; + if (isDynamicArray && !forNew) { + this.write('System.Collections.IList'); } else { - this.write('System.Collections.Generic.IList<'); + if (forNew) { + this.write('System.Collections.Generic.List<'); + } else { + this.write('System.Collections.Generic.IList<'); + } + this.writeType(arrayType.elementType); + this.write('>'); } - this.writeType(arrayType.elementType); - this.write('>'); + } break; @@ -632,7 +656,7 @@ export default class CSharpAstPrinter { this.write('TODO: ' + cs.SyntaxKind[type.nodeType]); break; } - if (type.isNullable && !forNew) { + if (type.isNullable && !forNew && !forTypeConstraint) { this.write('?'); } } @@ -741,6 +765,9 @@ export default class CSharpAstPrinter { case cs.SyntaxKind.DefaultExpression: this.writeDefaultExpression(expr as cs.DefaultExpression); break; + case cs.SyntaxKind.TypeOfExpression: + this.writeTypeOfExpression(expr as cs.TypeOfExpression); + break; default: throw new Error(`Unhandled expression type: ${cs.SyntaxKind[expr.nodeType]}`); } @@ -754,6 +781,15 @@ export default class CSharpAstPrinter { } } + private writeTypeOfExpression(expr: cs.TypeOfExpression) { + this.write('typeof'); + if (expr.expression) { + this.write('('); + this.writeExpression(expr.expression); + this.write(')'); + } + } + private writePrefixUnaryExpression(expr: cs.PrefixUnaryExpression) { this.write(expr.operator); this.writeExpression(expr.operand); diff --git a/src.compiler/csharp/CSharpAstTransformer.ts b/src.compiler/csharp/CSharpAstTransformer.ts index 0b130b349..e4937d801 100644 --- a/src.compiler/csharp/CSharpAstTransformer.ts +++ b/src.compiler/csharp/CSharpAstTransformer.ts @@ -282,7 +282,7 @@ export default class CSharpAstTransformer { additionalNestedNonExportsDeclarations?: ts.Declaration[], globalStatements?: ts.Statement[] ): ts.Node { - if (this.shouldSkip(node)) { + if (this.shouldSkip(node, false)) { return node; } @@ -304,12 +304,21 @@ export default class CSharpAstTransformer { return node; } - private shouldSkip(node: ts.Node) { + private shouldSkip(node: ts.Node, checkComments: boolean) { + if (checkComments) { + const text = node.getSourceFile().text; + // check for /*@target web*/ marker + const commentText = text.substr(node.getStart() - node.getLeadingTriviaWidth(), node.getLeadingTriviaWidth()); + if(commentText.indexOf('/*@target web*/') >= 0) { + return true; + } + } const tags = ts.getJSDocTags(node).filter(t => t.tagName.text === 'target'); - if (tags.length === 0) { - return false; + if (tags.length > 0) { + return !tags.find(t => t.comment === 'csharp'); } - return !tags.find(t => t.comment === 'csharp'); + + return false; } private visitEnumDeclaration(node: ts.EnumDeclaration) { @@ -320,7 +329,7 @@ export default class CSharpAstTransformer { parent: this._csharpFile.namespace, members: [], tsNode: node, - skipEmit: this.shouldSkip(node), + skipEmit: this.shouldSkip(node, false), tsSymbol: this._context.getSymbolForDeclaration(node) }; @@ -340,7 +349,7 @@ export default class CSharpAstTransformer { tsNode: enumMember, nodeType: cs.SyntaxKind.EnumMember, name: enumMember.name.getText(), - skipEmit: this.shouldSkip(enumMember) + skipEmit: this.shouldSkip(enumMember, false) }; if (enumMember.initializer) { @@ -375,7 +384,7 @@ export default class CSharpAstTransformer { parent: this._csharpFile.namespace, members: [], tsNode: node, - skipEmit: this.shouldSkip(node), + skipEmit: this.shouldSkip(node, false), tsSymbol: this._context.getSymbolForDeclaration(node) }; @@ -417,7 +426,8 @@ export default class CSharpAstTransformer { }; if (p.constraint) { - csTypeParameter.constraint = this.createUnresolvedTypeNode(csTypeParameter, p.constraint); + let constraintType = (ts.isUnionTypeNode(p.constraint) ? p.constraint.types[0] : p.constraint); + csTypeParameter.constraint = this.createUnresolvedTypeNode(csTypeParameter, constraintType); } return csTypeParameter; @@ -525,7 +535,7 @@ export default class CSharpAstTransformer { returnType: this.createUnresolvedTypeNode(null, d.type ?? d, returnType), visibility: this.mapVisibility(d.modifiers), tsNode: d, - skipEmit: this.shouldSkip(d) + skipEmit: this.shouldSkip(d, true) }; csMethod.isAsync = !!d.modifiers && @@ -560,7 +570,8 @@ export default class CSharpAstTransformer { tsNode: d.arguments[1] } as cs.PrimitiveTypeNode, visibility: cs.Visibility.Public, - tsNode: d + tsNode: d, + skipEmit: this.shouldSkip(d, true) }; if (csMethod.name.match(/^[^a-zA-Z].*/)) { @@ -693,7 +704,7 @@ export default class CSharpAstTransformer { isAbstract: !!node.modifiers && !!node.modifiers.find(m => m.kind === ts.SyntaxKind.AbstractKeyword), partial: !!ts.getJSDocTags(node).find(t => t.tagName.text === 'partial'), members: [], - skipEmit: this.shouldSkip(node), + skipEmit: this.shouldSkip(node, false), tsSymbol: this._context.getSymbolForDeclaration(node) }; @@ -866,7 +877,7 @@ export default class CSharpAstTransformer { type: this.createUnresolvedTypeNode(null, classElement.type ?? classElement, type), visibility: cs.Visibility.None, tsNode: classElement, - skipEmit: this.shouldSkip(classElement) + skipEmit: this.shouldSkip(classElement, false) }; if (classElement.name) { @@ -928,7 +939,7 @@ export default class CSharpAstTransformer { parent: parent, visibility: this.mapVisibility(classElement.modifiers), type: this.createUnresolvedTypeNode(null, classElement.type ?? classElement, returnType), - skipEmit: this.shouldSkip(classElement) + skipEmit: this.shouldSkip(classElement, false) }; if (newProperty.visibility === cs.Visibility.Public || newProperty.visibility === cs.Visibility.Protected) { @@ -999,7 +1010,7 @@ export default class CSharpAstTransformer { parent: parent, visibility: this.mapVisibility(classElement.modifiers), type: this.createUnresolvedTypeNode(null, classElement.type ?? classElement, returnType), - skipEmit: this.shouldSkip(classElement) + skipEmit: this.shouldSkip(classElement, false) }; if (newProperty.visibility === cs.Visibility.Public || newProperty.visibility === cs.Visibility.Protected) { @@ -1061,7 +1072,7 @@ export default class CSharpAstTransformer { type: this.createUnresolvedTypeNode(null, classElement.type ?? classElement, type), visibility: visibility, tsNode: classElement, - skipEmit: this.shouldSkip(classElement) + skipEmit: this.shouldSkip(classElement, false) }; if (csProperty.visibility === cs.Visibility.Public || csProperty.visibility === cs.Visibility.Protected) { @@ -1145,7 +1156,9 @@ export default class CSharpAstTransformer { classElement: ts.MethodDeclaration ) { const signature = this._context.typeChecker.getSignatureFromDeclaration(classElement); - const returnType = this._context.typeChecker.getReturnTypeOfSignature(signature!); + const returnType: ts.Type | undefined = signature + ? this._context.typeChecker.getReturnTypeOfSignature(signature) + : undefined; const csMethod: cs.MethodDeclaration = { parent: parent, @@ -1159,7 +1172,7 @@ export default class CSharpAstTransformer { returnType: this.createUnresolvedTypeNode(null, classElement.type ?? classElement, returnType), visibility: this.mapVisibility(classElement.modifiers), tsNode: classElement, - skipEmit: this.shouldSkip(classElement) + skipEmit: this.shouldSkip(classElement, false) }; if (classElement.name) { @@ -1204,16 +1217,7 @@ export default class CSharpAstTransformer { if (classElement.typeParameters && classElement.typeParameters.length > 0) { csMethod.typeParameters = []; classElement.typeParameters.forEach(p => { - const csp = { - parent: csMethod, - name: p.name.text, - nodeType: cs.SyntaxKind.TypeParameterDeclaration, - tsNode: p - } as cs.TypeParameterDeclaration; - if (p.constraint) { - csp.constraint = this.createUnresolvedTypeNode(csp, p.constraint); - } - + const csp = this.visitTypeParameterDeclaration(csMethod, p); csMethod.typeParameters!.push(csp); }); } @@ -1230,6 +1234,10 @@ export default class CSharpAstTransformer { } private visitStatement(parent: cs.Node, s: ts.Statement): cs.Statement | null { + if (this.shouldSkip(s, true)) { + return null; + } + switch (s.kind) { case ts.SyntaxKind.EmptyStatement: return this.visitEmptyStatement(parent, s as ts.EmptyStatement); @@ -1350,7 +1358,7 @@ export default class CSharpAstTransformer { nodeType: cs.SyntaxKind.VariableDeclaration, parent: parent, tsNode: s, - name: s.name.getText(), + name: (s.name as ts.Identifier).text, type: {} as cs.TypeNode } as cs.VariableDeclaration; @@ -1650,6 +1658,10 @@ export default class CSharpAstTransformer { } private visitCaseClause(parent: cs.SwitchStatement, s: ts.CaseClause) { + if (this.shouldSkip(s, true)) { + return null; + } + const caseClause = { nodeType: cs.SyntaxKind.CaseClause, parent: parent, @@ -1741,7 +1753,7 @@ export default class CSharpAstTransformer { returnType: this.createUnresolvedTypeNode(null, classElement.type ?? classElement, returnType), visibility: cs.Visibility.None, tsNode: classElement, - skipEmit: this.shouldSkip(classElement) + skipEmit: this.shouldSkip(classElement, false) }; if (classElement.name) { @@ -1798,7 +1810,7 @@ export default class CSharpAstTransformer { const csParameter: cs.ParameterDeclaration = { nodeType: cs.SyntaxKind.ParameterDeclaration, - name: p.name.getText(), + name: (p.name as ts.Identifier).text, parent: csMethod, type: this.createUnresolvedTypeNode(null, p.type ?? p, type), tsNode: p, @@ -1836,7 +1848,7 @@ export default class CSharpAstTransformer { isStatic: false, visibility: this.mapVisibility(classElement.modifiers), tsNode: classElement, - skipEmit: this.shouldSkip(classElement) + skipEmit: this.shouldSkip(classElement, false) }; classElement.parameters.forEach(p => this.visitMethodParameter(csConstructor, p)); @@ -2012,13 +2024,27 @@ export default class CSharpAstTransformer { } private visitThisExpression(parent: cs.Node, expression: ts.ThisExpression) { - const csExpr = { - parent: parent, - tsNode: expression, - nodeType: cs.SyntaxKind.ThisLiteral - } as cs.ThisLiteral; + if (parent.nodeType === cs.SyntaxKind.MemberAccessExpression && + parent.tsSymbol && + this._context.isStaticSymbol(parent.tsSymbol)) { + const identifier = { + parent: parent, + tsNode: expression, + tsSymbol: this._context.typeChecker.getSymbolAtLocation(expression), + nodeType: cs.SyntaxKind.Identifier, + text: parent.tsSymbol.name + } as cs.Identifier; - return csExpr; + return identifier; + } else { + const csExpr = { + parent: parent, + tsNode: expression, + nodeType: cs.SyntaxKind.ThisLiteral + } as cs.ThisLiteral; + + return csExpr; + } } private visitSuperLiteralExpression(parent: cs.Node, expression: ts.SuperExpression) { @@ -2176,30 +2202,56 @@ export default class CSharpAstTransformer { break; } - const toInt = ((bitOp.left as cs.ParenthesizedExpression).expression = { - parent: bitOp, - nodeType: cs.SyntaxKind.CastExpression, - expression: {} as cs.Expression, - type: { - parent: null, - nodeType: cs.SyntaxKind.PrimitiveTypeNode, - type: cs.PrimitiveType.Int, + const leftType = this._context.typeChecker.getTypeAtLocation(expression.left); + const rightType = this._context.typeChecker.getTypeAtLocation(expression.right); + + const isLeftEnum = (leftType.flags & ts.TypeFlags.Enum) || (leftType.flags & ts.TypeFlags.EnumLiteral); + const isRightEnum = (rightType.flags & ts.TypeFlags.Enum) || (rightType.flags & ts.TypeFlags.EnumLiteral); + + if (!isLeftEnum || !isRightEnum) { + const toInt = ((bitOp.left as cs.ParenthesizedExpression).expression = { + parent: bitOp, + nodeType: cs.SyntaxKind.CastExpression, + expression: {} as cs.Expression, + type: { + parent: null, + nodeType: cs.SyntaxKind.PrimitiveTypeNode, + type: cs.PrimitiveType.Int, + tsNode: expression + } as cs.PrimitiveTypeNode, tsNode: expression - } as cs.PrimitiveTypeNode, - tsNode: expression - } as cs.CastExpression); - toInt.expression = this.visitExpression(assignment, expression.left)!; - if (!toInt.expression) { - return null; - } + } as cs.CastExpression); + toInt.expression = this.visitExpression(assignment, expression.left)!; + if (!toInt.expression) { + return null; + } - (bitOp.right as cs.ParenthesizedExpression).expression = this.visitExpression( - bitOp.right, - expression.right - )!; + (bitOp.right as cs.ParenthesizedExpression).expression = this.visitExpression( + bitOp.right, + expression.right + )!; - if (!(bitOp.right as cs.ParenthesizedExpression).expression) { - return null; + if (!(bitOp.right as cs.ParenthesizedExpression).expression) { + return null; + } + } else { + (bitOp.left as cs.ParenthesizedExpression).expression = this.visitExpression( + assignment, + expression.left + )!; + + if (!(bitOp.right as cs.ParenthesizedExpression).expression) { + return null; + } + + (bitOp.right as cs.ParenthesizedExpression).expression = this.visitExpression( + bitOp.right, + expression.right + )!; + + if (!(bitOp.right as cs.ParenthesizedExpression).expression) { + return null; + } } return assignment; @@ -2223,19 +2275,27 @@ export default class CSharpAstTransformer { return null; } - switch (expression.operatorToken.kind) { - case ts.SyntaxKind.AmpersandToken: - case ts.SyntaxKind.GreaterThanGreaterThanToken: - case ts.SyntaxKind.LessThanLessThanToken: - case ts.SyntaxKind.BarToken: - binaryExpression.left = this.makeInt(binaryExpression.left); - binaryExpression.right = this.makeInt(binaryExpression.right); - break; - case ts.SyntaxKind.SlashToken: - if (expression.left.kind === ts.SyntaxKind.NumericLiteral && expression.right.kind === ts.SyntaxKind.NumericLiteral) { - binaryExpression.right = this.makeDouble(binaryExpression.right); - } - break; + const leftType = this._context.typeChecker.getTypeAtLocation(expression.left); + const rightType = this._context.typeChecker.getTypeAtLocation(expression.right); + + const isLeftEnum = (leftType.flags & ts.TypeFlags.Enum) || (leftType.flags & ts.TypeFlags.EnumLiteral); + const isRightEnum = (rightType.flags & ts.TypeFlags.Enum) || (rightType.flags & ts.TypeFlags.EnumLiteral); + + if (!isLeftEnum || !isRightEnum) { + switch (expression.operatorToken.kind) { + case ts.SyntaxKind.AmpersandToken: + case ts.SyntaxKind.GreaterThanGreaterThanToken: + case ts.SyntaxKind.LessThanLessThanToken: + case ts.SyntaxKind.BarToken: + binaryExpression.left = this.makeInt(binaryExpression.left); + binaryExpression.right = this.makeInt(binaryExpression.right); + break; + case ts.SyntaxKind.SlashToken: + if (expression.left.kind === ts.SyntaxKind.NumericLiteral && expression.right.kind === ts.SyntaxKind.NumericLiteral) { + binaryExpression.right = this.makeDouble(binaryExpression.right); + } + break; + } } return binaryExpression; @@ -2783,25 +2843,35 @@ export default class CSharpAstTransformer { return null; } - const csArg = { - expression: {} as cs.Expression, - nodeType: cs.SyntaxKind.CastExpression, - parent: parent, - tsNode: expression.argumentExpression, - type: { - nodeType: cs.SyntaxKind.PrimitiveTypeNode, - type: cs.PrimitiveType.Int - } as cs.PrimitiveTypeNode - } as cs.CastExpression; - elementAccess.argumentExpression = csArg; + let type = symbol ? this._context.typeChecker.getTypeOfSymbolAtLocation(symbol!, expression.expression) : null; + if (type) { + type = this._context.typeChecker.getNonNullableType(type); + } + const isArrayAccessor = !symbol || (type && type.symbol && !!type.symbol.members?.has(ts.escapeLeadingUnderscores('slice'))); + if (isArrayAccessor) { + const csArg = { + expression: {} as cs.Expression, + nodeType: cs.SyntaxKind.CastExpression, + parent: parent, + tsNode: expression.argumentExpression, + type: { + nodeType: cs.SyntaxKind.PrimitiveTypeNode, + type: cs.PrimitiveType.Int + } as cs.PrimitiveTypeNode + } as cs.CastExpression; + elementAccess.argumentExpression = csArg; - const par = { - nodeType: cs.SyntaxKind.ParenthesizedExpression, - parent: csArg, - expression: argumentExpression - } as cs.ParenthesizedExpression; - argumentExpression.parent = par; - csArg.expression = par; + const par = { + nodeType: cs.SyntaxKind.ParenthesizedExpression, + parent: csArg, + expression: argumentExpression + } as cs.ParenthesizedExpression; + argumentExpression.parent = par; + csArg.expression = par; + } else { + elementAccess.argumentExpression = argumentExpression; + argumentExpression.parent = elementAccess; + } return this.wrapToSmartCast(parent, elementAccess, expression); } @@ -2999,6 +3069,7 @@ export default class CSharpAstTransformer { nodeType: cs.SyntaxKind.NullLiteral } as cs.NullLiteral; } + const identifier = { parent: parent, tsNode: expression, @@ -3007,6 +3078,27 @@ export default class CSharpAstTransformer { text: expression.text } as cs.Identifier; + if (identifier.tsSymbol) { + switch (expression.parent.kind) { + case ts.SyntaxKind.PropertyAccessExpression: + case ts.SyntaxKind.BinaryExpression: + break; + default: + switch (identifier.tsSymbol.flags) { + case ts.SymbolFlags.Alias: + case ts.SymbolFlags.RegularEnum: + return { + parent: parent, + nodeType: cs.SyntaxKind.TypeOfExpression, + tsNode: expression, + expression: identifier + } as cs.TypeOfExpression; + } + break; + } + + } + return this.wrapToSmartCast(parent, identifier, expression); } diff --git a/src.compiler/csharp/CSharpEmitter.ts b/src.compiler/csharp/CSharpEmitter.ts index a890bd7bb..20d5449d8 100644 --- a/src.compiler/csharp/CSharpEmitter.ts +++ b/src.compiler/csharp/CSharpEmitter.ts @@ -3,21 +3,22 @@ import CSharpAstTransformer from './CSharpAstTransformer'; import CSharpEmitterContext from './CSharpEmitterContext'; import CSharpAstPrinter from './CSharpAstPrinter'; -export default function emit(program: ts.Program): ts.Diagnostic[] { - const diagnostics: ts.Diagnostic[] = []; - +export default function emit(program: ts.Program, diagnostics: ts.Diagnostic[]) { const context = new CSharpEmitterContext(program); - + console.log('[C#] Transforming to C# AST'); program.getRootFileNames().forEach(file => { const sourceFile = program.getSourceFile(file)!; const transformer = new CSharpAstTransformer(sourceFile, context); transformer.transform(); }); + + console.log('[C#] Resolving types'); context.resolveAllUnresolvedTypeNodes(); context.rewriteVisibilities(); if (!context.hasErrors) { + console.log('[C#] Writing Result'); context.csharpFiles.forEach(file => { const printer = new CSharpAstPrinter(file, context); printer.print(); @@ -26,6 +27,4 @@ export default function emit(program: ts.Program): ts.Diagnostic[] { } diagnostics.push(...context.diagnostics); - - return diagnostics; } \ No newline at end of file diff --git a/src.compiler/csharp/CSharpEmitterContext.ts b/src.compiler/csharp/CSharpEmitterContext.ts index 8cf1055e1..7663d3e72 100644 --- a/src.compiler/csharp/CSharpEmitterContext.ts +++ b/src.compiler/csharp/CSharpEmitterContext.ts @@ -1,16 +1,11 @@ import * as cs from './CSharpAst'; import * as ts from 'typescript'; import * as path from 'path'; +import { indexOf } from 'lodash'; type SymbolKey = string; export default class CSharpEmitterContext { - public isNullableString(type: ts.Type) { - if (type.isUnion()) { - type = this.typeChecker.getNonNullableType(type); - } - return ((type.flags & ts.TypeFlags.String) || (type.flags & ts.TypeFlags.StringLiteral)); - } private _fileLookup: Map = new Map(); private _symbolLookup: Map = new Map(); private _exportedSymbols: Map = new Map(); @@ -303,10 +298,13 @@ export default class CSharpEmitterContext { return null; case 'Map': const mapType = tsType as ts.TypeReference; + let mapKeyType: cs.TypeNode | null = null; let mapValueType: cs.TypeNode | null = null; if (typeArguments) { + mapKeyType = this.resolveType(typeArguments[0]); mapValueType = this.resolveType(typeArguments[1]); } else if (mapType.typeArguments) { + mapKeyType = this.getTypeFromTsType(node, mapType.typeArguments[0]); mapValueType = this.getTypeFromTsType(node, mapType.typeArguments[1]); } @@ -340,7 +338,7 @@ export default class CSharpEmitterContext { parent: node.parent, tsNode: node.tsNode, reference: this.buildCoreNamespace(tsSymbol) + (isValueType ? 'ValueTypeMap' : 'Map'), - typeArguments: typeArguments + typeArguments: [mapKeyType, mapValueType] } as cs.TypeReference; case 'Array': const arrayType = tsType as ts.TypeReference; @@ -389,6 +387,10 @@ export default class CSharpEmitterContext { // typescript compiler API somehow does not provide proper type symbols // for function types, we need to attempt resolving the types via the function type declaration + if (!tsType.symbol || !tsType.symbol.declarations) { + return null; + } + let functionTypeNode: ts.FunctionTypeNode | null = null; for (const declaration of tsType.symbol.declarations) { if (ts.isFunctionTypeNode(declaration)) { @@ -610,6 +612,13 @@ export default class CSharpEmitterContext { return handleNullablePrimitive(cs.PrimitiveType.Dynamic); } + // object -> object + if(tsType.flags === ts.TypeFlags.NonPrimitive && 'objectFlags' in tsType && 'intrinsicName' in tsType) { + const unknown = handleNullablePrimitive(cs.PrimitiveType.Object); + unknown.isNullable = true; + return unknown; + } + // unknown -> object if ((tsType.flags & ts.TypeFlags.Unknown) !== 0) { const unknown = handleNullablePrimitive(cs.PrimitiveType.Object); @@ -809,7 +818,11 @@ export default class CSharpEmitterContext { } public isBooleanSmartCast(tsNode: ts.Node) { - let tsParent = tsNode.parent!; + let tsParent = tsNode.parent; + if (!tsParent) { + return false; + } + while (tsParent.kind === ts.SyntaxKind.ParenthesizedExpression) { tsNode = tsParent; tsParent = tsParent.parent!; @@ -945,6 +958,7 @@ export default class CSharpEmitterContext { let declaredType = this.typeChecker.getTypeAtLocation(symbol.declarations[0]); + let contextualTypeNullable = contextualType; contextualType = this.typeChecker.getNonNullableType(contextualType); declaredType = this.typeChecker.getNonNullableType(declaredType); @@ -989,7 +1003,7 @@ export default class CSharpEmitterContext { } return contextualType !== declaredType && !this.isTypeAssignable(contextualType, declaredType) - ? contextualType + ? contextualTypeNullable : null; } shouldSkipSmartCast(contextualType: ts.Type) { @@ -1007,6 +1021,7 @@ export default class CSharpEmitterContext { if (contextualType.symbol) { switch (contextualType.symbol.name) { case 'ArrayLike': + case '__type': return true; } } @@ -1211,4 +1226,17 @@ export default class CSharpEmitterContext { break; } } + + public isStaticSymbol(tsSymbol: ts.Symbol) { + return !!tsSymbol.declarations.find(d => d.modifiers && + !!d.modifiers.find(m => m.kind === ts.SyntaxKind.StaticKeyword) + ); + } + + public isNullableString(type: ts.Type) { + if (type.isUnion()) { + type = this.typeChecker.getNonNullableType(type); + } + return ((type.flags & ts.TypeFlags.String) || (type.flags & ts.TypeFlags.StringLiteral)); + } } diff --git a/src.compiler/csharp/CSharpTranspiler.ts b/src.compiler/csharp/CSharpTranspiler.ts index 4a7f53c11..949f6f3f8 100644 --- a/src.compiler/csharp/CSharpTranspiler.ts +++ b/src.compiler/csharp/CSharpTranspiler.ts @@ -1,87 +1,7 @@ -import * as ts from 'typescript'; import emit from './CSharpEmitter'; +import transpiler from '../TranspilerBase' -function createDiagnosticReporter(pretty?: boolean): ts.DiagnosticReporter { - const host: ts.FormatDiagnosticsHost = { - getCurrentDirectory: () => ts.sys.getCurrentDirectory(), - getNewLine: () => ts.sys.newLine, - getCanonicalFileName: ts.sys.useCaseSensitiveFileNames - ? x => x - : x => x.toLowerCase(), - }; - - if (!pretty) { - return diagnostic => ts.sys.write(ts.formatDiagnostic(diagnostic, host)); - } - - return diagnostic => { - ts.sys.write(ts.formatDiagnosticsWithColorAndContext([diagnostic], host) + host.getNewLine()); - }; -} - -const commandLine = ts.parseCommandLine(ts.sys.args); -if (!ts.sys.fileExists(commandLine.options.project!)) { - ts.sys.exit(ts.ExitStatus.InvalidProject_OutputsSkipped); -} - -let reportDiagnostic = createDiagnosticReporter(); - -const parseConfigFileHost: ts.ParseConfigFileHost = ts.sys; -parseConfigFileHost.onUnRecoverableConfigFileDiagnostic = diagnostic => { - reportDiagnostic(diagnostic); - ts.sys.exit(ts.ExitStatus.InvalidProject_OutputsSkipped); -}; - -const parsedCommandLine = ts.getParsedCommandLineOfConfigFile(commandLine.options.project!, commandLine.options, parseConfigFileHost, /*extendedConfigCache*/ undefined, commandLine.watchOptions)!; -const pretty = !!ts.sys.writeOutputIsTTY && ts.sys.writeOutputIsTTY(); -if (pretty) { - reportDiagnostic = createDiagnosticReporter(true); -} - -const program = ts.createProgram({ - rootNames: parsedCommandLine.fileNames, - options: parsedCommandLine.options, - projectReferences: parsedCommandLine.projectReferences, - host: ts.createCompilerHost(parsedCommandLine.options), -}); - -const allDiagnostics = program.getConfigFileParsingDiagnostics().slice(); -const configFileParsingDiagnosticsLength = allDiagnostics.length; -allDiagnostics.push(...program.getSyntacticDiagnostics()); - -if (allDiagnostics.length === configFileParsingDiagnosticsLength) { - allDiagnostics.push(...program.getOptionsDiagnostics()); - allDiagnostics.push(...program.getGlobalDiagnostics()); - allDiagnostics.push(...program.getSemanticDiagnostics()); -} - -const emitDiagnostics = emit(program); -allDiagnostics.push(...emitDiagnostics); - -let diagnostics = ts.sortAndDeduplicateDiagnostics(allDiagnostics); -let errorCount = 0; -let warningCount = 0; -diagnostics.forEach(d => { - switch (d.category) { - case ts.DiagnosticCategory.Error: errorCount++; break; - case ts.DiagnosticCategory.Warning: warningCount++; break; - } - reportDiagnostic(d); -}); - -if (pretty) { - reportDiagnostic({ - file: undefined, - start: undefined, - length: undefined, - code: 6194, - messageText:`Compilation completed with ${errorCount} errors and ${warningCount} warnings${ts.sys.newLine}`, - category: errorCount > 0 ? ts.DiagnosticCategory.Error : warningCount > 0 ? ts.DiagnosticCategory.Warning : ts.DiagnosticCategory.Message, - }); -} - -if (errorCount > 0) { - ts.sys.exit(ts.ExitStatus.DiagnosticsPresent_OutputsGenerated); -} else { - ts.sys.exit(ts.ExitStatus.Success); -} \ No newline at end of file +transpiler([{ + name: 'C#', + emit: emit +}], true); \ No newline at end of file diff --git a/src.compiler/typescript/AlphaTabGenerator.ts b/src.compiler/typescript/AlphaTabGenerator.ts new file mode 100644 index 000000000..9afea6cff --- /dev/null +++ b/src.compiler/typescript/AlphaTabGenerator.ts @@ -0,0 +1,14 @@ +import * as ts from 'typescript'; +import cloneEmit from './CloneEmitter'; +import serializerEmit from './SerializerEmitter'; +import transpiler from '../TranspilerBase' + +transpiler([{ + name: 'Clone', + emit: cloneEmit +}, { + name: 'Serializer', + emit: serializerEmit +}]); + +ts.sys.exit(ts.ExitStatus.Success); \ No newline at end of file diff --git a/src.compiler/typescript/CloneEmitter.ts b/src.compiler/typescript/CloneEmitter.ts new file mode 100644 index 000000000..343212181 --- /dev/null +++ b/src.compiler/typescript/CloneEmitter.ts @@ -0,0 +1,362 @@ +/** + * This file contains an emitter which generates classes to clone + * any data models following certain rules. + */ +import * as path from 'path'; +import * as ts from 'typescript'; +import createEmitter from './EmitterBase' +import { addNewLines } from '../BuilderHelpers'; +import { getTypeWithNullableInfo } from '../BuilderHelpers'; +import { unwrapArrayItemType } from '../BuilderHelpers'; + +function removeExtension(fileName: string) { + return fileName.substring(0, fileName.lastIndexOf('.')); +} + +function toImportPath(fileName: string) { + return "@" + removeExtension(fileName).split('\\').join('/'); +} + +function isClonable(type: ts.Type): boolean { + if (!type.symbol) { + return false; + } + + const declaration = type.symbol.valueDeclaration; + if (declaration) { + return !!ts.getJSDocTags(declaration).find(t => t.tagName.text === 'cloneable'); + } + + return false; +} + +function isCloneMember(propertyDeclaration: ts.PropertyDeclaration) { + if (propertyDeclaration.modifiers) { + if (propertyDeclaration.modifiers.find(m => m.kind === ts.SyntaxKind.StaticKeyword || m.kind === ts.SyntaxKind.ReadonlyKeyword)) { + return false; + } + + if (!propertyDeclaration.modifiers.find(m => m.kind === ts.SyntaxKind.PublicKeyword)) { + return false; + } + } + + if (ts.getJSDocTags(propertyDeclaration).find(t => t.tagName.text === 'clone_ignore')) { + return false; + } + + return true; +} + +function generateClonePropertyStatements(prop: ts.PropertyDeclaration, typeChecker: ts.TypeChecker, + importer: (name: string, module: string) => void): ts.Statement[] { + const propertyType = getTypeWithNullableInfo(typeChecker, prop.type!); + + const statements: ts.Statement[] = []; + + const propertyName = (prop.name as ts.Identifier).text; + + function assign(expr: ts.Expression) { + return [ts.factory.createExpressionStatement( + ts.factory.createAssignment( + ts.factory.createPropertyAccessExpression( + ts.factory.createIdentifier('clone'), + propertyName + ), + expr + ) + )]; + } + + const arrayItemType = unwrapArrayItemType(propertyType.type!, typeChecker); + if (arrayItemType) { + if (isClonable(arrayItemType)) { + const collectionAddMethod = ts.getJSDocTags(prop) + .filter(t => t.tagName.text === 'clone_add') + .map(t => t.comment ?? "")[0]; + + importer(arrayItemType.symbol!.name + "Cloner", './' + arrayItemType.symbol!.name + "Cloner"); + const loopItems = [ + ...assign(ts.factory.createArrayLiteralExpression(undefined)), + + ts.factory.createForOfStatement( + undefined, + ts.factory.createVariableDeclarationList( + [ts.factory.createVariableDeclaration('i')], + ts.NodeFlags.Const + ), + ts.factory.createPropertyAccessExpression( + ts.factory.createIdentifier('original'), + propertyName + ), + ts.factory.createBlock([ + ts.factory.createExpressionStatement( + collectionAddMethod + // clone.addProp(ItemTypeCloner.clone(i)) + ? ts.factory.createCallExpression( + ts.factory.createPropertyAccessExpression( + ts.factory.createIdentifier('clone'), + collectionAddMethod + ), + undefined, + [ts.factory.createCallExpression( + ts.factory.createPropertyAccessExpression( + ts.factory.createIdentifier(arrayItemType.symbol!.name + "Cloner"), + 'clone' + ), + undefined, + [ + ts.factory.createIdentifier('i') + ] + )] + ) + // clone.prop.push(ItemTypeCloner.clone(i)) + : ts.factory.createCallExpression( + ts.factory.createPropertyAccessExpression( + ts.factory.createPropertyAccessExpression( + ts.factory.createIdentifier('clone'), + propertyName + ), + 'push' + ), + undefined, + [ts.factory.createCallExpression( + ts.factory.createPropertyAccessExpression( + ts.factory.createIdentifier(arrayItemType.symbol!.name + "Cloner"), + 'clone' + ), + undefined, + [ + ts.factory.createIdentifier('i') + ] + )] + ) + ) + ]) + )]; + + if (propertyType.isNullable) { + // if(original.prop) { + // clone.prop = []; + // for(const i of original.prop) { clone.addProp(ItemTypeCloner.clone(i)); } + // // or + // for(const i of original.prop) { clone.prop.add(ItemTypeCloner.clone(i)); } + // } + statements.push( + ts.factory.createIfStatement( + ts.factory.createPropertyAccessExpression( + ts.factory.createIdentifier('original'), + propertyName + ), + ts.factory.createBlock( + loopItems + ), + undefined + ) + ) + } else { + // clone.prop = []; + // for(const i of original.prop) { clone.addProp(ItemTypeCloner.clone(i)); } + // // or + // for(const i of original.prop) { clone.prop.add(ItemTypeCloner.clone(i)); } + statements.push(...loopItems); + } + } else { + const sliceCall = + ts.factory.createCallExpression( + ts.factory.createPropertyAccessExpression( + ts.factory.createPropertyAccessExpression( + ts.factory.createIdentifier('original'), + propertyName + ), + 'slice' + ), + undefined, + [] + ); + + if (propertyType.isNullable) { + statements.push(...assign( + ts.factory.createConditionalExpression( + ts.factory.createPropertyAccessExpression( + ts.factory.createIdentifier('original'), + propertyName + ), + ts.factory.createToken(ts.SyntaxKind.QuestionToken), + sliceCall, + ts.factory.createToken(ts.SyntaxKind.ColonToken), + ts.factory.createNull() + ) + )); + } else { + // clone.prop = original.prop.splice() + statements.push(...assign(sliceCall)); + } + } + } else { + if (isClonable(propertyType.type!)) { + importer(propertyType.type.symbol!.name + "Cloner", './' + propertyType.type.symbol!.name + "Cloner"); + + // clone.prop = original.prop ? TypeNameCloner.clone(original.prop) : null + statements.push(...assign( + ts.factory.createConditionalExpression( + ts.factory.createPropertyAccessExpression( + ts.factory.createIdentifier('original'), + propertyName + ), + ts.factory.createToken(ts.SyntaxKind.QuestionToken), + ts.factory.createCallExpression( + ts.factory.createPropertyAccessExpression( + ts.factory.createIdentifier(propertyType.type.symbol!.name + "Cloner"), + 'clone' + ), + undefined, + [ + ts.factory.createPropertyAccessExpression( + ts.factory.createIdentifier('original'), + propertyName + ) + ] + ), + ts.factory.createToken(ts.SyntaxKind.ColonToken), + ts.factory.createNull() + ) + )); + } else { + // clone.prop = original.prop + statements.push(...assign( + ts.factory.createPropertyAccessExpression( + ts.factory.createIdentifier('original'), + propertyName + ) + )); + } + } + + return statements; +} + +function generateCloneBody(program: ts.Program, input: ts.ClassDeclaration, importer: (name: string, module: string) => void): ts.Block { + const typeChecker = program.getTypeChecker(); + const propertiesToSerialize = input.members.filter( + m => ts.isPropertyDeclaration(m) && isCloneMember(m) + ).map(m => m as ts.PropertyDeclaration); + + const bodyStatements = propertiesToSerialize.reduce((stmts, prop) => { + stmts.push(...generateClonePropertyStatements(prop, typeChecker, importer)); + return stmts; + }, new Array()); + + return ts.factory.createBlock(addNewLines([ + // const clone = new Type(); + ts.factory.createVariableStatement( + undefined, + ts.factory.createVariableDeclarationList([ + ts.factory.createVariableDeclaration( + 'clone', + undefined, + undefined, + ts.factory.createNewExpression( + ts.factory.createIdentifier(input.name!.text), + undefined, + [] + ) + ) + ], ts.NodeFlags.Const) + ), + ...bodyStatements, + // return json; + ts.factory.createReturnStatement(ts.factory.createIdentifier('clone')) + ])); +} + + +function createCloneMethod(program: ts.Program, input: ts.ClassDeclaration, importer: (name: string, module: string) => void) { + return ts.factory.createMethodDeclaration( + undefined, + [ + ts.factory.createModifier(ts.SyntaxKind.PublicKeyword), + ts.factory.createModifier(ts.SyntaxKind.StaticKeyword), + ], + undefined, + 'clone', + undefined, + undefined, + [ + ts.factory.createParameterDeclaration( + undefined, + undefined, + undefined, + 'original', + undefined, + ts.factory.createTypeReferenceNode( + input.name!.text, + undefined + ), + undefined + ) + ], + ts.factory.createTypeReferenceNode( + input.name!.text, + undefined + ), + generateCloneBody(program, input, importer) + ) +} + +export default createEmitter('cloneable', (program, input) => { + console.log(`Writing Cloner for ${input.name!.text}`); + const sourceFileName = path.relative( + path.join(path.resolve(program.getCompilerOptions().baseUrl!)), + path.resolve(input.getSourceFile().fileName) + ); + + const statements: ts.Statement[] = []; + + function importer(name: string, module: string) { + statements.push(ts.factory.createImportDeclaration( + undefined, + undefined, + ts.factory.createImportClause( + false, + undefined, + ts.factory.createNamedImports([ts.factory.createImportSpecifier( + undefined, + ts.factory.createIdentifier(name) + )]) + ), + ts.factory.createStringLiteral(module) + )) + } + + statements.push(ts.factory.createClassDeclaration( + [], + [ + ts.factory.createModifier(ts.SyntaxKind.ExportKeyword), + ], + input.name!.text + 'Cloner', + undefined, + undefined, + [ + createCloneMethod(program, input, importer) + ] + )); + + const sourceFile = ts.factory.createSourceFile([ + ts.factory.createImportDeclaration( + undefined, + undefined, + ts.factory.createImportClause(false, + undefined, + ts.factory.createNamedImports([ts.factory.createImportSpecifier( + undefined, + ts.factory.createIdentifier(input.name!.text) + )]) + ), + ts.factory.createStringLiteral(toImportPath(sourceFileName)) + ), + ...statements + ], ts.factory.createToken(ts.SyntaxKind.EndOfFileToken), ts.NodeFlags.None); + + return sourceFile; +}); \ No newline at end of file diff --git a/src.compiler/typescript/EmitterBase.ts b/src.compiler/typescript/EmitterBase.ts new file mode 100644 index 000000000..9d59f7fbf --- /dev/null +++ b/src.compiler/typescript/EmitterBase.ts @@ -0,0 +1,101 @@ +import * as path from 'path'; +import * as ts from 'typescript'; +import * as fs from 'fs'; + +export default function createEmitter(jsDocMarker: string, generate: (program: ts.Program, classDeclaration: ts.ClassDeclaration) => ts.SourceFile) { + + function generateClass(program: ts.Program, classDeclaration: ts.ClassDeclaration) { + const sourceFileName = path.relative( + path.resolve(program.getCompilerOptions().baseUrl!, 'src'), + path.resolve(classDeclaration.getSourceFile().fileName) + ); + + const result = generate(program, classDeclaration); + const defaultClass = result.statements.filter(stmt => ts.isClassDeclaration(stmt) && + stmt.modifiers!.find(m => m.kind === ts.SyntaxKind.ExportKeyword) + )[0] as ts.ClassDeclaration; + + const targetFileName = path.join( + path.resolve(program.getCompilerOptions().baseUrl!), + 'src/generated', + path.dirname(sourceFileName), + defaultClass.name!.text + '.ts' + ); + + fs.mkdirSync(path.dirname(targetFileName), { recursive: true }); + + const fileHandle = fs.openSync(targetFileName, 'w'); + + fs.writeSync(fileHandle, '// \n'); + fs.writeSync(fileHandle, '// This code was auto-generated.\n'); + fs.writeSync(fileHandle, '// Changes to this file may cause incorrect behavior and will be lost if\n'); + fs.writeSync(fileHandle, '// the code is regenerated.\n'); + fs.writeSync(fileHandle, '// \n'); + + const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed }); + const source = printer.printNode(ts.EmitHint.Unspecified, result, result); + const servicesHost: ts.LanguageServiceHost = { + getScriptFileNames: () => [targetFileName], + getScriptVersion: fileName => result.languageVersion.toString(), + getScriptSnapshot: fileName => { + if (fileName != targetFileName) { + return undefined; + } + + return ts.ScriptSnapshot.fromString(source); + }, + getCurrentDirectory: () => process.cwd(), + getCompilationSettings: () => program.getCompilerOptions(), + getDefaultLibFileName: options => ts.getDefaultLibFilePath(options), + fileExists: fileName => fileName === targetFileName, + readFile: fileName => fileName === targetFileName ? source : "", + readDirectory: ts.sys.readDirectory, + directoryExists: ts.sys.directoryExists, + getDirectories: ts.sys.getDirectories, + }; + + const languageService = ts.createLanguageService(servicesHost, ts.createDocumentRegistry()); + const textChanges: ts.TextChange[] = languageService.getFormattingEditsForDocument(targetFileName, { + convertTabsToSpaces: true, + insertSpaceAfterCommaDelimiter: true, + insertSpaceAfterKeywordsInControlFlowStatements: true, + insertSpaceBeforeAndAfterBinaryOperators: true, + newLineCharacter: "\n", + indentStyle: ts.IndentStyle.Smart, + indentSize: 4, + tabSize: 4, + }); + textChanges.sort((a, b) => b.span.start - a.span.start); + + let finalText = source; + for (const textChange of textChanges) { + const { span } = textChange; + finalText = finalText.slice(0, span.start) + textChange.newText + + finalText.slice(span.start + span.length); + } + + finalText = finalText.replace(/\/\/ */g, ''); + + fs.writeSync(fileHandle, finalText); + fs.writeSync(fileHandle, '\n'); + + fs.closeSync(fileHandle); + } + + function scanSourceFile(program: ts.Program, sourceFile: ts.SourceFile) { + sourceFile.statements.forEach(stmt => { + if (ts.isClassDeclaration(stmt)) { + const isActive = ts.getJSDocTags(stmt).find(t => t.tagName.text === jsDocMarker); + if (isActive) { + generateClass(program, stmt); + } + } + }); + } + + return function emit(program: ts.Program, _diagnostics: ts.Diagnostic[]) { + program.getRootFileNames().forEach(file => { + scanSourceFile(program, program.getSourceFile(file)!); + }); + } +} \ No newline at end of file diff --git a/src.compiler/typescript/SerializerEmitter.ts b/src.compiler/typescript/SerializerEmitter.ts new file mode 100644 index 000000000..dadc27755 --- /dev/null +++ b/src.compiler/typescript/SerializerEmitter.ts @@ -0,0 +1,1195 @@ +/** + * This file contains an emitter which generates classes to serialize + * any data models to and from JSON following certain rules. + */ + +import * as path from 'path'; +import * as ts from 'typescript'; +import createEmitter from './EmitterBase' +import { addNewLines } from '../BuilderHelpers'; +import { isPrimitiveType } from '../BuilderHelpers'; +import { hasFlag } from '../BuilderHelpers'; +import { getTypeWithNullableInfo } from '../BuilderHelpers'; +import { isTypedArray } from '../BuilderHelpers'; +import { unwrapArrayItemType } from '../BuilderHelpers'; +import { isMap } from '../BuilderHelpers'; +import { isEnumType } from '../BuilderHelpers'; +import { isNumberType } from '../BuilderHelpers'; +import { wrapToNonNull } from '../BuilderHelpers'; + +interface JsonProperty { + partialNames: boolean; + property: ts.PropertyDeclaration; + jsonNames: string[]; + target?: string; +} + +function isImmutable(type: ts.Type | null): boolean { + if (!type || !type.symbol) { + return false; + } + + const declaration = type.symbol.valueDeclaration; + if (declaration) { + return !!ts.getJSDocTags(declaration).find(t => t.tagName.text === 'json_immutable'); + } + + return false; +} + +function removeExtension(fileName: string) { + return fileName.substring(0, fileName.lastIndexOf('.')); +} + +function toImportPath(fileName: string) { + return "@" + removeExtension(fileName).split('\\').join('/'); +} + +function createStringUnknownMapNode(): ts.TypeNode { + return ts.factory.createTypeReferenceNode('Map', + [ + ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword), + ts.factory.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword), + ]); +} + + +function findModule(type: ts.Type, options: ts.CompilerOptions) { + if (type.symbol) { + for (const decl of type.symbol.declarations) { + const file = decl.getSourceFile(); + if (file) { + const relative = path.relative( + path.join(path.resolve(options.baseUrl!)), + path.resolve(file.fileName) + ); + return toImportPath(relative); + } + } + + return './' + type.symbol.name; + } + + return ''; +} + +function findSerializerModule(type: ts.Type, options: ts.CompilerOptions) { + let module = findModule(type, options); + const importPath = module.split('/'); + importPath.splice(1, 0, 'generated'); + importPath[importPath.length - 1] = type.symbol!.name + 'Serializer'; + return importPath.join('/'); +} + +// +// fromJson +function generateFromJsonBody(importer: (name: string, module: string) => void) { + importer('JsonHelper', '@src/io/JsonHelper'); + return ts.factory.createBlock(addNewLines([ + ts.factory.createIfStatement( + ts.factory.createPrefixUnaryExpression( + ts.SyntaxKind.ExclamationToken, + ts.factory.createIdentifier('m'), + ), + ts.factory.createBlock([ + ts.factory.createReturnStatement() + ]) + ), + ts.factory.createExpressionStatement( + ts.factory.createCallExpression( + ts.factory.createPropertyAccessExpression( + ts.factory.createIdentifier('JsonHelper'), + 'forEach' + ), + undefined, + [ + ts.factory.createIdentifier('m'), + ts.factory.createArrowFunction( + undefined, + undefined, + [ + ts.factory.createParameterDeclaration(undefined, undefined, undefined, 'v'), + ts.factory.createParameterDeclaration(undefined, undefined, undefined, 'k') + ], + undefined, + ts.factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), + ts.factory.createCallExpression( + ts.factory.createPropertyAccessExpression(ts.factory.createThis(), 'setProperty'), + undefined, + [ + ts.factory.createIdentifier('obj'), + ts.factory.createCallExpression( + ts.factory.createPropertyAccessExpression( + ts.factory.createIdentifier('k'), + 'toLowerCase' + ), + undefined, + [] + ), + ts.factory.createIdentifier('v'), + ] + ) + + ) + ] + ) + ), + ])); +} + +function createFromJsonMethod(input: ts.ClassDeclaration, + importer: (name: string, module: string) => void) { + return ts.factory.createMethodDeclaration( + undefined, + [ + ts.factory.createModifier(ts.SyntaxKind.PublicKeyword), + ts.factory.createModifier(ts.SyntaxKind.StaticKeyword) + ], + undefined, + 'fromJson', + undefined, + undefined, + [ + ts.factory.createParameterDeclaration( + undefined, + undefined, + undefined, + 'obj', + undefined, + ts.factory.createTypeReferenceNode( + input.name!.text, + undefined + ), + ), + ts.factory.createParameterDeclaration( + undefined, + undefined, + undefined, + 'm', + undefined, + ts.factory.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword) + ) + ], + ts.factory.createKeywordTypeNode(ts.SyntaxKind.VoidKeyword), + generateFromJsonBody(importer) + ) +} +// +// toJson +function isPrimitiveToJson(type: ts.Type, typeChecker: ts.TypeChecker) { + if (!type) { + return false; + } + + const isArray = isTypedArray(type); + const arrayItemType = unwrapArrayItemType(type, typeChecker); + + if (hasFlag(type, ts.TypeFlags.Unknown)) { + return true; + } + if (hasFlag(type, ts.TypeFlags.Number)) { + return true; + } + if (hasFlag(type, ts.TypeFlags.String)) { + return true; + } + if (hasFlag(type, ts.TypeFlags.Boolean)) { + return "val"; + } + + if (arrayItemType) { + if (isArray && hasFlag(arrayItemType, ts.TypeFlags.Number)) { + return true; + } + if (isArray && hasFlag(arrayItemType, ts.TypeFlags.String)) { + return true; + } + if (isArray && hasFlag(arrayItemType, ts.TypeFlags.Boolean)) { + return true; + } + } else if (type.symbol) { + switch (type.symbol.name) { + case 'Uint8Array': + case 'Uint16Array': + case 'Uint32Array': + case 'Int8Array': + case 'Int16Array': + case 'Int32Array': + case 'Float32Array': + case 'Float64Array': + return true; + } + } + + return false; +} + +function generateToJsonBody( + program: ts.Program, + propertiesToSerialize: JsonProperty[], + importer: (name: string, module: string) => void) { + + const statements: ts.Statement[] = []; + + statements.push(ts.factory.createIfStatement( + ts.factory.createPrefixUnaryExpression( + ts.SyntaxKind.ExclamationToken, + ts.factory.createIdentifier('obj') + ), + ts.factory.createBlock([ + ts.factory.createReturnStatement(ts.factory.createNull()) + ]) + )) + + statements.push(ts.factory.createVariableStatement( + undefined, + ts.factory.createVariableDeclarationList( + [ + ts.factory.createVariableDeclaration('o', + undefined, + undefined, + ts.factory.createNewExpression(ts.factory.createIdentifier('Map'), + [ + ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword), + ts.factory.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword), + ], + [] + )) + ], + ts.NodeFlags.Const + ) + )); + + for (let prop of propertiesToSerialize) { + const fieldName = (prop.property.name as ts.Identifier).text; + const jsonName = prop.jsonNames.filter(n => n !== '')[0]; + + if (!jsonName) { + continue; + } + const typeChecker = program.getTypeChecker(); + const type = getTypeWithNullableInfo(typeChecker, prop.property.type!); + const isArray = isTypedArray(type.type!); + + let propertyStatements: ts.Statement[] = []; + + if (isPrimitiveToJson(type.type!, typeChecker)) { + propertyStatements.push(ts.factory.createExpressionStatement(ts.factory.createCallExpression( + ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier('o'), 'set'), + undefined, + [ + ts.factory.createStringLiteral(jsonName), + ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier('obj'), fieldName), + ] + ))); + } else if (isEnumType(type.type!)) { + propertyStatements.push(ts.factory.createExpressionStatement(ts.factory.createCallExpression( + ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier('o'), 'set'), + undefined, + [ + ts.factory.createStringLiteral(jsonName), + ts.factory.createAsExpression( + ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier('obj'), fieldName), + type.isNullable + ? ts.factory.createUnionTypeNode([ + ts.factory.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword), + ts.factory.createLiteralTypeNode(ts.factory.createNull()) + ]) + : ts.factory.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword) + ) + ] + ))); + } else if (isArray) { + const arrayItemType = unwrapArrayItemType(type.type!, typeChecker)!; + let itemSerializer = arrayItemType.symbol.name + "Serializer"; + importer(itemSerializer, findSerializerModule(arrayItemType, program.getCompilerOptions())); + + propertyStatements.push(ts.factory.createExpressionStatement(ts.factory.createCallExpression( + ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier('o'), 'set'), + undefined, + [ + ts.factory.createStringLiteral(jsonName), + ts.factory.createCallExpression( + ts.factory.createPropertyAccessExpression( + ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier('obj'), fieldName), + 'map' + ), + undefined, + [ + ts.factory.createArrowFunction( + undefined, + undefined, + [ts.factory.createParameterDeclaration(undefined, undefined, undefined, 'i')], + undefined, + ts.factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), + ts.factory.createCallExpression( + ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier(itemSerializer), 'toJson'), + undefined, + [ + ts.factory.createIdentifier('i'), + ] + ) + ) + ] + ) + ] + ))); + } + else if (isMap(type.type)) { + const mapType = type.type as ts.TypeReference; + if (!isPrimitiveType(mapType.typeArguments![0])) { + throw new Error('only Map maps are supported extend if needed!'); + } + + let writeValue: ts.Expression; + if (isPrimitiveToJson(mapType.typeArguments![1], typeChecker)) { + writeValue = ts.factory.createIdentifier('v'); + } else if(isEnumType(mapType.typeArguments![1])) { + writeValue = ts.factory.createAsExpression( + ts.factory.createIdentifier('v'), + ts.factory.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword) + ); + } + else { + const itemSerializer = mapType.typeArguments![1].symbol.name + "Serializer"; + importer(itemSerializer, findSerializerModule(mapType.typeArguments![1], program.getCompilerOptions())); + + writeValue = ts.factory.createCallExpression( + ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier(itemSerializer), 'toJson'), + undefined, + [ + ts.factory.createIdentifier('v') + ] + ); + } + + propertyStatements.push(ts.factory.createBlock([ + ts.factory.createVariableStatement( + undefined, + ts.factory.createVariableDeclarationList([ + ts.factory.createVariableDeclaration('m', + undefined, + undefined, + ts.factory.createNewExpression(ts.factory.createIdentifier('Map'), + [ + ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword), + ts.factory.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword) + ], + [])) + ], ts.NodeFlags.Const) + ), + ts.factory.createExpressionStatement(ts.factory.createCallExpression( + ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier('o'), 'set'), + undefined, + [ + ts.factory.createStringLiteral(jsonName), + ts.factory.createIdentifier('m') + ] + )), + + ts.factory.createExpressionStatement( + ts.factory.createCallExpression( + ts.factory.createPropertyAccessExpression( + ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier('obj'), fieldName), + 'forEach' + ), + undefined, + [ + ts.factory.createArrowFunction( + undefined, + undefined, + [ + ts.factory.createParameterDeclaration(undefined, undefined, undefined, 'v'), + ts.factory.createParameterDeclaration(undefined, undefined, undefined, 'k') + ], + undefined, + ts.factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), + ts.factory.createCallExpression( + ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier('m'), 'set'), + undefined, + [ + // todo: key to string + ts.factory.createCallExpression( + ts.factory.createPropertyAccessExpression( + ts.factory.createIdentifier('k'), + 'toString' + ), + undefined, + [] + ), + writeValue + ] + ) + ) + ] + ) + ) + ])); + } else if (isImmutable(type.type)) { + let itemSerializer = type.type.symbol.name; + importer(itemSerializer, findModule(type.type, program.getCompilerOptions())); + propertyStatements.push(ts.factory.createExpressionStatement(ts.factory.createCallExpression( + ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier('o'), 'set'), + undefined, + [ + ts.factory.createStringLiteral(jsonName), + ts.factory.createCallExpression( + ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier(itemSerializer), 'toJson'), + [], + [ + ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier('obj'), fieldName), + ] + ), + ] + ))); + } else { + let itemSerializer = type.type.symbol.name + "Serializer"; + importer(itemSerializer, findSerializerModule(type.type, program.getCompilerOptions())); + propertyStatements.push(ts.factory.createExpressionStatement(ts.factory.createCallExpression( + ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier('o'), 'set'), + undefined, + [ + ts.factory.createStringLiteral(jsonName), + ts.factory.createCallExpression( + ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier(itemSerializer), 'toJson'), + [], + [ + ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier('obj'), fieldName), + ] + ), + ] + ))); + } + + if (prop.target) { + propertyStatements = propertyStatements.map(s => ts.addSyntheticLeadingComment(s, ts.SyntaxKind.MultiLineCommentTrivia, `@target ${prop.target}`, true)); + } + + statements.push(...propertyStatements); + } + + statements.push(ts.factory.createReturnStatement(ts.factory.createIdentifier('o'))); + + return ts.factory.createBlock(addNewLines(statements)) +} + +function createToJsonMethod(program: ts.Program, + input: ts.ClassDeclaration, + propertiesToSerialize: JsonProperty[], + importer: (name: string, module: string) => void +) { + return ts.factory.createMethodDeclaration( + undefined, + [ + ts.factory.createModifier(ts.SyntaxKind.PublicKeyword), + ts.factory.createModifier(ts.SyntaxKind.StaticKeyword) + ], + undefined, + 'toJson', + undefined, + undefined, + [ + ts.factory.createParameterDeclaration( + undefined, + undefined, + undefined, + 'obj', + undefined, + ts.factory.createUnionTypeNode([ + ts.factory.createTypeReferenceNode( + input.name!.text, + undefined + ), + ts.factory.createLiteralTypeNode(ts.factory.createNull()) + ]) + ) + ], + ts.factory.createUnionTypeNode([ + createStringUnknownMapNode(), + ts.factory.createLiteralTypeNode(ts.factory.createNull()) + ]), + generateToJsonBody(program, propertiesToSerialize, importer) + ) +} + +// +// setProperty + +function isPrimitiveFromJson(type: ts.Type, typeChecker: ts.TypeChecker) { + if (!type) { + return false; + } + + const isArray = isTypedArray(type); + const arrayItemType = unwrapArrayItemType(type, typeChecker); + + if (hasFlag(type, ts.TypeFlags.Unknown)) { + return true; + } + if (hasFlag(type, ts.TypeFlags.Number)) { + return true; + } + if (hasFlag(type, ts.TypeFlags.String)) { + return true; + } + if (hasFlag(type, ts.TypeFlags.Boolean)) { + return true; + } + + if (arrayItemType) { + if (isArray && hasFlag(arrayItemType, ts.TypeFlags.Number)) { + return true; + } + if (isArray && hasFlag(arrayItemType, ts.TypeFlags.String)) { + return true; + } + if (isArray && hasFlag(arrayItemType, ts.TypeFlags.Boolean)) { + return true; + } + } else if (type.symbol) { + switch (type.symbol.name) { + case 'Uint8Array': + case 'Uint16Array': + case 'Uint32Array': + case 'Int8Array': + case 'Int16Array': + case 'Int32Array': + case 'Float32Array': + case 'Float64Array': + return true; + } + } + + return null; + +} + +function createEnumMapping(type: ts.Type): ts.Expression { + return ts.factory.createCallExpression( + ts.factory.createPropertyAccessExpression( + ts.factory.createIdentifier('JsonHelper'), + 'parseEnum' + ), + [ts.factory.createTypeReferenceNode(type.symbol.name)], + [ + ts.factory.createIdentifier('v'), + ts.factory.createIdentifier(type.symbol.name) + ] + ); +} + +function stripRanges(node: T) { + (node as any).pos = -1; + (node as any).end = -1; + return node; +} + +function getDeepMutableClone(node: T): T { + return ts.transform(node, [ + context => node => deepCloneWithContext(node, context) + ]).transformed[0]; + + function deepCloneWithContext( + node: T, + context: ts.TransformationContext + ): T { + const clonedNode = ts.visitEachChild( + stripRanges(ts.getMutableClone(node)), + child => deepCloneWithContext(child, context), + context + ); + (clonedNode as any).parent = undefined as any; + ts.forEachChild(clonedNode, child => { (child as any).parent = clonedNode; }); + return clonedNode; + } +} + + +function generateSetPropertyBody(program: ts.Program, + propertiesToSerialize: JsonProperty[], + importer: (name: string, module: string) => void +) { + const statements: ts.Statement[] = []; + const cases: ts.CaseOrDefaultClause[] = []; + + const typeChecker = program.getTypeChecker(); + for (const prop of propertiesToSerialize) { + const jsonNames = prop.jsonNames.map(j => j.toLowerCase()); + const caseValues: string[] = jsonNames.filter(j => j !== ''); + const fieldName = (prop.property.name as ts.Identifier).text; + + const caseStatements: ts.Statement[] = []; + + const type = getTypeWithNullableInfo(typeChecker, prop.property.type); + + const assignField = function (expr: ts.Expression): ts.Statement { + return ts.factory.createExpressionStatement( + ts.factory.createAssignment(ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier('obj'), fieldName), expr) + ); + }; + + if (isPrimitiveFromJson(type.type!, typeChecker)) { + caseStatements.push(assignField(ts.factory.createAsExpression( + ts.factory.createIdentifier('v'), + getDeepMutableClone(prop.property.type!) + ))); + caseStatements.push(ts.factory.createReturnStatement(ts.factory.createTrue())); + } else if (isEnumType(type.type)) { + // obj.fieldName = enummapping + // return true; + importer(type.type.symbol!.name, findModule(type.type, program.getCompilerOptions())); + importer('JsonHelper', '@src/io/JsonHelper'); + const read = createEnumMapping(type.type); + if (type.isNullable) { + caseStatements.push(assignField(read)); + } else { + caseStatements.push(assignField(ts.factory.createNonNullExpression(read))); + } + caseStatements.push(ts.factory.createReturnStatement(ts.factory.createTrue())); + } else if (isTypedArray(type.type!)) { + const arrayItemType = unwrapArrayItemType(type.type!, typeChecker)!; + const collectionAddMethod = ts.getJSDocTags(prop.property) + .filter(t => t.tagName.text === 'json_add') + .map(t => t.comment ?? "")[0]; + + // obj.fieldName = []; + // for(const i of value) { + // obj.addFieldName(Type.FromJson(i)); + // } + // or + // for(const __li of value) { + // obj.fieldName.push(Type.FromJson(__li)); + // } + + let itemSerializer = arrayItemType.symbol.name + "Serializer"; + importer(itemSerializer, findSerializerModule(arrayItemType, program.getCompilerOptions())); + importer(arrayItemType.symbol.name, findModule(arrayItemType, program.getCompilerOptions())); + + const loopItems = [ + assignField(ts.factory.createArrayLiteralExpression(undefined)), + ts.factory.createForOfStatement( + undefined, + ts.factory.createVariableDeclarationList([ts.factory.createVariableDeclaration('o')], ts.NodeFlags.Const), + ts.factory.createAsExpression( + ts.factory.createIdentifier('v'), + ts.factory.createArrayTypeNode( + ts.factory.createUnionTypeNode([ + createStringUnknownMapNode(), + ts.factory.createLiteralTypeNode(ts.factory.createNull()) + ]) + ) + ), + ts.factory.createBlock([ + ts.factory.createVariableStatement( + undefined, + ts.factory.createVariableDeclarationList([ + ts.factory.createVariableDeclaration('i', + undefined, + undefined, + ts.factory.createNewExpression( + ts.factory.createIdentifier(arrayItemType.symbol.name), + undefined, + [] + )) + ], ts.NodeFlags.Const) + ), + ts.factory.createCallExpression( + ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier(itemSerializer), 'fromJson'), + undefined, + [ + ts.factory.createIdentifier('i'), + ts.factory.createIdentifier('o') + ] + ), + ts.factory.createExpressionStatement( + collectionAddMethod + // obj.addFieldName(i) + ? ts.factory.createCallExpression( + ts.factory.createPropertyAccessExpression( + ts.factory.createIdentifier('obj'), + collectionAddMethod + ), + undefined, + [ts.factory.createIdentifier('i')] + ) + // obj.fieldName.push(i) + : ts.factory.createCallExpression( + ts.factory.createPropertyAccessExpression( + ts.factory.createPropertyAccessExpression( + ts.factory.createIdentifier('obj'), + fieldName + ), + 'push' + ), + undefined, + [ts.factory.createIdentifier('i')] + ) + ) + ].filter(s => !!s) as ts.Statement[]) + ), + ]; + + if (type.isNullable) { + caseStatements.push(ts.factory.createIfStatement( + ts.factory.createIdentifier('v'), + ts.factory.createBlock(loopItems) + )); + } else { + caseStatements.push(...loopItems); + } + caseStatements.push(ts.factory.createReturnStatement(ts.factory.createTrue())); + + } else if (isMap(type.type)) { + const mapType = type.type as ts.TypeReference; + if (!isPrimitiveType(mapType.typeArguments![0])) { + throw new Error('only Map maps are supported extend if needed!'); + } + + let mapKey; + if (isEnumType(mapType.typeArguments![0])) { + importer(mapType.typeArguments![0].symbol!.name, findModule(mapType.typeArguments![0], program.getCompilerOptions())); + importer('JsonHelper', '@src/io/JsonHelper'); + mapKey = ts.factory.createNonNullExpression(ts.factory.createCallExpression( + ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier('JsonHelper'), 'parseEnum'), + [ts.factory.createTypeReferenceNode(mapType.typeArguments![0].symbol!.name)], + [ + ts.factory.createIdentifier('k'), + ts.factory.createIdentifier(mapType.typeArguments![0].symbol!.name), + ] + )); + } else if (isNumberType(mapType.typeArguments![0])) { + mapKey = ts.factory.createCallExpression( + ts.factory.createIdentifier('parseInt'), + undefined, + [ + ts.factory.createIdentifier('k') + ] + ); + } else { + mapKey = ts.factory.createIdentifier('k'); + } + + let mapValue; + let itemSerializer: string = ''; + if (isPrimitiveFromJson(mapType.typeArguments![1], typeChecker)) { + // const isNullable = mapType.typeArguments![1].flags & ts.TypeFlags.Union + // && !!(mapType.typeArguments![1] as ts.UnionType).types.find(t => t.flags & ts.TypeFlags.Null); + + mapValue = ts.factory.createAsExpression( + ts.factory.createIdentifier('v'), + ts.isTypeReferenceNode(prop.property.type!) && prop.property.type.typeArguments + ? getDeepMutableClone(prop.property.type.typeArguments[1]) + : ts.factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword) + ); + } else { + itemSerializer = mapType.typeArguments![1].symbol.name + "Serializer"; + importer(itemSerializer, findSerializerModule(mapType.typeArguments![1], program.getCompilerOptions())); + importer(mapType.typeArguments![1]!.symbol.name, findModule(mapType.typeArguments![1], program.getCompilerOptions())); + mapValue = ts.factory.createIdentifier('i'); + } + + const collectionAddMethod = ts.getJSDocTags(prop.property) + .filter(t => t.tagName.text === 'json_add') + .map(t => t.comment ?? "")[0]; + + caseStatements.push(assignField(ts.factory.createNewExpression(ts.factory.createIdentifier('Map'), [ + typeChecker.typeToTypeNode(mapType.typeArguments![0], undefined, undefined)!, + typeChecker.typeToTypeNode(mapType.typeArguments![1], undefined, undefined)!, + ], []))); + + caseStatements.push( + ts.factory.createExpressionStatement( + ts.factory.createCallExpression( + ts.factory.createPropertyAccessExpression( + ts.factory.createAsExpression( + ts.factory.createIdentifier('v'), + createStringUnknownMapNode() + ), + 'forEach' + ), + undefined, + [ + ts.factory.createArrowFunction( + undefined, + undefined, + [ + ts.factory.createParameterDeclaration(undefined, undefined, undefined, 'v'), + ts.factory.createParameterDeclaration(undefined, undefined, undefined, 'k') + ], + undefined, + ts.factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), + ts.factory.createBlock(addNewLines([ + itemSerializer.length > 0 && ts.factory.createVariableStatement( + undefined, + ts.factory.createVariableDeclarationList([ + ts.factory.createVariableDeclaration('i', + undefined, undefined, + ts.factory.createNewExpression(ts.factory.createIdentifier(mapType.typeArguments![1].symbol.name), undefined, []) + ) + ], ts.NodeFlags.Const), + ), + itemSerializer.length > 0 && ts.factory.createExpressionStatement( + ts.factory.createCallExpression( + ts.factory.createPropertyAccessExpression( + ts.factory.createIdentifier(itemSerializer), + 'fromJson' + ), + undefined, + [ + ts.factory.createIdentifier('i'), + ts.factory.createAsExpression( + ts.factory.createIdentifier('v'), + createStringUnknownMapNode() + ), + ] + ) + ), + ts.factory.createExpressionStatement( + ts.factory.createCallExpression( + collectionAddMethod + ? ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier('obj'), collectionAddMethod) + : ts.factory.createPropertyAccessExpression( + ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier('obj'), ts.factory.createIdentifier(fieldName)), + ts.factory.createIdentifier('set') + ), + undefined, + [ + mapKey, + mapValue + ] + ) + ) + ].filter(s => !!s) as ts.Statement[])) + ) + ] + ) + ) + ); + + caseStatements.push(ts.factory.createReturnStatement(ts.factory.createTrue())); + } else if (isImmutable(type.type)) { + let itemSerializer = type.type.symbol.name; + importer(itemSerializer, findModule(type.type, program.getCompilerOptions())); + + // obj.fieldName = TypeName.fromJson(value)! + // return true; + caseStatements.push( + assignField( + wrapToNonNull( + type.isNullable, + ts.factory.createCallExpression( + // TypeName.fromJson + ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier(itemSerializer), 'fromJson'), + [], + [ + ts.factory.createIdentifier('v') + ] + ), + ts.factory + ) + ) + ); + caseStatements.push(ts.factory.createReturnStatement(ts.factory.createTrue())); + } else { + // for complex types it is a bit more tricky + // if the property matches exactly, we use fromJson + // if the property starts with the field name, we try to set a sub-property + const jsonNameArray = ts.factory.createArrayLiteralExpression(jsonNames.map(n => ts.factory.createStringLiteral(n))); + + let itemSerializer = type.type.symbol.name + "Serializer"; + importer(itemSerializer, findSerializerModule(type.type, program.getCompilerOptions())); + if (type.isNullable) { + importer(type.type.symbol!.name, findModule(type.type, program.getCompilerOptions())); + } + + // TODO if no partial name support, simply generate cases + statements.push( + ts.factory.createIfStatement( + // if(["", "core"].indexOf(property) >= 0) + ts.factory.createBinaryExpression( + ts.factory.createCallExpression( + ts.factory.createPropertyAccessExpression(jsonNameArray, 'indexOf'), + [], + [ts.factory.createIdentifier('property')] + ), + ts.SyntaxKind.GreaterThanEqualsToken, + ts.factory.createNumericLiteral('0') + ), + ts.factory.createBlock( + !type.isNullable + ? [ + ts.factory.createExpressionStatement( + ts.factory.createCallExpression( + // TypeName.fromJson + ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier(itemSerializer), 'fromJson'), + [], + [ + ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier('obj'), fieldName), + ts.factory.createAsExpression( + ts.factory.createIdentifier('v'), + createStringUnknownMapNode() + ) + ] + ) + ), + ts.factory.createReturnStatement(ts.factory.createTrue()) + ] + : [ + ts.factory.createIfStatement( + ts.factory.createIdentifier('v'), + ts.factory.createBlock([ + assignField(ts.factory.createNewExpression( + ts.factory.createIdentifier(type.type.symbol.name), + undefined, + [] + )), + ts.factory.createExpressionStatement( + ts.factory.createCallExpression( + // TypeName.fromJson + ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier(itemSerializer), 'fromJson'), + [], + [ + ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier('obj'), fieldName), + ts.factory.createAsExpression( + ts.factory.createIdentifier('v'), + createStringUnknownMapNode() + ) + ] + ) + ) + ]), + ts.factory.createBlock([ + assignField(ts.factory.createNull()) + ]) + ), + ts.factory.createReturnStatement(ts.factory.createTrue()) + ]), + !prop.partialNames ? undefined : + ts.factory.createBlock([ + // for(const candidate of ["", "core"]) { + // if(candidate.indexOf(property) === 0) { + // if(!this.field) { this.field = new FieldType(); } + // if(this.field.setProperty(property.substring(candidate.length), value)) return true; + // } + // } + ts.factory.createForOfStatement( + undefined, + ts.factory.createVariableDeclarationList([ts.factory.createVariableDeclaration('c')], ts.NodeFlags.Const), + jsonNameArray, + ts.factory.createBlock([ + ts.factory.createIfStatement( + ts.factory.createBinaryExpression( + ts.factory.createCallExpression( + ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier('property'), 'indexOf'), + [], + [ts.factory.createIdentifier('c')] + ), + ts.SyntaxKind.EqualsEqualsEqualsToken, + ts.factory.createNumericLiteral('0') + ), + ts.factory.createBlock([ + type.isNullable && ts.factory.createIfStatement( + ts.factory.createPrefixUnaryExpression( + ts.SyntaxKind.ExclamationToken, + ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier('obj'), fieldName) + ), + ts.factory.createBlock([ + assignField(ts.factory.createNewExpression(ts.factory.createIdentifier(type.type!.symbol!.name), [], [])) + ]) + ), + ts.factory.createIfStatement( + ts.factory.createCallExpression( + ts.factory.createPropertyAccessExpression( + ts.factory.createIdentifier(itemSerializer), + 'setProperty' + ), + [], + [ + ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier('obj'), fieldName), + ts.factory.createCallExpression( + ts.factory.createPropertyAccessExpression( + ts.factory.createIdentifier('property'), + 'substring' + ), + [], + [ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier('c'), 'length')] + ), + ts.factory.createIdentifier('v') + ] + ), + ts.factory.createBlock([ + ts.factory.createReturnStatement(ts.factory.createTrue()) + ]) + ) + ].filter(s => !!s) as ts.Statement[]) + ) + ]) + ) + ]) + ) + ); + } + + if (caseStatements.length > 0) { + for (let i = 0; i < caseValues.length; i++) { + let caseClause = ts.factory.createCaseClause( + ts.factory.createStringLiteral(caseValues[i]), + // last case gets the statements, others are fall through + i < caseValues.length - 1 ? [] : caseStatements + ); + if (prop.target && i === 0) { + caseClause = ts.addSyntheticLeadingComment(caseClause, ts.SyntaxKind.MultiLineCommentTrivia, `@target ${prop.target}`, true); + } + cases.push(caseClause); + } + } + } + + if (cases.length > 0) { + const switchExpr = ts.factory.createSwitchStatement(ts.factory.createIdentifier('property'), ts.factory.createCaseBlock(cases)); + statements.unshift(switchExpr); + } + + statements.push(ts.factory.createReturnStatement(ts.factory.createFalse())); + + return ts.factory.createBlock(addNewLines(statements)); +} + +function createSetPropertyMethod( + program: ts.Program, + input: ts.ClassDeclaration, + propertiesToSerialize: JsonProperty[], + importer: (name: string, module: string) => void +) { + return ts.factory.createMethodDeclaration( + undefined, + [ + ts.factory.createModifier(ts.SyntaxKind.PublicKeyword), + ts.factory.createModifier(ts.SyntaxKind.StaticKeyword) + ], + undefined, + 'setProperty', + undefined, + undefined, + [ + ts.factory.createParameterDeclaration( + undefined, + undefined, + undefined, + 'obj', + undefined, + ts.factory.createTypeReferenceNode( + input.name!.text, + undefined + ) + ), + ts.factory.createParameterDeclaration( + undefined, + undefined, + undefined, + 'property', + undefined, + ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword) + ), + ts.factory.createParameterDeclaration( + undefined, + undefined, + undefined, + 'v', + undefined, + ts.factory.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword) + ) + ], + ts.factory.createKeywordTypeNode(ts.SyntaxKind.BooleanKeyword), + generateSetPropertyBody(program, propertiesToSerialize, importer) + ) +} + +export default createEmitter('json', (program, input) => { + console.log(`Writing Serializer for ${input.name!.text}`); + const sourceFileName = path.relative( + path.join(path.resolve(program.getCompilerOptions().baseUrl!)), + path.resolve(input.getSourceFile().fileName) + ); + + let propertiesToSerialize: JsonProperty[] = []; + input.members.forEach(member => { + if (ts.isPropertyDeclaration(member)) { + const propertyDeclaration = member as ts.PropertyDeclaration; + if (!propertyDeclaration.modifiers!.find(m => + m.kind === ts.SyntaxKind.StaticKeyword || + m.kind === ts.SyntaxKind.PrivateKeyword)) { + const jsonNames = [(member.name as ts.Identifier).text]; + + if (ts.getJSDocTags(member).find(t => t.tagName.text === 'json_on_parent')) { + jsonNames.push(''); + } + + if (!ts.getJSDocTags(member).find(t => t.tagName.text === 'json_ignore')) { + propertiesToSerialize.push({ + property: propertyDeclaration, + jsonNames: jsonNames, + partialNames: !!ts.getJSDocTags(member).find(t => t.tagName.text === 'json_partial_names'), + target: ts.getJSDocTags(member).find(t => t.tagName.text === 'target')?.comment + }); + } + } + } + }); + + const statements: ts.Statement[] = []; + + const importedNames = new Set(); + function importer(name: string, module: string) { + if (importedNames.has(name)) { + return; + } + importedNames.add(name); + statements.push(ts.factory.createImportDeclaration( + undefined, + undefined, + ts.factory.createImportClause( + false, + undefined, + ts.factory.createNamedImports([ts.factory.createImportSpecifier( + undefined, + ts.factory.createIdentifier(name) + )]) + ), + ts.factory.createStringLiteral(module) + )) + } + + statements.push(ts.factory.createClassDeclaration( + [], + [ + ts.factory.createModifier(ts.SyntaxKind.ExportKeyword), + ], + input.name!.text + 'Serializer', + undefined, + undefined, + [ + createFromJsonMethod(input, importer), + createToJsonMethod(program, input, propertiesToSerialize, importer), + createSetPropertyMethod(program, input, propertiesToSerialize, importer) + ] + )); + + const sourceFile = ts.factory.createSourceFile([ + ts.factory.createImportDeclaration( + undefined, + undefined, + ts.factory.createImportClause(false, + undefined, + ts.factory.createNamedImports([ts.factory.createImportSpecifier( + undefined, + ts.factory.createIdentifier(input.name!.text) + )]) + ), + ts.factory.createStringLiteral(toImportPath(sourceFileName)) + ), + ...statements + ], ts.factory.createToken(ts.SyntaxKind.EndOfFileToken), ts.NodeFlags.None); + + return sourceFile; +}); \ No newline at end of file diff --git a/src.csharp/AlphaTab.Test/Test/Globals.cs b/src.csharp/AlphaTab.Test/Test/Globals.cs index 80d918836..788dc47dc 100644 --- a/src.csharp/AlphaTab.Test/Test/Globals.cs +++ b/src.csharp/AlphaTab.Test/Test/Globals.cs @@ -1,4 +1,5 @@ -using Microsoft.VisualStudio.TestTools.UnitTesting; +using System; +using Microsoft.VisualStudio.TestTools.UnitTesting; namespace AlphaTab.Test { @@ -9,9 +10,9 @@ public static Expector Expect(T actual) return new Expector(actual); } - public static void Fail(string message) + public static void Fail(object message) { - Assert.Fail(message); + Assert.Fail(Convert.ToString(message)); } } diff --git a/src.csharp/AlphaTab.Test/TestPlatform.cs b/src.csharp/AlphaTab.Test/TestPlatform.cs index 21ba9308a..06280e1a4 100644 --- a/src.csharp/AlphaTab.Test/TestPlatform.cs +++ b/src.csharp/AlphaTab.Test/TestPlatform.cs @@ -1,4 +1,6 @@ -using System.IO; +using System.Collections.Generic; +using System.IO; +using System.Linq; using System.Threading.Tasks; using AlphaTab.Core.EcmaScript; @@ -13,5 +15,12 @@ public static async Task LoadFile(string path) await fs.CopyToAsync(ms); return new Uint8Array(ms.ToArray()); } + + public static Task> ListDirectory(string path) + { + return Task.FromResult((IList)Directory.EnumerateFiles(path) + .Select(Path.GetFileName) + .ToList()); + } } } diff --git a/src.csharp/AlphaTab/Core/EcmaScript/Array.cs b/src.csharp/AlphaTab/Core/EcmaScript/Array.cs new file mode 100644 index 000000000..d6eb420ec --- /dev/null +++ b/src.csharp/AlphaTab/Core/EcmaScript/Array.cs @@ -0,0 +1,19 @@ +using System.Collections; +using System.Collections.Generic; +using System.Linq; + +namespace AlphaTab.Core.EcmaScript +{ + internal static class Array + { + public static bool IsArray(object? o) + { + return o is IList; + } + + public static IList From(IEnumerable x) + { + return x.ToList(); + } + } +} diff --git a/src.csharp/AlphaTab/Core/EcmaScript/DataView.cs b/src.csharp/AlphaTab/Core/EcmaScript/DataView.cs index 483f4f8a9..2cc87959c 100644 --- a/src.csharp/AlphaTab/Core/EcmaScript/DataView.cs +++ b/src.csharp/AlphaTab/Core/EcmaScript/DataView.cs @@ -16,7 +16,7 @@ public void SetUint16(double offset, double value, bool littleEndian) var bytes = BitConverter.GetBytes((ushort) value); if (littleEndian != BitConverter.IsLittleEndian) { - Array.Reverse(bytes); + System.Array.Reverse(bytes); } Buffer.BlockCopy(bytes, 0, _buffer.Raw.Array, _buffer.Raw.Offset + (int) offset, @@ -30,7 +30,7 @@ public double GetInt16(double offset, bool littleEndian) bytes.Length); if (littleEndian != BitConverter.IsLittleEndian) { - Array.Reverse(bytes); + System.Array.Reverse(bytes); } return BitConverter.ToInt16(bytes, 0); @@ -41,7 +41,7 @@ public void SetInt16(double offset, double value, bool littleEndian) var bytes = BitConverter.GetBytes((short) value); if (littleEndian != BitConverter.IsLittleEndian) { - Array.Reverse(bytes); + System.Array.Reverse(bytes); } Buffer.BlockCopy(bytes, 0, _buffer.Raw.Array, _buffer.Raw.Offset + (int) offset, @@ -55,7 +55,7 @@ public double GetUint32(double offset, bool littleEndian) bytes.Length); if (littleEndian != BitConverter.IsLittleEndian) { - Array.Reverse(bytes); + System.Array.Reverse(bytes); } return BitConverter.ToUInt32(bytes, 0); @@ -66,7 +66,7 @@ public void SetInt32(double offset, double value, bool littleEndian) var bytes = BitConverter.GetBytes((int) value); if (littleEndian != BitConverter.IsLittleEndian) { - Array.Reverse(bytes); + System.Array.Reverse(bytes); } Buffer.BlockCopy(bytes, 0, _buffer.Raw.Array, _buffer.Raw.Offset + (int) offset, bytes @@ -80,7 +80,7 @@ public double GetUint16(double offset, bool littleEndian) bytes.Length); if (littleEndian != BitConverter.IsLittleEndian) { - Array.Reverse(bytes); + System.Array.Reverse(bytes); } return BitConverter.ToUInt16(bytes, 0); diff --git a/src.csharp/AlphaTab/Core/EcmaScript/Int32Array.cs b/src.csharp/AlphaTab/Core/EcmaScript/Int32Array.cs index 0ee1ba2c2..993a37cb0 100644 --- a/src.csharp/AlphaTab/Core/EcmaScript/Int32Array.cs +++ b/src.csharp/AlphaTab/Core/EcmaScript/Int32Array.cs @@ -26,7 +26,7 @@ public void Fill(int i) { if (i == 0) { - Array.Clear(_data, 0, _data.Length); + System.Array.Clear(_data, 0, _data.Length); } else { diff --git a/src.csharp/AlphaTab/Core/EcmaScript/Map.cs b/src.csharp/AlphaTab/Core/EcmaScript/Map.cs index 4cfc545a3..8aeb2ce20 100644 --- a/src.csharp/AlphaTab/Core/EcmaScript/Map.cs +++ b/src.csharp/AlphaTab/Core/EcmaScript/Map.cs @@ -5,7 +5,12 @@ namespace AlphaTab.Core.EcmaScript { - public class Map : IEnumerable> + public abstract class Map + { + } + + public class Map : Map, IEnumerable>, + IDictionary where TValue : class? { private readonly Dictionary _data; @@ -49,11 +54,44 @@ public void Delete(TKey key) _data.Remove(key); } + void ICollection>.Add(KeyValuePair item) + { + ((ICollection>) _data).Add(item); + } + public void Clear() { _data.Clear(); } + bool ICollection>.Contains(KeyValuePair item) + { + return ((ICollection>) _data).Contains(item); + } + + void ICollection>.CopyTo(KeyValuePair[] array, + int arrayIndex) + { + ((ICollection>) _data).CopyTo(array, arrayIndex); + } + + bool ICollection>.Remove(KeyValuePair item) + { + return ((ICollection>) _data).Remove(item); + } + + int ICollection>.Count => + ((ICollection>) _data).Count; + + bool ICollection>.IsReadOnly => + ((ICollection>) _data).IsReadOnly; + + IEnumerator> IEnumerable>. + GetEnumerator() + { + return ((IEnumerable>) _data).GetEnumerator(); + } + public IEnumerator> GetEnumerator() { return _data.Select(d => new MapEntry(d.Key, d.Value)).GetEnumerator(); @@ -80,5 +118,39 @@ public void ForEach(Action callback) } } + public IEnumerable Keys() + { + return _data.Keys; + } + + void IDictionary.Add(TKey key, TValue value) + { + _data.Add(key, value); + } + + bool IDictionary.ContainsKey(TKey key) + { + return _data.ContainsKey(key); + } + + bool IDictionary.Remove(TKey key) + { + return _data.Remove(key); + } + + bool IDictionary.TryGetValue(TKey key, out TValue value) + { + return _data.TryGetValue(key, out value); + } + + TValue IDictionary.this[TKey key] + { + get => _data[key]; + set => _data[key] = value; + } + + ICollection IDictionary.Keys => _data.Keys; + + ICollection IDictionary.Values => _data.Values; } } diff --git a/src.csharp/AlphaTab/Core/TypeHelper.cs b/src.csharp/AlphaTab/Core/TypeHelper.cs index 8a8741286..d18083523 100644 --- a/src.csharp/AlphaTab/Core/TypeHelper.cs +++ b/src.csharp/AlphaTab/Core/TypeHelper.cs @@ -54,11 +54,12 @@ public static void Reverse(this IList data) } else if (data is T[] array) { - Array.Reverse(array); + System.Array.Reverse(array); } else { - throw new NotSupportedException("Cannot reverse list of type " + data.GetType().FullName); + throw new NotSupportedException("Cannot reverse list of type " + + data.GetType().FullName); } } @@ -153,17 +154,32 @@ public static void InsertRange(this IList data, int index, IEnumerable public static void Sort(this IList data, Func func) { - if (data is System.Collections.Generic.List l) - { - l.Sort((a, b) => (int) func(a, b)); - } - else if(data is T[] array) + switch (data) { - Array.Sort(array, (a, b) => (int) func(a, b)); + case System.Collections.Generic.List l: + l.Sort((a, b) => (int) func(a, b)); + break; + case T[] array: + System.Array.Sort(array, (a, b) => (int) func(a, b)); + break; + default: + throw new NotSupportedException("Cannot sort list of type " + + data.GetType().FullName); } - else + } + public static void Sort(this IList data) + { + switch (data) { - throw new NotSupportedException("Cannot sort list of type " + data.GetType().FullName); + case List l: + l.Sort(); + break; + case T[] array: + System.Array.Sort(array); + break; + default: + throw new NotSupportedException("Cannot sort list of type " + + data.GetType().FullName); } } @@ -185,7 +201,7 @@ public static int CharCodeAt(this string s, double index) { return s[(int) index]; } - + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static string CharAt(this string s, double index) { @@ -252,5 +268,45 @@ public static bool IsTruthy(double s) { return !double.IsNaN(s) && s != 0; } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static IList Map(this IList source, + Func func) + { + return source.Select(func).ToList(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static string Substring(this string s, double startIndex, double endIndex) + { + return s.Substring((int) startIndex, (int) (endIndex - startIndex)); + } + + public static string TypeOf(object? actual) + { + switch (actual) + { + case string _: + return "string"; + case bool _: + return "boolean"; + case byte _: + case short _: + case int _: + case long _: + case sbyte _: + case ushort _: + case uint _: + case ulong _: + case float _: + case double _: + case Enum _: + return "number"; + case null: + return "undefined"; + default: + return "object"; + } + } } } diff --git a/src.csharp/AlphaTab/Io/JsonHelper.cs b/src.csharp/AlphaTab/Io/JsonHelper.cs new file mode 100644 index 000000000..9e8738fcb --- /dev/null +++ b/src.csharp/AlphaTab/Io/JsonHelper.cs @@ -0,0 +1,52 @@ +using System; +using System.Collections; +using System.Collections.Generic; + +namespace AlphaTab.Io +{ + internal static partial class JsonHelper + { + // ReSharper disable once UnusedParameter.Global + public static T? ParseEnum(object? o, Type _) where T : struct + { + switch (o) + { + case string s: + return Enum.TryParse(s, true, out T value) ? value : new T?(); + case double d: + return (T) (object) (int) d; + case int _: + return (T) o; + case null: + return null; + case T t: + return t; + } + + throw new AlphaTabError(AlphaTabErrorType.Format, $"Could not parse enum value '{o}' [({o.GetType()}]"); + } + + public static void ForEach(object o, Action func) + { + switch (o) + { + case IDictionary d: + foreach (var kvp in d) + { + func(kvp.Value, kvp.Key); + } + + break; + case IDictionary d: + foreach (DictionaryEntry entry in d) + { + func(entry.Value, Convert.ToString(entry.Key)); + } + + break; + } + + // ignore + } + } +} diff --git a/src/DisplaySettings.ts b/src/DisplaySettings.ts index ff645b69f..2d3d8fafa 100644 --- a/src/DisplaySettings.ts +++ b/src/DisplaySettings.ts @@ -88,11 +88,12 @@ export class DisplaySettings { /** * Gets or sets the resources used during rendering. This defines all fonts and colors used. + * @json_partial_names */ public resources: RenderingResources = new RenderingResources(); /** * Gets or sets the padding between the music notation and the border. */ - public padding: Float32Array | null = null; + public padding: number[] | null = null; } diff --git a/src/PlayerSettings.ts b/src/PlayerSettings.ts index 7851d250a..eb09ca1e7 100644 --- a/src/PlayerSettings.ts +++ b/src/PlayerSettings.ts @@ -96,7 +96,7 @@ export class SlidePlaybackSettings { */ export class PlayerSettings { /** - * Gets or sets the URl of the sound font to be loaded. + * Gets or sets the URL of the sound font to be loaded. */ public soundFont: string | null = null; @@ -153,11 +153,13 @@ export class PlayerSettings { /** * Gets or sets the settings on how the vibrato audio is generated. + * @json_partial_names */ public readonly vibrato: VibratoPlaybackSettings = new VibratoPlaybackSettings(); /** * Gets or sets the setitngs on how the slide audio is generated. + * @json_partial_names */ public readonly slide: SlidePlaybackSettings = new SlidePlaybackSettings(); diff --git a/src/Settings.ts b/src/Settings.ts index 0d1942ab1..9ff8e01fa 100644 --- a/src/Settings.ts +++ b/src/Settings.ts @@ -9,73 +9,38 @@ import { PlayerSettings } from '@src/PlayerSettings'; * @json */ export class Settings { - /** - * @target web - */ - public static fromJson(json: any): Settings { - // dynamically implemented via AST transformer - return new Settings(); - } - - /** - * @target web - */ - public fillFromJson(json: any): void { - // dynamically implemented via AST transformer - } - - /** - * @target web - */ - public static toJson(settings: Settings): unknown { - // dynamically implemented via AST transformer - return null; - } - - /** - * @target web - */ - public setProperty(property: string, value: any): boolean { - // dynamically implemented via macro - return false; - } - - /** - * @target web - */ - public fillFromDataAttributes(dataAttributes: Map): void { - dataAttributes.forEach((v, k) => { - this.setProperty(k.toLowerCase(), v); - }); - } - /** * The core settings control the general behavior of alphatab like * what modules are active. * @json_on_parent + * @json_partial_names */ public readonly core: CoreSettings = new CoreSettings(); /** * The display settings control how the general layout and display of alphaTab is done. * @json_on_parent + * @json_partial_names */ public readonly display: DisplaySettings = new DisplaySettings(); /** * The notation settings control how various music notation elements are shown and behaving. + * @json_partial_names */ public readonly notation: NotationSettings = new NotationSettings(); /** * All settings related to importers that decode file formats. + * @json_partial_names */ public readonly importer: ImporterSettings = new ImporterSettings(); /** * Contains all player related settings + * @json_partial_names */ - public readonly player: PlayerSettings = new PlayerSettings(); + public player: PlayerSettings = new PlayerSettings(); public setSongBookModeSettings(): void { this.notation.notationMode = NotationMode.SongBook; diff --git a/src/generated/CoreSettingsSerializer.ts b/src/generated/CoreSettingsSerializer.ts new file mode 100644 index 000000000..cfa36d789 --- /dev/null +++ b/src/generated/CoreSettingsSerializer.ts @@ -0,0 +1,85 @@ +// +// This code was auto-generated. +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +import { CoreSettings } from "@src/CoreSettings"; +import { JsonHelper } from "@src/io/JsonHelper"; +import { LogLevel } from "@src/LogLevel"; +export class CoreSettingsSerializer { + public static fromJson(obj: CoreSettings, m: unknown): void { + if (!m) { + return; + } + JsonHelper.forEach(m, (v, k) => this.setProperty(obj, k.toLowerCase(), v)); + } + public static toJson(obj: CoreSettings | null): Map | null { + if (!obj) { + return null; + } + const o = new Map(); + /*@target web*/ + o.set("scriptFile", obj.scriptFile); + /*@target web*/ + o.set("fontDirectory", obj.fontDirectory); + /*@target web*/ + o.set("file", obj.file); + /*@target web*/ + o.set("tex", obj.tex); + /*@target web*/ + o.set("tracks", obj.tracks); + /*@target web*/ + o.set("visibilityCheckInterval", obj.visibilityCheckInterval); + o.set("enableLazyLoading", obj.enableLazyLoading); + o.set("engine", obj.engine); + o.set("logLevel", (obj.logLevel as number)); + o.set("useWorkers", obj.useWorkers); + o.set("includeNoteBounds", obj.includeNoteBounds); + return o; + } + public static setProperty(obj: CoreSettings, property: string, v: unknown): boolean { + switch (property) { + /*@target web*/ + case "scriptfile": + obj.scriptFile = (v as string | null); + return true; + /*@target web*/ + case "fontdirectory": + obj.fontDirectory = (v as string | null); + return true; + /*@target web*/ + case "file": + obj.file = (v as string | null); + return true; + /*@target web*/ + case "tex": + obj.tex = (v as boolean); + return true; + /*@target web*/ + case "tracks": + obj.tracks = (v as unknown); + return true; + /*@target web*/ + case "visibilitycheckinterval": + obj.visibilityCheckInterval = (v as number); + return true; + case "enablelazyloading": + obj.enableLazyLoading = (v as boolean); + return true; + case "engine": + obj.engine = (v as string); + return true; + case "loglevel": + obj.logLevel = (JsonHelper.parseEnum(v, LogLevel)!); + return true; + case "useworkers": + obj.useWorkers = (v as boolean); + return true; + case "includenotebounds": + obj.includeNoteBounds = (v as boolean); + return true; + } + return false; + } +} + diff --git a/src/generated/DisplaySettingsSerializer.ts b/src/generated/DisplaySettingsSerializer.ts new file mode 100644 index 000000000..a44eaf92d --- /dev/null +++ b/src/generated/DisplaySettingsSerializer.ts @@ -0,0 +1,81 @@ +// +// This code was auto-generated. +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +import { DisplaySettings } from "@src/DisplaySettings"; +import { JsonHelper } from "@src/io/JsonHelper"; +import { RenderingResourcesSerializer } from "@src/generated/RenderingResourcesSerializer"; +import { LayoutMode } from "@src/DisplaySettings"; +import { StaveProfile } from "@src/DisplaySettings"; +export class DisplaySettingsSerializer { + public static fromJson(obj: DisplaySettings, m: unknown): void { + if (!m) { + return; + } + JsonHelper.forEach(m, (v, k) => this.setProperty(obj, k.toLowerCase(), v)); + } + public static toJson(obj: DisplaySettings | null): Map | null { + if (!obj) { + return null; + } + const o = new Map(); + o.set("scale", obj.scale); + o.set("stretchForce", obj.stretchForce); + o.set("layoutMode", (obj.layoutMode as number)); + o.set("staveProfile", (obj.staveProfile as number)); + o.set("barsPerRow", obj.barsPerRow); + o.set("startBar", obj.startBar); + o.set("barCount", obj.barCount); + o.set("barCountPerPartial", obj.barCountPerPartial); + o.set("resources", RenderingResourcesSerializer.toJson(obj.resources)); + o.set("padding", obj.padding); + return o; + } + public static setProperty(obj: DisplaySettings, property: string, v: unknown): boolean { + switch (property) { + case "scale": + obj.scale = (v as number); + return true; + case "stretchforce": + obj.stretchForce = (v as number); + return true; + case "layoutmode": + obj.layoutMode = (JsonHelper.parseEnum(v, LayoutMode)!); + return true; + case "staveprofile": + obj.staveProfile = (JsonHelper.parseEnum(v, StaveProfile)!); + return true; + case "barsperrow": + obj.barsPerRow = (v as number); + return true; + case "startbar": + obj.startBar = (v as number); + return true; + case "barcount": + obj.barCount = (v as number); + return true; + case "barcountperpartial": + obj.barCountPerPartial = (v as number); + return true; + case "padding": + obj.padding = (v as number[] | null); + return true; + } + if (["resources"].indexOf(property) >= 0) { + RenderingResourcesSerializer.fromJson(obj.resources, (v as Map)); + return true; + } + else { + for (const c of ["resources"]) { + if (property.indexOf(c) === 0) { + if (RenderingResourcesSerializer.setProperty(obj.resources, property.substring(c.length), v)) { + return true; + } + } + } + } + return false; + } +} + diff --git a/src/generated/ImporterSettingsSerializer.ts b/src/generated/ImporterSettingsSerializer.ts new file mode 100644 index 000000000..694f9991f --- /dev/null +++ b/src/generated/ImporterSettingsSerializer.ts @@ -0,0 +1,36 @@ +// +// This code was auto-generated. +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +import { ImporterSettings } from "@src/ImporterSettings"; +import { JsonHelper } from "@src/io/JsonHelper"; +export class ImporterSettingsSerializer { + public static fromJson(obj: ImporterSettings, m: unknown): void { + if (!m) { + return; + } + JsonHelper.forEach(m, (v, k) => this.setProperty(obj, k.toLowerCase(), v)); + } + public static toJson(obj: ImporterSettings | null): Map | null { + if (!obj) { + return null; + } + const o = new Map(); + o.set("encoding", obj.encoding); + o.set("mergePartGroupsInMusicXml", obj.mergePartGroupsInMusicXml); + return o; + } + public static setProperty(obj: ImporterSettings, property: string, v: unknown): boolean { + switch (property) { + case "encoding": + obj.encoding = (v as string); + return true; + case "mergepartgroupsinmusicxml": + obj.mergePartGroupsInMusicXml = (v as boolean); + return true; + } + return false; + } +} + diff --git a/src/generated/NotationSettingsSerializer.ts b/src/generated/NotationSettingsSerializer.ts new file mode 100644 index 000000000..deba6f6e7 --- /dev/null +++ b/src/generated/NotationSettingsSerializer.ts @@ -0,0 +1,83 @@ +// +// This code was auto-generated. +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +import { NotationSettings } from "@src/NotationSettings"; +import { JsonHelper } from "@src/io/JsonHelper"; +import { NotationMode } from "@src/NotationSettings"; +import { FingeringMode } from "@src/NotationSettings"; +import { NotationElement } from "@src/NotationSettings"; +import { TabRhythmMode } from "@src/NotationSettings"; +export class NotationSettingsSerializer { + public static fromJson(obj: NotationSettings, m: unknown): void { + if (!m) { + return; + } + JsonHelper.forEach(m, (v, k) => this.setProperty(obj, k.toLowerCase(), v)); + } + public static toJson(obj: NotationSettings | null): Map | null { + if (!obj) { + return null; + } + const o = new Map(); + o.set("notationMode", (obj.notationMode as number)); + o.set("fingeringMode", (obj.fingeringMode as number)); + { + const m = new Map(); + o.set("elements", m); + obj.elements.forEach((v, k) => m.set(k.toString(), v)); + } + o.set("rhythmMode", (obj.rhythmMode as number)); + o.set("rhythmHeight", obj.rhythmHeight); + o.set("transpositionPitches", obj.transpositionPitches); + o.set("displayTranspositionPitches", obj.displayTranspositionPitches); + o.set("smallGraceTabNotes", obj.smallGraceTabNotes); + o.set("extendBendArrowsOnTiedNotes", obj.extendBendArrowsOnTiedNotes); + o.set("extendLineEffectsToBeatEnd", obj.extendLineEffectsToBeatEnd); + o.set("slurHeight", obj.slurHeight); + return o; + } + public static setProperty(obj: NotationSettings, property: string, v: unknown): boolean { + switch (property) { + case "notationmode": + obj.notationMode = (JsonHelper.parseEnum(v, NotationMode)!); + return true; + case "fingeringmode": + obj.fingeringMode = (JsonHelper.parseEnum(v, FingeringMode)!); + return true; + case "elements": + obj.elements = new Map(); + (v as Map).forEach((v, k) => { + obj.elements.set((JsonHelper.parseEnum(k, NotationElement)!), (v as boolean)); + }); + return true; + case "rhythmmode": + obj.rhythmMode = (JsonHelper.parseEnum(v, TabRhythmMode)!); + return true; + case "rhythmheight": + obj.rhythmHeight = (v as number); + return true; + case "transpositionpitches": + obj.transpositionPitches = (v as number[]); + return true; + case "displaytranspositionpitches": + obj.displayTranspositionPitches = (v as number[]); + return true; + case "smallgracetabnotes": + obj.smallGraceTabNotes = (v as boolean); + return true; + case "extendbendarrowsontiednotes": + obj.extendBendArrowsOnTiedNotes = (v as boolean); + return true; + case "extendlineeffectstobeatend": + obj.extendLineEffectsToBeatEnd = (v as boolean); + return true; + case "slurheight": + obj.slurHeight = (v as number); + return true; + } + return false; + } +} + diff --git a/src/generated/PlayerSettingsSerializer.ts b/src/generated/PlayerSettingsSerializer.ts new file mode 100644 index 000000000..d761cc5d3 --- /dev/null +++ b/src/generated/PlayerSettingsSerializer.ts @@ -0,0 +1,107 @@ +// +// This code was auto-generated. +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +import { PlayerSettings } from "@src/PlayerSettings"; +import { JsonHelper } from "@src/io/JsonHelper"; +import { VibratoPlaybackSettingsSerializer } from "@src/generated/VibratoPlaybackSettingsSerializer"; +import { SlidePlaybackSettingsSerializer } from "@src/generated/SlidePlaybackSettingsSerializer"; +import { ScrollMode } from "@src/PlayerSettings"; +export class PlayerSettingsSerializer { + public static fromJson(obj: PlayerSettings, m: unknown): void { + if (!m) { + return; + } + JsonHelper.forEach(m, (v, k) => this.setProperty(obj, k.toLowerCase(), v)); + } + public static toJson(obj: PlayerSettings | null): Map | null { + if (!obj) { + return null; + } + const o = new Map(); + o.set("soundFont", obj.soundFont); + o.set("scrollElement", obj.scrollElement); + o.set("enablePlayer", obj.enablePlayer); + o.set("enableCursor", obj.enableCursor); + o.set("enableUserInteraction", obj.enableUserInteraction); + o.set("scrollOffsetX", obj.scrollOffsetX); + o.set("scrollOffsetY", obj.scrollOffsetY); + o.set("scrollMode", (obj.scrollMode as number)); + o.set("scrollSpeed", obj.scrollSpeed); + o.set("songBookBendDuration", obj.songBookBendDuration); + o.set("songBookDipDuration", obj.songBookDipDuration); + o.set("vibrato", VibratoPlaybackSettingsSerializer.toJson(obj.vibrato)); + o.set("slide", SlidePlaybackSettingsSerializer.toJson(obj.slide)); + o.set("playTripletFeel", obj.playTripletFeel); + return o; + } + public static setProperty(obj: PlayerSettings, property: string, v: unknown): boolean { + switch (property) { + case "soundfont": + obj.soundFont = (v as string | null); + return true; + case "scrollelement": + obj.scrollElement = (v as string); + return true; + case "enableplayer": + obj.enablePlayer = (v as boolean); + return true; + case "enablecursor": + obj.enableCursor = (v as boolean); + return true; + case "enableuserinteraction": + obj.enableUserInteraction = (v as boolean); + return true; + case "scrolloffsetx": + obj.scrollOffsetX = (v as number); + return true; + case "scrolloffsety": + obj.scrollOffsetY = (v as number); + return true; + case "scrollmode": + obj.scrollMode = (JsonHelper.parseEnum(v, ScrollMode)!); + return true; + case "scrollspeed": + obj.scrollSpeed = (v as number); + return true; + case "songbookbendduration": + obj.songBookBendDuration = (v as number); + return true; + case "songbookdipduration": + obj.songBookDipDuration = (v as number); + return true; + case "playtripletfeel": + obj.playTripletFeel = (v as boolean); + return true; + } + if (["vibrato"].indexOf(property) >= 0) { + VibratoPlaybackSettingsSerializer.fromJson(obj.vibrato, (v as Map)); + return true; + } + else { + for (const c of ["vibrato"]) { + if (property.indexOf(c) === 0) { + if (VibratoPlaybackSettingsSerializer.setProperty(obj.vibrato, property.substring(c.length), v)) { + return true; + } + } + } + } + if (["slide"].indexOf(property) >= 0) { + SlidePlaybackSettingsSerializer.fromJson(obj.slide, (v as Map)); + return true; + } + else { + for (const c of ["slide"]) { + if (property.indexOf(c) === 0) { + if (SlidePlaybackSettingsSerializer.setProperty(obj.slide, property.substring(c.length), v)) { + return true; + } + } + } + } + return false; + } +} + diff --git a/src/generated/RenderingResourcesSerializer.ts b/src/generated/RenderingResourcesSerializer.ts new file mode 100644 index 000000000..5b5e7d52b --- /dev/null +++ b/src/generated/RenderingResourcesSerializer.ts @@ -0,0 +1,98 @@ +// +// This code was auto-generated. +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +import { RenderingResources } from "@src/RenderingResources"; +import { JsonHelper } from "@src/io/JsonHelper"; +import { Font } from "@src/model/Font"; +import { Color } from "@src/model/Color"; +export class RenderingResourcesSerializer { + public static fromJson(obj: RenderingResources, m: unknown): void { + if (!m) { + return; + } + JsonHelper.forEach(m, (v, k) => this.setProperty(obj, k.toLowerCase(), v)); + } + public static toJson(obj: RenderingResources | null): Map | null { + if (!obj) { + return null; + } + const o = new Map(); + o.set("copyrightFont", Font.toJson(obj.copyrightFont)); + o.set("titleFont", Font.toJson(obj.titleFont)); + o.set("subTitleFont", Font.toJson(obj.subTitleFont)); + o.set("wordsFont", Font.toJson(obj.wordsFont)); + o.set("effectFont", Font.toJson(obj.effectFont)); + o.set("fretboardNumberFont", Font.toJson(obj.fretboardNumberFont)); + o.set("tablatureFont", Font.toJson(obj.tablatureFont)); + o.set("graceFont", Font.toJson(obj.graceFont)); + o.set("staffLineColor", Color.toJson(obj.staffLineColor)); + o.set("barSeparatorColor", Color.toJson(obj.barSeparatorColor)); + o.set("barNumberFont", Font.toJson(obj.barNumberFont)); + o.set("barNumberColor", Color.toJson(obj.barNumberColor)); + o.set("fingeringFont", Font.toJson(obj.fingeringFont)); + o.set("markerFont", Font.toJson(obj.markerFont)); + o.set("mainGlyphColor", Color.toJson(obj.mainGlyphColor)); + o.set("secondaryGlyphColor", Color.toJson(obj.secondaryGlyphColor)); + o.set("scoreInfoColor", Color.toJson(obj.scoreInfoColor)); + return o; + } + public static setProperty(obj: RenderingResources, property: string, v: unknown): boolean { + switch (property) { + case "copyrightfont": + obj.copyrightFont = (Font.fromJson(v)!); + return true; + case "titlefont": + obj.titleFont = (Font.fromJson(v)!); + return true; + case "subtitlefont": + obj.subTitleFont = (Font.fromJson(v)!); + return true; + case "wordsfont": + obj.wordsFont = (Font.fromJson(v)!); + return true; + case "effectfont": + obj.effectFont = (Font.fromJson(v)!); + return true; + case "fretboardnumberfont": + obj.fretboardNumberFont = (Font.fromJson(v)!); + return true; + case "tablaturefont": + obj.tablatureFont = (Font.fromJson(v)!); + return true; + case "gracefont": + obj.graceFont = (Font.fromJson(v)!); + return true; + case "stafflinecolor": + obj.staffLineColor = (Color.fromJson(v)!); + return true; + case "barseparatorcolor": + obj.barSeparatorColor = (Color.fromJson(v)!); + return true; + case "barnumberfont": + obj.barNumberFont = (Font.fromJson(v)!); + return true; + case "barnumbercolor": + obj.barNumberColor = (Color.fromJson(v)!); + return true; + case "fingeringfont": + obj.fingeringFont = (Font.fromJson(v)!); + return true; + case "markerfont": + obj.markerFont = (Font.fromJson(v)!); + return true; + case "mainglyphcolor": + obj.mainGlyphColor = (Color.fromJson(v)!); + return true; + case "secondaryglyphcolor": + obj.secondaryGlyphColor = (Color.fromJson(v)!); + return true; + case "scoreinfocolor": + obj.scoreInfoColor = (Color.fromJson(v)!); + return true; + } + return false; + } +} + diff --git a/src/generated/SettingsSerializer.ts b/src/generated/SettingsSerializer.ts new file mode 100644 index 000000000..922f10bb4 --- /dev/null +++ b/src/generated/SettingsSerializer.ts @@ -0,0 +1,101 @@ +// +// This code was auto-generated. +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +import { Settings } from "@src/Settings"; +import { JsonHelper } from "@src/io/JsonHelper"; +import { CoreSettingsSerializer } from "@src/generated/CoreSettingsSerializer"; +import { DisplaySettingsSerializer } from "@src/generated/DisplaySettingsSerializer"; +import { NotationSettingsSerializer } from "@src/generated/NotationSettingsSerializer"; +import { ImporterSettingsSerializer } from "@src/generated/ImporterSettingsSerializer"; +import { PlayerSettingsSerializer } from "@src/generated/PlayerSettingsSerializer"; +export class SettingsSerializer { + public static fromJson(obj: Settings, m: unknown): void { + if (!m) { + return; + } + JsonHelper.forEach(m, (v, k) => this.setProperty(obj, k.toLowerCase(), v)); + } + public static toJson(obj: Settings | null): Map | null { + if (!obj) { + return null; + } + const o = new Map(); + o.set("core", CoreSettingsSerializer.toJson(obj.core)); + o.set("display", DisplaySettingsSerializer.toJson(obj.display)); + o.set("notation", NotationSettingsSerializer.toJson(obj.notation)); + o.set("importer", ImporterSettingsSerializer.toJson(obj.importer)); + o.set("player", PlayerSettingsSerializer.toJson(obj.player)); + return o; + } + public static setProperty(obj: Settings, property: string, v: unknown): boolean { + if (["core", ""].indexOf(property) >= 0) { + CoreSettingsSerializer.fromJson(obj.core, (v as Map)); + return true; + } + else { + for (const c of ["core", ""]) { + if (property.indexOf(c) === 0) { + if (CoreSettingsSerializer.setProperty(obj.core, property.substring(c.length), v)) { + return true; + } + } + } + } + if (["display", ""].indexOf(property) >= 0) { + DisplaySettingsSerializer.fromJson(obj.display, (v as Map)); + return true; + } + else { + for (const c of ["display", ""]) { + if (property.indexOf(c) === 0) { + if (DisplaySettingsSerializer.setProperty(obj.display, property.substring(c.length), v)) { + return true; + } + } + } + } + if (["notation"].indexOf(property) >= 0) { + NotationSettingsSerializer.fromJson(obj.notation, (v as Map)); + return true; + } + else { + for (const c of ["notation"]) { + if (property.indexOf(c) === 0) { + if (NotationSettingsSerializer.setProperty(obj.notation, property.substring(c.length), v)) { + return true; + } + } + } + } + if (["importer"].indexOf(property) >= 0) { + ImporterSettingsSerializer.fromJson(obj.importer, (v as Map)); + return true; + } + else { + for (const c of ["importer"]) { + if (property.indexOf(c) === 0) { + if (ImporterSettingsSerializer.setProperty(obj.importer, property.substring(c.length), v)) { + return true; + } + } + } + } + if (["player"].indexOf(property) >= 0) { + PlayerSettingsSerializer.fromJson(obj.player, (v as Map)); + return true; + } + else { + for (const c of ["player"]) { + if (property.indexOf(c) === 0) { + if (PlayerSettingsSerializer.setProperty(obj.player, property.substring(c.length), v)) { + return true; + } + } + } + } + return false; + } +} + diff --git a/src/generated/SlidePlaybackSettingsSerializer.ts b/src/generated/SlidePlaybackSettingsSerializer.ts new file mode 100644 index 000000000..6ddbfac0e --- /dev/null +++ b/src/generated/SlidePlaybackSettingsSerializer.ts @@ -0,0 +1,40 @@ +// +// This code was auto-generated. +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +import { SlidePlaybackSettings } from "@src/PlayerSettings"; +import { JsonHelper } from "@src/io/JsonHelper"; +export class SlidePlaybackSettingsSerializer { + public static fromJson(obj: SlidePlaybackSettings, m: unknown): void { + if (!m) { + return; + } + JsonHelper.forEach(m, (v, k) => this.setProperty(obj, k.toLowerCase(), v)); + } + public static toJson(obj: SlidePlaybackSettings | null): Map | null { + if (!obj) { + return null; + } + const o = new Map(); + o.set("simpleSlidePitchOffset", obj.simpleSlidePitchOffset); + o.set("simpleSlideDurationRatio", obj.simpleSlideDurationRatio); + o.set("shiftSlideDurationRatio", obj.shiftSlideDurationRatio); + return o; + } + public static setProperty(obj: SlidePlaybackSettings, property: string, v: unknown): boolean { + switch (property) { + case "simpleslidepitchoffset": + obj.simpleSlidePitchOffset = (v as number); + return true; + case "simpleslidedurationratio": + obj.simpleSlideDurationRatio = (v as number); + return true; + case "shiftslidedurationratio": + obj.shiftSlideDurationRatio = (v as number); + return true; + } + return false; + } +} + diff --git a/src/generated/VibratoPlaybackSettingsSerializer.ts b/src/generated/VibratoPlaybackSettingsSerializer.ts new file mode 100644 index 000000000..fc540d62d --- /dev/null +++ b/src/generated/VibratoPlaybackSettingsSerializer.ts @@ -0,0 +1,60 @@ +// +// This code was auto-generated. +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +import { VibratoPlaybackSettings } from "@src/PlayerSettings"; +import { JsonHelper } from "@src/io/JsonHelper"; +export class VibratoPlaybackSettingsSerializer { + public static fromJson(obj: VibratoPlaybackSettings, m: unknown): void { + if (!m) { + return; + } + JsonHelper.forEach(m, (v, k) => this.setProperty(obj, k.toLowerCase(), v)); + } + public static toJson(obj: VibratoPlaybackSettings | null): Map | null { + if (!obj) { + return null; + } + const o = new Map(); + o.set("noteWideLength", obj.noteWideLength); + o.set("noteWideAmplitude", obj.noteWideAmplitude); + o.set("noteSlightLength", obj.noteSlightLength); + o.set("noteSlightAmplitude", obj.noteSlightAmplitude); + o.set("beatWideLength", obj.beatWideLength); + o.set("beatWideAmplitude", obj.beatWideAmplitude); + o.set("beatSlightLength", obj.beatSlightLength); + o.set("beatSlightAmplitude", obj.beatSlightAmplitude); + return o; + } + public static setProperty(obj: VibratoPlaybackSettings, property: string, v: unknown): boolean { + switch (property) { + case "notewidelength": + obj.noteWideLength = (v as number); + return true; + case "notewideamplitude": + obj.noteWideAmplitude = (v as number); + return true; + case "noteslightlength": + obj.noteSlightLength = (v as number); + return true; + case "noteslightamplitude": + obj.noteSlightAmplitude = (v as number); + return true; + case "beatwidelength": + obj.beatWideLength = (v as number); + return true; + case "beatwideamplitude": + obj.beatWideAmplitude = (v as number); + return true; + case "beatslightlength": + obj.beatSlightLength = (v as number); + return true; + case "beatslightamplitude": + obj.beatSlightAmplitude = (v as number); + return true; + } + return false; + } +} + diff --git a/src/generated/model/AutomationCloner.ts b/src/generated/model/AutomationCloner.ts new file mode 100644 index 000000000..fc0e50ff6 --- /dev/null +++ b/src/generated/model/AutomationCloner.ts @@ -0,0 +1,18 @@ +// +// This code was auto-generated. +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +import { Automation } from "@src/model/Automation"; +export class AutomationCloner { + public static clone(original: Automation): Automation { + const clone = new Automation(); + clone.isLinear = original.isLinear; + clone.type = original.type; + clone.value = original.value; + clone.ratioPosition = original.ratioPosition; + clone.text = original.text; + return clone; + } +} + diff --git a/src/generated/model/AutomationSerializer.ts b/src/generated/model/AutomationSerializer.ts new file mode 100644 index 000000000..ce0d82a92 --- /dev/null +++ b/src/generated/model/AutomationSerializer.ts @@ -0,0 +1,49 @@ +// +// This code was auto-generated. +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +import { Automation } from "@src/model/Automation"; +import { JsonHelper } from "@src/io/JsonHelper"; +import { AutomationType } from "@src/model/Automation"; +export class AutomationSerializer { + public static fromJson(obj: Automation, m: unknown): void { + if (!m) { + return; + } + JsonHelper.forEach(m, (v, k) => this.setProperty(obj, k.toLowerCase(), v)); + } + public static toJson(obj: Automation | null): Map | null { + if (!obj) { + return null; + } + const o = new Map(); + o.set("isLinear", obj.isLinear); + o.set("type", (obj.type as number)); + o.set("value", obj.value); + o.set("ratioPosition", obj.ratioPosition); + o.set("text", obj.text); + return o; + } + public static setProperty(obj: Automation, property: string, v: unknown): boolean { + switch (property) { + case "islinear": + obj.isLinear = (v as boolean); + return true; + case "type": + obj.type = (JsonHelper.parseEnum(v, AutomationType)!); + return true; + case "value": + obj.value = (v as number); + return true; + case "ratioposition": + obj.ratioPosition = (v as number); + return true; + case "text": + obj.text = (v as string); + return true; + } + return false; + } +} + diff --git a/src/generated/model/BarSerializer.ts b/src/generated/model/BarSerializer.ts new file mode 100644 index 000000000..8bc18677e --- /dev/null +++ b/src/generated/model/BarSerializer.ts @@ -0,0 +1,58 @@ +// +// This code was auto-generated. +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +import { Bar } from "@src/model/Bar"; +import { JsonHelper } from "@src/io/JsonHelper"; +import { VoiceSerializer } from "@src/generated/model/VoiceSerializer"; +import { Clef } from "@src/model/Clef"; +import { Ottavia } from "@src/model/Ottavia"; +import { Voice } from "@src/model/Voice"; +import { SimileMark } from "@src/model/SimileMark"; +export class BarSerializer { + public static fromJson(obj: Bar, m: unknown): void { + if (!m) { + return; + } + JsonHelper.forEach(m, (v, k) => this.setProperty(obj, k.toLowerCase(), v)); + } + public static toJson(obj: Bar | null): Map | null { + if (!obj) { + return null; + } + const o = new Map(); + o.set("id", obj.id); + o.set("clef", (obj.clef as number)); + o.set("clefOttava", (obj.clefOttava as number)); + o.set("voices", obj.voices.map(i => VoiceSerializer.toJson(i))); + o.set("simileMark", (obj.simileMark as number)); + return o; + } + public static setProperty(obj: Bar, property: string, v: unknown): boolean { + switch (property) { + case "id": + obj.id = (v as number); + return true; + case "clef": + obj.clef = (JsonHelper.parseEnum(v, Clef)!); + return true; + case "clefottava": + obj.clefOttava = (JsonHelper.parseEnum(v, Ottavia)!); + return true; + case "voices": + obj.voices = []; + for (const o of (v as (Map | null)[])) { + const i = new Voice(); + VoiceSerializer.fromJson(i, o) + obj.addVoice(i); + } + return true; + case "similemark": + obj.simileMark = (JsonHelper.parseEnum(v, SimileMark)!); + return true; + } + return false; + } +} + diff --git a/src/generated/model/BeatCloner.ts b/src/generated/model/BeatCloner.ts new file mode 100644 index 000000000..f9396dbb8 --- /dev/null +++ b/src/generated/model/BeatCloner.ts @@ -0,0 +1,65 @@ +// +// This code was auto-generated. +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +import { Beat } from "@src/model/Beat"; +import { NoteCloner } from "./NoteCloner"; +import { AutomationCloner } from "./AutomationCloner"; +import { BendPointCloner } from "./BendPointCloner"; +export class BeatCloner { + public static clone(original: Beat): Beat { + const clone = new Beat(); + clone.index = original.index; + clone.notes = []; + for (const i of original.notes) { + clone.addNote(NoteCloner.clone(i)); + } + clone.isEmpty = original.isEmpty; + clone.whammyStyle = original.whammyStyle; + clone.ottava = original.ottava; + clone.isLegatoOrigin = original.isLegatoOrigin; + clone.duration = original.duration; + clone.isLetRing = original.isLetRing; + clone.isPalmMute = original.isPalmMute; + clone.automations = []; + for (const i of original.automations) { + clone.automations.push(AutomationCloner.clone(i)); + } + clone.dots = original.dots; + clone.fadeIn = original.fadeIn; + clone.lyrics = original.lyrics ? original.lyrics.slice() : null; + clone.hasRasgueado = original.hasRasgueado; + clone.pop = original.pop; + clone.slap = original.slap; + clone.tap = original.tap; + clone.text = original.text; + clone.brushType = original.brushType; + clone.brushDuration = original.brushDuration; + clone.tupletDenominator = original.tupletDenominator; + clone.tupletNumerator = original.tupletNumerator; + clone.isContinuedWhammy = original.isContinuedWhammy; + clone.whammyBarType = original.whammyBarType; + clone.whammyBarPoints = []; + for (const i of original.whammyBarPoints) { + clone.addWhammyBarPoint(BendPointCloner.clone(i)); + } + clone.vibrato = original.vibrato; + clone.chordId = original.chordId; + clone.graceType = original.graceType; + clone.pickStroke = original.pickStroke; + clone.tremoloSpeed = original.tremoloSpeed; + clone.crescendo = original.crescendo; + clone.displayStart = original.displayStart; + clone.playbackStart = original.playbackStart; + clone.displayDuration = original.displayDuration; + clone.playbackDuration = original.playbackDuration; + clone.dynamics = original.dynamics; + clone.invertBeamDirection = original.invertBeamDirection; + clone.preferredBeamDirection = original.preferredBeamDirection; + clone.isEffectSlurOrigin = original.isEffectSlurOrigin; + clone.beamingMode = original.beamingMode; + return clone; + } +} + diff --git a/src/generated/model/BeatSerializer.ts b/src/generated/model/BeatSerializer.ts new file mode 100644 index 000000000..bd7a1037c --- /dev/null +++ b/src/generated/model/BeatSerializer.ts @@ -0,0 +1,209 @@ +// +// This code was auto-generated. +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +import { Beat } from "@src/model/Beat"; +import { JsonHelper } from "@src/io/JsonHelper"; +import { NoteSerializer } from "@src/generated/model/NoteSerializer"; +import { AutomationSerializer } from "@src/generated/model/AutomationSerializer"; +import { BendPointSerializer } from "@src/generated/model/BendPointSerializer"; +import { Note } from "@src/model/Note"; +import { BendStyle } from "@src/model/BendStyle"; +import { Ottavia } from "@src/model/Ottavia"; +import { Duration } from "@src/model/Duration"; +import { Automation } from "@src/model/Automation"; +import { BrushType } from "@src/model/BrushType"; +import { WhammyType } from "@src/model/WhammyType"; +import { BendPoint } from "@src/model/BendPoint"; +import { VibratoType } from "@src/model/VibratoType"; +import { GraceType } from "@src/model/GraceType"; +import { PickStroke } from "@src/model/PickStroke"; +import { CrescendoType } from "@src/model/CrescendoType"; +import { DynamicValue } from "@src/model/DynamicValue"; +import { BeamDirection } from "@src/rendering/utils/BeamDirection"; +import { BeatBeamingMode } from "@src/model/Beat"; +export class BeatSerializer { + public static fromJson(obj: Beat, m: unknown): void { + if (!m) { + return; + } + JsonHelper.forEach(m, (v, k) => this.setProperty(obj, k.toLowerCase(), v)); + } + public static toJson(obj: Beat | null): Map | null { + if (!obj) { + return null; + } + const o = new Map(); + o.set("id", obj.id); + o.set("notes", obj.notes.map(i => NoteSerializer.toJson(i))); + o.set("isEmpty", obj.isEmpty); + o.set("whammyStyle", (obj.whammyStyle as number)); + o.set("ottava", (obj.ottava as number)); + o.set("isLegatoOrigin", obj.isLegatoOrigin); + o.set("duration", (obj.duration as number)); + o.set("automations", obj.automations.map(i => AutomationSerializer.toJson(i))); + o.set("dots", obj.dots); + o.set("fadeIn", obj.fadeIn); + o.set("lyrics", obj.lyrics); + o.set("hasRasgueado", obj.hasRasgueado); + o.set("pop", obj.pop); + o.set("slap", obj.slap); + o.set("tap", obj.tap); + o.set("text", obj.text); + o.set("brushType", (obj.brushType as number)); + o.set("brushDuration", obj.brushDuration); + o.set("tupletDenominator", obj.tupletDenominator); + o.set("tupletNumerator", obj.tupletNumerator); + o.set("isContinuedWhammy", obj.isContinuedWhammy); + o.set("whammyBarType", (obj.whammyBarType as number)); + o.set("whammyBarPoints", obj.whammyBarPoints.map(i => BendPointSerializer.toJson(i))); + o.set("vibrato", (obj.vibrato as number)); + o.set("chordId", obj.chordId); + o.set("graceType", (obj.graceType as number)); + o.set("pickStroke", (obj.pickStroke as number)); + o.set("tremoloSpeed", (obj.tremoloSpeed as number | null)); + o.set("crescendo", (obj.crescendo as number)); + o.set("displayStart", obj.displayStart); + o.set("playbackStart", obj.playbackStart); + o.set("displayDuration", obj.displayDuration); + o.set("playbackDuration", obj.playbackDuration); + o.set("dynamics", (obj.dynamics as number)); + o.set("invertBeamDirection", obj.invertBeamDirection); + o.set("preferredBeamDirection", (obj.preferredBeamDirection as number | null)); + o.set("beamingMode", (obj.beamingMode as number)); + return o; + } + public static setProperty(obj: Beat, property: string, v: unknown): boolean { + switch (property) { + case "id": + obj.id = (v as number); + return true; + case "notes": + obj.notes = []; + for (const o of (v as (Map | null)[])) { + const i = new Note(); + NoteSerializer.fromJson(i, o) + obj.addNote(i); + } + return true; + case "isempty": + obj.isEmpty = (v as boolean); + return true; + case "whammystyle": + obj.whammyStyle = (JsonHelper.parseEnum(v, BendStyle)!); + return true; + case "ottava": + obj.ottava = (JsonHelper.parseEnum(v, Ottavia)!); + return true; + case "islegatoorigin": + obj.isLegatoOrigin = (v as boolean); + return true; + case "duration": + obj.duration = (JsonHelper.parseEnum(v, Duration)!); + return true; + case "automations": + obj.automations = []; + for (const o of (v as (Map | null)[])) { + const i = new Automation(); + AutomationSerializer.fromJson(i, o) + obj.automations.push(i); + } + return true; + case "dots": + obj.dots = (v as number); + return true; + case "fadein": + obj.fadeIn = (v as boolean); + return true; + case "lyrics": + obj.lyrics = (v as string[] | null); + return true; + case "hasrasgueado": + obj.hasRasgueado = (v as boolean); + return true; + case "pop": + obj.pop = (v as boolean); + return true; + case "slap": + obj.slap = (v as boolean); + return true; + case "tap": + obj.tap = (v as boolean); + return true; + case "text": + obj.text = (v as string | null); + return true; + case "brushtype": + obj.brushType = (JsonHelper.parseEnum(v, BrushType)!); + return true; + case "brushduration": + obj.brushDuration = (v as number); + return true; + case "tupletdenominator": + obj.tupletDenominator = (v as number); + return true; + case "tupletnumerator": + obj.tupletNumerator = (v as number); + return true; + case "iscontinuedwhammy": + obj.isContinuedWhammy = (v as boolean); + return true; + case "whammybartype": + obj.whammyBarType = (JsonHelper.parseEnum(v, WhammyType)!); + return true; + case "whammybarpoints": + obj.whammyBarPoints = []; + for (const o of (v as (Map | null)[])) { + const i = new BendPoint(); + BendPointSerializer.fromJson(i, o) + obj.addWhammyBarPoint(i); + } + return true; + case "vibrato": + obj.vibrato = (JsonHelper.parseEnum(v, VibratoType)!); + return true; + case "chordid": + obj.chordId = (v as string | null); + return true; + case "gracetype": + obj.graceType = (JsonHelper.parseEnum(v, GraceType)!); + return true; + case "pickstroke": + obj.pickStroke = (JsonHelper.parseEnum(v, PickStroke)!); + return true; + case "tremolospeed": + obj.tremoloSpeed = JsonHelper.parseEnum(v, Duration); + return true; + case "crescendo": + obj.crescendo = (JsonHelper.parseEnum(v, CrescendoType)!); + return true; + case "displaystart": + obj.displayStart = (v as number); + return true; + case "playbackstart": + obj.playbackStart = (v as number); + return true; + case "displayduration": + obj.displayDuration = (v as number); + return true; + case "playbackduration": + obj.playbackDuration = (v as number); + return true; + case "dynamics": + obj.dynamics = (JsonHelper.parseEnum(v, DynamicValue)!); + return true; + case "invertbeamdirection": + obj.invertBeamDirection = (v as boolean); + return true; + case "preferredbeamdirection": + obj.preferredBeamDirection = JsonHelper.parseEnum(v, BeamDirection); + return true; + case "beamingmode": + obj.beamingMode = (JsonHelper.parseEnum(v, BeatBeamingMode)!); + return true; + } + return false; + } +} + diff --git a/src/generated/model/BendPointCloner.ts b/src/generated/model/BendPointCloner.ts new file mode 100644 index 000000000..41a8dea7f --- /dev/null +++ b/src/generated/model/BendPointCloner.ts @@ -0,0 +1,15 @@ +// +// This code was auto-generated. +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +import { BendPoint } from "@src/model/BendPoint"; +export class BendPointCloner { + public static clone(original: BendPoint): BendPoint { + const clone = new BendPoint(); + clone.offset = original.offset; + clone.value = original.value; + return clone; + } +} + diff --git a/src/generated/model/BendPointSerializer.ts b/src/generated/model/BendPointSerializer.ts new file mode 100644 index 000000000..b25834dc5 --- /dev/null +++ b/src/generated/model/BendPointSerializer.ts @@ -0,0 +1,36 @@ +// +// This code was auto-generated. +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +import { BendPoint } from "@src/model/BendPoint"; +import { JsonHelper } from "@src/io/JsonHelper"; +export class BendPointSerializer { + public static fromJson(obj: BendPoint, m: unknown): void { + if (!m) { + return; + } + JsonHelper.forEach(m, (v, k) => this.setProperty(obj, k.toLowerCase(), v)); + } + public static toJson(obj: BendPoint | null): Map | null { + if (!obj) { + return null; + } + const o = new Map(); + o.set("offset", obj.offset); + o.set("value", obj.value); + return o; + } + public static setProperty(obj: BendPoint, property: string, v: unknown): boolean { + switch (property) { + case "offset": + obj.offset = (v as number); + return true; + case "value": + obj.value = (v as number); + return true; + } + return false; + } +} + diff --git a/src/generated/model/ChordSerializer.ts b/src/generated/model/ChordSerializer.ts new file mode 100644 index 000000000..a84a74163 --- /dev/null +++ b/src/generated/model/ChordSerializer.ts @@ -0,0 +1,56 @@ +// +// This code was auto-generated. +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +import { Chord } from "@src/model/Chord"; +import { JsonHelper } from "@src/io/JsonHelper"; +export class ChordSerializer { + public static fromJson(obj: Chord, m: unknown): void { + if (!m) { + return; + } + JsonHelper.forEach(m, (v, k) => this.setProperty(obj, k.toLowerCase(), v)); + } + public static toJson(obj: Chord | null): Map | null { + if (!obj) { + return null; + } + const o = new Map(); + o.set("name", obj.name); + o.set("firstFret", obj.firstFret); + o.set("strings", obj.strings); + o.set("barreFrets", obj.barreFrets); + o.set("showName", obj.showName); + o.set("showDiagram", obj.showDiagram); + o.set("showFingering", obj.showFingering); + return o; + } + public static setProperty(obj: Chord, property: string, v: unknown): boolean { + switch (property) { + case "name": + obj.name = (v as string); + return true; + case "firstfret": + obj.firstFret = (v as number); + return true; + case "strings": + obj.strings = (v as number[]); + return true; + case "barrefrets": + obj.barreFrets = (v as number[]); + return true; + case "showname": + obj.showName = (v as boolean); + return true; + case "showdiagram": + obj.showDiagram = (v as boolean); + return true; + case "showfingering": + obj.showFingering = (v as boolean); + return true; + } + return false; + } +} + diff --git a/src/generated/model/FermataSerializer.ts b/src/generated/model/FermataSerializer.ts new file mode 100644 index 000000000..2bbd8b5e0 --- /dev/null +++ b/src/generated/model/FermataSerializer.ts @@ -0,0 +1,37 @@ +// +// This code was auto-generated. +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +import { Fermata } from "@src/model/Fermata"; +import { JsonHelper } from "@src/io/JsonHelper"; +import { FermataType } from "@src/model/Fermata"; +export class FermataSerializer { + public static fromJson(obj: Fermata, m: unknown): void { + if (!m) { + return; + } + JsonHelper.forEach(m, (v, k) => this.setProperty(obj, k.toLowerCase(), v)); + } + public static toJson(obj: Fermata | null): Map | null { + if (!obj) { + return null; + } + const o = new Map(); + o.set("type", (obj.type as number)); + o.set("length", obj.length); + return o; + } + public static setProperty(obj: Fermata, property: string, v: unknown): boolean { + switch (property) { + case "type": + obj.type = (JsonHelper.parseEnum(v, FermataType)!); + return true; + case "length": + obj.length = (v as number); + return true; + } + return false; + } +} + diff --git a/src/generated/model/InstrumentArticulationSerializer.ts b/src/generated/model/InstrumentArticulationSerializer.ts new file mode 100644 index 000000000..88ba1d01c --- /dev/null +++ b/src/generated/model/InstrumentArticulationSerializer.ts @@ -0,0 +1,58 @@ +// +// This code was auto-generated. +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +import { InstrumentArticulation } from "@src/model/InstrumentArticulation"; +import { JsonHelper } from "@src/io/JsonHelper"; +import { MusicFontSymbol } from "@src/model/MusicFontSymbol"; +import { TextBaseline } from "@src/platform/ICanvas"; +export class InstrumentArticulationSerializer { + public static fromJson(obj: InstrumentArticulation, m: unknown): void { + if (!m) { + return; + } + JsonHelper.forEach(m, (v, k) => this.setProperty(obj, k.toLowerCase(), v)); + } + public static toJson(obj: InstrumentArticulation | null): Map | null { + if (!obj) { + return null; + } + const o = new Map(); + o.set("staffLine", obj.staffLine); + o.set("noteHeadDefault", (obj.noteHeadDefault as number)); + o.set("noteHeadHalf", (obj.noteHeadHalf as number)); + o.set("noteHeadWhole", (obj.noteHeadWhole as number)); + o.set("techniqueSymbol", (obj.techniqueSymbol as number)); + o.set("techniqueSymbolPlacement", (obj.techniqueSymbolPlacement as number)); + o.set("outputMidiNumber", obj.outputMidiNumber); + return o; + } + public static setProperty(obj: InstrumentArticulation, property: string, v: unknown): boolean { + switch (property) { + case "staffline": + obj.staffLine = (v as number); + return true; + case "noteheaddefault": + obj.noteHeadDefault = (JsonHelper.parseEnum(v, MusicFontSymbol)!); + return true; + case "noteheadhalf": + obj.noteHeadHalf = (JsonHelper.parseEnum(v, MusicFontSymbol)!); + return true; + case "noteheadwhole": + obj.noteHeadWhole = (JsonHelper.parseEnum(v, MusicFontSymbol)!); + return true; + case "techniquesymbol": + obj.techniqueSymbol = (JsonHelper.parseEnum(v, MusicFontSymbol)!); + return true; + case "techniquesymbolplacement": + obj.techniqueSymbolPlacement = (JsonHelper.parseEnum(v, TextBaseline)!); + return true; + case "outputmidinumber": + obj.outputMidiNumber = (v as number); + return true; + } + return false; + } +} + diff --git a/src/generated/model/MasterBarSerializer.ts b/src/generated/model/MasterBarSerializer.ts new file mode 100644 index 000000000..b0e8c8c3e --- /dev/null +++ b/src/generated/model/MasterBarSerializer.ts @@ -0,0 +1,120 @@ +// +// This code was auto-generated. +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +import { MasterBar } from "@src/model/MasterBar"; +import { JsonHelper } from "@src/io/JsonHelper"; +import { SectionSerializer } from "@src/generated/model/SectionSerializer"; +import { AutomationSerializer } from "@src/generated/model/AutomationSerializer"; +import { FermataSerializer } from "@src/generated/model/FermataSerializer"; +import { KeySignature } from "@src/model/KeySignature"; +import { KeySignatureType } from "@src/model/KeySignatureType"; +import { TripletFeel } from "@src/model/TripletFeel"; +import { Section } from "@src/model/Section"; +import { Automation } from "@src/model/Automation"; +import { Fermata } from "@src/model/Fermata"; +export class MasterBarSerializer { + public static fromJson(obj: MasterBar, m: unknown): void { + if (!m) { + return; + } + JsonHelper.forEach(m, (v, k) => this.setProperty(obj, k.toLowerCase(), v)); + } + public static toJson(obj: MasterBar | null): Map | null { + if (!obj) { + return null; + } + const o = new Map(); + o.set("alternateEndings", obj.alternateEndings); + o.set("keySignature", (obj.keySignature as number)); + o.set("keySignatureType", (obj.keySignatureType as number)); + o.set("isDoubleBar", obj.isDoubleBar); + o.set("isRepeatStart", obj.isRepeatStart); + o.set("repeatCount", obj.repeatCount); + o.set("timeSignatureNumerator", obj.timeSignatureNumerator); + o.set("timeSignatureDenominator", obj.timeSignatureDenominator); + o.set("timeSignatureCommon", obj.timeSignatureCommon); + o.set("tripletFeel", (obj.tripletFeel as number)); + o.set("section", SectionSerializer.toJson(obj.section)); + o.set("tempoAutomation", AutomationSerializer.toJson(obj.tempoAutomation)); + { + const m = new Map(); + o.set("fermata", m); + obj.fermata.forEach((v, k) => m.set(k.toString(), FermataSerializer.toJson(v))); + } + o.set("start", obj.start); + o.set("isAnacrusis", obj.isAnacrusis); + return o; + } + public static setProperty(obj: MasterBar, property: string, v: unknown): boolean { + switch (property) { + case "alternateendings": + obj.alternateEndings = (v as number); + return true; + case "keysignature": + obj.keySignature = (JsonHelper.parseEnum(v, KeySignature)!); + return true; + case "keysignaturetype": + obj.keySignatureType = (JsonHelper.parseEnum(v, KeySignatureType)!); + return true; + case "isdoublebar": + obj.isDoubleBar = (v as boolean); + return true; + case "isrepeatstart": + obj.isRepeatStart = (v as boolean); + return true; + case "repeatcount": + obj.repeatCount = (v as number); + return true; + case "timesignaturenumerator": + obj.timeSignatureNumerator = (v as number); + return true; + case "timesignaturedenominator": + obj.timeSignatureDenominator = (v as number); + return true; + case "timesignaturecommon": + obj.timeSignatureCommon = (v as boolean); + return true; + case "tripletfeel": + obj.tripletFeel = (JsonHelper.parseEnum(v, TripletFeel)!); + return true; + case "fermata": + obj.fermata = new Map(); + (v as Map).forEach((v, k) => { + const i = new Fermata(); + FermataSerializer.fromJson(i, (v as Map)); + obj.fermata.set(parseInt(k), i); + }); + return true; + case "start": + obj.start = (v as number); + return true; + case "isanacrusis": + obj.isAnacrusis = (v as boolean); + return true; + } + if (["section"].indexOf(property) >= 0) { + if (v) { + obj.section = new Section(); + SectionSerializer.fromJson(obj.section, (v as Map)); + } + else { + obj.section = null; + } + return true; + } + if (["tempoautomation"].indexOf(property) >= 0) { + if (v) { + obj.tempoAutomation = new Automation(); + AutomationSerializer.fromJson(obj.tempoAutomation, (v as Map)); + } + else { + obj.tempoAutomation = null; + } + return true; + } + return false; + } +} + diff --git a/src/generated/model/NoteCloner.ts b/src/generated/model/NoteCloner.ts new file mode 100644 index 000000000..8cc7eb248 --- /dev/null +++ b/src/generated/model/NoteCloner.ts @@ -0,0 +1,57 @@ +// +// This code was auto-generated. +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +import { Note } from "@src/model/Note"; +import { BendPointCloner } from "./BendPointCloner"; +export class NoteCloner { + public static clone(original: Note): Note { + const clone = new Note(); + clone.index = original.index; + clone.accentuated = original.accentuated; + clone.bendType = original.bendType; + clone.bendStyle = original.bendStyle; + clone.isContinuedBend = original.isContinuedBend; + clone.bendPoints = []; + for (const i of original.bendPoints) { + clone.addBendPoint(BendPointCloner.clone(i)); + } + clone.fret = original.fret; + clone.string = original.string; + clone.octave = original.octave; + clone.tone = original.tone; + clone.percussionArticulation = original.percussionArticulation; + clone.isVisible = original.isVisible; + clone.isLeftHandTapped = original.isLeftHandTapped; + clone.isHammerPullOrigin = original.isHammerPullOrigin; + clone.hammerPullOriginNoteId = original.hammerPullOriginNoteId; + clone.hammerPullDestinationNoteId = original.hammerPullDestinationNoteId; + clone.isSlurDestination = original.isSlurDestination; + clone.slurOriginNoteId = original.slurOriginNoteId; + clone.slurDestinationNoteId = original.slurDestinationNoteId; + clone.harmonicType = original.harmonicType; + clone.harmonicValue = original.harmonicValue; + clone.isGhost = original.isGhost; + clone.isLetRing = original.isLetRing; + clone.isPalmMute = original.isPalmMute; + clone.isDead = original.isDead; + clone.isStaccato = original.isStaccato; + clone.slideInType = original.slideInType; + clone.slideOutType = original.slideOutType; + clone.vibrato = original.vibrato; + clone.tieOriginNoteId = original.tieOriginNoteId; + clone.tieDestinationNoteId = original.tieDestinationNoteId; + clone.isTieDestination = original.isTieDestination; + clone.leftHandFinger = original.leftHandFinger; + clone.rightHandFinger = original.rightHandFinger; + clone.isFingering = original.isFingering; + clone.trillValue = original.trillValue; + clone.trillSpeed = original.trillSpeed; + clone.durationPercent = original.durationPercent; + clone.accidentalMode = original.accidentalMode; + clone.dynamics = original.dynamics; + return clone; + } +} + diff --git a/src/generated/model/NoteSerializer.ts b/src/generated/model/NoteSerializer.ts new file mode 100644 index 000000000..938483021 --- /dev/null +++ b/src/generated/model/NoteSerializer.ts @@ -0,0 +1,206 @@ +// +// This code was auto-generated. +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +import { Note } from "@src/model/Note"; +import { JsonHelper } from "@src/io/JsonHelper"; +import { BendPointSerializer } from "@src/generated/model/BendPointSerializer"; +import { AccentuationType } from "@src/model/AccentuationType"; +import { BendType } from "@src/model/BendType"; +import { BendStyle } from "@src/model/BendStyle"; +import { BendPoint } from "@src/model/BendPoint"; +import { HarmonicType } from "@src/model/HarmonicType"; +import { SlideInType } from "@src/model/SlideInType"; +import { SlideOutType } from "@src/model/SlideOutType"; +import { VibratoType } from "@src/model/VibratoType"; +import { Fingers } from "@src/model/Fingers"; +import { Duration } from "@src/model/Duration"; +import { NoteAccidentalMode } from "@src/model/NoteAccidentalMode"; +import { DynamicValue } from "@src/model/DynamicValue"; +export class NoteSerializer { + public static fromJson(obj: Note, m: unknown): void { + if (!m) { + return; + } + JsonHelper.forEach(m, (v, k) => this.setProperty(obj, k.toLowerCase(), v)); + } + public static toJson(obj: Note | null): Map | null { + if (!obj) { + return null; + } + const o = new Map(); + o.set("id", obj.id); + o.set("accentuated", (obj.accentuated as number)); + o.set("bendType", (obj.bendType as number)); + o.set("bendStyle", (obj.bendStyle as number)); + o.set("isContinuedBend", obj.isContinuedBend); + o.set("bendPoints", obj.bendPoints.map(i => BendPointSerializer.toJson(i))); + o.set("fret", obj.fret); + o.set("string", obj.string); + o.set("octave", obj.octave); + o.set("tone", obj.tone); + o.set("percussionArticulation", obj.percussionArticulation); + o.set("isVisible", obj.isVisible); + o.set("isLeftHandTapped", obj.isLeftHandTapped); + o.set("isHammerPullOrigin", obj.isHammerPullOrigin); + o.set("hammerPullOriginNoteId", obj.hammerPullOriginNoteId); + o.set("hammerPullDestinationNoteId", obj.hammerPullDestinationNoteId); + o.set("isSlurDestination", obj.isSlurDestination); + o.set("slurOriginNoteId", obj.slurOriginNoteId); + o.set("slurDestinationNoteId", obj.slurDestinationNoteId); + o.set("harmonicType", (obj.harmonicType as number)); + o.set("harmonicValue", obj.harmonicValue); + o.set("isGhost", obj.isGhost); + o.set("isLetRing", obj.isLetRing); + o.set("isPalmMute", obj.isPalmMute); + o.set("isDead", obj.isDead); + o.set("isStaccato", obj.isStaccato); + o.set("slideInType", (obj.slideInType as number)); + o.set("slideOutType", (obj.slideOutType as number)); + o.set("vibrato", (obj.vibrato as number)); + o.set("tieOriginNoteId", obj.tieOriginNoteId); + o.set("tieDestinationNoteId", obj.tieDestinationNoteId); + o.set("isTieDestination", obj.isTieDestination); + o.set("leftHandFinger", (obj.leftHandFinger as number)); + o.set("rightHandFinger", (obj.rightHandFinger as number)); + o.set("isFingering", obj.isFingering); + o.set("trillValue", obj.trillValue); + o.set("trillSpeed", (obj.trillSpeed as number)); + o.set("durationPercent", obj.durationPercent); + o.set("accidentalMode", (obj.accidentalMode as number)); + o.set("dynamics", (obj.dynamics as number)); + return o; + } + public static setProperty(obj: Note, property: string, v: unknown): boolean { + switch (property) { + case "id": + obj.id = (v as number); + return true; + case "accentuated": + obj.accentuated = (JsonHelper.parseEnum(v, AccentuationType)!); + return true; + case "bendtype": + obj.bendType = (JsonHelper.parseEnum(v, BendType)!); + return true; + case "bendstyle": + obj.bendStyle = (JsonHelper.parseEnum(v, BendStyle)!); + return true; + case "iscontinuedbend": + obj.isContinuedBend = (v as boolean); + return true; + case "bendpoints": + obj.bendPoints = []; + for (const o of (v as (Map | null)[])) { + const i = new BendPoint(); + BendPointSerializer.fromJson(i, o) + obj.addBendPoint(i); + } + return true; + case "fret": + obj.fret = (v as number); + return true; + case "string": + obj.string = (v as number); + return true; + case "octave": + obj.octave = (v as number); + return true; + case "tone": + obj.tone = (v as number); + return true; + case "percussionarticulation": + obj.percussionArticulation = (v as number); + return true; + case "isvisible": + obj.isVisible = (v as boolean); + return true; + case "islefthandtapped": + obj.isLeftHandTapped = (v as boolean); + return true; + case "ishammerpullorigin": + obj.isHammerPullOrigin = (v as boolean); + return true; + case "hammerpulloriginnoteid": + obj.hammerPullOriginNoteId = (v as number); + return true; + case "hammerpulldestinationnoteid": + obj.hammerPullDestinationNoteId = (v as number); + return true; + case "isslurdestination": + obj.isSlurDestination = (v as boolean); + return true; + case "sluroriginnoteid": + obj.slurOriginNoteId = (v as number); + return true; + case "slurdestinationnoteid": + obj.slurDestinationNoteId = (v as number); + return true; + case "harmonictype": + obj.harmonicType = (JsonHelper.parseEnum(v, HarmonicType)!); + return true; + case "harmonicvalue": + obj.harmonicValue = (v as number); + return true; + case "isghost": + obj.isGhost = (v as boolean); + return true; + case "isletring": + obj.isLetRing = (v as boolean); + return true; + case "ispalmmute": + obj.isPalmMute = (v as boolean); + return true; + case "isdead": + obj.isDead = (v as boolean); + return true; + case "isstaccato": + obj.isStaccato = (v as boolean); + return true; + case "slideintype": + obj.slideInType = (JsonHelper.parseEnum(v, SlideInType)!); + return true; + case "slideouttype": + obj.slideOutType = (JsonHelper.parseEnum(v, SlideOutType)!); + return true; + case "vibrato": + obj.vibrato = (JsonHelper.parseEnum(v, VibratoType)!); + return true; + case "tieoriginnoteid": + obj.tieOriginNoteId = (v as number); + return true; + case "tiedestinationnoteid": + obj.tieDestinationNoteId = (v as number); + return true; + case "istiedestination": + obj.isTieDestination = (v as boolean); + return true; + case "lefthandfinger": + obj.leftHandFinger = (JsonHelper.parseEnum(v, Fingers)!); + return true; + case "righthandfinger": + obj.rightHandFinger = (JsonHelper.parseEnum(v, Fingers)!); + return true; + case "isfingering": + obj.isFingering = (v as boolean); + return true; + case "trillvalue": + obj.trillValue = (v as number); + return true; + case "trillspeed": + obj.trillSpeed = (JsonHelper.parseEnum(v, Duration)!); + return true; + case "durationpercent": + obj.durationPercent = (v as number); + return true; + case "accidentalmode": + obj.accidentalMode = (JsonHelper.parseEnum(v, NoteAccidentalMode)!); + return true; + case "dynamics": + obj.dynamics = (JsonHelper.parseEnum(v, DynamicValue)!); + return true; + } + return false; + } +} + diff --git a/src/generated/model/PlaybackInformationSerializer.ts b/src/generated/model/PlaybackInformationSerializer.ts new file mode 100644 index 000000000..6cde5fde4 --- /dev/null +++ b/src/generated/model/PlaybackInformationSerializer.ts @@ -0,0 +1,60 @@ +// +// This code was auto-generated. +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +import { PlaybackInformation } from "@src/model/PlaybackInformation"; +import { JsonHelper } from "@src/io/JsonHelper"; +export class PlaybackInformationSerializer { + public static fromJson(obj: PlaybackInformation, m: unknown): void { + if (!m) { + return; + } + JsonHelper.forEach(m, (v, k) => this.setProperty(obj, k.toLowerCase(), v)); + } + public static toJson(obj: PlaybackInformation | null): Map | null { + if (!obj) { + return null; + } + const o = new Map(); + o.set("volume", obj.volume); + o.set("balance", obj.balance); + o.set("port", obj.port); + o.set("program", obj.program); + o.set("primaryChannel", obj.primaryChannel); + o.set("secondaryChannel", obj.secondaryChannel); + o.set("isMute", obj.isMute); + o.set("isSolo", obj.isSolo); + return o; + } + public static setProperty(obj: PlaybackInformation, property: string, v: unknown): boolean { + switch (property) { + case "volume": + obj.volume = (v as number); + return true; + case "balance": + obj.balance = (v as number); + return true; + case "port": + obj.port = (v as number); + return true; + case "program": + obj.program = (v as number); + return true; + case "primarychannel": + obj.primaryChannel = (v as number); + return true; + case "secondarychannel": + obj.secondaryChannel = (v as number); + return true; + case "ismute": + obj.isMute = (v as boolean); + return true; + case "issolo": + obj.isSolo = (v as boolean); + return true; + } + return false; + } +} + diff --git a/src/generated/model/RenderStylesheetSerializer.ts b/src/generated/model/RenderStylesheetSerializer.ts new file mode 100644 index 000000000..d2fcaf542 --- /dev/null +++ b/src/generated/model/RenderStylesheetSerializer.ts @@ -0,0 +1,32 @@ +// +// This code was auto-generated. +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +import { RenderStylesheet } from "@src/model/RenderStylesheet"; +import { JsonHelper } from "@src/io/JsonHelper"; +export class RenderStylesheetSerializer { + public static fromJson(obj: RenderStylesheet, m: unknown): void { + if (!m) { + return; + } + JsonHelper.forEach(m, (v, k) => this.setProperty(obj, k.toLowerCase(), v)); + } + public static toJson(obj: RenderStylesheet | null): Map | null { + if (!obj) { + return null; + } + const o = new Map(); + o.set("hideDynamics", obj.hideDynamics); + return o; + } + public static setProperty(obj: RenderStylesheet, property: string, v: unknown): boolean { + switch (property) { + case "hidedynamics": + obj.hideDynamics = (v as boolean); + return true; + } + return false; + } +} + diff --git a/src/generated/model/ScoreSerializer.ts b/src/generated/model/ScoreSerializer.ts new file mode 100644 index 000000000..474ac516d --- /dev/null +++ b/src/generated/model/ScoreSerializer.ts @@ -0,0 +1,104 @@ +// +// This code was auto-generated. +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +import { Score } from "@src/model/Score"; +import { JsonHelper } from "@src/io/JsonHelper"; +import { MasterBarSerializer } from "@src/generated/model/MasterBarSerializer"; +import { TrackSerializer } from "@src/generated/model/TrackSerializer"; +import { RenderStylesheetSerializer } from "@src/generated/model/RenderStylesheetSerializer"; +import { MasterBar } from "@src/model/MasterBar"; +import { Track } from "@src/model/Track"; +export class ScoreSerializer { + public static fromJson(obj: Score, m: unknown): void { + if (!m) { + return; + } + JsonHelper.forEach(m, (v, k) => this.setProperty(obj, k.toLowerCase(), v)); + } + public static toJson(obj: Score | null): Map | null { + if (!obj) { + return null; + } + const o = new Map(); + o.set("album", obj.album); + o.set("artist", obj.artist); + o.set("copyright", obj.copyright); + o.set("instructions", obj.instructions); + o.set("music", obj.music); + o.set("notices", obj.notices); + o.set("subTitle", obj.subTitle); + o.set("title", obj.title); + o.set("words", obj.words); + o.set("tab", obj.tab); + o.set("tempo", obj.tempo); + o.set("tempoLabel", obj.tempoLabel); + o.set("masterBars", obj.masterBars.map(i => MasterBarSerializer.toJson(i))); + o.set("tracks", obj.tracks.map(i => TrackSerializer.toJson(i))); + o.set("stylesheet", RenderStylesheetSerializer.toJson(obj.stylesheet)); + return o; + } + public static setProperty(obj: Score, property: string, v: unknown): boolean { + switch (property) { + case "album": + obj.album = (v as string); + return true; + case "artist": + obj.artist = (v as string); + return true; + case "copyright": + obj.copyright = (v as string); + return true; + case "instructions": + obj.instructions = (v as string); + return true; + case "music": + obj.music = (v as string); + return true; + case "notices": + obj.notices = (v as string); + return true; + case "subtitle": + obj.subTitle = (v as string); + return true; + case "title": + obj.title = (v as string); + return true; + case "words": + obj.words = (v as string); + return true; + case "tab": + obj.tab = (v as string); + return true; + case "tempo": + obj.tempo = (v as number); + return true; + case "tempolabel": + obj.tempoLabel = (v as string); + return true; + case "masterbars": + obj.masterBars = []; + for (const o of (v as (Map | null)[])) { + const i = new MasterBar(); + MasterBarSerializer.fromJson(i, o) + obj.addMasterBar(i); + } + return true; + case "tracks": + obj.tracks = []; + for (const o of (v as (Map | null)[])) { + const i = new Track(); + TrackSerializer.fromJson(i, o) + obj.addTrack(i); + } + return true; + } + if (["stylesheet"].indexOf(property) >= 0) { + RenderStylesheetSerializer.fromJson(obj.stylesheet, (v as Map)); + return true; + } + return false; + } +} + diff --git a/src/generated/model/SectionSerializer.ts b/src/generated/model/SectionSerializer.ts new file mode 100644 index 000000000..c5c82f271 --- /dev/null +++ b/src/generated/model/SectionSerializer.ts @@ -0,0 +1,36 @@ +// +// This code was auto-generated. +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +import { Section } from "@src/model/Section"; +import { JsonHelper } from "@src/io/JsonHelper"; +export class SectionSerializer { + public static fromJson(obj: Section, m: unknown): void { + if (!m) { + return; + } + JsonHelper.forEach(m, (v, k) => this.setProperty(obj, k.toLowerCase(), v)); + } + public static toJson(obj: Section | null): Map | null { + if (!obj) { + return null; + } + const o = new Map(); + o.set("marker", obj.marker); + o.set("text", obj.text); + return o; + } + public static setProperty(obj: Section, property: string, v: unknown): boolean { + switch (property) { + case "marker": + obj.marker = (v as string); + return true; + case "text": + obj.text = (v as string); + return true; + } + return false; + } +} + diff --git a/src/generated/model/StaffSerializer.ts b/src/generated/model/StaffSerializer.ts new file mode 100644 index 000000000..ed35f0514 --- /dev/null +++ b/src/generated/model/StaffSerializer.ts @@ -0,0 +1,90 @@ +// +// This code was auto-generated. +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +import { Staff } from "@src/model/Staff"; +import { JsonHelper } from "@src/io/JsonHelper"; +import { BarSerializer } from "@src/generated/model/BarSerializer"; +import { ChordSerializer } from "@src/generated/model/ChordSerializer"; +import { Bar } from "@src/model/Bar"; +import { Chord } from "@src/model/Chord"; +export class StaffSerializer { + public static fromJson(obj: Staff, m: unknown): void { + if (!m) { + return; + } + JsonHelper.forEach(m, (v, k) => this.setProperty(obj, k.toLowerCase(), v)); + } + public static toJson(obj: Staff | null): Map | null { + if (!obj) { + return null; + } + const o = new Map(); + o.set("bars", obj.bars.map(i => BarSerializer.toJson(i))); + { + const m = new Map(); + o.set("chords", m); + obj.chords.forEach((v, k) => m.set(k.toString(), ChordSerializer.toJson(v))); + } + o.set("capo", obj.capo); + o.set("transpositionPitch", obj.transpositionPitch); + o.set("displayTranspositionPitch", obj.displayTranspositionPitch); + o.set("tuning", obj.tuning); + o.set("tuningName", obj.tuningName); + o.set("showTablature", obj.showTablature); + o.set("showStandardNotation", obj.showStandardNotation); + o.set("isPercussion", obj.isPercussion); + o.set("standardNotationLineCount", obj.standardNotationLineCount); + return o; + } + public static setProperty(obj: Staff, property: string, v: unknown): boolean { + switch (property) { + case "bars": + obj.bars = []; + for (const o of (v as (Map | null)[])) { + const i = new Bar(); + BarSerializer.fromJson(i, o) + obj.addBar(i); + } + return true; + case "chords": + obj.chords = new Map(); + (v as Map).forEach((v, k) => { + const i = new Chord(); + ChordSerializer.fromJson(i, (v as Map)); + obj.addChord(k, i); + }); + return true; + case "capo": + obj.capo = (v as number); + return true; + case "transpositionpitch": + obj.transpositionPitch = (v as number); + return true; + case "displaytranspositionpitch": + obj.displayTranspositionPitch = (v as number); + return true; + case "tuning": + obj.tuning = (v as number[]); + return true; + case "tuningname": + obj.tuningName = (v as string); + return true; + case "showtablature": + obj.showTablature = (v as boolean); + return true; + case "showstandardnotation": + obj.showStandardNotation = (v as boolean); + return true; + case "ispercussion": + obj.isPercussion = (v as boolean); + return true; + case "standardnotationlinecount": + obj.standardNotationLineCount = (v as number); + return true; + } + return false; + } +} + diff --git a/src/generated/model/TrackSerializer.ts b/src/generated/model/TrackSerializer.ts new file mode 100644 index 000000000..717df3306 --- /dev/null +++ b/src/generated/model/TrackSerializer.ts @@ -0,0 +1,69 @@ +// +// This code was auto-generated. +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +import { Track } from "@src/model/Track"; +import { JsonHelper } from "@src/io/JsonHelper"; +import { StaffSerializer } from "@src/generated/model/StaffSerializer"; +import { PlaybackInformationSerializer } from "@src/generated/model/PlaybackInformationSerializer"; +import { Color } from "@src/model/Color"; +import { InstrumentArticulationSerializer } from "@src/generated/model/InstrumentArticulationSerializer"; +import { Staff } from "@src/model/Staff"; +import { InstrumentArticulation } from "@src/model/InstrumentArticulation"; +export class TrackSerializer { + public static fromJson(obj: Track, m: unknown): void { + if (!m) { + return; + } + JsonHelper.forEach(m, (v, k) => this.setProperty(obj, k.toLowerCase(), v)); + } + public static toJson(obj: Track | null): Map | null { + if (!obj) { + return null; + } + const o = new Map(); + o.set("staves", obj.staves.map(i => StaffSerializer.toJson(i))); + o.set("playbackInfo", PlaybackInformationSerializer.toJson(obj.playbackInfo)); + o.set("color", Color.toJson(obj.color)); + o.set("name", obj.name); + o.set("shortName", obj.shortName); + o.set("percussionArticulations", obj.percussionArticulations.map(i => InstrumentArticulationSerializer.toJson(i))); + return o; + } + public static setProperty(obj: Track, property: string, v: unknown): boolean { + switch (property) { + case "staves": + obj.staves = []; + for (const o of (v as (Map | null)[])) { + const i = new Staff(); + StaffSerializer.fromJson(i, o) + obj.addStaff(i); + } + return true; + case "color": + obj.color = (Color.fromJson(v)!); + return true; + case "name": + obj.name = (v as string); + return true; + case "shortname": + obj.shortName = (v as string); + return true; + case "percussionarticulations": + obj.percussionArticulations = []; + for (const o of (v as (Map | null)[])) { + const i = new InstrumentArticulation(); + InstrumentArticulationSerializer.fromJson(i, o) + obj.percussionArticulations.push(i); + } + return true; + } + if (["playbackinfo"].indexOf(property) >= 0) { + PlaybackInformationSerializer.fromJson(obj.playbackInfo, (v as Map)); + return true; + } + return false; + } +} + diff --git a/src/generated/model/VoiceSerializer.ts b/src/generated/model/VoiceSerializer.ts new file mode 100644 index 000000000..cb0eab375 --- /dev/null +++ b/src/generated/model/VoiceSerializer.ts @@ -0,0 +1,43 @@ +// +// This code was auto-generated. +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +import { Voice } from "@src/model/Voice"; +import { JsonHelper } from "@src/io/JsonHelper"; +import { BeatSerializer } from "@src/generated/model/BeatSerializer"; +import { Beat } from "@src/model/Beat"; +export class VoiceSerializer { + public static fromJson(obj: Voice, m: unknown): void { + if (!m) { + return; + } + JsonHelper.forEach(m, (v, k) => this.setProperty(obj, k.toLowerCase(), v)); + } + public static toJson(obj: Voice | null): Map | null { + if (!obj) { + return null; + } + const o = new Map(); + o.set("beats", obj.beats.map(i => BeatSerializer.toJson(i))); + o.set("isEmpty", obj.isEmpty); + return o; + } + public static setProperty(obj: Voice, property: string, v: unknown): boolean { + switch (property) { + case "beats": + obj.beats = []; + for (const o of (v as (Map | null)[])) { + const i = new Beat(); + BeatSerializer.fromJson(i, o) + obj.addBeat(i); + } + return true; + case "isempty": + obj.isEmpty = (v as boolean); + return true; + } + return false; + } +} + diff --git a/src/importer/AlphaTexImporter.ts b/src/importer/AlphaTexImporter.ts index 04da79585..da025f106 100644 --- a/src/importer/AlphaTexImporter.ts +++ b/src/importer/AlphaTexImporter.ts @@ -32,6 +32,7 @@ import { Voice } from '@src/model/Voice'; import { Logger } from '@src/Logger'; import { ModelUtils, TuningParseResult } from '@src/model/ModelUtils'; import { AlphaTabError, AlphaTabErrorType } from '@src/AlphaTabError'; +import { BeatCloner } from '@src/generated/model/BeatCloner'; /** * A list of terminals recognized by the alphaTex-parser @@ -1035,7 +1036,7 @@ export class AlphaTexImporter extends ScoreImporter { } this.beatEffects(beat); for (let i: number = 0; i < beatRepeat - 1; i++) { - voice.addBeat(beat.clone()); + voice.addBeat(BeatCloner.clone(beat)); } return true; } diff --git a/src/importer/CapellaParser.ts b/src/importer/CapellaParser.ts index e3b44a5f6..d8b986b1a 100644 --- a/src/importer/CapellaParser.ts +++ b/src/importer/CapellaParser.ts @@ -885,7 +885,7 @@ export class CapellaParser { } } else if (c.attributes.has('end') && this._tieStarts.length > 0 && !note.isTieDestination) { note.isTieDestination = true; - note.tieOrigin = this._tieStarts[0]; + note.tieOriginNoteId = this._tieStarts[0].id; this._tieStarts.splice(0, 1); this._tieStartIds.delete(note.id); } diff --git a/src/importer/GpifParser.ts b/src/importer/GpifParser.ts index 347923fe7..4068cb823 100644 --- a/src/importer/GpifParser.ts +++ b/src/importer/GpifParser.ts @@ -43,6 +43,8 @@ import { PercussionMapper } from '@src/model/PercussionMapper'; import { InstrumentArticulation } from '@src/model/InstrumentArticulation'; import { MusicFontSymbol } from '@src/model/MusicFontSymbol'; import { TextBaseline } from '@src/platform/ICanvas'; +import { BeatCloner } from '@src/generated/model/BeatCloner'; +import { NoteCloner } from '@src/generated/model/NoteCloner'; /** * This structure represents a duration within a gpif @@ -2066,7 +2068,7 @@ export class GpifParser { if (beatId !== GpifParser.InvalidId) { // important! we clone the beat because beats get reused // in gp6, our model needs to have unique beats. - let beat: Beat = this._beatById.get(beatId)!.clone(); + let beat: Beat = BeatCloner.clone(this._beatById.get(beatId)!); voice.addBeat(beat); let rhythmId: string = this._rhythmOfBeat.get(beatId)!; let rhythm: GpifRhythm = this._rhythmById.get(rhythmId)!; @@ -2079,7 +2081,7 @@ export class GpifParser { if (this._notesOfBeat.has(beatId)) { for (let noteId of this._notesOfBeat.get(beatId)!) { if (noteId !== GpifParser.InvalidId) { - const note = this._noteById.get(noteId)!.clone(); + const note = NoteCloner.clone(this._noteById.get(noteId)!); // reset midi value for non-percussion staves if (staff.isPercussion) { note.fret = -1; diff --git a/src/importer/MusicXmlImporter.ts b/src/importer/MusicXmlImporter.ts index bd47f226a..70725773f 100644 --- a/src/importer/MusicXmlImporter.ts +++ b/src/importer/MusicXmlImporter.ts @@ -805,7 +805,7 @@ export class MusicXmlImporter extends ScoreImporter { } } else if (element.getAttribute('type') === 'stop' && this._tieStarts.length > 0 && !note.isTieDestination) { note.isTieDestination = true; - note.tieOrigin = this._tieStarts[0]; + note.tieOriginNoteId = this._tieStarts[0].id; this._tieStarts.splice(0, 1); this._tieStartIds.delete(note.id); } @@ -849,8 +849,8 @@ export class MusicXmlImporter extends ScoreImporter { if (this._slurStarts.has(slurNumber)) { note.isSlurDestination = true; let slurStart: Note = this._slurStarts.get(slurNumber)!; - slurStart.slurDestination = note; - note.slurOrigin = note; + slurStart.slurDestinationNoteId = note.id; + note.slurOriginNoteId = note.id; } break; } diff --git a/src/io/JsonHelper.ts b/src/io/JsonHelper.ts new file mode 100644 index 000000000..85599a782 --- /dev/null +++ b/src/io/JsonHelper.ts @@ -0,0 +1,40 @@ +import { AlphaTabError } from "@src/alphatab"; +import { AlphaTabErrorType } from "@src/AlphaTabError"; + +/** + * @partial + */ +export class JsonHelper { + /** + * @target web + */ + public static parseEnum(s: unknown, enumType: any): T | null { + switch (typeof s) { + case 'string': + const num = parseInt(s); + return isNaN(num) + ? enumType[Object.keys(enumType).find(k => k.toLowerCase() === s.toLowerCase()) as any] as any + : num as unknown as T; + case 'number': + return s as unknown as T; + case 'undefined': + case 'object': + return null; + } + throw new AlphaTabError(AlphaTabErrorType.Format, `Could not parse enum value '${s}'`); + } + + /** + * @target web + */ + public static forEach(s: unknown, func: (v: unknown, k: string) => void): void { + if (s instanceof Map) { + (s as Map).forEach(func); + }else if (typeof s === 'object') { + for (const k in s) { + func((s as any)[k], k) + } + } + // skip + } +} \ No newline at end of file diff --git a/src/model/Automation.ts b/src/model/Automation.ts index 8f3b1d094..4fa9a336d 100644 --- a/src/model/Automation.ts +++ b/src/model/Automation.ts @@ -22,6 +22,8 @@ export enum AutomationType { /** * Automations are used to change the behaviour of a song. + * @cloneable + * @json */ export class Automation { /** @@ -66,18 +68,4 @@ export class Automation { automation.value = value * references[reference]; return automation; } - - public static copyTo(src: Automation, dst: Automation): void { - dst.isLinear = src.isLinear; - dst.ratioPosition = src.ratioPosition; - dst.text = src.text; - dst.type = src.type; - dst.value = src.value; - } - - public clone(): Automation { - let a: Automation = new Automation(); - Automation.copyTo(this, a); - return a; - } } diff --git a/src/model/Bar.ts b/src/model/Bar.ts index ab480c83a..92d01cd07 100644 --- a/src/model/Bar.ts +++ b/src/model/Bar.ts @@ -8,6 +8,7 @@ import { Settings } from '@src/Settings'; /** * A bar is a single block within a track, also known as Measure. + * @json */ export class Bar { private static _globalBarId: number = 0; @@ -19,16 +20,19 @@ export class Bar { /** * Gets or sets the zero-based index of this bar within the staff. + * @json_ignore */ public index: number = 0; /** * Gets or sets the next bar that comes after this bar. + * @json_ignore */ public nextBar: Bar | null = null; /** * Gets or sets the previous bar that comes before this bar. + * @json_ignore */ public previousBar: Bar | null = null; @@ -44,11 +48,13 @@ export class Bar { /** * Gets or sets the reference to the parent staff. + * @json_ignore */ public staff!: Staff; /** * Gets or sets the list of voices contained in this bar. + * @json_add addVoice */ public voices: Voice[] = []; @@ -70,14 +76,6 @@ export class Bar { return true; } - public static copyTo(src: Bar, dst: Bar): void { - dst.id = src.id; - dst.index = src.index; - dst.clef = src.clef; - dst.clefOttava = src.clefOttava; - dst.simileMark = src.simileMark; - } - public addVoice(voice: Voice): void { voice.bar = this; voice.index = this.voices.length; diff --git a/src/model/Beat.ts b/src/model/Beat.ts index 6494e5337..c814d7070 100644 --- a/src/model/Beat.ts +++ b/src/model/Beat.ts @@ -21,6 +21,7 @@ import { NotationMode } from '@src/NotationSettings'; import { Settings } from '@src/Settings'; import { Logger } from '@src/Logger'; import { BeamDirection } from '@src/rendering/utils/BeamDirection'; +import { BeatCloner } from '@src/generated/model/BeatCloner'; /** * Lists the different modes on how beaming for a beat should be done. @@ -43,27 +44,35 @@ export enum BeatBeamingMode { /** * A beat is a single block within a bar. A beat is a combination * of several notes played at the same time. + * @json + * @cloneable */ export class Beat { private static _globalBeatId: number = 0; /** * Gets or sets the unique id of this beat. + * @clone_ignore */ public id: number = Beat._globalBeatId++; /** * Gets or sets the zero-based index of this beat within the voice. + * @json_ignore */ public index: number = 0; /** * Gets or sets the previous beat within the whole song. + * @json_ignore + * @clone_ignore */ public previousBeat: Beat | null = null; /** * Gets or sets the next beat within the whole song. + * @json_ignore + * @clone_ignore */ public nextBeat: Beat | null = null; @@ -73,23 +82,29 @@ export class Beat { /** * Gets or sets the reference to the parent voice this beat belongs to. + * @json_ignore + * @clone_ignore */ public voice!: Voice; /** * Gets or sets the list of notes contained in this beat. + * @json_add addNote + * @clone_add addNote */ public notes: Note[] = []; /** * Gets the lookup where the notes per string are registered. * If this staff contains string based notes this lookup allows fast access. + * @json_ignore */ public readonly noteStringLookup: Map = new Map(); /** * Gets the lookup where the notes per value are registered. * If this staff contains string based notes this lookup allows fast access. + * @json_ignore */ public readonly noteValueLookup: Map = new Map(); @@ -110,6 +125,8 @@ export class Beat { /** * Gets or sets the fermata applied to this beat. + * @clone_ignore + * @json_ignore */ public fermata: Fermata | null = null; @@ -124,21 +141,29 @@ export class Beat { /** * Gets or sets the note with the lowest pitch in this beat. Only visible notes are considered. + * @json_ignore + * @clone_ignore */ public minNote: Note | null = null; /** * Gets or sets the note with the highest pitch in this beat. Only visible notes are considered. + * @json_ignore + * @clone_ignore */ public maxNote: Note | null = null; /** * Gets or sets the note with the highest string number in this beat. Only visible notes are considered. + * @json_ignore + * @clone_ignore */ public maxStringNote: Note | null = null; /** * Gets or sets the note with the lowest string number in this beat. Only visible notes are considered. + * @json_ignore + * @clone_ignore */ public minStringNote: Note | null = null; @@ -153,11 +178,13 @@ export class Beat { /** * Gets or sets whether any note in this beat has a let-ring applied. + * @json_ignore */ public isLetRing: boolean = false; /** * Gets or sets whether any note in this beat has a palm-mute paplied. + * @json_ignore */ public isPalmMute: boolean = false; @@ -233,6 +260,10 @@ export class Beat { ); } + /** + * @clone_ignore + * @json_ignore + */ public tupletGroup: TupletGroup | null = null; /** @@ -247,16 +278,22 @@ export class Beat { /** * Gets or sets the points defining the whammy bar usage. + * @json_add addWhammyBarPoint + * @clone_add addWhammyBarPoint */ public whammyBarPoints: BendPoint[] = []; /** * Gets or sets the highest point with for the highest whammy bar value. + * @json_ignore + * @clone_ignore */ public maxWhammyPoint: BendPoint | null = null; /** * Gets or sets the highest point with for the lowest whammy bar value. + * @json_ignore + * @clone_ignore */ public minWhammyPoint: BendPoint | null = null; @@ -352,14 +389,25 @@ export class Beat { */ public preferredBeamDirection: BeamDirection | null = null; + /** + * @json_ignore + */ public isEffectSlurOrigin: boolean = false; public get isEffectSlurDestination(): boolean { return !!this.effectSlurOrigin; } + /** + * @clone_ignore + * @json_ignore + */ public effectSlurOrigin: Beat | null = null; + /** + * @clone_ignore + * @json_ignore + */ public effectSlurDestination: Beat | null = null; /** @@ -367,66 +415,6 @@ export class Beat { */ public beamingMode:BeatBeamingMode = BeatBeamingMode.Auto; - public static copyTo(src: Beat, dst: Beat): void { - dst.id = src.id; - dst.index = src.index; - dst.isEmpty = src.isEmpty; - dst.duration = src.duration; - dst.dots = src.dots; - dst.fadeIn = src.fadeIn; - if (src.lyrics) { - dst.lyrics = new Array(src.lyrics.length); - for (let i: number = 0; i < src.lyrics.length; i++) { - dst.lyrics[i] = src.lyrics[i]; - } - } - dst.pop = src.pop; - dst.hasRasgueado = src.hasRasgueado; - dst.slap = src.slap; - dst.tap = src.tap; - dst.text = src.text; - dst.brushType = src.brushType; - dst.brushDuration = src.brushDuration; - dst.tupletDenominator = src.tupletDenominator; - dst.tupletNumerator = src.tupletNumerator; - dst.vibrato = src.vibrato; - dst.chordId = src.chordId; - dst.graceType = src.graceType; - dst.pickStroke = src.pickStroke; - dst.tremoloSpeed = src.tremoloSpeed; - dst.crescendo = src.crescendo; - dst.displayStart = src.displayStart; - dst.displayDuration = src.displayDuration; - dst.playbackStart = src.playbackStart; - dst.playbackDuration = src.playbackDuration; - dst.dynamics = src.dynamics; - dst.isLegatoOrigin = src.isLegatoOrigin; - dst.invertBeamDirection = src.invertBeamDirection; - dst.preferredBeamDirection = src.preferredBeamDirection; - dst.whammyBarType = src.whammyBarType; - dst.isContinuedWhammy = src.isContinuedWhammy; - dst.ottava = src.ottava; - dst.whammyStyle = src.whammyStyle; - dst.beamingMode = src.beamingMode; - } - - public clone(): Beat { - let beat: Beat = new Beat(); - let id: number = beat.id; - for (const p of this.whammyBarPoints) { - beat.addWhammyBarPoint(p.clone()); - } - for (const n of this.notes) { - beat.addNoteInternal(n.clone(), n.realValue); - } - Beat.copyTo(this, beat); - for (const a of this.automations) { - beat.automations.push(a.clone()); - } - beat.id = id; - return beat; - } - public addWhammyBarPoint(point: BendPoint): void { this.whammyBarPoints.push(point); if (!this.maxWhammyPoint || point.value > this.maxWhammyPoint.value) { @@ -471,20 +459,12 @@ export class Beat { } public addNote(note: Note): void { - this.addNoteInternal(note, -1); - } - - private addNoteInternal(note: Note, realValue: number = -1): void { note.beat = this; note.index = this.notes.length; this.notes.push(note); if (note.isStringed) { this.noteStringLookup.set(note.string, note); } - if (realValue === -1) { - realValue = note.realValue; - } - this.noteValueLookup.set(realValue, note); } public removeNote(note: Note): void { @@ -731,7 +711,7 @@ export class Beat { if (needCopyBeatForBend) { // if this beat is a simple bend convert it to a grace beat // and generate a placeholder beat with tied notes - let cloneBeat: Beat = this.clone(); + let cloneBeat: Beat = BeatCloner.clone(this); cloneBeat.id = Beat._globalBeatId++; cloneBeat.pickStroke = PickStroke.None; for (let i: number = 0, j: number = cloneBeat.notes.length; i < j; i++) { @@ -747,12 +727,12 @@ export class Beat { // fix ties if(note.isTieOrigin) { - cloneNote.tieDestination = note.tieDestination!; - note.tieDestination!.tieOrigin = cloneNote; + cloneNote.tieDestinationNoteId = note.tieDestination!.id; + note.tieDestination!.tieOriginNoteId = cloneNote.id; } if(note.isTieDestination) { - cloneNote.tieOrigin = note.tieOrigin; - note.tieOrigin!.tieDestination = cloneNote; + cloneNote.tieOriginNoteId = note.tieOrigin ? note.tieOrigin.id : -1; + note.tieOrigin!.tieDestinationNoteId = cloneNote.id; } // if the note has a bend which is continued on the next note @@ -810,8 +790,9 @@ export class Beat { return null; } - public chain() { + public chain() { for(const n of this.notes) { + this.noteValueLookup.set(n.realValue, n); n.chain(); } } diff --git a/src/model/BendPoint.ts b/src/model/BendPoint.ts index e76b27a0b..9d08cf1c9 100644 --- a/src/model/BendPoint.ts +++ b/src/model/BendPoint.ts @@ -1,6 +1,8 @@ /** * A single point of a bending graph. Used to * describe WhammyBar and String Bending effects. + * @cloneable + * @json */ export class BendPoint { public static readonly MaxPosition: number = 60; @@ -25,15 +27,4 @@ export class BendPoint { this.offset = offset; this.value = value; } - - public static copyTo(src: BendPoint, dst: BendPoint): void { - dst.offset = src.offset; - dst.value = src.value; - } - - public clone(): BendPoint { - let point: BendPoint = new BendPoint(0, 0); - BendPoint.copyTo(this, point); - return point; - } } diff --git a/src/model/Chord.ts b/src/model/Chord.ts index 775cc6e6e..b7b24e6ad 100644 --- a/src/model/Chord.ts +++ b/src/model/Chord.ts @@ -2,6 +2,7 @@ import { Staff } from '@src/model/Staff'; /** * A chord definition. + * @json */ export class Chord { /** @@ -28,6 +29,7 @@ export class Chord { /** * Gets or sets the staff the chord belongs to. + * @json_ignore */ public staff!: Staff; @@ -45,14 +47,4 @@ export class Chord { * Gets or sets whether the fingering is shown below the chord diagram. */ public showFingering: boolean = true; - - public static copyTo(src: Chord, dst: Chord): void { - dst.firstFret = src.firstFret; - dst.name = src.name; - dst.strings = src.strings.slice(0); - dst.barreFrets = src.barreFrets.slice(0); - dst.showName = src.showName; - dst.showDiagram = src.showDiagram; - dst.showFingering = src.showFingering; - } } diff --git a/src/model/Color.ts b/src/model/Color.ts index 2e41f7c7a..e247a4260 100644 --- a/src/model/Color.ts +++ b/src/model/Color.ts @@ -62,93 +62,85 @@ export class Color { return new Color((Math.random() * 255) | 0, (Math.random() * 255) | 0, (Math.random() * 255) | 0, opacity); } - /** - * @target web - */ - public static fromJson(json: unknown): Color | null { - if (!json) { - return null; - } - if (json instanceof Color) { - return json; - } - - switch (typeof json) { + public static fromJson(v: unknown): Color | null { + switch (typeof v) { case 'number': - let c: Color = new Color(0, 0, 0, 0); - c.raw = json | 0; - c.updateRgba(); - return c; - + { + const c = new Color(0, 0, 0, 0); + c.raw = v as number; + c.updateRgba(); + return c; + } case 'string': - if (json.startsWith('#')) { - if (json.length === 4) { - // #RGB - return new Color( - parseInt(json.substring(1, 1), 16) * 17, - parseInt(json.substring(2, 1), 16) * 17, - parseInt(json.substring(3, 1), 16) * 17 - ); - } - - if (json.length === 5) { - // #RGBA - return new Color( - parseInt(json.substring(1, 1), 16) * 17, - parseInt(json.substring(2, 1), 16) * 17, - parseInt(json.substring(3, 1), 16) * 17, - parseInt(json.substring(4, 1), 16) * 17 - ); - } - - if (json.length === 7) { - // #RRGGBB - return new Color( - parseInt(json.substring(1, 2), 16), - parseInt(json.substring(3, 2), 16), - parseInt(json.substring(5, 2), 16) - ); - } - - if (json.length === 9) { - // #RRGGBBAA - return new Color( - parseInt(json.substring(1, 2), 16), - parseInt(json.substring(3, 2), 16), - parseInt(json.substring(5, 2), 16), - parseInt(json.substring(7, 2), 16) - ); - } - } else if (json.startsWith('rgba') || json.startsWith('rgb')) { - const start = json.indexOf('('); - const end = json.lastIndexOf(')'); - if (start === -1 || end === -1) { - throw new FormatError('No values specified for rgb/rgba function'); - } - - const numbers = json.substring(start + 1, end - start - 1).split(','); - if (numbers.length === 3) { - return new Color(parseInt(numbers[0]), parseInt(numbers[1]), parseInt(numbers[2])); - } - - if (numbers.length === 4) { - return new Color( - parseInt(numbers[0]), - parseInt(numbers[1]), - parseInt(numbers[2]), - parseFloat(numbers[3]) * 255 - ); + { + const json = v as string; + if (json.startsWith('#')) { + if (json.length === 4) { + // #RGB + return new Color( + parseInt(json.substring(1, 1), 16) * 17, + parseInt(json.substring(2, 1), 16) * 17, + parseInt(json.substring(3, 1), 16) * 17 + ); + } + + if (json.length === 5) { + // #RGBA + return new Color( + parseInt(json.substring(1, 1), 16) * 17, + parseInt(json.substring(2, 1), 16) * 17, + parseInt(json.substring(3, 1), 16) * 17, + parseInt(json.substring(4, 1), 16) * 17 + ); + } + + if (json.length === 7) { + // #RRGGBB + return new Color( + parseInt(json.substring(1, 2), 16), + parseInt(json.substring(3, 2), 16), + parseInt(json.substring(5, 2), 16) + ); + } + + if (json.length === 9) { + // #RRGGBBAA + return new Color( + parseInt(json.substring(1, 2), 16), + parseInt(json.substring(3, 2), 16), + parseInt(json.substring(5, 2), 16), + parseInt(json.substring(7, 2), 16) + ); + } + } else if (json.startsWith('rgba') || json.startsWith('rgb')) { + const start = json.indexOf('('); + const end = json.lastIndexOf(')'); + if (start === -1 || end === -1) { + throw new FormatError('No values specified for rgb/rgba function'); + } + + const numbers = json.substring(start + 1, end - start - 1).split(','); + if (numbers.length === 3) { + return new Color(parseInt(numbers[0]), parseInt(numbers[1]), parseInt(numbers[2])); + } + + if (numbers.length === 4) { + return new Color( + parseInt(numbers[0]), + parseInt(numbers[1]), + parseInt(numbers[2]), + parseFloat(numbers[3]) * 255 + ); + } } + return null; } - break; } + throw new FormatError('Unsupported format for color'); } - /** - * @target web - */ - public static toJson(obj: Color): unknown { + public static toJson(obj: Color): number { return obj.raw; } } diff --git a/src/model/Fermata.ts b/src/model/Fermata.ts index c95d17307..8c47b3d64 100644 --- a/src/model/Fermata.ts +++ b/src/model/Fermata.ts @@ -18,6 +18,7 @@ export enum FermataType { /** * Represents a fermata. + * @json */ export class Fermata { /** @@ -29,9 +30,4 @@ export class Fermata { * Gets or sets the actual lenght of the fermata. */ public length: number = 0; - - public static copyTo(src: Fermata, dst: Fermata): void { - dst.type = src.type; - dst.length = src.length; - } } diff --git a/src/model/Font.ts b/src/model/Font.ts index d6ca14cca..1af1f2468 100644 --- a/src/model/Font.ts +++ b/src/model/Font.ts @@ -1,4 +1,261 @@ -import { Environment } from '@src/Environment'; +import { JsonHelper } from '@src/io/JsonHelper'; + +/** + * A very basic font parser which parses the fields according to + * https://www.w3.org/TR/CSS21/fonts.html#propdef-font + */ +class FontParserToken { + public startPos: number; + public endPos: number; + public text: string; + public constructor(text: string, startPos: number, endPos: number) { + this.text = text; + this.startPos = startPos; + this.endPos = endPos; + } +} + +class FontParser { + public style: string = 'normal'; + public variant: string = 'normal'; + public weight: string = 'normal'; + public stretch: string = 'normal'; + public lineHeight: string = 'normal'; + public size: string = '1rem'; + public families: string[] = []; + + private _tokens: FontParserToken[]; + private _currentTokenIndex: number = -1; + private _input: string = ''; + private _currentToken: FontParserToken | null = null; + + public constructor(input: string) { + this._input = input; + this._tokens = this.splitToTokens(input);; + } + + private splitToTokens(input: string): FontParserToken[] { + const tokens: FontParserToken[] = []; + + let startPos = 0; + while (startPos < input.length) { + let endPos = startPos; + while (endPos < input.length && input.charAt(endPos) !== ' ') { + endPos++; + } + + if (endPos > startPos) { + tokens.push(new FontParserToken(input.substring(startPos, endPos), startPos, endPos)); + } + + startPos = endPos + 1;; + } + + return tokens; + } + + public parse() { + this.reset(); + // default font flags + if (this._tokens.length === 1) { + switch (this._currentToken?.text) { + case 'caption': + case 'icon': + case 'menu': + case 'message-box': + case 'small-caption': + case 'status-bar': + case 'inherit': + return; + } + } + + this.fontStyleVariantWeight(); + this.fontSizeLineHeight(); + this.fontFamily(); + } + + private fontFamily() { + if (!this._currentToken) { + throw new Error(`Missing font list`); + } + + const familyListInput = this._input.substr(this._currentToken.startPos).trim(); + let pos = 0; + while (pos < familyListInput.length) { + let c = familyListInput.charAt(pos); + if (c === ' ' || c == ',') { + // skip whitespace and quotes + pos++; + } else if (c === '"' || c === "'") { + // quoted + const endOfString = this.findEndOfQuote(familyListInput, pos + 1, c); + this.families.push(familyListInput.substring(pos + 1, endOfString).split("\\" + c).join(c)); + pos = endOfString + 1; + } else { + // until comma + const endOfString = this.findEndOfQuote(familyListInput, pos + 1, ','); + this.families.push(familyListInput.substring(pos, endOfString).trim()); + pos = endOfString + 1; + } + } + } + + private findEndOfQuote(s: string, pos: number, quoteChar: string): number { + let escaped = false; + while (pos < s.length) { + const c = s.charAt(pos); + + if (!escaped && c === quoteChar) { + return pos; + } + + if (!escaped && c === '\\') { + escaped = true; + } else { + escaped = false; + } + pos++; + } + + return s.length; + } + + private fontSizeLineHeight() { + if (!this._currentToken) { + throw new Error(`Missing font size`); + } + + const parts = this._currentToken.text.split('/'); + if (parts.length >= 3) { + throw new Error(`Invalid font size '${this._currentToken}' specified`); + } + + this.nextToken(); + + if (parts.length >= 2) { + if (parts[1] === '/') { + // size/ line-height (space after slash) + if (!this._currentToken) { + throw new Error('Missing line-height after font size'); + } + this.lineHeight = this._currentToken.text; + this.nextToken(); + } else { + // size/line-height (no spaces) + this.size = parts[0]; + this.lineHeight = parts[1]; + } + } else if (parts.length >= 1) { + this.size = parts[0]; + if (this._currentToken?.text.indexOf('/') === 0) { + // size / line-height (with spaces befor and after slash) + if (this._currentToken.text === '/') { + this.nextToken(); + if (!this._currentToken) { + throw new Error('Missing line-height after font size'); + } + this.lineHeight = this._currentToken.text; + this.nextToken(); + } else { + this.lineHeight = this._currentToken.text.substr(1); + this.nextToken(); + } + } + } else { + throw new Error(`Missing font size`); + } + } + + private nextToken() { + this._currentTokenIndex++; + if (this._currentTokenIndex < this._tokens.length) { + this._currentToken = this._tokens[this._currentTokenIndex]; + } else { + this._currentToken = null; + } + } + + private fontStyleVariantWeight() { + let hasStyle = false; + let hasVariant = false; + let hasWeight = false; + let valuesNeeded = 3; + let ambiguous: string[] = []; + + while (true) { + switch (this._currentToken?.text) { + // ambiguous + case 'normal': + case 'inherit': + ambiguous.push(this._currentToken?.text); + valuesNeeded--; + this.nextToken(); + break; + + // style + case 'italic': + case 'oblique': + this.style = this._currentToken?.text; + hasStyle = true; + valuesNeeded--; + this.nextToken(); + break; + // variant + case 'small-caps': + this.variant = this._currentToken?.text; + hasVariant = true; + valuesNeeded--; + this.nextToken(); + break; + + // weight + case 'bold': + case 'bolder': + case 'lighter': + case '100': + case '200': + case '300': + case '400': + case '500': + case '600': + case '700': + case '800': + case '900': + this.weight = this._currentToken?.text; + hasWeight = true; + valuesNeeded--; + this.nextToken(); + break; + default: + // unknown token -> done with this part + return; + } + + if (valuesNeeded === 0) { + break; + } + } + + while (ambiguous.length > 0) { + const v = ambiguous.pop()!; + if (!hasWeight) { + this.weight = v; + } + else if (!hasVariant) { + this.variant = v; + } + else if (!hasStyle) { + this.style = v; + } + } + } + + private reset() { + this._currentTokenIndex = -1; + this.nextToken(); + } +} /** * Lists all flags for font styles. @@ -7,13 +264,15 @@ export enum FontStyle { /** * No flags. */ - Plain, + Plain = 0, /** * Font is bold - */ Bold, + */ + Bold = 1, /** * Font is italic. - */ Italic + */ + Italic = 2 } /** @@ -59,10 +318,6 @@ export class Font { this._css = this.toCssString(1); } - public clone(): Font { - return new Font(this.family, this.size, this.style); - } - public toCssString(scale: number): string { if (!this._css || !(Math.abs(scale - this._cssScale) < 0.01)) { let buf: string = ''; @@ -83,107 +338,98 @@ export class Font { return this._css; } - /** - * @target web - */ - public static fromJson(value: unknown): Font | null { - if (!value) { - return null; - } - - if (value instanceof Font) { - return value; - } - - if (typeof value === 'object' && (value as any).family) { - return new Font((value as any).family, (value as any).size, (value as any).style); - } - - if (typeof value === 'string' && !Environment.isRunningInWorker) { - let el: HTMLElement = document.createElement('span'); - el.setAttribute('style', 'font: ' + value); - let style: CSSStyleDeclaration = el.style; - if (!style.fontFamily) { - style.fontFamily = 'sans-serif'; - } + public static fromJson(v:unknown): Font | null { + switch (typeof v) { + case 'undefined': + return null; + case 'object': + { + const m = v as Map; + let family = m.get('family') as string; + let size = m.get('size') as number; + let style = JsonHelper.parseEnum(m.get('style'), FontStyle)!; + return new Font(family, size, style); + } + case 'string': + { + const parser = new FontParser(v as string); + parser.parse(); - let family: string = style.fontFamily; - if ((family.startsWith("'") && family.endsWith("'")) || (family.startsWith('"') && family.endsWith('"'))) { - family = family.substr(1, family.length - 2); - } + let family: string = parser.families[0]; + if ((family.startsWith("'") && family.endsWith("'")) || (family.startsWith('"') && family.endsWith('"'))) { + family = family.substr(1, family.length - 2); + } - let fontSizeString: string = style.fontSize.toLowerCase(); - let fontSize: number = 0; - // as per https://websemantics.uk/articles/font-size-conversion/ - switch (fontSizeString) { - case 'xx-small': - fontSize = 7; - break; - case 'x-small': - fontSize = 10; - break; - case 'small': - case 'smaller': - fontSize = 13; - break; - case 'medium': - fontSize = 16; - break; - case 'large': - case 'larger': - fontSize = 18; - break; - case 'x-large': - fontSize = 24; - break; - case 'xx-large': - fontSize = 32; - break; - default: - try { - if (fontSizeString.endsWith('em')) { - fontSize = parseFloat(fontSizeString.substr(0, fontSizeString.length - 2)) * 16; - } else if (fontSizeString.endsWith('pt')) { - fontSize = (parseFloat(fontSizeString.substr(0, fontSizeString.length - 2)) * 16.0) / 12.0; - } else if (fontSizeString.endsWith('px')) { - fontSize = parseFloat(fontSizeString.substr(0, fontSizeString.length - 2)); - } else { - fontSize = 12; - } - } catch (e) { - fontSize = 12; + let fontSizeString: string = parser.size.toLowerCase(); + let fontSize: number = 0; + // as per https://websemantics.uk/articles/font-size-conversion/ + switch (fontSizeString) { + case 'xx-small': + fontSize = 7; + break; + case 'x-small': + fontSize = 10; + break; + case 'small': + case 'smaller': + fontSize = 13; + break; + case 'medium': + fontSize = 16; + break; + case 'large': + case 'larger': + fontSize = 18; + break; + case 'x-large': + fontSize = 24; + break; + case 'xx-large': + fontSize = 32; + break; + default: + try { + if (fontSizeString.endsWith('em')) { + fontSize = parseFloat(fontSizeString.substr(0, fontSizeString.length - 2)) * 16; + } else if (fontSizeString.endsWith('pt')) { + fontSize = (parseFloat(fontSizeString.substr(0, fontSizeString.length - 2)) * 16.0) / 12.0; + } else if (fontSizeString.endsWith('px')) { + fontSize = parseFloat(fontSizeString.substr(0, fontSizeString.length - 2)); + } else { + fontSize = 12; + } + } catch (e) { + fontSize = 12; + } + break; } - break; - } - let fontStyle: FontStyle = FontStyle.Plain; - if (style.fontStyle === 'italic') { - fontStyle |= FontStyle.Italic; - } - let fontWeightString: string = style.fontWeight.toLowerCase(); - switch (fontWeightString) { - case 'normal': - case 'lighter': - break; - default: - fontStyle |= FontStyle.Bold; - break; - } + let fontStyle: FontStyle = FontStyle.Plain; + if (parser.style === 'italic') { + fontStyle |= FontStyle.Italic; + } + let fontWeightString: string = parser.weight.toLowerCase(); + switch (fontWeightString) { + case 'normal': + case 'lighter': + break; + default: + fontStyle |= FontStyle.Bold; + break; + } - return new Font(family, fontSize, fontStyle); + return new Font(family, fontSize, fontStyle); + } + default: + return null; } - - return null; } - - /** - * @target web - */ - public static toJson(font: Font): unknown { - return { - family: font.family, - size: font.size, - style: font.style - }; + + public static toJson(font: Font): Map { + const o = new Map(); + o.set('family', font.family); + o.set('size', font.size); + o.set('style', font.style as number); + return o; } } diff --git a/src/model/InstrumentArticulation.ts b/src/model/InstrumentArticulation.ts index bd0eb7ba8..fd50fbbfe 100644 --- a/src/model/InstrumentArticulation.ts +++ b/src/model/InstrumentArticulation.ts @@ -4,6 +4,7 @@ import { MusicFontSymbol } from "./MusicFontSymbol"; /** * Describes an instrument articulation which is used for percussions. + * @json */ export class InstrumentArticulation { /** @@ -51,16 +52,6 @@ export class InstrumentArticulation { this.techniqueSymbolPlacement = techniqueSymbolPlacement; } - public static copyTo(src: any, dst: InstrumentArticulation) { - dst.outputMidiNumber = src.outputMidiNumber; - dst.staffLine = src.staffLine; - dst.noteHeadDefault = src.noteHeadDefault; - dst.noteHeadHalf = src.noteHeadHalf; - dst.noteHeadWhole = src.noteHeadWhole; - dst.techniqueSymbol = src.techniqueSymbol; - dst.techniqueSymbolPlacement = src.techniqueSymbolPlacement; - } - public getSymbol(duration: Duration): MusicFontSymbol { switch (duration) { case Duration.Whole: diff --git a/src/model/JsonConverter.ts b/src/model/JsonConverter.ts index 5bcabba15..86cc07567 100644 --- a/src/model/JsonConverter.ts +++ b/src/model/JsonConverter.ts @@ -3,246 +3,65 @@ import { MetaNumberEvent } from '@src/midi/MetaNumberEvent'; import { MidiEvent } from '@src/midi/MidiEvent'; import { SystemExclusiveEvent } from '@src/midi/SystemExclusiveEvent'; import { MidiFile } from '@src/midi/MidiFile'; -import { Automation } from '@src/model/Automation'; -import { Bar } from '@src/model/Bar'; -import { Beat } from '@src/model/Beat'; -import { BendPoint } from '@src/model/BendPoint'; -import { Chord } from '@src/model/Chord'; -import { Fermata } from '@src/model/Fermata'; -import { MasterBar } from '@src/model/MasterBar'; -import { Note } from '@src/model/Note'; -import { PlaybackInformation } from '@src/model/PlaybackInformation'; -import { RenderStylesheet } from '@src/model/RenderStylesheet'; import { Score } from '@src/model/Score'; -import { Section } from '@src/model/Section'; -import { Staff } from '@src/model/Staff'; -import { Track } from '@src/model/Track'; -import { Voice } from '@src/model/Voice'; import { Settings } from '@src/Settings'; import { Midi20PerNotePitchBendEvent } from '@src/midi/Midi20ChannelVoiceEvent'; -import { InstrumentArticulation } from './InstrumentArticulation'; - -interface SerializedNote { - tieOriginId?: number; - tieDestinationId?: number; - slurOriginId?: number; - slurDestinationId?: number; - hammerPullOriginId?: number; - hammerPullDestinationId?: number; -} +import { ScoreSerializer } from '@src/generated/model/ScoreSerializer'; +import { SettingsSerializer } from '@src/generated/SettingsSerializer'; /** * This class can convert a full {@link Score} instance to a simple JavaScript object and back for further * JSON serialization. - * @target web */ export class JsonConverter { /** - * Converts the given score into a JSON encoded string. - * @param score The score to serialize. - * @returns A JSON encoded string that can be used togehter with for conversion. + * @target web */ - public static scoreToJson(score: Score): string { - let obj: unknown = JsonConverter.scoreToJsObject(score); - return JSON.stringify(obj, (_, v) => { - // patch arraybuffer to serialize as array - if (ArrayBuffer.isView(v)) { - return Array.apply([], [v]); - } - return v; - }); - } - - /** - * Converts the score into a JavaScript object without circular dependencies. - * @param score The score object to serialize - * @returns A serialized score object without ciruclar dependencies that can be used for further serializations. - */ - public static scoreToJsObject(score: Score): unknown { - let score2: Score = {} as any; - Score.copyTo(score, score2); - score2.masterBars = []; - score2.tracks = []; - - score2.stylesheet = {} as any; - RenderStylesheet.copyTo(score.stylesheet, score2.stylesheet); - - JsonConverter.masterBarsToJsObject(score, score2); - JsonConverter.tracksToJsObject(score, score2); - - return score2; - } - - private static tracksToJsObject(score: Score, score2: Score) { - for (let t: number = 0; t < score.tracks.length; t++) { - let track: Track = score.tracks[t]; - let track2: Track = {} as any; - track2.color = {} as any; - Track.copyTo(track, track2); - - track2.playbackInfo = {} as any; - PlaybackInformation.copyTo(track.playbackInfo, track2.playbackInfo); - - track2.percussionArticulations = []; - for(const articulation of track.percussionArticulations) { - const articulation2 = {} as any; - InstrumentArticulation.copyTo(articulation, articulation2); - track2.percussionArticulations.push(articulation2); + private static jsonReplacer(_: any, v: any) { + if (v instanceof Map) { + if ('fromEntries' in Object) { + return (Object as any).fromEntries(v); + } else { + const o: any = {}; + v.forEach((v, k) => o[k] = v); + return o; } - - JsonConverter.stavesToJsObject(track, track2); - score2.tracks.push(track2); } - } - - private static stavesToJsObject(track: Track, track2: Track) { - track2.staves = []; - for (let s: number = 0; s < track.staves.length; s++) { - let staff: Staff = track.staves[s]; - let staff2: Staff = {} as any; - Staff.copyTo(staff, staff2); - staff2.chords = new Map(); - staff.chords.forEach((chord, chordId) => { - let chord2: Chord = {} as any; - Chord.copyTo(chord, chord2); - staff2.chords.set(chordId, chord2); - }); - - JsonConverter.barsToJsObject(staff, staff2); - track2.staves.push(staff2); + else if (ArrayBuffer.isView(v)) { + return Array.apply([], [v]); } + return v; } - private static barsToJsObject(staff: Staff, staff2: Staff) { - staff2.bars = []; - for (let b: number = 0; b < staff.bars.length; b++) { - let bar: Bar = staff.bars[b]; - let bar2: Bar = {} as any; - Bar.copyTo(bar, bar2); - - JsonConverter.voicesToJsObject(bar, bar2); - - staff2.bars.push(bar2); - } - } - - private static voicesToJsObject(bar: Bar, bar2: Bar) { - bar2.voices = []; - for (let v: number = 0; v < bar.voices.length; v++) { - let voice: Voice = bar.voices[v]; - let voice2: Voice = {} as any; - Voice.copyTo(voice, voice2); - - JsonConverter.beatsToJsObject(voice, voice2); - - bar2.voices.push(voice2); - } - } - - private static beatsToJsObject(voice: Voice, voice2: Voice) { - voice2.beats = []; - for (let bb: number = 0; bb < voice.beats.length; bb++) { - let beat: Beat = voice.beats[bb]; - let dynamicBeat2: any = {} as any; - let beat2: Beat = dynamicBeat2; - Beat.copyTo(beat, beat2); - - beat2.automations = []; - for (let a: number = 0; a < beat.automations.length; a++) { - let automation: Automation = {} as any; - Automation.copyTo(beat.automations[a], automation); - beat2.automations.push(automation); - } - - beat2.whammyBarPoints = []; - for (let i: number = 0; i < beat.whammyBarPoints.length; i++) { - let point: BendPoint = {} as any; - BendPoint.copyTo(beat.whammyBarPoints[i], point); - beat2.whammyBarPoints.push(point); - } - - JsonConverter.notesToJsObject(beat, beat2); - - voice2.beats.push(beat2); - } - } - - private static notesToJsObject(beat: Beat, beat2: Beat) { - beat2.notes = []; - for (let n: number = 0; n < beat.notes.length; n++) { - let note: Note = beat.notes[n]; - let dynamicNote2: any = {} as any; - let note2: Note = dynamicNote2; - Note.copyTo(note, note2); - - if (note.isTieDestination) { - dynamicNote2.tieOriginId = note.tieOrigin!.id; - } - - if (note.isTieOrigin) { - dynamicNote2.tieDestinationId = note.tieDestination!.id; - } - - if (note.isSlurDestination) { - dynamicNote2.slurOriginId = note.slurOrigin!.id; - } - - if (note.isSlurOrigin) { - dynamicNote2.slurDestinationId = note.slurDestination!.id; - } - - if (note.isHammerPullDestination) { - dynamicNote2.hammerPullOriginId = note.hammerPullOrigin!.id; - } - - if (note.isHammerPullOrigin) { - dynamicNote2.hammerPullDestinationId = note.hammerPullDestination!.id; - } - - note2.bendPoints = []; - for (let i: number = 0; i < note.bendPoints.length; i++) { - let point: BendPoint = {} as any; - BendPoint.copyTo(note.bendPoints[i], point); - note2.bendPoints.push(point); - } - beat2.notes.push(note2); - } - } - - private static masterBarsToJsObject(score: Score, score2: Score) { - for (let i: number = 0; i < score.masterBars.length; i++) { - let masterBar: MasterBar = score.masterBars[i]; - let masterBar2: MasterBar = {} as any; - MasterBar.copyTo(masterBar, masterBar2); - if (masterBar.tempoAutomation) { - masterBar2.tempoAutomation = {} as any; - Automation.copyTo(masterBar.tempoAutomation, masterBar2.tempoAutomation!); - } - - if (masterBar.section) { - masterBar2.section = {} as any; - Section.copyTo(masterBar.section, masterBar2.section!); - } - - masterBar2.fermata = new Map(); - masterBar.fermata.forEach((fermata, fermataId) => { - let fermata2: any = {} as any; - masterBar2.fermata.set(fermataId, fermata2); - Fermata.copyTo(fermata, fermata2); - }); - - score2.masterBars.push(masterBar2); - } + /** + * Converts the given score into a JSON encoded string. + * @param score The score to serialize. + * @returns A JSON encoded string. + * @target web + */ + public static scoreToJson(score: Score): string { + let obj: unknown = JsonConverter.scoreToJsObject(score); + return JSON.stringify(obj, JsonConverter.jsonReplacer); } /** * Converts the given JSON string back to a {@link Score} object. - * @param json The JSON string that was created via {@link Score} + * @param json The JSON string * @param settings The settings to use during conversion. * @returns The converted score object. + * @target web */ public static jsonToScore(json: string, settings?: Settings): Score { - return JsonConverter.jsObjectToScore(JsonConverter.jsObjectToScore(JSON.parse(json), settings), settings); + return JsonConverter.jsObjectToScore(JSON.parse(json), settings); + } + + /** + * Converts the score into a JavaScript object without circular dependencies. + * @param score The score object to serialize + * @returns A serialized score object without ciruclar dependencies that can be used for further serializations. + */ + public static scoreToJsObject(score: Score): unknown { + return ScoreSerializer.toJson(score); } /** @@ -252,205 +71,57 @@ export class JsonConverter { * @returns The converted score object. */ public static jsObjectToScore(jsObject: unknown, settings?: Settings): Score { - let score: Score = jsObject as Score; - let score2: Score = new Score(); - - Score.copyTo(score, score2); - RenderStylesheet.copyTo(score.stylesheet, score2.stylesheet); - - let allNotes: Map = new Map(); - let notesToLink: Note[] = []; - - JsonConverter.jsObjectToMasterBars(score, score2); - - JsonConverter.jsObjectToTracks(score, score2, allNotes, notesToLink); - - for (let note of notesToLink) { - let serializedNote = note as SerializedNote; - - if (serializedNote.tieOriginId !== undefined) { - note.tieOrigin = allNotes.get(serializedNote.tieOriginId)!; - } - if (serializedNote.tieDestinationId !== undefined) { - note.tieDestination = allNotes.get(serializedNote.tieDestinationId)!; - } - if (serializedNote.slurOriginId !== undefined) { - note.slurOrigin = allNotes.get(serializedNote.slurOriginId)!; - } - if (serializedNote.slurDestinationId !== undefined) { - note.slurDestination = allNotes.get(serializedNote.slurDestinationId)!; - } - if (serializedNote.hammerPullOriginId !== undefined) { - note.hammerPullOrigin = allNotes.get(serializedNote.hammerPullOriginId)!; - } - if (serializedNote.hammerPullDestinationId !== undefined) { - note.hammerPullDestination = allNotes.get(serializedNote.hammerPullDestinationId)!; - } - } - score2.finish(settings ?? new Settings()); - return score2; - } - - private static jsObjectToTracks(score: Score, score2: Score, allNotes: Map, notesToLink: Note[]) { - for (let t: number = 0; t < score.tracks.length; t++) { - let track: Track = score.tracks[t]; - let track2: Track = new Track(); - track2.ensureStaveCount(track.staves.length); - Track.copyTo(track, track2); - score2.addTrack(track2); - PlaybackInformation.copyTo(track.playbackInfo, track2.playbackInfo); - - for(const articulation of track.percussionArticulations) { - const articulation2 = new InstrumentArticulation(); - InstrumentArticulation.copyTo(articulation, articulation2); - track2.percussionArticulations.push(articulation2); - } - - JsonConverter.jsObjectToStaves(track, track2, allNotes, notesToLink); - } - } - - private static jsObjectToStaves(track: Track, track2: Track, allNotes: Map, notesToLink: Note[]) { - for (let s: number = 0; s < track.staves.length; s++) { - let staff: Staff = track.staves[s]; - let staff2: Staff = track2.staves[s]; - Staff.copyTo(staff, staff2); - JsonConverter.jsObjectMapForEach(staff.chords, (chord, chordId) => { - let chord2: Chord = new Chord(); - Chord.copyTo(chord, chord2); - staff2.addChord(chordId, chord2); - }); - - JsonConverter.jsObjectToBars(staff, staff2, allNotes, notesToLink); - } - } - - private static jsObjectMapForEach(obj: any, callback: (value: any, key: any) => void) { - if ('forEach' in obj) { - obj.forEach(callback); - } else { - for (let x in obj) { - if (obj.hasOwnProperty(x)) { - callback(obj[x], x); - } - } - } - } - - private static jsObjectToBars(staff: Staff, staff2: Staff, allNotes: Map, notesToLink: Note[]) { - for (let b: number = 0; b < staff.bars.length; b++) { - let bar: Bar = staff.bars[b]; - let bar2: Bar = new Bar(); - Bar.copyTo(bar, bar2); - staff2.addBar(bar2); - - JsonConverter.jsObjectToVoices(bar, bar2, allNotes, notesToLink); - } + let score: Score = new Score(); + ScoreSerializer.fromJson(score, jsObject); + score.finish(settings ?? new Settings()); + return score; } - private static jsObjectToVoices(bar: Bar, bar2: Bar, allNotes: Map, notesToLink: Note[]) { - for (let v: number = 0; v < bar.voices.length; v++) { - let voice: Voice = bar.voices[v]; - let voice2: Voice = new Voice(); - Voice.copyTo(voice, voice2); - bar2.addVoice(voice2); - JsonConverter.jsObjectToBeats(voice, voice2, allNotes, notesToLink); - } + /** + * Converts the given settings into a JSON encoded string. + * @param settings The settings to serialize. + * @returns A JSON encoded string. + * @target web + */ + public static settingsToJson(settings: Settings): string { + let obj: unknown = JsonConverter.settingsToJsObject(settings); + return JSON.stringify(obj, JsonConverter.jsonReplacer); } - private static jsObjectToBeats(voice: Voice, voice2: Voice, allNotes: Map, notesToLink: Note[]) { - for (let bb: number = 0; bb < voice.beats.length; bb++) { - let beat: Beat = voice.beats[bb]; - let beat2: Beat = new Beat(); - Beat.copyTo(beat, beat2); - voice2.addBeat(beat2); - - for (let a: number = 0; a < beat.automations.length; a++) { - let automation: Automation = new Automation(); - Automation.copyTo(beat.automations[a], automation); - beat2.automations.push(automation); - } - - for (let i: number = 0; i < beat.whammyBarPoints.length; i++) { - let point: BendPoint = new BendPoint(0, 0); - BendPoint.copyTo(beat.whammyBarPoints[i], point); - beat2.addWhammyBarPoint(point); - } - - JsonConverter.jsObjectToNotes(beat, beat2, allNotes, notesToLink); - } + /** + * Converts the given JSON string back to a {@link Score} object. + * @param json The JSON string + * @returns The converted settings object. + * @target web + */ + public static jsonToSettings(json: string): Settings { + return JsonConverter.jsObjectToSettings(JSON.parse(json)); } - private static jsObjectToNotes(beat: Beat, beat2: Beat, allNotes: Map, notesToLink: Note[]) { - for (let n: number = 0; n < beat.notes.length; n++) { - let note: Note = beat.notes[n]; - let note2: Note = new Note(); - Note.copyTo(note, note2); - beat2.addNote(note2); - allNotes.set(note2.id, note2); - - let serializedNote = note as SerializedNote; - let serializedNote2 = note2 as SerializedNote; - - if (serializedNote.tieOriginId !== undefined) { - serializedNote2.tieOriginId = serializedNote.tieOriginId; - notesToLink.push(note2); - } - if (serializedNote.tieDestinationId !== undefined) { - serializedNote2.tieDestinationId = serializedNote.tieDestinationId; - notesToLink.push(note2); - } - if (serializedNote.slurOriginId !== undefined) { - serializedNote2.slurOriginId = serializedNote.slurOriginId; - notesToLink.push(note2); - } - if (serializedNote.slurDestinationId !== undefined) { - serializedNote2.slurDestinationId = serializedNote.slurDestinationId; - notesToLink.push(note2); - } - if (serializedNote.hammerPullOriginId !== undefined) { - serializedNote2.hammerPullOriginId = serializedNote.hammerPullOriginId; - notesToLink.push(note2); - } - if (serializedNote.hammerPullDestinationId !== undefined) { - serializedNote2.hammerPullDestinationId = serializedNote.hammerPullDestinationId; - notesToLink.push(note2); - } - - for (let i: number = 0; i < note.bendPoints.length; i++) { - let point: BendPoint = new BendPoint(0, 0); - BendPoint.copyTo(note.bendPoints[i], point); - note2.addBendPoint(point); - } - } + /** + * Converts the settings object into a JavaScript object for transmission between components or saving purposes. + * @param settings The settings object to serialize + * @returns A serialized settings object without ciruclar dependencies that can be used for further serializations. + */ + public static settingsToJsObject(settings: Settings): Map | null { + return SettingsSerializer.toJson(settings); } - private static jsObjectToMasterBars(score: Score, score2: Score) { - for (let i: number = 0; i < score.masterBars.length; i++) { - let masterBar: MasterBar = score.masterBars[i]; - let masterBar2: MasterBar = new MasterBar(); - MasterBar.copyTo(masterBar, masterBar2); - - if (masterBar.tempoAutomation) { - masterBar2.tempoAutomation = new Automation(); - Automation.copyTo(masterBar.tempoAutomation, masterBar2.tempoAutomation); - } - - if (masterBar.section) { - masterBar2.section = new Section(); - Section.copyTo(masterBar.section, masterBar2.section); - } - - JsonConverter.jsObjectMapForEach(masterBar.fermata, (fermata, key) => { - let fermata2: Fermata = new Fermata(); - Fermata.copyTo(fermata, fermata2); - masterBar2.addFermata(typeof key === 'string' ? parseInt(key) : key, fermata2); - }); - score2.addMasterBar(masterBar2); - } + /** + * Converts the given JavaScript object into a settings object. + * @param jsObject The javascript object created via {@link Settings} + * @returns The converted Settings object. + */ + public static jsObjectToSettings(jsObject: unknown): Settings { + let settings: Settings = new Settings(); + SettingsSerializer.fromJson(settings, jsObject); + return settings; } + /** + * @target web + */ public static jsObjectToMidiFile(midi: any): MidiFile { let midi2: MidiFile = new MidiFile(); midi2.division = midi.division; @@ -486,6 +157,9 @@ export class JsonConverter { return midi2; } + /** + * @target web + */ public static midiFileToJsObject(midi: MidiFile): unknown { let midi2: any = {} as any; midi2.division = midi.division; diff --git a/src/model/MasterBar.ts b/src/model/MasterBar.ts index 1f19c061d..6f01444b6 100644 --- a/src/model/MasterBar.ts +++ b/src/model/MasterBar.ts @@ -12,6 +12,7 @@ import { TripletFeel } from '@src/model/TripletFeel'; /** * The MasterBar stores information about a bar which affects * all tracks. + * @json */ export class MasterBar { public static readonly MaxAlternateEndings: number = 8; @@ -23,16 +24,19 @@ export class MasterBar { /** * Gets or sets the next masterbar in the song. + * @json_ignore */ public nextMasterBar: MasterBar | null = null; /** * Gets or sets the next masterbar in the song. + * @json_ignore */ public previousMasterBar: MasterBar | null = null; /** * Gets the zero based index of the masterbar. + * @json_ignore */ public index: number = 0; @@ -67,6 +71,7 @@ export class MasterBar { /** * Gets or sets the repeat group this bar belongs to. + * @json_ignore */ public repeatGroup!: RepeatGroup; @@ -106,6 +111,7 @@ export class MasterBar { /** * Gets or sets the reference to the score this song belongs to. + * @json_ignore */ public score!: Score; @@ -124,22 +130,6 @@ export class MasterBar { */ public isAnacrusis: boolean = false; - public static copyTo(src: MasterBar, dst: MasterBar): void { - dst.isAnacrusis = src.isAnacrusis; - dst.alternateEndings = src.alternateEndings; - dst.index = src.index; - dst.keySignature = src.keySignature; - dst.keySignatureType = src.keySignatureType; - dst.isDoubleBar = src.isDoubleBar; - dst.isRepeatStart = src.isRepeatStart; - dst.repeatCount = src.repeatCount; - dst.timeSignatureNumerator = src.timeSignatureNumerator; - dst.timeSignatureDenominator = src.timeSignatureDenominator; - dst.timeSignatureCommon = src.timeSignatureCommon; - dst.tripletFeel = src.tripletFeel; - dst.start = src.start; - } - /** * Calculates the time spent in this bar. (unit: midi ticks) */ diff --git a/src/model/Note.ts b/src/model/Note.ts index ef7cb5b11..78f900f98 100644 --- a/src/model/Note.ts +++ b/src/model/Note.ts @@ -25,16 +25,20 @@ import { PercussionMapper } from '@src/model/PercussionMapper'; * A note is a single played sound on a fretted instrument. * It consists of a fret offset and a string on which the note is played on. * It also can be modified by a lot of different effects. + * @cloneable + * @json */ export class Note { public static GlobalNoteId: number = 0; /** * Gets or sets the unique id of this note. + * @clone_ignore */ public id: number = Note.GlobalNoteId++; /** * Gets or sets the zero-based index of this note within the beat. + * @json_ignore */ public index: number = 0; @@ -55,6 +59,8 @@ export class Note { /** * Gets or sets the note from which this note continues the bend. + * @clone_ignore + * @json_ignore */ public bendOrigin: Note | null = null; @@ -65,11 +71,15 @@ export class Note { /** * Gets or sets a list of the points defining the bend behavior. + * @clone_add addBendPoint + * @json_add addBendPoint */ public bendPoints: BendPoint[] = []; /** * Gets or sets the bend point with the highest bend value. + * @clone_ignore + * @json_ignore */ public maxBendPoint: BendPoint | null = null; @@ -247,15 +257,29 @@ export class Note { return !!this.hammerPullOrigin; } + /** + * Gets the origin note id of the hammeron/pull-off of this note. + */ + public hammerPullOriginNoteId: number = -1; + /** * Gets the origin of the hammeron/pulloff of this note. */ - public hammerPullOrigin: Note | null = null; + public get hammerPullOrigin(): Note | null { + return this.hammerPullOriginNoteId === -1 ? null : this.beat.voice.bar.staff.track.score.getNoteById(this.hammerPullOriginNoteId); + } + + /** + * Gets the destination note id of the hammeron/pull-off of this note. + */ + public hammerPullDestinationNoteId: number = -1; /** * Gets the destination for the hammeron/pullof started by this note. */ - public hammerPullDestination: Note | null = null; + public get hammerPullDestination(): Note | null { + return this.hammerPullDestinationNoteId === -1 ? null : this.beat.voice.bar.staff.track.score.getNoteById(this.hammerPullDestinationNoteId); + } public get isSlurOrigin(): boolean { return !!this.slurDestination; @@ -266,15 +290,30 @@ export class Note { */ public isSlurDestination: boolean = false; + + /** + * Gets the note id where the slur of this note starts. + */ + public slurOriginNoteId: number = -1; + /** * Gets or sets the note where the slur of this note starts. */ - public slurOrigin: Note | null = null; + public get slurOrigin(): Note | null { + return this.slurOriginNoteId === -1 ? null : this.beat.voice.bar.staff.track.score.getNoteById(this.slurOriginNoteId); + } + + /** + * Gets or sets the note id where the slur of this note ends. + */ + public slurDestinationNoteId: number = -1; /** * Gets or sets the note where the slur of this note ends. */ - public slurDestination: Note | null = null; + public get slurDestination(): Note | null { + return this.slurDestinationNoteId === -1 ? null : this.beat.voice.bar.staff.track.score.getNoteById(this.slurDestinationNoteId); + } public get isHarmonic(): boolean { return this.harmonicType !== HarmonicType.None; @@ -302,6 +341,8 @@ export class Note { /** * Gets or sets the destination note for the let-ring effect. + * @clone_ignore + * @json_ignore */ public letRingDestination: Note | null = null; @@ -312,6 +353,8 @@ export class Note { /** * Gets or sets the destination note for the palm-mute effect. + * @clone_ignore + * @json_ignore */ public palmMuteDestination: Note | null = null; @@ -337,11 +380,15 @@ export class Note { /** * Gets or sets the target note for several slide types. + * @clone_ignore + * @json_ignore */ public slideTarget: Note | null = null; /** * Gets or sets the source note for several slide types. + * @clone_ignore + * @json_ignore */ public slideOrigin: Note | null = null; @@ -350,15 +397,30 @@ export class Note { */ public vibrato: VibratoType = VibratoType.None; + + /** + * Gets the origin note id of the tied if this note is tied. + */ + public tieOriginNoteId: number = -1; + + /** + * Gets the origin of the tied if this note is tied. + */ + public get tieOrigin(): Note | null { + return this.tieOriginNoteId === -1 ? null : this.beat.voice.bar.staff.track.score.getNoteById(this.tieOriginNoteId); + } + /** - * Gets or sets the origin of the tied if this note is tied. + * Gets the desination note id of the tie. */ - public tieOrigin: Note | null = null; + public tieDestinationNoteId: number = -1; /** - * Gets or sets the desination of the tie. + * Gets the desination of the tie. */ - public tieDestination: Note | null = null; + public get tieDestination(): Note | null { + return this.tieDestinationNoteId === -1 ? null : this.beat.voice.bar.staff.track.score.getNoteById(this.tieDestinationNoteId); + } /** * Gets or sets whether this note is ends a tied note. @@ -366,7 +428,7 @@ export class Note { public isTieDestination: boolean = false; public get isTieOrigin(): boolean { - return !!this.tieDestination; + return this.tieDestinationNoteId !== -1; } /** @@ -414,6 +476,8 @@ export class Note { /** * Gets or sets the reference to the parent beat to which this note belongs to. + * @clone_ignore + * @json_ignore */ public beat!: Beat; @@ -422,16 +486,32 @@ export class Note { */ public dynamics: DynamicValue = DynamicValue.F; + /** + * @clone_ignore + * @json_ignore + */ public isEffectSlurOrigin: boolean = false; + /** + * @clone_ignore + * @json_ignore + */ public hasEffectSlur: boolean = false; public get isEffectSlurDestination(): boolean { return !!this.effectSlurOrigin; } + /** + * @clone_ignore + * @json_ignore + */ public effectSlurOrigin: Note | null = null; + /** + * @clone_ignore + * @json_ignore + */ public effectSlurDestination: Note | null = null; public get stringTuning(): number { @@ -620,53 +700,6 @@ export class Note { return false; } - public static copyTo(src: Note, dst: Note): void { - dst.id = src.id; - dst.accentuated = src.accentuated; - dst.fret = src.fret; - dst.string = src.string; - dst.harmonicValue = src.harmonicValue; - dst.harmonicType = src.harmonicType; - dst.isGhost = src.isGhost; - dst.isLetRing = src.isLetRing; - dst.isPalmMute = src.isPalmMute; - dst.isDead = src.isDead; - dst.isStaccato = src.isStaccato; - dst.slideInType = src.slideInType; - dst.slideOutType = src.slideOutType; - dst.vibrato = src.vibrato; - dst.isTieDestination = src.isTieDestination; - dst.isSlurDestination = src.isSlurDestination; - dst.isHammerPullOrigin = src.isHammerPullOrigin; - dst.leftHandFinger = src.leftHandFinger; - dst.rightHandFinger = src.rightHandFinger; - dst.isFingering = src.isFingering; - dst.trillValue = src.trillValue; - dst.trillSpeed = src.trillSpeed; - dst.durationPercent = src.durationPercent; - dst.accidentalMode = src.accidentalMode; - dst.dynamics = src.dynamics; - dst.octave = src.octave; - dst.tone = src.tone; - dst.percussionArticulation = src.percussionArticulation; - dst.bendType = src.bendType; - dst.bendStyle = src.bendStyle; - dst.isContinuedBend = src.isContinuedBend; - dst.isVisible = src.isVisible; - dst.isLeftHandTapped = src.isLeftHandTapped; - } - - public clone(): Note { - let n: Note = new Note(); - let id: number = n.id; - Note.copyTo(this, n); - for (let i: number = 0, j: number = this.bendPoints.length; i < j; i++) { - n.addBendPoint(this.bendPoints[i].clone()); - } - n.id = id; - return n; - } - public addBendPoint(point: BendPoint): void { this.bendPoints.push(point); if (!this.maxBendPoint || point.value > this.maxBendPoint.value) { @@ -710,12 +743,12 @@ export class Note { } // set hammeron/pulloffs if (this.isHammerPullOrigin) { - this.hammerPullDestination = Note.findHammerPullDestination(this); - - if (!this.hammerPullDestination) { + let hammerPullDestination = Note.findHammerPullDestination(this); + if (!hammerPullDestination) { this.isHammerPullOrigin = false; } else { - this.hammerPullDestination.hammerPullOrigin = this; + this.hammerPullDestinationNoteId = hammerPullDestination.id; + hammerPullDestination.hammerPullOriginNoteId = this.id; } } // set slides @@ -751,7 +784,7 @@ export class Note { // try to detect what kind of bend was used and cleans unneeded points if required // Guitar Pro 6 and above (gpif.xml) uses exactly 4 points to define all bends if (this.bendPoints.length > 0 && this.bendType === BendType.Custom) { - let isContinuedBend: boolean = (this.isContinuedBend = !!this.tieOrigin && this.tieOrigin.hasBend); + let isContinuedBend: boolean = (this.isContinuedBend = this.isTieDestination && this.tieOrigin!.hasBend); if (this.bendPoints.length === 4) { let origin: BendPoint = this.bendPoints[0]; let middle1: BendPoint = this.bendPoints[1]; @@ -927,22 +960,27 @@ export class Note { } public chain() { + this.beat.voice.bar.staff.track.score.registerNote(this); if (!this.isTieDestination) { return; } - if (!this.tieOrigin) { - this.tieOrigin = Note.findTieOrigin(this); + let tieOrigin: Note | null; + if (this.tieOriginNoteId === -1) { + tieOrigin = Note.findTieOrigin(this); + this.tieOriginNoteId = tieOrigin ? tieOrigin.id : -1; + } else { + tieOrigin = this.tieOrigin; } - if (!this.tieOrigin) { + if (!tieOrigin) { this.isTieDestination = false; } else { - this.tieOrigin.tieDestination = this; - this.fret = this.tieOrigin.fret; - this.octave = this.tieOrigin.octave; - this.tone = this.tieOrigin.tone; - if (this.tieOrigin.hasBend) { + tieOrigin.tieDestinationNoteId = this.id; + this.fret = tieOrigin.fret; + this.octave = tieOrigin.octave; + this.tone = tieOrigin.tone; + if (tieOrigin.hasBend) { this.bendOrigin = this.tieOrigin; } } diff --git a/src/model/PlaybackInformation.ts b/src/model/PlaybackInformation.ts index 882599d46..696b09a55 100644 --- a/src/model/PlaybackInformation.ts +++ b/src/model/PlaybackInformation.ts @@ -1,6 +1,7 @@ /** * This public class stores the midi specific information of a track needed * for playback. + * @json */ export class PlaybackInformation { /** @@ -42,15 +43,4 @@ export class PlaybackInformation { * Gets or sets whether the track is playing alone. */ public isSolo: boolean = false; - - public static copyTo(src: PlaybackInformation, dst: PlaybackInformation): void { - dst.volume = src.volume; - dst.balance = src.balance; - dst.port = src.port; - dst.program = src.program; - dst.primaryChannel = src.primaryChannel; - dst.secondaryChannel = src.secondaryChannel; - dst.isMute = src.isMute; - dst.isSolo = src.isSolo; - } } diff --git a/src/model/RenderStylesheet.ts b/src/model/RenderStylesheet.ts index 7dae16ecb..445a56355 100644 --- a/src/model/RenderStylesheet.ts +++ b/src/model/RenderStylesheet.ts @@ -1,14 +1,11 @@ /** * This class represents the rendering stylesheet. * It contains settings which control the display of the score when rendered. + * @json */ export class RenderStylesheet { /** * Gets or sets whether dynamics are hidden. */ public hideDynamics: boolean = false; - - public static copyTo(src: RenderStylesheet, dst: RenderStylesheet): void { - dst.hideDynamics = src.hideDynamics; - } } diff --git a/src/model/Score.ts b/src/model/Score.ts index bc3bc5a31..2baadc108 100644 --- a/src/model/Score.ts +++ b/src/model/Score.ts @@ -3,13 +3,16 @@ import { RenderStylesheet } from '@src/model/RenderStylesheet'; import { RepeatGroup } from '@src/model/RepeatGroup'; import { Track } from '@src/model/Track'; import { Settings } from '@src/Settings'; +import { Note } from './Note'; /** * The score is the root node of the complete * model. It stores the basic information of * a song and stores the sub components. + * @json */ export class Score { + private _noteByIdLookup: Map = new Map(); private _currentRepeatGroup: RepeatGroup = new RepeatGroup(); /** @@ -74,11 +77,13 @@ export class Score { /** * Gets or sets a list of all masterbars contained in this song. + * @json_add addMasterBar */ public masterBars: MasterBar[] = []; /** * Gets or sets a list of all tracks contained in this song. + * @json_add addTrack */ public tracks: Track[] = []; @@ -87,21 +92,6 @@ export class Score { */ public stylesheet: RenderStylesheet = new RenderStylesheet(); - public static copyTo(src: Score, dst: Score): void { - dst.album = src.album; - dst.artist = src.artist; - dst.copyright = src.copyright; - dst.instructions = src.instructions; - dst.music = src.music; - dst.notices = src.notices; - dst.subTitle = src.subTitle; - dst.title = src.title; - dst.words = src.words; - dst.tab = src.tab; - dst.tempo = src.tempo; - dst.tempoLabel = src.tempoLabel; - } - public rebuildRepeatGroups(): void { let currentGroup: RepeatGroup = new RepeatGroup(); for (let bar of this.masterBars) { @@ -140,8 +130,20 @@ export class Score { } public finish(settings: Settings): void { + this._noteByIdLookup.clear(); + for (let i: number = 0, j: number = this.tracks.length; i < j; i++) { this.tracks[i].finish(settings); } } + + public registerNote(note: Note) { + this._noteByIdLookup.set(note.id, note); + } + + public getNoteById(noteId: number): Note | null { + return this._noteByIdLookup.has(noteId) + ? this._noteByIdLookup.get(noteId)! + : null; + } } diff --git a/src/model/Section.ts b/src/model/Section.ts index fbebe9370..cc1fb3821 100644 --- a/src/model/Section.ts +++ b/src/model/Section.ts @@ -1,6 +1,7 @@ /** * This public class is used to describe the beginning of a * section within a song. It acts like a marker. + * @json */ export class Section { /** @@ -12,9 +13,4 @@ export class Section { * Gets or sets the descriptional text of this section. */ public text: string = ''; - - public static copyTo(src: Section, dst: Section): void { - dst.marker = src.marker; - dst.text = src.text; - } } diff --git a/src/model/Staff.ts b/src/model/Staff.ts index 5e74a5ed0..ea301b9ea 100644 --- a/src/model/Staff.ts +++ b/src/model/Staff.ts @@ -6,30 +6,35 @@ import { Settings } from '@src/Settings'; /** * This class describes a single staff within a track. There are instruments like pianos * where a single track can contain multiple staffs. + * @json */ export class Staff { /** * Gets or sets the zero-based index of this staff within the track. + * @json_ignore */ public index: number = 0; /** * Gets or sets the reference to the track this staff belongs to. + * @json_ignore */ public track!: Track; /** * Gets or sets a list of all bars contained in this staff. + * @json_add addBar */ public bars: Bar[] = []; /** * Gets or sets a list of all chords defined for this staff. {@link Beat.chordId} refers to entries in this lookup. + * @json_add addChord */ - public chords: Map = new Map(); + public chords: Map = new Map(); /** - * Gets or sets the fret on which a capo is set. s + * Gets or sets the fret on which a capo is set. */ public capo: number = 0; @@ -82,18 +87,6 @@ export class Staff { */ public standardNotationLineCount: number = 5; - public static copyTo(src: Staff, dst: Staff): void { - dst.capo = src.capo; - dst.index = src.index; - dst.tuning = src.tuning.slice(); - dst.transpositionPitch = src.transpositionPitch; - dst.displayTranspositionPitch = src.displayTranspositionPitch; - dst.showStandardNotation = src.showStandardNotation; - dst.showTablature = src.showTablature; - dst.isPercussion = src.isPercussion; - dst.standardNotationLineCount = src.standardNotationLineCount; - } - public finish(settings: Settings): void { for (let i: number = 0, j: number = this.bars.length; i < j; i++) { this.bars[i].finish(settings); diff --git a/src/model/Track.ts b/src/model/Track.ts index 67beb2d76..7c6327d3b 100644 --- a/src/model/Track.ts +++ b/src/model/Track.ts @@ -10,21 +10,25 @@ import { InstrumentArticulation } from './InstrumentArticulation'; /** * This public class describes a single track or instrument of score. * It is bascially a list of staffs containing individual music notation kinds. + * @json */ export class Track { private static readonly ShortNameMaxLength: number = 10; /** * Gets or sets the zero-based index of this track. + * @json_ignore */ public index: number = 0; /** * Gets or sets the reference this track belongs to. + * @json_ignore */ public score!: Score; /** * Gets or sets the list of staffs that are defined for this track. + * @json_add addStaff */ public staves: Staff[] = []; @@ -66,14 +70,6 @@ export class Track { this.staves.push(staff); } - public static copyTo(src: Track, dst: Track): void { - dst.name = src.name; - dst.shortName = src.shortName; - dst.index = src.index; - dst.color.raw = src.color.raw; - dst.color.rgba = src.color.rgba; - } - public finish(settings: Settings): void { if (!this.shortName) { this.shortName = this.name; diff --git a/src/model/Voice.ts b/src/model/Voice.ts index 77a211da5..5f03671a0 100644 --- a/src/model/Voice.ts +++ b/src/model/Voice.ts @@ -8,22 +8,26 @@ import { Settings } from '@src/Settings'; /** * A voice represents a group of beats * that can be played during a bar. + * @json */ export class Voice { private _beatLookup!: Map; /** * Gets or sets the zero-based index of this voice within the bar. + * @json_ignore */ public index: number = 0; /** * Gets or sets the reference to the bar this voice belongs to. + * @json_ignore */ public bar!: Bar; /** * Gets or sets the list of beats contained in this voice. + * @json_add addBeat */ public beats: Beat[] = []; @@ -32,11 +36,6 @@ export class Voice { */ public isEmpty: boolean = true; - public static copyTo(src: Voice, dst: Voice): void { - dst.index = src.index; - dst.isEmpty = src.isEmpty; - } - public insertBeat(after: Beat, newBeat: Beat): void { newBeat.nextBeat = after.nextBeat; if (newBeat.nextBeat) { diff --git a/src/platform/javascript/AlphaTabWebWorker.ts b/src/platform/javascript/AlphaTabWebWorker.ts index 610693321..cc7f4ad99 100644 --- a/src/platform/javascript/AlphaTabWebWorker.ts +++ b/src/platform/javascript/AlphaTabWebWorker.ts @@ -6,6 +6,7 @@ import { ScoreRenderer } from '@src/rendering/ScoreRenderer'; import { Settings } from '@src/Settings'; import { Logger } from '@src/Logger'; import { Environment } from '@src/Environment'; +import { SettingsSerializer } from '@src/generated/SettingsSerializer'; /** * @target web @@ -28,8 +29,7 @@ export class AlphaTabWebWorker { let cmd: any = data ? data.cmd : ''; switch (cmd) { case 'alphaTab.initialize': - let settings: Settings = new Settings(); - settings.fillFromJson(data.settings); + let settings: Settings = JsonConverter.jsObjectToSettings(data.settings); new Settings(); Logger.logLevel = settings.core.logLevel; this._renderer = new ScoreRenderer(settings); this._renderer.partialRenderFinished.on(result => { @@ -90,7 +90,7 @@ export class AlphaTabWebWorker { } private updateSettings(json: unknown): void { - this._renderer.settings.fillFromJson(json); + SettingsSerializer.fromJson(this._renderer.settings, json); } private renderMultiple(score: Score, trackIndexes: number[]): void { diff --git a/src/platform/javascript/AlphaTabWorkerScoreRenderer.ts b/src/platform/javascript/AlphaTabWorkerScoreRenderer.ts index 77cd6f784..17068de98 100644 --- a/src/platform/javascript/AlphaTabWorkerScoreRenderer.ts +++ b/src/platform/javascript/AlphaTabWorkerScoreRenderer.ts @@ -22,7 +22,7 @@ export class AlphaTabWorkerScoreRenderer implements IScoreRenderer { public constructor(api: AlphaTabApiBase, settings: Settings) { this._api = api; - if(!settings.core.scriptFile) { + if (!settings.core.scriptFile) { Logger.error('Rendering', `Could not detect alphaTab script file, cannot initialize renderer`); return; } @@ -59,10 +59,10 @@ export class AlphaTabWorkerScoreRenderer implements IScoreRenderer { } private serializeSettingsForWorker(settings: Settings): unknown { - let json: any = Settings.toJson(settings); + const jsObject = JsonConverter.settingsToJsObject(settings)!; // cut out player settings, they are only needed on UI thread side - json.player = null; - return json; + jsObject.delete('player'); + return jsObject; } public render(): void { diff --git a/src/platform/javascript/BrowserUiFacade.ts b/src/platform/javascript/BrowserUiFacade.ts index 161bb571a..e3a64dd0f 100644 --- a/src/platform/javascript/BrowserUiFacade.ts +++ b/src/platform/javascript/BrowserUiFacade.ts @@ -23,6 +23,8 @@ import { AlphaTabApi } from '@src/platform/javascript/AlphaTabApi'; import { AlphaTabWorkerScoreRenderer } from '@src/platform/javascript/AlphaTabWorkerScoreRenderer'; import { BrowserMouseEventArgs } from '@src/platform/javascript/BrowserMouseEventArgs'; import { Cursors } from '@src/platform/Cursors'; +import { JsonConverter } from '@src/model/JsonConverter'; +import { SettingsSerializer } from '@src/generated/SettingsSerializer'; /** * @target web @@ -121,12 +123,11 @@ export class BrowserUiFacade implements IUiFacade { if (raw instanceof Settings) { settings = raw; } else { - settings = new Settings(); - settings.fillFromJson(raw); + settings = JsonConverter.jsObjectToSettings(raw); } - + let dataAttributes: Map = this.getDataAttributes(); - settings.fillFromDataAttributes(dataAttributes); + SettingsSerializer.fromJson(settings, dataAttributes); if (settings.notation.notationMode === NotationMode.SongBook) { settings.setSongBookModeSettings(); } @@ -136,7 +137,7 @@ export class BrowserUiFacade implements IUiFacade { api.container.resize.on(this.showSvgsInViewPort.bind(this)); } this.setupFontCheckers(settings); - + this._initialTrackIndexes = this.parseTracks(settings.core.tracks); this._contents = ''; let element: HtmlElementContainer = api.container as HtmlElementContainer; diff --git a/src/rendering/TabBarRenderer.ts b/src/rendering/TabBarRenderer.ts index 9906ea154..b8f900126 100644 --- a/src/rendering/TabBarRenderer.ts +++ b/src/rendering/TabBarRenderer.ts @@ -33,7 +33,7 @@ import { ModelUtils } from '@src/model/ModelUtils'; */ export class TabBarRenderer extends BarRendererBase { public static readonly StaffId: string = 'tab'; - public static readonly LineSpacing: number = 10; + public static readonly TabLineSpacing: number = 10; private _tupletSize: number = 0; @@ -46,7 +46,7 @@ export class TabBarRenderer extends BarRendererBase { } public get lineOffset(): number { - return (TabBarRenderer.LineSpacing + 1) * this.scale; + return (TabBarRenderer.TabLineSpacing + 1) * this.scale; } protected updateSizes(): void { diff --git a/src/rendering/layout/HorizontalScreenLayout.ts b/src/rendering/layout/HorizontalScreenLayout.ts index 37504e592..a3ebaea45 100644 --- a/src/rendering/layout/HorizontalScreenLayout.ts +++ b/src/rendering/layout/HorizontalScreenLayout.ts @@ -17,10 +17,10 @@ export class HorizontalScreenLayoutPartialInfo { * This layout arranges the bars all horizontally */ export class HorizontalScreenLayout extends ScoreLayout { - public static PagePadding: Float32Array = new Float32Array([20, 20, 20, 20]); + public static PagePadding: number[] = [20, 20, 20, 20]; public static readonly GroupSpacing: number = 20; private _group: StaveGroup | null = null; - private _pagePadding: Float32Array | null = null; + private _pagePadding: number[] | null = null; public get name(): string { return 'HorizontalScreen'; @@ -34,7 +34,7 @@ export class HorizontalScreenLayout extends ScoreLayout { return false; } - public resize(): void {} + public resize(): void { } protected doLayoutAndRender(): void { this._pagePadding = this.renderer.settings.display.padding; @@ -42,19 +42,19 @@ export class HorizontalScreenLayout extends ScoreLayout { this._pagePadding = HorizontalScreenLayout.PagePadding; } if (this._pagePadding.length === 1) { - this._pagePadding = new Float32Array([ + this._pagePadding = [ this._pagePadding[0], this._pagePadding[0], this._pagePadding[0], this._pagePadding[0] - ]); + ]; } else if (this._pagePadding.length === 2) { - this._pagePadding = new Float32Array([ + this._pagePadding = [ this._pagePadding[0], this._pagePadding[1], this._pagePadding[0], this._pagePadding[1] - ]); + ]; } let score: Score = this.renderer.score!; let canvas: ICanvas = this.renderer.canvas!; @@ -98,9 +98,9 @@ export class HorizontalScreenLayout extends ScoreLayout { Logger.debug( this.name, 'Finished partial from bar ' + - currentPartial.masterBars[0].index + - ' to ' + - currentPartial.masterBars[currentPartial.masterBars.length - 1].index, + currentPartial.masterBars[0].index + + ' to ' + + currentPartial.masterBars[currentPartial.masterBars.length - 1].index, null ); currentPartial = new HorizontalScreenLayoutPartialInfo(); @@ -118,9 +118,9 @@ export class HorizontalScreenLayout extends ScoreLayout { Logger.debug( this.name, 'Finished partial from bar ' + - currentPartial.masterBars[0].index + - ' to ' + - currentPartial.masterBars[currentPartial.masterBars.length - 1].index, + currentPartial.masterBars[0].index + + ' to ' + + currentPartial.masterBars[currentPartial.masterBars.length - 1].index, null ); } @@ -140,9 +140,9 @@ export class HorizontalScreenLayout extends ScoreLayout { Logger.debug( this.name, 'Rendering partial from bar ' + - partial.masterBars[0].index + - ' to ' + - partial.masterBars[partial.masterBars.length - 1].index, + partial.masterBars[0].index + + ' to ' + + partial.masterBars[partial.masterBars.length - 1].index, null ); this._group.paintPartial( diff --git a/src/rendering/layout/PageViewLayout.ts b/src/rendering/layout/PageViewLayout.ts index 02aadb52f..ccd6ccd97 100644 --- a/src/rendering/layout/PageViewLayout.ts +++ b/src/rendering/layout/PageViewLayout.ts @@ -14,12 +14,12 @@ import { NotationElement } from '@src/NotationSettings'; * This layout arranges the bars into a fixed width and dynamic height region. */ export class PageViewLayout extends ScoreLayout { - public static PagePadding: Float32Array = new Float32Array([40, 40, 40, 40]); + public static PagePadding: number[] = [40, 40, 40, 40]; public static readonly GroupSpacing: number = 20; private _groups: StaveGroup[] = []; private _allMasterBarRenderers: MasterBarsRenderers[] = []; private _barsFromPreviousGroup: MasterBarsRenderers[] = []; - private _pagePadding: Float32Array | null = null; + private _pagePadding: number[] | null = null; public get name(): string { return 'PageView'; @@ -35,19 +35,19 @@ export class PageViewLayout extends ScoreLayout { this._pagePadding = PageViewLayout.PagePadding; } if (this._pagePadding.length === 1) { - this._pagePadding = new Float32Array([ + this._pagePadding = [ this._pagePadding[0], this._pagePadding[0], this._pagePadding[0], this._pagePadding[0] - ]); + ]; } else if (this._pagePadding.length === 2) { - this._pagePadding = new Float32Array([ + this._pagePadding = [ this._pagePadding[0], this._pagePadding[1], this._pagePadding[0], this._pagePadding[1] - ]); + ]; } let x: number = this._pagePadding[0]; let y: number = this._pagePadding[1]; diff --git a/test/TestPlatform.ts b/test/TestPlatform.ts index 6fa8098de..92de3964f 100644 --- a/test/TestPlatform.ts +++ b/test/TestPlatform.ts @@ -34,6 +34,27 @@ export class TestPlatform { }); } + /** + * @target web + */ + public static listDirectory(path: string): Promise { + return new Promise((resolve, reject) => { + let x: XMLHttpRequest = new XMLHttpRequest(); + x.open('GET', 'http://localhost:8090/list-files?dir=' + path, true, null, null); + x.responseType = 'text'; + x.onreadystatechange = () => { + if (x.readyState === XMLHttpRequest.DONE) { + if (x.status === 200) { + resolve(JSON.parse(x.responseText)); + } else { + reject('Could not find path: ' + path + ', received:' + x.responseText); + } + } + }; + x.send(); + }); + } + public static async loadFileAsString(path: string): Promise { const data = await TestPlatform.loadFile(path); return IOHelper.toString(data, 'UTF-8'); diff --git a/test/model/Font.test.ts b/test/model/Font.test.ts new file mode 100644 index 000000000..97f0529ac --- /dev/null +++ b/test/model/Font.test.ts @@ -0,0 +1,48 @@ +import { Font, FontStyle } from "@src/model/Font"; + +describe('FontTests', () => { + function parseText(text: string, expected: Font) { + const font = Font.fromJson(text); + expect(font!.family).toEqual(expected.family); + expect(font!.isBold).toEqual(expected.isBold); + expect(font!.isItalic).toEqual(expected.isItalic); + expect(font!.size).toEqual(expected.size); + expect(font!.style).toEqual(expected.style); + } + + it('parses-full', function () { + parseText('italic small-caps bold 12px/1.5em "Arial"', new Font("Arial", 12, FontStyle.Italic | FontStyle.Bold)) + }); + + it('parses-partial-options', function () { + parseText('italic bold 12px/1.5em "Arial", sans', new Font("Arial", 12, FontStyle.Italic | FontStyle.Bold)) + parseText('bold italic 12px/1.5em "Arial", sans', new Font("Arial", 12, FontStyle.Italic | FontStyle.Bold)) + parseText('bold 12px/1.5em "Arial", sans', new Font("Arial", 12, FontStyle.Bold)) + parseText('italic 12px/1.5em "Arial", sans', new Font("Arial", 12, FontStyle.Italic)) + }); + + it('parses-no-options', function () { + parseText('12px/1.5em "Arial", sans', new Font("Arial", 12, FontStyle.Plain)) + }); + + it('parses-line-height-spaces', function () { + parseText('12px/1.5em "Arial", sans', new Font("Arial", 12, FontStyle.Plain)) + parseText('12px /1.5em "Arial", sans', new Font("Arial", 12, FontStyle.Plain)) + parseText('12px / 1.5em "Arial", sans', new Font("Arial", 12, FontStyle.Plain)) + parseText('12px / 1.5em "Arial", sans', new Font("Arial", 12, FontStyle.Plain)) + }); + + it('parses-multiple-families', function () { + parseText('12px/1.5em Arial, Verdana, sans', new Font("Arial", 12, FontStyle.Plain)) + parseText("12px/1.5em 'Arial', 'Verdana', 'sans'", new Font("Arial", 12, FontStyle.Plain)) + parseText('12px/1.5em "Arial", "Verdana", "sans"', new Font("Arial", 12, FontStyle.Plain)) + parseText('12px/1.5em Arial, "Verdana", sans', new Font("Arial", 12, FontStyle.Plain)) + parseText('12px/1.5em Arial, \'Verdana\', "sans"', new Font("Arial", 12, FontStyle.Plain)) + }); + it('parses-escaped-quotes', function () { + parseText("12px/1.5em \"Ari\\\"al\"", new Font("Ari\"al", 12, FontStyle.Plain)) + parseText('12px/1.5em \'Ari\\\'al\'', new Font("Ari'al", 12, FontStyle.Plain)) + parseText('12px/1.5em \'Ari\\\'\'', new Font('Ari\'', 12, FontStyle.Plain)) + parseText("12px/1.5em 'Ari\\al'", new Font("Ari\\al", 12, FontStyle.Plain)) + }); +}); \ No newline at end of file diff --git a/test/model/JsonConverter.test.ts b/test/model/JsonConverter.test.ts new file mode 100644 index 000000000..1a466bc44 --- /dev/null +++ b/test/model/JsonConverter.test.ts @@ -0,0 +1,294 @@ +import { FingeringMode, LayoutMode, LogLevel, NotationMode, Settings, StaveProfile } from "@src/alphatab"; +import { SettingsSerializer } from "@src/generated/SettingsSerializer"; +import { ScoreLoader } from "@src/importer/ScoreLoader"; +import { Color } from "@src/model/Color"; +import { Font, FontStyle } from "@src/model/Font"; +import { JsonConverter } from "@src/model/JsonConverter"; +import { Score } from "@src/model/Score"; +import { NotationElement, TabRhythmMode } from "@src/NotationSettings"; +import { TestPlatform } from "@test/TestPlatform"; + +describe('JsonConverterTest', () => { + const loadScore: (name: string) => Promise = async (name: string): Promise => { + const data = await TestPlatform.loadFile('test-data/' + name); + try { + return ScoreLoader.loadScoreFromBytes(data); + } + catch (e) { + return null; + } + }; + + function expectJsonEqual(expected: unknown, actual: unknown, path: string) { + const expectedType = typeof expected; + const actualType = typeof actual; + + // NOTE: performance wise expect() seems quite expensive + // that's why we do a manual check for most asserts + + if (actualType != expectedType) { + fail(`Type Mismatch on hierarchy: ${path}, '${actualType}' != '${expectedType}'`); + } + + switch (actualType) { + case 'boolean': + if ((actual as boolean) != (expected as boolean)) { + fail(`Boolean mismatch on hierarchy: ${path}, '${actual}' != '${expected}'`); + } + break; + case 'number': + if (Math.abs((actual as number) - (expected as number)) >= 0.000001) { + fail(`Number mismatch on hierarchy: ${path}, '${actual}' != '${expected}'`); + } + break; + case 'object': + if ((actual === null) !== (expected === null)) { + fail(`Null mismatch on hierarchy: ${path}, '${actual}' != '${expected}'`); + } else if (actual) { + if (Array.isArray(actual) !== Array.isArray(expected)) { + fail(`IsArray mismatch on hierarchy: ${path}`); + } else if (Array.isArray(actual) && Array.isArray(expected)) { + if (actual.length !== expected.length) { + fail(`Array Length mismatch on hierarchy: ${path}, ${actual.length} != ${expected.length}`); + } else { + for (let i = 0; i < actual.length; i++) { + expectJsonEqual(expected[i], actual[i], `${path}[${i}]`); + } + } + } else if (expected instanceof Map) { + if (!(actual instanceof Map)) { + fail(`Map mismatch on hierarchy: ${path}, '${actual}' != '${expected}'`); + } else { + const expectedMap = expected as Map; + const actualMap = actual as Map; + + const expectedKeys = Array.from(expectedMap.keys()); + const actualKeys = Array.from(actualMap.keys()); + expectedKeys.sort(); + actualKeys.sort(); + + const actualKeyList = actualKeys.join(','); + const expectedKeyList = expectedKeys.join(','); + if (actualKeyList !== expectedKeyList) { + fail(`Object Keys mismatch on hierarchy: ${path}, '${actualKeyList}' != '${expectedKeyList}'`); + } else { + for (const key of actualKeys) { + switch (key) { + // some ignored keys + case 'id': + case 'hammerPullOriginId': + case 'hammerPullDestinationId': + case 'tieOriginId': + case 'tieDestinationId': + break; + default: + expectJsonEqual(expectedMap.get(key), actualMap.get(key), `${path}.${key}`); + break; + } + } + + } + } + } else { + fail('Need Map serialization for comparing json objects'); + } + } + break; + case 'string': + if ((actual as string) != (expected as string)) { + fail(`String mismatch on hierarchy: ${path}, '${actual}' != '${expected}'`); + } + break; + case 'undefined': + if (actual !== expected) { + fail(`null mismatch on hierarchy: ${path}, '${actual}' != '${expected}'`); + } + break; + } + } + + const testRoundTripEqual: (name: string) => Promise = async (name: string): Promise => { + try { + const expected = await loadScore(name); + if (!expected) { + return; + } + + const expectedJson = JsonConverter.scoreToJsObject(expected); + const actual = JsonConverter.jsObjectToScore(expectedJson); + const actualJson = JsonConverter.scoreToJsObject(actual); + + expectJsonEqual(expectedJson, actualJson, '<' + name.substr(name.lastIndexOf('/') + 1) + '>'); + } catch (e) { + fail(e); + } + }; + + const testRoundTripFolderEqual: (name: string) => Promise = async (name: string): Promise => { + const files: string[] = await TestPlatform.listDirectory(`test-data/${name}`); + for (const file of files) { + await testRoundTripEqual(`${name}/${file}`); + } + }; + + it('importer', async () => { + await testRoundTripFolderEqual('guitarpro7'); + }); + + it('visual-effects-and-annotations', async () => { + await testRoundTripFolderEqual('visual-tests/effects-and-annotations'); + }); + + it('visual-general', async () => { + await testRoundTripFolderEqual('visual-tests/general'); + }); + + it('visual-guitar-tabs', async () => { + await testRoundTripFolderEqual('visual-tests/guitar-tabs'); + }); + + it('visual-layout', async () => { + await testRoundTripFolderEqual('visual-tests/layout'); + }); + + it('visual-music-notation', async () => { + await testRoundTripFolderEqual('visual-tests/music-notation'); + }); + + it('visual-notation-legend', async () => { + await testRoundTripFolderEqual('visual-tests/notation-legend'); + }); + + it('visual-special-notes', async () => { + await testRoundTripFolderEqual('visual-tests/special-notes'); + }); + + it('visual-special-tracks', async () => { + await testRoundTripFolderEqual('visual-tests/special-tracks'); + }); + + + it('settings', () => { + const expected = new Settings(); + // here we modifiy some properties of each level and some special ones additionally + // to ensure all properties are considered properly + + /**@target web*/ + expected.core.scriptFile = 'script'; + /**@target web*/ + expected.core.fontDirectory = 'font'; + /**@target web*/ + expected.core.tex = true; + /**@target web*/ + expected.core.tracks = [1, 2, 3]; + /**@target web*/ + expected.core.visibilityCheckInterval = 4711; + + expected.core.enableLazyLoading = false; + expected.core.engine = "engine"; + expected.core.logLevel = LogLevel.Error; + expected.core.useWorkers = false; + expected.core.includeNoteBounds = true; + + expected.display.scale = 10; + expected.display.stretchForce = 2; + expected.display.staveProfile = StaveProfile.ScoreTab; + expected.display.barCountPerPartial = 14; + expected.display.resources.copyrightFont = new Font('copy', 15, FontStyle.Plain); + expected.display.resources.staffLineColor = new Color(255, 0, 0, 100); + expected.display.padding = [1, 2, 3, 4]; + + expected.notation.notationMode = NotationMode.SongBook; + expected.notation.fingeringMode = FingeringMode.ScoreForcePiano; + expected.notation.elements.set(NotationElement.EffectCapo, false); + expected.notation.elements.set(NotationElement.ZerosOnDiveWhammys, true); + expected.notation.rhythmMode = TabRhythmMode.ShowWithBars; + expected.notation.rhythmHeight = 100; + expected.notation.transpositionPitches = [1, 2, 3, 4]; + expected.notation.displayTranspositionPitches = [5, 6, 7, 8]; + expected.notation.extendBendArrowsOnTiedNotes = false; + expected.notation.extendLineEffectsToBeatEnd = true; + expected.notation.slurHeight = 50; + + expected.importer.encoding = 'enc'; + expected.importer.mergePartGroupsInMusicXml = false; + + expected.player.soundFont = 'soundfont'; + expected.player.scrollElement = 'scroll'; + expected.player.vibrato.noteSlightAmplitude = 10; + expected.player.slide.simpleSlideDurationRatio = 8; + + const expectedJson = JsonConverter.settingsToJsObject(expected); + const actual = JsonConverter.jsObjectToSettings(expectedJson); + const actualJson = JsonConverter.settingsToJsObject(actual); + + expectJsonEqual(expectedJson, actualJson, ''); + }); + + it('settings-from-map', () => { + const settings = new Settings(); + + const raw = new Map(); + + // json_on_parent + raw.set('enableLazyLoading', false); + // string enum + raw.set('logLevel', 'error'); + raw.set('displayLayoutMode', 1.0 as number); + + // nested + const display = new Map(); + display.set('scale', 5.0 as number); + raw.set('display', display); + + // json_partial_names + raw.set('notationRhythmMode', 'sHoWWITHbArs'); + + // immutable + raw.set('displayResourcesCopyrightFont', 'italic 18px Roboto'); + + SettingsSerializer.fromJson(settings, raw); + + expect(settings.core.enableLazyLoading).toEqual(false); + expect(settings.core.logLevel).toEqual(LogLevel.Error); + expect(settings.display.layoutMode).toEqual(LayoutMode.Horizontal); + expect(settings.display.scale).toEqual(5); + expect(settings.notation.rhythmMode).toEqual(TabRhythmMode.ShowWithBars); + expect(settings.display.resources.copyrightFont.family).toEqual('Roboto'); + expect(settings.display.resources.copyrightFont.size).toEqual(18); + expect(settings.display.resources.copyrightFont.style).toEqual(FontStyle.Italic); + }); + + + /*@target web*/ + it('settings-from-object', () => { + const settings = new Settings(); + + const raw = { + // json_on_parent + enableLazyLoading: false, + // string enum + logLevel: 'error', + displayLayoutMode: 1.0, + // nested + display: { + scale: 5.0 + }, + // json_partial_names + notationRhythmMode: 'sHoWWITHbArs', + // immutable + displayResourcesCopyrightFont: 'italic 18px Roboto' + }; + + SettingsSerializer.fromJson(settings, raw); + + expect(settings.core.enableLazyLoading).toEqual(false); + expect(settings.core.logLevel).toEqual(LogLevel.Error); + expect(settings.display.layoutMode).toEqual(LayoutMode.Horizontal); + expect(settings.display.scale).toEqual(5); + expect(settings.notation.rhythmMode).toEqual(TabRhythmMode.ShowWithBars); + expect(settings.display.resources.copyrightFont.family).toEqual('Roboto'); + expect(settings.display.resources.copyrightFont.size).toEqual(18); + expect(settings.display.resources.copyrightFont.style).toEqual(FontStyle.Italic); + }); +}); diff --git a/test/visualTests/VisualTestHelper.ts b/test/visualTests/VisualTestHelper.ts index 8a18ad725..f94856b50 100644 --- a/test/visualTests/VisualTestHelper.ts +++ b/test/visualTests/VisualTestHelper.ts @@ -9,6 +9,7 @@ import { RenderFinishedEventArgs } from '@src/rendering/RenderFinishedEventArgs' import { AlphaTexImporter } from '@src/importer/AlphaTexImporter'; import { ByteBuffer } from '@src/io/ByteBuffer'; import { PixelMatch } from './PixelMatch'; +import { JsonConverter } from '@src/model/JsonConverter'; /** * @partial @@ -127,7 +128,11 @@ export class VisualTestHelper { api.error.on(e => { reject(`Failed to render image: ${e}`); }); - api.renderScore(score, tracks); + + // NOTE: on some platforms we serialize/deserialize the score objects + // this logic does the same just to ensure we get the right result + const renderScore = JsonConverter.jsObjectToScore(JsonConverter.scoreToJsObject(score), settings); + api.renderScore(renderScore, tracks); }); await Promise.race([ diff --git a/tsconfig.base.json b/tsconfig.base.json index fb3744d17..ec0669491 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -32,15 +32,10 @@ "typeRoots": [ "types", "node_modules/@types" - ], - "plugins": [ - { - "transform": "./src.compiler/JsonSerializationBuilder.ts" - }, ] }, "include": [ - "src" + "src", ], "exclude": [ "**/node_modules",