|  | @@ -0,0 +1,802 @@
 | 
	
		
			
				|  |  | +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')
 | 
	
		
			
				|  |  | +		output.Write(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
 | 
	
		
			
				|  |  | +	for i := 0; 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
 | 
	
		
			
				|  |  | +}
 |