Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 48 additions & 7 deletions download.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
<link rel="stylesheet" href="style.css" />
<link rel="stylesheet" href="themes/prism.css" data-noprefix />
<style>
form label {
form #components label,
form > p:first-of-type > label {
display: block;
padding: .2em;
padding-left: 1.9em;
Expand All @@ -17,16 +18,17 @@
-webkit-column-break-inside: avoid;
}

form label .name {
form #components label .name {
margin-right: .3em;
}

form label a.owner {
form #components label a.owner {
margin-left: 0;
hyphens: none;
}

form label input {
form #components label input,
form > p:first-of-type > label input {
margin-right: .7em;
margin-left: -1.7em;
}
Expand Down Expand Up @@ -54,7 +56,8 @@
}

section.options#category-languages,
section.options#category-plugins {
section.options#category-plugins,
section#options {
width: 100%;
float: none;
column-count: 3;
Expand All @@ -63,11 +66,13 @@
}

section.options#category-languages label,
section.options#category-plugins label {
section.options#category-plugins label,
section#options #option-container > div {
break-inside: avoid;
}
section.options#category-languages > h1,
section.options#category-plugins > h1 {
section.options#category-plugins > h1,
section#options > h1 {
margin-top: 0;
column-span: all;
}
Expand All @@ -91,6 +96,36 @@
column-span: all;
}


#option-container > div,
#option-container > div {
padding: 0 1em .5em 0;
}

#option-container label > span {
padding-right: .5em;
}

#option-container input[type="text"],
#option-container input[type="number"] {
display: block;
padding: 0;
margin: 0;
width: 100%;
box-sizing: border-box;
}

.option-item {
position: relative;
}

#option-container .option-item-error {
color: #B61500;
font-size: 90%;
display: none;
}


section.download {
width: 50%;
float: left;
Expand Down Expand Up @@ -148,6 +183,11 @@ <h2>Customize your download</h2>

<section id="components"></section>

<section id="options">
<h1>Options</h1>
<div id="option-container"></div>
</section>

<p>
Total filesize: <strong id="filesize"></strong> (<strong id="percent-js"></strong> JavaScript + <strong id="percent-css"></strong> CSS)
</p>
Expand Down Expand Up @@ -179,6 +219,7 @@ <h2>Customize your download</h2>
<script src="scripts/code.js"></script>
<script src="scripts/vendor/promise.js"></script>
<script src="scripts/vendor/FileSaver.min.js"></script>
<script src="scripts/download-options.js"></script>
<script src="scripts/download.js"></script>

</body>
Expand Down
243 changes: 243 additions & 0 deletions scripts/download-options.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
/**
* @typedef DownloadOption
* @property {string} id All ids have to be unique and start with `options-`.
* @property {string} title
* @property {string|string[]} [require] A list of required components for this option to take effect.
* @property {boolean} [enabled]
* @property {Object<string, OptionItem>} items
* @property {(this: DownloadOption, code: Code) => void} apply
*
* @typedef {StringOptionItem | NumberOptionItem | BooleanOptionItem} OptionItem
*
* @typedef StringOptionItem
* @property {string} title
* @property {string} [desc]
* @property {"string"} type
* @property {string} [value]
* @property {string} [default='']
* @property {Validator<string>} [validate]
*
* @typedef NumberOptionItem
* @property {string} title
* @property {string} [desc]
* @property {"number"} type
* @property {number} [value]
* @property {number} [default=0]
* @property {Validator<number>} [validate]
*
* @typedef BooleanOptionItem
* @property {string} title
* @property {string} [desc]
* @property {"boolean"} type
* @property {boolean} [value]
* @property {boolean} [default=false]
* @property {Validator<boolean>} [validate]
*
* @typedef Code
* @property {string} js The current JavaScript code of the build.
* @property {string} css The current CSS code of the build.
*/
/**
* A function that validates the given value of the item it is the validator for.
*
* It returns `undefined` if the given value is valid, or a non-empty error message (HTML code) that will be displayed
* to the user.
*
* @typedef {(value: T, option: DownloadOption) => string | undefined} Validator
* @template T
*/

/** @type {DownloadOption[]} */
var downloadOptions = [];

(function () {
/**
* Note: The keys of items are used as their identifier and will be be used in the URL hash.
*
* CHANGING OPTION IDS OR ITEM NAMES WILL BREAKING OLD URLS!
*/

addOption({
id: 'options-general',
title: 'General',
items: {
manual: {
title: 'Manual highlighting',
desc: 'Manual highlighting means that Prism will not highlight all code snippets and code blocks automatically. To highlight elements, you have to explicitly call Prism\'s highlight functions.',
type: 'boolean'
}
},
apply: function (code) {
if (this.items.manual.value) {
appendJs(code, 'Prism.manual=true;');
}
}
});

addOption({
id: 'options-custom-class',
title: 'Custom Class',
require: 'custom-class',
items: {
prefix: {
title: 'Class prefix',
desc: 'The classes of all tokens produced by Prism will have the given prefix.',
type: 'string',
validate: matchRegExp(/^(?:[a-z][\w-]*)?$/)
}
},
apply: function (code) {
var prefix = this.items.prefix.value;
if (!prefix) {
return;
}

// JS
appendJs(code, 'Prism.plugins.customClass.prefix(' + JSON.stringify(prefix) + ');');

// CSS
var tokens = Prism.tokenize(code.css, Prism.languages.css);

tokens.forEach(function (t) {
if (t.type === 'selector') {
var selector = stringify(t.content);

// The idea behind this regex is as follows:
// We have to detect all elements that have a `.token` class and change all classes of any such
// element. To do this, we search for all `.token` classes and include all surrounding component
// selectors as well. I.e. For the selector `code.style #id.token.foo:not(.bar)`, the text
// `#id.token.foo:not(.bar)` will be matched. We can then trivially detect all classes by the `.`
// prefix.
selector = selector.replace(/[-\w.#:()]*\.token(?![-\w])[-\w.#:()]*/g, function (m) {
return m.replace(/\.([\w-]+)/g, function (m, g1) {
return '.' + prefix + g1;
});
});

t.content = selector;
}
});

/**
* @param {string | Token | Token[]} t
* @returns {string}
*/
function stringify(t) {
if (typeof t === 'string') {
return t;
}
if (Array.isArray(t)) {
return t.map(stringify).join('');
}
return stringify(t.content);
}

code.css = stringify(tokens);
}
});

addOption({
id: 'options-filter-highlight-all',
title: 'Filter highlightAll',
require: 'filter-highlight-all',
items: {
filterKnown: {
title: 'Filter known',
type: 'boolean'
},
filterSelector: {
title: 'Filter CSS selector',
type: 'string'
},
rejectSelector: {
title: 'Reject CSS selector',
type: 'string'
}
},
apply: function (code) {
if (this.items.filterKnown.value) {
appendJs(code, 'Prism.plugins.filterHighlightAll.filterKnown=true;');
}
var filterSelector = this.items.filterSelector.value;
if (filterSelector) {
appendJs(code, 'Prism.plugins.filterHighlightAll.addSelector(' + JSON.stringify(filterSelector) + ');');
}
var rejectSelector = this.items.rejectSelector.value;
if (rejectSelector) {
appendJs(code, 'Prism.plugins.filterHighlightAll.reject.addSelector(' + JSON.stringify(rejectSelector) + ');');
}
}
});


// --- HELPER FUNCTIONS ---


/**
* This add the given option to the list of download option.
*
* @param {DownloadOption} option
*/
function addOption(option) {
if (!/^options-/.test(option.id)) {
throw new Error('Invalid id ' + option.id);
}
for (var name in option.items) {
if (option.items.hasOwnProperty(name)) {
if (!/^[\w-]+$/.test(name)) {
throw new Error("Invalid name: " + name);
}
var element = option.items[name];
element.validate = element.validate || voidFn;

if (element.default == undefined) {
if (element.type === 'boolean') {
element.default = false;
} else if (element.type === 'number') {
element.default = 0;
} else if (element.type === 'string') {
element.default = '';
}
}

if ('value' in element) {
throw new Error('The "value" property cannot be defined here. Use "default" instead.');
}
element.value = element.default;

// @ts-ignore
if (element.validate(element.value, option)) {
throw new Error('The default value of "' + name + '" has to be valid.');
}
}
}
downloadOptions.push(option);
}

function voidFn() { return undefined; }
/**
* @param {RegExp} re
* @returns {Validator<string>}
*/
function matchRegExp(re) {
return function (value) {
if (!re.test(value)) {
return 'The value must match the regular expression <code>' + text(re) + '</code>';
}
};
}

function text(str) {
return String(str).replace(/&/g, '&amp;').replace(/</g, '&lt;');
}

/**
* Appends the given JavaScript statement to the given code.
*
* @param {Code} code
* @param {string} statement
*/
function appendJs(code, statement) {
code.js = code.js + statement.replace(/;?\s*$/, ';\n');
}
}());
Loading