"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"
)
"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"
+++ /dev/null
-// Copyright 2014 The Gogs 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 markdown
-
-import (
- "bytes"
- "strings"
-
- "code.gitea.io/gitea/modules/markup"
- "code.gitea.io/gitea/modules/setting"
-
- "github.com/russross/blackfriday"
-)
-
-// Renderer is a extended version of underlying render object.
-type Renderer struct {
- blackfriday.Renderer
- urlPrefix string
- isWikiMarkdown bool
-}
-
-// Link defines how formal links should be processed to produce corresponding HTML elements.
-func (r *Renderer) Link(out *bytes.Buffer, link []byte, title []byte, content []byte) {
- if len(link) > 0 && !markup.IsLink(link) {
- if link[0] != '#' {
- lnk := string(link)
- if r.isWikiMarkdown {
- lnk = markup.URLJoin("wiki", lnk)
- }
- mLink := markup.URLJoin(r.urlPrefix, lnk)
- link = []byte(mLink)
- }
- }
-
- r.Renderer.Link(out, link, title, content)
-}
-
-// List renders markdown bullet or digit lists to HTML
-func (r *Renderer) List(out *bytes.Buffer, text func() bool, flags int) {
- marker := out.Len()
- if out.Len() > 0 {
- out.WriteByte('\n')
- }
-
- if flags&blackfriday.LIST_TYPE_DEFINITION != 0 {
- out.WriteString("<dl>")
- } else if flags&blackfriday.LIST_TYPE_ORDERED != 0 {
- out.WriteString("<ol class='ui list'>")
- } else {
- out.WriteString("<ul class='ui list'>")
- }
- if !text() {
- out.Truncate(marker)
- return
- }
- if flags&blackfriday.LIST_TYPE_DEFINITION != 0 {
- out.WriteString("</dl>\n")
- } else if flags&blackfriday.LIST_TYPE_ORDERED != 0 {
- out.WriteString("</ol>\n")
- } else {
- out.WriteString("</ul>\n")
- }
-}
-
-// ListItem defines how list items should be processed to produce corresponding HTML elements.
-func (r *Renderer) ListItem(out *bytes.Buffer, text []byte, flags int) {
- // Detect procedures to draw checkboxes.
- prefix := ""
- if bytes.HasPrefix(text, []byte("<p>")) {
- prefix = "<p>"
- }
- switch {
- case bytes.HasPrefix(text, []byte(prefix+"[ ] ")):
- text = append([]byte(`<span class="ui fitted disabled checkbox"><input type="checkbox" disabled="disabled" /><label /></span>`), text[3+len(prefix):]...)
- if prefix != "" {
- text = bytes.Replace(text, []byte(prefix), []byte{}, 1)
- }
- case bytes.HasPrefix(text, []byte(prefix+"[x] ")):
- text = append([]byte(`<span class="ui checked fitted disabled checkbox"><input type="checkbox" checked="" disabled="disabled" /><label /></span>`), text[3+len(prefix):]...)
- if prefix != "" {
- text = bytes.Replace(text, []byte(prefix), []byte{}, 1)
- }
- }
- r.Renderer.ListItem(out, text, flags)
-}
-
-// Note: this section is for purpose of increase performance and
-// reduce memory allocation at runtime since they are constant literals.
-var (
- svgSuffix = []byte(".svg")
- svgSuffixWithMark = []byte(".svg?")
-)
-
-// 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 = markup.URLJoin(prefix, "wiki", "src")
- }
- prefix = strings.Replace(prefix, "/src/", "/raw/", 1)
- if len(link) > 0 {
- if markup.IsLink(link) {
- // External link with .svg suffix usually means CI status.
- // TODO: define a keyword to allow non-svg images render as external link.
- if bytes.HasSuffix(link, svgSuffix) || bytes.Contains(link, svgSuffixWithMark) {
- r.Renderer.Image(out, link, title, alt)
- return
- }
- } else {
- lnk := string(link)
- lnk = markup.URLJoin(prefix, lnk)
- lnk = strings.Replace(lnk, " ", "+", -1)
- link = []byte(lnk)
- }
- }
-
- out.WriteString(`<a href="`)
- out.Write(link)
- out.WriteString(`">`)
- r.Renderer.Image(out, link, title, alt)
- out.WriteString("</a>")
-}
-
-// RenderRaw renders Markdown to HTML without handling special links.
-func RenderRaw(body []byte, urlPrefix string, wikiMarkdown bool) []byte {
- htmlFlags := 0
- htmlFlags |= blackfriday.HTML_SKIP_STYLE
- htmlFlags |= blackfriday.HTML_OMIT_CONTENTS
- renderer := &Renderer{
- Renderer: blackfriday.HtmlRenderer(htmlFlags, "", ""),
- urlPrefix: urlPrefix,
- isWikiMarkdown: wikiMarkdown,
- }
-
- // set up the parser
- extensions := 0
- extensions |= blackfriday.EXTENSION_NO_INTRA_EMPHASIS
- extensions |= blackfriday.EXTENSION_TABLES
- extensions |= blackfriday.EXTENSION_FENCED_CODE
- extensions |= blackfriday.EXTENSION_STRIKETHROUGH
- extensions |= blackfriday.EXTENSION_NO_EMPTY_LINE_BEFORE_BLOCK
-
- if setting.Markdown.EnableHardLineBreak {
- extensions |= blackfriday.EXTENSION_HARD_LINE_BREAK
- }
-
- body = blackfriday.Markdown(body, renderer, extensions)
- return body
-}
-
-var (
- // MarkupName describes markup's name
- MarkupName = "markdown"
-)
-
-func init() {
- markup.RegisterParser(Parser{})
-}
-
-// Parser implements markup.Parser
-type Parser struct {
-}
-
-// Name implements markup.Parser
-func (Parser) Name() string {
- return MarkupName
-}
-
-// Extensions implements markup.Parser
-func (Parser) Extensions() []string {
- return setting.Markdown.FileExtensions
-}
-
-// Render implements markup.Parser
-func (Parser) Render(rawBytes []byte, urlPrefix string, metas map[string]string, isWiki bool) []byte {
- return RenderRaw(rawBytes, urlPrefix, isWiki)
-}
-
-// Render renders Markdown to HTML with all specific handling stuff.
-func Render(rawBytes []byte, urlPrefix string, metas map[string]string) []byte {
- return markup.Render("a.md", rawBytes, urlPrefix, metas)
-}
-
-// RenderString renders Markdown to HTML with special links and returns string type.
-func RenderString(raw, urlPrefix string, metas map[string]string) string {
- return markup.RenderString("a.md", raw, urlPrefix, metas)
-}
-
-// RenderWiki renders markdown wiki page to HTML and return HTML string
-func RenderWiki(rawBytes []byte, urlPrefix string, metas map[string]string) string {
- return markup.RenderWiki("a.md", rawBytes, urlPrefix, metas)
-}
-
-// IsMarkdownFile reports whether name looks like a Markdown file
-// based on its extension.
-func IsMarkdownFile(name string) bool {
- return markup.IsMarkupFile(name, MarkupName)
-}
+++ /dev/null
-// 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 markdown_test
-
-import (
- "fmt"
- "strconv"
- "strings"
- "testing"
-
- . "code.gitea.io/gitea/modules/markdown"
- "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 + "/"
-
-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
-
- test := func(input, expected, expectedWiki string) {
- buffer := RenderString(input, setting.AppSubURL, nil)
- assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(string(buffer)))
- bufferWiki := RenderWiki([]byte(input), setting.AppSubURL, nil)
- assert.Equal(t, strings.TrimSpace(expectedWiki), strings.TrimSpace(bufferWiki))
- }
-
- googleRendered := `<p><a href="https://google.com/" rel="nofollow">https://google.com/</a></p>`
- test("<https://google.com/>", googleRendered, googleRendered)
-
- lnk := markup.URLJoin(AppSubURL, "WikiPage")
- lnkWiki := markup.URLJoin(AppSubURL, "wiki", "WikiPage")
- test("[WikiPage](WikiPage)",
- `<p><a href="`+lnk+`" rel="nofollow">WikiPage</a></p>`,
- `<p><a href="`+lnkWiki+`" rel="nofollow">WikiPage</a></p>`)
-}
-
-func TestRender_ShortLinks(t *testing.T) {
- setting.AppURL = AppURL
- setting.AppSubURL = AppSubURL
- tree := markup.URLJoin(AppSubURL, "src", "master")
-
- test := func(input, expected, expectedWiki string) {
- buffer := RenderString(input, tree, nil)
- assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(string(buffer)))
- buffer = RenderWiki([]byte(input), setting.AppSubURL, nil)
- assert.Equal(t, strings.TrimSpace(expectedWiki), strings.TrimSpace(string(buffer)))
- }
-
- rawtree := markup.URLJoin(AppSubURL, "raw", "master")
- url := markup.URLJoin(tree, "Link")
- otherUrl := markup.URLJoin(tree, "OtherLink")
- imgurl := markup.URLJoin(rawtree, "Link.jpg")
- urlWiki := markup.URLJoin(AppSubURL, "wiki", "Link")
- otherUrlWiki := markup.URLJoin(AppSubURL, "wiki", "OtherLink")
- imgurlWiki := markup.URLJoin(AppSubURL, "wiki", "raw", "Link.jpg")
- favicon := "http://google.com/favicon.ico"
-
- test(
- "[[Link]]",
- `<p><a href="`+url+`" rel="nofollow">Link</a></p>`,
- `<p><a href="`+urlWiki+`" rel="nofollow">Link</a></p>`)
- test(
- "[[Link.jpg]]",
- `<p><a href="`+imgurl+`" rel="nofollow"><img src="`+imgurl+`" alt="Link.jpg" title="Link.jpg"/></a></p>`,
- `<p><a href="`+imgurlWiki+`" rel="nofollow"><img src="`+imgurlWiki+`" alt="Link.jpg" title="Link.jpg"/></a></p>`)
- test(
- "[["+favicon+"]]",
- `<p><a href="`+favicon+`" rel="nofollow"><img src="`+favicon+`" title="favicon.ico"/></a></p>`,
- `<p><a href="`+favicon+`" rel="nofollow"><img src="`+favicon+`" title="favicon.ico"/></a></p>`)
- test(
- "[[Name|Link]]",
- `<p><a href="`+url+`" rel="nofollow">Name</a></p>`,
- `<p><a href="`+urlWiki+`" rel="nofollow">Name</a></p>`)
- test(
- "[[Name|Link.jpg]]",
- `<p><a href="`+imgurl+`" rel="nofollow"><img src="`+imgurl+`" alt="Name" title="Name"/></a></p>`,
- `<p><a href="`+imgurlWiki+`" rel="nofollow"><img src="`+imgurlWiki+`" alt="Name" title="Name"/></a></p>`)
- test(
- "[[Name|Link.jpg|alt=AltName]]",
- `<p><a href="`+imgurl+`" rel="nofollow"><img src="`+imgurl+`" alt="AltName" title="AltName"/></a></p>`,
- `<p><a href="`+imgurlWiki+`" rel="nofollow"><img src="`+imgurlWiki+`" alt="AltName" title="AltName"/></a></p>`)
- test(
- "[[Name|Link.jpg|title=Title]]",
- `<p><a href="`+imgurl+`" rel="nofollow"><img src="`+imgurl+`" alt="Title" title="Title"/></a></p>`,
- `<p><a href="`+imgurlWiki+`" rel="nofollow"><img src="`+imgurlWiki+`" alt="Title" title="Title"/></a></p>`)
- test(
- "[[Name|Link.jpg|alt=AltName|title=Title]]",
- `<p><a href="`+imgurl+`" rel="nofollow"><img src="`+imgurl+`" alt="AltName" title="Title"/></a></p>`,
- `<p><a href="`+imgurlWiki+`" rel="nofollow"><img src="`+imgurlWiki+`" alt="AltName" title="Title"/></a></p>`)
- test(
- "[[Name|Link.jpg|alt=\"AltName\"|title='Title']]",
- `<p><a href="`+imgurl+`" rel="nofollow"><img src="`+imgurl+`" alt="AltName" title="Title"/></a></p>`,
- `<p><a href="`+imgurlWiki+`" rel="nofollow"><img src="`+imgurlWiki+`" alt="AltName" title="Title"/></a></p>`)
- test(
- "[[Link]] [[OtherLink]]",
- `<p><a href="`+url+`" rel="nofollow">Link</a> <a href="`+otherUrl+`" rel="nofollow">OtherLink</a></p>`,
- `<p><a href="`+urlWiki+`" rel="nofollow">Link</a> <a href="`+otherUrlWiki+`" rel="nofollow">OtherLink</a></p>`)
-}
-
-func TestMisc_IsMarkdownFile(t *testing.T) {
- setting.Markdown.FileExtensions = []string{".md", ".markdown", ".mdown", ".mkd"}
- trueTestCases := []string{
- "test.md",
- "wow.MARKDOWN",
- "LOL.mDoWn",
- }
- falseTestCases := []string{
- "test",
- "abcdefg",
- "abcdefghijklmnopqrstuvwxyz",
- "test.md.test",
- }
-
- for _, testCase := range trueTestCases {
- assert.True(t, IsMarkdownFile(testCase))
- }
- for _, testCase := range falseTestCases {
- assert.False(t, IsMarkdownFile(testCase))
- }
-}
-
-func TestRender_Images(t *testing.T) {
- setting.AppURL = AppURL
- setting.AppSubURL = AppSubURL
-
- test := func(input, expected string) {
- buffer := RenderString(input, setting.AppSubURL, nil)
- assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(string(buffer)))
- }
-
- url := "../../.images/src/02/train.jpg"
- title := "Train"
- result := markup.URLJoin(AppSubURL, url)
-
- test(
- "!["+title+"]("+url+")",
- `<p><a href="`+result+`" rel="nofollow"><img src="`+result+`" alt="`+title+`"></a></p>`)
-
- test(
- "[["+title+"|"+url+"]]",
- `<p><a href="`+result+`" rel="nofollow"><img src="`+result+`" alt="`+title+`" title="`+title+`"/></a></p>`)
-}
-
-func TestRegExp_ShortLinkPattern(t *testing.T) {
- trueTestCases := []string{
- "[[stuff]]",
- "[[]]",
- "[[stuff|title=Difficult name with spaces*!]]",
- }
- falseTestCases := []string{
- "test",
- "abcdefg",
- "[[]",
- "[[",
- "[]",
- "]]",
- "abcdefghijklmnopqrstuvwxyz",
- }
-
- for _, testCase := range trueTestCases {
- assert.True(t, markup.ShortLinkPattern.MatchString(testCase))
- }
- for _, testCase := range falseTestCases {
- assert.False(t, markup.ShortLinkPattern.MatchString(testCase))
- }
-}
-
-func testAnswers(baseURLContent, baseURLImages string) []string {
- return []string{
- `<p>Wiki! Enjoy :)</p>
-
-<ul>
-<li><a href="` + baseURLContent + `/Links" rel="nofollow">Links, Language bindings, Engine bindings</a></li>
-<li><a href="` + baseURLContent + `/Tips" rel="nofollow">Tips</a></li>
-</ul>
-
-<p>Ideas and codes</p>
-
-<ul>
-<li>Bezier widget (by <a href="` + AppURL + `r-lyeh" rel="nofollow">@r-lyeh</a>) <a href="http://localhost:3000/ocornut/imgui/issues/786" rel="nofollow">#786</a></li>
-<li>Node graph editors https://github.com/ocornut/imgui/issues/306</li>
-<li><a href="` + baseURLContent + `/memory_editor_example" rel="nofollow">Memory Editor</a></li>
-<li><a href="` + baseURLContent + `/plot_var_example" rel="nofollow">Plot var helper</a></li>
-</ul>
-`,
- `<h2>What is Wine Staging?</h2>
-
-<p><strong>Wine Staging</strong> on website <a href="http://wine-staging.com" rel="nofollow">wine-staging.com</a>.</p>
-
-<h2>Quick Links</h2>
-
-<p>Here are some links to the most important topics. You can find the full list of pages at the sidebar.</p>
-
-<table>
-<thead>
-<tr>
-<th><a href="` + baseURLImages + `/images/icon-install.png" rel="nofollow"><img src="` + baseURLImages + `/images/icon-install.png" alt="images/icon-install.png" title="icon-install.png"/></a></th>
-<th><a href="` + baseURLContent + `/Installation" rel="nofollow">Installation</a></th>
-</tr>
-</thead>
-
-<tbody>
-<tr>
-<td><a href="` + baseURLImages + `/images/icon-usage.png" rel="nofollow"><img src="` + baseURLImages + `/images/icon-usage.png" alt="images/icon-usage.png" title="icon-usage.png"/></a></td>
-<td><a href="` + baseURLContent + `/Usage" rel="nofollow">Usage</a></td>
-</tr>
-</tbody>
-</table>
-`,
- `<p><a href="http://www.excelsiorjet.com/" rel="nofollow">Excelsior JET</a> allows you to create native executables for Windows, Linux and Mac OS X.</p>
-
-<ol>
-<li><a href="https://github.com/libgdx/libgdx/wiki/Gradle-on-the-Commandline#packaging-for-the-desktop" rel="nofollow">Package your libGDX application</a>
-<a href="` + baseURLImages + `/images/1.png" rel="nofollow"><img src="` + baseURLImages + `/images/1.png" alt="images/1.png" title="1.png"/></a></li>
-<li>Perform a test run by hitting the Run! button.
-<a href="` + baseURLImages + `/images/2.png" rel="nofollow"><img src="` + baseURLImages + `/images/2.png" alt="images/2.png" title="2.png"/></a></li>
-</ol>
-`,
- }
-}
-
-// Test cases without ambiguous links
-var sameCases = []string{
- // dear imgui wiki markdown extract: special wiki syntax
- `Wiki! Enjoy :)
-- [[Links, Language bindings, Engine bindings|Links]]
-- [[Tips]]
-
-Ideas and codes
-
-- Bezier widget (by @r-lyeh) ` + AppURL + `ocornut/imgui/issues/786
-- Node graph editors https://github.com/ocornut/imgui/issues/306
-- [[Memory Editor|memory_editor_example]]
-- [[Plot var helper|plot_var_example]]`,
- // wine-staging wiki home extract: tables, special wiki syntax, images
- `## What is Wine Staging?
-**Wine Staging** on website [wine-staging.com](http://wine-staging.com).
-
-## Quick Links
-Here are some links to the most important topics. You can find the full list of pages at the sidebar.
-
-| [[images/icon-install.png]] | [[Installation]] |
-|--------------------------------|----------------------------------------------------------|
-| [[images/icon-usage.png]] | [[Usage]] |
-`,
- // libgdx wiki page: inline images with special syntax
- `[Excelsior JET](http://www.excelsiorjet.com/) allows you to create native executables for Windows, Linux and Mac OS X.
-
-1. [Package your libGDX application](https://github.com/libgdx/libgdx/wiki/Gradle-on-the-Commandline#packaging-for-the-desktop)
-[[images/1.png]]
-2. Perform a test run by hitting the Run! button.
-[[images/2.png]]`,
-}
-
-func TestTotal_RenderWiki(t *testing.T) {
- answers := testAnswers(markup.URLJoin(AppSubURL, "wiki/"), markup.URLJoin(AppSubURL, "wiki", "raw/"))
-
- for i := 0; i < len(sameCases); i++ {
- line := RenderWiki([]byte(sameCases[i]), AppSubURL, nil)
- assert.Equal(t, answers[i], line)
- }
-
- testCases := []string{
- // Guard wiki sidebar: special syntax
- `[[Guardfile-DSL / Configuring-Guard|Guardfile-DSL---Configuring-Guard]]`,
- // rendered
- `<p><a href="` + AppSubURL + `wiki/Guardfile-DSL---Configuring-Guard" rel="nofollow">Guardfile-DSL / Configuring-Guard</a></p>
-`,
- // special syntax
- `[[Name|Link]]`,
- // rendered
- `<p><a href="` + AppSubURL + `wiki/Link" rel="nofollow">Name</a></p>
-`,
- }
-
- for i := 0; i < len(testCases); i += 2 {
- line := RenderWiki([]byte(testCases[i]), AppSubURL, nil)
- assert.Equal(t, testCases[i+1], line)
- }
-}
-
-func TestTotal_RenderString(t *testing.T) {
- answers := testAnswers(markup.URLJoin(AppSubURL, "src", "master/"), markup.URLJoin(AppSubURL, "raw", "master/"))
-
- for i := 0; i < len(sameCases); i++ {
- line := RenderString(sameCases[i], markup.URLJoin(AppSubURL, "src", "master/"), nil)
- assert.Equal(t, answers[i], line)
- }
-
- testCases := []string{}
-
- for i := 0; i < len(testCases); i += 2 {
- line := RenderString(testCases[i], AppSubURL, nil)
- assert.Equal(t, testCases[i+1], line)
- }
-}
"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"
--- /dev/null
+// Copyright 2014 The Gogs 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 markdown
+
+import (
+ "bytes"
+ "strings"
+
+ "code.gitea.io/gitea/modules/markup"
+ "code.gitea.io/gitea/modules/setting"
+
+ "github.com/russross/blackfriday"
+)
+
+// Renderer is a extended version of underlying render object.
+type Renderer struct {
+ blackfriday.Renderer
+ URLPrefix string
+ IsWiki bool
+}
+
+// Link defines how formal links should be processed to produce corresponding HTML elements.
+func (r *Renderer) Link(out *bytes.Buffer, link []byte, title []byte, content []byte) {
+ if len(link) > 0 && !markup.IsLink(link) {
+ if link[0] != '#' {
+ lnk := string(link)
+ if r.IsWiki {
+ lnk = markup.URLJoin("wiki", lnk)
+ }
+ mLink := markup.URLJoin(r.URLPrefix, lnk)
+ link = []byte(mLink)
+ }
+ }
+
+ r.Renderer.Link(out, link, title, content)
+}
+
+// List renders markdown bullet or digit lists to HTML
+func (r *Renderer) List(out *bytes.Buffer, text func() bool, flags int) {
+ marker := out.Len()
+ if out.Len() > 0 {
+ out.WriteByte('\n')
+ }
+
+ if flags&blackfriday.LIST_TYPE_DEFINITION != 0 {
+ out.WriteString("<dl>")
+ } else if flags&blackfriday.LIST_TYPE_ORDERED != 0 {
+ out.WriteString("<ol class='ui list'>")
+ } else {
+ out.WriteString("<ul class='ui list'>")
+ }
+ if !text() {
+ out.Truncate(marker)
+ return
+ }
+ if flags&blackfriday.LIST_TYPE_DEFINITION != 0 {
+ out.WriteString("</dl>\n")
+ } else if flags&blackfriday.LIST_TYPE_ORDERED != 0 {
+ out.WriteString("</ol>\n")
+ } else {
+ out.WriteString("</ul>\n")
+ }
+}
+
+// ListItem defines how list items should be processed to produce corresponding HTML elements.
+func (r *Renderer) ListItem(out *bytes.Buffer, text []byte, flags int) {
+ // Detect procedures to draw checkboxes.
+ prefix := ""
+ if bytes.HasPrefix(text, []byte("<p>")) {
+ prefix = "<p>"
+ }
+ switch {
+ case bytes.HasPrefix(text, []byte(prefix+"[ ] ")):
+ text = append([]byte(`<span class="ui fitted disabled checkbox"><input type="checkbox" disabled="disabled" /><label /></span>`), text[3+len(prefix):]...)
+ if prefix != "" {
+ text = bytes.Replace(text, []byte(prefix), []byte{}, 1)
+ }
+ case bytes.HasPrefix(text, []byte(prefix+"[x] ")):
+ text = append([]byte(`<span class="ui checked fitted disabled checkbox"><input type="checkbox" checked="" disabled="disabled" /><label /></span>`), text[3+len(prefix):]...)
+ if prefix != "" {
+ text = bytes.Replace(text, []byte(prefix), []byte{}, 1)
+ }
+ }
+ r.Renderer.ListItem(out, text, flags)
+}
+
+// Note: this section is for purpose of increase performance and
+// reduce memory allocation at runtime since they are constant literals.
+var (
+ svgSuffix = []byte(".svg")
+ svgSuffixWithMark = []byte(".svg?")
+)
+
+// 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.IsWiki {
+ prefix = markup.URLJoin(prefix, "wiki", "src")
+ }
+ prefix = strings.Replace(prefix, "/src/", "/raw/", 1)
+ if len(link) > 0 {
+ if markup.IsLink(link) {
+ // External link with .svg suffix usually means CI status.
+ // TODO: define a keyword to allow non-svg images render as external link.
+ if bytes.HasSuffix(link, svgSuffix) || bytes.Contains(link, svgSuffixWithMark) {
+ r.Renderer.Image(out, link, title, alt)
+ return
+ }
+ } else {
+ lnk := string(link)
+ lnk = markup.URLJoin(prefix, lnk)
+ lnk = strings.Replace(lnk, " ", "+", -1)
+ link = []byte(lnk)
+ }
+ }
+
+ out.WriteString(`<a href="`)
+ out.Write(link)
+ out.WriteString(`">`)
+ r.Renderer.Image(out, link, title, alt)
+ out.WriteString("</a>")
+}
+
+// RenderRaw renders Markdown to HTML without handling special links.
+func RenderRaw(body []byte, urlPrefix string, wikiMarkdown bool) []byte {
+ htmlFlags := 0
+ htmlFlags |= blackfriday.HTML_SKIP_STYLE
+ htmlFlags |= blackfriday.HTML_OMIT_CONTENTS
+ renderer := &Renderer{
+ Renderer: blackfriday.HtmlRenderer(htmlFlags, "", ""),
+ URLPrefix: urlPrefix,
+ IsWiki: wikiMarkdown,
+ }
+
+ // set up the parser
+ extensions := 0
+ extensions |= blackfriday.EXTENSION_NO_INTRA_EMPHASIS
+ extensions |= blackfriday.EXTENSION_TABLES
+ extensions |= blackfriday.EXTENSION_FENCED_CODE
+ extensions |= blackfriday.EXTENSION_STRIKETHROUGH
+ extensions |= blackfriday.EXTENSION_NO_EMPTY_LINE_BEFORE_BLOCK
+
+ if setting.Markdown.EnableHardLineBreak {
+ extensions |= blackfriday.EXTENSION_HARD_LINE_BREAK
+ }
+
+ body = blackfriday.Markdown(body, renderer, extensions)
+ return body
+}
+
+var (
+ // MarkupName describes markup's name
+ MarkupName = "markdown"
+)
+
+func init() {
+ markup.RegisterParser(Parser{})
+}
+
+// Parser implements markup.Parser
+type Parser struct {
+}
+
+// Name implements markup.Parser
+func (Parser) Name() string {
+ return MarkupName
+}
+
+// Extensions implements markup.Parser
+func (Parser) Extensions() []string {
+ return setting.Markdown.FileExtensions
+}
+
+// Render implements markup.Parser
+func (Parser) Render(rawBytes []byte, urlPrefix string, metas map[string]string, isWiki bool) []byte {
+ return RenderRaw(rawBytes, urlPrefix, isWiki)
+}
+
+// Render renders Markdown to HTML with all specific handling stuff.
+func Render(rawBytes []byte, urlPrefix string, metas map[string]string) []byte {
+ return markup.Render("a.md", rawBytes, urlPrefix, metas)
+}
+
+// RenderString renders Markdown to HTML with special links and returns string type.
+func RenderString(raw, urlPrefix string, metas map[string]string) string {
+ return markup.RenderString("a.md", raw, urlPrefix, metas)
+}
+
+// RenderWiki renders markdown wiki page to HTML and return HTML string
+func RenderWiki(rawBytes []byte, urlPrefix string, metas map[string]string) string {
+ return markup.RenderWiki("a.md", rawBytes, urlPrefix, metas)
+}
+
+// IsMarkdownFile reports whether name looks like a Markdown file
+// based on its extension.
+func IsMarkdownFile(name string) bool {
+ return markup.IsMarkupFile(name, MarkupName)
+}
--- /dev/null
+// 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 markdown_test
+
+import (
+ "strings"
+ "testing"
+
+ "code.gitea.io/gitea/modules/markup"
+ . "code.gitea.io/gitea/modules/markup/markdown"
+ "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, expectedWiki string) {
+ buffer := RenderString(input, setting.AppSubURL, nil)
+ assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(string(buffer)))
+ bufferWiki := RenderWiki([]byte(input), setting.AppSubURL, nil)
+ assert.Equal(t, strings.TrimSpace(expectedWiki), strings.TrimSpace(bufferWiki))
+ }
+
+ googleRendered := `<p><a href="https://google.com/" rel="nofollow">https://google.com/</a></p>`
+ test("<https://google.com/>", googleRendered, googleRendered)
+
+ lnk := markup.URLJoin(AppSubURL, "WikiPage")
+ lnkWiki := markup.URLJoin(AppSubURL, "wiki", "WikiPage")
+ test("[WikiPage](WikiPage)",
+ `<p><a href="`+lnk+`" rel="nofollow">WikiPage</a></p>`,
+ `<p><a href="`+lnkWiki+`" rel="nofollow">WikiPage</a></p>`)
+}
+
+func TestRender_ShortLinks(t *testing.T) {
+ setting.AppURL = AppURL
+ setting.AppSubURL = AppSubURL
+ tree := markup.URLJoin(AppSubURL, "src", "master")
+
+ test := func(input, expected, expectedWiki string) {
+ buffer := RenderString(input, tree, nil)
+ assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(string(buffer)))
+ buffer = RenderWiki([]byte(input), setting.AppSubURL, nil)
+ assert.Equal(t, strings.TrimSpace(expectedWiki), strings.TrimSpace(string(buffer)))
+ }
+
+ rawtree := markup.URLJoin(AppSubURL, "raw", "master")
+ url := markup.URLJoin(tree, "Link")
+ otherUrl := markup.URLJoin(tree, "OtherLink")
+ imgurl := markup.URLJoin(rawtree, "Link.jpg")
+ urlWiki := markup.URLJoin(AppSubURL, "wiki", "Link")
+ otherUrlWiki := markup.URLJoin(AppSubURL, "wiki", "OtherLink")
+ imgurlWiki := markup.URLJoin(AppSubURL, "wiki", "raw", "Link.jpg")
+ favicon := "http://google.com/favicon.ico"
+
+ test(
+ "[[Link]]",
+ `<p><a href="`+url+`" rel="nofollow">Link</a></p>`,
+ `<p><a href="`+urlWiki+`" rel="nofollow">Link</a></p>`)
+ test(
+ "[[Link.jpg]]",
+ `<p><a href="`+imgurl+`" rel="nofollow"><img src="`+imgurl+`" alt="Link.jpg" title="Link.jpg"/></a></p>`,
+ `<p><a href="`+imgurlWiki+`" rel="nofollow"><img src="`+imgurlWiki+`" alt="Link.jpg" title="Link.jpg"/></a></p>`)
+ test(
+ "[["+favicon+"]]",
+ `<p><a href="`+favicon+`" rel="nofollow"><img src="`+favicon+`" title="favicon.ico"/></a></p>`,
+ `<p><a href="`+favicon+`" rel="nofollow"><img src="`+favicon+`" title="favicon.ico"/></a></p>`)
+ test(
+ "[[Name|Link]]",
+ `<p><a href="`+url+`" rel="nofollow">Name</a></p>`,
+ `<p><a href="`+urlWiki+`" rel="nofollow">Name</a></p>`)
+ test(
+ "[[Name|Link.jpg]]",
+ `<p><a href="`+imgurl+`" rel="nofollow"><img src="`+imgurl+`" alt="Name" title="Name"/></a></p>`,
+ `<p><a href="`+imgurlWiki+`" rel="nofollow"><img src="`+imgurlWiki+`" alt="Name" title="Name"/></a></p>`)
+ test(
+ "[[Name|Link.jpg|alt=AltName]]",
+ `<p><a href="`+imgurl+`" rel="nofollow"><img src="`+imgurl+`" alt="AltName" title="AltName"/></a></p>`,
+ `<p><a href="`+imgurlWiki+`" rel="nofollow"><img src="`+imgurlWiki+`" alt="AltName" title="AltName"/></a></p>`)
+ test(
+ "[[Name|Link.jpg|title=Title]]",
+ `<p><a href="`+imgurl+`" rel="nofollow"><img src="`+imgurl+`" alt="Title" title="Title"/></a></p>`,
+ `<p><a href="`+imgurlWiki+`" rel="nofollow"><img src="`+imgurlWiki+`" alt="Title" title="Title"/></a></p>`)
+ test(
+ "[[Name|Link.jpg|alt=AltName|title=Title]]",
+ `<p><a href="`+imgurl+`" rel="nofollow"><img src="`+imgurl+`" alt="AltName" title="Title"/></a></p>`,
+ `<p><a href="`+imgurlWiki+`" rel="nofollow"><img src="`+imgurlWiki+`" alt="AltName" title="Title"/></a></p>`)
+ test(
+ "[[Name|Link.jpg|alt=\"AltName\"|title='Title']]",
+ `<p><a href="`+imgurl+`" rel="nofollow"><img src="`+imgurl+`" alt="AltName" title="Title"/></a></p>`,
+ `<p><a href="`+imgurlWiki+`" rel="nofollow"><img src="`+imgurlWiki+`" alt="AltName" title="Title"/></a></p>`)
+ test(
+ "[[Link]] [[OtherLink]]",
+ `<p><a href="`+url+`" rel="nofollow">Link</a> <a href="`+otherUrl+`" rel="nofollow">OtherLink</a></p>`,
+ `<p><a href="`+urlWiki+`" rel="nofollow">Link</a> <a href="`+otherUrlWiki+`" rel="nofollow">OtherLink</a></p>`)
+}
+
+func TestMisc_IsMarkdownFile(t *testing.T) {
+ setting.Markdown.FileExtensions = []string{".md", ".markdown", ".mdown", ".mkd"}
+ trueTestCases := []string{
+ "test.md",
+ "wow.MARKDOWN",
+ "LOL.mDoWn",
+ }
+ falseTestCases := []string{
+ "test",
+ "abcdefg",
+ "abcdefghijklmnopqrstuvwxyz",
+ "test.md.test",
+ }
+
+ for _, testCase := range trueTestCases {
+ assert.True(t, IsMarkdownFile(testCase))
+ }
+ for _, testCase := range falseTestCases {
+ assert.False(t, IsMarkdownFile(testCase))
+ }
+}
+
+func TestRender_Images(t *testing.T) {
+ setting.AppURL = AppURL
+ setting.AppSubURL = AppSubURL
+
+ test := func(input, expected string) {
+ buffer := RenderString(input, setting.AppSubURL, nil)
+ assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(string(buffer)))
+ }
+
+ url := "../../.images/src/02/train.jpg"
+ title := "Train"
+ result := markup.URLJoin(AppSubURL, url)
+
+ test(
+ "!["+title+"]("+url+")",
+ `<p><a href="`+result+`" rel="nofollow"><img src="`+result+`" alt="`+title+`"></a></p>`)
+
+ test(
+ "[["+title+"|"+url+"]]",
+ `<p><a href="`+result+`" rel="nofollow"><img src="`+result+`" alt="`+title+`" title="`+title+`"/></a></p>`)
+}
+
+func TestRegExp_ShortLinkPattern(t *testing.T) {
+ trueTestCases := []string{
+ "[[stuff]]",
+ "[[]]",
+ "[[stuff|title=Difficult name with spaces*!]]",
+ }
+ falseTestCases := []string{
+ "test",
+ "abcdefg",
+ "[[]",
+ "[[",
+ "[]",
+ "]]",
+ "abcdefghijklmnopqrstuvwxyz",
+ }
+
+ for _, testCase := range trueTestCases {
+ assert.True(t, markup.ShortLinkPattern.MatchString(testCase))
+ }
+ for _, testCase := range falseTestCases {
+ assert.False(t, markup.ShortLinkPattern.MatchString(testCase))
+ }
+}
+
+func testAnswers(baseURLContent, baseURLImages string) []string {
+ return []string{
+ `<p>Wiki! Enjoy :)</p>
+
+<ul>
+<li><a href="` + baseURLContent + `/Links" rel="nofollow">Links, Language bindings, Engine bindings</a></li>
+<li><a href="` + baseURLContent + `/Tips" rel="nofollow">Tips</a></li>
+</ul>
+
+<p>Ideas and codes</p>
+
+<ul>
+<li>Bezier widget (by <a href="` + AppURL + `r-lyeh" rel="nofollow">@r-lyeh</a>) <a href="http://localhost:3000/ocornut/imgui/issues/786" rel="nofollow">#786</a></li>
+<li>Node graph editors https://github.com/ocornut/imgui/issues/306</li>
+<li><a href="` + baseURLContent + `/memory_editor_example" rel="nofollow">Memory Editor</a></li>
+<li><a href="` + baseURLContent + `/plot_var_example" rel="nofollow">Plot var helper</a></li>
+</ul>
+`,
+ `<h2>What is Wine Staging?</h2>
+
+<p><strong>Wine Staging</strong> on website <a href="http://wine-staging.com" rel="nofollow">wine-staging.com</a>.</p>
+
+<h2>Quick Links</h2>
+
+<p>Here are some links to the most important topics. You can find the full list of pages at the sidebar.</p>
+
+<table>
+<thead>
+<tr>
+<th><a href="` + baseURLImages + `/images/icon-install.png" rel="nofollow"><img src="` + baseURLImages + `/images/icon-install.png" alt="images/icon-install.png" title="icon-install.png"/></a></th>
+<th><a href="` + baseURLContent + `/Installation" rel="nofollow">Installation</a></th>
+</tr>
+</thead>
+
+<tbody>
+<tr>
+<td><a href="` + baseURLImages + `/images/icon-usage.png" rel="nofollow"><img src="` + baseURLImages + `/images/icon-usage.png" alt="images/icon-usage.png" title="icon-usage.png"/></a></td>
+<td><a href="` + baseURLContent + `/Usage" rel="nofollow">Usage</a></td>
+</tr>
+</tbody>
+</table>
+`,
+ `<p><a href="http://www.excelsiorjet.com/" rel="nofollow">Excelsior JET</a> allows you to create native executables for Windows, Linux and Mac OS X.</p>
+
+<ol>
+<li><a href="https://github.com/libgdx/libgdx/wiki/Gradle-on-the-Commandline#packaging-for-the-desktop" rel="nofollow">Package your libGDX application</a>
+<a href="` + baseURLImages + `/images/1.png" rel="nofollow"><img src="` + baseURLImages + `/images/1.png" alt="images/1.png" title="1.png"/></a></li>
+<li>Perform a test run by hitting the Run! button.
+<a href="` + baseURLImages + `/images/2.png" rel="nofollow"><img src="` + baseURLImages + `/images/2.png" alt="images/2.png" title="2.png"/></a></li>
+</ol>
+`,
+ }
+}
+
+// Test cases without ambiguous links
+var sameCases = []string{
+ // dear imgui wiki markdown extract: special wiki syntax
+ `Wiki! Enjoy :)
+- [[Links, Language bindings, Engine bindings|Links]]
+- [[Tips]]
+
+Ideas and codes
+
+- Bezier widget (by @r-lyeh) ` + AppURL + `ocornut/imgui/issues/786
+- Node graph editors https://github.com/ocornut/imgui/issues/306
+- [[Memory Editor|memory_editor_example]]
+- [[Plot var helper|plot_var_example]]`,
+ // wine-staging wiki home extract: tables, special wiki syntax, images
+ `## What is Wine Staging?
+**Wine Staging** on website [wine-staging.com](http://wine-staging.com).
+
+## Quick Links
+Here are some links to the most important topics. You can find the full list of pages at the sidebar.
+
+| [[images/icon-install.png]] | [[Installation]] |
+|--------------------------------|----------------------------------------------------------|
+| [[images/icon-usage.png]] | [[Usage]] |
+`,
+ // libgdx wiki page: inline images with special syntax
+ `[Excelsior JET](http://www.excelsiorjet.com/) allows you to create native executables for Windows, Linux and Mac OS X.
+
+1. [Package your libGDX application](https://github.com/libgdx/libgdx/wiki/Gradle-on-the-Commandline#packaging-for-the-desktop)
+[[images/1.png]]
+2. Perform a test run by hitting the Run! button.
+[[images/2.png]]`,
+}
+
+func TestTotal_RenderWiki(t *testing.T) {
+ answers := testAnswers(markup.URLJoin(AppSubURL, "wiki/"), markup.URLJoin(AppSubURL, "wiki", "raw/"))
+
+ for i := 0; i < len(sameCases); i++ {
+ line := RenderWiki([]byte(sameCases[i]), AppSubURL, nil)
+ assert.Equal(t, answers[i], line)
+ }
+
+ testCases := []string{
+ // Guard wiki sidebar: special syntax
+ `[[Guardfile-DSL / Configuring-Guard|Guardfile-DSL---Configuring-Guard]]`,
+ // rendered
+ `<p><a href="` + AppSubURL + `wiki/Guardfile-DSL---Configuring-Guard" rel="nofollow">Guardfile-DSL / Configuring-Guard</a></p>
+`,
+ // special syntax
+ `[[Name|Link]]`,
+ // rendered
+ `<p><a href="` + AppSubURL + `wiki/Link" rel="nofollow">Name</a></p>
+`,
+ }
+
+ for i := 0; i < len(testCases); i += 2 {
+ line := RenderWiki([]byte(testCases[i]), AppSubURL, nil)
+ assert.Equal(t, testCases[i+1], line)
+ }
+}
+
+func TestTotal_RenderString(t *testing.T) {
+ answers := testAnswers(markup.URLJoin(AppSubURL, "src", "master/"), markup.URLJoin(AppSubURL, "raw", "master/"))
+
+ for i := 0; i < len(sameCases); i++ {
+ line := RenderString(sameCases[i], markup.URLJoin(AppSubURL, "src", "master/"), nil)
+ assert.Equal(t, answers[i], line)
+ }
+
+ testCases := []string{}
+
+ for i := 0; i < len(testCases); i += 2 {
+ line := RenderString(testCases[i], AppSubURL, nil)
+ assert.Equal(t, testCases[i+1], line)
+ }
+}
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"
)
--- /dev/null
+// 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)
+}
--- /dev/null
+// 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>`)
+}
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"
)
"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"
"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"
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)
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()))
"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 (
{{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">
--- /dev/null
+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.
--- /dev/null
+#+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!
--- /dev/null
+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
+}
--- /dev/null
+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
+
+}
"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",