From 0d80af649a50c4b9e5e4ba764399872fc92f70f2 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Thu, 21 Sep 2017 13:20:14 +0800 Subject: [PATCH] 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 --- main.go | 4 + models/mail.go | 2 +- modules/markup/html_test.go | 2 +- modules/{ => markup}/markdown/markdown.go | 18 +- .../{ => markup}/markdown/markdown_test.go | 43 +- modules/markup/markup_test.go | 2 +- modules/markup/orgmode/orgmode.go | 56 ++ modules/markup/orgmode/orgmode_test.go | 54 ++ routers/api/v1/misc/markdown.go | 2 +- routers/repo/issue.go | 2 +- routers/repo/release.go | 2 +- routers/repo/view.go | 10 +- routers/repo/wiki.go | 2 +- templates/repo/view_file.tmpl | 4 +- .../github.com/chaseadamsio/goorgeous/LICENSE | 21 + .../chaseadamsio/goorgeous/README.org | 66 ++ .../chaseadamsio/goorgeous/goorgeous.go | 803 ++++++++++++++++++ .../chaseadamsio/goorgeous/gopher.gif | Bin 0 -> 15232 bytes .../chaseadamsio/goorgeous/gopher_small.gif | Bin 0 -> 3270 bytes .../chaseadamsio/goorgeous/header.go | 70 ++ vendor/vendor.json | 6 + 21 files changed, 1103 insertions(+), 66 deletions(-) rename modules/{ => markup}/markdown/markdown.go (95%) rename modules/{ => markup}/markdown/markdown_test.go (89%) create mode 100644 modules/markup/orgmode/orgmode.go create mode 100644 modules/markup/orgmode/orgmode_test.go create mode 100644 vendor/github.com/chaseadamsio/goorgeous/LICENSE create mode 100644 vendor/github.com/chaseadamsio/goorgeous/README.org create mode 100644 vendor/github.com/chaseadamsio/goorgeous/goorgeous.go create mode 100644 vendor/github.com/chaseadamsio/goorgeous/gopher.gif create mode 100644 vendor/github.com/chaseadamsio/goorgeous/gopher_small.gif create mode 100644 vendor/github.com/chaseadamsio/goorgeous/header.go diff --git a/main.go b/main.go index 383dbc2093..c2acda99af 100644 --- a/main.go +++ b/main.go @@ -13,6 +13,10 @@ import ( "code.gitea.io/gitea/cmd" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" + // register supported doc types + _ "code.gitea.io/gitea/modules/markup/markdown" + _ "code.gitea.io/gitea/modules/markup/orgmode" + "github.com/urfave/cli" ) diff --git a/models/mail.go b/models/mail.go index afcddb6d23..98766f69f2 100644 --- a/models/mail.go +++ b/models/mail.go @@ -13,8 +13,8 @@ import ( "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/mailer" - "code.gitea.io/gitea/modules/markdown" "code.gitea.io/gitea/modules/markup" + "code.gitea.io/gitea/modules/markup/markdown" "code.gitea.io/gitea/modules/setting" "gopkg.in/gomail.v2" "gopkg.in/macaron.v1" diff --git a/modules/markup/html_test.go b/modules/markup/html_test.go index 407115526d..ab2ca5ef47 100644 --- a/modules/markup/html_test.go +++ b/modules/markup/html_test.go @@ -10,8 +10,8 @@ import ( "strings" "testing" - _ "code.gitea.io/gitea/modules/markdown" . "code.gitea.io/gitea/modules/markup" + _ "code.gitea.io/gitea/modules/markup/markdown" "code.gitea.io/gitea/modules/setting" "github.com/stretchr/testify/assert" diff --git a/modules/markdown/markdown.go b/modules/markup/markdown/markdown.go similarity index 95% rename from modules/markdown/markdown.go rename to modules/markup/markdown/markdown.go index 6cf2d9eaa1..f0ed0e03ab 100644 --- a/modules/markdown/markdown.go +++ b/modules/markup/markdown/markdown.go @@ -17,8 +17,8 @@ import ( // Renderer is a extended version of underlying render object. type Renderer struct { blackfriday.Renderer - urlPrefix string - isWikiMarkdown bool + URLPrefix string + IsWiki bool } // 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 [] if len(link) > 0 && !markup.IsLink(link) { if link[0] != '#' { lnk := string(link) - if r.isWikiMarkdown { + if r.IsWiki { lnk = markup.URLJoin("wiki", lnk) } - mLink := markup.URLJoin(r.urlPrefix, lnk) + mLink := markup.URLJoin(r.URLPrefix, lnk) link = []byte(mLink) } } @@ -95,8 +95,8 @@ var ( // Image defines how images should be processed to produce corresponding HTML elements. func (r *Renderer) Image(out *bytes.Buffer, link []byte, title []byte, alt []byte) { - prefix := r.urlPrefix - if r.isWikiMarkdown { + prefix := r.URLPrefix + if r.IsWiki { prefix = markup.URLJoin(prefix, "wiki", "src") } prefix = strings.Replace(prefix, "/src/", "/raw/", 1) @@ -129,9 +129,9 @@ func RenderRaw(body []byte, urlPrefix string, wikiMarkdown bool) []byte { htmlFlags |= blackfriday.HTML_SKIP_STYLE htmlFlags |= blackfriday.HTML_OMIT_CONTENTS renderer := &Renderer{ - Renderer: blackfriday.HtmlRenderer(htmlFlags, "", ""), - urlPrefix: urlPrefix, - isWikiMarkdown: wikiMarkdown, + Renderer: blackfriday.HtmlRenderer(htmlFlags, "", ""), + URLPrefix: urlPrefix, + IsWiki: wikiMarkdown, } // set up the parser diff --git a/modules/markdown/markdown_test.go b/modules/markup/markdown/markdown_test.go similarity index 89% rename from modules/markdown/markdown_test.go rename to modules/markup/markdown/markdown_test.go index 1b57e4f203..9ca3de01ca 100644 --- a/modules/markdown/markdown_test.go +++ b/modules/markup/markdown/markdown_test.go @@ -5,13 +5,11 @@ package markdown_test import ( - "fmt" - "strconv" "strings" "testing" - . "code.gitea.io/gitea/modules/markdown" "code.gitea.io/gitea/modules/markup" + . "code.gitea.io/gitea/modules/markup/markdown" "code.gitea.io/gitea/modules/setting" "github.com/stretchr/testify/assert" @@ -21,45 +19,6 @@ const AppURL = "http://localhost:3000/" const Repo = "gogits/gogs" const AppSubURL = AppURL + Repo + "/" -var numericMetas = map[string]string{ - "format": "https://someurl.com/{user}/{repo}/{index}", - "user": "someUser", - "repo": "someRepo", - "style": markup.IssueNameStyleNumeric, -} - -var alphanumericMetas = map[string]string{ - "format": "https://someurl.com/{user}/{repo}/{index}", - "user": "someUser", - "repo": "someRepo", - "style": markup.IssueNameStyleAlphanumeric, -} - -// numericLink an HTML to a numeric-style issue -func numericIssueLink(baseURL string, index int) string { - return link(markup.URLJoin(baseURL, strconv.Itoa(index)), fmt.Sprintf("#%d", index)) -} - -// alphanumLink an HTML link to an alphanumeric-style issue -func alphanumIssueLink(baseURL string, name string) string { - return link(markup.URLJoin(baseURL, name), name) -} - -// urlContentsLink an HTML link whose contents is the target URL -func urlContentsLink(href string) string { - return link(href, href) -} - -// link an HTML link -func link(href, contents string) string { - return fmt.Sprintf("%s", href, contents) -} - -func testRenderIssueIndexPattern(t *testing.T, input, expected string, metas map[string]string) { - assert.Equal(t, expected, - string(markup.RenderIssueIndexPattern([]byte(input), AppSubURL, metas))) -} - func TestRender_StandardLinks(t *testing.T) { setting.AppURL = AppURL setting.AppSubURL = AppSubURL diff --git a/modules/markup/markup_test.go b/modules/markup/markup_test.go index 8d061ae39e..b0ebfae57d 100644 --- a/modules/markup/markup_test.go +++ b/modules/markup/markup_test.go @@ -7,8 +7,8 @@ package markup_test import ( "testing" - _ "code.gitea.io/gitea/modules/markdown" . "code.gitea.io/gitea/modules/markup" + _ "code.gitea.io/gitea/modules/markup/markdown" "github.com/stretchr/testify/assert" ) diff --git a/modules/markup/orgmode/orgmode.go b/modules/markup/orgmode/orgmode.go new file mode 100644 index 0000000000..f9223a18b5 --- /dev/null +++ b/modules/markup/orgmode/orgmode.go @@ -0,0 +1,56 @@ +// Copyright 2017 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package markup + +import ( + "code.gitea.io/gitea/modules/markup" + "code.gitea.io/gitea/modules/markup/markdown" + + "github.com/chaseadamsio/goorgeous" + "github.com/russross/blackfriday" +) + +func init() { + markup.RegisterParser(Parser{}) +} + +// Parser implements markup.Parser for orgmode +type Parser struct { +} + +// Name implements markup.Parser +func (Parser) Name() string { + return "orgmode" +} + +// Extensions implements markup.Parser +func (Parser) Extensions() []string { + return []string{".org"} +} + +// Render renders orgmode rawbytes to HTML +func Render(rawBytes []byte, urlPrefix string, metas map[string]string, isWiki bool) []byte { + htmlFlags := blackfriday.HTML_USE_XHTML + htmlFlags |= blackfriday.HTML_SKIP_STYLE + htmlFlags |= blackfriday.HTML_OMIT_CONTENTS + renderer := &markdown.Renderer{ + Renderer: blackfriday.HtmlRenderer(htmlFlags, "", ""), + URLPrefix: urlPrefix, + IsWiki: isWiki, + } + + result := goorgeous.Org(rawBytes, renderer) + return result +} + +// RenderString reners orgmode string to HTML string +func RenderString(rawContent string, urlPrefix string, metas map[string]string, isWiki bool) string { + return string(Render([]byte(rawContent), urlPrefix, metas, isWiki)) +} + +// Render implements markup.Parser +func (Parser) Render(rawBytes []byte, urlPrefix string, metas map[string]string, isWiki bool) []byte { + return Render(rawBytes, urlPrefix, metas, isWiki) +} diff --git a/modules/markup/orgmode/orgmode_test.go b/modules/markup/orgmode/orgmode_test.go new file mode 100644 index 0000000000..a68ab5d3af --- /dev/null +++ b/modules/markup/orgmode/orgmode_test.go @@ -0,0 +1,54 @@ +// Copyright 2017 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package markup + +import ( + "strings" + "testing" + + "code.gitea.io/gitea/modules/markup" + "code.gitea.io/gitea/modules/setting" + + "github.com/stretchr/testify/assert" +) + +const AppURL = "http://localhost:3000/" +const Repo = "gogits/gogs" +const AppSubURL = AppURL + Repo + "/" + +func TestRender_StandardLinks(t *testing.T) { + setting.AppURL = AppURL + setting.AppSubURL = AppSubURL + + test := func(input, expected string) { + buffer := RenderString(input, setting.AppSubURL, nil, false) + assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer)) + } + + googleRendered := `

https://google.com/

` + test("[[https://google.com/]]", googleRendered) + + lnk := markup.URLJoin(AppSubURL, "WikiPage") + test("[[WikiPage][WikiPage]]", + `

WikiPage

`) +} + +func TestRender_Images(t *testing.T) { + setting.AppURL = AppURL + setting.AppSubURL = AppSubURL + + test := func(input, expected string) { + buffer := RenderString(input, setting.AppSubURL, nil, false) + assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer)) + } + + url := "../../.images/src/02/train.jpg" + title := "Train" + result := markup.URLJoin(AppSubURL, url) + + test( + "[[file:"+url+"]["+title+"]]", + `

`+title+`

`) +} diff --git a/routers/api/v1/misc/markdown.go b/routers/api/v1/misc/markdown.go index a2e65ecb0a..8e3c66841f 100644 --- a/routers/api/v1/misc/markdown.go +++ b/routers/api/v1/misc/markdown.go @@ -8,8 +8,8 @@ import ( api "code.gitea.io/sdk/gitea" "code.gitea.io/gitea/modules/context" - "code.gitea.io/gitea/modules/markdown" "code.gitea.io/gitea/modules/markup" + "code.gitea.io/gitea/modules/markup/markdown" "code.gitea.io/gitea/modules/setting" ) diff --git a/routers/repo/issue.go b/routers/repo/issue.go index 4c4f9037bf..091268116b 100644 --- a/routers/repo/issue.go +++ b/routers/repo/issue.go @@ -24,7 +24,7 @@ import ( "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/indexer" "code.gitea.io/gitea/modules/log" - "code.gitea.io/gitea/modules/markdown" + "code.gitea.io/gitea/modules/markup/markdown" "code.gitea.io/gitea/modules/notification" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/util" diff --git a/routers/repo/release.go b/routers/repo/release.go index fe68f1b6f1..da99dd7713 100644 --- a/routers/repo/release.go +++ b/routers/repo/release.go @@ -12,7 +12,7 @@ import ( "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/log" - "code.gitea.io/gitea/modules/markdown" + "code.gitea.io/gitea/modules/markup/markdown" "code.gitea.io/gitea/modules/setting" "github.com/Unknwon/paginater" diff --git a/routers/repo/view.go b/routers/repo/view.go index d794a57405..bfba7acac8 100644 --- a/routers/repo/view.go +++ b/routers/repo/view.go @@ -95,11 +95,11 @@ func renderDirectory(ctx *context.Context, treeLink string) { buf = append(buf, d...) newbuf := markup.Render(readmeFile.Name(), buf, treeLink, ctx.Repo.Repository.ComposeMetas()) if newbuf != nil { - ctx.Data["IsMarkdown"] = true + ctx.Data["IsMarkup"] = true } else { // FIXME This is the only way to show non-markdown files // instead of a broken "View Raw" link - ctx.Data["IsMarkdown"] = true + ctx.Data["IsMarkup"] = false newbuf = bytes.Replace(buf, []byte("\n"), []byte(`
`), -1) } ctx.Data["FileContent"] = string(newbuf) @@ -197,10 +197,8 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry, treeLink, rawLink st tp := markup.Type(blob.Name()) isSupportedMarkup := tp != "" - // FIXME: currently set IsMarkdown for compatible - ctx.Data["IsMarkdown"] = isSupportedMarkup - - readmeExist := isSupportedMarkup || markup.IsReadmeFile(blob.Name()) + ctx.Data["IsMarkup"] = isSupportedMarkup + readmeExist := markup.IsReadmeFile(blob.Name()) ctx.Data["ReadmeExist"] = readmeExist if readmeExist && isSupportedMarkup { ctx.Data["FileContent"] = string(markup.Render(blob.Name(), buf, path.Dir(treeLink), ctx.Repo.Repository.ComposeMetas())) diff --git a/routers/repo/wiki.go b/routers/repo/wiki.go index 2a73fdc41e..019c3d5d16 100644 --- a/routers/repo/wiki.go +++ b/routers/repo/wiki.go @@ -18,8 +18,8 @@ import ( "code.gitea.io/gitea/modules/auth" "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/context" - "code.gitea.io/gitea/modules/markdown" "code.gitea.io/gitea/modules/markup" + "code.gitea.io/gitea/modules/markup/markdown" ) const ( diff --git a/templates/repo/view_file.tmpl b/templates/repo/view_file.tmpl index 36fccb00b3..898b9b5557 100644 --- a/templates/repo/view_file.tmpl +++ b/templates/repo/view_file.tmpl @@ -36,8 +36,8 @@ {{end}}
-
- {{if .IsMarkdown}} +
+ {{if .IsMarkup}} {{if .FileContent}}{{.FileContent | Str2html}}{{end}} {{else if not .IsTextFile}}
diff --git a/vendor/github.com/chaseadamsio/goorgeous/LICENSE b/vendor/github.com/chaseadamsio/goorgeous/LICENSE new file mode 100644 index 0000000000..d7a37c6a3b --- /dev/null +++ b/vendor/github.com/chaseadamsio/goorgeous/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 Chase Adams + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/github.com/chaseadamsio/goorgeous/README.org b/vendor/github.com/chaseadamsio/goorgeous/README.org new file mode 100644 index 0000000000..37e0f2ec73 --- /dev/null +++ b/vendor/github.com/chaseadamsio/goorgeous/README.org @@ -0,0 +1,66 @@ +#+TITLE: chaseadamsio/goorgeous + +[[https://travis-ci.org/chaseadamsio/goorgeous.svg?branch=master]] +[[https://coveralls.io/repos/github/chaseadamsio/goorgeous/badge.svg?branch=master]] + +/goorgeous is a Go Org to HTML Parser./ + +[[file:gopher_small.gif]] + +*Pronounced: Go? Org? Yes!* + +#+BEGIN_QUOTE +"Org mode is for keeping notes, maintaining TODO lists, planning projects, and authoring documents with a fast and effective plain-text system." + +- [[orgmode.org]] +#+END_QUOTE + +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]]. + +* Installation + +#+BEGIN_SRC sh + go get -u github.com/chaseadamsio/goorgeous +#+END_SRC + +* Usage + +** Org Headers + +To retrieve the headers from a =[]byte=, call =OrgHeaders= and it will return a =map[string]interface{}=: + +#+BEGIN_SRC go + input := "#+title: goorgeous\n* Some Headline\n" + out := goorgeous.OrgHeaders(input) +#+END_SRC + +#+BEGIN_SRC go + map[string]interface{}{ + "title": "goorgeous" + } +#+END_SRC + +** Org Content + +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= + +#+BEGIN_SRC go + input := "#+TITLE: goorgeous\n* Some Headline\n" + out := goorgeous.Org(input) +#+END_SRC + +=out= will be: + +#+BEGIN_SRC html +

Some Headline

/n +#+END_SRC + +* Why? + +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. + +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. + +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. +* Acknowledgements +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! diff --git a/vendor/github.com/chaseadamsio/goorgeous/goorgeous.go b/vendor/github.com/chaseadamsio/goorgeous/goorgeous.go new file mode 100644 index 0000000000..f1b2671d65 --- /dev/null +++ b/vendor/github.com/chaseadamsio/goorgeous/goorgeous.go @@ -0,0 +1,803 @@ +package goorgeous + +import ( + "bufio" + "bytes" + "regexp" + + "github.com/russross/blackfriday" + "github.com/shurcooL/sanitized_anchor_name" +) + +type inlineParser func(p *parser, out *bytes.Buffer, data []byte, offset int) int + +type footnotes struct { + id string + def string +} + +type parser struct { + r blackfriday.Renderer + inlineCallback [256]inlineParser + notes []footnotes +} + +// NewParser returns a new parser with the inlineCallbacks required for org content +func NewParser(renderer blackfriday.Renderer) *parser { + p := new(parser) + p.r = renderer + + p.inlineCallback['='] = generateVerbatim + p.inlineCallback['~'] = generateCode + p.inlineCallback['/'] = generateEmphasis + p.inlineCallback['_'] = generateUnderline + p.inlineCallback['*'] = generateBold + p.inlineCallback['+'] = generateStrikethrough + p.inlineCallback['['] = generateLinkOrImg + + return p +} + +// OrgCommon is the easiest way to parse a byte slice of org content and makes assumptions +// that the caller wants to use blackfriday's HTMLRenderer with XHTML +func OrgCommon(input []byte) []byte { + renderer := blackfriday.HtmlRenderer(blackfriday.HTML_USE_XHTML, "", "") + return OrgOptions(input, renderer) +} + +// Org is a convenience name for OrgOptions +func Org(input []byte, renderer blackfriday.Renderer) []byte { + return OrgOptions(input, renderer) +} + +// OrgOptions takes an org content byte slice and a renderer to use +func OrgOptions(input []byte, renderer blackfriday.Renderer) []byte { + // in the case that we need to render something in isEmpty but there isn't a new line char + input = append(input, '\n') + var output bytes.Buffer + + p := NewParser(renderer) + + scanner := bufio.NewScanner(bytes.NewReader(input)) + // used to capture code blocks + marker := "" + syntax := "" + listType := "" + inParagraph := false + inList := false + inTable := false + inFixedWidthArea := false + var tmpBlock bytes.Buffer + + for scanner.Scan() { + data := scanner.Bytes() + + if !isEmpty(data) && isComment(data) || IsKeyword(data) { + switch { + case inList: + if tmpBlock.Len() > 0 { + p.generateList(&output, tmpBlock.Bytes(), listType) + } + inList = false + listType = "" + tmpBlock.Reset() + case inTable: + if tmpBlock.Len() > 0 { + p.generateTable(&output, tmpBlock.Bytes()) + } + inTable = false + tmpBlock.Reset() + case inParagraph: + if tmpBlock.Len() > 0 { + p.generateParagraph(&output, tmpBlock.Bytes()[:len(tmpBlock.Bytes())-1]) + } + inParagraph = false + tmpBlock.Reset() + case inFixedWidthArea: + if tmpBlock.Len() > 0 { + tmpBlock.WriteString("\n") + output.Write(tmpBlock.Bytes()) + } + inFixedWidthArea = false + tmpBlock.Reset() + } + + } + + switch { + case isEmpty(data): + switch { + case inList: + if tmpBlock.Len() > 0 { + p.generateList(&output, tmpBlock.Bytes(), listType) + } + inList = false + listType = "" + tmpBlock.Reset() + case inTable: + if tmpBlock.Len() > 0 { + p.generateTable(&output, tmpBlock.Bytes()) + } + inTable = false + tmpBlock.Reset() + case inParagraph: + if tmpBlock.Len() > 0 { + p.generateParagraph(&output, tmpBlock.Bytes()[:len(tmpBlock.Bytes())-1]) + } + inParagraph = false + tmpBlock.Reset() + case inFixedWidthArea: + if tmpBlock.Len() > 0 { + tmpBlock.WriteString("\n") + output.Write(tmpBlock.Bytes()) + } + inFixedWidthArea = false + tmpBlock.Reset() + case marker != "": + tmpBlock.WriteByte('\n') + default: + continue + } + case isPropertyDrawer(data) || marker == "PROPERTIES": + if marker == "" { + marker = "PROPERTIES" + } + if bytes.Equal(data, []byte(":END:")) { + marker = "" + } + continue + case isBlock(data) || marker != "": + matches := reBlock.FindSubmatch(data) + if len(matches) > 0 { + if string(matches[1]) == "END" { + switch marker { + case "QUOTE": + var tmpBuf bytes.Buffer + p.inline(&tmpBuf, tmpBlock.Bytes()) + p.r.BlockQuote(&output, tmpBuf.Bytes()) + case "CENTER": + var tmpBuf bytes.Buffer + output.WriteString("
\n") + p.inline(&tmpBuf, tmpBlock.Bytes()) + output.Write(tmpBuf.Bytes()) + output.WriteString("
\n") + default: + tmpBlock.WriteByte('\n') + p.r.BlockCode(&output, tmpBlock.Bytes(), syntax) + } + marker = "" + tmpBlock.Reset() + continue + } + + } + if marker != "" { + if marker != "SRC" && marker != "EXAMPLE" { + var tmpBuf bytes.Buffer + tmpBuf.Write([]byte("

\n")) + p.inline(&tmpBuf, data) + tmpBuf.WriteByte('\n') + tmpBuf.Write([]byte("

\n")) + tmpBlock.Write(tmpBuf.Bytes()) + + } else { + tmpBlock.WriteByte('\n') + tmpBlock.Write(data) + } + + } else { + marker = string(matches[2]) + syntax = string(matches[3]) + } + case isFootnoteDef(data): + matches := reFootnoteDef.FindSubmatch(data) + for i := range p.notes { + if p.notes[i].id == string(matches[1]) { + p.notes[i].def = string(matches[2]) + } + } + case isTable(data): + if inTable != true { + inTable = true + } + tmpBlock.Write(data) + tmpBlock.WriteByte('\n') + case IsKeyword(data): + continue + case isComment(data): + p.generateComment(&output, data) + case isHeadline(data): + p.generateHeadline(&output, data) + case isDefinitionList(data): + if inList != true { + listType = "dl" + inList = true + } + var work bytes.Buffer + flags := blackfriday.LIST_TYPE_DEFINITION + matches := reDefinitionList.FindSubmatch(data) + flags |= blackfriday.LIST_TYPE_TERM + p.inline(&work, matches[1]) + p.r.ListItem(&tmpBlock, work.Bytes(), flags) + work.Reset() + flags &= ^blackfriday.LIST_TYPE_TERM + p.inline(&work, matches[2]) + p.r.ListItem(&tmpBlock, work.Bytes(), flags) + case isUnorderedList(data): + if inList != true { + listType = "ul" + inList = true + } + matches := reUnorderedList.FindSubmatch(data) + var work bytes.Buffer + p.inline(&work, matches[2]) + p.r.ListItem(&tmpBlock, work.Bytes(), 0) + case isOrderedList(data): + if inList != true { + listType = "ol" + inList = true + } + matches := reOrderedList.FindSubmatch(data) + var work bytes.Buffer + tmpBlock.WriteString(" 0 { + tmpBlock.WriteString(" value=\"") + tmpBlock.Write(matches[2]) + tmpBlock.WriteString("\"") + matches[3] = matches[3][1:] + } + p.inline(&work, matches[3]) + tmpBlock.WriteString(">") + tmpBlock.Write(work.Bytes()) + tmpBlock.WriteString("\n") + case isHorizontalRule(data): + p.r.HRule(&output) + case isExampleLine(data): + if inParagraph == true { + if len(tmpBlock.Bytes()) > 0 { + p.generateParagraph(&output, tmpBlock.Bytes()[:len(tmpBlock.Bytes())-1]) + inParagraph = false + } + tmpBlock.Reset() + } + if inFixedWidthArea != true { + tmpBlock.WriteString("
\n")
+				inFixedWidthArea = true
+			}
+			matches := reExampleLine.FindSubmatch(data)
+			tmpBlock.Write(matches[1])
+			tmpBlock.WriteString("\n")
+			break
+		default:
+			if inParagraph == false {
+				inParagraph = true
+				if inFixedWidthArea == true {
+					if tmpBlock.Len() > 0 {
+						tmpBlock.WriteString("
") + output.Write(tmpBlock.Bytes()) + } + inFixedWidthArea = false + tmpBlock.Reset() + } + } + tmpBlock.Write(data) + tmpBlock.WriteByte('\n') + } + } + + if len(tmpBlock.Bytes()) > 0 { + if inParagraph == true { + p.generateParagraph(&output, tmpBlock.Bytes()[:len(tmpBlock.Bytes())-1]) + } else if inFixedWidthArea == true { + tmpBlock.WriteString("\n") + output.Write(tmpBlock.Bytes()) + } + } + + // Writing footnote def. list + if len(p.notes) > 0 { + flags := blackfriday.LIST_ITEM_BEGINNING_OF_LIST + p.r.Footnotes(&output, func() bool { + for i := range p.notes { + p.r.FootnoteItem(&output, []byte(p.notes[i].id), []byte(p.notes[i].def), flags) + } + return true + }) + } + + return output.Bytes() +} + +// Org Syntax has been broken up into 4 distinct sections based on +// the org-syntax draft (http://orgmode.org/worg/dev/org-syntax.html): +// - Headlines +// - Greater Elements +// - Elements +// - Objects + +// Headlines +func isHeadline(data []byte) bool { + if !charMatches(data[0], '*') { + return false + } + level := 0 + for level < 6 && charMatches(data[level], '*') { + level++ + } + return charMatches(data[level], ' ') +} + +func (p *parser) generateHeadline(out *bytes.Buffer, data []byte) { + level := 1 + status := "" + priority := "" + + for level < 6 && data[level] == '*' { + level++ + } + + start := skipChar(data, level, ' ') + + data = data[start:] + i := 0 + + // Check if has a status so it can be rendered as a separate span that can be hidden or + // modified with CSS classes + if hasStatus(data[i:4]) { + status = string(data[i:4]) + i += 5 // one extra character for the next whitespace + } + + // Check if the next byte is a priority marker + if data[i] == '[' && hasPriority(data[i+1]) { + priority = string(data[i+1]) + i += 4 // for "[c]" + ' ' + } + + tags, tagsFound := findTags(data, i) + + headlineID := sanitized_anchor_name.Create(string(data[i:])) + + generate := func() bool { + dataEnd := len(data) + if tagsFound > 0 { + dataEnd = tagsFound + } + + headline := bytes.TrimRight(data[i:dataEnd], " \t") + + if status != "" { + out.WriteString("" + status + "") + out.WriteByte(' ') + } + + if priority != "" { + out.WriteString("[" + priority + "]") + out.WriteByte(' ') + } + + p.inline(out, headline) + + if tagsFound > 0 { + for _, tag := range tags { + out.WriteByte(' ') + out.WriteString("" + tag + "") + out.WriteByte(' ') + } + } + return true + } + + p.r.Header(out, generate, level, headlineID) +} + +func hasStatus(data []byte) bool { + return bytes.Contains(data, []byte("TODO")) || bytes.Contains(data, []byte("DONE")) +} + +func hasPriority(char byte) bool { + return (charMatches(char, 'A') || charMatches(char, 'B') || charMatches(char, 'C')) +} + +func findTags(data []byte, start int) ([]string, int) { + tags := []string{} + tagOpener := 0 + tagMarker := tagOpener + for tIdx := start; tIdx < len(data); tIdx++ { + if tagMarker > 0 && data[tIdx] == ':' { + tags = append(tags, string(data[tagMarker+1:tIdx])) + tagMarker = tIdx + } + if data[tIdx] == ':' && tagOpener == 0 && data[tIdx-1] == ' ' { + tagMarker = tIdx + tagOpener = tIdx + } + } + return tags, tagOpener +} + +// Greater Elements +// ~~ Definition Lists +var reDefinitionList = regexp.MustCompile(`^\s*-\s+(.+?)\s+::\s+(.*)`) + +func isDefinitionList(data []byte) bool { + return reDefinitionList.Match(data) +} + +// ~~ Example lines +var reExampleLine = regexp.MustCompile(`^\s*:\s(\s*.*)|^\s*:$`) + +func isExampleLine(data []byte) bool { + return reExampleLine.Match(data) +} + +// ~~ Ordered Lists +var reOrderedList = regexp.MustCompile(`^(\s*)\d+\.\s+\[?@?(\d*)\]?(.+)`) + +func isOrderedList(data []byte) bool { + return reOrderedList.Match(data) +} + +// ~~ Unordered Lists +var reUnorderedList = regexp.MustCompile(`^(\s*)[-\+]\s+(.+)`) + +func isUnorderedList(data []byte) bool { + return reUnorderedList.Match(data) +} + +// ~~ Tables +var reTableHeaders = regexp.MustCompile(`^[|+-]*$`) + +func isTable(data []byte) bool { + return charMatches(data[0], '|') +} + +func (p *parser) generateTable(output *bytes.Buffer, data []byte) { + var table bytes.Buffer + rows := bytes.Split(bytes.Trim(data, "\n"), []byte("\n")) + hasTableHeaders := len(rows) > 1 + if len(rows) > 1 { + hasTableHeaders = reTableHeaders.Match(rows[1]) + } + tbodySet := false + + for idx, row := range rows { + var rowBuff bytes.Buffer + if hasTableHeaders && idx == 0 { + table.WriteString("") + for _, cell := range bytes.Split(row[1:len(row)-1], []byte("|")) { + p.r.TableHeaderCell(&rowBuff, bytes.Trim(cell, " \t"), 0) + } + p.r.TableRow(&table, rowBuff.Bytes()) + table.WriteString("\n") + } else if hasTableHeaders && idx == 1 { + continue + } else { + if !tbodySet { + table.WriteString("") + tbodySet = true + } + if !reTableHeaders.Match(row) { + for _, cell := range bytes.Split(row[1:len(row)-1], []byte("|")) { + var cellBuff bytes.Buffer + p.inline(&cellBuff, bytes.Trim(cell, " \t")) + p.r.TableCell(&rowBuff, cellBuff.Bytes(), 0) + } + p.r.TableRow(&table, rowBuff.Bytes()) + } + if tbodySet && idx == len(rows)-1 { + table.WriteString("\n") + tbodySet = false + } + } + } + + output.WriteString("\n\n") + output.Write(table.Bytes()) + output.WriteString("
\n") +} + +// ~~ Property Drawers + +func isPropertyDrawer(data []byte) bool { + return bytes.Equal(data, []byte(":PROPERTIES:")) +} + +// ~~ Dynamic Blocks +var reBlock = regexp.MustCompile(`^#\+(BEGIN|END)_(\w+)\s*([0-9A-Za-z_\-]*)?`) + +func isBlock(data []byte) bool { + return reBlock.Match(data) +} + +// ~~ Footnotes +var reFootnoteDef = regexp.MustCompile(`^\[fn:([\w]+)\] +(.+)`) + +func isFootnoteDef(data []byte) bool { + return reFootnoteDef.Match(data) +} + +// Elements +// ~~ Keywords +func IsKeyword(data []byte) bool { + return len(data) > 2 && charMatches(data[0], '#') && charMatches(data[1], '+') && !charMatches(data[2], ' ') +} + +// ~~ Comments +func isComment(data []byte) bool { + return charMatches(data[0], '#') && charMatches(data[1], ' ') +} + +func (p *parser) generateComment(out *bytes.Buffer, data []byte) { + var work bytes.Buffer + work.WriteString("") + work.WriteByte('\n') + out.Write(work.Bytes()) +} + +// ~~ Horizontal Rules +var reHorizontalRule = regexp.MustCompile(`^\s*?-----\s?$`) + +func isHorizontalRule(data []byte) bool { + return reHorizontalRule.Match(data) +} + +// ~~ Paragraphs +func (p *parser) generateParagraph(out *bytes.Buffer, data []byte) { + generate := func() bool { + p.inline(out, bytes.Trim(data, " ")) + return true + } + p.r.Paragraph(out, generate) +} + +func (p *parser) generateList(output *bytes.Buffer, data []byte, listType string) { + generateList := func() bool { + output.WriteByte('\n') + p.inline(output, bytes.Trim(data, " ")) + return true + } + switch listType { + case "ul": + p.r.List(output, generateList, 0) + case "ol": + p.r.List(output, generateList, blackfriday.LIST_TYPE_ORDERED) + case "dl": + p.r.List(output, generateList, blackfriday.LIST_TYPE_DEFINITION) + } +} + +// Objects + +func (p *parser) inline(out *bytes.Buffer, data []byte) { + i, end := 0, 0 + + for i < len(data) { + for end < len(data) && p.inlineCallback[data[end]] == nil { + end++ + } + + p.r.Entity(out, data[i:end]) + + if end >= len(data) { + break + } + i = end + + handler := p.inlineCallback[data[i]] + + if consumed := handler(p, out, data, i); consumed > 0 { + i += consumed + end = i + continue + } + + end = i + 1 + } +} + +func isAcceptablePreOpeningChar(dataIn, data []byte, offset int) bool { + if len(dataIn) == len(data) { + return true + } + + char := dataIn[offset-1] + return charMatches(char, ' ') || isPreChar(char) +} + +func isPreChar(char byte) bool { + return charMatches(char, '>') || charMatches(char, '(') || charMatches(char, '{') || charMatches(char, '[') +} + +func isAcceptablePostClosingChar(char byte) bool { + return charMatches(char, ' ') || isTerminatingChar(char) +} + +func isTerminatingChar(char byte) bool { + return charMatches(char, '.') || charMatches(char, ',') || charMatches(char, '?') || charMatches(char, '!') || charMatches(char, ')') || charMatches(char, '}') || charMatches(char, ']') +} + +func findLastCharInInline(data []byte, char byte) int { + timesFound := 0 + last := 0 + // Start from character after the inline indicator + for i := 1; i < len(data); i++ { + if timesFound == 1 { + break + } + if data[i] == char { + if len(data) == i+1 || (len(data) > i+1 && isAcceptablePostClosingChar(data[i+1])) { + last = i + timesFound += 1 + } + } + } + return last +} + +func generator(p *parser, out *bytes.Buffer, dataIn []byte, offset int, char byte, doInline bool, renderer func(*bytes.Buffer, []byte)) int { + data := dataIn[offset:] + c := byte(char) + start := 1 + i := start + if len(data) <= 1 { + return 0 + } + + lastCharInside := findLastCharInInline(data, c) + + // Org mode spec says a non-whitespace character must immediately follow. + // if the current char is the marker, then there's no text between, not a candidate + if isSpace(data[i]) || lastCharInside == i || !isAcceptablePreOpeningChar(dataIn, data, offset) { + return 0 + } + + if lastCharInside > 0 { + var work bytes.Buffer + if doInline { + p.inline(&work, data[start:lastCharInside]) + renderer(out, work.Bytes()) + } else { + renderer(out, data[start:lastCharInside]) + } + next := lastCharInside + 1 + return next + } + + return 0 +} + +// ~~ Text Markup +func generateVerbatim(p *parser, out *bytes.Buffer, data []byte, offset int) int { + return generator(p, out, data, offset, '=', false, p.r.CodeSpan) +} + +func generateCode(p *parser, out *bytes.Buffer, data []byte, offset int) int { + return generator(p, out, data, offset, '~', false, p.r.CodeSpan) +} + +func generateEmphasis(p *parser, out *bytes.Buffer, data []byte, offset int) int { + return generator(p, out, data, offset, '/', true, p.r.Emphasis) +} + +func generateUnderline(p *parser, out *bytes.Buffer, data []byte, offset int) int { + underline := func(out *bytes.Buffer, text []byte) { + out.WriteString("") + out.Write(text) + out.WriteString("") + } + + return generator(p, out, data, offset, '_', true, underline) +} + +func generateBold(p *parser, out *bytes.Buffer, data []byte, offset int) int { + return generator(p, out, data, offset, '*', true, p.r.DoubleEmphasis) +} + +func generateStrikethrough(p *parser, out *bytes.Buffer, data []byte, offset int) int { + return generator(p, out, data, offset, '+', true, p.r.StrikeThrough) +} + +// ~~ Images and Links (inc. Footnote) +var reLinkOrImg = regexp.MustCompile(`\[\[(.+?)\]\[?(.*?)\]?\]`) + +func generateLinkOrImg(p *parser, out *bytes.Buffer, data []byte, offset int) int { + data = data[offset+1:] + start := 1 + i := start + var hyperlink []byte + isImage := false + isFootnote := false + closedLink := false + hasContent := false + + if bytes.Equal(data[0:3], []byte("fn:")) { + isFootnote = true + } else if data[0] != '[' { + return 0 + } + + if bytes.Equal(data[1:6], []byte("file:")) { + isImage = true + } + + for i < len(data) { + currChar := data[i] + switch { + case charMatches(currChar, ']') && closedLink == false: + if isImage { + hyperlink = data[start+5 : i] + } else if isFootnote { + refid := data[start+2 : i] + if bytes.Equal(refid, bytes.Trim(refid, " ")) { + p.notes = append(p.notes, footnotes{string(refid), "DEFINITION NOT FOUND"}) + p.r.FootnoteRef(out, refid, len(p.notes)) + return i + 2 + } else { + return 0 + } + } else if bytes.Equal(data[i-4:i], []byte(".org")) { + orgStart := start + if bytes.Equal(data[orgStart:orgStart+2], []byte("./")) { + orgStart = orgStart + 1 + } + hyperlink = data[orgStart : i-4] + } else { + hyperlink = data[start:i] + } + closedLink = true + case charMatches(currChar, '['): + start = i + 1 + hasContent = true + case charMatches(currChar, ']') && closedLink == true && hasContent == true && isImage == true: + p.r.Image(out, hyperlink, data[start:i], data[start:i]) + return i + 3 + case charMatches(currChar, ']') && closedLink == true && hasContent == true: + var tmpBuf bytes.Buffer + p.inline(&tmpBuf, data[start:i]) + p.r.Link(out, hyperlink, tmpBuf.Bytes(), tmpBuf.Bytes()) + return i + 3 + case charMatches(currChar, ']') && closedLink == true && hasContent == false && isImage == true: + p.r.Image(out, hyperlink, hyperlink, hyperlink) + return i + 2 + case charMatches(currChar, ']') && closedLink == true && hasContent == false: + p.r.Link(out, hyperlink, hyperlink, hyperlink) + return i + 2 + } + i++ + } + + return 0 +} + +// Helpers +func skipChar(data []byte, start int, char byte) int { + i := start + for i < len(data) && charMatches(data[i], char) { + i++ + } + return i +} + +func isSpace(char byte) bool { + return charMatches(char, ' ') +} + +func isEmpty(data []byte) bool { + if len(data) == 0 { + return true + } + + for i := 0; i < len(data) && !charMatches(data[i], '\n'); i++ { + if !charMatches(data[i], ' ') && !charMatches(data[i], '\t') { + return false + } + } + return true +} + +func charMatches(a byte, b byte) bool { + return a == b +} diff --git a/vendor/github.com/chaseadamsio/goorgeous/gopher.gif b/vendor/github.com/chaseadamsio/goorgeous/gopher.gif new file mode 100644 index 0000000000000000000000000000000000000000..be7567e3cfd666ee40c56b1d808f97116c092053 GIT binary patch literal 15232 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 literal 0 HcmV?d00001 diff --git a/vendor/github.com/chaseadamsio/goorgeous/gopher_small.gif b/vendor/github.com/chaseadamsio/goorgeous/gopher_small.gif new file mode 100644 index 0000000000000000000000000000000000000000..1cd31fdd0c1cdd945e36871c78525bef41b7a1b9 GIT binary patch literal 3270 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 literal 0 HcmV?d00001 diff --git a/vendor/github.com/chaseadamsio/goorgeous/header.go b/vendor/github.com/chaseadamsio/goorgeous/header.go new file mode 100644 index 0000000000..66e8b99321 --- /dev/null +++ b/vendor/github.com/chaseadamsio/goorgeous/header.go @@ -0,0 +1,70 @@ +package goorgeous + +import ( + "bufio" + "bytes" + "regexp" + "strings" +) + +// ExtractOrgHeaders finds and returns all of the headers +// from a bufio.Reader and returns them as their own byte slice +func ExtractOrgHeaders(r *bufio.Reader) (fm []byte, err error) { + var out bytes.Buffer + endOfHeaders := true + for endOfHeaders { + p, err := r.Peek(2) + if err != nil { + return nil, err + } + if !charMatches(p[0], '#') && !charMatches(p[1], '+') { + endOfHeaders = false + break + } + line, _, err := r.ReadLine() + if err != nil { + return nil, err + } + out.Write(line) + out.WriteByte('\n') + } + return out.Bytes(), nil +} + +var reHeader = regexp.MustCompile(`^#\+(\w+?): (.*)`) + +// OrgHeaders find all of the headers from a byte slice and returns +// them as a map of string interface +func OrgHeaders(input []byte) (map[string]interface{}, error) { + out := make(map[string]interface{}) + scanner := bufio.NewScanner(bytes.NewReader(input)) + + for scanner.Scan() { + data := scanner.Bytes() + if !charMatches(data[0], '#') && !charMatches(data[1], '+') { + return out, nil + } + matches := reHeader.FindSubmatch(data) + + if len(matches) < 3 { + continue + } + + key := string(matches[1]) + val := matches[2] + switch { + case strings.ToLower(key) == "tags" || strings.ToLower(key) == "categories" || strings.ToLower(key) == "aliases": + bTags := bytes.Split(val, []byte(" ")) + tags := make([]string, len(bTags)) + for idx, tag := range bTags { + tags[idx] = string(tag) + } + out[key] = tags + default: + out[key] = string(val) + } + + } + return out, nil + +} diff --git a/vendor/vendor.json b/vendor/vendor.json index d9cb18cd1a..101c72c217 100644 --- a/vendor/vendor.json +++ b/vendor/vendor.json @@ -299,6 +299,12 @@ "revision": "fb1f79c6b65acda83063cbc69f6bba1522558bfc", "revisionTime": "2016-01-17T19:21:50Z" }, + { + "checksumSHA1": "x1svIugw39oEZGU5/HMUHzgRUZM=", + "path": "github.com/chaseadamsio/goorgeous", + "revision": "098da33fde5f9220736531b3cb26a2dec86a8367", + "revisionTime": "2017-09-01T13:22:37Z" + }, { "checksumSHA1": "agNqSytP0indDCoGizlMyC1L/m4=", "path": "github.com/coreos/etcd/error",