micro wiki written in go

It’s been quite a while since I last blogged about writing in Google’s Go. The language itself has evolved considerably over that period of time, but my skills in it have basically stood still. Go, however, has not, especially its use around today’s contemporary software environments, such as containers. It’s also available as a regular installable package on Arch ARM for the Raspberry Pi 2/3.

I’ve been investigating network programming on the Raspberry Pi, looking for a minimalist set of tools that can provide robust connectivity and functionality. One of the tools I’ve been looking for is a web server or wiki. Turns out that there’s a Go tutorial that leads the reader through the steps necessary to create a minimal wiki. It’s located here: Writing Web Applications.

Once I finished the tutorial I followed through with the suggested simple tasks at the bottom of the tutorial. One of them was the ability to add WiKi markup to create tags within the body of any page I created. I followed Help:Wikitext for syntax and to see how regular Wiki handles markup. I implemented very little: links, headers, single unordered lists, italic and bold text. I felt that was enough to get started, and it’s enough to drop onto a Raspberry Pi as a bare executable. There are a few more key features I’d like to add, such as table support, but that’s for another round of coding. Now, I’m taking a break while I go off and do other tasks.

Here’s the code.

// Personal Wiki Sandbox.//// Original Copyright 2010 The Go Authors. All rights reserved.// Use of this source code is governed by a BSD-style// license that can be found in the LICENSE file.//package mainimport ("html/template""io/ioutil""net/http""os""regexp""fmt")import s "strings"var pr = fmt.Printlnconst dataDir = "data/"const dataType = ".txt"const templateDir = "templates/"type Page struct {Title   stringBody[]byteDisplayBody template.HTML}func (page *Page) save() error {os.Mkdir(dataDir, 0775)filename := dataDir + page.Title + dataTypereturn ioutil.WriteFile(filename, page.Body, 0600)}func loadPage(title string) (*Page, error) {filename := dataDir + title + dataTypebody, err := ioutil.ReadFile(filename)if err != nil {return nil, err}return &Page{Title: title, Body: body}, nil}// I'm a regular expression neophyte. If you can figure out what I'm doing and can offer a better way of writing// any of them, then I'm ready to learn.//var linkRegexp =   regexp.MustCompile( "\\[\\[([\\w ]+)\\]\\]" )var headingOne =   regexp.MustCompile( "^=(.*)=$" )var headingTwo =   regexp.MustCompile( "^==(.*)==$" )var headingThree = regexp.MustCompile( "^===(.*)===$" )var headingFour =  regexp.MustCompile( "^====(.*)====$" )var headingFive =  regexp.MustCompile( "^=====(.*)=====$" )var headingSix =   regexp.MustCompile( "^======(.*)======$" )var boldText = regexp.MustCompile( "'''(.*?)'''" )var italicText =   regexp.MustCompile( "''(.*?)''" )func expandHeading(line string) string {result := lineif headingSix.MatchString(line) {result = "<h6>" + s.TrimSpace(s.Trim(line, "=")) + "</h6>\n"} else if headingFive.MatchString(line) {result = "<h5>" + s.TrimSpace(s.Trim(line, "=")) + "</h5>\n"} else if headingFour.MatchString(line) {result = "<h4>" + s.TrimSpace(s.Trim(line, "=")) + "</h4>\n"} else if headingThree.MatchString(line) {result = "<h3>" + s.TrimSpace(s.Trim(line, "=")) + "</h3>\n"} else if headingTwo.MatchString(line) {result = "<h2 style=\"margin-bottom: 0.25em; border-bottom: 1px solid #808080;\">" +s.TrimSpace(s.Trim(line, "=")) + "</h2>\n"} else if headingOne.MatchString(line) {result = "<h1 style=\"margin-bottom: 0.25em; border-bottom: 2px solid #808080;\">" +s.TrimSpace(s.Trim(line, "=")) + "</h1>\n"}return result}func viewHandler(writer http.ResponseWriter, request *http.Request, title string) {page, err := loadPage(title)// If the page doesn't exist, then redirect to the editing function and create the page.//if err != nil {http.Redirect(writer, request, "/edit/"+title, http.StatusFound)return}// Start page processing.// Break on the new line (\n) because that's common to both Windows/DOS and Unix and Unix-like editors.// We'll strip off any carriage returns (\r) as white space.//lines := s.Split(string(page.Body), "\n")var buildingUnorderedList bool// expandedBody is where all the displayable text with HTML decorations will be collected.//var expandedBody []stringfor _, oneline := range lines {//// Strip all leading and trailing white space, including carriage returns (\r).// This will help later when we join all the strings back together using the HTML break token// followed by a new line (\n).//newLine := s.TrimSpace(oneline)// Bold and italicize any marked text. Try to handle the mixing of both.//newLine = boldText.ReplaceAllStringFunc(newLine,func(str string ) string {matched := boldText.FindStringSubmatch(str)return "<b>" + matched[1] + "</b>"})newLine = italicText.ReplaceAllStringFunc(newLine,func(str string ) string {matched := italicText.FindStringSubmatch(str)return "<i>" + matched[1] + "</i>"})// Create links from [[PageName]] pattern.// Note that we can accept a link with spaces. I substitute the '_' for a space. I could, perhaps,// use the %20 called for in the standards, but I decided underscores were better.//newLine = linkRegexp.ReplaceAllStringFunc(newLine,func(str string) string {matched := linkRegexp.FindStringSubmatch(str)return "<a href=\"/view/" + s.Replace(matched[1], " ", "_", -1) + "\">" + matched[1] + "</a>"})// newLine = expandHeading(newLine)// Look for unordered lists. If we find an unordered list notation, then start building that list.//if s.HasPrefix(newLine, "*") {nl2 := s.TrimSpace(s.SplitAfterN(newLine, "*", 2)[1])if !buildingUnorderedList {buildingUnorderedList = trueexpandedBody = append(expandedBody, "<ul>\n")}expandedBody = append(expandedBody, "<li>"+nl2+"</li>\n")continue} else if buildingUnorderedList {buildingUnorderedList = falseexpandedBody = append(expandedBody, "</ul>\n")}// Look for headings and the rest of the document.//if s.HasPrefix( newLine, "=" ) && s.HasSuffix( newLine, "=" ) {expandedBody = append(expandedBody, expandHeading(newLine))continue}expandedBody = append(expandedBody, newLine+"<br />\n")}// Rejoin all the lines we created with the initial split using he HTML break followed by a new line.//page.DisplayBody = template.HTML([]byte(s.Join(expandedBody, "")))renderTemplate(writer, "view", page)}func editHandler(writer http.ResponseWriter, request *http.Request, title string) {p, err := loadPage(title)if err != nil {p = &Page{Title: title}}renderTemplate(writer, "edit", p)}func saveHandler(writer http.ResponseWriter, request *http.Request, title string) {body := request.FormValue("body")p := &Page{Title: title, Body: []byte(body)}err := p.save()if err != nil {http.Error(writer, err.Error(), http.StatusInternalServerError)return}http.Redirect(writer, request, "/view/"+title, http.StatusFound)}var templates = template.Must(template.ParseFiles(templateDir+"edit.html", templateDir+"view.html"))func renderTemplate(writer http.ResponseWriter, tmpl string, p *Page) {err := templates.ExecuteTemplate(writer, tmpl+".html", p)if err != nil {http.Error(writer, err.Error(), http.StatusInternalServerError)}}func rootHandler(writer http.ResponseWriter, request *http.Request) {http.Redirect(writer, request, "/view/FrontPage", http.StatusFound)}var validPath = regexp.MustCompile("^/(edit|save|view)/([\\w ]+)$")func makeHandler(fn func(http.ResponseWriter, *http.Request, string)) http.HandlerFunc {return func(w http.ResponseWriter, r *http.Request) {m := validPath.FindStringSubmatch(r.URL.Path)if m == nil {http.NotFound(w, r)return}fn(w, r, m[2])}}func main() {http.HandleFunc("/", rootHandler)http.HandleFunc("/view/", makeHandler(viewHandler))http.HandleFunc("/edit/", makeHandler(editHandler))http.HandleFunc("/save/", makeHandler(saveHandler))http.ListenAndServe(":8080", nil)}

Here’s a typical markup page.

= Main Page =This is the front page. Basic testing of all the capabilities that can be added to a page of text with minimal Wiki markup support.= Heading 1 With ''Bloody === Equals'' In The Middle === Heading 2 ===== Heading 3 ======= Heading 4 ========= Heading 5 =========== Heading 6 ======Example testing of links.[[TestName1]] [[foobar]][[TestName2]][[Test Name 3]][[ANewPage]][[ANewPage2]]* one ''italic'' bulleted list entry with a link: [[ANewPage]]* two '''bold''' bulleted list entryExample of testing '''bold text multiple''' times in the '''same''' sentence.Example of testing ''italic '''text''' multiple'' times in the ''same'' sentence.This is ''' ''Italic and Bold text together.'' '''

And here’s what it looks like rendered in Chrome on a Macbook Pro.

Features

  1. Handles links with [[ and ]] notation. Also handles spaces within a page link.
  2. Handles italic and bold text, and combinations of the two.
  3. Handles headings, from h1 to h6.

Blank lines and lines that aren’t headings and part of an unordered list have a single HTML break appended to them. Other than that it’s just a way to organize text. I’m even writing a stripped down version that won’t allow you to edit any of the pages, for embedded read-only rendering.

raspian pixel

Just a quick entry about Raspian Pixel, the latest Debian-based distribution for the Raspberry Pi. In this entry, I have it running off of a 32GB microSDHC card, a Sandisk Ultra Extreme with an 80MB write speed, plugged into the Raspberry Pi 3. The Pixel desktop is something of a minor wonder, a reasonable graphical desktop that isn’t glacially slow. This entry is being written on the Pixel desktop within Chromium “Version 51.0.2704.91 Built on Ubuntu 14.04, running on Raspbian 8.0” according to the about screen. I’ve been dabbling with some of the more current distributions lately, specifically Raspbian and Fedora 25 for ARM.

Raspbian Pixel is a nice, clean, reasonably fast distribution for the Raspberry Pi 3. My only real complaint is that Raspbian, like Arch Linux ARM and Fedora 25, is still compiled for 32-bit ARM, not 64-bit. Other than that I can’t really complain. If anything, I have high praise for Pixel, especially its inclusion of Chromium/Chrome. Everything on the web I’ve attempted to view on Chromium renders as well as a regular Chrome on Windows, macOS, and Ubuntu. It handles multiple tabs, although with the limited memory on the RPi 3 I make sure to have as absolutely few open as possible.

I won’t be able to do anything of significance until after Christmas. But with a decent version of Raspbian on one of my RPi boxen, I now have a reference installation that will allow me to check to see if any of my more esoteric projects failures are due to me or the fact that Arch Linux doesn’t fully support what I’m trying to do.

Unfortunately, Fedora 25 has taken the place of the older versions of Raspian as the slowest, least usable distribution you can install on the Raspberry Pi 3. “Glacial” doesn’t even begin to describe how slow it is. After 30 frustrating minutes of dealing with “did it crash or is it just that slow,” I installed Raspbian Pixel over the top of it and moved on. I’ll admit Fedora looks pretty, but that’s no reason to keep it around. Ugly but highly functional will always beat pretty but slow as molasses in a New England winter. If you’re reading this and trying to make a decision, take my advice, and install Raspbian Pixel. Don’t even waste bandwidth and diskspace on Fedora 25 for ARM.