1
0
mirror of https://github.com/osmarks/mycorrhiza.git synced 2024-10-30 03:36:16 +00:00

Implement some features of mycomarkup

This commit is contained in:
bouncepaw 2020-10-30 18:25:48 +05:00
parent 657fb5d357
commit d6c6ad4ae3
20 changed files with 335 additions and 59 deletions

View File

@ -4,8 +4,13 @@ A wiki engine.
This is the development branch for version 0.10. Features planned for this version:
* [x] New file structure
* [ ] Mycomarkup
* [x] 6 headings
* [x] Paragraph styling
* [ ] Inline links
* [ ] Better lists
* [ ] Better quotes
* [x] CLI options
* [ ] CSS improvements
* [x] CSS improvements
## Building
```sh

View File

@ -77,7 +77,7 @@ func (hop *HistoryOp) WithFilesRenamed(pairs map[string]string) *HistoryOp {
for from, to := range pairs {
if from != "" {
os.MkdirAll(filepath.Dir(to), 0777)
hop.gitop(append([]string{"mv"}, from, to)...)
Rename(from, to)
}
}
return hop

View File

@ -118,7 +118,7 @@ func handlerEdit(w http.ResponseWriter, rq *http.Request) {
} else {
warning = `<p>You are creating a new hypha.</p>`
}
util.HTTP200Page(w, base("Edit"+hyphaName, templates.EditHTML(hyphaName, textAreaFill, warning)))
util.HTTP200Page(w, base("Edit "+hyphaName, templates.EditHTML(hyphaName, textAreaFill, warning)))
}
// handlerUploadText uploads a new text part for the hypha.

View File

@ -9,8 +9,8 @@ import (
"path/filepath"
"strings"
"github.com/bouncepaw/mycorrhiza/gemtext"
"github.com/bouncepaw/mycorrhiza/history"
"github.com/bouncepaw/mycorrhiza/markup"
"github.com/bouncepaw/mycorrhiza/templates"
"github.com/bouncepaw/mycorrhiza/tree"
"github.com/bouncepaw/mycorrhiza/util"
@ -37,7 +37,7 @@ func handlerRevision(w http.ResponseWriter, rq *http.Request) {
textContents, err = history.FileAtRevision(textPath, revHash)
)
if err == nil {
contents = gemtext.ToHtml(hyphaName, textContents)
contents = markup.ToHtml(hyphaName, textContents)
}
page := templates.RevisionHTML(
hyphaName,
@ -104,7 +104,7 @@ func handlerPage(w http.ResponseWriter, rq *http.Request) {
fileContentsT, errT := ioutil.ReadFile(data.textPath)
_, errB := os.Stat(data.binaryPath)
if errT == nil {
contents = gemtext.ToHtml(hyphaName, string(fileContentsT))
contents = markup.ToHtml(hyphaName, string(fileContentsT))
}
if !os.IsNotExist(errB) {
contents = binaryHtmlBlock(hyphaName, data) + contents

View File

@ -10,17 +10,17 @@ import (
"path/filepath"
"strings"
"github.com/bouncepaw/mycorrhiza/gemtext"
"github.com/bouncepaw/mycorrhiza/history"
"github.com/bouncepaw/mycorrhiza/markup"
"github.com/bouncepaw/mycorrhiza/util"
)
func init() {
gemtext.HyphaExists = func(hyphaName string) bool {
markup.HyphaExists = func(hyphaName string) bool {
_, hyphaExists := HyphaStorage[hyphaName]
return hyphaExists
}
gemtext.HyphaAccess = func(hyphaName string) (rawText, binaryBlock string, err error) {
markup.HyphaAccess = func(hyphaName string) (rawText, binaryBlock string, err error) {
if hyphaData, ok := HyphaStorage[hyphaName]; ok {
rawText, err = FetchTextPart(hyphaData)
if hyphaData.binaryPath != "" {

View File

@ -1,4 +1,4 @@
package gemtext
package markup
import (
"fmt"
@ -13,7 +13,7 @@ var HyphaExists func(string) bool
// HyphaAccess holds function that accesses a hypha by its name.
var HyphaAccess func(string) (rawText, binaryHtml string, err error)
// GemLexerState is used by gemtext parser to remember what is going on.
// GemLexerState is used by markup parser to remember what is going on.
type GemLexerState struct {
// Name of hypha being parsed
name string
@ -29,7 +29,7 @@ type Line struct {
contents interface{}
}
// Parse gemtext line starting with "=>" according to wikilink rules.
// Parse markup line starting with "=>" according to wikilink rules.
// See http://localhost:1737/page/wikilink
func wikilink(src string, state *GemLexerState) (href, text, class string) {
src = strings.TrimSpace(remover("=>")(src))
@ -79,7 +79,7 @@ func lex(name, content string) (ast []Line) {
return ast
}
// Lex `line` in gemtext and save it to `ast` using `state`.
// Lex `line` in markup and save it to `ast` using `state`.
func geminiLineToAST(line string, state *GemLexerState, ast *[]Line) {
addLine := func(text interface{}) {
*ast = append(*ast, Line{id: state.id, contents: text})
@ -142,20 +142,29 @@ normalState:
case startsWith("```"):
state.where = "pre"
state.buf = fmt.Sprintf("<pre id='%d' alt='%s' class='codeblock'><code>", state.id, strings.TrimPrefix(line, "```"))
case startsWith("*"):
case startsWith("* "):
state.where = "list"
state.buf = fmt.Sprintf("<ul id='%d'>\n", state.id)
goto listState
case startsWith("###"):
case startsWith("###### "):
addLine(fmt.Sprintf(
"<h3 id='%d'>%s</h3>", state.id, removeHeadingOctothorps(line)))
case startsWith("##"):
"<h6 id='%d'>%s</h6>", state.id, line[7:]))
case startsWith("##### "):
addLine(fmt.Sprintf(
"<h2 id='%d'>%s</h2>", state.id, removeHeadingOctothorps(line)))
case startsWith("#"):
"<h5 id='%d'>%s</h5>", state.id, line[6:]))
case startsWith("#### "):
addLine(fmt.Sprintf(
"<h1 id='%d'>%s</h1>", state.id, removeHeadingOctothorps(line)))
"<h4 id='%d'>%s</h4>", state.id, line[5:]))
case startsWith("### "):
addLine(fmt.Sprintf(
"<h3 id='%d'>%s</h3>", state.id, line[4:]))
case startsWith("## "):
addLine(fmt.Sprintf(
"<h2 id='%d'>%s</h2>", state.id, line[3:]))
case startsWith("# "):
addLine(fmt.Sprintf(
"<h1 id='%d'>%s</h1>", state.id, line[2:]))
case startsWith(">"):
addLine(fmt.Sprintf(
@ -168,6 +177,6 @@ normalState:
case startsWith("<="):
addLine(parseTransclusion(line, state.name))
default:
addLine(fmt.Sprintf("<p id='%d'>%s</p>", state.id, line))
addLine(fmt.Sprintf("<p id='%d'>%s</p>", state.id, ParagraphToHtml(line)))
}
}

View File

@ -1,4 +1,4 @@
package gemtext
package markup
import (
"fmt"
@ -7,7 +7,7 @@ import (
"testing"
)
// TODO: move test gemtext docs to files, perhaps? These strings sure are ugly
// TODO: move test markup docs to files, perhaps? These strings sure are ugly
func TestLex(t *testing.T) {
check := func(name, content string, expectedAst []Line) {
if ast := lex(name, content); !reflect.DeepEqual(ast, expectedAst) {
@ -25,9 +25,9 @@ func TestLex(t *testing.T) {
}
}
}
contentsB, err := ioutil.ReadFile("testdata/test.gmi")
contentsB, err := ioutil.ReadFile("testdata/test.myco")
if err != nil {
t.Error("Could not read test gemtext file!")
t.Error("Could not read test markup file!")
}
contents := string(contentsB)
check("Apple", contents, []Line{
@ -46,7 +46,7 @@ func TestLex(t *testing.T) {
<li>li\n"+</li>
</ul>`},
{10, `<pre id='10' alt='alt text goes here' class='codeblock'><code>=&gt; preformatted text
where gemtext is not lexed</code></pre>`},
where markup is not lexed</code></pre>`},
{11, `<p><a id='11' class='wikilink_internal' href="/page/linking">linking</a></p>`},
{12, "<p id='12'>text</p>"},
{13, `<pre id='13' alt='' class='codeblock'><code>()

102
markup/mycomarkup.go Normal file
View File

@ -0,0 +1,102 @@
// This is not done yet
package markup
import (
"html"
"strings"
)
// A Mycomarkup-formatted document
type MycoDoc struct {
// data
hyphaName string
contents string
// state
recursionDepth int
// results
}
// Constructor
func Doc(hyphaName, contents string) *MycoDoc {
return &MycoDoc{
hyphaName: hyphaName,
contents: contents,
}
}
// AsHtml returns an html representation of the document
func (md *MycoDoc) AsHtml() string {
return ""
}
type BlockType int
const (
BlockH1 = iota
BlockH2
BlockH3
BlockH4
BlockH5
BlockH6
BlockRocket
BlockPre
BlockQuote
BlockPara
)
type CrawlWhere int
const (
inSomewhere = iota
inPre
inEnd
)
func crawl(name, content string) []string {
stateStack := []CrawlWhere{inSomewhere}
startsWith := func(token string) bool {
return strings.HasPrefix(content, token)
}
pop := func() {
stateStack = stateStack[:len(stateStack)-1]
}
push := func(s CrawlWhere) {
stateStack = append(stateStack, s)
}
readln := func(c string) (string, string) {
parts := strings.SplitN(c, "\n", 1)
return parts[0], parts[1]
}
preAcc := ""
line := ""
for {
switch stateStack[0] {
case inSomewhere:
switch {
case startsWith("```"):
push(inPre)
_, content = readln(content)
default:
}
case inPre:
switch {
case startsWith("```"):
pop()
_, content = readln(content)
default:
line, content = readln(content)
preAcc += html.EscapeString(line)
}
}
}
return []string{}
}

108
markup/paragraph.go Normal file
View File

@ -0,0 +1,108 @@
package markup
import (
"bytes"
"fmt"
"html"
"strings"
)
type spanTokenType int
const (
spanTextNode = iota
spanItalic
spanBold
spanMono
spanSuper
spanSub
spanMark
)
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)
}
}
// getTextNode splits the `p` into two parts `textNode` and `rest` by the first encountered rune that resembles a span tag. If there is none, `textNode = p`, `rest = ""`. It handles escaping with backslash.
func getTextNode(input *bytes.Buffer) string {
var (
textNodeBuffer = bytes.Buffer{}
escaping = false
)
// 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 {
textNodeBuffer.WriteByte(b)
}
}
return textNodeBuffer.String()
}
func ParagraphToHtml(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,
}
startsWith = func(t string) bool {
return bytes.HasPrefix(p.Bytes(), []byte(t))
}
)
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)
default:
ret.WriteString(html.EscapeString(getTextNode(p)))
}
}
return ret.String()
}

44
markup/paragraph_test.go Normal file
View File

@ -0,0 +1,44 @@
package markup
import (
"fmt"
"testing"
)
/*
func TestGetTextNode(t *testing.T) {
tests := [][]string{
// input textNode rest
{"barab", "barab", ""},
{"test, ", "test", ", "},
{"/test/", "", "/test/"},
{"\\/test/", "/test", "/"},
{"test \\/ar", "test /ar", ""},
{"test //italian// test", "test ", "//italian// test"},
}
for _, triplet := range tests {
a, b := getTextNode([]byte(triplet[0]))
if a != triplet[1] || string(b) != triplet[2] {
t.Error(fmt.Sprintf("Wanted: %q\nGot: %q %q", triplet, a, b))
}
}
}
*/
func TestParagraphToHtml(t *testing.T) {
tests := [][]string{
{"a simple paragraph", "a simple paragraph"},
{"//italic//", "<em>italic</em>"},
{"Embedded //italic//", "Embedded <em>italic</em>"},
{"double //italian// //text//", "double <em>italian</em> <em>text</em>"},
{"it has `mono`", "it has <code>mono</code>"},
{"this is a left **bold", "this is a left <strong>bold"},
{"this line has a ,comma, two of them", "this line has a ,comma, two of them"},
{"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."},
}
for _, test := range tests {
if ParagraphToHtml(test[0]) != test[1] {
t.Error(fmt.Sprintf("%q: Wanted %q, got %q", test[0], test[1], ParagraphToHtml(test[0])))
}
}
}

View File

@ -1,4 +1,4 @@
package gemtext
package markup
import ()

View File

@ -12,7 +12,7 @@ more text
* li\n"+
```alt text goes here
=> preformatted text
where gemtext is not lexed
where markup is not lexed
```it ends here"
=>linking

View File

@ -1,4 +1,4 @@
package gemtext
package markup
import (
"strings"

View File

@ -1,4 +1,4 @@
package gemtext
package markup
import (
"fmt"
@ -9,7 +9,7 @@ import (
const xclError = -9
// Transclusion is used by gemtext parser to remember what hyphae shall be transcluded.
// Transclusion is used by markup parser to remember what hyphae shall be transcluded.
type Transclusion struct {
name string
from int // inclusive

View File

@ -1,4 +1,4 @@
package gemtext
package markup
import (
"testing"
@ -6,7 +6,7 @@ import (
func TestParseTransclusion(t *testing.T) {
check := func(line string, expectedXclusion Transclusion) {
if xcl := parseTransclusion(line); xcl != expectedXclusion {
if xcl := parseTransclusion(line, "t"); xcl != expectedXclusion {
t.Error(line, "; got:", xcl, "wanted:", expectedXclusion)
}
}

@ -1 +1 @@
Subproject commit 46cc59eeca16e873b47fdda84009adabb4e82d2b
Subproject commit 58d000edaa4f06586accc6199190ca69bebf2ad4

View File

@ -9,17 +9,21 @@
html {height:100%; padding:0; background-color:#ddd;
background-image: url("data:image/svg+xml,%3Csvg width='42' height='44' viewBox='0 0 42 44' xmlns='http://www.w3.org/2000/svg'%3E%3Cg id='Page-1' fill='none' fill-rule='evenodd'%3E%3Cg id='brick-wall' fill='%23bbbbbb' fill-opacity='0.4'%3E%3Cpath d='M0 0h42v44H0V0zm1 1h40v20H1V1zM0 23h20v20H0V23zm22 0h20v20H22V23z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E");} /* heropatterns.com */
body {height:100%; margin:0; font-size:16px; font-family:sans-serif;}
main {padding:1rem; background-color: white; box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.2);}
main {padding:1rem; background-color: white; box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.2); }
main > form {margin-bottom:1rem;}
textarea {font-size:15px;}
.upload-text-form {height:90%;}
.upload-text-form textarea {width:100%;height:90%;}
.edit {height:100%;}
.edit-form {height:90%;}
.edit-form textarea {width:100%;height:90%;}
main h1:not(.navi-title) {font-size:1.7rem;}
blockquote {border-left: 4px black solid; margin-left: 0; padding-left: 1rem;}
.wikilink_new {color:#a55858;}
.wikilink_new:visited {color:#a55858;}
.wikilink_external::after {content:"🌐"; margin-left: .5rem; font-size: small; text-decoration: none; align: bottom;}
p code {background-color:#eee; padding: .1rem .3rem; border-radius: .25rem; font-size: 90%; }
.codeblock {background-color:#eee; padding:.5rem; font-size:16px; white-space: pre-wrap;}
.transclusion .codeblock {background-color:#ddd;}
.transclusion code, .transclusion .codeblock {background-color:#ddd;}
.transclusion {background-color:#eee; padding:0 0 0 1.5rem;}
.transclusion__link {display: block; position: absolute; transform-origin: 0 0; transform: rotate(90deg); margin-top: 0.5rem; color: black; text-decoration: none;}

View File

@ -31,17 +31,21 @@ func StreamDefaultCSS(qw422016 *qt422016.Writer) {
html {height:100%; padding:0; background-color:#ddd;
background-image: url("data:image/svg+xml,%3Csvg width='42' height='44' viewBox='0 0 42 44' xmlns='http://www.w3.org/2000/svg'%3E%3Cg id='Page-1' fill='none' fill-rule='evenodd'%3E%3Cg id='brick-wall' fill='%23bbbbbb' fill-opacity='0.4'%3E%3Cpath d='M0 0h42v44H0V0zm1 1h40v20H1V1zM0 23h20v20H0V23zm22 0h20v20H22V23z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E");} /* heropatterns.com */
body {height:100%; margin:0; font-size:16px; font-family:sans-serif;}
main {padding:1rem; background-color: white; box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.2);}
main {padding:1rem; background-color: white; box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.2); }
main > form {margin-bottom:1rem;}
textarea {font-size:15px;}
.upload-text-form {height:90%;}
.upload-text-form textarea {width:100%;height:90%;}
.edit {height:100%;}
.edit-form {height:90%;}
.edit-form textarea {width:100%;height:90%;}
main h1:not(.navi-title) {font-size:1.7rem;}
blockquote {border-left: 4px black solid; margin-left: 0; padding-left: 1rem;}
.wikilink_new {color:#a55858;}
.wikilink_new:visited {color:#a55858;}
.wikilink_external::after {content:"🌐"; margin-left: .5rem; font-size: small; text-decoration: none; align: bottom;}
p code {background-color:#eee; padding: .1rem .3rem; border-radius: .25rem; font-size: 90%; }
.codeblock {background-color:#eee; padding:.5rem; font-size:16px; white-space: pre-wrap;}
.transclusion .codeblock {background-color:#ddd;}
.transclusion code, .transclusion .codeblock {background-color:#ddd;}
.transclusion {background-color:#eee; padding:0 0 0 1.5rem;}
.transclusion__link {display: block; position: absolute; transform-origin: 0 0; transform: rotate(90deg); margin-top: 0.5rem; color: black; text-decoration: none;}
@ -55,31 +59,31 @@ nav ul li {list-style-type:none;margin-right:1rem;}
#new-name {width:100%;}
`)
//line templates/css.qtpl:35
//line templates/css.qtpl:39
}
//line templates/css.qtpl:35
//line templates/css.qtpl:39
func WriteDefaultCSS(qq422016 qtio422016.Writer) {
//line templates/css.qtpl:35
//line templates/css.qtpl:39
qw422016 := qt422016.AcquireWriter(qq422016)
//line templates/css.qtpl:35
//line templates/css.qtpl:39
StreamDefaultCSS(qw422016)
//line templates/css.qtpl:35
//line templates/css.qtpl:39
qt422016.ReleaseWriter(qw422016)
//line templates/css.qtpl:35
//line templates/css.qtpl:39
}
//line templates/css.qtpl:35
//line templates/css.qtpl:39
func DefaultCSS() string {
//line templates/css.qtpl:35
//line templates/css.qtpl:39
qb422016 := qt422016.AcquireByteBuffer()
//line templates/css.qtpl:35
//line templates/css.qtpl:39
WriteDefaultCSS(qb422016)
//line templates/css.qtpl:35
//line templates/css.qtpl:39
qs422016 := string(qb422016.B)
//line templates/css.qtpl:35
//line templates/css.qtpl:39
qt422016.ReleaseByteBuffer(qb422016)
//line templates/css.qtpl:35
//line templates/css.qtpl:39
return qs422016
//line templates/css.qtpl:35
//line templates/css.qtpl:39
}

View File

@ -1,8 +1,8 @@
{% func EditHTML(hyphaName, textAreaFill, warning string) %}
<main>
<main class="edit">
<h1>Edit {%s hyphaName %}</h1>
{%s= warning %}
<form method="post" class="upload-text-form"
<form method="post" class="edit-form"
action="/upload-text/{%s hyphaName %}">
<textarea name="text">{%s textAreaFill %}</textarea>
<br/>

View File

@ -21,7 +21,7 @@ var (
func StreamEditHTML(qw422016 *qt422016.Writer, hyphaName, textAreaFill, warning string) {
//line templates/http_mutators.qtpl:1
qw422016.N().S(`
<main>
<main class="edit">
<h1>Edit `)
//line templates/http_mutators.qtpl:3
qw422016.E().S(hyphaName)
@ -32,7 +32,7 @@ func StreamEditHTML(qw422016 *qt422016.Writer, hyphaName, textAreaFill, warning
qw422016.N().S(warning)
//line templates/http_mutators.qtpl:4
qw422016.N().S(`
<form method="post" class="upload-text-form"
<form method="post" class="edit-form"
action="/upload-text/`)
//line templates/http_mutators.qtpl:6
qw422016.E().S(hyphaName)