Skip to content

Commit

Permalink
feat: All
Browse files Browse the repository at this point in the history
  • Loading branch information
Howardzhangdqs committed Nov 1, 2023
0 parents commit 9b20849
Show file tree
Hide file tree
Showing 20 changed files with 1,331 additions and 0 deletions.
20 changes: 20 additions & 0 deletions .eslintrc.json
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/"
]
}
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
node_modules/
dist/

pnpm-lock.yaml
41 changes: 41 additions & 0 deletions README.md
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解析

25 changes: 25 additions & 0 deletions TampermonkeyConfig.js
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",
};
21 changes: 21 additions & 0 deletions UpdateLog.txt
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上
40 changes: 40 additions & 0 deletions package.json
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"
}
10 changes: 10 additions & 0 deletions scripts/add-version.js
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));
37 changes: 37 additions & 0 deletions scripts/build-tampermonkey.js
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");
29 changes: 29 additions & 0 deletions src/download2zip.ts
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;
}
1 change: 1 addition & 0 deletions src/entry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
import "./index";
1 change: 1 addition & 0 deletions src/index.html

Large diffs are not rendered by default.

130 changes: 130 additions & 0 deletions src/index.ts
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);
Loading

0 comments on commit 9b20849

Please sign in to comment.