Browse Source

Add init support of orgmode document type on file view and readme (#2525)

* add init support of orgmode document type on file view and readme

* fix imports

* fix imports and readmeExist

* fix imports order

* fix format

* remove unnecessary convert
Lunny Xiao 2 years ago
parent
commit
0d80af649a

+ 4 - 0
main.go

@@ -13,6 +13,10 @@ import (
13 13
 	"code.gitea.io/gitea/cmd"
14 14
 	"code.gitea.io/gitea/modules/log"
15 15
 	"code.gitea.io/gitea/modules/setting"
16
+	// register supported doc types
17
+	_ "code.gitea.io/gitea/modules/markup/markdown"
18
+	_ "code.gitea.io/gitea/modules/markup/orgmode"
19
+
16 20
 	"github.com/urfave/cli"
17 21
 )
18 22
 

+ 1 - 1
models/mail.go

@@ -13,8 +13,8 @@ import (
13 13
 	"code.gitea.io/gitea/modules/base"
14 14
 	"code.gitea.io/gitea/modules/log"
15 15
 	"code.gitea.io/gitea/modules/mailer"
16
-	"code.gitea.io/gitea/modules/markdown"
17 16
 	"code.gitea.io/gitea/modules/markup"
17
+	"code.gitea.io/gitea/modules/markup/markdown"
18 18
 	"code.gitea.io/gitea/modules/setting"
19 19
 	"gopkg.in/gomail.v2"
20 20
 	"gopkg.in/macaron.v1"

+ 1 - 1
modules/markup/html_test.go

@@ -10,8 +10,8 @@ import (
10 10
 	"strings"
11 11
 	"testing"
12 12
 
13
-	_ "code.gitea.io/gitea/modules/markdown"
14 13
 	. "code.gitea.io/gitea/modules/markup"
14
+	_ "code.gitea.io/gitea/modules/markup/markdown"
15 15
 	"code.gitea.io/gitea/modules/setting"
16 16
 
17 17
 	"github.com/stretchr/testify/assert"

+ 9 - 9
modules/markdown/markdown.go

@@ -17,8 +17,8 @@ import (
17 17
 // Renderer is a extended version of underlying render object.
18 18
 type Renderer struct {
19 19
 	blackfriday.Renderer
20
-	urlPrefix      string
21
-	isWikiMarkdown bool
20
+	URLPrefix string
21
+	IsWiki    bool
22 22
 }
23 23
 
24 24
 // Link defines how formal links should be processed to produce corresponding HTML elements.
@@ -26,10 +26,10 @@ func (r *Renderer) Link(out *bytes.Buffer, link []byte, title []byte, content []
26 26
 	if len(link) > 0 && !markup.IsLink(link) {
27 27
 		if link[0] != '#' {
28 28
 			lnk := string(link)
29
-			if r.isWikiMarkdown {
29
+			if r.IsWiki {
30 30
 				lnk = markup.URLJoin("wiki", lnk)
31 31
 			}
32
-			mLink := markup.URLJoin(r.urlPrefix, lnk)
32
+			mLink := markup.URLJoin(r.URLPrefix, lnk)
33 33
 			link = []byte(mLink)
34 34
 		}
35 35
 	}
@@ -95,8 +95,8 @@ var (
95 95
 
96 96
 // Image defines how images should be processed to produce corresponding HTML elements.
97 97
 func (r *Renderer) Image(out *bytes.Buffer, link []byte, title []byte, alt []byte) {
98
-	prefix := r.urlPrefix
99
-	if r.isWikiMarkdown {
98
+	prefix := r.URLPrefix
99
+	if r.IsWiki {
100 100
 		prefix = markup.URLJoin(prefix, "wiki", "src")
101 101
 	}
102 102
 	prefix = strings.Replace(prefix, "/src/", "/raw/", 1)
@@ -129,9 +129,9 @@ func RenderRaw(body []byte, urlPrefix string, wikiMarkdown bool) []byte {
129 129
 	htmlFlags |= blackfriday.HTML_SKIP_STYLE
130 130
 	htmlFlags |= blackfriday.HTML_OMIT_CONTENTS
131 131
 	renderer := &Renderer{
132
-		Renderer:       blackfriday.HtmlRenderer(htmlFlags, "", ""),
133
-		urlPrefix:      urlPrefix,
134
-		isWikiMarkdown: wikiMarkdown,
132
+		Renderer:  blackfriday.HtmlRenderer(htmlFlags, "", ""),
133
+		URLPrefix: urlPrefix,
134
+		IsWiki:    wikiMarkdown,
135 135
 	}
136 136
 
137 137
 	// set up the parser

+ 1 - 42
modules/markdown/markdown_test.go

@@ -5,13 +5,11 @@
5 5
 package markdown_test
6 6
 
7 7
 import (
8
-	"fmt"
9
-	"strconv"
10 8
 	"strings"
11 9
 	"testing"
12 10
 
13
-	. "code.gitea.io/gitea/modules/markdown"
14 11
 	"code.gitea.io/gitea/modules/markup"
12
+	. "code.gitea.io/gitea/modules/markup/markdown"
15 13
 	"code.gitea.io/gitea/modules/setting"
16 14
 
17 15
 	"github.com/stretchr/testify/assert"
@@ -21,45 +19,6 @@ const AppURL = "http://localhost:3000/"
21 19
 const Repo = "gogits/gogs"
22 20
 const AppSubURL = AppURL + Repo + "/"
23 21
 
24
-var numericMetas = map[string]string{
25
-	"format": "https://someurl.com/{user}/{repo}/{index}",
26
-	"user":   "someUser",
27
-	"repo":   "someRepo",
28
-	"style":  markup.IssueNameStyleNumeric,
29
-}
30
-
31
-var alphanumericMetas = map[string]string{
32
-	"format": "https://someurl.com/{user}/{repo}/{index}",
33
-	"user":   "someUser",
34
-	"repo":   "someRepo",
35
-	"style":  markup.IssueNameStyleAlphanumeric,
36
-}
37
-
38
-// numericLink an HTML to a numeric-style issue
39
-func numericIssueLink(baseURL string, index int) string {
40
-	return link(markup.URLJoin(baseURL, strconv.Itoa(index)), fmt.Sprintf("#%d", index))
41
-}
42
-
43
-// alphanumLink an HTML link to an alphanumeric-style issue
44
-func alphanumIssueLink(baseURL string, name string) string {
45
-	return link(markup.URLJoin(baseURL, name), name)
46
-}
47
-
48
-// urlContentsLink an HTML link whose contents is the target URL
49
-func urlContentsLink(href string) string {
50
-	return link(href, href)
51
-}
52
-
53
-// link an HTML link
54
-func link(href, contents string) string {
55
-	return fmt.Sprintf("<a href=\"%s\">%s</a>", href, contents)
56
-}
57
-
58
-func testRenderIssueIndexPattern(t *testing.T, input, expected string, metas map[string]string) {
59
-	assert.Equal(t, expected,
60
-		string(markup.RenderIssueIndexPattern([]byte(input), AppSubURL, metas)))
61
-}
62
-
63 22
 func TestRender_StandardLinks(t *testing.T) {
64 23
 	setting.AppURL = AppURL
65 24
 	setting.AppSubURL = AppSubURL

+ 1 - 1
modules/markup/markup_test.go

@@ -7,8 +7,8 @@ package markup_test
7 7
 import (
8 8
 	"testing"
9 9
 
10
-	_ "code.gitea.io/gitea/modules/markdown"
11 10
 	. "code.gitea.io/gitea/modules/markup"
11
+	_ "code.gitea.io/gitea/modules/markup/markdown"
12 12
 
13 13
 	"github.com/stretchr/testify/assert"
14 14
 )

+ 56 - 0
modules/markup/orgmode/orgmode.go

@@ -0,0 +1,56 @@
1
+// Copyright 2017 The Gitea Authors. All rights reserved.
2
+// Use of this source code is governed by a MIT-style
3
+// license that can be found in the LICENSE file.
4
+
5
+package markup
6
+
7
+import (
8
+	"code.gitea.io/gitea/modules/markup"
9
+	"code.gitea.io/gitea/modules/markup/markdown"
10
+
11
+	"github.com/chaseadamsio/goorgeous"
12
+	"github.com/russross/blackfriday"
13
+)
14
+
15
+func init() {
16
+	markup.RegisterParser(Parser{})
17
+}
18
+
19
+// Parser implements markup.Parser for orgmode
20
+type Parser struct {
21
+}
22
+
23
+// Name implements markup.Parser
24
+func (Parser) Name() string {
25
+	return "orgmode"
26
+}
27
+
28
+// Extensions implements markup.Parser
29
+func (Parser) Extensions() []string {
30
+	return []string{".org"}
31
+}
32
+
33
+// Render renders orgmode rawbytes to HTML
34
+func Render(rawBytes []byte, urlPrefix string, metas map[string]string, isWiki bool) []byte {
35
+	htmlFlags := blackfriday.HTML_USE_XHTML
36
+	htmlFlags |= blackfriday.HTML_SKIP_STYLE
37
+	htmlFlags |= blackfriday.HTML_OMIT_CONTENTS
38
+	renderer := &markdown.Renderer{
39
+		Renderer:  blackfriday.HtmlRenderer(htmlFlags, "", ""),
40
+		URLPrefix: urlPrefix,
41
+		IsWiki:    isWiki,
42
+	}
43
+
44
+	result := goorgeous.Org(rawBytes, renderer)
45
+	return result
46
+}
47
+
48
+// RenderString reners orgmode string to HTML string
49
+func RenderString(rawContent string, urlPrefix string, metas map[string]string, isWiki bool) string {
50
+	return string(Render([]byte(rawContent), urlPrefix, metas, isWiki))
51
+}
52
+
53
+// Render implements markup.Parser
54
+func (Parser) Render(rawBytes []byte, urlPrefix string, metas map[string]string, isWiki bool) []byte {
55
+	return Render(rawBytes, urlPrefix, metas, isWiki)
56
+}

+ 54 - 0
modules/markup/orgmode/orgmode_test.go

@@ -0,0 +1,54 @@
1
+// Copyright 2017 The Gitea Authors. All rights reserved.
2
+// Use of this source code is governed by a MIT-style
3
+// license that can be found in the LICENSE file.
4
+
5
+package markup
6
+
7
+import (
8
+	"strings"
9
+	"testing"
10
+
11
+	"code.gitea.io/gitea/modules/markup"
12
+	"code.gitea.io/gitea/modules/setting"
13
+
14
+	"github.com/stretchr/testify/assert"
15
+)
16
+
17
+const AppURL = "http://localhost:3000/"
18
+const Repo = "gogits/gogs"
19
+const AppSubURL = AppURL + Repo + "/"
20
+
21
+func TestRender_StandardLinks(t *testing.T) {
22
+	setting.AppURL = AppURL
23
+	setting.AppSubURL = AppSubURL
24
+
25
+	test := func(input, expected string) {
26
+		buffer := RenderString(input, setting.AppSubURL, nil, false)
27
+		assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
28
+	}
29
+
30
+	googleRendered := `<p><a href="https://google.com/" title="https://google.com/">https://google.com/</a></p>`
31
+	test("[[https://google.com/]]", googleRendered)
32
+
33
+	lnk := markup.URLJoin(AppSubURL, "WikiPage")
34
+	test("[[WikiPage][WikiPage]]",
35
+		`<p><a href="`+lnk+`" title="WikiPage">WikiPage</a></p>`)
36
+}
37
+
38
+func TestRender_Images(t *testing.T) {
39
+	setting.AppURL = AppURL
40
+	setting.AppSubURL = AppSubURL
41
+
42
+	test := func(input, expected string) {
43
+		buffer := RenderString(input, setting.AppSubURL, nil, false)
44
+		assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
45
+	}
46
+
47
+	url := "../../.images/src/02/train.jpg"
48
+	title := "Train"
49
+	result := markup.URLJoin(AppSubURL, url)
50
+
51
+	test(
52
+		"[[file:"+url+"]["+title+"]]",
53
+		`<p><a href="`+result+`"><img src="`+result+`" alt="`+title+`" title="`+title+`" /></a></p>`)
54
+}

+ 1 - 1
routers/api/v1/misc/markdown.go

@@ -8,8 +8,8 @@ import (
8 8
 	api "code.gitea.io/sdk/gitea"
9 9
 
10 10
 	"code.gitea.io/gitea/modules/context"
11
-	"code.gitea.io/gitea/modules/markdown"
12 11
 	"code.gitea.io/gitea/modules/markup"
12
+	"code.gitea.io/gitea/modules/markup/markdown"
13 13
 	"code.gitea.io/gitea/modules/setting"
14 14
 )
15 15
 

+ 1 - 1
routers/repo/issue.go

@@ -24,7 +24,7 @@ import (
24 24
 	"code.gitea.io/gitea/modules/context"
25 25
 	"code.gitea.io/gitea/modules/indexer"
26 26
 	"code.gitea.io/gitea/modules/log"
27
-	"code.gitea.io/gitea/modules/markdown"
27
+	"code.gitea.io/gitea/modules/markup/markdown"
28 28
 	"code.gitea.io/gitea/modules/notification"
29 29
 	"code.gitea.io/gitea/modules/setting"
30 30
 	"code.gitea.io/gitea/modules/util"

+ 1 - 1
routers/repo/release.go

@@ -12,7 +12,7 @@ import (
12 12
 	"code.gitea.io/gitea/modules/base"
13 13
 	"code.gitea.io/gitea/modules/context"
14 14
 	"code.gitea.io/gitea/modules/log"
15
-	"code.gitea.io/gitea/modules/markdown"
15
+	"code.gitea.io/gitea/modules/markup/markdown"
16 16
 	"code.gitea.io/gitea/modules/setting"
17 17
 
18 18
 	"github.com/Unknwon/paginater"

+ 4 - 6
routers/repo/view.go

@@ -95,11 +95,11 @@ func renderDirectory(ctx *context.Context, treeLink string) {
95 95
 			buf = append(buf, d...)
96 96
 			newbuf := markup.Render(readmeFile.Name(), buf, treeLink, ctx.Repo.Repository.ComposeMetas())
97 97
 			if newbuf != nil {
98
-				ctx.Data["IsMarkdown"] = true
98
+				ctx.Data["IsMarkup"] = true
99 99
 			} else {
100 100
 				// FIXME This is the only way to show non-markdown files
101 101
 				// instead of a broken "View Raw" link
102
-				ctx.Data["IsMarkdown"] = true
102
+				ctx.Data["IsMarkup"] = false
103 103
 				newbuf = bytes.Replace(buf, []byte("\n"), []byte(`<br>`), -1)
104 104
 			}
105 105
 			ctx.Data["FileContent"] = string(newbuf)
@@ -197,10 +197,8 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry, treeLink, rawLink st
197 197
 
198 198
 		tp := markup.Type(blob.Name())
199 199
 		isSupportedMarkup := tp != ""
200
-		// FIXME: currently set IsMarkdown for compatible
201
-		ctx.Data["IsMarkdown"] = isSupportedMarkup
202
-
203
-		readmeExist := isSupportedMarkup || markup.IsReadmeFile(blob.Name())
200
+		ctx.Data["IsMarkup"] = isSupportedMarkup
201
+		readmeExist := markup.IsReadmeFile(blob.Name())
204 202
 		ctx.Data["ReadmeExist"] = readmeExist
205 203
 		if readmeExist && isSupportedMarkup {
206 204
 			ctx.Data["FileContent"] = string(markup.Render(blob.Name(), buf, path.Dir(treeLink), ctx.Repo.Repository.ComposeMetas()))

+ 1 - 1
routers/repo/wiki.go

@@ -18,8 +18,8 @@ import (
18 18
 	"code.gitea.io/gitea/modules/auth"
19 19
 	"code.gitea.io/gitea/modules/base"
20 20
 	"code.gitea.io/gitea/modules/context"
21
-	"code.gitea.io/gitea/modules/markdown"
22 21
 	"code.gitea.io/gitea/modules/markup"
22
+	"code.gitea.io/gitea/modules/markup/markdown"
23 23
 )
24 24
 
25 25
 const (

+ 2 - 2
templates/repo/view_file.tmpl

@@ -36,8 +36,8 @@
36 36
 		{{end}}
37 37
 	</h4>
38 38
 	<div class="ui attached table segment">
39
-		<div class="file-view {{if .IsMarkdown}}markdown{{else if .IsTextFile}}code-view{{end}} has-emoji">
40
-			{{if .IsMarkdown}}
39
+		<div class="file-view {{if .IsMarkup}}markdown{{else if .IsTextFile}}code-view{{end}} has-emoji">
40
+			{{if .IsMarkup}}
41 41
 				{{if .FileContent}}{{.FileContent | Str2html}}{{end}}
42 42
 			{{else if not .IsTextFile}}
43 43
 				<div class="view-raw ui center">

+ 21 - 0
vendor/github.com/chaseadamsio/goorgeous/LICENSE

@@ -0,0 +1,21 @@
1
+MIT License
2
+
3
+Copyright (c) 2017 Chase Adams <realchaseadams@gmail.com>
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.

+ 66 - 0
vendor/github.com/chaseadamsio/goorgeous/README.org

@@ -0,0 +1,66 @@
1
+#+TITLE: chaseadamsio/goorgeous
2
+
3
+[[https://travis-ci.org/chaseadamsio/goorgeous.svg?branch=master]]
4
+[[https://coveralls.io/repos/github/chaseadamsio/goorgeous/badge.svg?branch=master]]
5
+
6
+/goorgeous is a Go Org to HTML Parser./
7
+
8
+[[file:gopher_small.gif]] 
9
+
10
+*Pronounced: Go? Org? Yes!*
11
+
12
+#+BEGIN_QUOTE
13
+"Org mode is for keeping notes, maintaining TODO lists, planning projects, and authoring documents with a fast and effective plain-text system."
14
+
15
+- [[orgmode.org]]
16
+#+END_QUOTE
17
+
18
+The purpose of this package is to come as close as possible as parsing an =*.org= document into HTML, the same way one might publish [[http://orgmode.org/worg/org-tutorials/org-publish-html-tutorial.html][with org-publish-html from Emacs]]. 
19
+
20
+* Installation
21
+
22
+#+BEGIN_SRC sh
23
+  go get -u github.com/chaseadamsio/goorgeous
24
+#+END_SRC
25
+
26
+* Usage
27
+
28
+** Org Headers
29
+
30
+To retrieve the headers from a =[]byte=, call =OrgHeaders= and it will return a =map[string]interface{}=: 
31
+
32
+#+BEGIN_SRC go
33
+  input := "#+title: goorgeous\n* Some Headline\n"
34
+  out := goorgeous.OrgHeaders(input) 
35
+#+END_SRC
36
+
37
+#+BEGIN_SRC go
38
+  map[string]interface{}{ 
39
+          "title": "goorgeous"
40
+  }
41
+#+END_SRC
42
+
43
+** Org Content
44
+
45
+After importing =github.com/chaseadamsio/goorgeous=, you can call =Org= with a =[]byte= and it will return an =html= version of the content as a =[]byte=
46
+
47
+#+BEGIN_SRC go
48
+  input := "#+TITLE: goorgeous\n* Some Headline\n"
49
+  out := goorgeous.Org(input) 
50
+#+END_SRC
51
+
52
+=out= will be:
53
+
54
+#+BEGIN_SRC html
55
+  <h1>Some Headline</h1>/n
56
+#+END_SRC
57
+
58
+* Why? 
59
+
60
+First off, I've become an unapologetic user of Emacs & ever since finding =org-mode= I use it for anything having to do with writing content, organizing my life and keeping documentation of my days/weeks/months.
61
+
62
+Although I like Emacs & =emacs-lisp=, I publish all of my html sites with [[https://gohugo.io][Hugo Static Site Generator]] and wanted to be able to write my content in =org-mode= in Emacs rather than markdown.
63
+
64
+Hugo's implementation of templating and speed are unmatched, so the only way I knew for sure I could continue to use Hugo and write in =org-mode= seamlessly was to write a golang parser for org content and submit a PR for Hugo to use it.
65
+* Acknowledgements
66
+I leaned heavily on russross' [[https://github.com/russross/blackfriday][blackfriday markdown renderer]] as both an example of how to write a parser (with some updates to leverage the go we know today) and reusing the blackfriday HTML Renderer so I didn't have to write my own!

+ 803 - 0
vendor/github.com/chaseadamsio/goorgeous/goorgeous.go

@@ -0,0 +1,803 @@
1
+package goorgeous
2
+
3
+import (
4
+	"bufio"
5
+	"bytes"
6
+	"regexp"
7
+
8
+	"github.com/russross/blackfriday"
9
+	"github.com/shurcooL/sanitized_anchor_name"
10
+)
11
+
12
+type inlineParser func(p *parser, out *bytes.Buffer, data []byte, offset int) int
13
+
14
+type footnotes struct {
15
+	id  string
16
+	def string
17
+}
18
+
19
+type parser struct {
20
+	r              blackfriday.Renderer
21
+	inlineCallback [256]inlineParser
22
+	notes          []footnotes
23
+}
24
+
25
+// NewParser returns a new parser with the inlineCallbacks required for org content
26
+func NewParser(renderer blackfriday.Renderer) *parser {
27
+	p := new(parser)
28
+	p.r = renderer
29
+
30
+	p.inlineCallback['='] = generateVerbatim
31
+	p.inlineCallback['~'] = generateCode
32
+	p.inlineCallback['/'] = generateEmphasis
33
+	p.inlineCallback['_'] = generateUnderline
34
+	p.inlineCallback['*'] = generateBold
35
+	p.inlineCallback['+'] = generateStrikethrough
36
+	p.inlineCallback['['] = generateLinkOrImg
37
+
38
+	return p
39
+}
40
+
41
+// OrgCommon is the easiest way to parse a byte slice of org content and makes assumptions
42
+// that the caller wants to use blackfriday's HTMLRenderer with XHTML
43
+func OrgCommon(input []byte) []byte {
44
+	renderer := blackfriday.HtmlRenderer(blackfriday.HTML_USE_XHTML, "", "")
45
+	return OrgOptions(input, renderer)
46
+}
47
+
48
+// Org is a convenience name for OrgOptions
49
+func Org(input []byte, renderer blackfriday.Renderer) []byte {
50
+	return OrgOptions(input, renderer)
51
+}
52
+
53
+// OrgOptions takes an org content byte slice and a renderer to use
54
+func OrgOptions(input []byte, renderer blackfriday.Renderer) []byte {
55
+	// in the case that we need to render something in isEmpty but there isn't a new line char
56
+	input = append(input, '\n')
57
+	var output bytes.Buffer
58
+
59
+	p := NewParser(renderer)
60
+
61
+	scanner := bufio.NewScanner(bytes.NewReader(input))
62
+	// used to capture code blocks
63
+	marker := ""
64
+	syntax := ""
65
+	listType := ""
66
+	inParagraph := false
67
+	inList := false
68
+	inTable := false
69
+	inFixedWidthArea := false
70
+	var tmpBlock bytes.Buffer
71
+
72
+	for scanner.Scan() {
73
+		data := scanner.Bytes()
74
+
75
+		if !isEmpty(data) && isComment(data) || IsKeyword(data) {
76
+			switch {
77
+			case inList:
78
+				if tmpBlock.Len() > 0 {
79
+					p.generateList(&output, tmpBlock.Bytes(), listType)
80
+				}
81
+				inList = false
82
+				listType = ""
83
+				tmpBlock.Reset()
84
+			case inTable:
85
+				if tmpBlock.Len() > 0 {
86
+					p.generateTable(&output, tmpBlock.Bytes())
87
+				}
88
+				inTable = false
89
+				tmpBlock.Reset()
90
+			case inParagraph:
91
+				if tmpBlock.Len() > 0 {
92
+					p.generateParagraph(&output, tmpBlock.Bytes()[:len(tmpBlock.Bytes())-1])
93
+				}
94
+				inParagraph = false
95
+				tmpBlock.Reset()
96
+			case inFixedWidthArea:
97
+				if tmpBlock.Len() > 0 {
98
+					tmpBlock.WriteString("</pre>\n")
99
+					output.Write(tmpBlock.Bytes())
100
+				}
101
+				inFixedWidthArea = false
102
+				tmpBlock.Reset()
103
+			}
104
+
105
+		}
106
+
107
+		switch {
108
+		case isEmpty(data):
109
+			switch {
110
+			case inList:
111
+				if tmpBlock.Len() > 0 {
112
+					p.generateList(&output, tmpBlock.Bytes(), listType)
113
+				}
114
+				inList = false
115
+				listType = ""
116
+				tmpBlock.Reset()
117
+			case inTable:
118
+				if tmpBlock.Len() > 0 {
119
+					p.generateTable(&output, tmpBlock.Bytes())
120
+				}
121
+				inTable = false
122
+				tmpBlock.Reset()
123
+			case inParagraph:
124
+				if tmpBlock.Len() > 0 {
125
+					p.generateParagraph(&output, tmpBlock.Bytes()[:len(tmpBlock.Bytes())-1])
126
+				}
127
+				inParagraph = false
128
+				tmpBlock.Reset()
129
+			case inFixedWidthArea:
130
+				if tmpBlock.Len() > 0 {
131
+					tmpBlock.WriteString("</pre>\n")
132
+					output.Write(tmpBlock.Bytes())
133
+				}
134
+				inFixedWidthArea = false
135
+				tmpBlock.Reset()
136
+			case marker != "":
137
+				tmpBlock.WriteByte('\n')
138
+			default:
139
+				continue
140
+			}
141
+		case isPropertyDrawer(data) || marker == "PROPERTIES":
142
+			if marker == "" {
143
+				marker = "PROPERTIES"
144
+			}
145
+			if bytes.Equal(data, []byte(":END:")) {
146
+				marker = ""
147
+			}
148
+			continue
149
+		case isBlock(data) || marker != "":
150
+			matches := reBlock.FindSubmatch(data)
151
+			if len(matches) > 0 {
152
+				if string(matches[1]) == "END" {
153
+					switch marker {
154
+					case "QUOTE":
155
+						var tmpBuf bytes.Buffer
156
+						p.inline(&tmpBuf, tmpBlock.Bytes())
157
+						p.r.BlockQuote(&output, tmpBuf.Bytes())
158
+					case "CENTER":
159
+						var tmpBuf bytes.Buffer
160
+						output.WriteString("<center>\n")
161
+						p.inline(&tmpBuf, tmpBlock.Bytes())
162
+						output.Write(tmpBuf.Bytes())
163
+						output.WriteString("</center>\n")
164
+					default:
165
+						tmpBlock.WriteByte('\n')
166
+						p.r.BlockCode(&output, tmpBlock.Bytes(), syntax)
167
+					}
168
+					marker = ""
169
+					tmpBlock.Reset()
170
+					continue
171
+				}
172
+
173
+			}
174
+			if marker != "" {
175
+				if marker != "SRC" && marker != "EXAMPLE" {
176
+					var tmpBuf bytes.Buffer
177
+					tmpBuf.Write([]byte("<p>\n"))
178
+					p.inline(&tmpBuf, data)
179
+					tmpBuf.WriteByte('\n')
180
+					tmpBuf.Write([]byte("</p>\n"))
181
+					tmpBlock.Write(tmpBuf.Bytes())
182
+
183
+				} else {
184
+					tmpBlock.WriteByte('\n')
185
+					tmpBlock.Write(data)
186
+				}
187
+
188
+			} else {
189
+				marker = string(matches[2])
190
+				syntax = string(matches[3])
191
+			}
192
+		case isFootnoteDef(data):
193
+			matches := reFootnoteDef.FindSubmatch(data)
194
+			for i := range p.notes {
195
+				if p.notes[i].id == string(matches[1]) {
196
+					p.notes[i].def = string(matches[2])
197
+				}
198
+			}
199
+		case isTable(data):
200
+			if inTable != true {
201
+				inTable = true
202
+			}
203
+			tmpBlock.Write(data)
204
+			tmpBlock.WriteByte('\n')
205
+		case IsKeyword(data):
206
+			continue
207
+		case isComment(data):
208
+			p.generateComment(&output, data)
209
+		case isHeadline(data):
210
+			p.generateHeadline(&output, data)
211
+		case isDefinitionList(data):
212
+			if inList != true {
213
+				listType = "dl"
214
+				inList = true
215
+			}
216
+			var work bytes.Buffer
217
+			flags := blackfriday.LIST_TYPE_DEFINITION
218
+			matches := reDefinitionList.FindSubmatch(data)
219
+			flags |= blackfriday.LIST_TYPE_TERM
220
+			p.inline(&work, matches[1])
221
+			p.r.ListItem(&tmpBlock, work.Bytes(), flags)
222
+			work.Reset()
223
+			flags &= ^blackfriday.LIST_TYPE_TERM
224
+			p.inline(&work, matches[2])
225
+			p.r.ListItem(&tmpBlock, work.Bytes(), flags)
226
+		case isUnorderedList(data):
227
+			if inList != true {
228
+				listType = "ul"
229
+				inList = true
230
+			}
231
+			matches := reUnorderedList.FindSubmatch(data)
232
+			var work bytes.Buffer
233
+			p.inline(&work, matches[2])
234
+			p.r.ListItem(&tmpBlock, work.Bytes(), 0)
235
+		case isOrderedList(data):
236
+			if inList != true {
237
+				listType = "ol"
238
+				inList = true
239
+			}
240
+			matches := reOrderedList.FindSubmatch(data)
241
+			var work bytes.Buffer
242
+			tmpBlock.WriteString("<li")
243
+			if len(matches[2]) > 0 {
244
+				tmpBlock.WriteString(" value=\"")
245
+				tmpBlock.Write(matches[2])
246
+				tmpBlock.WriteString("\"")
247
+				matches[3] = matches[3][1:]
248
+			}
249
+			p.inline(&work, matches[3])
250
+			tmpBlock.WriteString(">")
251
+			tmpBlock.Write(work.Bytes())
252
+			tmpBlock.WriteString("</li>\n")
253
+		case isHorizontalRule(data):
254
+			p.r.HRule(&output)
255
+		case isExampleLine(data):
256
+			if inParagraph == true {
257
+				if len(tmpBlock.Bytes()) > 0 {
258
+					p.generateParagraph(&output, tmpBlock.Bytes()[:len(tmpBlock.Bytes())-1])
259
+					inParagraph = false
260
+				}
261
+				tmpBlock.Reset()
262
+			}
263
+			if inFixedWidthArea != true {
264
+				tmpBlock.WriteString("<pre class=\"example\">\n")
265
+				inFixedWidthArea = true
266
+			}
267
+			matches := reExampleLine.FindSubmatch(data)
268
+			tmpBlock.Write(matches[1])
269
+			tmpBlock.WriteString("\n")
270
+			break
271
+		default:
272
+			if inParagraph == false {
273
+				inParagraph = true
274
+				if inFixedWidthArea == true {
275
+					if tmpBlock.Len() > 0 {
276
+						tmpBlock.WriteString("</pre>")
277
+						output.Write(tmpBlock.Bytes())
278
+					}
279
+					inFixedWidthArea = false
280
+					tmpBlock.Reset()
281
+				}
282
+			}
283
+			tmpBlock.Write(data)
284
+			tmpBlock.WriteByte('\n')
285
+		}
286
+	}
287
+
288
+	if len(tmpBlock.Bytes()) > 0 {
289
+		if inParagraph == true {
290
+			p.generateParagraph(&output, tmpBlock.Bytes()[:len(tmpBlock.Bytes())-1])
291
+		} else if inFixedWidthArea == true {
292
+			tmpBlock.WriteString("</pre>\n")
293
+			output.Write(tmpBlock.Bytes())
294
+		}
295
+	}
296
+
297
+	// Writing footnote def. list
298
+	if len(p.notes) > 0 {
299
+		flags := blackfriday.LIST_ITEM_BEGINNING_OF_LIST
300
+		p.r.Footnotes(&output, func() bool {
301
+			for i := range p.notes {
302
+				p.r.FootnoteItem(&output, []byte(p.notes[i].id), []byte(p.notes[i].def), flags)
303
+			}
304
+			return true
305
+		})
306
+	}
307
+
308
+	return output.Bytes()
309
+}
310
+
311
+// Org Syntax has been broken up into 4 distinct sections based on
312
+// the org-syntax draft (http://orgmode.org/worg/dev/org-syntax.html):
313
+// - Headlines
314
+// - Greater Elements
315
+// - Elements
316
+// - Objects
317
+
318
+// Headlines
319
+func isHeadline(data []byte) bool {
320
+	if !charMatches(data[0], '*') {
321
+		return false
322
+	}
323
+	level := 0
324
+	for level < 6 && charMatches(data[level], '*') {
325
+		level++
326
+	}
327
+	return charMatches(data[level], ' ')
328
+}
329
+
330
+func (p *parser) generateHeadline(out *bytes.Buffer, data []byte) {
331
+	level := 1
332
+	status := ""
333
+	priority := ""
334
+
335
+	for level < 6 && data[level] == '*' {
336
+		level++
337
+	}
338
+
339
+	start := skipChar(data, level, ' ')
340
+
341
+	data = data[start:]
342
+	i := 0
343
+
344
+	// Check if has a status so it can be rendered as a separate span that can be hidden or
345
+	// modified with CSS classes
346
+	if hasStatus(data[i:4]) {
347
+		status = string(data[i:4])
348
+		i += 5 // one extra character for the next whitespace
349
+	}
350
+
351
+	// Check if the next byte is a priority marker
352
+	if data[i] == '[' && hasPriority(data[i+1]) {
353
+		priority = string(data[i+1])
354
+		i += 4 // for "[c]" + ' '
355
+	}
356
+
357
+	tags, tagsFound := findTags(data, i)
358
+
359
+	headlineID := sanitized_anchor_name.Create(string(data[i:]))
360
+
361
+	generate := func() bool {
362
+		dataEnd := len(data)
363
+		if tagsFound > 0 {
364
+			dataEnd = tagsFound
365
+		}
366
+
367
+		headline := bytes.TrimRight(data[i:dataEnd], " \t")
368
+
369
+		if status != "" {
370
+			out.WriteString("<span class=\"todo " + status + "\">" + status + "</span>")
371
+			out.WriteByte(' ')
372
+		}
373
+
374
+		if priority != "" {
375
+			out.WriteString("<span class=\"priority " + priority + "\">[" + priority + "]</span>")
376
+			out.WriteByte(' ')
377
+		}
378
+
379
+		p.inline(out, headline)
380
+
381
+		if tagsFound > 0 {
382
+			for _, tag := range tags {
383
+				out.WriteByte(' ')
384
+				out.WriteString("<span class=\"tags " + tag + "\">" + tag + "</span>")
385
+				out.WriteByte(' ')
386
+			}
387
+		}
388
+		return true
389
+	}
390
+
391
+	p.r.Header(out, generate, level, headlineID)
392
+}
393
+
394
+func hasStatus(data []byte) bool {
395
+	return bytes.Contains(data, []byte("TODO")) || bytes.Contains(data, []byte("DONE"))
396
+}
397
+
398
+func hasPriority(char byte) bool {
399
+	return (charMatches(char, 'A') || charMatches(char, 'B') || charMatches(char, 'C'))
400
+}
401
+
402
+func findTags(data []byte, start int) ([]string, int) {
403
+	tags := []string{}
404
+	tagOpener := 0
405
+	tagMarker := tagOpener
406
+	for tIdx := start; tIdx < len(data); tIdx++ {
407
+		if tagMarker > 0 && data[tIdx] == ':' {
408
+			tags = append(tags, string(data[tagMarker+1:tIdx]))
409
+			tagMarker = tIdx
410
+		}
411
+		if data[tIdx] == ':' && tagOpener == 0 && data[tIdx-1] == ' ' {
412
+			tagMarker = tIdx
413
+			tagOpener = tIdx
414
+		}
415
+	}
416
+	return tags, tagOpener
417
+}
418
+
419
+// Greater Elements
420
+// ~~ Definition Lists
421
+var reDefinitionList = regexp.MustCompile(`^\s*-\s+(.+?)\s+::\s+(.*)`)
422
+
423
+func isDefinitionList(data []byte) bool {
424
+	return reDefinitionList.Match(data)
425
+}
426
+
427
+// ~~ Example lines
428
+var reExampleLine = regexp.MustCompile(`^\s*:\s(\s*.*)|^\s*:$`)
429
+
430
+func isExampleLine(data []byte) bool {
431
+	return reExampleLine.Match(data)
432
+}
433
+
434
+// ~~ Ordered Lists
435
+var reOrderedList = regexp.MustCompile(`^(\s*)\d+\.\s+\[?@?(\d*)\]?(.+)`)
436
+
437
+func isOrderedList(data []byte) bool {
438
+	return reOrderedList.Match(data)
439
+}
440
+
441
+// ~~ Unordered Lists
442
+var reUnorderedList = regexp.MustCompile(`^(\s*)[-\+]\s+(.+)`)
443
+
444
+func isUnorderedList(data []byte) bool {
445
+	return reUnorderedList.Match(data)
446
+}
447
+
448
+// ~~ Tables
449
+var reTableHeaders = regexp.MustCompile(`^[|+-]*$`)
450
+
451
+func isTable(data []byte) bool {
452
+	return charMatches(data[0], '|')
453
+}
454
+
455
+func (p *parser) generateTable(output *bytes.Buffer, data []byte) {
456
+	var table bytes.Buffer
457
+	rows := bytes.Split(bytes.Trim(data, "\n"), []byte("\n"))
458
+	hasTableHeaders := len(rows) > 1
459
+	if len(rows) > 1 {
460
+		hasTableHeaders = reTableHeaders.Match(rows[1])
461
+	}
462
+	tbodySet := false
463
+
464
+	for idx, row := range rows {
465
+		var rowBuff bytes.Buffer
466
+		if hasTableHeaders && idx == 0 {
467
+			table.WriteString("<thead>")
468
+			for _, cell := range bytes.Split(row[1:len(row)-1], []byte("|")) {
469
+				p.r.TableHeaderCell(&rowBuff, bytes.Trim(cell, " \t"), 0)
470
+			}
471
+			p.r.TableRow(&table, rowBuff.Bytes())
472
+			table.WriteString("</thead>\n")
473
+		} else if hasTableHeaders && idx == 1 {
474
+			continue
475
+		} else {
476
+			if !tbodySet {
477
+				table.WriteString("<tbody>")
478
+				tbodySet = true
479
+			}
480
+			if !reTableHeaders.Match(row) {
481
+				for _, cell := range bytes.Split(row[1:len(row)-1], []byte("|")) {
482
+					var cellBuff bytes.Buffer
483
+					p.inline(&cellBuff, bytes.Trim(cell, " \t"))
484
+					p.r.TableCell(&rowBuff, cellBuff.Bytes(), 0)
485
+				}
486
+				p.r.TableRow(&table, rowBuff.Bytes())
487
+			}
488
+			if tbodySet && idx == len(rows)-1 {
489
+				table.WriteString("</tbody>\n")
490
+				tbodySet = false
491
+			}
492
+		}
493
+	}
494
+
495
+	output.WriteString("\n<table>\n")
496
+	output.Write(table.Bytes())
497
+	output.WriteString("</table>\n")
498
+}
499
+
500
+// ~~ Property Drawers
501
+
502
+func isPropertyDrawer(data []byte) bool {
503
+	return bytes.Equal(data, []byte(":PROPERTIES:"))
504
+}
505
+
506
+// ~~ Dynamic Blocks
507
+var reBlock = regexp.MustCompile(`^#\+(BEGIN|END)_(\w+)\s*([0-9A-Za-z_\-]*)?`)
508
+
509
+func isBlock(data []byte) bool {
510
+	return reBlock.Match(data)
511
+}
512
+
513
+// ~~ Footnotes
514
+var reFootnoteDef = regexp.MustCompile(`^\[fn:([\w]+)\] +(.+)`)
515
+
516
+func isFootnoteDef(data []byte) bool {
517
+	return reFootnoteDef.Match(data)
518
+}
519
+
520
+// Elements
521
+// ~~ Keywords
522
+func IsKeyword(data []byte) bool {
523
+	return len(data) > 2 && charMatches(data[0], '#') && charMatches(data[1], '+') && !charMatches(data[2], ' ')
524
+}
525
+
526
+// ~~ Comments
527
+func isComment(data []byte) bool {
528
+	return charMatches(data[0], '#') && charMatches(data[1], ' ')
529
+}
530
+
531
+func (p *parser) generateComment(out *bytes.Buffer, data []byte) {
532
+	var work bytes.Buffer
533
+	work.WriteString("<!-- ")
534
+	work.Write(data[2:])
535
+	work.WriteString(" -->")
536
+	work.WriteByte('\n')
537
+	out.Write(work.Bytes())
538
+}
539
+
540
+// ~~ Horizontal Rules
541
+var reHorizontalRule = regexp.MustCompile(`^\s*?-----\s?$`)
542
+
543
+func isHorizontalRule(data []byte) bool {
544
+	return reHorizontalRule.Match(data)
545
+}
546
+
547
+// ~~ Paragraphs
548
+func (p *parser) generateParagraph(out *bytes.Buffer, data []byte) {
549
+	generate := func() bool {
550
+		p.inline(out, bytes.Trim(data, " "))
551
+		return true
552
+	}
553
+	p.r.Paragraph(out, generate)
554
+}
555
+
556
+func (p *parser) generateList(output *bytes.Buffer, data []byte, listType string) {
557
+	generateList := func() bool {
558
+		output.WriteByte('\n')
559
+		p.inline(output, bytes.Trim(data, " "))
560
+		return true
561
+	}
562
+	switch listType {
563
+	case "ul":
564
+		p.r.List(output, generateList, 0)
565
+	case "ol":
566
+		p.r.List(output, generateList, blackfriday.LIST_TYPE_ORDERED)
567
+	case "dl":
568
+		p.r.List(output, generateList, blackfriday.LIST_TYPE_DEFINITION)
569
+	}
570
+}
571
+
572
+// Objects
573
+
574
+func (p *parser) inline(out *bytes.Buffer, data []byte) {
575
+	i, end := 0, 0
576
+
577
+	for i < len(data) {
578
+		for end < len(data) && p.inlineCallback[data[end]] == nil {
579
+			end++
580
+		}
581
+
582
+		p.r.Entity(out, data[i:end])
583
+
584
+		if end >= len(data) {
585
+			break
586
+		}
587
+		i = end
588
+
589
+		handler := p.inlineCallback[data[i]]
590
+
591
+		if consumed := handler(p, out, data, i); consumed > 0 {
592
+			i += consumed
593
+			end = i
594
+			continue
595
+		}
596
+
597
+		end = i + 1
598
+	}
599
+}
600
+
601
+func isAcceptablePreOpeningChar(dataIn, data []byte, offset int) bool {
602
+	if len(dataIn) == len(data) {
603
+		return true
604
+	}
605
+
606
+	char := dataIn[offset-1]
607
+	return charMatches(char, ' ') || isPreChar(char)
608
+}
609
+
610
+func isPreChar(char byte) bool {
611
+	return charMatches(char, '>') || charMatches(char, '(') || charMatches(char, '{') || charMatches(char, '[')
612
+}
613
+
614
+func isAcceptablePostClosingChar(char byte) bool {
615
+	return charMatches(char, ' ') || isTerminatingChar(char)
616
+}
617
+
618
+func isTerminatingChar(char byte) bool {
619
+	return charMatches(char, '.') || charMatches(char, ',') || charMatches(char, '?') || charMatches(char, '!') || charMatches(char, ')') || charMatches(char, '}') || charMatches(char, ']')
620
+}
621
+
622
+func findLastCharInInline(data []byte, char byte) int {
623
+	timesFound := 0
624
+	last := 0
625
+	// Start from character after the inline indicator
626
+	for i := 1; i < len(data); i++ {
627
+		if timesFound == 1 {
628
+			break
629
+		}
630
+		if data[i] == char {
631
+			if len(data) == i+1 || (len(data) > i+1 && isAcceptablePostClosingChar(data[i+1])) {
632
+				last = i
633
+				timesFound += 1
634
+			}
635
+		}
636
+	}
637
+	return last
638
+}
639
+
640
+func generator(p *parser, out *bytes.Buffer, dataIn []byte, offset int, char byte, doInline bool, renderer func(*bytes.Buffer, []byte)) int {
641
+	data := dataIn[offset:]
642
+	c := byte(char)
643
+	start := 1
644
+	i := start
645
+	if len(data) <= 1 {
646
+		return 0
647
+	}
648
+
649
+	lastCharInside := findLastCharInInline(data, c)
650
+
651
+	// Org mode spec says a non-whitespace character must immediately follow.
652
+	// if the current char is the marker, then there's no text between, not a candidate
653
+	if isSpace(data[i]) || lastCharInside == i || !isAcceptablePreOpeningChar(dataIn, data, offset) {
654
+		return 0
655
+	}
656
+
657
+	if lastCharInside > 0 {
658
+		var work bytes.Buffer
659
+		if doInline {
660
+			p.inline(&work, data[start:lastCharInside])
661
+			renderer(out, work.Bytes())
662
+		} else {
663
+			renderer(out, data[start:lastCharInside])
664
+		}
665
+		next := lastCharInside + 1
666
+		return next
667
+	}
668
+
669
+	return 0
670
+}
671
+
672
+// ~~ Text Markup
673
+func generateVerbatim(p *parser, out *bytes.Buffer, data []byte, offset int) int {
674
+	return generator(p, out, data, offset, '=', false, p.r.CodeSpan)
675
+}
676
+
677
+func generateCode(p *parser, out *bytes.Buffer, data []byte, offset int) int {
678
+	return generator(p, out, data, offset, '~', false, p.r.CodeSpan)
679
+}
680
+
681
+func generateEmphasis(p *parser, out *bytes.Buffer, data []byte, offset int) int {
682
+	return generator(p, out, data, offset, '/', true, p.r.Emphasis)
683
+}
684
+
685
+func generateUnderline(p *parser, out *bytes.Buffer, data []byte, offset int) int {
686
+	underline := func(out *bytes.Buffer, text []byte) {
687
+		out.WriteString("<span style=\"text-decoration: underline;\">")
688
+		out.Write(text)
689
+		out.WriteString("</span>")
690
+	}
691
+
692
+	return generator(p, out, data, offset, '_', true, underline)
693
+}
694
+
695
+func generateBold(p *parser, out *bytes.Buffer, data []byte, offset int) int {
696
+	return generator(p, out, data, offset, '*', true, p.r.DoubleEmphasis)
697
+}
698
+
699
+func generateStrikethrough(p *parser, out *bytes.Buffer, data []byte, offset int) int {
700
+	return generator(p, out, data, offset, '+', true, p.r.StrikeThrough)
701
+}
702
+
703
+// ~~ Images and Links (inc. Footnote)
704
+var reLinkOrImg = regexp.MustCompile(`\[\[(.+?)\]\[?(.*?)\]?\]`)
705
+
706
+func generateLinkOrImg(p *parser, out *bytes.Buffer, data []byte, offset int) int {
707
+	data = data[offset+1:]
708
+	start := 1
709
+	i := start
710
+	var hyperlink []byte
711
+	isImage := false
712
+	isFootnote := false
713
+	closedLink := false
714
+	hasContent := false
715
+
716
+	if bytes.Equal(data[0:3], []byte("fn:")) {
717
+		isFootnote = true
718
+	} else if data[0] != '[' {
719
+		return 0
720
+	}
721
+
722
+	if bytes.Equal(data[1:6], []byte("file:")) {
723
+		isImage = true
724
+	}
725
+
726
+	for i < len(data) {
727
+		currChar := data[i]
728
+		switch {
729
+		case charMatches(currChar, ']') && closedLink == false:
730
+			if isImage {
731
+				hyperlink = data[start+5 : i]
732
+			} else if isFootnote {
733
+				refid := data[start+2 : i]
734
+				if bytes.Equal(refid, bytes.Trim(refid, " ")) {
735
+					p.notes = append(p.notes, footnotes{string(refid), "DEFINITION NOT FOUND"})
736
+					p.r.FootnoteRef(out, refid, len(p.notes))
737
+					return i + 2
738
+				} else {
739
+					return 0
740
+				}
741
+			} else if bytes.Equal(data[i-4:i], []byte(".org")) {
742
+				orgStart := start
743
+				if bytes.Equal(data[orgStart:orgStart+2], []byte("./")) {
744
+					orgStart = orgStart + 1
745
+				}
746
+				hyperlink = data[orgStart : i-4]
747
+			} else {
748
+				hyperlink = data[start:i]
749
+			}
750
+			closedLink = true
751
+		case charMatches(currChar, '['):
752
+			start = i + 1
753
+			hasContent = true
754
+		case charMatches(currChar, ']') && closedLink == true && hasContent == true && isImage == true:
755
+			p.r.Image(out, hyperlink, data[start:i], data[start:i])
756
+			return i + 3
757
+		case charMatches(currChar, ']') && closedLink == true && hasContent == true:
758
+			var tmpBuf bytes.Buffer
759
+			p.inline(&tmpBuf, data[start:i])
760
+			p.r.Link(out, hyperlink, tmpBuf.Bytes(), tmpBuf.Bytes())
761
+			return i + 3
762
+		case charMatches(currChar, ']') && closedLink == true && hasContent == false && isImage == true:
763
+			p.r.Image(out, hyperlink, hyperlink, hyperlink)
764
+			return i + 2
765
+		case charMatches(currChar, ']') && closedLink == true && hasContent == false:
766
+			p.r.Link(out, hyperlink, hyperlink, hyperlink)
767
+			return i + 2
768
+		}
769
+		i++
770
+	}
771
+
772
+	return 0
773
+}
774
+
775
+// Helpers
776
+func skipChar(data []byte, start int, char byte) int {
777
+	i := start
778
+	for i < len(data) && charMatches(data[i], char) {
779
+		i++
780
+	}
781
+	return i
782
+}
783
+
784
+func isSpace(char byte) bool {
785
+	return charMatches(char, ' ')
786
+}
787
+
788
+func isEmpty(data []byte) bool {
789
+	if len(data) == 0 {
790
+		return true
791
+	}
792
+
793
+	for i := 0; i < len(data) && !charMatches(data[i], '\n'); i++ {
794
+		if !charMatches(data[i], ' ') && !charMatches(data[i], '\t') {
795
+			return false
796
+		}
797
+	}
798
+	return true
799
+}
800
+
801
+func charMatches(a byte, b byte) bool {
802
+	return a == b
803
+}

BIN
vendor/github.com/chaseadamsio/goorgeous/gopher.gif


BIN
vendor/github.com/chaseadamsio/goorgeous/gopher_small.gif


+ 70 - 0
vendor/github.com/chaseadamsio/goorgeous/header.go

@@ -0,0 +1,70 @@
1
+package goorgeous
2
+
3
+import (
4
+	"bufio"
5
+	"bytes"
6
+	"regexp"
7
+	"strings"
8
+)
9
+
10
+// ExtractOrgHeaders finds and returns all of the headers
11
+// from a bufio.Reader and returns them as their own byte slice
12
+func ExtractOrgHeaders(r *bufio.Reader) (fm []byte, err error) {
13
+	var out bytes.Buffer
14
+	endOfHeaders := true
15
+	for endOfHeaders {
16
+		p, err := r.Peek(2)
17
+		if err != nil {
18
+			return nil, err
19
+		}
20
+		if !charMatches(p[0], '#') && !charMatches(p[1], '+') {
21
+			endOfHeaders = false
22
+			break
23
+		}
24
+		line, _, err := r.ReadLine()
25
+		if err != nil {
26
+			return nil, err
27
+		}
28
+		out.Write(line)
29
+		out.WriteByte('\n')
30
+	}
31
+	return out.Bytes(), nil
32
+}
33
+
34
+var reHeader = regexp.MustCompile(`^#\+(\w+?): (.*)`)
35
+
36
+// OrgHeaders find all of the headers from a byte slice and returns
37
+// them as a map of string interface
38
+func OrgHeaders(input []byte) (map[string]interface{}, error) {
39
+	out := make(map[string]interface{})
40
+	scanner := bufio.NewScanner(bytes.NewReader(input))
41
+
42
+	for scanner.Scan() {
43
+		data := scanner.Bytes()
44
+		if !charMatches(data[0], '#') && !charMatches(data[1], '+') {
45
+			return out, nil
46
+		}
47
+		matches := reHeader.FindSubmatch(data)
48
+
49
+		if len(matches) < 3 {
50
+			continue
51
+		}
52
+
53
+		key := string(matches[1])
54
+		val := matches[2]
55
+		switch {
56
+		case strings.ToLower(key) == "tags" || strings.ToLower(key) == "categories" || strings.ToLower(key) == "aliases":
57
+			bTags := bytes.Split(val, []byte(" "))
58
+			tags := make([]string, len(bTags))
59
+			for idx, tag := range bTags {
60
+				tags[idx] = string(tag)
61
+			}
62
+			out[key] = tags
63
+		default:
64
+			out[key] = string(val)
65
+		}
66
+
67
+	}
68
+	return out, nil
69
+
70
+}

+ 6 - 0
vendor/vendor.json

@@ -300,6 +300,12 @@
300 300
 			"revisionTime": "2016-01-17T19:21:50Z"
301 301
 		},
302 302
 		{
303
+			"checksumSHA1": "x1svIugw39oEZGU5/HMUHzgRUZM=",
304
+			"path": "github.com/chaseadamsio/goorgeous",
305
+			"revision": "098da33fde5f9220736531b3cb26a2dec86a8367",
306
+			"revisionTime": "2017-09-01T13:22:37Z"
307
+		},
308
+		{
303 309
 			"checksumSHA1": "agNqSytP0indDCoGizlMyC1L/m4=",
304 310
 			"path": "github.com/coreos/etcd/error",
305 311
 			"revision": "01c303113d0a3d5a8075864321c3aedb72035bdd",