我看了一下C11标准,这段代码应该不是UB,所以我倾向于这是GCC的一个bug。并且我用最新的GCC11试了上面的例子,也无法复现,说明这个bug很大可能已经被修复了。
需要做到上面两点,主要依赖了这样一个事实:C++ 20的协程可以在一个线程中暂停,然后在另一个线程中恢复执行。跨线程的协程暂停/恢复是很多语言/协程库所不具备的。
, \
等合法标签才会被服务端存储,具体使用时小伙伴们可以自己尝试。
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 | 
226 |
227 | 定时发布采用 prelog 技术,中途关闭 VSCode,关机不影响定时发布,只需保证发布时间 VSCode 处于打开状态 && 知乎插件激活状态即可。
228 |
229 | 时间到了之后,你会收到答案发布的通知,该事件也会从“安排”中移除。
230 |
231 | 如果想取消发布,则点击 ❌ 按钮即可:
232 |
233 | 
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 | 
12 |
13 | >可以在 VSCode 的设置栏中找到 `Use VSTheme` 设置项,取消打勾后,会开启知乎默认的白蓝主题。
14 |
15 | ### 支持定时发布
16 |
17 | 所有的答案,文章发布时,均会多一次询问,用户须选择是稍后发布还是马上发布,如果选择稍后发布,需要输入发布的时间,比如 “5:30 pm”,"9:45 am" 等,目前仅支持当天的时间选择,输入后,你就会在个人中心的“安排”处看到你将发布的答案和发布的时间(需要手动点击刷新):
18 |
19 | 
20 |
21 | 定时发布采用 prelog 技术,中途关闭 VSCode,关机不影响定时发布,只需保证发布时间 VSCode 处于打开状态 && 知乎插件激活状态即可。
22 |
23 | 时间到了之后,你会收到答案发布的通知,该事件也会从“安排”中移除。
24 |
25 | 如果想取消发布,则点击 ❌ 按钮即可:
26 |
27 | 
28 |
29 | >发布事件采用 md5 完整性校验,不允许用户同时预发两篇内容一摸一样的答案或文章。
30 |
31 | ### 增加“分享”和“在浏览器打开”两个按钮
32 |
33 | 由于插件自身轻量的定位,Webview 的内容没有浏览器端更全面,而且为了保证大家可以更方便地将内容分享给其他人,增加了如下两个按钮:
34 |
35 | 
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 | 
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 | 
14 |
15 | 点击按钮点赞。
16 |
17 | ### 显示作者头像,名字,个性签名
18 |
19 | 
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 | 
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 | 
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 | 
17 |
18 | ### 文章标题智能识别
19 |
20 | 文章标题无需手动输入,插件会自动检测文本的第一个一级头标签:
21 |
22 | ```
23 | # 这是一个标题(必须只是一个#)
24 | ```
25 |
26 | 然后将其作为标题,改行的内容也不会进入到正文中,如果没有检测到,还需用户手动输入。
27 |
28 | ### 背景图片智能识别
29 |
30 | 插件会自动扫描文本第一个一级头标签之前的内容,将第一个发现的图片链接作为背景图片:
31 |
32 | ```
33 | 
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 | 
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 | 
24 |
25 | 源 Markdown 中的所有图片都是相对路径或来自外链。
26 |
27 | 发布后的效果:
28 |
29 | 
30 |
31 | **It just works.**
32 |
33 | 所以随着这个功能的实现,知乎插件在内容创作上也真正实现了 “非侵入式”,用户本来怎么写,就怎么写,预览什么样,发布就什么样。
34 |
35 | 好的产品就应该尽可能复杂的东西隐藏起来,把简洁易用的接口让用户使用,如果用户觉得这个插件很复杂,那这个插件就是一个失败之作。
36 |
37 | >该功能刚上线,可能会带来更多的问题,欢迎用户们到小岱的代码仓库下用issue告诉插件存在的问题。
38 |
39 | ### 解决的 Issue:
40 |
41 | 1. 修复了代码块缩成一行的 Bug。
42 |
43 | 2. 文章发布过程中有发布提示:
44 |
45 | 
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 | 
4 |
5 | # Zhihu On VSCode 0.3 更新
6 |
7 | 经过和开源社区伙伴的深入讨论,0.3 版本的 feature 如下:
8 |
9 | 
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 | 
3 |
4 | # 在 VSCode 里用微信登录知乎是什么体验
5 |
6 | ### *Zhihu On VSCode* 现在支持微信登录啦!
7 |
8 | 在最新版 *Zhihu On VSCode 0.4* 中,知乎er 可以选择使用微信 APP 扫码登录,只需保证知乎账号和微信账号绑定,即可打开微信 APP 扫一扫:
9 |
10 | 
11 |
12 | > *Zhihu On VSCode* 为什么不支持账号密码登录了?
13 |
14 | 答:
15 |
16 | 因为账号密码登录安全性不好,现在已经支持了知乎 APP 和 微信 APP 的扫码登录,已经足够方便。
17 |
18 | ### 在 VSCode 里面 @ 你想 @ 的 Zhihuer!
19 |
20 | 该版本中,用户可以直接在 Markdown 文本里面 @ 知乎er 啦,只需要输入 @,弹出提示,点击回车:
21 |
22 | 
23 |
24 | 
25 |
26 | 
27 |
28 | 选择后,会自动生成 @ 链接,无需手动管理。
29 |
30 | ### 可以使用链接卡片啦!
31 |
32 | 知乎的链接可以变成卡片,只需在链接后面加入 *"card"* 即可:
33 |
34 | 
35 |
36 | 发布后:
37 |
38 | 
39 |
40 | ### 可以添加图片描述啦!
41 |
42 | 
43 |
44 | 发布后:
45 |
46 | 
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, ``)
198 | } else {
199 | e.insert(current.start, ``);
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 | 
12 |
13 | >可以在 VSCode 的设置栏中找到 `Use VSTheme` 设置项,取消打勾后,会开启知乎默认的白蓝主题。
14 |
15 | ### 支持定时发布
16 |
17 | 所有的答案,文章发布时,均会多一次询问,用户须选择是稍后发布还是马上发布,如果选择稍后发布,需要输入发布的时间,比如 “5:30 pm”,"9:45 am" 等,目前仅支持当天的时间选择,输入后,你就会在个人中心的“安排”处看到你将发布的答案和发布的时间(需要手动点击刷新):
18 |
19 | 
20 |
21 | 定时发布采用 prelog 技术,中途关闭 VSCode,关机不影响定时发布,只需保证发布时间 VSCode 处于打开状态 && 知乎插件激活状态即可。
22 |
23 | 时间到了之后,你会收到答案发布的通知,该事件也会从“安排”中移除。
24 |
25 | 如果想取消发布,则点击 ❌ 按钮即可:
26 |
27 | 
28 |
29 | >发布事件采用 md5 完整性校验,不允许用户同时预发两篇内容一摸一样的答案或文章。
30 |
31 | ### 增加“分享”和“在浏览器打开”两个按钮
32 |
33 | 由于插件自身轻量的定位,Webview 的内容没有浏览器端更全面,而且为了保证大家可以更方便地将内容分享给其他人,增加了如下两个按钮:
34 |
35 | 
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 | 
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 |
行内
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------