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 && bl.Title() != "" { 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 && cb.Title() != "" { 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() { sb, isSection := ch.(*SectionBlock) cb.AppendChild(duplicateBlock(ch, sections || (!isSection || sb.Title() == ""), 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 }