Refactor: Isolata in un pacchetto autonomo la struct che gestisce il registro degli articoli

main
Antonio De Lucreziis 2 years ago
parent 447e87b691
commit 280c3aaef4

@ -1,204 +0,0 @@
package main
import (
"bytes"
"fmt"
"os"
"path"
"sort"
"strings"
"time"
"github.com/yuin/goldmark"
"github.com/yuin/goldmark/extension"
"github.com/yuin/goldmark/parser"
"github.com/yuin/goldmark/renderer/html"
"github.com/yuin/goldmark/util"
chromahtml "github.com/alecthomas/chroma/formatters/html"
mathjax "github.com/litao91/goldmark-mathjax"
highlighting "github.com/yuin/goldmark-highlighting"
"gopkg.in/yaml.v3"
)
var md goldmark.Markdown
// https://github.com/yuin/goldmark-highlighting/blob/9216f9c5aa010c549cc9fc92bb2593ab299f90d4/highlighting_test.go#L27
func customCodeBlockWrapper(w util.BufWriter, c highlighting.CodeBlockContext, entering bool) {
lang, ok := c.Language()
if entering {
if ok {
w.WriteString(fmt.Sprintf(`<pre class="%s"><code>`, lang))
return
}
w.WriteString("<pre><code>")
} else {
if ok {
w.WriteString("</pre></code>")
return
}
w.WriteString("</pre></code>")
}
}
func init() {
md = goldmark.New(
goldmark.WithExtensions(
extension.GFM,
extension.Typographer,
// Questo pacchetto ha un nome stupido perché in realtà si occupa solo del parsing lato server del Markdown mentre lato client usiamo KaTeX.
mathjax.NewMathJax(),
highlighting.NewHighlighting(
highlighting.WithStyle("github"),
highlighting.WithWrapperRenderer(customCodeBlockWrapper),
highlighting.WithFormatOptions(
chromahtml.PreventSurroundingPre(true),
),
),
),
goldmark.WithParserOptions(
parser.WithAutoHeadingID(),
),
goldmark.WithRendererOptions(
html.WithHardWraps(),
html.WithXHTML(),
),
)
}
type ArticleFrontMatter struct {
Title string `yaml:"title"`
Description string `yaml:"description"`
Tags string `yaml:"tags"`
PublishDate string `yaml:"publish_date"`
}
type Article struct {
Id string
Title string
Description string
Tags []string
PublishDate time.Time
MarkdownSource string
renderedHTML string
}
func (article *Article) HasTag(tag string) bool {
for _, t := range article.Tags {
if t == tag {
return true
}
}
return false
}
func (article *Article) Render() (string, error) {
if article.renderedHTML == "" {
var buf bytes.Buffer
if err := md.Convert([]byte(article.MarkdownSource), &buf); err != nil {
return "", err
}
article.renderedHTML = buf.String()
}
return article.renderedHTML, nil
}
type ArticleRenderer struct {
RootPath string
}
func trimAll(vs []string) []string {
r := []string{}
for _, v := range vs {
r = append(r, strings.TrimSpace(v))
}
return r
}
func removeBlanks(v []string) []string {
r := []string{}
for _, s := range v {
if len(strings.TrimSpace(s)) > 0 {
r = append(r, s)
}
}
return r
}
func (registry *ArticleRenderer) Load(articlePath string) (*Article, error) {
fileBytes, err := os.ReadFile(path.Join(registry.RootPath, articlePath+".md"))
if err != nil {
return nil, err
}
source := string(fileBytes)
// TODO: Ehm bugia esiste "https://github.com/yuin/goldmark-meta" però boh per ora funge tutto
parts := removeBlanks(strings.Split(source, "---"))
frontMatterSource := parts[0]
markdownSource := strings.Join(parts[1:], "---")
var frontMatter ArticleFrontMatter
if err := yaml.Unmarshal([]byte(frontMatterSource), &frontMatter); err != nil {
return nil, err
}
publishDate, err := time.Parse("2006/01/02 15:04", frontMatter.PublishDate)
if err != nil {
return nil, err
}
tags := trimAll(strings.Split(frontMatter.Tags, ","))
article := &Article{
Id: articlePath,
Title: frontMatter.Title,
Description: frontMatter.Description,
Tags: tags,
PublishDate: publishDate,
MarkdownSource: markdownSource,
}
return article, nil
}
func (registry *ArticleRenderer) LoadAll() ([]*Article, error) {
entries, err := os.ReadDir(registry.RootPath)
if err != nil {
return nil, err
}
articles := []*Article{}
for _, entry := range entries {
if !entry.IsDir() {
name := strings.TrimRight(entry.Name(), ".md")
article, err := registry.Load(name)
if err != nil {
return nil, err
}
articles = append(articles, article)
}
}
sort.Slice(articles, func(i, j int) bool {
return articles[i].PublishDate.After(articles[j].PublishDate)
})
return articles, nil
}
func NewArticleRenderer(rootPath string) *ArticleRenderer {
return &ArticleRenderer{
rootPath,
}
}

@ -0,0 +1,113 @@
package articles
import (
"bytes"
"os"
"strings"
"time"
"github.com/phc-dm/phc-server/config"
"gopkg.in/yaml.v3"
)
type Article struct {
Id string
Title string
Description string
Tags []string
PublishDate time.Time
ArticlePath string
markdownSource string
renderedHTML string
}
func (article *Article) HasTag(tag string) bool {
for _, t := range article.Tags {
if t == tag {
return true
}
}
return false
}
func NewArticle(articlePath string) (*Article, error) {
article := &Article{
ArticlePath: articlePath,
}
err := article.load()
if err != nil {
return nil, err
}
return article, nil
}
func trimAll(vs []string) []string {
r := []string{}
for _, v := range vs {
r = append(r, strings.TrimSpace(v))
}
return r
}
func (article *Article) load() error {
fileBytes, err := os.ReadFile(article.ArticlePath)
if err != nil {
return err
}
source := string(fileBytes)
// TODO: Ehm bugia pare che esista "https://github.com/yuin/goldmark-meta" però non penso valga la pena aggiungerlo
parts := strings.SplitN(source, "---", 3)[1:]
frontMatterSource := parts[0]
markdownSource := parts[1]
var frontMatter struct {
Id string `yaml:"id"`
Title string `yaml:"title"`
Description string `yaml:"description"`
Tags string `yaml:"tags"`
PublishDate string `yaml:"publish_date"`
}
if err := yaml.Unmarshal([]byte(frontMatterSource), &frontMatter); err != nil {
return err
}
publishDate, err := time.Parse("2006/01/02 15:04", frontMatter.PublishDate)
if err != nil {
return err
}
article.Id = frontMatter.Id
article.Title = frontMatter.Title
article.Description = frontMatter.Description
article.Tags = trimAll(strings.Split(frontMatter.Tags, ","))
article.PublishDate = publishDate
article.markdownSource = markdownSource
article.renderedHTML = ""
return nil
}
func (article *Article) Render() (string, error) {
if config.Mode == "development" {
article.load()
}
if article.renderedHTML == "" {
var buf bytes.Buffer
if err := articleMarkdown.Convert([]byte(article.markdownSource), &buf); err != nil {
return "", err
}
article.renderedHTML = buf.String()
}
return article.renderedHTML, nil
}

@ -0,0 +1,77 @@
package articles
import (
"fmt"
"os"
"path"
"sort"
)
type Registry struct {
RootPath string
ArticleCache map[string]*Article
}
func NewRegistry(rootPath string) *Registry {
return &Registry{
rootPath,
map[string]*Article{},
}
}
func (registry *Registry) loadArticles() error {
entries, err := os.ReadDir(registry.RootPath)
if err != nil {
return err
}
for _, entry := range entries {
if !entry.IsDir() {
article, err := NewArticle(path.Join(registry.RootPath, entry.Name()))
if err != nil {
return err
}
registry.ArticleCache[article.Id] = article
}
}
return nil
}
func (registry *Registry) GetArticle(id string) (*Article, error) {
article, present := registry.ArticleCache[id]
if !present {
err := registry.loadArticles()
if err != nil {
return nil, err
}
article, present := registry.ArticleCache[id]
if !present {
return nil, fmt.Errorf(`no article with id "%s"`, id)
}
return article, nil
}
return article, nil
}
func (registry *Registry) GetArticles() ([]*Article, error) {
err := registry.loadArticles()
if err != nil {
return nil, err
}
articles := []*Article{}
for _, article := range registry.ArticleCache {
articles = append(articles, article)
}
sort.Slice(articles, func(i, j int) bool {
return articles[i].PublishDate.After(articles[j].PublishDate)
})
return articles, nil
}

@ -0,0 +1,60 @@
package articles
import (
"fmt"
"github.com/yuin/goldmark"
"github.com/yuin/goldmark/extension"
"github.com/yuin/goldmark/parser"
"github.com/yuin/goldmark/renderer/html"
"github.com/yuin/goldmark/util"
chromahtml "github.com/alecthomas/chroma/formatters/html"
mathjax "github.com/litao91/goldmark-mathjax"
highlighting "github.com/yuin/goldmark-highlighting"
)
var articleMarkdown goldmark.Markdown
// https://github.com/yuin/goldmark-highlighting/blob/9216f9c5aa010c549cc9fc92bb2593ab299f90d4/highlighting_test.go#L27
func customCodeBlockWrapper(w util.BufWriter, c highlighting.CodeBlockContext, entering bool) {
lang, ok := c.Language()
if entering {
if ok {
w.WriteString(fmt.Sprintf(`<pre class="%s"><code>`, lang))
return
}
w.WriteString("<pre><code>")
} else {
if ok {
w.WriteString("</pre></code>")
return
}
w.WriteString("</pre></code>")
}
}
func init() {
articleMarkdown = goldmark.New(
goldmark.WithExtensions(
extension.GFM,
extension.Typographer,
// Questo pacchetto ha un nome stupido perché in realtà si occupa solo del parsing lato server del Markdown mentre lato client usiamo KaTeX.
mathjax.NewMathJax(),
highlighting.NewHighlighting(
highlighting.WithStyle("github"),
highlighting.WithWrapperRenderer(customCodeBlockWrapper),
highlighting.WithFormatOptions(
chromahtml.PreventSurroundingPre(true),
),
),
),
goldmark.WithParserOptions(
parser.WithAutoHeadingID(),
),
goldmark.WithRendererOptions(
html.WithHardWraps(),
html.WithXHTML(),
),
)
}

@ -7,6 +7,7 @@ import (
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
"github.com/phc-dm/phc-server/articles"
"github.com/phc-dm/phc-server/config"
"github.com/phc-dm/phc-server/templates"
"github.com/phc-dm/phc-server/util"
@ -31,7 +32,7 @@ func main() {
templates.File("./views/base.html"),
templates.Pattern("./views/partials/*.html"),
)
newsArticlesRegistry := NewArticleRenderer("./news")
newsArticlesRegistry := articles.NewRegistry("./news")
// Routes
@ -71,7 +72,7 @@ func main() {
})
r.Get("/news", func(w http.ResponseWriter, r *http.Request) {
articles, err := newsArticlesRegistry.LoadAll()
articles, err := newsArticlesRegistry.GetArticles()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
@ -86,9 +87,9 @@ func main() {
})
r.Get("/news/{article}", func(w http.ResponseWriter, r *http.Request) {
articleName := chi.URLParam(r, "article")
articleID := chi.URLParam(r, "article")
article, err := newsArticlesRegistry.Load(articleName)
article, err := newsArticlesRegistry.GetArticle(articleID)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return

@ -1,4 +1,5 @@
---
id: 2021-12-22-notizia-1
title: "Notizia 1"
tags: important, prova, test, foo, bar
publish_date: 2021/12/22 22:00

@ -1,4 +1,5 @@
---
id: 2021-12-23-notizia-2
title: "Notizia 2"
tags: prova, test, foo, bar
publish_date: 2021/12/23 22:00

@ -1,4 +1,5 @@
---
id: 2021-12-24-notizia-3
title: "Notizia 3"
tags: prova, test, foo, bar
publish_date: 2021/12/24 18:00

Loading…
Cancel
Save