mirror of
https://codeberg.org/scip/epuppy.git
synced 2025-12-17 12:31:02 +01:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f524083210 | ||
|
|
cb671b7401 | ||
|
|
2c6e81a2c8 | ||
| c2abc4ba4d | |||
| b9b0ad8603 | |||
|
|
08f470e0d5 |
22
.github/workflows/ci.yaml
vendored
22
.github/workflows/ci.yaml
vendored
@@ -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
|
||||||
|
|||||||
67
README.md
67
README.md
@@ -1,3 +1,8 @@
|
|||||||
|
[](https://github.com/tlinden/epuppy/actions)
|
||||||
|
[](https://github.com/tlinden/epuppy/blob/master/LICENSE)
|
||||||
|
[](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
|
||||||
@@ -27,6 +32,66 @@ long run.
|
|||||||
- Showing the help
|
- Showing the help
|
||||||

|

|
||||||
|
|
||||||
|
## 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
|
||||||
@@ -83,4 +148,4 @@ version 3.
|
|||||||
|
|
||||||
# Author
|
# Author
|
||||||
|
|
||||||
Copyleft (c) 2024 Thomas von Dein
|
Copyleft (c) 2025 Thomas von Dein
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
|||||||
@@ -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,16 +32,20 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
Version string = `v0.0.3`
|
Version string = `v0.0.6`
|
||||||
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
|
-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`
|
||||||
@@ -39,10 +59,12 @@ type Config struct {
|
|||||||
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
|
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
|
||||||
@@ -58,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
|
||||||
@@ -69,6 +90,9 @@ func InitConfig(output io.Writer) (*Config, error) {
|
|||||||
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("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 {
|
||||||
@@ -117,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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -140,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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
106
cmd/pager.go
106
cmd/pager.go
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
25
cmd/root.go
25
cmd/root.go
@@ -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 {
|
||||||
|
|||||||
16
cmd/store.go
16
cmd/store.go
@@ -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 (
|
||||||
|
|||||||
45
cmd/view.go
45
cmd/view.go
@@ -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 (
|
||||||
@@ -6,6 +22,7 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/alecthomas/repr"
|
||||||
"github.com/tlinden/epuppy/pkg/epub"
|
"github.com/tlinden/epuppy/pkg/epub"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -28,7 +45,11 @@ 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) {
|
||||||
@@ -37,6 +58,14 @@ func ViewEpub(conf *Config) (int, error) {
|
|||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if conf.Debug {
|
||||||
|
repr.Println("book.Files()")
|
||||||
|
repr.Println(book.Files())
|
||||||
|
repr.Println(book.Ncx)
|
||||||
|
repr.Println(book.Sections)
|
||||||
|
repr.Println(book.Opf.Manifest)
|
||||||
|
}
|
||||||
|
|
||||||
buf := strings.Builder{}
|
buf := strings.Builder{}
|
||||||
head := strings.Builder{}
|
head := strings.Builder{}
|
||||||
|
|
||||||
@@ -58,11 +87,19 @@ func ViewEpub(conf *Config) (int, error) {
|
|||||||
return fmt.Println(buf.String())
|
return fmt.Println(buf.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
return Pager(conf, head.String(), buf.String())
|
if conf.Debug || conf.XML {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return Pager(&Ebook{
|
||||||
|
Config: conf,
|
||||||
|
Title: head.String(),
|
||||||
|
Body: buf.String(),
|
||||||
|
Cover: book.CoverImage,
|
||||||
|
MediaType: book.CoverMediaType,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME: since the switch to book.Files() in epub.Open() this
|
|
||||||
// returns invalid chapter numbering
|
|
||||||
func fetchByContent(conf *Config, buf *strings.Builder, book *epub.Book) bool {
|
func fetchByContent(conf *Config, buf *strings.Builder, book *epub.Book) bool {
|
||||||
chapter := 1
|
chapter := 1
|
||||||
var gotbody bool
|
var gotbody bool
|
||||||
|
|||||||
11
go.mod
11
go.mod
@@ -41,12 +41,21 @@ require (
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/antchfx/xpath v1.3.5 // indirect
|
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/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
|
||||||
golang.org/x/net v0.33.0 // 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
|
||||||
)
|
)
|
||||||
|
|||||||
25
go.sum
25
go.sum
@@ -8,6 +8,8 @@ github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiE
|
|||||||
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=
|
||||||
@@ -22,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=
|
||||||
@@ -47,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=
|
||||||
@@ -54,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=
|
||||||
@@ -66,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=
|
||||||
@@ -74,8 +85,14 @@ 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=
|
||||||
@@ -89,6 +106,8 @@ golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v
|
|||||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
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.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.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
@@ -104,6 +123,8 @@ 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.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||||
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
|
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.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-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.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.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
@@ -154,6 +175,10 @@ 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.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
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.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=
|
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
16
main.go
@@ -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
38
main_test.go
Normal 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",
|
||||||
|
})
|
||||||
|
}
|
||||||
2
mkrel.sh
2
mkrel.sh
@@ -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
21
pkg/epub/README.md
Normal 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>
|
||||||
@@ -9,6 +9,11 @@ import (
|
|||||||
"path"
|
"path"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// a section in the book
|
||||||
|
type Section struct {
|
||||||
|
File, Title, MediaType string
|
||||||
|
}
|
||||||
|
|
||||||
// Book epub book
|
// Book epub book
|
||||||
type Book struct {
|
type Book struct {
|
||||||
Ncx Ncx `json:"ncx"`
|
Ncx Ncx `json:"ncx"`
|
||||||
@@ -17,6 +22,11 @@ type Book struct {
|
|||||||
Mimetype string `json:"-"`
|
Mimetype string `json:"-"`
|
||||||
Content []Content
|
Content []Content
|
||||||
fd *zip.ReadCloser
|
fd *zip.ReadCloser
|
||||||
|
CoverImage []byte
|
||||||
|
CoverFile string
|
||||||
|
CoverMediaType string
|
||||||
|
Sections []Section
|
||||||
|
dumpxml bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// Open open resource file
|
// Open open resource file
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
package epub
|
package epub
|
||||||
|
|
||||||
//Container META-INF/container.xml file
|
// Container META-INF/container.xml file
|
||||||
type Container struct {
|
type Container struct {
|
||||||
Rootfile Rootfile `xml:"rootfiles>rootfile" json:"rootfile"`
|
Rootfile Rootfile `xml:"rootfiles>rootfile" json:"rootfile"`
|
||||||
}
|
}
|
||||||
|
|
||||||
//Rootfile root file
|
// Rootfile root file
|
||||||
type Rootfile struct {
|
type Rootfile struct {
|
||||||
Path string `xml:"full-path,attr" json:"path"`
|
Path string `xml:"full-path,attr" json:"path"`
|
||||||
Type string `xml:"media-type,attr" json:"type"`
|
Type string `xml:"media-type,attr" json:"type"`
|
||||||
|
|||||||
@@ -8,9 +8,10 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
cleanentitles = regexp.MustCompile(`&.+;`)
|
cleanentitles = regexp.MustCompile(`&[a-z]+;`)
|
||||||
empty = regexp.MustCompile(`(?s)^[\s ]*$`)
|
empty = regexp.MustCompile(`(?s)^[\s ]*$`)
|
||||||
newlines = regexp.MustCompile(`[\r\n]+`)
|
newlines = regexp.MustCompile(`[\r\n]+`)
|
||||||
|
cleanmarkup = regexp.MustCompile(`<[^<>]+>`)
|
||||||
)
|
)
|
||||||
|
|
||||||
// Content nav-point content
|
// Content nav-point content
|
||||||
@@ -22,30 +23,45 @@ 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 {
|
||||||
// parse XML, look for title and <p>.*</p> stuff
|
|
||||||
doc, err := xmlquery.Parse(
|
doc, err := xmlquery.Parse(
|
||||||
strings.NewReader(
|
strings.NewReader(
|
||||||
cleanentitles.ReplaceAllString(string(content), " ")))
|
cleanentitles.ReplaceAllString(string(content), " ")))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if c.Title == "" {
|
||||||
// extract the title
|
// extract the title
|
||||||
for _, item := range xmlquery.Find(doc, "//title") {
|
for _, item := range xmlquery.Find(doc, "//title") {
|
||||||
c.Title = strings.TrimSpace(item.InnerText())
|
c.Title = strings.TrimSpace(item.InnerText())
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// extract all paragraphs, ignore any formatting and re-fill the
|
// extract all paragraphs, ignore any formatting and re-fill the
|
||||||
// paragraph, that is, we replaces all newlines inside with one
|
// paragraph, that is, we replace all newlines inside with one
|
||||||
// space.
|
// space.
|
||||||
txt := strings.Builder{}
|
txt := strings.Builder{}
|
||||||
|
var have_p bool
|
||||||
for _, item := range xmlquery.Find(doc, "//p") {
|
for _, item := range xmlquery.Find(doc, "//p") {
|
||||||
if !empty.MatchString(item.InnerText()) {
|
if !empty.MatchString(item.InnerText()) {
|
||||||
|
have_p = true
|
||||||
txt.WriteString(newlines.ReplaceAllString(item.InnerText(), " ") + "\n\n")
|
txt.WriteString(newlines.ReplaceAllString(item.InnerText(), " ") + "\n\n")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !have_p {
|
||||||
|
// try <div></div>, which some ebooks use, so get all divs,
|
||||||
|
// remove markup and paragraphify the parts
|
||||||
|
for _, item := range xmlquery.Find(doc, "//div") {
|
||||||
|
if !empty.MatchString(item.InnerText()) {
|
||||||
|
cleaned := cleanmarkup.ReplaceAllString(item.InnerText(), "")
|
||||||
|
txt.WriteString(newlines.ReplaceAllString(cleaned, " ") + "\n\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
c.Body = strings.TrimSpace(txt.String())
|
c.Body = strings.TrimSpace(txt.String())
|
||||||
c.XML = content
|
c.XML = content
|
||||||
|
|
||||||
|
|||||||
185
pkg/epub/open.go
185
pkg/epub/open.go
@@ -4,75 +4,202 @@ import (
|
|||||||
"archive/zip"
|
"archive/zip"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Open open a epub file
|
var (
|
||||||
|
// to find content
|
||||||
|
types = regexp.MustCompile(`application/(xml|html|xhtml|htm)`)
|
||||||
|
|
||||||
|
// cleanup regexes
|
||||||
|
deanchor = regexp.MustCompile(`#.*$`)
|
||||||
|
cleanext = regexp.MustCompile(`^\.`)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Open open a epub file and return the filled Book structure
|
||||||
func Open(fn string, dumpxml bool) (*Book, error) {
|
func Open(fn string, dumpxml bool) (*Book, error) {
|
||||||
|
bk, err := openFile(fn, dumpxml)
|
||||||
|
if err != nil {
|
||||||
|
return bk, err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if err := bk.fd.Close(); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
if err := bk.getManifest(); err != nil {
|
||||||
|
return bk, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := bk.getSections(); err != nil {
|
||||||
|
return bk, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := bk.readSectionContent(); err != nil {
|
||||||
|
return bk, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return bk, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// load the epub zip file
|
||||||
|
func openFile(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
|
||||||
}
|
}
|
||||||
|
|
||||||
defer func() {
|
bk := &Book{fd: fd, dumpxml: dumpxml}
|
||||||
if err := fd.Close(); err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
bk := Book{fd: fd}
|
return bk, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// load the manifest
|
||||||
|
func (bk *Book) getManifest() error {
|
||||||
mt, err := bk.readBytes("mimetype")
|
mt, err := bk.readBytes("mimetype")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &bk, err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
bk.Mimetype = string(mt)
|
bk.Mimetype = string(mt)
|
||||||
|
|
||||||
|
// contains the root path
|
||||||
err = bk.readXML("META-INF/container.xml", &bk.Container)
|
err = bk.readXML("META-INF/container.xml", &bk.Container)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &bk, err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// contains the OPF data
|
||||||
err = bk.readXML(bk.Container.Rootfile.Path, &bk.Opf)
|
err = bk.readXML(bk.Container.Rootfile.Path, &bk.Opf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &bk, err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// look for TOC (might be incomplete, see below!)
|
||||||
for _, mf := range bk.Opf.Manifest {
|
for _, mf := range bk.Opf.Manifest {
|
||||||
if mf.ID == bk.Opf.Spine.Toc {
|
if mf.ID == bk.Opf.Spine.Toc {
|
||||||
err = bk.readXML(bk.filename(mf.Href), &bk.Ncx)
|
err = bk.readXML(bk.filename(mf.Href), &bk.Ncx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &bk, err
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if mf.ID == "cover-image" {
|
||||||
|
bk.CoverFile = mf.Href
|
||||||
|
bk.CoverMediaType = mf.MediaType
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// extract the readable sections of the epub
|
||||||
|
func (bk *Book) getSections() error {
|
||||||
|
// to store our final content sections
|
||||||
|
sections := []Section{}
|
||||||
|
|
||||||
|
// count the content items in the raw manifest
|
||||||
|
var manifestcount int
|
||||||
|
for _, item := range bk.Opf.Manifest {
|
||||||
|
if types.MatchString(item.MediaType) {
|
||||||
|
manifestcount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// we have ncx points from the TOC, try those
|
||||||
|
if len(bk.Ncx.Points) > 0 {
|
||||||
|
for _, block := range bk.Ncx.Points {
|
||||||
|
sect := Section{
|
||||||
|
File: "OEBPS/" + block.Content.Src,
|
||||||
|
Title: block.Text,
|
||||||
|
}
|
||||||
|
|
||||||
|
srcfile := deanchor.ReplaceAllString(block.Content.Src, "")
|
||||||
|
|
||||||
|
for _, file := range bk.Files() {
|
||||||
|
if strings.Contains(file, srcfile) {
|
||||||
|
sect.File = file
|
||||||
|
sect.MediaType = "application/" + cleanext.ReplaceAllString(filepath.Ext(file), "")
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, file := range bk.Files() {
|
sections = append(sections, sect)
|
||||||
content, err := bk.readBytes(file)
|
|
||||||
if err != nil {
|
|
||||||
return &bk, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ct := Content{Src: file}
|
if len(sections) < manifestcount {
|
||||||
if strings.Contains(string(content), "<?xml") {
|
// TOC was incomplete, restart from scratch but use the
|
||||||
if err := ct.String(content); err != nil {
|
// OPF Manifest directly
|
||||||
return &bk, err
|
|
||||||
|
sections = []Section{}
|
||||||
|
|
||||||
|
for _, item := range bk.Opf.Manifest {
|
||||||
|
if types.MatchString(item.MediaType) {
|
||||||
|
sect := Section{
|
||||||
|
File: "OEBPS/" + item.Href,
|
||||||
|
MediaType: item.MediaType,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
srcfile := deanchor.ReplaceAllString(item.Href, "")
|
||||||
|
|
||||||
|
for _, file := range bk.Files() {
|
||||||
|
if strings.Contains(file, srcfile) {
|
||||||
|
sect.File = file
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sections = append(sections, sect)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// no TOC, just pull in the files directly
|
||||||
|
for _, file := range bk.Files() {
|
||||||
|
sections = append(sections,
|
||||||
|
Section{
|
||||||
|
File: file,
|
||||||
|
MediaType: "application/" + cleanext.ReplaceAllString(filepath.Ext(file), ""),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// final sections to keep
|
||||||
|
bk.Sections = sections
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bk *Book) readSectionContent() error {
|
||||||
|
// now read in the actual xml contents
|
||||||
|
for _, section := range bk.Sections {
|
||||||
|
content, err := bk.readBytes(section.File)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.Contains(section.File, bk.CoverFile) {
|
||||||
|
bk.CoverImage = content
|
||||||
|
}
|
||||||
|
|
||||||
|
ct := Content{Src: section.File, Title: section.Title}
|
||||||
|
|
||||||
|
if types.MatchString(section.MediaType) {
|
||||||
|
if err := ct.String(content); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if bk.dumpxml {
|
||||||
|
fmt.Println(string(ct.XML))
|
||||||
}
|
}
|
||||||
|
|
||||||
bk.Content = append(bk.Content, ct)
|
bk.Content = append(bk.Content, ct)
|
||||||
|
|
||||||
if dumpxml {
|
|
||||||
fmt.Println(string(ct.XML))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if dumpxml {
|
return nil
|
||||||
os.Exit(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &bk, nil
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
package epub
|
package epub
|
||||||
|
|
||||||
//Opf content.opf
|
// Opf content.opf
|
||||||
type Opf struct {
|
type Opf struct {
|
||||||
Metadata Metadata `xml:"metadata" json:"metadata"`
|
Metadata Metadata `xml:"metadata" json:"metadata"`
|
||||||
Manifest []Manifest `xml:"manifest>item" json:"manifest"`
|
Manifest []Manifest `xml:"manifest>item" json:"manifest"`
|
||||||
Spine Spine `xml:"spine" json:"spine"`
|
Spine Spine `xml:"spine" json:"spine"`
|
||||||
}
|
}
|
||||||
|
|
||||||
//Metadata metadata
|
// Metadata metadata
|
||||||
type Metadata struct {
|
type Metadata struct {
|
||||||
Title []string `xml:"title" json:"title"`
|
Title []string `xml:"title" json:"title"`
|
||||||
Language []string `xml:"language" json:"language"`
|
Language []string `xml:"language" json:"language"`
|
||||||
@@ -53,7 +53,7 @@ type Metafield struct {
|
|||||||
Content string `xml:"content,attr" json:"content"`
|
Content string `xml:"content,attr" json:"content"`
|
||||||
}
|
}
|
||||||
|
|
||||||
//Manifest manifest
|
// Manifest manifest
|
||||||
type Manifest struct {
|
type Manifest struct {
|
||||||
ID string `xml:"id,attr" json:"id"`
|
ID string `xml:"id,attr" json:"id"`
|
||||||
Href string `xml:"href,attr" json:"href"`
|
Href string `xml:"href,attr" json:"href"`
|
||||||
|
|||||||
BIN
pkg/epub/test.epub
Normal file
BIN
pkg/epub/test.epub
Normal file
Binary file not shown.
7
t/basic.txtar
Normal file
7
t/basic.txtar
Normal 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
1698
t/read-parse.txtar
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user