Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4ca12b907b | |||
| 807a2712e5 | |||
| 0d80f0ef42 | |||
| 120b88803c | |||
| fc9ff4a23f | |||
|
|
c1f757197d | ||
| 7a12e8e8b0 | |||
|
|
238972f11f | ||
|
|
f524083210 | ||
|
|
cb671b7401 | ||
|
|
2c6e81a2c8 | ||
| c2abc4ba4d | |||
| b9b0ad8603 | |||
|
|
08f470e0d5 | ||
|
|
b50c6acff0 | ||
| 90d30cb3e1 | |||
| f942378b47 | |||
| 3d3bbe3266 | |||
| 84307c42eb |
BIN
.codeberg/assets/darkmode.png
Normal file
|
After Width: | Height: | Size: 80 KiB |
BIN
.codeberg/assets/help.png
Normal file
|
After Width: | Height: | Size: 86 KiB |
BIN
.codeberg/assets/light.png
Normal file
|
After Width: | Height: | Size: 81 KiB |
BIN
.codeberg/assets/margin.png
Normal file
|
After Width: | Height: | Size: 63 KiB |
BIN
.github/assets/darkmode.png
vendored
|
Before Width: | Height: | Size: 57 KiB |
BIN
.github/assets/help.png
vendored
|
Before Width: | Height: | Size: 79 KiB |
BIN
.github/assets/light.png
vendored
|
Before Width: | Height: | Size: 74 KiB |
BIN
.github/assets/margin.png
vendored
|
Before Width: | Height: | Size: 55 KiB |
34
.github/workflows/ci.yaml
vendored
@@ -1,34 +0,0 @@
|
||||
name: build-and-test
|
||||
on: [push]
|
||||
jobs:
|
||||
build:
|
||||
strategy:
|
||||
matrix:
|
||||
version: [1.24.9]
|
||||
os: [ubuntu-latest, windows-latest, macos-latest]
|
||||
name: Build
|
||||
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: build
|
||||
run: go build
|
||||
|
||||
|
||||
golangci:
|
||||
name: lint
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: 1.24
|
||||
- uses: actions/checkout@v5
|
||||
- name: golangci-lint
|
||||
uses: golangci/golangci-lint-action@v8
|
||||
69
.goreleaser.yaml
Normal file
@@ -0,0 +1,69 @@
|
||||
# vim: set ts=2 sw=2 tw=0 fo=cnqoj
|
||||
|
||||
version: 2
|
||||
|
||||
before:
|
||||
hooks:
|
||||
- go mod tidy
|
||||
|
||||
gitea_urls:
|
||||
api: https://codeberg.org/api/v1
|
||||
download: https://codeberg.org
|
||||
|
||||
builds:
|
||||
- env:
|
||||
- CGO_ENABLED=0
|
||||
goos:
|
||||
- linux
|
||||
- windows
|
||||
- darwin
|
||||
- freebsd
|
||||
|
||||
archives:
|
||||
- formats: [tar.gz]
|
||||
# this name template makes the OS and Arch compatible with the results of `uname`.
|
||||
name_template: >-
|
||||
{{ .ProjectName }}_
|
||||
{{- title .Os }}_
|
||||
{{- if eq .Arch "amd64" }}x86_64
|
||||
{{- else if eq .Arch "386" }}i386
|
||||
{{- else }}{{ .Arch }}{{ end }}
|
||||
{{- if .Arm }}v{{ .Arm }}{{ end }}_{{ .Tag }}
|
||||
# use zip for windows archives
|
||||
format_overrides:
|
||||
- goos: windows
|
||||
formats: [zip]
|
||||
- goos: linux
|
||||
formats: [tar.gz,binary]
|
||||
files:
|
||||
- src: "*.md"
|
||||
strip_parent: true
|
||||
- src: "docs/*"
|
||||
strip_parent: true
|
||||
- src: Makefile.dist
|
||||
dst: Makefile
|
||||
wrap_in_directory: true
|
||||
|
||||
changelog:
|
||||
sort: asc
|
||||
filters:
|
||||
exclude:
|
||||
- "^docs:"
|
||||
- "^test:"
|
||||
groups:
|
||||
- title: Improved
|
||||
regexp: '^.*?(feat|add|new)(\([[:word:]]+\))??!?:.+$'
|
||||
order: 0
|
||||
- title: Fixed
|
||||
regexp: '^.*?(bug|fix)(\([[:word:]]+\))??!?:.+$'
|
||||
order: 1
|
||||
- title: Changed
|
||||
order: 999
|
||||
|
||||
release:
|
||||
header: "# Release Notes"
|
||||
footer: >-
|
||||
|
||||
---
|
||||
|
||||
Full Changelog: [{{ .PreviousTag }}...{{ .Tag }}](https://codeberg.org/scip/epuppy/compare/{{ .PreviousTag }}...{{ .Tag }})
|
||||
27
.woodpecker/build.yaml
Normal file
@@ -0,0 +1,27 @@
|
||||
matrix:
|
||||
platform:
|
||||
- linux/amd64
|
||||
goversion:
|
||||
- 1.24
|
||||
|
||||
labels:
|
||||
platform: ${platform}
|
||||
|
||||
steps:
|
||||
build:
|
||||
when:
|
||||
event: [push]
|
||||
image: golang:${goversion}
|
||||
commands:
|
||||
- go get
|
||||
- go build
|
||||
- go test
|
||||
|
||||
linter:
|
||||
when:
|
||||
event: [push]
|
||||
image: golang:${goversion}
|
||||
commands:
|
||||
- curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/HEAD/install.sh | sh -s -- -b $(go env GOPATH)/bin v2.5.0
|
||||
- golangci-lint --version
|
||||
- golangci-lint run ./...
|
||||
15
.woodpecker/release.yaml
Normal file
@@ -0,0 +1,15 @@
|
||||
# build release
|
||||
|
||||
labels:
|
||||
platform: linux/amd64
|
||||
|
||||
steps:
|
||||
goreleaser:
|
||||
image: goreleaser/goreleaser
|
||||
when:
|
||||
event: [tag]
|
||||
environment:
|
||||
GITEA_TOKEN:
|
||||
from_secret: DEPLOY_TOKEN
|
||||
commands:
|
||||
- goreleaser release --clean --verbose
|
||||
18
CODE_OF_CONDUCT.md
Normal 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
@@ -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!
|
||||
20
Makefile.dist
Normal file
@@ -0,0 +1,20 @@
|
||||
# -*-make-*-
|
||||
|
||||
.PHONY: install all
|
||||
|
||||
tool = epuppy
|
||||
PREFIX = /usr/local
|
||||
UID = root
|
||||
GID = 0
|
||||
|
||||
all:
|
||||
@echo "Type 'sudo make install' to install the tool."
|
||||
@echo "To change prefix, type 'sudo make install PREFIX=/opt'"
|
||||
|
||||
install:
|
||||
install -d -o $(UID) -g $(GID) $(PREFIX)/bin
|
||||
install -d -o $(UID) -g $(GID) $(PREFIX)/man/man1
|
||||
install -d -o $(UID) -g $(GID) $(PREFIX)/share/doc
|
||||
install -o $(UID) -g $(GID) -m 555 $(tool) $(PREFIX)/sbin/
|
||||
install -o $(UID) -g $(GID) -m 444 $(tool).1 $(PREFIX)/man/man1/
|
||||
install -o $(UID) -g $(GID) -m 444 *.md Documentation.txt $(PREFIX)/share/doc/
|
||||
103
README.md
@@ -1,3 +1,8 @@
|
||||
[](https://ci.codeberg.org/repos/15473)
|
||||
[](https://codeberg.org/scip/epuppy/raw/branch/main/LICENSE)
|
||||
[](https://goreportcard.com/report/codeberg.org/scip/epuppy)
|
||||
|
||||
|
||||
# epuppy - terminal epub reader
|
||||
|
||||
This is a little TUI epub ebook reader. This is a work in progress and
|
||||
@@ -6,19 +11,90 @@ 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
|
||||
`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
|
||||
|
||||
- Viewing an ebook in dark mode
|
||||

|
||||
### Viewing an ebook in dark mode
|
||||
|
||||
- Viewing an ebook in light mode
|
||||

|
||||

|
||||
|
||||
- You can interactively adjust text width
|
||||

|
||||
### Viewing an ebook in light mode
|
||||
|
||||
- Showing the help
|
||||

|
||||

|
||||
|
||||
### You can interactively adjust text width
|
||||
|
||||

|
||||
|
||||
### 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
|
||||
|
||||
@@ -27,7 +103,12 @@ your platform from the releases page and you're good to go.
|
||||
|
||||
### Installation using a pre-compiled binary
|
||||
|
||||
Go to the [latest release page](https://github.com/TLINDEN/epuppy/releases/latest)
|
||||
You can use [stew](https://github.com/marwanhawari/stew) to install epuppy:
|
||||
```default
|
||||
stew install https://codeberg.org/scip/epuppy
|
||||
```
|
||||
|
||||
Or go to the [latest release page](https://codeberg.org/scip/epuppy/releases/)
|
||||
and look for your OS and platform. There are two options to install the binary:
|
||||
|
||||
Directly download the binary for your platform,
|
||||
@@ -67,7 +148,7 @@ sudo make install
|
||||
|
||||
# Report bugs
|
||||
|
||||
[Please open an issue](https://github.com/TLINDEN/epuppy/issues). Thanks!
|
||||
[Please open an issue](https://codeberg.org/scip/epuppy/issues). Thanks!
|
||||
|
||||
# License
|
||||
|
||||
@@ -76,4 +157,4 @@ version 3.
|
||||
|
||||
# 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
|
||||
|
||||
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
|
||||
|
||||
import (
|
||||
@@ -16,32 +32,40 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
Version string = `v0.0.2`
|
||||
Usage string = `Usage epuppy [options] <epub file>
|
||||
Version string = `v0.0.7`
|
||||
Usage string = `This is epuppy, a terminal ui ebook viewer.
|
||||
|
||||
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>
|
||||
-i --cover-image display cover image
|
||||
-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`
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Showversion bool `koanf:"version"` // -v
|
||||
Debug bool `koanf:"debug"` // -d
|
||||
StoreProgress bool `koanf:"store-progress"` // -s
|
||||
Darkmode bool `koanf:"dark"` // -D
|
||||
LineNumbers bool `koanf:"line-numbers"` // -n
|
||||
Dump bool `koanf:"txt"` // -t
|
||||
Config string `koanf:"config"` // -c
|
||||
ColorDark ColorSetting `koanf:"colordark"` // comes from config file only
|
||||
ColorLight ColorSetting `koanf:"colorlight"` // comes from config file only
|
||||
|
||||
Colors Colors // generated from user config file or internal defaults, respects dark mode
|
||||
Showversion bool `koanf:"version"` // -v
|
||||
Debug bool `koanf:"debug"` // -d
|
||||
StoreProgress bool `koanf:"store-progress"` // -s
|
||||
Darkmode bool `koanf:"dark"` // -D
|
||||
LineNumbers bool `koanf:"line-numbers"` // -n
|
||||
Dump bool `koanf:"txt"` // -t
|
||||
XML bool `koanf:"xml"` // -x
|
||||
NoColor bool `koanf:"no-color"` // -n
|
||||
Config string `koanf:"config"` // -c
|
||||
ColorDark ColorSetting `koanf:"colordark"` // 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
|
||||
Document string
|
||||
InitialProgress int // lines
|
||||
}
|
||||
@@ -56,7 +80,6 @@ func InitConfig(output io.Writer) (*Config, error) {
|
||||
if err != nil {
|
||||
log.Fatalf("failed to print to output: %s", err)
|
||||
}
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
// 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("line-numbers", "n", false, "add line numbers")
|
||||
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")
|
||||
|
||||
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 {
|
||||
conf.Document = flagset.Args()[0]
|
||||
} else {
|
||||
if !conf.Showversion {
|
||||
if !conf.Showversion && !conf.ShowHelp {
|
||||
flagset.Usage()
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -137,6 +165,11 @@ func InitConfig(output io.Writer) (*Config, error) {
|
||||
},
|
||||
conf)
|
||||
|
||||
// disable colors if requested by command line
|
||||
if conf.NoColor {
|
||||
_ = os.Setenv("NO_COLOR", "1")
|
||||
}
|
||||
|
||||
return conf, nil
|
||||
}
|
||||
|
||||
|
||||
120
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
|
||||
|
||||
// pager setup using bubbletea
|
||||
@@ -5,10 +21,13 @@ package cmd
|
||||
// https://github.com/charmbracelet/bubbletea/tree/main/examples/pager
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"image"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/blacktop/go-termimg"
|
||||
"github.com/charmbracelet/bubbles/help"
|
||||
"github.com/charmbracelet/bubbles/key"
|
||||
"github.com/charmbracelet/bubbles/viewport"
|
||||
@@ -46,18 +65,22 @@ type Meta struct {
|
||||
}
|
||||
|
||||
type keyMap struct {
|
||||
Up key.Binding
|
||||
Down key.Binding
|
||||
Left key.Binding
|
||||
Right key.Binding
|
||||
Help key.Binding
|
||||
Quit key.Binding
|
||||
Up key.Binding
|
||||
Down key.Binding
|
||||
Left key.Binding
|
||||
Right key.Binding
|
||||
Help key.Binding
|
||||
Quit key.Binding
|
||||
ToggleUI key.Binding
|
||||
Pad key.Binding
|
||||
}
|
||||
|
||||
func (k keyMap) FullHelp() [][]key.Binding {
|
||||
return [][]key.Binding{
|
||||
{k.Up, k.Down, k.Left, k.Right}, // first column
|
||||
{k.Help, k.Quit}, // second column
|
||||
// every item is one 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{
|
||||
Pad: key.NewBinding(
|
||||
key.WithKeys("__"),
|
||||
key.WithHelp(" ", ""),
|
||||
),
|
||||
Up: key.NewBinding(
|
||||
key.WithKeys("up", "k"),
|
||||
key.WithHelp("↑", "move up"),
|
||||
@@ -75,11 +102,11 @@ var keys = keyMap{
|
||||
key.WithHelp("↓", "move down"),
|
||||
),
|
||||
Left: key.NewBinding(
|
||||
key.WithKeys("left", "h"),
|
||||
key.WithKeys("left"),
|
||||
key.WithHelp("←", "decrease text width"),
|
||||
),
|
||||
Right: key.NewBinding(
|
||||
key.WithKeys("right", "l"),
|
||||
key.WithKeys("right"),
|
||||
key.WithHelp("→", "increase text width"),
|
||||
),
|
||||
Help: key.NewBinding(
|
||||
@@ -90,6 +117,10 @@ var keys = keyMap{
|
||||
key.WithKeys("q", "esc", "ctrl+c"),
|
||||
key.WithHelp("q", "quit"),
|
||||
),
|
||||
ToggleUI: key.NewBinding(
|
||||
key.WithKeys("h"),
|
||||
key.WithHelp("h", "toggle ui"),
|
||||
),
|
||||
}
|
||||
|
||||
type Doc struct {
|
||||
@@ -101,8 +132,10 @@ type Doc struct {
|
||||
lastwidth int
|
||||
margin int
|
||||
marginMod bool
|
||||
hideUi bool
|
||||
meta *Meta
|
||||
config *Config
|
||||
Cover *termimg.ImageWidget
|
||||
|
||||
keys keyMap
|
||||
help help.Model
|
||||
@@ -135,6 +168,8 @@ func (m Doc) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
m.margin -= MarginStep
|
||||
m.marginMod = true
|
||||
}
|
||||
case key.Matches(msg, m.keys.ToggleUI):
|
||||
m.hideUi = !m.hideUi
|
||||
}
|
||||
|
||||
case tea.WindowSizeMsg:
|
||||
@@ -189,7 +224,12 @@ func (m *Doc) Rewrap() {
|
||||
|
||||
// wrapping has changed, update viewport and line count
|
||||
if content != "" {
|
||||
m.viewport.SetContent(content)
|
||||
if m.Cover != nil {
|
||||
cover, _ := m.Cover.Render()
|
||||
m.viewport.SetContent(cover + "\n\n" + content)
|
||||
} else {
|
||||
m.viewport.SetContent(content)
|
||||
}
|
||||
m.meta.lines = len(strings.Split(content, "\n"))
|
||||
}
|
||||
|
||||
@@ -212,6 +252,10 @@ func (m Doc) View() string {
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -235,7 +279,15 @@ func max(a, b int) int {
|
||||
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
|
||||
scrollto := 0
|
||||
|
||||
@@ -246,32 +298,44 @@ func Pager(conf *Config, title, message string) (int, error) {
|
||||
}
|
||||
}
|
||||
|
||||
if conf.StoreProgress {
|
||||
scrollto = conf.InitialProgress
|
||||
if book.Config.StoreProgress {
|
||||
scrollto = book.Config.InitialProgress
|
||||
}
|
||||
|
||||
if conf.LineNumbers {
|
||||
if book.Config.LineNumbers {
|
||||
catn := ""
|
||||
for idx, line := range strings.Split(message, "\n") {
|
||||
catn += fmt.Sprintf("%d: %s\n", idx, line)
|
||||
for idx, line := range strings.Split(book.Body, "\n") {
|
||||
catn += fmt.Sprintf("%4d: %s\n", idx, line)
|
||||
}
|
||||
message = catn
|
||||
book.Body = catn
|
||||
}
|
||||
|
||||
meta := Meta{
|
||||
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(
|
||||
Doc{
|
||||
content: message,
|
||||
title: title,
|
||||
initialwidth: width,
|
||||
meta: &meta,
|
||||
config: conf,
|
||||
keys: keys,
|
||||
},
|
||||
doc,
|
||||
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
|
||||
)
|
||||
@@ -280,7 +344,7 @@ func Pager(conf *Config, title, message string) (int, error) {
|
||||
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",
|
||||
scrollto, meta.currentline, scrollto-meta.currentline)
|
||||
}
|
||||
|
||||
119
cmd/prepare.go
Normal file
@@ -0,0 +1,119 @@
|
||||
/*
|
||||
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
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"codeberg.org/scip/epuppy/pkg/epub"
|
||||
"github.com/alecthomas/repr"
|
||||
)
|
||||
|
||||
func Prepare(conf *Config) (*Ebook, error) {
|
||||
switch filepath.Ext(conf.Document) {
|
||||
case ".epub":
|
||||
return PrepareEpub(conf)
|
||||
default:
|
||||
return PrepareText(conf)
|
||||
}
|
||||
}
|
||||
|
||||
func PrepareText(conf *Config) (*Ebook, error) {
|
||||
data, err := os.ReadFile(conf.Document)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Ebook{
|
||||
Config: conf,
|
||||
Title: conf.Document,
|
||||
Body: string(data),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func PrepareEpub(conf *Config) (*Ebook, error) {
|
||||
book, err := epub.Open(conf.Document, conf.XML)
|
||||
if err != nil {
|
||||
return nil, 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{}
|
||||
head := strings.Builder{}
|
||||
|
||||
for _, creator := range book.Opf.Metadata.Creator {
|
||||
head.WriteString(creator.Data)
|
||||
head.WriteString(" ")
|
||||
}
|
||||
|
||||
head.WriteString("- ")
|
||||
|
||||
for _, title := range book.Opf.Metadata.Title {
|
||||
head.WriteString(title)
|
||||
head.WriteString(" ")
|
||||
}
|
||||
|
||||
fetchByContent(conf, &buf, book)
|
||||
|
||||
return &Ebook{
|
||||
Config: conf,
|
||||
Title: head.String(),
|
||||
Body: buf.String(),
|
||||
Cover: book.CoverImage,
|
||||
MediaType: book.CoverMediaType,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func fetchByContent(conf *Config, buf *strings.Builder, book *epub.Book) bool {
|
||||
chapter := 1
|
||||
var gotbody bool
|
||||
|
||||
for _, content := range book.Content {
|
||||
if len(content.Body) > 0 {
|
||||
if content.Title != "" {
|
||||
buf.WriteString(conf.Colors.Chapter.
|
||||
Render(fmt.Sprintf("──────┤ %s ├──────", content.Title)))
|
||||
|
||||
}
|
||||
buf.WriteString("\r\n\r\n")
|
||||
|
||||
if conf.Dump {
|
||||
// avoid excess whitespaces when printing to stdout
|
||||
buf.WriteString(content.Body)
|
||||
} else {
|
||||
buf.WriteString(conf.Colors.Body.Render(content.Body))
|
||||
}
|
||||
|
||||
buf.WriteString("\r\n\r\n\r\n\r\n")
|
||||
chapter++
|
||||
|
||||
gotbody = true
|
||||
}
|
||||
}
|
||||
|
||||
return gotbody
|
||||
}
|
||||
41
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
|
||||
|
||||
import (
|
||||
@@ -27,6 +43,15 @@ func Execute(output io.Writer) int {
|
||||
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 {
|
||||
progress, err := GetProgress(conf)
|
||||
if err == nil {
|
||||
@@ -34,7 +59,21 @@ func Execute(output io.Writer) int {
|
||||
}
|
||||
}
|
||||
|
||||
progress, err := View(conf)
|
||||
ebook, err := Prepare(conf)
|
||||
if err != nil {
|
||||
return Die(err)
|
||||
}
|
||||
|
||||
if conf.Dump {
|
||||
fmt.Println(ebook.Body)
|
||||
return 0
|
||||
}
|
||||
|
||||
if conf.Debug || conf.XML {
|
||||
return 0
|
||||
}
|
||||
|
||||
progress, err := Pager(ebook)
|
||||
if err != nil {
|
||||
return Die(err)
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
import (
|
||||
|
||||
93
cmd/view.go
@@ -1,93 +0,0 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/tlinden/epuppy/pkg/epub"
|
||||
)
|
||||
|
||||
func View(conf *Config) (int, error) {
|
||||
switch filepath.Ext(conf.Document) {
|
||||
case ".epub":
|
||||
return ViewEpub(conf)
|
||||
default:
|
||||
return ViewText(conf)
|
||||
}
|
||||
}
|
||||
|
||||
func ViewText(conf *Config) (int, error) {
|
||||
data, err := os.ReadFile(conf.Document)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if conf.Dump {
|
||||
return fmt.Println(string(data))
|
||||
}
|
||||
|
||||
return Pager(conf, conf.Document, string(data))
|
||||
}
|
||||
|
||||
func ViewEpub(conf *Config) (int, error) {
|
||||
book, err := epub.Open(conf.Document)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if err := book.Close(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}()
|
||||
|
||||
buf := strings.Builder{}
|
||||
head := strings.Builder{}
|
||||
|
||||
for _, creator := range book.Opf.Metadata.Creator {
|
||||
head.WriteString(creator.Data)
|
||||
head.WriteString(" ")
|
||||
}
|
||||
|
||||
head.WriteString("- ")
|
||||
|
||||
for _, title := range book.Opf.Metadata.Title {
|
||||
head.WriteString(title)
|
||||
head.WriteString(" ")
|
||||
}
|
||||
|
||||
// FIXME: since the switch to book.Files() in epub.Open() this
|
||||
// returns invalid chapter numbering
|
||||
chapter := 1
|
||||
|
||||
for _, content := range book.Content {
|
||||
if len(content.Body) > 0 {
|
||||
if content.Title != "" {
|
||||
buf.WriteString(conf.Colors.Chapter.
|
||||
Render(fmt.Sprintf("──────┤ %s ├──────", content.Title)))
|
||||
|
||||
}
|
||||
buf.WriteString("\r\n\r\n")
|
||||
|
||||
if conf.Dump {
|
||||
// avoid excess whitespaces when printing to stdout
|
||||
buf.WriteString(content.Body)
|
||||
} else {
|
||||
buf.WriteString(conf.Colors.Body.Render(content.Body))
|
||||
}
|
||||
|
||||
buf.WriteString("\r\n\r\n\r\n\r\n")
|
||||
chapter++
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if conf.Dump {
|
||||
return fmt.Println(buf.String())
|
||||
}
|
||||
|
||||
return Pager(conf, head.String(), buf.String())
|
||||
}
|
||||
118
cmd/view_test.go
Normal file
@@ -0,0 +1,118 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestPrepare(t *testing.T) {
|
||||
var tests = []struct {
|
||||
file string
|
||||
body string
|
||||
}{
|
||||
{
|
||||
"t/epub/basic-v3plus2.epub",
|
||||
`shirt court, an whinny retched a cordage offer groin-murder, picked inner windrow,`,
|
||||
},
|
||||
{
|
||||
"t/epub/childrens-literature.epub",
|
||||
`The child's natural literature. The world has lost certain secrets as the price of an advancing civilization. It is a commonplace of observation that no one can duplicate the success of Mother Goose, whether she be thought of as the maker of jingles or the teller of tales. The conditions of modern life preclude the generally naïve attitude that produced the folk rhymes, ballads, tales, proverbs, fables, and myths. The folk saw things simply and directly. The complex, analytic, questioning mind is not yet, either in or out of stories. The motives from which people act are to them plain and not mixed. Characters are good or bad. They feel no need of elaborately explaining their joys and sorrows. Such experiences come with the day's work. "To-morrow to fresh woods, and pastures new." The zest of life with them is emphatic. Their humor is fresh, unbounded, sincere; there is no trace of cynicism. In folk literature we do not feel the presence of a "writer" who is mightily concerned about maintaining his reputation for wisdom, originality, or style. Hence the freedom from any note of straining after effect, of artificiality. In the midst of a life limited to fundamental needs, their literature deals with fundamentals. On the whole, it was a literature for entertainment. A more learned upper class may have concerned itself then about "problems" and "purposes," as the whole world does now, but the literature of the folk had no such interests.`,
|
||||
},
|
||||
{
|
||||
"t/epub/cole-voyage-of-life.epub",
|
||||
`Thomas Cole is regarded as the founder of the Hudson River School, an American art movement that flourished in the mid-19th century and was concerned with the realistic and detailed portrayal of nature but with a strong influence from Romanticism. This group of American landscape painters worked between about 1825 and 1870 and shared a sense of national pride as well as an interest in celebrating the unique natural beauty found in the United States. The wild, untamed nature found in America was viewed as its special character; Europe had ancient ruins, but America had the uncharted wilderness. As Cole's friend William Cullen Bryant sermonized in verse, so Cole sermonized in paint. Both men saw nature as God's work and as a refuge from the ugly materialism of cities. Cole clearly intended the Voyage of Life to be a didactic, moralizing series of paintings using the landscape as an allegory for religious faith.`,
|
||||
},
|
||||
{
|
||||
"t/epub/epub30-spec.epub",
|
||||
`IDPF Members
|
||||
|
||||
Invited Experts/Observers
|
||||
|
||||
Version 2.0.1 of this specification was prepared by the International Digital Publishing Forum’s EPUB Maintenance Working Group under the leadership of:
|
||||
|
||||
Active members of the working group at the time of publication of revision 2.0.1 were:
|
||||
|
||||
Version 1.0 of this specification was prepared by the International Digital Publishing Forum’s Unified OEBPS Container Format Working Group under the leadership of:
|
||||
|
||||
Active members of the working group at the time of publication of revision 1.0 were:`,
|
||||
},
|
||||
{
|
||||
"t/epub/epub_sample_file_50KB.epub",
|
||||
`magna aliqua. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna`,
|
||||
},
|
||||
{
|
||||
"t/epub/Fundamental-Accessibility-Tests-Basic-Functionality-v2.0.0.epub",
|
||||
`Open the Fundamental Accessibility Test book in the reading system.
|
||||
|
||||
If the test book is not available in the bookshelf, then open any other book that is available.
|
||||
|
||||
If the reading system also supports side loading, then please provide notes about the accessibility of the side loading feature.
|
||||
|
||||
Indicate Pass or Fail.`,
|
||||
},
|
||||
{
|
||||
"t/epub/georgia-cfi.epub",
|
||||
`The Great Valley Region consists of folded sedimentary rocks, extensive erosion having removed the soft layers to form valleys, leaving the hard layers as ridges, both layers running in a N.E.-S.W. direction. In the extreme north-west corner of the state is a small part of the Cumberland Plateau, represented by Lookout and Sand Mts.
|
||||
|
||||
On the Blue Ridge escarpment near the N.E. corner of the state is a water-parting separating the waters which find their way respectively N.W. to the Tennessee river, S.W. to the Gulf of Mexico and S.E. to the Atlantic Ocean; indeed, according to B.M. and M.R. Hall (Water Resources of Georgia, p. 2), "there are three springs in north-east Georgia within a stone's throw of each other that send out their waters to Savannah, Ga., to Apalachicola, Fla., and to New Orleans, La." The water-parting between the waters flowing into the`,
|
||||
},
|
||||
{
|
||||
"t/epub/israelsailing.epub",
|
||||
` במשלוח דואר, מה שלפעמים היה נחמד כי גם חשבונות לתשלום לא היו מגיעים. 'טוב, מי`,
|
||||
},
|
||||
{
|
||||
"t/epub/jlreq-in-japanese.epub",
|
||||
` 2.5.1 基本版面からはみ出す例 2.5.2 基本版面で設定した行位置の適用 2.5.3 `,
|
||||
},
|
||||
{
|
||||
"t/epub/minimal-v2.epub",
|
||||
`This is a paragraph.`,
|
||||
},
|
||||
{
|
||||
"t/epub/minimal-v3.epub",
|
||||
`This is a paragraph.`,
|
||||
},
|
||||
{
|
||||
"t/epub/minimal-v3plus2.epub",
|
||||
`This is a paragraph.`,
|
||||
},
|
||||
{
|
||||
"t/epub/moby-dick.epub",
|
||||
`Call me Ishmael. Some years ago—never mind how long precisely—having little or no money in my purse, and nothing particular to interest me on shore, I thought I would sail about a little and see the watery part of the world. It is a way I have of driving off the spleen and regulating the circulation. Whenever I find myself growing grim about the mouth; whenever it is a damp, drizzly November in my soul; whenever I find myself involuntarily pausing before coffin warehouses, and bringing up the rear of every funeral I meet; and especially whenever my hypos get such an upper hand of me, that it requires a strong moral principle to prevent me from deliberately stepping into the street, and methodically knocking people’s hats off—then, I account it high time to get to sea as soon as I can. This is my substitute for pistol and ball. With a philosophical flourish Cato throws himself upon his sword; I quietly take to the ship. There is nothing surprising in this. If they but knew it, almost all men in their degree, some time or other, cherish very nearly the same feelings towards the ocean with me.`,
|
||||
},
|
||||
{
|
||||
"t/epub/sous-le-vent.epub",
|
||||
`SOUS LE VENT`,
|
||||
},
|
||||
{
|
||||
"t/epub/wasteland-otf.epub",
|
||||
`Line 20. Cf. Ezekiel 2:1.
|
||||
|
||||
23. Cf. Ecclesiastes 12:5.
|
||||
|
||||
31. V. Tristan und Isolde, i, verses 5-8.
|
||||
|
||||
42. Id. iii, verse 24.`,
|
||||
},
|
||||
{
|
||||
"pkg/epub/test.epub",
|
||||
`This EPUB file contains 10 hard coded page breaks. Note that these page breaks are different from the reflowed page numbers. If the total number of pages of this book in the reading app is not exactly 10, then you are looking at the reflowed pages. The app may be having a feature to switch between print page and reflowed pages for navigation. Or the print page list may be appearing in the TOC. If print page navigation feature does not exist or does not work then this test should be marked 'Fail'.`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
testname := fmt.Sprintf("prepare/%s", tt.file)
|
||||
t.Run(testname, func(t *testing.T) {
|
||||
conf := Config{
|
||||
Document: "../" + tt.file,
|
||||
}
|
||||
|
||||
ebook, err := Prepare(&conf)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, ebook.Body, tt.body, "expected text not found")
|
||||
})
|
||||
}
|
||||
}
|
||||
21
go.mod
@@ -1,4 +1,4 @@
|
||||
module github.com/tlinden/epuppy
|
||||
module codeberg.org/scip/epuppy
|
||||
|
||||
go 1.24.0
|
||||
|
||||
@@ -6,6 +6,7 @@ toolchain go1.24.9
|
||||
|
||||
require (
|
||||
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/providers/file v1.2.0
|
||||
github.com/knadh/koanf/providers/posflag v1.0.1
|
||||
@@ -35,14 +36,30 @@ require (
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // 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 (
|
||||
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/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/fsnotify/fsnotify v1.9.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/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/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/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
||||
github.com/soniakeys/quant v1.0.0 // indirect
|
||||
github.com/stretchr/testify v1.11.1 // 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
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
103
go.sum
@@ -1,9 +1,15 @@
|
||||
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/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/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
||||
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/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/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg=
|
||||
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/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/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/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/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
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/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/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/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI=
|
||||
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/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/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/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
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.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||
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/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
|
||||
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/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
|
||||
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/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
@@ -67,21 +85,102 @@ 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.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
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/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/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
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/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/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-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.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/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/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss=
|
||||
golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY=
|
||||
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
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/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
18
main.go
@@ -1,9 +1,25 @@
|
||||
/*
|
||||
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 (
|
||||
"os"
|
||||
|
||||
"github.com/tlinden/epuppy/cmd"
|
||||
"codeberg.org/scip/epuppy/cmd"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
||||
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",
|
||||
})
|
||||
}
|
||||
75
mkrel.sh
@@ -1,75 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Copyright © 2024 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/>.
|
||||
|
||||
|
||||
# get list with: go tool dist list
|
||||
DIST="darwin/amd64
|
||||
freebsd/amd64
|
||||
linux/amd64
|
||||
netbsd/amd64
|
||||
openbsd/amd64
|
||||
windows/amd64
|
||||
freebsd/arm64
|
||||
linux/arm64
|
||||
netbsd/arm64
|
||||
openbsd/arm64
|
||||
windows/arm64"
|
||||
|
||||
tool="$1"
|
||||
version="$2"
|
||||
|
||||
if test -z "$version"; then
|
||||
echo "Usage: $0 <tool name> <release version>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
rm -rf releases
|
||||
mkdir -p releases
|
||||
|
||||
|
||||
for D in $DIST; do
|
||||
os=${D/\/*/}
|
||||
arch=${D/*\//}
|
||||
binfile="releases/${tool}-${os}-${arch}-${version}"
|
||||
|
||||
if test "$os" = "windows"; then
|
||||
binfile="${binfile}.exe"
|
||||
fi
|
||||
|
||||
tardir="${tool}-${os}-${arch}-${version}"
|
||||
tarfile="releases/${tool}-${os}-${arch}-${version}.tar.gz"
|
||||
set -x
|
||||
GOOS=${os} GOARCH=${arch} go build -tags osusergo,netgo -ldflags "-extldflags=-static" -o ${binfile}
|
||||
mkdir -p ${tardir}
|
||||
cp ${binfile} README.md LICENSE ${tardir}/
|
||||
echo 'tool = epuppy
|
||||
PREFIX = /usr/local
|
||||
UID = root
|
||||
GID = 0
|
||||
|
||||
install:
|
||||
install -d -o $(UID) -g $(GID) $(PREFIX)/bin
|
||||
install -d -o $(UID) -g $(GID) $(PREFIX)/man/man1
|
||||
install -o $(UID) -g $(GID) -m 555 $(tool) $(PREFIX)/sbin/
|
||||
install -o $(UID) -g $(GID) -m 444 $(tool).1 $(PREFIX)/man/man1/' > ${tardir}/Makefile
|
||||
tar cpzf ${tarfile} ${tardir}
|
||||
sha256sum ${binfile} | cut -d' ' -f1 > ${binfile}.sha256
|
||||
sha256sum ${tarfile} | cut -d' ' -f1 > ${tarfile}.sha256
|
||||
rm -rf ${tardir}
|
||||
set +x
|
||||
done
|
||||
|
||||
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,15 +9,24 @@ import (
|
||||
"path"
|
||||
)
|
||||
|
||||
// a section in the book
|
||||
type Section struct {
|
||||
File, Title, MediaType string
|
||||
}
|
||||
|
||||
// Book epub book
|
||||
type Book struct {
|
||||
Ncx Ncx `json:"ncx"`
|
||||
Opf Opf `json:"opf"`
|
||||
Container Container `json:"-"`
|
||||
Mimetype string `json:"-"`
|
||||
Content []Content
|
||||
|
||||
fd *zip.ReadCloser
|
||||
Ncx Ncx `json:"ncx"`
|
||||
Opf Opf `json:"opf"`
|
||||
Container Container `json:"-"`
|
||||
Mimetype string `json:"-"`
|
||||
Content []Content
|
||||
fd *zip.ReadCloser
|
||||
CoverImage []byte
|
||||
CoverFile string
|
||||
CoverMediaType string
|
||||
Sections []Section
|
||||
dumpxml bool
|
||||
}
|
||||
|
||||
// Open open resource file
|
||||
@@ -34,11 +43,6 @@ func (p *Book) Files() []string {
|
||||
return fns
|
||||
}
|
||||
|
||||
// Close close file reader
|
||||
func (p *Book) Close() error {
|
||||
return p.fd.Close()
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
func (p *Book) filename(n string) string {
|
||||
return path.Join(path.Dir(p.Container.Rootfile.Path), n)
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
package epub
|
||||
|
||||
//Container META-INF/container.xml file
|
||||
// Container META-INF/container.xml file
|
||||
type Container struct {
|
||||
Rootfile Rootfile `xml:"rootfiles>rootfile" json:"rootfile"`
|
||||
}
|
||||
|
||||
//Rootfile root file
|
||||
// Rootfile root file
|
||||
type Rootfile struct {
|
||||
Path string `xml:"full-path,attr" json:"path"`
|
||||
Type string `xml:"media-type,attr" json:"type"`
|
||||
|
||||
@@ -1,19 +1,18 @@
|
||||
package epub
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/antchfx/xmlquery"
|
||||
)
|
||||
|
||||
var (
|
||||
cleantitle = regexp.MustCompile(`(?s)<head>.*</head>`)
|
||||
cleanentitles = regexp.MustCompile(`&[a-z]+;`)
|
||||
empty = regexp.MustCompile(`(?s)^[\s ]*$`)
|
||||
newlines = regexp.MustCompile(`[\r\n\s]+`)
|
||||
cleansvg = regexp.MustCompile(`(<svg.+</svg>|<!\[CDATA\[.+\]\]>)`)
|
||||
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
|
||||
@@ -25,26 +24,47 @@ type Content struct {
|
||||
XML []byte
|
||||
}
|
||||
|
||||
// parse XML, look for title and <p>.*</p> stuff
|
||||
func (c *Content) String(content []byte) error {
|
||||
title := Title{}
|
||||
|
||||
err := xml.Unmarshal(content, &title)
|
||||
doc, err := xmlquery.Parse(
|
||||
strings.NewReader(
|
||||
cleansvg.ReplaceAllString(
|
||||
cleanentitles.ReplaceAllString(string(content), " "), "")))
|
||||
if err != nil {
|
||||
if !strings.HasPrefix(err.Error(), "XML syntax error") {
|
||||
return fmt.Errorf("XML parser error %w", err)
|
||||
return 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), "")
|
||||
txt = cleanh1.ReplaceAllString(txt, "")
|
||||
txt = cleanmarkup.ReplaceAllString(txt, "")
|
||||
txt = cleanentities.ReplaceAllString(txt, " ")
|
||||
txt = cleancomments.ReplaceAllString(txt, "")
|
||||
txt = strings.TrimSpace(txt)
|
||||
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 = cleanspace.ReplaceAllString(txt, "")
|
||||
c.Body = strings.TrimSpace(txt.String())
|
||||
c.XML = content
|
||||
|
||||
if len(c.Body) == 0 {
|
||||
|
||||
@@ -1,36 +1,22 @@
|
||||
package epub
|
||||
|
||||
import (
|
||||
"log"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestEpub(t *testing.T) {
|
||||
bk, err := open(t, "test.epub")
|
||||
_, err := open(t, "test.epub")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if err := bk.Close(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}()
|
||||
|
||||
}
|
||||
|
||||
func open(t *testing.T, f string) (*Book, error) {
|
||||
bk, err := Open(f)
|
||||
bk, err := Open(f, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if err := bk.Close(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}()
|
||||
|
||||
t.Logf("files: %+v", bk.Files())
|
||||
t.Logf("book: %+v", bk)
|
||||
|
||||
|
||||
@@ -7,8 +7,9 @@ type Ncx struct {
|
||||
|
||||
// NavPoint nav point
|
||||
type NavPoint struct {
|
||||
Text string `xml:"navLabel>text" json:"text"`
|
||||
Points []NavPoint `xml:"navPoint" json:"points"`
|
||||
Text string `xml:"navLabel>text" json:"text"`
|
||||
Content Content `xml:"content" json:"content"`
|
||||
Points []NavPoint `xml:"navPoint" json:"points"`
|
||||
}
|
||||
|
||||
type Title struct {
|
||||
|
||||
180
pkg/epub/open.go
@@ -2,68 +2,204 @@ package epub
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"fmt"
|
||||
"log"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Open open a epub file
|
||||
func Open(fn string) (*Book, error) {
|
||||
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) {
|
||||
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)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if err := fd.Close(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}()
|
||||
bk := &Book{fd: fd, dumpxml: dumpxml}
|
||||
|
||||
bk := Book{fd: fd}
|
||||
return bk, nil
|
||||
}
|
||||
|
||||
// load the manifest
|
||||
func (bk *Book) getManifest() error {
|
||||
mt, err := bk.readBytes("mimetype")
|
||||
if err != nil {
|
||||
return &bk, err
|
||||
return err
|
||||
}
|
||||
|
||||
bk.Mimetype = string(mt)
|
||||
|
||||
// contains the root path
|
||||
err = bk.readXML("META-INF/container.xml", &bk.Container)
|
||||
if err != nil {
|
||||
return &bk, err
|
||||
return err
|
||||
}
|
||||
|
||||
// contains the OPF data
|
||||
err = bk.readXML(bk.Container.Rootfile.Path, &bk.Opf)
|
||||
if err != nil {
|
||||
return &bk, err
|
||||
return err
|
||||
}
|
||||
|
||||
// look for TOC (might be incomplete, see below!)
|
||||
for _, mf := range bk.Opf.Manifest {
|
||||
if mf.ID == bk.Opf.Spine.Toc {
|
||||
err = bk.readXML(bk.filename(mf.Href), &bk.Ncx)
|
||||
if err != nil {
|
||||
return &bk, err
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
break
|
||||
if mf.ID == "cover-image" {
|
||||
bk.CoverFile = mf.Href
|
||||
bk.CoverMediaType = mf.MediaType
|
||||
}
|
||||
}
|
||||
|
||||
for _, file := range bk.Files() {
|
||||
content, err := bk.readBytes(file)
|
||||
if err != nil {
|
||||
return &bk, err
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
sections = append(sections, sect)
|
||||
}
|
||||
|
||||
ct := Content{Src: file}
|
||||
if len(sections) < manifestcount {
|
||||
// TOC was incomplete, restart from scratch but use the
|
||||
// OPF Manifest directly
|
||||
|
||||
if strings.Contains(string(content), "DOCTYPE") {
|
||||
if err := ct.String(content); err != nil {
|
||||
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)
|
||||
}
|
||||
|
||||
return &bk, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
package epub
|
||||
|
||||
//Opf content.opf
|
||||
// Opf content.opf
|
||||
type Opf struct {
|
||||
Metadata Metadata `xml:"metadata" json:"metadata"`
|
||||
Manifest []Manifest `xml:"manifest>item" json:"manifest"`
|
||||
Spine Spine `xml:"spine" json:"spine"`
|
||||
}
|
||||
|
||||
//Metadata metadata
|
||||
// Metadata metadata
|
||||
type Metadata struct {
|
||||
Title []string `xml:"title" json:"title"`
|
||||
Language []string `xml:"language" json:"language"`
|
||||
@@ -53,7 +53,7 @@ type Metafield struct {
|
||||
Content string `xml:"content,attr" json:"content"`
|
||||
}
|
||||
|
||||
//Manifest manifest
|
||||
// Manifest manifest
|
||||
type Manifest struct {
|
||||
ID string `xml:"id,attr" json:"id"`
|
||||
Href string `xml:"href,attr" json:"href"`
|
||||
|
||||
BIN
pkg/epub/test.epub
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'
|
||||
|
||||