exportHTML.js 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252
  1. const JSZip = require('jszip');
  2. window.ExportHtml = Popup => {
  3. const saveAs = function(blob, filename) {
  4. const dl = document.createElement('a');
  5. dl.href = window.URL.createObjectURL(blob);
  6. dl.onclick = event => document.body.removeChild(event.target);
  7. dl.style.display = 'none';
  8. dl.target = '_blank';
  9. dl.download = filename;
  10. document.body.appendChild(dl);
  11. dl.click();
  12. };
  13. const asyncForEach = async function(array, callback) {
  14. for (let index = 0; index < array.length; index++) {
  15. await callback(array[index], index, array);
  16. }
  17. };
  18. const getPageHtmlString = () => {
  19. return `<!doctype html>${window.document.querySelector('html').outerHTML}`;
  20. };
  21. const removeAnchors = htmlString => {
  22. const replaceOpenAnchor = htmlString.replace(
  23. new RegExp('<a ', 'gim'),
  24. '<span ',
  25. );
  26. return replaceOpenAnchor.replace(new RegExp('</a', 'gim'), '</span');
  27. };
  28. const ensureSidebarRemoved = () => {
  29. document.querySelector('.board-sidebar.sidebar').remove();
  30. };
  31. const addJsonExportToZip = async (zip, boardSlug) => {
  32. const downloadJSONLink = document.querySelector('.download-json-link');
  33. const downloadJSONURL = downloadJSONLink.href;
  34. const response = await fetch(downloadJSONURL);
  35. const responseBody = await response.text();
  36. zip.file(`data/${boardSlug}.json`, responseBody);
  37. };
  38. const closeSidebar = () => {
  39. document.querySelector('.board-header-btn.js-toggle-sidebar').click();
  40. };
  41. const cleanBoardHtml = () => {
  42. Array.from(document.querySelectorAll('script')).forEach(elem =>
  43. elem.remove(),
  44. );
  45. Array.from(
  46. document.querySelectorAll('link:not([rel="stylesheet"])'),
  47. ).forEach(elem => elem.remove());
  48. document.querySelector('#header-quick-access').remove();
  49. Array.from(
  50. document.querySelectorAll('#header-main-bar .board-header-btns'),
  51. ).forEach(elem => elem.remove());
  52. Array.from(
  53. document.querySelectorAll('.js-pop-over, .pop-over'),
  54. ).forEach(elem => elem.remove());
  55. Array.from(document.querySelectorAll('.list-composer')).forEach(elem =>
  56. elem.remove(),
  57. );
  58. Array.from(
  59. document.querySelectorAll(
  60. '.list-composer,.js-card-composer, .js-add-card',
  61. ),
  62. ).forEach(elem => elem.remove());
  63. Array.from(document.querySelectorAll('[href]:not(link)')).forEach(elem =>
  64. elem.attributes.removeNamedItem('href'),
  65. );
  66. Array.from(document.querySelectorAll('[href]')).forEach(elem => {
  67. // eslint-disable-next-line no-self-assign
  68. elem.href = elem.href;
  69. // eslint-disable-next-line no-self-assign
  70. elem.src = elem.src;
  71. });
  72. Array.from(document.querySelectorAll('.is-editable')).forEach(elem => {
  73. elem.classList.remove('is-editable');
  74. });
  75. };
  76. const getBoardSlug = () => {
  77. return window.location.href.split('/').pop();
  78. };
  79. const getStylesheetList = () => {
  80. return Array.from(
  81. document.querySelectorAll('link[href][rel="stylesheet"]'),
  82. );
  83. };
  84. const downloadStylesheets = async (stylesheets, zip) => {
  85. await asyncForEach(stylesheets, async elem => {
  86. const response = await fetch(elem.href);
  87. const responseBody = await response.text();
  88. const finalResponse = responseBody.replace(
  89. new RegExp('packages/[^/]+/upstream/', 'gim'),
  90. '../',
  91. );
  92. const filename = elem.href
  93. .split('/')
  94. .pop()
  95. .split('?')
  96. .shift();
  97. const fileFullPath = `style/${filename}`;
  98. zip.file(fileFullPath, finalResponse);
  99. elem.href = `../${fileFullPath}`;
  100. });
  101. };
  102. const getSrcAttached = () => {
  103. return Array.from(document.querySelectorAll('[src]'));
  104. };
  105. const downloadSrcAttached = async (elements, zip, boardSlug) => {
  106. await asyncForEach(elements, async elem => {
  107. const response = await fetch(elem.src);
  108. const responseBody = await response.blob();
  109. const filename = elem.src
  110. .split('/')
  111. .pop()
  112. .split('?')
  113. .shift();
  114. const fileFullPath = `${boardSlug}/${elem.tagName.toLowerCase()}/${filename}`;
  115. zip.file(fileFullPath, responseBody);
  116. elem.src = `./${elem.tagName.toLowerCase()}/${filename}`;
  117. });
  118. };
  119. const removeCssUrlSurround = url => {
  120. const working = url || '';
  121. return working
  122. .split('url(')
  123. .join('')
  124. .split('")')
  125. .join('')
  126. .split('"')
  127. .join('')
  128. .split("')")
  129. .join('')
  130. .split("'")
  131. .join('')
  132. .split(')')
  133. .join('');
  134. };
  135. const getCardCovers = () => {
  136. return Array.from(document.querySelectorAll('.minicard-cover')).filter(
  137. elem => elem.style['background-image'],
  138. );
  139. };
  140. const getWebFonts = () => {
  141. fontUrls = [];
  142. for (let sheet of document.styleSheets) {
  143. // Get the base URL of the stylesheet
  144. let baseUrl = sheet.href ? new URL(sheet.href).origin : window.location.origin;
  145. try {
  146. for (let rule of sheet.cssRules) {
  147. if (rule.type === CSSRule.FONT_FACE_RULE) {
  148. let src = rule.style.getPropertyValue('src');
  149. let urlMatch = src.match(/url\(["']?(.+?)["']?\)/);
  150. if (urlMatch) {
  151. let fontUrl = urlMatch[1];
  152. // Resolve the URL relative to the stylesheet's base URL
  153. let resolvedUrl = new URL(fontUrl, baseUrl);
  154. fontUrls.push(resolvedUrl.href); // Using .href to get the absolute URL
  155. }
  156. }
  157. }
  158. } catch (e) {
  159. console.log('Access to stylesheet blocked:', e);
  160. }
  161. }
  162. return fontUrls;
  163. };
  164. const downloadFonts = async(elements, zip) => {
  165. await asyncForEach(elements, async elem => {
  166. const response = await fetch(elem);
  167. const responseBody = await response.blob();
  168. const filename = elem.split('/')
  169. .pop()
  170. .split('?')
  171. .shift()
  172. .split('#')
  173. .shift();
  174. const fileFullPath = `webfonts/${filename}`;
  175. zip.file(fileFullPath, responseBody);
  176. });
  177. }
  178. const downloadCardCovers = async (elements, zip, boardSlug) => {
  179. await asyncForEach(elements, async elem => {
  180. const response = await fetch(
  181. removeCssUrlSurround(elem.style['background-image']),
  182. );
  183. const responseBody = await response.blob();
  184. const filename = removeCssUrlSurround(elem.style['background-image'])
  185. .split('/')
  186. .pop()
  187. .split('?')
  188. .shift()
  189. .split('#')
  190. .shift();
  191. const fileFullPath = `${boardSlug}/covers/${filename}`;
  192. zip.file(fileFullPath, responseBody);
  193. elem.style = "background-image: url('" + `covers/${filename}` + "')";
  194. });
  195. };
  196. const addBoardHTMLToZip = (boardSlug, zip) => {
  197. ensureSidebarRemoved();
  198. const htmlOutputPath = `${boardSlug}/index.html`;
  199. zip.file(
  200. htmlOutputPath,
  201. new Blob([removeAnchors(getPageHtmlString())], {
  202. type: 'application/html',
  203. }),
  204. );
  205. };
  206. return async () => {
  207. const zip = new JSZip();
  208. const boardSlug = getBoardSlug();
  209. await addJsonExportToZip(zip, boardSlug);
  210. Popup.back();
  211. closeSidebar();
  212. cleanBoardHtml();
  213. await downloadStylesheets(getStylesheetList(), zip);
  214. await downloadSrcAttached(getSrcAttached(), zip, boardSlug);
  215. await downloadCardCovers(getCardCovers(), zip, boardSlug);
  216. await downloadFonts(getWebFonts(), zip);
  217. addBoardHTMLToZip(boardSlug, zip);
  218. const content = await zip.generateAsync({ type: 'blob' });
  219. saveAs(content, `${boardSlug}.zip`);
  220. window.location.reload();
  221. };
  222. };