Skip to content

Commit

Permalink
fm filter in nodesSelector
Browse files Browse the repository at this point in the history
  • Loading branch information
g-pavlov committed Feb 7, 2021
1 parent 9b5eb84 commit 9395e6c
Show file tree
Hide file tree
Showing 5 changed files with 372 additions and 20 deletions.
64 changes: 63 additions & 1 deletion docs/manifest-ref.md
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,69 @@ resolved at runtime dynamically.

Depth is a maximum depth of the recursion for selecting nodes from hierarchy.
If omitted or less than 0, the constraint is not considered.

- **FrontMatter**
Type: Map[string][any]
_Optional_

FrontMatter is a set of rules for a `nodesSelector` to **include** nodes with
compliant front-matter. The compliance is positive if a node matches one or
more rules. Applies to document nodes only.

If a node is evaluated to be compliant with both `FrontMatter` and
`ExcludeFrontMatter` rules, it will be excluded.

Markdown metadata is commonly provisioned as `front-matter` block at the head
of the document delimited by comment tags (`---`). The supported format of the
metadata is YAML.

The `FrontMatter` rules are mappings between path patterns identifying an
element in the front-matter and a value. If the path matches an actual path
to an element in the front-matter and the value of this element matches the
rule value, there is a positive match.

The path patterns are a very simplified form of JSONPath notation.
An object in path is modeled as dot (`.`). Paths start with the root object,
i.e. the most minimal path is `.`.
An object element value is referenced by its name (key) in the object map:
`.a.b.c` is path to element `c` in map `b` in map `a` in root object map.
Element values can be scalar, object maps or arrays.
An element in an array is referenced by its index: `.a.b[1]` references `b`
array element with index 1.
Paths can include up to one wildcard `**` symbol that models *any* path node.
A `.a.**.c` models any path starting with `.a.` and ending with `.c`.

- **ExcludeFrontMatter**
Type: Map[string][any]
_Optional_

ExcludeFrontMatter is a set of rules for a `nodesSelector` to **exclude** nodes
with compliant front-matter. The compliance is positive if a node matches one or
more rules. Applies to document nodes only.

If a node is evaluated to be compliant with both `FrontMatter` and
`ExcludeFrontMatter` rules, it will be excluded.

Markdown metadata is commonly provisioned as `front-matter` block at the head
of the document delimited by comment tags (`---`). The supported format of the
metadata is YAML.

The `ExcludeFrontMatter` rules are mappings between path patterns identifying an
element in the front-matter and a value. If the path matches an actual path
to an element in the front-matter and the value of this element matches the
rule value, there is a positive match.

The path patterns are a very simplified form of JSONPath notation.
An object in path is modeled as dot (`.`). Paths start with the root object,
i.e. the most minimal path is `.`.
An object element value is referenced by its name (key) in the object map:
`.a.b.c` is path to element `c` in map `b` in map `a` in root object map.
Element values can be scalar, object maps or arrays.
An element in an array is referenced by its index: `.a.b[1]` references `b`
array element with index 1.
Paths can include up to one wildcard `**` symbol that models *any* path node.
A `.a.**.c` models any path starting with `.a.` and ending with `.c`.

## ContentSelector

Type: Object
Expand Down Expand Up @@ -302,7 +364,7 @@ LinkRewriteRule specifies link components to be rewritten.
This setting has precedence over version if both are specified.

A link destination rewritten by this rule, which is also matched by a
downloads specification, will be converted to relative, using the result
downloads specification, will be converted to relative, using the result
of this destination substitution. The final result therefore may be different
from the destination substitution defined here.

Expand Down
66 changes: 52 additions & 14 deletions pkg/api/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -153,23 +153,61 @@ type NodeSelector struct {
//
// Optional
Depth int32 `yaml:"depth,omitempty"`
// ExcludeFrontMatter is an optional expression, filtering documents located at `Path`
// by their metadata properties. Markdown metadata is commonly provisioned as
// `front-matter` block at the head of the document delimited by comment
// tags (`---`).
// Documents with front matter that matches all map entries of this field
// are not selected.
// Note: WiP - proposed, not implemented yet.
// ExcludeFrontMatter is a set of rules for a `nodesSelector` to **exclude** nodes
// with compliant front-matter. The compliance is positive if a node matches one or
// more rules. Applies to document nodes only.

// If a node is evaluated to be compliant with both `FrontMatter` and
// `ExcludeFrontMatter` rules, it will be excluded.

// Markdown metadata is commonly provisioned as `front-matter` block at the head
// of the document delimited by comment tags (`---`). The supported format of the
// metadata is YAML.

// The `ExcludeFrontMatter` rules are mappings between path patterns identifying an
// element in the front-matter and a value. If the path matches an actual path
// to an element in the front-matter and the value of this element matches the
// rule value, there is a positive match.

// The path patterns are a very simplified form of JSONPath notation.
// An object in path is modeled as dot (`.`). Paths start with the root object,
// i.e. the most minimal path is `.`.
// An object element value is referenced by its name (key) in the object map:
// `.a.b.c` is path to element `c` in map `b` in map `a` in root object map.
// Element values can be scalar, object maps or arrays.
// An element in an array is referenced by its index: `.a.b[1]` references `b`
// array element with index 1.
// Paths can include up to one wildcard `**` symbol that models *any* path node.
// A `.a.**.c` models any path starting with `.a.` and ending with `.c`.
//
// Optional
ExcludeFrontMatter map[string]interface{} `yaml:"excludeFrontMatter,omitempty"`
// FrontMatter is an optional expression, filtering documents located at `Path`
// by their metadata properties. Markdown metadata is commonly provisioned as
// `front-matter` block at the head of the document delimited by comment
// tags (`---`).
// Documents with front matter that matches all map entries of this field
// are selected.
// Note: WiP - proposed, not implemented yet.
// FrontMatter is a set of rules for a `nodesSelector` to **include** nodes with
// compliant front-matter. The compliance is positive if a node matches one or
// more rules. Applies to document nodes only.

// If a node is evaluated to be compliant with both `FrontMatter` and
// `ExcludeFrontMatter` rules, it will be excluded.

// Markdown metadata is commonly provisioned as `front-matter` block at the head
// of the document delimited by comment tags (`---`). The supported format of the
// metadata is YAML.

// The `FrontMatter` rules are mappings between path patterns identifying an
// element in the front-matter and a value. If the path matches an actual path
// to an element in the front-matter and the value of this element matches the
// rule value, there is a positive match.

// The path patterns are a very simplified form of JSONPath notation.
// An object in path is modeled as dot (`.`). Paths start with the root object,
// i.e. the most minimal path is `.`.
// An object element value is referenced by its name (key) in the object map:
// `.a.b.c` is path to element `c` in map `b` in map `a` in root object map.
// Element values can be scalar, object maps or arrays.
// An element in an array is referenced by its index: `.a.b[1]` references `b`
// array element with index 1.
// Paths can include up to one wildcard `**` symbol that models *any* path node.
// A `.a.**.c` models any path starting with `.a.` and ending with `.c`.
//
// Optional
FrontMatter map[string]interface{} `yaml:"frontMatter,omitempty"`
Expand Down
65 changes: 65 additions & 0 deletions pkg/markdown/frontmatter.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@ package markdown
import (
"bytes"
"errors"
"fmt"
"io"
"io/ioutil"
"reflect"
"strings"
)

Expand Down Expand Up @@ -99,3 +101,66 @@ func InsertFrontMatter(fm []byte, content []byte) ([]byte, error) {
}
return data, nil
}

// Match explores a parsed frontmatter object `data` to match `value` at `path`
// pattern and return true on successfull match or false otherwise.
// Path is an expression with a JSONPath-like simplified notation.
// An object in path is modeled as dot (`.`). Paths start with the root object,
// i.e. the most minimal path is `.`.
// An object element value is referenced by its name (key) in the object map:
// `.a.b.c` is path to element `c` in map `b` in map `a` in root object map.
// Element values can be scalar, object maps or arrays.
// An element in an array is referenced by its index: `.a.b[1]` references `b`
// array element with index 1.
// Paths can include up to one wildcard `**` symbol that models *any* path node.
// A `.a.**.c` models any path starting with `.a.` and ending with `.c`.
func Match(path string, val interface{}, data interface{}) bool {
return match(path, val, nil, data)
}

func match(pathPattern string, val interface{}, path []string, data interface{}) bool {
if path == nil {
path = []string{"."}
}
p := strings.Join(path, "")
if _matchPath(pathPattern, p) {
if reflect.DeepEqual(val, data) {
return true
}
}
switch vv := data.(type) {
case []interface{}:
for i, u := range vv {
_p := append(path, fmt.Sprintf("[%d]", i))
if ok := match(pathPattern, val, _p, u); ok {
return true
}
}
case map[string]interface{}:
for k, u := range vv {
if path[(len(path))-1] != "." {
path = append(path, ".")
}
_p := append(path, k)
if ok := match(pathPattern, val, _p, u); ok {
return true
}
}
}
return false
}

func _matchPath(pathPattern, path string) bool {
if pathPattern == path {
return true
}
s := strings.Split(pathPattern, "**")
if len(s) > 1 {
if strings.HasPrefix(path, s[0]) {
if len(s[1]) == 0 || strings.HasSuffix(path, s[1]) {
return true
}
}
}
return false
}
144 changes: 144 additions & 0 deletions pkg/markdown/frontmatter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,3 +77,147 @@ title: Core Components
})
}
}

func TestMatch(t *testing.T) {
testCases := []struct {
matchPath string
matchVal interface{}
data map[string]interface{}
wantMatch bool
}{
{
matchPath: ".A.B",
matchVal: 5,
data: map[string]interface{}{
"A": map[string]interface{}{
"B": 5,
},
},
wantMatch: true,
},
{
matchPath: ".A.B",
matchVal: true,
data: map[string]interface{}{
"A": map[string]interface{}{
"B": true,
},
},
wantMatch: true,
},
{
matchPath: ".A.B",
matchVal: "a",
data: map[string]interface{}{
"A": map[string]interface{}{
"B": "a",
},
},
wantMatch: true,
},
{
matchPath: ".A.**.C",
matchVal: 5,
data: map[string]interface{}{
"A": map[string]interface{}{
"B": map[string]interface{}{
"C": 5,
},
},
},
wantMatch: true,
},
{
matchPath: ".**",
matchVal: 5,
data: map[string]interface{}{
"A": map[string]interface{}{
"B": map[string]interface{}{
"C": 5,
},
},
},
wantMatch: true,
},
{
matchPath: ".A.B[1]",
matchVal: "b",
data: map[string]interface{}{
"A": map[string]interface{}{
"B": []interface{}{"a", "b", "c"},
},
},
wantMatch: true,
},
{
matchPath: ".A.B[1].C2",
matchVal: 2,
data: map[string]interface{}{
"A": map[string]interface{}{
"B": []interface{}{
map[string]interface{}{
"C1": 1,
},
map[string]interface{}{
"C2": 2,
},
map[string]interface{}{
"C3": 3,
},
},
},
},
wantMatch: true,
},
{
matchPath: ".A.**.C2",
matchVal: 2,
data: map[string]interface{}{
"A": map[string]interface{}{
"B": []interface{}{
map[string]interface{}{
"C1": 1,
},
map[string]interface{}{
"C2": 2,
},
map[string]interface{}{
"C3": 3,
},
},
},
},
wantMatch: true,
},
{
matchPath: ".A.B",
matchVal: 2,
data: map[string]interface{}{
"A": map[string]interface{}{
"B": 5,
},
},
wantMatch: false,
},
{
matchPath: ".A",
matchVal: map[string]interface{}{
"B": []interface{}{"a", "b", "c"},
},
data: map[string]interface{}{
"A": map[string]interface{}{
"B": []interface{}{"a", "b", "c"},
},
},
wantMatch: true,
},
}
for _, tc := range testCases {
t.Run("", func(t *testing.T) {
matched := Match(tc.matchPath, tc.matchVal, tc.data)
if tc.wantMatch && !matched {
t.Errorf("expected a match for path %s, got no match", tc.matchPath)
}
})
}
}
Loading

0 comments on commit 9395e6c

Please sign in to comment.