package goorgeous import ( "bufio" "bytes" "regexp" "github.com/russross/blackfriday" "github.com/shurcooL/sanitized_anchor_name" ) type inlineParser func(p *parser, out *bytes.Buffer, data []byte, offset int) int type footnotes struct { id string def string } type parser struct { r blackfriday.Renderer inlineCallback [256]inlineParser notes []footnotes } // NewParser returns a new parser with the inlineCallbacks required for org content func NewParser(renderer blackfriday.Renderer) *parser { p := new(parser) p.r = renderer p.inlineCallback['='] = generateVerbatim p.inlineCallback['~'] = generateCode p.inlineCallback['/'] = generateEmphasis p.inlineCallback['_'] = generateUnderline p.inlineCallback['*'] = generateBold p.inlineCallback['+'] = generateStrikethrough p.inlineCallback['['] = generateLinkOrImg return p } // OrgCommon is the easiest way to parse a byte slice of org content and makes assumptions // that the caller wants to use blackfriday's HTMLRenderer with XHTML func OrgCommon(input []byte) []byte { renderer := blackfriday.HtmlRenderer(blackfriday.HTML_USE_XHTML, "", "") return OrgOptions(input, renderer) } // Org is a convenience name for OrgOptions func Org(input []byte, renderer blackfriday.Renderer) []byte { return OrgOptions(input, renderer) } // OrgOptions takes an org content byte slice and a renderer to use func OrgOptions(input []byte, renderer blackfriday.Renderer) []byte { // in the case that we need to render something in isEmpty but there isn't a new line char input = append(input, '\n') var output bytes.Buffer p := NewParser(renderer) scanner := bufio.NewScanner(bytes.NewReader(input)) // used to capture code blocks marker := "" syntax := "" listType := "" inParagraph := false inList := false inTable := false inFixedWidthArea := false var tmpBlock bytes.Buffer for scanner.Scan() { data := scanner.Bytes() if !isEmpty(data) && isComment(data) || IsKeyword(data) { switch { case inList: if tmpBlock.Len() > 0 { p.generateList(&output, tmpBlock.Bytes(), listType) } inList = false listType = "" tmpBlock.Reset() case inTable: if tmpBlock.Len() > 0 { p.generateTable(&output, tmpBlock.Bytes()) } inTable = false tmpBlock.Reset() case inParagraph: if tmpBlock.Len() > 0 { p.generateParagraph(&output, tmpBlock.Bytes()[:len(tmpBlock.Bytes())-1]) } inParagraph = false tmpBlock.Reset() case inFixedWidthArea: if tmpBlock.Len() > 0 { tmpBlock.WriteString("\n") output.Write(tmpBlock.Bytes()) } inFixedWidthArea = false tmpBlock.Reset() } } switch { case isEmpty(data): switch { case inList: if tmpBlock.Len() > 0 { p.generateList(&output, tmpBlock.Bytes(), listType) } inList = false listType = "" tmpBlock.Reset() case inTable: if tmpBlock.Len() > 0 { p.generateTable(&output, tmpBlock.Bytes()) } inTable = false tmpBlock.Reset() case inParagraph: if tmpBlock.Len() > 0 { p.generateParagraph(&output, tmpBlock.Bytes()[:len(tmpBlock.Bytes())-1]) } inParagraph = false tmpBlock.Reset() case inFixedWidthArea: if tmpBlock.Len() > 0 { tmpBlock.WriteString("\n") output.Write(tmpBlock.Bytes()) } inFixedWidthArea = false tmpBlock.Reset() case marker != "": tmpBlock.WriteByte('\n') default: continue } case isPropertyDrawer(data) || marker == "PROPERTIES": if marker == "" { marker = "PROPERTIES" } if bytes.Equal(data, []byte(":END:")) { marker = "" } continue case isBlock(data) || marker != "": matches := reBlock.FindSubmatch(data) if len(matches) > 0 { if string(matches[1]) == "END" { switch marker { case "QUOTE": var tmpBuf bytes.Buffer p.inline(&tmpBuf, tmpBlock.Bytes()) p.r.BlockQuote(&output, tmpBuf.Bytes()) case "CENTER": var tmpBuf bytes.Buffer output.WriteString("
\n")) p.inline(&tmpBuf, data) tmpBuf.WriteByte('\n') tmpBuf.Write([]byte("
\n")) tmpBlock.Write(tmpBuf.Bytes()) } else { tmpBlock.WriteByte('\n') tmpBlock.Write(data) } } else { marker = string(matches[2]) syntax = string(matches[3]) } case isFootnoteDef(data): matches := reFootnoteDef.FindSubmatch(data) for i := range p.notes { if p.notes[i].id == string(matches[1]) { p.notes[i].def = string(matches[2]) } } case isTable(data): if inTable != true { inTable = true } tmpBlock.Write(data) tmpBlock.WriteByte('\n') case IsKeyword(data): continue case isComment(data): p.generateComment(&output, data) case isHeadline(data): p.generateHeadline(&output, data) case isDefinitionList(data): if inList != true { listType = "dl" inList = true } var work bytes.Buffer flags := blackfriday.LIST_TYPE_DEFINITION matches := reDefinitionList.FindSubmatch(data) flags |= blackfriday.LIST_TYPE_TERM p.inline(&work, matches[1]) p.r.ListItem(&tmpBlock, work.Bytes(), flags) work.Reset() flags &= ^blackfriday.LIST_TYPE_TERM p.inline(&work, matches[2]) p.r.ListItem(&tmpBlock, work.Bytes(), flags) case isUnorderedList(data): if inList != true { listType = "ul" inList = true } matches := reUnorderedList.FindSubmatch(data) var work bytes.Buffer p.inline(&work, matches[2]) p.r.ListItem(&tmpBlock, work.Bytes(), 0) case isOrderedList(data): if inList != true { listType = "ol" inList = true } matches := reOrderedList.FindSubmatch(data) var work bytes.Buffer tmpBlock.WriteString("\n")
				inFixedWidthArea = true
			}
			matches := reExampleLine.FindSubmatch(data)
			tmpBlock.Write(matches[1])
			tmpBlock.WriteString("\n")
			break
		default:
			if inParagraph == false {
				inParagraph = true
				if inFixedWidthArea == true {
					if tmpBlock.Len() > 0 {
						tmpBlock.WriteString("")
						output.Write(tmpBlock.Bytes())
					}
					inFixedWidthArea = false
					tmpBlock.Reset()
				}
			}
			tmpBlock.Write(data)
			tmpBlock.WriteByte('\n')
		}
	}
	if len(tmpBlock.Bytes()) > 0 {
		if inParagraph == true {
			p.generateParagraph(&output, tmpBlock.Bytes()[:len(tmpBlock.Bytes())-1])
		} else if inFixedWidthArea == true {
			tmpBlock.WriteString("\n")
			output.Write(tmpBlock.Bytes())
		}
	}
	// Writing footnote def. list
	if len(p.notes) > 0 {
		flags := blackfriday.LIST_ITEM_BEGINNING_OF_LIST
		p.r.Footnotes(&output, func() bool {
			for i := range p.notes {
				p.r.FootnoteItem(&output, []byte(p.notes[i].id), []byte(p.notes[i].def), flags)
			}
			return true
		})
	}
	return output.Bytes()
}
// Org Syntax has been broken up into 4 distinct sections based on
// the org-syntax draft (http://orgmode.org/worg/dev/org-syntax.html):
// - Headlines
// - Greater Elements
// - Elements
// - Objects
// Headlines
func isHeadline(data []byte) bool {
	if !charMatches(data[0], '*') {
		return false
	}
	level := 0
	for level < 6 && charMatches(data[level], '*') {
		level++
	}
	return charMatches(data[level], ' ')
}
func (p *parser) generateHeadline(out *bytes.Buffer, data []byte) {
	level := 1
	status := ""
	priority := ""
	for level < 6 && data[level] == '*' {
		level++
	}
	start := skipChar(data, level, ' ')
	data = data[start:]
	i := 0
	// Check if has a status so it can be rendered as a separate span that can be hidden or
	// modified with CSS classes
	if hasStatus(data[i:4]) {
		status = string(data[i:4])
		i += 5 // one extra character for the next whitespace
	}
	// Check if the next byte is a priority marker
	if data[i] == '[' && hasPriority(data[i+1]) {
		priority = string(data[i+1])
		i += 4 // for "[c]" + ' '
	}
	tags, tagsFound := findTags(data, i)
	headlineID := sanitized_anchor_name.Create(string(data[i:]))
	generate := func() bool {
		dataEnd := len(data)
		if tagsFound > 0 {
			dataEnd = tagsFound
		}
		headline := bytes.TrimRight(data[i:dataEnd], " \t")
		if status != "" {
			out.WriteString("" + status + "")
			out.WriteByte(' ')
		}
		if priority != "" {
			out.WriteString("[" + priority + "]")
			out.WriteByte(' ')
		}
		p.inline(out, headline)
		if tagsFound > 0 {
			for _, tag := range tags {
				out.WriteByte(' ')
				out.WriteString("")
				out.WriteByte(' ')
			}
		}
		return true
	}
	p.r.Header(out, generate, level, headlineID)
}
func hasStatus(data []byte) bool {
	return bytes.Contains(data, []byte("TODO")) || bytes.Contains(data, []byte("DONE"))
}
func hasPriority(char byte) bool {
	return (charMatches(char, 'A') || charMatches(char, 'B') || charMatches(char, 'C'))
}
func findTags(data []byte, start int) ([]string, int) {
	tags := []string{}
	tagOpener := 0
	tagMarker := tagOpener
	for tIdx := start; tIdx < len(data); tIdx++ {
		if tagMarker > 0 && data[tIdx] == ':' {
			tags = append(tags, string(data[tagMarker+1:tIdx]))
			tagMarker = tIdx
		}
		if data[tIdx] == ':' && tagOpener == 0 && data[tIdx-1] == ' ' {
			tagMarker = tIdx
			tagOpener = tIdx
		}
	}
	return tags, tagOpener
}
// Greater Elements
// ~~ Definition Lists
var reDefinitionList = regexp.MustCompile(`^\s*-\s+(.+?)\s+::\s+(.*)`)
func isDefinitionList(data []byte) bool {
	return reDefinitionList.Match(data)
}
// ~~ Example lines
var reExampleLine = regexp.MustCompile(`^\s*:\s(\s*.*)|^\s*:$`)
func isExampleLine(data []byte) bool {
	return reExampleLine.Match(data)
}
// ~~ Ordered Lists
var reOrderedList = regexp.MustCompile(`^(\s*)\d+\.\s+\[?@?(\d*)\]?(.+)`)
func isOrderedList(data []byte) bool {
	return reOrderedList.Match(data)
}
// ~~ Unordered Lists
var reUnorderedList = regexp.MustCompile(`^(\s*)[-\+]\s+(.+)`)
func isUnorderedList(data []byte) bool {
	return reUnorderedList.Match(data)
}
// ~~ Tables
var reTableHeaders = regexp.MustCompile(`^[|+-]*$`)
func isTable(data []byte) bool {
	return charMatches(data[0], '|')
}
func (p *parser) generateTable(output *bytes.Buffer, data []byte) {
	var table bytes.Buffer
	rows := bytes.Split(bytes.Trim(data, "\n"), []byte("\n"))
	hasTableHeaders := len(rows) > 1
	if len(rows) > 1 {
		hasTableHeaders = reTableHeaders.Match(rows[1])
	}
	tbodySet := false
	for idx, row := range rows {
		var rowBuff bytes.Buffer
		if hasTableHeaders && idx == 0 {
			table.WriteString("")
			for _, cell := range bytes.Split(row[1:len(row)-1], []byte("|")) {
				p.r.TableHeaderCell(&rowBuff, bytes.Trim(cell, " \t"), 0)
			}
			p.r.TableRow(&table, rowBuff.Bytes())
			table.WriteString("\n")
		} else if hasTableHeaders && idx == 1 {
			continue
		} else {
			if !tbodySet {
				table.WriteString("")
				tbodySet = true
			}
			if !reTableHeaders.Match(row) {
				for _, cell := range bytes.Split(row[1:len(row)-1], []byte("|")) {
					var cellBuff bytes.Buffer
					p.inline(&cellBuff, bytes.Trim(cell, " \t"))
					p.r.TableCell(&rowBuff, cellBuff.Bytes(), 0)
				}
				p.r.TableRow(&table, rowBuff.Bytes())
			}
			if tbodySet && idx == len(rows)-1 {
				table.WriteString("\n")
				tbodySet = false
			}
		}
	}
	output.WriteString("\n