blob: 3da9f9e62de9f1e8e36efabcea4959f72f6f8ef2 [file]
/**
* @module skottie-compatibility-sk
* @description <h2><code>skottie-compatibility-sk</code></h2>
*
* <p>
* A skottie compatibility report. Reports the input lottie with various
* JSON schemas.
* </p>
*
* * @evt updateAnimation - This event is generated when the user updates
* a slot value.
* The updated json is available in the event detail.
*/
import Ajv from 'ajv/dist/2020';
import { html, TemplateResult } from 'lit/html.js';
import { define } from '../../../elements-sk/modules/define';
import { ElementSk } from '../../../infra-sk/modules/ElementSk';
import { LottieAnimation } from '../types';
import '../skottie-button-sk';
import { ProfileValidator, LottieError } from './profile-validator';
import { lottieSchema } from './schemas/lottie.schema';
import { lottiePerformanceWarningSchema } from './schemas/lottie-performance-warning.schema';
import { lowPowerLottieProfileSchema } from './schemas/low-power-lottie-profile.schema';
import {
LottieValidator,
LottieValidatorError,
} from '@lottie-animation-community/lottie-specs/src/validator';
import { sanitizeLottie, COMMON_EXPORTER_FIELDS, SANITIZE_KEYS_IF_SLOT } from './sanitize';
type SchemaEntry = {
name: string;
getContent: (ele: SkottieCompatibilitySk) => TemplateResult;
getErrorCount?: (ele: SkottieCompatibilitySk) => number;
profileValidator?: ProfileValidator;
};
export class SkottieCompatibilitySk extends ElementSk {
private _animation: LottieAnimation | null = null;
private schemas: SchemaEntry[] = [];
tabIndex = 0;
ignoreExporterFields = true;
private specValidator: LottieValidator;
private specErrors: LottieValidatorError[] = [];
constructor() {
super(SkottieCompatibilitySk.template);
// TODO(bwils) LottieValidator mutates the schema and then fails
// to process the mutated schema if this is called again.
const clonedSchema = JSON.parse(JSON.stringify(lottieSchema));
this.specValidator = new LottieValidator(Ajv, clonedSchema, { name_paths: true });
const lowPowerProfile = new ProfileValidator(lowPowerLottieProfileSchema);
const lowPowerWarningProfile = new ProfileValidator(lottiePerformanceWarningSchema);
this.schemas = [
{
name: 'Specification 1.0 Errors',
getContent: (ele) => SkottieCompatibilitySk.specReport(ele),
getErrorCount: (ele) => SkottieCompatibilitySk.specErrorCount(ele),
},
{
name: 'Specification 1.0 Warnings',
getContent: (ele) => SkottieCompatibilitySk.strictSpecReport(ele),
getErrorCount: (ele) => SkottieCompatibilitySk.strictSpecErrorCount(ele),
},
{
name: 'Low Power Profile Errors (WIP)',
getContent: (ele) =>
SkottieCompatibilitySk.featureErrorReport(lowPowerProfile, ele._animation),
getErrorCount: (ele) => lowPowerProfile.getErrors(ele._animation, true).length,
profileValidator: lowPowerProfile,
},
{
name: 'Low Power Profile Performance Warnings (WIP)',
getContent: (ele) =>
SkottieCompatibilitySk.featureErrorReport(lowPowerWarningProfile, ele._animation),
getErrorCount: (ele) => lowPowerWarningProfile.getErrors(ele._animation, true).length,
profileValidator: lowPowerWarningProfile,
},
];
}
connectedCallback() {
super.connectedCallback();
this._render();
}
disconnectedCallback() {
super.disconnectedCallback();
}
setTabIndex(tabIndex: number) {
this.tabIndex = tabIndex;
this._render();
}
private static tabContainer = (ele: SkottieCompatibilitySk) => html`
<div class="tab-container">
${ele.schemas.map((schema, index) => {
let resultSummary = 'Unknown';
if (schema.getErrorCount) {
const errorCount = schema.getErrorCount(ele);
resultSummary = errorCount === 0 ? 'Pass' : `${errorCount}`;
}
return html`
<skottie-button-sk
id="Report-${index}"
@select=${() => ele.setTabIndex(index)}
type="outline"
.content=${`${schema.name} (${resultSummary})`}
.classes=${ele.tabIndex === index ? ['active-tab'] : []}>
</skottie-button-sk>
`;
})}
</div>
`;
private static template = (ele: SkottieCompatibilitySk) => html`
<div>${this.tabContainer(ele)}</div>
<div class="tab-content">${this.report(ele)}</div>
`;
private static report = (ele: SkottieCompatibilitySk) => {
const tabIndex = ele.tabIndex;
const schema = ele.schemas[tabIndex];
return schema.getContent(ele);
};
private static specErrorCount(ele: SkottieCompatibilitySk) {
const filteredErrors = ele.specErrors.filter((error) => error.type === 'error');
return filteredErrors.length;
}
private static filterStrictErrors(errors: LottieValidatorError[], ignoreExporterFields = false) {
let filteredErrors = errors.filter((error) => error.type === 'warning');
if (ignoreExporterFields) {
filteredErrors = filteredErrors.filter((error) => {
if (error.warning === 'type') {
return true;
}
const pathParts = error.path.split('/');
const propName = pathParts[pathParts.length - 1];
if (pathParts[1] === 'slots' && SANITIZE_KEYS_IF_SLOT.includes(propName)) {
return false;
}
return !COMMON_EXPORTER_FIELDS.includes(propName);
});
}
return filteredErrors;
}
private static strictSpecErrorCount(ele: SkottieCompatibilitySk) {
return SkottieCompatibilitySk.filterStrictErrors(ele.specErrors, ele.ignoreExporterFields)
.length;
}
private static specErrorTable(errors: LottieValidatorError[]) {
return html`
<table>
<tr>
<th>Element Name</th>
<th>JSON Path</th>
<th>Error</th>
</tr>
${errors.map(
(error) =>
html`<tr>
<td>${`${error.path_names?.join(' > ')}`}</td>
<td>${error.path}</td>
<td>${error.message}</td>
</tr>`
)}
</table>
`;
}
private static specReport(ele: SkottieCompatibilitySk) {
const filteredErrors = ele.specErrors.filter((error) => error.type === 'error');
if (filteredErrors.length === 0) {
return html`<div>Pass</div>`;
}
return SkottieCompatibilitySk.specErrorTable(filteredErrors);
}
private static strictSpecReport(ele: SkottieCompatibilitySk) {
const filteredErrors = SkottieCompatibilitySk.filterStrictErrors(
ele.specErrors,
ele.ignoreExporterFields
);
return html`
<div class="report-content">
Specification warnings are for properties and types that have not been documented in the
official Lottie specification. Exporter tools and playback libraries may still support these
without any issues.
</div>
<div class="report-content">
<input
type="checkbox"
id="ignore-exporter-fields"
.checked="${ele.ignoreExporterFields}"
@click="${() => ele.onignoreExporterFields(ele)}" />
<label for="ignore-exporter-fields">
Ignore Common Exporter Properties. The current values in these properties will not affect
playback.</label
>
</div>
<div class="report-content">
<skottie-button-sk
id="sanitize"
@select=${() => ele.onSanitize(ele)}
type="filled"
.content=${'Sanitize Lottie'}>
</skottie-button-sk>
<span>
(Sanitization removes common exporter fields that are not yet specificied and shouldn't
affect playback.)
</span>
</div>
${SkottieCompatibilitySk.specErrorTable(filteredErrors)}
`;
}
private static featureErrorReport = (profileValidator: ProfileValidator, lottie: any) => {
const errors = profileValidator.getErrors(lottie, true);
if (errors.length === 0) {
return html`<div>Pass</div>`;
}
const featureToErrorList: Map<string, LottieError[]> = errors.reduce((map, error) => {
const { featureCode } = error;
if (!featureCode) {
return map;
}
if (!map.get(featureCode)) {
map.set(featureCode, []);
}
map.get(featureCode)?.push(error);
return map;
}, new Map<string, LottieError[]>());
return html`<table>
<tr>
<th>Feature ID</th>
<th>Element Name</th>
<th>JSON Path</th>
</tr>
${[...featureToErrorList].map(([, errorList]) =>
errorList.map(
(error, index) => html`
<tr>
${index === 0
? html`<td class="feature-id-cell" rowspan=${errorList.length}>
<a href="https://canilottie.com/${error.featureLink ?? error.featureCode}"
>${error.featureCode}</a
>
${error.featureDetails ? error.featureDetails : 'not supported'}
</td>`
: null}
<td>${error.nameHierarchy?.join(' > ')}</td>
<td>${error.instancePath}</td>
</tr>
`
)
)}
</table>`;
};
private onignoreExporterFields(ele: SkottieCompatibilitySk): void {
ele.ignoreExporterFields = !ele.ignoreExporterFields;
this._render();
}
private onSanitize(ele: SkottieCompatibilitySk): void {
// Shallow clone to trigger re-render
const lottie = { ...ele._animation } as LottieAnimation;
sanitizeLottie(lottie);
if (!lottie) {
return;
}
this.dispatchEvent(
new CustomEvent<LottieAnimation>('updateAnimation', {
detail: lottie,
})
);
this._render();
}
set animation(val: LottieAnimation) {
if (this._animation !== val) {
this._animation = val;
this.specErrors = this.specValidator.validate(this._animation);
// Errors are persisted in each validator object
this.schemas.forEach((schema) => {
schema.profileValidator?.validate(this._animation);
});
this._render();
}
}
}
define('skottie-compatibility-sk', SkottieCompatibilitySk);