Repository: ulivz/vuepress-plugin-yuque Branch: master Commit: 0333ed30aaa3 Files: 28 Total size: 38.8 KB Directory structure: gitextract__k5qwlck/ ├── .editorconfig ├── .gitattributes ├── .github/ │ └── FUNDING.yml ├── .gitignore ├── .release-it.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── docs/ │ └── .vuepress/ │ ├── config.js │ └── styles/ │ └── index.styl ├── example/ │ └── .vuepress/ │ └── config.js ├── lib/ │ ├── __test__/ │ │ ├── __snapshots__/ │ │ │ └── toc.test.js.snap │ │ ├── toc.constant.js │ │ └── toc.test.js │ ├── client.js │ ├── compose.js │ ├── constant.js │ ├── fetch.js │ ├── html.js │ ├── index.js │ ├── spinner.js │ ├── stack.js │ ├── store.js │ ├── style.styl │ ├── toc.js │ └── yuque.js ├── package.json └── test/ └── index.test.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .editorconfig ================================================ root = true [*] indent_style = space indent_size = 2 end_of_line = lf charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true [*.md] trim_trailing_whitespace = false ================================================ FILE: .gitattributes ================================================ * text=auto ================================================ FILE: .github/FUNDING.yml ================================================ # These are supported funding model platforms github: ulivz # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] patreon: ulivz # Replace with a single Patreon username open_collective: # Replace with a single Open Collective username ko_fi: # Replace with a single Ko-fi username tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry liberapay: # Replace with a single Liberapay username issuehunt: # Replace with a single IssueHunt username otechie: # Replace with a single Otechie username custom: # Replace with a single custom sponsorship URL ================================================ FILE: .gitignore ================================================ node_modules .temp* docs2 dist ================================================ FILE: .release-it.json ================================================ { "src": { "tagName": "v%s", "commitMessage": "%s" }, "increment": "conventional:angular", "beforeChangelogCommand": "conventional-changelog -p angular -i CHANGELOG.md -s", "changelogCommand": "conventional-changelog -p angular | tail -n +3", "safeBump": false } ================================================ FILE: CHANGELOG.md ================================================ ## [0.6.1](https://github.com/ulivz/vuepress-plugin-yuque/compare/v0.6.0...v0.6.1) (2020-01-15) ### Bug Fixes * 1. fail to parse pre tag, may cause code loss issues; 2.transformheander should ignore empty string ([3726743](https://github.com/ulivz/vuepress-plugin-yuque/commit/3726743f97c722706e66d0a28048eda231102159)) * re-instantiate when repoId has been changed ([565a347](https://github.com/ulivz/vuepress-plugin-yuque/commit/565a347039cad6ab13d94ab069025de319e40da8)) # [0.6.0](https://github.com/ulivz/vuepress-plugin-yuque/compare/v0.5.5...v0.6.0) (2019-04-05) ### Features * yuqueLink & yuqueLinkHtml option ([b3d7d06](https://github.com/ulivz/vuepress-plugin-yuque/commit/b3d7d06)) ## [0.5.5](https://github.com/ulivz/vuepress-plugin-yuque/compare/v0.5.4...v0.5.5) (2019-04-05) ### Bug Fixes * typo ([2a0b86b](https://github.com/ulivz/vuepress-plugin-yuque/commit/2a0b86b)) ## [0.5.4](https://github.com/ulivz/vuepress-plugin-yuque/compare/v0.5.3...v0.5.4) (2019-04-05) ### Bug Fixes * ## ... shouldn't be considered as title ([f30c893](https://github.com/ulivz/vuepress-plugin-yuque/commit/f30c893)) ## [0.5.3](https://github.com/ulivz/vuepress-plugin-yuque/compare/v0.5.2...v0.5.3) (2019-04-05) ### Bug Fixes * **html:** table's styles cannot be removed ([1035eb3](https://github.com/ulivz/vuepress-plugin-yuque/commit/1035eb3)) ## [0.5.2](https://github.com/ulivz/vuepress-plugin-yuque/compare/v0.5.1...v0.5.2) (2019-04-05) ### Bug Fixes * **html:** need to escape twice ([923b31a](https://github.com/ulivz/vuepress-plugin-yuque/commit/923b31a)) ## [0.5.1](https://github.com/ulivz/vuepress-plugin-yuque/compare/v0.5.0...v0.5.1) (2019-04-05) ### Bug Fixes * **html:** regression of escape html in code ([8f638e3](https://github.com/ulivz/vuepress-plugin-yuque/commit/8f638e3)) # [0.5.0](https://github.com/ulivz/vuepress-plugin-yuque/compare/v0.4.0...v0.5.0) (2019-04-05) ### Features * **$core:** bump vuepress-plugin-medium ([cce0e73](https://github.com/ulivz/vuepress-plugin-yuque/commit/cce0e73)) * **html:** remove html tags in code and remove table styles ([fe75e46](https://github.com/ulivz/vuepress-plugin-yuque/commit/fe75e46)) # [0.4.0](https://github.com/ulivz/vuepress-plugin-yuque/compare/v0.3.1...v0.4.0) (2019-04-05) ### Features * **$core:** functional source option (using markdown when return 'markdown') ([5ef63cd](https://github.com/ulivz/vuepress-plugin-yuque/commit/5ef63cd)) ## [0.3.1](https://github.com/ulivz/vuepress-plugin-yuque/compare/v0.3.0...v0.3.1) (2019-02-14) ### Bug Fixes * **$core:** cannot use correct api base url for custom yuque domain ([818343e](https://github.com/ulivz/vuepress-plugin-yuque/commit/818343e)) # [0.3.0](https://github.com/ulivz/vuepress-plugin-yuque/compare/v0.2.1...v0.3.0) (2019-02-11) ### Bug Fixes * **$core:** throw error when toc's length is 1 ([df4e48c](https://github.com/ulivz/vuepress-plugin-yuque/commit/df4e48c)) ### Features * **$toc:** always set top-level node to branch node instead of leaf node ([44216dd](https://github.com/ulivz/vuepress-plugin-yuque/commit/44216dd)) ## [0.2.1](https://github.com/ulivz/vuepress-plugin-yuque/compare/v0.2.0...v0.2.1) (2019-02-11) ### Bug Fixes * **$core:** cannot fetch data from private repo (close: [#2](https://github.com/ulivz/vuepress-plugin-yuque/issues/2)) ([a59c420](https://github.com/ulivz/vuepress-plugin-yuque/commit/a59c420)) * **$core:** throw error since "prettify" doesn't check null (close: [#1](https://github.com/ulivz/vuepress-plugin-yuque/issues/1)) ([1110274](https://github.com/ulivz/vuepress-plugin-yuque/commit/1110274)) # [0.2.0](https://github.com/ulivz/vuepress-plugin-yuque/compare/v0.1.0...v0.2.0) (2019-02-08) ### Bug Fixes * **$core:** changing repoUrl under dev doesn't take effect ([93e0c06](https://github.com/ulivz/vuepress-plugin-yuque/commit/93e0c06)) * **$core:** empty node: Cannot read property 'format' of undefined ([3c0c3bc](https://github.com/ulivz/vuepress-plugin-yuque/commit/3c0c3bc)) * **$toc:** using safe pop to avoid empty stack ([6eb19b4](https://github.com/ulivz/vuepress-plugin-yuque/commit/6eb19b4)) ### Features * **$core:** enable .html suffix by default ([9a5167f](https://github.com/ulivz/vuepress-plugin-yuque/commit/9a5167f)) # 0.1.0 (2019-02-08) ### Bug Fixes * **$fetch:** cannot save cache properly ([4b21277](https://github.com/ulivz/vuepress-plugin-yuque/commit/4b21277)) ### Features * **$core:** `authToken` option ([e953197](https://github.com/ulivz/vuepress-plugin-yuque/commit/e953197)) * **$core:** `html` option ([185fcab](https://github.com/ulivz/vuepress-plugin-yuque/commit/185fcab)) * **$core:** `home` option and default title and description ([7b2505d](https://github.com/ulivz/vuepress-plugin-yuque/commit/7b2505d)) * **$core:** enable `medium-zoom` by default ([3538cc8](https://github.com/ulivz/vuepress-plugin-yuque/commit/3538cc8)) * **$core:** handle uncreated page ([5665483](https://github.com/ulivz/vuepress-plugin-yuque/commit/5665483)) * **$fetch:** env: `SKIP_CACHE` and `AUTH_TOKEN` ([c0530d9](https://github.com/ulivz/vuepress-plugin-yuque/commit/c0530d9)) * **$toc:** push branch node when the depth of last node is 1 ([8809986](https://github.com/ulivz/vuepress-plugin-yuque/commit/8809986)) * **$html:** extract headers from yuque's HTML markups ([88900ae](https://github.com/ulivz/vuepress-plugin-yuque/commit/88900ae)) * **$html:** highlight code fence block at build time ([475448b](https://github.com/ulivz/vuepress-plugin-yuque/commit/475448b)) * **$core:** leverage "html content" by default for now ([85801e7](https://github.com/ulivz/vuepress-plugin-yuque/commit/85801e7)) * **$fetch:** only enable cache under dev mode ([98cc5d1](https://github.com/ulivz/vuepress-plugin-yuque/commit/98cc5d1)) * **$core:** `repoUrl` option ([81d07dd](https://github.com/ulivz/vuepress-plugin-yuque/commit/81d07dd)) * **$core:** use page's default format ([7b41867](https://github.com/ulivz/vuepress-plugin-yuque/commit/7b41867)) ================================================ FILE: LICENSE ================================================ The MIT License (MIT) Copyright (c) ULVIZ (https://github.com/ulivz) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # vuepress-plugin-yuque [![NPM version](https://badgen.net/npm/v/vuepress-plugin-yuque)](https://npmjs.com/package/vuepress-plugin-yuque) [![NPM downloads](https://badgen.net/npm/dm/vuepress-plugin-yuque)](https://npmjs.com/package/vuepress-plugin-yuque) > 本插件需要 VuePress >= `1.0.0-alpha.37`。 ## 特性 - **简单**:Zero-Markdown,只需配置你的语雀 repo 地址,就能获得一个 VuePress 站点; - **高效**:自动生成的文档主页、侧边栏让你享受 “高效” 的文档生成体验; - **缓存**:完美地解决语雀 Open API 调用次数超限的问题; 想了解更多,请移步: - **案例** - [视频演示](https://player.youku.com/embed/XNDA1MzAwMDIzNg==) - [**Site**](https://antd-course.ulivz.com/) - [语雀](https://www.yuque.com/ant-design/course) - [源码](https://github.com/ulivz/vuepress-plugin-yuque/tree/master/example/.vuepress) - **文档** - [**Site**](https://vuepress-plugin-yuque.ulivz.com/) - [语雀](https://www.yuque.com/vuepress/vuepress-plugin-yuque) - [源码](https://github.com/ulivz/vuepress-plugin-yuque/tree/master/docs/.vuepress) ## 快速上手 在 VuePress 中配置 `repoUrl`: ```js // .vuepress/config.js module.exports = { title: 'Ant Design 实战教程', description: '基于 umi 的 Ant Design 实战教程', plugins: [ ['vuepress-plugin-yuque', { repoUrl: 'https://www.yuque.com/ant-design/course', }] ] } ``` 你就能获得这个[**Site**](https://antd-course.ulivz.com/),更多使用方法,可参见[文档](https://vuepress-plugin-yuque.ulivz.com/)。 ## Contributing 1. Fork it! 2. Create your feature branch: `git checkout -b my-new-feature` 3. Commit your changes: `git commit -am 'Add some feature'` 4. Push to the branch: `git push origin my-new-feature` 5. Submit a pull request :D ## Author **vuepress-plugin-yuque** © [ULVIZ](https://github.com/ulivz), Released under the [MIT](./LICENSE) License.
Authored and maintained by ULVIZ with help from contributors ([list](https://github.com/ulivz/vuepress-plugin-yuque/contributors)). > [github.com/ulivz](https://github.com/ulivz) · GitHub [@ULVIZ](https://github.com/ulivz) · Twitter [@_ulivz](https://twitter.com/_ulivz) ================================================ FILE: docs/.vuepress/config.js ================================================ module.exports = { head: [ ['link', { rel: 'icon', href: `https://cdn.nlark.com/yuque/0/2019/png/242808/1549571925285-2372b0a0-0234-421c-a139-00ea16f1a106.png` }], ['meta', { name: 'theme-color', content: '#3eaf7c' }], ['meta', { name: 'apple-mobile-web-app-capable', content: 'yes' }], ['meta', { name: 'apple-mobile-web-app-status-bar-style', content: 'black' }], ['meta', { name: 'msapplication-TileColor', content: '#000000' }], ['meta', { name: 'referrer', content: 'no-referrer' }] ], themeConfig: { repo: 'ulivz/vuepress-plugin-yuque', nav: [ { text: '指南', link: '/intro.html' }, { text: '配置', link: '/config.html' }, { text: 'CHANGELOG', link: '/changelog.html' }, ] }, async additionalPages () { const fetch = require('node-fetch') const response = await fetch('https://raw.githubusercontent.com/ulivz/vuepress-plugin-yuque/master/CHANGELOG.md') const content = await response.text() return [ { path: '/changelog.html', content } ] }, plugins: [ [ require('../../lib'), { html: true, yuqueLink: true, repoUrl: 'https://www.yuque.com/vuepress/vuepress-plugin-yuque', home: { actionText: 'Getting Started →', actionLink: '/intro.html', heroImage: 'https://cdn.nlark.com/yuque/0/2019/png/242808/1549571925285-2372b0a0-0234-421c-a139-00ea16f1a106.png', footer: `Copyright © ULIVZ`, features: [ { title: '简单', details: 'Zero-Markdown,只需配置你的语雀 repo 地址,就能获得一个 VuePress 站点' }, { title: '高效', details: '自动生成的文档主页、侧边栏让你享受 “高效” 的文档生成体验' }, { title: '缓存', details: '完美地解决语雀 Open API 调用次数超限的问题' }, ] } } ] ] } ================================================ FILE: docs/.vuepress/styles/index.styl ================================================ .content img max-width 100% !important .home .hero img max-height 250px!important ================================================ FILE: example/.vuepress/config.js ================================================ module.exports = { title: 'Ant Design 实战教程', description: '基于 umi 的 Ant Design 实战教程', head: [ ['link', { rel: 'icon', href: `https://cdn.nlark.com/yuque/0/2019/png/242808/1549571925285-2372b0a0-0234-421c-a139-00ea16f1a106.png` }], ['meta', { name: 'theme-color', content: '#3eaf7c' }], ['meta', { name: 'apple-mobile-web-app-capable', content: 'yes' }], ['meta', { name: 'apple-mobile-web-app-status-bar-style', content: 'black' }], ['meta', { name: 'msapplication-TileColor', content: '#000000' }], ['meta', { name: 'referrer', content: 'no-referrer' }] ], plugins: [ [ require('../../lib'), { repoUrl: 'https://www.yuque.com/ant-design/course', home: { features: [ { title: '循序渐进', details: '本教程的难度依次递进,为阅读者呈现舒适的学习曲线' }, { title: '值得信赖', details: '由 antd 团队亲自打造,从技术栈、生态、研发流程等来为你提供系统化的学习体验' }, { title: '最佳实践', details: '通过结合实际开发过程中的案例,来描述不同场景下的最佳实践' }, ] } } ] ] } ================================================ FILE: lib/__test__/__snapshots__/toc.test.js.snap ================================================ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`getSidebarByToc 1`] = ` Array [ Object { "children": Array [], "collapsable": false, "path": "/intro.html", "title": "前言", }, Object { "children": Array [ Array [ "/sc1lvc.html", "前端开发的演变", ], Array [ "/wybhm9.html", "初始化项目", ], Array [ "/fd5af7.html", "第一个组件", ], Array [ "/agmsgo.html", "使用 Ant Design 组件", ], Array [ "/goozth.html", "受控组件与非受控组件", ], ], "collapsable": false, "title": "第一章: 基础知识", }, Object { "children": Array [ Array [ "/layout.html", "基本布局", ], Array [ "/ghalng.html", "侧边导航", ], Array [ "/ipsba8.html", "路由配置", ], ], "collapsable": false, "path": "/chapter2.html", "title": "第二章: 布局与路由", }, Object { "children": Array [ Array [ "/abl3ad.html", "使用 model", ], Array [ "/dsl8ee.html", "搭建基于 model 的卡片列表页面", ], Array [ "/ig6mzb.html", "在 model 中请求服务端数据", ], Array [ "/kmmq56.html", "模拟服务端数据", ], ], "collapsable": false, "path": "/chapter3.html", "title": "第三章: 小试牛刀", }, Object { "children": Array [ Array [ "/ndfx96.html", "表格", ], Array [ "/up1dn3.html", "表单", ], Array [ "/dk8xsp.html", "图表", ], ], "collapsable": false, "path": "/chapter4.html", "title": "第四章: 复杂页面", }, Object { "children": Array [ Array [ "/customized_styles.html", "自定义样式", ], Array [ "/wvbsue.html", "上传与下载", ], Array [ "/aut0sr.html", "国际化", ], Array [ "/lifemethods.html", "React 的生命周期", ], Array [ "/auth.html", "权限", ], Array [ "/unittest.html", "单元测试", ], Array [ "/typescript.html", "使用 TypeScript", ], ], "collapsable": false, "title": "第五章: 进阶功能", }, Object { "children": Array [ Array [ "/byllph.html", "你需要了解的 ES6 语法", ], Array [ "/tydf0a.html", "深入理解 umi", ], Array [ "/about.html", "关于我们", ], ], "collapsable": false, "title": "附录", }, ] `; exports[`getSidebarByToc 2`] = ` Array [ Object { "children": Array [ Array [ "/intro.html", "介绍", ], Array [ "/getting-started.html", "快速上手", ], Array [ "/export-first-website.html", "导出第一个站点", ], ], "collapsable": false, "title": "指南", }, Object { "children": Array [], "collapsable": false, "path": "/config.html", "title": "配置", }, ] `; exports[`getSidebarByToc 3`] = ` Array [ Object { "children": Array [ Array [ "/intro.html", "介绍", ], Array [ "/getting-started.html", "快速上手", ], Array [ "/generate-homepage.html", "生成主页", ], Array [ "/cache.html", "缓存", ], Array [ "/private-repo.html", "私有仓库", ], ], "collapsable": false, "title": "指南", }, Object { "children": Array [], "collapsable": false, "path": "/config.html", "title": "配置", }, Object { "children": Array [], "collapsable": false, "title": "进阶", }, Object { "children": Array [], "collapsable": false, "path": "/a.html", "title": "A", }, Object { "children": Array [ Array [ "/b-1.html", "B-1", ], ], "collapsable": false, "path": "/b.html", "title": "B", }, Object { "children": Array [], "collapsable": false, "path": "/c.html", "title": "C", }, ] `; ================================================ FILE: lib/__test__/toc.constant.js ================================================ const TOC_1 = [ { title: '前言', slug: 'intro', depth: 1 }, { title: '第一章: 基础知识', slug: '#', depth: 1 }, { title: '前端开发的演变', slug: 'sc1lvc', depth: 2 }, { title: '初始化项目', slug: 'wybhm9', depth: 2 }, { title: '第一个组件', slug: 'fd5af7', depth: 2 }, { title: '使用 Ant Design 组件', slug: 'agmsgo', depth: 2 }, { title: '受控组件与非受控组件', slug: 'goozth', depth: 2 }, { title: '第二章: 布局与路由', slug: 'chapter2', depth: 1 }, { title: '基本布局', slug: 'layout', depth: 2 }, { title: '侧边导航', slug: 'ghalng', depth: 2 }, { title: '路由配置', slug: 'ipsba8', depth: 2 }, { title: '第三章: 小试牛刀', slug: 'chapter3', depth: 1 }, { title: '使用 model', slug: 'abl3ad', depth: 2 }, { title: '搭建基于 model 的卡片列表页面', slug: 'dsl8ee', depth: 2 }, { title: '在 model 中请求服务端数据', slug: 'ig6mzb', depth: 2 }, { title: '模拟服务端数据', slug: 'kmmq56', depth: 2 }, { title: '第四章: 复杂页面', slug: 'chapter4', depth: 1 }, { title: '表格', slug: 'ndfx96', depth: 2 }, { title: '表单', slug: 'up1dn3', depth: 2 }, { title: '图表', slug: 'dk8xsp', depth: 2 }, { title: '第五章: 进阶功能', slug: '#', depth: 1 }, { title: '自定义样式', slug: 'customized_styles', depth: 2 }, { title: '上传与下载', slug: 'wvbsue', depth: 2 }, { title: '国际化', slug: 'aut0sr', depth: 2 }, { title: 'React 的生命周期', slug: 'lifemethods', depth: 2 }, { title: '权限', slug: 'auth', depth: 2 }, { title: '单元测试', slug: 'unittest', depth: 2 }, { title: '使用 TypeScript', slug: 'typescript', depth: 2 }, { title: '附录', slug: '#', depth: 1 }, { title: '你需要了解的 ES6 语法', slug: 'byllph', depth: 2 }, { title: '深入理解 umi', slug: 'tydf0a', depth: 2 }, { title: '关于我们', slug: 'about', depth: 2 } ] const TOC_2 = [ { title: '指南', slug: '#', depth: 1 }, { title: '介绍', slug: 'intro', depth: 2 }, { title: '快速上手', slug: 'getting-started', depth: 2 }, { title: '导出第一个站点', slug: 'export-first-website', depth: 2 }, { title: '配置', slug: 'config', depth: 1 } ] const TOC_3 = [ { title: '指南', slug: '#', depth: 1 }, { title: '介绍', slug: 'intro', depth: 2 }, { title: '快速上手', slug: 'getting-started', depth: 2 }, { title: '生成主页', slug: 'generate-homepage', depth: 2 }, { title: '缓存', slug: 'cache', depth: 2 }, { title: '私有仓库', slug: 'private-repo', depth: 2 }, { title: '配置', slug: 'config', depth: 1 }, { title: '进阶', slug: '#', depth: 1 }, { title: 'A', slug: 'a', depth: 1 }, { title: 'B', slug: 'b', depth: 1 }, { title: 'B-1', slug: 'b-1', depth: 2 }, { title: 'C', slug: 'c', depth: 1 }, ] module.exports = [ TOC_1, TOC_2, TOC_3 ] ================================================ FILE: lib/__test__/toc.test.js ================================================ const { getSidebarByToc } = require('../toc') const tocs = require('./toc.constant') test('getSidebarByToc', () => { tocs.forEach(toc => { const sidebar = getSidebarByToc(toc) expect(sidebar).toMatchSnapshot() }) }) ================================================ FILE: lib/client.js ================================================ import './style.styl' ================================================ FILE: lib/compose.js ================================================ /** * Expose compose. */ module.exports = function compose(...processors) { if (processors.length === 0) return (input) => input if (processors.length === 1) return processors[0] return processors.reduce((prev, next) => { return (...args) => next(prev(...args)) }) } ================================================ FILE: lib/constant.js ================================================ const pkg = require('./../package') module.exports = { PACKAGE_NAME: pkg.name } ================================================ FILE: lib/fetch.js ================================================ /** * Module dependencies. */ const fetch = require('node-fetch') /** * Expose a wrapped fetch which handles network error. */ module.exports = function (url, options = {}) { return fetch(url, options) .then(handleResponse, handleNetworkError) } /** * Response handler * * @param {Response} response * @returns {Promise>|never} */ function handleResponse(response) { if (response.ok) { return response.json() } return response.json().then(error => { throw error }) } /** * Network error handler * * @param {Error} error */ function handleNetworkError(error) { throw { msg: error.message } } ================================================ FILE: lib/html.js ================================================ /** * Module dependencies. */ const cheerio = require('cheerio') const compose = require('./compose') const escapeHtml = require('escape-html') /** * Expose a object containing the utilities of handling markups. */ module.exports = { prettify } const transformHeaders = compose( ...[ 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', ].map((h, i) => $ => { $(h).replaceWith((_i, el) => { const text = $(el).text().trim() if(!text) { return } return `\n\n ${getHash(i + 1)} ${text} \n\n` }) return $ } ), highlight, escapeHTMLTagInCode, removeTableStyls, ) function getHash(i) { return [...Array(i)].map(() => '#').join('') } function prettify(html) { html = prune(html) const $ = cheerio.load(html, { // ref: https://github.com/cheeriojs/cheerio/issues/565 decodeEntities: false }) transformHeaders($) return $('body').html() } function prune(html) { return html .replace(/

<\/p>/g, '') .replace(/

<\/span><\/p>/g, '') } const CODE_FENCE = '```' function highlight($) { $('pre').replaceWith((i, el) => { const $el = $(el) const $code = $el.children('code') const lang = $el.attr('data-lang') const code = $code.length ? $code.text() : $el.text() return `\n\n ${CODE_FENCE} ${lang} \n ${code} \n ${CODE_FENCE} \n\n` }) return $ } function escapeHTMLTagInCode($) { $('code').replaceWith((i, el) => { const content = $(el).text() return escapeHtml(`${escapeHtml(content)}`) }) return $ } function removeTableStyls($) { $('table').css('width', '') return $ } ================================================ FILE: lib/index.js ================================================ /** * Module dependencies. */ const assert = require('assert') const { chalk, parseFrontmatter, path } = require('@vuepress/shared-utils') const Yuque = require('./yuque') const { getSidebarByToc } = require('./toc') const spinner = require('./spinner') const { prettify } = require('./html') const { PACKAGE_NAME } = require('./constant') const debug = require('debug')(PACKAGE_NAME) /** * Expose vuepress-plugin-yuque */ module.exports = (opts, ctx) => { return { name: PACKAGE_NAME, enhanceAppFiles: [ path.join(__dirname, 'client.js') ], plugins: [ ['@vuepress/medium-zoom', true], ], chainMarkdown(config) { if (opts.html) { return } // Disable `html` for now. Many yuque writers would write raw HTML in the markdown // But only these markups under inline code(`) will be transpiled successfully when // `html` is enabled. config .options .html(false) }, async ready() { let { repoId, repoUrl, authToken, source } = opts if (repoId) { assert( typeof repoId === 'string' || typeof repoId === 'number', `[${PACKAGE_NAME}] repoId should be string or number, but got ${repoId}` ) } else if (repoUrl) { assert( typeof repoUrl === 'string', `[${PACKAGE_NAME}] repoUrl should be string, but got ${repoId}` ) } else { throw new Error(`Expected repoId or repoUrl`) } Yuque.setAuthToken(authToken) if (repoUrl) { const targetRepo = await Yuque.inferRepoByUrl(repoUrl) repoId = targetRepo.id } Yuque.setRepoId(repoId) const yuque = Yuque.getInstance() spinner.start(`Fetching repo detail ...`) const repoDetail = await yuque.getRepoDetail() if (!(repoDetail && repoDetail.data && repoDetail.data.name)) { spinner.fail( `Please check your repoId: ${chalk.yellow(repoId)} ` + `response: ${JSON.stringify(repoDetail)}` ) process.exit(1) } spinner.succeed(`Retrieved repo detail`) const { name, description, user } = repoDetail.data spinner.succeed( `Your Yuque website: ${chalk.green(name)} ` + `${chalk.gray(description)} ...` ) spinner.start(`Fetching TOC ... `) const toc = await yuque.getToc() assert( toc && toc.data, `[${PACKAGE_NAME}] cannot get toc of ${repoId}` ) debug('toc', toc.data) spinner.succeed(`Retrieved TOC`) // Apply sidebar config const sidebar = getSidebarByToc(toc.data) ctx.siteConfig.themeConfig = ctx.siteConfig.themeConfig || {} ctx.siteConfig.themeConfig.sidebar = sidebar if (!ctx.siteConfig.title) { ctx.siteConfig.title = name } if (!ctx.siteConfig.description) { ctx.siteConfig.description = description } let defaultActionLink // Apply pages for (const page of toc.data) { const { title, slug } = page // Once the slug is equal to '#', it means that current node // is an empty node. if (!slug || slug === '#') { continue } spinner.start(`Fetching ${chalk.cyan(title)} ... `) const { status, data } = await yuque.getPage(slug) debug('status = ', status) let postContent if (status && status === 404) { postContent = `# ${title}\n > 此文档尚未创建` } else { const useMarkdown = typeof source === 'string' ? source === 'markdown' : typeof source === 'function' ? source(page) === 'markdown' : false if (useMarkdown || data.format === 'markdown') { postContent = data.body || '' } else { postContent = prettify(data.body_html || '') } } const { data: frontmatter, content: strippedContent } = parseFrontmatter(postContent) const inferredTitle = inferTitle({}, strippedContent) let content = inferredTitle ? postContent : `# ${title} \n\n ${postContent}` if (opts.yuqueLink) { content += opts.yuqueLinkHtml || `

使用语雀查看` } const permalink = `/${slug}.html` await ctx.addPage({ content, frontmatter, permalink }) if (!defaultActionLink) { defaultActionLink = permalink } spinner.succeed(`Retrieved ${chalk.cyan(title)} ... `) } if (ctx.pages.every(page => page.path !== '/')) { const { home = {} } = opts const { actionText, actionLink, heroImage, features, footer } = home spinner.info(`Apply default homepage`) await ctx.addPage({ content: '', permalink: '/', frontmatter: { home: true, heroImage: heroImage || user.large_avatar_url, actionText: actionText || 'Getting Started →', actionLink: actionLink || defaultActionLink, features: features || undefined, footer: footer || `Copyright © ${user.name}`, } }) } } } } /** * Infer title, forked from VuePress. */ function inferTitle(frontmatter, strippedContent) { if (frontmatter.home) { return 'Home'; } if (frontmatter.title) { return frontmatter.title } const match = strippedContent.trim().match(/^#\s+(.*)/); if (match) { return match[1]; } }; ================================================ FILE: lib/spinner.js ================================================ /** * Module dependencies. */ const ora = require('ora') const { chalk } = require('@vuepress/shared-utils') /** * Expose a singleton of spinner */ module.exports = !process.env.DEBUG ? ora() : [ 'frame', 'clear', 'render', 'start', 'stop', 'succeed', 'fail', 'warn', 'info', 'stopAndPersist' ].reduce((memo, key) => { memo[key] = curry(key) return memo }, {}) function curry(name) { return (...args) => console.log(chalk.cyan(`spinner:${name}`), ...args) } ================================================ FILE: lib/stack.js ================================================ /** * Expose a simple stack */ module.exports = class Stack { constructor() { this.elements = [] } push(element) { this.elements.push(element) } pop() { return this.elements.pop() } peek() { return this.elements[this.size() - 1] } size() { return this.elements.length } } ================================================ FILE: lib/store.js ================================================ /** * Module dependencies. */ const Conf = require('conf') /** * Expose a object containing the utilities of cache. */ module.exports = { get, init } let store function init(configName) { store = new Conf({ configName: configName }) return store } function get(configName) { if (store) { return store } return init(configName) } ================================================ FILE: lib/style.styl ================================================ .yuque-link border: 1px solid #333; padding: 5px 10px; color: #333; border-radius: 3px; font-weight: 200; text-decoration none svg line-height: 20px; vertical-align: text-bottom; ================================================ FILE: lib/toc.js ================================================ /** * Module dependencies. */ const Stack = require('./stack') /** * Expose a object containing the utilities of TOC. */ module.exports = { getSidebarByToc } /** * Transform yuque's toc to vuepress's sidebar config. * * @param {Array>} toc * @returns {Array} */ function getSidebarByToc(toc) { if (toc.length === 1) { return [{ path: toc[0].slug, title: toc[0].title, collapsable: false }] } const stack = new Stack() const sidebar = [] stack.push({ children: sidebar }) for (let i = 0, l = toc.length; i < l; i++) { const prev = toc[i - 1] const cur = toc[i] const next = toc[i + 1] if (!prev) { if (next.depth >= cur.depth) { pushBranch(cur, stack) } else { pushLeaf(cur, stack) } } else if (!next) { if (cur.depth >= prev.depth) { pushLeaf(cur, stack) } else { stack.pop() if (cur.depth === 1) { pushBranch(cur, stack) } else { pushLeaf(cur, stack) } } } else { // Current node is the children node of previous node if (cur.depth > prev.depth) { if (next.depth > cur.depth) { pushBranch(cur, stack) } else { pushLeaf(cur, stack) } // Current node is the adjacent node of previous node } else if (cur.depth === prev.depth) { if (next.depth > cur.depth) { safePop(stack) pushBranch(cur, stack) } else if (next.depth === cur.depth) { if (cur.depth === 1) { safePop(stack) pushBranch(cur, stack) } else { pushLeaf(cur, stack) } } else { pushLeaf(cur, stack) } // Current node is the adjacent node previous node's parnent's node } else if (cur.depth < prev.depth) { if (next.depth > cur.depth) { safePop(stack) pushBranch(cur, stack) } else if (next.depth === cur.depth) { if (cur.depth === 1) { safePop(stack) pushBranch(cur, stack) } else { safePop(stack) pushLeaf(cur, stack) } } else { safePop(stack) pushLeaf(cur, stack) } } } } return sidebar } function safePop(stack) { if (stack.size() === 1) { return } stack.pop() } /** * Push branch node * * @param {TocNode} node * @param {Stack} stack */ function pushBranch(node, stack) { const group = getBranch(node) stack.peek().children.push(group) stack.push(group) } /** * Push leaf node * * @param {TocNode} node * @param {Stack} stack */ function pushLeaf(node, stack) { stack.peek().children.push(getLeaf(node)) } /** * Get leaf node for vuepress's sidebar group * * @param {string} title * @param {string} slug * @returns {Array} */ function getLeaf({ title, slug }) { return [`/${slug}.html`, title] } /** * Get branch node for vuepress's sidebar group * * @param {string} title * @param {string} slug * @returns {SidebarGroup} */ function getBranch({ title, slug }) { const group = { title, collapsable: false, children: [] } if (slug && slug !== '#') { group.path = `/${slug}.html` } return group } ================================================ FILE: lib/yuque.js ================================================ const assert = require('assert') const url = require('url') const wfetch = require('./fetch') const store = require('./store') const { hash, logger, chalk } = require('@vuepress/shared-utils') const { PACKAGE_NAME } = require('./constant') const debug = require('debug')(`${PACKAGE_NAME}:yuque`) const AUTH_TOKEN = process.env.YUQUE_QUTH_TOKEN const SKIP_CACHE = process.env.SKIP_CACHE const isProduction = process.env.NODE_ENV === 'production' let instance let repoId let authToken module.exports = class Yuque { static setRepoId(id) { repoId = id } static setAuthToken(token) { authToken = token } static getInstance() { if (!instance) { instance = new Yuque(repoId) } if (instance.repoId !== repoId) { instance = new Yuque(repoId) } return instance } static async fetch(url, options = {}) { debug('fetch', url, 'with', JSON.stringify(options, null, 2)) const { useCache = true } = options const key = hash(url + JSON.stringify(options)) const cacheKey = `fetch.${key}` let yuque if (repoId) { yuque = Yuque.getInstance() } if (yuque && !isProduction && useCache && !SKIP_CACHE) { const cached = yuque.store.get(cacheKey) if (cached) { debug(`Using cache for ${chalk.yellow(url)}`) return cached } } let response const foptions = {} if (authToken || AUTH_TOKEN) { foptions.headers = { 'X-Auth-Token': authToken || AUTH_TOKEN } } try { response = await wfetch(url, foptions) yuque && yuque.store.set(cacheKey, response) return response } catch (e) { return e } } static async get(base, path) { path = `${base}${path}` return Yuque.fetch(path) } static async getRepos(base, repoId) { return Yuque.get(base, `groups/${repoId}/repos`) } static async getRepoDetail(base, repoId) { return Yuque.get(base, `repos/${repoId}`) } static async getToc(base, repoId) { return this.get(base, `repos/${repoId}/toc`) } static async getPage(base, repoId, slug) { return this.get(base, `repos/${repoId}/docs/${slug}?raw=1`) } static async inferRepoByUrl(repoUrl) { const { protocol, host, pathname } = url.parse(repoUrl) assert( typeof pathname === 'string', `[CANNOT_RESOLVE_PATHNAME] Cannot infer repoId from repoUrl: ${repoUrl}` ) const normalizedPathname = pathname.replace(/(^\/|\/$)/g, '') const [groupId, repoId] = normalizedPathname.split('/') assert( typeof groupId === 'string' && typeof repoId === 'string', `[CANNOT_PARSE_PATHNAME] Cannot infer repoId from repoUrl: ${repoUrl}` ) const base = `${protocol}//${host}/api/v2/` Yuque.base = base const { data: repos } = await Yuque.getRepos(base, groupId) assert( Array.isArray(repos), `[CANNOT_FIND_GROUP] Cannot infer repoId from repoUrl: ${repoUrl}` ) const targetRepo = repos.find(repo => repo.namespace === normalizedPathname) assert( typeof targetRepo === 'object', `[CANNOT_FIND_REPO] Cannot infer repoId from repoUrl: ${repoUrl}` ) return targetRepo } constructor(repoId) { this.repoId = repoId this.store = store.get(`yuque_repo_${repoId}`) } get base() { return Yuque.base || 'https://www.yuque.com/api/v2/' } async getRepos() { return Yuque.getRepos(this.base, this.repoId) } async getRepoDetail() { return Yuque.getRepoDetail(this.base, this.repoId) } async getToc() { return Yuque.getToc(this.base, this.repoId) } async getPage(slug) { return Yuque.getPage(this.base, this.repoId, slug) } } ================================================ FILE: package.json ================================================ { "name": "vuepress-plugin-yuque", "version": "0.6.1", "description": "I: Yuque Repo, O: VuePress Site!", "main": "lib/index.js", "files": [ "lib" ], "scripts": { "test": "jest", "lint": "xo", "dev": "vuepress dev docs --temp .temp", "build": "vuepress build docs --temp .temp", "dev:example": "vuepress dev example --temp .temp2", "build:example": "vuepress build example --temp .temp2", "release": "release-it" }, "repository": { "url": "ulivz/vuepress-plugin-yuque", "type": "git" }, "author": "ulivz", "license": "MIT", "dependencies": { "@vuepress/plugin-medium-zoom": "^1.0.0-alpha.44", "cheerio": "^1.0.0-rc.2", "conf": "^2.2.0", "debug": "^4.1.1", "node-fetch": "^2.3.0", "ora": "^3.0.0" }, "devDependencies": { "conventional-changelog-cli": "^2.0.1", "eslint-config-sherry": "0.0.1", "husky": "1.2.0", "jest": "23.6.0", "lint-staged": "8.1.0", "release-it": "v7.4.8", "vuepress": "1.0.0-alpha.44", "xo": "0.23.0", "escape-html": "^1.0.3" }, "jest": { "testEnvironment": "node" }, "xo": { "extends": [ "sherry" ], "envs": [ "jest" ] }, "husky": { "hooks": { "pre-commit": "lint-staged" } }, "lint-staged": { "*.{js}": [ "xo --fix", "git add" ] } } ================================================ FILE: test/index.test.js ================================================ const vuepressPluginYuque = require('../') test('main', () => { expect(typeof vuepressPluginYuque).toBe('function') })