add parser for blog articles
Signed-off-by: Tobias Erbshäußer <tobias@tesoft.dev>
This commit is contained in:
+151
@@ -0,0 +1,151 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"backend/md"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type ArticleStatus = int
|
||||
|
||||
const (
|
||||
ArticleStatusDraft ArticleStatus = iota
|
||||
ArticleStatusPublished
|
||||
ArticleStatusOffline
|
||||
)
|
||||
|
||||
type ArticleProperties struct {
|
||||
Id int64 `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Status ArticleStatus `json:"status"`
|
||||
Tags []string `json:"tags"`
|
||||
ReleaseDate time.Time `json:"date"`
|
||||
ModificationDate *time.Time `json:"mod-date"`
|
||||
}
|
||||
|
||||
type Article struct {
|
||||
ArticleProperties
|
||||
Content string
|
||||
Files []ArticleFile
|
||||
}
|
||||
|
||||
type ArticleFile struct {
|
||||
Id int
|
||||
Data []byte
|
||||
}
|
||||
|
||||
func ParseArticle(reader io.Reader, filePrefix string) (*Article, error) {
|
||||
tarFiles := make(map[string][]byte)
|
||||
tarReader := tar.NewReader(reader)
|
||||
for {
|
||||
header, err := tarReader.Next()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
content, err := io.ReadAll(tarReader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tarFiles[header.Name] = content
|
||||
}
|
||||
|
||||
readmeBytes, found := tarFiles["README.md"]
|
||||
if !found {
|
||||
return nil, errors.New("README.md not found")
|
||||
}
|
||||
|
||||
usedFiles := make(map[string]ArticleFile)
|
||||
nextFileId := 0
|
||||
|
||||
pc, html, err := md.Parse(func(path string) (string, error) {
|
||||
content, ok := tarFiles[strings.TrimPrefix(path, "./")]
|
||||
if !ok {
|
||||
return "", errors.New("file '" + path + "' not found")
|
||||
}
|
||||
|
||||
usedFiles[path] = ArticleFile{nextFileId, content}
|
||||
nextFileId++
|
||||
|
||||
return filePrefix + strconv.Itoa(nextFileId), nil
|
||||
}, readmeBytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
title, err := md.GetProperty(pc, "title")
|
||||
if err != nil {
|
||||
if errors.Is(err, md.PropertyNotFoundError) {
|
||||
return nil, errors.New("title property not found")
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tagsStr, err := md.GetProperty(pc, "tags")
|
||||
if err != nil && !errors.Is(err, md.PropertyNotFoundError) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tags := make([]string, 0)
|
||||
if strings.TrimSpace(tagsStr) != "" {
|
||||
for _, tagStr := range strings.Split(tagsStr, ",") {
|
||||
tags = append(tags, strings.TrimSpace(tagStr))
|
||||
}
|
||||
}
|
||||
|
||||
dateStr, err := md.GetProperty(pc, "date")
|
||||
if err != nil {
|
||||
if errors.Is(err, md.PropertyNotFoundError) {
|
||||
return nil, errors.New("date property not found")
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
releaseDate, err := time.Parse(time.DateOnly, dateStr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid date property '%s': %v", dateStr, err)
|
||||
}
|
||||
|
||||
var modificationDate *time.Time
|
||||
dateStr, err = md.GetProperty(pc, "mod-date")
|
||||
if err == nil {
|
||||
tmp, err := time.Parse(time.DateOnly, dateStr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid mod-date property '%s': %v", dateStr, err)
|
||||
}
|
||||
|
||||
modificationDate = &tmp
|
||||
} else if !errors.Is(err, md.PropertyNotFoundError) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
files := make([]ArticleFile, 0, len(usedFiles))
|
||||
for _, file := range usedFiles {
|
||||
files = append(files, file)
|
||||
}
|
||||
|
||||
return &Article{
|
||||
ArticleProperties{
|
||||
-1,
|
||||
title,
|
||||
ArticleStatusDraft,
|
||||
tags,
|
||||
releaseDate,
|
||||
modificationDate,
|
||||
},
|
||||
string(html),
|
||||
files,
|
||||
}, nil
|
||||
}
|
||||
Reference in New Issue
Block a user