[add] Page Builer Component (30%)

This commit is contained in:
Max 2023-09-25 14:05:58 +08:00
parent bd0dbd4db8
commit 42fa2e140a
9 changed files with 449 additions and 186 deletions

58
sui/core/component.go Normal file
View file

@ -0,0 +1,58 @@
package core
import (
"fmt"
"strings"
"github.com/evanw/esbuild/pkg/api"
"github.com/yaoapp/gou/runtime/transform"
)
// Compile compile the component
func (component *Component) Compile() (string, error) {
// Typescript is the default language
// Typescript
if component.Codes.TS.Code != "" {
varName := strings.Replace(component.ID, "-", "_", -1)
ts := strings.Replace(component.Codes.TS.Code, "export default", fmt.Sprintf("window.component__%s =", varName), 1)
if component.Codes.HTML.Code != "" && !strings.Contains(component.Codes.TS.Code, "content:") {
html := strings.ReplaceAll(component.Codes.HTML.Code, "`", "\\`")
ts = strings.Replace(ts, "defaults: {", "defaults: {\n components: `"+html+"`,", 1)
}
js, err := transform.TypeScript(ts, api.TransformOptions{
Target: api.ESNext,
MinifyWhitespace: true,
MinifyIdentifiers: true,
MinifySyntax: true,
})
if err != nil {
return "", err
}
component.Compiled = js
return js, nil
}
// Javascript
if component.Codes.JS.Code == "" {
return "", fmt.Errorf("Block %s has no JS code", component.ID)
}
varName := strings.Replace(component.ID, "-", "_", -1)
js := strings.Replace(component.Codes.JS.Code, "export default", fmt.Sprintf("window.component__%s =", varName), 1)
if component.Codes.HTML.Code != "" && !strings.Contains(component.Codes.JS.Code, "content:") {
html := strings.ReplaceAll(component.Codes.HTML.Code, "`", "\\`")
js = strings.Replace(js, "defaults: {", "defaults: {\n components: `"+html+"`,", 1)
}
minified, err := transform.MinifyJS(js)
if err != nil {
return "", err
}
component.Compiled = minified
return minified, nil
}

View file

@ -25,8 +25,10 @@ type Page struct {
// Component is the struct for the component
type Component struct {
templage *Template
name string
ID string `json:"id"`
Name string `json:"name,omitempty"`
Compiled string `json:"-"`
Codes SourceCodes `json:"-"`
}
// Block is the struct for the block

View file

@ -1,9 +1,69 @@
package local
import (
"fmt"
"path/filepath"
"github.com/yaoapp/kun/log"
"github.com/yaoapp/yao/sui/core"
)
// Blocks get the blocks
func (tmpl *Template) Blocks() ([]core.IBlock, error) {
path := filepath.Join(tmpl.Root, "__blocks")
blocks := []core.IBlock{}
if exist, _ := tmpl.local.fs.Exists(path); !exist {
return blocks, nil
}
dirs, err := tmpl.local.fs.ReadDir(path, false)
if err != nil {
return nil, err
}
for _, dir := range dirs {
if !tmpl.local.fs.IsDir(dir) {
continue
}
block, err := tmpl.getBlockFrom(dir)
if err != nil {
log.Error("Get block error: %v", err)
continue
}
blocks = append(blocks, block)
}
return blocks, nil
}
// Block get the block
func (tmpl *Template) Block(id string) (core.IBlock, error) {
path := filepath.Join(tmpl.Root, "__blocks", id)
if exist, _ := tmpl.local.fs.Exists(path); !exist {
return nil, fmt.Errorf("Block %s not found", id)
}
block, err := tmpl.getBlockFrom(path)
if err != nil {
return nil, err
}
err = block.Load()
if err != nil {
return nil, err
}
_, err = block.Compile()
if err != nil {
return nil, err
}
return block, nil
}
// Load get the block from the storage
func (block *Block) Load() error {
@ -38,3 +98,37 @@ func (block *Block) Load() error {
return nil
}
func (tmpl *Template) getBlockFrom(path string) (core.IBlock, error) {
id := tmpl.getBlockID(path)
return tmpl.getBlock(id)
}
func (tmpl *Template) getBlock(id string) (core.IBlock, error) {
path := filepath.Join(tmpl.Root, "__blocks", id)
if !tmpl.local.fs.IsDir(path) {
return nil, fmt.Errorf("Block %s not found", id)
}
jsFile := filepath.Join("/", id, fmt.Sprintf("%s.js", id))
tsFile := filepath.Join("/", id, fmt.Sprintf("%s.ts", id))
htmlFile := filepath.Join("/", id, fmt.Sprintf("%s.html", id))
block := &Block{
tmpl: tmpl,
Block: &core.Block{
ID: id,
Codes: core.SourceCodes{
HTML: core.Source{File: htmlFile},
JS: core.Source{File: jsFile},
TS: core.Source{File: tsFile},
},
},
}
return block, nil
}
func (tmpl *Template) getBlockID(path string) string {
return filepath.Base(path)
}

View file

@ -0,0 +1,83 @@
package local
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestTemplateBlocks(t *testing.T) {
tests := prepare(t)
defer clean()
tmpl, err := tests.Demo.GetTemplate("tech-blue")
if err != nil {
t.Fatalf("GetTemplate error: %v", err)
}
blocks, err := tmpl.Blocks()
if err != nil {
t.Fatalf("Blocks error: %v", err)
}
if len(blocks) < 3 {
t.Fatalf("Blocks error: %v", len(blocks))
}
assert.Equal(t, "ColumnsTwo", blocks[0].(*Block).ID)
assert.Equal(t, "/ColumnsTwo/ColumnsTwo.html", blocks[0].(*Block).Codes.HTML.File)
assert.Equal(t, "/ColumnsTwo/ColumnsTwo.js", blocks[0].(*Block).Codes.JS.File)
assert.Equal(t, "/ColumnsTwo/ColumnsTwo.ts", blocks[0].(*Block).Codes.TS.File)
assert.Equal(t, "Hero", blocks[1].(*Block).ID)
assert.Equal(t, "/Hero/Hero.html", blocks[1].(*Block).Codes.HTML.File)
assert.Equal(t, "/Hero/Hero.js", blocks[1].(*Block).Codes.JS.File)
assert.Equal(t, "/Hero/Hero.ts", blocks[1].(*Block).Codes.TS.File)
assert.Equal(t, "Section", blocks[2].(*Block).ID)
assert.Equal(t, "/Section/Section.html", blocks[2].(*Block).Codes.HTML.File)
assert.Equal(t, "/Section/Section.js", blocks[2].(*Block).Codes.JS.File)
assert.Equal(t, "/Section/Section.ts", blocks[2].(*Block).Codes.TS.File)
}
func TestTemplateBlockJS(t *testing.T) {
tests := prepare(t)
defer clean()
tmpl, err := tests.Demo.GetTemplate("tech-blue")
if err != nil {
t.Fatalf("GetTemplate error: %v", err)
}
block, err := tmpl.Block("ColumnsTwo")
if err != nil {
t.Fatalf("Blocks error: %v", err)
}
assert.Equal(t, "ColumnsTwo", block.(*Block).ID)
assert.NotEmpty(t, block.(*Block).Codes.HTML.Code)
assert.NotEmpty(t, block.(*Block).Codes.JS.Code)
assert.Contains(t, block.(*Block).Compiled, "window.block__ColumnsTwo")
assert.Contains(t, block.(*Block).Compiled, `<div class="columns-two-left"`)
}
func TestTemplateBlockTS(t *testing.T) {
tests := prepare(t)
defer clean()
tmpl, err := tests.Demo.GetTemplate("tech-blue")
if err != nil {
t.Fatalf("GetTemplate error: %v", err)
}
block, err := tmpl.Block("Hero")
if err != nil {
t.Fatalf("Blocks error: %v", err)
}
assert.Equal(t, "Hero", block.(*Block).ID)
assert.Empty(t, block.(*Block).Codes.HTML.Code)
assert.NotEmpty(t, block.(*Block).Codes.TS.Code)
assert.Contains(t, block.(*Block).Compiled, "window.block__Hero")
assert.Contains(t, block.(*Block).Compiled, `<div data-gjs-type='Nav'></div>`)
}

View file

@ -1,16 +1,134 @@
package local
// NewComponent create a new local component
func NewComponent(file string) (*Component, error) {
return nil, nil
import (
"fmt"
"path/filepath"
"github.com/yaoapp/kun/log"
"github.com/yaoapp/yao/sui/core"
)
// Components get the components
func (tmpl *Template) Components() ([]core.IComponent, error) {
path := filepath.Join(tmpl.Root, "__components")
components := []core.IComponent{}
if exist, _ := tmpl.local.fs.Exists(path); !exist {
return components, nil
}
dirs, err := tmpl.local.fs.ReadDir(path, false)
if err != nil {
return nil, err
}
for _, dir := range dirs {
if !tmpl.local.fs.IsDir(dir) {
continue
}
block, err := tmpl.getComponentFrom(dir)
if err != nil {
log.Error("Get block error: %v", err)
continue
}
components = append(components, block)
}
return components, nil
}
// Get get the component
func (comp *Component) Get() error {
// Component get the component
func (tmpl *Template) Component(id string) (core.IComponent, error) {
path := filepath.Join(tmpl.Root, "__components", id)
if exist, _ := tmpl.local.fs.Exists(path); !exist {
return nil, fmt.Errorf("Block %s not found", id)
}
component, err := tmpl.getComponentFrom(path)
if err != nil {
return nil, err
}
err = component.Load()
if err != nil {
return nil, err
}
_, err = component.Compile()
if err != nil {
return nil, err
}
return component, nil
}
// Load get the component from the storage
func (component *Component) Load() error {
root := filepath.Join(component.tmpl.Root, "__components")
// Type script is the default language
tsFile := filepath.Join(root, component.Codes.TS.File)
if exist, _ := component.tmpl.local.fs.Exists(tsFile); exist {
tsCode, err := component.tmpl.local.fs.ReadFile(tsFile)
if err != nil {
return err
}
component.Codes.TS.Code = string(tsCode)
} else {
jsFile := filepath.Join(root, component.Codes.JS.File)
jsCode, err := component.tmpl.local.fs.ReadFile(jsFile)
if err != nil {
return err
}
component.Codes.JS.Code = string(jsCode)
}
htmlFile := filepath.Join(root, component.Codes.HTML.File)
if exist, _ := component.tmpl.local.fs.Exists(htmlFile); exist {
htmlCode, err := component.tmpl.local.fs.ReadFile(htmlFile)
if err != nil {
return err
}
component.Codes.HTML.Code = string(htmlCode)
}
return nil
}
// Save save the component
func (comp *Component) Save() error {
return nil
func (tmpl *Template) getComponentFrom(path string) (core.IComponent, error) {
id := tmpl.getComponentID(path)
return tmpl.getComponent(id)
}
func (tmpl *Template) getComponent(id string) (core.IComponent, error) {
path := filepath.Join(tmpl.Root, "__components", id)
if !tmpl.local.fs.IsDir(path) {
return nil, fmt.Errorf("Component %s not found", id)
}
jsFile := filepath.Join("/", id, fmt.Sprintf("%s.js", id))
tsFile := filepath.Join("/", id, fmt.Sprintf("%s.ts", id))
htmlFile := filepath.Join("/", id, fmt.Sprintf("%s.html", id))
component := &Component{
tmpl: tmpl,
Component: &core.Component{
ID: id,
Codes: core.SourceCodes{
HTML: core.Source{File: htmlFile},
JS: core.Source{File: jsFile},
TS: core.Source{File: tsFile},
},
},
}
return component, nil
}
func (tmpl *Template) getComponentID(path string) string {
return filepath.Base(path)
}

View file

@ -0,0 +1,83 @@
package local
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestTemplateComponents(t *testing.T) {
tests := prepare(t)
defer clean()
tmpl, err := tests.Demo.GetTemplate("tech-blue")
if err != nil {
t.Fatalf("GetTemplate error: %v", err)
}
components, err := tmpl.Components()
if err != nil {
t.Fatalf("Components error: %v", err)
}
if len(components) < 2 {
t.Fatalf("Components error: %v", len(components))
}
assert.Equal(t, "Box", components[0].(*Component).ID)
assert.Equal(t, "/Box/Box.html", components[0].(*Component).Codes.HTML.File)
assert.Equal(t, "/Box/Box.js", components[0].(*Component).Codes.JS.File)
assert.Equal(t, "/Box/Box.ts", components[0].(*Component).Codes.TS.File)
assert.Equal(t, "Card", components[1].(*Component).ID)
assert.Equal(t, "/Card/Card.html", components[1].(*Component).Codes.HTML.File)
assert.Equal(t, "/Card/Card.js", components[1].(*Component).Codes.JS.File)
assert.Equal(t, "/Card/Card.ts", components[1].(*Component).Codes.TS.File)
assert.Equal(t, "Nav", components[2].(*Component).ID)
assert.Equal(t, "/Nav/Nav.html", components[2].(*Component).Codes.HTML.File)
assert.Equal(t, "/Nav/Nav.js", components[2].(*Component).Codes.JS.File)
assert.Equal(t, "/Nav/Nav.ts", components[2].(*Component).Codes.TS.File)
}
func TestTemplateComponentJS(t *testing.T) {
tests := prepare(t)
defer clean()
tmpl, err := tests.Demo.GetTemplate("tech-blue")
if err != nil {
t.Fatalf("GetTemplate error: %v", err)
}
component, err := tmpl.Component("Card")
if err != nil {
t.Fatalf("Components error: %v", err)
}
assert.Equal(t, "Card", component.(*Component).ID)
assert.NotEmpty(t, component.(*Component).Codes.HTML.Code)
assert.NotEmpty(t, component.(*Component).Codes.JS.Code)
assert.Contains(t, component.(*Component).Compiled, "window.component__Card")
assert.Contains(t, component.(*Component).Compiled, `<h1>Card</h1>`)
}
func TestTemplateComponentTS(t *testing.T) {
tests := prepare(t)
defer clean()
tmpl, err := tests.Demo.GetTemplate("tech-blue")
if err != nil {
t.Fatalf("GetTemplate error: %v", err)
}
component, err := tmpl.Component("Box")
if err != nil {
t.Fatalf("Components error: %v", err)
}
assert.Equal(t, "Box", component.(*Component).ID)
assert.NotEmpty(t, component.(*Component).Codes.HTML.Code)
assert.NotEmpty(t, component.(*Component).Codes.TS.Code)
assert.Contains(t, component.(*Component).Compiled, "window.component__Box")
assert.Contains(t, component.(*Component).Compiled, `<div>Box</div>`)
}

View file

@ -91,106 +91,6 @@ func (tmpl *Template) getPageRoute(path string) string {
return strings.TrimSuffix(path[len(tmpl.Root):], filepath.Ext(path))
}
// Blocks get the blocks
func (tmpl *Template) Blocks() ([]core.IBlock, error) {
path := filepath.Join(tmpl.Root, "__blocks")
blocks := []core.IBlock{}
if exist, _ := tmpl.local.fs.Exists(path); !exist {
return blocks, nil
}
dirs, err := tmpl.local.fs.ReadDir(path, false)
if err != nil {
return nil, err
}
for _, dir := range dirs {
if !tmpl.local.fs.IsDir(dir) {
continue
}
block, err := tmpl.getBlockFrom(dir)
if err != nil {
log.Error("Get block error: %v", err)
continue
}
blocks = append(blocks, block)
}
return blocks, nil
}
// Block get the block
func (tmpl *Template) Block(id string) (core.IBlock, error) {
path := filepath.Join(tmpl.Root, "__blocks", id)
if exist, _ := tmpl.local.fs.Exists(path); !exist {
return nil, fmt.Errorf("Block %s not found", id)
}
block, err := tmpl.getBlockFrom(path)
if err != nil {
return nil, err
}
err = block.Load()
if err != nil {
return nil, err
}
_, err = block.Compile()
if err != nil {
return nil, err
}
return block, nil
}
func (tmpl *Template) getBlockFrom(path string) (core.IBlock, error) {
id := tmpl.getBlockID(path)
return tmpl.getBlock(id)
}
func (tmpl *Template) getBlock(id string) (core.IBlock, error) {
path := filepath.Join(tmpl.Root, "__blocks", id)
if !tmpl.local.fs.IsDir(path) {
return nil, fmt.Errorf("Block %s not found", id)
}
jsFile := filepath.Join("/", id, "main.js")
tsFile := filepath.Join("/", id, "main.ts")
htmlFile := filepath.Join("/", id, "main.html")
block := &Block{
tmpl: tmpl,
Block: &core.Block{
ID: id,
Codes: core.SourceCodes{
HTML: core.Source{File: htmlFile},
JS: core.Source{File: jsFile},
TS: core.Source{File: tsFile},
},
},
}
return block, nil
}
func (tmpl *Template) getBlockID(path string) string {
return filepath.Base(path)
}
// Components get the components
func (tmpl *Template) Components() ([]core.IComponent, error) {
return nil, nil
}
// Component get the component
func (tmpl *Template) Component(name string) (core.IComponent, error) {
return nil, nil
}
// Styles get the global styles
func (tmpl *Template) Styles() []string {
return nil

View file

@ -60,79 +60,3 @@ func TestTemplatePage(t *testing.T) {
_, err = tmpl.Page("/the/page/could/not/be/found")
assert.Contains(t, err.Error(), "Page /the/page/could/not/be/found not found")
}
func TestTemplateBlocks(t *testing.T) {
tests := prepare(t)
defer clean()
tmpl, err := tests.Demo.GetTemplate("tech-blue")
if err != nil {
t.Fatalf("GetTemplate error: %v", err)
}
blocks, err := tmpl.Blocks()
if err != nil {
t.Fatalf("Blocks error: %v", err)
}
if len(blocks) < 3 {
t.Fatalf("Blocks error: %v", len(blocks))
}
assert.Equal(t, "columns-two", blocks[0].(*Block).ID)
assert.Equal(t, "/columns-two/main.html", blocks[0].(*Block).Codes.HTML.File)
assert.Equal(t, "/columns-two/main.js", blocks[0].(*Block).Codes.JS.File)
assert.Equal(t, "/columns-two/main.ts", blocks[0].(*Block).Codes.TS.File)
assert.Equal(t, "hero", blocks[1].(*Block).ID)
assert.Equal(t, "/hero/main.html", blocks[1].(*Block).Codes.HTML.File)
assert.Equal(t, "/hero/main.js", blocks[1].(*Block).Codes.JS.File)
assert.Equal(t, "/hero/main.ts", blocks[1].(*Block).Codes.TS.File)
assert.Equal(t, "section", blocks[2].(*Block).ID)
assert.Equal(t, "/section/main.html", blocks[2].(*Block).Codes.HTML.File)
assert.Equal(t, "/section/main.js", blocks[2].(*Block).Codes.JS.File)
assert.Equal(t, "/section/main.ts", blocks[2].(*Block).Codes.TS.File)
}
func TestTemplateBlockJS(t *testing.T) {
tests := prepare(t)
defer clean()
tmpl, err := tests.Demo.GetTemplate("tech-blue")
if err != nil {
t.Fatalf("GetTemplate error: %v", err)
}
block, err := tmpl.Block("columns-two")
if err != nil {
t.Fatalf("Blocks error: %v", err)
}
assert.Equal(t, "columns-two", block.(*Block).ID)
assert.NotEmpty(t, block.(*Block).Codes.HTML.Code)
assert.NotEmpty(t, block.(*Block).Codes.JS.Code)
assert.Contains(t, block.(*Block).Compiled, "window.block__columns_two")
assert.Contains(t, block.(*Block).Compiled, `<div class="columns-two-left"`)
}
func TestTemplateBlockTS(t *testing.T) {
tests := prepare(t)
defer clean()
tmpl, err := tests.Demo.GetTemplate("tech-blue")
if err != nil {
t.Fatalf("GetTemplate error: %v", err)
}
block, err := tmpl.Block("hero")
if err != nil {
t.Fatalf("Blocks error: %v", err)
}
assert.Equal(t, "hero", block.(*Block).ID)
assert.Empty(t, block.(*Block).Codes.HTML.Code)
assert.NotEmpty(t, block.(*Block).Codes.TS.Code)
assert.Contains(t, block.(*Block).Compiled, "window.block__hero")
assert.Contains(t, block.(*Block).Compiled, `<div data-gjs-type='nav'></div>`)
}

View file

@ -33,5 +33,6 @@ type Block struct {
// Component is the struct for the local sui component
type Component struct {
tmpl *Template
*core.Component
}