index.js 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782
  1. import Awesome from '../background/awesome.js';
  2. const DEV_TOOLS_MY_TOOLS = 'DEV-TOOLS:MY-TOOLS';
  3. const TOOL_NAME_TPL = 'DYNAMIC_TOOL:#TOOL-NAME#';
  4. const TOOL_CONTENT_SCRIPT_TPL = 'DYNAMIC_TOOL:CS:#TOOL-NAME#';
  5. const TOOL_CONTENT_SCRIPT_CSS_TPL = 'DYNAMIC_TOOL:CS:CSS:#TOOL-NAME#';
  6. let editor = null;
  7. new Vue({
  8. el: '#pageContainer',
  9. data: {
  10. showGivenIcons: false,
  11. myTools: {},
  12. showEditorFlag: false,
  13. showNewToolForm: false,
  14. updateUrlMode: false,
  15. givenIconList: [],
  16. model: {},
  17. demo: {
  18. name: 'hello-world',
  19. files: ['fh-config.js', 'index.html', 'index.js', 'index.css', 'content-script.js','content-script.css']
  20. }
  21. },
  22. mounted: function () {
  23. this.getToolConfigs();
  24. },
  25. methods: {
  26. startByDemo() {
  27. this.loadDemo().then(() => {
  28. setTimeout(() => this.getToolConfigs(),300);
  29. });
  30. },
  31. toggleEditor(show, toolName) {
  32. this.showEditorFlag = show;
  33. if (show && toolName) {
  34. let toolObj = this.myTools[toolName] || {};
  35. this.model = {
  36. tool: toolName,
  37. name: toolObj.name
  38. };
  39. this.getToolFilesFromLocal(toolName).then(files => {
  40. this.model.files = files;
  41. if (!editor) {
  42. editor = CodeMirror.fromTextArea(this.$refs.txtEditor, {
  43. mode: 'javascript',
  44. lineNumbers: true,
  45. matchBrackets: true,
  46. styleActiveLine: true,
  47. lineWrapping: true
  48. });
  49. editor.on('change', (editor, changes) => {
  50. let result = this.saveContentToLocal(this.model.tool, this.model.editingFile, editor.getValue());
  51. if (this.model.editingFile === 'fh-config.js' && result) {
  52. result.contentScriptJs && !this.model.files.includes('content-script.js') && this.model.files.push('content-script.js');
  53. result.contentScriptCss && !this.model.files.includes('content-script.css') && this.model.files.push('content-script.css');
  54. this.$forceUpdate();
  55. }
  56. });
  57. editor.on('keydown', (editor, event) => {
  58. if (event.metaKey || event.ctrlKey) {
  59. if (event.code === 'KeyS') {
  60. this.toast('当前代码是自动保存的,无需Ctrl+S手动保存!');
  61. event.preventDefault();
  62. event.stopPropagation();
  63. return false;
  64. }
  65. }
  66. });
  67. }
  68. this.$nextTick(() => this.editFile(toolName, files[0]));
  69. });
  70. }
  71. },
  72. editFile(toolName, fileName) {
  73. let editorMode = {
  74. css: 'text/css',
  75. js: {name: 'javascript', json: true},
  76. html: 'htmlmixed'
  77. };
  78. let mode = editorMode[/\.(js|css|html)$/.exec(fileName)[1]];
  79. editor.setOption('mode', mode);
  80. this.model.editingFile = fileName;
  81. this.getContentFromLocal(toolName, fileName).then(content => {
  82. editor.setValue(content);
  83. editor.focus();
  84. this.$forceUpdate();
  85. });
  86. },
  87. importFile(toolName) {
  88. let fileInput = document.createElement('input');
  89. fileInput.type = 'file';
  90. fileInput.multiple = 'multiple';
  91. fileInput.accept = 'text/javascript,text/css,text/html';
  92. fileInput.style.cssText = 'position:absolute;top:-100px;left:-100px';
  93. fileInput.addEventListener('change', (evt) => {
  94. Array.prototype.slice.call(fileInput.files).forEach(file => {
  95. let reader = new FileReader();
  96. reader.onload = (evt) => {
  97. if (this.model.files.includes(file.name)) {
  98. if (!confirm(`文件 ${file.name} 已经存在,是否需要覆盖?`)) {
  99. return false;
  100. }
  101. } else {
  102. this.model.files.push(file.name);
  103. }
  104. this.saveContentToLocal(toolName, file.name, evt.target.result);
  105. this.editFile(toolName, file.name);
  106. };
  107. reader.readAsText(file);
  108. })
  109. }, false);
  110. document.body.appendChild(fileInput);
  111. fileInput.click();
  112. window.setTimeout(() => fileInput.remove(), 3000);
  113. },
  114. createFile(toolName) {
  115. let fileName = prompt('请输入你要创建的文件名!注意,只能是 *.html 、*.js 、*.css 类型的文件!').trim();
  116. let result = /^[\w\-_\.]+\.(html|js|css)$/.exec(fileName);
  117. if (!result) {
  118. return alert('文件格式不正确!创建文件失败!');
  119. }
  120. if (this.model.files.includes(fileName)) {
  121. return alert(`文件 ${fileName} 已经存在!`);
  122. }
  123. this.model.files.push(fileName);
  124. this.saveContentToLocal(toolName, fileName, '');
  125. this.editFile(toolName, fileName);
  126. },
  127. deleteFile(toolName, fileName, event) {
  128. event.preventDefault();
  129. event.stopPropagation();
  130. if (['fh-config.js', 'index.html'].includes(fileName)) {
  131. return alert(`文件 ${fileName} 不允许被删除!`);
  132. }
  133. if (confirm(`确定要删除文件 ${fileName} 吗?此操作不可撤销,请三思!`)) {
  134. this.model.files.splice(this.model.files.indexOf(fileName), 1);
  135. this.$forceUpdate();
  136. let key = '';
  137. switch (fileName) {
  138. case 'index.html':
  139. key = TOOL_NAME_TPL.replace('#TOOL-NAME#', toolName);
  140. break;
  141. case 'content-script.js':
  142. key = TOOL_CONTENT_SCRIPT_TPL.replace('#TOOL-NAME#', toolName);
  143. break;
  144. case 'content-script.css':
  145. key = TOOL_CONTENT_SCRIPT_CSS_TPL.replace('#TOOL-NAME#', toolName);
  146. break;
  147. default:
  148. key = fileName.startsWith(`../${toolName}/`) ? fileName : `../${toolName}/${fileName}`;
  149. }
  150. Awesome.StorageMgr.remove(key);
  151. }
  152. },
  153. addNewTool(localOrUrl) {
  154. this.showNewToolForm = true;
  155. this.updateUrlMode = localOrUrl === 'url';
  156. if (this.updateUrlMode) {
  157. this.toast('请务必载入自己的Web工具服务!PS:请尽量别把它当成网站原内容的爬取工具,因为别人的网站你爬取过来也不一定完全能运行!');
  158. }
  159. },
  160. newToolAction(event) {
  161. let toolId = this.$refs.toolId.value;
  162. let toolName = this.$refs.toolName.value;
  163. let toolIcon = this.$refs.toolIcon.value;
  164. let contentScript = this.$refs.hasContentScript.checked;
  165. let noPage = this.$refs.noPage.checked;
  166. let updateUrl = '';
  167. if (this.updateUrlMode) {
  168. updateUrl = this.$refs.updateUrl.value;
  169. if (updateUrl.indexOf('baidufe.com') > -1 || updateUrl.indexOf('fehelper.com') > -1) {
  170. return this.toast('如果你是要安装FeHelper官网的工具,请到插件配置页直接安装!');
  171. }
  172. }
  173. if (this.myTools[toolId]) {
  174. this.toast(`ID为 ${toolId} 的工具在本地已存在,请重新命名!`);
  175. event.preventDefault();
  176. return false;
  177. }
  178. // 关闭Form表单
  179. this.showNewToolForm = false;
  180. // 创建文件列表
  181. let files = ['fh-config.js'];
  182. (contentScript || noPage) && files.push('content-script.js');
  183. // 本地创建的模式,需要用模板来初始化
  184. if (!this.updateUrlMode) {
  185. files.push('index.html');
  186. if (!noPage) {
  187. files.push('index.css');
  188. files.push('index.js');
  189. }
  190. }
  191. // 初始化的文件内容,需要进行存储
  192. files.forEach(file => {
  193. let content = FileTpl[file].replace(/#toolName#/gm, toolId)
  194. .replace(/#toolFullName#/gm, toolName)
  195. .replace(/#toolIcon#/gm, toolIcon)
  196. .replace(/#updateUrl#/gm, updateUrl)
  197. .replace(/#contentScript#/gm, !!contentScript || !!noPage)
  198. .replace(/#noPage#/gm, !!noPage)
  199. .replace(/#toolNameLower#/gm, toolId.replace(/[\-_]/g, ''));
  200. if (noPage && file === 'content-script.js') {
  201. content += '\n\n' + FileTpl['noPage.js'].replace(/#toolName#/gm, toolId)
  202. .replace(/#toolNameLower#/gm, toolId.replace(/[\-_]/g, ''));
  203. }
  204. this.saveContentToLocal(toolId, file, content);
  205. });
  206. if (this.updateUrlMode) {
  207. // 远程下载并安装工具
  208. this.loadRemoteTool(toolId, updateUrl, this.toast).then(progress => {
  209. this.toast(progress);
  210. setTimeout(() => this.toggleEditor(true, toolId),300);
  211. this.toast('工具创建成功!现在可以进行实时编辑了!');
  212. });
  213. } else {
  214. setTimeout(() => this.toggleEditor(true, toolId),300);
  215. this.toast('工具创建成功!现在可以进行实时编辑了!');
  216. }
  217. event.preventDefault();
  218. return false;
  219. },
  220. loadRemoteTool(toolName, updateUrl, fnProgress) {
  221. return new Promise((resolve, reject) => {
  222. fnProgress && fnProgress('开始下载...');
  223. fetch(updateUrl).then(resp => resp.text()).then(html => {
  224. let result = this.htmlTplEncode(toolName, html, updateUrl);
  225. html = result.html;
  226. let files = result.jsCss;
  227. // 获取所有网络文件的总个数,以便于计算进度
  228. let total = evalCore.getEvalInstance(window)(Object.values(files).map(a => a.length).join('+')) + 1;
  229. let loaded = 1;
  230. fnProgress && fnProgress(Math.floor(100 * loaded / total) + '%');
  231. (async () => {
  232. let toolObj = this.myTools[toolName];
  233. for (let t in files) {
  234. for (let f = 0; f < files[t].length; f++) {
  235. let fs = files[t][f];
  236. // script-block内识别出来的代码,直接保存
  237. if (t === 'js' && fs[0].indexOf('fh-script-block.js') > -1) {
  238. this.saveContentToLocal(toolName, fs[0], fs[2]);
  239. continue;
  240. }
  241. await fetch(fs[2]).then(resp => resp.text()).then(txt => {
  242. this.saveContentToLocal(toolName, fs[0], txt);
  243. // 保存content-script / background-script
  244. if (toolObj.contentScriptJs && fs[0].indexOf(toolName + '/content-script.js') !== -1) {
  245. this.saveContentToLocal(toolName, 'content-script.js', txt);
  246. // 存储content-script.css文件内容
  247. if (toolObj.contentScriptCss) {
  248. fetch(fs[2].replace('content-script.js', 'content-script.css')).then(resp => resp.text()).then(css => {
  249. this.saveContentToLocal(toolName, 'content-script.css', css);
  250. });
  251. }
  252. }
  253. fnProgress && fnProgress(Math.floor(100 * ++loaded / total) + '%');
  254. });
  255. }
  256. }
  257. // 全部下载完成!
  258. resolve && resolve('100%');
  259. })();
  260. this.saveContentToLocal(toolName, 'index.html', html);
  261. }).catch(e => {
  262. this.delToolConfigs(toolName);
  263. fnProgress && fnProgress(`糟糕,下载出错,工具远程安装失败!${e.toString()}`);
  264. });
  265. });
  266. },
  267. loadDemo() {
  268. let demoName = this.demo.name;
  269. let files = this.demo.files;
  270. let site = '.';
  271. if (window.chrome && chrome.runtime && chrome.runtime.getURL) {
  272. site = chrome.runtime.getURL('devtools');
  273. }
  274. let arrPromise = files.map(file => fetch(`${site}/${demoName}/${file}`).then(resp => resp.text()));
  275. return Promise.all(arrPromise).then(contents => {
  276. // fh-config.js
  277. let json = evalCore.getEvalInstance(window)(contents[0]);
  278. this.addToolConfigs(json);
  279. // index.html
  280. let result = this.htmlTplEncode(demoName, contents[1]);
  281. this.saveContentToLocal(demoName, files[1], result.html, true);
  282. // 其他文件
  283. for (let i = 2; i < contents.length; i++) {
  284. this.saveContentToLocal(demoName, files[i], contents[i]);
  285. }
  286. this.toast('你的Hello World已安装成功!');
  287. });
  288. },
  289. downloadTool(tool) {
  290. let toolName = tool || this.demo.name;
  291. this.getToolFilesFromLocal(toolName).then(files => {
  292. let arrPromise = files.map(file => this.getContentFromLocal(toolName, file));
  293. Promise.all(arrPromise).then(contents => {
  294. let zipper = new JSZip();
  295. let zipPkg = zipper.folder(toolName);
  296. files.forEach((file, index) => zipPkg.file(file, contents[index]));
  297. zipper.generateAsync({type: "blob"})
  298. .then(function (content) {
  299. let elA = document.createElement('a');
  300. elA.style.cssText = 'position:absolute;top:-1000px;left:-10000px;';
  301. elA.setAttribute('download', `${toolName}.zip`);
  302. elA.href = URL.createObjectURL(new Blob([content], {type: 'application/octet-stream'}));
  303. document.body.appendChild(elA);
  304. elA.click();
  305. });
  306. });
  307. });
  308. },
  309. upgrade(tool, urlMode) {
  310. if (tool === this.demo.name) {
  311. this.loadDemo();
  312. } else if (urlMode) {
  313. // 远程下载并安装工具
  314. this.loadRemoteTool(tool, this.myTools[tool].updateUrl, this.toast).then(progress => {
  315. this.toast(progress);
  316. this.toast('工具更新完成!');
  317. });
  318. } else {
  319. this.loadTool(true, tool);
  320. }
  321. },
  322. loadTool(upgradeMode, upgradeToolName) {
  323. let Model = (function () {
  324. zip.useWebWorkers = false;
  325. return {
  326. getEntries: function (file, onend) {
  327. zip.createReader(new zip.BlobReader(file), function (zipReader) {
  328. zipReader.getEntries(onend);
  329. }, function (e) {
  330. console.log(e);
  331. });
  332. },
  333. getEntryFile: function (entry, onend, onprogress) {
  334. entry.getData(new zip.TextWriter(), function (text) {
  335. onend(text);
  336. }, onprogress);
  337. }
  338. };
  339. })();
  340. let fileInput = document.createElement('input');
  341. fileInput.type = 'file';
  342. fileInput.accept = 'application/zip';
  343. fileInput.style.cssText = 'position:absolute;top:-100px;left:-100px';
  344. fileInput.addEventListener('change', (evt) => {
  345. let toolName = fileInput.files[0].name.replace('.zip', '');
  346. if (upgradeMode && upgradeToolName !== toolName) {
  347. return this.toast(`请确保上传${upgradeToolName}.zip进行更新!`);
  348. }
  349. Model.getEntries(fileInput.files[0], (entries) => {
  350. entries = entries.filter(entry => !entry.directory && /\.(html|js|css)$/.test(entry.filename));
  351. let reg = /(fh-config\.js|index\.html|content-script\.(js|css))$/;
  352. let entPart1 = entries.filter(en => reg.test(en.filename));
  353. let entPart2 = entries.filter(en => !reg.test(en.filename));
  354. entPart1.forEach((entry) => {
  355. Model.getEntryFile(entry, (fileContent) => {
  356. let fileName = entry.filename.split('/').pop();
  357. try {
  358. if (fileName === `fh-config.js`) {
  359. let json = JSON.parse(fileContent);
  360. this.addToolConfigs(json);
  361. } else if (fileName === 'index.html') {
  362. let result = this.htmlTplEncode(toolName, fileContent);
  363. this.saveContentToLocal(toolName, fileName, result.html, true);
  364. // 所有被引用的静态文件都在这里进行遍历
  365. entPart2.forEach(jcEntry => {
  366. Model.getEntryFile(jcEntry, jcContent => {
  367. Object.keys(result.jsCss).forEach(tp => {
  368. result.jsCss[tp].some(file => {
  369. if (file[0].indexOf(jcEntry.filename) > -1) {
  370. this.saveContentToLocal(toolName, file[0].replace(`../${toolName}/`, ''), jcContent);
  371. return true;
  372. }
  373. });
  374. });
  375. });
  376. });
  377. } else if (['content-script.js', 'content-script.css'].includes(fileName)) {
  378. this.saveContentToLocal(toolName, fileName, fileContent);
  379. }
  380. } catch (err) {
  381. this.toast(`${fileName} 文件发生错误:${err.message}`);
  382. }
  383. });
  384. });
  385. this.toast('工具更新成功!');
  386. });
  387. }, false);
  388. document.body.appendChild(fileInput);
  389. fileInput.click();
  390. window.setTimeout(() => fileInput.remove(), 3000);
  391. },
  392. getToolConfigs() {
  393. return Awesome.StorageMgr.get(DEV_TOOLS_MY_TOOLS).then(data => {
  394. this.myTools = JSON.parse(data || localStorage.getItem(DEV_TOOLS_MY_TOOLS) || '{}');
  395. Object.keys(this.myTools).forEach(t => {
  396. if(this.myTools[t].menuConfig) {
  397. this.myTools.icon = this.myTools[t].menuConfig[0].icon;
  398. delete this.myTools[t].menuConfig;
  399. }
  400. if(this.myTools[t].contentScript) {
  401. this.myTools[t].contentScriptJs = this.myTools[t].contentScript;
  402. delete this.myTools[t].contentScript;
  403. }
  404. if(!this.myTools[t].icon) {
  405. this.myTools[t].icon = '◆';
  406. }
  407. });
  408. });
  409. },
  410. setToolConfigs() {
  411. Awesome.StorageMgr.set(DEV_TOOLS_MY_TOOLS,JSON.stringify(this.myTools));
  412. },
  413. addToolConfigs(configs) {
  414. this.getToolConfigs().then(() => {
  415. Object.keys(configs).forEach(key => {
  416. let config = configs[key];
  417. this.myTools[key] = {
  418. _devTool: true,
  419. _enable: this.myTools[key] && this.myTools[key]._enable,
  420. name: config.name,
  421. tips: config.tips,
  422. icon: config.icon,
  423. noPage: !!config.noPage,
  424. contentScriptJs: !!config.contentScriptJs || !!config.contentScript,
  425. contentScriptCss: !!config.contentScriptCss,
  426. updateUrl: config.updateUrl || null
  427. }
  428. });
  429. this.setToolConfigs();
  430. });
  431. },
  432. delToolConfigs(tools) {
  433. // 先删除文件
  434. [].concat(tools).forEach(tool => {
  435. this.getToolFilesFromLocal(tool).then(files => {
  436. Awesome.StorageMgr.remove(files.map(file =>
  437. file.startsWith(`../${tool}`) ? file : `../${tool}/${file}` ));
  438. });
  439. // 删模板等
  440. let removeItems = [
  441. TOOL_NAME_TPL.replace('#TOOL-NAME#', tool),
  442. TOOL_CONTENT_SCRIPT_TPL.replace('#TOOL-NAME#', tool),
  443. TOOL_CONTENT_SCRIPT_CSS_TPL.replace('#TOOL-NAME#', tool)
  444. ];
  445. Awesome.StorageMgr.remove(removeItems);
  446. });
  447. // 再删配置
  448. [].concat(tools).forEach(tool => {
  449. delete this.myTools[tool];
  450. });
  451. this.setToolConfigs();
  452. this.$forceUpdate();
  453. this.toast('工具删除成功!');
  454. },
  455. toggleToolEnableStatus(tool) {
  456. this.myTools[tool]._enable = !this.myTools[tool]._enable;
  457. this.setToolConfigs();
  458. this.$forceUpdate();
  459. },
  460. getToolFilesFromLocal(toolName) {
  461. return new Promise(resolve => {
  462. let files = ['fh-config.js', 'index.html'];
  463. let toolObj = this.myTools[toolName];
  464. toolObj.contentScriptJs && files.push('content-script.js');
  465. toolObj.contentScriptCss && files.push('content-script.css');
  466. chrome.storage.local.get(null, allDatas => {
  467. let fs = Object.keys(allDatas).filter(key => String(key).startsWith(`../${toolName}/`));
  468. files = files.concat(fs);
  469. resolve(files.map(f => f.replace(`../${toolName}/`, '')));
  470. });
  471. });
  472. },
  473. saveContentToLocal(toolName, fileName, content, htmlDone) {
  474. if (fileName === 'fh-config.js') {
  475. try {
  476. let json = JSON.parse(content);
  477. this.addToolConfigs(json);
  478. this.$forceUpdate();
  479. return json[toolName];
  480. } catch (e) {
  481. return null;
  482. }
  483. }
  484. let key = '';
  485. switch (fileName) {
  486. case 'index.html':
  487. key = TOOL_NAME_TPL.replace('#TOOL-NAME#', toolName);
  488. if (!htmlDone) {
  489. let result = this.htmlTplEncode(toolName, content);
  490. content = result.html;
  491. }
  492. break;
  493. case 'content-script.js':
  494. key = TOOL_CONTENT_SCRIPT_TPL.replace('#TOOL-NAME#', toolName);
  495. break;
  496. case 'content-script.css':
  497. key = TOOL_CONTENT_SCRIPT_CSS_TPL.replace('#TOOL-NAME#', toolName);
  498. break;
  499. default:
  500. key = fileName.startsWith(`../${toolName}/`) ? fileName : `../${toolName}/${fileName}`;
  501. }
  502. return Awesome.StorageMgr.set(key,content);
  503. },
  504. getContentFromLocal(toolName, fileName) {
  505. return new Promise((resolve, reject) => {
  506. if (fileName === 'fh-config.js') {
  507. let counter = 0;
  508. let config = {};
  509. config[toolName] = this.myTools[toolName];
  510. ['_devTool', '_enable'].forEach(k => delete config[toolName][k]);
  511. delete config[toolName].menuConfig;
  512. let jsonText = JSON.stringify(config,null,4);
  513. resolve(jsonText);
  514. } else {
  515. let key = '';
  516. switch (fileName) {
  517. case 'index.html':
  518. key = TOOL_NAME_TPL.replace('#TOOL-NAME#', toolName);
  519. break;
  520. case 'content-script.js':
  521. key = TOOL_CONTENT_SCRIPT_TPL.replace('#TOOL-NAME#', toolName);
  522. break;
  523. case 'content-script.css':
  524. key = TOOL_CONTENT_SCRIPT_CSS_TPL.replace('#TOOL-NAME#', toolName);
  525. break;
  526. default:
  527. key = fileName.startsWith(`../${toolName}/`) ? fileName : `../${toolName}/${fileName}`;
  528. }
  529. // 获取到的数据需要做二次加工
  530. let _update = (content) => {
  531. content = content || '';
  532. if (fileName === 'index.html') {
  533. content = this.htmlTplDecode(toolName, content);
  534. }
  535. // 如果noPage为true,但content-script.js中还没有window.xxxNoPage定义的话,就自动加一个
  536. else if (fileName === 'content-script.js' && this.myTools[toolName].noPage) {
  537. if (content.indexOf(`window.${toolName.replace(/[\-_]/g, '')}NoPage`) === -1) {
  538. content += '\n\n' + FileTpl['noPage.js'].replace(/#toolName#/gm, toolName)
  539. .replace(/#toolNameLower#/gm, toolName.replace(/[\-_]/g, ''));
  540. this.saveContentToLocal(toolName, fileName, content);
  541. }
  542. }
  543. return content
  544. };
  545. Awesome.StorageMgr.get(key).then(data => {
  546. resolve(_update(data));
  547. });
  548. }
  549. });
  550. },
  551. htmlTplEncode(toolName, html, updateUrl) {
  552. let jsReg = /<script[^>]*src=['"]([^'"]+)['"][^>]*>\s*?<\/[^>]*script>/igm;
  553. let csReg = /<link\s+[^>]*[^>]*href=['"]([^'"]+)['"][^>]*[^>]*>/igm;
  554. let scriptBlockReg = /<script(?:[^>]+|(?!src=))*>([^<]+|<(?!\/script>))+<\/script>/gim;
  555. let files = {};
  556. [csReg, jsReg].forEach(reg => {
  557. html = html.replace(reg, (tag, src) => {
  558. let tagName = /<script/.test(tag) ? 'js' : 'css';
  559. if (tagName === 'css' && !/stylesheet/i.test(tag)) {
  560. return tag;
  561. }
  562. // 这里必须保留带有md5戳的原地址,要不然会有cdn缓存,更新会失败
  563. let originSrc = src;
  564. // 这个src是携带了Query的,用于直接存储到html中
  565. let withQuerySrc = src;
  566. // src 去query处理,用于Storage存储,避免过多冗余key出现
  567. if (src.indexOf('?') !== -1) {
  568. let x = src.split('?');
  569. x.pop();
  570. src = x.join('');
  571. }
  572. if (!/^\./.test(src)) {
  573. src = `../${toolName}/${src}`;
  574. withQuerySrc = `../${toolName}/${withQuerySrc}`;
  575. }
  576. // 存储静态文件的内容
  577. let filePath = originSrc;
  578. if (!/^(http(s)?:)?\/\//.test(originSrc) && updateUrl) {
  579. filePath = new URL(originSrc, updateUrl).href;
  580. }
  581. files[tagName] = files[tagName] || [];
  582. files[tagName].push([src, withQuerySrc, filePath]);
  583. return '';
  584. });
  585. });
  586. // 识别所有无src属性的script标签
  587. let blockCodes = [];
  588. html = html.replace(scriptBlockReg, (tag, codes) => {
  589. codes = codes.trim();
  590. codes.length && blockCodes.push(codes);
  591. return '';
  592. });
  593. if (blockCodes.length) {
  594. let blockName = `../${toolName}/fh-script-block.js`;
  595. files.js = files.js || [];
  596. files.js.push([blockName, blockName, blockCodes.join(';\n\n')]);
  597. }
  598. // 如果是updateURL模式,需要替换所有相对链接为绝对链接,包括:img、a
  599. if (updateUrl) {
  600. let relativePathRegexp = /^(http:\/\/|https:\/\/|\/\/)[^\s'"]+/igm;
  601. let imgReg = /<img[^>]*src=['"]([^'"]+)['"][^>]*>(\s*?<\/[^>]*img>)?/igm;
  602. let aReg = /<a[^>]*href=['"]([^'"]+)['"][^>]*>(\s*?<\/[^>]*a>)?/igm;
  603. html = html.replace(imgReg, (tag, link) => {
  604. if (!relativePathRegexp.test(link)) {
  605. tag = tag.replace(/src=['"]([^'"]+)['"]/igm, () => ` src="${new URL(link, updateUrl).href}"`);
  606. }
  607. return tag;
  608. }).replace(aReg, (tag, link) => {
  609. if (!relativePathRegexp.test(link)) {
  610. tag = tag.replace(/href=['"]([^'"]+)['"]/igm, () => ` href="${new URL(link, updateUrl).href}"`);
  611. }
  612. return tag;
  613. });
  614. }
  615. html = html.replace('static/img/favicon.ico', 'static/img/fe-16.png') // 替换favicon
  616. .replace(/<\/body>/, () => { // 将静态文件添加到页面最底部
  617. return Object.keys(files).map(t => {
  618. return `<dynamic data-type="${t}" data-source="${files[t].map(f => f[1]).join(',')}"></dynamic>`;
  619. }).join('') + '</body>';
  620. });
  621. return {
  622. html: html,
  623. jsCss: files
  624. };
  625. },
  626. htmlTplDecode(toolName, html) {
  627. let reg = /<dynamic\s+data\-type="(js|css)"\s+data\-source=['"]([^'"]+)['"][^>]*>\s*?<\/[^>]*dynamic>/igm;
  628. return html.replace(reg, (frag, tag, list) => {
  629. list = list.split(',');
  630. if (tag === 'js') {
  631. return list.map(src => `<script src="${src.replace(`../${toolName}/`, '')}"></script>`).join('');
  632. } else {
  633. return list.map(href => `<link rel="stylesheet" type="text/css" href="${href.replace(`../${toolName}/`, '')}" />`).join('');
  634. }
  635. });
  636. },
  637. givenIcons(forceClose) {
  638. if (!this.givenIconList.length) {
  639. this.givenIconList = FileTpl['given-icons'].replace(/\s/gm, '').split('');
  640. }
  641. if (forceClose) {
  642. this.showGivenIcons = false;
  643. } else {
  644. this.showGivenIcons = !this.showGivenIcons;
  645. }
  646. this.$forceUpdate();
  647. },
  648. selectIcon(icon) {
  649. if (this.showNewToolForm) {
  650. this.$refs.toolIcon.value = icon;
  651. this.givenIcons(true);
  652. } else {
  653. this.copyToClipboard(icon);
  654. this.toast(`图标 ${icon} 复制成功,随处粘贴可用!`);
  655. }
  656. },
  657. toast(content) {
  658. window.clearTimeout(window.feHelperAlertMsgTid);
  659. let elAlertMsg = document.querySelector("#fehelper_alertmsg");
  660. if (!elAlertMsg) {
  661. let elWrapper = document.createElement('div');
  662. elWrapper.innerHTML = '<div id="fehelper_alertmsg">' + content + '</div>';
  663. elAlertMsg = elWrapper.childNodes[0];
  664. document.body.appendChild(elAlertMsg);
  665. } else {
  666. elAlertMsg.innerHTML = content;
  667. elAlertMsg.style.display = 'block';
  668. }
  669. window.feHelperAlertMsgTid = window.setTimeout(function () {
  670. elAlertMsg.style.display = 'none';
  671. }, 3000);
  672. },
  673. copyToClipboard(text) {
  674. let input = document.createElement('textarea');
  675. input.style.position = 'fixed';
  676. input.style.opacity = 0;
  677. input.value = text;
  678. document.body.appendChild(input);
  679. input.select();
  680. document.execCommand('Copy');
  681. document.body.removeChild(input);
  682. },
  683. fhDeveloperDoc() {
  684. window.open(`https://github.com/zxlie/FeHelper/blob/master/README_NEW.md#%E5%85%ADopen-api`);
  685. }
  686. }
  687. });