forked from ystyle/kaf-cli
-
Notifications
You must be signed in to change notification settings - Fork 1
/
convert.go
421 lines (404 loc) · 12.4 KB
/
convert.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
package kafcli
import (
"bufio"
"bytes"
"errors"
"flag"
"fmt"
"golang.org/x/net/html/charset"
"golang.org/x/text/encoding"
"golang.org/x/text/encoding/simplifiedchinese"
"golang.org/x/text/transform"
_ "image/jpeg"
_ "image/png"
"io"
"io/ioutil"
"os"
"path/filepath"
"regexp"
"runtime"
"strings"
"time"
"unicode/utf8"
)
type Book struct {
Filename string // 目录
Bookname string // 书名
Match string // 正则
VolumeMatch string // 卷匹配规则
Author string // 作者
Max uint // 标题最大字数
Indent uint // 段落缩进字段
Align string // 标题对齐方式
UnknowTitle string // 未知章节名称
Cover string // 封面图片
CoverOrlyColor string // 生成封面图片的颜色
CoverOrlyIdx int // 生成封面图片的动物
Font string // 嵌入字体
Bottom string // 段阿落间距
LineHeight string // 行高
Tips bool // 是否添加教程文本
Lang string // 设置语言
Out string // 输出文件名
Format string // 书籍格式
SectionList []Section // 章节
Decoder *encoding.Decoder
PageStylesFile string
Reg *regexp.Regexp
VolumeReg *regexp.Regexp
version string
}
type Section struct {
Title string
Content string
Sections []Section
}
type Converter interface {
Build(book Book) error
}
const (
htmlPStart = `<p class="content">`
htmlPEnd = "</p>"
htmlTitleStart = `<h3 class="title">`
mobiTtmlTitleStart = `<h3 style="text-align:%s;">`
htmlTitleEnd = "</h3>"
VolumeMatch = "^第[0-9一二三四五六七八九十零〇百千两 ]+[卷部]"
DefaultMatchTips = "^第[0-9一二三四五六七八九十零〇百千两 ]+[章回节集卷部]|^[Ss]ection.{1,20}$|^[Cc]hapter.{1,20}$|^[Pp]age.{1,20}$|^\\d{1,4}$|^\\d+、|^引子$|^楔子$|^章节目录|^章节|^序章"
cssContent = `
.title {text-align:%s}
.content {
margin-bottom: %s;
margin-top: 0;
text-indent: %dem;
%s
}
`
Tutorial = `本书由kaf-cli生成: <br/>
制作教程: <a href='https://ystyle.top/2019/12/31/txt-converto-epub-and-mobi/'>https://ystyle.top/2019/12/31/txt-converto-epub-and-mobi</a>
`
)
func NewBookSimple(filename string) (*Book, error) {
book := Book{
Filename: filename,
}
book.SetDefault()
return &book, nil
}
func NewBookArgs() *Book {
var book Book
flag.StringVar(&book.Filename, "filename", "", "txt 文件名")
flag.StringVar(&book.Bookname, "bookname", "", "书名: 默认为txt文件名")
flag.StringVar(&book.Author, "author", "YSTYLE", "作者")
flag.StringVar(&book.Match, "match", "", "匹配标题的正则表达式, 不写可以自动识别, 如果没生成章节就参考教程。例: -match 第.{1,8}章 表示第和章字之间可以有1-8个任意文字")
flag.StringVar(&book.VolumeMatch, "volume-match", VolumeMatch, "卷匹配规则,设置为false可以禁用卷识别")
flag.StringVar(&book.UnknowTitle, "unknow-title", "章节正文", "未知章节默认名称")
flag.StringVar(&book.Cover, "cover", "cover.png", "封面图片可为: 本地图片, 和orly。 设置为orly时生成orly风格的封面, 需要连接网络。")
flag.StringVar(&book.CoverOrlyColor, "cover-orly-color", "", "orly封面的主题色, 可以为1-16和hex格式的颜色代码, 不填时随机")
flag.IntVar(&book.CoverOrlyIdx, "cover-orly-idx", -1, "orly封面的动物, 可以为0-41, 不填时随机, 具体图案可以查看: https://orly.nanmu.me")
flag.UintVar(&book.Max, "max", 35, "标题最大字数")
flag.UintVar(&book.Indent, "indent", 2, "段落缩进字数")
flag.StringVar(&book.Align, "align", GetEnv("KAF_CLI_ALIGN", "center"), "标题对齐方式: left、center、righ。环境变量KAF_CLI_ALIGN可修改默认值")
flag.StringVar(&book.Bottom, "bottom", "1em", "段落间距(单位可以为em、px)")
flag.StringVar(&book.LineHeight, "line-height", "", "行高(用于设置行间距, 默认为1.5rem)")
flag.StringVar(&book.Font, "font", "", "嵌入字体, 之后epub的正文都将使用该字体")
flag.StringVar(&book.Lang, "lang", GetEnv("KAF_CLI_LANG", "zh"), "设置语言: en,de,fr,it,es,zh,ja,pt,ru,nl。环境变量KAF_CLI_LANG可修改默认值")
flag.StringVar(&book.Format, "format", GetEnv("KAF_CLI_FORMAT", "all"), "书籍格式: all、epub、mobi、azw3。环境变量KAF_CLI_FORMAT可修改默认值")
flag.StringVar(&book.Out, "out", "", "输出文件名,不需要包含格式后缀")
flag.BoolVar(&book.Tips, "tips", true, "添加本软件教程")
flag.Parse()
return &book
}
func (book *Book) SetDefault() {
book.Match = defaultString(book.Match, DefaultMatchTips)
book.VolumeMatch = defaultString(book.VolumeMatch, VolumeMatch)
book.Author = defaultString(book.Author, "YSTYLE")
book.UnknowTitle = defaultString(book.UnknowTitle, "章节正文")
book.Max = defalutInt(book.Max, 35)
book.Indent = defalutInt(book.Indent, 2)
book.Align = defaultString(book.Align, GetEnv("KAF_CLI_ALIGN", "center"))
book.Cover = defaultString(book.Cover, "cover.png")
book.Bottom = defaultString(book.Bottom, "1em")
book.Lang = defaultString(book.Lang, GetEnv("KAF_CLI_LANG", "zh"))
book.Format = defaultString(book.Format, GetEnv("KAF_CLI_FORMAT", "all"))
}
func (book *Book) Check(version string) error {
book.version = version
if book.Filename == "" {
fmt.Println("错误: 文件名不能为空")
fmt.Println("软件版本: \t", version)
fmt.Println("简洁模式: \t把文件拖放到kaf-cli上")
fmt.Println("命令行简单模式: kaf-cli ebook.txt")
fmt.Println("\n以下为kaf-cli的全部参数")
flag.PrintDefaults()
if runtime.GOOS == "windows" {
time.Sleep(time.Second * 10)
}
os.Exit(0)
}
if !strings.HasSuffix(book.Filename, ".txt") {
return errors.New("不是txt文件")
}
// 通过文件名解析书名
reg, _ := regexp.Compile(`《(.*)》.*作者[::](.*).txt`)
if reg.MatchString(book.Filename) {
group := reg.FindAllStringSubmatch(book.Filename, -1)
if len(group) == 1 && len(group[0]) >= 3 {
if book.Bookname == "" {
book.Bookname = group[0][1]
}
if book.Author == "" || book.Author == "YSTYLE" {
book.Author = group[0][2]
}
}
}
if book.Bookname == "" {
book.Bookname = strings.Split(filepath.Base(book.Filename), ".")[0]
}
if book.Out == "" {
book.Out = book.Bookname
}
book.Lang = parseLang(book.Lang)
switch book.Cover {
case "none":
book.Cover = ""
case "gen", "orly":
cover, err := GenCover(book.Bookname, book.Author, book.CoverOrlyColor, book.CoverOrlyIdx)
if err != nil {
panic(err)
}
book.Cover = cover
default:
if exists, _ := isExists(book.Cover); !exists {
book.Cover = ""
}
}
// 编译正则表达式
if book.Match == "" {
book.Match = DefaultMatchTips
}
reg, err := regexp.Compile(book.Match)
if err != nil {
return fmt.Errorf("生成匹配规则出错: %s\n%s\n", book.Match, err.Error())
}
book.Reg = reg
reg2, err := regexp.Compile(book.VolumeMatch)
if err != nil {
return fmt.Errorf("生成匹配规则出错: %s\n%s\n", book.VolumeMatch, err.Error())
}
book.VolumeReg = reg2
return nil
}
func (book *Book) readBuffer(filename string) *bufio.Reader {
f, err := os.Open(filename)
if err != nil {
fmt.Println("读取文件出错: ", err.Error())
os.Exit(1)
}
temBuf := bufio.NewReader(f)
bs, _ := temBuf.Peek(1024)
encodig, encodename, _ := charset.DetermineEncoding(bs, "text/plain")
if encodename != "utf-8" {
f.Seek(0, 0)
bs, err := ioutil.ReadAll(f)
if err != nil {
fmt.Println("读取文件出错: ", err.Error())
os.Exit(1)
}
var buf bytes.Buffer
book.Decoder = encodig.NewDecoder()
if encodename == "windows-1252" {
book.Decoder = simplifiedchinese.GB18030.NewDecoder()
}
bs, _, _ = transform.Bytes(book.Decoder, bs)
buf.Write(bs)
return bufio.NewReader(&buf)
} else {
f.Seek(0, 0)
buf := bufio.NewReader(f)
return buf
}
}
func (book *Book) ToString() {
fmt.Println("转换信息:")
fmt.Println("软件版本:", book.version)
fmt.Println("文件名:\t", book.Filename)
fmt.Println("书籍书名:", book.Bookname)
fmt.Println("书籍作者:", book.Author)
if book.Cover != "" {
fmt.Println("书籍封面:", book.Cover)
}
fmt.Println("书籍语言:", book.Lang)
if book.Match == DefaultMatchTips {
fmt.Println("匹配条件:", "自动匹配")
} else {
fmt.Println("匹配条件:", book.Match)
}
fmt.Println("卷匹配条件:", book.VolumeMatch)
fmt.Println("转换格式:", book.Format)
fmt.Println()
}
func (book *Book) Parse() error {
var contentList []Section
fmt.Println("正在读取txt文件...")
start := time.Now()
buf := book.readBuffer(book.Filename)
var title string
var content bytes.Buffer
for {
line, err := buf.ReadString('\n')
if err != nil {
if err == io.EOF {
if line != "" {
if line = strings.TrimSpace(line); line != "" {
addPart(&content, line)
}
}
contentList = append(contentList, Section{
Title: title,
Content: content.String(),
})
content.Reset()
break
}
return fmt.Errorf("读取文件出错: %w", err)
}
line = strings.TrimSpace(line)
line = strings.ReplaceAll(line, "<", "<")
line = strings.ReplaceAll(line, ">", ">")
// 空行直接跳过
if len(line) == 0 {
continue
}
// 处理标题
if utf8.RuneCountInString(line) <= int(book.Max) &&
(book.Reg.MatchString(line) || book.VolumeReg.MatchString(line)) {
if title == "" {
title = book.UnknowTitle
}
if content.Len() > 0 || title != book.UnknowTitle {
contentList = append(contentList, Section{
Title: title,
Content: content.String(),
})
}
title = line
content.Reset()
continue
}
addPart(&content, line)
}
// 没识别到章节又没识别到 EOF 时,把所有的内容写到最后一章
if content.Len() != 0 {
if title == "" {
title = "章节正文"
}
contentList = append(contentList, Section{
Title: title,
Content: content.String(),
})
}
var sectionList []Section
var volumeSection *Section
for _, section := range contentList {
if book.VolumeMatch != "false" && book.VolumeReg.MatchString(section.Title) {
if volumeSection != nil {
sectionList = append(sectionList, *volumeSection)
volumeSection = nil
}
temp := section
volumeSection = &temp
} else {
if volumeSection == nil {
sectionList = append(sectionList, section)
} else {
volumeSection.Sections = append(volumeSection.Sections, section)
}
}
}
// 如果有最后一卷,添加到章节列表
if volumeSection != nil {
sectionList = append(sectionList, *volumeSection)
volumeSection = nil
}
end := time.Now().Sub(start)
fmt.Println("读取文件耗时:", end)
fmt.Println("匹配章节:", sectionCount(sectionList))
// 添加提示
if book.Tips {
tuorialSection := Section{
Title: "制作说明",
Content: Tutorial,
}
sectionList = append([]Section{tuorialSection}, sectionList...)
sectionList = append(sectionList, tuorialSection)
}
book.SectionList = sectionList
return nil
}
func sectionCount(sections []Section) int {
var count int
for _, section := range sections {
count += 1 + len(section.Sections)
}
return count
}
func (book *Book) Convert() {
start := time.Now()
// 解析文本
fmt.Println()
// 判断要生成的格式
var isEpub, isMobi, isAzw3 bool
switch book.Format {
case "epub":
isEpub = true
case "mobi":
isEpub = true
isMobi = true
case "azw3":
isAzw3 = true
default:
isEpub = true
isMobi = true
isAzw3 = true
}
hasKinldegen := lookKindlegen()
if book.Format == "mobi" && hasKinldegen == "" {
isEpub = false
}
var convert Converter
// 生成epub
if isEpub {
convert = EpubConverter{}
convert.Build(*book)
fmt.Println()
}
// 生成azw3格式
if isAzw3 {
convert = Azw3Converter{}
// 生成kindle格式
convert.Build(*book)
}
// 生成mobi格式
if isMobi {
if hasKinldegen == "" {
convert = MobiConverter{}
convert.Build(*book)
} else {
converToMobi(fmt.Sprintf("%s.epub", book.Out), book.Lang)
}
}
end := time.Now().Sub(start)
fmt.Println("\n转换完成! 总耗时:", end)
}
func addPart(buff *bytes.Buffer, content string) {
if strings.HasSuffix(content, "==") ||
strings.HasSuffix(content, "**") ||
strings.HasSuffix(content, "--") ||
strings.HasSuffix(content, "//") {
buff.WriteString(content)
return
}
buff.WriteString(htmlPStart)
buff.WriteString(content)
buff.WriteString(htmlPEnd)
}