# 外置手册

本次更新一个主要功能就是添加自定义添加手册的功能。一方面随着手册的增加，插件的体积会不断增大，使得插件本身变得臃肿，同时将数据和界面分离，方便后期维护；另一方面个人精力、资源都有限，无法满足所有人需求，增加这个功能，大家可以一起来维护，如果你有好的手册，欢迎发送给我[fofolee@qq.com](mailto:fofolee@qq.com)，我会添加到内置或者外置的手册库中，并标明贡献者。

闲话少说，下面进入正题

以下均以外置手册`Java`为例

该外置下载地址 [百度网盘](https://pan.baidu.com/s/188sFN_oktGulGTdvnCQPqw) 提取码: `yfh7`

## 配置格式

增加外置手册需要填写的参数如下图所示

![Snipaste_2019-04-20_10-45-20.png](https://i.loli.net/2019/04/20/5cba8859b3a66.png)

- `名称`是这个手册的唯一标识，类似于`uTools`插件开发中的`code`，手册中的大部分配置文件都以此命名，建议以语言的英文名称命名（如果不和已有的内置手册冲突的话）
- `关键字`是进入手册的关键词，多个以逗号隔开
- `说明`手册描述
- `路径`手册所在路径

## 手册格式

外置手册所在目录内应具备如下几个文件

![UTOOLS1557255615924.png](https://i.loli.net/2019/05/08/5cd1d5bf173c2.png)

可以看到，所有文件的名称几乎都是以之前配置中的`名称`命名的

- xxx.json 必须具备的文件，是整个手册的目录文件
- xxx文件夹 必须具备的文件，整个手册的主体就在这个文件夹里了
- xxx.png 非必备文件，在配置页面显示手册的图标，本来也作为这个手册在`uTools`主输入框的logo文件，但遗憾的是貌似`uTools`的logo文件不支持绝对路径，希望官方能够解决一下~
- assets 非必备文件，如需自定义CSS，可以将所有CSS文件放入此中

## 手册文件夹

手册文件夹内包含了所有手册的文件，获取途径不外乎有两种

- 如果官方提供了下载，直接下载，例如`php`

- 官方没有提供下载，用脚本爬取

  以爬取[https://blog.fondme.cn/apidoc/jdk-1.8-google](https://blog.fondme.cn/apidoc/jdk-1.8-google)的`java`文档为例，我使用的是`python`脚本，当然你可以使用任何你喜欢的语言

```python
import requests
from bs4 import BeautifulSoup 
import os
import sys

for i in range(1,28):
    url = "https://blog.fondme.cn/apidoc/jdk-1.8-google/index-files/index-%d.html"%i
    r=requests.get(url).content
    soup = BeautifulSoup(r).select('dt')
    for n, x in enumerate(soup):
        path = x.a.attrs['href']
        if '#' not in path:
            file = path[3:]
            url = "https://blog.fondme.cn/apidoc/jdk-1.8-google/index-files/" + path
            r = requests.get(url).content
            sou = BeautifulSoup(r).select('.header,.contentContainer')
            d = os.path.dirname(file)
            if not os.path.exists(d):
                os.makedirs(d)
            with open(file, 'w') as f:
                f.write(str(sou[0])+str(sou[1]))
        sys.stdout.write('\r[%d/%d/%d] %s done!'%(n, len(soup), i, file))
```

当然，你还可以使用`网站镜像`的软件或者`wget`进行递归下载，但是个人还是比较建议使用爬虫脚本，如果你仔细看了脚本的话，会发现这段代码

```python
sou = BeautifulSoup(r).select('.header,.contentContainer')
```

即在爬取网页时，只爬取了文档的主体内容，一些无关的内容（如侧栏、底栏等）没进行爬取，这样可以节省`uTools`这种小窗口的空间，同时整个手册也更加整洁。

值得注意的是，`<head>`标签内的内容也没爬取，因为在插件中内置了一个通用的`css`样式，如果效果不理想，你可以在外置手册目录中再额外放置一个`css`文件即可，并且无须在每个文档的`html`文件中引入，会通过插件自动加载

## 目录文件

目录文件是数组格式的`json`文件，数组的每一个值对应着一个函数、目录或者文档，如

```json
{
	"name": "Cipher(CipherSpi, Provider, String)",
	"type": "Cipher",
	"path": "java/javax/crypto/Cipher.html#Cipher-javax.crypto.CipherSpi-java.security.Provider-java.lang.String-",
	"desc": "从OutputStream和Cipher构造一个CipherOutputStream"
}
```

各个键值对应如下，其中`type`和`desc`可以留空

![Snipaste_2019-04-20_11-39-11.png](https://i.loli.net/2019/04/20/5cba9469a113d.png)

## 生成目录文件

生成目录文件一样需要使用脚本，同样以https://blog.fondme.cn/apidoc/jdk-1.8-google](https://blog.fondme.cn/apidoc/jdk-1.8-google)的`java`文档为例

```python
import codecs
import requests
from bs4 import BeautifulSoup 
import os
import sys

j = codecs.open('java.json', 'a' ,encoding='utf-8')
j.write("[")
for i in range(1,28):
    url = "https://blog.fondme.cn/apidoc/jdk-1.8-google/index-files/index-%d.html"%i
    r=requests.get(url).content
    soup = BeautifulSoup(r).select('dt')
    soup2 = BeautifulSoup(r).select('dd')
    for x,y in zip(soup,soup2):
        name = x.a.text
        path = 'java' + x.a.attrs['href'][2:]
        type = x.a.attrs['href'].split('/')[-1].split('.html#')[0]
        desc = y.text.replace('\n','').replace('"','&quot;')
        j.write('{"name":"%s","type":"%s","path":"%s","desc":"%s"},'%(name,type,path,desc))
        sys.stdout.write('\r[%d/%d/%d] %s done!'%(soup.index(x), len(soup), i, name))
j.write("]")
j.close()
```

## 关于DASH、ZEAL

**！！**由于增加了`devdocs`的在线文档和调用`dash`的功能，以下方法已经意义不大，仅供参考

------

`dash`或`zeal`的离线文档目录一般在`Resources`文件夹下，这个文件夹内以下几个文件值得关注：

- Documents 文档主体
- docSet.dsidx `sqlite3`格式的目录文件
- Tokens.xml `xml`格式的目录文件

`Tokens.xml`文件不是每个文档都有，当有`Tokens.xml`文件时，使用以下`nodejs`脚本将其转成本插件可用的目录文件

```js
const fs = require('fs');

xml2Json = (input, output) => {
    fs.readFile(input, 'utf8', (err, data) => {
        data = data.replace(/\"/g, "&quot;");
        data = data.replace(/<Token>.*?<Name>(.*?)<\/Name><Type>(.*?)<\/Type>.*?<Path>(.*?&gt;)*(.*?)<\/Path>\s+.*?<\/Token>/g, `{"name":"$1","type":"$2","path":"$4"},`);
        data = data.replace(/<\?xml.*?>\s+<Tokens.*?>([\s|\S]*?),\s+<\/Tokens>/, '[$1]');
        fs.writeFile(output, data, 'utf8', err => {
            err && console.log(err);
        })
    })
}

xml2Json('Tokens.xml', 'xxx.json')
```

当没有`Tokens.xml`文件时，使用以下`nodejs`脚本将`docSet.dsidx`转换为本插件可用的目录文件

```js
const sqlite3 = require('sqlite3');
const fs = require('fs');

sqlite2Json = (input, output) => {
    let db = new sqlite3.Database(input);
    var json = {}
    db.all(`SELECT * FROM searchIndex`, function (err, data) {
        if (err) {
            console.log(err);
            return;
        }
        for (var d of data) {
            json[d.name] = d
        }
        fs.writeFile(output, JSON.stringify(json), err => {
            err && console.log(err);
        })
    })
}

sqlite2Json('docSet.dsidx', 'xxx.json')
```

需要先安装`sqlite3`模块

```sh
npm i sqlite3
```

