9 changed files with 267 additions and 0 deletions
@ -0,0 +1,14 @@
|
||||
/* |
||||
Package tree is a subset of a class of languages called two dimensional |
||||
languages. |
||||
|
||||
One dimensional languages assume a one dimensional array of bits with a single |
||||
read head moving in order. |
||||
|
||||
Two dimensional languages break those assumptions. There may be multiple read |
||||
heads that can move not just on the x axis but on the y axis as well. |
||||
|
||||
Tree Notation is a middle ground that utilitizes ideas from the two dimensional |
||||
language world using present day technology. |
||||
*/ |
||||
package tree |
@ -0,0 +1,6 @@
|
||||
title Nursinos Services Offerered |
||||
paragraph Here is a list of home nursing services we offer |
||||
table |
||||
Service Price |
||||
BloodPressureCheck $10 |
||||
TemperatureCheck $5 |
@ -0,0 +1,6 @@
|
||||
name mypackage |
||||
version 2.1.1 |
||||
description A package |
||||
repository |
||||
type git |
||||
url git://github.com/username/mypackage |
@ -0,0 +1,113 @@
|
||||
package tree |
||||
|
||||
import ( |
||||
"bufio" |
||||
"io" |
||||
"strings" |
||||
) |
||||
|
||||
// Node represent a single data structure.
|
||||
type Node struct { |
||||
Parent *Node |
||||
Children []*Node |
||||
Value string |
||||
} |
||||
|
||||
const ( |
||||
// nodeBreakSymbol delimits nodes (lines).
|
||||
nodeBreakSymbol rune = '\n' |
||||
|
||||
// wordBreakSymbol delimits words (cells).
|
||||
wordBreakSymbol rune = ' ' |
||||
|
||||
// edgeSymbol is used to indicate the parent/child relationship between
|
||||
// nodes.
|
||||
//
|
||||
// TODO(toby3d): allow clients to change this to '\t' for example.
|
||||
edgeSymbol rune = wordBreakSymbol |
||||
) |
||||
|
||||
// Parse parses the data into a Tree Notation nodes.
|
||||
//
|
||||
// Note what all documents are valid Tree Notation documents. Like binary
|
||||
// notation, there are no syntax errors in Tree Notation.
|
||||
func Parse(data io.Reader) (root *Node) { |
||||
stack := make([]*Node, 0) |
||||
|
||||
scanner := bufio.NewScanner(data) |
||||
for scanner.Scan() { |
||||
node := &Node{ |
||||
Parent: nil, |
||||
Children: nil, |
||||
Value: scanner.Text(), |
||||
} |
||||
|
||||
if root == nil { |
||||
root = node |
||||
stack = append(stack, root) |
||||
|
||||
continue |
||||
} |
||||
|
||||
if lenIndent(node.Value) <= lenIndent(root.Value) && len(stack) > 0 { |
||||
root = stack[len(stack)-1] |
||||
stack = stack[:len(stack)-1] |
||||
} |
||||
|
||||
if root.Children == nil { |
||||
root.Children = make([]*Node, 0) |
||||
} |
||||
|
||||
node.Parent = root |
||||
root.Children = append(root.Children, node) |
||||
stack = append(stack, node) |
||||
root = stack[len(stack)-1] |
||||
} |
||||
|
||||
if root == nil { |
||||
return nil |
||||
} |
||||
|
||||
for root.Parent != nil { |
||||
root = root.Parent |
||||
} |
||||
|
||||
return root |
||||
} |
||||
|
||||
// Satisfies the fmt.Stringer interface to print node line passed in as an
|
||||
// operand to any format that accepts a string, or to an unformatted printer
|
||||
// such as fmt.Print.
|
||||
func (n Node) String() string { |
||||
return strings.TrimLeft(n.Value, string(edgeSymbol)) |
||||
} |
||||
|
||||
// GoString satisfies the fmt.GoStringer interface to print values passed as an
|
||||
// operand to a %#v format.
|
||||
func (n Node) GoString() (result string) { |
||||
result += n.Value |
||||
|
||||
for i := range n.Children { |
||||
result += string(nodeBreakSymbol) |
||||
result += n.Children[i].GoString() |
||||
} |
||||
|
||||
return result |
||||
} |
||||
|
||||
// lenIndent count egdeSymbol prefixes in line.
|
||||
//
|
||||
// Returns 0 if line starts from any word character.
|
||||
func lenIndent(v string) int { |
||||
count := 0 |
||||
|
||||
for i := range v { |
||||
if rune(v[i]) != edgeSymbol { |
||||
break |
||||
} |
||||
|
||||
count++ |
||||
} |
||||
|
||||
return count |
||||
} |
@ -0,0 +1,120 @@
|
||||
package tree_test |
||||
|
||||
import ( |
||||
"bytes" |
||||
"embed" |
||||
"fmt" |
||||
"log" |
||||
"path/filepath" |
||||
"strings" |
||||
"testing" |
||||
|
||||
"source.toby3d.me/toby3d/tree" |
||||
) |
||||
|
||||
//go:embed testdata/*
|
||||
var testData embed.FS |
||||
|
||||
func Example() { |
||||
file, err := testData.Open(filepath.Join(".", "testdata", "html.tree")) |
||||
if err != nil { |
||||
log.Fatalf("cannot open file: %v", err) |
||||
} |
||||
defer file.Close() |
||||
|
||||
tree := tree.Parse(file) |
||||
fmt.Printf("%#v", tree) |
||||
// Output:
|
||||
// html
|
||||
// body
|
||||
// div おはようございます
|
||||
} |
||||
|
||||
func TestParse(t *testing.T) { |
||||
t.Parallel() |
||||
|
||||
//nolint: paralleltest // testName used as range value
|
||||
for testName, filePath := range map[string]string{ |
||||
"html": filepath.Join(".", "testdata", "html.tree"), |
||||
"math": filepath.Join(".", "testdata", "math.tree"), |
||||
"nursinos": filepath.Join(".", "testdata", "nursinos.tree"), |
||||
"package": filepath.Join(".", "testdata", "package.tree"), |
||||
"print": filepath.Join(".", "testdata", "print.tree"), |
||||
} { |
||||
testName, filePath := testName, filePath |
||||
|
||||
t.Run(testName, func(t *testing.T) { |
||||
t.Parallel() |
||||
|
||||
src, err := testData.ReadFile(filePath) |
||||
if err != nil { |
||||
t.Fatalf("cannot open testing file: %v", err) |
||||
} |
||||
|
||||
result := tree.Parse(bytes.NewReader(src)) |
||||
if string(src) == fmt.Sprintf("%#v", result) { |
||||
return |
||||
} |
||||
|
||||
t.Fatalf(`'%#v' is not equal '%s'`, result, string(src)) |
||||
}) |
||||
} |
||||
} |
||||
|
||||
func TestNode_String(t *testing.T) { |
||||
t.Parallel() |
||||
|
||||
node := &tree.Node{ |
||||
Parent: nil, |
||||
Value: "print Hello world", |
||||
Children: nil, |
||||
} |
||||
|
||||
const expResult string = "print Hello world" |
||||
if node.String() == expResult { |
||||
return |
||||
} |
||||
|
||||
t.Fatalf(`'%s' is not equal '%s'`, node.String(), expResult) |
||||
} |
||||
|
||||
func TestNode_GoString(t *testing.T) { |
||||
t.Parallel() |
||||
|
||||
root := &tree.Node{ |
||||
Parent: nil, |
||||
Children: make([]*tree.Node, 0), |
||||
Value: "multiply", |
||||
} |
||||
root.Children = append(root.Children, []*tree.Node{{ |
||||
Parent: root, |
||||
Children: nil, |
||||
Value: " add 1 1", |
||||
}, { |
||||
Parent: root, |
||||
Children: nil, |
||||
Value: " add 2 2", |
||||
}}...) |
||||
|
||||
const expResult string = "multiply\n add 1 1\n add 2 2" |
||||
if root.GoString() == expResult { |
||||
return |
||||
} |
||||
|
||||
t.Fatalf(`'%s' is not equal '%s'`, root.GoString(), expResult) |
||||
} |
||||
|
||||
func BenchmarkParse(b *testing.B) { |
||||
input := strings.NewReader("title Nursinos Services Offerered\n" + |
||||
"paragraph Here is a list of home nursing services we offer\n" + |
||||
"table\n" + |
||||
" Service Price\n" + |
||||
" BloodPressureCheck $10\n" + |
||||
" TemperatureCheck $5") |
||||
|
||||
b.ResetTimer() |
||||
|
||||
for i := 0; i < b.N; i++ { |
||||
_ = tree.Parse(input) |
||||
} |
||||
} |
Loading…
Reference in new issue