From acf4ca06438ab7890ff02c942c1d0d0c2e5d1cfc Mon Sep 17 00:00:00 2001 From: Danielku15 Date: Sat, 29 Jun 2024 17:20:56 +0200 Subject: [PATCH] feat: Generate strong types for JSON serialized settings --- package.json | 2 +- src.compiler/BuilderHelpers.ts | 110 ++++-- src.compiler/csharp/CSharpAstTransformer.ts | 2 +- src.compiler/typescript/AlphaTabGenerator.ts | 4 + src.compiler/typescript/EmitterBase.ts | 4 +- .../typescript/JsonDeclarationEmitter.ts | 258 ++++++++++++++ src.compiler/typescript/Serializer.common.ts | 1 + .../typescript/Serializer.setProperty.ts | 314 +++++++++--------- src.compiler/typescript/Serializer.toJson.ts | 10 +- src.compiler/typescript/SerializerEmitter.ts | 1 + src/CoreSettings.ts | 4 +- src/DisplaySettings.ts | 1 + src/ImporterSettings.ts | 1 + src/NotationSettings.ts | 1 + src/PlayerSettings.ts | 3 + src/RenderingResources.ts | 1 + src/Settings.ts | 2 + src/generated/CoreSettingsJson.ts | 65 ++++ src/generated/CoreSettingsSerializer.ts | 2 +- src/generated/DisplaySettingsJson.ts | 111 +++++++ src/generated/ImporterSettingsJson.ts | 27 ++ src/generated/NotationSettingsJson.ts | 67 ++++ src/generated/PlayerSettingsJson.ts | 103 ++++++ src/generated/RenderingResourcesJson.ts | 83 +++++ src/generated/SettingsJson.ts | 47 +++ src/generated/SlidePlaybackSettingsJson.ts | 32 ++ src/generated/VibratoPlaybackSettingsJson.ts | 45 +++ src/model/Color.ts | 13 + src/model/Font.ts | 27 +- src/platform/javascript/AlphaTabApi.ts | 5 +- src/platform/javascript/BrowserUiFacade.ts | 3 +- 31 files changed, 1142 insertions(+), 207 deletions(-) create mode 100644 src.compiler/typescript/JsonDeclarationEmitter.ts create mode 100644 src/generated/CoreSettingsJson.ts create mode 100644 src/generated/DisplaySettingsJson.ts create mode 100644 src/generated/ImporterSettingsJson.ts create mode 100644 src/generated/NotationSettingsJson.ts create mode 100644 src/generated/PlayerSettingsJson.ts create mode 100644 src/generated/RenderingResourcesJson.ts create mode 100644 src/generated/SettingsJson.ts create mode 100644 src/generated/SlidePlaybackSettingsJson.ts create mode 100644 src/generated/VibratoPlaybackSettingsJson.ts diff --git a/package.json b/package.json index 027d31ae2..0a37bc6ae 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,7 @@ "node": ">=6.0.0" }, "scripts": { - "clean": "rimraf dist", + "clean": "rimraf dist && rimraf .rollup.cache", "lint": "eslint .", "start": "node scripts/setup-playground.js && npm run generate-typescript && rollup --config rollup.config.ts --configPlugin @rollup/plugin-typescript -w", "generate-typescript": "rimraf src/generated && tsx src.compiler/typescript/AlphaTabGenerator.ts", diff --git a/src.compiler/BuilderHelpers.ts b/src.compiler/BuilderHelpers.ts index 81b418122..f3c63116d 100644 --- a/src.compiler/BuilderHelpers.ts +++ b/src.compiler/BuilderHelpers.ts @@ -1,9 +1,7 @@ import * as ts from 'typescript'; -import * as path from 'path' +import * as path from 'path'; -const ignoredFiles = [ - /rollup.*/ -] +const ignoredFiles = [/rollup.*/]; export function transpileFilter(file: string): boolean { const fileName = path.basename(file); @@ -58,7 +56,7 @@ function findNode(node: ts.Node, kind: ts.SyntaxKind): ts.Node | null { export function getTypeWithNullableInfo( checker: ts.TypeChecker, - node: ts.TypeNode | undefined, + node: ts.TypeNode | ts.Type | undefined, allowUnionAsPrimitive: boolean ) { if (!node) { @@ -72,31 +70,53 @@ export function getTypeWithNullableInfo( let isNullable = false; let isUnionType = 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) { - if (allowUnionAsPrimitive) { - isUnionType = true; - type = checker.getTypeAtLocation(node); - break; + if ('kind' in node) { + 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) { + if (allowUnionAsPrimitive) { + isUnionType = true; + type = checker.getTypeAtLocation(node); + break; + } else { + 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); + } + } else if ('flags' in node) { + if (node.isUnion()) { + for (const t of node.types) { + if ((t.flags & ts.TypeFlags.Null) !== 0 || (t.flags & ts.TypeFlags.Undefined) !== 0) { + isNullable = true; + } else if (type !== null) { + if (allowUnionAsPrimitive) { + isUnionType = true; + type = node; + break; + } else { + throw new Error('Multi union types on JSON settings not supported: ' + node.symbol.name); + } } else { - throw new Error( - 'Multi union types on JSON settings not supported: ' + - node.getSourceFile().fileName + - ':' + - node.getText() - ); + type = t; } - } else { - type = checker.getTypeAtLocation(t); } + } else { + type = node; } - } else { - type = checker.getTypeAtLocation(node); } return { @@ -201,3 +221,41 @@ function markNodeSynthesized(node: ts.Node): ts.Node { }); return node; } + +export function cloneTypeNode(node: T): T { + if (ts.isUnionTypeNode(node)) { + return ts.factory.createUnionTypeNode(node.types.map(cloneTypeNode)) as any as T; + } else if ( + node.kind === ts.SyntaxKind.StringKeyword || + node.kind === ts.SyntaxKind.NumberKeyword || + node.kind === ts.SyntaxKind.BooleanKeyword || + node.kind === ts.SyntaxKind.UnknownKeyword || + node.kind === ts.SyntaxKind.AnyKeyword || + node.kind === ts.SyntaxKind.VoidKeyword + ) { + return ts.factory.createKeywordTypeNode(node.kind) as any as T; + } else if (ts.isLiteralTypeNode(node)) { + switch (node.literal.kind) { + case ts.SyntaxKind.StringLiteral: + return ts.factory.createLiteralTypeNode(ts.factory.createStringLiteral(node.literal.text)) as any as T; + default: + return ts.factory.createLiteralTypeNode(node.literal) as any as T; + } + } else if (ts.isArrayTypeNode(node)) { + return ts.factory.createArrayTypeNode(cloneTypeNode(node.elementType)) as any as T; + } else if (ts.isTypeReferenceNode(node)) { + return ts.factory.createTypeReferenceNode(cloneTypeNode(node.typeName), + node.typeArguments?.map(a => cloneTypeNode(a)) + ) as any as T; + } else if (ts.isIdentifier(node)) { + return ts.factory.createIdentifier(node.text) as any as T; + } else if (ts.isQualifiedName(node)) { + if (typeof node.right === 'string') { + return ts.factory.createQualifiedName(cloneTypeNode(node.left), node.right) as any as T; + } else { + return ts.factory.createQualifiedName(cloneTypeNode(node.left), cloneTypeNode(node.right)) as any as T; + } + } + + throw new Error(`Unsupported TypeNode: '${ts.SyntaxKind[node.kind]}' extend type node cloning`); +} diff --git a/src.compiler/csharp/CSharpAstTransformer.ts b/src.compiler/csharp/CSharpAstTransformer.ts index 7f0eff918..636e9f693 100644 --- a/src.compiler/csharp/CSharpAstTransformer.ts +++ b/src.compiler/csharp/CSharpAstTransformer.ts @@ -124,7 +124,7 @@ export default class CSharpAstTransformer { } else { additionalNonExportDeclarations.push(s); } - } else { + } else if(!this.shouldSkip(s, false)) { this._context.addTsNodeDiagnostics( s, 'Only FunctionType type aliases are allowed', diff --git a/src.compiler/typescript/AlphaTabGenerator.ts b/src.compiler/typescript/AlphaTabGenerator.ts index 71fe645e8..90c3e25c0 100644 --- a/src.compiler/typescript/AlphaTabGenerator.ts +++ b/src.compiler/typescript/AlphaTabGenerator.ts @@ -4,6 +4,7 @@ import { GENERATED_FILE_HEADER } from './EmitterBase'; import serializerEmit from './SerializerEmitter'; import transpiler from '../TranspilerBase'; import * as fs from 'fs'; +import jsonDeclarationEmit from './JsonDeclarationEmitter'; transpiler([{ name: 'Clone', @@ -11,6 +12,9 @@ transpiler([{ }, { name: 'Serializer', emit: serializerEmit +}, { + name: 'JSON Declarations', + emit: jsonDeclarationEmit }], false); // Write version file diff --git a/src.compiler/typescript/EmitterBase.ts b/src.compiler/typescript/EmitterBase.ts index 51849800b..2c952ebae 100644 --- a/src.compiler/typescript/EmitterBase.ts +++ b/src.compiler/typescript/EmitterBase.ts @@ -21,8 +21,8 @@ export default function createEmitter( const result = generate(program, classDeclaration); const defaultClass = result.statements.find( - stmt => ts.isClassDeclaration(stmt) && stmt.modifiers!.find(m => m.kind === ts.SyntaxKind.ExportKeyword) - ) as ts.ClassDeclaration; + stmt => (ts.isClassDeclaration(stmt) || ts.isInterfaceDeclaration(stmt)) && stmt.modifiers!.find(m => m.kind === ts.SyntaxKind.ExportKeyword) + ) as ts.DeclarationStatement; const targetFileName = path.join( path.resolve(program.getCompilerOptions().baseUrl!), diff --git a/src.compiler/typescript/JsonDeclarationEmitter.ts b/src.compiler/typescript/JsonDeclarationEmitter.ts new file mode 100644 index 000000000..ec7e7a04c --- /dev/null +++ b/src.compiler/typescript/JsonDeclarationEmitter.ts @@ -0,0 +1,258 @@ +/** + * This file contains an emitter which generates classes to clone + * any data models following certain rules. + */ +import * as path from 'path'; +import * as url from 'url'; +import * as ts from 'typescript'; +import createEmitter from './EmitterBase'; +import { + cloneTypeNode, + getTypeWithNullableInfo, + isEnumType, + isPrimitiveType, + unwrapArrayItemType +} from '../BuilderHelpers'; + +function removeExtension(fileName: string) { + return fileName.substring(0, fileName.lastIndexOf('.')); +} + +const __dirname = url.fileURLToPath(new URL('.', import.meta.url)); +const root = path.resolve(__dirname, '..', '..'); + +function toImportPath(fileName: string) { + return '@' + removeExtension(path.relative(root, fileName)).split('\\').join('/'); +} + +function createDefaultJsonTypeNode(checker: ts.TypeChecker, type: ts.Type, isNullable: boolean) { + if (isNullable) { + const notNullable = createDefaultJsonTypeNode(checker, type, false); + return ts.factory.createUnionTypeNode([notNullable, ts.factory.createLiteralTypeNode(ts.factory.createNull())]); + } + + if (isPrimitiveType(type)) { + if ('intrinsicName' in type) { + return ts.factory.createTypeReferenceNode(type.intrinsicName as string); + } else { + return ts.factory.createTypeReferenceNode('TODO'); + } + } else if (checker.isArrayType(type)) { + return ts.factory.createTypeReferenceNode(type.symbol.name); + } else if (type.symbol) { + return ts.factory.createTypeReferenceNode(type.symbol.name); + } else if (type.isStringLiteral()) { + return ts.factory.createLiteralTypeNode(ts.factory.createStringLiteral(type.value)); + } else if (type.isLiteral()) { + if (typeof type.value === 'string') { + return ts.factory.createLiteralTypeNode(ts.factory.createStringLiteral(type.value)); + } else { + return ts.factory.createTypeReferenceNode('TODO'); + } + } else if ('intrinsicName' in type) { + return ts.factory.createTypeReferenceNode(type.intrinsicName as string); + } else { + return ts.factory.createTypeReferenceNode('TODO'); + } +} + +function createJsonTypeNode( + checker: ts.TypeChecker, + type: ts.TypeNode | ts.Type, + importer: (name: string, module: string) => void +): ts.TypeNode | undefined { + const typeInfo = getTypeWithNullableInfo(checker, type!, true); + + if (isEnumType(typeInfo.type)) { + const sourceFile = typeInfo.type.symbol.valueDeclaration?.getSourceFile(); + if (sourceFile) { + importer(typeInfo.type.symbol.name, toImportPath(sourceFile.fileName)); + } + + return ts.factory.createUnionTypeNode([ + ts.factory.createTypeReferenceNode(typeInfo.type.symbol.name), + ts.factory.createTypeOperatorNode( + ts.SyntaxKind.KeyOfKeyword, + ts.factory.createTypeQueryNode(ts.factory.createIdentifier(typeInfo.type.symbol.name)) + ) + ]); + } else if (checker.isArrayType(typeInfo.type)) { + const arrayItemType = unwrapArrayItemType(typeInfo.type, checker); + if (arrayItemType) { + const result = createJsonTypeNode(checker, arrayItemType, importer); + if (result) { + return ts.factory.createArrayTypeNode(result); + } else { + return ts.factory.createArrayTypeNode(createDefaultJsonTypeNode(checker, arrayItemType, false)); + } + } + return undefined; + } else if (!isPrimitiveType(typeInfo.type) && typeInfo.type.isUnion()) { + const types: ts.TypeNode[] = []; + for (const type of typeInfo.type.types) { + const mapped = createJsonTypeNode(checker, type, importer); + if (mapped) { + types.push(mapped); + } else { + types.push(createDefaultJsonTypeNode(checker, type, false)); + } + } + return ts.factory.createUnionTypeNode(types); + } else if (typeInfo.type.symbol) { + const sourceFile = typeInfo.type.symbol.valueDeclaration?.getSourceFile(); + if (sourceFile) { + const isOwnType = !sourceFile.fileName.includes('node_modules'); + + if (isOwnType) { + const isGeneratedJsonDeclaration = ts + .getJSDocTags(typeInfo.type.symbol.valueDeclaration!) + .some(t => t.tagName.text === 'json_declaration'); + if (isGeneratedJsonDeclaration) { + importer(typeInfo.type.symbol.name + 'Json', './' + typeInfo.type.symbol.name + 'Json'); + } else { + importer(typeInfo.type.symbol.name + 'Json', toImportPath(sourceFile.fileName)); + } + } + + let args: ts.TypeNode[] | undefined = undefined; + if ('typeArguments' in typeInfo.type) { + args = []; + for (const arg of (typeInfo.type as ts.TypeReference).typeArguments ?? []) { + const mapped = createJsonTypeNode(checker, arg, importer); + + if (mapped) { + args.push(mapped); + } else { + const argTypeInfo = getTypeWithNullableInfo(checker, arg, true); + args.push(createDefaultJsonTypeNode(checker, argTypeInfo.type, argTypeInfo.isNullable)); + } + } + } + + let symbolType: ts.TypeNode = ts.factory.createTypeReferenceNode( + typeInfo.type.symbol.name + (isOwnType ? 'Json' : ''), + args + ); + + if (typeInfo.type.symbol.name === 'Map' && args && args.length === 2) { + // symbolType = ts.factory.createUnionTypeNode([ + // symbolType, + // ts.factory.createTypeReferenceNode('Record' + // ]); + } + + return symbolType; + } + } + return undefined; +} + +function cloneJsDoc(node: T, source: ts.Node, additionalTags:string[]): T { + const docs = ts + .getJSDocCommentsAndTags(source) + .filter(s => ts.isJSDoc(s)) + .map(s => s.getText()) as string[]; + + for (let text of docs) { + if (text.startsWith('/**')) { + text = text + .substring(2, text.length - 2) + .split('\n') + .map(l => ' ' + l.trimStart()) + .join('\n') + .trimStart(); + + for (const tag of additionalTags) { + if(!text.includes(tag)) { + text += `* ${tag}\n `; + } + } + } + + node = ts.addSyntheticLeadingComment(node, ts.SyntaxKind.MultiLineCommentTrivia, text, true); + } + + return node; +} + +function createJsonMember( + program: ts.Program, + input: ts.PropertyDeclaration, + importer: (name: string, module: string) => void +): ts.TypeElement { + const checker = program.getTypeChecker(); + let type = createJsonTypeNode(checker, input.type!, importer) ?? cloneTypeNode(input.type!); + + let prop = ts.factory.createPropertySignature( + undefined, + input.name, + ts.factory.createToken(ts.SyntaxKind.QuestionToken), + type + ); + + return cloneJsDoc(prop, input, []); +} + +function createJsonMembers( + program: ts.Program, + input: ts.ClassDeclaration, + importer: (name: string, module: string) => void +): ts.TypeElement[] { + return input.members + .filter( + m => + ts.isPropertyDeclaration(m) && + m.modifiers && + !m.modifiers.find(m => m.kind === ts.SyntaxKind.StaticKeyword) + ) + .map(m => createJsonMember(program, m as ts.PropertyDeclaration, importer)); +} + +export default createEmitter('json_declaration', (program, input) => { + console.log(`Writing JSON Type Declaration for ${input.name!.text}`); + const statements: ts.Statement[] = []; + + const imported = new Set(); + function importer(name: string, module: string) { + if (imported.has(name)) { + return; + } + imported.add(name); + + statements.push( + ts.factory.createImportDeclaration( + undefined, + ts.factory.createImportClause( + false, + undefined, + ts.factory.createNamedImports([ + ts.factory.createImportSpecifier(false, undefined, ts.factory.createIdentifier(name)) + ]) + ), + ts.factory.createStringLiteral(module) + ) + ); + } + + statements.push( + cloneJsDoc( + ts.factory.createInterfaceDeclaration( + [ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)], + input.name!.text + 'Json', + undefined, + undefined, + createJsonMembers(program, input, importer) + ), + input, + ['@target web'] + ) + ); + + const sourceFile = ts.factory.createSourceFile( + [...statements], + ts.factory.createToken(ts.SyntaxKind.EndOfFileToken), + ts.NodeFlags.None + ); + + return sourceFile; +}); diff --git a/src.compiler/typescript/Serializer.common.ts b/src.compiler/typescript/Serializer.common.ts index f45aed9de..1c356afca 100644 --- a/src.compiler/typescript/Serializer.common.ts +++ b/src.compiler/typescript/Serializer.common.ts @@ -5,6 +5,7 @@ export interface JsonProperty { partialNames: boolean; property: ts.PropertyDeclaration; jsonNames: string[]; + asRaw: boolean; target?: string; isReadOnly: boolean; } diff --git a/src.compiler/typescript/Serializer.setProperty.ts b/src.compiler/typescript/Serializer.setProperty.ts index 040760cfc..cf52db502 100644 --- a/src.compiler/typescript/Serializer.setProperty.ts +++ b/src.compiler/typescript/Serializer.setProperty.ts @@ -1,5 +1,5 @@ import * as ts from 'typescript'; -import { createNodeFromSource, setMethodBody } from '../BuilderHelpers'; +import { cloneTypeNode, createNodeFromSource, setMethodBody } from '../BuilderHelpers'; import { isPrimitiveType } from '../BuilderHelpers'; import { hasFlag } from '../BuilderHelpers'; import { getTypeWithNullableInfo } from '../BuilderHelpers'; @@ -66,37 +66,6 @@ function isPrimitiveFromJson(type: ts.Type, typeChecker: ts.TypeChecker) { return null; } -function cloneTypeNode(node: T): T { - if (ts.isUnionTypeNode(node)) { - return ts.factory.createUnionTypeNode(node.types.map(cloneTypeNode)) as any as T; - } else if ( - node.kind === ts.SyntaxKind.StringKeyword || - node.kind === ts.SyntaxKind.NumberKeyword || - node.kind === ts.SyntaxKind.BooleanKeyword || - node.kind === ts.SyntaxKind.UnknownKeyword || - node.kind === ts.SyntaxKind.AnyKeyword || - node.kind === ts.SyntaxKind.VoidKeyword - ) { - return ts.factory.createKeywordTypeNode(node.kind) as any as T; - } else if (ts.isLiteralTypeNode(node)) { - return ts.factory.createLiteralTypeNode(node.literal) as any as T; - } else if (ts.isArrayTypeNode(node)) { - return ts.factory.createArrayTypeNode(cloneTypeNode(node.elementType)) as any as T; - } else if (ts.isTypeReferenceNode(node)) { - return ts.factory.createTypeReferenceNode(cloneTypeNode(node.typeName)) as any as T; - } else if (ts.isIdentifier(node)) { - return ts.factory.createIdentifier(node.text) as any as T; - } else if (ts.isQualifiedName(node)) { - if (typeof node.right === 'string') { - return ts.factory.createQualifiedName(cloneTypeNode(node.left), node.right) as any as T; - } else { - return ts.factory.createQualifiedName(cloneTypeNode(node.left), cloneTypeNode(node.right)) as any as T; - } - } - - throw new Error(`Unsupported TypeNode: '${ts.SyntaxKind[node.kind]}' extend type node cloning`); -} - function generateSetPropertyBody( program: ts.Program, serializable: JsonSerializable, @@ -124,7 +93,7 @@ function generateSetPropertyBody( ); }; - if (type.isUnionType) { + if (!prop.asRaw && type.isUnionType) { caseStatements.push( assignField( ts.factory.createAsExpression( @@ -136,7 +105,7 @@ function generateSetPropertyBody( ) ); caseStatements.push(ts.factory.createReturnStatement(ts.factory.createTrue())); - } else if (isPrimitiveFromJson(type.type!, typeChecker)) { + } else if (prop.asRaw || isPrimitiveFromJson(type.type!, typeChecker)) { caseStatements.push( assignField( ts.factory.createAsExpression( @@ -205,7 +174,10 @@ function generateSetPropertyBody( if (type.isNullable) { caseStatements.push( - ts.factory.createIfStatement(ts.factory.createIdentifier('v'), ts.factory.createBlock(loopItems, true)) + ts.factory.createIfStatement( + ts.factory.createIdentifier('v'), + ts.factory.createBlock(loopItems, true) + ) ); } else { caseStatements.push(...loopItems); @@ -292,36 +264,36 @@ function generateSetPropertyBody( ts.factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), ts.factory.createBlock( [ - itemSerializer.length > 0 && - createNodeFromSource( - `const i = new ${mapType.typeArguments![1].symbol.name}();`, - ts.SyntaxKind.VariableStatement - ), - itemSerializer.length > 0 && - createNodeFromSource( - `${itemSerializer}.fromJson(i, v as Map)`, - ts.SyntaxKind.ExpressionStatement - ), - ts.factory.createExpressionStatement( - ts.factory.createCallExpression( - collectionAddMethod - ? ts.factory.createPropertyAccessExpression( + itemSerializer.length > 0 && + createNodeFromSource( + `const i = new ${mapType.typeArguments![1].symbol.name}();`, + ts.SyntaxKind.VariableStatement + ), + itemSerializer.length > 0 && + createNodeFromSource( + `${itemSerializer}.fromJson(i, v as Map)`, + ts.SyntaxKind.ExpressionStatement + ), + 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'), - collectionAddMethod - ) - : ts.factory.createPropertyAccessExpression( - ts.factory.createPropertyAccessExpression( - ts.factory.createIdentifier('obj'), - ts.factory.createIdentifier(fieldName) - ), - ts.factory.createIdentifier('set') + ts.factory.createIdentifier(fieldName) ), - undefined, - [mapKey, mapValue] - ) + ts.factory.createIdentifier('set') + ), + undefined, + [mapKey, mapValue] ) - ].filter(s => !!s) as ts.Statement[], - true + ) + ].filter(s => !!s) as ts.Statement[], + true ) ) ] @@ -400,133 +372,147 @@ function generateSetPropertyBody( : [ 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.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('obj'), - fieldName + ts.factory.createIdentifier(itemSerializer), + 'fromJson' ), - ts.factory.createAsExpression( - ts.factory.createIdentifier('v'), - createStringUnknownMapNode() - ) - ] + [], + [ + ts.factory.createPropertyAccessExpression( + ts.factory.createIdentifier('obj'), + fieldName + ), + ts.factory.createAsExpression( + ts.factory.createIdentifier('v'), + createStringUnknownMapNode() + ) + ] + ) ) - ) - ], true), + ], + true + ), ts.factory.createBlock([assignField(ts.factory.createNull())], true) ), ts.factory.createReturnStatement(ts.factory.createTrue()) ], - true + true ), !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.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(itemSerializer), - 'setProperty' + ts.factory.createIdentifier('property'), + 'indexOf' ), [], - [ - ts.factory.createPropertyAccessExpression( - ts.factory.createIdentifier('obj'), - fieldName + [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('property'), - 'substring' + ts.factory.createIdentifier(itemSerializer), + 'setProperty' ), [], [ ts.factory.createPropertyAccessExpression( - ts.factory.createIdentifier('c'), - 'length' - ) + 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.createIdentifier('v') - ] - ), - ts.factory.createBlock([ - ts.factory.createReturnStatement(ts.factory.createTrue()) - ], true) + ts.factory.createBlock( + [ + ts.factory.createReturnStatement( + ts.factory.createTrue() + ) + ], + true + ) + ) + ].filter(s => !!s) as ts.Statement[], + true ) - ].filter(s => !!s) as ts.Statement[], - true - ) + ) + ], + true ) - ], true) - ) - ], true) + ) + ], + true + ) ) ); } diff --git a/src.compiler/typescript/Serializer.toJson.ts b/src.compiler/typescript/Serializer.toJson.ts index 75dbbdcd2..fdc799f63 100644 --- a/src.compiler/typescript/Serializer.toJson.ts +++ b/src.compiler/typescript/Serializer.toJson.ts @@ -94,13 +94,15 @@ function generateToJsonBody( if (!jsonName || prop.isReadOnly) { continue; } - const typeChecker = program.getTypeChecker(); - const type = getTypeWithNullableInfo(typeChecker, prop.property.type!, false); - const isArray = isTypedArray(type.type!); let propertyStatements: ts.Statement[] = []; + + + const typeChecker = program.getTypeChecker(); + const type = getTypeWithNullableInfo(typeChecker, prop.property.type!, prop.asRaw); + const isArray = isTypedArray(type.type!); - if (isPrimitiveToJson(type.type!, typeChecker)) { + if (prop.asRaw || isPrimitiveToJson(type.type!, typeChecker)) { propertyStatements.push( createNodeFromSource( ` diff --git a/src.compiler/typescript/SerializerEmitter.ts b/src.compiler/typescript/SerializerEmitter.ts index ee6c37cd0..1a60acd4f 100644 --- a/src.compiler/typescript/SerializerEmitter.ts +++ b/src.compiler/typescript/SerializerEmitter.ts @@ -44,6 +44,7 @@ export default createEmitter('json', (program, input) => { serializable.properties.push({ property: propertyDeclaration, jsonNames: jsonNames, + asRaw: !!ts.getJSDocTags(member).find(t => t.tagName.text === 'json_raw'), partialNames: !!ts.getJSDocTags(member).find(t => t.tagName.text === 'json_partial_names'), target: ts.getJSDocTags(member).find(t => t.tagName.text === 'target')?.comment as string, isReadOnly: !!ts.getJSDocTags(member).find(t => t.tagName.text === 'json_read_only') diff --git a/src/CoreSettings.ts b/src/CoreSettings.ts index 67ecfb07a..cb59d45d1 100644 --- a/src/CoreSettings.ts +++ b/src/CoreSettings.ts @@ -3,6 +3,7 @@ import { LogLevel } from '@src/LogLevel'; /** * @json + * @json_declaration */ export class CoreSettings { /** @@ -33,8 +34,9 @@ export class CoreSettings { /** * Gets or sets the initial tracks that should be loaded for the score. * @target web + * @json_raw */ - public tracks: unknown = null; + public tracks: number | number[] | 'all' | null = null; /** * Gets or sets whether lazy loading for displayed elements is enabled. diff --git a/src/DisplaySettings.ts b/src/DisplaySettings.ts index af650961f..88977ddf4 100644 --- a/src/DisplaySettings.ts +++ b/src/DisplaySettings.ts @@ -20,6 +20,7 @@ export enum SystemsLayoutMode { /** * The display settings control how the general layout and display of alphaTab is done. * @json + * @json_declaration */ export class DisplaySettings { /** diff --git a/src/ImporterSettings.ts b/src/ImporterSettings.ts index 0ac429bb9..6b3c289c4 100644 --- a/src/ImporterSettings.ts +++ b/src/ImporterSettings.ts @@ -1,6 +1,7 @@ /** * All settings related to importers that decode file formats. * @json + * @json_declaration */ export class ImporterSettings { /** diff --git a/src/NotationSettings.ts b/src/NotationSettings.ts index f8024c2f1..10830720d 100644 --- a/src/NotationSettings.ts +++ b/src/NotationSettings.ts @@ -293,6 +293,7 @@ export enum NotationElement { /** * The notation settings control how various music notation elements are shown and behaving * @json + * @json_declaration */ export class NotationSettings { /** diff --git a/src/PlayerSettings.ts b/src/PlayerSettings.ts index d69cd453e..56e8a2539 100644 --- a/src/PlayerSettings.ts +++ b/src/PlayerSettings.ts @@ -19,6 +19,7 @@ export enum ScrollMode { /** * This object defines the details on how to generate the vibrato effects. * @json + * @json_declaration */ export class VibratoPlaybackSettings { /** @@ -65,6 +66,7 @@ export class VibratoPlaybackSettings { /** * This object defines the details on how to generate the slide effects. * @json + * @json_declaration */ export class SlidePlaybackSettings { /** @@ -109,6 +111,7 @@ export enum PlayerOutputMode { /** * The player settings control how the audio playback and UI is behaving. * @json + * @json_declaration */ export class PlayerSettings { /** diff --git a/src/RenderingResources.ts b/src/RenderingResources.ts index a6089de99..e9e009e9d 100644 --- a/src/RenderingResources.ts +++ b/src/RenderingResources.ts @@ -4,6 +4,7 @@ import { Font, FontStyle, FontWeight } from '@src/model/Font'; /** * This public class contains central definitions for controlling the visual appearance. * @json + * @json_declaration */ export class RenderingResources { private static sansFont: string = 'Arial, sans-serif'; diff --git a/src/Settings.ts b/src/Settings.ts index 689a2d8da..ef8ebab4a 100644 --- a/src/Settings.ts +++ b/src/Settings.ts @@ -8,6 +8,8 @@ import { SettingsSerializer } from '@src/generated/SettingsSerializer'; /** * This public class contains instance specific settings for alphaTab * @json + * @json_declaration + * @public_api */ export class Settings { /** diff --git a/src/generated/CoreSettingsJson.ts b/src/generated/CoreSettingsJson.ts new file mode 100644 index 000000000..37dddcd78 --- /dev/null +++ b/src/generated/CoreSettingsJson.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 { LogLevel } from "@src/LogLevel"; +/** + * @json + * @json_declaration + * @target web + */ +export interface CoreSettingsJson { + /** + * Gets or sets the script file url that will be used to spawn the workers. + * @target web + */ + scriptFile?: string | null; + /** + * Gets or sets the url to the fonts that will be used to generate the alphaTab font style. + * @target web + */ + fontDirectory?: string | null; + /** + * Gets or sets the file to load directly after initializing alphaTab. + * @target web + */ + file?: string | null; + /** + * Gets or sets whether the UI element contains alphaTex code that should be + * used to initialize alphaTab. + * @target web + */ + tex?: boolean; + /** + * Gets or sets the initial tracks that should be loaded for the score. + * @target web + * @json_raw + */ + tracks?: null | number | number[] | "all"; + /** + * Gets or sets whether lazy loading for displayed elements is enabled. + */ + enableLazyLoading?: boolean; + /** + * The engine which should be used to render the the tablature. + * + * - **default**- Platform specific default engine + * - **html5**- HTML5 Canvas + * - **svg**- SVG + */ + engine?: string; + /** + * The log level to use within alphaTab + */ + logLevel?: LogLevel | keyof typeof LogLevel; + /** + * Gets or sets whether the rendering should be done in a worker if possible. + */ + useWorkers?: boolean; + /** + * Gets or sets whether in the {@link BoundsLookup} also the + * position and area of each individual note is provided. + */ + includeNoteBounds?: boolean; +} diff --git a/src/generated/CoreSettingsSerializer.ts b/src/generated/CoreSettingsSerializer.ts index d201a8ba2..212fab032 100644 --- a/src/generated/CoreSettingsSerializer.ts +++ b/src/generated/CoreSettingsSerializer.ts @@ -55,7 +55,7 @@ export class CoreSettingsSerializer { return true; /*@target web*/ case "tracks": - obj.tracks = v! as unknown; + obj.tracks = v! as number | number[] | "all" | null; return true; case "enablelazyloading": obj.enableLazyLoading = v! as boolean; diff --git a/src/generated/DisplaySettingsJson.ts b/src/generated/DisplaySettingsJson.ts new file mode 100644 index 000000000..44e0e37cb --- /dev/null +++ b/src/generated/DisplaySettingsJson.ts @@ -0,0 +1,111 @@ +// +// This code was auto-generated. +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +import { LayoutMode } from "@src/LayoutMode"; +import { StaveProfile } from "@src/StaveProfile"; +import { RenderingResourcesJson } from "./RenderingResourcesJson"; +import { SystemsLayoutMode } from "@src/DisplaySettings"; +/** + * The display settings control how the general layout and display of alphaTab is done. + * @json + * @json_declaration + * @target web + */ +export interface DisplaySettingsJson { + /** + * Sets the zoom level of the rendered notation + */ + scale?: number; + /** + * The default stretch force to use for layouting. + */ + stretchForce?: number; + /** + * The layouting mode used to arrange the the notation. + */ + layoutMode?: LayoutMode | keyof typeof LayoutMode; + /** + * The stave profile to use. + */ + staveProfile?: StaveProfile | keyof typeof StaveProfile; + /** + * Limit the displayed bars per row. + */ + barsPerRow?: number; + /** + * The bar start number to start layouting with. Note that this is the bar number and not an index! + */ + startBar?: number; + /** + * The amount of bars to render overall. + */ + barCount?: number; + /** + * The number of bars that should be rendered per partial. This setting is not used by all layouts. + */ + barCountPerPartial?: number; + /** + * Whether the last system (row) should be also justified to the whole width of the music sheet. + * (applies only for page layout). + */ + justifyLastSystem?: boolean; + /** + * Gets or sets the resources used during rendering. This defines all fonts and colors used. + * @json_partial_names + */ + resources?: RenderingResourcesJson; + /** + * Gets or sets the padding between the music notation and the border. + */ + padding?: number[]; + /** + * Gets or sets the top padding applied to first system. + */ + firstSystemPaddingTop?: number; + /** + * Gets or sets the top padding applied to systems. + */ + systemPaddingTop?: number; + /** + * Gets or sets the bottom padding applied to systems. + */ + systemPaddingBottom?: number; + /** + * Gets or sets the bottom padding applied to last system. + */ + lastSystemPaddingBottom?: number; + /** + * Gets or sets the padding left to the track name label of the system. + */ + systemLabelPaddingLeft?: number; + /** + * Gets or sets the padding right to the track name label of the system. + */ + systemLabelPaddingRight?: number; + /** + * Gets or sets the padding between the accolade bar and the start of the bar itself. + */ + accoladeBarPaddingRight?: number; + /** + * Gets or sets the top padding applied to main notation staffs. + */ + notationStaffPaddingTop?: number; + /** + * Gets or sets the bottom padding applied to main notation staffs. + */ + notationStaffPaddingBottom?: number; + /** + * Gets or sets the top padding applied to effect annotation staffs. + */ + effectStaffPaddingTop?: number; + /** + * Gets or sets the bottom padding applied to effect annotation staffs. + */ + effectStaffPaddingBottom?: number; + /** + * Gets how the systems should be layed out. + */ + systemsLayoutMode?: SystemsLayoutMode | keyof typeof SystemsLayoutMode; +} diff --git a/src/generated/ImporterSettingsJson.ts b/src/generated/ImporterSettingsJson.ts new file mode 100644 index 000000000..4f74fa7e1 --- /dev/null +++ b/src/generated/ImporterSettingsJson.ts @@ -0,0 +1,27 @@ +// +// This code was auto-generated. +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +/** + * All settings related to importers that decode file formats. + * @json + * @json_declaration + * @target web + */ +export interface ImporterSettingsJson { + /** + * The text encoding to use when decoding strings. By default UTF-8 is used. + */ + encoding?: string; + /** + * If part-groups should be merged into a single track. + */ + mergePartGroupsInMusicXml?: boolean; + /** + * If set to true, text annotations on beats are attempted to be parsed as + * lyrics considering spaces as separators and removing underscores. + * If a track/staff has explicit lyrics the beat texts will not be detected as lyrics. + */ + beatTextAsLyrics?: boolean; +} diff --git a/src/generated/NotationSettingsJson.ts b/src/generated/NotationSettingsJson.ts new file mode 100644 index 000000000..d821a5bfe --- /dev/null +++ b/src/generated/NotationSettingsJson.ts @@ -0,0 +1,67 @@ +// +// This code was auto-generated. +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +import { NotationMode } from "@src/NotationSettings"; +import { FingeringMode } from "@src/NotationSettings"; +import { NotationElement } from "@src/NotationSettings"; +import { TabRhythmMode } from "@src/NotationSettings"; +/** + * The notation settings control how various music notation elements are shown and behaving + * @json + * @json_declaration + * @target web + */ +export interface NotationSettingsJson { + /** + * Gets or sets the mode to use for display and play music notation elements. + */ + notationMode?: NotationMode | keyof typeof NotationMode; + /** + * Gets or sets the fingering mode to use. + */ + fingeringMode?: FingeringMode | keyof typeof FingeringMode; + /** + * Gets or sets the configuration on whether music notation elements are visible or not. + * If notation elements are not specified, the default configuration will be applied. + */ + elements?: Map; + /** + * Whether to show rhythm notation in the guitar tablature. + */ + rhythmMode?: TabRhythmMode | keyof typeof TabRhythmMode; + /** + * The height of the rythm bars. + */ + rhythmHeight?: number; + /** + * The transposition pitch offsets for the individual tracks. + * They apply to rendering and playback. + */ + transpositionPitches?: number[]; + /** + * The transposition pitch offsets for the individual tracks. + * They apply to rendering only. + */ + displayTranspositionPitches?: number[]; + /** + * If set to true the guitar tabs on grace beats are rendered smaller. + */ + smallGraceTabNotes?: boolean; + /** + * If set to true bend arrows expand to the end of the last tied note + * of the string. Otherwise they end on the next beat. + */ + extendBendArrowsOnTiedNotes?: boolean; + /** + * If set to true, line effects (like w/bar, let-ring etc) + * are drawn until the end of the beat instead of the start. + */ + extendLineEffectsToBeatEnd?: boolean; + /** + * Gets or sets the height for slurs. The factor is multiplied with the a logarithmic distance + * between slur start and end. + */ + slurHeight?: number; +} diff --git a/src/generated/PlayerSettingsJson.ts b/src/generated/PlayerSettingsJson.ts new file mode 100644 index 000000000..11a227a15 --- /dev/null +++ b/src/generated/PlayerSettingsJson.ts @@ -0,0 +1,103 @@ +// +// This code was auto-generated. +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +import { PlayerOutputMode } from "@src/PlayerSettings"; +import { ScrollMode } from "@src/PlayerSettings"; +import { VibratoPlaybackSettingsJson } from "./VibratoPlaybackSettingsJson"; +import { SlidePlaybackSettingsJson } from "./SlidePlaybackSettingsJson"; +/** + * The player settings control how the audio playback and UI is behaving. + * @json + * @json_declaration + * @target web + */ +export interface PlayerSettingsJson { + /** + * Gets or sets the URL of the sound font to be loaded. + */ + soundFont?: string | null; + /** + * Gets or sets the element that should be used for scrolling. + * @target web + * @json_read_only + */ + scrollElement?: string | HTMLElement; + /** + * Gets or sets which output mode alphaTab should use. + * @target web + */ + outputMode?: PlayerOutputMode | keyof typeof PlayerOutputMode; + /** + * Gets or sets whether the player should be enabled. + */ + enablePlayer?: boolean; + /** + * Gets or sets whether playback cursors should be displayed. + */ + enableCursor?: boolean; + /** + * Gets or sets whether the beat cursor should be animated or just ticking. + */ + enableAnimatedBeatCursor?: boolean; + /** + * Gets or sets whether the notation elements of the currently played beat should be + * highlighted. + */ + enableElementHighlighting?: boolean; + /** + * Gets or sets alphaTab should provide user interaction features to + * select playback ranges and jump to the playback position by click (aka. seeking). + */ + enableUserInteraction?: boolean; + /** + * Gets or sets the X-offset to add when scrolling. + */ + scrollOffsetX?: number; + /** + * Gets or sets the Y-offset to add when scrolling + */ + scrollOffsetY?: number; + /** + * Gets or sets the mode how to scroll. + */ + scrollMode?: ScrollMode | keyof typeof ScrollMode; + /** + * Gets or sets how fast the scrolling to the new position should happen (in milliseconds) + */ + scrollSpeed?: number; + /** + * Gets or sets whether the native browser smooth scroll mechanism should be used over a custom animation. + * @target web + */ + nativeBrowserSmoothScroll?: boolean; + /** + * Gets or sets the bend duration in milliseconds for songbook bends. + */ + songBookBendDuration?: number; + /** + * Gets or sets the duration of whammy dips in milliseconds for songbook whammys. + */ + songBookDipDuration?: number; + /** + * Gets or sets the settings on how the vibrato audio is generated. + * @json_partial_names + */ + vibrato?: VibratoPlaybackSettingsJson; + /** + * Gets or sets the setitngs on how the slide audio is generated. + * @json_partial_names + */ + slide?: SlidePlaybackSettingsJson; + /** + * Gets or sets whether the triplet feel should be applied/played during audio playback. + */ + playTripletFeel?: boolean; + /** + * Gets or sets how many milliseconds of audio samples should be buffered in total. + * Larger buffers cause a delay from when audio settings like volumes will be applied. + * Smaller buffers can cause audio crackling due to constant buffering that is happening. + */ + bufferTimeInMilliseconds?: number; +} diff --git a/src/generated/RenderingResourcesJson.ts b/src/generated/RenderingResourcesJson.ts new file mode 100644 index 000000000..7108bc8df --- /dev/null +++ b/src/generated/RenderingResourcesJson.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 { FontJson } from "@src/model/Font"; +import { ColorJson } from "@src/model/Color"; +/** + * This public class contains central definitions for controlling the visual appearance. + * @json + * @json_declaration + * @target web + */ +export interface RenderingResourcesJson { + /** + * Gets or sets the font to use for displaying the songs copyright information in the header of the music sheet. + */ + copyrightFont?: FontJson; + /** + * Gets or sets the font to use for displaying the songs title in the header of the music sheet. + */ + titleFont?: FontJson; + /** + * Gets or sets the font to use for displaying the songs subtitle in the header of the music sheet. + */ + subTitleFont?: FontJson; + /** + * Gets or sets the font to use for displaying the lyrics information in the header of the music sheet. + */ + wordsFont?: FontJson; + /** + * Gets or sets the font to use for displaying certain effect related elements in the music sheet. + */ + effectFont?: FontJson; + /** + * Gets or sets the font to use for displaying the fretboard numbers in chord diagrams. + */ + fretboardNumberFont?: FontJson; + /** + * Gets or sets the font to use for displaying the guitar tablature numbers in the music sheet. + */ + tablatureFont?: FontJson; + /** + * Gets or sets the font to use for grace notation related texts in the music sheet. + */ + graceFont?: FontJson; + /** + * Gets or sets the color to use for rendering the lines of staves. + */ + staffLineColor?: ColorJson; + /** + * Gets or sets the color to use for rendering bar separators, the accolade and repeat signs. + */ + barSeparatorColor?: ColorJson; + /** + * Gets or sets the font to use for displaying the bar numbers above the music sheet. + */ + barNumberFont?: FontJson; + /** + * Gets or sets the color to use for displaying the bar numbers above the music sheet. + */ + barNumberColor?: ColorJson; + /** + * Gets or sets the font to use for displaying finger information in the music sheet. + */ + fingeringFont?: FontJson; + /** + * Gets or sets the font to use for section marker labels shown above the music sheet. + */ + markerFont?: FontJson; + /** + * Gets or sets the color to use for music notation elements of the primary voice. + */ + mainGlyphColor?: ColorJson; + /** + * Gets or sets the color to use for music notation elements of the secondary voices. + */ + secondaryGlyphColor?: ColorJson; + /** + * Gets or sets the color to use for displaying the song information above the music sheet. + */ + scoreInfoColor?: ColorJson; +} diff --git a/src/generated/SettingsJson.ts b/src/generated/SettingsJson.ts new file mode 100644 index 000000000..bb8f467e2 --- /dev/null +++ b/src/generated/SettingsJson.ts @@ -0,0 +1,47 @@ +// +// This code was auto-generated. +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +import { CoreSettingsJson } from "./CoreSettingsJson"; +import { DisplaySettingsJson } from "./DisplaySettingsJson"; +import { NotationSettingsJson } from "./NotationSettingsJson"; +import { ImporterSettingsJson } from "./ImporterSettingsJson"; +import { PlayerSettingsJson } from "./PlayerSettingsJson"; +/** + * This public class contains instance specific settings for alphaTab + * @json + * @json_declaration + * @public_api + * @target web + */ +export interface SettingsJson { + /** + * The core settings control the general behavior of alphatab like + * what modules are active. + * @json_on_parent + * @json_partial_names + */ + core?: CoreSettingsJson; + /** + * The display settings control how the general layout and display of alphaTab is done. + * @json_on_parent + * @json_partial_names + */ + display?: DisplaySettingsJson; + /** + * The notation settings control how various music notation elements are shown and behaving. + * @json_partial_names + */ + notation?: NotationSettingsJson; + /** + * All settings related to importers that decode file formats. + * @json_partial_names + */ + importer?: ImporterSettingsJson; + /** + * Contains all player related settings + * @json_partial_names + */ + player?: PlayerSettingsJson; +} diff --git a/src/generated/SlidePlaybackSettingsJson.ts b/src/generated/SlidePlaybackSettingsJson.ts new file mode 100644 index 000000000..02fef3827 --- /dev/null +++ b/src/generated/SlidePlaybackSettingsJson.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. +// +/** + * This object defines the details on how to generate the slide effects. + * @json + * @json_declaration + * @target web + */ +export interface SlidePlaybackSettingsJson { + /** + * Gets or sets 1/4 tones (bend value) offset that + * simple slides like slide-out-below or slide-in-above use. + */ + simpleSlidePitchOffset?: number; + /** + * Gets or sets the percentage which the simple slides should take up + * from the whole note. for "slide into" effects the slide will take place + * from time 0 where the note is plucked to 25% of the overall note duration. + * For "slide out" effects the slide will start 75% and finish at 100% of the overall + * note duration. + */ + simpleSlideDurationRatio?: number; + /** + * Gets or sets the percentage which the legato and shift slides should take up + * from the whole note. For a value 0.5 the sliding will start at 50% of the overall note duration + * and finish at 100% + */ + shiftSlideDurationRatio?: number; +} diff --git a/src/generated/VibratoPlaybackSettingsJson.ts b/src/generated/VibratoPlaybackSettingsJson.ts new file mode 100644 index 000000000..0363a0a12 --- /dev/null +++ b/src/generated/VibratoPlaybackSettingsJson.ts @@ -0,0 +1,45 @@ +// +// This code was auto-generated. +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +/** + * This object defines the details on how to generate the vibrato effects. + * @json + * @json_declaration + * @target web + */ +export interface VibratoPlaybackSettingsJson { + /** + * Gets or sets the wavelength of the note-wide vibrato in midi ticks. + */ + noteWideLength?: number; + /** + * Gets or sets the amplitude for the note-wide vibrato in semitones. + */ + noteWideAmplitude?: number; + /** + * Gets or sets the wavelength of the note-slight vibrato in midi ticks. + */ + noteSlightLength?: number; + /** + * Gets or sets the amplitude for the note-slight vibrato in semitones. + */ + noteSlightAmplitude?: number; + /** + * Gets or sets the wavelength of the beat-wide vibrato in midi ticks. + */ + beatWideLength?: number; + /** + * Gets or sets the amplitude for the beat-wide vibrato in semitones. + */ + beatWideAmplitude?: number; + /** + * Gets or sets the wavelength of the beat-slight vibrato in midi ticks. + */ + beatSlightLength?: number; + /** + * Gets or sets the amplitude for the beat-slight vibrato in semitones. + */ + beatSlightAmplitude?: number; +} diff --git a/src/model/Color.ts b/src/model/Color.ts index 3d336e41f..60b09a4b2 100644 --- a/src/model/Color.ts +++ b/src/model/Color.ts @@ -1,6 +1,15 @@ import { FormatError } from '@src/FormatError'; import { ModelUtils } from '@src/model/ModelUtils'; +/** + * Describes a color for rendering. + * If provided as string one of these formats needs to be used: #RGB, #RGBA, #RRGGBB, #RRGGBBAA, rgb(r,g,b), rgba(r,g,b,a) + * If provided as number a raw RGBA value needs to be used. + * + * @target web + */ +export type ColorJson = Color | string | number; + /** * @json_immutable */ @@ -62,6 +71,10 @@ export class Color { } public static fromJson(v: unknown): Color | null { + if (v instanceof Color) { + return v; + } + switch (typeof v) { case 'number': { const c = new Color(0, 0, 0, 0); diff --git a/src/model/Font.ts b/src/model/Font.ts index 7df966f5e..cccdaa14e 100644 --- a/src/model/Font.ts +++ b/src/model/Font.ts @@ -87,7 +87,7 @@ class FontParser { private fontFamily() { if (!this._currentToken) { - if(this.parseOnlyFamilies) { + if (this.parseOnlyFamilies) { return; } else { throw new Error(`Missing font list`); @@ -279,11 +279,11 @@ class FontParser { } public static quoteFont(f: string): string { - if(f.indexOf(' ') === -1) { + if (f.indexOf(' ') === -1) { return f; } - const escapedQuotes = f.replaceAll('"', '\\"'); + const escapedQuotes = f.replaceAll('"', '\\"'); return `"${escapedQuotes}"`; } } @@ -316,6 +316,21 @@ export enum FontWeight { Bold = 1 } +/** + * Describes a font to be used. + * If specified as string, a CSS `font` shorthand property compliant value needs to be used. + * @target web + */ +export type FontJson = + | Font + | string + | { + families: string[]; + size: number; + style: FontStyle | keyof typeof FontStyle; + weight: FontWeight | keyof typeof FontWeight; + }; + /** * @json_immutable */ @@ -448,7 +463,7 @@ export class Font { style: FontStyle = FontStyle.Plain, weight: FontWeight = FontWeight.Regular ) { - const f = new Font("", size, style, weight); + const f = new Font('', size, style, weight); f.families = families; return f; } @@ -472,6 +487,10 @@ export class Font { } public static fromJson(v: unknown): Font | null { + if(v instanceof Font) { + return v; + } + switch (typeof v) { case 'undefined': return null; diff --git a/src/platform/javascript/AlphaTabApi.ts b/src/platform/javascript/AlphaTabApi.ts index 19b595f9b..80b131345 100644 --- a/src/platform/javascript/AlphaTabApi.ts +++ b/src/platform/javascript/AlphaTabApi.ts @@ -11,12 +11,13 @@ import { ProgressEventArgs } from '@src/ProgressEventArgs'; import { Settings } from '@src/Settings'; import { JsonConverter } from '@src/model/JsonConverter'; import { SettingsSerializer } from '@src/generated/SettingsSerializer'; +import { SettingsJson } from '@src/generated/SettingsJson'; /** * @target web */ -export class AlphaTabApi extends AlphaTabApiBase { - public constructor(element: HTMLElement, options: any | Settings) { +export class AlphaTabApi extends AlphaTabApiBase { + public constructor(element: HTMLElement, options: SettingsJson | Settings) { super(new BrowserUiFacade(element), options); } diff --git a/src/platform/javascript/BrowserUiFacade.ts b/src/platform/javascript/BrowserUiFacade.ts index 02ba6d4f5..7fb82bc5e 100644 --- a/src/platform/javascript/BrowserUiFacade.ts +++ b/src/platform/javascript/BrowserUiFacade.ts @@ -30,6 +30,7 @@ import { AlphaTabError, AlphaTabErrorType } from '@src/AlphaTabError'; import { AlphaSynthAudioWorkletOutput } from '@src/platform/javascript/AlphaSynthAudioWorkletOutput'; import { ScalableHtmlElementContainer } from './ScalableHtmlElementContainer'; import { PlayerOutputMode } from '@src/PlayerSettings'; +import { SettingsJson } from '@src/generated/SettingsJson'; /** * @target web @@ -166,7 +167,7 @@ export class BrowserUiFacade implements IUiFacade { return new AlphaTabWorkerScoreRenderer(this._api, this._api.settings); } - public initialize(api: AlphaTabApiBase, raw: any | Settings): void { + public initialize(api: AlphaTabApiBase, raw: SettingsJson | Settings): void { this._api = api; let settings: Settings; if (raw instanceof Settings) {