Explorar el Código

第一个版本提交

zxlie hace 6 meses
commit
a3c0305959
Se han modificado 100 ficheros con 25542 adiciones y 0 borrados
  1. 32 0
      .cursorrules
  2. 10 0
      .gitignore
  3. 76 0
      CODE_OF_CONDUCT.md
  4. 21 0
      LICENSE
  5. 136 0
      README.md
  6. 505 0
      README_NEW.md
  7. 16 0
      README_TEST.md
  8. 63 0
      apps/aiagent/fh.ai.js
  9. 208 0
      apps/aiagent/index.css
  10. 78 0
      apps/aiagent/index.html
  11. 125 0
      apps/aiagent/index.js
  12. 314 0
      apps/background/awesome.js
  13. 502 0
      apps/background/background.js
  14. 147 0
      apps/background/crx-download.js
  15. 74 0
      apps/background/inject-tools.js
  16. 236 0
      apps/background/menu.js
  17. 100 0
      apps/background/monkey.js
  18. 253 0
      apps/background/tools.js
  19. 1130 0
      apps/chart-maker/chart-generator.js
  20. 11 0
      apps/chart-maker/chart-icon.svg
  21. 317 0
      apps/chart-maker/index.html
  22. 6 0
      apps/chart-maker/lib/chart.min.js
  23. 6 0
      apps/chart-maker/lib/chartjs-plugin-datalabels.min.js
  24. 6 0
      apps/chart-maker/lib/chartjs-plugin-zoom.min.js
  25. 5 0
      apps/chart-maker/lib/hammer.min.js
  26. 19 0
      apps/chart-maker/lib/html2canvas.min.js
  27. 1 0
      apps/chart-maker/lib/xlsx.full.min.js
  28. 712 0
      apps/chart-maker/main.js
  29. 646 0
      apps/chart-maker/style.css
  30. 270 0
      apps/code-beautify/beautify-css.js
  31. 616 0
      apps/code-beautify/beautify-html.js
  32. 358 0
      apps/code-beautify/beautify-vk.js
  33. 2108 0
      apps/code-beautify/beautify.js
  34. 161 0
      apps/code-beautify/content-script.css
  35. 201 0
      apps/code-beautify/content-script.js
  36. 9 0
      apps/code-beautify/index.css
  37. 63 0
      apps/code-beautify/index.html
  38. 136 0
      apps/code-beautify/index.js
  39. 6 0
      apps/code-compress/htmlminifier.min.js
  40. 44 0
      apps/code-compress/index.css
  41. 79 0
      apps/code-compress/index.html
  42. 160 0
      apps/code-compress/index.js
  43. 158 0
      apps/code-compress/uglifyjs3.js
  44. 542 0
      apps/color-picker/content-script.js
  45. 236 0
      apps/crontab/index.css
  46. 28 0
      apps/crontab/index.html
  47. 1674 0
      apps/crontab/index.js
  48. 58 0
      apps/devtools/file-tpl.js
  49. 3 0
      apps/devtools/hello-world/content-script.css
  50. 29 0
      apps/devtools/hello-world/content-script.js
  51. 11 0
      apps/devtools/hello-world/fh-config.js
  52. 4 0
      apps/devtools/hello-world/index.css
  53. 14 0
      apps/devtools/hello-world/index.html
  54. 1 0
      apps/devtools/hello-world/index.js
  55. 280 0
      apps/devtools/index.css
  56. 163 0
      apps/devtools/index.html
  57. 782 0
      apps/devtools/index.js
  58. 14 0
      apps/dynamic/index.html
  59. 82 0
      apps/dynamic/index.js
  60. 548 0
      apps/en-decode/endecode-lib.js
  61. 30 0
      apps/en-decode/he.js
  62. 55 0
      apps/en-decode/index.css
  63. 138 0
      apps/en-decode/index.html
  64. 124 0
      apps/en-decode/index.js
  65. 274 0
      apps/en-decode/md5.js
  66. 286 0
      apps/excel2json/CSVParser.js
  67. 538 0
      apps/excel2json/DataGridRenderer.js
  68. 190 0
      apps/excel2json/converter.js
  69. 143 0
      apps/excel2json/index.css
  70. 79 0
      apps/excel2json/index.html
  71. 104 0
      apps/excel2json/index.js
  72. 97 0
      apps/grid-ruler/content-script.css
  73. 176 0
      apps/grid-ruler/content-script.js
  74. 11 0
      apps/grid-ruler/index.html
  75. 201 0
      apps/html2markdown/demo-tpl.js
  76. 77 0
      apps/html2markdown/editor.css
  77. 187 0
      apps/html2markdown/index.css
  78. 75 0
      apps/html2markdown/index.html
  79. 256 0
      apps/html2markdown/index.js
  80. 1072 0
      apps/html2markdown/libs/marked.js
  81. 1671 0
      apps/html2markdown/libs/rawdeflate.js
  82. 753 0
      apps/html2markdown/libs/rawinflate.js
  83. 34 0
      apps/image-base64/index.css
  84. 85 0
      apps/image-base64/index.html
  85. 216 0
      apps/image-base64/index.js
  86. 53 0
      apps/json-diff/index.css
  87. 47 0
      apps/json-diff/index.html
  88. 42 0
      apps/json-diff/index.js
  89. 876 0
      apps/json-format/content-script.css
  90. 679 0
      apps/json-format/content-script.js
  91. 883 0
      apps/json-format/format-lib.js
  92. 179 0
      apps/json-format/index.css
  93. 85 0
      apps/json-format/index.html
  94. 297 0
      apps/json-format/index.js
  95. 74 0
      apps/json-format/json-abc.js
  96. 0 0
      apps/json-format/json-bigint.js
  97. 103 0
      apps/json-format/json-decode.js
  98. BIN
      apps/json-format/json-demo.jpg
  99. 662 0
      apps/json-format/json-lint.js
  100. 97 0
      apps/loan-rate/index.css

+ 32 - 0
.cursorrules

@@ -0,0 +1,32 @@
+# 角色
+你是一个Chrome浏览器扩展开发专家,对Chrome Extension Manifest V3非常熟悉。你需要帮助我开发和维护一个名为FeHelper的Chrome扩展。
+
+# 项目结构规范
+- apps/目录是项目的主目录
+- 每个功能模块都是apps/下的独立目录
+- manifest.json 是扩展的配置文件
+- background/ 目录包含后台服务脚本
+- popup/ 目录包含扩展的弹出窗口页面
+- options/ 目录包含扩展的配置页面
+- static/ 目录包含静态资源
+
+# 编码规范
+- 使用ES6+语法
+- 模块化开发,每个功能保持独立
+- 遵循Chrome Extension V3的最佳实践
+- 代码需要清晰的注释和文档
+- 保持一致的代码风格和缩进
+
+# 功能模块开发规范
+- 每个新功能模块需要在apps/下创建独立目录
+- 模块目录需包含完整的HTML、JS、CSS文件
+- 新增模块需要在manifest.json中正确配置
+- 需要在web_accessible_resources中声明可访问的资源
+- 遵循Chrome Extension的安全策略和最佳实践
+
+# 注意事项
+- 权限申请需要最小化原则
+- 需要考虑跨域访问的限制
+- 注意性能优化和资源占用
+- 保持代码的可维护性和可扩展性
+- 遵循Chrome商店的发布规范

+ 10 - 0
.gitignore

@@ -0,0 +1,10 @@
+# 工程文件
+.idea
+.DS_Store
+.todolist
+node_modules/
+package-lock.json
+Users/
+output/apps/
+output-firefox/apps/
+output-edge/apps/

+ 76 - 0
CODE_OF_CONDUCT.md

@@ -0,0 +1,76 @@
+# Contributor Covenant Code of Conduct
+
+## Our Pledge
+
+In the interest of fostering an open and welcoming environment, we as
+contributors and maintainers pledge to making participation in our project and
+our community a harassment-free experience for everyone, regardless of age, body
+size, disability, ethnicity, sex characteristics, gender identity and expression,
+level of experience, education, socio-economic status, nationality, personal
+appearance, race, religion, or sexual identity and orientation.
+
+## Our Standards
+
+Examples of behavior that contributes to creating a positive environment
+include:
+
+* Using welcoming and inclusive language
+* Being respectful of differing viewpoints and experiences
+* Gracefully accepting constructive criticism
+* Focusing on what is best for the community
+* Showing empathy towards other community members
+
+Examples of unacceptable behavior by participants include:
+
+* The use of sexualized language or imagery and unwelcome sexual attention or
+ advances
+* Trolling, insulting/derogatory comments, and personal or political attacks
+* Public or private harassment
+* Publishing others' private information, such as a physical or electronic
+ address, without explicit permission
+* Other conduct which could reasonably be considered inappropriate in a
+ professional setting
+
+## Our Responsibilities
+
+Project maintainers are responsible for clarifying the standards of acceptable
+behavior and are expected to take appropriate and fair corrective action in
+response to any instances of unacceptable behavior.
+
+Project maintainers have the right and responsibility to remove, edit, or
+reject comments, commits, code, wiki edits, issues, and other contributions
+that are not aligned to this Code of Conduct, or to ban temporarily or
+permanently any contributor for other behaviors that they deem inappropriate,
+threatening, offensive, or harmful.
+
+## Scope
+
+This Code of Conduct applies both within project spaces and in public spaces
+when an individual is representing the project or its community. Examples of
+representing a project or community include using an official project e-mail
+address, posting via an official social media account, or acting as an appointed
+representative at an online or offline event. Representation of a project may be
+further defined and clarified by project maintainers.
+
+## Enforcement
+
+Instances of abusive, harassing, or otherwise unacceptable behavior may be
+reported by contacting the project team at [email protected]. All
+complaints will be reviewed and investigated and will result in a response that
+is deemed necessary and appropriate to the circumstances. The project team is
+obligated to maintain confidentiality with regard to the reporter of an incident.
+Further details of specific enforcement policies may be posted separately.
+
+Project maintainers who do not follow or enforce the Code of Conduct in good
+faith may face temporary or permanent repercussions as determined by other
+members of the project's leadership.
+
+## Attribution
+
+This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
+available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
+
+[homepage]: https://www.contributor-covenant.org
+
+For answers to common questions about this code of conduct, see
+https://www.contributor-covenant.org/faq

+ 21 - 0
LICENSE

@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2019 zhaoxianlie([email protected]
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.

+ 136 - 0
README.md

@@ -0,0 +1,136 @@
+# FeHelper For Chrome, Firefox & MS-Edge
+
+FeHelper是一个功能强大的浏览器扩展,专为前端开发者设计,集成了多种实用工具,帮助开发者提高工作效率。无论是JSON数据处理、二维码生成与解码,还是代码美化与压缩,FeHelper都能为您提供便捷的解决方案。
+
+![FeHelper](https://user-images.githubusercontent.com/865735/75407628-7399c580-594e-11ea-8ef2-00adf39d61a8.jpg)
+
+[![Google Chrome](https://img.shields.io/chrome-web-store/v/pkgccpejnmalmdinmhkkfafefagiiiad.svg?logo=Google%20Chrome&logoColor=red&color=blue)](https://chrome.google.com/webstore/detail/pkgccpejnmalmdinmhkkfafefagiiiad)
+[![Google Chrome](https://img.shields.io/chrome-web-store/stars/pkgccpejnmalmdinmhkkfafefagiiiad.svg?logo=Google%20Chrome&logoColor=red&color=blue)](https://chrome.google.com/webstore/detail/pkgccpejnmalmdinmhkkfafefagiiiad)
+[![Google Chrome](https://img.shields.io/chrome-web-store/users/pkgccpejnmalmdinmhkkfafefagiiiad.svg?logo=Google%20Chrome&logoColor=red&color=blue)](https://chrome.google.com/webstore/detail/pkgccpejnmalmdinmhkkfafefagiiiad)
+
+## 一、功能展示
+
+![Web前端助手-FeHelper](https://user-images.githubusercontent.com/865735/75407048-020d4780-594d-11ea-9dd6-48f6d5774c2f.png)
+
+## 二、贡献指南
+
+我们欢迎社区的贡献!如果您有兴趣参与FeHelper的开发,请按照以下步骤进行:
+
+1. Fork本仓库并克隆到本地。
+2. 创建一个新的分支:`git checkout -b feature/YourFeature`
+3. 提交您的更改:`git commit -m 'Add some feature'`
+4. 推送到分支:`git push origin feature/YourFeature`
+5. 创建一个Pull Request。
+
+## 三、官网地址
+https://www.baidufe.com/fehelper/index/index.html
+
+### 1、扩展示例
+![Web前端助手-FeHelper](https://user-images.githubusercontent.com/865735/75407048-020d4780-594d-11ea-9dd6-48f6d5774c2f.png)
+
+### 2、关于新版
+- 新版本的FeHelper,是以开放平台为思路的设计,详细了解FeHelper新版,可以参考链接:
+    - [FeHelper-开放平台版介绍文档](/README_NEW.md)
+    - [开放平台思路下的FeHelper-阿烈叔的博客](https://www.baidufe.com/item/5b1e07d6f5106b6907bc.html)
+- 新版本FeHelper在一个新的`私有仓储`,待打磨得足够细腻了,再考虑开源    
+
+![FeHelper新版](https://user-images.githubusercontent.com/865735/75334978-b5315e80-58c3-11ea-9af0-e593149b0f7c.png)
+
+## 四、功能详细介绍
+
+FeHelper是一个功能强大的开发者工具,专为前端开发者设计,集成了多种实用工具,帮助开发者提高工作效率。以下是FeHelper的主要功能:
+
+- **JSON工具**: 
+  - **JSON美化工具**:自动检测并格式化JSON数据,支持手动格式化、乱码解码、排序、BigInt处理、编辑、下载和皮肤定制等功能,极大地方便了开发者对JSON数据的处理。
+  - **JSON比对工具**:支持两个JSON内容的自动键值比较,并高亮显示差异点,帮助开发者快速识别数据变化。
+
+- **二维码工具**: 
+  - **二维码生成器**:支持自定义颜色和icon的二维码生成,适用于多种应用场景。
+  - **二维码解码器**:支持多种模式的二维码解码,包括截图后粘贴解码,方便快捷。
+
+- **编码工具**: 
+  - **字符串编解码**:支持多格式的信息编解码,如Unicode、UTF-8、URL、Base64、MD5等,满足不同编码需求。
+  - **代码美化工具**:支持多语言的代码美化,包括Javascript、CSS、HTML、XML、SQL,帮助开发者保持代码整洁。
+  - **代码压缩工具**:提供简单的代码压缩功能,支持HTML、Javascript、CSS代码压缩,优化网页加载速度。
+
+- **开发者工具**: 
+  - **AI助手**:由AI强力支撑的超智能对话工具,支持代码编写、改代码、做方案设计等,提升开发效率。
+  - **简易Postman**:接口调试工具,支持GET/POST/HEAD请求方式,自动格式化JSON内容,简化API测试流程。
+  - **Websocket工具**:支持对Websocket接口的抓包测试,包括ws服务的连接测试、消息发送测试、结果分析等。
+
+- **其他工具**: 
+  - **时间(戳)转换**:本地化时间与时间戳之间的相互转换,支持秒/毫秒、世界时区切换、各时区时钟展示等。
+  - **随机密码生成**:将各种字符进行随机组合生成密码,支持指定长度和字符类型,确保密码安全性。
+  - **我的便签笔记**:便签笔记工具,支持创建目录对笔记进行分类管理,笔记支持一键导出/导入,方便信息管理。
+  - **Markdown转换**:Markdown编写/预览工具,支持HTML片段直接转Markdown,支持将内容以PDF格式进行下载。
+  - **网页截屏工具**:可对任意网页进行截屏,支持可视区域截屏、全网页滚动截屏,最终结果可预览后再保存。
+  - **页面取色工具**:可直接在网页上针对任意元素进行色值采集,将光标移动到需要取色的位置,单击确定即可。
+
+FeHelper不仅功能强大,而且易于使用,是前端开发者的得力助手。更多工具和详细介绍请访问[FeHelper官网](https://www.baidufe.com/fehelper/index/index.html)。
+
+## 五、扩展安装地址
+
+### 1、Chrome web store地址(推荐)
+https://chrome.google.com/webstore/detail/pkgccpejnmalmdinmhkkfafefagiiiad?hl=zh-cn
+
+- 官网安装,你可能需要额外的梯子
+    - [谷歌访问助手(官方正式版)](http://www.ggfwzs.com/)
+    - [谷歌访问助手(发烧友破解版)](https://github.com/haotian-wang/google-access-helper)
+
+### 2、Microsoft Edge Addons
+https://microsoftedge.microsoft.com/addons/detail/feolnkbgcbjmamimpfcnklggdcbgakhe?hl=zh-CN
+
+- 如果你在使用Microsoft Edge浏览器的话,`可以直接安装`,不用翻墙
+
+### 3、Firefox Add-ons
+- FeHelper官网一键安装: https://www.baidufe.com/fehelper/index/index.html
+- GitHub下载xpi安装:[点击进入下载页](/apps/static/screenshot/xpi)
+
+### 4、crx文件下载=>本地安装(Chrome)
+- 官网下载:https://www.baidufe.com/fehelper
+- 翻墙下载:https://chrome-extension-downloader.com/?extension=pkgccpejnmalmdinmhkkfafefagiiiad
+- 本站下载:[点击进入下载页](/apps/static/screenshot/crx)
+
+## 六、使用方法
+
+使用FeHelper时,请遵循以下最佳实践,以确保安全和高效:
+
+1. **从可信来源安装**:
+   - 确保只从官方浏览器商店(如Chrome Web Store)安装FeHelper,以避免潜在的安全风险。
+
+2. **定期检查和更新**:
+   - 定期检查FeHelper的更新,以确保使用最新版本,获得最新的功能和安全修复。
+
+3. **管理权限**:
+   - 在安装FeHelper时,仔细查看所请求的权限,确保它们与扩展的功能相符。
+
+4. **安全使用**:
+   - 只安装必要的扩展,避免过多的扩展增加攻击面。
+
+5. **用户反馈和支持**:
+   - 如果在使用FeHelper时遇到问题或有建议,及时通过提供的反馈渠道联系开发者。
+
+通过遵循这些指南,您可以更安全、更高效地使用FeHelper,充分发挥其强大的功能。
+
+## 七、用户反馈渠道
+- 在线反馈:https://www.baidufe.com/fehelper/feedback.html
+- Mail反馈:[email protected]
+- Wechat反馈:398824681 <br>
+![阿烈叔的个人微信](https://user-images.githubusercontent.com/865735/75407547-3cc3af80-594e-11ea-9abf-6168b94547a1.png)
+
+## 八、使用推荐与案例
+
+FeHelper在开发者社区中广受好评,以下是一些使用推荐和案例:
+
+1. **接口调试工具**: FeHelper被誉为强大的接口调试工具,支持模拟各种HTTP请求,保存cookie等信息,详情请参阅[CSDN博客](https://blog.csdn.net/fuhanghang/article/details/84592480)。
+
+2. **功能丰富的插件**: FeHelper支持多种浏览器,提供JSON格式化、代码美化与压缩等功能,更多信息请访问[CSDN博客](https://blog.csdn.net/weixin_42272869/article/details/124412501)。
+
+3. **前端开发测试利器**: FeHelper包含字符串编解码、代码美化、JSON格式化查看等前端实用工具,详见[博客园](https://www.cnblogs.com/oycyqr/p/8867362.html)。
+
+4. **网页源码压缩**: FeHelper在网页html源码压缩、css或javascript的压缩方面表现出色,更多内容在[阿里云开发者社区](https://developer.aliyun.com/article/710232)。
+
+5. **前端必备插件**: FeHelper被推荐为前端必备的Chrome插件,详情请查看[稀土掘金](https://juejin.cn/post/6854573211590836231)。
+
+## 九、一些样例
+- [点击进入查看>>](/apps/static/screenshot/crx)

+ 505 - 0
README_NEW.md

@@ -0,0 +1,505 @@
+# FeHelper(开放平台版)
+
+![FeHelper](https://user-images.githubusercontent.com/865735/75407628-7399c580-594e-11ea-8ef2-00adf39d61a8.jpg)
+
+## 一、序言
+鉴于Google Chrome官方强制要求的`插件单一用原则`,老版本(V2019.12)接到chrome webstore的整改通知;为保证用户能继续正常使用FeHelper,并且以后也尽可能少的对FeHelper进行提审,索性启动FeHelper的一次大升级,`支持所有工具热更新`。
+先了解一下啥是Google的单一用途原则,直接看Google官方的说明吧:https://developer.chrome.com/extensions/single_purpose
+
+## 二、新版思路
+- 原来的功能:可参加老版本Readme中的介绍 https://github.com/zxlie/FeHelper
+- 新版本功能:
+    - 包内仅保留JSON格式化工具,包含JSON页面自动检测并格式化、JSON内容手动格式化工具;以确保工具完全符合官方要求的「插件单一用途」原则
+    - 将FeHelper新版按`开放平台`的思路进行设计:插件平台化,其他老版本的所有工具,可在插件配置页面`选择性安装/更新/卸载`
+
+## 三、新版界面
+![FeHelper新版](https://user-images.githubusercontent.com/865735/75334978-b5315e80-58c3-11ea-9af0-e593149b0f7c.png)
+
+## 四、开放平台实现
+### 4.1 如何将插件配置页打造成`工具市场`,满足工具的上架、更新、卸载等操作?
+- 配置页增加远程服务接口,从服务器 https://www.baidufe.com 获取相关配置,用配置直接生成工具列表,配置格式如:
+```javascript
+{
+    newTools: { // 这里维护新上架、有更新的工具
+        'color-picker': {
+            name: '页面取色工具',      // 工具的名称
+            tips: '将光标移动到页面上需要取色的位置,单击确定即可取色', // 工具描述
+            noPage: true,            // true表示此工具无独立页面
+            contentScript: true,     // 是否有内容注入的脚本
+            contentScriptCss: false, // 是否注入Css
+            minVersion: '2020.02.0718', // 工具从FeHelper的哪个版本开始支持
+            menuConfig: [{          // 可以配置多个右键菜单
+                icon: '✑',          // 右键菜单的icon,也是工具的默认icon
+                text: '页面取色工具', // 右键菜单中的工具名称
+                onClick: function (info, tab) { // 右键菜单点以后的动作
+                    chrome.DynamicToolRunner({
+                        query: 'tool=color-picker',
+                        noPage: true
+                    });
+                }
+            }]
+        },
+        ...
+    },
+    
+    removeTools: [ // 需要下架的工具,直接在这里配置即可
+        'code-standards' 
+    ]
+}
+```
+- 每次进入到配置页/工具市场,都会从服务器端拉取最新配置,该配置与本地已存储的配置进行比对,可检测到每个工具是否有更新、是否需下架等
+- 针对已安装的工具,会从服务器端再次检测html模板内容与本地是否一致,不一致则`小红点提示`此工具有更新;这里有个细节,无论html、js、还是css文件发生过变更,都能通过只检测html模板的方式发现,因为html模板中引用js/css文件url后,都增加了文件`md5戳`,如此可做到只更新实际发生变更的文件
+    
+### 4.2 通过市场安装的工具,如何解决`资源存储`的问题,并确保存储空间足够大?
+- 下载的工具包,包含html、js、css文件资源,所有内容都需要在本地进行存储,如果工具越来越多,本地存储占用的空间会越来越大
+- 即便是在chrome-extension中,`localStorage`的存储上限也是`5M`,所以不能用它来存储已下载/安装的工具资源
+- 最终选择`chrome.storage.local`,搭配`unlimitedStorage`权限,可很好的解决资源存储问题,且突破存储空间的限制
+
+### 4.3 如何突破插件对`包外资源加载并执行`的限制,以实现动态安装的工具可在Chrome插件中正常运行?
+- 由于`content-security-policy`的限制,chrome-extension不允许执行包外资源,也严格不允许出现`inline`的脚本执行
+- 也不能无节制的去调整`content-security-policy`,将权限一再放大,这样确实会带来安全性的问题,审核也不容易通过
+- 最终方案,是用一个`dynamic/index.html`页面作为载体,通过`url-query`从本地存储中获取工具内容、加载并运行
+- 以加载`二维码/解码`工具为例,FeHelper最终会打开`chrome-extension://{id}/dynamic/index.html?tool=qr-code`
+- dynamic/index.html页面本身的内容很简单,就是一行代码`<script src="index.js" type="text/javascript"></script>`,在这个`index.js`文件中来加载数据,参考核心代码:
+```javascript
+(() => {
+    // 从本地存储获取工具资源进行渲染执行
+    let renderMyTool = (toolName, Awesome) => {
+
+        Awesome.getToolTpl(toolName).then(html => {
+            
+            // 回执html界面
+            document.write(html);
+
+            // 分析并获取静态文件列表
+            let allJs = [], allCss = [];
+            document.querySelectorAll('dynamic[data-source]').forEach(elm => {
+               let fileType = elm.getAttribute('data-type');
+               let files = (elm.getAttribute('data-source') || '').split(',');
+
+               if (fileType === 'js') {
+                   allJs = allJs.concat(files);
+               } else {
+                   allCss = allCss.concat(files);
+               } 
+            });
+
+            // 从本地存储中获取静态资源进行注入 & 执行
+            Promise.all([Awesome.StorageMgr.get(allCss), Awesome.StorageMgr.get(allJs)]).then(values => {
+                allCss = allCss.map(f => values[0][f]).join(' ');
+                if (allCss.length) {            // css内容可以直接inline注入
+                    let node = document.createElement('style');
+                    node.textContent = allCss;
+                    document.head.appendChild(node);
+                }
+                allJs = allJs.map(f => values[1][f]).join(';');
+                allJs.length && eval(allJs);    // js内容不能注入,可通过eval或者new Function的方式执行
+            });
+        });
+    };
+
+    // 从URL中获取工具名称
+    let toolName = new URL(location.href).searchParams.get('tool');
+    if (toolName) {
+        import('./awesome.js').then(dynamicModule => {
+            renderMyTool(toolName, dynamicModule.default);
+        });
+    } else {
+        chrome.runtime.openOptionsPage() && window.close();
+    }
+})();
+```
+- 如此,用户安装的二维码工具便可在FeHelper中正常运行 
+
+### 4.4 市场内工具包含`不同形式`:有独立界面形式、纯content-script形式、混合模式,平台如何支持?
+- 这里涉及到的是不同的工具表现形式,资源加载方式都不一样,`4.3`中讲到了独立界面形式的工具资源加载,下面说一下`content-script`的资源加载
+- 正常情况下,`content-script`的资源加载,都是`明码形式`在manifest.json中进行配置,如:
+```javascript
+"content_scripts": [{
+  "matches": [
+    "http://*/*",
+    "https://*/*",
+    "file://*/*"
+  ],
+  "js": [
+    "static/vendor/jquery/jquery-3.3.1.min.js",
+    "content-script/index.js",
+    ...
+  ],
+  "css": [
+      ...
+  ],
+  "run_at": "document_end",
+  "all_frames": false
+}]
+```
+- 如上,需要将js和css文件列表全都列出来,但是针对工具市场安装的应用,所有资源都属于`包外资源`,也非独立文件形式,这里就完全满足不了了
+- 最终解决思路:在`content_scripts`配置项中,只列出一个核心js文件`content-script/index.js`,其他动态安装的工具脚本,都通过它来动态载入:
+```javascript
+/* content-script/index.js 文件内容 */
+(() => {
+    chrome.runtime.sendMessage({
+        type: 'fh-dynamic-any-thing',
+        params: {
+            tabId: window.__FH_TAB_ID__ || null
+        },
+        func: ((params, callback) => {
+            Awesome.getInstalledTools().then(tools => {
+                let list = Object.keys(tools).filter(tool => tools[tool].contentScript);
+                let promiseArr = list.map(tool => Awesome.getContentScript(tool));
+                Promise.all(promiseArr).then(values => {
+                    let installedTools = {};
+                    values.forEach((v, i) => { installedTools[list[i]] = v; });
+                    return installedTools;
+                }).then(tools => {
+                    let jsCodes = [];
+                    Object.keys(tools).forEach(tool => {
+                        jsCodes.push(`(()=>{ ${tools[tool]} ; let f = window['${tool}ContentScript'];f&&f();})()`);
+                    });
+
+                    chrome.tabs.executeScript(params.tabId, { code: jsCodes.join(';') });
+                });
+            });
+            callback && callback();
+            return true;
+        }).toString()
+    });
+})();
+```
+- 对上面的代码实现做几个原理解释:
+    - content-script首先通过sendMessage的方式,告知`background`,当前tab需要获取content-script
+    - background收到消息并处理,遍历`获取所有已安装应用`的content-script内容
+    - 为保证每个应用独有的content-script不发生变量冲突,一律通过`闭包代码块`进行独立执行
+    - 所有content-script,通过`chrome.tabs.executeScript`一次性安全注入当前Tab,自动执行
+- 另外,这里没有提到`content-script-css`如何注入,其实这个工作交给了各自content-script-js自行完成,具体方法:
+```javascript
+chrome.runtime.sendMessage({
+    type: 'fh-dynamic-any-thing',
+    func: ((params, callback) => {
+        Awesome.getContentScriptCss('qr-code').then(css => chrome.tabs.insertCSS({code: css}));
+        callback && callback();
+        return true;
+    }).toString()
+});
+```
+- 以此,所有动态安装的工具,其内容脚本content-script都能完美的得到运行
+
+### 4.5 市场内工具与插件background之间的`消息通信`种类多样,如何提供统一接口进行支持?
+- chrome extension的核心,其实就是`消息通信`,包括background、popup、content-script之间的各种消息互通
+- 尤其content-script中,因为chrome-extension的限制,权限不足,很多操作必须由background来完成
+- 所以这里需要一个巧妙的设计,`能将操作虚拟的交给content-script`,原理简单,就是让background接受某一个固定类型的消息,执行`sender`传递过来的function-body:
+```javascript
+chrome.runtime.onMessage.addListener(function (request, sender, callback) {
+    // 从消息中获取 func 参数,转换成function实体,执行
+    if (request.type === 'fh-dynamic-any-thing') {
+        let func = new Function(`return ${request.func}`)();
+        typeof func === 'function' && func(request.params, callback);
+    }
+    
+    return true;
+});
+```
+- 有了这样一个Bridge,在各自的`content-script`中就可以编写任意的代码,直接执行(其实最终的执行还是background,只不过代码不用再hardcode到background中了),比如4.4中qr-code工具的css加载示例:
+```javascript
+chrome.runtime.sendMessage({
+    type: 'fh-dynamic-any-thing',
+    func: ((params, callback) => {
+        Awesome.getContentScriptCss('qr-code').then(css => chrome.tabs.insertCSS({code: css}));
+        callback && callback();
+        return true;
+    }).toString()
+});
+```
+- 或者更高级点儿的用法,把callback用起来(这里需要注意:func中的操作如果是异步的,则callback是拿不到参数的)
+```javascript
+chrome.runtime.sendMessage({
+    type: 'fh-dynamic-any-thing',
+    func: ((params, callback) => {
+        // 这里可以做任何事情 
+        let manifest = chrome.runtime.getManifest();
+        
+        // 最终结果在这里通知callback
+        callback && callback(manifest);
+        return true;
+    }).toString()
+}, manifest => {
+    // 这里已经拿到background中执行的结果,直接使用
+    alert(`当前插件版本号为:${manifest.version}`);    
+});
+```
+- 上面`func`参数指定的function,其实最终就是在background中执行的,只不过`background部分的代码可以由工具自己来管理`
+
+
+### 4.6 工具的`使用方式`分两种:Toolbar-Popup-Page模式、Page-Context-Menu模式,如何统一管理?
+- Toolbar-Popup-Page的模式,是直接在浏览器工具栏点击插件icon进行使用
+- Page-Context-Menu的模式,是通过右键菜单进行使用
+- 两种模式的渲染和执行过程完全不一样,需要一个统一的`任务管理器`进行管理,其实就是前面示例中已经提到的`chrome.DynamicToolRunner`,我们来看方法定义:
+```javascript
+/**
+ * 任务管理器,通过它,统一实现FeHelper工具的任务分配和运行
+ * @param  {Object}  configs      启动任务管理器所需要的配置项
+ * @config {String}  tool         要打开的工具名称,默认就是dynamic
+ * @config {String}  withContent  默认携带的内容,在打开工具以后可读取
+ * @config {String}  query        请求参数,访问页面可以携带一些默认参数
+ * @config {Boolean} noPage       是否无页面模式(默认false,即独立页面)
+ * @constructor
+ */
+chrome.DynamicToolRunner = async function (configs) {
+
+    let tool = configs.tool || MSG_TYPE.DYNAMIC_TOOL;
+    let withContent = configs.withContent;
+    let query = configs.query;
+
+    // 如果是noPage模式,则表名只完成content-script的工作,直接发送命令即可
+    if (configs.noPage) {
+        tool = new URL(`http://f.h?${query}`).searchParams.get('tool').replace(/-/g, '');
+        chrome.tabs.query({active: true, currentWindow: true}, tabs => {
+            if (/^(http(s)?|file):\/\//.test(tabs[0].url)) {
+                chrome.tabs.executeScript(tabs[0].id, {
+                    code: `window['${tool}NoPage'] && window['${tool}NoPage'](${JSON.stringify(tabs[0])});`
+                });
+            } else {
+                notifyText({
+                    message:'抱歉,此工具无法在当前页面使用!'
+                });
+            }
+        });
+        return;
+    }
+
+    chrome.tabs.create({
+        url: `${tool}/index.html?${query}`,
+        active: true
+    }, tab => {
+        withContent && setTimeout(function () {
+            chrome.tabs.sendMessage(tab.id, {
+                type: MSG_TYPE.TAB_CREATED_OR_UPDATED,
+                content: withContent,
+                event: tool
+            });
+        }, 300);
+    });
+};
+```
+- 上面是任务管理器的核心代码部分,举两个使用场景的例子:
+```javascript
+// test case 1: popup-page中唤起image-base64工具,并传递一个需要进行base64的图片地址
+chrome.DynamicToolRunner({
+    query : 'tool=image-base64',
+    withContent : 'https://www.baidu.com/img/bd_logo1.png'
+});
+
+// test case 2: popup-page中唤起color-picker工具,此工具无独立页面
+chrome.DynamicToolRunner({
+    query : 'tool=color-picker',
+    noPage : true
+});
+
+// test case 3: context-menu中唤起qr-code工具,并将需要生成二维码的内容传递到页面
+chrome.contextMenus.create({
+    title: '二维码生成器',
+    contexts: ['all'],
+    parentId: FeJson.contextMenuId,
+    onclick: function (info, tab) {
+        chrome.tabs.executeScript(tab.id, {
+            code: '(' + (function (pInfo) {
+                let linkUrl = pInfo.linkUrl;
+                let pageUrl = pInfo.pageUrl;
+                let imgUrl = pInfo.srcUrl;
+                let selection = pInfo.selectionText;
+                
+                return linkUrl || imgUrl || selection || pageUrl;
+                }).toString() + ')(' + JSON.stringify(info) + ')',
+            allFrames: false
+        }, function (contents) {
+            chrome.DynamicToolRunner({
+                withContent: contents[0],
+                query: `tool=qr-code`
+            });
+        });
+    }
+});
+
+```
+
+### 4.7 除作者外,第三方`开发者如何发布自己的应用`到FeHelper工具市场?
+- FeHelper工具市场内一个完整的工具(zip包),需要包含如下几个部分:
+```text
+- ${tool}文件夹              `必选`
+    - fh-config.js          `必选,具体配置项参考下文`
+    - index.html            `必选,要去双击可独立运行`
+    - index.js              `必选`
+    - index.css             `可选`
+    - content-script.js     `可选,除非config中配置了true`
+    - content-script.css    `可选,除非config中配置了true`
+    - other js/css files    `可选,需在index.html中显式引用`
+    - images                `禁止,如果需要可用base64替代`
+    - font                  `禁止,如果需要可用base64替代`
+```        
+- 附`fh-config.js`配置项说明,以FeHelper中默认提供的`hello-world`为例:
+```javascript
+(function () {
+    return {
+        "hello-world": {
+            "name": "Hello world!",      // 工具的名称
+            "tips": "这是一个FH自定义工具的入门示例!一切都从Hello world开始,大家可体验,或下载后学习!", // 工具描述
+            "noPage": true,             // true表示此工具无独立页面
+            "contentScript": true,      // 是否有内容注入的脚本
+            "contentScriptCss": false,  // 是否注入Css
+            "minVersion": "2020.02.0718", // 工具从FeHelper的哪个版本开始支持
+            "menuConfig": [{            // 可以配置多个右键菜单
+                "icon": "웃",            // 右键菜单的icon,也是工具的默认icon
+                "text": "Hello world",  // 右键菜单中的工具名称
+                "onClick": function (info, tab) {
+                    alert('你好,我是Hello world;这是一个无独立页面的功能,我的内容已经输出到控制台了哦!');
+                    chrome.DynamicToolRunner({
+                        query: "tool=hello-world",
+                        noPage: true
+                    });
+                }
+            }]
+        }
+    };
+})();
+```
+- 此工具在开发者自行测试通过后,可打zip包后,邮件给我(`阿烈叔`),格式:
+```text
+收件:[email protected]
+标题:【FeHelper新工具提审】+ 新工具名称
+正文:描述新工具的使用场景、使用方法,最好附操作gif图或者视频教程
+附件:工具zip包,可增加其他使用教程
+```
+- `工具提审在线化`:目前规划中,可取代邮件提审的形式
+
+## 五、FH开发者工具
+### 5.1 无图无真相
+![FH开发者工具预览](https://user-images.githubusercontent.com/865735/75334554-0b51d200-58c3-11ea-98bf-56cd74c2309a.png)
+![FH自带编辑器](https://user-images.githubusercontent.com/865735/75334604-1dcc0b80-58c3-11ea-8cd5-d7f3190c53c4.png)
+
+### 5.2 工具介绍
+- FH开发者工具能干什么?
+    - 简单说就是:你基本可以`零基础`、`1分钟`搞定一个FH工具!
+    - 可以直接开启并体验`Hello world!`工具,也可以直接`下载示例zip包`进行学习!
+    - 可以直接通过开发者工具的`界面向导`操作,创建一个简单/复杂的FH工具!
+    - 已经创建好的工具,可以`下载zip包`继续在本地开发!
+    - 你也可以直接下载zip包后,`分享`给其他小伙伴儿!
+    - 当然,如果你觉得你的工具很实用,你也可以下载zip包,直接`邮件提审`给我(阿烈叔)!
+- FH开发者工具的一些贴心功能
+    - 在线创建、在线Coding、`自动保存`、`实时生效`
+    - 在线编辑`fh-config`配置时,整个工具的文件列表也会自动实时生效
+    - 图标不好找?FH给你提供了一批`现成的字符图标`,点一下就能用!
+
+## 六、Open API
+> 建议安装`FH开发者工具`以后,直接拿`hello-world`示例来学习!
+
+### 6.1 chrome.* API
+- 官方提供的Api基本都可以用,可以直接去官网看: https://developer.chrome.com/extensions/devguide
+- 如果访问不了`chrome.com`,你可以用`360的插件开发者Api`来学习使用,也基本够用: http://open.chrome.360.cn/extension_dev/overview.html
+- 如果也不行看`360`的Api,你还可以看`baidu浏览器插件的开发者Api`,也差不多够用: https://chajian.baidu.com/developer/extensions/api_index.html
+
+### 6.2 在工具独立页面使用chrome.* API
+- 如果你的工具没有配置`noPage: true`,那么你可以在`index.html`引用的js文件中直接使用`chrome.*`API
+
+### 6.3 content-script.js
+- 只要你配置了`contentScript: true`,工具就一定需要有content-script.js脚本文件
+- content-script.js文件中,一定要显示的在window上绑定一个方法,以`hello-world`为例:
+```javascript
+/**
+ * 注意这里的方法名称,其实是:window[`${toolName.replace(/[-_]/g,'')}ContentScript`];
+ * @author 阿烈叔
+ */
+window.helloworldContentScript = function () {
+    console.log('你好,我是来自FeHelper的工具Demo:hello world!');
+};
+```
+- 你完全不必要担心`window对象被污染`,因为content-script是在一个独立的沙箱内运行的,对网页的正常运行毫无影响
+- content-script.js文件中,基本是除了`chrome.runtime`API,其他的`chrome.*`是用不了的,如果实在要用,可以参考`6.5`的消息机制
+- 在content-script.js中,你可以进行任意的`DOM操作`,就跟你正常的coding一样
+
+### 6.4 关于noPage配置
+- 如果你配置了`noPage: true`,那你的工具也一定需要有content-script.js脚本文件
+- content-script.js文件中,一定要显示的在window上绑定一个方法,依然以`hello-world`为例:
+```javascript
+/**
+ * 如果在 fh-config.js 中指定了 noPage参数为true,则这里必须定义noPage的接口方法,如:
+ * 注意这里的方法名称,其实是:window[`${toolName.replace(/[-_]/g,'')}NoPage`];
+ * @author 阿烈叔
+ */
+window.helloworldNoPage = function (tabInfo) {
+    alert('你好,我是来自FeHelper的工具Demo:hello world!你可以打开控制台看Demo的输出!');
+    console.log('你好,我是来自FeHelper的工具Demo:', tabInfo);
+};
+```
+- 既然noPage和content-script都一样,那`为什么还要有noPage`这个东西?
+    - noPage指明的是:该工具无独立页面
+    - 点击下拉列表中的工具入口、或者右键菜单中点击工具入口,会执行`window.xxxNoPage`中的代码
+- noPage的应用有没有一些实用的例子?    
+    - 比如,FeHelper中提供的`网页取色工具`,就是一个noPage的应用,点击工具,直接在网页上呼出一个取色器
+    - 再比如,FeHelper中提供的`二维码解码`工具,在二维码图片上右击,可以直接对该二维码进行解码
+    
+### 6.5 消息通信
+- 消息机制主要是提供给`content-script.js`使用的,它提供了一种`内容脚本使用chrome.* API`的可行性,示例:
+```javascript
+// background 示例:在content-script.js中获取当前浏览器的所有tab
+chrome.runtime.sendMessage({
+    type: 'fh-dynamic-any-thing',
+    params: {
+        tabId: window.__FH_TAB_ID__ // 这是FH的内置变量,表示当前Tab的id
+    },
+    func: ((params, callback) => {
+        // TODO: 这里可以调用 chrome.* 的API,随便用。。。
+
+        // Case1:获取当前窗口的全部tab
+        chrome.tabs.query({currentWindow: true}, tabs => {
+            let jsonInfo = JSON.stringify(tabs);
+
+            // 注入到页面,注意看这里如何读取外面传进来的参数
+            chrome.tabs.executeScript(params.tabId, {
+                code: 'console.log(' + jsonInfo + ');'
+            });
+        });
+
+        callback && callback();
+        return true;
+    }).toString()
+});
+```
+
+### 6.6 content-script.css
+- 如果配置了`contentScriptCss: true`,那说明你的FH工具还需要`向页面注入CSS代码`
+- FeHelper `v2020.03.1210`版本开始,内容css将有FH自动加载,content-script.js中调用下面方法即可注入
+```javascript
+// 以页面代码自动美化工具为例,注意这里的方法名:window.${toolName}ContentScriptCssInject()
+window.codebeautifyContentScriptCssInject();
+```
+- 以下为老版本FH的内容css加载方式(向后兼容)
+- content-script.css的加载机制,是在content-script.js中通过`6.5`中介绍的消息机制来完成的
+- 依然以`hello-world`为例,看代码示例:
+```javascript
+// 注入css and html fragment
+chrome.runtime.sendMessage({
+    type: 'fh-dynamic-any-thing',
+    func: ((params, callback) => {
+        // 通过这个内置方法来获取css内容,并直接注入当前网页
+        Awesome.getContentScript('hello-world', true).then(cssText => {
+            chrome.tabs.insertCSS({
+                code: cssText,
+                runAt: 'document_end'
+            });
+        });
+        callback && callback();
+        return true;
+    }).toString()
+});
+```
+- 当然,要想在content-script中使用自定义的css,办法还有很多,可以定义`contentScriptCss: false`,通过在页面上直接`硬编码插入`的方式来完成,比如:
+```javascript
+let cssText = `/* Your CSS Codes Here... */`;
+let elStyle = document.createElement('style');
+elStyle.textContent = cssText;
+document.head.appendChild(elStyle);
+```
+
+## 七、意见反馈
+- 大家可在feedback中反馈、也可加群反馈、或者直接Mail给我
+- 最后,欢迎搭建使用 FeHelper ,希望`开放平台`思路的FeHelper能给大家带来快感!

+ 16 - 0
README_TEST.md

@@ -0,0 +1,16 @@
+## FeHelper测试说明书
+
+> 本教程主要用于 output/fehelper.zip 包的本地测试,日常使用的话,建议安装线上正式版(Chrome商店版)
+
+### 一、测试包安装
+1. 下载fehelper.zip包,文件路径:`output/fehelper.zip`
+2. 解压fehelper.zip包,建议解压到一个`安全的目录`,别无意间被删掉了
+3. 打开chrome浏览器,地址栏输入:`chrome://extensions/` 进入插件管理界面
+4. 右上角`开启开发者模式`,确保所有的插件可被管理
+5. 如果本机已安装过线上FeHelper正式版,请找到它,并且`禁用它`
+6. 上方找到`加载已解压的扩展程序`按钮,选择fehelper.zip的解压目录
+7. 完成本地包载入,FeHelper本地包安装成功
+
+
+### 二、测试内容反馈
+大家可以把测试过程中发现的问题,统一提交到这里:[https://github.com/zxlie/FeHelper/issues/192](https://github.com/zxlie/FeHelper/issues/192) ,我会尽快跟进并修复它,为大家提供更高质量的FeHelper!

+ 63 - 0
apps/aiagent/fh.ai.js

@@ -0,0 +1,63 @@
+import EncodeUtils from '../en-decode/endecode-lib.js';
+/**
+ * 用零一万物大模型来进行流式问答输出
+ */
+let AI = (() => {
+    const defaultKey = 'MWFhZWE0M2Y3ZDBkNDJhNmJhNjMzOTZkOGJlNTA4ZmY=';
+
+    async function streamChatCompletions(prompt,receivingCallback,apiKey) {
+        const url = 'https://api.lingyiwanwu.com/v1/chat/completions';
+        const options = {
+            method: 'POST',
+            headers: {
+                'Content-Type': 'application/json',
+                'Authorization': `Bearer ${apiKey || EncodeUtils.base64Decode(defaultKey)}`
+            },
+            body: JSON.stringify({
+                model: "yi-large",
+                messages: [{ role: "user", content: prompt }],
+                temperature: 0.3,
+                stream: true
+            })
+        };
+    
+        try {
+            const response = await fetch(url, options);
+            if (!response.ok) {
+                throw new Error(`HTTP error! status: ${response.status}`);
+            }
+    
+            // 创建一个ReadableStream用于处理流式数据
+            const reader = response.body.getReader();
+            const decoder = new TextDecoder('utf-8');
+            let done = false;
+    
+            while (!done) {
+                const { value, done: readerDone } = await reader.read();
+                done = readerDone;
+                if (value) {
+                    // 将接收到的数据块解码为字符串,并假设每一行都是一个独立的JSON对象
+                    const lines = decoder.decode(value, { stream: true }).split('\n').filter(line => line.trim() !== '');
+                    for (const line of lines) {
+                        try {
+                            // 解析每一行作为单独的JSON对象
+                            const message = JSON.parse(line.replace(/^data:\s+/,''));
+                            receivingCallback && receivingCallback(message);
+                        } catch (jsonError) {
+                            if(line === 'data: [DONE]'){
+                                receivingCallback && receivingCallback(null,true);
+                            }
+                        }
+                    }
+                }
+            }
+        } catch (error) {
+            console.error('Error fetching chat completions:', error);
+        }
+    }
+    
+    return {askYiLarge: streamChatCompletions};
+})();
+
+
+export default AI;

+ 208 - 0
apps/aiagent/index.css

@@ -0,0 +1,208 @@
+@import url("../static/css/bootstrap.min.css");
+@import url("../static/vendor/highlight/github.css");
+
+html,body {
+    background: #fff;
+}
+.wrapper {
+    width:auto;
+}
+
+.table {
+    font-size: 14px;
+}
+
+.mod-inputs {
+    padding: 20px;
+    background-color: #f5f5f5;
+    border-radius: 6px;
+    border: 1px solid #eee;
+}
+.x-res-item {
+    margin: 5px 0;
+}
+.box-prompt {
+    position:fixed;
+    bottom:10px;
+    left:10px;
+    right:10px;
+    padding:10px;
+    margin: 10px;
+    background-color: darkslategray;
+}
+.box-prompt .form-control {
+    height:100px;
+    padding:6px;
+}
+.box-result {
+    position: fixed;
+    bottom: 150px;
+    top: 70px;
+    left: 10px;
+    right: 10px;
+    margin: 10px;
+    background-color: #fafafa;
+    border-color: #ccc;
+    overflow: auto;
+}
+.btn-chat {
+    position:absolute;
+    top:76px;
+    right:10px;
+    height:44px;
+    width:50px;
+}
+.box-tips {
+    margin: 0 15px 0 0;
+}
+ul.x-demos {
+    margin: 30px 0 10px 0;
+    padding: 0;
+}
+ul.x-demos li {
+    position: relative;
+    display: flex;
+    flex-shrink: 0;
+    flex-grow: 0;
+    overflow: hidden;
+    box-sizing: border-box;
+    border-radius: 8px;
+    flex-direction: column;
+    padding: 12px;
+    background-image: linear-gradient(to right, rgba(248, 160, 255, 0.098), rgba(71, 185, 255, 0.098));
+    align-items: flex-start;
+    justify-content: flex-start;
+    margin: 0px 0px 8px;
+    width: 100%;
+    height: auto;
+    cursor: pointer;
+    -webkit-box-flex: 0;
+    -webkit-box-orient: vertical;
+    -webkit-box-direction: normal;
+    -webkit-box-align: start;
+    -webkit-box-pack: start;
+}
+ul.x-demos li:hover {
+    color:#f00;
+}
+.x-loading{
+    margin: 0 auto;
+    width: 200px;
+    position: absolute;
+    left: calc(50% - 100px);
+    top: calc(50% - 100px);
+}
+.x-loading-inline{
+    width: 60px;
+}
+.x-hr {
+    margin: 10px 0;
+}
+.box-message {
+    margin: 10px 15px 10px 0;
+    border:1px solid #eee;
+}
+.box-message table {
+    width:100%;
+}
+.box-message table td{
+    padding: 10px 5px;
+    vertical-align: top;
+}
+.x-from-fh {
+    border: 1px solid #eee;
+    border-width: 1px 0 1px;
+    background: #f9f9f9;
+}
+.td-icon {
+    padding-right: 0 !important;
+}
+.td-icon img {
+    width:16px;
+}
+.td-icon.x-me img {
+    border: 1px solid #f00;
+    border-radius: 16px;
+}
+b.x-time {
+    font-weight: normal;
+    color: #999;
+}
+.x-xcontent {
+    margin-top: 6px;
+}
+.x-xcontent p {
+    margin:0 0 2px 0;
+}
+.x-toolbox {
+    font-size: 12px;
+    text-align: right;
+    float: right;
+    color: #48b;
+}
+.x-toolbox .x-line {
+	font-size: 12px;
+	margin-left: 10px;
+	color: #bbb;
+}
+.x-toolbox .x-tooltip:hover {
+    color: red;
+    cursor: pointer;
+}
+h3.panel-title {
+    position: relative;
+}
+.box-setting {
+    border: 1px solid #ccc;
+    border-radius: 4px;
+    background-color: #fff;
+    position: absolute;
+    top: 27px;
+    right: -16px;
+    z-index: 100;
+    padding: 5px;
+    font-size: 14px;
+    line-height: 26px;
+}
+.box-setting label {
+    font-weight: normal;
+    margin-left: 5px;
+    margin-right: 20px;
+}
+.box-setting .x-sure {
+    text-align: right;
+}
+.box-setting .x-sure button {
+    position: relative;
+    right: -30px;
+}
+
+#pageContainer fieldset {
+    padding: 10px;
+    margin: 10px;
+    border: 1px solid #ddd;
+}
+#pageContainer legend {
+    width: auto;
+    padding: 0 10px;
+    margin-bottom: 0px;
+    font-size: 14px;
+    color: #333;
+    border: 0;
+    border-bottom: 0;
+}
+.resp-error {
+    color:red;
+}
+.x-xcontent table {
+    width: auto;
+}
+.x-xcontent table th {
+    border: 1px solid #ccc;
+    text-align: center;
+    padding: 10px;
+    background: #eee;
+}
+.x-xcontent table td {
+    border: 1px solid #ccc;
+}

+ 78 - 0
apps/aiagent/index.html

@@ -0,0 +1,78 @@
+<!DOCTYPE HTML>
+<html lang="zh-CN">
+<head>
+    <title>AI,请帮帮忙</title>
+    <meta charset="UTF-8">
+    <link rel="shortcut icon" href="../static/img/favicon.ico">
+    <link rel="stylesheet" href="index.css"/>
+    <script type="text/javascript" src="../static/vendor/evalCore.min.js"></script>
+    <script type="text/javascript" src="../static/vendor/vue/vue.js"></script>
+</head>
+<body>
+
+<div class="wrapper" id="pageContainer">
+    <div class="panel panel-default" style="margin-bottom: 0px;">
+        <div class="panel-heading">
+            <h3 class="panel-title">
+                <a href="https://www.baidufe.com/fehelper/index/index.html" target="_blank" class="x-a-high">
+                    <img src="../static/img/fe-16.png" alt="fehelper"/> FeHelper:</a>AI,请帮帮忙
+            </h3>
+        </div>
+    </div>
+    <div class="panel-body">
+        <div class="row mod-inputs box-prompt">
+            <form class="ui-mt-10" @submit.prevent="goChat">
+                <textarea type="text" id="prompt" ref="prompt" v-model="prompt" class="form-control" placeholder="你有什么内容想要咨询AI的?"
+                    data-key="c2stNEJUbWRxYW00eENTUXJXRnlNajFUM0JsYmtGSlVlbDJnZ2dGbjI5MVBKQVdzZnZR"></textarea>
+                <input class="btn btn-sm btn-primary btn-chat" type="button" value="发送" @click="goChat">
+            </form>
+        </div>
+        <div class="row mod-inputs box-result" ref="boxResult">
+            <div class="row box-tips" v-if="!hideDemo">
+                <div>你好,我是你的专属AI助理,不管你是要查BUG、写代码、还是要咨询什么技术问题,你都可以跟我说,我会竭尽全力帮你解决,比如你可以这样问我👇👇👇</div>
+                <ul class="x-demos clearfix">
+                    <li v-for="demo in demos" @click="sendMessage(demo)">{{demo}}</li>
+                </ul>
+            </div>
+            <div class="row box-message">
+                <table>
+                    <template v-for="msg in history">
+                        <tr class="x-from-fh">
+                            <td class="td-icon x-me"><img src="../static/img/me.png" alt="me"/></td>
+                            <td class="td-content"><b class="x-time">{{msg.sendTime}} </b><div class="x-xcontent">{{msg.message}}</div></td>
+                        </tr>
+                        <tr class="x-back-gpt">
+                            <td class="td-icon">
+                                <img src="../static/img/fe-16.png" alt="fehelper"/></td></td>
+                            <td class="td-content"><b class="x-time">{{msg.respTime}}</b>
+                                <div :id="msg.id" class="x-xcontent" v-html="msg.respContent"></div>
+                            </td>
+                        </tr>
+                    </template>
+
+                    <template v-if="respResult.id">
+                        <tr class="x-from-fh">
+                            <td class="td-icon x-me"><img src="../static/img/me.png" alt="me"/></td>
+                            <td class="td-content"><b class="x-time">{{respResult.sendTime}} </b><div class="x-xcontent">{{respResult.message}}</div></td>
+                        </tr>
+                        <tr class="x-back-gpt">
+                            <td class="td-icon">
+                                <img src="../static/img/fe-16.png" alt="fehelper"/></td>
+                            <td class="td-content"><b class="x-time">{{respResult.respTime}}</b>
+                                <div :id="respResult.id" class="x-xcontent" v-html="respResult.respContent"></div>
+                            </td>
+                        </tr>
+                    </template>
+                </table>
+            </div>
+        </div>
+    </div>
+</div>
+<script type="text/javascript" src="../static/vendor/highlight/highlight.js"></script>
+<script type="text/javascript" src="../html2markdown/libs/marked.js"></script>
+<script type="text/javascript" src="../html2markdown/libs/rawdeflate.js"></script>
+<script type="text/javascript" src="../html2markdown/libs/rawinflate.js"></script>
+<script type="text/javascript" src="../static/js/utils.js"></script>
+<script type="module" src="index.js"></script>
+</body>
+</html>

+ 125 - 0
apps/aiagent/index.js

@@ -0,0 +1,125 @@
+/**
+ * ChatGPT工具
+ * @author zhaoxianlie
+ */
+
+import AI from './fh.ai.js';
+
+new Vue({
+    el: '#pageContainer',
+    data: {
+        prompt: '',
+        demos: [
+            'FeHelper是什么?怎么安装?',
+            '用Js写一个冒泡排序的Demo',
+            'Js里的Fetch API是怎么用的'
+        ],
+        initMessage: {
+            id:'id-test123',
+            sendTime:'2022/12/20 12:12:12',
+            message: '你好,可以告诉我你是谁吗?我该怎么和你沟通?',
+            respTime: '2022/12/20 12:12:13',
+            respContent: '你好,我是FeHelper智能助理,由OpenAI提供技术支持;你可以在下面的输入框向我提问,我会尽可能回答你~~~'
+        },
+        respResult:{
+            id: '',
+            sendTime:'',
+            message:'',
+            respTime:'',
+            respContent:''
+        },
+        history:[],
+        tempId:'',
+        hideDemo: false,
+        undergoing: false
+    },
+    mounted: function () {
+        this.$refs.prompt.focus();
+        this.hideDemo = !!(new URL(location.href)).searchParams.get('hideDemo');
+    },
+    methods: {
+        // 这个代码,主要用来判断大模型返回的内容是不是包含完整的代码块
+        validateCodeBlocks(content) {
+            let backticksCount = 0;
+            let inCodeBlock = false;
+            let codeBlockStartIndex = -1;
+        
+            for (let i = 0; i < content.length; i++) {
+                // 检查当前位置是否是三个连续的反引号
+                if (content.startsWith('```', i)) {
+                    backticksCount++;
+                    i += 2; // 跳过接下来的两个字符,因为它们也是反引号的一部分
+        
+                    // 如果我们遇到了奇数个反引号序列,那么我们进入了代码块
+                    if (backticksCount % 2 !== 0) {
+                        inCodeBlock = true;
+                        codeBlockStartIndex = i - 2;
+                    } else { // 否则,我们离开了代码块
+                        inCodeBlock = false;
+                        if (codeBlockStartIndex === -1 || codeBlockStartIndex > i) {
+                            return false; // 这意味着有不匹配的反引号
+                        }
+                        codeBlockStartIndex = -1;
+                    }
+                }
+            }
+        
+            // 如果最终 backticksCount 是偶数,则所有代码块都正确关闭
+            return backticksCount % 2 === 0 && !inCodeBlock;
+        },
+
+        sendMessage(prompt){
+            if(this.undergoing) return;
+            if(this.respResult.id){
+                this.history.push(this.respResult);
+                this.respResult.id = '';
+            }
+
+            this.undergoing = true;
+            let sendTime = (new Date()).format('yyyy/MM/dd HH:mm:ss');
+            this.$nextTick(() => {
+                this.scrollToBottom();
+            });
+
+            this.tempId = '';
+            let respContent = '';
+            AI.askYiLarge(prompt,(respJson,done) => {
+                if(done){
+                    this.undergoing = false;
+                    return;
+                }
+                let id = respJson.id;
+                respContent = respJson.content || '';
+                if(!this.validateCodeBlocks(respContent)) {
+                    respContent += '\n```';
+                }
+                respContent = marked(respContent);
+                if(this.tempId !== id) {
+                    this.tempId = id;
+                    let dateTime = new Date(respJson.created * 1000);
+                    let respTime = dateTime.format('yyyy/MM/dd HH:mm:ss');
+                    this.respResult = { id,sendTime,message:prompt,respTime,respContent };
+                }else{
+                    this.respResult.respContent = respContent;
+                }
+                
+                this.$nextTick(() => {
+                    let elm = document.getElementById(id);
+                    elm.querySelectorAll('pre code').forEach((block) => {
+                        hljs.highlightBlock(block);
+                    });
+                    this.scrollToBottom();
+                });
+            });            
+        },
+
+        scrollToBottom(){
+            this.$refs.boxResult.scrollTop = this.$refs.boxResult.scrollHeight;
+        },
+        goChat(){
+            this.sendMessage(this.prompt);
+            this.$nextTick(() => this.prompt='');
+        }
+    }
+
+});

+ 314 - 0
apps/background/awesome.js

@@ -0,0 +1,314 @@
+/**
+ * 工具更新
+ * @type {{download}}
+ */
+import toolMap from './tools.js';
+
+let Awesome = (() => {
+
+    let manifest = chrome.runtime ? chrome.runtime.getManifest() : {};
+
+    const SERVER_SITE = manifest.homepage_url;
+    const URL_TOOL_TPL = `${SERVER_SITE}/#TOOL-NAME#/index.html`;
+    const TOOL_NAME_TPL = 'DYNAMIC_TOOL:#TOOL-NAME#';
+    const TOOL_CONTENT_SCRIPT_TPL = 'DYNAMIC_TOOL:CS:#TOOL-NAME#';
+    const TOOL_CONTENT_SCRIPT_CSS_TPL = 'DYNAMIC_TOOL:CS:CSS:#TOOL-NAME#';
+    const TOOL_MENU_TPL = 'DYNAMIC_MENU:#TOOL-NAME#';
+
+    /**
+     * 管理本地存储
+     */
+    let StorageMgr = (() => {
+
+        let get = keyArr => {
+            return new Promise((resolve, reject) => {
+                chrome.storage.local.get(keyArr, result => {
+                    resolve(typeof keyArr === 'string' ? result[keyArr] : result);
+                });
+            });
+        };
+
+
+        let getSync = async (keyArr) => {
+            return await (new Promise((resolve, reject) => {
+                chrome.storage.local.get(keyArr, result => {
+                    resolve(typeof keyArr === 'string' ? result[keyArr] : result);
+                });
+            }));
+        };
+
+        let set = (items, values) => {
+            return new Promise((resolve, reject) => {
+                if (typeof items === 'string') {
+                    let tmp = {};
+                    tmp[items] = values;
+                    items = tmp;
+                }
+                chrome.storage.local.set(items, () => {
+                    resolve();
+                });
+            });
+        };
+
+        let remove = keyArr => {
+            return new Promise((resolve, reject) => {
+                keyArr = [].concat(keyArr);
+                chrome.storage.local.remove(keyArr, () => {
+                    resolve();
+                });
+            });
+        };
+
+        return {get, set, remove,getSync};
+    })();
+
+    /**
+     * 检测工具是否已被成功安装
+     * @param toolName 工具名称
+     * @param detectMenu 是否进一步检测Menu的设置情况
+     * @returns {Promise}
+     */
+    let detectInstall = (toolName, detectMenu) => {
+
+        let menuKey = TOOL_MENU_TPL.replace('#TOOL-NAME#', toolName);
+        let toolKey = TOOL_NAME_TPL.replace('#TOOL-NAME#', toolName);
+
+        return Promise.all([StorageMgr.get(toolKey), StorageMgr.get(menuKey)]).then(values => {
+            let toolInstalled = !!values[0];
+            // 系统预置的功能,是强制 installed 状态的
+            if(toolMap[toolName] && toolMap[toolName].systemInstalled) {
+                toolInstalled = true;
+            }
+            if (detectMenu) {
+                return toolInstalled && String(values[1]) === '1';
+            }
+            return toolInstalled;
+        });
+    };
+
+    let log = (txt) => {
+        // console.log(String(new Date(new Date() * 1 - (new Date().getTimezoneOffset()) * 60 * 1000).toJSON()).replace(/T/i, ' ').replace(/Z/i, '') + '>', txt);
+    };
+
+    /**
+     * 安装/更新工具,支持显示安装进度
+     * @param toolName
+     * @param fnProgress
+     * @returns {Promise<any>}
+     */
+    let install = (toolName, fnProgress) => {
+        return new Promise((resolve, reject) => {
+            // 存储html文件
+            StorageMgr.set(TOOL_NAME_TPL.replace('#TOOL-NAME#', toolName), '&nbsp;');
+            log(toolName + '工具html模板安装/更新成功!');
+            resolve();
+        });
+    };
+
+    let offLoad = (toolName) => {
+        let items = [];
+        items.push(TOOL_NAME_TPL.replace('#TOOL-NAME#', toolName));
+        items.push(TOOL_CONTENT_SCRIPT_TPL.replace('#TOOL-NAME#', toolName));
+        items.push(TOOL_CONTENT_SCRIPT_CSS_TPL.replace('#TOOL-NAME#', toolName));
+
+        // 删除所有静态文件
+        chrome.storage.local.get(null, allDatas => {
+            StorageMgr.remove(Object.keys(allDatas).filter(key => String(key).startsWith(`../${toolName}/`)));
+        });
+
+        log(toolName + ' 卸载成功!');
+
+        return StorageMgr.remove(items);
+    };
+
+    /**
+     * 有些工具其实已经卸载过了,但是本地还有冗余的静态文件,都需要统一清理一遍
+     */
+    let gcLocalFiles = () => getAllTools().then(tools => Object.keys(tools).forEach(tool => {
+        if (!tools[tool] || !tools[tool]._devTool && !tools[tool].installed) {
+            offLoad(tool);
+        }
+    }));
+
+    let getAllTools = async () => {
+
+        // 获取本地开发的插件,也拼接进来
+        try {
+            const DEV_TOOLS_MY_TOOLS = 'DEV-TOOLS:MY-TOOLS';
+            let _tools = await StorageMgr.getSync(DEV_TOOLS_MY_TOOLS);
+            let localDevTools = JSON.parse(_tools || '{}');
+            Object.keys(localDevTools).forEach(tool => {
+                toolMap[tool] = localDevTools[tool];
+            });
+        } catch (e) {
+        }
+
+        let tools = Object.keys(toolMap);
+        let promises = [];
+        tools.forEach(tool => {
+            promises = promises.concat([detectInstall(tool), detectInstall(tool, true)])
+        });
+        let pAll = Promise.all(promises).then(values => {
+            values.forEach((v, i) => {
+                let tool = tools[Math.floor(i / 2)];
+                let key = i % 2 === 0 ? 'installed' : 'menu';
+                toolMap[tool][key] = v;
+                // 本地工具,还需要看是否处于开启状态
+                if (toolMap[tool].hasOwnProperty('_devTool')) {
+                    toolMap[tool][key] = toolMap[tool][key] && toolMap[tool]._enable;
+                }
+            });
+            return toolMap;
+        });
+        let pSort = SortToolMgr.get();
+
+        return Promise.all([pAll,pSort]).then(vs => {
+            let allTools = vs[0];
+            let sortTools = vs[1];
+
+            if (sortTools && sortTools.length) {
+                let map = {};
+                sortTools.forEach(tool => {
+                    if(allTools[tool]) {
+                        map[tool] = allTools[tool];
+                    }
+                });
+                Object.keys(allTools).forEach(tool => {
+                    if (!map[tool]) {
+                        map[tool] = allTools[tool];
+                    }
+                });
+                return map;
+            }else{
+                return allTools;
+            }
+        });
+    };
+
+    /**
+     * 检查看本地已安装过哪些工具
+     * @returns {Promise}
+     */
+    let getInstalledTools = () => getAllTools().then(tools => {
+        let istTolls = {};
+        Object.keys(tools).filter(tool => {
+            if (tools[tool] && tools[tool].installed) {
+                istTolls[tool] = tools[tool];
+            }
+        });
+        return istTolls;
+    });
+
+    /**
+     * 获取工具的content-script
+     * @param toolName
+     * @param cssMode
+     */
+    let getContentScript = (toolName, cssMode) => {
+        return StorageMgr.get(cssMode ? TOOL_CONTENT_SCRIPT_CSS_TPL.replace('#TOOL-NAME#', toolName)
+            : TOOL_CONTENT_SCRIPT_TPL.replace('#TOOL-NAME#', toolName));
+    };
+
+    /**
+     * 获取工具的html模板
+     * @param toolName
+     * @returns {*}
+     */
+    let getToolTpl = (toolName) => StorageMgr.get(TOOL_NAME_TPL.replace('#TOOL-NAME#', toolName));
+
+    /**
+     * 从服务器检查,看本地已安装的工具,有哪些又已经升级过了
+     * @param tool
+     */
+    let checkUpgrade = (tool) => {
+        let getOnline = (toolName) => fetch(URL_TOOL_TPL.replace('#TOOL-NAME#', toolName)).then(resp => resp.text());
+        let getOffline = (toolName) => StorageMgr.get(TOOL_NAME_TPL.replace('#TOOL-NAME#', toolName));
+        return Promise.all([getOnline(tool), getOffline(tool)]).then(values => {
+            let onlineData = _tplHandler(tool, values[0]);
+            let local = values[1];
+            return local !== onlineData.html;
+        });
+    };
+
+    /**
+     * 管理右键菜单
+     * @param toolName
+     * @param action 具体动作install/offload/get
+     * @returns {Promise<any>}
+     */
+    let menuMgr = (toolName, action) => {
+        let menuKey = TOOL_MENU_TPL.replace('#TOOL-NAME#', toolName);
+        switch (action) {
+            case 'get':
+                return StorageMgr.get(menuKey);
+            case 'offload':
+                // 必须用setItem模式,而不是removeItem,要处理 0/1/null三种结果
+                log(toolName + ' 卸载成功!');
+                return StorageMgr.set(menuKey, 0);
+            case 'install':
+                log(toolName + ' 安装成功!');
+                return StorageMgr.set(menuKey, 1);
+        }
+    };
+
+    /**
+     * 远程获取的代码管理器
+     * @type {{get, set}}
+     */
+    let CodeCacheMgr = (() => {
+        const TOOLS_FROM_REMOTE = 'TOOLS_FROM_REMOTE';
+
+        let get = () => {
+            return StorageMgr.getSync(TOOLS_FROM_REMOTE);
+        };
+
+        let set = (remoteCodes) => {
+            let obj = {};
+            obj[TOOLS_FROM_REMOTE]=remoteCodes;
+            chrome.storage.local.set(obj);
+        };
+
+        return {get, set};
+    })();
+
+    /**
+     * 工具排序管理器
+     * @type {{get, set}}
+     */
+    let SortToolMgr = (() => {
+        const TOOLS_CUSTOM_SORT = 'TOOLS_CUSTOM_SORT';
+
+        let get = async () => {
+            let cache = await StorageMgr.getSync(TOOLS_CUSTOM_SORT);
+
+            return [].concat(JSON.parse(cache || '[]')).filter(t => !!t);
+        };
+
+        let set = (newSortArray) => {
+            let obj = {};
+            obj[TOOLS_CUSTOM_SORT] = JSON.stringify([].concat(newSortArray || []).filter(t => !!t));
+            chrome.storage.local.set(obj);
+        };
+
+        return {get, set};
+    })();
+
+
+    return {
+        StorageMgr,
+        detectInstall,
+        install,
+        offLoad,
+        getInstalledTools,
+        menuMgr,
+        checkUpgrade,
+        getContentScript,
+        getToolTpl,
+        gcLocalFiles,
+        getAllTools,
+        SortToolMgr,
+        CodeCacheMgr
+    }
+})();
+
+export default Awesome;

+ 502 - 0
apps/background/background.js

@@ -0,0 +1,502 @@
+/**
+ * FeJson后台运行程序
+ * @author zhaoxianlie
+ */
+
+
+import MSG_TYPE from '../static/js/common.js';
+import Settings from '../options/settings.js';
+import Menu from './menu.js';
+import Awesome from './awesome.js';
+import InjectTools from './inject-tools.js';
+import Monkey from './monkey.js';
+
+
+let BgPageInstance = (function () {
+
+    let FeJson = {
+        notifyTimeoutId: -1
+    };
+
+    // 黑名单页面
+    let blacklist = [
+        /^https:\/\/chrome\.google\.com/
+    ];
+
+    /**
+     * 文本格式,可以设置一个图标和标题
+     * @param {Object} options
+     * @config {string} type notification的类型,可选值:html、text
+     * @config {string} icon 图标
+     * @config {string} title 标题
+     * @config {string} message 内容
+     */
+    let notifyText = function (options) {
+        let notifyId = 'FeJson-notify-id';
+
+        clearTimeout(FeJson.notifyTimeoutId);
+        if (options.closeImmediately) {
+            return chrome.notifications.clear(notifyId);
+        }
+
+        if (!options.icon) {
+            options.icon = "static/img/fe-48.png";
+        }
+        if (!options.title) {
+            options.title = "温馨提示";
+        }
+        chrome.notifications.create(notifyId, {
+            type: 'basic',
+            title: options.title,
+            iconUrl: chrome.runtime.getURL(options.icon),
+            message: options.message
+        });
+
+        FeJson.notifyTimeoutId = setTimeout(() => {
+            chrome.notifications.clear(notifyId);
+        }, parseInt(options.autoClose || 3000, 10));
+
+    };
+
+    // 像页面注入css脚本
+    let _injectContentCss = function(tabId,toolName,isDevTool){
+        if(isDevTool){
+            Awesome.getContentScript(toolName, true)
+                .then(css => {
+                    InjectTools.inject(tabId, { css })
+                });
+        }else{
+            InjectTools.inject(tabId, {files: [`${toolName}/content-script.css`]});
+        }
+    };
+
+
+    // 往当前页面直接注入脚本,不再使用content-script的配置了
+    let _injectContentScripts = function (tabId) {
+
+        // FH工具脚本注入
+        Awesome.getInstalledTools().then(tools => {
+
+            // 注入js
+            let jsTools = Object.keys(tools)
+                        .filter(tool => !tools[tool]._devTool
+                                && (tools[tool].contentScriptJs || tools[tool].contentScript));
+            let jsCodes = [];
+            jsTools.forEach((t, i) => {
+                let func = `window['${t.replace(/-/g, '')}ContentScript']`;
+                jsCodes.push(`(()=>{let func=${func};func&&func();})()`);
+            });
+            let jsFiles = jsTools.map(tool => `${tool}/content-script.js`);
+            InjectTools.inject(tabId, {files: jsFiles,js: jsCodes.join(';')});
+        });
+
+        // 其他开发者自定义工具脚本注入======For FH DevTools
+        Awesome.getInstalledTools().then(tools => {
+            let list = Object.keys(tools).filter(tool => tools[tool]._devTool);
+
+            // 注入js脚本
+            list.filter(tool => (tools[tool].contentScriptJs || tools[tool].contentScript))
+                    .map(tool => Awesome.getContentScript(tool).then(js => {
+                        InjectTools.inject(tabId, { js });
+                    }));
+        });
+    };
+
+    /**
+     * 动态运行工具
+     * @param configs
+     * @config tool 工具名称
+     * @config withContent 默认携带的内容
+     * @config query 请求参数
+     * @config noPage 无页面模式
+     * @constructor
+     */
+    chrome.DynamicToolRunner = async function (configs) {
+
+        let tool = configs.tool || configs.page;
+        let withContent = configs.withContent;
+        let activeTab = null;
+        let query = configs.query;
+
+        // 如果是noPage模式,则表名只完成content-script的工作,直接发送命令即可
+        if (configs.noPage) {
+            let toolFunc = tool.replace(/-/g, '');
+            chrome.tabs.query({active: true, currentWindow: true}, tabs => {
+                let found = tabs.some(tab => {
+                    if (/^(http(s)?|file):\/\//.test(tab.url) && blacklist.every(reg => !reg.test(tab.url))) {
+                        let codes = `window['${toolFunc}NoPage'] && window['${toolFunc}NoPage'](${JSON.stringify(tab)});`;
+                        InjectTools.inject(tab.id, {js: codes});
+                        return true;
+                    }
+                    return false;
+                });
+                if (!found) {
+                    notifyText({
+                        message: '抱歉,此工具无法在当前页面使用!'
+                    });
+                }
+            });
+            return;
+        }
+
+        chrome.tabs.query({currentWindow: true}, function (tabs) {
+
+            activeTab = tabs.filter(tab => tab.active)[0];
+
+            Settings.getOptions((opts) => {
+                let isOpened = false;
+                let tabId;
+
+                // 允许在新窗口打开
+                if (String(opts['FORBID_OPEN_IN_NEW_TAB']) === 'true') {
+                    let reg = new RegExp("^chrome.*\\/" + tool + "\\/index.html" + (query ? "\\?" + query : '') + "$", "i");
+                    for (let i = 0, len = tabs.length; i < len; i++) {
+                        if (reg.test(tabs[i].url)) {
+                            isOpened = true;
+                            tabId = tabs[i].id;
+                            break;
+                        }
+                    }
+                }
+
+                if (!isOpened) {
+                    let url = `/${tool}/index.html` + (query ? "?" + query : '');
+                    chrome.tabs.create({
+                        url,
+                        active: true
+                    }).then(tab => { FeJson[tab.id] = { content: withContent }; });
+                } else {
+                    chrome.tabs.update(tabId, {highlighted: true}).then(tab => {
+                        FeJson[tab.id] = { content: withContent };
+                        chrome.tabs.reload(tabId);
+                    });
+                }
+
+            });
+
+        });
+    };
+
+    /**
+     * 动态在icon处显示提示
+     * @param tips
+     * @private
+     */
+    let _animateTips = (tips) => {
+        setTimeout(() => {
+            chrome.action.setBadgeText({text: tips});
+            setTimeout(() => {
+                chrome.action.setBadgeText({text: ''});
+            }, 2000);
+        }, 3000);
+    };
+
+    /**
+     * 插件图标点击后的默认动作
+     * @param request
+     * @param sender
+     * @param callback
+     */
+    let browserActionClickedHandler = function (request, sender, callback) {
+        chrome.DynamicToolRunner({
+            tool: MSG_TYPE.JSON_FORMAT
+        });
+    };
+
+    /**
+     * 更新browser action的点击动作
+     * @param action install / upgrade / offload
+     * @param showTips 是否notify
+     * @param menuOnly 只管理Menu
+     * @private
+     */
+    let _updateBrowserAction = function (action, showTips, menuOnly) {
+        if (!menuOnly) {
+            // 如果有安装过工具,则显示Popup模式
+            Awesome.getInstalledTools().then(tools => {
+                if (Object.keys(tools).length > 1) {
+                    chrome.action.setPopup({ popup: '/popup/index.html' });
+                } else {
+                    // 删除popup page
+                    chrome.action.setPopup({ popup: '' });
+
+                    // 否则点击图标,直接打开页面
+                    if (!chrome.action.onClicked.hasListener(browserActionClickedHandler)) {
+                        chrome.action.onClicked.addListener(browserActionClickedHandler);
+                    }
+                }
+            });
+
+            if (action === 'offload') {
+                _animateTips('-1');
+            } else if(!!action) {
+                _animateTips('+1');
+            }
+        } else {
+            // 重绘菜单
+            Menu.rebuild();
+        }
+
+        if (showTips) {
+            let actionTxt = '';
+            switch (action) {
+                case 'install':
+                    actionTxt = '工具已「安装」成功,并已添加到弹出下拉列表,点击FeHelper图标可正常使用!';
+                    break;
+                case 'offload':
+                    actionTxt = '工具已「卸载」成功,并已从弹出下拉列表中移除!';
+                    break;
+                case 'menu-install':
+                    actionTxt = '已将此工具快捷方式加入到「右键菜单」中!';
+                    break;
+                case 'menu-offload':
+                    actionTxt = '已将此工具快捷方式从「右键菜单」中移除!';
+                    break;
+                default:
+                    actionTxt = '恭喜,操作成功!';
+            }
+            notifyText({
+                message: actionTxt,
+                autoClose: 2500
+            });
+        }
+    };
+
+
+    // 捕获当前页面可视区域
+    let _captureVisibleTab = function (callback) {
+        chrome.tabs.captureVisibleTab(null, {format: 'png', quality: 100}, uri => {
+            callback && callback(uri);
+        });
+    };
+
+    let _addScreenShotByPages = function(params,callback){
+        chrome.tabs.captureVisibleTab(null, {format: 'png', quality: 100}, uri => {
+            callback({ params,uri });
+        });
+    };
+
+    let _showScreenShotResult = function(data){
+        chrome.DynamicToolRunner({
+            tool: 'screenshot',
+            withContent: data
+        });
+    };
+
+    let _colorPickerCapture = function(params) {
+        chrome.tabs.query({active: true, currentWindow: true}, function (tabs) {
+            chrome.tabs.captureVisibleTab(null, {format: 'png'}, function (dataUrl) {
+                let js = `window.colorpickerNoPage(${JSON.stringify({
+                    setPickerImage: true,
+                    pickerImage: dataUrl
+                })})`;
+                InjectTools.inject(tabs[0].id, { js });
+            });
+        });
+    };
+
+    let _codeBeautify = function(params){
+        if (['javascript', 'css'].includes(params.fileType)) {
+            Awesome.StorageMgr.get('JS_CSS_PAGE_BEAUTIFY').then(val => {
+                if(val !== '0') {
+                    let js = `window._codebutifydetect_('${params.fileType}')`;
+                    InjectTools.inject(params.tabId, { js });
+                }
+            });
+        }
+    };
+
+    /**
+     * 接收来自content_scripts发来的消息
+     */
+    let _addExtensionListener = function () {
+
+        _updateBrowserAction();
+
+        chrome.runtime.onMessage.addListener(function (request, sender, callback) {
+            // 如果发生了错误,就啥都别干了
+            if (chrome.runtime.lastError) {
+                console.log('chrome.runtime.lastError:',chrome.runtime.lastError);
+                return true;
+            }
+
+            // 动态安装工具或者卸载工具,需要更新browserAction
+            if (request.type === MSG_TYPE.DYNAMIC_TOOL_INSTALL_OR_OFFLOAD) {
+                _updateBrowserAction(request.action, request.showTips, request.menuOnly);
+                callback && callback();
+            }
+            // 截屏
+            else if (request.type === MSG_TYPE.CAPTURE_VISIBLE_PAGE) {
+                _captureVisibleTab(callback);
+            }
+            // 打开动态工具页面
+            else if (request.type === MSG_TYPE.OPEN_DYNAMIC_TOOL) {
+                chrome.DynamicToolRunner(request);
+                callback && callback();
+            }
+            // 打开其他页面
+            else if (request.type === MSG_TYPE.OPEN_PAGE) {
+                chrome.DynamicToolRunner({
+                    tool: request.page
+                });
+                callback && callback();
+            }
+            // 任何事件,都可以通过这个钩子来完成
+            else if (request.type === MSG_TYPE.DYNAMIC_ANY_THING) {
+                switch(request.thing){
+                    case 'save-options':
+                        notifyText({
+                            message: '配置修改已生效,请继续使用!',
+                            autoClose: 2000
+                        });
+                        break;
+                    case 'request-jsonformat-options':
+                        Awesome.StorageMgr.get(request.params).then(result => {
+                            Object.keys(result).forEach(key => {
+                                if (['MAX_JSON_KEYS_NUMBER', 'JSON_FORMAT_THEME'].includes(key)) {
+                                    result[key] = parseInt(result[key]);
+                                } else {
+                                    result[key] = (result[key] !== 'false');
+                                }
+                            });
+                            callback && callback(result);
+                        });
+                        return true; // 这个返回true是非常重要的!!!要不然callback会拿不到结果
+                    case 'save-jsonformat-options':
+                        Awesome.StorageMgr.set(request.params).then(() => {
+                            callback && callback();
+                        });
+                        return true;
+                    case 'toggle-jsonformat-options':
+                        Awesome.StorageMgr.get('JSON_TOOL_BAR_ALWAYS_SHOW').then(result => {
+                            let show = result !== false;
+                            Awesome.StorageMgr.set('JSON_TOOL_BAR_ALWAYS_SHOW',!show).then(() => {
+                                callback && callback(!show);
+                            });
+                        });
+                        return true; // 这个返回true是非常重要的!!!要不然callback会拿不到结果
+                    case 'code-beautify':
+                        _codeBeautify(request.params);
+                        break;
+                    case 'close-beautify':
+                        Awesome.StorageMgr.set('JS_CSS_PAGE_BEAUTIFY',0);
+                        break;
+                    case 'qr-decode':
+                        chrome.DynamicToolRunner({
+                            withContent: request.params.uri,
+                            tool: 'qr-code',
+                            query: `mode=decode`
+                        });
+                        break;
+                    case 'request-page-content':
+                        request.params = FeJson[request.tabId];
+                        delete FeJson[request.tabId];
+                        break;
+                    case 'set-page-timing-data':
+                        chrome.DynamicToolRunner({
+                            tool: 'page-timing',
+                            withContent: request.wpoInfo
+                        });
+                        break;
+                    case 'color-picker-capture':
+                        _colorPickerCapture(request.params);
+                        break;
+                    case 'add-screen-shot-by-pages':
+                        _addScreenShotByPages(request.params,callback);
+                        return true;
+                    case 'page-screenshot-done':
+                        _showScreenShotResult(request.params);
+                        break;
+                    case 'request-monkey-start':
+                        Monkey.start(request.params);
+                        break;
+                    case 'inject-content-css':
+                        _injectContentCss(sender.tab.id,request.tool,!!request.devTool);
+                        break;
+                }
+                callback && callback(request.params);
+            } else {
+                callback && callback();
+            }
+
+            return true;
+        });
+
+
+        // 每开一个窗口,都向内容脚本注入一个js,绑定tabId
+        chrome.tabs.onUpdated.addListener(function (tabId, changeInfo, tab) {
+
+            if (String(changeInfo.status).toLowerCase() === "complete") {
+
+                if(/^(http(s)?|file):\/\//.test(tab.url) && blacklist.every(reg => !reg.test(tab.url))){
+                    InjectTools.inject(tabId, { js: `window.__FH_TAB_ID__=${tabId};` });
+                    _injectContentScripts(tabId);
+                }
+            }
+        });
+
+        // 安装与更新
+        chrome.runtime.onInstalled.addListener(({reason, previousVersion}) => {
+            switch (reason) {
+                case 'install':
+                    chrome.runtime.openOptionsPage();
+                    break;
+                case 'update':
+                    _animateTips('+++1');
+                    if (previousVersion === '2019.12.2415') {
+                        notifyText({
+                            message: '历尽千辛万苦,FeHelper已升级到最新版本,可以到插件设置页去安装旧版功能了!',
+                            autoClose: 5000
+                        });
+                    }
+
+                    // 从V2020.02.1413版本开始,本地的数据存储大部分迁移至chrome.storage.local
+                    // 这里需要对老版本升级过来的情况进行强制数据迁移
+                    let getAbsNum = num => parseInt(num.split(/\./).map(n => n.padStart(4, '0')).join(''), 10);
+                    // let preVN = getAbsNum(previousVersion);
+                    // let minVN = getAbsNum('2020.02.1413');
+                    // if (preVN < minVN) {
+                    //     Awesome.makeStorageUnlimited();
+                    //     setTimeout(() => chrome.runtime.reload(), 1000 * 5);
+                    // }
+                    break;
+            }
+        });
+        // 卸载
+        chrome.runtime.setUninstallURL(chrome.runtime.getManifest().homepage_url);
+    };
+
+    /**
+     * 检查插件更新
+     * @private
+     */
+    let _checkUpdate = function () {
+        setTimeout(() => {
+            chrome.runtime.requestUpdateCheck((status) => {
+                if (status === "update_available") {
+                    chrome.runtime.reload();
+                }
+            });
+        }, 1000 * 30);
+    };
+
+    /**
+     * 初始化
+     */
+    let _init = function () {
+        _checkUpdate();
+        _addExtensionListener();
+        Menu.rebuild();
+        // 定期清理冗余的垃圾
+        setTimeout(() => {
+            Awesome.gcLocalFiles();
+        }, 1000 * 10);
+    };
+
+    return {
+        pageCapture: _captureVisibleTab,
+        init: _init
+    };
+})();
+
+BgPageInstance.init();

+ 147 - 0
apps/background/crx-download.js

@@ -0,0 +1,147 @@
+/**
+ * FeHelper:从chrome webstore下载extension文件的工具
+ * @author zhaoxianlie
+ */
+
+import MSG_TYPE from '../static/js/common.js';
+
+export default (function () {
+
+    let FeJson = {notifyTimeoutId:-1};
+    /**
+     * 文本格式,可以设置一个图标和标题
+     * @param {Object} options
+     * @config {string} type notification的类型,可选值:html、text
+     * @config {string} icon 图标
+     * @config {string} title 标题
+     * @config {string} message 内容
+     */
+    let notifyText = function (options) {
+        let notifyId = 'FeJson-notify-id';
+        if(typeof options === 'string') {
+            options = {message: options};
+        }
+        clearTimeout(FeJson.notifyTimeoutId);
+        if (options.closeImmediately) {
+            return chrome.notifications.clear(notifyId);
+        }
+
+        if (!options.icon) {
+            options.icon = "static/img/fe-48.png";
+        }
+        if (!options.title) {
+            options.title = "温馨提示";
+        }
+        chrome.notifications.create(notifyId, {
+            type: 'basic',
+            title: options.title,
+            iconUrl: chrome.runtime.getURL(options.icon),
+            message: options.message
+        });
+
+        FeJson.notifyTimeoutId = setTimeout(() => {
+            chrome.notifications.clear(notifyId);
+        }, parseInt(options.autoClose || 3000, 10));
+
+    };
+
+    /**
+     * 检测Google chrome服务能不能访问,在2s内检测心跳
+     * @param success
+     * @param failure
+     */
+    let detectGoogleDotCom = function (success, failure) {
+        Promise.race([
+            fetch('https://clients2.google.com/service/update2/crx'),
+            new Promise(function (resolve, reject) {
+                setTimeout(() => reject(new Error('request timeout')), 2000)
+            })])
+            .then((data) => {
+                success && success();
+            }).catch(() => {
+            failure && failure();
+        });
+    };
+
+    /**
+     * 从google官方渠道下载chrome扩展
+     * @param crxId 需要下载的extension id
+     * @param crxName 扩展名称
+     * @param callback 下载动作结束后的回调
+     */
+    let downloadCrxFileByCrxId = function (crxId, crxName, callback) {
+        detectGoogleDotCom(() => {
+            // google可以正常访问,则正常下载
+            let url = "https://clients2.google.com/service/update2/crx?response=redirect&acceptformat=crx2,crx3&x=id%3D"
+                + crxId + "%26uc&prodversion=" + navigator.userAgent.split("Chrome/")[1].split(" ")[0];
+            if (!chrome.downloads) {
+                let a = document.createElement('a');
+                a.href = url;
+                a.download = crxName || (crxId + '.crx');
+                (document.body || document.documentElement).appendChild(a);
+                a.click();
+                a.remove();
+            } else {
+                chrome.downloads.download({
+                    url: url,
+                    filename: crxName || crxId,
+                    conflictAction: 'overwrite',
+                    saveAs: true
+                }, function (downloadId) {
+                    if (chrome.runtime.lastError) {
+                        notifyText('抱歉,下载失败!错误信息:' + chrome.runtime.lastError.message);
+                    }
+                });
+            }
+        }, () => {
+            // google不能正常访问
+            callback ? callback() : notifyText('抱歉,下载失败!');
+        });
+
+    };
+
+    /**
+     * 从chrome webstore下载crx文件
+     * 在chrome extension详情页使用
+     */
+    let downloadCrxFileFromWebStoreDetailPage = function (callback) {
+
+        chrome.tabs.query({active: true, currentWindow: true}, function (tabs) {
+            let tab = tabs[0];
+            let crxId = tab.url.split("/")[6].split('?')[0];
+            let crxName = tab.title.split(" - Chrome")[0] + ".crx";
+            crxName = crxName.replace(/[&\/\\:"*<>|?]/g, '');
+
+            downloadCrxFileByCrxId(crxId, crxName, callback);
+        });
+    };
+
+    /**
+     * 通过右键菜单下载或者分享crx
+     * @param tab
+     * @private
+     */
+    let _downloadCrx = function (tab) {
+        let isWebStoreDetailPage = tab.url.indexOf('https://chrome.google.com/webstore/detail/') === 0;
+        if (isWebStoreDetailPage) {
+            // 如果是某个chrome extension的详情页面了,直接下载当前crx文件
+            downloadCrxFileFromWebStoreDetailPage(() => {
+                notifyText('下载失败,可能是当前网络无法访问Google站点!');
+            });
+        } else {
+            // 否则,下载FeHelper并分享出去
+            let crxId = MSG_TYPE.STABLE_EXTENSION_ID;
+            let crxName = chrome.runtime.getManifest().name + '-latestVersion.crx';
+
+            downloadCrxFileByCrxId(crxId, crxName, () => {
+                chrome.tabs.create({
+                    url: MSG_TYPE.DOWNLOAD_FROM_GITHUB
+                });
+            });
+        }
+    };
+
+    return {
+        downloadCrx: _downloadCrx
+    };
+})();

+ 74 - 0
apps/background/inject-tools.js

@@ -0,0 +1,74 @@
+import Settings from '../options/settings.js';
+
+export default (() => {
+    /**
+     * 如果tabId指定的tab还存在,就正常注入脚本
+     * @param tabId 需要注入脚本的tabId
+     * @param codeConfig 需要注入的代码
+     * @param callback 注入代码后的callback
+     */
+    let injectScriptIfTabExists = function (tabId, codeConfig, callback) {
+        chrome.tabs.query({currentWindow: true}, (tabs) => {
+            tabs.some(tab => {
+                if (tab.id !== tabId) return false;
+                Settings.getOptions((opts) => {
+
+                    if (!codeConfig.hasOwnProperty('allFrames')) {
+                        codeConfig.allFrames = String(opts['CONTENT_SCRIPT_ALLOW_ALL_FRAMES']) === 'true';
+                    }
+
+                    codeConfig.js = 'try{' + codeConfig.js + ';}catch(e){};';
+                    // 有文件就注入文件
+                    if(codeConfig.files && codeConfig.files.length){
+                        // 注入样式
+                        if(codeConfig.files.join(',').indexOf('.css') > -1) {
+                            chrome.scripting.insertCSS({
+                                target: {tabId, allFrames: codeConfig.allFrames},
+                                files: codeConfig.files
+                            }, function () {
+                                callback && callback.apply(this, arguments);
+                            });
+                        }
+                        // 注入js
+                        else {
+                            chrome.scripting.executeScript({
+                                target: {tabId, allFrames: codeConfig.allFrames},
+                                files: codeConfig.files
+                            }, function () {
+                                chrome.scripting.executeScript({
+                                    target: {tabId, allFrames: codeConfig.allFrames},
+                                    func: function(code){try{evalCore.getEvalInstance(window)(code)}catch(x){}},
+                                    args: [codeConfig.js]
+                                }, function () {
+                                    callback && callback.apply(this, arguments);
+                                });
+                            });
+                        }
+                    }else if(codeConfig.css){
+                        // 注入css样式
+                        chrome.scripting.executeScript({
+                            target: {tabId, allFrames: codeConfig.allFrames},
+                            css:codeConfig.css
+                        }, function () {
+                            callback && callback.apply(this, arguments);
+                        });
+                    }else{
+                        // 注入js脚本
+                        chrome.scripting.executeScript({
+                            target: {tabId, allFrames: codeConfig.allFrames},
+                            func: function(code){try{evalCore.getEvalInstance(window)(code)}catch(x){}},
+                            args: [codeConfig.js]
+                        }, function () {
+                            callback && callback.apply(this, arguments);
+                        });
+                    }
+
+                });
+
+                return true;
+            });
+        });
+    };
+
+    return { inject: injectScriptIfTabExists };
+})();

+ 236 - 0
apps/background/menu.js

@@ -0,0 +1,236 @@
+/**
+ * FeHelper 右键菜单管理
+ * @type {{manage}}
+ * @author zhaoxianlie
+ */
+
+import CrxDownloader from './crx-download.js';
+import Awesome from './awesome.js';
+import toolMap from './tools.js';
+import Settings from '../options/settings.js';
+
+export default (function () {
+
+    let FeJson = {
+        contextMenuId:"fhm_main"
+    };
+
+    // 邮件菜单配置项
+    let defaultMenuOptions = {
+        'download-crx': {
+            icon: '♥',
+            text: '插件下载分享',
+            onClick: function (info, tab) {
+                CrxDownloader.downloadCrx(tab);
+            }
+        },
+        'fehelper-setting': {
+            icon: '❂',
+            text: 'FeHelper设置',
+            onClick: function (info, tab) {
+                chrome.runtime.openOptionsPage();
+            }
+        }
+    };
+
+    // 初始化菜单配置
+    let _initMenuOptions = (() => {
+
+        Object.keys(toolMap).forEach(tool => {
+            // context-menu
+            switch (tool) {
+                case 'json-format':
+                    toolMap[tool].menuConfig[0].onClick = function (info, tab) {
+                        chrome.scripting.executeScript({
+                            target: {tabId:tab.id,allFrames:false},
+                            args: [info.selectionText || ''],
+                            func: (text) => text
+                        }, resp => chrome.DynamicToolRunner({
+                            tool, withContent: resp[0].result
+                        }));
+                    };
+                    break;
+
+                case 'code-beautify':
+                case 'en-decode':
+                    toolMap[tool].menuConfig[0].onClick = function (info, tab) {
+                        chrome.scripting.executeScript({
+                            target: {tabId:tab.id,allFrames:false},
+                            args: [info.linkUrl || info.srcUrl || info.selectionText || info.pageUrl || ''],
+                            func: (text) => text
+                        }, resp => chrome.DynamicToolRunner({
+                            tool, withContent: resp[0].result
+                        }));
+                    };
+                    break;
+
+                case 'qr-code':
+                    toolMap[tool].menuConfig[0].onClick = function (info, tab) {
+                        chrome.scripting.executeScript({
+                            target: {tabId:tab.id,allFrames:false},
+                            args: [info.linkUrl || info.srcUrl || info.selectionText || info.pageUrl || tab.url || ''],
+                            func: (text) => text
+                        }, resp => chrome.DynamicToolRunner({
+                            tool, withContent: resp[0].result
+                        }));
+                    };
+                    toolMap[tool].menuConfig[1].onClick = function (info, tab) {
+                        chrome.scripting.executeScript({
+                            target: {tabId:tab.id,allFrames:false},
+                            args: [info.srcUrl || ''],
+                            func: (text) => {
+                                try {
+                                    if (typeof window.qrcodeContentScript === 'function') {
+                                        let qrcode = window.qrcodeContentScript();
+                                        if (typeof qrcode.decode === 'function') {
+                                            qrcode.decode(text);
+                                            return 1;
+                                        }
+                                    }
+                                } catch (e) {
+                                    return 0;
+                                }
+                            }
+                        });
+                    };
+                    break;
+
+                default:
+                    toolMap[tool].menuConfig[0].onClick = function (info, tab) {
+                        chrome.DynamicToolRunner({
+                            tool, withContent: tool === 'image-base64' ? info.srcUrl : ''
+                        })
+                    };
+                    break;
+            }
+        });
+    })();
+
+    /**
+     * 创建一个menu 菜单
+     * @param toolName
+     * @param menuList
+     * @returns {boolean}
+     * @private
+     */
+    let _createItem = (toolName, menuList) => {
+        menuList && menuList.forEach && menuList.forEach(menu => {
+
+            // 确保每次创建出来的是一个新的主菜单,防止onClick事件冲突
+            let menuItemId = 'fhm_c' + escape(menu.text).replace(/\W/g,'') + new Date*1;
+
+            chrome.contextMenus.create({
+                id: menuItemId,
+                title: menu.icon + '  ' + menu.text,
+                contexts: menu.contexts || ['all'],
+                parentId: FeJson.contextMenuId
+            });
+
+            chrome.contextMenus.onClicked.addListener(((tool,mId,mFunc) => (info, tab) => {
+                if(info.menuItemId === mId) {
+                    if(mFunc) {
+                        mFunc(info,tab);
+                    }else{
+                        chrome.DynamicToolRunner({ tool });
+                    }
+                }
+            })(toolName,menuItemId,menu.onClick));
+        });
+    };
+
+
+    /**
+     * 绘制一条分割线
+     * @private
+     */
+    let _createSeparator = function () {
+        chrome.contextMenus.create({
+            id: 'fhm_s' + Math.ceil(Math.random()*10e9),
+            type: 'separator',
+            parentId: FeJson.contextMenuId
+        });
+    };
+
+    /**
+     * 创建扩展专属的右键菜单
+     */
+    let _initMenus = function () {
+        _removeContextMenu(() => {
+            let id = chrome.contextMenus.create({
+                id: FeJson.contextMenuId ,
+                title: "FeHelper",
+                contexts: ['page', 'selection', 'editable', 'link', 'image'],
+                documentUrlPatterns: ['http://*/*', 'https://*/*', 'file://*/*']
+            });
+
+            // 绘制用户安装的菜单,放在前面
+            Awesome.getInstalledTools().then(tools => {
+                let allMenus = Object.keys(tools).filter(tool => tools[tool].installed && tools[tool].menu);
+                let onlineTools = allMenus.filter(tool => tool !== 'devtools' && !tools[tool].hasOwnProperty('_devTool'));
+                let devTools = allMenus.filter(tool => tool === 'devtools' || tools[tool].hasOwnProperty('_devTool'));
+
+                // 绘制FH提供的工具菜单
+                onlineTools.forEach(tool => _createItem(tool, tools[tool].menuConfig));
+                // 如果有本地工具的菜单需要绘制,则需要加一条分割线
+                devTools.length && _createSeparator();
+                // 绘制本地工具的菜单
+                devTools.forEach(tool => {
+                    // 说明是自定义工具 构造menuConfig
+                    if(!tools[tool].menuConfig) {
+                        tools[tool].menuConfig = [{
+                            icon: tools[tool].icon,
+                            text: tools[tool].name,
+                            onClick: (info, tab) => {
+                                chrome.DynamicToolRunner({
+                                    page: 'dynamic',
+                                    noPage: !!tools[tool].noPage,
+                                    query: `tool=${tool}`
+                                });
+                                !!tools[tool].noPage && setTimeout(window.close, 200);
+                            }
+                        }];
+                    }
+                    _createItem(tool, tools[tool].menuConfig)
+                });
+              });
+
+            // 绘制两个系统提供的菜单,放到最后
+            let sysMenu = ['download-crx', 'fehelper-setting'];
+            let arrPromises = sysMenu.map(menu => Awesome.menuMgr(menu, 'get'));
+            Promise.all(arrPromises).then(values => {
+                let needDraw = String(values[0]) === '1' || String(values[1]) !== '0';
+
+                // 绘制一条分割线
+                _createSeparator();
+
+                // 绘制菜单
+                String(values[0]) === '1' && _createItem(sysMenu[0], [defaultMenuOptions[sysMenu[0]]]);
+                String(values[1]) !== '0' && _createItem(sysMenu[1], [defaultMenuOptions[sysMenu[1]]]);
+            });
+        });
+    };
+
+    /**
+     * 移除扩展专属的右键菜单
+     */
+    let _removeContextMenu = function (callback) {
+        chrome.contextMenus.removeAll(callback);
+    };
+
+    /**
+     * 创建或移除扩展专属的右键菜单
+     */
+    let _createOrRemoveContextMenu = function () {
+        Settings.getOptions((opts) => {
+            if (String(opts['OPT_ITEM_CONTEXTMENUS']) !== 'false') {
+                _initMenus();
+            } else {
+                _removeContextMenu();
+            }
+        });
+    };
+
+    return {
+        rebuild: _createOrRemoveContextMenu
+    };
+})();

+ 100 - 0
apps/background/monkey.js

@@ -0,0 +1,100 @@
+
+import InjectTools from './inject-tools.js';
+
+export default (() => {
+    let start = (params) => {
+        try {
+            const PAGE_MONKEY_LOCAL_STORAGE_KEY = 'PAGE-MODIFIER-LOCAL-STORAGE-KEY';
+
+            let handler = monkeys => {
+                monkeys.filter(cm => !cm.mDisabled).forEach(cm => {
+                    let result = null;
+                    let matched = cm.mPattern.match(/^\/(.*)\/(.*)?$/);
+                    if (matched && ( !matched[2] || matched[2] && !/[^igm]*/i.test(matched[2]))) {
+                        // 正则,直接匹配
+                        cm.mPattern = new RegExp(matched[1], matched[2] || "");
+                        result = cm.mPattern.test(params.url) && cm;
+                    } else if (cm.mPattern.indexOf('*') > -1) {
+                        if (cm.mPattern.startsWith('*://')) {
+                            cm.mPattern = cm.mPattern.replace('*://', '(http|https|file)://');
+                        } else if (cm.mPattern.indexOf('://') < 0) {
+                            cm.mPattern = '(http|https|file)://' + cm.mPattern;
+                        }
+
+                        // 通配符,则转换为正则再匹配
+                        cm.mPattern = new RegExp('^' + cm.mPattern.replace(/\./g,'\\.').replace(/\//g, '\\/').replace(/\*/g, '.*').replace(/\?/g, '\\?') + '$');
+                        result = cm.mPattern.test(params.url) && cm;
+                    } else {
+                        // 绝对地址,直接比对
+                        let arr = [cm.mPattern, `${cm.mPattern}/`];
+                        if (!cm.mPattern.startsWith('http://') && !cm.mPattern.startsWith('https://')) {
+                            arr = arr.concat([`http://${cm.mPattern}`, `http://${cm.mPattern}/`,
+                                `https://${cm.mPattern}`, `https://${cm.mPattern}/`]);
+                        }
+                        if (arr.includes(params.url)) {
+                            result = cm;
+                        }
+                    }
+
+                    if (result) {
+                        let scripts = '(' + ((monkey) => {
+                            let injectFunc = () => {
+                                // 执行脚本
+                                try{evalCore.getEvalInstance(window)(monkey.mScript)}catch(x){}
+
+                                parseInt(monkey.mRefresh) && setTimeout(() => {
+                                    location.reload(true);
+                                }, parseInt(monkey.mRefresh) * 1000);
+                            };
+
+                            window._fhImportJs = js => {
+                                return fetch(js).then(resp => resp.text()).then(jsText => {
+                                    if(window.evalCore && window.evalCore.getEvalInstance){
+                                         return window.evalCore.getEvalInstance(window)(jsText);
+                                    }
+                                    let el = document.createElement('script');
+                                    el.textContent = jsText;
+                                    document.head.appendChild(el);
+                                });
+                            };
+
+                            let jsFiles = (monkey.mRequireJs || '').split(/[\s,,]+/).filter(js => js.length);
+                            if (jsFiles.length) {
+                                let arrPromise = Array.from(new Set(jsFiles)).map(js => window._fhImportJs(js));
+                                Promise.all(arrPromise).then(injectFunc);
+                            } else {
+                                injectFunc();
+                            }
+                        }).toString() + `)(${JSON.stringify(result)})`;
+                        InjectTools.inject(params.tabId, {js: scripts, allFrames: false});
+                    }
+                });
+            };
+
+            chrome.storage.local.get(PAGE_MONKEY_LOCAL_STORAGE_KEY, (resps) => {
+                let cacheMonkeys, storageMode = false;
+                if (!resps || !resps[PAGE_MONKEY_LOCAL_STORAGE_KEY]) {
+                    cacheMonkeys = localStorage.getItem(PAGE_MONKEY_LOCAL_STORAGE_KEY) || '[]';
+                    storageMode = true;
+                } else {
+                    cacheMonkeys = resps[PAGE_MONKEY_LOCAL_STORAGE_KEY] || '[]';
+                }
+
+                params && params.url && handler(JSON.parse(cacheMonkeys));
+
+                // 本地存储的内容,需要全部迁移到chrome.storage.local中,以确保unlimitedStorage
+                if (storageMode) {
+                    let storageData = {};
+                    storageData[PAGE_MONKEY_LOCAL_STORAGE_KEY] = cacheMonkeys;
+                    chrome.storage.local.set(storageData);
+                }
+            });
+
+        } catch (e) {
+            console.log('monkey error',e);
+        }
+        return true;
+    };
+
+    return {start};
+})();

+ 253 - 0
apps/background/tools.js

@@ -0,0 +1,253 @@
+let toolMap = {
+    'json-format': {
+        name: 'JSON美化工具',
+        tips: '页面自动检测并格式化、手动格式化、乱码解码、排序、BigInt、编辑、下载、皮肤定制等',
+        contentScriptJs: true,
+        contentScriptCss: true,
+        systemInstalled: true,
+        menuConfig: [{
+            icon: '⒥',
+            text: 'JSON格式化',
+            contexts: ['page', 'selection', 'editable']
+        }]
+    },
+    'json-diff': {
+        name: 'JSON比对工具',
+        tips: '支持两个JSON内容的自动键值比较,并高亮显示差异点,同时也能判断JSON是否合法',
+        menuConfig: [{
+            icon: '☷',
+            text: 'JSON比对器'
+        }]
+    },
+    'qr-code': {
+        name: '二维码/解码',
+        tips: '支持自定义颜色和icon的二维码生成器,并且支持多种模式的二维码解码,包括截图后粘贴解码',
+        contentScriptJs: true,
+        menuConfig: [{
+            icon: '▣',
+            text: '二维码生成器',
+            contexts: ['page', 'selection', 'editable', 'link', 'image']
+        }, {
+            icon: '◈',
+            text: '二维码解码器',
+            contexts: ['image']
+        }]
+    },
+    'image-base64': {
+        name: '图片转Base64',
+        tips: '支持多种模式的图片转Base64格式,比如链接粘贴/截图粘贴等,也支持Base64数据逆转图片',
+        menuConfig: [{
+            icon: '▤',
+            text: '图片与base64',
+            contexts: ['image']
+        }]
+    },
+    'en-decode': {
+        name: '信息编码转换',
+        tips: '支持多格式的信息编解码,如Unicode、UTF-8、UTF-16、URL、Base64、MD5、Hex、Gzip等',
+        menuConfig: [{
+            icon: '♨',
+            text: '字符串编解码',
+            contexts: ['page', 'selection', 'editable']
+        }]
+    },
+    'code-beautify': {
+        name: '代码美化工具',
+        tips: '支持多语言的代码美化,包括 Javascript、CSS、HTML、XML、SQL,且会陆续支持更多格式',
+        contentScriptJs: true,
+        contentScriptCss: true,
+        menuConfig: [{
+            icon: '✡',
+            text: '代码美化工具',
+            contexts: ['page', 'selection', 'editable']
+        }]
+    },
+    'code-compress': {
+        name: '代码压缩工具',
+        tips: 'Web开发用,提供简单的代码压缩功能,支持HTML、Javascript、CSS代码压缩',
+        menuConfig: [{
+            icon: '♯',
+            text: '代码压缩工具'
+        }]
+    },
+    'aiagent': {
+        name: 'AI,请帮帮忙',
+        tips: '由AI强力支撑的超智能对话工具,可以让它帮你写代码、改代码、做方案设计、查资料、做分析等',
+        menuConfig: [{
+            icon: '֍',
+            text: 'AI(智能助手)'
+        }]
+    },
+    'timestamp': {
+        name: '时间(戳)转换',
+        tips: '本地化时间与时间戳之间的相互转换,支持秒/毫秒、支持世界时区切换、各时区时钟展示等',
+        menuConfig: [{
+            icon: '♖',
+            text: '时间(戳)转换'
+        }]
+    },
+    'password': {
+        name: '随机密码生成',
+        tips: '将各种字符进行随机组合生成密码,可以由数字、大小写字母、特殊符号组成,支持指定长度',
+        menuConfig: [{
+            icon: '♆',
+            text: '随机密码生成'
+        }]
+    },
+    'sticky-notes': {
+        name: '我的便签笔记',
+        tips: '方便快捷的浏览器便签笔记工具,支持创建目录对笔记进行分类管理,笔记支持一键导出/导入',
+        menuConfig: [{
+            icon: '✐',
+            text: '我的便签笔记'
+        }]
+    },
+    'html2markdown': {
+        name: 'Markdown转换',
+        tips: 'Markdown编写/预览工具,支持HTML片段直接转Markdown,支持将内容以PDF格式进行下载',
+        menuConfig: [{
+            icon: 'ⓜ',
+            text: 'markown工具'
+        }]
+    },
+    'postman': {
+        name: '简易Postman',
+        tips: '开发过程中的接口调试工具,支持GET/POST/HEAD请求方式,且支持JSON内容自动格式化',
+        menuConfig: [{
+            icon: '☯',
+            text: '简易Postman'
+        }]
+    },
+    'websocket': {
+        name: 'Websocket工具',
+        tips: '支持对Websocket接口的抓包测试,包括ws服务的连接测试、消息发送测试、结果分析等',
+        menuConfig: [{
+            icon: 'ⓦ',
+            text: 'Websocket工具'
+        }]
+    },
+    'regexp': {
+        name: 'JS正则表达式',
+        tips: '正则校验工具,默认提供一些工作中常用的正则表达式,支持内容实时匹配并高亮显示结果',
+        menuConfig: [{
+            icon: '✙',
+            text: 'JS正则表达式'
+        }]
+    },
+    'trans-radix': {
+        name: '进制转换工具',
+        tips: '支持2进制到36进制数据之间的任意转换,比如:10进制转2进制,8进制转16进制,等等',
+        menuConfig: [{
+            icon: '❖',
+            text: '进制转换工具'
+        }]
+    },
+    'trans-color': {
+        name: '颜色转换工具',
+        tips: '支持HEX颜色到RGB格式的互转,比如HEX颜色「#43ad7f」转RGB后为「rgb(67, 173, 127)」',
+        menuConfig: [{
+            icon: '▶',
+            text: '颜色转换工具'
+        }]
+    },
+    'crontab': {
+        name: 'Crontab工具',
+        tips: '一个简易的Crontab生成工具,支持随机生成Demo,编辑过程中,分时日月周会高亮提示',
+        menuConfig: [{
+            icon: '½',
+            text: 'Crontab工具'
+        }]
+    },
+    'loan-rate': {
+        name: '贷(还)款利率',
+        tips: '贷款或还款利率的计算器,按月呈现还款计划;并支持按还款额反推贷款实际利率',
+        menuConfig: [{
+            icon: '$',
+            text: '贷(还)款利率'
+        }]
+    },
+    'devtools': {
+        name: 'FH开发者工具',
+        tips: '以开发平台的思想,FeHelper支持用户进行本地开发,将自己的插件功能集成进FH工具市场',
+        menuConfig: [{
+            icon: '㉿',
+            text: 'FH开发者工具'
+        }]
+    },
+    'page-monkey': {
+        name: '网页油猴工具',
+        tips: '自行配置页面匹配规则、编写Hack脚本,实现网页Hack,如页面自动刷新、自动抢票等',
+        contentScriptJs: true,
+        menuConfig: [{
+            icon: '♀',
+            text: '网页油猴工具'
+        }]
+    },
+    'screenshot': {
+        name: '网页截屏工具',
+        tips: '可对任意网页进行截屏,支持可视区域截屏、全网页滚动截屏,最终结果可预览后再保存',
+        contentScriptJs: true,
+        noPage: true,
+        menuConfig: [{
+            icon: '✂',
+            text: '网页截屏工具'
+        }]
+    },
+    'color-picker': {
+        name: '页面取色工具',
+        tips: '可直接在网页上针对任意元素进行色值采集,将光标移动到需要取色的位置,单击确定即可',
+        contentScriptJs: true,
+        noPage: true,
+        menuConfig: [{
+            icon: '✑',
+            text: '页面取色工具'
+        }]
+    },
+    'naotu': {
+        name: '便捷思维导图',
+        tips: '轻量便捷,随想随用,支持自动保存、本地数据存储、批量数据导入导出、图片格式下载等',
+        menuConfig: [{
+            icon: 'Ψ',
+            text: '便捷思维导图'
+        }]
+    },
+    'grid-ruler': {
+        name: '网页栅格标尺',
+        tips: 'Web开发用,横竖两把尺子,以10px为单位,用以检测&校准当前网页的栅格对齐率',
+        contentScriptJs: true,
+        contentScriptCss: true,
+        noPage: true,
+        menuConfig: [{
+            icon: 'Ⅲ',
+            text: '网页栅格标尺'
+        }]
+    },
+    'page-timing': {
+        name: '网页性能检测',
+        tips: '检测网页加载性能,包括握手、响应、渲染等各阶段耗时,同时提供Response Headers以便分析',
+        contentScriptJs: true,
+        noPage: true,
+        menuConfig: [{
+            icon: 'Σ',
+            text: '网页性能检测'
+        }]
+    },
+    'excel2json': {
+        name: 'Excel转JSON',
+        tips: '将Excel或CVS中的数据,直接转换成为结构化数据,如JSON、XML、MySQL、PHP等(By @hpng)',
+        menuConfig: [{
+            icon: 'Ⓗ',
+            text: 'Excel转JSON'
+        }]
+    },
+    'chart-maker': {
+        name: '图表制作工具',
+        tips: '快速制作各类数据可视化图表,支持柱状图、折线图、饼图等多种图表类型,可导出为图片格式',
+        menuConfig: [{
+            icon: '📊',
+            text: '图表制作工具'
+        }]
+    },
+};
+
+export default toolMap;

+ 1130 - 0
apps/chart-maker/chart-generator.js

@@ -0,0 +1,1130 @@
+// 图表实例,方便后续更新
+window.chartInstance = null;
+
+// 注册Chart.js插件
+if (Chart && Chart.register) {
+    // 如果有ChartDataLabels插件,注册它
+    if (window.ChartDataLabels) {
+        Chart.register(ChartDataLabels);
+    }
+}
+
+// 生成图表的主函数
+function createChart(data, settings) {
+    // 获取Canvas元素
+    const canvas = document.getElementById('chart-canvas');
+    const ctx = canvas.getContext('2d');
+    
+    // 如果已有图表,先销毁
+    if (window.chartInstance) {
+        window.chartInstance.destroy();
+    }
+    
+    // 应用颜色方案
+    applyColorScheme(data, settings.colorScheme, settings.type);
+    
+    // 配置图表选项
+    const options = getChartOptions(settings);
+    
+    // 处理特殊图表类型
+    let type = settings.type;
+    let chartData = {...data};
+
+    // 检查是否为首系列图表类型
+    const isFirstSeriesOnly = settings.type.includes(" (首系列)");
+    if (isFirstSeriesOnly) {
+        // 提取真正的图表类型
+        type = settings.type.replace(" (首系列)", "");
+        
+        // 只保留第一个数据系列
+        if (chartData.datasets.length > 1) {
+            const firstDataset = chartData.datasets[0];
+            chartData.datasets = [{
+                ...firstDataset,
+                label: firstDataset.label || '数据'
+            }];
+        }
+    }
+
+    // 移除可能存在的旧堆叠设置
+    if (options.scales && options.scales.x) {
+        delete options.scales.x.stacked;
+    }
+    if (options.scales && options.scales.y) {
+        delete options.scales.y.stacked;
+    }
+
+    // 移除旧的填充设置和其他特殊属性
+    chartData.datasets.forEach(dataset => {
+        delete dataset.fill;
+        delete dataset.tension;
+        delete dataset.stepped;
+        delete dataset.borderDash;
+    });
+
+    // 基本图表类型处理
+    switch(type) {
+        // 柱状图系列
+        case 'horizontalBar':
+            type = 'bar';
+            options.indexAxis = 'y';
+            break;
+        case 'stackedBar':
+            type = 'bar';
+            if (!options.scales) options.scales = {};
+            if (!options.scales.x) options.scales.x = {};
+            if (!options.scales.y) options.scales.y = {};
+            options.scales.x.stacked = true;
+            options.scales.y.stacked = true;
+            break;
+        case 'groupedBar':
+            type = 'bar';
+            // 分组柱状图是默认行为
+            break;
+        case 'gradientBar':
+            type = 'bar';
+            // 渐变效果在applyColorScheme函数中处理
+            break;
+        case 'barWithError':
+            type = 'bar';
+            // 添加误差线
+            chartData.datasets.forEach(dataset => {
+                dataset.errorBars = {
+                    y: {
+                        plus: dataset.data.map(() => Math.random() * 5 + 2),
+                        minus: dataset.data.map(() => Math.random() * 5 + 2)
+                    }
+                };
+            });
+            break;
+        case 'rangeBar':
+            type = 'bar';
+            // 转换数据为范围格式
+            chartData.datasets.forEach(dataset => {
+                dataset.data = dataset.data.map(value => {
+                    const min = Math.max(0, value - Math.random() * value * 0.4);
+                    return [min, value];
+                });
+            });
+            break;
+        
+        // 线/面积图系列
+        case 'area':
+            type = 'line';
+            chartData.datasets.forEach(dataset => {
+                dataset.fill = true;
+            });
+            break;
+        case 'curvedLine':
+            type = 'line';
+            chartData.datasets.forEach(dataset => {
+                dataset.tension = 0.4; // 更平滑的曲线
+            });
+            break;
+        case 'stepLine':
+            type = 'line';
+            chartData.datasets.forEach(dataset => {
+                dataset.stepped = true;
+            });
+            break;
+        case 'stackedArea':
+            type = 'line';
+            chartData.datasets.forEach(dataset => {
+                dataset.fill = true;
+            });
+            if (!options.scales) options.scales = {};
+            if (!options.scales.y) options.scales.y = {};
+            options.scales.y.stacked = true;
+            break;
+        case 'streamgraph':
+            type = 'line';
+            // 流图效果:堆叠面积图 + 居中对齐
+            chartData.datasets.forEach(dataset => {
+                dataset.fill = true;
+            });
+            if (!options.scales) options.scales = {};
+            if (!options.scales.y) options.scales.y = {};
+            options.scales.y.stacked = true;
+            options.scales.y.offset = true; // 居中对齐堆叠
+            break;
+        case 'timeline':
+            type = 'line';
+            chartData.datasets.forEach(dataset => {
+                dataset.stepped = 'before';
+                dataset.borderDash = [5, 5]; // 虚线效果
+            });
+            break;
+        
+        // 饼图/环形图系列
+        case 'halfPie':
+            type = 'doughnut';
+            options.circumference = Math.PI;
+            options.rotation = -Math.PI / 2;
+            break;
+        case 'nestedPie':
+            type = 'doughnut';
+            // 嵌套效果通过多个饼图叠加实现,简化实现仅调整内外半径
+            if (chartData.datasets.length > 0) {
+                chartData.datasets[0].radius = '70%';
+                chartData.datasets[0].weight = 0.7;
+            }
+            break;
+        
+        // 散点/气泡图系列
+        case 'scatter':
+            chartData.datasets = transformScatterData(chartData.datasets);
+            break;
+        case 'bubble':
+            chartData.datasets = transformBubbleData(chartData.datasets);
+            type = 'bubble';
+            break;
+        case 'scatterSmooth':
+            chartData.datasets = transformScatterData(chartData.datasets);
+            type = 'scatter';
+            // 添加趋势线
+            chartData.datasets.forEach(dataset => {
+                const smoothedDataset = {
+                    ...dataset,
+                    type: 'line',
+                    data: [...dataset.data],
+                    pointRadius: 0,
+                    tension: 0.4,
+                    fill: false
+                };
+                chartData.datasets.push(smoothedDataset);
+            });
+            break;
+        
+        // 专业图表系列
+        case 'funnel':
+            type = 'bar';
+            // 简化的漏斗图实现
+            options.indexAxis = 'y';
+            if (chartData.datasets.length > 0) {
+                // 对数据进行排序
+                const sortedData = [...chartData.datasets[0].data].sort((a, b) => b - a);
+                chartData.datasets[0].data = sortedData;
+                // 确保Y轴反转
+                if (!options.scales) options.scales = {};
+                if (!options.scales.y) options.scales.y = {};
+                options.scales.y.reverse = true;
+            }
+            break;
+        case 'gauge':
+            type = 'doughnut';
+            // 简化的仪表盘实现
+            if (chartData.datasets.length > 0 && chartData.datasets[0].data.length > 0) {
+                const value = chartData.datasets[0].data[0];
+                const max = Math.max(...chartData.datasets[0].data) * 1.2;
+                const remainder = max - value;
+                
+                chartData.datasets[0].data = [value, remainder];
+                chartData.datasets[0].backgroundColor = ['#36A2EB', '#E0E0E0'];
+                chartData.labels = ['Value', ''];
+                
+                options.circumference = Math.PI;
+                options.rotation = -Math.PI;
+                options.cutout = '70%';
+            }
+            break;
+        case 'boxplot':
+            // 简化的箱线图实现(基于柱状图)
+            type = 'bar';
+            // 转换数据为箱线图格式
+            chartData.datasets.forEach(dataset => {
+                dataset.data = dataset.data.map(value => {
+                    const q1 = Math.max(0, value * 0.7);
+                    const median = value * 0.85;
+                    const q3 = value * 1.15;
+                    const min = Math.max(0, q1 - (median - q1));
+                    const max = q3 + (q3 - median);
+                    return [min, q1, median, q3, max];
+                });
+            });
+            break;
+        case 'waterfall':
+            type = 'bar';
+            // 瀑布图实现
+            if (chartData.datasets.length > 0) {
+                const data = chartData.datasets[0].data;
+                let cumulative = 0;
+                
+                // 创建新的数据数组,包含每个点的起点和终点
+                const waterfallData = data.map((value, index) => {
+                    const start = cumulative;
+                    cumulative += value;
+                    return {
+                        start: start,
+                        end: cumulative,
+                        value: value
+                    };
+                });
+                
+                // 转换为柱状图数据
+                chartData.datasets[0].data = waterfallData.map(d => d.end - d.start);
+                
+                // 添加起点数据集
+                chartData.datasets.push({
+                    label: '起点',
+                    data: waterfallData.map(d => d.start),
+                    backgroundColor: 'rgba(0,0,0,0)',
+                    borderColor: 'rgba(0,0,0,0)',
+                    stack: 'waterfall'
+                });
+                
+                // 设置为堆叠柱状图
+                if (!options.scales) options.scales = {};
+                if (!options.scales.x) options.scales.x = {};
+                if (!options.scales.y) options.scales.y = {};
+                options.scales.x.stacked = true;
+                options.scales.y.stacked = true;
+            }
+            break;
+        case 'treemap':
+        case 'sunburst':
+        case 'sankey':
+        case 'chord':
+        case 'network':
+            // 这些高级图表需要专门的库支持,这里简化为提示信息
+            type = 'bar';
+            if (chartData.datasets.length > 0) {
+                // 显示一个提示信息
+                chartData.datasets = [{
+                    label: `${settings.type}需要专门的图表库支持`,
+                    data: [100],
+                    backgroundColor: '#f8d7da'
+                }];
+                chartData.labels = ['请尝试其他图表类型'];
+            }
+            break;
+    }
+
+    // 热力图特殊处理
+    if (type === 'heatmap') {
+        // 热力图不是Chart.js的标准类型,需要使用插件或自定义渲染
+        // 简单实现一个基于颜色渐变的矩阵图
+        type = 'matrix';
+        renderHeatmap(ctx, chartData, options);
+        return;
+    }
+
+    // 饼图、环形图和极地面积图特殊处理 (如果不是由其他类型转换而来的)
+    if (['pie', 'doughnut', 'polarArea'].includes(type) && !['halfPie', 'nestedPie', 'gauge'].includes(settings.type) && !isFirstSeriesOnly) {
+        // 如果有多个数据集,只取第一个
+        if (chartData.datasets.length > 1) {
+            const firstDataset = chartData.datasets[0];
+            chartData.datasets = [{
+                ...firstDataset,
+                label: undefined // 这些图表类型不需要数据集标签
+            }];
+        }
+    }
+
+    // 创建图表实例
+    window.chartInstance = new Chart(ctx, {
+        type: type,
+        data: chartData,
+        options: options
+    });
+    
+    // 设置标记属性,表示图表已渲染,去除背景
+    canvas.setAttribute('data-chart-rendered', 'true');
+    
+    return window.chartInstance;
+}
+
+// 辅助函数:获取当前图表实例
+function getChartInstance() {
+    return window.chartInstance;
+}
+
+// 辅助函数:设置当前图表实例
+function setChartInstance(instance) {
+    window.chartInstance = instance;
+}
+
+// 获取图表配置选项
+function getChartOptions(settings) {
+    const options = {
+        responsive: true,
+        maintainAspectRatio: false,
+        plugins: {
+            title: {
+                display: !!settings.title,
+                text: settings.title,
+                font: {
+                    size: 18,
+                    weight: 'bold'
+                },
+                padding: {
+                    top: 10,
+                    bottom: 20
+                }
+            },
+            legend: {
+                display: settings.legendPosition !== 'none',
+                position: settings.legendPosition === 'none' ? 'top' : settings.legendPosition,
+                labels: {
+                    usePointStyle: true,
+                    padding: 15,
+                    font: {
+                        size: 12
+                    }
+                }
+            },
+            tooltip: {
+                enabled: true,
+                backgroundColor: 'rgba(0, 0, 0, 0.7)',
+                titleFont: {
+                    size: 14
+                },
+                bodyFont: {
+                    size: 13
+                },
+                padding: 10,
+                displayColors: true
+            }
+        },
+        animation: {
+            duration: settings.animateChart ? 1000 : 0,
+            easing: 'easeOutQuart'
+        }
+    };
+    
+    // 检查是否为简单数据模式(只有一个数据集)
+    const isSimpleData = settings.isSimpleData || 
+                         (window.chartData && window.chartData.datasets && window.chartData.datasets.length === 1);
+    
+    // 如果是简单数据模式,隐藏图例
+    if (isSimpleData) {
+        options.plugins.legend.display = false;
+    }
+    
+    // 只有部分图表类型需要轴线配置
+    if (!['pie', 'doughnut', 'polarArea'].includes(settings.type.replace(" (首系列)", ""))) { // 兼容(首系列)后缀
+        options.scales = {
+            x: {
+                title: {
+                    display: !!settings.xAxisLabel,
+                    text: settings.xAxisLabel,
+                    font: {
+                        size: 14,
+                        weight: 'bold'
+                    },
+                    padding: {
+                        top: 10
+                    }
+                },
+                grid: {
+                    display: settings.showGridLines,
+                    color: 'rgba(0, 0, 0, 0.1)'
+                },
+                ticks: {
+                    font: {
+                        size: 12
+                    }
+                }
+            },
+            y: {
+                title: {
+                    display: !!settings.yAxisLabel,
+                    text: settings.yAxisLabel,
+                    font: {
+                        size: 14,
+                        weight: 'bold'
+                    },
+                    padding: {
+                        bottom: 10
+                    }
+                },
+                grid: {
+                    display: settings.showGridLines,
+                    color: 'rgba(0, 0, 0, 0.1)'
+                },
+                ticks: {
+                    font: {
+                        size: 12
+                    },
+                    beginAtZero: true
+                }
+            }
+        };
+        
+        // 水平柱状图X和Y轴配置需要互换
+        if (settings.type === 'horizontalBar') {
+            const temp = options.scales.x;
+            options.scales.x = options.scales.y;
+            options.scales.y = temp;
+        }
+    }
+    
+    // 数据标签配置
+    if (settings.showDataLabels) {
+        options.plugins.datalabels = {
+            display: true,
+            color: function(context) {
+                const actualType = settings.type.replace(" (首系列)", "");
+                const dataset = context.dataset;
+                
+                // 首先检查数据集是否有自定义的datalabels配置
+                if (dataset.datalabels && dataset.datalabels.color) {
+                    const labelColors = dataset.datalabels.color;
+                    
+                    // 如果color是数组,则使用对应索引的颜色
+                    if (Array.isArray(labelColors)) {
+                        return labelColors[context.dataIndex] || '#333333';
+                    }
+                    // 如果color是单个颜色值
+                    return labelColors;
+                }
+                
+                // 如果没有自定义配置,则使用智能检测
+                // 为饼图和环形图使用对比色
+                if (['pie', 'doughnut', 'polarArea'].includes(actualType)) {
+                    // 获取背景色
+                    const index = context.dataIndex;
+                    const backgroundColor = dataset.backgroundColor[index];
+                    
+                    // 计算背景色的亮度
+                    return isColorDark(backgroundColor) ? '#ffffff' : '#000000';
+                } else if (actualType === 'bar' || actualType === 'horizontalBar' || 
+                           actualType === 'stackedBar' || actualType === 'gradientBar') {
+                    // 柱状图系列也需要对比色
+                    let backgroundColor;
+                    
+                    // 背景色可能是数组或单个颜色
+                    if (Array.isArray(dataset.backgroundColor)) {
+                        backgroundColor = dataset.backgroundColor[context.dataIndex];
+                    } else {
+                        backgroundColor = dataset.backgroundColor;
+                    }
+                    
+                    return isColorDark(backgroundColor) ? '#ffffff' : '#333333';
+                } else {
+                    // 其他图表类型使用默认深色
+                    return '#333333';
+                }
+            },
+            align: function(context) {
+                const dataset = context.dataset;
+                // 使用数据集中的align配置(如果有的话)
+                if (dataset.datalabels && dataset.datalabels.align) {
+                    return dataset.datalabels.align;
+                }
+                
+                // 默认配置
+                const chartType = settings.type.replace(" (首系列)", "");
+                if (['line', 'area', 'scatter', 'bubble'].includes(chartType)) {
+                    return 'top';
+                }
+                return 'center';
+            },
+            font: {
+                weight: 'bold'
+            },
+            formatter: function(value, context) {
+                const actualType = settings.type.replace(" (首系列)", "");
+                
+                // 饼图、环形图和极地面积图显示百分比
+                if (['pie', 'doughnut', 'polarArea'].includes(actualType)) {
+                    // 计算百分比
+                    const dataset = context.chart.data.datasets[context.datasetIndex];
+                    const total = dataset.data.reduce((total, value) => total + value, 0);
+                    const percentage = ((value / total) * 100).toFixed(1) + '%';
+                    
+                    // 对于较小的扇区只显示百分比,否则显示值和百分比
+                    const percent = value / total * 100;
+                    if (percent < 5) {
+                        return percentage;
+                    } else {
+                        return `${value} (${percentage})`;
+                    }
+                }
+                
+                // 对散点图特殊处理
+                if (settings.type === 'scatter') {
+                    if (context && context.dataset && context.dataset.data && 
+                        context.dataset.data[context.dataIndex] && 
+                        typeof context.dataset.data[context.dataIndex].y !== 'undefined') {
+                        return context.dataset.data[context.dataIndex].y;
+                    }
+                    return '';
+                }
+                
+                return value;
+            }
+        };
+    }
+    
+    return options;
+}
+
+// 判断颜色是否为深色
+function isColorDark(color) {
+    // 处理rgba格式
+    if (color && color.startsWith('rgba')) {
+        const parts = color.match(/rgba\((\d+),\s*(\d+),\s*(\d+),\s*([\d.]+)\)/);
+        if (parts) {
+            const r = parseInt(parts[1]);
+            const g = parseInt(parts[2]);
+            const b = parseInt(parts[3]);
+            // 计算亮度 (根据人眼对RGB的敏感度加权)
+            const brightness = (r * 0.299 + g * 0.587 + b * 0.114) / 255;
+            return brightness < 0.7; // 亮度小于0.7认为是深色
+        }
+    }
+    
+    // 处理rgb格式
+    if (color && color.startsWith('rgb(')) {
+        const parts = color.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/);
+        if (parts) {
+            const r = parseInt(parts[1]);
+            const g = parseInt(parts[2]);
+            const b = parseInt(parts[3]);
+            const brightness = (r * 0.299 + g * 0.587 + b * 0.114) / 255;
+            return brightness < 0.7;
+        }
+    }
+    
+    // 处理十六进制格式
+    if (color && color.startsWith('#')) {
+        color = color.replace('#', '');
+        const r = parseInt(color.length === 3 ? color.substring(0, 1).repeat(2) : color.substring(0, 2), 16);
+        const g = parseInt(color.length === 3 ? color.substring(1, 2).repeat(2) : color.substring(2, 4), 16);
+        const b = parseInt(color.length === 3 ? color.substring(2, 3).repeat(2) : color.substring(4, 6), 16);
+        const brightness = (r * 0.299 + g * 0.587 + b * 0.114) / 255;
+        return brightness < 0.7;
+    }
+    
+    // 默认返回true,使用白色文本
+    return true;
+}
+
+// 应用颜色方案
+function applyColorScheme(data, colorScheme, chartType) {
+    // 定义颜色方案 - 全新设计,确保各个方案风格迥异
+    const colorSchemes = {
+        default: [
+            '#4e73df', '#1cc88a', '#36b9cc', '#f6c23e', '#e74a3b',
+            '#6f42c1', '#fd7e14', '#20c9a6', '#36b9cc', '#858796'
+        ],
+        pastel: [
+            '#FFB6C1', '#FFD700', '#98FB98', '#87CEFA', '#FFA07A',
+            '#DDA0DD', '#FFDAB9', '#B0E0E6', '#F0E68C', '#E6E6FA'
+        ],
+        bright: [
+            '#FF1E1E', '#FFFF00', '#00FF00', '#00FFFF', '#0000FF',
+            '#FF00FF', '#FF7F00', '#FF1493', '#00FA9A', '#7B68EE'
+        ],
+        cool: [
+            '#5F4B8B', '#42BFDD', '#00A7E1', '#00344B', '#143642',
+            '#0F8B8D', '#4CB5F5', '#1D3557', '#A8DADC', '#457B9D'
+        ],
+        warm: [
+            '#FF7700', '#FF9E00', '#FFCF00', '#FFF400', '#E20000',
+            '#D91A1A', '#A60000', '#FF5252', '#FF7B7B', '#FFBF69'
+        ],
+        corporate: [
+            '#003F5C', '#2F4B7C', '#665191', '#A05195', '#D45087',
+            '#F95D6A', '#FF7C43', '#FFA600', '#004D40', '#00695C'
+        ],
+        contrast: [
+            '#000000', '#E63946', '#457B9D', '#F1C40F', '#2ECC71',
+            '#9B59B6', '#1ABC9C', '#F39C12', '#D35400', '#7F8C8D'
+        ],
+        rainbow: [
+            '#FF0000', '#FF7F00', '#FFFF00', '#00FF00', '#0000FF',
+            '#4B0082', '#9400D3', '#FF1493', '#00FFFF', '#FF00FF'
+        ],
+        earth: [
+            '#5D4037', '#795548', '#A1887F', '#4E342E', '#3E2723',
+            '#33691E', '#558B2F', '#7CB342', '#8D6E63', '#6D4C41'
+        ],
+        ocean: [
+            '#006064', '#00838F', '#0097A7', '#00ACC1', '#00BCD4',
+            '#26C6DA', '#4DD0E1', '#80DEEA', '#01579B', '#0277BD'
+        ],
+        vintage: [
+            '#8D8741', '#659DBD', '#DAAD86', '#BC986A', '#FBEEC1',
+            '#605B56', '#837A75', '#9E8B8B', '#D8C3A5', '#E8DDCD'
+        ]
+    };
+    
+    // 获取选定的颜色方案
+    const colors = colorSchemes[colorScheme] || colorSchemes.default;
+    
+    const actualChartType = chartType.replace(" (首系列)", ""); // 获取基础类型
+
+    // 为每个数据集应用颜色
+    data.datasets.forEach((dataset, index) => {
+        const color = colors[index % colors.length];
+        
+        // 设置不同图表类型的颜色
+        if (['pie', 'doughnut', 'polarArea', 'halfPie', 'nestedPie', 'gauge'].includes(actualChartType)) {
+            // 这些图表类型需要为每个数据点设置不同颜色
+            // 对于gauge特殊处理,不使用这种方式
+            if (actualChartType === 'gauge' && dataset.backgroundColor) {
+                // 保留gauge的特殊颜色设置
+            } else {
+                dataset.backgroundColor = dataset.data.map((_, i) => colors[i % colors.length]);
+                dataset.borderColor = 'white';
+                dataset.borderWidth = 1;
+                
+                // 为每个扇区添加对应的前景色(用于数据标签)
+                dataset.datalabels = {
+                    color: dataset.backgroundColor.map(bgColor => isColorDark(bgColor) ? '#ffffff' : '#000000')
+                };
+            }
+        } else if (['line', 'area', 'stackedArea', 'curvedLine', 'stepLine', 'timeline', 'streamgraph'].includes(actualChartType)) {
+            // 折线图和面积图样式
+            dataset.borderColor = color;
+            // 根据图表类型调整透明度
+            let alpha = 0.1; // 默认折线图半透明
+            if (['area', 'stackedArea', 'streamgraph'].includes(actualChartType)) {
+                alpha = 0.3; // 面积图相对更不透明
+            }
+            dataset.backgroundColor = hexToRgba(color, alpha);
+            dataset.pointBackgroundColor = color;
+            dataset.pointBorderColor = '#fff';
+            dataset.pointHoverBackgroundColor = '#fff';
+            dataset.pointHoverBorderColor = color;
+            
+            // 特殊线型
+            if (actualChartType === 'curvedLine') {
+                dataset.tension = 0.4;
+            } else if (actualChartType === 'stepLine') {
+                dataset.stepped = true;
+            } else if (actualChartType === 'timeline') {
+                dataset.stepped = 'before';
+                dataset.borderDash = [5, 5];
+            } else {
+                dataset.tension = 0.3; 
+            }
+            
+            // 设置数据标签颜色
+            // 对于线图和面积图,标签通常放在点的上方,使用与线条相同的颜色
+            dataset.datalabels = {
+                color: isColorDark(color) ? color : '#333333',
+                align: 'top'
+            };
+        } else if (actualChartType === 'radar') {
+            // 雷达图样式
+            dataset.borderColor = color;
+            dataset.backgroundColor = hexToRgba(color, 0.2);
+            dataset.pointBackgroundColor = color;
+            dataset.pointBorderColor = '#fff';
+            
+            // 雷达图数据标签颜色 - 使用与边框相同的颜色
+            dataset.datalabels = {
+                color: isColorDark(color) ? color : '#333333'
+            };
+        } else if (['scatter', 'bubble', 'scatterSmooth'].includes(actualChartType)) {
+            // 散点图样式
+            dataset.backgroundColor = color;
+            dataset.borderColor = hexToRgba(color, 0.8);
+            
+            // 散点平滑图特殊处理
+            if (actualChartType === 'scatterSmooth' && dataset.type === 'line') {
+                dataset.borderColor = color;
+                dataset.backgroundColor = 'transparent';
+            }
+            
+            // 散点图数据标签颜色
+            dataset.datalabels = {
+                color: isColorDark(color) ? '#ffffff' : '#333333',
+                align: 'top'
+            };
+        } else if (actualChartType === 'gradientBar') {
+            // 渐变柱状图
+            const ctx = document.createElement('canvas').getContext('2d');
+            const gradient = ctx.createLinearGradient(0, 0, 0, 300);
+            gradient.addColorStop(0, color);
+            gradient.addColorStop(1, hexToRgba(color, 0.3));
+            dataset.backgroundColor = gradient;
+            dataset.borderColor = color;
+            dataset.borderWidth = 1;
+            dataset.hoverBackgroundColor = color;
+            
+            // 渐变柱状图标签颜色 - 使用顶部颜色判断
+            dataset.datalabels = {
+                color: isColorDark(color) ? '#ffffff' : '#333333'
+            };
+        } else if (actualChartType === 'waterfall') {
+            // 瀑布图特殊处理
+            if (dataset.label === '起点') {
+                // 这是为瀑布图添加的起点数据集,保持透明
+            } else {
+                const values = dataset.data;
+                // 根据值的正负设置不同颜色
+                const positiveColor = '#36b9cc';
+                const negativeColor = '#e74a3b';
+                
+                dataset.backgroundColor = values.map(value => 
+                    value >= 0 ? hexToRgba(positiveColor, 0.7) : hexToRgba(negativeColor, 0.7)
+                );
+                dataset.borderColor = values.map(value => 
+                    value >= 0 ? positiveColor : negativeColor
+                );
+                dataset.borderWidth = 1;
+                
+                // 瀑布图数据标签颜色 - 根据每个柱子的背景色决定
+                dataset.datalabels = {
+                    color: values.map(value => 
+                        value >= 0 ? (isColorDark(positiveColor) ? '#ffffff' : '#333333') : 
+                                    (isColorDark(negativeColor) ? '#ffffff' : '#333333')
+                    )
+                };
+            }
+        } else if (actualChartType === 'funnel') {
+            // 漏斗图特殊处理 - 使用渐变颜色
+            const data = dataset.data;
+            if (data.length) {
+                dataset.backgroundColor = data.map((_, i) => {
+                    const ratio = 1 - (i / data.length); // 1 到 0
+                    return hexToRgba(color, 0.5 + ratio * 0.5); // 透明度从1到0.5
+                });
+                dataset.borderColor = color;
+                dataset.borderWidth = 1;
+                
+                // 漏斗图数据标签颜色 - 根据每个部分的背景色决定
+                dataset.datalabels = {
+                    color: dataset.backgroundColor.map(bgColor => isColorDark(bgColor) ? '#ffffff' : '#333333')
+                };
+            }
+        } else {
+            // 默认样式(用于柱状图等)
+            dataset.backgroundColor = hexToRgba(color, 0.7);
+            dataset.borderColor = color;
+            dataset.borderWidth = 1;
+            dataset.hoverBackgroundColor = color;
+            
+            // 默认数据标签颜色 - 根据背景色决定
+            dataset.datalabels = {
+                color: isColorDark(dataset.backgroundColor) ? '#ffffff' : '#333333'
+            };
+        }
+    });
+}
+
+// 将十六进制颜色转换为rgba格式
+function hexToRgba(hex, alpha) {
+    // 移除井号
+    hex = hex.replace('#', '');
+    
+    // 解析RGB值
+    const r = parseInt(hex.length === 3 ? hex.substring(0, 1).repeat(2) : hex.substring(0, 2), 16);
+    const g = parseInt(hex.length === 3 ? hex.substring(1, 2).repeat(2) : hex.substring(2, 4), 16);
+    const b = parseInt(hex.length === 3 ? hex.substring(2, 3).repeat(2) : hex.substring(4, 6), 16);
+    
+    // 返回rgba字符串
+    return `rgba(${r}, ${g}, ${b}, ${alpha})`;
+}
+
+// 渲染热力图(自定义实现)
+function renderHeatmap(ctx, data, options) {
+    // 清除Canvas
+    ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
+    
+    // 设置尺寸和边距
+    const margin = {
+        top: 50,
+        right: 30,
+        bottom: 50,
+        left: 60
+    };
+    
+    const width = ctx.canvas.width - margin.left - margin.right;
+    const height = ctx.canvas.height - margin.top - margin.bottom;
+    
+    // 获取数据
+    const rows = data.labels;
+    const columns = data.datasets.map(dataset => dataset.label);
+    
+    // 创建值矩阵
+    const matrix = [];
+    rows.forEach((_, rowIndex) => {
+        const row = [];
+        data.datasets.forEach(dataset => {
+            row.push(dataset.data[rowIndex]);
+        });
+        matrix.push(row);
+    });
+    
+    // 找出最大值和最小值
+    const allValues = matrix.flat();
+    const min = Math.min(...allValues);
+    const max = Math.max(...allValues);
+    
+    // 绘制标题
+    if (options.plugins && options.plugins.title && options.plugins.title.display) {
+        ctx.textAlign = 'center';
+        ctx.font = '18px Arial';
+        ctx.fillStyle = '#333';
+        ctx.fillText(options.plugins.title.text, ctx.canvas.width / 2, 25);
+    }
+    
+    // 绘制单元格和标签
+    const cellWidth = width / columns.length;
+    const cellHeight = height / rows.length;
+    
+    // 行标签(Y轴)
+    ctx.textAlign = 'right';
+    ctx.textBaseline = 'middle';
+    ctx.font = '12px Arial';
+    ctx.fillStyle = '#666';
+    
+    rows.forEach((label, i) => {
+        const y = margin.top + i * cellHeight + cellHeight / 2;
+        ctx.fillText(label, margin.left - 10, y);
+    });
+    
+    // 列标签(X轴)
+    ctx.textAlign = 'center';
+    ctx.textBaseline = 'top';
+    
+    columns.forEach((label, i) => {
+        const x = margin.left + i * cellWidth + cellWidth / 2;
+        ctx.fillText(label, x, margin.top + height + 10);
+    });
+    
+    // 绘制热力图单元格
+    matrix.forEach((row, i) => {
+        row.forEach((value, j) => {
+            // 归一化值 (0-1)
+            const normalizedValue = (value - min) / (max - min || 1);
+            
+            // 计算颜色(红-黄-绿渐变)
+            const color = getHeatmapColor(normalizedValue);
+            
+            // 绘制单元格
+            const x = margin.left + j * cellWidth;
+            const y = margin.top + i * cellHeight;
+            
+            ctx.fillStyle = color;
+            ctx.fillRect(x, y, cellWidth, cellHeight);
+            
+            // 添加值标签,根据背景色的亮度自动选择标签颜色
+            const brightness = getColorBrightness(color);
+            ctx.fillStyle = brightness < 0.7 ? 'white' : 'black'; // 亮度阈值为0.7
+            ctx.textAlign = 'center';
+            ctx.textBaseline = 'middle';
+            ctx.font = 'bold 12px Arial';
+            ctx.fillText(value, x + cellWidth / 2, y + cellHeight / 2);
+        });
+    });
+    
+    // 绘制坐标轴
+    ctx.strokeStyle = '#ddd';
+    ctx.lineWidth = 1;
+    
+    // X轴
+    ctx.beginPath();
+    ctx.moveTo(margin.left, margin.top + height);
+    ctx.lineTo(margin.left + width, margin.top + height);
+    ctx.stroke();
+    
+    // Y轴
+    ctx.beginPath();
+    ctx.moveTo(margin.left, margin.top);
+    ctx.lineTo(margin.left, margin.top + height);
+    ctx.stroke();
+}
+
+// 获取颜色亮度
+function getColorBrightness(color) {
+    // 处理rgb格式
+    if (color.startsWith('rgb(')) {
+        const parts = color.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/);
+        if (parts) {
+            const r = parseInt(parts[1]);
+            const g = parseInt(parts[2]);
+            const b = parseInt(parts[3]);
+            // 计算亮度 (根据人眼对RGB的敏感度加权)
+            return (r * 0.299 + g * 0.587 + b * 0.114) / 255;
+        }
+    }
+    
+    // 默认返回0.5
+    return 0.5;
+}
+
+// 获取热力图颜色
+function getHeatmapColor(value) {
+    // 红-黄-绿渐变
+    const r = value < 0.5 ? 255 : Math.round(255 * (1 - 2 * (value - 0.5)));
+    const g = value < 0.5 ? Math.round(255 * (2 * value)) : 255;
+    const b = 0;
+    
+    return `rgb(${r}, ${g}, ${b})`;
+}
+
+// 注册Chart.js插件以支持数据标签
+Chart.register({
+    id: 'datalabels',
+    beforeDraw: (chart) => {
+        const ctx = chart.ctx;
+        const options = chart.options.plugins.datalabels;
+        
+        if (!options || !options.display) {
+            return;
+        }
+        
+        chart.data.datasets.forEach((dataset, datasetIndex) => {
+            const meta = chart.getDatasetMeta(datasetIndex);
+            
+            meta.data.forEach((element, index) => {
+                // 获取值
+                let value = dataset.data[index];
+                if (typeof value === 'object' && value !== null) {
+                    // 散点图等复杂数据结构
+                    value = value.y;
+                }
+                
+                // 获取位置
+                const { x, y } = element.getCenterPoint();
+                
+                // 确定文本颜色
+                let fillColor;
+                if (typeof options.color === 'function') {
+                    fillColor = options.color({
+                        datasetIndex, 
+                        index, 
+                        dataset,
+                        dataIndex: index,
+                        chart: chart
+                    });
+                } else {
+                    fillColor = options.color || '#666';
+                }
+                
+                ctx.fillStyle = fillColor;
+                
+                // 设置字体
+                ctx.font = options.font.weight + ' 12px Arial';
+                ctx.textAlign = 'center';
+                ctx.textBaseline = 'middle';
+                
+                // 格式化值
+                let text = typeof options.formatter === 'function' 
+                    ? options.formatter(value, {
+                        datasetIndex, 
+                        index, 
+                        dataset,
+                        dataIndex: index,
+                        chart: chart
+                      }) 
+                    : value;
+                
+                // 绘制文本
+                ctx.fillText(text, x, y - 15);
+            });
+        });
+    }
+});
+
+// 辅助函数:转换散点图数据
+function transformScatterData(datasets) {
+    return datasets.map(dataset => {
+        if (!dataset.data || !Array.isArray(dataset.data)) {
+            return {
+                ...dataset,
+                data: []
+            };
+        }
+        
+        return {
+            ...dataset,
+            data: dataset.data.map((value, index) => {
+                // 确保value是一个有效的数值
+                const y = parseFloat(value);
+                if (isNaN(y)) {
+                    return { x: index + 1, y: 0 };
+                }
+                return { x: index + 1, y: y };
+            })
+        };
+    });
+}
+
+// 辅助函数:转换气泡图数据
+function transformBubbleData(datasets) {
+    return datasets.map(dataset => {
+        if (!dataset.data || !Array.isArray(dataset.data)) {
+            return {
+                ...dataset,
+                data: []
+            };
+        }
+        
+        return {
+            ...dataset,
+            data: dataset.data.map((value, index) => {
+                // 确保value是一个有效的数值
+                const y = parseFloat(value);
+                if (isNaN(y)) {
+                    return { x: index + 1, y: 0, r: 5 };
+                }
+                // 气泡大小与值成比例
+                const r = Math.max(5, Math.min(20, y / 10));
+                return { x: index + 1, y: y, r: r };
+            })
+        };
+    });
+}
+
+/**
+ * 初始化图表类型预览画廊
+ */
+function initChartTypeGallery() {
+    // 获取所有图表类型预览项
+    const chartTypeItems = document.querySelectorAll('.chart-type-item');
+    
+    // 获取图表类型选择下拉框
+    const chartTypeSelect = document.getElementById('chart-type');
+    
+    // 为每个预览项添加点击事件
+    chartTypeItems.forEach(item => {
+        item.addEventListener('click', function() {
+            // 获取图表类型值
+            const chartType = this.getAttribute('data-chart-type');
+            
+            // 设置下拉框的值
+            chartTypeSelect.value = chartType;
+            
+            // 触发change事件以更新图表
+            const event = new Event('change');
+            chartTypeSelect.dispatchEvent(event);
+            
+            // 更新活动状态
+            chartTypeItems.forEach(item => item.classList.remove('active'));
+            this.classList.add('active');
+            
+            // 如果已经有图表实例,立即生成图表
+            if (window.chartInstance) {
+                // 假设generateChart是全局函数
+                if (typeof window.generateChart === 'function') {
+                    window.generateChart();
+                }
+            }
+        });
+    });
+    
+    // 初始化时设置当前选中的图表类型为活动状态
+    const currentChartType = chartTypeSelect.value;
+    const activeItem = document.querySelector(`.chart-type-item[data-chart-type="${currentChartType}"]`);
+    if (activeItem) {
+        activeItem.classList.add('active');
+    }
+    
+    // 当下拉框选择变化时,同步更新活动预览项
+    chartTypeSelect.addEventListener('change', function() {
+        const selectedType = this.value;
+        chartTypeItems.forEach(item => {
+            if (item.getAttribute('data-chart-type') === selectedType) {
+                item.classList.add('active');
+            } else {
+                item.classList.remove('active');
+            }
+        });
+    });
+} 

+ 11 - 0
apps/chart-maker/chart-icon.svg

@@ -0,0 +1,11 @@
+<svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <rect width="512" height="512" rx="80" fill="#4568DC"/>
+  <rect x="96" y="352" width="64" height="96" rx="8" fill="white"/>
+  <rect x="192" y="256" width="64" height="192" rx="8" fill="white"/>
+  <rect x="288" y="160" width="64" height="288" rx="8" fill="white"/>
+  <rect x="384" y="64" width="64" height="384" rx="8" fill="white"/>
+  <path d="M96 144C96 136.268 102.268 130 110 130H434C441.732 130 448 136.268 448 144V144C448 151.732 441.732 158 434 158H110C102.268 158 96 151.732 96 144V144Z" fill="#B06AB3" fill-opacity="0.6"/>
+  <path d="M96 96C96 88.268 102.268 82 110 82H338C345.732 82 352 88.268 352 96V96C352 103.732 345.732 110 338 110H110C102.268 110 96 103.732 96 96V96Z" fill="#B06AB3" fill-opacity="0.6"/>
+  <circle cx="448" cy="96" r="16" fill="white"/>
+  <circle cx="352" cy="144" r="16" fill="white"/>
+</svg>

+ 317 - 0
apps/chart-maker/index.html

@@ -0,0 +1,317 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>图表生成器 - 快速创建专业数据可视化</title>
+    <link rel="stylesheet" href="style.css">
+    <link rel="icon" href="chart-icon.svg" type="image/svg+xml">
+    <!-- 引入Chart.js及其插件 (改为本地引用) -->
+    <script src="lib/chart.min.js"></script>
+    <!-- 引入html2canvas库用于导出图片 (改为本地引用) -->
+    <script src="lib/html2canvas.min.js"></script>
+    <!-- 引入XLSX库用于解析Excel文件 (改为本地引用) -->
+    <script src="lib/xlsx.full.min.js"></script>
+    <!-- 引入Hammer.js用于图表交互 (改为本地引用) -->
+    <script src="lib/hammer.min.js"></script>
+    <!-- 引入Chart.js缩放插件 (改为本地引用) -->
+    <script src="lib/chartjs-plugin-zoom.min.js"></script>
+    <!-- 引入Chart.js数据标签插件 -->
+    <script src="lib/chartjs-plugin-datalabels.min.js"></script>
+</head>
+<body>
+    <div class="wrapper" id="pageContainer">
+        <div class="page-header">
+            <a href="https://www.baidufe.com/fehelper/index/index.html" target="_blank" class="header-link">
+                <img src="../static/img/fe-16.png" alt="fehelper"/>
+                <span class="fehelper-text">FeHelper</span>
+            </a>
+            <span class="page-title-suffix">:图表生成器</span>
+        </div>
+    </div>
+
+    <main class="container">
+        <div class="app-container">
+            <!-- 左侧面板:数据输入区 -->
+            <div class="sidebar">
+                <div class="panel">
+                    <h2>数据输入</h2>
+                    <div class="input-group">
+                        <label>数据输入方式</label>
+                        <div class="input-methods">
+                            <label><input type="radio" name="data-input-method" value="manual" checked> 手动录入</label>
+                            <label><input type="radio" name="data-input-method" value="upload-csv"> 上传Excel/CSV</label>
+                        </div>
+                    </div>
+
+                    <div class="input-group" id="manual-format-container" style="display: none;">
+                        <label for="manual-format">选择数据格式</label>
+                        <select id="manual-format">
+                            <option value="simple">简单数据 (标签,数值)</option>
+                            <option value="series">系列数据 (多组)</option>
+                            <option value="csv">CSV格式</option>
+                        </select>
+                    </div>
+
+                    <div class="input-group" id="simple-data-container">
+                        <label for="data-input">输入数据 (每行一条,格式: 标签,数值)</label>
+                        <textarea id="data-input" rows="8" placeholder="产品A,120&#10;产品B,80&#10;产品C,60&#10;产品D,40&#10;产品E,30"></textarea>
+                    </div>
+
+                    <div class="input-group" id="series-data-container" style="display: none;">
+                        <label for="series-data-input">输入系列数据 (每行一个系列,格式: 系列名,值1,值2...)</label>
+                        <textarea id="series-data-input" rows="8" placeholder="一季度,120,80,60,40,30&#10;二季度,130,70,65,45,35&#10;三季度,140,90,55,48,38"></textarea>
+                        <label for="series-labels">标签 (逗号分隔)</label>
+                        <input type="text" id="series-labels" placeholder="产品A,产品B,产品C,产品D,产品E">
+                    </div>
+
+                    <div class="input-group" id="csv-data-container" style="display: none;">
+                        <label for="csv-data-input">粘贴CSV数据 (首行为标题)</label>
+                        <textarea id="csv-data-input" rows="8" placeholder="类别,一季度,二季度,三季度&#10;产品A,120,130,140&#10;产品B,80,70,90&#10;产品C,60,65,55&#10;产品D,40,45,48"></textarea>
+                        <div class="csv-options">
+                            <label>
+                                <input type="checkbox" id="first-row-header" checked> 
+                                第一行为标题
+                            </label>
+                            <label>
+                                <input type="checkbox" id="first-col-labels" checked> 
+                                第一列为标签
+                            </label>
+                        </div>
+                    </div>
+
+                    <div class="input-group">
+                        <input type="file" id="file-upload" accept=".csv, .xlsx">
+                        <label for="file-upload">选择CSV/Excel文件</label>
+                    </div>
+
+                    <div class="input-group">
+                        <button id="generate-btn" class="btn btn-primary">生成图表</button>
+                        <button id="sample-data-btn" class="btn btn-secondary">加载样例数据</button>
+                    </div>
+                </div>
+                
+                <div class="panel">
+                    <h2>图表设置</h2>
+                    <input type="hidden" id="chart-type" value="bar">
+                    <div class="input-group">
+                        <label for="chart-title">图表标题</label>
+                        <input type="text" id="chart-title" placeholder="图表标题">
+                    </div>
+                    
+                    <div class="input-group">
+                        <label for="x-axis-label">X轴标签</label>
+                        <input type="text" id="x-axis-label" placeholder="X轴标签">
+                    </div>
+                    
+                    <div class="input-group">
+                        <label for="y-axis-label">Y轴标签</label>
+                        <input type="text" id="y-axis-label" placeholder="Y轴标签">
+                    </div>
+
+                    <div class="input-group">
+                        <label for="color-scheme">颜色方案</label>
+                        <select id="color-scheme">
+                            <option value="default">默认方案</option>
+                            <option value="pastel">柔和色系</option>
+                            <option value="bright">明亮色系</option>
+                            <option value="cool">冷色调</option>
+                            <option value="warm">暖色调</option>
+                            <option value="corporate">企业风格</option>
+                            <option value="contrast">高对比度</option>
+                            <option value="rainbow">彩虹色系</option>
+                            <option value="earth">大地色系</option>
+                            <option value="ocean">海洋色系</option>
+                            <option value="vintage">复古色系</option>
+                        </select>
+                    </div>
+
+                    <div class="input-group">
+                        <label for="legend-position">图例位置</label>
+                        <select id="legend-position">
+                            <option value="top">顶部</option>
+                            <option value="right">右侧</option>
+                            <option value="bottom">底部</option>
+                            <option value="left">左侧</option>
+                            <option value="none">不显示</option>
+                        </select>
+                    </div>
+                    
+                    <div class="input-group checkbox-group">
+                        <!-- 删除"显示网格线"和"启用动画"复选框 -->
+                    </div>
+                </div>
+            </div>
+
+            <!-- 右侧面板:图表显示区域 -->
+            <div class="main-content">
+                <div class="chart-container">
+                    <div class="chart-actions">
+                        <div class="checkbox-group" style="text-align: left;">
+                            <label><input type="checkbox" id="show-grid-lines" checked> 显示网格线</label>
+                            <label><input type="checkbox" id="animate-chart" checked> 启用动画</label>
+                        </div>
+                        <button id="export-png-btn" class="btn btn-primary" disabled>导出PNG</button>
+                        <button id="export-jpg-btn" class="btn btn-primary" disabled>导出JPG</button>
+                        <button id="copy-img-btn" class="btn btn-secondary" disabled>复制图像</button>
+                    </div>
+                    <div id="chart-wrapper">
+                        <canvas id="chart-canvas"></canvas>
+                    </div>
+                    
+                    <div class="chart-type-gallery">
+                        <h3>图表类型选择</h3>
+                        <div class="chart-type-scroller">
+                            <div class="chart-type-group">
+                                <h4>基础图表</h4>
+                                <div class="chart-type-items">
+                                    <div class="chart-type-item" data-chart-type="bar">
+                                        <svg width="60" height="60" viewBox="0 0 60 60">
+                                            <rect x="10" y="10" width="8" height="40" fill="#4e73df" />
+                                            <rect x="26" y="20" width="8" height="30" fill="#4e73df" />
+                                            <rect x="42" y="30" width="8" height="20" fill="#4e73df" />
+                                        </svg>
+                                        <span>柱状图</span>
+                                    </div>
+                                    <div class="chart-type-item" data-chart-type="horizontalBar">
+                                        <svg width="60" height="60" viewBox="0 0 60 60">
+                                            <rect x="10" y="10" width="40" height="8" fill="#4e73df" />
+                                            <rect x="10" y="26" width="30" height="8" fill="#4e73df" />
+                                            <rect x="10" y="42" width="20" height="8" fill="#4e73df" />
+                                        </svg>
+                                        <span>水平柱状图</span>
+                                    </div>
+                                    <div class="chart-type-item" data-chart-type="line">
+                                        <svg width="60" height="60" viewBox="0 0 60 60">
+                                            <polyline points="10,45 25,15 40,30 55,10" stroke="#4e73df" stroke-width="2" fill="none" />
+                                            <circle cx="10" cy="45" r="3" fill="#4e73df" />
+                                            <circle cx="25" cy="15" r="3" fill="#4e73df" />
+                                            <circle cx="40" cy="30" r="3" fill="#4e73df" />
+                                            <circle cx="55" cy="10" r="3" fill="#4e73df" />
+                                        </svg>
+                                        <span>折线图</span>
+                                    </div>
+                                    <div class="chart-type-item" data-chart-type="area">
+                                        <svg width="60" height="60" viewBox="0 0 60 60">
+                                            <path d="M10,45 L25,15 L40,30 L55,10 L55,50 L10,50 Z" fill="#4e73df" opacity="0.3" />
+                                            <polyline points="10,45 25,15 40,30 55,10" stroke="#4e73df" stroke-width="2" fill="none" />
+                                            <circle cx="10" cy="45" r="3" fill="#4e73df" />
+                                            <circle cx="25" cy="15" r="3" fill="#4e73df" />
+                                            <circle cx="40" cy="30" r="3" fill="#4e73df" />
+                                            <circle cx="55" cy="10" r="3" fill="#4e73df" />
+                                        </svg>
+                                        <span>面积图</span>
+                                    </div>
+                                </div>
+                            </div>
+                            
+                            <div class="chart-type-group">
+                                <h4>饼图/环形图</h4>
+                                <div class="chart-type-items">
+                                    <div class="chart-type-item" data-chart-type="pie">
+                                        <svg width="60" height="60" viewBox="0 0 60 60">
+                                            <path d="M30,30 L30,5 A25,25 0 0,1 55,30 Z" fill="#4e73df" />
+                                            <path d="M30,30 L55,30 A25,25 0 0,1 30,55 Z" fill="#1cc88a" />
+                                            <path d="M30,30 L30,55 A25,25 0 0,1 5,30 Z" fill="#36b9cc" />
+                                            <path d="M30,30 L5,30 A25,25 0 0,1 30,5 Z" fill="#f6c23e" />
+                                        </svg>
+                                        <span>饼图</span>
+                                    </div>
+                                    <div class="chart-type-item" data-chart-type="doughnut">
+                                        <svg width="60" height="60" viewBox="0 0 60 60">
+                                            <path d="M30,30 L30,5 A25,25 0 0,1 55,30 L45,30 A15,15 0 0,0 30,15 Z" fill="#4e73df" />
+                                            <path d="M30,30 L55,30 A25,25 0 0,1 30,55 L30,45 A15,15 0 0,0 45,30 Z" fill="#1cc88a" />
+                                            <path d="M30,30 L30,55 A25,25 0 0,1 5,30 L15,30 A15,15 0 0,0 30,45 Z" fill="#36b9cc" />
+                                            <path d="M30,30 L5,30 A25,25 0 0,1 30,5 L30,15 A15,15 0 0,0 15,30 Z" fill="#f6c23e" />
+                                        </svg>
+                                        <span>环形图</span>
+                                    </div>
+                                    <div class="chart-type-item" data-chart-type="polarArea">
+                                        <svg width="60" height="60" viewBox="0 0 60 60">
+                                            <path d="M30,30 L30,10 A20,20 0 0,1 50,30 Z" fill="#4e73df" />
+                                            <path d="M30,30 L50,30 A20,20 0 0,1 30,50 Z" fill="#1cc88a" />
+                                            <path d="M30,30 L30,50 A20,20 0 0,1 10,30 Z" fill="#36b9cc" />
+                                            <path d="M30,30 L10,30 A20,20 0 0,1 30,10 Z" fill="#f6c23e" />
+                                        </svg>
+                                        <span>极地面积图</span>
+                                    </div>
+                                </div>
+                            </div>
+                            
+                            <div class="chart-type-group">
+                                <h4>高级图表</h4>
+                                <div class="chart-type-items">
+                                    <div class="chart-type-item" data-chart-type="scatter">
+                                        <svg width="60" height="60" viewBox="0 0 60 60">
+                                            <circle cx="15" cy="20" r="3" fill="#4e73df" />
+                                            <circle cx="25" cy="40" r="3" fill="#4e73df" />
+                                            <circle cx="35" cy="15" r="3" fill="#4e73df" />
+                                            <circle cx="45" cy="30" r="3" fill="#4e73df" />
+                                            <circle cx="20" cy="45" r="3" fill="#4e73df" />
+                                        </svg>
+                                        <span>散点图</span>
+                                    </div>
+                                    <div class="chart-type-item" data-chart-type="radar">
+                                        <svg width="60" height="60" viewBox="0 0 60 60">
+                                            <polygon points="30,10 45,20 40,40 20,40 15,20" stroke="#4e73df" stroke-width="1" fill="#4e73df" fill-opacity="0.2" />
+                                            <line x1="30" y1="30" x2="30" y2="10" stroke="#ccc" />
+                                            <line x1="30" y1="30" x2="45" y2="20" stroke="#ccc" />
+                                            <line x1="30" y1="30" x2="40" y2="40" stroke="#ccc" />
+                                            <line x1="30" y1="30" x2="20" y2="40" stroke="#ccc" />
+                                            <line x1="30" y1="30" x2="15" y2="20" stroke="#ccc" />
+                                        </svg>
+                                        <span>雷达图</span>
+                                    </div>
+                                    <div class="chart-type-item" data-chart-type="bubble">
+                                        <svg width="60" height="60" viewBox="0 0 60 60">
+                                            <circle cx="15" cy="25" r="5" fill="#4e73df" fill-opacity="0.7" />
+                                            <circle cx="30" cy="15" r="7" fill="#4e73df" fill-opacity="0.7" />
+                                            <circle cx="45" cy="35" r="4" fill="#4e73df" fill-opacity="0.7" />
+                                            <circle cx="25" cy="45" r="6" fill="#4e73df" fill-opacity="0.7" />
+                                        </svg>
+                                        <span>气泡图</span>
+                                    </div>
+                                    <div class="chart-type-item" data-chart-type="stackedBar">
+                                        <svg width="60" height="60" viewBox="0 0 60 60">
+                                            <rect x="10" y="10" width="8" height="15" fill="#4e73df" />
+                                            <rect x="10" y="25" width="8" height="15" fill="#1cc88a" />
+                                            <rect x="26" y="20" width="8" height="10" fill="#4e73df" />
+                                            <rect x="26" y="30" width="8" height="10" fill="#1cc88a" />
+                                            <rect x="42" y="30" width="8" height="5" fill="#4e73df" />
+                                            <rect x="42" y="35" width="8" height="5" fill="#1cc88a" />
+                                        </svg>
+                                        <span>堆叠柱状图</span>
+                                    </div>
+                                    <div class="chart-type-item" data-chart-type="stackedArea">
+                                        <svg width="60" height="60" viewBox="0 0 60 60">
+                                            <path d="M10,45 L25,35 L40,30 L55,20 L55,50 L10,50 Z" fill="#4e73df" opacity="0.3" />
+                                            <path d="M10,30 L25,20 L40,20 L55,10 L55,20 L40,30 L25,35 L10,45 Z" fill="#1cc88a" opacity="0.3" />
+                                            <polyline points="10,30 25,20 40,20 55,10" stroke="#1cc88a" stroke-width="2" fill="none" />
+                                            <polyline points="10,45 25,35 40,30 55,20" stroke="#4e73df" stroke-width="2" fill="none" />
+                                        </svg>
+                                        <span>堆叠面积图</span>
+                                    </div>
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+                
+                <div class="tips-panel">
+                    <h3>使用提示</h3>
+                    <ul>
+                        <li>选择适合您数据的图表类型:数值比较用柱状图,趋势用折线图,占比用饼图</li>
+                        <li>点击图表类型预览图可以直接切换图表</li>
+                        <li>生成图表后可以导出为PNG或JPG格式,方便插入PPT或Word文档</li>
+                        <li>使用"数据标签"功能可以在图表上直接显示数值</li>
+                        <li>尝试不同的颜色方案,为您的报告增添专业感</li>
+                    </ul>
+                </div>
+            </div>
+        </div>
+    </main>
+    
+    <script src="main.js"></script>
+    <script src="chart-generator.js"></script>
+</body>
+</html> 

La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 6 - 0
apps/chart-maker/lib/chart.min.js


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 6 - 0
apps/chart-maker/lib/chartjs-plugin-datalabels.min.js


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 6 - 0
apps/chart-maker/lib/chartjs-plugin-zoom.min.js


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 5 - 0
apps/chart-maker/lib/hammer.min.js


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 19 - 0
apps/chart-maker/lib/html2canvas.min.js


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 1 - 0
apps/chart-maker/lib/xlsx.full.min.js


+ 712 - 0
apps/chart-maker/main.js

@@ -0,0 +1,712 @@
+document.addEventListener('DOMContentLoaded', function() {
+    // 获取DOM元素
+    const simpleDataContainer = document.getElementById('simple-data-container');
+    const seriesDataContainer = document.getElementById('series-data-container');
+    const csvDataContainer = document.getElementById('csv-data-container');
+    const generateBtn = document.getElementById('generate-btn');
+    const sampleDataBtn = document.getElementById('sample-data-btn');
+    const exportPngBtn = document.getElementById('export-png-btn');
+    const exportJpgBtn = document.getElementById('export-jpg-btn');
+    const copyImgBtn = document.getElementById('copy-img-btn');
+    const chartTypeSelect = document.getElementById('chart-type');
+    const manualFormatSelect = document.getElementById('manual-format');
+    const fileUploadInput = document.getElementById('file-upload');
+    const manualFormatContainer = document.getElementById('manual-format-container');
+    const manualInputContainers = [simpleDataContainer, seriesDataContainer, csvDataContainer];
+    
+    // 初始化显示状态
+    const initialMethod = document.querySelector('input[name="data-input-method"]:checked').value;
+    toggleManualInputs(initialMethod === 'manual');
+    fileUploadInput.parentElement.style.display = initialMethod === 'upload-csv' ? 'block' : 'none';
+
+    // 初始化图表类型画廊
+    initChartTypeGallery();
+
+    function toggleManualInputs(show) {
+        manualFormatContainer.style.display = show ? 'block' : 'none';
+        const selectedFormat = manualFormatSelect.value;
+        manualInputContainers.forEach(container => {
+            const containerId = container.id.split('-')[0]; // 'simple', 'series', 'csv'
+            container.style.display = (show && containerId === selectedFormat) ? 'block' : 'none';
+        });
+    }
+
+    // 初始化时调用updateChartTypeOptions函数
+    // 无论当前选择的是什么输入方式,都初始化图表类型
+    // 默认使用'series'格式以显示最多的图表类型选项
+    updateChartTypeOptions('series');
+
+    // 监听数据输入方式切换
+    document.querySelectorAll('input[name="data-input-method"]').forEach(radio => {
+        radio.addEventListener('change', function() {
+            const method = this.value;
+            toggleManualInputs(method === 'manual');
+            fileUploadInput.parentElement.style.display = method === 'upload-csv' ? 'block' : 'none';
+            
+            // 在切换到"上传Excel/CSV"时,更新图表类型选项为多系列数据
+            if (method === 'upload-csv') {
+                updateChartTypeOptions('series');
+            } else if (method === 'manual') {
+                // 切换回"手动录入"时,根据当前选择的格式更新图表类型选项
+                updateChartTypeOptions(manualFormatSelect.value);
+                uploadedData = null;
+                fileUploadInput.value = ''; // 清空文件选择
+            }
+        });
+    });
+
+    // 监听手动格式选择变化
+    manualFormatSelect.addEventListener('change', function() {
+        const format = this.value;
+        manualInputContainers.forEach(container => {
+            const containerId = container.id.split('-')[0];
+            container.style.display = (containerId === format) ? 'block' : 'none';
+        });
+        
+        // 更新图表类型选项
+        updateChartTypeOptions(format);
+    });
+
+    // 文件上传处理
+    fileUploadInput.addEventListener('change', function(event) {
+        const file = event.target.files[0];
+        if (!file) {
+            uploadedData = null;
+            return;
+        }
+
+        const reader = new FileReader();
+        reader.onload = function(e) {
+            try {
+                const data = new Uint8Array(e.target.result);
+                const workbook = XLSX.read(data, { type: 'array' });
+                const firstSheetName = workbook.SheetNames[0];
+                const worksheet = workbook.Sheets[firstSheetName];
+                const jsonData = XLSX.utils.sheet_to_json(worksheet, { header: 1 });
+                uploadedData = parseExcelData(jsonData);
+                showNotification('文件上传成功,可以点击"生成图表"');
+                
+                // 上传Excel文件后,更新图表类型选项为多系列数据类型
+                updateChartTypeOptions('series');
+            } catch (error) {
+                showNotification('文件解析失败: ' + error.message, true);
+                uploadedData = null;
+                fileUploadInput.value = ''; // 清空文件选择
+            }
+        };
+        reader.onerror = function() {
+            showNotification('文件读取失败', true);
+            uploadedData = null;
+            fileUploadInput.value = ''; // 清空文件选择
+        };
+        reader.readAsArrayBuffer(file);
+    });
+
+    // 生成图表按钮点击事件 (修改为独立的函数)
+    function generateChart() {
+        try {
+            console.log('开始生成图表...');
+            let parsedData;
+            const method = document.querySelector('input[name="data-input-method"]:checked').value;
+            console.log('数据输入方式:', method);
+
+            if (method === 'upload-csv' && uploadedData) {
+                parsedData = uploadedData;
+                console.log('使用上传的数据');
+            } else if (method === 'manual') {
+                parsedData = parseInputData(); // 使用现有的手动数据解析函数
+                console.log('使用手动输入的数据');
+            } else if (method === 'upload-csv' && !uploadedData) {
+                throw new Error('请先上传文件');
+            } else {
+                throw new Error('请选择有效的数据输入方式并提供数据');
+            }
+            
+            console.log('解析后的数据:', parsedData);
+            
+            if (!parsedData || 
+                (parsedData.labels && parsedData.labels.length === 0) || 
+                (parsedData.datasets && parsedData.datasets.length === 0)) {
+                throw new Error('无法解析数据或数据为空');
+            }
+            
+            // 保存数据到全局变量,方便其他函数访问
+            window.chartData = parsedData;
+            
+            const chartSettings = getChartSettings();
+            
+            // 将简单数据标记添加到设置中
+            if (parsedData.isSimpleData) {
+                chartSettings.isSimpleData = true;
+            }
+            
+            console.log('图表设置:', chartSettings);
+            
+            // 调用chart-generator.js中的createChart函数
+            if (typeof createChart !== 'function') {
+                throw new Error('createChart函数未定义,请确保chart-generator.js正确加载');
+            }
+            
+            createChart(parsedData, chartSettings);
+            console.log('图表生成成功');
+            
+            exportPngBtn.disabled = false;
+            exportJpgBtn.disabled = false;
+            copyImgBtn.disabled = false;
+        } catch (error) {
+            console.error('生成图表时出错:', error);
+            showNotification(error.message, true);
+        }
+    }
+
+    // 将generateChart函数暴露为全局函数
+    window.generateChart = generateChart;
+
+    generateBtn.addEventListener('click', generateChart);
+
+    // 监听图表设置的变化事件,实时更新图表 (仅在有图表时)
+    ['chart-title', 'x-axis-label', 'y-axis-label', 'color-scheme', 'legend-position'].forEach(id => {
+        document.getElementById(id).addEventListener('input', function() {
+            const instance = getChartInstance();
+            if (instance) { // 检查是否有图表实例
+                generateChart(); // 重新生成图表以应用设置
+            }
+        });
+    });
+    
+    // 初始化图表类型画廊
+    function initChartTypeGallery() {
+        console.log('初始化图表类型预览画廊...');
+        
+        // 获取所有图表类型预览项
+        const chartTypeItems = document.querySelectorAll('.chart-type-item');
+        console.log(`找到${chartTypeItems.length}个图表类型预览项`);
+        
+        // 为每个预览项添加点击事件
+        chartTypeItems.forEach(item => {
+            item.addEventListener('click', function() {
+                const chartType = this.getAttribute('data-chart-type');
+                console.log('选择了图表类型:', chartType);
+                
+                // 更新活动状态
+                chartTypeItems.forEach(item => item.classList.remove('active'));
+                this.classList.add('active');
+                
+                // 无论是否有图表实例都应该重新生成图表
+                // 删除之前的检查条件,始终调用generateChart
+                generateChart();
+            });
+        });
+        
+        // 初始设置默认图表类型为活动状态
+        const defaultChartType = "bar"; // 默认为柱状图
+        const activeItem = document.querySelector(`.chart-type-item[data-chart-type="${defaultChartType}"]`);
+        if (activeItem) {
+            activeItem.classList.add('active');
+        }
+    }
+    
+    // 加载样例数据
+    sampleDataBtn.addEventListener('click', function() {
+        // 确保选中"手动录入"选项
+        const manualRadio = document.querySelector('input[name="data-input-method"][value="manual"]');
+        if (manualRadio && !manualRadio.checked) {
+            manualRadio.checked = true;
+            // 触发change事件以显示相关的输入控件
+            manualRadio.dispatchEvent(new Event('change'));
+        }
+        
+        const currentFormat = manualFormatSelect.value;
+        
+        switch(currentFormat) {
+            case 'simple':
+                document.getElementById('data-input').value = 
+                    '智能手机,2458\n平板电脑,1678\n笔记本电脑,1892\n智能手表,986\n耳机,1342';
+                document.getElementById('chart-title').value = '2023年电子产品销量(万台)';
+                document.getElementById('x-axis-label').value = '产品类别';
+                document.getElementById('y-axis-label').value = '销量(万台)';
+                break;
+            case 'series':
+                document.getElementById('series-data-input').value = 
+                    '第一季度,2458,1678,1892,986,1342\n第二季度,2612,1524,1953,1104,1587\n第三季度,2845,1701,2135,1287,1643\n第四季度,3256,1835,2278,1452,1821';
+                document.getElementById('series-labels').value = 
+                    '智能手机,平板电脑,笔记本电脑,智能手表,耳机';
+                document.getElementById('chart-title').value = '2023年电子产品季度销量(万台)';
+                document.getElementById('x-axis-label').value = '产品类别';
+                document.getElementById('y-axis-label').value = '销量(万台)';
+                break;
+            case 'csv':
+                document.getElementById('csv-data-input').value = 
+                    '品牌,2021年,2022年,2023年\n华为,786.5,845.2,921.6\n小米,651.2,712.8,768.3\n苹果,598.7,642.1,724.5\n三星,542.3,575.8,612.4\nOPPO,487.6,524.3,547.8\nvivo,452.8,501.7,532.9';
+                document.getElementById('chart-title').value = '国内智能手机品牌销量趋势(万台)';
+                document.getElementById('x-axis-label').value = '品牌';
+                document.getElementById('y-axis-label').value = '销量(万台)';
+                break;
+        }
+        
+        // 提示用户下一步操作
+        showNotification('已加载样例数据,点击"生成图表"查看效果');
+    });
+    
+    // 显示通知
+    function showNotification(message, isError = false) {
+        // 移除现有通知
+        const existingNotification = document.querySelector('.notification');
+        if (existingNotification) {
+            existingNotification.remove();
+        }
+        
+        // 创建新通知
+        const notification = document.createElement('div');
+        notification.className = 'notification' + (isError ? ' error' : '');
+        notification.textContent = message;
+        
+        // 添加到文档
+        document.body.appendChild(notification);
+        
+        // 显示通知
+        setTimeout(() => notification.classList.add('show'), 10);
+        
+        // 自动隐藏
+        setTimeout(() => {
+            notification.classList.remove('show');
+            setTimeout(() => notification.remove(), 300);
+        }, 3000);
+    }
+    
+    // 解析输入数据
+    function parseInputData() {
+        const currentFormat = manualFormatSelect.value;
+        
+        switch(currentFormat) {
+            case 'simple':
+                return parseSimpleData();
+            case 'series':
+                return parseSeriesData();
+            case 'csv':
+                return parseCsvData();
+            default:
+                throw new Error('未知的数据格式');
+        }
+    }
+    
+    // 解析简单数据
+    function parseSimpleData() {
+        const input = document.getElementById('data-input').value.trim();
+        if (!input) {
+            throw new Error('请输入数据');
+        }
+        
+        const lines = input.split('\n').filter(line => line.trim());
+        const labels = [];
+        const data = [];
+        
+        lines.forEach(line => {
+            const parts = line.split(',').map(part => part.trim());
+            if (parts.length >= 2) {
+                labels.push(parts[0]);
+                const value = parseFloat(parts[1]);
+                if (isNaN(value)) {
+                    throw new Error(`"${parts[1]}"不是有效的数值`);
+                }
+                data.push(value);
+            }
+        });
+        
+        if (labels.length === 0 || data.length === 0) {
+            throw new Error('无法解析数据,请检查格式是否正确');
+        }
+        
+        return {
+            labels: labels,
+            datasets: [{
+                data: data,
+                label: '数值'
+            }],
+            isSimpleData: true // 添加标记,表示这是简单数据格式
+        };
+    }
+    
+    // 解析系列数据
+    function parseSeriesData() {
+        const input = document.getElementById('series-data-input').value.trim();
+        const labelsInput = document.getElementById('series-labels').value.trim();
+        
+        if (!input) {
+            throw new Error('请输入系列数据');
+        }
+        
+        if (!labelsInput) {
+            throw new Error('请输入标签数据');
+        }
+        
+        const lines = input.split('\n').filter(line => line.trim());
+        const labels = labelsInput.split(',').map(label => label.trim());
+        const datasets = [];
+        
+        lines.forEach(line => {
+            const parts = line.split(',').map(part => part.trim());
+            if (parts.length >= 2) {
+                const seriesName = parts[0];
+                const seriesData = parts.slice(1).map(val => {
+                    const value = parseFloat(val);
+                    if (isNaN(value)) {
+                        throw new Error(`"${val}"不是有效的数值`);
+                    }
+                    return value;
+                });
+                
+                datasets.push({
+                    label: seriesName,
+                    data: seriesData
+                });
+            }
+        });
+        
+        if (labels.length === 0 || datasets.length === 0) {
+            throw new Error('无法解析数据,请检查格式是否正确');
+        }
+        
+        return {
+            labels: labels,
+            datasets: datasets
+        };
+    }
+    
+    // 解析CSV数据
+    function parseCsvData() {
+        const input = document.getElementById('csv-data-input').value.trim();
+        const firstRowHeader = document.getElementById('first-row-header').checked;
+        const firstColLabels = document.getElementById('first-col-labels').checked;
+        
+        if (!input) {
+            throw new Error('请输入CSV数据');
+        }
+        
+        const lines = input.split('\n').filter(line => line.trim());
+        if (lines.length < 2) {
+            throw new Error('CSV数据至少需要两行');
+        }
+        
+        const rows = lines.map(line => line.split(',').map(cell => cell.trim()));
+        
+        let labels = [];
+        let datasets = [];
+        
+        if (firstRowHeader && firstColLabels) {
+            // 第一行是标题,第一列是标签
+            labels = rows.slice(1).map(row => row[0]);
+            
+            const headers = rows[0].slice(1);
+            headers.forEach((header, i) => {
+                const data = rows.slice(1).map(row => {
+                    const value = parseFloat(row[i+1]);
+                    if (isNaN(value)) {
+                        throw new Error(`"${row[i+1]}"不是有效的数值`);
+                    }
+                    return value;
+                });
+                
+                datasets.push({
+                    label: header,
+                    data: data
+                });
+            });
+        } else if (firstRowHeader && !firstColLabels) {
+            // 第一行是标题,但第一列不是标签
+            labels = Array.from({length: rows[0].length}, (_, i) => `数据${i+1}`);
+            
+            const headers = rows[0];
+            headers.forEach((header, i) => {
+                const data = rows.slice(1).map(row => {
+                    const value = parseFloat(row[i]);
+                    if (isNaN(value)) {
+                        throw new Error(`"${row[i]}"不是有效的数值`);
+                    }
+                    return value;
+                });
+                
+                datasets.push({
+                    label: header,
+                    data: data
+                });
+            });
+        } else if (!firstRowHeader && firstColLabels) {
+            // 第一行不是标题,第一列是标签
+            labels = rows.map(row => row[0]);
+            
+            for (let i = 1; i < rows[0].length; i++) {
+                const data = rows.map(row => {
+                    const value = parseFloat(row[i]);
+                    if (isNaN(value)) {
+                        throw new Error(`"${row[i]}"不是有效的数值`);
+                    }
+                    return value;
+                });
+                
+                datasets.push({
+                    label: `系列${i}`,
+                    data: data
+                });
+            }
+        } else {
+            // 第一行不是标题,第一列也不是标签
+            labels = Array.from({length: rows.length}, (_, i) => `标签${i+1}`);
+            
+            for (let i = 0; i < rows[0].length; i++) {
+                const data = rows.map(row => {
+                    const value = parseFloat(row[i]);
+                    if (isNaN(value)) {
+                        throw new Error(`"${row[i]}"不是有效的数值`);
+                    }
+                    return value;
+                });
+                
+                datasets.push({
+                    label: `系列${i+1}`,
+                    data: data
+                });
+            }
+        }
+        
+        if (labels.length === 0 || datasets.length === 0) {
+            throw new Error('无法解析数据,请检查格式是否正确');
+        }
+        
+        return {
+            labels: labels,
+            datasets: datasets
+        };
+    }
+    
+    // 获取图表设置
+    function getChartSettings() {
+        // 从活跃的图表类型项获取图表类型,而不是从下拉框
+        let chartType = 'bar'; // 默认值
+        const activeChartTypeItem = document.querySelector('.chart-type-item.active');
+        if (activeChartTypeItem) {
+            chartType = activeChartTypeItem.getAttribute('data-chart-type');
+        }
+        
+        return {
+            type: chartType,
+            title: document.getElementById('chart-title').value,
+            xAxisLabel: document.getElementById('x-axis-label').value,
+            yAxisLabel: document.getElementById('y-axis-label').value,
+            colorScheme: document.getElementById('color-scheme').value,
+            legendPosition: document.getElementById('legend-position').value,
+            showGridLines: document.getElementById('show-grid-lines').checked,
+            animateChart: document.getElementById('animate-chart').checked
+        };
+    }
+    
+    // 导出PNG图像
+    exportPngBtn.addEventListener('click', function() {
+        exportChart('png');
+    });
+    
+    // 导出JPG图像
+    exportJpgBtn.addEventListener('click', function() {
+        exportChart('jpg');
+    });
+    
+    // 复制图像到剪贴板
+    copyImgBtn.addEventListener('click', function() {
+        copyChartToClipboard();
+    });
+    
+    // 导出图表为图像
+    function exportChart(format) {
+        const chartWrapper = document.getElementById('chart-wrapper');
+        
+        // 创建加载指示器
+        const loadingOverlay = document.createElement('div');
+        loadingOverlay.className = 'loading-overlay';
+        loadingOverlay.innerHTML = '<div class="loading-spinner"></div>';
+        chartWrapper.appendChild(loadingOverlay);
+        
+        setTimeout(() => {
+            // 获取原始canvas的尺寸
+            const originalCanvas = document.getElementById('chart-canvas');
+            const width = originalCanvas.width;
+            const height = originalCanvas.height;
+            
+            // 创建一个临时的高分辨率canvas
+            const tempCanvas = document.createElement('canvas');
+            const tempCtx = tempCanvas.getContext('2d');
+            
+            // 设置更高的分辨率
+            const scale = 8; // 提升到8倍分辨率
+            tempCanvas.width = width * scale;
+            tempCanvas.height = height * scale;
+            
+            // 优化渲染质量
+            tempCtx.imageSmoothingEnabled = true;
+            tempCtx.imageSmoothingQuality = 'high';
+            
+            html2canvas(originalCanvas, {
+                backgroundColor: '#ffffff',
+                scale: scale, // 使用8倍缩放
+                width: width,
+                height: height,
+                useCORS: true,
+                allowTaint: true,
+                logging: false,
+                imageTimeout: 0,
+                onclone: (document) => {
+                    const clonedCanvas = document.getElementById('chart-canvas');
+                    if(clonedCanvas) {
+                        clonedCanvas.style.width = width + 'px';
+                        clonedCanvas.style.height = height + 'px';
+                    }
+                },
+                // 添加高级渲染选项
+                canvas: tempCanvas,
+                renderCallback: (canvas) => {
+                    // 应用锐化效果
+                    const ctx = canvas.getContext('2d');
+                    ctx.filter = 'contrast(1.1) saturate(1.2)';
+                }
+            }).then(canvas => {
+                // 移除加载指示器
+                loadingOverlay.remove();
+                
+                // 导出图像时使用更高的质量设置
+                let imgUrl;
+                if (format === 'jpg') {
+                    // JPEG使用最高质量
+                    imgUrl = canvas.toDataURL('image/jpeg', 1.0);
+                } else {
+                    // PNG使用无损压缩
+                    imgUrl = canvas.toDataURL('image/png');
+                }
+                
+                // 创建下载链接
+                const link = document.createElement('a');
+                const chartTitle = document.getElementById('chart-title').value || '图表';
+                const fileName = `${chartTitle.replace(/[^\w\u4e00-\u9fa5]/g, '_')}_Ultra_HD.${format}`;
+                
+                link.download = fileName;
+                link.href = imgUrl;
+                link.click();
+                
+                showNotification(`已成功导出超高清${format.toUpperCase()}图像`);
+            }).catch(error => {
+                // 移除加载指示器
+                loadingOverlay.remove();
+                
+                showNotification('导出图像失败,请重试', true);
+                console.error('导出图像出错:', error);
+            });
+        }, 100);
+    }
+    
+    // 复制图表到剪贴板
+    function copyChartToClipboard() {
+        const chartWrapper = document.getElementById('chart-wrapper');
+        
+        // 创建加载指示器
+        const loadingOverlay = document.createElement('div');
+        loadingOverlay.className = 'loading-overlay';
+        loadingOverlay.innerHTML = '<div class="loading-spinner"></div>';
+        chartWrapper.appendChild(loadingOverlay);
+        
+        setTimeout(() => {
+            html2canvas(document.getElementById('chart-canvas'), {
+                backgroundColor: '#ffffff',
+                scale: 2
+            }).then(canvas => {
+                // 移除加载指示器
+                loadingOverlay.remove();
+                
+                canvas.toBlob(blob => {
+                    try {
+                        // 尝试使用现代API复制到剪贴板
+                        if (navigator.clipboard && navigator.clipboard.write) {
+                            const clipboardItem = new ClipboardItem({'image/png': blob});
+                            navigator.clipboard.write([clipboardItem])
+                                .then(() => {
+                                    showNotification('图表已复制到剪贴板');
+                                })
+                                .catch(err => {
+                                    console.error('剪贴板API错误:', err);
+                                    legacyCopyToClipboard(canvas);
+                                });
+                        } else {
+                            legacyCopyToClipboard(canvas);
+                        }
+                    } catch (e) {
+                        console.error('复制到剪贴板出错:', e);
+                        legacyCopyToClipboard(canvas);
+                    }
+                });
+            }).catch(error => {
+                // 移除加载指示器
+                loadingOverlay.remove();
+                
+                showNotification('复制图像失败,请重试', true);
+                console.error('复制图像出错:', error);
+            });
+        }, 100);
+    }
+    
+    // 兼容性较好的复制方法(通过创建临时链接)
+    function legacyCopyToClipboard(canvas) {
+        const imgUrl = canvas.toDataURL('image/png');
+        
+        // 创建临时链接
+        const link = document.createElement('a');
+        link.download = '图表.png';
+        link.href = imgUrl;
+        
+        showNotification('已准备下载图表,无法直接复制到剪贴板');
+        link.click();
+    }
+    
+    // 解析Excel数据
+    function parseExcelData(jsonData) {
+        if (!jsonData || jsonData.length < 2 || !jsonData[0] || jsonData[0].length < 2) {
+            throw new Error('Excel数据格式不正确,至少需要表头行和数据行');
+        }
+        
+        // 假设第一行为标题,第一列为标签
+        const labels = jsonData.slice(1).map(row => row && row[0] ? row[0].toString() : '');
+        const datasets = [];
+
+        const headers = jsonData[0].slice(1);
+        headers.forEach((header, i) => {
+            const data = jsonData.slice(1).map(row => {
+                // 确保每个单元格数据都是数值类型
+                if (!row || !row[i + 1]) return 0;
+                const value = parseFloat(row[i + 1]);
+                return isNaN(value) ? 0 : value;
+            });
+            
+            datasets.push({
+                label: header ? header.toString() : `系列${i+1}`,
+                data: data
+            });
+        });
+
+        return {
+            labels: labels,
+            datasets: datasets
+        };
+    }
+
+    // 从chart-generator.js中导入图表生成函数
+    function getChartInstance() {
+        return window.chartInstance;
+    }
+
+    function setChartInstance(instance) {
+        window.chartInstance = instance;
+    }
+
+    // 根据数据格式更新图表类型选项
+    function updateChartTypeOptions(dataFormat) {
+        // 由于移除了图表类型下拉框,这个函数现在仅记录当前数据格式,不再修改任何选项
+        console.log('当前数据格式:', dataFormat);
+        // 未来可以根据数据格式来调整图表类型画廊的可见性或提示,但现在不需要操作
+    }
+}); 

+ 646 - 0
apps/chart-maker/style.css

@@ -0,0 +1,646 @@
+/* 基本样式重置 */
+
+* {
+    margin: 0;
+    padding: 0;
+    box-sizing: border-box;
+}
+
+body {
+    font-family: 'PingFang SC', 'Microsoft YaHei', 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
+    line-height: 1.6;
+    color: #333;
+    background-color: #f5f7fa;
+    background-image: linear-gradient(to right, rgba(245, 247, 250, 0.8) 1px, transparent 1px),
+                      linear-gradient(to bottom, rgba(245, 247, 250, 0.8) 1px, transparent 1px);
+    background-size: 20px 20px;
+    padding-top: 15px;
+    max-width: 1200px;
+    margin: 0 auto;
+    font-size:100%;
+}
+
+.container {
+    width: 100%;
+    max-width: 1200px;
+    margin: 0 auto;
+    padding: 0 20px;
+}
+
+/* 主内容区 */
+main {
+    padding: 40px 0;
+}
+
+.app-container {
+    display: flex;
+    gap: 30px;
+    margin-bottom: 30px;
+}
+
+/* 左侧边栏 */
+.sidebar {
+    flex: 0 0 350px;
+    display: flex;
+    flex-direction: column;
+    gap: 15px;
+}
+
+/* 面板共用样式 */
+.panel {
+    background: white;
+    border-radius: 12px;
+    padding: 15px;
+    box-shadow: 0 10px 30px rgba(0, 0, 0, 0.08);
+    transition: box-shadow 0.3s;
+    position: relative;
+    overflow: hidden;
+}
+
+.panel:hover {
+    box-shadow: 0 15px 35px rgba(0, 0, 0, 0.12);
+}
+
+.panel::after {
+    content: '';
+    position: absolute;
+    top: 0;
+    left: 0;
+    width: 100%;
+    height: 4px;
+    background: linear-gradient(90deg, #4568dc, #b06ab3);
+}
+
+.panel h2 {
+    font-size: 1.4rem;
+    font-weight: 600;
+    margin-bottom: 20px;
+    color: #333;
+    border-bottom: 1px solid #eee;
+    padding-bottom: 15px;
+    position: relative;
+}
+
+.panel h2::after {
+    content: '';
+    position: absolute;
+    bottom: -1px;
+    left: 0;
+    width: 60px;
+    height: 3px;
+    background: linear-gradient(90deg, #4568dc, #b06ab3);
+    border-radius: 3px;
+}
+
+/* 输入组样式 */
+.input-group {
+    margin-bottom: 20px;
+}
+
+.input-group label {
+    display: block;
+    margin-bottom: 8px;
+    font-weight: 500;
+    color: #444;
+}
+
+.input-group input[type="text"],
+.input-group select,
+.input-group textarea {
+    width: 100%;
+    padding: 12px;
+    border: 1px solid #ddd;
+    border-radius: 8px;
+    font-size: 12.6px;
+    transition: all 0.3s;
+    background-color: #f9f9f9;
+}
+
+.input-group input[type="text"]:focus,
+.input-group select:focus,
+.input-group textarea:focus {
+    border-color: #4568dc;
+    outline: none;
+    box-shadow: 0 0 0 3px rgba(69, 104, 220, 0.2);
+    background-color: white;
+}
+
+.checkbox-group {
+    display: flex;
+    align-items: center;
+    position: relative;
+    left: -230px;
+}
+
+.checkbox-group label {
+    display: flex;
+    align-items: center;
+    cursor: pointer;
+    margin-right: 10px;
+}
+
+.checkbox-group input[type="checkbox"] {
+    margin-right: 2px;
+    accent-color: #4568dc;
+    width: 16px;
+    height: 16px;
+}
+
+.csv-options {
+    display: flex;
+    gap: 15px;
+    margin-top: 10px;
+}
+
+/* 按钮样式 */
+.btn {
+    display: inline-block;
+    padding: 12px 20px;
+    border: none;
+    border-radius: 8px;
+    font-size: 13.5px;
+    font-weight: 500;
+    cursor: pointer;
+    transition: all 0.3s;
+    position: relative;
+    overflow: hidden;
+}
+
+.btn::after {
+    content: '';
+    position: absolute;
+    top: 0;
+    left: -100%;
+    width: 100%;
+    height: 100%;
+    background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
+    transition: left 0.5s;
+}
+
+.btn:hover::after {
+    left: 100%;
+}
+
+.btn-primary {
+    background: linear-gradient(135deg, #4568dc, #5c7be5);
+    color: white;
+    box-shadow: 0 5px 15px rgba(69, 104, 220, 0.3);
+}
+
+.btn-primary:hover {
+    background: linear-gradient(135deg, #3a56bb, #4d69cd);
+    transform: translateY(-2px);
+    box-shadow: 0 8px 20px rgba(69, 104, 220, 0.4);
+}
+
+.btn-primary:disabled {
+    background: #a5b1e2;
+    cursor: not-allowed;
+    box-shadow: none;
+}
+
+.btn-secondary {
+    background: linear-gradient(135deg, #f0f2f5, #e4e7f0);
+    color: #444;
+    box-shadow: 0 5px 15px rgba(0, 0, 0, 0.05);
+}
+
+.btn-secondary:hover {
+    background: linear-gradient(135deg, #e4e6e9, #d8dce6);
+    transform: translateY(-2px);
+    box-shadow: 0 8px 20px rgba(0, 0, 0, 0.1);
+}
+
+.input-group .btn {
+    margin-right: 10px;
+}
+
+/* 右侧主要内容区 */
+.main-content {
+    flex: 1;
+    display: flex;
+    flex-direction: column;
+    gap: 25px;
+    max-width: 800px;
+    margin: 0 auto;
+}
+
+.chart-container {
+    background-color: #fff;
+    border-radius: 12px;
+    box-shadow: 0 10px 30px rgba(0, 0, 0, 0.08);
+    padding: 25px;
+    margin-bottom: 6px;
+    transition: box-shadow 0.3s;
+    position: relative;
+    overflow: hidden;
+    max-width: 100%;
+    overflow-x: auto;
+}
+
+.chart-container::after {
+    content: '';
+    position: absolute;
+    top: 0;
+    left: 0;
+    width: 100%;
+    height: 4px;
+    background: linear-gradient(90deg, #4568dc, #b06ab3);
+}
+
+.chart-container:hover {
+    box-shadow: 0 15px 35px rgba(0, 0, 0, 0.12);
+}
+
+#chart-wrapper {
+    flex: 1;
+    position: relative;
+    border-radius: 8px;
+    overflow: hidden;
+    margin: 0 0 20px 0;
+    border: 1px solid #e0e0e0;
+    padding: 15px;
+    background: white;
+    transition: all 0.3s ease;
+    box-shadow: inset 0 0 10px rgba(0, 0, 0, 0.03);
+}
+
+/* 添加图表区域默认背景样式 */
+#chart-wrapper:empty {
+    background-image: url("data:image/svg+xml,%3Csvg width='100%25' height='100%25' xmlns='http://www.w3.org/2000/svg'%3E%3Cdefs%3E%3ClinearGradient id='grad' x1='0%25' y1='0%25' x2='100%25' y2='100%25'%3E%3Cstop offset='0%25' style='stop-color:%23f0f4ff;stop-opacity:1' /%3E%3Cstop offset='100%25' style='stop-color:%23eef4ff;stop-opacity:1' /%3E%3C/linearGradient%3E%3Cpattern id='grid' width='40' height='40' patternUnits='userSpaceOnUse'%3E%3Cpath d='M 0 10 L 40 10 M 10 0 L 10 40 M 0 20 L 40 20 M 20 0 L 20 40 M 0 30 L 40 30 M 30 0 L 30 40' fill='none' stroke='%23e0e0e0' stroke-width='0.5'/%3E%3C/pattern%3E%3C/defs%3E%3Crect width='100%25' height='100%25' fill='url(%23grad)'/%3E%3Crect width='100%25' height='100%25' fill='url(%23grid)'/%3E%3Ctext x='50%25' y='50%25' font-family='Arial, sans-serif' font-size='16' font-weight='bold' text-anchor='middle' fill='%23aaaaaa' dominant-baseline='middle'%3E选择数据并点击"生成图表"%3C/text%3E%3Cg transform='translate(50%25, 40%25)'%3E%3Cpath d='M-40,-30 L-40,30 L40,30' stroke='%234568dc' stroke-width='2' fill='none' stroke-opacity='0.3'/%3E%3Ccircle cx='-20' cy='10' r='5' fill='%234568dc' fill-opacity='0.3'/%3E%3Ccircle cx='0' cy='-10' r='5' fill='%234568dc' fill-opacity='0.3'/%3E%3Ccircle cx='20' cy='0' r='5' fill='%234568dc' fill-opacity='0.3'/%3E%3Cpath d='M-20,10 L0,-10 L20,0' stroke='%234568dc' stroke-width='2' fill='none' stroke-opacity='0.3'/%3E%3C/g%3E%3C/svg%3E");
+    background-size: cover;
+    background-position: center;
+    min-height: 300px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+}
+
+.chart-actions {
+    display: flex;
+    justify-content: flex-end;
+    gap: 12px;
+    margin-bottom: 20px;
+    padding: 0 0 15px 0;
+    border-bottom: 1px solid #e0e0e0;
+}
+
+/* 提示面板 */
+.tips-panel {
+    background: white;
+    border-radius: 12px;
+    padding: 25px;
+    box-shadow: 0 10px 30px rgba(0, 0, 0, 0.08);
+    position: relative;
+    overflow: hidden;
+    max-width: 100%;
+    overflow-x: auto;
+}
+
+.tips-panel::after {
+    content: '';
+    position: absolute;
+    top: 0;
+    left: 0;
+    width: 100%;
+    height: 4px;
+    background: linear-gradient(90deg, #4568dc, #b06ab3);
+}
+
+.tips-panel h3 {
+    font-size: 1.2rem;
+    margin-bottom: 15px;
+    color: #333;
+    position: relative;
+    padding-bottom: 10px;
+}
+
+.tips-panel h3::after {
+    content: '';
+    position: absolute;
+    bottom: 0;
+    left: 0;
+    width: 40px;
+    height: 3px;
+    background: linear-gradient(90deg, #4568dc, #b06ab3);
+    border-radius: 3px;
+}
+
+.tips-panel ul {
+    padding-left: 20px;
+}
+
+.tips-panel li {
+    margin-bottom: 10px;
+    color: #555;
+    position: relative;
+}
+
+.tips-panel li::before {
+    content: '';
+    position: absolute;
+    left: -20px;
+    top: 8px;
+    width: 8px;
+    height: 8px;
+    background: linear-gradient(135deg, #4568dc, #b06ab3);
+    border-radius: 50%;
+}
+
+/* 响应式设计 */
+@media (max-width: 900px) {
+    .app-container {
+        flex-direction: column;
+    }
+    
+    .sidebar {
+        flex: initial;
+        width: 100%;
+    }
+    
+    .chart-container {
+        height: 400px;
+    }
+}
+
+/* 数据格式切换相关样式 */
+#simple-data-container,
+#series-data-container,
+#csv-data-container {
+    transition: all 0.3s ease;
+}
+
+/* 图表导出loading效果 */
+.loading-overlay {
+    position: absolute;
+    top: 0;
+    left: 0;
+    right: 0;
+    bottom: 0;
+    background: rgba(255, 255, 255, 0.8);
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    z-index: 10;
+    backdrop-filter: blur(3px);
+}
+
+.loading-spinner {
+    width: 45px;
+    height: 45px;
+    border: 4px solid rgba(69, 104, 220, 0.3);
+    border-radius: 50%;
+    border-top: 4px solid #4568dc;
+    animation: spin 1s linear infinite;
+    box-shadow: 0 0 10px rgba(69, 104, 220, 0.2);
+}
+
+@keyframes spin {
+    0% { transform: rotate(0deg); }
+    100% { transform: rotate(360deg); }
+}
+
+/* 通知弹窗样式 */
+.notification {
+    position: fixed;
+    top: 20px;
+    right: 20px;
+    padding: 15px 25px;
+    background: linear-gradient(135deg, #4caf50, #45a049);
+    color: white;
+    border-radius: 8px;
+    box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
+    z-index: 100;
+    opacity: 0;
+    transform: translateY(-20px);
+    transition: all 0.3s;
+}
+
+.notification.show {
+    opacity: 1;
+    transform: translateY(0);
+}
+
+.notification.error {
+    background: linear-gradient(135deg, #f44336, #e53935);
+}
+
+/* 添加引导动画 */
+@keyframes pulse {
+    0% { box-shadow: 0 0 0 0 rgba(69, 104, 220, 0.6); }
+    70% { box-shadow: 0 0 0 12px rgba(69, 104, 220, 0); }
+    100% { box-shadow: 0 0 0 0 rgba(69, 104, 220, 0); }
+}
+
+#generate-btn {
+    animation: pulse 2s infinite;
+}
+
+/* 图表类型画廊 */
+.chart-type-gallery {
+    margin-top: 25px;
+    border-top: 1px solid #e0e0e0;
+    padding-top: 20px;
+}
+
+.chart-type-gallery h3 {
+    font-size: 1.08rem;
+    margin-bottom: 15px;
+    color: #333;
+    position: relative;
+    display: inline-block;
+    padding-bottom: 8px;
+}
+
+.chart-type-gallery h3::after {
+    content: '';
+    position: absolute;
+    bottom: 0;
+    left: 0;
+    width: 100%;
+    height: 3px;
+    background: linear-gradient(90deg, #4568dc, #b06ab3);
+    border-radius: 3px;
+}
+
+.chart-type-scroller {
+    overflow-x: auto;
+    white-space: nowrap;
+    padding-bottom: 15px;
+    /* 自定义滚动条 */
+    scrollbar-width: thin;
+    scrollbar-color: #ccc #f0f0f0;
+}
+
+.chart-type-scroller::-webkit-scrollbar {
+    height: 8px;
+}
+
+.chart-type-scroller::-webkit-scrollbar-track {
+    background: #f0f0f0;
+    border-radius: 10px;
+}
+
+.chart-type-scroller::-webkit-scrollbar-thumb {
+    background: linear-gradient(90deg, #4568dc, #b06ab3);
+    border-radius: 10px;
+}
+
+.chart-type-group {
+    display: inline-block;
+    vertical-align: top;
+    margin-right: 25px;
+    min-width: 250px;
+}
+
+.chart-type-group h4 {
+    font-size: 15.3px;
+    margin: 0 0 12px 0;
+    color: #444;
+    border-bottom: 1px dashed #e0e0e0;
+    padding-bottom: 8px;
+}
+
+.chart-type-items {
+    display: flex;
+    flex-wrap: wrap;
+    gap: 12px;
+}
+
+.chart-type-item {
+    width: 85px;
+    text-align: center;
+    cursor: pointer;
+    transition: box-shadow 0.3s, background-color 0.3s;
+    border-radius: 8px;
+    padding: 10px 5px;
+    background-color: #f9f9f9;
+    box-shadow: 0 3px 8px rgba(0,0,0,0.05);
+}
+
+.chart-type-item:hover {
+    background-color: #f0f8ff;
+    box-shadow: 0 8px 15px rgba(0,0,0,0.1);
+}
+
+.chart-type-item.active {
+    background: linear-gradient(135deg, #e6f2ff, #eef8ff);
+    border: 1px solid #b3d7ff;
+    box-shadow: 0 5px 15px rgba(69, 104, 220, 0.15);
+}
+
+.chart-type-item img {
+    width: 60px;
+    height: 60px;
+    object-fit: contain;
+    margin-bottom: 8px;
+    border-radius: 4px;
+    background: white;
+    padding: 3px;
+    border: 1px solid #eee;
+}
+
+.chart-type-item span {
+    display: block;
+    font-size: 11.7px;
+    color: #444;
+    white-space: normal;
+    line-height: 1.3;
+}
+
+/* 添加canvas样式 */
+canvas#chart-canvas {
+    height: 450px !important;
+}
+
+/* 为canvas添加默认背景,但在绘制图表时不显示背景 */
+canvas#chart-canvas:not([data-chart-rendered]) {
+    background-image: url("data:image/svg+xml,%3Csvg width='100%25' height='100%25' xmlns='http://www.w3.org/2000/svg'%3E%3Cdefs%3E%3ClinearGradient id='grad' x1='0%25' y1='0%25' x2='100%25' y2='100%25'%3E%3Cstop offset='0%25' style='stop-color:%23f0f4ff;stop-opacity:1' /%3E%3Cstop offset='100%25' style='stop-color:%23eef4ff;stop-opacity:1' /%3E%3C/linearGradient%3E%3Cpattern id='grid' width='40' height='40' patternUnits='userSpaceOnUse'%3E%3Cpath d='M 0 10 L 40 10 M 10 0 L 10 40 M 0 20 L 40 20 M 20 0 L 20 40 M 0 30 L 40 30 M 30 0 L 30 40' fill='none' stroke='%23e0e0e0' stroke-width='0.5'/%3E%3C/pattern%3E%3C/defs%3E%3Crect width='100%25' height='100%25' fill='url(%23grad)'/%3E%3Crect width='100%25' height='100%25' fill='url(%23grid)'/%3E%3Cg transform='translate(50%25, 50%25)'%3E%3Cpath d='M-40,-30 L-40,30 L40,30' stroke='%234568dc' stroke-width='2' fill='none' stroke-opacity='0.3'/%3E%3Ccircle cx='-20' cy='10' r='5' fill='%234568dc' fill-opacity='0.3'/%3E%3Ccircle cx='0' cy='-10' r='5' fill='%234568dc' fill-opacity='0.3'/%3E%3Ccircle cx='20' cy='0' r='5' fill='%234568dc' fill-opacity='0.3'/%3E%3Cpath d='M-20,10 L0,-10 L20,0' stroke='%234568dc' stroke-width='2' fill='none' stroke-opacity='0.3'/%3E%3C/g%3E%3C/svg%3E");
+    background-size: cover;
+    background-position: center;
+}
+
+/* 确保有data-chart-rendered属性的canvas没有背景图 */
+canvas#chart-canvas[data-chart-rendered] {
+    background-image: none !important;
+}
+
+/* 美化文件上传控件 */
+.input-group input[type="file"] {
+    width: 0.1px;
+    height: 0.1px;
+    opacity: 0;
+    overflow: hidden;
+    position: absolute;
+    z-index: -1;
+}
+
+.input-group input[type="file"] + label {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    padding: 12px 15px;
+    border-radius: 8px;
+    background: linear-gradient(135deg, #f0f2f5, #e4e7f0);
+    color: #444;
+    cursor: pointer;
+    font-weight: 500;
+    transition: all 0.3s;
+    box-shadow: 0 3px 10px rgba(0, 0, 0, 0.05);
+    margin-bottom: 0;
+    text-align: center;
+    width: 100%;
+}
+
+.input-group input[type="file"] + label:hover {
+    background: #e9ecef;
+    border-color: #adb5bd;
+}
+
+.input-group input[type="file"] + label::before {
+    content: '\f07c'; /* Use Font Awesome icon code for folder */
+    font-family: "Font Awesome 5 Free"; /* Ensure Font Awesome is loaded */
+    font-weight: 900;
+    margin-right: 8px;
+    display: inline-block; /* Ensure icon aligns properly */
+}
+
+/* 新增的页面头部样式 */
+.page-header {
+    background-color: #f8f9fa;
+    padding: 12px 20px;
+    border-bottom: 1px solid #e3e6f0;
+    display: flex;
+    align-items: center;
+    margin-bottom: 20px;
+}
+
+.page-header .header-link {
+    display: inline-flex;
+    align-items: center;
+    text-decoration: none;
+    color: #5a5c69;
+    font-weight: 500;
+    font-size: 1.2rem;
+    transition: color 0.2s ease-in-out;
+}
+
+.page-header .header-link:hover {
+    color: #4e73df;
+}
+
+.page-header .header-link img {
+    margin-right: 8px;
+    height: 18px;
+    width: 18px;
+}
+
+.page-header .header-link .fehelper-text {
+    font-weight: 700;
+}
+
+.page-header .page-title-suffix {
+    margin-left: 8px;
+    font-size: 1.2rem;
+    color: #858796;
+} 

+ 270 - 0
apps/code-beautify/beautify-css.js

@@ -0,0 +1,270 @@
+/*jshint curly:true, eqeqeq:true, laxbreak:true, noempty:false */
+/*
+
+  The MIT License (MIT)
+
+  Copyright (c) 2007-2013 Einar Lielmanis and contributors.
+
+  Permission is hereby granted, free of charge, to any person
+  obtaining a copy of this software and associated documentation files
+  (the "Software"), to deal in the Software without restriction,
+  including without limitation the rights to use, copy, modify, merge,
+  publish, distribute, sublicense, and/or sell copies of the Software,
+  and to permit persons to whom the Software is furnished to do so,
+  subject to the following conditions:
+
+  The above copyright notice and this permission notice shall be
+  included in all copies or substantial portions of the Software.
+
+  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+  MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+  NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+  BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+  ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+  CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+  SOFTWARE.
+
+
+ CSS Beautifier
+---------------
+
+    Written by Harutyun Amirjanyan, ([email protected])
+
+    Based on code initially developed by: Einar Lielmanis, <[email protected]>
+        http://jsbeautifier.org/
+
+    Usage:
+        css_beautify(source_text);
+        css_beautify(source_text, options);
+
+    The options are:
+        indent_size (default 4)          — indentation size,
+        indent_char (default space)      — character to indent with,
+
+    e.g
+
+    css_beautify(css_source_text, {
+      'indent_size': 1,
+      'indent_char': '\t'
+    });
+*/
+
+// http://www.w3.org/TR/CSS21/syndata.html#tokenization
+// http://www.w3.org/TR/css3-syntax/
+
+(function () {
+
+    function css_beautify(source_text, options, callback) {
+        "use strict";
+
+        // 用webwork的方式来进行格式化,效率更高
+        let worker = new Worker(URL.createObjectURL(new Blob(["(" + beautifyWebWorker.toString() + ")()"], {type: 'text/javascript'})));
+        worker.onmessage = function (evt) {
+            callback && callback(evt.data);
+        };
+        worker.postMessage({
+            source_text: (source_text || '').trim(),
+            options: options
+        });
+    }
+
+    function beautifyWebWorker() {
+        function Beautifier(source_text, options) {
+            options = options || {};
+            var indentSize = options.indent_size || 4;
+            var indentCharacter = options.indent_char || ' ';
+
+            // compatibility
+            if (typeof indentSize === "string") {
+                indentSize = parseInt(indentSize, 10);
+            }
+
+
+            // tokenizer
+            var whiteRe = /^\s+$/;
+            var wordRe = /[\w$\-_]/;
+
+            var pos = -1, ch;
+
+            function next() {
+                ch = source_text.charAt(++pos);
+                return ch;
+            }
+
+            function peek() {
+                return source_text.charAt(pos + 1);
+            }
+
+            function eatString(comma) {
+                var start = pos;
+                while (next()) {
+                    if (ch === "\\") {
+                        next();
+                        next();
+                    } else if (ch === comma) {
+                        break;
+                    } else if (ch === "\n") {
+                        break;
+                    }
+                }
+                return source_text.substring(start, pos + 1);
+            }
+
+            function eatWhitespace() {
+                var start = pos;
+                while (whiteRe.test(peek())) {
+                    pos++;
+                }
+                return pos !== start;
+            }
+
+            function skipWhitespace() {
+                var start = pos;
+                do {
+                } while (whiteRe.test(next()));
+                return pos !== start + 1;
+            }
+
+            function eatComment() {
+                var start = pos;
+                next();
+                while (next()) {
+                    if (ch === "*" && peek() === "/") {
+                        pos++;
+                        break;
+                    }
+                }
+
+                return source_text.substring(start, pos + 1);
+            }
+
+
+            function lookBack(str) {
+                return source_text.substring(pos - str.length, pos).toLowerCase() === str;
+            }
+
+            // printer
+            var indentString = source_text.match(/^[\r\n]*[\t ]*/)[0];
+            var singleIndent = Array(indentSize + 1).join(indentCharacter);
+            var indentLevel = 0;
+
+            function indent() {
+                indentLevel++;
+                indentString += singleIndent;
+            }
+
+            function outdent() {
+                indentLevel--;
+                indentString = indentString.slice(0, -indentSize);
+            }
+
+            var print = {};
+            print["{"] = function (ch) {
+                print.singleSpace();
+                output.push(ch);
+                print.newLine();
+            };
+            print["}"] = function (ch) {
+                print.newLine();
+                output.push(ch);
+                print.newLine();
+            };
+
+            print.newLine = function (keepWhitespace) {
+                if (!keepWhitespace) {
+                    while (whiteRe.test(output[output.length - 1])) {
+                        output.pop();
+                    }
+                }
+
+                if (output.length) {
+                    output.push('\n');
+                }
+                if (indentString) {
+                    output.push(indentString);
+                }
+            };
+            print.singleSpace = function () {
+                if (output.length && !whiteRe.test(output[output.length - 1])) {
+                    output.push(' ');
+                }
+            };
+            var output = [];
+            if (indentString) {
+                output.push(indentString);
+            }
+            /*_____________________--------------------_____________________*/
+
+            while (true) {
+                var isAfterSpace = skipWhitespace();
+
+                if (!ch) {
+                    break;
+                }
+
+
+                if (ch === '{') {
+                    indent();
+                    print["{"](ch);
+                } else if (ch === '}') {
+                    outdent();
+                    print["}"](ch);
+                } else if (ch === '"' || ch === '\'') {
+                    output.push(eatString(ch));
+                } else if (ch === ';') {
+                    output.push(ch, '\n', indentString);
+                } else if (ch === '/' && peek() === '*') { // comment
+                    print.newLine();
+                    output.push(eatComment(), "\n", indentString);
+                } else if (ch === '(') { // may be a url
+                    if (lookBack("url")) {
+                        output.push(ch);
+                        eatWhitespace();
+                        if (next()) {
+                            if (ch !== ')' && ch !== '"' && ch !== '\'') {
+                                output.push(eatString(')'));
+                            } else {
+                                pos--;
+                            }
+                        }
+                    } else {
+                        if (isAfterSpace) {
+                            print.singleSpace();
+                        }
+                        output.push(ch);
+                        eatWhitespace();
+                    }
+                } else if (ch === ')') {
+                    output.push(ch);
+                } else if (ch === ',') {
+                    eatWhitespace();
+                    output.push(ch);
+                    print.singleSpace();
+                } else if (ch === ']') {
+                    output.push(ch);
+                } else if (ch === '[' || ch === '=') { // no whitespace before or after
+                    eatWhitespace();
+                    output.push(ch);
+                } else {
+                    if (isAfterSpace) {
+                        print.singleSpace();
+                    }
+
+                    output.push(ch);
+                }
+            }
+
+
+            var sweetCode = output.join('').replace(/[\n ]+$/, '');
+            return sweetCode;
+        }
+
+        self.onmessage = function (evt) {
+            var result = Beautifier(evt.data.source_text, evt.data.options);
+            self.postMessage(result);
+        };
+    }
+
+    window.css_beautify = css_beautify;
+}());

+ 616 - 0
apps/code-beautify/beautify-html.js

@@ -0,0 +1,616 @@
+/*jshint curly:true, eqeqeq:true, laxbreak:true, noempty:false */
+/*
+
+  The MIT License (MIT)
+
+  Copyright (c) 2007-2013 Einar Lielmanis and contributors.
+
+  Permission is hereby granted, free of charge, to any person
+  obtaining a copy of this software and associated documentation files
+  (the "Software"), to deal in the Software without restriction,
+  including without limitation the rights to use, copy, modify, merge,
+  publish, distribute, sublicense, and/or sell copies of the Software,
+  and to permit persons to whom the Software is furnished to do so,
+  subject to the following conditions:
+
+  The above copyright notice and this permission notice shall be
+  included in all copies or substantial portions of the Software.
+
+  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+  MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+  NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+  BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+  ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+  CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+  SOFTWARE.
+
+
+ Style HTML
+---------------
+
+  Written by Nochum Sossonko, ([email protected])
+
+  Based on code initially developed by: Einar Lielmanis, <[email protected]>
+    http://jsbeautifier.org/
+
+  Usage:
+    style_html(html_source);
+
+    style_html(html_source, options);
+
+  The options are:
+    indent_size (default 4)          — indentation size,
+    indent_char (default space)      — character to indent with,
+    max_char (default 250)            -  maximum amount of characters per line (0 = disable)
+    brace_style (default "collapse") - "collapse" | "expand" | "end-expand"
+            put braces on the same line as control statements (default), or put braces on own line (Allman / ANSI style), or just put end braces on own line.
+    unformatted (defaults to inline tags) - list of tags, that shouldn't be reformatted
+    indent_scripts (default normal)  - "keep"|"separate"|"normal"
+
+    e.g.
+
+    style_html(html_source, {
+      'indent_size': 2,
+      'indent_char': ' ',
+      'max_char': 78,
+      'brace_style': 'expand',
+      'unformatted': ['a', 'sub', 'sup', 'b', 'i', 'u']
+    });
+*/
+
+(function() {
+
+    function style_html(html_source, options, js_beautify, css_beautify) {
+    //Wrapper function to invoke all the necessary constructors and deal with the output.
+
+      var multi_parser,
+          indent_size,
+          indent_character,
+          max_char,
+          brace_style,
+          unformatted;
+
+      options = options || {};
+      indent_size = options.indent_size || 4;
+      indent_character = options.indent_char || ' ';
+      brace_style = options.brace_style || 'collapse';
+      max_char = options.max_char === 0 ? Infinity : options.max_char || 250;
+      unformatted = options.unformatted || ['a', 'span', 'bdo', 'em', 'strong', 'dfn', 'code', 'samp', 'kbd', 'var', 'cite', 'abbr', 'acronym', 'q', 'sub', 'sup', 'tt', 'i', 'b', 'big', 'small', 'u', 's', 'strike', 'font', 'ins', 'del', 'pre', 'address', 'dt', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6'];
+
+      function Parser() {
+
+        this.pos = 0; //Parser position
+        this.token = '';
+        this.current_mode = 'CONTENT'; //reflects the current Parser mode: TAG/CONTENT
+        this.tags = { //An object to hold tags, their position, and their parent-tags, initiated with default values
+          parent: 'parent1',
+          parentcount: 1,
+          parent1: ''
+        };
+        this.tag_type = '';
+        this.token_text = this.last_token = this.last_text = this.token_type = '';
+
+        this.Utils = { //Uilities made available to the various functions
+          whitespace: "\n\r\t ".split(''),
+          single_token: 'br,input,link,meta,!doctype,basefont,base,area,hr,wbr,param,img,isindex,?xml,embed,?php,?,?='.split(','), //all the single tags for HTML
+          extra_liners: 'head,body,/html'.split(','), //for tags that need a line of whitespace before them
+          in_array: function (what, arr) {
+            for (var i=0; i<arr.length; i++) {
+              if (what === arr[i]) {
+                return true;
+              }
+            }
+            return false;
+          }
+        };
+
+        this.get_content = function () { //function to capture regular content between tags
+
+          var input_char = '',
+              content = [],
+              space = false; //if a space is needed
+
+          while (this.input.charAt(this.pos) !== '<') {
+            if (this.pos >= this.input.length) {
+              return content.length?content.join(''):['', 'TK_EOF'];
+            }
+
+            input_char = this.input.charAt(this.pos);
+            this.pos++;
+            this.line_char_count++;
+
+            if (this.Utils.in_array(input_char, this.Utils.whitespace)) {
+              if (content.length) {
+                space = true;
+              }
+              this.line_char_count--;
+              continue; //don't want to insert unnecessary space
+            }
+            else if (space) {
+              if (this.line_char_count >= this.max_char) { //insert a line when the max_char is reached
+                content.push('\n');
+                for (var i=0; i<this.indent_level; i++) {
+                  content.push(this.indent_string);
+                }
+                this.line_char_count = 0;
+              }
+              else{
+                content.push(' ');
+                this.line_char_count++;
+              }
+              space = false;
+            }
+            content.push(input_char); //letter at-a-time (or string) inserted to an array
+          }
+          return content.length?content.join(''):'';
+        };
+
+        this.get_contents_to = function (name) { //get the full content of a script or style to pass to js_beautify
+          if (this.pos === this.input.length) {
+            return ['', 'TK_EOF'];
+          }
+          var input_char = '';
+          var content = '';
+          var reg_match = new RegExp('</' + name + '\\s*>', 'igm');
+          reg_match.lastIndex = this.pos;
+          var reg_array = reg_match.exec(this.input);
+          var end_script = reg_array?reg_array.index:this.input.length; //absolute end of script
+          if(this.pos < end_script) { //get everything in between the script tags
+            content = this.input.substring(this.pos, end_script);
+            this.pos = end_script;
+          }
+          return content;
+        };
+
+        this.record_tag = function (tag){ //function to record a tag and its parent in this.tags Object
+          if (this.tags[tag + 'count']) { //check for the existence of this tag type
+            this.tags[tag + 'count']++;
+            this.tags[tag + this.tags[tag + 'count']] = this.indent_level; //and record the present indent level
+          }
+          else { //otherwise initialize this tag type
+            this.tags[tag + 'count'] = 1;
+            this.tags[tag + this.tags[tag + 'count']] = this.indent_level; //and record the present indent level
+          }
+          this.tags[tag + this.tags[tag + 'count'] + 'parent'] = this.tags.parent; //set the parent (i.e. in the case of a div this.tags.div1parent)
+          this.tags.parent = tag + this.tags[tag + 'count']; //and make this the current parent (i.e. in the case of a div 'div1')
+        };
+
+        this.retrieve_tag = function (tag) { //function to retrieve the opening tag to the corresponding closer
+          if (this.tags[tag + 'count']) { //if the openener is not in the Object we ignore it
+            var temp_parent = this.tags.parent; //check to see if it's a closable tag.
+            while (temp_parent) { //till we reach '' (the initial value);
+              if (tag + this.tags[tag + 'count'] === temp_parent) { //if this is it use it
+                break;
+              }
+              temp_parent = this.tags[temp_parent + 'parent']; //otherwise keep on climbing up the DOM Tree
+            }
+            if (temp_parent) { //if we caught something
+              this.indent_level = this.tags[tag + this.tags[tag + 'count']]; //set the indent_level accordingly
+              this.tags.parent = this.tags[temp_parent + 'parent']; //and set the current parent
+            }
+            delete this.tags[tag + this.tags[tag + 'count'] + 'parent']; //delete the closed tags parent reference...
+            delete this.tags[tag + this.tags[tag + 'count']]; //...and the tag itself
+            if (this.tags[tag + 'count'] === 1) {
+              delete this.tags[tag + 'count'];
+            }
+            else {
+              this.tags[tag + 'count']--;
+            }
+          }
+        };
+
+        this.get_tag = function (peek) { //function to get a full tag and parse its type
+          var input_char = '',
+              content = [],
+              comment = '',
+              space = false,
+              tag_start, tag_end,
+              orig_pos = this.pos,
+              orig_line_char_count = this.line_char_count;
+
+          peek = peek !== undefined ? peek : false;
+
+          do {
+            if (this.pos >= this.input.length) {
+              if (peek) {
+                this.pos = orig_pos;
+                this.line_char_count = orig_line_char_count;
+              }
+              return content.length?content.join(''):['', 'TK_EOF'];
+            }
+
+            input_char = this.input.charAt(this.pos);
+            this.pos++;
+            this.line_char_count++;
+
+            if (this.Utils.in_array(input_char, this.Utils.whitespace)) { //don't want to insert unnecessary space
+              space = true;
+              this.line_char_count--;
+              continue;
+            }
+
+            if (input_char === "'" || input_char === '"') {
+              if (!content[1] || content[1] !== '!') { //if we're in a comment strings don't get treated specially
+                input_char += this.get_unformatted(input_char);
+                space = true;
+              }
+            }
+
+            if (input_char === '=') { //no space before =
+              space = false;
+            }
+
+            if (content.length && content[content.length-1] !== '=' && input_char !== '>' && space) {
+                //no space after = or before >
+              if (this.line_char_count >= this.max_char) {
+                this.print_newline(false, content);
+                this.line_char_count = 0;
+              }
+              else {
+                content.push(' ');
+                this.line_char_count++;
+              }
+              space = false;
+            }
+            if (input_char === '<') {
+              tag_start = this.pos - 1;
+            }
+            content.push(input_char); //inserts character at-a-time (or string)
+          } while (input_char !== '>');
+
+          var tag_complete = content.join('');
+          var tag_index;
+          if (tag_complete.indexOf(' ') !== -1) { //if there's whitespace, thats where the tag name ends
+            tag_index = tag_complete.indexOf(' ');
+          }
+          else { //otherwise go with the tag ending
+            tag_index = tag_complete.indexOf('>');
+          }
+          var tag_check = tag_complete.substring(1, tag_index).toLowerCase();
+          if (tag_complete.charAt(tag_complete.length-2) === '/' ||
+            this.Utils.in_array(tag_check, this.Utils.single_token)) { //if this tag name is a single tag type (either in the list or has a closing /)
+            if ( ! peek) {
+              this.tag_type = 'SINGLE';
+            }
+          }
+          else if (tag_check === 'script') { //for later script handling
+            if ( ! peek) {
+              this.record_tag(tag_check);
+              this.tag_type = 'SCRIPT';
+            }
+          }
+          else if (tag_check === 'style') { //for future style handling (for now it justs uses get_content)
+            if ( ! peek) {
+              this.record_tag(tag_check);
+              this.tag_type = 'STYLE';
+            }
+          }
+          else if (this.is_unformatted(tag_check, unformatted)) { // do not reformat the "unformatted" tags
+            comment = this.get_unformatted('</'+tag_check+'>', tag_complete); //...delegate to get_unformatted function
+            content.push(comment);
+            // Preserve collapsed whitespace either before or after this tag.
+            if (tag_start > 0 && this.Utils.in_array(this.input.charAt(tag_start - 1), this.Utils.whitespace)){
+                content.splice(0, 0, this.input.charAt(tag_start - 1));
+            }
+            tag_end = this.pos - 1;
+            if (this.Utils.in_array(this.input.charAt(tag_end + 1), this.Utils.whitespace)){
+                content.push(this.input.charAt(tag_end + 1));
+            }
+            this.tag_type = 'SINGLE';
+          }
+          else if (tag_check.charAt(0) === '!') { //peek for <!-- comment
+            if (tag_check.indexOf('[if') !== -1) { //peek for <!--[if conditional comment
+              if (tag_complete.indexOf('!IE') !== -1) { //this type needs a closing --> so...
+                comment = this.get_unformatted('-->', tag_complete); //...delegate to get_unformatted
+                content.push(comment);
+              }
+              if ( ! peek) {
+                this.tag_type = 'START';
+              }
+            }
+            else if (tag_check.indexOf('[endif') !== -1) {//peek for <!--[endif end conditional comment
+              this.tag_type = 'END';
+              this.unindent();
+            }
+            else if (tag_check.indexOf('[cdata[') !== -1) { //if it's a <[cdata[ comment...
+              comment = this.get_unformatted(']]>', tag_complete); //...delegate to get_unformatted function
+              content.push(comment);
+              if ( ! peek) {
+                this.tag_type = 'SINGLE'; //<![CDATA[ comments are treated like single tags
+              }
+            }
+            else {
+              comment = this.get_unformatted('-->', tag_complete);
+              content.push(comment);
+              this.tag_type = 'SINGLE';
+            }
+          }
+          else if ( ! peek) {
+            if (tag_check.charAt(0) === '/') { //this tag is a double tag so check for tag-ending
+              this.retrieve_tag(tag_check.substring(1)); //remove it and all ancestors
+              this.tag_type = 'END';
+            }
+            else { //otherwise it's a start-tag
+              this.record_tag(tag_check); //push it on the tag stack
+              this.tag_type = 'START';
+            }
+            if (this.Utils.in_array(tag_check, this.Utils.extra_liners)) { //check if this double needs an extra line
+              this.print_newline(true, this.output);
+            }
+          }
+
+          if (peek) {
+            this.pos = orig_pos;
+            this.line_char_count = orig_line_char_count;
+          }
+
+          return content.join(''); //returns fully formatted tag
+        };
+
+        this.get_unformatted = function (delimiter, orig_tag) { //function to return unformatted content in its entirety
+
+          if (orig_tag && orig_tag.toLowerCase().indexOf(delimiter) !== -1) {
+            return '';
+          }
+          var input_char = '';
+          var content = '';
+          var space = true;
+          do {
+
+            if (this.pos >= this.input.length) {
+              return content;
+            }
+
+            input_char = this.input.charAt(this.pos);
+            this.pos++;
+
+            if (this.Utils.in_array(input_char, this.Utils.whitespace)) {
+              if (!space) {
+                this.line_char_count--;
+                continue;
+              }
+              if (input_char === '\n' || input_char === '\r') {
+                content += '\n';
+                /*  Don't change tab indention for unformatted blocks.  If using code for html editing, this will greatly affect <pre> tags if they are specified in the 'unformatted array'
+                for (var i=0; i<this.indent_level; i++) {
+                  content += this.indent_string;
+                }
+                space = false; //...and make sure other indentation is erased
+                */
+                this.line_char_count = 0;
+                continue;
+              }
+            }
+            content += input_char;
+            this.line_char_count++;
+            space = true;
+
+
+          } while (content.toLowerCase().indexOf(delimiter) === -1);
+          return content;
+        };
+
+        this.get_token = function () { //initial handler for token-retrieval
+          var token;
+
+          if (this.last_token === 'TK_TAG_SCRIPT' || this.last_token === 'TK_TAG_STYLE') { //check if we need to format javascript
+           var type = this.last_token.substr(7);
+           token = this.get_contents_to(type);
+            if (typeof token !== 'string') {
+              return token;
+            }
+            return [token, 'TK_' + type];
+          }
+          if (this.current_mode === 'CONTENT') {
+            token = this.get_content();
+            if (typeof token !== 'string') {
+              return token;
+            }
+            else {
+              return [token, 'TK_CONTENT'];
+            }
+          }
+
+          if (this.current_mode === 'TAG') {
+            token = this.get_tag();
+            if (typeof token !== 'string') {
+              return token;
+            }
+            else {
+              var tag_name_type = 'TK_TAG_' + this.tag_type;
+              return [token, tag_name_type];
+            }
+          }
+        };
+
+        this.get_full_indent = function (level) {
+          level = this.indent_level + level || 0;
+          if (level < 1) {
+            return '';
+          }
+
+          return Array(level + 1).join(this.indent_string);
+        };
+
+        this.is_unformatted = function(tag_check, unformatted) {
+            //is this an HTML5 block-level link?
+            if (!this.Utils.in_array(tag_check, unformatted)){
+                return false;
+            }
+
+            if (tag_check.toLowerCase() !== 'a' || !this.Utils.in_array('a', unformatted)){
+                return true;
+            }
+
+            //at this point we have an  tag; is its first child something we want to remain
+            //unformatted?
+            var next_tag = this.get_tag(true /* peek. */);
+
+            // tets next_tag to see if it is just html tag (no external content)
+            var tag = (next_tag || "").match(/^\s*<\s*\/?([a-z]*)\s*[^>]*>\s*$/);
+
+            // if next_tag comes back but is not an isolated tag, then
+            // let's treat the 'a' tag as having content
+            // and respect the unformatted option
+            if (!tag || this.Utils.in_array(tag, unformatted)){
+                return true;
+            } else {
+                return false;
+            }
+        };
+
+        this.printer = function (js_source, indent_character, indent_size, max_char, brace_style) { //handles input/output and some other printing functions
+
+          this.input = js_source || ''; //gets the input for the Parser
+          this.output = [];
+          this.indent_character = indent_character;
+          this.indent_string = '';
+          this.indent_size = indent_size;
+          this.brace_style = brace_style;
+          this.indent_level = 0;
+          this.max_char = max_char;
+          this.line_char_count = 0; //count to see if max_char was exceeded
+
+          for (var i=0; i<this.indent_size; i++) {
+            this.indent_string += this.indent_character;
+          }
+
+          this.print_newline = function (ignore, arr) {
+            this.line_char_count = 0;
+            if (!arr || !arr.length) {
+              return;
+            }
+            if (!ignore) { //we might want the extra line
+              while (this.Utils.in_array(arr[arr.length-1], this.Utils.whitespace)) {
+                arr.pop();
+              }
+            }
+            arr.push('\n');
+            for (var i=0; i<this.indent_level; i++) {
+              arr.push(this.indent_string);
+            }
+          };
+
+          this.print_token = function (text) {
+            this.output.push(text);
+          };
+
+          this.indent = function () {
+            this.indent_level++;
+          };
+
+          this.unindent = function () {
+            if (this.indent_level > 0) {
+              this.indent_level--;
+            }
+          };
+        };
+        return this;
+      }
+
+      /*_____________________--------------------_____________________*/
+
+      multi_parser = new Parser(); //wrapping functions Parser
+      multi_parser.printer(html_source, indent_character, indent_size, max_char, brace_style); //initialize starting values
+
+      while (true) {
+          var t = multi_parser.get_token();
+          multi_parser.token_text = t[0];
+          multi_parser.token_type = t[1];
+
+        if (multi_parser.token_type === 'TK_EOF') {
+          break;
+        }
+
+        switch (multi_parser.token_type) {
+          case 'TK_TAG_START':
+            multi_parser.print_newline(false, multi_parser.output);
+            multi_parser.print_token(multi_parser.token_text);
+            multi_parser.indent();
+            multi_parser.current_mode = 'CONTENT';
+            break;
+          case 'TK_TAG_STYLE':
+          case 'TK_TAG_SCRIPT':
+            multi_parser.print_newline(false, multi_parser.output);
+            multi_parser.print_token(multi_parser.token_text);
+            multi_parser.current_mode = 'CONTENT';
+            break;
+          case 'TK_TAG_END':
+            //Print new line only if the tag has no content and has child
+            if (multi_parser.last_token === 'TK_CONTENT' && multi_parser.last_text === '') {
+                var tag_name = multi_parser.token_text.match(/\w+/)[0];
+                var tag_extracted_from_last_output = multi_parser.output[multi_parser.output.length -1].match(/<\s*(\w+)/);
+                if (tag_extracted_from_last_output === null || tag_extracted_from_last_output[1] !== tag_name) {
+                    multi_parser.print_newline(true, multi_parser.output);
+                }
+            }
+            multi_parser.print_token(multi_parser.token_text);
+            multi_parser.current_mode = 'CONTENT';
+            break;
+          case 'TK_TAG_SINGLE':
+            // Don't add a newline before elements that should remain unformatted.
+            var tag_check = multi_parser.token_text.match(/^\s*<([a-z]+)/i);
+            if (!tag_check || !multi_parser.Utils.in_array(tag_check[1], unformatted)){
+                multi_parser.print_newline(false, multi_parser.output);
+            }
+            multi_parser.print_token(multi_parser.token_text);
+            multi_parser.current_mode = 'CONTENT';
+            break;
+          case 'TK_CONTENT':
+            if (multi_parser.token_text !== '') {
+              multi_parser.print_token(multi_parser.token_text);
+            }
+            multi_parser.current_mode = 'TAG';
+            break;
+          case 'TK_STYLE':
+          case 'TK_SCRIPT':
+            if (multi_parser.token_text !== '') {
+              multi_parser.output.push('\n');
+              var text = multi_parser.token_text,
+                  _beautifier,
+                  script_indent_level = 1;
+              if (multi_parser.token_type === 'TK_SCRIPT') {
+                _beautifier = typeof js_beautify === 'function' && js_beautify;
+              } else if (multi_parser.token_type === 'TK_STYLE') {
+                _beautifier = typeof css_beautify === 'function' && css_beautify;
+              }
+
+              if (options.indent_scripts === "keep") {
+                script_indent_level = 0;
+              } else if (options.indent_scripts === "separate") {
+                script_indent_level = -multi_parser.indent_level;
+              }
+
+              var indentation = multi_parser.get_full_indent(script_indent_level);
+              if (_beautifier) {
+                // call the Beautifier if avaliable
+                text = _beautifier(text.replace(/^\s*/, indentation), options);
+              } else {
+                // simply indent the string otherwise
+                var white = text.match(/^\s*/)[0];
+                var _level = white.match(/[^\n\r]*$/)[0].split(multi_parser.indent_string).length - 1;
+                var reindent = multi_parser.get_full_indent(script_indent_level -_level);
+                text = text.replace(/^\s*/, indentation)
+                       .replace(/\r\n|\r|\n/g, '\n' + reindent)
+                       .replace(/\s*$/, '');
+              }
+              if (text) {
+                multi_parser.print_token(text);
+                multi_parser.print_newline(true, multi_parser.output);
+              }
+            }
+            multi_parser.current_mode = 'TAG';
+            break;
+        }
+        multi_parser.last_token = multi_parser.token_type;
+        multi_parser.last_text = multi_parser.token_text;
+      }
+      return multi_parser.output.join('');
+    }
+
+    // If we're running a web page and don't have either of the above, add our one global
+    window.html_beautify = function(html_source, options) {
+        return style_html(html_source, options, window.js_beautify, window.css_beautify);
+    };
+}());

+ 358 - 0
apps/code-beautify/beautify-vk.js

@@ -0,0 +1,358 @@
+/**
+* vkBeautify - javascript plugin to pretty-print or minify text in XML, JSON, CSS and SQL formats.
+*  
+* Version - 0.99.00.beta 
+* Copyright (c) 2012 Vadim Kiryukhin
+* vkiryukhin @ gmail.com
+* http://www.eslinstructor.net/vkbeautify/
+* 
+* MIT license:
+*   http://www.opensource.org/licenses/mit-license.php
+*
+*   Pretty print
+*
+*        vkbeautify.xml(text [,indent_pattern]);
+*        vkbeautify.json(text [,indent_pattern]);
+*        vkbeautify.css(text [,indent_pattern]);
+*        vkbeautify.sql(text [,indent_pattern]);
+*
+*        @text - String; text to beatufy;
+*        @indent_pattern - Integer | String;
+*                Integer:  number of white spaces;
+*                String:   character string to visualize indentation ( can also be a set of white spaces )
+*   Minify
+*
+*        vkbeautify.xmlmin(text [,preserve_comments]);
+*        vkbeautify.jsonmin(text);
+*        vkbeautify.cssmin(text [,preserve_comments]);
+*        vkbeautify.sqlmin(text);
+*
+*        @text - String; text to minify;
+*        @preserve_comments - Bool; [optional];
+*                Set this flag to true to prevent removing comments from @text ( minxml and mincss functions only. )
+*
+*   Examples:
+*        vkbeautify.xml(text); // pretty print XML
+*        vkbeautify.json(text, 4 ); // pretty print JSON
+*        vkbeautify.css(text, '. . . .'); // pretty print CSS
+*        vkbeautify.sql(text, '----'); // pretty print SQL
+*
+*        vkbeautify.xmlmin(text, true);// minify XML, preserve comments
+*        vkbeautify.jsonmin(text);// minify JSON
+*        vkbeautify.cssmin(text);// minify CSS, remove comments ( default )
+*        vkbeautify.sqlmin(text);// minify SQL
+*
+*/
+
+(function() {
+
+function createShiftArr(step) {
+
+	var space = '    ';
+	
+	if ( isNaN(parseInt(step)) ) {  // argument is string
+		space = step;
+	} else { // argument is integer
+		switch(step) {
+			case 1: space = ' '; break;
+			case 2: space = '  '; break;
+			case 3: space = '   '; break;
+			case 4: space = '    '; break;
+			case 5: space = '     '; break;
+			case 6: space = '      '; break;
+			case 7: space = '       '; break;
+			case 8: space = '        '; break;
+			case 9: space = '         '; break;
+			case 10: space = '          '; break;
+			case 11: space = '           '; break;
+			case 12: space = '            '; break;
+		}
+	}
+
+	var shift = ['\n']; // array of shifts
+	for(ix=0;ix<100;ix++){
+		shift.push(shift[ix]+space); 
+	}
+	return shift;
+}
+
+function vkbeautify(){
+	this.step = '\t'; // 4 spaces
+	this.shift = createShiftArr(this.step);
+};
+
+vkbeautify.prototype.xml = function(text,step) {
+
+	var ar = text.replace(/>\s{0,}</g,"><")
+				 .replace(/</g,"~::~<")
+				 .replace(/\s*xmlns\:/g,"~::~xmlns:")
+				 .replace(/\s*xmlns\=/g,"~::~xmlns=")
+				 .split('~::~'),
+		len = ar.length,
+		inComment = false,
+		deep = 0,
+		str = '',
+		ix = 0,
+		shift = step ? createShiftArr(step) : this.shift;
+
+		for(ix=0;ix<len;ix++) {
+			// start comment or <![CDATA[...]]> or <!DOCTYPE //
+			if(ar[ix].search(/<!/) > -1) { 
+				str += shift[deep]+ar[ix];
+				inComment = true; 
+				// end comment  or <![CDATA[...]]> //
+				if(ar[ix].search(/-->/) > -1 || ar[ix].search(/\]>/) > -1 || ar[ix].search(/!DOCTYPE/) > -1 ) { 
+					inComment = false; 
+				}
+			} else 
+			// end comment  or <![CDATA[...]]> //
+			if(ar[ix].search(/-->/) > -1 || ar[ix].search(/\]>/) > -1) { 
+				str += ar[ix];
+				inComment = false; 
+			} else 
+			// <elm></elm> //
+			if( /^<\w/.exec(ar[ix-1]) && /^<\/\w/.exec(ar[ix]) &&
+				/^<[\w:\-\.\,]+/.exec(ar[ix-1]) == /^<\/[\w:\-\.\,]+/.exec(ar[ix])[0].replace('/','')) { 
+				str += ar[ix];
+				if(!inComment) deep--;
+			} else
+			 // <elm> //
+			if(ar[ix].search(/<\w/) > -1 && ar[ix].search(/<\//) == -1 && ar[ix].search(/\/>/) == -1 ) {
+				str = !inComment ? str += shift[deep++]+ar[ix] : str += ar[ix];
+			} else 
+			 // <elm>...</elm> //
+			if(ar[ix].search(/<\w/) > -1 && ar[ix].search(/<\//) > -1) {
+				str = !inComment ? str += shift[deep]+ar[ix] : str += ar[ix];
+			} else 
+			// </elm> //
+			if(ar[ix].search(/<\//) > -1) { 
+				str = !inComment ? str += shift[--deep]+ar[ix] : str += ar[ix];
+			} else 
+			// <elm/> //
+			if(ar[ix].search(/\/>/) > -1 ) { 
+				str = !inComment ? str += shift[deep]+ar[ix] : str += ar[ix];
+			} else 
+			// <? xml ... ?> //
+			if(ar[ix].search(/<\?/) > -1) { 
+				str += shift[deep]+ar[ix];
+			} else 
+			// xmlns //
+			if( ar[ix].search(/xmlns\:/) > -1  || ar[ix].search(/xmlns\=/) > -1) { 
+				str += shift[deep]+ar[ix];
+			} 
+			
+			else {
+				str += ar[ix];
+			}
+		}
+		
+	return  (str[0] == '\n') ? str.slice(1) : str;
+}
+
+vkbeautify.prototype.json = function(text,step) {
+
+	var step = step ? step : this.step;
+	
+	if (typeof JSON === 'undefined' ) return text; 
+	
+	if ( typeof text === "string" ) return JSON.stringify(JSON.parse(text), null, step);
+	if ( typeof text === "object" ) return JSON.stringify(text, null, step);
+		
+	return text; // text is not string nor object
+}
+
+vkbeautify.prototype.css = function(text, step) {
+
+	var ar = text.replace(/\s{1,}/g,' ')
+				.replace(/\{/g,"{~::~")
+				.replace(/\}/g,"~::~}~::~")
+				.replace(/\;/g,";~::~")
+				.replace(/\/\*/g,"~::~/*")
+				.replace(/\*\//g,"*/~::~")
+				.replace(/~::~\s{0,}~::~/g,"~::~")
+				.split('~::~'),
+		len = ar.length,
+		deep = 0,
+		str = '',
+		ix = 0,
+		shift = step ? createShiftArr(step) : this.shift;
+		
+		for(ix=0;ix<len;ix++) {
+
+			if( /\{/.exec(ar[ix]))  { 
+				str += shift[deep++]+ar[ix];
+			} else 
+			if( /\}/.exec(ar[ix]))  { 
+				str += shift[--deep]+ar[ix];
+			} else
+			if( /\*\\/.exec(ar[ix]))  { 
+				str += shift[deep]+ar[ix];
+			}
+			else {
+				str += shift[deep]+ar[ix];
+			}
+		}
+		return str.replace(/^\n{1,}/,'');
+}
+
+//----------------------------------------------------------------------------
+
+function isSubquery(str, parenthesisLevel) {
+	return  parenthesisLevel - (str.replace(/\(/g,'').length - str.replace(/\)/g,'').length )
+}
+
+function split_sql(str, tab) {
+
+	return str.replace(/\s{1,}/g," ")
+
+				.replace(/ AND /ig,"~::~"+tab+tab+"AND ")
+				.replace(/ BETWEEN /ig,"~::~"+tab+"BETWEEN ")
+				.replace(/ CASE /ig,"~::~"+tab+"CASE ")
+				.replace(/ ELSE /ig,"~::~"+tab+"ELSE ")
+				.replace(/ END /ig,"~::~"+tab+"END ")
+				.replace(/ FROM /ig,"~::~FROM ")
+				.replace(/ GROUP\s{1,}BY/ig,"~::~GROUP BY ")
+				.replace(/ HAVING /ig,"~::~HAVING ")
+				//.replace(/ SET /ig," SET~::~")
+				.replace(/ IN /ig," IN ")
+				
+				.replace(/ JOIN /ig,"~::~JOIN ")
+				.replace(/ CROSS~::~{1,}JOIN /ig,"~::~CROSS JOIN ")
+				.replace(/ INNER~::~{1,}JOIN /ig,"~::~INNER JOIN ")
+				.replace(/ LEFT~::~{1,}JOIN /ig,"~::~LEFT JOIN ")
+				.replace(/ RIGHT~::~{1,}JOIN /ig,"~::~RIGHT JOIN ")
+				
+				.replace(/ ON /ig,"~::~"+tab+"ON ")
+				.replace(/ OR /ig,"~::~"+tab+tab+"OR ")
+				.replace(/ ORDER\s{1,}BY/ig,"~::~ORDER BY ")
+				.replace(/ OVER /ig,"~::~"+tab+"OVER ")
+
+				.replace(/\(\s{0,}SELECT /ig,"~::~(SELECT ")
+				.replace(/\)\s{0,}SELECT /ig,")~::~SELECT ")
+				
+				.replace(/ THEN /ig," THEN~::~"+tab+"")
+				.replace(/ UNION\s*/ig,"~::~UNION~::~")
+        		.replace(/~::~UNION~::~ALL /ig,"~::~UNION ALL~::~")
+				.replace(/ USING /ig,"~::~USING ")
+				.replace(/ WHEN /ig,"~::~"+tab+"WHEN ")
+				.replace(/ WHERE /ig,"~::~WHERE ")
+				.replace(/ WITH /ig,"~::~WITH ")
+				
+				//.replace(/\,\s{0,}\(/ig,",~::~( ")
+				//.replace(/\,/ig,",~::~"+tab+tab+"")
+
+				.replace(/ ALL /ig," ALL ")
+				.replace(/ AS /ig," AS ")
+				.replace(/ ASC /ig," ASC ")	
+				.replace(/ DESC /ig," DESC ")	
+				.replace(/ DISTINCT /ig," DISTINCT ")
+				.replace(/ EXISTS /ig," EXISTS ")
+				.replace(/ NOT /ig," NOT ")
+				.replace(/ NULL /ig," NULL ")
+				.replace(/ LIKE /ig," LIKE ")
+				.replace(/\s{0,}SELECT /ig,"SELECT ")
+				.replace(/\s{0,}UPDATE /ig,"UPDATE ")
+				.replace(/ SET /ig," SET ")
+							
+				.replace(/~::~{1,}/g,"~::~")
+				.split('~::~');
+}
+
+vkbeautify.prototype.sql = function(text,step) {
+
+	var ar_by_quote = text.replace(/\s{1,}/g," ")
+							.replace(/\'/ig,"~::~\'")
+							.split('~::~'),
+		len = ar_by_quote.length,
+		ar = [],
+		deep = 0,
+		tab = this.step,//+this.step,
+		inComment = true,
+		inQuote = false,
+		parenthesisLevel = 0,
+		str = '',
+		ix = 0,
+		shift = step ? createShiftArr(step) : this.shift;;
+
+		for(ix=0;ix<len;ix++) {
+			if(ix%2) {
+				ar = ar.concat(ar_by_quote[ix]);
+			} else {
+				ar = ar.concat(split_sql(ar_by_quote[ix], tab) );
+			}
+		}
+		
+		len = ar.length;
+		for(ix=0;ix<len;ix++) {
+			
+			parenthesisLevel = isSubquery(ar[ix], parenthesisLevel);
+			
+			if( /\s{0,}\s{0,}SELECT\s{0,}/.exec(ar[ix]))  { 
+				ar[ix] = ar[ix].replace(/\,/g,",\n"+tab+tab+"")
+			} 
+			
+			if( /\s{0,}\s{0,}SET\s{0,}/.exec(ar[ix]))  { 
+				ar[ix] = ar[ix].replace(/\,/g,",\n"+tab+tab+"")
+			} 
+			
+			if( /\s{0,}\(\s{0,}SELECT\s{0,}/.exec(ar[ix]))  { 
+				deep++;
+				str += shift[deep]+ar[ix];
+			} else 
+			if( /\'/.exec(ar[ix]) )  { 
+				if(parenthesisLevel<1 && deep) {
+					deep--;
+				}
+				str += ar[ix];
+			}
+			else  { 
+				str += shift[deep]+ar[ix];
+				if(parenthesisLevel<1 && deep) {
+					deep--;
+				}
+			} 
+			var junk = 0;
+		}
+
+		str = str.replace(/^\n{1,}/,'').replace(/\n{1,}/g,"\n");
+		return str;
+}
+
+
+vkbeautify.prototype.xmlmin = function(text, preserveComments) {
+
+	var str = preserveComments ? text
+							   : text.replace(/\<![ \r\n\t]*(--([^\-]|[\r\n]|-[^\-])*--[ \r\n\t]*)\>/g,"")
+									 .replace(/[ \r\n\t]{1,}xmlns/g, ' xmlns');
+	return  str.replace(/>\s{0,}</g,"><"); 
+}
+
+vkbeautify.prototype.jsonmin = function(text) {
+
+	if (typeof JSON === 'undefined' ) return text; 
+	
+	return JSON.stringify(JSON.parse(text), null, 0); 
+				
+}
+
+vkbeautify.prototype.cssmin = function(text, preserveComments) {
+	
+	var str = preserveComments ? text
+							   : text.replace(/\/\*([^*]|[\r\n]|(\*+([^*/]|[\r\n])))*\*+\//g,"") ;
+
+	return str.replace(/\s{1,}/g,' ')
+			  .replace(/\{\s{1,}/g,"{")
+			  .replace(/\}\s{1,}/g,"}")
+			  .replace(/\;\s{1,}/g,";")
+			  .replace(/\/\*\s{1,}/g,"/*")
+			  .replace(/\*\/\s{1,}/g,"*/");
+}
+
+vkbeautify.prototype.sqlmin = function(text) {
+	return text.replace(/\s{1,}/g," ").replace(/\s{1,}\(/,"(").replace(/\s{1,}\)/,")");
+}
+
+window.vkbeautify = new vkbeautify();
+
+})();
+

La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 2108 - 0
apps/code-beautify/beautify.js


+ 161 - 0
apps/code-beautify/content-script.css

@@ -0,0 +1,161 @@
+@import url("../static/vendor/highlight/github.css");
+
+html.fh-cb body {
+    font-size: 14px;
+    background: #fff;
+}
+#fehelper_tips {
+    position: fixed;
+    top: 0;
+    left: 0;
+    z-index: 100;
+    box-sizing: border-box;
+    padding: 2px 10px 3px 40px;
+    width: 100%;
+    height: 37px;
+    border-bottom: 1px solid #AAAAAB;
+    background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAA7UlEQVQ4EWN4KSwb8EJO7fxzbon/6Lg+Yc5/3wlvseLgKW/v+/a/CWB4xi/9AF0jjL9dzxurZpihfhPfnmeAKcZGP+SX/h/S+QivIXgNABlalboUqwF+EyFegxvwnwwA8gr1DUAPizOyBnC3wQIPmSbogtvCinADkBkwQwga8JRHElkfnI3TAHQvgPgwANOETGO4AN2Ax7xSMP0YNMggggZcF1PB0AgTQDEA3WYY/4SCKdaEBPMG3AUwDej0Tl1P/Aa8lFHBmZlAhi1wzMZpQODktxcYXkopBzzjl76AbjOMjytLh09/+yBmzvsAAD5azgb97zJPAAAAAElFTkSuQmCC), linear-gradient(to bottom, #FEEFAE, #FAE692);
+    background-position: 10px 50%, 0 0;
+    background-repeat: no-repeat, repeat-x;
+    font: 15px/32px 'Helvetica', 'Segoe UI', Arial, 'Microsoft Yahei', Simsun, sans-serif;
+    -webkit-user-select: none;
+    user-select: none;
+}
+
+#fehelper_tips .desc {
+    margin-right: 1em;
+    vertical-align: middle;
+}
+
+#fehelper_tips .desc i {
+    color:red;
+    padding: 0 5px;
+    text-decoration: underline;
+}
+
+#fehelper_tips .doing {
+    color: red;
+    font-size: 14px;
+    margin-left: 20px;
+}
+
+#fehelper_tips button {
+    margin: 0 0 0 10px;
+    height: 28px;
+    outline: none;
+    border: 1px solid #968A59;
+    border-radius: 3px;
+    background-image: linear-gradient(to bottom, #FFFBEA, #FBEDB1);
+    vertical-align: middle;
+    color:#000;
+}
+
+#fehelper_tips button:hover {
+    border-color: #4B452C;
+}
+
+#fehelper_tips button:active {
+    border-color: #4C4733;
+    background-image: linear-gradient(to bottom, #FDF2C0, #FEF9E3);
+}
+
+#fehelper_tips .close {
+    float: right;
+    margin: 8px 0 0;
+    padding: 0;
+    width: 16px;
+    height: 16px;
+    border: none;
+    cursor: pointer;
+    background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAABs0lEQVQ4EZ2TzWtTQRTFf3d8WrMwGyWLdmGEYkFFFylIaruQ/qMVXOnKnUs11ohGrUptoGBoitRSRZMIUfPeXJk7k/TDjeYt3mHmnjtz5pwZyQ/uKQIoTINZ6FRNvVOgU8YCpkNn6tVH9ROUE+NQd+ikfsh3NinEokBeCJvtTyinEBFUFXGn6XS/GJ7kO4J/6gOg6mhv7/Hg4WseP91CcSAZjxofuHu/SWdnH5KScZ+ZaN0WQ861K/MMhmdZb77COVuf1rtdVm8vcak6ixY/It34EBc4EqEWPeq1Ks4Jz19sGHllaZGbtYvo6CuIHos8ixmaCSAR1ffBDymVSkncTzTvHcn6kJ8FRnQ3GOYRmeHJszYbm3ss12t2uxrNFr+GPZbrlxH8Mb4dIUQZdIlkvH2/w8s3XVZuLVK7PmeVPL9BY71F5cI5FuYrCEXkh+VH+3fUokqR5Vpmu9Pn6kIFPzqwKCU7z8fugOrcGUQHMdpxxL8/r2k6erLA4bIyPv8+tiSiKyO+/5dlk7cQvQj/Ap9/M+nhjYTPsOil4NI4zVvQ0YNocmj4n3G6iXGr4EXc8d/xD6YQKq6fj1B0AAAAAElFTkSuQmCC) no-repeat 50% 50%;
+}
+
+#fehelper_tips .close span {
+    display: block;
+    height: 100%;
+    background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAACyklEQVQ4EVWTS28URxSFv1tV3dOe7nn6IRsPSAMo4hUnC2QLIhRlA0gRsAJ2WUXKIrvwD/IDss+KRTYIZxXxkJlNVgmWCVIUFLFAToI1gx0YsD1+zUx3V1Q9w0hZXVXVPfeeW/cciV/ftQhgYRAFlAarSA562b3J+1ibAglYO8wb5BuX8f4OPPaabV7+9Aubf/yFSh0IUqWofFindvU8YW0ca/uDXq5W//UdK+4oPs37j3l1f5l8kEN6fbTRGbUkTrG+YXe/x/Tlsxy+9gnWdnE4M2BvaD1Y5k3jKZFnyEcBJlcY0pUsxv0YlSS8/fl3xGhqny9g0y7KWst+6y0bS0/IG6E0XaW0cAGxFnHEhjE8OUdxskxehH8bv7HbbCMiKMSj1VghGvMJyxHB3DzVL25Runwjm99VKV26ztTX35L74DRhNSIKPF41VrBojIim83yNsqsmwkbjIZv7wvGvvhmuBgqXbvLi++9IVh4TlQt4Imw+X0PEYOKDHsqC75uMUmWqwvrSIi9gWIQMvL20yHS9lrHyPYPq9uh39jBuh8oolFYDytgsUUqF4RnCUpF8fXb0rjyNxAo3ntGBD3kPGw+E4VYTXbxO4eKNrLNDuXF2qkW2Hy1mq0vTFBXm0GHgGMQUTxwhXV3Hi1OC+c9G4M6jH7Ou78fpt/6m+2yFdMyncGwGsTHS3/jBHrRj/rl9j4oVtto7xLVTxH/+ykx9NlNca7WJPXySaGuNwkSJNzal/uUVchXtdJAQTIZUL3xEx9cUKyF69ekI7Cxy6Ogs0dZLilNltn3N5KcfkxsPEUlRmReSXSbOnaJ0/gzbgaZ47NDow8R5RYT8kSneGaE4f4Lxc6ch3cWJUHrrt+3IjCqk+26P9vIzdlabqGRoJq0Jj84wsTCHXxlDHHho3qzAwI4DzaM8UAWUHiPuJpl1jaew9gAbd4D4fx75D8MvHH6oTBAOAAAAAElFTkSuQmCC) no-repeat 50% 50%;
+    opacity: 0;
+    transition: opacity .2s;
+}
+
+#fehelper_tips .close:hover span {
+    opacity: .85;
+}
+
+#fehelper_tips .close:active span {
+    opacity: 1;
+}
+#fehelper_tips .ask {
+    color: #f00;
+}
+
+#fehelper_tips .forbid,
+#fehelper_tips .encoding{
+    float: right;
+    font-size: 12px;
+    color: #666;
+    text-decoration: underline;
+    margin-right:20px;
+    cursor: pointer;
+}
+#fehelper_tips .forbid:hover,
+#fehelper_tips .encoding:hover{
+    color:red;
+}
+#fehelper_tips .encoding {
+    float: none;
+    margin-left: 10px;
+}
+
+html.fh-cb body.show-tipsbar > *,
+html.fh-cb body.show-beautified > *{
+    transition: .2s linear;
+}
+
+html.fh-cb body.show-tipsbar pre,
+html.fh-cb body.show-beautified pre {
+    transform: translate3d(0, 37px, 0);
+}
+
+html.fh-cb body #fehelper_tips {
+    transform: none;
+}
+html.fh-cb body.show-tipsbar .copy{
+    display: none;
+}
+html.fh-cb body.show-beautified .copy {
+    display: inline-block;
+}
+
+html.fh-cb body.processing > :not(#fehelper_tips) {
+    opacity: .5;
+    pointer-events: none;
+    cursor: wait;
+}
+html.fh-cb pre {
+    padding-top: 0;
+}
+html.fh-cb pre>code[class*="language"] {
+    overflow: initial;
+}
+
+html.fh-cb pre ol {
+    background: #f9f9f9;
+    color:#ccc;
+}
+html.fh-cb pre ol li {
+    margin: 0 0 0 2px;
+    padding: 1px 0 1px 5px;
+    background: #fff;
+    border-left: 1px solid #ddd;
+}
+html.fh-cb pre ol li>span {
+    color: #444;
+}
+html.fh-cb pre ol li:hover {
+    background:#f5f5f5;
+}
+.show-tipsbar pre,
+html.jf-cb pre {
+    padding: 30px 2px 0;
+}

+ 201 - 0
apps/code-beautify/content-script.js

@@ -0,0 +1,201 @@
+
+window.codebeautifyContentScript = (() => {
+    let __importScript = (filename) => {
+        let url = filename;
+
+        if (location.protocol === 'chrome-extension:' || chrome.runtime && chrome.runtime.getURL) {
+            url = chrome.runtime.getURL('code-beautify/' + filename);
+        }
+        fetch(url).then(resp => resp.text()).then(jsText => {
+            if(window.evalCore && window.evalCore.getEvalInstance){
+                return window.evalCore.getEvalInstance(window)(jsText);
+            }
+            let el = document.createElement('script');
+            el.textContent = jsText;
+            document.head.appendChild(el);
+        });
+    };
+
+    __importScript('beautify.js');
+    __importScript('beautify-css.js');
+
+
+    let highlightWebWorker = () => {
+        // TODO ...
+        // __importScript('../static/vendor/highlight/highlight.js');
+
+        self.onmessage = (event) => {
+            // const result = self.hljs.highlightAuto(event.data);
+            // postMessage(result.value);
+            postMessage(event.data);
+        };
+    };
+
+    let formattedCodes = '';
+    let cssInjected = false;
+    // **************************************************************
+
+    /**
+     * 代码美化
+     */
+    let format = (fileType, source, callback) => {
+
+        let beauty = txtResult => {
+
+            let code = document.getElementsByTagName('pre')[0];
+            formattedCodes = txtResult;
+            code.textContent = txtResult;
+            code.classList.add('language-' + fileType.toLowerCase());
+            document.querySelector('html').classList.add('jf-cb');
+
+            // 用webwork的方式来进行格式化,效率更高
+            let worker = new Worker(URL.createObjectURL(new Blob(["(" + highlightWebWorker.toString() + ")()"], {type: 'text/javascript'})));
+            worker.onmessage = (event) => {
+                code.innerHTML = "<ol><li><span>" + event.data
+                    .replace(/</gm,'&lt;').replace(/>/gm,'&gt;')
+                    .replace(/\n/gm, '</span></li><li><span>') + '</span></li></ol>';
+                callback && callback();
+            };
+            worker.postMessage(txtResult);
+        };
+
+        switch (fileType) {
+            case 'javascript':
+                let opts = {
+                    brace_style: "collapse",
+                    break_chained_methods: false,
+                    indent_char: " ",
+                    indent_scripts: "keep",
+                    indent_size: "4",
+                    keep_array_indentation: true,
+                    preserve_newlines: true,
+                    space_after_anon_function: true,
+                    space_before_conditional: true,
+                    unescape_strings: false,
+                    wrap_line_length: "120"
+                };
+                beauty(js_beautify(source, opts));
+                break;
+            case 'css':
+                css_beautify(source, {}, resp => beauty(resp));
+                break;
+        }
+
+    };
+
+    /**
+     * 检测
+     * @returns {boolean}
+     */
+    window._codebutifydetect_ = (fileType) => {
+
+        if (!document.getElementsByTagName('pre')[0]) {
+            return;
+        }
+        let source = document.getElementsByTagName('pre')[0].textContent;
+
+        // 提前注入css
+        if(!cssInjected) {
+            chrome.runtime.sendMessage({
+                type: 'fh-dynamic-any-thing',
+                thing:'inject-content-css',
+                tool: 'code-beautify'
+            });
+        }
+
+        $(document.body).addClass('show-tipsbar');
+
+        let tipsBar = $('<div id="fehelper_tips">' +
+            '<span class="desc">FeHelper检测到这可能是<i>' + fileType + '</i>代码,<span class="ask">是否进行美化处理?</span></span>' +
+            '<a class="encoding">有乱码?点击修正!</a>' +
+            '<button class="yes">代码美化</button>' +
+            '<button class="no">放弃!</button>' +
+            '<button class="copy hide">复制美化过的代码</button>' +
+            '<button class="close"><span></span></button>' +
+            '<a class="forbid">彻底关闭这个功能!&gt;&gt;</a>' +
+            '</div>').prependTo('body');
+
+        tipsBar.find('button.yes').click((evt) => {
+            tipsBar.find('button.yes,button.no').hide();
+            let elAsk = tipsBar.find('span.ask').text('正在努力美化,请稍候...');
+            format(fileType, source, () => {
+                elAsk.text('已为您美化完毕!');
+                $(document.body).removeClass('show-tipsbar').addClass('show-beautified');
+            });
+        });
+
+        tipsBar.find('a.forbid').click((evt) => {
+            evt.preventDefault();
+            if (confirm('一旦彻底关闭,不可恢复,请确认?')) {
+                chrome.runtime.sendMessage({
+                    type: 'fh-dynamic-any-thing',
+                    thing: 'close-beautify'
+                }, () => {
+                    alert('已关闭,如果要恢复,请在FeHelper「设置页」重新安装「代码美化工具」!');
+                });
+            }
+        });
+
+        tipsBar.find('button.no,button.close').click((evt) => {
+            $(document.body).removeClass('show-tipsbar').removeClass('show-beautified');
+            tipsBar.remove();
+        });
+
+        tipsBar.find('button.copy').click((evt) => {
+            _copyToClipboard(formattedCodes);
+        });
+
+        tipsBar.find('a.encoding').click((evt) => {
+            evt.preventDefault();
+            fetch(location.href).then(res => res.text()).then(text => {
+                source = text;
+                if ($(document.body).hasClass('show-beautified')) {
+                    tipsBar.find('button.yes').trigger('click');
+                } else {
+                    $('#fehelper_tips+pre').text(text);
+                }
+            });
+        });
+    };
+
+
+    /**
+     * chrome 下复制到剪贴板
+     * @param text
+     */
+    let _copyToClipboard = function (text) {
+        let input = document.createElement('textarea');
+        input.style.position = 'fixed';
+        input.style.opacity = 0;
+        input.value = text;
+        document.body.appendChild(input);
+        input.select();
+        document.execCommand('Copy');
+        document.body.removeChild(input);
+
+        alert('代码复制成功,随处粘贴可用!')
+    };
+
+    return function () {
+        let ext = location.pathname.substring(location.pathname.lastIndexOf(".") + 1).toLowerCase();
+        let fileType = ({'js': 'javascript', 'css': 'css'})[ext];
+        let contentType = document.contentType.toLowerCase();
+
+        if (!fileType) {
+            if (/\/javascript$/.test(contentType)) {
+                fileType = 'javascript';
+            } else if (/\/css$/.test(contentType)) {
+                fileType = 'css';
+            }
+        } else if (contentType === 'text/html') {
+            fileType = undefined;
+        }
+
+        chrome.runtime.sendMessage({
+            type: 'fh-dynamic-any-thing',
+            thing: 'code-beautify',
+            params: { fileType, tabId: window.__FH_TAB_ID__ || null }
+        });
+    };
+
+})();

+ 9 - 0
apps/code-beautify/index.css

@@ -0,0 +1,9 @@
+@import url("../static/vendor/prism/prism.css");
+@import url("../static/css/bootstrap.min.css");
+
+#codeSource {
+    height: 150px;
+}
+#btnFormat {
+    outline: none;
+}

+ 63 - 0
apps/code-beautify/index.html

@@ -0,0 +1,63 @@
+<!DOCTYPE HTML>
+<html lang="zh-CN">
+<head>
+    <title>代码美化工具</title>
+    <meta charset="UTF-8">
+    <link rel="shortcut icon" href="../static/img/favicon.ico">
+    <link rel="stylesheet" href="index.css"/>
+    <script type="text/javascript" src="../static/vendor/evalCore.min.js"></script>
+    <script type="text/javascript" src="../static/vendor/vue/vue.js"></script>
+</head>
+<body>
+<div class="wrapper" id="pageContainer">
+    <div class="panel panel-default" style="margin-bottom: 0px;">
+        <div class="panel-heading">
+            <h3 class="panel-title">
+                <a href="https://www.baidufe.com/fehelper/index/index.html" target="_blank" class="x-a-high">
+                    <img src="../static/img/fe-16.png" alt="fehelper"/> FeHelper</a>:代码美化 - <span id="codeTitle">{{selectedType}}</span>
+            </h3>
+        </div>
+    </div>
+    <div class="panel-body mod-code">
+
+        <div class="row">
+            <textarea class="form-control mod-textarea" id="codeSource" ref="codeSource" placeholder="在这里粘贴需要进行美化的代码" v-model="sourceContent"></textarea>
+        </div>
+        <div class="row ui-mt-10">
+
+            <div class="radio ui-d-ib">
+                <label><input id="codeTypeJs" name="codeType" type="radio" value="Javascript" v-model="selectedType">JS代码美化</label>
+            </div>
+
+            <div class="radio ui-d-ib ui-ml-10">
+                <label><input id="codeTypeCss" name="codeType" type="radio" value="CSS" v-model="selectedType">CSS代码美化</label>
+            </div>
+
+            <div class="radio ui-d-ib ui-ml-10">
+                <label><input id="codeTypeHTML" name="codeType" type="radio" value="HTML" v-model="selectedType">HTML代码美化</label>
+            </div>
+
+            <div class="radio ui-d-ib ui-ml-10">
+                <label><input id="codeTypeXML" name="codeType" type="radio" value="XML" v-model="selectedType">XML代码美化</label>
+            </div>
+
+            <div class="radio ui-d-ib ui-ml-10">
+                <label><input id="codeTypeSQL" name="codeType" type="radio" value="SQL" v-model="selectedType">SQL代码美化</label>
+            </div>
+
+            <button id="btnFormat" class="btn btn-success ui-fl-r" @click="format">格式化</button>
+            <button id="btnCopy" class="btn btn-warning ui-fl-r ui-mr-10" @click="copy" v-show="showCopyBtn">复制结果</button>
+        </div>
+        <div class="row" id="jfContent" ref="jfContentBox" v-html="resultContent"></div>
+    </div>
+</div>
+<script src="../static/vendor/jquery/jquery-3.3.1.min.js"></script>
+<script type="text/javascript" src="beautify-html.js"></script>
+<script type="text/javascript" src="beautify-vk.js"></script>
+<script type="text/javascript" src="beautify.js"></script>
+<script type="text/javascript" src="beautify-css.js"></script>
+<script type="text/javascript" src="../static/vendor/prism/prism.js"></script>
+<script type="text/javascript" src="index.js"></script>
+
+</body>
+</html>

+ 136 - 0
apps/code-beautify/index.js

@@ -0,0 +1,136 @@
+/**
+ * FeHelper 代码美化工具
+ */
+new Vue({
+    el: '#pageContainer',
+    data: {
+        selectedType: 'Javascript',
+        sourceContent: '',
+        resultContent: '',
+        showCopyBtn: false
+    },
+
+    mounted: function () {
+        // 在tab创建或者更新时候,监听事件,看看是否有参数传递过来
+        if (location.protocol === 'chrome-extension:') {
+            chrome.tabs.query({currentWindow: true,active: true, }, (tabs) => {
+                let activeTab = tabs.filter(tab => tab.active)[0];
+                chrome.runtime.sendMessage({
+                    type: 'fh-dynamic-any-thing',
+                    thing: 'request-page-content',
+                    tabId: activeTab.id
+                }).then(resp => {
+                    if(!resp || !resp.content) return ;
+                    this.sourceContent = resp.content;
+                    this.format();
+                });
+            });
+        }
+
+        //输入框聚焦
+        this.$refs.codeSource.focus();
+    },
+
+    methods: {
+        format: function () {
+            if (!this.sourceContent.trim()) {
+                return alert('内容为空,不需要美化处理!');
+            }else{
+                this.toast('格式化进行中...');
+            }
+
+            let beauty = (result) => {
+                result = result.replace(/>/g, '&gt;').replace(/</g, '&lt;');
+                result = '<pre class="language-' + this.selectedType.toLowerCase() + ' line-numbers"><code>' + result + '</code></pre>';
+                this.resultContent = result;
+
+                // 代码高亮
+                this.$nextTick(() => {
+                    Prism.highlightAll();
+                    this.showCopyBtn = true;
+                    this.toast('格式化完成!');
+                });
+            };
+
+            switch (this.selectedType) {
+                case 'Javascript':
+                    let opts = {
+                        brace_style: "collapse",
+                        break_chained_methods: false,
+                        indent_char: " ",
+                        indent_scripts: "keep",
+                        indent_size: "4",
+                        keep_array_indentation: true,
+                        preserve_newlines: true,
+                        space_after_anon_function: true,
+                        space_before_conditional: true,
+                        unescape_strings: false,
+                        wrap_line_length: "120",
+                        "max_preserve_newlines": "5",
+                        "jslint_happy": false,
+                        "end_with_newline": false,
+                        "indent_inner_html": false,
+                        "comma_first": false,
+                        "e4x": false
+                    };
+                    beauty(js_beautify(this.sourceContent, opts));
+                    break;
+                case 'CSS':
+                    css_beautify(this.sourceContent, {}, result => beauty(result));
+                    break;
+                case 'HTML':
+                    beauty(html_beautify(this.sourceContent,{indent_size:15}));
+                    break;
+                case 'SQL':
+                    beauty(vkbeautify.sql(this.sourceContent, 4));
+                    break;
+                default:
+                    beauty(vkbeautify.xml(this.sourceContent));
+            }
+
+        },
+
+        copy: function () {
+
+            let _copyToClipboard = function (text) {
+                let input = document.createElement('textarea');
+                input.style.position = 'fixed';
+                input.style.opacity = 0;
+                input.value = text;
+                document.body.appendChild(input);
+                input.select();
+                document.execCommand('Copy');
+                document.body.removeChild(input);
+
+                alert('复制成功,随处粘贴可用!')
+            };
+
+            let txt = this.$refs.jfContentBox.textContent;
+            _copyToClipboard(txt);
+        },
+
+        /**
+         * 自动消失的Alert弹窗
+         * @param content
+         */
+        toast (content) {
+            window.clearTimeout(window.feHelperAlertMsgTid);
+            let elAlertMsg = document.querySelector("#fehelper_alertmsg");
+            if (!elAlertMsg) {
+                let elWrapper = document.createElement('div');
+                elWrapper.innerHTML = '<div id="fehelper_alertmsg" style="position:fixed;bottom:5px;left:5px;z-index:1000000">' +
+                    '<p style="background:#000;display:inline-block;color:#fff;text-align:center;' +
+                    'padding:10px 10px;margin:0 auto;font-size:14px;border-radius:4px;">' + content + '</p></div>';
+                elAlertMsg = elWrapper.childNodes[0];
+                document.body.appendChild(elAlertMsg);
+            } else {
+                elAlertMsg.querySelector('p').innerHTML = content;
+                elAlertMsg.style.display = 'block';
+            }
+
+            window.feHelperAlertMsgTid = window.setTimeout(function () {
+                elAlertMsg.style.display = 'none';
+            }, 3000);
+        }
+    }
+});

La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 6 - 0
apps/code-compress/htmlminifier.min.js


+ 44 - 0
apps/code-compress/index.css

@@ -0,0 +1,44 @@
+@import url("../static/vendor/codemirror/codemirror.css");
+@import url("../static/css/bootstrap.min.css");
+
+.mod-textarea {
+    min-height: 200px;
+}
+.panel-title .x-xother {
+    float: right;
+    font-size: 12px;
+    margin-top: 3px;
+    color: blue;
+    text-decoration: underline;
+}
+.panel-title .x-xother:hover {
+    color:red;
+}
+.x-error {
+    color:red;
+}
+[v-cloak] {
+    display: none;
+}
+.box-infos {
+    color:green;
+    margin-right: 20px;
+}
+.x-right-info {
+    margin-top:5px;
+}
+#fehelper_alertmsg {
+    position: fixed;
+    top: 0;
+    left: 0;
+    right: 0;
+    z-index: 1000000;
+    background: #000;
+    display: inline-block;
+    color: #fff;
+    text-align: center;
+    padding: 10px 10px;
+    margin: 0 auto;
+    font-size: 14px;
+    border-bottom: 1px solid #aaa;
+}

+ 79 - 0
apps/code-compress/index.html

@@ -0,0 +1,79 @@
+<!DOCTYPE HTML>
+<html lang="zh-CN">
+    <head>
+        <title>FeHelper-代码压缩工具(支持HTML/Javascript/CSS)</title>
+        <meta charset="UTF-8">
+        <link rel="shortcut icon" href="../static/img/favicon.ico">
+        <link rel="stylesheet" href="index.css" />
+        <script type="text/javascript" src="../static/vendor/evalCore.min.js"></script>
+        <script type="text/javascript" src="../static/vendor/vue/vue.js"></script>
+    </head>
+    <body>
+    <div class="wrapper" id="pageContainer">
+        <div class="panel panel-default" style="margin-bottom: 0px;">
+            <div class="panel-heading">
+                <h3 class="panel-title">
+                    <a href="https://www.baidufe.com/fehelper/index/index.html" target="_blank" class="x-a-high">
+                        <img src="../static/img/fe-16.png" alt="fehelper"/> FeHelper</a>:代码压缩工具
+                </h3>
+            </div>
+        </div>
+        <div class="panel-body mod-code">
+
+            <div class="row">
+                <textarea class="form-control mod-textarea" id="codeSource" ref="codeSource" v-model="sourceContent" placeholder="在这里粘贴需要进行压缩的代码"></textarea>
+            </div>
+
+            <div class="row ui-mt-10 ui-mb-10">
+
+                <button id="btnFormat" class="btn btn-success btn-sm ui-mr-20" @click="compress()">压缩</button>
+
+                <div class="radio ui-d-ib">
+                    <label><input id="codeTypeHtml" name="codeType" type="radio" value="html" v-model="codeType" @click="changeCodeType('html')">HTML压缩</label>
+                </div>
+
+                <div class="radio ui-d-ib ui-ml-20">
+                    <label><input id="codeTypeJs" name="codeType" type="radio" value="js" v-model="codeType" @click="changeCodeType('js')">Javascript压缩</label>
+                </div>
+
+                <div class="radio ui-d-ib ui-ml-20">
+                    <label><input id="codeTypeCss" name="codeType" type="radio" value="css" v-model="codeType" @click="changeCodeType('css')">CSS压缩</label>
+                </div>
+
+                <div class="ui-fl-r x-right-info" v-cloak v-show="!!resultContent">
+                    <span class="box-infos" v-html="compressInfo"></span>
+                    <button class="btn btn-sm btn-primary" @click="copyToClipboard(resultContent)">复制结果</button>
+                </div>
+            </div>
+
+            <div class="row rst-item" v-cloak v-show="!!resultContent">
+                <textarea class="form-control mod-textarea" :class="hasError?'x-error':''" ref="jfContent" disabled
+                          placeholder="压缩后的代码将在这里显示" v-model="resultContent"></textarea>
+            </div>
+        </div>
+    </div>
+
+    <script src="../static/vendor/uglifyjs3/minify.js"></script>
+    <script src="../static/vendor/uglifyjs3/utils.js"></script>
+    <script src="../static/vendor/uglifyjs3/ast.js"></script>
+    <script src="../static/vendor/uglifyjs3/parse.js"></script>
+    <script src="../static/vendor/uglifyjs3/transform.js"></script>
+    <script src="../static/vendor/uglifyjs3/scope.js"></script>
+    <script src="../static/vendor/uglifyjs3/output.js"></script>
+    <script src="../static/vendor/uglifyjs3/compress.js"></script>
+    <script type="text/javascript" src="uglifyjs3.js"></script>
+
+    <script src="../static/vendor/codemirror/codemirror.js"></script>
+    <script src="../static/vendor/codemirror/javascript.js"></script>
+    <script src="../static/vendor/codemirror/css.js"></script>
+    <script src="../static/vendor/codemirror/xml.js"></script>
+    <script src="../static/vendor/codemirror/htmlmixed.js"></script>
+    <script src="../static/vendor/codemirror/active-line.js"></script>
+    <script src="../static/vendor/codemirror/matchbrackets.js"></script>
+    <script src="../static/vendor/codemirror/placeholder.js"></script>
+    <script type="text/javascript" src="htmlminifier.min.js"></script>
+    <script type="text/javascript" src="index.js"></script>
+
+    <script src="../static/vendor/jquery/jquery-3.3.1.min.js"></script>
+    </body>
+</html>

+ 160 - 0
apps/code-compress/index.js

@@ -0,0 +1,160 @@
+/**
+ * FeHelper Code Compress
+ */
+let editor = {};
+
+new Vue({
+    el: '#pageContainer',
+    data: {
+        codeType: 'html',
+        sourceContent: '',
+        resultContent: '',
+        hasError: false,
+        compressInfo: ''
+    },
+
+    mounted: function () {
+
+        editor = CodeMirror.fromTextArea(this.$refs.codeSource, {
+            mode: "htmlmixed",
+            lineNumbers: true,
+            matchBrackets: true,
+            styleActiveLine: true,
+            lineWrapping: true
+        });
+
+        //输入框聚焦
+        editor.focus();
+    },
+
+    methods: {
+        compress: function () {
+            this.hasError = false;
+            this.compressInfo = '';
+            this.sourceContent = editor.getValue().trim();
+
+            if (!this.sourceContent) {
+                alert('请先粘贴您需要压缩的代码');
+            } else {
+                if (this.codeType === 'js') {
+                    this.jsCompress(this.sourceContent);
+                } else if (this.codeType === 'css') {
+                    this.cssCompress(this.sourceContent);
+                } else {
+                    this.htmlCompress(this.sourceContent);
+                }
+            }
+        },
+
+        changeCodeType(ctype) {
+            let editorMode = {
+                css: 'text/css',
+                js: {name: 'javascript', json: true},
+                html: 'htmlmixed'
+            };
+            editor.setOption('mode', editorMode[ctype]);
+            if (editor.getValue().trim()) {
+                this.$nextTick(this.compress);
+            }
+        },
+
+        buildCompressInfo(original, minified) {
+            let commify = str => String(str).split('').reverse().join('').replace(/(...)(?!$)/g, '$1,').split('').reverse().join('');
+            let diff = original.length - minified.length;
+            let savings = original.length ? (100 * diff / minified.length).toFixed(2) : 0;
+            this.compressInfo = '压缩前: <strong>' + commify(original.length) + '</strong>' +
+                ',压缩后: <strong>' + commify(minified.length) + '</strong>' +
+                ',压缩率: <strong>' + commify(diff) + ' (' + savings + '%)</strong>';
+        },
+
+        jsCompress(js) {
+            let result = UglifyJs3.compress(js);
+            this.hasError = !!result.error;
+            this.resultContent = result.out || result.error;
+            !this.hasError && this.buildCompressInfo(this.sourceContent, this.resultContent);
+        },
+
+        cssCompress(css) {
+            let res = css.replace(/\/\*(.|\n)*?\*\//g, "")
+                .replace(/\s*([\{\}\:\;\,])\s*/g, "$1")
+                .replace(/\,[\s\.\#\d]*\{/g, "{")
+                .replace(/;\s*;/g, ";")
+                .match(/^\s*(\S+(\s+\S+)*)\s*$/);
+            this.resultContent = (res === null) ? css : res[1];
+            this.buildCompressInfo(this.sourceContent, this.resultContent);
+        },
+
+        htmlCompress(html) {
+            let options = {
+                "caseSensitive": false,
+                "collapseBooleanAttributes": true,
+                "collapseInlineTagWhitespace": false,
+                "collapseWhitespace": true,
+                "conservativeCollapse": false,
+                "decodeEntities": true,
+                "html5": true,
+                "includeAutoGeneratedTags": false,
+                "keepClosingSlash": false,
+                "minifyCSS": true,
+                "minifyJS": true,
+                "preserveLineBreaks": false,
+                "preventAttributesEscaping": false,
+                "processConditionalComments": true,
+                "processScripts": ["text/html"],
+                "removeAttributeQuotes": true,
+                "removeComments": true,
+                "removeEmptyAttributes": true,
+                "removeEmptyElements": false,
+                "removeOptionalTags": true,
+                "removeRedundantAttributes": true,
+                "removeScriptTypeAttributes": true,
+                "removeStyleLinkTypeAttributes": true,
+                "removeTagWhitespace": true,
+                "sortAttributes": true,
+                "sortClassName": true,
+                "trimCustomFragments": true,
+                "useShortDoctype": true
+            };
+            options.log = console.log;
+            try {
+                this.resultContent = require('html-minifier').minify(html, options);
+                this.buildCompressInfo(this.sourceContent, this.resultContent);
+            } catch (err) {
+                this.hasError = true;
+                this.resultContent = err;
+            }
+        },
+
+        toast(content) {
+            window.clearTimeout(window.feHelperAlertMsgTid);
+            let elAlertMsg = document.querySelector("#fehelper_alertmsg");
+            if (!elAlertMsg) {
+                let elWrapper = document.createElement('div');
+                elWrapper.innerHTML = '<div id="fehelper_alertmsg">' + content + '</div>';
+                elAlertMsg = elWrapper.childNodes[0];
+                document.body.appendChild(elAlertMsg);
+            } else {
+                elAlertMsg.innerHTML = content;
+                elAlertMsg.style.display = 'block';
+            }
+
+            window.feHelperAlertMsgTid = window.setTimeout(function () {
+                elAlertMsg.style.display = 'none';
+            }, 3000);
+        },
+        copyToClipboard(text) {
+            if (this.hasError) return false;
+
+            let input = document.createElement('textarea');
+            input.style.position = 'fixed';
+            input.style.opacity = 0;
+            input.value = text;
+            document.body.appendChild(input);
+            input.select();
+            document.execCommand('Copy');
+            document.body.removeChild(input);
+
+            this.toast('压缩结果已复制成功,随处粘贴可用!');
+        }
+    }
+});

+ 158 - 0
apps/code-compress/uglifyjs3.js

@@ -0,0 +1,158 @@
+/**
+ * Uglifyjs3 Js压缩
+ * @type {{compress}}
+ */
+var UglifyJs3 = (() => {
+
+    function get_options() {
+        return {
+            parse: {
+                bare_returns: false,
+                ecma: 8,
+                expression: false,
+                filename: null,
+                html5_comments: true,
+                shebang: true,
+                strict: false,
+                toplevel: null
+            },
+            compress: {
+                arrows: true,
+                booleans: true,
+                comparisons: true,
+                computed_props: true,
+                conditionals: true,
+                dead_code: true,
+                drop_console: false,
+                drop_debugger: true,
+                ecma: 6,
+                evaluate: false,
+                expression: false,
+                global_defs: {},
+                hoist_funs: false,
+                hoist_props: true,
+                ie8: false,
+                if_return: true,
+                inline: true,
+                keep_classnames: false,
+                keep_fargs: true,
+                keep_fnames: false,
+                keep_infinity: false,
+                loops: true,
+                negate_iife: true,
+                passes: 1,
+                properties: true,
+                pure_getters: "strict",
+                pure_funcs: null,
+                reduce_funcs: true,
+                sequences: true,
+                side_effects: true,
+                switches: true,
+                top_retain: null,
+                toplevel: false,
+                typeofs: true,
+                unsafe: false,
+                unsafe_arrows: false,
+                unsafe_comps: false,
+                unsafe_Function: false,
+                unsafe_math: false,
+                unsafe_methods: false,
+                unsafe_proto: false,
+                unsafe_regexp: false,
+                unsafe_undefined: false,
+                unused: true,
+                warnings: false
+            },
+            mangle: {
+                eval: false,
+                ie8: false,
+                keep_classnames: false,
+                keep_fnames: false,
+                properties: false,
+                reserved: [],
+                safari10: false,
+                toplevel: false
+            },
+            output: {
+                ascii_only: false,
+                beautify: false,
+                bracketize: false,
+                comments: /@license|@preserve|^!/,
+                ecma: 5,
+                ie8: false,
+                indent_level: 4,
+                indent_start: 0,
+                inline_script: true,
+                keep_quoted_props: false,
+                max_line_len: false,
+                preamble: null,
+                preserve_line: false,
+                quote_keys: false,
+                quote_style: 0,
+                safari10: false,
+                semicolons: true,
+                shebang: true,
+                source_map: null,
+                webkit: false,
+                width: 80,
+                wrap_iife: false
+            },
+            wrap: false
+        };
+    }
+
+    function show_error(e, param) {
+
+        let row = 0, column = 0;
+        if (e instanceof JS_Parse_Error) {
+            let lines = param.split('\n');
+            let line = lines[e.line - 1];
+            row = e.line;
+            column = e.col + 1;
+            e = '压缩出错了:\n\n' + e.message + '\n' +
+                'Line ' + e.line + ', column ' + (e.col + 1) + '\n\n' +
+                (lines[e.line - 2] ? (e.line - 1) + ': ' + lines[e.line - 2] + '\n' : '') + e.line + ': ' +
+                line.substr(0, e.col) + (line.substr(e.col, 1) || ' ') +
+                line.substr(e.col + 1) + '\n' +
+                (lines[e.line] ? (e.line + 1) + ': ' + lines[e.line] : '');
+        } else if (e instanceof Error) {
+            e = e.name + ': ' + e.message;
+        }
+
+        return {
+            error: e,
+            errPos: {
+                row: row,
+                col: column
+            }
+        };
+    }
+
+    function compress(input) {
+
+        try {
+            return main();
+        } catch (e) {
+            return show_error(e, input);
+        }
+
+        function main() {
+            if (!input) {
+                return;
+            }
+
+            let res = minify(input, get_options());
+            if (res.error) {
+                throw res.error;
+            }
+
+            return {
+                out: res.code || '/* 无内容输出! */'
+            };
+        }
+    }
+
+    return {
+        compress: compress
+    }
+})();

+ 542 - 0
apps/color-picker/content-script.js

@@ -0,0 +1,542 @@
+/**
+ * FeHelper Page Color Picker Tools
+ */
+
+window.colorpickerContentScript = function () {
+
+
+    let FeHelper = window.FeHelper || {};
+
+    FeHelper.elemTool = {
+        elm: function (nodeType, attributes, addchilds, appnedTo) {
+            var ne = document.createElement(nodeType), i, l;
+            if (attributes) {
+                if (attributes.event || attributes.events) {
+                    var lev = attributes.event || attributes.events;
+                    if (typeof(lev[0]) == 'string') ne.addEventListener(lev[0], lev[1], lev[2]);
+                    else if (lev.length)
+                        for (i = 0, l = lev.length; i < l; i++)
+                            ne.addEventListener(lev[i][0], lev[i][1], lev[i][2]);
+                }
+            }
+            for (i in attributes) {
+                if (i.substring(0, 5) == 'event') {
+                    //handled earlier
+                } else if (i == 'checked' || i == 'selected') {
+                    if (attributes[i]) ne.setAttribute(i, i);
+                } else ne.setAttribute(i, attributes[i]);
+            }
+            if (addchilds) {
+                for (i = 0, l = addchilds.length; i < l; i++) {
+                    if (addchilds[i]) ne.appendChild(addchilds[i]);//you probably forgot a comma when calling the function
+                }
+            }
+            if (appnedTo) {
+                this.insertNode(ne, appnedTo);
+            }
+
+            return ne;
+        },
+        /*elemTool.txt creates text nodes, does not support HTML entiteis */
+        txt: function (textContent) {
+            return document.createTextNode(textContent);
+        },
+        /*elemTool.ent creates text nodes that may or may not contain HTML entities.  From a
+        single entity to many entities interspersed with text are all supported by this */
+        ent: function (textContent) {
+            return document.createTextNode(this.unescapeHtml(textContent));
+        },
+        /*elemTool.paragraphs creates an array of nodes that may or may not contain HTML entities.*/
+        paragraphs: function (textContent) {
+            var textPieces = textContent.split("\n");
+            var elmArray = [];
+            for (var i = 0, l = textPieces.length; i < l; i++) {
+                elmArray.push(elemTool.elm('p', {}, [elemTool.ent(textPieces[i])]));
+            }
+            return elmArray;
+        },
+        insertNode: function (newNode, parentElem, optionalInsertBefore) {
+            if (!parentElem) parentElem = document.body;
+            if (optionalInsertBefore && optionalInsertBefore.parentNode == parentElem) {
+                parentElem.insertBefore(newNode, optionalInsertBefore);
+            } else {
+                parentElem.appendChild(newNode);
+            }
+        },
+        insertNodes: function (newNodes, parentElem, optionalInsertBefore) {
+            if (typeof(newNodes) != 'array')
+                this.insertNode(newNodes, parentElem, optionalInsertBefore);
+            else {
+                for (var i = 0, l = newNodes.length; i < l; i++) {
+                    this.insertNode(newNodes[i], parentElem, optionalInsertBefore, true);
+                }
+            }
+        },
+        empty: function (node) {
+            while (node.lastChild) node.removeChild(node.lastChild);
+        },
+        unescapeHtml: function (str) { //trick used to make HTMLentiites work inside textNodes
+            if (str.length < 1) return str;
+            var temp = document.createElement("div");
+            str = str.replace(/<\/?\w(?:[^"'>]|"[^"]*"|'[^']*')*>/gmi, '');
+            temp.innerHTML = str;
+            var result = temp.childNodes[0].nodeValue;
+            this.empty(temp);
+            return result;
+        }
+    };
+
+    /**
+     * 页面取色器
+     */
+    FeHelper.ColorPicker = (function () {
+
+        if (!(document.documentElement instanceof HTMLElement)) {
+            return;
+        }
+
+        var elmid1 = 'fehelper-colorpicker-box', elmid2 = 'fehelper-colorpicker-result';
+
+        function _ge(n) {
+            return document.getElementById(n);
+        }
+
+        var n = false, c = false, hex = 'F00BAF', lasthex = '', rgb = null;
+        var hsv = null;
+        var ex = 0, ey = 0, isEnabled = false, isLocked = false, hexIsLowerCase = false, borderValue = '1px solid #666',
+            blankgif = '';
+        var isUpdating = false, lastTimeout = 0, lx = 0, ly = 0;
+        var cvs = document.createElement('canvas');
+        var ctx = cvs.getContext('2d'), x_cvs_scale = 1, y_cvs_scale = 1;
+
+        function RGBtoHex(R, G, B) {
+            return applyHexCase(toHex(R) + toHex(G) + toHex(B))
+        }
+
+        function applyHexCase(hex) {
+            return hexIsLowerCase ? hex.toLowerCase() : hex;
+        }
+
+        function toHex(N) {//http://www.javascripter.net/faq/rgbtohex.htm
+            if (N == null) return "00";
+            N = parseInt(N);
+            if (N == 0 || isNaN(N)) return "00";
+            N = Math.max(0, N);
+            N = Math.min(N, 255);
+            N = Math.round(N);
+            return "0123456789ABCDEF".charAt((N - N % 16) / 16) + "0123456789ABCDEF".charAt(N % 16);
+        }
+
+        function rgb2hsl(r, g, b) {//http://mjijackson.com/2008/02/rgb-to-hsl-and-rgb-to-hsv-color-model-conversion-algorithms-in-javascript
+            r /= 255, g /= 255, b /= 255;
+            var max = Math.max(r, g, b), min = Math.min(r, g, b);
+            var h, s, l = (max + min) / 2;
+            if (max == min) {
+                h = s = 0; // achromatic
+            } else {
+                var d = max - min;
+                s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
+                switch (max) {
+                    case r:
+                        h = (g - b) / d + (g < b ? 6 : 0);
+                        break;
+                    case g:
+                        h = (b - r) / d + 2;
+                        break;
+                    case b:
+                        h = (r - g) / d + 4;
+                        break;
+                }
+                h /= 6;
+            }
+            return {
+                h: Math.round(h * 360),
+                s: Math.round(s * 100),
+                v: Math.round(l * 100)
+            };
+        }
+
+        function emptyNode(node) {
+            while (node.lastChild) node.removeChild(node.lastChild);
+        }
+
+        function snapshotLoaded() {
+            c.style.height = 'auto';
+            c.style.width = (innerWidth) + 'px';
+            x_cvs_scale = c.naturalWidth / innerWidth;
+            y_cvs_scale = c.naturalHeight / innerHeight;
+            cvs.width = c.naturalWidth;
+            cvs.height = c.naturalHeight;
+            ctx.drawImage(c, 0, 0);
+
+            setTimeout(function () {
+                isMakingNew = false;
+                c.style.visibility = "visible";
+                n.style.visibility = "visible";
+                document.body.style.cursor = 'url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACEAAAAhCAYAAABX5MJvAAAA8ElEQVRYCe1WAQ6DIAwsm//e9rP9jOU2z0CpyWhlcQkkpoCld15rVcQ5cs5ZRLZrXbuiXVynDj40SVDQkBKfshChZdBe6yaRUkolmF6X934xxxsSGm4lQqjq8CRBQa6cBCwK9Bk4LzMdVO8USizW129k47HwFtX43i0YjiOIkIDGRGV/2/EezKGydxHBZY2btWntbT8mIMRhOe7s7RFo3IvYFSZqonEekQqAIO5KpMJETVRfw+rugIWFd4pXdJI4MtttZXdGn+mgYP+vBBoPnoaWT9Zr3UpoYL3uIeImARA2W9oe4NI3RKIMFJlPElTvBRTxXFcOwvQSAAAAAElFTkSuQmCC) 16 16,crosshair';
+                updateColorPreview();
+            }, 255);
+        }
+
+
+        function setPixelPreview(pix, hxe, lhex) {
+            if (isLocked) return;
+            var wid = 75, padr = 32;
+            wid = 150;
+            hex = hxe ? hxe : hex;
+            if (!_ge('fehelper-colorpicker-cpimprev') || (rgb && !_ge('cprgbvl'))) {
+                emptyNode(n);
+                FeHelper.elemTool.elm('div', {}, [
+                    FeHelper.elemTool.elm('img', {
+                        id: 'fehelper-colorpicker-cpimprev',
+                        height: wid,
+                        width: wid,
+                        src: pix,
+                        style: 'margin:0px;padding:0px;margin:0px;'
+                    }),
+                    FeHelper.elemTool.elm('br'),
+                    FeHelper.elemTool.elm('input', {
+                        type: 'text',
+                        size: 7,
+                        style: 'width:60px;height:20px;line-height:20px;font-size:10pt;border:' + borderValue,
+                        id: 'fehelper-colorpicker-cphexvl',
+                        value: '#' + hex,
+                        event: ['mouseover', selectTargElm]
+                    })
+                ], n)
+                keepOnScreen();
+            } else {
+                _ge('fehelper-colorpicker-cpimprev').src = pix;
+                _ge('fehelper-colorpicker-cpimprev').width = wid;
+                _ge('fehelper-colorpicker-cpimprev').height = wid;
+                _ge('fehelper-colorpicker-cphexvl').value = hex;
+                n.style.backgroundColor = '#' + hex;
+            }
+        }
+
+        function setCurColor(r) {
+            if (!n) return;
+            hex = r.hex ? r.hex : hex;
+            n.style.backgroundColor = '#' + hex;
+            if (isLocked) setDisplay();
+        }
+
+        function selectTargElm(ev) {
+            ev.target.select();
+        }
+
+        function setDisplay() {//FeHelper.elemTool.elm
+            emptyNode(n);
+            FeHelper.elemTool.elm('div', {}, [
+                FeHelper.elemTool.elm('input', {
+                    type: 'text',
+                    size: 7,
+                    style: 'width:80px;height:20px;line-height:20px;font-size:10pt;border:' + borderValue,
+                    id: 'fehelper-colorpicker-cphexvl',
+                    value: '#' + hex,
+                    event: ['mouseover', selectTargElm]
+                }),
+                FeHelper.elemTool.elm('img', {
+                    style: 'width:20px;height:20px;position:absolute;top:-10px;right:-10px;cursor:pointer;',
+                    src: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAACwklEQVQ4Ea1US0tbQRg9kwmp8YXBi7hKyMZAcaFuFN26daFEUg1BEF24FQr9D/0BIqhbDe0/0I0LceNCQSIVIcZdLkrER2LUm6+caeZiEmkp7cDlMnfOOfM9zneB/7zUn/REJAEgXsfllVI/fscJvncoIhrAZwArrutGb25uoJRCb28vROQKwBqAr0opr5nfEqGIpGq12s7h4SH29vZQKpUMR2uN9vZ29PX1YWJiAsPDw7zkk1Iq+1a0QVBEkuVy+dvW1hYuLi58HKMLBoPo6uoyb+6j0ShmZmYQDodnlVLfLdgXFJGQ53nVjY0NnJ+fo1arMT2Do0BbWxs6OjoQCASMqOd5iMViSKVS0Fp/UEo9ExywygBWj4+PUSwWTWpDQ0MYHx/3jylE4cHBQSwuLiIUCqFQKODk5ISYVQt8K7jCujE1RtPf349kMomRkRGLRSKRwNTUlMng+dkEBHLYPAsyXRaRj9fX19H7+3sTBQ+Pjo5Miul0Gg8PD+aS6elp831/f9/ycXd3B3KpoZTKWdvEb29vfRBT4yKR0S4vL5uLTk9Psbu7619qCeQ6jkOv5t6mbM/Nm6J8WFfWj4sXsFG2WQ2E+sYK5nt6elrOHcfB/Py8sdDl5SUWFhZokxbROjdPASPI3B3Hueru7vZFCZqbm2N9sL29jc3NTVMv1pQdtpHSm+RSwxesq6yNjY35grQMm5TNZvHy8oKnpyesr6/Tc6bz3L++vmJ0dJQcjqJZLcamAP1F+7B2lUoFj4+PRpBRdXZ2mm9kDwwMYGlp6X1j0+la61lag2PF26vVqpkYToVdtBD38XgcmUyGYhy9X6YE4EdoCfw5eJ63wwk4ODiA67ool8tGhJhIJILJyUmwPFrrlp+D1Wl4c65F5IuIFFzXlbOzM8nlclIsFoXf6mehBlJ90xJhM+hvf7DN/H/e/wRZ4k9klRmUggAAAABJRU5ErkJggg==',
+                    alt: 'Close',
+                    title: '[esc]键可直接关闭',
+                    id: 'fehelper-colorpicker-exitbtn',
+                    event: ['click', dissableColorPickerFromHere, true]
+                })
+            ], n);
+            if (_ge('fehelper-colorpicker-cphexvl')) _ge('fehelper-colorpicker-cphexvl').select();
+            keepOnScreen();
+        }
+
+        function picked() {
+            if (isLocked) {
+                lasthex = hex;
+                isLocked = false;
+                emptyNode(n);
+            } else {
+                isLocked = true;
+                setDisplay();
+            }
+        }
+
+        function dissableColorPickerFromHere() {
+            setTimeout(disableColorPicker, 500)
+        }
+
+        function disableColorPicker() {
+            isEnabled = false, isLocked = false;
+            document.removeEventListener('mousemove', mmf);
+            removeEventListener('scroll', ssf);
+            removeEventListener('resize', ssf);
+            removeEventListener('keyup', wk);
+            removeExistingNodes();
+            clearTimeout(lastNewTimeout);
+        }
+
+        function removeExistingNodes() {
+            if (document.body) {
+                c = _ge(elmid1), n = _ge(elmid2);
+                if (c) document.body.removeChild(c);
+                if (n) document.body.removeChild(n);
+                c = false, n = false;
+                document.body.style.cursor = '';
+            }
+        }
+
+        function wk(ev) {
+            if (!isEnabled) return;
+            if (ev.keyCode == 27) {
+                dissableColorPickerFromHere();
+            } else if (ev.keyCode == 82 || ev.keyCode == 74) {//r or j refresh
+                ssf();
+            } else if (ev.keyCode == 13) {
+                picked();
+            }
+        }
+
+        function mmf(ev) {
+            if (!isEnabled) return;
+            if (!isLocked) {
+                lx = (ev.pageX - pageXOffset), ly = (ev.pageY - pageYOffset);
+                ex = Math.round(lx * x_cvs_scale),
+                    ey = Math.round(ly * y_cvs_scale);
+                updateColorPreview();
+            }
+        }
+
+        function ssf(ev) {
+            if (!isEnabled) return;
+            n.style.visibility = "hidden";
+            c.style.visibility = "hidden";//redundent?
+            clearTimeout(lastNewTimeout);
+            lastNewTimeout = setTimeout(function () {
+                newImage()//some delay required OR it won't update
+            }, 250);
+        }
+
+        function initialInit() {
+            removeExistingNodes();
+            c = FeHelper.elemTool.elm('img', {
+                id: elmid1,
+                src: blankgif,
+                style: 'position:fixed;max-width:none!important;max-height:none!important;top:0px;left:0px;margin:0px;padding:0px;overflow:hidden;z-index:2147483646;',
+                events: [['click', picked, true], ['load', snapshotLoaded]]
+            }, [], document.body);
+            n = FeHelper.elemTool.elm('div', {
+                id: elmid2,
+                style: 'position:fixed;min-width:30px;max-width:300px;box-shadow:2px 2px 2px #666;border:' + borderValue + ';border-radius:5px;z-index:2147483646;cursor:default;padding:10px;text-align:center;'
+            }, [], document.body);
+            document.addEventListener('mousemove', mmf);
+            addEventListener('keyup', wk);
+            addEventListener('scroll', ssf);
+            addEventListener('resize', ssf);
+            initializeCanvas();
+            remainingInit();
+        }
+
+        function enableColorPicker() {
+            disableColorPicker();
+            if (!n) {
+                initialInit();
+                return false;
+            }
+            return remainingInit();
+        }
+
+        function remainingInit() {
+            if (!isEnabled) {
+                n.style.visibility = "hidden";
+                c.style.visibility = "hidden";
+                if (isLocked) picked();//unlocks for next pick
+                document.body.style.cursor = 'url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACEAAAAhCAYAAABX5MJvAAAA8ElEQVRYCe1WAQ6DIAwsm//e9rP9jOU2z0CpyWhlcQkkpoCld15rVcQ5cs5ZRLZrXbuiXVynDj40SVDQkBKfshChZdBe6yaRUkolmF6X934xxxsSGm4lQqjq8CRBQa6cBCwK9Bk4LzMdVO8USizW129k47HwFtX43i0YjiOIkIDGRGV/2/EezKGydxHBZY2btWntbT8mIMRhOe7s7RFo3IvYFSZqonEekQqAIO5KpMJETVRfw+rugIWFd4pXdJI4MtttZXdGn+mgYP+vBBoPnoaWT9Zr3UpoYL3uIeImARA2W9oe4NI3RKIMFJlPElTvBRTxXFcOwvQSAAAAAElFTkSuQmCC) 16 16,crosshair';
+                isEnabled = true;
+                setTimeout(newImage, 1);
+                return false;
+            }
+            return true;
+        }
+
+        function keepOnScreen() {
+
+            if (!n) return;
+            n.style.top = (ly + 8) + "px";
+            n.style.left = (lx + 8) + "px";
+            if (n.clientWidth + n.offsetLeft + 24 > innerWidth) {
+                n.style.left = (lx - 8 - n.clientWidth) + "px";
+            }
+            if (n.clientHeight + n.offsetTop + 24 > innerHeight) {
+                n.style.top = (ly - 8 - n.clientHeight) + "px";
+            }
+        }
+
+        function updateColorPreview(ev) {
+            if (!isEnabled) return;
+            keepOnScreen();
+            var data = ctx.getImageData(ex, ey, 1, 1).data;
+            hsv = rgb2hsl(data[0], data[1], data[2]);
+            rgb = {r: data[0], g: data[1], b: data[2]};
+            setCurColor({hex: RGBtoHex(data[0], data[1], data[2])});
+            handleRendering();
+        }
+
+        var isMakingNew = false, lastNewTimeout = 0;
+
+        function newImage() {
+            if (!isEnabled) return;
+            if (isMakingNew) {
+                clearTimeout(lastNewTimeout);
+                lastNewTimeout = setTimeout(function () {
+                    newImage()
+                }, 255);
+                return;
+            }
+            document.body.style.cursor = 'wait';
+            isMakingNew = true;
+            n.style.visibility = "hidden";
+            c.style.visibility = "hidden";
+            c.src = blankgif;
+            var x = innerWidth, y = innerHeight;
+            c.style.width = x + 'px';
+            c.style.height = y + 'px';
+
+            setTimeout(function () {
+                try {
+                    chrome.runtime.sendMessage({
+                        type: 'fh-dynamic-any-thing',
+                        thing:'color-picker-capture',
+                        params: {
+                            url: location.href
+                        }
+                    });
+                } catch (e) {
+                    console.log('有错误发生,可提交此反馈到官网!', e);
+                }
+            }, 255);
+        }
+
+        var lastPreviewURI;
+
+        var icvs = 0, totalWidth = 150;//750
+
+        function handleRendering(quick) {
+            var x = ex, y = ey;
+            if (isMakingNew) {
+                isUpdating = false;
+                return;
+            }
+
+            var startPoint = Math.floor(totalWidth * 0.5);
+            var ox = Math.round(x), oy = Math.round(y);
+
+            if (quick) {
+                var ictx = getMain2dContext();
+                ictx.scale(2, 2);
+                ictx.drawImage(cvs, -ox + (startPoint * 0.5), -oy + (startPoint * 0.5));
+                ictx.scale(0.5, 0.5);
+
+                ictx.fillStyle = "rgba(0,0,0,0.3)";//croshair
+
+                ictx.fillRect(startPoint, 0, 1, totalWidth);
+                ictx.fillRect(0, startPoint, totalWidth, 1);
+
+            } else {
+                var ictx = getMain2dContext();
+                ictx.drawImage(cvs, -ox + (startPoint), -oy + (startPoint));
+                var smi, spi, mp = 15 - 0;
+                //xx,yy
+                for (var i = 0; i < startPoint; i += 2) {
+                    smi = startPoint - i;
+                    spi = startPoint + i;
+                    //drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight) //CANVAS
+                    ictx.drawImage(icvs, spi, 0, smi, totalWidth,//total width really??
+                        spi + 1, 0, smi, totalWidth);
+
+                    ictx.drawImage(icvs, 0, 0, smi + 1, totalWidth,
+                        -1, 0, smi + 1, totalWidth);
+
+                    ictx.drawImage(icvs, 0, spi, totalWidth, smi,
+                        0, spi + 1, totalWidth, smi);
+
+                    ictx.drawImage(icvs, 0, 0, totalWidth, smi + 1,
+                        0, -1, totalWidth, smi + 1);
+
+                    if (i == 0) {
+                        var dat = ictx.getImageData(startPoint, startPoint, 1, 1).data;//notarget
+                        var d = dat[0] + dat[1] + dat[2];
+                        if (d > 192) ictx.fillStyle = "rgba(30,30,30,0.8)";
+                        else ictx.fillStyle = "rgba(225,225,225,0.8)";
+                    } else ictx.fillStyle = "rgba(255,255,255,0.4)";
+
+                    for (var c = 0; c < mp; c++) {
+                        if (++i >= startPoint) break;
+                        smi = startPoint - i;
+                        spi = startPoint + i;
+                        ictx.drawImage(icvs, spi, 0, smi, totalWidth,
+                            spi + 1, 0, smi, totalWidth);
+
+                        ictx.drawImage(icvs, 0, 0, smi + 1, totalWidth,
+                            -1, 0, smi + 1, totalWidth);
+
+                        ictx.drawImage(icvs, 0, spi, totalWidth, smi,
+                            0, spi + 1, totalWidth, smi);
+
+                        ictx.drawImage(icvs, 0, 0, totalWidth, smi + 1,
+                            0, -1, totalWidth, smi + 1);
+                    }
+                    mp--;
+                    if (mp < 1) mp = 1;
+                    ictx.fillRect(spi + 1, 0, 1, totalWidth);
+                    ictx.fillRect(smi - 1, 0, 1, totalWidth);
+                    ictx.fillRect(0, spi + 1, totalWidth, 1);
+                    ictx.fillRect(0, smi - 1, totalWidth, 1);
+                }
+            }
+
+            lastPreviewURI = icvs.toDataURL();//the last one, large size, is cached for revisiting the menu
+
+            var browseIconWidth = (devicePixelRatio > 1 ? 38 : 19);
+            var browseIconHalfWidth = Math.floor(browseIconWidth * 0.5);
+
+            var tmpCvs = document.createElement('canvas');
+            tmpCvs.width = browseIconWidth, tmpCvs.height = browseIconWidth;
+            var tctx = tmpCvs.getContext("2d");
+            tctx.drawImage(icvs, startPoint - browseIconHalfWidth, startPoint - browseIconHalfWidth, browseIconWidth, browseIconWidth, 0, 0, browseIconWidth, browseIconWidth);
+            var pathData = {};
+            pathData[browseIconWidth] = tmpCvs.toDataURL();
+
+            setPixelPreview(lastPreviewURI, hex, lasthex);
+
+            isUpdating = false;
+        }
+
+        function getMain2dContext() {
+            var context = icvs.getContext("2d");
+            if (context) return context;
+            else {
+                initializeCanvas();
+                return icvs.getContext("2d");
+            }
+        }
+
+        function initializeCanvas() {
+            icvs = document.createElement('canvas');//icon canvas
+            icvs.width = totalWidth;
+            icvs.height = totalWidth;
+        }
+
+        return function (request) {
+            if (request.setPickerImage) {
+                c.src = request.pickerImage;
+            } else {
+                enableColorPicker();
+                picked();
+            }
+        };
+    })();
+
+    // 给background page直接调用的
+    window.colorpickerNoPage = function (request) {
+        FeHelper.ColorPicker(request)
+    };
+};

+ 236 - 0
apps/crontab/index.css

@@ -0,0 +1,236 @@
+@import url("../static/css/bootstrap.min.css");
+
+*,
+*::before,
+*::after {
+    box-sizing: border-box;
+}
+::-moz-selection {
+    background-color: rgba(255, 255, 128, 0.2);
+}
+::selection {
+    background-color: rgba(255, 255, 128, 0.2);
+}
+
+.mod-crontab {
+    text-align: center;
+}
+.mod-crontab a,
+.mod-crontab a:visited,
+.mod-crontab a.active {
+    color: #cccccc;
+}
+.mod-crontab h1,
+.mod-crontab .text-highlight {
+    color: gold;
+    font-size: 75%;
+    margin-bottom: 10px;
+    margin-top: 25px;
+}
+.mod-crontab .text-highlight a {
+    color: gold;
+}
+.mod-crontab .monitoring {
+    font-size: 75%;
+    line-height: 1.25em;
+    color: #a8a8a8;
+    margin-bottom: 25px;
+}
+.mod-crontab div.select {
+    position: relative;
+    display: inline-block;
+    height: 15em;
+}
+.mod-crontab div.select label {
+    position: absolute;
+    top: 28px;
+    left: 25px;
+    transform: rotate(45deg);
+    transform-origin: left top 0;
+}
+.mod-crontab table {
+    margin-left: auto;
+    margin-right: auto;
+    border-collapse: collapse;
+    border-top: 1px solid #333333;
+    border-bottom: 1px solid #333333;
+    width: 100%;
+    max-width: 400px;
+}
+.mod-crontab table th,
+.mod-crontab table td {
+    border-top: 1px solid #333333;
+    border-bottom: 1px solid #333333;
+}
+.mod-crontab table th {
+    width: 13em;
+    text-align: right;
+    padding: 0.2em 1em 0.2em 0;
+}
+.mod-crontab table td {
+    text-align: left;
+    padding: 0.2em 0 0.2em 1em;
+}
+.mod-crontab .blurb {
+    font-size: 75%;
+    color: #fff !important;
+}
+.mod-crontab .blurb a {
+    color: #fff;
+}
+#contabContentBox {
+    min-height: 500px;
+}
+.mod-crontab div.warning {
+    color: saddlebrown;
+    font-size: 75%;
+}
+.mod-crontab .text-editor input {
+    font-family: "Courier New", Courier, monospace;
+    text-align: center;
+    font-size: 250%;
+    width: 100%;
+    background-color: #333333;
+    border: 1px solid #cccccc;
+    border-radius: 0.6em;
+    color: #ffffff;
+    padding-top: 0.075rem;
+    box-shadow: 0 0 20px #888;
+}
+.mod-crontab .text-editor {
+    margin-bottom: 10px;
+}
+.mod-crontab .text-editor input.invalid {
+    border: 1px solid darkred;
+}
+.mod-crontab .text-editor input.warning {
+    border: 1px solid saddlebrown;
+}
+.mod-crontab .text-editor input:focus {
+    outline: none;
+}
+.mod-crontab .text-editor input::-ms-clear {
+    width: 0;
+    height: 0;
+}
+.mod-crontab .text-editor input::-moz-selection {
+    color: #f00;
+    background-color: rgba(255, 255, 128, 0.2);
+}
+.mod-crontab .text-editor input::selection {
+    color: #f00;
+    background-color: rgba(255, 255, 128, 0.2);
+}
+.mod-crontab .clickable {
+    text-decoration: underline;
+    cursor: pointer;
+    -webkit-user-select: none;
+    -moz-user-select: none;
+    -ms-user-select: none;
+    user-select: none;
+}
+.mod-crontab .clickable:hover {
+    color:#b00;
+}
+.mod-crontab .part-explanation {
+    font-size: 75%;
+    color: #888;
+    height: 24em;
+}
+.mod-crontab .part-explanation div {
+    display: inline-block;
+    vertical-align: top;
+    margin: 0 1em 0 0;
+}
+.mod-crontab .part-explanation .active {
+    color: #f00;
+}
+.mod-crontab .part-explanation .invalid {
+    background-color: darkred;
+    color: #ff0;
+}
+.mod-crontab .part-explanation .warning {
+    background-color: saddlebrown;
+}
+.mod-crontab .part-explanation .clickable {
+    border-radius: 1em;
+    padding: 0.1em 0.36em;
+}
+.mod-crontab .part-explanation .clickable:last-child {
+    margin: 0;
+}
+.mod-crontab .human-readable {
+    font-size: 14px;
+    min-height: 2.2em;
+    display: -ms-flexbox;
+    display: flex;
+    -ms-flex-pack: end;
+    justify-content: flex-end;
+    -ms-flex-line-pack: end;
+    align-content: flex-end;
+    -ms-flex-direction: column;
+    flex-direction: column;
+    margin-bottom: 0.2em;
+    margin-top: 1.25em;
+}
+.mod-crontab .human-readable .active {
+    color: #f00;
+}
+.mod-crontab .next-date {
+    font-size: 75%;
+    margin-left: 0.5em;
+}
+.mod-crontab .example {
+    text-align: right;
+    font-size: 75%;
+    margin-top: -1em;
+    margin-bottom: 3px;
+}
+.mod-crontab .monitor {
+    border: 1px dotted #666666;
+    border-radius: 0.2em;
+    margin: 3em 0;
+    padding: 0.2em 0;
+    min-height: 10em;
+    color: #666666;
+}
+.mod-crontab .monitor a,
+.mod-crontab .monitor a:visited,
+.mod-crontab .monitor a:active {
+    color: #666666;
+}
+.mod-crontab .monitor .input-copy {
+    display: -ms-flexbox;
+    display: flex;
+    margin: 0.5em 0;
+}
+.mod-crontab .monitor .input-copy input {
+    -ms-flex: 1 1 auto;
+    flex: 1 1 auto;
+    margin: 0 0;
+    border: none;
+    color: inherit;
+    font-family: inherit;
+    font-size: inherit;
+    padding: 0.2em 0.5em;
+    background-color: #262626;
+}
+.mod-crontab .monitor .input-copy button {
+    -ms-flex: 0 1 auto;
+    flex: 0 1 auto;
+    line-height: 0;
+    margin: 0;
+    font-family: inherit;
+    font-size: inherit;
+    background-color: #333333;
+    border-color: #666666;
+    border-width: 1px;
+}
+.mod-crontab .monitor .input-copy .copy {
+    fill: #666666;
+    color: inherit;
+}
+.mod-crontab .monitor .info {
+    font-size: 75%;
+    margin-top: 2em;
+}

+ 28 - 0
apps/crontab/index.html

@@ -0,0 +1,28 @@
+<!DOCTYPE HTML>
+<html lang="zh-CN">
+<head>
+    <title>Crontab生成器</title>
+    <meta charset="UTF-8">
+    <link rel="shortcut icon" href="../static/img/favicon.ico">
+    <link rel="stylesheet" href="index.css" />
+    <script type="text/javascript" src="../static/vendor/evalCore.min.js"></script>
+    <script type="text/javascript" src="../static/vendor/vue/vue.js"></script>
+</head>
+<body>
+
+<div class="wrapper" id="pageContainer">
+    <div class="panel panel-default" style="margin-bottom: 0px;">
+        <div class="panel-heading">
+            <h3 class="panel-title">
+                <a href="https://www.baidufe.com/fehelper/index/index.html" target="_blank" class="x-a-high">
+                    <img src="../static/img/fe-16.png" alt="fehelper"/> FeHelper</a>:Crontab生成器</h3>
+        </div>
+    </div>
+    <div class="panel-body mod-crontab">
+        <div id="contabContentBox">loading...</div>
+    </div>
+</div>
+<script src="../static/vendor/jquery/jquery-3.3.1.min.js"></script>
+<script type="text/javascript" src="index.js"></script>
+</body>
+</html>

+ 1674 - 0
apps/crontab/index.js

@@ -0,0 +1,1674 @@
+/**
+ * codes from crontab.guru
+ * copyright by crontab.guru
+ */
+let crontabGuruStarter = function () {
+    !function a(u, i, s) {
+        function c(t, e) {
+            if (!i[t]) {
+                if (!u[t]) {
+                    var n = "function" == typeof require && require;
+                    if (!e && n) return n(t, !0);
+                    if (l) return l(t, !0);
+                    var r = new Error("Cannot find module '" + t + "'");
+                    throw r.code = "MODULE_NOT_FOUND", r
+                }
+                var o = i[t] = {exports: {}};
+                u[t][0].call(o.exports, function (e) {
+                    return c(u[t][1][e] || e)
+                }, o, o.exports, a, u, i, s)
+            }
+            return i[t].exports
+        }
+
+        for (var l = "function" == typeof require && require, e = 0; e < s.length; e++) c(s[e]);
+        return c
+    }({
+        1: [function (r, e, t) {
+            r("string.prototype.startswith"), r("string.prototype.endswith"), Number.isInteger || (Number.isInteger = r("is-integer")), Array.prototype.includes || (Array.prototype.includes = function (e) {
+                return 0 <= this.indexOf(e)
+            }), String.prototype.includes || (String.prototype.includes = r("string-includes"));
+            var o = r("choo");
+            window.start = function () {
+                var e = document.getElementById("contabContentBox");
+                if (["flexBasis", "webkitFlexBasis", "msFlexAlign"].some(function (e) {
+                        return e in document.body.style
+                    })) {
+                    var t = o();
+                    t.model(r("./models/app")), t.router(function (e) {
+                        return [e("/", r("./pages/home"))]
+                    });
+                    var n = t.start({history: !1, href: !1});
+                    e.replaceChild(n, e.firstChild)
+                } else e.innerHTML = "Your browser is not supported."
+            }
+        }, {
+            "./models/app": 16,
+            "./pages/home": 49,
+            choo: 21,
+            "is-integer": 28,
+            "string-includes": 39,
+            "string.prototype.endswith": 40,
+            "string.prototype.startswith": 41
+        }], 2: [function (r, e, t) {
+            r("choo/html");
+            e.exports = function (e, t, n) {
+                return function () {
+                    var e = r("./yo-yoify/lib/appendChild.js"),
+                        t = document.createElement("span");
+                    return e(t, [arguments[0]]), t
+                }(e.commonBlurb)
+            }
+        }, {
+            "./yo-yoify/lib/appendChild.js": 48,
+            "choo/html": 20
+        }], 3: [function (d, e, t) {
+            d("choo/html");
+
+            function o(e, t) {
+                return t.selectedPart === e ? "active" : ""
+            }
+
+            e.exports = function (e, t, n) {
+                if (e.description) {
+                    var r = e.description;
+                    return r.special ? function () {
+                        var e = d("./yo-yoify/lib/appendChild.js"),
+                            t = document.createElement("div");
+                        t.setAttribute("id", "hr"), t.setAttribute("class", "human-readable");
+                        var n = document.createElement("i"), r = document.createElement("span");
+                        return e(r, [arguments[0]]), e(n, ["\n          “", r, "”\n        "]), e(t, ["\n        ", n, "\n      "]), t
+                    }(r.special) : (setTimeout(function () {
+                        var e = document.getElementById("hr");
+                        e.style.display = "none", e.offsetHeight, e.style.display = ""
+                    }, 0), r.isTime ? function () {
+                        var e = d("./yo-yoify/lib/appendChild.js"),
+                            t = document.createElement("div");
+                        t.setAttribute("id", "hr"), t.setAttribute("class", "human-readable");
+                        var n = document.createElement("i"), r = document.createElement("span");
+                        e(r, [arguments[0]]);
+                        var o = document.createElement("span");
+                        o.setAttribute("class", arguments[1]), e(o, [arguments[2]]);
+                        var a = document.createElement("span");
+                        a.setAttribute("class", arguments[3]), e(a, [arguments[4]]);
+                        var u = document.createElement("span");
+                        u.setAttribute("class", arguments[5]), e(u, [arguments[6]]);
+                        var i = document.createElement("span");
+                        e(i, [arguments[7]]);
+                        var s = document.createElement("span");
+                        s.setAttribute("class", arguments[8]), e(s, [arguments[9]]);
+                        var c = document.createElement("span");
+                        c.setAttribute("class", arguments[10]), e(c, [arguments[11]]);
+                        var l = document.createElement("span");
+                        return e(l, [arguments[12]]), e(n, ["\n            “", r, " ", o, ":", a, arguments[13], u, arguments[14], i, arguments[15], s, arguments[16], c, l, "”\n          "]), e(t, ["\n          ", n, "\n        "]), t
+                    }(r.start, o(2, e), r.hours, o(1, e), r.minutes, o(3, e), r.dates, r.datesWeekdays, o(5, e), r.weekdays, o(4, e), r.months, r.end, r.dates ? " " : "", r.datesWeekdays ? " " : "", r.weekdays ? " " : "", r.months ? " " : "") : function () {
+                        var e = d("./yo-yoify/lib/appendChild.js"),
+                            t = document.createElement("div");
+                        t.setAttribute("id", "hr"), t.setAttribute("class", "human-readable");
+                        var n = document.createElement("i"), r = document.createElement("span");
+                        e(r, [arguments[0]]);
+                        var o = document.createElement("span");
+                        o.setAttribute("class", arguments[1]), e(o, [arguments[2]]);
+                        var a = document.createElement("span");
+                        a.setAttribute("class", arguments[3]), e(a, [arguments[4]]);
+                        var u = document.createElement("span");
+                        u.setAttribute("class", arguments[5]), e(u, [arguments[6]]);
+                        var i = document.createElement("span");
+                        e(i, [arguments[7]]);
+                        var s = document.createElement("span");
+                        s.setAttribute("class", arguments[8]), e(s, [arguments[9]]);
+                        var c = document.createElement("span");
+                        c.setAttribute("class", arguments[10]), e(c, [arguments[11]]);
+                        var l = document.createElement("span");
+                        return e(l, [arguments[12]]), e(n, ["\n          “", r, arguments[13], o, arguments[14], a, arguments[15], u, arguments[16], i, arguments[17], s, arguments[18], c, l, "”\n        "]), e(t, ["\n        ", n, "\n      "]), t
+                    }(r.start, o(1, e), r.minutes, o(2, e), r.hours, o(3, e), r.dates, r.datesWeekdays, o(5, e), r.weekdays, o(4, e), r.months, r.end, r.minutes ? " " : "", r.hours ? " " : "", r.dates ? " " : "", r.datesWeekdays ? " " : "", r.weekdays ? " " : "", r.months ? " " : ""))
+                }
+                return function () {
+                    var e = d("./yo-yoify/lib/appendChild.js"),
+                        t = document.createElement("div");
+                    t.setAttribute("id", "hr"), t.setAttribute("class", "human-readable");
+                    var n = document.createElement("i");
+                    return e(n, [arguments[0]]), e(t, [n]), t
+                }(" ")
+            }
+        }, {
+            "./yo-yoify/lib/appendChild.js": 48,
+            "choo/html": 20
+        }], 4: [function (i, e, t) {
+            i("choo/html");
+            var s = i("../lib/index"), c = i("../lib/dateFormatter");
+            e.exports = function (e, t, a) {
+                var n = e.moreNextDates ? 5 : 1, r = [];
+                if (e.schedule && !e.schedule.errors) {
+                    var o = e.date;
+                    if (o = new Date(Date.UTC(o.getFullYear(), o.getMonth(), o.getDate(), o.getHours(), o.getMinutes(), o.getSeconds())), o = s.nextDate(e.schedule, o)) {
+                        for (var u = [o]; 0 < --n;) o = s.nextDate(e.schedule, new Date(o.getTime() + 1)), u.push(o);
+                        return function () {
+                            var e = i("./yo-yoify/lib/appendChild.js"),
+                                t = document.createElement("div");
+                            return t.setAttribute("class", "next-date"), e(t, ["\n          ", arguments[0], "\n        "]), t
+                        }(r = u.map(function (e, t) {
+                            var n, r = 0 === t ? function () {
+                                    var e = i("./yo-yoify/lib/appendChild.js"),
+                                        t = document.createElement("span");
+                                    return t.onclick = arguments[0], t.setAttribute("class", "clickable"), e(t, ["next"]), t
+                                }(function (e) {
+                                    return a("toggleMoreNextDates")
+                                }) : (i("./yo-yoify/lib/appendChild.js")(n = document.createElement("span"), ["then"]), n),
+                                o = c(e).utc;
+                            return function () {
+                                var e = i("./yo-yoify/lib/appendChild.js"),
+                                    t = document.createElement("div");
+                                return e(t, ["\n            ", arguments[0], " at ", arguments[1], "-", arguments[2], "-", arguments[3], " ", arguments[4], ":", arguments[5], ":00\n          "]), t
+                            }(r, o.year, o.month, o.date, o.hour, o.minute)
+                        }))
+                    }
+                }
+                for (; 0 < n--;) r.push(function () {
+                    var e = i("./yo-yoify/lib/appendChild.js"),
+                        t = document.createElement("div");
+                    return e(t, [arguments[0]]), t
+                }(" "));
+                return function () {
+                    var e = i("./yo-yoify/lib/appendChild.js"),
+                        t = document.createElement("div");
+                    return t.setAttribute("class", "next-date"), e(t, ["\n      ", arguments[0], "\n    "]), t
+                }(r)
+            }
+        }, {
+            "../lib/dateFormatter": 9,
+            "../lib/index": 11,
+            "./yo-yoify/lib/appendChild.js": 48,
+            "choo/html": 20
+        }], 5: [function (Se, e, t) {
+            Se("choo/html");
+            var o = function (e, t) {
+                var n = ["clickable"];
+                e.selectedPart === t && n.push("active");
+                var r = [null, "minutes", "hours", "dates", "months", "weekdays"];
+                return e.schedule.errors && e.schedule.errors.includes(r[t]) ? n.push("invalid") : e.schedule.warnings && e.schedule.warnings.includes(r[t]) && n.push("warning"), n.join(" ")
+            }, a = function (e, t) {
+                return e.selectedPart === t ? "" : "display: none"
+            };
+            e.exports = function (e, t, n) {
+                function r(e, t) {
+                    e.preventDefault(), n("selectPart", t)
+                }
+
+                return function () {
+                    var e = Se("./yo-yoify/lib/appendChild.js"),
+                        t = document.createElement("div");
+                    t.setAttribute("class", "part-explanation");
+                    var n = document.createElement("p");
+                    n.setAttribute("class", "cron-parts");
+                    var r = document.createElement("div"), o = document.createElement("span");
+                    o.onmousedown = arguments[0], o.setAttribute("class", arguments[1]), e(o, ["minute"]), e(r, ["\n          ", o]);
+                    var a = document.createElement("div"), u = document.createElement("span");
+                    u.onmousedown = arguments[2], u.setAttribute("class", arguments[3]), e(u, ["hour"]), e(a, [u]);
+                    var i = document.createElement("div"), s = document.createElement("span");
+                    s.onmousedown = arguments[4], s.setAttribute("class", arguments[5]), e(s, ["day"]), e(i, [s]);
+                    var c = document.createElement("div"), l = document.createElement("span");
+                    l.onmousedown = arguments[6], l.setAttribute("class", arguments[7]), e(l, ["month"]), e(c, [l]);
+                    var d = document.createElement("div"), m = document.createElement("span");
+                    m.onmousedown = arguments[8], m.setAttribute("class", arguments[9]), e(m, ["week"]), e(d, [m]), e(n, ["\n        ", r, a, i, c, d, "\n      "]);
+                    var f = document.createElement("table"), p = document.createElement("tbody"),
+                        h = document.createElement("tr"), v = document.createElement("th");
+                    e(v, ["*"]);
+                    var y = document.createElement("td");
+                    e(y, ["any value"]), e(h, [v, y]);
+                    var b = document.createElement("tr"), g = document.createElement("th");
+                    e(g, [","]);
+                    var w = document.createElement("td");
+                    e(w, ["value list separator"]), e(b, [g, w]);
+                    var E = document.createElement("tr"), x = document.createElement("th");
+                    e(x, ["-"]);
+                    var A = document.createElement("td");
+                    e(A, ["range of values"]), e(E, [x, A]);
+                    var S = document.createElement("tr"), N = document.createElement("th");
+                    e(N, ["/"]);
+                    var C = document.createElement("td");
+                    e(C, ["step values"]), e(S, [N, C]), e(p, ["\n          ", h, "\n          ", b, "\n          ", E, "\n          ", S, "\n        "]);
+                    var j = document.createElement("tbody");
+                    j.setAttribute("style", arguments[10]);
+                    var D = document.createElement("tr"), T = document.createElement("th");
+                    e(T, ["@yearly"]);
+                    var U = document.createElement("td");
+                    e(U, ["(non-standard)"]), e(D, [T, U]);
+                    var k = document.createElement("tr"), _ = document.createElement("th");
+                    e(_, ["@annually"]);
+                    var O = document.createElement("td");
+                    e(O, ["(non-standard)"]), e(k, [_, O]);
+                    var I = document.createElement("tr"), M = document.createElement("th");
+                    e(M, ["@monthly"]);
+                    var P = document.createElement("td");
+                    e(P, ["(non-standard)"]), e(I, [M, P]);
+                    var L = document.createElement("tr"), F = document.createElement("th");
+                    e(F, ["@weekly"]);
+                    var R = document.createElement("td");
+                    e(R, ["(non-standard)"]), e(L, [F, R]);
+                    var z = document.createElement("tr"), $ = document.createElement("th");
+                    e($, ["@daily"]);
+                    var B = document.createElement("td");
+                    e(B, ["(non-standard)"]), e(z, [$, B]);
+                    var H = document.createElement("tr"), V = document.createElement("th");
+                    e(V, ["@hourly"]);
+                    var W = document.createElement("td");
+                    e(W, ["(non-standard)"]), e(H, [V, W]);
+                    var q = document.createElement("tr"), Y = document.createElement("th");
+                    e(Y, ["@reboot"]);
+                    var J = document.createElement("td");
+                    e(J, ["(non-standard)"]), e(q, [Y, J]), e(j, ["\n          ", D, "\n          ", k, "\n          ", I, "\n          ", L, "\n          ", z, "\n          ", H, "\n          ", q, "\n        "]);
+                    var K = document.createElement("tbody");
+                    K.setAttribute("style", arguments[11]);
+                    var X = document.createElement("tr"), G = document.createElement("th");
+                    e(G, ["0-59"]);
+                    var Z = document.createElement("td");
+                    e(Z, ["allowed values"]), e(X, [G, Z]), e(K, ["\n          ", X, "\n        "]);
+                    var Q = document.createElement("tbody");
+                    Q.setAttribute("style", arguments[12]);
+                    var ee = document.createElement("tr"), te = document.createElement("th");
+                    e(te, ["0-23"]);
+                    var ne = document.createElement("td");
+                    e(ne, ["allowed values"]), e(ee, [te, ne]), e(Q, ["\n          ", ee, "\n        "]);
+                    var re = document.createElement("tbody");
+                    re.setAttribute("style", arguments[13]);
+                    var oe = document.createElement("tr"), ae = document.createElement("th");
+                    e(ae, ["1-31"]);
+                    var ue = document.createElement("td");
+                    e(ue, ["allowed values"]), e(oe, [ae, ue]), e(re, ["\n          ", oe, "\n        "]);
+                    var ie = document.createElement("tbody");
+                    ie.setAttribute("style", arguments[14]);
+                    var se = document.createElement("tr"), ce = document.createElement("th");
+                    e(ce, ["1-12"]);
+                    var le = document.createElement("td");
+                    e(le, ["allowed values"]), e(se, [ce, le]);
+                    var de = document.createElement("tr"), me = document.createElement("th");
+                    e(me, ["JAN-DEC"]);
+                    var fe = document.createElement("td");
+                    e(fe, ["alternative single values"]), e(de, [me, fe]), e(ie, ["\n          ", se, "\n          ", de, "\n        "]);
+                    var pe = document.createElement("tbody");
+                    pe.setAttribute("style", arguments[15]);
+                    var he = document.createElement("tr"), ve = document.createElement("th");
+                    e(ve, ["0-6"]);
+                    var ye = document.createElement("td");
+                    e(ye, ["allowed values"]), e(he, [ve, ye]);
+                    var be = document.createElement("tr"), ge = document.createElement("th");
+                    e(ge, ["SUN-SAT"]);
+                    var we = document.createElement("td");
+                    e(we, ["alternative single values"]), e(be, [ge, we]);
+                    var Ee = document.createElement("tr"), xe = document.createElement("th");
+                    e(xe, ["7"]);
+                    var Ae = document.createElement("td");
+                    return e(Ae, ["sunday (non-standard)"]), e(Ee, [xe, Ae]), e(pe, ["\n          ", he, "\n          ", be, "\n          ", Ee, "\n        "]), e(f, ["\n        ", p, "\n        ", j, "\n        ", K, "\n        ", Q, "\n        ", re, "\n        ", ie, "\n        ", pe, "\n      "]), e(t, ["\n      ", n, "\n      ", f, "\n    "]), t
+                }(function (e) {
+                    return r(e, 1)
+                }, o(e, 1), function (e) {
+                    return r(e, 2)
+                }, o(e, 2), function (e) {
+                    return r(e, 3)
+                }, o(e, 3), function (e) {
+                    return r(e, 4)
+                }, o(e, 4), function (e) {
+                    return r(e, 5)
+                }, o(e, 5), a(e, null), a(e, 1), a(e, 2), a(e, 3), a(e, 4), a(e, 5))
+            }
+        }, {
+            "./yo-yoify/lib/appendChild.js": 48,
+            "choo/html": 20
+        }], 6: [function (r, e, t) {
+            r("choo/html");
+            e.exports = function (e, t, n) {
+                return function () {
+                    var e = r("./yo-yoify/lib/appendChild.js"),
+                        t = document.createElement("div");
+                    t.setAttribute("class", "example");
+                    var n = document.createElement("span");
+                    return n.onclick = arguments[0], n.setAttribute("class", "clickable"), e(n, ["Random Example"]), e(t, ["\n      ", n, "\n    "]), t
+                }(function (e) {
+                    return n("showNextExample")
+                })
+            }
+        }, {
+            "./yo-yoify/lib/appendChild.js": 48,
+            "choo/html": 20
+        }], 7: [function (a, e, t) {
+            a("choo/html");
+            var n = a("debounce");
+            var o = n(function (e, t) {
+                t("inputSelect", e.target)
+            }, 20), u = n(function (e, t) {
+                t("inputEnter", e.target.value)
+            }, 100);
+            e.exports = function (t, e, n) {
+                setTimeout(function () {
+                    return function (e, t) {
+                        if (e.selectedPart && e.selectedPart !== t.selectedPart && !e.selectedDirectly) {
+                            var n = e.text.split(" ").slice(0, e.selectedPart), r = n.pop().length,
+                                o = n.join(" ").length;
+                            0 < o && (o += 1);
+                            var a = document.getElementById("input");
+                            a.selectionStart = o, a.selectionEnd = o + r, a.focus()
+                        }
+                    }(t, e)
+                }, 0);
+                var r = "";
+                return t.schedule.errors ? r = "invalid" : t.schedule.warnings && (r = "warning"), t.focussed ? function () {
+                    var e = a("./on-load/server.js"),
+                        t = a("./yo-yoify/lib/appendChild.js"),
+                        n = document.createElement("div");
+                    n.setAttribute("class", "text-editor");
+                    var r = document.createElement("input"), o = arguments;
+                    return e(r, function () {
+                        o[0](r)
+                    }, function () {
+                    }, "o0"), r.setAttribute("id", "input"), r.setAttribute("type", "text"), r.setAttribute("autocomplete", "off"), r.oninput = arguments[1], r.onblur = arguments[2], r.onfocus = arguments[3], r.onselect = arguments[4], r.onkeydown = arguments[5], r.onmousedown = arguments[6], r.setAttribute("class", arguments[7]), t(n, ["\n        ", r, "\n      "]), n
+                }(function (e) {
+                    e.value = t.text
+                }, function (e) {
+                    n("inputUpdate", e.target.value), u(e, n)
+                }, function (e) {
+                    return n("inputBlur")
+                }, function (e) {
+                    n("inputFocus"), o(e, n)
+                }, function (e) {
+                    return o(e, n)
+                }, function (e) {
+                    return o(e, n)
+                }, function (e) {
+                    return o(e, n)
+                }, r) : function () {
+                    var e = a("./on-load/server.js"),
+                        t = a("./yo-yoify/lib/appendChild.js"),
+                        n = document.createElement("div");
+                    n.setAttribute("class", "text-editor");
+                    var r = document.createElement("input"), o = arguments;
+                    return e(r, function () {
+                        o[0](r)
+                    }, function () {
+                    }, "o1"), r.setAttribute("id", "input"), r.setAttribute("type", "text"), r.setAttribute("value", arguments[1]), r.oninput = arguments[2], r.onblur = arguments[3], r.onfocus = arguments[4], r.onselect = arguments[5], r.onkeydown = arguments[6], r.onmousedown = arguments[7], r.setAttribute("class", arguments[8]), t(n, ["\n        ", r, "\n      "]), n
+                }(function (e) {
+                    e.value = t.text
+                }, t.text, function (e) {
+                    n("inputUpdate", e.target.value), u(e, n)
+                }, function (e) {
+                    return n("inputBlur")
+                }, function (e) {
+                    n("inputFocus"), o(e, n)
+                }, function (e) {
+                    return o(e, n)
+                }, function (e) {
+                    return o(e, n)
+                }, function (e) {
+                    return o(e, n)
+                }, r)
+            }
+        }, {
+            "./on-load/server.js": 33,
+            "./yo-yoify/lib/appendChild.js": 48,
+            "choo/html": 20,
+            debounce: 22
+        }], 8: [function (a, e, t) {
+            a("choo/html");
+            e.exports = function (e, t, n) {
+                return e.schedule.warnings ? (r = a("./yo-yoify/lib/appendChild.js"), (o = document.createElement("div")).setAttribute("class", "warning"), r(o, ["Non standard! May not work with every cron."]), o) : function () {
+                    a("./yo-yoify/lib/appendChild.js");
+                    var e = document.createElement("div");
+                    return e.setAttribute("class", "warning"), e
+                }();
+                var r, o
+            }
+        }, {
+            "./yo-yoify/lib/appendChild.js": 48,
+            "choo/html": 20
+        }], 9: [function (e, t, n) {
+            function r(e) {
+                return ("0" + e).slice(-2)
+            }
+
+            t.exports = function (e) {
+                var t = e.toTimeString().split(/[()]/)[1];
+                return t && 3 < t.length && (t = t.replace(/[^A-Z]*/g, "")), t && 3 === t.length || (t = "local"), {
+                    utc: {
+                        year: e.getUTCFullYear(),
+                        month: r(e.getUTCMonth() + 1),
+                        date: r(e.getUTCDate()),
+                        hour: r(e.getUTCHours()),
+                        minute: r(e.getUTCMinutes()),
+                        second: r(e.getUTCSeconds()),
+                        zone: "UTC"
+                    },
+                    local: {
+                        year: e.getFullYear(),
+                        month: r(e.getMonth() + 1),
+                        date: r(e.getDate()),
+                        hour: r(e.getHours()),
+                        minute: r(e.getMinutes()),
+                        second: r(e.getSeconds()),
+                        zone: t
+                    }
+                }
+            }
+        }, {}], 10: [function (e, t, n) {
+            "use strict";
+
+            function u(e) {
+                var t = parseInt(e);
+                switch (20 < t ? t % 10 : t) {
+                    case 1:
+                        return e + "st";
+                    case 2:
+                        return e + "nd";
+                    case 3:
+                        return e + "rd";
+                    default:
+                        return e + "th"
+                }
+            }
+
+            function i(e, t, n, r) {
+                return "*" === e ? "every " + t : function (e, t, n, r) {
+                    var o = e.match(/\d+|./g).map(function (e) {
+                        var t = Number(e);
+                        return isNaN(t) ? e : t
+                    }), a = o[0];
+                    if (Number.isInteger(a)) {
+                        if (1 === o.length) return "" + (n[a] || a);
+                        if (3 === o.length && "/" === o[1] && Number.isInteger(o[2])) return "every " + u(o[2]) + " " + t + " from " + (n[a] || a) + " through " + (n[r] || r);
+                        if (3 === o.length && "-" === o[1] && Number.isInteger(o[2]) && o[2] >= a) return "every " + t + " from " + (n[a] || a) + " through " + (n[o[2]] || o[2]);
+                        if (5 === o.length && "-" === o[1] && Number.isInteger(o[2]) && o[2] >= a && "/" === o[3] && Number.isInteger(o[4]) && 1 <= o[4]) return "every " + u(o[4]) + " " + t + " from " + (n[a] || a) + " through " + (n[o[2]] || o[2])
+                    } else if (3 === o.length && "/" === o[1] && Number.isInteger(o[2]) && "*" === o[0]) return "every " + u(o[2]) + " " + t;
+                    return ""
+                }(e, t, n, r)
+            }
+
+            function h(e, t, n, r, o) {
+                var a = e.split(",");
+                return ((o ? "" : t + " ") + function (e) {
+                    switch (e.length) {
+                        case 0:
+                            return "";
+                        case 1:
+                            return e[0];
+                        case 2:
+                            return e[0] + " and " + e[1];
+                        default:
+                            return e.slice(0, e.length - 1).join(", ") + ", and " + e[e.length - 1]
+                    }
+                }(a.map(function (e) {
+                    return i(e, t, n, r)
+                }))).replace("every 1st", "every").replace(t + " every", "every").replace(", " + t, ", ").replace(", and " + t, ", and ")
+            }
+
+            var v = [null, "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
+            var y = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"];
+            var b = /^0*\d\d?$/;
+            var g = "After rebooting.";
+            t.exports = function (e) {
+                if ("@reboot" === e.originalParts[0]) return {full: g, special: g};
+                var t, n, r, o = e.parts, a = "*" === (r = o[2]) ? "" : "on " + h(r, "day-of-month", {}, 31),
+                    u = "*" === (n = o[3]) ? "" : "in " + h(n, "month", v, 12, !0),
+                    i = "*" === (t = o[4]) ? "" : "on " + h(t, "day-of-week", y, 7, !0), s = "";
+                a && i && (s = e.daysAnded ? "if it's" : "and");
+                var c, l,
+                    d = (c = o[0], l = o[1], b.test(c) && b.test(l) ? [("0" + c).slice(-2), ("0" + l).slice(-2)] : null);
+                if (d) return {
+                    start: "At",
+                    minutes: d[0],
+                    hours: d[1],
+                    isTime: !0,
+                    dates: a || null,
+                    datesWeekdays: s || null,
+                    weekdays: i || null,
+                    months: u || null,
+                    end: ".",
+                    full: ("At " + d[1] + ":" + d[0] + " " + a + " " + s + " " + i + " " + u).replace(/ +/g, " ").trim() + "."
+                };
+                var m, f = h(o[0], "minute", {}, 59), p = "*" === (m = o[1]) ? "" : "past " + h(m, "hour", {}, 23);
+                return {
+                    start: "At",
+                    minutes: f || null,
+                    hours: p || null,
+                    dates: a || null,
+                    datesWeekdays: s || null,
+                    weekdays: i || null,
+                    months: u || null,
+                    end: ".",
+                    full: ("At " + f + " " + p + " " + a + " " + s + " " + i + " " + u).replace(/ +/g, " ").trim() + "."
+                }
+            }
+        }, {}], 11: [function (e, t, n) {
+            "use strict";
+            var r = e("./describe"), o = e("./nextDate"), a = e("./normalize"), u = e("./prenormalize");
+            t.exports = {prenormalize: u, normalize: a, describe: r, nextDate: o}
+        }, {"./describe": 10, "./nextDate": 12, "./normalize": 13, "./prenormalize": 15}], 12: [function (e, t, n) {
+            "use strict";
+
+            function r(e) {
+                var t, n, r = 0 !== (n = (t = e).getUTCMilliseconds()) ? new Date(t.getTime() + (1e3 - n)) : t,
+                    o = r.getUTCSeconds();
+                return 0 !== o ? new Date(r.getTime() + 1e3 * (60 - o)) : r
+            }
+
+            function m(e, t, n, r, o) {
+                return new Date(Date.UTC(e, t, n, r, o))
+            }
+
+            t.exports = function (e, t) {
+                return Object.keys(e).length && e.months.length && e.dates.length && e.weekdays.length && e.hours.length && e.minutes.length ? function e(t, n, r) {
+                    if (127 < r) return null;
+                    var o = n.getUTCMonth() + 1, a = n.getUTCFullYear();
+                    if (!t.months.includes(o)) return e(t, m(a, o + 1 - 1, 1, 0, 0), ++r);
+                    var u = n.getUTCDate(), i = n.getUTCDay(), s = t.dates.includes(u), c = t.weekdays.includes(i);
+                    if (t.daysAnded && (!s || !c) || !t.daysAnded && !s && !c) return e(t, m(a, o - 1, u + 1, 0, 0), ++r);
+                    var l = n.getUTCHours();
+                    if (!t.hours.includes(l)) return e(t, m(a, o - 1, u, l + 1, 0), ++r);
+                    var d = n.getUTCMinutes();
+                    return t.minutes.includes(d) ? n : e(t, m(a, o - 1, u, l, d + 1), ++r)
+                }(e, r(t), 1) : null
+            }
+        }, {}], 13: [function (e, t, n) {
+            "use strict";
+
+            function h(e, t) {
+                return e - t
+            }
+
+            function v(e) {
+                return e.reduce(function (e, t) {
+                    return e.indexOf(t) < 0 && e.push(t), e
+                }, [])
+            }
+
+            function r(e) {
+                return e.reduce(function (e, t) {
+                    return e.concat(Array.isArray(t) ? r(t) : t)
+                }, [])
+            }
+
+            function o(e, t, n) {
+                for (var r = [], o = e; o <= t; o += n) r.push(o);
+                return r
+            }
+
+            var a = /(^|[,-\/])\*($|[,-\/])/g;
+
+            function y(e, t) {
+                var n = "$1" + t + "$2";
+                return e.replace(a, n).replace(a, n)
+            }
+
+            function b(e, t) {
+                var n = e.split(",").map(function (e) {
+                    return function (e, t) {
+                        var n = e ? e.match(/\d+|./g).map(function (e) {
+                            var t = Number(e);
+                            return isNaN(t) ? e : t
+                        }) : [], r = n[0];
+                        if (Number.isInteger(r)) {
+                            if (1 === n.length) return {list: [r]};
+                            if (3 === n.length && "/" === n[1] && Number.isInteger(n[2]) && 1 <= n[2]) return {
+                                list: o(r, t, n[2]),
+                                warnings: ["nonstandard"]
+                            };
+                            if (3 === n.length && "-" === n[1] && Number.isInteger(n[2]) && n[2] >= r) return {list: o(r, n[2], 1)};
+                            if (5 === n.length && "-" === n[1] && Number.isInteger(n[2]) && n[2] >= r && "/" === n[3] && Number.isInteger(n[4]) && 1 <= n[4]) return {list: o(r, n[2], n[4])}
+                        }
+                        return {errors: ["invalid part"]}
+                    }(e, t)
+                });
+                return {
+                    list: v(r(n.map(function (e) {
+                        return e.list || []
+                    }))).sort(h).filter(function (e) {
+                        return !isNaN(e)
+                    }), errors: v(r(n.map(function (e) {
+                        return e.errors || []
+                    }))), warnings: v(r(n.map(function (e) {
+                        return e.warnings || []
+                    })))
+                }
+            }
+
+            function g(e, t, n) {
+                return e.length && (e[0] < t || e[e.length - 1] > n)
+            }
+
+            var w = /[^\d\-\/\,]/i;
+            t.exports = function (e) {
+                var t = e.parts.map(function (e) {
+                    return e.slice(0)
+                }).map(function (e) {
+                    return e.replace(/\*\/1(?!\d)/g, "*")
+                });
+                if (0 === t.length && e.originalParts.length) return {};
+                var n = {errors: [], warnings: []};
+                if (void 0 !== e.daysAnded && (n.daysAnded = e.daysAnded), 5 !== t.length && n.errors.push("fields"), t[0] && t[0].length) {
+                    var r = y(t[0], "0-59"), o = b(r, 59);
+                    n.minutes = o.list, (o.errors.length || g(n.minutes, 0, 59) || w.test(r)) && (n.minutes = [], n.errors.push("minutes")), o.warnings.length && n.warnings.push("minutes")
+                } else void 0 === t[0] && n.errors.push("minutes");
+                if (t[1] && t[1].length) {
+                    var a = y(t[1], "0-23"), u = b(a, 23);
+                    n.hours = u.list, (u.errors.length || g(n.hours, 0, 23) || w.test(a)) && (n.hours = [], n.errors.push("hours")), u.warnings.length && n.warnings.push("hours")
+                } else void 0 === t[1] && n.errors.push("hours");
+                if (t[2] && t[2].length) {
+                    var i = y(t[2], "1-31"), s = b(i, 31);
+                    n.dates = s.list, (s.errors.length || g(n.dates, 1, 31) || w.test(i)) && (n.dates = [], n.errors.push("dates")), s.warnings.length && n.warnings.push("dates")
+                } else void 0 === t[2] && n.errors.push("dates");
+                if (t[3] && t[3].length) {
+                    var c = y(t[3], "1-12"), l = e.originalParts[3], d = b(c, 12);
+                    n.months = d.list, (d.errors.length || g(n.months, 1, 12) || w.test(c)) && (n.months = [], n.errors.push("months")), (d.warnings.length || l && t[3] !== l && 3 < l.length && /\D/.test(l)) && n.warnings.push("months")
+                } else void 0 === t[3] && n.errors.push("months");
+                if (t[4] && t[4].length) {
+                    var m = y(t[4], "0-6"), f = e.originalParts[4], p = b(m, 7);
+                    n.weekdays = v(p.list.map(function (e) {
+                        return 7 === e ? 0 : e
+                    })).sort(h), (p.errors.length || g(n.weekdays, 0, 6) || w.test(m)) && (n.weekdays = [], n.errors.push("weekdays")), (p.warnings.length || p.list.includes(7) || f && t[4] !== f && 3 < f.length && /\D/.test(f)) && n.warnings.push("weekdays")
+                } else void 0 === t[4] && n.errors.push("weekdays");
+                return n.errors.length || delete n.errors, n.warnings.length || delete n.warnings, n
+            }
+        }, {}], 14: [function (e, t, n) {
+            var r = "crontab.guru - the cron schedule expression editor", o = {
+                "/every-minute": "* * * * *",
+                "/every-1-minute": "* * * * *",
+                "/every-2-minutes": "*/2 * * * *",
+                "/every-even-minute": "*/2 * * * *",
+                "/every-uneven-minute": "1-59/2 * * * *",
+                "/every-3-minutes": "*/3 * * * *",
+                "/every-4-minutes": "*/4 * * * *",
+                "/every-5-minutes": "*/5 * * * *",
+                "/every-five-minutes": "*/5 * * * *",
+                "/every-6-minutes": "*/6 * * * *",
+                "/every-10-minutes": "*/10 * * * *",
+                "/every-ten-minutes": "*/10 * * * *",
+                "/every-15-minutes": "*/15 * * * *",
+                "/every-fifteen-minutes": "*/15 * * * *",
+                "/every-quarter-hour": "*/15 * * * *",
+                "/every-20-minutes": "*/20 * * * *",
+                "/every-30-minutes": "*/30 * * * *",
+                "/every-hour-at-30-minutes": "30 * * * *",
+                "/every-half-hour": "*/30 * * * *",
+                "/every-60-minutes": "0 * * * *",
+                "/every-hour": "0 * * * *",
+                "/every-1-hour": "0 * * * *",
+                "/every-2-hours": "0 */2 * * *",
+                "/every-two-hours": "0 */2 * * *",
+                "/every-even-hour": "0 */2 * * *",
+                "/every-other-hour": "0 */2 * * *",
+                "/every-3-hours": "0 */3 * * *",
+                "/every-three-hours": "0 */3 * * *",
+                "/every-4-hours": "0 */4 * * *",
+                "/every-6-hours": "0 */6 * * *",
+                "/every-six-hours": "0 */6 * * *",
+                "/every-8-hours": "0 */8 * * *",
+                "/every-12-hours": "0 */12 * * *",
+                "/hour-range": "0 9-17 * * *",
+                "/between-certain-hours": "0 9-17 * * *",
+                "/every-day": "0 0 * * *",
+                "/daily": "0 0 * * *",
+                "/once-a-day": "0 0 * * *",
+                "/every-night": "0 0 * * *",
+                "/every-day-at-1am": "0 1 * * *",
+                "/every-day-at-2am": "0 2 * * *",
+                "/every-day-8am": "0 8 * * *",
+                "/every-morning": "0 9 * * *",
+                "/every-midnight": "0 0 * * *",
+                "/every-day-at-midnight": "0 0 * * *",
+                "/every-night-at-midnight": "0 0 * * *",
+                "/every-sunday": "0 0 * * SUN",
+                "/every-monday": "0 0 * * MON",
+                "/every-tuesday": "0 0 * * TUE",
+                "/every-wednesday": "0 0 * * WED",
+                "/every-thursday": "0 0 * * THU",
+                "/every-friday": "0 0 * * FRI",
+                "/every-friday-at-midnight": "0 0 * * FRI",
+                "/every-saturday": "0 0 * * SAT",
+                "/every-weekday": "0 0 * * 1-5",
+                "/weekdays-only": "0 0 * * 1-5",
+                "/monday-to-friday": "0 0 * * 1-5",
+                "/every-weekend": "0 0 * * 6,0",
+                "/weekends-only": "0 0 * * 6,0",
+                "/every-7-days": "0 0 * * 0",
+                "/weekly": "0 0 * * 0",
+                "/once-a-week": "0 0 * * 0",
+                "/every-week": "0 0 * * 0",
+                "/every-month": "0 0 1 * *",
+                "/monthly": "0 0 1 * *",
+                "/once-a-month": "0 0 1 * *",
+                "/every-other-month": "0 0 1 */2 *",
+                "/every-quarter": "0 0 1 */3 *",
+                "/every-6-months": "0 0 1 */6 *",
+                "/every-year": "0 0 1 1 *"
+            };
+            t.exports = {
+                textFromLocation: function () {
+                    if (window.location.hash) return decodeURIComponent(window.location.hash).replace("#", "").replace(/_/g, " ");
+                    if (window.location.pathname) {
+                        var e = decodeURIComponent(window.location.pathname);
+                    }
+                    return null
+                }, updateLocation: function (e) {
+                }, defaultTitle: r
+            }
+        }, {}], 15: [function (e, t, n) {
+            "use strict";
+
+            function a(e, i) {
+                return Object.keys(i).reduce(function (e, t) {
+                    return n = e, o = i[r = t], a = new RegExp("(^|[ ,-/])" + r + "($|[ ,-/])", "gi"), u = "$1" + o + "$2", n.replace(a, u).replace(a, u);
+                    var n, r, o, a, u
+                }, e)
+            }
+
+            var u = {sun: "0", mon: "1", tue: "2", wed: "3", thu: "4", fri: "5", sat: "6"};
+            var i = {
+                jan: "1",
+                feb: "2",
+                mar: "3",
+                apr: "4",
+                may: "5",
+                jun: "6",
+                jul: "7",
+                aug: "8",
+                sep: "9",
+                oct: "10",
+                nov: "11",
+                dec: "12"
+            };
+            var s = {
+                "@yearly": ["0", "0", "1", "1", "*"],
+                "@annually": ["0", "0", "1", "1", "*"],
+                "@monthly": ["0", "0", "1", "*", "*"],
+                "@weekly": ["0", "0", "*", "*", "0"],
+                "@daily": ["0", "0", "*", "*", "*"],
+                "@midnight": ["0", "0", "*", "*", "*"],
+                "@hourly": ["0", "*", "*", "*", "*"]
+            };
+            t.exports = function (e) {
+                var t = e.trim().split(/\s+/).filter(function (e) {
+                    return e
+                });
+                if (1 === t.length && "@reboot" === t[0]) return {originalParts: t, parts: []};
+                var n, r, o = (1 === t.length ? (n = t[0], r = s[n], void 0 !== r ? r : [n]) : t).map(function (e, t) {
+                    switch (t) {
+                        case 3:
+                            return a(e, i);
+                        case 4:
+                            return a(e, u);
+                        default:
+                            return e
+                    }
+                });
+                return {originalParts: t, parts: o, daysAnded: !!o[2] && "*" === o[2][0] || !!o[4] && "*" === o[4][0]}
+            }
+        }, {}], 16: [function (e, t, n) {
+            var i = e("../lib/index"), s = e("../lib/path"),
+                o = ["5 0 * 8 *", "15 14 1 * *", "0 22 * * 1-5", "23 0-20/2 * * *", "5 4 * * sun", "0 0,12 1 */2 *", "0 4 8-14 * *", "0 0 1,15 * 3", "@weekly"],
+                c = function (e) {
+                    return e.trim().replace(/ +/g, " ")
+                };
+
+            function a(e) {
+                var t = c(e),
+                    n = null,
+                    r = i.prenormalize(t), o = i.normalize(r), a = o.errors ? null : i.describe(r), u = t.split(" ");
+                return {
+                    schedule: o,
+                    description: a,
+                    commonBlurb: n,
+                    isSpecialString: 1 <= u.length && u[0].startsWith("@")
+                }
+            }
+
+            var r = s.textFromLocation() || "5 4 * * *", u = a(r);
+            t.exports = {
+                state: {
+                    text: r,
+                    schedule: u.schedule,
+                    description: u.description,
+                    exampleIndex: 0,
+                    selectedPart: null,
+                    selectedDirectly: !1,
+                    moreNextDates: !1,
+                    commonBlurb: u.commonBlurb,
+                    isSpecialString: u.isSpecialString,
+                    date: new Date,
+                    focussed: !1
+                }, reducers: {
+                    showNextExample: function (e, t) {
+                        var n = o[t.exampleIndex], r = a(n);
+                        return s.updateLocation(n), {
+                            text: n,
+                            schedule: r.schedule,
+                            description: r.description,
+                            exampleIndex: (t.exampleIndex + 1) % o.length,
+                            isSpecialString: r.isSpecialString
+                        }
+                    }, inputFocus: function (e, t) {
+                        return {focussed: !0}
+                    }, inputBlur: function (e, t) {
+                        return {text: c(t.text), selectedPart: null, focussed: !1}
+                    }, selectPart: function (e, t) {
+                        return {selectedPart: t.isSpecialString ? null : e, selectedDirectly: !1}
+                    }, inputUpdate: function (e, t) {
+                        return {text: e}
+                    }, inputEnter: function (e, t) {
+                        return s.updateLocation(e), a(e)
+                    }, inputSelect: function (e, t) {
+                        if (!t.focussed) return {};
+                        if (t.isSpecialString) return {selectedPart: null, selectedDirectly: !0};
+                        var n = e.selectionStart, r = e.selectionEnd,
+                            o = c(t.text.substring(0, n + 1)).split(" ").length;
+                        return {
+                            selectedPart: o === c(t.text.substring(0, r + 1)).split(" ").length ? Math.max(Math.min(o, 5), 1) : null,
+                            selectedDirectly: !0
+                        }
+                    }, toggleMoreNextDates: function (e, t) {
+                        return {moreNextDates: !t.moreNextDates}
+                    }, setNextMinute: function (e, t) {
+                        return {date: e}
+                    }
+                }, effects: {}, subscriptions: [function (e, t) {
+                    var n = 61 - (new Date).getUTCSeconds();
+                    setTimeout(function () {
+                        e("setNextMinute", new Date, t), setInterval(function () {
+                            e("setNextMinute", new Date, t)
+                        }, 6e4)
+                    }, 1e3 * n)
+                }]
+            }
+        }, {"../lib/index": 11, "../lib/path": 14}], 17: [function (e, t, n) {
+            t.exports = function (e, t, n, r, o, a) {
+                e.forEach(function (e) {
+                    e(t, n, r, o, a)
+                })
+            }
+        }, {}], 18: [function (e, t, n) {
+            var x = e("xtend/mutable"), A = e("xtend"), S = e("./apply-hook");
+
+            function N(n, r, o, a) {
+                n && !o[n] && (o[n] = {}), Object.keys(r).forEach(function (e) {
+                    var t = a ? a(r[e], e) : r[e];
+                    n ? o[n][e] = t : o[e] = t
+                })
+            }
+
+            function r(e) {
+                throw e
+            }
+
+            function C(r) {
+                return function (e, t, n) {
+                    e && r(e, t, n)
+                }
+            }
+
+            function j(t, e) {
+                return e.forEach(function (e) {
+                    t = e(t)
+                }), t
+            }
+
+            t.exports = function (e) {
+                var h = [], v = [], i = [], o = [], s = [], a = [], c = [];
+                t(e = e || {});
+                var l = !1, d = !1, m = !1, f = !1, y = n._subscriptions = {}, b = n._reducers = {},
+                    g = n._effects = {},
+                    w = n._models = [], E = {};
+                return n.model = function (e) {
+                    w.push(e)
+                }, n.state = function (e) {
+                    var r = (e = e || {}).state;
+                    if (!e.state && !1 === e.freeze) return A(E);
+                    if (!e.state) return Object.freeze(A(E));
+                    var o = [], a = {};
+                    w.forEach(function (e) {
+                        var t = e.namespace;
+                        o.push(t);
+                        var n = e.state || {};
+                        t ? (a[t] = a[t] || {}, N(t, n, a), a[t] = A(a[t], r[t])) : x(a, n)
+                    }), Object.keys(r).forEach(function (e) {
+                        -1 === o.indexOf(e) && (a[e] = r[e])
+                    });
+                    var t = j(A(E, A(r, a)), s);
+                    return !1 === e.freeze ? t : Object.freeze(t)
+                }, (n.start = n).use = t, n;
+
+                function t(e) {
+                    e.onStateChange && h.push(e.onStateChange), e.onError && i.push(C(e.onError)), e.onAction && v.push(e.onAction), e.wrapSubscriptions && o.push(e.wrapSubscriptions), e.wrapInitialState && s.push(e.wrapInitialState), e.wrapReducers && a.push(e.wrapReducers), e.wrapEffects && c.push(e.wrapEffects)
+                }
+
+                function n(n) {
+                    return n = n || {}, w.forEach(function (e) {
+                        var r = e.namespace;
+                        if (!m && e.state && !1 !== n.state) {
+                            var t = e.state || {};
+                            r ? (E[r] = E[r] || {}, N(r, t, E)) : x(E, t)
+                        }
+                        !l && e.reducers && !1 !== n.reducers && N(r, e.reducers, b, function (e) {
+                            return j(e, a)
+                        }), !d && e.effects && !1 !== n.effects && N(r, e.effects, g, function (e) {
+                            return j(e, c)
+                        }), !f && e.subscriptions && !1 !== n.subscriptions && N(r, e.subscriptions, y, function (e, t) {
+                            var n = p("subscription: " + (r ? r + ":" + t : t));
+                            return (e = j(e, o))(n, function (e) {
+                                S(i, e, E, p)
+                            }), e
+                        })
+                    }), m || !1 === n.state || (E = j(E, s)), n.noSubscriptions || (f = !0), n.noReducers || (l = !0), n.noEffects || (d = !0), n.noState || (m = !0), i.length || i.push(C(r)), p;
+
+                    function p(o, a) {
+                        return function (e, t, n) {
+                            n || a || (n = t, t = null);
+                            var r = a ? function (e) {
+                                (e = e || null) && S(i, e, E, function (n) {
+                                    return function (e, t) {
+                                        u(e, t = void 0 === t ? null : t, n, r)
+                                    }
+                                })
+                            } : n;
+                            u(e, t = void 0 === t ? null : t, o, r)
+                        }
+                    }
+
+                    function u(l, d, m, f) {
+                        setTimeout(function () {
+                            var e = !1, t = !1, n = A(E);
+                            v.length && S(v, d, E, l, m, p);
+                            var r = l;
+                            if (/:/.test(l)) {
+                                var o = l.split(":"), a = o.shift();
+                                r = o.join(":")
+                            }
+                            var u = a ? b[a] : b;
+                            if (u && u[r]) {
+                                if (a) {
+                                    var i = u[r](d, E[a]);
+                                    n[a] = A(E[a], i)
+                                } else x(n, b[r](d, E));
+                                e = !0, h.length && S(h, d, n, E, r, p), f(null, E = n)
+                            }
+                            var s = a ? g[a] : g;
+                            if (!e && s && s[r]) {
+                                var c = p("effect: " + l);
+                                a ? s[r](d, E[a], c, f) : s[r](d, E, c, f), t = !0
+                            }
+                            if (!e && !t) throw new Error("Could not find action " + r)
+                        }, 0)
+                    }
+                }
+            }
+        }, {"./apply-hook": 17, xtend: 44, "xtend/mutable": 45}], 19: [function (e, t, n) {
+        }, {}], 20: [function (e, t, n) {
+            t.exports = e("yo-yo")
+        }, {"yo-yo": 46}], 21: [function (e, t, n) {
+            var p = e("sheet-router/history"), r = e("sheet-router"), h = e("global/document"), v = e("document-ready"),
+                y = e("sheet-router/href"), b = e("sheet-router/hash"), g = e("hash-match"), o = e("barracks"),
+                w = e("nanoraf"), E = e("xtend"), x = e("yo-yo");
+            t.exports = function (s) {
+                s = s || {};
+                var a = m._store = o(), u = m._router = null, i = null, c = null, l = null, d = null;
+                return a.use({
+                    onStateChange: function (e, t, n, r, o) {
+                        d || (d = w(function (e, t) {
+                            var n = u(e.location.pathname, e, t);
+                            c = x.update(c, n)
+                        })), d(t, n)
+                    }
+                }), a.use(s), m.toString = function (e, t) {
+                    t = t || {}, a.start({subscriptions: !1, reducers: !1, effects: !1});
+                    var n = a.state({state: t}), r = f(i, l, function () {
+                        return function () {
+                        }
+                    })(e, n);
+                    return r.outerHTML || r.toString()
+                }, m.router = function (e, t) {
+                    i = e, l = t
+                }, m.model = function (e) {
+                    a.model(e)
+                }, (m.start = m).use = function (e) {
+                    a.use(e)
+                }, m;
+
+                function m(n, e) {
+                    e || "string" == typeof n || (e = n, n = null), e = e || {}, a.model(function (e) {
+                        var t = h.location, n = {pathname: e.hash ? g(t.hash) : t.href}, r = {};
+                        return !0 === e.hash ? o(function (t) {
+                            b(function (e) {
+                                t(g(e))
+                            })
+                        }, "handleHash", r) : (!1 !== e.history && o(p, "handleHistory", r), !1 !== e.href && o(y, "handleHref", r)), {
+                            namespace: "location",
+                            subscriptions: r,
+                            reducers: {
+                                setLocation: function (e, t) {
+                                    return {pathname: e.location.replace(/#.*/, "")}
+                                }
+                            },
+                            state: n
+                        };
+
+                        function o(e, t, n) {
+                            n[t] = function (t, n) {
+                                e(function (e) {
+                                    t("location:setLocation", {location: e}, n)
+                                })
+                            }
+                        }
+                    }(e));
+                    var t = a.start(e);
+                    u = m._router = f(i, l, t);
+                    var r = a.state({state: {}});
+                    if (!n) {
+                        var o = u(r.location.pathname, r);
+                        return c = o
+                    }
+                    v(function () {
+                        var e = h.querySelector(n), t = u(r.location.pathname, r);
+                        c = x.update(e, t)
+                    })
+                }
+
+                function f(e, t, u) {
+                    var i = {params: {}};
+                    return r(e, t, function (r) {
+                        return function (e, t, n) {
+                            return "function" == typeof t && (o = t, a = u("view: " + e, !0), t = function (e, t) {
+                                var n = i, r = i = E(t, {params: e});
+                                return !1 !== s.freeze && Object.freeze(r), o(r, n, a)
+                            }), r(e, t, n);
+                            var o, a
+                        }
+                    })
+                }
+            }
+        }, {
+            barracks: 18,
+            "document-ready": 23,
+            "global/document": 24,
+            "hash-match": 26,
+            nanoraf: 30,
+            "sheet-router": 38,
+            "sheet-router/hash": 35,
+            "sheet-router/history": 36,
+            "sheet-router/href": 37,
+            xtend: 44,
+            "yo-yo": 46
+        }], 22: [function (e, t, n) {
+            t.exports = function (t, n, r) {
+                var o, a, u, i, s;
+
+                function c() {
+                    var e = Date.now() - i;
+                    e < n && 0 <= e ? o = setTimeout(c, n - e) : (o = null, r || (s = t.apply(u, a), u = a = null))
+                }
+
+                null == n && (n = 100);
+                var e = function () {
+                    u = this, a = arguments, i = Date.now();
+                    var e = r && !o;
+                    return o || (o = setTimeout(c, n)), e && (s = t.apply(u, a), u = a = null), s
+                };
+                return e.clear = function () {
+                    o && (clearTimeout(o), o = null)
+                }, e.flush = function () {
+                    o && (s = t.apply(u, a), u = a = null, clearTimeout(o), o = null)
+                }, e
+            }
+        }, {}], 23: [function (e, t, n) {
+            "use strict";
+            var r = e("global/document");
+            t.exports = r.addEventListener ? function (e) {
+                var t = r.readyState;
+                if ("complete" === t || "interactive" === t) return setTimeout(e, 0);
+                r.addEventListener("DOMContentLoaded", function () {
+                    e()
+                })
+            } : function () {
+            }
+        }, {"global/document": 24}], 24: [function (o, a, e) {
+            (function (e) {
+                var t, n = void 0 !== e ? e : "undefined" != typeof window ? window : {}, r = o("min-document");
+                "undefined" != typeof document ? t = document : (t = n["__GLOBAL_DOCUMENT_CACHE@4"]) || (t = n["__GLOBAL_DOCUMENT_CACHE@4"] = r), a.exports = t
+            }).call(this, "undefined" != typeof global ? global : "undefined" != typeof self ? self : "undefined" != typeof window ? window : {})
+        }, {"min-document": 19}], 25: [function (e, n, t) {
+            (function (e) {
+                var t;
+                t = "undefined" != typeof window ? window : void 0 !== e ? e : "undefined" != typeof self ? self : {}, n.exports = t
+            }).call(this, "undefined" != typeof global ? global : "undefined" != typeof self ? self : "undefined" != typeof window ? window : {})
+        }, {}], 26: [function (e, t, n) {
+            t.exports = function (e, t) {
+                var n = t || "/";
+                return 0 === e.length ? n : (0 != (e = (e = e.replace("#", "")).replace(/\/$/, "")).indexOf("/") && (e = "/" + e), "/" == n ? e : e.replace(n, ""))
+            }
+        }, {}], 27: [function (e, t, n) {
+            "use strict";
+            var r = e("number-is-nan");
+            t.exports = Number.isFinite || function (e) {
+                return !("number" != typeof e || r(e) || e === 1 / 0 || e === -1 / 0)
+            }
+        }, {"number-is-nan": 31}], 28: [function (e, t, n) {
+            var r = e("is-finite");
+            t.exports = Number.isInteger || function (e) {
+                return "number" == typeof e && r(e) && Math.floor(e) === e
+            }
+        }, {"is-finite": 27}], 29: [function (e, t, n) {
+            "use strict";
+            var T, U = "http://www.w3.org/1999/xhtml", k = "undefined" == typeof document ? void 0 : document,
+                r = k ? k.body || k.createElement("div") : {}, s = r.hasAttributeNS ? function (e, t, n) {
+                    return e.hasAttributeNS(t, n)
+                } : r.hasAttribute ? function (e, t, n) {
+                    return e.hasAttribute(n)
+                } : function (e, t, n) {
+                    return null != e.getAttributeNode(t, n)
+                };
+
+            function _(e, t) {
+                var n = e.nodeName, r = t.nodeName;
+                return n === r || !!(t.actualize && n.charCodeAt(0) < 91 && 90 < r.charCodeAt(0)) && n === r.toUpperCase()
+            }
+
+            function o(e, t, n) {
+                e[n] !== t[n] && (e[n] = t[n], e[n] ? e.setAttribute(n, "") : e.removeAttribute(n, ""))
+            }
+
+            var O = {
+                OPTION: function (e, t) {
+                    o(e, t, "selected")
+                }, INPUT: function (e, t) {
+                    o(e, t, "checked"), o(e, t, "disabled"), e.value !== t.value && (e.value = t.value), s(t, null, "value") || e.removeAttribute("value")
+                }, TEXTAREA: function (e, t) {
+                    var n = t.value;
+                    e.value !== n && (e.value = n);
+                    var r = e.firstChild;
+                    if (r) {
+                        var o = r.nodeValue;
+                        if (o == n || !n && o == e.placeholder) return;
+                        r.nodeValue = n
+                    }
+                }, SELECT: function (e, t) {
+                    if (!s(t, null, "multiple")) {
+                        for (var n = 0, r = t.firstChild; r;) {
+                            var o = r.nodeName;
+                            if (o && "OPTION" === o.toUpperCase()) {
+                                if (s(r, null, "selected")) break;
+                                n++
+                            }
+                            r = r.nextSibling
+                        }
+                        e.selectedIndex = n
+                    }
+                }
+            };
+
+            function I() {
+            }
+
+            function M(e) {
+                return e.id
+            }
+
+            var P, a = (P = function (e, t) {
+                var n, r, o, a, u, i = t.attributes;
+                for (n = i.length - 1; 0 <= n; --n) o = (r = i[n]).name, a = r.namespaceURI, u = r.value, a ? (o = r.localName || o, e.getAttributeNS(a, o) !== u && e.setAttributeNS(a, o, u)) : e.getAttribute(o) !== u && e.setAttribute(o, u);
+                for (n = (i = e.attributes).length - 1; 0 <= n; --n) !1 !== (r = i[n]).specified && (o = r.name, (a = r.namespaceURI) ? (o = r.localName || o, s(t, a, o) || e.removeAttributeNS(a, o)) : s(t, null, o) || e.removeAttribute(o))
+            }, function (h, v, e) {
+                if (e || (e = {}), "string" == typeof v) if ("#document" === h.nodeName || "HTML" === h.nodeName) {
+                    var t = v;
+                    (v = k.createElement("html")).innerHTML = t
+                } else n = v, !T && k.createRange && (T = k.createRange()).selectNode(k.body), T && T.createContextualFragment ? r = T.createContextualFragment(n) : (r = k.createElement("body")).innerHTML = n, v = r.childNodes[0];
+                var n, r, o, y = e.getNodeKey || M, b = e.onBeforeNodeAdded || I, a = e.onNodeAdded || I,
+                    g = e.onBeforeElUpdated || I, w = e.onElUpdated || I, u = e.onBeforeNodeDiscarded || I,
+                    i = e.onNodeDiscarded || I, E = e.onBeforeElChildrenUpdated || I, s = !0 === e.childrenOnly, x = {};
+
+                function A(e) {
+                    o ? o.push(e) : o = [e]
+                }
+
+                function S(e, t, n) {
+                    !1 !== u(e) && (t && t.removeChild(e), i(e), function e(t, n) {
+                        if (1 === t.nodeType) for (var r = t.firstChild; r;) {
+                            var o = void 0;
+                            n && (o = y(r)) ? A(o) : (i(r), r.firstChild && e(r, n)), r = r.nextSibling
+                        }
+                    }(e, n))
+                }
+
+                function N(e) {
+                    a(e);
+                    for (var t = e.firstChild; t;) {
+                        var n = t.nextSibling, r = y(t);
+                        if (r) {
+                            var o = x[r];
+                            o && _(t, o) && (t.parentNode.replaceChild(o, t), C(o, t))
+                        }
+                        N(t), t = n
+                    }
+                }
+
+                function C(e, t, n) {
+                    var r, o = y(t);
+                    if (o && delete x[o], !v.isSameNode || !v.isSameNode(h)) {
+                        if (!n) {
+                            if (!1 === g(e, t)) return;
+                            if (P(e, t), w(e), !1 === E(e, t)) return
+                        }
+                        if ("TEXTAREA" !== e.nodeName) {
+                            var a, u, i, s, c = t.firstChild, l = e.firstChild;
+                            e:for (; c;) {
+                                for (i = c.nextSibling, a = y(c); l;) {
+                                    if (u = l.nextSibling, c.isSameNode && c.isSameNode(l)) {
+                                        c = i, l = u;
+                                        continue e
+                                    }
+                                    r = y(l);
+                                    var d = l.nodeType, m = void 0;
+                                    if (d === c.nodeType && (1 === d ? (a ? a !== r && ((s = x[a]) ? l.nextSibling === s ? m = !1 : (e.insertBefore(s, l), u = l.nextSibling, r ? A(r) : S(l, e, !0), l = s) : m = !1) : r && (m = !1), (m = !1 !== m && _(l, c)) && C(l, c)) : 3 !== d && 8 != d || (m = !0, l.nodeValue !== c.nodeValue && (l.nodeValue = c.nodeValue))), m) {
+                                        c = i, l = u;
+                                        continue e
+                                    }
+                                    r ? A(r) : S(l, e, !0), l = u
+                                }
+                                if (a && (s = x[a]) && _(s, c)) e.appendChild(s), C(s, c); else {
+                                    var f = b(c);
+                                    !1 !== f && (f && (c = f), c.actualize && (c = c.actualize(e.ownerDocument || k)), e.appendChild(c), N(c))
+                                }
+                                c = i, l = u
+                            }
+                            for (; l;) u = l.nextSibling, (r = y(l)) ? A(r) : S(l, e, !0), l = u
+                        }
+                        var p = O[e.nodeName];
+                        p && p(e, t)
+                    }
+                }
+
+                !function e(t) {
+                    if (1 === t.nodeType) for (var n = t.firstChild; n;) {
+                        var r = y(n);
+                        r && (x[r] = n), e(n), n = n.nextSibling
+                    }
+                }(h);
+                var c, l, d = h, m = d.nodeType, f = v.nodeType;
+                if (!s) if (1 === m) 1 === f ? _(h, v) || (i(h), d = function (e, t) {
+                    for (var n = e.firstChild; n;) {
+                        var r = n.nextSibling;
+                        t.appendChild(n), n = r
+                    }
+                    return t
+                }(h, (c = v.nodeName, (l = v.namespaceURI) && l !== U ? k.createElementNS(l, c) : k.createElement(c)))) : d = v; else if (3 === m || 8 === m) {
+                    if (f === m) return d.nodeValue !== v.nodeValue && (d.nodeValue = v.nodeValue), d;
+                    d = v
+                }
+                if (d === v) i(h); else if (C(d, v, s), o) for (var p = 0, j = o.length; p < j; p++) {
+                    var D = x[o[p]];
+                    D && S(D, D.parentNode, !1)
+                }
+                return !s && d !== h && h.parentNode && (d.actualize && (d = d.actualize(h.ownerDocument || k)), h.parentNode.replaceChild(d, h)), d
+            });
+            t.exports = a
+        }, {}], 30: [function (e, t, n) {
+            var u = e("global/window");
+            t.exports = function (n, r) {
+                r = r || u.requestAnimationFrame;
+                var o = !1, a = null;
+                return function (e, t) {
+                    null !== a || o || (o = !0, r(function () {
+                        o = !1, a && (n(a, t), a = null)
+                    })), a = e
+                }
+            }
+        }, {"global/window": 25}], 31: [function (e, t, n) {
+            "use strict";
+            t.exports = Number.isNaN || function (e) {
+                return e != e
+            }
+        }, {}], 32: [function (e, t, n) {
+            var r = e("global/document"), o = e("global/window"), s = Object.create(null),
+                a = "onloadid" + (new Date % 9e6).toString(36), c = "data-" + a, u = 0;
+            if (o && o.MutationObserver) {
+                var i = new MutationObserver(function (e) {
+                    if (!(Object.keys(s).length < 1)) for (var t = 0; t < e.length; t++) e[t].attributeName !== c ? (f(e[t].removedNodes, m), f(e[t].addedNodes, d)) : (o = e[t], a = d, u = m, void 0, i = o.target.getAttribute(c), n = o.oldValue, r = i, n && r && s[n][3] === s[r][3] ? s[i] = s[o.oldValue] : (s[o.oldValue] && u(o.oldValue, o.target), s[i] && a(i, o.target)));
+                    var n, r, o, a, u, i
+                });
+                r.body ? l(i) : r.addEventListener("DOMContentLoaded", function (e) {
+                    l(i)
+                })
+            }
+
+            function l(e) {
+                e.observe(r.documentElement, {
+                    childList: !0,
+                    subtree: !0,
+                    attributes: !0,
+                    attributeOldValue: !0,
+                    attributeFilter: [c]
+                })
+            }
+
+            function d(e, t) {
+                s[e][0] && 0 === s[e][2] && (s[e][0](t), s[e][2] = 1)
+            }
+
+            function m(e, t) {
+                s[e][1] && 1 === s[e][2] && (s[e][1](t), s[e][2] = 0)
+            }
+
+            function f(t, n) {
+                for (var e = Object.keys(s), r = 0; r < t.length; r++) {
+                    if (t[r] && t[r].getAttribute && t[r].getAttribute(c)) {
+                        var o = t[r].getAttribute(c);
+                        e.forEach(function (e) {
+                            o === e && n(e, t[r])
+                        })
+                    }
+                    0 < t[r].childNodes.length && f(t[r].childNodes, n)
+                }
+            }
+
+            t.exports = function e(t, n, r, o) {
+                return n = n || function () {
+                }, r = r || function () {
+                }, t.setAttribute(c, "o" + u), s["o" + u] = [n, r, 0, o || e.caller], u += 1, t
+            }, t.exports.KEY_ATTR = c, t.exports.KEY_ID = a
+        }, {"global/document": 24, "global/window": 25}], 33: [function (e, t, n) {
+            var r = e("global/window");
+            r && r.process && "renderer" === r.process.type ? t.exports = e("./index.js") : t.exports = function () {
+            }
+        }, {"./index.js": 32, "global/window": 25}], 34: [function (e, t, n) {
+            t.exports = function (e) {
+                return e.trim().replace(/[\?|#].*$/, "").replace(/^(?:https?\:)\/\//, "").replace(/^.*?(\/.*)/, "$1").replace(/\/$/, "")
+            }
+        }, {}], 35: [function (e, t, n) {
+            var r = e("global/window");
+            t.exports = function (t) {
+                r.onhashchange = function (e) {
+                    t(r.location.hash)
+                }
+            }
+        }, {"global/window": 25}], 36: [function (e, t, n) {
+            var r = e("global/document"), o = e("global/window");
+            t.exports = function (e) {
+                o.onpopstate = function () {
+                    e(r.location.href)
+                }
+            }
+        }, {"global/document": 24, "global/window": 25}], 37: [function (e, t, n) {
+            var o = e("global/window");
+            t.exports = function (r) {
+                o.onclick = function (e) {
+                    var t = function e(t) {
+                        if (t) return "a" !== t.localName ? e(t.parentNode) : void 0 === t.href ? e(t.parentNode) : o.location.host !== t.host ? e(t.parentNode) : t
+                    }(e.target);
+                    if (t) {
+                        e.preventDefault();
+                        var n = t.href.replace(/#$/, "");
+                        r(n), o.history.pushState({}, null, n)
+                    }
+                }
+            }
+        }, {"global/window": 25}], 38: [function (e, t, n) {
+            var r = e("pathname-match"), o = e("wayfarer");
+
+            function u(e, t, n) {
+                return n || (n = t, t = null), [e = e.replace(/^\//, ""), t, n]
+            }
+
+            t.exports = function (e, t, n) {
+                n = n ? n(u) : u, t || (t = e, e = "");
+                var a = o(e);
+                return function t(e, n) {
+                    if (Array.isArray(e[0])) e.forEach(function (e) {
+                        t(e, n)
+                    }); else if (e[1]) {
+                        var r = e[0] ? n.concat(e[0]).join("/") : n.length ? n.join("/") : e[0];
+                        a.on(r, e[1]), t(e[2], n.concat(e[0]))
+                    } else if (Array.isArray(e[2])) t(e[2], n.concat(e[0])); else {
+                        var o = e[0] ? n.concat(e[0]).join("/") : n.length ? n.join("/") : e[0];
+                        a.on(o, e[2])
+                    }
+                }(t(n), []), function (e) {
+                    var t = [].slice.call(arguments);
+                    return t[0] = r(t[0]), a.apply(null, t)
+                }
+            }
+        }, {"pathname-match": 34, wayfarer: 42}], 39: [function (e, t, n) {
+            "use strict";
+            t.exports = function (e, t, n) {
+                if (n = "number" == typeof n ? n : 0, "string" != typeof e) throw new TypeError("Expected a string");
+                return -1 !== e.indexOf(t, n)
+            }
+        }, {}], 40: [function (e, t, n) {
+            String.prototype.endsWith || function () {
+                "use strict";
+                var e = function () {
+                    try {
+                        var e = {}, t = Object.defineProperty, n = t(e, e, e) && t
+                    } catch (e) {
+                    }
+                    return n
+                }(), c = {}.toString, t = function (e) {
+                    if (null == this) throw TypeError();
+                    var t = String(this);
+                    if (e && "[object RegExp]" == c.call(e)) throw TypeError();
+                    var n = t.length, r = String(e), o = r.length, a = n;
+                    if (1 < arguments.length) {
+                        var u = arguments[1];
+                        void 0 !== u && (a = u ? Number(u) : 0) != a && (a = 0)
+                    }
+                    var i = Math.min(Math.max(a, 0), n) - o;
+                    if (i < 0) return !1;
+                    for (var s = -1; ++s < o;) if (t.charCodeAt(i + s) != r.charCodeAt(s)) return !1;
+                    return !0
+                };
+                e ? e(String.prototype, "endsWith", {
+                    value: t,
+                    configurable: !0,
+                    writable: !0
+                }) : String.prototype.endsWith = t
+            }()
+        }, {}], 41: [function (e, t, n) {
+            String.prototype.startsWith || function () {
+                "use strict";
+                var e = function () {
+                    try {
+                        var e = {}, t = Object.defineProperty, n = t(e, e, e) && t
+                    } catch (e) {
+                    }
+                    return n
+                }(), c = {}.toString, t = function (e) {
+                    if (null == this) throw TypeError();
+                    var t = String(this);
+                    if (e && "[object RegExp]" == c.call(e)) throw TypeError();
+                    var n = t.length, r = String(e), o = r.length, a = 1 < arguments.length ? arguments[1] : void 0,
+                        u = a ? Number(a) : 0;
+                    u != u && (u = 0);
+                    var i = Math.min(Math.max(u, 0), n);
+                    if (n < o + i) return !1;
+                    for (var s = -1; ++s < o;) if (t.charCodeAt(i + s) != r.charCodeAt(s)) return !1;
+                    return !0
+                };
+                e ? e(String.prototype, "startsWith", {
+                    value: t,
+                    configurable: !0,
+                    writable: !0
+                }) : String.prototype.startsWith = t
+            }()
+        }, {}], 42: [function (e, t, n) {
+            var s = e("./trie");
+            t.exports = function e(t) {
+                if (!(this instanceof e)) return new e(t);
+                var r = (t || "").replace(/^\//, ""), o = s();
+                return a._trie = o, a.on = function (e, t) {
+                    var n = t._wayfarer && t._trie ? t : function () {
+                        return t.apply(this, Array.prototype.slice.call(arguments))
+                    };
+                    return e = e || "/", n.route = e, n._wayfarer && n._trie ? o.mount(e, n._trie.trie) : o.create(e).cb = n, a
+                }, (a.emit = a).match = u, a._wayfarer = !0, a;
+
+                function a(e) {
+                    var t = u(e), n = new Array(arguments.length);
+                    n[0] = t.params;
+                    for (var r = 1; r < n.length; r++) n[r] = arguments[r];
+                    return t.cb.apply(t.cb, n)
+                }
+
+                function u(e) {
+                    var t = o.match(e);
+                    if (t && t.cb) return new i(t);
+                    var n = o.match(r);
+                    if (n && n.cb) return new i(n);
+                    throw new Error("route '" + e + "' did not match")
+                }
+
+                function i(e) {
+                    this.cb = e.cb, this.route = e.cb.route, this.params = e.params
+                }
+            }
+        }, {"./trie": 43}], 43: [function (e, t, n) {
+            var u = e("xtend/mutable"), r = e("xtend");
+
+            function o() {
+                if (!(this instanceof o)) return new o;
+                this.trie = {nodes: {}}
+            }
+
+            (t.exports = o).prototype.create = function (e) {
+                var a = e.replace(/^\//, "").split("/");
+                return function e(t, n) {
+                    var r = a.hasOwnProperty(t) && a[t];
+                    if (!1 === r) return n;
+                    var o = null;
+                    return /^:|^\*/.test(r) ? (n.nodes.hasOwnProperty("$$") ? o = n.nodes.$$ : (o = {nodes: {}}, n.nodes.$$ = o), "*" === r[0] && (n.wildcard = !0), n.name = r.replace(/^:|^\*/, "")) : n.nodes.hasOwnProperty(r) ? o = n.nodes[r] : (o = {nodes: {}}, n.nodes[r] = o), e(t + 1, o)
+                }(0, this.trie)
+            }, o.prototype.match = function (e) {
+                var o = e.replace(/^\//, "").split("/"), a = {}, t = function t(n, e) {
+                    if (void 0 !== e) {
+                        var r = o[n];
+                        if (void 0 === r) return e;
+                        if (e.nodes.hasOwnProperty(r)) return t(n + 1, e.nodes[r]);
+                        if (e.name) {
+                            try {
+                                a[e.name] = decodeURIComponent(r)
+                            } catch (e) {
+                                return t(n, void 0)
+                            }
+                            return t(n + 1, e.nodes.$$)
+                        }
+                        if (e.wildcard) {
+                            try {
+                                a.wildcard = decodeURIComponent(o.slice(n).join("/"))
+                            } catch (e) {
+                                return t(n, void 0)
+                            }
+                            return e.nodes.$$
+                        }
+                        return t(n + 1)
+                    }
+                }(0, this.trie);
+                if (t) return (t = r(t)).params = a, t
+            }, o.prototype.mount = function (e, t) {
+                var n = e.replace(/^\//, "").split("/"), r = null, o = null;
+                if (1 === n.length) o = n[0], r = this.create(o); else {
+                    var a = n.join("/");
+                    o = n[0], r = this.create(a)
+                }
+                u(r.nodes, t.nodes), t.name && (r.name = t.name), r.nodes[""] && (Object.keys(r.nodes[""]).forEach(function (e) {
+                    "nodes" !== e && (r[e] = r.nodes[""][e])
+                }), u(r.nodes, r.nodes[""].nodes), delete r.nodes[""].nodes)
+            }
+        }, {xtend: 44, "xtend/mutable": 45}], 44: [function (e, t, n) {
+            t.exports = function () {
+                for (var e = {}, t = 0; t < arguments.length; t++) {
+                    var n = arguments[t];
+                    for (var r in n) o.call(n, r) && (e[r] = n[r])
+                }
+                return e
+            };
+            var o = Object.prototype.hasOwnProperty
+        }, {}], 45: [function (e, t, n) {
+            t.exports = function (e) {
+                for (var t = 1; t < arguments.length; t++) {
+                    var n = arguments[t];
+                    for (var r in n) o.call(n, r) && (e[r] = n[r])
+                }
+                return e
+            };
+            var o = Object.prototype.hasOwnProperty
+        }, {}], 46: [function (e, t, n) {
+            var r = e("morphdom"), s = e("./update-events.js");
+            t.exports = {}, t.exports.update = function (e, t, i) {
+                return i || (i = {}), !1 !== i.events && (i.onBeforeElUpdated || (i.onBeforeElUpdated = function (e, t) {
+                    for (var n = i.events || s, r = 0; r < n.length; r++) {
+                        var o = n[r];
+                        t[o] ? e[o] = t[o] : e[o] && (e[o] = void 0)
+                    }
+                    var a = e.value, u = t.value;
+                    "INPUT" === e.nodeName && "file" !== e.type || "SELECT" === e.nodeName ? u || t.hasAttribute("value") ? u !== a && (e.value = u) : t.value = e.value : "TEXTAREA" === e.nodeName && null === t.getAttribute("value") && (e.value = t.value)
+                })), r(e, t, i)
+            }
+        }, {"./update-events.js": 47, morphdom: 29}], 47: [function (e, t, n) {
+            t.exports = ["onclick", "ondblclick", "onmousedown", "onmouseup", "onmouseover", "onmousemove", "onmouseout", "ondragstart", "ondrag", "ondragenter", "ondragleave", "ondragover", "ondrop", "ondragend", "onkeydown", "onkeypress", "onkeyup", "onunload", "onabort", "onerror", "onresize", "onscroll", "onselect", "onchange", "onsubmit", "onreset", "onfocus", "onblur", "oninput", "oncontextmenu", "onfocusin", "onfocusout"]
+        }, {}], 48: [function (e, t, n) {
+            t.exports = function e(t, n) {
+                for (var r = 0; r < n.length; r++) {
+                    var o = n[r];
+                    if (Array.isArray(o)) e(t, o); else {
+                        if (("number" == typeof o || "boolean" == typeof o || o instanceof Date || o instanceof RegExp) && (o = o.toString()), "string" == typeof o) {
+                            if (t.lastChild && "#text" === t.lastChild.nodeName) {
+                                t.lastChild.nodeValue += o;
+                                continue
+                            }
+                            o = document.createTextNode(o)
+                        }
+                        o && o.nodeType && t.appendChild(o)
+                    }
+                }
+            }
+        }, {}], 49: [function (r, e, t) {
+            r("choo/html");
+            var o = r("../elements/part-explanation"), a = r("../elements/text-editor"), u = r("../elements/warning"),
+                i = r("../elements/random-example"), s = r("../elements/next-date"),
+                c = r("../elements/human-readable"),
+                l = r("../elements/blurb");
+            e.exports = function (e, t, n) {
+                return function () {
+                    var e = r("./yo-yoify/lib/appendChild.js"),
+                        t = document.createElement("div");
+                    return e(t, ["\n    ", arguments[0], "\n    ", arguments[1], "\n    ", arguments[2], "\n    ", arguments[3], "\n    ", arguments[4], "\n    ", arguments[5], "\n    ", arguments[6], "\n  "]), t
+                }(c(e, t, n), s(e, t, n), i(e, t, n), a(e, t, n), u(e, t, n), o(e, t, n), l(e, t, n))
+            }
+        }, {
+            "../elements/blurb": 2,
+            "../elements/human-readable": 3,
+            "../elements/next-date": 4,
+            "../elements/part-explanation": 5,
+            "../elements/random-example": 6,
+            "../elements/text-editor": 7,
+            "../elements/warning": 8,
+            "./yo-yoify/lib/appendChild.js": 48,
+            "choo/html": 20
+        }]
+    }, {}, [1]);
+};
+
+/**
+ * FeHelper 进制转换工具
+ */
+new Vue({
+    el: '#containerCrontab',
+    data: {},
+
+    mounted: function () {
+        // 初始化
+        crontabGuruStarter();
+        window.start() ;
+        // 触发一次随机
+        this.randomCron();
+    },
+
+    methods: {
+        randomCron: function(){
+            document.querySelector('#contabContentBox .example span.clickable').click();
+        }
+    }
+});
+

+ 58 - 0
apps/devtools/file-tpl.js

@@ -0,0 +1,58 @@
+window.FileTpl = {
+    // 配置文件
+    'fh-config.js': `{
+    "#toolName#" : {
+        "name": "#toolFullName#",
+        "tips": "我是 #toolName# 的描述信息!你可以在这里修改!",
+        "icon": "#toolIcon#",
+        "contentScript": #contentScript#,
+        "noPage": #noPage#,
+        "updateUrl":"#updateUrl#"
+    }
+}`,
+
+    // 主入口文件
+    'index.html': `<!DOCTYPE html>
+<html>
+<head>
+	<title>#toolName#</title>
+	<link rel="stylesheet" type="text/css" href="index.css">
+</head>
+<body>
+	#toolName#
+	<script type="text/javascript" src="index.js"></script>
+</body>
+</html>`,
+
+    // 内容脚本
+    'content-script.js': `/**
+ * 注意这里的方法名称,不要改!不要改!不要改!
+ */
+window.#toolNameLower#ContentScript = function () {
+    console.log('你好,我是来自FeHelper的工具:#toolName#!');
+};`,
+
+    // noPage为true时需要追加的内容脚本
+    'noPage.js': `/**
+ * 如果在 fh-config.js 中指定了 noPage参数为true,则这里必须定义noPage的接口方法,如:
+ * 注意这里的方法名称,不要改!不要改!不要改!
+ */
+window.#toolNameLower#NoPage = function (tabInfo) {
+    alert('你好,我是来自FeHelper的工具:#toolName#!你可以打开控制台看Demo的输出!');
+    console.log('你好,我是来自FeHelper的工具:#toolName#', tabInfo);
+};`,
+
+    // index.js & index.css
+    "index.js": `/* code here... */\n`,
+    "index.css": `/* code here... */\n`,
+
+    // 系统图标
+    'given-icons': `❤❥웃유☮☏☢☠✔☑♚▲♪✈✞÷↑↓◆◇⊙■□△▽¿─♥❣♂♀☿Ⓐ✉☣☤✘☒♛▼♫⌘☪≈←→◈◎☉★☆⊿※¡━♡ღツ☼☁❅✎©®™Σ✪✯☭➳卐√↖↗●◐Θ℃℉°✿ϟ☃☂✄¢€£∞✫★½✡×↙↘○◑⊕☽☾✚〓↔↕☽☾の①②③④⑤⑥⑦⑧⑨⑩ⅠⅡ
+                    ⅢⅣⅤⅥⅦⅧⅨⅩ♨♛❖☪✙┉☹☺☻ﭢ™℠℗©®♥❤❥❣❦❧♡۵웃유ღ♂♀☿☼☀☁☂☄☾☽❄☃☈⊙☉℃℉❅✺ϟ☇♤♧♡♢♠♣♥♦☜☞☚☛☟✽✾✿❁❃❋❀⚘☑✓✔√☐☒✗✘ㄨ✕✖✖⋆✢✣✤✥❋✦✧✩✰✪✫✬✭✮✯❂✡★✱✲✳✴✵✶✷✸✹✺✻✼❄❅❆❇❈❉
+                    ❊†☨✞✝☥☦☓☩☯☧☬☸✡♁✙♆☩☨☦✞✛✜✝✙✠✚†‡◉○◌◍◎●◐◑◒◓◔◕◖◗❂☢⊗⊙◘◙◍⅟½⅓⅕⅙⅛⅔⅖⅚⅜¾⅗⅝⅞⅘⊰⊱⋛⋚∫∬∭∮∯∰∱∲∳%℅‰‱㊣㊎㊍㊌㊋㊏㊐㊊㊚㊛㊤㊥㊦㊧㊨㊒㊞㊑㊒㊓㊔㊕㊖㊗㊘㊜㊝㊟
+                    ㊠㊡㊢㊩㊪㊫㊬㊭㊮㊯㊰㊙㉿囍♔♕♖♗♘♙♚♛♜♝♞♟ℂℍℕℙℚℝℤℬℰℯℱℊℋℎℐℒℓℳℴ℘ℛℭ℮ℌℑℜℨ♪♫♩♬♭♮♯°øⒶ☮☪✡☭✯卐✐✎✏✑✒✉✁✂✃✄✆✉☎☏➟➡➢➣➤➥➦➧➨➚➘➙➛➜➝➞➸➲➳⏎➴➵➶➷➸➹➺➻➼➽
+                    ←↑→↓↔↕↖↗↘↙↚↛↜↝↞↟↠↡↢↣↤↥↦↧↨➫➬➩➪➭➮➯➱↩↪↫↬↭↮↶↷↸↹↺↻↼⇄⇅⇆⇇⇈⇉⇊⇋⇌⇍⇎⇏⇐⇑⇒⇓⇔⇕⇖⇗⇘⇙⇚⇛⇜⇝⇞⇟⇦⇧⇨⇩⇪♤♧♡♢♠♣♥♦☀☁☂❄☃♨웃유❖☽☾☪✿♂♀✪✯☭➳卍卐√×■◆●○◐◑✙☺☻❀⚘♔♕♖♗♘♙♚♛♜
+                    ♝♞♟♧♡♂♀♠♣♥❤☜☞☎☏⊙◎☺☻☼▧▨♨◐◑↔↕▪▒◊◦▣▤▥▦▩◘◈◇♬♪♩♭♪の★☆♦◊◘◙◦☼♠♣▣▤▥▦▩◘◙◈♫♬♪£Ю〓§♤♥▶¤✲❈✿✲❈➹☀☂☁【】┱┲❣✚✪✣✤✥✦❉❥❦❧❃❂❁❀✄☪☣☢☠☭ღ▶▷◀◁☀☁☂☃☄★☆☇☈⊙☊☋☌☍➀
+                    ➁➂➃➄➅➆➇➈➉➊➋➌➍➎➏➐➑➒➓㊀㊁㊂㊃㊄㊅㊆㊇㊈㊉ⒶⒷⒸⒹⒺⒻⒼⒽⒾⒿⓀⓁⓂⓃⓄⓅⓆⓇⓈⓉⓊⓋⓌⓍⓎⓏⓐⓑⓒⓓⓔⓕⓖⓗⓘⓙⓚⓛⓜⓝⓞⓟⓠⓡⓢⓣⓤⓥⓦⓧⓨⓩ⒜
+                    ⒝⒞⒟⒠⒡⒢⒣⒤⒥⒦⒧⒨⒩⒪⒫⒬⒭⒮⒯⒰⒱⒲⒳⒴⒵`
+};

+ 3 - 0
apps/devtools/hello-world/content-script.css

@@ -0,0 +1,3 @@
+.hello {
+    color:red;
+}

+ 29 - 0
apps/devtools/hello-world/content-script.js

@@ -0,0 +1,29 @@
+
+let cssInjected = false;
+/**
+ * 注意这里的方法名称,其实是:window[`${toolName.replace(/[-_]/g,'')}ContentScript`];
+ * @author 阿烈叔
+ */
+window.helloworldContentScript = function () {
+    // 动态注入css
+    if(!cssInjected) {
+        chrome.runtime.sendMessage({
+            type: 'fh-dynamic-any-thing',
+            thing:'inject-content-css',
+            devTool: true,
+            tool: 'hello-world'
+        });
+    }
+
+    console.log('你好,我是来自FeHelper的工具Demo:hello world!');
+};
+
+/**
+ * 如果在 fh-config.js 中指定了 noPage参数为true,则这里必须定义noPage的接口方法,如:
+ * 注意这里的方法名称,其实是:window[`${toolName.replace(/[-_]/g,'')}NoPage`];
+ * @author 阿烈叔
+ */
+window.helloworldNoPage = function (tabInfo) {
+    alert('你好,我是来自FeHelper的工具Demo:hello world!你可以打开控制台看Demo的输出!');
+    console.log('你好,我是来自FeHelper的工具Demo:', tabInfo);
+};

+ 11 - 0
apps/devtools/hello-world/fh-config.js

@@ -0,0 +1,11 @@
+config = {
+    "hello-world": {
+        "name": "Hello world!",
+        "tips": "这是一个FH自定义工具的入门示例!一切都从Hello world开始,大家可体验,或下载后学习!",
+        "icon": "웃",
+        "noPage": true,
+        "contentScriptJs": true,
+        "contentScriptCss": true,
+        "minVersion": "2020.02.0718"
+    }
+}

+ 4 - 0
apps/devtools/hello-world/index.css

@@ -0,0 +1,4 @@
+body {
+	text-align: center;
+	font-size: 16px;
+}

+ 14 - 0
apps/devtools/hello-world/index.html

@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<html>
+<head>
+	<title>demo</title>
+	<link rel="stylesheet" type="text/css" href="index.css">
+</head>
+<body>
+	hello world!
+	<br>
+	修改fh-config.js中的noPage参数为false时,点击工具菜单,就会在新窗口打开这个工具了!去试试吧!
+	<script type="text/javascript" src="index.js"></script>
+	<script type="text/javascript" src="content-script.js"></script>
+</body>
+</html>

+ 1 - 0
apps/devtools/hello-world/index.js

@@ -0,0 +1 @@
+alert('hello world');

+ 280 - 0
apps/devtools/index.css

@@ -0,0 +1,280 @@
+@import url("../static/css/bootstrap.min.css");
+@import url("../static/vendor/codemirror/codemirror.css");
+
+ul {
+    margin: 0;
+    padding: 0;
+}
+
+ul.box-tools li {
+    list-style: none;
+    float: left;
+    font-size: 14px;
+    margin: 10px 20px 10px 0;
+    border: 1px solid #ccc;
+    padding: 20px 15px 10px;
+    border-radius: 10px;
+    background: #fefefe;
+    width: 400px;
+    box-shadow: 2px 2px #f2f2f2;
+    height: 154px;
+    user-select: none;
+}
+
+ul.box-tools li i.x-icon {
+    float: left;
+    font-size: 30pt;
+    font-style: normal;
+    display: block;
+    width: 60px;
+    height: 60px;
+    text-align: center;
+    color: red;
+    border: 1px solid #ddd;
+    background: #fff;
+    border-radius: 4px;
+    box-shadow: 2px 2px #eee;
+}
+
+ul.box-tools li .x-infos {
+    margin-left: 80px;
+    height: 90px;
+    overflow: hidden;
+}
+
+ul.box-tools li .x-tips {
+    margin-top: 10px;
+    color: #888;
+    font-size: 12px;
+}
+
+ul.box-tools li .x-btns {
+    border-top: 1px solid #eee;
+    padding-top: 10px;
+}
+
+.given-icons {
+    width: 70%;
+    margin: 0 auto;
+    box-sizing: border-box;
+    font-size: 18px;
+    word-break: break-all;
+    background: #fff;
+    border: 1px solid #aaa;
+    border-radius: 10px;
+    padding: 10px 10px 10px 20px;
+    position: fixed;
+    top: 120px;
+    left: 15%;
+    box-shadow: 4px 4px #eee;
+    z-index: 100;
+}
+
+.given-icons h3 {
+    color: #48b;
+    margin:0;
+}
+
+.given-icons .the-icons i {
+    font-style: normal;
+    padding: 5px;
+    cursor: default;
+}
+
+.given-icons .the-icons i:hover {
+    color: #f00;
+    background: #ff0;
+}
+
+.given-icons .x-close {
+    position: absolute;
+    top: 10px;
+    right: 10px;
+    color: #f00;
+    cursor: pointer;
+    font-size: 14px;
+}
+
+.given-icons .x-close:hover {
+    color: #b00;
+}
+
+ul.box-tools li.x-addtool {
+    width: 154px;
+    border-radius: 78px;
+    text-align: center;
+    font-size: 120pt;
+    line-height: 120px;
+    color: #eee;
+    text-shadow: 5px 4px #fff;
+    background: #fcfcfc;
+    border-color: #eee;
+}
+
+ul.box-tools li.x-addtool:hover {
+    color: #ccc;
+    cursor: pointer;
+    border-color: #ccc;
+    background: #f5f5f5;
+}
+
+.x-mask {
+    position: absolute;
+    top: 0;
+    left: 0;
+    bottom: 0;
+    right: 0;
+    background: #000;
+    opacity: 0.4;
+}
+
+.fh-editor .x-editor-container {
+    width: 90%;
+    margin: 0 auto;
+    position: absolute;
+    left: 5%;
+    top: 40px;
+    height: calc(100% - 60px);
+    border: 1px solid #aaa;
+    border-radius: 4px;
+    box-shadow: 3px 3px #aaa;
+}
+
+.fh-editor .x-editor-container .x-topbar {
+    background: #f5f5f5;
+    height: 30px;
+    border-radius: 4px 4px 0 0;
+    line-height: 30px;
+    padding: 0 10px 0 10px;
+}
+
+.fh-editor .x-editor-container .x-leftside {
+    background: #fff;
+    width: 250px;
+    height: calc(100% - 30px);
+    float: left;
+    border-radius: 0 0 0 4px;
+    border-top: 1px solid #ddd;
+    overflow-x: hidden;
+    overflow-y: auto;
+}
+
+.fh-editor .x-editor-container .x-rightside {
+    position: absolute;
+    left: 250px;
+    right: 0;
+    top: 30px;
+    bottom: 0;
+}
+
+.fh-editor .x-editor-container .CodeMirror {
+    width: 100%;
+    height: 100%;
+    border-radius: 0 0 4px 0;
+}
+
+.fh-editor .x-editor-container .x-topbar .x-icon {
+    color: #f00;
+    font-style: normal;
+}
+
+.fh-editor .x-editor-container .x-topbar .x-close {
+    color: #f00;
+    font-size: 14px;
+    cursor: pointer;
+}
+
+.fh-editor .x-editor-container .x-topbar .x-close:hover {
+    text-decoration: underline;
+}
+
+.fh-editor .x-editor-container .x-topbar .x-save {
+    color: #48b;
+}
+
+.fh-editor .x-editor-container .x-topbar .x-save:hover {
+    color: #68b;
+}
+
+ul.box-files li {
+    list-style: none;
+    user-select: none;
+    padding: 0 2px 0 10px;
+    border-bottom: 1px solid #f2f2f2;
+    height: 26px;
+    line-height: 26px;
+    font-size: 14px;
+    cursor: default;
+    position: relative;
+}
+ul.box-files li.x-selected:after {
+    content: '\2714';
+    position: absolute;
+    right: 10px;
+}
+ul.box-files li:hover {
+    background: #eee;
+}
+ul.box-files li .x-delete {
+    position: absolute;
+    right: 30px;
+    top:0;
+    font-size: 12px;
+    color: #48b;
+    text-decoration: underline;
+    display: none;
+    cursor: pointer;
+}
+ul.box-files li .x-delete:hover {
+    color: #f00;
+}
+ul.box-files li:hover .x-delete {
+    display: inline-block;
+}
+ul.box-files li .x-folder {
+    color: #ddd;
+    font-style: italic;
+}
+ul.box-files li .x-file {
+    word-break: keep-all;
+    white-space: nowrap;
+}
+
+ul.box-files li.x-tools {
+    border-bottom: none;
+    padding: 10px 2px 0 10px
+}
+ul.box-files li.x-tools:hover {
+    background: transparent;
+}
+
+.x-new-tool-form form {
+    width: 600px;
+    position: absolute;
+    top: 130px;
+    left: calc(50% - 300px);
+    background: #fff;
+    padding: 10px;
+    border: 1px solid #aaa;
+    border-radius: 6px;
+}
+
+#fehelper_alertmsg {
+    position: fixed;
+    top: 0;
+    left: 0;
+    right: 0;
+    z-index: 1000000;
+    background: #000;
+    display: inline-block;
+    color: #fff;
+    text-align: center;
+    padding: 10px 10px;
+    margin: 0 auto;
+    font-size: 14px;
+    border-bottom: 1px solid #aaa;
+}
+
+[v-cloak] {
+    display: none;
+}

+ 163 - 0
apps/devtools/index.html

@@ -0,0 +1,163 @@
+<!DOCTYPE HTML>
+<html lang="zh-CN">
+    <head>
+        <title>FH开发者工具</title>
+        <meta charset="UTF-8">
+        <link rel="shortcut icon" href="../static/img/favicon.ico">
+        <link rel="stylesheet" href="index.css" />
+        <script type="text/javascript" src="../static/vendor/evalCore.min.js"></script>
+        <script type="text/javascript" src="../static/vendor/vue/vue.js"></script>
+    </head>
+    <body>
+        <div class="wrapper wp-json" id="pageContainer">
+            <div class="panel panel-default" style="margin-bottom: 0px;">
+                <div class="panel-heading">
+                    <h3 class="panel-title">
+                        <a href="https://www.baidufe.com/fehelper/index/index.html" target="_blank" class="x-a-high">
+                            <img src="../static/img/fe-16.png" alt="fehelper"/> FeHelper</a>:开发者工具
+                    </h3>
+                </div>
+            </div>
+
+            <div class="panel-body">
+                <button class="btn btn-primary" @click="addNewTool">开始我的第一个FH工具</button>
+                <button class="btn btn-default ui-ml-10" @click="startByDemo()">来个HelloWorld试试</button>
+                <button class="btn btn-default ui-ml-10" @click="loadTool(false)">载入本地工具包(*.zip)</button>
+                <button class="btn btn-default ui-ml-10" @click="addNewTool('url')">从远程服务载入工具</button>
+                <button class="btn btn-default ui-ml-10" @click="givenIcons()">获取现成的图标</button>
+                <button class="btn btn-default ui-ml-10" @click="fhDeveloperDoc">FH工具开发文档</button>
+
+                <hr>
+                <ul class="box-tools">
+                    <li v-for="tool in Object.keys(myTools)" v-cloak>
+                        <i class="x-icon">{{myTools[tool].icon}}</i>
+                        <div class="x-infos">
+                            <b>{{myTools[tool].name}}</b>
+                            <div class="x-tips">{{myTools[tool].tips}}</div>
+                        </div>
+
+                        <div class="x-btns">
+                            <button class="btn btn-xs btn-success" @click="upgrade(tool)">{{tool===demo.name ? '在线' : 'zip包'}}更新</button>
+                            <button class="btn btn-xs btn-success ui-ml-10" @click="upgrade(tool,true)" v-if="myTools[tool].updateUrl">URL更新</button>
+                            <button class="btn btn-xs btn-primary ui-ml-10" @click="downloadTool(tool)">下载zip包</button>
+                            <button class="btn btn-xs btn-primary ui-ml-10" @click="toggleEditor(true,tool)">编辑</button>
+                            <button class="btn btn-xs btn-danger ui-ml-10"  @click="delToolConfigs(tool)">删除</button>
+                            <button class="btn btn-xs ui-fl-r" :class="myTools[tool]._enable ? 'btn-success' : 'btn-warning'" @click="toggleToolEnableStatus(tool)">{{myTools[tool]._enable ? '启用中' : '已停用'}}</button>
+                        </div>
+                    </li>
+
+                    <li class="x-addtool" title="创建工具" @click="addNewTool('local')">+</li>
+                </ul>
+            </div>
+
+            <div class="fh-editor" v-cloak v-show="showEditorFlag">
+                <div class="x-mask"></div>
+                <div class="x-editor-container">
+                    <div class="x-topbar">
+                        <i class="x-icon">{{model.icon}}</i>
+                        <b class="x-title">{{model.name + ' : ' + model.editingFile}}</b>
+                        <span class="x-close ui-fl-r" @click="toggleEditor(false)">关闭</span>
+                    </div>
+                    <div class="x-leftside">
+                        <ul class="box-files">
+                            <li v-for="file in model.files" @click="editFile(model.tool,file)" :class="model.editingFile === file ? 'x-selected' : ''">
+                                <span class="x-file">{{file.split('/').pop()}} <i class="x-folder" v-if="file.split('/').length>1">({{file.substr(0,file.lastIndexOf('/')).split('/').splice(-2,2).join('/')}})</i></span>
+                                <span class="x-delete" @click="deleteFile(model.tool,file,$event)" v-if="!['index.html','fh-config.js'].includes(file)">删除</span>
+                            </li>
+
+                            <li class="x-tools">
+                                <button class="btn btn-xs btn-primary" @click="createFile(model.tool)">创建文件</button>
+                                <button class="btn btn-xs btn-success ui-ml-10" @click="importFile(model.tool)">导入文件</button>
+                            </li>
+                        </ul>
+                    </div>
+                    <div class="x-rightside">
+                        <textarea class="form-control" id="txtEditor" ref="txtEditor" placeholder="FH开发者工具-编辑器"></textarea>
+                    </div>
+                </div>
+            </div>
+
+            <div class="given-icons" v-cloak v-if="showGivenIcons">
+                <h3>FeHelper为您提供的字符图标</h3>
+                <span class="ui-fl-r x-close" @click="givenIcons(true)">关闭</span>
+                <hr>
+                <div class="the-icons">
+                    <i class="x-icon" v-for="icon in givenIconList" @click="selectIcon(icon)">{{icon}}</i>
+                </div>
+            </div>
+
+            <div class="x-new-tool-form" v-cloak v-if="showNewToolForm">
+                <div class="x-mask"></div>
+                <form @submit.prevent="newToolAction($event)" action="#" class="form-horizontal">
+                    <div class="form-group">
+                        <label for="toolId" class="col-sm-2 control-label">工具ID</label>
+                        <div class="col-sm-10">
+                            <input type="text" class="form-control" id="toolId" ref="toolId" placeholder="由数字、字母、减号组成,如:hello-world" maxlength="30" required="required" pattern="[a-z\-0-9]+">
+                        </div>
+                    </div>
+                    <div class="form-group">
+                        <label for="toolName" class="col-sm-2 control-label">工具名称</label>
+                        <div class="col-sm-10">
+                            <input type="text" class="form-control" id="toolName" ref="toolName" placeholder="中英文均可,但不要超过6个中文字符的宽度" maxlength="20" required="required" pattern="[^\s]+">
+                        </div>
+                    </div>
+                    <div class="form-group">
+                        <label for="toolIcon" class="col-sm-2 control-label">工具Icon</label>
+                        <div class="col-sm-10">
+                            <input type="text" class="form-control" id="toolIcon" ref="toolIcon" placeholder="工具字符图标" maxlength="1" required="required" pattern="[^\s]+" style="width:300px;display: inline-block">
+                            <span class="btn btn-sm btn-primary" @click="givenIcons()">获取现成图标</span>
+                        </div>
+                    </div>
+                    <div class="form-group" v-if="updateUrlMode">
+                        <label for="updateUrl" class="col-sm-2 control-label">Web地址</label>
+                        <div class="col-sm-10">
+                            <input type="text" class="form-control" id="updateUrl" ref="updateUrl" placeholder="该独立工具所在的Web服务URL" required="required" pattern="http(s)?:\/\/[\w\.\-]+\S+">
+                        </div>
+                    </div>
+                    <div class="form-group">
+                        <div class="col-sm-offset-2 col-sm-10">
+                            <div class="checkbox">
+                                <label>
+                                    <input type="checkbox" id="hasContentScript" ref="hasContentScript"> 需要进行页面脚本注入
+                                </label>
+                            </div>
+                            <div class="checkbox">
+                                <label>
+                                    <input type="checkbox" id="noPage" ref="noPage"> 此工具不需要独立界面
+                                </label>
+                            </div>
+                        </div>
+                    </div>
+                    <div class="form-group">
+                        <div class="col-sm-offset-2 col-sm-10">
+                            <button type="submit" class="btn btn-success">确认创建</button>
+                            <button class="btn btn-default ui-ml-20" @click="showNewToolForm=false">取消</button>
+                        </div>
+                    </div>
+                </form>
+            </div>
+
+        </div>
+        <script src="../static/vendor/jszip/jszip.js"></script>
+        <script src="../static/vendor/jszip/zip.js"></script>
+        <script src="../static/vendor/jszip/zip-ext.js"></script>
+        <script src="../static/vendor/jszip/deflate.js"></script>
+        <script src="../static/vendor/jszip/inflate.js"></script>
+        <script src="../static/vendor/jszip/mime-types.js"></script>
+        <script src="../static/vendor/jszip/zip-fs.js"></script>
+        <script src="../static/vendor/jszip/z-worker.js"></script>
+
+        <script src="../static/vendor/codemirror/codemirror.js"></script>
+        <script src="../static/vendor/codemirror/javascript.js"></script>
+        <script src="../static/vendor/codemirror/htmlmixed.js"></script>
+        <script src="../static/vendor/codemirror/xml.js"></script>
+        <script src="../static/vendor/codemirror/css.js"></script>
+        <script src="../static/vendor/codemirror/active-line.js"></script>
+        <script src="../static/vendor/codemirror/matchbrackets.js"></script>
+        <script src="../static/vendor/codemirror/placeholder.js"></script>
+        <script src="../static/vendor/codemirror/formatting.js"></script>
+
+        <script src="./file-tpl.js"></script>
+        <script type="module" src="./index.js"></script>
+    </body>
+</html>

+ 782 - 0
apps/devtools/index.js

@@ -0,0 +1,782 @@
+import Awesome from '../background/awesome.js';
+
+const DEV_TOOLS_MY_TOOLS = 'DEV-TOOLS:MY-TOOLS';
+const TOOL_NAME_TPL = 'DYNAMIC_TOOL:#TOOL-NAME#';
+const TOOL_CONTENT_SCRIPT_TPL = 'DYNAMIC_TOOL:CS:#TOOL-NAME#';
+const TOOL_CONTENT_SCRIPT_CSS_TPL = 'DYNAMIC_TOOL:CS:CSS:#TOOL-NAME#';
+let editor = null;
+
+new Vue({
+    el: '#pageContainer',
+    data: {
+        showGivenIcons: false,
+        myTools: {},
+        showEditorFlag: false,
+        showNewToolForm: false,
+        updateUrlMode: false,
+        givenIconList: [],
+        model: {},
+        demo: {
+            name: 'hello-world',
+            files: ['fh-config.js', 'index.html', 'index.js', 'index.css', 'content-script.js','content-script.css']
+        }
+    },
+    mounted: function () {
+
+        this.getToolConfigs();
+    },
+
+    methods: {
+
+        startByDemo() {
+            this.loadDemo().then(() => {
+                setTimeout(() => this.getToolConfigs(),300);
+            });
+        },
+        toggleEditor(show, toolName) {
+            this.showEditorFlag = show;
+            if (show && toolName) {
+
+                let toolObj = this.myTools[toolName] || {};
+                this.model = {
+                    tool: toolName,
+                    name: toolObj.name
+                };
+
+                this.getToolFilesFromLocal(toolName).then(files => {
+                    this.model.files = files;
+
+                    if (!editor) {
+                        editor = CodeMirror.fromTextArea(this.$refs.txtEditor, {
+                            mode: 'javascript',
+                            lineNumbers: true,
+                            matchBrackets: true,
+                            styleActiveLine: true,
+                            lineWrapping: true
+                        });
+                        editor.on('change', (editor, changes) => {
+                            let result = this.saveContentToLocal(this.model.tool, this.model.editingFile, editor.getValue());
+                            if (this.model.editingFile === 'fh-config.js' && result) {
+                                result.contentScriptJs && !this.model.files.includes('content-script.js') && this.model.files.push('content-script.js');
+                                result.contentScriptCss && !this.model.files.includes('content-script.css') && this.model.files.push('content-script.css');
+                                this.$forceUpdate();
+                            }
+                        });
+                        editor.on('keydown', (editor, event) => {
+                            if (event.metaKey || event.ctrlKey) {
+                                if (event.code === 'KeyS') {
+                                    this.toast('当前代码是自动保存的,无需Ctrl+S手动保存!');
+                                    event.preventDefault();
+                                    event.stopPropagation();
+                                    return false;
+                                }
+                            }
+                        });
+                    }
+                    this.$nextTick(() => this.editFile(toolName, files[0]));
+                });
+            }
+        },
+        editFile(toolName, fileName) {
+
+            let editorMode = {
+                css: 'text/css',
+                js: {name: 'javascript', json: true},
+                html: 'htmlmixed'
+            };
+            let mode = editorMode[/\.(js|css|html)$/.exec(fileName)[1]];
+            editor.setOption('mode', mode);
+            this.model.editingFile = fileName;
+
+            this.getContentFromLocal(toolName, fileName).then(content => {
+                editor.setValue(content);
+                editor.focus();
+                this.$forceUpdate();
+            });
+        },
+        importFile(toolName) {
+            let fileInput = document.createElement('input');
+            fileInput.type = 'file';
+            fileInput.multiple = 'multiple';
+            fileInput.accept = 'text/javascript,text/css,text/html';
+            fileInput.style.cssText = 'position:absolute;top:-100px;left:-100px';
+            fileInput.addEventListener('change', (evt) => {
+                Array.prototype.slice.call(fileInput.files).forEach(file => {
+                    let reader = new FileReader();
+                    reader.onload = (evt) => {
+                        if (this.model.files.includes(file.name)) {
+                            if (!confirm(`文件 ${file.name} 已经存在,是否需要覆盖?`)) {
+                                return false;
+                            }
+                        } else {
+                            this.model.files.push(file.name);
+                        }
+                        this.saveContentToLocal(toolName, file.name, evt.target.result);
+                        this.editFile(toolName, file.name);
+                    };
+                    reader.readAsText(file);
+                })
+            }, false);
+
+            document.body.appendChild(fileInput);
+            fileInput.click();
+            window.setTimeout(() => fileInput.remove(), 3000);
+        },
+        createFile(toolName) {
+            let fileName = prompt('请输入你要创建的文件名!注意,只能是 *.html 、*.js 、*.css 类型的文件!').trim();
+            let result = /^[\w\-_\.]+\.(html|js|css)$/.exec(fileName);
+            if (!result) {
+                return alert('文件格式不正确!创建文件失败!');
+            }
+            if (this.model.files.includes(fileName)) {
+                return alert(`文件 ${fileName} 已经存在!`);
+            }
+            this.model.files.push(fileName);
+            this.saveContentToLocal(toolName, fileName, '');
+            this.editFile(toolName, fileName);
+        },
+        deleteFile(toolName, fileName, event) {
+            event.preventDefault();
+            event.stopPropagation();
+
+            if (['fh-config.js', 'index.html'].includes(fileName)) {
+                return alert(`文件 ${fileName} 不允许被删除!`);
+            }
+
+            if (confirm(`确定要删除文件 ${fileName} 吗?此操作不可撤销,请三思!`)) {
+                this.model.files.splice(this.model.files.indexOf(fileName), 1);
+                this.$forceUpdate();
+
+                let key = '';
+                switch (fileName) {
+                    case 'index.html':
+                        key = TOOL_NAME_TPL.replace('#TOOL-NAME#', toolName);
+                        break;
+                    case 'content-script.js':
+                        key = TOOL_CONTENT_SCRIPT_TPL.replace('#TOOL-NAME#', toolName);
+                        break;
+                    case 'content-script.css':
+                        key = TOOL_CONTENT_SCRIPT_CSS_TPL.replace('#TOOL-NAME#', toolName);
+                        break;
+                    default:
+                        key = fileName.startsWith(`../${toolName}/`) ? fileName : `../${toolName}/${fileName}`;
+                }
+
+                Awesome.StorageMgr.remove(key);
+            }
+        },
+
+        addNewTool(localOrUrl) {
+            this.showNewToolForm = true;
+            this.updateUrlMode = localOrUrl === 'url';
+
+            if (this.updateUrlMode) {
+                this.toast('请务必载入自己的Web工具服务!PS:请尽量别把它当成网站原内容的爬取工具,因为别人的网站你爬取过来也不一定完全能运行!');
+            }
+        },
+        newToolAction(event) {
+            let toolId = this.$refs.toolId.value;
+            let toolName = this.$refs.toolName.value;
+            let toolIcon = this.$refs.toolIcon.value;
+            let contentScript = this.$refs.hasContentScript.checked;
+            let noPage = this.$refs.noPage.checked;
+            let updateUrl = '';
+            if (this.updateUrlMode) {
+                updateUrl = this.$refs.updateUrl.value;
+                if (updateUrl.indexOf('baidufe.com') > -1 || updateUrl.indexOf('fehelper.com') > -1) {
+                    return this.toast('如果你是要安装FeHelper官网的工具,请到插件配置页直接安装!');
+                }
+            }
+
+            if (this.myTools[toolId]) {
+                this.toast(`ID为 ${toolId} 的工具在本地已存在,请重新命名!`);
+                event.preventDefault();
+                return false;
+            }
+
+            // 关闭Form表单
+            this.showNewToolForm = false;
+
+            // 创建文件列表
+            let files = ['fh-config.js'];
+            (contentScript || noPage) && files.push('content-script.js');
+
+            // 本地创建的模式,需要用模板来初始化
+            if (!this.updateUrlMode) {
+                files.push('index.html');
+                if (!noPage) {
+                    files.push('index.css');
+                    files.push('index.js');
+                }
+            }
+
+            // 初始化的文件内容,需要进行存储
+            files.forEach(file => {
+                let content = FileTpl[file].replace(/#toolName#/gm, toolId)
+                    .replace(/#toolFullName#/gm, toolName)
+                    .replace(/#toolIcon#/gm, toolIcon)
+                    .replace(/#updateUrl#/gm, updateUrl)
+                    .replace(/#contentScript#/gm, !!contentScript || !!noPage)
+                    .replace(/#noPage#/gm, !!noPage)
+                    .replace(/#toolNameLower#/gm, toolId.replace(/[\-_]/g, ''));
+                if (noPage && file === 'content-script.js') {
+                    content += '\n\n' + FileTpl['noPage.js'].replace(/#toolName#/gm, toolId)
+                        .replace(/#toolNameLower#/gm, toolId.replace(/[\-_]/g, ''));
+                }
+
+                this.saveContentToLocal(toolId, file, content);
+            });
+
+            if (this.updateUrlMode) {
+                // 远程下载并安装工具
+                this.loadRemoteTool(toolId, updateUrl, this.toast).then(progress => {
+                    this.toast(progress);
+                    setTimeout(() => this.toggleEditor(true, toolId),300);
+                    this.toast('工具创建成功!现在可以进行实时编辑了!');
+                });
+            } else {
+                setTimeout(() => this.toggleEditor(true, toolId),300);
+                this.toast('工具创建成功!现在可以进行实时编辑了!');
+            }
+
+            event.preventDefault();
+            return false;
+        },
+
+        loadRemoteTool(toolName, updateUrl, fnProgress) {
+            return new Promise((resolve, reject) => {
+                fnProgress && fnProgress('开始下载...');
+                fetch(updateUrl).then(resp => resp.text()).then(html => {
+                    let result = this.htmlTplEncode(toolName, html, updateUrl);
+                    html = result.html;
+                    let files = result.jsCss;
+
+                    // 获取所有网络文件的总个数,以便于计算进度
+                    let total = evalCore.getEvalInstance(window)(Object.values(files).map(a => a.length).join('+')) + 1;
+                    let loaded = 1;
+                    fnProgress && fnProgress(Math.floor(100 * loaded / total) + '%');
+
+                    (async () => {
+
+                        let toolObj = this.myTools[toolName];
+
+                        for (let t in files) {
+                            for (let f = 0; f < files[t].length; f++) {
+                                let fs = files[t][f];
+
+                                // script-block内识别出来的代码,直接保存
+                                if (t === 'js' && fs[0].indexOf('fh-script-block.js') > -1) {
+                                    this.saveContentToLocal(toolName, fs[0], fs[2]);
+                                    continue;
+                                }
+
+                                await fetch(fs[2]).then(resp => resp.text()).then(txt => {
+                                    this.saveContentToLocal(toolName, fs[0], txt);
+
+                                    // 保存content-script / background-script
+                                    if (toolObj.contentScriptJs && fs[0].indexOf(toolName + '/content-script.js') !== -1) {
+                                        this.saveContentToLocal(toolName, 'content-script.js', txt);
+
+                                        // 存储content-script.css文件内容
+                                        if (toolObj.contentScriptCss) {
+                                            fetch(fs[2].replace('content-script.js', 'content-script.css')).then(resp => resp.text()).then(css => {
+                                                this.saveContentToLocal(toolName, 'content-script.css', css);
+                                            });
+                                        }
+                                    }
+
+                                    fnProgress && fnProgress(Math.floor(100 * ++loaded / total) + '%');
+                                });
+                            }
+                        }
+
+                        // 全部下载完成!
+                        resolve && resolve('100%');
+                    })();
+
+                    this.saveContentToLocal(toolName, 'index.html', html);
+                }).catch(e => {
+                    this.delToolConfigs(toolName);
+                    fnProgress && fnProgress(`糟糕,下载出错,工具远程安装失败!${e.toString()}`);
+                });
+            });
+        },
+
+
+        loadDemo() {
+            let demoName = this.demo.name;
+            let files = this.demo.files;
+            let site = '.';
+            if (window.chrome && chrome.runtime && chrome.runtime.getURL) {
+                site = chrome.runtime.getURL('devtools');
+            }
+            let arrPromise = files.map(file => fetch(`${site}/${demoName}/${file}`).then(resp => resp.text()));
+            return Promise.all(arrPromise).then(contents => {
+                // fh-config.js
+                let json = evalCore.getEvalInstance(window)(contents[0]);
+                this.addToolConfigs(json);
+
+                // index.html
+                let result = this.htmlTplEncode(demoName, contents[1]);
+                this.saveContentToLocal(demoName, files[1], result.html, true);
+
+                // 其他文件
+                for (let i = 2; i < contents.length; i++) {
+                    this.saveContentToLocal(demoName, files[i], contents[i]);
+                }
+
+                this.toast('你的Hello World已安装成功!');
+            });
+        },
+
+        downloadTool(tool) {
+            let toolName = tool || this.demo.name;
+
+            this.getToolFilesFromLocal(toolName).then(files => {
+                let arrPromise = files.map(file => this.getContentFromLocal(toolName, file));
+
+                Promise.all(arrPromise).then(contents => {
+                    let zipper = new JSZip();
+                    let zipPkg = zipper.folder(toolName);
+                    files.forEach((file, index) => zipPkg.file(file, contents[index]));
+
+                    zipper.generateAsync({type: "blob"})
+                        .then(function (content) {
+                            let elA = document.createElement('a');
+                            elA.style.cssText = 'position:absolute;top:-1000px;left:-10000px;';
+                            elA.setAttribute('download', `${toolName}.zip`);
+                            elA.href = URL.createObjectURL(new Blob([content], {type: 'application/octet-stream'}));
+                            document.body.appendChild(elA);
+                            elA.click();
+                        });
+                });
+            });
+        },
+
+        upgrade(tool, urlMode) {
+            if (tool === this.demo.name) {
+                this.loadDemo();
+            } else if (urlMode) {
+                // 远程下载并安装工具
+                this.loadRemoteTool(tool, this.myTools[tool].updateUrl, this.toast).then(progress => {
+                    this.toast(progress);
+                    this.toast('工具更新完成!');
+                });
+            } else {
+                this.loadTool(true, tool);
+            }
+        },
+
+        loadTool(upgradeMode, upgradeToolName) {
+            let Model = (function () {
+                zip.useWebWorkers = false;
+
+                return {
+                    getEntries: function (file, onend) {
+                        zip.createReader(new zip.BlobReader(file), function (zipReader) {
+                            zipReader.getEntries(onend);
+                        }, function (e) {
+                            console.log(e);
+                        });
+                    },
+
+                    getEntryFile: function (entry, onend, onprogress) {
+                        entry.getData(new zip.TextWriter(), function (text) {
+                            onend(text);
+                        }, onprogress);
+                    }
+                };
+            })();
+
+            let fileInput = document.createElement('input');
+            fileInput.type = 'file';
+            fileInput.accept = 'application/zip';
+            fileInput.style.cssText = 'position:absolute;top:-100px;left:-100px';
+            fileInput.addEventListener('change', (evt) => {
+                let toolName = fileInput.files[0].name.replace('.zip', '');
+                if (upgradeMode && upgradeToolName !== toolName) {
+                    return this.toast(`请确保上传${upgradeToolName}.zip进行更新!`);
+                }
+                Model.getEntries(fileInput.files[0], (entries) => {
+                    entries = entries.filter(entry => !entry.directory && /\.(html|js|css)$/.test(entry.filename));
+                    let reg = /(fh-config\.js|index\.html|content-script\.(js|css))$/;
+                    let entPart1 = entries.filter(en => reg.test(en.filename));
+                    let entPart2 = entries.filter(en => !reg.test(en.filename));
+
+                    entPart1.forEach((entry) => {
+                        Model.getEntryFile(entry, (fileContent) => {
+                            let fileName = entry.filename.split('/').pop();
+                            try {
+                                if (fileName === `fh-config.js`) {
+                                    let json = JSON.parse(fileContent);
+                                    this.addToolConfigs(json);
+                                } else if (fileName === 'index.html') {
+                                    let result = this.htmlTplEncode(toolName, fileContent);
+                                    this.saveContentToLocal(toolName, fileName, result.html, true);
+
+                                    // 所有被引用的静态文件都在这里进行遍历
+                                    entPart2.forEach(jcEntry => {
+                                        Model.getEntryFile(jcEntry, jcContent => {
+                                            Object.keys(result.jsCss).forEach(tp => {
+                                                result.jsCss[tp].some(file => {
+                                                    if (file[0].indexOf(jcEntry.filename) > -1) {
+                                                        this.saveContentToLocal(toolName, file[0].replace(`../${toolName}/`, ''), jcContent);
+                                                        return true;
+                                                    }
+                                                });
+                                            });
+                                        });
+                                    });
+                                } else if (['content-script.js', 'content-script.css'].includes(fileName)) {
+                                    this.saveContentToLocal(toolName, fileName, fileContent);
+                                }
+                            } catch (err) {
+                                this.toast(`${fileName} 文件发生错误:${err.message}`);
+                            }
+                        });
+                    });
+                    this.toast('工具更新成功!');
+                });
+            }, false);
+
+            document.body.appendChild(fileInput);
+            fileInput.click();
+            window.setTimeout(() => fileInput.remove(), 3000);
+        },
+
+        getToolConfigs() {
+            return Awesome.StorageMgr.get(DEV_TOOLS_MY_TOOLS).then(data => {
+                this.myTools = JSON.parse(data || localStorage.getItem(DEV_TOOLS_MY_TOOLS) || '{}');
+                Object.keys(this.myTools).forEach(t => {
+                    if(this.myTools[t].menuConfig) {
+                        this.myTools.icon = this.myTools[t].menuConfig[0].icon;
+                        delete this.myTools[t].menuConfig;
+                    }
+                    if(this.myTools[t].contentScript) {
+                        this.myTools[t].contentScriptJs = this.myTools[t].contentScript;
+                        delete this.myTools[t].contentScript;
+                    }
+                    if(!this.myTools[t].icon) {
+                        this.myTools[t].icon = '◆';
+                    }
+                });
+            });
+        },
+
+        setToolConfigs() {
+            Awesome.StorageMgr.set(DEV_TOOLS_MY_TOOLS,JSON.stringify(this.myTools));
+        },
+
+        addToolConfigs(configs) {
+            this.getToolConfigs().then(() => {
+                Object.keys(configs).forEach(key => {
+                    let config = configs[key];
+                    this.myTools[key] = {
+                        _devTool: true,
+                        _enable: this.myTools[key] && this.myTools[key]._enable,
+                        name: config.name,
+                        tips: config.tips,
+                        icon: config.icon,
+                        noPage: !!config.noPage,
+                        contentScriptJs: !!config.contentScriptJs || !!config.contentScript,
+                        contentScriptCss: !!config.contentScriptCss,
+                        updateUrl: config.updateUrl || null
+                    }
+                });
+                this.setToolConfigs();
+            });
+        },
+
+        delToolConfigs(tools) {
+            // 先删除文件
+            [].concat(tools).forEach(tool => {
+                this.getToolFilesFromLocal(tool).then(files => {
+                    Awesome.StorageMgr.remove(files.map(file =>
+                        file.startsWith(`../${tool}`) ? file : `../${tool}/${file}` ));
+                });
+
+                // 删模板等
+                let removeItems = [
+                    TOOL_NAME_TPL.replace('#TOOL-NAME#', tool),
+                    TOOL_CONTENT_SCRIPT_TPL.replace('#TOOL-NAME#', tool),
+                    TOOL_CONTENT_SCRIPT_CSS_TPL.replace('#TOOL-NAME#', tool)
+                ];
+                Awesome.StorageMgr.remove(removeItems);
+            });
+
+            // 再删配置
+            [].concat(tools).forEach(tool => {
+                delete this.myTools[tool];
+            });
+
+            this.setToolConfigs();
+            this.$forceUpdate();
+
+            this.toast('工具删除成功!');
+        },
+
+        toggleToolEnableStatus(tool) {
+            this.myTools[tool]._enable = !this.myTools[tool]._enable;
+            this.setToolConfigs();
+            this.$forceUpdate();
+        },
+
+        getToolFilesFromLocal(toolName) {
+            return new Promise(resolve => {
+                let files = ['fh-config.js', 'index.html'];
+                let toolObj = this.myTools[toolName];
+                toolObj.contentScriptJs && files.push('content-script.js');
+                toolObj.contentScriptCss && files.push('content-script.css');
+
+                chrome.storage.local.get(null, allDatas => {
+                    let fs = Object.keys(allDatas).filter(key => String(key).startsWith(`../${toolName}/`));
+                    files = files.concat(fs);
+                    resolve(files.map(f => f.replace(`../${toolName}/`, '')));
+                });
+            });
+        },
+
+        saveContentToLocal(toolName, fileName, content, htmlDone) {
+
+            if (fileName === 'fh-config.js') {
+                try {
+                    let json = JSON.parse(content);
+                    this.addToolConfigs(json);
+                    this.$forceUpdate();
+                    return json[toolName];
+                } catch (e) {
+                    return null;
+                }
+            }
+
+            let key = '';
+            switch (fileName) {
+                case 'index.html':
+                    key = TOOL_NAME_TPL.replace('#TOOL-NAME#', toolName);
+                    if (!htmlDone) {
+                        let result = this.htmlTplEncode(toolName, content);
+                        content = result.html;
+                    }
+                    break;
+                case 'content-script.js':
+                    key = TOOL_CONTENT_SCRIPT_TPL.replace('#TOOL-NAME#', toolName);
+                    break;
+                case 'content-script.css':
+                    key = TOOL_CONTENT_SCRIPT_CSS_TPL.replace('#TOOL-NAME#', toolName);
+                    break;
+                default:
+                    key = fileName.startsWith(`../${toolName}/`) ? fileName : `../${toolName}/${fileName}`;
+            }
+
+            return Awesome.StorageMgr.set(key,content);
+        },
+
+        getContentFromLocal(toolName, fileName) {
+
+            return new Promise((resolve, reject) => {
+                if (fileName === 'fh-config.js') {
+                    let counter = 0;
+                    let config = {};
+                    config[toolName] = this.myTools[toolName];
+                    ['_devTool', '_enable'].forEach(k => delete config[toolName][k]);
+                    delete config[toolName].menuConfig;
+
+                    let jsonText = JSON.stringify(config,null,4);
+
+                    resolve(jsonText);
+                } else {
+                    let key = '';
+                    switch (fileName) {
+                        case 'index.html':
+                            key = TOOL_NAME_TPL.replace('#TOOL-NAME#', toolName);
+                            break;
+                        case 'content-script.js':
+                            key = TOOL_CONTENT_SCRIPT_TPL.replace('#TOOL-NAME#', toolName);
+                            break;
+                        case 'content-script.css':
+                            key = TOOL_CONTENT_SCRIPT_CSS_TPL.replace('#TOOL-NAME#', toolName);
+                            break;
+                        default:
+                            key = fileName.startsWith(`../${toolName}/`) ? fileName : `../${toolName}/${fileName}`;
+                    }
+
+                    // 获取到的数据需要做二次加工
+                    let _update = (content) => {
+                        content = content || '';
+                        if (fileName === 'index.html') {
+                            content = this.htmlTplDecode(toolName, content);
+                        }
+                        // 如果noPage为true,但content-script.js中还没有window.xxxNoPage定义的话,就自动加一个
+                        else if (fileName === 'content-script.js' && this.myTools[toolName].noPage) {
+                            if (content.indexOf(`window.${toolName.replace(/[\-_]/g, '')}NoPage`) === -1) {
+                                content += '\n\n' + FileTpl['noPage.js'].replace(/#toolName#/gm, toolName)
+                                    .replace(/#toolNameLower#/gm, toolName.replace(/[\-_]/g, ''));
+                                this.saveContentToLocal(toolName, fileName, content);
+                            }
+                        }
+
+                        return content
+                    };
+
+                    Awesome.StorageMgr.get(key).then(data => {
+                        resolve(_update(data));
+                    });
+                }
+            });
+        },
+
+        htmlTplEncode(toolName, html, updateUrl) {
+            let jsReg = /<script[^>]*src=['"]([^'"]+)['"][^>]*>\s*?<\/[^>]*script>/igm;
+            let csReg = /<link\s+[^>]*[^>]*href=['"]([^'"]+)['"][^>]*[^>]*>/igm;
+            let scriptBlockReg = /<script(?:[^>]+|(?!src=))*>([^<]+|<(?!\/script>))+<\/script>/gim;
+            let files = {};
+
+            [csReg, jsReg].forEach(reg => {
+                html = html.replace(reg, (tag, src) => {
+
+                    let tagName = /<script/.test(tag) ? 'js' : 'css';
+                    if (tagName === 'css' && !/stylesheet/i.test(tag)) {
+                        return tag;
+                    }
+
+                    // 这里必须保留带有md5戳的原地址,要不然会有cdn缓存,更新会失败
+                    let originSrc = src;
+
+                    // 这个src是携带了Query的,用于直接存储到html中
+                    let withQuerySrc = src;
+
+                    // src 去query处理,用于Storage存储,避免过多冗余key出现
+                    if (src.indexOf('?') !== -1) {
+                        let x = src.split('?');
+                        x.pop();
+                        src = x.join('');
+                    }
+
+                    if (!/^\./.test(src)) {
+                        src = `../${toolName}/${src}`;
+                        withQuerySrc = `../${toolName}/${withQuerySrc}`;
+                    }
+
+                    // 存储静态文件的内容
+                    let filePath = originSrc;
+                    if (!/^(http(s)?:)?\/\//.test(originSrc) && updateUrl) {
+                        filePath = new URL(originSrc, updateUrl).href;
+                    }
+
+                    files[tagName] = files[tagName] || [];
+                    files[tagName].push([src, withQuerySrc, filePath]);
+
+                    return '';
+                });
+            });
+
+            // 识别所有无src属性的script标签
+            let blockCodes = [];
+            html = html.replace(scriptBlockReg, (tag, codes) => {
+                codes = codes.trim();
+                codes.length && blockCodes.push(codes);
+                return '';
+            });
+            if (blockCodes.length) {
+                let blockName = `../${toolName}/fh-script-block.js`;
+                files.js = files.js || [];
+                files.js.push([blockName, blockName, blockCodes.join(';\n\n')]);
+            }
+
+            // 如果是updateURL模式,需要替换所有相对链接为绝对链接,包括:img、a
+            if (updateUrl) {
+                let relativePathRegexp = /^(http:\/\/|https:\/\/|\/\/)[^\s'"]+/igm;
+                let imgReg = /<img[^>]*src=['"]([^'"]+)['"][^>]*>(\s*?<\/[^>]*img>)?/igm;
+                let aReg = /<a[^>]*href=['"]([^'"]+)['"][^>]*>(\s*?<\/[^>]*a>)?/igm;
+
+                html = html.replace(imgReg, (tag, link) => {
+                    if (!relativePathRegexp.test(link)) {
+                        tag = tag.replace(/src=['"]([^'"]+)['"]/igm, () => ` src="${new URL(link, updateUrl).href}"`);
+                    }
+                    return tag;
+                }).replace(aReg, (tag, link) => {
+                    if (!relativePathRegexp.test(link)) {
+                        tag = tag.replace(/href=['"]([^'"]+)['"]/igm, () => ` href="${new URL(link, updateUrl).href}"`);
+                    }
+                    return tag;
+                });
+            }
+
+            html = html.replace('static/img/favicon.ico', 'static/img/fe-16.png')  // 替换favicon
+                .replace(/<\/body>/, () => { // 将静态文件添加到页面最底部
+                    return Object.keys(files).map(t => {
+                        return `<dynamic data-type="${t}" data-source="${files[t].map(f => f[1]).join(',')}"></dynamic>`;
+                    }).join('') + '</body>';
+                });
+            return {
+                html: html,
+                jsCss: files
+            };
+        },
+
+        htmlTplDecode(toolName, html) {
+            let reg = /<dynamic\s+data\-type="(js|css)"\s+data\-source=['"]([^'"]+)['"][^>]*>\s*?<\/[^>]*dynamic>/igm;
+            return html.replace(reg, (frag, tag, list) => {
+                list = list.split(',');
+                if (tag === 'js') {
+                    return list.map(src => `<script src="${src.replace(`../${toolName}/`, '')}"></script>`).join('');
+                } else {
+                    return list.map(href => `<link rel="stylesheet" type="text/css" href="${href.replace(`../${toolName}/`, '')}" />`).join('');
+                }
+            });
+        },
+
+        givenIcons(forceClose) {
+            if (!this.givenIconList.length) {
+                this.givenIconList = FileTpl['given-icons'].replace(/\s/gm, '').split('');
+            }
+
+            if (forceClose) {
+                this.showGivenIcons = false;
+            } else {
+                this.showGivenIcons = !this.showGivenIcons;
+            }
+            this.$forceUpdate();
+        },
+        selectIcon(icon) {
+            if (this.showNewToolForm) {
+                this.$refs.toolIcon.value = icon;
+                this.givenIcons(true);
+            } else {
+                this.copyToClipboard(icon);
+                this.toast(`图标 ${icon} 复制成功,随处粘贴可用!`);
+            }
+        },
+
+        toast(content) {
+            window.clearTimeout(window.feHelperAlertMsgTid);
+            let elAlertMsg = document.querySelector("#fehelper_alertmsg");
+            if (!elAlertMsg) {
+                let elWrapper = document.createElement('div');
+                elWrapper.innerHTML = '<div id="fehelper_alertmsg">' + content + '</div>';
+                elAlertMsg = elWrapper.childNodes[0];
+                document.body.appendChild(elAlertMsg);
+            } else {
+                elAlertMsg.innerHTML = content;
+                elAlertMsg.style.display = 'block';
+            }
+
+            window.feHelperAlertMsgTid = window.setTimeout(function () {
+                elAlertMsg.style.display = 'none';
+            }, 3000);
+        },
+        copyToClipboard(text) {
+            let input = document.createElement('textarea');
+            input.style.position = 'fixed';
+            input.style.opacity = 0;
+            input.value = text;
+            document.body.appendChild(input);
+            input.select();
+            document.execCommand('Copy');
+            document.body.removeChild(input);
+        },
+        fhDeveloperDoc() {
+            window.open(`https://github.com/zxlie/FeHelper/blob/master/README_NEW.md#%E5%85%ADopen-api`);
+        }
+    }
+});

+ 14 - 0
apps/dynamic/index.html

@@ -0,0 +1,14 @@
+<!doctype html>
+<html lang="zh-CN">
+<head>
+    <meta charset="UTF-8">
+    <title>FeHelper</title>
+    <script type="text/javascript" src="../static/vendor/evalCore.min.js"></script>
+    <script type="text/javascript" src="../static/vendor/vue/vue.js"></script>
+    <script src="index.js" type="text/javascript"></script>
+    <script src="../static/js/dark-mode.js"></script>
+</head>
+<body>
+    <img src="../static/img/loading.gif" alt="loading" style="margin: 0 auto;width: 200px;position: absolute;left: calc(50% - 100px);top: calc(50% - 100px);">
+</body>
+</html>

+ 82 - 0
apps/dynamic/index.js

@@ -0,0 +1,82 @@
+/**
+ * FeHelper动态工具管理器,主要解决新版本向下兼容,为用户按需找回老版本的功能
+ * @author zhaoxianlie
+ */
+
+let DynamicTool = (() => {
+
+    // 工具渲染
+    let render = (toolName, Awesome) => {
+
+        Awesome.getToolTpl(toolName).then(html => {
+            if (!html) {
+                if (confirm('检测到当前指定的工具还未安装,这就去设置页面安装工具!')) {
+                    location.replace('../options/index.html');
+                } else {
+                    window.close();
+                }
+                return;
+            }
+            // 生成界面
+            document.write(html);
+
+            // 页面滤镜:关掉
+            DarkModeMgr.turnLightAuto();
+
+            // 更新静态文件
+            let list = document.querySelectorAll('dynamic[data-source]');
+            if (!list.length) return;
+            let allJs = [];
+            let allCss = [];
+            for (let i = 0; i < list.length; i++) {
+                let elm = list[i];
+                let fileType = elm.getAttribute('data-type');
+                let sources = elm.getAttribute('data-source') || '';
+                let files = sources.split(',').map(source => {
+                    // 去query处理,获得干净的local key
+                    if (source.indexOf('?') !== -1) {
+                        let x = source.split('?');
+                        x.pop();
+                        source = x.join('');
+                    }
+                    return source;
+                });
+
+                if (fileType === 'js') {
+                    allJs = allJs.concat(files);
+                } else {
+                    allCss = allCss.concat(files);
+                }
+            }
+
+            Promise.all([Awesome.StorageMgr.get(allCss), Awesome.StorageMgr.get(allJs)]).then(values => {
+                document.body.style.display = 'block';
+                allCss = allCss.map(f => values[0][f]).join(' ');
+                if (allCss.length) {
+                    let node = document.createElement('style');
+                    node.textContent = allCss;
+                    document.head.appendChild(node);
+                }
+                allJs = allJs.map(f => values[1][f]).join(';');
+                allJs.length && window.evalCore.getEvalInstance(window)(allJs);
+            });
+        });
+    };
+
+    // 页面初始化
+    let init = () => {
+        // 从Query中寻找需要动态渲染的工具名称
+        let toolName = new URL(location.href).searchParams.get('tool');
+        if (toolName) {
+            import('../background/awesome.js').then(dynamicModule => {
+                render(toolName, dynamicModule.default);
+            });
+        } else {
+            location.replace('../options/index.html');
+        }
+    };
+
+    return {init}
+})();
+
+DynamicTool.init();

+ 548 - 0
apps/en-decode/endecode-lib.js

@@ -0,0 +1,548 @@
+/**
+ * 本库提供几个常用方法:
+ * 1、enDecodeTools.uniEncode(text);    将中文进行Unicode编码并输出
+ * 2、enDecodeTools.base64Encode(text); 将文字进行base64编码并输出
+ * 3、enDecodeTools.base64Decode(text); 将经过base64编码的文字进行base64解码并输出
+ * 4、enDecodeTools.utf8Encode(text); 将文字进行utf-8编码并输出
+ * 5、enDecodeTools.utf8Decode(text); 将经过utf-8编码的文字进行utf-8解码并输出
+ */
+
+import Md5Utils from './md5.js';
+
+let EncodeUtils = (() => {
+    //base64编码字符集
+    let _base64EncodeChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
+    //base64解码字符集
+    let _base64DecodeChars = [
+        -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+        -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+        -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63,
+        52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1,
+        -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14,
+        15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1,
+        -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40,
+        41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1];
+
+
+    /**
+     * 此方法实现中文向Unicode的转码,与Jre中的"native2ascii"命令一样
+     * @param {String} text 需要进行转码的字符串
+     * @return {String} Unicode码
+     */
+    let _uniEncode = function (text) {
+
+        let res = [];
+        for (let i = 0; i < text.length; i++) {
+            res[i] = ("00" + text.charCodeAt(i).toString(16)).slice(-4);
+        }
+        return "\\u" + res.join("\\u");
+    };
+
+    /**
+     * 此方法用于将Unicode码解码为正常字符串
+     * @param {Object} text
+     */
+    let _uniDecode = function (text) {
+        text = text = text.replace(/(\\)?\\u/gi, "%u").replace('%u0025', '%25');
+        text = unescape(text.toString().replace(/%2B/g, "+"));
+
+        let matches = text.match(/(%u00([0-9A-F]{2}))/gi);
+        if (matches) {
+            for (let matchid = 0; matchid < matches.length; matchid++) {
+                let code = matches[matchid].substring(1, 3);
+                let x = Number("0x" + code);
+                if (x >= 128) {
+                    text = text.replace(matches[matchid], code);
+                }
+            }
+        }
+        text = unescape(text.toString().replace(/%2B/g, "+"));
+
+        return text;
+    };
+
+    /**
+     * 此方法用于将文字进行UTF-8编码
+     * @param {Object} str 源码
+     * @return {String} UTF-8码
+     */
+    let _utf8Encode = function (str) {
+        let out, i, len, c;
+        out = "";
+        len = str.length;
+        for (i = 0; i < len; i++) {
+            c = str.charCodeAt(i);
+            if ((c >= 0x0001) && (c <= 0x007F)) {
+                out += str.charAt(i);
+            } else if (c > 0x07FF) {
+                out += String.fromCharCode(0xE0 | ((c >> 12) & 0x0F));
+                out += String.fromCharCode(0x80 | ((c >> 6) & 0x3F));
+                out += String.fromCharCode(0x80 | ((c >> 0) & 0x3F));
+            } else {
+                out += String.fromCharCode(0xC0 | ((c >> 6) & 0x1F));
+                out += String.fromCharCode(0x80 | ((c >> 0) & 0x3F));
+            }
+        }
+        return out;
+    };
+
+    /**
+     * 此方法用于将文字进行UTF-8解码
+     * @param {Object} str
+     * @return {String} 原文字
+     */
+    let _utf8Decode = function (str) {
+        let out, i, len, c;
+        let char2, char3;
+        out = "";
+        len = str.length;
+        i = 0;
+        while (i < len) {
+            c = str.charCodeAt(i++);
+            switch (c >> 4) {
+                case 0:
+                case 1:
+                case 2:
+                case 3:
+                case 4:
+                case 5:
+                case 6:
+                case 7:
+                    // 0xxxxxxx
+                    out += str.charAt(i - 1);
+                    break;
+                case 12:
+                case 13:
+                    // 110x xxxx  10xx xxxx
+                    char2 = str.charCodeAt(i++);
+                    out += String.fromCharCode(((c & 0x1F) << 6) | (char2 & 0x3F));
+                    break;
+                case 14:
+                    // 1110 xxxx 10xx xxxx 10xx xxxx
+                    char2 = str.charCodeAt(i++);
+                    char3 = str.charCodeAt(i++);
+                    out += String.fromCharCode(((c & 0x0F) << 12) |
+                        ((char2 & 0x3F) << 6) |
+                        ((char3 & 0x3F) << 0));
+                    break;
+            }
+        }
+        return out;
+    };
+
+    /**
+     * 此方法用于将文字进行base64编码
+     * @param {Object} str 源码
+     * @return {String} base64码
+     */
+    let _base64Encode = function (str) {
+        let out, i, len;
+        let c1, c2, c3;
+        len = str.length;
+        i = 0;
+        out = "";
+        while (i < len) {
+            c1 = str.charCodeAt(i++) & 0xff;
+            if (i == len) {
+                out += _base64EncodeChars.charAt(c1 >> 2);
+                out += _base64EncodeChars.charAt((c1 & 0x3) << 4);
+                out += "==";
+                break;
+            }
+            c2 = str.charCodeAt(i++);
+            if (i == len) {
+                out += _base64EncodeChars.charAt(c1 >> 2);
+                out += _base64EncodeChars.charAt(((c1 & 0x3) << 4) | ((c2 & 0xF0) >> 4));
+                out += _base64EncodeChars.charAt((c2 & 0xF) << 2);
+                out += "=";
+                break;
+            }
+            c3 = str.charCodeAt(i++);
+            out += _base64EncodeChars.charAt(c1 >> 2);
+            out += _base64EncodeChars.charAt(((c1 & 0x3) << 4) | ((c2 & 0xF0) >> 4));
+            out += _base64EncodeChars.charAt(((c2 & 0xF) << 2) | ((c3 & 0xC0) >> 6));
+            out += _base64EncodeChars.charAt(c3 & 0x3F);
+        }
+        return out;
+    };
+
+    /**
+     * 此方法用于将文字进行base64解码
+     * @param {Object} str 源码
+     * @return {String} 源码
+     */
+    let _base64Decode = function (str) {
+        let c1, c2, c3, c4;
+        let i, len, out;
+        len = str.length;
+        i = 0;
+        out = "";
+        while (i < len) {
+            /* c1 */
+            do {
+                c1 = _base64DecodeChars[str.charCodeAt(i++) & 0xff];
+            }
+            while (i < len && c1 == -1);
+            if (c1 == -1)
+                break;
+            /* c2 */
+            do {
+                c2 = _base64DecodeChars[str.charCodeAt(i++) & 0xff];
+            }
+            while (i < len && c2 == -1);
+            if (c2 == -1)
+                break;
+            out += String.fromCharCode((c1 << 2) | ((c2 & 0x30) >> 4));
+            /* c3 */
+            do {
+                c3 = str.charCodeAt(i++) & 0xff;
+                if (c3 == 61)
+                    return out;
+                c3 = _base64DecodeChars[c3];
+            }
+            while (i < len && c3 == -1);
+            if (c3 == -1)
+                break;
+            out += String.fromCharCode(((c2 & 0XF) << 4) | ((c3 & 0x3C) >> 2));
+            /* c4 */
+            do {
+                c4 = str.charCodeAt(i++) & 0xff;
+                if (c4 == 61)
+                    return out;
+                c4 = _base64DecodeChars[c4];
+            }
+            while (i < len && c4 == -1);
+            if (c4 == -1)
+                break;
+            out += String.fromCharCode(((c3 & 0x03) << 6) | c4);
+        }
+
+        return out;
+    };
+
+    /**
+     * 中文,一般情况下Unicode是UTF-16实现,长度2位,而UTF-8编码是3位
+     * @param str
+     * @return {String}
+     */
+    let _utf16to8 = function (str) {
+        return str.replace(/\\x/g, '%');
+    };
+
+    let _utf8to16 = function (str) {
+        return str.replace(/%/g, '\\x');
+    };
+
+    /**
+     * md5加密
+     * @param str
+     */
+    let md5 = (str) => {
+        return Md5Utils.md5(str);
+    };
+
+    /**
+     * 字符串与Hex编码互转
+     * @param input
+     * @returns {string}
+     */
+    let hexTools = (function (input) {
+        let utf8encode = function (str, isGetBytes) {
+            let back = [];
+            let byteSize = 0;
+            for (let i = 0; i < str.length; i++) {
+                let code = str.charCodeAt(i);
+                if (0x00 <= code && code <= 0x7f) {
+                    byteSize += 1;
+                    back.push(code);
+                } else if (0x80 <= code && code <= 0x7ff) {
+                    byteSize += 2;
+                    back.push((192 | (31 & (code >> 6))));
+                    back.push((128 | (63 & code)))
+                } else if ((0x800 <= code && code <= 0xd7ff)
+                    || (0xe000 <= code && code <= 0xffff)) {
+                    byteSize += 3;
+                    back.push((224 | (15 & (code >> 12))));
+                    back.push((128 | (63 & (code >> 6))));
+                    back.push((128 | (63 & code)))
+                }
+            }
+            for (let i = 0; i < back.length; i++) {
+                back[i] &= 0xff;
+            }
+            if (isGetBytes) {
+                return back
+            }
+            if (byteSize <= 0xff) {
+                return [0, byteSize].concat(back);
+            } else {
+                return [byteSize >> 8, byteSize & 0xff].concat(back);
+            }
+        };
+
+
+        let utf8decode = function (arr) {
+            if (typeof arr === 'string') {
+                return arr;
+            }
+            let UTF = '', _arr = arr;
+            for (let i = 0; i < _arr.length; i++) {
+                let one = _arr[i].toString(2),
+                    v = one.match(/^1+?(?=0)/);
+                if (v && one.length === 8) {
+                    let bytesLength = v[0].length;
+                    let store = _arr[i].toString(2).slice(7 - bytesLength);
+                    for (let st = 1; st < bytesLength; st++) {
+                        store += _arr[st + i].toString(2).slice(2)
+                    }
+                    UTF += String.fromCharCode(parseInt(store, 2));
+                    i += bytesLength - 1
+                } else {
+                    UTF += String.fromCharCode(_arr[i])
+                }
+            }
+            return UTF
+        };
+
+
+        let hexEncode = function (str) {
+            let charBuf = utf8encode(str, true);
+            let re = '';
+
+            for (let i = 0; i < charBuf.length; i++) {
+                let x = (charBuf[i] & 0xFF).toString(16);
+                if (x.length === 1) {
+                    x = '0' + x;
+                }
+                re += x;
+            }
+            return re;
+        };
+
+
+        let hexDecode = function (str) {
+            let buf = [];
+            for (let i = 0; i < str.length; i += 2) {
+                buf.push(parseInt(str.substring(i, i + 2), 16));
+            }
+            return utf8decode(buf);
+        };
+
+        return {hexEncode, hexDecode};
+    })();
+
+
+    /**
+     * html代码转换成js
+     * @param txt
+     * @returns {string}
+     */
+    let _html2js = function (txt) {
+        let htmlArr = txt.replace(/\\/g, "\\\\").replace(/\\/g, "\\/").replace(/\'/g, "\\\'").split('\n');
+        let len = htmlArr.length;
+        let outArr = [];
+        outArr.push("let htmlCodes = [\n");
+        htmlArr.forEach((value, index) => {
+            if (value !== "") {
+                if (index === len - 1) {
+                    outArr.push("\'" + value + "\'");
+                } else {
+                    outArr.push("\'" + value + "\',\n");
+                }
+            }
+
+        });
+        outArr.push("\n].join(\"\");");
+        return outArr.join("");
+    };
+
+
+    /**
+     * URL 参数解析
+     * @param url
+     * @returns {{url: *, params: Array}}
+     * @private
+     */
+    let _urlParamsDecode = function (url) {
+        let res = {};
+        try {
+            let params = [];
+            let urlObj = new URL(url);
+            for (let item of urlObj.searchParams) {
+                params.push(item);
+            }
+            res = {
+                url: urlObj.href,
+                params: params,
+                protocol: urlObj.protocol,
+                pathname: urlObj.pathname,
+                hostname: urlObj.hostname
+            }
+        } catch (e) {
+            res.error = '这不是一个合法的URL!无法完成解析!'
+        }
+        return res;
+    };
+
+    // sha1加密
+    let _sha1Encode = function (str) {
+        function encodeUTF8(s) {
+            let i, r = [], c, x;
+            for (i = 0; i < s.length; i++)
+                if ((c = s.charCodeAt(i)) < 0x80) r.push(c);
+                else if (c < 0x800) r.push(0xC0 + (c >> 6 & 0x1F), 0x80 + (c & 0x3F));
+                else {
+                    if ((x = c ^ 0xD800) >> 10 == 0)
+                        c = (x << 10) + (s.charCodeAt(++i) ^ 0xDC00) + 0x10000,
+                            r.push(0xF0 + (c >> 18 & 0x7), 0x80 + (c >> 12 & 0x3F));
+                    else r.push(0xE0 + (c >> 12 & 0xF));
+                    r.push(0x80 + (c >> 6 & 0x3F), 0x80 + (c & 0x3F));
+                }
+            return r;
+        }
+
+        var data = new Uint8Array(encodeUTF8(str))
+        var i, j, t;
+        var l = ((data.length + 8) >>> 6 << 4) + 16, s = new Uint8Array(l << 2);
+        s.set(new Uint8Array(data.buffer)), s = new Uint32Array(s.buffer);
+        for (t = new DataView(s.buffer), i = 0; i < l; i++)s[i] = t.getUint32(i << 2);
+        s[data.length >> 2] |= 0x80 << (24 - (data.length & 3) * 8);
+        s[l - 1] = data.length << 3;
+        var w = [], f = [
+                function () { return m[1] & m[2] | ~m[1] & m[3]; },
+                function () { return m[1] ^ m[2] ^ m[3]; },
+                function () { return m[1] & m[2] | m[1] & m[3] | m[2] & m[3]; },
+                function () { return m[1] ^ m[2] ^ m[3]; }
+            ], rol = function (n, c) { return n << c | n >>> (32 - c); },
+            k = [1518500249, 1859775393, -1894007588, -899497514],
+            m = [1732584193, -271733879, null, null, -1009589776];
+        m[2] = ~m[0], m[3] = ~m[1];
+        for (i = 0; i < s.length; i += 16) {
+            var o = m.slice(0);
+            for (j = 0; j < 80; j++)
+                w[j] = j < 16 ? s[i + j] : rol(w[j - 3] ^ w[j - 8] ^ w[j - 14] ^ w[j - 16], 1),
+                    t = rol(m[0], 5) + f[j / 20 | 0]() + m[4] + w[j] + k[j / 20 | 0] | 0,
+                    m[1] = rol(m[1], 30), m.pop(), m.unshift(t);
+            for (j = 0; j < 5; j++)m[j] = m[j] + o[j] | 0;
+        };
+        t = new DataView(new Uint32Array(m).buffer);
+        for (var i = 0; i < 5; i++)m[i] = t.getUint32(i << 2);
+
+        var hex = Array.prototype.map.call(new Uint8Array(new Uint32Array(m).buffer), function (e) {
+            return (e < 16 ? "0" : "") + e.toString(16);
+        }).join("");
+        return hex;
+    };
+
+    // 来自网友的贡献,做jwt解码
+    let jwtDecode = (() => {
+        class InvalidTokenError extends Error {
+        }
+        InvalidTokenError.prototype.name = "InvalidTokenError";
+        function b64DecodeUnicode(str) {
+            return decodeURIComponent(atob(str).replace(/(.)/g, (m, p) => {
+                let code = p.charCodeAt(0).toString(16).toUpperCase();
+                if (code.length < 2) {
+                    code = "0" + code;
+                }
+                return "%" + code;
+            }));
+        }
+        function base64UrlDecode(str) {
+            let output = str.replace(/-/g, "+").replace(/_/g, "/");
+            switch (output.length % 4) {
+                case 0:
+                    break;
+                case 2:
+                    output += "==";
+                    break;
+                case 3:
+                    output += "=";
+                    break;
+                default:
+                    throw new Error("base64 string is not of the correct length");
+            }
+            try {
+                return b64DecodeUnicode(output);
+            }
+            catch (err) {
+                return atob(output);
+            }
+        }
+        return function(token) {
+            if (typeof token !== "string") {
+                throw new InvalidTokenError("Invalid token specified: must be a string");
+            }
+            const parts = token.split(".");
+           
+            if (parts.length !== 3) {
+                throw new InvalidTokenError("Invalid token specified: must be three parts");
+            }
+            
+            for(let part of parts){
+                if (typeof part !== "string") {
+                    throw new InvalidTokenError(`Invalid token specified: missing part #${pos + 1}`);
+                }        
+            }
+        
+            return {
+                header: base64UrlDecode(parts[0]),
+                payload: base64UrlDecode(parts[1]),
+                sign: parts[2]
+            }
+        }
+    })();
+
+
+    /**
+     * 将cookie字符串格式化为JSON对象
+     * @param {string} cookieString - 原始的cookie字符串
+     * @returns {string} 格式化后的JSON字符串
+     */
+    let formatCookieStringToJson = (cookieString) => {
+        // 将原始cookie字符串分割成各个键值对,并解析为JSON对象
+        const cookiesArray = cookieString.split(';').map(pair => {
+            const [key, value] = pair.trim().split('=');
+            let obj = {},dk , vk ;
+            try {
+                dk = decodeURIComponent(key);
+                vk = decodeURIComponent(value);
+            } catch (error) {
+                dk = key;
+                vk = value;
+            }
+            obj[dk] = vk;
+            return obj;
+        }).reduce((accumulator, current) => {
+            // 合并所有键值对到一个对象中
+            const key = Object.keys(current)[0];
+            accumulator[key] = current[key];
+            return accumulator;
+        }, {});
+    
+        // 返回格式化的JSON
+        return cookiesArray;
+    }
+    
+
+    return {
+        uniEncode: _uniEncode,
+        uniDecode: _uniDecode,
+        base64Encode: _base64Encode,
+        base64Decode: _base64Decode,
+        utf8Encode: _utf8Encode,
+        utf8Decode: _utf8Decode,
+        utf16to8: _utf16to8,
+        utf8to16: _utf8to16,
+        md5: md5,
+        hexEncode: hexTools.hexEncode,
+        hexDecode: hexTools.hexDecode,
+        html2js: _html2js,
+        urlParamsDecode: _urlParamsDecode,
+        sha1Encode: _sha1Encode,
+        jwtDecode,
+        formatCookieStringToJson
+    };
+})();
+
+export default EncodeUtils;

La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 30 - 0
apps/en-decode/he.js


+ 55 - 0
apps/en-decode/index.css

@@ -0,0 +1,55 @@
+@import url("../static/css/bootstrap.min.css");
+
+#srcText {
+    height: 100px;
+}
+#rstCode {
+    height: 280px;
+}
+.ui-ml-05 {
+    margin-left: 5px;
+}
+.x-ps {
+    color:#bbb;
+}
+.x-url-infos ul {
+    padding:0;
+    margin:0;
+}
+.x-url-infos ul li {
+    list-style: none;
+    font-size: 14px;
+    line-height: 24px;
+    padding:0;
+    margin:0;
+}
+.x-url-infos table {
+    width:700px;
+}
+#rst h5 {
+    padding-bottom: 10px;
+    border-bottom: 1px solid #eee;
+}
+.x-btns {
+    float: right;
+    top: -6px;
+    position: relative;
+}
+.x-opts {
+    font-size: 14px;
+}
+.x-opts .radio,
+.x-opts .checkbox{
+    margin-top:0;
+    margin-bottom: 0;
+}
+td.td-label {
+    width:60px;
+}
+.x-opts td {
+    padding:5px 0;
+    vertical-align: middle;
+}
+.x-opts tr:first-child {
+    border-bottom: 1px solid #eee;
+}

+ 138 - 0
apps/en-decode/index.html

@@ -0,0 +1,138 @@
+<!DOCTYPE HTML>
+<html lang="zh-CN">
+    <head>
+        <title>信息编解码工具</title>
+        <meta charset="UTF-8">
+        <link rel="shortcut icon" href="../static/img/favicon.ico">
+        <link rel="stylesheet" href="index.css" />
+        <script type="text/javascript" src="../static/vendor/evalCore.min.js"></script>
+        <script type="text/javascript" src="../static/vendor/vue/vue.js"></script>
+    </head>
+    <body>
+
+    <div class="wrapper" id="pageContainer">
+        <div class="panel panel-default" style="margin-bottom: 0px;">
+            <div class="panel-heading">
+                <h3 class="panel-title">
+                    <a href="https://www.baidufe.com/fehelper/index/index.html" target="_blank" class="x-a-high">
+                        <img src="../static/img/fe-16.png" alt="fehelper"/> FeHelper</a>:信息编解码工具
+
+                    <span class="x-btns">
+                        <input id="btnCodeChange" class="btn btn-sm btn-success" type="button" value="转换" @click="convert()">
+                        <input id="btnCodeClear" class="btn btn-sm btn-warning ui-ml-10" type="button" value="清空" @click="clear()">
+                    </span>
+                </h3>
+            </div>
+        </div>
+        <div class="panel-body mod-endecode">
+            <div class="row">
+                <textarea class="form-control mod-textarea ui-mb-10" id="srcText" ref="srcText" v-model="sourceContent" placeholder="粘贴需要进行转换的字符串"></textarea>
+            </div>
+
+            <div class="row">
+                <table class="x-opts">
+                    <tr>
+                        <td class="td-label"><label>加密:</label></td>
+                        <td>
+                            <div class="radio ui-d-ib ui-mr-20">
+                                <label><input type="radio" name="codeType" value="uniEncode" v-model="selectedType" @click="convert()">Unicode编码<span class="x-ps">(\u开头)</span></label>
+                            </div>
+                            <div class="radio ui-d-ib ui-mr-20">
+                                <label><input type="radio" name="codeType" value="utf8Encode" v-model="selectedType" @click="convert()">URL编码<span class="x-ps">(%开头)</span></label>
+                            </div>
+                            <div class="radio ui-d-ib ui-mr-20">
+                                <label><input type="radio" name="codeType" value="utf16Encode" v-model="selectedType" @click="convert()">UTF16编码<span class="x-ps">(\x开头)</span></label>
+                            </div>
+                            <div class="radio ui-d-ib ui-mr-20">
+                                <label><input type="radio" name="codeType" value="base64Encode" v-model="selectedType" @click="convert()">Base64编码</label>
+                            </div>
+                            <div class="radio ui-d-ib ui-mr-20">
+                                <label><input type="radio" name="codeType" value="md5Encode" v-model="selectedType" @click="convert()">MD5计算</label>
+                            </div>
+                            <div class="radio ui-d-ib ui-mr-20">
+                                <label><input type="radio" name="codeType" value="hexEncode" v-model="selectedType" @click="convert()">十六进制编码</label>
+                            </div>
+                            <div class="radio ui-d-ib ui-mr-20">
+                                <label><input type="radio" name="codeType" value="sha1Encode" v-model="selectedType" @click="convert()">Sha1加密</label>
+                            </div>
+                            <div class="radio ui-d-ib ui-mr-20">
+                                <label><input type="radio" name="codeType" value="htmlEntityEncode" v-model="selectedType" @click="convert()">HTML普通编码</label>
+                            </div>
+                            <div class="radio ui-d-ib ui-mr-20">
+                                <label><input type="radio" name="codeType" value="htmlEntityFullEncode" v-model="selectedType" @click="convert()">HTML深度编码</label>
+                            </div>
+                            <div class="radio ui-d-ib ui-mr-20">
+                                <label><input type="radio" name="codeType" value="html2js" v-model="selectedType" @click="convert()">HTML转JS</label>
+                            </div>
+                        </td>
+                    </tr>
+                    <tr>
+                        <td class="td-label"><label>解密:</label></td>
+                        <td>
+                            <div class="radio ui-d-ib ui-mr-20">
+                                <label><input type="radio" name="codeType" value="uniDecode" v-model="selectedType" @click="convert()">Unicode解码<span class="x-ps">(\u开头)</span></label>
+                            </div>
+                            <div class="radio ui-d-ib ui-mr-20">
+                                <label><input type="radio" name="codeType" value="utf8Decode" v-model="selectedType" @click="convert()">URL解码<span class="x-ps">(%开头)</span></label>
+                            </div>
+                            <div class="radio ui-d-ib ui-mr-20">
+                                <label><input type="radio" name="codeType" value="utf16Decode" v-model="selectedType" @click="convert()">UTF16解码<span class="x-ps">(\x开头)</span></label>
+                            </div>
+                            <div class="radio ui-d-ib ui-mr-20">
+                                <label><input type="radio" name="codeType" value="base64Decode" v-model="selectedType" @click="convert()">Base64解码</label>
+                            </div>
+                            <div class="radio ui-d-ib ui-mr-20">
+                                <label><input type="radio" name="codeType" value="hexDecode" v-model="selectedType" @click="convert()">十六进制解码</label>
+                            </div>
+                            <div class="radio ui-d-ib ui-mr-20">
+                                <label><input type="radio" name="codeType" value="htmlEntityDecode" v-model="selectedType" @click="convert()">HTML实体解码</label>
+                            </div>
+                            <div class="radio ui-d-ib ui-mr-20">
+                                <label><input type="radio" name="codeType" value="urlParamsDecode" v-model="selectedType" @click="convert()">URL参数解析</label>
+                            </div>
+                            <div class="radio ui-d-ib ui-mr-20">
+                                <label><input type="radio" name="codeType" value="jwtDecode" v-model="selectedType" @click="convert()">JWT解码</label>
+                            </div>
+                            <div class="radio ui-d-ib ui-mr-20">
+                                <label><input type="radio" name="codeType" value="cookieDecode" v-model="selectedType" @click="convert()">Cookie格式化</label>
+                            </div>
+                        </td>
+                    </tr>
+                </table>
+
+            </div>
+
+            <div id="rst" class="row ui-mt-20" v-show="resultContent.length || urlResult">
+                <h5>当前数据解析结果如下:</h5>
+                <textarea class="form-control mod-textarea" id="rstCode" ref="rstCode" v-model="resultContent" @mouseover="getResult()" v-if="!urlResult"></textarea>
+
+                <div class="x-url-infos" v-if="urlResult">
+                    <ul>
+                        <li><b>协议</b>:<span>{{urlResult.protocol}}</span></li>
+                        <li><b>域名</b>:<span>{{urlResult.hostname}}</span></li>
+                        <li><b>路径</b>:<span>{{urlResult.pathname}}</span></li>
+                        <li>
+                            <table class="table table-bordered table-hover ui-mt-10">
+                                <tr>
+                                    <thead>
+                                        <th>参数Key</th>
+                                        <th>参数Value</th>
+                                    </thead>
+                                </tr>
+                                <tr v-for="item in urlResult.params">
+                                    <td>{{item[0]}}</td>
+                                    <td>{{item[1]}}</td>
+                                </tr>
+                            </table>
+                        </li>
+                    </ul>
+
+                </div>
+            </div>
+        </div>
+    </div>
+    <script type="text/javascript" src="he.js"></script>
+    <script src="../static/vendor/jquery/jquery-3.3.1.min.js"></script>
+    <script type="module" src="index.js"></script>
+    </body>
+</html>

+ 124 - 0
apps/en-decode/index.js

@@ -0,0 +1,124 @@
+/**
+ * FeHelper 信息编解码
+ */
+import EncodeUtils from './endecode-lib.js';
+
+new Vue({
+    el: '#pageContainer',
+    data: {
+        selectedType: 'uniEncode',
+        sourceContent: '',
+        resultContent: '',
+        urlResult: null
+    },
+
+    mounted: function () {
+
+        // 在tab创建或者更新时候,监听事件,看看是否有参数传递过来
+        if (location.protocol === 'chrome-extension:') {
+            chrome.tabs.query({currentWindow: true,active: true, }, (tabs) => {
+                let activeTab = tabs.filter(tab => tab.active)[0];
+                chrome.runtime.sendMessage({
+                    type: 'fh-dynamic-any-thing',
+                    thing: 'request-page-content',
+                    tabId: activeTab.id
+                }).then(resp => {
+                    if(!resp || !resp.content) return ;
+                    this.sourceContent = resp.content;
+                    this.convert();
+                });
+            });
+        }
+
+        this.$refs.srcText.focus();
+    },
+    methods: {
+        convert: function () {
+            this.$nextTick(() => {
+                this.urlResult = null;
+
+                if (this.selectedType === 'uniEncode') {
+
+                    this.resultContent = EncodeUtils.uniEncode(this.sourceContent);
+                } else if (this.selectedType === 'uniDecode') {
+
+                    this.resultContent = EncodeUtils.uniDecode(this.sourceContent.replace(/\\U/g, '\\u'));
+                } else if (this.selectedType === 'utf8Encode') {
+
+                    this.resultContent = encodeURIComponent(this.sourceContent);
+                } else if (this.selectedType === 'utf8Decode') {
+
+                    this.resultContent = decodeURIComponent(this.sourceContent);
+                } else if (this.selectedType === 'utf16Encode') {
+
+                    this.resultContent = EncodeUtils.utf8to16(encodeURIComponent(this.sourceContent));
+                } else if (this.selectedType === 'utf16Decode') {
+
+                    this.resultContent = decodeURIComponent(EncodeUtils.utf16to8(this.sourceContent));
+                } else if (this.selectedType === 'base64Encode') {
+
+                    this.resultContent = EncodeUtils.base64Encode(EncodeUtils.utf8Encode(this.sourceContent));
+                } else if (this.selectedType === 'base64Decode') {
+
+                    this.resultContent = EncodeUtils.utf8Decode(EncodeUtils.base64Decode(this.sourceContent));
+                } else if (this.selectedType === 'md5Encode') {
+
+                    this.resultContent = EncodeUtils.md5(this.sourceContent);
+                } else if (this.selectedType === 'hexEncode') {
+
+                    this.resultContent = EncodeUtils.hexEncode(this.sourceContent);
+                } else if (this.selectedType === 'hexDecode') {
+
+                    this.resultContent = EncodeUtils.hexDecode(this.sourceContent);
+                } else if (this.selectedType === 'html2js') {
+
+                    this.resultContent = EncodeUtils.html2js(this.sourceContent);
+                } else if (this.selectedType === 'sha1Encode') {
+
+                    this.resultContent = EncodeUtils.sha1Encode(this.sourceContent);
+                } else if (this.selectedType === 'htmlEntityEncode') {
+
+                    this.resultContent = he.encode(this.sourceContent, {
+                        'useNamedReferences': true,
+                        'allowUnsafeSymbols': true
+                    });
+                } else if (this.selectedType === 'htmlEntityFullEncode') {
+
+                    this.resultContent = he.encode(this.sourceContent, {
+                        'encodeEverything': true,
+                        'useNamedReferences': true,
+                        'allowUnsafeSymbols': true
+                    });
+                } else if (this.selectedType === 'htmlEntityDecode') {
+
+                    this.resultContent = he.decode(this.sourceContent, {
+                        'isAttributeValue': false
+                    });
+                } else if (this.selectedType === 'urlParamsDecode') {
+                    let res = EncodeUtils.urlParamsDecode(this.sourceContent);
+                    if (res.error) {
+                        this.resultContent = res.error;
+                    } else {
+                        this.urlResult = res;
+                    }
+                } else if(this.selectedType === 'jwtDecode') {
+                    let {header,payload,sign} = EncodeUtils.jwtDecode(this.sourceContent);
+                    this.resultContent = `Header: ${header}\n\nPayload: ${payload}\n\nSign: ${sign}`;
+                } else if(this.selectedType === 'cookieDecode') {
+                    let ckJson = EncodeUtils.formatCookieStringToJson(this.sourceContent);
+                    this.resultContent = JSON.stringify(ckJson,null,4);
+                }
+                this.$forceUpdate();
+            });
+        },
+
+        clear: function () {
+            this.sourceContent = '';
+            this.resultContent = '';
+        },
+
+        getResult: function () {
+            this.$refs.rstCode.select();
+        }
+    }
+});

+ 274 - 0
apps/en-decode/md5.js

@@ -0,0 +1,274 @@
+/*
+ * JavaScript MD5
+ * https://github.com/blueimp/JavaScript-MD5
+ *
+ * Copyright 2011, Sebastian Tschan
+ * https://blueimp.net
+ *
+ * Licensed under the MIT license:
+ * https://opensource.org/licenses/MIT
+ *
+ * Based on
+ * A JavaScript implementation of the RSA Data Security, Inc. MD5 Message
+ * Digest Algorithm, as defined in RFC 1321.
+ * Version 2.2 Copyright (C) Paul Johnston 1999 - 2009
+ * Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet
+ * Distributed under the BSD License
+ * See http://pajhome.org.uk/crypt/md5 for more info.
+ */
+
+/* global define */
+
+let Md5Utils = (function ($) {
+    'use strict'
+
+    /*
+    * Add integers, wrapping at 2^32. This uses 16-bit operations internally
+    * to work around bugs in some JS interpreters.
+    */
+    function safeAdd (x, y) {
+        var lsw = (x & 0xffff) + (y & 0xffff)
+        var msw = (x >> 16) + (y >> 16) + (lsw >> 16)
+        return (msw << 16) | (lsw & 0xffff)
+    }
+
+    /*
+    * Bitwise rotate a 32-bit number to the left.
+    */
+    function bitRotateLeft (num, cnt) {
+        return (num << cnt) | (num >>> (32 - cnt))
+    }
+
+    /*
+    * These functions implement the four basic operations the algorithm uses.
+    */
+    function md5cmn (q, a, b, x, s, t) {
+        return safeAdd(bitRotateLeft(safeAdd(safeAdd(a, q), safeAdd(x, t)), s), b)
+    }
+    function md5ff (a, b, c, d, x, s, t) {
+        return md5cmn((b & c) | (~b & d), a, b, x, s, t)
+    }
+    function md5gg (a, b, c, d, x, s, t) {
+        return md5cmn((b & d) | (c & ~d), a, b, x, s, t)
+    }
+    function md5hh (a, b, c, d, x, s, t) {
+        return md5cmn(b ^ c ^ d, a, b, x, s, t)
+    }
+    function md5ii (a, b, c, d, x, s, t) {
+        return md5cmn(c ^ (b | ~d), a, b, x, s, t)
+    }
+
+    /*
+    * Calculate the MD5 of an array of little-endian words, and a bit length.
+    */
+    function binlMD5 (x, len) {
+        /* append padding */
+        x[len >> 5] |= 0x80 << (len % 32)
+        x[((len + 64) >>> 9 << 4) + 14] = len
+
+        var i
+        var olda
+        var oldb
+        var oldc
+        var oldd
+        var a = 1732584193
+        var b = -271733879
+        var c = -1732584194
+        var d = 271733878
+
+        for (i = 0; i < x.length; i += 16) {
+            olda = a
+            oldb = b
+            oldc = c
+            oldd = d
+
+            a = md5ff(a, b, c, d, x[i], 7, -680876936)
+            d = md5ff(d, a, b, c, x[i + 1], 12, -389564586)
+            c = md5ff(c, d, a, b, x[i + 2], 17, 606105819)
+            b = md5ff(b, c, d, a, x[i + 3], 22, -1044525330)
+            a = md5ff(a, b, c, d, x[i + 4], 7, -176418897)
+            d = md5ff(d, a, b, c, x[i + 5], 12, 1200080426)
+            c = md5ff(c, d, a, b, x[i + 6], 17, -1473231341)
+            b = md5ff(b, c, d, a, x[i + 7], 22, -45705983)
+            a = md5ff(a, b, c, d, x[i + 8], 7, 1770035416)
+            d = md5ff(d, a, b, c, x[i + 9], 12, -1958414417)
+            c = md5ff(c, d, a, b, x[i + 10], 17, -42063)
+            b = md5ff(b, c, d, a, x[i + 11], 22, -1990404162)
+            a = md5ff(a, b, c, d, x[i + 12], 7, 1804603682)
+            d = md5ff(d, a, b, c, x[i + 13], 12, -40341101)
+            c = md5ff(c, d, a, b, x[i + 14], 17, -1502002290)
+            b = md5ff(b, c, d, a, x[i + 15], 22, 1236535329)
+
+            a = md5gg(a, b, c, d, x[i + 1], 5, -165796510)
+            d = md5gg(d, a, b, c, x[i + 6], 9, -1069501632)
+            c = md5gg(c, d, a, b, x[i + 11], 14, 643717713)
+            b = md5gg(b, c, d, a, x[i], 20, -373897302)
+            a = md5gg(a, b, c, d, x[i + 5], 5, -701558691)
+            d = md5gg(d, a, b, c, x[i + 10], 9, 38016083)
+            c = md5gg(c, d, a, b, x[i + 15], 14, -660478335)
+            b = md5gg(b, c, d, a, x[i + 4], 20, -405537848)
+            a = md5gg(a, b, c, d, x[i + 9], 5, 568446438)
+            d = md5gg(d, a, b, c, x[i + 14], 9, -1019803690)
+            c = md5gg(c, d, a, b, x[i + 3], 14, -187363961)
+            b = md5gg(b, c, d, a, x[i + 8], 20, 1163531501)
+            a = md5gg(a, b, c, d, x[i + 13], 5, -1444681467)
+            d = md5gg(d, a, b, c, x[i + 2], 9, -51403784)
+            c = md5gg(c, d, a, b, x[i + 7], 14, 1735328473)
+            b = md5gg(b, c, d, a, x[i + 12], 20, -1926607734)
+
+            a = md5hh(a, b, c, d, x[i + 5], 4, -378558)
+            d = md5hh(d, a, b, c, x[i + 8], 11, -2022574463)
+            c = md5hh(c, d, a, b, x[i + 11], 16, 1839030562)
+            b = md5hh(b, c, d, a, x[i + 14], 23, -35309556)
+            a = md5hh(a, b, c, d, x[i + 1], 4, -1530992060)
+            d = md5hh(d, a, b, c, x[i + 4], 11, 1272893353)
+            c = md5hh(c, d, a, b, x[i + 7], 16, -155497632)
+            b = md5hh(b, c, d, a, x[i + 10], 23, -1094730640)
+            a = md5hh(a, b, c, d, x[i + 13], 4, 681279174)
+            d = md5hh(d, a, b, c, x[i], 11, -358537222)
+            c = md5hh(c, d, a, b, x[i + 3], 16, -722521979)
+            b = md5hh(b, c, d, a, x[i + 6], 23, 76029189)
+            a = md5hh(a, b, c, d, x[i + 9], 4, -640364487)
+            d = md5hh(d, a, b, c, x[i + 12], 11, -421815835)
+            c = md5hh(c, d, a, b, x[i + 15], 16, 530742520)
+            b = md5hh(b, c, d, a, x[i + 2], 23, -995338651)
+
+            a = md5ii(a, b, c, d, x[i], 6, -198630844)
+            d = md5ii(d, a, b, c, x[i + 7], 10, 1126891415)
+            c = md5ii(c, d, a, b, x[i + 14], 15, -1416354905)
+            b = md5ii(b, c, d, a, x[i + 5], 21, -57434055)
+            a = md5ii(a, b, c, d, x[i + 12], 6, 1700485571)
+            d = md5ii(d, a, b, c, x[i + 3], 10, -1894986606)
+            c = md5ii(c, d, a, b, x[i + 10], 15, -1051523)
+            b = md5ii(b, c, d, a, x[i + 1], 21, -2054922799)
+            a = md5ii(a, b, c, d, x[i + 8], 6, 1873313359)
+            d = md5ii(d, a, b, c, x[i + 15], 10, -30611744)
+            c = md5ii(c, d, a, b, x[i + 6], 15, -1560198380)
+            b = md5ii(b, c, d, a, x[i + 13], 21, 1309151649)
+            a = md5ii(a, b, c, d, x[i + 4], 6, -145523070)
+            d = md5ii(d, a, b, c, x[i + 11], 10, -1120210379)
+            c = md5ii(c, d, a, b, x[i + 2], 15, 718787259)
+            b = md5ii(b, c, d, a, x[i + 9], 21, -343485551)
+
+            a = safeAdd(a, olda)
+            b = safeAdd(b, oldb)
+            c = safeAdd(c, oldc)
+            d = safeAdd(d, oldd)
+        }
+        return [a, b, c, d]
+    }
+
+    /*
+    * Convert an array of little-endian words to a string
+    */
+    function binl2rstr (input) {
+        var i
+        var output = ''
+        var length32 = input.length * 32
+        for (i = 0; i < length32; i += 8) {
+            output += String.fromCharCode((input[i >> 5] >>> (i % 32)) & 0xff)
+        }
+        return output
+    }
+
+    /*
+    * Convert a raw string to an array of little-endian words
+    * Characters >255 have their high-byte silently ignored.
+    */
+    function rstr2binl (input) {
+        var i
+        var output = []
+        output[(input.length >> 2) - 1] = undefined
+        for (i = 0; i < output.length; i += 1) {
+            output[i] = 0
+        }
+        var length8 = input.length * 8
+        for (i = 0; i < length8; i += 8) {
+            output[i >> 5] |= (input.charCodeAt(i / 8) & 0xff) << (i % 32)
+        }
+        return output
+    }
+
+    /*
+    * Calculate the MD5 of a raw string
+    */
+    function rstrMD5 (s) {
+        return binl2rstr(binlMD5(rstr2binl(s), s.length * 8))
+    }
+
+    /*
+    * Calculate the HMAC-MD5, of a key and some data (raw strings)
+    */
+    function rstrHMACMD5 (key, data) {
+        var i
+        var bkey = rstr2binl(key)
+        var ipad = []
+        var opad = []
+        var hash
+        ipad[15] = opad[15] = undefined
+        if (bkey.length > 16) {
+            bkey = binlMD5(bkey, key.length * 8)
+        }
+        for (i = 0; i < 16; i += 1) {
+            ipad[i] = bkey[i] ^ 0x36363636
+            opad[i] = bkey[i] ^ 0x5c5c5c5c
+        }
+        hash = binlMD5(ipad.concat(rstr2binl(data)), 512 + data.length * 8)
+        return binl2rstr(binlMD5(opad.concat(hash), 512 + 128))
+    }
+
+    /*
+    * Convert a raw string to a hex string
+    */
+    function rstr2hex (input) {
+        var hexTab = '0123456789abcdef'
+        var output = ''
+        var x
+        var i
+        for (i = 0; i < input.length; i += 1) {
+            x = input.charCodeAt(i)
+            output += hexTab.charAt((x >>> 4) & 0x0f) + hexTab.charAt(x & 0x0f)
+        }
+        return output
+    }
+
+    /*
+    * Encode a string as utf-8
+    */
+    function str2rstrUTF8 (input) {
+        return unescape(encodeURIComponent(input))
+    }
+
+    /*
+    * Take string arguments and return either raw or hex encoded strings
+    */
+    function rawMD5 (s) {
+        return rstrMD5(str2rstrUTF8(s))
+    }
+    function hexMD5 (s) {
+        return rstr2hex(rawMD5(s))
+    }
+    function rawHMACMD5 (k, d) {
+        return rstrHMACMD5(str2rstrUTF8(k), str2rstrUTF8(d))
+    }
+    function hexHMACMD5 (k, d) {
+        return rstr2hex(rawHMACMD5(k, d))
+    }
+
+    function md5 (string, key, raw) {
+        if (!key) {
+            if (!raw) {
+                return hexMD5(string)
+            }
+            return rawMD5(string)
+        }
+        if (!raw) {
+            return hexHMACMD5(key, string)
+        }
+        return rawHMACMD5(key, string)
+    }
+
+    return {md5};
+})(this)
+
+export default Md5Utils;

+ 286 - 0
apps/excel2json/CSVParser.js

@@ -0,0 +1,286 @@
+//
+//  CSVParser.js
+//  Mr-Data-Converter
+//
+//  Input CSV or Tab-delimited data and this will parse it into a Data Grid Javascript object
+//
+//  CSV Parsing Function from Ben Nadel, http://www.bennadel.com/blog/1504-Ask-Ben-Parsing-CSV-Strings-With-Javascript-Exec-Regular-Expression-Command.htm
+
+
+var isDecimal_re     = /^\s*(\+|-)?((\d+([,\.]\d+)?)|([,\.]\d+))\s*$/;
+
+var CSVParser = {
+
+  //---------------------------------------
+  // UTILS
+  //---------------------------------------
+
+  isNumber: function(string) {
+    if( (string == null) || isNaN( new Number(string) ) ) {
+      return false;
+    }
+    return true;
+  },
+
+
+  //---------------------------------------
+  // PARSE
+  //---------------------------------------
+  //var parseOutput = CSVParser.parse(this.inputText, this.headersProvided, this.delimiter, this.downcaseHeaders, this.upcaseHeaders);
+
+  parse: function (input, headersIncluded, delimiterType, downcaseHeaders, upcaseHeaders, decimalSign) {
+
+    var dataArray = [];
+
+    var errors = [];
+
+    //test for delimiter
+    //count the number of commas
+    var RE = new RegExp("[^,]", "gi");
+    var numCommas = input.replace(RE, "").length;
+
+    //count the number of tabs
+    RE = new RegExp("[^\t]", "gi");
+    var numTabs = input.replace(RE, "").length;
+
+    var rowDelimiter = "\n";
+    //set delimiter
+    var columnDelimiter = ",";
+    if (numTabs > numCommas) {
+      columnDelimiter = "\t"
+    };
+
+    if (delimiterType === "comma") {
+      columnDelimiter = ","
+    } else if (delimiterType === "tab") {
+      columnDelimiter = "\t"
+    }
+
+
+    // kill extra empty lines
+    RE = new RegExp("^" + rowDelimiter + "+", "gi");
+    input = input.replace(RE, "");
+    RE = new RegExp(rowDelimiter + "+$", "gi");
+    input = input.replace(RE, "");
+
+    // var arr = input.split(rowDelimiter);
+    //
+    // for (var i=0; i < arr.length; i++) {
+    //   dataArray.push(arr[i].split(columnDelimiter));
+    // };
+
+
+    // dataArray = jQuery.csv(columnDelimiter)(input);
+    dataArray = this.CSVToArray(input, columnDelimiter);
+
+    //escape out any tabs or returns or new lines
+    for (var i = dataArray.length - 1; i >= 0; i--){
+      for (var j = dataArray[i].length - 1; j >= 0; j--){
+        dataArray[i][j] = dataArray[i][j].replace("\t", "\\t");
+        dataArray[i][j] = dataArray[i][j].replace("\n", "\\n");
+        dataArray[i][j] = dataArray[i][j].replace("\r", "\\r");
+      };
+    };
+
+
+    var headerNames = [];
+    var headerTypes = [];
+    var numColumns = dataArray[0].length;
+    var numRows = dataArray.length;
+    if (headersIncluded) {
+
+      //remove header row
+      headerNames = dataArray.splice(0,1)[0];
+      numRows = dataArray.length;
+
+    } else { //if no headerNames provided
+
+      //create generic property names
+      for (var i=0; i < numColumns; i++) {
+        headerNames.push("val"+String(i));
+        headerTypes.push("");
+      };
+
+    }
+
+
+    if (upcaseHeaders) {
+      for (var i = headerNames.length - 1; i >= 0; i--){
+        headerNames[i] = headerNames[i].toUpperCase();
+      };
+    };
+    if (downcaseHeaders) {
+      for (var i = headerNames.length - 1; i >= 0; i--){
+        headerNames[i] = headerNames[i].toLowerCase();
+      };
+    };
+
+    //test all the rows for proper number of columns.
+    for (var i=0; i < dataArray.length; i++) {
+      var numValues = dataArray[i].length;
+      if (numValues != numColumns) {this.log("Error parsing row "+String(i)+". Wrong number of columns.")};
+    };
+
+    //test columns for number data type
+    var numRowsToTest = dataArray.length;
+    var threshold = 0.9;
+    for (var i=0; i < headerNames.length; i++) {
+      var numFloats = 0;
+      var numInts = 0;
+      for (var r=0; r < numRowsToTest; r++) {
+        if (dataArray[r]) {
+          //replace comma with dot if comma is decimal separator
+          if(decimalSign='comma' && isDecimal_re.test(dataArray[r][i])){
+            dataArray[r][i] = dataArray[r][i].replace(",", ".");
+          }
+          if (CSVParser.isNumber(dataArray[r][i])) {
+            numInts++
+            if (String(dataArray[r][i]).indexOf(".") > 0) {
+              numFloats++
+            }
+          };
+        };
+
+      };
+
+      if ((numInts / numRowsToTest) > threshold){
+        if (numFloats > 0) {
+          headerTypes[i] = "float"
+        } else {
+          headerTypes[i] = "int"
+        }
+      } else {
+        headerTypes[i] = "string"
+      }
+    }
+
+
+
+
+
+    return {'dataGrid':dataArray, 'headerNames':headerNames, 'headerTypes':headerTypes, 'errors':this.getLog()}
+
+  },
+
+
+  //---------------------------------------
+  // ERROR LOGGING
+  //---------------------------------------
+  errorLog:[],
+
+  resetLog: function() {
+    this.errorLog = [];
+  },
+
+  log: function(l) {
+    this.errorLog.push(l);
+  },
+
+  getLog: function() {
+    var out = "";
+    if (this.errorLog.length > 0) {
+      for (var i=0; i < this.errorLog.length; i++) {
+        out += ("!!"+this.errorLog[i] + "!!\n");
+      };
+      out += "\n"
+    };
+
+    return out;
+  },
+
+
+
+  //---------------------------------------
+  // UTIL
+  //---------------------------------------
+
+    // This Function from Ben Nadel, http://www.bennadel.com/blog/1504-Ask-Ben-Parsing-CSV-Strings-With-Javascript-Exec-Regular-Expression-Command.htm
+    // This will parse a delimited string into an array of
+    // arrays. The default delimiter is the comma, but this
+    // can be overriden in the second argument.
+    CSVToArray: function( strData, strDelimiter ){
+      // Check to see if the delimiter is defined. If not,
+      // then default to comma.
+      strDelimiter = (strDelimiter || ",");
+
+      // Create a regular expression to parse the CSV values.
+      var objPattern = new RegExp(
+        (
+          // Delimiters.
+          "(\\" + strDelimiter + "|\\r?\\n|\\r|^)" +
+
+          // Quoted fields.
+          "(?:\"([^\"]*(?:\"\"[^\"]*)*)\"|" +
+
+          // Standard fields.
+          "([^\"\\" + strDelimiter + "\\r\\n]*))"
+        ),
+        "gi"
+        );
+
+
+      // Create an array to hold our data. Give the array
+      // a default empty first row.
+      var arrData = [[]];
+
+      // Create an array to hold our individual pattern
+      // matching groups.
+      var arrMatches = null;
+
+
+      // Keep looping over the regular expression matches
+      // until we can no longer find a match.
+      while (arrMatches = objPattern.exec( strData )){
+
+        // Get the delimiter that was found.
+        var strMatchedDelimiter = arrMatches[ 1 ];
+
+        // Check to see if the given delimiter has a length
+        // (is not the start of string) and if it matches
+        // field delimiter. If id does not, then we know
+        // that this delimiter is a row delimiter.
+        if (
+          strMatchedDelimiter.length &&
+          (strMatchedDelimiter != strDelimiter)
+          ){
+
+          // Since we have reached a new row of data,
+          // add an empty row to our data array.
+          arrData.push( [] );
+
+        }
+
+
+        // Now that we have our delimiter out of the way,
+        // let's check to see which kind of value we
+        // captured (quoted or unquoted).
+
+        if (arrMatches[ 2 ]){
+
+          // We found a quoted value. When we capture
+          // this value, unescape any double quotes.
+          var strMatchedValue = arrMatches[ 2 ].replace(
+            new RegExp( "\"\"", "g" ),
+            "\""
+            );
+
+        } else {
+
+          // We found a non-quoted value.
+          var strMatchedValue = arrMatches[ 3 ];
+
+        }
+
+
+        // Now that we have our value string, let's add
+        // it to the data array.
+        arrData[ arrData.length - 1 ].push( strMatchedValue );
+      }
+
+      // Return the parsed data.
+      return( arrData );
+    }
+
+
+
+}

+ 538 - 0
apps/excel2json/DataGridRenderer.js

@@ -0,0 +1,538 @@
+// 
+//  DataGridRenderer.js
+//  Part of Mr-Data-Converter
+//  
+//  Created by Shan Carter on 2010-10-18.
+// 
+
+
+var DataGridRenderer = {
+  
+  //---------------------------------------
+  // Actionscript
+  //---------------------------------------
+  
+  as: function (dataGrid, headerNames, headerTypes, indent, newLine) {
+    //inits...
+    var commentLine = "//";
+    var commentLineEnd = "";
+    var outputText = "[";
+    var numRows = dataGrid.length;
+    var numColumns = headerNames.length;
+    
+    //begin render loops
+    for (var i=0; i < numRows; i++) {
+      var row = dataGrid[i];
+      outputText += "{";
+      for (var j=0; j < numColumns; j++) {
+        if ((headerTypes[j] == "int")||(headerTypes[j] == "float")) {
+          var rowOutput = row[j] || "null";
+        } else {
+          var rowOutput = '"'+( row[j] || "" )+'"';
+        };      
+        outputText += (headerNames[j] + ":" + rowOutput)
+        if (j < (numColumns-1)) {outputText+=","};
+      };
+      outputText += "}";
+      if (i < (numRows-1)) {outputText += ","+newLine};
+    };
+    outputText += "];";
+    
+    
+    return outputText;
+  },
+  
+  
+  //---------------------------------------
+  // ASP / VBScript
+  //---------------------------------------
+  
+  asp: function (dataGrid, headerNames, headerTypes, indent, newLine) {
+    //inits...
+    var commentLine = "'";
+    var commentLineEnd = "";
+    var outputText = "";
+    var numRows = dataGrid.length;
+    var numColumns = headerNames.length;
+    
+    //begin render loop
+    for (var i=0; i < numRows; i++) {
+      var row = dataGrid[i];
+      for (var j=0; j < numColumns; j++) {
+        if ((headerTypes[j] == "int")||(headerTypes[j] == "float")) {
+          var rowOutput = row[j] || "null";
+        } else {
+          var rowOutput = '"'+( row[j] || "" )+'"';
+        };
+      outputText += 'myArray('+j+','+i+') = '+rowOutput+newLine;        
+      };
+    };
+    outputText = 'Dim myArray('+(j-1)+','+(i-1)+')'+newLine+outputText;
+    
+    return outputText;
+  },
+  
+  
+  //---------------------------------------
+  // HTML Table
+  //---------------------------------------
+  
+  html: function (dataGrid, headerNames, headerTypes, indent, newLine) {
+    //inits...
+    var commentLine = "<!--";
+    var commentLineEnd = "-->";
+    var outputText = "";
+    var numRows = dataGrid.length;
+    var numColumns = headerNames.length;
+    
+    //begin render loop
+    outputText += "<table>"+newLine;
+    outputText += indent+"<thead>"+newLine;
+    outputText += indent+indent+"<tr>"+newLine;
+    
+    for (var j=0; j < numColumns; j++) {
+      outputText += indent+indent+indent+'<th class="'+headerNames[j]+'-cell">';          
+      outputText += headerNames[j];
+      outputText += '</th>'+newLine;
+    };
+    outputText += indent+indent+"</tr>"+newLine;
+    outputText += indent+"</thead>"+newLine;
+    outputText += indent+"<tbody>"+newLine;
+    for (var i=0; i < numRows; i++) {
+      var row = dataGrid[i];
+      var rowClassName = ""
+      if (i === numRows-1) {
+        rowClassName = ' class="lastRow"';
+      } else if (i === 0){
+        rowClassName = ' class="firstRow"';
+      }
+      outputText += indent+indent+"<tr"+rowClassName+">"+newLine;
+      for (var j=0; j < numColumns; j++) {
+        outputText += indent+indent+indent+'<td class="'+headerNames[j]+'-cell">';          
+        outputText += row[j]
+        outputText += '</td>'+newLine
+      };
+      outputText += indent+indent+"</tr>"+newLine;
+    };
+    outputText += indent+"</tbody>"+newLine;
+    outputText += "</table>";
+    
+    return outputText;
+  },
+  
+  
+  //---------------------------------------
+  // JSON properties
+  //---------------------------------------
+  
+  json: function (dataGrid, headerNames, headerTypes, indent, newLine) {
+    //inits...
+    var commentLine = "//";
+    var commentLineEnd = "";
+    var outputText = "[";
+    var numRows = dataGrid.length;
+    var numColumns = headerNames.length;
+    
+    //begin render loop
+    for (var i=0; i < numRows; i++) {
+      var row = dataGrid[i];
+      outputText += "{";
+      for (var j=0; j < numColumns; j++) {
+        if ((headerTypes[j] == "int")||(headerTypes[j] == "float")) {
+          var rowOutput = row[j] || "null";
+        } else {
+          var rowOutput = '"' + ( row[j] || "" ) + '"';
+        };
+  
+      outputText += ('"'+headerNames[j] +'"' + ":" + rowOutput );
+  
+        if (j < (numColumns-1)) {outputText+=","};
+      };
+      outputText += "}";
+      if (i < (numRows-1)) {outputText += ","+newLine};
+    };
+    outputText += "]";
+    
+    return outputText;
+  },
+  
+  //---------------------------------------
+  // JSON Array of Columns
+  //---------------------------------------
+  jsonArrayCols: function (dataGrid, headerNames, headerTypes, indent, newLine) {
+    //inits...
+    var commentLine = "//";
+    var commentLineEnd = "";
+    var outputText = "";
+    var numRows = dataGrid.length;
+    var numColumns = headerNames.length;
+    
+    //begin render loop
+    outputText += "{"+newLine;
+    for (var i=0; i < numColumns; i++) {
+      outputText += indent+'"'+headerNames[i]+'":[';
+      for (var j=0; j < numRows; j++) {
+        if ((headerTypes[i] == "int")||(headerTypes[i] == "float")) {
+          outputText += dataGrid[j][i] || 0;
+        } else {
+          outputText += '"'+(dataGrid[j][i] || "")+'"' ;
+        }
+        if (j < (numRows-1)) {outputText+=","};
+      };
+      outputText += "]";
+      if (i < (numColumns-1)) {outputText += ","+newLine};
+    };
+    outputText += newLine+"}";
+    
+    
+    return outputText;
+  },
+  
+  
+  //---------------------------------------
+  // JSON Array of Rows
+  //---------------------------------------
+  jsonArrayRows: function (dataGrid, headerNames, headerTypes, indent, newLine) {
+    //inits...
+    var commentLine = "//";
+    var commentLineEnd = "";
+    var outputText = "";
+    var numRows = dataGrid.length;
+    var numColumns = headerNames.length;
+    
+    //begin render loop
+    outputText += "["+newLine;
+    for (var i=0; i < numRows; i++) {
+      outputText += indent+"[";
+      for (var j=0; j < numColumns; j++) {
+        if ((headerTypes[j] == "int")||(headerTypes[j] == "float")) {
+          outputText += dataGrid[i][j] || 0;
+        } else {
+          outputText += '"'+(dataGrid[i][j] || "")+'"' ;
+        }
+        if (j < (numColumns-1)) {outputText+=","};
+      };
+      outputText += "]";
+      if (i < (numRows-1)) {outputText += ","+newLine};
+    };
+    outputText += newLine+"]";
+    
+    
+    return outputText;
+  },
+  
+  
+
+  //---------------------------------------
+  // JSON Dictionary
+  //---------------------------------------
+  jsonDict: function(dataGrid, headerNames, headerTypes, indent, newLine) {
+    //inits...
+    var commentLine = "//";
+    var commentLineEnd = "";
+    var outputText = "";
+    var numRows = dataGrid.length;
+    var numColumns = headerNames.length;
+
+    //begin render loop
+    outputText += "{" + newLine;
+    for (var i = 0; i < numRows; i++) {
+      outputText += indent + '"' + dataGrid[i][0] + '": ';
+      if (numColumns == 2) {
+        outputText += _fmtVal(i, 1, dataGrid);
+      } else {
+        outputText += '{ ';
+        for (var j = 1; j < numColumns; j++) {
+          if (j > 1) outputText += ', ';
+          outputText += '"' + headerNames[j] + '"' + ":" + _fmtVal(i, j, dataGrid);
+        }
+        outputText += '}';
+      }
+      if (i < (numRows - 1)) {
+        outputText += "," + newLine;
+      }
+    }
+    outputText += newLine + "}";
+
+    function _fmtVal(i, j) {
+      if ((headerTypes[j] == "int")||(headerTypes[j] == "float")) {
+        return dataGrid[i][j] || 0;
+      } else {
+        return '"'+(dataGrid[i][j] || "")+'"' ;
+      }
+    }
+
+    return outputText;
+  },
+
+
+  //---------------------------------------
+  // MYSQL
+  //---------------------------------------
+  mysql: function (dataGrid, headerNames, headerTypes, indent, newLine) {
+    //inits...
+    var commentLine = "/*";
+    var commentLineEnd = "*/";
+    var outputText = "";
+    var numRows = dataGrid.length;
+    var numColumns = headerNames.length;
+    var tableName = "MrDataConverter"
+    
+    //begin render loop
+    outputText += 'CREATE TABLE '+tableName+' (' + newLine;
+    outputText += indent+"id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,"+newLine;
+    for (var j=0; j < numColumns; j++) {
+      var dataType = "VARCHAR(255)";
+      if ((headerTypes[j] == "int")||(headerTypes[j] == "float")) {
+        dataType = headerTypes[j].toUpperCase();
+      };
+      outputText += indent+""+headerNames[j]+" "+dataType;
+      if (j < numColumns - 1) {outputText += ","};
+      outputText += newLine;
+    };
+    outputText += ');' + newLine;
+    outputText += "INSERT INTO "+tableName+" "+newLine+indent+"(";
+    for (var j=0; j < numColumns; j++) {
+      outputText += headerNames[j];
+      if (j < numColumns - 1) {outputText += ","};
+    };
+    outputText += ") "+newLine+"VALUES "+newLine;
+    for (var i=0; i < numRows; i++) {
+      outputText += indent+"(";
+      for (var j=0; j < numColumns; j++) {
+        if ((headerTypes[j] == "int")||(headerTypes[j] == "float"))  {
+          outputText += dataGrid[i][j] || "null";
+        } else {
+          outputText += "'"+( dataGrid[i][j] || "" )+"'";
+        };
+        
+        if (j < numColumns - 1) {outputText += ","};
+      };
+      outputText += ")";
+      if (i < numRows - 1) {outputText += ","+newLine;};
+    };
+    outputText += ";";
+    
+    return outputText;
+  },
+  
+  
+  //---------------------------------------
+  // PHP
+  //---------------------------------------
+  php: function (dataGrid, headerNames, headerTypes, indent, newLine) {
+    //inits...
+    var commentLine = "//";
+    var commentLineEnd = "";
+    var outputText = "";
+    var numRows = dataGrid.length;
+    var numColumns = headerNames.length;
+    var tableName = "MrDataConverter"
+    
+    //begin render loop
+    outputText += "array(" + newLine;
+    for (var i=0; i < numRows; i++) {
+      var row = dataGrid[i];
+      outputText += indent + "array(";
+      for (var j=0; j < numColumns; j++) {
+        if ((headerTypes[j] == "int")||(headerTypes[j] == "float"))  {
+          var rowOutput = row[j] || "null";
+        } else {
+          var rowOutput = '"'+(row[j] || "")+'"';
+        };          
+        outputText += ('"'+headerNames[j]+'"' + "=>" + rowOutput)
+        if (j < (numColumns-1)) {outputText+=","};
+      };
+      outputText += ")";
+      if (i < (numRows-1)) {outputText += ","+newLine};
+    };
+    outputText += newLine + ");";
+    
+    return outputText;
+  },
+  
+  //---------------------------------------
+  // Python dict
+  //---------------------------------------
+  
+  python: function (dataGrid, headerNames, headerTypes, indent, newLine) {
+    //inits...
+    var commentLine = "//";
+    var commentLineEnd = "";
+    var outputText = "[";
+    var numRows = dataGrid.length;
+    var numColumns = headerNames.length;
+    
+    //begin render loop
+    for (var i=0; i < numRows; i++) {
+      var row = dataGrid[i];
+      outputText += "{";
+      for (var j=0; j < numColumns; j++) {
+        if ((headerTypes[j] == "int")||(headerTypes[j] == "float")) {
+          var rowOutput = row[j] || "None";
+        } else {
+          var rowOutput = '"'+(row[j] || "")+'"';
+        };
+  
+      outputText += ('"'+headerNames[j] +'"' + ":" + rowOutput );
+  
+        if (j < (numColumns-1)) {outputText+=","};
+      };
+      outputText += "}";
+      if (i < (numRows-1)) {outputText += ","+newLine};
+    };
+    outputText += "];";
+    
+    return outputText;
+  },
+  
+  
+  //---------------------------------------
+  // Ruby
+  //---------------------------------------
+  ruby: function (dataGrid, headerNames, headerTypes, indent, newLine) {
+    //inits...
+    var commentLine = "#";
+    var commentLineEnd = "";
+    var outputText = "";
+    var numRows = dataGrid.length;
+    var numColumns = headerNames.length;
+    var tableName = "MrDataConverter"
+    
+    //begin render loop
+    outputText += "[";
+    for (var i=0; i < numRows; i++) {
+      var row = dataGrid[i];
+      outputText += "{";
+      for (var j=0; j < numColumns; j++) {
+        if ((headerTypes[j] == "int")||(headerTypes[j] == "float")) {
+          var rowOutput = row[j] || "nil"
+        } else {
+          var rowOutput = '"'+(row[j] || "")+'"';
+        };         
+        outputText += ('"'+headerNames[j]+'"' + "=>" + rowOutput)
+        if (j < (numColumns-1)) {outputText+=","};
+      };
+      outputText += "}";
+      if (i < (numRows-1)) {outputText += ","+newLine};
+    };
+    outputText += "];";
+    
+    return outputText;
+  },
+  
+  
+  //---------------------------------------
+  // XML Nodes
+  //---------------------------------------
+  xml: function (dataGrid, headerNames, headerTypes, indent, newLine) {
+    //inits...
+    var commentLine = "<!--";
+    var commentLineEnd = "-->";
+    var outputText = "";
+    var numRows = dataGrid.length;
+    var numColumns = headerNames.length;
+    
+    //begin render loop
+    outputText = '<?xml version="1.0" encoding="UTF-8"?>' + newLine;
+    outputText += "<rows>"+newLine;
+    for (var i=0; i < numRows; i++) {
+      var row = dataGrid[i];
+      outputText += indent+"<row>"+newLine;
+      for (var j=0; j < numColumns; j++) {
+        outputText += indent+indent+'<'+headerNames[j]+'>';          
+        outputText += row[j] || ""
+        outputText += '</'+headerNames[j]+'>'+newLine
+      };
+      outputText += indent+"</row>"+newLine;
+    };
+    outputText += "</rows>";
+    
+    return outputText;
+    
+  },
+  
+  
+  
+  //---------------------------------------
+  // XML properties
+  //---------------------------------------
+  xmlProperties: function (dataGrid, headerNames, headerTypes, indent, newLine) {
+    //inits...
+    var commentLine = "<!--";
+    var commentLineEnd = "-->";
+    var outputText = "";
+    var numRows = dataGrid.length;
+    var numColumns = headerNames.length;
+  
+    //begin render loop
+    outputText = '<?xml version="1.0" encoding="UTF-8"?>' + newLine;
+    outputText += "<rows>"+newLine;
+    for (var i=0; i < numRows; i++) {
+      var row = dataGrid[i];
+      outputText += indent+"<row ";
+      for (var j=0; j < numColumns; j++) {
+        outputText += headerNames[j]+'=';          
+        outputText += '"' + row[j] + '" ';
+      };
+      outputText += "></row>"+newLine;
+    };
+    outputText += "</rows>";
+    
+    return outputText;
+    
+  },
+  
+  //---------------------------------------
+  // XML Illustrator
+  //---------------------------------------
+  xmlIllustrator: function (dataGrid, headerNames, headerTypes, indent, newLine) {
+    //inits...
+    var commentLine = "<!--";
+    var commentLineEnd = "-->";
+    var outputText = "";
+    var numRows = dataGrid.length;
+    var numColumns = headerNames.length;
+    
+    //begin render loop
+    outputText = '<?xml version="1.0" encoding="utf-8"?>' + newLine;
+    outputText += '<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20001102//EN"    "http://www.w3.org/TR/2000/CR-SVG-20001102/DTD/svg-20001102.dtd" [' + newLine;
+    outputText += indent+'<!ENTITY ns_graphs "http://ns.adobe.com/Graphs/1.0/">' + newLine;
+    outputText += indent+'<!ENTITY ns_vars "http://ns.adobe.com/Variables/1.0/">' + newLine;
+    outputText += indent+'<!ENTITY ns_imrep "http://ns.adobe.com/ImageReplacement/1.0/">' + newLine;
+    outputText += indent+'<!ENTITY ns_custom "http://ns.adobe.com/GenericCustomNamespace/1.0/">' + newLine;
+    outputText += indent+'<!ENTITY ns_flows "http://ns.adobe.com/Flows/1.0/">' + newLine;
+    outputText += indent+'<!ENTITY ns_extend "http://ns.adobe.com/Extensibility/1.0/">' + newLine;
+    outputText += ']>' + newLine;
+    outputText += '<svg>' + newLine;
+    outputText += '<variableSets  xmlns="&ns_vars;">' + newLine;
+    outputText += indent+'<variableSet  varSetName="binding1" locked="none">' + newLine;
+    outputText += indent+indent+'<variables>' + newLine;
+    for (var i=0; i < numColumns; i++) {
+      outputText += indent+indent+indent+'<variable varName="'+headerNames[i]+'" trait="textcontent" category="&ns_flows;"></variable>' + newLine;
+    };
+    outputText += indent+indent+'</variables>' + newLine;
+    outputText += indent+indent+'<v:sampleDataSets  xmlns:v="http://ns.adobe.com/Variables/1.0/" xmlns="http://ns.adobe.com/GenericCustomNamespace/1.0/">' + newLine;
+    
+    for (var i=0; i < numRows; i++) {
+      var row = dataGrid[i];
+      outputText += indent+indent+indent+'<v:sampleDataSet dataSetName="' + row[0] + '">'+newLine;
+      for (var j=0; j < numColumns; j++) {
+        outputText += indent+indent+indent+indent+'<'+headerNames[j]+'>'+newLine;          
+        outputText += indent+indent+indent+indent+indent+'<p>' + row[j] + '</p>' +newLine;
+        outputText += indent+indent+indent+indent+'</'+headerNames[j]+'>'+newLine
+      };
+      outputText += indent+indent+indent+'</v:sampleDataSet>'+newLine;
+    };
+    
+    outputText += indent+indent+'</v:sampleDataSets>' + newLine;
+    outputText += indent+'</variableSet>' + newLine;
+    outputText += '</variableSets>' + newLine;
+    outputText += '</svg>' + newLine;
+    
+    
+    return outputText;
+    
+  },
+  
+}

+ 190 - 0
apps/excel2json/converter.js

@@ -0,0 +1,190 @@
+//
+//  converter.js
+//  Mr-Data-Converter
+//
+//  Created by Shan Carter on 2010-09-01.
+//
+
+
+
+function DataConverter(nodeId) {
+
+  //---------------------------------------
+  // PUBLIC PROPERTIES
+  //---------------------------------------
+
+  this.nodeId                 = nodeId;
+  this.node                   = $("#"+nodeId);
+
+  this.outputDataTypes        = [
+                                {"text":"Actionscript",           "id":"as",               "notes":""},
+                                {"text":"ASP/VBScript",           "id":"asp",              "notes":""},
+                                {"text":"HTML",                   "id":"html",             "notes":""},
+                                {"text":"JSON - Properties",      "id":"json",             "notes":""},
+                                {"text":"JSON - Column Arrays",   "id":"jsonArrayCols",    "notes":""},
+                                {"text":"JSON - Row Arrays",      "id":"jsonArrayRows",    "notes":""},
+                                {"text":"JSON - Dictionary",      "id":"jsonDict",         "notes":""},
+                                {"text":"MySQL",                  "id":"mysql",            "notes":""},
+                                {"text":"PHP",                    "id":"php",              "notes":""},
+                                {"text":"Python - Dict",          "id":"python",           "notes":""},
+                                {"text":"Ruby",                   "id":"ruby",             "notes":""},
+                                {"text":"XML - Properties",       "id":"xmlProperties",    "notes":""},
+                                {"text":"XML - Nodes",            "id":"xml",              "notes":""},
+                                {"text":"XML - Illustrator",      "id":"xmlIllustrator",   "notes":""}];
+  this.outputDataType         = "json";
+
+  this.columnDelimiter        = "\t";
+  this.rowDelimiter           = "\n";
+
+  this.inputTextArea          = {};
+  this.outputTextArea         = {};
+
+  this.inputHeader            = {};
+  this.outputHeader           = {};
+  this.dataSelect             = {};
+
+  this.inputText              = "";
+  this.outputText             = "";
+
+  this.newLine                = "\n";
+  this.indent                 = "  ";
+
+  this.root                   = "rows";
+  this.child                  = "row";
+
+  this.commentLine            = "//";
+  this.commentLineEnd         = "";
+  this.tableName              = "MrDataConverter"
+
+  this.useUnderscores         = true;
+  this.headersProvided        = true;
+  this.downcaseHeaders        = true;
+  this.upcaseHeaders          = false;
+  this.includeWhiteSpace      = true;
+  this.useTabsForIndent       = false;
+
+}
+
+//---------------------------------------
+// PUBLIC METHODS
+//---------------------------------------
+
+DataConverter.prototype.create = function(w,h) {
+  var self = this;
+
+  //build HTML for converter
+  this.inputHeader = $('<div class="groupHeader" id="inputHeader"><p class="groupHeadline">数据源输入<span class="subhead">(可以直接从Excel/CVS中拷贝内容到这里! <a href="#" id="insertSample">简单示例!</a>)</span></p></div>');
+  this.inputTextArea = $('<textarea class="textInputs" id="dataInput" placeholder="输入CVS或者以tab为间隔的数据"></textarea>');
+  var outputHeaderText = '<div class="groupHeader" id="outputHeader"><p class="groupHeadline">结果转换为<select name="Data Types" id="dataSelector" class="form-control">';
+    for (var i=0; i < this.outputDataTypes.length; i++) {
+
+      outputHeaderText += '<option value="'+this.outputDataTypes[i]["id"]+'" '
+              + (this.outputDataTypes[i]["id"] == this.outputDataType ? 'selected="selected"' : '')
+              + '>'
+              + this.outputDataTypes[i]["text"]+'</option>';
+    };
+    outputHeaderText += '</select><span class="subhead" id="outputNotes"></span></p></div>';
+  this.outputHeader = $(outputHeaderText);
+  this.outputTextArea = $('<textarea class="textInputs" id="dataOutput" placeholder="转换后的结果会显示在这里"></textarea>');
+
+  this.node.append(this.inputHeader);
+  this.node.append(this.inputTextArea);
+  this.node.append(this.outputHeader);
+  this.node.append(this.outputTextArea);
+
+  this.dataSelect = this.outputHeader.find("#dataSelector");
+
+
+  //add event listeners
+
+  // $("#convertButton").bind('click',function(evt){
+  //   evt.preventDefault();
+  //   self.convert();
+  // });
+
+  this.outputTextArea.click(function(evt){this.select();});
+
+
+  $("#insertSample").bind('click',function(evt){
+    evt.preventDefault();
+    self.insertSampleData();
+    self.convert();
+    _gaq.push(['_trackEvent', 'SampleData','InsertGeneric']);
+  });
+
+  $("#dataInput").keyup(function() {self.convert()});
+  $("#dataInput").change(function() {
+    self.convert();
+    _gaq.push(['_trackEvent', 'DataType',self.outputDataType]);
+  });
+
+  $("#dataSelector").bind('change',function(evt){
+       self.outputDataType = $(this).val();
+       self.convert();
+     });
+
+  this.resize(w,h);
+}
+
+DataConverter.prototype.resize = function(w,h) {
+
+  var paneWidth = w;
+  var paneHeight = (h-90)/2-20;
+
+  this.node.css({width:paneWidth});
+  this.inputTextArea.css({width:paneWidth-20,height:paneHeight});
+  this.outputTextArea.css({width: paneWidth-20, height:paneHeight});
+
+}
+
+DataConverter.prototype.convert = function() {
+
+  this.inputText = this.inputTextArea.val();
+  this.outputText = "";
+
+
+  //make sure there is input data before converting...
+  if (this.inputText.length > 0) {
+
+    if (this.includeWhiteSpace) {
+      this.newLine = "\n";
+    } else {
+      this.indent = "";
+      this.newLine = "";
+    }
+
+    CSVParser.resetLog();
+    var parseOutput = CSVParser.parse(this.inputText, this.headersProvided, this.delimiter, this.downcaseHeaders, this.upcaseHeaders);
+
+    var dataGrid = parseOutput.dataGrid;
+    var headerNames = parseOutput.headerNames;
+    var headerTypes = parseOutput.headerTypes;
+    var errors = parseOutput.errors;
+
+    this.outputText = DataGridRenderer[this.outputDataType](dataGrid, headerNames, headerTypes, this.indent, this.newLine);
+
+
+    //验证成功,将会对其中的节点进行替换
+    //否者,直接对数据进行输出
+    if(this.root
+        && this.child
+        && (this.outputDataType === "xmlProperties"
+            || this.outputDataType === "xml")){
+
+      //替换其中的根节点与字节点
+      this.outputText = this.outputText.replace(/rows/g,this.root)
+      this.outputText = this.outputText.replace(/row/g,this.child);
+    }
+    this.outputTextArea.val(errors + this.outputText);
+
+
+
+  }; //end test for existence of input text
+}
+
+
+DataConverter.prototype.insertSampleData = function() {
+  this.inputTextArea.val("NAME\tVALUE\tCOLOR\tDATE\nAlan\t12\tblue\tSep. 25, 2009\nShan\t13\t\"green\tblue\"\tSep. 27, 2009\nJohn\t45\torange\tSep. 29, 2009\nMinna\t27\tteal\tSep. 30, 2009");
+}
+
+

+ 143 - 0
apps/excel2json/index.css

@@ -0,0 +1,143 @@
+@import url("../static/css/bootstrap.min.css");
+
+#pageContainer > .panel-body {
+    margin: 0 auto;
+}
+
+body {
+    overflow: hidden;
+}
+
+#base {
+    padding: 15px;
+    position: relative;
+}
+
+/*header*/
+
+#header {
+    width: 300px;
+    overflow: auto;
+}
+
+#header a {
+    color: #99FFFF;
+}
+
+#description p {
+    margin-bottom: 18px;
+}
+
+#header h3 {
+    font-size: 16px;
+    margin: 15px 0 10px 0;
+    padding: 15px 0 0 0;
+    border-top: solid 1px #CCC;
+    text-transform: uppercase;
+}
+
+h1 {
+    color: #DEDEDE;
+    font-family: palatino, Georgia;
+    font-size: 40px;
+    line-height: 40px;
+    font-weight: bold;
+    margin-bottom: 8px;
+    text-shadow: 1px 1px 3px #000;
+}
+
+p {
+    font-size: 15px;
+    line-height: 22px;
+}
+
+/*settings*/
+#settings {
+    font-size:12px;
+    line-height: 30px;
+}
+
+#settings h5 {
+    line-height: 24px;
+}
+
+#settings p {
+    line-height: 24px;
+}
+
+#settings .settingsGroup p {
+    padding-left: 20px;
+}
+
+/*converter*/
+
+#converter {
+    position: absolute;
+    top: 15px;
+    left: 330px;
+    border:1px solid #ccc;
+}
+
+p.dataHeaders {
+    height: 30px;
+    padding: 15px 15px 10px;
+}
+
+.textInputs {
+    border: none;
+    color: #664D63;
+    font-family: monospace;
+    font-size: 12px;
+    height: 300px;
+    line-height: 18px;
+    padding: 10px;
+    text-shadow: #DED4DD 0px 1px 0px;
+    outline: none;
+    resize: none;
+}
+
+.groupHeader {
+    width: 100%;
+    color: #000;
+    height: 45px;
+    background-color: #f1f1f1;
+}
+
+p.groupHeadline {
+    padding: 10px;
+}
+
+.groupHeader span.subhead {
+    opacity: 0.7;
+    font-size: 12px;
+}
+
+.groupHeader a {
+    color: #FF66FF;
+}
+
+#outputHeader {
+    border-top:1px solid #ccc;
+}
+
+#dataInput, #dataOutput {
+    width: 100% !important;
+}
+
+#dataSelector {
+    width: 200px;
+    line-height: 22px;
+    font-size: 12px;
+    position: relative;
+    top: -2px;
+    left: 10px;
+}
+
+#dataSelector option {
+
+}
+.form-control {
+    display: inline-block;
+    width:200px;
+    height:28px;
+}

+ 79 - 0
apps/excel2json/index.html

@@ -0,0 +1,79 @@
+<!DOCTYPE HTML>
+<html lang="zh-CN">
+<head>
+    <title>Excel/CVS转JSON</title>
+    <meta charset="UTF-8">
+    <link rel="shortcut icon" href="../static/img/favicon.ico">
+    <link rel="stylesheet" href="index.css" />
+</head>
+<body>
+
+<div class="wrapper" id="pageContainer">
+    <div class="panel panel-default" style="margin-bottom: 0px;">
+        <div class="panel-heading">
+            <h3 class="panel-title">
+                <a href="https://www.baidufe.com/fehelper/index/index.html" target="_blank" class="x-a-high">
+                    <img src="../static/img/fe-16.png" alt="fehelper"/> FeHelper</a>:Excel/CVS转JSON
+            </h3>
+        </div>
+    </div>
+    <div class="panel-body mod-endecode">
+        <div id='base'>
+            <div id='header'>
+
+                <div id='settings'>
+                    <h3>输入配置</h3>
+                    <form id='settingsForm'>
+                        <div><label><input class="settingsElement" type="checkbox" name="" value="" id="headersProvidedCB" checked/>首行为标题</label>
+                        (<span class="settingsGroup">表头转换:
+                            <label><input class="settingsElement" type="radio" name="headerModifications" value="downcase" id='headersDowncase'/>小写&nbsp;&nbsp;</label>
+                            <label><input class="settingsElement" type="radio" name="headerModifications" id='headersUpcase' value="upcase"/> 大写&nbsp;&nbsp;</label>
+                            <label><input class="settingsElement" type="radio" name="headerModifications" id='headersNoTransform' value="none" checked/> 无</label>
+                        </span>)</div>
+
+                        <div>字段分隔符:
+                            <label><input class="settingsElement" type="radio" name="delimiter" id='delimiterAuto' value="auto" checked/>自动&nbsp;&nbsp;</label>
+                            <label><input class="settingsElement" type="radio" name="delimiter" id='delimiterComma' value="comma"/>逗号&nbsp;&nbsp;</label>
+                            <label><input class="settingsElement" type="radio" name="delimiter" id='delimiterTab' value="tab"/>Tab键</label>
+                        </div>
+                        <div>数字分隔符:
+                            <label><input class="settingsElement" type="radio" name="decimal" id='decimalDot' value="dot" checked/>点&nbsp;&nbsp;</label>
+                            <label><input class="settingsElement" type="radio" name="decimal" id='decimalComma' value="comma"/>逗号</label>
+                        </div>
+                    </form>
+
+                    <h3 style="margin-top:40px">输出配置</h3>
+                    <form id="outSettingsForm">
+                        <div>
+                            <label><input class="settingsElement" type="checkbox" name="some_name" value="" id="includeWhiteSpaceCB" checked/>格式调整</label>
+                            (缩进:
+                            <label><input class="settingsElement" type="radio" name="indentType" value="tabs" id='includeWhiteSpaceTabs'/>tab键&nbsp;&nbsp;</label>
+                            <label><input class="settingsElement" type="radio" name="indentType" value="spaces" id='includeWhiteSpaceSpaces' checked/>空格</label>)
+                        </div>
+
+                        <div class="settingsGroup">
+                            <div>
+                            </div>
+                            <div><label>XML节点定义:</label>(仅针对XML结果有效)
+                                <div><label>根节点:<input class="settingsElement form-control" type="text" name="indentType" value="root" id='root'/></label></div>
+                                <div><label>子节点:<input class="settingsElement form-control" type="text" name="indentType" value="row" id='child'/></label></div>
+                            </div>
+                        </div>
+                    </form>
+                </div>
+            </div>
+
+            <div id='converter' class=''></div>
+        </div>
+    </div>
+</div>
+
+<script type="text/javascript" src="../static/vendor/evalCore.min.js"></script>
+<script src="../static/vendor/jquery/jquery-3.3.1.min.js"></script>
+<script type="text/javascript" src="CSVParser.js"></script>
+<script type="text/javascript" src="DataGridRenderer.js"></script>
+<script type="text/javascript" src="converter.js"></script>
+<script type="text/javascript" src="index.js"></script>
+
+</body>
+</html>

+ 104 - 0
apps/excel2json/index.js

@@ -0,0 +1,104 @@
+/* code here... */
+
+var _gaq = _gaq || [];
+
+var widthOffset = 375;
+var heightOffset = 90
+
+
+/**
+ * @type {DataConverter}
+ * 对数据内容进行转换
+ */
+var d = new DataConverter('converter');
+
+var sidebar = $('#header');
+
+var win = $(window);
+var base = $('#pageContainer');
+var w = base.width() - widthOffset;
+var h = win.height() - heightOffset;
+
+//重载页面,解决无法显示textarea问题
+d.create(w, h);
+d.resize(w, h);
+sidebar.height(h);
+
+$(".settingsElement").change(updateSettings);
+
+
+/**
+ * win发生窗口变化的时候,验证窗口的高宽
+ * 修正sidebar的高宽
+ */
+$(window).bind('resize', function () {
+
+    w = base.width() - widthOffset;
+    h = win.height() - heightOffset;
+    d.resize(w, h);
+    sidebar.height(h);
+
+});
+
+
+/**
+ * 监听dom树,修改设置内容
+ * 定界符
+ * 第一行标题
+ * 输出格式内容
+ * @param evt
+ */
+function updateSettings(evt) {
+
+    if (evt) {
+        _gaq.push(['_trackEvent', 'Settings', evt.currentTarget.id]);
+    }
+
+    d.includeWhiteSpace = $('#includeWhiteSpaceCB').prop('checked');
+
+    if (d.includeWhiteSpace) {
+        $("input[name=indentType]").removeAttr("disabled");
+        var indentType = $('input[name=indentType]:checked').val();
+        if (indentType === "tabs") {
+            d.indent = "\t";
+        } else if (indentType === "spaces") {
+            d.indent = "  "
+        }
+    } else {
+        $("input[name=indentType]").attr("disabled", "disabled");
+    }
+
+    d.headersProvided = $('#headersProvidedCB').prop('checked');
+
+    if (d.headersProvided) {
+        $("input[name=headerModifications]").removeAttr("disabled");
+
+        var hm = $('input[name=headerModifications]:checked').val();
+        if (hm === "downcase") {
+            d.downcaseHeaders = true;
+            d.upcaseHeaders = false;
+        } else if (hm === "upcase") {
+            d.downcaseHeaders = false;
+            d.upcaseHeaders = true;
+        } else if (hm === "none") {
+            d.downcaseHeaders = false;
+            d.upcaseHeaders = false;
+        }
+    } else {
+        $("input[name=headerModifications]").attr("disabled", "disabled");
+    }
+
+    d.delimiter = $('input[name=delimiter]:checked').val();
+    d.decimal = $('input[name=decimal]:checked').val();
+
+    d.useUnderscores = true;
+
+
+    d.root = $('#root').val();
+    d.child = $('#child').val();
+
+    d.convert();
+};
+
+updateSettings();
+  

+ 97 - 0
apps/grid-ruler/content-script.css

@@ -0,0 +1,97 @@
+#fe-helper-box {
+    position: fixed;
+    left: 1px;
+    bottom: 0;
+    right:8px;
+    z-index: 2147483646;
+    font-size:12px;
+}
+#fe-helper-grid {
+    position:fixed;
+    top:0;
+    left:0;
+    z-index:2147483647;
+    opacity:0.03;
+    overflow:hidden;
+    -webkit-user-select:none;
+    background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAADCAYAAACTWi8uAAAANElEQVQYGWP8Jij7n4EIwPnuERGqGBiYwKrQjUTnE2UURBHEQEYGBgZkQ0B8EEAWgwoRogAZUQgA7keT1AAAAABJRU5ErkJggg==) repeat;
+}
+#fe-helper-btn-close-grid {
+    position:fixed;
+    bottom:10px;
+    right:10px;
+    font-size:12px;
+    font-weight:bold;
+    color:#00f;
+    z-index:2147483647;
+    cursor:pointer;
+}
+#fe-helper-btn-close-grid:hover {
+    color:#f00;
+}
+#fe-helper-g-pos {
+    background:none;
+    position:absolute;
+    top:0;
+    left:0;
+    z-index:2147483646;
+    border:1px solid #0b0;
+    border-width:0 1px 1px 0;
+}
+#fe-helper-gp-info {
+    position:absolute;
+    z-index:2147483646;
+    background:#ffc;
+    border:1px solid #666;
+    font-size:12px;
+    text-align:left;
+    padding:2px 10px;
+    display:none;
+    color:#000;
+}
+#fe-helper-ruler-top {
+    position:fixed;
+    top:0;
+    left:0;
+    right:0;
+    height:30px;
+    background:#fc0;
+    border-bottom:1px solid #000;
+    z-index:2147483647;
+    overflow:hidden;
+}
+#fe-helper-ruler-left {
+    position:fixed;
+    top:0;
+    left:0;
+    bottom:0;
+    width:30px;
+    background:#fc0;
+    border-right:1px solid #000;
+    z-index:2147483647;
+    overflow:hidden;
+}
+#fe-helper-ruler-top .h-line{
+    position:absolute;
+    width:1px;
+    background:#000;
+}
+#fe-helper-ruler-top .h-text{
+    position:absolute;
+    top:0;
+    font-size:8px;
+    color:#000;
+}
+#fe-helper-ruler-left .v-line{
+    position:absolute;
+    height:1px;
+    background:#000;
+}
+#fe-helper-ruler-left .v-text{
+    position:absolute;
+    left:0;
+    font-size:8px;
+    color:#000;
+    -webkit-transform:rotate(90deg);
+    margin-top:4px;
+}

+ 176 - 0
apps/grid-ruler/content-script.js

@@ -0,0 +1,176 @@
+/**
+ * 栅格相关处理
+ * @author zhaoxianlie
+ */
+window.gridrulerContentScript = function () {
+
+    // 创建栅格
+    let _createGrid = function () {
+
+        let box = jQuery('#fe-helper-box');
+        if (box[0]) {	//已经有栅格,则移除
+            box.remove();
+        }
+        //没有栅格,则创建
+        let $win = jQuery(window);
+        let $body = jQuery('body');
+        jQuery('<div id="fe-helper-box"></div>').appendTo('body').css({
+            position: 'static'
+        });
+        jQuery('<div id="fe-helper-grid"></div>').appendTo('#fe-helper-box').css({
+            width: $body.width(),
+            height: Math.max($win.height(), $body.height())
+        }).mousemove(function (e) {
+            let pos = {};
+            try {
+                pos = document.getElementsByTagName('body')[0].getBoundingClientRect();
+            } catch (err) {
+                pos = {left: 0, top: 0};
+            }
+            //虚线框
+            jQuery('#fe-helper-g-pos').show().css({
+                width: e.pageX - pos.left,
+                height: e.pageY
+            });
+
+            let _t = Math.min(e.pageY, jQuery(window).height() + $body.scrollTop() - 40);
+            let _l = Math.min(e.pageX, jQuery(window).width() + $body.scrollLeft() - 200) + 5 - pos.left;
+
+            //坐标tooltip
+            jQuery('#fe-helper-gp-info').show().css({
+                top: _t,
+                left: _l
+            }).html('top = ' + e.pageY + ' px ,left = ' + e.pageX + ' px');
+        }).mouseout(function (e) {
+            jQuery('#fe-helper-g-pos,#fe-helper-gp-info').hide();
+        });
+
+        jQuery('<div id="fe-helper-g-pos"></div><div id="fe-helper-gp-info"></div>').appendTo('#fe-helper-box');
+        jQuery('<span id="fe-helper-btn-close-grid">关闭栅格层</span>')
+            .appendTo('#fe-helper-box').click(function () {
+            jQuery('#fe-helper-box').remove();
+        });
+    };
+
+    // 绘制Ruler
+    let _drawRuler = function () {
+
+        let _t = 0, _h = 30, _w = 30;
+        let $win = jQuery(window);
+        let $page = jQuery('html');
+        let elScroll = jQuery(document.scrollingElement || 'html');
+        let $width = Math.max($win.width(), $page.width(), elScroll[0].scrollWidth);
+        let $height = Math.max($win.height(), $page.height(), elScroll[0].scrollHeight);
+        let rulerTop = jQuery('#fe-helper-ruler-top').width($width);
+        let rulerLeft = jQuery('#fe-helper-ruler-left').height($height);
+
+        if (!rulerTop.children().length || rulerTop.children().last().position().left < $width - 50) {
+            rulerTop.html('');
+            for (let i = 30; i <= $width; i += 10) {
+                _t = (i % 50) ? 10 : 0;
+                jQuery('<div class="h-line"></div>').appendTo('#fe-helper-ruler-top').css({
+                    left: i - 1,
+                    top: _t + 15,
+                    height: _h - _t - 5
+                });
+                if (_t === 0 && i !== 0) {
+                    jQuery('<div class="h-text">' + i + '</div>').appendTo('#fe-helper-ruler-top').css({
+                        left: i - (String(i).length * 4)
+                    });
+                }
+            }
+        }
+
+        if (!rulerLeft.children().length || rulerLeft.children().last().position().top < $height - 50) {
+            rulerLeft.html('');
+            for (let i = 0; i <= $height; i += 10) {
+                _l = (i % 50) ? 10 : 0;
+                jQuery('<div class="v-line"></div>').appendTo('#fe-helper-ruler-left').css({
+                    left: _l + 15,
+                    top: i - 1,
+                    width: _w - _l - 5
+                });
+                if (_l === 0) {
+                    jQuery('<div class="v-text">' + i + '</div>').appendTo('#fe-helper-ruler-left').css({
+                        top: i - (String(i).length * 4),
+                        left: i === 0 ? 5 : 0
+                    });
+                }
+            }
+        }
+    };
+
+    // 创建页面标尺
+    let _createPageRuler = function () {
+        if (!jQuery('#fe-helper-box')[0]) {
+            jQuery('<div id="fe-helper-box"></div>').appendTo('body');
+        }
+        jQuery('<div id="fe-helper-ruler-top"></div><div id="fe-helper-ruler-left"></div>').appendTo('#fe-helper-box');
+        _drawRuler();
+
+    };
+
+    // 全局事件绑定
+    let _bindEvent = function () {
+
+        //为页面注册按键监听
+        jQuery('body').keydown(function (e) {
+            if (jQuery('#fe-helper-box')[0]) {
+                if (e.which === 27) { //ESC
+                    jQuery('#fe-helper-box').remove();
+                }
+            }
+        });
+
+        //window.onresize
+        jQuery(window).resize(function () {
+            if (jQuery('#fe-helper-box')[0]) {
+                let $win = jQuery(window);
+                let $body = jQuery('body');
+                jQuery('#fe-helper-grid').css({
+                    width: Math.max($win.width(), $body.width()),
+                    height: Math.max($win.height(), $body.height())
+                });
+
+                _drawRuler();
+            }
+        });
+
+        //处理scroll的时候,标尺跟着移动
+        jQuery(window).scroll(function (e) {
+            if (jQuery('#fe-helper-box')[0]) {
+                let elScroll = jQuery(document.scrollingElement || 'html');
+                //水平标尺定位
+                jQuery('#fe-helper-ruler-top').css('left', 0 - elScroll.scrollLeft());
+                //垂直标尺
+                jQuery('#fe-helper-ruler-left').css('top', 0 - elScroll.scrollTop());
+            }
+        });
+    };
+
+    let cssInjected = false;
+    /**
+     * 执行栅格系统检测
+     */
+    window.gridrulerNoPage = function (tabInfo) {
+
+        // 提前注入css
+        if(!cssInjected) {
+            chrome.runtime.sendMessage({
+                type: 'fh-dynamic-any-thing',
+                thing:'inject-content-css',
+                tool: 'grid-ruler'
+            });
+        }
+
+        //创建栅格
+        _createGrid();
+
+        //创建页面标尺
+        _createPageRuler();
+
+        // 事件绑定
+        _bindEvent();
+    };
+
+};

+ 11 - 0
apps/grid-ruler/index.html

@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <title>FeHelper-网页栅格系统</title>
+    <link rel="stylesheet" href="content-script.css">
+</head>
+<body>
+<script src="content-script.js"></script>
+</body>
+</html>

La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 201 - 0
apps/html2markdown/demo-tpl.js


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 77 - 0
apps/html2markdown/editor.css


+ 187 - 0
apps/html2markdown/index.css

@@ -0,0 +1,187 @@
+@import url("../static/css/bootstrap.min.css");
+@import url("editor.css");
+
+#pageContainer>.panel-default {
+    margin:0;
+}
+
+#srcText {
+    height: 150px;
+}
+#rstCode {
+    height: 330px;
+    width: 100%;
+    border:none;
+    padding: 5px;
+    resize:vertical;
+    background:#000;
+    color:#fff;
+}
+
+#rstCode::selection {
+    background: #faf2cc ;
+}
+
+.ui-ml-05 {
+    margin-left: 5px;
+}
+.x-preview {
+    height: auto;
+    word-break: break-all;
+    white-space: pre-wrap;
+}
+.x-xdemo,.x-switch {
+    margin-left: 30px;
+    font-size: 12px;
+    color: blue;
+    cursor: pointer;
+    text-decoration: underline;
+}
+.x-xdemo:hover,.x-switch:hover {
+    color: #cc8841;
+}
+.x-switch {
+    color:#f00;
+    border-bottom: 1px solid #f00;
+}
+
+
+.tbl-editor {
+    position: fixed;
+    top: 0;
+    left: 0;
+    right: 0;
+    bottom: 0;
+    width: 100%;
+    background: #fff;
+}
+
+#title {
+    width: 320px;
+    padding: 0 5px;
+    height: 24px;
+}
+
+.mod-toolbar {
+    position: absolute;
+    left: 0px;
+    right: 0px;
+    top: 46px;
+    background: #f9f9f9;
+    border: 1px solid #ddd;
+}
+
+.mod-editor, .markdown-body{
+    position: absolute;
+    top: 87px;
+    bottom: 0;
+}
+
+.mod-editor {
+    overflow: auto;
+    font-size: 14px;
+    left: 0;
+    right: 50%;
+}
+
+.CodeMirror-scroll {
+    height: 100%;
+    min-height: 100%;
+}
+
+.markdown-body {
+    overflow: auto;
+    padding: 10px;
+    padding-left: 20px;
+    left: 50%;
+    right: 0;
+    box-shadow: inset 1px -5px 10px #ddd
+}
+
+.blog-aside {
+    position: fixed;
+    top: 50px;
+    left: 0;
+    right: 0;
+    bottom: 0;
+    z-index: 100;
+    padding: 20px;
+    background: rgba(0, 0, 0, .5);
+    border: 1px solid #ddd;
+    font-size: 14px;
+}
+
+.as-container {
+    background: #f1f1f1;
+    width: 800px;
+    margin: 50px auto 0;
+    padding: 20px;
+    border-radius: 6px;
+    position: relative;
+}
+
+fieldset {
+    padding: 0px 10px 5px;
+    border: 1px solid #ccc;
+    line-height: 28px;
+}
+
+.as-item {
+    margin: 0 0 5px;
+    position: relative;
+}
+
+
+.mod-pageheader, #editor, .mod-footer {
+    display: none;
+}
+.mod-markdown.preview-closed .markdown-body {
+    display:none;
+}
+.mod-markdown.preview-closed .mod-editor {
+    right:0;
+}
+.mod-markdown.mode-h2m .editor-toolbar .x-mask {
+    position: absolute;
+    top:0;
+    left:0;
+    right:0;
+    bottom:0;
+    background: #eee;
+    opacity: 0.3;
+    width: 356px;
+}
+.mod-markdown.mode-h2m textarea {
+    position: absolute;
+    top: 0;
+    left: 0;
+    right: 0;
+    bottom: 0;
+    width: 100%;
+    resize: none;
+    border: none;
+    background: transparent;
+    outline: none;
+    font-size: 14px;
+    padding: 15px;
+}
+
+.mod-markdown.mode-h2m .icon-bold,
+.mod-markdown.mode-h2m .icon-italic,
+.mod-markdown.mode-h2m .icon-quote,
+.mod-markdown.mode-h2m .icon-unordered-list,
+.mod-markdown.mode-h2m .icon-ordered-list,
+.mod-markdown.mode-h2m .icon-link,
+.mod-markdown.mode-h2m .icon-image,
+.mod-markdown.mode-h2m .icon-play,
+.mod-markdown.mode-h2m .icon-music,
+.mod-markdown.mode-h2m .icon-contract,
+.mod-markdown.mode-h2m .icon-fullscreen,
+.mod-markdown.mode-h2m .icon-question,
+.mod-markdown.mode-h2m .icon-info,
+.mod-markdown.mode-h2m .icon-undo,
+.mod-markdown.mode-h2m .icon-redo,
+.mod-markdown.mode-h2m .icon-code,
+.mod-markdown.mode-h2m .icon-preview {
+    opacity: 0.2;
+}

+ 75 - 0
apps/html2markdown/index.html

@@ -0,0 +1,75 @@
+<!DOCTYPE HTML>
+<html lang="zh-CN">
+<head>
+    <title>Markdown编辑器(支持HTML转Markdown)</title>
+    <meta charset="UTF-8">
+    <link rel="shortcut icon" href="../static/img/favicon.ico">
+    <link rel="stylesheet" href="index.css" />
+    <script type="text/javascript" src="../static/vendor/evalCore.min.js"></script>
+    <script type="text/javascript" src="../static/vendor/vue/vue.js"></script>
+</head>
+<body>
+
+<div class="wrapper" id="pageContainer">
+    <div class="panel panel-default" style="margin-bottom: 0px;">
+        <div class="panel-heading">
+            <h3 class="panel-title">
+                <a href="https://www.baidufe.com/fehelper/index/index.html" target="_blank" class="x-a-high">
+                    <img src="../static/img/fe-16.png" alt="fehelper"/> FeHelper</a>:{{toolName[codeType]}}
+                <span class="x-xdemo" ref="demoLink1" @click="setDemo">{{codeType}}是什么?</span>
+                <span class="x-xdemo" ref="importLink" @click="importContent">导入{{codeType}}文件</span>
+
+                <span class="x-switch ui-fl-r" ref="btnSwitch" @click="trans">{{toolName[nextCodeType]}}&gt;&gt;</span>
+            </h3>
+        </div>
+    </div>
+    <div class="panel-body mod-markdown" ref="modMarkdownBox">
+
+        <div class="mod-toolbar">
+            <div class="editor-toolbar">
+                <a class="icon-bold" @click="insert('b')" title="加粗"></a>
+                <a class="icon-italic" @click="insert('i')" title="斜体"></a>
+                <i class="separator">|</i>
+                <a class="icon-quote" @click="insert('quote')" title="备注"></a>
+                <a class="icon-code" @click="insert('code')" title="代码"></a>
+                <i class="separator">|</i>
+                <a class="icon-unordered-list" @click="insert('unordered-list')" title="无序列表"></a>
+                <a class="icon-ordered-list" @click="insert('ordered-list')" title="有序列表"></a>
+                <i class="separator">|</i>
+                <a class="icon-link" @click="insert('link')" title="链接"></a>
+                <a class="icon-image" @click="insert('image')" title="图片"></a>
+                <i class="separator">|</i>
+                <a class="icon-preview" @click="togglePreview" title="实时预览切换"></a>
+                <a class="icon-save" @click="saveMarkdown('md')" title="下载Markdown内容"></a>
+                <a class="icon-pdf" @click="saveMarkdown('html')" title="下载HTML内容"></a>
+
+                <div class="x-mask"></div>
+            </div>
+        </div>
+        <div class="mod-editor">
+            <textarea id="editor" ref="elEditor"></textarea>
+        </div>
+        <div class="markdown-body" ref="boxPreview"></div>
+    </div>
+
+</div>
+
+<script type="text/javascript" src="../static/vendor/codemirror/codemirror.js"></script>
+<script type="text/javascript" src="../static/vendor/codemirror/overlay.js"></script>
+<script type="text/javascript" src="../static/vendor/codemirror/markdown.js"></script>
+<script type="text/javascript" src="../static/vendor/codemirror/gfm.js"></script>
+<script type="text/javascript" src="../static/vendor/codemirror/xml.js"></script>
+<script type="text/javascript" src="../static/vendor/codemirror/javascript.js"></script>
+<script type="text/javascript" src="../static/vendor/codemirror/css.js"></script>
+<script type="text/javascript" src="../static/vendor/codemirror/htmlmixed.js"></script>
+<script type="text/javascript" src="../static/vendor/codemirror/clike.js"></script>
+<script type="text/javascript" src="../static/vendor/codemirror/meta.js"></script>
+<script type="text/javascript" src="../static/vendor/h2m/h2m.js"></script>
+<script type="text/javascript" src="../static/vendor/highlight/highlight.js"></script>
+<script type="text/javascript" src="libs/marked.js"></script>
+<script type="text/javascript" src="libs/rawdeflate.js"></script>
+<script type="text/javascript" src="libs/rawinflate.js"></script>
+<script type="text/javascript" src="demo-tpl.js"></script>
+<script type="text/javascript" src="index.js"></script>
+</body>
+</html>

+ 256 - 0
apps/html2markdown/index.js

@@ -0,0 +1,256 @@
+/**
+ * FeHelper HTML转Markdown
+ */
+
+
+let editor = null;
+let hashtoTimeoutId;
+let previewElm;
+let previewTextArea;
+
+new Vue({
+    el: '#pageContainer',
+    data: {
+        showPreview: false,
+        previewText: '效果预览',
+        codeType: 'Markdown',
+        nextCodeType: 'HTML',
+        toolName: {
+            HTML: 'HTML转Markdown',
+            Markdown: 'Markdown编辑器'
+        }
+    },
+
+    mounted: function () {
+        this.init();
+    },
+    methods: {
+
+        trans: function () {
+            editor.setValue('');
+
+            this.codeType = {HTML: 'Markdown', Markdown: 'HTML'}[this.codeType];
+            this.nextCodeType = {HTML: 'Markdown', Markdown: 'HTML'}[this.nextCodeType];
+
+            let classList = this.$refs.modMarkdownBox.classList;
+            if (this.codeType === 'HTML') {
+                classList.add('mode-h2m');
+                previewElm.innerHTML = `<textarea disabled></textarea>`;
+                previewTextArea = previewElm.querySelector('textarea');
+            } else {
+                classList.remove('mode-h2m');
+                previewElm.innerHTML = '';
+            }
+        },
+
+        /**
+         * 初始化ctrl+s的保存
+         */
+        init() {
+
+            previewElm = this.$refs.boxPreview;
+
+            // ===========================editor初始化
+            editor = CodeMirror.fromTextArea(this.$refs.elEditor, {
+                mode: "gfm",
+                lineNumbers: true,
+                matchBrackets: true,
+                lineWrapping: true,
+                theme: 'default'
+            });
+            editor.on('change', this.updateHashAndPreview);
+
+            // ===========================支持save-as
+            window.URL = window.URL || window.webkitURL || window.mozURL || window.msURL;
+            navigator.saveBlob = navigator.saveBlob || navigator.msSaveBlob || navigator.mozSaveBlob || navigator.webkitSaveBlob;
+            window.saveAs = window.saveAs || window.webkitSaveAs || window.mozSaveAs || window.msSaveAs;
+
+            document.addEventListener('keydown', (e) => {
+                if (e.keyCode === 83 && (e.ctrlKey || e.metaKey)) {
+                    e.preventDefault();
+                    this.saveMarkdown('md');
+                    return false;
+                }
+            });
+
+            // ===========================支持页面拖拽识别
+            document.addEventListener('drop', function (e) {
+                e.preventDefault();
+                e.stopPropagation();
+
+                let theFile = e.dataTransfer.files[0];
+                let theReader = new FileReader();
+                theReader.onload = function (e) {
+                    editor.setValue(e.target.result);
+                };
+
+                theReader.readAsText(theFile);
+            }, false);
+
+            this.initWithHash();
+        },
+
+        /**
+         * 根据Hash进行更新编辑器
+         */
+        initWithHash() {
+            if (this.codeType === 'HTML') return;
+
+            if (window.location.hash) {
+                let h = window.location.hash.replace(/^#/, '');
+                if (h.slice(0, 5) === 'view:') {
+                    let val = decodeURIComponent(escape(RawDeflate.inflate(atob(h.slice(5)))));
+                    previewElm.innerHTML = marked(val);
+                    previewElm.querySelectorAll('pre code').forEach((block) => {
+                        hljs.highlightBlock(block);
+                    });
+                    document.body.className = 'view';
+                } else {
+                    editor.setValue(decodeURIComponent(escape(RawDeflate.inflate(atob(h)))));
+                    this.updateHashAndPreview(editor);
+                    editor.focus();
+                }
+            } else {
+                this.updateHashAndPreview(editor);
+                editor.focus();
+            }
+        },
+
+        /**
+         * 更新预览区域
+         */
+        updateHashAndPreview() {
+            try {
+                if (this.codeType === 'HTML') {
+                    previewTextArea.value = h2m(editor.getValue(), {
+                        converter: 'CommonMark' // CommonMark | MarkdownExtra
+                    });
+                } else {
+                    previewElm.innerHTML = marked(editor.getValue());
+                    previewElm.querySelectorAll('pre code').forEach((block) => {
+                        hljs.highlightBlock(block);
+                    });
+                    clearTimeout(hashtoTimeoutId);
+                    hashtoTimeoutId = setTimeout(function () {
+                        window.location.hash = btoa(RawDeflate.deflate(unescape(encodeURIComponent(editor.getValue()))))
+                    }, 1000);
+                }
+            } catch (e) {
+                console.log(e);
+            }
+        },
+
+        saveMarkdown(type) {
+
+            let date = new Date();
+            let name = "FH-" + date.getFullYear() + (date.getMonth() + 1) + date.getDate()
+                + date.getHours() + date.getMinutes() + date.getSeconds() + `.${type}`;
+
+            let code = editor.getValue();
+            if (this.codeType === 'HTML') {
+                if (type !== 'html') {
+                    code = previewTextArea.value;
+                }
+            } else {
+                if (type === 'html') {
+                    code = DemoTpl.exportHtml.replace('#title#', name).replace('#style#', DemoTpl.exportCss).replace('#html#', this.getParsedHtml());
+                }
+            }
+
+            let blob = new Blob([code], {type: type === 'md' ? 'text/plain' : 'text/html'});
+
+            if (window.saveAs) {
+                window.saveAs(blob, name);
+            } else if (navigator.saveBlob) {
+                navigator.saveBlob(blob, name);
+            } else {
+                let url = URL.createObjectURL(blob);
+                let link = document.createElement("a");
+                link.setAttribute("href", url);
+                link.setAttribute("download", name);
+                let event = document.createEvent('MouseEvents');
+                event.initMouseEvent('click', true, true, window, 1, 0, 0, 0, 0, false, false, false, false, 0, null);
+                link.dispatchEvent(event);
+            }
+        },
+
+        /**
+         * 获取编译后的
+         * @returns {*}
+         */
+        getParsedHtml() {
+            return previewElm.innerHTML;
+        },
+
+        insert(type) {
+            let textConfig = {
+                b: '** text-here **',
+                i: '* text-here *',
+                quote: '\n> text-here ',
+                code: '\n```javascript\n\n\n```\n',
+                'unordered-list': '\n\n- text-here\n- text-here\n- text-here\n',
+                'ordered-list': '\n\n1. text-here\n2. text-here\n3. text-here\n',
+                link: '\n[text-here](your-link-url)',
+                image: '\n![text-here](your-image-src)'
+            };
+            editor.replaceSelection(textConfig[type] || '');
+            editor.focus();
+        },
+
+        getResult: function () {
+            this.$refs.rstCode.select();
+        },
+
+        setDemo: function () {
+            editor.setValue(DemoTpl[this.codeType.toLowerCase()]);
+        },
+
+        // 导入内容
+        importContent: function () {
+            let that = this;
+            let fileInput = document.getElementById('fileInput');
+            if (!fileInput) {
+                fileInput = document.createElement('input');
+                fileInput.id = 'fileInput';
+                fileInput.type = 'file';
+                fileInput.accept = {HTML: 'text/html', Markdown: 'text/x-markdown'}[that.codeType];
+                fileInput.style.cssText = 'position:relative;top:-1000px;left:-1000px;';
+                fileInput.onchange = function (event) {
+                    let reader = new FileReader();
+                    reader.readAsText(fileInput.files[0], 'utf-8');
+                    reader.onload = (evt) => {
+                        editor.setValue(evt.target.result);
+                        document.body.removeChild(fileInput);
+                    };
+                };
+                document.body.appendChild(fileInput);
+            }
+            fileInput.click();
+        },
+
+        togglePreview() {
+            let classList = this.$refs.modMarkdownBox.classList;
+            let closeClass = 'preview-closed';
+            if (classList.contains(closeClass)) {
+                classList.remove(closeClass);
+            } else {
+                classList.add(closeClass);
+            }
+        },
+
+        // 通过调用系统打印的形式,打印为pdf
+        exportContent: function (previewMode) {
+            let newContent = "<html><head><meta charset='utf-8'/><title></title>" +
+                "<style>" + DemoTpl.printCss + "</style>" +
+                "</head><body class='markdown-body'>" + this.getParsedHtml() + "</body></html>";
+            let newWin = window.open();
+            newWin.focus();
+            newWin.document.write(newContent);
+            if (!previewMode) {
+                newWin.print();
+                newWin.document.close();
+                newWin.close();
+            }
+        }
+    }
+});

+ 1072 - 0
apps/html2markdown/libs/marked.js

@@ -0,0 +1,1072 @@
+/**
+ * marked - a markdown parser
+ * Copyright (c) 2011-2013, Christopher Jeffrey. (MIT Licensed)
+ * https://github.com/chjj/marked
+ */
+
+;(function() {
+
+/**
+ * Block-Level Grammar
+ */
+
+var block = {
+  newline: /^\n+/,
+  code: /^( {4}[^\n]+\n*)+/,
+  fences: noop,
+  hr: /^( *[-*_]){3,} *(?:\n+|$)/,
+  heading: /^ *(#{1,6}) *([^\n]+?) *#* *(?:\n+|$)/,
+  nptable: noop,
+  lheading: /^([^\n]+)\n *(=|-){3,} *\n*/,
+  blockquote: /^( *>[^\n]+(\n[^\n]+)*\n*)+/,
+  list: /^( *)(bull) [\s\S]+?(?:hr|\n{2,}(?! )(?!\1bull )\n*|\s*$)/,
+  html: /^ *(?:comment|closed|closing) *(?:\n{2,}|\s*$)/,
+  def: /^ *\[([^\]]+)\]: *<?([^\s>]+)>?(?: +["(]([^\n]+)[")])? *(?:\n+|$)/,
+  table: noop,
+  paragraph: /^((?:[^\n]+\n?(?!hr|heading|lheading|blockquote|tag|def))+)\n*/,
+  text: /^[^\n]+/
+};
+
+block.bullet = /(?:[*+-]|\d+\.)/;
+block.item = /^( *)(bull) [^\n]*(?:\n(?!\1bull )[^\n]*)*/;
+block.item = replace(block.item, 'gm')
+  (/bull/g, block.bullet)
+  ();
+
+block.list = replace(block.list)
+  (/bull/g, block.bullet)
+  ('hr', /\n+(?=(?: *[-*_]){3,} *(?:\n+|$))/)
+  ();
+
+block._tag = '(?!(?:'
+  + 'a|em|strong|small|s|cite|q|dfn|abbr|data|time|code'
+  + '|var|samp|kbd|sub|sup|i|b|u|mark|ruby|rt|rp|bdi|bdo'
+  + '|span|br|wbr|ins|del|img)\\b)\\w+(?!:/|@)\\b';
+
+block.html = replace(block.html)
+  ('comment', /<!--[\s\S]*?-->/)
+  ('closed', /<(tag)[\s\S]+?<\/\1>/)
+  ('closing', /<tag(?:"[^"]*"|'[^']*'|[^'">])*?>/)
+  (/tag/g, block._tag)
+  ();
+
+block.paragraph = replace(block.paragraph)
+  ('hr', block.hr)
+  ('heading', block.heading)
+  ('lheading', block.lheading)
+  ('blockquote', block.blockquote)
+  ('tag', '<' + block._tag)
+  ('def', block.def)
+  ();
+
+/**
+ * Normal Block Grammar
+ */
+
+block.normal = merge({}, block);
+
+/**
+ * GFM Block Grammar
+ */
+
+block.gfm = merge({}, block.normal, {
+  fences: /^ *(`{3,}|~{3,}) *(\S+)? *\n([\s\S]+?)\s*\1 *(?:\n+|$)/,
+  paragraph: /^/
+});
+
+block.gfm.paragraph = replace(block.paragraph)
+  ('(?!', '(?!' + block.gfm.fences.source.replace('\\1', '\\2') + '|')
+  ();
+
+/**
+ * GFM + Tables Block Grammar
+ */
+
+block.tables = merge({}, block.gfm, {
+  nptable: /^ *(\S.*\|.*)\n *([-:]+ *\|[-| :]*)\n((?:.*\|.*(?:\n|$))*)\n*/,
+  table: /^ *\|(.+)\n *\|( *[-:]+[-| :]*)\n((?: *\|.*(?:\n|$))*)\n*/
+});
+
+/**
+ * Block Lexer
+ */
+
+function Lexer(options) {
+  this.tokens = [];
+  this.tokens.links = {};
+  this.options = options || marked.defaults;
+  this.rules = block.normal;
+
+  if (this.options.gfm) {
+    if (this.options.tables) {
+      this.rules = block.tables;
+    } else {
+      this.rules = block.gfm;
+    }
+  }
+}
+
+/**
+ * Expose Block Rules
+ */
+
+Lexer.rules = block;
+
+/**
+ * Static Lex Method
+ */
+
+Lexer.lex = function(src, options) {
+  var lexer = new Lexer(options);
+  return lexer.lex(src);
+};
+
+/**
+ * Preprocessing
+ */
+
+Lexer.prototype.lex = function(src) {
+  src = src
+    .replace(/\r\n|\r/g, '\n')
+    .replace(/\t/g, '    ')
+    .replace(/\u00a0/g, ' ')
+    .replace(/\u2424/g, '\n');
+
+  return this.token(src, true);
+};
+
+/**
+ * Lexing
+ */
+
+Lexer.prototype.token = function(src, top) {
+  var src = src.replace(/^ +$/gm, '')
+    , next
+    , loose
+    , cap
+    , bull
+    , b
+    , item
+    , space
+    , i
+    , l;
+
+  while (src) {
+    // newline
+    if (cap = this.rules.newline.exec(src)) {
+      src = src.substring(cap[0].length);
+      if (cap[0].length > 1) {
+        this.tokens.push({
+          type: 'space'
+        });
+      }
+    }
+
+    // code
+    if (cap = this.rules.code.exec(src)) {
+      src = src.substring(cap[0].length);
+      cap = cap[0].replace(/^ {4}/gm, '');
+      this.tokens.push({
+        type: 'code',
+        text: !this.options.pedantic
+          ? cap.replace(/\n+$/, '')
+          : cap
+      });
+      continue;
+    }
+
+    // fences (gfm)
+    if (cap = this.rules.fences.exec(src)) {
+      src = src.substring(cap[0].length);
+      this.tokens.push({
+        type: 'code',
+        lang: cap[2],
+        text: cap[3]
+      });
+      continue;
+    }
+
+    // heading
+    if (cap = this.rules.heading.exec(src)) {
+      src = src.substring(cap[0].length);
+      this.tokens.push({
+        type: 'heading',
+        depth: cap[1].length,
+        text: cap[2]
+      });
+      continue;
+    }
+
+    // table no leading pipe (gfm)
+    if (top && (cap = this.rules.nptable.exec(src))) {
+      src = src.substring(cap[0].length);
+
+      item = {
+        type: 'table',
+        header: cap[1].replace(/^ *| *\| *$/g, '').split(/ *\| */),
+        align: cap[2].replace(/^ *|\| *$/g, '').split(/ *\| */),
+        cells: cap[3].replace(/\n$/, '').split('\n')
+      };
+
+      for (i = 0; i < item.align.length; i++) {
+        if (/^ *-+: *$/.test(item.align[i])) {
+          item.align[i] = 'right';
+        } else if (/^ *:-+: *$/.test(item.align[i])) {
+          item.align[i] = 'center';
+        } else if (/^ *:-+ *$/.test(item.align[i])) {
+          item.align[i] = 'left';
+        } else {
+          item.align[i] = null;
+        }
+      }
+
+      for (i = 0; i < item.cells.length; i++) {
+        item.cells[i] = item.cells[i].split(/ *\| */);
+      }
+
+      this.tokens.push(item);
+
+      continue;
+    }
+
+    // lheading
+    if (cap = this.rules.lheading.exec(src)) {
+      src = src.substring(cap[0].length);
+      this.tokens.push({
+        type: 'heading',
+        depth: cap[2] === '=' ? 1 : 2,
+        text: cap[1]
+      });
+      continue;
+    }
+
+    // hr
+    if (cap = this.rules.hr.exec(src)) {
+      src = src.substring(cap[0].length);
+      this.tokens.push({
+        type: 'hr'
+      });
+      continue;
+    }
+
+    // blockquote
+    if (cap = this.rules.blockquote.exec(src)) {
+      src = src.substring(cap[0].length);
+
+      this.tokens.push({
+        type: 'blockquote_start'
+      });
+
+      cap = cap[0].replace(/^ *> ?/gm, '');
+
+      // Pass `top` to keep the current
+      // "toplevel" state. This is exactly
+      // how markdown.pl works.
+      this.token(cap, top);
+
+      this.tokens.push({
+        type: 'blockquote_end'
+      });
+
+      continue;
+    }
+
+    // list
+    if (cap = this.rules.list.exec(src)) {
+      src = src.substring(cap[0].length);
+      bull = cap[2];
+
+      this.tokens.push({
+        type: 'list_start',
+        ordered: bull.length > 1
+      });
+
+      // Get each top-level item.
+      cap = cap[0].match(this.rules.item);
+
+      next = false;
+      l = cap.length;
+      i = 0;
+
+      for (; i < l; i++) {
+        item = cap[i];
+
+        // Remove the list item's bullet
+        // so it is seen as the next token.
+        space = item.length;
+        item = item.replace(/^ *([*+-]|\d+\.) +/, '');
+
+        // Outdent whatever the
+        // list item contains. Hacky.
+        if (~item.indexOf('\n ')) {
+          space -= item.length;
+          item = !this.options.pedantic
+            ? item.replace(new RegExp('^ {1,' + space + '}', 'gm'), '')
+            : item.replace(/^ {1,4}/gm, '');
+        }
+
+        // Determine whether the next list item belongs here.
+        // Backpedal if it does not belong in this list.
+        if (this.options.smartLists && i !== l - 1) {
+          b = block.bullet.exec(cap[i+1])[0];
+          if (bull !== b && !(bull.length > 1 && b.length > 1)) {
+            src = cap.slice(i + 1).join('\n') + src;
+            i = l - 1;
+          }
+        }
+
+        // Determine whether item is loose or not.
+        // Use: /(^|\n)(?! )[^\n]+\n\n(?!\s*$)/
+        // for discount behavior.
+        loose = next || /\n\n(?!\s*$)/.test(item);
+        if (i !== l - 1) {
+          next = item[item.length-1] === '\n';
+          if (!loose) loose = next;
+        }
+
+        this.tokens.push({
+          type: loose
+            ? 'loose_item_start'
+            : 'list_item_start'
+        });
+
+        // Recurse.
+        this.token(item, false);
+
+        this.tokens.push({
+          type: 'list_item_end'
+        });
+      }
+
+      this.tokens.push({
+        type: 'list_end'
+      });
+
+      continue;
+    }
+
+    // html
+    if (cap = this.rules.html.exec(src)) {
+      src = src.substring(cap[0].length);
+      this.tokens.push({
+        type: this.options.sanitize
+          ? 'paragraph'
+          : 'html',
+        pre: cap[1] === 'pre',
+        text: cap[0]
+      });
+      continue;
+    }
+
+    // def
+    if (top && (cap = this.rules.def.exec(src))) {
+      src = src.substring(cap[0].length);
+      this.tokens.links[cap[1].toLowerCase()] = {
+        href: cap[2],
+        title: cap[3]
+      };
+      continue;
+    }
+
+    // table (gfm)
+    if (top && (cap = this.rules.table.exec(src))) {
+      src = src.substring(cap[0].length);
+
+      item = {
+        type: 'table',
+        header: cap[1].replace(/^ *| *\| *$/g, '').split(/ *\| */),
+        align: cap[2].replace(/^ *|\| *$/g, '').split(/ *\| */),
+        cells: cap[3].replace(/(?: *\| *)?\n$/, '').split('\n')
+      };
+
+      for (i = 0; i < item.align.length; i++) {
+        if (/^ *-+: *$/.test(item.align[i])) {
+          item.align[i] = 'right';
+        } else if (/^ *:-+: *$/.test(item.align[i])) {
+          item.align[i] = 'center';
+        } else if (/^ *:-+ *$/.test(item.align[i])) {
+          item.align[i] = 'left';
+        } else {
+          item.align[i] = null;
+        }
+      }
+
+      for (i = 0; i < item.cells.length; i++) {
+        item.cells[i] = item.cells[i]
+          .replace(/^ *\| *| *\| *$/g, '')
+          .split(/ *\| */);
+      }
+
+      this.tokens.push(item);
+
+      continue;
+    }
+
+    // top-level paragraph
+    if (top && (cap = this.rules.paragraph.exec(src))) {
+      src = src.substring(cap[0].length);
+      this.tokens.push({
+        type: 'paragraph',
+        text: cap[1][cap[1].length-1] === '\n'
+          ? cap[1].slice(0, -1)
+          : cap[1]
+      });
+      continue;
+    }
+
+    // text
+    if (cap = this.rules.text.exec(src)) {
+      // Top-level should never reach here.
+      src = src.substring(cap[0].length);
+      this.tokens.push({
+        type: 'text',
+        text: cap[0]
+      });
+      continue;
+    }
+
+    if (src) {
+      throw new
+        Error('Infinite loop on byte: ' + src.charCodeAt(0));
+    }
+  }
+
+  return this.tokens;
+};
+
+/**
+ * Inline-Level Grammar
+ */
+
+var inline = {
+  escape: /^\\([\\`*{}\[\]()#+\-.!_>])/,
+  autolink: /^<([^ >]+(@|:\/)[^ >]+)>/,
+  url: noop,
+  tag: /^<!--[\s\S]*?-->|^<\/?\w+(?:"[^"]*"|'[^']*'|[^'">])*?>/,
+  link: /^!?\[(inside)\]\(href\)/,
+  reflink: /^!?\[(inside)\]\s*\[([^\]]*)\]/,
+  nolink: /^!?\[((?:\[[^\]]*\]|[^\[\]])*)\]/,
+  strong: /^__([\s\S]+?)__(?!_)|^\*\*([\s\S]+?)\*\*(?!\*)/,
+  em: /^\b_((?:__|[\s\S])+?)_\b|^\*((?:\*\*|[\s\S])+?)\*(?!\*)/,
+  code: /^(`+)\s*([\s\S]*?[^`])\s*\1(?!`)/,
+  br: /^ {2,}\n(?!\s*$)/,
+  del: noop,
+  text: /^[\s\S]+?(?=[\\<!\[_*`]| {2,}\n|$)/
+};
+
+inline._inside = /(?:\[[^\]]*\]|[^\]]|\](?=[^\[]*\]))*/;
+inline._href = /\s*<?([^\s]*?)>?(?:\s+['"]([\s\S]*?)['"])?\s*/;
+
+inline.link = replace(inline.link)
+  ('inside', inline._inside)
+  ('href', inline._href)
+  ();
+
+inline.reflink = replace(inline.reflink)
+  ('inside', inline._inside)
+  ();
+
+/**
+ * Normal Inline Grammar
+ */
+
+inline.normal = merge({}, inline);
+
+/**
+ * Pedantic Inline Grammar
+ */
+
+inline.pedantic = merge({}, inline.normal, {
+  strong: /^__(?=\S)([\s\S]*?\S)__(?!_)|^\*\*(?=\S)([\s\S]*?\S)\*\*(?!\*)/,
+  em: /^_(?=\S)([\s\S]*?\S)_(?!_)|^\*(?=\S)([\s\S]*?\S)\*(?!\*)/
+});
+
+/**
+ * GFM Inline Grammar
+ */
+
+inline.gfm = merge({}, inline.normal, {
+  escape: replace(inline.escape)('])', '~|])')(),
+  url: /^(https?:\/\/[^\s<]+[^<.,:;"')\]\s])/,
+  del: /^~~(?=\S)([\s\S]*?\S)~~/,
+  text: replace(inline.text)
+    (']|', '~]|')
+    ('|', '|https?://|')
+    ()
+});
+
+/**
+ * GFM + Line Breaks Inline Grammar
+ */
+
+inline.breaks = merge({}, inline.gfm, {
+  br: replace(inline.br)('{2,}', '*')(),
+  text: replace(inline.gfm.text)('{2,}', '*')()
+});
+
+/**
+ * Inline Lexer & Compiler
+ */
+
+function InlineLexer(links, options) {
+  this.options = options || marked.defaults;
+  this.links = links;
+  this.rules = inline.normal;
+
+  if (!this.links) {
+    throw new
+      Error('Tokens array requires a `links` property.');
+  }
+
+  if (this.options.gfm) {
+    if (this.options.breaks) {
+      this.rules = inline.breaks;
+    } else {
+      this.rules = inline.gfm;
+    }
+  } else if (this.options.pedantic) {
+    this.rules = inline.pedantic;
+  }
+}
+
+/**
+ * Expose Inline Rules
+ */
+
+InlineLexer.rules = inline;
+
+/**
+ * Static Lexing/Compiling Method
+ */
+
+InlineLexer.output = function(src, links, options) {
+  var inline = new InlineLexer(links, options);
+  return inline.output(src);
+};
+
+/**
+ * Lexing/Compiling
+ */
+
+InlineLexer.prototype.output = function(src) {
+  var out = ''
+    , link
+    , text
+    , href
+    , cap;
+
+  while (src) {
+    // escape
+    if (cap = this.rules.escape.exec(src)) {
+      src = src.substring(cap[0].length);
+      out += cap[1];
+      continue;
+    }
+
+    // autolink
+    if (cap = this.rules.autolink.exec(src)) {
+      src = src.substring(cap[0].length);
+      if (cap[2] === '@') {
+        text = cap[1][6] === ':'
+          ? this.mangle(cap[1].substring(7))
+          : this.mangle(cap[1]);
+        href = this.mangle('mailto:') + text;
+      } else {
+        text = escape(cap[1]);
+        href = text;
+      }
+      out += '<a href="'
+        + href
+        + '">'
+        + text
+        + '</a>';
+      continue;
+    }
+
+    // url (gfm)
+    if (cap = this.rules.url.exec(src)) {
+      src = src.substring(cap[0].length);
+      text = escape(cap[1]);
+      href = text;
+      out += '<a href="'
+        + href
+        + '">'
+        + text
+        + '</a>';
+      continue;
+    }
+
+    // tag
+    if (cap = this.rules.tag.exec(src)) {
+      src = src.substring(cap[0].length);
+      out += this.options.sanitize
+        ? escape(cap[0])
+        : cap[0];
+      continue;
+    }
+
+    // link
+    if (cap = this.rules.link.exec(src)) {
+      src = src.substring(cap[0].length);
+      out += this.outputLink(cap, {
+        href: cap[2],
+        title: cap[3]
+      });
+      continue;
+    }
+
+    // reflink, nolink
+    if ((cap = this.rules.reflink.exec(src))
+        || (cap = this.rules.nolink.exec(src))) {
+      src = src.substring(cap[0].length);
+      link = (cap[2] || cap[1]).replace(/\s+/g, ' ');
+      link = this.links[link.toLowerCase()];
+      if (!link || !link.href) {
+        out += cap[0][0];
+        src = cap[0].substring(1) + src;
+        continue;
+      }
+      out += this.outputLink(cap, link);
+      continue;
+    }
+
+    // strong
+    if (cap = this.rules.strong.exec(src)) {
+      src = src.substring(cap[0].length);
+      out += '<strong>'
+        + this.output(cap[2] || cap[1])
+        + '</strong>';
+      continue;
+    }
+
+    // em
+    if (cap = this.rules.em.exec(src)) {
+      src = src.substring(cap[0].length);
+      out += '<em>'
+        + this.output(cap[2] || cap[1])
+        + '</em>';
+      continue;
+    }
+
+    // code
+    if (cap = this.rules.code.exec(src)) {
+      src = src.substring(cap[0].length);
+      out += '<code>'
+        + escape(cap[2], true)
+        + '</code>';
+      continue;
+    }
+
+    // br
+    if (cap = this.rules.br.exec(src)) {
+      src = src.substring(cap[0].length);
+      out += '<br>';
+      continue;
+    }
+
+    // del (gfm)
+    if (cap = this.rules.del.exec(src)) {
+      src = src.substring(cap[0].length);
+      out += '<del>'
+        + this.output(cap[1])
+        + '</del>';
+      continue;
+    }
+
+    // text
+    if (cap = this.rules.text.exec(src)) {
+      src = src.substring(cap[0].length);
+      out += escape(cap[0]);
+      continue;
+    }
+
+    if (src) {
+      throw new
+        Error('Infinite loop on byte: ' + src.charCodeAt(0));
+    }
+  }
+
+  return out;
+};
+
+/**
+ * Compile Link
+ */
+
+InlineLexer.prototype.outputLink = function(cap, link) {
+  if (cap[0][0] !== '!') {
+    return '<a href="'
+      + escape(link.href)
+      + '"'
+      + (link.title
+      ? ' title="'
+      + escape(link.title)
+      + '"'
+      : '')
+      + '>'
+      + this.output(cap[1])
+      + '</a>';
+  } else {
+    return '<img src="'
+      + escape(link.href)
+      + '" alt="'
+      + escape(cap[1])
+      + '"'
+      + (link.title
+      ? ' title="'
+      + escape(link.title)
+      + '"'
+      : '')
+      + '>';
+  }
+};
+
+/**
+ * Mangle Links
+ */
+
+InlineLexer.prototype.mangle = function(text) {
+  var out = ''
+    , l = text.length
+    , i = 0
+    , ch;
+
+  for (; i < l; i++) {
+    ch = text.charCodeAt(i);
+    if (Math.random() > 0.5) {
+      ch = 'x' + ch.toString(16);
+    }
+    out += '&#' + ch + ';';
+  }
+
+  return out;
+};
+
+/**
+ * Parsing & Compiling
+ */
+
+function Parser(options) {
+  this.tokens = [];
+  this.token = null;
+  this.options = options || marked.defaults;
+}
+
+/**
+ * Static Parse Method
+ */
+
+Parser.parse = function(src, options) {
+  var parser = new Parser(options);
+  return parser.parse(src);
+};
+
+/**
+ * Parse Loop
+ */
+
+Parser.prototype.parse = function(src) {
+  this.inline = new InlineLexer(src.links, this.options);
+  this.tokens = src.reverse();
+
+  var out = '';
+  while (this.next()) {
+    out += this.tok();
+  }
+
+  return out;
+};
+
+/**
+ * Next Token
+ */
+
+Parser.prototype.next = function() {
+  return this.token = this.tokens.pop();
+};
+
+/**
+ * Preview Next Token
+ */
+
+Parser.prototype.peek = function() {
+  return this.tokens[this.tokens.length-1] || 0;
+};
+
+/**
+ * Parse Text Tokens
+ */
+
+Parser.prototype.parseText = function() {
+  var body = this.token.text;
+
+  while (this.peek().type === 'text') {
+    body += '\n' + this.next().text;
+  }
+
+  return this.inline.output(body);
+};
+
+/**
+ * Parse Current Token
+ */
+
+Parser.prototype.tok = function() {
+  switch (this.token.type) {
+    case 'space': {
+      return '';
+    }
+    case 'hr': {
+      return '<hr>\n';
+    }
+    case 'heading': {
+      return '<h'
+        + this.token.depth
+        + '>'
+        + this.inline.output(this.token.text)
+        + '</h'
+        + this.token.depth
+        + '>\n';
+    }
+    case 'code': {
+      if (this.options.highlight) {
+        var code = this.options.highlight(this.token.text, this.token.lang);
+        if (code != null && code !== this.token.text) {
+          this.token.escaped = true;
+          this.token.text = code;
+        }
+      }
+
+      if (!this.token.escaped) {
+        this.token.text = escape(this.token.text, true);
+      }
+
+      return '<pre><code'
+        + (this.token.lang
+        ? ' class="'
+        + this.options.langPrefix
+        + this.token.lang
+        + '"'
+        : '')
+        + '>'
+        + this.token.text
+        + '</code></pre>\n';
+    }
+    case 'table': {
+      var body = ''
+        , heading
+        , i
+        , row
+        , cell
+        , j;
+
+      // header
+      body += '<thead>\n<tr>\n';
+      for (i = 0; i < this.token.header.length; i++) {
+        heading = this.inline.output(this.token.header[i]);
+        body += this.token.align[i]
+          ? '<th align="' + this.token.align[i] + '">' + heading + '</th>\n'
+          : '<th>' + heading + '</th>\n';
+      }
+      body += '</tr>\n</thead>\n';
+
+      // body
+      body += '<tbody>\n'
+      for (i = 0; i < this.token.cells.length; i++) {
+        row = this.token.cells[i];
+        body += '<tr>\n';
+        for (j = 0; j < row.length; j++) {
+          cell = this.inline.output(row[j]);
+          body += this.token.align[j]
+            ? '<td align="' + this.token.align[j] + '">' + cell + '</td>\n'
+            : '<td>' + cell + '</td>\n';
+        }
+        body += '</tr>\n';
+      }
+      body += '</tbody>\n';
+
+      return '<table>\n'
+        + body
+        + '</table>\n';
+    }
+    case 'blockquote_start': {
+      var body = '';
+
+      while (this.next().type !== 'blockquote_end') {
+        body += this.tok();
+      }
+
+      return '<blockquote>\n'
+        + body
+        + '</blockquote>\n';
+    }
+    case 'list_start': {
+      var type = this.token.ordered ? 'ol' : 'ul'
+        , body = '';
+
+      while (this.next().type !== 'list_end') {
+        body += this.tok();
+      }
+
+      return '<'
+        + type
+        + '>\n'
+        + body
+        + '</'
+        + type
+        + '>\n';
+    }
+    case 'list_item_start': {
+      var body = '';
+
+      while (this.next().type !== 'list_item_end') {
+        body += this.token.type === 'text'
+          ? this.parseText()
+          : this.tok();
+      }
+
+      return '<li>'
+        + body
+        + '</li>\n';
+    }
+    case 'loose_item_start': {
+      var body = '';
+
+      while (this.next().type !== 'list_item_end') {
+        body += this.tok();
+      }
+
+      return '<li>'
+        + body
+        + '</li>\n';
+    }
+    case 'html': {
+      return !this.token.pre && !this.options.pedantic
+        ? this.inline.output(this.token.text)
+        : this.token.text;
+    }
+    case 'paragraph': {
+      return '<p>'
+        + this.inline.output(this.token.text)
+        + '</p>\n';
+    }
+    case 'text': {
+      return '<p>'
+        + this.parseText()
+        + '</p>\n';
+    }
+  }
+};
+
+/**
+ * Helpers
+ */
+
+function escape(html, encode) {
+  return html
+    .replace(!encode ? /&(?!#?\w+;)/g : /&/g, '&amp;')
+    .replace(/</g, '&lt;')
+    .replace(/>/g, '&gt;')
+    .replace(/"/g, '&quot;')
+    .replace(/'/g, '&#39;');
+}
+
+function replace(regex, opt) {
+  regex = regex.source;
+  opt = opt || '';
+  return function self(name, val) {
+    if (!name) return new RegExp(regex, opt);
+    val = val.source || val;
+    val = val.replace(/(^|[^\[])\^/g, '$1');
+    regex = regex.replace(name, val);
+    return self;
+  };
+}
+
+function noop() {}
+noop.exec = noop;
+
+function merge(obj) {
+  var i = 1
+    , target
+    , key;
+
+  for (; i < arguments.length; i++) {
+    target = arguments[i];
+    for (key in target) {
+      if (Object.prototype.hasOwnProperty.call(target, key)) {
+        obj[key] = target[key];
+      }
+    }
+  }
+
+  return obj;
+}
+
+/**
+ * Marked
+ */
+
+function marked(src, opt) {
+  try {
+    if (opt) opt = merge({}, marked.defaults, opt);
+    return Parser.parse(Lexer.lex(src, opt), opt);
+  } catch (e) {
+    e.message += '\nPlease report this to https://github.com/chjj/marked.';
+    if ((opt || marked.defaults).silent) {
+      return '<p>An error occured:</p><pre>'
+        + escape(e.message + '', true)
+        + '</pre>';
+    }
+    throw e;
+  }
+}
+
+/**
+ * Options
+ */
+
+marked.options =
+marked.setOptions = function(opt) {
+  merge(marked.defaults, opt);
+  return marked;
+};
+
+marked.defaults = {
+  gfm: true,
+  tables: true,
+  breaks: false,
+  pedantic: false,
+  sanitize: false,
+  smartLists: false,
+  silent: false,
+  highlight: null,
+  langPrefix: 'lang-'
+};
+
+/**
+ * Expose
+ */
+
+marked.Parser = Parser;
+marked.parser = Parser.parse;
+
+marked.Lexer = Lexer;
+marked.lexer = Lexer.lex;
+
+marked.InlineLexer = InlineLexer;
+marked.inlineLexer = InlineLexer.output;
+
+marked.parse = marked;
+
+if (typeof exports === 'object') {
+  module.exports = marked;
+} else if (typeof define === 'function' && define.amd) {
+  define(function() { return marked; });
+} else {
+  this.marked = marked;
+}
+
+}).call(function() {
+  return this || (typeof window !== 'undefined' ? window : global);
+}());

+ 1671 - 0
apps/html2markdown/libs/rawdeflate.js

@@ -0,0 +1,1671 @@
+/*
+ * $Id: rawdeflate.js,v 0.3 2009/03/01 19:05:05 dankogai Exp dankogai $
+ *
+ * Original:
+ *   http://www.onicos.com/staff/iz/amuse/javascript/expert/deflate.txt
+ */
+
+(function(){
+
+/* Copyright (C) 1999 Masanao Izumo <[email protected]>
+ * Version: 1.0.1
+ * LastModified: Dec 25 1999
+ */
+
+/* Interface:
+ * data = zip_deflate(src);
+ */
+
+/* constant parameters */
+var zip_WSIZE = 32768;		// Sliding Window size
+var zip_STORED_BLOCK = 0;
+var zip_STATIC_TREES = 1;
+var zip_DYN_TREES    = 2;
+
+/* for deflate */
+var zip_DEFAULT_LEVEL = 6;
+var zip_FULL_SEARCH = true;
+var zip_INBUFSIZ = 32768;	// Input buffer size
+var zip_INBUF_EXTRA = 64;	// Extra buffer
+var zip_OUTBUFSIZ = 1024 * 8;
+var zip_window_size = 2 * zip_WSIZE;
+var zip_MIN_MATCH = 3;
+var zip_MAX_MATCH = 258;
+var zip_BITS = 16;
+// for SMALL_MEM
+var zip_LIT_BUFSIZE = 0x2000;
+var zip_HASH_BITS = 13;
+// for MEDIUM_MEM
+// var zip_LIT_BUFSIZE = 0x4000;
+// var zip_HASH_BITS = 14;
+// for BIG_MEM
+// var zip_LIT_BUFSIZE = 0x8000;
+// var zip_HASH_BITS = 15;
+if(zip_LIT_BUFSIZE > zip_INBUFSIZ)
+    alert("error: zip_INBUFSIZ is too small");
+if((zip_WSIZE<<1) > (1<<zip_BITS))
+    alert("error: zip_WSIZE is too large");
+if(zip_HASH_BITS > zip_BITS-1)
+    alert("error: zip_HASH_BITS is too large");
+if(zip_HASH_BITS < 8 || zip_MAX_MATCH != 258)
+    alert("error: Code too clever");
+var zip_DIST_BUFSIZE = zip_LIT_BUFSIZE;
+var zip_HASH_SIZE = 1 << zip_HASH_BITS;
+var zip_HASH_MASK = zip_HASH_SIZE - 1;
+var zip_WMASK = zip_WSIZE - 1;
+var zip_NIL = 0; // Tail of hash chains
+var zip_TOO_FAR = 4096;
+var zip_MIN_LOOKAHEAD = zip_MAX_MATCH + zip_MIN_MATCH + 1;
+var zip_MAX_DIST = zip_WSIZE - zip_MIN_LOOKAHEAD;
+var zip_SMALLEST = 1;
+var zip_MAX_BITS = 15;
+var zip_MAX_BL_BITS = 7;
+var zip_LENGTH_CODES = 29;
+var zip_LITERALS =256;
+var zip_END_BLOCK = 256;
+var zip_L_CODES = zip_LITERALS + 1 + zip_LENGTH_CODES;
+var zip_D_CODES = 30;
+var zip_BL_CODES = 19;
+var zip_REP_3_6 = 16;
+var zip_REPZ_3_10 = 17;
+var zip_REPZ_11_138 = 18;
+var zip_HEAP_SIZE = 2 * zip_L_CODES + 1;
+var zip_H_SHIFT = parseInt((zip_HASH_BITS + zip_MIN_MATCH - 1) /
+			   zip_MIN_MATCH);
+
+/* variables */
+var zip_free_queue;
+var zip_qhead, zip_qtail;
+var zip_initflag;
+var zip_outbuf = null;
+var zip_outcnt, zip_outoff;
+var zip_complete;
+var zip_window;
+var zip_d_buf;
+var zip_l_buf;
+var zip_prev;
+var zip_bi_buf;
+var zip_bi_valid;
+var zip_block_start;
+var zip_ins_h;
+var zip_hash_head;
+var zip_prev_match;
+var zip_match_available;
+var zip_match_length;
+var zip_prev_length;
+var zip_strstart;
+var zip_match_start;
+var zip_eofile;
+var zip_lookahead;
+var zip_max_chain_length;
+var zip_max_lazy_match;
+var zip_compr_level;
+var zip_good_match;
+var zip_nice_match;
+var zip_dyn_ltree;
+var zip_dyn_dtree;
+var zip_static_ltree;
+var zip_static_dtree;
+var zip_bl_tree;
+var zip_l_desc;
+var zip_d_desc;
+var zip_bl_desc;
+var zip_bl_count;
+var zip_heap;
+var zip_heap_len;
+var zip_heap_max;
+var zip_depth;
+var zip_length_code;
+var zip_dist_code;
+var zip_base_length;
+var zip_base_dist;
+var zip_flag_buf;
+var zip_last_lit;
+var zip_last_dist;
+var zip_last_flags;
+var zip_flags;
+var zip_flag_bit;
+var zip_opt_len;
+var zip_static_len;
+var zip_deflate_data;
+var zip_deflate_pos;
+
+/* objects (deflate) */
+
+var zip_DeflateCT = function() {
+    this.fc = 0; // frequency count or bit string
+    this.dl = 0; // father node in Huffman tree or length of bit string
+}
+
+var zip_DeflateTreeDesc = function() {
+    this.dyn_tree = null;	// the dynamic tree
+    this.static_tree = null;	// corresponding static tree or NULL
+    this.extra_bits = null;	// extra bits for each code or NULL
+    this.extra_base = 0;	// base index for extra_bits
+    this.elems = 0;		// max number of elements in the tree
+    this.max_length = 0;	// max bit length for the codes
+    this.max_code = 0;		// largest code with non zero frequency
+}
+
+/* Values for max_lazy_match, good_match and max_chain_length, depending on
+ * the desired pack level (0..9). The values given below have been tuned to
+ * exclude worst case performance for pathological files. Better values may be
+ * found for specific files.
+ */
+var zip_DeflateConfiguration = function(a, b, c, d) {
+    this.good_length = a; // reduce lazy search above this match length
+    this.max_lazy = b;    // do not perform lazy search above this match length
+    this.nice_length = c; // quit search above this match length
+    this.max_chain = d;
+}
+
+var zip_DeflateBuffer = function() {
+    this.next = null;
+    this.len = 0;
+    this.ptr = new Array(zip_OUTBUFSIZ);
+    this.off = 0;
+}
+
+/* constant tables */
+var zip_extra_lbits = new Array(
+    0,0,0,0,0,0,0,0,1,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5,0);
+var zip_extra_dbits = new Array(
+    0,0,0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,10,10,11,11,12,12,13,13);
+var zip_extra_blbits = new Array(
+    0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,3,7);
+var zip_bl_order = new Array(
+    16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15);
+var zip_configuration_table = new Array(
+	new zip_DeflateConfiguration(0,    0,   0,    0),
+	new zip_DeflateConfiguration(4,    4,   8,    4),
+	new zip_DeflateConfiguration(4,    5,  16,    8),
+	new zip_DeflateConfiguration(4,    6,  32,   32),
+	new zip_DeflateConfiguration(4,    4,  16,   16),
+	new zip_DeflateConfiguration(8,   16,  32,   32),
+	new zip_DeflateConfiguration(8,   16, 128,  128),
+	new zip_DeflateConfiguration(8,   32, 128,  256),
+	new zip_DeflateConfiguration(32, 128, 258, 1024),
+	new zip_DeflateConfiguration(32, 258, 258, 4096));
+
+
+/* routines (deflate) */
+
+var zip_deflate_start = function(level) {
+    var i;
+
+    if(!level)
+	level = zip_DEFAULT_LEVEL;
+    else if(level < 1)
+	level = 1;
+    else if(level > 9)
+	level = 9;
+
+    zip_compr_level = level;
+    zip_initflag = false;
+    zip_eofile = false;
+    if(zip_outbuf != null)
+	return;
+
+    zip_free_queue = zip_qhead = zip_qtail = null;
+    zip_outbuf = new Array(zip_OUTBUFSIZ);
+    zip_window = new Array(zip_window_size);
+    zip_d_buf = new Array(zip_DIST_BUFSIZE);
+    zip_l_buf = new Array(zip_INBUFSIZ + zip_INBUF_EXTRA);
+    zip_prev = new Array(1 << zip_BITS);
+    zip_dyn_ltree = new Array(zip_HEAP_SIZE);
+    for(i = 0; i < zip_HEAP_SIZE; i++)
+	zip_dyn_ltree[i] = new zip_DeflateCT();
+    zip_dyn_dtree = new Array(2*zip_D_CODES+1);
+    for(i = 0; i < 2*zip_D_CODES+1; i++)
+	zip_dyn_dtree[i] = new zip_DeflateCT();
+    zip_static_ltree = new Array(zip_L_CODES+2);
+    for(i = 0; i < zip_L_CODES+2; i++)
+	zip_static_ltree[i] = new zip_DeflateCT();
+    zip_static_dtree = new Array(zip_D_CODES);
+    for(i = 0; i < zip_D_CODES; i++)
+	zip_static_dtree[i] = new zip_DeflateCT();
+    zip_bl_tree = new Array(2*zip_BL_CODES+1);
+    for(i = 0; i < 2*zip_BL_CODES+1; i++)
+	zip_bl_tree[i] = new zip_DeflateCT();
+    zip_l_desc = new zip_DeflateTreeDesc();
+    zip_d_desc = new zip_DeflateTreeDesc();
+    zip_bl_desc = new zip_DeflateTreeDesc();
+    zip_bl_count = new Array(zip_MAX_BITS+1);
+    zip_heap = new Array(2*zip_L_CODES+1);
+    zip_depth = new Array(2*zip_L_CODES+1);
+    zip_length_code = new Array(zip_MAX_MATCH-zip_MIN_MATCH+1);
+    zip_dist_code = new Array(512);
+    zip_base_length = new Array(zip_LENGTH_CODES);
+    zip_base_dist = new Array(zip_D_CODES);
+    zip_flag_buf = new Array(parseInt(zip_LIT_BUFSIZE / 8));
+}
+
+var zip_deflate_end = function() {
+    zip_free_queue = zip_qhead = zip_qtail = null;
+    zip_outbuf = null;
+    zip_window = null;
+    zip_d_buf = null;
+    zip_l_buf = null;
+    zip_prev = null;
+    zip_dyn_ltree = null;
+    zip_dyn_dtree = null;
+    zip_static_ltree = null;
+    zip_static_dtree = null;
+    zip_bl_tree = null;
+    zip_l_desc = null;
+    zip_d_desc = null;
+    zip_bl_desc = null;
+    zip_bl_count = null;
+    zip_heap = null;
+    zip_depth = null;
+    zip_length_code = null;
+    zip_dist_code = null;
+    zip_base_length = null;
+    zip_base_dist = null;
+    zip_flag_buf = null;
+}
+
+var zip_reuse_queue = function(p) {
+    p.next = zip_free_queue;
+    zip_free_queue = p;
+}
+
+var zip_new_queue = function() {
+    var p;
+
+    if(zip_free_queue != null)
+    {
+	p = zip_free_queue;
+	zip_free_queue = zip_free_queue.next;
+    }
+    else
+	p = new zip_DeflateBuffer();
+    p.next = null;
+    p.len = p.off = 0;
+
+    return p;
+}
+
+var zip_head1 = function(i) {
+    return zip_prev[zip_WSIZE + i];
+}
+
+var zip_head2 = function(i, val) {
+    return zip_prev[zip_WSIZE + i] = val;
+}
+
+/* put_byte is used for the compressed output, put_ubyte for the
+ * uncompressed output. However unlzw() uses window for its
+ * suffix table instead of its output buffer, so it does not use put_ubyte
+ * (to be cleaned up).
+ */
+var zip_put_byte = function(c) {
+    zip_outbuf[zip_outoff + zip_outcnt++] = c;
+    if(zip_outoff + zip_outcnt == zip_OUTBUFSIZ)
+	zip_qoutbuf();
+}
+
+/* Output a 16 bit value, lsb first */
+var zip_put_short = function(w) {
+    w &= 0xffff;
+    if(zip_outoff + zip_outcnt < zip_OUTBUFSIZ - 2) {
+	zip_outbuf[zip_outoff + zip_outcnt++] = (w & 0xff);
+	zip_outbuf[zip_outoff + zip_outcnt++] = (w >>> 8);
+    } else {
+	zip_put_byte(w & 0xff);
+	zip_put_byte(w >>> 8);
+    }
+}
+
+/* ==========================================================================
+ * Insert string s in the dictionary and set match_head to the previous head
+ * of the hash chain (the most recent string with same hash key). Return
+ * the previous length of the hash chain.
+ * IN  assertion: all calls to to INSERT_STRING are made with consecutive
+ *    input characters and the first MIN_MATCH bytes of s are valid
+ *    (except for the last MIN_MATCH-1 bytes of the input file).
+ */
+var zip_INSERT_STRING = function() {
+    zip_ins_h = ((zip_ins_h << zip_H_SHIFT)
+		 ^ (zip_window[zip_strstart + zip_MIN_MATCH - 1] & 0xff))
+	& zip_HASH_MASK;
+    zip_hash_head = zip_head1(zip_ins_h);
+    zip_prev[zip_strstart & zip_WMASK] = zip_hash_head;
+    zip_head2(zip_ins_h, zip_strstart);
+}
+
+/* Send a code of the given tree. c and tree must not have side effects */
+var zip_SEND_CODE = function(c, tree) {
+    zip_send_bits(tree[c].fc, tree[c].dl);
+}
+
+/* Mapping from a distance to a distance code. dist is the distance - 1 and
+ * must not have side effects. dist_code[256] and dist_code[257] are never
+ * used.
+ */
+var zip_D_CODE = function(dist) {
+    return (dist < 256 ? zip_dist_code[dist]
+	    : zip_dist_code[256 + (dist>>7)]) & 0xff;
+}
+
+/* ==========================================================================
+ * Compares to subtrees, using the tree depth as tie breaker when
+ * the subtrees have equal frequency. This minimizes the worst case length.
+ */
+var zip_SMALLER = function(tree, n, m) {
+    return tree[n].fc < tree[m].fc ||
+      (tree[n].fc == tree[m].fc && zip_depth[n] <= zip_depth[m]);
+}
+
+/* ==========================================================================
+ * read string data
+ */
+var zip_read_buff = function(buff, offset, n) {
+    var i;
+    for(i = 0; i < n && zip_deflate_pos < zip_deflate_data.length; i++)
+	buff[offset + i] =
+	    zip_deflate_data.charCodeAt(zip_deflate_pos++) & 0xff;
+    return i;
+}
+
+/* ==========================================================================
+ * Initialize the "longest match" routines for a new file
+ */
+var zip_lm_init = function() {
+    var j;
+
+    /* Initialize the hash table. */
+    for(j = 0; j < zip_HASH_SIZE; j++)
+//	zip_head2(j, zip_NIL);
+	zip_prev[zip_WSIZE + j] = 0;
+    /* prev will be initialized on the fly */
+
+    /* Set the default configuration parameters:
+     */
+    zip_max_lazy_match = zip_configuration_table[zip_compr_level].max_lazy;
+    zip_good_match     = zip_configuration_table[zip_compr_level].good_length;
+    if(!zip_FULL_SEARCH)
+	zip_nice_match = zip_configuration_table[zip_compr_level].nice_length;
+    zip_max_chain_length = zip_configuration_table[zip_compr_level].max_chain;
+
+    zip_strstart = 0;
+    zip_block_start = 0;
+
+    zip_lookahead = zip_read_buff(zip_window, 0, 2 * zip_WSIZE);
+    if(zip_lookahead <= 0) {
+	zip_eofile = true;
+	zip_lookahead = 0;
+	return;
+    }
+    zip_eofile = false;
+    /* Make sure that we always have enough lookahead. This is important
+     * if input comes from a device such as a tty.
+     */
+    while(zip_lookahead < zip_MIN_LOOKAHEAD && !zip_eofile)
+	zip_fill_window();
+
+    /* If lookahead < MIN_MATCH, ins_h is garbage, but this is
+     * not important since only literal bytes will be emitted.
+     */
+    zip_ins_h = 0;
+    for(j = 0; j < zip_MIN_MATCH - 1; j++) {
+//      UPDATE_HASH(ins_h, window[j]);
+	zip_ins_h = ((zip_ins_h << zip_H_SHIFT) ^ (zip_window[j] & 0xff)) & zip_HASH_MASK;
+    }
+}
+
+/* ==========================================================================
+ * Set match_start to the longest match starting at the given string and
+ * return its length. Matches shorter or equal to prev_length are discarded,
+ * in which case the result is equal to prev_length and match_start is
+ * garbage.
+ * IN assertions: cur_match is the head of the hash chain for the current
+ *   string (strstart) and its distance is <= MAX_DIST, and prev_length >= 1
+ */
+var zip_longest_match = function(cur_match) {
+    var chain_length = zip_max_chain_length; // max hash chain length
+    var scanp = zip_strstart; // current string
+    var matchp;		// matched string
+    var len;		// length of current match
+    var best_len = zip_prev_length;	// best match length so far
+
+    /* Stop when cur_match becomes <= limit. To simplify the code,
+     * we prevent matches with the string of window index 0.
+     */
+    var limit = (zip_strstart > zip_MAX_DIST ? zip_strstart - zip_MAX_DIST : zip_NIL);
+
+    var strendp = zip_strstart + zip_MAX_MATCH;
+    var scan_end1 = zip_window[scanp + best_len - 1];
+    var scan_end  = zip_window[scanp + best_len];
+
+    /* Do not waste too much time if we already have a good match: */
+    if(zip_prev_length >= zip_good_match)
+	chain_length >>= 2;
+
+//  Assert(encoder->strstart <= window_size-MIN_LOOKAHEAD, "insufficient lookahead");
+
+    do {
+//    Assert(cur_match < encoder->strstart, "no future");
+	matchp = cur_match;
+
+	/* Skip to next match if the match length cannot increase
+	    * or if the match length is less than 2:
+	*/
+	if(zip_window[matchp + best_len]	!= scan_end  ||
+	   zip_window[matchp + best_len - 1]	!= scan_end1 ||
+	   zip_window[matchp]			!= zip_window[scanp] ||
+	   zip_window[++matchp]			!= zip_window[scanp + 1]) {
+	    continue;
+	}
+
+	/* The check at best_len-1 can be removed because it will be made
+         * again later. (This heuristic is not always a win.)
+         * It is not necessary to compare scan[2] and match[2] since they
+         * are always equal when the other bytes match, given that
+         * the hash keys are equal and that HASH_BITS >= 8.
+         */
+	scanp += 2;
+	matchp++;
+
+	/* We check for insufficient lookahead only every 8th comparison;
+         * the 256th check will be made at strstart+258.
+         */
+	do {
+	} while(zip_window[++scanp] == zip_window[++matchp] &&
+		zip_window[++scanp] == zip_window[++matchp] &&
+		zip_window[++scanp] == zip_window[++matchp] &&
+		zip_window[++scanp] == zip_window[++matchp] &&
+		zip_window[++scanp] == zip_window[++matchp] &&
+		zip_window[++scanp] == zip_window[++matchp] &&
+		zip_window[++scanp] == zip_window[++matchp] &&
+		zip_window[++scanp] == zip_window[++matchp] &&
+		scanp < strendp);
+
+      len = zip_MAX_MATCH - (strendp - scanp);
+      scanp = strendp - zip_MAX_MATCH;
+
+      if(len > best_len) {
+	  zip_match_start = cur_match;
+	  best_len = len;
+	  if(zip_FULL_SEARCH) {
+	      if(len >= zip_MAX_MATCH) break;
+	  } else {
+	      if(len >= zip_nice_match) break;
+	  }
+
+	  scan_end1  = zip_window[scanp + best_len-1];
+	  scan_end   = zip_window[scanp + best_len];
+      }
+    } while((cur_match = zip_prev[cur_match & zip_WMASK]) > limit
+	    && --chain_length != 0);
+
+    return best_len;
+}
+
+/* ==========================================================================
+ * Fill the window when the lookahead becomes insufficient.
+ * Updates strstart and lookahead, and sets eofile if end of input file.
+ * IN assertion: lookahead < MIN_LOOKAHEAD && strstart + lookahead > 0
+ * OUT assertions: at least one byte has been read, or eofile is set;
+ *    file reads are performed for at least two bytes (required for the
+ *    translate_eol option).
+ */
+var zip_fill_window = function() {
+    var n, m;
+
+    // Amount of free space at the end of the window.
+    var more = zip_window_size - zip_lookahead - zip_strstart;
+
+    /* If the window is almost full and there is insufficient lookahead,
+     * move the upper half to the lower one to make room in the upper half.
+     */
+    if(more == -1) {
+	/* Very unlikely, but possible on 16 bit machine if strstart == 0
+         * and lookahead == 1 (input done one byte at time)
+         */
+	more--;
+    } else if(zip_strstart >= zip_WSIZE + zip_MAX_DIST) {
+	/* By the IN assertion, the window is not empty so we can't confuse
+         * more == 0 with more == 64K on a 16 bit machine.
+         */
+//	Assert(window_size == (ulg)2*WSIZE, "no sliding with BIG_MEM");
+
+//	System.arraycopy(window, WSIZE, window, 0, WSIZE);
+	for(n = 0; n < zip_WSIZE; n++)
+	    zip_window[n] = zip_window[n + zip_WSIZE];
+      
+	zip_match_start -= zip_WSIZE;
+	zip_strstart    -= zip_WSIZE; /* we now have strstart >= MAX_DIST: */
+	zip_block_start -= zip_WSIZE;
+
+	for(n = 0; n < zip_HASH_SIZE; n++) {
+	    m = zip_head1(n);
+	    zip_head2(n, m >= zip_WSIZE ? m - zip_WSIZE : zip_NIL);
+	}
+	for(n = 0; n < zip_WSIZE; n++) {
+	    /* If n is not on any hash chain, prev[n] is garbage but
+	     * its value will never be used.
+	     */
+	    m = zip_prev[n];
+	    zip_prev[n] = (m >= zip_WSIZE ? m - zip_WSIZE : zip_NIL);
+	}
+	more += zip_WSIZE;
+    }
+    // At this point, more >= 2
+    if(!zip_eofile) {
+	n = zip_read_buff(zip_window, zip_strstart + zip_lookahead, more);
+	if(n <= 0)
+	    zip_eofile = true;
+	else
+	    zip_lookahead += n;
+    }
+}
+
+/* ==========================================================================
+ * Processes a new input file and return its compressed length. This
+ * function does not perform lazy evaluationof matches and inserts
+ * new strings in the dictionary only for unmatched strings or for short
+ * matches. It is used only for the fast compression options.
+ */
+var zip_deflate_fast = function() {
+    while(zip_lookahead != 0 && zip_qhead == null) {
+	var flush; // set if current block must be flushed
+
+	/* Insert the string window[strstart .. strstart+2] in the
+	 * dictionary, and set hash_head to the head of the hash chain:
+	 */
+	zip_INSERT_STRING();
+
+	/* Find the longest match, discarding those <= prev_length.
+	 * At this point we have always match_length < MIN_MATCH
+	 */
+	if(zip_hash_head != zip_NIL &&
+	   zip_strstart - zip_hash_head <= zip_MAX_DIST) {
+	    /* To simplify the code, we prevent matches with the string
+	     * of window index 0 (in particular we have to avoid a match
+	     * of the string with itself at the start of the input file).
+	     */
+	    zip_match_length = zip_longest_match(zip_hash_head);
+	    /* longest_match() sets match_start */
+	    if(zip_match_length > zip_lookahead)
+		zip_match_length = zip_lookahead;
+	}
+	if(zip_match_length >= zip_MIN_MATCH) {
+//	    check_match(strstart, match_start, match_length);
+
+	    flush = zip_ct_tally(zip_strstart - zip_match_start,
+				 zip_match_length - zip_MIN_MATCH);
+	    zip_lookahead -= zip_match_length;
+
+	    /* Insert new strings in the hash table only if the match length
+	     * is not too large. This saves time but degrades compression.
+	     */
+	    if(zip_match_length <= zip_max_lazy_match) {
+		zip_match_length--; // string at strstart already in hash table
+		do {
+		    zip_strstart++;
+		    zip_INSERT_STRING();
+		    /* strstart never exceeds WSIZE-MAX_MATCH, so there are
+		     * always MIN_MATCH bytes ahead. If lookahead < MIN_MATCH
+		     * these bytes are garbage, but it does not matter since
+		     * the next lookahead bytes will be emitted as literals.
+		     */
+		} while(--zip_match_length != 0);
+		zip_strstart++;
+	    } else {
+		zip_strstart += zip_match_length;
+		zip_match_length = 0;
+		zip_ins_h = zip_window[zip_strstart] & 0xff;
+//		UPDATE_HASH(ins_h, window[strstart + 1]);
+		zip_ins_h = ((zip_ins_h<<zip_H_SHIFT) ^ (zip_window[zip_strstart + 1] & 0xff)) & zip_HASH_MASK;
+
+//#if MIN_MATCH != 3
+//		Call UPDATE_HASH() MIN_MATCH-3 more times
+//#endif
+
+	    }
+	} else {
+	    /* No match, output a literal byte */
+	    flush = zip_ct_tally(0, zip_window[zip_strstart] & 0xff);
+	    zip_lookahead--;
+	    zip_strstart++;
+	}
+	if(flush) {
+	    zip_flush_block(0);
+	    zip_block_start = zip_strstart;
+	}
+
+	/* Make sure that we always have enough lookahead, except
+	 * at the end of the input file. We need MAX_MATCH bytes
+	 * for the next match, plus MIN_MATCH bytes to insert the
+	 * string following the next match.
+	 */
+	while(zip_lookahead < zip_MIN_LOOKAHEAD && !zip_eofile)
+	    zip_fill_window();
+    }
+}
+
+var zip_deflate_better = function() {
+    /* Process the input block. */
+    while(zip_lookahead != 0 && zip_qhead == null) {
+	/* Insert the string window[strstart .. strstart+2] in the
+	 * dictionary, and set hash_head to the head of the hash chain:
+	 */
+	zip_INSERT_STRING();
+
+	/* Find the longest match, discarding those <= prev_length.
+	 */
+	zip_prev_length = zip_match_length;
+	zip_prev_match = zip_match_start;
+	zip_match_length = zip_MIN_MATCH - 1;
+
+	if(zip_hash_head != zip_NIL &&
+	   zip_prev_length < zip_max_lazy_match &&
+	   zip_strstart - zip_hash_head <= zip_MAX_DIST) {
+	    /* To simplify the code, we prevent matches with the string
+	     * of window index 0 (in particular we have to avoid a match
+	     * of the string with itself at the start of the input file).
+	     */
+	    zip_match_length = zip_longest_match(zip_hash_head);
+	    /* longest_match() sets match_start */
+	    if(zip_match_length > zip_lookahead)
+		zip_match_length = zip_lookahead;
+
+	    /* Ignore a length 3 match if it is too distant: */
+	    if(zip_match_length == zip_MIN_MATCH &&
+	       zip_strstart - zip_match_start > zip_TOO_FAR) {
+		/* If prev_match is also MIN_MATCH, match_start is garbage
+		 * but we will ignore the current match anyway.
+		 */
+		zip_match_length--;
+	    }
+	}
+	/* If there was a match at the previous step and the current
+	 * match is not better, output the previous match:
+	 */
+	if(zip_prev_length >= zip_MIN_MATCH &&
+	   zip_match_length <= zip_prev_length) {
+	    var flush; // set if current block must be flushed
+
+//	    check_match(strstart - 1, prev_match, prev_length);
+	    flush = zip_ct_tally(zip_strstart - 1 - zip_prev_match,
+				 zip_prev_length - zip_MIN_MATCH);
+
+	    /* Insert in hash table all strings up to the end of the match.
+	     * strstart-1 and strstart are already inserted.
+	     */
+	    zip_lookahead -= zip_prev_length - 1;
+	    zip_prev_length -= 2;
+	    do {
+		zip_strstart++;
+		zip_INSERT_STRING();
+		/* strstart never exceeds WSIZE-MAX_MATCH, so there are
+		 * always MIN_MATCH bytes ahead. If lookahead < MIN_MATCH
+		 * these bytes are garbage, but it does not matter since the
+		 * next lookahead bytes will always be emitted as literals.
+		 */
+	    } while(--zip_prev_length != 0);
+	    zip_match_available = 0;
+	    zip_match_length = zip_MIN_MATCH - 1;
+	    zip_strstart++;
+	    if(flush) {
+		zip_flush_block(0);
+		zip_block_start = zip_strstart;
+	    }
+	} else if(zip_match_available != 0) {
+	    /* If there was no match at the previous position, output a
+	     * single literal. If there was a match but the current match
+	     * is longer, truncate the previous match to a single literal.
+	     */
+	    if(zip_ct_tally(0, zip_window[zip_strstart - 1] & 0xff)) {
+		zip_flush_block(0);
+		zip_block_start = zip_strstart;
+	    }
+	    zip_strstart++;
+	    zip_lookahead--;
+	} else {
+	    /* There is no previous match to compare with, wait for
+	     * the next step to decide.
+	     */
+	    zip_match_available = 1;
+	    zip_strstart++;
+	    zip_lookahead--;
+	}
+
+	/* Make sure that we always have enough lookahead, except
+	 * at the end of the input file. We need MAX_MATCH bytes
+	 * for the next match, plus MIN_MATCH bytes to insert the
+	 * string following the next match.
+	 */
+	while(zip_lookahead < zip_MIN_LOOKAHEAD && !zip_eofile)
+	    zip_fill_window();
+    }
+}
+
+var zip_init_deflate = function() {
+    if(zip_eofile)
+	return;
+    zip_bi_buf = 0;
+    zip_bi_valid = 0;
+    zip_ct_init();
+    zip_lm_init();
+
+    zip_qhead = null;
+    zip_outcnt = 0;
+    zip_outoff = 0;
+
+    if(zip_compr_level <= 3)
+    {
+	zip_prev_length = zip_MIN_MATCH - 1;
+	zip_match_length = 0;
+    }
+    else
+    {
+	zip_match_length = zip_MIN_MATCH - 1;
+	zip_match_available = 0;
+    }
+
+    zip_complete = false;
+}
+
+/* ==========================================================================
+ * Same as above, but achieves better compression. We use a lazy
+ * evaluation for matches: a match is finally adopted only if there is
+ * no better match at the next window position.
+ */
+var zip_deflate_internal = function(buff, off, buff_size) {
+    var n;
+
+    if(!zip_initflag)
+    {
+	zip_init_deflate();
+	zip_initflag = true;
+	if(zip_lookahead == 0) { // empty
+	    zip_complete = true;
+	    return 0;
+	}
+    }
+
+    if((n = zip_qcopy(buff, off, buff_size)) == buff_size)
+	return buff_size;
+
+    if(zip_complete)
+	return n;
+
+    if(zip_compr_level <= 3) // optimized for speed
+	zip_deflate_fast();
+    else
+	zip_deflate_better();
+    if(zip_lookahead == 0) {
+	if(zip_match_available != 0)
+	    zip_ct_tally(0, zip_window[zip_strstart - 1] & 0xff);
+	zip_flush_block(1);
+	zip_complete = true;
+    }
+    return n + zip_qcopy(buff, n + off, buff_size - n);
+}
+
+var zip_qcopy = function(buff, off, buff_size) {
+    var n, i, j;
+
+    n = 0;
+    while(zip_qhead != null && n < buff_size)
+    {
+	i = buff_size - n;
+	if(i > zip_qhead.len)
+	    i = zip_qhead.len;
+//      System.arraycopy(qhead.ptr, qhead.off, buff, off + n, i);
+	for(j = 0; j < i; j++)
+	    buff[off + n + j] = zip_qhead.ptr[zip_qhead.off + j];
+	
+	zip_qhead.off += i;
+	zip_qhead.len -= i;
+	n += i;
+	if(zip_qhead.len == 0) {
+	    var p;
+	    p = zip_qhead;
+	    zip_qhead = zip_qhead.next;
+	    zip_reuse_queue(p);
+	}
+    }
+
+    if(n == buff_size)
+	return n;
+
+    if(zip_outoff < zip_outcnt) {
+	i = buff_size - n;
+	if(i > zip_outcnt - zip_outoff)
+	    i = zip_outcnt - zip_outoff;
+	// System.arraycopy(outbuf, outoff, buff, off + n, i);
+	for(j = 0; j < i; j++)
+	    buff[off + n + j] = zip_outbuf[zip_outoff + j];
+	zip_outoff += i;
+	n += i;
+	if(zip_outcnt == zip_outoff)
+	    zip_outcnt = zip_outoff = 0;
+    }
+    return n;
+}
+
+/* ==========================================================================
+ * Allocate the match buffer, initialize the various tables and save the
+ * location of the internal file attribute (ascii/binary) and method
+ * (DEFLATE/STORE).
+ */
+var zip_ct_init = function() {
+    var n;	// iterates over tree elements
+    var bits;	// bit counter
+    var length;	// length value
+    var code;	// code value
+    var dist;	// distance index
+
+    if(zip_static_dtree[0].dl != 0) return; // ct_init already called
+
+    zip_l_desc.dyn_tree		= zip_dyn_ltree;
+    zip_l_desc.static_tree	= zip_static_ltree;
+    zip_l_desc.extra_bits	= zip_extra_lbits;
+    zip_l_desc.extra_base	= zip_LITERALS + 1;
+    zip_l_desc.elems		= zip_L_CODES;
+    zip_l_desc.max_length	= zip_MAX_BITS;
+    zip_l_desc.max_code		= 0;
+
+    zip_d_desc.dyn_tree		= zip_dyn_dtree;
+    zip_d_desc.static_tree	= zip_static_dtree;
+    zip_d_desc.extra_bits	= zip_extra_dbits;
+    zip_d_desc.extra_base	= 0;
+    zip_d_desc.elems		= zip_D_CODES;
+    zip_d_desc.max_length	= zip_MAX_BITS;
+    zip_d_desc.max_code		= 0;
+
+    zip_bl_desc.dyn_tree	= zip_bl_tree;
+    zip_bl_desc.static_tree	= null;
+    zip_bl_desc.extra_bits	= zip_extra_blbits;
+    zip_bl_desc.extra_base	= 0;
+    zip_bl_desc.elems		= zip_BL_CODES;
+    zip_bl_desc.max_length	= zip_MAX_BL_BITS;
+    zip_bl_desc.max_code	= 0;
+
+    // Initialize the mapping length (0..255) -> length code (0..28)
+    length = 0;
+    for(code = 0; code < zip_LENGTH_CODES-1; code++) {
+	zip_base_length[code] = length;
+	for(n = 0; n < (1<<zip_extra_lbits[code]); n++)
+	    zip_length_code[length++] = code;
+    }
+    // Assert (length == 256, "ct_init: length != 256");
+
+    /* Note that the length 255 (match length 258) can be represented
+     * in two different ways: code 284 + 5 bits or code 285, so we
+     * overwrite length_code[255] to use the best encoding:
+     */
+    zip_length_code[length-1] = code;
+
+    /* Initialize the mapping dist (0..32K) -> dist code (0..29) */
+    dist = 0;
+    for(code = 0 ; code < 16; code++) {
+	zip_base_dist[code] = dist;
+	for(n = 0; n < (1<<zip_extra_dbits[code]); n++) {
+	    zip_dist_code[dist++] = code;
+	}
+    }
+    // Assert (dist == 256, "ct_init: dist != 256");
+    dist >>= 7; // from now on, all distances are divided by 128
+    for( ; code < zip_D_CODES; code++) {
+	zip_base_dist[code] = dist << 7;
+	for(n = 0; n < (1<<(zip_extra_dbits[code]-7)); n++)
+	    zip_dist_code[256 + dist++] = code;
+    }
+    // Assert (dist == 256, "ct_init: 256+dist != 512");
+
+    // Construct the codes of the static literal tree
+    for(bits = 0; bits <= zip_MAX_BITS; bits++)
+	zip_bl_count[bits] = 0;
+    n = 0;
+    while(n <= 143) { zip_static_ltree[n++].dl = 8; zip_bl_count[8]++; }
+    while(n <= 255) { zip_static_ltree[n++].dl = 9; zip_bl_count[9]++; }
+    while(n <= 279) { zip_static_ltree[n++].dl = 7; zip_bl_count[7]++; }
+    while(n <= 287) { zip_static_ltree[n++].dl = 8; zip_bl_count[8]++; }
+    /* Codes 286 and 287 do not exist, but we must include them in the
+     * tree construction to get a canonical Huffman tree (longest code
+     * all ones)
+     */
+    zip_gen_codes(zip_static_ltree, zip_L_CODES + 1);
+
+    /* The static distance tree is trivial: */
+    for(n = 0; n < zip_D_CODES; n++) {
+	zip_static_dtree[n].dl = 5;
+	zip_static_dtree[n].fc = zip_bi_reverse(n, 5);
+    }
+
+    // Initialize the first block of the first file:
+    zip_init_block();
+}
+
+/* ==========================================================================
+ * Initialize a new block.
+ */
+var zip_init_block = function() {
+    var n; // iterates over tree elements
+
+    // Initialize the trees.
+    for(n = 0; n < zip_L_CODES;  n++) zip_dyn_ltree[n].fc = 0;
+    for(n = 0; n < zip_D_CODES;  n++) zip_dyn_dtree[n].fc = 0;
+    for(n = 0; n < zip_BL_CODES; n++) zip_bl_tree[n].fc = 0;
+
+    zip_dyn_ltree[zip_END_BLOCK].fc = 1;
+    zip_opt_len = zip_static_len = 0;
+    zip_last_lit = zip_last_dist = zip_last_flags = 0;
+    zip_flags = 0;
+    zip_flag_bit = 1;
+}
+
+/* ==========================================================================
+ * Restore the heap property by moving down the tree starting at node k,
+ * exchanging a node with the smallest of its two sons if necessary, stopping
+ * when the heap property is re-established (each father smaller than its
+ * two sons).
+ */
+var zip_pqdownheap = function(
+    tree,	// the tree to restore
+    k) {	// node to move down
+    var v = zip_heap[k];
+    var j = k << 1;	// left son of k
+
+    while(j <= zip_heap_len) {
+	// Set j to the smallest of the two sons:
+	if(j < zip_heap_len &&
+	   zip_SMALLER(tree, zip_heap[j + 1], zip_heap[j]))
+	    j++;
+
+	// Exit if v is smaller than both sons
+	if(zip_SMALLER(tree, v, zip_heap[j]))
+	    break;
+
+	// Exchange v with the smallest son
+	zip_heap[k] = zip_heap[j];
+	k = j;
+
+	// And continue down the tree, setting j to the left son of k
+	j <<= 1;
+    }
+    zip_heap[k] = v;
+}
+
+/* ==========================================================================
+ * Compute the optimal bit lengths for a tree and update the total bit length
+ * for the current block.
+ * IN assertion: the fields freq and dad are set, heap[heap_max] and
+ *    above are the tree nodes sorted by increasing frequency.
+ * OUT assertions: the field len is set to the optimal bit length, the
+ *     array bl_count contains the frequencies for each bit length.
+ *     The length opt_len is updated; static_len is also updated if stree is
+ *     not null.
+ */
+var zip_gen_bitlen = function(desc) { // the tree descriptor
+    var tree		= desc.dyn_tree;
+    var extra		= desc.extra_bits;
+    var base		= desc.extra_base;
+    var max_code	= desc.max_code;
+    var max_length	= desc.max_length;
+    var stree		= desc.static_tree;
+    var h;		// heap index
+    var n, m;		// iterate over the tree elements
+    var bits;		// bit length
+    var xbits;		// extra bits
+    var f;		// frequency
+    var overflow = 0;	// number of elements with bit length too large
+
+    for(bits = 0; bits <= zip_MAX_BITS; bits++)
+	zip_bl_count[bits] = 0;
+
+    /* In a first pass, compute the optimal bit lengths (which may
+     * overflow in the case of the bit length tree).
+     */
+    tree[zip_heap[zip_heap_max]].dl = 0; // root of the heap
+
+    for(h = zip_heap_max + 1; h < zip_HEAP_SIZE; h++) {
+	n = zip_heap[h];
+	bits = tree[tree[n].dl].dl + 1;
+	if(bits > max_length) {
+	    bits = max_length;
+	    overflow++;
+	}
+	tree[n].dl = bits;
+	// We overwrite tree[n].dl which is no longer needed
+
+	if(n > max_code)
+	    continue; // not a leaf node
+
+	zip_bl_count[bits]++;
+	xbits = 0;
+	if(n >= base)
+	    xbits = extra[n - base];
+	f = tree[n].fc;
+	zip_opt_len += f * (bits + xbits);
+	if(stree != null)
+	    zip_static_len += f * (stree[n].dl + xbits);
+    }
+    if(overflow == 0)
+	return;
+
+    // This happens for example on obj2 and pic of the Calgary corpus
+
+    // Find the first bit length which could increase:
+    do {
+	bits = max_length - 1;
+	while(zip_bl_count[bits] == 0)
+	    bits--;
+	zip_bl_count[bits]--;		// move one leaf down the tree
+	zip_bl_count[bits + 1] += 2;	// move one overflow item as its brother
+	zip_bl_count[max_length]--;
+	/* The brother of the overflow item also moves one step up,
+	 * but this does not affect bl_count[max_length]
+	 */
+	overflow -= 2;
+    } while(overflow > 0);
+
+    /* Now recompute all bit lengths, scanning in increasing frequency.
+     * h is still equal to HEAP_SIZE. (It is simpler to reconstruct all
+     * lengths instead of fixing only the wrong ones. This idea is taken
+     * from 'ar' written by Haruhiko Okumura.)
+     */
+    for(bits = max_length; bits != 0; bits--) {
+	n = zip_bl_count[bits];
+	while(n != 0) {
+	    m = zip_heap[--h];
+	    if(m > max_code)
+		continue;
+	    if(tree[m].dl != bits) {
+		zip_opt_len += (bits - tree[m].dl) * tree[m].fc;
+		tree[m].fc = bits;
+	    }
+	    n--;
+	}
+    }
+}
+
+  /* ==========================================================================
+   * Generate the codes for a given tree and bit counts (which need not be
+   * optimal).
+   * IN assertion: the array bl_count contains the bit length statistics for
+   * the given tree and the field len is set for all tree elements.
+   * OUT assertion: the field code is set for all tree elements of non
+   *     zero code length.
+   */
+var zip_gen_codes = function(tree,	// the tree to decorate
+		   max_code) {	// largest code with non zero frequency
+    var next_code = new Array(zip_MAX_BITS+1); // next code value for each bit length
+    var code = 0;		// running code value
+    var bits;			// bit index
+    var n;			// code index
+
+    /* The distribution counts are first used to generate the code values
+     * without bit reversal.
+     */
+    for(bits = 1; bits <= zip_MAX_BITS; bits++) {
+	code = ((code + zip_bl_count[bits-1]) << 1);
+	next_code[bits] = code;
+    }
+
+    /* Check that the bit counts in bl_count are consistent. The last code
+     * must be all ones.
+     */
+//    Assert (code + encoder->bl_count[MAX_BITS]-1 == (1<<MAX_BITS)-1,
+//	    "inconsistent bit counts");
+//    Tracev((stderr,"\ngen_codes: max_code %d ", max_code));
+
+    for(n = 0; n <= max_code; n++) {
+	var len = tree[n].dl;
+	if(len == 0)
+	    continue;
+	// Now reverse the bits
+	tree[n].fc = zip_bi_reverse(next_code[len]++, len);
+
+//      Tracec(tree != static_ltree, (stderr,"\nn %3d %c l %2d c %4x (%x) ",
+//	  n, (isgraph(n) ? n : ' '), len, tree[n].fc, next_code[len]-1));
+    }
+}
+
+/* ==========================================================================
+ * Construct one Huffman tree and assigns the code bit strings and lengths.
+ * Update the total bit length for the current block.
+ * IN assertion: the field freq is set for all tree elements.
+ * OUT assertions: the fields len and code are set to the optimal bit length
+ *     and corresponding code. The length opt_len is updated; static_len is
+ *     also updated if stree is not null. The field max_code is set.
+ */
+var zip_build_tree = function(desc) { // the tree descriptor
+    var tree	= desc.dyn_tree;
+    var stree	= desc.static_tree;
+    var elems	= desc.elems;
+    var n, m;		// iterate over heap elements
+    var max_code = -1;	// largest code with non zero frequency
+    var node = elems;	// next internal node of the tree
+
+    /* Construct the initial heap, with least frequent element in
+     * heap[SMALLEST]. The sons of heap[n] are heap[2*n] and heap[2*n+1].
+     * heap[0] is not used.
+     */
+    zip_heap_len = 0;
+    zip_heap_max = zip_HEAP_SIZE;
+
+    for(n = 0; n < elems; n++) {
+	if(tree[n].fc != 0) {
+	    zip_heap[++zip_heap_len] = max_code = n;
+	    zip_depth[n] = 0;
+	} else
+	    tree[n].dl = 0;
+    }
+
+    /* The pkzip format requires that at least one distance code exists,
+     * and that at least one bit should be sent even if there is only one
+     * possible code. So to avoid special checks later on we force at least
+     * two codes of non zero frequency.
+     */
+    while(zip_heap_len < 2) {
+	var xnew = zip_heap[++zip_heap_len] = (max_code < 2 ? ++max_code : 0);
+	tree[xnew].fc = 1;
+	zip_depth[xnew] = 0;
+	zip_opt_len--;
+	if(stree != null)
+	    zip_static_len -= stree[xnew].dl;
+	// new is 0 or 1 so it does not have extra bits
+    }
+    desc.max_code = max_code;
+
+    /* The elements heap[heap_len/2+1 .. heap_len] are leaves of the tree,
+     * establish sub-heaps of increasing lengths:
+     */
+    for(n = zip_heap_len >> 1; n >= 1; n--)
+	zip_pqdownheap(tree, n);
+
+    /* Construct the Huffman tree by repeatedly combining the least two
+     * frequent nodes.
+     */
+    do {
+	n = zip_heap[zip_SMALLEST];
+	zip_heap[zip_SMALLEST] = zip_heap[zip_heap_len--];
+	zip_pqdownheap(tree, zip_SMALLEST);
+
+	m = zip_heap[zip_SMALLEST];  // m = node of next least frequency
+
+	// keep the nodes sorted by frequency
+	zip_heap[--zip_heap_max] = n;
+	zip_heap[--zip_heap_max] = m;
+
+	// Create a new node father of n and m
+	tree[node].fc = tree[n].fc + tree[m].fc;
+//	depth[node] = (char)(MAX(depth[n], depth[m]) + 1);
+	if(zip_depth[n] > zip_depth[m] + 1)
+	    zip_depth[node] = zip_depth[n];
+	else
+	    zip_depth[node] = zip_depth[m] + 1;
+	tree[n].dl = tree[m].dl = node;
+
+	// and insert the new node in the heap
+	zip_heap[zip_SMALLEST] = node++;
+	zip_pqdownheap(tree, zip_SMALLEST);
+
+    } while(zip_heap_len >= 2);
+
+    zip_heap[--zip_heap_max] = zip_heap[zip_SMALLEST];
+
+    /* At this point, the fields freq and dad are set. We can now
+     * generate the bit lengths.
+     */
+    zip_gen_bitlen(desc);
+
+    // The field len is now set, we can generate the bit codes
+    zip_gen_codes(tree, max_code);
+}
+
+/* ==========================================================================
+ * Scan a literal or distance tree to determine the frequencies of the codes
+ * in the bit length tree. Updates opt_len to take into account the repeat
+ * counts. (The contribution of the bit length codes will be added later
+ * during the construction of bl_tree.)
+ */
+var zip_scan_tree = function(tree,// the tree to be scanned
+		       max_code) {  // and its largest code of non zero frequency
+    var n;			// iterates over all tree elements
+    var prevlen = -1;		// last emitted length
+    var curlen;			// length of current code
+    var nextlen = tree[0].dl;	// length of next code
+    var count = 0;		// repeat count of the current code
+    var max_count = 7;		// max repeat count
+    var min_count = 4;		// min repeat count
+
+    if(nextlen == 0) {
+	max_count = 138;
+	min_count = 3;
+    }
+    tree[max_code + 1].dl = 0xffff; // guard
+
+    for(n = 0; n <= max_code; n++) {
+	curlen = nextlen;
+	nextlen = tree[n + 1].dl;
+	if(++count < max_count && curlen == nextlen)
+	    continue;
+	else if(count < min_count)
+	    zip_bl_tree[curlen].fc += count;
+	else if(curlen != 0) {
+	    if(curlen != prevlen)
+		zip_bl_tree[curlen].fc++;
+	    zip_bl_tree[zip_REP_3_6].fc++;
+	} else if(count <= 10)
+	    zip_bl_tree[zip_REPZ_3_10].fc++;
+	else
+	    zip_bl_tree[zip_REPZ_11_138].fc++;
+	count = 0; prevlen = curlen;
+	if(nextlen == 0) {
+	    max_count = 138;
+	    min_count = 3;
+	} else if(curlen == nextlen) {
+	    max_count = 6;
+	    min_count = 3;
+	} else {
+	    max_count = 7;
+	    min_count = 4;
+	}
+    }
+}
+
+  /* ==========================================================================
+   * Send a literal or distance tree in compressed form, using the codes in
+   * bl_tree.
+   */
+var zip_send_tree = function(tree, // the tree to be scanned
+		   max_code) { // and its largest code of non zero frequency
+    var n;			// iterates over all tree elements
+    var prevlen = -1;		// last emitted length
+    var curlen;			// length of current code
+    var nextlen = tree[0].dl;	// length of next code
+    var count = 0;		// repeat count of the current code
+    var max_count = 7;		// max repeat count
+    var min_count = 4;		// min repeat count
+
+    /* tree[max_code+1].dl = -1; */  /* guard already set */
+    if(nextlen == 0) {
+      max_count = 138;
+      min_count = 3;
+    }
+
+    for(n = 0; n <= max_code; n++) {
+	curlen = nextlen;
+	nextlen = tree[n+1].dl;
+	if(++count < max_count && curlen == nextlen) {
+	    continue;
+	} else if(count < min_count) {
+	    do { zip_SEND_CODE(curlen, zip_bl_tree); } while(--count != 0);
+	} else if(curlen != 0) {
+	    if(curlen != prevlen) {
+		zip_SEND_CODE(curlen, zip_bl_tree);
+		count--;
+	    }
+	    // Assert(count >= 3 && count <= 6, " 3_6?");
+	    zip_SEND_CODE(zip_REP_3_6, zip_bl_tree);
+	    zip_send_bits(count - 3, 2);
+	} else if(count <= 10) {
+	    zip_SEND_CODE(zip_REPZ_3_10, zip_bl_tree);
+	    zip_send_bits(count-3, 3);
+	} else {
+	    zip_SEND_CODE(zip_REPZ_11_138, zip_bl_tree);
+	    zip_send_bits(count-11, 7);
+	}
+	count = 0;
+	prevlen = curlen;
+	if(nextlen == 0) {
+	    max_count = 138;
+	    min_count = 3;
+	} else if(curlen == nextlen) {
+	    max_count = 6;
+	    min_count = 3;
+	} else {
+	    max_count = 7;
+	    min_count = 4;
+	}
+    }
+}
+
+/* ==========================================================================
+ * Construct the Huffman tree for the bit lengths and return the index in
+ * bl_order of the last bit length code to send.
+ */
+var zip_build_bl_tree = function() {
+    var max_blindex;  // index of last bit length code of non zero freq
+
+    // Determine the bit length frequencies for literal and distance trees
+    zip_scan_tree(zip_dyn_ltree, zip_l_desc.max_code);
+    zip_scan_tree(zip_dyn_dtree, zip_d_desc.max_code);
+
+    // Build the bit length tree:
+    zip_build_tree(zip_bl_desc);
+    /* opt_len now includes the length of the tree representations, except
+     * the lengths of the bit lengths codes and the 5+5+4 bits for the counts.
+     */
+
+    /* Determine the number of bit length codes to send. The pkzip format
+     * requires that at least 4 bit length codes be sent. (appnote.txt says
+     * 3 but the actual value used is 4.)
+     */
+    for(max_blindex = zip_BL_CODES-1; max_blindex >= 3; max_blindex--) {
+	if(zip_bl_tree[zip_bl_order[max_blindex]].dl != 0) break;
+    }
+    /* Update opt_len to include the bit length tree and counts */
+    zip_opt_len += 3*(max_blindex+1) + 5+5+4;
+//    Tracev((stderr, "\ndyn trees: dyn %ld, stat %ld",
+//	    encoder->opt_len, encoder->static_len));
+
+    return max_blindex;
+}
+
+/* ==========================================================================
+ * Send the header for a block using dynamic Huffman trees: the counts, the
+ * lengths of the bit length codes, the literal tree and the distance tree.
+ * IN assertion: lcodes >= 257, dcodes >= 1, blcodes >= 4.
+ */
+var zip_send_all_trees = function(lcodes, dcodes, blcodes) { // number of codes for each tree
+    var rank; // index in bl_order
+
+//    Assert (lcodes >= 257 && dcodes >= 1 && blcodes >= 4, "not enough codes");
+//    Assert (lcodes <= L_CODES && dcodes <= D_CODES && blcodes <= BL_CODES,
+//	    "too many codes");
+//    Tracev((stderr, "\nbl counts: "));
+    zip_send_bits(lcodes-257, 5); // not +255 as stated in appnote.txt
+    zip_send_bits(dcodes-1,   5);
+    zip_send_bits(blcodes-4,  4); // not -3 as stated in appnote.txt
+    for(rank = 0; rank < blcodes; rank++) {
+//      Tracev((stderr, "\nbl code %2d ", bl_order[rank]));
+	zip_send_bits(zip_bl_tree[zip_bl_order[rank]].dl, 3);
+    }
+
+    // send the literal tree
+    zip_send_tree(zip_dyn_ltree,lcodes-1);
+
+    // send the distance tree
+    zip_send_tree(zip_dyn_dtree,dcodes-1);
+}
+
+/* ==========================================================================
+ * Determine the best encoding for the current block: dynamic trees, static
+ * trees or store, and output the encoded block to the zip file.
+ */
+var zip_flush_block = function(eof) { // true if this is the last block for a file
+    var opt_lenb, static_lenb; // opt_len and static_len in bytes
+    var max_blindex;	// index of last bit length code of non zero freq
+    var stored_len;	// length of input block
+
+    stored_len = zip_strstart - zip_block_start;
+    zip_flag_buf[zip_last_flags] = zip_flags; // Save the flags for the last 8 items
+
+    // Construct the literal and distance trees
+    zip_build_tree(zip_l_desc);
+//    Tracev((stderr, "\nlit data: dyn %ld, stat %ld",
+//	    encoder->opt_len, encoder->static_len));
+
+    zip_build_tree(zip_d_desc);
+//    Tracev((stderr, "\ndist data: dyn %ld, stat %ld",
+//	    encoder->opt_len, encoder->static_len));
+    /* At this point, opt_len and static_len are the total bit lengths of
+     * the compressed block data, excluding the tree representations.
+     */
+
+    /* Build the bit length tree for the above two trees, and get the index
+     * in bl_order of the last bit length code to send.
+     */
+    max_blindex = zip_build_bl_tree();
+
+    // Determine the best encoding. Compute first the block length in bytes
+    opt_lenb	= (zip_opt_len   +3+7)>>3;
+    static_lenb = (zip_static_len+3+7)>>3;
+
+//    Trace((stderr, "\nopt %lu(%lu) stat %lu(%lu) stored %lu lit %u dist %u ",
+//	   opt_lenb, encoder->opt_len,
+//	   static_lenb, encoder->static_len, stored_len,
+//	   encoder->last_lit, encoder->last_dist));
+
+    if(static_lenb <= opt_lenb)
+	opt_lenb = static_lenb;
+    if(stored_len + 4 <= opt_lenb // 4: two words for the lengths
+       && zip_block_start >= 0) {
+	var i;
+
+	/* The test buf != NULL is only necessary if LIT_BUFSIZE > WSIZE.
+	 * Otherwise we can't have processed more than WSIZE input bytes since
+	 * the last block flush, because compression would have been
+	 * successful. If LIT_BUFSIZE <= WSIZE, it is never too late to
+	 * transform a block into a stored block.
+	 */
+	zip_send_bits((zip_STORED_BLOCK<<1)+eof, 3);  /* send block type */
+	zip_bi_windup();		 /* align on byte boundary */
+	zip_put_short(stored_len);
+	zip_put_short(~stored_len);
+
+      // copy block
+/*
+      p = &window[block_start];
+      for(i = 0; i < stored_len; i++)
+	put_byte(p[i]);
+*/
+	for(i = 0; i < stored_len; i++)
+	    zip_put_byte(zip_window[zip_block_start + i]);
+
+    } else if(static_lenb == opt_lenb) {
+	zip_send_bits((zip_STATIC_TREES<<1)+eof, 3);
+	zip_compress_block(zip_static_ltree, zip_static_dtree);
+    } else {
+	zip_send_bits((zip_DYN_TREES<<1)+eof, 3);
+	zip_send_all_trees(zip_l_desc.max_code+1,
+			   zip_d_desc.max_code+1,
+			   max_blindex+1);
+	zip_compress_block(zip_dyn_ltree, zip_dyn_dtree);
+    }
+
+    zip_init_block();
+
+    if(eof != 0)
+	zip_bi_windup();
+}
+
+/* ==========================================================================
+ * Save the match info and tally the frequency counts. Return true if
+ * the current block must be flushed.
+ */
+var zip_ct_tally = function(
+	dist, // distance of matched string
+	lc) { // match length-MIN_MATCH or unmatched char (if dist==0)
+    zip_l_buf[zip_last_lit++] = lc;
+    if(dist == 0) {
+	// lc is the unmatched char
+	zip_dyn_ltree[lc].fc++;
+    } else {
+	// Here, lc is the match length - MIN_MATCH
+	dist--;		    // dist = match distance - 1
+//      Assert((ush)dist < (ush)MAX_DIST &&
+//	     (ush)lc <= (ush)(MAX_MATCH-MIN_MATCH) &&
+//	     (ush)D_CODE(dist) < (ush)D_CODES,  "ct_tally: bad match");
+
+	zip_dyn_ltree[zip_length_code[lc]+zip_LITERALS+1].fc++;
+	zip_dyn_dtree[zip_D_CODE(dist)].fc++;
+
+	zip_d_buf[zip_last_dist++] = dist;
+	zip_flags |= zip_flag_bit;
+    }
+    zip_flag_bit <<= 1;
+
+    // Output the flags if they fill a byte
+    if((zip_last_lit & 7) == 0) {
+	zip_flag_buf[zip_last_flags++] = zip_flags;
+	zip_flags = 0;
+	zip_flag_bit = 1;
+    }
+    // Try to guess if it is profitable to stop the current block here
+    if(zip_compr_level > 2 && (zip_last_lit & 0xfff) == 0) {
+	// Compute an upper bound for the compressed length
+	var out_length = zip_last_lit * 8;
+	var in_length = zip_strstart - zip_block_start;
+	var dcode;
+
+	for(dcode = 0; dcode < zip_D_CODES; dcode++) {
+	    out_length += zip_dyn_dtree[dcode].fc * (5 + zip_extra_dbits[dcode]);
+	}
+	out_length >>= 3;
+//      Trace((stderr,"\nlast_lit %u, last_dist %u, in %ld, out ~%ld(%ld%%) ",
+//	     encoder->last_lit, encoder->last_dist, in_length, out_length,
+//	     100L - out_length*100L/in_length));
+	if(zip_last_dist < parseInt(zip_last_lit/2) &&
+	   out_length < parseInt(in_length/2))
+	    return true;
+    }
+    return (zip_last_lit == zip_LIT_BUFSIZE-1 ||
+	    zip_last_dist == zip_DIST_BUFSIZE);
+    /* We avoid equality with LIT_BUFSIZE because of wraparound at 64K
+     * on 16 bit machines and because stored blocks are restricted to
+     * 64K-1 bytes.
+     */
+}
+
+  /* ==========================================================================
+   * Send the block data compressed using the given Huffman trees
+   */
+var zip_compress_block = function(
+	ltree,	// literal tree
+	dtree) {	// distance tree
+    var dist;		// distance of matched string
+    var lc;		// match length or unmatched char (if dist == 0)
+    var lx = 0;		// running index in l_buf
+    var dx = 0;		// running index in d_buf
+    var fx = 0;		// running index in flag_buf
+    var flag = 0;	// current flags
+    var code;		// the code to send
+    var extra;		// number of extra bits to send
+
+    if(zip_last_lit != 0) do {
+	if((lx & 7) == 0)
+	    flag = zip_flag_buf[fx++];
+	lc = zip_l_buf[lx++] & 0xff;
+	if((flag & 1) == 0) {
+	    zip_SEND_CODE(lc, ltree); /* send a literal byte */
+//	Tracecv(isgraph(lc), (stderr," '%c' ", lc));
+	} else {
+	    // Here, lc is the match length - MIN_MATCH
+	    code = zip_length_code[lc];
+	    zip_SEND_CODE(code+zip_LITERALS+1, ltree); // send the length code
+	    extra = zip_extra_lbits[code];
+	    if(extra != 0) {
+		lc -= zip_base_length[code];
+		zip_send_bits(lc, extra); // send the extra length bits
+	    }
+	    dist = zip_d_buf[dx++];
+	    // Here, dist is the match distance - 1
+	    code = zip_D_CODE(dist);
+//	Assert (code < D_CODES, "bad d_code");
+
+	    zip_SEND_CODE(code, dtree);	  // send the distance code
+	    extra = zip_extra_dbits[code];
+	    if(extra != 0) {
+		dist -= zip_base_dist[code];
+		zip_send_bits(dist, extra);   // send the extra distance bits
+	    }
+	} // literal or match pair ?
+	flag >>= 1;
+    } while(lx < zip_last_lit);
+
+    zip_SEND_CODE(zip_END_BLOCK, ltree);
+}
+
+/* ==========================================================================
+ * Send a value on a given number of bits.
+ * IN assertion: length <= 16 and value fits in length bits.
+ */
+var zip_Buf_size = 16; // bit size of bi_buf
+var zip_send_bits = function(
+	value,	// value to send
+	length) {	// number of bits
+    /* If not enough room in bi_buf, use (valid) bits from bi_buf and
+     * (16 - bi_valid) bits from value, leaving (width - (16-bi_valid))
+     * unused bits in value.
+     */
+    if(zip_bi_valid > zip_Buf_size - length) {
+	zip_bi_buf |= (value << zip_bi_valid);
+	zip_put_short(zip_bi_buf);
+	zip_bi_buf = (value >> (zip_Buf_size - zip_bi_valid));
+	zip_bi_valid += length - zip_Buf_size;
+    } else {
+	zip_bi_buf |= value << zip_bi_valid;
+	zip_bi_valid += length;
+    }
+}
+
+/* ==========================================================================
+ * Reverse the first len bits of a code, using straightforward code (a faster
+ * method would use a table)
+ * IN assertion: 1 <= len <= 15
+ */
+var zip_bi_reverse = function(
+	code,	// the value to invert
+	len) {	// its bit length
+    var res = 0;
+    do {
+	res |= code & 1;
+	code >>= 1;
+	res <<= 1;
+    } while(--len > 0);
+    return res >> 1;
+}
+
+/* ==========================================================================
+ * Write out any remaining bits in an incomplete byte.
+ */
+var zip_bi_windup = function() {
+    if(zip_bi_valid > 8) {
+	zip_put_short(zip_bi_buf);
+    } else if(zip_bi_valid > 0) {
+	zip_put_byte(zip_bi_buf);
+    }
+    zip_bi_buf = 0;
+    zip_bi_valid = 0;
+}
+
+var zip_qoutbuf = function() {
+    if(zip_outcnt != 0) {
+	var q, i;
+	q = zip_new_queue();
+	if(zip_qhead == null)
+	    zip_qhead = zip_qtail = q;
+	else
+	    zip_qtail = zip_qtail.next = q;
+	q.len = zip_outcnt - zip_outoff;
+//      System.arraycopy(zip_outbuf, zip_outoff, q.ptr, 0, q.len);
+	for(i = 0; i < q.len; i++)
+	    q.ptr[i] = zip_outbuf[zip_outoff + i];
+	zip_outcnt = zip_outoff = 0;
+    }
+}
+
+var zip_deflate = function(str, level) {
+    var i, j;
+
+    zip_deflate_data = str;
+    zip_deflate_pos = 0;
+    if(typeof level == "undefined")
+	level = zip_DEFAULT_LEVEL;
+    zip_deflate_start(level);
+
+    var buff = new Array(1024);
+    var aout = [];
+    while((i = zip_deflate_internal(buff, 0, buff.length)) > 0) {
+	var cbuf = new Array(i);
+	for(j = 0; j < i; j++){
+	    cbuf[j] = String.fromCharCode(buff[j]);
+	}
+	aout[aout.length] = cbuf.join("");
+    }
+    zip_deflate_data = null; // G.C.
+    return aout.join("");
+}
+
+if (! window.RawDeflate) RawDeflate = {};
+RawDeflate.deflate = zip_deflate;
+
+})();

+ 753 - 0
apps/html2markdown/libs/rawinflate.js

@@ -0,0 +1,753 @@
+/*
+ * $Id: rawinflate.js,v 0.2 2009/03/01 18:32:24 dankogai Exp $
+ *
+ * original:
+ * http://www.onicos.com/staff/iz/amuse/javascript/expert/inflate.txt
+ */
+
+(function(){
+
+/* Copyright (C) 1999 Masanao Izumo <[email protected]>
+ * Version: 1.0.0.1
+ * LastModified: Dec 25 1999
+ */
+
+/* Interface:
+ * data = zip_inflate(src);
+ */
+
+/* constant parameters */
+var zip_WSIZE = 32768;		// Sliding Window size
+var zip_STORED_BLOCK = 0;
+var zip_STATIC_TREES = 1;
+var zip_DYN_TREES    = 2;
+
+/* for inflate */
+var zip_lbits = 9; 		// bits in base literal/length lookup table
+var zip_dbits = 6; 		// bits in base distance lookup table
+var zip_INBUFSIZ = 32768;	// Input buffer size
+var zip_INBUF_EXTRA = 64;	// Extra buffer
+
+/* variables (inflate) */
+var zip_slide;
+var zip_wp;			// current position in slide
+var zip_fixed_tl = null;	// inflate static
+var zip_fixed_td;		// inflate static
+var zip_fixed_bl, fixed_bd;	// inflate static
+var zip_bit_buf;		// bit buffer
+var zip_bit_len;		// bits in bit buffer
+var zip_method;
+var zip_eof;
+var zip_copy_leng;
+var zip_copy_dist;
+var zip_tl, zip_td;	// literal/length and distance decoder tables
+var zip_bl, zip_bd;	// number of bits decoded by tl and td
+
+var zip_inflate_data;
+var zip_inflate_pos;
+
+
+/* constant tables (inflate) */
+var zip_MASK_BITS = new Array(
+    0x0000,
+    0x0001, 0x0003, 0x0007, 0x000f, 0x001f, 0x003f, 0x007f, 0x00ff,
+    0x01ff, 0x03ff, 0x07ff, 0x0fff, 0x1fff, 0x3fff, 0x7fff, 0xffff);
+// Tables for deflate from PKZIP's appnote.txt.
+var zip_cplens = new Array( // Copy lengths for literal codes 257..285
+    3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 15, 17, 19, 23, 27, 31,
+    35, 43, 51, 59, 67, 83, 99, 115, 131, 163, 195, 227, 258, 0, 0);
+/* note: see note #13 above about the 258 in this list. */
+var zip_cplext = new Array( // Extra bits for literal codes 257..285
+    0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2,
+    3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 0, 99, 99); // 99==invalid
+var zip_cpdist = new Array( // Copy offsets for distance codes 0..29
+    1, 2, 3, 4, 5, 7, 9, 13, 17, 25, 33, 49, 65, 97, 129, 193,
+    257, 385, 513, 769, 1025, 1537, 2049, 3073, 4097, 6145,
+    8193, 12289, 16385, 24577);
+var zip_cpdext = new Array( // Extra bits for distance codes
+    0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6,
+    7, 7, 8, 8, 9, 9, 10, 10, 11, 11,
+    12, 12, 13, 13);
+var zip_border = new Array(  // Order of the bit length code lengths
+    16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15);
+/* objects (inflate) */
+
+var zip_HuftList = function() {
+    this.next = null;
+    this.list = null;
+}
+
+var zip_HuftNode = function() {
+    this.e = 0; // number of extra bits or operation
+    this.b = 0; // number of bits in this code or subcode
+
+    // union
+    this.n = 0; // literal, length base, or distance base
+    this.t = null; // (zip_HuftNode) pointer to next level of table
+}
+
+var zip_HuftBuild = function(b,	// code lengths in bits (all assumed <= BMAX)
+		       n,	// number of codes (assumed <= N_MAX)
+		       s,	// number of simple-valued codes (0..s-1)
+		       d,	// list of base values for non-simple codes
+		       e,	// list of extra bits for non-simple codes
+		       mm	// maximum lookup bits
+		   ) {
+    this.BMAX = 16;   // maximum bit length of any code
+    this.N_MAX = 288; // maximum number of codes in any set
+    this.status = 0;	// 0: success, 1: incomplete table, 2: bad input
+    this.root = null;	// (zip_HuftList) starting table
+    this.m = 0;		// maximum lookup bits, returns actual
+
+/* Given a list of code lengths and a maximum table size, make a set of
+   tables to decode that set of codes.	Return zero on success, one if
+   the given code set is incomplete (the tables are still built in this
+   case), two if the input is invalid (all zero length codes or an
+   oversubscribed set of lengths), and three if not enough memory.
+   The code with value 256 is special, and the tables are constructed
+   so that no bits beyond that code are fetched when that code is
+   decoded. */
+    {
+	var a;			// counter for codes of length k
+	var c = new Array(this.BMAX+1);	// bit length count table
+	var el;			// length of EOB code (value 256)
+	var f;			// i repeats in table every f entries
+	var g;			// maximum code length
+	var h;			// table level
+	var i;			// counter, current code
+	var j;			// counter
+	var k;			// number of bits in current code
+	var lx = new Array(this.BMAX+1);	// stack of bits per table
+	var p;			// pointer into c[], b[], or v[]
+	var pidx;		// index of p
+	var q;			// (zip_HuftNode) points to current table
+	var r = new zip_HuftNode(); // table entry for structure assignment
+	var u = new Array(this.BMAX); // zip_HuftNode[BMAX][]  table stack
+	var v = new Array(this.N_MAX); // values in order of bit length
+	var w;
+	var x = new Array(this.BMAX+1);// bit offsets, then code stack
+	var xp;			// pointer into x or c
+	var y;			// number of dummy codes added
+	var z;			// number of entries in current table
+	var o;
+	var tail;		// (zip_HuftList)
+
+	tail = this.root = null;
+	for(i = 0; i < c.length; i++)
+	    c[i] = 0;
+	for(i = 0; i < lx.length; i++)
+	    lx[i] = 0;
+	for(i = 0; i < u.length; i++)
+	    u[i] = null;
+	for(i = 0; i < v.length; i++)
+	    v[i] = 0;
+	for(i = 0; i < x.length; i++)
+	    x[i] = 0;
+
+	// Generate counts for each bit length
+	el = n > 256 ? b[256] : this.BMAX; // set length of EOB code, if any
+	p = b; pidx = 0;
+	i = n;
+	do {
+	    c[p[pidx]]++;	// assume all entries <= BMAX
+	    pidx++;
+	} while(--i > 0);
+	if(c[0] == n) {	// null input--all zero length codes
+	    this.root = null;
+	    this.m = 0;
+	    this.status = 0;
+	    return;
+	}
+
+	// Find minimum and maximum length, bound *m by those
+	for(j = 1; j <= this.BMAX; j++)
+	    if(c[j] != 0)
+		break;
+	k = j;			// minimum code length
+	if(mm < j)
+	    mm = j;
+	for(i = this.BMAX; i != 0; i--)
+	    if(c[i] != 0)
+		break;
+	g = i;			// maximum code length
+	if(mm > i)
+	    mm = i;
+
+	// Adjust last length count to fill out codes, if needed
+	for(y = 1 << j; j < i; j++, y <<= 1)
+	    if((y -= c[j]) < 0) {
+		this.status = 2;	// bad input: more codes than bits
+		this.m = mm;
+		return;
+	    }
+	if((y -= c[i]) < 0) {
+	    this.status = 2;
+	    this.m = mm;
+	    return;
+	}
+	c[i] += y;
+
+	// Generate starting offsets into the value table for each length
+	x[1] = j = 0;
+	p = c;
+	pidx = 1;
+	xp = 2;
+	while(--i > 0)		// note that i == g from above
+	    x[xp++] = (j += p[pidx++]);
+
+	// Make a table of values in order of bit lengths
+	p = b; pidx = 0;
+	i = 0;
+	do {
+	    if((j = p[pidx++]) != 0)
+		v[x[j]++] = i;
+	} while(++i < n);
+	n = x[g];			// set n to length of v
+
+	// Generate the Huffman codes and for each, make the table entries
+	x[0] = i = 0;		// first Huffman code is zero
+	p = v; pidx = 0;		// grab values in bit order
+	h = -1;			// no tables yet--level -1
+	w = lx[0] = 0;		// no bits decoded yet
+	q = null;			// ditto
+	z = 0;			// ditto
+
+	// go through the bit lengths (k already is bits in shortest code)
+	for(; k <= g; k++) {
+	    a = c[k];
+	    while(a-- > 0) {
+		// here i is the Huffman code of length k bits for value p[pidx]
+		// make tables up to required level
+		while(k > w + lx[1 + h]) {
+		    w += lx[1 + h]; // add bits already decoded
+		    h++;
+
+		    // compute minimum size table less than or equal to *m bits
+		    z = (z = g - w) > mm ? mm : z; // upper limit
+		    if((f = 1 << (j = k - w)) > a + 1) { // try a k-w bit table
+			// too few codes for k-w bit table
+			f -= a + 1;	// deduct codes from patterns left
+			xp = k;
+			while(++j < z) { // try smaller tables up to z bits
+			    if((f <<= 1) <= c[++xp])
+				break;	// enough codes to use up j bits
+			    f -= c[xp];	// else deduct codes from patterns
+			}
+		    }
+		    if(w + j > el && w < el)
+			j = el - w;	// make EOB code end at table
+		    z = 1 << j;	// table entries for j-bit table
+		    lx[1 + h] = j; // set table size in stack
+
+		    // allocate and link in new table
+		    q = new Array(z);
+		    for(o = 0; o < z; o++) {
+			q[o] = new zip_HuftNode();
+		    }
+
+		    if(tail == null)
+			tail = this.root = new zip_HuftList();
+		    else
+			tail = tail.next = new zip_HuftList();
+		    tail.next = null;
+		    tail.list = q;
+		    u[h] = q;	// table starts after link
+
+		    /* connect to last table, if there is one */
+		    if(h > 0) {
+			x[h] = i;		// save pattern for backing up
+			r.b = lx[h];	// bits to dump before this table
+			r.e = 16 + j;	// bits in this table
+			r.t = q;		// pointer to this table
+			j = (i & ((1 << w) - 1)) >> (w - lx[h]);
+			u[h-1][j].e = r.e;
+			u[h-1][j].b = r.b;
+			u[h-1][j].n = r.n;
+			u[h-1][j].t = r.t;
+		    }
+		}
+
+		// set up table entry in r
+		r.b = k - w;
+		if(pidx >= n)
+		    r.e = 99;		// out of values--invalid code
+		else if(p[pidx] < s) {
+		    r.e = (p[pidx] < 256 ? 16 : 15); // 256 is end-of-block code
+		    r.n = p[pidx++];	// simple code is just the value
+		} else {
+		    r.e = e[p[pidx] - s];	// non-simple--look up in lists
+		    r.n = d[p[pidx++] - s];
+		}
+
+		// fill code-like entries with r //
+		f = 1 << (k - w);
+		for(j = i >> w; j < z; j += f) {
+		    q[j].e = r.e;
+		    q[j].b = r.b;
+		    q[j].n = r.n;
+		    q[j].t = r.t;
+		}
+
+		// backwards increment the k-bit code i
+		for(j = 1 << (k - 1); (i & j) != 0; j >>= 1)
+		    i ^= j;
+		i ^= j;
+
+		// backup over finished tables
+		while((i & ((1 << w) - 1)) != x[h]) {
+		    w -= lx[h];		// don't need to update q
+		    h--;
+		}
+	    }
+	}
+
+	/* return actual size of base table */
+	this.m = lx[1];
+
+	/* Return true (1) if we were given an incomplete table */
+	this.status = ((y != 0 && g != 1) ? 1 : 0);
+    } /* end of constructor */
+}
+
+
+/* routines (inflate) */
+
+var zip_GET_BYTE = function() {
+    if(zip_inflate_data.length == zip_inflate_pos)
+	return -1;
+    return zip_inflate_data.charCodeAt(zip_inflate_pos++) & 0xff;
+}
+
+var zip_NEEDBITS = function(n) {
+    while(zip_bit_len < n) {
+	zip_bit_buf |= zip_GET_BYTE() << zip_bit_len;
+	zip_bit_len += 8;
+    }
+}
+
+var zip_GETBITS = function(n) {
+    return zip_bit_buf & zip_MASK_BITS[n];
+}
+
+var zip_DUMPBITS = function(n) {
+    zip_bit_buf >>= n;
+    zip_bit_len -= n;
+}
+
+var zip_inflate_codes = function(buff, off, size) {
+    /* inflate (decompress) the codes in a deflated (compressed) block.
+       Return an error code or zero if it all goes ok. */
+    var e;		// table entry flag/number of extra bits
+    var t;		// (zip_HuftNode) pointer to table entry
+    var n;
+
+    if(size == 0)
+      return 0;
+
+    // inflate the coded data
+    n = 0;
+    for(;;) {			// do until end of block
+	zip_NEEDBITS(zip_bl);
+	t = zip_tl.list[zip_GETBITS(zip_bl)];
+	e = t.e;
+	while(e > 16) {
+	    if(e == 99)
+		return -1;
+	    zip_DUMPBITS(t.b);
+	    e -= 16;
+	    zip_NEEDBITS(e);
+	    t = t.t[zip_GETBITS(e)];
+	    e = t.e;
+	}
+	zip_DUMPBITS(t.b);
+
+	if(e == 16) {		// then it's a literal
+	    zip_wp &= zip_WSIZE - 1;
+	    buff[off + n++] = zip_slide[zip_wp++] = t.n;
+	    if(n == size)
+		return size;
+	    continue;
+	}
+
+	// exit if end of block
+	if(e == 15)
+	    break;
+
+	// it's an EOB or a length
+
+	// get length of block to copy
+	zip_NEEDBITS(e);
+	zip_copy_leng = t.n + zip_GETBITS(e);
+	zip_DUMPBITS(e);
+
+	// decode distance of block to copy
+	zip_NEEDBITS(zip_bd);
+	t = zip_td.list[zip_GETBITS(zip_bd)];
+	e = t.e;
+
+	while(e > 16) {
+	    if(e == 99)
+		return -1;
+	    zip_DUMPBITS(t.b);
+	    e -= 16;
+	    zip_NEEDBITS(e);
+	    t = t.t[zip_GETBITS(e)];
+	    e = t.e;
+	}
+	zip_DUMPBITS(t.b);
+	zip_NEEDBITS(e);
+	zip_copy_dist = zip_wp - t.n - zip_GETBITS(e);
+	zip_DUMPBITS(e);
+
+	// do the copy
+	while(zip_copy_leng > 0 && n < size) {
+	    zip_copy_leng--;
+	    zip_copy_dist &= zip_WSIZE - 1;
+	    zip_wp &= zip_WSIZE - 1;
+	    buff[off + n++] = zip_slide[zip_wp++]
+		= zip_slide[zip_copy_dist++];
+	}
+
+	if(n == size)
+	    return size;
+    }
+
+    zip_method = -1; // done
+    return n;
+}
+
+var zip_inflate_stored = function(buff, off, size) {
+    /* "decompress" an inflated type 0 (stored) block. */
+    var n;
+
+    // go to byte boundary
+    n = zip_bit_len & 7;
+    zip_DUMPBITS(n);
+
+    // get the length and its complement
+    zip_NEEDBITS(16);
+    n = zip_GETBITS(16);
+    zip_DUMPBITS(16);
+    zip_NEEDBITS(16);
+    if(n != ((~zip_bit_buf) & 0xffff))
+	return -1;			// error in compressed data
+    zip_DUMPBITS(16);
+
+    // read and output the compressed data
+    zip_copy_leng = n;
+
+    n = 0;
+    while(zip_copy_leng > 0 && n < size) {
+	zip_copy_leng--;
+	zip_wp &= zip_WSIZE - 1;
+	zip_NEEDBITS(8);
+	buff[off + n++] = zip_slide[zip_wp++] =
+	    zip_GETBITS(8);
+	zip_DUMPBITS(8);
+    }
+
+    if(zip_copy_leng == 0)
+      zip_method = -1; // done
+    return n;
+}
+
+var zip_inflate_fixed = function(buff, off, size) {
+    /* decompress an inflated type 1 (fixed Huffman codes) block.  We should
+       either replace this with a custom decoder, or at least precompute the
+       Huffman tables. */
+
+    // if first time, set up tables for fixed blocks
+    if(zip_fixed_tl == null) {
+	var i;			// temporary variable
+	var l = new Array(288);	// length list for huft_build
+	var h;	// zip_HuftBuild
+
+	// literal table
+	for(i = 0; i < 144; i++)
+	    l[i] = 8;
+	for(; i < 256; i++)
+	    l[i] = 9;
+	for(; i < 280; i++)
+	    l[i] = 7;
+	for(; i < 288; i++)	// make a complete, but wrong code set
+	    l[i] = 8;
+	zip_fixed_bl = 7;
+
+	h = new zip_HuftBuild(l, 288, 257, zip_cplens, zip_cplext,
+			      zip_fixed_bl);
+	if(h.status != 0) {
+	    alert("HufBuild error: "+h.status);
+	    return -1;
+	}
+	zip_fixed_tl = h.root;
+	zip_fixed_bl = h.m;
+
+	// distance table
+	for(i = 0; i < 30; i++)	// make an incomplete code set
+	    l[i] = 5;
+	zip_fixed_bd = 5;
+
+	h = new zip_HuftBuild(l, 30, 0, zip_cpdist, zip_cpdext, zip_fixed_bd);
+	if(h.status > 1) {
+	    zip_fixed_tl = null;
+	    alert("HufBuild error: "+h.status);
+	    return -1;
+	}
+	zip_fixed_td = h.root;
+	zip_fixed_bd = h.m;
+    }
+
+    zip_tl = zip_fixed_tl;
+    zip_td = zip_fixed_td;
+    zip_bl = zip_fixed_bl;
+    zip_bd = zip_fixed_bd;
+    return zip_inflate_codes(buff, off, size);
+}
+
+var zip_inflate_dynamic = function(buff, off, size) {
+    // decompress an inflated type 2 (dynamic Huffman codes) block.
+    var i;		// temporary variables
+    var j;
+    var l;		// last length
+    var n;		// number of lengths to get
+    var t;		// (zip_HuftNode) literal/length code table
+    var nb;		// number of bit length codes
+    var nl;		// number of literal/length codes
+    var nd;		// number of distance codes
+    var ll = new Array(286+30); // literal/length and distance code lengths
+    var h;		// (zip_HuftBuild)
+
+    for(i = 0; i < ll.length; i++)
+	ll[i] = 0;
+
+    // read in table lengths
+    zip_NEEDBITS(5);
+    nl = 257 + zip_GETBITS(5);	// number of literal/length codes
+    zip_DUMPBITS(5);
+    zip_NEEDBITS(5);
+    nd = 1 + zip_GETBITS(5);	// number of distance codes
+    zip_DUMPBITS(5);
+    zip_NEEDBITS(4);
+    nb = 4 + zip_GETBITS(4);	// number of bit length codes
+    zip_DUMPBITS(4);
+    if(nl > 286 || nd > 30)
+      return -1;		// bad lengths
+
+    // read in bit-length-code lengths
+    for(j = 0; j < nb; j++)
+    {
+	zip_NEEDBITS(3);
+	ll[zip_border[j]] = zip_GETBITS(3);
+	zip_DUMPBITS(3);
+    }
+    for(; j < 19; j++)
+	ll[zip_border[j]] = 0;
+
+    // build decoding table for trees--single level, 7 bit lookup
+    zip_bl = 7;
+    h = new zip_HuftBuild(ll, 19, 19, null, null, zip_bl);
+    if(h.status != 0)
+	return -1;	// incomplete code set
+
+    zip_tl = h.root;
+    zip_bl = h.m;
+
+    // read in literal and distance code lengths
+    n = nl + nd;
+    i = l = 0;
+    while(i < n) {
+	zip_NEEDBITS(zip_bl);
+	t = zip_tl.list[zip_GETBITS(zip_bl)];
+	j = t.b;
+	zip_DUMPBITS(j);
+	j = t.n;
+	if(j < 16)		// length of code in bits (0..15)
+	    ll[i++] = l = j;	// save last length in l
+	else if(j == 16) {	// repeat last length 3 to 6 times
+	    zip_NEEDBITS(2);
+	    j = 3 + zip_GETBITS(2);
+	    zip_DUMPBITS(2);
+	    if(i + j > n)
+		return -1;
+	    while(j-- > 0)
+		ll[i++] = l;
+	} else if(j == 17) {	// 3 to 10 zero length codes
+	    zip_NEEDBITS(3);
+	    j = 3 + zip_GETBITS(3);
+	    zip_DUMPBITS(3);
+	    if(i + j > n)
+		return -1;
+	    while(j-- > 0)
+		ll[i++] = 0;
+	    l = 0;
+	} else {		// j == 18: 11 to 138 zero length codes
+	    zip_NEEDBITS(7);
+	    j = 11 + zip_GETBITS(7);
+	    zip_DUMPBITS(7);
+	    if(i + j > n)
+		return -1;
+	    while(j-- > 0)
+		ll[i++] = 0;
+	    l = 0;
+	}
+    }
+
+    // build the decoding tables for literal/length and distance codes
+    zip_bl = zip_lbits;
+    h = new zip_HuftBuild(ll, nl, 257, zip_cplens, zip_cplext, zip_bl);
+    if(zip_bl == 0)	// no literals or lengths
+	h.status = 1;
+    if(h.status != 0) {
+	if(h.status == 1)
+	    ;// **incomplete literal tree**
+	return -1;		// incomplete code set
+    }
+    zip_tl = h.root;
+    zip_bl = h.m;
+
+    for(i = 0; i < nd; i++)
+	ll[i] = ll[i + nl];
+    zip_bd = zip_dbits;
+    h = new zip_HuftBuild(ll, nd, 0, zip_cpdist, zip_cpdext, zip_bd);
+    zip_td = h.root;
+    zip_bd = h.m;
+
+    if(zip_bd == 0 && nl > 257) {   // lengths but no distances
+	// **incomplete distance tree**
+	return -1;
+    }
+
+    if(h.status == 1) {
+	;// **incomplete distance tree**
+    }
+    if(h.status != 0)
+	return -1;
+
+    // decompress until an end-of-block code
+    return zip_inflate_codes(buff, off, size);
+}
+
+var zip_inflate_start = function() {
+    var i;
+
+    if(zip_slide == null)
+	zip_slide = new Array(2 * zip_WSIZE);
+    zip_wp = 0;
+    zip_bit_buf = 0;
+    zip_bit_len = 0;
+    zip_method = -1;
+    zip_eof = false;
+    zip_copy_leng = zip_copy_dist = 0;
+    zip_tl = null;
+}
+
+var zip_inflate_internal = function(buff, off, size) {
+    // decompress an inflated entry
+    var n, i;
+
+    n = 0;
+    while(n < size) {
+	if(zip_eof && zip_method == -1)
+	    return n;
+
+	if(zip_copy_leng > 0) {
+	    if(zip_method != zip_STORED_BLOCK) {
+		// STATIC_TREES or DYN_TREES
+		while(zip_copy_leng > 0 && n < size) {
+		    zip_copy_leng--;
+		    zip_copy_dist &= zip_WSIZE - 1;
+		    zip_wp &= zip_WSIZE - 1;
+		    buff[off + n++] = zip_slide[zip_wp++] =
+			zip_slide[zip_copy_dist++];
+		}
+	    } else {
+		while(zip_copy_leng > 0 && n < size) {
+		    zip_copy_leng--;
+		    zip_wp &= zip_WSIZE - 1;
+		    zip_NEEDBITS(8);
+		    buff[off + n++] = zip_slide[zip_wp++] = zip_GETBITS(8);
+		    zip_DUMPBITS(8);
+		}
+		if(zip_copy_leng == 0)
+		    zip_method = -1; // done
+	    }
+	    if(n == size)
+		return n;
+	}
+
+	if(zip_method == -1) {
+	    if(zip_eof)
+		break;
+
+	    // read in last block bit
+	    zip_NEEDBITS(1);
+	    if(zip_GETBITS(1) != 0)
+		zip_eof = true;
+	    zip_DUMPBITS(1);
+
+	    // read in block type
+	    zip_NEEDBITS(2);
+	    zip_method = zip_GETBITS(2);
+	    zip_DUMPBITS(2);
+	    zip_tl = null;
+	    zip_copy_leng = 0;
+	}
+
+	switch(zip_method) {
+	  case 0: // zip_STORED_BLOCK
+	    i = zip_inflate_stored(buff, off + n, size - n);
+	    break;
+
+	  case 1: // zip_STATIC_TREES
+	    if(zip_tl != null)
+		i = zip_inflate_codes(buff, off + n, size - n);
+	    else
+		i = zip_inflate_fixed(buff, off + n, size - n);
+	    break;
+
+	  case 2: // zip_DYN_TREES
+	    if(zip_tl != null)
+		i = zip_inflate_codes(buff, off + n, size - n);
+	    else
+		i = zip_inflate_dynamic(buff, off + n, size - n);
+	    break;
+
+	  default: // error
+	    i = -1;
+	    break;
+	}
+
+	if(i == -1) {
+	    if(zip_eof)
+		return 0;
+	    return -1;
+	}
+	n += i;
+    }
+    return n;
+}
+
+var zip_inflate = function(str) {
+    var i, j;
+
+    zip_inflate_start();
+    zip_inflate_data = str;
+    zip_inflate_pos = 0;
+
+    var buff = new Array(1024);
+    var aout = [];
+    while((i = zip_inflate_internal(buff, 0, buff.length)) > 0) {
+	var cbuf = new Array(i);
+	for(j = 0; j < i; j++){
+	    cbuf[j] = String.fromCharCode(buff[j]);
+	}
+	aout[aout.length] = cbuf.join("");
+    }
+    zip_inflate_data = null; // G.C.
+    return aout.join("");
+}
+
+if (! window.RawDeflate) RawDeflate = {};
+RawDeflate.inflate = zip_inflate;
+
+})();

La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 34 - 0
apps/image-base64/index.css


+ 85 - 0
apps/image-base64/index.html

@@ -0,0 +1,85 @@
+<!DOCTYPE HTML>
+<html lang="zh-CN">
+    <head>
+        <title>图片Base64工具(DataURI数据)</title>
+        <meta charset="UTF-8">
+        <link rel="shortcut icon" href="../static/img/favicon.ico">
+        <link rel="stylesheet" href="index.css" />
+        <script type="text/javascript" src="../static/vendor/evalCore.min.js"></script>
+        <script type="text/javascript" src="../static/vendor/vue/vue.js"></script>
+    </head>
+    <body>
+        <div class="wrapper" id="pageContainer">
+            <div class="panel panel-default" style="margin-bottom: 0px;">
+                <div class="panel-heading">
+                    <h3 class="panel-title">
+                        <a href="https://www.baidufe.com/fehelper/index/index.html" target="_blank" class="x-a-high">
+                            <img src="../static/img/fe-16.png" alt="fehelper"/> FeHelper
+                        </a>:{{ toolName[curType] }}
+
+                        <span class="x-switch ui-fl-r" ref="btnSwitch" @click="trans">切换为{{toolName[nextType]}}&gt;&gt;</span>
+                    </h3>
+                </div>
+            </div>
+            <div class="panel-body mod-imagebase64" ref="imageBase64" v-show="curType=='image'">
+                <div class="row">
+                  <table>
+                      <tr>
+                          <td>
+                            <div class="x-panel" ref="panelBox">
+                              <img id="preview" alt="" :src="previewSrc" v-show="!!previewSrc.length">
+                              <div class="x-tips">
+                                  <a id="upload" href="#" ref="uploadBox" @click="upload($event)">选择图片</a><br>
+                                  或者选择一张图片拖拽图片到这里来
+                              </div>
+                            </div>
+
+                              <div class="tips">
+                                  1、支持<i>屏幕截图</i>后直接在此处粘贴进行转化<br/>2、支持<i>复制文件、复制图片</i>在线地址在此处直接粘贴进行转化
+                              </div>
+                          </td>
+                          <td>
+                              <textarea id="base64Result" title="点击自动选择" placeholder="内容会自动生成..." readonly ref="resultBox" @click="select()" v-model="resultContent" class="form-control"></textarea>
+                              <div class="x-result-info">
+                                  <div class="x-item">
+                                      <span class="x-title">原始图片大小:</span><span id="sizeOri">{{sizeOri}}</span>
+                                  </div>
+                                  <div class="x-item">
+                                      <span class="x-title">DataUri&nbsp;&nbsp;大小:</span><span id="sizeBase">{{sizeBase}}</span>
+                                  </div>
+                              </div>
+                          </td>
+                      </tr>
+                  </table>
+                  <form action="#">
+                      <input type="file" id="file" accept=".jpg,.jpeg,.gif,.png,.bmp" ref="fileBox" @change="convert()">
+                  </form>
+                  <img id="img" alt="">
+                </div>
+            </div>
+
+            <div class="panel-body mod-base64image" ref="base64Image" v-show="curType=='base64'">
+                <div class="row">
+                    <table>
+                        <tr>
+                            <td>
+                                <textarea id="base64Input" class="form-control" title="点击自动选择" placeholder="在这里粘贴DataURI数据..." v-model="txtBase64Input"></textarea>
+                            </td>
+                            <td>
+                                <div class="x-panel">
+                                    <img id="base64Image" alt="" :src="txtBase64Output" v-show="!!txtBase64Input.length" @error="loadError">
+                                </div>
+                            </td>
+                        </tr>
+                    </table>
+                </div>
+            </div>
+
+            <div v-show="!!error.length" v-cloak class="x-error" v-html="error"></div>
+        </div>
+
+        <script type="text/javascript" src="index.js"></script>
+
+        <script src="../static/vendor/jquery/jquery-3.3.1.min.js"></script>
+    </body>
+</html>

+ 216 - 0
apps/image-base64/index.js

@@ -0,0 +1,216 @@
+/**
+ * FeHelper Image Base64 Tools
+ */
+new Vue({
+    el: '#pageContainer',
+    data: {
+        sizeOri: '暂无数据',
+        sizeBase: '暂无数据',
+        previewSrc: '',
+        resultContent: '',
+        toolName: {'image': '图片转Base64', 'base64': 'Base64转图片'},
+        curType: 'image',
+        nextType: 'base64',
+        txtBase64Input: '',
+        txtBase64Output: '',
+        error:''
+    },
+
+    watch: {
+        txtBase64Input:{
+            immediate: true,
+            handler(newVal, oldVal) {
+                this.error = ''
+                this.txtBase64Output = ''
+                if(newVal.length === 0) return
+                if(newVal.indexOf("data:") === -1) {
+                    this.txtBase64Output = "data:image/jpeg;base64,"+newVal
+                } else {
+                    this.txtBase64Output = newVal
+                }
+            },
+        }
+    },
+
+    mounted: function () {
+
+        // 在tab创建或者更新时候,监听事件,看看是否有参数传递过来
+        if (location.protocol === 'chrome-extension:') {
+            chrome.tabs.query({currentWindow: true,active: true, }, (tabs) => {
+                let activeTab = tabs.filter(tab => tab.active)[0];
+                chrome.runtime.sendMessage({
+                    type: 'fh-dynamic-any-thing',
+                    thing: 'request-page-content',
+                    tabId: activeTab.id
+                }).then(resp => {
+                    if(!resp || !resp.content) return ;
+                    if (this.curType !== 'image') {
+                        this.trans();
+                    }
+                    this.convertOnline(resp.content, flag => {
+                        if (!flag) {
+                            alert('抱歉,' + resp.content + ' 对应的图片未转码成功!');
+                        }
+                    });
+                });
+            });
+        }
+
+        //监听paste事件
+        document.addEventListener('paste', (event) => {
+            if (this.curType !== 'image') return;
+            this.paste(event);
+        }, false);
+
+        // 监听拖拽
+        document.addEventListener('drop', (event) => {
+            event.preventDefault();
+            event.stopPropagation();
+            if (this.curType !== 'image') return;
+            let files = event.dataTransfer.files;
+            if (files.length) {
+                if (/image\//.test(files[0].type)) {
+                    this._getDataUri(files[0]);
+                } else {
+                    alert('请选择图片文件!');
+                }
+            }
+        }, false);
+
+        document.addEventListener('dragover', (event) => {
+            if (this.curType !== 'image') return;
+            event.preventDefault();
+            event.stopPropagation();
+        }, false);
+    },
+    methods: {
+
+        _sizeFormat: function (num) {
+            if (isNaN(num)) {
+                return '暂无数据';
+            }
+            num = +num;
+            if (num < 1024) {
+                return num + ' B';
+            } else if (num < 1024 * 1024) {
+                return (num / 1024).toFixed(2) + ' KB';
+            } else {
+                return (num / 1024 / 1024).toFixed(2) + ' MB';
+            }
+        },
+
+        _getDataUri: function (file) {
+            let reader = new FileReader();
+            reader.onload = (evt) => {
+                this.resultContent = evt.target.result;
+                this.previewSrc = evt.target.result;
+                this.$refs.panelBox.style.backgroundImage = 'none';
+                this.sizeOri = this._sizeFormat(file.size);
+                this.sizeBase = this._sizeFormat(evt.target.result.length);
+            };
+            reader.readAsDataURL(file);
+        },
+
+        convertOnline: function (onlineSrc, callback) {
+            let that = this;
+            that.previewSrc = onlineSrc;
+            let image = new Image();
+            image.src = onlineSrc;
+            image.onload = function () {
+                let width = this.naturalWidth;
+                let height = this.naturalHeight;
+
+                // url方式解码失败,再转换成data uri后继续解码
+                (function createCanvasContext(img, t, l, w, h) {
+                    let canvas = document.createElement('canvas');
+                    canvas.setAttribute('id', 'qr-canvas');
+                    canvas.height = h + 100;
+                    canvas.width = w + 100;
+                    let context = canvas.getContext('2d');
+                    context.fillStyle = 'rgb(255,255,255)';
+                    context.fillRect(0, 0, canvas.width, canvas.height);
+                    context.drawImage(img, l, t, w, h, 50, 50, w, h);
+
+                    that.resultContent = canvas.toDataURL();
+                    that.$refs.panelBox.style.backgroundImage = 'none';
+                    that.sizeOri = width + 'x' + height;
+                    that.sizeBase = that._sizeFormat(that.resultContent.length);
+
+                    callback && callback(true);
+                })(image, 0, 0, width, height);
+            };
+            image.onerror = function () {
+                callback && callback(false);
+            };
+        },
+
+        convert: function () {
+            if (this.$refs.fileBox.files.length) {
+                this._getDataUri(this.$refs.fileBox.files[0]);
+                this.$refs.fileBox.value = '';
+            }
+        },
+
+        select: function () {
+            this.$refs.resultBox.select();
+        },
+
+        upload: function (evt) {
+            evt.preventDefault();
+            this.$refs.fileBox.click();
+        },
+
+        paste: function (event) {
+            let items = event.clipboardData.items || {};
+
+            // 优先处理图片
+            for (let index in items) {
+                let item = items[index];
+                if (/image\//.test(item.type)) {
+                    let file = item.getAsFile();
+                    return this._getDataUri(file);
+                }
+            }
+
+            // 然后处理url
+            try {
+                // 逐个遍历
+                (async () => {
+                    for (let index in items) {
+                        let item = items[index];
+                        if (/text\/plain/.test(item.type)) {
+                            let url = await new Promise(resolve => {
+                                item.getAsString(url => resolve(url))
+                            });
+                            let flag = await new Promise(resolve => {
+                                this.convertOnline(url, flag => resolve(flag));
+                            });
+                            if (flag) break;
+                        }
+                    }
+                })();
+            } catch (ex) {
+                // 只能处理一个了
+                for (let index in items) {
+                    let item = items[index];
+                    if (/text\/plain/.test(item.type)) {
+                        return item.getAsString(url => {
+                            this.convertOnline(url);
+                        });
+                    }
+                }
+            }
+        },
+
+        trans: function () {
+            this.curType = {image: 'base64', base64: 'image'}[this.curType];
+            this.nextType = {image: 'base64', base64: 'image'}[this.nextType];
+        },
+
+        loadError: function (e) {
+            if (this.curType === 'base64' && this.txtBase64Input.trim().length) {
+                this.error = ('无法识别的Base64编码,请确认是正确的图片Data URI?');
+            }
+        }
+    }
+});

+ 53 - 0
apps/json-diff/index.css

@@ -0,0 +1,53 @@
+@import url("../static/vendor/codemirror/codemirror.css");
+@import url("../static/css/bootstrap.min.css");
+
+.wp-json {
+    width:auto;
+}
+.wp-json .mod-json {
+    position: absolute;
+    top: 60px;
+    bottom: 0;
+    right:0;
+    left:0;
+}
+body[browser-extension] .wp-json .mod-json {
+    top: 60px;
+}
+.wp-json .mod-json .panel-txt {
+    position: absolute;
+    width: 500px;
+    top: 15px;
+    bottom: 0;
+}
+.wp-json .panel-body {
+    padding:15px 0;
+}
+.wp-json .CodeMirror-scroll {
+    min-height: 550px;
+}
+
+.box-wrapper-left {
+    height: 100%;
+    padding:0 5px 0 0;
+}
+.box-wrapper-right {
+    height: 100%;
+    padding:0 0 0 5px;
+}
+#jsonSourceLeft, #jsonSourceRight, .CodeMirror {
+    height: calc(100%);
+    font-size: 10px;
+}
+.x-error {
+    color: #1f0606;
+    font-size: 12px;
+    margin-left: 30px;
+}
+.x-error .x-hlt {
+    color:#f00;
+    font-weight: bold;
+}
+.x-error .x-hlt1 {
+    color:#f00;
+}

+ 47 - 0
apps/json-diff/index.html

@@ -0,0 +1,47 @@
+<!DOCTYPE HTML>
+<html lang="zh-CN">
+    <head>
+        <title>JSON比对工具</title>
+        <meta charset="UTF-8">
+        <link rel="shortcut icon" href="../static/img/favicon.ico">
+        <link rel="stylesheet" href="index.css" />
+        <script type="text/javascript" src="../static/vendor/evalCore.min.js"></script>
+        <script type="text/javascript" src="../static/vendor/vue/vue.js"></script>
+    </head>
+    <body>
+        <div class="wrapper wp-json" id="pageContainer">
+            <div class="panel panel-default" style="margin-bottom: 0px;">
+                <div class="panel-heading">
+                    <h3 class="panel-title">
+                        <a href="https://www.baidufe.com/fehelper/index/index.html" target="_blank" class="x-a-high">
+                            <img src="../static/img/fe-16.png" alt="fehelper"/> FeHelper</a>:JSON比对工具
+
+                        <span class="x-error" v-bind:class="{'x-hlt' : errorHighlight}" v-html="errorMessage"></span>
+                    </h3>
+                </div>
+            </div>
+
+            <div class="panel-body mod-json">
+                <div class="col-md-6 box-wrapper-left">
+                    <textarea class="form-control mod-textarea" id="jsonSourceLeft" ref="srcLeft" placeholder="在这里粘贴JSON代码"></textarea>
+                </div>
+                <div class="col-md-6 box-wrapper-right">
+                    <textarea class="form-control mod-textarea" id="jsonSourceRight" ref="srcRight" placeholder="在这里粘贴JSON代码"></textarea>
+                </div>
+            </div>
+
+        </div>
+        <script src="../static/vendor/jquery/jquery-3.3.1.min.js"></script>
+        <script src="../static/vendor/codemirror/codemirror.js"></script>
+        <script src="../static/vendor/codemirror/javascript.js"></script>
+        <script src="../static/vendor/codemirror/active-line.js"></script>
+        <script src="../static/vendor/codemirror/matchbrackets.js"></script>
+        <script src="../static/vendor/codemirror/placeholder.js"></script>
+        <script src="../static/vendor/codemirror/formatting.js"></script>
+        <script src="../static/vendor/json-diff/backbone-events.min.js"></script>
+        <script src="../static/vendor/json-diff/json-patch-duplex.min.js"></script>
+        <script src="../static/vendor/json-diff/json-source-map.js"></script>
+        <script src="../static/vendor/json-diff/json-diff.js"></script>
+        <script src="./index.js"></script>
+    </body>
+</html>

+ 42 - 0
apps/json-diff/index.js

@@ -0,0 +1,42 @@
+new Vue({
+    el: '#pageContainer',
+    data: {
+        errorMessage: '',
+        errorHighlight: false
+    },
+    mounted: function () {
+        // 错误处理器
+        let errorHandler = (which, ok) => {
+            let message = '';
+            if (ok) {
+                message = '两侧JSON比对完成!';
+                this.errorHighlight = false;
+            } else {
+                let side = {'left': '左', 'right': '右', 'left-right': '两'}[which];
+                if(!jsonBox.left.getValue().trim().length) {
+                    message = '请在<span class="x-hlt1">左侧</span>填入待比对的JSON内容!'
+                }else if(!jsonBox.right.getValue().trim().length) {
+                    message = '请在<span class="x-hlt1">右侧</span>填入待比对的JSON内容!'
+                }else{
+                    message = '<span class="x-hlt1">' + side + '侧</span>JSON不合法!';
+                }
+                this.errorHighlight = true;
+            }
+            this.errorMessage = '<span class="x-hlt">Tips:</span>' + message;
+        };
+
+        // diff处理器
+        let diffHandler = (diffs) => {
+            if (!this.errorHighlight) {
+                if (diffs.length) {
+                    this.errorMessage += '共有 <span class="x-hlt">' + diffs.length + '</span> 处不一致!';
+                } else {
+                    this.errorMessage += '且JSON内容一致!';
+                }
+            }
+        };
+
+        // 代码比对
+        let jsonBox = JsonDiff.init(this.$refs.srcLeft, this.$refs.srcRight, errorHandler, diffHandler);
+    }
+});

+ 876 - 0
apps/json-format/content-script.css

@@ -0,0 +1,876 @@
+#jfContent {
+    -webkit-user-select: text;
+    margin: 0;
+}
+
+.xjf-btn {
+    cursor: pointer;
+    -webkit-border-radius: 2px;
+    -webkit-box-shadow: 0px 1px 3px rgba(0, 0, 0, 0.1);
+    -webkit-user-select: none;
+    background: -webkit-linear-gradient(#fafafa, #f4f4f4 40%, #e5e5e5);
+    outline: none;
+    border: 1px solid #aaa;
+    color: #444;
+    font-size: 12px;
+    margin-bottom: 0px;
+    min-width: 4em;
+    position: relative;
+    z-index: 10;
+    display: inline-block;
+    padding: 2px 10px;
+    text-shadow: 1px 1px rgba(255, 255, 255, 0.3)
+}
+
+.xjf-btn-mid, .xjf-btn-right {
+    margin-left: 0;
+    border-top-left-radius: 0;
+    border-bottom-left-radius: 0;
+}
+
+.xjf-btn-mid, .xjf-btn-left {
+    margin-right: 0;
+    border-top-right-radius: 0;
+    border-bottom-right-radius: 0;
+    border-right: none
+}
+
+.xjf-btn:hover {
+    -webkit-box-shadow: 0px 1px 3px rgba(0, 0, 0, 0.2);
+    background: #ebebeb -webkit-linear-gradient(#fefefe, #f8f8f8 40%, #e9e9e9);
+    border-color: #999;
+    color: #222
+}
+
+.xjf-btn:active {
+    -webkit-box-shadow: inset 0px 1px 3px rgba(0, 0, 0, 0.2);
+    background: #ebebeb -webkit-linear-gradient(#f4f4f4, #efefef 40%, #dcdcdc);
+    color: #333
+}
+
+.xjf-btn.selected {
+    -webkit-box-shadow: inset 0px 1px 5px rgba(0, 0, 0, 0.2);
+    background: #ebebeb -webkit-linear-gradient(#e4e4e4, #dfdfdf 40%, #dcdcdc);
+    color: #333
+}
+
+#jsonpOpener, #jsonpCloser {
+    padding: 4px 0 0 8px;
+    color: black;
+    margin-bottom: -6px
+}
+
+#jsonpCloser {
+    margin-top: 0
+}
+
+#formattedJson {
+    padding-left: 21px;
+    padding-top: 6px
+}
+
+html.fh-jf  pre {
+    /* padding: 36px 5px 5px 5px */
+}
+
+#jfContent {
+    margin-bottom: 25px;
+}
+
+/*================json format style start===================*/
+html.fh-jf .item {
+    display: block;
+    padding-left: 20px;
+    margin-left: -20px;
+    position: relative;
+    padding-top:1px;
+    padding-bottom: 1px;
+}
+
+html.fh-jf .item .kv-list {
+    display: block;
+    padding-left: 24px;
+    border-left: 1px dashed #bbb;
+    margin-left: 2px
+}
+html.fh-jf .item .string {
+    word-wrap: break-word
+}
+
+html.fh-jf .item .string a {
+    text-decoration: underline;
+}
+
+html.fh-jf .item .string a:hover {
+    color: #b00;
+}
+
+html.fh-jf .item .brace {
+    font-weight: bold;
+}
+
+html.fh-jf .item .expand {
+    width: 20px;
+    height: 18px;
+    display: block;
+    position: absolute;
+    left: -2px;
+    top: 4px;
+    z-index: 5;
+    opacity: 0.35;
+    -webkit-user-select: none;
+    cursor: pointer;
+}
+
+html.fh-jf .item .expand:after {
+    content: "\25bc";
+}
+
+html.fh-jf .item .expand:hover {
+    opacity: 0.35
+}
+
+html.fh-jf .item .expand:active {
+    opacity: 0.5
+}
+
+html.fh-jf .item.collapsed {
+    white-space: nowrap
+}
+
+html.fh-jf .item.collapsed > .kv-list {
+    display: none
+}
+
+html.fh-jf .item.collapsed .item .expand {
+    display: none
+}
+
+html.fh-jf .item.collapsed > .ellipsis:after {
+    content: "\2026";
+    font-weight: bold
+}
+
+html.fh-jf .item.collapsed > .ellipsis {
+    margin: 0 4px;
+    color: #888
+}
+
+html.fh-jf .item.collapsed .item {
+    display: inline
+}
+
+html.fh-jf .item.collapsed > .expand {
+    -webkit-transform: rotate(-90deg);
+    top: -1px
+}
+html.fh-jf .remove-quote .quote {
+    display: none;
+}
+/*================json format style end===================*/
+
+
+#formattedJson, #jsonpOpener, #jsonpCloser {
+    color: #333;
+    font: 14px/18px monospace;
+}
+
+#formattedJson {
+    color: #444
+}
+
+
+#formattingMsg {
+    position: absolute;
+    top: calc(40vh);
+    left: calc(45vw);
+    z-index: 100;
+    display: none;
+}
+
+#formattingMsg .x-loading {
+    width: 12px;
+    height: 12px;
+    border: 1px solid #f00;
+    border-radius: 50%;
+    box-shadow: 0 0 10px 2px;
+    color: #cc0000;
+    border-right-color: transparent;
+    border-top-color: transparent;
+    animation: spin-right 1s linear infinite normal;
+    animation-delay: 0s;
+    margin: 0 5px 0 0;
+    display: inline-block;
+}
+
+#formattingMsg .x-loading:before {
+    display: block;
+    width: 8px;
+    height: 8px;
+    margin: 1px;
+    border: 2px solid #f00;
+    content: " ";
+    border-radius: 50%;
+    border-left-color: transparent;
+    border-bottom-color: transparent;
+}
+
+@keyframes spin-right {
+    from {
+        transform: rotate(0deg);
+        opacity: 0.2;
+    }
+    50% {
+        transform: rotate(180deg);
+        opacity: 1.0;
+    }
+    to {
+        transform: rotate(360deg);
+        opacity: 0.2;
+    }
+}
+
+[hidden] {
+    display: none !important
+}
+
+#jfContentspan {
+    white-space: pre-wrap
+}
+
+@-webkit-keyframes spin {
+    from {
+        -webkit-transform: rotate(0deg)
+    }
+    to {
+        -webkit-transform: rotate(360deg)
+    }
+}
+
+#spinner {
+    -webkit-animation: spin 1s 0 infinite
+}
+
+html.fh-jf  * {
+    -webkit-font-smoothing: antialiased
+}
+
+#jfContent .x-json-tips {
+    color: red;
+}
+
+#jfContent_pre {
+    padding: 0;
+    margin: 0;
+    word-break: break-all;
+}
+
+html.fh-jf  {
+    font-size: 14px;
+    color: #333;
+    direction: ltr;
+}
+
+html.fh-jf  body {
+    direction: inherit;
+    margin: 0;
+}
+
+html..fh-jf body {
+    padding: 0 8px;
+}
+
+/* 工具栏 */
+html.fh-jf .x-toolbar {
+    background-color: #f5f5f5;
+    background: -webkit-linear-gradient(#fafafa, #f4f4f4 40%, #e5e5e5);
+    margin: 0 0 10px ;
+    border: 1px solid #ddd;
+    border-bottom-color: #ccc;
+    border-radius: 4px;
+    -webkit-box-shadow: 0 1px 1px rgba(0, 0, 0, .05);
+    box-shadow: 0 1px 1px rgba(0, 0, 0, .05);
+    padding: 5px 15px;
+    position: sticky;
+    top: 0;
+    z-index: 10000;
+    font-size: 12px;
+    user-select: none;
+}
+
+html.fh-jf .x-toolbar .x-sort input {
+    margin-right: 10px;
+}
+html.fh-jf .x-toolbar span {
+    white-space: normal !important;
+}
+
+html.fh-jf  .x-toolbar .x-sort input#sort_desc {
+    margin-right: 0;
+}
+
+html.fh-jf  .x-toolbar .x-sort {
+    display: inline;
+    margin: 0;
+}
+
+html.fh-jf  .x-toolbar .x-split {
+    margin: 0 15px;
+    color: #ccc;
+    display: inline;
+}
+
+html.fh-jf  .x-toolbar label {
+    font-weight: normal;
+    margin-bottom: 0;
+}
+
+html.fh-jf  .x-toolbar img {
+    vertical-align: middle;
+}
+
+html.fh-jf  .x-toolbar .x-a-title {
+    font-size: 14px;
+    color: blue;
+    text-decoration: none;
+    font-weight: bold;
+}
+
+html.fh-jf  .x-toolbar .x-b-title {
+    font-size: 14px;
+    font-weight: bold;
+}
+
+html.fh-jf  .x-toolbar.t-collapse {
+    position: fixed;
+    left: 100%;
+    margin-left: -24px;
+    width: 10000px;
+    top: -10px;
+}
+
+html.fh-jf .x-toolbar.t-collapse .fe-feedback {
+    float: left;
+    margin-left: -10px;
+    margin-right: 20px;
+}
+html.fh-jf .x-toolbar.t-collapse .fe-feedback .x-settings{
+    display: none;
+}
+
+html.fh-jf .mod-json .format-item button {
+    width: 80px;
+    height: 30px;
+    float: right;
+}
+
+html.fh-jf .mod-contentscript {
+    width: auto;
+}
+
+#optionBar {
+    display: inline-block !important;
+    position: static !important;
+}
+
+html.fh-jf body>#optionBar ,
+html.fh-jf body>#jfContent {
+    display: none !important;
+}
+
+#formatTips {
+    color: #888;
+    font-size: 14px;
+    display: block;
+    position: absolute;
+    top: 0px;
+    left: 0px;
+}
+
+#jsonSource {
+    height: 120px;
+}
+
+html.fh-jf .mod-json .callback-name {
+    font-weight: bolder;
+    color: #a00;
+}
+
+#errorMsg {
+    margin-top: 10px;
+    float: left;
+    color: #f00;
+}
+
+#statusBar {
+    position: fixed;
+    bottom: 0;
+    left: 0;
+    right: 0;
+    background: #f5f5f5;
+    border-top:1px solid #f4f4f4;
+    font-size: 12px;
+    font-weight: bold;
+    padding: 2px 10px 2px 2px;
+    z-index: 10
+}
+html.fh-jf .hide-status-bar #statusBar {
+    width:0;
+    height:0;
+    opacity: 0;
+}
+#jsonPath {
+    cursor: default;
+}
+
+html.fh-jf .boxOpt {
+    position: absolute;
+    right: 0;
+    top:-1px;
+    background: -webkit-linear-gradient(#fafafa,#f4f4f4 40%,#e5e5e5);
+    color: #888;
+    border:1px solid #ddd;
+    user-select: none;
+    font-weight: normal;
+    z-index: 1000;
+}
+
+html.fh-jf .boxOpt a {
+    cursor: pointer;
+    margin: 0 5px;
+    font-size: 12px;
+    color: #666;
+    text-decoration: none;
+}
+html.fh-jf .boxOpt a:hover {
+    color:#000;
+    text-decoration: none;
+}
+
+html.fh-jf .fe-feedback {
+    font-size: 12px;
+    padding-top: 3px;
+    color: #888;
+    float: right;
+    cursor: pointer;
+    font-weight: bold;
+}
+
+.fe-feedback a {
+    color: #888;
+    text-decoration: none;
+    text-align: left;
+    margin-right: 5px;
+}
+
+.fe-feedback a:hover {
+    color: #c00;
+}
+
+html.fh-jf svg:not(:root) {
+    overflow: hidden;
+}
+
+.fe-feedback svg {
+    vertical-align: text-bottom;
+    display: inline-block;
+    fill: currentColor;
+}
+
+.fe-feedback img {
+    opacity: 0.6;
+    position: relative;
+    top: 1px;
+}
+
+.fe-feedback a:focus {
+    outline: none;
+}
+
+.fe-feedback a:hover img {
+    opacity: 1.0;
+}
+
+.fe-feedback .x-settings {
+    cursor: pointer;
+    margin-right: 10px;
+}
+
+.fe-feedback .x-settings:hover {
+    color: #c00;
+}
+#fehelper_alertmsg {
+    font-family: "monospace", "微软雅黑";
+}
+
+#aLinkDownload {
+    position: absolute;
+    top:-10000px;
+    left:-10000px;
+}
+
+/*设置面板*/
+.mod-setting-panel {
+    position: absolute;
+    top: 32px;
+    right: 0;
+    z-index:10000;
+    width:300px;
+    background:#f1f1f1;
+    padding:0 20px 20px;
+    border:1px solid #ddd;
+    border-radius: 4px;
+    font-size:12px;
+    color:#454545;
+    box-shadow: 2px 2px #f9f9f9;
+}
+.mod-setting-panel h4 {
+    border-bottom: 1px solid #ccc;
+    padding-bottom: 4px;
+    margin:20px 0 10px;
+}
+.mod-setting-panel input[name="maxlength"] {
+    width:50px;
+    background: #fff;
+    border: 1px solid #eee;
+}
+.mod-setting-panel ul {
+    margin:0;
+    padding:0 10px;
+}
+.mod-setting-panel li {
+    padding:2px 0;
+    list-style: none;
+}
+.mod-setting-panel li:hover {
+    color:#600;
+}
+.mod-setting-panel ol {
+    padding:0 0 0 10px;
+}
+.mod-setting-panel .btns {
+    margin-top:20px;
+}
+.mod-setting-panel input[name="reset"] {
+    margin-left:20px;
+}
+.mod-setting-panel form {
+    margin:0;
+    padding:0;
+}
+.t-collapse .mod-setting-panel {
+    position: fixed;
+    right: 8px;
+}
+
+/*================ 皮肤:theme-default ===================*/
+html.fh-jf body.theme-default {
+    background-color: #fff;
+}
+.theme-default .item .key {
+    color: #000;
+}
+
+.theme-default .item .kv-list {
+    border-left: 1px dashed #bbb;
+}
+.theme-default .item .string {
+    color: #0B7500;
+}
+
+.theme-default .item .string a {
+    color: #00b;
+    text-decoration: underline;
+}
+
+.theme-default .item .string a:hover {
+    color: #b00;
+}
+
+.theme-default .item .bool,
+.theme-default .item .null,
+.theme-default .item .number {
+    font-weight: bold;
+    color: #ff6b00
+}
+.theme-default .item .number {
+    color:#1A01CC;
+}
+
+.theme-default .item .item-object:hover > span,
+.theme-default .item .item-array:hover > span,
+.theme-default .item .item-block:hover > span{
+    font-weight: bold;
+    color: #f00;
+}
+
+.theme-default .item .item-line:hover,
+.theme-default .item-line.x-selected{
+    background: #f9f9f9;
+}
+
+.theme-default .item .item-line:hover span ,
+.theme-default .item.x-selected>span {
+    font-weight: bolder;
+    background: #ff5;
+}
+
+
+/*================ 皮肤:theme-simple ===================*/
+.theme-simple .rootItem {
+    white-space: pre !important;
+    color: #000;
+}
+
+
+/*================ 皮肤:theme-light ===================*/
+.theme-light .item .key {
+    color: #4d4d4c;
+}
+
+.theme-light .item .kv-list {
+    border-left: 1px dashed #f1f1f1;
+}
+.theme-light .item .string {
+    color: #718c00;
+}
+
+.theme-light .item .string a {
+    color: #00b;
+    text-decoration: underline;
+}
+
+.theme-light .item .string a:hover {
+    color: #b00;
+}
+.theme-light .item .brace {
+    font-weight: normal;
+}
+.theme-light .item .bool,
+.theme-light .item .null,
+.theme-light .item .number {
+    font-weight: bold;
+    color: #f5871f
+}
+.theme-light .item .number {
+    color:#ff7c00;
+}
+
+.theme-light .item .item-object:hover > span,
+.theme-light .item .item-array:hover > span,
+.theme-light .item .item-block:hover > span{
+
+}
+
+.theme-light .item .item-line:hover,
+.theme-light .item-line.x-selected {
+    background: #f7f3f3;
+}
+
+.theme-light .item .item-line:hover span ,
+.theme-light .item.x-selected>span {
+    font-weight: bolder;
+    background: #f7f3f3;
+}
+.theme-light .item .quote {
+    color:#ccc;
+}
+
+
+/*================ 皮肤:theme-dark ===================*/
+.theme-dark {
+    background:#000;
+}
+.theme-dark .x-toolbar {
+    background: -webkit-linear-gradient(#222,#222,#111);
+    border: 1px solid #333;
+    border-bottom-color: #363636;
+    color:#ddd;
+}
+.theme-dark .x-toolbar .x-a-title {
+    color:#48b;
+}
+.theme-dark .xjf-btn {
+    background:-webkit-linear-gradient(#222,#222,#111);
+    border:1px solid #444;
+    color:#ddd;
+}
+.theme-dark .xjf-btn:hover {
+    background:-webkit-linear-gradient(#333,#333,#222);
+}
+.theme-dark #statusBar {
+    background: #333;
+    color: #ddd;
+    border-top: 1px solid #555;
+}
+.theme-dark .boxOpt {
+    background: -webkit-linear-gradient(#222,#222,#111);
+    border: 1px solid #333;
+}
+.theme-dark .boxOpt a:hover {
+    color:#eee;
+}
+.theme-dark .mod-setting-panel {
+    background: #222;
+    color: #fff;
+    border-color: #444;
+    box-shadow: 1px 1px #111;
+}
+.theme-dark .mod-setting-panel li:hover {
+    color:#ff0;
+}
+.theme-dark .mod-setting-panel input[name="maxlength"] {
+    background: #000;
+    border: 1px solid #555;
+    color: #fff;
+}
+
+.theme-dark .item .key {
+    color: #fff;
+}
+
+.theme-dark .item .kv-list {
+    border-left: 1px dashed #222;
+}
+.theme-dark .item .string {
+    color: #0f0;
+}
+
+.theme-dark .item .string a {
+    color: #369ad6;
+    text-decoration: underline;
+}
+
+.theme-dark .item .string a:hover {
+    color: #b00;
+}
+.theme-dark .item .brace,
+.theme-dark .item .colon,
+.theme-dark .item .comma{
+    color:#eaeaea;
+}
+
+.theme-dark .item .bool,
+.theme-dark .item .null,
+.theme-dark .item .number {
+    font-weight: bold;
+    color: #f00
+}
+.theme-dark .item .item-object:hover > span,
+.theme-dark .item .item-array:hover > span,
+.theme-dark .item .item-block:hover > span{
+    font-weight: bold;
+    color: orange;
+}
+
+.theme-dark .item .item-line:hover,
+.theme-dark .item-line.x-selected {
+    background: #111;
+}
+
+.theme-dark .item .item-line:hover span ,
+.theme-dark .item.x-selected>span {
+    font-weight: bolder;
+    background: #444;
+}
+.theme-dark .item .quote {
+    color:#eaeaea;
+}
+.theme-dark .item .expand {
+    color:#fff;
+}
+
+
+/*================ 皮肤:theme-vscode ===================*/
+.theme-vscode .item .key {
+    color: #f00;
+}
+.theme-vscode .item .quote {
+    color:#eaa;
+}
+.theme-vscode .item .kv-list {
+    border-left: 1px dashed #f1f1f1;
+}
+.theme-vscode .item .string {
+    color: #a31515;
+}
+.theme-vscode .item .string a {
+    color: #00b;
+    text-decoration: underline;
+}
+.theme-vscode .item .string a:hover {
+    color: #b00;
+}
+.theme-vscode .item .brace {
+    font-weight: normal;
+}
+.theme-vscode .item .bool,
+.theme-vscode .item .null,
+.theme-vscode .item .number {
+    font-weight: bold;
+    color: #a31515
+}
+.theme-vscode .item .number {
+    color: blue;
+}
+.theme-vscode .item .item-line:hover,
+.theme-vscode .item-line.x-selected {
+    background: #f7f3f3;
+}
+.theme-vscode .item .item-line:hover span ,
+.theme-vscode .item.x-selected>span {
+    font-weight: bolder;
+    background: #f7f3f3;
+}
+
+
+/*================ 皮肤:theme-github ===================*/
+.theme-github {
+    background: #f8f8f8;
+}
+.theme-github .item .key {
+    color: #333;
+}
+.theme-github .item .quote {
+    color:#aaa;
+}
+.theme-github .item .kv-list {
+    border-left: 1px dashed #e8e8e8;
+}
+.theme-github .item .string {
+    color: #d14;
+}
+.theme-github .item .string a {
+    color: #00b;
+    text-decoration: underline;
+}
+.theme-github .item .string a:hover {
+    color: #b00;
+}
+.theme-github .item .brace {
+    font-weight: normal;
+}
+.theme-github .item .bool,
+.theme-github .item .null,
+.theme-github .item .number {
+    font-weight: bold;
+    color: #008080
+}
+.theme-github .item .number {
+    color: blue;
+}
+.theme-github .item .item-line:hover,
+.theme-github .item-line.x-selected {
+    background: #f7f3f3;
+}
+.theme-github .item .item-line:hover span ,
+.theme-github .item.x-selected>span {
+    font-weight: bolder;
+    background: #f7f3f3;
+}
+
+
+/*================ 皮肤:theme-vegetarian ===================*/
+.theme-vegetarian .item-line.x-selected {
+    background: #f9f9f9;
+}

+ 679 - 0
apps/json-format/content-script.js

@@ -0,0 +1,679 @@
+/**
+ * Json Page Automatic Format Via FeHelper
+ * @author zhaoxianlie
+ */
+
+window.JsonAutoFormat = (() => {
+
+    // 留100ms时间给静态文件加载,当然,这个代码只是留给未开发过程中用的
+    let pleaseLetJsLoaded = 0;
+    let __importScript = (filename) => {
+        pleaseLetJsLoaded = 100;
+        let url = filename;
+
+        if (location.protocol === 'chrome-extension:' || chrome.runtime && chrome.runtime.getURL) {
+            url = chrome.runtime.getURL('json-format/' + filename);
+        }
+        fetch(url).then(resp => resp.text()).then(jsText => {
+            if(window.evalCore && window.evalCore.getEvalInstance){
+                return window.evalCore.getEvalInstance(window)(jsText);
+            }
+            let el = document.createElement('script');
+            el.textContent = jsText;
+            document.head.appendChild(el);
+        });
+    };
+
+    __importScript('json-bigint.js');
+    __importScript('format-lib.js');
+    __importScript('json-abc.js');
+    __importScript('json-decode.js');
+
+    const JSON_SORT_TYPE_KEY = 'json_sort_type_key';
+
+    // 本地永久存储的key
+    const STORAGE_KEYS = {
+        // 总是开启JSON自动格式化功能
+        JSON_PAGE_FORMAT: 'JSON_PAGE_FORMAT',
+        // 总是显示顶部工具栏
+        JSON_TOOL_BAR_ALWAYS_SHOW: 'JSON_TOOL_BAR_ALWAYS_SHOW',
+        // 启用底部状态栏
+        STATUS_BAR_ALWAYS_SHOW: 'STATUS_BAR_ALWAYS_SHOW',
+        // 自动进行URL、Unicode解码
+        AUTO_TEXT_DECODE: 'AUTO_TEXT_DECODE',
+        // 修正乱码
+        FIX_ERROR_ENCODING: 'FIX_ERROR_ENCODING',
+        // 启用JSON key排序功能
+        ENABLE_JSON_KEY_SORT: 'ENABLE_JSON_KEY_SORT',
+        // 保留键值双引号
+        KEEP_KEY_VALUE_DBL_QUOTE: 'KEEP_KEY_VALUE_DBL_QUOTE',
+        // 最大json key数量
+        MAX_JSON_KEYS_NUMBER: 'MAX_JSON_KEYS_NUMBER',
+        // 自定义皮肤
+        JSON_FORMAT_THEME: 'JSON_FORMAT_THEME'
+    };
+
+    // 皮肤定义
+    const SKIN_THEME = {
+        '0': 'theme-default',
+        '1': 'theme-simple',
+        '2': 'theme-light',
+        '3': 'theme-dark',
+        '4': 'theme-vscode',
+        '5': 'theme-github',
+        '6': 'theme-vegetarian'
+    };
+
+    let cssInjected = false;
+
+    // JSONP形式下的callback name
+    let funcName = null;
+    let fnTry = null;
+    let fnCatch = null;
+
+    // 格式化的配置
+    let formatOptions = {
+        JSON_FORMAT_THEME: 0,
+        sortType: 0,
+        autoDecode: false,
+        originalSource: ''
+    };
+
+    // 获取JSON格式化的配置信息
+    let _getAllOptions = (success) => {
+        chrome.runtime.sendMessage({
+            type: 'fh-dynamic-any-thing',
+            thing:'request-jsonformat-options',
+            params: STORAGE_KEYS
+        }, result => success(result));
+    };
+
+    let _getHtmlFragment = () => {
+        return [
+            '<div id="jfToolbar" class="x-toolbar" style="display:none">' +
+            '    <a href="https://www.baidufe.com/fehelper/index.html" target="_blank" class="x-a-title">' +
+            '        <img src="' + chrome.runtime.getURL('static/img/fe-16.png') + '" alt="fehelper"/> FeHelper</a>' +
+            '    <span class="x-b-title"></span>' +
+            '    <span class="x-sort">' +
+            '        <span class="x-split">|</span>' +
+            '        <span class="x-stitle">排序:</span>' +
+            '        <label for="sort_null">默认</label><input type="radio" name="jsonsort" id="sort_null" value="0" checked>' +
+            '        <label for="sort_asc">升序</label><input type="radio" name="jsonsort" id="sort_asc" value="1">' +
+            '        <label for="sort_desc">降序</label><input type="radio" name="jsonsort" id="sort_desc" value="-1">' +
+            '    </span>' +
+            '    <span class="x-fix-encoding"><span class="x-split">|</span><button class="xjf-btn" id="jsonGetCorrectCnt">乱码修正</button></span>' +
+            '    <span id="optionBar"></span>' +
+            '    <span class="fe-feedback">' +
+            '       <span class="x-settings"><svg aria-hidden="true" height="16" version="1.1" viewBox="0 0 14 16" width="14">' +
+            '           <path fill-rule="evenodd" d="M14 8.77v-1.6l-1.94-.64-.45-1.09.88-1.84-1.13-1.13-1.81.91-1.09-.45-.69-1.92h-1.6l-.63 1.94-1.11.45-1.84-.88-1.13 1.13.91 1.81-.45 1.09L0 7.23v1.59l1.94.64.45 1.09-.88 1.84 1.13 1.13 1.81-.91 1.09.45.69 1.92h1.59l.63-1.94 1.11-.45 1.84.88 1.13-1.13-.92-1.81.47-1.09L14 8.75v.02zM7 11c-1.66 0-3-1.34-3-3s1.34-3 3-3 3 1.34 3 3-1.34 3-3 3z"></path>' +
+            '       </svg>高级定制</span>' +
+            '       <a id="toggleBtn" title="展开或收起工具栏">隐藏&gt;&gt;</a>' +
+            '    </span>' +
+            '</div>',
+            '<div id="formattingMsg"><span class="x-loading"></span>格式化中...</div>',
+            '<div class="mod-json mod-contentscript"><div class="rst-item">',
+            '<div id="jfCallbackName_start" class="callback-name"></div>',
+            '<div id="jfContent"></div>',
+            '<pre id="jfContent_pre"></pre>',
+            '<div id="jfCallbackName_end" class="callback-name"></div>',
+            '</div></div>'
+        ].join('')
+    };
+
+    let _createSettingPanel = () => {
+        let html = `<div id="jfSettingPanel" class="mod-setting-panel">
+            <h4>基本配置项</h4>
+            <form action="#">
+                <ul>
+                    <li><label><input type="checkbox" name="alwaysOn" value="1">总是开启JSON自动格式化功能</label></li>
+                    <li><label><input type="checkbox" name="alwaysShowToolbar" value="1">总是显示顶部工具栏</label></li>
+                    <li><label><input type="checkbox" name="alwaysShowStatusbar" value="1">启用状态栏(包含复制/下载/删除)</label></li>
+                    <li><label><input type="checkbox" name="autoDecode" value="1">自动进行URL、Unicode解码</label></li>
+                    <li><label><input type="checkbox" name="errorEncoding" value="1">乱码修正(需手动操作,一键修正)</label></li>
+                    <li><label><input type="checkbox" name="enableSort" value="1">启用JSON键名排序功能</label></li>
+                    <li><label><input type="checkbox" name="keepQuote" value="1">格式化后保留键值对的双引号</label></li>
+                    <li><label><input type="text" name="maxlength" value="10000">最大支持的JSON Key数量</label></li>
+               </ul>
+
+               <h4>自定义皮肤</h4>
+               <ul>
+                    <li><label><input type="radio" name="skinId" value="0">默认模式(简约风格)</label></li>
+                    <li><label><input type="radio" name="skinId" value="1">极简模式(纯源码)</label></li>
+                    <li><label><input type="radio" name="skinId" value="2">清爽模式(明亮、跳跃)</label></li>
+                    <li><label><input type="radio" name="skinId" value="3">暗黑模式(安静、忧郁)</label></li>
+                    <li><label><input type="radio" name="skinId" value="4">vscode模式(醒目、专注)</label></li>
+                    <li><label><input type="radio" name="skinId" value="5">github模式(纵享丝滑)</label></li>
+                    <li><label><input type="radio" name="skinId" value="6">素人模式(清心寡欲)</label></li>
+               </ul>
+
+               <div class="btns">
+                    <input type="submit" class="xjf-btn" name="submit" value="确定">
+                    <input type="reset" class="xjf-btn" name="reset" value="取消">
+               </div>
+            </form>
+        </div>`;
+
+        let sPanel = $('#jfSettingPanel');
+        if (!sPanel.length) {
+            sPanel = $(html).appendTo('#jfToolbar');
+            // 表单提交时,保存数据
+            sPanel.find('input[type="submit"]').on('click', function (e) {
+                e.preventDefault();
+                e.stopPropagation();
+
+                let formData = {};
+                formData.JSON_PAGE_FORMAT = sPanel.find('input[name="alwaysOn"]').prop('checked');
+                formData.JSON_TOOL_BAR_ALWAYS_SHOW = sPanel.find('input[name="alwaysShowToolbar"]').prop('checked');
+                formData.STATUS_BAR_ALWAYS_SHOW = sPanel.find('input[name="alwaysShowStatusbar"]').prop('checked');
+                formData.AUTO_TEXT_DECODE = sPanel.find('input[name="autoDecode"]').prop('checked');
+                formData.FIX_ERROR_ENCODING = sPanel.find('input[name="errorEncoding"]').prop('checked');
+                formData.ENABLE_JSON_KEY_SORT = sPanel.find('input[name="enableSort"]').prop('checked');
+                formData.KEEP_KEY_VALUE_DBL_QUOTE = sPanel.find('input[name="keepQuote"]').prop('checked');
+                formData.MAX_JSON_KEYS_NUMBER = sPanel.find('input[name="maxlength"]').val();
+                formData.JSON_FORMAT_THEME = sPanel.find('input[name="skinId"]:checked').val();
+
+                chrome.runtime.sendMessage({
+                    type: 'fh-dynamic-any-thing',
+                    thing: 'save-jsonformat-options',
+                    params: formData
+                }, result => sPanel.hide());
+            });
+
+            sPanel.find('input[name="alwaysShowToolbar"]').on('click', function (e) {
+                $('.fe-feedback #toggleBtn').trigger('click');
+            });
+
+            sPanel.find('input[name="errorEncoding"]').on('click', function (e) {
+                let el = $('#jfToolbar').find('.x-fix-encoding');
+                $(this).prop('checked') ? el.show() : el.hide();
+            });
+
+            sPanel.find('input[name="enableSort"]').on('click', function (e) {
+                let el = $('#jfToolbar').find('.x-sort');
+                $(this).prop('checked') ? el.show() : el.hide();
+            });
+
+            sPanel.find('input[type="reset"]').on('click', (e) => sPanel.hide());
+
+            sPanel.find('input[name="skinId"]').on('click', function (e) {
+                formatOptions.JSON_FORMAT_THEME = this.value;
+                _didFormat();
+            });
+
+            sPanel.find('input[name="alwaysShowStatusbar"]').on('click', function (e) {
+                formatOptions.STATUS_BAR_ALWAYS_SHOW = $(this).prop('checked');
+                let elBody = $('body');
+                if (formatOptions.STATUS_BAR_ALWAYS_SHOW) {
+                    elBody.removeClass('hide-status-bar');
+                } else {
+                    elBody.addClass('hide-status-bar');
+                }
+            });
+
+            sPanel.find('input[name="keepQuote"]').on('click', function (e) {
+                formatOptions.KEEP_KEY_VALUE_DBL_QUOTE = $(this).prop('checked');
+                let elBody = $('body');
+                if (formatOptions.KEEP_KEY_VALUE_DBL_QUOTE) {
+                    elBody.removeClass('remove-quote');
+                } else {
+                    elBody.addClass('remove-quote');
+                }
+            });
+        } else if (sPanel[0].offsetHeight) {
+            return sPanel.hide();
+        } else {
+            sPanel.show();
+        }
+
+        _getAllOptions(result => {
+            result.JSON_PAGE_FORMAT && sPanel.find('input[name="alwaysOn"]').prop('checked', true);
+            result.JSON_TOOL_BAR_ALWAYS_SHOW && sPanel.find('input[name="alwaysShowToolbar"]').prop('checked', true);
+            result.STATUS_BAR_ALWAYS_SHOW && sPanel.find('input[name="alwaysShowStatusbar"]').prop('checked', true);
+            result.AUTO_TEXT_DECODE && sPanel.find('input[name="autoDecode"]').prop('checked', true);
+            result.FIX_ERROR_ENCODING && sPanel.find('input[name="errorEncoding"]').prop('checked', true);
+            result.ENABLE_JSON_KEY_SORT && sPanel.find('input[name="enableSort"]').prop('checked', true);
+            result.KEEP_KEY_VALUE_DBL_QUOTE && sPanel.find('input[name="keepQuote"]').prop('checked', true);
+            sPanel.find('input[name="maxlength"]').attr('value', result.MAX_JSON_KEYS_NUMBER || 10000);
+            sPanel.find(`input[name="skinId"][value="${result.JSON_FORMAT_THEME || 0}"]`).attr('checked', true);
+        });
+    };
+
+
+    // 检测当前页面的CSP,防止出现这种情况:
+    // DOMException: Failed to read the 'localStorage' property from 'Window': The document is sandboxed and lacks the 'allow-same-origin' flag.
+    let _checkContentSecurityPolicy = () => {
+        try {
+            localStorage.getItem(1);
+        } catch (e) {
+            return false;
+        }
+        return true;
+    };
+
+    let _initToolbar = () => {
+
+        let cspSafe = _checkContentSecurityPolicy();
+        if (cspSafe) {
+            // =============================排序:获取上次记录的排序方式
+            if (formatOptions.ENABLE_JSON_KEY_SORT) {
+                formatOptions.sortType = parseInt(localStorage.getItem(JSON_SORT_TYPE_KEY) || 0);
+                // 排序选项初始化
+                $('[name=jsonsort][value=' + formatOptions.sortType + ']').attr('checked', 1);
+            } else {
+                formatOptions.sortType = 0;
+                $('#jfToolbar .x-sort').hide();
+            }
+
+            // =============================事件初始化
+            $('[name=jsonsort]').click(function (e) {
+                let sortType = parseInt(this.value);
+                if (sortType !== formatOptions.sortType) {
+                    formatOptions.sortType = sortType;
+                    _didFormat();
+                }
+                localStorage.setItem(JSON_SORT_TYPE_KEY, sortType);
+            });
+        } else {
+            $('#jfToolbar .x-sort').hide();
+        }
+
+
+        // =============================乱码修正
+        if (!formatOptions.FIX_ERROR_ENCODING) {
+            $('#jfToolbar .x-fix-encoding').hide();
+        }
+
+        // =============================工具栏的显示与隐藏控制
+        let toolBarClassList = document.querySelector('#jfToolbar').classList;
+        let tgBtn = $('.fe-feedback #toggleBtn');
+        if (formatOptions.JSON_TOOL_BAR_ALWAYS_SHOW) {
+            toolBarClassList.remove('t-collapse');
+            tgBtn.html('隐藏&gt;&gt;');
+        } else {
+            toolBarClassList.add('t-collapse');
+            tgBtn.html('&lt;&lt;');
+        }
+        tgBtn.click(function (e) {
+            e.preventDefault();
+            e.stopPropagation();
+
+            chrome.runtime.sendMessage({
+                type: 'fh-dynamic-any-thing',
+                thing: 'toggle-jsonformat-options'
+            }, show => {
+                let toolBarClassList = document.querySelector('#jfToolbar').classList;
+                if (show) {
+                    toolBarClassList.remove('t-collapse');
+                    tgBtn.html('隐藏&gt;&gt;');
+                } else {
+                    toolBarClassList.add('t-collapse');
+                    tgBtn.html('&lt;&lt;');
+                }
+                $('#jfToolbar input[name="alwaysShowToolbar"]').prop('checked', show);
+            });
+        });
+
+        $('.fe-feedback .x-settings').click(e => _createSettingPanel());
+        $('#jsonGetCorrectCnt').click(e => _getCorrectContent());
+    };
+
+    let _didFormat = function () {
+        let source = formatOptions.originalSource;
+
+        if (formatOptions.sortType !== 0) {
+            let jsonObj = JsonABC.sortObj(JSON.parse(formatOptions.originalSource), parseInt(formatOptions.sortType), true);
+            source = JSON.stringify(jsonObj);
+        }
+
+        let elBody = $('body');
+
+        let theme = SKIN_THEME[formatOptions.JSON_FORMAT_THEME || 0];
+        Object.values(SKIN_THEME).forEach(th => elBody.removeClass(th));
+        elBody.addClass(theme);
+
+        // 控制引号
+        if (formatOptions.KEEP_KEY_VALUE_DBL_QUOTE) {
+            elBody.removeClass('remove-quote');
+        } else {
+            elBody.addClass('remove-quote');
+        }
+
+        // 控制底部状态栏
+        if (formatOptions.STATUS_BAR_ALWAYS_SHOW) {
+            elBody.removeClass('hide-status-bar');
+        } else {
+            elBody.addClass('hide-status-bar');
+        }
+
+        if (formatOptions.autoDecode) {
+            (async () => {
+                let txt = await JsonEnDecode.urlDecodeByFetch(source);
+                source = JsonEnDecode.uniDecode(txt);
+
+                // 格式化
+                try {
+                    Formatter.format(source, theme);
+                } catch (e) {
+                    Formatter.formatSync(source, theme)
+                }
+                $('#jfToolbar').fadeIn(500);
+            })();
+        } else {
+            // 格式化
+            try {
+                Formatter.format(source, theme);
+            } catch (e) {
+                Formatter.formatSync(source, theme)
+            }
+
+            $('#jfToolbar').fadeIn(500);
+        }
+
+
+        // 如果是JSONP格式的,需要把方法名也显示出来
+        if (funcName != null) {
+            if (fnTry && fnCatch) {
+                $('#jfCallbackName_start').html('<pre style="padding:0">' + fnTry + '</pre>' + funcName + '(');
+                $('#jfCallbackName_end').html(')<br><pre style="padding:0">' + fnCatch + '</pre>');
+            } else {
+                $('#jfCallbackName_start').html(funcName + '(');
+                $('#jfCallbackName_end').html(')');
+            }
+        }
+    };
+
+    let _getCorrectContent = function () {
+        fetch(location.href).then(res => res.text()).then(text => {
+            formatOptions.originalSource = text;
+            _didFormat();
+        });
+    };
+
+    /**
+     * 从一个dom节点去获取json内容,这里面有很多的判断
+     */
+    let _getJsonContentFromDOM = function (dom) {
+        let source = dom.textContent.trim();
+
+        if (!source) {
+            source = (document.body.textContent || '').trim()
+        }
+
+        if (!source) {
+            return false;
+        }
+
+        // 1、如果body的内容还包含HTML标签,肯定不是合法的json了
+        // 2、如果是合法的json,也只可能有一个text节点
+        // 3、但是要兼容一下其他插件对页面的破坏情况
+        // 4、对于content-type是application/json的页面可以做宽松处理
+        let nodes = document.body.childNodes;
+        let jsonText = '';
+        let isJsonContentType = document.contentType === 'application/json';
+        for (let i = 0, len = nodes.length; i < len; i++) {
+            let elm = nodes[i];
+            if (elm.nodeType === Node.TEXT_NODE) {
+                jsonText += (elm.textContent || '').trim();
+            } else if (isJsonContentType) {
+                if ((elm.offsetHeight + elm.offsetWidth !== 0) && elm.textContent.length > jsonText.length) {
+                    jsonText = elm.textContent;
+                }
+            } else {
+                if (nodes[i].nodeType === Node.ELEMENT_NODE) {
+                    let tagName = elm.tagName.toLowerCase();
+                    let text = (elm.textContent || '').trim();
+
+                    // 如果包含了script和link标签,需要看标签的src和href属性值,如果不是chrome-extensions注入的,也要跳出
+                    if (['script', 'link'].includes(tagName)) {
+                        let url = elm.getAttribute('src') || elm.getAttribute('href');
+                        if (!!url && !/^chrome\-extension:\/\//.test(url)) {
+                            return false;
+                        }
+                    }
+
+                    // 如果不是pre标签,并且还不是隐藏节点,且内容不为空,也要跳出
+                    else if (tagName !== 'pre' && (elm.offsetWidth + elm.offsetHeight !== 0 && !!text)) {
+                        return false;
+                    }
+
+                    // 如果是pre标签,但当前节点内容与最初body.textContent提取值不一致,都跳出
+                    else if (tagName === 'pre' && text !== source) {
+                        return false;
+                    }
+                } else {
+                    return false;
+                }
+            }
+        }
+
+        return (jsonText || '').trim() || source;
+    };
+
+    /**
+     * 从页面提取JSON文本
+     * @returns {string}
+     * @private
+     */
+    let _getJsonText = function () {
+        // 如果是js内容,则不进行json格式化
+        let isJs = /\.js$/.test(new URL(location.href).pathname);
+        isJs = isJs && document.contentType === 'application/javascript';
+        if (isJs) {
+            return false;
+        }
+
+        // 如果是 HTML 页面,也要看一下内容是不是明显就是个JSON,如果不是,则也不进行 json 格式化
+        if (document.contentType === 'text/html') {
+            // 使用 DOMParser 解析 HTML
+            const parser = new DOMParser();
+            const doc = parser.parseFromString(document.body.outerHTML, "text/html");
+            // 移除不需要的标签
+            doc.querySelectorAll('style, script').forEach(el => el.remove());
+            // 获取清理后的文本
+            const cleanText = doc.body.textContent;
+            let jsonObj = _getJsonObject(cleanText);
+            if(!jsonObj) {
+                return false;
+            }
+        }
+
+        let pre = document.querySelectorAll('body>pre')[0] || {textContent: ""};
+        
+        return _getJsonContentFromDOM(pre);
+    };
+
+    /**
+     * 获取一个JSON的所有Key数量
+     * @param json
+     * @returns {number}
+     * @private
+     */
+    let _getAllKeysCount = function (json) {
+        let count = 0;
+
+        if (typeof json === 'object') {
+            let keys = Object.keys(json);
+            count += keys.length;
+
+            keys.forEach(key => {
+                if (json[key] && typeof json[key] === 'object') {
+                    count += _getAllKeysCount(json[key]);
+                }
+            });
+        }
+
+        return count;
+    };
+
+    // 用新的options来覆盖默认options
+    let _extendsOptions = options => {
+        options = options || {};
+        Object.keys(options).forEach(opt => formatOptions[opt] = options[opt]);
+    };
+
+
+    /**
+     * 判断字符串参数是否为一个合法的json,如果是则返回json对象
+     * @param {*} source 
+     * @returns 
+     */
+    let _getJsonObject = function (source) {
+        let jsonObj = null;
+
+        // 下面校验给定字符串是否为一个合法的json
+        try {
+
+            // 再看看是不是jsonp的格式
+            let reg = /^([\w\.]+)\(\s*([\s\S]*)\s*\)$/gm;
+            let reTry = /^(try\s*\{\s*)?/g;
+            let reCatch = /([;\s]*\}\s*catch\s*\(\s*\S+\s*\)\s*\{([\s\S])*\})?[;\s]*$/g;
+
+            // 检测是否有try-catch包裹
+            let sourceReplaced = source.replace(reTry, function () {
+                fnTry = fnTry ? fnTry : arguments[1];
+                return '';
+            }).replace(reCatch, function () {
+                fnCatch = fnCatch ? fnCatch : arguments[1];
+                return '';
+            }).trim();
+
+            let matches = reg.exec(sourceReplaced);
+            if (matches != null && (fnTry && fnCatch || !fnTry && !fnCatch)) {
+                funcName = matches[1];
+                source = matches[2];
+            } else {
+                reg = /^([\{\[])/;
+                if (!reg.test(source)) {
+                    return;
+                }
+            }
+
+            // 这里可能会throw exception
+            jsonObj = JSON.parse(source);
+        } catch (ex) {
+
+            // new Function的方式,能自动给key补全双引号,但是不支持bigint,所以是下下策,放在try-catch里搞
+            try {
+                jsonObj = new Function("return " + source)();
+            } catch (exx) {
+                try {
+                    // 再给你一次机会,是不是下面这种情况:  "{\"ret\":\"0\", \"msg\":\"ok\"}"
+                    jsonObj = new Function("return '" + source + "'")();
+                    if (typeof jsonObj === 'string') {
+                        try {
+                            // 确保bigint不会失真
+                            jsonObj = JSON.parse(jsonObj);
+                        } catch (ie) {
+                            // 最后给你一次机会,是个字符串,老夫给你再转一次
+                            jsonObj = new Function("return " + jsonObj)();
+                        }
+                    }
+                } catch (exxx) {
+                    return;
+                }
+            }
+        }
+
+        try {
+            // 要尽量保证格式化的东西一定是一个json,所以需要把内容进行JSON.stringify处理
+            source = JSON.stringify(jsonObj);
+        } catch (ex) {
+            // 通过JSON反解不出来的,一定有问题
+            return;
+        }
+        return jsonObj;
+    };
+
+    /**
+     * 根据最终拿到的json source,对页面进行格式化操作
+     * @param {*} source 
+     * @returns 
+     */
+    let _formatTheSource = function (source) {
+        let jsonObj = _getJsonObject(source);
+
+        // 是json格式,可以进行JSON自动格式化
+        if (jsonObj != null && typeof jsonObj === "object") {
+
+            // 提前注入css
+            if(!cssInjected) {
+                chrome.runtime.sendMessage({
+                    type: 'fh-dynamic-any-thing',
+                    thing:'inject-content-css',
+                    tool: 'json-format'
+                });
+            }
+
+            // JSON的所有key不能超过预设的值,比如 10000 个,要不然自动格式化会比较卡
+            if (formatOptions['MAX_JSON_KEYS_NUMBER']) {
+                let keysCount = _getAllKeysCount(jsonObj);
+                if (keysCount > formatOptions['MAX_JSON_KEYS_NUMBER']) {
+                    let msg = '当前JSON共 <b style="color:red">' + keysCount + '</b> 个Key,大于预设值' + formatOptions['MAX_JSON_KEYS_NUMBER'] + ',已取消自动格式化;可到FeHelper设置页调整此配置!';
+                    return toast(msg);
+                }
+            }
+
+            $('html').addClass('fh-jf');
+            $('body').prepend(_getHtmlFragment());
+            let preLength = $('body>pre').remove().length;
+            if (!preLength) {
+                Array.prototype.slice.call(document.body.childNodes).forEach(node => {
+                    (node.nodeType === Node.TEXT_NODE) && node.remove();
+                });
+            }
+
+            formatOptions.originalSource = JSON.stringify(jsonObj);
+
+            _initToolbar();
+            _didFormat();
+        }
+    };
+
+    /**
+     * 执行format操作
+     * @private
+     */
+    let _format = function () {
+
+        let source = _getJsonText();
+        if (source) {
+            _formatTheSource(source);
+        }else{
+            // 原计划,是兜底走fetch的方式,再尝试做一次格式化,但是这里会有很多corner Case我没法回归,所以注释掉
+            // fetch(location.href)
+            // .then(response => response.text())
+            // .then(html => {
+            //     // 使用 DOMParser 解析 HTML
+            //     const parser = new DOMParser();
+            //     const doc = parser.parseFromString(html, "text/html");
+
+            //     // 移除不需要的标签
+            //     doc.querySelectorAll('style, script').forEach(el => el.remove());
+            //     const text = _getJsonContentFromDOM(doc.body);
+            //     if(text){
+            //         _formatTheSource(text);
+            //     }
+            // })
+            // .catch();
+        }
+    };
+
+    return {
+        format: () => _getAllOptions(options => {
+            if(options.JSON_PAGE_FORMAT) {
+                let intervalId = setTimeout(() => {
+                    if(typeof Formatter !== 'undefined') {
+                        clearInterval(intervalId);
+                        _extendsOptions(options);
+                        _format();
+                    }
+                },pleaseLetJsLoaded);
+            }
+        })
+    };
+})();
+
+
+if(location.protocol !== 'chrome-extension:') {
+    window.JsonAutoFormat.format();
+}

+ 883 - 0
apps/json-format/format-lib.js

@@ -0,0 +1,883 @@
+/**
+ * 日期格式化
+ * @param {Object} pattern
+ */
+Date.prototype.format = function (pattern) {
+    let pad = function (source, length) {
+        let pre = "",
+            negative = (source < 0),
+            string = String(Math.abs(source));
+
+        if (string.length < length) {
+            pre = (new Array(length - string.length + 1)).join('0');
+        }
+
+        return (negative ? "-" : "") + pre + string;
+    };
+
+    if ('string' !== typeof pattern) {
+        return this.toString();
+    }
+
+    let replacer = function (patternPart, result) {
+        pattern = pattern.replace(patternPart, result);
+    };
+
+    let year = this.getFullYear(),
+        month = this.getMonth() + 1,
+        date2 = this.getDate(),
+        hours = this.getHours(),
+        minutes = this.getMinutes(),
+        seconds = this.getSeconds(),
+        milliSec = this.getMilliseconds();
+
+    replacer(/yyyy/g, pad(year, 4));
+    replacer(/yy/g, pad(parseInt(year.toString().slice(2), 10), 2));
+    replacer(/MM/g, pad(month, 2));
+    replacer(/M/g, month);
+    replacer(/dd/g, pad(date2, 2));
+    replacer(/d/g, date2);
+
+    replacer(/HH/g, pad(hours, 2));
+    replacer(/H/g, hours);
+    replacer(/hh/g, pad(hours % 12, 2));
+    replacer(/h/g, hours % 12);
+    replacer(/mm/g, pad(minutes, 2));
+    replacer(/m/g, minutes);
+    replacer(/ss/g, pad(seconds, 2));
+    replacer(/s/g, seconds);
+    replacer(/SSS/g, pad(milliSec, 3));
+    replacer(/S/g, milliSec);
+
+    return pattern;
+};
+
+/**
+ * 自动消失的Alert弹窗
+ * @param content
+ */
+window.toast = function (content) {
+    window.clearTimeout(window.feHelperAlertMsgTid);
+    let elAlertMsg = document.querySelector("#fehelper_alertmsg");
+    if (!elAlertMsg) {
+        let elWrapper = document.createElement('div');
+        elWrapper.innerHTML = '<div id="fehelper_alertmsg" style="position:fixed;bottom:25px;left:5px;z-index:1000000">' +
+            '<p style="background:#000;display:inline-block;color:#fff;text-align:center;' +
+            'padding:10px 10px;margin:0 auto;font-size:14px;border-radius:4px;">' + content + '</p></div>';
+        elAlertMsg = elWrapper.childNodes[0];
+        document.body.appendChild(elAlertMsg);
+    } else {
+        elAlertMsg.querySelector('p').innerHTML = content;
+        elAlertMsg.style.display = 'block';
+    }
+
+    window.feHelperAlertMsgTid = window.setTimeout(function () {
+        elAlertMsg.style.display = 'none';
+    }, 1000);
+};
+
+
+/**
+ * FeHelper Json Format Lib,入口文件
+ * @example
+ *  Formatter.format(jsonString)
+ */
+window.Formatter = (function () {
+
+    "use strict";
+
+    let jfContent,
+        jfPre,
+        jfStyleEl,
+        jfStatusBar,
+        formattingMsg;
+
+    let lastItemIdGiven = 0;
+    let cachedJsonString = '';
+
+    let _initElements = function () {
+
+        jfContent = $('#jfContent');
+        if (!jfContent[0]) {
+            jfContent = $('<div id="jfContent" />').appendTo('body');
+        }
+
+        jfPre = $('#jfContent_pre');
+        if (!jfPre[0]) {
+            jfPre = $('<pre id="jfContent_pre" />').appendTo('body');
+        }
+
+        jfStyleEl = $('#jfStyleEl');
+        if (!jfStyleEl[0]) {
+            jfStyleEl = $('<style id="jfStyleEl" />').appendTo('head');
+        }
+
+        formattingMsg = $('#formattingMsg');
+        if (!formattingMsg[0]) {
+            formattingMsg = $('<div id="formattingMsg"><span class="x-loading"></span>格式化中...</div>').appendTo('body');
+        }
+
+        try {
+            jfContent.html('').show();
+            jfPre.html('').hide();
+            jfStatusBar && jfStatusBar.hide();
+            formattingMsg.hide();
+        } catch (e) {
+        }
+    };
+
+    /**
+     * HTML特殊字符格式化
+     * @param str
+     * @returns {*}
+     */
+    let htmlspecialchars = function (str) {
+        str = str.replace(/&/g, '&amp;');
+        str = str.replace(/</g, '&lt;');
+        str = str.replace(/>/g, '&gt;');
+        str = str.replace(/"/g, '&quot;');
+        str = str.replace(/'/g, '&#039;');
+        return str;
+    };
+
+    /**
+     * 直接下载,能解决中文乱码
+     * @param content
+     * @private
+     */
+    let _downloadSupport = function (content) {
+
+        // 下载链接
+        let dt = (new Date()).format('yyyyMMddHHmmss');
+        let blob = new Blob([content], {type: 'application/octet-stream'});
+
+        let button = $('<button class="xjf-btn xjf-btn-right">下载JSON</button>').appendTo('#optionBar');
+
+        if (typeof chrome === 'undefined' || !chrome.permissions) {
+            button.click(function (e) {
+                let aLink = $('#aLinkDownload');
+                if (!aLink[0]) {
+                    aLink = $('<a id="aLinkDownload" target="_blank" title="保存到本地">下载JSON数据</a>').appendTo('body');
+                    aLink.attr('download', 'FeHelper-' + dt + '.json');
+                    aLink.attr('href', URL.createObjectURL(blob));
+                }
+                aLink[0].click();
+            });
+        } else {
+            button.click(function (e) {
+                // 请求权限
+                chrome.permissions.request({
+                    permissions: ['downloads']
+                }, (granted) => {
+                    if (granted) {
+                        chrome.downloads.download({
+                            url: URL.createObjectURL(blob),
+                            saveAs: true,
+                            conflictAction: 'overwrite',
+                            filename: 'FeHelper-' + dt + '.json'
+                        });
+                    } else {
+                        toast('必须接受授权,才能正常下载!');
+                    }
+                });
+            });
+        }
+
+    };
+
+
+    /**
+     * chrome 下复制到剪贴板
+     * @param text
+     */
+    let _copyToClipboard = function (text) {
+        let input = document.createElement('textarea');
+        input.style.position = 'fixed';
+        input.style.opacity = 0;
+        input.value = text;
+        document.body.appendChild(input);
+        input.select();
+        document.execCommand('Copy');
+        document.body.removeChild(input);
+
+        toast('Json片段复制成功,随处粘贴可用!')
+    };
+
+
+    /**
+     * 从el中获取json文本
+     * @param el
+     * @returns {string}
+     */
+    let getJsonText = function (el) {
+
+        let txt = el.text().replace(/复制\|下载\|删除/gm,'').replace(/":\s/gm, '":').replace(/,$/, '').trim();
+        if (!(/^{/.test(txt) && /\}$/.test(txt)) && !(/^\[/.test(txt) && /\]$/.test(txt))) {
+            txt = '{' + txt + '}';
+        }
+        try {
+            txt = JSON.stringify(JSON.parse(txt), null, 4);
+        } catch (err) {
+        }
+
+        return txt;
+    };
+
+    // 添加json路径
+    let _showJsonPath = function (curEl) {
+        let keys = [];
+        do {
+            if (curEl.hasClass('item-block')) {
+                if (!curEl.hasClass('rootItem')) {
+                    keys.unshift('[' + curEl.prevAll('.item').length + ']');
+                } else {
+                    break;
+                }
+            } else {
+                keys.unshift(curEl.find('>.key').text());
+            }
+
+            if (curEl.parent().hasClass('rootItem') || curEl.parent().parent().hasClass('rootItem')) {
+                break;
+            }
+
+            curEl = curEl.parent().parent();
+
+        } while (curEl.length && !curEl.hasClass('rootItem'));
+
+        let path = keys.join('#@#').replace(/#@#\[/g, '[').replace(/#@#/g, '.');
+
+        let jfPath = $('#jsonPath');
+        if (!jfPath.length) {
+            jfPath = $('<span id="jsonPath"/>').prependTo(jfStatusBar);
+        }
+        jfPath.html('当前节点:JSON.' + path);
+    };
+
+    // 给某个节点增加操作项
+    let _addOptForItem = function (el, show) {
+
+        // 下载json片段
+        let fnDownload = function (event) {
+            event.stopPropagation();
+
+            let txt = getJsonText(el);
+            // 下载片段
+            let dt = (new Date()).format('yyyyMMddHHmmss');
+            let blob = new Blob([txt], {type: 'application/octet-stream'});
+
+            if (typeof chrome === 'undefined' || !chrome.permissions) {
+                // 下载JSON的简单形式
+                $(this).attr('download', 'FeHelper-' + dt + '.json').attr('href', URL.createObjectURL(blob));
+            } else {
+                // 请求权限
+                chrome.permissions.request({
+                    permissions: ['downloads']
+                }, (granted) => {
+                    if (granted) {
+                        chrome.downloads.download({
+                            url: URL.createObjectURL(blob),
+                            saveAs: true,
+                            conflictAction: 'overwrite',
+                            filename: 'FeHelper-' + dt + '.json'
+                        });
+                    } else {
+                        toast('必须接受授权,才能正常下载!');
+                    }
+                });
+            }
+
+        };
+
+        // 复制json片段
+        let fnCopy = function (event) {
+            event.stopPropagation();
+            _copyToClipboard(getJsonText(el));
+        };
+
+        // 删除json片段
+        let fnDel = function (event) {
+            event.stopPropagation();
+            if (el.parent().is('#formattedJson')) {
+                toast('如果连最外层的Json也删掉的话,就没啥意义了哦!');
+                return false;
+            }
+            toast('节点已删除成功!');
+            el.remove();
+            jfStatusBar && jfStatusBar.hide();
+        };
+
+        $('.boxOpt').hide();
+        if (show) {
+            let jfOptEl = el.children('.boxOpt');
+            if (!jfOptEl.length) {
+                jfOptEl = $('<b class="boxOpt">' +
+                    '<a class="opt-copy" title="复制当前选中节点的JSON数据">复制</a>|' +
+                    '<a class="opt-download" target="_blank" title="下载当前选中节点的JSON数据">下载</a>|' +
+                    '<a class="opt-del" title="删除当前选中节点的JSON数据">删除</a></b>').appendTo(el);
+            } else {
+                jfOptEl.show();
+            }
+
+            jfOptEl.find('a.opt-download').unbind('click').bind('click', fnDownload);
+            jfOptEl.find('a.opt-copy').unbind('click').bind('click', fnCopy);
+            jfOptEl.find('a.opt-del').unbind('click').bind('click', fnDel);
+        }
+
+    };
+
+    // 显示当前节点的Key
+    let _toogleStatusBar = function (curEl, show) {
+        if (!jfStatusBar) {
+            jfStatusBar = $('<div id="statusBar"/>').appendTo('body');
+        }
+
+        if (!show) {
+            jfStatusBar.hide();
+            return;
+        } else {
+            jfStatusBar.show();
+        }
+
+        _showJsonPath(curEl);
+    };
+
+
+    /**
+     * 折叠所有
+     * @param elements
+     */
+    function collapse(elements) {
+        let el;
+
+        $.each(elements, function (i) {
+            el = $(this);
+            if (el.children('.kv-list').length) {
+                el.addClass('collapsed');
+
+                if (!el.attr('id')) {
+                    el.attr('id', 'item' + (++lastItemIdGiven));
+
+                    let count = el.children('.kv-list').eq(0).children().length;
+                    // Generate comment text eg "4 items"
+                    let comment = count + (count === 1 ? ' item' : ' items');
+                    // Add CSS that targets it
+                    jfStyleEl[0].insertAdjacentHTML(
+                        'beforeend',
+                        '\n#item' + lastItemIdGiven + '.collapsed:after{color: #aaa; content:" // ' + comment + '"}'
+                    );
+                }
+
+            }
+        });
+    }
+
+    /**
+     * 创建几个全局操作的按钮,置于页面右上角即可
+     * @private
+     */
+    let _buildOptionBar = function () {
+
+        let optionBar = $('#optionBar');
+        if (optionBar.length) {
+            optionBar.html('');
+        } else {
+            optionBar = $('<span id="optionBar" />').appendTo(jfContent.parent());
+        }
+
+        $('<span class="x-split">|</span>').appendTo(optionBar);
+        let buttonFormatted = $('<button class="xjf-btn xjf-btn-left">元数据</button>').appendTo(optionBar);
+        let buttonCollapseAll = $('<button class="xjf-btn xjf-btn-mid">折叠所有</button>').appendTo(optionBar);
+        let plainOn = false;
+
+        buttonFormatted.bind('click', function (e) {
+            if (plainOn) {
+                plainOn = false;
+                jfPre.hide();
+                jfContent.show();
+                buttonFormatted.text('元数据');
+            } else {
+                plainOn = true;
+                jfPre.show();
+                jfContent.hide();
+                buttonFormatted.text('格式化');
+            }
+
+            jfStatusBar && jfStatusBar.hide();
+        });
+
+        buttonCollapseAll.bind('click', function (e) {
+            // 如果内容还没有格式化过,需要再格式化一下
+            if (plainOn) {
+                buttonFormatted.trigger('click');
+            }
+
+            if (buttonCollapseAll.text() === '折叠所有') {
+                buttonCollapseAll.text('展开所有');
+                collapse($('.item-object,.item-block'));
+            } else {
+                buttonCollapseAll.text('折叠所有');
+                $('.item-object,.item-block').removeClass('collapsed');
+            }
+            jfStatusBar && jfStatusBar.hide();
+        });
+
+    };
+
+    // 附加操作
+    let _addEvents = function () {
+
+        // 折叠、展开
+        $('#jfContent span.expand').bind('click', function (ev) {
+            ev.preventDefault();
+            ev.stopPropagation();
+
+            let parentEl = $(this).parent();
+            parentEl.toggleClass('collapsed');
+
+            if (parentEl.hasClass('collapsed')) {
+                collapse(parentEl);
+            }
+        });
+
+        // 点击选中:高亮
+        $('#jfContent .item').bind('click', function (e) {
+
+            let el = $(this);
+
+            if (el.hasClass('x-selected')) {
+                _toogleStatusBar(el, false);
+                _addOptForItem(el, false);
+                el.removeClass('x-selected');
+                e.stopPropagation();
+                return true;
+            }
+
+            $('.x-selected').removeClass('x-selected');
+            el.addClass('x-selected');
+
+            // 显示底部状态栏
+            _toogleStatusBar(el, true);
+            _addOptForItem(el, true);
+
+            if (!$(e.target).is('.item .expand')) {
+                e.stopPropagation();
+            } else {
+                $(e.target).parent().trigger('click');
+            }
+
+            // 触发钩子
+            if (typeof window._OnJsonItemClickByFH === 'function') {
+                window._OnJsonItemClickByFH(getJsonText(el));
+            }
+        });
+
+    };
+
+    /**
+     * 执行代码格式化
+     */
+    let format = function (jsonStr, skin) {
+        cachedJsonString = JSON.stringify(JSON.parse(jsonStr), null, 4);
+
+        _initElements();
+        jfPre.html(htmlspecialchars(cachedJsonString));
+
+        // 用webwork的方式来进行格式化,效率更高
+        let worker = new Worker(URL.createObjectURL(new Blob(["(" + JsonFormatWebWorker.toString() + ")()"], {type: 'text/javascript'})));
+        worker.onmessage = function (evt) {
+            let msg = evt.data;
+            switch (msg[0]) {
+                case 'FORMATTING' :
+                    formattingMsg.show();
+                    break;
+
+                case 'FORMATTED' :
+                    formattingMsg.hide();
+                    jfContent.html(msg[1]);
+
+                    _buildOptionBar();
+                    // 事件绑定
+                    _addEvents();
+                    // 支持文件下载
+                    _downloadSupport(cachedJsonString);
+
+                    break;
+            }
+        };
+        worker.postMessage({
+            jsonString: jsonStr,
+            skin: skin
+        });
+    };
+
+    // 同步的方式格式化
+    let formatSync = function (jsonStr, skin) {
+        cachedJsonString = JSON.stringify(JSON.parse(jsonStr), null, 4);
+
+        _initElements();
+        jfPre.html(htmlspecialchars(cachedJsonString));
+        let worker = new JsonFormatWebWorker(true);
+        worker.getFormattedHtml({
+            data: {
+                jsonString: jsonStr,
+                skin: skin
+            },
+            onFormatting: function (msg) {
+                formattingMsg.show();
+            },
+            onFormatted: function (msg) {
+                formattingMsg.hide();
+                jfContent.html(msg[1]);
+
+                _buildOptionBar();
+                // 事件绑定
+                _addEvents();
+                // 支持文件下载
+                _downloadSupport(cachedJsonString);
+            }
+        });
+    };
+
+    return {
+        format: format,
+        formatSync: formatSync
+    }
+})();
+
+
+/*============================================== web worker =========================================================*/
+
+/**
+ * 用webworker的形式来进行json格式化,在应对大json的时候,效果会非常明显
+ * @constructor
+ */
+var JsonFormatWebWorker = function (isUnSupportWorker = false) {
+
+    // 引入big-json.js解决大数字的问题
+    let __importScript = (filename) => {
+        this.compress && fetch(filename).then(resp => resp.text()).then(jsText => eval(jsText));
+    };
+    __importScript('json-bigint.js');
+
+    // Constants
+    let
+        TYPE_STRING = 1,
+        TYPE_NUMBER = 2,
+        TYPE_OBJECT = 3,
+        TYPE_ARRAY = 4,
+        TYPE_BOOL = 5,
+        TYPE_NULL = 6;
+
+    /**
+     * HTML特殊字符格式化
+     * @param str
+     * @returns {*}
+     */
+    let htmlspecialchars = function (str) {
+        str = str.replace(/&/g, '&amp;');
+        str = str.replace(/</g, '&lt;');
+        str = str.replace(/>/g, '&gt;');
+        str = str.replace(/"/g, '&quot;');
+        str = str.replace(/'/g, '&#039;');
+        return str;
+    };
+
+    /**
+     * FH 虚拟DOM
+     * @constructor
+     */
+    let FhVDom = function () {
+
+        this._id = 'fhvd_' + (new Date * 1);
+        this.tag = '';
+        this.innerText = '';
+        this.textContent = '';
+        this.childNodes = [];
+        this.className = '';
+        this.attributes = [];
+        this.classList = [];
+        this.classList.__proto__.add = this.classList.__proto__.push;
+
+        this.createElement = tag => {
+            this.tag = tag;
+            return this;
+        };
+
+        this.setAttribute = (attr, value) => {
+            this.attributes.push([attr, value]);
+        };
+
+        this.appendChild = child => {
+            this.childNodes.push(child);
+            return this;
+        };
+
+        this.getOuterHTML = () => {
+            let outerHtml = [];
+            if (this.tag) {
+                outerHtml.push(`<${this.tag}`);
+                let clsName = (this.className || '') + ' ' + this.classList.join(' ');
+                clsName.replace(/\s/g, '').length && outerHtml.push(` class="${clsName}"`);
+                this.attributes.length && outerHtml.push(this.attributes.map(attr => ` ${attr[0]}="${attr[1]}"`).join(''));
+                outerHtml.push(`>`);
+                if (('' + this.innerText).length) {
+                    outerHtml.push(this.innerText);
+                } else if (('' + this.textContent).length) {
+                    outerHtml.push(this.textContent);
+                } else {
+                    outerHtml.push(this.childNodes.map(node => node.getOuterHTML()).join(''))
+                }
+                outerHtml.push(`</${this.tag}>`);
+            } else {
+                if (('' + this.innerText).length) {
+                    outerHtml.push(this.innerText);
+                } else if (('' + this.textContent).length) {
+                    outerHtml.push(this.textContent);
+                }
+            }
+            return outerHtml.join('');
+        };
+
+        this.cloneNode = (deep) => {
+            let newDom = FhVDom.getInstance();
+            newDom.tag = this.tag;
+            if (deep || !this.tag) {
+                newDom.innerText = this.innerText;
+                newDom.textContent = this.textContent;
+            } else {
+                newDom.innerText = '';
+                newDom.textContent = '';
+            }
+            newDom.className = this.className;
+            newDom.classList = Array.from(this.classList);
+            newDom.attributes = Array.from(this.attributes);
+            return newDom;
+        };
+    };
+
+    // 构造器
+    FhVDom.getInstance = () => new FhVDom();
+
+    function createSpanNode(innerText, className) {
+        let span = FhVDom.getInstance().createElement('span');
+        span.className = className || '';
+        span.innerText = innerText || '';
+        return span;
+    }
+
+    function createDivNode(className) {
+        let div = FhVDom.getInstance().createElement('div');
+        div.className = className || '';
+        return div;
+    }
+
+    // Create template nodes
+    let templatesObj = {
+        t_item: createDivNode('item'),
+        t_key: createSpanNode('', 'key'),
+        t_string: createSpanNode('', 'string'),
+        t_number: createSpanNode('', 'number'),
+        t_exp: createSpanNode('', 'expand'),
+
+        t_null: createSpanNode('null', 'null'),
+        t_true: createSpanNode('true', 'bool'),
+        t_false: createSpanNode('false', 'bool'),
+
+        t_oBrace: createSpanNode('{', 'brace'),
+        t_cBrace: createSpanNode('}', 'brace'),
+        t_oBracket: createSpanNode('[', 'brace'),
+        t_cBracket: createSpanNode(']', 'brace'),
+
+        t_ellipsis: createSpanNode('', 'ellipsis'),
+        t_kvList: createDivNode('kv-list'),
+
+        t_colonAndSpace: createSpanNode(':\u00A0', 'colon'),
+        t_commaText: createSpanNode(',', 'comma'),
+        t_dblqText: createSpanNode('"', 'quote')
+    };
+
+    // Core recursive DOM-building function
+    function getItemDOM(value, keyName) {
+        let type,
+            item,
+            nonZeroSize,
+            templates = templatesObj,
+            objKey,
+            keySpan,
+            valueElement;
+
+        // Establish value type
+        if (typeof value === 'string')
+            type = TYPE_STRING;
+        else if (typeof value === 'number')
+            type = TYPE_NUMBER;
+        else if (value === false || value === true)
+            type = TYPE_BOOL;
+        else if (value === null)
+            type = TYPE_NULL;
+        else if (value instanceof Array)
+            type = TYPE_ARRAY;
+        else
+            type = TYPE_OBJECT;
+
+        item = templates.t_item.cloneNode(false);
+
+        // Add an 'expander' first (if this is object/array with non-zero size)
+        if (type === TYPE_OBJECT || type === TYPE_ARRAY) {
+
+            if (typeof JSON.BigNumber === 'function' && value instanceof JSON.BigNumber) {
+                value = JSON.stringify(value);
+                type = TYPE_NUMBER;
+            } else {
+                nonZeroSize = false;
+                for (objKey in value) {
+                    if (value.hasOwnProperty(objKey)) {
+                        nonZeroSize = true;
+                        break; // no need to keep counting; only need one
+                    }
+                }
+                if (nonZeroSize)
+                    item.appendChild(templates.t_exp.cloneNode(true));
+            }
+        }
+
+        // If there's a key, add that before the value
+        if (keyName !== false) { // NB: "" is a legal keyname in JSON
+            item.classList.add(type === TYPE_OBJECT ? 'item-object' : type === TYPE_ARRAY ? 'item-array' : 'item-line');
+            keySpan = templates.t_key.cloneNode(false);
+            keySpan.textContent = JSON.stringify(keyName).slice(1, -1); // remove quotes
+            item.appendChild(templates.t_dblqText.cloneNode(true));
+            item.appendChild(keySpan);
+            item.appendChild(templates.t_dblqText.cloneNode(true));
+            item.appendChild(templates.t_colonAndSpace.cloneNode(true));
+        }
+        else {
+            item.classList.add('item-block');
+        }
+
+        let kvList, childItem;
+        switch (type) {
+            case TYPE_STRING:
+                let innerStringEl = FhVDom.getInstance().createElement('span'),
+                    escapedString = JSON.stringify(value);
+                escapedString = escapedString.substring(1, escapedString.length - 1); // remove quotes
+                let isLink = false;
+                if (/^[\w]+:\/\//.test(value)) {
+                    try {
+                        let url = new URL(value);
+                        let innerStringA = FhVDom.getInstance().createElement('A');
+                        innerStringA.setAttribute('href', url.href);
+                        innerStringA.setAttribute('target', '_blank');
+                        innerStringA.innerText = htmlspecialchars(escapedString);
+                        innerStringEl.appendChild(innerStringA);
+                        isLink = true;
+                    } catch (e) {
+                    }
+                }
+
+                if (!isLink) {
+                    innerStringEl.innerText = htmlspecialchars(escapedString);
+                }
+                valueElement = templates.t_string.cloneNode(false);
+                valueElement.appendChild(templates.t_dblqText.cloneNode(true));
+                valueElement.appendChild(innerStringEl);
+                valueElement.appendChild(templates.t_dblqText.cloneNode(true));
+                item.appendChild(valueElement);
+                break;
+
+            case TYPE_NUMBER:
+                valueElement = templates.t_number.cloneNode(false);
+                valueElement.innerText = value;
+                item.appendChild(valueElement);
+                break;
+
+            case TYPE_OBJECT:
+                // Add opening brace
+                item.appendChild(templates.t_oBrace.cloneNode(true));
+                if (nonZeroSize) {
+                    item.appendChild(templates.t_ellipsis.cloneNode(false));
+                    kvList = templates.t_kvList.cloneNode(false);
+                    let keys = Object.keys(value).filter(k => value.hasOwnProperty(k));
+                    keys.forEach((k, index) => {
+                        childItem = getItemDOM(value[k], k);
+                        if (index < keys.length - 1) {
+                            childItem.appendChild(templates.t_commaText.cloneNode(true));
+                        }
+                        kvList.appendChild(childItem);
+                    });
+                    item.appendChild(kvList);
+                }
+
+                // Add closing brace
+                item.appendChild(templates.t_cBrace.cloneNode(true));
+                break;
+
+            case TYPE_ARRAY:
+                item.appendChild(templates.t_oBracket.cloneNode(true));
+                if (nonZeroSize) {
+                    item.appendChild(templates.t_ellipsis.cloneNode(false));
+                    kvList = templates.t_kvList.cloneNode(false);
+                    for (let i = 0, length = value.length, lastIndex = length - 1; i < length; i++) {
+                        childItem = getItemDOM(value[i], false);
+                        if (i < lastIndex)
+                            childItem.appendChild(templates.t_commaText.cloneNode(true));
+                        kvList.appendChild(childItem);
+                    }
+                    item.appendChild(kvList);
+                }
+                // Add closing bracket
+                item.appendChild(templates.t_cBracket.cloneNode(true));
+                break;
+
+            case TYPE_BOOL:
+                if (value)
+                    item.appendChild(templates.t_true.cloneNode(true));
+                else
+                    item.appendChild(templates.t_false.cloneNode(true));
+                break;
+
+            case TYPE_NULL:
+                item.appendChild(templates.t_null.cloneNode(true));
+                break;
+        }
+
+        return item;
+    }
+
+    // Listen for requests from content pages wanting to set up a port
+    // isUnSupportWorker 为true时,表示不支持webworker,不需要监听消息
+    if (!isUnSupportWorker) {
+        self.onmessage = function (event) {
+            // 插件在乎的是json字符串,所以只有json字符串时才进行格式化
+            if (event.data.jsonString) {
+                self.postMessage(['FORMATTING']);
+                let rootItem;
+                if (event.data.skin && event.data.skin === 'theme-simple') {
+                    rootItem = createDivNode('rootItem');
+                    rootItem.textContent = JSON.stringify(JSON.parse(event.data.jsonString), null, 4);
+                } else {
+                    rootItem = getItemDOM(JSON.parse(event.data.jsonString), false);
+                    rootItem.classList.add('rootItem');
+                }
+                let formattedHtml = `<div id="formattedJson">${rootItem.getOuterHTML()}</div>`;
+                self.postMessage(['FORMATTED', formattedHtml]);
+            }
+        };
+    }
+
+    // 针对不支持webworker的情况,允许直接调用
+    this.getFormattedHtml = function (options) {
+        options.onFormatting && options.onFormatting(['FORMATTING']);
+        let rootItem;
+        if (options.data.skin && options.data.skin === 'theme-simple') {
+            rootItem = createDivNode('rootItem');
+            rootItem.textContent = JSON.stringify(JSON.parse(options.data.jsonString), null, 4);
+        } else {
+            rootItem = getItemDOM(JSON.parse(options.data.jsonString), false);
+            rootItem.classList.add('rootItem');
+        }
+        let formattedHtml = `<div id="formattedJson">${rootItem.getOuterHTML()}</div>`;
+        options.onFormatted && options.onFormatted(['FORMATTED', formattedHtml]);
+    };
+};

+ 179 - 0
apps/json-format/index.css

@@ -0,0 +1,179 @@
+@import url("../static/vendor/codemirror/codemirror.css");
+@import url("../static/css/bootstrap.min.css");
+@import url("content-script.css");
+
+body {
+    background-color: #fff;
+}
+html,body {
+    padding: 0;
+    margin: 0;
+    height:100%;
+}
+
+.wp-json {
+    width:auto;
+}
+
+.wp-json .panel-body {
+    padding: 0;
+}
+#jfContent_pre {
+    display: none;
+    padding: 10px;
+}
+
+#errorMsg {
+    color: #f00;
+    margin-left: 10px;
+    float: right;
+}
+.x-placeholder img{
+    width: 400px;
+    opacity: 0.3;
+    margin: 5px 0 0 -8px;
+}
+.x-xdemo,a.x-xdemo {
+    margin-left: 30px;
+    font-size: 12px;
+    color: blue;
+    cursor: pointer;
+    text-decoration: underline;
+}
+.x-xdemo:hover {
+    text-decoration: underline;
+}
+
+#errorTips {
+    border-radius: 4px;
+    box-shadow: 6px 5px 7px rgba(229, 163, 163, 0.4);
+    background-color: #ffecf1;
+    border: 1px solid #dbb2b2;
+    margin-top:10px;
+}
+#errorCode .errorEm {
+    background-color:#ff921b;
+}
+#errorTarget {
+    color:#000;
+    background-color:#fff;
+}
+#errorCode {
+    padding:0 10px 10px;
+    word-break: break-all;
+}
+#errorCode ol {
+    padding:0 0 0 3em;
+}
+#errorCode .x-ph {
+    color: #f00;
+    font-weight: bold;
+}
+#tipsBox {
+    color:#b4465c;
+    padding: 6px 0 4px 2em;
+    background-color: #ffecf1;
+}
+#errorCode ol li {
+    padding:4px 7px 0;
+    list-style-type: decimal;
+    color:#b4465c;
+    background-color:#fdf7f7;
+}
+#errorCode ol li div {
+    color:#000;
+}
+
+.x-error {
+    color:red;
+}
+
+/*layout-up-down:上下布局模式*/
+.wp-json.layout-up-down .mod-json .panel-txt {
+    position: static;
+    width: 100%;
+    height: 250px;
+    margin: 0;
+}
+.wp-json.layout-up-down  .CodeMirror {
+    height: 250px;
+}
+.wp-json.layout-up-down .mod-json .rst-item {
+    margin: 0;
+}
+.wp-json.layout-up-down .mod-json .x-placeholder {
+    padding-top: 0;
+    text-align: left;
+}
+
+/* layout-left-right: 左右布局 */
+.wp-json.layout-left-right .mod-json .panel-txt {
+    width: 500px;
+    height: calc(100% - 10px);
+    float: left;
+}
+.wp-json.layout-left-right .mod-json .rst-item {
+    margin: 0 20px 0 30px;
+    padding-bottom:20px;
+    float: left;
+    width: calc(100% - 520px);
+}
+.wp-json.layout-left-right .mod-json .x-placeholder {
+    padding-top: 50px;
+    text-align: center;
+}
+.wp-json.layout-left-right .panel-body {
+    height:calc(100% - 95px);
+    padding-left: 15px;
+}
+.wp-json.layout-left-right  #jsonSource,
+.wp-json.layout-left-right  .CodeMirror {
+    height: calc(100% - 10px);
+}
+.wp-json.layout-left-right #formattedJson {
+    padding-top: 2px;
+}
+.wp-json.layout-left-right #errorTips {
+    margin-top: 0;
+}
+
+html.fh-jf .x-toolbar {
+    padding-top:5px;
+    padding-bottom: 5px;
+}
+html.fh-jf .x-toolbar.x-inpage {
+    margin:10px 20px;
+}
+.panel-title>a.x-other-tools {
+    margin:10px 0 0;
+    font-size: 12px;
+    cursor: pointer;
+    text-decoration: underline;
+    -webkit-user-select: none;
+    user-select: none;
+    color: #f00;
+    border-bottom: 1px solid #f00;
+    float: right;
+}
+.panel-title>a.x-other-tools:hover {
+    color:#00f;
+}
+.panel-heading {
+    padding:5px 15px;
+}
+
+#layoutBar {margin-left:30px}
+#btnLeftRight,#btnUpDown{
+    -webkit-border-radius:2px;-webkit-box-shadow:0px 1px 3px rgba(0,0,0,0.1);
+    -webkit-user-select:none;background:-webkit-linear-gradient(#fafafa, #f4f4f4 40%, #e5e5e5);outline: none;
+    border:1px solid #aaa;color:#444;font-size:12px;margin-bottom:0px;
+    position:relative;z-index:10;display:inline-block;
+    padding: 0 10px;
+    height: 30px;
+    text-shadow:1px 1px rgba(255,255,255,0.3)}
+#btnUpDown{margin-left:0;border-top-left-radius:0;border-bottom-left-radius:0;border-left:0;}
+#btnLeftRight{margin-right:0;border-top-right-radius:0;border-bottom-right-radius:0;border-right:none}
+#btnLeftRight:hover,#btnUpDown:hover{-webkit-box-shadow:0px 1px 3px rgba(0,0,0,0.2);
+    background:#ebebeb -webkit-linear-gradient(#fefefe, #f8f8f8 40%, #e9e9e9);border-color:#999;color:#222}
+#btnLeftRight.selected, #btnUpDown.selected{-webkit-box-shadow:inset 0px 1px 5px rgba(0,0,0,0.2);
+    background:#ebebeb -webkit-linear-gradient(#e4e4e4, #dfdfdf 40%, #dcdcdc);color:#333}

+ 85 - 0
apps/json-format/index.html

@@ -0,0 +1,85 @@
+<!DOCTYPE HTML>
+<html lang="zh-CN" class="fh-jf">
+    <head>
+        <title>JSON格式化查看工具</title>
+        <meta charset="UTF-8">
+        <link rel="stylesheet" href="index.css" />
+		<script type="text/javascript" src="../static/vendor/evalCore.min.js"></script>
+        <script type="text/javascript" src="../static/vendor/vue/vue.js"></script>
+    </head>
+    <body class="theme-default">
+        <div class="wrapper wp-json" id="pageContainer">
+            <div class="panel panel-default" style="margin-bottom: 0px;">
+                <div class="panel-heading">
+                    <h3 class="panel-title">
+                        <a href="https://www.baidufe.com/fehelper/index/index.html" target="_blank" class="x-a-high">
+                            <img src="../static/img/fe-16.png" alt="fehelper"/> FeHelper</a>:JSON格式化
+                        <span class="x-xdemo" ref="demoLink1" @click="setDemo">示例1:JSON片段</span>
+                        <a class="x-xdemo" href="http://t.weather.sojson.com/api/weather/city/101030100" target="_blank">示例2:在线JSON</a>
+
+                        <span id="layoutBar">
+                            <button id="btnLeftRight" ref="btnLeftRight" class="selected" @click="changeLayout('left-right')">左右布局</button><button id="btnUpDown" ref="btnUpDown" @click="changeLayout('up-down')">上下布局</button>
+                        </span>
+
+                        <a class="x-other-tools" @click="openOptionsPage()">去开启FeHelper的更多功能&gt;&gt;</a>
+                    </h3>
+                </div>
+            </div>
+
+            <div class="x-toolbar x-inpage">
+
+                <button id="btnFormat" class="btn btn-primary btn-xs ui-mr-10" @click="format">格式化</button>
+                <button id="btnCompress" class="btn btn-success btn-xs" @click="compress">压缩</button>
+                <span class="x-split">|</span>
+                <input type="checkbox" v-model="jsonLintSwitch" id="jsonLint" @click="lintOn"><label for="jsonLint">JSONLint</label>
+                <span class="x-split">|</span>
+                <input type="checkbox" v-model="autoDecode" id="endecode" @click="autoDecodeFn"><label for="endecode">自动解码</label>
+                <span class="x-split">|</span>
+                <input type="checkbox" v-model="overrideJson" id="jsonOvrd" @click="setCache"><label for="jsonOvrd">节点编辑</label>
+                <span class="x-split">|</span>
+                <span class="x-sort">
+                    <span class="x-stitle">排序:</span>
+                    <label for="sort_null">默认</label>
+                    <input type="radio" name="jsonsort" id="sort_null" value="0" checked @click="format">
+                    <label for="sort_asc">升序</label>
+                    <input type="radio" name="jsonsort" id="sort_asc" value="1" @click="format">
+                    <label for="sort_desc">降序</label>
+                    <input type="radio" name="jsonsort" id="sort_desc" value="-1" @click="format">
+                </span>
+                <span class="x-split">|</span>
+                <span class="x-endecode">
+                    <button class="xjf-btn xjf-btn-left" @click="uniEncode">Uni编码</button><button class="xjf-btn xjf-btn-mid" @click="uniDecode">Uni解码</button><button class="xjf-btn xjf-btn-right" @click="urlDecode">URL解码</button>
+                </span>
+
+                <span id="optionBar"></span>
+            </div>
+
+            <div class="panel-body mod-json">
+                <div class="row panel-txt">
+                    <textarea class="form-control mod-textarea" id="jsonSource" placeholder="在这里粘贴您需要进行格式化的JSON代码" ref="jsonBox"></textarea>
+                </div>
+
+                <div class="row rst-item" id="modJsonResult">
+                    <div id="formattingMsg"><span class="x-loading"></span>格式化中...</div>
+                    <div id="jfCallbackName_start" class="callback-name" v-html="jfCallbackName_start"></div>
+                    <div id="jfContent" v-html="placeHolder"></div>
+                    <pre id="jfContent_pre"></pre>
+                    <div id="jfCallbackName_end" class="callback-name" v-html="jfCallbackName_end"></div>
+                </div>
+            </div>
+        </div>
+        <script src="../static/vendor/jquery/jquery-3.3.1.min.js"></script>
+        <script src="../static/vendor/codemirror/codemirror.js"></script>
+        <script src="../static/vendor/codemirror/javascript.js"></script>
+        <script src="../static/vendor/codemirror/active-line.js"></script>
+        <script src="../static/vendor/codemirror/matchbrackets.js"></script>
+        <script src="../static/vendor/codemirror/placeholder.js"></script>
+        <script src="json-lint.js"></script>
+        <script src="json-bigint.js"></script>
+        <script src="format-lib.js"></script>
+        <script src="json-abc.js"></script>
+        <script src="json-decode.js"></script>
+        <script src="../static/js/dark-mode.js"></script>
+        <script src="index.js" type="module"></script>
+    </body>
+</html>

+ 297 - 0
apps/json-format/index.js

@@ -0,0 +1,297 @@
+/**
+ * FeHelper Json Format Tools
+ */
+
+// 一些全局变量
+let editor = {};
+let LOCAL_KEY_OF_LAYOUT = 'local-layout-key';
+let JSON_LINT = 'jsonformat:json-lint-switch';
+let EDIT_ON_CLICK = 'jsonformat:edit-on-click';
+let AUTO_DECODE = 'jsonformat:auto-decode';
+
+new Vue({
+    el: '#pageContainer',
+    data: {
+        defaultResultTpl: '<div class="x-placeholder"><img src="../json-format/json-demo.jpg" alt="json-placeholder"></div>',
+        placeHolder: '',
+        jsonFormattedSource: '',
+        errorMsg: '',
+        errorJsonCode: '',
+        errorPos: '',
+        jfCallbackName_start: '',
+        jfCallbackName_end: '',
+        jsonLintSwitch: true,
+        autoDecode: false,
+        fireChange: true,
+        overrideJson: false
+    },
+    mounted: function () {
+        // 自动开关灯控制
+        DarkModeMgr.turnLightAuto();
+
+        this.placeHolder = this.defaultResultTpl;
+
+        this.autoDecode = localStorage.getItem(AUTO_DECODE);
+        this.autoDecode = this.autoDecode === 'true';
+
+        this.jsonLintSwitch = (localStorage.getItem(JSON_LINT) !== 'false');
+        this.overrideJson = (localStorage.getItem(EDIT_ON_CLICK) === 'true');
+        this.changeLayout(localStorage.getItem(LOCAL_KEY_OF_LAYOUT));
+
+        editor = CodeMirror.fromTextArea(this.$refs.jsonBox, {
+            mode: "text/javascript",
+            lineNumbers: true,
+            matchBrackets: true,
+            styleActiveLine: true,
+            lineWrapping: true
+        });
+
+        //输入框聚焦
+        editor.focus();
+
+        // 格式化以后的JSON,点击以后可以重置原内容
+        window._OnJsonItemClickByFH = (jsonTxt) => {
+            if (this.overrideJson) {
+                this.disableEditorChange(jsonTxt);
+            }
+        };
+        editor.on('change', (editor, changes) => {
+            this.jsonFormattedSource = editor.getValue().replace(/\n/gm, ' ');
+            this.fireChange && this.format();
+        });
+
+        // 在tab创建或者更新时候,监听事件,看看是否有参数传递过来
+        if (location.protocol === 'chrome-extension:') {
+            chrome.tabs.query({currentWindow: true,active: true, }, (tabs) => {
+                let activeTab = tabs.filter(tab => tab.active)[0];
+                chrome.runtime.sendMessage({
+                    type: 'fh-dynamic-any-thing',
+                    thing: 'request-page-content',
+                    tabId: activeTab.id
+                }).then(resp => {
+                    if(!resp || !resp.content) return ;
+                    editor.setValue(resp.content || '');
+                    this.format();
+                });
+            });
+        }
+    },
+    methods: {
+        format: function () {
+            this.errorMsg = '';
+            this.placeHolder = this.defaultResultTpl;
+            this.jfCallbackName_start = '';
+            this.jfCallbackName_end = '';
+
+            let source = editor.getValue().replace(/\n/gm, ' ');
+            if (!source) {
+                return false;
+            }
+
+            // JSONP形式下的callback name
+            let funcName = null;
+            // json对象
+            let jsonObj = null;
+
+            // 下面校验给定字符串是否为一个合法的json
+            try {
+                // 再看看是不是jsonp的格式
+                let reg = /^([\w\.]+)\(\s*([\s\S]*)\s*\)$/igm;
+                let matches = reg.exec(source);
+                if (matches != null) {
+                    funcName = matches[1];
+                    source = matches[2];
+                }
+                // 这里可能会throw exception
+                jsonObj = JSON.parse(source);
+
+            } catch (ex) {
+                // new Function的方式,能自动给key补全双引号,但是不支持bigint,所以是下下策,放在try-catch里搞
+                try {
+                    jsonObj = new Function("return " + source)();
+                } catch (exx) {
+                    try {
+                        // 再给你一次机会,是不是下面这种情况:  "{\"ret\":\"0\", \"msg\":\"ok\"}"
+                        jsonObj = new Function("return '" + source + "'")();
+                        if (typeof jsonObj === 'string') {
+                            try {
+                                // 确保bigint不会失真
+                                jsonObj = JSON.parse(jsonObj);
+                            } catch (ie) {
+                                // 最后给你一次机会,是个字符串,老夫给你再转一次
+                                jsonObj = new Function("return " + jsonObj)();
+                            }
+                        }
+                    } catch (exxx) {
+                        this.errorMsg = exxx.message;
+                    }
+                }
+            }
+
+            try{
+                // 这里多做一个动作,给没有携带双引号的Key都自动加上,防止Long类型失真
+                const regex = /([{,]\s*)(\w+)(\s*:)/g;
+                source = source.replace(regex, '$1"$2"$3');
+                jsonObj = JSON.parse(source);
+            }catch(e){
+                // 这里什么动作都不需要做,这种情况下转换失败的,肯定是Value被污染了,抛弃即可
+            }
+
+            // 是json格式,可以进行JSON自动格式化
+            if (jsonObj != null && typeof jsonObj === "object" && !this.errorMsg.length) {
+                try {
+                    let sortType = document.querySelectorAll('[name=jsonsort]:checked')[0].value;
+                    if (sortType !== '0') {
+                        jsonObj = JsonABC.sortObj(jsonObj, parseInt(sortType), true);
+                    }
+                    source = JSON.stringify(jsonObj);
+                } catch (ex) {
+                    // 通过JSON反解不出来的,一定有问题
+                    this.errorMsg = ex.message;
+                }
+
+                if (!this.errorMsg.length) {
+
+                    if (this.autoDecode) {
+                        (async () => {
+                            let txt = await JsonEnDecode.urlDecodeByFetch(source);
+                            source = JsonEnDecode.uniDecode(txt);
+                            Formatter.format(source);
+                        })();
+                    } else {
+                        Formatter.format(source);
+                    }
+
+                    this.placeHolder = '';
+                    this.jsonFormattedSource = source;
+
+                    // 如果是JSONP格式的,需要把方法名也显示出来
+                    if (funcName != null) {
+                        this.jfCallbackName_start = funcName + '(';
+                        this.jfCallbackName_end = ')';
+                    } else {
+                        this.jfCallbackName_start = '';
+                        this.jfCallbackName_end = '';
+                    }
+
+                    this.$nextTick(() => {
+                        this.updateWrapperHeight();
+                    })
+                }
+            }
+
+            if (this.errorMsg.length) {
+                if (this.jsonLintSwitch) {
+                    return this.lintOn();
+                } else {
+                    this.placeHolder = '<span class="x-error">' + this.errorMsg + '</span>';
+                    return false;
+                }
+            }
+
+            return true;
+        },
+
+        compress: function () {
+            if (this.format()) {
+                let jsonTxt = this.jfCallbackName_start + this.jsonFormattedSource + this.jfCallbackName_end;
+                this.disableEditorChange(jsonTxt);
+            }
+        },
+
+        autoDecodeFn: function () {
+            this.$nextTick(() => {
+                localStorage.setItem(AUTO_DECODE, this.autoDecode);
+                this.format();
+            });
+        },
+
+        uniEncode: function () {
+            editor.setValue(JsonEnDecode.uniEncode(editor.getValue()));
+        },
+
+        uniDecode: function () {
+            editor.setValue(JsonEnDecode.uniDecode(editor.getValue()));
+        },
+
+        urlDecode: function () {
+            JsonEnDecode.urlDecodeByFetch(editor.getValue()).then(text => editor.setValue(text));
+        },
+
+        updateWrapperHeight: function () {
+            let curLayout = localStorage.getItem(LOCAL_KEY_OF_LAYOUT);
+            let elPc = document.querySelector('#pageContainer');
+            if (curLayout === 'up-down') {
+                elPc.style.height = 'auto';
+            } else {
+                elPc.style.height = Math.max(elPc.scrollHeight, document.body.scrollHeight) + 'px';
+            }
+        },
+
+        changeLayout: function (type) {
+            let elPc = document.querySelector('#pageContainer');
+            if (type === 'up-down') {
+                elPc.classList.remove('layout-left-right');
+                elPc.classList.add('layout-up-down');
+                this.$refs.btnLeftRight.classList.remove('selected');
+                this.$refs.btnUpDown.classList.add('selected');
+            } else {
+                elPc.classList.remove('layout-up-down');
+                elPc.classList.add('layout-left-right');
+                this.$refs.btnLeftRight.classList.add('selected');
+                this.$refs.btnUpDown.classList.remove('selected');
+            }
+            localStorage.setItem(LOCAL_KEY_OF_LAYOUT, type);
+            this.updateWrapperHeight();
+        },
+
+        setCache: function () {
+            this.$nextTick(() => {
+                localStorage.setItem(EDIT_ON_CLICK, this.overrideJson);
+            });
+        },
+
+        lintOn: function () {
+            this.$nextTick(() => {
+                localStorage.setItem(JSON_LINT, this.jsonLintSwitch);
+            });
+            if (!editor.getValue().trim()) {
+                return true;
+            }
+            this.$nextTick(() => {
+                if (!this.jsonLintSwitch) {
+                    return;
+                }
+                let lintResult = JsonLint.lintDetect(editor.getValue());
+                if (!isNaN(lintResult.line)) {
+                    this.placeHolder = '<div id="errorTips">' +
+                        '<div id="tipsBox">错误位置:' + (lintResult.line + 1) + '行,' + (lintResult.col + 1) + '列;缺少字符或字符不正确</div>' +
+                        '<div id="errorCode">' + lintResult.dom + '</div></div>';
+                }
+            });
+            return false;
+        },
+
+        disableEditorChange: function (jsonTxt) {
+            this.fireChange = false;
+            this.$nextTick(() => {
+                editor.setValue(jsonTxt);
+                this.$nextTick(() => {
+                    this.fireChange = true;
+                })
+            })
+        },
+
+        openOptionsPage: function(){
+            chrome.runtime.openOptionsPage();
+        },
+
+        setDemo: function () {
+            let demo = '{"BigIntSupported":995815895020119788889,"date":"20180322","message":"Success !","status":200,"city":"北京","count":632,"data":{"shidu":"34%","pm25":73,"pm10":91,"quality":"良","wendu":"5","ganmao":"极少数敏感人群应减少户外活动","yesterday":{"date":"21日星期三","sunrise":"06:19","high":"高温 11.0℃","low":"低温 1.0℃","sunset":"18:26","aqi":85,"fx":"南风","fl":"<3级","type":"多云","notice":"阴晴之间,谨防紫外线侵扰"},"forecast":[{"date":"22日星期四","sunrise":"06:17","high":"高温 17.0℃","low":"低温 1.0℃","sunset":"18:27","aqi":98,"fx":"西南风","fl":"<3级","type":"晴","notice":"愿你拥有比阳光明媚的心情"},{"date":"23日星期五","sunrise":"06:16","high":"高温 18.0℃","low":"低温 5.0℃","sunset":"18:28","aqi":118,"fx":"无持续风向","fl":"<3级","type":"多云","notice":"阴晴之间,谨防紫外线侵扰"},{"date":"24日星期六","sunrise":"06:14","high":"高温 21.0℃","low":"低温 7.0℃","sunset":"18:29","aqi":52,"fx":"西南风","fl":"<3级","type":"晴","notice":"愿你拥有比阳光明媚的心情"},{"date":"25日星期日","sunrise":"06:13","high":"高温 22.0℃","low":"低温 7.0℃","sunset":"18:30","aqi":71,"fx":"西南风","fl":"<3级","type":"晴","notice":"愿你拥有比阳光明媚的心情"},{"date":"26日星期一","sunrise":"06:11","high":"高温 21.0℃","low":"低温 8.0℃","sunset":"18:31","aqi":97,"fx":"西南风","fl":"<3级","type":"多云","notice":"阴晴之间,谨防紫外线侵扰"}]}}';
+            editor.setValue(demo);
+            this.$nextTick(() => {
+                this.format();
+            })
+        }
+    }
+});

+ 74 - 0
apps/json-format/json-abc.js

@@ -0,0 +1,74 @@
+/**
+ * JSON排序处理
+ * @author zhaoxianlie
+ */
+window.JsonABC = (function () {
+
+    // Is a value an array?
+    function isArray(val) {
+        return Object.prototype.toString.call(val) === '[object Array]';
+    }
+
+    // Is a value an Object?
+    function isPlainObject(val) {
+        return Object.prototype.toString.call(val) === '[object Object]';
+    }
+
+    /**
+     * 排序算法
+     * @param un 需要排序的JSON
+     * @param asc 是否正序
+     * @param noarray 不包括数组,默认false
+     * @returns {{}}
+     */
+    function sortObj(un, asc, noarray) {
+        asc = asc !== -1 ? 1 : -1;
+        noarray = noarray || false;
+
+        let or = {};
+
+        // 如果是BigInt的对象,则不参与排序
+        if (typeof JSON.BigNumber === 'function' && un instanceof JSON.BigNumber) {
+            return un;
+        }
+
+        if (isArray(un)) {
+            // Sort or don't sort arrays
+            if (noarray) {
+                or = un;
+            } else {
+                or = un.sort();
+            }
+
+            or.forEach(function (v, i) {
+                or[i] = sortObj(v, asc, noarray);
+            });
+
+            if (!noarray) {
+                or = or.sort(function (a, b) {
+                    a = (typeof a === 'object') ? JSON.stringify(a) : a;
+                    b = (typeof b === 'object') ? JSON.stringify(b) : b;
+                    return a < b ? -1 * asc : (a > b ? 1 * asc : 0);
+                });
+            }
+        } else if (isPlainObject(un)) {
+            or = {};
+            Object.keys(un).sort(function (a, b) {
+                if (a.toLowerCase() < b.toLowerCase()) return -1 * asc;
+                if (a.toLowerCase() > b.toLowerCase()) return 1 * asc;
+                return 0;
+            }).forEach(function (key) {
+                or[key] = sortObj(un[key], asc, noarray);
+            });
+        } else {
+            or = un;
+        }
+
+        return or;
+    }
+
+    return {
+        sortObj: sortObj
+    };
+
+})();

La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
apps/json-format/json-bigint.js


+ 103 - 0
apps/json-format/json-decode.js

@@ -0,0 +1,103 @@
+/**
+ * 此方法用于将Json内容的Unicode编解码
+ * @param {Object} text
+ */
+window.JsonEnDecode = {
+    uniEncode: function (str) {
+        return escape(str)
+            .replace(/%u/gi, '\\u')
+            .replace(/%7b/gi, '{')
+            .replace(/%7d/gi, '}')
+            .replace(/%3a/gi, ':')
+            .replace(/%2c/gi, ',')
+            .replace(/%27/gi, '\'')
+            .replace(/%22/gi, '"')
+            .replace(/%5b/gi, '[')
+            .replace(/%5d/gi, ']')
+            .replace(/%3D/gi, '=')
+            .replace(/%08/gi, '\b')
+            .replace(/%0D/gi, '\r')
+            .replace(/%0C/gi, '\f')
+            .replace(/%09/gi, '\t')
+            .replace(/%20/gi, ' ')
+            .replace(/%0A/gi, '\n')
+            .replace(/%3E/gi, '>')
+            .replace(/%3C/gi, '<')
+            .replace(/%3F/gi, '?');
+    },
+    uniDecode: function (text) {
+        text = text.replace(/(\\)?\\u/gi, "%u").replace('%u0025', '%25');
+        text = unescape(text.toString().replace(/%2B/g, "+"));
+
+        let matches = text.match(/(%u00([0-9A-F]{2}))/gi);
+        if (matches) {
+            for (let matchid = 0; matchid < matches.length; matchid++) {
+                let code = matches[matchid].substring(1, 3);
+                let x = Number("0x" + code);
+                if (x >= 128) {
+                    text = text.replace(matches[matchid], code);
+                }
+            }
+        }
+        text = unescape(text.toString().replace(/%2B/g, "+"));
+
+        return text;
+    },
+
+    urlDecode: function (str) {
+        try {
+            return decodeURIComponent(str);
+        } catch (e) {
+            return str;
+        }
+    },
+
+    // 此种模式,随便用
+    urlDecodeByFetch: function (str) {
+        return new Promise((resolve, reject) => {
+            try {
+                fetch(`data:text/javascript;charset=utf8,${str.replace(/"/g, '%22').replace(/#/g, '%23')}`)
+                    .then(res => res.text(), error => {
+                        reject && reject(error);
+                    })
+                    .then(text => {
+                        resolve && resolve(text);
+                    });
+            } catch (e) {
+                resolve && resolve(str);
+            }
+        });
+    },
+
+    // 此种形式需要在manifest中增加csp策略,暂且不用
+    urlDecodeByIframe: function (str, charset) {
+        charset = charset || 'utf8';
+        return new Promise((resolve, reject) => {
+            let iframe = document.querySelector('#_urlDecode_iframe_');
+            if (iframe) {
+                iframe.remove();
+            }
+            iframe = document.createElement('iframe');
+            iframe.setAttribute('id', '_urlDecode_iframe_');
+            iframe.style.display = 'none';
+            iframe.width = "0";
+            iframe.height = "0";
+            iframe.scrolling = "no";
+            iframe.allowtransparency = "true";
+            iframe.frameborder = "0";
+            iframe.src = 'about:blank';
+            document.body.appendChild(iframe);
+            window._urlDecodeCallback = window._urlDecodeCallback || function (e) {
+                resolve && resolve(e.data);
+                iframe.remove();
+            };
+            window.removeEventListener('message', window._urlDecodeCallback);
+            window.addEventListener('message', window._urlDecodeCallback, false);
+            try {
+                iframe.contentWindow.document.write('<html><scrip' + `t charset="${charset}" src="data:text/javascript;charset=${charset},parent.postMessage(\`${str.replace(/"/g, '%22').replace(/#/g, '%23')}\`)"></scrip` + 't></html>');
+            } catch (e) {
+                reject && reject(e);
+            }
+        });
+    }
+};

BIN
apps/json-format/json-demo.jpg


+ 662 - 0
apps/json-format/json-lint.js

@@ -0,0 +1,662 @@
+/* Jison generated parser */
+let jsonlint = (function () {
+    var parser = {
+        trace: function trace() {
+        },
+        yy: {},
+        symbols_: {
+            "error": 2,
+            "JSONString": 3,
+            "STRING": 4,
+            "JSONNumber": 5,
+            "NUMBER": 6,
+            "JSONNullLiteral": 7,
+            "NULL": 8,
+            "JSONBooleanLiteral": 9,
+            "TRUE": 10,
+            "FALSE": 11,
+            "JSONText": 12,
+            "JSONValue": 13,
+            "EOF": 14,
+            "JSONObject": 15,
+            "JSONArray": 16,
+            "{": 17,
+            "}": 18,
+            "JSONMemberList": 19,
+            "JSONMember": 20,
+            ":": 21,
+            ",": 22,
+            "[": 23,
+            "]": 24,
+            "JSONElementList": 25,
+            "$accept": 0,
+            "$end": 1
+        },
+        terminals_: {
+            2: "error",
+            4: "STRING",
+            6: "NUMBER",
+            8: "NULL",
+            10: "TRUE",
+            11: "FALSE",
+            14: "EOF",
+            17: "{",
+            18: "}",
+            21: ":",
+            22: ",",
+            23: "[",
+            24: "]"
+        },
+        productions_: [0, [3, 1], [5, 1], [7, 1], [9, 1], [9, 1], [12, 2], [13, 1], [13, 1], [13, 1], [13, 1], [13, 1], [13, 1], [15, 2], [15, 3], [20, 3], [19, 1], [19, 3], [16, 2], [16, 3], [25, 1], [25, 3]],
+        performAction: function anonymous(yytext, yyleng, yylineno, yy, yystate, $$, _$) {
+
+            var $0 = $$.length - 1;
+            switch (yystate) {
+                case 1: // replace escaped characters with actual character
+                    this.$ = yytext.replace(/\\(\\|")/g, "$" + "1")
+                        .replace(/\\n/g, '\n')
+                        .replace(/\\r/g, '\r')
+                        .replace(/\\t/g, '\t')
+                        .replace(/\\v/g, '\v')
+                        .replace(/\\f/g, '\f')
+                        .replace(/\\b/g, '\b');
+
+                    break;
+                case 2:
+                    this.$ = Number(yytext);
+                    break;
+                case 3:
+                    this.$ = null;
+                    break;
+                case 4:
+                    this.$ = true;
+                    break;
+                case 5:
+                    this.$ = false;
+                    break;
+                case 6:
+                    return this.$ = $$[$0 - 1];
+                    break;
+                case 13:
+                    this.$ = {};
+                    break;
+                case 14:
+                    this.$ = $$[$0 - 1];
+                    break;
+                case 15:
+                    this.$ = [$$[$0 - 2], $$[$0]];
+                    break;
+                case 16:
+                    this.$ = {};
+                    this.$[$$[$0][0]] = $$[$0][1];
+                    break;
+                case 17:
+                    this.$ = $$[$0 - 2];
+                    $$[$0 - 2][$$[$0][0]] = $$[$0][1];
+                    break;
+                case 18:
+                    this.$ = [];
+                    break;
+                case 19:
+                    this.$ = $$[$0 - 1];
+                    break;
+                case 20:
+                    this.$ = [$$[$0]];
+                    break;
+                case 21:
+                    this.$ = $$[$0 - 2];
+                    $$[$0 - 2].push($$[$0]);
+                    break;
+            }
+        },
+        table: [{
+            3: 5,
+            4: [1, 12],
+            5: 6,
+            6: [1, 13],
+            7: 3,
+            8: [1, 9],
+            9: 4,
+            10: [1, 10],
+            11: [1, 11],
+            12: 1,
+            13: 2,
+            15: 7,
+            16: 8,
+            17: [1, 14],
+            23: [1, 15]
+        }, {1: [3]}, {14: [1, 16]}, {14: [2, 7], 18: [2, 7], 22: [2, 7], 24: [2, 7]}, {
+            14: [2, 8],
+            18: [2, 8],
+            22: [2, 8],
+            24: [2, 8]
+        }, {14: [2, 9], 18: [2, 9], 22: [2, 9], 24: [2, 9]}, {
+            14: [2, 10],
+            18: [2, 10],
+            22: [2, 10],
+            24: [2, 10]
+        }, {14: [2, 11], 18: [2, 11], 22: [2, 11], 24: [2, 11]}, {
+            14: [2, 12],
+            18: [2, 12],
+            22: [2, 12],
+            24: [2, 12]
+        }, {14: [2, 3], 18: [2, 3], 22: [2, 3], 24: [2, 3]}, {
+            14: [2, 4],
+            18: [2, 4],
+            22: [2, 4],
+            24: [2, 4]
+        }, {14: [2, 5], 18: [2, 5], 22: [2, 5], 24: [2, 5]}, {
+            14: [2, 1],
+            18: [2, 1],
+            21: [2, 1],
+            22: [2, 1],
+            24: [2, 1]
+        }, {14: [2, 2], 18: [2, 2], 22: [2, 2], 24: [2, 2]}, {3: 20, 4: [1, 12], 18: [1, 17], 19: 18, 20: 19}, {
+            3: 5,
+            4: [1, 12],
+            5: 6,
+            6: [1, 13],
+            7: 3,
+            8: [1, 9],
+            9: 4,
+            10: [1, 10],
+            11: [1, 11],
+            13: 23,
+            15: 7,
+            16: 8,
+            17: [1, 14],
+            23: [1, 15],
+            24: [1, 21],
+            25: 22
+        }, {1: [2, 6]}, {14: [2, 13], 18: [2, 13], 22: [2, 13], 24: [2, 13]}, {18: [1, 24], 22: [1, 25]}, {
+            18: [2, 16],
+            22: [2, 16]
+        }, {21: [1, 26]}, {14: [2, 18], 18: [2, 18], 22: [2, 18], 24: [2, 18]}, {
+            22: [1, 28],
+            24: [1, 27]
+        }, {22: [2, 20], 24: [2, 20]}, {14: [2, 14], 18: [2, 14], 22: [2, 14], 24: [2, 14]}, {
+            3: 20,
+            4: [1, 12],
+            20: 29
+        }, {
+            3: 5,
+            4: [1, 12],
+            5: 6,
+            6: [1, 13],
+            7: 3,
+            8: [1, 9],
+            9: 4,
+            10: [1, 10],
+            11: [1, 11],
+            13: 30,
+            15: 7,
+            16: 8,
+            17: [1, 14],
+            23: [1, 15]
+        }, {14: [2, 19], 18: [2, 19], 22: [2, 19], 24: [2, 19]}, {
+            3: 5,
+            4: [1, 12],
+            5: 6,
+            6: [1, 13],
+            7: 3,
+            8: [1, 9],
+            9: 4,
+            10: [1, 10],
+            11: [1, 11],
+            13: 31,
+            15: 7,
+            16: 8,
+            17: [1, 14],
+            23: [1, 15]
+        }, {18: [2, 17], 22: [2, 17]}, {18: [2, 15], 22: [2, 15]}, {22: [2, 21], 24: [2, 21]}],
+        defaultActions: {16: [2, 6]},
+        parseError: function parseError(str, hash) {
+            throw new Error(str);
+        },
+        parse: function parse(input) {
+            var self = this,
+                stack = [0],
+                vstack = [null], // semantic value stack
+                lstack = [], // location stack
+                table = this.table,
+                yytext = '',
+                yylineno = 0,
+                yyleng = 0,
+                recovering = 0,
+                TERROR = 2,
+                EOF = 1;
+
+            //this.reductionCount = this.shiftCount = 0;
+
+            this.lexer.setInput(input);
+            this.lexer.yy = this.yy;
+            this.yy.lexer = this.lexer;
+            if (typeof this.lexer.yylloc == 'undefined')
+                this.lexer.yylloc = {};
+            var yyloc = this.lexer.yylloc;
+            lstack.push(yyloc);
+
+            if (typeof this.yy.parseError === 'function')
+                this.parseError = this.yy.parseError;
+
+            function popStack(n) {
+                stack.length = stack.length - 2 * n;
+                vstack.length = vstack.length - n;
+                lstack.length = lstack.length - n;
+            }
+
+            function lex() {
+                var token;
+                token = self.lexer.lex() || 1; // $end = 1
+                // if token isn't its numeric value, convert
+                if (typeof token !== 'number') {
+                    token = self.symbols_[token] || token;
+                }
+                return token;
+            }
+
+            var symbol, preErrorSymbol, state, action, a, r, yyval = {}, p, len, newState, expected;
+            while (true) {
+                // retreive state number from top of stack
+                state = stack[stack.length - 1];
+
+                // use default actions if available
+                if (this.defaultActions[state]) {
+                    action = this.defaultActions[state];
+                } else {
+                    if (symbol == null)
+                        symbol = lex();
+                    // read action for current state and first input
+                    action = table[state] && table[state][symbol];
+                }
+
+                // handle parse error
+                _handle_error:
+                    if (typeof action === 'undefined' || !action.length || !action[0]) {
+
+                        if (!recovering) {
+                            // Report error
+                            expected = [];
+                            for (p in table[state]) if (this.terminals_[p] && p > 2) {
+                                expected.push("'" + this.terminals_[p] + "'");
+                            }
+                            var errStr = '';
+                            if (this.lexer.showPosition) {
+                                errStr = 'Parse error on line ' + (yylineno + 1) + ":\n" + this.lexer.showPosition() + "\nExpecting " + expected.join(', ') + ", got '" + this.terminals_[symbol] + "'";
+                            } else {
+                                errStr = 'Parse error on line ' + (yylineno + 1) + ": Unexpected " +
+                                    (symbol == 1 /*EOF*/ ? "end of input" :
+                                        ("'" + (this.terminals_[symbol] || symbol) + "'"));
+                            }
+                            this.parseError(errStr,
+                                {
+                                    text: this.lexer.match,
+                                    token: this.terminals_[symbol] || symbol,
+                                    lineText: this.lexer._sLine,
+                                    line: this.lexer.yylineno,
+                                    pos: this.lexer._pre,
+                                    loc: yyloc,
+                                    expected: expected
+                                });
+                        }
+
+                        // just recovered from another error
+                        if (recovering == 3) {
+                            if (symbol == EOF) {
+                                throw new Error(errStr || 'Parsing halted.');
+                            }
+
+                            // discard current lookahead and grab another
+                            yyleng = this.lexer.yyleng;
+                            yytext = this.lexer.yytext;
+                            yylineno = this.lexer.yylineno;
+                            yyloc = this.lexer.yylloc;
+                            symbol = lex();
+                        }
+
+                        // try to recover from error
+                        while (1) {
+                            // check for error recovery rule in this state
+                            if ((TERROR.toString()) in table[state]) {
+                                break;
+                            }
+                            if (state == 0) {
+                                throw new Error(errStr || 'Parsing halted.');
+                            }
+                            popStack(1);
+                            state = stack[stack.length - 1];
+                        }
+
+                        preErrorSymbol = symbol; // save the lookahead token
+                        symbol = TERROR;         // insert generic error symbol as new lookahead
+                        state = stack[stack.length - 1];
+                        action = table[state] && table[state][TERROR];
+                        recovering = 3; // allow 3 real symbols to be shifted before reporting a new error
+                    }
+
+                // this shouldn't happen, unless resolve defaults are off
+                if (action[0] instanceof Array && action.length > 1) {
+                    throw new Error('Parse Error: multiple actions possible at state: ' + state + ', token: ' + symbol);
+                }
+
+                switch (action[0]) {
+
+                    case 1: // shift
+                        //this.shiftCount++;
+
+                        stack.push(symbol);
+                        vstack.push(this.lexer.yytext);
+                        lstack.push(this.lexer.yylloc);
+                        stack.push(action[1]); // push state
+                        symbol = null;
+                        if (!preErrorSymbol) { // normal execution/no error
+                            yyleng = this.lexer.yyleng;
+                            yytext = this.lexer.yytext;
+                            yylineno = this.lexer.yylineno;
+                            yyloc = this.lexer.yylloc;
+                            if (recovering > 0)
+                                recovering--;
+                        } else { // error just occurred, resume old lookahead f/ before error
+                            symbol = preErrorSymbol;
+                            preErrorSymbol = null;
+                        }
+                        break;
+
+                    case 2: // reduce
+                        //this.reductionCount++;
+
+                        len = this.productions_[action[1]][1];
+
+                        // perform semantic action
+                        yyval.$ = vstack[vstack.length - len]; // default to $$ = $1
+                        // default location, uses first token for firsts, last for lasts
+                        yyval._$ = {
+                            first_line: lstack[lstack.length - (len || 1)].first_line,
+                            last_line: lstack[lstack.length - 1].last_line,
+                            first_column: lstack[lstack.length - (len || 1)].first_column,
+                            last_column: lstack[lstack.length - 1].last_column
+                        };
+                        r = this.performAction.call(yyval, yytext, yyleng, yylineno, this.yy, action[1], vstack, lstack);
+
+                        if (typeof r !== 'undefined') {
+                            return r;
+                        }
+
+                        // pop off stack
+                        if (len) {
+                            stack = stack.slice(0, -1 * len * 2);
+                            vstack = vstack.slice(0, -1 * len);
+                            lstack = lstack.slice(0, -1 * len);
+                        }
+
+                        stack.push(this.productions_[action[1]][0]);    // push nonterminal (reduce)
+                        vstack.push(yyval.$);
+                        lstack.push(yyval._$);
+                        // goto new state = table[STATE][NONTERMINAL]
+                        newState = table[stack[stack.length - 2]][stack[stack.length - 1]];
+                        stack.push(newState);
+                        break;
+
+                    case 3: // accept
+                        return true;
+                }
+
+            }
+
+            return true;
+        }
+    };
+    /* Jison generated lexer */
+    var lexer = (function () {
+        var lexer = ({
+            EOF: 1,
+            parseError: function parseError(str, hash) {
+                if (this.yy.parseError) {
+                    this.yy.parseError(str, hash);
+                } else {
+                    throw new Error(str);
+                }
+            },
+            setInput: function (input) {
+                this._input = input;
+                this._more = this._less = this.done = false;
+                this.yylineno = this.yyleng = 0;
+                this.yytext = this.matched = this.match = '';
+                this.conditionStack = ['INITIAL'];
+                this.yylloc = {first_line: 1, first_column: 0, last_line: 1, last_column: 0};
+                return this;
+            },
+            input: function () {
+                var ch = this._input[0];
+                this.yytext += ch;
+                this.yyleng++;
+                this.match += ch;
+                this.matched += ch;
+                var lines = ch.match(/\n/);
+                if (lines) this.yylineno++;
+                this._input = this._input.slice(1);
+                return ch;
+            },
+            unput: function (ch) {
+                this._input = ch + this._input;
+                return this;
+            },
+            more: function () {
+                this._more = true;
+                return this;
+            },
+            less: function (n) {
+                this._input = this.match.slice(n) + this._input;
+            },
+            pastInput: function () {
+                var past = this.matched.substr(0, this.matched.length - this.match.length);
+
+                this._pre = past.slice(past.lastIndexOf('\n')).length - 1;
+                return (past.length > 20 ? '...' : '') + past.substr(-20).replace(/\n/g, "");
+            },
+            upcomingInput: function () {
+                var next = this.match;
+                if (next.length < 20) {
+                    next += this._input.substr(0, 20 - next.length);
+                }
+                var s = (next.substr(0, 20) + (next.length > 20 ? '...' : '')).replace(/\n/g, "");
+                return s;
+            },
+            showPosition: function () {
+                var pre = this.pastInput();
+                var c = new Array(pre.length + 1).join("-");
+                var sW = this.upcomingInput();
+                var s = pre + sW;
+                this._sLine = s;
+                return s + "\n" + c + "^";
+            },
+            next: function () {
+                if (this.done) {
+                    return this.EOF;
+                }
+                if (!this._input) this.done = true;
+
+                var token,
+                    match,
+                    tempMatch,
+                    index,
+                    col,
+                    lines;
+                if (!this._more) {
+                    this.yytext = '';
+                    this.match = '';
+                }
+                var rules = this._currentRules();
+                for (var i = 0; i < rules.length; i++) {
+                    tempMatch = this._input.match(this.rules[rules[i]]);
+                    if (tempMatch && (!match || tempMatch[0].length > match[0].length)) {
+                        match = tempMatch;
+                        index = i;
+                        if (!this.options.flex) break;
+                    }
+                }
+                if (match) {
+                    lines = match[0].match(/\n.*/g);
+                    if (lines) this.yylineno += lines.length;
+                    this.yylloc = {
+                        first_line: this.yylloc.last_line,
+                        last_line: this.yylineno + 1,
+                        first_column: this.yylloc.last_column,
+                        last_column: lines ? lines[lines.length - 1].length - 1 : this.yylloc.last_column + match[0].length
+                    }
+                    this.yytext += match[0];
+                    this.match += match[0];
+                    this.yyleng = this.yytext.length;
+                    this._more = false;
+                    this._input = this._input.slice(match[0].length);
+                    this.matched += match[0];
+                    token = this.performAction.call(this, this.yy, this, rules[index], this.conditionStack[this.conditionStack.length - 1]);
+                    if (this.done && this._input) this.done = false;
+                    if (token) return token;
+                    else return;
+                }
+                if (this._input === "") {
+                    return this.EOF;
+                } else {
+                    this.parseError('Lexical error on line ' + (this.yylineno + 1) + '. Unrecognized text.\n' + this.showPosition(),
+                        {
+                            text: "",
+                            token: null,
+                            lineText: this.lexer._sLine,
+                            line: this.yylineno,
+                            pos: this.lexer._pre
+                        });
+                }
+            },
+            lex: function lex() {
+                var r = this.next();
+                if (typeof r !== 'undefined') {
+                    return r;
+                } else {
+                    return this.lex();
+                }
+            },
+            begin: function begin(condition) {
+                this.conditionStack.push(condition);
+            },
+            popState: function popState() {
+                return this.conditionStack.pop();
+            },
+            _currentRules: function _currentRules() {
+                return this.conditions[this.conditionStack[this.conditionStack.length - 1]].rules;
+            },
+            topState: function () {
+                return this.conditionStack[this.conditionStack.length - 2];
+            },
+            pushState: function begin(condition) {
+                this.begin(condition);
+            }
+        });
+        lexer.options = {};
+        lexer.performAction = function anonymous(yy, yy_, $avoiding_name_collisions, YY_START) {
+
+            var YYSTATE = YY_START
+            switch ($avoiding_name_collisions) {
+                case 0:/* skip whitespace */
+                    break;
+                case 1:
+                    return 6
+                    break;
+                case 2:
+                    yy_.yytext = yy_.yytext.substr(1, yy_.yyleng - 2);
+                    return 4
+                    break;
+                case 3:
+                    return 17
+                    break;
+                case 4:
+                    return 18
+                    break;
+                case 5:
+                    return 23
+                    break;
+                case 6:
+                    return 24
+                    break;
+                case 7:
+                    return 22
+                    break;
+                case 8:
+                    return 21
+                    break;
+                case 9:
+                    return 10
+                    break;
+                case 10:
+                    return 11
+                    break;
+                case 11:
+                    return 8
+                    break;
+                case 12:
+                    return 14
+                    break;
+                case 13:
+                    return 'INVALID'
+                    break;
+            }
+        };
+        lexer.rules = [/^(?:\s+)/, /^(?:(-?([0-9]|[1-9][0-9]+))(\.[0-9]+)?([eE][-+]?[0-9]+)?\b)/, /^(?:"(?:\\[\\"bfnrt/]|\\u[a-fA-F0-9]{4}|[^\\\0-\x09\x0a-\x1f"])*")/, /^(?:\{)/, /^(?:\})/, /^(?:\[)/, /^(?:\])/, /^(?:,)/, /^(?::)/, /^(?:true\b)/, /^(?:false\b)/, /^(?:null\b)/, /^(?:$)/, /^(?:.)/];
+        lexer.conditions = {"INITIAL": {"rules": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13], "inclusive": true}};
+
+        return lexer;
+    })();
+    parser.lexer = lexer;
+    return parser;
+})();
+
+
+/**
+ * json lint entry
+ * @param sJson
+ * @returns {{}}
+ */
+let lintDetect = function (sJson) {
+    let result = {};
+
+    let insertErrorFlag = function (s) {
+        s = s.replace(/^(\s*){([^\s])/, ($0, $1, $2) => ($1 + '{ ' + $2));
+        s = s.replace(/([\s,]+)([^,:\{\}\[\]\s'"]+)(\s*:)/gm, ($0, $1, $2, $3) => ($1 + '"' + $2 + '"' + $3));
+        s = s.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
+        let aLine = s.split('\n');
+        jsonlint.yy.parseError = function (sError, oError) {
+            let theLineNum = oError.line === oError.loc.first_line ? oError.line - 1 : oError.line;
+            let sLine = aLine[theLineNum];
+            result.line = theLineNum;
+            result.col = oError.loc.first_column;
+            aLine[theLineNum] = sLine.slice(0, oError.loc.first_column) +
+                '@◆$#errorEm#$◆@' + sLine.slice(oError.loc.first_column, oError.loc.last_column) + '@◆$#/errorEm#$◆@' +
+                sLine.slice(oError.loc.last_column);
+        };
+
+        try {
+            jsonlint.parse(s);
+        } catch (e) {
+            result["hasError"] = true;
+        }
+        return aLine.join('\n');
+    };
+
+    let escape = function (s) {
+        s = s || '';
+        return s.replace(/\&/g, '&amp;').replace(/\</g, '&lt;').replace(/\>/g, '&gt;').replace(/\"/g, '&quot;').replace(/ /g, '&nbsp;');
+    };
+
+    sJson = insertErrorFlag(sJson);
+    sJson = escape(sJson);
+    let placeholder = '<span class="x-ph"><<<<</span>';
+    sJson = sJson.replace('@◆$#errorEm#$◆@', '<span class="errorEm">').replace('@◆$#/errorEm#$◆@', '</span>' + placeholder);
+    sJson = '<ol><li><div>' + sJson.split('\n').join('</div></li><li><div>') + '</div></li></ol>';
+    result["dom"] = '<div class="line-code">' + sJson + '</div>';
+
+    return result;
+};
+
+window.JsonLint = {
+    lintDetect: lintDetect
+};

+ 97 - 0
apps/loan-rate/index.css

@@ -0,0 +1,97 @@
+@import url("../static/css/bootstrap.min.css");
+.mod-radios {
+    font-size: 14px;
+}
+.mod-value label {
+    margin-right: 0;
+}
+.mod-value .x-input {
+    display: inline-block;
+    width: 400px;
+}
+select.x-select {
+    display: inline-block;
+    width: 114px;
+}
+.radix-tips {
+    margin-left: 160px;
+    font-size: 12px;
+    color: #bbb;
+    font-style: italic;
+}
+
+#containerPayback table th {
+    vertical-align: middle;
+    text-align: center;
+    background-color: #f9f9f9;
+}
+#containerPayback table td {
+    font-weight: normal;
+    font-size: 14px;
+}
+#containerPayback table td.x-name {
+    text-align: center;
+}
+#containerPayback table td.x-bill {
+    color: #000;
+}
+#containerPayback table td.x-left {
+     color: #aaa;
+}
+#containerPayback table tr.x-all,
+#containerPayback table tr.x-last td.x-total {
+    background-color: #ffffe4;
+}
+#containerPayback table tr.x-all td.x-name ,
+#containerPayback table tr.x-all td.x-bill ,
+#containerPayback table tr.x-all td.x-amount ,
+#containerPayback table tr.x-all td.x-interest  {
+    color: #f00;
+}
+.mod-inputs {
+    font-size: 14px;
+}
+.mod-inputs .form-control {
+    width: 120px;
+    display: inline-block;
+    margin:0 10px 10px 0;
+}
+.mod-inputs label {
+    margin-right: 0;
+}
+.mod-inputs .x-sp {
+    margin-left: 50px;
+}
+.mod-inputs span {
+    color: #aaa;
+}
+#containerPayback .x-tips {
+    margin: 10px 0 20px 0;
+}
+#containerPayback .x-btn {
+    margin-left: 20px;
+}
+#containerPayback .x-exchange {
+    float: right;
+    font-size: 12px;
+    color: #48b;
+    text-decoration: underline;
+}
+#containerPayback .x-exchange:hover {
+    cursor: pointer;
+    color: #f00;
+}
+.x-calc-title {
+    border-bottom: 1px solid #ccc;
+    padding-bottom: 5px;
+    margin-bottom: 15px;
+}
+.x-revrate {
+    color: #f00;
+}
+.row.x-tips {
+    font-size: 14px;
+    color: #aaa;
+    font-style: italic;
+    margin-bottom: 40px;
+}

Algunos archivos no se mostraron porque demasiados archivos cambiaron en este cambio