mirror of
https://codeberg.org/scip/epuppy.git
synced 2025-12-16 20:11:00 +01:00
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:
63
cmd/colors.go
Normal file
63
cmd/colors.go
Normal 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
|
||||||
|
}
|
||||||
@@ -7,20 +7,29 @@ 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>`
|
||||||
)
|
)
|
||||||
|
|
||||||
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
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
102
cmd/pager.go
102
cmd/pager.go
@@ -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
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
15
cmd/view.go
15
cmd/view.go
@@ -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
16
config.toml
Normal 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
4
go.mod
@@ -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
8
go.sum
@@ -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=
|
||||||
|
|||||||
@@ -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
46
pkg/epub/content.go
Normal 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
|
||||||
|
}
|
||||||
@@ -1,9 +1,7 @@
|
|||||||
package epub
|
package epub
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/xml"
|
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -22,50 +20,10 @@ 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
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package epub
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"archive/zip"
|
"archive/zip"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Open open a epub file
|
// Open open a epub file
|
||||||
@@ -34,19 +35,29 @@ 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)
|
||||||
|
if err != nil {
|
||||||
|
return &bk, err
|
||||||
|
}
|
||||||
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, ncx := range bk.Ncx.Points {
|
for _, file := range bk.Files() {
|
||||||
content, err := bk.readBytes(bk.filename(ncx.Content.Src))
|
content, err := bk.readBytes(file)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &bk, err
|
return &bk, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := ncx.Content.String(content); err != nil {
|
ct := Content{Src: file}
|
||||||
return &bk, err
|
|
||||||
|
if strings.Contains(string(content), "DOCTYPE") {
|
||||||
|
if err := ct.String(content); err != nil {
|
||||||
|
return &bk, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bk.Content = append(bk.Content, ct)
|
||||||
}
|
}
|
||||||
|
|
||||||
return &bk, nil
|
return &bk, nil
|
||||||
|
|||||||
Reference in New Issue
Block a user