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 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 277 insertions(+) create mode 100644 selector.go (limited to 'selector.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 +} -- cgit