Compare commits
12 Commits
v0.0.5
...
5fcfb0aa98
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5fcfb0aa98 | ||
|
|
85d23c42f0 | ||
| 02c99da8e9 | |||
| 4ca12b907b | |||
| 807a2712e5 | |||
| 0d80f0ef42 | |||
| 120b88803c | |||
| fc9ff4a23f | |||
|
|
c1f757197d | ||
| 7a12e8e8b0 | |||
|
|
238972f11f | ||
|
|
f524083210 |
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: 35 KiB |
BIN
.github/assets/help.png
vendored
|
Before Width: | Height: | Size: 40 KiB |
BIN
.github/assets/light.png
vendored
|
Before Width: | Height: | Size: 37 KiB |
BIN
.github/assets/margin.png
vendored
|
Before Width: | Height: | Size: 32 KiB |
54
.github/workflows/ci.yaml
vendored
@@ -1,54 +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
|
|
||||||
|
|
||||||
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:
|
|
||||||
name: Lintercheck
|
|
||||||
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
|
||||||
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/
|
||||||
47
README.md
@@ -1,6 +1,6 @@
|
|||||||
[](https://github.com/tlinden/epuppy/actions)
|
[](https://ci.codeberg.org/repos/15473)
|
||||||
[](https://github.com/tlinden/epuppy/blob/master/LICENSE)
|
[](https://codeberg.org/scip/epuppy/raw/branch/main/LICENSE)
|
||||||
[](https://goreportcard.com/report/github.com/tlinden/epuppy)
|
[](https://goreportcard.com/report/codeberg.org/scip/epuppy)
|
||||||
|
|
||||||
|
|
||||||
# epuppy - terminal epub reader
|
# epuppy - terminal epub reader
|
||||||
@@ -20,17 +20,21 @@ long run.
|
|||||||
|
|
||||||
## Screenshots
|
## 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
|
## Usage
|
||||||
|
|
||||||
@@ -42,7 +46,7 @@ progress.
|
|||||||
Sometimes you may be unhappy with the colors. Depending on your
|
Sometimes you may be unhappy with the colors. Depending on your
|
||||||
terminal style you can enable dark mode with `-D`, light mode is the
|
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
|
default. You can also configure custom colors in a config file in
|
||||||
`$HOME/.config/epuppy/confit.toml`:
|
`$HOME/.config/epuppy/config.toml`:
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
# color setting for dark mode
|
# color setting for dark mode
|
||||||
@@ -92,6 +96,16 @@ Options:
|
|||||||
-v --version show program version
|
-v --version show program version
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Reading mobi files
|
||||||
|
|
||||||
|
`epuppy` doesn't support mobi files, but you can install
|
||||||
|
[mobitool](https://github.com/bfabiszewski/libmobi/) and use it to
|
||||||
|
convert mobi files to epub. The ubuntu package is `libmobi-tools`. To convert, execute:
|
||||||
|
|
||||||
|
```default
|
||||||
|
mobitool -e somebook.epub
|
||||||
|
```
|
||||||
|
|
||||||
## 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
|
||||||
@@ -99,7 +113,12 @@ your platform from the releases page and you're good to go.
|
|||||||
|
|
||||||
### Installation using a pre-compiled binary
|
### 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:
|
and look for your OS and platform. There are two options to install the binary:
|
||||||
|
|
||||||
Directly download the binary for your platform,
|
Directly download the binary for your platform,
|
||||||
@@ -139,7 +158,7 @@ sudo make install
|
|||||||
|
|
||||||
# Report bugs
|
# 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
|
# License
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
Copyright © 2025 Thomas von Dein
|
Copyright © 2025-2026 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
|
||||||
@@ -27,12 +27,13 @@ import (
|
|||||||
"github.com/knadh/koanf/parsers/toml"
|
"github.com/knadh/koanf/parsers/toml"
|
||||||
"github.com/knadh/koanf/providers/file"
|
"github.com/knadh/koanf/providers/file"
|
||||||
"github.com/knadh/koanf/providers/posflag"
|
"github.com/knadh/koanf/providers/posflag"
|
||||||
|
"github.com/knadh/koanf/providers/structs"
|
||||||
"github.com/knadh/koanf/v2"
|
"github.com/knadh/koanf/v2"
|
||||||
flag "github.com/spf13/pflag"
|
flag "github.com/spf13/pflag"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
Version string = `v0.0.5`
|
Version string = `v0.0.8`
|
||||||
Usage string = `This is epuppy, a terminal ui ebook viewer.
|
Usage string = `This is epuppy, a terminal ui ebook viewer.
|
||||||
|
|
||||||
Usage: epuppy [options] <epub file>
|
Usage: epuppy [options] <epub file>
|
||||||
@@ -42,6 +43,7 @@ Options:
|
|||||||
-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>
|
||||||
|
--create-config create a default config file
|
||||||
-i --cover-image display cover image
|
-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
|
||||||
@@ -51,6 +53,31 @@ Options:
|
|||||||
-v --version show program version`
|
-v --version show program version`
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
DefaultDark = ColorSetting{
|
||||||
|
Title: "#ff4500",
|
||||||
|
Chapter: "#ff4500",
|
||||||
|
Body: "#cdb79e",
|
||||||
|
}
|
||||||
|
|
||||||
|
DefaultLight = ColorSetting{
|
||||||
|
Title: "#ff0000",
|
||||||
|
Chapter: "#8b0000",
|
||||||
|
Body: "#696969",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// used for config writing, it's a subset of the Config struct
|
||||||
|
// add fields as you add user configurable fields to Config
|
||||||
|
type UserConfig struct {
|
||||||
|
StoreProgress bool `koanf:"store-progress"`
|
||||||
|
Darkmode bool `koanf:"dark"`
|
||||||
|
LineNumbers bool `koanf:"line-numbers"`
|
||||||
|
NoColor bool `koanf:"no-color"`
|
||||||
|
ColorDark ColorSetting `koanf:"colordark"`
|
||||||
|
ColorLight ColorSetting `koanf:"colorlight"`
|
||||||
|
}
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Showversion bool `koanf:"version"` // -v
|
Showversion bool `koanf:"version"` // -v
|
||||||
Debug bool `koanf:"debug"` // -d
|
Debug bool `koanf:"debug"` // -d
|
||||||
@@ -64,13 +91,15 @@ type Config struct {
|
|||||||
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"`
|
ShowHelp bool `koanf:"help"`
|
||||||
ShowCover bool `koanf:"cover-image"` // -i
|
ShowCover bool `koanf:"cover-image"` // -i
|
||||||
|
CreateConfig bool `koanf:"create-config"` // --create-config
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
func InitConfig(output io.Writer) (*Config, error) {
|
func InitConfig(output io.Writer) (*Config, error) {
|
||||||
|
conf := &Config{}
|
||||||
var kloader = koanf.New(".")
|
var kloader = koanf.New(".")
|
||||||
|
|
||||||
// setup custom usage
|
// setup custom usage
|
||||||
@@ -93,6 +122,7 @@ func InitConfig(output io.Writer) (*Config, error) {
|
|||||||
flagset.BoolP("no-color", "N", false, "disable colors")
|
flagset.BoolP("no-color", "N", false, "disable colors")
|
||||||
flagset.BoolP("cover-image", "i", false, "show cover image")
|
flagset.BoolP("cover-image", "i", false, "show cover image")
|
||||||
flagset.BoolP("help", "h", false, "show help")
|
flagset.BoolP("help", "h", false, "show help")
|
||||||
|
flagset.BoolP("create-config", "", false, "create a config file with default settings")
|
||||||
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 {
|
||||||
@@ -104,14 +134,13 @@ func InitConfig(output io.Writer) (*Config, error) {
|
|||||||
var configfiles []string
|
var configfiles []string
|
||||||
|
|
||||||
configfile, _ := flagset.GetString("config")
|
configfile, _ := flagset.GetString("config")
|
||||||
home, _ := os.UserHomeDir()
|
|
||||||
|
|
||||||
if configfile != "" {
|
if configfile != "" {
|
||||||
configfiles = []string{configfile}
|
configfiles = []string{configfile}
|
||||||
} else {
|
} else {
|
||||||
configfiles = []string{
|
configfiles = []string{
|
||||||
"/etc/epuppy.toml", "/usr/local/etc/epuppy.toml", // unix variants
|
"/etc/epuppy.toml", "/usr/local/etc/epuppy.toml", // unix variants
|
||||||
filepath.Join(home, ".config", "epuppy", "config.toml"),
|
filepath.Join(conf.GetConfigDir(), "config.toml"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -132,7 +161,6 @@ func InitConfig(output io.Writer) (*Config, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// fetch values
|
// fetch values
|
||||||
conf := &Config{}
|
|
||||||
if err := kloader.Unmarshal("", &conf); err != nil {
|
if err := kloader.Unmarshal("", &conf); err != nil {
|
||||||
return nil, fmt.Errorf("error unmarshalling: %w", err)
|
return nil, fmt.Errorf("error unmarshalling: %w", err)
|
||||||
}
|
}
|
||||||
@@ -141,7 +169,7 @@ 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 && !conf.ShowHelp {
|
if !conf.Showversion && !conf.ShowHelp && !conf.CreateConfig {
|
||||||
flagset.Usage()
|
flagset.Usage()
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
@@ -152,18 +180,7 @@ func InitConfig(output io.Writer) (*Config, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// setup color config
|
// setup color config
|
||||||
conf.Colors = SetColorconfig(
|
conf.Colors = SetColorconfig(DefaultDark, DefaultLight, conf)
|
||||||
ColorSetting{ // Dark
|
|
||||||
Title: "#ff4500",
|
|
||||||
Chapter: "#ff4500",
|
|
||||||
Body: "#cdb79e",
|
|
||||||
},
|
|
||||||
ColorSetting{ // Light
|
|
||||||
Title: "#ff0000",
|
|
||||||
Chapter: "#8b0000",
|
|
||||||
Body: "#696969",
|
|
||||||
},
|
|
||||||
conf)
|
|
||||||
|
|
||||||
// disable colors if requested by command line
|
// disable colors if requested by command line
|
||||||
if conf.NoColor {
|
if conf.NoColor {
|
||||||
@@ -173,7 +190,41 @@ func InitConfig(output io.Writer) (*Config, error) {
|
|||||||
return conf, nil
|
return conf, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Config) GetConfigDir() string {
|
func (conf *Config) GetConfigDir() string {
|
||||||
home, _ := os.UserHomeDir()
|
home, _ := os.UserHomeDir()
|
||||||
return filepath.Join(home, ".config", "epuppy")
|
return filepath.Join(home, ".config", "epuppy")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (conf *Config) WriteConfigFile() (string, error) {
|
||||||
|
cfgfile := filepath.Join(conf.GetConfigDir(), "config.toml")
|
||||||
|
|
||||||
|
if FileExists(cfgfile) {
|
||||||
|
return "", fmt.Errorf("config file %s already exists", cfgfile)
|
||||||
|
}
|
||||||
|
|
||||||
|
var kloader = koanf.New(".")
|
||||||
|
|
||||||
|
userconf := UserConfig{
|
||||||
|
StoreProgress: conf.StoreProgress,
|
||||||
|
Darkmode: conf.Darkmode,
|
||||||
|
LineNumbers: conf.LineNumbers,
|
||||||
|
NoColor: conf.NoColor,
|
||||||
|
ColorDark: DefaultDark,
|
||||||
|
ColorLight: DefaultLight,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := kloader.Load(structs.Provider(userconf, "koanf"), nil); err != nil {
|
||||||
|
return "", fmt.Errorf("error loading config struct: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
toml, err := kloader.Marshal(toml.Parser())
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("error unmarshalling config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := WriteFile(cfgfile, toml); err != nil {
|
||||||
|
return "", fmt.Errorf("error writing config file %s: %w", cfgfile, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("new config file %s created.", cfgfile), nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -22,45 +22,44 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"codeberg.org/scip/epuppy/pkg/epub"
|
||||||
"github.com/alecthomas/repr"
|
"github.com/alecthomas/repr"
|
||||||
"github.com/tlinden/epuppy/pkg/epub"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func View(conf *Config) (int, error) {
|
func Prepare(conf *Config) (*Ebook, error) {
|
||||||
switch filepath.Ext(conf.Document) {
|
switch filepath.Ext(conf.Document) {
|
||||||
case ".epub":
|
case ".epub":
|
||||||
return ViewEpub(conf)
|
return PrepareEpub(conf)
|
||||||
default:
|
default:
|
||||||
return ViewText(conf)
|
return PrepareText(conf)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func ViewText(conf *Config) (int, error) {
|
func PrepareText(conf *Config) (*Ebook, error) {
|
||||||
data, err := os.ReadFile(conf.Document)
|
data, err := os.ReadFile(conf.Document)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if conf.Dump {
|
return &Ebook{
|
||||||
return fmt.Println(string(data))
|
|
||||||
}
|
|
||||||
|
|
||||||
return Pager(&Ebook{
|
|
||||||
Config: conf,
|
Config: conf,
|
||||||
Title: conf.Document,
|
Title: conf.Document,
|
||||||
Body: string(data),
|
Body: string(data),
|
||||||
})
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func ViewEpub(conf *Config) (int, error) {
|
func PrepareEpub(conf *Config) (*Ebook, error) {
|
||||||
book, err := epub.Open(conf.Document, conf.XML)
|
book, err := epub.Open(conf.Document, conf.XML)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if conf.Debug {
|
if conf.Debug {
|
||||||
|
repr.Println("book.Files()")
|
||||||
repr.Println(book.Files())
|
repr.Println(book.Files())
|
||||||
repr.Println(book.Ncx)
|
repr.Println(book.Ncx)
|
||||||
|
repr.Println(book.Sections)
|
||||||
|
repr.Println(book.Opf.Manifest)
|
||||||
}
|
}
|
||||||
|
|
||||||
buf := strings.Builder{}
|
buf := strings.Builder{}
|
||||||
@@ -80,21 +79,13 @@ func ViewEpub(conf *Config) (int, error) {
|
|||||||
|
|
||||||
fetchByContent(conf, &buf, book)
|
fetchByContent(conf, &buf, book)
|
||||||
|
|
||||||
if conf.Dump {
|
return &Ebook{
|
||||||
return fmt.Println(buf.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
if conf.Debug {
|
|
||||||
return 0, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return Pager(&Ebook{
|
|
||||||
Config: conf,
|
Config: conf,
|
||||||
Title: head.String(),
|
Title: head.String(),
|
||||||
Body: buf.String(),
|
Body: buf.String(),
|
||||||
Cover: book.CoverImage,
|
Cover: book.CoverImage,
|
||||||
MediaType: book.CoverMediaType,
|
MediaType: book.CoverMediaType,
|
||||||
})
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func fetchByContent(conf *Config, buf *strings.Builder, book *epub.Book) bool {
|
func fetchByContent(conf *Config, buf *strings.Builder, book *epub.Book) bool {
|
||||||
30
cmd/root.go
@@ -52,6 +52,20 @@ func Execute(output io.Writer) int {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if conf.CreateConfig {
|
||||||
|
msg, err := conf.WriteConfigFile()
|
||||||
|
if err != nil {
|
||||||
|
return Die(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = fmt.Fprintln(output, msg)
|
||||||
|
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 {
|
||||||
@@ -59,7 +73,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 {
|
if err != nil {
|
||||||
return Die(err)
|
return Die(err)
|
||||||
}
|
}
|
||||||
|
|||||||
30
cmd/store.go
@@ -102,9 +102,37 @@ func Mkdir(dir string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME: check https://github.com/gosimple/slug
|
|
||||||
func Slug(input string) string {
|
func Slug(input string) string {
|
||||||
slug := slugify.ReplaceAllString(input, "-")
|
slug := slugify.ReplaceAllString(input, "-")
|
||||||
slug = suffix.ReplaceAllString(slug, "")
|
slug = suffix.ReplaceAllString(slug, "")
|
||||||
return nonprintable.ReplaceAllString(slug, "")
|
return nonprintable.ReplaceAllString(slug, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func FileExists(filename string) bool {
|
||||||
|
info, err := os.Stat(filename)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
// return false on any error
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return !info.IsDir()
|
||||||
|
}
|
||||||
|
|
||||||
|
func WriteFile(filename string, content []byte) error {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
fd, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0755)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to open file %s for writing: %w", filename, err)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if err := fd.Close(); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
_, err = fd.Write(content)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|||||||
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")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
8
go.mod
@@ -1,4 +1,4 @@
|
|||||||
module github.com/tlinden/epuppy
|
module codeberg.org/scip/epuppy
|
||||||
|
|
||||||
go 1.24.0
|
go 1.24.0
|
||||||
|
|
||||||
@@ -43,19 +43,25 @@ 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/blacktop/go-termimg v0.1.20 // indirect
|
||||||
github.com/charmbracelet/x/mosaic v0.0.0-20250702191427-5bdfc8f2e4ff // indirect
|
github.com/charmbracelet/x/mosaic v0.0.0-20250702191427-5bdfc8f2e4ff // indirect
|
||||||
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
|
github.com/fatih/structs v1.1.0 // 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/knadh/koanf/providers/structs v1.0.0 // indirect
|
||||||
github.com/makeworld-the-better-one/dither/v2 v2.4.0 // indirect
|
github.com/makeworld-the-better-one/dither/v2 v2.4.0 // indirect
|
||||||
github.com/mattn/go-sixel v0.0.5 // 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/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect
|
||||||
github.com/pelletier/go-toml v1.9.5 // indirect
|
github.com/pelletier/go-toml v1.9.5 // indirect
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
||||||
github.com/soniakeys/quant v1.0.0 // 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/image v0.25.0 // indirect
|
||||||
golang.org/x/net v0.44.0 // indirect
|
golang.org/x/net v0.44.0 // indirect
|
||||||
golang.org/x/tools v0.37.0 // indirect
|
golang.org/x/tools v0.37.0 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
6
go.sum
@@ -33,6 +33,8 @@ 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=
|
||||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
|
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
|
||||||
|
github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
|
||||||
|
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
|
||||||
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||||
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
|
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
|
||||||
@@ -48,6 +50,8 @@ github.com/knadh/koanf/providers/file v1.2.0 h1:hrUJ6Y9YOA49aNu/RSYzOTFlqzXSCpmY
|
|||||||
github.com/knadh/koanf/providers/file v1.2.0/go.mod h1:bp1PM5f83Q+TOUu10J/0ApLBd9uIzg+n9UgthfY+nRA=
|
github.com/knadh/koanf/providers/file v1.2.0/go.mod h1:bp1PM5f83Q+TOUu10J/0ApLBd9uIzg+n9UgthfY+nRA=
|
||||||
github.com/knadh/koanf/providers/posflag v1.0.1 h1:EnMxHSrPkYCFnKgBUl5KBgrjed8gVFrcXDzaW4l/C6Y=
|
github.com/knadh/koanf/providers/posflag v1.0.1 h1:EnMxHSrPkYCFnKgBUl5KBgrjed8gVFrcXDzaW4l/C6Y=
|
||||||
github.com/knadh/koanf/providers/posflag v1.0.1/go.mod h1:3Wn3+YG3f4ljzRyCUgIwH7G0sZ1pMjCOsNBovrbKmAk=
|
github.com/knadh/koanf/providers/posflag v1.0.1/go.mod h1:3Wn3+YG3f4ljzRyCUgIwH7G0sZ1pMjCOsNBovrbKmAk=
|
||||||
|
github.com/knadh/koanf/providers/structs v1.0.0 h1:DznjB7NQykhqCar2LvNug3MuxEQsZ5KvfgMbio+23u4=
|
||||||
|
github.com/knadh/koanf/providers/structs v1.0.0/go.mod h1:kjo5TFtgpaZORlpoJqcbeLowM2cINodv8kX+oFAeQ1w=
|
||||||
github.com/knadh/koanf/v2 v2.3.0 h1:Qg076dDRFHvqnKG97ZEsi9TAg2/nFTa9hCdcSa1lvlM=
|
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=
|
||||||
@@ -95,6 +99,8 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
|
|||||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
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/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 h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
||||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
|
|||||||
2
main.go
@@ -19,7 +19,7 @@ package main
|
|||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/tlinden/epuppy/cmd"
|
"codeberg.org/scip/epuppy/cmd"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
|||||||
75
mkrel.sh
@@ -1,75 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# 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/>.
|
|
||||||
|
|
||||||
|
|
||||||
# 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
|
|
||||||
|
|
||||||
@@ -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"`
|
||||||
@@ -20,6 +25,8 @@ type Book struct {
|
|||||||
CoverImage []byte
|
CoverImage []byte
|
||||||
CoverFile string
|
CoverFile string
|
||||||
CoverMediaType string
|
CoverMediaType string
|
||||||
|
Sections []Section
|
||||||
|
dumpxml bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// Open open resource file
|
// Open open resource file
|
||||||
|
|||||||
@@ -8,10 +8,12 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
cleanentitles = regexp.MustCompile(`&[a-z]+;`)
|
cleanenTitles = regexp.MustCompile(`&[a-z]+;`)
|
||||||
empty = regexp.MustCompile(`(?s)^[\s ]*$`)
|
isEmpty = regexp.MustCompile(`(?s)^[\s ]*$`)
|
||||||
newlines = regexp.MustCompile(`[\r\n]+`)
|
cleanNewlines = regexp.MustCompile(`[\r\n\s]+`)
|
||||||
cleanmarkup = regexp.MustCompile(`<[^<>]+>`)
|
cleanSVG = regexp.MustCompile(`(<svg.+</svg>|<!\[CDATA\[.+\]\]>)`)
|
||||||
|
cleanMarkup = regexp.MustCompile(`<[^<>]+>`)
|
||||||
|
cleanMobiPageBreaks = regexp.MustCompile(`<mbp:pagebreak/>`)
|
||||||
)
|
)
|
||||||
|
|
||||||
// Content nav-point content
|
// Content nav-point content
|
||||||
@@ -24,12 +26,28 @@ type Content struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// parse XML, look for title and <p>.*</p> stuff
|
// parse XML, look for title and <p>.*</p> stuff
|
||||||
func (c *Content) String(content []byte) error {
|
func (c *Content) Extract(content []byte) error {
|
||||||
doc, err := xmlquery.Parse(
|
rawXML := cleanSVG.ReplaceAllString(
|
||||||
strings.NewReader(
|
cleanenTitles.ReplaceAllString(string(content), " "), "")
|
||||||
cleanentitles.ReplaceAllString(string(content), " ")))
|
|
||||||
|
var doc *xmlquery.Node
|
||||||
|
var err error
|
||||||
|
|
||||||
|
doc, err = xmlquery.Parse(strings.NewReader(rawXML))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
if strings.Contains(err.Error(), `namespace mbp is missing`) {
|
||||||
|
fixedmbp := strings.NewReader(
|
||||||
|
cleanMobiPageBreaks.ReplaceAllString(
|
||||||
|
rawXML, `<span style="page-break-after: always" />`))
|
||||||
|
|
||||||
|
doc, err = xmlquery.Parse(fixedmbp)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.Title == "" {
|
if c.Title == "" {
|
||||||
@@ -45,9 +63,9 @@ func (c *Content) String(content []byte) error {
|
|||||||
txt := strings.Builder{}
|
txt := strings.Builder{}
|
||||||
var have_p bool
|
var have_p bool
|
||||||
for _, item := range xmlquery.Find(doc, "//p") {
|
for _, item := range xmlquery.Find(doc, "//p") {
|
||||||
if !empty.MatchString(item.InnerText()) {
|
if !isEmpty.MatchString(item.InnerText()) {
|
||||||
have_p = true
|
have_p = true
|
||||||
txt.WriteString(newlines.ReplaceAllString(item.InnerText(), " ") + "\n\n")
|
txt.WriteString(cleanNewlines.ReplaceAllString(item.InnerText(), " ") + "\n\n")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,9 +73,9 @@ func (c *Content) String(content []byte) error {
|
|||||||
// try <div></div>, which some ebooks use, so get all divs,
|
// try <div></div>, which some ebooks use, so get all divs,
|
||||||
// remove markup and paragraphify the parts
|
// remove markup and paragraphify the parts
|
||||||
for _, item := range xmlquery.Find(doc, "//div") {
|
for _, item := range xmlquery.Find(doc, "//div") {
|
||||||
if !empty.MatchString(item.InnerText()) {
|
if !isEmpty.MatchString(item.InnerText()) {
|
||||||
cleaned := cleanmarkup.ReplaceAllString(item.InnerText(), "")
|
cleaned := cleanMarkup.ReplaceAllString(item.InnerText(), "")
|
||||||
txt.WriteString(newlines.ReplaceAllString(cleaned, " ") + "\n\n")
|
txt.WriteString(cleanNewlines.ReplaceAllString(cleaned, " ") + "\n\n")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
205
pkg/epub/open.go
@@ -4,48 +4,87 @@ import (
|
|||||||
"archive/zip"
|
"archive/zip"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/alecthomas/repr"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// 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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,58 +94,120 @@ func Open(fn string, dumpxml bool) (*Book, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type section struct {
|
return nil
|
||||||
file, title string
|
}
|
||||||
|
|
||||||
|
// 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++
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sections := []section{}
|
// we have ncx points from the TOC, try those
|
||||||
|
|
||||||
if len(bk.Ncx.Points) > 0 {
|
if len(bk.Ncx.Points) > 0 {
|
||||||
|
known := map[string]int{}
|
||||||
|
|
||||||
for _, block := range bk.Ncx.Points {
|
for _, block := range bk.Ncx.Points {
|
||||||
sections = append(sections,
|
sect := Section{
|
||||||
section{
|
File: "OEBPS/" + block.Content.Src,
|
||||||
file: "OEBPS/" + block.Content.Src,
|
Title: block.Text,
|
||||||
title: block.Text,
|
}
|
||||||
})
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
for _, file := range bk.Files() {
|
|
||||||
sections = append(sections,
|
|
||||||
section{
|
|
||||||
file: file,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, section := range sections {
|
srcfile := deanchor.ReplaceAllString(block.Content.Src, "")
|
||||||
content, err := bk.readBytes(section.file)
|
|
||||||
if err != nil {
|
|
||||||
return &bk, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if strings.Contains(section.file, bk.CoverFile) {
|
for _, file := range bk.Files() {
|
||||||
bk.CoverImage = content
|
if strings.Contains(file, srcfile) {
|
||||||
}
|
sect.File = file
|
||||||
|
sect.MediaType = "application/" + cleanext.ReplaceAllString(filepath.Ext(file), "")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ct := Content{Src: section.file, Title: section.title}
|
if _, haveFile := known[sect.File]; !haveFile {
|
||||||
|
// sometimes epub's have many sections but they all
|
||||||
if strings.Contains(string(content), "<?xml") || strings.Contains(string(content), "<!DOCTYPE") {
|
// point to the same file. To avoid duplicate content
|
||||||
if err := ct.String(content); err != nil {
|
// we ignore sections (thus files) we have already seen.
|
||||||
return &bk, err
|
sections = append(sections, sect)
|
||||||
|
known[sect.File] = 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if dumpxml {
|
if len(sections) < manifestcount {
|
||||||
|
// TOC was incomplete, restart from scratch but use the
|
||||||
|
// OPF Manifest directly
|
||||||
|
|
||||||
|
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.Extract(content); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if bk.dumpxml {
|
||||||
fmt.Println(string(ct.XML))
|
fmt.Println(string(ct.XML))
|
||||||
}
|
}
|
||||||
|
|
||||||
bk.Content = append(bk.Content, ct)
|
bk.Content = append(bk.Content, ct)
|
||||||
}
|
}
|
||||||
|
|
||||||
if dumpxml {
|
return nil
|
||||||
repr.Println(sections)
|
|
||||||
os.Exit(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &bk, nil
|
|
||||||
}
|
}
|
||||||
|
|||||||