mirror of
https://github.com/osmarks/mycorrhiza.git
synced 2025-01-21 07:46:52 +00:00
Delete the markup module, depend on the library instead
The old module had been moved to the library, but I changed it a little, so now both projects are broken. I sure do love programming.
This commit is contained in:
parent
b4dd2dcfad
commit
da6ea25bb6
@ -20,8 +20,9 @@ import (
|
||||
|
||||
"github.com/bouncepaw/mycorrhiza/cfg"
|
||||
"github.com/bouncepaw/mycorrhiza/hyphae"
|
||||
"github.com/bouncepaw/mycorrhiza/markup"
|
||||
"github.com/bouncepaw/mycorrhiza/util"
|
||||
|
||||
"github.com/bouncepaw/mycomarkup/legacy"
|
||||
)
|
||||
|
||||
func geminiHomeHypha(w *gemini.ResponseWriter, rq *gemini.Request) {
|
||||
|
2
go.mod
2
go.mod
@ -5,7 +5,7 @@ go 1.14
|
||||
require (
|
||||
git.sr.ht/~adnano/go-gemini v0.1.13
|
||||
github.com/adrg/xdg v0.2.2
|
||||
github.com/bouncepaw/mycomarkup v0.0.0-20210511092446-956f169b1499
|
||||
github.com/bouncepaw/mycomarkup v0.0.0-20210512131946-becfae76473f
|
||||
github.com/go-ini/ini v1.62.0
|
||||
github.com/gorilla/feeds v1.1.1
|
||||
github.com/kr/pretty v0.2.1 // indirect
|
||||
|
3
go.sum
3
go.sum
@ -7,6 +7,8 @@ github.com/bouncepaw/mycomarkup v0.0.0-20210502065108-4ddae294864d h1:H20wX93QMe
|
||||
github.com/bouncepaw/mycomarkup v0.0.0-20210502065108-4ddae294864d/go.mod h1:fx0FBKaCaV1fofgCQOu2lNIVB/5EoDg/83i8BgBdgpc=
|
||||
github.com/bouncepaw/mycomarkup v0.0.0-20210511092446-956f169b1499 h1:RLKVj992QuayqjpIJ7M2csRjfhAnu9EO2r1Bm4D/WlU=
|
||||
github.com/bouncepaw/mycomarkup v0.0.0-20210511092446-956f169b1499/go.mod h1:fx0FBKaCaV1fofgCQOu2lNIVB/5EoDg/83i8BgBdgpc=
|
||||
github.com/bouncepaw/mycomarkup v0.0.0-20210512131946-becfae76473f h1:PgXG/AqO96jahMGQA81CWclIg90gsGt5kK4o7y0eZy8=
|
||||
github.com/bouncepaw/mycomarkup v0.0.0-20210512131946-becfae76473f/go.mod h1:4Fz80hsrXi3lRVZuVfIk5+2xocp50CFAbJfGeJWb5aM=
|
||||
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/go-ini/ini v1.62.0 h1:7VJT/ZXjzqSrvtraFp4ONq80hTcRQth1c9ZnQ3uNQvU=
|
||||
@ -62,6 +64,7 @@ golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210426230700-d19ff857e887/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210511113859-b0526f3d8744/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
|
34
markup/hr.go
34
markup/hr.go
@ -1,34 +0,0 @@
|
||||
package markup
|
||||
|
||||
import (
|
||||
"unicode"
|
||||
)
|
||||
|
||||
// MatchesHorizontalLine checks if the string can be interpreted as suitable for rendering as <hr/>.
|
||||
//
|
||||
// The rule is: if there are more than 4 characters "-" in the string, then make it a horizontal line.
|
||||
// Otherwise it is a paragraph (<p>).
|
||||
func MatchesHorizontalLine(line string) bool {
|
||||
counter := 0
|
||||
|
||||
// Check initially that the symbol is "-". If it is not a "-", it is most likely a space or another character.
|
||||
// With unicode.IsLetter() we can separate spaces and characters.
|
||||
for _, ch := range line {
|
||||
if ch == '-' {
|
||||
counter++
|
||||
continue
|
||||
}
|
||||
// If we bump into any other character (letter) in the line, it is immediately an incorrect horizontal line.
|
||||
// There is no point in counting further, we end the loop.
|
||||
if unicode.IsLetter(ch) {
|
||||
counter = 0
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if counter >= 4 {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
203
markup/img.go
203
markup/img.go
@ -1,203 +0,0 @@
|
||||
package markup
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/bouncepaw/mycomarkup/links"
|
||||
)
|
||||
|
||||
var imgRe = regexp.MustCompile(`^img\s+{`)
|
||||
|
||||
func MatchesImg(line string) bool {
|
||||
return imgRe.MatchString(line)
|
||||
}
|
||||
|
||||
type imgState int
|
||||
|
||||
const (
|
||||
inRoot imgState = iota
|
||||
inName
|
||||
inDimensionsW
|
||||
inDimensionsH
|
||||
inDescription
|
||||
)
|
||||
|
||||
type Img struct {
|
||||
entries []imgEntry
|
||||
currEntry imgEntry
|
||||
hyphaName string
|
||||
state imgState
|
||||
}
|
||||
|
||||
func (img *Img) pushEntry() {
|
||||
if strings.TrimSpace(img.currEntry.path.String()) != "" {
|
||||
img.currEntry.srclink = links.From(img.currEntry.path.String(), "", img.hyphaName)
|
||||
// img.currEntry.srclink.DoubtExistence()
|
||||
img.entries = append(img.entries, img.currEntry)
|
||||
img.currEntry = imgEntry{}
|
||||
img.currEntry.path.Reset()
|
||||
}
|
||||
}
|
||||
|
||||
func (img *Img) Process(line string) (shouldGoBackToNormal bool) {
|
||||
stateToProcessor := map[imgState]func(rune) bool{
|
||||
inRoot: img.processInRoot,
|
||||
inName: img.processInName,
|
||||
inDimensionsW: img.processInDimensionsW,
|
||||
inDimensionsH: img.processInDimensionsH,
|
||||
inDescription: img.processInDescription,
|
||||
}
|
||||
for _, r := range line {
|
||||
if shouldReturnTrue := stateToProcessor[img.state](r); shouldReturnTrue {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (img *Img) processInDescription(r rune) (shouldReturnTrue bool) {
|
||||
switch r {
|
||||
case '}':
|
||||
img.state = inName
|
||||
default:
|
||||
img.currEntry.desc.WriteRune(r)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (img *Img) processInRoot(r rune) (shouldReturnTrue bool) {
|
||||
switch r {
|
||||
case '}':
|
||||
img.pushEntry()
|
||||
return true
|
||||
case '\n', '\r':
|
||||
img.pushEntry()
|
||||
case ' ', '\t':
|
||||
default:
|
||||
img.state = inName
|
||||
img.currEntry = imgEntry{}
|
||||
img.currEntry.path.Reset()
|
||||
img.currEntry.path.WriteRune(r)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (img *Img) processInName(r rune) (shouldReturnTrue bool) {
|
||||
switch r {
|
||||
case '}':
|
||||
img.pushEntry()
|
||||
return true
|
||||
case '|':
|
||||
img.state = inDimensionsW
|
||||
case '{':
|
||||
img.state = inDescription
|
||||
case '\n', '\r':
|
||||
img.pushEntry()
|
||||
img.state = inRoot
|
||||
default:
|
||||
img.currEntry.path.WriteRune(r)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (img *Img) processInDimensionsW(r rune) (shouldReturnTrue bool) {
|
||||
switch r {
|
||||
case '}':
|
||||
img.pushEntry()
|
||||
return true
|
||||
case '*':
|
||||
img.state = inDimensionsH
|
||||
case ' ', '\t', '\n':
|
||||
case '{':
|
||||
img.state = inDescription
|
||||
default:
|
||||
img.currEntry.sizeW.WriteRune(r)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (img *Img) processInDimensionsH(r rune) (shouldGoBackToNormal bool) {
|
||||
switch r {
|
||||
case '}':
|
||||
img.pushEntry()
|
||||
return true
|
||||
case ' ', '\t', '\n':
|
||||
case '{':
|
||||
img.state = inDescription
|
||||
default:
|
||||
img.currEntry.sizeH.WriteRune(r)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func ImgFromFirstLine(line, hyphaName string) (img *Img, shouldGoBackToNormal bool) {
|
||||
img = &Img{
|
||||
hyphaName: hyphaName,
|
||||
entries: make([]imgEntry, 0),
|
||||
}
|
||||
line = line[strings.IndexRune(line, '{')+1:]
|
||||
return img, img.Process(line)
|
||||
}
|
||||
|
||||
func (img *Img) pagePathFor(path string) string {
|
||||
path = strings.TrimSpace(path)
|
||||
if strings.IndexRune(path, ':') != -1 || strings.IndexRune(path, '/') == 0 {
|
||||
return path
|
||||
} else {
|
||||
return "/page/" + xclCanonicalName(img.hyphaName, path)
|
||||
}
|
||||
}
|
||||
|
||||
func parseDimensions(dimensions string) (sizeW, sizeH string) {
|
||||
xIndex := strings.IndexRune(dimensions, '*')
|
||||
if xIndex == -1 { // If no x in dimensions
|
||||
sizeW = strings.TrimSpace(dimensions)
|
||||
} else {
|
||||
sizeW = strings.TrimSpace(dimensions[:xIndex])
|
||||
sizeH = strings.TrimSpace(strings.TrimPrefix(dimensions, dimensions[:xIndex+1]))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (img *Img) markExistenceOfSrcLinks() {
|
||||
HyphaIterate(func(hn string) {
|
||||
for _, entry := range img.entries {
|
||||
if hn == entry.srclink.Address() {
|
||||
entry.srclink.DestinationUnknown = false
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (img *Img) ToHtml() (html string) {
|
||||
img.markExistenceOfSrcLinks()
|
||||
isOneImageOnly := len(img.entries) == 1 && img.entries[0].desc.Len() == 0
|
||||
if isOneImageOnly {
|
||||
html += `<section class="img-gallery img-gallery_one-image">`
|
||||
} else {
|
||||
html += `<section class="img-gallery img-gallery_many-images">`
|
||||
}
|
||||
|
||||
for _, entry := range img.entries {
|
||||
html += `<figure>`
|
||||
if entry.srclink.DestinationUnknown {
|
||||
html += fmt.Sprintf(
|
||||
`<a class="%s" href="%s">Hypha <i>%s</i> does not exist</a>`,
|
||||
entry.srclink.Classes(),
|
||||
entry.srclink.Href(),
|
||||
entry.srclink.Address)
|
||||
} else {
|
||||
html += fmt.Sprintf(
|
||||
`<a href="%s"><img src="%s" %s %s></a>`,
|
||||
entry.srclink.Href(),
|
||||
entry.srclink.ImgSrc(),
|
||||
entry.sizeWAsAttr(),
|
||||
entry.sizeHAsAttr())
|
||||
}
|
||||
html += entry.descriptionAsHtml(img.hyphaName)
|
||||
html += `</figure>`
|
||||
}
|
||||
return html + `</section>`
|
||||
}
|
@ -1,45 +0,0 @@
|
||||
package markup
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/bouncepaw/mycomarkup/links"
|
||||
)
|
||||
|
||||
type imgEntry struct {
|
||||
srclink *links.Link
|
||||
path strings.Builder
|
||||
sizeW strings.Builder
|
||||
sizeH strings.Builder
|
||||
desc strings.Builder
|
||||
}
|
||||
|
||||
func (entry *imgEntry) descriptionAsHtml(hyphaName string) (html string) {
|
||||
if entry.desc.Len() == 0 {
|
||||
return ""
|
||||
}
|
||||
lines := strings.Split(entry.desc.String(), "\n")
|
||||
for _, line := range lines {
|
||||
if line = strings.TrimSpace(line); line != "" {
|
||||
if html != "" {
|
||||
html += `<br>`
|
||||
}
|
||||
html += ParagraphToHtml(hyphaName, line)
|
||||
}
|
||||
}
|
||||
return `<figcaption>` + html + `</figcaption>`
|
||||
}
|
||||
|
||||
func (entry *imgEntry) sizeWAsAttr() string {
|
||||
if entry.sizeW.Len() == 0 {
|
||||
return ""
|
||||
}
|
||||
return ` width="` + entry.sizeW.String() + `"`
|
||||
}
|
||||
|
||||
func (entry *imgEntry) sizeHAsAttr() string {
|
||||
if entry.sizeH.Len() == 0 {
|
||||
return ""
|
||||
}
|
||||
return ` height="` + entry.sizeH.String() + `"`
|
||||
}
|
229
markup/lexer.go
229
markup/lexer.go
@ -1,229 +0,0 @@
|
||||
package markup
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html"
|
||||
"strings"
|
||||
|
||||
"github.com/bouncepaw/mycorrhiza/util"
|
||||
)
|
||||
|
||||
// HyphaExists holds function that checks that a hypha is present.
|
||||
var HyphaExists func(string) bool
|
||||
|
||||
//
|
||||
var HyphaImageForOG func(string) string
|
||||
|
||||
// HyphaAccess holds function that accesses a hypha by its name.
|
||||
var HyphaAccess func(string) (rawText, binaryHtml string, err error)
|
||||
|
||||
// HyphaIterate is a function that iterates all hypha names existing.
|
||||
var HyphaIterate func(func(string))
|
||||
|
||||
// GemLexerState is used by markup parser to remember what is going on.
|
||||
type GemLexerState struct {
|
||||
// Name of hypha being parsed
|
||||
name string
|
||||
where string // "", "list", "pre"
|
||||
// Line id
|
||||
id int
|
||||
buf string
|
||||
// Temporaries
|
||||
img *Img
|
||||
table *Table
|
||||
list *List
|
||||
}
|
||||
|
||||
type Line struct {
|
||||
id int
|
||||
// interface{} may be bad. TODO: a proper type
|
||||
contents interface{}
|
||||
}
|
||||
|
||||
func (md *MycoDoc) lex() (ast []Line) {
|
||||
var state = GemLexerState{name: md.hyphaName}
|
||||
|
||||
for _, line := range append(strings.Split(md.contents, "\n"), "") {
|
||||
lineToAST(line, &state, &ast)
|
||||
}
|
||||
return ast
|
||||
}
|
||||
|
||||
// Lex `line` in markup and save it to `ast` using `state`.
|
||||
func lineToAST(line string, state *GemLexerState, ast *[]Line) {
|
||||
addLine := func(text interface{}) {
|
||||
*ast = append(*ast, Line{id: state.id, contents: text})
|
||||
}
|
||||
addParagraphIfNeeded := func() {
|
||||
if state.where == "p" {
|
||||
state.where = ""
|
||||
addLine(fmt.Sprintf("<p id='%d'>%s</p>", state.id, strings.ReplaceAll(ParagraphToHtml(state.name, state.buf), "\n", "<br>")))
|
||||
state.buf = ""
|
||||
}
|
||||
}
|
||||
|
||||
// Process empty lines depending on the current state
|
||||
if "" == strings.TrimSpace(line) {
|
||||
switch state.where {
|
||||
case "pre":
|
||||
state.buf += "\n"
|
||||
case "launchpad":
|
||||
state.where = ""
|
||||
addLine(state.buf + "</ul>")
|
||||
case "p":
|
||||
addParagraphIfNeeded()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
startsWith := func(token string) bool {
|
||||
return strings.HasPrefix(line, token)
|
||||
}
|
||||
addHeading := func(i int) {
|
||||
id := util.LettersNumbersOnly(line[i+1:])
|
||||
addLine(fmt.Sprintf(`<h%d id='%d'>%s<a href="#%s" id="%s" class="heading__link"></a></h%d>`, i, state.id, ParagraphToHtml(state.name, line[i+1:]), id, id, i))
|
||||
}
|
||||
|
||||
// Beware! Usage of goto. Some may say it is considered evil but in this case it helped to make a better-structured code.
|
||||
switch state.where {
|
||||
case "img":
|
||||
goto imgState
|
||||
case "table":
|
||||
goto tableState
|
||||
case "list":
|
||||
goto listState
|
||||
case "pre":
|
||||
goto preformattedState
|
||||
case "launchpad":
|
||||
goto launchpadState
|
||||
default: // "p" or ""
|
||||
goto normalState
|
||||
}
|
||||
|
||||
imgState:
|
||||
if shouldGoBackToNormal := state.img.Process(line); shouldGoBackToNormal {
|
||||
state.where = ""
|
||||
addLine(*state.img)
|
||||
}
|
||||
return
|
||||
|
||||
tableState:
|
||||
if shouldGoBackToNormal := state.table.Process(line); shouldGoBackToNormal {
|
||||
state.where = ""
|
||||
addLine(*state.table)
|
||||
}
|
||||
return
|
||||
|
||||
listState:
|
||||
if done := state.list.Parse(line); done {
|
||||
state.list.Finalize()
|
||||
state.where = ""
|
||||
goto normalState
|
||||
}
|
||||
return
|
||||
|
||||
preformattedState:
|
||||
switch {
|
||||
case startsWith("```"):
|
||||
state.where = ""
|
||||
state.buf = strings.TrimSuffix(state.buf, "\n")
|
||||
addLine(state.buf + "</code></pre>")
|
||||
state.buf = ""
|
||||
default:
|
||||
state.buf += html.EscapeString(line) + "\n"
|
||||
}
|
||||
return
|
||||
|
||||
launchpadState:
|
||||
switch {
|
||||
case startsWith("=>"):
|
||||
href, text, class := Rocketlink(line, state.name)
|
||||
state.buf += fmt.Sprintf(` <li class="launchpad__entry"><a href="%s" class="rocketlink %s">%s</a></li>`, href, class, text)
|
||||
case startsWith("```"):
|
||||
state.where = "pre"
|
||||
addLine(state.buf + "</ul>")
|
||||
state.id++
|
||||
state.buf = fmt.Sprintf("<pre id='%d' alt='%s' class='codeblock'><code>", state.id, strings.TrimPrefix(line, "```"))
|
||||
default:
|
||||
state.where = ""
|
||||
addLine(state.buf + "</ul>")
|
||||
goto normalState
|
||||
}
|
||||
return
|
||||
|
||||
normalState:
|
||||
state.id++
|
||||
switch {
|
||||
|
||||
case startsWith("```"):
|
||||
addParagraphIfNeeded()
|
||||
state.where = "pre"
|
||||
state.buf = fmt.Sprintf("<pre id='%d' alt='%s' class='codeblock'><code>", state.id, strings.TrimPrefix(line, "```"))
|
||||
|
||||
case startsWith("###### "):
|
||||
addParagraphIfNeeded()
|
||||
addHeading(6)
|
||||
case startsWith("##### "):
|
||||
addParagraphIfNeeded()
|
||||
addHeading(5)
|
||||
case startsWith("#### "):
|
||||
addParagraphIfNeeded()
|
||||
addHeading(4)
|
||||
case startsWith("### "):
|
||||
addParagraphIfNeeded()
|
||||
addHeading(3)
|
||||
case startsWith("## "):
|
||||
addParagraphIfNeeded()
|
||||
addHeading(2)
|
||||
case startsWith("# "):
|
||||
addParagraphIfNeeded()
|
||||
addHeading(1)
|
||||
|
||||
case startsWith(">"):
|
||||
addParagraphIfNeeded()
|
||||
addLine(
|
||||
fmt.Sprintf(
|
||||
"<blockquote id='%d'>%s</blockquote>",
|
||||
state.id,
|
||||
ParagraphToHtml(state.name, remover(">")(line)),
|
||||
),
|
||||
)
|
||||
case startsWith("=>"):
|
||||
addParagraphIfNeeded()
|
||||
state.where = "launchpad"
|
||||
state.buf = fmt.Sprintf("<ul class='launchpad' id='%d'>\n", state.id)
|
||||
goto launchpadState
|
||||
|
||||
case startsWith("<="):
|
||||
addParagraphIfNeeded()
|
||||
addLine(parseTransclusion(line, state.name))
|
||||
case MatchesHorizontalLine(line):
|
||||
addParagraphIfNeeded()
|
||||
*ast = append(*ast, Line{id: -1, contents: "<hr/>"})
|
||||
case MatchesList(line):
|
||||
addParagraphIfNeeded()
|
||||
list, _ := NewList(line, state.name)
|
||||
state.where = "list"
|
||||
state.list = list
|
||||
addLine(state.list)
|
||||
case MatchesImg(line):
|
||||
addParagraphIfNeeded()
|
||||
img, shouldGoBackToNormal := ImgFromFirstLine(line, state.name)
|
||||
if shouldGoBackToNormal {
|
||||
addLine(*img)
|
||||
} else {
|
||||
state.where = "img"
|
||||
state.img = img
|
||||
}
|
||||
case MatchesTable(line):
|
||||
addParagraphIfNeeded()
|
||||
state.where = "table"
|
||||
state.table = TableFromFirstLine(line, state.name)
|
||||
|
||||
case state.where == "p":
|
||||
state.buf += "\n" + line
|
||||
default:
|
||||
state.where = "p"
|
||||
state.buf = line
|
||||
}
|
||||
}
|
@ -1,33 +0,0 @@
|
||||
package markup
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/bouncepaw/mycomarkup/links"
|
||||
)
|
||||
|
||||
// LinkParts determines what href, text and class should resulting <a> have based on mycomarkup's addr, display and hypha name.
|
||||
//
|
||||
// => addr display
|
||||
// [[addr|display]]
|
||||
// TODO: deprecate
|
||||
func LinkParts(addr, display, hyphaName string) (href, text, class string) {
|
||||
l := links.From(addr, display, hyphaName)
|
||||
if l.OfKind(links.LinkLocalHypha) && !HyphaExists(l.Address()) {
|
||||
l.DestinationUnknown = true
|
||||
}
|
||||
return l.Href(), l.Display(), l.Classes()
|
||||
}
|
||||
|
||||
// Parse markup line starting with "=>" according to wikilink rules.
|
||||
// See http://localhost:1737/page/wikilink
|
||||
func Rocketlink(src, hyphaName string) (href, text, class string) {
|
||||
src = strings.TrimSpace(src[2:]) // Drop =>
|
||||
if src == "" {
|
||||
return
|
||||
}
|
||||
// Href is text after => till first whitespace
|
||||
addr := strings.Fields(src)[0]
|
||||
display := strings.TrimPrefix(src, addr)
|
||||
return LinkParts(addr, display, hyphaName)
|
||||
}
|
171
markup/list.go
171
markup/list.go
@ -1,171 +0,0 @@
|
||||
package markup
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func parseListItem(line string) (level int, offset int, ordered bool, err error) {
|
||||
for line[level] == '*' {
|
||||
level++
|
||||
}
|
||||
|
||||
if line[level] == '.' {
|
||||
ordered = true
|
||||
offset = level + 2
|
||||
} else {
|
||||
ordered = false
|
||||
offset = level + 1
|
||||
}
|
||||
|
||||
if line[offset-1] != ' ' || len(line) < offset+2 || level < 1 || level > 6 {
|
||||
err = errors.New("ill-formatted list item")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func MatchesList(line string) bool {
|
||||
level, _, _, err := parseListItem(line)
|
||||
return err == nil && level == 1
|
||||
}
|
||||
|
||||
type listItem struct {
|
||||
content string
|
||||
parent *listItem
|
||||
children []*listItem
|
||||
depth int
|
||||
}
|
||||
|
||||
func newListItem(parent *listItem) *listItem {
|
||||
depth := 0
|
||||
if parent != nil {
|
||||
depth = parent.depth + 1
|
||||
}
|
||||
return &listItem{
|
||||
parent: parent,
|
||||
children: make([]*listItem, 0),
|
||||
depth: depth,
|
||||
}
|
||||
}
|
||||
|
||||
func (item *listItem) renderAsHtmlTo(b *strings.Builder, hyphaName string, ordered bool) {
|
||||
if len(item.content) > 0 {
|
||||
b.WriteString("<li>")
|
||||
b.WriteString(ParagraphToHtml(hyphaName, item.content))
|
||||
}
|
||||
|
||||
if len(item.children) > 0 {
|
||||
if ordered {
|
||||
b.WriteString("<ol>")
|
||||
} else {
|
||||
b.WriteString("<ul>")
|
||||
}
|
||||
|
||||
for _, child := range item.children {
|
||||
child.renderAsHtmlTo(b, hyphaName, ordered)
|
||||
}
|
||||
|
||||
if ordered {
|
||||
b.WriteString("</ol>")
|
||||
} else {
|
||||
b.WriteString("</ul>")
|
||||
}
|
||||
}
|
||||
|
||||
if len(item.content) > 0 {
|
||||
b.WriteString("</li>")
|
||||
}
|
||||
}
|
||||
|
||||
// A structure representing ordered and unordered lists in the AST.
|
||||
type List struct {
|
||||
curr *listItem
|
||||
hyphaName string
|
||||
ordered bool
|
||||
finalized bool
|
||||
}
|
||||
|
||||
func NewList(line, hyphaName string) (*List, bool) {
|
||||
list := &List{
|
||||
hyphaName: hyphaName,
|
||||
curr: newListItem(nil),
|
||||
}
|
||||
return list, list.Parse(line)
|
||||
}
|
||||
|
||||
func (list *List) pushItem() {
|
||||
item := newListItem(list.curr)
|
||||
list.curr.children = append(list.curr.children, item)
|
||||
list.curr = item
|
||||
}
|
||||
|
||||
func (list *List) popItem() {
|
||||
if list.curr == nil {
|
||||
return
|
||||
}
|
||||
list.curr = list.curr.parent
|
||||
}
|
||||
|
||||
func (list *List) balance(level int) {
|
||||
for level > list.curr.depth {
|
||||
list.pushItem()
|
||||
}
|
||||
|
||||
for level < list.curr.depth {
|
||||
list.popItem()
|
||||
}
|
||||
}
|
||||
|
||||
func (list *List) Parse(line string) (done bool) {
|
||||
level, offset, ordered, err := parseListItem(line)
|
||||
if err != nil {
|
||||
list.Finalize()
|
||||
return true
|
||||
}
|
||||
|
||||
// update ordered flag if the current node is the root one
|
||||
// (i.e. no parsing has been done yet)
|
||||
if list.curr.parent == nil {
|
||||
list.ordered = ordered
|
||||
}
|
||||
|
||||
// if list type has suddenly changed (ill-formatted list), quit
|
||||
if ordered != list.ordered {
|
||||
list.Finalize()
|
||||
return true
|
||||
}
|
||||
|
||||
list.balance(level)
|
||||
|
||||
// if the current node already has content, create a new one
|
||||
// to prevent overwriting existing content (effectively creating
|
||||
// a new sibling node)
|
||||
if len(list.curr.content) > 0 {
|
||||
list.popItem()
|
||||
list.pushItem()
|
||||
}
|
||||
|
||||
list.curr.content = line[offset:]
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (list *List) Finalize() {
|
||||
if !list.finalized {
|
||||
// close all opened nodes, effectively going up to the root node
|
||||
list.balance(0)
|
||||
list.finalized = true
|
||||
}
|
||||
}
|
||||
|
||||
func (list *List) RenderAsHtml() (html string) {
|
||||
// for a good measure
|
||||
list.Finalize()
|
||||
|
||||
b := &strings.Builder{}
|
||||
|
||||
// fire up recursive render process
|
||||
list.curr.renderAsHtmlTo(b, list.hyphaName, list.ordered)
|
||||
|
||||
return b.String()
|
||||
}
|
@ -1,98 +0,0 @@
|
||||
// This is not done yet
|
||||
package markup
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/bouncepaw/mycorrhiza/cfg"
|
||||
|
||||
"github.com/bouncepaw/mycomarkup/links"
|
||||
)
|
||||
|
||||
// A Mycomarkup-formatted document
|
||||
type MycoDoc struct {
|
||||
// data
|
||||
hyphaName string
|
||||
contents string
|
||||
// indicators
|
||||
parsedAlready bool
|
||||
// results
|
||||
ast []Line
|
||||
html string
|
||||
firstImageURL string
|
||||
description string
|
||||
}
|
||||
|
||||
// Constructor
|
||||
func Doc(hyphaName, contents string) *MycoDoc {
|
||||
md := &MycoDoc{
|
||||
hyphaName: hyphaName,
|
||||
contents: contents,
|
||||
}
|
||||
return md
|
||||
}
|
||||
|
||||
func (md *MycoDoc) Lex(recursionLevel int) *MycoDoc {
|
||||
if !md.parsedAlready {
|
||||
md.ast = md.lex()
|
||||
}
|
||||
md.parsedAlready = true
|
||||
return md
|
||||
}
|
||||
|
||||
// AsHTML returns an html representation of the document
|
||||
func (md *MycoDoc) AsHTML() string {
|
||||
md.html = Parse(md.Lex(0).ast, 0, 0, 0)
|
||||
return md.html
|
||||
}
|
||||
|
||||
// AsGemtext returns a gemtext representation of the document. Currently really limited, just returns source text
|
||||
func (md *MycoDoc) AsGemtext() string {
|
||||
return md.contents
|
||||
}
|
||||
|
||||
// Used to clear opengraph description from html tags. This method is usually bad because of dangers of malformed HTML, but I'm going to use it only for Mycorrhiza-generated HTML, so it's okay. The question mark is required; without it the whole string is eaten away.
|
||||
var htmlTagRe = regexp.MustCompile(`<.*?>`)
|
||||
|
||||
// OpenGraphHTML returns an html representation of og: meta tags.
|
||||
func (md *MycoDoc) OpenGraphHTML() string {
|
||||
md.ogFillVars()
|
||||
return strings.Join([]string{
|
||||
ogTag("title", md.hyphaName),
|
||||
ogTag("type", "article"),
|
||||
ogTag("image", md.firstImageURL),
|
||||
ogTag("url", cfg.URL+"/hypha/"+md.hyphaName),
|
||||
ogTag("determiner", ""),
|
||||
ogTag("description", htmlTagRe.ReplaceAllString(md.description, "")),
|
||||
}, "\n")
|
||||
}
|
||||
|
||||
func (md *MycoDoc) ogFillVars() *MycoDoc {
|
||||
md.firstImageURL = cfg.URL + "/favicon.ico"
|
||||
foundDesc := false
|
||||
foundImg := false
|
||||
for _, line := range md.ast {
|
||||
switch v := line.contents.(type) {
|
||||
case string:
|
||||
if !foundDesc {
|
||||
md.description = v
|
||||
foundDesc = true
|
||||
}
|
||||
case Img:
|
||||
if !foundImg && len(v.entries) > 0 {
|
||||
md.firstImageURL = v.entries[0].srclink.ImgSrc()
|
||||
if !v.entries[0].srclink.OfKind(links.LinkExternal) {
|
||||
md.firstImageURL = cfg.URL + md.firstImageURL
|
||||
}
|
||||
foundImg = true
|
||||
}
|
||||
}
|
||||
}
|
||||
return md
|
||||
}
|
||||
|
||||
func ogTag(property, content string) string {
|
||||
return fmt.Sprintf(`<meta property="og:%s" content="%s"/>`, property, content)
|
||||
}
|
@ -1,57 +0,0 @@
|
||||
package markup
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/bouncepaw/mycomarkup/links"
|
||||
)
|
||||
|
||||
// OutLinks returns a channel of names of hyphae this mycodocument links.
|
||||
// Links include:
|
||||
// * Regular links
|
||||
// * Rocketlinks
|
||||
// * Transclusion
|
||||
// * Image galleries
|
||||
// Not needed anymore, I guess.
|
||||
func (md *MycoDoc) OutLinks() chan string {
|
||||
ch := make(chan string)
|
||||
if !md.parsedAlready {
|
||||
md.Lex(0)
|
||||
}
|
||||
go func() {
|
||||
for _, line := range md.ast {
|
||||
switch v := line.contents.(type) {
|
||||
case string:
|
||||
if strings.HasPrefix(v, "<p") || strings.HasPrefix(v, "<ul class='launchpad'") {
|
||||
extractLinks(v, ch)
|
||||
}
|
||||
case Transclusion:
|
||||
ch <- v.name
|
||||
case Img:
|
||||
extractImageLinks(v, ch)
|
||||
}
|
||||
}
|
||||
close(ch)
|
||||
}()
|
||||
return ch
|
||||
}
|
||||
|
||||
var reLinks = regexp.MustCompile(`<a href="/hypha/([^"]*)".*?</a>`)
|
||||
|
||||
func extractLinks(html string, ch chan string) {
|
||||
if results := reLinks.FindAllStringSubmatch(html, -1); results != nil {
|
||||
for _, result := range results {
|
||||
// result[0] is always present at this point and is not needed, because it is the whole matched substring (which we don't need)
|
||||
ch <- result[1]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func extractImageLinks(img Img, ch chan string) {
|
||||
for _, entry := range img.entries {
|
||||
if entry.srclink.OfKind(links.LinkLocalHypha) {
|
||||
ch <- entry.srclink.Address()
|
||||
}
|
||||
}
|
||||
}
|
@ -1,185 +0,0 @@
|
||||
package markup
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"html"
|
||||
"strings"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
type spanTokenType int
|
||||
|
||||
const (
|
||||
spanTextNode = iota
|
||||
spanItalic
|
||||
spanBold
|
||||
spanMono
|
||||
spanSuper
|
||||
spanSub
|
||||
spanMark
|
||||
spanStrike
|
||||
spanLink
|
||||
)
|
||||
|
||||
func tagFromState(stt spanTokenType, tagState map[spanTokenType]bool, tagName, originalForm string) string {
|
||||
if tagState[spanMono] && (stt != spanMono) {
|
||||
return originalForm
|
||||
}
|
||||
if tagState[stt] {
|
||||
tagState[stt] = false
|
||||
return fmt.Sprintf("</%s>", tagName)
|
||||
} else {
|
||||
tagState[stt] = true
|
||||
return fmt.Sprintf("<%s>", tagName)
|
||||
}
|
||||
}
|
||||
|
||||
func getLinkNode(input *bytes.Buffer, hyphaName string, isBracketedLink bool) string {
|
||||
if isBracketedLink {
|
||||
input.Next(2) // drop those [[
|
||||
}
|
||||
var (
|
||||
escaping = false
|
||||
addrBuf = bytes.Buffer{}
|
||||
displayBuf = bytes.Buffer{}
|
||||
currBuf = &addrBuf
|
||||
)
|
||||
for input.Len() != 0 {
|
||||
b, _ := input.ReadByte()
|
||||
if escaping {
|
||||
currBuf.WriteByte(b)
|
||||
escaping = false
|
||||
} else if isBracketedLink && b == '|' && currBuf == &addrBuf {
|
||||
currBuf = &displayBuf
|
||||
} else if isBracketedLink && b == ']' && bytes.HasPrefix(input.Bytes(), []byte{']'}) {
|
||||
input.Next(1)
|
||||
break
|
||||
} else if !isBracketedLink && (unicode.IsSpace(rune(b)) || strings.ContainsRune("<>{}|\\^[]`,()", rune(b))) {
|
||||
input.UnreadByte()
|
||||
break
|
||||
} else {
|
||||
currBuf.WriteByte(b)
|
||||
}
|
||||
}
|
||||
href, text, class := LinkParts(addrBuf.String(), displayBuf.String(), hyphaName)
|
||||
return fmt.Sprintf(`<a href="%s" class="%s">%s</a>`, href, class, html.EscapeString(text))
|
||||
}
|
||||
|
||||
// getTextNode splits the `input` into two parts `textNode` and `rest` by the first encountered rune that resembles a span tag. If there is none, `textNode = input`, `rest = ""`. It handles escaping with backslash.
|
||||
func getTextNode(input *bytes.Buffer) string {
|
||||
var (
|
||||
textNodeBuffer = bytes.Buffer{}
|
||||
escaping = false
|
||||
startsWith = func(t string) bool {
|
||||
return bytes.HasPrefix(input.Bytes(), []byte(t))
|
||||
}
|
||||
couldBeLinkStart = func() bool {
|
||||
return startsWith("https://") || startsWith("http://") || startsWith("gemini://") || startsWith("gopher://") || startsWith("ftp://")
|
||||
}
|
||||
)
|
||||
// Always read the first byte in advance to avoid endless loops that kill computers (sad experience)
|
||||
if input.Len() != 0 {
|
||||
b, _ := input.ReadByte()
|
||||
textNodeBuffer.WriteByte(b)
|
||||
}
|
||||
for input.Len() != 0 {
|
||||
// Assume no error is possible because we check for length
|
||||
b, _ := input.ReadByte()
|
||||
if escaping {
|
||||
textNodeBuffer.WriteByte(b)
|
||||
escaping = false
|
||||
} else if b == '\\' {
|
||||
escaping = true
|
||||
} else if strings.IndexByte("/*`^,![~", b) >= 0 {
|
||||
input.UnreadByte()
|
||||
break
|
||||
} else if couldBeLinkStart() {
|
||||
textNodeBuffer.WriteByte(b)
|
||||
break
|
||||
} else {
|
||||
textNodeBuffer.WriteByte(b)
|
||||
}
|
||||
}
|
||||
return textNodeBuffer.String()
|
||||
}
|
||||
|
||||
func ParagraphToHtml(hyphaName, input string) string {
|
||||
var (
|
||||
p = bytes.NewBufferString(input)
|
||||
ret strings.Builder
|
||||
// true = tag is opened, false = tag is not opened
|
||||
tagState = map[spanTokenType]bool{
|
||||
spanItalic: false,
|
||||
spanBold: false,
|
||||
spanMono: false,
|
||||
spanSuper: false,
|
||||
spanSub: false,
|
||||
spanMark: false,
|
||||
spanLink: false,
|
||||
}
|
||||
startsWith = func(t string) bool {
|
||||
return bytes.HasPrefix(p.Bytes(), []byte(t))
|
||||
}
|
||||
noTagsActive = func() bool {
|
||||
return !(tagState[spanItalic] || tagState[spanBold] || tagState[spanMono] || tagState[spanSuper] || tagState[spanSub] || tagState[spanMark] || tagState[spanLink])
|
||||
}
|
||||
)
|
||||
|
||||
for p.Len() != 0 {
|
||||
switch {
|
||||
case startsWith("//"):
|
||||
ret.WriteString(tagFromState(spanItalic, tagState, "em", "//"))
|
||||
p.Next(2)
|
||||
case startsWith("**"):
|
||||
ret.WriteString(tagFromState(spanBold, tagState, "strong", "**"))
|
||||
p.Next(2)
|
||||
case startsWith("`"):
|
||||
ret.WriteString(tagFromState(spanMono, tagState, "code", "`"))
|
||||
p.Next(1)
|
||||
case startsWith("^"):
|
||||
ret.WriteString(tagFromState(spanSuper, tagState, "sup", "^"))
|
||||
p.Next(1)
|
||||
case startsWith(",,"):
|
||||
ret.WriteString(tagFromState(spanSub, tagState, "sub", ",,"))
|
||||
p.Next(2)
|
||||
case startsWith("!!"):
|
||||
ret.WriteString(tagFromState(spanMark, tagState, "mark", "!!"))
|
||||
p.Next(2)
|
||||
case startsWith("~~"):
|
||||
ret.WriteString(tagFromState(spanMark, tagState, "s", "~~"))
|
||||
p.Next(2)
|
||||
case startsWith("[["):
|
||||
ret.WriteString(getLinkNode(p, hyphaName, true))
|
||||
case (startsWith("https://") || startsWith("http://") || startsWith("gemini://") || startsWith("gopher://") || startsWith("ftp://")) && noTagsActive():
|
||||
ret.WriteString(getLinkNode(p, hyphaName, false))
|
||||
default:
|
||||
ret.WriteString(html.EscapeString(getTextNode(p)))
|
||||
}
|
||||
}
|
||||
|
||||
for stt, open := range tagState {
|
||||
if open {
|
||||
switch stt {
|
||||
case spanItalic:
|
||||
ret.WriteString(tagFromState(spanItalic, tagState, "em", "//"))
|
||||
case spanBold:
|
||||
ret.WriteString(tagFromState(spanBold, tagState, "strong", "**"))
|
||||
case spanMono:
|
||||
ret.WriteString(tagFromState(spanMono, tagState, "code", "`"))
|
||||
case spanSuper:
|
||||
ret.WriteString(tagFromState(spanSuper, tagState, "sup", "^"))
|
||||
case spanSub:
|
||||
ret.WriteString(tagFromState(spanSub, tagState, "sub", ",,"))
|
||||
case spanMark:
|
||||
ret.WriteString(tagFromState(spanMark, tagState, "mark", "!!"))
|
||||
case spanStrike:
|
||||
ret.WriteString(tagFromState(spanMark, tagState, "s", "~~"))
|
||||
case spanLink:
|
||||
ret.WriteString(tagFromState(spanLink, tagState, "a", "[["))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ret.String()
|
||||
}
|
@ -1,28 +0,0 @@
|
||||
package markup
|
||||
|
||||
const maxRecursionLevel = 3
|
||||
|
||||
func Parse(ast []Line, from, to int, recursionLevel int) (html string) {
|
||||
if recursionLevel > maxRecursionLevel {
|
||||
return "Transclusion depth limit"
|
||||
}
|
||||
for _, line := range ast {
|
||||
if line.id >= from && (line.id <= to || to == 0) || line.id == -1 {
|
||||
switch v := line.contents.(type) {
|
||||
case Transclusion:
|
||||
html += Transclude(v, recursionLevel)
|
||||
case Img:
|
||||
html += v.ToHtml()
|
||||
case Table:
|
||||
html += v.asHtml()
|
||||
case *List:
|
||||
html += v.RenderAsHtml()
|
||||
case string:
|
||||
html += v
|
||||
default:
|
||||
html += "<b class='error'>Unknown element.</b>"
|
||||
}
|
||||
}
|
||||
}
|
||||
return html
|
||||
}
|
230
markup/table.go
230
markup/table.go
@ -1,230 +0,0 @@
|
||||
package markup
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
var tableRe = regexp.MustCompile(`^table\s+{`)
|
||||
|
||||
func MatchesTable(line string) bool {
|
||||
return tableRe.MatchString(line)
|
||||
}
|
||||
|
||||
func TableFromFirstLine(line, hyphaName string) *Table {
|
||||
return &Table{
|
||||
hyphaName: hyphaName,
|
||||
caption: line[strings.IndexRune(line, '{')+1:],
|
||||
rows: make([]*tableRow, 0),
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Table) Process(line string) (shouldGoBackToNormal bool) {
|
||||
if strings.TrimSpace(line) == "}" && !t.inMultiline {
|
||||
return true
|
||||
}
|
||||
if !t.inMultiline {
|
||||
t.pushRow()
|
||||
}
|
||||
var (
|
||||
inLink bool
|
||||
skipNext bool
|
||||
escaping bool
|
||||
lookingForNonSpace = !t.inMultiline
|
||||
countingColspan bool
|
||||
)
|
||||
for i, r := range line {
|
||||
switch {
|
||||
case skipNext:
|
||||
skipNext = false
|
||||
continue
|
||||
|
||||
case lookingForNonSpace && unicode.IsSpace(r):
|
||||
case lookingForNonSpace && (r == '!' || r == '|'):
|
||||
t.currCellMarker = r
|
||||
t.currColspan = 1
|
||||
lookingForNonSpace = false
|
||||
countingColspan = true
|
||||
case lookingForNonSpace:
|
||||
t.currCellMarker = '^' // ^ represents implicit |, not part of syntax
|
||||
t.currColspan = 1
|
||||
lookingForNonSpace = false
|
||||
t.currCellBuilder.WriteRune(r)
|
||||
|
||||
case escaping:
|
||||
t.currCellBuilder.WriteRune(r)
|
||||
case inLink && r == ']' && len(line)-1 > i && line[i+1] == ']':
|
||||
t.currCellBuilder.WriteString("]]")
|
||||
inLink = false
|
||||
skipNext = true
|
||||
case inLink:
|
||||
t.currCellBuilder.WriteRune(r)
|
||||
|
||||
case t.inMultiline && r == '}':
|
||||
t.inMultiline = false
|
||||
case t.inMultiline && i == len(line)-1:
|
||||
t.currCellBuilder.WriteRune('\n')
|
||||
case t.inMultiline:
|
||||
t.currCellBuilder.WriteRune(r)
|
||||
|
||||
// Not in multiline:
|
||||
case (r == '|' || r == '!') && !countingColspan:
|
||||
t.pushCell()
|
||||
t.currCellMarker = r
|
||||
t.currColspan = 1
|
||||
countingColspan = true
|
||||
case r == t.currCellMarker && (r == '|' || r == '!') && countingColspan:
|
||||
t.currColspan++
|
||||
case r == '{':
|
||||
t.inMultiline = true
|
||||
countingColspan = false
|
||||
case r == '[' && len(line)-1 > i && line[i+1] == '[':
|
||||
t.currCellBuilder.WriteString("[[")
|
||||
inLink = true
|
||||
skipNext = true
|
||||
case i == len(line)-1:
|
||||
t.pushCell()
|
||||
default:
|
||||
t.currCellBuilder.WriteRune(r)
|
||||
countingColspan = false
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type Table struct {
|
||||
// data
|
||||
hyphaName string
|
||||
caption string
|
||||
rows []*tableRow
|
||||
// state
|
||||
inMultiline bool
|
||||
// tmp
|
||||
currCellMarker rune
|
||||
currColspan uint
|
||||
currCellBuilder strings.Builder
|
||||
}
|
||||
|
||||
func (t *Table) pushRow() {
|
||||
t.rows = append(t.rows, &tableRow{
|
||||
cells: make([]*tableCell, 0),
|
||||
})
|
||||
}
|
||||
|
||||
func (t *Table) pushCell() {
|
||||
tc := &tableCell{
|
||||
content: t.currCellBuilder.String(),
|
||||
colspan: t.currColspan,
|
||||
}
|
||||
switch t.currCellMarker {
|
||||
case '|', '^':
|
||||
tc.kind = tableCellDatum
|
||||
case '!':
|
||||
tc.kind = tableCellHeader
|
||||
}
|
||||
// We expect the table to have at least one row ready, so no nil-checking
|
||||
tr := t.rows[len(t.rows)-1]
|
||||
tr.cells = append(tr.cells, tc)
|
||||
t.currCellBuilder = strings.Builder{}
|
||||
}
|
||||
|
||||
func (t *Table) asHtml() (html string) {
|
||||
if t.caption != "" {
|
||||
html += fmt.Sprintf("<caption>%s</caption>", t.caption)
|
||||
}
|
||||
if len(t.rows) > 0 && t.rows[0].looksLikeThead() {
|
||||
html += fmt.Sprintf("<thead>%s</thead>", t.rows[0].asHtml(t.hyphaName))
|
||||
t.rows = t.rows[1:]
|
||||
}
|
||||
html += "\n<tbody>\n"
|
||||
for _, tr := range t.rows {
|
||||
html += tr.asHtml(t.hyphaName)
|
||||
}
|
||||
return fmt.Sprintf(`<table>%s</tbody></table>`, html)
|
||||
}
|
||||
|
||||
type tableRow struct {
|
||||
cells []*tableCell
|
||||
}
|
||||
|
||||
func (tr *tableRow) asHtml(hyphaName string) (html string) {
|
||||
for _, tc := range tr.cells {
|
||||
html += tc.asHtml(hyphaName)
|
||||
}
|
||||
return fmt.Sprintf("<tr>%s</tr>\n", html)
|
||||
}
|
||||
|
||||
// Most likely, rows with more than two header cells are theads. I allow one extra datum cell for tables like this:
|
||||
// | ! a ! b
|
||||
// ! c | d | e
|
||||
// ! f | g | h
|
||||
func (tr *tableRow) looksLikeThead() bool {
|
||||
var (
|
||||
headerAmount = 0
|
||||
datumAmount = 0
|
||||
)
|
||||
for _, tc := range tr.cells {
|
||||
switch tc.kind {
|
||||
case tableCellHeader:
|
||||
headerAmount++
|
||||
case tableCellDatum:
|
||||
datumAmount++
|
||||
}
|
||||
}
|
||||
return headerAmount >= 2 && datumAmount <= 1
|
||||
}
|
||||
|
||||
type tableCell struct {
|
||||
kind tableCellKind
|
||||
colspan uint
|
||||
content string
|
||||
}
|
||||
|
||||
func (tc *tableCell) asHtml(hyphaName string) string {
|
||||
return fmt.Sprintf(
|
||||
"<%[1]s %[2]s>%[3]s</%[1]s>\n",
|
||||
tc.kind.tagName(),
|
||||
tc.colspanAttribute(),
|
||||
tc.contentAsHtml(hyphaName),
|
||||
)
|
||||
}
|
||||
|
||||
func (tc *tableCell) colspanAttribute() string {
|
||||
if tc.colspan <= 1 {
|
||||
return ""
|
||||
}
|
||||
return fmt.Sprintf(`colspan="%d"`, tc.colspan)
|
||||
}
|
||||
|
||||
func (tc *tableCell) contentAsHtml(hyphaName string) (html string) {
|
||||
for _, line := range strings.Split(tc.content, "\n") {
|
||||
if line = strings.TrimSpace(line); line != "" {
|
||||
if html != "" {
|
||||
html += `<br>`
|
||||
}
|
||||
html += ParagraphToHtml(hyphaName, line)
|
||||
}
|
||||
}
|
||||
return html
|
||||
}
|
||||
|
||||
type tableCellKind int
|
||||
|
||||
const (
|
||||
tableCellUnknown tableCellKind = iota
|
||||
tableCellHeader
|
||||
tableCellDatum
|
||||
)
|
||||
|
||||
func (tck tableCellKind) tagName() string {
|
||||
switch tck {
|
||||
case tableCellHeader:
|
||||
return "th"
|
||||
case tableCellDatum:
|
||||
return "td"
|
||||
default:
|
||||
return "p"
|
||||
}
|
||||
}
|
38
markup/testdata/test.myco
vendored
38
markup/testdata/test.myco
vendored
@ -1,38 +0,0 @@
|
||||
# 1
|
||||
## 2
|
||||
### 3
|
||||
> quote
|
||||
|
||||
* li 1
|
||||
* li 2
|
||||
text
|
||||
more text
|
||||
=> Pear some link
|
||||
|
||||
* li\n"+
|
||||
```alt text goes here
|
||||
=> preformatted text
|
||||
where markup is not lexed
|
||||
```it ends here"
|
||||
=>linking
|
||||
|
||||
text
|
||||
```
|
||||
()
|
||||
/\
|
||||
```
|
||||
<= Apple : 1..3
|
||||
|
||||
img {
|
||||
hypha1
|
||||
hypha2|
|
||||
hypha3| 60
|
||||
hypha4| { line1
|
||||
line2
|
||||
} this is ignored
|
||||
|
||||
hypha5| {
|
||||
state of minnesota
|
||||
}
|
||||
}
|
||||
|
@ -1,12 +0,0 @@
|
||||
package markup
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Function that returns a function that can strip `prefix` and trim whitespace when called.
|
||||
func remover(prefix string) func(string) string {
|
||||
return func(l string) string {
|
||||
return strings.TrimSpace(strings.TrimPrefix(l, prefix))
|
||||
}
|
||||
}
|
@ -1,108 +0,0 @@
|
||||
package markup
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/bouncepaw/mycorrhiza/util"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const xclError = -9
|
||||
|
||||
// Transclusion is used by markup parser to remember what hyphae shall be transcluded.
|
||||
type Transclusion struct {
|
||||
name string
|
||||
from int // inclusive
|
||||
to int // inclusive
|
||||
}
|
||||
|
||||
// Transclude transcludes `xcl` and returns html representation.
|
||||
func Transclude(xcl Transclusion, recursionLevel int) (html string) {
|
||||
recursionLevel++
|
||||
tmptOk := `<section class="transclusion transclusion_ok">
|
||||
<a class="transclusion__link" href="/page/%s">%s</a>
|
||||
<div class="transclusion__content">%s</div>
|
||||
</section>`
|
||||
tmptFailed := `<section class="transclusion transclusion_failed">
|
||||
<p class="error">Hypha <a class="wikilink_new" href="/page/%s">%s</a> does not exist</p>
|
||||
</section>`
|
||||
if xcl.from == xclError || xcl.to == xclError || xcl.from > xcl.to {
|
||||
return fmt.Sprintf(tmptFailed, xcl.name, xcl.name)
|
||||
}
|
||||
|
||||
rawText, binaryHtml, err := HyphaAccess(xcl.name)
|
||||
if err != nil {
|
||||
return fmt.Sprintf(tmptFailed, xcl.name, xcl.name)
|
||||
}
|
||||
md := Doc(xcl.name, rawText)
|
||||
xclText := Parse(md.lex(), xcl.from, xcl.to, recursionLevel)
|
||||
return fmt.Sprintf(tmptOk, xcl.name, xcl.name, binaryHtml+xclText)
|
||||
}
|
||||
|
||||
/* Grammar from hypha ‘transclusion’:
|
||||
transclusion_line ::= transclusion_token hypha_name LWS* [":" LWS* range LWS*]
|
||||
transclusion_token ::= "<=" LWS+
|
||||
hypha_name ::= canonical_name | noncanonical_name
|
||||
range ::= id | (from_id two_dots to_id) | (from_id two_dots) | (two_dots to_id)
|
||||
two_dots ::= ".."
|
||||
*/
|
||||
|
||||
func parseTransclusion(line, hyphaName string) (xclusion Transclusion) {
|
||||
line = strings.TrimSpace(remover("<=")(line))
|
||||
if line == "" {
|
||||
return Transclusion{"", xclError, xclError}
|
||||
}
|
||||
|
||||
if strings.ContainsRune(line, ':') {
|
||||
parts := strings.SplitN(line, ":", 2)
|
||||
xclusion.name = xclCanonicalName(hyphaName, strings.TrimSpace(parts[0]))
|
||||
selector := strings.TrimSpace(parts[1])
|
||||
xclusion.from, xclusion.to = parseSelector(selector)
|
||||
} else {
|
||||
xclusion.name = xclCanonicalName(hyphaName, strings.TrimSpace(line))
|
||||
}
|
||||
return xclusion
|
||||
}
|
||||
|
||||
func xclCanonicalName(hyphaName, xclName string) string {
|
||||
switch {
|
||||
case strings.HasPrefix(xclName, "./"):
|
||||
return util.CanonicalName(path.Join(hyphaName, strings.TrimPrefix(xclName, "./")))
|
||||
case strings.HasPrefix(xclName, "../"):
|
||||
return util.CanonicalName(path.Join(path.Dir(hyphaName), strings.TrimPrefix(xclName, "../")))
|
||||
default:
|
||||
return util.CanonicalName(xclName)
|
||||
}
|
||||
}
|
||||
|
||||
// At this point:
|
||||
// selector ::= id
|
||||
// | from ".."
|
||||
// | from ".." to
|
||||
// | ".." to
|
||||
// If it is not, return (xclError, xclError).
|
||||
func parseSelector(selector string) (from, to int) {
|
||||
if selector == "" {
|
||||
return 0, 0
|
||||
}
|
||||
if strings.Contains(selector, "..") {
|
||||
parts := strings.Split(selector, "..")
|
||||
|
||||
var (
|
||||
fromStr = strings.TrimSpace(parts[0])
|
||||
from, fromErr = strconv.Atoi(fromStr)
|
||||
toStr = strings.TrimSpace(parts[1])
|
||||
to, toErr = strconv.Atoi(toStr)
|
||||
)
|
||||
if fromStr == "" && toStr == "" {
|
||||
return 0, 0
|
||||
}
|
||||
if fromErr == nil || toErr == nil {
|
||||
return from, to
|
||||
}
|
||||
} else if id, err := strconv.Atoi(selector); err == nil {
|
||||
return id, id
|
||||
}
|
||||
return xclError, xclError
|
||||
}
|
@ -2,11 +2,11 @@ package shroom
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/bouncepaw/mycorrhiza/cfg"
|
||||
|
||||
"github.com/bouncepaw/mycorrhiza/hyphae"
|
||||
"github.com/bouncepaw/mycorrhiza/markup"
|
||||
"github.com/bouncepaw/mycorrhiza/views"
|
||||
|
||||
"github.com/bouncepaw/mycomarkup/legacy"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@ -29,10 +29,4 @@ func init() {
|
||||
λ(h.Name)
|
||||
}
|
||||
}
|
||||
markup.HyphaImageForOG = func(hyphaName string) string {
|
||||
if h := hyphae.ByName(hyphaName); h.Exists && h.BinaryPath != "" {
|
||||
return cfg.URL + "/binary/" + hyphaName
|
||||
}
|
||||
return cfg.URL + "/favicon.ico"
|
||||
}
|
||||
}
|
||||
|
@ -1,12 +1,13 @@
|
||||
package shroom
|
||||
|
||||
import (
|
||||
"github.com/bouncepaw/mycorrhiza/cfg"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
|
||||
"github.com/bouncepaw/mycorrhiza/cfg"
|
||||
"github.com/bouncepaw/mycorrhiza/hyphae"
|
||||
"github.com/bouncepaw/mycorrhiza/markup"
|
||||
|
||||
"github.com/bouncepaw/mycomarkup/blocks"
|
||||
)
|
||||
|
||||
// FetchTextPart tries to read text file of the given hypha. If there is no file, empty string is returned.
|
||||
@ -32,7 +33,7 @@ func SetHeaderLinks() {
|
||||
cfg.SetDefaultHeaderLinks()
|
||||
} else {
|
||||
text := string(contents)
|
||||
cfg.ParseHeaderLinks(text, markup.Rocketlink)
|
||||
cfg.ParseHeaderLinks(text, blocks.Rocketlink)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -7,11 +7,12 @@ import (
|
||||
|
||||
"github.com/bouncepaw/mycorrhiza/history"
|
||||
"github.com/bouncepaw/mycorrhiza/hyphae"
|
||||
"github.com/bouncepaw/mycorrhiza/markup"
|
||||
"github.com/bouncepaw/mycorrhiza/shroom"
|
||||
"github.com/bouncepaw/mycorrhiza/user"
|
||||
"github.com/bouncepaw/mycorrhiza/util"
|
||||
"github.com/bouncepaw/mycorrhiza/views"
|
||||
|
||||
"github.com/bouncepaw/mycomarkup/legacy"
|
||||
)
|
||||
|
||||
func initMutators() {
|
||||
|
@ -11,11 +11,12 @@ import (
|
||||
|
||||
"github.com/bouncepaw/mycorrhiza/history"
|
||||
"github.com/bouncepaw/mycorrhiza/hyphae"
|
||||
"github.com/bouncepaw/mycorrhiza/markup"
|
||||
"github.com/bouncepaw/mycorrhiza/mimetype"
|
||||
"github.com/bouncepaw/mycorrhiza/user"
|
||||
"github.com/bouncepaw/mycorrhiza/util"
|
||||
"github.com/bouncepaw/mycorrhiza/views"
|
||||
|
||||
"github.com/bouncepaw/mycomarkup/legacy"
|
||||
)
|
||||
|
||||
func initReaders() {
|
||||
|
Loading…
Reference in New Issue
Block a user