├── .gitignore ├── README.md ├── apple.go ├── awz3.go ├── common.go ├── css.go ├── epub.go ├── go.mod ├── go.sum ├── image.go ├── main.go ├── mobi.go └── pdf.go /.gitignore: -------------------------------------------------------------------------------- 1 | *.epub 2 | *.mobi 3 | *.pdf 4 | *.azw3 5 | /build* 6 | /book 7 | /-* 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This project is used to produce Go 101 eBooks. 2 | The code in this repository is ugly and full of bad practices. 3 | Don't expect to learn some good programming habits from the code. 4 | 5 | Please install `calibre` before creating ebooks with formats other than epub. 6 | 7 | Program options: 8 | ``` 9 | -book-project-dir : the path to Go 101 book project. 10 | -book-version : the version of the book, presented in the names of output files. 11 | -target : output format. [epub | azw3 | mobi | apple | pdf | print | all] 12 | ``` 13 | 14 | For any format, an epub file will be produced firstly, 15 | then the epub version will be converted to the target format 16 | by using the calibre GUI or command line tools. 17 | So please install the calibre software before running this program 18 | (except for producing epub books only). 19 | 20 | 21 | Some examples: 22 | 23 | ``` 24 | $ export BookVersion=v1.12.c.7 25 | $ export BookProjectDir=/home/go/src/github.com/go101/go101 26 | 27 | $ go run . -target=epub -book-version=$BookVersion -book-project-dir=$BookProjectDir 28 | $ go run . -target=azw3 -book-version=$BookVersion -book-project-dir=$BookProjectDir 29 | $ go run . -target=apple -book-version=$BookVersion -book-project-dir=$BookProjectDir 30 | $ go run . -target=pdf -book-version=$BookVersion -book-project-dir=$BookProjectDir 31 | $ go run . -target=print -book-version=$BookVersion -book-project-dir=$BookProjectDir 32 | $ go run . -target=all -book-version=$BookVersion -book-project-dir=$BookProjectDir 33 | ``` 34 | -------------------------------------------------------------------------------- /apple.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "os" 6 | 7 | "github.com/bmaupin/go-epub" 8 | ) 9 | 10 | func genetateAppleFile(bookProjectDir, bookVersion, coverImagePath string) string { 11 | var e *epub.Epub 12 | var outFilename string 13 | var indexArticleTitle string 14 | var bookWebsite string 15 | var engVersion bool 16 | 17 | projectName := confirmBookProjectName(bookProjectDir) 18 | switch projectName { 19 | default: 20 | log.Fatal("unknow book porject: ", projectName) 21 | case "Go101": 22 | e = epub.NewEpub("Go 101") 23 | e.SetAuthor("Tapir Liu") 24 | indexArticleTitle = "Contents" 25 | bookWebsite = "https://go101.org" 26 | engVersion = true 27 | outFilename = "Go101-" + bookVersion + ".apple.epub" 28 | case "Golang101": 29 | e = epub.NewEpub("Go语言101") 30 | e.SetAuthor("老貘") 31 | indexArticleTitle = "目录" 32 | bookWebsite = "https://gfw.go101.org" 33 | engVersion = false 34 | outFilename = "Golang101-" + bookVersion + ".apple.epub" 35 | } 36 | 37 | cssFilename := "all.css" 38 | tempCssFile := mustCreateTempFile("all*.css", []byte(AppleCSS)) 39 | defer os.Remove(tempCssFile) 40 | cssPath, err := e.AddCSS(tempCssFile, cssFilename) 41 | if err != nil { 42 | log.Fatalln("add css", cssFilename, "failed:", err) 43 | } 44 | 45 | writeEpub_Go101(outFilename, e, -1, bookWebsite, projectName, indexArticleTitle, bookProjectDir, coverImagePath, cssPath, "apple", engVersion) 46 | log.Println("Create", outFilename, "done!") 47 | 48 | return outFilename 49 | } 50 | -------------------------------------------------------------------------------- /awz3.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "os" 6 | 7 | "github.com/bmaupin/go-epub" 8 | ) 9 | 10 | func genetateAzw3File(bookProjectDir, bookVersion, coverImagePath string) { 11 | genetateAzw3FileForBook(bookProjectDir, bookVersion, coverImagePath, 0) 12 | 13 | //genetateAzw3FileForBook(bookProjectDir, bookVersion, 1) 14 | //genetateAzw3FileForBook(bookProjectDir, bookVersion, 2) 15 | } 16 | 17 | // zero bookId means all. 18 | func genetateAzw3FileForBook(bookProjectDir, bookVersion, coverImagePath string, bookId int) { 19 | var e *epub.Epub 20 | var outFilename string 21 | var indexArticleTitle string 22 | var bookWebsite string 23 | var engVersion bool 24 | var css string 25 | 26 | projectName := confirmBookProjectName(bookProjectDir) 27 | switch projectName { 28 | default: 29 | log.Fatal("unknow book porject: ", projectName) 30 | case "Go101": 31 | if bookId == 0 { 32 | e = epub.NewEpub("Go 101") 33 | outFilename = "Go101-" + bookVersion + ".azw3" 34 | } else if bookId == 1 { 35 | e = epub.NewEpub("Go 101 (Type System)") 36 | outFilename = "Go101-" + bookVersion + "-types.azw3" 37 | } else if bookId == 2 { 38 | e = epub.NewEpub("Go 101 (Extended)") 39 | outFilename = "Go101-" + bookVersion + "-extended.azw3" 40 | } else { 41 | log.Fatal("unknown book id: ", bookId) 42 | } 43 | e.SetAuthor("Tapir Liu") 44 | bookWebsite = "https://go101.org" 45 | engVersion = true 46 | indexArticleTitle = "Contents" 47 | css = Awz3CSS 48 | case "Golang101": 49 | if bookId == 0 { 50 | e = epub.NewEpub("Go语言101") 51 | outFilename = "Golang101-" + bookVersion + ".azw3" 52 | } else if bookId == 1 { 53 | e = epub.NewEpub("Go语言101(类型系统)") 54 | outFilename = "Golang101" + bookVersion + "-types.azw3" 55 | } else if bookId == 2 { 56 | e = epub.NewEpub("Go语言101(扩展阅读)") 57 | outFilename = "Golang101-" + bookVersion + "-extended.azw3" 58 | } else { 59 | log.Fatal("unknown book id: ", bookId) 60 | } 61 | e.SetAuthor("老貘") 62 | bookWebsite = "https://gfw.go101.org" 63 | engVersion = false 64 | indexArticleTitle = "目录" 65 | css = Awz3CSS_Chinese 66 | } 67 | 68 | cssFilename := "all.css" 69 | tempCssFile := mustCreateTempFile("all*.css", []byte(css)) 70 | defer os.Remove(tempCssFile) 71 | cssPath, err := e.AddCSS(tempCssFile, cssFilename) 72 | if err != nil { 73 | log.Fatalln("add css", cssFilename, "failed:", err) 74 | } 75 | 76 | //tempOutFilename := outFilename + "*.epub" 77 | //tempOutFilename = mustCreateTempFile(tempOutFilename, nil) 78 | //defer os.Remove(tempOutFilename) 79 | tempOutFilename := outFilename + ".epub" 80 | 81 | writeEpub_Go101(tempOutFilename, e, bookId, bookWebsite, projectName, indexArticleTitle, bookProjectDir, coverImagePath, cssPath, "azw3", engVersion) 82 | 83 | runShellCommand(".", "ebook-convert", tempOutFilename, outFilename) 84 | runShellCommand(".", "ebook-convert", tempOutFilename, outFilename+".mobi") 85 | log.Println("Create", outFilename, "done!") 86 | } 87 | -------------------------------------------------------------------------------- /common.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "encoding/base64" 6 | "fmt" 7 | "io/ioutil" 8 | "log" 9 | "os" 10 | "os/exec" 11 | "path/filepath" 12 | "strconv" 13 | "strings" 14 | ) 15 | 16 | func confirmBookProjectName(bookProjectDir string) string { 17 | checkFileExistence := func(filename string) bool { 18 | info, err := os.Stat(filepath.Join(bookProjectDir, filename)) 19 | return err == nil && !info.IsDir() 20 | } 21 | if checkFileExistence("go101.go") { 22 | return "Go101" 23 | } 24 | if checkFileExistence("golang101.go") { 25 | return "Golang101" 26 | } 27 | return "" 28 | } 29 | 30 | func mustCreateTempFile(pattern string, content []byte) string { 31 | tmpfile, err := ioutil.TempFile("", pattern) 32 | if err != nil { 33 | log.Fatal(err) 34 | } 35 | 36 | if _, err := tmpfile.Write(content); err != nil { 37 | log.Fatal(err) 38 | } 39 | if err := tmpfile.Close(); err != nil { 40 | log.Fatal(err) 41 | } 42 | 43 | return tmpfile.Name() 44 | } 45 | 46 | func mustParseImageData(s string) []byte { 47 | decoded, err := base64.StdEncoding.DecodeString(s) 48 | if err != nil { 49 | log.Fatal(err) 50 | } 51 | return decoded 52 | } 53 | 54 | func runShellCommand(rootPath, cmd string, args ...string) { 55 | runShellCommand2(false, rootPath, cmd, args...) 56 | } 57 | 58 | func runShellCommand2(needOutput bool, rootPath, cmd string, args ...string) []byte { 59 | log.Println(append([]string{cmd}, args...)) 60 | 61 | command := exec.Command(cmd, args...) 62 | command.Stdin = os.Stdin 63 | command.Stderr = os.Stderr 64 | command.Dir = rootPath 65 | 66 | if needOutput { 67 | o, err := command.Output() 68 | if err != nil { 69 | log.Fatal(err) 70 | } 71 | return o 72 | } else { 73 | command.Stdout = os.Stdout 74 | if err := command.Run(); err != nil { 75 | log.Fatal(err) 76 | } 77 | return nil 78 | } 79 | } 80 | 81 | type Article struct { 82 | Filename string 83 | Title string 84 | Content []byte 85 | 86 | chapter, chapter2 string 87 | internalFilename []byte 88 | } 89 | 90 | //const ArticlesFolder = "articles" 91 | const ArticlesFolder = "fundamentals" 92 | 93 | func mustArticles(root string, engVersion bool) (index *Article, articles []*Article, chapterMapping map[string]*Article) { 94 | index = mustArticle(engVersion, -1, root, "pages", ArticlesFolder, "101.html") 95 | articles, chapterMapping = must101Articles(root, index, engVersion) 96 | //for _, a := range articles { 97 | // log.Println(a.Title) 98 | //} 99 | return 100 | } 101 | 102 | // The last path token is the filename. 103 | func mustArticle(engVersion bool, chapterNumber int, root string, pathTokens ...string) *Article { 104 | path := filepath.Join(root, filepath.Join(pathTokens...)) 105 | content, err := ioutil.ReadFile(path) 106 | if err != nil { 107 | log.Fatalln("read file("+path+") error:", err) 108 | } 109 | 110 | title := retrieveArticleTitle(content) 111 | if title == "" { 112 | log.Fatalln("title not found in file(" + path + ")") 113 | } 114 | 115 | chapter, chapter2 := "", "" 116 | if engVersion { 117 | chapter = fmt.Sprintf(" (§%d)", chapterNumber) 118 | chapter2 = fmt.Sprintf("§%d. ", chapterNumber) 119 | title = fmt.Sprintf("§%d. %s", chapterNumber, title) 120 | } else { 121 | chapter = fmt.Sprintf("(第%d章)", chapterNumber) 122 | chapter2 = fmt.Sprintf("第%d章:", chapterNumber) 123 | title = fmt.Sprintf("第%d章:%s", chapterNumber, title) 124 | } 125 | 126 | return &Article{ 127 | Filename: pathTokens[len(pathTokens)-1], 128 | Title: title, 129 | Content: content, 130 | 131 | chapter: chapter, 132 | chapter2: chapter2, 133 | } 134 | } 135 | 136 | const MaxLen = 256 137 | 138 | var H1, _H1 = []byte("") 139 | var TagSigns = [2]rune{'<', '>'} 140 | 141 | func retrieveArticleTitle(content []byte) string { 142 | j, i := -1, bytes.Index(content, H1) 143 | if i < 0 { 144 | return "" 145 | } 146 | 147 | i += len(H1) 148 | i2 := bytes.IndexByte(content[i:i+MaxLen], '>') 149 | if i2 < 0 { 150 | return "" 151 | } 152 | i += i2 + 1 153 | 154 | j = bytes.Index(content[i:i+MaxLen], _H1) 155 | if j < 0 { 156 | return "" 157 | } 158 | 159 | //return string(content[i-len(H1) : i+j+len(_H1)]) 160 | //return string(content[i : i+j]) 161 | 162 | title := string(bytes.TrimSpace(content[i : i+j])) 163 | k, s := 0, make([]rune, 0, MaxLen) 164 | for _, r := range title { 165 | if r == TagSigns[k] { 166 | k = (k + 1) & 1 167 | } else if k == 0 { 168 | s = append(s, r) 169 | } 170 | } 171 | return string(s) 172 | } 173 | 174 | var Anchor, _Anchor, LineToRemoveTag, endl = []byte(`
  • `), []byte(`(to remove)`), []byte("\n") 175 | var IndexContentStart, IndexContentEnd = []byte(``), []byte(``) 176 | 177 | func must101Articles(root string, indexArticle *Article, engVersion bool) (articles []*Article, chapterMapping map[string]*Article) { 178 | chapterMapping = make(map[string]*Article) 179 | 180 | content := indexArticle.Content 181 | i := bytes.Index(content, IndexContentStart) 182 | if i < 0 { 183 | log.Fatalf("%s not found", IndexContentStart) 184 | } 185 | i += len(IndexContentStart) 186 | content = content[i:] 187 | i = bytes.Index(content, IndexContentEnd) 188 | if i >= 0 { 189 | content = content[:i] 190 | } 191 | 192 | var buf bytes.Buffer 193 | for range [1000]struct{}{} { 194 | i = bytes.Index(content, LineToRemoveTag) 195 | if i < 0 { 196 | break 197 | } 198 | start := bytes.LastIndex(content[:i], endl) 199 | if start >= 0 { 200 | buf.Write(content[:start]) 201 | } 202 | end := bytes.Index(content[i:], endl) 203 | content = content[i:] 204 | if end < 0 { 205 | end = len(content) 206 | } 207 | content = content[end:] 208 | } 209 | buf.Write(content) 210 | 211 | // modify index content 212 | indexArticle.Content = buf.Bytes() 213 | 214 | // find all articles from links 215 | content = indexArticle.Content 216 | chapter := 0 217 | for range [1000]struct{}{} { 218 | i = bytes.Index(content, Anchor) 219 | if i < 0 { 220 | break 221 | } 222 | content = content[i+len(Anchor):] 223 | i = bytes.Index(content, _Anchor) 224 | if i < 0 { 225 | break 226 | } 227 | 228 | article := mustArticle(engVersion, chapter, root, "pages", ArticlesFolder, string(content[:i])) 229 | articles = append(articles, article) 230 | chapter++ 231 | 232 | if i := strings.Index(article.Filename, ".html"); i >= 0 { 233 | filename := article.Filename 234 | internalFilename := []byte(strings.ReplaceAll(filename, ".html", ".xhtml")) 235 | //internalFilename := href[:i] 236 | if internalFilename[0] >= '0' && internalFilename[0] <= '9' { 237 | internalFilename = append([]byte("go"), internalFilename...) 238 | } 239 | 240 | article.internalFilename = internalFilename 241 | 242 | chapterMapping[article.Filename] = article 243 | //log.Println(article.Filename, ":", string(article.internalFilename)) 244 | } 245 | 246 | content = content[i+len(_Anchor):] 247 | } 248 | 249 | return 250 | } 251 | 252 | /* 253 | func collectInternalArticles(content []byte) map[string][]byte { 254 | var m = make(map[string][]byte) 255 | 256 | for range [1000]struct{}{} { 257 | aStart := find(content, 0, A) 258 | if aStart < 0 { 259 | break 260 | } 261 | index := aStart+len(A) 262 | 263 | end := find(content, index, []byte(">")) 264 | if end < 0 { 265 | fatalError("a tag is incomplete", content[aStart:]) 266 | } 267 | end++ 268 | 269 | hrefStart := find(content[:end], index, Href) 270 | if hrefStart < 0 { 271 | //fatalError("a tag has not href", content[aStart:]) 272 | content = content[end:] 273 | continue 274 | } 275 | hrefStart += len(Href) 276 | 277 | quotaStart := find(content[:end], hrefStart, []byte(`"`)) 278 | if quotaStart < 0 { 279 | fatalError("a href is incomplete", content[aStart:]) 280 | } 281 | quotaStart++ 282 | 283 | quotaEnd := find(content[:end], quotaStart, []byte(`"`)) 284 | if quotaEnd < 0 { 285 | fatalError("a href is incomplete", content[aStart:]) 286 | } 287 | 288 | href := bytes.TrimSpace(content[quotaStart:quotaEnd]) 289 | if bytes.HasPrefix(href, []byte("http")) { 290 | 291 | } else if i := bytes.Index(href, []byte(".html")); i >= 0 { 292 | filename := string(href[:i+len(".html")]) 293 | internalFilename := []byte(strings.ReplaceAll(filename, ".html", ".xhtml")) 294 | //internalFilename := href[:i] 295 | if internalFilename[0] >= '0' && internalFilename[0] <= '9' { 296 | internalFilename = append([]byte("go"), internalFilename...) 297 | } 298 | m[filename] = internalFilename 299 | } 300 | 301 | content = content[end:] 302 | } 303 | 304 | return m 305 | } 306 | */ 307 | 308 | func find(content []byte, start int, s []byte) int { 309 | i := bytes.Index(content[start:], s) 310 | if i >= 0 { 311 | i += start 312 | } 313 | return i 314 | } 315 | 316 | func fatalError(err string, content []byte) { 317 | n := len(content) 318 | if n > 100 { 319 | n = 100 320 | } 321 | log.Fatalln(err, ":", string(content[:n])) 322 | } 323 | 324 | func wrapContentDiv(articles []*Article) { 325 | for _, article := range articles { 326 | var buf = bytes.NewBuffer(make([]byte, 0, len(article.Content)+100)) 327 | buf.WriteString(`
    `) 328 | buf.Write(article.Content) 329 | buf.WriteString(`
    `) 330 | 331 | article.Content = buf.Bytes() 332 | } 333 | } 334 | 335 | func calLineWidth(line string) int { 336 | var n int 337 | var lastI int 338 | for i, r := range line { 339 | if r == '&' { 340 | n -= 3 341 | } 342 | if k := i - lastI; k > 1 { 343 | n += 2 344 | } else if k == 1 { 345 | n++ 346 | } 347 | lastI = i 348 | } 349 | if k := len(line) - lastI; k > 1 { 350 | n += 2 351 | } else if k == 1 { 352 | n++ 353 | } 354 | return n 355 | } 356 | 357 | var Pre, _Pre = []byte(``) 358 | var Code, _Code = []byte(``) 359 | var tabSpaces = strings.Repeat(" ", 3) 360 | 361 | type Commend struct { 362 | slashStart int 363 | numSpaces int 364 | } 365 | 366 | var commends [1024]Commend 367 | 368 | func escapeCharactorWithinCodeTags(articles []*Article, target string) { 369 | for _, article := range articles { 370 | content := article.Content 371 | var buf = bytes.NewBuffer(make([]byte, 0, len(content)+10000)) 372 | for range [1000]struct{}{} { 373 | preStart := find(content, 0, Pre) 374 | if preStart < 0 { 375 | break 376 | } 377 | 378 | index := preStart + len(Pre) 379 | preClose := find(content, index, _Pre) 380 | if preClose < 0 { 381 | fatalError("pre tag is incomplete in article:"+article.Filename, content[index:]) 382 | } 383 | preEnd := preClose + len(_Pre) 384 | 385 | codeStart := find(content[:preClose], index, Code) 386 | if codeStart < 0 { 387 | buf.Write(content[:preEnd]) 388 | content = content[preEnd:] 389 | continue 390 | } 391 | 392 | programStart := find(content[:preClose], codeStart, []byte(">")) 393 | if programStart < 0 { 394 | fatalError("code start tag is incomplete in article:"+article.Filename, content[codeStart:]) 395 | } 396 | programStart++ 397 | 398 | codeClose := find(content[:preClose], programStart, _Code) 399 | if codeClose < 0 { 400 | fatalError("code tag doesn't match in article:"+article.Filename, content[:programStart]) 401 | } 402 | 403 | codeCloseEnd := find(content[:preClose], codeClose, []byte(">")) 404 | if codeCloseEnd < 0 { 405 | fatalError("code close tag is incomplete in article:"+article.Filename, content[:preClose]) 406 | } 407 | codeCloseEnd++ 408 | 409 | temp := string(content[programStart:codeClose]) 410 | 411 | // Cancelled for the experience of copy-paste from pdf is bad. 412 | // At least, it is a little better to paste spaces than to paste nothing. 413 | //if target != "pdf" && target != "print" { 414 | // for pdf, keep tabs 415 | temp = strings.ReplaceAll(temp, "\t", tabSpaces) 416 | //} 417 | 418 | temp = strings.ReplaceAll(temp, "&", "&") 419 | temp = strings.ReplaceAll(temp, "<", "<") 420 | temp = strings.ReplaceAll(temp, ">", ">") 421 | 422 | temp = strings.ReplaceAll(temp, "&", "&") 423 | temp = strings.ReplaceAll(temp, "<", "<") 424 | temp = strings.ReplaceAll(temp, ">", ">") 425 | 426 | var mustLineNumbers = bytes.Index(content[preStart:codeStart], []byte("must-line-numbers")) >= 0 427 | var disableLineNumbers = bytes.Index(content[preStart:codeStart], []byte("disable-line-numbers111")) >= 0 || 428 | bytes.Index(content[preStart:codeStart], []byte("must-not-line-numbers-on-kindle")) >= 0 429 | if disableLineNumbers { 430 | buf.Write(content[:preStart]) 431 | buf.Write(bytes.ReplaceAll(content[preStart:codeStart], []byte("line-numbers"), []byte("xxx-yyy"))) 432 | buf.Write(content[codeStart:programStart]) 433 | buf.WriteString(temp) 434 | buf.Write(content[codeClose:preEnd]) 435 | } else { 436 | switch target { 437 | case "epub", "apple", "pdf", "print": 438 | mustLineNumbers = true 439 | fallthrough 440 | case "azw3": 441 | mustLineNumbers = true // still better to have line numbers 442 | if mustLineNumbers { 443 | buf.Write(content[:codeStart]) 444 | 445 | lines := strings.Split(temp, "\n") 446 | if strings.TrimSpace(lines[len(lines)-1]) == "" { 447 | lines = lines[:len(lines)-1] 448 | } 449 | if strings.TrimSpace(lines[0]) == "" { 450 | lines = lines[1:] 451 | } 452 | for _, line := range lines { 453 | buf.WriteString("") 454 | if len(line) > 0 && line[len(line)-1] == '\r' { 455 | line = line[:len(line)-1] 456 | } 457 | buf.WriteString(line) 458 | buf.WriteString("\n") 459 | 460 | if n := calLineWidth(line); n > 62 { 461 | log.Println(" ", n, ":", line) 462 | } 463 | } 464 | 465 | buf.Write(content[codeCloseEnd:preEnd]) 466 | } else { 467 | buf.Write(content[:preStart]) 468 | buf.Write(bytes.ReplaceAll(content[preStart:codeStart], []byte("line-numbers"), []byte("xxx-yyy"))) 469 | buf.Write(content[codeStart:programStart]) 470 | buf.WriteString(temp) 471 | buf.Write(content[codeClose:preEnd]) 472 | } 473 | 474 | case "mobi": 475 | buf.Write(content[:programStart]) 476 | lines := strings.Split(temp, "\n") 477 | if strings.TrimSpace(lines[len(lines)-1]) == "" { 478 | lines = lines[:len(lines)-1] 479 | } 480 | if strings.TrimSpace(lines[0]) == "" { 481 | lines = lines[1:] 482 | } 483 | for i, line := range lines { 484 | if mustLineNumbers { 485 | fmt.Fprintf(buf, "%3d. ", i+1) 486 | } 487 | if len(line) > 0 && line[len(line)-1] == '\r' { 488 | line = line[:len(line)-1] 489 | } 490 | buf.WriteString(line) 491 | buf.WriteString("\n") 492 | 493 | if n := calLineWidth(line); n > 62 { 494 | log.Println(" ", n, ":", line) 495 | } 496 | } 497 | buf.Write(content[codeClose:preEnd]) 498 | /* 499 | default: // LienNumbers_Manually // epub 500 | linecount := strings.Count(temp, "\n") 501 | if linecount > 0 && temp[len(temp)-1] != '\n' { 502 | linecount++ 503 | } 504 | var b strings.Builder 505 | for i := 1; i <= linecount; i++ { 506 | fmt.Fprintf(&b, "%d.", i) 507 | if i < linecount { 508 | fmt.Fprint(&b, "\n") 509 | } 510 | } 511 | buf.Write(content[:preStart]) 512 | buf.WriteString(` 513 | 514 | 515 | 520 | 527 | 528 |
    516 |
    `)
    517 | 							buf.WriteString(b.String())
    518 | 							buf.WriteString(`
    519 |
    521 | `) 522 | buf.Write(content[preStart:programStart]) 523 | buf.WriteString(temp) 524 | buf.Write(content[codeClose:preEnd]) 525 | buf.WriteString(` 526 |
    529 | `) 530 | */ 531 | } 532 | } 533 | 534 | content = content[preEnd:] 535 | } 536 | buf.Write(content) 537 | article.Content = buf.Bytes() 538 | } 539 | } 540 | 541 | var Img, Src = []byte(`")) 555 | if end < 0 { 556 | fatalError("img tag is incomplete in article:"+article.Filename, content[imgStart:]) 557 | } 558 | end++ 559 | 560 | srcStart := find(content[:end], index, Src) 561 | if srcStart < 0 { 562 | fatalError("img tag has not src in article:"+article.Filename, content[imgStart:]) 563 | } 564 | srcStart += len(Src) 565 | 566 | quotaStart := find(content[:end], srcStart, []byte(`"`)) 567 | if quotaStart < 0 { 568 | fatalError("img src is incomplete in article:"+article.Filename, content[imgStart:]) 569 | } 570 | quotaStart++ 571 | 572 | quotaEnd := find(content[:end], quotaStart, []byte(`"`)) 573 | if quotaEnd < 0 { 574 | fatalError("img src is incomplete in article:"+article.Filename, content[imgStart:]) 575 | } 576 | 577 | src := bytes.TrimSpace(content[quotaStart:quotaEnd]) 578 | newSrc := imagePaths[string(src)] 579 | if newSrc == "" { 580 | log.Fatalf("%s has no image path", src) 581 | } 582 | 583 | buf.Write(content[:quotaStart]) 584 | buf.WriteString(newSrc) 585 | buf.Write(content[quotaEnd:end]) 586 | 587 | content = content[end:] 588 | } 589 | buf.Write(content) 590 | 591 | if rewardImage != "" { // Go语言101 592 | fmt.Fprintf(buf, ` 593 |
    594 |
    595 |
    本书由老貘历时三年写成。目前本书仍在不断改进和增容中。你的赞赏是本书和Go101.org网站不断增容和维护的动力。
    596 | 赞赏 597 |
    (请搜索关注微信公众号“Go 101”或者访问github.com/golang101/golang101获取本书最新版)
    598 |
    599 | `, imagePaths[rewardImage]) 600 | } else { // Go 101 601 | fmt.Fprintf(buf, ` 602 |
    603 |
    604 |
    (The Go 101 book is still being improved frequently from time to time. 605 | Please visit go101.org or follow 606 | @zigo_101 607 | to get the latest news of this book. BTW, Tapir, 608 | the author of the book, has developed several fun games. 609 | You can visit tapirgames.com 610 | to get more information about these games. Hope you enjoy them.)
    611 |
    612 | `) 613 | } 614 | 615 | article.Content = buf.Bytes() 616 | } 617 | 618 | } 619 | 620 | var A, _A, Href = []byte(``), []byte(`href`) 621 | 622 | var linkFmtPatterns = map[bool]string{true: " (%s)", false: "(%s)"} 623 | 624 | func replaceInternalLinks(articles []*Article, chapterMapping map[string]*Article, bookWebsite string, forPrint, engVersion bool) { 625 | for _, article := range articles { 626 | content := article.Content 627 | var buf = bytes.NewBuffer(make([]byte, 0, len(content)+10000)) 628 | for range [1000]struct{}{} { 629 | aStart := find(content, 0, A) 630 | if aStart < 0 { 631 | break 632 | } 633 | index := aStart + len(A) 634 | 635 | aClose := find(content, index, _A) 636 | if aClose < 0 { 637 | fatalError("a href is not closed in article:"+article.Filename, content[aStart:]) 638 | } 639 | aEnd := aClose + len(_A) 640 | 641 | //openEnd := find(content, index, []byte(">")) 642 | //if openEnd < 0 { 643 | // fatalError("a tag is incomplete in article:" + article.Filename, content[aStart:]) 644 | //} 645 | //openEnd++ 646 | 647 | hrefStart := find(content[:aEnd], index, Href) 648 | if hrefStart < 0 { 649 | //fatalError("a tag has not href in article:" + article.Filename, content[aStart:]) 650 | buf.Write(content[:aEnd]) 651 | content = content[aEnd:] 652 | continue 653 | } 654 | hrefStart += len(Href) 655 | 656 | quotaStart := find(content[:aEnd], hrefStart, []byte(`"`)) 657 | if quotaStart < 0 { 658 | fatalError("a href is incomplete in article:"+article.Filename, content[aStart:]) 659 | } 660 | quotaStart++ 661 | 662 | quotaEnd := find(content[:aEnd], quotaStart, []byte(`"`)) 663 | if quotaEnd < 0 { 664 | fatalError("a href is incomplete in article:"+article.Filename, content[aStart:]) 665 | } 666 | 667 | href := bytes.TrimSpace(content[quotaStart:quotaEnd]) 668 | done := false 669 | if bytes.HasPrefix(href, []byte("http")) { 670 | //buf.Write(content[:aEnd]) 671 | } else if i := bytes.Index(href, []byte(".html")); i >= 0 { 672 | done = true 673 | 674 | var newHref []byte 675 | filename := string(href[:i+len(".html")]) 676 | 677 | linkArticle := chapterMapping[filename] 678 | if linkArticle != nil { 679 | //newHref = bytes.ReplaceAll(href, []byte(".html"), []byte(internalName)) 680 | newHref = linkArticle.internalFilename 681 | } else { 682 | //log.Println("internal url path", filename, "not found!") 683 | panic("internal url path " + filename + " not found!") 684 | newHref = append([]byte(bookWebsite+"/article/"), href...) 685 | } 686 | 687 | if article.Filename == "101.html" { 688 | buf.Write(content[:aStart]) 689 | buf.WriteString(linkArticle.chapter2) 690 | buf.Write(content[aStart:quotaStart]) 691 | buf.Write(newHref) 692 | buf.Write(content[quotaEnd:aEnd]) 693 | } else { 694 | buf.Write(content[:quotaStart]) 695 | buf.Write(newHref) 696 | buf.Write(content[quotaEnd:aEnd]) 697 | buf.WriteString(linkArticle.chapter) 698 | } 699 | } else { 700 | //buf.Write(content[:aEnd]) 701 | } 702 | if !done { 703 | if forPrint { 704 | buf.Write(content[:aEnd]) 705 | fmt.Fprintf(buf, linkFmtPatterns[engVersion], href) 706 | } else { 707 | buf.Write(content[:aEnd]) 708 | } 709 | } 710 | 711 | content = content[aEnd:] 712 | } 713 | buf.Write(content) 714 | article.Content = buf.Bytes() 715 | } 716 | 717 | } 718 | 719 | /* 720 | :not(pre) > code { 721 | => 722 | 723 | 724 | 725 | //pre { 726 | //=> 727 | //
    
    728 | */
    729 | func setHtml32Atributes(articles []*Article) {
    730 | 
    731 | 	for _, article := range articles {
    732 | 		content := article.Content
    733 | 		var buf = bytes.NewBuffer(make([]byte, 0, len(content)+10000))
    734 | 		for range [1000]struct{}{} {
    735 | 			codeStart := find(content, 0, Code)
    736 | 			if codeStart < 0 {
    737 | 				break
    738 | 			}
    739 | 			preStart := find(content, 0, Pre)
    740 | 			if preStart >= 0 && preStart < codeStart {
    741 | 				index := preStart + len(Pre)
    742 | 				preClose := find(content, index, _Pre)
    743 | 				if preClose < 0 {
    744 | 					fatalError("pre tag is incomplete in article:"+article.Filename, content[index:])
    745 | 				}
    746 | 				preEnd := preClose + len(_Pre)
    747 | 
    748 | 				buf.Write(content[:index])
    749 | 				//buf.WriteString(` vspace=5`)
    750 | 				buf.Write(content[index:preEnd])
    751 | 
    752 | 				content = content[preEnd:]
    753 | 				continue
    754 | 			}
    755 | 
    756 | 			index := codeStart + len(Code)
    757 | 
    758 | 			codeClose := find(content, index, _Code)
    759 | 			if codeClose < 0 {
    760 | 				fatalError("code tag doesn't match in article:"+article.Filename, content[:codeStart])
    761 | 			}
    762 | 			codeEnd := codeClose + len(_Code)
    763 | 
    764 | 			buf.Write(content[:index])
    765 | 			buf.WriteString(` bgcolor="#dddddd" vspace="1" hspace="1"`)
    766 | 			buf.Write(content[index:codeEnd])
    767 | 
    768 | 			content = content[codeEnd:]
    769 | 		}
    770 | 		buf.Write(content)
    771 | 		article.Content = buf.Bytes()
    772 | 	}
    773 | }
    774 | 
    775 | var Kindle, _Kindle, CommentEnd = []byte("kindle starts:"), []byte("kindle ends:"), []byte("-->")
    776 | 
    777 | func filterArticles(content []byte, bookId int) []byte {
    778 | 	var buf = bytes.NewBuffer(make([]byte, 0, len(content)+10000))
    779 | 
    780 | 	for range [1000]struct{}{} {
    781 | 		kindleStart := bytes.Index(content, Kindle)
    782 | 		if kindleStart < 0 {
    783 | 			break
    784 | 		}
    785 | 
    786 | 		index := kindleStart + len(Kindle)
    787 | 
    788 | 		startEnd := find(content, index, CommentEnd)
    789 | 		if startEnd < 0 {
    790 | 			fatalError("kindle tag is imcomplete", content[:kindleStart])
    791 | 		}
    792 | 
    793 | 		kindleEnd := find(content, startEnd+len(CommentEnd), _Kindle)
    794 | 		if kindleEnd < 0 {
    795 | 			fatalError("kindle tag doesn't match a", content[:startEnd])
    796 | 		}
    797 | 
    798 | 		endEnd := find(content, kindleEnd+len(_Kindle), CommentEnd)
    799 | 		if endEnd < 0 {
    800 | 			fatalError("kindle tag doesn't match b", content[:kindleEnd])
    801 | 		}
    802 | 		endEnd += len(CommentEnd)
    803 | 
    804 | 		idStr := string(bytes.TrimSpace(content[index:startEnd]))
    805 | 		n, err := strconv.Atoi(idStr)
    806 | 		if err != nil {
    807 | 			fatalError("bad kindle book id: "+idStr+". "+err.Error(), content[:endEnd])
    808 | 		}
    809 | 
    810 | 		if n == bookId {
    811 | 			buf.Write(content[:endEnd])
    812 | 		} else {
    813 | 			buf.Write(content[:kindleStart])
    814 | 		}
    815 | 		content = content[endEnd:]
    816 | 	}
    817 | 	buf.Write(content)
    818 | 	return buf.Bytes()
    819 | }
    820 | 
    821 | func hintExternalLinks(articles []*Article, externalLinkPngPath string) {
    822 | 	img := ``
    823 | 
    824 | 	for _, article := range articles {
    825 | 		content := article.Content
    826 | 		var buf = bytes.NewBuffer(make([]byte, 0, len(content)+10000))
    827 | 		for range [1000]struct{}{} {
    828 | 			aStart := find(content, 0, A)
    829 | 			if aStart < 0 {
    830 | 				break
    831 | 			}
    832 | 			index := aStart + len(A)
    833 | 
    834 | 			end := find(content, index, []byte(">"))
    835 | 			if end < 0 {
    836 | 				fatalError("a tag is incomplete in article:"+article.Filename, content[aStart:])
    837 | 			}
    838 | 			end++
    839 | 
    840 | 			hrefStart := find(content[:end], index, Href)
    841 | 			if hrefStart < 0 {
    842 | 				//fatalError("a tag has not href in article:" + article.Filename, content[aStart:])
    843 | 				buf.Write(content[:end])
    844 | 				content = content[end:]
    845 | 				continue
    846 | 			}
    847 | 			hrefStart += len(Href)
    848 | 
    849 | 			quotaStart := find(content[:end], hrefStart, []byte(`"`))
    850 | 			if quotaStart < 0 {
    851 | 				fatalError("a href is incomplete in article:"+article.Filename, content[aStart:])
    852 | 			}
    853 | 			quotaStart++
    854 | 
    855 | 			quotaEnd := find(content[:end], quotaStart, []byte(`"`))
    856 | 			if quotaEnd < 0 {
    857 | 				fatalError("a href is incomplete in article:"+article.Filename, content[aStart:])
    858 | 			}
    859 | 
    860 | 			aEnd := find(content, index, _A)
    861 | 			if aEnd < 0 {
    862 | 				fatalError("a tag doesn't match in article:"+article.Filename, content[index:])
    863 | 			}
    864 | 			endEnd := aEnd + len(_A)
    865 | 
    866 | 			href := bytes.TrimSpace(content[quotaStart:quotaEnd])
    867 | 			if bytes.HasPrefix(href, []byte("http")) {
    868 | 				buf.Write(content[:aEnd])
    869 | 				buf.WriteString(img)
    870 | 				buf.Write(content[aEnd:endEnd])
    871 | 			} else {
    872 | 				buf.Write(content[:endEnd])
    873 | 			}
    874 | 
    875 | 			content = content[endEnd:]
    876 | 		}
    877 | 		buf.Write(content)
    878 | 		article.Content = buf.Bytes()
    879 | 	}
    880 | }
    881 | 
    882 | var (
    883 | 	attribtuesTobeRemoved = [][]byte{
    884 | 		[]byte(` valign="bottom"`),
    885 | 		[]byte(` valign="middle"`),
    886 | 		[]byte(` align="center"`),
    887 | 		[]byte(` align="left"`),
    888 | 		[]byte(` border="1"`),
    889 | 		[]byte(` scope="row"`),
    890 | 	}
    891 | )
    892 | 
    893 | func removeXhtmlAttributes(articles []*Article) {
    894 | 
    895 | 	for _, article := range articles {
    896 | 		content := article.Content
    897 | 		log.Println("===========", article.Title, len(content))
    898 | 		for _, attr := range attribtuesTobeRemoved {
    899 | 			content = bytes.ReplaceAll(content, attr, []byte{})
    900 | 			log.Printf("%s: %d", attr, len(content))
    901 | 		}
    902 | 		article.Content = content
    903 | 	}
    904 | }
    905 | 
    
    
    --------------------------------------------------------------------------------
    /css.go:
    --------------------------------------------------------------------------------
      1 | package main
      2 | 
      3 | /*
      4 | const EpubCSS = `
      5 | .content {
      6 |        font-size: 15px;
      7 |        line-height: 150%;
      8 | }
      9 | 
     10 | :not(pre) > code {
     11 |        padding: 1px 2px;
     12 | }
     13 | 
     14 | code {
     15 |        background-color: #dddddd;
     16 | }
     17 | 
     18 | pre {
     19 |        background-color: #dddddd;
     20 |        padding: 3px 6px;
     21 |        margin-left: 0px;
     22 |        margin-right: 0px;
     23 | }
     24 | 
     25 | table.table-bordered {
     26 |        border-collapse: collapse;
     27 |        border: 1px solid #999999;
     28 | }
     29 | 
     30 | table.table-bordered td {
     31 |       border: 1px solid black;
     32 | }
     33 | 
     34 | table.table-bordered th {
     35 |       border: 1px solid black;
     36 | }
     37 | 
     38 | .text-center {
     39 |        text-align: center;
     40 | }
     41 | 
     42 | .text-left {
     43 |        text-align: left;
     44 | }
     45 | 
     46 | a[href*='//']::after {
     47 | 	content: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAQElEQVR42qXKwQkAIAxDUUdxtO6/RBQkQZvSi8I/pL4BoGw/XPkh4XigPmsUgh0626AjRsgxHTkUThsG2T/sIlzdTsp52kSS1wAAAABJRU5ErkJggg==);
     48 | 	margin: 0 3px 0 5px;
     49 | }
     50 | `
     51 | */
     52 | 
     53 | const CommonCSS = `
     54 | 
     55 | body {
     56 |        line-height: 1.1;
     57 | }
     58 | 
     59 | .text-center {
     60 |        text-align: center;
     61 | }
     62 | 
     63 | .text-left {
     64 |        text-align: left;
     65 | }
     66 | 
     67 | table.table-bordered {
     68 |        border-collapse: collapse;
     69 |        border: 1px solid #999999;
     70 | }
     71 | 
     72 | table.table-bordered td {
     73 |       border: 1px solid black;
     74 | }
     75 | 
     76 | table.table-bordered th {
     77 |       border: 1px solid black;
     78 | }
     79 | 
     80 | div.alert {
     81 | 	border: 1px solid;
     82 |        margin-left: 12pt;
     83 |        margin-right: 12pt;
     84 | 	padding: 3pt 10pt;
     85 | }
     86 | 
     87 | blockquote {
     88 | 	border: 1px solid;
     89 |        margin-left: 12pt;
     90 |        margin-right: 12pt;
     91 | 	padding: 5pt 10pt 5pt 16pt;
     92 | }
     93 | 
     94 | :not(pre) > code {
     95 |        padding: 1px 2px;
     96 | }
     97 | 
     98 | code {
     99 | }
    100 | 
    101 | pre {
    102 | 	border: 1px solid;
    103 |        line-height: 1;
    104 | }
    105 | 
    106 | pre.line-numbers {
    107 | 	counter-reset: line;
    108 | }
    109 | 
    110 | pre.line-numbers > code {
    111 | 	counter-increment: line;
    112 | }
    113 | 
    114 | pre.line-numbers > code:before {
    115 | 	content: counter(line);
    116 | 	display: inline-block;
    117 | 	text-align:right;
    118 | 	width: 21pt;
    119 | 	padding: 0 2pt 0 0;
    120 | 	margin: 0 4pt 0 2pt;
    121 | 	content: counter(line)"|";
    122 | 	border-right: 0;
    123 | 	user-select: none;
    124 | 	-webkit-user-select: none;
    125 | 	-moz-user-select: none;
    126 | 	-ms-user-select: none;
    127 | }
    128 | `
    129 | 
    130 | const Awz3CSS_Chinese = CommonCSS + `
    131 | 
    132 | pre > code {
    133 |        font-family: Courier, Futura, "Caecilia Condensed";
    134 |        font-size: 10pt;
    135 |        text-align: left;
    136 |        line-height: 1;
    137 | }
    138 | 
    139 | pre.fixed-width > code {
    140 |        font-family: Courier, Futura, "Caecilia Condensed";
    141 | }
    142 | 
    143 | pre.fixed-width {
    144 |        font-family: Courier, Futura, "Caecilia Condensed";
    145 | }
    146 | 
    147 | pre {
    148 |        padding: 3px 3px;
    149 |        margin-left: 0px;
    150 |        margin-right: 0px;
    151 | }
    152 | 
    153 | span.invisible {
    154 | 	visibility:hidden
    155 | }
    156 | 
    157 | `
    158 | 
    159 | const Awz3CSS = CommonCSS + `
    160 | 
    161 | pre > code {
    162 |        font-family: Courier, Futura, "Caecilia Condensed";
    163 |        font-size: 7pt;
    164 |        text-align: left;
    165 |        line-height: 1;
    166 | }
    167 | 
    168 | pre.fixed-width > code {
    169 |        font-family: Courier, Futura, "Caecilia Condensed";
    170 | }
    171 | 
    172 | pre.fixed-width {
    173 |        font-family: Courier, Futura, "Caecilia Condensed";
    174 | }
    175 | 
    176 | pre {
    177 |        padding: 3px 3px;
    178 |        margin-left: 0px;
    179 |        margin-right: 0px;
    180 | }
    181 | 
    182 | span.invisible {
    183 | 	visibility:hidden
    184 | }
    185 | 
    186 | `
    187 | 
    188 | const EpubCSS = CommonCSS + `
    189 | 
    190 | pre > code {
    191 |        text-align: left;
    192 |        line-height: 150%;
    193 | 	tab-size: 7;
    194 | 	-moz-tab-size: 7;
    195 | }
    196 | 
    197 | pre {
    198 |        padding: 3px 6px;
    199 |        margin-left: 0px;
    200 |        margin-right: 0px;
    201 | }
    202 | 
    203 | a[href*='//']::after {
    204 | 	content: "🗗";
    205 | 	margin: 0 3px 0 5px;
    206 | }
    207 | 
    208 | h1 {
    209 | 	font-size: 300%;
    210 | }
    211 | 
    212 | h3 {
    213 | 	font-size: 182%;
    214 | }
    215 | 
    216 | h4 {
    217 | 	font-size: 128%;
    218 | 	border-left: 3px solid #333;
    219 | 	padding-left: 3px;
    220 | }
    221 | 
    222 | .content {
    223 |        font-size: 15px;
    224 |        line-height: 150%;
    225 | }
    226 | `
    227 | 
    228 | const PdfCommonCSS = CommonCSS + `
    229 | 
    230 | pre > code {
    231 |        text-align: left;
    232 |        line-height: 150%;
    233 | 	tab-size: 7;
    234 | 	-moz-tab-size: 7;
    235 | }
    236 | 
    237 | pre {
    238 |        padding: 3px 6px;
    239 |        margin-left: 0px;
    240 |        margin-right: 0px;
    241 | }
    242 | 
    243 | 
    244 | h1, h3, h4 {
    245 | 	font-weight: bold;
    246 |        line-height: 110%;
    247 | }
    248 | 
    249 | h1 {
    250 | 	font-size: 300%;
    251 | }
    252 | 
    253 | h3 {
    254 | 	font-size: 182%;
    255 | }
    256 | 
    257 | h4 {
    258 | 	font-size: 128%;
    259 | 	border-left: 3px solid #333;
    260 | 	padding-left: 3px;
    261 | }
    262 | 
    263 | .content {
    264 |        font-size: 15px;
    265 |        line-height: 150%;
    266 | }
    267 | `
    268 | 
    269 | //const PdfCSS = PdfCommonCSS + `
    270 | //
    271 | //a[href*='//']::after {
    272 | //	content: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAQElEQVR42qXKwQkAIAxDUUdxtO6/RBQkQZvSi8I///pL4BoGw/XPkh4XigPmsUgh0626AjRsgxHTkUThsG2T/sIlzdTsp52kSS1wAAAABJRU5ErkJggg==);
    273 | //	margin: 0 3px 0 5px;
    274 | //}
    275 | //`
    276 | 
    277 | const PrintCSS = PdfCommonCSS + `
    278 | 
    279 | h3 {
    280 | 	padding-bottom: 2px;
    281 | 	border-bottom: 2px solid #333;
    282 | 	padding-left: 3px;
    283 | 	border-left: 6px solid #333;
    284 | }
    285 | `
    286 | 
    287 | const PdfCSS = PrintCSS + `
    288 | 
    289 | a[href*='//']::after {
    290 | 	content: "🗗";
    291 | 	margin: 0 3px 0 5px;
    292 | 	vertical-align: top;
    293 | 	font-size: small;
    294 | }
    295 | `
    296 | 
    297 | const AppleCSS = PdfCSS + `
    298 | 
    299 | pre.line-numbers > code:before {
    300 | 	width: 28pt;
    301 | 	padding: 0 9pt 0 0;
    302 | }
    303 | 
    304 | pre > code {
    305 | 	font-size: small;
    306 | }
    307 | `
    308 | 
    309 | const MobiCSS = `
    310 | 
    311 | .text-center {
    312 |        text-align: center;
    313 | }
    314 | 
    315 | .text-left {
    316 |        text-align: left;
    317 | }
    318 | 
    319 | `
    320 | 
    
    
    --------------------------------------------------------------------------------
    /epub.go:
    --------------------------------------------------------------------------------
      1 | package main
      2 | 
      3 | import (
      4 | 	"archive/zip"
      5 | 	//"bytes"
      6 | 	"io"
      7 | 	"log"
      8 | 	"os"
      9 | 	"path/filepath"
     10 | 	"strings"
     11 | 
     12 | 	"github.com/bmaupin/go-epub"
     13 | )
     14 | 
     15 | func genetateEpubFile(bookProjectDir, bookVersion, coverImagePath string) string {
     16 | 	var e *epub.Epub
     17 | 	var outFilename string
     18 | 	var indexArticleTitle string
     19 | 	var bookWebsite string
     20 | 	var engVersion bool
     21 | 
     22 | 	projectName := confirmBookProjectName(bookProjectDir)
     23 | 	switch projectName {
     24 | 	default:
     25 | 		log.Fatal("unknow book porject: ", projectName)
     26 | 	case "Go101":
     27 | 		e = epub.NewEpub("Go 101")
     28 | 		e.SetAuthor("Tapir Liu")
     29 | 		indexArticleTitle = "Contents"
     30 | 		bookWebsite = "https://go101.org"
     31 | 		engVersion = true
     32 | 		outFilename = "Go101-" + bookVersion + ".epub"
     33 | 	case "Golang101":
     34 | 		e = epub.NewEpub("Go语言101")
     35 | 		e.SetAuthor("老貘")
     36 | 		indexArticleTitle = "目录"
     37 | 		bookWebsite = "https://gfw.go101.org"
     38 | 		engVersion = false
     39 | 		outFilename = "Golang101-" + bookVersion + ".epub"
     40 | 	}
     41 | 
     42 | 	cssFilename := "all.css"
     43 | 	tempCssFile := mustCreateTempFile("all*.css", []byte(EpubCSS))
     44 | 	defer os.Remove(tempCssFile)
     45 | 	cssPath, err := e.AddCSS(tempCssFile, cssFilename)
     46 | 	if err != nil {
     47 | 		log.Fatalln("add css", cssFilename, "failed:", err)
     48 | 	}
     49 | 
     50 | 	writeEpub_Go101(outFilename, e, -1, bookWebsite, projectName, indexArticleTitle, bookProjectDir, coverImagePath, cssPath, "epub", engVersion)
     51 | 	log.Println("Create", outFilename, "done!")
     52 | 
     53 | 	return outFilename
     54 | }
     55 | 
     56 | const (
     57 | 	LienNumbers_Manually = iota
     58 | 	LienNumbers_Unchange
     59 | 	LienNumbers_Selectable
     60 | 	LienNumbers_Automatically
     61 | )
     62 | 
     63 | // zero bookId means all.
     64 | func writeEpub_Go101(outputFilename string, e *epub.Epub, bookId int, bookWebsite, projectName, indexArticleTitle, bookProjectDir, coverImagePath, cssPath, target string, engVersion bool) {
     65 | 	imagePaths, coverImagePathInEpub := addImages(e, bookProjectDir, coverImagePath)
     66 | 	var rewardImage string
     67 | 	if projectName == "Golang101" {
     68 | 		rewardImage = "res/101-reward-qrcode-8.png"
     69 | 	}
     70 | 
     71 | 	index, articles, chapterMapping := mustArticles(bookProjectDir, engVersion)
     72 | 	index.Title = indexArticleTitle
     73 | 	index.Content = append([]byte("

    "+index.Title+"

    "), index.Content...) 74 | 75 | if bookId > 0 { 76 | index.Content = filterArticles(index.Content, bookId) 77 | } 78 | //internalArticles := collectInternalArticles(index.Content) 79 | //log.Println("internalArticles:", internalArticles) 80 | 81 | oldArticles := articles 82 | articles = nil 83 | for _, article := range oldArticles { 84 | if _, present := chapterMapping[article.Filename]; present { 85 | articles = append(articles, article) 86 | } 87 | } 88 | articles = append([]*Article{index}, articles...) 89 | 90 | escapeCharactorWithinCodeTags(articles, target) 91 | replaceInternalLinks(articles, chapterMapping, bookWebsite, target == "print", engVersion) 92 | replaceImageSources(articles, imagePaths, rewardImage) 93 | 94 | switch target { 95 | case "azw3": 96 | fallthrough 97 | case "mobi": // mobi 98 | setHtml32Atributes(articles) 99 | 100 | pngFilename := "external-link.png" 101 | tempPngFile := mustCreateTempFile("external-link*.png", mustParseImageData(ExternalLinkPNG)) 102 | defer os.Remove(tempPngFile) 103 | imgpath, err := e.AddImage(tempPngFile, pngFilename) 104 | if err != nil { 105 | log.Fatalln("add image", pngFilename, "failed:", err) 106 | } 107 | imagePaths[pngFilename] = imgpath 108 | 109 | hintExternalLinks(articles, imgpath) 110 | case "apple": 111 | removeXhtmlAttributes(articles) 112 | default: 113 | } 114 | 115 | wrapContentDiv(articles) 116 | 117 | // ... 118 | e.SetCover(coverImagePathInEpub, "") 119 | 120 | for _, article := range articles { 121 | internalFilename := string(article.internalFilename) 122 | e.AddSection(string(article.Content), article.Title, internalFilename, cssPath) 123 | } 124 | 125 | if err := e.Write(outputFilename); err != nil { 126 | log.Fatalln("write epub failed:", err) 127 | } 128 | } 129 | 130 | func addImages(e *epub.Epub, bookProjectDir, coverImagePath string) (map[string]string, string) { 131 | imagePaths := make(map[string]string) 132 | 133 | root := filepath.Join(bookProjectDir, "pages", ArticlesFolder, "res") 134 | f := func(path string, info os.FileInfo, err error) error { 135 | if err != nil { 136 | return err 137 | } 138 | if info.IsDir() && path != root { 139 | return filepath.SkipDir 140 | } 141 | //log.Printf("visited file or dir: %q\n", path) 142 | if !info.IsDir() { 143 | index := strings.Index(path, "res"+string(filepath.Separator)) 144 | imgsrc := path[index:] 145 | filename := filepath.Base(path) 146 | lower := strings.ToLower(imgsrc) 147 | if strings.Index(filename, "front-cover") < 0 && 148 | (strings.HasSuffix(lower, ".png") || 149 | strings.HasSuffix(lower, ".gif") || 150 | strings.HasSuffix(lower, ".jpg") || 151 | strings.HasSuffix(lower, ".jpeg")) { 152 | imgpath, err := e.AddImage(path, filename) 153 | if err != nil { 154 | log.Fatalln("add image", filename, "failed:", err) 155 | } 156 | imagePaths[imgsrc] = imgpath 157 | //log.Println(imgsrc, filename, imgpath) 158 | } 159 | } 160 | 161 | return nil 162 | } 163 | 164 | if err := filepath.Walk(root, f); err != nil { 165 | log.Fatalln("list article res image files error:", err) 166 | } 167 | 168 | // Cover image 169 | var coverImagePathInEpub string 170 | { 171 | filename := filepath.Base(coverImagePath) 172 | imgpath, err := e.AddImage(coverImagePath, filename) 173 | if err != nil { 174 | log.Fatalln("add cover image", filename, "failed:", err) 175 | } 176 | coverImagePathInEpub = imgpath 177 | } 178 | 179 | return imagePaths, coverImagePathInEpub 180 | } 181 | 182 | func removePagesFromEpub(epubFilename string, pagesToRemove ...string) { 183 | r, err := zip.OpenReader(epubFilename) 184 | if err != nil { 185 | log.Fatal(err) 186 | } 187 | defer r.Close() 188 | 189 | os.Remove(epubFilename) 190 | 191 | outputFile, err := os.Create(epubFilename) 192 | if err != nil { 193 | log.Fatal(err) 194 | } 195 | defer outputFile.Close() 196 | 197 | w := zip.NewWriter(outputFile) 198 | 199 | shouldRemove := map[string]bool{} 200 | for _, page := range pagesToRemove { 201 | shouldRemove[page] = true 202 | } 203 | 204 | // Iterate through the files in the archive, 205 | // printing some of their contents. 206 | for _, f := range r.File { 207 | //log.Printf("Contents of %s:\n", f.Name) 208 | if shouldRemove[f.Name] { 209 | continue 210 | } 211 | 212 | rc, err := f.Open() 213 | if err != nil { 214 | log.Fatal(err) 215 | } 216 | 217 | of, err := w.Create(f.Name) 218 | if err != nil { 219 | log.Fatal(err) 220 | } 221 | 222 | _, err = io.Copy(of, rc) 223 | if err != nil { 224 | log.Fatal(err) 225 | } 226 | 227 | rc.Close() 228 | log.Println() 229 | } 230 | 231 | err = w.Close() 232 | if err != nil { 233 | log.Fatal(err) 234 | } 235 | 236 | outputFile.Sync() 237 | } 238 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module go101.org/book 2 | 3 | require ( 4 | github.com/bmaupin/go-epub v0.5.0 5 | github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 6 | golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8 7 | ) 8 | 9 | go 1.12 10 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/bmaupin/go-epub v0.5.0 h1:ptOZQuO3YBRytuhRPsHUJh5bRmvP66/GvB+NWTC2dyE= 2 | github.com/bmaupin/go-epub v0.5.0/go.mod h1:4RBr0Zo03mRGOyGAcc25eLOqIPCkMbfz+tINVmH6clQ= 3 | github.com/gofrs/uuid v3.1.0+incompatible h1:q2rtkjaKT4YEr6E1kamy0Ha4RtepWlQBedyHx0uzKwA= 4 | github.com/gofrs/uuid v3.1.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= 5 | github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= 6 | github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= 7 | golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8 h1:hVwzHzIUGRjiF7EcUjqNxk3NCfkPxbDKRdnNE1Rpg0U= 8 | golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 9 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 10 | -------------------------------------------------------------------------------- /image.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "image" 6 | "image/color" 7 | "image/draw" 8 | "image/png" 9 | "log" 10 | "os" 11 | "path/filepath" 12 | 13 | //"golang.org/x/image/math/fixed" 14 | "github.com/golang/freetype" 15 | "golang.org/x/image/font/gofont/goregular" 16 | //"github.com/golang/freetype/truetype" 17 | ) 18 | 19 | const ExternalLinkPNG = `iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAAZiS0dEAAAAAAAA+UO7fwAAAAlwSFlzAAAASAAAAEgARslrPgAAAKpJREFUSMftlTsOg0AMRF8QNd1KuVCkhPvmIlDQk+3ScYFQsJEC7MdmoYkYyZ1nnm2tAE4pVQMW+ETqK0nPSq+EMRuQbPDIAM1RAAO0ztNK/BrA7+QdcN0T4AsX+SWAUPgugFi4SDFAdngMIA3fdCLN5GpAxfydG+0FyoRhAJ6u7wa8yZRvxYvbZKtf16AFFLkrp7QE2MUk2oLpkx/UA9k/IVQ9cD/6Kn+mESDFiPdj8h9+AAAAJXRFWHRkYXRlOmNyZWF0ZQAyMDE5LTA0LTA2VDIxOjAzOjUzKzAwOjAwwCzL2wAAACV0RVh0ZGF0ZTptb2RpZnkAMjAxOS0wNC0wNlQyMTowMzo1MyswMDowMLFxc2cAAAAodEVYdHN2ZzpiYXNlLXVyaQBmaWxlOi8vL3RtcC9tYWdpY2stbkRub0JiNjHXYHRtAAAAAElFTkSuQmCC` 20 | 21 | const CoverImageFilename = "101-front-cover-1400x.png" 22 | const CoverImageTempFilePattern = "101-front-cover-*.png" 23 | 24 | func buildCoverImage(bookProjectDir, bookVersion string) string { 25 | 26 | revison := "" 27 | hash := runShellCommand2(true, bookProjectDir, "git", "rev-parse", bookVersion) 28 | hash = bytes.TrimSpace(hash) 29 | if len(hash) > 7 { 30 | hash = hash[:7] 31 | revison = string(hash) 32 | } 33 | 34 | // git log -1 --pretty='%ad' --date=format:'%Y/%m/%d' v1.16.a 35 | // 2021/02/18 36 | 37 | var versionText string 38 | if revison != "" { 39 | versionText = "-= " + bookVersion + "-" + revison + " =-" 40 | } else { 41 | versionText = "-= " + bookVersion + " =-" 42 | } 43 | 44 | // Load cover image 45 | inFile, err := os.Open(filepath.Join(bookProjectDir, "pages", ArticlesFolder, "res", CoverImageFilename)) 46 | if err != nil { 47 | log.Fatal(err) 48 | } 49 | defer inFile.Close() 50 | 51 | pngImage, err := png.Decode(inFile) 52 | if err != nil { 53 | log.Fatal(err) 54 | } 55 | 56 | // Draw cover image 57 | output := image.NewRGBA(image.Rect(0, 0, pngImage.Bounds().Max.X, pngImage.Bounds().Max.Y)) 58 | draw.Draw(output, output.Bounds(), pngImage, image.ZP, draw.Src) 59 | 60 | // Load font 61 | utf8Font, err := freetype.ParseFont(goregular.TTF) 62 | if err != nil { 63 | log.Fatal(err) 64 | } 65 | 66 | // Draw text 67 | dpi := float64(72) 68 | fontsize := float64(29.0) 69 | //spacing := float64(1.5) 70 | 71 | ctx := new(freetype.Context) 72 | ctx = freetype.NewContext() 73 | ctx.SetDPI(dpi) 74 | ctx.SetFont(utf8Font) 75 | ctx.SetFontSize(fontsize) 76 | ctx.SetClip(output.Bounds()) 77 | ctx.SetDst(output) 78 | 79 | pt := freetype.Pt(0, int(ctx.PointToFixed(fontsize)>>6)) 80 | ctx.SetSrc(image.NewUniform(color.RGBA{0, 0, 0, 0})) 81 | extent, err := ctx.DrawString(versionText, pt) 82 | if err != nil { 83 | log.Fatal(err) 84 | } 85 | 86 | pt = freetype.Pt(output.Bounds().Max.X/2, 469) 87 | pt.X -= extent.X / 2 88 | 89 | ctx.SetSrc(image.NewUniform(color.RGBA{0, 0, 0, 255})) 90 | _, err = ctx.DrawString(versionText, pt) 91 | if err != nil { 92 | log.Fatal(err) 93 | } 94 | 95 | log.Println(extent) 96 | //pt.Y += ctx.PointToFixed(fontsize * spacing) 97 | 98 | // Save new cover image 99 | pngFilename := mustCreateTempFile(CoverImageTempFilePattern, nil) 100 | 101 | pngFile, err := os.Create(pngFilename) 102 | if err != nil { 103 | log.Fatal(err) 104 | } 105 | defer pngFile.Close() 106 | 107 | err = png.Encode(pngFile, output) 108 | if err != nil { 109 | log.Fatal(err) 110 | } 111 | 112 | err = pngFile.Sync() 113 | if err != nil { 114 | log.Fatal(err) 115 | } 116 | 117 | return pngFilename 118 | } 119 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "flag" 6 | "log" 7 | "os" 8 | "strings" 9 | ) 10 | 11 | // The code of this project is very ugly. It just makes the job done. 12 | 13 | var bookProjectDirFlag = flag.String("book-project-dir", "", "the path to the book project") 14 | var bookVersionFlag = flag.String("book-version", "", "the version of the book") 15 | var targetFlag = flag.String("target", "all", "output target (epub | azw3 | mobi | apple | pdf | print | all)") 16 | 17 | func main() { 18 | log.SetFlags(0) 19 | flag.Parse() 20 | 21 | bookProjectDirs := make([]string, 0, 2) 22 | projectDir := strings.TrimSpace(*bookProjectDirFlag) 23 | if projectDir != "" { 24 | bookProjectDirs = append(bookProjectDirs, projectDir) 25 | } else { 26 | if path := os.Getenv("Go101Path"); path != "" { 27 | bookProjectDirs = append(bookProjectDirs, path) 28 | } 29 | if path := os.Getenv("Golang101Path"); path != "" { 30 | bookProjectDirs = append(bookProjectDirs, path) 31 | } 32 | } 33 | 34 | if len(bookProjectDirs) == 0 { 35 | log.Fatal("-book-project-dir is required.") 36 | } 37 | 38 | for _, bookProjectDir := range bookProjectDirs { 39 | bookVersion := strings.TrimSpace(*bookVersionFlag) 40 | if bookVersion == "" { 41 | tag := runShellCommand2(true, bookProjectDir, "git", "describe", "--tags", "--abbrev=0") 42 | tag = bytes.TrimSpace(tag) 43 | if len(tag) > 0 { 44 | bookVersion = string(tag) 45 | } 46 | } 47 | 48 | if bookVersion == "" { 49 | log.Fatal("-book-version is required.") 50 | } 51 | 52 | coverImagePath := buildCoverImage(bookProjectDir, bookVersion) 53 | 54 | switch target := strings.TrimSpace(*targetFlag); target { 55 | case "epub": 56 | genetateEpubFile(bookProjectDir, bookVersion, coverImagePath) 57 | case "azw3": 58 | genetateAzw3File(bookProjectDir, bookVersion, coverImagePath) 59 | case "mobi": 60 | genetateMobiFile(bookProjectDir, bookVersion, coverImagePath) 61 | case "apple": 62 | genetateAppleFile(bookProjectDir, bookVersion, coverImagePath) 63 | case "pdf": 64 | genetatePdfFile(bookProjectDir, bookVersion, coverImagePath, false) 65 | case "print": 66 | genetatePdfFile(bookProjectDir, bookVersion, coverImagePath, true) 67 | case "all", "": 68 | genetateAzw3File(bookProjectDir, bookVersion, coverImagePath) 69 | genetateEpubFile(bookProjectDir, bookVersion, coverImagePath) 70 | genetateMobiFile(bookProjectDir, bookVersion, coverImagePath) 71 | genetateAppleFile(bookProjectDir, bookVersion, coverImagePath) 72 | genetatePdfFile(bookProjectDir, bookVersion, coverImagePath, false) 73 | genetatePdfFile(bookProjectDir, bookVersion, coverImagePath, true) 74 | default: 75 | log.Fatal("Unknown target:", target) 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /mobi.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "os" 6 | 7 | "github.com/bmaupin/go-epub" 8 | ) 9 | 10 | func genetateMobiFile(bookProjectDir, bookVersion, coverImagePath string) { 11 | genetateMobiFileForBook(bookProjectDir, bookVersion, coverImagePath, 0) 12 | 13 | //genetateMobiFileForBook(bookProjectDir, bookVersion, 1) 14 | //genetateMobiFileForBook(bookProjectDir, bookVersion, 2) 15 | } 16 | 17 | // zero bookId means all. 18 | func genetateMobiFileForBook(bookProjectDir, bookVersion, coverImagePath string, bookId int) { 19 | var e *epub.Epub 20 | var outFilename string 21 | var indexArticleTitle string 22 | var bookWebsite string 23 | var engVersion bool 24 | 25 | projectName := confirmBookProjectName(bookProjectDir) 26 | switch projectName { 27 | default: 28 | log.Fatal("unknow book porject: ", projectName) 29 | case "Go101": 30 | if bookId == 0 { 31 | e = epub.NewEpub("Go 101") 32 | outFilename = "Go101-" + bookVersion + ".mobi" 33 | } else if bookId == 1 { 34 | e = epub.NewEpub("Go 101 (Type System)") 35 | outFilename = "Go101-" + bookVersion + "-types.mobi" 36 | } else if bookId == 2 { 37 | e = epub.NewEpub("Go 101 (Extended)") 38 | outFilename = "Go101-" + bookVersion + "-extended.mobi" 39 | } else { 40 | log.Fatal("unknown book id: ", bookId) 41 | } 42 | e.SetAuthor("Tapir Liu") 43 | bookWebsite = "https://go101.org" 44 | engVersion = true 45 | indexArticleTitle = "Contents" 46 | case "Golang101": 47 | if bookId == 0 { 48 | e = epub.NewEpub("Go语言101") 49 | outFilename = "Golang101-" + bookVersion + ".mobi" 50 | } else if bookId == 1 { 51 | e = epub.NewEpub("Go语言101(类型系统)") 52 | outFilename = "Golang101" + bookVersion + "-types.mobi" 53 | } else if bookId == 2 { 54 | e = epub.NewEpub("Go语言101(扩展阅读)") 55 | outFilename = "Golang101-" + bookVersion + "-extended.mobi" 56 | } else { 57 | log.Fatal("unknown book id: ", bookId) 58 | } 59 | e.SetAuthor("老貘") 60 | bookWebsite = "https://gfw.go101.org" 61 | engVersion = false 62 | indexArticleTitle = "目录" 63 | } 64 | 65 | cssFilename := "all.css" 66 | tempCssFile := mustCreateTempFile("all*.css", []byte(MobiCSS)) 67 | defer os.Remove(tempCssFile) 68 | cssPath, err := e.AddCSS(tempCssFile, cssFilename) 69 | if err != nil { 70 | log.Fatalln("add css", cssFilename, "failed:", err) 71 | } 72 | 73 | tempOutFilename := outFilename + "*.epub" 74 | tempOutFilename = mustCreateTempFile(tempOutFilename, nil) 75 | defer os.Remove(tempOutFilename) 76 | //tempOutFilename := outFilename + ".epub" 77 | 78 | writeEpub_Go101(tempOutFilename, e, bookId, bookWebsite, projectName, indexArticleTitle, bookProjectDir, coverImagePath, cssPath, "mobi", engVersion) 79 | println("ebook-convert", tempOutFilename, outFilename) 80 | runShellCommand(".", "ebook-convert", tempOutFilename, outFilename) 81 | log.Println("Create", outFilename, "done!") 82 | } 83 | -------------------------------------------------------------------------------- /pdf.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "os" 6 | "strings" 7 | 8 | "github.com/bmaupin/go-epub" 9 | ) 10 | 11 | func genetatePdfFile(bookProjectDir, bookVersion, coverImagePath string, forPrint bool) string { 12 | var e *epub.Epub 13 | var outFilename string 14 | var indexArticleTitle string 15 | var bookWebsite string 16 | var engVersion bool 17 | 18 | target := "pdf" 19 | css := PdfCSS 20 | ext := ".pdf" 21 | if forPrint { 22 | target = "print" 23 | css = PrintCSS 24 | } 25 | 26 | projectName := confirmBookProjectName(bookProjectDir) 27 | switch projectName { 28 | default: 29 | log.Fatal("unknow book porject: ", projectName) 30 | case "Go101": 31 | e = epub.NewEpub("Go 101") 32 | e.SetAuthor("Tapir Liu") 33 | indexArticleTitle = "Contents" 34 | bookWebsite = "https://go101.org" 35 | engVersion = true 36 | outFilename = "Go101-" + bookVersion + ext 37 | case "Golang101": 38 | e = epub.NewEpub("Go语言101") 39 | e.SetAuthor("老貘") 40 | indexArticleTitle = "目录" 41 | bookWebsite = "https://gfw.go101.org" 42 | engVersion = false 43 | outFilename = "Golang101-" + bookVersion + ext 44 | } 45 | 46 | cssFilename := "all.css" 47 | tempCssFile := mustCreateTempFile("all*.css", []byte(css)) 48 | defer os.Remove(tempCssFile) 49 | cssPath, err := e.AddCSS(tempCssFile, cssFilename) 50 | if err != nil { 51 | log.Fatalln("add css", cssFilename, "failed:", err) 52 | } 53 | 54 | // ... 55 | tempOutFilename := outFilename + "*.epub" 56 | tempOutFilename = mustCreateTempFile(tempOutFilename, nil) 57 | defer os.Remove(tempOutFilename) 58 | //tempOutFilename := outFilename + ".epub" 59 | 60 | writeEpub_Go101(tempOutFilename, e, -1, bookWebsite, projectName, indexArticleTitle, bookProjectDir, coverImagePath, cssPath, target, engVersion) 61 | 62 | removePagesFromEpub(tempOutFilename, "EPUB/xhtml/cover.xhtml") 63 | 64 | epub2pdf := func(serifFont, fontSize, inputFilename, outputFilename string) { 65 | conversionParameters := make([]string, 0, 32) 66 | pushParams := func(params ...string) { 67 | conversionParameters = append(conversionParameters, params...) 68 | } 69 | pushParams(inputFilename, outputFilename) 70 | pushParams("--toc-title", indexArticleTitle) 71 | pushParams("--pdf-header-template", `

    _SECTION_

    `) 72 | pushParams("--pdf-footer-template", `

    _PAGENUM_

    `) 73 | //pushParams("--pdf-page-numbers") 74 | pushParams("--paper-size", "a4") 75 | pushParams("--pdf-serif-family", serifFont) 76 | //pushParams("--pdf-sans-family", serifFont) 77 | pushParams("--pdf-mono-family", "Liberation Mono") 78 | pushParams("--pdf-default-font-size", fontSize) 79 | pushParams("--pdf-mono-font-size", "15") 80 | pushParams("--pdf-page-margin-top", "36") 81 | pushParams("--pdf-page-margin-bottom", "36") 82 | if forPrint { 83 | pushParams("--pdf-add-toc") 84 | pushParams("--pdf-page-margin-left", "72") 85 | pushParams("--pdf-page-margin-right", "72") 86 | } else { 87 | pushParams("--pdf-page-margin-left", "36") 88 | pushParams("--pdf-page-margin-right", "36") 89 | } 90 | pushParams("--preserve-cover-aspect-ratio") 91 | 92 | runShellCommand(".", "ebook-convert", conversionParameters...) 93 | 94 | log.Println("Create", outputFilename, "done!") 95 | } 96 | 97 | if forPrint { 98 | outFilenameForPrinting := strings.Replace(outFilename, ".pdf", ".pdf-ForPrinting.pdf", 1) 99 | if projectName == "Go101" { 100 | epub2pdf("Liberation Serif", "17", tempOutFilename, outFilenameForPrinting) 101 | } else if projectName == "Golang101" { 102 | epub2pdf("AR PL SungtiL GB", "16", tempOutFilename, outFilenameForPrinting) 103 | } 104 | } else { 105 | if projectName == "Go101" { 106 | epub2pdf("Liberation Serif", "17", tempOutFilename, outFilename) 107 | } else if projectName == "Golang101" { 108 | outFilenameKaiTi := strings.Replace(outFilename, ".pdf", ".pdf-KaiTi.pdf", 1) 109 | epub2pdf("AR PL KaitiM GB", "16", tempOutFilename, outFilenameKaiTi) 110 | 111 | outFilenameSongTi := strings.Replace(outFilename, ".pdf", ".pdf-SongTi.pdf", 1) 112 | epub2pdf("AR PL SungtiL GB", "16", tempOutFilename, outFilenameSongTi) 113 | } 114 | } 115 | 116 | return outFilename 117 | } 118 | --------------------------------------------------------------------------------