Add initial nest version, add tests
This commit is contained in:
parent
0152ebeb91
commit
5b1368dca0
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
templates/* linguist-vendored
|
22
README.org
Normal file
22
README.org
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
#+title: Template Nest
|
||||||
|
#+subtitle: manipulate a generic template structure
|
||||||
|
|
||||||
|
~Template Nest~ is a template engine module for Go, designed to process nested
|
||||||
|
templates quickly and efficiently.
|
||||||
|
|
||||||
|
For more details on the idea behind ~Template::Nest~ read:
|
||||||
|
https://metacpan.org/pod/Template::Nest#DESCRIPTION and
|
||||||
|
https://pypi.org/project/template-nest/.
|
||||||
|
|
||||||
|
* News
|
||||||
|
|
||||||
|
** v0.1.0 - Upcoming
|
||||||
|
|
||||||
|
+ Initial Release.
|
||||||
|
|
||||||
|
* Other Implementations
|
||||||
|
|
||||||
|
- [[https://metacpan.org/pod/Template::Nest][Template::Nest (Perl 5)]]
|
||||||
|
- [[https://pypi.org/project/template-nest/][template-nest (Python)]]
|
||||||
|
- [[https://raku.land/zef:jaffa4/Template::Nest::XS][Template::Nest::XS (Raku)]]
|
||||||
|
- [[https://raku.land/zef:andinus/Template::Nest::Fast][Template::Nest::Fast (Raku)]]
|
11
go.mod
Normal file
11
go.mod
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
module git.virtual.blue/tomgracey/template-nest-go
|
||||||
|
|
||||||
|
go 1.22
|
||||||
|
|
||||||
|
require github.com/stretchr/testify v1.9.0
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
)
|
10
go.sum
Normal file
10
go.sum
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||||
|
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
262
template_nest.go
Normal file
262
template_nest.go
Normal file
@ -0,0 +1,262 @@
|
|||||||
|
package templatenest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Hash map[string]interface{}
|
||||||
|
|
||||||
|
// Option holds configuration for TemplateNest
|
||||||
|
type Option struct {
|
||||||
|
Delimiters [2]string
|
||||||
|
NameLabel string
|
||||||
|
TemplateDir string
|
||||||
|
TemplateExtension string
|
||||||
|
DieOnBadParams bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type TemplateNest struct {
|
||||||
|
option Option
|
||||||
|
cache map[string]TemplateFileIndex
|
||||||
|
}
|
||||||
|
|
||||||
|
// TemplateFileIndex represents an indexed template file.
|
||||||
|
type TemplateFileIndex struct {
|
||||||
|
Contents string
|
||||||
|
LastModified time.Time // Last modified timestamp of the template file
|
||||||
|
Variables []TemplateFileVariable // List of variables in the template file
|
||||||
|
VariableNames map[string]struct{} // Set of variable names
|
||||||
|
}
|
||||||
|
|
||||||
|
// TemplateFileVariable represents a variable in a template file.
|
||||||
|
type TemplateFileVariable struct {
|
||||||
|
Name string
|
||||||
|
StartPosition uint // Start position of the complete variable string (including delimiters)
|
||||||
|
EndPosition uint // End position of the complete variable string (including delimiters)
|
||||||
|
IndentLevel uint // Indentation level of the variable
|
||||||
|
EscapedToken bool // Indicates if the variable was escaped with token escape character
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(opts Option) (*TemplateNest, error) {
|
||||||
|
// Check if the TemplateDir exists and is a directory.
|
||||||
|
templateDirInfo, err := os.Stat(opts.TemplateDir)
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return nil, fmt.Errorf("template dir `%s` does not exist", opts.TemplateDir)
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("error checking template dir: %w", err)
|
||||||
|
}
|
||||||
|
if !templateDirInfo.IsDir() {
|
||||||
|
return nil, fmt.Errorf("template dir `%s` is not a directory", opts.TemplateDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set defaults for options that the user hasn't provided.
|
||||||
|
if opts.Delimiters == [2]string{} {
|
||||||
|
opts.Delimiters = [2]string{"<!--%", "%-->"}
|
||||||
|
}
|
||||||
|
if opts.NameLabel == "" {
|
||||||
|
opts.NameLabel = "TEMPLATE"
|
||||||
|
}
|
||||||
|
if opts.TemplateExtension == "" {
|
||||||
|
opts.TemplateExtension = "html"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize TemplateNest with the final options.
|
||||||
|
nest := &TemplateNest{
|
||||||
|
option: opts,
|
||||||
|
cache: make(map[string]TemplateFileIndex),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Walk through the template directory and index the templates.
|
||||||
|
err = filepath.Walk(opts.TemplateDir, func(path string, info os.FileInfo, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the file has the correct extension.
|
||||||
|
if !strings.HasSuffix(info.Name(), "."+opts.TemplateExtension) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Index the template and store it in the cache.
|
||||||
|
templateIndex, err := nest.index(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the relative path of the file.
|
||||||
|
relPath, err := filepath.Rel(opts.TemplateDir, path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove the extension from the relative path.
|
||||||
|
templateName := strings.TrimSuffix(relPath, "."+opts.TemplateExtension)
|
||||||
|
nest.cache[templateName] = templateIndex
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nest, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (nest *TemplateNest) index(filePath string) (TemplateFileIndex, error) {
|
||||||
|
contents, err := ioutil.ReadFile(filePath)
|
||||||
|
if err != nil {
|
||||||
|
return TemplateFileIndex{}, fmt.Errorf("Error reading file (`%s`): %w", filePath, err)
|
||||||
|
}
|
||||||
|
// Capture last modified time
|
||||||
|
fileInfo, err := os.Stat(filePath)
|
||||||
|
if err != nil {
|
||||||
|
return TemplateFileIndex{}, fmt.Errorf("Error getting file (`%s`) info: %w", filePath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
variableNames := make(map[string]struct{})
|
||||||
|
variables := []TemplateFileVariable{}
|
||||||
|
|
||||||
|
delimiterStart := nest.option.Delimiters[0]
|
||||||
|
delimiterEnd := nest.option.Delimiters[1]
|
||||||
|
|
||||||
|
re := regexp.MustCompile(fmt.Sprintf(
|
||||||
|
"%s\\s*(.+?)\\s*%s", regexp.QuoteMeta(delimiterStart), regexp.QuoteMeta(delimiterEnd),
|
||||||
|
))
|
||||||
|
|
||||||
|
matches := re.FindAllStringSubmatchIndex(string(contents), -1)
|
||||||
|
for _, match := range matches {
|
||||||
|
if len(match) < 4 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
startIdx := match[0]
|
||||||
|
endIdx := match[1]
|
||||||
|
nameStartIdx := match[2]
|
||||||
|
nameEndIdx := match[3]
|
||||||
|
|
||||||
|
varName := string(contents[nameStartIdx:nameEndIdx])
|
||||||
|
variableNames[varName] = struct{}{}
|
||||||
|
|
||||||
|
variables = append(variables, TemplateFileVariable{
|
||||||
|
Name: varName,
|
||||||
|
StartPosition: uint(startIdx),
|
||||||
|
EndPosition: uint(endIdx),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fileIndex := TemplateFileIndex{
|
||||||
|
Contents: string(contents),
|
||||||
|
LastModified: fileInfo.ModTime(),
|
||||||
|
VariableNames: variableNames,
|
||||||
|
Variables: variables,
|
||||||
|
}
|
||||||
|
|
||||||
|
return fileIndex, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getTemplateFilePath takes a template name and returns the file path for the
|
||||||
|
// template.
|
||||||
|
func (nest *TemplateNest) getTemplateFilePath(templateName string) string {
|
||||||
|
return filepath.Join(
|
||||||
|
nest.option.TemplateDir,
|
||||||
|
fmt.Sprintf("%s.%s", templateName, nest.option.TemplateExtension),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (nest *TemplateNest) MustRender(toRender interface{}) string {
|
||||||
|
render, err := nest.Render(toRender)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return render
|
||||||
|
}
|
||||||
|
|
||||||
|
func (nest *TemplateNest) Render(toRender interface{}) (string, error) {
|
||||||
|
switch v := toRender.(type) {
|
||||||
|
case nil:
|
||||||
|
return "", nil
|
||||||
|
|
||||||
|
case bool:
|
||||||
|
return fmt.Sprintf("%t", v), nil
|
||||||
|
|
||||||
|
case string:
|
||||||
|
return v, nil
|
||||||
|
|
||||||
|
case float64, int, int64:
|
||||||
|
return fmt.Sprintf("%v", v), nil
|
||||||
|
|
||||||
|
case []Hash:
|
||||||
|
var rendered strings.Builder
|
||||||
|
for _, item := range v {
|
||||||
|
renderedItem, err := nest.Render(item)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
rendered.WriteString(renderedItem)
|
||||||
|
}
|
||||||
|
return rendered.String(), nil
|
||||||
|
|
||||||
|
case Hash:
|
||||||
|
tLabel, ok := v[nest.option.NameLabel]
|
||||||
|
if !ok {
|
||||||
|
return "", fmt.Errorf("missing template label: %s", nest.option.NameLabel)
|
||||||
|
}
|
||||||
|
|
||||||
|
tName, ok := tLabel.(string)
|
||||||
|
if !ok {
|
||||||
|
return "", fmt.Errorf("invalid template label: %+v", tLabel)
|
||||||
|
}
|
||||||
|
|
||||||
|
tFile := nest.getTemplateFilePath(tName)
|
||||||
|
fileInfo, err := os.Stat(tFile)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("error getting file info: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tIndex, exists := nest.cache[tName]
|
||||||
|
|
||||||
|
// If cache doesn't exist or has expired, re-index the file.
|
||||||
|
if !exists || fileInfo.ModTime().After(tIndex.LastModified) {
|
||||||
|
newIndex, err := nest.index(tFile)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("File index failed: %w", err)
|
||||||
|
}
|
||||||
|
tIndex = newIndex
|
||||||
|
}
|
||||||
|
|
||||||
|
rendered := tIndex.Contents
|
||||||
|
for i := len(tIndex.Variables) - 1; i >= 0; i-- {
|
||||||
|
variable := tIndex.Variables[i]
|
||||||
|
|
||||||
|
// If the variable doesn't exist in template hash then replace it
|
||||||
|
// with an empty string.
|
||||||
|
replacement := ""
|
||||||
|
|
||||||
|
if value, exists := v[variable.Name]; exists {
|
||||||
|
if text, ok := value.(string); ok {
|
||||||
|
replacement = text
|
||||||
|
} else {
|
||||||
|
subRender, err := nest.Render(value)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
replacement = subRender
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Replace in rendered template
|
||||||
|
rendered = rendered[:variable.StartPosition] + replacement + rendered[variable.EndPosition:]
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.TrimSpace(rendered), nil
|
||||||
|
|
||||||
|
default:
|
||||||
|
return "", fmt.Errorf("Unsupported template type: %+v", v)
|
||||||
|
}
|
||||||
|
}
|
@ -1,131 +0,0 @@
|
|||||||
package template_nest
|
|
||||||
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"log"
|
|
||||||
"regexp"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
type Nest struct {
|
|
||||||
|
|
||||||
templateLabel string
|
|
||||||
templateDir string
|
|
||||||
templates map[string]string
|
|
||||||
tokenDelims [2]string
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
func New( templates map[string]string ) Nest {
|
|
||||||
|
|
||||||
nest := Nest{
|
|
||||||
templateLabel: "TEMPLATE",
|
|
||||||
tokenDelims: [2]string{"<!--%", "%-->"},
|
|
||||||
templates: templates,
|
|
||||||
}
|
|
||||||
|
|
||||||
return nest
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
func (obj *Nest) Render ( sjson []byte ) string {
|
|
||||||
|
|
||||||
var nesti interface{}
|
|
||||||
err := json.Unmarshal(sjson, &nesti)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal("Failed to parse JSON")
|
|
||||||
}
|
|
||||||
|
|
||||||
nested := nesti.(map[string]interface{})
|
|
||||||
|
|
||||||
html := obj._Render( nested )
|
|
||||||
|
|
||||||
return html
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
func (obj *Nest) _Render( nested interface{} ) string{
|
|
||||||
|
|
||||||
var html string
|
|
||||||
|
|
||||||
switch vtype := nested.(type) {
|
|
||||||
|
|
||||||
case string:
|
|
||||||
html = vtype
|
|
||||||
|
|
||||||
case []interface{}:
|
|
||||||
|
|
||||||
html = obj._RenderArray( vtype )
|
|
||||||
|
|
||||||
case map[string]interface{}:
|
|
||||||
html = obj._RenderMap( vtype )
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
return html
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
func (obj *Nest) _RenderArray( nested []interface{} ) string {
|
|
||||||
|
|
||||||
html := ""
|
|
||||||
for v := range nested {
|
|
||||||
html += obj._Render( v )
|
|
||||||
}
|
|
||||||
|
|
||||||
return html
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
func (obj *Nest) _RenderMap( nested map[string]interface{} ) string {
|
|
||||||
|
|
||||||
templateName := nested[ obj.templateLabel ].(string)
|
|
||||||
if templateName == "" {
|
|
||||||
panic("Encountered map with no TEMPLATE label")
|
|
||||||
}
|
|
||||||
|
|
||||||
template := obj.templates[ templateName ]
|
|
||||||
|
|
||||||
params := make(map[string]string)
|
|
||||||
|
|
||||||
for k, v := range nested {
|
|
||||||
|
|
||||||
if k == obj.templateLabel {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
params[ k ] = obj._Render( v )
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
html := obj._FillIn( templateName, template, params )
|
|
||||||
|
|
||||||
return html
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
func (obj *Nest) _FillIn( templateName string, template string, params map[string]string ) string {
|
|
||||||
|
|
||||||
html := template
|
|
||||||
for param, val := range params {
|
|
||||||
|
|
||||||
regex, err := regexp.Compile( obj.tokenDelims[0] + `\s*` + param + `\s*` + obj.tokenDelims[1] )
|
|
||||||
if err != nil {
|
|
||||||
panic( err )
|
|
||||||
}
|
|
||||||
|
|
||||||
html = regex.ReplaceAllString( html, val )
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
return html
|
|
||||||
}
|
|
42
test_nest.go
42
test_nest.go
@ -1,42 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"tntester/template_nest"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main(){
|
|
||||||
|
|
||||||
templates := map[string]string{
|
|
||||||
"00-simple-page": `<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
||||||
<title>Simple Page</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<p>A fairly simple page to test the performance of Template::Nest.</p>
|
|
||||||
<p><!--% variable %--></p>
|
|
||||||
<!--% simple_component %-->
|
|
||||||
</body>
|
|
||||||
</html>`, "00-simple-component": `<p><!--% variable %--></p>`}
|
|
||||||
|
|
||||||
json := []byte(`{
|
|
||||||
"TEMPLATE": "00-simple-page",
|
|
||||||
"variable": "Simple Variable",
|
|
||||||
"simple_component": {
|
|
||||||
"TEMPLATE":"01-simple-component",
|
|
||||||
"variable": "Simple Variable in Simple Component"
|
|
||||||
}
|
|
||||||
}`)
|
|
||||||
|
|
||||||
|
|
||||||
nest := template_nest.New( templates )
|
|
||||||
|
|
||||||
html := nest.Render( json )
|
|
||||||
|
|
||||||
fmt.Println( html )
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
15
tests/00_basic_test.go
Normal file
15
tests/00_basic_test.go
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
package tests
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.virtual.blue/tomgracey/template-nest-go"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestInitialize(t *testing.T) {
|
||||||
|
_, err := templatenest.New(templatenest.Option{
|
||||||
|
TemplateDir: "templates",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to initialize TemplateNest: %+v", err)
|
||||||
|
}
|
||||||
|
}
|
145
tests/01_render_test.go
Normal file
145
tests/01_render_test.go
Normal file
@ -0,0 +1,145 @@
|
|||||||
|
package tests
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.virtual.blue/tomgracey/template-nest-go"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRenderSimplePage(t *testing.T) {
|
||||||
|
nest, err := templatenest.New(templatenest.Option{
|
||||||
|
TemplateDir: "templates",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to initialize TemplateNest: %+v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
page := templatenest.Hash{
|
||||||
|
"TEMPLATE": "00-simple-page",
|
||||||
|
"variable": "Simple Variable",
|
||||||
|
"simple_component": []templatenest.Hash{
|
||||||
|
templatenest.Hash{
|
||||||
|
"TEMPLATE": "01-simple-component",
|
||||||
|
"variable": "Simple Variable in Simple Component",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
outputPage := templatenest.Hash{"TEMPLATE": "output/01-simple-page"}
|
||||||
|
|
||||||
|
render, err := nest.Render(page)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Render failed for page: %+v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
outputRender := nest.MustRender(outputPage)
|
||||||
|
|
||||||
|
assert.Equal(t, outputRender, render, "Rendered output does not match expected output")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRenderIncompletePage(t *testing.T) {
|
||||||
|
nest, err := templatenest.New(templatenest.Option{TemplateDir: "templates"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to initialize TemplateNest: %+v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
page := templatenest.Hash{
|
||||||
|
"TEMPLATE": "00-simple-page",
|
||||||
|
"variable": "Simple Variable",
|
||||||
|
"simple_component": []templatenest.Hash{
|
||||||
|
templatenest.Hash{
|
||||||
|
"TEMPLATE": "01-simple-component",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
outputPage := templatenest.Hash{"TEMPLATE": "output/03-incomplete-page"}
|
||||||
|
|
||||||
|
render := nest.MustRender(page)
|
||||||
|
outputRender := nest.MustRender(outputPage)
|
||||||
|
|
||||||
|
assert.Equal(t, outputRender, render, "Rendered output does not match expected output")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRenderComplexPage(t *testing.T) {
|
||||||
|
nest, err := templatenest.New(templatenest.Option{TemplateDir: "templates"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to initialize TemplateNest: %+v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
page := templatenest.Hash{
|
||||||
|
"TEMPLATE": "10-complex-page",
|
||||||
|
"title": "Complex Page",
|
||||||
|
"pre_body": templatenest.Hash{
|
||||||
|
"TEMPLATE": "18-styles",
|
||||||
|
},
|
||||||
|
"navigation": templatenest.Hash{
|
||||||
|
"TEMPLATE": "11-navigation",
|
||||||
|
"banner": templatenest.Hash{
|
||||||
|
"TEMPLATE": "12-navigation-banner",
|
||||||
|
},
|
||||||
|
"items": []templatenest.Hash{
|
||||||
|
templatenest.Hash{"TEMPLATE": "13-navigation-item-00-services"},
|
||||||
|
templatenest.Hash{"TEMPLATE": "13-navigation-item-01-resources"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"hero_section": templatenest.Hash{
|
||||||
|
"TEMPLATE": "14-hero-section",
|
||||||
|
},
|
||||||
|
"main_content": []templatenest.Hash{
|
||||||
|
templatenest.Hash{"TEMPLATE": "15-isdc-card"},
|
||||||
|
templatenest.Hash{
|
||||||
|
"TEMPLATE": "16-vb-brand-cards",
|
||||||
|
"cards": []templatenest.Hash{
|
||||||
|
templatenest.Hash{
|
||||||
|
"TEMPLATE": "17-vb-brand-card-00",
|
||||||
|
"parent_classes": "p-card brand-card col-4",
|
||||||
|
},
|
||||||
|
templatenest.Hash{
|
||||||
|
"TEMPLATE": "17-vb-brand-card-01",
|
||||||
|
"parent_classes": "p-card brand-card col-4",
|
||||||
|
},
|
||||||
|
templatenest.Hash{
|
||||||
|
"TEMPLATE": "17-vb-brand-card-02",
|
||||||
|
"parent_classes": "p-card brand-card col-4",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"post_footer": templatenest.Hash{
|
||||||
|
"TEMPLATE": "19-scripts",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
outputPage := templatenest.Hash{"TEMPLATE": "output/02-complex-page"}
|
||||||
|
|
||||||
|
render := nest.MustRender(page)
|
||||||
|
outputRender := nest.MustRender(outputPage)
|
||||||
|
|
||||||
|
assert.Equal(t, outputRender, render, "Rendered output does not match expected output")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRenderArrayOfTemplateHash(t *testing.T) {
|
||||||
|
nest, err := templatenest.New(templatenest.Option{TemplateDir: "templates"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to initialize TemplateNest: %+v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
page := []templatenest.Hash{
|
||||||
|
templatenest.Hash{
|
||||||
|
"TEMPLATE": "01-simple-component",
|
||||||
|
"variable": "This is a variable",
|
||||||
|
},
|
||||||
|
templatenest.Hash{
|
||||||
|
"TEMPLATE": "01-simple-component",
|
||||||
|
"variable": "This is another variable",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
outputPage := templatenest.Hash{"TEMPLATE": "output/13-render-with-array-of-template-hash"}
|
||||||
|
|
||||||
|
render := nest.MustRender(page)
|
||||||
|
outputRender := nest.MustRender(outputPage)
|
||||||
|
|
||||||
|
assert.Equal(t, outputRender, render, "Rendered output does not match expected output")
|
||||||
|
}
|
13
tests/templates/00-simple-page-alt-delim.html
Normal file
13
tests/templates/00-simple-page-alt-delim.html
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>Simple Page</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<p>A fairly simple page to test the performance of Template::Nest.</p>
|
||||||
|
<p><% variable %></p>
|
||||||
|
<% simple_component %>
|
||||||
|
</body>
|
||||||
|
</html>
|
13
tests/templates/00-simple-page.html
Normal file
13
tests/templates/00-simple-page.html
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>Simple Page</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<p>A fairly simple page to test the performance of Template::Nest.</p>
|
||||||
|
<p><!--% variable %--></p>
|
||||||
|
<!--% simple_component %-->
|
||||||
|
</body>
|
||||||
|
</html>
|
1
tests/templates/01-simple-component-alt-delim.html
Normal file
1
tests/templates/01-simple-component-alt-delim.html
Normal file
@ -0,0 +1 @@
|
|||||||
|
<p><% variable %></p>
|
1
tests/templates/01-simple-component-token-escape.html
Normal file
1
tests/templates/01-simple-component-token-escape.html
Normal file
@ -0,0 +1 @@
|
|||||||
|
<p>\<!--% variable %--></p>
|
1
tests/templates/01-simple-component.html
Normal file
1
tests/templates/01-simple-component.html
Normal file
@ -0,0 +1 @@
|
|||||||
|
<p><!--% variable %--></p>
|
@ -0,0 +1,7 @@
|
|||||||
|
<p>
|
||||||
|
This is a simple component on multiple lines.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
This is used for fixed-indent testing.
|
||||||
|
</p>
|
7
tests/templates/02-simple-component-multi-line.html
Normal file
7
tests/templates/02-simple-component-multi-line.html
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<p>
|
||||||
|
This is a simple component on multiple lines.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
This is used for fixed-indent testing.
|
||||||
|
</p>
|
12
tests/templates/03-namespace-page.html
Normal file
12
tests/templates/03-namespace-page.html
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>Simple Page</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<p>A fairly simple page to test the performance of Template::Nest.</p>
|
||||||
|
<p><!--% space.inside %--></p>
|
||||||
|
</body>
|
||||||
|
</html>
|
1
tests/templates/03-var-at-begin.html
Normal file
1
tests/templates/03-var-at-begin.html
Normal file
@ -0,0 +1 @@
|
|||||||
|
<!--% variable %-->
|
29
tests/templates/10-complex-page.html
Normal file
29
tests/templates/10-complex-page.html
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||||
|
<title><!--% title %--></title>
|
||||||
|
<link rel="stylesheet" href="/03-resources/style.css" />
|
||||||
|
<!--% pre_body %-->
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header id="navigation" class="p-navigation is-dark">
|
||||||
|
<!--% navigation %-->
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<section class="p-strip--light is-bordered hero-section">
|
||||||
|
<!--% hero_section %-->
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<main class="row">
|
||||||
|
<!--% main_content %-->
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
<h2>virtual.blue</h2>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<!--% post_footer %-->
|
||||||
|
</body>
|
||||||
|
</html>
|
20
tests/templates/11-navigation.html
Normal file
20
tests/templates/11-navigation.html
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
<div class="p-navigation__row">
|
||||||
|
<!--% banner %-->
|
||||||
|
<nav class="p-navigation__nav" aria-label="Example sub navigation">
|
||||||
|
<ul class="p-navigation__items">
|
||||||
|
<!--% items %-->
|
||||||
|
</ul>
|
||||||
|
<ul class="p-navigation__items">
|
||||||
|
<li class="p-navigation__item--dropdown-toggle" id="link-4">
|
||||||
|
<a class="p-navigation__link" aria-controls="account-menu">
|
||||||
|
My account
|
||||||
|
</a>
|
||||||
|
<ul class="p-navigation__dropdown--right" id="account-menu" aria-hidden="true">
|
||||||
|
<li>
|
||||||
|
<a href="#" class="p-navigation__dropdown-item">Sign out</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</div>
|
11
tests/templates/12-navigation-banner.html
Normal file
11
tests/templates/12-navigation-banner.html
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<div class="p-navigation__banner">
|
||||||
|
<div class="p-navigation__tagged-logo">
|
||||||
|
<a class="p-navigation__link" href="#">
|
||||||
|
<span class="p-navigation__logo-title">
|
||||||
|
<img class="nav-vb-logo" src="https://virtual.blue/resources/img/vb-small-dark-bg.png" alt="virtual.blue logo">
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<a href="#navigation" class="p-navigation__toggle--open" title="menu">Menu</a>
|
||||||
|
<a href="#navigation-closed" class="p-navigation__toggle--close" title="close menu">Close menu</a>
|
||||||
|
</div>
|
17
tests/templates/13-navigation-item-00-services.html
Normal file
17
tests/templates/13-navigation-item-00-services.html
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<li class="p-navigation__item--dropdown-toggle" id="link-2">
|
||||||
|
<a href="#link-2-menu" aria-controls="link-2-menu" class="p-navigation__link">Services</a>
|
||||||
|
<ul class="p-navigation__dropdown" id="link-2-menu" aria-hidden="true">
|
||||||
|
<li>
|
||||||
|
<a href="#" class="p-navigation__dropdown-item">Web & Mobile Apps</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="#" class="p-navigation__dropdown-item">Legacy Code</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="#" class="p-navigation__dropdown-item">Migrations</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="#" class="p-navigation__dropdown-item">Replacement Systems</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
14
tests/templates/13-navigation-item-01-resources.html
Normal file
14
tests/templates/13-navigation-item-01-resources.html
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<li class="p-navigation__item--dropdown-toggle" id="link-3">
|
||||||
|
<a href="#link-3-menu" aria-controls="link-3-menu" class="p-navigation__link">Resources</a>
|
||||||
|
<ul class="p-navigation__dropdown" id="link-3-menu" aria-hidden="true">
|
||||||
|
<li>
|
||||||
|
<a href="#" class="p-navigation__dropdown-item">Point Drag Controls</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="#" class="p-navigation__dropdown-item">Delay Proxy</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="#" class="p-navigation__dropdown-item">TaskPipe</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
4
tests/templates/14-hero-section.html
Normal file
4
tests/templates/14-hero-section.html
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
<div class="row">
|
||||||
|
<h1>Software Systems</h1>
|
||||||
|
<p>Quality web and app specialists at low cost. New app development. Web-scrapers and crawlers. Full systems. Legacy repairs.</p>
|
||||||
|
</div>
|
14
tests/templates/15-isdc-card.html
Normal file
14
tests/templates/15-isdc-card.html
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<div class="p-card">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-4">
|
||||||
|
<img src="https://virtual.blue/resources/img/isdc-zoom.png">
|
||||||
|
</div>
|
||||||
|
<div class="col-8">
|
||||||
|
<h3>Raku Prodigy ISDC</h3>
|
||||||
|
<p class="p-card__content">
|
||||||
|
Are you a rising coding talent with a flair for innovative system development?
|
||||||
|
Make a name for yourself by winning our <a href="https://virtual.blue/isdc">web development competition</a>.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
3
tests/templates/16-vb-brand-cards.html
Normal file
3
tests/templates/16-vb-brand-cards.html
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<div class="row brand-cards-wrapper">
|
||||||
|
<!--% cards %-->
|
||||||
|
</div>
|
9
tests/templates/17-vb-brand-card-00.html
Normal file
9
tests/templates/17-vb-brand-card-00.html
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<div class="<!--% parent_classes %-->">
|
||||||
|
<div class="p-card__content">
|
||||||
|
<img class="p-card__image" alt="" src="https://virtual.blue/resources/img/home/Cost%20Minimising.svg">
|
||||||
|
<h4>
|
||||||
|
Cost Minimising
|
||||||
|
</h4>
|
||||||
|
<p class="u-no-padding--bottom">Through our efficient approach to project management we are able to offer some of the lowest service rates in the industry - without compromising on quality.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
9
tests/templates/17-vb-brand-card-01.html
Normal file
9
tests/templates/17-vb-brand-card-01.html
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<div class="<!--% parent_classes %-->">
|
||||||
|
<div class="p-card__content">
|
||||||
|
<img class="p-card__image" alt="" src="https://virtual.blue/resources/img/home/Code%20Warranty.svg">
|
||||||
|
<h4>
|
||||||
|
Code Warranty
|
||||||
|
</h4>
|
||||||
|
<p class="u-no-padding--bottom">As standard we continue to provide support for 6 months after you have accepted the code. This includes explaining usage, performing minor adjustments and ironing out any bugs. Subject to contract terms.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
9
tests/templates/17-vb-brand-card-02.html
Normal file
9
tests/templates/17-vb-brand-card-02.html
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<div class="<!--% parent_classes %-->">
|
||||||
|
<div class="p-card__content">
|
||||||
|
<img class="p-card__image" alt="" src="https://virtual.blue/resources/img/home/Tailor%20Made%20Solutions.svg">
|
||||||
|
<h4>
|
||||||
|
Tailor Made Solutions
|
||||||
|
</h4>
|
||||||
|
<p class="u-no-padding--bottom">When evaluating your case we'll consider your individual requirements, your specific business needs and the particular problem you are facing - so we'll always propose solutions which are uniquely tailored to your situation.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
20
tests/templates/18-styles.html
Normal file
20
tests/templates/18-styles.html
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
<style>
|
||||||
|
.nav-vb-logo {
|
||||||
|
max-width: 200px;
|
||||||
|
}
|
||||||
|
.hero-section {
|
||||||
|
background-image: url('/03-resources/bg-header.jpg');
|
||||||
|
background-size: cover;
|
||||||
|
}
|
||||||
|
main {
|
||||||
|
margin-top: 1em;
|
||||||
|
}
|
||||||
|
main .brand-card img {
|
||||||
|
max-width: 50%;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
footer {
|
||||||
|
text-align: center;
|
||||||
|
padding: 4em;
|
||||||
|
}
|
||||||
|
</style>
|
49
tests/templates/19-scripts.html
Normal file
49
tests/templates/19-scripts.html
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
<script>
|
||||||
|
function toggleDropdown(toggle, open) {
|
||||||
|
let parentElement = toggle.parentNode;
|
||||||
|
let dropdown = document.getElementById(toggle.getAttribute('aria-controls'));
|
||||||
|
dropdown.setAttribute('aria-hidden', !open);
|
||||||
|
|
||||||
|
if (open) {
|
||||||
|
parentElement.classList.add('is-active');
|
||||||
|
} else {
|
||||||
|
parentElement.classList.remove('is-active');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeAllDropdowns(toggles) {
|
||||||
|
toggles.forEach(function (toggle) {
|
||||||
|
toggleDropdown(toggle, false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleClickOutside(toggles, containerClass) {
|
||||||
|
document.addEventListener('click', function (event) {
|
||||||
|
let target = event.target;
|
||||||
|
|
||||||
|
if (target.closest) {
|
||||||
|
if (!target.closest(containerClass)) {
|
||||||
|
closeAllDropdowns(toggles);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function initNavDropdowns(containerClass) {
|
||||||
|
let toggles = [].slice.call(document.querySelectorAll(containerClass + ' [aria-controls]'));
|
||||||
|
|
||||||
|
handleClickOutside(toggles, containerClass);
|
||||||
|
|
||||||
|
toggles.forEach(function (toggle) {
|
||||||
|
toggle.addEventListener('click', function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const shouldOpen = !toggle.parentNode.classList.contains('is-active');
|
||||||
|
closeAllDropdowns(toggles);
|
||||||
|
toggleDropdown(toggle, shouldOpen);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
initNavDropdowns('.p-navigation__item--dropdown-toggle');
|
||||||
|
</script>
|
3
tests/templates/30-main.js
Normal file
3
tests/templates/30-main.js
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
const tmp = () => {
|
||||||
|
/* <!--% var %--> */
|
||||||
|
};
|
13
tests/templates/output/01-simple-page.html
Normal file
13
tests/templates/output/01-simple-page.html
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>Simple Page</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<p>A fairly simple page to test the performance of Template::Nest.</p>
|
||||||
|
<p>Simple Variable</p>
|
||||||
|
<p>Simple Variable in Simple Component</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
196
tests/templates/output/02-complex-page.html
Normal file
196
tests/templates/output/02-complex-page.html
Normal file
@ -0,0 +1,196 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||||
|
<title>Complex Page</title>
|
||||||
|
<link rel="stylesheet" href="/03-resources/style.css" />
|
||||||
|
<style>
|
||||||
|
.nav-vb-logo {
|
||||||
|
max-width: 200px;
|
||||||
|
}
|
||||||
|
.hero-section {
|
||||||
|
background-image: url('/03-resources/bg-header.jpg');
|
||||||
|
background-size: cover;
|
||||||
|
}
|
||||||
|
main {
|
||||||
|
margin-top: 1em;
|
||||||
|
}
|
||||||
|
main .brand-card img {
|
||||||
|
max-width: 50%;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
footer {
|
||||||
|
text-align: center;
|
||||||
|
padding: 4em;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header id="navigation" class="p-navigation is-dark">
|
||||||
|
<div class="p-navigation__row">
|
||||||
|
<div class="p-navigation__banner">
|
||||||
|
<div class="p-navigation__tagged-logo">
|
||||||
|
<a class="p-navigation__link" href="#">
|
||||||
|
<span class="p-navigation__logo-title">
|
||||||
|
<img class="nav-vb-logo" src="https://virtual.blue/resources/img/vb-small-dark-bg.png" alt="virtual.blue logo">
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<a href="#navigation" class="p-navigation__toggle--open" title="menu">Menu</a>
|
||||||
|
<a href="#navigation-closed" class="p-navigation__toggle--close" title="close menu">Close menu</a>
|
||||||
|
</div>
|
||||||
|
<nav class="p-navigation__nav" aria-label="Example sub navigation">
|
||||||
|
<ul class="p-navigation__items">
|
||||||
|
<li class="p-navigation__item--dropdown-toggle" id="link-2">
|
||||||
|
<a href="#link-2-menu" aria-controls="link-2-menu" class="p-navigation__link">Services</a>
|
||||||
|
<ul class="p-navigation__dropdown" id="link-2-menu" aria-hidden="true">
|
||||||
|
<li>
|
||||||
|
<a href="#" class="p-navigation__dropdown-item">Web & Mobile Apps</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="#" class="p-navigation__dropdown-item">Legacy Code</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="#" class="p-navigation__dropdown-item">Migrations</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="#" class="p-navigation__dropdown-item">Replacement Systems</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li><li class="p-navigation__item--dropdown-toggle" id="link-3">
|
||||||
|
<a href="#link-3-menu" aria-controls="link-3-menu" class="p-navigation__link">Resources</a>
|
||||||
|
<ul class="p-navigation__dropdown" id="link-3-menu" aria-hidden="true">
|
||||||
|
<li>
|
||||||
|
<a href="#" class="p-navigation__dropdown-item">Point Drag Controls</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="#" class="p-navigation__dropdown-item">Delay Proxy</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="#" class="p-navigation__dropdown-item">TaskPipe</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<ul class="p-navigation__items">
|
||||||
|
<li class="p-navigation__item--dropdown-toggle" id="link-4">
|
||||||
|
<a class="p-navigation__link" aria-controls="account-menu">
|
||||||
|
My account
|
||||||
|
</a>
|
||||||
|
<ul class="p-navigation__dropdown--right" id="account-menu" aria-hidden="true">
|
||||||
|
<li>
|
||||||
|
<a href="#" class="p-navigation__dropdown-item">Sign out</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<section class="p-strip--light is-bordered hero-section">
|
||||||
|
<div class="row">
|
||||||
|
<h1>Software Systems</h1>
|
||||||
|
<p>Quality web and app specialists at low cost. New app development. Web-scrapers and crawlers. Full systems. Legacy repairs.</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<main class="row">
|
||||||
|
<div class="p-card">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-4">
|
||||||
|
<img src="https://virtual.blue/resources/img/isdc-zoom.png">
|
||||||
|
</div>
|
||||||
|
<div class="col-8">
|
||||||
|
<h3>Raku Prodigy ISDC</h3>
|
||||||
|
<p class="p-card__content">
|
||||||
|
Are you a rising coding talent with a flair for innovative system development?
|
||||||
|
Make a name for yourself by winning our <a href="https://virtual.blue/isdc">web development competition</a>.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div><div class="row brand-cards-wrapper">
|
||||||
|
<div class="p-card brand-card col-4">
|
||||||
|
<div class="p-card__content">
|
||||||
|
<img class="p-card__image" alt="" src="https://virtual.blue/resources/img/home/Cost%20Minimising.svg">
|
||||||
|
<h4>
|
||||||
|
Cost Minimising
|
||||||
|
</h4>
|
||||||
|
<p class="u-no-padding--bottom">Through our efficient approach to project management we are able to offer some of the lowest service rates in the industry - without compromising on quality.</p>
|
||||||
|
</div>
|
||||||
|
</div><div class="p-card brand-card col-4">
|
||||||
|
<div class="p-card__content">
|
||||||
|
<img class="p-card__image" alt="" src="https://virtual.blue/resources/img/home/Code%20Warranty.svg">
|
||||||
|
<h4>
|
||||||
|
Code Warranty
|
||||||
|
</h4>
|
||||||
|
<p class="u-no-padding--bottom">As standard we continue to provide support for 6 months after you have accepted the code. This includes explaining usage, performing minor adjustments and ironing out any bugs. Subject to contract terms.</p>
|
||||||
|
</div>
|
||||||
|
</div><div class="p-card brand-card col-4">
|
||||||
|
<div class="p-card__content">
|
||||||
|
<img class="p-card__image" alt="" src="https://virtual.blue/resources/img/home/Tailor%20Made%20Solutions.svg">
|
||||||
|
<h4>
|
||||||
|
Tailor Made Solutions
|
||||||
|
</h4>
|
||||||
|
<p class="u-no-padding--bottom">When evaluating your case we'll consider your individual requirements, your specific business needs and the particular problem you are facing - so we'll always propose solutions which are uniquely tailored to your situation.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
<h2>virtual.blue</h2>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function toggleDropdown(toggle, open) {
|
||||||
|
let parentElement = toggle.parentNode;
|
||||||
|
let dropdown = document.getElementById(toggle.getAttribute('aria-controls'));
|
||||||
|
dropdown.setAttribute('aria-hidden', !open);
|
||||||
|
|
||||||
|
if (open) {
|
||||||
|
parentElement.classList.add('is-active');
|
||||||
|
} else {
|
||||||
|
parentElement.classList.remove('is-active');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeAllDropdowns(toggles) {
|
||||||
|
toggles.forEach(function (toggle) {
|
||||||
|
toggleDropdown(toggle, false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleClickOutside(toggles, containerClass) {
|
||||||
|
document.addEventListener('click', function (event) {
|
||||||
|
let target = event.target;
|
||||||
|
|
||||||
|
if (target.closest) {
|
||||||
|
if (!target.closest(containerClass)) {
|
||||||
|
closeAllDropdowns(toggles);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function initNavDropdowns(containerClass) {
|
||||||
|
let toggles = [].slice.call(document.querySelectorAll(containerClass + ' [aria-controls]'));
|
||||||
|
|
||||||
|
handleClickOutside(toggles, containerClass);
|
||||||
|
|
||||||
|
toggles.forEach(function (toggle) {
|
||||||
|
toggle.addEventListener('click', function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const shouldOpen = !toggle.parentNode.classList.contains('is-active');
|
||||||
|
closeAllDropdowns(toggles);
|
||||||
|
toggleDropdown(toggle, shouldOpen);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
initNavDropdowns('.p-navigation__item--dropdown-toggle');
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
13
tests/templates/output/03-incomplete-page.html
Normal file
13
tests/templates/output/03-incomplete-page.html
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>Simple Page</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<p>A fairly simple page to test the performance of Template::Nest.</p>
|
||||||
|
<p>Simple Variable</p>
|
||||||
|
<p></p>
|
||||||
|
</body>
|
||||||
|
</html>
|
17
tests/templates/output/04-simple-page-with-labels.html
Normal file
17
tests/templates/output/04-simple-page-with-labels.html
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<!-- BEGIN 00-simple-page -->
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>Simple Page</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<p>A fairly simple page to test the performance of Template::Nest.</p>
|
||||||
|
<p>Simple Variable</p>
|
||||||
|
<!-- BEGIN 01-simple-component -->
|
||||||
|
<p>Simple Variable in Simple Component</p>
|
||||||
|
<!-- END 01-simple-component -->
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
<!-- END 00-simple-page -->
|
@ -0,0 +1,17 @@
|
|||||||
|
<!--! BEGIN 00-simple-page !-->
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>Simple Page</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<p>A fairly simple page to test the performance of Template::Nest.</p>
|
||||||
|
<p>Simple Variable</p>
|
||||||
|
<!--! BEGIN 01-simple-component !-->
|
||||||
|
<p>Simple Variable in Simple Component</p>
|
||||||
|
<!--! END 01-simple-component !-->
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
<!--! END 00-simple-page !-->
|
3
tests/templates/output/06-main-template-extension.js
Normal file
3
tests/templates/output/06-main-template-extension.js
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
const tmp = () => {
|
||||||
|
/* Simple Variable */
|
||||||
|
};
|
19
tests/templates/output/07-simple-page-fixed-indent.html
Normal file
19
tests/templates/output/07-simple-page-fixed-indent.html
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>Simple Page</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<p>A fairly simple page to test the performance of Template::Nest.</p>
|
||||||
|
<p>Simple Variable</p>
|
||||||
|
<p>
|
||||||
|
This is a simple component on multiple lines.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
This is used for fixed-indent testing.
|
||||||
|
</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
196
tests/templates/output/08-complex-page-fixed-indent.html
Normal file
196
tests/templates/output/08-complex-page-fixed-indent.html
Normal file
@ -0,0 +1,196 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||||
|
<title>Complex Page</title>
|
||||||
|
<link rel="stylesheet" href="/03-resources/style.css" />
|
||||||
|
<style>
|
||||||
|
.nav-vb-logo {
|
||||||
|
max-width: 200px;
|
||||||
|
}
|
||||||
|
.hero-section {
|
||||||
|
background-image: url('/03-resources/bg-header.jpg');
|
||||||
|
background-size: cover;
|
||||||
|
}
|
||||||
|
main {
|
||||||
|
margin-top: 1em;
|
||||||
|
}
|
||||||
|
main .brand-card img {
|
||||||
|
max-width: 50%;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
footer {
|
||||||
|
text-align: center;
|
||||||
|
padding: 4em;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header id="navigation" class="p-navigation is-dark">
|
||||||
|
<div class="p-navigation__row">
|
||||||
|
<div class="p-navigation__banner">
|
||||||
|
<div class="p-navigation__tagged-logo">
|
||||||
|
<a class="p-navigation__link" href="#">
|
||||||
|
<span class="p-navigation__logo-title">
|
||||||
|
<img class="nav-vb-logo" src="https://virtual.blue/resources/img/vb-small-dark-bg.png" alt="virtual.blue logo">
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<a href="#navigation" class="p-navigation__toggle--open" title="menu">Menu</a>
|
||||||
|
<a href="#navigation-closed" class="p-navigation__toggle--close" title="close menu">Close menu</a>
|
||||||
|
</div>
|
||||||
|
<nav class="p-navigation__nav" aria-label="Example sub navigation">
|
||||||
|
<ul class="p-navigation__items">
|
||||||
|
<li class="p-navigation__item--dropdown-toggle" id="link-2">
|
||||||
|
<a href="#link-2-menu" aria-controls="link-2-menu" class="p-navigation__link">Services</a>
|
||||||
|
<ul class="p-navigation__dropdown" id="link-2-menu" aria-hidden="true">
|
||||||
|
<li>
|
||||||
|
<a href="#" class="p-navigation__dropdown-item">Web & Mobile Apps</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="#" class="p-navigation__dropdown-item">Legacy Code</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="#" class="p-navigation__dropdown-item">Migrations</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="#" class="p-navigation__dropdown-item">Replacement Systems</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li><li class="p-navigation__item--dropdown-toggle" id="link-3">
|
||||||
|
<a href="#link-3-menu" aria-controls="link-3-menu" class="p-navigation__link">Resources</a>
|
||||||
|
<ul class="p-navigation__dropdown" id="link-3-menu" aria-hidden="true">
|
||||||
|
<li>
|
||||||
|
<a href="#" class="p-navigation__dropdown-item">Point Drag Controls</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="#" class="p-navigation__dropdown-item">Delay Proxy</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="#" class="p-navigation__dropdown-item">TaskPipe</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<ul class="p-navigation__items">
|
||||||
|
<li class="p-navigation__item--dropdown-toggle" id="link-4">
|
||||||
|
<a class="p-navigation__link" aria-controls="account-menu">
|
||||||
|
My account
|
||||||
|
</a>
|
||||||
|
<ul class="p-navigation__dropdown--right" id="account-menu" aria-hidden="true">
|
||||||
|
<li>
|
||||||
|
<a href="#" class="p-navigation__dropdown-item">Sign out</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<section class="p-strip--light is-bordered hero-section">
|
||||||
|
<div class="row">
|
||||||
|
<h1>Software Systems</h1>
|
||||||
|
<p>Quality web and app specialists at low cost. New app development. Web-scrapers and crawlers. Full systems. Legacy repairs.</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<main class="row">
|
||||||
|
<div class="p-card">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-4">
|
||||||
|
<img src="https://virtual.blue/resources/img/isdc-zoom.png">
|
||||||
|
</div>
|
||||||
|
<div class="col-8">
|
||||||
|
<h3>Raku Prodigy ISDC</h3>
|
||||||
|
<p class="p-card__content">
|
||||||
|
Are you a rising coding talent with a flair for innovative system development?
|
||||||
|
Make a name for yourself by winning our <a href="https://virtual.blue/isdc">web development competition</a>.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div><div class="row brand-cards-wrapper">
|
||||||
|
<div class="p-card brand-card col-4">
|
||||||
|
<div class="p-card__content">
|
||||||
|
<img class="p-card__image" alt="" src="https://virtual.blue/resources/img/home/Cost%20Minimising.svg">
|
||||||
|
<h4>
|
||||||
|
Cost Minimising
|
||||||
|
</h4>
|
||||||
|
<p class="u-no-padding--bottom">Through our efficient approach to project management we are able to offer some of the lowest service rates in the industry - without compromising on quality.</p>
|
||||||
|
</div>
|
||||||
|
</div><div class="p-card brand-card col-4">
|
||||||
|
<div class="p-card__content">
|
||||||
|
<img class="p-card__image" alt="" src="https://virtual.blue/resources/img/home/Code%20Warranty.svg">
|
||||||
|
<h4>
|
||||||
|
Code Warranty
|
||||||
|
</h4>
|
||||||
|
<p class="u-no-padding--bottom">As standard we continue to provide support for 6 months after you have accepted the code. This includes explaining usage, performing minor adjustments and ironing out any bugs. Subject to contract terms.</p>
|
||||||
|
</div>
|
||||||
|
</div><div class="p-card brand-card col-4">
|
||||||
|
<div class="p-card__content">
|
||||||
|
<img class="p-card__image" alt="" src="https://virtual.blue/resources/img/home/Tailor%20Made%20Solutions.svg">
|
||||||
|
<h4>
|
||||||
|
Tailor Made Solutions
|
||||||
|
</h4>
|
||||||
|
<p class="u-no-padding--bottom">When evaluating your case we'll consider your individual requirements, your specific business needs and the particular problem you are facing - so we'll always propose solutions which are uniquely tailored to your situation.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
<h2>virtual.blue</h2>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function toggleDropdown(toggle, open) {
|
||||||
|
let parentElement = toggle.parentNode;
|
||||||
|
let dropdown = document.getElementById(toggle.getAttribute('aria-controls'));
|
||||||
|
dropdown.setAttribute('aria-hidden', !open);
|
||||||
|
|
||||||
|
if (open) {
|
||||||
|
parentElement.classList.add('is-active');
|
||||||
|
} else {
|
||||||
|
parentElement.classList.remove('is-active');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeAllDropdowns(toggles) {
|
||||||
|
toggles.forEach(function (toggle) {
|
||||||
|
toggleDropdown(toggle, false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleClickOutside(toggles, containerClass) {
|
||||||
|
document.addEventListener('click', function (event) {
|
||||||
|
let target = event.target;
|
||||||
|
|
||||||
|
if (target.closest) {
|
||||||
|
if (!target.closest(containerClass)) {
|
||||||
|
closeAllDropdowns(toggles);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function initNavDropdowns(containerClass) {
|
||||||
|
let toggles = [].slice.call(document.querySelectorAll(containerClass + ' [aria-controls]'));
|
||||||
|
|
||||||
|
handleClickOutside(toggles, containerClass);
|
||||||
|
|
||||||
|
toggles.forEach(function (toggle) {
|
||||||
|
toggle.addEventListener('click', function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const shouldOpen = !toggle.parentNode.classList.contains('is-active');
|
||||||
|
closeAllDropdowns(toggles);
|
||||||
|
toggleDropdown(toggle, shouldOpen);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
initNavDropdowns('.p-navigation__item--dropdown-toggle');
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
13
tests/templates/output/09-simple-page-token-escape.html
Normal file
13
tests/templates/output/09-simple-page-token-escape.html
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>Simple Page</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<p>A fairly simple page to test the performance of Template::Nest.</p>
|
||||||
|
<p>Simple Variable</p>
|
||||||
|
<p><!--% variable %--></p>
|
||||||
|
</body>
|
||||||
|
</html>
|
1
tests/templates/output/10-var-at-begin.html
Normal file
1
tests/templates/output/10-var-at-begin.html
Normal file
@ -0,0 +1 @@
|
|||||||
|
Simple Variable
|
12
tests/templates/output/11-namespace-page.html
Normal file
12
tests/templates/output/11-namespace-page.html
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>Simple Page</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<p>A fairly simple page to test the performance of Template::Nest.</p>
|
||||||
|
<p>A variable inside a space.</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
13
tests/templates/output/12-simple-page-arrays.html
Normal file
13
tests/templates/output/12-simple-page-arrays.html
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>Simple Page</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<p>A fairly simple page to test the performance of Template::Nest.</p>
|
||||||
|
<p>Simple Variable</p>
|
||||||
|
<p>Simple Variable in Simple Component</p><strong>Another test</strong><p>Simple Variable in Simple Component</p><strong>Another nested test 2</strong>
|
||||||
|
</body>
|
||||||
|
</html>
|
@ -0,0 +1 @@
|
|||||||
|
<p>This is a variable</p><p>This is another variable</p>
|
Loading…
Reference in New Issue
Block a user