Skip to content

Commit

Permalink
Merge pull request #96 from CloudCannon/fix/edge-cases
Browse files Browse the repository at this point in the history
Features and fixes for various edge cases across SSGs, largely around the live editing experience.
  • Loading branch information
bglw authored Feb 25, 2022
2 parents b1e2081 + eeda4d6 commit dc8cedd
Show file tree
Hide file tree
Showing 43 changed files with 953 additions and 80 deletions.
8 changes: 8 additions & 0 deletions guides/hugo.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,14 @@ components:

TIP: _The structures generated by Bookshop for CloudCannon include the `_bookshop_name` field for you. If you're not using structures you will need to add the `_bookshop_name` key by hand_

Lastly, if your component takes no data, you can just reference the component name:
.*index.html*
```html
---
---
{{ partial "bookshop" "logo" }}
```

== Using Bookshop Partials

Bookshop partials can be placed in the `shared/hugo` directory. i.e:
Expand Down
38 changes: 38 additions & 0 deletions guides/live-editing.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -89,3 +89,41 @@ As a result, it is recommended to render groups of Bookshop components using a `
== Editor Links

When live editing, Bookshop will automatically add CloudCannon Editor Links to your components. See the link:editor-links.adoc[Editor Links Guide] for more.

== Customization

Bookshop enables live editing for any component found on the page. For most use cases this will work out-of-the-box, but for complex components that use site functions or data, you might want to disable the live render, so that the component doesn't break in the Visual Editor.

Bookshop can disable live rendering based a flag in the component's props. Bookshop will look for any of the following attributes:

* live_render
* liveRender
* _live_render
* _liveRender

So for example, in Eleventy:

.*index.html*
```
<!-- This component will re-render in the visual editor -->
{% bookshop 'item' props: props %}

<!-- This component will **not** re-render in the visual editor -->
{% bookshop 'page' live_render: false props: props %}
```

If you have a specific component that you never want to live render, you can set `_live_render` in the component props.

.*component.bookshop.toml*
```
[component]
...

[props]
_live_render = false
...
```

This will be hidden from editors due to the leading underscore, and will disable live rendering for that component anywhere it is used.

NOTE: This flag will only work correctly if the component is rendered directly from a site layout. If this component is within another component, it will still update live (as the parent re-rendering will encapsulate it)
4 changes: 2 additions & 2 deletions guides/visual-data-bindings.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,10 @@ So for example, in Eleventy:
.*index.html*
```
<!-- This component will **not** get a binding -->
{% bookshop 'item' data_binding: false, props: props %}
{% bookshop 'item' data_binding: false props: props %}

<!-- This include will get a binding -->
{% bookshop_include 'page' data_binding: true, props: props %}
{% bookshop_include 'page' data_binding: true props: props %}
```

NOTE: This flag only applies to the component directly and doesn't cascade down. Any subcomponents will follow the standard rules, or can also specify their own Data Binding flag.
Expand Down
9 changes: 7 additions & 2 deletions hugo/v2/core/bookshop.html
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
_bookshop_name: <string>, # Component name
..., # Component props
}

Or a <string>: # Component name
*/}}

{{- $component_name := false -}}
Expand All @@ -38,11 +40,14 @@
{{- $err := printf "Expected the provided map to contain a _bookshop_name key. Was given %+v" . -}}
{{- partial "_bookshop/errors/bad_bookshop_tag" $err -}}
{{- end -}}
{{- else if eq (printf "%T" .) "string" -}}
{{- $component_name = . -}}
{{- $component_props = true -}}
{{- else if . -}}
{{- $err := printf "Expected a map or a slice. Was given the %T: %+v" . . -}}
{{- $err := printf "Expected a map, slice, or string. Was given the %T: %+v" . . -}}
{{- partial "_bookshop/errors/bad_bookshop_tag" $err -}}
{{- else -}}
{{- $err := printf "Expected a map or a slice. Was provided with no arguments" -}}
{{- $err := printf "Expected a map, slice, or string. Was provided with no arguments" -}}
{{- partial "_bookshop/errors/bad_bookshop_tag" $err -}}
{{- end -}}

Expand Down
9 changes: 7 additions & 2 deletions hugo/v2/core/bookshop_partial.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
<string>, # Partial name
<_> # Partial props
]

Or a <string>: # Component name
*/}}

{{- $partial_name := false -}}
Expand All @@ -24,11 +26,14 @@
{{- $err := printf "Expected a slice of length 2, was given %d" (len .) -}}
{{- partial "_bookshop/errors/bad_bookshop_tag" $err -}}
{{- end -}}
{{- else if eq (printf "%T" .) "string" -}}
{{- $partial_name = . -}}
{{- $partial_props = true -}}
{{- else if . -}}
{{- $err := printf "bookshop_partial tag expected a slice. Was given the %T: %+v" . . -}}
{{- $err := printf "bookshop_partial tag expected a slice or string. Was given the %T: %+v" . . -}}
{{- partial "_bookshop/errors/bad_bookshop_tag" $err -}}
{{- else -}}
{{- $err := printf "bookshop_partial tag expected a slice. Was provided with no arguments" -}}
{{- $err := printf "bookshop_partial tag expected a slice or string. Was provided with no arguments" -}}
{{- partial "_bookshop/errors/bad_bookshop_tag" $err -}}
{{- end -}}

Expand Down
5 changes: 5 additions & 0 deletions hugo/v2/core/errors/bad_bookshop_tag.html
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@
" │ {{ partial \"bookshop\" .Params.component_structure }}"
" └─"
""
" ► Render a \"logo\" component with no data:"
" ┌─"
" │ {{ partial \"bookshop\" \"logo\" }}"
" └─"
""
" ► Render a \"tag\" partial with data:"
" ┌─"
" │ {{ partial \"bookshop_partial\" (slice \"tag\" (dict \"message\" \"Hello World\")) }}"
Expand Down
2 changes: 1 addition & 1 deletion javascript-modules/engines/eleventy-engine/lib/engine.js
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ export class Engine {
}
return index ? result?.[index] : result;
} catch (e) {
console.error(`Error evaluating \`${str}\` in the Eleventy engine`, e);
console.warn(`Error evaluating \`${str}\` in the Eleventy engine`, e.toString());
return '';
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ const rewriteTag = function (token, src, liveMarkup) {
let componentName;
token.name = 'include';
raw = raw.replace(
/bookshop_include ('|")?(\S+)/,
/bookshop_include[\r\n\s]+('|")?(\S+)/,
(_, quote, component) => {
componentName = component.replace(/('|")$/, '');
return `include ${quote || ''}_bookshop_include_${component}`
Expand All @@ -54,7 +54,7 @@ const rewriteTag = function (token, src, liveMarkup) {
let componentName;
token.name = 'include';
raw = raw.replace(
/bookshop ('|")?(\S+)/,
/bookshop[\r\n\s]+('|")?(\S+)/,
(_, quote, component) => {
componentName = component.replace(/('|")$/, '');
return `include ${quote || ''}_bookshop_${component}`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,14 +86,24 @@ func UnwrapBookshopComponent(context interface{}) (string, interface{}) {
key := fmt.Sprintf("components/%s/%s.hugo.html", component.(string), component.(string))
return key, context
}
return "err_no_bookshop_name_key", context
}
if componentData, ok := context.([]interface{}); ok {
if component, ok := componentData[0].(string); ok {
key := fmt.Sprintf("components/%s/%s.hugo.html", component, component)
return key, componentData[1]
}
return "err_slice_zero_not_string", context
}
return "err_no_bookshop_name", context
if componentData, ok := context.([]string); ok {
key := fmt.Sprintf("components/%s/%s.hugo.html", componentData[0], componentData[0])
return key, componentData[1]
}
if componentName, ok := context.(string); ok {
key := fmt.Sprintf("components/%s/%s.hugo.html", componentName, componentName)
return key, nil
}
return "err_not_map_slice_or_string", context
}

func UnwrapBookshopPartial(context interface{}) (string, interface{}) {
Expand All @@ -102,6 +112,11 @@ func UnwrapBookshopPartial(context interface{}) (string, interface{}) {
key := fmt.Sprintf("shared/hugo/%s.hugo.html", partial)
return key, partialData[1]
}
return "err_slice_zero_not_string", context
}
if partialName, ok := context.(string); ok {
key := fmt.Sprintf("shared/hugo/%s.hugo.html", partialName)
return key, nil
}
return "err_no_bookshop_name", context
return "err_not_slice_or_string", context
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// Copyright 2019 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package transform

import (
"encoding/json"

"github.com/pkg/errors"
)

// Unmarshal unmarshals the data given, which can be either a string, json.RawMessage
// or a Resource. Supported formats are JSON, TOML, YAML, and CSV.
// You can optionally provide an options map as the first argument.
func (ns *Namespace) BookshopUnmarshal(args ...interface{}) (interface{}, error) {
if len(args) != 1 {
return nil, errors.New("Bookshop unmarshal takes 1 argument")
}

var data interface{}

if r, ok := args[0].(string); ok {
if err := json.Unmarshal([]byte(r), &data); err != nil {
return nil, err
}
return data, nil
}
return nil, errors.New("Needed a string")
}
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,13 @@ func init() {

// Bookshop: unmarshal has been removed (for now) due to tendrils

ns.AddMethodMapping(ctx.BookshopUnmarshal,
[]string{"bookshopunmarshal"},
[][2]string{
{`{{ "{ hello = \"Hello World\" }" | transform.BookshopUnmarshal }}`, "map[hello:Hello World]"},
},
)

return ns
}

Expand Down
13 changes: 11 additions & 2 deletions javascript-modules/engines/hugo-engine/lib/engine.js
Original file line number Diff line number Diff line change
Expand Up @@ -162,13 +162,22 @@ export class Engine {
return `index (${obj}) ${index}`;
})

const eval_str = `{{ jsonify (${str}) }}`;
const assignments = Object.entries(props_obj).filter(([key]) => key.startsWith('$')).map(([key, value]) => {
if (Array.isArray(value)) {
return `{{ ${key} := index ( \`{"a": ${JSON.stringify(value)}}\` | transform.BookshopUnmarshal ) "a" }}`
} else if (typeof value === 'object') {
return `{{ ${key} := \`${JSON.stringify(value)}\` | transform.BookshopUnmarshal }}`
} else {
return `{{ ${key} := ${JSON.stringify(value)} }}`
}
}).join('');
const eval_str = `${assignments}{{ jsonify (${str}) }}`;
const output = window.renderHugo(eval_str, JSON.stringify(props_obj));

try {
return JSON.parse(output);
} catch (e) {
console.error(`Error evaluating \`${str}\` in the Hugo engine`, output);
console.warn(`Error evaluating \`${str}\` in the Hugo engine`, output);
return null;
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
const TOKENS = {
DELIM: /"|'|`/,
ESCAPE: /\\/,
SPACE: /\s/,
SPACE: /\s|\r|\n/,
INSCOPE: /\(/,
OUTSCOPE: /\)/,
SCOPE: /\./,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,4 +88,23 @@ test(`Converts a dot index into a JS object property`, async t => {
ident = `(index . 1234)`;
output = (new IdentifierParser(ident)).build();
t.deepEqual(output, `1234`);
});

test(`Multiline dicts and slices`, async t => {
const ident = `(dict
"contents"
(slice 1 "2"
(dict
"a" .Params.b)
) "len" (
len
.some_array
))`;
const output = (new IdentifierParser(ident)).build();
t.deepEqual(output, {
"contents": ["1", `"2"`, {
"a": "Params.b"
}],
"len": `(\n len\n .some_array\n)`
});
});
Loading

0 comments on commit dc8cedd

Please sign in to comment.