├── .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("
`)
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 |
516 | `)
517 | buf.WriteString(b.String())
518 | buf.WriteString(`
519 |
520 |
521 | `)
522 | buf.Write(content[preStart:programStart])
523 | buf.WriteString(temp)
524 | buf.Write(content[codeClose:preEnd])
525 | buf.WriteString(`
526 |
527 |
528 |
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 |
--------------------------------------------------------------------------------