-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 9b20849
Showing
20 changed files
with
1,331 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
{ | ||
"parser": "@typescript-eslint/parser", | ||
"parserOptions": { | ||
"sourceType": "module" | ||
}, | ||
"rules": { | ||
// tab缩进 | ||
"indent": ["error", "tab"], | ||
// 使用双引号 | ||
"quotes": ["error", "double"] | ||
}, | ||
"extends": [ | ||
// "eslint:recommended" | ||
], | ||
"ignorePatterns": [ | ||
"node_modules/", | ||
"dist/", | ||
"build/" | ||
] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
node_modules/ | ||
dist/ | ||
|
||
pnpm-lock.yaml |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
# 知乎下载器 | ||
|
||
一键复制知乎文章/回答为Markdown,下载文章/回答为zip(包含素材图片与文章/回答信息),备份你珍贵的回答与文章。 | ||
|
||
代码仓库:<https://github.com/Howardzhangdqs/zhihu-copy-as-markdown> | ||
|
||
## Usage | ||
|
||
1. 安装依赖 | ||
|
||
```bash | ||
pnpm i | ||
``` | ||
|
||
2. 测试 | ||
|
||
```bash | ||
pnpm dev | ||
``` | ||
|
||
3. 打包 | ||
|
||
```bash | ||
pnpm build | ||
``` | ||
|
||
`dist/tampermonkey-script.js` 即为脚本,复制到油猴即可使用。 | ||
|
||
|
||
## 原理 | ||
|
||
1. 获取页面中所有的富文本框 `DOM` | ||
2. 将 `DOM` 使用 `./src/lexer.ts` 转换为 `Lex` | ||
3. 将 `Lex` 使用 `./src/parser.ts` 转换为 `Markdown` | ||
|
||
|
||
## TODO | ||
|
||
- [ ] 下载文章时同时包含头图 | ||
- [ ] TOC解析 | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
import fs from "fs"; | ||
import md5 from "md5"; | ||
|
||
const packageInfo = JSON.parse(fs.readFileSync("./package.json", "utf-8").toString()); | ||
|
||
export const UserScriptContent = fs | ||
.readFileSync("./dist/bundle.min.js", "utf-8") | ||
.toString() | ||
.replace(/\/\*[\s\S]*?\*\/|([^\\:]|^)\/\/.*$/gm, "").trim(); | ||
|
||
export const UserScript = { | ||
"name" : "知乎下载器", | ||
"namespace" : "http://howardzhangdqs.eu.org/", | ||
"source" : "https://github.com/Howardzhangdqs/zhihu-copy-as-markdown", | ||
"version" : packageInfo.version + "-" + md5(UserScriptContent).slice(0, 6), | ||
"description": "一键复制知乎文章/回答为Markdown,下载文章/回答为zip(包含素材图片与文章/回答信息),备份你珍贵的回答与文章。", | ||
"author" : packageInfo.author, | ||
"match" : [ | ||
"*:\/\/www.zhihu.com\/*", | ||
"*:\/\/zhuanlan.zhihu.com\/*" | ||
], | ||
"license": packageInfo.license, | ||
"icon" : "https://static.zhihu.com/heifetz/favicon.ico", | ||
"grant" : "none", | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
23.10.30: 脚本开写 | ||
23.10.31: | ||
feat: 解析渲染表格 | ||
feat: 解析渲染链接 | ||
fix: 加了一个被忘掉的break,但是我忘了是哪忘了加了 | ||
fix: 修复编辑框会被加上`复制为Markdown`的按钮 | ||
doc: 给types加了完整的注释 | ||
doc: 给Lexer和Parser函数添加完整的注释 | ||
chore: 更改触发方式 | ||
feat: 链接解析为直达链接 | ||
feat: 下载内容为zip | ||
feat: 下载的zip中包含内容信息(`info.txt`) | ||
fix: 修复回答详情里图片无法下载 | ||
fix: 首页按钮无法正常加载 | ||
23.11.1: | ||
feat: `info.txt`加入作者信息 | ||
chore: `info.txt`更名为`info.json` | ||
fix: 主页中`info.json > url`字段获取错误 | ||
chore: `info.json`中`url`字段改为`link` | ||
fix: 主页文章作者信息获取错误 | ||
chore: 发布在Github上 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
{ | ||
"name": "zhihu-downloader", | ||
"version": "0.2.10", | ||
"scripts": { | ||
"dev": "run-p watch serve", | ||
"serve": "live-server ./dist", | ||
"watch": "webpack --mode development --watch", | ||
"build:production": "webpack --mode production", | ||
"build:tampermonkey": "node ./scripts/build-tampermonkey.js", | ||
"build": "run-s build:update build:production build:tampermonkey", | ||
"build:update": "node ./scripts/add-version.js", | ||
"lint": "eslint --fix --ext .js,.ts ./src ./scripts" | ||
}, | ||
"devDependencies": { | ||
"html-webpack-plugin": "^5.5.3", | ||
"ts-loader": "^9.4.4", | ||
"typescript": "^5.2.2", | ||
"webpack": "^5.88.2", | ||
"webpack-cli": "^5.1.4" | ||
}, | ||
"dependencies": { | ||
"@babel/core": "^7.23.0", | ||
"@babel/preset-env": "^7.22.20", | ||
"@types/file-saver": "^2.0.6", | ||
"@types/md5": "^2.3.4", | ||
"@types/node": "^20.7.1", | ||
"@typescript-eslint/parser": "^6.9.1", | ||
"babel-loader": "^9.1.3", | ||
"eslint": "^8.52.0", | ||
"file-saver": "^2.0.5", | ||
"jszip": "^3.10.1", | ||
"live-server": "^1.2.2", | ||
"md5": "^2.3.0", | ||
"npm-run-all": "^4.1.5", | ||
"uglifyjs-webpack-plugin": "^2.2.0" | ||
}, | ||
"type": "module", | ||
"author": "HowardZhangdqs", | ||
"license": "MIT" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
import fs from "fs"; | ||
|
||
const packageJson = JSON.parse(fs.readFileSync("./package.json")); | ||
const version = packageJson.version.split(".").map((val) => parseInt(val)); | ||
|
||
version[version.length - 1] += 1; | ||
|
||
packageJson.version = version.join("."); | ||
|
||
fs.writeFileSync("./package.json", JSON.stringify(packageJson, null, 2)); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
import fs from "fs"; | ||
import { UserScript, UserScriptContent } from "../TampermonkeyConfig.js"; | ||
|
||
|
||
// Padding 长度 | ||
const paddingLength = Object.entries(UserScript).reduce((maxLength, [key]) => { | ||
return Math.max(maxLength, key.length); | ||
}, 0) + 1; | ||
|
||
// Tampermonkey UserScript Config | ||
const TampermonkeyConfig = Object.entries(UserScript).map(([key, value]) => { | ||
if (!value) return; | ||
|
||
if (typeof value == "object") | ||
return Object.entries(value).map(([_key, value]) => { | ||
return `// @${key.padEnd(paddingLength, " ")} ${value}`; | ||
}).join("\n"); | ||
|
||
return `// @${key.padEnd(paddingLength, " ")} ${value}`; | ||
|
||
}).filter((val) => val).join("\n"); | ||
|
||
// 更新日志 | ||
const UpdateLog = fs.readFileSync("./UpdateLog.txt", "utf-8").toString().split("\n").map((line) => { | ||
return ` * ${line}`; | ||
}).join("\n"); | ||
|
||
|
||
fs.writeFileSync("./dist/tampermonkey-script.js", `// ==UserScript== | ||
${TampermonkeyConfig} | ||
// ==/UserScript== | ||
/** 更新日志 | ||
${UpdateLog} | ||
*/ | ||
${UserScriptContent}`, "utf-8"); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
import * as JSZip from "jszip"; | ||
|
||
/** | ||
* 下载文件并将其添加到zip文件中 | ||
* @param url 下载文件的URL | ||
* @param zip JSZip对象,用于创建zip文件 | ||
* @returns 添加了下载文件的zip文件 | ||
*/ | ||
export async function downloadAndZip(url: string, zip: JSZip): Promise<{ zip: JSZip, file_name: string }> { | ||
|
||
const response = await fetch(url); | ||
const arrayBuffer = await response.arrayBuffer(); | ||
const fileName = url.replace(/\?.*?$/g, "").split("/").pop(); | ||
|
||
// 添加到zip文件 | ||
zip.file(fileName, arrayBuffer); | ||
return { zip, file_name: fileName }; | ||
} | ||
|
||
/** | ||
* 下载一系列文件并将其添加到zip文件中 | ||
* @param urls 下载文件的URL | ||
* @param zip JSZip对象,用于创建zip文件 | ||
* @returns 添加了下载文件的zip文件 | ||
*/ | ||
export async function downloadAndZipAll(urls: string[], zip: JSZip): Promise<JSZip> { | ||
for (let url of urls) zip = (await downloadAndZip(url, zip)).zip; | ||
return zip; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
import "./index"; |
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,130 @@ | ||
import { lexer } from "./lexer"; | ||
import { parser } from "./parser"; | ||
import saveLex from "./savelex"; | ||
import { saveAs } from "file-saver"; | ||
import { MakeButton, getAuthor, getParent, getTitle, getURL } from "./utils"; | ||
|
||
const main = async () => { | ||
|
||
console.log("Starting…"); | ||
|
||
const RichTexts = Array.from(document.querySelectorAll(".RichText")) as HTMLElement[]; | ||
|
||
for (let RichText of RichTexts) { | ||
|
||
try { | ||
|
||
if (RichText.parentElement.classList.contains("Editable")) continue; | ||
|
||
if (RichText.children[0].classList.contains("zhihucopier-button")) continue; | ||
|
||
console.log(RichText); | ||
|
||
const lex = lexer(RichText.childNodes as NodeListOf<Element>); | ||
const markdown = parser(lex); | ||
|
||
const title = getTitle(RichText), author = getAuthor(RichText); | ||
const url = getURL(RichText); | ||
|
||
console.log("good", lex, markdown, title, author); | ||
|
||
const ButtonZipDownload = MakeButton(); | ||
ButtonZipDownload.innerHTML = "下载全文为Zip"; | ||
ButtonZipDownload.style.borderRadius = "0 1em 1em 0"; | ||
ButtonZipDownload.style.width = "100px"; | ||
ButtonZipDownload.style.paddingRight = ".4em"; | ||
|
||
RichText.prepend(ButtonZipDownload); | ||
|
||
ButtonZipDownload.addEventListener("click", async () => { | ||
try { | ||
const zopQuestion = (() => { | ||
const element = document.querySelector("[data-zop-question]"); | ||
try { | ||
if (element instanceof HTMLElement) | ||
return JSON.parse(decodeURIComponent(element.getAttribute("data-zop-question"))); | ||
} catch { } | ||
return null; | ||
})(); | ||
|
||
const zop = (() => { | ||
let element = getParent(RichText, "AnswerItem"); | ||
if (! element) element = getParent(RichText, "Post-content"); | ||
|
||
try { | ||
if (element instanceof HTMLElement) | ||
return JSON.parse(decodeURIComponent(element.getAttribute("data-zop"))); | ||
} catch { } | ||
|
||
return null; | ||
})(); | ||
|
||
const zaExtra = (() => { | ||
const element = document.querySelector("[data-za-extra-module]"); | ||
try { | ||
if (element instanceof HTMLElement) | ||
return JSON.parse(decodeURIComponent(element.getAttribute("data-za-extra-module"))); | ||
} catch { } | ||
return null; | ||
})(); | ||
|
||
const zip = await saveLex(lex); | ||
zip.file("info.json", JSON.stringify({ | ||
title, url, author, | ||
zop, | ||
"zop-question": zopQuestion, | ||
"zop-extra-module": zaExtra, | ||
}, null, 4)); | ||
|
||
console.log(zip); | ||
const blob = await zip.generateAsync({ type: "blob" }); | ||
saveAs(blob, title + ".zip"); | ||
|
||
ButtonZipDownload.innerHTML = "下载成功✅"; | ||
setTimeout(() => { | ||
ButtonZipDownload.innerHTML = "下载全文为Zip"; | ||
}, 1000); | ||
} catch { | ||
ButtonZipDownload.innerHTML = "发生未知错误<br>请联系开发者"; | ||
ButtonZipDownload.style.height = "4em"; | ||
setTimeout(() => { | ||
ButtonZipDownload.style.height = "2em"; | ||
ButtonZipDownload.innerHTML = "下载全文为Zip"; | ||
}, 1000); | ||
} | ||
}); | ||
|
||
const ButtonCopyMarkdown = MakeButton(); | ||
ButtonCopyMarkdown.innerHTML = "复制为Markdown"; | ||
ButtonCopyMarkdown.style.borderRadius = "1em 0 0 1em"; | ||
ButtonCopyMarkdown.style.paddingLeft = ".4em"; | ||
RichText.prepend(ButtonCopyMarkdown); | ||
|
||
ButtonCopyMarkdown.addEventListener("click", () => { | ||
try { | ||
navigator.clipboard.writeText(markdown.join("\n\n")); | ||
ButtonCopyMarkdown.innerHTML = "复制成功✅"; | ||
setTimeout(() => { | ||
ButtonCopyMarkdown.innerHTML = "复制为Markdown"; | ||
}, 1000); | ||
} catch { | ||
ButtonCopyMarkdown.innerHTML = "发生未知错误<br>请联系开发者"; | ||
ButtonCopyMarkdown.style.height = "4em"; | ||
setTimeout(() => { | ||
ButtonCopyMarkdown.style.height = "2em"; | ||
ButtonCopyMarkdown.innerHTML = "复制为Markdown"; | ||
}, 1000); | ||
} | ||
}); | ||
|
||
} catch (e) { | ||
console.log(e); | ||
} | ||
|
||
} | ||
}; | ||
|
||
|
||
setTimeout(main, 300); | ||
|
||
setInterval(main, 1000); |
Oops, something went wrong.