diff options
-rw-r--r-- | README.md | 36 | ||||
-rw-r--r-- | addons/github.com/ponzu-cms/addons/reference/reference.go | 106 | ||||
-rw-r--r-- | cmd/ponzu/vendor/github.com/tidwall/gjson/LICENSE | 20 | ||||
-rw-r--r-- | cmd/ponzu/vendor/github.com/tidwall/gjson/README.md | 371 | ||||
-rw-r--r-- | cmd/ponzu/vendor/github.com/tidwall/gjson/gjson.go | 1942 | ||||
-rw-r--r-- | cmd/ponzu/vendor/github.com/tidwall/gjson/logo.png | bin | 0 -> 15936 bytes | |||
-rw-r--r-- | management/editor/dom.go | 231 | ||||
-rw-r--r-- | management/editor/elements.go | 146 | ||||
-rw-r--r-- | management/editor/repeaters.go | 88 | ||||
-rw-r--r-- | management/editor/values.go | 13 | ||||
-rw-r--r-- | system/addon/api.go | 4 | ||||
-rw-r--r-- | system/admin/admin.go | 2 | ||||
-rw-r--r-- | system/admin/handlers.go | 6 | ||||
-rw-r--r-- | system/api/handlers.go | 88 | ||||
-rw-r--r-- | system/api/push.go | 37 | ||||
-rw-r--r-- | system/db/content.go | 10 | ||||
-rw-r--r-- | system/item/item.go | 13 | ||||
-rw-r--r-- | system/item/types.go | 5 |
18 files changed, 2846 insertions, 272 deletions
@@ -16,16 +16,21 @@ Ponzu is released under the BSD-3-Clause license (see LICENSE). $ go get github.com/ponzu-cms/ponzu/... ``` +### Requirements +Go 1.8+ + +Since HTTP/2 Server Push is used, Go 1.8+ is required. However, it is not +required of clients conntecing to a Ponzu server to make requests over HTTP/2. + ## Usage ```bash $ ponzu [flags] command <params> ``` -### COMMANDS +## Commands - -### new \<directory\>: +### new Creates a 'ponzu' directory, or one by the name supplied as a parameter immediately following the 'new' option in the $GOPATH/src directory. Note: @@ -44,7 +49,7 @@ Errors will be reported, but successful commands retrun nothing. --- -### generate, gen, g \<type (,...fields)\>: +### generate, gen, g Generate a content type file with boilerplate code to implement the editor.Editable interface. Must be given one (1) parameter of @@ -55,7 +60,13 @@ fieldName:"T" Example: ```bash + struct fields and built-in types... + | + v $ ponzu gen review title:"string" body:"string" rating:"int" tags:"[]string" + ^ + | + struct type ``` The command above will generate a file `content/review.go` with boilerplate @@ -83,16 +94,21 @@ From within your Ponzu project directory, running build will copy and move the necessary files from your workspace into the vendored directory, and will build/compile the project to then be run. +Optional flags: +- `--gocmd` sets the binary used when executing `go build` witin `ponzu` build step + Example: ```bash $ ponzu build +(or) +$ ponzu --gocmd=go1.8beta2 build # useful for testing ``` Errors will be reported, but successful build commands return nothing. --- -### [[--port=8080] [--https]] run \<service(,service)\>: +### run Starts the HTTP server for the JSON API, Admin System, or both. The segments, separated by a comma, describe which services to start, either @@ -100,6 +116,11 @@ The segments, separated by a comma, describe which services to start, either if the server should utilize TLS encryption - served over HTTPS, which is automatically managed using Let's Encrypt (https://letsencrypt.org) +Optional flags: +- `--port` sets the port on which the server listens for requests [defaults to 8080] +- `--https` enables auto HTTPS management via Let's Encrypt (port is always 443) +- `--devhttps` generates self-signed SSL certificates for development-only (port is 10443) + Example: ```bash $ ponzu run @@ -109,6 +130,8 @@ $ ponzu --port=8080 --https run admin,api $ ponzu run admin (or) $ ponzu --port=8888 run api +(or) +$ ponzu --devhttps run ``` Defaults to `$ ponzu --port=8080 run admin,api` (running Admin & API on port 8080, without TLS) @@ -126,7 +149,7 @@ to run the Admin and API on separate processes, you must call them with the 1. Checkout branch ponzu-dev 2. Make code changes 3. Test changes to ponzu-dev branch - - make a commit to ponzu-dev (I know, a little unnatural. Advice gladly accepted.) + - make a commit to ponzu-dev - to manually test, you will need to use a new copy (ponzu new path/to/code), but pass the --dev flag so that ponzu generates a new copy from the ponzu-dev branch, not master by default (i.e. `$ponzu --dev new /path/to/code`) - build and run with $ ponzu build and $ ponzu run 4. To add back to master: @@ -183,6 +206,7 @@ $ ponzu --dev --fork=github.com/nilslice/ponzu new /path/to/new/project - [github.com/nilslice/email](https://github.com/nilslice/email) - [github.com/gorilla/schema](https://github.com/gorilla/schema) - [github.com/satori/go.uuid](https://github.com/satori/go.uuid) +- [github.com/tidwall/gjson](https://github.com/tidwall/gjson) - [github.com/boltdb/bolt](https://github.com/boltdb/bolt) - [github.com/sluu99/um](https://github.com/sluu99/um) - [Materialnote Editor](http://www.web-forge.info/projects/materialNote) diff --git a/addons/github.com/ponzu-cms/addons/reference/reference.go b/addons/github.com/ponzu-cms/addons/reference/reference.go index f90964c..9918f36 100644 --- a/addons/github.com/ponzu-cms/addons/reference/reference.go +++ b/addons/github.com/ponzu-cms/addons/reference/reference.go @@ -9,6 +9,7 @@ import ( "fmt" "html/template" "log" + "strings" "github.com/ponzu-cms/ponzu/management/editor" "github.com/ponzu-cms/ponzu/system/addon" @@ -19,7 +20,101 @@ import ( // The `fieldName` argument will cause a panic if it is not exactly the string // form of the struct field that this editor input is representing func Select(fieldName string, p interface{}, attrs map[string]string, contentType, tmplString string) []byte { - // decode all content type from db into options map + options, err := encodeDataToOptions(contentType, tmplString) + if err != nil { + log.Println("Error encoding data to options for", contentType, err) + return nil + } + + return editor.Select(fieldName, p, attrs, options) +} + +// SelectRepeater returns the []byte of a <select> HTML element plus internal <options> with a label. +// It also includes repeat controllers (+ / -) so the element can be +// dynamically multiplied or reduced. +// IMPORTANT: +// The `fieldName` argument will cause a panic if it is not exactly the string +// form of the struct field that this editor input is representing +func SelectRepeater(fieldName string, p interface{}, attrs map[string]string, contentType, tmplString string) []byte { + scope := editor.TagNameFromStructField(fieldName, p) + html := bytes.Buffer{} + _, err := html.WriteString(`<span class="__ponzu-repeat ` + scope + `">`) + if err != nil { + log.Println("Error writing HTML string to SelectRepeater buffer") + return nil + } + + if _, ok := attrs["class"]; ok { + attrs["class"] += " browser-default" + } else { + attrs["class"] = "browser-default" + } + + // find the field values in p to determine if an option is pre-selected + fieldVals := editor.ValueFromStructField(fieldName, p) + vals := strings.Split(fieldVals, "__ponzu") + + options, err := encodeDataToOptions(contentType, tmplString) + if err != nil { + log.Println("Error encoding data to options for", contentType, err) + return nil + } + + for _, val := range vals { + sel := editor.NewElement("select", attrs["label"], fieldName, p, attrs) + var opts []*editor.Element + + // provide a call to action for the select element + cta := &editor.Element{ + TagName: "option", + Attrs: map[string]string{"disabled": "true", "selected": "true"}, + Data: "Select an option...", + ViewBuf: &bytes.Buffer{}, + } + + // provide a selection reset (will store empty string in db) + reset := &editor.Element{ + TagName: "option", + Attrs: map[string]string{"value": ""}, + Data: "None", + ViewBuf: &bytes.Buffer{}, + } + + opts = append(opts, cta, reset) + + for k, v := range options { + optAttrs := map[string]string{"value": k} + if k == val { + optAttrs["selected"] = "true" + } + opt := &editor.Element{ + TagName: "option", + Attrs: optAttrs, + Data: v, + ViewBuf: &bytes.Buffer{}, + } + + opts = append(opts, opt) + } + + _, err := html.Write(editor.DOMElementWithChildrenSelect(sel, opts)) + if err != nil { + log.Println("Error writing DOMElementWithChildrenSelect to SelectRepeater buffer") + return nil + } + } + + _, err = html.WriteString("</span>") + if err != nil { + log.Println("Error writing HTML string to SelectRepeater buffer") + return nil + } + + return append(html.Bytes(), editor.RepeatController(fieldName, p, "select", ".input-field")...) +} + +func encodeDataToOptions(contentType, tmplString string) (map[string]string, error) { + // encode all content type from db into options map // options in form of map["?type=<contentType>&id=<id>"]t.String() options := make(map[string]string) @@ -28,7 +123,7 @@ func Select(fieldName string, p interface{}, attrs map[string]string, contentTyp err := json.Unmarshal(j, &all) if err != nil { - return nil + return nil, err } // make template for option html display @@ -43,12 +138,13 @@ func Select(fieldName string, p interface{}, attrs map[string]string, contentTyp v := &bytes.Buffer{} err := tmpl.Execute(v, item) if err != nil { - log.Println("Error executing template for reference of:", contentType) - return nil + return nil, fmt.Errorf( + "Error executing template for reference of %s: %s", + contentType, err.Error()) } options[k] = v.String() } - return editor.Select(fieldName, p, attrs, options) + return options, nil } diff --git a/cmd/ponzu/vendor/github.com/tidwall/gjson/LICENSE b/cmd/ponzu/vendor/github.com/tidwall/gjson/LICENSE new file mode 100644 index 0000000..58f5819 --- /dev/null +++ b/cmd/ponzu/vendor/github.com/tidwall/gjson/LICENSE @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2016 Josh Baker + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/cmd/ponzu/vendor/github.com/tidwall/gjson/README.md b/cmd/ponzu/vendor/github.com/tidwall/gjson/README.md new file mode 100644 index 0000000..bc26c3c --- /dev/null +++ b/cmd/ponzu/vendor/github.com/tidwall/gjson/README.md @@ -0,0 +1,371 @@ +<p align="center"> +<img + src="logo.png" + width="240" height="78" border="0" alt="GJSON"> +<br> +<a href="https://travis-ci.org/tidwall/gjson"><img src="https://img.shields.io/travis/tidwall/gjson.svg?style=flat-square" alt="Build Status"></a><!-- +<a href="http://gocover.io/github.com/tidwall/gjson"><img src="https://img.shields.io/badge/coverage-97%25-brightgreen.svg?style=flat-square" alt="Code Coverage"></a> +--> +<a href="https://godoc.org/github.com/tidwall/gjson"><img src="https://img.shields.io/badge/api-reference-blue.svg?style=flat-square" alt="GoDoc"></a> +</p> + +<p align="center">get a json value quickly</a></p> + +GJSON is a Go package that provides a [very fast](#performance) and simple way to get a value from a json document. The purpose for this library it to give efficient json indexing for the [BuntDB](https://github.com/tidwall/buntdb) project. + +For a command line interface check out [JSONed](https://github.com/tidwall/jsoned). + +Getting Started +=============== + +## Installing + +To start using GJSON, install Go and run `go get`: + +```sh +$ go get -u github.com/tidwall/gjson +``` + +This will retrieve the library. + +## Get a value +Get searches json for the specified path. A path is in dot syntax, such as "name.last" or "age". This function expects that the json is well-formed and validates. Invalid json will not panic, but it may return back unexpected results. When the value is found it's returned immediately. + +```go +package main + +import "github.com/tidwall/gjson" + +const json = `{"name":{"first":"Janet","last":"Prichard"},"age":47}` + +func main() { + value := gjson.Get(json, "name.last") + println(value.String()) +} +``` + +This will print: + +``` +Prichard +``` +*There's also the [GetMany](#get-multiple-values-at-once) function to get multiple values at once, and [GetBytes](#working-with-bytes) for working with JSON byte slices.* + +## Path Syntax + +A path is a series of keys separated by a dot. +A key may contain special wildcard characters '\*' and '?'. +To access an array value use the index as the key. +To get the number of elements in an array or to access a child path, use the '#' character. +The dot and wildcard characters can be escaped with '\'. + +```json +{ + "name": {"first": "Tom", "last": "Anderson"}, + "age":37, + "children": ["Sara","Alex","Jack"], + "fav.movie": "Deer Hunter", + "friends": [ + {"first": "Dale", "last": "Murphy", "age": 44}, + {"first": "Roger", "last": "Craig", "age": 68}, + {"first": "Jane", "last": "Murphy", "age": 47} + ] +} +``` +``` +"name.last" >> "Anderson" +"age" >> 37 +"children" >> ["Sara","Alex","Jack"] +"children.#" >> 3 +"children.1" >> "Alex" +"child*.2" >> "Jack" +"c?ildren.0" >> "Sara" +"fav\.movie" >> "Deer Hunter" +"friends.#.first" >> ["Dale","Roger","Jane"] +"friends.1.last" >> "Craig" +``` + +You can also query an array for the first match by using `#[...]`, or find all matches with `#[...]#`. +Queries support the `==`, `!=`, `<`, `<=`, `>`, `>=` comparison operators and the simple pattern matching `%` operator. + +``` +friends.#[last=="Murphy"].first >> "Dale" +friends.#[last=="Murphy"]#.first >> ["Dale","Jane"] +friends.#[age>45]#.last >> ["Craig","Murphy"] +friends.#[first%"D*"].last >> "Murphy" +``` + +## Result Type + +GJSON supports the json types `string`, `number`, `bool`, and `null`. +Arrays and Objects are returned as their raw json types. + +The `Result` type holds one of these: + +``` +bool, for JSON booleans +float64, for JSON numbers +string, for JSON string literals +nil, for JSON null +``` + +To directly access the value: + +```go +result.Type // can be String, Number, True, False, Null, or JSON +result.Str // holds the string +result.Num // holds the float64 number +result.Raw // holds the raw json +result.Index // index of raw value in original json, zero means index unknown +``` + +There are a variety of handy functions that work on a result: + +```go +result.Value() interface{} +result.Int() int64 +result.Uint() uint64 +result.Float() float64 +result.String() string +result.Bool() bool +result.Array() []gjson.Result +result.Map() map[string]gjson.Result +result.Get(path string) Result +result.ForEach(iterator func(key, value Result) bool) +result.Less(token Result, caseSensitive bool) bool +``` + +The `result.Value()` function returns an `interface{}` which requires type assertion and is one of the following Go types: + + + +The `result.Array()` function returns back an array of values. +If the result represents a non-existent value, then an empty array will be returned. +If the result is not a JSON array, the return value will be an array containing one result. + +```go +boolean >> bool +number >> float64 +string >> string +null >> nil +array >> []interface{} +object >> map[string]interface{} +``` + +## Get nested array values + +Suppose you want all the last names from the following json: + +```json +{ + "programmers": [ + { + "firstName": "Janet", + "lastName": "McLaughlin", + }, { + "firstName": "Elliotte", + "lastName": "Hunter", + }, { + "firstName": "Jason", + "lastName": "Harold", + } + ] +}` +``` + +You would use the path "programmers.#.lastName" like such: + +```go +result := gjson.Get(json, "programmers.#.lastName") +for _,name := range result.Array() { + println(name.String()) +} +``` + +You can also query an object inside an array: + +```go +name := gjson.Get(json, `programmers.#[lastName="Hunter"].firstName`) +println(name.String()) // prints "Elliotte" +``` + +## Iterate through an object or array + +The `ForEach` function allows for quickly iterating through an object or array. +The key and value are passed to the iterator function for objects. +Only the value is passed for arrays. +Returning `false` from an iterator will stop iteration. + +```go +result := gjson.Get(json, "programmers") +result.ForEach(func(key, value gjson.Result) bool{ + println(value.String()) + return true // keep iterating +}) +``` + +## Simple Parse and Get + +There's a `Parse(json)` function that will do a simple parse, and `result.Get(path)` that will search a result. + +For example, all of these will return the same result: + +```go +gjson.Parse(json).Get("name").Get("last") +gjson.Get(json, "name").Get("last") +gjson.Get(json, "name.last") +``` + +## Check for the existence of a value + +Sometimes you just want to know if a value exists. + +```go +value := gjson.Get(json, "name.last") +if !value.Exists() { + println("no last name") +} else { + println(value.String()) +} + +// Or as one step +if gjson.Get(json, "name.last").Exists(){ + println("has a last name") +} +``` + +## Unmarshal to a map + +To unmarshal to a `map[string]interface{}`: + +```go +m, ok := gjson.Parse(json).Value().(map[string]interface{}) +if !ok{ + // not a map +} +``` + +## Working with Bytes + +If your JSON is contained in a `[]byte` slice, there's the [GetBytes](https://godoc.org/github.com/tidwall/gjson#GetBytes) function. This is preferred over `Get(string(data), path)`. + +```go +var json []byte = ... +result := gjson.GetBytes(json, path) +``` + +If you are using the `gjson.GetBytes(json, path)` function and you want to avoid converting `result.Raw` to a `[]byte`, then you can use this pattern: + +```go +var json []byte = ... +result := gjson.GetBytes(json, path) +var raw []byte +if result.Index > 0 { + raw = json[result.Index:result.Index+len(result.Raw)] +} else { + raw = []byte(result.Raw) +} +``` + +This is a best-effort no allocation sub slice of the original json. This method utilizes the `result.Index` field, which is the position of the raw data in the original json. It's possible that the value of `result.Index` equals zero, in which case the `result.Raw` is converted to a `[]byte`. + +## Get multiple values at once + +The `GetMany` function can be used to get multiple values at the same time, and is optimized to scan over a JSON payload once. + +```go +results := gjson.GetMany(json, "name.first", "name.last", "age") +``` + +The return value is a `[]Result`, which will always contain exactly the same number of items as the input paths. + +## Performance + +Benchmarks of GJSON alongside [encoding/json](https://golang.org/pkg/encoding/json/), +[ffjson](https://github.com/pquerna/ffjson), +[EasyJSON](https://github.com/mailru/easyjson), +and [jsonparser](https://github.com/buger/jsonparser) + +``` +BenchmarkGJSONGet-8 15000000 333 ns/op 0 B/op 0 allocs/op +BenchmarkGJSONUnmarshalMap-8 900000 4188 ns/op 1920 B/op 26 allocs/op +BenchmarkJSONUnmarshalMap-8 600000 8908 ns/op 3048 B/op 69 allocs/op +BenchmarkJSONUnmarshalStruct-8 600000 9026 ns/op 1832 B/op 69 allocs/op +BenchmarkJSONDecoder-8 300000 14339 ns/op 4224 B/op 184 allocs/op +BenchmarkFFJSONLexer-8 1500000 3156 ns/op 896 B/op 8 allocs/op +BenchmarkEasyJSONLexer-8 3000000 938 ns/op 613 B/op 6 allocs/op +BenchmarkJSONParserGet-8 3000000 442 ns/op 21 B/op 0 allocs/op +``` + +Benchmarks for the `GetMany` function: + +``` +BenchmarkGJSONGetMany4Paths-8 4000000 319 ns/op 112 B/op 0 allocs/op +BenchmarkGJSONGetMany8Paths-8 8000000 218 ns/op 56 B/op 0 allocs/op +BenchmarkGJSONGetMany16Paths-8 16000000 160 ns/op 56 B/op 0 allocs/op +BenchmarkGJSONGetMany32Paths-8 32000000 130 ns/op 64 B/op 0 allocs/op +BenchmarkGJSONGetMany64Paths-8 64000000 117 ns/op 64 B/op 0 allocs/op +BenchmarkGJSONGetMany128Paths-8 128000000 109 ns/op 64 B/op 0 allocs/op +``` + +JSON document used: + +```json +{ + "widget": { + "debug": "on", + "window": { + "title": "Sample Konfabulator Widget", + "name": "main_window", + "width": 500, + "height": 500 + }, + "image": { + "src": "Images/Sun.png", + "hOffset": 250, + "vOffset": 250, + "alignment": "center" + }, + "text": { + "data": "Click Here", + "size": 36, + "style": "bold", + "vOffset": 100, + "alignment": "center", + "onMouseUp": "sun1.opacity = (sun1.opacity / 100) * 90;" + } + } +} +``` + +Each operation was rotated though one of the following search paths: + +``` +widget.window.name +widget.image.hOffset +widget.text.onMouseUp +``` + +For the `GetMany` benchmarks these paths are used: + +``` +widget.window.name +widget.image.hOffset +widget.text.onMouseUp +widget.window.title +widget.image.alignment +widget.text.style +widget.window.height +widget.image.src +widget.text.data +widget.text.size +``` + +*These benchmarks were run on a MacBook Pro 15" 2.8 GHz Intel Core i7 using Go 1.7.* + +## Contact +Josh Baker [@tidwall](http://twitter.com/tidwall) + +## License + +GJSON source code is available under the MIT [License](/LICENSE). diff --git a/cmd/ponzu/vendor/github.com/tidwall/gjson/gjson.go b/cmd/ponzu/vendor/github.com/tidwall/gjson/gjson.go new file mode 100644 index 0000000..1ee26c9 --- /dev/null +++ b/cmd/ponzu/vendor/github.com/tidwall/gjson/gjson.go @@ -0,0 +1,1942 @@ +// Package gjson provides searching for json strings. +package gjson + +import ( + "reflect" + "strconv" + "unsafe" + + "github.com/tidwall/match" +) + +// Type is Result type +type Type int + +const ( + // Null is a null json value + Null Type = iota + // False is a json false boolean + False + // Number is json number + Number + // String is a json string + String + // True is a json true boolean + True + // JSON is a raw block of JSON + JSON +) + +// String returns a string representation of the type. +func (t Type) String() string { + switch t { + default: + return "" + case Null: + return "Null" + case False: + return "False" + case Number: + return "Number" + case String: + return "String" + case True: + return "True" + case JSON: + return "JSON" + } +} + +// Result represents a json value that is returned from Get(). +type Result struct { + // Type is the json type + Type Type + // Raw is the raw json + Raw string + // Str is the json string + Str string + // Num is the json number + Num float64 + // Index of raw value in original json, zero means index unknown + Index int +} + +// String returns a string representation of the value. +func (t Result) String() string { + switch t.Type { + default: + return "null" + case False: + return "false" + case Number: + return strconv.FormatFloat(t.Num, 'f', -1, 64) + case String: + return t.Str + case JSON: + return t.Raw + case True: + return "true" + } +} + +// Bool returns an boolean representation. +func (t Result) Bool() bool { + switch t.Type { + default: + return false + case True: + return true + case String: + return t.Str != "" && t.Str != "0" + case Number: + return t.Num != 0 + } +} + +// Int returns an integer representation. +func (t Result) Int() int64 { + switch t.Type { + default: + return 0 + case True: + return 1 + case String: + n, _ := strconv.ParseInt(t.Str, 10, 64) + return n + case Number: + return int64(t.Num) + } +} + +// Uint returns an unsigned integer representation. +func (t Result) Uint() uint64 { + switch t.Type { + default: + return 0 + case True: + return 1 + case String: + n, _ := strconv.ParseUint(t.Str, 10, 64) + return n + case Number: + return uint64(t.Num) + } +} + +// Float returns an float64 representation. +func (t Result) Float() float64 { + switch t.Type { + default: + return 0 + case True: + return 1 + case String: + n, _ := strconv.ParseFloat(t.Str, 64) + return n + case Number: + return t.Num + } +} + +// Array returns back an array of values. +// If the result represents a non-existent value, then an empty array will be returned. +// If the result is not a JSON array, the return value will be an array containing one result. +func (t Result) Array() []Result { + if !t.Exists() { + return nil + } + if t.Type != JSON { + return []Result{t} + } + r := t.arrayOrMap('[', false) + return r.a +} + +// ForEach iterates through values. +// If the result represents a non-existent value, then no values will be iterated. +// If the result is an Object, the iterator will pass the key and value of each item. +// If the result is an Array, the iterator will only pass the value of each item. +// If the result is not a JSON array or object, the iterator will pass back one value equal to the result. +func (t Result) ForEach(iterator func(key, value Result) bool) { + if !t.Exists() { + return + } + if t.Type != JSON { + iterator(Result{}, t) + return + } + json := t.Raw + var keys bool + var i int + var key, value Result + for ; i < len(json); i++ { + if json[i] == '{' { + i++ + key.Type = String + keys = true + break + } else if json[i] == '[' { + i++ + break + } + if json[i] > ' ' { + return + } + } + var str string + var vesc bool + var ok bool + for ; i < len(json); i++ { + if keys { + if json[i] != '"' { + continue + } + s := i + i, str, vesc, ok = parseString(json, i+1) + if !ok { + return + } + if vesc { + key.Str = unescape(str[1 : len(str)-1]) + } else { + key.Str = str[1 : len(str)-1] + } + key.Raw = str + key.Index = s + } + for ; i < len(json); i++ { + if json[i] <= ' ' || json[i] == ',' || json[i] == ':' { + continue + } + break + } + s := i + i, value, ok = parseAny(json, i, true) + if !ok { + return + } + value.Index = s + if !iterator(key, value) { + return + } + } +} + +// Map returns back an map of values. The result should be a JSON array. +func (t Result) Map() map[string]Result { + if t.Type != JSON { + return map[string]Result{} + } + r := t.arrayOrMap('{', false) + return r.o +} + +// Get searches result for the specified path. +// The result should be a JSON array or object. +func (t Result) Get(path string) Result { + return Get(t.Raw, path) +} + +type arrayOrMapResult struct { + a []Result + ai []interface{} + o map[string]Result + oi map[string]interface{} + vc byte +} + +func (t Result) arrayOrMap(vc byte, valueize bool) (r arrayOrMapResult) { + var json = t.Raw + var i int + var value Result + var count int + var key Result + if vc == 0 { + for ; i < len(json); i++ { + if json[i] == '{' || json[i] == '[' { + r.vc = json[i] + i++ + break + } + if json[i] > ' ' { + goto end + } + } + } else { + for ; i < len(json); i++ { + if json[i] == vc { + i++ + break + } + if json[i] > ' ' { + goto end + } + } + r.vc = vc + } + if r.vc == '{' { + if valueize { + r.oi = make(map[string]interface{}) + } else { + r.o = make(map[string]Result) + } + } else { + if valueize { + r.ai = make([]interface{}, 0) + } else { + r.a = make([]Result, 0) + } + } + for ; i < len(json); i++ { + if json[i] <= ' ' { + continue + } + // get next value + if json[i] == ']' || json[i] == '}' { + break + } + switch json[i] { + default: + if (json[i] >= '0' && json[i] <= '9') || json[i] == '-' { + value.Type = Number + value.Raw, value.Num = tonum(json[i:]) + } else { + continue + } + case '{', '[': + value.Type = JSON + value.Raw = squash(json[i:]) + case 'n': + value.Type = Null + value.Raw = tolit(json[i:]) + case 't': + value.Type = True + value.Raw = tolit(json[i:]) + case 'f': + value.Type = False + value.Raw = tolit(json[i:]) + case '"': + value.Type = String + value.Raw, value.Str = tostr(json[i:]) + } + i += len(value.Raw) - 1 + + if r.vc == '{' { + if count%2 == 0 { + key = value + } else { + if valueize { + r.oi[key.Str] = value.Value() + } else { + r.o[key.Str] = value + } + } + count++ + } else { + if valueize { + r.ai = append(r.ai, value.Value()) + } else { + r.a = append(r.a, value) + } + } + } +end: + return +} + +// Parse parses the json and returns a result. +func Parse(json string) Result { + var value Result + for i := 0; i < len(json); i++ { + if json[i] == '{' || json[i] == '[' { + value.Type = JSON + value.Raw = json[i:] // just take the entire raw + break + } + if json[i] <= ' ' { + continue + } + switch json[i] { + default: + if (json[i] >= '0' && json[i] <= '9') || json[i] == '-' { + value.Type = Number + value.Raw, value.Num = tonum(json[i:]) + } else { + return Result{} + } + case 'n': + value.Type = Null + value.Raw = tolit(json[i:]) + case 't': + value.Type = True + value.Raw = tolit(json[i:]) + case 'f': + value.Type = False + value.Raw = tolit(json[i:]) + case '"': + value.Type = String + value.Raw, value.Str = tostr(json[i:]) + } + break + } + return value +} + +// ParseBytes parses the json and returns a result. +// If working with bytes, this method preferred over Parse(string(data)) +func ParseBytes(json []byte) Result { + return Parse(string(json)) +} + +func squash(json string) string { + // expects that the lead character is a '[' or '{' + // squash the value, ignoring all nested arrays and objects. + // the first '[' or '{' has already been read + depth := 1 + for i := 1; i < len(json); i++ { + if json[i] >= '"' && json[i] <= '}' { + switch json[i] { + case '"': + i++ + s2 := i + for ; i < len(json); i++ { + if json[i] > '\\' { + continue + } + if json[i] == '"' { + // look for an escaped slash + if json[i-1] == '\\' { + n := 0 + for j := i - 2; j > s2-1; j-- { + if json[j] != '\\' { + break + } + n++ + } + if n%2 == 0 { + continue + } + } + break + } + } + case '{', '[': + depth++ + case '}', ']': + depth-- + if depth == 0 { + return json[:i+1] + } + } + } + } + return json +} + +func tonum(json string) (raw string, num float64) { + for i := 1; i < len(json); i++ { + // less than dash might have valid characters + if json[i] <= '-' { + if json[i] <= ' ' || json[i] == ',' { + // break on whitespace and comma + raw = json[:i] + num, _ = strconv.ParseFloat(raw, 64) + return + } + // could be a '+' or '-'. let's assume so. + continue + } + if json[i] < ']' { + // probably a valid number + continue + } + if json[i] == 'e' || json[i] == 'E' { + // allow for exponential numbers + continue + } + // likely a ']' or '}' + raw = json[:i] + num, _ = strconv.ParseFloat(raw, 64) + return + } + raw = json + num, _ = strconv.ParseFloat(raw, 64) + return +} + +func tolit(json string) (raw string) { + for i := 1; i < len(json); i++ { + if json[i] <= 'a' || json[i] >= 'z' { + return json[:i] + } + } + return json +} + +func tostr(json string) (raw string, str string) { + // expects that the lead character is a '"' + for i := 1; i < len(json); i++ { + if json[i] > '\\' { + continue + } + if json[i] == '"' { + return json[:i+1], json[1:i] + } + if json[i] == '\\' { + i++ + for ; i < len(json); i++ { + if json[i] > '\\' { + continue + } + if json[i] == '"' { + // look for an escaped slash + if json[i-1] == '\\' { + n := 0 + for j := i - 2; j > 0; j-- { + if json[j] != '\\' { + break + } + n++ + } + if n%2 == 0 { + continue + } + } + break + } + } + var ret string + if i+1 < len(json) { + ret = json[:i+1] + } else { + ret = json[:i] + } + return ret, unescape(json[1:i]) + } + } + return json, json[1:] +} + +// Exists returns true if value exists. +// +// if gjson.Get(json, "name.last").Exists(){ +// println("value exists") +// } +func (t Result) Exists() bool { + return t.Type != Null || len(t.Raw) != 0 +} + +// Value returns one of these types: +// +// bool, for JSON booleans +// float64, for JSON numbers +// Number, for JSON numbers +// string, for JSON string literals +// nil, for JSON null +// +func (t Result) Value() interface{} { + if t.Type == String { + return t.Str + } + switch t.Type { + default: + return nil + case False: + return false + case Number: + return t.Num + case JSON: + r := t.arrayOrMap(0, true) + if r.vc == '{' { + return r.oi + } else if r.vc == '[' { + return r.ai + } + return nil + case True: + return true + } +} + +func parseString(json string, i int) (int, string, bool, bool) { + var s = i + for ; i < len(json); i++ { + if json[i] > '\\' { + continue + } + if json[i] == '"' { + return i + 1, json[s-1 : i+1], false, true + } + if json[i] == '\\' { + i++ + for ; i < len(json); i++ { + if json[i] > '\\' { + continue + } + if json[i] == '"' { + // look for an escaped slash + if json[i-1] == '\\' { + n := 0 + for j := i - 2; j > 0; j-- { + if json[j] != '\\' { + break + } + n++ + } + if n%2 == 0 { + continue + } + } + return i + 1, json[s-1 : i+1], true, true + } + } + break + } + } + return i, json[s-1:], false, false +} + +func parseNumber(json string, i int) (int, string) { + var s = i + i++ + for ; i < len(json); i++ { + if json[i] <= ' ' || json[i] == ',' || json[i] == ']' || json[i] == '}' { + return i, json[s:i] + } + } + return i, json[s:] +} + +func parseLiteral(json string, i int) (int, string) { + var s = i + i++ + for ; i < len(json); i++ { + if json[i] < 'a' || json[i] > 'z' { + return i, json[s:i] + } + } + return i, json[s:] +} + +type arrayPathResult struct { + part string + path string + more bool + alogok bool + arrch bool + alogkey string + query struct { + on bool + path string + op string + value string + all bool + } +} + +func parseArrayPath(path string) (r arrayPathResult) { + for i := 0; i < len(path); i++ { + if path[i] == '.' { + r.part = path[:i] + r.path = path[i+1:] + r.more = true + return + } + if path[i] == '#' { + r.arrch = true + if i == 0 && len(path) > 1 { + if path[1] == '.' { + r.alogok = true + r.alogkey = path[2:] + r.path = path[:1] + } else if path[1] == '[' { + r.query.on = true + // query + i += 2 + // whitespace + for ; i < len(path); i++ { + if path[i] > ' ' { + break + } + } + s := i + for ; i < len(path); i++ { + if path[i] <= ' ' || + path[i] == '!' || + path[i] == '=' || + path[i] == '<' || + path[i] == '>' || + path[i] == '%' || + path[i] == ']' { + break + } + } + r.query.path = path[s:i] + // whitespace + for ; i < len(path); i++ { + if path[i] > ' ' { + break + } + } + if i < len(path) { + s = i + if path[i] == '!' { + if i < len(path)-1 && path[i+1] == '=' { + i++ + } + } else if path[i] == '<' || path[i] == '>' { + if i < len(path)-1 && path[i+1] == '=' { + i++ + } + } else if path[i] == '=' { + if i < len(path)-1 && path[i+1] == '=' { + s++ + i++ + } + } + i++ + r.query.op = path[s:i] + // whitespace + for ; i < len(path); i++ { + if path[i] > ' ' { + break + } + } + s = i + for ; i < len(path); i++ { + if path[i] == '"' { + i++ + s2 := i + for ; i < len(path); i++ { + if path[i] > '\\' { + continue + } + if path[i] == '"' { + // look for an escaped slash + if path[i-1] == '\\' { + n := 0 + for j := i - 2; j > s2-1; j-- { + if path[j] != '\\' { + break + } + n++ + } + if n%2 == 0 { + continue + } + } + break + } + } + } else if path[i] == ']' { + if i+1 < len(path) && path[i+1] == '#' { + r.query.all = true + } + break + } + } + if i > len(path) { + i = len(path) + } + v := path[s:i] + for len(v) > 0 && v[len(v)-1] <= ' ' { + v = v[:len(v)-1] + } + r.query.value = v + } + } + } + continue + } + } + r.part = path + r.path = "" + return +} + +type objectPathResult struct { + part string + path string + wild bool + more bool +} + +func parseObjectPath(path string) (r objectPathResult) { + for i := 0; i < len(path); i++ { + if path[i] == '.' { + r.part = path[:i] + r.path = path[i+1:] + r.more = true + return + } + if path[i] == '*' || path[i] == '?' { + r.wild = true + continue + } + if path[i] == '\\' { + // go into escape mode. this is a slower path that + // strips off the escape character from the part. + epart := []byte(path[:i]) + i++ + if i < len(path) { + epart = append(epart, path[i]) + i++ + for ; i < len(path); i++ { + if path[i] == '\\' { + i++ + if i < len(path) { + epart = append(epart, path[i]) + } + continue + } else if path[i] == '.' { + r.part = string(epart) + r.path = path[i+1:] + r.more = true + return + } else if path[i] == '*' || path[i] == '?' { + r.wild = true + } + epart = append(epart, path[i]) + } + } + // append the last part + r.part = string(epart) + return + } + } + r.part = path + return +} + +func parseSquash(json string, i int) (int, string) { + // expects that the lead character is a '[' or '{' + // squash the value, ignoring all nested arrays and objects. + // the first '[' or '{' has already been read + s := i + i++ + depth := 1 + for ; i < len(json); i++ { + if json[i] >= '"' && json[i] <= '}' { + switch json[i] { + case '"': + i++ + s2 := i + for ; i < len(json); i++ { + if json[i] > '\\' { + continue + } + if json[i] == '"' { + // look for an escaped slash + if json[i-1] == '\\' { + n := 0 + for j := i - 2; j > s2-1; j-- { + if json[j] != '\\' { + break + } + n++ + } + if n%2 == 0 { + continue + } + } + break + } + } + case '{', '[': + depth++ + case '}', ']': + depth-- + if depth == 0 { + i++ + return i, json[s:i] + } + } + } + } + return i, json[s:] +} + +func parseObject(c *parseContext, i int, path string) (int, bool) { + var pmatch, kesc, vesc, ok, hit bool + var key, val string + rp := parseObjectPath(path) + for i < len(c.json) { + for ; i < len(c.json); i++ { + if c.json[i] == '"' { + // parse_key_string + // this is slightly different from getting s string value + // because we don't need the outer quotes. + i++ + var s = i + for ; i < len(c.json); i++ { + if c.json[i] > '\\' { + continue + } + if c.json[i] == '"' { + i, key, kesc, ok = i+1, c.json[s:i], false, true + goto parse_key_string_done + } + if c.json[i] == '\\' { + i++ + for ; i < len(c.json); i++ { + if c.json[i] > '\\' { + continue + } + if c.json[i] == '"' { + // look for an escaped slash + if c.json[i-1] == '\\' { + n := 0 + for j := i - 2; j > 0; j-- { + if c.json[j] != '\\' { + break + } + n++ + } + if n%2 == 0 { + continue + } + } + i, key, kesc, ok = i+1, c.json[s:i], true, true + goto parse_key_string_done + } + } + break + } + } + i, key, kesc, ok = i, c.json[s:], false, false + parse_key_string_done: + break + } + if c.json[i] == '}' { + return i + 1, false + } + } + if !ok { + return i, false + } + if rp.wild { + if kesc { + pmatch = match.Match(unescape(key), rp.part) + } else { + pmatch = match.Match(key, rp.part) + } + } else { + if kesc { + pmatch = rp.part == unescape(key) + } else { + pmatch = rp.part == key + } + } + hit = pmatch && !rp.more + for ; i < len(c.json); i++ { + switch c.json[i] { + default: + continue + case '"': + i++ + i, val, vesc, ok = parseString(c.json, i) + if !ok { + return i, false + } + if hit { + if vesc { + c.value.Str = unescape(val[1 : len(val)-1]) + } else { + c.value.Str = val[1 : len(val)-1] + } + c.value.Raw = val + c.value.Type = String + return i, true + } + case '{': + if pmatch && !hit { + i, hit = parseObject(c, i+1, rp.path) + if hit { + return i, true + } + } else { + i, val = parseSquash(c.json, i) + if hit { + c.value.Raw = val + c.value.Type = JSON + return i, true + } + } + case '[': + if pmatch && !hit { + i, hit = parseArray(c, i+1, rp.path) + if hit { + return i, true + } + } else { + i, val = parseSquash(c.json, i) + if hit { + c.value.Raw = val + c.value.Type = JSON + return i, true + } + } + case '-', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': + i, val = parseNumber(c.json, i) + if hit { + c.value.Raw = val + c.value.Type = Number + c.value.Num, _ = strconv.ParseFloat(val, 64) + return i, true + } + case 't', 'f', 'n': + vc := c.json[i] + i, val = parseLiteral(c.json, i) + if hit { + c.value.Raw = val + switch vc { + case 't': + c.value.Type = True + case 'f': + c.value.Type = False + } + return i, true + } + } + break + } + } + return i, false +} +func queryMatches(rp *arrayPathResult, value Result) bool { + rpv := rp.query.value + if len(rpv) > 2 && rpv[0] == '"' && rpv[len(rpv)-1] == '"' { + rpv = rpv[1 : len(rpv)-1] + } + switch value.Type { + case String: + switch rp.query.op { + case "=": + return value.Str == rpv + case "!=": + return value.Str != rpv + case "<": + return value.Str < rpv + case "<=": + return value.Str <= rpv + case ">": + return value.Str > rpv + case ">=": + return value.Str >= rpv + case "%": + return match.Match(value.Str, rpv) + } + case Number: + rpvn, _ := strconv.ParseFloat(rpv, 64) + switch rp.query.op { + case "=": + return value.Num == rpvn + case "!=": + return value.Num == rpvn + case "<": + return value.Num < rpvn + case "<=": + return value.Num <= rpvn + case ">": + return value.Num > rpvn + case ">=": + return value.Num >= rpvn + } + case True: + switch rp.query.op { + case "=": + return rpv == "true" + case "!=": + return rpv != "true" + case ">": + return rpv == "false" + case ">=": + return true + } + case False: + switch rp.query.op { + case "=": + return rpv == "false" + case "!=": + return rpv != "false" + case "<": + return rpv == "true" + case "<=": + return true + } + } + return false +} +func parseArray(c *parseContext, i int, path string) (int, bool) { + var pmatch, vesc, ok, hit bool + var val string + var h int + var alog []int + var partidx int + var multires []byte + rp := parseArrayPath(path) + if !rp.arrch { + n, err := strconv.ParseUint(rp.part, 10, 64) + if err != nil { + partidx = -1 + } else { + partidx = int(n) + } + } + for i < len(c.json) { + if !rp.arrch { + pmatch = partidx == h + hit = pmatch && !rp.more + } + h++ + if rp.alogok { + alog = append(alog, i) + } + for ; i < len(c.json); i++ { + switch c.json[i] { + default: + continue + case '"': + i++ + i, val, vesc, ok = parseString(c.json, i) + if !ok { + return i, false + } + if hit { + if rp.alogok { + break + } + if vesc { + c.value.Str = unescape(val[1 : len(val)-1]) + } else { + c.value.Str = val[1 : len(val)-1] + } + c.value.Raw = val + c.value.Type = String + return i, true + } + case '{': + if pmatch && !hit { + i, hit = parseObject(c, i+1, rp.path) + if hit { + if rp.alogok { + break + } + return i, true + } + } else { + i, val = parseSquash(c.json, i) + if rp.query.on { + res := Get(val, rp.query.path) + if queryMatches(&rp, res) { + if rp.more { + res = Get(val, rp.path) + } else { + res = Result{Raw: val, Type: JSON} + } + if rp.query.all { + if len(multires) == 0 { + multires = append(multires, '[') + } else { + multires = append(multires, ',') + } + multires = append(multires, res.Raw...) + } else { + c.value = res + return i, true + } + } + } else if hit { + if rp.alogok { + break + } + c.value.Raw = val + c.value.Type = JSON + return i, true + } + } + case '[': + if pmatch && !hit { + i, hit = parseArray(c, i+1, rp.path) + if hit { + if rp.alogok { + break + } + return i, true + } + } else { + i, val = parseSquash(c.json, i) + if hit { + if rp.alogok { + break + } + c.value.Raw = val + c.value.Type = JSON + return i, true + } + } + case '-', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': + i, val = parseNumber(c.json, i) + if hit { + if rp.alogok { + break + } + c.value.Raw = val + c.value.Type = Number + c.value.Num, _ = strconv.ParseFloat(val, 64) + return i, true + } + case 't', 'f', 'n': + vc := c.json[i] + i, val = parseLiteral(c.json, i) + if hit { + if rp.alogok { + break + } + c.value.Raw = val + switch vc { + case 't': + c.value.Type = True + case 'f': + c.value.Type = False + } + return i, true + } + case ']': + if rp.arrch && rp.part == "#" { + if rp.alogok { + var jsons = make([]byte, 0, 64) + jsons = append(jsons, '[') + for j, k := 0, 0; j < len(alog); j++ { + res := Get(c.json[alog[j]:], rp.alogkey) + if res.Exists() { + if k > 0 { + jsons = append(jsons, ',') + } + jsons = append(jsons, []byte(res.Raw)...) + k++ + } + } + jsons = append(jsons, ']') + c.value.Type = JSON + c.value.Raw = string(jsons) + return i + 1, true + } else { + if rp.alogok { + break + } + c.value.Raw = val + c.value.Type = Number + c.value.Num = float64(h - 1) + c.calcd = true + return i + 1, true + } + } + if len(multires) > 0 && !c.value.Exists() { + c.value = Result{ + Raw: string(append(multires, ']')), + Type: JSON, + } + } + return i + 1, false + } + break + } + } + return i, false +} + +type parseContext struct { + json string + value Result + calcd bool +} + +// Get searches json for the specified path. +// A path is in dot syntax, such as "name.last" or "age". +// This function expects that the json is well-formed, and does not validate. +// Invalid json will not panic, but it may return back unexpected results. +// When the value is found it's returned immediately. +// +// A path is a series of keys searated by a dot. +// A key may contain special wildcard characters '*' and '?'. +// To access an array value use the index as the key. +// To get the number of elements in an array or to access a child path, use the '#' character. +// The dot and wildcard character can be escaped with '\'. +// +// { +// "name": {"first": "Tom", "last": "Anderson"}, +// "age":37, +// "children": ["Sara","Alex","Jack"], +// "friends": [ +// {"first": "James", "last": "Murphy"}, +// {"first": "Roger", "last": "Craig"} +// ] +// } +// "name.last" >> "Anderson" +// "age" >> 37 +// "children" >> ["Sara","Alex","Jack"] +// "children.#" >> 3 +// "children.1" >> "Alex" +// "child*.2" >> "Jack" +// "c?ildren.0" >> "Sara" +// "friends.#.first" >> ["James","Roger"] +// +func Get(json, path string) Result { + var i int + var c = &parseContext{json: json} + for ; i < len(c.json); i++ { + if c.json[i] == '{' { + i++ + parseObject(c, i, path) + break + } + if c.json[i] == '[' { + i++ + parseArray(c, i, path) + break + } + } + if len(c.value.Raw) > 0 && !c.calcd { + jhdr := *(*reflect.StringHeader)(unsafe.Pointer(&json)) + rhdr := *(*reflect.StringHeader)(unsafe.Pointer(&(c.value.Raw))) + c.value.Index = int(rhdr.Data - jhdr.Data) + if c.value.Index < 0 || c.value.Index >= len(json) { + c.value.Index = 0 + } + } + return c.value +} +func fromBytesGet(result Result) Result { + // safely get the string headers + rawhi := *(*reflect.StringHeader)(unsafe.Pointer(&result.Raw)) + strhi := *(*reflect.StringHeader)(unsafe.Pointer(&result.Str)) + // create byte slice headers + rawh := reflect.SliceHeader{Data: rawhi.Data, Len: rawhi.Len} + strh := reflect.SliceHeader{Data: strhi.Data, Len: strhi.Len} + if strh.Data == 0 { + // str is nil + if rawh.Data == 0 { + // raw is nil + result.Raw = "" + } else { + // raw has data, safely copy the slice header to a string + result.Raw = string(*(*[]byte)(unsafe.Pointer(&rawh))) + } + result.Str = "" + } else if rawh.Data == 0 { + // raw is nil + result.Raw = "" + // str has data, safely copy the slice header to a string + result.Str = string(*(*[]byte)(unsafe.Pointer(&strh))) + } else if strh.Data >= rawh.Data && + int(strh.Data)+strh.Len <= int(rawh.Data)+rawh.Len { + // Str is a substring of Raw. + start := int(strh.Data - rawh.Data) + // safely copy the raw slice header + result.Raw = string(*(*[]byte)(unsafe.Pointer(&rawh))) + // substring the raw + result.Str = result.Raw[start : start+strh.Len] + } else { + // safely copy both the raw and str slice headers to strings + result.Raw = string(*(*[]byte)(unsafe.Pointer(&rawh))) + result.Str = string(*(*[]byte)(unsafe.Pointer(&strh))) + } + return result +} + +// GetBytes searches json for the specified path. +// If working with bytes, this method preferred over Get(string(data), path) +func GetBytes(json []byte, path string) Result { + var result Result + if json != nil { + // unsafe cast to string + result = Get(*(*string)(unsafe.Pointer(&json)), path) + result = fromBytesGet(result) + } + return result +} + +// unescape unescapes a string +func unescape(json string) string { //, error) { + var str = make([]byte, 0, len(json)) + for i := 0; i < len(json); i++ { + switch { + default: + str = append(str, json[i]) + case json[i] < ' ': + return "" //, errors.New("invalid character in string") + case json[i] == '\\': + i++ + if i >= len(json) { + return "" //, errors.New("invalid escape sequence") + } + switch json[i] { + default: + return "" //, errors.New("invalid escape sequence") + case '\\': + str = append(str, '\\') + case '/': + str = append(str, '/') + case 'b': + str = append(str, '\b') + case 'f': + str = append(str, '\f') + case 'n': + str = append(str, '\n') + case 'r': + str = append(str, '\r') + case 't': + str = append(str, '\t') + case '"': + str = append(str, '"') + case 'u': + if i+5 > len(json) { + return "" //, errors.New("invalid escape sequence") + } + i++ + // extract the codepoint + var code int + for j := i; j < i+4; j++ { + switch { + default: + return "" //, errors.New("invalid escape sequence") + case json[j] >= '0' && json[j] <= '9': + code += (int(json[j]) - '0') << uint(12-(j-i)*4) + case json[j] >= 'a' && json[j] <= 'f': + code += (int(json[j]) - 'a' + 10) << uint(12-(j-i)*4) + case json[j] >= 'a' && json[j] <= 'f': + code += (int(json[j]) - 'a' + 10) << uint(12-(j-i)*4) + } + } + str = append(str, []byte(string(code))...) + i += 3 // only 3 because we will increment on the for-loop + } + } + } + return string(str) //, nil +} + +// Less return true if a token is less than another token. +// The caseSensitive paramater is used when the tokens are Strings. +// The order when comparing two different type is: +// +// Null < False < Number < String < True < JSON +// +func (t Result) Less(token Result, caseSensitive bool) bool { + if t.Type < token.Type { + return true + } + if t.Type > token.Type { + return false + } + if t.Type == String { + if caseSensitive { + return t.Str < token.Str + } + return stringLessInsensitive(t.Str, token.Str) + } + if t.Type == Number { + return t.Num < token.Num + } + return t.Raw < token.Raw +} + +func stringLessInsensitive(a, b string) bool { + for i := 0; i < len(a) && i < len(b); i++ { + if a[i] >= 'A' && a[i] <= 'Z' { + if b[i] >= 'A' && b[i] <= 'Z' { + // both are uppercase, do nothing + if a[i] < b[i] { + return true + } else if a[i] > b[i] { + return false + } + } else { + // a is uppercase, convert a to lowercase + if a[i]+32 < b[i] { + return true + } else if a[i]+32 > b[i] { + return false + } + } + } else if b[i] >= 'A' && b[i] <= 'Z' { + // b is uppercase, convert b to lowercase + if a[i] < b[i]+32 { + return true + } else if a[i] > b[i]+32 { + return false + } + } else { + // neither are uppercase + if a[i] < b[i] { + return true + } else if a[i] > b[i] { + return false + } + } + } + return len(a) < len(b) +} + +// parseAny parses the next value from a json string. +// A Result is returned when the hit param is set. +// The return values are (i int, res Result, ok bool) +func parseAny(json string, i int, hit bool) (int, Result, bool) { + var res Result + var val string + for ; i < len(json); i++ { + if json[i] == '{' || json[i] == '[' { + i, val = parseSquash(json, i) + if hit { + res.Raw = val + res.Type = JSON + } + return i, res, true + } + if json[i] <= ' ' { + continue + } + switch json[i] { + case '"': + i++ + var vesc bool + var ok bool + i, val, vesc, ok = parseString(json, i) + if !ok { + return i, res, false + } + if hit { + res.Type = String + res.Raw = val + if vesc { + res.Str = unescape(val[1 : len(val)-1]) + } else { + res.Str = val[1 : len(val)-1] + } + } + return i, res, true + case '-', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': + i, val = parseNumber(json, i) + if hit { + res.Raw = val + res.Type = Number + res.Num, _ = strconv.ParseFloat(val, 64) + } + return i, res, true + case 't', 'f', 'n': + vc := json[i] + i, val = parseLiteral(json, i) + if hit { + res.Raw = val + switch vc { + case 't': + res.Type = True + case 'f': + res.Type = False + } + return i, res, true + } + } + } + return i, res, false +} + +var ( // used for testing + testWatchForFallback bool + testLastWasFallback bool +) + +// areSimplePaths returns true if all the paths are simple enough +// to parse quickly for GetMany(). Allows alpha-numeric, dots, +// underscores, and the dollar sign. It does not allow non-alnum, +// escape characters, or keys which start with a numbers. +// For example: +// "name.last" == OK +// "user.id0" == OK +// "user.ID" == OK +// "user.first_name" == OK +// "user.firstName" == OK +// "user.0item" == BAD +// "user.#id" == BAD +// "user\.name" == BAD +func areSimplePaths(paths []string) bool { + for _, path := range paths { + var fi int // first key index, for keys with numeric prefix + for i := 0; i < len(path); i++ { + if path[i] >= 'a' && path[i] <= 'z' { + // a-z is likely to be the highest frequency charater. + continue + } + if path[i] == '.' { + fi = i + 1 + continue + } + if path[i] >= 'A' && path[i] <= 'Z' { + continue + } + if path[i] == '_' || path[i] == '$' { + continue + } + if i > fi && path[i] >= '0' && path[i] <= '9' { + continue + } + return false + } + } + return true +} + +// GetMany searches json for the multiple paths. +// The return value is a Result array where the number of items +// will be equal to the number of input paths. +func GetMany(json string, paths ...string) []Result { + if len(paths) < 4 { + if testWatchForFallback { + testLastWasFallback = false + } + switch len(paths) { + case 0: + // return nil when no paths are specified. + return nil + case 1: + return []Result{Get(json, paths[0])} + case 2: + return []Result{Get(json, paths[0]), Get(json, paths[1])} + case 3: + return []Result{Get(json, paths[0]), Get(json, paths[1]), Get(json, paths[2])} + } + } + var results []Result + var ok bool + var i int + if len(paths) > 512 { + // we can only support up to 512 paths. Is that too many? + goto fallback + } + if !areSimplePaths(paths) { + // If there is even one path that is not considered "simple" then + // we need to use the fallback method. + goto fallback + } + // locate the object token. + for ; i < len(json); i++ { + if json[i] == '{' { + i++ + break + } + if json[i] <= ' ' { + continue + } + goto fallback + } + // use the call function table. + if len(paths) <= 8 { + results, ok = getMany8(json, i, paths) + } else if len(paths) <= 16 { + results, ok = getMany16(json, i, paths) + } else if len(paths) <= 32 { + results, ok = getMany32(json, i, paths) + } else if len(paths) <= 64 { + results, ok = getMany64(json, i, paths) + } else if len(paths) <= 128 { + results, ok = getMany128(json, i, paths) + } else if len(paths) <= 256 { + results, ok = getMany256(json, i, paths) + } else if len(paths) <= 512 { + results, ok = getMany512(json, i, paths) + } + if !ok { + // there was some fault while parsing. we should try the + // fallback method. This could result in performance + // degregation in some cases. + goto fallback + } + if testWatchForFallback { + testLastWasFallback = false + } + return results +fallback: + results = results[:0] + for i := 0; i < len(paths); i++ { + results = append(results, Get(json, paths[i])) + } + if testWatchForFallback { + testLastWasFallback = true + } + return results +} + +// GetManyBytes searches json for the specified path. +// If working with bytes, this method preferred over +// GetMany(string(data), paths...) +func GetManyBytes(json []byte, paths ...string) []Result { + if json == nil { + return GetMany("", paths...) + } + results := GetMany(*(*string)(unsafe.Pointer(&json)), paths...) + for i := range results { + results[i] = fromBytesGet(results[i]) + } + return results +} + +// parseGetMany parses a json object for keys that match against the callers +// paths. It's a best-effort attempt and quickly locating and assigning the +// values to the []Result array. If there are failures such as bad json, or +// invalid input paths, or too much recursion, the function will exit with a +// return value of 'false'. +func parseGetMany( + json string, i int, + level uint, kplen int, + paths []string, completed []bool, matches []uint64, results []Result, +) (int, bool) { + if level > 62 { + // The recursion level is limited because the matches []uint64 + // array cannot handle more the 64-bits. + return i, false + } + // At this point the last character read was a '{'. + // Read all object keys and try to match against the paths. + var key string + var val string + var vesc, ok bool +next_key: + for ; i < len(json); i++ { + if json[i] == '"' { + // read the key + i, val, vesc, ok = parseString(json, i+1) + if !ok { + return i, false + } + if vesc { + // the value is escaped + key = unescape(val[1 : len(val)-1]) + } else { + // just a plain old ascii key + key = val[1 : len(val)-1] + } + var hasMatch bool + var parsedVal bool + var valOrgIndex int + var valPathIndex int + for j := 0; j < len(key); j++ { + if key[j] == '.' { + // we need to look for keys with dot and ignore them. + if i, _, ok = parseAny(json, i, false); !ok { + return i, false + } + continue next_key + } + } + var usedPaths int + // loop through paths and look for matches + for j := 0; j < len(paths); j++ { + if completed[j] { + usedPaths++ + // ignore completed paths + continue + } + if level > 0 && (matches[j]>>(level-1))&1 == 0 { + // ignore unmatched paths + usedPaths++ + continue + } + + // try to match the key to the path + // this is spaghetti code but the idea is to minimize + // calls and variable assignments when comparing the + // key to paths + if len(paths[j])-kplen >= len(key) { + i, k := kplen, 0 + for ; k < len(key); k, i = k+1, i+1 { + if key[k] != paths[j][i] { + // no match + goto nomatch + } + } + if i < len(paths[j]) { + if paths[j][i] == '.' { + // matched, but there still more keys in the path + goto match_not_atend + } + } + // matched and at the end of the path + goto match_atend + } + // no match, jump to the nomatch label + goto nomatch + match_atend: + // found a match + // at the end of the path. we must take the value. + usedPaths++ + if !parsedVal { + // the value has not been parsed yet. let's do so. + valOrgIndex = i // keep track of the current position. + i, results[j], ok = parseAny(json, i, true) + if !ok { + return i, false + } + parsedVal = true + valPathIndex = j + } else { + results[j] = results[valPathIndex] + } + // mark as complete + completed[j] = true + // jump over the match_not_atend label + goto nomatch + match_not_atend: + // found a match + // still in the middle of the path. + usedPaths++ + // mark the path as matched + matches[j] |= 1 << level + if !hasMatch { + hasMatch = true + } + nomatch: // noop label + } + + if !parsedVal { + if hasMatch { + // we found a match and the value has not been parsed yet. + // let's find out if the next value type is an object. + for ; i < len(json); i++ { + if json[i] <= ' ' || json[i] == ':' { + continue + } + break + } + if i < len(json) { + if json[i] == '{' { + // it's an object. let's go deeper + i, ok = parseGetMany(json, i+1, level+1, kplen+len(key)+1, paths, completed, matches, results) + if !ok { + return i, false + } + } else { + // not an object. just parse and ignore. + if i, _, ok = parseAny(json, i, false); !ok { + return i, false + } + } + } + } else { + // Since there was no matches we can just parse the value and + // ignore the result. + if i, _, ok = parseAny(json, i, false); !ok { + return i, false + } + } + } else if hasMatch && len(results[valPathIndex].Raw) > 0 && results[valPathIndex].Raw[0] == '{' { + // The value was already parsed and the value type is an object. + // Rewind the json index and let's parse deeper. + i = valOrgIndex + for ; i < len(json); i++ { + if json[i] == '{' { + break + } + } + i, ok = parseGetMany(json, i+1, level+1, kplen+len(key)+1, paths, completed, matches, results) + if !ok { + return i, false + } + } + if usedPaths == len(paths) { + // all paths have been used, either completed or matched. + // we should stop parsing this object to save CPU cycles. + if level > 0 && i < len(json) { + i, _ = parseSquash(json, i) + } + return i, true + } + } else if json[i] == '}' { + // reached the end of the object. end it here. + return i + 1, true + } + } + return i, true +} + +// Call table for GetMany. Using an isolated function allows for allocating +// arrays with know capacities on the stack, as opposed to dynamically +// allocating on the heap. This can provide a tremendous performance boost +// by avoiding the GC. +func getMany8(json string, i int, paths []string) ([]Result, bool) { + const max = 8 + var completed = make([]bool, 0, max) + var matches = make([]uint64, 0, max) + var results = make([]Result, 0, max) + completed = completed[0:len(paths):max] + matches = matches[0:len(paths):max] + results = results[0:len(paths):max] + _, ok := parseGetMany(json, i, 0, 0, paths, completed, matches, results) + return results, ok +} +func getMany16(json string, i int, paths []string) ([]Result, bool) { + const max = 16 + var completed = make([]bool, 0, max) + var matches = make([]uint64, 0, max) + var results = make([]Result, 0, max) + completed = completed[0:len(paths):max] + matches = matches[0:len(paths):max] + results = results[0:len(paths):max] + _, ok := parseGetMany(json, i, 0, 0, paths, completed, matches, results) + return results, ok +} +func getMany32(json string, i int, paths []string) ([]Result, bool) { + const max = 32 + var completed = make([]bool, 0, max) + var matches = make([]uint64, 0, max) + var results = make([]Result, 0, max) + completed = completed[0:len(paths):max] + matches = matches[0:len(paths):max] + results = results[0:len(paths):max] + _, ok := parseGetMany(json, i, 0, 0, paths, completed, matches, results) + return results, ok +} +func getMany64(json string, i int, paths []string) ([]Result, bool) { + const max = 64 + var completed = make([]bool, 0, max) + var matches = make([]uint64, 0, max) + var results = make([]Result, 0, max) + completed = completed[0:len(paths):max] + matches = matches[0:len(paths):max] + results = results[0:len(paths):max] + _, ok := parseGetMany(json, i, 0, 0, paths, completed, matches, results) + return results, ok +} +func getMany128(json string, i int, paths []string) ([]Result, bool) { + const max = 128 + var completed = make([]bool, 0, max) + var matches = make([]uint64, 0, max) + var results = make([]Result, 0, max) + completed = completed[0:len(paths):max] + matches = matches[0:len(paths):max] + results = results[0:len(paths):max] + _, ok := parseGetMany(json, i, 0, 0, paths, completed, matches, results) + return results, ok +} +func getMany256(json string, i int, paths []string) ([]Result, bool) { + const max = 256 + var completed = make([]bool, 0, max) + var matches = make([]uint64, 0, max) + var results = make([]Result, 0, max) + completed = completed[0:len(paths):max] + matches = matches[0:len(paths):max] + results = results[0:len(paths):max] + _, ok := parseGetMany(json, i, 0, 0, paths, completed, matches, results) + return results, ok +} +func getMany512(json string, i int, paths []string) ([]Result, bool) { + const max = 512 + var completed = make([]bool, 0, max) + var matches = make([]uint64, 0, max) + var results = make([]Result, 0, max) + completed = completed[0:len(paths):max] + matches = matches[0:len(paths):max] + results = results[0:len(paths):max] + _, ok := parseGetMany(json, i, 0, 0, paths, completed, matches, results) + return results, ok +} diff --git a/cmd/ponzu/vendor/github.com/tidwall/gjson/logo.png b/cmd/ponzu/vendor/github.com/tidwall/gjson/logo.png Binary files differnew file mode 100644 index 0000000..17a8bbe --- /dev/null +++ b/cmd/ponzu/vendor/github.com/tidwall/gjson/logo.png diff --git a/management/editor/dom.go b/management/editor/dom.go index 41aafa7..5888db5 100644 --- a/management/editor/dom.go +++ b/management/editor/dom.go @@ -7,288 +7,291 @@ import ( "strings" ) -type element struct { - tagName string - attrs map[string]string - name string - label string - data string - viewBuf *bytes.Buffer +// Element is a basic struct for representing DOM elements +type Element struct { + TagName string + Attrs map[string]string + Name string + Label string + Data string + ViewBuf *bytes.Buffer } -func newElement(tagName, label, fieldName string, p interface{}, attrs map[string]string) *element { - return &element{ - tagName: tagName, - attrs: attrs, - name: tagNameFromStructField(fieldName, p), - label: label, - data: valueFromStructField(fieldName, p), - viewBuf: &bytes.Buffer{}, +// NewElement returns an Element with Name and Data already processed from the +// fieldName and content interface provided +func NewElement(tagName, label, fieldName string, p interface{}, attrs map[string]string) *Element { + return &Element{ + TagName: tagName, + Attrs: attrs, + Name: TagNameFromStructField(fieldName, p), + Label: label, + Data: ValueFromStructField(fieldName, p), + ViewBuf: &bytes.Buffer{}, } } -// domElementSelfClose is a special DOM element which is parsed as a +// DOMElementSelfClose is a special DOM element which is parsed as a // self-closing tag and thus needs to be created differently -func domElementSelfClose(e *element) []byte { - _, err := e.viewBuf.WriteString(`<div class="input-field col s12">`) +func DOMElementSelfClose(e *Element) []byte { + _, err := e.ViewBuf.WriteString(`<div class="input-field col s12">`) if err != nil { - log.Println("Error writing HTML string to buffer: domElementSelfClose") + log.Println("Error writing HTML string to buffer: DOMElementSelfClose") return nil } - if e.label != "" { - _, err = e.viewBuf.WriteString( + if e.Label != "" { + _, err = e.ViewBuf.WriteString( `<label class="active" for="` + - strings.Join(strings.Split(e.label, " "), "-") + `">` + e.label + + strings.Join(strings.Split(e.Label, " "), "-") + `">` + e.Label + `</label>`) if err != nil { - log.Println("Error writing HTML string to buffer: domElementSelfClose") + log.Println("Error writing HTML string to buffer: DOMElementSelfClose") return nil } } - _, err = e.viewBuf.WriteString(`<` + e.tagName + ` value="`) + _, err = e.ViewBuf.WriteString(`<` + e.TagName + ` value="`) if err != nil { - log.Println("Error writing HTML string to buffer: domElementSelfClose") + log.Println("Error writing HTML string to buffer: DOMElementSelfClose") return nil } - _, err = e.viewBuf.WriteString(html.EscapeString(e.data) + `" `) + _, err = e.ViewBuf.WriteString(html.EscapeString(e.Data) + `" `) if err != nil { - log.Println("Error writing HTML string to buffer: domElementSelfClose") + log.Println("Error writing HTML string to buffer: DOMElementSelfClose") return nil } - for attr, value := range e.attrs { - _, err := e.viewBuf.WriteString(attr + `="` + value + `" `) + for attr, value := range e.Attrs { + _, err := e.ViewBuf.WriteString(attr + `="` + value + `" `) if err != nil { - log.Println("Error writing HTML string to buffer: domElementSelfClose") + log.Println("Error writing HTML string to buffer: DOMElementSelfClose") return nil } } - _, err = e.viewBuf.WriteString(` name="` + e.name + `" />`) + _, err = e.ViewBuf.WriteString(` name="` + e.Name + `" />`) if err != nil { - log.Println("Error writing HTML string to buffer: domElementSelfClose") + log.Println("Error writing HTML string to buffer: DOMElementSelfClose") return nil } - _, err = e.viewBuf.WriteString(`</div>`) + _, err = e.ViewBuf.WriteString(`</div>`) if err != nil { - log.Println("Error writing HTML string to buffer: domElementSelfClose") + log.Println("Error writing HTML string to buffer: DOMElementSelfClose") return nil } - return e.viewBuf.Bytes() + return e.ViewBuf.Bytes() } -// domElementCheckbox is a special DOM element which is parsed as a +// DOMElementCheckbox is a special DOM element which is parsed as a // checkbox input tag and thus needs to be created differently -func domElementCheckbox(e *element) []byte { - _, err := e.viewBuf.WriteString(`<p class="col s6">`) +func DOMElementCheckbox(e *Element) []byte { + _, err := e.ViewBuf.WriteString(`<p class="col s6">`) if err != nil { - log.Println("Error writing HTML string to buffer: domElementCheckbox") + log.Println("Error writing HTML string to buffer: DOMElementCheckbox") return nil } - _, err = e.viewBuf.WriteString(`<` + e.tagName + ` `) + _, err = e.ViewBuf.WriteString(`<` + e.TagName + ` `) if err != nil { - log.Println("Error writing HTML string to buffer: domElementCheckbox") + log.Println("Error writing HTML string to buffer: DOMElementCheckbox") return nil } - for attr, value := range e.attrs { - _, err := e.viewBuf.WriteString(attr + `="` + value + `" `) + for attr, value := range e.Attrs { + _, err := e.ViewBuf.WriteString(attr + `="` + value + `" `) if err != nil { - log.Println("Error writing HTML string to buffer: domElementCheckbox") + log.Println("Error writing HTML string to buffer: DOMElementCheckbox") return nil } } - _, err = e.viewBuf.WriteString(` name="` + e.name + `" />`) + _, err = e.ViewBuf.WriteString(` name="` + e.Name + `" />`) if err != nil { - log.Println("Error writing HTML string to buffer: domElementCheckbox") + log.Println("Error writing HTML string to buffer: DOMElementCheckbox") return nil } - if e.label != "" { - _, err = e.viewBuf.WriteString( + if e.Label != "" { + _, err = e.ViewBuf.WriteString( `<label for="` + - strings.Join(strings.Split(e.label, " "), "-") + `">` + - e.label + `</label>`) + strings.Join(strings.Split(e.Label, " "), "-") + `">` + + e.Label + `</label>`) if err != nil { - log.Println("Error writing HTML string to buffer: domElementCheckbox") + log.Println("Error writing HTML string to buffer: DOMElementCheckbox") return nil } } - _, err = e.viewBuf.WriteString(`</p>`) + _, err = e.ViewBuf.WriteString(`</p>`) if err != nil { - log.Println("Error writing HTML string to buffer: domElementCheckbox") + log.Println("Error writing HTML string to buffer: DOMElementCheckbox") return nil } - return e.viewBuf.Bytes() + return e.ViewBuf.Bytes() } -// domElement creates a DOM element -func domElement(e *element) []byte { - _, err := e.viewBuf.WriteString(`<div class="input-field col s12">`) +// DOMElement creates a DOM element +func DOMElement(e *Element) []byte { + _, err := e.ViewBuf.WriteString(`<div class="input-field col s12">`) if err != nil { - log.Println("Error writing HTML string to buffer: domElement") + log.Println("Error writing HTML string to buffer: DOMElement") return nil } - if e.label != "" { - _, err = e.viewBuf.WriteString( + if e.Label != "" { + _, err = e.ViewBuf.WriteString( `<label class="active" for="` + - strings.Join(strings.Split(e.label, " "), "-") + `">` + e.label + + strings.Join(strings.Split(e.Label, " "), "-") + `">` + e.Label + `</label>`) if err != nil { - log.Println("Error writing HTML string to buffer: domElement") + log.Println("Error writing HTML string to buffer: DOMElement") return nil } } - _, err = e.viewBuf.WriteString(`<` + e.tagName + ` `) + _, err = e.ViewBuf.WriteString(`<` + e.TagName + ` `) if err != nil { - log.Println("Error writing HTML string to buffer: domElement") + log.Println("Error writing HTML string to buffer: DOMElement") return nil } - for attr, value := range e.attrs { - _, err = e.viewBuf.WriteString(attr + `="` + string(value) + `" `) + for attr, value := range e.Attrs { + _, err = e.ViewBuf.WriteString(attr + `="` + string(value) + `" `) if err != nil { - log.Println("Error writing HTML string to buffer: domElement") + log.Println("Error writing HTML string to buffer: DOMElement") return nil } } - _, err = e.viewBuf.WriteString(` name="` + e.name + `" > `) + _, err = e.ViewBuf.WriteString(` name="` + e.Name + `" >`) if err != nil { - log.Println("Error writing HTML string to buffer: domElement") + log.Println("Error writing HTML string to buffer: DOMElement") return nil } - _, err = e.viewBuf.WriteString(html.EscapeString(e.data)) + _, err = e.ViewBuf.WriteString(html.EscapeString(e.Data)) if err != nil { - log.Println("Error writing HTML string to buffer: domElement") + log.Println("Error writing HTML string to buffer: DOMElement") return nil } - _, err = e.viewBuf.WriteString(`</` + e.tagName + `>`) + _, err = e.ViewBuf.WriteString(`</` + e.TagName + `>`) if err != nil { - log.Println("Error writing HTML string to buffer: domElement") + log.Println("Error writing HTML string to buffer: DOMElement") return nil } - _, err = e.viewBuf.WriteString(`</div>`) + _, err = e.ViewBuf.WriteString(`</div>`) if err != nil { - log.Println("Error writing HTML string to buffer: domElement") + log.Println("Error writing HTML string to buffer: DOMElement") return nil } - return e.viewBuf.Bytes() + return e.ViewBuf.Bytes() } -func domElementWithChildrenSelect(e *element, children []*element) []byte { - _, err := e.viewBuf.WriteString(`<div class="input-field col s6">`) +func DOMElementWithChildrenSelect(e *Element, children []*Element) []byte { + _, err := e.ViewBuf.WriteString(`<div class="input-field col s6">`) if err != nil { - log.Println("Error writing HTML string to buffer: domElementWithChildrenSelect") + log.Println("Error writing HTML string to buffer: DOMElementWithChildrenSelect") return nil } - _, err = e.viewBuf.WriteString(`<` + e.tagName + ` `) + _, err = e.ViewBuf.WriteString(`<` + e.TagName + ` `) if err != nil { - log.Println("Error writing HTML string to buffer: domElementWithChildrenSelect") + log.Println("Error writing HTML string to buffer: DOMElementWithChildrenSelect") return nil } - for attr, value := range e.attrs { - _, err = e.viewBuf.WriteString(attr + `="` + value + `" `) + for attr, value := range e.Attrs { + _, err = e.ViewBuf.WriteString(attr + `="` + value + `" `) if err != nil { - log.Println("Error writing HTML string to buffer: domElementWithChildrenSelect") + log.Println("Error writing HTML string to buffer: DOMElementWithChildrenSelect") return nil } } - _, err = e.viewBuf.WriteString(` name="` + e.name + `" >`) + _, err = e.ViewBuf.WriteString(` name="` + e.Name + `" >`) if err != nil { - log.Println("Error writing HTML string to buffer: domElementWithChildrenSelect") + log.Println("Error writing HTML string to buffer: DOMElementWithChildrenSelect") return nil } - // loop over children and create domElement for each child + // loop over children and create DOMElement for each child for _, child := range children { - _, err = e.viewBuf.Write(domElement(child)) + _, err = e.ViewBuf.Write(DOMElement(child)) if err != nil { - log.Println("Error writing HTML domElement to buffer: domElementWithChildrenSelect") + log.Println("Error writing HTML DOMElement to buffer: DOMElementWithChildrenSelect") return nil } } - _, err = e.viewBuf.WriteString(`</` + e.tagName + `>`) + _, err = e.ViewBuf.WriteString(`</` + e.TagName + `>`) if err != nil { - log.Println("Error writing HTML string to buffer: domElementWithChildrenSelect") + log.Println("Error writing HTML string to buffer: DOMElementWithChildrenSelect") return nil } - if e.label != "" { - _, err = e.viewBuf.WriteString(`<label class="active">` + e.label + `</label>`) + if e.Label != "" { + _, err = e.ViewBuf.WriteString(`<label class="active">` + e.Label + `</label>`) if err != nil { - log.Println("Error writing HTML string to buffer: domElementWithChildrenSelect") + log.Println("Error writing HTML string to buffer: DOMElementWithChildrenSelect") return nil } } - _, err = e.viewBuf.WriteString(`</div>`) + _, err = e.ViewBuf.WriteString(`</div>`) if err != nil { - log.Println("Error writing HTML string to buffer: domElementWithChildrenSelect") + log.Println("Error writing HTML string to buffer: DOMElementWithChildrenSelect") return nil } - return e.viewBuf.Bytes() + return e.ViewBuf.Bytes() } -func domElementWithChildrenCheckbox(e *element, children []*element) []byte { - _, err := e.viewBuf.WriteString(`<` + e.tagName + ` `) +func DOMElementWithChildrenCheckbox(e *Element, children []*Element) []byte { + _, err := e.ViewBuf.WriteString(`<` + e.TagName + ` `) if err != nil { - log.Println("Error writing HTML string to buffer: domElementWithChildrenCheckbox") + log.Println("Error writing HTML string to buffer: DOMElementWithChildrenCheckbox") return nil } - for attr, value := range e.attrs { - _, err = e.viewBuf.WriteString(attr + `="` + value + `" `) + for attr, value := range e.Attrs { + _, err = e.ViewBuf.WriteString(attr + `="` + value + `" `) if err != nil { - log.Println("Error writing HTML string to buffer: domElementWithChildrenCheckbox") + log.Println("Error writing HTML string to buffer: DOMElementWithChildrenCheckbox") return nil } } - _, err = e.viewBuf.WriteString(` >`) + _, err = e.ViewBuf.WriteString(` >`) if err != nil { - log.Println("Error writing HTML string to buffer: domElementWithChildrenCheckbox") + log.Println("Error writing HTML string to buffer: DOMElementWithChildrenCheckbox") return nil } - if e.label != "" { - _, err = e.viewBuf.WriteString(`<label class="active">` + e.label + `</label>`) + if e.Label != "" { + _, err = e.ViewBuf.WriteString(`<label class="active">` + e.Label + `</label>`) if err != nil { - log.Println("Error writing HTML string to buffer: domElementWithChildrenCheckbox") + log.Println("Error writing HTML string to buffer: DOMElementWithChildrenCheckbox") return nil } } - // loop over children and create domElement for each child + // loop over children and create DOMElement for each child for _, child := range children { - _, err = e.viewBuf.Write(domElementCheckbox(child)) + _, err = e.ViewBuf.Write(DOMElementCheckbox(child)) if err != nil { - log.Println("Error writing HTML domElementCheckbox to buffer: domElementWithChildrenCheckbox") + log.Println("Error writing HTML DOMElementCheckbox to buffer: DOMElementWithChildrenCheckbox") return nil } } - _, err = e.viewBuf.WriteString(`</` + e.tagName + `><div class="clear padding"> </div>`) + _, err = e.ViewBuf.WriteString(`</` + e.TagName + `><div class="clear padding"> </div>`) if err != nil { - log.Println("Error writing HTML string to buffer: domElementWithChildrenCheckbox") + log.Println("Error writing HTML string to buffer: DOMElementWithChildrenCheckbox") return nil } - return e.viewBuf.Bytes() + return e.ViewBuf.Bytes() } diff --git a/management/editor/elements.go b/management/editor/elements.go index b82b220..4e0ff17 100644 --- a/management/editor/elements.go +++ b/management/editor/elements.go @@ -30,9 +30,9 @@ import ( // ) // } func Input(fieldName string, p interface{}, attrs map[string]string) []byte { - e := newElement("input", attrs["label"], fieldName, p, attrs) + e := NewElement("input", attrs["label"], fieldName, p, attrs) - return domElementSelfClose(e) + return DOMElementSelfClose(e) } // Textarea returns the []byte of a <textarea> HTML element with a label. @@ -49,9 +49,9 @@ func Textarea(fieldName string, p interface{}, attrs map[string]string) []byte { attrs["class"] = className } - e := newElement("textarea", attrs["label"], fieldName, p, attrs) + e := NewElement("textarea", attrs["label"], fieldName, p, attrs) - return domElement(e) + return DOMElement(e) } // Timestamp returns the []byte of an <input> HTML element with a label. @@ -60,23 +60,23 @@ func Textarea(fieldName string, p interface{}, attrs map[string]string) []byte { // form of the struct field that this editor input is representing func Timestamp(fieldName string, p interface{}, attrs map[string]string) []byte { var data string - val := valueFromStructField(fieldName, p) + val := ValueFromStructField(fieldName, p) if val == "0" { data = "" } else { data = val } - e := &element{ - tagName: "input", - attrs: attrs, - name: tagNameFromStructField(fieldName, p), - label: attrs["label"], - data: data, - viewBuf: &bytes.Buffer{}, + e := &Element{ + TagName: "input", + Attrs: attrs, + Name: TagNameFromStructField(fieldName, p), + Label: attrs["label"], + Data: data, + ViewBuf: &bytes.Buffer{}, } - return domElementSelfClose(e) + return DOMElementSelfClose(e) } // File returns the []byte of a <input type="file"> HTML element with a label. @@ -84,7 +84,7 @@ func Timestamp(fieldName string, p interface{}, attrs map[string]string) []byte // The `fieldName` argument will cause a panic if it is not exactly the string // form of the struct field that this editor input is representing func File(fieldName string, p interface{}, attrs map[string]string) []byte { - name := tagNameFromStructField(fieldName, p) + name := TagNameFromStructField(fieldName, p) tmpl := `<div class="file-input ` + name + ` input-field col s12"> <label class="active">` + attrs["label"] + `</label> @@ -98,7 +98,7 @@ func File(fieldName string, p interface{}, attrs map[string]string) []byte { </div> </div> <div class="preview"><div class="img-clip"></div></div> - <input class="store ` + name + `" type="hidden" name="` + name + `" value="` + valueFromStructField(fieldName, p) + `" /> + <input class="store ` + name + `" type="hidden" name="` + name + `" value="` + ValueFromStructField(fieldName, p) + `" /> </div>` script := @@ -161,25 +161,35 @@ func Richtext(fieldName string, p interface{}, attrs map[string]string) []byte { iso := []byte(`<div class="iso-texteditor input-field col s12"><label>` + attrs["label"] + `</label>`) isoClose := []byte(`</div>`) + if _, ok := attrs["class"]; ok { + attrs["class"] += "richtext " + fieldName + } else { + attrs["class"] = "richtext " + fieldName + } + + if _, ok := attrs["id"]; ok { + attrs["id"] += "richtext-" + fieldName + } else { + attrs["id"] = "richtext-" + fieldName + } + // create the target element for the editor to attach itself - attrs["class"] = "richtext " + fieldName - attrs["id"] = "richtext-" + fieldName - div := &element{ - tagName: "div", - attrs: attrs, - name: "", - label: "", - data: "", - viewBuf: &bytes.Buffer{}, + div := &Element{ + TagName: "div", + Attrs: attrs, + Name: "", + Label: "", + Data: "", + ViewBuf: &bytes.Buffer{}, } // create a hidden input to store the value from the struct - val := valueFromStructField(fieldName, p) - name := tagNameFromStructField(fieldName, p) + val := ValueFromStructField(fieldName, p) + name := TagNameFromStructField(fieldName, p) input := `<input type="hidden" name="` + name + `" class="richtext-value ` + fieldName + `" value="` + html.EscapeString(val) + `"/>` // build the dom tree for the entire richtext component - iso = append(iso, domElement(div)...) + iso = append(iso, DOMElement(div)...) iso = append(iso, []byte(input)...) iso = append(iso, isoClose...) @@ -257,26 +267,31 @@ func Select(fieldName string, p interface{}, attrs, options map[string]string) [ // <option value="{map key}">{map value}</option> // find the field value in p to determine if an option is pre-selected - fieldVal := valueFromStructField(fieldName, p) + fieldVal := ValueFromStructField(fieldName, p) - attrs["class"] = "browser-default" - sel := newElement("select", attrs["label"], fieldName, p, attrs) - var opts []*element + if _, ok := attrs["class"]; ok { + attrs["class"] += " browser-default" + } else { + attrs["class"] = "browser-default" + } + + sel := NewElement("select", attrs["label"], fieldName, p, attrs) + var opts []*Element // provide a call to action for the select element - cta := &element{ - tagName: "option", - attrs: map[string]string{"disabled": "true", "selected": "true"}, - data: "Select an option...", - viewBuf: &bytes.Buffer{}, + cta := &Element{ + TagName: "option", + Attrs: map[string]string{"disabled": "true", "selected": "true"}, + Data: "Select an option...", + ViewBuf: &bytes.Buffer{}, } // provide a selection reset (will store empty string in db) - reset := &element{ - tagName: "option", - attrs: map[string]string{"value": ""}, - data: "None", - viewBuf: &bytes.Buffer{}, + reset := &Element{ + TagName: "option", + Attrs: map[string]string{"value": ""}, + Data: "None", + ViewBuf: &bytes.Buffer{}, } opts = append(opts, cta, reset) @@ -286,17 +301,17 @@ func Select(fieldName string, p interface{}, attrs, options map[string]string) [ if k == fieldVal { optAttrs["selected"] = "true" } - opt := &element{ - tagName: "option", - attrs: optAttrs, - data: v, - viewBuf: &bytes.Buffer{}, + opt := &Element{ + TagName: "option", + Attrs: optAttrs, + Data: v, + ViewBuf: &bytes.Buffer{}, } opts = append(opts, opt) } - return domElementWithChildrenSelect(sel, opts) + return DOMElementWithChildrenSelect(sel, opts) } // Checkbox returns the []byte of a set of <input type="checkbox"> HTML elements @@ -305,13 +320,18 @@ func Select(fieldName string, p interface{}, attrs, options map[string]string) [ // The `fieldName` argument will cause a panic if it is not exactly the string // form of the struct field that this editor input is representing func Checkbox(fieldName string, p interface{}, attrs, options map[string]string) []byte { - attrs["class"] = "input-field col s12" - div := newElement("div", attrs["label"], fieldName, p, attrs) + if _, ok := attrs["class"]; ok { + attrs["class"] += "input-field col s12" + } else { + attrs["class"] = "input-field col s12" + } + + div := NewElement("div", attrs["label"], fieldName, p, attrs) - var opts []*element + var opts []*Element // get the pre-checked options if this is already an existing post - checkedVals := valueFromStructField(fieldName, p) + checkedVals := ValueFromStructField(fieldName, p) checked := strings.Split(checkedVals, "__ponzu") i := 0 @@ -329,22 +349,22 @@ func Checkbox(fieldName string, p interface{}, attrs, options map[string]string) } } - // create a *element manually using the modified tagNameFromStructFieldMulti + // create a *element manually using the modified TagNameFromStructFieldMulti // func since this is for a multi-value name - input := &element{ - tagName: "input", - attrs: inputAttrs, - name: tagNameFromStructFieldMulti(fieldName, i, p), - label: v, - data: "", - viewBuf: &bytes.Buffer{}, + input := &Element{ + TagName: "input", + Attrs: inputAttrs, + Name: TagNameFromStructFieldMulti(fieldName, i, p), + Label: v, + Data: "", + ViewBuf: &bytes.Buffer{}, } opts = append(opts, input) i++ } - return domElementWithChildrenCheckbox(div, opts) + return DOMElementWithChildrenCheckbox(div, opts) } // Tags returns the []byte of a tag input (in the style of Materialze 'Chips') with a label. @@ -352,10 +372,10 @@ func Checkbox(fieldName string, p interface{}, attrs, options map[string]string) // The `fieldName` argument will cause a panic if it is not exactly the string // form of the struct field that this editor input is representing func Tags(fieldName string, p interface{}, attrs map[string]string) []byte { - name := tagNameFromStructField(fieldName, p) + name := TagNameFromStructField(fieldName, p) // get the saved tags if this is already an existing post - values := valueFromStructField(fieldName, p) + values := ValueFromStructField(fieldName, p) var tags []string if strings.Contains(values, "__ponzu") { tags = strings.Split(values, "__ponzu") @@ -375,7 +395,7 @@ func Tags(fieldName string, p interface{}, attrs map[string]string) []byte { var initial []string i := 0 for _, tag := range tags { - tagName := tagNameFromStructFieldMulti(fieldName, i, p) + tagName := TagNameFromStructFieldMulti(fieldName, i, p) html += `<input type="hidden" class="__ponzu-tag ` + tag + `" name=` + tagName + ` value="` + tag + `"/>` initial = append(initial, `{tag: '`+tag+`'}`) i++ diff --git a/management/editor/repeaters.go b/management/editor/repeaters.go index 617caee..250f2d2 100644 --- a/management/editor/repeaters.go +++ b/management/editor/repeaters.go @@ -34,10 +34,10 @@ import ( // } func InputRepeater(fieldName string, p interface{}, attrs map[string]string) []byte { // find the field values in p to determine pre-filled inputs - fieldVals := valueFromStructField(fieldName, p) + fieldVals := ValueFromStructField(fieldName, p) vals := strings.Split(fieldVals, "__ponzu") - scope := tagNameFromStructField(fieldName, p) + scope := TagNameFromStructField(fieldName, p) html := bytes.Buffer{} _, err := html.WriteString(`<span class="__ponzu-repeat ` + scope + `">`) @@ -47,22 +47,22 @@ func InputRepeater(fieldName string, p interface{}, attrs map[string]string) []b } for i, val := range vals { - el := &element{ - tagName: "input", - attrs: attrs, - name: tagNameFromStructFieldMulti(fieldName, i, p), - data: val, - viewBuf: &bytes.Buffer{}, + el := &Element{ + TagName: "input", + Attrs: attrs, + Name: TagNameFromStructFieldMulti(fieldName, i, p), + Data: val, + ViewBuf: &bytes.Buffer{}, } // only add the label to the first input in repeated list if i == 0 { - el.label = attrs["label"] + el.Label = attrs["label"] } - _, err := html.Write(domElementSelfClose(el)) + _, err := html.Write(DOMElementSelfClose(el)) if err != nil { - log.Println("Error writing domElementSelfClose to InputRepeater buffer") + log.Println("Error writing DOMElementSelfClose to InputRepeater buffer") return nil } } @@ -84,7 +84,7 @@ func InputRepeater(fieldName string, p interface{}, attrs map[string]string) []b func SelectRepeater(fieldName string, p interface{}, attrs, options map[string]string) []byte { // options are the value attr and the display value, i.e. // <option value="{map key}">{map value}</option> - scope := tagNameFromStructField(fieldName, p) + scope := TagNameFromStructField(fieldName, p) html := bytes.Buffer{} _, err := html.WriteString(`<span class="__ponzu-repeat ` + scope + `">`) if err != nil { @@ -93,43 +93,47 @@ func SelectRepeater(fieldName string, p interface{}, attrs, options map[string]s } // find the field values in p to determine if an option is pre-selected - fieldVals := valueFromStructField(fieldName, p) + fieldVals := ValueFromStructField(fieldName, p) vals := strings.Split(fieldVals, "__ponzu") - attrs["class"] = "browser-default" + if _, ok := attrs["class"]; ok { + attrs["class"] += " browser-default" + } else { + attrs["class"] = "browser-default" + } // loop through vals and create selects and options for each, adding to html if len(vals) > 0 { for i, val := range vals { - sel := &element{ - tagName: "select", - attrs: attrs, - name: tagNameFromStructFieldMulti(fieldName, i, p), - viewBuf: &bytes.Buffer{}, + sel := &Element{ + TagName: "select", + Attrs: attrs, + Name: TagNameFromStructFieldMulti(fieldName, i, p), + ViewBuf: &bytes.Buffer{}, } // only add the label to the first select in repeated list if i == 0 { - sel.label = attrs["label"] + sel.Label = attrs["label"] } // create options for select element - var opts []*element + var opts []*Element // provide a call to action for the select element - cta := &element{ - tagName: "option", - attrs: map[string]string{"disabled": "true", "selected": "true"}, - data: "Select an option...", - viewBuf: &bytes.Buffer{}, + cta := &Element{ + TagName: "option", + Attrs: map[string]string{"disabled": "true", "selected": "true"}, + Data: "Select an option...", + ViewBuf: &bytes.Buffer{}, } // provide a selection reset (will store empty string in db) - reset := &element{ - tagName: "option", - attrs: map[string]string{"value": ""}, - data: "None", - viewBuf: &bytes.Buffer{}, + reset := &Element{ + TagName: "option", + Attrs: map[string]string{"value": ""}, + Data: "None", + ViewBuf: &bytes.Buffer{}, } opts = append(opts, cta, reset) @@ -139,19 +143,19 @@ func SelectRepeater(fieldName string, p interface{}, attrs, options map[string]s if k == val { optAttrs["selected"] = "true" } - opt := &element{ - tagName: "option", - attrs: optAttrs, - data: v, - viewBuf: &bytes.Buffer{}, + opt := &Element{ + TagName: "option", + Attrs: optAttrs, + Data: v, + ViewBuf: &bytes.Buffer{}, } opts = append(opts, opt) } - _, err := html.Write(domElementWithChildrenSelect(sel, opts)) + _, err := html.Write(DOMElementWithChildrenSelect(sel, opts)) if err != nil { - log.Println("Error writing domElementWithChildrenSelect to SelectRepeater buffer") + log.Println("Error writing DOMElementWithChildrenSelect to SelectRepeater buffer") return nil } } @@ -174,7 +178,7 @@ func SelectRepeater(fieldName string, p interface{}, attrs, options map[string]s // form of the struct field that this editor input is representing func FileRepeater(fieldName string, p interface{}, attrs map[string]string) []byte { // find the field values in p to determine if an option is pre-selected - fieldVals := valueFromStructField(fieldName, p) + fieldVals := ValueFromStructField(fieldName, p) vals := strings.Split(fieldVals, "__ponzu") addLabelFirst := func(i int, label string) string { @@ -251,7 +255,7 @@ func FileRepeater(fieldName string, p interface{}, attrs map[string]string) []by </script>` // 1=nameidx, 2=className - name := tagNameFromStructField(fieldName, p) + name := TagNameFromStructField(fieldName, p) html := bytes.Buffer{} _, err := html.WriteString(`<span class="__ponzu-repeat ` + name + `">`) @@ -262,7 +266,7 @@ func FileRepeater(fieldName string, p interface{}, attrs map[string]string) []by for i, val := range vals { className := fmt.Sprintf("%s-%d", name, i) - nameidx := tagNameFromStructFieldMulti(fieldName, i, p) + nameidx := TagNameFromStructFieldMulti(fieldName, i, p) _, err := html.WriteString(fmt.Sprintf(tmpl, nameidx, addLabelFirst(i, attrs["label"]), val, className, fieldName)) if err != nil { @@ -288,7 +292,7 @@ func FileRepeater(fieldName string, p interface{}, attrs map[string]string) []by // RepeatController generates the javascript to control any repeatable form // element in an editor based on its type, field name and HTML tag name func RepeatController(fieldName string, p interface{}, inputSelector, cloneSelector string) []byte { - scope := tagNameFromStructField(fieldName, p) + scope := TagNameFromStructField(fieldName, p) script := ` <script> $(function() { diff --git a/management/editor/values.go b/management/editor/values.go index 7d71d3f..bc97f99 100644 --- a/management/editor/values.go +++ b/management/editor/values.go @@ -6,7 +6,9 @@ import ( "strings" ) -func tagNameFromStructField(name string, post interface{}) string { +// TagNameFromStructField does a lookup on the `json` struct tag for a given +// field of a struct +func TagNameFromStructField(name string, post interface{}) string { // sometimes elements in these environments will not have a name, // and thus no tag name in the struct which correlates to it. if name == "" { @@ -26,16 +28,19 @@ func tagNameFromStructField(name string, post interface{}) string { return tag } +// TagNameFromStructFieldMulti calls TagNameFromStructField and formats is for +// use with gorilla/schema // due to the format in which gorilla/schema expects form names to be when // one is associated with multiple values, we need to output the name as such. // Ex. 'category.0', 'category.1', 'category.2' and so on. -func tagNameFromStructFieldMulti(name string, i int, post interface{}) string { - tag := tagNameFromStructField(name, post) +func TagNameFromStructFieldMulti(name string, i int, post interface{}) string { + tag := TagNameFromStructField(name, post) return fmt.Sprintf("%s.%d", tag, i) } -func valueFromStructField(name string, post interface{}) string { +// ValueFromStructField returns the string value of a field in a struct +func ValueFromStructField(name string, post interface{}) string { field := reflect.Indirect(reflect.ValueOf(post)).FieldByName(name) switch field.Kind() { diff --git a/system/addon/api.go b/system/addon/api.go index 39f1b4d..7167202 100644 --- a/system/addon/api.go +++ b/system/addon/api.go @@ -25,7 +25,7 @@ func ContentAll(namespace string) []byte { j, err := Get(URL) if err != nil { - log.Println("Error in ContentAll for reference HTTP request:", endpoint) + log.Println("Error in ContentAll for reference HTTP request:", URL) return nil } @@ -42,7 +42,7 @@ func Query(namespace string, opts QueryOptions) []byte { j, err := Get(URL) if err != nil { - log.Println("Error in Query for reference HTTP request:", endpoint) + log.Println("Error in Query for reference HTTP request:", URL) return nil } diff --git a/system/admin/admin.go b/system/admin/admin.go index 76f36f6..e3ae2d6 100644 --- a/system/admin/admin.go +++ b/system/admin/admin.go @@ -487,7 +487,7 @@ var analyticsHTML = ` <div class="analytics"> <div class="card"> <div class="card-content"> - <p class="right">Data range: {{ .from }} - {{ .to }} (GMT)</p> + <p class="right">Data range: {{ .from }} - {{ .to }} (UTC)</p> <div class="card-title">API Requests</div> <canvas id="analytics-chart"></canvas> <script> diff --git a/system/admin/handlers.go b/system/admin/handlers.go index ff30040..1ff7156 100644 --- a/system/admin/handlers.go +++ b/system/admin/handlers.go @@ -909,7 +909,7 @@ func contentsHandler(res http.ResponseWriter, req *http.Request) { for i := range posts { err := json.Unmarshal(posts[i], &p) if err != nil { - log.Println("Error unmarshal json into", t, err, posts[i]) + log.Println("Error unmarshal json into", t, err, string(posts[i])) post := `<li class="col s12">Error decoding data. Possible file corruption.</li>` b.Write([]byte(post)) @@ -934,7 +934,7 @@ func contentsHandler(res http.ResponseWriter, req *http.Request) { for i := len(posts) - 1; i >= 0; i-- { err := json.Unmarshal(posts[i], &p) if err != nil { - log.Println("Error unmarshal json into", t, err, posts[i]) + log.Println("Error unmarshal json into", t, err, string(posts[i])) post := `<li class="col s12">Error decoding data. Possible file corruption.</li>` b.Write([]byte(post)) @@ -950,7 +950,7 @@ func contentsHandler(res http.ResponseWriter, req *http.Request) { for i := range posts { err := json.Unmarshal(posts[i], &p) if err != nil { - log.Println("Error unmarshal json into", t, err, posts[i]) + log.Println("Error unmarshal json into", t, err, string(posts[i])) post := `<li class="col s12">Error decoding data. Possible file corruption.</li>` b.Write([]byte(post)) diff --git a/system/api/handlers.go b/system/api/handlers.go index 7b59dbd..8b4a387 100644 --- a/system/api/handlers.go +++ b/system/api/handlers.go @@ -15,8 +15,10 @@ import ( func typesHandler(res http.ResponseWriter, req *http.Request) { var types = []string{} - for t := range item.Types { - types = append(types, string(t)) + for t, fn := range item.Types { + if !hide(fn(), res, req) { + types = append(types, t) + } } j, err := toJSON(types) @@ -36,11 +38,16 @@ func contentsHandler(res http.ResponseWriter, req *http.Request) { return } - if _, ok := item.Types[t]; !ok { + it, ok := item.Types[t] + if !ok { res.WriteHeader(http.StatusNotFound) return } + if hide(it(), res, req) { + return + } + count, err := strconv.Atoi(q.Get("count")) // int: determines number of posts to return (10 default, -1 is all) if err != nil { if q.Get("count") == "" { @@ -98,13 +105,18 @@ func contentHandler(res http.ResponseWriter, req *http.Request) { return } - if _, ok := item.Types[t]; !ok { + if t == "" || id == "" { + res.WriteHeader(http.StatusBadRequest) + return + } + + pt, ok := item.Types[t] + if !ok { res.WriteHeader(http.StatusNotFound) return } - if t == "" || id == "" { - res.WriteHeader(http.StatusBadRequest) + if hide(pt(), res, req) { return } @@ -114,6 +126,8 @@ func contentHandler(res http.ResponseWriter, req *http.Request) { return } + defer push(res, req, pt, post) + j, err := fmtJSON(json.RawMessage(post)) if err != nil { res.WriteHeader(http.StatusInternalServerError) @@ -126,14 +140,31 @@ func contentHandler(res http.ResponseWriter, req *http.Request) { func contentHandlerBySlug(res http.ResponseWriter, req *http.Request) { slug := req.URL.Query().Get("slug") + if slug == "" { + res.WriteHeader(http.StatusBadRequest) + return + } + // lookup type:id by slug key in __contentIndex - post, err := db.ContentBySlug(slug) + t, post, err := db.ContentBySlug(slug) if err != nil { log.Println("Error finding content by slug:", slug, err) res.WriteHeader(http.StatusInternalServerError) return } + it, ok := item.Types[t] + if !ok { + res.WriteHeader(http.StatusBadRequest) + return + } + + if hide(it(), res, req) { + return + } + + defer push(res, req, it, post) + j, err := fmtJSON(json.RawMessage(post)) if err != nil { res.WriteHeader(http.StatusInternalServerError) @@ -143,6 +174,26 @@ func contentHandlerBySlug(res http.ResponseWriter, req *http.Request) { sendData(res, j, http.StatusOK) } +func hide(it interface{}, res http.ResponseWriter, req *http.Request) bool { + // check if should be hidden + if h, ok := it.(item.Hideable); ok { + err := h.Hide(req) + if err != nil && err.Error() == item.AllowHiddenItem { + return false + } + + if err != nil { + res.WriteHeader(http.StatusInternalServerError) + return true + } + + res.WriteHeader(http.StatusNotFound) + return true + } + + return false +} + func fmtJSON(data ...json.RawMessage) ([]byte, error) { var msg = []json.RawMessage{} for _, d := range data { @@ -193,36 +244,19 @@ func sendData(res http.ResponseWriter, data []byte, code int) { } } -// SendPreflight is used to respond to a cross-origin "OPTIONS" request -func SendPreflight(res http.ResponseWriter) { +// sendPreflight is used to respond to a cross-origin "OPTIONS" request +func sendPreflight(res http.ResponseWriter) { res.Header().Set("Access-Control-Allow-Headers", "Accept, Authorization, Content-Type") res.Header().Set("Access-Control-Allow-Origin", "*") res.WriteHeader(200) return } -// SendJSON returns a Response to a client as JSON -func SendJSON(res http.ResponseWriter, j map[string]interface{}) { - var data []byte - var err error - - data, err = json.Marshal(j) - if err != nil { - log.Println(err) - data, _ = json.Marshal(map[string]interface{}{ - "status": "fail", - "message": err.Error(), - }) - } - - sendData(res, data, 200) -} - // CORS wraps a HandleFunc to respond to OPTIONS requests properly func CORS(next http.HandlerFunc) http.HandlerFunc { return db.CacheControl(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { if req.Method == http.MethodOptions { - SendPreflight(res) + sendPreflight(res) return } diff --git a/system/api/push.go b/system/api/push.go new file mode 100644 index 0000000..5db0a53 --- /dev/null +++ b/system/api/push.go @@ -0,0 +1,37 @@ +package api + +import ( + "log" + "net/http" + + "github.com/ponzu-cms/ponzu/system/item" + + "github.com/tidwall/gjson" +) + +func push(res http.ResponseWriter, req *http.Request, pt func() interface{}, data []byte) { + // Push(target string, opts *PushOptions) error + if pusher, ok := res.(http.Pusher); ok { + if p, ok := pt().(item.Pushable); ok { + // get fields to pull values from data + fields := p.Push() + + // parse values from data to push + values := gjson.GetManyBytes(data, fields...) + + // push all values from Pushable items' fields + for i := range values { + val := values[i] + val.ForEach(func(k, v gjson.Result) bool { + err := pusher.Push(req.URL.Path+v.String(), nil) + if err != nil { + log.Println("Error during Push of value:", v.String()) + } + + return true + }) + } + } + } + +} diff --git a/system/db/content.go b/system/db/content.go index 8bc76a6..dc4477f 100644 --- a/system/db/content.go +++ b/system/db/content.go @@ -229,11 +229,11 @@ func Content(target string) ([]byte, error) { // ContentBySlug does a lookup in the content index to find the type and id of // the requested content. Subsequently, issues the lookup in the type bucket and -// returns the data at that ID or nil if nothing exists. -func ContentBySlug(slug string) ([]byte, error) { +// returns the the type and data at that ID or nil if nothing exists. +func ContentBySlug(slug string) (string, []byte, error) { val := &bytes.Buffer{} + var t, id string err := store.View(func(tx *bolt.Tx) error { - var t, id string b := tx.Bucket([]byte("__contentIndex")) idx := b.Get([]byte(slug)) @@ -256,10 +256,10 @@ func ContentBySlug(slug string) ([]byte, error) { return nil }) if err != nil { - return nil, err + return t, nil, err } - return val.Bytes(), nil + return t, val.Bytes(), nil } // ContentAll retrives all items from the database within the provided namespace diff --git a/system/item/item.go b/system/item/item.go index a813669..761b2cf 100644 --- a/system/item/item.go +++ b/system/item/item.go @@ -55,6 +55,19 @@ type Hookable interface { AfterReject(req *http.Request) error } +// Hideable lets a user keep items hidden +type Hideable interface { + Hide(*http.Request) error +} + +// Pushable lets a user define which values of certain struct fields are +// 'pushed' down to a client via HTTP/2 Server Push. All items in the slice +// should be the json tag names of the struct fields to which they coorespond +type Pushable interface { + // the values contained by fields returned by Push must strictly be URL paths + Push() []string +} + // Item should only be embedded into content type structs. type Item struct { UUID uuid.UUID `json:"uuid"` diff --git a/system/item/types.go b/system/item/types.go index 33e9ced..b4b361b 100644 --- a/system/item/types.go +++ b/system/item/types.go @@ -14,6 +14,11 @@ Add this to the file which defines %[1]s{} in the 'content' package: ` + + // AllowHiddenItem should be used as an error to tell a caller of Hideable#Hide + // that this type is hidden, but should be shown in a particular case, i.e. + // if requested by a valid admin or user + AllowHiddenItem = `Allow hidden item` ) // Types is a map used to reference a type name to its actual Editable type |