pax_global_header 0000666 0000000 0000000 00000000064 14162165732 0014521 g ustar 00root root 0000000 0000000 52 comment=f2286d48ff1197b6f02794d911829e9f6ce36fe5 go-org-1.6.0/ 0000775 0000000 0000000 00000000000 14162165732 0012717 5 ustar 00root root 0000000 0000000 go-org-1.6.0/.github/ 0000775 0000000 0000000 00000000000 14162165732 0014257 5 ustar 00root root 0000000 0000000 go-org-1.6.0/.github/workflows/ 0000775 0000000 0000000 00000000000 14162165732 0016314 5 ustar 00root root 0000000 0000000 go-org-1.6.0/.github/workflows/ci.yml 0000664 0000000 0000000 00000001656 14162165732 0017442 0 ustar 00root root 0000000 0000000 name: CI on: push: branches: [ master ] jobs: build: runs-on: ubuntu-latest steps: - name: git run: | git clone --depth 1 "https://x-access-token:${{secrets.GITHUB_TOKEN}}@github.com/${GITHUB_REPOSITORY}" . git config user.name "GitHub Action" git config user.email "action@github.com" git log -1 --format="%H" - name: go run: sudo snap install go --classic - name: test run: make test - name: gh-pages run: | git checkout --orphan gh-pages && git reset make generate-gh-pages git add -f docs/ && git commit -m deploy git push -f origin gh-pages - name: notify if: ${{ failure() }} run: | text="https://github.com/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID} failed" curl --silent --output /dev/null ${{secrets.TELEGRAM_URL}} -d "chat_id=${{secrets.TELEGRAM_CHAT_ID}}&text=${text}" go-org-1.6.0/.gitignore 0000664 0000000 0000000 00000000042 14162165732 0014703 0 ustar 00root root 0000000 0000000 /docs/ /go-org /fuzz /org-fuzz.zip go-org-1.6.0/LICENSE 0000664 0000000 0000000 00000002060 14162165732 0013722 0 ustar 00root root 0000000 0000000 MIT License Copyright (c) 2018 Niklas Fasching Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. go-org-1.6.0/Makefile 0000664 0000000 0000000 00000001531 14162165732 0014357 0 ustar 00root root 0000000 0000000 .PHONY: default default: test go-org: *.go */*.go go.mod go.sum go get -d ./... go build . .PHONY: build build: go-org .PHONY: test test: go get -d -t ./... go test ./... -v .PHONY: setup setup: git config core.hooksPath etc/githooks .PHONY: preview preview: generate xdg-open docs/index.html .PHONY: generate generate: generate-gh-pages generate-fixtures .PHONY: generate-gh-pages generate-gh-pages: build ./etc/generate-gh-pages .PHONY: generate-fixtures generate-fixtures: build ./etc/generate-fixtures .PHONY: fuzz fuzz: build @echo also see "http://lcamtuf.coredump.cx/afl/README.txt" go get github.com/dvyukov/go-fuzz/go-fuzz go get github.com/dvyukov/go-fuzz/go-fuzz-build mkdir -p fuzz fuzz/corpus cp org/testdata/*.org fuzz/corpus go-fuzz-build github.com/niklasfasching/go-org/org go-fuzz -bin=./org-fuzz.zip -workdir=fuzz go-org-1.6.0/README.org 0000664 0000000 0000000 00000004157 14162165732 0014374 0 ustar 00root root 0000000 0000000 * go-org An Org mode parser and static site generator in go. Take a look at github pages - for [[https://niklasfasching.github.io/go-org/][org to html conversion]] examples - for a [[https://niklasfasching.github.io/go-org/blorg][static site]] generated by blorg - to [[https://niklasfasching.github.io/go-org/convert.html][try it out live]] in your browser [[https://raw.githubusercontent.com/niklasfasching/go-org/master/etc/example.png]] 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 bash $ go-org Usage: go-org COMMAND [ARGS]... Commands: - render [FILE] FORMAT FORMAT: org, html, html-chroma Instead of specifying a file, org mode content can also be passed on stdin - blorg - blorg init - blorg build - blorg serve #+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 1. =make setup install= 2. change things 3. =make preview= (regenerates fixtures & shows output in a browser) in general, have a look at the Makefile - it's short enough. * resources - test files - [[https://raw.githubusercontent.com/kaushalmodi/ox-hugo/master/test/site/content-org/all-posts.org][ox-hugo all-posts.org]] - https://ox-hugo.scripter.co/doc/examples/ - https://orgmode.org/manual/ - https://orgmode.org/worg/dev/org-syntax.html - https://code.orgmode.org/bzg/org-mode/src/master/lisp/org.el - https://code.orgmode.org/bzg/org-mode/src/master/lisp/org-element.el - mostly those & ox-html.el, but yeah, all of [[https://code.orgmode.org/bzg/org-mode/src/master/lisp/]] - existing Org mode implementations: [[https://github.com/emacsmirror/org][org]], [[https://github.com/bdewey/org-ruby/blob/master/spec/html_examples][org-ruby]], [[https://github.com/chaseadamsio/goorgeous/][goorgeous]], [[https://github.com/jgm/pandoc/][pandoc]] go-org-1.6.0/blorg/ 0000775 0000000 0000000 00000000000 14162165732 0014024 5 ustar 00root root 0000000 0000000 go-org-1.6.0/blorg/config.go 0000664 0000000 0000000 00000014257 14162165732 0015631 0 ustar 00root root 0000000 0000000 // 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" _ "embed" "github.com/niklasfasching/go-org/org" ) type Config struct { ConfigFile string ContentDir string PublicDir string Address string BaseUrl string Template *template.Template OrgConfig *org.Configuration } var DefaultConfigFile = "blorg.org" //go:embed testdata/blorg.org var DefaultConfig string var TemplateFuncs = map[string]interface{}{ "Slugify": slugify, } func ReadConfig(configFile string) (*Config, error) { baseUrl, 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 !strings.HasSuffix(m["BASE_URL"], "/") { m["BASE_URL"] += "/" } 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 _, exists := m["BASE_URL"]; exists { baseUrl = m["BASE_URL"] } 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, BaseUrl: baseUrl, 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 = c.BaseUrl + 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{}{"": {"": nil}} for _, p := range pages { if p.BufferSettings["DRAFT"] != "" { continue } 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) } go-org-1.6.0/blorg/config_test.go 0000664 0000000 0000000 00000001624 14162165732 0016662 0 ustar 00root root 0000000 0000000 package blorg import ( "fmt" "io/ioutil" "os/exec" "strings" "testing" ) func TestBlorg(t *testing.T) { config, err := ReadConfig("testdata/blorg.org") if err != nil { t.Errorf("Could not read config: %s", err) return } commitedHashBs, err := ioutil.ReadFile("testdata/public.md5") if err != nil { t.Errorf("Could not read hash bytes: %s", err) return } if err := config.Render(); err != nil { t.Errorf("Could not render: %s", err) return } renderedHashBs, err := exec.Command("bash", "-c", fmt.Sprintf("find %s -type f | sort -u | xargs cat | md5sum", config.PublicDir)).Output() if err != nil { t.Errorf("Could not hash PublicDir: %s", err) return } rendered, committed := strings.TrimSpace(string(renderedHashBs)), strings.TrimSpace(string(commitedHashBs)) if rendered != committed { t.Errorf("PublicDir hashes do not match: '%s' -> '%s'", committed, rendered) return } } go-org-1.6.0/blorg/page.go 0000664 0000000 0000000 00000003410 14162165732 0015265 0 ustar 00root root 0000000 0000000 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 { if p.BufferSettings["DRAFT"] != "" { return nil } 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 "" } go-org-1.6.0/blorg/testdata/ 0000775 0000000 0000000 00000000000 14162165732 0015635 5 ustar 00root root 0000000 0000000 go-org-1.6.0/blorg/testdata/blorg.org 0000664 0000000 0000000 00000004317 14162165732 0017460 0 ustar 00root root 0000000 0000000 #+AUTHOR: testdata #+TITLE: blorg #+BASE_URL: /go-org/blorg #+OPTIONS: toc:nil title:nil #+CONTENT: ./content #+PUBLIC: ./public * templates ** head #+name: head #+begin_src html
Only pages that have a date will be listed here - e.g. not about.html
This site is generated from go-org/blorg/testdata/content using the configuration in blorg.org
#+AUTHOR: testdata
#+TITLE: blorg
#+BASE_URL: /go-org/blorg
#+OPTIONS: toc:nil title: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="/go-org/blorg/style.css" type="text/css" />
<title>{{ .Title }}</title>
</head>
#+end_src
** header
#+name: header
#+begin_src html
<header class='header'>
<a class="logo" href="/go-org/blorg">home</a>
<nav>
<a href="https://www.github.com/niklasfasching/go-org">github</a>
</nav>
</header>
#+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="/go-org/blorg/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
** index
#+name: index
#+begin_src html
<!doctype html>
<html>
{{ template "head" . }}
<body>
{{ template "header" . }}
<div class="container">
<h1 class="title">{{ .Title }}</h1>
<p>Only pages that have a date will be listed here - e.g. not <a href="about.html">about.html</a>
<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
Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.
Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.
Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.
%s", err)) } else { out.Set("innerHTML", html) } return nil })) select {} // stay alive } go-org-1.6.0/etc/example.png 0000664 0000000 0000000 00000555405 14162165732 0015651 0 ustar 00root root 0000000 0000000 PNG IHDR C M sBIT|d IDATxw|[^`y4K,MCP=Kf]