markdown-paper 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +83 -0
- package/bin/mdp.ts +37 -0
- package/bun.lockb +0 -0
- package/lib/docx.ts +18 -0
- package/lib/main.ts +177 -0
- package/license +674 -0
- package/package.json +28 -0
- package/theme/aps/aps.css +134 -0
- package/theme/aps/aps.md +41 -0
- package/theme/aps/aps.ts +122 -0
- package/theme/bnu/bnu.css +0 -0
- package/theme/bnu/bnu.md +0 -0
- package/theme/bnu/bnu.ts +0 -0
- package/theme/theme.ts +45 -0
- package/tsconfig.json +27 -0
package/README.md
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
以心理学报的格式从 Markdown 生成 PDF / HTML / DOCX 文件
|
|
2
|
+
|
|
3
|
+
## 使用方法
|
|
4
|
+
```bash
|
|
5
|
+
# 安装 Bun (参见 bun.sh)
|
|
6
|
+
...
|
|
7
|
+
# 安装 MarkdownPaper
|
|
8
|
+
bun add -g markdown-paper
|
|
9
|
+
# 使用 (第一个参数为源文件相对路径)
|
|
10
|
+
mdp <path> [--option]
|
|
11
|
+
# 查看使用方法
|
|
12
|
+
mdp
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
> 如果您安装过旧版本的 `MarkdownPaper` (小于 `2.0.0`), 请先卸载旧版本再安装新版本
|
|
16
|
+
|
|
17
|
+
| 参数 | 说明 |
|
|
18
|
+
| :---: | :---: |
|
|
19
|
+
| `--out=xxx` | 输出文件相对路径<br>默认为源文件路径的同名 `PDF` 文件 |
|
|
20
|
+
| `--browser=xxx` | 浏览器绝对路径, 非 `Windows` 系统则必须<br>默认为 `C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe` |
|
|
21
|
+
| `--theme=xxx` | 论文模板, 默认为 `aps` (`Acta Psychologica Sinica`)<br>暂时没有其他模板, 欢迎贡献 |
|
|
22
|
+
| `--outputHTML` | 输出 `HTML` 文件, 默认不输出 |
|
|
23
|
+
| `--outputDOCX` | 输出 `DOCX` 文件, 默认不输出<br>**须先执行 `pip install pdf2docx` 安装依赖**<br>使用时推荐开启 `--hideFooter` 参数 |
|
|
24
|
+
|
|
25
|
+
## 模板说明
|
|
26
|
+
`/theme/theme.ts` 中的 `Theme` 抽象类定义了模板的样式, 按照类似于 `aps` 文件夹的结构可自定义模板; 模板可以提供自定义功能
|
|
27
|
+
|
|
28
|
+
模板制作完成后, 在 `/lib/main.ts` 中导入并添加到 `class Options -> constructor -> case '--theme':` 中即可使用
|
|
29
|
+
|
|
30
|
+
### APS 模板
|
|
31
|
+
#### 额外命令行参数
|
|
32
|
+
| 参数 | 说明 |
|
|
33
|
+
| :---: | :---: |
|
|
34
|
+
| `--showTitle` | 在页眉显示文件名, 默认不显示 |
|
|
35
|
+
| `--hideFooter` | 隐藏页码, 默认显示 |
|
|
36
|
+
| `--zhPunctuation` | 将正文中的英文标点符号替换为中文标点符号, 默认不替换<br>仅替换 `PDF` 和 `DOCX` 文件 |
|
|
37
|
+
| `--enPunctuation` | 将正文中的中文标点符号替换为英文标点符号, 默认不替换<br>仅替换 `PDF` 和 `DOCX` 文件 |
|
|
38
|
+
|
|
39
|
+
#### 编写格式
|
|
40
|
+
```markdown
|
|
41
|
+
# 中文标题
|
|
42
|
+
#author# 作者信息
|
|
43
|
+
#school# 单位信息
|
|
44
|
+
#abstract# 摘要内容
|
|
45
|
+
#keywords# 关键词内容
|
|
46
|
+
|
|
47
|
+
## 1 一级标题
|
|
48
|
+
### 1.1 二级标题
|
|
49
|
+
#### 1.1.1 三级标题
|
|
50
|
+
正文
|
|
51
|
+
|
|
52
|
+

|
|
53
|
+
|
|
54
|
+
> 图片标题
|
|
55
|
+
|
|
56
|
+
> 表格标题
|
|
57
|
+
|
|
58
|
+
| 表头1 | 表头2 |
|
|
59
|
+
| :---: | :---: |
|
|
60
|
+
| 内容1 | 内容2 |
|
|
61
|
+
|
|
62
|
+
##### 参考文献
|
|
63
|
+
- 文献1
|
|
64
|
+
- 文献2
|
|
65
|
+
- 文献3
|
|
66
|
+
|
|
67
|
+
##### 附录
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## 更新日志
|
|
71
|
+
- `2.0.0` (2024-06-20): 重构代码, 完善模板功能
|
|
72
|
+
- `1.4.0` (2024-05-29): 新增替换中英文标点符号功能
|
|
73
|
+
- `1.3.2` (2024-05-28): 优化图表标题显示
|
|
74
|
+
- `1.3.1` (2024-05-28): 优化样式
|
|
75
|
+
- `1.3.0` (2024-05-26): 支持自定义 `CSS` 文件, 简化编写格式, 优化帮助信息
|
|
76
|
+
- `1.2.3` (2024-05-25): 修复中文图片路径导致的错误
|
|
77
|
+
- `1.2.2` (2024-05-21): 优化样式, 优化命令
|
|
78
|
+
- `1.2.1` (2024-05-21): 优化摘要样式
|
|
79
|
+
- `1.2.0` (2024-05-21): 支持表格, 优化书写语法
|
|
80
|
+
- `1.1.3` (2024-05-20): 修复图片显示问题, 并新增显示图片标题
|
|
81
|
+
- `1.1.2` (2024-05-20): 优化 `DOCX` 文件输出
|
|
82
|
+
- `1.1.1` (2024-05-19): 支持 `DOCX` 文件输出
|
|
83
|
+
- `1.0.0` (2024-05-19): 初版发布
|
package/bin/mdp.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import { Options, renderMarkdown } from '../lib/main.ts'
|
|
4
|
+
/** 命令行参数 */
|
|
5
|
+
const args = process.argv.slice(2)
|
|
6
|
+
/** 当前工作目录 */
|
|
7
|
+
const cwd = process.cwd()
|
|
8
|
+
/**
|
|
9
|
+
* 主函数
|
|
10
|
+
* @param args 命令行参数
|
|
11
|
+
* @param cwd 当前工作目录
|
|
12
|
+
*/
|
|
13
|
+
void async function main(args: string[], cwd: string) {
|
|
14
|
+
try {
|
|
15
|
+
// 如果没有参数, 显示帮助信息
|
|
16
|
+
if (args.length === 0) {
|
|
17
|
+
console.log(`\n使用方法:\n${Options.format}\n`)
|
|
18
|
+
process.exit(0)
|
|
19
|
+
}
|
|
20
|
+
// 解析参数
|
|
21
|
+
console.log('\n开始生成\n')
|
|
22
|
+
const options = new Options(args, cwd)
|
|
23
|
+
// 渲染 markdown
|
|
24
|
+
await renderMarkdown(options)
|
|
25
|
+
console.log('生成成功\n')
|
|
26
|
+
}
|
|
27
|
+
catch (e) {
|
|
28
|
+
if (e instanceof SyntaxError) {
|
|
29
|
+
console.error(`参数错误, 正确格式:\n${Options.format}\n`)
|
|
30
|
+
} else if (e instanceof Error) {
|
|
31
|
+
console.error(`错误:\n${e.message}\n`)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
finally {
|
|
35
|
+
process.exit(0)
|
|
36
|
+
}
|
|
37
|
+
}(args, cwd)
|
package/bun.lockb
ADDED
|
Binary file
|
package/lib/docx.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
declare var self: Worker
|
|
2
|
+
|
|
3
|
+
import { $ } from 'bun'
|
|
4
|
+
import { Options } from './main'
|
|
5
|
+
|
|
6
|
+
self.onmessage = async (e) => {
|
|
7
|
+
const options = e.data as Options
|
|
8
|
+
await $`pdf2docx convert ${options.out}`
|
|
9
|
+
.then(() => {
|
|
10
|
+
postMessage('success')
|
|
11
|
+
})
|
|
12
|
+
.catch(() => {
|
|
13
|
+
postMessage('error')
|
|
14
|
+
})
|
|
15
|
+
.finally(() => {
|
|
16
|
+
process.exit(0)
|
|
17
|
+
})
|
|
18
|
+
}
|
package/lib/main.ts
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { marked } from 'marked'
|
|
2
|
+
import puppeteer from 'puppeteer-core'
|
|
3
|
+
import fs from 'node:fs/promises'
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
import { readFileSync } from 'node:fs'
|
|
6
|
+
import { APS } from '../theme/aps/aps'
|
|
7
|
+
import { Theme } from '../theme/theme'
|
|
8
|
+
|
|
9
|
+
/** 应用参数 */
|
|
10
|
+
export class Options {
|
|
11
|
+
/** markdown 文件绝对路径 */
|
|
12
|
+
src: string
|
|
13
|
+
/** pdf 文件绝对路径 */
|
|
14
|
+
out: string
|
|
15
|
+
/** 自定义浏览器 */
|
|
16
|
+
browser: string
|
|
17
|
+
/** 是否输出 html */
|
|
18
|
+
outputHTML: boolean
|
|
19
|
+
/** 是否输出 docx */
|
|
20
|
+
outputDOCX: boolean
|
|
21
|
+
/** 样式 */
|
|
22
|
+
theme: Theme
|
|
23
|
+
/** 正确格式 */
|
|
24
|
+
static format = `mdp <markdown> [--options]
|
|
25
|
+
|
|
26
|
+
<markdown>: markdown 文件相对路径
|
|
27
|
+
--out=<path>: 输出文件相对路径 (默认为 <markdown>)
|
|
28
|
+
--theme=<name>: 论文模板 (默认为 APS)
|
|
29
|
+
--outputHTML: 输出 html 文件 (默认不输出)
|
|
30
|
+
--outputDOCX: 输出 docx 文件 (默认不输出)
|
|
31
|
+
--browser=<path>: 自定义浏览器路径 (默认为 Edge)
|
|
32
|
+
|
|
33
|
+
模板的自定义参数见模板说明`
|
|
34
|
+
/**
|
|
35
|
+
* 生成应用参数
|
|
36
|
+
* @param args 命令行参数
|
|
37
|
+
* @param cwd 当前工作目录
|
|
38
|
+
*/
|
|
39
|
+
constructor(args: string[], cwd: string) {
|
|
40
|
+
// 默认参数
|
|
41
|
+
this.src = ''
|
|
42
|
+
this.out = ''
|
|
43
|
+
this.outputHTML = false
|
|
44
|
+
this.outputDOCX = false
|
|
45
|
+
this.theme = new APS(args, cwd)
|
|
46
|
+
this.browser = 'C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe'
|
|
47
|
+
// 解析路径参数
|
|
48
|
+
if (args.length === 0) throw SyntaxError()
|
|
49
|
+
else args[0] = `--src=${args[0]}`
|
|
50
|
+
// 解析其他参数
|
|
51
|
+
args.forEach(arg => {
|
|
52
|
+
switch (arg.split('=')[0]) {
|
|
53
|
+
case '--src': {
|
|
54
|
+
const a = arg.split('=')
|
|
55
|
+
if (a.length !== 2 || a[1] === '') throw SyntaxError()
|
|
56
|
+
this.src = a[1].endsWith('.md') ? path.resolve(cwd, a[1]) : path.resolve(cwd, a[1] + '.md')
|
|
57
|
+
break
|
|
58
|
+
}
|
|
59
|
+
case '--out': {
|
|
60
|
+
const a = arg.split('=')
|
|
61
|
+
if (a.length !== 2 || a[1] === '') throw SyntaxError()
|
|
62
|
+
this.out = a[1].endsWith('.pdf') ? path.resolve(cwd, a[1]) : path.resolve(cwd, a[1] + '.pdf')
|
|
63
|
+
break
|
|
64
|
+
}
|
|
65
|
+
case '--theme': {
|
|
66
|
+
const a = arg.split('=')
|
|
67
|
+
if (a.length !== 2 || a[1] === '') throw SyntaxError()
|
|
68
|
+
switch (a[1].toUpperCase()) {
|
|
69
|
+
case 'APS': {
|
|
70
|
+
break
|
|
71
|
+
}
|
|
72
|
+
default: {
|
|
73
|
+
throw Error(`模板 ${a[1]} 不存在`)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
break
|
|
77
|
+
}
|
|
78
|
+
case '--outputHTML': {
|
|
79
|
+
this.outputHTML = true
|
|
80
|
+
break
|
|
81
|
+
}
|
|
82
|
+
case '--outputDOCX': {
|
|
83
|
+
this.outputDOCX = true
|
|
84
|
+
break
|
|
85
|
+
}
|
|
86
|
+
case '--browser': {
|
|
87
|
+
const a = arg.split('=')
|
|
88
|
+
if (a.length !== 2 || a[1] === '') throw SyntaxError()
|
|
89
|
+
this.browser = a[1]
|
|
90
|
+
break
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
})
|
|
94
|
+
// 检查参数
|
|
95
|
+
if (this.out === '') this.out = this.src.replace('.md', '.pdf')
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* 渲染 markdown
|
|
101
|
+
* @param options 参数
|
|
102
|
+
*/
|
|
103
|
+
export async function renderMarkdown(options: Options): Promise<void> {
|
|
104
|
+
// 读取 markdown 文件
|
|
105
|
+
let md = await fs.readFile(options.src, { encoding: 'utf-8' })
|
|
106
|
+
// 预处理 markdown
|
|
107
|
+
md = options.theme.preParseMarkdown(md)
|
|
108
|
+
// 转换 markdown 为 html
|
|
109
|
+
const html = await marked(md)
|
|
110
|
+
// 创建网页文件
|
|
111
|
+
const title = path.basename(options.src).replace('.md', '')
|
|
112
|
+
let web = `
|
|
113
|
+
<!DOCTYPE html>
|
|
114
|
+
<html lang="zh-CN">
|
|
115
|
+
<head>
|
|
116
|
+
<meta charset="UTF-8">
|
|
117
|
+
<title>${title}</title>
|
|
118
|
+
<style>${options.theme.css}</style>
|
|
119
|
+
</head>
|
|
120
|
+
<body>
|
|
121
|
+
${html}
|
|
122
|
+
</body>
|
|
123
|
+
</html>
|
|
124
|
+
`
|
|
125
|
+
// 预处理 html
|
|
126
|
+
web = options.theme.preParseHTML(web)
|
|
127
|
+
// 保存 html 文件
|
|
128
|
+
options.outputHTML && await fs.writeFile(options.out.replace('.pdf', '.html'), web)
|
|
129
|
+
// 把图片转换为 base64
|
|
130
|
+
web = web.replace(/<img src="(.+?)"/g, (match, p1) => {
|
|
131
|
+
if (p1.startsWith('http')) return match
|
|
132
|
+
try {
|
|
133
|
+
const url = path.resolve(path.dirname(options.src), decodeURI(p1))
|
|
134
|
+
const data = readFileSync(url).toString('base64')
|
|
135
|
+
return `<img src="data:image/${path.extname(p1).replace('.', '')};base64,${data}"`
|
|
136
|
+
} catch (_) {
|
|
137
|
+
console.error(`图片 ${p1} 不存在`)
|
|
138
|
+
return match
|
|
139
|
+
}
|
|
140
|
+
})
|
|
141
|
+
// 创建浏览器
|
|
142
|
+
const browser = await puppeteer.launch({ executablePath: options.browser })
|
|
143
|
+
// 创建页面
|
|
144
|
+
const page = await browser.newPage()
|
|
145
|
+
// 设置页面内容
|
|
146
|
+
await page.setContent(web)
|
|
147
|
+
// 执行脚本
|
|
148
|
+
await page.evaluate(options.theme.script)
|
|
149
|
+
// 生成 pdf
|
|
150
|
+
await page.pdf({ path: options.out, ...options.theme.pdfOptions })
|
|
151
|
+
// 关闭浏览器
|
|
152
|
+
await browser.close()
|
|
153
|
+
// 保存 docx 文件
|
|
154
|
+
if (options.outputDOCX) {
|
|
155
|
+
await new Promise<void>((resolve, reject) => {
|
|
156
|
+
const worker = new Worker(new URL('docx.ts', import.meta.url).href)
|
|
157
|
+
worker.onmessage = (e) => {
|
|
158
|
+
switch (e.data) {
|
|
159
|
+
case 'success': {
|
|
160
|
+
console.log('')
|
|
161
|
+
resolve()
|
|
162
|
+
break
|
|
163
|
+
}
|
|
164
|
+
case 'error': {
|
|
165
|
+
reject(Error('生成 docx 文件失败'))
|
|
166
|
+
break
|
|
167
|
+
}
|
|
168
|
+
default: {
|
|
169
|
+
reject(Error('调用 Python 时发生未知错误'))
|
|
170
|
+
break
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
worker.postMessage(options)
|
|
175
|
+
})
|
|
176
|
+
}
|
|
177
|
+
}
|