primate 0.0.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +27 -0
- package/README.md +50 -0
- package/package.json +15 -1
- package/source/client/Action.js +159 -0
- package/source/client/App.js +16 -0
- package/source/client/Base.js +5 -0
- package/source/client/Client.js +65 -0
- package/source/client/Context.js +53 -0
- package/source/client/Element.js +245 -0
- package/source/client/Node.js +13 -0
- package/source/client/View.js +90 -0
- package/source/client/document.js +6 -0
- package/source/client/exports.js +15 -0
- package/source/preset/client/Element.js +2 -0
- package/source/preset/client/app.js +2 -0
- package/source/preset/data/stores/default.js +2 -0
- package/source/preset/primate.json +31 -0
- package/source/preset/static/index.html +10 -0
- package/source/server/Action.js +118 -0
- package/source/server/App.js +42 -0
- package/source/server/Base.js +35 -0
- package/source/server/Bundler.js +180 -0
- package/source/server/Context.js +90 -0
- package/source/server/Crypto.js +8 -0
- package/source/server/Directory.js +35 -0
- package/source/server/File.js +117 -0
- package/source/server/Router.js +61 -0
- package/source/server/Session.js +69 -0
- package/source/server/attributes.js +11 -0
- package/source/server/cache.js +17 -0
- package/source/server/conf.js +33 -0
- package/source/server/domain/Domain.js +277 -0
- package/source/server/domain/Field.js +115 -0
- package/source/server/domain/domains.js +15 -0
- package/source/server/errors/Fallback.js +1 -0
- package/source/server/errors/InternalServer.js +1 -0
- package/source/server/errors/Predicate.js +1 -0
- package/source/server/errors.js +3 -0
- package/source/server/exports.js +27 -0
- package/source/server/fallback.js +11 -0
- package/source/server/invariants.js +19 -0
- package/source/server/log.js +22 -0
- package/source/server/promises/Eager.js +49 -0
- package/source/server/promises/Meta.js +42 -0
- package/source/server/promises.js +2 -0
- package/source/server/servers/Dynamic.js +51 -0
- package/source/server/servers/Server.js +5 -0
- package/source/server/servers/Static.js +113 -0
- package/source/server/servers/content-security-policy.json +8 -0
- package/source/server/servers/http-codes.json +5 -0
- package/source/server/servers/mimes.json +12 -0
- package/source/server/store/Memory.js +60 -0
- package/source/server/store/Store.js +17 -0
- package/source/server/types/Array.js +33 -0
- package/source/server/types/Boolean.js +29 -0
- package/source/server/types/Date.js +20 -0
- package/source/server/types/Domain.js +16 -0
- package/source/server/types/File.js +20 -0
- package/source/server/types/Instance.js +8 -0
- package/source/server/types/Number.js +45 -0
- package/source/server/types/Object.js +12 -0
- package/source/server/types/Primitive.js +7 -0
- package/source/server/types/Storeable.js +51 -0
- package/source/server/types/String.js +49 -0
- package/source/server/types/errors/Array.json +7 -0
- package/source/server/types/errors/Boolean.json +5 -0
- package/source/server/types/errors/Date.json +3 -0
- package/source/server/types/errors/Number.json +9 -0
- package/source/server/types/errors/Object.json +3 -0
- package/source/server/types/errors/String.json +11 -0
- package/source/server/types.js +7 -0
- package/source/server/utils/extend_object.js +10 -0
- package/source/server/view/Parser.js +122 -0
- package/source/server/view/TreeNode.js +195 -0
- package/source/server/view/View.js +30 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import {PredicateError} from "../errors.js";
|
|
2
|
+
|
|
3
|
+
export default class {
|
|
4
|
+
static async verify(property, value, predicates, type) {
|
|
5
|
+
const coerced = this.coerce(value);
|
|
6
|
+
if (!await this.is(coerced, type)) {
|
|
7
|
+
throw new PredicateError(this.type_error(type));
|
|
8
|
+
}
|
|
9
|
+
await this.has(property, coerced, predicates);
|
|
10
|
+
return coerced;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
static type_error() {
|
|
14
|
+
return this.errors.type;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
static is() {
|
|
18
|
+
throw new Error("must be implemented");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
static async has(property, value, predicates) {
|
|
22
|
+
for (const predicate of predicates) {
|
|
23
|
+
if (typeof predicate === "object") {
|
|
24
|
+
await predicate.function(property, ...predicate.params);
|
|
25
|
+
} else {
|
|
26
|
+
const [name, ...params] = predicate.split(":");
|
|
27
|
+
if (!this[name](value, ...params)) {
|
|
28
|
+
let error = this.errors[name];
|
|
29
|
+
for (let i = 0; i < params.length; i++) {
|
|
30
|
+
error = error.replace(`$${i+1}`, params[i]);
|
|
31
|
+
}
|
|
32
|
+
throw new PredicateError(error);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
static coerce(value) {
|
|
39
|
+
return value;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// noop for builtin types
|
|
43
|
+
static serialize(value) {
|
|
44
|
+
return value;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// noop for builtin types
|
|
48
|
+
static deserialize(value) {
|
|
49
|
+
return value;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import Primitive from "./Primitive.js";
|
|
2
|
+
import errors from "./errors/String.json" assert {"type": "json"};
|
|
3
|
+
|
|
4
|
+
export default class extends Primitive {
|
|
5
|
+
static get type() {
|
|
6
|
+
return "string";
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
static get instance() {
|
|
10
|
+
return String;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
static get errors() {
|
|
14
|
+
return errors;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
static length(value, length) {
|
|
18
|
+
return value.length === Number(length);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
static min(value, minimum) {
|
|
22
|
+
return value.length >= Number(minimum);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
static max(value, maximum) {
|
|
26
|
+
return value.length <= Number(maximum);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
static between(value, minimum, maximum) {
|
|
30
|
+
const length = value.length;
|
|
31
|
+
return length >= Number(minimum) && length <= Number(maximum);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
static lowercase(value) {
|
|
35
|
+
return value === value.toLowerCase();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
static uppercase(value) {
|
|
39
|
+
return value === value.toUpperCase();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
static alphanumeric(value) {
|
|
43
|
+
return /^[a-z0-9]+$/iu.test(value);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
static regex(value, pattern) {
|
|
47
|
+
return new RegExp(pattern, "u").test(value);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"type": "Must be a string",
|
|
3
|
+
"length": "Must be $1 characters in length",
|
|
4
|
+
"min": "Must be at least $1 characters in length",
|
|
5
|
+
"max": "Must be at most $1 characters in length",
|
|
6
|
+
"between": "Must be between $1 and $2 characters in length",
|
|
7
|
+
"lowercase": "Must be lowercase",
|
|
8
|
+
"uppercase": "Must be uppercase",
|
|
9
|
+
"alphanumeric": "Must be alphanumeric",
|
|
10
|
+
"regex": "Must adhere to the regular expression pattern '$1'"
|
|
11
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export {default as ArrayType} from "./types/Array.js";
|
|
2
|
+
export {default as BooleanType} from "./types/Boolean.js";
|
|
3
|
+
export {default as DateType} from "./types/Date.js";
|
|
4
|
+
export {default as NumberType} from "./types/Number.js";
|
|
5
|
+
export {default as ObjectType} from "./types/Object.js";
|
|
6
|
+
export {default as StringType} from "./types/String.js";
|
|
7
|
+
export {default as FileType} from "./types/File.js";
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
const extend_object = (base = {}, extension = {}) =>
|
|
2
|
+
Object.keys(extension).reduce((result, property) => {
|
|
3
|
+
const value = extension[property];
|
|
4
|
+
result[property] = value?.constructor === Object
|
|
5
|
+
? extend_object(base[property], value)
|
|
6
|
+
: value;
|
|
7
|
+
return result;
|
|
8
|
+
}, base);
|
|
9
|
+
|
|
10
|
+
export default extend_object;
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import TreeNode from "./TreeNode.js";
|
|
2
|
+
|
|
3
|
+
const OPEN_TAG = "open_tag";
|
|
4
|
+
const CLOSE_TAG = "close_tag";
|
|
5
|
+
const OPEN_AND_CLOSE_TAG = "open_and_close_tag";
|
|
6
|
+
const last_index = -1;
|
|
7
|
+
|
|
8
|
+
export default class Parser {
|
|
9
|
+
constructor(html) {
|
|
10
|
+
this.html = html;
|
|
11
|
+
this.result = [];
|
|
12
|
+
this.index = 0;
|
|
13
|
+
this.buffer = "";
|
|
14
|
+
this.balance = 0;
|
|
15
|
+
this.reading_tag = false;
|
|
16
|
+
this.node = new TreeNode();
|
|
17
|
+
this.tree = this.node;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
at(index) {
|
|
21
|
+
return this.html[index];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
get previous() {
|
|
25
|
+
return this.at(this.index+last_index);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
get current() {
|
|
29
|
+
return this.at(this.index);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
get next() {
|
|
33
|
+
return this.at(this.index+1);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
remove_whitespace() {
|
|
37
|
+
this.html = this.html.replace(/[\n\t\r]/gu, "");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
open_tag() {
|
|
41
|
+
this.node = new TreeNode(this.node, this.buffer);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
close_tag() {
|
|
45
|
+
if (this.node.parent !== undefined) {
|
|
46
|
+
this.node = this.node.parent;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
open_and_close_tag() {
|
|
51
|
+
this.open_tag();
|
|
52
|
+
this.close_tag();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// currently inside tag
|
|
56
|
+
process_reading_tag() {
|
|
57
|
+
if (this.current === ">") {
|
|
58
|
+
// mark as outside tag
|
|
59
|
+
this.reading_tag = false;
|
|
60
|
+
// if the previous character is '/', it's an open and close tag
|
|
61
|
+
if (this.previous === "/") {
|
|
62
|
+
this.tag = OPEN_AND_CLOSE_TAG;
|
|
63
|
+
this.balance--;
|
|
64
|
+
this.buffer = this.buffer.slice(0, last_index);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// execute the function associated with this kind of tag
|
|
68
|
+
this[this.tag]();
|
|
69
|
+
// empty buffer
|
|
70
|
+
this.buffer = "";
|
|
71
|
+
} else {
|
|
72
|
+
this.buffer += this.current;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// currently outside of a tag
|
|
77
|
+
process_not_reading_tag() {
|
|
78
|
+
// encountered '<'
|
|
79
|
+
if (this.current === "<") {
|
|
80
|
+
// mark as inside tag
|
|
81
|
+
this.reading_tag = true;
|
|
82
|
+
if (this.next === "/") {
|
|
83
|
+
// next character is slash, this is a close tag
|
|
84
|
+
this.tag = CLOSE_TAG;
|
|
85
|
+
this.balance--;
|
|
86
|
+
} else {
|
|
87
|
+
// this is an open tag (or open-and-close)
|
|
88
|
+
this.tag = OPEN_TAG;
|
|
89
|
+
this.balance++;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
read() {
|
|
95
|
+
if (this.reading_tag) {
|
|
96
|
+
this.process_reading_tag();
|
|
97
|
+
} else {
|
|
98
|
+
this.process_not_reading_tag();
|
|
99
|
+
}
|
|
100
|
+
this.index++;
|
|
101
|
+
|
|
102
|
+
return this.current !== undefined;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return_checked() {
|
|
106
|
+
if (this.balance !== 0) {
|
|
107
|
+
throw Error(`unbalanced DOM tree: ${this.balance}`);
|
|
108
|
+
}
|
|
109
|
+
return this.tree;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// TODO: ignore comments in the analysed html
|
|
113
|
+
parse() {
|
|
114
|
+
this.remove_whitespace();
|
|
115
|
+
do {} while (this.read());
|
|
116
|
+
return this.return_checked();
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
static parse(html) {
|
|
120
|
+
return new Parser(html).parse();
|
|
121
|
+
}
|
|
122
|
+
}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
const data_regex = /\${([^}]*)}/g;
|
|
2
|
+
|
|
3
|
+
export default class TreeNode {
|
|
4
|
+
constructor(parent, content) {
|
|
5
|
+
this.children = [];
|
|
6
|
+
this.content = content;
|
|
7
|
+
if (parent !== undefined) {
|
|
8
|
+
this.parent = parent;
|
|
9
|
+
this.parent.attach(this);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
attach(child) {
|
|
14
|
+
this.children.push(child);
|
|
15
|
+
child.parent = this;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// splices the child at the position i and hoists its children to i...
|
|
19
|
+
splice(i) {
|
|
20
|
+
if (this.children[i] !== undefined) {
|
|
21
|
+
const children = this.children[i].children;
|
|
22
|
+
for (const child of children) {
|
|
23
|
+
child.parent = this;
|
|
24
|
+
}
|
|
25
|
+
this.children.splice(i, 1, ...children);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
filter(predicate) {
|
|
30
|
+
for (let i = this.children.length-1; i >= 0; i--) {
|
|
31
|
+
this.children[i].filter(predicate);
|
|
32
|
+
predicate(this.children[i].content) && this.splice(i);
|
|
33
|
+
}
|
|
34
|
+
return this;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
each(operation) {
|
|
38
|
+
if (this.content !== undefined) {
|
|
39
|
+
operation(this);
|
|
40
|
+
}
|
|
41
|
+
for (let i = 0; i < this.children.length; i++) {
|
|
42
|
+
this.children[i].each(operation);
|
|
43
|
+
}
|
|
44
|
+
return this;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
transform() {
|
|
48
|
+
const tree = [];
|
|
49
|
+
let flatten = [];
|
|
50
|
+
|
|
51
|
+
this.filter(content => !content.includes("data-"))
|
|
52
|
+
.each(node => {
|
|
53
|
+
const data_tags = node.content.split("data-");
|
|
54
|
+
data_tags.shift();
|
|
55
|
+
node.content = data_tags
|
|
56
|
+
.filter(part => part.includes("="))
|
|
57
|
+
.map(part => {
|
|
58
|
+
const index = part.indexOf("=");
|
|
59
|
+
const key = part.slice(0, index);
|
|
60
|
+
const right = part.slice(index+1);
|
|
61
|
+
return {key, "value": right.slice(1, right.indexOf("\"", 1))};
|
|
62
|
+
});
|
|
63
|
+
})
|
|
64
|
+
// transform template strings
|
|
65
|
+
.each(node => {
|
|
66
|
+
for (let i = 0; i < node.content.length; i++) {
|
|
67
|
+
const part = node.content[i];
|
|
68
|
+
const key = part.key;
|
|
69
|
+
const matches = [...part.value.matchAll(data_regex)];
|
|
70
|
+
if (matches.length > 0) {
|
|
71
|
+
for (const match of matches) {
|
|
72
|
+
// add new entries to node.content
|
|
73
|
+
node.content.push({key, "value": match[1]});
|
|
74
|
+
}
|
|
75
|
+
// remove this entry
|
|
76
|
+
node.content.splice(i, 1);
|
|
77
|
+
i--;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
})
|
|
81
|
+
// unfold scopes
|
|
82
|
+
.each(node => {
|
|
83
|
+
for (let i = 0; i < node.content.length; i++) {
|
|
84
|
+
const part = node.content[i];
|
|
85
|
+
// memove scope and rework subtree
|
|
86
|
+
if (part.key === "scope") {
|
|
87
|
+
const scope = part.value;
|
|
88
|
+
for (let j = 0; j < node.children.length; j++) {
|
|
89
|
+
let recurse = true;
|
|
90
|
+
node.children[j].each(scopeable => {
|
|
91
|
+
// skip descending into children of data-scope
|
|
92
|
+
if (!recurse) {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
let found_scope = false;
|
|
96
|
+
scopeable.content.forEach(content => {
|
|
97
|
+
if (content.key === "scope") {
|
|
98
|
+
found_scope = true;
|
|
99
|
+
}
|
|
100
|
+
content.value = `${scope}.${content.value}`;
|
|
101
|
+
});
|
|
102
|
+
recurse = !found_scope;
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
node.content.splice(i, 1);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
})
|
|
109
|
+
.each(node => {
|
|
110
|
+
flatten = flatten.concat(node.content.map(content => content.value));
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
const error = key => { throw new Error(
|
|
114
|
+
`\`${key}\` appears as both value and object key`); };
|
|
115
|
+
const put = (tree, part) => {
|
|
116
|
+
if (!part.includes(".")) {
|
|
117
|
+
if (!tree.includes(part)) {
|
|
118
|
+
tree.push(part);
|
|
119
|
+
}
|
|
120
|
+
for (const node of tree) {
|
|
121
|
+
if (typeof node === "object") {
|
|
122
|
+
if (node[part] !== undefined) {
|
|
123
|
+
error(part);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
} else {
|
|
128
|
+
const index = part.indexOf(".");
|
|
129
|
+
const left = part.slice(0, index);
|
|
130
|
+
const right = part.slice(index+1);
|
|
131
|
+
if (tree.includes(left)) {
|
|
132
|
+
error(left);
|
|
133
|
+
}
|
|
134
|
+
let found_node = undefined;
|
|
135
|
+
for (const node of tree) {
|
|
136
|
+
if (typeof node === "object") {
|
|
137
|
+
if (node[left] !== undefined) {
|
|
138
|
+
found_node = node[left];
|
|
139
|
+
break;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
if (found_node === undefined) {
|
|
144
|
+
found_node = [];
|
|
145
|
+
tree.push({[left]: found_node});
|
|
146
|
+
}
|
|
147
|
+
put(found_node, right);
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
flatten.forEach(part => put(tree, part));
|
|
151
|
+
return tree;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
flatten() {
|
|
155
|
+
const object = {};
|
|
156
|
+
if (this.children.length === 0) {
|
|
157
|
+
return this.content.key;
|
|
158
|
+
}
|
|
159
|
+
const key = this.content ? this.content.key : "root";
|
|
160
|
+
object[key] = [];
|
|
161
|
+
for (const child of this.children) {
|
|
162
|
+
if (child !== undefined) {
|
|
163
|
+
const interim = child.flatten();
|
|
164
|
+
if (interim instanceof Array) {
|
|
165
|
+
object[key] = object[key].concat(interim);
|
|
166
|
+
} else {
|
|
167
|
+
object[key].push(interim);
|
|
168
|
+
}
|
|
169
|
+
const strings = [];
|
|
170
|
+
const objects = {};
|
|
171
|
+
object[key] = object[key].filter(value => {
|
|
172
|
+
if (typeof value === "string") {
|
|
173
|
+
if (strings.includes(value)) {
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
strings.push(value);
|
|
177
|
+
return true;
|
|
178
|
+
} else {
|
|
179
|
+
const key = Object.keys(value)[0];
|
|
180
|
+
value = value[key];
|
|
181
|
+
// merge with it
|
|
182
|
+
if (objects[key] !== undefined) {
|
|
183
|
+
objects[key].push(...value);
|
|
184
|
+
return false;
|
|
185
|
+
} else {
|
|
186
|
+
objects[key] = value;
|
|
187
|
+
return true;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return object;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import Parser from "./Parser.js";
|
|
2
|
+
import {InternalServerError} from "../errors.js";
|
|
3
|
+
|
|
4
|
+
const $content = "${content}";
|
|
5
|
+
|
|
6
|
+
export default class View {
|
|
7
|
+
constructor(path, content, layouts) {
|
|
8
|
+
this.path = path;
|
|
9
|
+
this.content = content;
|
|
10
|
+
this.layouts = layouts;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
elements(layout) {
|
|
14
|
+
try {
|
|
15
|
+
return Parser.parse(layout === undefined
|
|
16
|
+
? this.content
|
|
17
|
+
: this.file(layout)).transform();
|
|
18
|
+
} catch (error) {
|
|
19
|
+
throw new InternalServerError(`${this.path} ${error.message}`);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
layout(layout) {
|
|
24
|
+
return this.layouts[layout] ?? $content;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
file(layout = "default") {
|
|
28
|
+
return this.layout(layout).replace($content, this.content);
|
|
29
|
+
}
|
|
30
|
+
}
|