diff --git a/.gitignore b/.gitignore index 9c90724..1df9037 100644 --- a/.gitignore +++ b/.gitignore @@ -51,3 +51,6 @@ Temporary Items *.project +*.temp +*.log +*.cmd \ No newline at end of file diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..6574c45 --- /dev/null +++ b/.npmignore @@ -0,0 +1,21 @@ +# Windows image file caches +Thumbs.db +ehthumbs.db + +# Folder config file +Desktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +.DS_Store +.AppleDouble +.LSOverride + +npm-debug.log +node_modules + +*.project +*.temp +*.log +*.cmd \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 7a795f5..d43ed98 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,179 @@ -# 版本日志 +# amWiki 版本日志 + +## [v1.2.1](https://github.com/TevinLi/amWiki/releases/tag/v1.2.1) +_2017-08-08_ + +- 改善 Mac 上点号开头的系统文件带来的诸多问题(例如导出报错等)([#32](https://github.com/TevinLi/amWiki/issues/32)、[#67](https://github.com/TevinLi/amWiki/issues/67)) +- 允许在 config 上重新定义 library 路径 ([#47](https://github.com/TevinLi/amWiki/issues/47)) +- 文章篇内目录显示优化 ([#33](https://github.com/TevinLi/amWiki/issues/33)) +- 文章篇内目录序号识别,已有序号不再增加序号 ([#33](https://github.com/TevinLi/amWiki/issues/33)) +- 基础 Markdown 语法扩充,实现文字飘红 +- 统一官网和文档中心网址 +- 修复几处显示细节 + +## [v1.2.0](https://github.com/TevinLi/amWiki/releases/tag/v1.2.0) +_2017-07-18_ + +- 新增本地数据挂载模式,可以直接双击 index 在浏览器中访问整个文库 + - Atom 平台和 Node-Npm 平台挂载数据操作绑定 + - 本地页面挂载数据生成与嵌入模块 + - 页面加载数据,页面打开时立即判断和读取挂载数据 + - 改进搜索,网络不可用时不再用 worker 而是直接搜索 + - disable 本地模式状态下的接口测试入口 + - 隐藏本地模式状态下的重建缓存按钮 +- 基础 Markdown 语法扩充,支持 todo list 复选框 ([#56](https://github.com/TevinLi/amWiki/issues/56)) +- 终端本地服务器命令新增可关闭 amWiki 索引页的可选参数 +- 优化本地服务器容错性 +- 搜索面板结果列表点击不再跳转页面 +- 导航增加文档文件名转义,文档文件名允许使用语法符号`()[]'"`而不再冲突 +- 对带`&`符号的文件名在URL地址上用特殊转义替代,不会再被作为参数截断 ([#64](https://github.com/TevinLi/amWiki/issues/64)) +- 修复服务器文件系统为GBK编码时深目录无法读取的bug +- 调整大量注释、调整一些变量名,标记私有与公有 + +## [v1.1.3](https://github.com/TevinLi/amWiki/releases/tag/v1.1.3) +_2017-06-25_ + +- 增加大图预览模式第一期 ([#44](https://github.com/TevinLi/amWiki/issues/44)) +- Markdown 基础语法扩充,支持定义图片大小与对齐方式 ([#45](https://github.com/TevinLi/amWiki/issues/45)) +- 允许首页的文件名通过增加 `home-` 前缀来自定义 +- 修复图片拖拽丢失扩展名的问题 ([#58](https://github.com/TevinLi/amWiki/issues/58)) +- 更改 highlight.js 为完整版,支持 176 种语言语法高亮 +- 修复测试面板切换文档时的一个显示问题 + +## [v1.1.2](https://github.com/TevinLi/amWiki/releases/tag/v1.1.2) +_2017-06-03_ + +- 允许在 config.json 上使用 imports 将额外的 css 和 js 文件嵌入页面 ([#43](https://github.com/TevinLi/amWiki/issues/43)) +- 允许本地 server 多开,端口被占用时累加端口号 ([#46](https://github.com/TevinLi/amWiki/issues/46)) +- 新增本地 server 索引页,本地根 url 不再 404 +- 修复创建时取消创建操作的一个问题 +- 调整一些文案 + +## [v1.1.1](https://github.com/TevinLi/amWiki/releases/tag/v1.1.1) +_2017-05-21_ + +- 紧急修复 win 平台 npm 创建报错的问题 + +## [v1.1.0](https://github.com/TevinLi/amWiki/releases/tag/v1.1.0) +_2017-05-20_ + +- 实现无限级导航 ([#5](https://github.com/TevinLi/amWiki/issues/5)、[#30](https://github.com/TevinLi/amWiki/pull/30)) + - 实现导航水平滚动条,调和水平垂直两滚动条冲突 + - 实现 PC、Mobi 端导航栏无限级梯度的层级显示 + - 实现目录无限级深度树形与列表数据读取 + - 实现无限级导航 Markdown 生成 + - 实现无限级重复的排序 ID 检查 + - 实现无限级图片粘贴、文件拖拽 +- 优化工作端查找 library 文件夹 +- 实现拖拽文件文件名汉字转拼音 +- 修复导航初始化时展开的一个问题 + +--- + +### v1.0.6 +_2017-05-05_ + +- 修复未开启测试模块的文库 web 端无内容显示的问题 +- 修复导航未展开目录时显示不全只有一边的问题 +- 修复上一版本 npm 失误的发布造成 Linux 平台命令无效的问题 +- 移动端点击搜索按钮时关闭导航层 ([#40](https://github.com/TevinLi/amWiki/issues/40)) + +### v1.0.5 +_2017-04-28_ + +- 导航筛选功能增强 + - 文件夹匹配 + - 筛选多个词 + - 忽略筛选词大小写 + - 筛选条件非主动删除不消失 + - 使用递归进行筛选,为无限级做准备 ([#5](https://github.com/TevinLi/amWiki/issues/5)) +- 修复web端在移动端点击导航导航不自动收起的问题 ([#40](https://github.com/TevinLi/amWiki/issues/40)) +- 修复web端测试面板在不关闭的条件下跳转文档后的一个显示问题 +- 菜单栏增加 `从剪切板粘贴截图` 项,调和与其他插件快捷键冲突 ([#41](https://github.com/TevinLi/amWiki/issues/41)) + +### v1.0.4 +_2017-04-22_ + +- 紧急修复命令行本地服务器不能启动的问题 + +### v1.0.3 +_2017-04-22_ + +- 基础 Markdown 语法扩展,支持 footnote 脚注 ([#37](https://github.com/TevinLi/amWiki/issues/37)) +- 修复 Windows 平台下 gitbash 执行命令结束报错的问题 +- 修复 Linux 平台下 `/usr/bin/env` 解释器调用失败的问题 +- 修复 Linux 平台下 create、version 报错的问题 +- 释放快捷键绑定 Ctrl+D、Ctrl+H ([#38](https://github.com/TevinLi/amWiki/issues/38)) +- 取消 readme 单篇长文介绍,仅保留安装方法,详细介绍引导至文档中心 + +### v1.0.2 +_2017-04-04_ + +- 命令行启动服务器允许使用自定义端口参数 +- 截图粘贴增强,QQ聊天、历史记录面板可复制单张图片直接粘入文档 +- 修复命令行板块创建文库操作 $navigation.md 缺失的问题 +- 修复本地 http 服务器 500 页面崩溃的问题 +- 修复 web 端打开带 hash 地址链接时,hash 无效的问题 +- 修复命令行显示版本号报错问题 + +### v1.0.1 +_2017-03-30_ + +- 修复 npm 渠道安装失败的问题 +- readme 添加 npm icon + +### v1.0.0 +_2017-03-30_ + +- [ _祝贺 amWiki 项目一周岁_ ] +- 构建基于 npm 的**命令行板块**,使用命令行可以同样创建维护文库 +- 重构原工作端,分离为**构建板块**与**编辑器板块** +- 引入 ES6 异步编程,使用 Generator、yield、Thunk,将编辑器板块的 confirm 转为 confirm2,并与命令行板块的 confirm2 调用形式统一化 +- 使用 ES6 异步编程,对构建板块部分回调方法进行重构,优先使用 return 与异步编程而不是 callback +- 使用 const/let 变量、解构赋值、箭头函数、Class 等 ES6 语法润色原工作端代码 +- 增加命令行、终端界面信息打印着色、帮助菜单、版本显示 +- 规范化部分公共方法注释 + +--- + +### v0.8.0 +_2016-12-30_ + +- Web 端页面左右分栏滚动 ([#26](https://github.com/TevinLi/amWiki/issues/26)) +- 工作端解除部分判断对 F12 造成的限制 ([#27](https://github.com/TevinLi/amWiki/issues/27)) + +--- + +### v0.7.7 +_2016-12-19_ + +- 全文库搜索,增加大小写忽略 ([#24](https://github.com/TevinLi/amWiki/issues/24)) +- 修复导出后,在 github wiki 页内目录无法跳转的问题 ([#20](https://github.com/TevinLi/amWiki/issues/20#issuecomment-266927563)) +- 修复导出时,html 格式的 img src 相对地址未转换问题 ([#23](https://github.com/TevinLi/amWiki/issues/23)) +- 修复导出时,链接与 a 标签相对地址未转换问题 ([#23](https://github.com/TevinLi/amWiki/issues/23)) + +### v0.7.6 +_2016-12-11_ + +- 增加文库导出模块,可以将文库导出为 Github Wiki 项目 (**[#20](https://github.com/TevinLi/amWiki/issues/20)**) +- 允许 `.md` 文件文件名带空格 ([#19](https://github.com/TevinLi/amWiki/issues/19)) +- Web 移动端点击页内目录以外的区域将折叠目录 ([#22](https://github.com/TevinLi/amWiki/issues/22)) +- 修复某些情况下跳页面不返回顶部的问题 ([#21](https://github.com/TevinLi/amWiki/issues/21)) + +### v0.7.5 +_2016-12-03_ +(none) + +### v0.7.4 +_2016-12-03_ + +- 增加 Web 端右侧目录悬浮窗 ([#15](https://github.com/TevinLi/amWiki/issues/15)) +- 增加对 `[TOC]` 目录生成标签的支持 ([#15](https://github.com/TevinLi/amWiki/issues/15)) +- 修复工作端抽取页内目录含单双引号、圆括号时无法正确解析的问题 ([#16](https://github.com/TevinLi/amWiki/pull/16)) +- 修复工作端抽取页内目录无法跳转的问题 ([#15](https://github.com/TevinLi/amWiki/issues/15)) +- 修复工作端抽取页内目录时内容为空报错的问题 ([#13](https://github.com/TevinLi/amWiki/issues/13), [#14](https://github.com/TevinLi/amWiki/pull/14)) +- 修复 Web 端浏览器后退按钮报错问题 +- Atom 菜单栏添加 “关于amWiki” 按钮 ### v0.7.3 _2016-10-28_ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2fa2540..b14ba02 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,4 +1,4 @@ # amWiki 贡献指引 - 如果您是 coder,您的所有 issues 或 pull requests,请在[Github项目托管地址](https://github.com/TevinLi/amWiki)提交 -- 如果您是非技术人员,需要提问或建议,请加QQ群347125653 \ No newline at end of file +- 如果您需要直接提问或建议,请加QQ群347125653 \ No newline at end of file diff --git a/README.md b/README.md index 421767c..51076ed 100644 --- a/README.md +++ b/README.md @@ -1,133 +1,69 @@ # amWiki 轻文库 ![amWiki logo](https://amwiki.xf09.net/docs/assets/logo.png) -amWiki 是一款基于 Javascript 脚本语言、依赖 Atom 编辑器、使用 Markdown 标记语法的轻量级开源 wiki 文库系统。 -amWiki 致力于让大家可以更简单、更便捷的建设个人和团队文库系统! +amWiki 是一款由 JS 开发、依赖 Atom 或 Nodejs-Npm 的 Markdown 轻量级前端化开源文库系统。 +amWiki 致力于让大家可以更简单、更便捷的建设个人和团队文库! -**传送门:** -[amWiki 项目 Github 托管地址](https://github.com/TevinLi/amWiki "amWiki项目Github托管地址") -[![amWiki github star](https://amwiki.xf09.net/githubicons/Tevinli/amWiki/star) ![amWiki github fork](https://amwiki.xf09.net/githubicons/Tevinli/amWiki/fork)](https://github.com/TevinLi/amWiki) +[[fork amWiki on Github](https://github.com/TevinLi/amWiki)] -### amWiki优势 +GitHub: +[![](https://img.shields.io/github/stars/TevinLi/amWiki.svg?style=social&label=Star)](https://github.com/TevinLi/amWiki "GitHub Stars") [![](https://img.shields.io/github/forks/TevinLi/amWiki.svg?style=social&label=Fork)](https://github.com/TevinLi/amWiki "GitHub Forks") [![](https://img.shields.io/github/issues-raw/TevinLi/amWiki.svg)](https://github.com/TevinLi/amWiki "GitHub Open Issues") [![](https://img.shields.io/github/issues-closed-raw/TevinLi/amWiki.svg)](https://github.com/TevinLi/amWiki "GitHub Closed Issues") +[![GitHub Tag](https://img.shields.io/github/tag/TevinLi/amWiki.svg)](https://github.com/TevinLi/amWiki "GitHub Tag") [![apm](https://img.shields.io/github/license/TevinLi/amWiki.svg)](https://atom.io/packages/amWiki "MIT License") +APM: +[![APM Version](https://img.shields.io/apm/v/amWiki.svg)](https://atom.io/packages/amWiki "APM Version") [![APM Downloads](https://img.shields.io/apm/dm/amWiki.svg)](https://atom.io/packages/amWiki "APM Downloads") +NPM: +[![NPM Version](https://img.shields.io/npm/v/amwiki.svg)](https://www.npmjs.com/package/amwiki "NPM Version") [![APM Downloads](https://img.shields.io/npm/dt/amwiki.svg)](https://www.npmjs.com/package/amwiki "APM Downloads") [![npm](https://img.shields.io/npm/dm/amwiki.svg)](https://www.npmjs.com/package/amwiki) + +
+ +## amWiki 的优势 - 文档系统采用 markdown 语法 - 不用数据库,文档使用 `.md` 格式保存本地文件 - 无需服务端开发,只需支持 http 静态访问网页空间 - 一键创建新的文库 - 自动更新文库导航目录 +- 支持多级目录 - 支持截图直接粘帖为本地 png 并插入当前 markdown - 文档web端自适应显示,适合所有平台 - 支持接口文档自动抓取内容生成简单的ajax测试 +- 无需服务端的全文库标题内容搜索与计分排序 - ... (更多内容期待您的发现) -### web端效果演示 -一键创建新文库将自动生成一套Web端html页面,默认生成效果一览:[https://tevinli.github.io/amWiki/](https://tevinli.github.io/amWiki/index.html) +
-## 如何开始 +## 安装 amWiki +amWiki 可以同时在 Atom 编辑器和 nodejs npm 的命令行两个平台工作,两个平台的工作相互独立,但所创建的文库却可以相互共用 +(PS:对这两个平台的依赖都是编辑需求而不是服务器需求,amWiki 创建的文库是纯静态 html,可以布置到任意服务器) -1. 下载Github开源文本编辑器 [Atom 官网](https://atom.io/ "Atom官网"),并安装 +### 作为 Atom 插件安装 +1. 下载 Github 开源文本编辑器 [Atom](https://atom.io/ "Atom官网"),并安装 +2. 安装 Atom 插件 amWiki,并在完成后重启 Atom + - 前往 Github 的 [amWiki版本发布页](https://github.com/TevinLi/amWiki/releases) 下载最新版压缩包,解压到 C:\Users\Administrator\.atom\packages,并将文件夹名 `amWiki-1.x.x` 改为 `amWiki` + - 或者,Atom 菜单,File -> Setting -> Install -> 搜索 `amWiki` -> 找到 amWiki 并 Install + - 或者,在 cmd 或终端中命令:`apm install amWiki`(_第三字母 W 大写_) +3. 在 Atom 菜单,File -> `Add Project Folder` 添加一个项目文件夹 +4. 在此文件夹下创建一个名为 `config.json` 的文件 +5. 在 Atom 菜单,amWiki轻文库 -> 通过“config.json”创建新文库 -2. 安装Atom插件amWiki,并重启Atom - - Atom菜单,File -> Setting -> Install -> 搜索`amWiki` - - 或者,运行cmd:`apm install amWiki` - - 或者,从Github的 [amWiki版本发布](https://github.com/TevinLi/amWiki/releases) 下载zip,解压到`C:\Users\Administrator\.atom\packages`,并将文件夹名`amWiki-master`改为`amWiki` +### 作为 nodejs 全局模块安装 +1. 下载 [nodejs](https://nodejs.org/) 并安装 +2. 执行命令: `npm install -g amwiki`(_第三字母 w 小写_) +3. cd 到某个文件夹,通过命令 `amwiki create` 创建文库 +4. 通过命令 `amwiki help` 查看帮助 -3. (在本地服务器静态目录)创建一个文件夹 +
-4. 在Atom中 **添加项目文件夹**,并指向刚创建的文件夹 +## amWiki 教程 & 文档 +更多详细功能与使用介绍请前往: [ [amWiki 官网文档中心:amwiki.org/doc/](http://amwiki.org/doc/) ] +(amWiki 的文档由项目自身创建与维护) -5. 在Atom刚创建的项目下新建`config.json`文件,并输入一下内容: - ```javascript - { - "name": "", //您的文库名称,设置但为空或不设置将显示默认名 - "ver": "", //文本版本号或维护者名号,设置但为空将不显示,注意诺不设置此属性将显示amWiki作者 - "logo": "", //logo的url,设置但为空或不设置将显示默认logo - "testing": true, //是否启用接口测试,默认值false - "colour": "" //自定义颜色,默认为蓝色 - } - ``` -6. 点击Atom菜单: - - Packages -> amWiki文库 -> 通过“config.json”创建新文库 - -7. 此时项目中自动创建了许多内容,其中 library 文件夹即为您的文档库,编辑您的文库文档 - -8. 使用 F12 启动本地静态服务器,访问刚刚自动创建的 index.html - - -## 目录结构 -自动生创建的内容目录如下 - - index.html //http访问首页 - amWiki/ //amWiki工作文件目录 - library/ //您的markdown文库目录,所有文件必须使用.md格式 - ├ $navigation.md //amWiki文库目录导航文件,可自动/手动更新 - ├ 首页.md //内容区默认显示内容 - ├ 01-关于amWiki文库/ //markdown文件夹01 - │ ├ 001-关于amWiki //一些markdown文档,支持二级目录 - │ └ 002-... - ├ 02-学习markdown/ //markdown文件夹02 - │ ├ 001-md入门 //一些markdown文档,支持二级目录 - │ └ 002-... - └ 03-... //更多markdown文件夹 - (assetes/) //如果您粘帖截图,图片文件将自动创建在此处 - - -## 如何编辑 -1. 新建或更改文件夹和文档名,组织您自己的文档结构 (文档必须markdown文档、**使用.md扩展名**) - -2. 编辑markdown文档,制作您自己的文档内容 - -3. 每个文件夹或文件使用“id-名称”来实现命名排序,请勿删除id,删除后将无法正常工作 - id仅允许整数或浮点数类型,且使用连接符或下划线与具体名称组成命名 - -4. library文件夹下`首页.md`为打开时默认的或url参数错误时的显示内容 - -5. `$navigation.md`导航文件无需人工维护,创建新文件夹或文件时将自动更新,也可以在菜单栏手动刷新: - - 菜单栏 -> amWiki文库 -> 手动更新当前文库导航文件 - -6. 如果需要在markdown文档中插入图片,**请先截图**,然后在.md文档对应位置使用快捷键:`Ctrl + Shift + V` - -7. 对于较长文章,可以使用页内目录,依次进行如下操作,或使用快捷键`Ctrl + D`,即可在光标处插入页内目录。 - 菜单栏 -> amWiki文库 -> 提取h2、h3标题为页内目录 -注意:请按顺序使用h1、h2、h3,且h1仅使用一次。 - -8. 如果启用了测试,对于文档中**同时存在“请求地址”“请求类型”“请求参数”三个h3标题**的文档,自动在右上角激活接口测试功能 - - 请求参数的表格,请按`参数名、类型、必填、描述、默认值`的顺序建立表格,否则不能正常抓取 - - 只能请求同域接口,不能跨域 - - 如果接口需要登录权限,请先登录您自己的系统 - - -9. 本插件升级后,您想更新`项目/amWiki/`文件夹下web端的工作文件,您只需重新打开`config.json`文件,然后在Atom菜单上选择`通过“config.json”创建新文库`即可。 -这个二次创建操作不会影响您 library 与 assetes 文件夹下的内容。 - -10. 借助版本管理 SVN、Git、Hg,传输协议FTP/SFTP,文件同步Dropbox、百度云等等工具,便捷实现网络访问。 - - -## 如何使用 - -1. 使用 http 访问项目的 index.html。 - -2. PC端使用左侧导航栏、移动端使用右上角导航弹出菜单切换页面。 - -3. 在导航栏顶部,可以使用筛选功能整个导航目录进行筛选。 - -4. 如果存在页内目录,直接点击,页内目录使用hash滚动;同时您可以直接带hash分享,以方便他人快速浏览指定内容。 - -5. 如果页面激活了接口测试功能,点击右上角“测试接口”打开测试面板,输出参数并点击“发送Ajax”,即可看到响应内容。 - - -## 后记 -这个项目的初始灵感来自 MDwiki,这是一个使用 html5/js 纯静态的 markdown wiki 系统。 -一开始,我也准备使用它来写一些文档,但是我很快发现,如果我新增一篇文档,需要我手动去编辑导航栏文件增加一条导航,这个体验并不友好。然后想写一套类似 [微信公众平台开发者文档](http://mp.weixin.qq.com/wiki/home/index.html) 这样较大的文档库,发现 MDwiki 由于其容量所限,并不能很好的胜任。 -于是经过一番折腾,就有了amWiki。 -个人开源不易,欢迎来github上star支持一下,如果发现问题或好的建议,请尽量在github上发issues,不胜感谢! +
## 更新日志 ### 如何获取最新版本信息与动态? -- 欢迎添加作者微信为好友,微信号 **XF90_Net** - _(大家加微信的时候都尽量备注一下原因吧,没有备注的不加哦!目前主要集中精力在开发,暂不接受各种调戏跟闲聊~)_ -- 其他途径: QQ群`347125653` / [微博](http://weibo.com/1706602953/profile?is_all=1) / [Twitter](https://twitter.com/tevinli2009) +- QQ群 **347125653** +- 如果可以,欢迎直接加我微信 + ![](http://img.amwiki.org/global/wx.qrcode.150.png) ### 如何查看历史版本日志? [amWiki 版本更新日志](https://github.com/TevinLi/amWiki/blob/master/CHANGELOG.md "amWiki版本更新日志") diff --git a/SCHEDULE.md b/SCHEDULE.md new file mode 100644 index 0000000..2f53980 --- /dev/null +++ b/SCHEDULE.md @@ -0,0 +1,15 @@ +# amWiki 开发计划表 + +## v1.2.1 + +- 增加修改 web 端 library 目录路径的配置项 #47 +- markdown 基础语法扩展,允许使用高亮标签对文字飘红 +- 目录生成优化,识别标题是否已经带有序号 #33 + +## v2.0.0 _(under way)_ + +- 重新设计 Web 端架构,大幅强化可扩展性 +  - 设计可扩展的管理、通讯、调用机制 +  - 设计 Web 端插件模块机制 +  - 设计 Web 端自定义主题模块机制 +  - 分离原有功能为独立模块 diff --git a/bin/main.js b/bin/main.js new file mode 100644 index 0000000..c23cfe2 --- /dev/null +++ b/bin/main.js @@ -0,0 +1,186 @@ +#!/usr/bin/env node + +/** + * 工作端 - 终端(命令行) - 入口模块 + */ + +const fs = require('fs'); + +//获取命令内容 +let [nodePath, mainPath, command, ...parameters] = process.argv; +//转换 linux 下软链接路径为真实路径 +mainPath = fs.realpathSync(mainPath); + +// co 模块,异步函数编程 +const co = require('../modules/co'); + +//在全局变量上绑定 alert、confirm2、prompt2 方法,作为命令行输出与交互 +({alert: global.alert, confirm2: global.confirm2, prompt2: global.prompt2} = require('./messageBox')); + +// wiki 创建器 +const creator = require('../build/creator'); +//文库管理 +const mngWiki = require('../build/manageWiki'); +//文件夹管理 +const mngFolder = require('../build/manageFolder'); +//手动刷新导航文件 +const makeNav = require('../build/makeNavigation'); +//手动刷新页面挂载数据 +const makeMut = require('../build/makeMounts'); +//本地服务器模块 +const localServer = require('../build/localServer'); +//项目导出模块 +const exportGithub = require('../build/exportGithub'); + +//格式化显示 +const printFn = require('./printf'); + +//读取项目包配置 +const packageConf = JSON.parse(fs.readFileSync(mainPath.split(/bin[\\\/]main/)[0] + 'package.json', 'utf-8')); + +//项目根目录 +const root = mngFolder.isAmWiki(process.cwd()); +const wikis = {}; + +//注册文库 +mngWiki.linkWikis(wikis); +if (root) { + mngWiki.addWiki(root); +} + +co(function*() { + + //执行命令 + switch (command) { + //依据引导创建 wiki + case 'init': + break; + //依据 config.json 创建 wiki + case 'create': + case '-c': + // config.json 文件路径 + const configPath = process.cwd().replace(/\\/g, '/') + '/config.json'; + //项目 files 文件夹路径 + const filesPath = mainPath.replace(/\\/g, '/').split('bin')[0] + 'files/'; + //开始创建 + const root2 = yield creator.create(configPath, filesPath, packageConf); + if (!root2) { + break; + } + //更新导航 + makeNav.refresh(root2 + 'library/'); + makeMut.make(root, true); + break; + //更新 wiki + case 'update': + case '-u': + if (!root) { + console.error('非 amWiki 项目文件夹,无法更新导航!'); + break; + } + const type = parameters[0]; + //更新导航 + if (type === 'nav') { + makeNav.refresh(root); + } + //更新文库嵌入数据 + else if (type === 'mut') { + makeMut.make(root); + } + //更新 SEO 模块 + else if (type === 'seo') { + } + //完整更新 + else if (typeof type === 'undefined') { + makeNav.refresh(root); + makeMut.make(root, true); + } + break; + //启动本地服务器 + case 'server': + case '-s': + if (!root) { + console.error('非 amWiki 项目文件夹,无法启动服务器!'); + break; + } + const port = parameters[0]; + const noIndex = parameters[1] || ''; + //有输入端口 + if (typeof port !== 'undefined') { + //端口合法 + if (/^\d+$/.test(port)) { + yield localServer.run(wikis, port); + if (noIndex === 'no-index') { + localServer.setOffIndex(); + } + } + //端口写成 noIndex + else { + yield localServer.run(wikis); + if (port === 'no-index' || noIndex === 'no-index') { + localServer.setOffIndex(); + } + } + } + //没输入端口号 + else { + yield localServer.run(wikis); + } + break; + //本地浏览文档 + case 'browser': + case '-b': + if (!root) { + console.error('非 amWiki 项目文件夹,无法浏览文库!'); + break; + } + const fileId = parameters[0]; + //未给出需要浏览的文档id + if (typeof fileId === 'undefined') { + yield localServer.browser(root + 'library/', wikis); + } + //有给出需要浏览的文档id + else { + } + break; + //导出 wiki + case 'export': + case '-e': + if (!root) { + console.error('非amWiki项目文件夹,无法导出!'); + break; + } + if (parameters[0] !== 'github-wiki') { + console.error('除 github-wiki 外,目前不支持其他类型导出!'); + break; + } + let outPath = parameters[1]; + if (typeof outPath === 'undefined') { + outPath = yield prompt2('请输入导出文件夹:'); + } + yield exportGithub.export(root, outPath.replace(/\\/g, '/')); + break; + //显示版本号 + case 'version': + case '-v': + printFn.ver(packageConf); + break; + //显示帮助 + case 'help': + case '-h': + default: + printFn.help(); + break; + } + + //关闭用户输入 + process.stdin.on('error', (e) => { + if (e.code !== 'EPIPE' || e.syscall !== 'shutdown') { + throw e; + } + }); + process.stdin.end(); + +}).catch((e) => { + console.error(e); +}); diff --git a/bin/messageBox.js b/bin/messageBox.js new file mode 100644 index 0000000..4056e7f --- /dev/null +++ b/bin/messageBox.js @@ -0,0 +1,85 @@ +/** + * 工作端 - 终端(命令行) - 模拟浏览器 alert、confirm、prompt 方法进行信息交互 + * @author Tevin + * + * @example + * co(function* () { + * const c = yield confirm2('测试confirm2'); + * console.log(c); + * const p = yield prompt2('测试prompt2'); + * console.log(p); + * process.stdin.end(); + * }); + * + */ + +/** + * 模拟消息框 + * @param {String} msg + */ +const alert = (msg) => { + msg = typeof msg !== 'string' ? String(msg) : msg; + process.stdout.write(msg + '\n') +}; + +/** + * 模拟确认框,回调传参为用户选择的布尔值 + * @param {String} msg + * @returns {Promise} + */ +const confirm2 = (msg) => { + return new Promise((resolve, reject) => { + msg = typeof msg !== 'string' ? String(msg) : msg; + let readStdin = () => { + let chunk = process.stdin.read(); + if (chunk !== null) { + chunk = chunk.replace(/[\n\r\u0004]*$/, ''); + if (/^(y|yes|ok)$/i.test(chunk)) { + resolve(true); + } else { + resolve(false); + } + readStdin = null; + } + }; + process.stdout.write(msg + '(y/n) '); + process.stdin.setEncoding('utf8'); + process.stdin.on('readable', () => { + readStdin && readStdin(); + }); + }); +}; + +/** + * 模拟输入框,回调传参为用户输入的字符串 + * @param {String} msg + * @returns {Promise} + */ +const prompt2 = (msg) => { + return new Promise((resolve, reject) => { + msg = typeof msg !== 'string' ? String(msg) : msg; + let readStdin = () => { + let chunk = process.stdin.read(); + if (chunk !== null) { + chunk = chunk.replace(/[\n\r\u0004]*$/, ''); + resolve(chunk); + readStdin = null; + } + }; + process.stdout.write(msg + ' '); + process.stdin.setEncoding('utf8'); + process.stdin.on('readable', () => { + readStdin && readStdin(); + }); + }); +}; + +const messageBox = (function () { + return { + alert: alert, + confirm2: confirm2, + prompt2: prompt2 + }; +})(); + +module.exports = messageBox; \ No newline at end of file diff --git a/bin/printf.js b/bin/printf.js new file mode 100644 index 0000000..249eb4a --- /dev/null +++ b/bin/printf.js @@ -0,0 +1,140 @@ +/** + * 工作端 - 终端(命令行) - 着色打印 + * @author Tevin + */ + +const fs = require('fs'); + +//命令行彩色输出简化,command line color +const clc = (colorId, text) => `\x1b[${colorId}m${text}\x1b[0m`; + +//文件与文件夹创建操作着色打印 +const {mkdir, mkdirSync, writeFile, writeFileSync} = fs; +fs.mkdir = (path, mode, callback) => { + alert(` ${clc(36, 'create')} ${path}`); + return mkdir(path, mode, callback); +}; +fs.mkdirSync = (path, mode) => { + alert(` ${clc(36, 'create')} ${path}`); + return mkdirSync(path, mode); +}; +fs.writeFile = (path, data, options, callback) => { + alert(` ${clc(36, 'create')} ${path}`); + return writeFile(path, data, options, callback); +}; +fs.writeFileSync = (path, data, options) => { + alert(` ${clc(36, 'create')} ${path}`); + return writeFileSync(path, data, options); +}; + +//文件与文件夹删除操作着色打印 +const {rmdir, rmdirSync, unlink, unlinkSync} = fs; +fs.rmdir = (path, callback) => { + alert(` ${clc(31, 'delete')} ${path}`); + return rmdir(path, callback); +}; +fs.rmdirSync = (path) => { + alert(` ${clc(31, 'delete')} ${path}`); + return rmdirSync(path); +}; +fs.unlink = (path, callback) => { + alert(` ${clc(31, 'delete')} ${path}`); + return unlink(path, callback); +}; +fs.unlinkSync = (path) => { + alert(` ${clc(31, 'delete')} ${path}`); + return unlinkSync(path); +}; + +//终端输出操作着色打印 +const {info, warn, error} = console; +console.info = (...content) => { + return console.log(`${clc(46, 'INFO:')}`, ...content); +}; +console.warn = (...content) => { + return console.log(`${clc(43, 'WARN:')}`, ...content); +}; +console.error = (...content) => { + return console.log(`${clc(41, 'ERROR:')}`, ...content); +}; + +//帮助文本 +const helpWords = ` +用法: + amwiki [command] [arguments] + +命令列表: + -h, help 显示本帮助 + -v, version 显示版本号 + -c, create 创建 wiki + -u, update 更新 wiki 数据,默认根据文库配置选择适当内容更新 + 可选参数: + [nav|mut] + 指定仅更新导航 + 指定仅更新页面挂载数据 + ${clc(90, '(eg: amwik update nav)')} + -s, server 启动本地服务器,默认本地服务器使用端口号 5171 + 可选参数: + [8080] 重新指定端口号 + [no-index] 关闭 amwiki 索引页 + ${clc(90, '(eg: amwiki server 8080 no-index)')} + -b, browser 本地浏览文档,默认访问文库首页 + 可选参数: + [002/001] 按目录文档的id打开指定文档(待开发) + ${clc(90, '(eg: amwiki browser 002/001)')} + -e, export amwiki 项目导出 + 必需参数: + github-wiki 指定导出类型为 github 项目附属 wiki + d:\/your-path 指定本次导出的本地输出路径 + ${clc(90, '(eg: amwiki export github-wiki d:\/your-path)')} + +更多文档: + http://amwiki.org/doc/ +`; +//[seo] 仅更新SEO模块(待开发) + +//amWiki logo +const vesWords = ` + kk kk kk kk ww + wwwwww www www kk kk kk + ww ww ww ww kk kk kk kk kk kk kk + wwwwwww ww ww ww kk kkkk kk kk kk kk kk +ww ww ww ww ww kk kk kk kk kk kkkk kk + wwwwwww ww ww ww kk kk kk kk kk kk + +`.replace(/w/g, `${clc(32, '$')}`).replace(/k/g, `${clc(36, '$')}`); + +const printf = (function () { + return { + /** + * 命令行彩色输出 + * @public + */ + clc: clc, + /** + * 显示帮助信息 + * @public + */ + help: function () { + alert(helpWords); + }, + /** + * 显示版本信息 + * @param {Object} packageConf + * @public + */ + ver: function (packageConf) { + const text = [ + 'Package: amWiki', + `Version: ${clc(0, packageConf.version)}`, + 'Author: Tevin Li', + 'HomePage: amwiki.org', + 'Fork: github.com/tevinli/amWiki', + 'License: MIT' + ]; + alert(vesWords + text.join('\n') + '\n'); + } + }; +})(); + +module.exports = printf; \ No newline at end of file diff --git a/build/creator.js b/build/creator.js new file mode 100644 index 0000000..0e42c3e --- /dev/null +++ b/build/creator.js @@ -0,0 +1,333 @@ +/** + * 工作端 - 输出 web 静态资源模块 + * @author Tevin + */ + +const fs = require('fs'); +const co = require('../modules/co'); +const mngWiki = require('./manageWiki'); +const mngFolder = require('./manageFolder'); +const makeNav = require('./makeNavigation'); + +const creator = (function () { + return { + /** + * 复制文件 + * @param {String} from + * @param {String} to + * @private + */ + _copyFile: function (from, to) { + fs.createReadStream(from).pipe(fs.createWriteStream(to)); + }, + /** + * 创建 amWiki 必要的文件夹 + * @param {String} outputPath + * @returns {Boolean} + * @private + */ + _createWikiFolder: function (outputPath) { + if (!fs.existsSync(outputPath + 'amWiki/')) { + fs.mkdirSync(outputPath + 'amWiki/', 0o777); + } + if (!fs.existsSync(outputPath + 'amWiki/js/')) { + fs.mkdirSync(outputPath + 'amWiki/js/', 0o777); + } + if (!fs.existsSync(outputPath + 'amWiki/css/')) { + fs.mkdirSync(outputPath + 'amWiki/css/', 0o777); + } + if (!fs.existsSync(outputPath + 'amWiki/images/')) { + fs.mkdirSync(outputPath + 'amWiki/images/', 0o777); + } + if (!fs.existsSync(outputPath + 'library/')) { + fs.mkdirSync(outputPath + 'library/', 0o777); + if (!fs.existsSync(outputPath + 'library/001-学习amWiki/')) { + fs.mkdirSync(outputPath + 'library/001-学习amWiki/', 0o777); + } + if (!fs.existsSync(outputPath + 'library/001-学习amWiki/05-学习markdown/')) { + fs.mkdirSync(outputPath + 'library/001-学习amWiki/05-学习markdown/', 0o777); + } + if (!fs.existsSync(outputPath + 'library/002-文档示范/')) { + fs.mkdirSync(outputPath + 'library/002-文档示范/', 0o777); + } + return false; + } + return true; + }, + /** + * 创建着色色系 + * (ps: 颜色变亮变暗不是 rgb 分别同时增加或减少一个值) + * @param {String} color + * @returns {{base: String, light: String, dark: String}} - 生成的浅、中、深三色 + * @private + */ + _clacWikiColour: function (color) { + const colours = {}; + //修复16进制下的1位数 + const lessThan = (str) => { + if (str <= 9) { + str = '0' + str; + } else { + const num = parseInt('0x' + str); + if (num >= 10 && num <= 15) { + str = '0' + str; + } + } + return str; + }; + //颜色合法检查 + const atBinary16 = /^#([0-9a-fA-F]{3}){1,2}$/.test(color); + const atRGB = /^[rR][gG][bB]([aA])?\(\d{1,3},\d{1,3},\d{1,3}(,.*?)?\)$/.test(color); + const atList = /(blue|default)/.test(color); + if (!atBinary16 && !atRGB && !atList) { + alert('请使用标准16进制颜色、标准RGB颜色、2种特定颜色名指定颜色!本次创建使用默认颜色'); + return this._clacWikiColour('default'); + } + //颜色名称转色值 + const colorList = { + default: '#4296eb', + blue: '#4296eb' + }; + if (atList) { + color = colorList[color]; + } + //解析颜色为rgb + const rgb = []; + if (color.indexOf('#') === 0) { + rgb[0] = parseInt('0x' + color.substr(1, 2)); + rgb[1] = parseInt('0x' + color.substr(3, 2)); + rgb[2] = parseInt('0x' + color.substr(5, 2)); + } else { + const rgbStr = color.split('(')[1].split(')')[0].split(','); + rgb[0] = parseInt(rgbStr[0]); + rgb[1] = parseInt(rgbStr[1]); + rgb[2] = parseInt(rgbStr[2]); + } + //生成主色base + colours.base = '#' + [ + lessThan(rgb[0].toString(16)), + lessThan(rgb[1].toString(16)), + lessThan(rgb[2].toString(16)) + ].join(''); + //颜色变化量,越小的值变化越大 + const increment = [ + Math.round((255 - rgb[0]) / 3), + Math.round((255 - rgb[1]) / 3), + Math.round((255 - rgb[2]) / 3) + ]; + //生成亮色light + colours.light = '#' + [ + (rgb[0] + increment[0]).toString(16), + (rgb[1] + increment[1]).toString(16), + (rgb[2] + increment[2]).toString(16) + ].join(''); + //生成暗色dark + let larger = 0, + largeIndex = -1; + const largerFd = (i) => { + if (increment[i] > larger) { + larger = increment[i]; + largeIndex = i; + } + }; + largerFd(0); + largerFd(1); + largerFd(2); + if (rgb[largeIndex] - larger < 0) { + //修复加深颜色值为负的情况,最多减到0,其他三色等比例减小 + const reduce = (rgb[largeIndex] - larger) / larger; + increment[0] += Math.round(increment[0] * reduce); + increment[1] += Math.round(increment[1] * reduce); + increment[2] += Math.round(increment[2] * reduce); + } + colours.dark = '#' + [ + lessThan((rgb[0] - increment[0]).toString(16)), + lessThan((rgb[1] - increment[1]).toString(16)), + lessThan((rgb[2] - increment[2]).toString(16)) + ].join(''); + return colours; + }, + /** + * 配置检查 + * @param {String} configPath + * @param {String} filesPath + * @returns {Promise} + * @private + */ + _checkConfig: function (configPath, filesPath) { + const that = this; + return co(function*() { + const options = {}; + //创建路径 + options.filesPath = filesPath; + options.outputPath = configPath.split('config.json')[0].replace(/\\/g, '/'); + //读取配置 + let configStr = ''; + let noConfig = false; + if (fs.existsSync(configPath)) { + configStr = fs.readFileSync(configPath, 'utf-8') || ''; + } else { + noConfig = true; + } + if (configStr.length === 0) { + if (yield confirm2('没有读取到任何配置,继续创建么?')) { + configStr = '{}'; + if (noConfig) { + fs.writeFileSync(configPath, configStr, 'utf-8'); + } + } else { + return; + } + } + //解析默认配置 + let parseOk = true, + config = null; + try { + config = JSON.parse(configStr); + } catch (e) { + alert('配置解析失败,请检查您的 config.json 是否有严格按 Json 格式书写!\n错误消息:' + e.message); + parseOk = false; + } + if (!parseOk) { + return; + } + return { + options: options, + config: mngWiki.parseConfig(config) + }; + }).catch((e) => { + console.error(e); + }); + }, + /** + * 创建amWiki本地文件 + * @param {String} configPath - config.json文件的路径 + * @param {String} filesPath - 项目包files文件夹路径 + * @returns {Promise} 项目根目录 + * @public + */ + create: function (configPath, filesPath) { + const that = this; + return co(function*() { + const {options, config} = yield that._checkConfig(configPath, filesPath); + //开始创建 + const files = mngFolder.readFolder(options.outputPath); + if (files.length > 1) { + if (!(yield confirm2('此处已有一些文件或文件夹,是否仍然在此创建amWiki?'))) { + return false; + } + } + //创建index.html + let indexPage = fs.readFileSync(options.filesPath + 'amWiki.tpl', 'utf-8'); + indexPage = indexPage.replace(/\{\{name\}\}/g, config.name) + .replace('{{version}}', config.version) + .replace('{{logo}}', config.logo); + //嵌入配置 + indexPage = indexPage.replace('{{config}}', 'AWConfig=' + JSON.stringify(config)); + //测试模块 + if (config.testing) { + const testingTpl = fs.readFileSync(options.filesPath + 'amWiki.testing.tpl', 'utf-8'); + const testingScript = ''; + indexPage = indexPage + .replace('{{amWiki.testing.tpl}}', testingTpl) + .replace('{{amWiki.testing.js}}', testingScript); + } else { + indexPage = indexPage + .replace('{{amWiki.testing.tpl}}', '') + .replace('{{amWiki.testing.js}}', ''); + } + //自定义 css、js 嵌入 + if (config.imports) { + let item, + cssList = '', + jsList = ''; + for (item of config.imports.css) { + cssList = '' + } + for (item of config.imports.js) { + jsList = '' + } + indexPage = indexPage + .replace('{{custom.css}}', cssList) + .replace('{{custom.js}}', jsList); + } else { + indexPage = indexPage + .replace('{{custom.css}}', '') + .replace('{{custom.js}}', ''); + } + fs.writeFileSync(options.outputPath + 'index.html', indexPage, 'utf-8'); + //创建文件夹 + const hasLibrary = that._createWikiFolder(options.outputPath); + //创建amWiki.css + let wikiCss = fs.readFileSync(options.filesPath + 'amWiki.css', 'utf-8'); + if (config.testing) { + wikiCss += fs.readFileSync(options.filesPath + 'amWiki.testing.css', 'utf-8'); + } + wikiCss += fs.readFileSync(options.filesPath + 'amWiki.print.css', 'utf-8'); + wikiCss += fs.readFileSync(options.filesPath + 'amWiki.search.css', 'utf-8'); + let colors = that._clacWikiColour(config.colour); + wikiCss = wikiCss + .replace(/@colour-base/g, colors.base) + .replace(/@colour-light/g, colors.light) + .replace(/@colour-dark/g, colors.dark); + fs.writeFileSync(options.outputPath + 'amWiki/css/amWiki.css', wikiCss, 'utf-8'); + //拷贝页面资源 + const fileList = [ + ['markdownbody.github.css', 'amWiki/css/markdownbody.github.css'], + ['lhjs.github-gist.css', 'amWiki/css/lhjs.github-gist.css'], + ['gbk.js', 'amWiki/js/gbk.js'], + ['pinyin.js', 'amWiki/js/pinyin.js'], + ['flowchart.min.js', 'amWiki/js/flowchart.min.js'], + ['raphael-min.js', 'amWiki/js/raphael-min.js'], + ['jquery-compat-3.1.0.min.js', 'amWiki/js/jquery-compat-3.1.0.min.js'], + ['marked.min.js', 'amWiki/js/marked.min.js'], + ['highlight.pack.js', 'amWiki/js/highlight.pack.js'], + ['amWiki.js', 'amWiki/js/amWiki.js'], + ['amWiki.scrollbar.js', 'amWiki/js/amWiki.scrollbar.js'], + ['amWiki.imgsView.js', 'amWiki/js/amWiki.imgsView.js'], + ['amWiki.testing.js', 'amWiki/js/amWiki.testing.js'], + ['amWiki.tools.js', 'amWiki/js/amWiki.tools.js'], + ['amWiki.storage.js', 'amWiki/js/amWiki.storage.js'], + ['amWiki.docs.js', 'amWiki/js/amWiki.docs.js'], + ['amWiki.search.js', 'amWiki/js/amWiki.search.js'], + ['amWiki.search.worker.js', 'amWiki/js/amWiki.search.worker.js'], + ['icons.svg', 'amWiki/images/icons.svg'], + ['logo.png', 'amWiki/images/logo.png'], + ['menubar_bg.png', 'amWiki/images/menubar_bg.png'] + ]; + for (let file of fileList) { + that._copyFile(options.filesPath + file[0], options.outputPath + file[1]); + } + //如果没有library则复制一套默认文档 + if (!hasLibrary) { + //首页文档 + const home = fs.readFileSync(options.filesPath + 'doc.home.md', 'utf-8').replace('{{name}}', config.name); + fs.writeFileSync(options.outputPath + 'library/home-首页.md', home, 'utf-8'); + //其他页面文档 + let fileList2 = [ + ['doc.amwiki-introduce.md', 'library/001-学习amWiki/01-amWiki轻文库简介.md'], + ['doc.amwiki-mind-map.md', 'library/001-学习amWiki/02-amWiki功能导图.md'], + ['doc.amwiki-new.md', 'library/001-学习amWiki/03-如何开始一个新amWiki轻文库.md'], + ['doc.amwiki-edit.md', 'library/001-学习amWiki/04-如何编辑amWiki轻文库.md'], + ['doc.amwiki-testing.md', 'library/001-学习amWiki/06-使用测试模块测试接口.md'], + ['doc.amwiki-test-cross.md', 'library/001-学习amWiki/07-amWiki转接到任意域名进行接口测试.md'], + ['doc.md-start.md', 'library/001-学习amWiki/05-学习markdown/01-Markdown快速开始.md'], + ['doc.md-high-lighting.md', 'library/001-学习amWiki/05-学习markdown/02-amWiki与语法高亮.md'], + ['doc.md-flow-chart.md', 'library/001-学习amWiki/05-学习markdown/03-amWiki与流程图.md'], + ['doc.md-atom.md', 'library/001-学习amWiki/05-学习markdown/05-Atom对Markdown的原生支持.md'], + ['doc.demo-api.md', 'library/002-文档示范/001-通用API接口文档示例.md'], + ['doc.demo-long-article.md', 'library/002-文档示范/002-超长文档页内目录示例.md'] + ]; + for (let file of fileList2) { + that._copyFile(options.filesPath + file[0], options.outputPath + file[1]); + } + } + return options.outputPath; + }).catch((e) => { + console.error(e); + }); + } + }; +})(); + +module.exports = creator; \ No newline at end of file diff --git a/build/exportGithub.js b/build/exportGithub.js new file mode 100644 index 0000000..b1d880a --- /dev/null +++ b/build/exportGithub.js @@ -0,0 +1,314 @@ +/** + * 工作端 - 导出 github-wiki 模块 + * @author Tevin + */ + +const fs = require('fs'); +const co = require('../modules/co'); +const mngFolder = require('./manageFolder'); +const mngWiki = require('./manageWiki'); + +const exportGithub = (function () { + return { + /** + * 拷贝一张图片 + * @param {String} from + * @param {String} to + * @private + */ + _copyImg: function (from, to) { + fs.createReadStream(from).pipe(fs.createWriteStream(to)); + }, + /** + * 拷贝一篇文档 + * @param {String} fileName + * @param {String} pathTo + * @private + */ + _copyMd: function (fileName, pathTo) { + let file = fs.readFileSync(fileName[0], 'utf-8'); + //相对路径图片地址转换 + file = file + .replace(/!\[(.*?)]\(assets(.*?)\)/g, '![$1](' + this._githubUrl + 'wiki/images$2)') + .replace(/\[(.*?)]\(assets(.*?)\)/g, '[$1](' + this._githubUrl + 'wiki/images$2)') + .replace(/ { + this._copyImg(pathFrom + '../amWiki/images/logo.png', pathTo + '/images/amWiki-logo.png'); + return this._githubUrl + 'wiki/images/amWiki-logo.png'; + }); + fs.writeFileSync(pathTo + '/Home.md', file, 'utf-8'); + }, + /** + * 导出图片 + * @param {String} pathFrom + * @param {String} pathTo + * @private + */ + _exportImage: function (pathFrom, pathTo) { + if (!fs.existsSync(pathTo + '/images/')) { + fs.mkdirSync(pathTo + '/images/', 0o777); + } + if (fs.existsSync(pathFrom + '../assets/')) { + mngFolder.copyFolder(pathFrom + '../assets/', pathTo + '/images/'); + } + }, + /** + * 检查重名 + * @param {[Array]} fileList + * @returns {[Array]} + * @private + */ + _checkDuplicate: function (fileList) { + const dup = []; + for (let i = 0, item; item = fileList[i]; i++) { + for (let j = i + 1, item2; item2 = fileList[j]; j++) { + if (item[1] === item2[1]) { + dup.push([item, item2]); + } + } + } + return dup; + }, + /** + * 导出重复文档 + * @param {Array} list + * @param {String} pathTo + * @private + */ + _exportDuplicate: function (list, pathTo) { + if (list.length === 0) { + return; + } + const checkExist = (fileName) => { + //如果已存在,增加空格 + if (fs.existsSync(pathTo + '/' + fileName[1])) { + fileName[1] = fileName[1].replace(/\.md$/, ' .md'); + checkExist(fileName); + } + //不存在正常复制 + else { + this._copyMd(fileName, pathTo); + } + }; + for (let fileName of list) { + checkExist(fileName); + } + }, + /** + * 导出普通文档 + * @param {Array} list + * @param {String} pathTo + * @private + */ + _exportNormal: function (list, pathTo) { + if (list.length === 0) { + return; + } + for (let fileName of list) { + this._copyMd(fileName, pathTo); + } + }, + /** + * 开始导出 + * @param {String} pathFrom + * @param {String} pathTo + * @param {[Array]} fileList + * @param {[Array]} duplicates + * @returns {Promise} + * @private + */ + _toExport: function (pathFrom, pathTo, fileList, duplicates) { + const that = this; + return co(function* () { + if (mngFolder.readFolder(pathTo).length > 0 && (yield confirm2('所选导出文件夹不为空,是否需要清空?'))) { + mngFolder.cleanFolder(pathTo); + } + let fileList2 = []; + const duplicate2 = []; + if (typeof duplicates !== 'undefined') { + for (let file of fileList) { + let fileDup = false; + for (let dup of duplicates) { + if (file[0] === dup) { + fileDup = true; + duplicate2.push(file); + break; + } + } + if (!fileDup) { + fileList2.push(file); + } + } + } else { + fileList2 = fileList; + } + //开始拷贝 + that._exportNormal(fileList2, pathTo); + that._exportDuplicate(duplicate2, pathTo); + that._exportImage(pathFrom, pathTo); + that._exportHome(pathFrom, pathTo); + that._exportNavigation(fileList2, duplicate2, pathFrom, pathTo); + }).catch((e) => { + console.error(e); + }); + }, + /** + * 导出准备 + * @param {String} pathFrom + * @param {String} pathTo + * @returns {Promise} + * @private + */ + _toPrepare: function (pathFrom, pathTo) { + const that = this; + return co(function*() { + const fileList = []; + pathFrom += /[\\\/]$/.test(pathFrom) ? 'library/' : '/library/'; + //读取文件夹 + const [, , files] = mngFolder.readLibraryTree(pathFrom); + if (files.length === 0) { + return; + } + let fileName = ''; + //提取文件名 + for (let item of files) { + fileName = item.split(/[\\\/]/); + fileList.push([ + item, + fileName[fileName.length - 1].replace(/^\d+(\.\d+)?[-_](.*?)$/, '$2') + ]); + } + //重名检查 + const duplicate = that._checkDuplicate(fileList); + //重名文件单独处理 + if (duplicate.length > 0) { + const dups = []; + let message = '以下文件脱离文件夹偏平化时将会重名:\n\n'; + for (let j = 0, dup; dup = duplicate[j]; j++) { + message += dup[0][0].replace(/\//g, '\\') + '\n' + + dup[1][0].replace(/\//g, '\\') + '\n\n'; + dups.push(dup[0][0], dup[1][0]); + } + message += '点击确认将自动处理(追加额外空格)并继续导出;\n' + + '点击取消将退出导出,您可以在改名后再次重新导出。'; + if (yield confirm2(message)) { + return that._toExport(pathFrom, pathTo, fileList, dups); + } + } else { + return that._toExport(pathFrom, pathTo, fileList); + } + }).catch((e) => { + console.error(e); + }); + }, + /** + * GitHub项目地址 + * @private + */ + _githubUrl: '', + /** + * 解析 GitHub url + * @param {String} url + * @returns {Boolean|String} + * @private + */ + _parseGithubUrl: function (url) { + if (url === '') { + alert('导出失败!\n未检测到 “github-url” 配置,请在 config.json 中配置您项目的 github-url'); + return false; + } + if (url.indexOf('github.com') === -1) { + alert('导出失败!\n配置完整的 Github 项目 URL!\n例如 https://github.com/TevinLi/amWiki'); + return false; + } + const urlArr = url.split('github.com')[1].split('/'); + if (urlArr.length < 3) { + alert('导出失败!\n请配置完整 Github 项目 URL!\n例如 https://github.com/TevinLi/amWiki'); + return false; + } + urlArr[2] = urlArr[2].split(/\?#/)[0].split(/\.git/i)[0]; + return '/' + urlArr[1] + '/' + urlArr[2] + '/'; + }, + /** + * 导出为github-wiki + * @param {String} root - 文库项目根目录 + * @param {String} outputPath - 输出路径 + * @returns {Promise} + * @public + */ + export: function (root, outputPath) { + const that = this; + return co(function* () { + const wikiConfig = mngWiki.getWikiByRoot(root).config; + //检测 GitHub 项目地址 + that._githubUrl = that._parseGithubUrl(wikiConfig.githubUrl); + if (!that._githubUrl) { + return; + } + yield that._toPrepare(root, outputPath); + }); + } + }; +})(); + +module.exports = exportGithub; diff --git a/build/localServer.js b/build/localServer.js new file mode 100644 index 0000000..8f8d033 --- /dev/null +++ b/build/localServer.js @@ -0,0 +1,104 @@ +/** + * 工作端 - 本地服务器模块 + * @author Tevin + */ + +const fs = require('fs'); +const child_process = require('child_process'); +const co = require('../modules/co'); +const Sever = require('./server.class'); +const mngWiki = require('./manageWiki'); + +const localServer = (function () { + return { + /** + * 本地 server 实例 + * @private + */ + _server: null, + /** + * 启动服务器 + * @param {Object} wikis - 文库记录列表的引用 + * @param {Number} port - 服务器端口号 + * @public + */ + run: function (wikis, port) { + const that = this; + return co(function*() { + if (!that._server) { + that._server = new Sever(wikis, port); + yield that._server.run(); + } + }); + }, + /** + * 浏览当前文档 + * @param {String} editPath - 当前文档的路径 + * @param {Object} wikis - 文库记录列表的引用 + * @returns {Promise} + * @public + */ + browser: function (editPath, wikis) { + const that = this; + return co(function*() { + //判断服务器 + if (!that._server) { + if (yield confirm2('本地服务器还未启动,您需要启动服务器么?')) { + that._server = new Sever(wikis); + yield that._server.run(); + } else { + return; + } + } + //解析地址 + const host = 'http://' + that._server.getLocalIP() + ':' + that._server.getPort(); + const wikiId = mngWiki.createWikiId(editPath.split('library')[0]); + let url; + if (editPath.indexOf('$navigation.md') >= 0) { + url = host + '/wiki' + wikiId + '/index.html'; + } else { + let filePath = editPath.split('library/')[1]; + if (typeof filePath === 'undefined') { + url = host + '/wiki' + wikiId + '/index.html'; + } else { + filePath = filePath.replace(/\\/g, '/').replace(/ /g, '%20').replace('.md', ''); + url = host + '/wiki' + wikiId + '/index.html?file=' + filePath; + } + } + //呼起默认浏览器打开页面 + let cmd; + //windows + if (process.platform === 'win32') { + cmd = 'start'; + } + //linux + else if (process.platform === 'linux') { + cmd = 'xdg-open'; + } + //mac + else if (process.platform === 'darwin') { + cmd = 'open'; + } + child_process.exec(cmd + ' ' + url); + }).catch((e) => { + console.error(e); + }); + }, + /** + * 关闭 amWiki 索引页显示 + * @public + */ + setOffIndex: function () { + this._server.offIndex(); + }, + /** + * 关闭服务器 + * @public + */ + destroy: function () { + this._server && this._server.close(); + } + }; +})(); + +module.exports = localServer; \ No newline at end of file diff --git a/build/makeMounts.js b/build/makeMounts.js new file mode 100644 index 0000000..35651cb --- /dev/null +++ b/build/makeMounts.js @@ -0,0 +1,187 @@ +/** + * 工作端 - 创建 mounts/*.js 页面挂载数据模块 + * @author Tevin + */ + + +const fs = require('fs'); +const mngFolder = require('./manageFolder'); +const mngWiki = require('./manageWiki'); + +const makeMounts = (function () { + return { + /** + * 创建 mounts 数据 + * @param {String} editPath + * @param {Boolean} [offMsg] - 是否关闭提示 + * @public + */ + make: function (editPath, offMsg) { + const rootPath = mngFolder.getProjectFolder(editPath); + if (!this._checkMountsAllow(rootPath)) { + if (offMsg !== true) { + alert('更新失败!\n配置项“page-mounts”不存在或不为 true,请在 config.json 中配置'); + } + return; + } + const libPath = rootPath + 'library/'; + const [, list,] = mngFolder.readLibraryTree(libPath); + const mountData = this._getMountsData(libPath, rootPath, list); + const mountFiles = this._exportMounts(mountData, rootPath); + this._updateMountLinks(mountFiles, rootPath); + }, + /** + * 检查 mounts 创建许可 + * @param {String} rootPath + * @returns {Boolean} + * @private + */ + _checkMountsAllow: function (rootPath) { + const wiki = mngWiki.getWikiByRoot(rootPath); + if (!wiki) { + return false; + } + const config = wiki.config; + if (typeof config.pageMounts !== 'undefined') { + return config.pageMounts === true || typeof config.pageMounts === 'object'; + } else { + return false; + } + }, + /** + * 收集 mounts 数据 + * @param {String} libPath + * @param {String} rootPath + * @param {[Object]} list + * @returns {{Array}} + * @private + */ + _getMountsData: function (libPath, rootPath, list) { + const mountData = {}; + const timestamp = Date.now(); + for (let file of list) { + if (file.type !== 'file') { + continue; + } + let filePath = file.path + '/' + file.name; + let lv1Id = 'm' + mngFolder.getLevel1Id(filePath, libPath); + if (lv1Id.length === 0) { + continue; + } + if (typeof mountData[lv1Id] === 'undefined') { + mountData[lv1Id] = []; + } + mountData[lv1Id].push({ + name: file.name, + path: filePath.substr(libPath.length), + content: fs.readFileSync(filePath, 'utf-8'), + timestamp: timestamp + }); + } + mountData['home'] = this._getHomeMount(libPath); + mountData['nav'] = this._getNavMount(libPath); + mountData['icon'] = this._getIconsMount(rootPath); + //console.log(mountData['home'].name, mountData['nav'].name, mountData['icon'].name); + mountData['home'].timestamp = mountData['nav'].timestamp = mountData['icon'].timestamp = timestamp; + return mountData; + }, + /** + * 获取首页挂载数据 + * @param {String} libPath + * @returns {{name: String, path: String, content: String}} + * @private + */ + _getHomeMount: function (libPath) { + const list = mngFolder.readFolder(libPath); + for (let name of list) { + if (/^home[-_].*?\.md$/.test(name) || name === '首页.md') { + const homePath = libPath + name; + return { + name: name, + path: homePath.substr(libPath.length), + content: fs.readFileSync(homePath, 'utf-8'), + } + } + } + }, + /** + * 获取导航挂载数据 + * @param {String} libPath + * @returns {{name: String, path: String, content: String}} + * @private + */ + _getNavMount: function (libPath) { + const filePath = libPath + '$navigation.md'; + return { + name: '$navigation.md', + path: filePath.substr(libPath.length), + content: fs.readFileSync(filePath, 'utf-8') + } + }, + /** + * 获取图标挂载数据 + * @param {String} rootPath + * @returns {{name: String, path: String, content: String}} + * @private + */ + _getIconsMount: function (rootPath) { + const filePath = rootPath + 'amWiki/images/icons.svg'; + return { + name: 'icons.svg', + path: '../' + filePath.substr(rootPath.length), + content: fs.readFileSync(filePath, 'utf-8') + } + }, + /** + * 输出 ./mounts/*.js 文件 + * @param {{Array}} mountData + * @param {String} rootPath + * @returns {Array} + * @private + */ + _exportMounts: function (mountData, rootPath) { + const mountPath = rootPath + 'amWiki/mounts/'; + if (!fs.existsSync(mountPath)) { + fs.mkdirSync(mountPath, 0o777); + } else { + mngFolder.cleanFolder(mountPath); + } + const mountTemplate = 'if(typeof AWPageMounts==\'undefined\'){AWPageMounts={}};' + + 'AWPageMounts[\'{{id}}\']={{content}}'; + const mountFiles = []; + for (let mountId in mountData) { + if (mountData.hasOwnProperty(mountId)) { + let mountContent = JSON.stringify(mountData[mountId]).replace(/'/g, '\\\''); + mountContent = mountTemplate + .replace('{{id}}', mountId) + .replace('{{content}}', mountContent); + let mountName = mountId + '-' + mountContent.length + '.js'; + mountFiles.push(mountName); + fs.writeFileSync(mountPath + mountName, mountContent, 'utf-8'); + } + } + return mountFiles; + }, + /** + * 更新 index.html 文件的挂载 + * @param {Array} mountFiles + * @param {String} rootPath + * @private + */ + _updateMountLinks: function (mountFiles, rootPath) { + let linksHtml = ''; + for (let fileName of mountFiles) { + linksHtml += ''; + } + let indexSrc = fs.readFileSync(rootPath + 'index.html', 'utf-8'); + const mountReg = /([\s\S]*?)<\/div>/; + indexSrc = indexSrc.replace(mountReg, function (m, s1, s2, s3) { + return '' + + linksHtml + s3 + ''; + }); + fs.writeFileSync(rootPath + 'index.html', indexSrc, 'utf-8'); + } + }; +})(); + +module.exports = makeMounts; diff --git a/build/makeNavigation.js b/build/makeNavigation.js new file mode 100644 index 0000000..60af6e0 --- /dev/null +++ b/build/makeNavigation.js @@ -0,0 +1,175 @@ +/** + * 工作端 - 创建 $navigation.md 导航文件模块 + * @author Tevin + */ + +const fs = require('fs'); +const mngFolder = require('./manageFolder'); + +const makeNavigation = (function () { + return { + /** + * 刷新导航 + * @param {String} editPath - 当前文档的路径 + * @returns {String} library文件夹路径 + * @public + */ + refresh: function (editPath) { + const path = editPath.replace(/\\/g, '/').split('library')[0] + 'library/'; + const [tree, list] = mngFolder.readLibraryTree(path); + if (!tree) { + return ''; + } + this.make(path, tree, list); + return path + }, + /** + * 创建 .md 导航文件 + * @param {String} path - 文库library文件夹路径 + * @param {Object} tree - 目录结构树形数据 + * @param {Array} list - 目录结构列表数据 + * @public + */ + make: function (path, tree, list) { + //先检查id重复 + if (this._checkDuplicateId(tree)) { + return; + } + let errorHas = false; + let markdown = ''; + //首页链接输出为 Markdown 文本 + for (let fileName in tree) { + if (tree.hasOwnProperty(fileName)) { + if (fileName === '首页.md') { + break; + } else if (/^home[-_].*?\.md$/.test(fileName)) { + const [, path, name] = fileName.match(/^(home[-_])(.*?)\.md$/); + markdown += '\n#### [' + name + '](?file=' + path + name + ')\n'; + break; + } + } + } + markdown = (markdown === '') ? '\n#### [首页](?file=首页 "返回首页")\n' : markdown; + //导航数据输出为 Markdown 文本 + for (let item of list) { + //检查 id 合法性 + if (!this._checkFileId(item.name, item.path)) { + errorHas = true; + continue; + } + //转义文件名中包含的语法符号 + item.name = item.name + .replace(/'/g, ''') //转义单引号 + .replace(/"/g, '"') //转义双引号 + .replace(/\(/g, '(') //转义左圆括号 + .replace(/\)/g, ')') //转义右圆括号 + .replace(/\[/g, '[') //转义左中括号 + .replace(/\]/g, ']'); //转义右中括号 + //一级目录 + if (item.depth === 0) { + //一级目录不允许文件 + if (item.type !== 'folder') { + continue; + } + markdown += '\n##### ' + item.name.match(/^\d+(\.\d+)?[-_](.*?)$/)[2] + '\n'; + } + //深目录 + else { + //层级缩进 + markdown += new Array(item.depth).join(' '); + //文件夹和文件分开处理 + if (item.type === 'folder') { + markdown += '- **' + item.name.match(/^\d+(\.\d+)?[-_](.*?)$/)[2] + '**\n'; + } else if (item.type === 'file') { + //检查名称合法性 + if (!this._checkFileName(item.name, item.path)) { + errorHas = true; + continue; + } + const name = item.name.match(/^\d+(\.\d+)?[-_](.*?)\.md$/)[2]; + const path = item.path.split('library/')[1] + '/' + item.name.split('.md')[0]; + markdown += '- [' + name + '](?file=' + path + ' "' + name + '")\n'; + } + } + } + if (!errorHas) { + fs.writeFileSync(path + '$navigation.md', markdown, 'utf-8'); + } + }, + /** + * 检查id合法性 + * @param {String} name + * @param {String} path + * @returns {Boolean} + * @private + */ + _checkFileId: function (name, path) { + if (/^\d+(\.\d+)?[-_](.*?)$/.test(name)) { + return true + } else { + const text = '不合法的排序ID!\n请修改排序ID为整数或浮点数,且使用连接符或下划线与具体名称相接\n' + + ' at path "library/' + path.split('library/')[1] + '"\n' + + ' at file "' + name + '"'; + alert(text); + return false; + } + }, + /** + * 检查名称合法性 + * @param {String} name + * @param {String} path + * @returns {Boolean} + * @private + */ + _checkFileName: function (name, path) { + if (/^\d+(\.\d+)?[-_](.*?)\.md$/.test(name)) { + return true + } else { + const errText = '不合法的文件名!\n请将文件名修改为形如 “排序ID-名称.md” 格式的名称\n' + + ' at path "library/' + path.split('library/')[1] + '/"\n' + + ' at file "' + name + '"'; + alert(errText); + return false; + } + }, + /** + * 递归检查(某一目录下的同级文件和文件夹的)ID是否存在重复 + * @param {Object} tree - 当前层级的目录结构 + * @param {String} path - 当前目录所在路径 + * @return {Boolean} 如果返回为true,则说明存在重复ID + * @private + */ + _checkDuplicateId: function (tree, path = '') { + let duplicate = false; + let idList = {}; + for (let name in tree) { + if (tree.hasOwnProperty(name)) { + if (!idList[name.split(/[-_]/)[0]]) { + idList[name.split(/[-_]/)[0]] = name; + } else { + alert('重复的排序ID!\n同级目录下存在相同的排序ID(' + name.split('-')[0] + '),请更改为不同的ID\n' + + ' at path "library/' + path + '"\n' + + ' at file1 "' + idList[name.split('-')[0]] + '"\n' + + ' at file2 "' + name + '"'); + duplicate = true; + } + //文件夹 + if (tree[name]) { + // duplicate 为真后,不再改变其值 + if (duplicate) { + this._checkDuplicateId(tree[name], path + name + '/') + } + // duplicate 为否时,接收子级真假值 + else { + duplicate = this._checkDuplicateId(tree[name], path + name + '/'); + } + } + } + } + return duplicate; + } + }; +})(); + + +module.exports = makeNavigation; \ No newline at end of file diff --git a/build/manageFolder.js b/build/manageFolder.js new file mode 100644 index 0000000..b0a78ec --- /dev/null +++ b/build/manageFolder.js @@ -0,0 +1,307 @@ +/** + * 工作端 - 文件夹管理模块 + * @author Tevin + */ + +const fs = require('fs'); + +const manageFolder = (function () { + return { + /** + * 判断一个文件夹是否为 amWiki 文库项目 + * @param {String} path - 需要判断的文件夹路径 + * @returns {Boolean|String} 判断为否时返回 false,判断为真时返回项目根目录的路径 + * @public + */ + isAmWiki: function (path) { + if (!path && typeof path !== 'string') { + return false; + } + path = path.replace(/\\/g, '/'); + path = path.indexOf('config.json') < 0 ? path : path.split('config.json')[0]; + path = path.indexOf('index.html') < 0 ? path : path.split('index.html')[0]; + //获取 library 路径 + path = this.getProjectFolder(path); + if (!path) { + return false; + } + //通过识别文件夹子项来判定 + else { + let states = [ + fs.existsSync(path + 'library/'), + fs.existsSync(path + 'amWiki/'), + fs.existsSync(path + 'config.json'), + fs.existsSync(path + 'index.html') + ]; + return states[0] && states[1] && states[2] && states[3] ? path : false; + } + }, + /** + * 递归分析指定文件夹下的目录结构 + * @param {String} dirPath - 指定要分析的目录 + * @param {Number} [depth=0] - 文件夹深度 + * @param {Object} [tree={}] - 当前深度结构的树形 + * @param {[Object]} [list=[]] - 当前深度结构的列表 + * @param {[String]} [files=[]] - 当前深度文件的列表 + * @returns {{tree: Object, list: [Object], files: [String]}} - 树形数据、列表数据、仅文件列表数据 + * @private + */ + _listSubFolder: function (dirPath, depth = 0, tree = {}, list = [], files = []) { + try { + let filePath; + for (let fileName of this.readFolder(dirPath)) { + //文件路径 + filePath = dirPath + (depth > 0 ? '/' : '') + fileName; + //文件夹及其递归处理 + if (this.isFolder(filePath)) { + //读取下一级数据 + const [tempTree, tempList, tempFiles] = this._listSubFolder(filePath, depth + 1); + //记录子级的树形 + tree[fileName] = tempTree; + //将此文件夹和其所有子级文件(夹)加入结构列表 + list.push({ + depth: depth, + type: 'folder', + name: fileName, + path: dirPath + }, ...tempList); + //将所有子级文件加入文件列表 + files.push(...tempFiles); + } + //第二层以下深度才允许使用文件(第一层仅允许为文件夹) + else { + if (depth > 0) { + //类型为文件时,树形标记为否 + tree[fileName] = false; + //将此文件加入结构列表 + list.push({ + depth: depth, + type: 'file', + name: fileName, + path: dirPath + }); + //将此文件加入文件列表 + files.push(filePath); + } + } + } + } catch (err) { + //readdirSync 读取失败时仅抛错 + console.error(err); + } + return [tree, list, files]; + }, + /** + * 读取文库library文件夹树形数据 + * @param {String} path - 文库library文件夹路径 + * @returns {{tree: Object, list: [Object], files: [String]}} - 树形数据、列表数据、仅文件列表数据 + * @public + */ + readLibraryTree: function (path) { + if (!/library[\\\/]$/.test(path)) { + console.warn('The path is not a library.'); + return []; + } + const tree = {}; + for (let fileName of this.readFolder(path)) { + if (/^home[-_].*?\.md$/.test(fileName) || fileName === '首页.md') { + tree[fileName] = false; + break; + } + } + return this._listSubFolder(path, 0, tree); + }, + /** + * fs 同步读取文件夹改进版,忽略以点开头的文件 + * (比如 Mac 的 .DS_Store 文件) + * @param {String} path + * @param {Boolean} [force] - 是否强制读取文件夹内所有内容 + * @returns {Array} + * @public + */ + readFolder: function (path, force) { + let files = []; + try { + if (force === true) { + files = fs.readdirSync(path); + } else { + fs.readdirSync(path).forEach(function (fileName) { + //忽略点开头的文件 + if (fileName.indexOf('.') !== 0) { + files.push(fileName); + } + }); + } + } catch (e) {} + return files; + }, + /** + * 递归清空文件夹 + * @param {String} path - 要清空的文件夹 + * @param {Boolean} [force] - 是否强制清理文件夹内所有文件 + * @public + */ + cleanFolder: function (path, force) { + const list = this.readFolder(path, force); + let path2; + for (let item of list) { + path2 = path.replace(/\\?$/, '/') + item; + //文件夹,递归删除 + if (this.isFolder(path2)) { + this.cleanFolder(path2, true); + fs.rmdirSync(path2); + } + //文件 + else { + fs.unlinkSync(path2); + } + } + }, + /** + * 递归创建文件夹 + * @param {String} path - 需要创建的文件夹路径 + * @public + */ + createFolder: function (path) { + //先判断父级文件夹是否存在,不存在先创建父级文件夹 + const parentPath = this.getParentFolder(path); + if (!fs.existsSync(parentPath)) { + this.createFolder(parentPath); //向上递归创建父级 + this.createFolder(path); //创建完父级后再创建本级 + } + //如果父级已存在,直接创建本级 + else { + if (!fs.existsSync(path)) { + fs.mkdirSync(path, 0o777); + } + } + }, + /** + * 深度拷贝一个文件夹到指定位置 + * @param {String} from + * @param {String} to + * @public + */ + copyFolder: function (from, to) { + const list = this.readFolder(from); + let from2, to2; + for (let item of list) { + from2 = from + '/' + item; + to2 = to + '/' + item; + if (this.isFolder(from2)) { + this.createFolder(to2); + this.copyFolder(from2, to2); + } else { + fs.createReadStream(from2).pipe(fs.createWriteStream(to2)); + } + } + }, + /** + * 判断一个地址是否为文件夹 + * @param {String} path + * @returns {Boolean} + * @public + */ + isFolder: function (path) { + let state; + try { + state = fs.statSync(path).isDirectory(path); + } catch (e) { + state = false; + } + return state; + }, + /** + * 获取项目文件夹 + * @param {String} path - 需要计算的文件夹路径 + * @return {String|Boolean} 放回当前文库项目的路径,如果非文库返回 false + * @public + */ + getProjectFolder: function (path) { + path = this.getLibraryFolder(path); + if (path) { + return path.replace(/library(\\|\/)?$/, ''); + } else { + return false; + } + }, + /** + * 获取 library 目录路径 (通过反向查找,而不是 split 截断) + * @param {String} path - 需要计算的文件夹路径 + * @returns {String|Boolean} 返回当前文库 library 目录的路径,如果非文库则返回 false + * @public + */ + getLibraryFolder: function (path) { + if (!path) { + return false; + } + //如果当前路径不存在 library 字眼,则判断路径下是否存在 library 文件夹 + if (path.indexOf('library') < 0) { + //存在返回拼合路径 + if (fs.existsSync(path + '/library/')) { + return path.replace(/\/$/, '') + '/library/'; + } + //不存在继续检查上一级 + else { + return this.getLibraryFolder(this.getParentFolder(path)); + } + } + //存在 library 字眼,反向层层识别,直到 library/ 为止 + else { + if (/library\/?$/.test(path)) { + return path.replace(/\/?$/, '/'); + } else { + return this.getLibraryFolder(this.getParentFolder(path)); + } + } + }, + /** + * 获取当前文件(夹)所属上级目录的路径 + * @param {String} path - 需要计算的文件夹路径 + * @returns {String|Boolean} 父级文件夹路径 + * @public + */ + getParentFolder: function (path) { + path = path.replace(/\\/g, '/'); + // Win 平台,如果太短直接返回 false + if (/^[a-zA-Z]:/.test(path) && path.length < 3) { + return false; + } + // Linux 平台,如果斜杆数只有一个,返回 false + if (path.replace('/', '').indexOf('/') < 0) { + return false; + } + return path.replace(/\/$/, '').replace(/\/[^\/]+$/, '/'); + }, + /** + * 获取当前文件(夹)的名称 + * @param {String} path - 需要计算的路径 + * @return {String} 文件(夹)名称 + * @public + */ + getBaseName: function (path) { + return path.replace(/\\/g, '/').replace(/\/$/, '').match(/\/([^\/]*?)$/)[1]; + }, + /** + * 获取路径对应的一级目录 id + * @param {String} path + * @param {String} [libPath] + * @returns {String} + * @public + */ + getLevel1Id: function (path, libPath) { + if (path.indexOf('library') < 0) { + return ''; + } + if (typeof libPath === 'undefined' || libPath < 8) { + let libPath = this.getLibraryFolder(path); + if (!libPath || libPath.length < 8) { + return ''; + } + } + return path.substr(libPath.length).split(/[-_]/)[0]; + } + }; +})(); + +module.exports = manageFolder; \ No newline at end of file diff --git a/build/manageWiki.js b/build/manageWiki.js new file mode 100644 index 0000000..13be086 --- /dev/null +++ b/build/manageWiki.js @@ -0,0 +1,199 @@ +/** + * 工作端 - 文库管理模块 + * @author Tevin + */ + +const fs = require('fs'); +const mngFolder = require('./manageFolder'); +const tools = require('./tools'); + +const manageWiki = (function () { + return { + /** + * 文库记录 + * @private + */ + _wikis: null, + /** + * 绑定文库记录 + * @param {Object} wikis - 文库记录列表的引用 + * @public + */ + linkWikis: function (wikis) { + this._wikis = wikis; + }, + /** + * 获取所有文库记录 + * @returns {Object} + * @public + */ + getWikis: function () { + return this._wikis + }, + /** + * 通过 root 地址获取单个文库记录 + * @param {String} root + * @returns {Object} + * @public + */ + getWikiByRoot: function (root) { + for (let wId in this._wikis){ + if (this._wikis.hasOwnProperty(wId)) { + if (this._wikis[wId].root === root) { + return this._wikis[wId] + } + } + } + return null; + }, + /** + * 添加一个文库到记录 + * @param {String} root - 文库根目录 + * @param {Number} [id] - 文库的计算id + * @public + */ + addWiki: function (root, id = this.createWikiId(root)) { + if (typeof root === 'undefined') { + return; + } + if (typeof this._wikis[id] !== 'undefined') { + //读取配置文件 + this.updateWikiConfig(); + return; + } + //文库信息 + this._wikis[id] = { + //文库id,由真实地址计算而来,也就是说文库是基于不同地址来管理的 + id: id, + //文库根目录地址 + root: root, + //文库文档地址 + path: root + 'library/', + //弃用标记,当文库被删除或转移走后,原有地址标记为弃用 + deprecated: false, + //文件清单md5值,用于检测文库目录树变化 + treeMD5: '', + //文库配置文件的解析结果 + config: null + }; + //读取配置文件 + this.updateWikiConfig(); + }, + /** + * 计算文库id + * @param {String} root - 需要计算id的文库根目录 + * @returns {Number} 文库id,由根目录路径字符串计算而来 + * @public + */ + createWikiId: function (root) { + root = root.replace(/\\/g, '/').replace(/\/$/, ''); + //累加地址字符串每个字符Unicode值与此字符序号的乘积 + let code = 0; + for (let j = 0; j < root.length; j++) { + code += root.charCodeAt(j) * j; + } + //再与地址字符串长度拼合 + code = parseInt(root.length + '' + code); + return code; + }, + /** + * 判断文库是否加入记录,未加入则补录 + * @param {String} path - 需要判断的路径 + * @public + */ + checkAddWiki: function (path) { + //验证转换 path 为 wiki 根目录 + const root = mngFolder.isAmWiki(path); + if (root) { + this.addWiki(root); + } + }, + /** + * 检查文库是否仍然有效,失效则标记弃用 + * @param {Number} wId - 需要判断的文库id + * @public + */ + checkWikiValid: function (wId) { + if (typeof this._wikis[wId] !== 'undefined') { + const root = this._wikis[wId].root; + //当文件夹不存在或者文件夹不为amWiki时,标记弃用 + //也就是说,从atom目录树删除项目,并不会标记弃用,必须删除文库本地文件和文件夹 + this._wikis[wId].deprecated = !fs.existsSync(root) || !mngFolder.isAmWiki(root) + } + }, + /** + * 更新文库记录中对应 config.json 的内容 + * @public + */ + updateWikiConfig: function () { + for (let wId in this._wikis) { + if (this._wikis.hasOwnProperty(wId)) { + //读取 config + if (!this._wikis[wId].deprecated) { + let configStr = fs.readFileSync(this._wikis[wId].root + 'config.json') || '{}'; + try { + this._wikis[wId].config = this.parseConfig(JSON.parse(configStr)); + } catch (e) { + console.error('您位于 ' + this._wikis[wId].root + + ' 的文库,config.json 内容格式不合法,请按照 json 格式书写!'); + } + } + //弃用的文库删除 config + else { + this._wikis[wId].config = null; + } + } + } + }, + /** + * 重新解析/过滤 config 配置 + * @param {Object} config + * @return {Object} config + * @public + */ + parseConfig: function (config) { + //库名称 + config.name = typeof config.name === 'undefined' ? 'amWiki轻文库' : config.name + ''; + //库版本号 + config.version = typeof config.ver === 'undefined' ? 'by Tevin' : config.ver + ''; + //logo地址 + config.logo = typeof config.logo === 'undefined' ? 'amWiki/images/logo.png' : config.logo + ''; + //是否开启接口测试 + config.testing = !!config.testing || false; + //设置自定义颜色 + config.colour = typeof config.colour === 'undefined' ? '#4296eb' : config.color + ''; + //本地页面数据挂载 + config.pageMounts = !!(config['page-mounts'] || config.pageMounts) || false; + delete config['page-mounts']; + //项目的 github 地址 + config.githubUrl = config['github-url'] || config.githubUrl; + config.githubUrl = typeof config.githubUrl === 'undefined' ? '' : config.githubUrl + ''; + delete config['github-url']; + //转接 library 地址 + config.libraryPrefix = config['library-prefix'] || config.libraryPrefix; + config.libraryPrefix = typeof config.libraryPrefix === 'undefined' ? '' : config.libraryPrefix + ''; + delete config['library-prefix']; + //自定义 css、js 文件 + if (tools.isArray(config.imports) && config.imports.length > 0) { + const imports2 = { + js: [], + css: [] + }; + for (let file of config.imports) { + let type = file.split(/[?#]/)[0].match(/((\.js)|(\.css))$/)[0]; + if (type === '.js') { + imports2.js.push(file); + } else if (type === '.css') { + imports2.css.push(file); + } + } + config.imports = imports2; + } else { + delete config.imports; + } + return config; + } + }; +})(); + +module.exports = manageWiki; \ No newline at end of file diff --git a/build/server.class.js b/build/server.class.js new file mode 100644 index 0000000..e23982c --- /dev/null +++ b/build/server.class.js @@ -0,0 +1,295 @@ +/** + * 工作端 - 本地服务器类 + * @author Tevin + */ + +const fs = require('fs'); +const util = require('util'); +const http = require('http'); +const url = require('url'); +const os = require('os'); +const mngWiki = require('./manageWiki'); +const gbk = require('../files/gbk.js').GBK; + +//文件类型 +const MimeType = { + 'txt': 'text/plain', + 'html': 'text/html', + 'css': 'text/css', + 'xml': 'application/xml', + 'json': 'application/json', + 'js': 'application/javascript', + 'jpg': 'image/jpeg', + 'jpeg': 'image/jpeg', + 'gif': 'image/gif', + 'png': 'image/png', + 'svg': 'image/svg+xml', + 'swf': 'application/x-shockwave-flash', + 'tiff': 'image/tiff', + 'wav': 'audio/x-wav', + 'wma': 'audio/x-ms-wma', + 'wmv': 'video/x-ms-wmv' +}; + +/** + * @class Server + */ +class Server { + + /** + * @constructor + * @param {[Object]} wikis + * @param {Number} port=5171 + */ + constructor(wikis, port = 5171) { + this._wikis = wikis; + this._port = port; + this._localIP = this.getLocalIP(); + this._indexShow = true; + this._nodeServer = http.createServer((req, res) => { + this._parse(req, res); + }); + } + + /** + * 服务器启动 + * @return {Promise} + * @public + */ + run() { + return new Promise((resolve, reject) => { + let portCount = 0; + this._nodeServer + .on('listening', () => { + mngWiki.updateWikiConfig(); + console.info('Server running at http://' + this._localIP + ':' + this._port + '/'); + resolve(); + }) + .on('error', (e) => { + if (e.syscall === 'listen' && e.code === 'EADDRINUSE') { + if (portCount < 12) { + console.warn('端口 ' + this._port + ' 已被其他程序占用,尝试端口号+1,监听端口 ' + (this._port + 1)); + portCount++; + this._nodeServer.listen(++this._port); + } else { + console.error('端口从 ' + (this._port - portCount) + ' 到 ' + this._port + + ' 尽皆被占用,请使用其他段位的端口!'); + reject(); + } + } else { + throw e; + } + }); + this._nodeServer.listen(this._port); + }); + } + + /** + * 获取本地ip + * @returns {String} 本地ip地址 + * @public + */ + getLocalIP() { + if (this._localIP) { + return this._localIP; + } + const iFaces = os.networkInterfaces(); + let ip = ''; + for (let dev in iFaces) { + if (iFaces.hasOwnProperty(dev)) { + iFaces[dev].forEach((details) => { + if (details.family === 'IPv4' && details.address.indexOf(192) >= 0) { + ip = details.address; + } + }); + } + } + return ip === '' ? '127.0.0.1' : ip; + } + + /** + * 获取当前 server 监听的端口 + * @return {Number} 端口号 + * @public + */ + getPort() { + return this._port; + } + + /** + * 关闭 amWiki 索引页显示 + * @public + */ + offIndex() { + this._indexShow = false; + } + + /** + * 解析请求 + * @param {Object} req - 请求体 + * @param {Object} res - 响应体 + * @private + */ + _parse(req, res) { + const pathname = url.parse(req.url).pathname; + //文库列表页 + if (/^(\/|(\/index(\.html)?))$/.test(pathname)) { + if (this._indexShow) { + return this._renderIndexPage(req, res); + } else { + return Server.page404(req, res, pathname); + } + } + //文库 + let wId = ''; + let filePath = pathname.replace(/^\/wiki(\d{3,}?)\//g, function (match, $1) { + wId = $1; + return ''; + }); + //如果文库不存在或已弃用,返回404 + if (typeof this._wikis[wId] === 'undefined' || this._wikis[wId].deprecated) { + return Server.page404(req, res, pathname); + } + //编码切换 + try { + filePath = decodeURI(filePath); + } catch (e) { + filePath = gbk.decode(filePath); + } + //真实地址 + const realPath = this._wikis[wId].root + filePath; + //解析文件 + try { + fs.exists(realPath, (exists) => { + if (!exists) { + return Server.page404(req, res, pathname); + } else { + const file = fs.createReadStream(realPath); + res.writeHead(200, { + 'Content-Type': MimeType[realPath.split('.').pop()] || 'text/plain' + }); + file.on('data', res.write.bind(res)); + file.on('close', res.end.bind(res)); + file.on('error', function (err) { + return Server.page500(req, res, err); + }); + } + }); + } catch (err) { + return Server.page500(req, res, err); + } + } + + /** + * 渲染索引页 + * @param {Object} req - 请求体 + * @param {Object} res - 响应体 + * @private + */ + _renderIndexPage(req, res) { + //样式 + let styleTp = 'body{background:#f5f5f5}h1,ul{display:block;margin:0 auto;padding:0;width:960px;max-width:95%}' + + 'h1{line-height:100px;text-align:center;font-weight:normal}' + + 'ul{display:block;margin-bottom:30px;}' + + 'li{display:block;position:relative;min-height:100px;padding:20px;margin-bottom:10px;color:gray;' + + 'background:#fff;border-radius:5px}' + + 'li>a{display:block;text-decoration:none;}' + + 'li strong{display:block;margin-right:150px;padding-bottom:8px;font-size:20px;font-weight:normal;' + + 'line-height:38px;color:#333;border-right:#e5e5e5 1px solid;border-bottom:#e5e5e5 1px solid;}' + + 'li>a:hover strong{border-bottom:#4296eb 1px solid;}' + + 'li strong small{margin-left:4px;padding:0 4px;font-style:normal;font-size:12px;line-height:20px;' + + 'color:#fff;vertical-align:text-top;background:#bbbbbb;}' + + 'li img{position:absolute;top:50%;right:90px;max-width:120px;max-height:120px;' + + '-webkit-transform:translateY(-50%) translateX(50%);-moz-transform:translateY(-50%) translateX(50%);' + + '-ms-transform:translateY(-50%) translateX(50%);transform:translateY(-50%) translateX(50%)}' + + 'li p{margin:0 150px 0 0;padding-top:10px;line-height:30px;word-wrap:break-word;word-break:break-all;' + + 'border-right:#e5e5e5 1px solid}' + + 'li p span{display:block}li p a,li p b{color:gray}'; + //布局 + let layoutTp = '' + + 'amWiki Index Page' + + '' + + '

amWiki Index Page (文库列表索引)

' + + '
    {{list}}
'; + //列表单项 + let itemTp = '
  • ' + + '{{title}}{{version}}' + + '

    {{content}}

  • '; + //列表 + let list = ''; + for (let wId in this._wikis) { + if (this._wikis.hasOwnProperty(wId) && !this._wikis[wId].deprecated) { + let wiki = this._wikis[wId]; + //文库链接 + let link = 'http://' + this.getLocalIP() + ':' + this.getPort() + '/wiki' + wiki.id + '/index.html'; + //文库 logo 地址 + let logo = wiki.config.logo; + if (logo.indexOf('http') < 0) { + logo = '/wiki' + wiki.id + '/' + logo; + } + //文库详情 + let content = 'ID: ' + wiki.id + '' + + '本地位置: ' + wiki.root + '' + + '本地网址: ' + link + ''; + if (wiki.config.testing) { + content += '测试模块已开启'; + } + if (wiki.config['github-url']) { + content += 'Github-url: ' + + wiki.config['github-url'] + ''; + } + //添加到列表 + list += itemTp.replace('{{link}}', link) + .replace('{{title}}', wiki.config.name) + .replace('{{version}}', wiki.config.version) + .replace('{{logo}}', logo) + .replace('{{content}}', content); + } + } + //输出 + res.writeHead(200, { + 'Content-Type': 'text/html' + }); + res.write(layoutTp.replace('{{style}}', styleTp).replace('{{list}}', list)); + res.end(); + } + + /** + * 404未找到页面 + * @param {Object} req + * @param {Object} res + * @param {String} path + * @static + */ + static page404(req, res, path) { + res.writeHead(404, { + 'Content-Type': 'text/html' + }); + res.write('\n'); + res.write('404 Not Found\n'); + res.write('

    Not Found

    '); + res.write('

    The requested URL ' + path + ' was not found on this server.

    '); + res.end(); + } + + /** + * 500错误页面 + * @param {Object} req + * @param {Object} res + * @param {Error} error + * @static + */ + static page500(req, res, error) { + res.writeHead(500, { + 'Content-Type': 'text/html' + }); + res.write('\n'); + res.write('Internal Server Error\n'); + res.write('

    Internal Server Error

    '); + res.write('
    ' + util.inspect(error) + '
    '); + res.end(); + } + +} + +module.exports = Server; \ No newline at end of file diff --git a/build/tools.js b/build/tools.js new file mode 100644 index 0000000..8207fa8 --- /dev/null +++ b/build/tools.js @@ -0,0 +1,21 @@ +/** + * 工作端 - 公用工具模块 + * @author Tevin + */ + + +const tools = (function () { + return { + /** + * 判断一个对象是否属于数组 + * @param {Object} obj - 需要判断的对象 + * @return {Boolean} + * @public + */ + isArray: function (obj) { + return Object.prototype.toString.call(obj) === '[object Array]'; + } + }; +})(); + +module.exports = tools; \ No newline at end of file diff --git a/files/amWiki.css b/files/amWiki.css index 09cd09f..bf3295d 100644 --- a/files/amWiki.css +++ b/files/amWiki.css @@ -14,8 +14,11 @@ .markdown-body .anchor svg{visibility:hidden;width:20px;height:20px;color:#828282;} .markdown-body h3 .anchor{position:relative;top:2px;} .markdown-body h1:hover .anchor svg,.markdown-body h2:hover .anchor svg,.markdown-body h3:hover .anchor svg{visibility:visible;} -.markdown-body :not(.anchor)>img:hover{outline:rgba(0,0,0,.05) 1px solid;box-shadow:1px 1px 1px rgba(0,0,0,.1);} +.markdown-body img:hover{outline:rgba(0,0,0,.05) 1px solid;box-shadow:1px 1px 1px rgba(0,0,0,.1);cursor:pointer;} +.markdown-body img[align="center"]{display:block;margin:0 auto;} .markdown-body tbody{word-break:initial;} +.markdown-body input[type="checkbox"]{margin-right:5px;vertical-align:middle;cursor:pointer;} +.markdown-body input[type="checkbox"]+label{vertical-align:middle;margin-right:7px;cursor:pointer;} .markdown-body pre{position:relative;} .markdown-body pre code{padding-top:0.15em;padding-bottom:0.15em;margin:0 1px;font-size:90%;color:#000;word-wrap:break-word;white-space:pre-wrap;} .markdown-body pre code.hljs{color:#333;} @@ -29,29 +32,30 @@ .markdown-body .lang-off-js-comment:hover i{color:#999;} .markdown-body .lang-off-js-comment.on{display:block;color:#333;background:#ccc;} .markdown-body .lang-off-js-comment.on i{color:#f00;} +.markdown-body mark{color:#e61e1e;background:transparent;} /* ---------- base ---------- */ -body{position:relative;background:#e7e8eb;} +html{height:100%;} +body{position:relative;height:100%;background:#e7e8eb;} .hidden{display:none !important;} /* ---------- header ---------- */ -.header{width:100%;height:80px;font-family:Microsoft YaHei,STXihei,STHeiti,Verdana,Arial,Helvetica,sans-serif;;background:#fff;border-top:@colour-dark 6px solid;border-bottom:#c3c4c6 1px solid;} -.header .header-inner{height:80px;margin:0 30px;} -.header .logo{float:left;padding-top:20px;} -.header .logo img{float:left;margin-left:5px;max-height:33px;} +.header{width:100%;height:70px;font-family:Microsoft YaHei,STXihei,STHeiti,Verdana,Arial,Helvetica,sans-serif;;background:#fff;border-top:@colour-dark 6px solid;border-bottom:#c3c4c6 1px solid;} +.header .header-inner{margin:0 30px;} +.header .logo{float:left;padding-top:15px;} +.header .logo img{float:left;margin-left:5px;max-height:32px;} .header .logo i{float:left;display:block;width:5px;height:5px;margin:16px 0 0 3px;background:#666666;border-radius:50%;} -.header .logo b{float:left;max-width:470px;height:33px;padding-left:10px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-size:24px;font-weight:normal;color:#666;} +.header .logo b{float:left;max-width:470px;height:32px;padding-left:10px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-size:24px;font-weight:normal;color:#666;} .header .logo span{float:left;padding:0 3px;margin:1px 0 0 2px;font-size:xx-small;color:#fff;background:#bbbbbb;} -.header .menu_icon{display:none;} +.header .menu-icon{display:none;} /* ---------- container ---------- */ -.container{position:relative;min-height:500px;margin:30px;overflow:hidden;background:url(../images/menubar_bg.png) repeat-y #fff;border:#c3c4c6 1px solid;border-top:@colour-base 2px solid;} +.container{position:relative;margin:20px 18px 0;background:url(../images/menubar_bg.png) repeat-y #fff;border:#c3c4c6 1px solid;border-top:@colour-base 2px solid;} /* ---------- menu ---------- */ -.container .nav{position:relative;left:auto;top:auto;width:250px;} - -/* --- filter --- */ -.menu-filter{box-sizing:content-box;position:absolute;top:0;left:0;z-index:10;width:176px;height:37px;padding-left:35px;} +.container .nav{position:absolute;left:0px;top:0px;width:250px;height:100%;} +/* filter */ +.menu-filter{box-sizing:content-box;position:absolute;top:0;left:0;z-index:10;width:176px;height:37px;padding-left:35px;border-bottom:1px solid #D9D9D9;} .menu-filter svg{position:absolute;top:12px;left:12px;width:16px;height:16px;color:@colour-base;} .menu-filter input{display:block;box-sizing:content-box;width:140px;height:22px;padding:0 25px 0 2px;margin-top:8px;line-height:20px;font-size:14px;background:none;border:none;border-bottom:#e0e0e0 1px dashed;} .menu-filter input:focus{outline:none;border-bottom:#aaa 1px solid;} @@ -61,44 +65,80 @@ body{position:relative;background:#e7e8eb;} .menu-filter :-moz-placeholder{color:#ccc;font-size:13px;} .menu-filter ::-moz-placeholder{color:#ccc;font-size:13px;} .menu-filter ::-ms-clear{display:none !important;width:0;height:0;} -.menu-filter i{display:block;box-sizing:content-box;position:absolute;top:0px;right:0px;width:23px;padding:2px 10px 0;font-size:25px;text-align:center;color:#ccc;cursor:pointer;} +.menu-filter i{display:block;box-sizing:content-box;position:absolute;top:0px;right:0px;width:23px;padding:0 10px;font-size:25px;text-align:center;color:#ccc;cursor:pointer;} .menu-filter i:hover{color:#e6660d;} .menu-filter i.off{display:none;} - -.menu-search{display:block;position:absolute;top:0;right:1px;z-index:11;width:39px;height:37px;border-left:1px solid #D9D9D9;cursor:pointer;} +/* search */ +.menu-search{display:block;position:absolute;top:0;right:1px;z-index:11;width:39px;height:38px;font-style:normal;border-left:1px solid #D9D9D9;border-bottom:1px solid #D9D9D9;cursor:pointer;} .menu-search:hover{background-color:#f4f5f9;} .menu-search svg{position:absolute;top:12px;left:12px;width:16px;height:16px;color:@colour-base;} - -/* --- menubar --- */ -.menubar{float:left;position:relative;width:250px;height:100%;padding-top:38px;font-family:Microsoft YaHei,STXihei,STHeiti,Verdana,Arial,Helvetica,sans-serif;border-right:1px solid #e7e7eb;} +/* menubar */ +.menubar{position:relative;z-index:0;width:250px;height:100%;padding-top:38px;font-family:Microsoft YaHei,STXihei,STHeiti,Verdana,Arial,Helvetica,sans-serif;border-right:1px solid #e7e7eb;} .menubar h4,.menubar h5{display:block;position:relative;height:37px;margin:-1px 0 0;padding:0 10px 0 30px;font-size:14px;font-weight:normal;color:#333;line-height:37px;text-shadow:#666 1px 0 0;border:1px solid #D9D9D9;border-width:1px 0;cursor:pointer;} +.menubar h4:hover{background-color:#f4f5f9;} +.menubar h5.off{display:none;} +.menubar h5.on{display:block;} .menubar h4 svg{position:absolute;top:12px;left:12px;width:13px;height:13px;color:#acacac;} .menubar h4.on svg{color:@colour-base;} -.menubar h4:hover{background-color:#f4f5f9;} .menubar h4 a{display:block;height:36px;color:#333;text-decoration:none;} .menubar h4.on a{color:@colour-dark;} .menubar h5 svg{position:absolute;top:12px;left:12px;width:13px;height:13px;color:#acacac;-webkit-transform:rotate(-90deg);-moz-transform:rotate(-90deg);-ms-transform:rotate(-90deg);transform:rotate(-90deg);} .menubar h5.on svg{color:@colour-base;-webkit-transform:rotate(0);-moz-transform:rotate(0);-ms-transform:rotate(0);transform:rotate(0);} .menubar ul{display:none;margin:0;padding:0;list-style:none;} -.menubar ul li{position:relative;} +.menubar ul li{padding-left:23px;} +.menubar .scroller-content > ul > li{padding-left:45px;} .menubar ul li.off{display:none;} -.menubar ul li a,.menubar ul li > strong{display:block;padding:0 20px 0 0;font-weight:normal;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;line-height:30px;font-size:12px;color:#333;text-indent:45px;text-decoration:none;cursor:pointer;-webkit-transition-duration:.3s;-moz-transition-duration:.3s;-ms-transition-duration:.3s;transition-duration:.3s;} -.menubar ul li:first-child a,.menubar ul li:first-child > strong{padding-top:2px;} -.menubar ul li:last-child a,.menubar ul li:last-child > strong{padding-bottom:2px;} -.menubar ul li a:hover,.menubar ul li > strong:hover{background:#f5f5f5;-webkit-transition-duration:0s;-moz-transition-duration:0s;-ms-transition-duration:0s;transition-duration:0s;} -.menubar ul li a.on{color:@colour-dark;background:#f5f5f5;} -.menubar ul li a.on:after{display:block;position:absolute;top:0px;right:0px;width:2px;height:30px;content:" ";background:@colour-base;} -.menubar ul li:first-child a.on:after{height:32px;} -.menubar ul li:last-child a.on:after{height:32px;} +.menubar ul li > a,.menubar ul li > strong{display:block;height:30px;padding:0 55px 0 0;font-weight:normal;white-space:nowrap;line-height:30px;font-size:12px;color:#333;text-decoration:none;cursor:pointer;} +.menubar ul li > a:after,.menubar ul li > strong:after{display:block;position:absolute;left:0;right:0;height:30px;content:" ";-webkit-transition-duration:.3s;-moz-transition-duration:.3s;-ms-transition-duration:.3s;transition-duration:.3s;} +.menubar ul li > a:hover:after,.menubar ul li > strong:hover:after{background:#f5f5f5;-webkit-transition-duration:0s;-moz-transition-duration:0s;-ms-transition-duration:0s;transition-duration:0s;} +.menubar ul li > a > span,.menubar ul li strong > span{float:left;position:relative;z-index:1;} +.menubar ul li > a.on{color:@colour-dark;} +.menubar ul li > a.on:before{display:block;position:absolute;left:0px;z-index:1;width:2px;height:30px;content:" ";background:@colour-base;} +.menubar ul li > a.on:after{background:#f5f5f5;} .menubar ul li > strong.on{text-shadow:#666 1px 0 0;} -.menubar ul li > strong:before{position:absolute;top:13px;left:32px;width:6px;height:6px;background:#ccc;border-radius:50%;content:" ";} -.menubar ul li:first-child > strong:before{top:15px;} +.menubar ul li > strong:before{position:absolute;z-index:1;width:6px;height:6px;margin:12px 0 0 -12px;background:#acacac;border-radius:50%;content:" ";} .menubar ul li > strong.on:before{background:@colour-base;} -.menubar ul li li a{text-indent:65px;} .menubar mark{color:red;background:#f5f5f5;} -.main{position:relative;padding:45px 45px 155px;margin-left:250px;} -.main-sibling{display:none;position:absolute;left:0;bottom:0;width:100%;padding:35px 45px 20px;font-size:14px;border-top:1px solid #e7e7eb;} +/* ---------- scroller ---------- */ +body[onselectstart]{cursor:pointer;-moz-user-select:none;-webkit-user-select:none;user-select:none;} +.scroller{height:100%;overflow:hidden;} +.scroller .scroller-inner{box-sizing:content-box;width:100%;height:100%;-webkit-overflow-scrolling:touch;overflow-scrolling:touch;} +.scroller .scroller-inner.on{overflow-y:scroll;} +.scroller .scrollbar{position:absolute;opacity:0;transition-duration:.5s;-moz-user-select:none;-webkit-user-select:none;user-select:none;cursor:pointer;} +.scroller:hover .scrollbar{opacity:1;transition-duration:.3s;} +.scroller .scrollbar.active{opacity:1;} +.scroller .scrollbar.off{opacity:0;} +.scroller .scrollbar div{position:relative;transition-duration:.1s;} +.scroller .scrollbar i{position:absolute;background:rgba(50,50,50,.25);border-radius:2px;transition-duration:.1s;} +.scroller .scrollbar:active{padding:0;} +.scroller .scrollbar:active div{background:rgba(50,50,50,.07);} +.scroller .scrollbar:active i{border-radius:0;} +/* scrollbar-y */ +.container .nav .scrollbar.scrollbar-y,.container .nav .scrollbar.scrollbar-y:active{padding-top:38px;padding-bottom:10px;} +.scroller .scrollbar.scrollbar-y{top:0;right:0;width:10px;height:100%;} +.scroller .scrollbar.scrollbar-y div{height:100%;} +.scroller .scrollbar.scrollbar-y i{top:0;left:4px;width:3px;height:100%;} +.scroller .scrollbar.scrollbar-y:active i{left:0;width:10px;} +/* scrollbar-x */ +.scroller[data-x-rolling="true"] .scroller-inner.on{overflow-x:scroll;} +.scroller[data-x-rolling="true"] .scroller-content{display:inline-block;position:relative;padding-bottom:10px;white-space:nowrap;min-width:250px;} +.scroller .scrollbar.scrollbar-x{bottom:0;left:0;width:100%;height:10px;padding-right:10px;} +.scroller .scrollbar.scrollbar-x div{width:100%;height:100%;} +.scroller .scrollbar.scrollbar-x i{bottom:0;top:4px;height:3px;width:100%;} +.scroller .scrollbar.scrollbar-x:active i{top:0;height:10px;} + +/* ---------- main ---------- */ +.main{position:relative;height:100%;margin-left:250px;} +.main .markdown-body{padding:45px;} +/* footnote */ +.main .footnote{position:relative;padding-top:35px;border-top:#ddd 1px solid;} +.main .footnote i{position:absolute;top:5px;left:10px;font-size:15px;font-style:normal;color:#999;} +.main .footnote li:focus{background:#EAFFEA;} +.main .footnote svg{display:inline-block;width:14px;height:14px;margin-bottom:-1px;} +.main .footnote-none{width:0;height:0;overflow:hidden;} +/* sibling */ +.main-sibling{display:none;width:100%;padding:30px 45px 20px;font-size:14px;border-top:1px solid #e7e7eb;} .main-sibling.on{display:block;} .main-sibling p{position:relative;height:22px;} .main-sibling span{position:absolute;left:0;top:0;} @@ -106,18 +146,46 @@ body{position:relative;background:#e7e8eb;} .main-sibling a:not([href]){color:#333;} .main-sibling a:not([href]):hover{text-decoration:none;} -/* ---------- footer ---------- */ -footer{position:relative;width:100%;height:15px;} -.signature{position:absolute;right:40px;bottom:17px;color:#999;} +/* ---------- imgsView ---------- */ +.imgsv{display:none;position:fixed;top:0;left:0;z-index:999999;width:100%;height:100%;} +.imgsv-background{position:absolute;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,.1);cursor:pointer;} +.imgsv-view img{position:absolute;top:48%;left:50%;opacity:0;box-shadow:rgba(0,0,0,.2) 1px 2px 5px;-webkit-transform:translate(-50%,-50%);-moz-transform:translate(-50%,-50%);-ms-transform:translate(-50%,-50%);transform:translate(-50%,-50%);-webkit-transition-duration:.5s;-moz-transition-duration:.5s;-ms-transition-duration:.5s;transition-duration:.5s;/*cursor:move;*/ +} +.imgsv.on .imgsv-view img{opacity:1;} +.imgsv-panel{position:absolute;left:50%;bottom:-50px;padding:0 10px;line-height:50px;list-style:none;white-space:nowrap;background:rgba(255,255,255,.8);border-top-left-radius:5px;border-top-right-radius:5px;box-shadow:rgba(0,0,0,.2) 0 -1px 5px;-webkit-user-select:none;-moz-user-select:none;user-select:none;-webkit-transform:translateX(-50%);-moz-transform:translateX(-50%);-ms-transform:translateX(-50%);transform:translateX(-50%);-webkit-transition-duration:.5s;-moz-transition-duration:.5s;-ms-transition-duration:.5s;transition-duration:.5s;} +.imgsv.on .imgsv-panel{bottom:0px;} +.imgsv-panel li{display:inline-block;min-width:50px;padding:0 10px;text-align:center;cursor:pointer;} +.imgsv-panel li.off{color:#999;cursor:default;} +.imgsv-panel li:hover{background:rgba(225,225,225,1);} +.imgsv-panel li.off:hover{background:none;} -/* ---------- backTop ---------- */ -.back-top{position:fixed;bottom:100px;right:7px;z-index: 9900;height:38px;width:38px;background:#f4f5f9;border:1px solid #ccc;} -.back-icon{display:block;position:relative;width:32px;margin:2px auto;} -.back-icon svg{position:absolute;top:6px;left:5px;width:22px;height:22px;color:#aaa;} -.back-text{display:none;position:absolute;top:0;left:0;z-index:99;margin:4px;font-family:Microsoft YaHei,STXihei,STHeiti,Verdana,Arial,Helvetica,sans-serif;font-size:11px;text-align:center;letter-spacing:2px;line-height:14px;background-color:#f4f5f9;cursor:pointer;} -.back-text:hover{text-decoration:none;} -.back-top:hover .back-icon{border:none;} -.back-top:hover .back-text{display:block;color:#aaa;} +/* ---------- contents ---------- */ +aside .contents{position:fixed;bottom:137px;right:7px;z-index:9900;height:38px;width:38px;background:#f4f5f9;border:1px solid #ccc;} +aside .contents .btn{cursor:pointer;} +aside .contents .btn svg{position:absolute;top:7px;left:7px;width:22px;height:22px;color:#aaa;} +aside .contents.hover .btn svg,aside .contents.on .btn svg{display:none;} +aside .contents .btn span{display:none;padding:12px 0;font-family:Microsoft YaHei,STXihei,STHeiti,Verdana,Arial,Helvetica,sans-serif;line-height:14px;text-align:center;color:#aaa;} +aside .contents.hover .btn span,aside .contents.on .btn span{display:block;} +aside .contents-list{display:none;position:absolute;right:36px;bottom:-1px;width:270px;max-height:500px;padding:18px;overflow:hidden;overflow-y:scroll;background:#f4f5f9;border:1px solid #ccc;} +aside .contents.hover .contents-list,aside .contents.on .contents-list{display:block;} +aside .contents-list p{padding-bottom:5px;margin-bottom:8px;border-bottom:#ccc 1px solid;} +blockquote.markdown-contents{padding-left:20px;} +.markdown-contents ol{padding-left:0;list-style:none;} +.markdown-contents ol ol{padding-left:2em;} +.markdown-contents ol li>i{font-style:normal;margin-right:4px;color:#4078c0;} + +/* ---------- aside-backTop ---------- */ +aside .back-top{position:fixed;bottom:100px;right:7px;z-index: 9900;height:38px;width:38px;background:#f4f5f9;border:1px solid #ccc;} +aside .back-icon{display:block;position:relative;width:32px;margin:2px auto;} +aside .back-icon svg{position:absolute;top:6px;left:5px;width:22px;height:22px;color:#aaa;} +aside .back-text{display:none;position:absolute;top:0;left:0;z-index:99;padding:4px;font-family:Microsoft YaHei,STXihei,STHeiti,Verdana,Arial,Helvetica,sans-serif;font-size:11px;text-align:center;letter-spacing:2px;line-height:14px;background-color:#f4f5f9;cursor:pointer;} +aside .back-text:hover{text-decoration:none;} +aside .back-top:hover .back-icon{border:none;} +aside .back-top:hover .back-text{display:block;color:#aaa;} + +/* ---------- footer ---------- */ +footer{position:absolute;left:0;bottom:0;width:100%;} +footer .signature{position:absolute;right:20px;bottom:8px;color:#999;} /* ---------- lowBrowser ---------- */ #lowBrowser{position:fixed;top:0;left:0;z-index:9999999;width:100%;height:100%;} @@ -142,47 +210,63 @@ footer{position:relative;width:100%;height:15px;} .header .logo i{width:3px;height:3px;margin:9px 0 0 3px;} .header .logo b{max-width:40%;height:auto;padding-left:7px;font-size:14px;background-size:4px 4px;} .header .logo span{margin:-5px 0 0 0;-webkit-transform:scale(0.8);-moz-transform:scale(0.8);-ms-transform:scale(0.8);transform:scale(0.8);} - .header .menu_icon{display:block;position:absolute;right:0;top:0;z-index:1;width:40px;height:47px;} - .header .menu_icon svg{position:absolute;top:9px;left:5px;width:28px;height:28px;color:#acacac;} - .header .menu_icon.close svg{top:14px;left:10px;width:18px;height:18px;} + .header .menu-icon{display:block;position:absolute;right:0;top:0;z-index:1;width:40px;height:47px;} + .header .menu-icon svg{position:absolute;top:9px;left:5px;width:28px;height:28px;color:#acacac;} + .header .menu-icon.close svg{top:14px;left:10px;width:18px;height:18px;} - .container{position:relative;margin:50px auto 0;overflow:hidden;background:#fff;border:none;} + .container{position:relative;margin:50px auto 0;background:#fff;border:none;} /* ---------- menu ---------- */ - .container .nav{display:none;position:fixed;left:0;top:50px;z-index:9999;width:100%;height:100%;padding-bottom:30px;background:rgba(0,0,0,0.8);overflow-y:scroll;} + .container .nav{display:none;position:fixed;left:0;top:50px;z-index:9999;width:100%;height:100%;padding-bottom:30px;background:rgba(0,0,0,0.92);overflow-y:scroll;} .container .nav.on{display:block;} .menu-filter{width:100%;padding-right:39px;background-color:none;box-sizing:border-box;} .menu-filter input{width:90%;width:-webkit-calc(100% - 39px);width:-moz-calc(100% - 39px);width:-ms-calc(100% - 39px);width:calc(100% - 39px);color:#fff;background:none;border:none;border-bottom:#999 1px dashed;} .menu-filter i{right:50px;padding:2px 0 0 10px;} + .menu-search{height:37px;} .menu-search:hover{background-color:transparent;} .menu-search:active{background-color:#f4f5f9;} + .menubar{width:100%;height:auto;padding-bottom:50px;border-right:none;} .menubar i{border-top:1px solid #999;} .menubar i:hover{background-color:transparent;} .menubar i:active{background-color:#8c8888;} .menubar h4,.menubar h5{color:#f5f5f5;border-top:1px solid #999999;border-bottom:none;cursor:pointer;} + .menubar h4,.menubar h5,.menubar ul{max-width:none;} .menubar h4:hover{background-color:#8c8888;} .menubar h4 a{color:#f5f5f5;} .menubar h4.on a{color:#fff;} .menubar ul li a{color:#f5f5f5;} - .menubar ul li a.on{color:#fff;background:#8c8888;} - .menubar ul li a:hover{color:#fff;background:#8c8888;} + .menubar ul li > a.on{color:#fff;} + .menubar ul li > a:hover{color:#fff;} + .menubar ul li > a.on:before{width:3px;} + .menubar ul li > a.on:after{background:#8c8888;} + .menubar ul li > a:hover:after,.menubar ul li > strong:hover:after{background:none;} .menubar ul li > strong{color:#fff;} .menubar ul li > strong:hover{background:none;} + .scroller .scroller-inner{overflow-x:scroll;} + .scroller[data-x-rolling="true"] .scroller-content{min-width:100%;} + .scroller .scrollbar{display:none;} - .main{width:100%;padding:25px 25px 150px;margin-left:0;box-sizing:border-box;} + .main{width:100%;height:auto;margin-left:0;overflow:visible;box-sizing:border-box;} + .main article.markdown-body{padding:25px;} + .main article iframe{border:0;} + .main .search-results{height:auto !important;} .main-sibling{padding:30px 25px 25px;} .main-sibling p{height:22px;} .main-sibling a{padding-right:20px;} + + /* ---------- contents ---------- */ + aside .contents{bottom:75px;border-radius:50%;} + aside .contents-list{right:40px;width:255px;max-height:310px;} + + /* ---------- backTop ---------- */ + aside .back-top{border-radius:50%;bottom:32px;} + aside .back-top:hover .back-text{display:none;} /* ---------- footer ---------- */ - footer{position:relative;width:100%;height:0;} - .signature{position:absolute;right:12px;bottom:6px;color:#ccc;} - .signature a{color:#aaa;} - - /* ---------- backTop ---------- */ - .back-top{border-radius:50%;bottom:32px;} - .back-top:hover .back-text{display:none;} + footer{position:relative;left:0;bottom:0;width:100%;height:0;} + footer .signature{position:absolute;right:12px;bottom:6px;color:#ccc;} + footer .signature a{color:#aaa;} } diff --git a/files/amWiki.docs.js b/files/amWiki.docs.js index 831d67c..3204010 100644 --- a/files/amWiki.docs.js +++ b/files/amWiki.docs.js @@ -1,5 +1,5 @@ /** - * @desc 文档加载渲染模块 + * amWiki Web端 - 文档加载与渲染模块 * @author Tevin */ @@ -13,23 +13,32 @@ var URL_ENCODE_NAME = 'AMWikiUrlEncode'; //记录url编码的键名 /** - * @class 创建一个文档管理对象 + * 文档管理 + * @constructor */ var Docs = function () { this.$e = { win: $(win), - //markdown文档内容容器 - view: $('#view') + //markdown 文档内容容器 + view: $('#view'), + //网页 title 标签 + title: $('title'), + //目录悬浮窗标题 + contentsTitle: $('#contentsTitle') }; this.data = { + //记录页面宽度 pageWidth: 0 }; - this.initUrlEncode(); - this.initHashEvent(); + this._initUrlEncode(); + this._initHashEvent(); }; - //初始化url编码类型标记 - Docs.prototype.initUrlEncode = function () { + /** + * 初始化url编码类型标记 + * @private + */ + Docs.prototype._initUrlEncode = function () { /* * 由于win与linux采用不同编码保存中文文件名 * 记录浏览器打开当前域名的wiki时服务器中文文件名的编码类型 @@ -43,8 +52,11 @@ delete localStorage.urlEcode }; - //修正移动端hash变化时滚动位置 - Docs.prototype.initHashEvent = function () { + /** + * 修正移动端hash变化时滚动位置 + * @private + */ + Docs.prototype._initHashEvent = function () { var that = this; this.data.pageWidth = this.$e.win.width(); this.$e.win.on('resize', function () { @@ -64,11 +76,33 @@ }); }; - //设置文档h1、h2、h3描记 - Docs.prototype.setTitlesAnchor = function () { + /** + * 转换链接文本 + * @param {String} str + * @returns {String} + * @private + */ + Docs.prototype._tramsformLinkText = function (str) { + return str + .replace(/^\s+|\s+$/g, '') //去除首尾空格 + .replace(/'/g, ''') //转义单引号 + .replace(/"/g, '"') //转义双引号,由于双引号无法正确传递给html属性,当作为hash时将删除处理 + .replace(/\(/g, '(') //转义左圆括号 + .replace(/\)/g, ')') //转义右圆括号 + .replace(/\[/g, '[') //转义左中括号 + .replace(/\]/g, ']'); //转义右中括号 + }; + + /** + * 设置文档h1、h2、h3描记 + * @returns {string} + * @private + */ + Docs.prototype._setTitlesAnchor = function () { var that = this; var $titles = null; var hash = ''; + var contentsMd = ''; //提取目录为markdown字符串 if (location.hash && location.hash.length > 1) { hash = location.hash.split('#')[1]; } @@ -77,11 +111,18 @@ $titles = that.$e.view.find('h1,h2,h3'); $titles.each(function (index, element) { var $this = $(element); - var text = $.trim($this.text()); + var text1 = that._tramsformLinkText($this.text()); + var text2 = text1.replace(/"/g, ''); //删除双引号 + //提取目录 + if ($this.is('h2')) { + contentsMd += '1. [' + text1 + '](#' + text2 + ' "' + text2 + '")\n'; + } else if ($this.is('h3')) { + contentsMd += '\t1. [' + text1 + '](#' + text2 + ' "' + text2 + '")\n'; + } //设置描记 - $this.prepend(anchorHtml.replace(/\{title\}/g, text)); + $this.prepend(anchorHtml.replace(/{title}/g, text2)); //首次打开页面滚动位置修正 - if (hash == text) { + if (hash == $this.text().replace(/"/g, '')) { if (that.data.pageWidth <= 720) { that.$e.win.scrollTop($this.offset().top - 55); } else { @@ -89,10 +130,82 @@ } } }); + if (contentsMd.indexOf('\t') == 0) { + contentsMd = '1.  \n' + contentsMd; + } + return contentsMd; }; - //设置js注释隐藏 - Docs.prototype.setJSCommentDisable = function ($elm) { + /** + * 创建脚注 + * @param {String} text + * @returns {String} + * @private + */ + Docs.prototype._setFootnote = function (text) { + var footnotes = []; + var noteReg = /\[\^([ a-zA-Z\d]+)]/g; + var footReg = /\[\^([ a-zA-Z\d]+)]: ?([\S\s]+?)(?=\[\^(?:[ a-zA-Z\d]+)]|\n\n|$)/g; + var templates = $.trim($('#template\\:footnote').text()).split(/[\r\n]+\s*/g); + templates[4] += templates[5] + templates[6] + templates[7] + templates[8]; + var html = ''; + //提取脚注内容 + text = text.replace(footReg, function (match, s1, s2, index) { + var title = ''; + s2 = s2.replace(/"(.*?)"\s*$/, function (m, ss1) { + title = ss1; + return ''; + }); + footnotes.push({ + index: index, + note: s1, + content: s2, + title: title, + used: false + }); + //从页面上删除底部脚注内容 + return ''; + }); + //将脚注的标记转为序号 + text = text.replace(noteReg, function (match, s1) { + for (var i = 0, foot; foot = footnotes[i]; i++) { + if (foot.note == s1) { + foot.used = true; + return templates[0].replace(/{{index}}/g, i + 1 + '').replace('{{title}}', foot.title); + } + } + //当脚注的正文不存在,视标记文本为正文 + var length = footnotes.push({ + index: 0, + note: s1, + content: s1, + used: true + }); + return templates[0].replace(/{{index}}/g, length + ''); + }); + //生成底部脚注html + if (footnotes.length >= 1) { + for (var i = 0, foot; foot = footnotes[i]; i++) { + if (foot.used) { + html += templates[2] + .replace('{{index}}', i + 1) + .replace('{{content}}', foot.content) + .replace('{{back}}', templates[4].replace('{{index}}', i + 1)); + } else { + html += templates[3].replace('{{content}}', foot.content); + } + } + html = templates[1].replace('{{list}}', html); + } + return text + '\n
    ' + html; + }; + + /** + * 设置js代码块注释显示隐藏 + * @param {Object} $elm + * @private + */ + Docs.prototype._setJSCommentDisable = function ($elm) { var $disBtn = $('
    /
    '); $disBtn.on('click', function () { var $this = $(this); @@ -107,8 +220,12 @@ $elm.prepend($disBtn); }; - //解析流程图 - Docs.prototype.createFlowChart = function ($elm) { + /** + * 解析流程图 + * @param {Object} $elm + * @private + */ + Docs.prototype._createFlowChart = function ($elm) { var code = $elm.text(); $elm.text(''); var id = 'flowChart' + parseInt(Math.random() * 500); @@ -116,7 +233,7 @@ var chart = flowchart.parse(code); chart.drawSVG(id, { 'line-width': 1.3, - 'line-length': 55, + 'line-length': 56, 'line-color': '#666', 'text-margin': 10, 'font-size': 12, @@ -134,30 +251,159 @@ }); }; - //编码url - Docs.prototype.encodeUrl = function (path, type) { - var url = ''; + /** + * 解析 Markdown 目录 + * @param {String} html + * @returns {String} + * @private + */ + Docs.prototype._setTOC = function (html) { + return html.replace(/\[(TOC|MENU)]/g, '
    '); + }; + + /** + * 解析 Markdown 自定义图片大小与对齐方式 + * @param {String} html + * @returns {String} + * @private + */ + Docs.prototype._setImgResize = function (html) { + return html.replace(/' : 'checked="true">'; + checkboxHtml += ''; + return checkboxHtml + s3; + }); + }; + + /** + * 解析 markdown 文字飘红 + * @param {String} html + * @returns {String} + * @private + */ + Docs.prototype._setRedText = function (html) { + return html.replace(/==(.*?)==/g, function (m, s1) { + return '' + s1 + ''; + }); + }; + + /** + * 创建目录 + * @param {String} contents - markdown 目录 + * @private + */ + Docs.prototype._createContents = function (contents) { + var $contents = $('.markdown-contents').html(marked(contents)); + $('blockquote').each(function () { + var $this = $(this); + var $links = $this.find('ol>li>a'); + //至少2条链接才视为目录 + if ($links.length > 1) { + $this.addClass('markdown-contents'); + $contents = $contents.add($this); + } + }); + //自带序号的目录,不再额外显示一层序号 + $contents.find('ol').each(function () { + var $this = $(this); + var $links = $this.children('li').children('a'); + var text1 = $links.eq(0).text(), + text2 = $links.eq(1).text(); + var conditions = [ + //普通数字类型 + /^[\((]?(\d+\.?)+[^\d]{2,}/, + //汉字序号类型 + /^[\((【第]?[一二三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]+/, + //英文序号类型 + /^(chapter)|(section)|(part)|(step)/i, + //罗马序号类型 + /^[ⅠⅡⅢⅣIⅤⅥⅦⅧⅨⅩⅪⅫLCDM]+\.?/ + ]; + for (var i = 0, cond; cond = conditions[i]; i++) { + if (cond.test(text1) && cond.test(text2)) { + $this.addClass('unindex'); + break; + } + } + }); + //没写序号的目录,创建序号 + var setIndex = function ($elm, index1) { + if ($elm.length == 0) { + return; + } + $elm.children('li').each(function (index2) { + var $this = $(this); + if (!$elm.hasClass('unindex')) { + var index = typeof index1 == 'number' ? (index1 + 1) + '.' + (index2 + 1): (index2 + 1) + '.'; + $this.prepend('' + index + ''); + } + setIndex($this.children('ol'), index2); + }); + }; + $contents.children('ol').each(function () { + setIndex($(this)); + }); + }; + + /** + * 编码 url + * 由于服务器可能存在 GBK 或 UTF-8 两种编码,中文路径编码不对需要切换才能访问 + * @param {String} path + * @param {String} type - 是否需要编码类型反转, GBK、UTF-8切换 + * @returns {String} + * @private + */ + Docs.prototype._encodeUrl = function (path, type) { + var url = 'library/'; + if (typeof AWConfig.libraryPrefix == 'string' && AWConfig.libraryPrefix.length > 0) { + url = AWConfig.libraryPrefix.replace(/\\/g, '/').replace(/\/?&/, '/'); + } var paths = []; //正常编码 if (type == 'normal') { if (localStorage[URL_ENCODE_NAME] == 'utf8') { - url = 'library/' + encodeURI(path); + url += encodeURI(path); } else if (localStorage[URL_ENCODE_NAME] == 'gbk') { - paths = path.split('/'); - url = 'library/' + GBK.encode(paths[0]); - url += paths[1] ? '/' + GBK.encode(paths[1]) : ''; - url += paths[2] ? '/' + GBK.encode(paths[2]) : ''; + paths = path.split('/').map(function (path) { + return GBK.encode(path); + }); + url += paths.join('/'); } } //反转编码 else if (type == 'reverse') { if (localStorage[URL_ENCODE_NAME] == 'utf8') { - paths = path.split('/'); - url = 'library/' + GBK.encode(paths[0]); - url += paths[1] ? '/' + GBK.encode(paths[1]) : ''; - url += paths[2] ? '/' + GBK.encode(paths[2]) : ''; + paths = path.split('/').map(function (path) { + return GBK.encode(path); + }); + url += paths.join('/'); } else if (localStorage[URL_ENCODE_NAME] == 'gbk') { - url = 'library/' + encodeURI(path); + url += encodeURI(path); } } url += '.md?t=' + (new Date()).getTime(); @@ -165,38 +411,66 @@ }; /** - * @desc 渲染文档 - * @param content {string} - 需要渲染的文档内容 + * 渲染文档 + * @param {String} content - 需要渲染的文档内容 + * @public */ Docs.prototype.renderDoc = function (content) { var that = this; + var html = ''; this.cleanView(); - this.$e.view - .html(marked(content)) - .find('pre code').each(function (i, element) { - var $elm = $(element); - var className = $elm.attr('class') || ''; - //流程图 - if (className.indexOf('lang-flow') >= 0) { - that.createFlowChart($elm); - } - //语法高亮 - else if (className.indexOf('lang') >= 0) { - hljs.highlightBlock(element); - } - //js注释开关 - className = $elm.attr('class') || ''; - if (className.indexOf('javascript') >= 0) { - that.setJSCommentDisable($elm); - } - }); + //增加"\"符转义功能 + content = content.replace(/\\(.)/g, function (m, s1) { + return '&#' + s1.charCodeAt(0) + ';'; + }); + //创建脚注 + content = this._setFootnote(content); + //编译 markdown + html = marked(content); + //创建目录标记,和悬浮窗格式统一 + html = this._setTOC(html); + //自定义图片大小与对齐方式 + html = this._setImgResize(html); + //复选框 + html = this._setCheckbox(html); + //文字飘红 + html = this._setRedText(html); + //填充到页面 + this.$e.view.html(html); + //功能化代码块 + this.$e.view.find('pre code').each(function (i, element) { + var $elm = $(element); + var className = $elm.attr('class') || ''; + //创建流程图 + if (className.indexOf('lang-flow') >= 0) { + that._createFlowChart($elm); + } + //创建语法高亮 + else if (className.indexOf('lang') >= 0) { + hljs.highlightBlock(element); + } + //创建js注释开关 + className = $elm.attr('class') || ''; + if (className.indexOf('javascript') >= 0) { + that._setJSCommentDisable($elm); + } + }); //设置网页title - $('title').text(this.$e.view.find('h1').eq(0).text()); - //设置描点 - this.setTitlesAnchor(); + var title = this.$e.view.find('h1').eq(0).text(); + this.$e.title.text(title); + this.$e.contentsTitle.text(title).attr('href', '#' + title.replace(/"/g, '')); + //创建描点 + var contents = this._setTitlesAnchor(); + //创建目录 + this._createContents(contents); }; - //读取文档 + /** + * 读取文档 + * @param {String} url + * @param {Function} callback + * @public + */ Docs.prototype.getDoc = function (url, callback) { var that = this; var ajaxData = { @@ -220,23 +494,21 @@ }; /** - * @callback loadPageCallback - * @param type {string} - 加载页面最终状态,success、error 两种 - * @param content {string} - 加载页面成功时,传递加载的内容 - */ - /** - * @desc 加载指定页面 - * @param path {string} - 页面资源地址 - * @param callback {loadPageCallback} - 加载完成后的回调 + * 加载指定页面 + * @param {String} path - 页面资源地址 + * @param {Function} callback - 加载完成后的回调,包含参数: + * type - 加载页面最终状态,success、error 两种 + * content - 加载页面成功时,传递加载的内容 + * @public */ Docs.prototype.loadPage = function (path, callback) { //console.log(path); var that = this; - var url = this.encodeUrl(path, 'normal'); + var url = this._encodeUrl(path, 'normal'); this.getDoc(url, function (type, data) { if (type == 'fail') { //如果第一失败,转换url编码类型后发送第二次请求 - var url = that.encodeUrl(path, 'reverse'); + var url = that._encodeUrl(path, 'reverse'); that.getDoc(url, function (type) { //第二次仍然失败,视为打开文档失败 if (type == 'fail') { @@ -255,7 +527,8 @@ }; /** - * @desc 清理页面 + * 清理页面 + * @public */ Docs.prototype.cleanView = function () { this.$e.view.find('.lang-off-js-comment').off('click'); diff --git a/files/amWiki.imgsView.js b/files/amWiki.imgsView.js new file mode 100644 index 0000000..78fdb03 --- /dev/null +++ b/files/amWiki.imgsView.js @@ -0,0 +1,155 @@ +/** + * amWiki Web端 - 图片查看模块 + * @author Tevin + */ + +;(function (win, $) { + + 'use strict'; + + /** + * 图片查看器 + * @param {Element} _this + * @constructor + */ + var ImgsView = function (_this) { + this.$e = { + container: $(_this), //文档主容器 + imgList: null, //页面图片列表 + imgsView: $('#imgsView'), //图片弹窗层 + imgsViewInner: null, //图片容器 + btnPrev: null, + btnNext: null + }; + this._data = { + winW: 0, + winH: 0, + curIndex: -1 + }; + this._init(); + }; + + /** + * 初始化 + * @private + */ + ImgsView.prototype._init = function () { + var that = this; + this.$e.imgsViewInner = this.$e.imgsView.find('#imgsViewInner'); + this.$e.btnPrev = this.$e.imgsView.find('.prev'); + this.$e.btnNext = this.$e.imgsView.find('.next'); + this._data.winW = $(win).width(); + this._data.winH = $(win).height(); + this.$e.container.on('click', function (e) { + if (e.target.tagName.toLowerCase() == 'img') { + //抓取图片列表 + that.$e.imgList = that.$e.container.find('img'); + that._data.curIndex = that.$e.imgList.index(e.target); + that.open(); + } + }); + this.$e.imgsView.children('.imgsv-background').on('click', function () { + that.close(); + }); + this.$e.imgsView.find('.original').on('click', function () { + that.resizeAs('org'); + }); + this.$e.imgsView.find('.suit').on('click', function () { + that.resizeAs('suit'); + }); + this.$e.imgsView.find('.prev').on('click', function () { + var $this = $(this); + if (!$this.hasClass('off')) { + that.playTo(that._data.curIndex - 1); + } + }); + this.$e.imgsView.find('.next').on('click', function () { + var $this = $(this); + if (!$this.hasClass('off')) { + that.playTo(that._data.curIndex + 1); + } + }); + }; + + /** + * 显示图片浏览弹层 + * @public + */ + ImgsView.prototype.open = function () { + var that = this; + this.playTo(this._data.curIndex); + this.$e.imgsView.fadeIn(80); + setTimeout(function () { + that.$e.imgsView.addClass('on'); + }, 50); + }; + + /** + * 关闭图片浏览弹层 + * @public + */ + ImgsView.prototype.close = function () { + var that = this; + this.$e.imgsView.removeClass('on'); + setTimeout(function () { + that.$e.imgsView.fadeOut(50); + }, 200); + that.$e.imgList = null; + that._data.curIndex = -1; + }; + + /** + * 切换图片 + * @param {Number} index + * @public + */ + ImgsView.prototype.playTo = function (index) { + if (index == 0) { + this.$e.btnPrev.addClass('off'); + } else { + this.$e.btnPrev.removeClass('off'); + } + if (index == this.$e.imgList.length - 1) { + this.$e.btnNext.addClass('off'); + } else { + this.$e.btnNext.removeClass('off'); + } + this.$e.imgCur = this.$e.imgList.eq(index).clone().removeAttr('align'); + this.resizeAs('suit'); + this.$e.imgsViewInner.html(this.$e.imgCur); + this._data.curIndex = index; + }; + + /** + * 设置图片尺寸 + * @param {String} type - 尺寸模式:suit 适合 / org 原大小 + * @public + */ + ImgsView.prototype.resizeAs = function (type) { + if (type == 'suit') { + this.$e.imgCur.removeAttr('width').removeAttr('height'); + if (this.$e.imgCur[0].naturalWidth > this._data.winW - 60) { + this.$e.imgCur.css('max-width', this._data.winW - 60); + } + if (this.$e.imgCur[0].naturalHeight > this._data.winH - 60) { + this.$e.imgCur.css('max-height', this._data.winH - 60); + } + } else if (type == 'org') { + this.$e.imgCur.removeAttr('style'); + this.$e.imgCur.attr({ + 'width': this.$e.imgCur[0].naturalWidth, + 'height': this.$e.imgCur[0].naturalHeight + }); + } + }; + + //注册到 jQuery + $.extend($.fn, { + imgsView: function () { + return this.each(function () { + return new ImgsView(this); + }); + } + }); + +})(window, jQuery); \ No newline at end of file diff --git a/files/amWiki.js b/files/amWiki.js index 429dff2..bfa65c3 100644 --- a/files/amWiki.js +++ b/files/amWiki.js @@ -1,5 +1,5 @@ /** - * @desc amWiki Web端·主执行模块 + * amWiki Web端 - 入口模块 * @author Tevin * @see {@link https://github.com/TevinLi/amWiki} * @license MIT - Released under the MIT license. @@ -9,6 +9,10 @@ $(function () { 'use strict'; + /* + 引入 + */ + //工具集 var tools = window.tools; //文档管理器 @@ -24,21 +28,46 @@ $(function () { //是否支持history.state的API (IE9不支持) var HISTORY_STATE = 'pushState' in history; - //页面基本显示与操作 - var $menuBar = $('#menuBar'); - var pageBase = function () { - //菜单折叠 - $menuBar.on('click', 'h5', function () { - var $this = $(this), - $next = $this.next('ul'); - if ($this.hasClass('on')) { - $this.removeClass('on'); - $next.slideUp(200); + /* + 页面基本 + */ + + //页面元素 + var $win = $(window), + $body = $('body'), + $menuIcon = $('#menuIcon'), //顶部折叠显示导航按钮 + $container = $('#container'), //页面主容器 + $nav = $('#nav'), //左侧导航 + $menuBar = $('#menuBar'), //左侧导航内容 + $filter = $('#menuFilter'), + $filterClean = $filter.next('i'), + $main = $('#main'), + $mainInner = $main.children('.main-inner'), + $mainSibling = $('#mainSibling'), //其他文章 + $contents = $('#contents'); //目录 + + //是否为移动端 + var isMobi = window.isMobi = (function () { + var winW = $win.width(); + var threshold = 720; + var onResize = function () { + winW = $win.width(); + if (winW <= threshold) { + $container.removeAttr('style'); } else { - $this.addClass('on'); - $next.slideDown(200); + $container.height($win.height() - 70 - 15 - 20 * 2); } - }); + }; + onResize(); + $win.on('resize', onResize); + return function () { + return winW <= threshold; + }; + })(); + + //页面基本显示与操作 + (function () { + //菜单折叠 $menuBar.on('click', 'h4', function () { var $this = $(this); if (!$this.hasClass('on')) { @@ -47,90 +76,76 @@ $(function () { } $menuBar.find('h5').removeClass('on').next('ul').hide(); }); + $menuBar.on('click', 'h5', function () { + var $this = $(this), + $next = $this.next('ul'); + if ($this.hasClass('on')) { + $this.removeClass('on'); + $next.slideUp(200, function () { + $menuBar.trigger('scrollbar'); + }); + } else { + $this.addClass('on'); + $next.slideDown(200, function () { + $menuBar.trigger('scrollbar'); + }); + } + }); $menuBar.on('click', 'strong', function () { var $this = $(this), $next = $this.next('ul'); if ($this.hasClass('on')) { $this.removeClass('on'); - $next.slideUp(200); + $next.slideUp(200, function () { + $menuBar.trigger('scrollbar'); + }); } else { $this.addClass('on'); - $next.slideDown(200); + $next.slideDown(200, function () { + $menuBar.trigger('scrollbar'); + }); } }); - //展开折叠所有导航栏位按钮 - /*$('.menu-fold').on('click', function () { - var $this = $(this); - if ($this.hasClass('on')) { - $this.removeClass('on').find('use').attr('xlink:href', '#navFolder1'); - $menuBar.find('h5').removeClass('on').next('ul').hide(); - } else { - $this.addClass('on').find('use').attr('xlink:href', '#navFolder2'); - $menuBar.find('h5').addClass('on').next('ul').show(); - } - });*/ //响应式菜单 - var $nav = $('.nav'), - $menuIcon = $('.menu_icon'); $menuIcon.on('click', function () { var $this = $(this); if ($this.hasClass('close')) { - $this.removeClass('close'); - $menuIcon.find('use').attr('xlink:href', '#icon:navStart'); + $this.removeClass('close') + .find('use').attr('xlink:href', '#icon:navStart'); $nav.removeClass('on'); } else { - $this.addClass('close'); - $menuIcon.find('use').attr('xlink:href', '#icon:navClose'); + $this.addClass('close') + .find('use').attr('xlink:href', '#icon:navClose'); $nav.addClass('on'); } }); - $menuBar.on('click', 'li a', function () { + $nav.on('navchange searchon searchoff', function (e) { + $menuIcon.removeClass('close') + .find('use').attr('xlink:href', '#icon:navStart'); $nav.removeClass('on'); }); - $menuBar.on('click', 'h4', function () { - $nav.removeClass('on'); - }); - //页面筛选 - var $filter = $('#menuFilter'); - var $filterClean = $filter.next('i'); - $filter.on('blur change input propertychange', function () { - var value = $filter.val(); - if (value != '') { + //筛选操作 + $filter.on('input propertychange input2', function () { + var value = $filter.val().replace(/([\(\)\[\]\^\$\+])/g, '\\$1'); + var valReg = new RegExp('(' + $.trim($filter.val()).split(/[ ,]/).join('|') + ')', 'ig'); + if (value != '' && !/^\s$/g.test(value)) { $filterClean.removeClass('off'); - $menuBar.find('h5').removeClass('on').next('ul').hide(); - $menuBar.find('a').each(function () { - var $this = $(this); - if ($this.text().indexOf(value) >= 0) { - $this.html($this.text().replace(value, '' + value + '')); - var $prev = $this.parent().removeClass('off').parent().show().prev(); - //一级目录 - if ($prev.is('h5')) { - $prev.addClass('on'); - } - //二级目录 - else if ($prev.is('strong')) { - $prev.addClass('on').parent().removeClass('off').parent().show().prev().addClass('on'); - } - } else { - if ($this.find('mark').length > 0) { - $this.text($this.text()); - } - $this.parent().addClass('off'); - } + $menuBar.find('h5').each(function () { + filterNav('filter', valReg, $(this)); }); + storage.setStates('navFilterKey', value); } else { $filterClean.addClass('off'); - $menuBar.find('a').each(function () { - var $this = $(this); - if ($this.find('mark').length > 0) { - $this.text($this.text()); - } + $menuBar.find('h5').each(function () { + filterNav('open', null, $(this)); }); - $menuBar.find('a').parent().removeClass('off'); + storage.setStates('navFilterKey'); } + $menuBar.trigger('scrollbar'); }); + //清空筛选 $filterClean.on('click', function () { - $filter.val('').trigger('change'); + $filter.val('').trigger('input2'); }); //显示svg图标 if (sessionStorage['AMWikiIconsSvg']) { @@ -139,12 +154,168 @@ $(function () { $.get('amWiki/images/icons.svg', function (svg) { sessionStorage['AMWikiIconsSvg'] = svg; $('#svgSymbols').append(svg); - }, 'text'); + }, 'text').fail(function () { + if (typeof AWPageMounts != 'undefined') { + sessionStorage['AMWikiIconsSvg'] = AWPageMounts['icon'].content; + $('#svgSymbols').append(AWPageMounts['icon'].content); + } + }); + } + //目录悬浮窗展开折叠 + $contents.children('.btn').on('click', function (e) { + $contents.toggleClass('on').removeClass('hover'); + }); + $contents.hover(function () { + $contents.addClass('hover'); + }, function () { + $contents.removeClass('hover'); + }); + //开启滚动条 + $('.scroller').scrollbar(); + $('#backTop').on('click', function () { + $mainInner.scrollTop(0); + }); + //图片放大 + $main.imgsView(); + //全局点击 + $(document).on('click', function (e) { + var $tag = $(e.target); + //移动端 + if (isMobi()) { + //折叠目录悬浮窗 + if ($tag.closest('#contents').length == 0) { + $contents.removeClass('on').removeClass('on'); + } + } + }); + })(); + + /* + 业务操作函数 + */ + + /** + * 向下递归进行导航筛选 + * 当类型为筛选时,必定有正则 + * 当文件夹匹配时,其所属链接和所有子级全部显示且显示匹配 + * 当文件夹不匹配时,其所属链接和当前子级仅显示匹配,隐藏不匹配的项,下一级继续筛选 + * 当类型为打开时,所属链接和子级一律全部显示不隐藏 + * 如果有正则,显示当前匹配 + * 如果无正则,清除匹配 + * @param {String} type - 筛选类型,有 filter / open 两个值 + * @param {regexp} valReg - 过滤筛选的正则 + * @param {Object} $title - jquery 对象,“标题-列表”DOM结构中的标题 + */ + var filterNav = function (type, valReg, $title) { + var $ul = $title.next('ul'); + //因为一级文件夹和子级文件夹DOM结构不同,所以区分对待 + // 显示上,strong 带 on 加粗显示,h5 加 off 隐藏 + // 文本操作使用变量 $span,其他操作使用变量 $title + var $span = $title.find('span'); + //当类型为筛选时 + if (type == 'filter' && valReg) { + //当文件夹标题匹配时 + if (valReg.test($span.text())) { + $span.html($span.text().replace(valReg, '$1')); + $title.addClass('on').removeClass('off'); + //所属链接全部显示,且显示匹配 + $ul.show().find('> li > a').each(function () { + var $this = $(this); + var $span2 = $this.find('span'); + $span2.html($span2.text().replace(valReg, '$1')); + $this.parent().removeClass('off'); + }); + //父级显示 + showNavParents($title); + //下一级筛选类型更改,文件和文件夹全部显示,且显示匹配 + $ul.find('> li > strong').each(function () { + filterNav('open', valReg, $(this)); + }); + } + //当文件夹标题不匹配时 + else { + $span.text($span.text()); + $title.removeClass('on'); + //隐藏父级或隐藏h5 + if ($title.is('h5')) { + $title.parent().addClass('off'); + } else { + $title.addClass('off'); + } + //所属链接仅显示匹配的 + $ul.hide().find('> li > a').each(function () { + var $this = $(this); + var $span2 = $this.find('span'); + if (valReg.test($this.text())) { + $span2.html($span2.text().replace(valReg, '$1')); + $this.parent().removeClass('off'); + //存在匹配时父级才显示 + showNavParents($ul.show().prev()); + } else { + $span2.text($span2.text()); + $this.parent().addClass('off'); + } + }); + //下一级继续完全筛选 + $ul.find('> li > strong').each(function () { + filterNav('filter', valReg, $(this)); + }); + } + } + //当类型为打开,显示全部链接和文件夹 + else if (type == 'open') { + $title.removeClass('off'); + if ($title.hasClass('on')) { + $ul.show(); + } else { + $ul.hide(); + } + //当存在正则时,显示匹配 + if (!!valReg) { + $span.html($span.text().replace(valReg, '$1')); + if (valReg.test($span.text())) { + $ul.show(); //当文件夹名称命中,展开文件夹 + } + $ul.find('> li > a').each(function () { + var $this = $(this); + var $span2 = $this.find('span'); + $span2.html($span2.text().replace(valReg, '$1')); + if (valReg.test($this.text())) { + $ul.show(); //当链接名称命中,展开文件夹 + } + $this.parent().removeClass('off'); + }); + //父级显示 + showNavParents($title); + //下一级继续以相同类型显示 + $ul.find('> li > strong').each(function () { + filterNav('open', valReg, $(this)); + }); + } + //当正则不存在,显示所有本级和子级、清除匹配 + else { + $span.text($span.text()); + $ul.find('> li > a').each(function () { + var $span2 = $(this).find('span'); + $span2.text($span2.text()); + }); + $ul.children('li').removeClass('off').children('strong').each(function () { + filterNav('open', null, $(this)); + }); + } + } + }; + //向上递归显示父级 + var showNavParents = function ($title) { + $title.addClass('on').removeClass('off'); + //向上显示直到一级目录 + if (!$title.is('h5')) { + var $prev2 = $title.parent().removeClass('off').parent().show().prev(); + showNavParents($prev2); } }; //改变底部上下篇目 - var $mainSibling = $('#mainSibling'); var changeSibling = function ($item) { //如果未传导航项进来,隐藏上下篇目栏位 if (!$item) { @@ -177,12 +348,14 @@ $(function () { }; setSiblingNav(0, getDocLink('prev', $item)); setSiblingNav(1, getDocLink('next', $item)); - $mainSibling.addClass('on'); + if (testing && !testing.isOpen()) { + $mainSibling.addClass('on'); + } }; //改变导航显示 var changeNav = function (path) { - if (path == '首页') { + if (/^home[-_].*?/.test(path) || path == '首页') { $menuBar.find('h4').addClass('on'); $menuBar.find('a').removeClass('on'); changeSibling(null); @@ -195,10 +368,9 @@ $(function () { hsLink = true; //本层加高亮 var $prev = $this.addClass('on').parent().parent().show().prev().addClass('on'); - //如果本层处于第二层,对应的第一层加高亮 - if ($prev[0].tagName.toLowerCase() == 'strong') { - $prev.parent().parent().show().prev().addClass('on'); - } + //父级高亮 + showNavParents($prev); + //改变上下篇切换 changeSibling($this.parent()); } else { $this.removeClass('on'); @@ -208,78 +380,107 @@ $(function () { $menuBar.find('h4').removeClass('on'); } } + curPath = path; + $menuBar.trigger('scrollbar'); + }; + + //返回首页 + var backHome = function () { + docs.loadPage(homePage.path, function (state, content) { + if (state == 'success') { + changeNav(homePage.path); + docs.renderDoc(content); + storage.saveDoc(homePage.path, content); + $main.trigger('scrollbar'); + } + }); + if (HISTORY_STATE) { + history.replaceState({path: homePage.path}, '', homePage.url); + } }; //改变页面 - var changePage = function (path, withOutPushState) { + var changePage = function (path, withOutPushState, callback) { //第一步,从本地缓存读取并渲染页面 var localDoc = storage.read(path); docs.renderDoc(localDoc); testing && testing.crawlContent(); + $main.trigger('scrollbar'); + $mainInner.scrollTop(0); //返回顶部 //更新history记录 if (!withOutPushState && HISTORY_STATE) { - history.pushState({path: path}, '', '?file=' + path); + var path2 = path.replace(/&/g, '%26'); //对带 & 符号的地址特殊处理 + history.pushState({path: path}, '', '?file=' + path2); } //第二步,加载服务器上的文档资源,如果有更新重新渲染 docs.loadPage(path, function (state, content) { - //读取服务器文档失败 + //读取服务器文档失败时 if (state == 'error') { - //如果本地缓存为空,且服务器文档读取失败,跳回首页 + //如果本地缓存为空 if (localDoc == '') { - docs.loadPage('首页', function (state, content) { - if (state == 'success') { - docs.renderDoc(content); - storage.saveDoc('首页', content); - } - }); - if (HISTORY_STATE) { - history.replaceState({path: '首页'}, '', '?file=首页'); - } + backHome(); } - //如果本地缓存不为空,但服务器文档读取失败 + //如果本地缓存不为空 else { //记录文档打开数 storage.increaseOpenedCount(path); + callback && callback(); } } - //读取服务器文档成功 + //读取服务器文档成功时 else if (state == 'success') { //如果服务器文档有更新,更新本地缓存、重新渲染页面、重新判断接口测试 if (content != localDoc) { docs.renderDoc(content); storage.saveDoc(path, content); testing && testing.crawlContent(); + $main.trigger('scrollbar'); } //如果服务器文档与本地缓存一致,不进行任何操作 - else {} + else { + } //记录文档打开数 storage.increaseOpenedCount(path); + callback && callback(); } }); }; //读取目录导航 + var homePage = {}; var loadNav = function (callback) { - $.get('library/$navigation.md?t=' + Date.now(), function (data) { - $menuBar.html(marked(data)); - $menuBar - .find('h4').prepend('').end() - .find('h5').prepend(''); + var fillNav = function (data) { + $menuBar.find('.scroller-content').html(marked(data)); + //首页 + var menuBarHome = $menuBar.find('h4'); + homePage.text = menuBarHome.text(); + homePage.url = menuBarHome.find('a').attr('href'); + homePage.path = homePage.url.split('file=')[1]; + menuBarHome.prepend(''); + //列表 + $menuBar.find('h5').each(function () { + var $this = $(this); + $this.html('' + $this.text() + '') + }); + $menuBar.trigger('scrollbar'); var pathList = []; //支持history api时,改变默认事件,导航不再跳转页面 $menuBar.find('a').each(function () { + var $this = $(this); + $this.html('' + $this.text() + ''); if (HISTORY_STATE) { - var $this = $(this); var path = $this.attr('href').split('file=')[1]; pathList.push(path); $this.on('click', function () { search.displayBox('off'); //关闭搜索面板 changeNav(path); changePage(path); + $this.trigger('navchange'); return false; }); } }); + //上下翻页不再跳页面 $mainSibling.find('a').on('click', function () { if (HISTORY_STATE) { var $this = $(this); @@ -288,22 +489,165 @@ $(function () { var path = href.split('file=')[1]; changeNav(path); changePage(path); + $this.trigger('navchange'); } return false; } }); + $menuBar.find('strong').each(function () { + var $this = $(this); + $this.html('' + $this.text() + ''); + }); + //搜索结果不再跳转页面 + $('#results').on('click', 'a', function () { + if (HISTORY_STATE) { + var $this = $(this); + var href = $this.attr('href'); + if (typeof href != 'undefined' && href != '') { + var path = href.split('file=')[1]; + search.displayBox('off'); //关闭搜索面板 + changeNav(path); + changePage(path); + $this.trigger('navchange'); + } + return false; + } + }); + //设置导航筛选初始值 + var filterVal = storage.getStates('navFilterKey'); + if (typeof filterVal != 'undefined' && filterVal != '') { + $filter.val(filterVal).trigger('input2'); + } + //回调 callback && callback(pathList); - }, 'text'); + }; + $.get('library/$navigation.md?t=' + Date.now(), fillNav, 'text').fail(function () { + if (typeof AWPageMounts != 'undefined') { + fillNav(AWPageMounts['nav'].content); + } + }); }; + //读取页面挂载数据文档部分 + var loadPageMounts = function () { + if (typeof AWPageMounts == 'undefined') { + return; + } + //打开页面立即比较页面挂载数据时间与本地缓存更新时间 + // 因为首页总是更新的,如果首页页面挂载数据时间大于本地缓存时间,则挂载数据一定已经经过重建 + var homePath = AWPageMounts['home'].name.replace(/\.md$/, ''); + var homeStorageTime = storage.readTime(homePath); + //如果页面挂载数据经过了重建,开始更新,读取所有挂载数据插入到本地缓存,完成后删除释放资源占用 + if (AWPageMounts['home'].timestamp > homeStorageTime) { + //首页 + storage.saveDoc(homePath, AWPageMounts['home'].content); + delete AWPageMounts['home']; + //其他文档 + for (var lv1 in AWPageMounts) { + if (!AWPageMounts.hasOwnProperty(lv1)) { + continue; + } + if (!/^m\d+/.test(lv1)) { + continue; + } + for (var i = 0, page; page = AWPageMounts[lv1][i]; i++) { + storage.saveDoc(page.path.replace(/\.md$/, ''), page.content); + } + delete AWPageMounts[lv1]; + } + storage.saveRebuild(); + } + //否则直接删除页面挂载数据的文档部分,释放内存资源占用 + else { + delete AWPageMounts['home']; + for (var name in AWPageMounts) { + if (AWPageMounts.hasOwnProperty(name)) { + if (/^m\d+/.test(name)) { + delete AWPageMounts[name]; + } + } + } + } + }; + + //根据hash改变滚动位置 + var changeScrollByHash = function () { + var hash = location.hash.split('#')[1]; + //当不存在hash + if (!hash || hash.length == '') { + //检测是否在顶部,不在顶部滚动至顶部 + if ($mainInner.scrollTop() != 0) { + $mainInner.scrollTop(0); + } + return; + } + //获取hash指向的元素 + var $hash = $('.anchor[name="' + hash + '"]'); + if ($hash.length == 0) { + return + } + //滚动至元素 + $mainInner.scrollTop($hash.position().top + $mainInner.scrollTop() - 10); + }; + + /* + 启动应用 + */ + //解析地址参数 - var path = tools.getURLParameter('file'); - if (!path) { - path = '首页'; - } else { - path = decodeURI(path); + var curPath = tools.getURLParameter('file'); + curPath = !curPath ? '首页' : decodeURI(curPath); + curPath = curPath.replace(/%26/g, '&'); + + //加载导航 + loadNav(function (list) { + //核对本地存储 + storage.checkLibChange(list); + //读取页面挂载数据文档部分 + loadPageMounts(); + //首次打开改变导航 + changeNav(curPath); + //首次打开改变页面 + changePage(curPath, true, changeScrollByHash); + }); + + //history api 浏览器前进后退操作响应 + if (HISTORY_STATE) { + $win.on('popstate', function (e) { + var path; + //当有状态记录时,直接跳转 + if (e.originalEvent.state) { + path = e.originalEvent.state.path; + path = path.replace(/%26/g, '&'); + //改变导航 + changeNav(path); + //改变页面 + changePage(path, true, changeScrollByHash); + } + //当没有状态记录时 + else { + path = tools.getURLParameter('file'); + path = !path ? '首页' : decodeURI(path); + path = path.replace(/%26/g, '&'); + //判断 url 路径是否和当前一样,不一样才跳转 + if (path != curPath) { + //改变导航 + changeNav(path); + //改变页面 + changePage(path, true, changeScrollByHash); + } + //相同时不跳转,根据 hash 变化改变位置 + else { + changeScrollByHash(); + } + } + }); } + /* + 回调中转 + */ + //重建缓存 search.onNeedRebuildStorage = function (callback) { storage.clearLibraries(); @@ -330,29 +674,6 @@ $(function () { } }; - //页面基本 - pageBase(); - //加载导航 - loadNav(function (list) { - //核对本地存储 - storage.checkLibChange(list); - //首次打开改变导航 - changeNav(path); - //首次打开改变页面 - changePage(path, true); - }); - - //history api 浏览器前进后退操作响应 - if (HISTORY_STATE) { - $(window).on('popstate', function (e) { - var path = e.originalEvent.state.path; - //改变导航 - changeNav(path); - //改变页面 - changePage(path, true); - }); - } - }); diff --git a/files/amWiki.scrollbar.js b/files/amWiki.scrollbar.js new file mode 100644 index 0000000..e80745c --- /dev/null +++ b/files/amWiki.scrollbar.js @@ -0,0 +1,288 @@ +/** + * amWiki Web端 - 滚动条模块 + * @author Tevin + */ + +; +(function (win, $) { + + 'use strict'; + + //定时检测内容高度作为补充 + var list = []; + setInterval(function () { + for (var i = 0, item; item = list[i]; i++) { + item.checkHeightWidth(); + } + }, 500); + + /** + * 页面滚动 + * @param {Element} _this + * @constructor + */ + var Scroller = function (_this) { + this.$e = { + container: $(_this), //主容器 + inner: null, //正文内容 + barY: null, //垂直滚动条外层 + boxY: null, //垂直滚动条 + sliderY: null, //垂直滑块 + barX: null, //水平滚动条外层 + boxX: null, //水平滚动条 + sliderX: null //水平滑块 + }; + this._data = { + xRolling: this.$e.container.attr('data-x-rolling') == 'true', + maxTop: 0, //最大top值 + barH: 0, //滚动条高度 + sliderH: 0, //滑块高度 + contentH: 0, //内容高度 + containerH: 0, //容器高度 + contentHLast: 0, //上次内容高度 + barW: 0, //滚动条宽度 + sliderW: 0, //滑块宽度 + contentW: 0, //内容宽度 + containerW: 0, //容器宽度 + contentWLast: 0 //上次内容宽度 + }; + this._init(); + }; + + /** + * 初始化 + * @private + */ + Scroller.prototype._init = function () { + var that = this; + var barHtml = '
    '; + barHtml += this._data.xRolling ? + '
    ' : ''; + this.$e.container.append(barHtml); + //获取元素 + this.$e.barY = this.$e.container.find('.scrollbar-y'); + this.$e.boxY = this.$e.barY.children('div'); + this.$e.sliderY = this.$e.barY.find('i'); + if (this._data.xRolling) { + this.$e.barX = this.$e.container.find('.scrollbar-x'); + this.$e.boxX = this.$e.barX.children('div'); + this.$e.sliderX = this.$e.barX.find('i'); + } + this.$e.inner = this.$e.container.children('.scroller-inner'); + //绑定事件 + this._onWinResize(); + $(win).on('resize', function () { + that._onWinResize(); + that.checkHeightWidth(); + }); + this.$e.container.on('scrollbar', function () { + that.checkHeightWidth(); + }); + this.$e.inner.on({ + 'click': function () { + that.checkHeightWidth(); + }, + 'scroll': function () { + that._reScroll(); + } + }); + this.checkHeightWidth(); + this._bindAction(); + }; + + /** + * 滑块操作 + * @private + */ + Scroller.prototype._bindAction = function () { + var that = this; + var onDrag = false; + var _y = 0, + top = 0, + _x = 0, + left = 0; + this.$e.body = $('body'); + this.$e.boxY.on({ + 'mousedown': function (e) { + onDrag = 'y'; + that.$e.body.attr('onselectstart', 'return false'); + that.$e.barY.addClass('active'); + _y = e.pageY; + top = parseFloat(that.$e.sliderY.css('top')); + if (that.$e.barY.parents('#main').length > 0) { + that.$e.body.children('aside').hide(); + } + } + }); + if (this._data.xRolling) { + this.$e.boxX.on({ + 'mousedown': function (e) { + onDrag = 'x'; + that.$e.body.attr('onselectstart', 'return false'); + that.$e.barX.addClass('active'); + _x = e.pageX; + left = parseFloat(that.$e.sliderX.css('left')); + } + }); + } + $(win.document).on({ + 'mousemove': function (e) { + if (onDrag == 'y') { + that.scrollYTo(top + e.pageY - _y); + } else if (onDrag == 'x') { + that.scrollXTo(left + e.pageX - _x); + } + }, + 'mouseup': function () { + that.$e.body.removeAttr('onselectstart', 'return false') + .children('aside').show(); + if (onDrag == 'y') { + that.$e.barY.removeClass('active'); + } else if (onDrag == 'x') { + that.$e.barX.removeClass('active'); + } + onDrag = false; + } + }); + }; + + /** + * 尺寸缩放 + * @private + */ + Scroller.prototype._onWinResize = function () { + if (isMobi()) { + this.$e.inner.removeAttr('style').removeClass('on'); + } else { + this.$e.inner.css({ + width: this.$e.container.width() + 30, + paddingRight: 13, + paddingBottom: 16 + }).addClass('on'); + } + }; + + /** + * 重设滑块大小 + * @private + */ + Scroller.prototype._resize = function () { + var that = this; + this._data.containerH = this.$e.inner.height(); + //当内容高度小于等于容器时,不显示滚动条 + if (this._data.contentH <= this._data.containerH) { + this._data.contentH = this._data.containerH; + this.$e.barY.addClass('off'); + } else { + this.$e.barY.removeClass('off'); + } + //设置滑块大小 + this._data.barH = this.$e.boxY.height(); + this._data.sliderH = this._data.containerH / that._data.contentH * this._data.barH; + this.$e.sliderY.height(this._data.sliderH); + //计算最大高度 + this._data.maxTop = (this._data.barH - this._data.sliderH) / this._data.barH * this._data.contentH + 1; + //水平方向 + if (this._data.xRolling) { + this._data.containerW = this.$e.inner.width(); + //当内容宽度不超过容器时,不显示滚动条 + if (this._data.contentW <= this._data.containerW) { + this._data.contentW = this._data.containerW; + this.$e.barX.addClass('off'); + } else { + this.$e.barX.removeClass('off'); + } + //设置滑块大小 + this._data.barW = this.$e.boxX.width(); + this._data.sliderW = this._data.containerW / that._data.contentW * this._data.barW; + this.$e.sliderX.width(this._data.sliderW); + } + }; + + /** + * 重设滑块顶部距离 + * @private + */ + Scroller.prototype._reScroll = function () { + var that = this; + var top1 = this.$e.inner.scrollTop(); + var barTop = top1 / this._data.contentH * this._data.barH; + //限制滑动边界 (由事件触发的滚动不会向上过界) + if (barTop + this._data.sliderH >= this._data.barH) { + barTop = this._data.barH - this._data.sliderH; + this.$e.sliderY.css('top', barTop); + this.$e.inner.scrollTop(this._data.maxTop); + } else { + this.$e.sliderY.css('top', barTop); + } + //水平方向 + if (this._data.xRolling) { + var barLeft = this.$e.inner.scrollLeft() / this._data.contentW * this._data.barW; + //水平方向没有鼠标滚动事件 + this.$e.sliderX.css('left', barLeft); + } + }; + + /** + * 垂直滚动到某位置 + * @param {Number} num + * @public + */ + Scroller.prototype.scrollYTo = function (num) { + var barTop = num; + barTop = barTop < 0 ? 0 : barTop; + barTop = barTop + this._data.sliderH > this._data.barH ? this._data.barH - this._data.sliderH : barTop; + this.$e.sliderY.css('top', barTop); + this.$e.inner.scrollTop(barTop / this._data.barH * this._data.contentH); + }; + + /** + * 水平滚动到某位置 + * @param {Number} num + * @public + */ + Scroller.prototype.scrollXTo = function (num) { + var barLeft = num; + barLeft = barLeft < 0 ? 0 : barLeft; + barLeft = barLeft + this._data.sliderW > this._data.barW ? this._data.barW - this._data.sliderW : barLeft; + this.$e.sliderX.css('left', barLeft); + this.$e.inner.scrollLeft(barLeft / this._data.barW * this._data.contentW); + }; + + /** + * 检查高度宽度 + * @public + */ + Scroller.prototype.checkHeightWidth = function () { + var that = this; + this._data.contentH = 0; + this._data.contentW = 0; + this.$e.inner.children('.scroller-content').each(function () { + that._data.contentH += $(this).outerHeight(); + that._data.contentW += $(this).outerWidth(); + }); + //修正 webkit 中滚动条本身占位 + if (/webkit/.test(navigator.userAgent.toLowerCase())) { + that._data.contentH += 15; + } + //如果高度未改变不进行操作 + if (this._data.contentH == this._data.contentHLast && this._data.contentW == this._data.contentWLast) { + return; + } else { + this._data.contentHLast = this._data.contentH; + this._data.contentWLast = this._data.contentW; + } + this._resize(); + this._reScroll(); + }; + + //注册到 jQuery + $.extend($.fn, { + scrollbar: function () { + return this.each(function () { + list.push(new Scroller(this)); + }); + } + }); + +})(window, jQuery); \ No newline at end of file diff --git a/files/amWiki.search.css b/files/amWiki.search.css index 3939039..08b0313 100644 --- a/files/amWiki.search.css +++ b/files/amWiki.search.css @@ -15,6 +15,10 @@ .search-update .text i{font-style:normal;} .search-update input{position:relative;z-index:10;float:right;margin-top:6px;padding:4px 12px;} .search-update input[disabled]{padding:5px 13px;color:#ccc;background:#f2f2f2;border:#aaa 1px solid;border-radius:1px;} +.search-update.off .text:after,.search-update.off .text span,.search-update.off .text i,.search-update.off input{display:none;} +.search-update.off .text b {margin-right:0;} +.search-update.off .text{padding-right:5px;} + .search-box h2{height:65px;color:#666;font-size:24px;font-weight:normal;} /* ---------- search-input ---------- */ @@ -25,7 +29,7 @@ .search-input input[type="button"][disabled]{color:#ccc;background:#f2f2f2;border:#aaa 1px solid;border-radius:1px;} /* ---------- search-results ---------- */ -.search-results{background:#eee;overflow-y:scroll;} +.search-results{overflow-y:scroll;background:#eee;border-top:#eee 1px solid;border-bottom:#eee 1px solid;} .search-result-message{display:none;padding-top:50px;text-align:center;color:#777;font-size:22px;line-height:35px;} .search-list{list-style:none;padding:18px 16px 5px;} .search-item{margin-bottom:14px;background:#fff;border:#e6e6e6 1px solid;} @@ -56,3 +60,6 @@ .search-input{padding-right:90px;} .search-input input[type="button"]{width:86px;} } +@media screen and (max-width:720px){ + .search-box{height:auto;min-height:100%;} +} \ No newline at end of file diff --git a/files/amWiki.search.js b/files/amWiki.search.js index 28e3e9f..f810ff3 100644 --- a/files/amWiki.search.js +++ b/files/amWiki.search.js @@ -1,5 +1,5 @@ /** - * @desc amWiki Web端·全库搜索 + * amWiki Web端 - 全库搜索模块 * @author Tevin */ @@ -9,27 +9,29 @@ 'use strict'; /** - * @class 创建全局搜索 + * 全局搜索 + * @param {Storage} _storage + * @constructor */ var Search = function (_storage) { this._storage = _storage; - this.elm = { + this.$e = { //显示搜索面板按钮 - $searchShow: $('#searchShow'), + searchShow: $('#searchShow'), //更新全部缓存按钮 - $searchUpdate: this._storage.elm.$searchUpdate, + searchUpdate: this._storage.$e.searchUpdate, //搜索面板 - $searchBox: $('#searchBox'), + searchBox: $('#searchBox'), //搜索结果列表 - $results: $('#results'), + results: $('#results'), //搜索结果信息提示 - $resultMsg: $('#resultMsg'), + resultMsg: $('#resultMsg'), //搜素结果显示更多 - $resultMore: $('#resultMore'), + resultMore: $('#resultMore'), //搜索按钮 - $search: $('#search'), + search: $('#search'), //搜索文本 - $searchText: $('#searchText') + searchText: $('#searchText') }; this._data = { //搜素结果 @@ -46,43 +48,52 @@ this.onNeedRebuildStorage = null; }; - //初始化 + /** + * 绑定用户操作 + * @private + */ Search.prototype._bindCtrl = function () { var that = this; //展开折叠搜索面板 - this.elm.$searchShow.on('click', function () { - if (that.elm.$searchBox.hasClass('on')) { + this.$e.searchShow.on('click', function () { + if (that.$e.searchBox.hasClass('on')) { that.displayBox('off'); + that.$e.searchShow.trigger('searchoff'); } else { that.displayBox('on', function () { resetResHeight(); }); + that.$e.searchShow.trigger('searchon'); } }); //设置结果区域高度 var resetResHeight = function () { - var hOut = that.elm.$searchBox.height(); - var dt = that.elm.$results.offset().top - that.elm.$searchUpdate.offset().top; - that.elm.$results.height(hOut - dt); + var hOut = that.$e.searchBox.height(); + var dt = that.$e.results.offset().top - that.$e.searchUpdate.offset().top; + that.$e.results.height(hOut - dt); }; $(win).on('resize', function () { - if (that.elm.$searchBox.hasClass('on')) { + if (that.$e.searchBox.hasClass('on')) { resetResHeight(); } }); + //当本地浏览且存在页面挂载数据时,隐藏重建缓存按钮 + if (location.protocol == 'file:' && typeof AWPageMounts != 'undefined') { + this.$e.searchUpdate.parent().addClass('off'); + } //重建缓存 - this.elm.$searchUpdate.on('click', function () { + this.$e.searchUpdate.on('click', function () { //开启重建缓存时,如果存在搜索子进程,则干掉子进程 if (that._worker) { that._worker.terminate(); that._worker = null; - that.elm.$resultMsg.hide(); + that.$e.resultMsg.hide(); } - that.elm.$search.prop('disabled', true); - that.elm.$searchUpdate.prop('disabled', true); + that.$e.search.prop('disabled', true); + that.$e.searchUpdate.prop('disabled', true); that.onNeedRebuildStorage(function () { - that.elm.$search.prop('disabled', false); - that.elm.$searchUpdate.val('请勿频繁使用'); + that.$e.search.prop('disabled', false); + that.$e.searchUpdate.val('请勿频繁使用'); }); }); //更新全部缓存按钮使用的时间限制:一小时内不允许重复使用 @@ -90,28 +101,33 @@ if (lastBuild) { var lave = Date.now() - lastBuild; if (lave < 60 * 60 * 1000) { - this.elm.$searchUpdate.prop('disabled', true).val('请勿频繁使用'); + this.$e.searchUpdate.prop('disabled', true).val('请勿频繁使用'); } } //点击搜索 - this.elm.$search.on('click', function () { + this.$e.search.on('click', function () { that._search(); }); - this.elm.$searchText.on('keyup', function (e) { + this.$e.searchText.on('keyup', function (e) { if (e.keyCode == 13) { that._search(); } }); //结果翻页 - this.elm.$resultMore.on('click', function () { + this.$e.resultMore.on('click', function () { that._nextResultPage(); }); }; - //显示隐藏搜索面板 + /** + * 显示隐藏搜索面板 + * @param {String} type - on 显示 / off 隐藏 + * @param {Function} callback + * @public + */ Search.prototype.displayBox = function (type, callback) { var that = this; - var $box = this.elm.$searchBox; + var $box = this.$e.searchBox; if (type == 'on' && !$box.hasClass('on')) { $box .addClass('on') @@ -139,66 +155,104 @@ } }; - //启动搜素 + /** + * 启动搜素 + * @private + */ Search.prototype._search = function () { var that = this; - if (this.elm.$searchText.val() == '') { - this.elm.$searchText.focus(); + if (this.$e.searchText.val() == '') { + this.$e.searchText.focus(); return; } - var words = this.elm.$searchText.val(); + var words = this.$e.searchText.val(); + this.$e.resultMsg.show().text('创建搜索中...'); if (typeof win.Worker !== "undefined") { //开启一次新搜索时,如果存在搜索子进程,则干掉子进程 if (this._worker) { this._worker.terminate(); this._worker = null; - this.elm.$resultMsg.hide(); + this.$e.resultMsg.hide(); + } + try { + //创建搜素子进程搜素 + this._worker = new win.Worker('amWiki/js/amWiki.search.worker.js'); + this._searchByWorker(words); + } catch (e) { + //在当前环境搜索 + this._searchByPresent(words); } - this.elm.$resultMsg.show().text('创建搜索中...'); - //创建子进程 - this._worker = new win.Worker('amWiki/js/amWiki.search.worker.js'); - //收到子进程搜素消息 - this._worker.onmessage = function (event) { - var data = event.data; - //加载成功后发送文档数据 - if (data.type == 'loaded') { - that._worker.postMessage({type: 'docs', docs: that._storage.getAllDocs()}); - } - //文档预处理完成后开始搜索 - else if (data.type == 'ready') { - that.elm.$resultMsg.show().html('正在搜索,请稍后...'); - that._worker.postMessage({type: 'search', words: words}); - } - //搜索结果排行 - else if (data.type == 'result') { - that._data.result = data.result; - that._showResultList(); - that._worker.terminate(); - that._worker = null; - } - }; - //子进程出错 - this._worker.onerror = function (e) { - console.error(e); - that._worker.terminate(); - that._worker = null; - }; } else { - this.elm.$resultMsg.show().text('Sorry,您的浏览器不支持搜索!'); - this.elm.$search.prop('disabled', true); + //在当前环境搜索 + this._searchByPresent(words); } - }; - //显示结果列表 + /** + * 搜索子进程通讯 + * @param {String} words + * @private + */ + Search.prototype._searchByWorker = function (words) { + var that = this; + //收到子进程搜素消息 + this._worker.onmessage = function (event) { + var data = event.data; + //加载成功后发送文档数据 + if (data.type == 'searcher:loaded') { + that._worker.postMessage({type: 'searcher:docs', docs: that._storage.getAllDocs()}); + } + //文档预处理完成后开始搜索 + else if (data.type == 'searcher:ready') { + that.$e.resultMsg.show().html('正在搜索,请稍后...'); + that._worker.postMessage({type: 'searcher:search', words: words}); + } + //搜索结果排行 + else if (data.type == 'searcher:result') { + that._data.result = data.result; + that._showResultList(); + that._worker.terminate(); + that._worker = null; + } + }; + //子进程出错 + this._worker.onerror = function (e) { + console.error(e); + this.$e.resultMsg.show().text('Sorry,出错了!
    ' + e.msg); + that._worker.terminate(); + that._worker = null; + }; + }; + + /** + * 在当前环境搜索 + * @param {String} words + * @private + */ + Search.prototype._searchByPresent = function (words) { + var searcher = new AWSearcher(); + searcher.initDocs(this._storage.getAllDocs()); + this.$e.resultMsg.show().html('正在搜索,请稍后...'); + searcher.matchWords(words); + this._data.result = searcher.getResult(); + this._showResultList(); + }; + + /** + * 显示结果列表 + * @private + */ Search.prototype._showResultList = function () { - this.elm.$results.children('ul').children().remove(); - this.elm.$resultMsg.hide(); + this.$e.results.children('ul').children().remove(); + this.$e.resultMsg.hide(); this._data.pagination = 0; this._nextResultPage(); }; - //显示结果列表下一页 + /** + * 显示结果列表下一页 + * @private + */ Search.prototype._nextResultPage = function () { var html = ''; var count = 0; @@ -209,17 +263,23 @@ break; } } - this.elm.$results.children('ul').append(html); + this.$e.results.children('ul').append(html); this._data.pagination++; //如果还有结果没显示完,显示显示更多按钮 if (this._data.pagination * this._data.pageSize >= this._data.result.length) { - this.elm.$resultMore.hide(); + this.$e.resultMore.hide(); } else { - this.elm.$resultMore.show(); + this.$e.resultMore.show(); } }; - //渲染单条模板 + /** + * 渲染单条模板 + * @param {String} template + * @param {Object} data + * @returns {String} + * @private + */ Search.prototype._renderRankItem = function (template, data) { var tmpl = template; data.time = win.tools.formatTime(data.timestamp); diff --git a/files/amWiki.search.worker.js b/files/amWiki.search.worker.js index 047c78b..7b4bff5 100644 --- a/files/amWiki.search.worker.js +++ b/files/amWiki.search.worker.js @@ -1,5 +1,5 @@ /** - * @desc amWiki Web端·搜索子进程 + * amWiki Web端 - 搜索计算子进程模块 * @author Tevin */ @@ -8,9 +8,15 @@ 'use strict'; - //计算器 - var searcher = (function () { + //通过 jQuery 检查判断是否处于子进程工作 + var atWorker = typeof self.jQuery == 'undefined'; + (function () { + + /** + * 搜索计算器 + * @constructor + */ var Searcher = function () { //文档存储 this._documents = null; @@ -29,7 +35,7 @@ //初始文档 Searcher.prototype.initDocs = function (docs) { - this._documents = docs; + this._documents = atWorker ? docs : JSON.parse(JSON.stringify(docs)); for (var id in this._documents) { if (this._documents.hasOwnProperty(id)) { this._preDoc(this._documents[id]); @@ -48,10 +54,10 @@ }) //分离测试文档请求地址 .replace(/([^#]#{3} *请求地址[\n\r]{1,4})([-\w:\/\.]+?)[\n\r]{1,}(#{3} *请求类型[\s\S]+?#{3} *请求参数)/, - function (match, s1, s2, s3) { - doc.api = s2; - return s1 + s3; - }) + function (match, s1, s2, s3) { + doc.api = s2; + return s1 + s3; + }) //清除 Markdown 标题标记 .replace(/#{1,6}(.*?)#{0,6}\s*[\r\n]/g, '$1') //清除 Markdown 强调斜体删除线标记 @@ -108,34 +114,46 @@ //匹配搜索词与得分计算 Searcher.prototype.matchWords = function (words) { + var wordsReg = new RegExp(words, 'gi'); for (var id in this._documents) { if (this._documents.hasOwnProperty(id)) { //标题命中 - if (this._documents[id].title && this._documents[id].title.indexOf(words) >= 0) { - var title = this._documents[id].title.replace(words, '' + words + ''); - this._addPorcessing(id, 'title', title); - this._addPorcessing(id, 'score', this._data.titleScore); + if (this._documents[id].title) { + var titleMatch = this._documents[id].title.match(wordsReg); + if (titleMatch && titleMatch.length > 0) { + var title = this._documents[id].title.replace(wordsReg, function (match) { + return '' + match + ''; + }); + this._addPorcessing(id, 'title', title); + this._addPorcessing(id, 'score', this._data.titleScore); + } } //接口地址命中 - if (this._documents[id].api && this._documents[id].api.indexOf(words) >= 0) { - var api = '

    接口' + this._documents[id].api.replace(words, '' + words + '') + '

    '; - this._addPorcessing(id, 'api', api); - this._addPorcessing(id, 'score', this._data.apiScore); + if (this._documents[id].api) { + var apiMatch = this._documents[id].api.match(wordsReg); + if (apiMatch && apiMatch.length > 0) { + var api = '

    接口' + + this._documents[id].api.replace(wordsReg, function (match) { + return '' + match + ''; + }) + '

    '; + this._addPorcessing(id, 'api', api); + this._addPorcessing(id, 'score', this._data.apiScore); + } } //内容命中 - if (this._documents[id].content.indexOf(words) >= 0) { - var matches = this._documents[id].content.match(new RegExp('.{0,15}' + words + '.{0,30}', 'g')); - if (matches) { - var content = '

    '; - for (var i = 0, item; item = matches[i]; i++) { - if (i < 2) { - content += item.replace(words, '' + words + '') + '... '; - } + var contentMatch = this._documents[id].content.match(new RegExp('.{0,15}' + words + '.{0,30}', 'gi')); + if (contentMatch) { + var content = '

    '; + for (var i = 0, item; item = contentMatch[i]; i++) { + if (i < 2) { + content += item.replace(wordsReg, function (match) { + return '' + match + ''; + }) + '... '; } - content += '

    '; - this._addPorcessing(id, 'content', content); - this._addPorcessing(id, 'score', matches.length * this._data.textScore); } + content += '

    '; + this._addPorcessing(id, 'content', content); + this._addPorcessing(id, 'score', contentMatch.length * this._data.textScore); } } } @@ -176,21 +194,27 @@ return this._sortByScore(); }; - return new Searcher(); + //作为子进程加载时,仅子进程内有效 + //作为全局加载时,全局有效 + return this.AWSearcher = Searcher; - })(); + }).call(self); - self.onmessage = function (event) { - var data = event.data; - if (data.type == 'docs') { - searcher.initDocs(data.docs); - self.postMessage({type: 'ready'}); - } else if (data.type == 'search') { - searcher.matchWords(data.words); - self.postMessage({type: 'result', result: searcher.getResult()}); - } - }; - - self.postMessage({type: 'loaded'}); + //作为子进程工作时,通过 message 通讯工作 + if (atWorker) { + //计算器 + var searcher = new self.AWSearcher(); + self.onmessage = function (event) { + var data = event.data; + if (data.type == 'searcher:docs') { + searcher.initDocs(data.docs); + self.postMessage({type: 'searcher:ready'}); + } else if (data.type == 'searcher:search') { + searcher.matchWords(data.words); + self.postMessage({type: 'searcher:result', result: searcher.getResult()}); + } + }; + self.postMessage({type: 'searcher:loaded'}); + } })(self); \ No newline at end of file diff --git a/files/amWiki.storage.js b/files/amWiki.storage.js index 875b181..f44c494 100644 --- a/files/amWiki.storage.js +++ b/files/amWiki.storage.js @@ -1,5 +1,5 @@ /** - * @desc amWiki Web端·本地缓存模块 + * amWiki Web端 - 浏览器数据缓存模块 * @author Tevin */ @@ -11,28 +11,35 @@ var tools = win.tools; var wikiPath = tools.simString(win.location.pathname.replace('/', '').replace(/\//g, '_')).toUpperCase(); var LOCAL_STORAGE_NAME = 'AMWikiDataBase@' + wikiPath; //本地数据localStorage键名 + var LOCAL_STATES = 'AMWikiStates@' + wikiPath; //本地状态集键名 /** - * @class 创建一个本地存储管理对象 + * 本地存储管理 + * @constructor */ var Storage = function () { this._db = null; //内存中的文库缓存 - this.elm = { - $win: $(win), + this._states = null; //内存中的状态集 + this.$e = { + win: $(win), //更新全部缓存按钮 - $searchUpdate: $('#searchUpdate'), + searchUpdate: $('#searchUpdate'), //缓存状态 - $cacheState: $('#cacheState'), + cacheState: $('#cacheState'), //文档总数 - $cacheDocTotal: $('#cacheDocTotal'), + cacheDocTotal: $('#cacheDocTotal'), //上次全部缓存更新时间 - $cacheLasttime: $('#cacheLasttime') + cacheLasttime: $('#cacheLasttime') }; this._bridgeLocalStorage('read'); this._bindCtrl(); }; - //存取本地存储 + /** + * 存取本地存储 + * @param {String} type - read / save + * @private + */ Storage.prototype._bridgeLocalStorage = function (type) { if (type == 'read') { var defaultStr = '{"documents":{},"lastBuild":0}'; @@ -47,15 +54,24 @@ } }; - //绑定操作 + /** + * 绑定操作 + * @private + */ Storage.prototype._bindCtrl = function () { var that = this; - this.elm.$win.on('beforeunload', function () { + this.$e.win.on('beforeunload', function () { that._bridgeLocalStorage('save'); }); }; - //更新一篇文档,如果相同则不操作(对应内容不用重新渲染) + /** + * 更新一篇文档,如果相同则不操作(对应内容不用重新渲染) + * @param {String} uri + * @param {String} content + * @returns {Boolean} + * @public + */ Storage.prototype.update = function (uri, content) { var id = tools.simString(uri, 'short'); if (this._db.documents[id]) { @@ -72,10 +88,11 @@ }; /** - * @desc 保存一篇文档 - * @param uri {string} - 文档资源地址 - * @param content {string} - 文档内容 - * @param [id] {string} - 已经编码的文档地址,可选 + * 保存一篇文档 + * @param {String} uri - 文档资源地址 + * @param {String} content - 文档内容 + * @param {String} [id] - 已经编码的文档地址,可选 + * @public */ Storage.prototype.saveDoc = function (uri, content, id) { this.saveDocToDB(uri, content, id); @@ -83,7 +100,13 @@ this._changeSummary('sateOnly'); }; - //将文档存储到内存 + /** + * 将文档存储到内存 + * @param {String} uri + * @param {String} content + * @param {String} id + * @public + */ Storage.prototype.saveDocToDB = function (uri, content, id) { if (typeof uri != 'string' && uri == '') { throw new Error('Error, uri must be a string!'); @@ -101,9 +124,10 @@ }; /** - * @desc 读取一篇文档 - * @param uri {string} - 文档资源地址 - * @returns {string} - 文档内容 + * 读取一篇文档 + * @param {String} uri - 文档资源地址 + * @returns {String} 文档内容 + * @public */ Storage.prototype.read = function (uri) { var id = tools.simString(uri, 'short'); @@ -115,8 +139,24 @@ }; /** - * @desc 删除一篇文档 - * @param uri {string} - 文档资源地址 + * 读取一篇文档的时间 + * @param {String} uri - 文档资源地址 + * @returns {String} 文档内容 + * @public + */ + Storage.prototype.readTime = function (uri) { + var id = tools.simString(uri, 'short'); + if (this._db.documents[id]) { + return this._db.documents[id].timestamp; + } else { + return 0; + } + }; + + /** + * 删除一篇文档 + * @param {String} uri - 文档资源地址 + * @public */ Storage.prototype.remove = function (uri) { var id = tools.simString(uri, 'short'); @@ -125,15 +165,20 @@ this._changeSummary('sateOnly'); }; - //增涨文档打开数记录 + /** + * 增涨文档打开数记录 + * @param {String} uri + * @public + */ Storage.prototype.increaseOpenedCount = function (uri) { var id = tools.simString(uri, 'short'); //TODO: 待续...打开次数将一定程度影响排行 }; /** - * @desc 校对列表,清除失效文档 - * @param list {Array} - 由导航树偏平化生成的文档列表 + * 校对列表,清除失效文档 + * @param {Array} list - 由导航树偏平化生成的文档列表 + * @public */ Storage.prototype.checkLibChange = function (list) { this._indexing = list; @@ -150,7 +195,12 @@ this._changeSummary(); }; - //更新缓存摘要(位于搜素面板) + /** + * 更新缓存摘要(位于搜素面板) + * @param {String} stateOnly - 是否为只读 stateOnly / ... + * @param {String} prepare - 是否为预先 prepare / ... + * @private + */ Storage.prototype._changeSummary = function (stateOnly, prepare) { var libraryiesLong = 0; for (var p in this._db.documents) { @@ -160,51 +210,100 @@ } //如果是预先,百分数减1 if (prepare == 'prepare') { - this.elm.$cacheState.text(parseInt(libraryiesLong / this._indexing.length * 100 - 1) + '%'); + this.$e.cacheState.text(parseInt(libraryiesLong / this._indexing.length * 100 - 1) + '%'); } //非预先则正常 else { - this.elm.$cacheState.text(parseInt(libraryiesLong / this._indexing.length * 100) + '%'); + this.$e.cacheState.text(parseInt(libraryiesLong / this._indexing.length * 100) + '%'); } //如果不只是状态 if (stateOnly != 'stateOnly') { - this.elm.$cacheDocTotal.text(this._indexing.length); + this.$e.cacheDocTotal.text(this._indexing.length); if (this._db.lastBuild) { - this.elm.$cacheLasttime.text(win.tools.formatTime(this._db.lastBuild)); + this.$e.cacheLasttime.text(win.tools.formatTime(this._db.lastBuild)); } else { - this.elm.$cacheLasttime.text('0000-00-00 00:00:00'); + this.$e.cacheLasttime.text('0000-00-00 00:00:00'); } } }; - //清除内存中的库列表 + /** + * 清除内存中的库列表 + * @public + */ Storage.prototype.clearLibraries = function () { this._db.documents = {}; this._changeSummary('sateOnly'); }; - //完成本次缓存重建 + /** + * 完成本次缓存重建 + * @public + */ Storage.prototype.saveRebuild = function () { this._db.lastBuild = Date.now(); this._bridgeLocalStorage('save'); this._changeSummary(); }; - //返回导航列表 + /** + * 返回导航列表 + * @returns {Array} + * @public + */ Storage.prototype.getIndexList = function () { return this._indexing; }; - //获取当前缓存的所有文档 + /** + * 获取当前缓存的所有文档 + * @returns {{Object}} + * @public + */ Storage.prototype.getAllDocs = function () { return this._db.documents; }; - //获取缓存最后重建时间 + /** + * 获取缓存最后重建时间 + * @returns {Number} + * @public + */ Storage.prototype.getLastBuildTs = function () { return this._db.lastBuild; }; + /** + * 获取本地存储中指定名称的值 + * @param {String} name + * @returns {*} + * @public + */ + Storage.prototype.getStates = function (name) { + if (!this._states) { + this._states = JSON.parse(win.localStorage[LOCAL_STATES] || '{}'); + } + return this._states[name]; + }; + + /** + * 保持键值对到本地存储 + * @param {String} name + * @param {*} value + * @public + */ + Storage.prototype.setStates = function (name, value) { + if (!this._states) { + this._states = JSON.parse(win.localStorage[LOCAL_STATES] || '{}'); + } + if (typeof value == 'undefined') { + delete this._states[name]; + } else { + this._states[name] = value; + } + win.localStorage[LOCAL_STATES] = JSON.stringify(this._states); + }; + return win.AWStorage = Storage; })(window); \ No newline at end of file diff --git a/files/amWiki.testing.css b/files/amWiki.testing.css index a108d6c..ffdb544 100644 --- a/files/amWiki.testing.css +++ b/files/amWiki.testing.css @@ -4,9 +4,14 @@ .testing-show{display:none;position:absolute;top:0;right:0;padding:15px;cursor:pointer;} .testing-show.show{display:block;} .testing-show span{color:@colour-dark;} +.testing-show[disabled=disabled]{cursor:not-allowed;} +.testing-show[disabled=disabled] span{color:#999;} +.testing-show[disabled=disabled] i{display:none;} +.testing-show[disabled=disabled].on i{display:block;position:absolute;top:35px;right:8px;width:200px;padding:5px 7px;color:#fff;font-size:12px;line-height:18px;background:rgba(0,0,0,.75);border-radius:5px;} +.testing-show[disabled=disabled].on i:after{display:block;position:absolute;top:-4px;right:32px;width:0;height:0;border-top:8px solid rgba(0,0,0,.75);border-left:8px solid transparent;content:' ';transform:rotate(-45deg);} /* ---------- testing ---------- */ -.testing-box{display:none;position:absolute;top:0;right:0;width:100%;padding:45px;background-color:#fff;} +.testing-box{display:none;position:absolute;top:0;right:0;width:100%;padding:45px;min-height:100%;background-color:#fff;} .testing-box h2{position:relative;min-width:320px;padding:2px 10px;margin:5px 0;color:#fff;background-color:@colour-light;} .testing-box h2:after{position:absolute;top:0;right:-45px;width:45px;height:100%;content:" ";background-color:@colour-light;} .testing-box h2 small{position:absolute;top:0;right:0;font-size:14px;line-height:38px;font-weight:normal;} @@ -61,6 +66,7 @@ @media screen and (max-width:720px){ .testing-show{position:fixed;top:3px;right:40px;z-index:11000;height:46px;padding:0 10px;line-height:47px;background:#fff;} .testing-box{padding:25px;} + .testing-box h2:after{right:-25px;width:25px;} .testing-send{padding:20px 10px 35px;} .testing-send-url{width:70%;} .testing-param-val{width:45%;} diff --git a/files/amWiki.testing.js b/files/amWiki.testing.js index 3a21f52..923dc30 100644 --- a/files/amWiki.testing.js +++ b/files/amWiki.testing.js @@ -1,7 +1,8 @@ /** - * @desc amWiki Web端·简单ajax测试模块 + * amWiki Web端 - 简单 ajax 测试模块 * @author Tevin - * @summary 仅当页面存在“请求地址”、“请求类型”、“请求参数”三个h3标题时触发 + * + * @notice 仅当页面存在“请求地址”、“请求类型”、“请求参数”三个h3标题时触发,且参数列表表格顺序不能打乱 */ (function (win, $) { @@ -9,23 +10,26 @@ 'use strict'; /** - * @class 创建接口测试 + * 建接口测试 + * @constructor */ var Testing = function () { //缓存元素 - this.elm = { - $win: $(win), + this.$e = { + win: $(win), //测试面板 - $testingBox: $('#testingBox'), + testingBox: $('#testingBox'), //md文档渲染处 - $view: $('#view'), + view: $('#view'), + //上一篇下一篇切换 + sibling: $('#mainSibling'), //面板显示隐藏按钮 - $testingShow: null, + testingShow: null, //参数列表的容器 - $testingParam: $('#testingParam') + testingParam: $('#testingParam') }; //缓存数据 - this.data = { + this._data = { //全局参数列队 globalParams: [], //全局参数是否生效 @@ -34,7 +38,7 @@ paramTemplate: $('#template\\:formList').text() }; //请求数据 - this.request = { + this._request = { //请求地址 url: '', //请求类型 @@ -44,51 +48,52 @@ //全局参数 paramGlobal: [] }; - this.useGlobalParam(); - this.bindPanelCtrl(); - this.bindAjaxSend(); + this._useGlobalParam(); + this._bindPanelCtrl(); + this._bindAjaxSend(); }; /** - * @desc 抓取请求内容,抓取成功才显示按钮 + * 抓取请求内容,抓取成功才显示按钮 + * @public */ Testing.prototype.crawlContent = function () { var that = this; var testingReqState = [false, false, false]; - this.elm.$testingShow.removeClass('show'); - this.elm.$view.find('h3').each(function () { + this.$e.testingShow.removeClass('show'); + this.$e.view.find('h3').each(function () { var $this = $(this); var name = $.trim($this.text()); //抓取请求地址 if (name == '请求地址' && !testingReqState[0]) { - that.request.url = $.trim($this.next().text()); - if (that.request.url.indexOf('http') < 0) { - if (that.request.url.indexOf('/') == 0) { - that.request.url = 'http://' + location.host + that.request.url; + that._request.url = $.trim($this.next().text()); + if (that._request.url.indexOf('http') < 0) { + if (that._request.url.indexOf('/') == 0) { + that._request.url = 'http://' + location.host + that._request.url; } else { - that.request.url = 'http://' + location.host + '/' + that.request.url; + that._request.url = 'http://' + location.host + '/' + that._request.url; } } testingReqState[0] = true; } //抓取请求类型 else if (name == '请求类型' && !testingReqState[1]) { - that.request.method = $.trim($this.next().text()).toUpperCase(); + that._request.method = $.trim($this.next().text()).toUpperCase(); var methodState = false; ['GET', 'POST', 'PUT', 'DELETE'].forEach(function (value, index) { - if (that.request.method == value) { + if (that._request.method == value) { methodState = true; } }); if (!methodState) { - that.request.method = 'POST'; + that._request.method = 'POST'; } testingReqState[1] = true; } //抓取请求参数 else if (name == '请求参数' && !testingReqState[2]) { //清空参数列表 - that.request.params.length = 0; + that._request.params.length = 0; //不存在table直接无参数,存在table时开始解析 if ($this.next('table').length > 0) { $this.next('table').find('tbody').find('tr').each(function () { @@ -118,7 +123,7 @@ param.default = ''; } } - that.request.params.push(param); + that._request.params.push(param); } }); } @@ -126,154 +131,180 @@ } }); if (testingReqState[0] && testingReqState[1] && testingReqState[2]) { - this.initPanel(); + this._initPanel(); } else { - this.offPanel(); + this._offPanel(); } testingReqState = [false, false, false]; }; - //关闭测试面板 - Testing.prototype.offPanel = function () { - this.elm.$testingShow.removeClass('show'); - if (this.elm.$testingShow.hasClass('on')) { + /** + * 关闭测试面板 + * @private + */ + Testing.prototype._offPanel = function () { + this.$e.testingShow.removeClass('show'); + if (this.$e.testingShow.hasClass('on')) { this.displayBox('off'); } //清除抓取参数 - this.request.url = ''; - this.request.method = ''; - this.request.params = []; + this._request.url = ''; + this._request.method = ''; + this._request.params = []; //清空请求地址 $('#testingSendUrl').val(''); //还原请求类型 $('#testingSendType').find('option[value="POST"]').prop('selected', true); //清空参数列表 - this.elm.$testingParam.html(''); + this.$e.testingParam.html(''); //重置iframe $('#testingResponse')[0].contentWindow.location.reload(); }; - //测试面板填充数据 - Testing.prototype.initPanel = function () { - this.elm.$testingShow.addClass('show'); + /** + * 测试面板填充数据 + * @private + */ + Testing.prototype._initPanel = function () { + this.$e.testingShow.addClass('show'); //填充请求地址 - $('#testingSendUrl').val(this.request.url); + $('#testingSendUrl').val(this._request.url); //填充请求类型 - $('#testingSendType').find('option[value="' + this.request.method + '"]').prop('selected', true); + $('#testingSendType').find('option[value="' + this._request.method + '"]').prop('selected', true); //清空现有参数列表 - this.elm.$testingParam.html(''); + this.$e.testingParam.html(''); //填充参数列表 - if (this.request.params.length > 0) { + if (this._request.params.length > 0) { var paramsHTML = ''; - for (var i = 0; i < this.request.params.length; i++) { - paramsHTML += this.data.paramTemplate - .replace('{{describe}}', this.request.params[i].describe) - .replace('{{keyName}}', this.request.params[i].keyName) - .replace('{{default}}', this.request.params[i].default) - .replace('{{valueType}}', this.request.params[i].valueType) - .replace('{{required}}', this.request.params[i].required); + for (var i = 0; i < this._request.params.length; i++) { + paramsHTML += this._data.paramTemplate + .replace('{{describe}}', this._request.params[i].describe) + .replace('{{keyName}}', this._request.params[i].keyName) + .replace('{{default}}', this._request.params[i].default) + .replace('{{valueType}}', this._request.params[i].valueType) + .replace('{{required}}', this._request.params[i].required); } - this.elm.$testingParam.append(paramsHTML); + this.$e.testingParam.append(paramsHTML); } else { - this.elm.$testingParam.append('
  • '); + this.$e.testingParam.append('
  • '); } }; - //切换测试面板显示隐藏状态 + /** + * 切换测试面板显示隐藏状态 + * @param {String} type - on / off + * @public + */ Testing.prototype.displayBox = function (type) { var that = this; if (type == 'off') { - this.elm.$testingShow.removeClass('on').find('span').text('测试接口'); - this.elm.$testingBox.css({ - 'position': 'absolute', - 'padding': this.elm.$win.width() > 720 ? 45 : 25 + this.$e.testingShow.removeClass('on').find('span').text('测试接口'); + this.$e.testingBox.css({ + 'position': 'absolute' }); - this.elm.$view.show(); - this.elm.$testingBox.animate({ + this.$e.view.show().addClass('scroller-content'); + this.$e.sibling.addClass('scroller-content').addClass('on'); + this.$e.testingBox.removeClass('scroller-content').stop().animate({ 'width': '30%', 'opacity': 0 }, 200, 'swing', function () { - that.elm.$testingBox.removeAttr('style'); + that.$e.testingBox.removeAttr('style'); }); } else if (type == 'on') { - this.elm.$testingShow.addClass('on').find('span').text('关闭测试'); - this.elm.$testingBox + this.$e.testingShow.addClass('on').find('span').text('关闭测试'); + this.$e.testingBox .css({ 'display': 'block', 'width': '0', - 'min-height': this.elm.$view.height() + 90, 'opacity': 0 }) + .stop() .animate({ 'width': '100%', 'opacity': 1 }, 300, 'swing', function () { - that.elm.$view.hide(); - that.elm.$testingBox.css({ - 'position': 'relative', - 'padding': 0 + that.$e.view.hide().removeClass('scroller-content'); + that.$e.sibling.removeClass('scroller-content').removeClass('on'); + that.$e.testingBox.addClass('scroller-content').css({ + 'position': 'relative' }); }); } }; - //测试面板普通操作 - Testing.prototype.bindPanelCtrl = function () { + /** + * 测试面板普通操作 + * @private + */ + Testing.prototype._bindPanelCtrl = function () { var that = this; //显示隐藏控制按钮 - this.elm.$testingShow = $('
    [测试接口]
    '); - $('#main').append(this.elm.$testingShow); + this.$e.testingShow = $('
    [测试接口]
    '); + if (location.protocol == 'file:') { + this.$e.testingShow + .attr('disabled', 'disabled') + .append('您当前为本地模式打开,file:// 协议下不开放接口测试模块,请使用 http(s):// 网址打开'); + } + $('#main').append(this.$e.testingShow); //显示隐藏测试面板 - this.elm.$testingShow.on('click', function () { - if (that.elm.$testingShow.hasClass('on')) { - that.displayBox('off'); + this.$e.testingShow.on('click', function () { + var $this = that.$e.testingShow; + if ($this.attr('disabled') == 'disabled') { + $this.toggleClass('on'); } else { - that.displayBox('on'); + if (that.$e.testingShow.hasClass('on')) { + that.displayBox('off'); + } else { + that.displayBox('on'); + } } }); //填充请求地址 $('#testingSendUrl').on('change', function () { - that.request.url = $(this).val(); + that._request.url = $(this).val(); }); //填充请求类型 $('#testingSendType').on('change', function () { - that.request.method = $(this).find("option:selected").val(); + that._request.method = $(this).find("option:selected").val(); }); //清空所有普通参数的值 $('#testingBtnReset').on('click', function () { - that.elm.$testingParam.find('.testing-param-val').val(''); + that.$e.testingParam.find('.testing-param-val').val(''); }); //新增一个参数 $('#testingBtnAdd').on('click', function () { - var pHTML = that.data.paramTemplate + var pHTML = that._data.paramTemplate .replace('{{describe}}', '新增参数') .replace('{{keyName}}', '') .replace('{{default}}', '') .replace('({{valueType}})', '') .replace('{{required}}', ''); - that.elm.$testingParam.append(pHTML); + that.$e.testingParam.append(pHTML); }); }; - //全局参数模块 - Testing.prototype.useGlobalParam = function () { + /** + * 全局参数模块 + * @private + */ + Testing.prototype._useGlobalParam = function () { var that = this; - this.data.globalParams = JSON.parse(localStorage['amWikiGlobalParam'] || '[]'); //全局参数 + this._data.globalParams = JSON.parse(localStorage['amWikiGlobalParam'] || '[]'); //全局参数 var gParamTmpl = $('#template\\:globalParam').text(); //全局参数模板 var $testingGlobalParam = $('#testingGlobalParam'); //全局参数显示容器 var $testingGlobal = $('#testingGlobal'); //全局参数弹窗 - this.data.globalParamWorking = (localStorage['amWikiGParamWorking'] || 'on') == 'on'; //全局参数是否工作 + this._data.globalParamWorking = (localStorage['amWikiGParamWorking'] || 'on') == 'on'; //全局参数是否工作 //显示弹窗 $('#testingBtnGParam').on('click', function () { $testingGlobalParam.html(''); - that.data.globalParams = JSON.parse(localStorage['amWikiGlobalParam'] || '[]'); - if (that.data.globalParams.length == 0) { + that._data.globalParams = JSON.parse(localStorage['amWikiGlobalParam'] || '[]'); + if (that._data.globalParams.length == 0) { $testingGlobalParam.append('
  • '); } else { - for (var p = 0; p < that.data.globalParams.length; p++) { - $testingGlobalParam.append(gParamTmpl.replace('{{describe}}', that.data.globalParams[p].describe) - .replace('{{keyName}}', that.data.globalParams[p].keyName) - .replace('{{value}}', that.data.globalParams[p].value)); + for (var p = 0; p < that._data.globalParams.length; p++) { + $testingGlobalParam.append(gParamTmpl.replace('{{describe}}', that._data.globalParams[p].describe) + .replace('{{keyName}}', that._data.globalParams[p].keyName) + .replace('{{value}}', that._data.globalParams[p].value)); } } $testingGlobal.show(); @@ -294,18 +325,18 @@ } //保存 else if ($elm.hasClass('save')) { - that.data.globalParams.length = 0; + that._data.globalParams.length = 0; $testingGlobalParam.find('li').each(function (i, elment) { var $inputs = $(this).find('input'); if ($inputs.eq(1).val()) { - that.data.globalParams.push({ + that._data.globalParams.push({ describe: $inputs.eq(0).val(), keyName: $inputs.eq(1).val(), value: $inputs.eq(2).val() }); } }); - localStorage['amWikiGlobalParam'] = JSON.stringify(that.data.globalParams); + localStorage['amWikiGlobalParam'] = JSON.stringify(that._data.globalParams); $testingGlobal.hide(); } }); @@ -317,20 +348,23 @@ } }); $('#testingGlobalWorking').on('click', function () { - if (that.data.globalParamWorking) { - that.data.globalParamWorking = false; + if (that._data.globalParamWorking) { + that._data.globalParamWorking = false; localStorage['amWikiGParamWorking'] = 'off'; $(this).addClass('off'); } else { - that.data.globalParamWorking = true; + that._data.globalParamWorking = true; localStorage['amWikiGParamWorking'] = 'on'; $(this).removeClass('off'); } - }).addClass(this.data.globalParamWorking ? '' : 'off'); + }).addClass(this._data.globalParamWorking ? '' : 'off'); }; - //发送请求 - Testing.prototype.bindAjaxSend = function () { + /** + * 发送请求操作 + * @private + */ + Testing.prototype._bindAjaxSend = function () { var that = this; var frame = $('#testingResponse')[0]; var $duration = $('#testingDuration'); //耗时输出 @@ -347,17 +381,17 @@ }); } //全局参数 - if (that.data.globalParams.length > 0 && that.data.globalParamWorking) { - for (var i = 0; i < that.data.globalParams.length; i++) { - realParam[that.data.globalParams[i].keyName] = that.data.globalParams[i].value; + if (that._data.globalParams.length > 0 && that._data.globalParamWorking) { + for (var i = 0; i < that._data.globalParams.length; i++) { + realParam[that._data.globalParams[i].keyName] = that._data.globalParams[i].value; } } frame.contentWindow.location.reload(); //刷新iframe以便重新输出内容 $loading.show(); var startTime = Date.now(); $.ajax({ - type: that.request.method, - url: that.request.url, + type: that._request.method, + url: that._request.url, data: realParam, dataType: 'text', success: function (data) { @@ -413,6 +447,15 @@ }); }; + /** + * 判断测试面板是否处于打开状态 + * @returns {Boolean} + * @public + */ + Testing.prototype.isOpen = function () { + return this.$e.testingShow.hasClass('on'); + }; + return win.AWTesting = Testing; })(window, jQuery); \ No newline at end of file diff --git a/files/amWiki.tools.js b/files/amWiki.tools.js index ea1d42f..cd835c0 100644 --- a/files/amWiki.tools.js +++ b/files/amWiki.tools.js @@ -1,5 +1,5 @@ /** - * @desc amWiki Web端·工具集 + * amWiki Web端·工具集 * @author Tevin */ @@ -11,9 +11,9 @@ return win.tools = { /** - * @desc 获取url参数 - * @param name {string} - * @returns {string|null} - 获取的参数 + * 获取url参数 + * @param {String} name + * @returns {String|Null} - 获取的参数 */ getURLParameter: function (name) { var reg = new RegExp("(^|&)" + name + "=([^&]*)(&|$)", "i"); @@ -26,10 +26,10 @@ }, /** - * @desc 转换字符中每个汉字为两个字符 - * @param str {string} - 要编码的字符串 - * @param [mod] {string} - 编码模式选择,可选,normal(默认)一个汉字对应两位字符,short一个汉字对应一位字符 - * @returns {string} - 编码后的字符串 + * 转换字符中每个汉字为两个字符 + * @param {String} str - 要编码的字符串 + * @param {String} [mod] - 编码模式选择,可选,normal(默认)一个汉字对应两位字符,short一个汉字对应一位字符 + * @returns {String} 编码后的字符串 */ simString: function (str, mod) { mod = mod == 'short'; //短字符串 @@ -59,12 +59,12 @@ }, /** - * @desc json格式化 - * @param str {string} - 需要格式化的json字符串 - * @returns {string} - 格式化后的json字符串 + * json格式化 + * @param {String} str - 需要格式化的json字符串 + * @returns {String} 格式化后的json字符串 */ formatJson: function (str) { - var json = decodeURI(str); + var json = decodeURI(str.replace(/%([^0-9A-Z]{0,2})/g, '%25$1')); var reg = null, formatted = '', pad = 0, @@ -130,9 +130,9 @@ }, /** - * @desc 时间戳格式化为日期时间 - * @param timestamp {number} - 时间戳 - * @returns {string} - 日期时间 + * 时间戳格式化为日期时间 + * @param {Number} timestamp - 时间戳 + * @returns {String} 日期时间 */ formatTime: function (timestamp) { var time = new Date(timestamp); diff --git a/files/amWiki.tpl b/files/amWiki.tpl index 1b97c1a..3ed150d 100644 --- a/files/amWiki.tpl +++ b/files/amWiki.tpl @@ -10,6 +10,7 @@ + {{custom.css}} @@ -21,7 +22,7 @@ {{name}} {{version}} -