summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--selector.go277
-rw-r--r--selector_test.go305
2 files changed, 582 insertions, 0 deletions
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())
+}