/** * @license * Copyright Google Inc. All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ import {CompileAnimationAnimateMetadata, CompileAnimationEntryMetadata, CompileAnimationGroupMetadata, CompileAnimationKeyframesSequenceMetadata, CompileAnimationMetadata, CompileAnimationSequenceMetadata, CompileAnimationStateDeclarationMetadata, CompileAnimationStateTransitionMetadata, CompileAnimationStyleMetadata, CompileAnimationWithStepsMetadata, CompileDirectiveMetadata} from '../compile_metadata'; import {ListWrapper, StringMapWrapper} from '../facade/collection'; import {isBlank, isPresent} from '../facade/lang'; import {ParseError} from '../parse_util'; import {ANY_STATE, FILL_STYLE_FLAG} from '../private_import_core'; import {AnimationAst, AnimationEntryAst, AnimationGroupAst, AnimationKeyframeAst, AnimationSequenceAst, AnimationStateDeclarationAst, AnimationStateTransitionAst, AnimationStateTransitionExpression, AnimationStepAst, AnimationStylesAst, AnimationWithStepsAst} from './animation_ast'; import {StylesCollection} from './styles_collection'; const _INITIAL_KEYFRAME = 0; const _TERMINAL_KEYFRAME = 1; const _ONE_SECOND = 1000; declare type Styles = { [key: string]: string | number }; export class AnimationParseError extends ParseError { constructor(message: string) { super(null, message); } toString(): string { return `${this.msg}`; } } export class AnimationEntryParseResult { constructor(public ast: AnimationEntryAst, public errors: AnimationParseError[]) {} } export class AnimationParser { parseComponent(component: CompileDirectiveMetadata): AnimationEntryAst[] { const errors: string[] = []; const componentName = component.type.name; const animationTriggerNames = new Set(); const asts = component.template.animations.map(entry => { const result = this.parseEntry(entry); const ast = result.ast; const triggerName = ast.name; if (animationTriggerNames.has(triggerName)) { result.errors.push(new AnimationParseError( `The animation trigger "${triggerName}" has already been registered for the ${componentName} component`)); } else { animationTriggerNames.add(triggerName); } if (result.errors.length > 0) { let errorMessage = `- Unable to parse the animation sequence for "${triggerName}" on the ${componentName} component due to the following errors:`; result.errors.forEach( (error: AnimationParseError) => { errorMessage += '\n-- ' + error.msg; }); errors.push(errorMessage); } return ast; }); if (errors.length > 0) { const errorString = errors.join('\n'); throw new Error(`Animation parse errors:\n${errorString}`); } return asts; } parseEntry(entry: CompileAnimationEntryMetadata): AnimationEntryParseResult { var errors: AnimationParseError[] = []; var stateStyles: {[key: string]: AnimationStylesAst} = {}; var transitions: CompileAnimationStateTransitionMetadata[] = []; var stateDeclarationAsts: AnimationStateDeclarationAst[] = []; entry.definitions.forEach(def => { if (def instanceof CompileAnimationStateDeclarationMetadata) { _parseAnimationDeclarationStates(def, errors).forEach(ast => { stateDeclarationAsts.push(ast); stateStyles[ast.stateName] = ast.styles; }); } else { transitions.push(def); } }); var stateTransitionAsts = transitions.map(transDef => _parseAnimationStateTransition(transDef, stateStyles, errors)); var ast = new AnimationEntryAst(entry.name, stateDeclarationAsts, stateTransitionAsts); return new AnimationEntryParseResult(ast, errors); } } function _parseAnimationDeclarationStates( stateMetadata: CompileAnimationStateDeclarationMetadata, errors: AnimationParseError[]): AnimationStateDeclarationAst[] { var styleValues: Styles[] = []; stateMetadata.styles.styles.forEach(stylesEntry => { // TODO (matsko): change this when we get CSS class integration support if (typeof stylesEntry === 'object' && stylesEntry !== null) { styleValues.push(stylesEntry as Styles); } else { errors.push(new AnimationParseError( `State based animations cannot contain references to other states`)); } }); var defStyles = new AnimationStylesAst(styleValues); var states = stateMetadata.stateNameExpr.split(/\s*,\s*/); return states.map(state => new AnimationStateDeclarationAst(state, defStyles)); } function _parseAnimationStateTransition( transitionStateMetadata: CompileAnimationStateTransitionMetadata, stateStyles: {[key: string]: AnimationStylesAst}, errors: AnimationParseError[]): AnimationStateTransitionAst { var styles = new StylesCollection(); var transitionExprs: AnimationStateTransitionExpression[] = []; var transitionStates = transitionStateMetadata.stateChangeExpr.split(/\s*,\s*/); transitionStates.forEach( expr => { transitionExprs.push(..._parseAnimationTransitionExpr(expr, errors)); }); var entry = _normalizeAnimationEntry(transitionStateMetadata.steps); var animation = _normalizeStyleSteps(entry, stateStyles, errors); var animationAst = _parseTransitionAnimation(animation, 0, styles, stateStyles, errors); if (errors.length == 0) { _fillAnimationAstStartingKeyframes(animationAst, styles, errors); } var stepsAst: AnimationWithStepsAst = (animationAst instanceof AnimationWithStepsAst) ? animationAst : new AnimationSequenceAst([animationAst]); return new AnimationStateTransitionAst(transitionExprs, stepsAst); } function _parseAnimationAlias(alias: string, errors: AnimationParseError[]): string { switch (alias) { case ':enter': return 'void => *'; case ':leave': return '* => void'; default: errors.push( new AnimationParseError(`the transition alias value "${alias}" is not supported`)); return '* => *'; } } function _parseAnimationTransitionExpr( eventStr: string, errors: AnimationParseError[]): AnimationStateTransitionExpression[] { var expressions: AnimationStateTransitionExpression[] = []; if (eventStr[0] == ':') { eventStr = _parseAnimationAlias(eventStr, errors); } var match = eventStr.match(/^(\*|[-\w]+)\s*()\s*(\*|[-\w]+)$/); if (!isPresent(match) || match.length < 4) { errors.push(new AnimationParseError(`the provided ${eventStr} is not of a supported format`)); return expressions; } var fromState = match[1]; var separator = match[2]; var toState = match[3]; expressions.push(new AnimationStateTransitionExpression(fromState, toState)); var isFullAnyStateExpr = fromState == ANY_STATE && toState == ANY_STATE; if (separator[0] == '<' && !isFullAnyStateExpr) { expressions.push(new AnimationStateTransitionExpression(toState, fromState)); } return expressions; } function _normalizeAnimationEntry(entry: CompileAnimationMetadata | CompileAnimationMetadata[]): CompileAnimationMetadata { return Array.isArray(entry) ? new CompileAnimationSequenceMetadata(entry) : entry; } function _normalizeStyleMetadata( entry: CompileAnimationStyleMetadata, stateStyles: {[key: string]: AnimationStylesAst}, errors: AnimationParseError[]): {[key: string]: string | number}[] { var normalizedStyles: {[key: string]: string | number}[] = []; entry.styles.forEach(styleEntry => { if (typeof styleEntry === 'string') { ListWrapper.addAll( normalizedStyles, _resolveStylesFromState(styleEntry, stateStyles, errors)); } else { normalizedStyles.push(<{[key: string]: string | number}>styleEntry); } }); return normalizedStyles; } function _normalizeStyleSteps( entry: CompileAnimationMetadata, stateStyles: {[key: string]: AnimationStylesAst}, errors: AnimationParseError[]): CompileAnimationMetadata { var steps = _normalizeStyleStepEntry(entry, stateStyles, errors); return (entry instanceof CompileAnimationGroupMetadata) ? new CompileAnimationGroupMetadata(steps) : new CompileAnimationSequenceMetadata(steps); } function _mergeAnimationStyles( stylesList: any[], newItem: {[key: string]: string | number} | string) { if (typeof newItem === 'object' && newItem !== null && stylesList.length > 0) { var lastIndex = stylesList.length - 1; var lastItem = stylesList[lastIndex]; if (typeof lastItem === 'object' && lastItem !== null) { stylesList[lastIndex] = StringMapWrapper.merge( <{[key: string]: string | number}>lastItem, <{[key: string]: string | number}>newItem); return; } } stylesList.push(newItem); } function _normalizeStyleStepEntry( entry: CompileAnimationMetadata, stateStyles: {[key: string]: AnimationStylesAst}, errors: AnimationParseError[]): CompileAnimationMetadata[] { var steps: CompileAnimationMetadata[]; if (entry instanceof CompileAnimationWithStepsMetadata) { steps = entry.steps; } else { return [entry]; } var newSteps: CompileAnimationMetadata[] = []; var combinedStyles: Styles[]; steps.forEach(step => { if (step instanceof CompileAnimationStyleMetadata) { // this occurs when a style step is followed by a previous style step // or when the first style step is run. We want to concatenate all subsequent // style steps together into a single style step such that we have the correct // starting keyframe data to pass into the animation player. if (!isPresent(combinedStyles)) { combinedStyles = []; } _normalizeStyleMetadata(step, stateStyles, errors) .forEach(entry => { _mergeAnimationStyles(combinedStyles, entry); }); } else { // it is important that we create a metadata entry of the combined styles // before we go on an process the animate, sequence or group metadata steps. // This will ensure that the AST will have the previous styles painted on // screen before any further animations that use the styles take place. if (isPresent(combinedStyles)) { newSteps.push(new CompileAnimationStyleMetadata(0, combinedStyles)); combinedStyles = null; } if (step instanceof CompileAnimationAnimateMetadata) { // we do not recurse into CompileAnimationAnimateMetadata since // those style steps are not going to be squashed var animateStyleValue = (step).styles; if (animateStyleValue instanceof CompileAnimationStyleMetadata) { animateStyleValue.styles = _normalizeStyleMetadata(animateStyleValue, stateStyles, errors); } else if (animateStyleValue instanceof CompileAnimationKeyframesSequenceMetadata) { animateStyleValue.steps.forEach( step => { step.styles = _normalizeStyleMetadata(step, stateStyles, errors); }); } } else if (step instanceof CompileAnimationWithStepsMetadata) { let innerSteps = _normalizeStyleStepEntry(step, stateStyles, errors); step = step instanceof CompileAnimationGroupMetadata ? new CompileAnimationGroupMetadata(innerSteps) : new CompileAnimationSequenceMetadata(innerSteps); } newSteps.push(step); } }); // this happens when only styles were animated within the sequence if (isPresent(combinedStyles)) { newSteps.push(new CompileAnimationStyleMetadata(0, combinedStyles)); } return newSteps; } function _resolveStylesFromState( stateName: string, stateStyles: {[key: string]: AnimationStylesAst}, errors: AnimationParseError[]) { var styles: Styles[] = []; if (stateName[0] != ':') { errors.push(new AnimationParseError(`Animation states via styles must be prefixed with a ":"`)); } else { var normalizedStateName = stateName.substring(1); var value = stateStyles[normalizedStateName]; if (!isPresent(value)) { errors.push(new AnimationParseError( `Unable to apply styles due to missing a state: "${normalizedStateName}"`)); } else { value.styles.forEach(stylesEntry => { if (typeof stylesEntry === 'object' && stylesEntry !== null) { styles.push(stylesEntry as Styles); } }); } } return styles; } class _AnimationTimings { constructor(public duration: number, public delay: number, public easing: string) {} } function _parseAnimationKeyframes( keyframeSequence: CompileAnimationKeyframesSequenceMetadata, currentTime: number, collectedStyles: StylesCollection, stateStyles: {[key: string]: AnimationStylesAst}, errors: AnimationParseError[]): AnimationKeyframeAst[] { var totalEntries = keyframeSequence.steps.length; var totalOffsets = 0; keyframeSequence.steps.forEach(step => totalOffsets += (isPresent(step.offset) ? 1 : 0)); if (totalOffsets > 0 && totalOffsets < totalEntries) { errors.push(new AnimationParseError( `Not all style() entries contain an offset for the provided keyframe()`)); totalOffsets = totalEntries; } var limit = totalEntries - 1; var margin = totalOffsets == 0 ? (1 / limit) : 0; var rawKeyframes: any[] /** TODO #9100 */ = []; var index = 0; var doSortKeyframes = false; var lastOffset = 0; keyframeSequence.steps.forEach(styleMetadata => { var offset = styleMetadata.offset; var keyframeStyles: Styles = {}; styleMetadata.styles.forEach(entry => { Object.keys(entry).forEach(prop => { if (prop != 'offset') { keyframeStyles[prop] = (entry as Styles)[prop]; } }); }); if (isPresent(offset)) { doSortKeyframes = doSortKeyframes || (offset < lastOffset); } else { offset = index == limit ? _TERMINAL_KEYFRAME : (margin * index); } rawKeyframes.push([offset, keyframeStyles]); lastOffset = offset; index++; }); if (doSortKeyframes) { ListWrapper.sort(rawKeyframes, (a, b) => a[0] <= b[0] ? -1 : 1); } var firstKeyframe = rawKeyframes[0]; if (firstKeyframe[0] != _INITIAL_KEYFRAME) { ListWrapper.insert(rawKeyframes, 0, firstKeyframe = [_INITIAL_KEYFRAME, {}]); } var firstKeyframeStyles = firstKeyframe[1]; limit = rawKeyframes.length - 1; var lastKeyframe = rawKeyframes[limit]; if (lastKeyframe[0] != _TERMINAL_KEYFRAME) { rawKeyframes.push(lastKeyframe = [_TERMINAL_KEYFRAME, {}]); limit++; } var lastKeyframeStyles = lastKeyframe[1]; for (let i = 1; i <= limit; i++) { let entry = rawKeyframes[i]; let styles = entry[1]; Object.keys(styles).forEach(prop => { if (!isPresent(firstKeyframeStyles[prop])) { firstKeyframeStyles[prop] = FILL_STYLE_FLAG; } }); } for (let i = limit - 1; i >= 0; i--) { let entry = rawKeyframes[i]; let styles = entry[1]; Object.keys(styles).forEach(prop => { if (!isPresent(lastKeyframeStyles[prop])) { lastKeyframeStyles[prop] = styles[prop]; } }); } return rawKeyframes.map( entry => new AnimationKeyframeAst(entry[0], new AnimationStylesAst([entry[1]]))); } function _parseTransitionAnimation( entry: CompileAnimationMetadata, currentTime: number, collectedStyles: StylesCollection, stateStyles: {[key: string]: AnimationStylesAst}, errors: AnimationParseError[]): AnimationAst { var ast: any /** TODO #9100 */; var playTime = 0; var startingTime = currentTime; if (entry instanceof CompileAnimationWithStepsMetadata) { var maxDuration = 0; var steps: any[] /** TODO #9100 */ = []; var isGroup = entry instanceof CompileAnimationGroupMetadata; var previousStyles: any /** TODO #9100 */; entry.steps.forEach(entry => { // these will get picked up by the next step... var time = isGroup ? startingTime : currentTime; if (entry instanceof CompileAnimationStyleMetadata) { entry.styles.forEach(stylesEntry => { // by this point we know that we only have stringmap values var map = stylesEntry as Styles; Object.keys(map).forEach( prop => { collectedStyles.insertAtTime(prop, time, map[prop]); }); }); previousStyles = entry.styles; return; } var innerAst = _parseTransitionAnimation(entry, time, collectedStyles, stateStyles, errors); if (isPresent(previousStyles)) { if (entry instanceof CompileAnimationWithStepsMetadata) { let startingStyles = new AnimationStylesAst(previousStyles); steps.push(new AnimationStepAst(startingStyles, [], 0, 0, '')); } else { var innerStep = innerAst; ListWrapper.addAll(innerStep.startingStyles.styles, previousStyles); } previousStyles = null; } var astDuration = innerAst.playTime; currentTime += astDuration; playTime += astDuration; maxDuration = Math.max(astDuration, maxDuration); steps.push(innerAst); }); if (isPresent(previousStyles)) { let startingStyles = new AnimationStylesAst(previousStyles); steps.push(new AnimationStepAst(startingStyles, [], 0, 0, '')); } if (isGroup) { ast = new AnimationGroupAst(steps); playTime = maxDuration; currentTime = startingTime + playTime; } else { ast = new AnimationSequenceAst(steps); } } else if (entry instanceof CompileAnimationAnimateMetadata) { var timings = _parseTimeExpression(entry.timings, errors); var styles = entry.styles; var keyframes: any /** TODO #9100 */; if (styles instanceof CompileAnimationKeyframesSequenceMetadata) { keyframes = _parseAnimationKeyframes(styles, currentTime, collectedStyles, stateStyles, errors); } else { let styleData = styles; let offset = _TERMINAL_KEYFRAME; let styleAst = new AnimationStylesAst(styleData.styles as Styles[]); var keyframe = new AnimationKeyframeAst(offset, styleAst); keyframes = [keyframe]; } ast = new AnimationStepAst( new AnimationStylesAst([]), keyframes, timings.duration, timings.delay, timings.easing); playTime = timings.duration + timings.delay; currentTime += playTime; keyframes.forEach( (keyframe: any /** TODO #9100 */) => keyframe.styles.styles.forEach( (entry: any /** TODO #9100 */) => Object.keys(entry).forEach( prop => { collectedStyles.insertAtTime(prop, currentTime, entry[prop]); }))); } else { // if the code reaches this stage then an error // has already been populated within the _normalizeStyleSteps() // operation... ast = new AnimationStepAst(null, [], 0, 0, ''); } ast.playTime = playTime; ast.startTime = startingTime; return ast; } function _fillAnimationAstStartingKeyframes( ast: AnimationAst, collectedStyles: StylesCollection, errors: AnimationParseError[]): void { // steps that only contain style will not be filled if ((ast instanceof AnimationStepAst) && ast.keyframes.length > 0) { var keyframes = ast.keyframes; if (keyframes.length == 1) { var endKeyframe = keyframes[0]; var startKeyframe = _createStartKeyframeFromEndKeyframe( endKeyframe, ast.startTime, ast.playTime, collectedStyles, errors); ast.keyframes = [startKeyframe, endKeyframe]; } } else if (ast instanceof AnimationWithStepsAst) { ast.steps.forEach(entry => _fillAnimationAstStartingKeyframes(entry, collectedStyles, errors)); } } function _parseTimeExpression( exp: string | number, errors: AnimationParseError[]): _AnimationTimings { var regex = /^([\.\d]+)(m?s)(?:\s+([\.\d]+)(m?s))?(?:\s+([-a-z]+(?:\(.+?\))?))?/i; var duration: number; var delay: number = 0; var easing: string = null; if (typeof exp === 'string') { const matches = exp.match(regex); if (matches === null) { errors.push(new AnimationParseError(`The provided timing value "${exp}" is invalid.`)); return new _AnimationTimings(0, 0, null); } var durationMatch = parseFloat(matches[1]); var durationUnit = matches[2]; if (durationUnit == 's') { durationMatch *= _ONE_SECOND; } duration = Math.floor(durationMatch); var delayMatch = matches[3]; var delayUnit = matches[4]; if (isPresent(delayMatch)) { var delayVal: number = parseFloat(delayMatch); if (isPresent(delayUnit) && delayUnit == 's') { delayVal *= _ONE_SECOND; } delay = Math.floor(delayVal); } var easingVal = matches[5]; if (!isBlank(easingVal)) { easing = easingVal; } } else { duration = exp; } return new _AnimationTimings(duration, delay, easing); } function _createStartKeyframeFromEndKeyframe( endKeyframe: AnimationKeyframeAst, startTime: number, duration: number, collectedStyles: StylesCollection, errors: AnimationParseError[]): AnimationKeyframeAst { var values: Styles = {}; var endTime = startTime + duration; endKeyframe.styles.styles.forEach((styleData: Styles) => { Object.keys(styleData).forEach(prop => { const val = styleData[prop]; if (prop == 'offset') return; var resultIndex = collectedStyles.indexOfAtOrBeforeTime(prop, startTime); var resultEntry: any /** TODO #9100 */, nextEntry: any /** TODO #9100 */, value: any /** TODO #9100 */; if (isPresent(resultIndex)) { resultEntry = collectedStyles.getByIndex(prop, resultIndex); value = resultEntry.value; nextEntry = collectedStyles.getByIndex(prop, resultIndex + 1); } else { // this is a flag that the runtime code uses to pass // in a value either from the state declaration styles // or using the AUTO_STYLE value (e.g. getComputedStyle) value = FILL_STYLE_FLAG; } if (isPresent(nextEntry) && !nextEntry.matches(endTime, val)) { errors.push(new AnimationParseError( `The animated CSS property "${prop}" unexpectedly changes between steps "${resultEntry.time}ms" and "${endTime}ms" at "${nextEntry.time}ms"`)); } values[prop] = value; }); }); return new AnimationKeyframeAst(_INITIAL_KEYFRAME, new AnimationStylesAst([values])); }