(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); } } }; }());