You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
303 lines
8.5 KiB
303 lines
8.5 KiB
(function () { |
|
|
|
if (typeof Prism === 'undefined' || typeof document === 'undefined') { |
|
return; |
|
} |
|
|
|
/** |
|
* @callback Adapter |
|
* @param {any} response |
|
* @param {HTMLPreElement} [pre] |
|
* @returns {string | null} |
|
*/ |
|
|
|
/** |
|
* The list of adapter which will be used if `data-adapter` is not specified. |
|
* |
|
* @type {Array<{adapter: Adapter, name: string}>} |
|
*/ |
|
var adapters = []; |
|
|
|
/** |
|
* Adds a new function to the list of adapters. |
|
* |
|
* If the given adapter is already registered or not a function or there is an adapter with the given name already, |
|
* nothing will happen. |
|
* |
|
* @param {Adapter} adapter The adapter to be registered. |
|
* @param {string} [name] The name of the adapter. Defaults to the function name of `adapter`. |
|
*/ |
|
function registerAdapter(adapter, name) { |
|
name = name || adapter.name; |
|
if (typeof adapter === 'function' && !getAdapter(adapter) && !getAdapter(name)) { |
|
adapters.push({ adapter: adapter, name: name }); |
|
} |
|
} |
|
/** |
|
* Returns the given adapter itself, if registered, or a registered adapter with the given name. |
|
* |
|
* If no fitting adapter is registered, `null` will be returned. |
|
* |
|
* @param {string|Function} adapter The adapter itself or the name of an adapter. |
|
* @returns {Adapter} A registered adapter or `null`. |
|
*/ |
|
function getAdapter(adapter) { |
|
if (typeof adapter === 'function') { |
|
for (var i = 0, item; (item = adapters[i++]);) { |
|
if (item.adapter.valueOf() === adapter.valueOf()) { |
|
return item.adapter; |
|
} |
|
} |
|
} else if (typeof adapter === 'string') { |
|
// eslint-disable-next-line no-redeclare |
|
for (var i = 0, item; (item = adapters[i++]);) { |
|
if (item.name === adapter) { |
|
return item.adapter; |
|
} |
|
} |
|
} |
|
return null; |
|
} |
|
/** |
|
* Remove the given adapter or the first registered adapter with the given name from the list of |
|
* registered adapters. |
|
* |
|
* @param {string|Function} adapter The adapter itself or the name of an adapter. |
|
*/ |
|
function removeAdapter(adapter) { |
|
if (typeof adapter === 'string') { |
|
adapter = getAdapter(adapter); |
|
} |
|
if (typeof adapter === 'function') { |
|
var index = adapters.findIndex(function (item) { |
|
return item.adapter === adapter; |
|
}); |
|
if (index >= 0) { |
|
adapters.splice(index, 1); |
|
} |
|
} |
|
} |
|
|
|
registerAdapter(function github(rsp) { |
|
if (rsp && rsp.meta && rsp.data) { |
|
if (rsp.meta.status && rsp.meta.status >= 400) { |
|
return 'Error: ' + (rsp.data.message || rsp.meta.status); |
|
} else if (typeof (rsp.data.content) === 'string') { |
|
return typeof (atob) === 'function' |
|
? atob(rsp.data.content.replace(/\s/g, '')) |
|
: 'Your browser cannot decode base64'; |
|
} |
|
} |
|
return null; |
|
}, 'github'); |
|
registerAdapter(function gist(rsp, el) { |
|
if (rsp && rsp.meta && rsp.data && rsp.data.files) { |
|
if (rsp.meta.status && rsp.meta.status >= 400) { |
|
return 'Error: ' + (rsp.data.message || rsp.meta.status); |
|
} |
|
|
|
var files = rsp.data.files; |
|
var filename = el.getAttribute('data-filename'); |
|
if (filename == null) { |
|
// Maybe in the future we can somehow render all files |
|
// But the standard <script> include for gists does that nicely already, |
|
// so that might be getting beyond the scope of this plugin |
|
for (var key in files) { |
|
if (files.hasOwnProperty(key)) { |
|
filename = key; |
|
break; |
|
} |
|
} |
|
} |
|
|
|
if (files[filename] !== undefined) { |
|
return files[filename].content; |
|
} |
|
return 'Error: unknown or missing gist file ' + filename; |
|
} |
|
return null; |
|
}, 'gist'); |
|
registerAdapter(function bitbucket(rsp) { |
|
if (rsp && rsp.node && typeof (rsp.data) === 'string') { |
|
return rsp.data; |
|
} |
|
return null; |
|
}, 'bitbucket'); |
|
|
|
|
|
var jsonpCallbackCounter = 0; |
|
/** |
|
* Makes a JSONP request. |
|
* |
|
* @param {string} src The URL of the resource to request. |
|
* @param {string | undefined | null} callbackParameter The name of the callback parameter. If falsy, `"callback"` |
|
* will be used. |
|
* @param {(data: unknown) => void} onSuccess |
|
* @param {(reason: "timeout" | "network") => void} onError |
|
* @returns {void} |
|
*/ |
|
function jsonp(src, callbackParameter, onSuccess, onError) { |
|
var callbackName = 'prismjsonp' + jsonpCallbackCounter++; |
|
|
|
var uri = document.createElement('a'); |
|
uri.href = src; |
|
uri.href += (uri.search ? '&' : '?') + (callbackParameter || 'callback') + '=' + callbackName; |
|
|
|
var script = document.createElement('script'); |
|
script.src = uri.href; |
|
script.onerror = function () { |
|
cleanup(); |
|
onError('network'); |
|
}; |
|
|
|
var timeoutId = setTimeout(function () { |
|
cleanup(); |
|
onError('timeout'); |
|
}, Prism.plugins.jsonphighlight.timeout); |
|
|
|
function cleanup() { |
|
clearTimeout(timeoutId); |
|
document.head.removeChild(script); |
|
delete window[callbackName]; |
|
} |
|
|
|
// the JSONP callback function |
|
window[callbackName] = function (response) { |
|
cleanup(); |
|
onSuccess(response); |
|
}; |
|
|
|
document.head.appendChild(script); |
|
} |
|
|
|
var LOADING_MESSAGE = 'Loading…'; |
|
var MISSING_ADAPTER_MESSAGE = function (name) { |
|
return '✖ Error: JSONP adapter function "' + name + '" doesn\'t exist'; |
|
}; |
|
var TIMEOUT_MESSAGE = function (url) { |
|
return '✖ Error: Timeout loading ' + url; |
|
}; |
|
var UNKNOWN_FAILURE_MESSAGE = '✖ Error: Cannot parse response (perhaps you need an adapter function?)'; |
|
|
|
var STATUS_ATTR = 'data-jsonp-status'; |
|
var STATUS_LOADING = 'loading'; |
|
var STATUS_LOADED = 'loaded'; |
|
var STATUS_FAILED = 'failed'; |
|
|
|
var SELECTOR = 'pre[data-jsonp]:not([' + STATUS_ATTR + '="' + STATUS_LOADED + '"])' |
|
+ ':not([' + STATUS_ATTR + '="' + STATUS_LOADING + '"])'; |
|
|
|
|
|
Prism.hooks.add('before-highlightall', function (env) { |
|
env.selector += ', ' + SELECTOR; |
|
}); |
|
|
|
Prism.hooks.add('before-sanity-check', function (env) { |
|
var pre = /** @type {HTMLPreElement} */ (env.element); |
|
if (pre.matches(SELECTOR)) { |
|
env.code = ''; // fast-path the whole thing and go to complete |
|
|
|
// mark as loading |
|
pre.setAttribute(STATUS_ATTR, STATUS_LOADING); |
|
|
|
// add code element with loading message |
|
var code = pre.appendChild(document.createElement('CODE')); |
|
code.textContent = LOADING_MESSAGE; |
|
|
|
// set language |
|
var language = env.language; |
|
code.className = 'language-' + language; |
|
|
|
// preload the language |
|
var autoloader = Prism.plugins.autoloader; |
|
if (autoloader) { |
|
autoloader.loadLanguages(language); |
|
} |
|
|
|
var adapterName = pre.getAttribute('data-adapter'); |
|
var adapter = null; |
|
if (adapterName) { |
|
if (typeof window[adapterName] === 'function') { |
|
adapter = window[adapterName]; |
|
} else { |
|
// mark as failed |
|
pre.setAttribute(STATUS_ATTR, STATUS_FAILED); |
|
|
|
code.textContent = MISSING_ADAPTER_MESSAGE(adapterName); |
|
return; |
|
} |
|
} |
|
|
|
var src = pre.getAttribute('data-jsonp'); |
|
|
|
jsonp( |
|
src, |
|
pre.getAttribute('data-callback'), |
|
function (response) { |
|
// interpret the received data using the adapter(s) |
|
var data = null; |
|
if (adapter) { |
|
data = adapter(response, pre); |
|
} else { |
|
for (var i = 0, l = adapters.length; i < l; i++) { |
|
data = adapters[i].adapter(response, pre); |
|
if (data !== null) { |
|
break; |
|
} |
|
} |
|
} |
|
|
|
if (data === null) { |
|
// mark as failed |
|
pre.setAttribute(STATUS_ATTR, STATUS_FAILED); |
|
|
|
code.textContent = UNKNOWN_FAILURE_MESSAGE; |
|
} else { |
|
// mark as loaded |
|
pre.setAttribute(STATUS_ATTR, STATUS_LOADED); |
|
|
|
code.textContent = data; |
|
Prism.highlightElement(code); |
|
} |
|
}, |
|
function () { |
|
// mark as failed |
|
pre.setAttribute(STATUS_ATTR, STATUS_FAILED); |
|
|
|
code.textContent = TIMEOUT_MESSAGE(src); |
|
} |
|
); |
|
} |
|
}); |
|
|
|
|
|
Prism.plugins.jsonphighlight = { |
|
/** |
|
* The timeout after which an error message will be displayed. |
|
* |
|
* __Note:__ If the request succeeds after the timeout, it will still be processed and will override any |
|
* displayed error messages. |
|
*/ |
|
timeout: 5000, |
|
registerAdapter: registerAdapter, |
|
removeAdapter: removeAdapter, |
|
|
|
/** |
|
* Highlights all `pre` elements under the given container with a `data-jsonp` attribute by requesting the |
|
* specified JSON and using the specified adapter or a registered adapter to extract the code to highlight |
|
* from the response. The highlighted code will be inserted into the `pre` element. |
|
* |
|
* Note: Elements which are already loaded or currently loading will not be touched by this method. |
|
* |
|
* @param {Element | Document} [container=document] |
|
*/ |
|
highlight: function (container) { |
|
var elements = (container || document).querySelectorAll(SELECTOR); |
|
|
|
for (var i = 0, element; (element = elements[i++]);) { |
|
Prism.highlightElement(element); |
|
} |
|
} |
|
}; |
|
|
|
}());
|
|
|