pax_global_header 0000666 0000000 0000000 00000000064 14443122371 0014513 g ustar 00root root 0000000 0000000 52 comment=6eb20dbda93cb88c3503f7508dc78cbbc639378f go-org-1.7.0/ 0000775 0000000 0000000 00000000000 14443122371 0012712 5 ustar 00root root 0000000 0000000 go-org-1.7.0/.github/ 0000775 0000000 0000000 00000000000 14443122371 0014252 5 ustar 00root root 0000000 0000000 go-org-1.7.0/.github/workflows/ 0000775 0000000 0000000 00000000000 14443122371 0016307 5 ustar 00root root 0000000 0000000 go-org-1.7.0/.github/workflows/ci.yml 0000664 0000000 0000000 00000001656 14443122371 0017435 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.7.0/.gitignore 0000664 0000000 0000000 00000000042 14443122371 0014676 0 ustar 00root root 0000000 0000000 /docs/ /go-org /fuzz /org-fuzz.zip go-org-1.7.0/LICENSE 0000664 0000000 0000000 00000002060 14443122371 0013715 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.7.0/Makefile 0000664 0000000 0000000 00000002151 14443122371 0014351 0 ustar 00root root 0000000 0000000 .PHONY: default default: test go_files=$(shell find . -name '*.go' ! -path './docs/*') go-org: $(go_files) go.mod go.sum go get -d ./... go build . build: go-org .PHONY: test test: go get -d -t ./... go test ./... -v .PHONY: setup setup: git config core.hooksPath etc/githooks command -v go > /dev/null || (echo "go not installed" && false) .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 $(files) .PHONY: serve-gh-pages serve-gh-pages: generate-gh-pages cd docs && mkdir go-org && mv * go-org 2> /dev/null || true cd docs && python3 -m http.server .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.7.0/README.org 0000664 0000000 0000000 00000004147 14443122371 0014366 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= 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.7.0/blorg/ 0000775 0000000 0000000 00000000000 14443122371 0014017 5 ustar 00root root 0000000 0000000 go-org-1.7.0/blorg/config.go 0000664 0000000 0000000 00000014262 14443122371 0015620 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.7.0/blorg/config_test.go 0000664 0000000 0000000 00000004132 14443122371 0016652 0 ustar 00root root 0000000 0000000 package blorg import ( "bufio" "crypto/md5" "encoding/hex" "io/fs" "io/ioutil" "os" "path/filepath" "strings" "testing" ) func TestBlorg(t *testing.T) { // Re-generate this file with `find testdata/public -type f | sort -u | xargs md5sum > testdata/public.md5` hashFile, err := os.Open("testdata/public.md5") if err != nil { t.Errorf("Could not open hash file: %s", err) return } defer hashFile.Close() scanner := bufio.NewScanner(hashFile) committedHashes := make(map[string]string) for scanner.Scan() { parts := strings.Fields(scanner.Text()) if len(parts) != 2 { t.Errorf("Could not split hash entry line in 2: len(parts)=%d", len(parts)) return } hash := parts[0] fileName := parts[1] committedHashes[fileName] = hash } if err := scanner.Err(); err != nil { t.Errorf("Failed to read hash file: %s", err) return } 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) return } renderedFileHashes := make(map[string]string) err = filepath.WalkDir(config.PublicDir, func(path string, d fs.DirEntry, err error) error { if err != nil { return err } if d.IsDir() { return nil } data, err := ioutil.ReadFile(path) if err != nil { return err } hash := md5.Sum(data) renderedFileHashes[path] = hex.EncodeToString(hash[:]) return nil }) if err != nil { t.Errorf("Could not determine hashes of rendered files: %s", err) return } for file, rendered := range renderedFileHashes { if _, ok := committedHashes[file]; !ok { t.Errorf("New file %s does not have a committed hash", file) continue } committed := committedHashes[file] committedHashes[file] = "" // To check if there are missing files later. if rendered != committed { t.Errorf("PublicDir hashes do not match for %s: '%s' -> '%s'", file, committed, rendered) } } for file, committed := range committedHashes { if committed != "" { t.Errorf("Missing file %s has a committed hash, but was not rendered", file) } } } go-org-1.7.0/blorg/page.go 0000664 0000000 0000000 00000003410 14443122371 0015260 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.7.0/blorg/testdata/ 0000775 0000000 0000000 00000000000 14443122371 0015630 5 ustar 00root root 0000000 0000000 go-org-1.7.0/blorg/testdata/blorg.org 0000664 0000000 0000000 00000004317 14443122371 0017453 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.7.0/etc/example.png 0000664 0000000 0000000 00000555405 14443122371 0015644 0 ustar 00root root 0000000 0000000 PNG IHDR C M sBIT|d IDATxw|[^`y4K,MCP=Kf]