summaryrefslogtreecommitdiffstats
path: root/selector.go
diff options
context:
space:
mode:
Diffstat (limited to 'selector.go')
-rw-r--r--selector.go277
1 files changed, 277 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
+}