├── .github └── workflows │ └── ci.yml ├── .gitignore ├── LICENSE ├── Makefile ├── README.org ├── blorg ├── config.go ├── config_test.go ├── page.go ├── testdata │ ├── blorg.org │ ├── content │ │ ├── about.org │ │ ├── another-post.org │ │ ├── some-post.org │ │ ├── style.css │ │ └── yet-another-post │ │ │ └── index.org │ ├── public.md5 │ └── public │ │ ├── about.html │ │ ├── another-post.html │ │ ├── index.html │ │ ├── some-post.html │ │ ├── style.css │ │ ├── tags │ │ ├── another │ │ │ └── index.html │ │ ├── some │ │ │ └── index.html │ │ ├── static │ │ │ └── index.html │ │ └── yet │ │ │ └── index.html │ │ └── yet-another-post │ │ └── index.html └── util.go ├── etc ├── _wasm.go ├── example.png ├── generate-fixtures ├── generate-gh-pages ├── githooks │ └── pre-push ├── push-hugo-branch └── style.css ├── go.mod ├── go.sum ├── main.go └── org ├── block.go ├── document.go ├── drawer.go ├── footnote.go ├── fuzz.go ├── headline.go ├── html_entity.go ├── html_writer.go ├── html_writer_test.go ├── inline.go ├── keyword.go ├── list.go ├── org_writer.go ├── org_writer_test.go ├── paragraph.go ├── table.go ├── testdata ├── blocks.html ├── blocks.org ├── blocks.pretty_org ├── captions.html ├── captions.org ├── captions.pretty_org ├── east_asian_line_breaks.html ├── east_asian_line_breaks.org ├── east_asian_line_breaks.pretty_org ├── footnotes.html ├── footnotes.org ├── footnotes.pretty_org ├── footnotes_in_headline.html ├── footnotes_in_headline.org ├── footnotes_in_headline.pretty_org ├── headlines.html ├── headlines.org ├── headlines.pretty_org ├── hl-lines.html ├── hl-lines.org ├── hl-lines.pretty_org ├── inline.html ├── inline.org ├── inline.pretty_org ├── keywords.html ├── keywords.org ├── keywords.pretty_org ├── latex.html ├── latex.org ├── latex.pretty_org ├── lists.html ├── lists.org ├── lists.pretty_org ├── misc.html ├── misc.org ├── misc.pretty_org ├── options.html ├── options.org ├── options.pretty_org ├── paragraphs.html ├── paragraphs.org ├── paragraphs.pretty_org ├── setup_file_org ├── tables.html ├── tables.org └── tables.pretty_org ├── util.go ├── util_test.go └── writer.go /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: git 12 | run: | 13 | git clone --depth 1 "https://x-access-token:${{secrets.GITHUB_TOKEN}}@github.com/${GITHUB_REPOSITORY}" . 14 | git config user.name "GitHub Action" 15 | git config user.email "action@github.com" 16 | git log -1 --format="%H" 17 | - name: go 18 | run: sudo snap install go --classic 19 | - name: test 20 | run: make test 21 | - name: gh-pages 22 | run: | 23 | git checkout --orphan gh-pages && git reset 24 | make generate-gh-pages 25 | git add -f docs/ && git commit -m deploy 26 | git push -f origin gh-pages 27 | - name: notify 28 | if: ${{ failure() }} 29 | run: | 30 | text="https://github.com/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID} failed" 31 | curl --silent --output /dev/null ${{secrets.TELEGRAM_URL}} -d "chat_id=${{secrets.TELEGRAM_CHAT_ID}}&text=${text}" 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /docs/ 2 | /go-org 3 | /fuzz 4 | /org-fuzz.zip -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Niklas Fasching 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: default 2 | default: test 3 | 4 | go_files=$(shell find . -name '*.go' ! -path './docs/*') 5 | 6 | go-org: $(go_files) go.mod go.sum 7 | go get -d ./... 8 | go build . 9 | 10 | build: go-org 11 | 12 | .PHONY: test 13 | test: 14 | go get -d -t ./... 15 | go test ./... -v 16 | 17 | .PHONY: setup 18 | setup: 19 | git config core.hooksPath etc/githooks 20 | command -v go > /dev/null || (echo "go not installed" && false) 21 | 22 | .PHONY: preview 23 | preview: generate 24 | xdg-open docs/index.html 25 | 26 | .PHONY: generate 27 | generate: generate-gh-pages generate-fixtures 28 | 29 | .PHONY: generate-gh-pages 30 | generate-gh-pages: build 31 | ./etc/generate-gh-pages 32 | 33 | .PHONY: generate-fixtures 34 | generate-fixtures: build 35 | ./etc/generate-fixtures $(files) 36 | 37 | .PHONY: serve-gh-pages 38 | serve-gh-pages: generate-gh-pages 39 | cd docs && mkdir go-org && mv * go-org 2> /dev/null || true 40 | cd docs && python3 -m http.server 41 | 42 | .PHONY: fuzz 43 | fuzz: build 44 | @echo also see "http://lcamtuf.coredump.cx/afl/README.txt" 45 | go get github.com/dvyukov/go-fuzz/go-fuzz 46 | go get github.com/dvyukov/go-fuzz/go-fuzz-build 47 | mkdir -p fuzz fuzz/corpus 48 | cp org/testdata/*.org fuzz/corpus 49 | go-fuzz-build github.com/niklasfasching/go-org/org 50 | go-fuzz -bin=./org-fuzz.zip -workdir=fuzz 51 | -------------------------------------------------------------------------------- /README.org: -------------------------------------------------------------------------------- 1 | * go-org 2 | An Org mode parser and static site generator in go. 3 | Take a look at github pages 4 | - for [[https://niklasfasching.github.io/go-org/][org to html conversion]] examples 5 | - for a [[https://niklasfasching.github.io/go-org/blorg][static site]] generated by blorg 6 | - to [[https://niklasfasching.github.io/go-org/convert.html][try it out live]] in your browser 7 | 8 | [[https://raw.githubusercontent.com/niklasfasching/go-org/master/etc/example.png]] 9 | 10 | Please note 11 | - the goal for the html export is to produce sensible html output, not to exactly reproduce the output of =org-html-export=. 12 | - 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. 13 | * usage 14 | ** command line 15 | #+begin_src bash 16 | $ go-org 17 | Usage: go-org COMMAND [ARGS]... 18 | Commands: 19 | - render [FILE] FORMAT 20 | FORMAT: org, html, html-chroma 21 | Instead of specifying a file, org mode content can also be passed on stdin 22 | - blorg 23 | - blorg init 24 | - blorg build 25 | - blorg serve 26 | #+end_src 27 | ** as a library 28 | 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]] 29 | * development 30 | 1. =make setup= 31 | 2. change things 32 | 3. =make preview= (regenerates fixtures & shows output in a browser) 33 | 34 | in general, have a look at the Makefile - it's short enough. 35 | * resources 36 | - test files 37 | - [[https://raw.githubusercontent.com/kaushalmodi/ox-hugo/master/test/site/content-org/all-posts.org][ox-hugo all-posts.org]] 38 | - https://ox-hugo.scripter.co/doc/examples/ 39 | - https://orgmode.org/manual/ 40 | - https://orgmode.org/worg/dev/org-syntax.html 41 | - https://code.orgmode.org/bzg/org-mode/src/master/lisp/org.el 42 | - https://code.orgmode.org/bzg/org-mode/src/master/lisp/org-element.el 43 | - mostly those & ox-html.el, but yeah, all of [[https://code.orgmode.org/bzg/org-mode/src/master/lisp/]] 44 | - 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]] 45 | -------------------------------------------------------------------------------- /blorg/config.go: -------------------------------------------------------------------------------- 1 | // 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 2 | // 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 3 | // for a blog in a fraction of the LOC (hugo is a whooping 80k+ excluding dependencies - this will very likely stay <5k). 4 | package blorg 5 | 6 | import ( 7 | "fmt" 8 | "html/template" 9 | "log" 10 | "net/http" 11 | "os" 12 | "path" 13 | "path/filepath" 14 | "sort" 15 | "strconv" 16 | "strings" 17 | "time" 18 | 19 | _ "embed" 20 | 21 | "github.com/niklasfasching/go-org/org" 22 | ) 23 | 24 | type Config struct { 25 | ConfigFile string 26 | ContentDir string 27 | PublicDir string 28 | Address string 29 | BaseUrl string 30 | Template *template.Template 31 | OrgConfig *org.Configuration 32 | } 33 | 34 | var DefaultConfigFile = "blorg.org" 35 | 36 | //go:embed testdata/blorg.org 37 | var DefaultConfig string 38 | 39 | var TemplateFuncs = map[string]interface{}{ 40 | "Slugify": slugify, 41 | } 42 | 43 | func ReadConfig(configFile string) (*Config, error) { 44 | baseUrl, address, publicDir, contentDir, workingDir := "/", ":3000", "public", "content", filepath.Dir(configFile) 45 | f, err := os.Open(configFile) 46 | if err != nil { 47 | return nil, err 48 | } 49 | orgConfig := org.New() 50 | document := orgConfig.Parse(f, configFile) 51 | if document.Error != nil { 52 | return nil, document.Error 53 | } 54 | m := document.BufferSettings 55 | if !strings.HasSuffix(m["BASE_URL"], "/") { 56 | m["BASE_URL"] += "/" 57 | } 58 | if v, exists := m["AUTO_LINK"]; exists { 59 | orgConfig.AutoLink = v == "true" 60 | delete(m, "AUTO_LINK") 61 | } 62 | if v, exists := m["ADDRESS"]; exists { 63 | address = v 64 | delete(m, "ADDRESS") 65 | } 66 | if _, exists := m["BASE_URL"]; exists { 67 | baseUrl = m["BASE_URL"] 68 | } 69 | if v, exists := m["PUBLIC"]; exists { 70 | publicDir = v 71 | delete(m, "PUBLIC") 72 | } 73 | if v, exists := m["CONTENT"]; exists { 74 | contentDir = v 75 | delete(m, "CONTENT") 76 | } 77 | if v, exists := m["MAX_EMPHASIS_NEW_LINES"]; exists { 78 | i, err := strconv.Atoi(v) 79 | if err != nil { 80 | return nil, fmt.Errorf("MAX_EMPHASIS_NEW_LINES: %v %w", v, err) 81 | } 82 | orgConfig.MaxEmphasisNewLines = i 83 | delete(m, "MAX_EMPHASIS_NEW_LINES") 84 | } 85 | 86 | for k, v := range m { 87 | if k == "OPTIONS" { 88 | orgConfig.DefaultSettings[k] = v + " " + orgConfig.DefaultSettings[k] 89 | } else { 90 | orgConfig.DefaultSettings[k] = v 91 | } 92 | } 93 | 94 | config := &Config{ 95 | ConfigFile: configFile, 96 | ContentDir: filepath.Join(workingDir, contentDir), 97 | PublicDir: filepath.Join(workingDir, publicDir), 98 | Address: address, 99 | BaseUrl: baseUrl, 100 | Template: template.New("_").Funcs(TemplateFuncs), 101 | OrgConfig: orgConfig, 102 | } 103 | for name, node := range document.NamedNodes { 104 | if block, ok := node.(org.Block); ok { 105 | if block.Parameters[0] != "html" { 106 | continue 107 | } 108 | if _, err := config.Template.New(name).Parse(org.String(block.Children...)); err != nil { 109 | return nil, err 110 | } 111 | } 112 | } 113 | return config, nil 114 | } 115 | 116 | func (c *Config) Serve() error { 117 | http.Handle("/", http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { 118 | if strings.HasSuffix(req.URL.Path, ".html") || strings.HasSuffix(req.URL.Path, "/") { 119 | start := time.Now() 120 | if c, err := ReadConfig(c.ConfigFile); err != nil { 121 | log.Fatal(err) 122 | } else { 123 | if err := c.Render(); err != nil { 124 | log.Fatal(err) 125 | } 126 | } 127 | log.Printf("render took %s", time.Since(start)) 128 | } 129 | http.ServeFile(res, req, filepath.Join(c.PublicDir, path.Clean(req.URL.Path))) 130 | })) 131 | log.Printf("listening on: %s", c.Address) 132 | return http.ListenAndServe(c.Address, nil) 133 | } 134 | 135 | func (c *Config) Render() error { 136 | if err := os.RemoveAll(c.PublicDir); err != nil { 137 | return err 138 | } 139 | if err := os.MkdirAll(c.PublicDir, os.ModePerm); err != nil { 140 | return err 141 | } 142 | pages, err := c.RenderContent() 143 | if err != nil { 144 | return err 145 | } 146 | return c.RenderLists(pages) 147 | } 148 | 149 | func (c *Config) RenderContent() ([]*Page, error) { 150 | pages := []*Page{} 151 | err := filepath.Walk(c.ContentDir, func(path string, info os.FileInfo, err error) error { 152 | if err != nil { 153 | return err 154 | } 155 | relPath, err := filepath.Rel(c.ContentDir, path) 156 | if err != nil { 157 | return err 158 | } 159 | publicPath := filepath.Join(c.PublicDir, relPath) 160 | publicInfo, err := os.Stat(publicPath) 161 | if err != nil && !os.IsNotExist(err) { 162 | return err 163 | } 164 | if info.IsDir() { 165 | return os.MkdirAll(publicPath, info.Mode()) 166 | } 167 | if filepath.Ext(path) != ".org" && (os.IsNotExist(err) || info.ModTime().After(publicInfo.ModTime())) { 168 | return os.Link(path, publicPath) 169 | } 170 | p, err := NewPage(c, path, info) 171 | if err != nil { 172 | return err 173 | } 174 | pages = append(pages, p) 175 | 176 | p.PermaLink = c.BaseUrl + relPath[:len(relPath)-len(".org")] + ".html" 177 | return p.Render(publicPath[:len(publicPath)-len(".org")] + ".html") 178 | }) 179 | sort.Slice(pages, func(i, j int) bool { return pages[i].Date.After(pages[j].Date) }) 180 | return pages, err 181 | } 182 | 183 | func (c *Config) RenderLists(pages []*Page) error { 184 | ms := toMap(c.OrgConfig.DefaultSettings, nil) 185 | ms["Pages"] = pages 186 | lists := map[string]map[string][]interface{}{"": {"": nil}} 187 | for _, p := range pages { 188 | if p.BufferSettings["DRAFT"] != "" { 189 | continue 190 | } 191 | mp := toMap(p.BufferSettings, p) 192 | if p.BufferSettings["DATE"] != "" { 193 | lists[""][""] = append(lists[""][""], mp) 194 | } 195 | for k, v := range p.BufferSettings { 196 | if strings.HasSuffix(k, "[]") { 197 | list := strings.ToLower(k[:len(k)-2]) 198 | if lists[list] == nil { 199 | lists[list] = map[string][]interface{}{} 200 | } 201 | for _, sublist := range strings.Fields(v) { 202 | lists[list][sublist] = append(lists[list][strings.ToLower(sublist)], mp) 203 | } 204 | } 205 | } 206 | } 207 | for list, sublists := range lists { 208 | for sublist, pages := range sublists { 209 | ms["Title"] = strings.Title(sublist) 210 | ms["Pages"] = pages 211 | if err := c.RenderList(list, sublist, ms); err != nil { 212 | return err 213 | } 214 | } 215 | } 216 | return nil 217 | } 218 | 219 | func (c *Config) RenderList(list, sublist string, m map[string]interface{}) error { 220 | t := c.Template.Lookup(list) 221 | if list == "" { 222 | m["Title"] = c.OrgConfig.DefaultSettings["TITLE"] 223 | t = c.Template.Lookup("index") 224 | } 225 | if t == nil { 226 | t = c.Template.Lookup("list") 227 | } 228 | if t == nil { 229 | return fmt.Errorf("cannot render list: neither template %s nor list", list) 230 | } 231 | path := filepath.Join(c.PublicDir, slugify(list), slugify(sublist)) 232 | if err := os.MkdirAll(path, os.ModePerm); err != nil { 233 | return err 234 | } 235 | f, err := os.Create(filepath.Join(path, "index.html")) 236 | if err != nil { 237 | return err 238 | } 239 | defer f.Close() 240 | return t.Execute(f, m) 241 | } 242 | -------------------------------------------------------------------------------- /blorg/config_test.go: -------------------------------------------------------------------------------- 1 | package blorg 2 | 3 | import ( 4 | "bufio" 5 | "crypto/md5" 6 | "encoding/hex" 7 | "io/fs" 8 | "io/ioutil" 9 | "os" 10 | "path/filepath" 11 | "strings" 12 | "testing" 13 | ) 14 | 15 | func TestBlorg(t *testing.T) { 16 | // Re-generate this file with `find testdata/public -type f | sort -u | xargs md5sum > testdata/public.md5` 17 | hashFile, err := os.Open("testdata/public.md5") 18 | if err != nil { 19 | t.Errorf("Could not open hash file: %s", err) 20 | return 21 | } 22 | defer hashFile.Close() 23 | scanner := bufio.NewScanner(hashFile) 24 | committedHashes := make(map[string]string) 25 | for scanner.Scan() { 26 | parts := strings.Fields(scanner.Text()) 27 | if len(parts) != 2 { 28 | t.Errorf("Could not split hash entry line in 2: len(parts)=%d", len(parts)) 29 | return 30 | } 31 | hash := parts[0] 32 | fileName := parts[1] 33 | committedHashes[fileName] = hash 34 | } 35 | if err := scanner.Err(); err != nil { 36 | t.Errorf("Failed to read hash file: %s", err) 37 | return 38 | } 39 | 40 | config, err := ReadConfig("testdata/blorg.org") 41 | if err != nil { 42 | t.Errorf("Could not read config: %s", err) 43 | return 44 | } 45 | if err := config.Render(); err != nil { 46 | t.Errorf("Could not render: %s", err) 47 | return 48 | } 49 | 50 | renderedFileHashes := make(map[string]string) 51 | err = filepath.WalkDir(config.PublicDir, func(path string, d fs.DirEntry, err error) error { 52 | if err != nil { 53 | return err 54 | } 55 | if d.IsDir() { 56 | return nil 57 | } 58 | data, err := ioutil.ReadFile(path) 59 | if err != nil { 60 | return err 61 | } 62 | hash := md5.Sum(data) 63 | renderedFileHashes[path] = hex.EncodeToString(hash[:]) 64 | return nil 65 | }) 66 | if err != nil { 67 | t.Errorf("Could not determine hashes of rendered files: %s", err) 68 | return 69 | } 70 | 71 | for file, rendered := range renderedFileHashes { 72 | if _, ok := committedHashes[file]; !ok { 73 | t.Errorf("New file %s does not have a committed hash", file) 74 | continue 75 | } 76 | committed := committedHashes[file] 77 | committedHashes[file] = "" // To check if there are missing files later. 78 | if rendered != committed { 79 | t.Errorf("PublicDir hashes do not match for %s: '%s' -> '%s'", file, committed, rendered) 80 | } 81 | } 82 | for file, committed := range committedHashes { 83 | if committed != "" { 84 | t.Errorf("Missing file %s has a committed hash, but was not rendered", file) 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /blorg/page.go: -------------------------------------------------------------------------------- 1 | package blorg 2 | 3 | import ( 4 | "fmt" 5 | "html/template" 6 | "os" 7 | "time" 8 | 9 | "github.com/niklasfasching/go-org/org" 10 | ) 11 | 12 | type Page struct { 13 | *Config 14 | Document *org.Document 15 | Info os.FileInfo 16 | PermaLink string 17 | Date time.Time 18 | Content template.HTML 19 | BufferSettings map[string]string 20 | } 21 | 22 | func NewPage(c *Config, path string, info os.FileInfo) (*Page, error) { 23 | f, err := os.Open(path) 24 | if err != nil { 25 | return nil, err 26 | } 27 | d := c.OrgConfig.Parse(f, path) 28 | content, err := d.Write(getWriter()) 29 | if err != nil { 30 | return nil, err 31 | } 32 | date, err := time.Parse("2006-01-02", d.Get("DATE")) 33 | if err != nil { 34 | date, _ = time.Parse("2006-01-02", "1970-01-01") 35 | } 36 | return &Page{ 37 | Config: c, 38 | Document: d, 39 | Info: info, 40 | Date: date, 41 | Content: template.HTML(content), 42 | BufferSettings: d.BufferSettings, 43 | }, nil 44 | } 45 | 46 | func (p *Page) Render(path string) error { 47 | if p.BufferSettings["DRAFT"] != "" { 48 | return nil 49 | } 50 | f, err := os.Create(path) 51 | if err != nil { 52 | return err 53 | } 54 | defer f.Close() 55 | templateName := "item" 56 | if v, ok := p.BufferSettings["TEMPLATE"]; ok { 57 | templateName = v 58 | } 59 | t := p.Template.Lookup(templateName) 60 | if t == nil { 61 | return fmt.Errorf("cannot render page %s: unknown template %s", p.Info.Name(), templateName) 62 | } 63 | return t.Execute(f, toMap(p.BufferSettings, p)) 64 | } 65 | 66 | func (p *Page) Summary() template.HTML { 67 | for _, n := range p.Document.Nodes { 68 | switch n := n.(type) { 69 | case org.Block: 70 | if n.Name == "SUMMARY" { 71 | w := getWriter() 72 | org.WriteNodes(w, n.Children...) 73 | return template.HTML(w.String()) 74 | } 75 | } 76 | } 77 | for i, n := range p.Document.Nodes { 78 | switch n.(type) { 79 | case org.Headline: 80 | w := getWriter() 81 | org.WriteNodes(w, p.Document.Nodes[:i]...) 82 | return template.HTML(w.String()) 83 | } 84 | } 85 | return "" 86 | } 87 | -------------------------------------------------------------------------------- /blorg/testdata/blorg.org: -------------------------------------------------------------------------------- 1 | #+AUTHOR: testdata 2 | #+TITLE: blorg 3 | #+BASE_URL: /go-org/blorg 4 | #+OPTIONS: toc:nil title:nil 5 | #+CONTENT: ./content 6 | #+PUBLIC: ./public 7 | 8 | * templates 9 | ** head 10 | #+name: head 11 | #+begin_src html 12 |
13 | 14 | 15 | 16 |Only pages that have a date will be listed here - e.g. not about.html 90 |
29 | Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod 30 | tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At 31 | vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd 32 | gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum 33 | dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor 34 | invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero 35 | eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no 36 | sea takimata sanctus est Lorem ipsum dolor sit amet.
37 | 38 |Only pages that have a date will be listed here - e.g. not about.html 21 |
29 | Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod 30 | tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At 31 | vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd 32 | gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum 33 | dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor 34 | invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero 35 | eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no 36 | sea takimata sanctus est Lorem ipsum dolor sit amet.
37 | 38 |31 | Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod 32 | tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At 33 | vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd 34 | gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum 35 | dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor 36 | invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero 37 | eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no 38 | sea takimata sanctus est Lorem ipsum dolor sit amet.
39 | 40 |%s", err)) 20 | } else { 21 | out.Set("innerHTML", html) 22 | } 23 | return nil 24 | })) 25 | 26 | select {} // stay alive 27 | } 28 | -------------------------------------------------------------------------------- /etc/example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niklasfasching/go-org/ed88fea76dc0ac4126e557d4e1d7a53547a6ce47/etc/example.png -------------------------------------------------------------------------------- /etc/generate-fixtures: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | org_files="$(ls org/testdata/*.org)" 4 | if [[ ! -z $1 ]]; then 5 | org_files="$1" 6 | else 7 | (cd blorg && find testdata/public -type f | sort -u | xargs md5sum > testdata/public.md5) 8 | fi 9 | 10 | for org_file in $org_files; do 11 | echo $org_file 12 | ./go-org render $org_file html > org/testdata/$(basename $org_file .org).html 13 | ./go-org render $org_file org > org/testdata/$(basename $org_file .org).pretty_org 14 | done 15 | -------------------------------------------------------------------------------- /etc/generate-gh-pages: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -eu 3 | 4 | examples_style=" 5 | .source { 6 | display: grid; 7 | grid-template-columns: 1fr 1fr; 8 | grid-gap: 1rem; 9 | } 10 | .org, .html { 11 | border: 1px dashed grey; 12 | padding: 1em; 13 | overflow-x: auto; 14 | } 15 | .sections { margin-left: 2rem; } 16 | .sections a { display: block; padding: 0.25em 0; } 17 | .sections a:hover, .sections a:focus, .sections a:active { background: rgba(200, 200, 200, 0.2); }" 18 | 19 | org_files=org/testdata/*.org 20 | go_org_examples=" 21 |
$(sed 's/&/\&/g; s/\</g; s/>/\>/g;' $org_file)37 |
2 | some results without a block 3 |4 |
8 | echo "a bash source block"
9 |
10 | function hello {
11 | echo Hello World!
12 | }
13 |
14 | hello
15 |
16 |
25 | a source block with leading newline, trailing newline characters
26 | and a line started
27 | with leading space
28 | and line leading tab.
29 |
30 |
35 | a source block without a language
36 |
37 |
42 | echo a source block with results
43 |
44 | 47 | a source block with results 48 |49 |
50 | a source block that only exports results 51 |52 |
53 | but the result block is 54 |55 |
56 | an example block with 57 | multiple lines including 58 | 59 | 60 | empty lines! 61 | 62 | it also has multiple parameters 63 | 64 | src, example & export blocks treat their content as raw text 65 | /inline/ *markup* is ignored 66 | and whitespace is honored and not removed 67 | 68 | content of example blocks is still html escaped - see <script>alert("escaped")</script> 69 |70 |
71 | examples like this 72 | are also supported 73 | 74 | note that /inline/ *markup* ignored 75 |76 |
77 |106 |Mongodb is webscale. (source: mongodb-is-web-scale)
78 |79 | blocks like the quote block parse their content and can contain
80 |81 |
105 |- lists
82 |- inline markup
83 |- 84 |
99 |tables
85 |86 | 87 |
98 |88 | 90 |foo 89 |91 | 93 |bar 92 |94 | 96 | 97 |baz 95 |- paragraphs
100 |- … 101 | 102 | whitespace is honored and not removed (but is not displayed because that's how html works by default) 103 | it can be made visible using css (e.g.
104 |white-space: pre
).
109 | #+BEGIN_SRC bash
110 | echo src (with language org) and example blocks support escaping using commata
111 | #+END_SRC
112 |
113 | ,* I am not a real headline - commata escape characters aren't renderered
114 |
115 | 118 | #+BEGIN_SRC bash 119 | echo src (with language org) and example blocks support escaping using commata 120 | #+END_SRC 121 | 122 | ,* I am not a real headline - commata escape characters aren't renderered 123 |124 | 127 |
list item 1 130 | blocks can contain unindented lines that would normally end a list item
131 |132 | this line is not indented - if it was outside of a block the list item would end 133 |134 |
135 |137 |this line is not indented - if it was outside of a block the list item would end
136 |
now we're outside the block again and the following unindented line will be outside of the list item
138 |this unindented line is outside of the list item
141 |list item 2
144 |
147 | #+BEGIN_EXAMPLE
148 |
149 | #+END_EXAMPLE
152 |153 |157 |154 | #+END_QUOTE 155 |156 |
verse blocks
160 |emacs / ox-hugo rendering
163 |
164 | Great clouds overhead
165 | Tiny black birds rise and fall
166 | Snow covers Emacs
167 |
168 | ---AlexSchroeder
169 |
go-org rendering
173 |
176 | <style>
177 | .verse-block p { white-space: pre; }
178 | .verse-block p + p { margin: 0; }
179 | </style>
180 |
181 | Great clouds overhead 189 | Tiny black birds rise and fall 190 | Snow covers Emacs
191 |192 | —AlexSchroeder
193 |
201 | describe <a b c>;
202 |
203 |
150 | Great clouds overhead
151 | Tiny black birds rise and fall
152 | Snow covers Emacs
153 |
154 | ---AlexSchroeder
155 |
150 | Great clouds overhead
151 | Tiny black birds rise and fall
152 | Snow covers Emacs
153 |
154 | ---AlexSchroeder
155 |
Anything can be captioned.
2 |
6 | echo "i have a caption!"
7 |
8 | 20 | note that the whole paragraph is captioned, so a linebreak is needed for images to caption correctly
21 |
23 | see?
2 | Line breaks between multi-byte characters are omitted when the ealb
option is set:
[\w-]
)and other elements
46 |like blocks
49 |
52 | other non-plain
53 |
54 | and tables
59 |1 | 63 |a | 64 |
2 | 67 |b | 68 |
3 | 71 |c | 72 |
85 | Footnotes break after two consecutive empty lines - just like paragraphs - see https://orgmode.org/worg/dev/org-syntax.html. 86 | This shouldn't happen when the definition line and the line after that are empty.
87 |yolo
93 |the inline footnote definition
99 |the anonymous inline footnote definition
105 |so this definition will not be at the end of this section in the exported document. 111 | Rather, it will be somewhere down below in the footnotes section.
112 |118 | There's multiple reasons for that. Among others, doing so requires i18n (to recognize the section) and silently 119 | hides content before and after the footnotes9.
120 |Footnotes can be linked from another footnote's definition.
126 |this test file just exists to reproduce a bug with footnotes in headlines - that only happens in very specific circumstances. 19 | The TLDR is:
20 |footnotes.add()
transparently gets translated to (&footnotes).add()
(docs).[1/2]
22 | [100%]
<
29 | (Org mode proper does the same)47 | we can link to headlines that define a custom_id: #this-will-be-the-id-of-the-headline
48 |Still outside the drawer
57 |This is inside the drawer
58 |Still outside the drawer
59 |it's possible to use #+SETUPFILE
- in this case the setup file contains the following
71 | #+TODO: TODO DONE CUSTOM
72 | #+EXCLUDE_TAGS: noexport custom_noexport
73 |
74 | :PROPERTIES: 84 | not a property
85 |:END:
86 |The toc option allows setting a level limit. For this file we set it to 1 - which means that the following headlines 94 | won't be included in the table of contents.
95 |you get the gist…
113 |Lines in a source block can be highlighted with :hl_lines
.
5 | (+ 1 2)
6 | (+ 1 2)
7 | (+ 1 2)
8 | (+ 1 2)
9 | (+ 1 2)
10 |
11 | explict line break
plus more emphasis
verbatim
code
18 | <h1>hello</h1>
19 |
20 |
24 | world
25 |
26 | multiline emphasis is
30 | supported - and respects MaxEmphasisNewLines (default: 1)
31 | so this
32 | is emphasized
33 |
34 | /but
35 | this
36 | is
37 | not emphasized/links
42 |\[[square brackets]\]
https://www.example.comtimestamps
57 |#+LINK
based links:
#+MACROs
:
org entities
83 |\pi
& \pi{}
=> π & π\angle{}
& \angle
& \ang
>
∠ ∠ ∠echo "a bash source block with custom html attributes"
10 |
11 | 18 | and an image with custom html attributes and a caption
19 |named paragraph
26 |
29 | named block
30 |
31 | #not a comment because there's no space after the hashtag
34 |A table of contents can be rendered anywhere in the document by using
42 |
45 | #+TOC: headlines $n
46 |
47 | Where $n
is the max headline lvl that will be included. You can use headlines 0
to include all headlines.
without latex delimiters the _{i=1}
in \sum_{i=1}^n a_n
is interpreted as subscript.
2 | we support \(...\)
, \[...\]
, $$...$$
and \begin{$env}...\end{$env}
as latex fragment delimiters.
unordered list item 2 - with inline
markup
ordered sublist item 1
12 |unordered list item 3 - and a link 26 | and some lines of text
27 |and another subitem
30 |
33 | echo with a block
34 |
35 | and another one with a table
40 |a | 44 |b | 45 |c | 46 |
---|---|---|
1 | 51 |2 | 52 |3 | 53 |
57 | and text with an empty line in between as well!
58 |unordered list item 4
63 |64 | with an example 65 | 66 | that spans multiple lines 67 |68 |
71 | descriptive lists
72 |92 | details on a new line (with an empty line in between) 93 | continued
94 |
97 | echo "Hello World!"
98 |
99 | some list termination tests
104 |neato!
5 | *** DONE [[https://github.com/chaseadamsio/goorgeous/issues/29][#29:]] Support verse block 6 | #+BEGIN_VERSE 7 | This 8 | *is* 9 | verse 10 | #+END_VERSE 11 | 12 | #+BEGIN_CUSTOM 13 | or even a *totally* /custom/ kind of block 14 | crazy ain't it? 15 | #+END_CUSTOM 16 | *** DONE [[https://github.com/chaseadamsio/goorgeous/issues/30][#30]]: Support #+SETUPFILE 17 | see =./headlines.org= 18 | *** DONE [[https://github.com/chaseadamsio/goorgeous/issues/31][#31]]: Support #+INCLUDE 19 | Note that only src/example/export block inclusion is supported for now. 20 | There's quite a lot more to include (see the [[https://orgmode.org/manual/Include-files.html][org manual for include files]]) but I 21 | don't have a use case for this yet and stuff like namespacing footnotes of included files 22 | adds quite a bit of complexity. 23 | 24 | for now files can be included as: 25 | - src block 26 | #+INCLUDE: "./headlines.org" src org 27 | - export block 28 | #+INCLUDE: "./paragraphs.html" export html 29 | - example block 30 | #+INCLUDE: "../../.github/workflows/ci.yml" example yaml 31 | *** DONE [[https://github.com/chaseadamsio/goorgeous/issues/33][#33]]: Wrong output when mixing html with Org mode 32 | #+HTML:neato!
5 | *** DONE [[https://github.com/chaseadamsio/goorgeous/issues/29][#29:]] Support verse block 6 | #+BEGIN_VERSE 7 | This 8 | *is* 9 | verse 10 | #+END_VERSE 11 | 12 | #+BEGIN_CUSTOM 13 | or even a *totally* /custom/ kind of block 14 | crazy ain't it? 15 | #+END_CUSTOM 16 | *** DONE [[https://github.com/chaseadamsio/goorgeous/issues/30][#30]]: Support #+SETUPFILE 17 | see =./headlines.org= 18 | *** DONE [[https://github.com/chaseadamsio/goorgeous/issues/31][#31]]: Support #+INCLUDE 19 | Note that only src/example/export block inclusion is supported for now. 20 | There's quite a lot more to include (see the [[https://orgmode.org/manual/Include-files.html][org manual for include files]]) but I 21 | don't have a use case for this yet and stuff like namespacing footnotes of included files 22 | adds quite a bit of complexity. 23 | 24 | for now files can be included as: 25 | - src block 26 | #+INCLUDE: "./headlines.org" src org 27 | - export block 28 | #+INCLUDE: "./paragraphs.html" export html 29 | - example block 30 | #+INCLUDE: "../../.github/workflows/ci.yml" example yaml 31 | *** DONE [[https://github.com/chaseadamsio/goorgeous/issues/33][#33]]: Wrong output when mixing html with Org mode 32 | #+HTML:#+OPTIONS:
toggles supported by go-org
6 | go-org
supports multiple export toggles as described in the export settings section of the Org mode manual.
9 | By default (most of?) those toggles are enabled. This file starts with #+OPTIONS: toc:nil f:nil e:nil
and thus
10 | disables the table of contents, footnotes & entities.
11 | That means, entities like ---
--- (mdash) will be left untouched, footnotes like [fn:1]
will
12 | not be exported and there won't be a table of contents at the top.
13 | As buffer options are merged with the defaults, the above headline will be exported with priority, todo status & tags.
key | 18 |description | 19 |
---|---|
f | 24 |Include footnotes (definitions & links) | 25 |
e | 28 |Include entities | 29 |
toc | 32 |Include table of contents (outline) | 33 |
pri | 38 |Include priority [#A] , [#B] , [#C] in headline title |
39 |
todo | 42 |Include todo status in headline title | 43 |
tags | 46 |Include tags in headline title | 47 |
ealb | 52 |Omit newlines between multi-byte characters (east asian line breaks, non-standard) | 53 |
Paragraphs are the default element.
2 |3 | Empty lines and other elements end paragraphs - but paragraphs 4 | can 5 | obviously 6 | span 7 | multiple 8 | lines.
9 |10 | Paragraphs can contain inline markup like emphasis strong and links example.com and stuff.
11 | -------------------------------------------------------------------------------- /org/testdata/paragraphs.org: -------------------------------------------------------------------------------- 1 | Paragraphs are the default element. 2 | 3 | Empty lines and other elements end paragraphs - but paragraphs 4 | can 5 | obviously 6 | span 7 | multiple 8 | lines. 9 | 10 | Paragraphs can contain inline markup like /emphasis/ *strong* and links [[https://www.example.com][example.com]] and stuff. 11 | 12 | -------------------------------------------------------------------------------- /org/testdata/paragraphs.pretty_org: -------------------------------------------------------------------------------- 1 | Paragraphs are the default element. 2 | 3 | Empty lines and other elements end paragraphs - but paragraphs 4 | can 5 | obviously 6 | span 7 | multiple 8 | lines. 9 | 10 | Paragraphs can contain inline markup like /emphasis/ *strong* and links [[https://www.example.com][example.com]] and stuff. 11 | 12 | -------------------------------------------------------------------------------- /org/testdata/setup_file_org: -------------------------------------------------------------------------------- 1 | #+TODO: TODO DONE CUSTOM 2 | #+EXCLUDE_TAGS: noexport custom_noexport 3 | -------------------------------------------------------------------------------- /org/testdata/tables.html: -------------------------------------------------------------------------------- 1 |a | 6 |b | 7 |c | 8 |
---|---|---|
1 | 13 |2 | 14 |3 | 15 |
a | 27 |b | 28 |c | 29 |
---|---|---|
1 | 34 |2 | 35 |3 | 36 |
Character | 48 |Org | 49 |Rendered HTML | 50 |
---|---|---|
Hyphen | 55 |a - b |
56 | a - b | 57 |
Ndash | 60 |a -- b |
61 | a – b | 62 |
Mdash | 65 |a --- b |
66 | a — b | 67 |
Ellipsis | 70 |a ... b |
71 | a … b | 72 |
1 | 84 |2 | 85 |3 | 86 |
1 | 98 |2 | 99 |3 | 100 |
left aligned | 112 |right aligned | 113 |center aligned | 114 |
---|---|---|
42 | 119 |42 | 120 |42 | 121 |
foobar | 124 |foobar | 125 |foobar | 126 |
long column a | 138 |long column b | 139 |long column c | 140 |
---|---|---|
1 | 145 |2 | 146 |3 | 147 |
a | 159 |b | 160 |c | 161 |
---|---|---|
1 | 166 |2 | 167 |3 | 168 |
. | 171 |. | 172 |. | 173 |
1 | 178 |2 | 179 |3 | 180 |
1 | 185 |2 | 186 |3 | 187 |