Skip to content

Commit aa6d8f0

Browse files
kiki830621claude
andcommitted
feat: Add macdoc bib subcommand for .bib → APA 7 conversion
Supports to-html, to-md, and list subcommands. Converts BibLaTeX files to APA 7 formatted HTML or Markdown reference lists with key filtering, CSS style options, and full HTML document output. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 5d6e605 commit aa6d8f0

4 files changed

Lines changed: 197 additions & 7 deletions

File tree

Package.resolved

Lines changed: 6 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Package.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ let package = Package(
1515
.package(url: "https://github.com/PsychQuant/markdown-swift.git", from: "0.1.0"),
1616
.package(url: "https://github.com/PsychQuant/marker-swift.git", from: "0.1.0"),
1717
.package(name: "pdf-to-latex-swift", path: "packages/pdf-to-latex-swift"),
18+
.package(name: "APABibToHTML", path: "packages/apa-bib-to-html-swift"),
19+
.package(name: "APABibToMD", path: "packages/apa-bib-to-md-swift"),
1820
],
1921
targets: [
2022
.target(
@@ -33,6 +35,8 @@ let package = Package(
3335
.product(name: "WordToMDSwift", package: "word-to-md-swift"),
3436
"MarkerWordConverter",
3537
.product(name: "PDFToLaTeXCore", package: "pdf-to-latex-swift"),
38+
.product(name: "APABibToHTML", package: "APABibToHTML"),
39+
.product(name: "APABibToMD", package: "APABibToMD"),
3640
.product(name: "ArgumentParser", package: "swift-argument-parser"),
3741
]
3842
),

Sources/MacDocCLI/MacDoc+Bib.swift

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
import ArgumentParser
2+
import Foundation
3+
import APABibToHTML
4+
import APABibToMD
5+
import BiblatexAPA
6+
7+
// MARK: - Bib 子命令群
8+
extension MacDoc {
9+
struct Bib: ParsableCommand {
10+
static let configuration = CommandConfiguration(
11+
commandName: "bib",
12+
abstract: "BibLaTeX (.bib) → APA 7 格式轉換",
13+
subcommands: [ToHTML.self, ToMarkdown.self, List.self]
14+
)
15+
}
16+
}
17+
18+
// MARK: - bib to-html
19+
extension MacDoc.Bib {
20+
struct ToHTML: ParsableCommand {
21+
static let configuration = CommandConfiguration(
22+
commandName: "to-html",
23+
abstract: "將 .bib 轉換為 APA 7 HTML 參考文獻列表"
24+
)
25+
26+
@Argument(help: "輸入 .bib 檔案路徑")
27+
var input: String
28+
29+
@Option(name: [.short, .long], help: "輸出 .html 檔案路徑(預設為 stdout)")
30+
var output: String?
31+
32+
@Flag(name: .long, help: "輸出完整 HTML 文件(含 <html>, <head>, CSS)")
33+
var full: Bool = false
34+
35+
@Option(name: .long, help: "CSS 風格:minimal(學術)或 web(現代)")
36+
var css: CSSStyle = .web
37+
38+
@Option(name: .long, help: "只輸出指定 entry key(可多次使用)")
39+
var key: [String] = []
40+
41+
mutating func run() throws {
42+
let entries = try loadEntries(from: input, filterKeys: key)
43+
44+
let html: String
45+
if full {
46+
html = buildFullHTML(entries: entries)
47+
} else {
48+
let cssString = css == .minimal ? APACSS.minimal : APACSS.web
49+
html = BibToAPAHTMLFormatter.formatReferenceListWithCSS(entries, css: cssString)
50+
}
51+
52+
try writeOutput(html, to: output)
53+
}
54+
55+
private func buildFullHTML(entries: [BibEntry]) -> String {
56+
let cssString = css == .minimal ? APACSS.minimal : APACSS.web
57+
let body = BibToAPAHTMLFormatter.formatReferenceList(entries)
58+
return """
59+
<!DOCTYPE html>
60+
<html lang="en">
61+
<head>
62+
<meta charset="UTF-8">
63+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
64+
<title>APA 7 References</title>
65+
<style>
66+
\(cssString)
67+
</style>
68+
</head>
69+
<body>
70+
<div class="apa-reference-list">
71+
\(body)
72+
</div>
73+
</body>
74+
</html>
75+
"""
76+
}
77+
}
78+
}
79+
80+
// MARK: - bib to-md
81+
extension MacDoc.Bib {
82+
struct ToMarkdown: ParsableCommand {
83+
static let configuration = CommandConfiguration(
84+
commandName: "to-md",
85+
abstract: "將 .bib 轉換為 APA 7 Markdown 參考文獻列表"
86+
)
87+
88+
@Argument(help: "輸入 .bib 檔案路徑")
89+
var input: String
90+
91+
@Option(name: [.short, .long], help: "輸出 .md 檔案路徑(預設為 stdout)")
92+
var output: String?
93+
94+
@Flag(name: .long, help: "加入 '## References' 標題")
95+
var heading: Bool = false
96+
97+
@Option(name: .long, help: "只輸出指定 entry key(可多次使用)")
98+
var key: [String] = []
99+
100+
mutating func run() throws {
101+
let entries = try loadEntries(from: input, filterKeys: key)
102+
103+
var md = BibToAPAFormatter.formatReferenceList(entries)
104+
if heading {
105+
md = "## References\n\n" + md
106+
}
107+
108+
try writeOutput(md, to: output)
109+
}
110+
}
111+
}
112+
113+
// MARK: - bib list
114+
extension MacDoc.Bib {
115+
struct List: ParsableCommand {
116+
static let configuration = CommandConfiguration(
117+
commandName: "list",
118+
abstract: "列出 .bib 檔案中的所有 entry keys"
119+
)
120+
121+
@Argument(help: "輸入 .bib 檔案路徑")
122+
var input: String
123+
124+
@Flag(name: .long, help: "顯示 entry type")
125+
var showType: Bool = false
126+
127+
mutating func run() throws {
128+
let inputURL = URL(fileURLWithPath: input)
129+
guard FileManager.default.fileExists(atPath: inputURL.path) else {
130+
throw ValidationError("找不到輸入檔案: \(input)")
131+
}
132+
133+
let bibFile = try BibParser.parse(filePath: inputURL.path)
134+
for entry in bibFile.entries {
135+
if showType {
136+
print("\(entry.key)\t\(entry.entryType)")
137+
} else {
138+
print(entry.key)
139+
}
140+
}
141+
FileHandle.standardError.write(
142+
Data("\n\(bibFile.entries.count) 筆 entries\n".utf8)
143+
)
144+
}
145+
}
146+
}
147+
148+
// MARK: - CSS Style Enum
149+
enum CSSStyle: String, ExpressibleByArgument, CaseIterable {
150+
case minimal
151+
case web
152+
}
153+
154+
// MARK: - Shared Helpers
155+
156+
private func loadEntries(from path: String, filterKeys: [String]) throws -> [BibEntry] {
157+
let inputURL = URL(fileURLWithPath: path)
158+
guard FileManager.default.fileExists(atPath: inputURL.path) else {
159+
throw ValidationError("找不到輸入檔案: \(path)")
160+
}
161+
162+
let bibFile = try BibParser.parse(filePath: inputURL.path)
163+
var entries = bibFile.entries
164+
165+
if !filterKeys.isEmpty {
166+
let keySet = Set(filterKeys)
167+
entries = entries.filter { keySet.contains($0.key) }
168+
if entries.isEmpty {
169+
throw ValidationError("找不到指定的 entry keys: \(filterKeys.joined(separator: ", "))")
170+
}
171+
}
172+
173+
return entries
174+
}
175+
176+
private func writeOutput(_ content: String, to outputPath: String?) throws {
177+
if let outputPath = outputPath {
178+
let outputURL = URL(fileURLWithPath: outputPath)
179+
try content.write(to: outputURL, atomically: true, encoding: .utf8)
180+
FileHandle.standardError.write(
181+
Data("已寫入: \(outputURL.path)\n".utf8)
182+
)
183+
} else {
184+
print(content)
185+
}
186+
}

Sources/MacDocCLI/MacDoc.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ struct MacDoc: AsyncParsableCommand {
1111
commandName: "macdoc",
1212
abstract: "原生 macOS 文件處理工具",
1313
version: "0.3.0",
14-
subcommands: [Word.self, PDF.self, Config.self]
14+
subcommands: [Word.self, PDF.self, Bib.self, Config.self]
1515
)
1616
}
1717

0 commit comments

Comments
 (0)