prism-jsonp-highlight.js 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276
  1. (function () {
  2. if (typeof self === 'undefined' || !self.Prism || !self.document) {
  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. }
  48. else if (typeof adapter === "string") {
  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, el) {
  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. }
  81. else if (typeof (rsp.data.content) === "string") {
  82. return typeof (atob) === "function"
  83. ? atob(rsp.data.content.replace(/\s/g, ""))
  84. : "Your browser cannot decode base64";
  85. }
  86. }
  87. return null;
  88. }, 'github');
  89. registerAdapter(function gist(rsp, el) {
  90. if (rsp && rsp.meta && rsp.data && rsp.data.files) {
  91. if (rsp.meta.status && rsp.meta.status >= 400) {
  92. return "Error: " + (rsp.data.message || rsp.meta.status);
  93. }
  94. var files = rsp.data.files;
  95. var filename = el.getAttribute("data-filename");
  96. if (filename == null) {
  97. // Maybe in the future we can somehow render all files
  98. // But the standard <script> include for gists does that nicely already,
  99. // so that might be getting beyond the scope of this plugin
  100. for (var key in files) {
  101. if (files.hasOwnProperty(key)) {
  102. filename = key;
  103. break;
  104. }
  105. }
  106. }
  107. if (files[filename] !== undefined) {
  108. return files[filename].content;
  109. }
  110. return "Error: unknown or missing gist file " + filename;
  111. }
  112. return null;
  113. }, 'gist');
  114. registerAdapter(function bitbucket(rsp, el) {
  115. if (rsp && rsp.node && typeof (rsp.data) === "string") {
  116. return rsp.data;
  117. }
  118. return null;
  119. }, 'bitbucket');
  120. var jsonpCallbackCounter = 0;
  121. var LOADING_MESSAGE = 'Loading…';
  122. var MISSING_ADAPTER_MESSAGE = function (name) {
  123. return '✖ Error: JSONP adapter function "' + name + '" doesn\'t exist';
  124. };
  125. var TIMEOUT_MESSAGE = function (url) {
  126. return '✖ Error: Timeout loading ' + url;
  127. };
  128. var UNKNOWN_FAILURE_MESSAGE = '✖ Error: Cannot parse response (perhaps you need an adapter function?)';
  129. var STATUS_ATTR = 'data-jsonp-status';
  130. var STATUS_LOADING = 'loading';
  131. var STATUS_LOADED = 'loaded';
  132. var STATUS_FAILED = 'failed';
  133. var SELECTOR = 'pre[data-jsonp]:not([' + STATUS_ATTR + '="' + STATUS_LOADED + '"])'
  134. + ':not([' + STATUS_ATTR + '="' + STATUS_LOADING + '"])';
  135. Prism.hooks.add('before-highlightall', function (env) {
  136. env.selector += ', ' + SELECTOR;
  137. });
  138. Prism.hooks.add('before-sanity-check', function (env) {
  139. var pre = /** @type {HTMLPreElement} */ (env.element);
  140. if (pre.matches(SELECTOR)) {
  141. env.code = ''; // fast-path the whole thing and go to complete
  142. // mark as loading
  143. pre.setAttribute(STATUS_ATTR, STATUS_LOADING);
  144. // add code element with loading message
  145. var code = pre.appendChild(document.createElement('CODE'));
  146. code.textContent = LOADING_MESSAGE;
  147. // set language
  148. var language = env.language;
  149. code.className = 'language-' + language;
  150. // preload the language
  151. var autoloader = Prism.plugins.autoloader;
  152. if (autoloader) {
  153. autoloader.loadLanguages(language);
  154. }
  155. var adapterName = pre.getAttribute('data-adapter');
  156. var adapter = null;
  157. if (adapterName) {
  158. if (typeof window[adapterName] === 'function') {
  159. adapter = window[adapterName];
  160. } else {
  161. // mark as failed
  162. pre.setAttribute(STATUS_ATTR, STATUS_FAILED);
  163. code.textContent = MISSING_ADAPTER_MESSAGE(adapterName);
  164. return;
  165. }
  166. }
  167. var callbackName = 'prismjsonp' + jsonpCallbackCounter++;
  168. var uri = document.createElement('a');
  169. var src = uri.href = pre.getAttribute('data-jsonp');
  170. uri.href += (uri.search ? '&' : '?') + (pre.getAttribute('data-callback') || 'callback') + '=' + callbackName;
  171. var timeout = setTimeout(function () {
  172. // we could clean up window[cb], but if the request finally succeeds, keeping it around is a good thing
  173. // mark as failed
  174. pre.setAttribute(STATUS_ATTR, STATUS_FAILED);
  175. code.textContent = TIMEOUT_MESSAGE(src);
  176. }, Prism.plugins.jsonphighlight.timeout);
  177. var script = document.createElement('script');
  178. script.src = uri.href;
  179. // the JSONP callback function
  180. window[callbackName] = function (response) {
  181. // clean up
  182. document.head.removeChild(script);
  183. clearTimeout(timeout);
  184. delete window[callbackName];
  185. // interpret the received data using the adapter(s)
  186. var data = null;
  187. if (adapter) {
  188. data = adapter(response, pre);
  189. } else {
  190. for (var i = 0, l = adapters.length; i < l; i++) {
  191. data = adapters[i].adapter(response, pre);
  192. if (data !== null) {
  193. break;
  194. }
  195. }
  196. }
  197. if (data === null) {
  198. // mark as failed
  199. pre.setAttribute(STATUS_ATTR, STATUS_FAILED);
  200. code.textContent = UNKNOWN_FAILURE_MESSAGE;
  201. } else {
  202. // mark as loaded
  203. pre.setAttribute(STATUS_ATTR, STATUS_LOADED);
  204. code.textContent = data;
  205. Prism.highlightElement(code);
  206. }
  207. };
  208. document.head.appendChild(script);
  209. }
  210. });
  211. Prism.plugins.jsonphighlight = {
  212. /**
  213. * The timeout after which an error message will be displayed.
  214. *
  215. * __Note:__ If the request succeeds after the timeout, it will still be processed and will override any
  216. * displayed error messages.
  217. */
  218. timeout: 5000,
  219. registerAdapter: registerAdapter,
  220. removeAdapter: removeAdapter,
  221. /**
  222. * Highlights all `pre` elements under the given container with a `data-jsonp` attribute by requesting the
  223. * specified JSON and using the specified adapter or a registered adapter to extract the code to highlight
  224. * from the response. The highlighted code will be inserted into the `pre` element.
  225. *
  226. * Note: Elements which are already loaded or currently loading will not be touched by this method.
  227. *
  228. * @param {Element | Document} [container=document]
  229. */
  230. highlight: function (container) {
  231. var elements = (container || document).querySelectorAll(SELECTOR);
  232. for (var i = 0, element; element = elements[i++];) {
  233. Prism.highlightElement(element);
  234. }
  235. }
  236. };
  237. })();