Introduce blorg: MVP static site generator
hugo is nice - but it's huge. I've never built a static site generator before and thought the world could use one more - it's not like there's already enough choice out there! No, but seriously. I love hugo and it has all the bells and whistles and you should definitely use that and not this. I just like reinventing the wheel to learn about stuff - and I like the 80/20 rule. This gives like 60% of what I want already and is tiny fraction of hugo in terms of LOC (hugo without it's bazillion dependencies is like 80k+ - this is like 500 and very likely won't ever grow above let's say 5k). Also org mode is awesome and why not use it as a configuration format as well. Let's see where this goes. YOLO.
This commit is contained in:
parent
5e50794af0
commit
30dd2794cf
16 changed files with 760 additions and 33 deletions
26
README.org
26
README.org
|
@ -1,25 +1,23 @@
|
||||||
* go-org [[https://travis-ci.org/niklasfasching/go-org.svg?branch=master]]
|
* go-org [[https://travis-ci.org/niklasfasching/go-org.svg?branch=master]]
|
||||||
An Org mode parser in go.
|
An Org mode parser in go. And soon a blog generator.
|
||||||
Take a look at [[https://niklasfasching.github.io/go-org/][github pages]] for some examples and an online org -> html demo (wasm based).
|
Take a look at [[https://niklasfasching.github.io/go-org/][github pages]] for some examples and to try it out live in your browser.
|
||||||
[[https://raw.githubusercontent.com/niklasfasching/go-org/master/etc/example.png]]
|
[[https://raw.githubusercontent.com/niklasfasching/go-org/master/etc/example.png]]
|
||||||
Please note that the goal for the html export is to produce sensible html output, not to exactly reproduce output the output of =org-html-export=.
|
Please note
|
||||||
|
- the goal for the html export is to produce sensible html output, not to exactly reproduce the output of =org-html-export=.
|
||||||
|
- the goal for the parser is to support a reasonable subset of Org mode. Org mode is *huge* and I like to follow the 80/20 rule.
|
||||||
|
* usage
|
||||||
|
** command line
|
||||||
|
#+begin_src
|
||||||
|
go-org
|
||||||
|
#+end_src
|
||||||
|
** as a library
|
||||||
|
see [[https://github.com/niklasfasching/go-org/blob/master/main.go][main.go]] and hugo [[https://github.com/gohugoio/hugo/blob/master/markup/org/convert.go][org/convert.go]]
|
||||||
* development
|
* development
|
||||||
1. =make setup install=
|
1. =make setup install=
|
||||||
2. change things
|
2. change things
|
||||||
3. =make preview= (regenerates fixtures & shows output in a browser)
|
3. =make preview= (regenerates fixtures & shows output in a browser)
|
||||||
|
|
||||||
in general, have a look at the Makefile - it's short enough.
|
in general, have a look at the Makefile - it's short enough.
|
||||||
* not yet implemented
|
|
||||||
** deadlines and scheduling
|
|
||||||
see https://orgmode.org/manual/Deadlines-and-scheduling.html
|
|
||||||
** more types of links
|
|
||||||
see https://orgmode.org/manual/External-links.html & https://orgmode.org/manual/Internal-links.html
|
|
||||||
- radio target <<<MyTarget>>>
|
|
||||||
- link target: <<go-org>>
|
|
||||||
- link: [[go-org]]
|
|
||||||
- link to headline
|
|
||||||
- links with image as description
|
|
||||||
- MyTarget <- this will automatically become a link - not sure i want this...
|
|
||||||
* resources
|
* resources
|
||||||
- test files
|
- test files
|
||||||
- [[https://raw.githubusercontent.com/kaushalmodi/ox-hugo/master/test/site/content-org/all-posts.org][ox-hugo all-posts.org]]
|
- [[https://raw.githubusercontent.com/kaushalmodi/ox-hugo/master/test/site/content-org/all-posts.org][ox-hugo all-posts.org]]
|
||||||
|
|
239
blorg/config.go
Normal file
239
blorg/config.go
Normal file
|
@ -0,0 +1,239 @@
|
||||||
|
// blorg is a very minimal and broken static site generator. Don't use this. I initially wrote go-org to use Org mode in hugo
|
||||||
|
// and non crazy people should keep using hugo. I just like the idea of not having dependencies / following 80/20 rule. And blorg gives me what I need
|
||||||
|
// for a blog in a fraction of the LOC (hugo is a whooping 80k+ excluding dependencies - this will very likely stay <5k).
|
||||||
|
package blorg
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"html/template"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"path/filepath"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/niklasfasching/go-org/org"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
ConfigFile string
|
||||||
|
ContentDir string
|
||||||
|
PublicDir string
|
||||||
|
Address string
|
||||||
|
Template *template.Template
|
||||||
|
OrgConfig *org.Configuration
|
||||||
|
}
|
||||||
|
|
||||||
|
var DefaultConfigFile = "blorg.org"
|
||||||
|
|
||||||
|
var DefaultConfig = `
|
||||||
|
#+CONTENT: content
|
||||||
|
#+PUBLIC: public
|
||||||
|
|
||||||
|
* templates
|
||||||
|
** item
|
||||||
|
#+name: item
|
||||||
|
#+begin_src html
|
||||||
|
{{ . }}
|
||||||
|
#+end_src
|
||||||
|
|
||||||
|
** list
|
||||||
|
#+name: list
|
||||||
|
#+begin_src html
|
||||||
|
{{ . }}
|
||||||
|
#+end_src`
|
||||||
|
|
||||||
|
var TemplateFuncs = map[string]interface{}{
|
||||||
|
"Slugify": slugify,
|
||||||
|
}
|
||||||
|
|
||||||
|
func ReadConfig(configFile string) (*Config, error) {
|
||||||
|
address, publicDir, contentDir, workingDir := ":3000", "public", "content", filepath.Dir(configFile)
|
||||||
|
f, err := os.Open(configFile)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
orgConfig := org.New()
|
||||||
|
document := orgConfig.Parse(f, configFile)
|
||||||
|
if document.Error != nil {
|
||||||
|
return nil, document.Error
|
||||||
|
}
|
||||||
|
m := document.BufferSettings
|
||||||
|
if v, exists := m["AUTO_LINK"]; exists {
|
||||||
|
orgConfig.AutoLink = v == "true"
|
||||||
|
delete(m, "AUTO_LINK")
|
||||||
|
}
|
||||||
|
if v, exists := m["ADDRESS"]; exists {
|
||||||
|
address = v
|
||||||
|
delete(m, "ADDRESS")
|
||||||
|
}
|
||||||
|
if v, exists := m["PUBLIC"]; exists {
|
||||||
|
publicDir = v
|
||||||
|
delete(m, "PUBLIC")
|
||||||
|
}
|
||||||
|
if v, exists := m["CONTENT"]; exists {
|
||||||
|
contentDir = v
|
||||||
|
delete(m, "CONTENT")
|
||||||
|
}
|
||||||
|
if v, exists := m["MAX_EMPHASIS_NEW_LINES"]; exists {
|
||||||
|
i, err := strconv.Atoi(v)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("MAX_EMPHASIS_NEW_LINES: %v %w", v, err)
|
||||||
|
}
|
||||||
|
orgConfig.MaxEmphasisNewLines = i
|
||||||
|
delete(m, "MAX_EMPHASIS_NEW_LINES")
|
||||||
|
}
|
||||||
|
for k, v := range m {
|
||||||
|
if k == "OPTIONS" {
|
||||||
|
orgConfig.DefaultSettings[k] = v + " " + orgConfig.DefaultSettings[k]
|
||||||
|
} else {
|
||||||
|
orgConfig.DefaultSettings[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
config := &Config{
|
||||||
|
ConfigFile: configFile,
|
||||||
|
ContentDir: filepath.Join(workingDir, contentDir),
|
||||||
|
PublicDir: filepath.Join(workingDir, publicDir),
|
||||||
|
Address: address,
|
||||||
|
Template: template.New("_").Funcs(TemplateFuncs),
|
||||||
|
OrgConfig: orgConfig,
|
||||||
|
}
|
||||||
|
for name, node := range document.NamedNodes {
|
||||||
|
if block, ok := node.(org.Block); ok {
|
||||||
|
if block.Parameters[0] != "html" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, err := config.Template.New(name).Parse(org.String(block.Children)); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return config, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) Serve() error {
|
||||||
|
http.Handle("/", http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
|
||||||
|
if strings.HasSuffix(req.URL.Path, ".html") || strings.HasSuffix(req.URL.Path, "/") {
|
||||||
|
start := time.Now()
|
||||||
|
if c, err := ReadConfig(c.ConfigFile); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
} else {
|
||||||
|
if err := c.Render(); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.Printf("render took %s", time.Since(start))
|
||||||
|
}
|
||||||
|
http.ServeFile(res, req, filepath.Join(c.PublicDir, path.Clean(req.URL.Path)))
|
||||||
|
}))
|
||||||
|
log.Printf("listening on: %s", c.Address)
|
||||||
|
return http.ListenAndServe(c.Address, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) Render() error {
|
||||||
|
if err := os.RemoveAll(c.PublicDir); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := os.MkdirAll(c.PublicDir, os.ModePerm); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
pages, err := c.RenderContent()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return c.RenderLists(pages)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) RenderContent() ([]*Page, error) {
|
||||||
|
pages := []*Page{}
|
||||||
|
err := filepath.Walk(c.ContentDir, func(path string, info os.FileInfo, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
relPath, err := filepath.Rel(c.ContentDir, path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
publicPath := filepath.Join(c.PublicDir, relPath)
|
||||||
|
publicInfo, err := os.Stat(publicPath)
|
||||||
|
if err != nil && !os.IsNotExist(err) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if info.IsDir() {
|
||||||
|
return os.MkdirAll(publicPath, info.Mode())
|
||||||
|
}
|
||||||
|
if filepath.Ext(path) != ".org" && (os.IsNotExist(err) || info.ModTime().After(publicInfo.ModTime())) {
|
||||||
|
return os.Link(path, publicPath)
|
||||||
|
}
|
||||||
|
p, err := NewPage(c, path, info)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
pages = append(pages, p)
|
||||||
|
p.PermaLink = "/" + relPath[:len(relPath)-len(".org")] + ".html"
|
||||||
|
return p.Render(publicPath[:len(publicPath)-len(".org")] + ".html")
|
||||||
|
})
|
||||||
|
sort.Slice(pages, func(i, j int) bool { return pages[i].Date.After(pages[j].Date) })
|
||||||
|
return pages, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) RenderLists(pages []*Page) error {
|
||||||
|
ms := toMap(c.OrgConfig.DefaultSettings, nil)
|
||||||
|
ms["Pages"] = pages
|
||||||
|
lists := map[string]map[string][]interface{}{"": map[string][]interface{}{"": nil}}
|
||||||
|
for _, p := range pages {
|
||||||
|
mp := toMap(p.BufferSettings, p)
|
||||||
|
if p.BufferSettings["DATE"] != "" {
|
||||||
|
lists[""][""] = append(lists[""][""], mp)
|
||||||
|
}
|
||||||
|
for k, v := range p.BufferSettings {
|
||||||
|
if strings.HasSuffix(k, "[]") {
|
||||||
|
list := strings.ToLower(k[:len(k)-2])
|
||||||
|
if lists[list] == nil {
|
||||||
|
lists[list] = map[string][]interface{}{}
|
||||||
|
}
|
||||||
|
for _, sublist := range strings.Fields(v) {
|
||||||
|
lists[list][sublist] = append(lists[list][strings.ToLower(sublist)], mp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for list, sublists := range lists {
|
||||||
|
for sublist, pages := range sublists {
|
||||||
|
ms["Title"] = strings.Title(sublist)
|
||||||
|
ms["Pages"] = pages
|
||||||
|
if err := c.RenderList(list, sublist, ms); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) RenderList(list, sublist string, m map[string]interface{}) error {
|
||||||
|
t := c.Template.Lookup(list)
|
||||||
|
if list == "" {
|
||||||
|
m["Title"] = c.OrgConfig.DefaultSettings["TITLE"]
|
||||||
|
t = c.Template.Lookup("index")
|
||||||
|
}
|
||||||
|
if t == nil {
|
||||||
|
t = c.Template.Lookup("list")
|
||||||
|
}
|
||||||
|
if t == nil {
|
||||||
|
return fmt.Errorf("cannot render list: neither template %s nor list", list)
|
||||||
|
}
|
||||||
|
path := filepath.Join(c.PublicDir, slugify(list), slugify(sublist))
|
||||||
|
if err := os.MkdirAll(path, os.ModePerm); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
f, err := os.Create(filepath.Join(path, "index.html"))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
return t.Execute(f, m)
|
||||||
|
}
|
14
blorg/config_test.go
Normal file
14
blorg/config_test.go
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
package blorg
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestBlorg(t *testing.T) {
|
||||||
|
config, err := ReadConfig("testdata/blorg.org")
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Could not read config: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := config.Render(); err != nil {
|
||||||
|
t.Errorf("Could not render: %s", err)
|
||||||
|
}
|
||||||
|
}
|
83
blorg/page.go
Normal file
83
blorg/page.go
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
package blorg
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"html/template"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/niklasfasching/go-org/org"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Page struct {
|
||||||
|
*Config
|
||||||
|
Document *org.Document
|
||||||
|
Info os.FileInfo
|
||||||
|
PermaLink string
|
||||||
|
Date time.Time
|
||||||
|
Content template.HTML
|
||||||
|
BufferSettings map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPage(c *Config, path string, info os.FileInfo) (*Page, error) {
|
||||||
|
f, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
d := c.OrgConfig.Parse(f, path)
|
||||||
|
content, err := d.Write(getWriter())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
date, err := time.Parse("2006-01-02", d.Get("DATE"))
|
||||||
|
if err != nil {
|
||||||
|
date, _ = time.Parse("2006-01-02", "1970-01-01")
|
||||||
|
}
|
||||||
|
return &Page{
|
||||||
|
Config: c,
|
||||||
|
Document: d,
|
||||||
|
Info: info,
|
||||||
|
Date: date,
|
||||||
|
Content: template.HTML(content),
|
||||||
|
BufferSettings: d.BufferSettings,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Page) Render(path string) error {
|
||||||
|
f, err := os.Create(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
templateName := "item"
|
||||||
|
if v, ok := p.BufferSettings["TEMPLATE"]; ok {
|
||||||
|
templateName = v
|
||||||
|
}
|
||||||
|
t := p.Template.Lookup(templateName)
|
||||||
|
if t == nil {
|
||||||
|
return fmt.Errorf("cannot render page %s: unknown template %s", p.Info.Name(), templateName)
|
||||||
|
}
|
||||||
|
return t.Execute(f, toMap(p.BufferSettings, p))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Page) Summary() template.HTML {
|
||||||
|
for _, n := range p.Document.Nodes {
|
||||||
|
switch n := n.(type) {
|
||||||
|
case org.Block:
|
||||||
|
if n.Name == "SUMMARY" {
|
||||||
|
w := getWriter()
|
||||||
|
org.WriteNodes(w, n.Children...)
|
||||||
|
return template.HTML(w.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for i, n := range p.Document.Nodes {
|
||||||
|
switch n.(type) {
|
||||||
|
case org.Headline:
|
||||||
|
w := getWriter()
|
||||||
|
org.WriteNodes(w, p.Document.Nodes[:i]...)
|
||||||
|
return template.HTML(w.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
100
blorg/testdata/blorg.org
vendored
Normal file
100
blorg/testdata/blorg.org
vendored
Normal file
|
@ -0,0 +1,100 @@
|
||||||
|
#+AUTHOR: author
|
||||||
|
#+TITLE: blog
|
||||||
|
#+BASE_URL: https://www.example.com
|
||||||
|
#+OPTIONS: toc:nil
|
||||||
|
#+CONTENT: ./content
|
||||||
|
#+PUBLIC: ./public
|
||||||
|
|
||||||
|
* templates
|
||||||
|
** head
|
||||||
|
#+name: head
|
||||||
|
#+begin_src html
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<link rel="stylesheet" href="/style.css" type="text/css" />
|
||||||
|
<title>{{ .Title }}</title>
|
||||||
|
</head>
|
||||||
|
#+end_src
|
||||||
|
** header
|
||||||
|
#+name: header
|
||||||
|
#+begin_src html
|
||||||
|
<header class='header'>
|
||||||
|
<a class="logo" href="/">home</a>
|
||||||
|
<nav>
|
||||||
|
<a href="https://www.github.com/niklasfasching">github</a>
|
||||||
|
<a href="/about">about</a>
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
#+end_src
|
||||||
|
** index
|
||||||
|
#+name: index
|
||||||
|
#+begin_src html
|
||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
{{ template "head" . }}
|
||||||
|
<body>
|
||||||
|
{{ template "header" . }}
|
||||||
|
<h1 class="title">{{ .Title }}</h1>
|
||||||
|
<ul class="posts">
|
||||||
|
{{ range .Pages }}
|
||||||
|
<li class="post">
|
||||||
|
<a href="{{ .PermaLink }}">
|
||||||
|
<date>{{ .Date.Format "02.01.2006" }}</date>
|
||||||
|
<span>{{ .Title }}</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{{ end }}
|
||||||
|
</ul>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
#+end_src
|
||||||
|
** item
|
||||||
|
#+name: item
|
||||||
|
#+begin_src html
|
||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
{{ template "head" . }}
|
||||||
|
<body>
|
||||||
|
{{ template "header" . }}
|
||||||
|
<div class="container">
|
||||||
|
<h1 class="title">{{ .Title }}
|
||||||
|
<br>
|
||||||
|
<span class="subtitle">{{ .Subtitle }}</span>
|
||||||
|
</h1>
|
||||||
|
<ul class="tags">
|
||||||
|
{{ range .Tags }}
|
||||||
|
<li><a href="/tags/{{ . | Slugify }}">{{ . }}</a></li>
|
||||||
|
{{ end }}
|
||||||
|
</ul>
|
||||||
|
{{ .Content }}
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
#+end_src
|
||||||
|
|
||||||
|
** list
|
||||||
|
#+name: list
|
||||||
|
#+begin_src html
|
||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
{{ template "head" . }}
|
||||||
|
<body>
|
||||||
|
{{ template "header" . }}
|
||||||
|
<div class="container">
|
||||||
|
<h1 class="title">{{ .Title }}</h1>
|
||||||
|
<ul class="posts">
|
||||||
|
{{ range .Pages }}
|
||||||
|
<li class="post">
|
||||||
|
<a href="{{ .PermaLink }}">
|
||||||
|
<date>{{ .Date.Format "02.01.2006" }}</date>
|
||||||
|
<span>{{ .Title }}</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{{ end }}
|
||||||
|
</ul>
|
||||||
|
<ul>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
#+end_src
|
4
blorg/testdata/content/about.org
vendored
Normal file
4
blorg/testdata/content/about.org
vendored
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
#+TITLE: About
|
||||||
|
#+TAG[]: static
|
||||||
|
|
||||||
|
and some content
|
2
blorg/testdata/content/post.org
vendored
Normal file
2
blorg/testdata/content/post.org
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
#+TITLE: some post
|
||||||
|
#+TAG[]: post
|
37
blorg/testdata/public/about.html
vendored
Normal file
37
blorg/testdata/public/about.html
vendored
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<link rel="stylesheet" href="/style.css" type="text/css" />
|
||||||
|
<title>About</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<header class='header'>
|
||||||
|
<a class="logo" href="/">home</a>
|
||||||
|
<nav>
|
||||||
|
<a href="https://www.github.com/niklasfasching">github</a>
|
||||||
|
<a href="/about">about</a>
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<h1 class="title">About
|
||||||
|
<br>
|
||||||
|
<span class="subtitle"></span>
|
||||||
|
</h1>
|
||||||
|
<ul class="tags">
|
||||||
|
|
||||||
|
</ul>
|
||||||
|
<h1 class="title"><p>
|
||||||
|
About
|
||||||
|
</p>
|
||||||
|
</h1>
|
||||||
|
<p>
|
||||||
|
and some content
|
||||||
|
</p>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
24
blorg/testdata/public/index.html
vendored
Normal file
24
blorg/testdata/public/index.html
vendored
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<link rel="stylesheet" href="/style.css" type="text/css" />
|
||||||
|
<title>blog</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<header class='header'>
|
||||||
|
<a class="logo" href="/">home</a>
|
||||||
|
<nav>
|
||||||
|
<a href="https://www.github.com/niklasfasching">github</a>
|
||||||
|
<a href="/about">about</a>
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<h1 class="title">blog</h1>
|
||||||
|
<ul class="posts">
|
||||||
|
|
||||||
|
</ul>
|
||||||
|
</body>
|
||||||
|
</html>
|
34
blorg/testdata/public/post.html
vendored
Normal file
34
blorg/testdata/public/post.html
vendored
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<link rel="stylesheet" href="/style.css" type="text/css" />
|
||||||
|
<title>some post</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<header class='header'>
|
||||||
|
<a class="logo" href="/">home</a>
|
||||||
|
<nav>
|
||||||
|
<a href="https://www.github.com/niklasfasching">github</a>
|
||||||
|
<a href="/about">about</a>
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<h1 class="title">some post
|
||||||
|
<br>
|
||||||
|
<span class="subtitle"></span>
|
||||||
|
</h1>
|
||||||
|
<ul class="tags">
|
||||||
|
|
||||||
|
</ul>
|
||||||
|
<h1 class="title"><p>
|
||||||
|
some post
|
||||||
|
</p>
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
34
blorg/testdata/public/tag/post/index.html
vendored
Normal file
34
blorg/testdata/public/tag/post/index.html
vendored
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<link rel="stylesheet" href="/style.css" type="text/css" />
|
||||||
|
<title>Post</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<header class='header'>
|
||||||
|
<a class="logo" href="/">home</a>
|
||||||
|
<nav>
|
||||||
|
<a href="https://www.github.com/niklasfasching">github</a>
|
||||||
|
<a href="/about">about</a>
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<h1 class="title">Post</h1>
|
||||||
|
<ul class="posts">
|
||||||
|
|
||||||
|
<li class="post">
|
||||||
|
<a href="/post.html">
|
||||||
|
<date>01.01.1970</date>
|
||||||
|
<span>some post</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
</ul>
|
||||||
|
<ul>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
34
blorg/testdata/public/tag/static/index.html
vendored
Normal file
34
blorg/testdata/public/tag/static/index.html
vendored
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<link rel="stylesheet" href="/style.css" type="text/css" />
|
||||||
|
<title>Static</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<header class='header'>
|
||||||
|
<a class="logo" href="/">home</a>
|
||||||
|
<nav>
|
||||||
|
<a href="https://www.github.com/niklasfasching">github</a>
|
||||||
|
<a href="/about">about</a>
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<h1 class="title">Static</h1>
|
||||||
|
<ul class="posts">
|
||||||
|
|
||||||
|
<li class="post">
|
||||||
|
<a href="/about.html">
|
||||||
|
<date>01.01.1970</date>
|
||||||
|
<span>About</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
</ul>
|
||||||
|
<ul>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
71
blorg/util.go
Normal file
71
blorg/util.go
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
package blorg
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/alecthomas/chroma"
|
||||||
|
"github.com/alecthomas/chroma/formatters/html"
|
||||||
|
"github.com/alecthomas/chroma/lexers"
|
||||||
|
"github.com/alecthomas/chroma/styles"
|
||||||
|
"github.com/niklasfasching/go-org/org"
|
||||||
|
)
|
||||||
|
|
||||||
|
var snakeCaseRegexp = regexp.MustCompile(`(^[A-Za-z])|_([A-Za-z])`)
|
||||||
|
var whitespaceRegexp = regexp.MustCompile(`\s+`)
|
||||||
|
var nonWordCharRegexp = regexp.MustCompile(`[^\w-]`)
|
||||||
|
|
||||||
|
func toMap(bufferSettings map[string]string, x interface{}) map[string]interface{} {
|
||||||
|
m := map[string]interface{}{}
|
||||||
|
for k, v := range bufferSettings {
|
||||||
|
k = toCamelCase(k)
|
||||||
|
if strings.HasSuffix(k, "[]") {
|
||||||
|
m[k[:len(k)-2]] = strings.Fields(v)
|
||||||
|
} else {
|
||||||
|
m[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if x == nil {
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
v := reflect.ValueOf(x).Elem()
|
||||||
|
for i := 0; i < v.NumField(); i++ {
|
||||||
|
m[v.Type().Field(i).Name] = v.Field(i).Interface()
|
||||||
|
}
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
func toCamelCase(s string) string {
|
||||||
|
return snakeCaseRegexp.ReplaceAllStringFunc(strings.ToLower(s), func(s string) string {
|
||||||
|
return strings.ToUpper(strings.Replace(s, "_", "", -1))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func slugify(s string) string {
|
||||||
|
s = strings.ToLower(s)
|
||||||
|
s = whitespaceRegexp.ReplaceAllString(s, "-")
|
||||||
|
s = nonWordCharRegexp.ReplaceAllString(s, "")
|
||||||
|
return strings.Trim(s, "-")
|
||||||
|
}
|
||||||
|
|
||||||
|
func getWriter() org.Writer {
|
||||||
|
w := org.NewHTMLWriter()
|
||||||
|
w.HighlightCodeBlock = highlightCodeBlock
|
||||||
|
return w
|
||||||
|
}
|
||||||
|
|
||||||
|
func highlightCodeBlock(source, lang string, inline bool) string {
|
||||||
|
var w strings.Builder
|
||||||
|
l := lexers.Get(lang)
|
||||||
|
if l == nil {
|
||||||
|
l = lexers.Fallback
|
||||||
|
}
|
||||||
|
l = chroma.Coalesce(l)
|
||||||
|
it, _ := l.Tokenise(nil, source)
|
||||||
|
_ = html.New().Format(&w, styles.Get("github"), it)
|
||||||
|
if inline {
|
||||||
|
return `<div class="highlight-inline">` + "\n" + w.String() + "\n" + `</div>`
|
||||||
|
}
|
||||||
|
return `<div class="highlight">` + "\n" + w.String() + "\n" + `</div>`
|
||||||
|
}
|
|
@ -2,6 +2,6 @@
|
||||||
|
|
||||||
for org_file in org/testdata/*.org; do
|
for org_file in org/testdata/*.org; do
|
||||||
echo $org_file
|
echo $org_file
|
||||||
./go-org $org_file html > org/testdata/$(basename $org_file .org).html
|
./go-org render $org_file html > org/testdata/$(basename $org_file .org).html
|
||||||
./go-org $org_file org > org/testdata/$(basename $org_file .org).pretty_org
|
./go-org render $org_file org > org/testdata/$(basename $org_file .org).pretty_org
|
||||||
done
|
done
|
||||||
|
|
|
@ -34,7 +34,7 @@ for org_file in $org_files; do
|
||||||
<h2><a id='${name}' href='#toc-${name}'>${name}</a></h2>
|
<h2><a id='${name}' href='#toc-${name}'>${name}</a></h2>
|
||||||
<div class='source'>
|
<div class='source'>
|
||||||
<pre class='org'>$(sed 's/&/\&/g; s/</\</g; s/>/\>/g;' $org_file)</pre>
|
<pre class='org'>$(sed 's/&/\&/g; s/</\</g; s/>/\>/g;' $org_file)</pre>
|
||||||
<div class='html'>$(./go-org $org_file html-chroma)</div>
|
<div class='html'>$(./go-org render $org_file html-chroma)</div>
|
||||||
</div>"
|
</div>"
|
||||||
done
|
done
|
||||||
|
|
||||||
|
@ -143,7 +143,7 @@ for org_file in $org_files; do
|
||||||
<h2><a id='${name}' href='#toc-${name}'>${name}</a></h2>
|
<h2><a id='${name}' href='#toc-${name}'>${name}</a></h2>
|
||||||
<div class='source'>
|
<div class='source'>
|
||||||
<div class='html'>$(./gh-pages/goorgeous $org_file)</div>
|
<div class='html'>$(./gh-pages/goorgeous $org_file)</div>
|
||||||
<div class='html'>$(./go-org $org_file html-chroma)</div>
|
<div class='html'>$(./go-org render $org_file html-chroma)</div>
|
||||||
</div>"
|
</div>"
|
||||||
done
|
done
|
||||||
go_org_vs_goorgeous_examples+="</body></html>"
|
go_org_vs_goorgeous_examples+="</body></html>"
|
||||||
|
|
83
main.go
83
main.go
|
@ -12,38 +12,91 @@ import (
|
||||||
"github.com/alecthomas/chroma/formatters/html"
|
"github.com/alecthomas/chroma/formatters/html"
|
||||||
"github.com/alecthomas/chroma/lexers"
|
"github.com/alecthomas/chroma/lexers"
|
||||||
"github.com/alecthomas/chroma/styles"
|
"github.com/alecthomas/chroma/styles"
|
||||||
|
"github.com/niklasfasching/go-org/blorg"
|
||||||
"github.com/niklasfasching/go-org/org"
|
"github.com/niklasfasching/go-org/org"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
log := log.New(os.Stderr, "", 0)
|
log := log.New(os.Stderr, "", 0)
|
||||||
if len(os.Args) < 3 {
|
if len(os.Args) < 2 {
|
||||||
log.Println("USAGE: org FILE OUTPUT_FORMAT")
|
log.Println("USAGE: org COMMAND [ARGS]")
|
||||||
log.Fatal("Supported output formats: org, html, html-chroma")
|
log.Println("- org render FILE OUTPUT_FORMAT")
|
||||||
|
log.Println(" OUTPUT_FORMAT: org, html, html-chroma")
|
||||||
|
log.Println("- org blorg init")
|
||||||
|
log.Println("- org blorg build")
|
||||||
|
log.Println("- org blorg serve")
|
||||||
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
path := os.Args[1]
|
|
||||||
|
switch cmd := strings.ToLower(os.Args[1]); cmd {
|
||||||
|
case "render":
|
||||||
|
if len(os.Args) < 4 {
|
||||||
|
log.Fatal("USAGE: org render FILE OUTPUT_FORMAT")
|
||||||
|
}
|
||||||
|
out, err := render(os.Args[2], os.Args[3])
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Error: %v", err)
|
||||||
|
}
|
||||||
|
fmt.Fprint(os.Stdout, out)
|
||||||
|
case "blorg":
|
||||||
|
if err := runBlorg(os.Args[2]); err != nil {
|
||||||
|
log.Fatalf("Error: %v", err)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
log.Fatalf("Unsupported command: %s", cmd)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func runBlorg(cmd string) error {
|
||||||
|
switch strings.ToLower(cmd) {
|
||||||
|
case "init":
|
||||||
|
if _, err := os.Stat(blorg.DefaultConfigFile); !os.IsNotExist(err) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := ioutil.WriteFile(blorg.DefaultConfigFile, []byte(blorg.DefaultConfig), os.ModePerm); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
log.Printf("blorg init finished: Wrote ./%s", blorg.DefaultConfigFile)
|
||||||
|
return nil
|
||||||
|
case "build":
|
||||||
|
config, err := blorg.ReadConfig(blorg.DefaultConfigFile)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := config.Render(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
log.Println("blorg build finished")
|
||||||
|
return nil
|
||||||
|
case "serve":
|
||||||
|
config, err := blorg.ReadConfig(blorg.DefaultConfigFile)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return config.Serve()
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("Supported commands: init build serve")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func render(path, format string) (string, error) {
|
||||||
bs, err := ioutil.ReadFile(path)
|
bs, err := ioutil.ReadFile(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
return "", err
|
||||||
}
|
}
|
||||||
out, err := "", nil
|
|
||||||
d := org.New().Parse(bytes.NewReader(bs), path)
|
d := org.New().Parse(bytes.NewReader(bs), path)
|
||||||
switch strings.ToLower(os.Args[2]) {
|
switch strings.ToLower(format) {
|
||||||
case "org":
|
case "org":
|
||||||
out, err = d.Write(org.NewOrgWriter())
|
return d.Write(org.NewOrgWriter())
|
||||||
case "html":
|
case "html":
|
||||||
out, err = d.Write(org.NewHTMLWriter())
|
return d.Write(org.NewHTMLWriter())
|
||||||
case "html-chroma":
|
case "html-chroma":
|
||||||
writer := org.NewHTMLWriter()
|
writer := org.NewHTMLWriter()
|
||||||
writer.HighlightCodeBlock = highlightCodeBlock
|
writer.HighlightCodeBlock = highlightCodeBlock
|
||||||
out, err = d.Write(writer)
|
return d.Write(writer)
|
||||||
default:
|
default:
|
||||||
log.Fatal("Unsupported output format")
|
return "", fmt.Errorf("unsupported output format: %s", format)
|
||||||
}
|
}
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
fmt.Fprint(os.Stdout, out)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func highlightCodeBlock(source, lang string, inline bool) string {
|
func highlightCodeBlock(source, lang string, inline bool) string {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue