├── .eslintrc.js ├── .eslintrc.origin.js ├── .gitignore ├── .gitignore.origin ├── .vscode ├── launch.json ├── settings.json └── tasks.json ├── .vscodeignore ├── CHANGELOG.md ├── CHANGELOG.origin.md ├── CONTRIBUTING.origin.md ├── LICENSE ├── README.en.md ├── README.md ├── README.origin.md ├── USAGE.md ├── azure-pipelines.yml ├── context.json ├── dev.md ├── docs ├── wpls-flow-知乎文章发布.png ├── wpls-flow.drawio └── wpls-flow.png ├── package-lock.json ├── package.cmd ├── package.json ├── release_notes ├── 0.2.0.md ├── 0.2.1.md ├── 0.2.2.md ├── 0.2.3.md ├── 0.3.0.md └── 0.4.0.md ├── res ├── media │ ├── dark │ │ ├── collect.svg │ │ ├── delete.svg │ │ ├── drafts_white_24dp.svg │ │ ├── login_white_24dp.svg │ │ ├── publish_white_24dp.svg │ │ ├── refresh_white_24dp.svg │ │ ├── right-arrow.svg │ │ └── search_white_24dp.svg │ ├── extension.png │ ├── light │ │ ├── collect.svg │ │ ├── delete.svg │ │ ├── drafts_black_24dp.svg │ │ ├── login_black_24dp.svg │ │ ├── outline_drafts_black_24dp.png │ │ ├── outline_login_black_24dp.png │ │ ├── outline_publish_black_24dp.png │ │ ├── publish_black_24dp.svg │ │ ├── refresh_black_24dp.svg │ │ ├── right-arrow.svg │ │ └── search_black_24dp.svg │ ├── local_cafe_black_48dp.svg │ ├── outline_local_cafe_black_48dp.png │ ├── vs-code-extension-search-zhihu-this.png │ ├── vs-code-extension-search-zhihu.png │ ├── zhihu-logo-fluent.svg │ └── zhihu-logo-material.svg ├── shell │ ├── linux.sh │ ├── mac.applescript │ └── pc.ps1 └── template │ ├── article.pug │ ├── captcha.pug │ ├── css.pug │ ├── css │ ├── captcha.css │ ├── global-vs.css │ ├── global.css │ └── qrcode.css │ ├── header.pug │ ├── js │ └── global.js │ ├── qrcode.pug │ └── questions-answers.pug ├── src ├── const │ ├── CMD.ts │ ├── ENUM.ts │ ├── HTTP.ts │ ├── PATH.ts │ ├── REG.ts │ └── URL.ts ├── extension.ts ├── global │ ├── cache.ts │ ├── cookie.ts │ ├── globa-var.ts │ └── logger.ts ├── lang │ └── completion-provider.ts ├── model │ ├── article │ │ └── article-detail.ts │ ├── error │ │ └── error.model.ts │ ├── hot-story.model.ts │ ├── login.model.ts │ ├── paging.model.ts │ ├── publish │ │ ├── answer.model.ts │ │ ├── article.model.ts │ │ ├── column.model.ts │ │ └── image.model.ts │ ├── search-results.ts │ └── target │ │ ├── target.ts │ │ └── test.json ├── service │ ├── account.service.ts │ ├── authenticate.service.ts │ ├── collection.service.ts │ ├── cookie.service.ts │ ├── event.service.ts │ ├── http.service.ts │ ├── paste.service.ts │ ├── pipe.service.ts │ ├── profile.service.ts │ ├── publish.service.ts │ ├── release-note.service.ts │ ├── search.service.ts │ ├── update.service.ts │ └── webview.service.ts ├── test │ ├── runTest.ts │ └── suite │ │ ├── extension.test.ts │ │ ├── index.ts │ │ └── paste.service.test.ts ├── treeview │ ├── collection-treeview-provider.ts │ ├── feed-treeview-provider.ts │ └── hotstory-treeview-provider.ts └── util │ └── md-html-utils.ts ├── test.md ├── test ├── fixtures │ └── publishTest │ │ ├── test.js │ │ ├── test.md │ │ └── test_assert.html ├── runTest.ts └── suite │ ├── extension.test.ts │ ├── global.test.ts │ ├── index.ts │ └── mdparser.service.test.ts ├── tsconfig.json ├── tslint.json ├── webpack.config.js └── zhihu-reverse.md /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "env": { 3 | "browser": true, 4 | "es6": true 5 | }, 6 | "extends": [ 7 | "eslint:recommended", 8 | "plugin:@typescript-eslint/eslint-recommended" 9 | ], 10 | "globals": { 11 | "Atomics": "readonly", 12 | "SharedArrayBuffer": "readonly" 13 | }, 14 | "parser": "@typescript-eslint/parser", 15 | "parserOptions": { 16 | "ecmaVersion": 2018, 17 | "sourceType": "module" 18 | }, 19 | "plugins": [ 20 | "@typescript-eslint" 21 | ], 22 | "rules": { 23 | // https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-unused-vars.md 24 | // note you must disable the base rule as it can report incorrect errors 25 | "no-unused-vars": "off", 26 | // FIXME: Also false positive, disables the rule. Opt in when it works 27 | // "@typescript-eslint/no-unused-vars": ["error"] 28 | } 29 | }; -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | 106 | ## Jks add 107 | cookie.json 108 | qrcode.png 109 | events.json 110 | cache.json 111 | -------------------------------------------------------------------------------- /.gitignore.origin: -------------------------------------------------------------------------------- 1 | out 2 | node_modules 3 | # login.js 4 | /zhihu_login/* 5 | cookie.json 6 | captcha.jpg 7 | /tmp_test 8 | collection.json 9 | qrcode.png 10 | image.png 11 | release_notes/images 12 | *.vsix 13 | dist 14 | README.pdf 15 | events.json 16 | cache.json 17 | **/images 18 | .vscode-test 19 | *.log 20 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that compiles the extension and then opens it inside a new window 2 | { 3 | "version": "0.2.0", 4 | "configurations": [ 5 | { 6 | "type": "node", 7 | "request": "launch", 8 | "name": "launch script", 9 | "program": "${file}" 10 | // "args": ["${file}"] 11 | }, 12 | { 13 | "name": "Launch Extension", 14 | "type": "extensionHost", 15 | "request": "launch", 16 | "runtimeExecutable": "${execPath}", 17 | "args": [ 18 | "--disable-extensions", 19 | "--extensionDevelopmentPath=${workspaceRoot}", 20 | ], 21 | "stopOnEntry": false, 22 | "sourceMaps": true, 23 | // "outFiles": [ 24 | // "${workspaceRoot}/dist/**" 25 | // ], 26 | "preLaunchTask": "Run Develop" 27 | }, 28 | { 29 | "name": "Run Extension Tests", 30 | "type": "extensionHost", 31 | "request": "launch", 32 | "runtimeExecutable": "${execPath}", 33 | "args": [ 34 | "--extensionDevelopmentPath=${workspaceFolder}", 35 | "--extensionTestsPath=${workspaceFolder}/out/test/suite/index" 36 | ], 37 | "stopOnEntry": false, 38 | // "outFiles": ["${workspaceFolder}/out/test/**/*.js"], 39 | "preLaunchTask": "npm: compile" 40 | }, 41 | { 42 | "type": "node", 43 | "request": "attach", 44 | "name": "Attach to Extension Host", 45 | "protocol": "inspector", 46 | "port": 5870, 47 | "restart": true, 48 | "outFiles": [ 49 | "${workspaceRoot}/dist/*" 50 | ] 51 | } 52 | ] 53 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "convertapi", 4 | "jianshu", 5 | "KHTML", 6 | "wpls", 7 | "xsrf", 8 | "Zhuanlan" 9 | ] 10 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // See https://go.microsoft.com/fwlink/?LinkId=733558 2 | // for the documentation about the tasks.json format 3 | { 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "label": "Run Develop", 8 | "type": "npm", 9 | "script": "develop", 10 | "isBackground": true, 11 | "presentation": { 12 | "echo": true, 13 | "reveal": "never", 14 | "focus": false, 15 | "panel": "shared", 16 | "showReuseMessage": true, 17 | "clear": false 18 | }, 19 | "problemMatcher": { 20 | "base": "$ts-webpack-watch", 21 | "background": { 22 | "activeOnStart": true, 23 | "beginsPattern": "webpack is watching the files…", 24 | "endsPattern": "^Built at: .* [AP]M" 25 | } 26 | } 27 | }, 28 | { 29 | "type": "npm", 30 | "script": "lint", 31 | "problemMatcher": ["$eslint-stylish"] 32 | } 33 | ] 34 | } -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | src 2 | out 3 | # login.js 4 | /zhihu_login/* 5 | node_modules/ 6 | cookie.* 7 | captcha.jpg 8 | tmp_test 9 | collection.json 10 | .vscode 11 | qrcode.png 12 | image.png 13 | *.vsix 14 | package.cmd 15 | out/test/** 16 | **/*.map 17 | .gitignore 18 | tsconfig.json 19 | tslint.json 20 | release_notes/images 21 | **/*.pdf 22 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 更新记录 2 | 3 | # v1.5.1 (2022/7/16) 4 | 修复登录二维码无法显示的问题 5 | 6 | # v1.5.0 7 | 支持知乎标签 8 | 9 | # v1.4.1 10 | - 知乎回答也支持草稿啦 11 | 12 | # v1.4.0 13 | - 支持草稿(仅知乎专栏文章) 14 | - 将图标改为黑白色([#1](https://github.com/jks-liu/WPL-s/issues/1)) 15 | 16 | # v1.3.0 17 | - 支持知乎链接卡片 18 | 19 | # v1.2.0 20 | - v1.1.0 的紧急修复版 (去除了本地 SVG 图片的支持) 21 | 22 | # v1.1.0 - 由于 sharp 依赖的问题,此版无法正常工作 23 | - 支持本地的 SVG 图片 24 | - 修复知乎用户名有时显示为 `undefined` 的bug 25 | 26 | # v1.0.0 27 | - 支持元数据 28 | 29 | # v0.4.0 30 | - 支持参考文献 31 | 32 | # v0.3.0 33 | - 支持任务列表 34 | 35 | # v0.2.1 36 | - 支持Emoji 37 | - 支持表格 38 | -------------------------------------------------------------------------------- /CHANGELOG.origin.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [0.3.0] 8 | 9 | ### 文章/答案发布后自动生成头部链接 10 | 11 | 在以往的版本中,发布一篇新文章,新答案,就会产生新的链接,但是如果想修改这篇文章或答案,就需要用 `#! https://zhuanlan.zhihu.com/p/126167760` 形式的链接置于文章顶部,在 0.3 版本中,该操作插件会替你自动完成,也就是说,一份源 markdown 文件,发布后会自动指向对应的文章和答案,修改后再发布,即可修改源文章。 12 | 13 | ### 域外图片缓存加速 14 | 15 | 上一个版本中,插件支持了域外链接和本地链接的图片,但是使用起来会发现,发布的时间比较长,尤其是有域外链接的图片时,插件会先把域外的图片下载到本地,再传到知乎图床上,完成链接的替换,在 0.3 版本中,已经上传过的域外链接,插件会有缓存记录,再次发布时,会直接完成链接替换,跳过下载和上传过程。 16 | 17 | 如要清理缓存,请使用 `zhihu.clearCache` 命令。 18 | 19 | ### 支持本地绝对路径 20 | 21 | 上一个版本,图片链接只支持本地相对路径和域外 https 链接,新版本支持本地的绝对路径。(请注意 http:// 协议仍然不支持,请保证域外图片为 https://) 22 | 23 | ## [0.2.3] 24 | 25 | 众所周知,一个来自外域的图片链接,是不一定能在知乎平台上正常显示的,因为会涉及到跨域问题,为了安全起见,原则上所有答案/文章中的图片,都要上传至知乎的图床,然后将链接放在答案中,这样才能正常显示。 26 | 27 | 不仅如此,知乎服务端也不允许上传的答案或文章中的图片的来源为非知乎的源,即便我们试图这样做,其实也是不可行的。 28 | 29 | 所以在图片链接上这点,用户需要用插件自身提供的图片上传功能,上传至知乎图床,插件也会自动在 Markdown 文本里插入图片链接,其实已经比较方便了,但不能算作“非侵入式”,因为这改变了用户本来的习惯。 30 | 31 | 于是小岱开始开发了一个新 Feature,这个 Feature 可以让用户无需在意图片链接是否是来自知乎图床,这个链接可以是本地的相对路径,也可以来自于知乎域外,发布至知乎时,所有的图片都能够正常显示。 32 | 33 | 也就是说,随便在你的电脑中拿出一个你以前写好的某个 README,就算这个 README 里面有一堆相对路径的图片,或来自奇奇怪怪的图床的图片,只需右键点击发布,it just works。 34 | 35 | 比如: 36 | 37 | ![Image](https://pic4.zhimg.com/80/v2-0b00790259520bdbda398cd05731b06b.png) 38 | 39 | 源 Markdown 中的所有图片都是相对路径或来自外链。 40 | 41 | 发布后的效果: 42 | 43 | ![Image](https://pic4.zhimg.com/80/v2-22d902f8c869bc61c44c7711fa8e4e00.png) 44 | 45 | 46 | ## [0.2.2] 47 | 48 | ### 专栏管理 49 | 50 | 为了让创作者群体更好地创作,在发布文章的时候,用户可以直接选择发布至自己的专栏下: 51 | 52 | ![Image](https://pic4.zhimg.com/80/v2-b6358e6d673e5feb84dd0ad0bd4d52e4.png) 53 | 54 | ### 文章标题智能识别 55 | 56 | 文章标题无需手动输入,插件会自动检测文本的第一个一级头标签: 57 | 58 | ``` 59 | # 这是一个标题(必须只是一个#) 60 | ``` 61 | 62 | 然后将其作为标题,改行的内容也不会进入到正文中,如果没有检测到,还需用户手动输入。 63 | 64 | ### 背景图片智能识别 65 | 66 | 插件会自动扫描文本第一个一级头标签之前的内容,将第一个发现的图片链接作为背景图片: 67 | 68 | ``` 69 | ![Image](https://pic4.zhimg.com/80/v2-157583e100e9e181191d285355332ebf.png) 70 | ``` 71 | ``` 72 | # 标题在这, 上面的链接会变成背景图片, 不会进入正文 73 | ``` 74 | 75 | ### Html 支持 76 | 77 | 可以在正常的 Markdown 文本中插入 html 文本, 扩展了写作能力。 78 | 79 | >绝大多数 html 标签为非法标签,包括 table 在内,会被服务端过滤掉,只有 \, \
, \ 等合法标签才会被服务端存储,具体使用时小伙伴们可以自己尝试。 80 | 81 | ### 增加 Zhihu: Is Title Image Full Screen 配置项 82 | 83 | 用户可以在设置中找到 `Zhihu: Is Title Image Full Screen` 配置项,勾选后,知乎文章的背景图片会变为全屏模式。 84 | 85 | ## [0.2.1] 86 | 87 | ### 可以查看点赞数,并给喜欢的内容点赞 88 | 89 | ![Image](https://pic4.zhimg.com/80/v2-d8f61703c731711fe3a3585122c0d676_hd.png) 90 | 91 | 点击按钮点赞。 92 | 93 | ### 显示作者头像,名字,个性签名 94 | 95 | ![Image](https://pic4.zhimg.com/80/v2-157583e100e9e181191d285355332ebf_hd.png) 96 | 97 | 点击头像,可以在浏览器打开该作者的个人主页。 98 | 99 | ### 修改文章 100 | 101 | 发布后的文章,按照和答案相同的方式,将文章的链接以形如: 102 | 103 | ``` 104 | #! https://zhuanlan.zhihu.com/p/107810342 105 | ``` 106 | 107 | 复制至文章顶部,发布即可对原文章进行修改。 108 | 109 | ### 行内latex 110 | 111 | 现在创作的时候,可以直接用 `$\sqrt5$` 的方式写行内 latex, 而块latex还是原来的 `$$\sqrt$$` 语法。 112 | 113 | ### 优化了图标样式 114 | 115 | ![Image](https://pic4.zhimg.com/80/v2-b293132e3d47d8cd48394caf78611bd2_hd.png) 116 | 117 | ## [0.2.0] - 2020-02-17 118 | 119 | ### Webview 默认使用 VSCode 主题色 120 | 121 | 板块是透明的,会看起来像透明亚克力: 122 | 123 | ![](https://raw.githubusercontent.com/niudai/ImageHost/master/zhihu/2020-02-16-11-11-22.png) 124 | 125 | >可以在 VSCode 的设置栏中找到 `Use VSTheme` 设置项,取消打勾后,会开启知乎默认的白蓝主题。 126 | 127 | ### 支持定时发布 128 | 129 | 所有的答案,文章发布时,均会多一次询问,用户须选择是稍后发布还是马上发布,如果选择稍后发布,需要输入发布的时间,比如 “5:30 pm”,"9:45 am" 等,目前仅支持当天的时间选择,输入后,你就会在个人中心的“安排”处看到你将发布的答案和发布的时间(需要手动点击刷新): 130 | 131 | ![](https://raw.githubusercontent.com/niudai/ImageHost/master/zhihu/2020-02-16-11-20-14.png) 132 | 133 | 定时发布采用 prelog 技术,中途关闭 VSCode,关机不影响定时发布,只需保证发布时间 VSCode 处于打开状态 && 知乎插件激活状态即可。 134 | 135 | 时间到了之后,你会收到答案发布的通知,该事件也会从“安排”中移除。 136 | 137 | 如果想取消发布,则点击 ❌ 按钮即可: 138 | 139 | ![](https://raw.githubusercontent.com/niudai/ImageHost/master/zhihu/2020-02-16-15-56-31.png) 140 | 141 | >发布事件采用 md5 完整性校验,不允许用户同时预发两篇内容一摸一样的答案或文章。 142 | 143 | ### 增加“分享”和“在浏览器打开”两个按钮 144 | 145 | 由于插件自身轻量的定位,Webview 的内容没有浏览器端更全面,而且为了保证大家可以更方便地将内容分享给其他人,增加了如下两个按钮: 146 | 147 | ![](https://raw.githubusercontent.com/niudai/ImageHost/master/zhihu/2020-02-16-15-29-09.png) 148 | 149 | 点击左侧按钮会在浏览器中打开该页面,点击中间的会将页面的链接复制至粘贴板中。 150 | 151 | ## [0.1.0] - 2020-02-10 152 | 153 | ### Added 154 | 155 | - 二维码/账密登录 156 | - 内容创作 157 | - 内容发布 158 | - 一键上传图片 159 | - 个性推荐 160 | - 实时热榜 161 | - 搜索全站 162 | - 收藏夹 163 | 164 | ### Changed 165 | 166 | ### Removed 167 | 168 | 169 | -------------------------------------------------------------------------------- /CONTRIBUTING.origin.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | 👍🎉 First off, thanks for taking the time to contribute! 🎉👍 4 | 5 | When contributing to this project, please first discuss the changes you wish to make via an issue before making changes. 6 | 7 | ## Your First Code Contribution 8 | 9 | Unsure where to begin contributing? You can start a new issue to give a feedback of bugs you found, features you wanna implemented, etc. 10 | 11 | ## Getting the code 12 | 13 | ``` 14 | git clone https://github.com/niudai/VSCode-Zhihu.git 15 | ``` 16 | 17 | Prerequisites 18 | 19 | - [Git](https://git-scm.com/) 20 | - [NodeJS](https://nodejs.org/), `>= 10.0.0` 21 | - [npm](https://www.npmjs.com/), `>= 6.0.0` 22 | 23 | 24 | ## Dependencies 25 | 26 | From a terminal, where you have cloned the repository, execute the following command to install the required dependencies: 27 | 28 | ``` 29 | npm install 30 | ``` 31 | 32 | ## Build 33 | 34 | run `npm install` first to install all deps and dev-deps. 35 | 36 | VSCode-Zhihu uses webpack as dev&prod tool. In order to build and run, you need to execute `npm run develop` first, which compiles the typescript code and bundles them together into a single big .js file in `\dist` folder, and since the webpack starts in **watch mode**, evertime you altered the source .ts file, webpack would recompile for you, so you don't have to compile it manually. 37 | 38 | > You don't need to execute `npm run develop` any more, cuz the `Launch Extenison` task do it for you. 39 | 40 | You could check the scripts in `package.json` to see what `develop` do, knowing some webpack concepts would be helpful. 41 | 42 | Since webpack is always used as bundler and minimizer in client side, the npm dependencies in this project would sometimes break the behavior of thoses packages used for front-end, which you will see later. 43 | 44 | After the compiling phase, you could open the `debug` view in VSCode and run the `Launch Extension` to run it. If things goes right, a new VSCode windows would pop out. 45 | 46 | Open the Zhihu view and execute any command provided by Zhihu, you would see this error: 47 | 48 | ``` 49 | Activating extension 'niudai.vscode-zhihu' failed: Cannot find module '../lib/utils.js'. 50 | ``` 51 | 52 | This is where the tricky stuff comes in. 53 | 54 | Here's the thing, the project use `pug` as its html template, and `pug` depends on `uglify-js` module, which you could find'em all in `/node_modules`. Things like pug, uglify-js are always used in server-side, in which people don't need to bundle them. 55 | 56 | Webpack use syntaxes like `require()`, `import()` to recognize the dependency graph, but the code in `/node_modules/uglify-js/tools/node.js` just use the file I/O to read the file, making webpack misunderstand it. So to make the code run as it should, you should comment out this part of code in `/node_modules/uglify-js/tools/node.js`: 57 | 58 | ```js 59 | var FILES = UglifyJS.FILES = [ 60 | "../lib/utils.js", 61 | "../lib/ast.js", 62 | "../lib/parse.js", 63 | "../lib/transform.js", 64 | "../lib/scope.js", 65 | "../lib/output.js", 66 | "../lib/compress.js", 67 | "../lib/sourcemap.js", 68 | "../lib/mozilla-ast.js", 69 | "../lib/propmangle.js", 70 | "./exports.js", 71 | ].map(function(file){ 72 | return require.resolve(file); 73 | }); 74 | 75 | new Function("MOZ_SourceMap", "exports", FILES.map(function(file){ 76 | return fs.readFileSync(file, "utf8"); 77 | }).join("\n\n"))( 78 | require("source-map"), 79 | UglifyJS 80 | ); 81 | 82 | UglifyJS.AST_Node.warn_function = function(txt) { 83 | console.error("WARN: %s", txt); 84 | }; 85 | ``` 86 | 87 | after you do this, re-run `npm run develop` and launch extension again, you would find everything works. 88 | 89 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Jks Liu 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 | 23 | The MIT License (MIT) 24 | 25 | Copyright (c) 2020-2100 牛岱 26 | 27 | Permission is hereby granted, free of charge, to any person obtaining a copy 28 | of this software and associated documentation files (the "Software"), to deal 29 | in the Software without restriction, including without limitation the rights 30 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 31 | copies of the Software, and to permit persons to whom the Software is 32 | furnished to do so, subject to the following conditions: 33 | 34 | The above copyright notice and this permission notice shall be included in all 35 | copies or substantial portions of the Software. 36 | 37 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 38 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 39 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 40 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 41 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 42 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 43 | SOFTWARE. 44 | -------------------------------------------------------------------------------- /README.en.md: -------------------------------------------------------------------------------- 1 | # WPL/s 2 | 3 | Write-Publish-Loop w/ statistic. 4 | 5 | This code is based on by 牛岱 under MIT license. That repo seems no longer maintained, so I will continue work here. 6 | 7 | Project icon is from [Google Material icons](https://fonts.google.com/icons?icon.query=coffee) licensed under [Apache License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.html) 8 | 9 | # Development & contribute 10 | 11 | ## Notes & useful link 12 | 13 | Zhihu text edit is based on [Draft.js](https://draftjs.org). ref: [这是一篇关于知乎编辑器的专栏文章](https://zhuanlan.zhihu.com/p/31559179). 14 | 15 | [drmingdrmer/md2zhihu](https://github.com/drmingdrmer/md2zhihu)) converts markdown to zhihu compatible format. 16 | 17 | # Post from browser for getting format 18 | 19 | ## Tag 20 | 21 | ## Foot note 22 | 23 | ```html 24 | content:

建议大家在说一段代码是UB之前,不说查看一下标准原文,最少也Google一下。


我看了一下C11标准,这段代码应该不是UB,所以我倾向于这是GCC的一个bug。并且我用最新的GCC11试了上面的例子,也无法复现,说明这个bug很大可能已经被修复了。


下面是标准原文[1]Section 6.9.1, P174-12:

If the } that terminates a function is reached, and the value of the function call is used by the caller, the behavior is undefined.

注意我标粗的那一句,用的是and。意味着只有在返回值被使用的时候才是UB,显然题主不是这个情况。

25 | ``` 26 | 27 | ## Table 28 | 29 | ## 好物推荐 30 | 31 | ## 打赏 32 | 33 | ## link to info block 34 | 35 | ```html 36 | content:

本文系抛砖引玉,主要是探索一下最新的C++ 20协程(C++ 20 Coroutine)在线程池中的应用能力。

简介

线程池中与C++ 20协程有关的部分主要有两点:

  1. 线程池本身是使用协程实现的;
  2. 提交给线程池的任务可以是协程。

需要做到上面两点,主要依赖了这样一个事实:C++ 20的协程可以在一个线程中暂停,然后在另一个线程中恢复执行。跨线程的协程暂停/恢复是很多语言/协程库所不具备的。

协程线程池的完整实现可以在这里找到。专栏文章《使用C++20协程(Coroutine)实现线程池》也有详细介绍,同时这篇专栏还详细介绍了C++ 20协程,对协程不了解的朋友可以参考。

https://github.com/jks-liu/coroutine-thread-pool.hJks Liu:使用C++20协程(Coroutine)实现线程池

核心介绍

线程池的核心是将一个任务(task)协程化,并保存其句柄供线程池恢复执行。

template <std::invocable F>
future<std::invoke_result_t<F>> submit(F task)
{
using RT = std::invoke_result_t<F>;
using PT = future<RT>::promise_type;
std::coroutine_handle<PT> h = co_await awaitable<RT>();

if constexpr (std::is_void_v<RT>) {
task();
} else {
h.promise().set_value(task());
}
}

其中核心语句是:std::coroutine_handle<PT> h = co_await awaitable<RT>();co_await让任务暂停,awaitable则将任务句柄保存起来。

总体来说整个思路还是比较清晰的。

例子

#include <iostream>
#include <chrono>
#include <string>

#include "thread-pool.h"

using namespace jks;

void a_ordinary_function_return_nothing()
{
std::cout << __func__ << std::endl;
}

std::string a_ordinary_function_return_string()
{
return std::string(__func__);
}

future<void> a_coroutine_return_nothing()
{
co_await thread_pool::awaitable<void>();
std::cout << __func__ << std::endl;
}

future<std::string> a_coroutine_return_string()
{
auto h = co_await thread_pool::awaitable<std::string>();
h.promise().set_value(__func__);
}


std::string a_function_calling_a_coroutine()
{
auto r = a_coroutine_return_string();
return r.get() + " in " + __func__;
}

// You can submit your coroutine handle in your own awaitable
// This implementation is a simplified version of jks::thread_pool::awaitable
struct submit_awaitable: std::suspend_never
{
void await_suspend(std::coroutine_handle<> h)
{
thread_pool::get(0).submit_coroutine(h);
}
};

future<void> submit_raw_coroutine_handle()
{
co_await submit_awaitable();
std::cout << __func__ << std::endl;
}

int main()
{
using namespace std::chrono_literals;

constexpr auto n_pool = 3;
// get thread pool singleton
auto& tpool = thread_pool::get(n_pool);

// 任务可以是一个普通的函数
tpool.submit(a_ordinary_function_return_nothing);
auto func_return_sth = tpool.submit(a_ordinary_function_return_string);

// 任务可以是一个协程
tpool.submit(a_coroutine_return_nothing);
auto coro_return_sth = tpool.submit(a_coroutine_return_string);

// 任务可以是一个调用了协程的函数
auto func_calling_coro = tpool.submit(a_function_calling_a_coroutine);

// 我们也可以直接提交协程句柄
submit_raw_coroutine_handle();

std::this_thread::sleep_for(1s);

// Lambda也是支持的
for (int i=0; i<=n_pool; ++i) {
tpool.submit([i]() -> int{
std::cout << "* Task " << i << '+' << std::endl;
std::this_thread::sleep_for(3s);
std::cout << "* Task " << i << '-' << std::endl;
return i;
});
}
std::this_thread::sleep_for(1s);

// 最后,我们可以得到任务的执行结果
std::cout << func_return_sth.get() << std::endl;
std::cout << coro_return_sth.get().get() << std::endl;
std::cout << func_calling_coro.get() << std::endl;

// Destructor of thread_pool blocks until tasks current executing completed
// Tasks which are still in queue will not be executed
// So above lambda example, Task 3 is not executed
}


37 | ``` 38 | 39 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: WPL/s v1.5.1发布 - 支持标签 - 让你在 VS Code 中编写发布知乎文章及回答问题 3 | zhihu-title-image: ./res/media/vs-code-extension-search-zhihu-this.png 4 | zhihu-tags: Markdown 编辑器, Markdown, Visual Studio Code 5 | zhihu-url: https://zhuanlan.zhihu.com/p/435836671 6 | --- 7 | 8 | 在元数据中: 9 | ``` 10 | zhihu-tags: tag1, tag 2, tag-3, 标签4 11 | ``` 12 | 13 | 14 | # WPL/s - 让你在VS Code中编写发布知乎文章及回答问题 15 | 16 | 这是一个开源项目,你可以在[jks-liu.WPL-s@Github](https://github.com/jks-liu/WPL-s)上找到它。 17 | 18 | [![zhihu-link-card:本项目 GitHub 主页](res/media/vs-code-extension-search-zhihu.png)](https://github.com/jks-liu/WPL-s) 19 | 20 | 本项目源于牛岱的开源项目(开源协议:MIT)[VSCode-Zhihu](https://github.com/niudai/VSCode-Zhihu),在此表示感谢。 21 | 22 | 插件图标来自[Google Material icons](https://fonts.google.com/icons?icon.query=coffee),在 [Apache License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.html)下授权。 23 | 24 | # 安装 25 | 在VS Code中搜索`zhihu`,安装即可,如下图。虽然目前排在最后一个:cry:。 26 | 27 | ![在VS Code中搜索`zhihu`](./res/media/vs-code-extension-search-zhihu.png) 28 | 29 | 为了更高效地编写 Markdown 文件,建议安装 [Markdown All in One](https://github.com/yzhang-gh/vscode-markdown) 插件。 30 | 31 | # 支持的功能 32 | 33 | 测试使用[这个 Markdown 文件](https://github.com/jks-liu/zhihu/blob/master/WPLs-introduction-and-test.md),测试结果可以查看[这篇知乎专栏文章](https://zhuanlan.zhihu.com/p/390528313)。 34 | 35 | | Markdown基础功能 | 支持与否 | 36 | | :--- | :--- | 37 | | 章节标题 | :heavy_check_mark: *1 | 38 | | 分割线 | :heavy_check_mark: | 39 | | 引用 | :heavy_check_mark: | 40 | | 链接 | :heavy_check_mark: *8 | 41 | | 图片 | :heavy_check_mark: *6 | 42 | | 表格 | :heavy_check_mark: *2 | 43 | | 公式 | :heavy_check_mark: | 44 | | 代码块 | :heavy_check_mark: | 45 | | 加粗 | :heavy_check_mark: | 46 | | 斜体 | :heavy_check_mark: | 47 | | 加粗斜体嵌套 | :heavy_check_mark: | 48 | | 删除线 | :x: *3 | 49 | | 列表 | :heavy_check_mark: | 50 | | 参考文献 | :heavy_check_mark: *4 | 51 | 52 | | 其它特色功能 | 支持与否 | 53 | | :--- | :--- | 54 | | 元数据 | :heavy_check_mark: *4 | 55 | | 目录 | :x: *0 | 56 | | 章节标题自动编号 | :x: *0 | 57 | | Emoji表情 | :heavy_check_mark: *5 | 58 | | 任务列表 | :heavy_check_mark: | 59 | 60 | 61 | | 知乎特色功能 | 支持与否 | 62 | | --- | --- | 63 | | 标题 | :heavy_check_mark: *7 | 64 | | 回答问题 | :heavy_check_mark: | 65 | | 发布文章 | :heavy_check_mark: | 66 | | 题图 | :heavy_check_mark: *7 | 67 | | 链接卡片 | :heavy_check_mark: *4 | 68 | | 视频 | :x: | 69 | | 好物推荐 | :x: | 70 | | 附件 | :x: | 71 | | 标签 | :heavy_check_mark: *7 | 72 | | 草稿 | :heavy_check_mark:| 73 | | 赞赏 | :x: | 74 | | 追更 | :x: | 75 | 76 | (0)打算近期支持,star,点赞,收藏,一键三连给我动力呀 77 | 78 | 1. 最多可支持 4 级标题 79 | 2. 表格暂时不支持对齐 80 | 3. 知乎本身不支持,请大家踊跃向[知乎小管家](https://www.zhihu.com/people/zhihuadmin)提建议 81 | 4. 格式见下一小节 82 | 5. 支持大部分Emoji(很多emoji刚发的时候可以看到,但一段时间过后就会被知乎过滤掉),具体列表请查看上面的链接。 83 | 6. - 同时支持本地图片和网络链接(暂时不支持 SVG 格式) 84 | 7. 在元数据中指定 85 | 8. 不支持为图片设置连接 86 | 87 | # Markdown 语法文档 88 | 89 | 最直接的方法是参考[上面提到的 Markdown 测试文件](https://github.com/jks-liu/zhihu/blob/master/WPLs-introduction-and-test.md)。 90 | 91 | ## Markdown语法 92 | 自行 Google,或查看上面的测试文件。由于本项目使用 `markdown-it` 来渲染 Markdown,所以遵循 [CommonMark](https://commonmark.org/) 规范。 93 | 94 | ## [Jekyll 元数据](https://jekyllrb.com/docs/front-matter/) 95 | 目前仅支持如下元数据: 96 | ```md 97 | --- 98 | title: 请输入标题(若是回答的话,请删除本行) 99 | zhihu-url: 请输入知乎链接(删除本行发表新的知乎专栏文章) 100 | zhihu-title-image: 请输入专栏文章题图(若无需题图,删除本行) 101 | zhihu-tags: tag1, tag 2, tag-3, 标签4, 标签以半角逗号分隔, 只有知乎已经存在的标签才能添加成功 102 | 注意: 所有的冒号是半角冒号,冒号后面有一个半角空格 103 | --- 104 | ``` 105 | 106 | ## 链接卡片 107 | ```md 108 | [![zhihu-link-card:本项目 GitHub 主页](./pics/vs-code-extension-search-zhihu.png)](https://github.com/jks-liu/WPL-s) 109 | ``` 110 | 语法上和一个图片链接一样,但图片的文字需要以`zhihu-link-card:`开头。 111 | 112 | ## 任务列表 113 | ```md 114 | - [ ] 未完成的任务 115 | - [x] 已完成的任务 116 | - [ ] 嵌套未完成的任务 117 | - [x] 嵌套已完成的任务 118 | ``` 119 | 120 | ## Emoji表情 121 | 语法和 Github 中使用 Emoji 一样,自行 Google 或查看上面的测试文件。 122 | 123 | ## 参考文献 124 | ```md 125 | 用[^n]来引用。 126 | 127 | [^n]: https://网址.com 说明文字 128 | 129 | 注意字符 ^ 不能少。冒号后面有一个空格。网址中不能有空格。网址和说明文字之间有一个空格,说明文字自己可以有空格。 130 | ``` 131 | 132 | 133 | # 使用方法 134 | 135 | ## 登录 136 | 点击左上角![登录按钮](res/media/light/outline_login_black_24dp.png),用知乎扫描二维码。 137 | 138 | ## 发布文章 139 | 点击右上角![发布按钮](res/media/light/outline_publish_black_24dp.png)。 140 | 141 | ## 保存草稿 142 | 点击右上角![草稿按钮](res/media/light/outline_drafts_black_24dp.png)。 143 | 144 | 145 | # 开源协议 146 | 147 | MIT 许可,详情请查看[LICENSE](./LICENSE)。 148 | 149 | # 贡献 150 | 欢迎提交 issue 和 pr。 151 | 152 | # 未来功能展望 153 | 154 | ![未来功能展望](./docs/wpls-flow.png) 155 | 156 | # 其它信息 157 | ## 知乎文章发布的代码逻辑 158 | 159 | ![知乎文章发布的代码逻辑](./docs/wpls-flow-知乎文章发布.png) -------------------------------------------------------------------------------- /README.origin.md: -------------------------------------------------------------------------------- 1 | [![](https://vsmarketplacebadge.apphb.com/version-short/niudai.vscode-zhihu.svg)](https://marketplace.visualstudio.com/items?itemName=niudai.vscode-zhihu) 2 | [![](https://vsmarketplacebadge.apphb.com/downloads-short/niudai.vscode-zhihu.svg)](https://marketplace.visualstudio.com/items?itemName=niudai.vscode-zhihu) 3 | [![](https://vsmarketplacebadge.apphb.com/rating-short/niudai.vscode-zhihu.svg)](https://marketplace.visualstudio.com/items?itemName=niudai.vscode-zhihu) 4 | 5 | 6 |

7 | vscode-zhihu logo 8 |

9 | 10 |

11 | 打一颗 ⭐,世界更亮。 12 |

13 | 14 | 15 | # 👽 Zhihu On VSCode 16 | 17 | 基于 VSCode 的知乎客户端提供包括阅读,搜索,创作,发布等一站式服务,内容加载速度比 Web 端更快,创新的 Markdown-Latex 混合语法让内容创作者更方便地插入代码块,数学公式,并一键发布至知乎平台。项目由 [牛岱](https://www.zhihu.com/people/niu-dai-68-44) 独立设计开发,喜欢的话请献出你的 [⭐](https://github.com/niudai/VSCode-Zhihu '给一个Star')。 18 | 19 | ## ⚡ Features 20 | 21 | - 登录 22 | - [二维码/账密登录](#🔑-登录 ) 23 | - 创作 24 | - [内容创作](#🖍-内容创作) 25 | - [内容发布](#📩-内容发布) 26 | - [一键上传图片](#📊-上传图片) 27 | - [定时发布](#🕐-定时发布) 28 | - 浏览 29 | - [个性推荐](#🎭-个性推荐) 30 | - [实时热榜](#hot-story) 31 | - [搜索全站](#🔎-搜索 ) 32 | - [收藏夹](#🎫-收藏夹) 33 | 34 | 35 | ## 📃 Reference 36 | 37 | - [图标按钮](#😀-图标按钮) 38 | - [快捷键](#⌨-快捷键) 39 | - [配置项](#⚙-配置项) 40 | 41 | --- 42 | 43 | ## 🔑 登录 44 | 45 | 46 | 47 | 48 | 进入主页面,左侧最上方栏为个人中心,点击登录图标,或使用 `Ctrl + Shift + P` 打开命令面板,搜索并执行 `Zhihu: Login` 命令。 49 | 50 | 选择登录方式: 51 | 52 | ### 二维码 53 | 54 | 选择二维码登陆后,会弹出二维码页面,打开知乎 APP,扫码后点击确认登录: 55 | 56 |

57 |

58 | 59 | ### 账号密码 60 | 61 | 视情况,插件会加载并显示验证码,提示你输入验证码,输入后,再依次根据提示输入手机号和密码即可。 62 | 63 | 登录成功后会有问候语,推荐栏会自动刷新出你的个性签名和头像: 64 | 65 |

66 |

67 | 68 |

69 |

70 | 71 | 72 | 73 | --- 74 | 75 | ## 🎭 个性推荐 76 | 77 | 登陆成功后,个性推荐板块会自动刷新,提供你的个性推荐内容: 78 | 79 |

80 |

81 | 82 | 内容可能为答案,问题,或文章,点击条目,就会打开VSCode知乎页面: 83 | 84 |

85 |

86 | 87 | ___ 88 | 89 | ## 💥 热榜 90 | 91 | 在左侧的中间位置,你会看到热榜栏,内部有六个分类,内容与知乎Web端、移动端同步,助你掌控实时资讯: 92 | 93 |

94 | 95 | --- 96 | 97 | ## 🔎 搜索 98 | 99 | 100 | 点击搜索按钮,或搜索命令 `Zhihu: Search Items`,搜索全站知乎内容: 101 | 102 |

103 |

104 | 105 | --- 106 | 107 | ## 🖍 内容创作 108 | 109 | 110 | 新建一个后缀名为`.md`的文件,若不需要数学公式,只需要按照你最熟悉的 Markdown 语法写即可。 111 | 112 | ### Latex 语法支持 113 | 114 | 为了更好地支持数学公式的写作,知乎定制的 Markdown 转换器提供了 Latex 语法拓展,语法示例: 115 | 116 | ``` 117 | $$ 118 | |\vec{A}|=\sqrt{A_x^2 + A_y^2 + A_z^2}. 119 | $$ 120 | ``` 121 | 122 | 用 `$$` 包围的部分会被当做 latex 语言进行解析,生成知乎的数学公式,比如上方的数学公式发布至知乎会生成如下公式: 123 | 124 |

125 |

126 | 127 | 行内 latex 也同样支持,语法举例:`$\sqrt6$`,一个dollar符号包裹公式即可。 128 | 129 | 代码块: 130 | 131 | 记得声明语言标签, 这样发布至知乎的答案才能获得正确的语法高亮,示例如下: 132 | 133 | ```java 134 | public class Apple { 135 | public Apple() {} 136 | } 137 | ``` 138 | 139 | 发布后会提供 java 的语法高亮: 140 | 141 |

142 |

143 | 144 | >由于知乎服务端的限制,表格暂不支持,答案中的表格会被服务端过滤。 145 | 146 | ## 📩 内容发布 147 | 148 | 149 | ### 链接扫描 😊 150 | 151 | 若你想在特定的问题下回答,或想修改自己的某个原有回答,就将问题/答案链接以 `#! https://...` 的格式放置于答案的第一行,发布时,插件会自动扫描识别,发布至相应的问题下,或修改原有的答案。 152 | 153 | 比如,你想在 [轻功是否真的存在,其在科学上可以解释吗?](https://www.zhihu.com/question/19602618) 该问题下回答问题, 只需将 154 | 155 | ``` 156 | #! https://www.zhihu.com/question/19602618 157 | ``` 158 | 159 | 若是你已经创作过的答案, 则将答案的链接, 形如: 160 | 161 | ``` 162 | #! https://www.zhihu.com/question/355223335/answer/1003461264 163 | ``` 164 | 165 | 的链接复制至文件顶部即可。 166 | 167 | 若是你已经创作过的文章,则将文章的链接,形如: 168 | 169 | ``` 170 | #! https://zhuanlan.zhihu.com/p/107810342 171 | ``` 172 | 173 | 若插件没有在首行扫描到链接,则会询问创作者接下来的操作,你可以选择发布新文章,或从收藏夹中选取相应问题,发布至相应问题下: 174 | 175 |

176 |

177 | 178 | ### 发布文章 179 | 180 | 选择发布文章后,会继续提示你输入文章标题,输入完成后,按下回车,当前的文档就会以文章的形式发布至你的账号。 181 | 182 | #### 文章标题智能识别 183 | 184 | 文章标题无需手动输入,插件会自动检测文本的第一个一级头标签: 185 | 186 | ``` 187 | # 这是一个标题(必须只是一个#) 188 | ``` 189 | 190 | 然后将其作为标题,改行的内容也不会进入到正文中,如果没有检测到,还需用户手动输入。 191 | 192 | #### 背景图片智能识别 193 | 194 | 插件会自动扫描文本第一个一级头标签之前的内容,将第一个发现的图片链接作为背景图片: 195 | 196 | ``` 197 | ![Image](https://pic4.zhimg.com/80/v2-157583e100e9e181191d285355332ebf.png) 198 | 199 | # 标题在这, 上面的链接会变成背景图片, 不会进入正文 200 | ``` 201 | 202 | ### Html 支持 203 | 204 | 可以在正常的 Markdown 文本中插入 html 文本, 扩展了写作能力。 205 | 206 | >绝大多数 html 标签为非法标签,包括 table 在内,会被服务端过滤掉,只有 \, \
, \ 等合法标签才会被服务端存储,具体使用时小伙伴们可以自己尝试。 207 | 208 | ### 从收藏夹中选取 209 | 210 | >关于如何管理收藏夹,请移至 [收藏夹](#collect)。 211 | 212 | 插件会提示选择你收藏过的问题: 213 | 214 |

215 |

216 | 217 | 选择后,答案就会发布至相应的答案下(若已在该答案下发布过问题,请用顶部链接的方式来发布!)。 218 | 219 | --- 220 | 221 | ## 🕐 定时发布 222 | 223 | 所有的答案,文章发布时,均会多一次询问,用户须选择是稍后发布还是马上发布,如果选择稍后发布,需要输入发布的时间,比如 “5:30 pm”,"9:45 am" 等,目前仅支持当天的时间选择,输入后,你就会在个人中心的“安排”处看到你将发布的答案和发布的时间(需要手动点击刷新): 224 | 225 | ![](https://raw.githubusercontent.com/niudai/ImageHost/master/zhihu/2020-02-16-11-20-14.png) 226 | 227 | 定时发布采用 prelog 技术,中途关闭 VSCode,关机不影响定时发布,只需保证发布时间 VSCode 处于打开状态 && 知乎插件激活状态即可。 228 | 229 | 时间到了之后,你会收到答案发布的通知,该事件也会从“安排”中移除。 230 | 231 | 如果想取消发布,则点击 ❌ 按钮即可: 232 | 233 | ![](https://raw.githubusercontent.com/niudai/ImageHost/master/zhihu/2020-02-16-15-56-31.png) 234 | 235 | >发布事件采用 md5 完整性校验,不允许用户同时预发两篇内容一摸一样的答案或文章。 236 | --- 237 | 238 | ## 🎫 收藏夹 239 | 240 | 241 | ### ➕ 添加收藏 242 | 243 | 不管是文章,答案,还是问题,在知乎页面顶栏的右侧,都会看到一个粉色的星状图标: 244 | 245 |

246 |

247 | 248 | ### ➖ 查看收藏 249 | 250 | 收藏的内容会在左侧下方显示,插件会自动分类: 251 | 252 |

253 |

254 | 255 | ### ✖ 删除收藏 256 | 257 | 鼠标移至相应的行,会出现叉状图标,点击即可删除: 258 | 259 |

260 |

261 | 262 | --- 263 | 264 | ## 📊 上传图片 265 | 266 | 一篇优质的答案,离不开图片,知乎插件提供了三种非常便携的图片上传方式,支持上传 `.gif`, `.png`, `.jpg` 格式,且在图片上传的时候自动在当前 Markdown 光标所在行自动生成图片链接,无需创作者手动管理,Windows,MacOS,Linux 全平台支持。 267 | 268 | ### 从粘贴板上传图片 269 | 270 | 调用 `Zhihu: PasteImage` 命令,自动将系统粘贴板中的图片上传至知乎图床,并生成相应链接。 271 | 272 | 快捷键为 `ctrl+alt+p`,也可以通过打开命令行面板搜索命令。 273 | 274 | --- 275 | 276 | ### 工作区中右键上传 277 | 278 | 在当前VSCode打开的文件夹内部,将鼠标放在你想上传的图片上,右键单击即可上传+生成链接: 279 | 280 |

281 |

282 | 283 | 可以看到,可以将文件的路径复制至剪贴板,再调用 `Zhihu: PasteImageFromPath` 命令,插件会自动将该路径的文件上传至知乎图床,生成链接。 284 | 285 | ### 打开文件浏览器选择图片 286 | 287 | 在正在编辑的 Markdown 文档下右键,可以看到菜单项 `Zhihu: Upload Image From Explorer`,点击即可打开文件管理器,选择一张图片点击确定即可。 288 | 289 |

290 |

291 | 292 | --- 293 | 294 | ## 😀 图标按钮 295 | 296 | 297 | 298 | 点击左侧活动栏的知乎按钮,进入知乎插件页面,在推荐的上方可以看到三个按钮,对应的命令分别为 `Zhihu: Login`(登录),`Zhihu: Refresh`(刷新), `Zhihu: Search`(搜素)。 299 | 300 |

301 |

302 | 303 | 最右侧的更多栏点开,可以看到 `Zhihu: Logout` (注销) 命令按钮: 304 | 305 |

306 |

307 | 308 | 在 Markdown 页面内,可以在编辑窗口的右上角看到两个按钮: 309 | 310 |

311 |

312 | 313 | 左侧的为 `Zhihu: Publish`(发布答案),右侧已删除。 314 | 315 | ## ⌨ 快捷键 316 | 317 | >表格中未涉及的命令没有默认快捷键,用户可以根据自己需要进行设置,注意快捷键的下按方式是先按住 ctrl+z,松开 ctrl,再按下一个按键。 318 | 319 | | 命令 | Windows | Mac | 320 | | :-------------: |:-------------:| :-----:| 321 | | Zhihu: Paste Image From Clipboard | ctrl+alt+p | cmd+alt+p | 322 | |Zhihu: Upload Image From Path | ctrl+alt+q | cmd+alt+q 323 | | Zhihu: Upload Image From Explorer | ctrl+alt+f | cmd+alt+f 324 | 325 | ## ⚙ 配置项 326 | 327 | 328 | | 配置 | 效果 | 329 | | :-------------: |:-------------:| 330 | | Zhihu: Use VSTheme | 打勾开启知乎默认主题样式 | 331 | |Zhihu: Is Title Image Full Screen | 打勾开让文章背景图片变成全屏 | 332 | 333 | -------------------------------------------------------------------------------- /USAGE.md: -------------------------------------------------------------------------------- 1 | $$ 2 | \sqrt5 3 | $$ -------------------------------------------------------------------------------- /azure-pipelines.yml: -------------------------------------------------------------------------------- 1 | trigger: 2 | - '*' 3 | pr: 4 | - '*' 5 | 6 | strategy: 7 | matrix: 8 | linux: 9 | imageName: 'ubuntu-16.04' 10 | mac: 11 | imageName: 'macos-10.14' 12 | windows: 13 | imageName: 'vs2017-win2016' 14 | 15 | pool: 16 | vmImage: $(imageName) 17 | 18 | steps: 19 | 20 | - task: NodeTool@0 21 | inputs: 22 | versionSpec: '8.x' 23 | displayName: 'Install Node.js' 24 | 25 | - bash: | 26 | /usr/bin/Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 & 27 | echo ">>> Started xvfb" 28 | displayName: Start xvfb 29 | condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux')) 30 | 31 | - bash: | 32 | echo ">>> run tests" 33 | npm install && npm test 34 | displayName: Run Tests 35 | env: 36 | DISPLAY: ':99.0' -------------------------------------------------------------------------------- /context.json: -------------------------------------------------------------------------------- 1 | {"extensionPath":"c:\\Users\\12444\\OneDrive\\Zhihu-VSCode"} -------------------------------------------------------------------------------- /dev.md: -------------------------------------------------------------------------------- 1 | # Version bump checklist 2 | 3 | - Update readme 4 | - Update changelog 5 | - Update document 6 | - bump version 7 | - Commit to github 8 | - vsce publish 9 | - 1. Update the test markdown 10 | 2. Commit to github 11 | 3. Publish to zhihu 12 | - Publish readme to zhihu 13 | - Promote 14 | * oschina 15 | * cnblogs 16 | * juejin 17 | * jianshu 18 | * zhihu 19 | * github 20 | * bilibili 21 | * medium 22 | * twitter 23 | * facebook 24 | * linkedin 25 | * weibo 26 | * reddit 27 | * telegram 28 | 29 | -------------------------------------------------------------------------------- /docs/wpls-flow-知乎文章发布.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jks-liu/WPL-s/808c135cf1a184bfe14c300044ecf4eaa5e1d928/docs/wpls-flow-知乎文章发布.png -------------------------------------------------------------------------------- /docs/wpls-flow.drawio: -------------------------------------------------------------------------------- 1 | 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 -------------------------------------------------------------------------------- /docs/wpls-flow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jks-liu/WPL-s/808c135cf1a184bfe14c300044ecf4eaa5e1d928/docs/wpls-flow.png -------------------------------------------------------------------------------- /package.cmd: -------------------------------------------------------------------------------- 1 | call vsce package 2 | call code --install-extension zhihu-vscode-extension-0.0.1.vsix -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wpls", 3 | "displayName": "WPL/s - Zhihu Write Publish Loop w/ statistic", 4 | "description": "使用 Markdown 在 VS Code 中进行知乎创作(回答问题,写专栏文章)", 5 | "version": "1.5.1", 6 | "publisher": "jks-liu", 7 | "license": "MIT", 8 | "enableProposedApi": false, 9 | "engines": { 10 | "vscode": "^1.41.0" 11 | }, 12 | "keywords": [ 13 | "zhihu", 14 | "知乎专栏", 15 | "writing", 16 | "知乎", 17 | "Markdown", 18 | "wpls", 19 | "wpl/s", 20 | "wpl", 21 | "写作" 22 | ], 23 | "categories": [ 24 | "Notebooks" 25 | ], 26 | "activationEvents": [ 27 | "onCommand:zhihu.refreshFeed", 28 | "onCommand:zhihu.refreshHotstories", 29 | "onCommand:zhihu.refreshCollection", 30 | "onCommand:zhihu.openWebView", 31 | "onCommand:zhihu.publish", 32 | "onCommand:zhihu.drafts", 33 | "onCommand:zhihu.jianshuPublish", 34 | "onCommand:zhihu.search", 35 | "onCommand:zhihu.login", 36 | "onCommand:zhihu.logout", 37 | "onCommand:zhihu.previousPage", 38 | "onCommand:zhihu.nextPage", 39 | "onCommand:zhihu.uploadImageFromClipboard", 40 | "onCommand:zhihu.uploadImageFromPath", 41 | "onCommand:zhihu.uploadImageFromExplorer", 42 | "onCommand:zhihu.atPeople", 43 | "onCommand:zhihu.jianshuLogin", 44 | "onView:zhihu-explorer", 45 | "onLanguage:markdown" 46 | ], 47 | "repository": { 48 | "type": "git", 49 | "url": "https://github.com/jks-liu/WPL-s" 50 | }, 51 | "icon": "res/media/outline_local_cafe_black_48dp.png", 52 | "main": "./dist/extension.js", 53 | "contributes": { 54 | "viewsContainers": { 55 | "activitybar": [ 56 | { 57 | "id": "zhihu-explorer", 58 | "title": "知乎", 59 | "icon": "res/media/local_cafe_black_48dp.svg" 60 | } 61 | ] 62 | }, 63 | "views": { 64 | "zhihu-explorer": [ 65 | { 66 | "id": "zhihu-feed", 67 | "name": "推荐" 68 | }, 69 | { 70 | "id": "zhihu-hotStories", 71 | "name": "热榜" 72 | }, 73 | { 74 | "id": "zhihu-collection", 75 | "name": "收藏" 76 | } 77 | ] 78 | }, 79 | "commands": [ 80 | { 81 | "command": "zhihu.refreshFeed", 82 | "title": "Zhihu: Refresh Feed", 83 | "icon": { 84 | "light": "res/media/light/refresh_black_24dp.svg", 85 | "dark": "res/media/dark/refresh_white_24dp.svg" 86 | } 87 | }, 88 | { 89 | "command": "zhihu.refreshHotstories", 90 | "title": "Zhihu: Refresh HotStories", 91 | "icon": { 92 | "light": "res/media/light/refresh_black_24dp.svg", 93 | "dark": "res/media/dark/refresh_white_24dp.svg" 94 | } 95 | }, 96 | { 97 | "command": "zhihu.jianshuLogin", 98 | "title": "Jianshu: Login" 99 | }, 100 | { 101 | "command": "zhihu.refreshCollection", 102 | "title": "Zhihu: Refresh Collection", 103 | "icon": { 104 | "light": "res/media/light/refresh_black_24dp.svg", 105 | "dark": "res/media/dark/refresh_write_24dp.svg" 106 | } 107 | }, 108 | { 109 | "command": "zhihu.openWebView", 110 | "title": "Zhihu: openWebView" 111 | }, 112 | { 113 | "command": "zhihu.clearCache", 114 | "title": "Zhihu: Clear Cache" 115 | }, 116 | { 117 | "command": "zhihu.search", 118 | "title": "Zhihu: Search Items", 119 | "icon": { 120 | "light": "res/media/light/search_black_24dp.svg", 121 | "dark": "res/media/dark/search_white_24dp.svg" 122 | } 123 | }, 124 | { 125 | "command": "zhihu.publish", 126 | "title": "Zhihu: Publish", 127 | "icon": { 128 | "light": "res/media/light/publish_black_24dp.svg", 129 | "dark": "res/media/dark/publish_white_24dp.svg" 130 | } 131 | }, 132 | { 133 | "command": "zhihu.drafts", 134 | "title": "Zhihu: drafts", 135 | "icon": { 136 | "light": "res/media/light/drafts_black_24dp.svg", 137 | "dark": "res/media/dark/drafts_white_24dp.svg" 138 | } 139 | }, 140 | { 141 | "command": "zhihu.collect", 142 | "title": "Zhihu: Collect", 143 | "icon": { 144 | "light": "res/media/light/collect.svg", 145 | "dark": "res/media/dark/collect.svg" 146 | } 147 | }, 148 | { 149 | "command": "zhihu.deleteCollectionItem", 150 | "title": "Zhihu: Delete Collection Item", 151 | "icon": { 152 | "light": "res/media/light/delete.svg", 153 | "dark": "res/media/dark/delete.svg" 154 | } 155 | }, 156 | { 157 | "command": "zhihu.deleteEventItem", 158 | "title": "Zhihu: Delete Event Item", 159 | "icon": { 160 | "light": "res/media/light/delete.svg", 161 | "dark": "res/media/dark/delete.svg" 162 | } 163 | }, 164 | { 165 | "command": "zhihu.uploadImageFromClipboard", 166 | "title": "Zhihu: Paste Image From Clipboard" 167 | }, 168 | { 169 | "command": "zhihu.uploadImageFromPath", 170 | "title": "Zhihu: Upload Image From Path" 171 | }, 172 | { 173 | "command": "zhihu.uploadImageFromExplorer", 174 | "title": "Zhihu: Upload Image From Explorer" 175 | }, 176 | { 177 | "command": "zhihu.atPeople", 178 | "title": "Zhihu: @ Zhihuer" 179 | }, 180 | { 181 | "command": "zhihu.login", 182 | "title": "Zhihu: Login", 183 | "icon": { 184 | "light": "res/media/light/login_black_24dp.svg", 185 | "dark": "res/media/dark/login_white_24dp.svg" 186 | } 187 | }, 188 | { 189 | "command": "zhihu.logout", 190 | "title": "Zhihu: Logout" 191 | }, 192 | { 193 | "command": "zhihu.previousPage", 194 | "title": "Zhihu: PreviousPage" 195 | }, 196 | { 197 | "command": "zhihu.nextPage", 198 | "title": "Zhihu: NextPage", 199 | "icon": { 200 | "light": "res/media/light/right-arrow.svg", 201 | "dark": "res/media/dark/right-arrow.svg" 202 | } 203 | }, 204 | { 205 | "command": "zhihu.getLink", 206 | "title": "Zhihu: Get Link" 207 | } 208 | ], 209 | "keybindings": [ 210 | { 211 | "command": "zhihu.uploadImageFromClipboard", 212 | "key": "ctrl+alt+p", 213 | "mac": "cmd+alt+p" 214 | }, 215 | { 216 | "command": "zhihu.uploadImageFromPath", 217 | "key": "ctrl+alt+q", 218 | "mac": "cmd+alt+p" 219 | }, 220 | { 221 | "command": "zhihu.uploadImageFromExplorer", 222 | "key": "ctrl+alt+f", 223 | "mac": "cmd+alt+e" 224 | } 225 | ], 226 | "menus": { 227 | "editor/context": [ 228 | { 229 | "command": "zhihu.publish", 230 | "when": "resourceLangId == markdown", 231 | "group": "zhihu@0" 232 | }, 233 | { 234 | "command": "zhihu.uploadImageFromExplorer", 235 | "when": "resourceLangId == markdown", 236 | "group": "zhihu@2" 237 | } 238 | ], 239 | "editor/title": [ 240 | { 241 | "command": "zhihu.publish", 242 | "when": "resourceLangId == markdown", 243 | "group": "navigation@0" 244 | }, 245 | { 246 | "command": "zhihu.drafts", 247 | "when": "resourceLangId == markdown", 248 | "group": "navigation@1" 249 | } 250 | ], 251 | "explorer/context": [ 252 | { 253 | "command": "zhihu.uploadImageFromPath", 254 | "group": "extension", 255 | "when": "resourceExtname == .png || resourceExtname == .gif || resourceExtname == .jpg" 256 | } 257 | ], 258 | "view/title": [ 259 | { 260 | "command": "zhihu.refreshFeed", 261 | "when": "view == zhihu-feed", 262 | "group": "navigation@0" 263 | }, 264 | { 265 | "command": "zhihu.refreshHotstories", 266 | "when": "view == zhihu-hotStories", 267 | "group": "navigation@0" 268 | }, 269 | { 270 | "command": "zhihu.refreshCollection", 271 | "when": "view == zhihu-collection", 272 | "group": "navigation@0" 273 | }, 274 | { 275 | "command": "zhihu.login", 276 | "when": "view == zhihu-feed", 277 | "group": "navigation" 278 | }, 279 | { 280 | "command": "zhihu.logout", 281 | "when": "view == zhihu-feed", 282 | "group": "secondary" 283 | }, 284 | { 285 | "command": "zhihu.search", 286 | "when": "view == zhihu-feed", 287 | "group": "navigation" 288 | } 289 | ], 290 | "view/item/context": [ 291 | { 292 | "command": "zhihu.previousPage", 293 | "when": "view == zhihu-feed && viewItem == feed", 294 | "group": "more" 295 | }, 296 | { 297 | "command": "zhihu.nextPage", 298 | "when": "view == zhihu-feed && viewItem == feed", 299 | "group": "inline" 300 | }, 301 | { 302 | "command": "zhihu.deleteCollectionItem", 303 | "when": "view == zhihu-collection && viewItem == collect-item", 304 | "group": "inline" 305 | }, 306 | { 307 | "command": "zhihu.deleteEventItem", 308 | "when": "view == zhihu-feed && viewItem == event", 309 | "group": "inline" 310 | } 311 | ] 312 | }, 313 | "configuration": { 314 | "title": "Zhihu", 315 | "properties": { 316 | "zhihu.useVSTheme": { 317 | "type": "boolean", 318 | "default": true, 319 | "description": "Use VSCode default theme color, set false to disable" 320 | }, 321 | "zhihu.isTitleImageFullScreen": { 322 | "type": "boolean", 323 | "default": false, 324 | "description": "Set true to enable full-sized background image" 325 | }, 326 | "zhihu.useWaterMark": { 327 | "type": "boolean", 328 | "default": false, 329 | "description": "Set true to enable watermark" 330 | } 331 | } 332 | }, 333 | "markdown.markdownItPlugins": true, 334 | "markdown.previewStyles": [ 335 | "./node_modules/katex/dist/katex.min.css" 336 | ] 337 | }, 338 | "scripts": { 339 | "vscode:prepublish": "webpack --mode production", 340 | "develop": "webpack --mode development --watch", 341 | "compile": "tsc -p ./", 342 | "watch": "tsc -watch -p ./", 343 | "test": "npm run compile && node ./out/test/runTest.js", 344 | "lint": "eslint -c .eslintrc.js --ext .ts ./src/**/*.ts" 345 | }, 346 | "devDependencies": { 347 | "@types/ali-oss": "^6.0.4", 348 | "@types/cookie": "^0.3.3", 349 | "@types/form-urlencoded": "^2.0.1", 350 | "@types/markdown-it": "0.0.9", 351 | "@types/md5": "^2.1.33", 352 | "@types/mocha": "^5.2.7", 353 | "@types/node": "^10.17.14", 354 | "@types/on-change": "^1.1.0", 355 | "@types/pug": "^2.0.4", 356 | "@types/request": "^2.48.4", 357 | "@types/request-promise": "^4.1.45", 358 | "@types/vscode": "^1.39.0", 359 | "@typescript-eslint/eslint-plugin": "^4.28.5", 360 | "@typescript-eslint/eslint-plugin-tslint": "^2.23.0", 361 | "@typescript-eslint/parser": "^4.28.5", 362 | "eslint": "^6.8.0", 363 | "mocha": "^7.0.1", 364 | "ts-loader": "^9.2.5", 365 | "typescript": "^4.4.2", 366 | "vscode-test": "^1.3.0", 367 | "webpack": "^5.52.0", 368 | "webpack-cli": "^4.8.0" 369 | }, 370 | "dependencies": { 371 | "@types/cheerio": "^0.22.17", 372 | "ali-oss": "^6.5.1", 373 | "Base64": "^1.1.0", 374 | "cheerio": "^1.0.0-rc.3", 375 | "crypto": "^1.0.1", 376 | "form-urlencoded": "^4.1.1", 377 | "markdown-it": "^12.1.0", 378 | "markdown-it-katex": "^2.0.3", 379 | "markdown-it-meta": "^0.0.1", 380 | "markdown-it-zhihu-common": "^1.0.0", 381 | "md5": "^2.2.1", 382 | "on-change": "^1.6.2", 383 | "pug": "^2.0.4", 384 | "request": "^2.88.0", 385 | "request-promise": "^4.2.5", 386 | "tough-cookie": "^3.0.1", 387 | "tough-cookie-filestore": "^0.0.1", 388 | "zhihu-encrypt": "^1.0.0" 389 | } 390 | } 391 | -------------------------------------------------------------------------------- /release_notes/0.2.0.md: -------------------------------------------------------------------------------- 1 | # Zhihu On VSCode 0.20 版本有哪些新功能? 2 | 3 | 不知初版 Zhihu On VSCode 插件使用体验如何?如果喜欢的话,记得去小岱的[项目仓库](https://github.com/niudai/Zhihu-VSCode)打颗 ⭐ 哦! 4 | 5 | 经过和开源社区伙伴的深入讨论,0.20 版本的 feature 如下: 6 | 7 | ### Webview 默认使用 VSCode 主题色 8 | 9 | 板块是透明的,会看起来像透明亚克力: 10 | 11 | ![](https://raw.githubusercontent.com/niudai/ImageHost/master/zhihu/2020-02-16-11-11-22.png) 12 | 13 | >可以在 VSCode 的设置栏中找到 `Use VSTheme` 设置项,取消打勾后,会开启知乎默认的白蓝主题。 14 | 15 | ### 支持定时发布 16 | 17 | 所有的答案,文章发布时,均会多一次询问,用户须选择是稍后发布还是马上发布,如果选择稍后发布,需要输入发布的时间,比如 “5:30 pm”,"9:45 am" 等,目前仅支持当天的时间选择,输入后,你就会在个人中心的“安排”处看到你将发布的答案和发布的时间(需要手动点击刷新): 18 | 19 | ![](https://raw.githubusercontent.com/niudai/ImageHost/master/zhihu/2020-02-16-11-20-14.png) 20 | 21 | 定时发布采用 prelog 技术,中途关闭 VSCode,关机不影响定时发布,只需保证发布时间 VSCode 处于打开状态 && 知乎插件激活状态即可。 22 | 23 | 时间到了之后,你会收到答案发布的通知,该事件也会从“安排”中移除。 24 | 25 | 如果想取消发布,则点击 ❌ 按钮即可: 26 | 27 | ![](https://raw.githubusercontent.com/niudai/ImageHost/master/zhihu/2020-02-16-15-56-31.png) 28 | 29 | >发布事件采用 md5 完整性校验,不允许用户同时预发两篇内容一摸一样的答案或文章。 30 | 31 | ### 增加“分享”和“在浏览器打开”两个按钮 32 | 33 | 由于插件自身轻量的定位,Webview 的内容没有浏览器端更全面,而且为了保证大家可以更方便地将内容分享给其他人,增加了如下两个按钮: 34 | 35 | ![](https://raw.githubusercontent.com/niudai/ImageHost/master/zhihu/2020-02-16-15-29-09.png) 36 | 37 | 点击左侧按钮会在浏览器中打开该页面,点击中间的会将页面的链接复制至粘贴板中。 38 | 39 | ## 其它优化 40 | 41 | 1. 取消了上传图片的默认文件夹。 42 | 2. 取消了宏刷新,点击相应的刷新按钮,只刷新当前的内容。 43 | 44 | >关于知乎插件的一些误解: 45 | 46 | 1. 只能用知乎插件发文章,不能发答案? 47 | 48 | ``` 49 | 错! 知乎插件既可以发答案也可以发文章! 只需按照 readme 里面的要求, 将答案或问题的链接放在顶部即可! 50 | ``` 51 | 52 | 2. 只有一种上传图片的方式? 53 | 54 | ``` 55 | 错! 知乎插件提供了多达三种图片上传方式, 分别是直接从粘贴板中获取图片上传, 一种是在左侧的 explorer 里面右击图片上传, 一种是在编辑页面右击点击 upload image! 每种方式都有其方便的地方, 创作者应该灵活运用。 56 | ``` 57 | 58 | ![](https://raw.githubusercontent.com/niudai/ImageHost/master/zhihu/2020-02-16-16-13-04.png) 59 | 60 |
牛岱
61 | 62 |
63 | 64 |
Feb 16th 2020
65 | 66 | 67 | -------------------------------------------------------------------------------- /release_notes/0.2.1.md: -------------------------------------------------------------------------------- 1 | #! https://zhuanlan.zhihu.com/p/107839880 2 | 3 | >该文章发布于 VSCode-Zhihu 插件 4 | 5 | # Zhihu On VSCode 0.21 版本有哪些新功能? 6 | 7 | 记得去小岱的[项目仓库](https://github.com/niudai/VSCode-Zhihu)打颗 ⭐ 哦! 8 | 9 | 经过和开源社区伙伴的深入讨论,0.21 版本的 feature 如下: 10 | 11 | ### 可以查看点赞数,并给喜欢的内容点赞 12 | 13 | ![Image](https://pic4.zhimg.com/80/v2-d8f61703c731711fe3a3585122c0d676_hd.png) 14 | 15 | 点击按钮点赞。 16 | 17 | ### 显示作者头像,名字,个性签名 18 | 19 | ![Image](https://pic4.zhimg.com/80/v2-157583e100e9e181191d285355332ebf_hd.png) 20 | 21 | 点击头像,可以在浏览器打开该作者的个人主页。 22 | 23 | ### 修改文章 24 | 25 | 发布后的文章,按照和答案相同的方式,将文章的链接以形如: 26 | 27 | ``` 28 | #! https://zhuanlan.zhihu.com/p/107810342 29 | ``` 30 | 31 | 复制至文章顶部,发布即可对原文章进行修改。 32 | 33 | ### 行内latex 34 | 35 | 现在创作的时候,可以直接用 `$\sqrt5$` 的方式写行内 latex, 而块latex还是原来的 `$$\sqrt$$` 语法。 36 | 37 | ### 优化了图标样式 38 | 39 | ![Image](https://pic4.zhimg.com/80/v2-b293132e3d47d8cd48394caf78611bd2_hd.png) 40 | 41 | ## 修复的问题 42 | 43 | >欢迎关注小岱说公众号(daitalk),分享编程心法。 44 | 45 | 1. 修复了文章很多图片显示不出来的bug。 46 | 47 | 关注作者 [@牛岱](https://zhuanlan.zhihu.com/p/107839880)。 48 | 49 | 50 | -------------------------------------------------------------------------------- /release_notes/0.2.2.md: -------------------------------------------------------------------------------- 1 | #! https://zhuanlan.zhihu.com/p/110200460 2 | >该文章发布于 VSCode-Zhihu 插件 3 | 4 | ![Image](https://pic4.zhimg.com/80/v2-4eca8edd31d2a69265d170ebdbb24db7.png) 5 | 6 | # Zhihu On VSCode 0.22: 专栏管理上线? 7 | 8 | 记得去小岱的[项目仓库](https://github.com/niudai/VSCode-Zhihu)打颗 ⭐ 哦! 9 | 10 | 经过和开源社区伙伴的深入讨论,0.22 版本的 feature 如下: 11 | 12 | ### 专栏管理 13 | 14 | 为了让创作者群体更好地创作,在发布文章的时候,用户可以直接选择发布至自己的专栏下: 15 | 16 | ![Image](https://pic4.zhimg.com/80/v2-b6358e6d673e5feb84dd0ad0bd4d52e4.png) 17 | 18 | ### 文章标题智能识别 19 | 20 | 文章标题无需手动输入,插件会自动检测文本的第一个一级头标签: 21 | 22 | ``` 23 | # 这是一个标题(必须只是一个#) 24 | ``` 25 | 26 | 然后将其作为标题,改行的内容也不会进入到正文中,如果没有检测到,还需用户手动输入。 27 | 28 | ### 背景图片智能识别 29 | 30 | 插件会自动扫描文本第一个一级头标签之前的内容,将第一个发现的图片链接作为背景图片: 31 | 32 | ``` 33 | ![Image](https://pic4.zhimg.com/80/v2-157583e100e9e181191d285355332ebf.png) 34 | ``` 35 | ``` 36 | # 标题在这, 上面的链接会变成背景图片, 不会进入正文 37 | ``` 38 | 39 | ### Html 支持 40 | 41 | 可以在正常的 Markdown 文本中插入 html 文本, 扩展了写作能力。 42 | 43 | >绝大多数 html 标签为非法标签,包括 table 在内,会被服务端过滤掉,只有 \, \
, \ 等合法标签才会被服务端存储,具体使用时小伙伴们可以自己尝试。 44 | 45 | ### 增加 Zhihu: Is Title Image Full Screen 配置项 46 | 47 | 用户可以在设置中找到 `Zhihu: Is Title Image Full Screen` 配置项,勾选后,知乎文章的背景图片会变为全屏模式。 48 | 49 | ### 解决的 Issue: 50 | 51 | 1. 解决了大图清晰度不足问题,用户在手机端可点击 `查看原图` 查看高清原图。 52 | 53 | 2. 修复了用 Zhihu On VSCode 发布的文章在 Web 端打开行间距变大的问题。 54 | 55 | 关注项目作者 [@牛岱](https://zhuanlan.zhihu.com/p/107839880)。 56 | 57 | > 声明:该项目为非官方,非盈利的开源软件,目的在于帮助创作者更稳定地输出高质量内容。 58 | 59 | 60 | -------------------------------------------------------------------------------- /release_notes/0.2.3.md: -------------------------------------------------------------------------------- 1 | ![Image](https://media.giphy.com/media/ODy29v7YAJrck/giphy.gif) 2 | 3 | # Zhihu On VSCode 史诗级特性:非侵入式编辑 4 | 5 | >该文章发布于 VSCode-Zhihu 插件 6 | 7 | 如果你熟悉一些开源框架,你一定听过 *Non-Invasive* 这个词,它常常指的是,用户没有必要因为使用这个框架,而在用户编辑的源码或文档中添加关于框架的一些东西。 8 | 9 | 事实上,小岱在 *Zhihu On VSCode* 中,已经做的还不错:用户原本怎么在 VSCode 里写 markdown,就怎么写,无需为了发布在知乎平台上而在文档中添加或改变一些东西,所有的兼容性问题,文档的解析转换过程,都隐藏在了插件中,用户基本上可以达到 “在 VSCode 里预览的是什么样,发布在知乎上就是什么样”,然而,还有一个重要问题没有得以解决,那就是图片的问题。 10 | 11 | 众所周知,一个来自外域的图片链接,是不一定能在知乎平台上正常显示的,因为会涉及到跨域问题,为了安全起见,原则上所有答案/文章中的图片,都要上传至知乎的图床,然后将链接放在答案中,这样才能正常显示。 12 | 13 | 不仅如此,知乎服务端也不允许上传的答案或文章中的图片的来源为非知乎的源,即便我们试图这样做,其实也是不可行的。 14 | 15 | 所以在图片链接上这点,用户需要用插件自身提供的图片上传功能,上传至知乎图床,插件也会自动在 Markdown 文本里插入图片链接,其实已经比较方便了,但不能算作“非侵入式”,因为这改变了用户本来的习惯。 16 | 17 | 于是小岱开始开发了一个新 Feature,这个 Feature 可以让用户无需在意图片链接是否是来自知乎图床,这个链接可以是本地的相对路径,也可以来自于知乎域外,发布至知乎时,所有的图片都能够正常显示。 18 | 19 | 也就是说,随便在你的电脑中拿出一个你以前写好的某个 README,就算这个 README 里面有一堆相对路径的图片,或来自奇奇怪怪的图床的图片,只需右键点击发布,it just works。 20 | 21 | 比如: 22 | 23 | ![Image](https://pic4.zhimg.com/80/v2-0b00790259520bdbda398cd05731b06b.png) 24 | 25 | 源 Markdown 中的所有图片都是相对路径或来自外链。 26 | 27 | 发布后的效果: 28 | 29 | ![Image](https://pic4.zhimg.com/80/v2-22d902f8c869bc61c44c7711fa8e4e00.png) 30 | 31 | **It just works.** 32 | 33 | 所以随着这个功能的实现,知乎插件在内容创作上也真正实现了 “非侵入式”,用户本来怎么写,就怎么写,预览什么样,发布就什么样。 34 | 35 | 好的产品就应该尽可能复杂的东西隐藏起来,把简洁易用的接口让用户使用,如果用户觉得这个插件很复杂,那这个插件就是一个失败之作。 36 | 37 | >该功能刚上线,可能会带来更多的问题,欢迎用户们到小岱的代码仓库下用issue告诉插件存在的问题。 38 | 39 | ### 解决的 Issue: 40 | 41 | 1. 修复了代码块缩成一行的 Bug。 42 | 43 | 2. 文章发布过程中有发布提示: 44 | 45 | ![Image](https://pic4.zhimg.com/80/v2-96e32e783f89ac8714dade4e7c4b1632.png) 46 | 47 | ### 彩蛋 48 | 49 | 因为插件本身利用了知乎 Web 和 App 端没有提供的特性,因此文章的背景图片可以采用 gif 动图。 50 | 51 | 关注项目作者 [@牛岱](https://zhuanlan.zhihu.com/p/107839880)。 52 | 53 | > 声明:该项目为非官方,非盈利的开源软件,目的在于帮助创作者更稳定地输出高质量内容。 54 | 55 | 记得去小岱的[项目仓库](https://github.com/niudai/VSCode-Zhihu)打颗 ⭐ 哦! 56 | 57 | 58 | -------------------------------------------------------------------------------- /release_notes/0.3.0.md: -------------------------------------------------------------------------------- 1 | >该文章发布于 VSCode-Zhihu 插件 2 | 3 | ![Image](https://pic4.zhimg.com/80/v2-4eca8edd31d2a69265d170ebdbb24db7.png) 4 | 5 | # Zhihu On VSCode 0.3 更新 6 | 7 | 经过和开源社区伙伴的深入讨论,0.3 版本的 feature 如下: 8 | 9 | ![Image](https://pic4.zhimg.com/80/v2-d41d8cd98f00b204e9800998ecf8427e.png) 10 | 11 | ### 文章/答案发布后自动生成头部链接 12 | 13 | 在以往的版本中,发布一篇新文章,新答案,就会产生新的链接,但是如果想修改这篇文章或答案,就需要用 `#! https://zhuanlan.zhihu.com/p/126167760` 形式的链接置于文章顶部,在 0.3 版本中,该操作插件会替你自动完成,也就是说,一份源 markdown 文件,发布后会自动指向对应的文章和答案,修改后再发布,即可修改源文章。 14 | 15 | ### 域外图片缓存加速 16 | 17 | 上一个版本中,插件支持了域外链接和本地链接的图片,但是使用起来会发现,发布的时间比较长,尤其是有域外链接的图片时,插件会先把域外的图片下载到本地,再传到知乎图床上,完成链接的替换,在 0.3 版本中,已经上传过的域外链接,插件会有缓存记录,再次发布时,会直接完成链接替换,跳过下载和上传过程。 18 | 19 | 如要清理缓存,请使用 `zhihu.clearCache` 命令。 20 | 21 | ### 支持本地绝对路径 22 | 23 | 上一个版本,图片链接只支持本地相对路径和域外 https 链接,新版本支持本地的绝对路径。(请注意 http:// 协议仍然不支持,请保证域外图片为 https://) 24 | 25 | ### 解决的 Issue: 26 | 27 | 1. 解决了修改文章时,标题和背景图片进入正文的 bug。 28 | 29 | 2. 修复了第二次上传相同图片无法插入链接的问题。 30 | 31 | 记得去小岱的[项目仓库](https://github.com/niudai/VSCode-Zhihu)打颗 ⭐ 哦! 32 | 33 | 关注项目作者 [@牛岱](https://zhuanlan.zhihu.com/p/107839880)。 34 | 35 | > 声明:该项目为非官方,非盈利的开源软件,目的在于帮助创作者更稳定地输出高质量内容。 36 | 37 | 38 | -------------------------------------------------------------------------------- /release_notes/0.4.0.md: -------------------------------------------------------------------------------- 1 | #! https://zhuanlan.zhihu.com/p/141123098 2 | ![Image](https://pic4.zhimg.com/80/v2-0c8e73a2a092ed3686d2fddfcfe93f1a.png) 3 | 4 | # 在 VSCode 里用微信登录知乎是什么体验 5 | 6 | ### *Zhihu On VSCode* 现在支持微信登录啦! 7 | 8 | 在最新版 *Zhihu On VSCode 0.4* 中,知乎er 可以选择使用微信 APP 扫码登录,只需保证知乎账号和微信账号绑定,即可打开微信 APP 扫一扫: 9 | 10 | ![Image](https://pic4.zhimg.com/80/v2-8408ca3963bc92635277a4273c631e76.png) 11 | 12 | > *Zhihu On VSCode* 为什么不支持账号密码登录了? 13 | 14 | 答: 15 | 16 | 因为账号密码登录安全性不好,现在已经支持了知乎 APP 和 微信 APP 的扫码登录,已经足够方便。 17 | 18 | ### 在 VSCode 里面 @ 你想 @ 的 Zhihuer! 19 | 20 | 该版本中,用户可以直接在 Markdown 文本里面 @ 知乎er 啦,只需要输入 @,弹出提示,点击回车: 21 | 22 | ![Image](https://pic4.zhimg.com/80/v2-093163b4907692447791cdeefc70426e.png) 23 | 24 | ![Image](https://pic4.zhimg.com/80/v2-9060706cd61673d83e56f0bd38a1f6bb.png) 25 | 26 | ![Image](https://pic4.zhimg.com/80/v2-b4c1b8ff1cd34657552f3640edb24124.png) 27 | 28 | 选择后,会自动生成 @ 链接,无需手动管理。 29 | 30 | ### 可以使用链接卡片啦! 31 | 32 | 知乎的链接可以变成卡片,只需在链接后面加入 *"card"* 即可: 33 | 34 | ![Image](https://pic4.zhimg.com/80/v2-4ba858842c81a7774c8891ec755867fb.png) 35 | 36 | 发布后: 37 | 38 | ![Image](https://pic4.zhimg.com/80/v2-2b11de9d069601b95cf67c631082f7c1.png) 39 | 40 | ### 可以添加图片描述啦! 41 | 42 | ![Image](https://pic4.zhimg.com/80/v2-382d5746fd25e675a608bb6281cd7753.png) 43 | 44 | 发布后: 45 | 46 | ![Image](https://pic4.zhimg.com/80/v2-6b7730e332dc14821bc431f038ae6d4e.png) 47 | 48 | 记得去小岱的[项目仓库](https://github.com/niudai/VSCode-Zhihu)打颗 ⭐ 哦! 49 | 50 | 关注项目作者 [@牛岱](https://zhuanlan.zhihu.com/p/107839880)。 51 | 52 | > 声明:该项目为非官方,非盈利的开源软件,目的在于帮助创作者更稳定地输出高质量内容。 53 | 54 | 55 | -------------------------------------------------------------------------------- /res/media/dark/collect.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /res/media/dark/delete.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 9 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /res/media/dark/drafts_white_24dp.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /res/media/dark/login_white_24dp.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /res/media/dark/publish_white_24dp.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /res/media/dark/refresh_white_24dp.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /res/media/dark/right-arrow.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 9 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /res/media/dark/search_white_24dp.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /res/media/extension.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jks-liu/WPL-s/808c135cf1a184bfe14c300044ecf4eaa5e1d928/res/media/extension.png -------------------------------------------------------------------------------- /res/media/light/collect.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /res/media/light/delete.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 9 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /res/media/light/drafts_black_24dp.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /res/media/light/login_black_24dp.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /res/media/light/outline_drafts_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jks-liu/WPL-s/808c135cf1a184bfe14c300044ecf4eaa5e1d928/res/media/light/outline_drafts_black_24dp.png -------------------------------------------------------------------------------- /res/media/light/outline_login_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jks-liu/WPL-s/808c135cf1a184bfe14c300044ecf4eaa5e1d928/res/media/light/outline_login_black_24dp.png -------------------------------------------------------------------------------- /res/media/light/outline_publish_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jks-liu/WPL-s/808c135cf1a184bfe14c300044ecf4eaa5e1d928/res/media/light/outline_publish_black_24dp.png -------------------------------------------------------------------------------- /res/media/light/publish_black_24dp.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /res/media/light/refresh_black_24dp.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /res/media/light/right-arrow.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /res/media/light/search_black_24dp.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /res/media/local_cafe_black_48dp.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /res/media/outline_local_cafe_black_48dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jks-liu/WPL-s/808c135cf1a184bfe14c300044ecf4eaa5e1d928/res/media/outline_local_cafe_black_48dp.png -------------------------------------------------------------------------------- /res/media/vs-code-extension-search-zhihu-this.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jks-liu/WPL-s/808c135cf1a184bfe14c300044ecf4eaa5e1d928/res/media/vs-code-extension-search-zhihu-this.png -------------------------------------------------------------------------------- /res/media/vs-code-extension-search-zhihu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jks-liu/WPL-s/808c135cf1a184bfe14c300044ecf4eaa5e1d928/res/media/vs-code-extension-search-zhihu.png -------------------------------------------------------------------------------- /res/media/zhihu-logo-fluent.svg: -------------------------------------------------------------------------------- 1 | 2 | 18 | 20 | 40 | 42 | Created by potrace 1.10, written by Peter Selinger 2001-2011 43 | 44 | 46 | image/svg+xml 47 | 49 | 50 | 51 | 52 | 57 | 62 | 67 | 68 | -------------------------------------------------------------------------------- /res/media/zhihu-logo-material.svg: -------------------------------------------------------------------------------- 1 | 2 | 18 | 20 | 40 | 42 | Created by potrace 1.10, written by Peter Selinger 2001-2011 43 | 44 | 46 | image/svg+xml 47 | 49 | 50 | 51 | 52 | 53 | 58 | 63 | 68 | 69 | -------------------------------------------------------------------------------- /res/shell/linux.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # require xclip(see http://stackoverflow.com/questions/592620/check-if-a-program-exists-from-a-bash-script/677212#677212) 4 | command -v xclip >/dev/null 2>&1 || { echo >&1 "no xclip"; exit 1; } 5 | 6 | # write image in clipboard to file (see http://unix.stackexchange.com/questions/145131/copy-image-from-clipboard-to-file) 7 | if 8 | xclip -selection clipboard -target image/png -o >/dev/null 2>&1 9 | then 10 | xclip -selection clipboard -target image/png -o >$1 2>/dev/null 11 | echo $1 12 | else 13 | echo "no image" 14 | fi -------------------------------------------------------------------------------- /res/shell/mac.applescript: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jks-liu/WPL-s/808c135cf1a184bfe14c300044ecf4eaa5e1d928/res/shell/mac.applescript -------------------------------------------------------------------------------- /res/shell/pc.ps1: -------------------------------------------------------------------------------- 1 | param($imagePath) 2 | 3 | # Adapted from https://github.com/octan3/img-clipboard-dump/blob/master/dump-clipboard-png.ps1 4 | 5 | Add-Type -Assembly PresentationCore 6 | $img = [Windows.Clipboard]::GetImage() 7 | 8 | if ($img -eq $null) { 9 | "no image" 10 | Exit 1 11 | } 12 | 13 | if (-not $imagePath) { 14 | "no image" 15 | Exit 1 16 | } 17 | 18 | $fcb = new-object Windows.Media.Imaging.FormatConvertedBitmap($img, [Windows.Media.PixelFormats]::Rgb24, $null, 0) 19 | $stream = [IO.File]::Open($imagePath, "OpenOrCreate") 20 | $encoder = New-Object Windows.Media.Imaging.PngBitmapEncoder 21 | $encoder.Frames.Add([Windows.Media.Imaging.BitmapFrame]::Create($fcb)) | out-null 22 | $encoder.Save($stream) | out-null 23 | $stream.Dispose() | out-null 24 | 25 | $imagePath -------------------------------------------------------------------------------- /res/template/article.pug: -------------------------------------------------------------------------------- 1 | doctype html 2 | html 3 | head 4 | include css.pug 5 | title= title 6 | body 7 | div(class = 'header') 8 | include header.pug 9 | div(class = 'container') 10 | div(class = 'author') 11 | span(class = 'avatar-span') 12 | a(href = `https://www.zhihu.com/people/${article.author.url_token}`) 13 | img(class = 'avatar-img', src = `${article.author.avatar_url}`, ) 14 | div(class = 'profile') 15 | div(class = 'author-name') #{article.author.name} 16 | div(class = 'author-headline') #{article.author.headline} 17 | div(class='content') !{article.content} 18 | div(class = 'voteup') 19 | button(class='voteup-btn', onclick=`articleUpvote(${article.id})`) #{article.voteup_count} 20 | script 21 | include js/global.js 22 | 23 | -------------------------------------------------------------------------------- /res/template/captcha.pug: -------------------------------------------------------------------------------- 1 | doctype html 2 | html 3 | head 4 | include css.pug 5 | style 6 | include css/captcha.css 7 | title= title 8 | body 9 | div(class = 'header') 10 | include header.pug 11 | div(class = 'container') 12 | img(src= captchaSrc, class= 'captcha') -------------------------------------------------------------------------------- /res/template/css.pug: -------------------------------------------------------------------------------- 1 | style 2 | include css/global-vs.css 3 | if !useVSTheme 4 | style 5 | include css/global.css 6 | title= title 7 | -------------------------------------------------------------------------------- /res/template/css/captcha.css: -------------------------------------------------------------------------------- 1 | img.captcha { 2 | display: block; 3 | margin: 0 auto; 4 | height: 130px; 5 | padding: 60px; 6 | } -------------------------------------------------------------------------------- /res/template/css/global-vs.css: -------------------------------------------------------------------------------- 1 | header.title { 2 | position: fixed; 3 | top: 0; 4 | padding: 10px; 5 | margin-top: px; 6 | box-shadow: 0 1px 3px rgba(10, 10, 10, 0.1); 7 | width: 100%; 8 | } 9 | 10 | svg.Zi.Zi--LabelSpecial { 11 | height: 20px; 12 | right: 8px; 13 | position: relative; 14 | } 15 | 16 | div.voteup { 17 | box-shadow: 0px 0px 4px 4px antiquewhite; 18 | padding: 12px; 19 | } 20 | 21 | img.content_image.lazy { 22 | display: none; 23 | } 24 | 25 | svg.Icon.ZhihuLogo.ZhihuLogo--blue.Icon--logo { 26 | fill: #0084ff; 27 | margin-left: 5px; 28 | margin-bottom: -4px; 29 | } 30 | 31 | img.origin_image.zh-lightbox-thumb { 32 | box-shadow: 0px 0px 10px powderblue; 33 | } 34 | 35 | body.vscode-light, body.vscode-dark { 36 | padding: 0; 37 | } 38 | 39 | body.vscode-dark > .header { 40 | box-shadow: 3px 0px 6px 3px rgba(255, 252, 252, 0.1); 41 | /* width: 700px; */ 42 | } 43 | 44 | body.vscode-dark > .container { 45 | /* border-style: groove; */ 46 | box-shadow: 0px 0px 6px rgba(253, 250, 250, 0.1); 47 | } 48 | 49 | button#favorite, button#share, button#open { 50 | float: right; 51 | margin-right: 20px; 52 | min-width: 0; 53 | border: none; 54 | border-style: none; 55 | font-size: 13px; 56 | padding: 5px; 57 | width: 30px; 58 | height: 30px; 59 | box-shadow: 0 3px 1px -2px rgba(0,0,0,.2), 0 2px 2px 0 rgba(0,0,0,.14), 0 1px 5px 0 rgba(0,0,0,.12); 60 | cursor: pointer; 61 | border-radius: 50%; 62 | } 63 | 64 | img.qrcode { 65 | margin: 0 auto; 66 | width: 200px; 67 | /* margin-top: 180px; */ 68 | box-shadow: 0px 0px 9px rgba(76, 49, 40, 1); 69 | } 70 | 71 | .qr-container { 72 | margin: 0 auto; 73 | text-align: center; 74 | } 75 | 76 | li { 77 | margin-top: 16px; 78 | } 79 | 80 | svg.Zi.Zi--Star.Button-zi { 81 | vertical-align: bottom; 82 | } 83 | 84 | a.internal, a.external { 85 | text-decoration: none; 86 | background: hsla(177, 100%, 91%, 0.88); 87 | padding: 2px; 88 | border-radius: 10px; 89 | box-shadow: 0 5px 5px wheat; 90 | } 91 | 92 | p { 93 | font-weight: 500; 94 | font-size: 16px; 95 | } 96 | 97 | .voteup { 98 | box-shadow: 0px 0px 4px 4px antiquewhite; 99 | padding: 12px; 100 | } 101 | 102 | .profile { 103 | display: inline-block; 104 | margin-left: 14px; 105 | } 106 | 107 | .author-name { 108 | font-size: 1.3em; 109 | font-weight: bolder; 110 | } 111 | 112 | .author { 113 | border-bottom: #99ded8; 114 | border-bottom-style: double; 115 | border-bottom-width: 2px; 116 | padding-bottom: 10px; 117 | /* border-style: dashed; */ 118 | } 119 | 120 | img.avatar-img { 121 | border-radius: 10px; 122 | width: 60px; 123 | box-shadow: 0px 0px 5px #95cab6; 124 | } 125 | 126 | svg.Zi.Zi--Share.Button-zi { 127 | fill: #0084ff; 128 | margin-top: 2px; 129 | } 130 | 131 | svg.Zi.Zi--Star.Button-zi { 132 | vertical-align: bottom; 133 | fill: #0084ff; 134 | } 135 | 136 | svg.Zi.Zi--LabelSpecial { 137 | fill: #0084ff; 138 | } 139 | 140 | div.voteup > button { 141 | cursor: pointer; 142 | border-style: none; 143 | border-radius: 5px; 144 | background: #0084ff; 145 | color: white; 146 | padding: 5px; 147 | padding-left: 10px; 148 | padding-right: 10px; 149 | } 150 | 151 | .container { 152 | /* border-style: groove; */ 153 | padding: 25px; 154 | box-shadow: 0 1px 3px rgba(26,26,26,.1); 155 | width: 700px; 156 | margin: 0 auto; 157 | margin-top: 15px; 158 | } 159 | 160 | .header { 161 | padding: 5px; 162 | box-shadow: 0 1px 3px rgba(26,26,26,.1); 163 | /* width: 700px; */ 164 | margin: 0 auto; 165 | margin-top: 65px; 166 | } 167 | 168 | .description { 169 | width: 900px; 170 | margin: 0 auto; 171 | } 172 | 173 | img.origin_image.zh-lightbox-thumb.lazy { 174 | display: none; 175 | } -------------------------------------------------------------------------------- /res/template/css/global.css: -------------------------------------------------------------------------------- 1 | body { 2 | color: black; 3 | } 4 | 5 | header.title { 6 | background: white; 7 | box-shadow: 0 1px 3px rgba(26,26,26,.1); 8 | } 9 | 10 | svg.Icon.ZhihuLogo.ZhihuLogo--blue.Icon--logo { 11 | fill: #0084ff; 12 | } 13 | 14 | img.origin_image.zh-lightbox-thumb { 15 | box-shadow: 0px 0px 10px powderblue; 16 | } 17 | 18 | body.vscode-light, body.vscode-dark { 19 | background: #f6f6f6; 20 | } 21 | 22 | button#favorite { 23 | color: white; 24 | background-color: #ff1493; 25 | } 26 | 27 | img.qrcode { 28 | box-shadow: 0px 0px 9px rgba(76, 49, 40, 1); 29 | } 30 | 31 | a.internal, a.external { 32 | background: hsla(177, 100%, 91%, 0.88); 33 | box-shadow: 0 5px 5px wheat; 34 | } 35 | 36 | h1, h2, h3, p, blockquote { 37 | /* display: inline; */ 38 | color: black; 39 | /* padding-bottom: 10px; */ 40 | } 41 | 42 | p { 43 | color: black; 44 | } 45 | 46 | .container { 47 | /* border-style: groove; */ 48 | background-color: white; 49 | box-shadow: 0 1px 3px rgba(26,26,26,.1); 50 | } 51 | 52 | .header { 53 | background-color: white; 54 | } 55 | -------------------------------------------------------------------------------- /res/template/css/qrcode.css: -------------------------------------------------------------------------------- 1 | .qr-container { 2 | background: white; 3 | } 4 | 5 | img.qrcode { 6 | padding: 40px; 7 | } -------------------------------------------------------------------------------- /res/template/header.pug: -------------------------------------------------------------------------------- 1 | header(class = 'title') 2 | a 3 | button(id= 'favorite', title= '收藏') 4 | button(id= 'share', title= '分享') 5 | button(id= 'open', title= '在浏览器中打开') 6 | div(class = 'description') 7 | h1 !{title} 8 | h2 !{subTitle} 9 | 10 | -------------------------------------------------------------------------------- /res/template/js/global.js: -------------------------------------------------------------------------------- 1 | const favoriteBtn = document.getElementById('favorite'); 2 | const shareBtn = document.getElementById('share'); 3 | const openBtn = document.getElementById('open'); 4 | const upvoteCode = document.getElementById('upvote'); 5 | const vscode = acquireVsCodeApi(); 6 | 7 | function answerUpvote(id) { 8 | vscode.postMessage({ 9 | command: 'upvoteAnswer', 10 | id: id 11 | }) 12 | } 13 | 14 | function articleUpvote(id) { 15 | vscode.postMessage({ 16 | command: 'upvoteArticle', 17 | id: id 18 | }) 19 | } 20 | 21 | favoriteBtn.addEventListener('click', e => { 22 | vscode.postMessage({ 23 | command: 'collect' 24 | }) 25 | console.log('Favorite Btn Clicked'); 26 | }) 27 | shareBtn.addEventListener('click', e => { 28 | vscode.postMessage({ 29 | command: 'share' 30 | }) 31 | }) 32 | openBtn.addEventListener('click', e => { 33 | vscode.postMessage({ 34 | command: 'open' 35 | }) 36 | }) 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /res/template/qrcode.pug: -------------------------------------------------------------------------------- 1 | doctype html 2 | html 3 | head 4 | include css.pug 5 | style 6 | //- include css/captcha.css 7 | include css/qrcode.css 8 | title= title 9 | body 10 | div(class = 'header') 11 | include header.pug 12 | div(class = 'qr-container') 13 | img(src= qrcodeSrc, class= 'qrcode') -------------------------------------------------------------------------------- /res/template/questions-answers.pug: -------------------------------------------------------------------------------- 1 | doctype html 2 | html 3 | head 4 | include css.pug 5 | body 6 | div(class = 'header') 7 | include header.pug 8 | div(class = 'container') 9 | each answer in answers 10 | div 11 | div(class = 'author') 12 | span(class = 'avatar-span') 13 | a(href = `https://www.zhihu.com/people/${answer.author.url_token}`) 14 | img(class = 'avatar-img', src = `${answer.author.avatar_url}`) 15 | div(class = 'profile') 16 | div(class = 'author-name') #{answer.author.name} 17 | div(class = 'author-headline') #{answer.author.headline} 18 | div(class = 'content') !{answer.content} 19 | div(class = 'voteup') 20 | button(class='voteup-btn', onclick=`answerUpvote(${answer.id})`) #{answer.voteup_count} 21 | script 22 | include js/global.js 23 | 24 | -------------------------------------------------------------------------------- /src/const/CMD.ts: -------------------------------------------------------------------------------- 1 | export enum FeedCmds { 2 | refresh = 'zhihu.refreshFeed', 3 | previousPage = 'zhihu.previousPage', 4 | nextPage = 'zhihu.nextPage' 5 | } 6 | 7 | export enum HotstoriesCmds { 8 | refresh = 'zhihu.refreshHotstories' 9 | } 10 | 11 | export enum CollectionCmds { 12 | refresh = 'zhihu.refreshCollection', 13 | add = 'zhihu.collect', 14 | delete = 'zhihu.deleteCollectionItem' 15 | } 16 | 17 | export enum WebviewCmds { 18 | open = 'zhihu.openWebview' 19 | } 20 | 21 | export enum SearchCmds { 22 | search = 'zhihu.search', 23 | preview = 'zhihu.preview' 24 | } 25 | 26 | export enum AuthorCmds { 27 | publish = 'zhihu.publish', 28 | uploadImageFromClipboard = 'zhihu.uploadImageFromClipboard', 29 | uploadImageFromPath = 'zhihu.uploadImageFromPath', 30 | uploadImageFromExplorer = 'zhihu.uploadImageFromExplorer', 31 | deleteEvent = 'zhihu.deleteEventItem' 32 | } 33 | 34 | export enum AuthCmds { 35 | login = 'zhihu.login', 36 | logout = 'zhihu.logout' 37 | } 38 | 39 | export enum UtilCmds { 40 | /** 41 | * get link of a tree node inherits linkable tree item 42 | */ 43 | getLink = 'zhihu.getLink' 44 | 45 | } -------------------------------------------------------------------------------- /src/const/ENUM.ts: -------------------------------------------------------------------------------- 1 | export enum MediaTypes { 2 | answer = 'answer', 3 | question = 'question', 4 | article = 'article' 5 | } 6 | 7 | export enum SearchTypes { 8 | general = 'general', 9 | question = 'question', 10 | answer = 'answer', 11 | article = 'article' 12 | } 13 | 14 | export enum Weekdays { 15 | Mon = 'Mon', 16 | Tue = 'Tue', 17 | Wed = 'Wed', 18 | Tur = 'Tur', 19 | Fri = 'Fri', 20 | Sat = 'Sat', 21 | Sun = 'Sun' 22 | } 23 | 24 | export const WeekdaysDict = { 25 | Mon: 1, 26 | Tue: 2, 27 | Wed: 3, 28 | Tur: 4, 29 | Fri: 5, 30 | Sat: 6, 31 | Sun: 7 32 | } 33 | 34 | export const LegalImageExt = [ '.jpg', '.jpeg', '.gif', '.png' ]; 35 | 36 | export enum LoginEnum { 37 | sms, 38 | password, 39 | qrcode, 40 | weixin 41 | } 42 | 43 | export const LoginTypes = [ 44 | { value: LoginEnum.qrcode, ch: '二维码'}, 45 | // { value: LoginEnum.sms, ch: '短信验证码' }, 46 | { value: LoginEnum.weixin, ch: '微信'}, 47 | // { value: LoginEnum.password, ch: '密码' }, 48 | ]; 49 | 50 | export const JianshuLoginTypes = [ 51 | { value: LoginEnum.weixin, ch: '微信' } 52 | ] 53 | 54 | export enum SettingEnum { 55 | useVSTheme = 'useVSTheme', 56 | isTitleImageFullScreen = 'isTitleImageFullScreen' 57 | } 58 | 59 | export enum WebviewEvents { 60 | collect = 'collect', 61 | share = 'share', 62 | open = 'open', 63 | upvoteAnswer = 'upvoteAnswer', 64 | upvoteArticle = 'upvoteArticle' 65 | } -------------------------------------------------------------------------------- /src/const/HTTP.ts: -------------------------------------------------------------------------------- 1 | export const DefaultHTTPHeader = { 2 | 'accept-encoding': 'gzip', 3 | // 'Host': 'www.zhihu.com', 4 | // 'Referer': 'https://www.zhihu.com/', 5 | 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ' + 6 | '(KHTML, like Gecko) Chrome/66.0.3359.181 Safari/537.36', 7 | 'content-type': 'application/x-www-form-urlencoded', 8 | // 'x-zse-83': '3_1.1', 9 | // 'x-xsrftoken': 'dCyt1Kb97IN7jeh5SJo92A9mw2bvv9Es', 10 | } 11 | 12 | export const LoginPostHeader = { 13 | 'x-zse-83': '3_2.0', 14 | 'x-xsrftoken': 'HXVUoGikKN8nor8BW9AZEdJAVayIRWSl', 15 | 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ' + 16 | '(KHTML, like Gecko) Chrome/66.0.3359.181 Safari/537.36', 17 | 'accept-encoding': 'gzip', 18 | 'content-type': 'application/x-www-form-urlencoded' 19 | } 20 | 21 | export function WeixinLoginHeader(referer: string) { 22 | 23 | return { 24 | 'authority': 'www.zhihu.com', 25 | 'pragma': 'no-cache', 26 | 'cache-control': 'no-cache', 27 | 'upgrade-insecure-requests': '1', 28 | 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4099.0 Safari/537.36 Edg/83.0.473.0', 29 | 'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9', 30 | 'sec-fetch-site': 'none', 31 | 'sec-fetch-mode': 'navigate', 32 | // 'sec-fetch-user': '?1', 33 | 'sec-fetch-dest': 'document', 34 | 'accept-language': 'en-US,en;q=0.9', 35 | // 'Connection': 'keep-alive', 36 | // 'Accept-Encoding': 'gzip, deflate, br', 37 | // 'referer': encodeURIComponent(referer), 38 | 39 | } 40 | } 41 | 42 | export const QRCodeOptionHeader = { 43 | 'authority': 'www.zhihu.com' 44 | } 45 | 46 | export const ZhihuOSSAgent = { 47 | userAgent: "aliyun-sdk-js/6.1.1 Chrome 81.0.4023.0 on Windows 10 64-bit", 48 | options: { 49 | accessKeyId: "STS.NUn1kMAT3Vd1rX5oeVr6j89y2", // 50 | accessKeySecret: "5XcAJT1xnifo6Vw9Wp3TsbCzBk79g9bY1DUqyAMRPGwy", // access_key 51 | stsToken: "CAISuQJ1q6Ft5B2yfSjIr5bbetH5rIsS4abacH6Ei2UDfrlG1/zS0Dz2IHpJeXNsA+gZtP01n2hT6/4YlqVrSpRCHnvZdc9355gPeOVzkR6E6aKP9rUhpMCPDQr6UmzkvqL7Z+H+U6mDGJOEYEzFkSle2KbzcS7YMXWuLZyOj+wRDLEQRRLqVSdaI91UKwB+yqodLmCDEfe2LibjmHbLdhQK3DBxkmRi86+y79SB4x7F9j3Ax/QSup76L+rWDbllN4wtVMyujq4kNPjT0C9Q9l1S9axty+5mgW6X4YnFWQQLs0vebruPrYNVQVUnNvRgKcltt+PhkPB0gOvXmrnsxgxFVeMvCH6CGdr8mpObQrrzbY5iKO6hIQDf0tGPK9ztsgg/JG8DMARDd58+MH5sBFkrTDXLOjdFBr9RksbIGoABD0qIVcA4CMJeGoHysYZtNBCvOxuQEDA6mSjTNs3+qlbjHM7MRvGhAo5zHg2YvRQckiOaT/MHFab7f/28bBsdmEg6+pnK3padBYIuYPvvx93/Z+n1Z5XQsEMwZbTbdkn1ksmymVYDbgih2i27AjE+9SDvUGBHTVandDAfADXc9AQ=", 52 | bucket: "zhihu-pics", 53 | endpoint: "zhihu-pics-upload.zhimg.com", 54 | region: "oss-cn-hangzhou", 55 | internal: false, 56 | secure: true, 57 | cname: true, 58 | } 59 | }; 60 | 61 | export const JianshuDefaultHeader = { 62 | 'Accept': 'application/json', 63 | 'Content-Type': 'application/json; charset=UTF-8' 64 | } 65 | 66 | -------------------------------------------------------------------------------- /src/const/PATH.ts: -------------------------------------------------------------------------------- 1 | 2 | export const TemplatePath = "res/template"; 3 | 4 | export const CollectionPath = "collection.json"; 5 | 6 | export const EventsPath = "events.json" 7 | 8 | export const ShellScriptPath = "res/shell"; 9 | 10 | export const LightIconPath = "res/media/light"; 11 | 12 | export const DarkIconPath = "res/media/dark"; 13 | 14 | export const ZhihuIconPath = "res/media/zhihu-logo-material.svg"; 15 | 16 | export const ReleaseNotesPath = "release_notes"; -------------------------------------------------------------------------------- /src/const/REG.ts: -------------------------------------------------------------------------------- 1 | export const QuestionPathReg = /^\/question\/(\d+)$/i 2 | 3 | export const QuestionAnswerPathReg = /(^\/question\/(\d+))?\/answer\/(\d+)$/i 4 | 5 | export const ArticlePathReg = /^\/p\/(\d+)$/i 6 | 7 | export const ZhihuPicReg = /^http[s]?:\/\/pic4.zhimg.com/g 8 | -------------------------------------------------------------------------------- /src/const/URL.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * GET, PUT, POST Captcha through this API 3 | */ 4 | export const CaptchaAPI = `https://www.zhihu.com/api/v3/oauth/captcha?lang=en`; 5 | 6 | 7 | /** 8 | * Prefetch QRCode https://www.zhihu.com/api/v3/account/api/login/qrcode 9 | * Get QRCode https://www.zhihu.com/api/v3/account/api/login/qrcode/${token}/image 10 | * Query ScanInfo https://www.zhihu.com/api/v3/account/api/login/qrcode/${token}/scan_info 11 | */ 12 | export const QRCodeAPI = 'https://www.zhihu.com/api/v3/account/api/login/qrcode'; 13 | 14 | /** 15 | * API for Aliyun OSS File Upload 16 | */ 17 | export const ImageUpload = 'https://api.zhihu.com/images'; 18 | 19 | 20 | /** 21 | * Image-hosting domain for zhihu 22 | * `https://pic4.zhimg.com/80/${file_name}_hd.png` 23 | */ 24 | export const ImageHostAPI = 'https://pic4.zhimg.com/80'; 25 | 26 | /** 27 | * Get qrcode ticket 28 | */ 29 | export const UDIDAPI = 'https://www.zhihu.com/udid'; 30 | 31 | /** 32 | * POST Login data to this API to aquire authentication 33 | */ 34 | export const LoginAPI = 'https://www.zhihu.com/api/v3/oauth/sign_in'; 35 | 36 | /** 37 | * Helper link to indicate if already login in 38 | */ 39 | export const SignUpRedirectPage = 'https://www.zhihu.com/signup'; 40 | 41 | /** 42 | * Feed Story 43 | */ 44 | export const FeedStoryAPI = 'https://www.zhihu.com/api/v3/feed/topstory/recommend'; 45 | 46 | /** 47 | * Get hot stories 48 | */ 49 | export const HotStoryAPI = 'https://www.zhihu.com/api/v3/feed/topstory/hot-lists'; 50 | 51 | /** 52 | * Get info about myself 53 | */ 54 | export const SelfProfileAPI = 'https://www.zhihu.com/api/v4/me'; 55 | 56 | /** 57 | * AnswerAPI = `https://www.zhihu.com/api/v4/answers/${answerId}` 58 | * Voters = `https://www.zhihu.com/api/v4/answers/${answerId}/voters` 59 | */ 60 | export const AnswerAPI = 'https://www.zhihu.com/api/v4/answers'; 61 | 62 | /** 63 | * Answer URL 'https://www.zhihu.com/answers' 64 | */ 65 | export const AnswerURL = 'https://www.zhihu.com/answer'; 66 | 67 | /** 68 | * QuestionAPI = 'https://www.zhihu.com/api/v4/questions/${questionId}' 69 | */ 70 | export const QuestionAPI = 'https://www.zhihu.com/api/v4/questions' 71 | 72 | /** 73 | * QuestionURL = 'https://www.zhihu.com/question/${question' 74 | */ 75 | export const QuestionURL = 'https://www.zhihu.com/question' 76 | 77 | /** 78 | * ArticleAPI = 'https://zhuanlan.zhihu.com/api/articles/${articleId}/publish' 79 | * 80 | * `POST` https://zhuanlan.zhihu.com/api/articles/drafts for creation 81 | * 82 | * `PATCH` https://zhuanlan.zhihu.com/api/articles/${articleId}/draft` for patching 83 | * 84 | * `PUT` https://zhuanlan.zhihu.com/api/articles/${articleId}/publish for publishing 85 | */ 86 | 87 | export const ZhuanlanAPI = 'https://zhuanlan.zhihu.com/api/articles'; 88 | 89 | /** 90 | * get columns info 91 | * @param urltoken urlToken of people 92 | */ 93 | export function ColumnAPI(urltoken: string) { 94 | return `https://www.zhihu.com/api/v4/members/${urltoken}/column-contributions?include=data%5B*%5D.column.intro%2Cfollowers%2Carticles_count&offset=0&limit=20` 95 | } 96 | 97 | export function TopicsAPI(searchToken: string) { 98 | return `https://zhuanlan.zhihu.com/api/autocomplete/topics?token=${searchToken}&max_matches=5&use_similar=0&topic_filter=1` 99 | } 100 | 101 | /** 102 | * Html Page: 'https://zhuanlan.zhihu.com/p/${articleId}' 103 | */ 104 | export const ZhuanlanURL = 'https://zhuanlan.zhihu.com/p/'; 105 | 106 | /** 107 | * ArticleAPI = 'https://www.zhihu.com/api/v4/articles/${articleId}' 108 | */ 109 | export const ArticleAPI = 'https://www.zhihu.com/api/v4/articles' 110 | 111 | /** 112 | * Search All items in Zhihu 113 | */ 114 | export const SearchAPI: string = "https://www.zhihu.com/api/v4/search_v3"; 115 | 116 | /** 117 | * return the href link of weixin qrcode 118 | * @param qrId the qrcode img src 119 | */ 120 | export function WeixinLoginQRCodeAPI(qrId: string) { 121 | return `https://open.weixin.qq.com${qrId}` + 122 | "?appid=wx268fcfe924dcb171&redirect_uri=https%3A%2F%2Fwww.zhihu.com%2Foauth%2Fcallback%2Fwechat%3Faction%3Dlogin%26from%3D" + 123 | "&response_type=code&scope=snsapi_login&state=" + 124 | WeixinState + 125 | "#wechat" 126 | 127 | } 128 | 129 | export const WeixinState = "35623532396136362d663237392d343964352d613131652d343037363062383430663164"; 130 | 131 | export function WeixinLoginPageAPI(): string { 132 | return "https://open.weixin.qq.com/connect/qrconnect" + 133 | "?appid=wx268fcfe924dcb171&redirect_uri=https%3A%2F%2Fwww.zhihu.com%2Foauth%2Fcallback%2Fwechat%3Faction%3Dlogin%26from%3D" + 134 | "&response_type=code&scope=snsapi_login&state=" + 135 | WeixinState 136 | } 137 | 138 | 139 | export function WeixinLoginRedirectAPI(): string { 140 | return "https://www.zhihu.com/oauth/redirect/login/wechat?next=/oauth/account_callback&ref_source=other_https://www.zhihu.com/signin?next=%2F"; 141 | } 142 | 143 | export function JianshuWeixinLoginRedirectAPI(): string { 144 | return "https://www.jianshu.com/users/auth/wechat" 145 | } 146 | 147 | /** 148 | * get sms 149 | */ 150 | export const SMSAPI = 'https://www.zhihu.com/api/v3/oauth/sign_in/digits'; 151 | 152 | /** 153 | * default zhihu domain 154 | */ 155 | export const ZhihuDomain = 'zhihu.com' 156 | 157 | 158 | export function AtAutoCompleteURL(token: string): string { 159 | return encodeURI(`https://www.zhihu.com/people/autocomplete?token=${token}&max_matches=10&use_similar=0`); 160 | } 161 | -------------------------------------------------------------------------------- /src/extension.ts: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | import * as fs from "fs"; 4 | import * as MarkdownIt from "markdown-it"; 5 | import markdown_it_zhihu from "markdown-it-zhihu-common"; 6 | import * as meta from "markdown-it-meta"; // import meta from "markdown-it-meta"; not working why? 7 | import * as path from "path"; 8 | import * as vscode from "vscode"; 9 | import { AccountService } from "./service/account.service"; 10 | import { AuthenticateService } from "./service/authenticate.service"; 11 | import { CollectionService } from "./service/collection.service"; 12 | import { EventService } from "./service/event.service"; 13 | import { HttpService, clearCache } from "./service/http.service"; 14 | import { PasteService } from "./service/paste.service"; 15 | import { PipeService } from "./service/pipe.service"; 16 | import { ProfileService } from "./service/profile.service"; 17 | import { PublishService } from "./service/publish.service"; 18 | import { showReleaseNote } from "./service/release-note.service"; 19 | import { SearchService } from "./service/search.service"; 20 | import { WebviewService } from "./service/webview.service"; 21 | import { CollectionItem, CollectionTreeviewProvider } from "./treeview/collection-treeview-provider"; 22 | import { EventTreeItem, FeedTreeItem, FeedTreeViewProvider } from "./treeview/feed-treeview-provider"; 23 | import { HotStoryTreeViewProvider } from "./treeview/hotstory-treeview-provider"; 24 | import { setContext } from "./global/globa-var"; 25 | import { Output } from "./global/logger"; 26 | import * as CacheManager from "./global/cache" 27 | import { ZhihuCompletionProvider, AtPeople } from "./lang/completion-provider"; 28 | 29 | export async function activate(context: vscode.ExtensionContext) { 30 | Output('Extension Activated') 31 | if(!fs.existsSync(path.join(context.extensionPath, './cookie.json'))) { 32 | fs.createWriteStream(path.join(context.extensionPath, './cookie.json')).end() 33 | } 34 | setContext(context); 35 | // Dependency Injection 36 | showReleaseNote() 37 | const zhihuMdParser = new MarkdownIt({ html: true }).use(markdown_it_zhihu).use(meta); 38 | const defualtMdParser = new MarkdownIt(); 39 | const accountService = new AccountService(); 40 | const profileService = new ProfileService(accountService); 41 | await profileService.fetchProfile(); 42 | const collectionService = new CollectionService(); 43 | const hotStoryTreeViewProvider = new HotStoryTreeViewProvider(); 44 | const collectionTreeViewProvider = new CollectionTreeviewProvider(profileService, collectionService) 45 | const webviewService = new WebviewService(collectionService, collectionTreeViewProvider); 46 | const eventService = new EventService(); 47 | const feedTreeViewProvider = new FeedTreeViewProvider(accountService, profileService, eventService); 48 | const searchService = new SearchService(webviewService); 49 | const authenticateService = new AuthenticateService(profileService, accountService, feedTreeViewProvider, webviewService); 50 | const pasteService = new PasteService(); 51 | const pipeService = new PipeService(pasteService); 52 | const publishService = new PublishService(zhihuMdParser, defualtMdParser, webviewService, collectionService, eventService, profileService, pasteService, pipeService); 53 | 54 | 55 | context.subscriptions.push( 56 | vscode.commands.registerCommand("zhihu.openWebView", async (object) => { 57 | await webviewService.openWebview(object); 58 | } 59 | )); 60 | vscode.commands.registerCommand("zhihu.search", async () => 61 | await searchService.getSearchItems() 62 | ); 63 | vscode.commands.registerCommand("zhihu.clearCache", () => { 64 | clearCache() 65 | CacheManager.clearCache() 66 | }) 67 | vscode.commands.registerCommand("zhihu.login", () => 68 | authenticateService.login() 69 | ); 70 | vscode.commands.registerCommand("zhihu.jianshuLogin", () => { 71 | authenticateService.jianshuLogin() 72 | }); 73 | vscode.commands.registerCommand("zhihu.logout", () => 74 | authenticateService.logout() 75 | ); 76 | vscode.window.registerTreeDataProvider( 77 | "zhihu-feed", 78 | feedTreeViewProvider 79 | ); 80 | vscode.window.registerTreeDataProvider( 81 | "zhihu-hotStories", 82 | hotStoryTreeViewProvider 83 | ); 84 | vscode.window.registerTreeDataProvider( 85 | "zhihu-collection", 86 | collectionTreeViewProvider, 87 | ) 88 | vscode.commands.registerTextEditorCommand('zhihu.publish', (textEditor: vscode.TextEditor, edit: vscode.TextEditorEdit) => { 89 | publishService.publish(textEditor, edit, false); 90 | }) 91 | vscode.commands.registerTextEditorCommand('zhihu.drafts', (textEditor: vscode.TextEditor, edit: vscode.TextEditorEdit) => { 92 | publishService.publish(textEditor, edit, true); 93 | }) 94 | vscode.commands.registerCommand('zhihu.uploadImageFromClipboard', async () => { 95 | pasteService.uploadImageFromClipboard() 96 | }) 97 | 98 | vscode.commands.registerCommand('zhihu.uploadImageFromPath', (uri: vscode.Uri) => { 99 | pasteService.uploadImageFromPath(uri) 100 | }) 101 | 102 | vscode.commands.registerCommand('zhihu.uploadImageFromExplorer', () => { 103 | pasteService.uploadImageFromExplorer() 104 | }) 105 | vscode.commands.registerCommand("zhihu.refreshFeed", () => { 106 | feedTreeViewProvider.refresh(); 107 | } 108 | ); 109 | vscode.commands.registerCommand("zhihu.refreshHotstories", () => { 110 | hotStoryTreeViewProvider.refresh(); 111 | }) 112 | vscode.commands.registerCommand("zhihu.refreshCollection", () => { 113 | collectionTreeViewProvider.refresh(); 114 | }) 115 | vscode.commands.registerCommand("zhihu.atPeople", () => { 116 | AtPeople() 117 | }) 118 | context.subscriptions.push(vscode.languages.registerCompletionItemProvider('markdown', new ZhihuCompletionProvider 119 | , '@')); 120 | 121 | vscode.commands.registerCommand( 122 | "zhihu.deleteCollectionItem", 123 | (node: CollectionItem) => { 124 | collectionService.deleteCollectionItem(node.item); 125 | collectionTreeViewProvider.refresh(node.parent); 126 | vscode.window.showInformationMessage('已从收藏夹移除'); 127 | } 128 | ) 129 | vscode.commands.registerCommand( 130 | "zhihu.deleteEventItem", 131 | (node: EventTreeItem) => { 132 | eventService.destroyEvent(node.event.hash); 133 | vscode.window.showInformationMessage(`已取消发布!`); 134 | feedTreeViewProvider.refresh(node.parent); 135 | } 136 | ) 137 | vscode.commands.registerCommand( 138 | "zhihu.nextPage", 139 | (node: FeedTreeItem) => { 140 | node.page++; 141 | feedTreeViewProvider.refresh(node); 142 | } 143 | ) 144 | vscode.commands.registerCommand( 145 | "zhihu.previousPage", 146 | (node: FeedTreeItem) => { 147 | node.page--; 148 | feedTreeViewProvider.refresh(node); 149 | } 150 | ) 151 | 152 | 153 | return { 154 | extendMarkdownIt(md: any) { 155 | return md.use(require('markdown-it-katex')); 156 | } 157 | } 158 | } -------------------------------------------------------------------------------- /src/global/cache.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "fs"; 2 | import * as path from "path"; 3 | import { getExtensionPath } from "./globa-var"; 4 | 5 | let cache = {} 6 | 7 | if(!fs.existsSync(path.join(getExtensionPath(), './cache.json'))) { 8 | fs.createWriteStream(path.join(getExtensionPath(), './cookie.json')).end() 9 | } 10 | 11 | function persist() { 12 | fs.writeFileSync(path.join(getExtensionPath(), './cache.json'), JSON.stringify(cache), 'utf8'); 13 | } 14 | 15 | export function setCache(key: string, value: string) { 16 | cache[key] = value; 17 | persist() 18 | } 19 | 20 | export function getCache(key: string) { 21 | return cache[key] 22 | } 23 | 24 | export function clearCache() { 25 | cache = {}; 26 | fs.writeFileSync(path.join(getExtensionPath(), './cache.json'), '') 27 | } 28 | -------------------------------------------------------------------------------- /src/global/cookie.ts: -------------------------------------------------------------------------------- 1 | import { getExtensionPath } from "./globa-var"; 2 | import * as path from "path" 3 | import * as FileCookieStore from "tough-cookie-filestore"; 4 | import { CookieJar, Store } from "tough-cookie"; 5 | import { writeFileSync } from "fs"; 6 | 7 | var store: Store; 8 | var cookieJar: CookieJar; 9 | 10 | export function getCookieStore() { 11 | loadCookie() 12 | return store 13 | } 14 | 15 | export function clearCookieStore() { 16 | writeFileSync(path.join(getExtensionPath(), './cookie.json'), ''); 17 | } 18 | 19 | export function getCookieJar() { 20 | loadCookie() 21 | return cookieJar 22 | } 23 | 24 | function loadCookie() { 25 | if (!store) { 26 | store = new FileCookieStore(path.join(getExtensionPath(), './cookie.json')); 27 | } 28 | if (!cookieJar) { 29 | cookieJar = new CookieJar(store); 30 | } 31 | } -------------------------------------------------------------------------------- /src/global/globa-var.ts: -------------------------------------------------------------------------------- 1 | import * as path from "path"; 2 | import * as vscode from 'vscode'; 3 | import { Output } from './logger'; 4 | 5 | var context: vscode.ExtensionContext; 6 | 7 | export function setContext(c: vscode.ExtensionContext) { 8 | Output('set context') 9 | context = c; 10 | } 11 | 12 | export function getExtensionPath() { 13 | return context ? context.extensionPath : path.join(__dirname, '../../') ; 14 | } 15 | 16 | export function getSubscriptions() { 17 | return context.subscriptions; 18 | } 19 | 20 | export function getGlobalState() { 21 | return context.globalState; 22 | } -------------------------------------------------------------------------------- /src/global/logger.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | 3 | const channel = vscode.window.createOutputChannel('ZHIHU'); 4 | 5 | export function Output(str: string, level?: string) { 6 | if (level) { 7 | switch (level) { 8 | case 'warn': 9 | vscode.window.showWarningMessage(str); 10 | break; 11 | case 'info': 12 | vscode.window.showInformationMessage(str); 13 | break; 14 | case 'error': 15 | vscode.window.showErrorMessage(str); 16 | break; 17 | } 18 | } 19 | channel.appendLine(str); 20 | } -------------------------------------------------------------------------------- /src/lang/completion-provider.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { AtAutoCompleteURL } from "../const/URL"; 3 | import { sendRequest } from "../service/http.service"; 4 | 5 | 6 | 7 | export class ZhihuCompletionProvider implements vscode.CompletionItemProvider { 8 | 9 | async provideCompletionItems(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken, context: vscode.CompletionContext): Promise { 10 | // var curWord = document.getText(document.getWordRangeAtPosition(position)); 11 | // if (curWord == '@') { 12 | var item = new vscode.CompletionItem('@ 知乎er?', vscode.CompletionItemKind.Event); 13 | item.command = { command: "zhihu.atPeople", title: "@ 知乎er" }; 14 | item.insertText = ""; 15 | // list.push(new vscode.Comp('牛岱', vscode.CompletionItemKind.EnumMember)); 16 | return [item]; 17 | } 18 | 19 | resolveCompletionItem?(item: vscode.CompletionItem, token: vscode.CancellationToken): vscode.ProviderResult { 20 | throw new Error("Method not implemented."); 21 | } 22 | 23 | } 24 | 25 | export async function AtPeople() { 26 | const keywordString: string | undefined = await vscode.window.showInputBox({ 27 | ignoreFocusOut: true, 28 | prompt: "输入你想 @ 的人:", 29 | placeHolder: "", 30 | }); 31 | if (!keywordString) return; 32 | var respArray = (await sendRequest({ 33 | uri: AtAutoCompleteURL(keywordString), 34 | gzip: true, 35 | json: true 36 | }))[0]; 37 | if (!respArray) return; 38 | // respArray contains item like this: 39 | /** 40 | * [ 41 | * "people", 42 | * "牛岱", 43 | * "niu-dai-68-44", 44 | * "https://pic3.zhimg.com/50/v2-7cafc2ea67c9088537e95f4f039486f5_s.jpg", 45 | * "b50644ff6e611664f9518847da1d2e05", 46 | * "VSCode知乎插件作者。微信公众号 小岱说", 47 | * [ 48 | * 0, 49 | * 0, 50 | * 0 51 | * ], 52 | * "" 53 | * ] 54 | */ 55 | const selectedPeople: any[] = await vscode.window.showQuickPick( 56 | respArray.slice(1).map(item => ({ user: item, id: item[2], label: item[1], description: item[5] })), 57 | { placeHolder: "选择你想要的结果:" } 58 | ).then(r => r ? r.user : undefined); 59 | if (!selectedPeople) return 60 | const editor = vscode.window.activeTextEditor; 61 | const uri = editor.document.uri; 62 | if (uri.scheme === "untitled") { 63 | vscode.window.showWarningMessage("请先保存当前编辑文件!"); 64 | return; 65 | } 66 | editor.edit(e => { 67 | const current = editor.selection; 68 | var range: vscode.Range = new vscode.Range(new vscode.Position(current.start.line, current.start.character-1), current.start); 69 | e.delete(range); 70 | e.insert(current.start, `[@${selectedPeople[1]}](https://www.zhihu.com/people/${selectedPeople[2]})`) 71 | }); 72 | 73 | // Output(selectedPeople, 'info'); 74 | // this.webviewService.openWebview(selectedPeople.object); 75 | } -------------------------------------------------------------------------------- /src/model/article/article-detail.ts: -------------------------------------------------------------------------------- 1 | import { ITarget } from "../target/target"; 2 | 3 | export interface IArticle extends ITarget { 4 | title: string, 5 | excerpt_title: "", 6 | image_url: "", 7 | title_image: "", 8 | excerpt: string, 9 | content: string, 10 | } -------------------------------------------------------------------------------- /src/model/error/error.model.ts: -------------------------------------------------------------------------------- 1 | export interface IErrorMessage { 2 | error: { 3 | code: number, 4 | name: string, 5 | message: string 6 | } 7 | } -------------------------------------------------------------------------------- /src/model/hot-story.model.ts: -------------------------------------------------------------------------------- 1 | import { Paging } from "./paging.model"; 2 | import { IStoryTarget } from "./target/target"; 3 | 4 | export interface HotStoryPage { 5 | fresh_text?: string; 6 | paging?: Paging; 7 | data?: HotStory[]; 8 | } 9 | 10 | export interface HotStory { 11 | style_type?: string; 12 | detail_text?: string; 13 | target?: IStoryTarget; 14 | trend?: number; 15 | debut?: boolean; 16 | card_id?: string; 17 | children?: [ 18 | { 19 | type?: string; 20 | thumbnail?: string; // pic for current story 21 | } 22 | ]; 23 | attached_info: string; 24 | type: string; 25 | id: string; 26 | } -------------------------------------------------------------------------------- /src/model/login.model.ts: -------------------------------------------------------------------------------- 1 | export interface ILogin { 2 | client_id: string; // c3cef7c66a1843f8b3a9e6a1e3160e20 3 | grant_type: string; // password 4 | source: string; // com.zhihu.web 5 | username: string; // +86 6 | password: string; 7 | lang: string; // cn 8 | ref_source: string; // other_https://www.zhihu.com/signin?next=%2F 9 | utm_source: ''; 10 | captcha: any; 11 | timestamp: number; // instant.now() 12 | signature: string; 13 | } 14 | 15 | export interface ISmsData { 16 | phone_no: string; // +86..., 17 | sms_type: string; // text as default 18 | } -------------------------------------------------------------------------------- /src/model/paging.model.ts: -------------------------------------------------------------------------------- 1 | export interface Paging { 2 | is_end: boolean; 3 | previous: string; 4 | next: string; 5 | } -------------------------------------------------------------------------------- /src/model/publish/answer.model.ts: -------------------------------------------------------------------------------- 1 | export interface IPostAnswer { 2 | 3 | /** 4 | * main content 5 | */ 6 | content: string; 7 | 8 | /** 9 | * Default as 'allowed' 10 | */ 11 | reshipment_settings?: string; 12 | 13 | /** 14 | * Default as 'all' 15 | */ 16 | comment_permission?: string; 17 | 18 | /** 19 | * reward setting 20 | */ 21 | reward_setting?: { 22 | /** 23 | * default as false 24 | */ 25 | can_reward: boolean; 26 | } 27 | } 28 | 29 | export class PostAnswer implements IPostAnswer { 30 | constructor( 31 | public content: string, 32 | public reshipment_settings?: string, 33 | public comment_permission?: string, 34 | public reward_setting?: { 35 | can_reward: boolean 36 | } 37 | ) { 38 | if (!this.reshipment_settings) this.reshipment_settings = 'allowed' 39 | if (!this.comment_permission) this.comment_permission = 'all'; 40 | if (!this.reward_setting) this.reward_setting = { 41 | can_reward: false 42 | } 43 | } 44 | 45 | } -------------------------------------------------------------------------------- /src/model/publish/article.model.ts: -------------------------------------------------------------------------------- 1 | import { ITarget, IAuthorTarget } from "../target/target"; 2 | import { IColumn } from "./column.model"; 3 | 4 | export interface IPostArticle { 5 | 6 | /** 7 | * background image link 8 | */ 9 | titleImage: string; // title image link 10 | 11 | isTitleImageFullScreen: boolean; 12 | 13 | delta_time: number; // usually 0 14 | 15 | /** 16 | * article title 17 | */ 18 | title: string; 19 | 20 | /** 21 | * inner html for content 22 | */ 23 | content: string; 24 | 25 | column: IColumn; 26 | } 27 | 28 | export interface IPostArticleResp extends ITarget { 29 | updated: number, 30 | reviewers: [], 31 | topics: [], 32 | excerpt: string, 33 | excerpt_title: string, 34 | title_image_size: {"width": 0, "height": 0}, 35 | title: string, 36 | comment_permission: string, 37 | summary: string, 38 | content: string, 39 | has_publishing_draft: false, 40 | state: string, 41 | is_title_image_full_screen: false, 42 | created: 1581062490, 43 | image_url: string, 44 | title_image: string, 45 | } -------------------------------------------------------------------------------- /src/model/publish/column.model.ts: -------------------------------------------------------------------------------- 1 | // Generated by https://quicktype.io 2 | 3 | export interface IColumn { 4 | accept_submission: boolean; 5 | title: string; 6 | url: string; 7 | comment_permission: string; 8 | author: Author; 9 | updated: number; 10 | intro: string; 11 | image_url: string; 12 | followers: number; 13 | type: string; 14 | id: string; 15 | articles_count: number; 16 | } 17 | 18 | export interface Author { 19 | avatar_url_template: string; 20 | name: string; 21 | headline: string; 22 | gender: number; 23 | user_type: string; 24 | url_token: string; 25 | is_advertiser: boolean; 26 | avatar_url: string; 27 | url: string; 28 | type: string; 29 | badge: any[]; 30 | id: string; 31 | is_org: boolean; 32 | } 33 | -------------------------------------------------------------------------------- /src/model/publish/image.model.ts: -------------------------------------------------------------------------------- 1 | export interface IImageUploadToken { 2 | upload_token: { 3 | access_key: string, 4 | access_token: string, 5 | access_timestamp: number, 6 | access_id: string 7 | }, 8 | upload_file: { 9 | image_id: string, 10 | state: number, 11 | object_key: string // file name 12 | } 13 | } -------------------------------------------------------------------------------- /src/model/search-results.ts: -------------------------------------------------------------------------------- 1 | import { Paging } from "./paging.model"; 2 | import { ISearchTarget } from "./target/target"; 3 | 4 | 5 | export interface ISearchResults { 6 | data?: ISearchItem[]; 7 | paging?: Paging; 8 | } 9 | 10 | export interface ISearchItem { 11 | type?: string; 12 | highlight?: { 13 | description?: string; 14 | title?: string; 15 | }; 16 | object?: ISearchTarget; 17 | 18 | } -------------------------------------------------------------------------------- /src/model/target/target.ts: -------------------------------------------------------------------------------- 1 | export interface ITarget { 2 | id: number; 3 | type: string; // feed_advert should be filtered 4 | author: IAuthorTarget; 5 | url: string; 6 | } 7 | 8 | export interface IStoryTarget extends ITarget { 9 | bound_topic_ids?: number[]; 10 | excerpt?: string; 11 | answer_count?: number; 12 | is_following?: false; 13 | title?: string; 14 | created?: number; 15 | comment_count?: number; 16 | follower_count: number; 17 | } 18 | 19 | // Generated by https://quicktype.io 20 | 21 | export interface ITopicTarget extends ITarget { 22 | introduction: string; 23 | avatar_url: string; 24 | name: string; 25 | excerpt: string; 26 | } 27 | 28 | 29 | export interface IProfile { 30 | id: string, 31 | url_token: string, 32 | name: string, 33 | use_default_avatar: false, 34 | avatar_url: string, 35 | avatar_url_template: string, 36 | is_org: false, 37 | type: string, 38 | url: string, 39 | user_type: string, 40 | headline: string, 41 | gender: number, 42 | uid: string, 43 | } 44 | 45 | export interface IQuestionAnswerTarget extends ITarget { 46 | answer_type?: string; 47 | question?: IQuestionTarget; 48 | is_collapsed?: boolean; 49 | created_time?: number; 50 | updated_time?: number; 51 | extras?: string; 52 | is_copyable?: boolean; 53 | is_normal?: boolean; 54 | content?: string; // inner Html 55 | editable_content?: string; 56 | excerpt?: string; 57 | relationship?: any; 58 | } 59 | 60 | export interface IQuestionTarget extends ITarget { 61 | title?: string; 62 | question_type?: string; 63 | created?: number; 64 | updated_time?: number; 65 | relationship?: any; 66 | 67 | /** 68 | * with html tag 69 | */ 70 | detail: string, 71 | 72 | /** 73 | * no html tag 74 | */ 75 | excerpt: string 76 | } 77 | 78 | export interface IArticleTarget extends ITarget { 79 | title: string; 80 | excerpt_title: string; 81 | image_url: string; 82 | created: number; 83 | updated: number; 84 | voteup_count: 4413; 85 | voting: 0; 86 | comment_count: 201; 87 | excerpt: string; 88 | excerpt_new: string; 89 | } 90 | 91 | export interface IFeedTarget { 92 | id: string; 93 | type: string; 94 | offset: number; 95 | verb: string; 96 | created_time: number; 97 | updated_time: number; 98 | target: IQuestionAnswerTarget & IArticleTarget; 99 | } 100 | 101 | export interface IAuthorTarget extends ITarget { 102 | headline?: string; 103 | avatar_url?: string; 104 | avatar_url_template?: string; 105 | is_org?: boolean; 106 | name?: string; 107 | badge?: any; 108 | gender?: number; 109 | is_advertiser?: boolean; 110 | is_followed?: boolean; 111 | is_privacy?: boolean; 112 | url_token?: string; 113 | user_type?: string; 114 | } 115 | 116 | export interface ISearchTarget extends ITarget { 117 | title?: string; 118 | excerpt?: string; 119 | voteup_count?: number; 120 | comment_count?: number; 121 | created_time?: number; 122 | updated_time?: number; 123 | content?: string; 124 | thumbnail_info?: any; 125 | voting: number; 126 | relationship: any; 127 | flag?: any; 128 | attached_info_bytes?: string; 129 | } 130 | -------------------------------------------------------------------------------- /src/service/account.service.ts: -------------------------------------------------------------------------------- 1 | 2 | import { SelfProfileAPI, SignUpRedirectPage } from "../const/URL"; 3 | import { IProfile } from "../model/target/target"; 4 | import { sendRequest } from "./http.service"; 5 | 6 | 7 | export class AccountService { 8 | public profile: IProfile; 9 | 10 | constructor () {} 11 | 12 | async fetchProfile() { 13 | this.profile = await sendRequest({ 14 | uri: SelfProfileAPI, 15 | json: true 16 | }); 17 | } 18 | 19 | async isAuthenticated(): Promise { 20 | 21 | let checkIfSignedIn; 22 | try { 23 | checkIfSignedIn = await sendRequest({ 24 | uri: SignUpRedirectPage, 25 | followRedirect: false, 26 | followAllRedirects: false, 27 | resolveWithFullResponse: true, 28 | gzip: true, 29 | simple: false 30 | }); 31 | } catch (err) { 32 | console.error('Http error', err); 33 | return false; 34 | } 35 | return Promise.resolve(checkIfSignedIn ? checkIfSignedIn.statusCode == '302' : false); 36 | } 37 | 38 | } -------------------------------------------------------------------------------- /src/service/collection.service.ts: -------------------------------------------------------------------------------- 1 | 2 | import * as fs from "fs"; 3 | import * as path from "path"; 4 | import * as vscode from "vscode"; 5 | import { CollectionPath } from "../const/PATH"; 6 | import { MediaTypes } from "../const/ENUM"; 7 | import { HttpService, sendRequest } from "./http.service"; 8 | import { AnswerAPI, QuestionAPI, ArticleAPI } from "../const/URL"; 9 | import { ITarget } from "../model/target/target"; 10 | import { getExtensionPath } from "../global/globa-var"; 11 | 12 | export interface ICollectionItem { 13 | type: MediaTypes, 14 | id: string 15 | } 16 | 17 | export class CollectionService { 18 | public collection: ICollectionItem[]; 19 | constructor () { 20 | if(fs.existsSync(path.join(getExtensionPath(), CollectionPath))) { 21 | this.collection = JSON.parse(fs.readFileSync(path.join(getExtensionPath(), 'collection.json'), 'utf8')); 22 | } else { 23 | this.collection = [] 24 | } 25 | } 26 | 27 | addItem(item: ICollectionItem) { 28 | if(!this.collection.find(v => v.id == item.id && v.type == item.type)) { 29 | this.collection.push(item); 30 | this.persist(); 31 | return true; 32 | } else return false; 33 | } 34 | 35 | deleteCollectionItem(item: ICollectionItem) { 36 | this.collection = this.collection.filter(c => !(c.id == item.id && c.type == item.type)) 37 | this.persist(); 38 | } 39 | 40 | async getTargets(type?: MediaTypes): Promise<(ITarget & any) []> { 41 | var _collection; 42 | if (type) _collection = this.collection.filter(c => c.type == type) 43 | else _collection = this.collection 44 | var c: ICollectionItem; 45 | var targets: ITarget[] = []; 46 | for (c of _collection) { 47 | var t; 48 | if (c.type == MediaTypes.answer) { 49 | t = await sendRequest({ 50 | uri: `${AnswerAPI}/${c.id}?include=data[*].content,excerpt`, 51 | json: true, 52 | gzip: true 53 | }) 54 | } else if (c.type == MediaTypes.question) { 55 | t = await sendRequest({ 56 | uri: `${QuestionAPI}/${c.id}`, 57 | json: true, 58 | gzip: true 59 | }) 60 | } else if (c.type == MediaTypes.article) { 61 | t = await sendRequest({ 62 | uri: `${ArticleAPI}/${c.id}`, 63 | json: true, 64 | gzip: true 65 | }) 66 | } 67 | targets.push(t); 68 | } 69 | return Promise.resolve(targets) 70 | } 71 | 72 | persist() { 73 | fs.writeFileSync(path.join(getExtensionPath(), CollectionPath), JSON.stringify(this.collection), 'utf8'); 74 | } 75 | 76 | } -------------------------------------------------------------------------------- /src/service/cookie.service.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import * as fs from "fs"; 3 | import * as path from "path"; 4 | import * as cookieUtil from "tough-cookie"; 5 | 6 | export class CookieService { 7 | 8 | constructor(protected context: vscode.ExtensionContext, 9 | protected cookieJar: cookieUtil.CookieJar) { 10 | } 11 | /** 12 | * getCookieString 13 | */ 14 | public getCookieString(currentUrl): string { 15 | return this.cookieJar.getCookieStringSync(currentUrl); 16 | } 17 | 18 | public putCookie(_cookies: string[], currentUrl: string) { 19 | _cookies.map(c => { 20 | return cookieUtil.Cookie.parse(c); 21 | }).forEach(c => { 22 | this.cookieJar.setCookieSync(c, currentUrl); 23 | }); 24 | } 25 | 26 | } -------------------------------------------------------------------------------- /src/service/event.service.ts: -------------------------------------------------------------------------------- 1 | 2 | import * as fs from "fs"; 3 | import * as path from "path"; 4 | import * as vscode from "vscode"; 5 | import { MediaTypes } from "../const/ENUM"; 6 | import { EventsPath } from "../const/PATH"; 7 | import { getExtensionPath } from "../global/globa-var"; 8 | 9 | export interface IEvent { 10 | 11 | type: MediaTypes, 12 | 13 | /** 14 | * id is optional because the new article has no id 15 | */ 16 | id?: string, 17 | 18 | content: string, 19 | 20 | /** 21 | * md5 hash used to identify the publish event. 22 | * Since the collision possibility is almost zero, (1/2^128), 23 | * so in 10s scale we could convince every hash is unique. 24 | */ 25 | hash: string, 26 | 27 | /** 28 | * the publishing time 29 | */ 30 | date: Date, 31 | title?: string, 32 | 33 | /** 34 | * used to cancel the event 35 | */ 36 | timeoutId?: NodeJS.Timeout, 37 | 38 | /** 39 | * the handler to be excuted in the due time. 40 | */ 41 | handler(t?: string): any; 42 | } 43 | 44 | export class EventService { 45 | private events: IEvent[]; 46 | constructor () { 47 | if(fs.existsSync(path.join(getExtensionPath(), EventsPath))) { 48 | let _events: any[] = JSON.parse(fs.readFileSync(path.join(getExtensionPath(), EventsPath), 'utf8')); 49 | this.events = _events.map(e => { e.date = new Date(e.date); 50 | return e}); 51 | } else { 52 | this.events = []; 53 | } 54 | } 55 | 56 | getEvents(): IEvent[] { 57 | return this.events; 58 | } 59 | 60 | /** 61 | * Set events to observed proxy events 62 | * @param evts the observed proxy evts 63 | */ 64 | setEvents(evts: IEvent[]) { 65 | this.events = evts; 66 | } 67 | 68 | registerEvent(e: IEvent) { 69 | e.timeoutId = setTimeout(e.handler, e.date.getTime() - Date.now()); 70 | if(!this.events.find(v => v.hash == e.hash)) { 71 | this.events.push(e); 72 | this.persist(); 73 | return true; 74 | } else return false; 75 | } 76 | 77 | /** 78 | * destroy an event. This could be called normally when event occured, 79 | * but also called intendedly for deletion. 80 | * @param hash the hash of the event 81 | */ 82 | destroyEvent(hash: string) { 83 | // find the target event and destroy its timeout event 84 | let eventTarget = this.events.find(c => (c.hash == hash)) 85 | 86 | // if the timeout handler still registerd, remove it. 87 | if (eventTarget.timeoutId) clearTimeout(eventTarget.timeoutId); 88 | 89 | // filter the target out 90 | this.events = this.events.filter(c => !(c.hash == hash)); 91 | this.persist(); 92 | } 93 | 94 | persist() { 95 | fs.writeFileSync(path.join(getExtensionPath(), EventsPath), JSON.stringify(this.events, (k, v) => { 96 | if (k == 'timeoutId') return undefined; 97 | else return v; 98 | }), 'utf8'); 99 | } 100 | 101 | } -------------------------------------------------------------------------------- /src/service/http.service.ts: -------------------------------------------------------------------------------- 1 | 2 | import * as httpClient from "request-promise"; 3 | import { Cookie, CookieJar, Store } from "tough-cookie"; 4 | import { DefaultHTTPHeader } from "../const/HTTP"; 5 | import { ZhihuDomain } from "../const/URL"; 6 | import { getCookieJar, getCookieStore, clearCookieStore } from "../global/cookie"; 7 | import { Output } from "../global/logger"; 8 | import { IProfile } from "../model/target/target"; 9 | 10 | interface CacheItem { 11 | url: string, 12 | data: any 13 | } 14 | 15 | export class HttpService { 16 | public profile: IProfile; 17 | public xsrfToken: string; 18 | public cache = {}; 19 | 20 | constructor() { 21 | } 22 | 23 | 24 | public async sendRequest(options): Promise { 25 | 26 | if (options.headers == undefined) { 27 | options.headers = DefaultHTTPHeader; 28 | try { 29 | options.headers['cookie'] = getCookieJar().getCookieStringSync(options.uri); 30 | } catch (error) { 31 | console.log(error) 32 | } 33 | } 34 | if (this.xsrfToken) { 35 | options.headers['x-xsrftoken'] = this.xsrfToken; 36 | } 37 | options.headers['cookie'] = getCookieJar().getCookieStringSync(options.uri); 38 | // options.headers['cookie'] = getCookieJar().getCookieStringSync('www.zhihu.com'); 39 | // headers['cookie'] = cookieService.getCookieString(options.uri); 40 | let returnBody; 41 | if (options.resolveWithFullResponse == undefined || options.resolveWithFullResponse == false) { 42 | returnBody = true; 43 | } else { 44 | returnBody = false; 45 | } 46 | options.resolveWithFullResponse = true; 47 | 48 | options.simple = false; 49 | 50 | let resp; 51 | if (!this.cache) this.cache = {} 52 | try { 53 | if (this.cache[options.uri]) { 54 | // cache hit 55 | resp = this.cache[options.uri] 56 | } else { 57 | // cache miss 58 | resp = await httpClient(options); 59 | if (resp.headers['set-cookie']) { 60 | resp.headers['set-cookie'].map(c => Cookie.parse(c)) 61 | .forEach(c => { 62 | // delete c.domain 63 | getCookieJar().setCookieSync(c, options.uri) 64 | getCookieStore().findCookie(ZhihuDomain, '/', '_xsrf', (err, c) => { 65 | if(c) { this.xsrfToken = c.value } 66 | }) 67 | }); 68 | } 69 | if (options.enableCache) { 70 | this.cache[options.uri] = resp; 71 | } 72 | } 73 | } catch (error) { 74 | // vscode.window.showInformationMessage('请求错误'); 75 | Output(error); 76 | return Promise.resolve(null); 77 | } 78 | if (returnBody) { 79 | return Promise.resolve(resp.body) 80 | } else { 81 | return Promise.resolve(resp); 82 | } 83 | 84 | } 85 | 86 | public clearCookie(domain?: string) { 87 | if (domain == undefined) { 88 | getCookieStore().removeCookies(ZhihuDomain, null, err => console.log(err)); 89 | clearCookieStore(); 90 | } 91 | this.xsrfToken = undefined; 92 | } 93 | 94 | public clearCache() { 95 | this.cache = {} 96 | } 97 | } 98 | 99 | const httpService = new HttpService() 100 | 101 | export const sendRequest = (options) => httpService.sendRequest(options); 102 | export const clearCookie = (domain?: string) => httpService.clearCookie(domain); 103 | export const clearCache = () => httpService.clearCache(); 104 | -------------------------------------------------------------------------------- /src/service/paste.service.ts: -------------------------------------------------------------------------------- 1 | import * as OSS from "ali-oss"; 2 | import * as childProcess from "child_process"; 3 | import * as fs from "fs"; 4 | import * as os from "os"; 5 | import * as md5 from "md5"; 6 | import * as path from "path"; 7 | import * as vscode from "vscode"; 8 | import { LegalImageExt } from "../const/ENUM"; 9 | import { ZhihuOSSAgent } from "../const/HTTP"; 10 | import { ShellScriptPath } from "../const/PATH"; 11 | import { ImageHostAPI, ImageUpload } from "../const/URL"; 12 | import { getExtensionPath } from "../global/globa-var"; 13 | import { Output } from "../global/logger"; 14 | import { IImageUploadToken } from "../model/publish/image.model"; 15 | import { sendRequest } from "./http.service"; 16 | import { getCache, setCache } from "../global/cache"; 17 | // import * as sharp from "sharp"; 18 | 19 | /** 20 | * Paste Service for image upload 21 | */ 22 | export class PasteService { 23 | 24 | public constructor( 25 | ) { 26 | } 27 | /** 28 | * ## @zhihu.uploadImageFromClipboard 29 | * @param imagePath path to be pasted. use default if not set. 30 | * @return object_name generated by OSS 31 | */ 32 | public uploadImageFromClipboard() { 33 | const imagePath = path.join(getExtensionPath(), `${Date.now()}.png`); 34 | this.saveClipboardImageToFileAndGetPath(imagePath, async () => { 35 | await this.uploadImageFromLink(imagePath, true); 36 | fs.unlinkSync(imagePath); 37 | }); 38 | } 39 | 40 | public async uploadImageFromExplorer(uri?: vscode.Uri) { 41 | const imageUri = await vscode.window.showOpenDialog({ 42 | canSelectFiles: true, 43 | canSelectFolders: false, 44 | canSelectMany: false, 45 | filters: { 46 | Images: ["png", "jpg", "gif"], 47 | }, 48 | openLabel: "选择要上传的图片:", 49 | }).then(uris => { 50 | return uris ? uris[0] : undefined 51 | ; 52 | }); 53 | this.uploadImageFromLink(imageUri.fsPath, true); 54 | } 55 | 56 | /** 57 | * Upload image from other domains or relative link specified by `link`, and return the resolved zhihu link 58 | * @param link the outer link 59 | */ 60 | public async uploadImageFromLink(link: string, insert?: boolean): Promise { 61 | if (getCache(link)) { 62 | if (insert) { 63 | this.insertImageLink(getCache(link), true); 64 | } 65 | return Promise.resolve(getCache(link)); 66 | } 67 | const zhihu_agent = ZhihuOSSAgent; 68 | const outerPic = /^https?:\/\/.*/g; 69 | let buffer; 70 | if (outerPic.test(link)) { 71 | if (path.extname(link).toLowerCase() === ".svg") { 72 | Output('暂时不支持 SVG 格式的图片', 'warn'); 73 | buffer = undefined 74 | } else { 75 | buffer = await sendRequest({ 76 | uri: link, 77 | gzip: false, 78 | encoding: null, 79 | enableCache: true 80 | }); 81 | } 82 | // const tmpPath = path.join(os.tmpdir(), path.basename(link)); 83 | // fs.writeFileSync(tmpPath, buffer); 84 | // return this.uploadImageFromLink(tmpPath, insert); 85 | } else { 86 | // Get absolute image path 87 | if(!path.isAbsolute(link)) { 88 | const _dir = path.dirname(vscode.window.activeTextEditor.document.uri.fsPath); 89 | link = path.join(_dir, link); 90 | } 91 | try { 92 | // Convert svg to png 93 | if (path.extname(link).toLowerCase() === ".svg") { 94 | Output('暂时不支持 SVG 格式的图片', 'warn'); 95 | buffer = undefined 96 | } else { 97 | buffer = fs.readFileSync(link); 98 | } 99 | } catch (error) { 100 | Output('图片获取失败!', 'warn'); 101 | buffer = undefined 102 | } 103 | } 104 | if (!buffer) { 105 | Output(`${link} 图片获取异常,请调整链接再试!`, 'warn') 106 | throw new Error(`${link} 图片获取异常,请调整链接再试!`); 107 | } 108 | const hash = md5(buffer); 109 | 110 | const options = { 111 | method: "POST", 112 | uri: ImageUpload, 113 | body: { 114 | image_hash: hash, 115 | source: "answer", 116 | }, 117 | headers: {}, 118 | json: true, 119 | resolveWithFullResponse: true, 120 | simple: false, 121 | }; 122 | 123 | const prefetchResp = await sendRequest(options); 124 | if (prefetchResp.statusCode == 401) { 125 | vscode.window.showWarningMessage("登录之后才可上传图片!"); 126 | 127 | return; 128 | } 129 | const prefetchBody: IImageUploadToken = prefetchResp.body; 130 | const upload_file = prefetchBody.upload_file; 131 | if (prefetchBody.upload_token) { 132 | zhihu_agent.options.accessKeyId = prefetchBody.upload_token.access_id; 133 | zhihu_agent.options.accessKeySecret = prefetchBody.upload_token.access_key; 134 | zhihu_agent.options.stsToken = prefetchBody.upload_token.access_token; 135 | const client = new OSS(zhihu_agent.options); 136 | console.log(prefetchBody); 137 | // Object表示上传到OSS的Object名称,localfile表示本地文件或者文件路径 138 | const putResp = client.put(upload_file.object_key, buffer); 139 | console.log(putResp); 140 | putResp.then(r => { 141 | if (insert) { 142 | this.insertImageLink(`${prefetchBody.upload_file.object_key}${path.extname(link)}`); 143 | } 144 | }).catch(e => { 145 | Output(`上传图片${link}失败!`, 'warn') 146 | }) 147 | } else { 148 | if (insert) { 149 | this.insertImageLink(`v2-${hash}${path.extname(link)}`); 150 | } 151 | } 152 | setCache(link, `${ImageHostAPI}/v2-${hash}${path.extname(link)}`); 153 | return Promise.resolve(`${ImageHostAPI}/v2-${hash}${path.extname(link)}`); 154 | } 155 | /** 156 | * ### @zhihu.uploadImageFromPath 157 | * 158 | */ 159 | public async uploadImageFromPath(uri?: vscode.Uri) { 160 | let _path: string; 161 | if (uri) { 162 | _path = uri.fsPath; 163 | } else { 164 | _path = await vscode.env.clipboard.readText(); 165 | } 166 | if (LegalImageExt.includes(path.extname(_path))) { 167 | if (path.isAbsolute(_path)) { 168 | this.uploadImageFromLink(_path, true); 169 | } else { 170 | const workspaceFolders = vscode.workspace.workspaceFolders; 171 | if (workspaceFolders) { 172 | this.uploadImageFromLink(path.join(vscode.workspace.workspaceFolders[0].uri.fsPath, _path), true); 173 | } else { 174 | vscode.window.showWarningMessage("上传图片前请先打开一个文件夹!"); 175 | } 176 | } 177 | } else { 178 | vscode.window.showWarningMessage(`不支持的文件类型!${path.extname(_path)}\n\ 179 | 仅支持上传 ${LegalImageExt.toString()}`); 180 | } 181 | } 182 | 183 | /** 184 | * Insert Markdown inline image in terms of filename 185 | * @param filename 186 | */ 187 | private insertImageLink(filename: string, absolute?: boolean) { 188 | const editor = vscode.window.activeTextEditor; 189 | const uri = editor.document.uri; 190 | if (uri.scheme === "untitled") { 191 | vscode.window.showWarningMessage("请先保存当前编辑文件!"); 192 | return; 193 | } 194 | editor.edit(e => { 195 | const current = editor.selection; 196 | if (absolute) { 197 | e.insert(current.start, `![Image](${filename})`) 198 | } else { 199 | e.insert(current.start, `![Image](${ImageHostAPI}/${filename})`); 200 | } 201 | }); 202 | 203 | } 204 | 205 | private saveClipboardImageToFileAndGetPath(imagePath: string, cb: () => void) { 206 | if (!imagePath) { 207 | return; 208 | } 209 | 210 | const platform = process.platform; 211 | if (platform === "win32") { 212 | // Windows 213 | const scriptPath = path.join(getExtensionPath(), ShellScriptPath, "pc.ps1"); 214 | 215 | let command = "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe"; 216 | const powershellExisted = fs.existsSync(command); 217 | if (!powershellExisted) { 218 | command = "powershell"; 219 | } 220 | 221 | const powershell = childProcess.spawn(command, [ 222 | "-noprofile", 223 | "-noninteractive", 224 | "-nologo", 225 | "-sta", 226 | "-executionpolicy", "unrestricted", 227 | "-windowstyle", "hidden", 228 | "-file", scriptPath, 229 | imagePath, 230 | ]); 231 | powershell.on("error", function (e: Error) { 232 | vscode.window.showErrorMessage(e.message); 233 | }); 234 | powershell.on("exit", function (code, signal) { 235 | // Console.log('exit', code, signal); 236 | }); 237 | powershell.stdout.on("data", function (data: Buffer) { 238 | cb(); 239 | }); 240 | } else if (platform === "darwin") { 241 | // Mac 242 | const scriptPath = path.join(__dirname, ShellScriptPath, "mac.applescript"); 243 | 244 | const ascript = childProcess.spawn("osascript", [scriptPath, imagePath]); 245 | ascript.on("error", function (e) { 246 | vscode.window.showErrorMessage(e.message); 247 | }); 248 | ascript.on("exit", function (code, signal) { 249 | // Console.log('exit',code,signal); 250 | }); 251 | ascript.stdout.on("data", function (data: Buffer) { 252 | cb(); 253 | }); 254 | } else { 255 | // Linux 256 | 257 | const scriptPath = path.join(__dirname, ShellScriptPath, "linux.sh"); 258 | 259 | const ascript = childProcess.spawn("sh", [scriptPath, imagePath]); 260 | ascript.on("error", function (e) { 261 | vscode.window.showErrorMessage(e.message); 262 | }); 263 | ascript.on("exit", function (code, signal) { 264 | // Console.log('exit',code,signal); 265 | }); 266 | ascript.stdout.on("data", function (data: Buffer) { 267 | const result = data.toString().trim(); 268 | if (result == "no xclip") { 269 | vscode.window.showInformationMessage("You need to install xclip command first."); 270 | 271 | return; 272 | } 273 | cb(); 274 | }); 275 | } 276 | } 277 | 278 | } 279 | -------------------------------------------------------------------------------- /src/service/pipe.service.ts: -------------------------------------------------------------------------------- 1 | 2 | import * as vscode from "vscode"; 3 | import { IProfile } from "../model/target/target"; 4 | import { PasteService } from "./paste.service"; 5 | import { ZhihuPicReg } from "../const/REG"; 6 | import Token = require("markdown-it/lib/token"); 7 | 8 | export class PipeService { 9 | public profile: IProfile; 10 | 11 | constructor(protected pasteService: PasteService) { 12 | } 13 | 14 | /** 15 | * convert all cors or local resources into under-zhihu resources 16 | * @param tokens 17 | */ 18 | public async sanitizeMdTokens(tokens: Token[]): Promise { 19 | const images = this.findCorsImage(tokens); 20 | for (const img of images) { 21 | img.attrs[0][1] = await this.pasteService.uploadImageFromLink(img.attrs[0][1]); 22 | } 23 | 24 | // Image in zhihu link card 25 | for (let i = 0; i < tokens.length; i++) { 26 | if (tokens[i].type === 'inline' && tokens[i].children) { 27 | const children = tokens[i].children as Token[]; 28 | for (let i = 0; i < children.length; i++) { 29 | if (children[i].type === 'link_open') { 30 | const image_path = children[i].attrGet("data-image") 31 | if (image_path !== undefined && image_path !== null) { 32 | children[i].attrSet("data-image", await this.pasteService.uploadImageFromLink(image_path)); 33 | } 34 | } 35 | } 36 | } 37 | } 38 | return Promise.resolve(tokens); 39 | } 40 | 41 | private findCorsImage(tokens) { 42 | let images = []; 43 | tokens.forEach(t => images = images.concat(this._findCorsImage(t))); 44 | return images; 45 | } 46 | 47 | private _findCorsImage(token) { 48 | let images = []; 49 | if (token.type == 'image') { 50 | if (!ZhihuPicReg.test(token.attrs[0][1])) 51 | images.push(token); 52 | } 53 | if (token.children) { 54 | token.children.forEach(t => images = images.concat(this._findCorsImage(t))) 55 | } 56 | return images; 57 | } 58 | 59 | } -------------------------------------------------------------------------------- /src/service/profile.service.ts: -------------------------------------------------------------------------------- 1 | 2 | import * as vscode from "vscode"; 3 | import { SelfProfileAPI, ColumnAPI } from "../const/URL"; 4 | import { IProfile } from "../model/target/target"; 5 | import { HttpService, sendRequest } from "./http.service"; 6 | import { AccountService } from "./account.service"; 7 | import { IColumn } from "../model/publish/column.model"; 8 | 9 | export class ProfileService { 10 | public profile: IProfile; 11 | 12 | constructor(protected accountService: AccountService) { 13 | } 14 | 15 | public async fetchProfile() { 16 | if (await this.accountService.isAuthenticated()) { 17 | this.profile = await sendRequest({ 18 | uri: SelfProfileAPI, 19 | json: true, 20 | gzip: true, 21 | }); 22 | } else { 23 | this.profile = undefined; 24 | } 25 | } 26 | 27 | get name(): string { 28 | // this.fetchProfile(); 29 | return this.profile ? this.profile.name : undefined; 30 | } 31 | 32 | get headline(): string { 33 | return this.profile ? this.profile.headline : undefined; 34 | } 35 | 36 | get avatarUrl(): string { 37 | return this.profile ? this.profile.avatar_url : undefined; 38 | } 39 | 40 | async getColumns(): Promise { 41 | if (this.profile) { 42 | return sendRequest({ 43 | uri: ColumnAPI(this.profile.url_token), 44 | json: true, 45 | gzip: true, 46 | method: 'get' 47 | }).then(resp => { 48 | return resp.data.map(element => element.column); 49 | }) 50 | } else { 51 | vscode.window.showWarningMessage('请先登录!'); 52 | return Promise.resolve(null); 53 | } 54 | } 55 | } -------------------------------------------------------------------------------- /src/service/release-note.service.ts: -------------------------------------------------------------------------------- 1 | 2 | import * as fs from "fs"; 3 | import * as path from "path"; 4 | import * as vscode from "vscode"; 5 | import { ReleaseNotesPath } from "../const/PATH"; 6 | import { getGlobalState, getExtensionPath } from "../global/globa-var"; 7 | 8 | export function showReleaseNote() { 9 | let fileNames: string[] = fs.readdirSync(path.join(getExtensionPath(), ReleaseNotesPath)) 10 | let mdFileReg = /^(\d)\.(\d)\.(\d)\.md$/; 11 | let latestVer = fileNames.filter(name => mdFileReg.test(name)).reduce((prev, curr, index, arr) => { 12 | return curr > prev ? curr : prev; 13 | }); 14 | let usrLatestVer = getGlobalState().get('latestVersion'); 15 | if (!usrLatestVer || usrLatestVer < latestVer) { 16 | vscode.commands.executeCommand("markdown.showPreview", vscode.Uri.file(path.join( 17 | getExtensionPath(), ReleaseNotesPath, latestVer 18 | )), null, { 19 | sideBySide: false, 20 | locked: true 21 | }); 22 | getGlobalState().update('latestVersion', latestVer); 23 | } 24 | } 25 | 26 | -------------------------------------------------------------------------------- /src/service/search.service.ts: -------------------------------------------------------------------------------- 1 | import * as httpClient from "request-promise"; 2 | import * as vscode from "vscode"; 3 | import { ISearchItem, ISearchResults } from "../model/search-results"; 4 | import { WebviewService } from "./webview.service"; 5 | import { removeHtmlTag } from "../util/md-html-utils"; 6 | import { SearchTypes } from "../const/ENUM"; 7 | import { SearchAPI } from "../const/URL"; 8 | 9 | 10 | export const SearchDict = [ 11 | { value: SearchTypes.general, ch: '综合' }, 12 | { value: SearchTypes.question, ch: '问题' }, 13 | ]; 14 | 15 | export class SearchService { 16 | constructor( 17 | protected webviewService: WebviewService) { } 18 | 19 | public async getSearchResults(keyword: string, searchType: string): Promise { 20 | const params = { 21 | t: searchType, 22 | q: keyword, 23 | offset: '0', 24 | limit: '10' 25 | }; 26 | const result = await httpClient(`${SearchAPI}?${toQueryString(params)}`); 27 | const jsonResult: ISearchResults = JSON.parse(result); 28 | return Promise.resolve(jsonResult.data.filter(o => o.type == 'search_result')); 29 | } 30 | 31 | public async getSearchItems() { 32 | const selectedSearchType: string = await vscode.window.showQuickPick( 33 | SearchDict.map(type => ({ value: type.value, label: type.ch, description: '' })), 34 | { placeHolder: "你要搜什么?" } 35 | ).then(item => item ? item.value : undefined); 36 | 37 | if (!selectedSearchType) return 38 | 39 | const keywordString: string | undefined = await vscode.window.showInputBox({ 40 | ignoreFocusOut: true, 41 | prompt: "输入关键字, 搜索知乎内容", 42 | placeHolder: "", 43 | }); 44 | if (!keywordString) return; 45 | const searchResults = await this.getSearchResults(keywordString, selectedSearchType); 46 | const selectedItem: ISearchItem | undefined = await vscode.window.showQuickPick( 47 | searchResults.map(item => ({ value: item, label: `${removeHtmlTag(item.highlight.title)}`, description: removeHtmlTag(item.highlight.description) })), 48 | { placeHolder: "选择你想要的结果:" } 49 | ).then(vscodeItem => vscodeItem ? vscodeItem.value : undefined); 50 | if (!selectedItem) return 51 | 52 | this.webviewService.openWebview(selectedItem.object); 53 | } 54 | } 55 | 56 | 57 | function toQueryString(params: { [key: string]: any }): string { 58 | return Object.keys(params).map(k => `${k}=${encodeURIComponent(params[k].toString())}`).join('&'); 59 | } -------------------------------------------------------------------------------- /src/service/update.service.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jks-liu/WPL-s/808c135cf1a184bfe14c300044ecf4eaa5e1d928/src/service/update.service.ts -------------------------------------------------------------------------------- /src/service/webview.service.ts: -------------------------------------------------------------------------------- 1 | 2 | import * as path from "path"; 3 | import { compileFile } from "pug"; 4 | import * as vscode from "vscode"; 5 | import { MediaTypes, SettingEnum, WebviewEvents } from "../const/ENUM"; 6 | import { TemplatePath, ZhihuIconPath } from "../const/PATH"; 7 | import { AnswerAPI, AnswerURL, QuestionAPI, QuestionURL, ZhuanlanURL, ArticleAPI } from "../const/URL"; 8 | import { IArticle } from "../model/article/article-detail"; 9 | import { IQuestionAnswerTarget, IQuestionTarget, ITarget } from "../model/target/target"; 10 | import { CollectionTreeviewProvider } from "../treeview/collection-treeview-provider"; 11 | import { CollectionService, ICollectionItem } from "./collection.service"; 12 | import { HttpService, sendRequest } from "./http.service"; 13 | import { getExtensionPath, getSubscriptions } from "../global/globa-var"; 14 | 15 | export interface IWebviewPugRender { 16 | viewType?: string, 17 | title?: string, 18 | showOptions?: vscode.ViewColumn | { viewColumn: vscode.ViewColumn, preserveFocus?: boolean }, 19 | options?: vscode.WebviewOptions & vscode.WebviewPanelOptions, 20 | pugTemplatePath: string, 21 | pugObjects?: any, 22 | iconPath?: any 23 | } 24 | 25 | export class WebviewService { 26 | 27 | constructor( 28 | protected collectService: CollectionService, 29 | protected collectionTreeviewProvider: CollectionTreeviewProvider 30 | ) { 31 | } 32 | 33 | /** 34 | * Create and show a webview provided by pug 35 | */ 36 | public renderHtml(w: IWebviewPugRender, panel?: vscode.WebviewPanel): vscode.WebviewPanel { 37 | if (panel == undefined) { 38 | panel = vscode.window.createWebviewPanel( 39 | w.viewType ? w.viewType : 'zhihu', 40 | w.title ? w.title : '知乎', 41 | w.showOptions ? w.showOptions : vscode.ViewColumn.One, 42 | w.options ? w.options : { enableScripts: true } 43 | ); 44 | } 45 | const compiledFunction = compileFile( 46 | w.pugTemplatePath 47 | ); 48 | panel.iconPath = vscode.Uri.file(w.iconPath ? w.iconPath : path.join( 49 | getExtensionPath(), 50 | ZhihuIconPath)); 51 | panel.webview.html = compiledFunction(w.pugObjects); 52 | return panel; 53 | } 54 | 55 | public async openWebview(object: ITarget & any) { 56 | if (object.type == MediaTypes.question) { 57 | 58 | const includeContent = "data[*].is_normal,content,voteup_count;"; 59 | let offset = 0; 60 | let questionAPI = `${QuestionAPI}/${object.id}?include=detail%2cexcerpt`; 61 | let answerAPI = `${QuestionAPI}/${object.id}/answers?include=${includeContent}?offset=${offset}`; 62 | let question: IQuestionTarget = await sendRequest({ 63 | uri: questionAPI, 64 | json: true, 65 | gzip: true 66 | }); 67 | let body: { data: IQuestionAnswerTarget[] } = await sendRequest({ 68 | uri: answerAPI, 69 | json: true, 70 | gzip: true 71 | }); 72 | let useVSTheme = vscode.workspace.getConfiguration('zhihu').get(SettingEnum.useVSTheme); 73 | 74 | let panel = this.renderHtml({ 75 | title: "知乎问题", 76 | pugTemplatePath: path.join( 77 | getExtensionPath(), 78 | TemplatePath, 79 | "questions-answers.pug" 80 | ), 81 | pugObjects: { 82 | answers: body.data.map(t => { 83 | t.content = this.actualSrcNormalize(t.content); 84 | return t; 85 | }), 86 | title: question.title, 87 | subTitle: question.detail, 88 | useVSTheme: useVSTheme 89 | } 90 | }) 91 | this.registerEvent(panel, { type: MediaTypes.question, id: object.id }, `${QuestionURL}/${question.id}`); 92 | } else if (object.type == MediaTypes.answer) { 93 | let body: IQuestionAnswerTarget = await sendRequest({ 94 | uri: `${AnswerAPI}/${object.id}?include=data[*].content,excerpt,voteup_count`, 95 | json: true, 96 | gzip: true 97 | }) 98 | let useVSTheme = vscode.workspace.getConfiguration('zhihu').get(SettingEnum.useVSTheme); 99 | body.content = this.actualSrcNormalize(body.content); 100 | let panel = this.renderHtml({ 101 | title: "知乎回答", 102 | pugTemplatePath: path.join( 103 | getExtensionPath(), 104 | TemplatePath, 105 | "questions-answers.pug" 106 | ), 107 | pugObjects: { 108 | answers: [ 109 | body 110 | ], 111 | title: object.question.name, 112 | useVSTheme 113 | } 114 | }) 115 | this.registerEvent(panel, { type: MediaTypes.answer, id: object.id }, `${AnswerURL}/${body.id}`) 116 | } else if (object.type == MediaTypes.article) { 117 | let article: IArticle = await sendRequest({ 118 | uri: `${object.url}?include=voteup_count`, 119 | json: true, 120 | gzip: true, 121 | headers: null 122 | }); 123 | let useVSTheme = vscode.workspace.getConfiguration('zhihu').get(SettingEnum.useVSTheme); 124 | article.content = this.actualSrcNormalize(article.content); 125 | let panel = this.renderHtml({ 126 | title: "知乎文章", 127 | pugTemplatePath: path.join( 128 | getExtensionPath(), 129 | TemplatePath, 130 | "article.pug" 131 | ), 132 | pugObjects: { 133 | article: article, 134 | title: article.title, 135 | useVSTheme 136 | } 137 | }) 138 | this.registerEvent(panel, { type: MediaTypes.article, id: object.id }, `${ZhuanlanURL}${article.id}`) 139 | } 140 | } 141 | 142 | private registerEvent(panel: vscode.WebviewPanel, c: ICollectionItem, link?: string) { 143 | panel.webview.onDidReceiveMessage(e => { 144 | if (e.command == WebviewEvents.collect) { 145 | if (this.collectService.addItem(c)) { 146 | vscode.window.showInformationMessage('收藏成功!'); 147 | } else { 148 | vscode.window.showWarningMessage('你已经收藏了它!'); 149 | } 150 | this.collectionTreeviewProvider.refresh() 151 | } else if (e.command == WebviewEvents.open) { 152 | vscode.env.openExternal(vscode.Uri.parse(link)); 153 | } else if (e.command == WebviewEvents.share) { 154 | vscode.env.clipboard.writeText(link).then(() => { 155 | vscode.window.showInformationMessage('链接已复制至粘贴板。'); 156 | }) 157 | } else if (e.command == WebviewEvents.upvoteAnswer) { 158 | sendRequest({ 159 | uri: `${AnswerAPI}/${e.id}/voters`, 160 | method: 'post', 161 | headers: {}, 162 | json: true, 163 | body: { type: "up" }, 164 | resolveWithFullResponse: true 165 | }).then(r => {if(r.statusCode == 200) vscode.window.showInformationMessage('点赞成功!') 166 | else if(r.statusCode == 403) vscode.window.showWarningMessage('你已经投过票了!')}) 167 | } else if (e.command == WebviewEvents.upvoteArticle) { 168 | sendRequest({ 169 | uri: `${ArticleAPI}/${e.id}/voters`, 170 | method: 'post', 171 | headers: {}, 172 | json: true, 173 | body: { voting: 1 }, 174 | resolveWithFullResponse: true 175 | }).then(r => { if(r.statusCode == 200) vscode.window.showInformationMessage('点赞成功!') 176 | else if(r.statusCode == 403) vscode.window.showWarningMessage('你已经投过票了!'); 177 | }) 178 | } 179 | }, undefined, getSubscriptions()) 180 | } 181 | 182 | private actualSrcNormalize(html: string): string { 183 | return html.replace(/<\/?noscript>/g, ''); 184 | } 185 | } -------------------------------------------------------------------------------- /src/test/runTest.ts: -------------------------------------------------------------------------------- 1 | import * as path from "path"; 2 | 3 | import { runTests } from "vscode-test"; 4 | 5 | /** 6 | * run test 7 | */ 8 | async function main() { 9 | try { 10 | // The folder containing the Extension Manifest package.json 11 | // Passed to `--extensionDevelopmentPath` 12 | const extensionDevelopmentPath: string = path.resolve( __dirname, "../../" ); 13 | 14 | // The path to the extension test script 15 | // Passed to --extensionTestsPath 16 | const extensionTestsPath: string = path.resolve( __dirname, "./suite/index" ); 17 | 18 | // Download VS Code, unzip it and run the integration test 19 | await runTests( { extensionDevelopmentPath, extensionTestsPath } ); 20 | } catch ( err ) { 21 | process.exit(1); 22 | } 23 | } 24 | 25 | main(); 26 | -------------------------------------------------------------------------------- /src/test/suite/extension.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import * as fs from 'fs'; 3 | import * as path from 'path'; 4 | // You can import and use all API from the 'vscode' module 5 | // as well as import your extension to test it 6 | import * as vscode from 'vscode'; 7 | 8 | // import * as myExtension from '../../extension'; 9 | 10 | suite('Extension Test Suite', async () => { 11 | vscode.window.showInformationMessage('Start all tests.'); 12 | 13 | // beans mock initialization 14 | let context: vscode.ExtensionContext = { 15 | extensionPath: path.join(__dirname, '../../../'), 16 | globalState: { 17 | get() {}, 18 | update(key: string, v: string): Promise { return Promise.resolve()} 19 | }, 20 | logPath: '', 21 | storagePath: '', 22 | asAbsolutePath(str) { return ''}, 23 | globalStoragePath: '', 24 | subscriptions: [{dispose() {}} ], 25 | workspaceState: undefined 26 | }; 27 | if(!fs.existsSync(path.join(context.extensionPath, './cookie.json'))) { 28 | fs.createWriteStream(path.join(context.extensionPath, './cookie.json')).end() 29 | } 30 | 31 | 32 | test('Sample test', () => { 33 | assert.equal([1, 2, 3].indexOf(5), -1); 34 | assert.equal([1, 2, 3].indexOf(0), -1); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /src/test/suite/index.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import * as Mocha from 'mocha'; 3 | import * as glob from 'glob'; 4 | 5 | export function run(): Promise { 6 | // Create the mocha test 7 | const mocha = new Mocha({ 8 | ui: 'tdd' 9 | }); 10 | mocha.useColors(true); 11 | 12 | const testsRoot = path.resolve(__dirname, '..'); 13 | 14 | return new Promise((c, e) => { 15 | glob('**/**.test.js', { cwd: testsRoot }, (err, files) => { 16 | if (err) { 17 | return e(err); 18 | } 19 | 20 | // Add files to the test suite 21 | files.forEach(f => mocha.addFile(path.resolve(testsRoot, f))); 22 | 23 | try { 24 | // Run the mocha test 25 | mocha.run(failures => { 26 | if (failures > 0) { 27 | e(new Error(`${failures} tests failed.`)); 28 | } else { 29 | c(); 30 | } 31 | }); 32 | } catch (err) { 33 | console.error(err); 34 | e(err); 35 | } 36 | }); 37 | }); 38 | } 39 | -------------------------------------------------------------------------------- /src/test/suite/paste.service.test.ts: -------------------------------------------------------------------------------- 1 | import { PasteService } from "../../service/paste.service"; 2 | import { join } from "path"; 3 | 4 | export function pasteServiceTest(pasteService: PasteService) { 5 | } -------------------------------------------------------------------------------- /src/treeview/collection-treeview-provider.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { MediaTypes } from '../const/ENUM'; 3 | import { CollectionService, ICollectionItem } from '../service/collection.service'; 4 | import { ProfileService } from '../service/profile.service'; 5 | import { IQuestionAnswerTarget, IQuestionTarget, IArticleTarget } from '../model/target/target'; 6 | import { LinkableTreeItem } from './hotstory-treeview-provider'; 7 | 8 | export interface CollectType { 9 | type?: string; 10 | ch?: string; 11 | } 12 | 13 | export const COLLECT_TYPES = [ 14 | { type: MediaTypes.answer, ch: '答案' }, 15 | { type: MediaTypes.article, ch: '文章' }, 16 | { type: MediaTypes.question, ch: '问题' } 17 | ]; 18 | 19 | export class CollectionTreeviewProvider implements vscode.TreeDataProvider { 20 | 21 | private _onDidChangeTreeData: vscode.EventEmitter = new vscode.EventEmitter(); 22 | readonly onDidChangeTreeData: vscode.Event = this._onDidChangeTreeData.event; 23 | 24 | constructor(private profileService: ProfileService, 25 | private collectionService: CollectionService) { 26 | } 27 | 28 | refresh(node?: CollectionItem): void { 29 | this._onDidChangeTreeData.fire(node); 30 | } 31 | 32 | getTreeItem(element: CollectionItem): vscode.TreeItem { 33 | return element; 34 | } 35 | 36 | getChildren(element?: CollectionItem): Thenable { 37 | 38 | if (element) { 39 | return new Promise(async (resolve, reject) => { 40 | let targets = await this.collectionService.getTargets(element.type); 41 | resolve(targets.map(t => { 42 | return new CollectionItem(t.title ? t.title : t.excerpt, t.type, { type: t.type, id: t.id }, vscode.TreeItemCollapsibleState.None, { 43 | command: 'zhihu.openWebView', 44 | title: 'openWebView', 45 | arguments: [t] 46 | }, element, t); 47 | })) 48 | }); 49 | } else { 50 | return Promise.resolve(this.getCollectionsType()); 51 | } 52 | } 53 | 54 | getParent(element?: CollectionItem): Thenable { 55 | return Promise.resolve(element ? element.parent : undefined); 56 | } 57 | 58 | private async getCollectionsType(): Promise { 59 | await this.profileService.fetchProfile(); 60 | return Promise.resolve(COLLECT_TYPES.map(c => { 61 | return new CollectionItem(c.ch, c.type, undefined, vscode.TreeItemCollapsibleState.Collapsed); 62 | })); 63 | } 64 | 65 | } 66 | 67 | export class CollectionItem extends LinkableTreeItem { 68 | 69 | constructor( 70 | public readonly label: string, 71 | public type: MediaTypes, 72 | public item: ICollectionItem | undefined, 73 | public readonly collapsibleState: vscode.TreeItemCollapsibleState, 74 | public readonly command?: vscode.Command, 75 | public readonly parent?: CollectionItem, 76 | public readonly target?: IQuestionAnswerTarget | IQuestionTarget | IArticleTarget, 77 | ) { 78 | super(label, collapsibleState, target ? target.url : ''); 79 | this.tooltip = this.target ? this.target.excerpt : ''; 80 | this.description = this.target ? this.target.excerpt : ''; 81 | } 82 | 83 | contextValue = this.collapsibleState == vscode.TreeItemCollapsibleState.None ? 'collect-item' : this.type; 84 | 85 | } 86 | -------------------------------------------------------------------------------- /src/treeview/feed-treeview-provider.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { FeedStoryAPI } from '../const/URL'; 3 | import { IArticleTarget, IQuestionAnswerTarget, ITarget, IFeedTarget } from '../model/target/target'; 4 | import { AccountService } from '../service/account.service'; 5 | import { HttpService, sendRequest } from '../service/http.service'; 6 | import { ProfileService } from '../service/profile.service'; 7 | import { LinkableTreeItem } from './hotstory-treeview-provider'; 8 | import { EventService, IEvent } from '../service/event.service'; 9 | import { MediaTypes } from '../const/ENUM'; 10 | import * as onChange from 'on-change'; 11 | import { removeHtmlTag, removeSpace, beautifyDate } from '../util/md-html-utils'; 12 | 13 | export interface FeedType { 14 | type?: string; 15 | ch?: string; 16 | } 17 | 18 | export const FEED_TYPES: FeedType[] = [ 19 | { type: 'feed', ch: '推荐' }, 20 | { type: 'event', ch: '安排' } 21 | ]; 22 | 23 | export class FeedTreeViewProvider implements vscode.TreeDataProvider { 24 | 25 | private _onDidChangeTreeData: vscode.EventEmitter = new vscode.EventEmitter(); 26 | readonly onDidChangeTreeData: vscode.Event = this._onDidChangeTreeData.event; 27 | 28 | constructor( 29 | private accountService: AccountService, 30 | private profileService: ProfileService, 31 | private eventService: EventService) { 32 | } 33 | 34 | refresh(node?: vscode.TreeItem): void { 35 | this._onDidChangeTreeData.fire(node); 36 | } 37 | 38 | getTreeItem(element: FeedTreeItem): vscode.TreeItem { 39 | return element; 40 | } 41 | 42 | getChildren(element?: FeedTreeItem): Thenable { 43 | 44 | if (element) { 45 | if (element.type == 'root') { 46 | return Promise.resolve(FEED_TYPES.map(f => { 47 | return new FeedTreeItem(f.ch, f.type, f.type == 'feed' ? vscode.TreeItemCollapsibleState.Expanded : vscode.TreeItemCollapsibleState.Collapsed); 48 | })) 49 | } else if (element.type == 'feed') { 50 | return new Promise(async (resolve, reject) => { 51 | if (! await this.accountService.isAuthenticated()) { 52 | return resolve([new FeedTreeItem('(请先登录,查看个性内容)', '', vscode.TreeItemCollapsibleState.None)]); 53 | } 54 | let feedAPI = `${FeedStoryAPI}?page_number=${element.page}&limit=10&action=down`; 55 | let feedResp = await sendRequest( 56 | { 57 | uri: feedAPI, 58 | json: true, 59 | gzip: true 60 | }); 61 | feedResp = feedResp.data.filter(f => { return f.target.type != 'feed_advert'; }); 62 | let deps: FeedTreeItem[] = feedResp.map(feed => { 63 | let type = feed.target.type; 64 | if (type == MediaTypes.article) { 65 | return new FeedTreeItem(feed.target.title, feed.target.type, vscode.TreeItemCollapsibleState.None, { 66 | command: 'zhihu.openWebView', 67 | title: 'openWebView', 68 | arguments: [feed.target] 69 | }, feed.target); 70 | } else if (type == MediaTypes.answer) { 71 | return new FeedTreeItem(feed.target.question.title, feed.target.type, vscode.TreeItemCollapsibleState.None, { 72 | command: 'zhihu.openWebView', 73 | title: 'openWebView', 74 | arguments: [feed.target.question] 75 | }, feed.target); 76 | } else { 77 | return new FeedTreeItem('', '', vscode.TreeItemCollapsibleState.None); 78 | } 79 | }); 80 | resolve(deps); 81 | }) 82 | } else if (element.type == 'event') { 83 | let events = this.eventService.getEvents(); 84 | // this.eventService.setEvents(onChange(events, (path, value, previousValue) => { 85 | // this.refresh(element); 86 | // })); 87 | return Promise.resolve(this.eventService.getEvents().map(e => { 88 | return new EventTreeItem(e, vscode.TreeItemCollapsibleState.None, element); 89 | })) 90 | } 91 | } else { 92 | return Promise.resolve(this.getRootItem()); 93 | } 94 | 95 | } 96 | 97 | private async getRootItem(): Promise { 98 | await this.profileService.fetchProfile(); 99 | return Promise.resolve([ 100 | new FeedTreeItem(`${this.profileService.name} - ${this.profileService.headline}`, 'root', vscode.TreeItemCollapsibleState.Expanded, null, undefined, 0, this.profileService.avatarUrl) 101 | ]); 102 | } 103 | 104 | } 105 | 106 | export class FeedTreeItem extends LinkableTreeItem { 107 | 108 | /** 109 | * 110 | * @param label show in the tool bar 111 | * @param type used to classify items 112 | * @param collapsibleState if collapsible 113 | * @param command command to be executed if clicked 114 | * @param target stores the zhihu content object 115 | * @param page stores the page number 116 | * @param avatarUrl avatarUrl 117 | */ 118 | constructor( 119 | public readonly label: string, 120 | public type: string, 121 | public readonly collapsibleState: vscode.TreeItemCollapsibleState, 122 | public readonly command?: vscode.Command, 123 | public readonly target?: IQuestionAnswerTarget | IArticleTarget, 124 | public page?: number, 125 | public avatarUrl?: string 126 | ) { 127 | super(label, collapsibleState, target ? target.url : ''); 128 | this.tooltip = this.target ? this.target.excerpt : ''; 129 | this.description = this.target && this.target.excerpt ? this.target.excerpt : ''; 130 | } 131 | 132 | // get tooltip(): string | undefined { 133 | // return this.target ? this.target.excerpt : ''; 134 | // } 135 | 136 | // get description(): string { 137 | // return this.target && this.target.excerpt ? this.target.excerpt : ''; 138 | // } 139 | 140 | iconPath = this.avatarUrl ? vscode.Uri.parse(this.avatarUrl) : false; 141 | 142 | contextValue = (this.type == 'feed') ? 'feed' : 'dependency'; 143 | 144 | } 145 | 146 | export class EventTreeItem extends vscode.TreeItem { 147 | 148 | /** 149 | * 150 | * @param event the event 151 | * @param collapsibleState if collapsible 152 | */ 153 | constructor( 154 | public readonly event: IEvent, 155 | public readonly collapsibleState: vscode.TreeItemCollapsibleState, 156 | public readonly parent: vscode.TreeItem 157 | ) { 158 | super(removeSpace(removeHtmlTag(event.content)).slice(0, 12) + '...', collapsibleState); 159 | this.tooltip = removeHtmlTag(this.event.content); 160 | this.description = beautifyDate(this.event.date); 161 | } 162 | 163 | iconPath = false; 164 | 165 | contextValue = 'event'; 166 | 167 | } 168 | -------------------------------------------------------------------------------- /src/treeview/hotstory-treeview-provider.ts: -------------------------------------------------------------------------------- 1 | import * as httpClient from 'request'; 2 | import * as vscode from 'vscode'; 3 | import { HotStory } from '../model/hot-story.model'; 4 | import { IStoryTarget } from '../model/target/target'; 5 | import { HotStoryAPI } from '../const/URL'; 6 | 7 | export interface StoryType { 8 | storyType?: string; 9 | ch?: string; 10 | } 11 | 12 | export const STORY_TYPES = [ 13 | { storyType: 'total', ch: '全站' }, 14 | { storyType: 'sport', ch: '运动' }, 15 | { storyType: 'science', ch: '科学'}, 16 | { storyType: 'fashion', ch: '时尚'}, 17 | { storyType: 'film', ch: '影视'}, 18 | { storyType: 'digital', ch: '数码'} 19 | ]; 20 | 21 | export class HotStoryTreeViewProvider implements vscode.TreeDataProvider { 22 | 23 | private _onDidChangeTreeData: vscode.EventEmitter = new vscode.EventEmitter(); 24 | readonly onDidChangeTreeData: vscode.Event = this._onDidChangeTreeData.event; 25 | 26 | constructor() { 27 | } 28 | 29 | refresh(node?: ZhihuTreeItem): void { 30 | this._onDidChangeTreeData.fire(node); 31 | } 32 | 33 | getTreeItem(element: ZhihuTreeItem): vscode.TreeItem { 34 | return element; 35 | } 36 | 37 | getChildren(element?: ZhihuTreeItem): Thenable { 38 | 39 | if (element) { 40 | return new Promise(async (resolve, reject) => { 41 | let hotStoryAPI = `${HotStoryAPI}/${element.type}?desktop=true`; 42 | httpClient(hotStoryAPI, { json: true }, (err, _res, body) => { 43 | let questions: HotStory[] = body.data; 44 | let deps: ZhihuTreeItem[] = questions.map(story => { 45 | return new ZhihuTreeItem(story && story.target && story.target.title ? story.target.title : '', '', vscode.TreeItemCollapsibleState.None, 46 | { 47 | command: 'zhihu.openWebView', 48 | title: 'openWebView', 49 | arguments: [story.target] 50 | }, story.target); 51 | }); 52 | resolve(deps); 53 | }); 54 | }); 55 | } else { 56 | return Promise.resolve(this.getHotStoriesType()); 57 | } 58 | 59 | } 60 | 61 | private getHotStoriesType(): ZhihuTreeItem[] { 62 | return STORY_TYPES.map(type => { 63 | return new ZhihuTreeItem(type.ch, type.storyType, vscode.TreeItemCollapsibleState.Collapsed); 64 | }); 65 | } 66 | } 67 | 68 | export class LinkableTreeItem extends vscode.TreeItem { 69 | constructor( 70 | public readonly label: string, 71 | public collapsibleState: vscode.TreeItemCollapsibleState, 72 | public link: string | undefined 73 | ) { super(label, collapsibleState) } 74 | } 75 | 76 | export class ZhihuTreeItem extends LinkableTreeItem { 77 | 78 | constructor( 79 | public readonly label: string, 80 | public type: string, 81 | public readonly collapsibleState: vscode.TreeItemCollapsibleState, 82 | public readonly command?: vscode.Command, 83 | public target?: IStoryTarget, 84 | public page?: number, 85 | ) { 86 | super(label, collapsibleState, target && target.url ? target.url : ''); 87 | this.tooltip = this.target && this.target.excerpt ? this.target.excerpt : ''; 88 | this.description = this.target && this.target.excerpt ? this.target.excerpt : ''; 89 | } 90 | 91 | contextValue = (this.type == 'feed') ? 'feed' : 'dependency'; 92 | 93 | } 94 | -------------------------------------------------------------------------------- /src/util/md-html-utils.ts: -------------------------------------------------------------------------------- 1 | var UNESCAPE_MD_RE = /\\([!"#$%&'()*+,\-.\/:;<=>?@[\\\]^_`{|}~])/g; 2 | 3 | var HTML_ESCAPE_TEST_RE = /[&<>"]/; 4 | var HTML_ESCAPE_REPLACE_RE = /[&<>"]/g; 5 | var HTML_REPLACEMENTS = { 6 | '&': '&', 7 | '<': '<', 8 | '>': '>', 9 | '"': '"' 10 | }; 11 | 12 | function replaceUnsafeChar(ch) { 13 | return HTML_REPLACEMENTS[ch]; 14 | } 15 | 16 | function escapeHtml(str) { 17 | if (HTML_ESCAPE_TEST_RE.test(str)) { 18 | return str.replace(HTML_ESCAPE_REPLACE_RE, replaceUnsafeChar); 19 | } 20 | return str; 21 | } 22 | 23 | function unescapeMd(str) { 24 | if (str.indexOf('\\') < 0) { return str; } 25 | return str.replace(UNESCAPE_MD_RE, '$1'); 26 | } 27 | 28 | /** 29 | * Remove html tag from text 30 | */ 31 | function removeHtmlTag(text: string) { 32 | let TagRegExp = /<[^<>]+\/?>/g; 33 | return text.replace(TagRegExp, ''); 34 | } 35 | 36 | function removeSpace(text: string) { 37 | let SpaceReg = /\s+/g; 38 | return text.replace(SpaceReg, ' '); 39 | } 40 | 41 | /** 42 | * get human-readable time formate, like `5:24 pm` 43 | * @param hour the hour to be converted 44 | */ 45 | function beautifyDate(date: Date) { 46 | let hour = date.getHours(), minute = date.getMinutes(); 47 | let isAm = hour < 12; 48 | return `${isAm ? hour : hour - 12}:${minute < 10 ? '0' + minute : minute} ${isAm ? 'am' : 'pm'}` 49 | } 50 | 51 | export { escapeHtml, unescapeMd, removeHtmlTag, removeSpace, beautifyDate } 52 | -------------------------------------------------------------------------------- /test.md: -------------------------------------------------------------------------------- 1 | # Zhihu On VSCode 0.20 版本有哪些新功能? 2 | 3 | 不知初版 Zhihu On VSCode 插件使用体验如何?如果喜欢的话,记得去小岱的[项目仓库](https://github.com/niudai/Zhihu-VSCode)打颗 ⭐ 哦! 4 | 5 | 经过和开源社区伙伴的深入讨论,0.20 版本的 feature 如下: 6 | 7 | ### Webview 默认使用 VSCode 主题色 8 | 9 | 板块是透明的,会看起来像透明亚克力: 10 | 11 | ![Image](https://pic4.zhimg.com/80/v2-ab3797cbcb49de06937144f9fd4590cd_hd.png) 12 | 13 | >可以在 VSCode 的设置栏中找到 `Use VSTheme` 设置项,取消打勾后,会开启知乎默认的白蓝主题。 14 | 15 | ### 支持定时发布 16 | 17 | 所有的答案,文章发布时,均会多一次询问,用户须选择是稍后发布还是马上发布,如果选择稍后发布,需要输入发布的时间,比如 “5:30 pm”,"9:45 am" 等,目前仅支持当天的时间选择,输入后,你就会在个人中心的“安排”处看到你将发布的答案和发布的时间(需要手动点击刷新): 18 | 19 | ![Image](https://pic4.zhimg.com/80/v2-8a78e0643563309008f47a1510816ada_hd.png) 20 | 21 | 定时发布采用 prelog 技术,中途关闭 VSCode,关机不影响定时发布,只需保证发布时间 VSCode 处于打开状态 && 知乎插件激活状态即可。 22 | 23 | 时间到了之后,你会收到答案发布的通知,该事件也会从“安排”中移除。 24 | 25 | 如果想取消发布,则点击 ❌ 按钮即可: 26 | 27 | ![Image](https://pic4.zhimg.com/80/v2-f830eecfe7c3f8d6ba13e50d4c7e394a_hd.png) 28 | 29 | >发布事件采用 md5 完整性校验,不允许用户同时预发两篇内容一摸一样的答案或文章。 30 | 31 | ### 增加“分享”和“在浏览器打开”两个按钮 32 | 33 | 由于插件自身轻量的定位,Webview 的内容没有浏览器端更全面,而且为了保证大家可以更方便地将内容分享给其他人,增加了如下两个按钮: 34 | 35 | ![Image](https://pic4.zhimg.com/80/v2-2c4638805537065c52ee8b990790d699_hd.png) 36 | 37 | 点击左侧按钮会在浏览器中打开该页面,点击中间的会将页面的链接复制至粘贴板中。 38 | 39 | ## 其它优化 40 | 41 | 1. 取消了上传图片的默认文件夹。 42 | 2. 取消了宏刷新,点击相应的刷新按钮,只刷新当前的内容。 43 | 44 | >关于知乎插件的一些误解: 45 | 46 | 1. 只能用知乎插件发文章,不能发答案? 47 | 48 | ``` 49 | 错! 知乎插件既可以发答案也可以发文章! 只需按照 readme 里面的要求, 将答案或问题的链接放在顶部即可! 50 | ``` 51 | 52 | 2. 只有一种上传图片的方式? 53 | 54 | ``` 55 | 错! 知乎插件提供了多达三种图片上传方式, 分别是直接从粘贴板中获取图片上传, 一种是在左侧的 explorer 里面右击图片上传, 一种是在编辑页面右击点击 upload image! 每种方式都有其方便的地方, 创作者应该灵活运用。 56 | ``` 57 | 58 | -------------------------------------------------------------------------------- /test/fixtures/publishTest/test.js: -------------------------------------------------------------------------------- 1 | const MarkdownIt = require('markdown-it'); 2 | const markdown_it_zhihu = require('markdown-it-zhihu-common'); 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | 6 | const zhihuMdParser = new MarkdownIt({ html: true }).use(markdown_it_zhihu); 7 | 8 | let MdStr = fs.readFileSync(path.join(__dirname, 'test.md'), 'utf8'); 9 | 10 | console.log(zhihuMdParser.render(MdStr)); 11 | 12 | fs.writeFileSync(path.join(__dirname, 'test_assert.html'), zhihuMdParser.render(MdStr)) -------------------------------------------------------------------------------- /test/fixtures/publishTest/test.md: -------------------------------------------------------------------------------- 1 | ## 文章 2 | 3 | ![Image](https://pic4.zhimg.com/80/v2-00de3f8c56765b4b68ed43fe8513a8c4.png) 4 | 5 | ```java 6 | 7 | public class Apple { 8 | hello(); 9 | } 10 | ``` 11 | 12 | $$ 13 | \sqrt5\sqrt7 14 | $$ 15 | 16 | 行内$Latex$ 17 | -------------------------------------------------------------------------------- /test/fixtures/publishTest/test_assert.html: -------------------------------------------------------------------------------- 1 |

文章

2 | public class Apple {
3 |     hello();
4 | }
5 | 

\sqrt5\sqrt7

行内Latex

-------------------------------------------------------------------------------- /test/runTest.ts: -------------------------------------------------------------------------------- 1 | import * as path from "path"; 2 | 3 | import { runTests } from "vscode-test"; 4 | 5 | /** 6 | * run test 7 | */ 8 | async function main() { 9 | try { 10 | // The folder containing the Extension Manifest package.json 11 | // Passed to `--extensionDevelopmentPath` 12 | const extensionDevelopmentPath: string = path.resolve( __dirname, "../../" ); 13 | 14 | // The path to the extension test script 15 | // Passed to --extensionTestsPath 16 | const extensionTestsPath: string = path.resolve( __dirname, "./suite/index" ); 17 | 18 | // Download VS Code, unzip it and run the integration test 19 | await runTests( { extensionDevelopmentPath, extensionTestsPath } ); 20 | } catch ( err ) { 21 | process.exit(1); 22 | } 23 | } 24 | 25 | main(); 26 | -------------------------------------------------------------------------------- /test/suite/extension.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | // You can import and use all API from the 'vscode' module 3 | // as well as import your extension to test it 4 | import * as vscode from 'vscode'; 5 | 6 | // import * as myExtension from '../../extension'; 7 | 8 | 9 | suite('Extension Test Suite', async () => { 10 | vscode.window.showInformationMessage('Start all tests.'); 11 | 12 | // beans mock initialization 13 | // let context: vscode.ExtensionContext = { 14 | // extensionPath: path.join(__dirname, '../../../'), 15 | // globalState: { 16 | // get() {}, 17 | // update(key: string, v: string): Promise { return Promise.resolve()} 18 | // }, 19 | // logPath: '', 20 | // storagePath: '', 21 | // asAbsolutePath(str) { return ''}, 22 | // globalStoragePath: '', 23 | // subscriptions: [{dispose() {}} ], 24 | // workspaceState: undefined 25 | // }; 26 | // if(!fs.existsSync(path.join(context.extensionPath, './cookie.json'))) { 27 | // fs.createWriteStream(path.join(context.extensionPath, './cookie.json')).end() 28 | // } 29 | // Dependency Injection 30 | 31 | test('Sample test', () => { 32 | assert.equal([1, 2, 3].indexOf(5), -1); 33 | assert.equal([1, 2, 3].indexOf(0), -1); 34 | }); 35 | 36 | test('') 37 | }); 38 | -------------------------------------------------------------------------------- /test/suite/global.test.ts: -------------------------------------------------------------------------------- 1 | import { join } from "path"; 2 | 3 | export const fixturePath = join(__dirname, '../../../test/fixtures') -------------------------------------------------------------------------------- /test/suite/index.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import * as Mocha from 'mocha'; 3 | import * as glob from 'glob'; 4 | 5 | export function run(): Promise { 6 | // Create the mocha test 7 | const mocha = new Mocha({ 8 | ui: 'tdd' 9 | }); 10 | mocha.useColors(true); 11 | 12 | const testsRoot = path.resolve(__dirname, '..'); 13 | 14 | return new Promise((c, e) => { 15 | glob('**/**.test.js', { cwd: testsRoot }, (err, files) => { 16 | if (err) { 17 | return e(err); 18 | } 19 | 20 | // Add files to the test suite 21 | files.forEach(f => mocha.addFile(path.resolve(testsRoot, f))); 22 | 23 | try { 24 | // Run the mocha test 25 | mocha.run(failures => { 26 | if (failures > 0) { 27 | e(new Error(`${failures} tests failed.`)); 28 | } else { 29 | c(); 30 | } 31 | }); 32 | } catch (err) { 33 | console.error(err); 34 | e(err); 35 | } 36 | }); 37 | }); 38 | } 39 | -------------------------------------------------------------------------------- /test/suite/mdparser.service.test.ts: -------------------------------------------------------------------------------- 1 | import MarkdownIt = require("markdown-it"); 2 | import * as assert from 'assert'; 3 | import markdown_it_zhihu from "markdown-it-zhihu-common"; 4 | // You can import and use all API from the 'vscode' module 5 | // as well as import your extension to test it 6 | import * as vscode from 'vscode'; 7 | 8 | const testMdFile = ` 9 | \`\`\`java 10 | 11 | public class Apple { 12 | hello(); 13 | } 14 | \`\`\` 15 | ` 16 | 17 | const testHtml = `
18 | public class Apple {
19 |     hello();
20 | }
21 | 
` 22 | 23 | 24 | 25 | import md5 = require('md5'); 26 | import { readFile, readFileSync } from "fs"; 27 | import { fixturePath } from "./global.test"; 28 | import { join } from "path"; 29 | 30 | const samplesPath = join(fixturePath, 'publishTest'); 31 | // import * as myExtension from '../../extension'; 32 | 33 | suite('Markdown Parser Test', async () => { 34 | vscode.window.showInformationMessage('Start all tests.'); 35 | const zhihuMdParser = new MarkdownIt({ html: true }).use(markdown_it_zhihu); 36 | // Dependency Injection 37 | 38 | test('parse test', () => { 39 | let testMd = readFileSync(join(samplesPath, 'test.md'), 'utf8'); 40 | let assertHtml = readFileSync(join(samplesPath, 'test_assert.html'), 'utf8'); 41 | assert.equal(zhihuMdParser.render(testMd, {}), assertHtml); 42 | }); 43 | 44 | }); -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es6", 5 | "outDir": "out", 6 | "sourceMap": true, 7 | // "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 8 | // "strict": true /* enable all strict type-checking options */ 9 | }, 10 | "include": [ 11 | "test/**/*", 12 | "src/**/*" 13 | ], 14 | "exclude": [ 15 | "node_modules", 16 | ".vscode-test", 17 | ] 18 | } -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tslint:recommended" 3 | } -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | //@ts-check 2 | 3 | 'use strict'; 4 | 5 | const path = require('path'); 6 | 7 | /**@type {import('webpack').Configuration}*/ 8 | const config = { 9 | target: 'node', // vscode extensions run in a Node.js-context 📖 -> https://webpack.js.org/configuration/node/ 10 | 11 | entry: './src/extension.ts', // the entry point of this extension, 📖 -> https://webpack.js.org/configuration/entry-context/ 12 | output: { 13 | 14 | 15 | // the bundle is stored in the 'dist' folder (check package.json), 📖 -> https://webpack.js.org/configuration/output/ 16 | path: path.resolve(__dirname, 'dist'), 17 | filename: 'extension.js', 18 | libraryTarget: 'commonjs2', 19 | devtoolModuleFilenameTemplate: '../[resource-path]' 20 | }, 21 | devtool: 'source-map', 22 | externals: [{ 23 | vscode: 'commonjs vscode', // the vscode-module is created on-the-fly and must be excluded. Add other modules that cannot be webpack'ed, 📖 -> https://webpack.js.org/configuration/externals/ 24 | }, 25 | // { 26 | // 'uglify-js': 'uglify-js' 27 | 28 | // } 29 | ], 30 | resolve: { 31 | // support reading TypeScript and JavaScript files, 📖 -> https://github.com/TypeStrong/ts-loader 32 | extensions: ['.ts', '.js'], 33 | }, 34 | module: { 35 | rules: [ 36 | { 37 | test: /\.test\.ts$/, 38 | exclude: /node_modules/, 39 | use: [ 40 | { loader: 'ignore-loader'} 41 | ] 42 | }, 43 | { 44 | test: /\.ts$/, 45 | exclude: /node_modules/, 46 | use: [ 47 | { 48 | loader: 'ts-loader' 49 | } 50 | ] 51 | } 52 | ] 53 | } 54 | 55 | }; 56 | module.exports = config; -------------------------------------------------------------------------------- /zhihu-reverse.md: -------------------------------------------------------------------------------- 1 | # 知乎格式逆向 2 | 3 | # 表格 4 | 5 | ```html 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
头左头右
数据左数据右
18 | ``` 19 | 20 | # 链接卡片 21 | 22 | ```html 23 | jks-liu - Overview 32 | ``` 33 | 34 | ```html 35 | Jks Liu:VS Code插件WPL/s介绍及测试 44 | ``` 45 | 46 | # 标签 47 | 48 | DELETE https://zhuanlan.zhihu.com/api/articles/101553734/topics/20053651 49 | 50 | POST https://zhuanlan.zhihu.com/api/articles/101553734/topics 51 | 52 | ```json 53 | { 54 | "introduction": "", 55 | "avatarUrl": "https://pic2.zhimg.com/80/da8e974dc_l.jpg?source=4e949a73", 56 | "name": "Zhihu学佛 话题", 57 | "url": "https://www.zhihu.com/topic/21672344", 58 | "type": "topic", 59 | "excerpt": "", 60 | "id": "21672344" 61 | } 62 | ``` 63 | 64 | # 参考文献 65 | 66 | ```html 67 |

68 | [1] 76 |

77 |

78 | [1] 86 |

87 |

88 | [2] 96 |

97 |


98 | ``` 99 | 100 | content:

Markdown 测试专用

#! https://zhuanlan.zhihu.com/p/101553734

测试 4

ab

101 | 102 | # 标签 103 | 104 | 首先 GET `https://zhuanlan.zhihu.com/api/articles/101553734/draft` 获得 105 | 106 | ```json 107 | { 108 | "image_url": "https://pic4.zhimg.com/v2-c6dfa5adc2f6980e4382114c60236710_b.jpg", 109 | "updated": 1634565226, 110 | "copyright_permission": "need_review", 111 | "reviewers": [], 112 | "topics": [ 113 | { 114 | "url": "https://www.zhihu.com/api/v4/topics/21504097", 115 | "type": "topic", 116 | "id": "21504097", 117 | "name": "\u6492\u53d1\u751f\u7684\u7b97\u6cd5" 118 | }, 119 | { 120 | "url": "https://www.zhihu.com/api/v4/topics/19586973", 121 | "type": "topic", 122 | "id": "19586973", 123 | "name": "4A \u5e7f\u544a\u516c\u53f8" 124 | } 125 | ], 126 | "excerpt": "", 127 | "article_type": "normal", 128 | "excerpt_title": "", 129 | "summary": "\u672c\u6587\u4e13\u6587\u7528\u6765\u6d4b\u8bd5\u77e5\u4e4e\u7684\u5404\u79cd\u529f\u80fd\u3002", 130 | "title_image_size": { "width": 0, "height": 0 }, 131 | "id": 101553734, 132 | "author": { 133 | "is_followed": false, 134 | "avatar_url_template": "https://pic2.zhimg.com/6d957ba5a_{size}.jpg", 135 | "uid": "30962201133056", 136 | "user_type": "people", 137 | "is_following": false, 138 | "url_token": "jks-liu", 139 | "id": "70179d5c52a3edbaa459e10e28c73748", 140 | "description": "", 141 | "name": "Jks Liu", 142 | "is_advertiser": false, 143 | "headline": "\u8bf7\u52ff\u9080\u8bf7\u6211\u56de\u7b54\u95ee\u9898", 144 | "gender": 0, 145 | "url": "/people/70179d5c52a3edbaa459e10e28c73748", 146 | "avatar_url": "https://pic2.zhimg.com/6d957ba5a_l.jpg", 147 | "is_org": false, 148 | "type": "people" 149 | }, 150 | "url": "https://zhuanlan.zhihu.com/p/101553734", 151 | "comment_permission": "all", 152 | "settings": { 153 | "commercial_report_info": { "is_report": false, "commercial_types": [] }, 154 | "table_of_contents": { "enabled": false } 155 | }, 156 | "created": 1578406772, 157 | "content": "

\u672c\u6587\u4e13\u6587\u7528\u6765\u6d4b\u8bd5\u77e5\u4e4e\u7684\u5404\u79cd\u529f\u80fd\u3002

", 158 | "has_publishing_draft": false, 159 | "state": "published", 160 | "is_title_image_full_screen": false, 161 | "title": "\u6d4b\u8bd5\u4e13\u7528", 162 | "title_image": "https://pic4.zhimg.com/v2-c6dfa5adc2f6980e4382114c60236710_b.jpg", 163 | "type": "article_draft" 164 | } 165 | ``` 166 | 167 | get https://zhuanlan.zhihu.com/api/autocomplete/topics?token=a&max_matches=5&use_similar=0&topic_filter=1 168 | 169 | ```json 170 | [ 171 | { 172 | "introduction": "", 173 | "avatar_url": "https://pica.zhimg.com/80/c02c1ee9f_l.jpg?source=4e949a73", 174 | "name": "4A \u5e7f\u544a\u516c\u53f8", 175 | "url": "https://www.zhihu.com/topic/19586973", 176 | "type": "topic", 177 | "excerpt": "", 178 | "id": "19586973" 179 | }, 180 | { 181 | "introduction": "", 182 | "avatar_url": "https://pic1.zhimg.com/80/281aa82e7b9bf232dfbf1b3a9cf6d909_l.jpg?source=4e949a73", 183 | "name": "A \u80a1\u5927\u8dcc", 184 | "url": "https://www.zhihu.com/topic/20013362", 185 | "type": "topic", 186 | "excerpt": "", 187 | "id": "20013362" 188 | }, 189 | { 190 | "introduction": "", 191 | "avatar_url": "https://pica.zhimg.com/80/v2-349955d95b18302d02a48c590955b61c_l.jpg?source=4e949a73", 192 | "name": "Sony A7", 193 | "url": "https://www.zhihu.com/topic/20014872", 194 | "type": "topic", 195 | "excerpt": "", 196 | "id": "20014872" 197 | }, 198 | { 199 | "introduction": "", 200 | "avatar_url": "https://pic3.zhimg.com/80/v2-fa472d5ad9a7df0e6f5ac737c14f32ce_l.jpg?source=4e949a73", 201 | "name": "\u5965\u8feaA3", 202 | "url": "https://www.zhihu.com/topic/20008717", 203 | "type": "topic", 204 | "excerpt": "", 205 | "id": "20008717" 206 | } 207 | ] 208 | ``` 209 | 210 | 添加标签 POST `https://zhuanlan.zhihu.com/api/articles/101553734/topics` 211 | 212 | ```json 213 | { 214 | "introduction": "", 215 | "avatarUrl": "https://pic3.zhimg.com/80/281aa82e7b9bf232dfbf1b3a9cf6d909_l.jpg?source=4e949a73", 216 | "name": "A 股大跌", 217 | "url": "https://www.zhihu.com/topic/20013362", 218 | "type": "topic", 219 | "excerpt": "", 220 | "id": "20013362" 221 | } 222 | ``` 223 | 224 | 删除标签 DELETE `https://zhuanlan.zhihu.com/api/articles/101553734/topics/20013362` 225 | --------------------------------------------------------------------------------