630 lines
15 KiB
JavaScript
630 lines
15 KiB
JavaScript
/**
|
||
* Manage downloads
|
||
*/
|
||
|
||
(function () {
|
||
|
||
var cache = {};
|
||
var form = $('form');
|
||
var minified = true;
|
||
|
||
var dependencies = {};
|
||
|
||
var treeURL = 'https://api.github.com/repos/PrismJS/prism/git/trees/master?recursive=1';
|
||
var treePromise = new Promise(function (resolve) {
|
||
$u.xhr({
|
||
url: treeURL,
|
||
callback: function (xhr) {
|
||
if (xhr.status < 400) {
|
||
resolve(JSON.parse(xhr.responseText).tree);
|
||
}
|
||
}
|
||
});
|
||
});
|
||
|
||
/**
|
||
* Converts the given value into an array.
|
||
*
|
||
* @param {T | T[] | null | undefined} value
|
||
* @returns {T[]}
|
||
* @template T
|
||
*/
|
||
function toArray(value) {
|
||
if (Array.isArray(value)) {
|
||
return value;
|
||
} else {
|
||
return value == null ? [] : [value];
|
||
}
|
||
}
|
||
|
||
var hstr = window.location.hash.match(/(?:languages|plugins)=[-+\w]+|themes=[-\w]+/g);
|
||
if (hstr) {
|
||
hstr.forEach(function (str) {
|
||
var kv = str.split('=', 2);
|
||
var category = kv[0];
|
||
var ids = kv[1].split('+');
|
||
if (category !== 'meta' && category !== 'core' && components[category]) {
|
||
for (var id in components[category]) {
|
||
if (components[category][id].option) {
|
||
delete components[category][id].option;
|
||
}
|
||
}
|
||
if (category === 'themes' && ids.length) {
|
||
var themeInput = $('#theme input[value="' + ids[0] + '"]');
|
||
if (themeInput) {
|
||
themeInput.checked = true;
|
||
}
|
||
// eslint-disable-next-line no-undef
|
||
setTheme(ids[0]);
|
||
}
|
||
var makeDefault = function (id) {
|
||
if (id !== 'meta') {
|
||
if (components[category][id]) {
|
||
if (components[category][id].option !== 'default') {
|
||
if (typeof components[category][id] === 'string') {
|
||
components[category][id] = { title: components[category][id] };
|
||
}
|
||
components[category][id].option = 'default';
|
||
}
|
||
|
||
toArray(components[category][id].require).forEach(makeDefault);
|
||
}
|
||
}
|
||
};
|
||
ids.forEach(makeDefault);
|
||
}
|
||
});
|
||
}
|
||
|
||
// Stay compatible with old querystring feature
|
||
var qstr = window.location.search.match(/(?:languages|plugins)=[-+\w]+|themes=[-\w]+/g);
|
||
if (qstr && !hstr) {
|
||
window.location.hash = window.location.search.replace(/^\?/, '');
|
||
window.location.search = '';
|
||
}
|
||
|
||
var storedTheme = localStorage.getItem('theme');
|
||
|
||
for (var category in components) {
|
||
var all = components[category];
|
||
|
||
all.meta.section = $u.element.create('section', {
|
||
className: 'options',
|
||
id: 'category-' + category,
|
||
contents: {
|
||
tag: 'h1',
|
||
contents: category.charAt(0).toUpperCase() + category.slice(1)
|
||
},
|
||
inside: '#components'
|
||
});
|
||
|
||
if (all.meta.addCheckAll) {
|
||
$u.element.create('label', {
|
||
attributes: {
|
||
'data-id': 'check-all-' + category
|
||
},
|
||
contents: [
|
||
{
|
||
tag: 'input',
|
||
properties: {
|
||
type: 'checkbox',
|
||
name: 'check-all-' + category,
|
||
value: '',
|
||
checked: false,
|
||
onclick: (function (category, all) {
|
||
return function () {
|
||
var checkAll = this;
|
||
$$('input[name="download-' + category + '"]').forEach(function (input) {
|
||
all[input.value].enabled = input.checked = checkAll.checked;
|
||
});
|
||
|
||
update(category);
|
||
};
|
||
}(category, all))
|
||
}
|
||
},
|
||
'Select/unselect all'
|
||
],
|
||
inside: all.meta.section
|
||
});
|
||
}
|
||
|
||
for (var id in all) {
|
||
if (id === 'meta') {
|
||
continue;
|
||
}
|
||
|
||
var checked = false; var disabled = false;
|
||
var option = all[id].option || all.meta.option;
|
||
|
||
switch (option) {
|
||
case 'mandatory': disabled = true; // fallthrough
|
||
case 'default': checked = true;
|
||
}
|
||
if (category === 'themes' && storedTheme) {
|
||
checked = id === storedTheme;
|
||
}
|
||
|
||
var filepath = all.meta.path.replace(/\{id\}/g, id);
|
||
|
||
var info = all[id] = {
|
||
title: all[id].title || all[id],
|
||
aliasTitles: all[id].aliasTitles,
|
||
noCSS: all[id].noCSS || all.meta.noCSS,
|
||
noJS: all[id].noJS || all.meta.noJS,
|
||
enabled: checked,
|
||
require: toArray(all[id].require),
|
||
after: toArray(all[id].after),
|
||
modify: toArray(all[id].modify),
|
||
owner: all[id].owner,
|
||
files: {
|
||
minified: {
|
||
paths: [],
|
||
size: 0
|
||
},
|
||
dev: {
|
||
paths: [],
|
||
size: 0
|
||
}
|
||
}
|
||
};
|
||
|
||
info.require.forEach(function (v) {
|
||
dependencies[v] = (dependencies[v] || []).concat(id);
|
||
});
|
||
|
||
if (!all[id].noJS && !/\.css$/.test(filepath)) {
|
||
info.files.minified.paths.push(filepath.replace(/(\.js)?$/, '.min.js'));
|
||
info.files.dev.paths.push(filepath.replace(/(\.js)?$/, '.js'));
|
||
}
|
||
|
||
|
||
if ((!all[id].noCSS && !/\.js$/.test(filepath)) || /\.css$/.test(filepath)) {
|
||
var cssFile = filepath.replace(/(\.css)?$/, '.css');
|
||
var minCSSFile = cssFile.replace(/(?:\.css)$/, '.min.css');
|
||
|
||
info.files.minified.paths.push(minCSSFile);
|
||
info.files.dev.paths.push(cssFile);
|
||
}
|
||
|
||
function getLanguageTitle(lang) {
|
||
if (!lang.aliasTitles) {
|
||
return lang.title;
|
||
}
|
||
|
||
var titles = [lang.title];
|
||
for (var alias in lang.aliasTitles) {
|
||
if (lang.aliasTitles.hasOwnProperty(alias)) {
|
||
titles.push(lang.aliasTitles[alias]);
|
||
}
|
||
}
|
||
return titles.join(' + ');
|
||
}
|
||
|
||
var label = $u.element.create('label', {
|
||
attributes: {
|
||
'data-id': id
|
||
},
|
||
contents: [
|
||
{
|
||
tag: 'input',
|
||
properties: {
|
||
type: all.meta.exclusive ? 'radio' : 'checkbox',
|
||
name: 'download-' + category,
|
||
value: id,
|
||
checked: checked,
|
||
disabled: disabled,
|
||
onclick: (function (id, category, all) {
|
||
return function () {
|
||
$$('input[name="' + this.name + '"]').forEach(function (input) {
|
||
all[input.value].enabled = input.checked;
|
||
});
|
||
|
||
if (all[id].require && this.checked) {
|
||
all[id].require.forEach(function (v) {
|
||
var input = $('label[data-id="' + v + '"] > input');
|
||
input.checked = true;
|
||
|
||
input.onclick();
|
||
});
|
||
}
|
||
|
||
if (dependencies[id] && !this.checked) { // It’s required by others
|
||
dependencies[id].forEach(function (dependent) {
|
||
var input = $('label[data-id="' + dependent + '"] > input');
|
||
input.checked = false;
|
||
|
||
input.onclick();
|
||
});
|
||
}
|
||
|
||
update(category, id);
|
||
};
|
||
}(id, category, all))
|
||
}
|
||
},
|
||
all.meta.link ? {
|
||
tag: 'a',
|
||
properties: {
|
||
href: all.meta.link.replace(/\{id\}/g, id),
|
||
className: 'name'
|
||
},
|
||
contents: info.title
|
||
} : {
|
||
tag: 'span',
|
||
properties: {
|
||
className: 'name'
|
||
},
|
||
contents: getLanguageTitle(info)
|
||
},
|
||
' ',
|
||
all[id].owner ? {
|
||
tag: 'a',
|
||
properties: {
|
||
href: 'https://github.com/' + all[id].owner,
|
||
className: 'owner',
|
||
target: '_blank'
|
||
},
|
||
contents: all[id].owner
|
||
} : ' ',
|
||
{
|
||
tag: 'strong',
|
||
className: 'filesize'
|
||
}
|
||
],
|
||
inside: all.meta.section
|
||
});
|
||
|
||
// Add click events on main theme selector too.
|
||
(function (label) {
|
||
if (category === 'themes') {
|
||
var themeInput = $('#theme input[value="' + id + '"]');
|
||
var input = $('input', label);
|
||
if (themeInput) {
|
||
var themeInputOnclick = themeInput.onclick;
|
||
themeInput.onclick = function () {
|
||
input.checked = true;
|
||
input.onclick();
|
||
themeInputOnclick && themeInputOnclick.call(themeInput);
|
||
};
|
||
}
|
||
}
|
||
}(label));
|
||
}
|
||
}
|
||
|
||
form.elements.compression[0].onclick =
|
||
form.elements.compression[1].onclick = function () {
|
||
minified = !!+this.value;
|
||
|
||
getFilesSizes();
|
||
};
|
||
|
||
function getFileSize(filepath) {
|
||
return treePromise.then(function (tree) {
|
||
for (var i = 0, l = tree.length; i < l; i++) {
|
||
if (tree[i].path === filepath) {
|
||
return tree[i].size;
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
function getFilesSizes() {
|
||
for (var category in components) {
|
||
var all = components[category];
|
||
|
||
for (var id in all) {
|
||
if (id === 'meta') {
|
||
continue;
|
||
}
|
||
|
||
var distro = all[id].files[minified ? 'minified' : 'dev'];
|
||
var files = distro.paths;
|
||
|
||
files.forEach(function (filepath) {
|
||
var file = cache[filepath] = cache[filepath] || {};
|
||
|
||
if (!file.size) {
|
||
|
||
(function (category, id) {
|
||
getFileSize(filepath).then(function (size) {
|
||
if (size) {
|
||
file.size = size;
|
||
distro.size += file.size;
|
||
|
||
update(category, id);
|
||
}
|
||
});
|
||
}(category, id));
|
||
} else {
|
||
update(category, id);
|
||
}
|
||
});
|
||
}
|
||
}
|
||
}
|
||
|
||
getFilesSizes();
|
||
|
||
function getFileContents(filepath) {
|
||
return new Promise(function (resolve, reject) {
|
||
$u.xhr({
|
||
url: filepath,
|
||
callback: function (xhr) {
|
||
if (xhr.status < 400 && xhr.responseText) {
|
||
resolve(xhr.responseText);
|
||
} else {
|
||
reject();
|
||
}
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
function prettySize(size) {
|
||
return Math.round(100 * size / 1024) / 100 + 'KB';
|
||
}
|
||
|
||
function update(updatedCategory, updatedId) {
|
||
// Update total size
|
||
var total = { js: 0, css: 0 }; var updated = { js: 0, css: 0 };
|
||
|
||
for (var category in components) {
|
||
var all = components[category];
|
||
var allChecked = true;
|
||
|
||
for (var id in all) {
|
||
var info = all[id];
|
||
|
||
if (info.enabled || id == updatedId) {
|
||
var distro = info.files[minified ? 'minified' : 'dev'];
|
||
|
||
distro.paths.forEach(function (path) {
|
||
if (cache[path]) {
|
||
var file = cache[path];
|
||
|
||
var type = path.match(/\.(\w+)$/)[1];
|
||
var size = file.size || 0;
|
||
|
||
if (info.enabled) {
|
||
|
||
if (!file.contentsPromise) {
|
||
file.contentsPromise = getFileContents(path);
|
||
}
|
||
|
||
total[type] += size;
|
||
}
|
||
|
||
if (id == updatedId) {
|
||
updated[type] += size;
|
||
}
|
||
}
|
||
});
|
||
}
|
||
if (id !== 'meta' && !info.enabled) {
|
||
allChecked = false;
|
||
}
|
||
|
||
// Select main theme
|
||
if (category === 'themes' && id === updatedId && info.enabled) {
|
||
var themeInput = $('#theme input[value="' + updatedId + '"]');
|
||
if (themeInput) {
|
||
themeInput.checked = true;
|
||
}
|
||
// eslint-disable-next-line no-undef
|
||
setTheme(updatedId);
|
||
}
|
||
}
|
||
|
||
if (all.meta.addCheckAll) {
|
||
$('input[name="check-all-' + category + '"]').checked = allChecked;
|
||
}
|
||
}
|
||
|
||
total.all = total.js + total.css;
|
||
|
||
if (updatedId) {
|
||
updated.all = updated.js + updated.css;
|
||
|
||
$u.element.prop($('label[data-id="' + updatedId + '"] .filesize'), {
|
||
textContent: prettySize(updated.all),
|
||
title: (updated.js ? Math.round(100 * updated.js / updated.all) + '% JavaScript' : '') +
|
||
(updated.js && updated.css ? ' + ' : '') +
|
||
(updated.css ? Math.round(100 * updated.css / updated.all) + '% CSS' : '')
|
||
});
|
||
}
|
||
|
||
$('#filesize').textContent = prettySize(total.all);
|
||
|
||
$u.element.prop($('#percent-js'), {
|
||
textContent: Math.round(100 * total.js / total.all) + '%',
|
||
title: prettySize(total.js)
|
||
});
|
||
|
||
$u.element.prop($('#percent-css'), {
|
||
textContent: Math.round(100 * total.css / total.all) + '%',
|
||
title: prettySize(total.css)
|
||
});
|
||
|
||
delayedGenerateCode();
|
||
}
|
||
|
||
var timerId = 0;
|
||
// "debounce" multiple rapid requests to generate and highlight code
|
||
function delayedGenerateCode() {
|
||
if (timerId !== 0) {
|
||
clearTimeout(timerId);
|
||
}
|
||
timerId = setTimeout(generateCode, 500);
|
||
}
|
||
|
||
function generateCode() {
|
||
/** @type {CodePromiseInfo[]} */
|
||
var promises = [];
|
||
var redownload = {};
|
||
|
||
for (var category in components) {
|
||
for (var id in components[category]) {
|
||
if (id === 'meta') {
|
||
continue;
|
||
}
|
||
|
||
var info = components[category][id];
|
||
if (info.enabled) {
|
||
if (category !== 'core') {
|
||
redownload[category] = redownload[category] || [];
|
||
redownload[category].push(id);
|
||
}
|
||
info.files[minified ? 'minified' : 'dev'].paths.forEach(function (path) {
|
||
if (cache[path]) {
|
||
var type = path.match(/\.(\w+)$/)[1];
|
||
|
||
promises.push({
|
||
contentsPromise: cache[path].contentsPromise,
|
||
id: id,
|
||
category: category,
|
||
path: path,
|
||
type: type
|
||
});
|
||
}
|
||
});
|
||
}
|
||
}
|
||
}
|
||
|
||
// Hide error message if visible
|
||
var error = $('#download .error');
|
||
error.style.display = '';
|
||
|
||
Promise.all([buildCode(promises), getVersion()]).then(function (arr) {
|
||
var res = arr[0];
|
||
var version = arr[1];
|
||
var code = res.code;
|
||
var errors = res.errors;
|
||
|
||
if (errors.length) {
|
||
error.style.display = 'block';
|
||
error.innerHTML = '';
|
||
$u.element.contents(error, errors);
|
||
}
|
||
|
||
var redownloadUrl = window.location.href.split('#')[0] + '#';
|
||
for (var category in redownload) {
|
||
redownloadUrl += category + '=' + redownload[category].join('+') + '&';
|
||
}
|
||
redownloadUrl = redownloadUrl.replace(/&$/, '');
|
||
window.location.replace(redownloadUrl);
|
||
|
||
var versionComment = '/* PrismJS ' + version + '\n' + redownloadUrl + ' */';
|
||
|
||
for (var type in code) {
|
||
(function (type) {
|
||
var text = versionComment + '\n' + code[type];
|
||
var fileName = 'prism.' + type;
|
||
|
||
var codeElement = $('#download-' + type + ' code');
|
||
var pre = codeElement.parentElement;
|
||
|
||
var newCode = document.createElement('CODE');
|
||
newCode.className = codeElement.className;
|
||
newCode.textContent = text;
|
||
|
||
Prism.highlightElement(newCode, true, function () {
|
||
pre.replaceChild(newCode, codeElement);
|
||
});
|
||
|
||
|
||
$('#download-' + type + ' .download-button').onclick = function () {
|
||
saveAs(new Blob([text], { type: 'application/octet-stream;charset=utf-8' }), fileName);
|
||
};
|
||
}(type));
|
||
}
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Returns a promise of the code of the Prism bundle.
|
||
*
|
||
* @param {CodePromiseInfo[]} promises
|
||
* @returns {Promise<{ code: { js: string, css: string }, errors: HTMLElement[] }>}
|
||
*
|
||
* @typedef CodePromiseInfo
|
||
* @property {Promise} contentsPromise
|
||
* @property {string} id
|
||
* @property {string} category
|
||
* @property {string} path
|
||
* @property {string} type
|
||
*/
|
||
function buildCode(promises) {
|
||
// sort the promises
|
||
|
||
/** @type {CodePromiseInfo[]} */
|
||
var finalPromises = [];
|
||
/** @type {Object<string, CodePromiseInfo[]>} */
|
||
var toSortMap = {};
|
||
|
||
promises.forEach(function (p) {
|
||
if (p.category == 'core' || p.category == 'themes') {
|
||
finalPromises.push(p);
|
||
} else {
|
||
var infos = toSortMap[p.id];
|
||
if (!infos) {
|
||
toSortMap[p.id] = infos = [];
|
||
}
|
||
infos.push(p);
|
||
}
|
||
});
|
||
|
||
// this assumes that the ids in `toSortMap` are complete under transitive requirements
|
||
getLoader(components, Object.keys(toSortMap)).getIds().forEach(function (id) {
|
||
if (!toSortMap[id]) {
|
||
console.error(id + ' not found.');
|
||
}
|
||
finalPromises.push.apply(finalPromises, toSortMap[id]);
|
||
});
|
||
promises = finalPromises;
|
||
|
||
// build
|
||
var i = 0;
|
||
var l = promises.length;
|
||
var code = { js: '', css: '' };
|
||
var errors = [];
|
||
|
||
var f = function (resolve) {
|
||
if (i < l) {
|
||
var p = promises[i];
|
||
p.contentsPromise.then(function (contents) {
|
||
code[p.type] += contents + (p.type === 'js' && !/;\s*$/.test(contents) ? ';' : '') + '\n';
|
||
i++;
|
||
f(resolve);
|
||
});
|
||
p.contentsPromise['catch'](function () {
|
||
errors.push($u.element.create({
|
||
tag: 'p',
|
||
prop: {
|
||
textContent: 'An error occurred while fetching the file "' + p.path + '".'
|
||
}
|
||
}));
|
||
i++;
|
||
f(resolve);
|
||
});
|
||
} else {
|
||
resolve({ code: code, errors: errors });
|
||
}
|
||
};
|
||
|
||
return new Promise(f);
|
||
}
|
||
|
||
/**
|
||
* @returns {Promise<string>}
|
||
*/
|
||
function getVersion() {
|
||
return getFileContents('./package.json').then(function (jsonStr) {
|
||
return JSON.parse(jsonStr).version;
|
||
});
|
||
}
|
||
|
||
}());
|