Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 48 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ How markdown-proxy compares to other Markdown viewing tools:
| PlantUML diagrams | ✅ | ✅ | ❌ | ❌ | ❌ |
| Math rendering | ✅ (KaTeX) | ✅ (KaTeX/MathJax) | ❌ | ❌ | ❌ |
| Code highlighting | ✅ | ✅ | ✅ | ✅ | ✅ |
| CSS themes | 3 built-in | 15+ built-in | GitHub only | Customizable | 5 built-in |
| CSS themes | 3 built-in + user-defined | 15+ built-in | GitHub only | Customizable | 5 built-in |
| Full-text search | ❌ | ❌ | ❌ | ✅ | ❌ |
| Export (PDF, HTML) | △ ² | ✅ (PDF, HTML, Word) | ✅ (HTML) | ❌ | ❌ |
| Authentication | Token-based | — | — | HTTP Basic | ❌ |
Expand Down Expand Up @@ -65,7 +65,7 @@ How markdown-proxy compares to other Markdown viewing tools:
- Toolbar actions
- Print: browser print with clean filename (toolbar hidden in print output)
- Source: link to original URL on remote server (remote pages only)
- Multiple CSS themes (GitHub, Simple, Dark) with switching UI
- CSS themes with dropdown switching — 3 built-in themes (GitHub, Simple, Dark) plus user-defined themes
- Table of contents sidebar: toggle `TOC` in the toolbar to open a right-side panel with auto-extracted headings; visibility persists per browser (localStorage)
- Live reload for local files (auto-refreshes browser on file changes)
- Directory listing for local files
Expand Down Expand Up @@ -152,7 +152,7 @@ When `file-or-url` is provided:
|------|-------------|---------|
| `-port`, `-p` | Listen port | `9080` |
| `-listen` | Bind address (`127.0.0.1` for local, `0.0.0.0` for remote) | `127.0.0.1` |
| `-theme` | Default CSS theme (`github`, `simple`, `dark`) | `github` |
| `-theme` | Default CSS theme name (any file in the themes directory) | `github` |
| `-plantuml-server` | PlantUML server URL | (disabled) |
| `-auth-token` | Authentication token (required in remote mode) | |
| `-auth-cookie-max-age` | Authentication cookie max age in days | `30` |
Expand Down Expand Up @@ -198,6 +198,49 @@ The configuration file stores these settings as JSON:

Command-line flags override configuration file values. Security-sensitive settings (`-auth-token`, `-access-log`, etc.) are not stored in the config file.

## CSS Themes

markdown-proxy supports user-defined CSS themes in addition to the three built-in themes.

### Theme Directory

| Platform | Path |
|----------|------|
| Linux | `~/.config/markdown-proxy/themes/` |
| Windows | `%APPDATA%/markdown-proxy/themes/` |

On first launch, the three built-in themes are written here as editable CSS files:

- `github.css` — GitHub-style light theme
- `simple.css` — Serif light theme
- `dark.css` — Dark theme

### Adding a Custom Theme

1. Create a `.css` file in the themes directory, e.g. `~/.config/markdown-proxy/themes/mycolor.css`
2. Write standard CSS (no special scoping required — the file is loaded as the sole active stylesheet)
3. Restart markdown-proxy (or simply open a new page — the theme list is read at startup)
4. Select `mycolor` from the Theme dropdown

**Example:**

```css
body { font-family: "Source Serif Pro", serif; color: #1a1a1a; background: #fffff8; }
.toolbar { background: #f0ece0; border-color: #c8bfa0; }
.home-link, .toolbar-link { color: #8b4513; }
.markdown-body a { color: #8b4513; }
.markdown-body pre { background: #f5f0e8; border: 1px solid #c8bfa0; overflow: auto; }
.markdown-body pre code { background: none; padding: 0; font-size: 100%; }
.copy-btn { background: #fff; color: #444; border-color: #c8bfa0; }
.copy-btn:hover { background: #f0ece0; }
.copy-btn.copied { background: #d4edda; color: #155724; border-color: #c3e6cb; }
.toc-panel { background: #f5f0e8; border-left-color: #c8bfa0; }
.toc-header { border-bottom-color: #c8bfa0; }
.toc-list a.active { border-left-color: #8b4513; background: rgba(139,69,19,0.08); }
```

You can also edit the built-in themes directly — changes take effect on the next request (the file is read from disk on each theme CSS request).

## Operation Modes

### Local Mode (default)
Expand Down Expand Up @@ -396,5 +439,6 @@ internal/
markdown/ - Markdown→HTML conversion, link rewriting, code block processing
credential/ - git credential helper integration
github/ - GitHub/GitLab URL resolution
template/ - HTML templates and CSS themes
template/ - HTML templates and structural CSS
themes/ - Theme management (built-in CSS generation, file I/O)
```
10 changes: 7 additions & 3 deletions internal/handler/local.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,12 @@ import (
)

type LocalHandler struct {
cfg *config.Config
cfg *config.Config
themes []string
}

func NewLocalHandler(cfg *config.Config) *LocalHandler {
return &LocalHandler{cfg: cfg}
func NewLocalHandler(cfg *config.Config, themes []string) *LocalHandler {
return &LocalHandler{cfg: cfg, themes: themes}
}

func (h *LocalHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
Expand Down Expand Up @@ -82,6 +83,7 @@ func (h *LocalHandler) serveFile(w http.ResponseWriter, filePath string) {
Title: filepath.Base(filePath),
Content: template.HTML(htmlContent),
Theme: h.cfg.Theme,
Themes: h.themes,
WatchPath: filePath,
})
if err != nil {
Expand Down Expand Up @@ -118,6 +120,7 @@ func (h *LocalHandler) serveFile(w http.ResponseWriter, filePath string) {
Title: filepath.Base(filePath),
Content: template.HTML(htmlContent),
Theme: h.cfg.Theme,
Themes: h.themes,
WatchPath: filePath,
})
if err != nil {
Expand Down Expand Up @@ -178,6 +181,7 @@ func (h *LocalHandler) serveDirectory(w http.ResponseWriter, dirPath string) {
Path: dirPath,
Entries: dirEntries,
Theme: h.cfg.Theme,
Themes: h.themes,
WatchPath: dirPath,
})
if err != nil {
Expand Down
9 changes: 7 additions & 2 deletions internal/handler/remote.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,11 @@ import (
type RemoteHandler struct {
cfg *config.Config
client *http.Client
themes []string
}

func NewRemoteHandler(cfg *config.Config, client *http.Client) *RemoteHandler {
return &RemoteHandler{cfg: cfg, client: client}
func NewRemoteHandler(cfg *config.Config, client *http.Client, themes []string) *RemoteHandler {
return &RemoteHandler{cfg: cfg, client: client, themes: themes}
}

func (h *RemoteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
Expand Down Expand Up @@ -112,6 +113,7 @@ func (h *RemoteHandler) renderMarkdownResponse(w http.ResponseWriter, body []byt
Title: path.Base(remotePath) + " - README.md",
Content: template.HTML(htmlContent),
Theme: h.cfg.Theme,
Themes: h.themes,
SourceURL: scheme + "://" + remotePath,
})
if err != nil {
Expand All @@ -138,6 +140,7 @@ func (h *RemoteHandler) renderResponse(w http.ResponseWriter, body []byte, conte
Title: path.Base(remotePath),
Content: template.HTML(htmlContent),
Theme: h.cfg.Theme,
Themes: h.themes,
SourceURL: scheme + "://" + remotePath,
})
if err != nil {
Expand Down Expand Up @@ -172,6 +175,7 @@ func (h *RemoteHandler) renderResponse(w http.ResponseWriter, body []byte, conte
Title: path.Base(remotePath),
Content: template.HTML(htmlContent),
Theme: h.cfg.Theme,
Themes: h.themes,
SourceURL: scheme + "://" + remotePath,
})
if err != nil {
Expand Down Expand Up @@ -367,6 +371,7 @@ func (h *RemoteHandler) renderAuthError(w http.ResponseWriter, ae *authError) {
page, err := tmpl.RenderError(&tmpl.ErrorPageData{
Title: "Access Denied",
Theme: h.cfg.Theme,
Themes: h.themes,
Status: statusCode,
Message: message,
Hints: hints,
Expand Down
41 changes: 39 additions & 2 deletions internal/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,25 +7,62 @@ import (
"log"
"net"
"net/http"
"net/url"
"os"
"strings"
"time"

"github.com/patakuti/markdown-proxy/internal/config"
"github.com/patakuti/markdown-proxy/internal/handler"
"github.com/patakuti/markdown-proxy/internal/network"
"github.com/patakuti/markdown-proxy/internal/themes"
"gopkg.in/natefinch/lumberjack.v2"
)

func Run(cfg *config.Config) error {
// Set up themes directory and load available themes.
themesDir, err := themes.DefaultThemesDir()
if err != nil {
log.Printf("Warning: could not determine themes directory: %v", err)
} else {
if err := themes.EnsureBuiltinThemes(themesDir); err != nil {
log.Printf("Warning: could not write built-in themes: %v", err)
}
}
themeList := themes.ListThemes(themesDir)

mux := http.NewServeMux()

// Serve theme CSS files from the themes directory.
mux.HandleFunc("/_theme/", func(w http.ResponseWriter, r *http.Request) {
// Use url.PathUnescape and path.Base equivalent for safety.
raw := strings.TrimPrefix(r.URL.Path, "/_theme/")
name, _ := url.PathUnescape(raw)
name = strings.TrimSuffix(name, ".css")
// Only allow names without path separators or dots.
if strings.ContainsAny(name, "/\\.") || name == "" {
http.NotFound(w, r)
return
}
data, readErr := themes.ReadTheme(themesDir, name)
if readErr != nil {
// Fall back to in-memory built-in.
data = themes.BuiltinCSS(name)
}
if data == nil {
http.NotFound(w, r)
return
}
w.Header().Set("Content-Type", "text/css; charset=utf-8")
w.Write(data)
})

// In local mode, allow private network access (user is local).
// In remote mode, block private network access (SSRF prevention).
client := network.NewSafeClient(!cfg.IsRemoteMode())

topHandler := handler.NewTopHandler(cfg)
remoteHandler := handler.NewRemoteHandler(cfg, client)
remoteHandler := handler.NewRemoteHandler(cfg, client, themeList)

mux.HandleFunc("/", topHandler.ServeHTTP)
mux.HandleFunc("/http/", remoteHandler.ServeHTTP)
Expand All @@ -44,7 +81,7 @@ func Run(cfg *config.Config) error {
mux.HandleFunc("/_login", loginHandler.ServeHTTP)
} else {
// In local mode, enable local file access and SSE
localHandler := handler.NewLocalHandler(cfg)
localHandler := handler.NewLocalHandler(cfg, themeList)
sseHandler := handler.NewSSEHandler()
mux.HandleFunc("/local/", localHandler.ServeHTTP)
mux.HandleFunc("/_sse", sseHandler.ServeHTTP)
Expand Down
31 changes: 0 additions & 31 deletions internal/template/copybutton.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,37 +24,6 @@ const copyButtonCSS = `
.copy-btn.copied {
opacity: 1;
}
.theme-github .copy-btn,
.theme-simple .copy-btn {
background: #fff;
color: #444;
border-color: #d0d7de;
}
.theme-github .copy-btn:hover,
.theme-simple .copy-btn:hover {
background: #f3f4f6;
border-color: #adb5bd;
}
.theme-github .copy-btn.copied,
.theme-simple .copy-btn.copied {
background: #d4edda;
color: #155724;
border-color: #c3e6cb;
}
.theme-dark .copy-btn {
background: #21262d;
color: #c9d1d9;
border-color: #30363d;
}
.theme-dark .copy-btn:hover {
background: #2d333b;
border-color: #6e7681;
}
.theme-dark .copy-btn.copied {
background: #1a3d2b;
color: #3fb950;
border-color: #2ea043;
}
@media print {
.copy-btn { display: none !important; }
}
Expand Down
Loading
Loading