mirror of
https://github.com/osmarks/mycorrhiza.git
synced 2024-12-13 22:00:27 +00:00
commit
4f10aa35fe
20
README.md
20
README.md
@ -1,4 +1,4 @@
|
||||
# 🍄 MycorrhizaWiki 0.9
|
||||
# 🍄 MycorrhizaWiki 0.10
|
||||
A wiki engine.
|
||||
|
||||
## Building
|
||||
@ -11,12 +11,25 @@ make
|
||||
# * create an executable called `mycorrhiza`. Run it with path to your wiki.
|
||||
```
|
||||
|
||||
## Usage
|
||||
```
|
||||
mycorrhiza [OPTIONS...] WIKI_PATH
|
||||
|
||||
Options:
|
||||
-home string
|
||||
The home page (default "home")
|
||||
-port string
|
||||
Port to serve the wiki at (default "1737")
|
||||
-title string
|
||||
How to call your wiki in the navititle (default "🍄")
|
||||
```
|
||||
|
||||
## Features
|
||||
* Edit pages through html forms
|
||||
* Responsive design
|
||||
* Works in text browsers
|
||||
* Wiki pages (called hyphae) are in gemtext
|
||||
* Everything is stored as simple files, no database required
|
||||
* Wiki pages (called hyphae) are written in mycomarkup
|
||||
* Everything is stored as simple files, no database required. You can run a wiki on almost any directory and get something to work with.
|
||||
* Page trees
|
||||
* Changes are saved to git
|
||||
* List of hyphae page
|
||||
@ -34,4 +47,3 @@ Help is always needed. We have a [tg chat](https://t.me/mycorrhizadev) where som
|
||||
* Tagging system
|
||||
* Authorization
|
||||
* Better history viewing
|
||||
* More markups
|
||||
|
36
flag.go
Normal file
36
flag.go
Normal file
@ -0,0 +1,36 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"log"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/bouncepaw/mycorrhiza/util"
|
||||
)
|
||||
|
||||
func init() {
|
||||
flag.StringVar(&util.ServerPort, "port", "1737", "Port to serve the wiki at")
|
||||
flag.StringVar(&util.HomePage, "home", "home", "The home page")
|
||||
flag.StringVar(&util.SiteTitle, "title", "🍄", "How to call your wiki in the navititle")
|
||||
}
|
||||
|
||||
// Do the things related to cli args and die maybe
|
||||
func parseCliArgs() {
|
||||
flag.Parse()
|
||||
|
||||
args := flag.Args()
|
||||
if len(args) == 0 {
|
||||
log.Fatal("Error: pass a wiki directory")
|
||||
}
|
||||
|
||||
var err error
|
||||
WikiDir, err = filepath.Abs(args[0])
|
||||
util.WikiDir = WikiDir
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if !isCanonicalName(util.HomePage) {
|
||||
log.Fatal("Error: you must use a proper name for the homepage")
|
||||
}
|
||||
}
|
@ -59,16 +59,16 @@ func (rev Revision) HyphaeLinks() (html string) {
|
||||
}
|
||||
for _, filename := range strings.Split(out.String(), "\n") {
|
||||
// If filename has an ampersand:
|
||||
if strings.IndexRune(filename, '&') >= 0 {
|
||||
if strings.IndexRune(filename, '.') >= 0 {
|
||||
// Remove ampersanded suffix from filename:
|
||||
ampersandPos := strings.LastIndexByte(filename, '&')
|
||||
ampersandPos := strings.LastIndexByte(filename, '.')
|
||||
hyphaName := string([]byte(filename)[0:ampersandPos]) // is it safe?
|
||||
if isNewName(hyphaName) {
|
||||
// Entries are separated by commas
|
||||
if len(set) > 1 {
|
||||
html += `<span aria-hidden="true">, </span>`
|
||||
}
|
||||
html += fmt.Sprintf(`<a href="/rev/%[1]s/%[2]s">%[2]s</a>`, rev.Hash, hyphaName)
|
||||
html += fmt.Sprintf(`<a href="/page/%[1]s">%[1]s</a>`, hyphaName)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -77,10 +77,10 @@ func (rev Revision) HyphaeLinks() (html string) {
|
||||
|
||||
func (rev Revision) RecentChangesEntry() (html string) {
|
||||
return fmt.Sprintf(`
|
||||
<li><time>%s</time></li>
|
||||
<li>%s</li>
|
||||
<li>%s</li>
|
||||
<li>%s</li>
|
||||
<li class="rc-entry__time"><time>%s</time></li>
|
||||
<li class="rc-entry__hash">%s</li>
|
||||
<li class="rc-entry__links">%s</li>
|
||||
<li class="rc-entry__msg">%s</li>
|
||||
`, rev.TimeString(), rev.Hash, rev.HyphaeLinks(), rev.Message)
|
||||
}
|
||||
|
||||
@ -125,6 +125,6 @@ func unixTimestampAsTime(ts string) *time.Time {
|
||||
// Rename renames from `from` to `to` using `git mv`.
|
||||
func Rename(from, to string) error {
|
||||
log.Println(util.ShorterPath(from), util.ShorterPath(to))
|
||||
_, err := gitsh("mv", from, to)
|
||||
_, err := gitsh("mv", "--force", from, to)
|
||||
return err
|
||||
}
|
||||
|
@ -39,7 +39,7 @@ func Revisions(hyphaName string) ([]Revision, error) {
|
||||
"log", "--oneline", "--no-merges",
|
||||
// Hash, Commiter email, Commiter time, Commit msg separated by tab
|
||||
"--pretty=format:\"%h\t%ce\t%ct\t%s\"",
|
||||
"--", hyphaName+"&.*",
|
||||
"--", hyphaName+".*",
|
||||
)
|
||||
revs []Revision
|
||||
)
|
||||
|
@ -55,6 +55,12 @@ func (hop *HistoryOp) gitop(args ...string) *HistoryOp {
|
||||
return hop
|
||||
}
|
||||
|
||||
// WithError appends the `err` to the list of errors.
|
||||
func (hop *HistoryOp) WithError(err error) *HistoryOp {
|
||||
hop.Errs = append(hop.Errs, err)
|
||||
return hop
|
||||
}
|
||||
|
||||
// WithFilesRemoved git-rm-s all passed `paths`. Paths can be rooted or not. Paths that are empty strings are ignored.
|
||||
func (hop *HistoryOp) WithFilesRemoved(paths ...string) *HistoryOp {
|
||||
args := []string{"rm", "--quiet", "--"}
|
||||
@ -71,7 +77,9 @@ 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)...)
|
||||
if err := Rename(from, to); err != nil {
|
||||
hop.Errs = append(hop.Errs, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
return hop
|
||||
|
@ -2,13 +2,9 @@ package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/bouncepaw/mycorrhiza/history"
|
||||
"github.com/bouncepaw/mycorrhiza/templates"
|
||||
"github.com/bouncepaw/mycorrhiza/util"
|
||||
)
|
||||
@ -116,14 +112,13 @@ func handlerEdit(w http.ResponseWriter, rq *http.Request) {
|
||||
textAreaFill, err = FetchTextPart(hyphaData)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
HttpErr(w, http.StatusInternalServerError, hyphaName, "Error",
|
||||
"Could not fetch text data")
|
||||
HttpErr(w, http.StatusInternalServerError, hyphaName, "Error", "Could not fetch text data")
|
||||
return
|
||||
}
|
||||
} 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.
|
||||
@ -133,42 +128,19 @@ func handlerUploadText(w http.ResponseWriter, rq *http.Request) {
|
||||
hyphaName = HyphaNameFromRq(rq, "upload-text")
|
||||
hyphaData, isOld = HyphaStorage[hyphaName]
|
||||
textData = rq.PostFormValue("text")
|
||||
textDataBytes = []byte(textData)
|
||||
fullPath = filepath.Join(WikiDir, hyphaName+"&.gmi")
|
||||
)
|
||||
if textData == "" {
|
||||
HttpErr(w, http.StatusBadRequest, hyphaName, "Error",
|
||||
"No text data passed")
|
||||
return
|
||||
}
|
||||
// For some reason, only 0777 works. Why?
|
||||
if err := os.MkdirAll(filepath.Dir(fullPath), 0777); err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
if err := ioutil.WriteFile(fullPath, textDataBytes, 0644); err != nil {
|
||||
log.Println(err)
|
||||
HttpErr(w, http.StatusInternalServerError, hyphaName, "Error",
|
||||
fmt.Sprintf("Failed to write %d bytes to %s",
|
||||
len(textDataBytes), fullPath))
|
||||
return
|
||||
}
|
||||
if !isOld {
|
||||
hd := HyphaData{
|
||||
textType: TextGemini,
|
||||
textPath: fullPath,
|
||||
hyphaData = &HyphaData{}
|
||||
}
|
||||
HyphaStorage[hyphaName] = &hd
|
||||
hyphaData = &hd
|
||||
if textData == "" {
|
||||
HttpErr(w, http.StatusBadRequest, hyphaName, "Error", "No text data passed")
|
||||
return
|
||||
}
|
||||
if hop := hyphaData.UploadText(hyphaName, textData, isOld); len(hop.Errs) != 0 {
|
||||
HttpErr(w, http.StatusInternalServerError, hyphaName, "Error", hop.Errs[0].Error())
|
||||
} else {
|
||||
hyphaData.textType = TextGemini
|
||||
hyphaData.textPath = fullPath
|
||||
}
|
||||
history.Operation(history.TypeEditText).
|
||||
WithFiles(fullPath).
|
||||
WithMsg(fmt.Sprintf("Edit ‘%s’", hyphaName)).
|
||||
WithSignature("anon").
|
||||
Apply()
|
||||
http.Redirect(w, rq, "/page/"+hyphaName, http.StatusSeeOther)
|
||||
}
|
||||
}
|
||||
|
||||
// handlerUploadBinary uploads a new binary part for the hypha.
|
||||
@ -176,62 +148,29 @@ func handlerUploadBinary(w http.ResponseWriter, rq *http.Request) {
|
||||
log.Println(rq.URL)
|
||||
hyphaName := HyphaNameFromRq(rq, "upload-binary")
|
||||
rq.ParseMultipartForm(10 << 20)
|
||||
// Read file
|
||||
|
||||
file, handler, err := rq.FormFile("binary")
|
||||
if file != nil {
|
||||
defer file.Close()
|
||||
}
|
||||
// If file is not passed:
|
||||
if err != nil {
|
||||
HttpErr(w, http.StatusBadRequest, hyphaName, "Error",
|
||||
"No binary data passed")
|
||||
HttpErr(w, http.StatusBadRequest, hyphaName, "Error", "No binary data passed")
|
||||
return
|
||||
}
|
||||
// If file is passed:
|
||||
var (
|
||||
hyphaData, isOld = HyphaStorage[hyphaName]
|
||||
mimeType = MimeToBinaryType(handler.Header.Get("Content-Type"))
|
||||
ext = mimeType.Extension()
|
||||
fullPath = filepath.Join(WikiDir, hyphaName+"&"+ext)
|
||||
mime = handler.Header.Get("Content-Type")
|
||||
)
|
||||
|
||||
data, err := ioutil.ReadAll(file)
|
||||
if err != nil {
|
||||
HttpErr(w, http.StatusInternalServerError, hyphaName, "Error",
|
||||
"Could not read passed data")
|
||||
return
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Dir(fullPath), 0777); err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
if !isOld {
|
||||
hd := HyphaData{
|
||||
binaryPath: fullPath,
|
||||
binaryType: mimeType,
|
||||
hyphaData = &HyphaData{}
|
||||
}
|
||||
HyphaStorage[hyphaName] = &hd
|
||||
hyphaData = &hd
|
||||
hop := hyphaData.UploadBinary(hyphaName, mime, file, isOld)
|
||||
|
||||
if len(hop.Errs) != 0 {
|
||||
HttpErr(w, http.StatusInternalServerError, hyphaName, "Error", hop.Errs[0].Error())
|
||||
} else {
|
||||
if hyphaData.binaryPath != fullPath {
|
||||
if err := history.Rename(hyphaData.binaryPath, fullPath); err != nil {
|
||||
log.Println(err)
|
||||
} else {
|
||||
log.Println("Moved", hyphaData.binaryPath, "to", fullPath)
|
||||
}
|
||||
}
|
||||
hyphaData.binaryPath = fullPath
|
||||
hyphaData.binaryType = mimeType
|
||||
}
|
||||
if err = ioutil.WriteFile(fullPath, data, 0644); err != nil {
|
||||
HttpErr(w, http.StatusInternalServerError, hyphaName, "Error",
|
||||
"Could not save passed data")
|
||||
return
|
||||
}
|
||||
log.Println("Written", len(data), "of binary data for", hyphaName, "to path", fullPath)
|
||||
history.Operation(history.TypeEditText).
|
||||
WithFiles(fullPath, hyphaData.binaryPath).
|
||||
WithMsg(fmt.Sprintf("Upload binary part for ‘%s’ with type ‘%s’", hyphaName, mimeType.Mime())).
|
||||
WithSignature("anon").
|
||||
Apply()
|
||||
http.Redirect(w, rq, "/page/"+hyphaName, http.StatusSeeOther)
|
||||
}
|
||||
}
|
||||
|
@ -6,10 +6,11 @@ import (
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"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"
|
||||
@ -32,11 +33,11 @@ func handlerRevision(w http.ResponseWriter, rq *http.Request) {
|
||||
revHash = shorterUrl[:firstSlashIndex]
|
||||
hyphaName = CanonicalName(shorterUrl[firstSlashIndex+1:])
|
||||
contents = fmt.Sprintf(`<p>This hypha had no text at this revision.</p>`)
|
||||
textPath = hyphaName + "&.gmi"
|
||||
textPath = hyphaName + ".myco"
|
||||
textContents, err = history.FileAtRevision(textPath, revHash)
|
||||
)
|
||||
if err == nil {
|
||||
contents = gemtext.ToHtml(hyphaName, textContents)
|
||||
contents = markup.ToHtml(hyphaName, textContents)
|
||||
}
|
||||
page := templates.RevisionHTML(
|
||||
hyphaName,
|
||||
@ -75,7 +76,7 @@ func handlerText(w http.ResponseWriter, rq *http.Request) {
|
||||
hyphaName := HyphaNameFromRq(rq, "text")
|
||||
if data, ok := HyphaStorage[hyphaName]; ok {
|
||||
log.Println("Serving", data.textPath)
|
||||
w.Header().Set("Content-Type", data.textType.Mime())
|
||||
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||
http.ServeFile(w, rq, data.textPath)
|
||||
}
|
||||
}
|
||||
@ -86,7 +87,7 @@ func handlerBinary(w http.ResponseWriter, rq *http.Request) {
|
||||
hyphaName := HyphaNameFromRq(rq, "binary")
|
||||
if data, ok := HyphaStorage[hyphaName]; ok {
|
||||
log.Println("Serving", data.binaryPath)
|
||||
w.Header().Set("Content-Type", data.binaryType.Mime())
|
||||
w.Header().Set("Content-Type", ExtensionToMime(filepath.Ext(data.binaryPath)))
|
||||
http.ServeFile(w, rq, data.binaryPath)
|
||||
}
|
||||
}
|
||||
@ -103,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
|
||||
|
109
hypha.go
109
hypha.go
@ -5,21 +5,22 @@ import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"mime/multipart"
|
||||
"os"
|
||||
"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 != "" {
|
||||
@ -35,9 +36,54 @@ func init() {
|
||||
// HyphaData represents a hypha's meta information: binary and text parts rooted paths and content types.
|
||||
type HyphaData struct {
|
||||
textPath string
|
||||
textType TextType
|
||||
binaryPath string
|
||||
binaryType BinaryType
|
||||
}
|
||||
|
||||
// uploadHelp is a helper function for UploadText and UploadBinary
|
||||
func (hd *HyphaData) uploadHelp(hop *history.HistoryOp, hyphaName, ext string, originalFullPath *string, isOld bool, data []byte) *history.HistoryOp {
|
||||
var (
|
||||
fullPath = filepath.Join(WikiDir, hyphaName+ext)
|
||||
)
|
||||
if err := os.MkdirAll(filepath.Dir(fullPath), 0777); err != nil {
|
||||
return hop.WithError(err)
|
||||
}
|
||||
|
||||
if err := ioutil.WriteFile(fullPath, data, 0644); err != nil {
|
||||
return hop.WithError(err)
|
||||
}
|
||||
if isOld && *originalFullPath != fullPath && *originalFullPath != "" {
|
||||
if err := history.Rename(*originalFullPath, fullPath); err != nil {
|
||||
return hop.WithError(err)
|
||||
}
|
||||
log.Println("Move", *originalFullPath, "to", fullPath)
|
||||
}
|
||||
if !isOld {
|
||||
HyphaStorage[hyphaName] = hd
|
||||
}
|
||||
*originalFullPath = fullPath
|
||||
log.Printf("%v\n", *hd)
|
||||
return hop.WithFiles(fullPath).
|
||||
WithSignature("anon").
|
||||
Apply()
|
||||
}
|
||||
|
||||
// UploadText loads a new text part from `textData` for hypha `hyphaName` with `hd`. It must be marked if the hypha `isOld`.
|
||||
func (hd *HyphaData) UploadText(hyphaName, textData string, isOld bool) *history.HistoryOp {
|
||||
hop := history.Operation(history.TypeEditText).WithMsg(fmt.Sprintf("Edit ‘%s’", hyphaName))
|
||||
return hd.uploadHelp(hop, hyphaName, ".myco", &hd.textPath, isOld, []byte(textData))
|
||||
}
|
||||
|
||||
// UploadBinary loads a new binary part from `file` for hypha `hyphaName` with `hd`. The contents have the specified `mime` type. It must be marked if the hypha `isOld`.
|
||||
func (hd *HyphaData) UploadBinary(hyphaName, mime string, file multipart.File, isOld bool) *history.HistoryOp {
|
||||
var (
|
||||
hop = history.Operation(history.TypeEditBinary).WithMsg(fmt.Sprintf("Upload binary part for ‘%s’ with type ‘%s’", hyphaName, mime))
|
||||
data, err = ioutil.ReadAll(file)
|
||||
)
|
||||
if err != nil {
|
||||
return hop.WithError(err).Apply()
|
||||
}
|
||||
|
||||
return hd.uploadHelp(hop, hyphaName, MimeToExtension(mime), &hd.binaryPath, isOld, data)
|
||||
}
|
||||
|
||||
// DeleteHypha deletes hypha and makes a history record about that.
|
||||
@ -61,10 +107,13 @@ func findHyphaeToRename(hyphaName string, recursive bool) []string {
|
||||
return hyphae
|
||||
}
|
||||
|
||||
func renamingPairs(hyphaNames []string, replaceName func(string) string) map[string]string {
|
||||
func renamingPairs(hyphaNames []string, replaceName func(string) string) (map[string]string, error) {
|
||||
renameMap := make(map[string]string)
|
||||
for _, hn := range hyphaNames {
|
||||
if hd, ok := HyphaStorage[hn]; ok {
|
||||
if _, nameUsed := HyphaStorage[replaceName(hn)]; nameUsed {
|
||||
return nil, errors.New("Hypha " + replaceName(hn) + " already exists")
|
||||
}
|
||||
if hd.textPath != "" {
|
||||
renameMap[hd.textPath] = replaceName(hd.textPath)
|
||||
}
|
||||
@ -73,7 +122,7 @@ func renamingPairs(hyphaNames []string, replaceName func(string) string) map[str
|
||||
}
|
||||
}
|
||||
}
|
||||
return renameMap
|
||||
return renameMap, nil
|
||||
}
|
||||
|
||||
// word Data is plural here
|
||||
@ -95,10 +144,14 @@ func (hd *HyphaData) RenameHypha(hyphaName, newName string, recursive bool) *his
|
||||
return strings.Replace(str, hyphaName, newName, 1)
|
||||
}
|
||||
hyphaNames = findHyphaeToRename(hyphaName, recursive)
|
||||
renameMap = renamingPairs(hyphaNames, replaceName)
|
||||
renameMap, err = renamingPairs(hyphaNames, replaceName)
|
||||
renameMsg = "Rename ‘%s’ to ‘%s’"
|
||||
hop = history.Operation(history.TypeRenameHypha)
|
||||
)
|
||||
if err != nil {
|
||||
hop.Errs = append(hop.Errs, err)
|
||||
return hop
|
||||
}
|
||||
if recursive {
|
||||
renameMsg += " recursively"
|
||||
}
|
||||
@ -113,14 +166,14 @@ func (hd *HyphaData) RenameHypha(hyphaName, newName string, recursive bool) *his
|
||||
}
|
||||
|
||||
// binaryHtmlBlock creates an html block for binary part of the hypha.
|
||||
func binaryHtmlBlock(hyphaName string, d *HyphaData) string {
|
||||
switch d.binaryType {
|
||||
case BinaryJpeg, BinaryGif, BinaryPng, BinaryWebp, BinarySvg, BinaryIco:
|
||||
func binaryHtmlBlock(hyphaName string, hd *HyphaData) string {
|
||||
switch filepath.Ext(hd.binaryPath) {
|
||||
case ".jpg", ".gif", ".png", ".webp", ".svg", ".ico":
|
||||
return fmt.Sprintf(`
|
||||
<div class="binary-container binary-container_with-img">
|
||||
<img src="/binary/%s"/>
|
||||
<a href="/page/%[1]s"><img src="/binary/%[1]s"/></a>
|
||||
</div>`, hyphaName)
|
||||
case BinaryOgg, BinaryWebm, BinaryMp4:
|
||||
case ".ogg", ".webm", ".mp4":
|
||||
return fmt.Sprintf(`
|
||||
<div class="binary-container binary-container_with-video">
|
||||
<video>
|
||||
@ -128,7 +181,7 @@ func binaryHtmlBlock(hyphaName string, d *HyphaData) string {
|
||||
<p>Your browser does not support video. See video's <a href="/binary/%[1]s">direct url</a></p>
|
||||
</video>
|
||||
`, hyphaName)
|
||||
case BinaryMp3:
|
||||
case ".mp3":
|
||||
return fmt.Sprintf(`
|
||||
<div class="binary-container binary-container_with-audio">
|
||||
<audio>
|
||||
@ -153,30 +206,36 @@ func Index(path string) {
|
||||
}
|
||||
|
||||
for _, node := range nodes {
|
||||
// If this hypha looks like it can be a hypha path, go deeper
|
||||
if node.IsDir() && isCanonicalName(node.Name()) {
|
||||
// If this hypha looks like it can be a hypha path, go deeper. Do not touch the .git and static folders for they have an admnistrative importance!
|
||||
if node.IsDir() && isCanonicalName(node.Name()) && node.Name() != ".git" && node.Name() != "static" {
|
||||
Index(filepath.Join(path, node.Name()))
|
||||
continue
|
||||
}
|
||||
|
||||
hyphaPartFilename := filepath.Join(path, node.Name())
|
||||
skip, hyphaName, isText, mimeId := DataFromFilename(hyphaPartFilename)
|
||||
if !skip {
|
||||
var (
|
||||
hyphaPartPath = filepath.Join(path, node.Name())
|
||||
hyphaName, isText, skip = DataFromFilename(hyphaPartPath)
|
||||
hyphaData *HyphaData
|
||||
ok bool
|
||||
)
|
||||
if hyphaData, ok = HyphaStorage[hyphaName]; !ok {
|
||||
if !skip {
|
||||
// Reuse the entry for existing hyphae, create a new one for those that do not exist yet.
|
||||
if hd, ok := HyphaStorage[hyphaName]; ok {
|
||||
hyphaData = hd
|
||||
} else {
|
||||
hyphaData = &HyphaData{}
|
||||
HyphaStorage[hyphaName] = hyphaData
|
||||
}
|
||||
if isText {
|
||||
hyphaData.textPath = hyphaPartFilename
|
||||
hyphaData.textType = TextType(mimeId)
|
||||
hyphaData.textPath = hyphaPartPath
|
||||
} else {
|
||||
hyphaData.binaryPath = hyphaPartFilename
|
||||
hyphaData.binaryType = BinaryType(mimeId)
|
||||
// Notify the user about binary part collisions. It's a design decision to just use any of them, it's the user's fault that they have screwed up the folder structure, but the engine should at least let them know, right?
|
||||
if hyphaData.binaryPath != "" {
|
||||
log.Println("There is a file collision for binary part of a hypha:", hyphaData.binaryPath, "and", hyphaPartPath, "-- going on with the latter")
|
||||
}
|
||||
hyphaData.binaryPath = hyphaPartPath
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
31
main.go
31
main.go
@ -29,7 +29,7 @@ var HyphaStorage = make(map[string]*HyphaData)
|
||||
|
||||
// IterateHyphaNamesWith is a closure to be passed to subpackages to let them iterate all hypha names read-only.
|
||||
func IterateHyphaNamesWith(f func(string)) {
|
||||
for hyphaName, _ := range HyphaStorage {
|
||||
for hyphaName := range HyphaStorage {
|
||||
f(hyphaName)
|
||||
}
|
||||
}
|
||||
@ -40,7 +40,7 @@ func HttpErr(w http.ResponseWriter, status int, name, title, errMsg string) {
|
||||
w.Header().Set("Content-Type", "text/html;charset=utf-8")
|
||||
w.WriteHeader(status)
|
||||
fmt.Fprint(w, base(title, fmt.Sprintf(
|
||||
`<p>%s. <a href="/page/%s">Go back to the hypha.<a></p>`,
|
||||
`<main><p>%s. <a href="/page/%s">Go back to the hypha.<a></p></main>`,
|
||||
errMsg, name)))
|
||||
}
|
||||
|
||||
@ -52,7 +52,7 @@ func handlerList(w http.ResponseWriter, rq *http.Request) {
|
||||
pageCount = len(HyphaStorage)
|
||||
)
|
||||
for hyphaName, data := range HyphaStorage {
|
||||
tbody += templates.HyphaListRowHTML(hyphaName, data.binaryType.Mime(), data.binaryPath != "")
|
||||
tbody += templates.HyphaListRowHTML(hyphaName, ExtensionToMime(filepath.Ext(data.binaryPath)), data.binaryPath != "")
|
||||
}
|
||||
util.HTTP200Page(w, base("List of pages", templates.HyphaListHTML(tbody, pageCount)))
|
||||
}
|
||||
@ -92,22 +92,26 @@ func handlerRecentChanges(w http.ResponseWriter, rq *http.Request) {
|
||||
noPrefix = strings.TrimPrefix(rq.URL.String(), "/recent-changes/")
|
||||
n, err = strconv.Atoi(noPrefix)
|
||||
)
|
||||
if err == nil {
|
||||
if err == nil && n < 101 {
|
||||
util.HTTP200Page(w, base(strconv.Itoa(n)+" recent changes", history.RecentChanges(n)))
|
||||
} else {
|
||||
http.Redirect(w, rq, "/recent-changes/20", http.StatusSeeOther)
|
||||
}
|
||||
}
|
||||
|
||||
func handlerStyle(w http.ResponseWriter, rq *http.Request) {
|
||||
log.Println(rq.URL)
|
||||
if _, err := os.Stat(WikiDir + "/static/common.css"); err == nil {
|
||||
http.ServeFile(w, rq, WikiDir+"/static/common.css")
|
||||
} else {
|
||||
w.Header().Set("Content-Type", "text/css;charset=utf-8")
|
||||
w.Write([]byte(templates.DefaultCSS()))
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
log.Println("Running MycorrhizaWiki β")
|
||||
|
||||
var err error
|
||||
WikiDir, err = filepath.Abs(os.Args[1])
|
||||
util.WikiDir = WikiDir
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
parseCliArgs()
|
||||
if err := os.Chdir(WikiDir); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
@ -128,8 +132,9 @@ func main() {
|
||||
http.HandleFunc("/favicon.ico", func(w http.ResponseWriter, rq *http.Request) {
|
||||
http.ServeFile(w, rq, WikiDir+"/static/favicon.ico")
|
||||
})
|
||||
http.HandleFunc("/static/common.css", handlerStyle)
|
||||
http.HandleFunc("/", func(w http.ResponseWriter, rq *http.Request) {
|
||||
http.Redirect(w, rq, "/page/home", http.StatusSeeOther)
|
||||
http.Redirect(w, rq, "/page/"+util.HomePage, http.StatusSeeOther)
|
||||
})
|
||||
log.Fatal(http.ListenAndServe("0.0.0.0:1737", nil))
|
||||
log.Fatal(http.ListenAndServe("0.0.0.0:"+util.ServerPort, nil))
|
||||
}
|
||||
|
140
markup/img.go
Normal file
140
markup/img.go
Normal file
@ -0,0 +1,140 @@
|
||||
package markup
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var imgRe = regexp.MustCompile(`^img\s+{`)
|
||||
|
||||
func MatchesImg(line string) bool {
|
||||
return imgRe.MatchString(line)
|
||||
}
|
||||
|
||||
type imgEntry struct {
|
||||
path string
|
||||
sizeH string
|
||||
sizeV string
|
||||
desc string
|
||||
}
|
||||
|
||||
type Img struct {
|
||||
entries []imgEntry
|
||||
inDesc bool
|
||||
hyphaName string
|
||||
}
|
||||
|
||||
func (img *Img) Process(line string) (shouldGoBackToNormal bool) {
|
||||
if img.inDesc {
|
||||
rightBraceIndex := strings.IndexRune(line, '}')
|
||||
if cnt := len(img.entries); rightBraceIndex == -1 && cnt != 0 {
|
||||
img.entries[cnt-1].desc += "\n" + line
|
||||
} else if rightBraceIndex != -1 && cnt != 0 {
|
||||
img.entries[cnt-1].desc += "\n" + line[:rightBraceIndex]
|
||||
img.inDesc = false
|
||||
}
|
||||
if strings.Count(line, "}") > 1 {
|
||||
return true
|
||||
}
|
||||
} else if s := strings.TrimSpace(line); s != "" {
|
||||
if s[0] == '}' {
|
||||
return true
|
||||
}
|
||||
img.parseStartOfEntry(line)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func ImgFromFirstLine(line, hyphaName string) Img {
|
||||
img := Img{
|
||||
hyphaName: hyphaName,
|
||||
entries: make([]imgEntry, 0),
|
||||
}
|
||||
line = line[strings.IndexRune(line, '{'):]
|
||||
if len(line) == 1 { // if { only
|
||||
} else {
|
||||
line = line[1:] // Drop the {
|
||||
}
|
||||
return img
|
||||
}
|
||||
|
||||
func (img *Img) canonicalPathFor(path string) string {
|
||||
path = strings.TrimSpace(path)
|
||||
if strings.IndexRune(path, ':') != -1 || strings.IndexRune(path, '/') == 0 {
|
||||
return path
|
||||
} else {
|
||||
return "/binary/" + xclCanonicalName(img.hyphaName, path)
|
||||
}
|
||||
}
|
||||
|
||||
func (img *Img) parseStartOfEntry(line string) (entry imgEntry, followedByDesc bool) {
|
||||
pipeIndex := strings.IndexRune(line, '|')
|
||||
if pipeIndex == -1 { // If no : in string
|
||||
entry.path = img.canonicalPathFor(line)
|
||||
} else {
|
||||
entry.path = img.canonicalPathFor(line[:pipeIndex])
|
||||
line = strings.TrimPrefix(line, line[:pipeIndex+1])
|
||||
|
||||
var (
|
||||
leftBraceIndex = strings.IndexRune(line, '{')
|
||||
rightBraceIndex = strings.IndexRune(line, '}')
|
||||
dimensions string
|
||||
)
|
||||
|
||||
if leftBraceIndex == -1 {
|
||||
dimensions = line
|
||||
} else {
|
||||
dimensions = line[:leftBraceIndex]
|
||||
}
|
||||
|
||||
sizeH, sizeV := parseDimensions(dimensions)
|
||||
entry.sizeH = sizeH
|
||||
entry.sizeV = sizeV
|
||||
|
||||
if leftBraceIndex != -1 && rightBraceIndex == -1 {
|
||||
img.inDesc = true
|
||||
followedByDesc = true
|
||||
entry.desc = strings.TrimPrefix(line, line[:leftBraceIndex+1])
|
||||
} else if leftBraceIndex != -1 && rightBraceIndex != -1 {
|
||||
entry.desc = line[leftBraceIndex+1 : rightBraceIndex]
|
||||
}
|
||||
}
|
||||
img.entries = append(img.entries, entry)
|
||||
return
|
||||
}
|
||||
|
||||
func parseDimensions(dimensions string) (sizeH, sizeV string) {
|
||||
xIndex := strings.IndexRune(dimensions, '*')
|
||||
if xIndex == -1 { // If no x in dimensions
|
||||
sizeH = strings.TrimSpace(dimensions)
|
||||
} else {
|
||||
sizeH = strings.TrimSpace(dimensions[:xIndex])
|
||||
sizeV = strings.TrimSpace(strings.TrimPrefix(dimensions, dimensions[:xIndex+1]))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (img Img) ToHtml() (html string) {
|
||||
for _, entry := range img.entries {
|
||||
html += fmt.Sprintf(`<figure>
|
||||
<img src="%s" width="%s" height="%s">
|
||||
`, entry.path, entry.sizeH, entry.sizeV)
|
||||
if entry.desc != "" {
|
||||
html += ` <figcaption>`
|
||||
for i, line := range strings.Split(entry.desc, "\n") {
|
||||
if line != "" {
|
||||
if i > 0 {
|
||||
html += `<br>`
|
||||
}
|
||||
html += ParagraphToHtml(img.hyphaName, line)
|
||||
}
|
||||
}
|
||||
html += `</figcaption>`
|
||||
}
|
||||
html += `</figure>`
|
||||
}
|
||||
return `<section class="img-gallery">
|
||||
` + html + `
|
||||
</section>`
|
||||
}
|
47
markup/img_test.go
Normal file
47
markup/img_test.go
Normal file
@ -0,0 +1,47 @@
|
||||
package markup
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParseStartOfEntry(t *testing.T) {
|
||||
img := ImgFromFirstLine("img {", "h")
|
||||
tests := []struct {
|
||||
line string
|
||||
entry imgEntry
|
||||
followedByDesc bool
|
||||
}{
|
||||
{"apple", imgEntry{"/binary/apple", "", "", ""}, false},
|
||||
{"pear|", imgEntry{"/binary/pear", "", "", ""}, false},
|
||||
{"яблоко| 30*60", imgEntry{"/binary/яблоко", "30", "60", ""}, false},
|
||||
{"груша | 65 ", imgEntry{"/binary/груша", "65", "", ""}, false},
|
||||
{"жеронимо | 30 { full desc }", imgEntry{"/binary/жеронимо", "30", "", " full desc "}, false},
|
||||
{"жорно жованна | *5555 {partial description", imgEntry{"/binary/жорно_жованна", "", "5555", "partial description"}, true},
|
||||
{"иноске | {full}", imgEntry{"/binary/иноске", "", "", "full"}, false},
|
||||
{"j|{partial", imgEntry{"/binary/j", "", "", "partial"}, true},
|
||||
}
|
||||
for _, triplet := range tests {
|
||||
entry, followedByDesc := img.parseStartOfEntry(triplet.line)
|
||||
if entry != triplet.entry || followedByDesc != triplet.followedByDesc {
|
||||
t.Error(fmt.Sprintf("%q:%q != %q; %v != %v", triplet.line, entry, triplet.entry, followedByDesc, triplet.followedByDesc))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseDimensions(t *testing.T) {
|
||||
tests := [][]string{
|
||||
{"500", "500", ""},
|
||||
{"3em", "3em", ""},
|
||||
{"500*", "500", ""},
|
||||
{"*500", "", "500"},
|
||||
{"800*520", "800", "520"},
|
||||
{"17%*5rem", "17%", "5rem"},
|
||||
}
|
||||
for _, triplet := range tests {
|
||||
sizeH, sizeV := parseDimensions(triplet[0])
|
||||
if sizeH != triplet[1] || sizeV != triplet[2] {
|
||||
t.Error(sizeH, "*", sizeV, " != ", triplet[1], "*", triplet[2])
|
||||
}
|
||||
}
|
||||
}
|
@ -1,9 +1,8 @@
|
||||
package gemtext
|
||||
package markup
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html"
|
||||
"path"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@ -13,7 +12,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
|
||||
@ -21,6 +20,8 @@ type GemLexerState struct {
|
||||
// Line id
|
||||
id int
|
||||
buf string
|
||||
// Temporaries
|
||||
img Img
|
||||
}
|
||||
|
||||
type Line struct {
|
||||
@ -29,47 +30,6 @@ type Line struct {
|
||||
contents interface{}
|
||||
}
|
||||
|
||||
// Parse gemtext 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))
|
||||
if src == "" {
|
||||
return
|
||||
}
|
||||
// Href is text after => till first whitespace
|
||||
href = strings.Fields(src)[0]
|
||||
// Text is everything after whitespace.
|
||||
// If there's no text, make it same as href
|
||||
if text = strings.TrimPrefix(src, href); text == "" {
|
||||
text = href
|
||||
}
|
||||
|
||||
class = "wikilink_internal"
|
||||
|
||||
switch {
|
||||
case strings.HasPrefix(href, "./"):
|
||||
hyphaName := canonicalName(path.Join(
|
||||
state.name, strings.TrimPrefix(href, "./")))
|
||||
if !HyphaExists(hyphaName) {
|
||||
class = "wikilink_new"
|
||||
}
|
||||
href = path.Join("/page", hyphaName)
|
||||
case strings.HasPrefix(href, "../"):
|
||||
hyphaName := canonicalName(path.Join(
|
||||
path.Dir(state.name), strings.TrimPrefix(href, "../")))
|
||||
if !HyphaExists(hyphaName) {
|
||||
class = "wikilink_new"
|
||||
}
|
||||
href = path.Join("/page", hyphaName)
|
||||
case strings.HasPrefix(href, "/"):
|
||||
case strings.ContainsRune(href, ':'):
|
||||
class = "wikilink_external"
|
||||
default:
|
||||
href = path.Join("/page", href)
|
||||
}
|
||||
return href, strings.TrimSpace(text), class
|
||||
}
|
||||
|
||||
func lex(name, content string) (ast []Line) {
|
||||
var state = GemLexerState{name: name}
|
||||
|
||||
@ -79,7 +39,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})
|
||||
@ -89,6 +49,9 @@ func geminiLineToAST(line string, state *GemLexerState, ast *[]Line) {
|
||||
if state.where == "list" {
|
||||
state.where = ""
|
||||
addLine(state.buf + "</ul>")
|
||||
} else if state.where == "number" {
|
||||
state.where = ""
|
||||
addLine(state.buf + "</ol>")
|
||||
}
|
||||
return
|
||||
}
|
||||
@ -99,14 +62,25 @@ func geminiLineToAST(line string, state *GemLexerState, ast *[]Line) {
|
||||
|
||||
// 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 "pre":
|
||||
goto preformattedState
|
||||
case "list":
|
||||
goto listState
|
||||
case "number":
|
||||
goto numberState
|
||||
default:
|
||||
goto normalState
|
||||
}
|
||||
|
||||
imgState:
|
||||
if shouldGoBackToNormal := state.img.Process(line); shouldGoBackToNormal {
|
||||
state.where = ""
|
||||
addLine(state.img)
|
||||
}
|
||||
return
|
||||
|
||||
preformattedState:
|
||||
switch {
|
||||
case startsWith("```"):
|
||||
@ -121,8 +95,8 @@ preformattedState:
|
||||
|
||||
listState:
|
||||
switch {
|
||||
case startsWith("*"):
|
||||
state.buf += fmt.Sprintf("\t<li>%s</li>\n", remover("*")(line))
|
||||
case startsWith("* "):
|
||||
state.buf += fmt.Sprintf("\t<li>%s</li>\n", ParagraphToHtml(state.name, line[2:]))
|
||||
case startsWith("```"):
|
||||
state.where = "pre"
|
||||
addLine(state.buf + "</ul>")
|
||||
@ -135,6 +109,22 @@ listState:
|
||||
}
|
||||
return
|
||||
|
||||
numberState:
|
||||
switch {
|
||||
case startsWith("*. "):
|
||||
state.buf += fmt.Sprintf("\t<li>%s</li>\n", ParagraphToHtml(state.name, line[3:]))
|
||||
case startsWith("```"):
|
||||
state.where = "pre"
|
||||
addLine(state.buf + "</ol>")
|
||||
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 + "</ol>")
|
||||
goto normalState
|
||||
}
|
||||
return
|
||||
|
||||
normalState:
|
||||
state.id++
|
||||
switch {
|
||||
@ -142,32 +132,50 @@ 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("*. "):
|
||||
state.where = "number"
|
||||
state.buf = fmt.Sprintf("<ol id='%d'>\n", state.id)
|
||||
goto numberState
|
||||
|
||||
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(
|
||||
"<blockquote id='%d'>%s</blockquote>", state.id, remover(">")(line)))
|
||||
case startsWith("=>"):
|
||||
source, content, class := wikilink(line, state)
|
||||
href, text, class := Rocketlink(line, state.name)
|
||||
addLine(fmt.Sprintf(
|
||||
`<p><a id='%d' class='%s' href="%s">%s</a></p>`, state.id, class, source, content))
|
||||
`<p><a id='%d' class='rocketlink %s' href="%s">%s</a></p>`, state.id, class, href, text))
|
||||
|
||||
case startsWith("<="):
|
||||
addLine(parseTransclusion(line, state.name))
|
||||
case line == "----":
|
||||
*ast = append(*ast, Line{id: -1, contents: "<hr/>"})
|
||||
case MatchesImg(line):
|
||||
state.where = "img"
|
||||
state.img = ImgFromFirstLine(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(state.name, line)))
|
||||
}
|
||||
}
|
@ -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) {
|
||||
@ -19,15 +19,15 @@ func TestLex(t *testing.T) {
|
||||
return
|
||||
}
|
||||
for i, e := range ast {
|
||||
if e != expectedAst[i] {
|
||||
t.Error("Mismatch when lexing", name, "\nExpected:", expectedAst[i], "\nGot:", e)
|
||||
if !reflect.DeepEqual(e, expectedAst[i]) {
|
||||
t.Error(fmt.Sprintf("Expected: %q\nGot:%q", expectedAst[i], e))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
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{
|
||||
@ -41,17 +41,27 @@ func TestLex(t *testing.T) {
|
||||
</ul>`},
|
||||
{6, "<p id='6'>text</p>"},
|
||||
{7, "<p id='7'>more text</p>"},
|
||||
{8, `<p><a id='8' class='wikilink_internal' href="/page/Pear">some link</a></p>`},
|
||||
{8, `<p><a id='8' class='rocketlink wikilink_internal' href="/page/Pear">some link</a></p>`},
|
||||
{9, `<ul id='9'>
|
||||
<li>li\n"+</li>
|
||||
<li>lin"+</li>
|
||||
</ul>`},
|
||||
{10, `<pre id='10' alt='alt text goes here' class='codeblock'><code>=> preformatted text
|
||||
where gemtext is not lexed</code></pre>`},
|
||||
{11, `<p><a id='11' class='wikilink_internal' href="/page/linking">linking</a></p>`},
|
||||
where markup is not lexed</code></pre>`},
|
||||
{11, `<p><a id='11' class='rocketlink wikilink_internal' href="/page/linking">linking</a></p>`},
|
||||
{12, "<p id='12'>text</p>"},
|
||||
{13, `<pre id='13' alt='' class='codeblock'><code>()
|
||||
/\</code></pre>`},
|
||||
// More thorough testing of xclusions is done in xclusion_test.go
|
||||
{14, Transclusion{"apple", 1, 3}},
|
||||
{15, Img{
|
||||
hyphaName: "Apple",
|
||||
inDesc: false,
|
||||
entries: []imgEntry{
|
||||
{"/binary/hypha1", "", "", ""},
|
||||
{"/binary/hypha2", "", "", ""},
|
||||
{"/binary/hypha3", "60", "", ""},
|
||||
{"/binary/hypha4", "", "", " line1\nline2\n"},
|
||||
{"/binary/hypha5", "", "", "\nstate of minnesota\n"},
|
||||
},
|
||||
}},
|
||||
})
|
||||
}
|
49
markup/link.go
Normal file
49
markup/link.go
Normal file
@ -0,0 +1,49 @@
|
||||
package markup
|
||||
|
||||
import (
|
||||
"path"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// LinkParts determines what href, text and class should resulting <a> have based on mycomarkup's addr, display and hypha name.
|
||||
//
|
||||
// => addr display
|
||||
// [[addr|display]]
|
||||
func LinkParts(addr, display, hyphaName string) (href, text, class string) {
|
||||
if display == "" {
|
||||
text = addr
|
||||
} else {
|
||||
text = strings.TrimSpace(display)
|
||||
}
|
||||
class = "wikilink_internal"
|
||||
|
||||
switch {
|
||||
case strings.ContainsRune(addr, ':'):
|
||||
return addr, text, "wikilink_external"
|
||||
case strings.HasPrefix(addr, "/"):
|
||||
return addr, text, class
|
||||
case strings.HasPrefix(addr, "./"):
|
||||
hyphaName = canonicalName(path.Join(hyphaName, addr[2:]))
|
||||
case strings.HasPrefix(addr, "../"):
|
||||
hyphaName = canonicalName(path.Join(path.Dir(hyphaName), addr[3:]))
|
||||
default:
|
||||
hyphaName = canonicalName(addr)
|
||||
}
|
||||
if !HyphaExists(hyphaName) {
|
||||
class += " wikilink_new"
|
||||
}
|
||||
return "/page/" + hyphaName, text, class
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
102
markup/mycomarkup.go
Normal file
102
markup/mycomarkup.go
Normal 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{}
|
||||
}
|
159
markup/paragraph.go
Normal file
159
markup/paragraph.go
Normal file
@ -0,0 +1,159 @@
|
||||
package markup
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"html"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type spanTokenType int
|
||||
|
||||
const (
|
||||
spanTextNode = iota
|
||||
spanItalic
|
||||
spanBold
|
||||
spanMono
|
||||
spanSuper
|
||||
spanSub
|
||||
spanMark
|
||||
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) string {
|
||||
input.Next(2)
|
||||
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 b == '|' && currBuf == &addrBuf {
|
||||
currBuf = &displayBuf
|
||||
} else if b == ']' && bytes.HasPrefix(input.Bytes(), []byte{']'}) {
|
||||
input.Next(1)
|
||||
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
|
||||
)
|
||||
// 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(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))
|
||||
}
|
||||
)
|
||||
|
||||
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(getLinkNode(p, hyphaName))
|
||||
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 spanLink:
|
||||
ret.WriteString(tagFromState(spanLink, tagState, "a", "[["))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ret.String()
|
||||
}
|
49
markup/paragraph_test.go
Normal file
49
markup/paragraph_test.go
Normal file
@ -0,0 +1,49 @@
|
||||
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</strong>"},
|
||||
{"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."},
|
||||
{"A [[simple]] link", `A <a href="/page/simple" class="wikilink_internal">simple</a> link`},
|
||||
}
|
||||
for _, test := range tests {
|
||||
if ParagraphToHtml("Apple", test[0]) != test[1] {
|
||||
t.Error(fmt.Sprintf("%q: Wanted %q, got %q", test[0], test[1], ParagraphToHtml("Apple", test[0])))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
HyphaExists = func(_ string) bool { return true }
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package gemtext
|
||||
package markup
|
||||
|
||||
import ()
|
||||
|
||||
@ -13,10 +13,12 @@ func Parse(ast []Line, from, to int, state GemParserState) (html string) {
|
||||
return "Transclusion depth limit"
|
||||
}
|
||||
for _, line := range ast {
|
||||
if line.id >= from && (line.id <= to || to == 0) {
|
||||
if line.id >= from && (line.id <= to || to == 0) || line.id == -1 {
|
||||
switch v := line.contents.(type) {
|
||||
case Transclusion:
|
||||
html += Transclude(v, state)
|
||||
case Img:
|
||||
html += v.ToHtml()
|
||||
case string:
|
||||
html += v
|
||||
}
|
@ -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
|
||||
|
||||
@ -22,3 +22,17 @@ text
|
||||
/\
|
||||
```
|
||||
<= Apple : 1..3
|
||||
|
||||
img {
|
||||
hypha1
|
||||
hypha2|
|
||||
hypha3| 60
|
||||
hypha4| { line1
|
||||
line2
|
||||
} this is ignored
|
||||
|
||||
hypha5| {
|
||||
state of minnesota
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
package gemtext
|
||||
package markup
|
||||
|
||||
import (
|
||||
"strings"
|
@ -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
|
@ -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 2c0e43199ed28f7022a38463a0eec3af3ecb03c9
|
||||
Subproject commit d78a90718ae9c36f7a114901ddad84b0e23221b3
|
138
mime.go
138
mime.go
@ -5,104 +5,58 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// TextType is content type of text part of a hypha.
|
||||
type TextType int
|
||||
|
||||
const (
|
||||
// TextPlain is default text content type.
|
||||
TextPlain TextType = iota
|
||||
// TextGemini is content type for MycorrhizaWiki's dialect of gemtext.
|
||||
TextGemini
|
||||
)
|
||||
|
||||
// Mime returns mime type representation of `t`.
|
||||
func (t TextType) Mime() string {
|
||||
return [...]string{"text/plain", "text/gemini"}[t]
|
||||
}
|
||||
|
||||
// Extension returns extension (with dot) to be used for files with content type `t`.
|
||||
func (t TextType) Extension() string {
|
||||
return [...]string{".txt", ".gmi"}[t]
|
||||
}
|
||||
|
||||
// BinaryType is content type of binary part of a hypha.
|
||||
type BinaryType int
|
||||
|
||||
// Supported binary content types
|
||||
const (
|
||||
// BinaryOctet is default binary content type.
|
||||
BinaryOctet BinaryType = iota
|
||||
BinaryJpeg
|
||||
BinaryGif
|
||||
BinaryPng
|
||||
BinaryWebp
|
||||
BinarySvg
|
||||
BinaryIco
|
||||
BinaryOgg
|
||||
BinaryWebm
|
||||
BinaryMp3
|
||||
BinaryMp4
|
||||
)
|
||||
|
||||
var binaryMimes = [...]string{
|
||||
"application/octet-stream",
|
||||
"image/jpeg", "image/gif", "image/png", "image/webp",
|
||||
"image/svg+xml", "image/x-icon",
|
||||
"application/ogg", "video/webm", "audio/mp3", "video/mp4",
|
||||
}
|
||||
|
||||
// Mime returns mime type representation of `t`.
|
||||
func (t BinaryType) Mime() string {
|
||||
return binaryMimes[t]
|
||||
}
|
||||
|
||||
var binaryExtensions = [...]string{
|
||||
".bin", ".jpg", ".gif", ".png", ".webp", ".svg", ".ico",
|
||||
".ogg", ".webm", ".mp3", ".mp4",
|
||||
}
|
||||
|
||||
// Extension returns extension (with dot) to be used for files with content type `t`.
|
||||
func (t BinaryType) Extension() string {
|
||||
return binaryExtensions[t]
|
||||
}
|
||||
|
||||
// MimeToBinaryType converts mime type to BinaryType. If the mime type is not supported, BinaryOctet is returned as a fallback type.
|
||||
func MimeToBinaryType(mime string) BinaryType {
|
||||
for i, binaryMime := range binaryMimes {
|
||||
if binaryMime == mime {
|
||||
return BinaryType(i)
|
||||
func MimeToExtension(mime string) string {
|
||||
mm := map[string]string{
|
||||
"application/octet-stream": "bin",
|
||||
"image/jpeg": "jpg",
|
||||
"image/gif": "gif",
|
||||
"image/png": "png",
|
||||
"image/webp": "webp",
|
||||
"image/svg+xml": "svg",
|
||||
"image/x-icon": "ico",
|
||||
"application/ogg": "ogg",
|
||||
"video/webm": "webm",
|
||||
"audio/mp3": "mp3",
|
||||
"video/mp4": "mp4",
|
||||
}
|
||||
if ext, ok := mm[mime]; ok {
|
||||
return "." + ext
|
||||
}
|
||||
return BinaryOctet
|
||||
return ".bin"
|
||||
}
|
||||
|
||||
func ExtensionToMime(ext string) string {
|
||||
mm := map[string]string{
|
||||
".bin": "application/octet-stream",
|
||||
".jpg": "image/jpeg",
|
||||
".jpeg": "image/jpeg",
|
||||
".gif": "image/gif",
|
||||
".png": "image/png",
|
||||
".webp": "image/webp",
|
||||
".svg": "image/svg+xml",
|
||||
".ico": "image/x-icon",
|
||||
".ogg": "application/ogg",
|
||||
".webm": "video/webm",
|
||||
".mp3": "audio/mp3",
|
||||
".mp4": "video/mp4",
|
||||
}
|
||||
if mime, ok := mm[ext]; ok {
|
||||
return mime
|
||||
}
|
||||
return "application/octet-stream"
|
||||
}
|
||||
|
||||
// DataFromFilename fetches all meta information from hypha content file with path `fullPath`. If it is not a content file, `skip` is true, and you are expected to ignore this file when indexing hyphae. `name` is name of the hypha to which this file relates. `isText` is true when the content file is text, false when is binary. `mimeId` is an integer representation of content type. Cast it to TextType if `isText == true`, cast it to BinaryType if `isText == false`.
|
||||
func DataFromFilename(fullPath string) (skip bool, name string, isText bool, mimeId int) {
|
||||
func DataFromFilename(fullPath string) (name string, isText bool, skip bool) {
|
||||
shortPath := strings.TrimPrefix(fullPath, WikiDir)[1:]
|
||||
// Special files start with &
|
||||
// &. is used in normal hypha part names
|
||||
if shortPath[0] == '&' || strings.LastIndex(shortPath, "&.") < 0 {
|
||||
skip = true
|
||||
return
|
||||
}
|
||||
ext := filepath.Ext(shortPath)
|
||||
name = strings.TrimSuffix(shortPath, "&"+ext)
|
||||
isText, mimeId = mimeData(ext)
|
||||
name = CanonicalName(strings.TrimSuffix(shortPath, ext))
|
||||
switch ext {
|
||||
case ".myco":
|
||||
isText = true
|
||||
case "", shortPath:
|
||||
skip = true
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// mimeData determines what content type file has judging by its `ext`ension. `itText` and `mimeId` are the same as in DataFromFilename.
|
||||
func mimeData(ext string) (isText bool, mimeId int) {
|
||||
switch ext {
|
||||
case ".txt":
|
||||
return true, int(TextPlain)
|
||||
case ".gmi":
|
||||
return true, int(TextGemini)
|
||||
}
|
||||
for i, binExt := range binaryExtensions {
|
||||
if ext == binExt {
|
||||
return false, i
|
||||
}
|
||||
}
|
||||
return false, 0
|
||||
}
|
||||
|
9
name.go
9
name.go
@ -4,6 +4,8 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/bouncepaw/mycorrhiza/util"
|
||||
)
|
||||
|
||||
// isCanonicalName checks if the `name` is canonical.
|
||||
@ -17,16 +19,17 @@ func CanonicalName(name string) string {
|
||||
}
|
||||
|
||||
// naviTitle turns `canonicalName` into html string with each hypha path parts higlighted as links.
|
||||
// TODO: rework as a template
|
||||
func naviTitle(canonicalName string) string {
|
||||
var (
|
||||
html = `<h1 class="navi-title" id="0">
|
||||
<a href="/">🍄</a>`
|
||||
html = fmt.Sprintf(`<h1 class="navi-title" id="navi-title">
|
||||
<a href="/page/%s">%s</a>`, util.HomePage, util.SiteTitle)
|
||||
prevAcc = `/page/`
|
||||
parts = strings.Split(canonicalName, "/")
|
||||
)
|
||||
for _, part := range parts {
|
||||
html += fmt.Sprintf(`
|
||||
<span>/</span>
|
||||
<span aria-hidden="true">/</span>
|
||||
<a href="%s">%s</a>`,
|
||||
prevAcc+part,
|
||||
strings.Title(part))
|
||||
|
48
templates/css.qtpl
Normal file
48
templates/css.qtpl
Normal file
@ -0,0 +1,48 @@
|
||||
{% func DefaultCSS() %}
|
||||
@media screen and (min-width: 700px) {
|
||||
main {margin: 0 auto; width: 700px;}
|
||||
}
|
||||
@media screen and (max-width: 700px) {
|
||||
main {margin: 0; width: 100%;}
|
||||
}
|
||||
*, *::before, *::after {box-sizing: border-box;}
|
||||
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 > form {margin-bottom:1rem;}
|
||||
textarea {font-size:15px;}
|
||||
.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 code, .transclusion .codeblock {background-color:#ddd;}
|
||||
.transclusion {background-color:#eee; }
|
||||
.transclusion__content > *:not(.binary-container) {margin: 0.5rem; }
|
||||
.transclusion__link {display: block; text-align: right; font-style: italic; margin-top: 0.5rem; color: black; text-decoration: none;}
|
||||
.transclusion__link::before {content: "⇐ ";}
|
||||
|
||||
.binary-container_with-img img,
|
||||
.binary-container_with-video video,
|
||||
.binary-container_with-audio audio {width: 100%}
|
||||
.navi-title a {text-decoration:none;}
|
||||
.img-gallery img { max-width: 100%; }
|
||||
figure { margin: 0; }
|
||||
|
||||
nav ul {display:flex; padding-left:0; flex-wrap:wrap; margin-top:0;}
|
||||
nav ul li {list-style-type:none;margin-right:1rem;}
|
||||
|
||||
#new-name {width:100%;}
|
||||
|
||||
.rc-entry { display: grid; list-style-type: none; padding: .25rem; background-color: #eee; grid-template-columns: 1fr 1fr; }
|
||||
.rc-entry__time { font-style: italic; }
|
||||
.rc-entry__hash { font-style: italic; text-align: right; }
|
||||
.rc-entry__links { grid-column: 1 / span 2; }
|
||||
{% endfunc %}
|
98
templates/css.qtpl.go
Normal file
98
templates/css.qtpl.go
Normal file
@ -0,0 +1,98 @@
|
||||
// Code generated by qtc from "css.qtpl". DO NOT EDIT.
|
||||
// See https://github.com/valyala/quicktemplate for details.
|
||||
|
||||
//line templates/css.qtpl:1
|
||||
package templates
|
||||
|
||||
//line templates/css.qtpl:1
|
||||
import (
|
||||
qtio422016 "io"
|
||||
|
||||
qt422016 "github.com/valyala/quicktemplate"
|
||||
)
|
||||
|
||||
//line templates/css.qtpl:1
|
||||
var (
|
||||
_ = qtio422016.Copy
|
||||
_ = qt422016.AcquireByteBuffer
|
||||
)
|
||||
|
||||
//line templates/css.qtpl:1
|
||||
func StreamDefaultCSS(qw422016 *qt422016.Writer) {
|
||||
//line templates/css.qtpl:1
|
||||
qw422016.N().S(`
|
||||
@media screen and (min-width: 700px) {
|
||||
main {margin: 0 auto; width: 700px;}
|
||||
}
|
||||
@media screen and (max-width: 700px) {
|
||||
main {margin: 0; width: 100%;}
|
||||
}
|
||||
*, *::before, *::after {box-sizing: border-box;}
|
||||
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 > form {margin-bottom:1rem;}
|
||||
textarea {font-size:15px;}
|
||||
.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 code, .transclusion .codeblock {background-color:#ddd;}
|
||||
.transclusion {background-color:#eee; }
|
||||
.transclusion__content > *:not(.binary-container) {margin: 0.5rem; }
|
||||
.transclusion__link {display: block; text-align: right; font-style: italic; margin-top: 0.5rem; color: black; text-decoration: none;}
|
||||
.transclusion__link::before {content: "⇐ ";}
|
||||
|
||||
.binary-container_with-img img,
|
||||
.binary-container_with-video video,
|
||||
.binary-container_with-audio audio {width: 100%}
|
||||
.navi-title a {text-decoration:none;}
|
||||
.img-gallery img { max-width: 100%; }
|
||||
figure { margin: 0; }
|
||||
|
||||
nav ul {display:flex; padding-left:0; flex-wrap:wrap; margin-top:0;}
|
||||
nav ul li {list-style-type:none;margin-right:1rem;}
|
||||
|
||||
#new-name {width:100%;}
|
||||
|
||||
.rc-entry { display: grid; list-style-type: none; padding: .25rem; background-color: #eee; grid-template-columns: 1fr 1fr; }
|
||||
.rc-entry__time { font-style: italic; }
|
||||
.rc-entry__hash { font-style: italic; text-align: right; }
|
||||
.rc-entry__links { grid-column: 1 / span 2; }
|
||||
`)
|
||||
//line templates/css.qtpl:48
|
||||
}
|
||||
|
||||
//line templates/css.qtpl:48
|
||||
func WriteDefaultCSS(qq422016 qtio422016.Writer) {
|
||||
//line templates/css.qtpl:48
|
||||
qw422016 := qt422016.AcquireWriter(qq422016)
|
||||
//line templates/css.qtpl:48
|
||||
StreamDefaultCSS(qw422016)
|
||||
//line templates/css.qtpl:48
|
||||
qt422016.ReleaseWriter(qw422016)
|
||||
//line templates/css.qtpl:48
|
||||
}
|
||||
|
||||
//line templates/css.qtpl:48
|
||||
func DefaultCSS() string {
|
||||
//line templates/css.qtpl:48
|
||||
qb422016 := qt422016.AcquireByteBuffer()
|
||||
//line templates/css.qtpl:48
|
||||
WriteDefaultCSS(qb422016)
|
||||
//line templates/css.qtpl:48
|
||||
qs422016 := string(qb422016.B)
|
||||
//line templates/css.qtpl:48
|
||||
qt422016.ReleaseByteBuffer(qb422016)
|
||||
//line templates/css.qtpl:48
|
||||
return qs422016
|
||||
//line templates/css.qtpl:48
|
||||
}
|
@ -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/>
|
||||
|
@ -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)
|
||||
|
@ -1,6 +1,7 @@
|
||||
{% func RecentChangesHTML(changes []string, n int) %}
|
||||
<main class="recent-changes">
|
||||
<h1>Recent Changes</h1>
|
||||
<p><a href="/">← Back</a></p>
|
||||
|
||||
<nav class="recent-changes__count">
|
||||
See
|
||||
@ -29,7 +30,7 @@
|
||||
<p>Could not find any recent changes.</p>
|
||||
{% else %}
|
||||
{% for i, entry := range changes %}
|
||||
<ul class="recent-changes__entry" role="article"
|
||||
<ul class="recent-changes__entry rc-entry" role="article"
|
||||
aria-setsize="{%d n %}" aria-posinset="{%d i %}">
|
||||
{%s= entry %}
|
||||
</ul>
|
||||
|
@ -23,134 +23,135 @@ func StreamRecentChangesHTML(qw422016 *qt422016.Writer, changes []string, n int)
|
||||
qw422016.N().S(`
|
||||
<main class="recent-changes">
|
||||
<h1>Recent Changes</h1>
|
||||
<p><a href="/">← Back</a></p>
|
||||
|
||||
<nav class="recent-changes__count">
|
||||
See
|
||||
`)
|
||||
//line templates/recent_changes.qtpl:7
|
||||
//line templates/recent_changes.qtpl:8
|
||||
for _, m := range []int{20, 0, 50, 0, 100} {
|
||||
//line templates/recent_changes.qtpl:7
|
||||
//line templates/recent_changes.qtpl:8
|
||||
qw422016.N().S(`
|
||||
`)
|
||||
//line templates/recent_changes.qtpl:8
|
||||
//line templates/recent_changes.qtpl:9
|
||||
switch m {
|
||||
//line templates/recent_changes.qtpl:9
|
||||
//line templates/recent_changes.qtpl:10
|
||||
case 0:
|
||||
//line templates/recent_changes.qtpl:9
|
||||
//line templates/recent_changes.qtpl:10
|
||||
qw422016.N().S(`
|
||||
<span aria-hidden="true">|</span>
|
||||
`)
|
||||
//line templates/recent_changes.qtpl:11
|
||||
//line templates/recent_changes.qtpl:12
|
||||
case n:
|
||||
//line templates/recent_changes.qtpl:11
|
||||
//line templates/recent_changes.qtpl:12
|
||||
qw422016.N().S(`
|
||||
<b>`)
|
||||
//line templates/recent_changes.qtpl:12
|
||||
//line templates/recent_changes.qtpl:13
|
||||
qw422016.N().D(n)
|
||||
//line templates/recent_changes.qtpl:12
|
||||
//line templates/recent_changes.qtpl:13
|
||||
qw422016.N().S(`</b>
|
||||
`)
|
||||
//line templates/recent_changes.qtpl:13
|
||||
//line templates/recent_changes.qtpl:14
|
||||
default:
|
||||
//line templates/recent_changes.qtpl:13
|
||||
//line templates/recent_changes.qtpl:14
|
||||
qw422016.N().S(`
|
||||
<a href="/recent-changes/`)
|
||||
//line templates/recent_changes.qtpl:14
|
||||
//line templates/recent_changes.qtpl:15
|
||||
qw422016.N().D(m)
|
||||
//line templates/recent_changes.qtpl:14
|
||||
//line templates/recent_changes.qtpl:15
|
||||
qw422016.N().S(`">`)
|
||||
//line templates/recent_changes.qtpl:14
|
||||
//line templates/recent_changes.qtpl:15
|
||||
qw422016.N().D(m)
|
||||
//line templates/recent_changes.qtpl:14
|
||||
//line templates/recent_changes.qtpl:15
|
||||
qw422016.N().S(`</a>
|
||||
`)
|
||||
//line templates/recent_changes.qtpl:15
|
||||
//line templates/recent_changes.qtpl:16
|
||||
}
|
||||
//line templates/recent_changes.qtpl:15
|
||||
//line templates/recent_changes.qtpl:16
|
||||
qw422016.N().S(`
|
||||
`)
|
||||
//line templates/recent_changes.qtpl:16
|
||||
//line templates/recent_changes.qtpl:17
|
||||
}
|
||||
//line templates/recent_changes.qtpl:16
|
||||
//line templates/recent_changes.qtpl:17
|
||||
qw422016.N().S(`
|
||||
recent changes
|
||||
</nav>
|
||||
|
||||
`)
|
||||
//line templates/recent_changes.qtpl:25
|
||||
//line templates/recent_changes.qtpl:26
|
||||
qw422016.N().S(`
|
||||
|
||||
<section class="recent-changes__list" role="feed">
|
||||
`)
|
||||
//line templates/recent_changes.qtpl:28
|
||||
//line templates/recent_changes.qtpl:29
|
||||
if len(changes) == 0 {
|
||||
//line templates/recent_changes.qtpl:28
|
||||
//line templates/recent_changes.qtpl:29
|
||||
qw422016.N().S(`
|
||||
<p>Could not find any recent changes.</p>
|
||||
`)
|
||||
//line templates/recent_changes.qtpl:30
|
||||
//line templates/recent_changes.qtpl:31
|
||||
} else {
|
||||
//line templates/recent_changes.qtpl:30
|
||||
//line templates/recent_changes.qtpl:31
|
||||
qw422016.N().S(`
|
||||
`)
|
||||
//line templates/recent_changes.qtpl:31
|
||||
//line templates/recent_changes.qtpl:32
|
||||
for i, entry := range changes {
|
||||
//line templates/recent_changes.qtpl:31
|
||||
//line templates/recent_changes.qtpl:32
|
||||
qw422016.N().S(`
|
||||
<ul class="recent-changes__entry" role="article"
|
||||
<ul class="recent-changes__entry rc-entry" role="article"
|
||||
aria-setsize="`)
|
||||
//line templates/recent_changes.qtpl:33
|
||||
//line templates/recent_changes.qtpl:34
|
||||
qw422016.N().D(n)
|
||||
//line templates/recent_changes.qtpl:33
|
||||
//line templates/recent_changes.qtpl:34
|
||||
qw422016.N().S(`" aria-posinset="`)
|
||||
//line templates/recent_changes.qtpl:33
|
||||
//line templates/recent_changes.qtpl:34
|
||||
qw422016.N().D(i)
|
||||
//line templates/recent_changes.qtpl:33
|
||||
//line templates/recent_changes.qtpl:34
|
||||
qw422016.N().S(`">
|
||||
`)
|
||||
//line templates/recent_changes.qtpl:34
|
||||
//line templates/recent_changes.qtpl:35
|
||||
qw422016.N().S(entry)
|
||||
//line templates/recent_changes.qtpl:34
|
||||
//line templates/recent_changes.qtpl:35
|
||||
qw422016.N().S(`
|
||||
</ul>
|
||||
`)
|
||||
//line templates/recent_changes.qtpl:36
|
||||
//line templates/recent_changes.qtpl:37
|
||||
}
|
||||
//line templates/recent_changes.qtpl:36
|
||||
//line templates/recent_changes.qtpl:37
|
||||
qw422016.N().S(`
|
||||
`)
|
||||
//line templates/recent_changes.qtpl:37
|
||||
//line templates/recent_changes.qtpl:38
|
||||
}
|
||||
//line templates/recent_changes.qtpl:37
|
||||
//line templates/recent_changes.qtpl:38
|
||||
qw422016.N().S(`
|
||||
</section>
|
||||
</main>
|
||||
`)
|
||||
//line templates/recent_changes.qtpl:40
|
||||
//line templates/recent_changes.qtpl:41
|
||||
}
|
||||
|
||||
//line templates/recent_changes.qtpl:40
|
||||
//line templates/recent_changes.qtpl:41
|
||||
func WriteRecentChangesHTML(qq422016 qtio422016.Writer, changes []string, n int) {
|
||||
//line templates/recent_changes.qtpl:40
|
||||
//line templates/recent_changes.qtpl:41
|
||||
qw422016 := qt422016.AcquireWriter(qq422016)
|
||||
//line templates/recent_changes.qtpl:40
|
||||
//line templates/recent_changes.qtpl:41
|
||||
StreamRecentChangesHTML(qw422016, changes, n)
|
||||
//line templates/recent_changes.qtpl:40
|
||||
//line templates/recent_changes.qtpl:41
|
||||
qt422016.ReleaseWriter(qw422016)
|
||||
//line templates/recent_changes.qtpl:40
|
||||
//line templates/recent_changes.qtpl:41
|
||||
}
|
||||
|
||||
//line templates/recent_changes.qtpl:40
|
||||
//line templates/recent_changes.qtpl:41
|
||||
func RecentChangesHTML(changes []string, n int) string {
|
||||
//line templates/recent_changes.qtpl:40
|
||||
//line templates/recent_changes.qtpl:41
|
||||
qb422016 := qt422016.AcquireByteBuffer()
|
||||
//line templates/recent_changes.qtpl:40
|
||||
//line templates/recent_changes.qtpl:41
|
||||
WriteRecentChangesHTML(qb422016, changes, n)
|
||||
//line templates/recent_changes.qtpl:40
|
||||
//line templates/recent_changes.qtpl:41
|
||||
qs422016 := string(qb422016.B)
|
||||
//line templates/recent_changes.qtpl:40
|
||||
//line templates/recent_changes.qtpl:41
|
||||
qt422016.ReleaseByteBuffer(qb422016)
|
||||
//line templates/recent_changes.qtpl:40
|
||||
//line templates/recent_changes.qtpl:41
|
||||
return qs422016
|
||||
//line templates/recent_changes.qtpl:40
|
||||
//line templates/recent_changes.qtpl:41
|
||||
}
|
||||
|
@ -5,7 +5,12 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
var WikiDir string
|
||||
var (
|
||||
ServerPort string
|
||||
HomePage string
|
||||
SiteTitle string
|
||||
WikiDir string
|
||||
)
|
||||
|
||||
// ShorterPath is used by handlerList to display shorter path to the files. It simply strips WikiDir.
|
||||
func ShorterPath(path string) string {
|
||||
|
Loading…
Reference in New Issue
Block a user