Repository: buxlabs/boxwood (js)

Issue:
Nesting layouts (or layout inheritance) is not possible
I'm trying to inherit from a layout with the following setup

`base` layout

```html
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>

<body>
  <h1>Base Layout</h1>
  <slot />
</body>

</html>
```

`docs` layout

```html
<import base from="base.html" />

<base>
  <h1>Inside Docs</h1>
  <slot />
</base>
```

finally a documentation page that inherits from the `docs`

```html
<import layout from='layouts/docs.html' />

<layout>
  <h1>Specific Doc</h1>
</layout
```

I'm getting:

```
RangeError: Maximum call stack size exceeded
Exception in PromiseRejectCallback:
internal/process/promises.js:80
      unhandledRejection(promise, reason);
```

Pull request description:
Handle nested slots
Closes #609 

Patch:
diff --git a/src/utilities/collect.js b/src/utilities/collect.js
index 7a49288..64fb0e3 100644
--- a/src/utilities/collect.js
+++ b/src/utilities/collect.js
@@ -129,7 +129,7 @@ function collectComponentFromPath (path, fragment, assets, context, plugins, err
   }
   const asset = findAsset(path, assets, { paths, aliases: options.aliases })
   if (!asset) return
-  resolveComponent(asset.source, asset.path, null, fragment, plugins, errors, assets, options)
+  resolveComponent(asset.source, asset.path, null, fragment, [], plugins, errors, assets, options)
 }
 
 function inlineData (htmlTree, content, path, assets, plugins, errors, localVariables, options) {
@@ -153,7 +153,11 @@ function collectInlineComponents (fragment, attributes, components) {
   })
 }
 
-function resolveComponent (content, path, component, fragment, plugins, errors, assets, options) {
+function isSlotOrYield (node) {
+  return node.tagName === 'slot' || node.tagName === 'yield'
+}
+
+function resolveComponent (content, path, component, fragment, queue, plugins, errors, assets, options) {
   const localVariables = normalizeAttributes(fragment.attributes)
   const htmlComponent = new Component(content, localVariables)
   htmlComponent.optimize()
@@ -165,7 +169,7 @@ function resolveComponent (content, path, component, fragment, plugins, errors,
   const currentComponents = []
   let slots = 0
   let children = fragment.children
-  walk(htmlTree, async (current, parent) => {
+  walk(htmlTree, async (current) => {
     const attrs = current.attributes || []
     const keys = attrs.map(attr => attr.key)
     if (isImportTag(current.tagName)) {
@@ -181,15 +185,22 @@ function resolveComponent (content, path, component, fragment, plugins, errors,
     const unresolvable = component && current.tagName === component.name && !currentComponent
     if (unresolvable) { current.unresolvable = true }
     if (currentComponent && !current.unresolvable) {
-      resolveComponent(currentComponent.content, currentComponent.path, currentComponent, current, plugins, errors, assets, options)
+      queue.push(children)
+      resolveComponent(currentComponent.content, currentComponent.path, currentComponent, current, queue, plugins, errors, assets, options)
       current.used = true
     }
-    if ((current.tagName === 'slot' || current.tagName === 'yield') && current.children.length === 0) {
+    if (isSlotOrYield(current) && current.children.length === 0 && !current.yielded) {
       if (current.attributes.length === 0) {
         // putting a slot into a slot is problematic
         if (children.length === 1 && children[0].tagName === 'slot') {
           children = children[0].children
         }
+        walk(children, leaf => {
+          leaf.yielded = true
+          if (isSlotOrYield(leaf)) {
+            leaf.children = queue.pop()
+          }
+        })
         if (slots === 0) {
           current.children = children
         } else {
@@ -199,9 +210,10 @@ function resolveComponent (content, path, component, fragment, plugins, errors,
       } else {
         const name = current.attributes[0].key
         walk(children, leaf => {
-          if ((leaf.tagName === 'slot' || leaf.tagName === 'yield') && leaf.attributes.length > 0 && leaf.attributes[0].key === name) {
+          if (isSlotOrYield(leaf) && leaf.attributes.length > 0 && leaf.attributes[0].key === name) {
             // the following might not be super performant in case of components with multiple slots
             // we could do this only if a slot with given name is not unique (e.g. in if / else statements)
+            // TODO handle deeply nested named slots with queue.pop()
             if (slots > 2) {
               current.children = clone(leaf.children)
             } else {
@@ -256,7 +268,7 @@ async function collect ({ source, tree, fragment, assets, variables, filters, co
       })
     })
     if (component && !fragment.imported) {
-      const { localVariables } = resolveComponent(component.content, component.path, component, fragment, plugins, errors, assets, options)
+      const { localVariables } = resolveComponent(component.content, component.path, component, fragment, [], plugins, errors, assets, options)
       localVariables.forEach(variable => variables.push(variable.key))
       const ast = new AbstractSyntaxTree('')
       collectChildren(fragment, ast)


Test patch:
diff --git a/test/fixtures/acceptance/nested-slots-2-levels/actual.html b/test/fixtures/acceptance/nested-slots-2-levels/actual.html
new file mode 100644
index 0000000..4f30a9b
--- /dev/null
+++ b/test/fixtures/acceptance/nested-slots-2-levels/actual.html
@@ -0,0 +1,3 @@
+<import layout from="layouts/default.html" />
+
+<layout>foo</layout>
diff --git a/test/fixtures/acceptance/nested-slots-2-levels/data.json b/test/fixtures/acceptance/nested-slots-2-levels/data.json
new file mode 100644
index 0000000..0967ef4
--- /dev/null
+++ b/test/fixtures/acceptance/nested-slots-2-levels/data.json
@@ -0,0 +1,1 @@
+{}
diff --git a/test/fixtures/acceptance/nested-slots-2-levels/expected.html b/test/fixtures/acceptance/nested-slots-2-levels/expected.html
new file mode 100644
index 0000000..aac8206
--- /dev/null
+++ b/test/fixtures/acceptance/nested-slots-2-levels/expected.html
@@ -0,0 +1,8 @@
+<html>
+  <head>
+    <title>nested-slots</title>
+  </head>
+  <body>
+    <main>foo</main>
+  </body>
+</html>
diff --git a/test/fixtures/acceptance/nested-slots-2-levels/layouts/base.html b/test/fixtures/acceptance/nested-slots-2-levels/layouts/base.html
new file mode 100644
index 0000000..6dc2790
--- /dev/null
+++ b/test/fixtures/acceptance/nested-slots-2-levels/layouts/base.html
@@ -0,0 +1,8 @@
+<html>
+  <head>
+    <title>nested-slots</title>
+  </head>
+  <body>
+    <slot />
+  </body>
+</html>
diff --git a/test/fixtures/acceptance/nested-slots-2-levels/layouts/default.html b/test/fixtures/acceptance/nested-slots-2-levels/layouts/default.html
new file mode 100644
index 0000000..0cb3caf
--- /dev/null
+++ b/test/fixtures/acceptance/nested-slots-2-levels/layouts/default.html
@@ -0,0 +1,5 @@
+<import base from="./base.html" />
+
+<base>
+  <main><slot/></main>
+</base>
diff --git a/test/fixtures/acceptance/nested-slots-3-levels/actual.html b/test/fixtures/acceptance/nested-slots-3-levels/actual.html
new file mode 100644
index 0000000..8335044
--- /dev/null
+++ b/test/fixtures/acceptance/nested-slots-3-levels/actual.html
@@ -0,0 +1,3 @@
+<import layout from="./layouts/default.html" />
+
+<layout>foo</layout>
diff --git a/test/fixtures/acceptance/nested-slots-3-levels/data.json b/test/fixtures/acceptance/nested-slots-3-levels/data.json
new file mode 100644
index 0000000..0967ef4
--- /dev/null
+++ b/test/fixtures/acceptance/nested-slots-3-levels/data.json
@@ -0,0 +1,1 @@
+{}
diff --git a/test/fixtures/acceptance/nested-slots-3-levels/expected.html b/test/fixtures/acceptance/nested-slots-3-levels/expected.html
new file mode 100644
index 0000000..4cf4eec
--- /dev/null
+++ b/test/fixtures/acceptance/nested-slots-3-levels/expected.html
@@ -0,0 +1,8 @@
+<html>
+  <head>
+    <title>nested-slots-deep</title>
+  </head>
+  <body>
+    <main><div>foo</div></main>
+  </body>
+</html>
diff --git a/test/fixtures/acceptance/nested-slots-3-levels/layouts/blank.html b/test/fixtures/acceptance/nested-slots-3-levels/layouts/blank.html
new file mode 100644
index 0000000..bbb132c
--- /dev/null
+++ b/test/fixtures/acceptance/nested-slots-3-levels/layouts/blank.html
@@ -0,0 +1,8 @@
+<html>
+  <head>
+    <title>nested-slots-deep</title>
+  </head>
+  <body>
+    <slot/>
+  </body>
+</html>
diff --git a/test/fixtures/acceptance/nested-slots-3-levels/layouts/default.html b/test/fixtures/acceptance/nested-slots-3-levels/layouts/default.html
new file mode 100644
index 0000000..0e1f1ae
--- /dev/null
+++ b/test/fixtures/acceptance/nested-slots-3-levels/layouts/default.html
@@ -0,0 +1,5 @@
+<import layout from="./standard.html" />
+
+<layout>
+  <div><slot /></div>
+</layout>
diff --git a/test/fixtures/acceptance/nested-slots-3-levels/layouts/standard.html b/test/fixtures/acceptance/nested-slots-3-levels/layouts/standard.html
new file mode 100644
index 0000000..98fbf05
--- /dev/null
+++ b/test/fixtures/acceptance/nested-slots-3-levels/layouts/standard.html
@@ -0,0 +1,5 @@
+<import layout from="./blank.html" />
+
+<layout>
+  <main><slot /></main>
+</layout>
diff --git a/test/fixtures/acceptance/nested-slots-4-levels/actual.html b/test/fixtures/acceptance/nested-slots-4-levels/actual.html
new file mode 100644
index 0000000..cce4330
--- /dev/null
+++ b/test/fixtures/acceptance/nested-slots-4-levels/actual.html
@@ -0,0 +1,3 @@
+<import foo from="./components/foo.html"/>
+
+<foo>quux</foo>
diff --git a/test/fixtures/acceptance/nested-slots-4-levels/components/bar.html b/test/fixtures/acceptance/nested-slots-4-levels/components/bar.html
new file mode 100644
index 0000000..c0ff109
--- /dev/null
+++ b/test/fixtures/acceptance/nested-slots-4-levels/components/bar.html
@@ -0,0 +1,3 @@
+<import baz from="./baz.html"/>
+
+<div><baz><slot /></baz></div>
diff --git a/test/fixtures/acceptance/nested-slots-4-levels/components/baz.html b/test/fixtures/acceptance/nested-slots-4-levels/components/baz.html
new file mode 100644
index 0000000..a970952
--- /dev/null
+++ b/test/fixtures/acceptance/nested-slots-4-levels/components/baz.html
@@ -0,0 +1,3 @@
+<import qux from="./qux.html"/>
+
+<div><qux><slot /></qux></div>
diff --git a/test/fixtures/acceptance/nested-slots-4-levels/components/foo.html b/test/fixtures/acceptance/nested-slots-4-levels/components/foo.html
new file mode 100644
index 0000000..33e29be
--- /dev/null
+++ b/test/fixtures/acceptance/nested-slots-4-levels/components/foo.html
@@ -0,0 +1,3 @@
+<import bar from="./bar.html"/>
+
+<div><bar><slot /></bar></div>
diff --git a/test/fixtures/acceptance/nested-slots-4-levels/components/qux.html b/test/fixtures/acceptance/nested-slots-4-levels/components/qux.html
new file mode 100644
index 0000000..d2314b7
--- /dev/null
+++ b/test/fixtures/acceptance/nested-slots-4-levels/components/qux.html
@@ -0,0 +1,1 @@
+<div><slot /></div>
diff --git a/test/fixtures/acceptance/nested-slots-4-levels/data.json b/test/fixtures/acceptance/nested-slots-4-levels/data.json
new file mode 100644
index 0000000..0967ef4
--- /dev/null
+++ b/test/fixtures/acceptance/nested-slots-4-levels/data.json
@@ -0,0 +1,1 @@
+{}
diff --git a/test/fixtures/acceptance/nested-slots-4-levels/expected.html b/test/fixtures/acceptance/nested-slots-4-levels/expected.html
new file mode 100644
index 0000000..955b33f
--- /dev/null
+++ b/test/fixtures/acceptance/nested-slots-4-levels/expected.html
@@ -0,0 +1,9 @@
+<div>
+  <div>
+    <div>
+      <div>
+        quux
+      </div>
+    </div>
+  </div>
+</div>
diff --git a/test/spec/acceptance.spec.js b/test/spec/acceptance.spec.js
index 107a577..a5b892e 100644
--- a/test/spec/acceptance.spec.js
+++ b/test/spec/acceptance.spec.js
@@ -168,6 +168,21 @@ test('acceptance: data', async assert => {
   assert.deepEqual(actual, expected)
 })
 
+test('acceptance: nested-slots-2-levels', async assert => {
+  const { actual, expected } = await suite('nested-slots-2-levels')
+  assert.deepEqual(actual, expected)
+})
+
+test('acceptance: nested-slots-3-levels', async assert => {
+  const { actual, expected } = await suite('nested-slots-3-levels')
+  assert.deepEqual(actual, expected)
+})
+
+test('acceptance: nested-slots-4-levels', async assert => {
+  const { actual, expected } = await suite('nested-slots-4-levels')
+  assert.deepEqual(actual, expected)
+})
+
 test.skip('acceptance: slot as a variable', async assert => {
   const { actual, expected } = await suite('slot-as-variable')
   assert.deepEqual(actual, expected)