├── .eslintignore
├── .eslintrc
├── .github
└── workflows
│ └── nodejs.yml
├── .gitignore
├── .prettierrc.yaml
├── .travis.yml
├── LICENSE
├── README.md
├── adapter
├── hexo.js
└── markdown.js
├── appveyor.yml
├── bin
└── yuque-hexo.js
├── command
├── clean.js
└── sync.js
├── config.js
├── index.js
├── lib
├── Downloader.js
├── cleaner.js
├── out.js
├── qetag.js
└── yuque.js
├── package.json
├── test
├── custom-adapter-project
│ ├── custom.adapter.js
│ ├── package.json
│ └── yuque-hexo-last-generate-timestamp.txt
├── hexo-project
│ └── package.json
├── markdown-project
│ ├── package.json
│ └── yuque-hexo-last-generate-timestamp.txt
└── yuque.test.js
└── util
├── imageBeds
├── cos.js
├── github.js
├── index.js
├── oss.js
├── qiniu.js
└── upyun.js
├── img2cdn.js
└── index.js
/.eslintignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | coverage/
3 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "eslint-config-egg",
3 | "parserOptions": {
4 | "ecmaVersion": 2018
5 | },
6 | "rules": {
7 | "linebreak-style": 0
8 | }
9 | }
--------------------------------------------------------------------------------
/.github/workflows/nodejs.yml:
--------------------------------------------------------------------------------
1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
3 |
4 | name: Node.js CI
5 |
6 | on:
7 | push:
8 | branches: [ master ]
9 | pull_request:
10 | branches: [ master ]
11 | schedule:
12 | - cron: '0 2 * * *'
13 |
14 | jobs:
15 | build:
16 | runs-on: ${{ matrix.os }}
17 |
18 | strategy:
19 | fail-fast: false
20 | matrix:
21 | node-version: [12]
22 | os: [windows-latest, macos-latest]
23 |
24 | steps:
25 | - name: Checkout Git Source
26 | uses: actions/checkout@v2
27 |
28 | - name: Use Node.js ${{ matrix.node-version }}
29 | uses: actions/setup-node@v1
30 | with:
31 | node-version: ${{ matrix.node-version }}
32 |
33 | - name: Install Dependencies
34 | run: npm i
35 |
36 | - name: Continuous Integration
37 | run: npm run ci
38 | env:
39 | YUQUE_TOKEN: ${{secrets.YUQUE_TOKEN}}
40 |
41 | - name: Code Coverage
42 | uses: codecov/codecov-action@v1
43 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 |
8 | # Runtime data
9 | pids
10 | *.pid
11 | *.seed
12 | *.pid.lock
13 |
14 | # Directory for instrumented libs generated by jscoverage/JSCover
15 | lib-cov
16 |
17 | # Coverage directory used by tools like istanbul
18 | coverage
19 |
20 | # nyc test coverage
21 | .nyc_output
22 |
23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
24 | .grunt
25 |
26 | # Bower dependency directory (https://bower.io/)
27 | bower_components
28 |
29 | # node-waf configuration
30 | .lock-wscript
31 |
32 | # Compiled binary addons (https://nodejs.org/api/addons.html)
33 | build/Release
34 |
35 | # Dependency directories
36 | node_modules/
37 | jspm_packages/
38 |
39 | # TypeScript v1 declaration files
40 | typings/
41 |
42 | # Optional npm cache directory
43 | .npm
44 |
45 | # Optional eslint cache
46 | .eslintcache
47 |
48 | # Optional REPL history
49 | .node_repl_history
50 |
51 | # Output of 'npm pack'
52 | *.tgz
53 |
54 | # Yarn Integrity file
55 | .yarn-integrity
56 |
57 | # dotenv environment variables file
58 | .env
59 |
60 | # next.js build output
61 | .next
62 |
63 | package-lock.json
64 |
65 | yarn.lock
66 | .idea
67 |
--------------------------------------------------------------------------------
/.prettierrc.yaml:
--------------------------------------------------------------------------------
1 | tabWidth: 2
2 | semi: true
3 | singleQuote: true
4 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 |
2 | language: node_js
3 | node_js:
4 | - '8'
5 | - '10'
6 | - '12'
7 | before_install:
8 | - npm i npminstall -g
9 | install:
10 | - npminstall
11 | script:
12 | - npm run ci
13 | after_script:
14 | - npminstall codecov && codecov
15 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 xcold
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # yuque-hexo
2 |
3 | [![NPM version][npm-image]][npm-url]
4 | [![build status][gitflow-image]][gitflow-url]
5 | [![Test coverage][codecov-image]][codecov-url]
6 | [![David deps][david-image]][david-url]
7 | [![npm download][download-image]][download-url]
8 |
9 | [npm-image]: https://img.shields.io/npm/v/yuque-hexo.svg?style=flat-square
10 | [npm-url]: https://npmjs.org/package/yuque-hexo
11 | [gitflow-image]: https://github.com/x-cold/yuque-hexo/actions/workflows/nodejs.yml/badge.svg?branch=master
12 | [gitflow-url]: https://github.com/x-cold/yuque-hexo/actions/workflows/nodejs.yml
13 | [codecov-image]: https://codecov.io/gh/x-cold/yuque-hexo/branch/master/graph/badge.svg
14 | [codecov-url]: https://codecov.io/gh/x-cold/yuque-hexo
15 | [david-image]: https://img.shields.io/david/x-cold/yuque-hexo.svg?style=flat-square
16 | [david-url]: https://david-dm.org/x-cold/yuque-hexo
17 | [download-image]: https://badgen.net/npm/dt/yuque-hexo
18 | [download-url]: https://npmjs.org/package/yuque-hexo
19 |
20 | A downloader for articles from yuque(语雀知识库同步工具)
21 |
22 | # Usage
23 |
24 | ## Premise
25 |
26 | > 建议使用 Node.js >= 12
27 |
28 | 事先拥有一个 [hexo](https://github.com/hexojs/hexo) 项目,并在 `package.json` 中配置相关信息,可参考 [例子](#Example)。
29 |
30 | ## Config
31 |
32 | ### 配置 YUQUE_TOKEN
33 |
34 | 出于对知识库安全性的调整,使用第三方 API 访问知识库,需要传入环境变量 YUQUE_TOKEN,在语雀上点击 个人头像 -> 设置 -> Token 即可获取。传入 YUQUE_TOKEN 到 yuque-hexo 的进程有两种方式:
35 |
36 | - 设置全局的环境变量 YUQUE_TOKEN
37 | - 命令执行时传入环境变量
38 | - mac / linux: `YUQUE_TOKEN=xxx yuque-hexo sync`
39 | - windows: `set YUQUE_TOKEN=xxx && yuque-hexo sync`
40 |
41 | ### 配置 图床TOKEN(可选)
42 | 语雀的url存在防盗链的问题,直接部署可能导致图片无法加载。
43 | 如果需要语雀URL上传到图床中并替换原链接,就需要配置上传密钥。
44 |
45 | 访问图床的密钥管理获取密钥,然后传入密钥到yuque-hexo
46 | - 腾讯云[API密钥管理](https://console.cloud.tencent.com/cam/capi)
47 | - 阿里云[API密钥管理](https://ram.console.aliyun.com/manage/ak)
48 | - 七牛云[API密钥管理](https://portal.qiniu.com/user/key)
49 | - 又拍云[操作员管理](https://console.upyun.com/account/operators/)
50 | - GitHub图床[生成Github Token](https://github.com/settings/tokens)
51 | > 又拍云的SECRET_ID=操作员账号,SECRET_KEY=操作员密码
52 | >
53 | > Github图床的`SECRET_ID=用户名`,`SECRET_KEY=Github Token`。
54 | > 注意在生成`token`时,记得勾选上读写权限,即 `write:packages`和`read:packages`
55 |
56 | - 在设置YUQUE_TOKEN的基础上配置SECRET_ID和SECRET_KEY
57 | - 命令执行时传入环境变量
58 | - mac / linux: `YUQUE_TOKEN=xxx SECRET_ID=xxx SECRET_KEY=xxx yuque-hexo sync`
59 | - windows: `set YUQUE_TOKEN=xxx SECRET_ID=xxx SECRET_KEY=xxx && yuque-hexo sync`
60 |
61 |
62 | ### 配置知识库
63 |
64 | > package.json
65 |
66 | ```json
67 | {
68 | "name": "your hexo project",
69 | "yuqueConfig": {
70 | "postPath": "source/_posts/yuque",
71 | "cachePath": "yuque.json",
72 | "mdNameFormat": "title",
73 | "adapter": "hexo",
74 | "concurrency": 5,
75 | "baseUrl": "https://www.yuque.com/api/v2",
76 | "login": "yinzhi",
77 | "repo": "blog",
78 | "onlyPublished": false,
79 | "onlyPublic": false,
80 | "lastGeneratePath": "lastGeneratePath.log",
81 | "imgCdn": {
82 | "enabled": false,
83 | "concurrency": 0,
84 | "imageBed": "qiniu",
85 | "host": "",
86 | "bucket": "",
87 | "region": "",
88 | "prefixKey": ""
89 | }
90 | }
91 | }
92 | ```
93 |
94 | | 参数名 | 含义 | 默认值 |
95 | | ------------- | ------------------------------------ | -------------------- |
96 | | postPath | 文档同步后生成的路径 | source/\_posts/yuque |
97 | | cachePath | 文档下载缓存文件 | yuque.json |
98 | | lastGeneratePath | 上一次同步结束的时间戳的文件 | |
99 | | mdNameFormat | 文件名命名方式 (title / slug) | title |
100 | | adapter | 文档生成格式 (hexo/markdown) | hexo |
101 | | concurrency | 下载文章并发数 | 5 |
102 | | baseUrl | 语雀 API 地址 | - |
103 | | login | 语雀 login (group), 也称为个人路径 | - |
104 | | repo | 语雀仓库短名称,也称为语雀知识库路径 | - |
105 | | onlyPublished | 只展示已经发布的文章 | false |
106 | | onlyPublic | 只展示公开文章 | false |
107 | | imgCdn | 语雀图片转CDN配置 | |
108 | > slug 是语雀的永久链接名,一般是几个随机字母。
109 |
110 | imgCdn 语雀图片转图床配置说明
111 |
112 | 注意:开启后会将匹配到的所有的图片都上传到图床
113 |
114 | | 参数名 | 含义 | 默认值 |
115 | |-----------|-----------------------------------------------------------------------------------------|---------|
116 | | enabled | 是否开启 | false |
117 | | concurrency | 上传图片并发数, 0代表无限制,使用github图床时,并发问题严重,建议设置为1 | 0 |
118 | | imageBed | 选择将图片上传的图床
目前支持腾讯云(cos)、阿里云(oss)和七牛云(qiniu),又拍云(upyun),Github图床(github)
默认使用七牛云 | 'qiniu' |
119 | | host | 使用七牛云/又拍云图床时,需要指定CDN域名前缀 | |
120 | | bucket | 图床的bucket名称 | - |
121 | | region | 图床的的region | - |
122 | | prefixKey | 文件前缀 | - |
123 |
124 | > host 说明
125 | >
126 | > 由于七牛云默认使用CND进行图片外链访问(默认提供30天的临时域名或者添加自定义CDN域名),所以需要指定访问的域名前缀
127 | > 例如:'host': `http://image.1874.cool`,域名后面不需要加斜杠
128 |
129 | > 又拍云和七牛云有点类似,默认使用临时域名访问,但不同的是又拍云的临时域名暂时是由`服务名.test.upcdn.net`组成,默认为http访问
130 | >
131 | > 如果使用临时域名,host可不填。使用自定义域名,默认为http访问,如果需要指定协议为https,则需要填写完整的域名
132 | >
133 | > 例如 'host': `upyun.1874.cool`或 `https://upyun.1874.cool `
134 |
135 | > Github图床的默认域名是`raw.githubusercontent.com`,如果要使用jsdelivr进行加速,可以配置`host`为`cdn.jsdelivr.net`,
136 | >
137 | > 例如 'host': `cdn.jsdelivr.net`
138 |
139 | > bucket和region说明
140 | >
141 | > [获取腾讯云的bucket和region](https://console.cloud.tencent.com/cos/bucket),示例:{ bucket: "blog", region: "ap-guangzhou" }
142 | >
143 | > [获取阿里云的bucket和region](https://oss.console.aliyun.com/bucket),示例:{ bucket: "blog", region: "oss-cn-shenzhen" }
144 | >
145 | > [获取七牛云的bucket(空间)和region(机房)](https://portal.qiniu.com/kodo/overview),示例:{ bucket: "blog", region: "Zone_z2" }
146 | >
147 | > 七牛云机房取值: 华东(Zone_z0)华北(Zone_z1)华南(Zone_z2)
148 | >
149 | > 又拍云没有bucket和region的概念,只有服务名。所以这里的bucket=服务名,region暂时保留不需要填写
150 | >
151 | > Github图床也没有bucket和region的概念。所以bucket=仓库名,region暂时保留不需要填写
152 |
153 | > prefixKey 说明
154 | >
155 | > 如果需要将图片上传到bucket的根目录,那么prefixKey不用配置。
156 | >
157 | > 如果想上传到指定目录blog/image下,则需要配置prefixKey为"prefixKey": "blog/image"。
158 | >
159 | > 目录名前后都不需要加斜杠
160 |
161 | > 配置示例: [yuque-hexo配置示例](https://github.com/LetTTGACO/yuque-hexo-example)
162 |
163 |
164 | ## Install
165 |
166 | ```bash
167 | npm i -g yuque-hexo
168 | # or
169 | npm i --save-dev yuque-hexo
170 | ```
171 |
172 | ## Sync
173 |
174 | ```
175 | yuque-hexo sync
176 | ```
177 |
178 | ## Clean
179 |
180 | ```
181 | yuque-hexo clean
182 | ```
183 |
184 | ## Npm Scripts
185 |
186 | ```json
187 | {
188 | "sync": "yuque-hexo sync",
189 | "clean:yuque": "yuque-hexo clean"
190 | }
191 | ```
192 |
193 | ## Debug
194 |
195 | ```
196 | DEBUG=yuque-hexo.* yuque-hexo sync
197 | ```
198 |
199 | ## Best practice
200 | - [语雀云端写作Hexo+Github Actions+COS持续集成](https://www.yuque.com/1874w/1874.cool/roeayv)
201 | - [Hexo 博客终极玩法:云端写作,自动部署](https://www.yuque.com/u46795/blog/dlloc7)
202 | - [Hexo:语雀云端写作 Github Actions 持续集成](https://www.zhwei.cn/hexo-github-actions-yuque/)
203 |
204 | > 另外 x-cold 本人提供了一个触发 Travis CI 构建的 HTTP API 接口,详情请查看[文档](https://github.com/x-cold/aliyun-function/tree/master/travis_ci) (请勿恶意使用)
205 |
206 | # Notice
207 |
208 | - 语雀同步过来的文章会生成两部分文件;
209 |
210 | - yuque.json: 从语雀 API 拉取的数据
211 | - source/\_posts/yuque/\*.md: 生成的 md 文件
212 |
213 | - 支持配置 front-matter, 语雀编辑器编写示例如下:
214 |
215 | - 语雀编辑器示例,可参考[原文](https://www.yuque.com/u46795/blog/dlloc7)
216 |
217 | ```markdown
218 | tags: [hexo, node]
219 | categories: [fe]
220 | cover: https://cdn.nlark.com/yuque/0/2019/jpeg/155457/1546857679810-d82e3d46-e960-419c-a715-0a82c48a2fd6.jpeg#align=left&display=inline&height=225&name=image.jpeg&originHeight=225&originWidth=225&size=6267&width=225
221 |
222 | ---
223 |
224 | some description
225 |
226 |
227 |
228 | more detail
229 | ```
230 |
231 | - - 如果遇到上传到语雀的图片无法加载的问题, 可以考虑开启imgCdn配置或者参考这个处理方式[yuque-hexo插件语雀图片防盗链限制的解决方案](https://1874.cool/osar7h/)或者参考这个处理方式 [#41](https://github.com/x-cold/yuque-hexo/issues/41)
232 |
233 | # Example
234 |
235 | - yuque to hexo: [x-cold/blog](https://github.com/x-cold/blog/blob/master/package.json)
236 | - yuque to github repo: [txd-team/monthly](https://github.com/txd-team/monthly/blob/master/package.json)
237 | - [yuque-hexo配置示例](https://github.com/LetTTGACO/yuque-hexo-example)
238 |
239 | # Changelog
240 |
241 | ### v1.9.5
242 | - 修复腾讯云图床/Github图床上传问题
243 | - 图片上传失败时,取消停止进程
244 | - 新增图片上传时的并发数concurrency,使用Github图床时,并发问题严重,建议设置为1
245 |
246 | ### v1.9.4
247 | - 🔥 新增GitHub图床和又拍云图床
248 |
249 | ### v1.9.2
250 | - 修复上传图片到图床时可能由于用户代理缺失导致403问题
251 |
252 | ### v1.9.1
253 | - 修复不使用图床配置时报错的问题
254 |
255 | ### v1.9.0
256 | - 🔥 支持腾讯云/阿里云/七牛云图床链接替换语雀链接
257 |
258 | ### v1.8.0
259 | - 🔥 支持自定义的适配器 adapter,具体查看 [配置示例](https://github.com/x-cold/yuque-hexo/tree/master/test/custom-adapter-project)
260 |
261 | ### v1.7.0
262 |
263 | - 🔥 支持配置 lastGeneratePath,同步文章后会记录一个时间戳,下一次同步文档时不再清空全部文档,只同步修改时间大于这个时间戳的文档
264 | - 🔥 支持语雀提示区块语法
265 | - 🐸 修复 front-matter 中 “:” 等特殊字符会导致文章无法正常生成
266 | - 🐸 由于 [prettier 不再支持 Node 8](https://github.com/prettier/eslint-config-prettier/issues/140),markdown 格式化仅在 node 版本 >= 10 生效
267 | - 🐸 现在必须配置 YUQUE_TOKEN 工具才能正常工作
268 |
269 | ### v1.6.5
270 |
271 | - 🔥 支持过滤 public 文章
272 | - 🔥 生成的 markdown 自动格式化
273 | - 🔥 移除去除语雀的锚点
274 |
275 | ### v1.6.4
276 |
277 | - 🐸 修复多行
的[问题](https://github.com/x-cold/yuque-hexo/pull/59)
278 |
279 | ### v1.6.3
280 |
281 | - 🔥 支持嵌套的 categories 解析 #56
282 | - 🐸 使用 [filenamify](https://github.com/sindresorhus/filenamify) 修复因为特殊字符的标题,生成非法的文件名导致的程序错误
283 |
284 | ### v1.6.2
285 |
286 | - 🔥 使用 slug 自定义 [urlname](https://github.com/x-cold/yuque-hexo/pull/37)
287 |
288 | ### v1.6.1
289 |
290 | - 🐸 修复 tags 格式化[问题](https://github.com/x-cold/yuque-hexo/issues/31)
291 |
292 | ### v1.6.0
293 |
294 | - 🐸 修复 descrption 导致的 front-matter 解析错误[问题](https://github.com/x-cold/yuque-hexo/issues/27#issuecomment-490138318)
295 | - 🔥 支持私有仓库同步
296 | - 🔥 使用语雀官方的 SDK,支持 YUQUE_TOKEN,可以解除 API 调用次数限制
297 |
298 | ### v1.5.0
299 |
300 | - 支持自定义 front-matter
301 |
302 | ### v1.4.3
303 |
304 | - 支持过滤未发布文章 `onlyPublished`
305 |
306 | ### v1.4.2
307 |
308 | - 支持纯 markdown 导出
309 | - 支持请求并发数量参数 `concurrency`
310 |
311 | ### v1.4.0
312 |
313 | - 升级项目架构,增强扩展性,支持自定义 adpter
314 |
315 | ### v1.3.1
316 |
317 | - 修复 front-matter 处理格式问题
318 |
319 | ### v1.2.1
320 |
321 | - 修复 windows 环境下命令行报错的问题
322 | - 支持自定义文件夹和博客文件命名
323 |
324 | ### v1.1.1
325 |
326 | - 支持 hexo-front-matter,可以在文章中编辑 tags / date 等属性
327 |
--------------------------------------------------------------------------------
/adapter/hexo.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const ejs = require('ejs');
4 | const Entities = require('html-entities').AllHtmlEntities;
5 | const FrontMatter = require('hexo-front-matter');
6 | const { formatDate, formatRaw } = require('../util');
7 | const img2Cdn = require('../util/img2cdn');
8 | const config = require('../config');
9 |
10 |
11 | const entities = new Entities();
12 | // 背景色区块支持
13 | const colorBlocks = {
14 | ':::tips\n': '
',
15 | ':::danger\n': '
',
16 | ':::info\n': '
',
17 | '\\s+:::': '
',
18 | };
19 |
20 | // 文章模板
21 | const template = `---
22 | <%- matter -%>
23 |
24 | <%- raw -%>`;
25 |
26 | /**
27 | * front matter 反序列化
28 | * @description
29 | * docs: https://www.npmjs.com/package/hexo-front-matter
30 | *
31 | * @param {String} body md 文档
32 | * @return {String} result
33 | */
34 | function parseMatter(body) {
35 | body = entities.decode(body);
36 | try {
37 | // front matter信息的
换成 \n
38 | const regex = /(title:|layout:|tags:|date:|categories:){1}(\S|\s)+?---/gi;
39 | body = body.replace(regex, a =>
40 | a.replace(/(
|
|
)/gi, '\n')
41 | );
42 | // 支持提示区块语法
43 | for (const key in colorBlocks) {
44 | body = body.replace(new RegExp(key, 'igm'), colorBlocks[key]);
45 | }
46 | const result = FrontMatter.parse(body);
47 | result.body = result._content;
48 | if (result.date) {
49 | result.date = formatDate(result.date);
50 | }
51 | delete result._content;
52 | return result;
53 | } catch (error) {
54 | return {
55 | body,
56 | };
57 | }
58 | }
59 |
60 | /**
61 | * hexo 文章生产适配器
62 | *
63 | * @param {Object} post 文章
64 | * @return {String} text
65 | */
66 | module.exports = async function(post) {
67 | // 语雀img转成自己的cdn图片
68 | if (config.imgCdn.enabled) {
69 | post = await img2Cdn(post);
70 | }
71 | // matter 解析
72 | const parseRet = parseMatter(post.body);
73 | const { body, ...data } = parseRet;
74 | const { title, slug: urlname, created_at } = post;
75 | const raw = formatRaw(body);
76 | const date = data.date || formatDate(created_at);
77 | const tags = data.tags || [];
78 | const categories = data.categories || [];
79 | const props = {
80 | title: title.replace(/"/g, ''), // 临时去掉标题中的引号,至少保证文章页面是正常可访问的
81 | urlname,
82 | date,
83 | ...data,
84 | tags,
85 | categories,
86 | };
87 | const text = ejs.render(template, {
88 | raw,
89 | matter: FrontMatter.stringify(props),
90 | });
91 | return text;
92 | };
93 |
--------------------------------------------------------------------------------
/adapter/markdown.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const { formatRaw } = require('../util');
4 | const img2Cdn = require('../util/img2cdn');
5 | const config = require('../config');
6 |
7 | /**
8 | * markdown 文章生产适配器
9 | *
10 | * @param {Object} post 文章
11 | * @return {String} text
12 | */
13 | module.exports = async function(post) {
14 | // 语雀img转成自己的cdn图片
15 | if (config.imgCdn.enabled) {
16 | post = await img2Cdn(post);
17 | }
18 | const { body } = post;
19 | const raw = formatRaw(body);
20 | return raw;
21 | };
22 |
--------------------------------------------------------------------------------
/appveyor.yml:
--------------------------------------------------------------------------------
1 | environment:
2 | matrix:
3 | - nodejs_version: '8, 10, 12'
4 |
5 | install:
6 | - ps: Install-Product node $env:nodejs_version
7 | - npm i npminstall && node_modules\.bin\npminstall
8 |
9 | test_script:
10 | - node --version
11 | - npm --version
12 | - npm run test
13 |
14 | build: off
15 |
--------------------------------------------------------------------------------
/bin/yuque-hexo.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | 'use strict';
4 |
5 | const checkForUpdate = require('update-check');
6 | const chalk = require('chalk');
7 | const pkg = require('../package');
8 | const out = require('../lib/out');
9 | const Command = require('..');
10 |
11 | (async function() {
12 | let update = null;
13 |
14 | try {
15 | update = await checkForUpdate(pkg, {
16 | interval: 3600000, // For how long to cache latest version (default: 1 day)
17 | });
18 | } catch (err) {
19 | out.warn(`Failed to check for updates: ${err}`);
20 | }
21 |
22 | if (update) {
23 | out.info(`Current yuque-hexo version is ${chalk.yellow(pkg.version)}, and the latest version is ${chalk.green(update.latest)}. Please update!`);
24 | out.info('View more detail: https://github.com/x-cold/yuque-hexo#changelog');
25 | }
26 |
27 | new Command().start();
28 | })();
29 |
--------------------------------------------------------------------------------
/command/clean.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const Command = require('common-bin');
4 | const initConfig = require('../config'); // 初始化 config
5 | const cleaner = require('../lib/cleaner');
6 | const out = require('../lib/out');
7 |
8 | class CleanCommand extends Command {
9 | constructor(rawArgv) {
10 | super(rawArgv);
11 | this.usage = 'Usage: yuque-hexo clean';
12 | }
13 |
14 | async run() {
15 | if (!initConfig) {
16 | process.exit(0);
17 | }
18 | cleaner.cleanPosts();
19 | cleaner.clearCache();
20 | cleaner.clearLastGenerate();
21 | out.info('yuque-hexo clean done!');
22 | }
23 | }
24 |
25 | module.exports = CleanCommand;
26 |
--------------------------------------------------------------------------------
/command/sync.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const Command = require('common-bin');
4 | const initConfig = require('../config'); // 初始化 config
5 | const cleaner = require('../lib/cleaner');
6 | const Downloader = require('../lib/Downloader');
7 | const out = require('../lib/out');
8 |
9 | class SyncCommand extends Command {
10 | constructor(rawArgv) {
11 | super(rawArgv);
12 | this.usage = 'Usage: yuque-hexo sync';
13 | }
14 |
15 | async run() {
16 | if (!initConfig) {
17 | process.exit(0);
18 | }
19 |
20 | // clear previous directory.
21 | if (initConfig.lastGeneratePath === '') {
22 | out.info('clear previous directory.');
23 | cleaner.cleanPosts();
24 | }
25 |
26 | // get articles from yuque or cache
27 | const downloader = new Downloader(initConfig);
28 | await downloader.autoUpdate();
29 | out.info('yuque-hexo sync done!');
30 | }
31 | }
32 |
33 | module.exports = SyncCommand;
34 |
--------------------------------------------------------------------------------
/config.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const path = require('path');
4 | const lodash = require('lodash');
5 | const out = require('./lib/out');
6 |
7 | const cwd = process.cwd();
8 | const token = process.env.YUQUE_TOKEN;
9 | const defaultConfig = {
10 | postPath: 'source/_posts/yuque',
11 | cachePath: 'yuque.json',
12 | lastGeneratePath: '',
13 | mdNameFormat: 'title',
14 | baseUrl: 'https://www.yuque.com/api/v2/',
15 | token,
16 | login: '',
17 | repo: '',
18 | adapter: 'hexo',
19 | concurrency: 5,
20 | onlyPublished: false,
21 | onlyPublic: false,
22 | imgCdn: {
23 | concurrency: 0,
24 | enabled: false,
25 | imageBed: 'qiniu',
26 | host: '',
27 | bucket: '',
28 | region: '',
29 | prefixKey: '',
30 | },
31 | };
32 |
33 | function loadConfig() {
34 | const pkg = loadJson() || loadYaml();
35 | if (!pkg) {
36 | out.error('current directory should have a package.json');
37 | return null;
38 | }
39 | const { yuqueConfig } = pkg;
40 | if (!lodash.isObject(yuqueConfig)) {
41 | out.error('package.yueConfig should be an object.');
42 | return null;
43 | }
44 | const config = Object.assign({}, defaultConfig, yuqueConfig);
45 | return config;
46 | }
47 |
48 | function loadJson() {
49 | const pkgPath = path.join(cwd, 'package.json');
50 | // out.info(`loading config: ${pkgPath}`);
51 | try {
52 | const pkg = require(pkgPath);
53 | return pkg;
54 | } catch (error) {
55 | // do nothing
56 | }
57 | }
58 |
59 | function loadYaml() {
60 | // TODO
61 | }
62 |
63 | module.exports = loadConfig();
64 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const path = require('path');
4 | const Command = require('common-bin');
5 |
6 | class MainCommand extends Command {
7 | constructor(rawArgv) {
8 | super(rawArgv);
9 | this.usage = 'Usage: yuque-hexo
';
10 |
11 | // load sub command
12 | this.load(path.join(__dirname, 'command'));
13 | }
14 | }
15 |
16 | module.exports = MainCommand;
17 |
--------------------------------------------------------------------------------
/lib/Downloader.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const path = require('path');
4 | const fs = require('fs');
5 | const mkdirp = require('mkdirp');
6 | const lodash = require('lodash');
7 | const Queue = require('queue');
8 | const filenamify = require('filenamify');
9 | const YuqueClient = require('./yuque');
10 | const { isPost } = require('../util');
11 | const out = require('./out');
12 | const rimraf = require('rimraf');
13 |
14 | const cwd = process.cwd();
15 |
16 | // 需要提取的文章属性字段
17 | const PICK_PROPERTY = [
18 | 'title',
19 | 'description',
20 | 'created_at',
21 | 'updated_at',
22 | 'published_at',
23 | 'format',
24 | 'slug',
25 | 'last_editor',
26 | ];
27 |
28 | /**
29 | * Constructor 下载器
30 | *
31 | * @prop {Object} client 语雀 client
32 | * @prop {Object} config 知识库配置
33 | * @prop {String} cachePath 下载的文章缓存的 JSON 文件
34 | * @prop {String} postBasicPath 下载的文章最终生成 markdown 的目录
35 | * @prop {Array} _cachedArticles 文章列表
36 | *
37 | */
38 | class Downloader {
39 | constructor(config) {
40 | this.client = new YuqueClient(config);
41 | this.config = config;
42 | this.cachePath = path.join(cwd, config.cachePath);
43 | this.postBasicPath = path.join(cwd, config.postPath);
44 | this.lastGeneratePath = config.lastGeneratePath ? path.join(cwd, config.lastGeneratePath) : '';
45 | this._cachedArticles = [];
46 | this.fetchArticle = this.fetchArticle.bind(this);
47 | this.generatePost = this.generatePost.bind(this);
48 | this.lastGenerate = 0;
49 | if (this.lastGeneratePath !== '') {
50 | try {
51 | this.lastGenerate = Number(
52 | fs.readFileSync(this.lastGeneratePath).toString()
53 | );
54 | } catch (error) {
55 | out.warn(`get last generate time err: ${error}`);
56 | }
57 | }
58 | }
59 |
60 | /**
61 | * 下载文章详情
62 | *
63 | * @param {Object} item 文章概要
64 | * @param {Number} index 所在缓存数组的下标
65 | *
66 | * @return {Promise} data
67 | */
68 | fetchArticle(item, index) {
69 | const { client, _cachedArticles } = this;
70 | return function() {
71 | out.info(`download article body: ${item.title}`);
72 | return client.getArticle(item.slug).then(({ data: article }) => {
73 | _cachedArticles[index] = article;
74 | });
75 | };
76 | }
77 |
78 | /**
79 | * 下载所有文章
80 | * 并根据文章是否有更新来决定是否需要重新下载文章详情
81 | *
82 | * @return {Promise} queue
83 | */
84 | async fetchArticles() {
85 | const { client, config, postBasicPath } = this;
86 | const articles = await client.getArticles();
87 | if (!Array.isArray(articles.data)) {
88 | throw new Error(
89 | `fail to fetch article list, response: ${JSON.stringify(articles)}`
90 | );
91 | }
92 | out.info(`article amount: ${articles.data.length}`);
93 | const realArticles = articles.data
94 | .filter(article =>
95 | (config.onlyPublished ? !!article.published_at : true)
96 | )
97 | .filter(article => (config.onlyPublic ? !!article.public : true))
98 | .map(article => lodash.pick(article, PICK_PROPERTY));
99 |
100 | const deletedArticles = this._cachedArticles.filter(cache => realArticles.findIndex(item => item.slug === cache.slug) === -1);
101 |
102 | // 删除本地已存在的但是语雀上面被删除的文章
103 | for (const article of deletedArticles) {
104 | const fileName = filenamify(article[config.mdNameFormat]);
105 | const postPath = path.join(postBasicPath, `${fileName}.md`);
106 | rimraf.sync(postPath);
107 | }
108 |
109 | this._cachedArticles = this._cachedArticles.filter(cache => realArticles.findIndex(item => item.slug === cache.slug) !== -1);
110 |
111 | const queue = new Queue({ concurrency: config.concurrency });
112 |
113 | let article;
114 | let cacheIndex;
115 | let cacheArticle;
116 | let cacheAvaliable;
117 |
118 | const findIndexFn = function(item) {
119 | return item.slug === article.slug;
120 | };
121 |
122 | const { _cachedArticles } = this;
123 |
124 | for (let i = 0; i < realArticles.length; i++) {
125 | article = realArticles[i];
126 | cacheIndex = _cachedArticles.findIndex(findIndexFn);
127 | if (cacheIndex < 0) {
128 | // 未命中缓存,新增一条
129 | cacheIndex = _cachedArticles.length;
130 | _cachedArticles.push(article);
131 | queue.push(this.fetchArticle(article, cacheIndex));
132 | } else {
133 | cacheArticle = _cachedArticles[cacheIndex];
134 | cacheAvaliable =
135 | +new Date(article.updated_at) === +new Date(cacheArticle.updated_at);
136 | // 文章有变更,更新缓存
137 | if (!cacheAvaliable) {
138 | queue.push(this.fetchArticle(article, cacheIndex));
139 | }
140 | }
141 | }
142 |
143 | return new Promise((resolve, reject) => {
144 | queue.start(function(err) {
145 | if (err) return reject(err);
146 | out.info('download articles done!');
147 | resolve();
148 | });
149 | });
150 | }
151 |
152 | /**
153 | * 读取语雀的文章缓存 json 文件
154 | */
155 | readYuqueCache() {
156 | const { cachePath } = this;
157 | out.info(`reading from yuque.json: ${cachePath}`);
158 | try {
159 | const articles = require(cachePath);
160 | if (Array.isArray(articles)) {
161 | this._cachedArticles = articles;
162 | return;
163 | }
164 | } catch (error) {
165 | out.warn(error.message);
166 | // Do noting
167 | }
168 | this._cachedArticles = [];
169 | }
170 |
171 | /**
172 | * 写入语雀的文章缓存 json 文件
173 | */
174 | writeYuqueCache() {
175 | const { cachePath, _cachedArticles } = this;
176 | out.info(`writing to local file: ${cachePath}`);
177 | fs.writeFileSync(cachePath, JSON.stringify(_cachedArticles, null, 2), {
178 | encoding: 'UTF8',
179 | });
180 | }
181 |
182 | /**
183 | * 生成一篇 markdown 文章
184 | *
185 | * @param {Object} post 文章详情
186 | */
187 | async generatePost(post) {
188 | if (!isPost(post)) {
189 | out.error(`invalid post: ${post}`);
190 | return;
191 | }
192 |
193 | if (new Date(post.published_at).getTime() < this.lastGenerate) {
194 | out.info(`post not updated skip: ${post.title}`);
195 | return;
196 | }
197 |
198 | const { postBasicPath } = this;
199 | const { mdNameFormat, adapter } = this.config;
200 | const fileName = filenamify(post[mdNameFormat]);
201 | const postPath = path.join(postBasicPath, `${fileName}.md`);
202 | const internalAdapters = [ 'markdown', 'hexo' ];
203 | const adpaterPath = internalAdapters.includes(adapter)
204 | ? path.join(__dirname, '../adapter', adapter)
205 | : path.join(process.cwd(), adapter);
206 |
207 | let transform;
208 | try {
209 | transform = require(adpaterPath);
210 | } catch (error) {
211 | out.error(`adpater (${adapter}) is invalid.`);
212 | process.exit(-1);
213 | }
214 | out.info(`generate post file: ${postPath}`);
215 | const text = await transform(post);
216 | fs.writeFileSync(postPath, text, {
217 | encoding: 'UTF8',
218 | });
219 | }
220 |
221 | /**
222 | * 全量生成所有 markdown 文章
223 | */
224 | async generatePosts() {
225 | const { _cachedArticles, postBasicPath } = this;
226 | mkdirp.sync(postBasicPath);
227 | out.info(`create posts directory (if it not exists): ${postBasicPath}`);
228 | const promiseList = _cachedArticles.map(item => {
229 | return async () => {
230 | return await this.generatePost(item);
231 | };
232 | });
233 | // 并发数
234 | const concurrency = this.config.imgCdn.concurrency || promiseList.length;
235 | const queue = new Queue({ concurrency });
236 | queue.push(...promiseList);
237 | await new Promise((resolve, reject) => {
238 | queue.start(function(err) {
239 | if (err) return reject(err);
240 | resolve();
241 | });
242 | });
243 | }
244 |
245 | // 文章下载 => 增量更新文章到缓存 json 文件 => 全量生成 markdown 文章
246 | async autoUpdate() {
247 | this.readYuqueCache();
248 | await this.fetchArticles();
249 | this.writeYuqueCache();
250 | await this.generatePosts();
251 | if (this.lastGeneratePath) {
252 | fs.writeFileSync(this.lastGeneratePath, new Date().getTime().toString());
253 | }
254 | }
255 | }
256 |
257 | module.exports = Downloader;
258 |
--------------------------------------------------------------------------------
/lib/cleaner.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const path = require('path');
4 | const fs = require('fs');
5 | const rimraf = require('rimraf');
6 | const config = require('../config');
7 | const out = require('./out');
8 |
9 | const cwd = process.cwd();
10 |
11 | module.exports = {
12 | // clear directory of generated posts
13 | cleanPosts() {
14 | const { postPath } = config;
15 | const dist = path.join(cwd, postPath);
16 | out.info(`remove yuque posts: ${dist}`);
17 | rimraf.sync(dist);
18 | },
19 |
20 | // clear cache of posts' data
21 | clearCache() {
22 | const cachePath = path.join(cwd, 'yuque.json');
23 | try {
24 | out.info(`remove yuque.json: ${cachePath}`);
25 | fs.unlinkSync(cachePath);
26 | } catch (error) {
27 | out.warn(`remove empty yuque.json: ${error.message}`);
28 | }
29 | },
30 |
31 | // clear last generated timestamp file
32 | clearLastGenerate() {
33 | const { lastGeneratePath } = config;
34 | if (!lastGeneratePath) {
35 | return;
36 | }
37 | const dist = path.join(cwd, lastGeneratePath);
38 | out.info(`remove last generated timestamp: ${dist}`);
39 | rimraf.sync(dist);
40 | },
41 | };
42 |
--------------------------------------------------------------------------------
/lib/out.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const chalk = require('chalk');
4 |
5 | module.exports = {
6 | info(...args) {
7 | const prefix = chalk.green('[INFO]');
8 | args.unshift(prefix);
9 | console.log.apply(console, args);
10 | },
11 | warn(...args) {
12 | const prefix = chalk.yellow('[WARNING]');
13 | args.unshift(prefix);
14 | console.log.apply(console, args);
15 | },
16 | error(...args) {
17 | const prefix = chalk.red('[ERROR]');
18 | args.unshift(prefix);
19 | console.log.apply(console, args);
20 | },
21 | };
22 |
--------------------------------------------------------------------------------
/lib/qetag.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | // 计算文件的eTag,参数为buffer或者readableStream或者文件路径
4 | function getEtag(buffer, callback) {
5 |
6 | // 判断传入的参数是buffer还是stream还是filepath
7 | let mode = 'buffer';
8 |
9 | if (typeof buffer === 'string') {
10 | buffer = require('fs').createReadStream(buffer);
11 | mode = 'stream';
12 | } else if (buffer instanceof require('stream')) {
13 | mode = 'stream';
14 | }
15 |
16 | // sha1算法
17 | const sha1 = function(content) {
18 | const crypto = require('crypto');
19 | const sha1 = crypto.createHash('sha1');
20 | sha1.update(content);
21 | return sha1.digest();
22 | };
23 |
24 | // 以4M为单位分割
25 | const blockSize = 4 * 1024 * 1024;
26 | const sha1String = [];
27 | let prefix = 0x16;
28 | let blockCount = 0;
29 |
30 | // eslint-disable-next-line default-case
31 | switch (mode) {
32 | case 'buffer':
33 | // eslint-disable-next-line
34 | const bufferSize = buffer.length;
35 | blockCount = Math.ceil(bufferSize / blockSize);
36 |
37 | for (let i = 0; i < blockCount; i++) {
38 | sha1String.push(sha1(buffer.slice(i * blockSize, (i + 1) * blockSize)));
39 | }
40 | process.nextTick(function() {
41 | callback(calcEtag());
42 | });
43 | break;
44 | case 'stream':
45 | // eslint-disable-next-line no-case-declarations
46 | const stream = buffer;
47 | stream.on('readable', function() {
48 | let chunk;
49 | // eslint-disable-next-line no-cond-assign
50 | while (chunk = stream.read(blockSize)) {
51 | sha1String.push(sha1(chunk));
52 | blockCount++;
53 | }
54 | });
55 | stream.on('end', function() {
56 | callback(calcEtag());
57 | });
58 | break;
59 | }
60 |
61 | function calcEtag() {
62 | if (!sha1String.length) {
63 | return 'Fto5o-5ea0sNMlW_75VgGJCv2AcJ';
64 | }
65 | let sha1Buffer = Buffer.concat(sha1String, blockCount * 20);
66 |
67 | // 如果大于4M,则对各个块的sha1结果再次sha1
68 | if (blockCount > 1) {
69 | prefix = 0x96;
70 | sha1Buffer = sha1(sha1Buffer);
71 | }
72 |
73 | sha1Buffer = Buffer.concat(
74 | [ new Buffer([ prefix ]), sha1Buffer ],
75 | sha1Buffer.length + 1
76 | );
77 |
78 | return sha1Buffer.toString('base64')
79 | .replace(/\//g, '_').replace(/\+/g, '-');
80 |
81 | }
82 |
83 | }
84 |
85 | module.exports = getEtag;
86 |
--------------------------------------------------------------------------------
/lib/yuque.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const urllib = require('urllib');
4 | const debug = require('debug')('yuque-hexo:client');
5 |
6 | class YuqueClient {
7 | constructor(config) {
8 | const { baseUrl, login, repo, token } = config;
9 | this.config = Object.assign({}, config);
10 | this.token = token;
11 | this.config.namespace = `${login}/${repo}`;
12 | debug(`create client: baseUrl: ${baseUrl}, login: ${login}, repo: ${repo}`);
13 | }
14 |
15 | async _fetch(method, api, data) {
16 | const { baseUrl, namespace, timeout = 10000 } = this.config;
17 | const path = `${baseUrl}/repos/${namespace}${api}`;
18 | debug(`request data: api: ${path}, data: ${data}`);
19 | try {
20 | const result = await urllib.request(path, {
21 | dataType: 'json',
22 | method,
23 | data,
24 | timeout,
25 | headers: {
26 | 'User-Agent': 'yuque-hexo',
27 | 'X-Auth-Token': this.token,
28 | },
29 | });
30 | return result.data;
31 | } catch (error) {
32 | throw new Error(`请求数据失败: ${error.message}`);
33 | }
34 | }
35 |
36 | async getArticles() {
37 | const api = '/docs';
38 | const result = await this._fetch('GET', api);
39 | return result;
40 | }
41 |
42 | async getArticle(slug) {
43 | const api = `/docs/${slug}?raw=1`;
44 | const result = await this._fetch('GET', api);
45 | return result;
46 | }
47 |
48 | // async getToc() {
49 | // const api = '/toc';
50 | // const result = await this._fetch('GET', api);
51 | // return result;
52 | // }
53 | }
54 |
55 | module.exports = YuqueClient;
56 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "yuque-hexo",
3 | "version": "1.9.5",
4 | "description": "A downloader for articles from yuque",
5 | "main": "index.js",
6 | "scripts": {
7 | "clean": "rimraf coverage",
8 | "lint": "eslint .",
9 | "test": "npm run lint -- --fix && npm run test-local",
10 | "test-local": "egg-bin test",
11 | "cov": "egg-bin cov",
12 | "ci": "npm run clean && npm run lint && egg-bin cov"
13 | },
14 | "repository": {
15 | "type": "git",
16 | "url": "git+https://github.com/x-cold/yuque-hexo.git"
17 | },
18 | "bin": {
19 | "yuque-hexo": "bin/yuque-hexo.js"
20 | },
21 | "author": "x-cold ",
22 | "license": "MIT",
23 | "bugs": {
24 | "url": "https://github.com/x-cold/yuque-hexo/issues"
25 | },
26 | "homepage": "https://github.com/x-cold/yuque-hexo#readme",
27 | "dependencies": {
28 | "@yuque/sdk": "^1.1.1",
29 | "ali-oss": "6.17.0",
30 | "chalk": "^2.4.1",
31 | "common-bin": "^2.7.3",
32 | "cos-nodejs-sdk-v5": "^2.11.6",
33 | "debug": "^3.1.0",
34 | "depd": "^2.0.0",
35 | "ejs": "^3.1.6",
36 | "filenamify": "^4.1.0",
37 | "hexo-front-matter": "^0.2.3",
38 | "html-entities": "^1.2.1",
39 | "lodash": "^4.17.10",
40 | "mkdirp": "^1.0.0",
41 | "moment": "^2.22.2",
42 | "prettier": "^2.0.4",
43 | "qiniu": "^7.4.0",
44 | "queue": "^4.5.0",
45 | "rimraf": "^2.6.2",
46 | "superagent": "^7.0.2",
47 | "update-check": "^1.5.3",
48 | "upyun": "^3.4.6",
49 | "urllib": "^3.0.0"
50 | },
51 | "devDependencies": {
52 | "coffee": "^5.1.0",
53 | "egg-bin": "^4.8.3",
54 | "egg-ci": "^1.8.0",
55 | "eslint": "^5.4.0",
56 | "eslint-config-egg": "^7.1.0"
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/test/custom-adapter-project/custom.adapter.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | /**
4 | * 格式化 markdown 内容
5 | *
6 | * @param {String} body md 文档
7 | * @return {String} body
8 | */
9 | function formatRaw(body) {
10 | const multiBr = /(
[\s\n]){2}/gi;
11 | const multiBrEnd = /(
[\n]?){2}/gi;
12 | const brBug = /
/g;
13 | const hiddenContent = /