diff --git a/docs/configuration.md b/docs/configuration.md index c42b04d9a..245060181 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -151,14 +151,14 @@ pages: columns: - size: full widgets: - $include: rss.yml + - $include: rss.yml - name: News columns: - size: full widgets: - type: group widgets: - $include: rss.yml + - $include: rss.yml - type: reddit subreddit: news ``` diff --git a/go.mod b/go.mod index d59c9e6c9..77bc62222 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.24.4 require ( github.com/fsnotify/fsnotify v1.9.0 + github.com/gomarkdown/markdown v0.0.0-20260217112301-37c66b85d6ab github.com/mmcdole/gofeed v1.3.0 github.com/shirou/gopsutil/v4 v4.25.5 github.com/tidwall/gjson v1.18.0 diff --git a/go.sum b/go.sum index 56ae190db..392e9652f 100644 --- a/go.sum +++ b/go.sum @@ -12,6 +12,8 @@ github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8 github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= +github.com/gomarkdown/markdown v0.0.0-20260217112301-37c66b85d6ab h1:VYNivV7P8IRHUam2swVUNkhIdp0LRRFKe4hXNnoZKTc= +github.com/gomarkdown/markdown v0.0.0-20260217112301-37c66b85d6ab/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= diff --git a/internal/glance/static/css/widget-markdown.css b/internal/glance/static/css/widget-markdown.css new file mode 100644 index 000000000..d8a00ef35 --- /dev/null +++ b/internal/glance/static/css/widget-markdown.css @@ -0,0 +1,27 @@ +.widget-markdown h1 { + font-size: 1.875rem; +} + +.widget-markdown h2 { + font-size: 1.5rem; +} + +.widget-markdown h3 { + font-size: 1.25rem; +} + +.widget-markdown h4 { + font-size: 1.125rem; +} + +.widget-markdown h5 { + font-size: 1rem; +} + +.widget-markdown h6 { + font-size: 0.875rem; +} + +.widget-markdown a { + text-decoration: underline; +} \ No newline at end of file diff --git a/internal/glance/static/css/widgets.css b/internal/glance/static/css/widgets.css index 3c231c49f..e8d0677ca 100644 --- a/internal/glance/static/css/widgets.css +++ b/internal/glance/static/css/widgets.css @@ -13,6 +13,8 @@ @import "widget-videos.css"; @import "widget-weather.css"; @import "widget-todo.css"; +@import "widget-markdown.css"; + @import "forum-posts.css"; .widget-error-header { diff --git a/internal/glance/templates/markdown.html b/internal/glance/templates/markdown.html new file mode 100644 index 000000000..b1d481877 --- /dev/null +++ b/internal/glance/templates/markdown.html @@ -0,0 +1,7 @@ +{{ template "widget-base.html" . }} + +{{ define "widget-content" }} +
+{{ end }} diff --git a/internal/glance/widget-markdown.go b/internal/glance/widget-markdown.go new file mode 100644 index 000000000..681b95821 --- /dev/null +++ b/internal/glance/widget-markdown.go @@ -0,0 +1,62 @@ +package glance + +import ( + "fmt" + "html/template" + "os" + + "github.com/gomarkdown/markdown" + "github.com/gomarkdown/markdown/html" + "github.com/gomarkdown/markdown/parser" +) + +var markdownWidgetTemplate = mustParseTemplate("markdown.html", "widget-base.html") + +type markdownWidget struct { + widgetBase `yaml:",inline"` + CompiledHTML template.HTML `yaml:"-"` + Source string `yaml:"source"` + File string `yaml:"file"` +} + +func (widget *markdownWidget) initialize() error { + widget.WIP = true + + if widget.Title == "" { + widget.Title = "Markdown" + } + + widget.withTitle(widget.Title).withError(nil) + + var content string + + if widget.File != "" { + fileContent, err := os.ReadFile(widget.File) + if err != nil { + return fmt.Errorf("failed to read markdown file: %w", err) + } + content = string(fileContent) + } else if widget.Source != "" { + content = widget.Source + } else { + return fmt.Errorf("either 'source' or 'file' must be specified") + } + + // Parse markdown with standard extensions + extensions := parser.CommonExtensions | parser.AutoHeadingIDs + p := parser.NewWithExtensions(extensions) + doc := p.Parse([]byte(content)) + + // Render to HTML + renderer := html.NewRenderer(html.RendererOptions{ + Flags: html.CommonFlags | html.HrefTargetBlank, + }) + + widget.CompiledHTML = template.HTML(markdown.Render(doc, renderer)) + + return nil +} + +func (widget *markdownWidget) Render() template.HTML { + return widget.renderTemplate(widget, markdownWidgetTemplate) +} diff --git a/internal/glance/widget.go b/internal/glance/widget.go index 832ba2177..6758132b6 100644 --- a/internal/glance/widget.go +++ b/internal/glance/widget.go @@ -39,6 +39,8 @@ func newWidget(widgetType string) (widget, error) { w = &iframeWidget{} case "html": w = &htmlWidget{} + case "markdown": + w = &markdownWidget{} case "hacker-news": w = &hackerNewsWidget{} case "releases":