lots of fixes and additions:

- add color and color config support
- fix epub parsing
- add variable margin (left and right arrow keys)
- add help screen (hit `?`)
- add config file support
This commit is contained in:
2025-10-15 14:36:43 +02:00
parent 0d4c44ee11
commit 7544f8877a
12 changed files with 337 additions and 65 deletions

63
cmd/colors.go Normal file
View File

@@ -0,0 +1,63 @@
package cmd
import (
"github.com/charmbracelet/lipgloss"
)
type ColorSetting struct {
Title string `koanf:"title"`
Chapter string `koanf:"chapter"`
Body string `koanf:"body"`
}
type Colors struct {
Title lipgloss.Style
Chapter lipgloss.Style
Body lipgloss.Style
}
func SetColorconfig(defaultdark, defaultlight ColorSetting, conf *Config) Colors {
var defaults, user ColorSetting
switch conf.Darkmode {
case true:
defaults = defaultdark
user = conf.ColorDark
default:
defaults = defaultlight
user = conf.ColorLight
}
var colors Colors
var fg string
border := lipgloss.RoundedBorder()
border.Right = "├"
styletitle := lipgloss.NewStyle().BorderStyle(border).Padding(0, 1)
if user.Title != "" {
fg = user.Title
} else {
fg = defaults.Title
}
colors.Title = styletitle.Foreground(lipgloss.Color(fg))
if user.Chapter != "" {
fg = user.Chapter
} else {
fg = defaults.Chapter
}
colors.Chapter = lipgloss.NewStyle().Foreground(lipgloss.Color(fg))
if user.Body != "" {
fg = user.Body
} else {
fg = defaults.Body
}
colors.Body = lipgloss.NewStyle().Foreground(lipgloss.Color(fg))
return colors
}

View File

@@ -7,13 +7,16 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"github.com/alecthomas/repr"
"github.com/knadh/koanf/parsers/toml"
"github.com/knadh/koanf/providers/file"
"github.com/knadh/koanf/providers/posflag" "github.com/knadh/koanf/providers/posflag"
"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.1` Version string = `v0.0.2`
Usage string = `epuppy [-vd] <epub file>` Usage string = `epuppy [-vd] <epub file>`
) )
@@ -21,6 +24,12 @@ type Config struct {
Showversion bool `koanf:"version"` // -v Showversion bool `koanf:"version"` // -v
Debug bool `koanf:"debug"` // -d Debug bool `koanf:"debug"` // -d
StoreProgress bool `koanf:"store-progress"` // -s StoreProgress bool `koanf:"store-progress"` // -s
Darkmode bool `koanf:"dark"` // -D
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
Document string Document string
InitialProgress int // lines InitialProgress int // lines
} }
@@ -41,12 +50,41 @@ func InitConfig(output io.Writer) (*Config, error) {
// parse commandline flags // parse commandline flags
flagset.BoolP("version", "v", false, "show program version") flagset.BoolP("version", "v", false, "show program version")
flagset.BoolP("debug", "d", false, "enable debugging") flagset.BoolP("debug", "d", false, "enable debugging")
flagset.BoolP("dark", "D", false, "enable dark mode")
flagset.BoolP("store-progress", "s", false, "store reading progress") flagset.BoolP("store-progress", "s", false, "store reading progress")
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 {
return nil, fmt.Errorf("failed to parse program arguments: %w", err) return nil, fmt.Errorf("failed to parse program arguments: %w", err)
} }
// generate a list of config files to try to load, including the
// one provided via -c, if any
var configfiles []string
configfile, _ := flagset.GetString("config")
home, _ := os.UserHomeDir()
if configfile != "" {
configfiles = []string{configfile}
} else {
configfiles = []string{
"/etc/epuppy.toml", "/usr/local/etc/epuppy.toml", // unix variants
filepath.Join(home, ".config", "epuppy", "config.toml"),
}
}
// Load the config file[s]
for _, cfgfile := range configfiles {
if path, err := os.Stat(cfgfile); !os.IsNotExist(err) {
if !path.IsDir() {
if err := kloader.Load(file.Provider(cfgfile), toml.Parser()); err != nil {
return nil, fmt.Errorf("error loading config file: %w", err)
}
}
} // else: we ignore the file if it doesn't exists
}
// command line setup // command line setup
if err := kloader.Load(posflag.Provider(flagset, ".", kloader), nil); err != nil { if err := kloader.Load(posflag.Provider(flagset, ".", kloader), nil); err != nil {
return nil, fmt.Errorf("error loading flags: %w", err) return nil, fmt.Errorf("error loading flags: %w", err)
@@ -63,6 +101,24 @@ func InitConfig(output io.Writer) (*Config, error) {
conf.Document = flagset.Args()[0] conf.Document = flagset.Args()[0]
} }
if conf.Debug {
repr.Println(conf)
}
// setup color config
conf.Colors = SetColorconfig(
ColorSetting{ // Dark
Title: "#ff4500",
Chapter: "#ff4500",
Body: "#cdb79e",
},
ColorSetting{ // Light
Title: "#ff0000",
Chapter: "#8b0000",
Body: "#696969",
},
conf)
return conf, nil return conf, nil
} }

View File

@@ -9,6 +9,8 @@ import (
"os" "os"
"strings" "strings"
"github.com/charmbracelet/bubbles/help"
"github.com/charmbracelet/bubbles/key"
"github.com/charmbracelet/bubbles/viewport" "github.com/charmbracelet/bubbles/viewport"
tea "github.com/charmbracelet/bubbletea" tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss" "github.com/charmbracelet/lipgloss"
@@ -32,6 +34,11 @@ var (
viewstyle = lipgloss.NewStyle() viewstyle = lipgloss.NewStyle()
) )
const (
MarginStep = 5
MinSize = 40
)
type Meta struct { type Meta struct {
lines int lines int
currentline int currentline int
@@ -39,13 +46,67 @@ type Meta struct {
document string document string
} }
type keyMap struct {
Up key.Binding
Down key.Binding
Left key.Binding
Right key.Binding
Help key.Binding
Quit 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
}
}
func (k keyMap) ShortHelp() []key.Binding {
return []key.Binding{k.Help, k.Quit}
}
var keys = keyMap{
Up: key.NewBinding(
key.WithKeys("up", "k"),
key.WithHelp("↑", "move up"),
),
Down: key.NewBinding(
key.WithKeys("down", "j"),
key.WithHelp("↓", "move down"),
),
Left: key.NewBinding(
key.WithKeys("left", "h"),
key.WithHelp("←", "decrease text width"),
),
Right: key.NewBinding(
key.WithKeys("right", "l"),
key.WithHelp("→", "increase text width"),
),
Help: key.NewBinding(
key.WithKeys("?"),
key.WithHelp("?", "toggle help"),
),
Quit: key.NewBinding(
key.WithKeys("q", "esc", "ctrl+c"),
key.WithHelp("q", "quit"),
),
}
type Doc struct { type Doc struct {
content string content string
title string title string
ready bool ready bool
viewport viewport.Model viewport viewport.Model
initialwidth int initialwidth int
lastwidth int
margin int
marginMod bool
meta *Meta meta *Meta
config *Config
keys keyMap
help help.Model
} }
func (m Doc) Init() tea.Cmd { func (m Doc) Init() tea.Cmd {
@@ -60,8 +121,21 @@ func (m Doc) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) { switch msg := msg.(type) {
case tea.KeyMsg: case tea.KeyMsg:
if k := msg.String(); k == "ctrl+c" || k == "q" || k == "esc" { switch {
case key.Matches(msg, m.keys.Help):
m.help.ShowAll = !m.help.ShowAll
case key.Matches(msg, m.keys.Quit):
return m, tea.Quit return m, tea.Quit
case key.Matches(msg, m.keys.Left):
if m.lastwidth-(m.margin*2) >= MinSize {
m.margin += MarginStep
m.marginMod = true
}
case key.Matches(msg, m.keys.Right):
if m.margin >= MarginStep {
m.margin -= MarginStep
m.marginMod = true
}
} }
case tea.WindowSizeMsg: case tea.WindowSizeMsg:
@@ -85,10 +159,11 @@ func (m Doc) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
} else { } else {
m.viewport.Width = msg.Width m.viewport.Width = msg.Width
m.viewport.Height = msg.Height - verticalMarginHeight m.viewport.Height = msg.Height - verticalMarginHeight
m.viewport.SetContent(wordwrap.String(m.content, msg.Width))
} }
} }
m.Rewrap()
// Handle keyboard and mouse events in the viewport // Handle keyboard and mouse events in the viewport
m.viewport, cmd = m.viewport.Update(msg) m.viewport, cmd = m.viewport.Update(msg)
cmds = append(cmds, cmd) cmds = append(cmds, cmd)
@@ -96,6 +171,17 @@ func (m Doc) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, tea.Batch(cmds...) return m, tea.Batch(cmds...)
} }
// re-calculate word wrapping, also add left margin
func (m *Doc) Rewrap() {
if m.lastwidth != m.viewport.Width || m.marginMod {
m.viewport.SetContent(wordwrap.String(m.content, m.viewport.Width-(m.margin*2)))
m.lastwidth = m.viewport.Width
m.marginMod = false
m.viewport.Style = viewstyle.MarginLeft(m.margin)
}
}
func (m Doc) View() string { func (m Doc) View() string {
if !m.ready { if !m.ready {
return "\n Initializing..." return "\n Initializing..."
@@ -105,17 +191,23 @@ func (m Doc) View() string {
// FIXME: doesn't work correctly yet // FIXME: doesn't work correctly yet
m.meta.currentline = int(float64(m.meta.lines) * m.viewport.ScrollPercent()) m.meta.currentline = int(float64(m.meta.lines) * m.viewport.ScrollPercent())
return fmt.Sprintf("%s\n%s\n%s", m.headerView(), m.viewport.View(), m.footerView()) var helpView string
if m.help.ShowAll {
helpView = "\n" + m.help.View(m.keys)
}
return fmt.Sprintf("%s\n%s\n%s%s", m.headerView(), m.viewport.View(), m.footerView(), helpView)
} }
func (m Doc) headerView() string { func (m Doc) headerView() string {
title := titleStyle.Render(m.title) title := m.config.Colors.Title.Render(m.title)
line := strings.Repeat("─", max(0, m.viewport.Width-lipgloss.Width(title))) line := strings.Repeat("─", max(0, m.viewport.Width-lipgloss.Width(title)))
return lipgloss.JoinHorizontal(lipgloss.Center, title, line) return lipgloss.JoinHorizontal(lipgloss.Center, title, line)
} }
func (m Doc) footerView() string { func (m Doc) footerView() string {
info := infoStyle.Render(fmt.Sprintf("%3.f%%", m.viewport.ScrollPercent()*100)) info := infoStyle.Render(fmt.Sprintf("%3.f%%", m.viewport.ScrollPercent()*100))
line := strings.Repeat("─", max(0, m.viewport.Width-lipgloss.Width(info))) line := strings.Repeat("─", max(0, m.viewport.Width-lipgloss.Width(info)))
return lipgloss.JoinHorizontal(lipgloss.Center, line, info) return lipgloss.JoinHorizontal(lipgloss.Center, line, info)
} }
@@ -148,7 +240,7 @@ func Pager(conf *Config, title, message string) (int, error) {
} }
p := tea.NewProgram( p := tea.NewProgram(
Doc{content: message, title: title, initialwidth: width, meta: &meta}, Doc{content: message, title: title, initialwidth: width, meta: &meta, config: conf, keys: keys},
tea.WithAltScreen(), // use the full size of the terminal in its "alternate screen buffer" tea.WithAltScreen(), // use the full size of the terminal in its "alternate screen buffer"
tea.WithMouseCellMotion(), // turn on mouse support so we can track the mouse wheel tea.WithMouseCellMotion(), // turn on mouse support so we can track the mouse wheel
) )

View File

@@ -3,11 +3,11 @@ package cmd
import ( import (
"fmt" "fmt"
"io" "io"
"log" "os"
) )
func Die(err error) int { func Die(err error) int {
log.Fatal("Error: ", err.Error()) fmt.Fprintln(os.Stderr, "Error: ", err.Error())
return 1 return 1
} }

View File

@@ -1,6 +1,7 @@
package cmd package cmd
import ( import (
"fmt"
"strings" "strings"
"github.com/tlinden/epuppy/pkg/epub" "github.com/tlinden/epuppy/pkg/epub"
@@ -28,12 +29,18 @@ func View(conf *Config) (int, error) {
head.WriteString(" ") head.WriteString(" ")
} }
for _, point := range book.Ncx.Points { // FIXME: since the switch to book.Files() in epub.Open() this
if len(point.Content.Body) > 0 { // returns invalid chapter numbering
buf.WriteString("### " + point.Content.Title) chapter := 1
for _, content := range book.Content {
if len(content.Body) > 0 {
buf.WriteString(conf.Colors.Chapter.
Render(fmt.Sprintf("Chapter %d: %s", chapter, content.Title)))
buf.WriteString("\r\n\r\n") buf.WriteString("\r\n\r\n")
buf.WriteString(point.Content.Body) buf.WriteString(conf.Colors.Body.Render(content.Body))
buf.WriteString("\r\n\r\n\r\n\r\n") buf.WriteString("\r\n\r\n\r\n\r\n")
chapter++
} }
} }

16
config.toml Normal file
View File

@@ -0,0 +1,16 @@
# 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

4
go.mod
View File

@@ -35,12 +35,16 @@ require (
require ( require (
github.com/alecthomas/repr v0.5.2 // indirect github.com/alecthomas/repr v0.5.2 // 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/knadh/koanf/maps v0.1.2 // indirect github.com/knadh/koanf/maps v0.1.2 // indirect
github.com/knadh/koanf/parsers/toml v0.1.0 // indirect
github.com/knadh/koanf/providers/file v1.2.0 // indirect
github.com/knadh/koanf/providers/posflag v1.0.1 // indirect github.com/knadh/koanf/providers/posflag v1.0.1 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/muesli/reflow v0.3.0 // indirect github.com/muesli/reflow v0.3.0 // indirect
github.com/pelletier/go-toml v1.9.5 // indirect
golang.org/x/term v0.36.0 // indirect golang.org/x/term v0.36.0 // indirect
) )

8
go.sum
View File

@@ -70,6 +70,8 @@ github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
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/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
@@ -170,6 +172,10 @@ github.com/knadh/koanf v1.5.0 h1:q2TSd/3Pyc/5yP9ldIrSdIz26MCcyNQzW0pEAugLPNs=
github.com/knadh/koanf v1.5.0/go.mod h1:Hgyjp4y8v44hpZtPzs7JZfRAW5AhN7KfZcwv1RYggDs= github.com/knadh/koanf v1.5.0/go.mod h1:Hgyjp4y8v44hpZtPzs7JZfRAW5AhN7KfZcwv1RYggDs=
github.com/knadh/koanf/maps v0.1.2 h1:RBfmAW5CnZT+PJ1CVc1QSJKf4Xu9kxfQgYVQSu8hpbo= github.com/knadh/koanf/maps v0.1.2 h1:RBfmAW5CnZT+PJ1CVc1QSJKf4Xu9kxfQgYVQSu8hpbo=
github.com/knadh/koanf/maps v0.1.2/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI= github.com/knadh/koanf/maps v0.1.2/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI=
github.com/knadh/koanf/parsers/toml v0.1.0 h1:S2hLqS4TgWZYj4/7mI5m1CQQcWurxUz6ODgOub/6LCI=
github.com/knadh/koanf/parsers/toml v0.1.0/go.mod h1:yUprhq6eo3GbyVXFFMdbfZSo928ksS+uo0FFqNMnO18=
github.com/knadh/koanf/providers/file v1.2.0 h1:hrUJ6Y9YOA49aNu/RSYzOTFlqzXSCpmYIDXI7OJU6+U=
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/v2 v2.3.0 h1:Qg076dDRFHvqnKG97ZEsi9TAg2/nFTa9hCdcSa1lvlM= github.com/knadh/koanf/v2 v2.3.0 h1:Qg076dDRFHvqnKG97ZEsi9TAg2/nFTa9hCdcSa1lvlM=
@@ -237,6 +243,8 @@ github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FI
github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pelletier/go-toml v1.7.0 h1:7utD74fnzVc/cpcyy8sjrlFr5vYpypUixARcHIMIGuI= github.com/pelletier/go-toml v1.7.0 h1:7utD74fnzVc/cpcyy8sjrlFr5vYpypUixARcHIMIGuI=
github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAvS1LBMMhTE= github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAvS1LBMMhTE=
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/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=

View File

@@ -14,6 +14,7 @@ type Book struct {
Opf Opf `json:"opf"` Opf Opf `json:"opf"`
Container Container `json:"-"` Container Container `json:"-"`
Mimetype string `json:"-"` Mimetype string `json:"-"`
Content []Content
fd *zip.ReadCloser fd *zip.ReadCloser
} }
@@ -49,7 +50,12 @@ func (p *Book) readXML(n string, v interface{}) error {
} }
defer fd.Close() defer fd.Close()
dec := xml.NewDecoder(fd) dec := xml.NewDecoder(fd)
return dec.Decode(v)
if err := dec.Decode(v); err != nil {
return fmt.Errorf("XML decoder error %w", err)
}
return nil
} }
func (p *Book) readBytes(n string) ([]byte, error) { func (p *Book) readBytes(n string) ([]byte, error) {
@@ -59,8 +65,12 @@ func (p *Book) readBytes(n string) ([]byte, error) {
} }
defer fd.Close() defer fd.Close()
return io.ReadAll(fd) data, err := io.ReadAll(fd)
if err != nil {
return nil, fmt.Errorf("failed to read contents from %s %w", n, err)
}
return data, nil
} }
func (p *Book) open(n string) (io.ReadCloser, error) { func (p *Book) open(n string) (io.ReadCloser, error) {
@@ -69,5 +79,6 @@ func (p *Book) open(n string) (io.ReadCloser, error) {
return f.Open() return f.Open()
} }
} }
return nil, fmt.Errorf("file %s not exist", n) return nil, fmt.Errorf("file %s not exist", n)
} }

46
pkg/epub/content.go Normal file
View File

@@ -0,0 +1,46 @@
package epub
import (
"encoding/xml"
"fmt"
"strings"
)
// Content nav-point content
type Content struct {
Src string `xml:"src,attr" json:"src"`
Empty bool
Body string
Title string
XML []byte
}
func (c *Content) String(content []byte) error {
title := Title{}
err := xml.Unmarshal(content, &title)
if err != nil {
if !strings.HasPrefix(err.Error(), "XML syntax error") {
return fmt.Errorf("XML parser error %w", err)
}
}
c.Title = strings.TrimSpace(title.Content)
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)
c.Body = cleanspace.ReplaceAllString(txt, "")
c.XML = content
if len(c.Body) == 0 {
c.Empty = true
}
return nil
}

View File

@@ -1,9 +1,7 @@
package epub package epub
import ( import (
"encoding/xml"
"regexp" "regexp"
"strings"
) )
var ( var (
@@ -23,49 +21,9 @@ type Ncx struct {
// NavPoint nav point // NavPoint nav point
type NavPoint struct { type NavPoint struct {
Text string `xml:"navLabel>text" json:"text"` Text string `xml:"navLabel>text" json:"text"`
Content Content `xml:"content" json:"content"`
Points []NavPoint `xml:"navPoint" json:"points"` Points []NavPoint `xml:"navPoint" json:"points"`
} }
type Title struct { type Title struct {
Content string `xml:"head>title"` Content string `xml:"head>title"`
} }
// Content nav-point content
type Content struct {
Src string `xml:"src,attr" json:"src"`
Empty bool
Body string
Title string
XML []byte
}
func (c *Content) String(content []byte) error {
title := Title{}
err := xml.Unmarshal(content, &title)
if err != nil {
if !strings.HasPrefix(err.Error(), "XML syntax error") {
return err
}
}
c.Title = title.Content
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)
c.Body = cleanspace.ReplaceAllString(txt, "")
c.XML = content
if len(c.Body) == 0 {
c.Empty = true
}
return nil
}

View File

@@ -2,6 +2,7 @@ package epub
import ( import (
"archive/zip" "archive/zip"
"strings"
) )
// Open open a epub file // Open open a epub file
@@ -34,20 +35,30 @@ func Open(fn string) (*Book, error) {
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)
break
}
}
for _, ncx := range bk.Ncx.Points {
content, err := bk.readBytes(bk.filename(ncx.Content.Src))
if err != nil { if err != nil {
return &bk, err return &bk, err
} }
if err := ncx.Content.String(content); err != nil { break
}
}
for _, file := range bk.Files() {
content, err := bk.readBytes(file)
if err != nil {
return &bk, err
}
ct := Content{Src: file}
if strings.Contains(string(content), "DOCTYPE") {
if err := ct.String(content); err != nil {
return &bk, err return &bk, err
} }
} }
bk.Content = append(bk.Content, ct)
}
return &bk, nil return &bk, nil
} }