10 Commits

Author SHA1 Message Date
T.v.Dein
cb671b7401 Fix crash and add support for content in <div> (#9)
* fix #8: better regex to remove html entities
* add opf and ncx debug
* make epub content retrieval more flexible
* fix epub content retrieval: also support html files with <div>
2025-10-19 22:30:13 +02:00
T.v.Dein
2c6e81a2c8 add cover image support (#7) 2025-10-19 20:39:04 +02:00
c2abc4ba4d gofmt 2025-10-17 14:22:03 +02:00
b9b0ad8603 add badges 2025-10-17 14:19:16 +02:00
T.v.Dein
08f470e0d5 Ui features (#6)
* fix #3: added h ui command to toggle ui
* fix #4: added -N flag to disable colors
* fixed styling of line numbers (-n)
* fix #5 add usage section
* add pkg/epub module readme
* add license terms to all file headers
* fixed usage printing
* added basic unit tests
* run the tests only on linux due to the use of base64 utitlity
2025-10-17 14:10:45 +02:00
T.v.Dein
b50c6acff0 fix XML parsing (#2)
- Use antchfx/xmlquery for easier XML parsing. No more regexp wrangling and the result is much more reliable over a variety of ebooks. Much good.
- fix chapter selection, look for `<?xml[...]` which is much more reliable
- add option `-x` to dump the XML ebook source for debugging
2025-10-16 18:57:05 +02:00
90d30cb3e1 enhanced wording 2025-10-16 13:03:42 +02:00
f942378b47 fixed copy/paste errors 2025-10-16 13:02:35 +02:00
3d3bbe3266 clarified purpose of epuppy 2025-10-16 12:50:37 +02:00
84307c42eb added contrib and minimal technical coc 2025-10-16 12:50:22 +02:00
30 changed files with 2454 additions and 126 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 79 KiB

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 32 KiB

View File

@@ -21,9 +21,29 @@ jobs:
- name: build - name: build
run: go build run: go build
test:
strategy:
matrix:
version: [1.24.9]
os: [ubuntu-latest]
name: Test
runs-on: ${{ matrix.os }}
steps:
- name: Set up Go ${{ matrix.os }}
uses: actions/setup-go@v6
with:
go-version: '${{ matrix.version }}'
id: go
- name: checkout
uses: actions/checkout@v5
- name: test
run: go test -cover ./...
golangci: golangci:
name: lint name: Lintercheck
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/setup-go@v6 - uses: actions/setup-go@v6

18
CODE_OF_CONDUCT.md Normal file
View File

@@ -0,0 +1,18 @@
# CODE_OF_CONDUCT.md (compact model)
Purpose: Maintain a collaborative, harassment-free environment focused
on shipping quality software.
Standards: Be respectful; assume good intent; no harassment; no
discrimination; keep critiques technical.
Scope: All project spaces (issues, PRs, forums, events).
Reporting: Email tom AT vondein DOT org. Acknowledge within 72 hours.
Enforcement: Two maintainers review, one recuses on
conflict. Sanctions range from warning to removal. Summary posted (no
personal details).
Escalation: If you believe maintainers handled a report in bad faith,
escalate to our foundation committee (link) for independent review.

94
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,94 @@
## Project Goals
The idea behind this project is to build a small TUI tool to be able
to just take a look into some epub file without the need to leave the
shell. It has to be fast enough to just peak into an ebook and should
be small and easy to understand.
There will be no GUI, no web interface, no public API of some sort, no
builtin interpreter. It is not intended to build a full blown ebook
reader.
The programming language used for this project will always be
[GOLANG](https://go.dev/) with the exception of the documentation
([Perl POD](https://perldoc.perl.org/perlpod)) and the Makefile.
# Contributing
You can contribute to this project in various ways:
## Open an issue
If you encounter a problem or don't understand how the program works
or if you think the documentation is unclear, please don't hesitate to
open an issue.
Please add as much information about the case as possible, such as:
- Your environment (operating system etc)
- program version
- Input data. Please replace sensitive information with mock data!
- Actual program output.
- Expected program output.
- Error message - if any.
Be aware that I am working on this (and some other) project in my
spare time which is scarce. Therefore please don't expect me to
respond to your query within hours or even days. Be patient, but I
WILL respond.
## Pull Requests
Code and documentation help is always much appreciated! Please follow
thes guidelines to successfully contribute:
- Every pull request shall be based on latest `development`
branch. `main` is only used for releases.
- Execute the unit tests before committing: `make test`. There shall
be no errors.
- Strive to be backwards compatible so that users who are already
using the program don't have to change their habits - unless it is
really neccessary.
- Try to add a unit test for your addition.
- Don't ever change existing unit tests!
- Add a meaningful and comprehensive rationale about your contribution:
- Why do you think it might be useful for others?
- What did you actually change or add?
- Is there an open issue which this PR fixes and if so, please link
to that issue.
- [Re-]format your code with `gofmt -s`.
- Avoid unneccesary dependencies, especially for very small functions.
- **If** a new dependency is being added, it must be compatible with
our [license agreement](LICENSE).
- You need to accept that the code or documentation you contribute
will be redistributed under the terms of said license agreement. If
your contribution is considerably large or if you contribute
regularly, then feel free to add your name and if you want your
email address to the *AUTHORS* section of the
manual page.
- Adhere to the above mentioned project goals.
- If you are unsure if your addition or change will be accepted,
better ask before starting coding. Open an issue about your proposal
and let's discuss it! That way we avoid doing unnessesary work on
both sides.
Each pull request will be carefully reviewed and if it is a useful
addition it will be accepted. However, please be prepared that
sometimes a PR will be rejected. The reasons may vary and will be
documented. Perhaps the above guidelines are not matched, or the
addition seems to be not so useful from my perspective, maybe there
are too much changes or there might be changes I don't even
understand.
But whatever happens: your contribution is always welcome!

View File

@@ -1,3 +1,8 @@
[![Actions](https://github.com/tlinden/epuppy/actions/workflows/ci.yaml/badge.svg)](https://github.com/tlinden/epuppy/actions)
[![License](https://img.shields.io/badge/license-GPL-blue.svg)](https://github.com/tlinden/epuppy/blob/master/LICENSE)
[![Go Report Card](https://goreportcard.com/badge/github.com/tlinden/epuppy)](https://goreportcard.com/report/github.com/tlinden/epuppy)
# epuppy - terminal epub reader # epuppy - terminal epub reader
This is a little TUI epub ebook reader. This is a work in progress and This is a little TUI epub ebook reader. This is a work in progress and
@@ -6,6 +11,13 @@ may not work for all EPUB files yet. It uses a modified version of the
unmaintained but the best I could find to parse EPUBs. Find it in the unmaintained but the best I could find to parse EPUBs. Find it in the
`pkg/epub/` directory. `pkg/epub/` directory.
The idea behind this tool is to be able to just take a look into some
epub file without the need to leave the shell. And it had to be fast
enough to just peak into an ebook. However, it is possible to actually
read epub ebooks with epuppy but I'd encourage you to buy a hardware
ebook reader with an e-ink display. It's better for your eyes in the
long run.
## Screenshots ## Screenshots
- Viewing an ebook in dark mode - Viewing an ebook in dark mode
@@ -20,6 +32,66 @@ unmaintained but the best I could find to parse EPUBs. Find it in the
- Showing the help - Showing the help
![Screenshot](https://github.com/TLINDEN/epuppy/blob/main/.github/assets/help.png) ![Screenshot](https://github.com/TLINDEN/epuppy/blob/main/.github/assets/help.png)
## Usage
To read an ebook, just give a filename as argument to `epuppy`.
Add the option `-s` to store and use a previously stored reading
progress.
Sometimes you may be unhappy with the colors. Depending on your
terminal style you can enable dark mode with `-D`, light mode is the
default. You can also configure custom colors in a config file in
`$HOME/.config/epuppy/confit.toml`:
```toml
# color setting for dark mode
colordark = {
body = "#ffffff",
title = "#7cfc00",
chapter = "#ffff00"
}
# color setting for light mode
colorlight = {
body = "#000000",
title = "#8470ff",
chapter = "#00008b"
}
# always use dark mode
dark = true
```
There are also cases where your current terminal just doesn't have the
capabilites for this stuff. I stumbled upon such a case during an SSH
session from my Android phone to a FreeBSD server. For this you can
either just disable colors with `-N` or by setting the environment
variable `$NO_COLOR` to 1. Or you can just dump the text of the ebook
and pipe it to some pager, e.g.:
```default
epuppy -t someebook.epub | less
```
There are also a couple of debug options etc, all options:
```default
Usage epuppy [options] <epub file>
Options:
-D --dark enable dark mode
-s --store-progress remember reading position
-n --line-numbers add line numbers
-c --config <file> use config <file>
-t --txt dump readable content to STDOUT
-x --xml dump source xml to STDOUT
-N --no-color disable colors (or use $NO_COLOR env var)
-d --debug enable debugging
-h --help show help message
-v --version show program version
```
## Installation ## Installation
The tool does not have any dependencies. Just download the binary for The tool does not have any dependencies. Just download the binary for
@@ -76,4 +148,4 @@ version 3.
# Author # Author
Copyleft (c) 2024 Thomas von Dein Copyleft (c) 2025 Thomas von Dein

View File

@@ -1,3 +1,19 @@
/*
Copyright © 2025 Thomas von Dein
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package cmd package cmd
import ( import (

View File

@@ -1,3 +1,19 @@
/*
Copyright © 2025 Thomas von Dein
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package cmd package cmd
import ( import (
@@ -16,15 +32,20 @@ import (
) )
const ( const (
Version string = `v0.0.2` Version string = `v0.0.5`
Usage string = `Usage epuppy [options] <epub file> Usage string = `This is epuppy, a terminal ui ebook viewer.
Usage: epuppy [options] <epub file>
Options: Options:
-D --dark enable dark mode -D --dark enable dark mode
-s --store-progress remember reading position -s --store-progress remember reading position
-n --line-numbers add line numbers -n --line-numbers add line numbers
-c --config <file> use config <file> -c --config <file> use config <file>
-i --cover-image display cover image
-t --txt dump readable content to STDOUT -t --txt dump readable content to STDOUT
-x --xml dump source xml to STDOUT
-N --no-color disable colors (or use $NO_COLOR env var)
-d --debug enable debugging -d --debug enable debugging
-h --help show help message -h --help show help message
-v --version show program version` -v --version show program version`
@@ -37,10 +58,13 @@ type Config struct {
Darkmode bool `koanf:"dark"` // -D Darkmode bool `koanf:"dark"` // -D
LineNumbers bool `koanf:"line-numbers"` // -n LineNumbers bool `koanf:"line-numbers"` // -n
Dump bool `koanf:"txt"` // -t Dump bool `koanf:"txt"` // -t
XML bool `koanf:"xml"` // -x
NoColor bool `koanf:"no-color"` // -n
Config string `koanf:"config"` // -c Config string `koanf:"config"` // -c
ColorDark ColorSetting `koanf:"colordark"` // comes from config file only ColorDark ColorSetting `koanf:"colordark"` // comes from config file only
ColorLight ColorSetting `koanf:"colorlight"` // comes from config file only ColorLight ColorSetting `koanf:"colorlight"` // comes from config file only
ShowHelp bool `koanf:"help"`
ShowCover bool `koanf:"cover-image"` // -i
Colors Colors // generated from user config file or internal defaults, respects dark mode Colors Colors // generated from user config file or internal defaults, respects dark mode
Document string Document string
InitialProgress int // lines InitialProgress int // lines
@@ -56,7 +80,6 @@ func InitConfig(output io.Writer) (*Config, error) {
if err != nil { if err != nil {
log.Fatalf("failed to print to output: %s", err) log.Fatalf("failed to print to output: %s", err)
} }
os.Exit(0)
} }
// parse commandline flags // parse commandline flags
@@ -66,6 +89,10 @@ func InitConfig(output io.Writer) (*Config, error) {
flagset.BoolP("store-progress", "s", false, "store reading progress") flagset.BoolP("store-progress", "s", false, "store reading progress")
flagset.BoolP("line-numbers", "n", false, "add line numbers") flagset.BoolP("line-numbers", "n", false, "add line numbers")
flagset.BoolP("txt", "t", false, "dump readable content to STDOUT") flagset.BoolP("txt", "t", false, "dump readable content to STDOUT")
flagset.BoolP("xml", "x", false, "dump xml to STDOUT")
flagset.BoolP("no-color", "N", false, "disable colors")
flagset.BoolP("cover-image", "i", false, "show cover image")
flagset.BoolP("help", "h", false, "show help")
flagset.StringP("config", "c", "", "read config from file") flagset.StringP("config", "c", "", "read config from file")
if err := flagset.Parse(os.Args[1:]); err != nil { if err := flagset.Parse(os.Args[1:]); err != nil {
@@ -114,8 +141,9 @@ func InitConfig(output io.Writer) (*Config, error) {
if len(flagset.Args()) > 0 { if len(flagset.Args()) > 0 {
conf.Document = flagset.Args()[0] conf.Document = flagset.Args()[0]
} else { } else {
if !conf.Showversion { if !conf.Showversion && !conf.ShowHelp {
flagset.Usage() flagset.Usage()
os.Exit(1)
} }
} }
@@ -137,6 +165,11 @@ func InitConfig(output io.Writer) (*Config, error) {
}, },
conf) conf)
// disable colors if requested by command line
if conf.NoColor {
_ = os.Setenv("NO_COLOR", "1")
}
return conf, nil return conf, nil
} }

View File

@@ -1,3 +1,19 @@
/*
Copyright © 2025 Thomas von Dein
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package cmd package cmd
// pager setup using bubbletea // pager setup using bubbletea
@@ -5,10 +21,13 @@ package cmd
// https://github.com/charmbracelet/bubbletea/tree/main/examples/pager // https://github.com/charmbracelet/bubbletea/tree/main/examples/pager
import ( import (
"bytes"
"fmt" "fmt"
"image"
"os" "os"
"strings" "strings"
"github.com/blacktop/go-termimg"
"github.com/charmbracelet/bubbles/help" "github.com/charmbracelet/bubbles/help"
"github.com/charmbracelet/bubbles/key" "github.com/charmbracelet/bubbles/key"
"github.com/charmbracelet/bubbles/viewport" "github.com/charmbracelet/bubbles/viewport"
@@ -52,12 +71,16 @@ type keyMap struct {
Right key.Binding Right key.Binding
Help key.Binding Help key.Binding
Quit key.Binding Quit key.Binding
ToggleUI key.Binding
Pad key.Binding
} }
func (k keyMap) FullHelp() [][]key.Binding { func (k keyMap) FullHelp() [][]key.Binding {
return [][]key.Binding{ return [][]key.Binding{
{k.Up, k.Down, k.Left, k.Right}, // first column // every item is one column
{k.Help, k.Quit}, // second column {k.Up, k.Down, k.Left, k.Right},
{k.Pad}, // fake key, we use it as spacing between columns
{k.Help, k.Quit, k.ToggleUI},
} }
} }
@@ -66,6 +89,10 @@ func (k keyMap) ShortHelp() []key.Binding {
} }
var keys = keyMap{ var keys = keyMap{
Pad: key.NewBinding(
key.WithKeys("__"),
key.WithHelp(" ", ""),
),
Up: key.NewBinding( Up: key.NewBinding(
key.WithKeys("up", "k"), key.WithKeys("up", "k"),
key.WithHelp("↑", "move up"), key.WithHelp("↑", "move up"),
@@ -75,11 +102,11 @@ var keys = keyMap{
key.WithHelp("↓", "move down"), key.WithHelp("↓", "move down"),
), ),
Left: key.NewBinding( Left: key.NewBinding(
key.WithKeys("left", "h"), key.WithKeys("left"),
key.WithHelp("←", "decrease text width"), key.WithHelp("←", "decrease text width"),
), ),
Right: key.NewBinding( Right: key.NewBinding(
key.WithKeys("right", "l"), key.WithKeys("right"),
key.WithHelp("→", "increase text width"), key.WithHelp("→", "increase text width"),
), ),
Help: key.NewBinding( Help: key.NewBinding(
@@ -90,6 +117,10 @@ var keys = keyMap{
key.WithKeys("q", "esc", "ctrl+c"), key.WithKeys("q", "esc", "ctrl+c"),
key.WithHelp("q", "quit"), key.WithHelp("q", "quit"),
), ),
ToggleUI: key.NewBinding(
key.WithKeys("h"),
key.WithHelp("h", "toggle ui"),
),
} }
type Doc struct { type Doc struct {
@@ -101,8 +132,10 @@ type Doc struct {
lastwidth int lastwidth int
margin int margin int
marginMod bool marginMod bool
hideUi bool
meta *Meta meta *Meta
config *Config config *Config
Cover *termimg.ImageWidget
keys keyMap keys keyMap
help help.Model help help.Model
@@ -135,6 +168,8 @@ func (m Doc) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.margin -= MarginStep m.margin -= MarginStep
m.marginMod = true m.marginMod = true
} }
case key.Matches(msg, m.keys.ToggleUI):
m.hideUi = !m.hideUi
} }
case tea.WindowSizeMsg: case tea.WindowSizeMsg:
@@ -189,7 +224,12 @@ func (m *Doc) Rewrap() {
// wrapping has changed, update viewport and line count // wrapping has changed, update viewport and line count
if content != "" { if content != "" {
if m.Cover != nil {
cover, _ := m.Cover.Render()
m.viewport.SetContent(cover + "\n\n" + content)
} else {
m.viewport.SetContent(content) m.viewport.SetContent(content)
}
m.meta.lines = len(strings.Split(content, "\n")) m.meta.lines = len(strings.Split(content, "\n"))
} }
@@ -212,6 +252,10 @@ func (m Doc) View() string {
helpView = "\n" + m.help.View(m.keys) helpView = "\n" + m.help.View(m.keys)
} }
if m.hideUi {
return fmt.Sprintf("%s\n%s", m.viewport.View(), helpView)
}
return fmt.Sprintf("%s\n%s\n%s%s", m.headerView(), m.viewport.View(), m.footerView(), helpView) return fmt.Sprintf("%s\n%s\n%s%s", m.headerView(), m.viewport.View(), m.footerView(), helpView)
} }
@@ -235,7 +279,15 @@ func max(a, b int) int {
return b return b
} }
func Pager(conf *Config, title, message string) (int, error) { type Ebook struct {
Config *Config
Title string
Body string
Cover []byte
MediaType string
}
func Pager(book *Ebook) (int, error) {
width := 80 width := 80
scrollto := 0 scrollto := 0
@@ -246,32 +298,44 @@ func Pager(conf *Config, title, message string) (int, error) {
} }
} }
if conf.StoreProgress { if book.Config.StoreProgress {
scrollto = conf.InitialProgress scrollto = book.Config.InitialProgress
} }
if conf.LineNumbers { if book.Config.LineNumbers {
catn := "" catn := ""
for idx, line := range strings.Split(message, "\n") { for idx, line := range strings.Split(book.Body, "\n") {
catn += fmt.Sprintf("%d: %s\n", idx, line) catn += fmt.Sprintf("%4d: %s\n", idx, line)
} }
message = catn book.Body = catn
} }
meta := Meta{ meta := Meta{
initialprogress: scrollto, initialprogress: scrollto,
lines: len(strings.Split(message, "\n")), lines: len(strings.Split(book.Body, "\n")),
}
doc := Doc{
content: book.Body,
title: book.Title,
initialwidth: width,
meta: &meta,
config: book.Config,
keys: keys,
}
if book.MediaType != "" && book.Config.ShowCover {
img, _, err := image.Decode(bytes.NewReader(book.Cover))
if err != nil {
return 0, fmt.Errorf("failed to load cover image: %w", err)
}
doc.Cover = termimg.NewImageWidgetFromImage(img)
doc.Cover.SetSize(width, width).SetProtocol(termimg.Auto)
} }
p := tea.NewProgram( p := tea.NewProgram(
Doc{ doc,
content: message,
title: title,
initialwidth: width,
meta: &meta,
config: conf,
keys: keys,
},
tea.WithAltScreen(), // use the full size of the terminal in its "alternate screen buffer" tea.WithAltScreen(), // use the full size of the terminal in its "alternate screen buffer"
tea.WithMouseCellMotion(), // turn on mouse support so we can track the mouse wheel tea.WithMouseCellMotion(), // turn on mouse support so we can track the mouse wheel
) )
@@ -280,7 +344,7 @@ func Pager(conf *Config, title, message string) (int, error) {
return 0, fmt.Errorf("could not run pager: %w", err) return 0, fmt.Errorf("could not run pager: %w", err)
} }
if conf.Debug { if book.Config.Debug {
fmt.Printf("scrollto: %d, last: %d, diff: %d\n", fmt.Printf("scrollto: %d, last: %d, diff: %d\n",
scrollto, meta.currentline, scrollto-meta.currentline) scrollto, meta.currentline, scrollto-meta.currentline)
} }

View File

@@ -1,3 +1,19 @@
/*
Copyright © 2025 Thomas von Dein
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package cmd package cmd
import ( import (
@@ -27,6 +43,15 @@ func Execute(output io.Writer) int {
return 0 return 0
} }
if conf.ShowHelp {
_, err := fmt.Fprintln(output, Usage)
if err != nil {
return Die(fmt.Errorf("failed to print to output: %s", err))
}
return 0
}
if conf.StoreProgress { if conf.StoreProgress {
progress, err := GetProgress(conf) progress, err := GetProgress(conf)
if err == nil { if err == nil {

View File

@@ -1,3 +1,19 @@
/*
Copyright © 2025 Thomas von Dein
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package cmd package cmd
import ( import (

View File

@@ -1,12 +1,28 @@
/*
Copyright © 2025 Thomas von Dein
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package cmd package cmd
import ( import (
"fmt" "fmt"
"log"
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
"github.com/alecthomas/repr"
"github.com/tlinden/epuppy/pkg/epub" "github.com/tlinden/epuppy/pkg/epub"
) )
@@ -29,20 +45,23 @@ func ViewText(conf *Config) (int, error) {
return fmt.Println(string(data)) return fmt.Println(string(data))
} }
return Pager(conf, conf.Document, string(data)) return Pager(&Ebook{
Config: conf,
Title: conf.Document,
Body: string(data),
})
} }
func ViewEpub(conf *Config) (int, error) { func ViewEpub(conf *Config) (int, error) {
book, err := epub.Open(conf.Document) book, err := epub.Open(conf.Document, conf.XML)
if err != nil { if err != nil {
return 0, err return 0, err
} }
defer func() { if conf.Debug {
if err := book.Close(); err != nil { repr.Println(book.Files())
log.Fatal(err) repr.Println(book.Ncx)
} }
}()
buf := strings.Builder{} buf := strings.Builder{}
head := strings.Builder{} head := strings.Builder{}
@@ -59,9 +78,28 @@ func ViewEpub(conf *Config) (int, error) {
head.WriteString(" ") head.WriteString(" ")
} }
// FIXME: since the switch to book.Files() in epub.Open() this fetchByContent(conf, &buf, book)
// returns invalid chapter numbering
if conf.Dump {
return fmt.Println(buf.String())
}
if conf.Debug {
return 0, nil
}
return Pager(&Ebook{
Config: conf,
Title: head.String(),
Body: buf.String(),
Cover: book.CoverImage,
MediaType: book.CoverMediaType,
})
}
func fetchByContent(conf *Config, buf *strings.Builder, book *epub.Book) bool {
chapter := 1 chapter := 1
var gotbody bool
for _, content := range book.Content { for _, content := range book.Content {
if len(content.Body) > 0 { if len(content.Body) > 0 {
@@ -81,13 +119,10 @@ func ViewEpub(conf *Config) (int, error) {
buf.WriteString("\r\n\r\n\r\n\r\n") buf.WriteString("\r\n\r\n\r\n\r\n")
chapter++ chapter++
gotbody = true
}
} }
} return gotbody
if conf.Dump {
return fmt.Println(buf.String())
}
return Pager(conf, head.String(), buf.String())
} }

15
go.mod
View File

@@ -6,6 +6,7 @@ toolchain go1.24.9
require ( require (
github.com/alecthomas/repr v0.5.2 github.com/alecthomas/repr v0.5.2
github.com/antchfx/xmlquery v1.5.0
github.com/knadh/koanf/parsers/toml v0.1.0 github.com/knadh/koanf/parsers/toml v0.1.0
github.com/knadh/koanf/providers/file v1.2.0 github.com/knadh/koanf/providers/file v1.2.0
github.com/knadh/koanf/providers/posflag v1.0.1 github.com/knadh/koanf/providers/posflag v1.0.1
@@ -35,14 +36,26 @@ require (
github.com/rivo/uniseg v0.4.7 // indirect github.com/rivo/uniseg v0.4.7 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
golang.org/x/sys v0.37.0 // indirect golang.org/x/sys v0.37.0 // indirect
golang.org/x/text v0.3.8 // indirect golang.org/x/text v0.30.0 // indirect
) )
require ( require (
github.com/antchfx/xpath v1.3.5 // indirect
github.com/blacktop/go-termimg v0.1.20 // indirect
github.com/charmbracelet/x/mosaic v0.0.0-20250702191427-5bdfc8f2e4ff // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/knadh/koanf/maps v0.1.2 // indirect github.com/knadh/koanf/maps v0.1.2 // indirect
github.com/makeworld-the-better-one/dither/v2 v2.4.0 // indirect
github.com/mattn/go-sixel v0.0.5 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect
github.com/pelletier/go-toml v1.9.5 // indirect github.com/pelletier/go-toml v1.9.5 // indirect
github.com/rogpeppe/go-internal v1.14.1 // indirect
github.com/soniakeys/quant v1.0.0 // indirect
golang.org/x/image v0.25.0 // indirect
golang.org/x/net v0.44.0 // indirect
golang.org/x/tools v0.37.0 // indirect
) )

101
go.sum
View File

@@ -1,9 +1,15 @@
github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs= github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs=
github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
github.com/antchfx/xmlquery v1.5.0 h1:uAi+mO40ZWfyU6mlUBxRVvL6uBNZ6LMU4M3+mQIBV4c=
github.com/antchfx/xmlquery v1.5.0/go.mod h1:lJfWRXzYMK1ss32zm1GQV3gMIW/HFey3xDZmkP1SuNc=
github.com/antchfx/xpath v1.3.5 h1:PqbXLC3TkfeZyakF5eeh3NTWEbYl4VHNVeufANzDbKQ=
github.com/antchfx/xpath v1.3.5/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs=
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8= github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8=
github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA= github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA=
github.com/blacktop/go-termimg v0.1.20 h1:+EAUc3c9hwE/fUYaqRV1BSLvAlOuLySgLTEBzxGbYK4=
github.com/blacktop/go-termimg v0.1.20/go.mod h1:nwxrOjfFcBjtS358oIGBLfscSLnCpNdRlMVRxsnZwMU=
github.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs= github.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs=
github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg= github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg=
github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw= github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
@@ -18,8 +24,11 @@ github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0G
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 h1:payRxjMjKgx2PaCWLZ4p3ro9y97+TVLZNaRZgJwSVDQ= github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 h1:payRxjMjKgx2PaCWLZ4p3ro9y97+TVLZNaRZgJwSVDQ=
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U= github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
github.com/charmbracelet/x/mosaic v0.0.0-20250702191427-5bdfc8f2e4ff h1:OVBKPzoa0k5ZVMoor27BReRZxER1IEDtLHXkRjaHElg=
github.com/charmbracelet/x/mosaic v0.0.0-20250702191427-5bdfc8f2e4ff/go.mod h1:5qLP4S++M5quSc/xbvWWW8vKkgKwOqOT/IVhAas26XI=
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
@@ -28,6 +37,9 @@ github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/knadh/koanf/maps v0.1.2 h1:RBfmAW5CnZT+PJ1CVc1QSJKf4Xu9kxfQgYVQSu8hpbo= github.com/knadh/koanf/maps v0.1.2 h1:RBfmAW5CnZT+PJ1CVc1QSJKf4Xu9kxfQgYVQSu8hpbo=
github.com/knadh/koanf/maps v0.1.2/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI= github.com/knadh/koanf/maps v0.1.2/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI=
github.com/knadh/koanf/parsers/toml v0.1.0 h1:S2hLqS4TgWZYj4/7mI5m1CQQcWurxUz6ODgOub/6LCI= github.com/knadh/koanf/parsers/toml v0.1.0 h1:S2hLqS4TgWZYj4/7mI5m1CQQcWurxUz6ODgOub/6LCI=
@@ -40,6 +52,8 @@ github.com/knadh/koanf/v2 v2.3.0 h1:Qg076dDRFHvqnKG97ZEsi9TAg2/nFTa9hCdcSa1lvlM=
github.com/knadh/koanf/v2 v2.3.0/go.mod h1:gRb40VRAbd4iJMYYD5IxZ6hfuopFcXBpc9bbQpZwo28= github.com/knadh/koanf/v2 v2.3.0/go.mod h1:gRb40VRAbd4iJMYYD5IxZ6hfuopFcXBpc9bbQpZwo28=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/makeworld-the-better-one/dither/v2 v2.4.0 h1:Az/dYXiTcwcRSe59Hzw4RI1rSnAZns+1msaCXetrMFE=
github.com/makeworld-the-better-one/dither/v2 v2.4.0/go.mod h1:VBtN8DXO7SNtyGmLiGA7IsFeKrBkQPze1/iAeM95arc=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
@@ -47,6 +61,8 @@ github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+Ei
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-sixel v0.0.5 h1:55w2FR5ncuhKhXrM5ly1eiqMQfZsnAHIpYNGZX03Cv8=
github.com/mattn/go-sixel v0.0.5/go.mod h1:h2Sss+DiUEHy0pUqcIB6PFXo5Cy8sTQEFr3a9/5ZLNw=
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
@@ -59,6 +75,8 @@ github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
@@ -67,21 +85,100 @@ github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJ
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/soniakeys/quant v1.0.0 h1:N1um9ktjbkZVcywBVAAYpZYSHxEfJGzshHCxx/DaI0Y=
github.com/soniakeys/quant v1.0.0/go.mod h1:HI1k023QuVbD4H8i9YdfZP2munIHU4QpjsImz6Y6zds=
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E= golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E=
golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE=
golang.org/x/image v0.25.0 h1:Y6uW6rH1y5y/LK1J8BPWZtr6yZ7hrsy6hFrXjgsc2fQ=
golang.org/x/image v0.25.0/go.mod h1:tCAmOEGthTtkalusGp1g3xa2gke8J6c2N565dTyl9Rs=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I=
golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q= golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q=
golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss= golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss=
golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE=
golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

16
main.go
View File

@@ -1,3 +1,19 @@
/*
Copyright © 2025 Thomas von Dein
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package main package main
import ( import (

38
main_test.go Normal file
View File

@@ -0,0 +1,38 @@
/*
Copyright © 2025 Thomas von Dein
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package main
import (
"testing"
"github.com/rogpeppe/go-internal/testscript"
)
// see https://bitfieldconsulting.com/golang/test-scripts
func TestMain(m *testing.M) {
testscript.Main(m, map[string]func(){
"epuppy": main,
})
}
func TestEpuppy(t *testing.T) {
testscript.Run(t, testscript.Params{
Dir: "t",
})
}

View File

@@ -1,6 +1,6 @@
#!/bin/bash #!/bin/bash
# Copyright © 2024 Thomas von Dein # Copyright © 2025 Thomas von Dein
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by # it under the terms of the GNU General Public License as published by

21
pkg/epub/README.md Normal file
View File

@@ -0,0 +1,21 @@
# epub module
This is a modified version of the [epub
module](https://github.com/kapmahc/epub/). I fixed a couple of issues
and I added code to retrieve the actual epub content, which the
original module doesn't implement. Since the last update on that
module was 9 years ago (as I write this), it doesn't make sense to
send a PR. And it also makes no sense to me to publish it separately.
# ORIGINAL README
## epub
A pure go implementation of epub file format.
### Documents
- <http://idpf.org/epub>
- <http://www.cnblogs.com/Alex80/p/5127104.html>
- <http://www.cnblogs.com/diligenceday/p/4999315.html>

View File

@@ -16,8 +16,10 @@ type Book struct {
Container Container `json:"-"` Container Container `json:"-"`
Mimetype string `json:"-"` Mimetype string `json:"-"`
Content []Content Content []Content
fd *zip.ReadCloser fd *zip.ReadCloser
CoverImage []byte
CoverFile string
CoverMediaType string
} }
// Open open resource file // Open open resource file
@@ -34,11 +36,6 @@ func (p *Book) Files() []string {
return fns return fns
} }
// Close close file reader
func (p *Book) Close() error {
return p.fd.Close()
}
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
func (p *Book) filename(n string) string { func (p *Book) filename(n string) string {
return path.Join(path.Dir(p.Container.Rootfile.Path), n) return path.Join(path.Dir(p.Container.Rootfile.Path), n)

View File

@@ -1,19 +1,17 @@
package epub package epub
import ( import (
"encoding/xml"
"fmt"
"regexp" "regexp"
"strings" "strings"
"github.com/antchfx/xmlquery"
) )
var ( var (
cleantitle = regexp.MustCompile(`(?s)<head>.*</head>`) cleanentitles = regexp.MustCompile(`&[a-z]+;`)
empty = regexp.MustCompile(`(?s)^[\s ]*$`)
newlines = regexp.MustCompile(`[\r\n]+`)
cleanmarkup = regexp.MustCompile(`<[^<>]+>`) cleanmarkup = regexp.MustCompile(`<[^<>]+>`)
cleanentities = regexp.MustCompile(`&.+;`)
cleancomments = regexp.MustCompile(`/*.*/`)
cleanspace = regexp.MustCompile(`^\s*`)
cleanh1 = regexp.MustCompile(`<h[1-6].*</h[1-6]>`)
) )
// Content nav-point content // Content nav-point content
@@ -25,26 +23,46 @@ type Content struct {
XML []byte XML []byte
} }
// parse XML, look for title and <p>.*</p> stuff
func (c *Content) String(content []byte) error { func (c *Content) String(content []byte) error {
title := Title{} doc, err := xmlquery.Parse(
strings.NewReader(
err := xml.Unmarshal(content, &title) cleanentitles.ReplaceAllString(string(content), " ")))
if err != nil { if err != nil {
if !strings.HasPrefix(err.Error(), "XML syntax error") { return err
return fmt.Errorf("XML parser error %w", err) }
if c.Title == "" {
// extract the title
for _, item := range xmlquery.Find(doc, "//title") {
c.Title = strings.TrimSpace(item.InnerText())
} }
} }
c.Title = strings.TrimSpace(title.Content) // extract all paragraphs, ignore any formatting and re-fill the
// paragraph, that is, we replace all newlines inside with one
// space.
txt := strings.Builder{}
var have_p bool
for _, item := range xmlquery.Find(doc, "//p") {
if !empty.MatchString(item.InnerText()) {
have_p = true
txt.WriteString(newlines.ReplaceAllString(item.InnerText(), " ") + "\n\n")
}
}
txt := cleantitle.ReplaceAllString(string(content), "") if !have_p {
txt = cleanh1.ReplaceAllString(txt, "") // try <div></div>, which some ebooks use, so get all divs,
txt = cleanmarkup.ReplaceAllString(txt, "") // remove markup and paragraphify the parts
txt = cleanentities.ReplaceAllString(txt, " ") for _, item := range xmlquery.Find(doc, "//div") {
txt = cleancomments.ReplaceAllString(txt, "") if !empty.MatchString(item.InnerText()) {
txt = strings.TrimSpace(txt) cleaned := cleanmarkup.ReplaceAllString(item.InnerText(), "")
txt.WriteString(newlines.ReplaceAllString(cleaned, " ") + "\n\n")
}
}
}
c.Body = cleanspace.ReplaceAllString(txt, "") c.Body = strings.TrimSpace(txt.String())
c.XML = content c.XML = content
if len(c.Body) == 0 { if len(c.Body) == 0 {

View File

@@ -1,36 +1,22 @@
package epub package epub
import ( import (
"log"
"testing" "testing"
) )
func TestEpub(t *testing.T) { func TestEpub(t *testing.T) {
bk, err := open(t, "test.epub") _, err := open(t, "test.epub")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
defer func() {
if err := bk.Close(); err != nil {
log.Fatal(err)
}
}()
} }
func open(t *testing.T, f string) (*Book, error) { func open(t *testing.T, f string) (*Book, error) {
bk, err := Open(f) bk, err := Open(f, false)
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer func() {
if err := bk.Close(); err != nil {
log.Fatal(err)
}
}()
t.Logf("files: %+v", bk.Files()) t.Logf("files: %+v", bk.Files())
t.Logf("book: %+v", bk) t.Logf("book: %+v", bk)

View File

@@ -8,6 +8,7 @@ type Ncx struct {
// NavPoint nav point // NavPoint nav point
type NavPoint struct { type NavPoint struct {
Text string `xml:"navLabel>text" json:"text"` Text string `xml:"navLabel>text" json:"text"`
Content Content `xml:"content" json:"content"`
Points []NavPoint `xml:"navPoint" json:"points"` Points []NavPoint `xml:"navPoint" json:"points"`
} }

View File

@@ -2,12 +2,16 @@ package epub
import ( import (
"archive/zip" "archive/zip"
"fmt"
"log" "log"
"os"
"strings" "strings"
"github.com/alecthomas/repr"
) )
// Open open a epub file // Open open a epub file
func Open(fn string) (*Book, error) { func Open(fn string, dumpxml bool) (*Book, error) {
fd, err := zip.OpenReader(fn) fd, err := zip.OpenReader(fn)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -43,27 +47,66 @@ func Open(fn string) (*Book, error) {
if err != nil { if err != nil {
return &bk, err return &bk, err
} }
}
break if mf.ID == "cover-image" {
bk.CoverFile = mf.Href
bk.CoverMediaType = mf.MediaType
} }
} }
type section struct {
file, title string
}
sections := []section{}
if len(bk.Ncx.Points) > 0 {
for _, block := range bk.Ncx.Points {
sections = append(sections,
section{
file: "OEBPS/" + block.Content.Src,
title: block.Text,
})
}
} else {
for _, file := range bk.Files() { for _, file := range bk.Files() {
content, err := bk.readBytes(file) sections = append(sections,
section{
file: file,
})
}
}
for _, section := range sections {
content, err := bk.readBytes(section.file)
if err != nil { if err != nil {
return &bk, err return &bk, err
} }
ct := Content{Src: file} if strings.Contains(section.file, bk.CoverFile) {
bk.CoverImage = content
}
if strings.Contains(string(content), "DOCTYPE") { ct := Content{Src: section.file, Title: section.title}
if strings.Contains(string(content), "<?xml") || strings.Contains(string(content), "<!DOCTYPE") {
if err := ct.String(content); err != nil { if err := ct.String(content); err != nil {
return &bk, err return &bk, err
} }
} }
if dumpxml {
fmt.Println(string(ct.XML))
}
bk.Content = append(bk.Content, ct) bk.Content = append(bk.Content, ct)
} }
if dumpxml {
repr.Println(sections)
os.Exit(0)
}
return &bk, nil return &bk, nil
} }

BIN
pkg/epub/test.epub Normal file

Binary file not shown.

7
t/basic.txtar Normal file
View File

@@ -0,0 +1,7 @@
# no args shall fail
! exec epuppy
# help
exec epuppy -h
stdout 'This is epuppy'

1698
t/read-parse.txtar Normal file

File diff suppressed because it is too large Load Diff