Skip to content

Commit

Permalink
Merge pull request #11539 from rtibbles/zippidy-doodah
Browse files Browse the repository at this point in the history
Create and use a standard utility library for handling zip files in the frontend
  • Loading branch information
rtibbles authored Jan 2, 2024
2 parents c372cd0 + 122d1e9 commit a855679
Show file tree
Hide file tree
Showing 13 changed files with 622 additions and 309 deletions.
212 changes: 81 additions & 131 deletions kolibri/plugins/perseus_viewer/assets/src/views/PerseusRendererIndex.vue
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,8 @@
<script>
import invert from 'lodash/invert';
import JSZip from 'jszip';
import client from 'kolibri.client';
import ZipFile from 'kolibri-zip';
import { Mapper, defaultFilePathMappers } from 'kolibri-zip/src/fileUtils';
import urls from 'kolibri.urls';
import useKResponsiveWindow from 'kolibri-design-system/lib/useKResponsiveWindow';
import { isTouchDevice } from 'kolibri.utils.browserInfo';
Expand Down Expand Up @@ -124,6 +124,67 @@
const blobImageRegex = /blob:[^)^"]+/g;
Khan.imageUrls = {};
function getImagePaths(itemResponse) {
const graphieMatches = {};
const imageMatches = {};
const matches = Array.from(itemResponse.matchAll(allImageRegex));
for (let i = 0; i < matches.length; i++) {
const match = matches[i];
if (match[1]) {
// We have a match for the optional web+graphie matching group
graphieMatches[match[3]] = true;
} else {
imageMatches[match[3]] = true;
}
}
const graphieImages = Object.keys(graphieMatches);
const images = Object.keys(imageMatches);
const svgAndJson = graphieImages.reduce(
(acc, image) => [...acc, `${image}.svg`, `${image}-data.json`],
[]
);
return images.concat(svgAndJson);
}
function replaceImageUrls(itemResponse, packageFiles) {
Object.assign(Khan.imageUrls, packageFiles);
// If the file is not present in the zip file, then fill in a missing image
// file for images, and an empty dummy json file for json
return itemResponse.replace(allImageRegex, (match, g1, g2, image) => {
if (g1) {
// Replace any placeholder values for image URLs with the
// `web+graphie:` prefix separately from any others,
// as they are parsed slightly differently to standard image
// urls (Perseus adds the protocol in place of `web+graphie:`).
if (!Khan.imageUrls[image]) {
Khan.imageUrls[image] = 'data:application/json,';
}
return `web+graphie:${image}`;
} else {
// Replace any placeholder values for image URLs with
// the base URL for the perseus file we are reading from
return packageFiles[image] || imageMissing;
}
});
}
class JSONMapper extends Mapper {
getPaths() {
return getImagePaths(this.file.toString());
}
replacePaths(packageFiles) {
return replaceImageUrls(this.file.toString(), packageFiles);
}
}
const filePathMappers = {
...defaultFilePathMappers,
json: JSONMapper,
};
export default {
name: 'PerseusRendererIndex',
setup() {
Expand Down Expand Up @@ -209,15 +270,14 @@
},
beforeDestroy() {
this.clearItemRenderer();
this.$emit('stopTracking');
this.clearItemRenderer();
if (this.perseusFile) {
this.perseusFile.close();
}
},
created() {
this.perseusFile = null;
this.imageUrls = {};
// Make a global reference for this object
// for access inside perseus.
Khan.imageUrls = this.imageUrls;
const initPromise = mathJaxPromise.then(() =>
perseus.init({ skipMathJax: true, loadExtraWidgets: true })
);
Expand Down Expand Up @@ -313,12 +373,7 @@
} catch (e) {
logging.debug('Error during unmounting of item renderer', e);
}
for (const key in this.imageUrls) {
if (this.imageUrls[key].indexOf('blob:') === 0) {
URL.revokeObjectURL(this.imageUrls[key]);
}
delete this.imageUrls[key];
}
Khan.imageUrls = {};
},
/*
* Special method to extract the current state of a Perseus Sorter widget
Expand Down Expand Up @@ -355,7 +410,7 @@
return this.restoreImageUrls({ hints, question });
},
restoreSerializedState(answerState) {
answerState = this.replaceImageUrls(JSON.stringify(answerState));
answerState = JSON.parse(replaceImageUrls(JSON.stringify(answerState)));
this.itemRenderer.restoreSerializedState(answerState);
this.itemRenderer.getWidgetIds().forEach(id => {
if (sorterWidgetRegex.test(id)) {
Expand Down Expand Up @@ -432,109 +487,21 @@
// dismiss the error message when user click anywhere inside the perseus element.
this.message = null;
},
loadPerseusFile() {
if (this.defaultFile && this.defaultFile.storage_url) {
this.loading = true;
if (!this.perseusFile || this.perseusFileUrl !== this.defaultFile.storage_url) {
return client({
method: 'get',
url: this.defaultFile.storage_url,
responseType: 'arraybuffer',
})
.then(response => {
return JSZip.loadAsync(response.data);
})
.then(perseusFile => {
this.perseusFile = perseusFile;
this.perseusFileUrl = this.defaultFile.storage_url;
})
.catch(err => {
logging.error('Error loading Perseus file', err);
this.reportLoadingError(err);
return Promise.reject(err);
});
} else {
return Promise.resolve();
}
}
},
loadItemData() {
// Only try to do this if itemId is defined.
if (this.itemId && this.defaultFile && this.defaultFile.storage_url) {
this.loading = true;
this.loadPerseusFile()
.then(() => {
const itemDataFile = this.perseusFile.file(`${this.itemId}.json`);
if (itemDataFile) {
return itemDataFile.async('string');
}
return Promise.reject(`item data for ${this.itemId} not found`);
})
.then(itemResponse => {
const graphieMatches = {};
const imageMatches = {};
const matches = Array.from(itemResponse.matchAll(allImageRegex));
for (let i = 0; i < matches.length; i++) {
const match = matches[i];
if (match[1]) {
// We have a match for the optional web+graphie matching group
graphieMatches[match[3]] = true;
} else {
imageMatches[match[3]] = true;
}
}
const graphieImages = Object.keys(graphieMatches);
const images = Object.keys(imageMatches);
const processFile = file => {
if (!this.imageUrls[file]) {
const fileData = this.perseusFile.file(file);
const ext = file.split('.').slice(-1)[0];
if (fileData) {
return fileData.async('arraybuffer').then(buffer => {
let type;
if (ext === 'json') {
type = 'application/json';
} else if (ext === 'svg') {
type = 'image/svg+xml';
} else {
type = `image/${ext}`;
}
const blob = new Blob([buffer], { type });
this.imageUrls[file] = URL.createObjectURL(blob);
});
} else {
// If the file is not present in the zip file, then fill in a missing image
// file for images, and an empty dummy json file for json
let url;
if (ext === 'json') {
url = 'data:application/json,';
} else {
url = imageMissing;
}
this.imageUrls[file] = url;
}
}
return Promise.resolve();
};
const promises = images.map(processFile).concat(
graphieImages.map(image => {
const svgFile = `${image}.svg`;
const jsonFile = `${image}-data.json`;
return Promise.all([processFile(svgFile), processFile(jsonFile)]);
})
);
return Promise.all(promises)
.catch(() => {
return Promise.reject('error loading assessment item images');
})
.then(() => {
this.setItemData(this.replaceImageUrls(itemResponse));
});
if (!this.perseusFile || this.perseusFileUrl !== this.defaultFile.storage_url) {
this.perseusFile = new ZipFile(this.defaultFile.storage_url, {
filePathMappers,
});
this.perseusFileUrl = this.defaultFile.storage_url;
}
this.perseusFile
.file(`${this.itemId}.json`)
.then(itemFile => {
const itemResponse = itemFile.toString();
this.setItemData(JSON.parse(itemResponse));
})
.catch(reason => {
logging.debug('There was an error loading the assessment item data: ', reason);
Expand All @@ -543,25 +510,8 @@
});
}
},
replaceImageUrls(itemResponse) {
return JSON.parse(
itemResponse.replace(allImageRegex, (match, g1, g2, image) => {
if (g1) {
// Replace any placeholder values for image URLs with the
// `web+graphie:` prefix separately from any others,
// as they are parsed slightly differently to standard image
// urls (Perseus adds the protocol in place of `web+graphie:`).
return `web+graphie:${image}`;
} else {
// Replace any placeholder values for image URLs with
// the base URL for the perseus file we are reading from
return this.imageUrls[image] || imageMissing;
}
})
);
},
restoreImageUrls(itemResponse) {
const lookup = invert(this.imageUrls);
const lookup = invert(Khan.imageUrls);
return JSON.parse(
JSON.stringify(itemResponse).replace(blobImageRegex, match => {
// Make sure to add our prefix back in
Expand Down
1 change: 0 additions & 1 deletion kolibri/plugins/perseus_viewer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
"fractional": "^1.0.0",
"immutable": "^3.8.1",
"jquery": "2.2.4",
"jszip": "^3.10.1",
"kmath": "^0.0.1",
"qtip2": "2.2.0",
"raphael": "^2.2.7",
Expand Down
3 changes: 0 additions & 3 deletions packages/hashi/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
"build": "yarn run build-base --mode=production",
"dev": "yarn run build-base --mode=development --watch",
"compat": "eslint -c ./compat.js ./src/*.js",
"mimetypes": "node ./generateH5PMimeTypeDB.js",
"build-h5p": "node ./downloadH5PVendor.js && webpack --config ./webpack.config.h5p.js --mode=production"
},
"author": "Learning Equality",
Expand All @@ -17,14 +16,12 @@
"eslint-plugin-compat": "^4.2.0",
"html-webpack-plugin": "5.5.4",
"jquery": "3.5.1",
"mime-db": "^1.52.0",
"mutationobserver-shim": "^0.3.7",
"purgecss": "^5.0.0"
},
"dependencies": {
"core-js": "3.34",
"dayjs": "^1.11.10",
"fflate": "^0.8.1",
"iri": "^1.3.1",
"is-language-code": "^3.1.0",
"iso8601-duration": "^2.1.2",
Expand Down
Loading

0 comments on commit a855679

Please sign in to comment.