├── .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 | {{ .Title }} 17 | 18 | #+end_src 19 | ** header 20 | #+name: header 21 | #+begin_src html 22 |
23 | 24 | 27 |
28 | #+end_src 29 | ** item 30 | #+name: item 31 | #+begin_src html 32 | 33 | 34 | {{ template "head" . }} 35 | 36 | {{ template "header" . }} 37 |
38 |

{{ .Title }} 39 |
40 | {{ .Subtitle }} 41 |

42 | 47 | {{ .Content }} 48 |
49 | 50 | 51 | #+end_src 52 | 53 | ** list 54 | #+name: list 55 | #+begin_src html 56 | 57 | 58 | {{ template "head" . }} 59 | 60 | {{ template "header" . }} 61 |
62 |

{{ .Title }}

63 | 73 |
75 | 76 | 77 | #+end_src 78 | 79 | ** index 80 | #+name: index 81 | #+begin_src html 82 | 83 | 84 | {{ template "head" . }} 85 | 86 | {{ template "header" . }} 87 |
88 |

{{ .Title }}

89 |

Only pages that have a date will be listed here - e.g. not about.html 90 |

100 |
102 | 103 | 104 | #+end_src 105 | -------------------------------------------------------------------------------- /blorg/testdata/content/about.org: -------------------------------------------------------------------------------- 1 | #+TITLE: About 2 | #+TAGS[]: static 3 | 4 | This site is generated from [[https://github.com/niklasfasching/go-org/tree/master/blorg/testdata/content][go-org/blorg/testdata/content]] using the configuration in [[https://github.com/niklasfasching/go-org/blob/master/blorg/testdata/blorg.org][blorg.org]] 5 | 6 | #+INCLUDE: "../blorg.org" src org 7 | -------------------------------------------------------------------------------- /blorg/testdata/content/another-post.org: -------------------------------------------------------------------------------- 1 | #+TITLE: another post 2 | #+DATE: 2020-06-24 3 | #+TAGS[]: another 4 | 5 | Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod 6 | tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At 7 | vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd 8 | gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum 9 | dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor 10 | invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero 11 | eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no 12 | sea takimata sanctus est Lorem ipsum dolor sit amet. 13 | -------------------------------------------------------------------------------- /blorg/testdata/content/some-post.org: -------------------------------------------------------------------------------- 1 | #+TITLE: some post 2 | #+DATE: 2020-06-23 3 | #+TAGS[]: some 4 | 5 | Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod 6 | tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At 7 | vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd 8 | gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum 9 | dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor 10 | invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero 11 | eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no 12 | sea takimata sanctus est Lorem ipsum dolor sit amet. 13 | -------------------------------------------------------------------------------- /blorg/testdata/content/style.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | margin: 0; 4 | padding: 0; 5 | } 6 | 7 | html { 8 | overflow-y: scroll; 9 | height: 100%; 10 | font: 100%/1.5 sans-serif; 11 | word-wrap: break-word; 12 | margin: 0 auto; 13 | padding: 1.5em; 14 | } 15 | 16 | @media (min-width: 768px) { 17 | html { 18 | font-size: 125%; 19 | max-width: 42em; 20 | } } 21 | 22 | h1, h2, h3, h4 { 23 | margin: 2.5rem 0 1.5rem 0; 24 | line-height: 1.25; 25 | color: #333; 26 | } 27 | 28 | a { 29 | color: #fa6432; 30 | text-decoration: none; 31 | } 32 | a:hover, a:focus, a:active { 33 | text-decoration: underline; 34 | } 35 | 36 | p { 37 | margin: 1em 0; 38 | line-height: 1.5; 39 | } 40 | p code { 41 | background-color: #eee; 42 | padding: 0.05em 0.2em; 43 | border: 1px solid #ccc; 44 | } 45 | 46 | ol, ul { 47 | margin: 1em; 48 | } 49 | ol li ol, ol li ul, ul li ol, ul li ul { 50 | margin: 0 2em; 51 | } 52 | ol li p, ul li p { 53 | margin: 0; 54 | } 55 | 56 | dl { 57 | font-family: monospace, monospace; 58 | } 59 | dl dt { 60 | font-weight: bold; 61 | } 62 | dl dd { 63 | margin: 1em; 64 | } 65 | 66 | img { 67 | max-width: 100%; 68 | display: block; 69 | margin: 0 auto; 70 | padding: 0.5em; 71 | } 72 | 73 | blockquote { 74 | padding-left: 1em; 75 | font-style: italic; 76 | border-left: solid 1px #fa6432; 77 | } 78 | 79 | table { 80 | font-size: 1rem; 81 | text-align: left; 82 | caption-side: bottom; 83 | margin-bottom: 2em; 84 | } 85 | table * { 86 | border: none; 87 | } 88 | table thead, table tr { 89 | display: table; 90 | table-layout: fixed; 91 | width: 100%; 92 | } 93 | table tr:nth-child(even) { 94 | background-color: rgba(200, 200, 200, 0.2); 95 | } 96 | table tbody { 97 | display: block; 98 | max-height: 70vh; 99 | overflow-y: auto; 100 | } 101 | table td, table th { 102 | padding: 0.25em; 103 | } 104 | 105 | table, .highlight > pre, pre.example { 106 | max-height: 70vh; 107 | margin: 1em 0; 108 | padding: 1em; 109 | overflow: auto; 110 | font-size: 0.85rem; 111 | font-family: monospace, monospace; 112 | border: 1px dashed rgba(250, 100, 50, 0.5); 113 | } 114 | 115 | .title { 116 | font-size: 2.5em; 117 | } 118 | 119 | .subtitle { 120 | font-weight: normal; 121 | font-size: 0.75em; 122 | color: #666; 123 | } 124 | 125 | .tags { 126 | margin-top: -1.5rem; 127 | padding-bottom: 1.5em; 128 | } 129 | .tags li { 130 | display: inline; 131 | margin-right: 0.5em; 132 | } 133 | 134 | figure { 135 | margin: 1em 0; 136 | } 137 | figure figcaption { 138 | font-family: monospace, monospace; 139 | font-size: 0.75em; 140 | text-align: center; 141 | color: grey; 142 | } 143 | 144 | .footnote-definition sup { 145 | margin-left: -1.5em; 146 | float: left; 147 | } 148 | 149 | .footnote-definition .footnote-body { 150 | margin: 1em 0; 151 | padding: 0 1em; 152 | border: 1px dashed rgba(250, 100, 50, 0.3); 153 | background-color: rgba(200, 200, 200, 0.2); 154 | } 155 | .footnote-definition .footnote-body p:only-child { 156 | margin: 0.2em 0; 157 | } 158 | 159 | header { 160 | display: flex; 161 | justify-content: space-between; 162 | } 163 | header nav { 164 | display: flex; 165 | align-items: center; 166 | justify-content: space-between; 167 | } 168 | header a + a { 169 | margin-left: 1rem; 170 | } 171 | 172 | .posts { 173 | margin: 0; 174 | list-style: none; 175 | } 176 | .posts .post a { 177 | display: block; 178 | padding: 0.5em 0; 179 | color: black; 180 | } 181 | .posts .post a:hover, .posts .post a:focus, .posts .post a:active { 182 | text-decoration: none; 183 | background: rgba(200, 200, 200, 0.2); 184 | } 185 | .posts .post date { 186 | font-family: monospace, monospace; 187 | font-size: 0.8rem; 188 | vertical-align: middle; 189 | padding-right: 2rem; 190 | color: grey; 191 | } 192 | -------------------------------------------------------------------------------- /blorg/testdata/content/yet-another-post/index.org: -------------------------------------------------------------------------------- 1 | #+TITLE: yet another post 2 | #+DATE: 2020-06-25 3 | #+TAGS[]: yet another 4 | 5 | Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod 6 | tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At 7 | vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd 8 | gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum 9 | dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor 10 | invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero 11 | eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no 12 | sea takimata sanctus est Lorem ipsum dolor sit amet. 13 | -------------------------------------------------------------------------------- /blorg/testdata/public.md5: -------------------------------------------------------------------------------- 1 | ce7dde7ff21d19e562c00553cfae3b2d testdata/public/about.html 2 | b93d8331258932e6bb18d866329b5e4e testdata/public/another-post.html 3 | a4e5753838107f8cf44f8dfabc577c04 testdata/public/index.html 4 | 6e770ea67bb154191530585cc60c8c2f testdata/public/some-post.html 5 | 7a893b0b9b90974cd7d26cfcb0a22dd4 testdata/public/style.css 6 | 3ac91ccf813551d639daed7fae8689ae testdata/public/tags/another/index.html 7 | f780413c40454793f50722300113f234 testdata/public/tags/some/index.html 8 | 967686d0550349659b60012f2449ef92 testdata/public/tags/static/index.html 9 | 2180a6f960a21f02c41028b2a2d418d6 testdata/public/tags/yet/index.html 10 | be670bbbac7aec31b8f1563d944ea1ab testdata/public/yet-another-post/index.html 11 | -------------------------------------------------------------------------------- /blorg/testdata/public/another-post.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | another post 8 | 9 | 10 | 11 |
12 | 13 | 16 |
17 | 18 |
19 |

another post 20 |
21 | 22 |

23 | 28 |

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 |
39 | 40 | 41 | -------------------------------------------------------------------------------- /blorg/testdata/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | blorg 8 | 9 | 10 | 11 |
12 | 13 | 16 |
17 | 18 |
19 |

blorg

20 |

Only pages that have a date will be listed here - e.g. not about.html 21 |

45 |
47 | 48 | 49 | -------------------------------------------------------------------------------- /blorg/testdata/public/some-post.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | some post 8 | 9 | 10 | 11 |
12 | 13 | 16 |
17 | 18 |
19 |

some post 20 |
21 | 22 |

23 | 28 |

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 |
39 | 40 | 41 | -------------------------------------------------------------------------------- /blorg/testdata/public/style.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | margin: 0; 4 | padding: 0; 5 | } 6 | 7 | html { 8 | overflow-y: scroll; 9 | height: 100%; 10 | font: 100%/1.5 sans-serif; 11 | word-wrap: break-word; 12 | margin: 0 auto; 13 | padding: 1.5em; 14 | } 15 | 16 | @media (min-width: 768px) { 17 | html { 18 | font-size: 125%; 19 | max-width: 42em; 20 | } } 21 | 22 | h1, h2, h3, h4 { 23 | margin: 2.5rem 0 1.5rem 0; 24 | line-height: 1.25; 25 | color: #333; 26 | } 27 | 28 | a { 29 | color: #fa6432; 30 | text-decoration: none; 31 | } 32 | a:hover, a:focus, a:active { 33 | text-decoration: underline; 34 | } 35 | 36 | p { 37 | margin: 1em 0; 38 | line-height: 1.5; 39 | } 40 | p code { 41 | background-color: #eee; 42 | padding: 0.05em 0.2em; 43 | border: 1px solid #ccc; 44 | } 45 | 46 | ol, ul { 47 | margin: 1em; 48 | } 49 | ol li ol, ol li ul, ul li ol, ul li ul { 50 | margin: 0 2em; 51 | } 52 | ol li p, ul li p { 53 | margin: 0; 54 | } 55 | 56 | dl { 57 | font-family: monospace, monospace; 58 | } 59 | dl dt { 60 | font-weight: bold; 61 | } 62 | dl dd { 63 | margin: 1em; 64 | } 65 | 66 | img { 67 | max-width: 100%; 68 | display: block; 69 | margin: 0 auto; 70 | padding: 0.5em; 71 | } 72 | 73 | blockquote { 74 | padding-left: 1em; 75 | font-style: italic; 76 | border-left: solid 1px #fa6432; 77 | } 78 | 79 | table { 80 | font-size: 1rem; 81 | text-align: left; 82 | caption-side: bottom; 83 | margin-bottom: 2em; 84 | } 85 | table * { 86 | border: none; 87 | } 88 | table thead, table tr { 89 | display: table; 90 | table-layout: fixed; 91 | width: 100%; 92 | } 93 | table tr:nth-child(even) { 94 | background-color: rgba(200, 200, 200, 0.2); 95 | } 96 | table tbody { 97 | display: block; 98 | max-height: 70vh; 99 | overflow-y: auto; 100 | } 101 | table td, table th { 102 | padding: 0.25em; 103 | } 104 | 105 | table, .highlight > pre, pre.example { 106 | max-height: 70vh; 107 | margin: 1em 0; 108 | padding: 1em; 109 | overflow: auto; 110 | font-size: 0.85rem; 111 | font-family: monospace, monospace; 112 | border: 1px dashed rgba(250, 100, 50, 0.5); 113 | } 114 | 115 | .title { 116 | font-size: 2.5em; 117 | } 118 | 119 | .subtitle { 120 | font-weight: normal; 121 | font-size: 0.75em; 122 | color: #666; 123 | } 124 | 125 | .tags { 126 | margin-top: -1.5rem; 127 | padding-bottom: 1.5em; 128 | } 129 | .tags li { 130 | display: inline; 131 | margin-right: 0.5em; 132 | } 133 | 134 | figure { 135 | margin: 1em 0; 136 | } 137 | figure figcaption { 138 | font-family: monospace, monospace; 139 | font-size: 0.75em; 140 | text-align: center; 141 | color: grey; 142 | } 143 | 144 | .footnote-definition sup { 145 | margin-left: -1.5em; 146 | float: left; 147 | } 148 | 149 | .footnote-definition .footnote-body { 150 | margin: 1em 0; 151 | padding: 0 1em; 152 | border: 1px dashed rgba(250, 100, 50, 0.3); 153 | background-color: rgba(200, 200, 200, 0.2); 154 | } 155 | .footnote-definition .footnote-body p:only-child { 156 | margin: 0.2em 0; 157 | } 158 | 159 | header { 160 | display: flex; 161 | justify-content: space-between; 162 | } 163 | header nav { 164 | display: flex; 165 | align-items: center; 166 | justify-content: space-between; 167 | } 168 | header a + a { 169 | margin-left: 1rem; 170 | } 171 | 172 | .posts { 173 | margin: 0; 174 | list-style: none; 175 | } 176 | .posts .post a { 177 | display: block; 178 | padding: 0.5em 0; 179 | color: black; 180 | } 181 | .posts .post a:hover, .posts .post a:focus, .posts .post a:active { 182 | text-decoration: none; 183 | background: rgba(200, 200, 200, 0.2); 184 | } 185 | .posts .post date { 186 | font-family: monospace, monospace; 187 | font-size: 0.8rem; 188 | vertical-align: middle; 189 | padding-right: 2rem; 190 | color: grey; 191 | } 192 | -------------------------------------------------------------------------------- /blorg/testdata/public/tags/another/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Another 8 | 9 | 10 | 11 |
12 | 13 | 16 |
17 | 18 |
19 |

Another

20 | 37 |
39 | 40 | 41 | -------------------------------------------------------------------------------- /blorg/testdata/public/tags/some/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Some 8 | 9 | 10 | 11 |
12 | 13 | 16 |
17 | 18 |
19 |

Some

20 | 30 |
32 | 33 | 34 | -------------------------------------------------------------------------------- /blorg/testdata/public/tags/static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Static 8 | 9 | 10 | 11 |
12 | 13 | 16 |
17 | 18 |
19 |

Static

20 | 30 |
32 | 33 | 34 | -------------------------------------------------------------------------------- /blorg/testdata/public/tags/yet/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Yet 8 | 9 | 10 | 11 |
12 | 13 | 16 |
17 | 18 |
19 |

Yet

20 | 30 |
32 | 33 | 34 | -------------------------------------------------------------------------------- /blorg/testdata/public/yet-another-post/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | yet another post 8 | 9 | 10 | 11 |
12 | 13 | 16 |
17 | 18 |
19 |

yet another post 20 |
21 | 22 |

23 | 30 |

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 |
41 | 42 | 43 | -------------------------------------------------------------------------------- /blorg/util.go: -------------------------------------------------------------------------------- 1 | package blorg 2 | 3 | import ( 4 | "reflect" 5 | "regexp" 6 | "strings" 7 | 8 | "github.com/alecthomas/chroma/v2" 9 | "github.com/alecthomas/chroma/v2/formatters/html" 10 | "github.com/alecthomas/chroma/v2/lexers" 11 | "github.com/alecthomas/chroma/v2/styles" 12 | "github.com/niklasfasching/go-org/org" 13 | ) 14 | 15 | var snakeCaseRegexp = regexp.MustCompile(`(^[A-Za-z])|_([A-Za-z])`) 16 | var whitespaceRegexp = regexp.MustCompile(`\s+`) 17 | var nonWordCharRegexp = regexp.MustCompile(`[^\w-]`) 18 | 19 | func toMap(bufferSettings map[string]string, x interface{}) map[string]interface{} { 20 | m := map[string]interface{}{} 21 | for k, v := range bufferSettings { 22 | k = toCamelCase(k) 23 | if strings.HasSuffix(k, "[]") { 24 | m[k[:len(k)-2]] = strings.Fields(v) 25 | } else { 26 | m[k] = v 27 | } 28 | } 29 | if x == nil { 30 | return m 31 | } 32 | v := reflect.ValueOf(x).Elem() 33 | for i := 0; i < v.NumField(); i++ { 34 | m[v.Type().Field(i).Name] = v.Field(i).Interface() 35 | } 36 | return m 37 | } 38 | 39 | func toCamelCase(s string) string { 40 | return snakeCaseRegexp.ReplaceAllStringFunc(strings.ToLower(s), func(s string) string { 41 | return strings.ToUpper(strings.Replace(s, "_", "", -1)) 42 | }) 43 | } 44 | 45 | func slugify(s string) string { 46 | s = strings.ToLower(s) 47 | s = whitespaceRegexp.ReplaceAllString(s, "-") 48 | s = nonWordCharRegexp.ReplaceAllString(s, "") 49 | return strings.Trim(s, "-") 50 | } 51 | 52 | func getWriter() org.Writer { 53 | w := org.NewHTMLWriter() 54 | w.HighlightCodeBlock = highlightCodeBlock 55 | return w 56 | } 57 | 58 | func highlightCodeBlock(source, lang string, inline bool, params map[string]string) string { 59 | var w strings.Builder 60 | l := lexers.Get(lang) 61 | if l == nil { 62 | l = lexers.Fallback 63 | } 64 | l = chroma.Coalesce(l) 65 | it, _ := l.Tokenise(nil, source) 66 | options := []html.Option{} 67 | if params[":hl_lines"] != "" { 68 | ranges := org.ParseRanges(params[":hl_lines"]) 69 | if ranges != nil { 70 | options = append(options, html.HighlightLines(ranges)) 71 | } 72 | } 73 | _ = html.New(options...).Format(&w, styles.Get("github"), it) 74 | if inline { 75 | return `
` + "\n" + w.String() + "\n" + `
` 76 | } 77 | return `
` + "\n" + w.String() + "\n" + `
` 78 | } 79 | -------------------------------------------------------------------------------- /etc/_wasm.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "syscall/js" 7 | 8 | "github.com/niklasfasching/go-org/org" 9 | ) 10 | 11 | func main() { 12 | js.Global().Call("initialized") 13 | doc := js.Global().Get("document") 14 | in, out := doc.Call("getElementById", "input"), doc.Call("getElementById", "output") 15 | js.Global().Set("run", js.FuncOf(func(js.Value, []js.Value) interface{} { 16 | in := strings.NewReader(in.Get("value").String()) 17 | html, err := org.New().Parse(in, "").Write(org.NewHTMLWriter()) 18 | if err != nil { 19 | out.Set("innerHTML", fmt.Sprintf("
%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 |

Sections

22 | 23 |
" 29 | 30 | for org_file in $org_files; do 31 | echo generating content for $org_file 32 | name=$(basename $org_file) 33 | go_org_examples+=" 34 |

${name}

35 |
36 |
$(sed 's/&/\&/g; s//\>/g;' $org_file)
37 |
$(./go-org render $org_file html-chroma)
38 |
" 39 | done 40 | 41 | convert=" 42 |

Blorg

43 | example blorg output 44 |

Convert

45 | 48 | or ctrl + return 49 | 50 |
51 | 84 | 85 | " 108 | 109 | index=" 110 | 111 | 112 | 113 | 114 | $convert 115 | $go_org_examples 116 | 117 | " 118 | 119 | 120 | convert=" 121 | 122 | 123 | 124 | 125 | $convert 126 | 127 | " 128 | 129 | rm -rf docs 130 | mkdir docs 131 | 132 | echo "$index" > docs/index.html 133 | echo "$convert" > docs/convert.html 134 | cp etc/_wasm.go docs/wasm.go 135 | GOOS=js GOARCH=wasm go build -o docs/main.wasm docs/wasm.go 136 | cp $(go env GOROOT)/lib/wasm/wasm_exec.js docs/wasm_exec.js 137 | 138 | mkdir -p docs/blorg 139 | cp -r blorg/testdata/public/* docs/blorg/ 140 | -------------------------------------------------------------------------------- /etc/githooks/pre-push: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | make test -------------------------------------------------------------------------------- /etc/push-hugo-branch: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # requires: push access to the hugo repo in your go path (i.e. your fork must be origin) 4 | 5 | tag=$(git tag | tail -1) 6 | 7 | cd ../../gohugoio/hugo 8 | hugo_tag=$(grep github.com/niklasfasching/go-org go.sum | head -1 | awk '{print $2}') 9 | 10 | if [[ "$tag" == "$hugo_tag" ]]; then 11 | echo "tag $tag already exists in hugo" 12 | exit 1 13 | fi 14 | 15 | git checkout -b "update-go-org-to-$tag" origin/master || git reset --hard origin/master 16 | git pull 17 | go get "github.com/niklasfasching/go-org@$tag" || exit 1 18 | sed -i "\_github.com/niklasfasching/go-org ${hugo_tag}_d" go.sum 19 | git commit -a -m "deps: Update go-org to $tag" 20 | git push --force-with-lease 21 | -------------------------------------------------------------------------------- /etc/style.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | margin: 0; 4 | padding: 0; } 5 | 6 | html { 7 | font: 100%/1.5 sans-serif; 8 | word-wrap: break-word; 9 | padding: 1.5em; } 10 | 11 | @media (min-width: 768px) { 12 | html { font-size: 125%; } } 13 | 14 | h1, h2, h3, h4 { 15 | margin: 2.5rem 0 1.5rem 0; 16 | line-height: 1.25; } 17 | 18 | .title { 19 | font-size: 2.5em; } 20 | 21 | .subtitle { 22 | font-weight: normal; 23 | font-size: 0.75em; 24 | color: #666; } 25 | 26 | a { 27 | color: #fa6432; 28 | text-decoration: none; } 29 | a:hover, a:focus, a:active { 30 | text-decoration: underline; } 31 | 32 | p { 33 | margin: 1em 0; } 34 | p code { 35 | background-color: #eee; 36 | padding: 0.05em 0.2em; 37 | border: 1px solid #ccc; } 38 | 39 | ol, ul { 40 | margin: 1em; } 41 | ol li ol, ol li ul, ul li ol, ul li ul { 42 | margin: 0 2em; } 43 | ol li p, ul li p { 44 | margin: 0; } 45 | 46 | img { 47 | max-width: 100%; 48 | display: block; 49 | margin: 0 auto; 50 | padding: 0.5em; } 51 | 52 | blockquote { 53 | padding-left: 1em; 54 | font-style: italic; 55 | border-left: solid 1px #fa6432; } 56 | 57 | table { 58 | font-family: monospace, monospace; /* https://github.com/necolas/normalize.css#extended-details-and-known-issues */ 59 | font-size: 1rem; 60 | text-align: left; 61 | caption-side: bottom; 62 | margin-bottom: 2em; } 63 | table * { 64 | border: none; } 65 | table thead, table tr { 66 | display: table; 67 | table-layout: fixed; 68 | width: 100%; } 69 | table tr:nth-child(even) { 70 | background-color: #ccc; } 71 | table tbody { 72 | display: block; 73 | max-height: 70vh; 74 | overflow-y: auto; } 75 | table td, table th { 76 | padding: 0.25em; } 77 | 78 | table, .highlight > pre, pre.example { 79 | max-height: 70vh; 80 | margin: 1em 0; 81 | padding: 1em; 82 | overflow: auto; 83 | font-family: monospace, monospace; /* https://github.com/necolas/normalize.css#extended-details-and-known-issues */ 84 | font-size: 0.85rem; 85 | border: 1px solid rgba(250, 100, 50, 0.5); } 86 | 87 | figure { 88 | margin: 1em 0; 89 | } 90 | 91 | figcaption { 92 | font-family: monospace, monospace; /* https://github.com/necolas/normalize.css#extended-details-and-known-issues */ 93 | font-size: 0.75em; 94 | text-align: center; 95 | color: grey; } 96 | 97 | .footnote-definition sup { 98 | float: left; } 99 | 100 | .footnote-definition .footnote-body { 101 | margin: 1em 0; 102 | padding: 0 1em; 103 | border: 1px solid rgba(250, 100, 50, 0.3); 104 | background-color: #ccc; } 105 | .footnote-definition .footnote-body p:only-child { 106 | margin: 0.2em 0; } 107 | 108 | .align-left { text-align: left; } 109 | .align-center { text-align: center; } 110 | .align-right { text-align: right; } 111 | 112 | 113 | dl { font-family: monospace, monospace; } 114 | dl > dt { font-weight: bold; } 115 | dl > dd { margin: 1em; } 116 | 117 | .todo, .priority, .tags { 118 | font-size: 0.8em; 119 | color: lightgrey; 120 | } 121 | 122 | .timestamp { 123 | background-color: #eee; 124 | padding: 0.05em 0.2em; 125 | border: 1px solid #ccc; } 126 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/niklasfasching/go-org 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.24.2 6 | 7 | require ( 8 | github.com/alecthomas/chroma/v2 v2.5.0 9 | github.com/pmezard/go-difflib v1.0.0 10 | golang.org/x/net v0.38.0 11 | ) 12 | 13 | require github.com/dlclark/regexp2 v1.4.0 // indirect 14 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/alecthomas/assert/v2 v2.2.1 h1:XivOgYcduV98QCahG8T5XTezV5bylXe+lBxLG2K2ink= 2 | github.com/alecthomas/chroma/v2 v2.5.0 h1:CQCdj1BiBV17sD4Bd32b/Bzuiq/EqoNTrnIhyQAZ+Rk= 3 | github.com/alecthomas/chroma/v2 v2.5.0/go.mod h1:yrkMI9807G1ROx13fhe1v6PN2DDeaR73L3d+1nmYQtw= 4 | github.com/alecthomas/repr v0.2.0 h1:HAzS41CIzNW5syS8Mf9UwXhNH1J9aix/BvDRf1Ml2Yk= 5 | github.com/dlclark/regexp2 v1.4.0 h1:F1rxgk7p4uKjwIQxBs9oAXe5CqrXlCduYEJvrF4u93E= 6 | github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= 7 | github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= 8 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 9 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 10 | golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= 11 | golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= 12 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "io/ioutil" 7 | "log" 8 | "os" 9 | "runtime/debug" 10 | "strings" 11 | 12 | "github.com/alecthomas/chroma/v2" 13 | "github.com/alecthomas/chroma/v2/formatters/html" 14 | "github.com/alecthomas/chroma/v2/lexers" 15 | "github.com/alecthomas/chroma/v2/styles" 16 | "github.com/niklasfasching/go-org/blorg" 17 | "github.com/niklasfasching/go-org/org" 18 | ) 19 | 20 | var usage = `Usage: go-org COMMAND [ARGS]... 21 | Commands: 22 | - render [FILE] FORMAT 23 | FORMAT: org, html, html-chroma 24 | Instead of specifying a file, org mode content can also be passed on stdin 25 | - blorg 26 | - blorg init 27 | - blorg build 28 | - blorg serve 29 | ` 30 | 31 | func main() { 32 | log.SetFlags(0) 33 | if len(os.Args) < 2 { 34 | log.Fatal(usage) 35 | } 36 | switch cmd, args := os.Args[1], os.Args[2:]; cmd { 37 | case "render": 38 | render(args) 39 | case "blorg": 40 | runBlorg(args) 41 | case "version": 42 | printVersion() 43 | default: 44 | log.Fatal(usage) 45 | } 46 | } 47 | 48 | func runBlorg(args []string) { 49 | if len(args) == 0 { 50 | log.Fatal(usage) 51 | } 52 | switch strings.ToLower(args[0]) { 53 | case "init": 54 | if _, err := os.Stat(blorg.DefaultConfigFile); !os.IsNotExist(err) { 55 | log.Fatalf("%s already exists", blorg.DefaultConfigFile) 56 | } 57 | if err := ioutil.WriteFile(blorg.DefaultConfigFile, []byte(blorg.DefaultConfig), os.ModePerm); err != nil { 58 | log.Fatal(err) 59 | } 60 | if err := os.MkdirAll("content", os.ModePerm); err != nil { 61 | log.Fatal(err) 62 | } 63 | log.Println("./blorg.org and ./content/ created. Please adapt ./blorg.org") 64 | case "build": 65 | config, err := blorg.ReadConfig(blorg.DefaultConfigFile) 66 | if err != nil { 67 | log.Fatal(err) 68 | } 69 | if err := config.Render(); err != nil { 70 | log.Fatal(err) 71 | } 72 | log.Println("blorg build finished") 73 | case "serve": 74 | config, err := blorg.ReadConfig(blorg.DefaultConfigFile) 75 | if err != nil { 76 | log.Fatal(err) 77 | } 78 | log.Fatal(config.Serve()) 79 | default: 80 | log.Fatal(usage) 81 | } 82 | } 83 | 84 | func render(args []string) { 85 | r, path, format := io.Reader(nil), "", "" 86 | if fi, err := os.Stdin.Stat(); err != nil { 87 | log.Fatal(err) 88 | } else if len(args) == 2 { 89 | f, err := os.Open(args[0]) 90 | if err != nil { 91 | log.Fatal(err) 92 | } 93 | defer f.Close() 94 | r, path, format = f, args[0], args[1] 95 | } else if fi.Mode()&os.ModeCharDevice == 0 { 96 | r, path, format = os.Stdin, "./STDIN", args[0] 97 | } else { 98 | log.Fatal(usage) 99 | } 100 | d := org.New().Parse(r, path) 101 | write := func(w org.Writer) { 102 | out, err := d.Write(w) 103 | if err != nil { 104 | log.Fatal(err) 105 | } 106 | fmt.Fprint(os.Stdout, out) 107 | } 108 | switch strings.ToLower(format) { 109 | case "org": 110 | write(org.NewOrgWriter()) 111 | case "html": 112 | write(org.NewHTMLWriter()) 113 | case "html-chroma": 114 | writer := org.NewHTMLWriter() 115 | writer.HighlightCodeBlock = highlightCodeBlock 116 | write(writer) 117 | default: 118 | log.Fatal(usage) 119 | } 120 | } 121 | 122 | func highlightCodeBlock(source, lang string, inline bool, params map[string]string) string { 123 | var w strings.Builder 124 | l := lexers.Get(lang) 125 | if l == nil { 126 | l = lexers.Fallback 127 | } 128 | l = chroma.Coalesce(l) 129 | it, _ := l.Tokenise(nil, source) 130 | options := []html.Option{} 131 | if params[":hl_lines"] != "" { 132 | ranges := org.ParseRanges(params[":hl_lines"]) 133 | if ranges != nil { 134 | options = append(options, html.HighlightLines(ranges)) 135 | } 136 | } 137 | _ = html.New(options...).Format(&w, styles.Get("friendly"), it) 138 | if inline { 139 | return `
` + "\n" + w.String() + "\n" + `
` 140 | } 141 | return `
` + "\n" + w.String() + "\n" + `
` 142 | } 143 | 144 | func printVersion() { 145 | bi, ok := debug.ReadBuildInfo() 146 | if !ok { 147 | log.Fatal("not build info available") 148 | } 149 | revision, modified := "", false 150 | for _, s := range bi.Settings { 151 | if s.Key == "vcs.revision" { 152 | revision = s.Value 153 | } else if s.Key == "vcs.modified" { 154 | modified = s.Value == "true" 155 | } 156 | } 157 | log.Printf("%s (modified: %v)", revision, modified) 158 | } 159 | -------------------------------------------------------------------------------- /org/block.go: -------------------------------------------------------------------------------- 1 | package org 2 | 3 | import ( 4 | "math" 5 | "regexp" 6 | "strings" 7 | "unicode" 8 | ) 9 | 10 | type Block struct { 11 | Name string 12 | Parameters []string 13 | Children []Node 14 | Result Node 15 | } 16 | 17 | type Result struct { 18 | Node Node 19 | } 20 | 21 | type Example struct { 22 | Children []Node 23 | } 24 | 25 | type LatexBlock struct { 26 | Content []Node 27 | } 28 | 29 | var exampleLineRegexp = regexp.MustCompile(`^(\s*):(\s(.*)|\s*$)`) 30 | var beginBlockRegexp = regexp.MustCompile(`(?i)^(\s*)#\+BEGIN_(\w+)(.*)`) 31 | var endBlockRegexp = regexp.MustCompile(`(?i)^(\s*)#\+END_(\w+)`) 32 | var beginLatexBlockRegexp = regexp.MustCompile(`(?i)^(\s*)\\begin{([^}]+)}(\s*)$`) 33 | var endLatexBlockRegexp = regexp.MustCompile(`(?i)^(\s*)\\end{([^}]+)}(\s*)$`) 34 | var resultRegexp = regexp.MustCompile(`(?i)^(\s*)#\+RESULTS:`) 35 | var exampleBlockEscapeRegexp = regexp.MustCompile(`(^|\n)([ \t]*),([ \t]*)(\*|,\*|#\+|,#\+)`) 36 | 37 | func lexBlock(line string) (token, bool) { 38 | if m := beginBlockRegexp.FindStringSubmatch(line); m != nil { 39 | return token{"beginBlock", len(m[1]), strings.ToUpper(m[2]), m}, true 40 | } else if m := endBlockRegexp.FindStringSubmatch(line); m != nil { 41 | return token{"endBlock", len(m[1]), strings.ToUpper(m[2]), m}, true 42 | } 43 | return nilToken, false 44 | } 45 | 46 | func lexLatexBlock(line string) (token, bool) { 47 | if m := beginLatexBlockRegexp.FindStringSubmatch(line); m != nil { 48 | return token{"beginLatexBlock", len(m[1]), strings.ToUpper(m[2]), m}, true 49 | } else if m := endLatexBlockRegexp.FindStringSubmatch(line); m != nil { 50 | return token{"endLatexBlock", len(m[1]), strings.ToUpper(m[2]), m}, true 51 | } 52 | return nilToken, false 53 | } 54 | 55 | func lexResult(line string) (token, bool) { 56 | if m := resultRegexp.FindStringSubmatch(line); m != nil { 57 | return token{"result", len(m[1]), "", m}, true 58 | } 59 | return nilToken, false 60 | } 61 | 62 | func lexExample(line string) (token, bool) { 63 | if m := exampleLineRegexp.FindStringSubmatch(line); m != nil { 64 | return token{"example", len(m[1]), m[3], m}, true 65 | } 66 | return nilToken, false 67 | } 68 | 69 | func isRawTextBlock(name string) bool { return name == "SRC" || name == "EXAMPLE" || name == "EXPORT" } 70 | 71 | func (d *Document) parseBlock(i int, parentStop stopFn) (int, Node) { 72 | t, start := d.tokens[i], i 73 | name, parameters := t.content, splitParameters(t.matches[3]) 74 | trim := trimIndentUpTo(d.tokens[i].lvl) 75 | stop := func(d *Document, i int) bool { 76 | return i >= len(d.tokens) || (d.tokens[i].kind == "endBlock" && d.tokens[i].content == name) 77 | } 78 | block, i := Block{name, parameters, nil, nil}, i+1 79 | if isRawTextBlock(name) { 80 | rawText := "" 81 | for ; !stop(d, i); i++ { 82 | rawText += trim(d.tokens[i].matches[0]) + "\n" 83 | } 84 | if name == "EXAMPLE" || (name == "SRC" && len(parameters) >= 1 && parameters[0] == "org") { 85 | rawText = exampleBlockEscapeRegexp.ReplaceAllString(rawText, "$1$2$3$4") 86 | } 87 | block.Children = d.parseRawInline(rawText) 88 | } else { 89 | consumed, nodes := d.parseMany(i, stop) 90 | block.Children = nodes 91 | i += consumed 92 | } 93 | if i >= len(d.tokens) || d.tokens[i].kind != "endBlock" || d.tokens[i].content != name { 94 | return 0, nil 95 | } 96 | if name == "SRC" { 97 | consumed, result := d.parseSrcBlockResult(i+1, parentStop) 98 | block.Result = result 99 | i += consumed 100 | } 101 | return i + 1 - start, block 102 | } 103 | 104 | func (d *Document) parseLatexBlock(i int, parentStop stopFn) (int, Node) { 105 | t, start := d.tokens[i], i 106 | name, rawText, trim := t.content, "", trimIndentUpTo(int(math.Max((float64(d.baseLvl)), float64(t.lvl)))) 107 | stop := func(d *Document, i int) bool { 108 | return i >= len(d.tokens) || (d.tokens[i].kind == "endLatexBlock" && d.tokens[i].content == name) 109 | } 110 | for ; !stop(d, i); i++ { 111 | rawText += trim(d.tokens[i].matches[0]) + "\n" 112 | } 113 | if i >= len(d.tokens) || d.tokens[i].kind != "endLatexBlock" || d.tokens[i].content != name { 114 | return 0, nil 115 | } 116 | rawText += trim(d.tokens[i].matches[0]) 117 | return i + 1 - start, LatexBlock{d.parseRawInline(rawText)} 118 | } 119 | 120 | func (d *Document) parseSrcBlockResult(i int, parentStop stopFn) (int, Node) { 121 | start := i 122 | for ; !parentStop(d, i) && d.tokens[i].kind == "text" && d.tokens[i].content == ""; i++ { 123 | } 124 | if parentStop(d, i) || d.tokens[i].kind != "result" { 125 | return 0, nil 126 | } 127 | consumed, result := d.parseResult(i, parentStop) 128 | return (i - start) + consumed, result 129 | } 130 | 131 | func (d *Document) parseExample(i int, parentStop stopFn) (int, Node) { 132 | example, start := Example{}, i 133 | for ; !parentStop(d, i) && d.tokens[i].kind == "example"; i++ { 134 | example.Children = append(example.Children, Text{d.tokens[i].content, true}) 135 | } 136 | return i - start, example 137 | } 138 | 139 | func (d *Document) parseResult(i int, parentStop stopFn) (int, Node) { 140 | if i+1 >= len(d.tokens) { 141 | return 0, nil 142 | } 143 | consumed, node := d.parseOne(i+1, parentStop) 144 | return consumed + 1, Result{node} 145 | } 146 | 147 | func trimIndentUpTo(max int) func(string) string { 148 | return func(line string) string { 149 | i := 0 150 | for ; i < len(line) && i < max && unicode.IsSpace(rune(line[i])); i++ { 151 | } 152 | return line[i:] 153 | } 154 | } 155 | 156 | func splitParameters(s string) []string { 157 | parameters, parts := []string{}, strings.Split(s, " :") 158 | lang, rest := strings.TrimSpace(parts[0]), parts[1:] 159 | if lang != "" { 160 | xs := strings.Fields(lang) 161 | parameters = append(parameters, xs[0]) 162 | for i := 1; i < len(xs); i++ { 163 | k, v := xs[i], "" 164 | if i+1 < len(xs) && xs[i+1][0] != '-' { 165 | v, i = xs[i+1], i+1 166 | } 167 | parameters = append(parameters, k, v) 168 | } 169 | } 170 | for _, p := range rest { 171 | kv := strings.SplitN(p+" ", " ", 2) 172 | parameters = append(parameters, ":"+kv[0], strings.TrimSpace(kv[1])) 173 | } 174 | return parameters 175 | } 176 | 177 | func (b Block) ParameterMap() map[string]string { 178 | if len(b.Parameters) == 0 { 179 | return nil 180 | } 181 | m := map[string]string{":lang": b.Parameters[0]} 182 | for i := 1; i+1 < len(b.Parameters); i += 2 { 183 | m[b.Parameters[i]] = b.Parameters[i+1] 184 | } 185 | return m 186 | } 187 | 188 | func (n Example) String() string { return String(n) } 189 | func (n Block) String() string { return String(n) } 190 | func (n LatexBlock) String() string { return String(n) } 191 | func (n Result) String() string { return String(n) } 192 | -------------------------------------------------------------------------------- /org/document.go: -------------------------------------------------------------------------------- 1 | // Package org is an Org mode syntax processor. 2 | // 3 | // It parses plain text into an AST and can export it as HTML or pretty printed Org mode syntax. 4 | // Further export formats can be defined using the Writer interface. 5 | // 6 | // You probably want to start with something like this: 7 | // input := strings.NewReader("Your Org mode input") 8 | // html, err := org.New().Parse(input, "./").Write(org.NewHTMLWriter()) 9 | // if err != nil { 10 | // log.Fatalf("Something went wrong: %s", err) 11 | // } 12 | // log.Print(html) 13 | package org 14 | 15 | import ( 16 | "bufio" 17 | "fmt" 18 | "io" 19 | "io/ioutil" 20 | "log" 21 | "os" 22 | "strings" 23 | "sync" 24 | ) 25 | 26 | type Configuration struct { 27 | MaxEmphasisNewLines int // Maximum number of newlines inside an emphasis. See org-emphasis-regexp-components newline. 28 | AutoLink bool // Try to convert text passages that look like hyperlinks into hyperlinks. 29 | DefaultSettings map[string]string // Default values for settings that are overriden by setting the same key in BufferSettings. 30 | Log *log.Logger // Log is used to print warnings during parsing. 31 | ReadFile func(filename string) ([]byte, error) // ReadFile is used to read e.g. #+INCLUDE files. 32 | ResolveLink func(protocol string, description []Node, link string) Node 33 | } 34 | 35 | // Document contains the parsing results and a pointer to the Configuration. 36 | type Document struct { 37 | *Configuration 38 | Path string // Path of the file containing the parse input - used to resolve relative paths during parsing (e.g. INCLUDE). 39 | tokens []token 40 | baseLvl int 41 | Macros map[string]string 42 | Links map[string]string 43 | Nodes []Node 44 | NamedNodes map[string]Node 45 | Outline Outline // Outline is a Table Of Contents for the document and contains all sections (headline + content). 46 | BufferSettings map[string]string // Settings contains all settings that were parsed from keywords. 47 | Error error 48 | } 49 | 50 | // Node represents a parsed node of the document. 51 | type Node interface { 52 | String() string // String returns the pretty printed Org mode string for the node (see OrgWriter). 53 | } 54 | 55 | type lexFn = func(line string) (t token, ok bool) 56 | type parseFn = func(*Document, int, stopFn) (int, Node) 57 | type stopFn = func(*Document, int) bool 58 | 59 | type token struct { 60 | kind string 61 | lvl int 62 | content string 63 | matches []string 64 | } 65 | 66 | var lexFns = []lexFn{ 67 | lexHeadline, 68 | lexDrawer, 69 | lexBlock, 70 | lexResult, 71 | lexList, 72 | lexTable, 73 | lexHorizontalRule, 74 | lexKeywordOrComment, 75 | lexFootnoteDefinition, 76 | lexExample, 77 | lexLatexBlock, 78 | lexText, 79 | } 80 | 81 | var nilToken = token{"nil", -1, "", nil} 82 | var orgWriterMutex = sync.Mutex{} 83 | var orgWriter = NewOrgWriter() 84 | 85 | // New returns a new Configuration with (hopefully) sane defaults. 86 | func New() *Configuration { 87 | return &Configuration{ 88 | AutoLink: true, 89 | MaxEmphasisNewLines: 1, 90 | DefaultSettings: map[string]string{ 91 | "TODO": "TODO | DONE", 92 | "EXCLUDE_TAGS": "noexport", 93 | "OPTIONS": "toc:t <:t e:t f:t pri:t todo:t tags:t title:t ealb:nil", 94 | }, 95 | Log: log.New(os.Stderr, "go-org: ", 0), 96 | ReadFile: ioutil.ReadFile, 97 | ResolveLink: func(protocol string, description []Node, link string) Node { 98 | return RegularLink{protocol, description, link, false} 99 | }, 100 | } 101 | } 102 | 103 | // String returns the pretty printed Org mode string for the given nodes (see OrgWriter). 104 | func String(nodes ...Node) string { 105 | orgWriterMutex.Lock() 106 | defer orgWriterMutex.Unlock() 107 | return orgWriter.WriteNodesAsString(nodes...) 108 | } 109 | 110 | // Write is called after with an instance of the Writer interface to export a parsed Document into another format. 111 | func (d *Document) Write(w Writer) (out string, err error) { 112 | defer func() { 113 | if recovered := recover(); recovered != nil { 114 | err = fmt.Errorf("could not write output: %s", recovered) 115 | } 116 | }() 117 | if d.Error != nil { 118 | return "", d.Error 119 | } else if d.Nodes == nil { 120 | return "", fmt.Errorf("could not write output: parse was not called") 121 | } 122 | w.Before(d) 123 | WriteNodes(w, d.Nodes...) 124 | w.After(d) 125 | return w.String(), err 126 | } 127 | 128 | // Parse parses the input into an AST (and some other helpful fields like Outline). 129 | // To allow method chaining, errors are stored in document.Error rather than being returned. 130 | func (c *Configuration) Parse(input io.Reader, path string) (d *Document) { 131 | outlineSection := &Section{} 132 | d = &Document{ 133 | Configuration: c, 134 | Outline: Outline{outlineSection, outlineSection, 0}, 135 | BufferSettings: map[string]string{}, 136 | NamedNodes: map[string]Node{}, 137 | Links: map[string]string{}, 138 | Macros: map[string]string{}, 139 | Path: path, 140 | } 141 | defer func() { 142 | if recovered := recover(); recovered != nil { 143 | d.Error = fmt.Errorf("could not parse input: %v", recovered) 144 | } 145 | }() 146 | if d.tokens != nil { 147 | d.Error = fmt.Errorf("parse was called multiple times") 148 | } 149 | d.tokenize(input) 150 | _, nodes := d.parseMany(0, func(d *Document, i int) bool { return i >= len(d.tokens) }) 151 | d.Nodes = nodes 152 | return d 153 | } 154 | 155 | // Silent disables all logging of warnings during parsing. 156 | func (c *Configuration) Silent() *Configuration { 157 | c.Log = log.New(ioutil.Discard, "", 0) 158 | return c 159 | } 160 | 161 | func (d *Document) tokenize(input io.Reader) { 162 | d.tokens = []token{} 163 | scanner := bufio.NewScanner(input) 164 | for scanner.Scan() { 165 | d.tokens = append(d.tokens, tokenize(scanner.Text())) 166 | } 167 | if err := scanner.Err(); err != nil { 168 | d.Error = fmt.Errorf("could not tokenize input: %s", err) 169 | } 170 | } 171 | 172 | // Get returns the value for key in BufferSettings or DefaultSettings if key does not exist in the former 173 | func (d *Document) Get(key string) string { 174 | if v, ok := d.BufferSettings[key]; ok { 175 | return v 176 | } 177 | if v, ok := d.DefaultSettings[key]; ok { 178 | return v 179 | } 180 | return "" 181 | } 182 | 183 | // GetOption returns the value associated to the export option key 184 | // Currently supported options: 185 | // - < (export timestamps) 186 | // - e (export org entities) 187 | // - f (export footnotes) 188 | // - title (export title) 189 | // - toc (export table of content. an int limits the included org headline lvl) 190 | // - todo (export headline todo status) 191 | // - pri (export headline priority) 192 | // - tags (export headline tags) 193 | // - ealb (non-standard) (export with east asian line breaks / ignore line breaks between multi-byte characters) 194 | // see https://orgmode.org/manual/Export-Settings.html for more information 195 | func (d *Document) GetOption(key string) string { 196 | get := func(settings map[string]string) string { 197 | for _, field := range strings.Fields(settings["OPTIONS"]) { 198 | if strings.HasPrefix(field, key+":") { 199 | return field[len(key)+1:] 200 | } 201 | } 202 | return "" 203 | } 204 | value := get(d.BufferSettings) 205 | if value == "" { 206 | value = get(d.DefaultSettings) 207 | } 208 | if value == "" { 209 | value = "nil" 210 | d.Log.Printf("Missing value for export option %s", key) 211 | } 212 | return value 213 | } 214 | 215 | func (d *Document) parseOne(i int, stop stopFn) (consumed int, node Node) { 216 | switch d.tokens[i].kind { 217 | case "unorderedList", "orderedList": 218 | consumed, node = d.parseList(i, stop) 219 | case "tableRow", "tableSeparator": 220 | consumed, node = d.parseTable(i, stop) 221 | case "beginBlock": 222 | consumed, node = d.parseBlock(i, stop) 223 | case "beginLatexBlock": 224 | consumed, node = d.parseLatexBlock(i, stop) 225 | case "result": 226 | consumed, node = d.parseResult(i, stop) 227 | case "beginDrawer": 228 | consumed, node = d.parseDrawer(i, stop) 229 | case "text": 230 | consumed, node = d.parseParagraph(i, stop) 231 | case "example": 232 | consumed, node = d.parseExample(i, stop) 233 | case "horizontalRule": 234 | consumed, node = d.parseHorizontalRule(i, stop) 235 | case "comment": 236 | consumed, node = d.parseComment(i, stop) 237 | case "keyword": 238 | consumed, node = d.parseKeyword(i, stop) 239 | case "headline": 240 | consumed, node = d.parseHeadline(i, stop) 241 | case "footnoteDefinition": 242 | consumed, node = d.parseFootnoteDefinition(i, stop) 243 | } 244 | 245 | if consumed != 0 { 246 | return consumed, node 247 | } 248 | d.Log.Printf("Could not parse token %#v in file %s: Falling back to treating it as plain text.", d.tokens[i], d.Path) 249 | m := plainTextRegexp.FindStringSubmatch(d.tokens[i].matches[0]) 250 | d.tokens[i] = token{"text", len(m[1]), m[2], m} 251 | return d.parseOne(i, stop) 252 | } 253 | 254 | func (d *Document) parseMany(i int, stop stopFn) (int, []Node) { 255 | start, nodes := i, []Node{} 256 | for i < len(d.tokens) && !stop(d, i) { 257 | consumed, node := d.parseOne(i, stop) 258 | i += consumed 259 | nodes = append(nodes, node) 260 | } 261 | return i - start, nodes 262 | } 263 | 264 | func (d *Document) addHeadline(headline *Headline) int { 265 | current := &Section{Headline: headline} 266 | d.Outline.last.add(current) 267 | if !headline.IsExcluded(d) { 268 | d.Outline.count++ 269 | } 270 | d.Outline.last = current 271 | return d.Outline.count 272 | } 273 | 274 | func tokenize(line string) token { 275 | for _, lexFn := range lexFns { 276 | if token, ok := lexFn(line); ok { 277 | return token 278 | } 279 | } 280 | panic(fmt.Sprintf("could not lex line: %s", line)) 281 | } 282 | -------------------------------------------------------------------------------- /org/drawer.go: -------------------------------------------------------------------------------- 1 | package org 2 | 3 | import ( 4 | "regexp" 5 | "strings" 6 | ) 7 | 8 | type Drawer struct { 9 | Name string 10 | Children []Node 11 | } 12 | 13 | type PropertyDrawer struct { 14 | Properties [][]string 15 | } 16 | 17 | var beginDrawerRegexp = regexp.MustCompile(`^(\s*):(\S+):\s*$`) 18 | var endDrawerRegexp = regexp.MustCompile(`(?i)^(\s*):END:\s*$`) 19 | var propertyRegexp = regexp.MustCompile(`^(\s*):(\S+):(\s+(.*)$|$)`) 20 | 21 | func lexDrawer(line string) (token, bool) { 22 | if m := endDrawerRegexp.FindStringSubmatch(line); m != nil { 23 | return token{"endDrawer", len(m[1]), "", m}, true 24 | } else if m := beginDrawerRegexp.FindStringSubmatch(line); m != nil { 25 | return token{"beginDrawer", len(m[1]), strings.ToUpper(m[2]), m}, true 26 | } 27 | return nilToken, false 28 | } 29 | 30 | func (d *Document) parseDrawer(i int, parentStop stopFn) (int, Node) { 31 | name := strings.ToUpper(d.tokens[i].content) 32 | if name == "PROPERTIES" { 33 | return d.parsePropertyDrawer(i, parentStop) 34 | } 35 | drawer, start := Drawer{Name: name}, i 36 | i++ 37 | stop := func(d *Document, i int) bool { 38 | if parentStop(d, i) { 39 | return true 40 | } 41 | kind := d.tokens[i].kind 42 | return kind == "beginDrawer" || kind == "endDrawer" || kind == "headline" 43 | } 44 | for { 45 | consumed, nodes := d.parseMany(i, stop) 46 | i += consumed 47 | drawer.Children = append(drawer.Children, nodes...) 48 | if i < len(d.tokens) && d.tokens[i].kind == "beginDrawer" { 49 | p := Paragraph{[]Node{Text{":" + d.tokens[i].content + ":", false}}} 50 | drawer.Children = append(drawer.Children, p) 51 | i++ 52 | } else { 53 | break 54 | } 55 | } 56 | if i < len(d.tokens) && d.tokens[i].kind == "endDrawer" { 57 | i++ 58 | } 59 | return i - start, drawer 60 | } 61 | 62 | func (d *Document) parsePropertyDrawer(i int, parentStop stopFn) (int, Node) { 63 | drawer, start := PropertyDrawer{}, i 64 | i++ 65 | stop := func(d *Document, i int) bool { 66 | return parentStop(d, i) || (d.tokens[i].kind != "text" && d.tokens[i].kind != "beginDrawer") 67 | } 68 | for ; !stop(d, i); i++ { 69 | m := propertyRegexp.FindStringSubmatch(d.tokens[i].matches[0]) 70 | if m == nil { 71 | return 0, nil 72 | } 73 | k, v := strings.ToUpper(m[2]), strings.TrimSpace(m[4]) 74 | drawer.Properties = append(drawer.Properties, []string{k, v}) 75 | } 76 | if i < len(d.tokens) && d.tokens[i].kind == "endDrawer" { 77 | i++ 78 | } else { 79 | return 0, nil 80 | } 81 | return i - start, drawer 82 | } 83 | 84 | func (d *PropertyDrawer) Get(key string) (string, bool) { 85 | if d == nil { 86 | return "", false 87 | } 88 | for _, kvPair := range d.Properties { 89 | if kvPair[0] == key { 90 | return kvPair[1], true 91 | } 92 | } 93 | return "", false 94 | } 95 | 96 | func (n Drawer) String() string { return String(n) } 97 | func (n PropertyDrawer) String() string { return String(n) } 98 | -------------------------------------------------------------------------------- /org/footnote.go: -------------------------------------------------------------------------------- 1 | package org 2 | 3 | import ( 4 | "regexp" 5 | ) 6 | 7 | type FootnoteDefinition struct { 8 | Name string 9 | Children []Node 10 | Inline bool 11 | } 12 | 13 | var footnoteDefinitionRegexp = regexp.MustCompile(`^\[fn:([\w-]+)\](\s+(.+)|\s*$)`) 14 | 15 | func lexFootnoteDefinition(line string) (token, bool) { 16 | if m := footnoteDefinitionRegexp.FindStringSubmatch(line); m != nil { 17 | return token{"footnoteDefinition", 0, m[1], m}, true 18 | } 19 | return nilToken, false 20 | } 21 | 22 | func (d *Document) parseFootnoteDefinition(i int, parentStop stopFn) (int, Node) { 23 | start, name := i, d.tokens[i].content 24 | d.tokens[i] = tokenize(d.tokens[i].matches[2]) 25 | stop := func(d *Document, i int) bool { 26 | return parentStop(d, i) || 27 | (isSecondBlankLine(d, i) && i > start+1) || 28 | d.tokens[i].kind == "headline" || d.tokens[i].kind == "footnoteDefinition" 29 | } 30 | consumed, nodes := d.parseMany(i, stop) 31 | definition := FootnoteDefinition{name, nodes, false} 32 | return consumed, definition 33 | } 34 | 35 | func (n FootnoteDefinition) String() string { return String(n) } 36 | -------------------------------------------------------------------------------- /org/fuzz.go: -------------------------------------------------------------------------------- 1 | // +build gofuzz 2 | 3 | package org 4 | 5 | import ( 6 | "bytes" 7 | "strings" 8 | ) 9 | 10 | // Fuzz function to be used by https://github.com/dvyukov/go-fuzz 11 | func Fuzz(input []byte) int { 12 | conf := New().Silent() 13 | d := conf.Parse(bytes.NewReader(input), "") 14 | orgOutput, err := d.Write(NewOrgWriter()) 15 | if err != nil { 16 | panic(err) 17 | } 18 | htmlOutputA, err := d.Write(NewHTMLWriter()) 19 | if err != nil { 20 | panic(err) 21 | } 22 | htmlOutputB, err := conf.Parse(strings.NewReader(orgOutput), "").Write(NewHTMLWriter()) 23 | if htmlOutputA != htmlOutputB { 24 | panic("rendered org results in different html than original input") 25 | } 26 | return 0 27 | } 28 | -------------------------------------------------------------------------------- /org/headline.go: -------------------------------------------------------------------------------- 1 | package org 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | "strings" 7 | "unicode" 8 | ) 9 | 10 | type Outline struct { 11 | *Section 12 | last *Section 13 | count int 14 | } 15 | 16 | type Section struct { 17 | Headline *Headline 18 | Parent *Section 19 | Children []*Section 20 | } 21 | 22 | type Headline struct { 23 | Index int 24 | Lvl int 25 | Status string 26 | IsComment bool 27 | Priority string 28 | Properties *PropertyDrawer 29 | Title []Node 30 | Tags []string 31 | Children []Node 32 | } 33 | 34 | var headlineRegexp = regexp.MustCompile(`^([*]+)\s+(.*)`) 35 | var tagRegexp = regexp.MustCompile(`(.*?)\s+(:[\p{L}0-9_@#%:]+:\s*$)`) 36 | 37 | func lexHeadline(line string) (token, bool) { 38 | if m := headlineRegexp.FindStringSubmatch(line); m != nil { 39 | return token{"headline", 0, m[2], m}, true 40 | } 41 | return nilToken, false 42 | } 43 | 44 | func (d *Document) parseHeadline(i int, parentStop stopFn) (int, Node) { 45 | t, headline := d.tokens[i], Headline{} 46 | headline.Lvl = len(t.matches[1]) 47 | text := t.content 48 | todoKeywords := trimFastTags( 49 | strings.FieldsFunc(d.Get("TODO"), func(r rune) bool { return unicode.IsSpace(r) || r == '|' }), 50 | ) 51 | for _, k := range todoKeywords { 52 | if strings.HasPrefix(text, k) && len(text) > len(k) && unicode.IsSpace(rune(text[len(k)])) { 53 | headline.Status = k 54 | text = text[len(k)+1:] 55 | break 56 | } 57 | } 58 | 59 | if len(text) >= 4 && text[0:2] == "[#" && strings.Contains("ABC", text[2:3]) && text[3] == ']' { 60 | headline.Priority = text[2:3] 61 | text = strings.TrimSpace(text[4:]) 62 | } 63 | if strings.HasPrefix(text, "COMMENT ") { 64 | headline.IsComment = true 65 | text = strings.TrimPrefix(text, "COMMENT ") 66 | } 67 | if m := tagRegexp.FindStringSubmatch(text); m != nil { 68 | text = m[1] 69 | headline.Tags = strings.FieldsFunc(m[2], func(r rune) bool { return r == ':' }) 70 | } 71 | headline.Index = d.addHeadline(&headline) 72 | headline.Title = d.parseInline(text) 73 | 74 | stop := func(d *Document, i int) bool { 75 | return parentStop(d, i) || d.tokens[i].kind == "headline" && len(d.tokens[i].matches[1]) <= headline.Lvl 76 | } 77 | consumed, nodes := d.parseMany(i+1, stop) 78 | if len(nodes) > 0 { 79 | if d, ok := nodes[0].(PropertyDrawer); ok { 80 | headline.Properties = &d 81 | nodes = nodes[1:] 82 | } 83 | } 84 | headline.Children = nodes 85 | return consumed + 1, headline 86 | } 87 | 88 | func trimFastTags(tags []string) []string { 89 | trimmedTags := make([]string, len(tags)) 90 | for i, t := range tags { 91 | lParen := strings.LastIndex(t, "(") 92 | rParen := strings.LastIndex(t, ")") 93 | end := len(t) - 1 94 | if lParen == end-2 && rParen == end { 95 | trimmedTags[i] = t[:end-2] 96 | } else { 97 | trimmedTags[i] = t 98 | } 99 | } 100 | return trimmedTags 101 | } 102 | 103 | func (h Headline) ID() string { 104 | if customID, ok := h.Properties.Get("CUSTOM_ID"); ok { 105 | return customID 106 | } 107 | return fmt.Sprintf("headline-%d", h.Index) 108 | } 109 | 110 | func (h Headline) IsExcluded(d *Document) bool { 111 | if h.IsComment { 112 | return true 113 | } 114 | for _, excludedTag := range strings.Fields(d.Get("EXCLUDE_TAGS")) { 115 | for _, tag := range h.Tags { 116 | if tag == excludedTag { 117 | return true 118 | } 119 | } 120 | } 121 | return false 122 | } 123 | 124 | func (parent *Section) add(current *Section) { 125 | if parent.Headline == nil || parent.Headline.Lvl < current.Headline.Lvl { 126 | parent.Children = append(parent.Children, current) 127 | current.Parent = parent 128 | } else { 129 | parent.Parent.add(current) 130 | } 131 | } 132 | 133 | func (n Headline) String() string { return String(n) } 134 | -------------------------------------------------------------------------------- /org/html_entity.go: -------------------------------------------------------------------------------- 1 | package org 2 | 3 | import ( 4 | "sort" 5 | "strings" 6 | ) 7 | 8 | var htmlEntityReplacer *strings.Replacer 9 | 10 | func init() { 11 | htmlEntities = append(htmlEntities, 12 | []string{"---", "—"}, 13 | []string{"--", "–"}, 14 | []string{"...", "…"}, 15 | ) 16 | // replace longer entities first to prevent partial replacements (e.g. \angle as \ang + le) 17 | sort.Slice(htmlEntities, func(i, j int) bool { 18 | return len(htmlEntities[i][0]) > len(htmlEntities[j][0]) 19 | }) 20 | xs := make([]string, len(htmlEntities)*2) 21 | for _, kv := range htmlEntities { 22 | // replace both \ and \{} 23 | xs = append(xs, kv[0]+"{}", kv[1], kv[0], kv[1]) 24 | } 25 | htmlEntityReplacer = strings.NewReplacer(xs...) 26 | } 27 | 28 | /* 29 | Generated & copied over using the following elisp 30 | (Setting up go generate seems like a waste for now - I call YAGNI on that one) 31 | 32 | (insert (mapconcat 33 | (lambda (entity) (concat "{`\\" (car entity) "`, `" (nth 6 entity) "`}")) ; entity -> utf8 34 | (cl-remove-if-not 'listp org-entities) 35 | ",\n")) 36 | */ 37 | 38 | var htmlEntities = [][]string{ 39 | {`\Agrave`, `À`}, 40 | {`\agrave`, `à`}, 41 | {`\Aacute`, `Á`}, 42 | {`\aacute`, `á`}, 43 | {`\Acirc`, `Â`}, 44 | {`\acirc`, `â`}, 45 | {`\Amacr`, `Ã`}, 46 | {`\amacr`, `ã`}, 47 | {`\Atilde`, `Ã`}, 48 | {`\atilde`, `ã`}, 49 | {`\Auml`, `Ä`}, 50 | {`\auml`, `ä`}, 51 | {`\Aring`, `Å`}, 52 | {`\AA`, `Å`}, 53 | {`\aring`, `å`}, 54 | {`\AElig`, `Æ`}, 55 | {`\aelig`, `æ`}, 56 | {`\Ccedil`, `Ç`}, 57 | {`\ccedil`, `ç`}, 58 | {`\Egrave`, `È`}, 59 | {`\egrave`, `è`}, 60 | {`\Eacute`, `É`}, 61 | {`\eacute`, `é`}, 62 | {`\Ecirc`, `Ê`}, 63 | {`\ecirc`, `ê`}, 64 | {`\Euml`, `Ë`}, 65 | {`\euml`, `ë`}, 66 | {`\Igrave`, `Ì`}, 67 | {`\igrave`, `ì`}, 68 | {`\Iacute`, `Í`}, 69 | {`\iacute`, `í`}, 70 | {`\Idot`, `İ`}, 71 | {`\inodot`, `ı`}, 72 | {`\Icirc`, `Î`}, 73 | {`\icirc`, `î`}, 74 | {`\Iuml`, `Ï`}, 75 | {`\iuml`, `ï`}, 76 | {`\Ntilde`, `Ñ`}, 77 | {`\ntilde`, `ñ`}, 78 | {`\Ograve`, `Ò`}, 79 | {`\ograve`, `ò`}, 80 | {`\Oacute`, `Ó`}, 81 | {`\oacute`, `ó`}, 82 | {`\Ocirc`, `Ô`}, 83 | {`\ocirc`, `ô`}, 84 | {`\Otilde`, `Õ`}, 85 | {`\otilde`, `õ`}, 86 | {`\Ouml`, `Ö`}, 87 | {`\ouml`, `ö`}, 88 | {`\Oslash`, `Ø`}, 89 | {`\oslash`, `ø`}, 90 | {`\OElig`, `Œ`}, 91 | {`\oelig`, `œ`}, 92 | {`\Scaron`, `Š`}, 93 | {`\scaron`, `š`}, 94 | {`\szlig`, `ß`}, 95 | {`\Ugrave`, `Ù`}, 96 | {`\ugrave`, `ù`}, 97 | {`\Uacute`, `Ú`}, 98 | {`\uacute`, `ú`}, 99 | {`\Ucirc`, `Û`}, 100 | {`\ucirc`, `û`}, 101 | {`\Uuml`, `Ü`}, 102 | {`\uuml`, `ü`}, 103 | {`\Yacute`, `Ý`}, 104 | {`\yacute`, `ý`}, 105 | {`\Yuml`, `Ÿ`}, 106 | {`\yuml`, `ÿ`}, 107 | {`\fnof`, `ƒ`}, 108 | {`\real`, `ℜ`}, 109 | {`\image`, `ℑ`}, 110 | {`\weierp`, `℘`}, 111 | {`\ell`, `ℓ`}, 112 | {`\imath`, `ı`}, 113 | {`\jmath`, `ȷ`}, 114 | {`\Alpha`, `Α`}, 115 | {`\alpha`, `α`}, 116 | {`\Beta`, `Β`}, 117 | {`\beta`, `β`}, 118 | {`\Gamma`, `Γ`}, 119 | {`\gamma`, `γ`}, 120 | {`\Delta`, `Δ`}, 121 | {`\delta`, `δ`}, 122 | {`\Epsilon`, `Ε`}, 123 | {`\epsilon`, `ε`}, 124 | {`\varepsilon`, `ε`}, 125 | {`\Zeta`, `Ζ`}, 126 | {`\zeta`, `ζ`}, 127 | {`\Eta`, `Η`}, 128 | {`\eta`, `η`}, 129 | {`\Theta`, `Θ`}, 130 | {`\theta`, `θ`}, 131 | {`\thetasym`, `ϑ`}, 132 | {`\vartheta`, `ϑ`}, 133 | {`\Iota`, `Ι`}, 134 | {`\iota`, `ι`}, 135 | {`\Kappa`, `Κ`}, 136 | {`\kappa`, `κ`}, 137 | {`\Lambda`, `Λ`}, 138 | {`\lambda`, `λ`}, 139 | {`\Mu`, `Μ`}, 140 | {`\mu`, `μ`}, 141 | {`\nu`, `ν`}, 142 | {`\Nu`, `Ν`}, 143 | {`\Xi`, `Ξ`}, 144 | {`\xi`, `ξ`}, 145 | {`\Omicron`, `Ο`}, 146 | {`\omicron`, `ο`}, 147 | {`\Pi`, `Π`}, 148 | {`\pi`, `π`}, 149 | {`\Rho`, `Ρ`}, 150 | {`\rho`, `ρ`}, 151 | {`\Sigma`, `Σ`}, 152 | {`\sigma`, `σ`}, 153 | {`\sigmaf`, `ς`}, 154 | {`\varsigma`, `ς`}, 155 | {`\Tau`, `Τ`}, 156 | {`\Upsilon`, `Υ`}, 157 | {`\upsih`, `ϒ`}, 158 | {`\upsilon`, `υ`}, 159 | {`\Phi`, `Φ`}, 160 | {`\phi`, `ɸ`}, 161 | {`\varphi`, `φ`}, 162 | {`\Chi`, `Χ`}, 163 | {`\chi`, `χ`}, 164 | {`\acutex`, `𝑥́`}, 165 | {`\Psi`, `Ψ`}, 166 | {`\psi`, `ψ`}, 167 | {`\tau`, `τ`}, 168 | {`\Omega`, `Ω`}, 169 | {`\omega`, `ω`}, 170 | {`\piv`, `ϖ`}, 171 | {`\varpi`, `ϖ`}, 172 | {`\partial`, `∂`}, 173 | {`\alefsym`, `ℵ`}, 174 | {`\aleph`, `ℵ`}, 175 | {`\gimel`, `ℷ`}, 176 | {`\beth`, `ב`}, 177 | {`\dalet`, `ד`}, 178 | {`\ETH`, `Ð`}, 179 | {`\eth`, `ð`}, 180 | {`\THORN`, `Þ`}, 181 | {`\thorn`, `þ`}, 182 | {`\dots`, `…`}, 183 | {`\cdots`, `⋯`}, 184 | {`\hellip`, `…`}, 185 | {`\middot`, `·`}, 186 | {`\iexcl`, `¡`}, 187 | {`\iquest`, `¿`}, 188 | {`\shy`, ``}, 189 | {`\ndash`, `–`}, 190 | {`\mdash`, `—`}, 191 | {`\quot`, `"`}, 192 | {`\acute`, `´`}, 193 | {`\ldquo`, `“`}, 194 | {`\rdquo`, `”`}, 195 | {`\bdquo`, `„`}, 196 | {`\lsquo`, `‘`}, 197 | {`\rsquo`, `’`}, 198 | {`\sbquo`, `‚`}, 199 | {`\laquo`, `«`}, 200 | {`\raquo`, `»`}, 201 | {`\lsaquo`, `‹`}, 202 | {`\rsaquo`, `›`}, 203 | {`\circ`, `∘`}, 204 | {`\vert`, `|`}, 205 | {`\vbar`, `|`}, 206 | {`\brvbar`, `¦`}, 207 | {`\S`, `§`}, 208 | {`\sect`, `§`}, 209 | {`\amp`, `&`}, 210 | {`\lt`, `<`}, 211 | {`\gt`, `>`}, 212 | {`\tilde`, `~`}, 213 | {`\slash`, `/`}, 214 | {`\plus`, `+`}, 215 | {`\under`, `_`}, 216 | {`\equal`, `=`}, 217 | {`\asciicirc`, `^`}, 218 | {`\dagger`, `†`}, 219 | {`\dag`, `†`}, 220 | {`\Dagger`, `‡`}, 221 | {`\ddag`, `‡`}, 222 | {`\nbsp`, ` `}, 223 | {`\ensp`, ` `}, 224 | {`\emsp`, ` `}, 225 | {`\thinsp`, ` `}, 226 | {`\curren`, `¤`}, 227 | {`\cent`, `¢`}, 228 | {`\pound`, `£`}, 229 | {`\yen`, `¥`}, 230 | {`\euro`, `€`}, 231 | {`\EUR`, `€`}, 232 | {`\dollar`, `$`}, 233 | {`\USD`, `$`}, 234 | {`\copy`, `©`}, 235 | {`\reg`, `®`}, 236 | {`\trade`, `™`}, 237 | {`\minus`, `−`}, 238 | {`\pm`, `±`}, 239 | {`\plusmn`, `±`}, 240 | {`\times`, `×`}, 241 | {`\frasl`, `⁄`}, 242 | {`\colon`, `:`}, 243 | {`\div`, `÷`}, 244 | {`\frac12`, `½`}, 245 | {`\frac14`, `¼`}, 246 | {`\frac34`, `¾`}, 247 | {`\permil`, `‰`}, 248 | {`\sup1`, `¹`}, 249 | {`\sup2`, `²`}, 250 | {`\sup3`, `³`}, 251 | {`\radic`, `√`}, 252 | {`\sum`, `∑`}, 253 | {`\prod`, `∏`}, 254 | {`\micro`, `µ`}, 255 | {`\macr`, `¯`}, 256 | {`\deg`, `°`}, 257 | {`\prime`, `′`}, 258 | {`\Prime`, `″`}, 259 | {`\infin`, `∞`}, 260 | {`\infty`, `∞`}, 261 | {`\prop`, `∝`}, 262 | {`\propto`, `∝`}, 263 | {`\not`, `¬`}, 264 | {`\neg`, `¬`}, 265 | {`\land`, `∧`}, 266 | {`\wedge`, `∧`}, 267 | {`\lor`, `∨`}, 268 | {`\vee`, `∨`}, 269 | {`\cap`, `∩`}, 270 | {`\cup`, `∪`}, 271 | {`\smile`, `⌣`}, 272 | {`\frown`, `⌢`}, 273 | {`\int`, `∫`}, 274 | {`\therefore`, `∴`}, 275 | {`\there4`, `∴`}, 276 | {`\because`, `∵`}, 277 | {`\sim`, `∼`}, 278 | {`\cong`, `≅`}, 279 | {`\simeq`, `≅`}, 280 | {`\asymp`, `≈`}, 281 | {`\approx`, `≈`}, 282 | {`\ne`, `≠`}, 283 | {`\neq`, `≠`}, 284 | {`\equiv`, `≡`}, 285 | {`\triangleq`, `≜`}, 286 | {`\le`, `≤`}, 287 | {`\leq`, `≤`}, 288 | {`\ge`, `≥`}, 289 | {`\geq`, `≥`}, 290 | {`\lessgtr`, `≶`}, 291 | {`\lesseqgtr`, `⋚`}, 292 | {`\ll`, `≪`}, 293 | {`\Ll`, `⋘`}, 294 | {`\lll`, `⋘`}, 295 | {`\gg`, `≫`}, 296 | {`\Gg`, `⋙`}, 297 | {`\ggg`, `⋙`}, 298 | {`\prec`, `≺`}, 299 | {`\preceq`, `≼`}, 300 | {`\preccurlyeq`, `≼`}, 301 | {`\succ`, `≻`}, 302 | {`\succeq`, `≽`}, 303 | {`\succcurlyeq`, `≽`}, 304 | {`\sub`, `⊂`}, 305 | {`\subset`, `⊂`}, 306 | {`\sup`, `⊃`}, 307 | {`\supset`, `⊃`}, 308 | {`\nsub`, `⊄`}, 309 | {`\sube`, `⊆`}, 310 | {`\nsup`, `⊅`}, 311 | {`\supe`, `⊇`}, 312 | {`\setminus`, `⧵`}, 313 | {`\forall`, `∀`}, 314 | {`\exist`, `∃`}, 315 | {`\exists`, `∃`}, 316 | {`\nexist`, `∄`}, 317 | {`\nexists`, `∄`}, 318 | {`\empty`, `∅`}, 319 | {`\emptyset`, `∅`}, 320 | {`\isin`, `∈`}, 321 | {`\in`, `∈`}, 322 | {`\notin`, `∉`}, 323 | {`\ni`, `∋`}, 324 | {`\nabla`, `∇`}, 325 | {`\ang`, `∠`}, 326 | {`\angle`, `∠`}, 327 | {`\perp`, `⊥`}, 328 | {`\parallel`, `∥`}, 329 | {`\sdot`, `⋅`}, 330 | {`\cdot`, `⋅`}, 331 | {`\lceil`, `⌈`}, 332 | {`\rceil`, `⌉`}, 333 | {`\lfloor`, `⌊`}, 334 | {`\rfloor`, `⌋`}, 335 | {`\lang`, `⟨`}, 336 | {`\rang`, `⟩`}, 337 | {`\langle`, `⟨`}, 338 | {`\rangle`, `⟩`}, 339 | {`\hbar`, `ℏ`}, 340 | {`\mho`, `℧`}, 341 | {`\larr`, `←`}, 342 | {`\leftarrow`, `←`}, 343 | {`\gets`, `←`}, 344 | {`\lArr`, `⇐`}, 345 | {`\Leftarrow`, `⇐`}, 346 | {`\uarr`, `↑`}, 347 | {`\uparrow`, `↑`}, 348 | {`\uArr`, `⇑`}, 349 | {`\Uparrow`, `⇑`}, 350 | {`\rarr`, `→`}, 351 | {`\to`, `→`}, 352 | {`\rightarrow`, `→`}, 353 | {`\rArr`, `⇒`}, 354 | {`\Rightarrow`, `⇒`}, 355 | {`\darr`, `↓`}, 356 | {`\downarrow`, `↓`}, 357 | {`\dArr`, `⇓`}, 358 | {`\Downarrow`, `⇓`}, 359 | {`\harr`, `↔`}, 360 | {`\leftrightarrow`, `↔`}, 361 | {`\hArr`, `⇔`}, 362 | {`\Leftrightarrow`, `⇔`}, 363 | {`\crarr`, `↵`}, 364 | {`\hookleftarrow`, `↵`}, 365 | {`\arccos`, `arccos`}, 366 | {`\arcsin`, `arcsin`}, 367 | {`\arctan`, `arctan`}, 368 | {`\arg`, `arg`}, 369 | {`\cos`, `cos`}, 370 | {`\cosh`, `cosh`}, 371 | {`\cot`, `cot`}, 372 | {`\coth`, `coth`}, 373 | {`\csc`, `csc`}, 374 | {`\deg`, `deg`}, 375 | {`\det`, `det`}, 376 | {`\dim`, `dim`}, 377 | {`\exp`, `exp`}, 378 | {`\gcd`, `gcd`}, 379 | {`\hom`, `hom`}, 380 | {`\inf`, `inf`}, 381 | {`\ker`, `ker`}, 382 | {`\lg`, `lg`}, 383 | {`\lim`, `lim`}, 384 | {`\liminf`, `liminf`}, 385 | {`\limsup`, `limsup`}, 386 | {`\ln`, `ln`}, 387 | {`\log`, `log`}, 388 | {`\max`, `max`}, 389 | {`\min`, `min`}, 390 | {`\Pr`, `Pr`}, 391 | {`\sec`, `sec`}, 392 | {`\sin`, `sin`}, 393 | {`\sinh`, `sinh`}, 394 | {`\sup`, `sup`}, 395 | {`\tan`, `tan`}, 396 | {`\tanh`, `tanh`}, 397 | {`\bull`, `•`}, 398 | {`\bullet`, `•`}, 399 | {`\star`, `⋆`}, 400 | {`\lowast`, `∗`}, 401 | {`\ast`, `*`}, 402 | {`\odot`, `ʘ`}, 403 | {`\oplus`, `⊕`}, 404 | {`\otimes`, `⊗`}, 405 | {`\check`, `✓`}, 406 | {`\checkmark`, `✓`}, 407 | {`\para`, `¶`}, 408 | {`\ordf`, `ª`}, 409 | {`\ordm`, `º`}, 410 | {`\cedil`, `¸`}, 411 | {`\oline`, `‾`}, 412 | {`\uml`, `¨`}, 413 | {`\zwnj`, `‌`}, 414 | {`\zwj`, `‍`}, 415 | {`\lrm`, `‎`}, 416 | {`\rlm`, `‏`}, 417 | {`\smiley`, `☺`}, 418 | {`\blacksmile`, `☻`}, 419 | {`\sad`, `☹`}, 420 | {`\frowny`, `☹`}, 421 | {`\clubs`, `♣`}, 422 | {`\clubsuit`, `♣`}, 423 | {`\spades`, `♠`}, 424 | {`\spadesuit`, `♠`}, 425 | {`\hearts`, `♥`}, 426 | {`\heartsuit`, `♥`}, 427 | {`\diams`, `◆`}, 428 | {`\diamondsuit`, `◆`}, 429 | {`\diamond`, `◆`}, 430 | {`\Diamond`, `◆`}, 431 | {`\loz`, `⧫`}, 432 | {`\_ `, ` `}, 433 | {`\_ `, `  `}, 434 | {`\_ `, `   `}, 435 | {`\_ `, `    `}, 436 | {`\_ `, `     `}, 437 | {`\_ `, `      `}, 438 | {`\_ `, `       `}, 439 | {`\_ `, `        `}, 440 | {`\_ `, `         `}, 441 | {`\_ `, `          `}, 442 | {`\_ `, `           `}, 443 | {`\_ `, `            `}, 444 | {`\_ `, `             `}, 445 | {`\_ `, `              `}, 446 | {`\_ `, `               `}, 447 | {`\_ `, `                `}, 448 | {`\_ `, `                 `}, 449 | {`\_ `, `                  `}, 450 | {`\_ `, `                   `}, 451 | {`\_ `, `                    `}, 452 | } 453 | -------------------------------------------------------------------------------- /org/html_writer_test.go: -------------------------------------------------------------------------------- 1 | package org 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | ) 7 | 8 | type ExtendedHTMLWriter struct { 9 | *HTMLWriter 10 | callCount int 11 | } 12 | 13 | func (w *ExtendedHTMLWriter) WriteText(t Text) { 14 | w.callCount++ 15 | w.HTMLWriter.WriteText(t) 16 | } 17 | 18 | func TestHTMLWriter(t *testing.T) { 19 | testWriter(t, func() Writer { return NewHTMLWriter() }, ".html") 20 | } 21 | 22 | func TestExtendedHTMLWriter(t *testing.T) { 23 | p := Paragraph{Children: []Node{Text{Content: "text"}, Text{Content: "more text"}}} 24 | htmlWriter := NewHTMLWriter() 25 | extendedWriter := &ExtendedHTMLWriter{htmlWriter, 0} 26 | htmlWriter.ExtendingWriter = extendedWriter 27 | WriteNodes(extendedWriter, p) 28 | if extendedWriter.callCount != 2 { 29 | t.Errorf("WriteText method of extending writer was not called: CallCount %d", extendedWriter.callCount) 30 | } 31 | } 32 | 33 | var prettyRelativeLinkTests = map[string]string{ 34 | "[[/hello.org][hello]]": `

hello

`, 35 | "[[hello.org][hello]]": `

hello

`, 36 | "[[file:/hello.org]]": `

/hello/

`, 37 | "[[file:hello.org]]": `

../hello/

`, 38 | "[[http://hello.org]]": `

http://hello.org

`, 39 | "[[/foo.png]]": `

/foo.png

`, 40 | "[[foo.png]]": `

../foo.png

`, 41 | "[[/foo.png][foo]]": `

foo

`, 42 | "[[foo.png][foo]]": `

foo

`, 43 | } 44 | 45 | func TestPrettyRelativeLinks(t *testing.T) { 46 | for org, expected := range prettyRelativeLinkTests { 47 | t.Run(org, func(t *testing.T) { 48 | writer := NewHTMLWriter() 49 | writer.PrettyRelativeLinks = true 50 | actual, err := New().Silent().Parse(strings.NewReader(org), "./prettyRelativeLinkTests.org").Write(writer) 51 | if err != nil { 52 | t.Errorf("%s\n got error: %s", org, err) 53 | } else if actual := strings.TrimSpace(actual); actual != expected { 54 | t.Errorf("%s:\n%s'", org, diff(actual, expected)) 55 | } 56 | }) 57 | } 58 | } 59 | 60 | var topLevelHLevelTests = map[struct { 61 | TopLevelHLevel int 62 | input string 63 | }]string{ 64 | {1, "* Top-level headline"}: "

\nTop-level headline\n

", 65 | {1, "** Second-level headline"}: "

\nSecond-level headline\n

", 66 | {1, "*** Third-level headline"}: "

\nThird-level headline\n

", 67 | {1, "**** Fourth-level headline"}: "

\nFourth-level headline\n

", 68 | {1, "***** Fifth-level headline"}: "
\nFifth-level headline\n
", 69 | {1, "****** Sixth-level headline"}: "
\nSixth-level headline\n
", 70 | 71 | {2, "* Top-level headline"}: "

\nTop-level headline\n

", 72 | {2, "** Second-level headline"}: "

\nSecond-level headline\n

", 73 | {2, "*** Third-level headline"}: "

\nThird-level headline\n

", 74 | {2, "**** Fourth-level headline"}: "
\nFourth-level headline\n
", 75 | {2, "***** Fifth-level headline"}: "
\nFifth-level headline\n
", 76 | 77 | {3, "* Top-level headline"}: "

\nTop-level headline\n

", 78 | {3, "** Second-level headline"}: "

\nSecond-level headline\n

", 79 | {3, "*** Third-level headline"}: "
\nThird-level headline\n
", 80 | {3, "**** Fourth-level headline"}: "
\nFourth-level headline\n
", 81 | 82 | {4, "* Top-level headline"}: "

\nTop-level headline\n

", 83 | {4, "** Second-level headline"}: "
\nSecond-level headline\n
", 84 | {4, "*** Third-level headline"}: "
\nThird-level headline\n
", 85 | 86 | {5, "* Top-level headline"}: "
\nTop-level headline\n
", 87 | {5, "** Second-level headline"}: "
\nSecond-level headline\n
", 88 | 89 | {6, "* Top-level headline"}: "
\nTop-level headline\n
", 90 | } 91 | 92 | func TestTopLevelHLevel(t *testing.T) { 93 | for org, expected := range topLevelHLevelTests { 94 | t.Run(org.input, func(t *testing.T) { 95 | writer := NewHTMLWriter() 96 | writer.TopLevelHLevel = org.TopLevelHLevel 97 | actual, err := New().Silent().Parse(strings.NewReader(org.input), "./topLevelHLevelTests.org").Write(writer) 98 | if err != nil { 99 | t.Errorf("TopLevelHLevel=%d %s\n got error: %s", org.TopLevelHLevel, org.input, err) 100 | } else if actual := strings.TrimSpace(actual); !strings.Contains(actual, expected) { 101 | t.Errorf("TopLevelHLevel=%d %s:\n%s'", org.TopLevelHLevel, org.input, diff(actual, expected)) 102 | } 103 | }) 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /org/keyword.go: -------------------------------------------------------------------------------- 1 | package org 2 | 3 | import ( 4 | "bytes" 5 | "path/filepath" 6 | "regexp" 7 | "strings" 8 | ) 9 | 10 | type Comment struct{ Content string } 11 | 12 | type Keyword struct { 13 | Key string 14 | Value string 15 | } 16 | 17 | type NodeWithName struct { 18 | Name string 19 | Node Node 20 | } 21 | 22 | type NodeWithMeta struct { 23 | Node Node 24 | Meta Metadata 25 | } 26 | 27 | type Metadata struct { 28 | Caption [][]Node 29 | HTMLAttributes [][]string 30 | } 31 | 32 | type Include struct { 33 | Keyword 34 | Resolve func() Node 35 | } 36 | 37 | var keywordRegexp = regexp.MustCompile(`^(\s*)#\+([^:]+):(\s+(.*)|$)`) 38 | var commentRegexp = regexp.MustCompile(`^(\s*)#\s(.*)`) 39 | 40 | var includeFileRegexp = regexp.MustCompile(`(?i)^"([^"]+)" (src|example|export) (\w+)$`) 41 | var attributeRegexp = regexp.MustCompile(`(?:^|\s+)(:[-\w]+)\s+(.*)$`) 42 | 43 | func lexKeywordOrComment(line string) (token, bool) { 44 | if m := keywordRegexp.FindStringSubmatch(line); m != nil { 45 | return token{"keyword", len(m[1]), m[2], m}, true 46 | } else if m := commentRegexp.FindStringSubmatch(line); m != nil { 47 | return token{"comment", len(m[1]), m[2], m}, true 48 | } 49 | return nilToken, false 50 | } 51 | 52 | func (d *Document) parseComment(i int, stop stopFn) (int, Node) { 53 | return 1, Comment{d.tokens[i].content} 54 | } 55 | 56 | func (d *Document) parseKeyword(i int, stop stopFn) (int, Node) { 57 | k := parseKeyword(d.tokens[i]) 58 | switch k.Key { 59 | case "NAME": 60 | return d.parseNodeWithName(k, i, stop) 61 | case "SETUPFILE": 62 | return d.loadSetupFile(k) 63 | case "INCLUDE": 64 | return d.parseInclude(k) 65 | case "LINK": 66 | if parts := strings.SplitN(k.Value, " ", 2); len(parts) == 2 { 67 | d.Links[parts[0]] = parts[1] 68 | } 69 | return 1, k 70 | case "MACRO": 71 | if parts := strings.Split(k.Value, " "); len(parts) >= 2 { 72 | d.Macros[parts[0]] = parts[1] 73 | } 74 | return 1, k 75 | case "CAPTION", "ATTR_HTML": 76 | consumed, node := d.parseAffiliated(i, stop) 77 | if consumed != 0 { 78 | return consumed, node 79 | } 80 | fallthrough 81 | default: 82 | if _, ok := d.BufferSettings[k.Key]; ok { 83 | d.BufferSettings[k.Key] = strings.Join([]string{d.BufferSettings[k.Key], k.Value}, "\n") 84 | } else { 85 | d.BufferSettings[k.Key] = k.Value 86 | } 87 | return 1, k 88 | } 89 | } 90 | 91 | func (d *Document) parseNodeWithName(k Keyword, i int, stop stopFn) (int, Node) { 92 | if stop(d, i+1) { 93 | return 0, nil 94 | } 95 | consumed, node := d.parseOne(i+1, stop) 96 | if consumed == 0 || node == nil { 97 | return 0, nil 98 | } 99 | d.NamedNodes[k.Value] = node 100 | return consumed + 1, NodeWithName{k.Value, node} 101 | } 102 | 103 | func (d *Document) parseAffiliated(i int, stop stopFn) (int, Node) { 104 | start, meta := i, Metadata{} 105 | for ; !stop(d, i) && d.tokens[i].kind == "keyword"; i++ { 106 | switch k := parseKeyword(d.tokens[i]); k.Key { 107 | case "CAPTION": 108 | meta.Caption = append(meta.Caption, d.parseInline(k.Value)) 109 | case "ATTR_HTML": 110 | attributes, rest := []string{}, k.Value 111 | for { 112 | if k, m := "", attributeRegexp.FindStringSubmatch(rest); m != nil { 113 | k, rest = m[1], m[2] 114 | attributes = append(attributes, k) 115 | if v, m := "", attributeRegexp.FindStringSubmatchIndex(rest); m != nil { 116 | v, rest = rest[:m[0]], rest[m[0]:] 117 | attributes = append(attributes, v) 118 | } else { 119 | attributes = append(attributes, strings.TrimSpace(rest)) 120 | break 121 | } 122 | } else { 123 | break 124 | } 125 | } 126 | meta.HTMLAttributes = append(meta.HTMLAttributes, attributes) 127 | default: 128 | return 0, nil 129 | } 130 | } 131 | if stop(d, i) { 132 | return 0, nil 133 | } 134 | consumed, node := d.parseOne(i, stop) 135 | if consumed == 0 || node == nil { 136 | return 0, nil 137 | } 138 | i += consumed 139 | return i - start, NodeWithMeta{node, meta} 140 | } 141 | 142 | func parseKeyword(t token) Keyword { 143 | k, v := t.matches[2], t.matches[4] 144 | return Keyword{strings.ToUpper(k), strings.TrimSpace(v)} 145 | } 146 | 147 | func (d *Document) parseInclude(k Keyword) (int, Node) { 148 | resolve := func() Node { 149 | d.Log.Printf("Bad include %#v", k) 150 | return k 151 | } 152 | if m := includeFileRegexp.FindStringSubmatch(k.Value); m != nil { 153 | path, kind, lang := m[1], m[2], m[3] 154 | if !filepath.IsAbs(path) { 155 | path = filepath.Join(filepath.Dir(d.Path), path) 156 | } 157 | resolve = func() Node { 158 | bs, err := d.ReadFile(path) 159 | if err != nil { 160 | d.Log.Printf("Bad include %#v: %s", k, err) 161 | return k 162 | } 163 | return Block{strings.ToUpper(kind), []string{lang}, d.parseRawInline(string(bs)), nil} 164 | } 165 | } 166 | return 1, Include{k, resolve} 167 | } 168 | 169 | func (d *Document) loadSetupFile(k Keyword) (int, Node) { 170 | path := k.Value 171 | if !filepath.IsAbs(path) { 172 | path = filepath.Join(filepath.Dir(d.Path), path) 173 | } 174 | bs, err := d.ReadFile(path) 175 | if err != nil { 176 | d.Log.Printf("Bad setup file: %#v: %s", k, err) 177 | return 1, k 178 | } 179 | setupDocument := d.Configuration.Parse(bytes.NewReader(bs), path) 180 | if err := setupDocument.Error; err != nil { 181 | d.Log.Printf("Bad setup file: %#v: %s", k, err) 182 | return 1, k 183 | } 184 | for k, v := range setupDocument.BufferSettings { 185 | d.BufferSettings[k] = v 186 | } 187 | return 1, k 188 | } 189 | 190 | func (n Comment) String() string { return String(n) } 191 | func (n Keyword) String() string { return String(n) } 192 | func (n NodeWithMeta) String() string { return String(n) } 193 | func (n NodeWithName) String() string { return String(n) } 194 | func (n Include) String() string { return String(n) } 195 | -------------------------------------------------------------------------------- /org/list.go: -------------------------------------------------------------------------------- 1 | package org 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | "strings" 7 | "unicode" 8 | ) 9 | 10 | type List struct { 11 | Kind string 12 | Items []Node 13 | } 14 | 15 | type ListItem struct { 16 | Bullet string 17 | Status string 18 | Value string 19 | Children []Node 20 | } 21 | 22 | type DescriptiveListItem struct { 23 | Bullet string 24 | Status string 25 | Term []Node 26 | Details []Node 27 | } 28 | 29 | var unorderedListRegexp = regexp.MustCompile(`^(\s*)([+*-])(\s+(.*)|$)`) 30 | var orderedListRegexp = regexp.MustCompile(`^(\s*)(([0-9]+|[a-zA-Z])[.)])(\s+(.*)|$)`) 31 | var descriptiveListItemRegexp = regexp.MustCompile(`\s::(\s|$)`) 32 | var listItemValueRegexp = regexp.MustCompile(`\[@(\d+)\]\s`) 33 | var listItemStatusRegexp = regexp.MustCompile(`\[( |X|-)\]\s`) 34 | 35 | func lexList(line string) (token, bool) { 36 | if m := unorderedListRegexp.FindStringSubmatch(line); m != nil { 37 | return token{"unorderedList", len(m[1]), m[4], m}, true 38 | } else if m := orderedListRegexp.FindStringSubmatch(line); m != nil { 39 | return token{"orderedList", len(m[1]), m[5], m}, true 40 | } 41 | return nilToken, false 42 | } 43 | 44 | func isListToken(t token) bool { 45 | return t.kind == "unorderedList" || t.kind == "orderedList" 46 | } 47 | 48 | func listKind(t token) (string, string) { 49 | kind := "" 50 | switch bullet := t.matches[2]; { 51 | case bullet == "*" || bullet == "+" || bullet == "-": 52 | kind = "unordered" 53 | case unicode.IsLetter(rune(bullet[0])), unicode.IsDigit(rune(bullet[0])): 54 | kind = "ordered" 55 | default: 56 | panic(fmt.Sprintf("bad list bullet '%s': %#v", bullet, t)) 57 | } 58 | if descriptiveListItemRegexp.MatchString(t.content) { 59 | return kind, "descriptive" 60 | } 61 | return kind, kind 62 | } 63 | 64 | func (d *Document) parseList(i int, parentStop stopFn) (int, Node) { 65 | start, lvl := i, d.tokens[i].lvl 66 | listMainKind, kind := listKind(d.tokens[i]) 67 | list := List{Kind: kind} 68 | stop := func(*Document, int) bool { 69 | if parentStop(d, i) || d.tokens[i].lvl != lvl || !isListToken(d.tokens[i]) { 70 | return true 71 | } 72 | itemMainKind, _ := listKind(d.tokens[i]) 73 | return itemMainKind != listMainKind 74 | } 75 | for !stop(d, i) { 76 | consumed, node := d.parseListItem(list, i, parentStop) 77 | i += consumed 78 | list.Items = append(list.Items, node) 79 | } 80 | return i - start, list 81 | } 82 | 83 | func (d *Document) parseListItem(l List, i int, parentStop stopFn) (int, Node) { 84 | start, nodes, bullet := i, []Node{}, d.tokens[i].matches[2] 85 | minIndent, dterm, content, status, value := d.tokens[i].lvl+len(bullet), "", d.tokens[i].content, "", "" 86 | originalBaseLvl := d.baseLvl 87 | d.baseLvl = minIndent + 1 88 | if m := listItemValueRegexp.FindStringSubmatch(content); m != nil && l.Kind == "ordered" { 89 | value, content = m[1], content[len("[@] ")+len(m[1]):] 90 | } 91 | if m := listItemStatusRegexp.FindStringSubmatch(content); m != nil { 92 | status, content = m[1], content[len("[ ] "):] 93 | } 94 | if l.Kind == "descriptive" { 95 | if m := descriptiveListItemRegexp.FindStringIndex(content); m != nil { 96 | dterm, content = content[:m[0]], content[m[1]:] 97 | d.baseLvl = strings.Index(d.tokens[i].matches[0], " ::") + 4 98 | } 99 | } 100 | 101 | d.tokens[i] = tokenize(strings.Repeat(" ", minIndent) + content) 102 | stop := func(d *Document, i int) bool { 103 | if parentStop(d, i) { 104 | return true 105 | } 106 | t := d.tokens[i] 107 | return t.lvl < minIndent && !(t.kind == "text" && t.content == "") 108 | } 109 | for !stop(d, i) && (i <= start+1 || !isSecondBlankLine(d, i)) { 110 | consumed, node := d.parseOne(i, stop) 111 | i += consumed 112 | nodes = append(nodes, node) 113 | } 114 | d.baseLvl = originalBaseLvl 115 | if l.Kind == "descriptive" { 116 | return i - start, DescriptiveListItem{bullet, status, d.parseInline(dterm), nodes} 117 | } 118 | return i - start, ListItem{bullet, status, value, nodes} 119 | } 120 | 121 | func (n List) String() string { return String(n) } 122 | func (n ListItem) String() string { return String(n) } 123 | func (n DescriptiveListItem) String() string { return String(n) } 124 | -------------------------------------------------------------------------------- /org/org_writer_test.go: -------------------------------------------------------------------------------- 1 | package org 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "path/filepath" 7 | "strings" 8 | "testing" 9 | 10 | "github.com/pmezard/go-difflib/difflib" 11 | ) 12 | 13 | type ExtendedOrgWriter struct { 14 | *OrgWriter 15 | callCount int 16 | } 17 | 18 | func (w *ExtendedOrgWriter) WriteText(t Text) { 19 | w.callCount++ 20 | w.OrgWriter.WriteText(t) 21 | } 22 | 23 | func TestOrgWriter(t *testing.T) { 24 | testWriter(t, func() Writer { return NewOrgWriter() }, ".pretty_org") 25 | } 26 | 27 | func TestExtendedOrgWriter(t *testing.T) { 28 | p := Paragraph{Children: []Node{Text{Content: "text"}, Text{Content: "more text"}}} 29 | orgWriter := NewOrgWriter() 30 | extendedWriter := &ExtendedOrgWriter{orgWriter, 0} 31 | orgWriter.ExtendingWriter = extendedWriter 32 | WriteNodes(extendedWriter, p) 33 | if extendedWriter.callCount != 2 { 34 | t.Errorf("WriteText method of extending writer was not called: CallCount %d", extendedWriter.callCount) 35 | } 36 | } 37 | 38 | func testWriter(t *testing.T, newWriter func() Writer, ext string) { 39 | for _, path := range orgTestFiles() { 40 | tmpPath := path[:len(path)-len(".org")] 41 | t.Run(filepath.Base(tmpPath), func(t *testing.T) { 42 | expected := fileString(t, tmpPath+ext) 43 | reader := strings.NewReader(fileString(t, path)) 44 | actual, err := New().Silent().Parse(reader, path).Write(newWriter()) 45 | if err != nil { 46 | t.Fatalf("%s\n got error: %s", path, err) 47 | } else if actual != expected { 48 | t.Fatalf("%s:\n%s'", path, diff(actual, expected)) 49 | } 50 | }) 51 | } 52 | } 53 | 54 | func orgTestFiles() []string { 55 | dir := "./testdata" 56 | files, err := ioutil.ReadDir(dir) 57 | if err != nil { 58 | panic(fmt.Sprintf("Could not read directory: %s", err)) 59 | } 60 | orgFiles := []string{} 61 | for _, f := range files { 62 | name := f.Name() 63 | if filepath.Ext(name) != ".org" { 64 | continue 65 | } 66 | orgFiles = append(orgFiles, filepath.Join(dir, name)) 67 | } 68 | return orgFiles 69 | } 70 | 71 | func fileString(t *testing.T, path string) string { 72 | bs, err := ioutil.ReadFile(path) 73 | if err != nil { 74 | t.Fatalf("Could not read file %s: %s", path, err) 75 | } 76 | return string(bs) 77 | } 78 | 79 | func diff(actual, expected string) string { 80 | diff := difflib.UnifiedDiff{ 81 | A: difflib.SplitLines(actual), 82 | B: difflib.SplitLines(expected), 83 | FromFile: "Actual", 84 | ToFile: "Expected", 85 | Context: 3, 86 | } 87 | text, _ := difflib.GetUnifiedDiffString(diff) 88 | return text 89 | } 90 | -------------------------------------------------------------------------------- /org/paragraph.go: -------------------------------------------------------------------------------- 1 | package org 2 | 3 | import ( 4 | "math" 5 | "regexp" 6 | "strings" 7 | ) 8 | 9 | type Paragraph struct{ Children []Node } 10 | type HorizontalRule struct{} 11 | 12 | var horizontalRuleRegexp = regexp.MustCompile(`^(\s*)-{5,}\s*$`) 13 | var plainTextRegexp = regexp.MustCompile(`^(\s*)(.*)`) 14 | 15 | func lexText(line string) (token, bool) { 16 | if m := plainTextRegexp.FindStringSubmatch(line); m != nil { 17 | return token{"text", len(m[1]), m[2], m}, true 18 | } 19 | return nilToken, false 20 | } 21 | 22 | func lexHorizontalRule(line string) (token, bool) { 23 | if m := horizontalRuleRegexp.FindStringSubmatch(line); m != nil { 24 | return token{"horizontalRule", len(m[1]), "", m}, true 25 | } 26 | return nilToken, false 27 | } 28 | 29 | func (d *Document) parseParagraph(i int, parentStop stopFn) (int, Node) { 30 | lines, start := []string{d.tokens[i].content}, i 31 | stop := func(d *Document, i int) bool { 32 | return parentStop(d, i) || d.tokens[i].kind != "text" || d.tokens[i].content == "" 33 | } 34 | for i += 1; !stop(d, i); i++ { 35 | lvl := math.Max(float64(d.tokens[i].lvl-d.baseLvl), 0) 36 | lines = append(lines, strings.Repeat(" ", int(lvl))+d.tokens[i].content) 37 | } 38 | consumed := i - start 39 | return consumed, Paragraph{d.parseInline(strings.Join(lines, "\n"))} 40 | } 41 | 42 | func (d *Document) parseHorizontalRule(i int, parentStop stopFn) (int, Node) { 43 | return 1, HorizontalRule{} 44 | } 45 | 46 | func (n Paragraph) String() string { return String(n) } 47 | func (n HorizontalRule) String() string { return String(n) } 48 | -------------------------------------------------------------------------------- /org/table.go: -------------------------------------------------------------------------------- 1 | package org 2 | 3 | import ( 4 | "regexp" 5 | "strconv" 6 | "strings" 7 | "unicode/utf8" 8 | ) 9 | 10 | type Table struct { 11 | Rows []Row 12 | ColumnInfos []ColumnInfo 13 | SeparatorIndices []int 14 | } 15 | 16 | type Row struct { 17 | Columns []Column 18 | IsSpecial bool 19 | } 20 | 21 | type Column struct { 22 | Children []Node 23 | *ColumnInfo 24 | } 25 | 26 | type ColumnInfo struct { 27 | Align string 28 | Len int 29 | DisplayLen int 30 | } 31 | 32 | var tableSeparatorRegexp = regexp.MustCompile(`^(\s*)(\|[+-|]*)\s*$`) 33 | var tableRowRegexp = regexp.MustCompile(`^(\s*)(\|.*)`) 34 | 35 | var columnAlignAndLengthRegexp = regexp.MustCompile(`^<(l|c|r)?(\d+)?>$`) 36 | 37 | func lexTable(line string) (token, bool) { 38 | if m := tableSeparatorRegexp.FindStringSubmatch(line); m != nil { 39 | return token{"tableSeparator", len(m[1]), m[2], m}, true 40 | } else if m := tableRowRegexp.FindStringSubmatch(line); m != nil { 41 | return token{"tableRow", len(m[1]), m[2], m}, true 42 | } 43 | return nilToken, false 44 | } 45 | 46 | func (d *Document) parseTable(i int, parentStop stopFn) (int, Node) { 47 | rawRows, separatorIndices, start := [][]string{}, []int{}, i 48 | for ; !parentStop(d, i); i++ { 49 | if t := d.tokens[i]; t.kind == "tableRow" { 50 | rawRow := strings.FieldsFunc(d.tokens[i].content, func(r rune) bool { return r == '|' }) 51 | for i := range rawRow { 52 | rawRow[i] = strings.TrimSpace(rawRow[i]) 53 | } 54 | rawRows = append(rawRows, rawRow) 55 | } else if t.kind == "tableSeparator" { 56 | separatorIndices = append(separatorIndices, i-start) 57 | rawRows = append(rawRows, nil) 58 | } else { 59 | break 60 | } 61 | } 62 | 63 | table := Table{nil, getColumnInfos(rawRows), separatorIndices} 64 | for _, rawColumns := range rawRows { 65 | row := Row{nil, isSpecialRow(rawColumns)} 66 | if len(rawColumns) != 0 { 67 | for i := range table.ColumnInfos { 68 | column := Column{nil, &table.ColumnInfos[i]} 69 | if i < len(rawColumns) { 70 | column.Children = d.parseInline(rawColumns[i]) 71 | } 72 | row.Columns = append(row.Columns, column) 73 | } 74 | } 75 | table.Rows = append(table.Rows, row) 76 | } 77 | return i - start, table 78 | } 79 | 80 | func getColumnInfos(rows [][]string) []ColumnInfo { 81 | columnCount := 0 82 | for _, columns := range rows { 83 | if n := len(columns); n > columnCount { 84 | columnCount = n 85 | } 86 | } 87 | 88 | columnInfos := make([]ColumnInfo, columnCount) 89 | for i := 0; i < columnCount; i++ { 90 | countNumeric, countNonNumeric := 0, 0 91 | for _, columns := range rows { 92 | if i >= len(columns) { 93 | continue 94 | } 95 | 96 | if n := utf8.RuneCountInString(columns[i]); n > columnInfos[i].Len { 97 | columnInfos[i].Len = n 98 | } 99 | 100 | if m := columnAlignAndLengthRegexp.FindStringSubmatch(columns[i]); m != nil && isSpecialRow(columns) { 101 | switch m[1] { 102 | case "l": 103 | columnInfos[i].Align = "left" 104 | case "c": 105 | columnInfos[i].Align = "center" 106 | case "r": 107 | columnInfos[i].Align = "right" 108 | } 109 | if m[2] != "" { 110 | l, _ := strconv.Atoi(m[2]) 111 | columnInfos[i].DisplayLen = l 112 | } 113 | } else if _, err := strconv.ParseFloat(columns[i], 32); err == nil { 114 | countNumeric++ 115 | } else if strings.TrimSpace(columns[i]) != "" { 116 | countNonNumeric++ 117 | } 118 | } 119 | 120 | if columnInfos[i].Align == "" && countNumeric >= countNonNumeric { 121 | columnInfos[i].Align = "right" 122 | } 123 | } 124 | return columnInfos 125 | } 126 | 127 | func isSpecialRow(rawColumns []string) bool { 128 | isAlignRow := true 129 | for _, rawColumn := range rawColumns { 130 | if !columnAlignAndLengthRegexp.MatchString(rawColumn) && rawColumn != "" { 131 | isAlignRow = false 132 | } 133 | } 134 | return isAlignRow 135 | } 136 | 137 | func (n Table) String() string { return String(n) } 138 | -------------------------------------------------------------------------------- /org/testdata/blocks.html: -------------------------------------------------------------------------------- 1 |
  2 | some results without a block
  3 | 
4 |
5 |
6 |
7 |
  8 | echo "a bash source block"
  9 | 
 10 | function hello {
 11 |     echo Hello World!
 12 | }
 13 | 
 14 | hello
 15 | 
16 |
17 |
18 |
19 | block caption 20 |
21 |
22 |
23 |
24 |
 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 |
31 |
32 |
33 |
34 |
 35 | a source block without a language
 36 | 
37 |
38 |
39 |
40 |
41 |
 42 | echo a source block with results
 43 | 
44 |
45 |
46 |
 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 |

Mongodb is webscale. (source: mongodb-is-web-scale)

78 |

79 | blocks like the quote block parse their content and can contain

80 |
    81 |
  • lists
  • 82 |
  • inline markup
  • 83 |
  • 84 |

    tables

    85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 |
    foo
    bar
    baz
    98 |
  • 99 |
  • 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. white-space: pre).
  • 104 |
105 |
106 |
107 |
108 |
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 |
116 |
117 |
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 | 140 |

this unindented line is outside of the list item

141 | 198 |
199 |
200 |
201 | describe <a b c>;
202 | 
203 |
204 |
205 | -------------------------------------------------------------------------------- /org/testdata/blocks.org: -------------------------------------------------------------------------------- 1 | #+RESULTS: 2 | : some results without a block 3 | 4 | #+CAPTION: block caption 5 | #+BEGIN_SRC bash :results raw 6 | echo "a bash source block" 7 | 8 | function hello { 9 | echo Hello World! 10 | } 11 | 12 | hello 13 | #+END_SRC 14 | 15 | #+BEGIN_SRC 16 | 17 | a source block with leading newline, trailing newline characters 18 | and a line started 19 | with leading space 20 | and line leading tab. 21 | 22 | #+END_SRC 23 | 24 | #+BEGIN_SRC 25 | a source block without a language 26 | #+END_SRC 27 | 28 | 29 | #+BEGIN_SRC bash 30 | echo a source block with results 31 | #+END_SRC 32 | 33 | #+RESULTS: 34 | : a source block with results 35 | 36 | #+BEGIN_SRC bash :exports none 37 | echo a source block with results that is not exported 38 | #+END_SRC 39 | 40 | #+RESULTS: 41 | : a source block with results that is not exported 42 | 43 | #+BEGIN_SRC bash :exports results 44 | echo a source block that only exports results 45 | #+END_SRC 46 | 47 | #+RESULTS: 48 | : a source block that only exports results 49 | 50 | #+begin_src bash :cmdline -foo -bar :exports results 51 | # the code block is not rendered 52 | echo but the result block is 53 | #+end_src 54 | 55 | #+RESULTS: 56 | : but the result block is 57 | 58 | #+BEGIN_EXAMPLE foo bar baz 59 | an example block with 60 | multiple lines including 61 | 62 | 63 | empty lines! 64 | 65 | it also has multiple parameters 66 | 67 | src, example & export blocks treat their content as raw text 68 | /inline/ *markup* is ignored 69 | and whitespace is honored and not removed 70 | 71 | content of example blocks is still html escaped - see 72 | #+END_EXAMPLE 73 | 74 | : examples like this 75 | : are also supported 76 | : 77 | : note that /inline/ *markup* ignored 78 | 79 | #+BEGIN_QUOTE 80 | Mongodb is *webscale*. (source: [[http://www.mongodb-is-web-scale.com/][mongodb-is-web-scale]]) 81 | 82 | blocks like the quote block parse their content and can contain 83 | - lists 84 | - inline /markup/ 85 | - tables 86 | | foo | 87 | | bar | 88 | | baz | 89 | - paragraphs 90 | - ... 91 | 92 | whitespace is honored and not removed (but is not displayed because that's how html works by default) 93 | it can be made visible using css (e.g. =white-space: pre=). 94 | #+END_QUOTE 95 | 96 | #+BEGIN_SRC org 97 | ,#+BEGIN_SRC bash 98 | echo src (with language org) and example blocks support escaping using commata 99 | ,#+END_SRC 100 | 101 | ,,* I am not a real headline - commata escape characters aren't renderered 102 | 103 | #+END_SRC 104 | 105 | #+BEGIN_EXAMPLE 106 | ,#+BEGIN_SRC bash 107 | echo src (with language org) and example blocks support escaping using commata 108 | ,#+END_SRC 109 | 110 | ,,* I am not a real headline - commata escape characters aren't renderered 111 | #+END_EXAMPLE 112 | 113 | #+BEGIN_EXPORT html 114 | 117 | #+END_EXPORT 118 | 119 | #+BEGIN_EXPORT something-other-than-html 120 | I won't be rendered as html 121 | #+END_EXPORT 122 | 123 | 124 | - list item 1 125 | blocks can contain unindented lines that would normally end a list item 126 | #+BEGIN_EXAMPLE 127 | this line is not indented - if it was outside of a block the list item would end 128 | #+END_EXAMPLE 129 | #+BEGIN_QUOTE 130 | this line is not indented - if it was outside of a block the list item would end 131 | #+END_QUOTE 132 | now we're outside the block again and the following unindented line will be outside of the list item 133 | this unindented line is outside of the list item 134 | - list item 2 135 | #+BEGIN_SRC 136 | #+BEGIN_EXAMPLE 137 | #+END_SRC 138 | #+END_EXAMPLE 139 | 140 | #+BEGIN_QUOTE 141 | #+BEGIN_EXAMPLE 142 | #+END_QUOTE 143 | #+END_EXAMPLE 144 | #+END_QUOTE 145 | 146 | - verse blocks 147 | - emacs / ox-hugo rendering 148 | #+BEGIN_EXPORT html 149 |

150 | Great clouds overhead
151 | Tiny black birds rise and fall
152 | Snow covers Emacs
153 |
154 |    ---AlexSchroeder
155 |

156 | #+END_EXPORT 157 | - go-org rendering 158 | #+BEGIN_SRC html 159 | 163 | #+END_SRC 164 | 165 | #+BEGIN_EXPORT html 166 | 170 | #+END_EXPORT 171 | 172 | #+BEGIN_VERSE 173 | Great clouds overhead 174 | Tiny black birds rise and fall 175 | Snow covers Emacs 176 | 177 | ---AlexSchroeder 178 | #+END_VERSE 179 | 180 | #+BEGIN_SRC raku :results output :noweb strip-export :exports both 181 | <>describe ; 182 | #+END_SRC 183 | -------------------------------------------------------------------------------- /org/testdata/blocks.pretty_org: -------------------------------------------------------------------------------- 1 | #+RESULTS: 2 | : some results without a block 3 | 4 | #+CAPTION: block caption 5 | #+BEGIN_SRC bash :results raw 6 | echo "a bash source block" 7 | 8 | function hello { 9 | echo Hello World! 10 | } 11 | 12 | hello 13 | #+END_SRC 14 | 15 | #+BEGIN_SRC 16 | 17 | a source block with leading newline, trailing newline characters 18 | and a line started 19 | with leading space 20 | and line leading tab. 21 | 22 | #+END_SRC 23 | 24 | #+BEGIN_SRC 25 | a source block without a language 26 | #+END_SRC 27 | 28 | 29 | #+BEGIN_SRC bash 30 | echo a source block with results 31 | #+END_SRC 32 | 33 | #+RESULTS: 34 | : a source block with results 35 | 36 | #+BEGIN_SRC bash :exports none 37 | echo a source block with results that is not exported 38 | #+END_SRC 39 | 40 | #+RESULTS: 41 | : a source block with results that is not exported 42 | 43 | #+BEGIN_SRC bash :exports results 44 | echo a source block that only exports results 45 | #+END_SRC 46 | 47 | #+RESULTS: 48 | : a source block that only exports results 49 | 50 | #+BEGIN_SRC bash :cmdline -foo -bar :exports results 51 | # the code block is not rendered 52 | echo but the result block is 53 | #+END_SRC 54 | 55 | #+RESULTS: 56 | : but the result block is 57 | 58 | #+BEGIN_EXAMPLE foo bar baz 59 | an example block with 60 | multiple lines including 61 | 62 | 63 | empty lines! 64 | 65 | it also has multiple parameters 66 | 67 | src, example & export blocks treat their content as raw text 68 | /inline/ *markup* is ignored 69 | and whitespace is honored and not removed 70 | 71 | content of example blocks is still html escaped - see 72 | #+END_EXAMPLE 73 | 74 | : examples like this 75 | : are also supported 76 | : 77 | : note that /inline/ *markup* ignored 78 | 79 | #+BEGIN_QUOTE 80 | Mongodb is *webscale*. (source: [[http://www.mongodb-is-web-scale.com/][mongodb-is-web-scale]]) 81 | 82 | blocks like the quote block parse their content and can contain 83 | - lists 84 | - inline /markup/ 85 | - tables 86 | | foo | 87 | | bar | 88 | | baz | 89 | - paragraphs 90 | - ... 91 | 92 | whitespace is honored and not removed (but is not displayed because that's how html works by default) 93 | it can be made visible using css (e.g. =white-space: pre=). 94 | #+END_QUOTE 95 | 96 | #+BEGIN_SRC org 97 | ,#+BEGIN_SRC bash 98 | echo src (with language org) and example blocks support escaping using commata 99 | ,#+END_SRC 100 | 101 | ,,* I am not a real headline - commata escape characters aren't renderered 102 | 103 | #+END_SRC 104 | 105 | #+BEGIN_EXAMPLE 106 | ,#+BEGIN_SRC bash 107 | echo src (with language org) and example blocks support escaping using commata 108 | ,#+END_SRC 109 | 110 | ,,* I am not a real headline - commata escape characters aren't renderered 111 | #+END_EXAMPLE 112 | 113 | #+BEGIN_EXPORT html 114 | 117 | #+END_EXPORT 118 | 119 | #+BEGIN_EXPORT something-other-than-html 120 | I won't be rendered as html 121 | #+END_EXPORT 122 | 123 | 124 | - list item 1 125 | blocks can contain unindented lines that would normally end a list item 126 | #+BEGIN_EXAMPLE 127 | this line is not indented - if it was outside of a block the list item would end 128 | #+END_EXAMPLE 129 | #+BEGIN_QUOTE 130 | this line is not indented - if it was outside of a block the list item would end 131 | #+END_QUOTE 132 | now we're outside the block again and the following unindented line will be outside of the list item 133 | this unindented line is outside of the list item 134 | - list item 2 135 | #+BEGIN_SRC 136 | #+BEGIN_EXAMPLE 137 | #+END_SRC 138 | #+END_EXAMPLE 139 | 140 | #+BEGIN_QUOTE 141 | #+BEGIN_EXAMPLE 142 | ,#+END_QUOTE 143 | #+END_EXAMPLE 144 | #+END_QUOTE 145 | 146 | - verse blocks 147 | - emacs / ox-hugo rendering 148 | #+BEGIN_EXPORT html 149 |

150 | Great clouds overhead
151 | Tiny black birds rise and fall
152 | Snow covers Emacs
153 |
154 |    ---AlexSchroeder
155 |

156 | #+END_EXPORT 157 | - go-org rendering 158 | #+BEGIN_SRC html 159 | 163 | #+END_SRC 164 | 165 | #+BEGIN_EXPORT html 166 | 170 | #+END_EXPORT 171 | 172 | #+BEGIN_VERSE 173 | Great clouds overhead 174 | Tiny black birds rise and fall 175 | Snow covers Emacs 176 | 177 | ---AlexSchroeder 178 | #+END_VERSE 179 | 180 | #+BEGIN_SRC raku :results output :noweb strip-export :exports both 181 | <>describe
; 182 | #+END_SRC 183 | -------------------------------------------------------------------------------- /org/testdata/captions.html: -------------------------------------------------------------------------------- 1 |

Anything can be captioned.

2 |
3 |
4 |
5 |
 6 | echo "i have a caption!"
 7 | 
8 |
9 |
10 |
11 | captioned soure block 12 |
13 |
14 |
15 | https://placekitten.com/200/200#.png
16 | captioned link (image in this case) 17 |
18 |
19 |

20 | note that the whole paragraph is captioned, so a linebreak is needed for images to caption correctly

21 |
22 |

https://placekitten.com/200/200#.png 23 | see?

24 |
25 | captioned link (image in this case) 26 |
27 |
28 | -------------------------------------------------------------------------------- /org/testdata/captions.org: -------------------------------------------------------------------------------- 1 | Anything can be captioned. 2 | 3 | #+CAPTION: captioned soure block 4 | #+BEGIN_SRC sh 5 | echo "i have a caption!" 6 | #+END_SRC 7 | 8 | #+CAPTION: captioned link (image in this case) 9 | [[https://placekitten.com/200/200#.png]] 10 | 11 | note that the whole paragraph is captioned, so a linebreak is needed for images to caption correctly 12 | 13 | #+CAPTION: captioned link (image in this case) 14 | [[https://placekitten.com/200/200#.png]] 15 | see? 16 | 17 | -------------------------------------------------------------------------------- /org/testdata/captions.pretty_org: -------------------------------------------------------------------------------- 1 | Anything can be captioned. 2 | 3 | #+CAPTION: captioned soure block 4 | #+BEGIN_SRC sh 5 | echo "i have a caption!" 6 | #+END_SRC 7 | 8 | #+CAPTION: captioned link (image in this case) 9 | [[https://placekitten.com/200/200#.png]] 10 | 11 | note that the whole paragraph is captioned, so a linebreak is needed for images to caption correctly 12 | 13 | #+CAPTION: captioned link (image in this case) 14 | [[https://placekitten.com/200/200#.png]] 15 | see? 16 | 17 | -------------------------------------------------------------------------------- /org/testdata/east_asian_line_breaks.html: -------------------------------------------------------------------------------- 1 |

2 | Line breaks between multi-byte characters are omitted when the ealb option is set:

3 |
    4 |
  • 中午吃啥
  • 5 |
  • something else 6 | 中午吃啥 7 | something else
  • 8 |
9 | -------------------------------------------------------------------------------- /org/testdata/east_asian_line_breaks.org: -------------------------------------------------------------------------------- 1 | #+OPTIONS: ealb:t 2 | 3 | Line breaks between multi-byte characters are omitted when the =ealb= option is set: 4 | 5 | - 中午 6 | 吃啥 7 | 8 | - something else 9 | 中午 10 | 吃啥 11 | something else 12 | -------------------------------------------------------------------------------- /org/testdata/east_asian_line_breaks.pretty_org: -------------------------------------------------------------------------------- 1 | #+OPTIONS: ealb:t 2 | 3 | Line breaks between multi-byte characters are omitted when the =ealb= option is set: 4 | 5 | - 中午 6 | 吃啥 7 | 8 | - something else 9 | 中午 10 | 吃啥 11 | something else 12 | -------------------------------------------------------------------------------- /org/testdata/footnotes.html: -------------------------------------------------------------------------------- 1 |
9 |
10 |

11 | Using some footnotes 12 |

13 |
14 |
    15 |
  • normal footnote reference 1 2 3 (footnote names can be anything in the format [\w-])
  • 16 |
  • further references to the same footnote should not 1 render duplicates in the footnote list
  • 17 |
  • inline footnotes are also supported via 4.
  • 18 |
  • anonymous inline footnotes are also supported via 5.
  • 19 |
  • Footnote definitions are not printed where they appear. 20 | Rather, they are gathered and exported at the end of the document in the footnote section. 6
  • 21 |
  • footnotes that reference a non-existant definition are rendered but log a warning 7
  • 22 |
23 |
24 |
25 |
26 |

27 | Footnotes 28 |

29 |
30 |

Please note that the footnotes section is not automatically excluded from the export like in emacs. 8

31 |

32 | this is not part of 8 anymore as there are 2 blank lines in between!

33 |
34 |
35 |
36 |
37 |
38 |
39 | 1 40 |
41 |

https://www.example.com

42 |
    43 |
  • footnotes can contain markup
  • 44 |
  • 45 |

    and other elements

    46 |
      47 |
    • 48 |

      like blocks

      49 |
      50 |
      51 |
       52 | other non-plain
       53 | 
      54 |
      55 |
      56 |
    • 57 |
    • 58 |

      and tables

      59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 |
      1a
      2b
      3c
      75 |
    • 76 |
    77 |
  • 78 |
79 |
80 |
81 |
82 | 2 83 |
84 |

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 |
88 |
89 |
90 | 3 91 |
92 |

yolo

93 |
94 |
95 |
96 | 4 97 |
98 |

the inline footnote definition

99 |
100 |
101 |
102 | 5 103 |
104 |

the anonymous inline footnote definition

105 |
106 |
107 |
108 | 6 109 |
110 |

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 |
113 |
114 |
115 | 8 116 |
117 |

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 |
121 |
122 |
123 | 9 124 |
125 |

Footnotes can be linked from another footnote's definition.

126 |
127 |
128 |
129 |
130 | -------------------------------------------------------------------------------- /org/testdata/footnotes.org: -------------------------------------------------------------------------------- 1 | * Using some footnotes 2 | - normal footnote reference [fn:1] [fn:6] [fn:foo-bar] (footnote names can be anything in the format =[\w-]=) 3 | - further references to the same footnote should not [fn:1] render duplicates in the footnote list 4 | - inline footnotes are also supported via [fn:2:the inline footnote definition]. 5 | - anonymous inline footnotes are also supported via [fn::the anonymous inline footnote definition]. 6 | - Footnote definitions are not printed where they appear. 7 | Rather, they are gathered and exported at the end of the document in the footnote section. [fn:4] 8 | - footnotes that reference a non-existant definition are rendered but log a warning [fn:does-not-exist] 9 | 10 | [fn:4] so this definition will not be at the end of this section in the exported document. 11 | Rather, it will be somewhere down below in the footnotes section. 12 | 13 | [fn:5] this definition will also not be exported here - not only that, it will be overwritten by a definition 14 | of the same name later on in the document. That will log a warning but carry on nonetheless. 15 | * Footnotes 16 | Please note that the footnotes section is not automatically excluded from the export like in emacs. [fn:7] 17 | 18 | [fn:foo-bar] yolo 19 | 20 | [fn:1] https://www.example.com 21 | - footnotes can contain *markup* 22 | - and other elements 23 | - like blocks 24 | #+BEGIN_SRC 25 | other non-plain 26 | #+END_SRC 27 | - and tables 28 | | 1 | a | 29 | | 2 | b | 30 | | 3 | c | 31 | 32 | [fn:3] [[http://example.com/unused-footnote][example.com/unused-footnote]] 33 | 34 | [fn:5] another unused footnote (this definition overwrites the previous definition of =fn:5=) 35 | 36 | [fn:6] 37 | 38 | Footnotes break after two consecutive empty lines - just like paragraphs - see https://orgmode.org/worg/dev/org-syntax.html. 39 | This shouldn't happen when the definition line and the line after that are empty. 40 | 41 | 42 | [fn:7] 43 | There's multiple reasons for that. Among others, doing so requires i18n (to recognize the section) and silently 44 | hides content before and after the footnotes[fn:8]. 45 | 46 | 47 | 48 | this is not part of [fn:7] anymore as there are 2 blank lines in between! 49 | 50 | 51 | [fn:8] Footnotes can be linked from another footnote's definition. 52 | -------------------------------------------------------------------------------- /org/testdata/footnotes.pretty_org: -------------------------------------------------------------------------------- 1 | * Using some footnotes 2 | - normal footnote reference [fn:1] [fn:6] [fn:foo-bar] (footnote names can be anything in the format =[\w-]=) 3 | - further references to the same footnote should not [fn:1] render duplicates in the footnote list 4 | - inline footnotes are also supported via [fn:2:the inline footnote definition]. 5 | - anonymous inline footnotes are also supported via [fn::the anonymous inline footnote definition]. 6 | - Footnote definitions are not printed where they appear. 7 | Rather, they are gathered and exported at the end of the document in the footnote section. [fn:4] 8 | - footnotes that reference a non-existant definition are rendered but log a warning [fn:does-not-exist] 9 | 10 | [fn:4] so this definition will not be at the end of this section in the exported document. 11 | Rather, it will be somewhere down below in the footnotes section. 12 | 13 | [fn:5] this definition will also not be exported here - not only that, it will be overwritten by a definition 14 | of the same name later on in the document. That will log a warning but carry on nonetheless. 15 | * Footnotes 16 | Please note that the footnotes section is not automatically excluded from the export like in emacs. [fn:7] 17 | 18 | [fn:foo-bar] yolo 19 | 20 | [fn:1] https://www.example.com 21 | - footnotes can contain *markup* 22 | - and other elements 23 | - like blocks 24 | #+BEGIN_SRC 25 | other non-plain 26 | #+END_SRC 27 | - and tables 28 | | 1 | a | 29 | | 2 | b | 30 | | 3 | c | 31 | 32 | [fn:3] [[http://example.com/unused-footnote][example.com/unused-footnote]] 33 | 34 | [fn:5] another unused footnote (this definition overwrites the previous definition of =fn:5=) 35 | 36 | [fn:6] 37 | 38 | Footnotes break after two consecutive empty lines - just like paragraphs - see https://orgmode.org/worg/dev/org-syntax.html. 39 | This shouldn't happen when the definition line and the line after that are empty. 40 | 41 | 42 | [fn:7] 43 | There's multiple reasons for that. Among others, doing so requires i18n (to recognize the section) and silently 44 | hides content before and after the footnotes[fn:8]. 45 | 46 | 47 | 48 | this is not part of [fn:7] anymore as there are 2 blank lines in between! 49 | 50 | 51 | [fn:8] Footnotes can be linked from another footnote's definition. 52 | -------------------------------------------------------------------------------- /org/testdata/footnotes_in_headline.html: -------------------------------------------------------------------------------- 1 | 7 |
8 |

9 | Title 1 10 |

11 |
12 |
13 |
14 |
15 |
16 | 1 17 |
18 |

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 |
    21 |
  • HTMLWriter.footnotes should be a pointer field. I didn't notice my error as go translated my pointer-method calls on 22 | non-pointer values rather than complaining - i.e. footnotes.add() transparently gets translated to (&footnotes).add() (docs).
  • 23 |
  • Headlines have to be htmlified twice - once for the outline and once for the headline itself. To do so we have to copy the writer
  • 24 |
  • Copying the writer copies footnotes - which contains a map and a slice. Changes to the map will always be reflected in the original map. 25 | Changes to the slice will only be reflected if the slice doesn't grow.
  • 26 |
  • We can thus end up with a footnote being in the mapping but not the slice - and get an index out of range error.
  • 27 |
28 |
29 |
30 |
31 |
32 | -------------------------------------------------------------------------------- /org/testdata/footnotes_in_headline.org: -------------------------------------------------------------------------------- 1 | * Title [fn:1] 2 | 3 | [fn:1] this test file just exists to reproduce a bug with footnotes in headlines - that only happens in very specific circumstances. 4 | The TLDR is: 5 | - HTMLWriter.footnotes should be a pointer field. I didn't notice my error as go translated my pointer-method calls on 6 | non-pointer values rather than complaining - i.e. =footnotes.add()= transparently gets translated to =(&footnotes).add()= ([[https://golang.org/ref/spec#Calls][docs]]). 7 | - Headlines have to be htmlified twice - once for the outline and once for the headline itself. To do so we have to copy the writer 8 | - Copying the writer copies footnotes - which contains a map and a slice. Changes to the map will always be reflected in the original map. 9 | Changes to the slice will only be reflected if the slice doesn't grow. 10 | - We can thus end up with a footnote being in the mapping but not the slice - and get an index out of range error. 11 | -------------------------------------------------------------------------------- /org/testdata/footnotes_in_headline.pretty_org: -------------------------------------------------------------------------------- 1 | * Title [fn:1] 2 | 3 | [fn:1] this test file just exists to reproduce a bug with footnotes in headlines - that only happens in very specific circumstances. 4 | The TLDR is: 5 | - HTMLWriter.footnotes should be a pointer field. I didn't notice my error as go translated my pointer-method calls on 6 | non-pointer values rather than complaining - i.e. =footnotes.add()= transparently gets translated to =(&footnotes).add()= ([[https://golang.org/ref/spec#Calls][docs]]). 7 | - Headlines have to be htmlified twice - once for the outline and once for the headline itself. To do so we have to copy the writer 8 | - Copying the writer copies footnotes - which contains a map and a slice. Changes to the map will always be reflected in the original map. 9 | Changes to the slice will only be reflected if the slice doesn't grow. 10 | - We can thus end up with a footnote being in the mapping but not the slice - and get an index out of range error. 11 | -------------------------------------------------------------------------------- /org/testdata/headlines.html: -------------------------------------------------------------------------------- 1 | 19 |
20 |

21 | Simple Headline [1/2] 22 |

23 |
24 |
    25 |
  • checked
  • 26 |
  • unchecked
  • 27 |
  • note that statistic tokens are marked up anywhere 28 | not just where they are actually meant to be - even here > [100%] < 29 | (Org mode proper does the same)
  • 30 |
31 |
32 |
33 |
34 |

35 | TODO 36 | [B] 37 | Headline with todo status & priority 38 |

39 |
40 |
41 |

42 | DONE 43 | Headline with TODO status 44 |

45 |
46 |

47 | we can link to headlines that define a custom_id: #this-will-be-the-id-of-the-headline

48 |
49 |
50 |
51 |

52 | [A] 53 | Headline with tags & priority   foo bar 54 |

55 |
56 |

Still outside the drawer

57 |

This is inside the drawer

58 |

Still outside the drawer

59 |
60 |
61 |
62 |

63 | CUSTOM 64 | headline with custom status 65 |

66 |
67 |

it's possible to use #+SETUPFILE - in this case the setup file contains the following

68 |
69 |
70 |
 71 | #+TODO: TODO DONE CUSTOM
 72 | #+EXCLUDE_TAGS: noexport custom_noexport
 73 | 
74 |
75 |
76 |
77 |
78 |
79 |

80 | malformed property drawer 81 |

82 |
83 |

:PROPERTIES: 84 | not a property

85 |

:END:

86 |
87 |
88 |
89 |

90 | level limit for headlines to be included in the table of contents 91 |

92 |
93 |

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 |
96 |

97 | headline 2 not in toc 98 |

99 |
100 |
101 |

102 | headline 3 not in toc 103 |

104 |
105 |
106 |
107 |
108 |

109 | anoter headline 2 not in toc 110 |

111 |
112 |

you get the gist…

113 |
114 |
115 |
116 |
117 | -------------------------------------------------------------------------------- /org/testdata/headlines.org: -------------------------------------------------------------------------------- 1 | #+SETUPFILE: setup_file_org 2 | #+OPTIONS: toc:1 3 | * Simple Headline [1/2] 4 | - [X] checked 5 | - [ ] unchecked 6 | - note that statistic tokens are marked up anywhere 7 | not just where they are actually meant to be - even here > [100%] < 8 | (Org mode proper does the same) 9 | * TODO [#B] Headline with todo status & priority 10 | * DONE Headline with TODO status 11 | :PROPERTIES: 12 | :custom_id: this-will-be-the-id-of-the-headline 13 | :note: property drawers are not exported as html like other drawers 14 | :END: 15 | 16 | we can link to headlines that define a custom_id: [[#this-will-be-the-id-of-the-headline]] 17 | * [#A] Headline with tags & priority :foo:bar: 18 | Still outside the drawer 19 | :DRAWERNAME: 20 | This is inside the drawer 21 | :end: 22 | Still outside the drawer 23 | * CUSTOM headline with custom status 24 | it's possible to use =#+SETUPFILE= - in this case the setup file contains the following 25 | 26 | #+INCLUDE: "setup_file_org" src org 27 | * excluded headline :custom_noexport: 28 | this headline and it's content are not exported as it is marked with an =EXCLUDE_TAGS= tag. 29 | By default =EXCLUDE_TAGS= is just =:noexport:=. 30 | 31 | * TODO [#A] COMMENT commented headline 32 | this headline is commented out. see [[https://orgmode.org/manual/Comment-Lines.html][comment lines]] 33 | * malformed property drawer 34 | :PROPERTIES: 35 | not a property 36 | :END: 37 | * level limit for headlines to be included in the table of contents 38 | The toc option allows setting a [[https://orgmode.org/manual/Export-settings.html][level limit]]. For this file we set it to 1 - which means that the following headlines 39 | won't be included in the table of contents. 40 | ** headline 2 not in toc 41 | *** headline 3 not in toc 42 | ** anoter headline 2 not in toc 43 | you get the gist... 44 | -------------------------------------------------------------------------------- /org/testdata/headlines.pretty_org: -------------------------------------------------------------------------------- 1 | #+SETUPFILE: setup_file_org 2 | #+OPTIONS: toc:1 3 | * Simple Headline [1/2] 4 | - [X] checked 5 | - [ ] unchecked 6 | - note that statistic tokens are marked up anywhere 7 | not just where they are actually meant to be - even here > [100%] < 8 | (Org mode proper does the same) 9 | * TODO [#B] Headline with todo status & priority 10 | * DONE Headline with TODO status 11 | :PROPERTIES: 12 | :CUSTOM_ID: this-will-be-the-id-of-the-headline 13 | :NOTE: property drawers are not exported as html like other drawers 14 | :END: 15 | 16 | we can link to headlines that define a custom_id: [[#this-will-be-the-id-of-the-headline]] 17 | * [#A] Headline with tags & priority :foo:bar: 18 | Still outside the drawer 19 | :DRAWERNAME: 20 | This is inside the drawer 21 | :END: 22 | Still outside the drawer 23 | * CUSTOM headline with custom status 24 | it's possible to use =#+SETUPFILE= - in this case the setup file contains the following 25 | 26 | #+INCLUDE: "setup_file_org" src org 27 | * excluded headline :custom_noexport: 28 | this headline and it's content are not exported as it is marked with an =EXCLUDE_TAGS= tag. 29 | By default =EXCLUDE_TAGS= is just =:noexport:=. 30 | 31 | * TODO [#A] commented headline 32 | this headline is commented out. see [[https://orgmode.org/manual/Comment-Lines.html][comment lines]] 33 | * malformed property drawer 34 | :PROPERTIES: 35 | not a property 36 | :END: 37 | * level limit for headlines to be included in the table of contents 38 | The toc option allows setting a [[https://orgmode.org/manual/Export-settings.html][level limit]]. For this file we set it to 1 - which means that the following headlines 39 | won't be included in the table of contents. 40 | ** headline 2 not in toc 41 | *** headline 3 not in toc 42 | ** anoter headline 2 not in toc 43 | you get the gist... 44 | -------------------------------------------------------------------------------- /org/testdata/hl-lines.html: -------------------------------------------------------------------------------- 1 |

Lines in a source block can be highlighted with :hl_lines.

2 |
3 |
4 |
 5 | (+ 1 2)
 6 | (+ 1 2)
 7 | (+ 1 2)
 8 | (+ 1 2)
 9 | (+ 1 2)
10 | 
11 |
12 |
13 | -------------------------------------------------------------------------------- /org/testdata/hl-lines.org: -------------------------------------------------------------------------------- 1 | Lines in a source block can be highlighted with =:hl_lines=. 2 | 3 | #+begin_src emacs-lisp :hl_lines 3-4 4 | (+ 1 2) 5 | (+ 1 2) 6 | (+ 1 2) 7 | (+ 1 2) 8 | (+ 1 2) 9 | #+end_src 10 | -------------------------------------------------------------------------------- /org/testdata/hl-lines.pretty_org: -------------------------------------------------------------------------------- 1 | Lines in a source block can be highlighted with =:hl_lines=. 2 | 3 | #+BEGIN_SRC emacs-lisp :hl_lines 3-4 4 | (+ 1 2) 5 | (+ 1 2) 6 | (+ 1 2) 7 | (+ 1 2) 8 | (+ 1 2) 9 | #+END_SRC 10 | -------------------------------------------------------------------------------- /org/testdata/inline.html: -------------------------------------------------------------------------------- 1 |
    2 |
  • emphasis and a hard line break
    3 | see?
    4 | also hard line breaks not followed by a newline get ignored, see \\
  • 5 |
  • .emphasis with dot border chars.
  • 6 |
  • emphasis with a slash/inside
  • 7 |
  • emphasis followed by raw text with slash /
  • 8 |
  • emphasis ending with a "difficult" multibyte character 习
  • 9 |
  • emphasis just before explict line break
    10 | plus more emphasis
  • 11 |
  • ->/not an emphasis/<-
  • 12 |
  • links with slashes do not become emphasis: https://somelinkshouldntrenderaccidentalemphasis.com/ emphasis
  • 13 |
  • underlined bold verbatim code strikethrough
  • 14 |
  • bold string with an *asterisk inside
  • 15 |
  • inline source blocks like
    16 |
    17 |
    18 | <h1>hello</h1>
    19 | 
    20 |
    21 |
    and this
    22 |
    23 |
    24 | world
    25 | 
    26 |
    27 |
  • 28 |
  • inline export blocks

    hello

  • 29 |
  • 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/
  • 38 |
  • empty emphasis markers like ++ // __ and so on are ignored
  • 39 |
  • use _{} for subscriptsub and ^{} for superscriptsuper
  • 40 |
  • 41 |

    links

    42 |
      43 |
    1. regular link https://example.com link without description
    2. 44 |
    3. regular link example.com link with description
    4. 45 |
    5. regular link to a file (image) my-img.png
    6. 46 |
    7. regular link to an org file (extension replaced with html) inline.html / ../testdata/inline.html
    8. 47 |
    9. regular link to a file (video)
    10. 48 |
    11. regular link to http (image) http://placekitten.com/200/200#.png
    12. 49 |
    13. regular link to https (image) https://placekitten.com/200/200#.png
    14. 50 |
    15. regular link with image as description https://placekitten.com/200/200#.png
    16. 51 |
    17. regular link enclosed in [] [https://www.example.com] [example.com]
    18. 52 |
    19. auto link, i.e. not inside \[[square brackets]\] https://www.example.com
    20. 53 |
    54 |
  • 55 |
  • 56 |

    timestamps

    57 |
      58 |
    • <2019-01-06 Sun>
    • 59 |
    • <2019-01-06 Sun>
    • 60 |
    • <2019-01-06 Sun 18:00>
    • 61 |
    • <2019-01-06 Sun 18:00 +1w>
    • 62 |
    • <2019-01-06 Sun 18:00>
    • 63 |
    • <2019-01-06 Sun 18:00 +1w>
    • 64 |
    65 |
  • 66 |
  • 67 |

    #+LINK based links:

    68 | 76 |
  • 77 |
  • 78 |

    #+MACROs:

    yolo

    79 |

    80 |
  • 81 |
  • 82 |

    org entities

    83 |
      84 |
    • \pi & \pi{} => π & π
    • 85 |
    • \angle{} & \angle & \ang > ∠ ∠ ∠
    • 86 |
    87 |
  • 88 |
89 | -------------------------------------------------------------------------------- /org/testdata/inline.org: -------------------------------------------------------------------------------- 1 | - /emphasis/ and a hard line break \\ 2 | see? \\ 3 | also hard line breaks not followed by a newline get ignored, see \\ 4 | - /.emphasis with dot border chars./ 5 | - /emphasis with a slash/inside/ 6 | - /emphasis/ followed by raw text with slash / 7 | - *emphasis ending with a "difficult" multibyte character 习* 8 | - emphasis just before =explict line break=\\ 9 | =plus more emphasis= 10 | - ->/not an emphasis/<- 11 | - links with slashes do not become /emphasis/: [[https://somelinkshouldntrenderaccidentalemphasis.com]]/ /emphasis/ 12 | - _underlined_ *bold* =verbatim= ~code~ +strikethrough+ 13 | - *bold string with an *asterisk inside* 14 | - inline source blocks like src_html[:eval no]{

hello

} and this src_schema[:eval no]{world} 15 | - inline export blocks @@html:

hello

@@ 16 | - =multiline emphasis is 17 | supported - and respects MaxEmphasisNewLines (default: 1)= 18 | /so this 19 | is emphasized/ 20 | 21 | /but 22 | this 23 | is 24 | not emphasized/ 25 | - empty emphasis markers like ++ // __ and so on are ignored 26 | - use _{} for subscript_{sub} and ^{} for superscript^{super} 27 | - links 28 | 1. regular link [[https://example.com]] link without description 29 | 2. regular link [[https://example.com][example.com]] link with description 30 | 3. regular link to a file (image) [[file:my-img.png]] 31 | 4. regular link to an org file (extension replaced with html) [[file:inline.org]] / [[../testdata/inline.org]] 32 | 5. regular link to a file (video) [[my-video.mp4]] 33 | 6. regular link to http (image) [[http://placekitten.com/200/200#.png]] 34 | 7. regular link to https (image) [[https://placekitten.com/200/200#.png]] 35 | 8. regular link with image as description [[https://placekitten.com][https://placekitten.com/200/200#.png]] 36 | 9. regular link enclosed in [] [[[https://www.example.com]]] [[[https://www.example.com][example.com]]] 37 | 10. auto link, i.e. not inside =\[[square brackets]\]= https://www.example.com 38 | - timestamps 39 | - <2019-01-06> 40 | - <2019-01-06 Sun> 41 | - <2019-01-06 Sun 18:00> 42 | - <2019-01-06 Sun 18:00 +1w> 43 | - <2019-01-06 18:00> 44 | - <2019-01-06 18:00 +1w> 45 | - =#+LINK= based links: 46 | #+LINK: example https://www.example.com/ 47 | #+LINK: example_interpolate_s https://www.example.com?raw_tag=%s 48 | #+LINK: example_interpolate_h https://www.example.com?encoded_tag=%h 49 | - [[example:foobar]] 50 | - [[example:]] 51 | - [[example]] 52 | - [[example][description]] 53 | - [[example_interpolate_s:tag value with specical chars % : &]] (w/o tag [[example_interpolate_s]]) 54 | - [[example_interpolate_h:tag value with specical chars % : &]] (w/o tag [[example_interpolate_h]]) 55 | - =#+MACROs=: {{{headline(yolo)}}} 56 | #+MACRO: headline @@html:

$1

@@ 57 | - org entities 58 | - =\pi= & =\pi{}= => \pi & \pi{} 59 | - =\angle{}= & =\angle= & =\ang= =>= \angle{} \angle \ang 60 | -------------------------------------------------------------------------------- /org/testdata/inline.pretty_org: -------------------------------------------------------------------------------- 1 | - /emphasis/ and a hard line break \\ 2 | see? \\ 3 | also hard line breaks not followed by a newline get ignored, see \\ 4 | - /.emphasis with dot border chars./ 5 | - /emphasis with a slash/inside/ 6 | - /emphasis/ followed by raw text with slash / 7 | - *emphasis ending with a "difficult" multibyte character 习* 8 | - emphasis just before =explict line break=\\ 9 | =plus more emphasis= 10 | - ->/not an emphasis/<- 11 | - links with slashes do not become /emphasis/: [[https://somelinkshouldntrenderaccidentalemphasis.com]]/ /emphasis/ 12 | - _underlined_ *bold* =verbatim= ~code~ +strikethrough+ 13 | - *bold string with an *asterisk inside* 14 | - inline source blocks like src_html[:eval no]{

hello

} and this src_schema[:eval no]{world} 15 | - inline export blocks @@html:

hello

@@ 16 | - =multiline emphasis is 17 | supported - and respects MaxEmphasisNewLines (default: 1)= 18 | /so this 19 | is emphasized/ 20 | 21 | /but 22 | this 23 | is 24 | not emphasized/ 25 | - empty emphasis markers like ++ // __ and so on are ignored 26 | - use _{} for subscript_{sub} and ^{} for superscript^{super} 27 | - links 28 | 1. regular link [[https://example.com]] link without description 29 | 2. regular link [[https://example.com][example.com]] link with description 30 | 3. regular link to a file (image) [[file:my-img.png]] 31 | 4. regular link to an org file (extension replaced with html) [[file:inline.org]] / [[../testdata/inline.org]] 32 | 5. regular link to a file (video) [[my-video.mp4]] 33 | 6. regular link to http (image) [[http://placekitten.com/200/200#.png]] 34 | 7. regular link to https (image) [[https://placekitten.com/200/200#.png]] 35 | 8. regular link with image as description [[https://placekitten.com][https://placekitten.com/200/200#.png]] 36 | 9. regular link enclosed in [] [[[https://www.example.com]]] [[[https://www.example.com][example.com]]] 37 | 10. auto link, i.e. not inside =\[[square brackets]\]= https://www.example.com 38 | - timestamps 39 | - <2019-01-06 Sun> 40 | - <2019-01-06 Sun> 41 | - <2019-01-06 Sun 18:00> 42 | - <2019-01-06 Sun 18:00 +1w> 43 | - <2019-01-06 Sun 18:00> 44 | - <2019-01-06 Sun 18:00 +1w> 45 | - =#+LINK= based links: 46 | #+LINK: example https://www.example.com/ 47 | #+LINK: example_interpolate_s https://www.example.com?raw_tag=%s 48 | #+LINK: example_interpolate_h https://www.example.com?encoded_tag=%h 49 | - [[example:foobar]] 50 | - [[example:]] 51 | - [[example]] 52 | - [[example][description]] 53 | - [[example_interpolate_s:tag value with specical chars % : &]] (w/o tag [[example_interpolate_s]]) 54 | - [[example_interpolate_h:tag value with specical chars % : &]] (w/o tag [[example_interpolate_h]]) 55 | - =#+MACROs=: {{{headline(yolo)}}} 56 | #+MACRO: headline @@html:

$1

@@ 57 | - org entities 58 | - =\pi= & =\pi{}= => \pi & \pi{} 59 | - =\angle{}= & =\angle= & =\ang= =>= \angle{} \angle \ang 60 | -------------------------------------------------------------------------------- /org/testdata/keywords.html: -------------------------------------------------------------------------------- 1 |
2 |

3 | captions, custom attributes and more 4 |

5 |
6 |
7 |
8 |
9 |
echo "a bash source block with custom html attributes"
10 | 
11 |
12 |
13 |
14 | and multiple lines of captions! 15 |
16 |
17 |

18 | and an image with custom html attributes and a caption

19 |
20 | https://placekitten.com/200/200#.png 21 |
22 | kittens! 23 |
24 |
25 |

named paragraph

26 |
27 |
28 |
29 | named block
30 | 
31 |
32 |
33 |

#not a comment because there's no space after the hashtag

34 |
35 |
36 |
37 |

38 | table of contents 39 |

40 |
41 |

A table of contents can be rendered anywhere in the document by using

42 |
43 |
44 |
45 | #+TOC: headlines $n
46 | 
47 |
48 |
49 |

Where $n is the max headline lvl that will be included. You can use headlines 0 to include all headlines.

50 | 58 |
59 |
60 | -------------------------------------------------------------------------------- /org/testdata/keywords.org: -------------------------------------------------------------------------------- 1 | #+OPTIONS: toc:nil 2 | * captions, custom attributes and more 3 | #+CAPTION: and _multiple_ 4 | #+CAPTION: lines of *captions*! 5 | #+ATTR_HTML: :class a b 6 | #+ATTR_HTML: :id it :class c d 7 | #+BEGIN_SRC sh 8 | echo "a bash source block with custom html attributes" 9 | #+END_SRC 10 | 11 | and an image with custom html attributes and a caption 12 | #+CAPTION: kittens! 13 | #+ATTR_HTML: :style height: 100%; :id overwritten 14 | #+ATTR_HTML: :style border: 10px solid black; :id kittens 15 | [[https://placekitten.com/200/200#.png]] 16 | 17 | #+NAME: foo 18 | named paragraph 19 | 20 | #+NAME: bar 21 | #+begin_src 22 | named block 23 | #+end_src 24 | 25 | # comments must have whitespace after the hashtag 26 | #not a comment because there's no space after the hashtag 27 | 28 | * table of contents 29 | A table of contents can be rendered anywhere in the document by using 30 | #+begin_src org 31 | ,#+TOC: headlines $n 32 | #+end_src 33 | Where =$n= is the max headline lvl that will be included. You can use =headlines 0= to include all headlines. 34 | #+TOC: headlines 0 35 | -------------------------------------------------------------------------------- /org/testdata/keywords.pretty_org: -------------------------------------------------------------------------------- 1 | #+OPTIONS: toc:nil 2 | * captions, custom attributes and more 3 | #+CAPTION: and _multiple_ 4 | #+CAPTION: lines of *captions*! 5 | #+ATTR_HTML: :class a b 6 | #+ATTR_HTML: :id it :class c d 7 | #+BEGIN_SRC sh 8 | echo "a bash source block with custom html attributes" 9 | #+END_SRC 10 | 11 | and an image with custom html attributes and a caption 12 | #+CAPTION: kittens! 13 | #+ATTR_HTML: :style height: 100%; :id overwritten 14 | #+ATTR_HTML: :style border: 10px solid black; :id kittens 15 | [[https://placekitten.com/200/200#.png]] 16 | 17 | #+NAME: foo 18 | named paragraph 19 | 20 | #+NAME: bar 21 | #+BEGIN_SRC 22 | named block 23 | #+END_SRC 24 | 25 | # comments must have whitespace after the hashtag 26 | #not a comment because there's no space after the hashtag 27 | 28 | * table of contents 29 | A table of contents can be rendered anywhere in the document by using 30 | #+BEGIN_SRC org 31 | ,#+TOC: headlines $n 32 | #+END_SRC 33 | Where =$n= is the max headline lvl that will be included. You can use =headlines 0= to include all headlines. 34 | #+TOC: headlines 0 35 | -------------------------------------------------------------------------------- /org/testdata/latex.html: -------------------------------------------------------------------------------- 1 |

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.

3 |
    4 |
  • i=1^n a_n (without latex delimiter)
  • 5 |
  • \(\sum_{i=1}^n a_n\)
  • 6 |
  • \[\sum_{i=1}^n a_n\]
  • 7 |
  • $$\sum_{i=1}^n a_n$$
  • 8 |
  • \begin{xyz}\sum_{i=1}^n a_n\end{xyz}
  • 9 |
  • 10 | \begin{xyz} 11 | \sum_{i=1}^n a_n 12 | \end{xyz} 13 |
  • 14 |
  • $2 + 2$, $3 - 3$
  • 15 |
  • 16 | \begin{xyz} 17 | latex block ignores block lvl elements (e.g. list - line starting with -) 18 | a = b 19 | - c 20 | - d 21 | \end{xyz} 22 |
  • 23 |
24 | -------------------------------------------------------------------------------- /org/testdata/latex.org: -------------------------------------------------------------------------------- 1 | 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. 3 | 4 | - \sum_{i=1}^n a_n (without latex delimiter) 5 | - \(\sum_{i=1}^n a_n\) 6 | - \[\sum_{i=1}^n a_n\] 7 | - $$\sum_{i=1}^n a_n$$ 8 | - \begin{xyz}\sum_{i=1}^n a_n\end{xyz} 9 | - \begin{xyz} 10 | \sum_{i=1}^n a_n 11 | \end{xyz} 12 | - $2 + 2$, $3 - 3$ 13 | - \begin{xyz} 14 | latex block ignores block lvl elements (e.g. list - line starting with -) 15 | a = b 16 | - c 17 | - d 18 | \end{xyz} 19 | -------------------------------------------------------------------------------- /org/testdata/latex.pretty_org: -------------------------------------------------------------------------------- 1 | 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. 3 | 4 | - \sum_{i=1}^n a_n (without latex delimiter) 5 | - \(\sum_{i=1}^n a_n\) 6 | - \[\sum_{i=1}^n a_n\] 7 | - $$\sum_{i=1}^n a_n$$ 8 | - \begin{xyz}\sum_{i=1}^n a_n\end{xyz} 9 | - \begin{xyz} 10 | \sum_{i=1}^n a_n 11 | \end{xyz} 12 | - $2 + 2$, $3 - 3$ 13 | - \begin{xyz} 14 | latex block ignores block lvl elements (e.g. list - line starting with -) 15 | a = b 16 | - c 17 | - d 18 | \end{xyz} 19 | -------------------------------------------------------------------------------- /org/testdata/lists.html: -------------------------------------------------------------------------------- 1 |
    2 |
  • unordered list item 1
  • 3 |
  • 4 | 5 | list item with empty first and second line
    6 | normally an empty line breaks the list item - but we make an exception for the first line and don't count it towards that limit
  • 7 |
  • 8 |

    unordered list item 2 - with inline markup

    9 |
      10 |
    1. 11 |

      ordered sublist item 1

      12 |
        13 |
      1. ordered sublist item 1
      2. 14 |
      3. ordered sublist item 2
      4. 15 |
      5. ordered sublist item 3
      6. 16 |
      17 |
    2. 18 |
    3. ordered sublist item 2
    4. 19 |
    5. 20 | 21 | list item with empty first and second line - see above
    6. 22 |
    23 |
  • 24 |
  • 25 |

    unordered list item 3 - and a link 26 | and some lines of text

    27 |
      28 |
    1. 29 |

      and another subitem

      30 |
      31 |
      32 |
       33 | echo with a block
       34 | 
      35 |
      36 |
      37 |
    2. 38 |
    3. 39 |

      and another one with a table

      40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 |
      abc
      123
      56 |

      57 | and text with an empty line in between as well!

      58 |
    4. 59 |
    60 |
  • 61 |
  • 62 |

    unordered list item 4

    63 |
     64 | with an example
     65 | 
     66 | that spans multiple lines
     67 | 
    68 |
  • 69 |
70 |

71 | descriptive lists

72 |
73 |
74 | term 75 |
76 |
details 77 | continued details
78 |
79 | ? 80 |
81 |
details without a term
82 |
83 | term 84 |
85 |
86 | details on a new line
87 |
88 | term 89 |
90 |
91 |

92 | details on a new line (with an empty line in between) 93 | continued

94 |
95 |
96 |
 97 | echo "Hello World!"
 98 | 
99 |
100 |
101 |
102 |
103 |

some list termination tests

104 |
    105 |
  • unordered 1
  • 106 |
  • unordered 2
  • 107 |
108 |
    109 |
  1. ordered 1
  2. 110 |
  3. ordered 2
  4. 111 |
112 |
    113 |
  1. ordered 1
  2. 114 |
  3. ordered 2
  4. 115 |
116 |
    117 |
  • unordered 1
  • 118 |
  • unordered 2
  • 119 |
120 |
    121 |
  1. ordered 1
  2. 122 |
  3. ordered 2
  4. 123 |
124 |
125 |
126 | unordered descriptive 127 |
128 |
1
129 |
130 | unordered descriptive 131 |
132 |
2
133 |
134 |
135 |
136 | ordered descriptive 137 |
138 |
1
139 |
140 | ordered descriptive 141 |
142 |
2
143 |
144 |
    145 |
  • unordered 1
  • 146 |
  • unordered 2
  • 147 |
148 |
    149 |
  1. use `[@n]` to change the value of list items
  2. 150 |
  3. foobar
  4. 151 |
  5. that even works in combination with list statuses (`[ ]`)
  6. 152 |
153 | -------------------------------------------------------------------------------- /org/testdata/lists.org: -------------------------------------------------------------------------------- 1 | - [ ] unordered list item 1 2 | - 3 | 4 | list item with empty first and second line \\ 5 | normally an empty line breaks the list item - but we make an exception for the first line and don't count it towards that limit 6 | - unordered list item 2 - with ~inline~ /markup/ 7 | 1. [-] ordered sublist item 1 8 | a) [X] ordered sublist item 1 9 | b) [ ] ordered sublist item 2 10 | c) [X] ordered sublist item 3 11 | 2. ordered sublist item 2 12 | 3. 13 | 14 | list item with empty first and second line - see above 15 | - [X] unordered list item 3 - and a [[https://example.com][link]] 16 | and some lines of text 17 | 1. and another subitem 18 | #+BEGIN_SRC sh 19 | echo with a block 20 | #+END_SRC 21 | 2. and another one with a table 22 | | a | b | c | 23 | |---+---+---| 24 | | 1 | 2 | 3 | 25 | 26 | and text with an empty line in between as well! 27 | - unordered list item 4 28 | : with an example 29 | : 30 | : that spans multiple lines 31 | 32 | 33 | descriptive lists 34 | - [ ] term :: details 35 | continued details 36 | - [ ] details without a term 37 | - [X] term :: 38 | details on a new line 39 | - term :: 40 | 41 | details on a new line (with an empty line in between) 42 | *continued* 43 | #+BEGIN_SRC bash 44 | echo "Hello World!" 45 | #+END_SRC 46 | 47 | some list termination tests 48 | 49 | - unordered 1 50 | - unordered 2 51 | 1. ordered 1 52 | 2. ordered 2 53 | 54 | 55 | 1. ordered 1 56 | 2. ordered 2 57 | - unordered 1 58 | - unordered 2 59 | 60 | 61 | 1. ordered 1 62 | 2. ordered 2 63 | - unordered descriptive :: 1 64 | - unordered descriptive :: 2 65 | 66 | 67 | 1. ordered descriptive :: 1 68 | 2. ordered descriptive :: 2 69 | - unordered 1 70 | - unordered 2 71 | 72 | 73 | 1. [@2] use `[@n]` to change the value of list items 74 | 2. [ ] foobar 75 | 3. [@10] [X] that even works in combination with list statuses (`[ ]`) 76 | -------------------------------------------------------------------------------- /org/testdata/lists.pretty_org: -------------------------------------------------------------------------------- 1 | - [ ] unordered list item 1 2 | - 3 | 4 | list item with empty first and second line \\ 5 | normally an empty line breaks the list item - but we make an exception for the first line and don't count it towards that limit 6 | - unordered list item 2 - with ~inline~ /markup/ 7 | 1. [-] ordered sublist item 1 8 | a) [X] ordered sublist item 1 9 | b) [ ] ordered sublist item 2 10 | c) [X] ordered sublist item 3 11 | 2. ordered sublist item 2 12 | 3. 13 | 14 | list item with empty first and second line - see above 15 | - [X] unordered list item 3 - and a [[https://example.com][link]] 16 | and some lines of text 17 | 1. and another subitem 18 | #+BEGIN_SRC sh 19 | echo with a block 20 | #+END_SRC 21 | 2. and another one with a table 22 | | a | b | c | 23 | |---+---+---| 24 | | 1 | 2 | 3 | 25 | 26 | and text with an empty line in between as well! 27 | - unordered list item 4 28 | : with an example 29 | : 30 | : that spans multiple lines 31 | 32 | 33 | descriptive lists 34 | - [ ] term :: details 35 | continued details 36 | - [ ] details without a term 37 | - [X] term :: 38 | details on a new line 39 | - term :: 40 | 41 | details on a new line (with an empty line in between) 42 | *continued* 43 | #+BEGIN_SRC bash 44 | echo "Hello World!" 45 | #+END_SRC 46 | 47 | some list termination tests 48 | 49 | - unordered 1 50 | - unordered 2 51 | 1. ordered 1 52 | 2. ordered 2 53 | 54 | 55 | 1. ordered 1 56 | 2. ordered 2 57 | - unordered 1 58 | - unordered 2 59 | 60 | 61 | 1. ordered 1 62 | 2. ordered 2 63 | - unordered descriptive :: 1 64 | - unordered descriptive :: 2 65 | 66 | 67 | 1. ordered descriptive :: 1 68 | 2. ordered descriptive :: 2 69 | - unordered 1 70 | - unordered 2 71 | 72 | 73 | 1. [@2] use `[@n]` to change the value of list items 74 | 2. [ ] foobar 75 | 3. [@10] [X] that even works in combination with list statuses (`[ ]`) 76 | -------------------------------------------------------------------------------- /org/testdata/misc.org: -------------------------------------------------------------------------------- 1 | #+TITLE: Misc title @@html:with an inline html export@@ 2 | ** issues from goorgeous (free test cases, yay!) 3 | *** DONE [[https://github.com/chaseadamsio/goorgeous/issues/19][#19]]: Support #+HTML 4 | #+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:
33 | | *foo* | foo | 34 | | *bar* | bar | 35 | #+HTML:
36 | *** DONE [[https://github.com/chaseadamsio/goorgeous/issues/41][#41]]: Support Table Of Contents 37 | *** DONE [[https://github.com/chaseadamsio/goorgeous/issues/46][#46]]: Support for symbols like ndash and mdash 38 | - ndash -- 39 | - mdash --- 40 | - ellipsis ... 41 | - acute \Aacute and so on 42 | - note that ------ is replaced with 2 mdashes and .... becomes ellipsis+. and so on - that's how org also does it 43 | 44 | 45 | 46 | *** DONE [[https://github.com/chaseadamsio/goorgeous/issues/47][#47:]] Consecutive ~code~ wrapped text gets joined 47 | either ~this~ or ~that~ foo. 48 | either ~this~ 49 | or ~that~ foo. 50 | *** DONE [[https://github.com/chaseadamsio/goorgeous/issues/50][#50]]: LineBreaks in lists are preserved 51 | - this list item 52 | has 53 | multiple 54 | linbreaks - but it's still just one paragraph (i.e. no line breaks are rendered) 55 | - foobar 56 | 1. same 57 | goes 58 | for 59 | ordered 60 | lists 61 | 2. foo 62 | *** DONE [[https://github.com/chaseadamsio/goorgeous/issues/68][#68]]: Quote block with inline markup 63 | #+BEGIN_QUOTE 64 | [[https://www.example.com][/this/ *is* _markup_!]] 65 | #+END_QUOTE 66 | *** DONE [[https://github.com/chaseadamsio/goorgeous/issues/72][#72]]: Support for #+ATTR_HTML 67 | #+ATTR_HTML: :alt Go is fine though. :id gopher-image 68 | #+ATTR_HTML: :width 300 :style border:2px solid black; 69 | [[https://golang.org/doc/gopher/pkg.png]] 70 | *** DONE [[https://github.com/chaseadamsio/goorgeous/issues/75][#75]]: Not parsing nested lists correctly 71 | - bullet 1 72 | - sub bullet 73 | 74 | *** DONE [[https://github.com/chaseadamsio/goorgeous/issues/77][#77]]: Recognize =code=--- as code plus dash 75 | *** DONE [[https://github.com/chaseadamsio/goorgeous/issues/78][#78]]: Emphasis at beginning of line 76 | /italics/ 77 | 78 | 79 | Text 80 | /italics/ 81 | *** DONE [[https://github.com/chaseadamsio/goorgeous/issues/82][#82]]: Crash on empty headline 82 | **** 83 | just a space as title... 84 | *** DONE [[https://github.com/chaseadamsio/goorgeous/issues/84][#84]]: Paragraphs that are not followed by an empty line are not parsed correctly 85 | **** Foo 86 | Foo paragraph. 87 | **** Bar 88 | Bar paragraph 89 | *** DONE [[https://github.com/chaseadamsio/goorgeous/issues/86][#86]]: Multiple hyphens not converted to dashes 90 | just like #46 91 | - =--= -> -- (en dash) 92 | - =---= -> --- (em dash) 93 | 94 | also, consecutive dashes inside 95 | - inline code =--= =---= and verbatim ~--~ ~---~ 96 | - src/example/export blocks should not be converted! 97 | #+BEGIN_SRC sh 98 | --, --- 99 | #+END_SRC 100 | 101 | #+BEGIN_EXAMPLE 102 | --, --- 103 | #+END_EXAMPLE 104 | 105 | #+BEGIN_EXPORT html 106 | --, --- 107 | #+END_EXPORT 108 | 109 | : --, --- 110 | 111 | *** DONE [[https://github.com/chaseadamsio/goorgeous/issues/87][#87]]: Markup in footnotes is rendered literally 112 | footnotes can contain *markup* - and other elements and stuff [fn:1] [fn:2:that also goes for *inline* footnote /definitions/] 113 | *** DONE [[https://github.com/chaseadamsio/goorgeous/issues/92][#92]]: src blocks only render in caps 114 | The behaviour of Org mode =with an inline html export@@ 2 | ** issues from goorgeous (free test cases, yay!) 3 | *** DONE [[https://github.com/chaseadamsio/goorgeous/issues/19][#19]]: Support #+HTML 4 | #+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:
33 | | *foo* | foo | 34 | | *bar* | bar | 35 | #+HTML:
36 | *** DONE [[https://github.com/chaseadamsio/goorgeous/issues/41][#41]]: Support Table Of Contents 37 | *** DONE [[https://github.com/chaseadamsio/goorgeous/issues/46][#46]]: Support for symbols like ndash and mdash 38 | - ndash -- 39 | - mdash --- 40 | - ellipsis ... 41 | - acute \Aacute and so on 42 | - note that ------ is replaced with 2 mdashes and .... becomes ellipsis+. and so on - that's how org also does it 43 | 44 | 45 | 46 | *** DONE [[https://github.com/chaseadamsio/goorgeous/issues/47][#47:]] Consecutive ~code~ wrapped text gets joined 47 | either ~this~ or ~that~ foo. 48 | either ~this~ 49 | or ~that~ foo. 50 | *** DONE [[https://github.com/chaseadamsio/goorgeous/issues/50][#50]]: LineBreaks in lists are preserved 51 | - this list item 52 | has 53 | multiple 54 | linbreaks - but it's still just one paragraph (i.e. no line breaks are rendered) 55 | - foobar 56 | 1. same 57 | goes 58 | for 59 | ordered 60 | lists 61 | 2. foo 62 | *** DONE [[https://github.com/chaseadamsio/goorgeous/issues/68][#68]]: Quote block with inline markup 63 | #+BEGIN_QUOTE 64 | [[https://www.example.com][/this/ *is* _markup_!]] 65 | #+END_QUOTE 66 | *** DONE [[https://github.com/chaseadamsio/goorgeous/issues/72][#72]]: Support for #+ATTR_HTML 67 | #+ATTR_HTML: :alt Go is fine though. :id gopher-image 68 | #+ATTR_HTML: :width 300 :style border:2px solid black; 69 | [[https://golang.org/doc/gopher/pkg.png]] 70 | *** DONE [[https://github.com/chaseadamsio/goorgeous/issues/75][#75]]: Not parsing nested lists correctly 71 | - bullet 1 72 | - sub bullet 73 | 74 | *** DONE [[https://github.com/chaseadamsio/goorgeous/issues/77][#77]]: Recognize =code=--- as code plus dash 75 | *** DONE [[https://github.com/chaseadamsio/goorgeous/issues/78][#78]]: Emphasis at beginning of line 76 | /italics/ 77 | 78 | 79 | Text 80 | /italics/ 81 | *** DONE [[https://github.com/chaseadamsio/goorgeous/issues/82][#82]]: Crash on empty headline 82 | **** 83 | just a space as title... 84 | *** DONE [[https://github.com/chaseadamsio/goorgeous/issues/84][#84]]: Paragraphs that are not followed by an empty line are not parsed correctly 85 | **** Foo 86 | Foo paragraph. 87 | **** Bar 88 | Bar paragraph 89 | *** DONE [[https://github.com/chaseadamsio/goorgeous/issues/86][#86]]: Multiple hyphens not converted to dashes 90 | just like #46 91 | - =--= -> -- (en dash) 92 | - =---= -> --- (em dash) 93 | 94 | also, consecutive dashes inside 95 | - inline code =--= =---= and verbatim ~--~ ~---~ 96 | - src/example/export blocks should not be converted! 97 | #+BEGIN_SRC sh 98 | --, --- 99 | #+END_SRC 100 | 101 | #+BEGIN_EXAMPLE 102 | --, --- 103 | #+END_EXAMPLE 104 | 105 | #+BEGIN_EXPORT html 106 | --, --- 107 | #+END_EXPORT 108 | 109 | : --, --- 110 | 111 | *** DONE [[https://github.com/chaseadamsio/goorgeous/issues/87][#87]]: Markup in footnotes is rendered literally 112 | footnotes can contain *markup* - and other elements and stuff [fn:1] [fn:2:that also goes for *inline* footnote /definitions/] 113 | *** DONE [[https://github.com/chaseadamsio/goorgeous/issues/92][#92]]: src blocks only render in caps 114 | The behaviour of Org mode = 2 |

3 | DONE 4 | [A] 5 | #+OPTIONS: toggles supported by go-org   tag1 tag2 6 |

7 |
8 |

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.

14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 |
keydescription
fInclude footnotes (definitions & links)
eInclude entities
tocInclude table of contents (outline)
priInclude priority [#A], [#B], [#C] in headline title
todoInclude todo status in headline title
tagsInclude tags in headline title
ealbOmit newlines between multi-byte characters (east asian line breaks, non-standard)
56 |
57 | 58 | -------------------------------------------------------------------------------- /org/testdata/options.org: -------------------------------------------------------------------------------- 1 | #+OPTIONS: toc:nil f:nil e:nil 2 | 3 | * DONE [#A] =#+OPTIONS:= toggles supported by =go-org= :tag1:tag2: 4 | =go-org= supports multiple export toggles as described in the [[https://orgmode.org/manual/Export-settings.html][export settings]] section of the Org mode manual. 5 | By default (most of?) those toggles are enabled. This file starts with =#+OPTIONS: toc:nil f:nil e:nil= and thus 6 | disables the table of contents, footnotes & entities. 7 | That means, entities like =---= --- (mdash) will be left untouched, footnotes like =[fn:1]= [fn:1] will 8 | not be exported and there won't be a table of contents at the top. 9 | As buffer options are merged with the defaults, the above headline will be exported *with* priority, todo status & tags. 10 | 11 | | key | description | 12 | |------+------------------------------------------------------------------------------------| 13 | | f | Include footnotes (definitions & links) | 14 | | e | Include entities | 15 | | toc | Include table of contents (outline) | 16 | |------+------------------------------------------------------------------------------------| 17 | | pri | Include priority =[#A]=, =[#B]=, =[#C]= in headline title | 18 | | todo | Include todo status in headline title | 19 | | tags | Include tags in headline title | 20 | |------+------------------------------------------------------------------------------------| 21 | | ealb | Omit newlines between multi-byte characters (east asian line breaks, non-standard) | 22 | 23 | [fn:1] This footnote definition won't be printed 24 | -------------------------------------------------------------------------------- /org/testdata/options.pretty_org: -------------------------------------------------------------------------------- 1 | #+OPTIONS: toc:nil f:nil e:nil 2 | 3 | * DONE [#A] =#+OPTIONS:= toggles supported by =go-org= :tag1:tag2: 4 | =go-org= supports multiple export toggles as described in the [[https://orgmode.org/manual/Export-settings.html][export settings]] section of the Org mode manual. 5 | By default (most of?) those toggles are enabled. This file starts with =#+OPTIONS: toc:nil f:nil e:nil= and thus 6 | disables the table of contents, footnotes & entities. 7 | That means, entities like =---= --- (mdash) will be left untouched, footnotes like =[fn:1]= [fn:1] will 8 | not be exported and there won't be a table of contents at the top. 9 | As buffer options are merged with the defaults, the above headline will be exported *with* priority, todo status & tags. 10 | 11 | | key | description | 12 | |------+------------------------------------------------------------------------------------| 13 | | f | Include footnotes (definitions & links) | 14 | | e | Include entities | 15 | | toc | Include table of contents (outline) | 16 | |------+------------------------------------------------------------------------------------| 17 | | pri | Include priority =[#A]=, =[#B]=, =[#C]= in headline title | 18 | | todo | Include todo status in headline title | 19 | | tags | Include tags in headline title | 20 | |------+------------------------------------------------------------------------------------| 21 | | ealb | Omit newlines between multi-byte characters (east asian line breaks, non-standard) | 22 | 23 | [fn:1] This footnote definition won't be printed 24 | -------------------------------------------------------------------------------- /org/testdata/paragraphs.html: -------------------------------------------------------------------------------- 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 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 |
2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
abc
123
18 |
19 | table with separator before and after header 20 |
21 |
22 |
23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 |
abc
123
39 |
40 | table with separator after header 41 |
42 |
43 |
44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 |
CharacterOrgRendered HTML
Hyphena - ba - b
Ndasha -- ba – b
Mdasha --- ba — b
Ellipsisa ... ba … b
75 |
76 | table with unicode characters 77 |
78 |
79 |
80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 |
123
89 |
90 | table without header (but separator before) 91 |
92 |
93 |
94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 |
123
103 |
104 | table without header 105 |
106 |
107 |
108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 |
left alignedright alignedcenter aligned
424242
foobarfoobarfoobar
129 |
130 | table with aligned and sized columns 131 |
132 |
133 |
134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 |
long column along column blong column c
123
150 |
151 | table with right aligned columns (because numbers) 152 |
153 |
154 |
155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 |
abc
123
...
123
123
190 |
191 | table with multiple separators (~ multiple tbodies) 192 |
193 |
194 | -------------------------------------------------------------------------------- /org/testdata/tables.org: -------------------------------------------------------------------------------- 1 | #+CAPTION: table with separator before and after header 2 | |---+---+---| 3 | | a | b | c | 4 | |---+---+---| 5 | | 1 | 2 | 3 | 6 | 7 | #+CAPTION: table with separator after header 8 | | a | b | c | 9 | |---+---+---| 10 | | 1 | 2 | 3 | 11 | 12 | #+CAPTION: table with unicode characters 13 | | Character | Org | Rendered HTML | 14 | |-----------+-----------+---------------| 15 | | Hyphen | =a - b= | a - b | 16 | | Ndash | =a -- b= | a – b | 17 | | Mdash | =a --- b= | a — b | 18 | | Ellipsis | =a ... b= | a … b | 19 | 20 | #+CAPTION: table without header (but separator before) 21 | |---+---+---| 22 | | 1 | 2 | 3 | 23 | 24 | #+CAPTION: table without header 25 | | 1 | 2 | 3 | 26 | 27 | #+CAPTION: table with aligned and sized columns 28 | | left aligned | right aligned | center aligned | 29 | |--------------+---------------+----------------| 30 | | | | | 31 | | | <1> | | 32 | | 42 | 42 | 42 | 33 | | foobar | foobar | foobar | 34 | 35 | #+CAPTION: table with right aligned columns (because numbers) 36 | | long column a | long column b | long column c | 37 | |---------------+---------------+---------------| 38 | | 1 | 2 | 3 | 39 | 40 | #+CAPTION: table with multiple separators (~ multiple tbodies) 41 | | a | b | c | 42 | |---+---+---| 43 | | 1 | 2 | 3 | 44 | | . | . | . | 45 | |---+---+---| 46 | | 1 | 2 | 3 | 47 | |---+---+---| 48 | | 1 | 2 | 3 | 49 | -------------------------------------------------------------------------------- /org/testdata/tables.pretty_org: -------------------------------------------------------------------------------- 1 | #+CAPTION: table with separator before and after header 2 | |---+---+---| 3 | | a | b | c | 4 | |---+---+---| 5 | | 1 | 2 | 3 | 6 | 7 | #+CAPTION: table with separator after header 8 | | a | b | c | 9 | |---+---+---| 10 | | 1 | 2 | 3 | 11 | 12 | #+CAPTION: table with unicode characters 13 | | Character | Org | Rendered HTML | 14 | |-----------+-----------+---------------| 15 | | Hyphen | =a - b= | a - b | 16 | | Ndash | =a -- b= | a – b | 17 | | Mdash | =a --- b= | a — b | 18 | | Ellipsis | =a ... b= | a … b | 19 | 20 | #+CAPTION: table without header (but separator before) 21 | |---+---+---| 22 | | 1 | 2 | 3 | 23 | 24 | #+CAPTION: table without header 25 | | 1 | 2 | 3 | 26 | 27 | #+CAPTION: table with aligned and sized columns 28 | | left aligned | right aligned | center aligned | 29 | |--------------+---------------+----------------| 30 | | | | | 31 | | | <1> | | 32 | | 42 | 42 | 42 | 33 | | foobar | foobar | foobar | 34 | 35 | #+CAPTION: table with right aligned columns (because numbers) 36 | | long column a | long column b | long column c | 37 | |---------------+---------------+---------------| 38 | | 1 | 2 | 3 | 39 | 40 | #+CAPTION: table with multiple separators (~ multiple tbodies) 41 | | a | b | c | 42 | |---+---+---| 43 | | 1 | 2 | 3 | 44 | | . | . | . | 45 | |---+---+---| 46 | | 1 | 2 | 3 | 47 | |---+---+---| 48 | | 1 | 2 | 3 | 49 | -------------------------------------------------------------------------------- /org/util.go: -------------------------------------------------------------------------------- 1 | package org 2 | 3 | import ( 4 | "strconv" 5 | "strings" 6 | ) 7 | 8 | func isSecondBlankLine(d *Document, i int) bool { 9 | if i-1 <= 0 { 10 | return false 11 | } 12 | t1, t2 := d.tokens[i-1], d.tokens[i] 13 | if t1.kind == "text" && t2.kind == "text" && t1.content == "" && t2.content == "" { 14 | return true 15 | } 16 | return false 17 | } 18 | 19 | func isImageOrVideoLink(n Node) bool { 20 | if l, ok := n.(RegularLink); ok && l.Kind() == "video" || l.Kind() == "image" { 21 | return true 22 | } 23 | return false 24 | } 25 | 26 | // Parse ranges like this: 27 | // "3-5" -> [[3, 5]] 28 | // "3 8-10" -> [[3, 3], [8, 10]] 29 | // "3 5 6" -> [[3, 3], [5, 5], [6, 6]] 30 | // 31 | // This is Hugo's hlLinesToRanges with "startLine" removed and errors 32 | // ignored. 33 | func ParseRanges(s string) [][2]int { 34 | var ranges [][2]int 35 | s = strings.TrimSpace(s) 36 | if s == "" { 37 | return ranges 38 | } 39 | fields := strings.Split(s, " ") 40 | for _, field := range fields { 41 | field = strings.TrimSpace(field) 42 | if field == "" { 43 | continue 44 | } 45 | numbers := strings.Split(field, "-") 46 | var r [2]int 47 | if len(numbers) > 1 { 48 | first, err := strconv.Atoi(numbers[0]) 49 | if err != nil { 50 | return ranges 51 | } 52 | second, err := strconv.Atoi(numbers[1]) 53 | if err != nil { 54 | return ranges 55 | } 56 | r[0] = first 57 | r[1] = second 58 | } else { 59 | first, err := strconv.Atoi(numbers[0]) 60 | if err != nil { 61 | return ranges 62 | } 63 | r[0] = first 64 | r[1] = first 65 | } 66 | 67 | ranges = append(ranges, r) 68 | } 69 | return ranges 70 | } 71 | 72 | func IsNewLineChar(r rune) bool { 73 | return r == '\n' || r == '\r' 74 | } 75 | -------------------------------------------------------------------------------- /org/util_test.go: -------------------------------------------------------------------------------- 1 | package org 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | var parseRangesTests = map[string][][2]int{ 9 | "3-5": {{3, 5}}, 10 | "3 8-10": {{3, 3}, {8, 10}}, 11 | "3 5 6": {{3, 3}, {5, 5}, {6, 6}}, 12 | " 9-10 5-6 3 ": {{9, 10}, {5, 6}, {3, 3}}, 13 | } 14 | 15 | func TestParseRanges(t *testing.T) { 16 | for s, expected := range parseRangesTests { 17 | t.Run(s, func(t *testing.T) { 18 | actual := ParseRanges(s) 19 | // If this fails it looks like: 20 | // util_test.go:: 9-10 5-6 3 : 21 | // --- Actual 22 | // +++ Expected 23 | // @@ -1 +1 @@ 24 | // -[[9 10] [5 9] [3 3]] 25 | // +[[9 10] [5 6] [3 3]] 26 | if len(actual) != len(expected) { 27 | t.Errorf("%v:\n%v", s, diff(fmt.Sprintf("%v", actual), fmt.Sprintf("%v", expected))) 28 | } else { 29 | for i := range actual { 30 | if actual[i] != expected[i] { 31 | t.Errorf("%v:\n%v", s, diff(fmt.Sprintf("%v", actual), fmt.Sprintf("%v", expected))) 32 | } 33 | } 34 | } 35 | }) 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /org/writer.go: -------------------------------------------------------------------------------- 1 | package org 2 | 3 | import "fmt" 4 | 5 | // Writer is the interface that is used to export a parsed document into a new format. See Document.Write(). 6 | type Writer interface { 7 | Before(*Document) // Before is called before any nodes are passed to the writer. 8 | After(*Document) // After is called after all nodes have been passed to the writer. 9 | String() string // String is called at the very end to retrieve the final output. 10 | 11 | WriterWithExtensions() Writer 12 | WriteNodesAsString(...Node) string 13 | 14 | WriteKeyword(Keyword) 15 | WriteInclude(Include) 16 | WriteComment(Comment) 17 | WriteNodeWithMeta(NodeWithMeta) 18 | WriteNodeWithName(NodeWithName) 19 | WriteHeadline(Headline) 20 | WriteBlock(Block) 21 | WriteResult(Result) 22 | WriteLatexBlock(LatexBlock) 23 | WriteInlineBlock(InlineBlock) 24 | WriteExample(Example) 25 | WriteDrawer(Drawer) 26 | WritePropertyDrawer(PropertyDrawer) 27 | WriteList(List) 28 | WriteListItem(ListItem) 29 | WriteDescriptiveListItem(DescriptiveListItem) 30 | WriteTable(Table) 31 | WriteHorizontalRule(HorizontalRule) 32 | WriteParagraph(Paragraph) 33 | WriteText(Text) 34 | WriteEmphasis(Emphasis) 35 | WriteLatexFragment(LatexFragment) 36 | WriteStatisticToken(StatisticToken) 37 | WriteExplicitLineBreak(ExplicitLineBreak) 38 | WriteLineBreak(LineBreak) 39 | WriteRegularLink(RegularLink) 40 | WriteMacro(Macro) 41 | WriteTimestamp(Timestamp) 42 | WriteFootnoteLink(FootnoteLink) 43 | WriteFootnoteDefinition(FootnoteDefinition) 44 | } 45 | 46 | func WriteNodes(w Writer, nodes ...Node) { 47 | w = w.WriterWithExtensions() 48 | for _, n := range nodes { 49 | switch n := n.(type) { 50 | case Keyword: 51 | w.WriteKeyword(n) 52 | case Include: 53 | w.WriteInclude(n) 54 | case Comment: 55 | w.WriteComment(n) 56 | case NodeWithMeta: 57 | w.WriteNodeWithMeta(n) 58 | case NodeWithName: 59 | w.WriteNodeWithName(n) 60 | case Headline: 61 | w.WriteHeadline(n) 62 | case Block: 63 | w.WriteBlock(n) 64 | case Result: 65 | w.WriteResult(n) 66 | case LatexBlock: 67 | w.WriteLatexBlock(n) 68 | case InlineBlock: 69 | w.WriteInlineBlock(n) 70 | case Example: 71 | w.WriteExample(n) 72 | case Drawer: 73 | w.WriteDrawer(n) 74 | case PropertyDrawer: 75 | w.WritePropertyDrawer(n) 76 | case List: 77 | w.WriteList(n) 78 | case ListItem: 79 | w.WriteListItem(n) 80 | case DescriptiveListItem: 81 | w.WriteDescriptiveListItem(n) 82 | case Table: 83 | w.WriteTable(n) 84 | case HorizontalRule: 85 | w.WriteHorizontalRule(n) 86 | case Paragraph: 87 | w.WriteParagraph(n) 88 | case Text: 89 | w.WriteText(n) 90 | case Emphasis: 91 | w.WriteEmphasis(n) 92 | case LatexFragment: 93 | w.WriteLatexFragment(n) 94 | case StatisticToken: 95 | w.WriteStatisticToken(n) 96 | case ExplicitLineBreak: 97 | w.WriteExplicitLineBreak(n) 98 | case LineBreak: 99 | w.WriteLineBreak(n) 100 | case RegularLink: 101 | w.WriteRegularLink(n) 102 | case Macro: 103 | w.WriteMacro(n) 104 | case Timestamp: 105 | w.WriteTimestamp(n) 106 | case FootnoteLink: 107 | w.WriteFootnoteLink(n) 108 | case FootnoteDefinition: 109 | w.WriteFootnoteDefinition(n) 110 | default: 111 | if n != nil { 112 | panic(fmt.Sprintf("bad node %T %#v", n, n)) 113 | } 114 | } 115 | } 116 | } 117 | --------------------------------------------------------------------------------