markdown-paper 2.4.0 → 3.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/CHANGELOG.md ADDED
@@ -0,0 +1,14 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file. See [commit-and-tag-version](https://github.com/absolute-version/commit-and-tag-version) for commit guidelines.
4
+
5
+ ## 3.0.0 (2025-06-09)
6
+
7
+
8
+ ### ⚠ BREAKING CHANGES
9
+
10
+ * 重构代码
11
+
12
+ ### Refactoring
13
+
14
+ * 重构代码 ([1702525](https://github.com/LeafYeeXYZ/MarkdownPaper/commit/1702525d7bf4a9470de8aeabb0cad1e7abff6e56))
package/README.md CHANGED
@@ -53,6 +53,10 @@ $$
53
53
  - 文献2
54
54
  - 文献3
55
55
 
56
+ ---
57
+
58
+ (上面的是分页符)
59
+
56
60
  ##### 附录
57
61
  ```
58
62
 
@@ -60,7 +64,7 @@ $$
60
64
 
61
65
  ## 2 安装 `Bun`
62
66
 
63
- `Bun` 是一个现代的 `JavaScript` / `TypeScript` 运行环境, 本项目基于 `Bun` 环境开发; 请在官网 [bun.sh](https://bun.sh) 下载并安装 `Bun`, 也可以直接使用 `npm install -g bun` 安装
67
+ `Bun` 是一个现代的 `JavaScript` / `TypeScript` 运行环境, 本项目基于 `Bun` 环境运行; 请在官网 [bun.sh](https://bun.sh) 下载并安装 `Bun`, 也可以直接使用 `npm install -g bun` 安装
64
68
 
65
69
  ## 3 安装 `MarkdownPaper`
66
70
 
@@ -97,6 +101,7 @@ mdp example.md --outputDOCX
97
101
  | `--outputDOCX` | 输出 `DOCX` 文件, 默认不输出<br>**导出后样式可能无法完全保留, 请自行调整** |
98
102
 
99
103
  # 模板说明
104
+
100
105
  `/theme/theme.ts` 中的 `MarkdownnPaperTheme` 接口定义了模板的样式, 按照类似于 `aps` 文件夹的结构可自定义模板; 模板可以提供自定义功能
101
106
 
102
107
  模板制作完成后, 在 `/lib/main.ts` 中导入并添加到 `class MarkdownPaperOptions -> constructor -> case '--theme':` 中, 并在下方添加使用文档即可
@@ -104,7 +109,9 @@ mdp example.md --outputDOCX
104
109
  推荐所有主题的文档和编写格式都尽量与 `aps` 主题保持一致
105
110
 
106
111
  ## APS 模板
112
+
107
113
  ### 额外命令行参数
114
+
108
115
  | 参数 | 说明 |
109
116
  | :---: | :---: |
110
117
  | `--showTitle` | 在页眉显示文件名, 默认不显示 |
@@ -113,9 +120,12 @@ mdp example.md --outputDOCX
113
120
  | `--enPunctuation` | 将正文中的中文标点符号替换为英文标点符号, 默认不替换<br>仅替换 `PDF` 和 `DOCX` 文件 |
114
121
 
115
122
  ### 编写格式
123
+
116
124
  同上
117
125
 
118
126
  # 更新日志
127
+
128
+ - `2.5.0` (2025-02-07): 支持分页符
119
129
  - `2.4.0` (2024-12-08): 导出 `DOCX` 文件时, 不再依赖 `Python`
120
130
  - `2.3.0` (2024-08-31): 支持数学公式
121
131
  - `2.2.0` (2024-08-26): 半重构, 优化导入导出, 优化文档
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env bun
2
+
3
+ import { Command } from 'commander'
4
+ import { themeLabels } from '../lib/types'
5
+ import { version } from '../package.json'
6
+
7
+ const program = new Command()
8
+
9
+ program
10
+ .name('mdp-themes')
11
+ .description('MarkdownPaper CLI - 显示所有可用的论文模板')
12
+ .version(version)
13
+ .action(() => {
14
+ console.log('可用的论文模板:')
15
+ for (const [id, label] of Object.entries(themeLabels)) {
16
+ console.log(`- ${id} (${label})`)
17
+ }
18
+ })
19
+
20
+ program.parse(process.argv)
package/bin/mdp.ts CHANGED
@@ -1,37 +1,46 @@
1
1
  #!/usr/bin/env bun
2
2
 
3
- import { MarkdownPaperOptions, renderMarkdown } from '../lib/main.ts'
4
- /** 命令行参数 */
5
- const args = process.argv.slice(2)
6
- /** 当前工作目录 */
3
+ import { resolve } from 'node:path'
4
+ import { Command } from 'commander'
5
+ import { render } from '../lib/main'
6
+ import { cliOptions } from '../lib/types'
7
+ import { version } from '../package.json'
8
+
9
+ const program = new Command()
7
10
  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${MarkdownPaperOptions.format}\n`)
18
- process.exit(0)
19
- }
20
- // 解析参数
21
- console.log('\n开始生成\n')
22
- const options = new MarkdownPaperOptions(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${MarkdownPaperOptions.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)
11
+
12
+ program
13
+ .name('mdp')
14
+ .description('MarkdownPaper CLI')
15
+ .version(version)
16
+ .argument('<markdown>', 'Markdown文件相对路径')
17
+ .option('--out <path>', '输出文件相对路径 (默认和Markdown文件相同)')
18
+ .option('--theme <name>', '论文模板 (默认为心理学报)', 'APS')
19
+ .option('--html --output-html', '输出HTML文件', false)
20
+ .option('--docx --output-docx', '输出DOCX文件', false)
21
+ .option('--hide-footer', '隐藏页脚页码', false)
22
+ .option('--punctuation <zh/en/origin>', '标点符号格式', 'zh')
23
+ .command('themes', '显示所有可用的论文模板')
24
+ .action((markdown, options) => {
25
+ console.log('开始生成')
26
+ const src = resolve(cwd, markdown)
27
+ const out = options.out
28
+ ? resolve(cwd, options.out)
29
+ : src.replace(/\.md$/, '.pdf')
30
+ const opts = cliOptions.parse({
31
+ ...options,
32
+ src,
33
+ out,
34
+ })
35
+ render(opts)
36
+ .then(() => {
37
+ console.log('生成成功')
38
+ process.exit(0)
39
+ })
40
+ .catch((error) => {
41
+ console.error(`生成失败: ${error.message}`)
42
+ process.exit(1)
43
+ })
44
+ })
45
+
46
+ program.parse(process.argv)
package/biome.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
3
+ "vcs": {
4
+ "enabled": true,
5
+ "clientKind": "git",
6
+ "useIgnoreFile": true
7
+ },
8
+ "organizeImports": {
9
+ "enabled": true
10
+ },
11
+ "linter": {
12
+ "enabled": true,
13
+ "rules": {
14
+ "recommended": true
15
+ }
16
+ },
17
+ "javascript": {
18
+ "formatter": {
19
+ "trailingCommas": "all",
20
+ "semicolons": "asNeeded",
21
+ "quoteStyle": "single"
22
+ }
23
+ }
24
+ }
package/bun.lockb ADDED
Binary file
package/lib/main.ts CHANGED
@@ -1,194 +1,50 @@
1
- import { marked } from 'marked'
2
- import puppeteer from 'puppeteer'
3
1
  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 type { MarkdownPaperTheme } from '../theme/theme'
8
- import type { PDFOptions } from 'puppeteer'
9
- import markedKatex from 'marked-katex-extension'
2
+ import { asBlob } from 'html-docx-js-typescript'
10
3
  // @ts-ignore
11
4
  import katexCss from 'katex/dist/katex.css' with { type: 'text' }
12
- import { asBlob } from 'html-docx-js-typescript'
13
-
14
- /** 应用参数 */
15
- class MarkdownPaperOptions {
16
- /** markdown 文件绝对路径 */
17
- src: string
18
- /** pdf 文件绝对路径 */
19
- out: string
20
- /** 是否输出 html */
21
- outputHTML: boolean
22
- /** 是否输出 docx */
23
- outputDOCX: boolean
24
- /** 样式 */
25
- theme: MarkdownPaperTheme
26
- /** 正确格式 */
27
- static format = `mdp <markdown> [--options]
28
-
29
- <markdown>: markdown 文件相对路径
30
- --out=<path>: 输出文件相对路径 (默认为 <markdown>)
31
- --theme=<name>: 论文模板 (默认为 APS)
32
- --outputHTML: 输出 html 文件 (默认不输出)
33
- --outputDOCX: 输出 docx 文件 (默认不输出)
34
-
35
- 模板的自定义参数见模板说明`
36
- /**
37
- * 生成应用参数
38
- * @param args 命令行参数
39
- * @param cwd 当前工作目录
40
- */
41
- constructor(args: string[], cwd: string) {
42
- // 默认参数
43
- this.src = ''
44
- this.out = ''
45
- this.outputHTML = false
46
- this.outputDOCX = false
47
- this.theme = new APS(args)
48
- // 解析路径参数
49
- if (args.length === 0) throw SyntaxError()
50
- else args[0] = `--src=${args[0]}`
51
- // 解析其他参数
52
- args.forEach(arg => {
53
- switch (arg.split('=')[0]) {
54
- case '--src': {
55
- const a = arg.split('=')
56
- if (a.length !== 2 || a[1] === '') throw SyntaxError()
57
- this.src = a[1].endsWith('.md') ? path.resolve(cwd, a[1]) : path.resolve(cwd, a[1] + '.md')
58
- break
59
- }
60
- case '--out': {
61
- const a = arg.split('=')
62
- if (a.length !== 2 || a[1] === '') throw SyntaxError()
63
- this.out = a[1].endsWith('.pdf') ? path.resolve(cwd, a[1]) : path.resolve(cwd, a[1] + '.pdf')
64
- break
65
- }
66
- case '--theme': {
67
- const a = arg.split('=')
68
- if (a.length !== 2 || a[1] === '') throw SyntaxError()
69
- switch (a[1].toUpperCase()) {
70
- case 'APS': {
71
- break
72
- }
73
- default: {
74
- throw Error(`模板 ${a[1]} 不存在`)
75
- }
76
- }
77
- break
78
- }
79
- case '--outputHTML': {
80
- this.outputHTML = true
81
- break
82
- }
83
- case '--outputDOCX': {
84
- this.outputDOCX = true
85
- break
86
- }
87
- }
88
- })
89
- // 检查参数
90
- if (this.out === '') this.out = this.src.replace('.md', '.pdf')
91
- }
92
- }
93
-
94
- /**
95
- * 渲染 markdown
96
- * @param options 参数
97
- */
98
- async function renderMarkdown(
99
- options: MarkdownPaperOptions,
100
- ): Promise<void> {
101
- // 读取 markdown 文件
102
- const raw = await fs.readFile(options.src, { encoding: 'utf-8' })
103
- // 生成 html 文件
104
- const html = await mdToHtml(raw, options.theme, path.basename(options.src).replace('.md', ''))
105
- // 保存 pdf 文件
106
- await htmlToPdf(
107
- html.replace(/<img src="(.+?)"/g, (match, p1) => {
108
- if (p1.startsWith('http')) return match
109
- try {
110
- const url = path.resolve(path.dirname(options.src), decodeURI(p1))
111
- const data = readFileSync(url).toString('base64')
112
- return `<img src="data:image/${path.extname(p1).replace('.', '')};base64,${data}"`
113
- } catch (_) {
114
- console.error(`图片 ${p1} 不存在`)
115
- return match
116
- }
117
- }),
118
- options.out,
119
- options.theme.pdfOptions,
120
- options.theme.script
121
- )
122
- // 保存 html 文件
123
- options.outputHTML && await fs.writeFile(options.out.replace('.pdf', '.html'), html)
124
- // 保存 docx 文件
125
- const docx = await asBlob(html)
126
- const docxBuffer = new Uint8Array(docx instanceof Blob ? await docx.arrayBuffer() : docx.buffer)
127
- options.outputDOCX && await fs.writeFile(options.out.replace('.pdf', '.docx'), docxBuffer)
128
- options.outputDOCX && console.warn('导出的 DOCX 文件可能存在格式丢失, 请手动调整\n')
129
- }
130
-
131
- /**
132
- * 把 html 转换为 pdf
133
- * @param html html 字符串, 图片为 base64
134
- * @param dist pdf 文件绝对路径
135
- * @param options pdf 参数, 无需设置路径
136
- * @param script 在网页中要执行的函数
137
- */
138
- async function htmlToPdf(html: string, dist: string, options: PDFOptions, script: () => void): Promise<void> {
139
- const browser = await puppeteer.launch()
140
- const page = await browser.newPage()
141
- await page.setContent(html)
142
- // 执行脚本
143
- await page.evaluate(script)
144
- await page.pdf({ path: dist, ...options })
145
- await browser.close()
146
- }
5
+ import { marked } from 'marked'
6
+ import markedKatex from 'marked-katex-extension'
7
+ import puppeteer from 'puppeteer'
8
+ import { type MarkdownPaperOptions, themes } from './types'
147
9
 
148
10
  /**
149
- * markdown 转换为 html
150
- * @param md markdown 字符串
151
- * @param theme 论文模板
152
- * @param pageTitle 页面标题
153
- * @returns html 字符串
11
+ * 渲染 MarkdownPaper 文档
12
+ * @param options MarkdownPaper CLI 选项
154
13
  */
155
- async function mdToHtml(
156
- md: string,
157
- theme: MarkdownPaperTheme,
158
- pageTitle: string = 'MarkdownPaper'
159
- ): Promise<string> {
160
- // 设置 marked
161
- marked.use(markedKatex({ throwOnError: false }))
162
- // 预处理 markdown
163
- let html = await theme.preParseMarkdown(md)
164
- // 转换 markdown 为 html
165
- html = `
14
+ export async function render(options: MarkdownPaperOptions): Promise<void> {
15
+ const theme = await themes[options.theme](options)
16
+ const markdown = await fs.readFile(options.src, { encoding: 'utf-8' })
17
+ marked.use(markedKatex({ throwOnError: false }))
18
+ const html = await theme.preParseHtml(`
166
19
  <!DOCTYPE html>
167
20
  <html lang="zh-CN">
168
21
  <head>
169
22
  <meta charset="UTF-8">
170
- <title>${pageTitle}</title>
171
- <style>${theme.css}</style>
23
+ <title>MarkdownPaper</title>
24
+ <style>\n${theme.css}\n</style>
172
25
  <style>\n${katexCss}\n</style>
173
26
  </head>
174
27
  <body>
175
- ${await marked(html)}
28
+ ${await marked(await theme.preParseMarkdown(markdown))}
176
29
  </body>
177
30
  </html>
178
- `
179
- // 预处理 html
180
- html = await theme.preParseHTML(html)
181
- return html
182
- }
183
-
184
-
185
- export {
186
- MarkdownPaperOptions,
187
- renderMarkdown,
188
- htmlToPdf,
189
- mdToHtml,
190
- APS
31
+ `)
32
+ const browser = await puppeteer.launch()
33
+ const page = await browser.newPage()
34
+ await page.setContent(html)
35
+ await page.evaluate(theme.script)
36
+ await page.pdf({ path: options.out, ...theme.pdfOptions })
37
+ const finalHtml = await page.content()
38
+ if (options.outputHtml) {
39
+ await fs.writeFile(options.out.replace('.pdf', '.html'), finalHtml)
40
+ }
41
+ if (options.outputDocx) {
42
+ const docx = await asBlob(finalHtml)
43
+ const docxBuffer = new Uint8Array(
44
+ docx instanceof Blob ? await docx.arrayBuffer() : docx.buffer,
45
+ )
46
+ await fs.writeFile(options.out.replace('.pdf', '.docx'), docxBuffer)
47
+ console.warn('注意: 导出的Word文件可能存在格式丢失, 请手动调整')
48
+ }
49
+ await browser.close()
191
50
  }
192
- export type {
193
- MarkdownPaperTheme
194
- }
@@ -0,0 +1,164 @@
1
+ * {
2
+ font-family: "Times", "Times New Roman", "宋体", "SimSun", "华文宋体",
3
+ "STSong", sans-serif; /* 所有数字和英文字体都用 Times New Roman */
4
+ line-height: 1.55em; /* 1.5倍行距 */
5
+ margin: 0;
6
+ }
7
+
8
+ hr {
9
+ /* 用作分页符 */
10
+ page-break-after: always;
11
+ border: none;
12
+ }
13
+
14
+ h1 {
15
+ /* 中文题目: 二号黑体 */
16
+ font-size: 29px;
17
+ font-weight: normal;
18
+ font-family: "黑体", "SimHei", "华文黑体", "STHeiti", sans-serif;
19
+ text-align: center;
20
+ margin-bottom: 9px;
21
+ }
22
+ .author {
23
+ /* 作者姓名: 四号仿宋 */
24
+ font-size: 18px;
25
+ font-weight: normal;
26
+ font-family: "仿宋", "Fangsong", "华文仿宋", "STFangsong", sans-serif;
27
+ text-align: center;
28
+ margin-bottom: 3px;
29
+ }
30
+ .school {
31
+ /* 作者单位: 小五宋体 */
32
+ font-size: 12px;
33
+ text-align: center;
34
+ margin-bottom: 38px;
35
+ }
36
+ .abstract {
37
+ /* 摘要和关键词: 五号宋体 */
38
+ font-size: 14px;
39
+ text-align: justify;
40
+ padding: 0 28px;
41
+ &::before {
42
+ content: "摘 要";
43
+ font-weight: normal;
44
+ font-family: "黑体", "SimHei", "华文黑体", "STHeiti", sans-serif;
45
+ display: inline-block;
46
+ margin-right: 14px;
47
+ }
48
+ }
49
+ .keywords {
50
+ font-size: 14px;
51
+ margin-bottom: 31px;
52
+ padding: 0 28px;
53
+ &::before {
54
+ content: "关键词";
55
+ font-weight: normal;
56
+ font-family: "黑体", "SimHei", "华文黑体", "STHeiti", sans-serif;
57
+ display: inline-block;
58
+ margin-right: 14px;
59
+ }
60
+ }
61
+
62
+ h2 {
63
+ /* 一级标题: 四号宋体 */
64
+ font-size: 18px;
65
+ font-weight: normal;
66
+ margin: 7px 0;
67
+ }
68
+ h3 {
69
+ /* 二级标题: 五号黑体 */
70
+ font-size: 14px;
71
+ font-weight: normal;
72
+ font-family: "黑体", "SimHei", "华文黑体", "STHeiti", sans-serif;
73
+ margin: 5px 0;
74
+ }
75
+ h4 {
76
+ /* 三级标题: 五号黑体 */
77
+ font-size: 14px;
78
+ font-weight: normal;
79
+ font-family: "黑体", "SimHei", "华文黑体", "STHeiti", sans-serif;
80
+ margin: 3px 0;
81
+ }
82
+
83
+ p {
84
+ /* 正文: 五号宋体 */
85
+ font-size: 14px;
86
+ text-indent: 24px;
87
+ text-align: justify;
88
+ }
89
+
90
+ h5 {
91
+ /* "参考文献": 五号黑体 */
92
+ font-size: 14px;
93
+ font-weight: normal;
94
+ font-family: "黑体", "SimHei", "华文黑体", "STHeiti", sans-serif;
95
+ text-align: center;
96
+ margin-bottom: 7px;
97
+ margin-top: 20px;
98
+ }
99
+ ul,
100
+ ol {
101
+ /* 参考文献的项目: 小五号宋体 */
102
+ list-style-type: none;
103
+ padding: 0;
104
+ & > li {
105
+ font-size: 12px;
106
+ margin: 6px 0;
107
+ text-align: justify;
108
+ text-indent: -24px;
109
+ padding-left: 24px;
110
+ }
111
+ & a {
112
+ text-decoration: none;
113
+ color: black;
114
+ }
115
+ }
116
+
117
+ img {
118
+ display: block;
119
+ max-width: 100%;
120
+ margin: 0 auto;
121
+ margin-top: 10px;
122
+ }
123
+
124
+ blockquote,
125
+ blockquote > p {
126
+ /* 图片和表格的标题: 小五号宋体 */
127
+ font-size: 12px;
128
+ font-weight: normal;
129
+ text-align: center;
130
+ margin: 0;
131
+ }
132
+ blockquote > p {
133
+ margin: 6px 0;
134
+ }
135
+ table {
136
+ /* 表格: 小五号宋体 */
137
+ font-size: 12px;
138
+ position: relative;
139
+ border-top: 1px solid black;
140
+ border-bottom: 1px solid black;
141
+ width: 100%;
142
+ max-width: 100%;
143
+ margin: 0 auto;
144
+ margin-bottom: 10px;
145
+ & th,
146
+ & td {
147
+ font-weight: normal;
148
+ }
149
+ & thead::after {
150
+ /* 用来做三线表中间的横线 */
151
+ content: "";
152
+ display: block;
153
+ position: absolute;
154
+ border-top: 1px solid #00000060;
155
+ width: 100%;
156
+ }
157
+ }
158
+
159
+ b,
160
+ strong {
161
+ /* 加粗按黑体处理 */
162
+ font-weight: normal;
163
+ font-family: "黑体", "SimHei", "华文黑体", "STHeiti", sans-serif;
164
+ }
@@ -0,0 +1,97 @@
1
+ import { readFile } from 'node:fs/promises'
2
+ import { resolve } from 'node:path'
3
+ import { type MarkdownPaperTheme, MarkdownPaperThemes } from '../types'
4
+
5
+ const cssPath = resolve(import.meta.dirname, 'aps.css')
6
+ const cssContent = await readFile(cssPath, 'utf-8')
7
+
8
+ export const themeAPS: MarkdownPaperTheme = async (args) => {
9
+ let script: () => void = () => {}
10
+ if (args.punctuation === 'zh') {
11
+ script = () => {
12
+ const nodes = document.querySelectorAll(
13
+ 'body > p, .abstract, .keywords, .author, .school',
14
+ )
15
+ // biome-ignore lint/complexity/noForEach: ...
16
+ nodes.forEach((node) => {
17
+ let text = node.textContent ?? ''
18
+ // 替换中英文逗号和句号
19
+ text = text
20
+ .replace(/, /g, ',')
21
+ .replace(/\. /g, '。')
22
+ .replace(/\.$/, '。')
23
+ // 替换中英文冒号
24
+ text = text.replace(/: /g, ':')
25
+ // 替换中英文分号
26
+ text = text.replace(/; /g, ';')
27
+ // 替换中英文感叹号
28
+ text = text.replace(/! /g, '!').replace(/!$/, '!')
29
+ // 替换中英文问号
30
+ text = text.replace(/\? /g, '?').replace(/\?$/, '?')
31
+ // 替换中英文括号
32
+ text = text.replace(/ \(/g, '(').replace(/\) /g, ')')
33
+ // 恢复et al.,
34
+ text = text
35
+ .replace(/et al\.,/g, 'et al., ')
36
+ .replace(/et al。/g, 'et al. ')
37
+ // 设置新文本
38
+ node.textContent = text
39
+ })
40
+ }
41
+ } else if (args.punctuation === 'en') {
42
+ script = () => {
43
+ const nodes = document.querySelectorAll(
44
+ 'body > p, .abstract, .keywords, .author, .school',
45
+ )
46
+ // biome-ignore lint/complexity/noForEach: ...
47
+ nodes.forEach((node) => {
48
+ let text = node.textContent ?? ''
49
+ // 替换中英文逗号和句号
50
+ text = text.replace(/,/g, ', ').replace(/。 /g, '. ')
51
+ // 替换中英文冒号
52
+ text = text.replace(/:/g, ': ')
53
+ // 替换中英文分号
54
+ text = text.replace(/;/g, '; ')
55
+ // 替换中英文感叹号
56
+ text = text.replace(/!/g, '! ')
57
+ // 替换中英文问号
58
+ text = text.replace(/?/g, '? ')
59
+ // 替换中英文括号
60
+ text = text.replace(/(/g, ' (').replace(/)/g, ') ')
61
+ // 设置新文本
62
+ node.textContent = text
63
+ })
64
+ }
65
+ }
66
+ return {
67
+ id: MarkdownPaperThemes.APS,
68
+ label: '心理学报',
69
+ css: cssContent,
70
+ script,
71
+ preParseMarkdown: async (md: string): Promise<string> => {
72
+ return md
73
+ .replace(/#author# (.*)/gm, '<div class="author">$1</div>')
74
+ .replace(/#school# (.*)/gm, '<div class="school">$1</div>')
75
+ .replace(/#keywords# (.*)/gm, '<div class="keywords">$1</div>')
76
+ .replace(/#abstract# (.*)/gm, '<div class="abstract">$1</div>')
77
+ },
78
+ preParseHtml: async (html: string): Promise<string> => {
79
+ // 把包裹图片的 p 标签去掉
80
+ return html.replace(/<p><img (.*?)><\/p>/g, '<img $1>')
81
+ },
82
+ pdfOptions: {
83
+ format: 'A4',
84
+ margin: {
85
+ top: '2cm',
86
+ right: '2.5cm',
87
+ bottom: '2cm',
88
+ left: '2.5cm',
89
+ },
90
+ displayHeaderFooter: !args.hideFooter,
91
+ headerTemplate: '<div></div>',
92
+ footerTemplate: args.hideFooter
93
+ ? '<div></div>'
94
+ : '<div style="font-size: 9px; font-family: \'SimSun\'; color: #333; padding: 5px; margin: 0 auto;">第 <span class="pageNumber"></span> 页 / 共 <span class="totalPages"></span> 页</div>',
95
+ },
96
+ }
97
+ }
package/lib/types.ts ADDED
@@ -0,0 +1,76 @@
1
+ import type { PDFOptions } from 'puppeteer'
2
+ import { z } from 'zod'
3
+ import { themeAPS } from './themes/aps'
4
+
5
+ /**
6
+ * MarkdownPaper 主题 ID
7
+ */
8
+ export enum MarkdownPaperThemes {
9
+ APS = 'APS',
10
+ }
11
+
12
+ export const themeLabels: Record<MarkdownPaperThemes, string> = {
13
+ [MarkdownPaperThemes.APS]: '心理学报',
14
+ }
15
+
16
+ export const themes: Record<MarkdownPaperThemes, MarkdownPaperTheme> = {
17
+ [MarkdownPaperThemes.APS]: themeAPS,
18
+ }
19
+
20
+ /**
21
+ * MarkdownPaper CLI 选项的 Zod Schema
22
+ */
23
+ export const cliOptions = z.object({
24
+ src: z.string(),
25
+ out: z.string(),
26
+ theme: z.nativeEnum(MarkdownPaperThemes),
27
+ outputHtml: z.boolean(),
28
+ outputDocx: z.boolean(),
29
+ hideFooter: z.boolean(),
30
+ punctuation: z.enum(['zh', 'en', 'origin']),
31
+ })
32
+
33
+ /**
34
+ * MarkdownPaper CLI 选项 (详见 `bin/mdp.ts`)
35
+ */
36
+ export type MarkdownPaperOptions = z.infer<typeof cliOptions>
37
+
38
+ /**
39
+ * MarkdownPaper 论文模板
40
+ * @param args 命令行参数, 如果不提供, 则只有效返回 id 和 label 参数
41
+ */
42
+ export type MarkdownPaperTheme = (args: MarkdownPaperOptions) => Promise<{
43
+ /**
44
+ * 主题ID
45
+ */
46
+ id: MarkdownPaperThemes
47
+ /**
48
+ * 主题名称
49
+ */
50
+ label: string
51
+ /**
52
+ * css 样式
53
+ * 不含 \<style>\</style>
54
+ */
55
+ css: string
56
+ /**
57
+ * 预处理 markdown 字符串 (用于转换自定义标签等)
58
+ * @param md markdown 字符串
59
+ * @returns 转换后的 markdown 字符串
60
+ */
61
+ preParseMarkdown(md: string): Promise<string>
62
+ /**
63
+ * 预处理 html 字符串
64
+ * @param html html 字符串
65
+ * @returns 转换后的 html 字符串
66
+ */
67
+ preParseHtml(html: string): Promise<string>
68
+ /**
69
+ * 在网页中要执行的函数
70
+ */
71
+ script(): void
72
+ /**
73
+ * PDF 参数 (无需设置路径)
74
+ */
75
+ pdfOptions: PDFOptions
76
+ }>
package/package.json CHANGED
@@ -1,31 +1,56 @@
1
1
  {
2
- "name": "markdown-paper",
3
- "type": "module",
4
- "version": "2.4.0",
5
- "author": {
6
- "name": "LeafYeeXYZ",
7
- "email": "xiaoyezi@leafyee.xyz"
8
- },
9
- "license": "GPL-3.0-only",
10
- "scripts": {
11
- "mdp": "bun ./bin/mdp.ts",
12
- "pub": "npm publish",
13
- "try": "bun mdp ./demo/论文.md --outputHTML --outputDOCX"
14
- },
15
- "main": "./lib/main.ts",
16
- "bin": {
17
- "mdp": "./bin/mdp.ts"
18
- },
19
- "devDependencies": {
20
- "@types/bun": "latest"
21
- },
22
- "peerDependencies": {
23
- "typescript": "^5.4.5"
24
- },
25
- "dependencies": {
26
- "html-docx-js-typescript": "^0.1.5",
27
- "marked": "^12.0.2",
28
- "marked-katex-extension": "^5.1.3",
29
- "puppeteer": "^22.15.0"
30
- }
31
- }
2
+ "name": "markdown-paper",
3
+ "version": "3.0.0",
4
+ "author": {
5
+ "name": "LeafYeeXYZ",
6
+ "email": "xiaoyezi@leafyee.xyz"
7
+ },
8
+ "dependencies": {
9
+ "commander": "^14.0.0",
10
+ "html-docx-js-typescript": "^0.1.5",
11
+ "marked": "^12.0.2",
12
+ "marked-katex-extension": "^5.1.4",
13
+ "puppeteer": "^22.15.0",
14
+ "zod": "^3.25.56"
15
+ },
16
+ "peerDependencies": {
17
+ "typescript": "^5.4.5"
18
+ },
19
+ "bin": {
20
+ "mdp": "bun ./bin/mdp.ts",
21
+ "mdp-themes": "bun ./bin/mdp-themes.ts"
22
+ },
23
+ "license": "GPL-3.0-only",
24
+ "scripts": {
25
+ "mdp": "bun ./bin/mdp.ts",
26
+ "try": "bun mdp ./demo/论文.md --html --docx",
27
+ "release:create": "commit-and-tag-version",
28
+ "release:publish": "git push --follow-tags origin main && npm publish",
29
+ "check": "biome check --write ."
30
+ },
31
+ "type": "module",
32
+ "devDependencies": {
33
+ "@biomejs/biome": "1.9.4",
34
+ "commit-and-tag-version": "^12.5.1"
35
+ },
36
+ "commit-and-tag-version": {
37
+ "types": [
38
+ {
39
+ "type": "feat",
40
+ "section": "Features"
41
+ },
42
+ {
43
+ "type": "fix",
44
+ "section": "Bug Fixes"
45
+ },
46
+ {
47
+ "type": "refactor",
48
+ "section": "Refactoring"
49
+ },
50
+ {
51
+ "type": "perf",
52
+ "section": "Improvements"
53
+ }
54
+ ]
55
+ }
56
+ }
package/tsconfig.json CHANGED
@@ -1,27 +1,27 @@
1
1
  {
2
- "compilerOptions": {
3
- // Enable latest features
4
- "lib": ["ESNext", "DOM"],
5
- "target": "ESNext",
6
- "module": "ESNext",
7
- "moduleDetection": "force",
8
- "jsx": "react-jsx",
9
- "allowJs": true,
2
+ "compilerOptions": {
3
+ // Enable latest features
4
+ "lib": ["ESNext", "DOM"],
5
+ "target": "ESNext",
6
+ "module": "ESNext",
7
+ "moduleDetection": "force",
8
+ "jsx": "react-jsx",
9
+ "allowJs": true,
10
10
 
11
- // Bundler mode
12
- "moduleResolution": "bundler",
13
- "allowImportingTsExtensions": true,
14
- "verbatimModuleSyntax": true,
15
- "noEmit": true,
11
+ // Bundler mode
12
+ "moduleResolution": "bundler",
13
+ "allowImportingTsExtensions": true,
14
+ "verbatimModuleSyntax": true,
15
+ "noEmit": true,
16
16
 
17
- // Best practices
18
- "strict": true,
19
- "skipLibCheck": true,
20
- "noFallthroughCasesInSwitch": true,
17
+ // Best practices
18
+ "strict": true,
19
+ "skipLibCheck": true,
20
+ "noFallthroughCasesInSwitch": true,
21
21
 
22
- // Some stricter flags (disabled by default)
23
- "noUnusedLocals": true,
24
- "noUnusedParameters": true,
25
- "noPropertyAccessFromIndexSignature": true
26
- }
22
+ // Some stricter flags (disabled by default)
23
+ "noUnusedLocals": true,
24
+ "noUnusedParameters": true,
25
+ "noPropertyAccessFromIndexSignature": true
26
+ }
27
27
  }
package/theme/aps/aps.css DELETED
@@ -1,137 +0,0 @@
1
- * {
2
- font-family: 'Times', 'Times New Roman', '宋体', 'SimSun', '华文宋体', 'STSong'; /* 所有数字和英文字体都用 Times New Roman */
3
- line-height: 1.55em; /* 1.5倍行距 */
4
- margin: 0;
5
- }
6
-
7
- h1 { /* 中文题目: 二号黑体 */
8
- font-size: 29px;
9
- font-weight: normal;
10
- font-family: '黑体', 'SimHei', '华文黑体', 'STHeiti';
11
- text-align: center;
12
- margin-bottom: 9px;
13
- }
14
- .author { /* 作者姓名: 四号仿宋 */
15
- font-size: 18px;
16
- font-weight: normal;
17
- font-family: '仿宋', 'Fangsong', '华文仿宋', 'STFangsong';
18
- text-align: center;
19
- margin-bottom: 3px;
20
- }
21
- .school { /* 作者单位: 小五宋体 */
22
- font-size: 12px;
23
- text-align: center;
24
- margin-bottom: 38px;
25
- }
26
- .abstract { /* 摘要和关键词: 五号宋体 */
27
- font-size: 14px;
28
- text-align: justify;
29
- padding: 0 28px;
30
- &::before {
31
- content: '摘 要';
32
- font-weight: normal;
33
- font-family: '黑体', 'SimHei', '华文黑体', 'STHeiti';
34
- display: inline-block;
35
- margin-right: 14px;
36
- }
37
- }
38
- .keywords {
39
- font-size: 14px;
40
- margin-bottom: 31px;
41
- padding: 0 28px;
42
- &::before {
43
- content: '关键词';
44
- font-weight: normal;
45
- font-family: '黑体', 'SimHei', '华文黑体', 'STHeiti';
46
- display: inline-block;
47
- margin-right: 14px;
48
- }
49
- }
50
-
51
- h2 { /* 一级标题: 四号宋体 */
52
- font-size: 18px;
53
- font-weight: normal;
54
- margin: 7px 0;
55
- }
56
- h3 { /* 二级标题: 五号黑体 */
57
- font-size: 14px;
58
- font-weight: normal;
59
- font-family: '黑体', 'SimHei', '华文黑体', 'STHeiti';
60
- margin: 5px 0;
61
- }
62
- h4 { /* 三级标题: 五号黑体 */
63
- font-size: 14px;
64
- font-weight: normal;
65
- font-family: '黑体', 'SimHei', '华文黑体', 'STHeiti';
66
- margin: 3px 0;
67
- }
68
-
69
- p { /* 正文: 五号宋体 */
70
- font-size: 14px;
71
- text-indent: 24px;
72
- text-align: justify;
73
- }
74
-
75
- h5 { /* "参考文献": 五号黑体 */
76
- font-size: 14px;
77
- font-weight: normal;
78
- font-family: '黑体', 'SimHei', '华文黑体', 'STHeiti';
79
- text-align: center;
80
- margin-bottom: 7px;
81
- margin-top: 20px;
82
- }
83
- ul, ol { /* 参考文献的项目: 小五号宋体 */
84
- list-style-type: none;
85
- padding: 0;
86
- & > li {
87
- font-size: 12px;
88
- margin: 6px 0;
89
- text-align: justify;
90
- text-indent: -24px;
91
- padding-left: 24px;
92
- }
93
- & a {
94
- text-decoration: none;
95
- color: black;
96
- }
97
- }
98
-
99
- img {
100
- display: block;
101
- max-width: 100%;
102
- margin: 0 auto;
103
- margin-top: 10px;
104
- }
105
-
106
- blockquote, blockquote > p { /* 图片和表格的标题: 小五号宋体 */
107
- font-size: 12px;
108
- font-weight: normal;
109
- text-align: center;
110
- margin: 0;
111
- }
112
- blockquote > p { margin: 6px 0; }
113
- table { /* 表格: 小五号宋体 */
114
- font-size: 12px;
115
- position: relative;
116
- border-top: 1px solid black;
117
- border-bottom: 1px solid black;
118
- width: 100%;
119
- max-width: 100%;
120
- margin: 0 auto;
121
- margin-bottom: 10px;
122
- & th, & td {
123
- font-weight: normal;
124
- }
125
- & thead::after { /* 用来做三线表中间的横线 */
126
- content: '';
127
- display: block;
128
- position: absolute;
129
- border-top: 1px solid #00000060;
130
- width: 100%;
131
- }
132
- }
133
-
134
- b, strong { /* 加粗按黑体处理 */
135
- font-weight: normal;
136
- font-family: '黑体', 'SimHei', '华文黑体', 'STHeiti';
137
- }
package/theme/aps/aps.ts DELETED
@@ -1,122 +0,0 @@
1
- import type { MarkdownPaperTheme } from '../theme'
2
- import type { PDFOptions } from 'puppeteer'
3
- import fs from 'node:fs'
4
- import path from 'node:path'
5
-
6
- export class APS implements MarkdownPaperTheme {
7
-
8
- css: string
9
- preParseMarkdown: (md: string) => Promise<string>
10
- preParseHTML: (html: string) => Promise<string>
11
- script: () => void
12
- pdfOptions: PDFOptions
13
-
14
- constructor(
15
- args: string[] = [],
16
- ) {
17
-
18
- // 默认自定义参数
19
- let showTitle: boolean = false
20
- let hideFooter: boolean = false
21
- let zhPunctuation: boolean = false
22
- let enPunctuation: boolean = false
23
- // 解析参数
24
- args.forEach(arg => {
25
- switch (arg.split('=')[0]) {
26
- case '--showTitle': showTitle = true; break
27
- case '--hideFooter': hideFooter = true; break
28
- case '--zhPunctuation': zhPunctuation = true; break
29
- case '--enPunctuation': enPunctuation = true; break
30
- }
31
- })
32
-
33
- // css
34
- this.css = fs.readFileSync(path.resolve(import.meta.dirname, 'aps.css'), 'utf-8')
35
-
36
- // preParseMarkdown
37
- this.preParseMarkdown = async (md: string): Promise<string> => {
38
- // 作者
39
- md = md.replace(/#author# (.*)/mg, '<div class="author">$1</div>')
40
- // 单位
41
- md = md.replace(/#school# (.*)/mg, '<div class="school">$1</div>')
42
- // 关键词
43
- md = md.replace(/#keywords# (.*)/mg, '<div class="keywords">$1</div>')
44
- // 摘要
45
- md = md.replace(/#abstract# (.*)/mg, '<div class="abstract">$1</div>')
46
- // 返回处理后的字符串
47
- return md
48
- }
49
-
50
- // preParseHTML
51
- this.preParseHTML = async (html: string): Promise<string> => {
52
- // 把包裹图片的 p 标签去掉
53
- html = html.replace(/<p><img (.*?)><\/p>/g, '<img $1>')
54
- // 返回处理后的字符串
55
- return html
56
- }
57
-
58
- // script
59
- // 替换标点
60
- if (zhPunctuation && !enPunctuation) {
61
- this.script = () => {
62
- const nodes = document.querySelectorAll('body > p, .abstract, .keywords, .author, .school')
63
- nodes.forEach(node => {
64
- let text = node.textContent ?? ''
65
- // 替换中英文逗号和句号
66
- text = text.replace(/, /g, ',').replace(/\. /g, '。').replace(/\.$/, '。')
67
- // 替换中英文冒号
68
- text = text.replace(/: /g, ':')
69
- // 替换中英文分号
70
- text = text.replace(/; /g, ';')
71
- // 替换中英文感叹号
72
- text = text.replace(/! /g, '!').replace(/!$/, '!')
73
- // 替换中英文问号
74
- text = text.replace(/\? /g, '?').replace(/\?$/, '?')
75
- // 替换中英文括号
76
- text = text.replace(/ \(/g, '(').replace(/\) /g, ')')
77
- // 恢复et al.,
78
- text = text.replace(/et al\.,/g, 'et al., ').replace(/et al。/g, 'et al. ')
79
- // 设置新文本
80
- node.textContent = text
81
- })
82
- }
83
- } else if (!zhPunctuation && enPunctuation) {
84
- this.script = () => {
85
- const nodes = document.querySelectorAll('body > p, .abstract, .keywords, .author, .school')
86
- nodes.forEach(node => {
87
- let text = node.textContent ?? ''
88
- // 替换中英文逗号和句号
89
- text = text.replace(/,/g, ', ').replace(/。 /g, '. ')
90
- // 替换中英文冒号
91
- text = text.replace(/:/g, ': ')
92
- // 替换中英文分号
93
- text = text.replace(/;/g, '; ')
94
- // 替换中英文感叹号
95
- text = text.replace(/!/g, '! ')
96
- // 替换中英文问号
97
- text = text.replace(/?/g, '? ')
98
- // 替换中英文括号
99
- text = text.replace(/(/g, ' (').replace(/)/g, ') ')
100
- // 设置新文本
101
- node.textContent = text
102
- })
103
- }
104
- } else {
105
- this.script = () => {}
106
- }
107
-
108
- // pdfOptions
109
- this.pdfOptions = {
110
- format: 'A4',
111
- margin: {
112
- top: '2cm',
113
- right: '2.5cm',
114
- bottom: '2cm',
115
- left: '2.5cm'
116
- },
117
- displayHeaderFooter: showTitle || !hideFooter,
118
- headerTemplate: showTitle ? `<div style="font-size: 9px; font-family: 'SimSun'; color: #333; padding: 5px; margin-left: 0.6cm;"> <span class="title"></span> </div>` : `<div></div>`,
119
- footerTemplate: hideFooter ? `<div></div>` : `<div style="font-size: 9px; font-family: 'SimSun'; color: #333; padding: 5px; margin: 0 auto;">第 <span class="pageNumber"></span> 页 / 共 <span class="totalPages"></span> 页</div>`,
120
- }
121
- }
122
- }
package/theme/theme.ts DELETED
@@ -1,33 +0,0 @@
1
- import type { PDFOptions } from 'puppeteer'
2
-
3
- export interface MarkdownPaperTheme {
4
- /**
5
- * css 样式
6
- * 不含 \<style>\</style>
7
- */
8
- css: string
9
- /**
10
- * 预处理 markdown 字符串
11
- * 用于转换自定义标签等
12
- * @param md markdown 字符串
13
- * @returns 转换后的 markdown 字符串
14
- */
15
- preParseMarkdown(md: string): Promise<string>
16
- /**
17
- * 预处理 html 字符串
18
- * 将在保存 html 文件前调用
19
- * @param html html 字符串
20
- * @returns 转换后的 html 字符串
21
- */
22
- preParseHTML(html: string): Promise<string>
23
- /**
24
- * 在网页中要执行的函数
25
- * 将在保存 html 文件后调用
26
- */
27
- script(): void
28
- /**
29
- * PDF 参数
30
- * 无需设置路径
31
- */
32
- pdfOptions: PDFOptions
33
- }