diff options
| -rw-r--r-- | selector.go | 277 | ||||
| -rw-r--r-- | selector_test.go | 305 | 
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()) +} |