Merge branch 'no_carriage_returns_allowed' of asdf/gfu into master

This commit is contained in:
Sloom Sloum Sluom IV 2019-09-21 17:25:52 -04:00 committed by Gitea
commit ddd1c370be
2 changed files with 233 additions and 64 deletions

165
main.go
View File

@ -1,22 +1,26 @@
/*
gfu - gophermap format utility
`gfu` manipulates gophermaps (gopher menus). It is intended to be used as part of an automation chain for managing a gopherhole using maps as the main doctype, easily allowing any document to contain links.
`gfu` manipulates gophermaps (gopher menus). It is intended to be used as part
of an automation chain for managing a gopherhole using maps as the main
doctype, easily allowing any document to contain links.
`gfu` can:
- Convert all lines in a file that are not valid gopher links into gopher info text (item type 'i') lines
- Deconstructing all gopher info text lines in a file back to plain text for easy editing
- Convert all lines in a file that are not valid gopher links into gopher info
text (item type 'i') lines
- Deconstruct all gopher info text lines in a file back to plain text for easy
editing
There are also plans to include additional features, such as:
- Adding the contents of a header file into the gophermap
- Adding the contents of a footer file into the gophermap
*Please note - Many servers already support includes, so the above may not be needed. If you are interested in this feature, you may want to check your server documentation first.*
*
- Adding the contents of a footer file into the gophermap *Please note - many
servers already support includes, so the above may not be needed. If you are
interested in this feature, you may want to check your server documentation
first.*
Documentation
For information on using `gfu`, see the help information:
gfu --help
For information on using `gfu`, see the help information: gfu --help
Information on building, downloading and installing `gfu` can be found at:
https://tildegit.org/sloum/gfu
@ -31,87 +35,116 @@ import (
"bytes"
"flag"
"fmt"
"io"
"log"
"os"
"regexp"
"strings"
)
var outFile bytes.Buffer
//command line flag global variables
var (
deconstructInfoTextLines bool
header string
footer string
stdout bool
)
func errorExit(e error, msg string) {
if e != nil {
fmt.Print(msg)
os.Exit(1)
}
}
//regex global variable that identifies the format of gopher menu lines
var reGopherLines = regexp.MustCompile(`.+\t.*\t.*\t.*`)
func buildComment(ln string, eof bool) string {
func buildInfoText(ln string) string {
var out strings.Builder
if eof && ln == "" {
return out.String()
}
out.Grow(20 + len(ln))
out.WriteString("i")
out.WriteString(strings.TrimRight(ln, "\n\r"))
out.WriteString(ln)
out.WriteString("\tfalse\tnull.host\t1")
if !eof {
out.WriteString("\n")
}
return out.String()
}
func deconstructComment(ln string) string {
func deconstructInfoText(ln string) string {
text := strings.SplitN(ln, "\t", 2)
comment := text[0]
if len(comment) > 1 {
return comment[1:] + "\n"
infotext := text[0]
if len(infotext) > 1 {
return infotext[1:]
}
return "\n"
return ""
}
func readFile(path string, buildComments bool) {
//takes a string representing the file path, reads the file, then passes the
//data for processing. returns data as a bytes.Buffer and any error
//information.
func readFile(path string) (outFile bytes.Buffer, err error) {
file, err := os.Open(path)
errorExit(err, fmt.Sprintf("Unable to open file for reading: %s\n", path))
if err != nil {
return
}
defer file.Close()
re := regexp.MustCompile(`.+\t.*\t.*\t.*`)
reader := bufio.NewReader(file)
outFile, err = processFile(file)
if err != nil {
return
}
if buildComments {
for {
l, e := reader.ReadString('\n')
eof := e != nil
if exp := re.MatchString(l); exp {
return outFile, err
}
//takes data as a bytes.Buffer, reads and processes each line according to the
//deconstructInfoTextLines flag. returns processed data as a bytes.Buffer and
//any error information.
func processFile(file io.Reader) (outFile bytes.Buffer, err error) {
scanner := bufio.NewScanner(file)
if !deconstructInfoTextLines {
for scanner.Scan() {
l := scanner.Text()
if exp := reGopherLines.MatchString(l); exp {
outFile.WriteString(l)
} else {
outFile.WriteString(buildComment(l, eof))
}
if eof {
break
outFile.WriteString(buildInfoText(l))
}
outFile.WriteString("\n")
}
} else {
for {
l, e := reader.ReadString('\n')
if exp := re.MatchString(l); exp && l[0] == 'i' {
outFile.WriteString(deconstructComment(l))
for scanner.Scan() {
l := scanner.Text()
if exp := reGopherLines.MatchString(l); exp && l[0] == 'i' {
outFile.WriteString(deconstructInfoText(l))
} else {
outFile.WriteString(l)
}
if e != nil {
break
}
outFile.WriteString("\n")
}
}
if err = scanner.Err(); err != nil {
return
}
return outFile, nil
}
func writeFile(path string) {
//takes a file path as a string and some data as bytes.Buffer. writes the data
//to the specified file. returns any error information.
func writeFile(path string, outFile bytes.Buffer) (err error) {
file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0)
errorExit(err, fmt.Sprintf("Unable to open file for writing: %s\n", path))
if err != nil {
return
}
defer file.Close()
file.Write(outFile.Bytes())
return nil
}
//configure command line flags
func init() {
flag.BoolVar(&deconstructInfoTextLines, "d", false, "Deconstruct a gophermap's info text lines back to plain text")
flag.StringVar(&header, "head", "", "Path to a file containing header content")
flag.StringVar(&footer, "foot", "", "Path to a file containing footer content")
flag.BoolVar(&stdout, "stdout", false, "Instead of writing changes to a file, return them to stdout")
}
//PrintHelp produces a nice display message when the --help flag is used
func PrintHelp() {
art := `gfu - gophermap formatting utility
@ -122,40 +155,44 @@ example: gfu -d ~/gopher/phlog/gophermap
default
Convert plain text lines to gophermap info text (item type 'i') lines
`
fmt.Fprint(os.Stderr, art)
fmt.Fprint(os.Stdout, art)
flag.PrintDefaults()
}
func main() {
deconstructCommentLinks := flag.Bool("d", false, "Deconstruct a gophermap's info text lines back to plain text")
header := flag.String("head", "", "Path to a file containing header content")
footer := flag.String("foot", "", "Path to a file containing footer content")
stdout := flag.Bool("stdout", false, "Instead of writing changes to a file, return them to stdout")
//process command line flags
flag.Usage = PrintHelp
flag.Parse()
args := flag.Args()
if l := len(args); l != 1 {
fmt.Printf("Incorrect number of arguments. Expected 1, got %d\n", l)
fmt.Fprintf(os.Stderr, "Incorrect number of arguments. Expected 1, got %d\n", l)
os.Exit(1)
}
if *header != "" {
if header != "" {
fmt.Println("Header functionality is not built yet, proceeding with general gophermap conversion...")
}
if *footer != "" {
if footer != "" {
fmt.Println("Footer functionality is not built yet, proceeding with general gophermap conversion...")
}
readFile(args[0], !*deconstructCommentLinks)
//read and process file
outFile, err := readFile(args[0])
if err != nil {
log.Fatalln("Error while reading file -", err)
}
if *stdout {
//output data to stdout or file
if stdout {
fmt.Print(outFile.String())
} else {
writeFile(args[0])
err = writeFile(args[0], outFile)
if err != nil {
log.Fatalln("Error while writing file -", err)
}
}
//the end
os.Exit(0)
}

132
main_test.go Normal file
View File

@ -0,0 +1,132 @@
package main
import (
"strings"
"testing"
)
var buildInfoTextTestCases = []struct {
testInput string
expectedOutput string
}{
{
//Plain line of text is converted to info text
"A line of text",
"iA line of text false null.host 1",
},
}
var processFileTestCases = []struct {
gophermap string
plaintext string
}{
{
//Plain line of text is converted to info text
"iA line of text false null.host 1\n",
"A line of text\n",
},
{
//Gopher submenu is not converted
"1Floodgap Home /home gopher.floodgap.com 70\n",
"1Floodgap Home /home gopher.floodgap.com 70\n",
},
{
//HTML file is not converted
"hhttp://tildegit.org/sloum/bombadillo url:http://tildegit.org/sloum/bombadillo colorfield.space 70\n",
"hhttp://tildegit.org/sloum/bombadillo url:http://tildegit.org/sloum/bombadillo colorfield.space 70\n",
},
}
var carriagereturnTestCases = []struct {
testInput string
expectedOutput string
deconstruct bool
}{
{
//Plaintext to gophermap
"A test line with a carriage return\r\n",
"iA test line with a carriage return false null.host 1\n",
false,
},
{
//Gophermap to plaintext
"iA test line with a cr false null.host 1\r\n",
"A test line with a cr\n",
true,
},
{
//HTML file
"hhttp://tildegit.org/sloum/bombadillo url:http://tildegit.org/sloum/bombadillo colorfield.space 70\r\n",
"hhttp://tildegit.org/sloum/bombadillo url:http://tildegit.org/sloum/bombadillo colorfield.space 70\n",
false,
},
}
func TestBuildInfoText(t *testing.T) {
for testNumber, testCase := range buildInfoTextTestCases {
testOutput := buildInfoText(testCase.testInput)
if testCase.expectedOutput != testOutput {
t.Errorf(`buildInfoText test case %d failed. Expected "%s", got "%s"`,
testNumber,
testCase.expectedOutput,
testOutput,
)
break
}
}
}
func TestProcessFileBuild(t *testing.T) {
for testNum, testCase := range processFileTestCases {
deconstructInfoTextLines = false
testOutput, err := processFile(strings.NewReader(testCase.plaintext))
if err != nil {
t.Errorf("Error occurred: %v", err)
}
if testCase.gophermap != testOutput.String() {
t.Errorf(`processFile build test case %d failed. Expected "%s", got "%s"`,
testNum,
testCase.plaintext,
testOutput.String(),
)
break
}
}
}
func TestProcessFileDecon(t *testing.T) {
for testNum, testCase := range processFileTestCases {
deconstructInfoTextLines = true
testOutput, err := processFile(strings.NewReader(testCase.gophermap))
if err != nil {
t.Errorf("Error occurred: %v", err)
}
if testCase.plaintext != testOutput.String() {
t.Errorf(`processFile decon test case %d failed. Expected "%s", got "%s"`,
testNum,
testCase.plaintext,
testOutput.String(),
)
break
}
}
}
func TestCarriageReturnsAreExpunged(t *testing.T) {
for testNum, testCase := range carriagereturnTestCases {
deconstructInfoTextLines = testCase.deconstruct
testOutput, err := processFile(strings.NewReader(testCase.testInput))
if err != nil {
t.Errorf("Error occurred: %v", err)
}
if testCase.expectedOutput != testOutput.String() {
t.Errorf(`CR test case %d failed. Expected "%s", got "%s"`,
testNum,
testCase.expectedOutput,
testOutput.String(),
)
break
}
}
}