UNPKG

8.67 kBJavaScriptView Raw
1(function () {
2
3 if (typeof Prism === 'undefined' || typeof document === 'undefined') {
4 return;
5 }
6
7 /**
8 * @callback Adapter
9 * @param {any} response
10 * @param {HTMLPreElement} [pre]
11 * @returns {string | null}
12 */
13
14 /**
15 * The list of adapter which will be used if `data-adapter` is not specified.
16 *
17 * @type {Array<{adapter: Adapter, name: string}>}
18 */
19 var adapters = [];
20
21 /**
22 * Adds a new function to the list of adapters.
23 *
24 * If the given adapter is already registered or not a function or there is an adapter with the given name already,
25 * nothing will happen.
26 *
27 * @param {Adapter} adapter The adapter to be registered.
28 * @param {string} [name] The name of the adapter. Defaults to the function name of `adapter`.
29 */
30 function registerAdapter(adapter, name) {
31 name = name || adapter.name;
32 if (typeof adapter === 'function' && !getAdapter(adapter) && !getAdapter(name)) {
33 adapters.push({ adapter: adapter, name: name });
34 }
35 }
36 /**
37 * Returns the given adapter itself, if registered, or a registered adapter with the given name.
38 *
39 * If no fitting adapter is registered, `null` will be returned.
40 *
41 * @param {string|Function} adapter The adapter itself or the name of an adapter.
42 * @returns {Adapter} A registered adapter or `null`.
43 */
44 function getAdapter(adapter) {
45 if (typeof adapter === 'function') {
46 for (var i = 0, item; (item = adapters[i++]);) {
47 if (item.adapter.valueOf() === adapter.valueOf()) {
48 return item.adapter;
49 }
50 }
51 } else if (typeof adapter === 'string') {
52 // eslint-disable-next-line no-redeclare
53 for (var i = 0, item; (item = adapters[i++]);) {
54 if (item.name === adapter) {
55 return item.adapter;
56 }
57 }
58 }
59 return null;
60 }
61 /**
62 * Remove the given adapter or the first registered adapter with the given name from the list of
63 * registered adapters.
64 *
65 * @param {string|Function} adapter The adapter itself or the name of an adapter.
66 */
67 function removeAdapter(adapter) {
68 if (typeof adapter === 'string') {
69 adapter = getAdapter(adapter);
70 }
71 if (typeof adapter === 'function') {
72 var index = adapters.findIndex(function (item) {
73 return item.adapter === adapter;
74 });
75 if (index >= 0) {
76 adapters.splice(index, 1);
77 }
78 }
79 }
80
81 registerAdapter(function github(rsp) {
82 if (rsp && rsp.meta && rsp.data) {
83 if (rsp.meta.status && rsp.meta.status >= 400) {
84 return 'Error: ' + (rsp.data.message || rsp.meta.status);
85 } else if (typeof (rsp.data.content) === 'string') {
86 return typeof (atob) === 'function'
87 ? atob(rsp.data.content.replace(/\s/g, ''))
88 : 'Your browser cannot decode base64';
89 }
90 }
91 return null;
92 }, 'github');
93 registerAdapter(function gist(rsp, el) {
94 if (rsp && rsp.meta && rsp.data && rsp.data.files) {
95 if (rsp.meta.status && rsp.meta.status >= 400) {
96 return 'Error: ' + (rsp.data.message || rsp.meta.status);
97 }
98
99 var files = rsp.data.files;
100 var filename = el.getAttribute('data-filename');
101 if (filename == null) {
102 // Maybe in the future we can somehow render all files
103 // But the standard <script> include for gists does that nicely already,
104 // so that might be getting beyond the scope of this plugin
105 for (var key in files) {
106 if (files.hasOwnProperty(key)) {
107 filename = key;
108 break;
109 }
110 }
111 }
112
113 if (files[filename] !== undefined) {
114 return files[filename].content;
115 }
116 return 'Error: unknown or missing gist file ' + filename;
117 }
118 return null;
119 }, 'gist');
120 registerAdapter(function bitbucket(rsp) {
121 if (rsp && rsp.node && typeof (rsp.data) === 'string') {
122 return rsp.data;
123 }
124 return null;
125 }, 'bitbucket');
126
127
128 var jsonpCallbackCounter = 0;
129 /**
130 * Makes a JSONP request.
131 *
132 * @param {string} src The URL of the resource to request.
133 * @param {string | undefined | null} callbackParameter The name of the callback parameter. If falsy, `"callback"`
134 * will be used.
135 * @param {(data: unknown) => void} onSuccess
136 * @param {(reason: "timeout" | "network") => void} onError
137 * @returns {void}
138 */
139 function jsonp(src, callbackParameter, onSuccess, onError) {
140 var callbackName = 'prismjsonp' + jsonpCallbackCounter++;
141
142 var uri = document.createElement('a');
143 uri.href = src;
144 uri.href += (uri.search ? '&' : '?') + (callbackParameter || 'callback') + '=' + callbackName;
145
146 var script = document.createElement('script');
147 script.src = uri.href;
148 script.onerror = function () {
149 cleanup();
150 onError('network');
151 };
152
153 var timeoutId = setTimeout(function () {
154 cleanup();
155 onError('timeout');
156 }, Prism.plugins.jsonphighlight.timeout);
157
158 function cleanup() {
159 clearTimeout(timeoutId);
160 document.head.removeChild(script);
161 delete window[callbackName];
162 }
163
164 // the JSONP callback function
165 window[callbackName] = function (response) {
166 cleanup();
167 onSuccess(response);
168 };
169
170 document.head.appendChild(script);
171 }
172
173 var LOADING_MESSAGE = 'Loading…';
174 var MISSING_ADAPTER_MESSAGE = function (name) {
175 return '✖ Error: JSONP adapter function "' + name + '" doesn\'t exist';
176 };
177 var TIMEOUT_MESSAGE = function (url) {
178 return '✖ Error: Timeout loading ' + url;
179 };
180 var UNKNOWN_FAILURE_MESSAGE = '✖ Error: Cannot parse response (perhaps you need an adapter function?)';
181
182 var STATUS_ATTR = 'data-jsonp-status';
183 var STATUS_LOADING = 'loading';
184 var STATUS_LOADED = 'loaded';
185 var STATUS_FAILED = 'failed';
186
187 var SELECTOR = 'pre[data-jsonp]:not([' + STATUS_ATTR + '="' + STATUS_LOADED + '"])'
188 + ':not([' + STATUS_ATTR + '="' + STATUS_LOADING + '"])';
189
190
191 Prism.hooks.add('before-highlightall', function (env) {
192 env.selector += ', ' + SELECTOR;
193 });
194
195 Prism.hooks.add('before-sanity-check', function (env) {
196 var pre = /** @type {HTMLPreElement} */ (env.element);
197 if (pre.matches(SELECTOR)) {
198 env.code = ''; // fast-path the whole thing and go to complete
199
200 // mark as loading
201 pre.setAttribute(STATUS_ATTR, STATUS_LOADING);
202
203 // add code element with loading message
204 var code = pre.appendChild(document.createElement('CODE'));
205 code.textContent = LOADING_MESSAGE;
206
207 // set language
208 var language = env.language;
209 code.className = 'language-' + language;
210
211 // preload the language
212 var autoloader = Prism.plugins.autoloader;
213 if (autoloader) {
214 autoloader.loadLanguages(language);
215 }
216
217 var adapterName = pre.getAttribute('data-adapter');
218 var adapter = null;
219 if (adapterName) {
220 if (typeof window[adapterName] === 'function') {
221 adapter = window[adapterName];
222 } else {
223 // mark as failed
224 pre.setAttribute(STATUS_ATTR, STATUS_FAILED);
225
226 code.textContent = MISSING_ADAPTER_MESSAGE(adapterName);
227 return;
228 }
229 }
230
231 var src = pre.getAttribute('data-jsonp');
232
233 jsonp(
234 src,
235 pre.getAttribute('data-callback'),
236 function (response) {
237 // interpret the received data using the adapter(s)
238 var data = null;
239 if (adapter) {
240 data = adapter(response, pre);
241 } else {
242 for (var i = 0, l = adapters.length; i < l; i++) {
243 data = adapters[i].adapter(response, pre);
244 if (data !== null) {
245 break;
246 }
247 }
248 }
249
250 if (data === null) {
251 // mark as failed
252 pre.setAttribute(STATUS_ATTR, STATUS_FAILED);
253
254 code.textContent = UNKNOWN_FAILURE_MESSAGE;
255 } else {
256 // mark as loaded
257 pre.setAttribute(STATUS_ATTR, STATUS_LOADED);
258
259 code.textContent = data;
260 Prism.highlightElement(code);
261 }
262 },
263 function () {
264 // mark as failed
265 pre.setAttribute(STATUS_ATTR, STATUS_FAILED);
266
267 code.textContent = TIMEOUT_MESSAGE(src);
268 }
269 );
270 }
271 });
272
273
274 Prism.plugins.jsonphighlight = {
275 /**
276 * The timeout after which an error message will be displayed.
277 *
278 * __Note:__ If the request succeeds after the timeout, it will still be processed and will override any
279 * displayed error messages.
280 */
281 timeout: 5000,
282 registerAdapter: registerAdapter,
283 removeAdapter: removeAdapter,
284
285 /**
286 * Highlights all `pre` elements under the given container with a `data-jsonp` attribute by requesting the
287 * specified JSON and using the specified adapter or a registered adapter to extract the code to highlight
288 * from the response. The highlighted code will be inserted into the `pre` element.
289 *
290 * Note: Elements which are already loaded or currently loading will not be touched by this method.
291 *
292 * @param {Element | Document} [container=document]
293 */
294 highlight: function (container) {
295 var elements = (container || document).querySelectorAll(SELECTOR);
296
297 for (var i = 0, element; (element = elements[i++]);) {
298 Prism.highlightElement(element);
299 }
300 }
301 };
302
303}());