diff --git a/doc.go b/doc.go new file mode 100644 index 0000000..7a674d1 --- /dev/null +++ b/doc.go @@ -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 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..e69de29 diff --git a/testdata/html.tree b/testdata/html.tree new file mode 100644 index 0000000..84bbac4 --- /dev/null +++ b/testdata/html.tree @@ -0,0 +1,3 @@ +html + body + div おはようございます \ No newline at end of file diff --git a/testdata/math.tree b/testdata/math.tree new file mode 100644 index 0000000..d07958f --- /dev/null +++ b/testdata/math.tree @@ -0,0 +1,3 @@ +multiply + add 1 1 + add 2 2 \ No newline at end of file diff --git a/testdata/nursinos.tree b/testdata/nursinos.tree new file mode 100644 index 0000000..92fe75a --- /dev/null +++ b/testdata/nursinos.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 \ No newline at end of file diff --git a/testdata/package.tree b/testdata/package.tree new file mode 100644 index 0000000..9a4fa4f --- /dev/null +++ b/testdata/package.tree @@ -0,0 +1,6 @@ +name mypackage +version 2.1.1 +description A package +repository + type git + url git://github.com/username/mypackage \ No newline at end of file diff --git a/testdata/print.tree b/testdata/print.tree new file mode 100644 index 0000000..a190705 --- /dev/null +++ b/testdata/print.tree @@ -0,0 +1,2 @@ +if true + print Hello world \ No newline at end of file diff --git a/tree.go b/tree.go new file mode 100644 index 0000000..e916294 --- /dev/null +++ b/tree.go @@ -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 +} diff --git a/tree_test.go b/tree_test.go new file mode 100644 index 0000000..d245d89 --- /dev/null +++ b/tree_test.go @@ -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) + } +}