From 6e909014e6826e14d629e4e49c51394b27a9094e Mon Sep 17 00:00:00 2001 From: clsr Date: Fri, 25 Aug 2017 15:06:28 +0200 Subject: Implement draft/cnm-selector --- selector.go | 277 ++++++++++++++++++++++++++++++++++++++++++++++++++ selector_test.go | 305 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 582 insertions(+) create mode 100644 selector.go create mode 100644 selector_test.go diff --git a/selector.go b/selector.go new file mode 100644 index 0000000..5259df2 --- /dev/null +++ b/selector.go @@ -0,0 +1,277 @@ +package cnm + +import ( + "errors" + "net/url" + "reflect" + "strconv" + "strings" +) + +// Select selects content from the document based on an extended CNM selector. +// +// See ContNet draft/cnm-selector for more information about content selectors. +// +// This function may create copies of the document and all container content +// blocks, but includes non-container blocks untouched in the copy. +func (doc *Document) Select(selector string) (*Document, error) { + deep := true + if len(selector) > 0 && selector[0] == '!' { + deep = false + selector = selector[1:] + } + + if len(selector) == 0 { + return duplicateBlock(doc, true, deep).(*Document), nil + } + + indices, block, err := doc.SelectBlock(selector) + if err != nil || block == nil { + return nil, err + } + + d := NewDocument() + if block := selectContent(doc.Content, indices, deep); block != nil { + d.Content = block.(*ContentBlock) + } + + return d, nil +} + +// SelectIndex selects a content block based on an index path. +// +// For example, indices=[]int{1, 4, 2} selects +// doc.Content.Children()[1].Children()[4].Children(2) if it exists, otherwise +// returns nil. Note that this is *not* the same as a CNM index selector. +func (doc *Document) SelectIndex(indices ...int) (block Block) { + return SelectIndex(doc.Content, indices...) +} + +// SelectBlock selects section blocks from the document with a selector query. +// +// Returns the block index path within content blocks, the target block and, if +// the query was invalid, an error. All are nil when no block was found with a +// valid selector. +// +// See ContNet draft/cnm-selector for more information. +func (doc *Document) SelectBlock(selector string) (indices []int, block Block, err error) { + if len(selector) == 0 { + block = doc + indices = []int{} + return + } + + switch selector[0] { + case '/': + indices, block, err = doc.selectPath(selector[1:]) + case '#': + indices, block, err = doc.selectTitle(selector[1:]) + case '$': + indices, block, err = doc.selectSectionIndex(selector[1:]) + default: + err = errors.New("cnm-go: invalid selector") + } + + return +} + +func (doc *Document) selectPath(selector string) (indices []int, block Block, err error) { + ss := []string{} + if selector != "" { + ss = strings.Split(selector, "/") + for i := range ss { + ss[i], err = url.PathUnescape(ss[i]) + if err != nil { + return + } + } + } + indices, block = selectSearch(doc.Content, ss, false) + return +} + +func (doc *Document) selectTitle(selector string) (indices []int, block Block, err error) { + if strings.ContainsRune(selector, '/') { + err = errors.New("cnm-go: invalid title selector") + return + } + selector, err = url.PathUnescape(selector) + if err != nil { + return + } + ss := []string{} + if selector != "" { + ss = []string{selector} + } + indices, block = selectSearch(doc.Content, ss, true) + return +} + +func (doc *Document) selectSectionIndex(selector string) (indices []int, block Block, err error) { + secs := []int{} + if selector != "" { + for _, s := range strings.Split(selector, ".") { + var n uint64 + n, err = strconv.ParseUint(s, 10, 31) + if err != nil { + return + } + secs = append(secs, int(n)) + } + } + indices, block = selectSectionIndex(doc.Content, secs) + return +} + +func selectSearch(block Block, titles []string, any bool) ([]int, Block) { + if len(titles) == 0 { + return []int{}, block + } + + if bl, ok := block.(*SectionBlock); ok { + if len(titles) == 1 && bl.Title() == titles[0] { + return []int{}, bl + } + if any || bl.Title() == titles[0] { + if !any { + titles = titles[1:] + } + for i, c := range bl.Children() { + if p, b := selectSearch(c, titles, any); b != nil { + return append([]int{i}, p...), b + } + } + } + } else if bl, ok := block.(ContainerBlock); ok { + for i, c := range bl.Children() { + if p, b := selectSearch(c, titles, any); b != nil { + return append([]int{i}, p...), b + } + } + } + + return nil, nil +} + +func selectSectionIndex(block Block, secs []int) ([]int, Block) { + if len(secs) == 0 { + return []int{}, block + } + if bl, ok := block.(ContainerBlock); ok { + for i, c := range bl.Children() { + if secs[0] < 1 { + return nil, nil + } + if cb, ok2 := c.(*SectionBlock); ok2 { + secs[0]-- // starts with 1 for first section + if secs[0] == 0 { + if len(secs) == 1 { + return []int{i}, cb + } + p, b := selectSectionIndex(cb, secs[1:]) + if b == nil { + return nil, nil + } + return append([]int{i}, p...), b + } + } else if _, ok2 := c.(ContainerBlock); ok2 { + p, b := selectSectionIndex(c, secs) + if b != nil { + return append([]int{i}, p...), b + } + } + } + } + return nil, nil +} + +func selectContent(block Block, indices []int, deep bool) Block { + if indices == nil { + return nil + } + if len(indices) == 0 { + return duplicateBlock(block, true, deep) + } + if bl, ok := block.(ContainerBlock); ok { + ch := bl.Children() + if len(ch) <= indices[0] { + return nil + } + b := duplicateBlock(block, false, true).(ContainerBlock) + c := selectContent(ch[indices[0]], indices[1:], deep) + if c != nil { + b.AppendChild(c) + } + return b + } + return nil +} + +func duplicateBlock(block Block, deep, sections bool) Block { + if bl, ok := block.(*Document); ok { + doc := &Document{ + Title: bl.Title, + Links: bl.Links, + Site: bl.Site, + } + if deep { + doc.Content = duplicateBlock(bl.Content, deep, sections).(*ContentBlock) + } + return doc + } + if _, ok := block.(ContainerBlock); !ok { + return block + } + + var cb ContainerBlock + + switch bl := block.(type) { + case *ContentBlock: + cb = NewContentBlock(bl.Name(), bl.Args()...) + case *HeaderBlock: + cb = NewHeaderBlock() + case *ListBlock: + cb = NewListBlock(bl.Ordered()) + case *RowBlock: + cb = NewRowBlock() + case *SectionBlock: + cb = NewSectionBlock(bl.Title()) + case *TableBlock: + cb = NewTableBlock() + default: // XXX + //return nil + val := reflect.New(reflect.TypeOf(bl).Elem()) + cb = val.Interface().(ContainerBlock) + } + + if cb != nil && deep { + for _, ch := range block.(ContainerBlock).Children() { + _, isSection := ch.(*SectionBlock) + cb.AppendChild(duplicateBlock(ch, sections || !isSection, sections)) + } + } + + return cb +} + +// SelectIndex selects a child block based on an index path. +// +// Note that this is *not* the same as a CNM section index selector. +func SelectIndex(block Block, indices ...int) Block { + if indices == nil { + return nil + } + for len(indices) > 0 { + if bl, ok := block.(ContainerBlock); ok { + ch := bl.Children() + if len(ch) <= indices[0] { + return nil + } + block = ch[indices[0]] + indices = indices[1:] + } else { + return nil + } + } + return block +} diff --git a/selector_test.go b/selector_test.go new file mode 100644 index 0000000..0c714dc --- /dev/null +++ b/selector_test.go @@ -0,0 +1,305 @@ +package cnm + +import ( + "bytes" + "strings" + "testing" +) + +var ( + sec11 = &SectionBlock{ContentBlock{ + name: "section", + args: []string{"foo/bar"}, + children: []Block{ + &TextBlock{ + Format: "pre", + Contents: TextPreContents{"qwe"}, + }, + }, + }} + sec1 = &SectionBlock{ContentBlock{ + name: "section", + args: []string{"bar"}, + children: []Block{ + sec11, + &TextBlock{ + Format: "pre", + Contents: TextPreContents{"..."}, + }, + }, + }} + sec31 = &SectionBlock{ContentBlock{ + name: "section", + args: []string{"baz"}, + children: []Block{ + &TextBlock{ + Format: "pre", + Contents: TextPreContents{"zxc"}, + }, + }, + }} + sec322 = &SectionBlock{ContentBlock{ + name: "section", + args: []string{"baz"}, + children: []Block{ + &TextBlock{ + Format: "pre", + Contents: TextPreContents{"xyz"}, + }, + }, + }} + sec32 = &SectionBlock{ContentBlock{ + name: "section", + args: []string{"quux"}, + children: []Block{ + &TextBlock{ + Format: "pre", + Contents: TextPreContents{"123"}, + }, + sec322, + }, + }} + sec4 = &SectionBlock{ContentBlock{ + name: "section", + args: []string{"foo/bar"}, + children: []Block{ + &TextBlock{ + Format: "pre", + Contents: TextPreContents{"abc"}, + }, + }, + }} + doc1 = &Document{ + Title: "test title", + Content: &ContentBlock{ + name: "content", + children: []Block{ + sec1, + &TextBlock{ + Format: "pre", + Contents: TextPreContents{"asd"}, + }, + &ListBlock{ContentBlock{ + name: "list", + children: []Block{ + sec31, + sec32, + &TextBlock{ + Format: "pre", + Contents: TextPreContents{"rty"}, + }, + }, + }}, + sec4, + }, + }, + } + + doc1Shallow = &Document{ + Title: doc1.Title, + Content: &ContentBlock{ + name: "content", + children: []Block{ + &SectionBlock{ContentBlock{ + name: "section", + args: sec1.Args(), + }}, + doc1.Content.Children()[1], + &ListBlock{ContentBlock{ + name: "list", + children: []Block{ + &SectionBlock{ContentBlock{ + name: "section", + args: sec31.Args(), + }}, + &SectionBlock{ContentBlock{ + name: "section", + args: sec32.Args(), + }}, + doc1.Content.Children()[2].(ContainerBlock).Children()[2], + }, + }}, + &SectionBlock{ContentBlock{ + name: "section", + args: sec4.Args(), + }}, + }, + }, + } + + sec1Shallow = &Document{Content: &ContentBlock{ + name: "content", + children: []Block{ + &SectionBlock{ContentBlock{ + name: sec1.Name(), + args: sec1.Args(), + children: []Block{ + &SectionBlock{ContentBlock{ + name: "section", + args: sec11.Args(), + }}, + sec1.Children()[1], + }, + }}, + }, + }} +) + +var selectTests = []struct { + sel string + doc *Document + res *Document +}{ + {"", doc1, doc1}, + {"/", doc1, &Document{Content: doc1.Content}}, + {"$", doc1, &Document{Content: doc1.Content}}, + {"#", doc1, &Document{Content: doc1.Content}}, + + {"!", doc1, doc1Shallow}, + {"!/", doc1, &Document{Content: doc1Shallow.Content}}, + {"!$", doc1, &Document{Content: doc1Shallow.Content}}, + {"!#", doc1, &Document{Content: doc1Shallow.Content}}, + + {"/bar", doc1, &Document{Content: &ContentBlock{ + name: "content", + children: []Block{ + sec1, + }, + }}}, + {"!/bar", doc1, sec1Shallow}, + {"#bar", doc1, &Document{Content: &ContentBlock{ + name: "content", + children: []Block{sec1}, + }}}, + {"!#bar", doc1, sec1Shallow}, + {"$1", doc1, &Document{Content: &ContentBlock{ + name: "content", + children: []Block{sec1}, + }}}, + {"!$1", doc1, sec1Shallow}, +} + +func TestSelect(t *testing.T) { + for _, v := range selectTests { + t.Run(strings.Replace(v.sel, "/", "_", -1), func(t *testing.T) { + res, err := v.doc.Select(v.sel) + if err != nil { + printTestDoc(t, v.doc) + t.Fatalf("Select(%q) on %#v:\nerror: %v", v.sel, v.doc, err) + } + if !documentEqual(t, res, v.res) { + t.Log("=============== doc") + printTestDoc(t, v.doc) + t.Log("=============== expect") + printTestDoc(t, v.res) + t.Log("=============== got") + printTestDoc(t, res) + t.Fatalf("Select(%q) on %#v:\nexpected: %#v\n got: %#v", v.sel, v.doc, v.res, res) + } + }) + } +} + +var selectBlockTests = []struct { + sel string + doc *Document + res Block + ind []int +}{ + {"", doc1, doc1, []int{}}, + + {"/", doc1, doc1.Content, []int{}}, + {"/qwe", doc1, nil, nil}, + {"/bar/qwe", doc1, nil, nil}, + {"/bar/", doc1, nil, nil}, + {"//", doc1, nil, nil}, + {"/./", doc1, nil, nil}, + {"/../", doc1, nil, nil}, + {"/.", doc1, nil, nil}, + {"/..", doc1, nil, nil}, + {"/foo%2fbar", doc1, sec4, []int{3}}, + {"/bar/foo%2fbar", doc1, sec11, []int{0, 0}}, + {"/baz", doc1, sec31, []int{2, 0}}, + {"/quux", doc1, sec32, []int{2, 1}}, + {"/quux/baz", doc1, sec322, []int{2, 1, 1}}, + {"/bar/baz", doc1, nil, nil}, + + {"$", doc1, doc1.Content, []int{}}, + {"$0", doc1, nil, nil}, + {"$1", doc1, sec1, []int{0}}, + {"$1.1", doc1, sec11, []int{0, 0}}, + {"$2", doc1, sec31, []int{2, 0}}, + {"$3", doc1, sec32, []int{2, 1}}, + {"$4", doc1, sec4, []int{3}}, + {"$5", doc1, nil, nil}, + {"$2.1", doc1, nil, nil}, + {"$3.1", doc1, sec322, []int{2, 1, 1}}, + {"$3.0", doc1, nil, nil}, + {"$3.2", doc1, nil, nil}, + + {"#", doc1, doc1.Content, []int{}}, + {"#qwe", doc1, nil, nil}, + {"#bar", doc1, sec1, []int{0}}, + {"#foo%2fbar", doc1, sec11, []int{0, 0}}, + {"#baz", doc1, sec31, []int{2, 0}}, + {"#quux", doc1, sec32, []int{2, 1}}, +} + +func TestSelectBlock(t *testing.T) { + for _, v := range selectBlockTests { + t.Run(strings.Replace(v.sel, "/", "_", -1), func(t *testing.T) { + ind, res, err := v.doc.SelectBlock(v.sel) + if err != nil { + printTestDoc(t, v.doc) + t.Fatalf("SelectBlock(%q) on %#v:\nerror: %v", v.sel, v.doc, err) + } + if !indicesEqual(t, ind, v.ind) { + printTestDoc(t, v.doc) + t.Fatalf("SelectBlock(%q) on %#v:\nexpected indices %#v, got %#v", v.sel, v.doc, v.ind, ind) + } + if !blockEqual(t, res, v.res) { + printTestDoc(t, v.doc) + t.Fatalf("SelectBlock(%q) on %#v:\nexpected: %#v\n got: %#v", v.sel, v.doc, v.res, res) + } + if doc, ok := res.(*Document); ok { + res = doc.Content + } + b := v.doc.SelectIndex(ind...) + if !blockEqual(t, b, res) { + printTestDoc(t, v.doc) + t.Fatalf("SelectIndex(%#v) (for %q):\nexpected: %#v\n got: %#v", ind, v.sel, res, b) + } + }) + } +} + +func indicesEqual(t *testing.T, a, b []int) bool { + if a == nil && b == nil { + return true + } + if a == nil || b == nil { + t.Log("indice slice nilness differs") + return false + } + if len(a) != len(b) { + t.Log("indice slice lengths differ") + return false + } + for i := range a { + if a[i] != b[i] { + t.Log("indice slice element differs") + return false + } + } + return true +} + +func printTestDoc(t *testing.T, doc *Document) { + if doc == nil { + t.Log(doc) + return + } + var buf bytes.Buffer + doc.Write(&buf) + t.Log("\n" + buf.String()) +} -- cgit