Something went wrong. Try again.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226import { LoaderUtils } from 'three';import { validateBytes } from 'gltf-validator';import { ValidatorToggle } from './components/validator-toggle';import { ValidatorReport } from './components/validator-report';
const SEVERITY_MAP = ['Errors', 'Warnings', 'Infos', 'Hints'];
export class Validator { /** * @param {Element} el */ constructor(el) { this.el = el; this.report = null;
this.toggleEl = document.createElement('div'); this.toggleEl.classList.add('report-toggle-wrap', 'hidden'); this.el.appendChild(this.toggleEl); }
/** * Runs validation against the given file URL and extra resources. * @param {string} rootFile * @param {string} rootPath * @param {Map<string, File>} assetMap * @param {Object} response * @return {Promise} */ validate(rootFile, rootPath, assetMap, response) { // TODO: This duplicates a request of the three.js loader, and could // take advantage of THREE.Cache after r90. return fetch(rootFile) .then((response) => response.arrayBuffer()) .then((buffer) => validateBytes(new Uint8Array(buffer), { externalResourceFunction: (uri) => this.resolveExternalResource(uri, rootFile, rootPath, assetMap), }), ) .then((report) => this.setReport(report, response)) .catch((e) => this.setReportException(e)); }
/** * Loads a resource (either locally or from the network) and returns it. * @param {string} uri * @param {string} rootFile * @param {string} rootPath * @param {Map<string, File>} assetMap * @return {Promise<Uint8Array>} */ resolveExternalResource(uri, rootFile, rootPath, assetMap) { const baseURL = LoaderUtils.extractUrlBase(rootFile); const normalizedURL = rootPath + decodeURI(uri) // validator applies URI encoding. .replace(baseURL, '') .replace(/^(\.?\/)/, '');
let objectURL;
if (assetMap.has(normalizedURL)) { const object = assetMap.get(normalizedURL); objectURL = URL.createObjectURL(object); }
return fetch(objectURL || baseURL + uri) .then((response) => response.arrayBuffer()) .then((buffer) => { if (objectURL) URL.revokeObjectURL(objectURL); return new Uint8Array(buffer); }); }
/** * @param {GLTFValidator.Report} report * @param {Object} response */ setReport(report, response) { report.generator = (report && report.info && report.info.generator) || ''; report.issues.maxSeverity = -1; SEVERITY_MAP.forEach((severity, index) => { if (report.issues[`num${severity}`] > 0 && report.issues.maxSeverity === -1) { report.issues.maxSeverity = index; } }); report.errors = report.issues.messages.filter((msg) => msg.severity === 0); report.warnings = report.issues.messages.filter((msg) => msg.severity === 1); report.infos = report.issues.messages.filter((msg) => msg.severity === 2); report.hints = report.issues.messages.filter((msg) => msg.severity === 3); groupMessages(report); this.report = report;
this.setResponse(response);
this.toggleEl.innerHTML = ValidatorToggle(report); this.showToggle(); this.bindListeners();
function groupMessages(report) { const CODES = { ACCESSOR_NON_UNIT: { message: '{count} accessor elements not of unit length: 0. [AGGREGATED]', pointerCounts: {}, }, ACCESSOR_ANIMATION_INPUT_NON_INCREASING: { message: '{count} animation input accessor elements not in ascending order. [AGGREGATED]', pointerCounts: {}, }, };
report.errors.forEach((message) => { if (!CODES[message.code]) return; if (!CODES[message.code].pointerCounts[message.pointer]) { CODES[message.code].pointerCounts[message.pointer] = 0; } CODES[message.code].pointerCounts[message.pointer]++; }); report.errors = report.errors.filter((message) => { if (!CODES[message.code]) return true; if (!CODES[message.code].pointerCounts[message.pointer]) return true; return CODES[message.code].pointerCounts[message.pointer] < 2; }); Object.keys(CODES).forEach((code) => { Object.keys(CODES[code].pointerCounts).forEach((pointer) => { report.errors.push({ code: code, pointer: pointer, message: CODES[code].message.replace( '{count}', CODES[code].pointerCounts[pointer], ), }); }); }); } }
/** * @param {Object} response */ setResponse(response) { const json = response && response.parser && response.parser.json;
if (!json) return;
if (json.asset && json.asset.extras) { const extras = json.asset.extras; this.report.info.extras = {}; if (extras.author) { this.report.info.extras.author = linkify(escapeHTML(extras.author)); } if (extras.license) { this.report.info.extras.license = linkify(escapeHTML(extras.license)); } if (extras.source) { this.report.info.extras.source = linkify(escapeHTML(extras.source)); } if (extras.title) { this.report.info.extras.title = extras.title; } } }
/** * @param {Error} e */ setReportException(e) { this.report = null; this.toggleEl.innerHTML = this.toggleTpl({ reportError: e, level: 0 }); this.showToggle(); this.bindListeners(); }
bindListeners() { const reportToggleBtn = this.toggleEl.querySelector('.report-toggle'); reportToggleBtn.addEventListener('click', () => this.showLightbox());
const reportToggleCloseBtn = this.toggleEl.querySelector('.report-toggle-close'); reportToggleCloseBtn.addEventListener('click', (e) => { this.hideToggle(); e.stopPropagation(); }); }
showToggle() { this.toggleEl.classList.remove('hidden'); }
hideToggle() { this.toggleEl.classList.add('hidden'); }
showLightbox() { if (!this.report) return; const tab = window.open('', '_blank'); tab.document.body.innerHTML = ` <!DOCTYPE html> <title>glTF 2.0 validation report</title> <link rel="stylesheet" href="{{location.protocol}}//{{location.host}}/style.css"> <style> body { overflow-y: auto; } html, body { background: #FFFFFF; } </style> ${ValidatorReport({ ...this.report, location })}`; }}
function escapeHTML(unsafe) { return unsafe .replace(/&/g, '&') .replace(/</g, '<') .replace(/>/g, '>') .replace(/"/g, '"') .replace(/'/g, ''');}
function linkify(text) { const urlPattern = /\b(?:https?):\/\/[a-z0-9-+&@#\/%?=~_|!:,.;]*[a-z0-9-+&@#\/%=~_|]/gim; const emailAddressPattern = /(([a-zA-Z0-9_\-\.]+)@[a-zA-Z_]+?(?:\.[a-zA-Z]{2,6}))+/gim; return text .replace(urlPattern, '<a target="_blank" href="$&">$&</a>') .replace(emailAddressPattern, '<a target="_blank" href="mailto:$1">$1</a>');}