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
release/v1.3
Lunny Xiao 7 years ago committed by GitHub
parent 66bc0ac251
commit 0d80af649a

@ -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"
)

@ -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"

@ -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"

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

@ -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("<a href=\"%s\">%s</a>", 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

@ -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"
)

@ -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)
}

@ -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 := `<p><a href="https://google.com/" title="https://google.com/">https://google.com/</a></p>`
test("[[https://google.com/]]", googleRendered)
lnk := markup.URLJoin(AppSubURL, "WikiPage")
test("[[WikiPage][WikiPage]]",
`<p><a href="`+lnk+`" title="WikiPage">WikiPage</a></p>`)
}
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+"]]",
`<p><a href="`+result+`"><img src="`+result+`" alt="`+title+`" title="`+title+`" /></a></p>`)
}

@ -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"
)

@ -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"

@ -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"

@ -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(`<br>`), -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()))

@ -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 (

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

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

@ -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
<h1>Some Headline</h1>/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!

@ -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("</pre>\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("</pre>\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("<center>\n")
p.inline(&tmpBuf, tmpBlock.Bytes())
output.Write(tmpBuf.Bytes())
output.WriteString("</center>\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("<p>\n"))
p.inline(&tmpBuf, data)
tmpBuf.WriteByte('\n')
tmpBuf.Write([]byte("</p>\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("<li")
if len(matches[2]) > 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("</li>\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("<pre class=\"example\">\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("</pre>")
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("</pre>\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("<span class=\"todo " + status + "\">" + status + "</span>")
out.WriteByte(' ')
}
if priority != "" {
out.WriteString("<span class=\"priority " + priority + "\">[" + priority + "]</span>")
out.WriteByte(' ')
}
p.inline(out, headline)
if tagsFound > 0 {
for _, tag := range tags {
out.WriteByte(' ')
out.WriteString("<span class=\"tags " + tag + "\">" + tag + "</span>")
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("<thead>")
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("</thead>\n")
} else if hasTableHeaders && idx == 1 {
continue
} else {
if !tbodySet {
table.WriteString("<tbody>")
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("</tbody>\n")
tbodySet = false
}
}
}
output.WriteString("\n<table>\n")
output.Write(table.Bytes())
output.WriteString("</table>\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.Write(data[2:])
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("<span style=\"text-decoration: underline;\">")
out.Write(text)
out.WriteString("</span>")
}
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
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

@ -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
}

@ -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",

Loading…
Cancel
Save