nirvash/archetype/eureka.go

471 lines
11 KiB
Go

package archetype
import (
"errors"
"fmt"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
)
type EurekaAdapter struct {
Root string
Config map[ConfigOption]string
}
func (self *EurekaAdapter) Init(cfg *Config) {
fileInfo, err := os.Stat(cfg.Root)
if os.IsNotExist(err) {
panic("SSG content root does not exist! Ensure your configs are correct or create it!")
} else if !fileInfo.IsDir() {
panic("SSG content root is not a directory!")
}
self.Root = cfg.Root
self.Config = make(map[ConfigOption]string)
err = self.readCfg()
if err != nil {
fmt.Printf(err.Error())
panic("config.h is malformed!")
}
}
func (self *EurekaAdapter) Name() string {
return "eureka"
}
func (self *EurekaAdapter) EditableSlugs() bool {
return false
}
func (self *EurekaAdapter) BuildOptions() []BuildOption {
return []BuildOption{
BuildOption{
Name: "twtxt",
Type: "string",
},
BuildOption{
Name: "remove newest twtxt",
Type: "bool",
},
BuildOption{
Name: "clear thumbnail cache",
Type: "bool",
},
}
}
func (self *EurekaAdapter) GetConfig() map[ConfigOption]string {
return self.Config
}
func (self *EurekaAdapter) SetConfig(cfg map[ConfigOption]string) error {
self.Config = cfg
return self.writeCfg()
}
func (self *EurekaAdapter) ListPages() map[string]string {
files, err := ioutil.ReadDir(
filepath.Join(self.Root, "inc"))
if err != nil {
panic(err.Error())
}
pages := map[string]string{}
pages["meta.nav.htm"] = "NAVIGATION"
for _, file := range files {
filename := file.Name()
if strings.HasSuffix(filename, ".htm") && filename != "meta.nav.htm" {
pages[filename] = strings.Replace(
strings.TrimSuffix(filename, ".htm"), "_", " ", -1)
}
}
return pages
}
func (self *EurekaAdapter) GetPage(filename string) Page {
fullPath := filepath.Join(self.Root, "inc", filename)
if !strings.HasPrefix(filepath.Clean(fullPath), self.Root) {
return Page{
Error: "You cannot escape!",
}
}
f, err := os.ReadFile(fullPath)
if err != nil {
return Page{
Error: err.Error(),
}
}
if !strings.HasSuffix(filename, ".htm") {
return Page{
Error: "Page file extension is not '.htm'",
}
}
title := strings.Replace(
strings.TrimSuffix(filename, ".htm"), "_", " ", -1)
fileInfo, _ := os.Stat(fullPath)
content := string(f[:])
return Page{
Title: title,
Content: strings.ReplaceAll(content, "\r", ""),
Edited: fileInfo.ModTime(),
}
}
func (self *EurekaAdapter) FormatPage(raw string) string {
// TODO: implement Eureka formatter to show preview
return raw
}
func (self *EurekaAdapter) FormattingHelp() string {
// TODO: show Eureka formatting guide
return `// shorthand for linking other pages
{page name}
// shorthand for page transclusion
{/page name}
// shorthand for arbitary link
{*destination url|text}
// shorthand for an image you can click to see the full sized version
{:anchor-id|image url|alt text}
// shorthand for an image with arbitrary link destination
{?anchor-id|destination url|image url|alt text}
// shorthand for an audio player
{_/path/to/media}
// shorthand for paragraphs, can embed other markup inside it
{&paragraph text {with a link} {@and some bold text}}
// shorthand for ordered lists, can embed other markup inside it
{#
{-item one}
{-item two}
{-item three}
}
// shorthand for unordered lists, can embed other markup inside it
{,
{-item one}
{-item two}
{-item three}
}
// shorthand for bold
{@bold text}
// shorthand for italic
{~italic text}
// shorthand for code
{` + "`" + `short code}
// shorthand for pre
{$longer code}
// shorthand for quote
{'short quote}
// shorthand for blockquote
{>longer quote}
// shorthand for strikethrough
{\crossed-out text}
// shorthand for level 3 heading
{!heading text}
// shorthand for level 4 heading
{.heading text}
// shorthand for publish date (renders as <time class='publish-date'>)
{+2022-02-22}
// shorthand for tables
{[column 1 header|column 2 header|column 3 header}
{|simple data|{@bold data}|{*https://nilfm.cc|link data}}
{|more|and more|and more data!}
{;}
// shorthand for publish date (renders as <time class='publish-date'>)
{+2022-02-22}
// shorthand for explicit monospace, sans, and serif font respecively:
{=some mono text}
{(some sans serif text}
{)some serif text}
// shorthand for a pictured directory/list item
{%page name|picture}
// shorthand for a shop listing
{^payment-link|description|img1|alttext1|...|imgn|alttextn}`
}
func (self *EurekaAdapter) CreatePage(slug, title, content string) error {
// eureka creates titles from slugs, so we transform the title into the slug
slug = strings.ToLower(strings.ReplaceAll(title, " ", "_")) + ".htm"
path := filepath.Join(self.Root, "inc", slug)
if !strings.HasPrefix(filepath.Clean(path), self.Root) {
errors.New("You cannot escape!")
}
_, err := os.Stat(path)
if err == nil || !os.IsNotExist(err) {
return errors.New("File already exists")
}
f, err := os.Create(path)
if err != nil {
return err
}
defer f.Close()
f.WriteString(strings.ReplaceAll(content, "\r", ""))
return nil
}
func (self *EurekaAdapter) SavePage(oldSlug, newSlug, title, content string) error {
// eureka creates titles from slugs, so we transform the title into the slug
newSlug = strings.ToLower(strings.ReplaceAll(title, " ", "_")) + ".htm"
oldPath := filepath.Join(self.Root, "inc", oldSlug)
newPath := filepath.Join(self.Root, "inc", newSlug)
if !strings.HasPrefix(filepath.Clean(oldPath), filepath.Join(self.Root, "inc")) ||
!strings.HasPrefix(filepath.Clean(newPath), filepath.Join(self.Root, "inc")) {
return errors.New("You cannot escape!")
}
f, err := os.Create(filepath.Join(self.Root, "inc", newSlug))
if err != nil {
return err
}
defer f.Close()
if oldSlug != newSlug {
siteRoot := self.Config[ConfigOption{
Name: "SITEROOT",
Type: "string",
}]
htmlFile := filepath.Join(self.Root, siteRoot, oldSlug+"l")
_, err := os.Stat(htmlFile)
if !os.IsNotExist(err) {
os.Remove(htmlFile)
}
os.Remove(filepath.Join(self.Root, "inc", oldSlug))
}
f.WriteString(strings.ReplaceAll(content, "\r", ""))
return nil
}
func (self *EurekaAdapter) DeletePage(slug string) error {
siteRoot := self.Config[ConfigOption{
Name: "SITEROOT",
Type: "string",
}]
htmlFile := filepath.Join(self.Root, siteRoot, slug+"l")
if !strings.HasPrefix(filepath.Clean(htmlFile), filepath.Join(self.Root, siteRoot)) {
return errors.New("You cannot escape!")
}
_, err := os.Stat(htmlFile)
if !os.IsNotExist(err) {
os.Remove(htmlFile)
}
return os.Remove(filepath.Join(self.Root, "inc", slug))
}
func (self *EurekaAdapter) Build(buildOptions map[BuildOption]string) BuildStatus {
twtxt := buildOptions[BuildOption{
Name: "twtxt",
Type: "string",
}]
rmNewestTwtxt := buildOptions[BuildOption{
Name: "remove newest twtxt",
Type: "bool",
}]
clearThumbs := buildOptions[BuildOption{
Name: "clear thumbnail cache",
Type: "bool",
}]
cmdArgs := []string{}
if clearThumbs != "" {
cmdArgs = append(cmdArgs, "-r")
}
if rmNewestTwtxt != "" {
cmdArgs = append(cmdArgs, "-d")
}
if twtxt != "" {
cmdArgs = append(cmdArgs, "-t")
cmdArgs = append(cmdArgs, twtxt)
}
cmd := exec.Command("./build.sh", cmdArgs...)
cmd.Dir = self.Root
out, err := cmd.CombinedOutput()
return BuildStatus{
Success: err == nil,
Message: string(out),
}
}
func (self *EurekaAdapter) Deploy() DeployStatus {
cmd := exec.Command("./deploy.sh")
cmd.Dir = self.Root
out, err := cmd.CombinedOutput()
return DeployStatus{
Success: err == nil,
Message: string(out),
}
}
func (self *EurekaAdapter) Revert() RevertStatus {
cmd := exec.Command("./deploy.sh", "--resync")
cmd.Dir = self.Root
out, err := cmd.CombinedOutput()
return RevertStatus{
Success: err == nil,
Message: string(out),
}
}
func (self *EurekaAdapter) readCfg() error {
configPath := filepath.Join(self.Root, "config.h")
_, err := os.Stat(filepath.Join(self.Root, "config.h"))
if os.IsNotExist(err) {
configPath = filepath.Join(self.Root, "config.def.h")
}
f, err := os.ReadFile(configPath)
if err != nil {
return err
}
fileData := strings.Replace(
strings.Replace(
string(f[:]), "/* clang-format on */", "", -1),
"/* clang-format off */", "", -1)
macros := strings.Split(fileData, "#define ")[1:]
for _, macro := range macros {
tokens := strings.Split(strings.TrimSpace(macro), " ")
k := tokens[0]
v := strings.TrimSpace(strings.Join(tokens[1:], " "))
if strings.Contains(v, "\"") {
if strings.HasSuffix(k, "_HTML") {
// process multiline string
lines := strings.Split(v, "\n")
cleanedString := ""
for _, l := range lines {
l = strings.TrimSuffix(l, "\r")
l = strings.TrimSuffix(l, "\\")
l = strings.TrimSpace(l)
l = strings.TrimPrefix(l, "\"")
l = strings.TrimSuffix(l, "\"")
l = strings.ReplaceAll(l, "\\\"", "\"")
l = strings.ReplaceAll(l, "\\n", "\n")
cleanedString += l
}
self.Config[ConfigOption{
Name: k,
Type: "multilinestring",
}] = cleanedString
} else {
cleanedString := strings.TrimSuffix(strings.TrimPrefix(v, "\""), "\"")
cleanedString = strings.ReplaceAll(cleanedString, "\\n", "\n")
cleanedString = strings.ReplaceAll(cleanedString, "\r", "")
cleanedString = strings.ReplaceAll(cleanedString, "\\\"", "\"")
self.Config[ConfigOption{
Name: k,
Type: "string",
Hidden: strings.HasPrefix(k, "DEPLOY") || k == "SITEROOT",
}] = cleanedString
}
} else if strings.Contains(v, ".") {
_, err := strconv.ParseFloat(v, 64)
if err != nil {
return err
}
self.Config[ConfigOption{
Name: k,
Type: "float",
}] = v
} else {
_, err := strconv.ParseInt(v, 10, 64)
if err != nil {
return err
}
cfgType := "int"
if strings.HasPrefix(k, "IS_") {
cfgType = "bool"
}
self.Config[ConfigOption{
Name: k,
Type: cfgType,
}] = v
}
}
return nil
}
func (self *EurekaAdapter) writeCfg() error {
f, err := os.Create(filepath.Join(self.Root, "config.h"))
if err != nil {
return err
}
defer f.Close()
f.WriteString("/* clang-format off */\n")
for k, v := range self.Config {
switch k.Type {
case "int":
fallthrough
case "bool":
_, err := strconv.ParseInt(v, 10, 64)
if err != nil {
return err
}
f.WriteString("#define " + k.Name + " " + v + "\n")
case "float":
_, err := strconv.ParseFloat(v, 64)
if err != nil {
return err
}
f.WriteString("#define " + k.Name + " " + v + "\n")
case "string":
fallthrough
case "multilinestring":
v = strings.ReplaceAll(v, "\"", "\\\"")
v = strings.ReplaceAll(v, "\n", "\\n\" \\\n\"")
v = strings.ReplaceAll(v, "\r", "")
f.WriteString("#define " + k.Name + " \"" + v + "\"\n")
default:
fmt.Println("Unsupported config value type: " + k.Type)
}
}
f.WriteString("/* clang-format on */\n")
return nil
}