prism-jsonp-highlight.js 8.5 KB

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