ventojs 0.10.2 → 0.11.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/esm/mod.js CHANGED
@@ -13,6 +13,7 @@ import exportTag from "./plugins/export.js";
13
13
  import echoTag from "./plugins/echo.js";
14
14
  import escape from "./plugins/escape.js";
15
15
  import unescape from "./plugins/unescape.js";
16
+ import trim from "./plugins/trim.js";
16
17
  export default function (options = {}) {
17
18
  const loader = typeof options.includes === "object"
18
19
  ? options.includes
@@ -36,5 +37,6 @@ export default function (options = {}) {
36
37
  env.use(echoTag());
37
38
  env.use(escape());
38
39
  env.use(unescape());
40
+ env.use(trim());
39
41
  return env;
40
42
  }
@@ -0,0 +1,8 @@
1
+ import type { Token } from "../src/tokenizer.js";
2
+ import type { Environment } from "../src/environment.js";
3
+ export declare const defaultTags: string[];
4
+ export type AutoTrimOptions = {
5
+ tags: string[];
6
+ };
7
+ export default function (options?: AutoTrimOptions): (env: Environment) => void;
8
+ export declare function autoTrim(tokens: Token[], options: AutoTrimOptions): void;
@@ -0,0 +1,36 @@
1
+ export const defaultTags = [
2
+ ">",
3
+ "#",
4
+ "set",
5
+ "/set",
6
+ "if",
7
+ "/if",
8
+ "else",
9
+ "for",
10
+ "/for",
11
+ "function",
12
+ "async",
13
+ "/function",
14
+ "export",
15
+ "/export",
16
+ "import",
17
+ ];
18
+ export default function (options = { tags: defaultTags }) {
19
+ return (env) => {
20
+ env.tokenPreprocessors.push((_, tokens) => autoTrim(tokens, options));
21
+ };
22
+ }
23
+ export function autoTrim(tokens, options) {
24
+ for (let i = 0; i < tokens.length; i++) {
25
+ const previous = tokens[i - 1];
26
+ const token = tokens[i];
27
+ const next = tokens[i + 1];
28
+ const [type, code] = token;
29
+ if (type === "tag" && options.tags.find((tag) => code.startsWith(tag))) {
30
+ // Remove leading horizontal space
31
+ previous[1] = previous[1].replace(/[ \t]*$/, "");
32
+ // Remove trailing horizontal space + newline
33
+ next[1] = next[1].replace(/^[ \t]*(?:\r\n|\n)/, "");
34
+ }
35
+ }
36
+ }
@@ -1,6 +1,7 @@
1
1
  import { html } from "../deps.js";
2
2
  export default function () {
3
3
  return (env) => {
4
- env.filters.escape = html.escape;
4
+ // deno-lint-ignore no-explicit-any
5
+ env.filters.escape = (value) => value ? html.escape(value.toString()) : "";
5
6
  };
6
7
  }
@@ -0,0 +1,4 @@
1
+ import type { Token } from "../src/tokenizer.js";
2
+ import type { Environment } from "../src/environment.js";
3
+ export default function (): (env: Environment) => void;
4
+ export declare function trim(_: Environment, tokens: Token[]): void;
@@ -0,0 +1,28 @@
1
+ export default function () {
2
+ return (env) => {
3
+ env.tokenPreprocessors.push(trim);
4
+ };
5
+ }
6
+ export function trim(_, tokens) {
7
+ for (let i = 0; i < tokens.length; i++) {
8
+ const previous = tokens[i - 1];
9
+ const token = tokens[i];
10
+ const next = tokens[i + 1];
11
+ let [type, code] = token;
12
+ if (type === "tag" && code.startsWith("-")) {
13
+ previous[1] = previous[1].trimEnd();
14
+ code = code.slice(1);
15
+ }
16
+ if (type === "tag" && code.endsWith("-")) {
17
+ next[1] = next[1].trimStart();
18
+ code = code.slice(0, -1);
19
+ }
20
+ // Trim tag and filter code
21
+ switch (type) {
22
+ case "tag":
23
+ case "filter":
24
+ token[1] = code.trim();
25
+ break;
26
+ }
27
+ }
28
+ }
@@ -1,6 +1,7 @@
1
1
  import { html } from "../deps.js";
2
2
  export default function () {
3
3
  return (env) => {
4
- env.filters.unescape = html.unescape;
4
+ // deno-lint-ignore no-explicit-any
5
+ env.filters.unescape = (value) => value ? html.unescape(value.toString()) : "";
5
6
  };
6
7
  }
@@ -16,6 +16,7 @@ export interface TemplateSync {
16
16
  code: string;
17
17
  file?: string;
18
18
  }
19
+ export type TokenPreprocessor = (env: Environment, tokens: Token[], path?: string) => Token[] | void;
19
20
  export type Tag = (env: Environment, code: string, output: string, tokens: Token[]) => string | undefined;
20
21
  export type FilterThis = {
21
22
  data: Record<string, unknown>;
@@ -33,6 +34,7 @@ export declare class Environment {
33
34
  cache: Map<string, Template>;
34
35
  options: Options;
35
36
  tags: Tag[];
37
+ tokenPreprocessors: TokenPreprocessor[];
36
38
  filters: Record<string, Filter>;
37
39
  utils: Record<string, unknown>;
38
40
  constructor(options: Options);
@@ -42,6 +44,7 @@ export declare class Environment {
42
44
  runStringSync(source: string, data?: Record<string, unknown>): TemplateResult;
43
45
  compile(source: string, path?: string, defaults?: Record<string, unknown>, sync?: false): Template;
44
46
  compile(source: string, path?: string, defaults?: Record<string, unknown>, sync?: true): TemplateSync;
47
+ tokenize(source: string, path?: string): Token[];
45
48
  load(file: string, from?: string): Promise<Template>;
46
49
  compileTokens(tokens: Token[], outputVar?: string, stopAt?: string[]): string[];
47
50
  compileFilters(tokens: Token[], output: string, autoescape?: boolean): string;
@@ -4,6 +4,7 @@ export class Environment {
4
4
  cache = new Map();
5
5
  options;
6
6
  tags = [];
7
+ tokenPreprocessors = [];
7
8
  filters = {};
8
9
  utils = {};
9
10
  constructor(options) {
@@ -34,10 +35,7 @@ export class Environment {
34
35
  return template(data);
35
36
  }
36
37
  compile(source, path, defaults, sync = false) {
37
- const { tokens, position, error } = tokenize(source);
38
- if (error) {
39
- throw this.createError(path || "unknown", source, position, error);
40
- }
38
+ const tokens = this.tokenize(source, path);
41
39
  const code = this.compileTokens(tokens).join("\n");
42
40
  const { dataVarname, useWith } = this.options;
43
41
  const constructor = new Function("__file", "__env", "__defaults", `return${sync ? "" : " async"} function (${dataVarname}) {
@@ -59,10 +57,29 @@ export class Environment {
59
57
  template.source = source;
60
58
  return template;
61
59
  }
60
+ tokenize(source, path) {
61
+ const result = tokenize(source);
62
+ let { tokens } = result;
63
+ const { position, error } = result;
64
+ if (error) {
65
+ throw this.createError(path || "unknown", source, position, error);
66
+ }
67
+ for (const tokenPreprocessor of this.tokenPreprocessors) {
68
+ const result = tokenPreprocessor(this, tokens, path);
69
+ if (result !== undefined) {
70
+ tokens = result;
71
+ }
72
+ }
73
+ return tokens;
74
+ }
62
75
  async load(file, from) {
63
76
  const path = from ? this.options.loader.resolve(from, file) : file;
64
77
  if (!this.cache.has(path)) {
65
- const { source, data } = await this.options.loader.load(path);
78
+ // Remove query and hash params from path before loading
79
+ const cleanPath = path
80
+ .split("?")[0]
81
+ .split("#")[0];
82
+ const { source, data } = await this.options.loader.load(cleanPath);
66
83
  const template = this.compile(source, path, data);
67
84
  this.cache.set(path, template);
68
85
  }
@@ -1,20 +1,13 @@
1
1
  export default function tokenize(source) {
2
2
  const tokens = [];
3
3
  let type = "string";
4
- let trimNext = false;
5
4
  let position = 0;
6
5
  try {
7
6
  while (source.length > 0) {
8
7
  if (type === "string") {
9
8
  const index = source.indexOf("{{");
10
9
  const code = index === -1 ? source : source.slice(0, index);
11
- if (trimNext) {
12
- tokens.push([type, code.trimStart(), position]);
13
- trimNext = false;
14
- }
15
- else {
16
- tokens.push([type, code, position]);
17
- }
10
+ tokens.push([type, code, position]);
18
11
  if (index === -1) {
19
12
  break;
20
13
  }
@@ -41,39 +34,23 @@ export default function tokenize(source) {
41
34
  const lastIndex = indexes.length - 1;
42
35
  let tag;
43
36
  indexes.reduce((prev, curr, index) => {
44
- let code = source.slice(prev, curr - 2);
37
+ const code = source.slice(prev, curr - 2);
45
38
  // Tag
46
39
  if (index === 1) {
47
- // Left trim
48
- if (code.startsWith("-")) {
49
- code = code.slice(1);
50
- const lastToken = tokens[tokens.length - 1];
51
- lastToken[1] = lastToken[1].trimEnd();
52
- }
53
- // Right trim
54
- if (code.endsWith("-") && index === lastIndex) {
55
- code = code.slice(0, -1);
56
- trimNext = true;
57
- }
58
- tag = [type, code.trim(), position];
40
+ tag = [type, code, position];
59
41
  tokens.push(tag);
60
42
  return curr;
61
43
  }
62
- // Right trim
63
- if (index === lastIndex && code.endsWith("-")) {
64
- code = code.slice(0, -1);
65
- trimNext = true;
66
- }
67
44
  // Filters
68
- tokens.push(["filter", code.trim()]);
45
+ tokens.push(["filter", code]);
69
46
  return curr;
70
47
  });
71
48
  position += indexes[lastIndex];
72
49
  source = source.slice(indexes[lastIndex]);
73
50
  type = "string";
74
51
  // Search the closing echo tag {{ /echo }}
75
- if (tag?.[1] === "echo") {
76
- const end = source.match(/{{\s*\/echo\s*}}/);
52
+ if (tag?.[1].match(/^\-?\s*echo\s*\-?$/)) {
53
+ const end = source.match(/{{\-?\s*\/echo\s*\-?}}/);
77
54
  if (!end) {
78
55
  throw new Error("Unclosed echo tag");
79
56
  }
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "module": "./esm/mod.js",
3
3
  "main": "./script/mod.js",
4
4
  "name": "ventojs",
5
- "version": "0.10.2",
5
+ "version": "0.11.0",
6
6
  "description": "🌬 A minimal but powerful template engine",
7
7
  "license": "MIT",
8
8
  "repository": "github:oscarotero/vento",
@@ -12,6 +12,10 @@
12
12
  ".": {
13
13
  "import": "./esm/mod.js",
14
14
  "require": "./script/mod.js"
15
+ },
16
+ "./plugins/auto_trim.js": {
17
+ "import": "./esm/plugins/auto_trim.js",
18
+ "require": "./script/plugins/auto_trim.js"
15
19
  }
16
20
  },
17
21
  "scripts": {
package/script/mod.js CHANGED
@@ -41,6 +41,7 @@ const export_js_1 = __importDefault(require("./plugins/export.js"));
41
41
  const echo_js_1 = __importDefault(require("./plugins/echo.js"));
42
42
  const escape_js_1 = __importDefault(require("./plugins/escape.js"));
43
43
  const unescape_js_1 = __importDefault(require("./plugins/unescape.js"));
44
+ const trim_js_1 = __importDefault(require("./plugins/trim.js"));
44
45
  function default_1(options = {}) {
45
46
  const loader = typeof options.includes === "object"
46
47
  ? options.includes
@@ -64,6 +65,7 @@ function default_1(options = {}) {
64
65
  env.use((0, echo_js_1.default)());
65
66
  env.use((0, escape_js_1.default)());
66
67
  env.use((0, unescape_js_1.default)());
68
+ env.use((0, trim_js_1.default)());
67
69
  return env;
68
70
  }
69
71
  exports.default = default_1;
@@ -0,0 +1,8 @@
1
+ import type { Token } from "../src/tokenizer.js";
2
+ import type { Environment } from "../src/environment.js";
3
+ export declare const defaultTags: string[];
4
+ export type AutoTrimOptions = {
5
+ tags: string[];
6
+ };
7
+ export default function (options?: AutoTrimOptions): (env: Environment) => void;
8
+ export declare function autoTrim(tokens: Token[], options: AutoTrimOptions): void;
@@ -0,0 +1,41 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.autoTrim = exports.defaultTags = void 0;
4
+ exports.defaultTags = [
5
+ ">",
6
+ "#",
7
+ "set",
8
+ "/set",
9
+ "if",
10
+ "/if",
11
+ "else",
12
+ "for",
13
+ "/for",
14
+ "function",
15
+ "async",
16
+ "/function",
17
+ "export",
18
+ "/export",
19
+ "import",
20
+ ];
21
+ function default_1(options = { tags: exports.defaultTags }) {
22
+ return (env) => {
23
+ env.tokenPreprocessors.push((_, tokens) => autoTrim(tokens, options));
24
+ };
25
+ }
26
+ exports.default = default_1;
27
+ function autoTrim(tokens, options) {
28
+ for (let i = 0; i < tokens.length; i++) {
29
+ const previous = tokens[i - 1];
30
+ const token = tokens[i];
31
+ const next = tokens[i + 1];
32
+ const [type, code] = token;
33
+ if (type === "tag" && options.tags.find((tag) => code.startsWith(tag))) {
34
+ // Remove leading horizontal space
35
+ previous[1] = previous[1].replace(/[ \t]*$/, "");
36
+ // Remove trailing horizontal space + newline
37
+ next[1] = next[1].replace(/^[ \t]*(?:\r\n|\n)/, "");
38
+ }
39
+ }
40
+ }
41
+ exports.autoTrim = autoTrim;
@@ -3,7 +3,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const deps_js_1 = require("../deps.js");
4
4
  function default_1() {
5
5
  return (env) => {
6
- env.filters.escape = deps_js_1.html.escape;
6
+ // deno-lint-ignore no-explicit-any
7
+ env.filters.escape = (value) => value ? deps_js_1.html.escape(value.toString()) : "";
7
8
  };
8
9
  }
9
10
  exports.default = default_1;
@@ -0,0 +1,4 @@
1
+ import type { Token } from "../src/tokenizer.js";
2
+ import type { Environment } from "../src/environment.js";
3
+ export default function (): (env: Environment) => void;
4
+ export declare function trim(_: Environment, tokens: Token[]): void;
@@ -0,0 +1,33 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.trim = void 0;
4
+ function default_1() {
5
+ return (env) => {
6
+ env.tokenPreprocessors.push(trim);
7
+ };
8
+ }
9
+ exports.default = default_1;
10
+ function trim(_, tokens) {
11
+ for (let i = 0; i < tokens.length; i++) {
12
+ const previous = tokens[i - 1];
13
+ const token = tokens[i];
14
+ const next = tokens[i + 1];
15
+ let [type, code] = token;
16
+ if (type === "tag" && code.startsWith("-")) {
17
+ previous[1] = previous[1].trimEnd();
18
+ code = code.slice(1);
19
+ }
20
+ if (type === "tag" && code.endsWith("-")) {
21
+ next[1] = next[1].trimStart();
22
+ code = code.slice(0, -1);
23
+ }
24
+ // Trim tag and filter code
25
+ switch (type) {
26
+ case "tag":
27
+ case "filter":
28
+ token[1] = code.trim();
29
+ break;
30
+ }
31
+ }
32
+ }
33
+ exports.trim = trim;
@@ -3,7 +3,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const deps_js_1 = require("../deps.js");
4
4
  function default_1() {
5
5
  return (env) => {
6
- env.filters.unescape = deps_js_1.html.unescape;
6
+ // deno-lint-ignore no-explicit-any
7
+ env.filters.unescape = (value) => value ? deps_js_1.html.unescape(value.toString()) : "";
7
8
  };
8
9
  }
9
10
  exports.default = default_1;
@@ -16,6 +16,7 @@ export interface TemplateSync {
16
16
  code: string;
17
17
  file?: string;
18
18
  }
19
+ export type TokenPreprocessor = (env: Environment, tokens: Token[], path?: string) => Token[] | void;
19
20
  export type Tag = (env: Environment, code: string, output: string, tokens: Token[]) => string | undefined;
20
21
  export type FilterThis = {
21
22
  data: Record<string, unknown>;
@@ -33,6 +34,7 @@ export declare class Environment {
33
34
  cache: Map<string, Template>;
34
35
  options: Options;
35
36
  tags: Tag[];
37
+ tokenPreprocessors: TokenPreprocessor[];
36
38
  filters: Record<string, Filter>;
37
39
  utils: Record<string, unknown>;
38
40
  constructor(options: Options);
@@ -42,6 +44,7 @@ export declare class Environment {
42
44
  runStringSync(source: string, data?: Record<string, unknown>): TemplateResult;
43
45
  compile(source: string, path?: string, defaults?: Record<string, unknown>, sync?: false): Template;
44
46
  compile(source: string, path?: string, defaults?: Record<string, unknown>, sync?: true): TemplateSync;
47
+ tokenize(source: string, path?: string): Token[];
45
48
  load(file: string, from?: string): Promise<Template>;
46
49
  compileTokens(tokens: Token[], outputVar?: string, stopAt?: string[]): string[];
47
50
  compileFilters(tokens: Token[], output: string, autoescape?: boolean): string;
@@ -33,6 +33,7 @@ class Environment {
33
33
  cache = new Map();
34
34
  options;
35
35
  tags = [];
36
+ tokenPreprocessors = [];
36
37
  filters = {};
37
38
  utils = {};
38
39
  constructor(options) {
@@ -63,10 +64,7 @@ class Environment {
63
64
  return template(data);
64
65
  }
65
66
  compile(source, path, defaults, sync = false) {
66
- const { tokens, position, error } = (0, tokenizer_js_1.default)(source);
67
- if (error) {
68
- throw this.createError(path || "unknown", source, position, error);
69
- }
67
+ const tokens = this.tokenize(source, path);
70
68
  const code = this.compileTokens(tokens).join("\n");
71
69
  const { dataVarname, useWith } = this.options;
72
70
  const constructor = new Function("__file", "__env", "__defaults", `return${sync ? "" : " async"} function (${dataVarname}) {
@@ -88,10 +86,29 @@ class Environment {
88
86
  template.source = source;
89
87
  return template;
90
88
  }
89
+ tokenize(source, path) {
90
+ const result = (0, tokenizer_js_1.default)(source);
91
+ let { tokens } = result;
92
+ const { position, error } = result;
93
+ if (error) {
94
+ throw this.createError(path || "unknown", source, position, error);
95
+ }
96
+ for (const tokenPreprocessor of this.tokenPreprocessors) {
97
+ const result = tokenPreprocessor(this, tokens, path);
98
+ if (result !== undefined) {
99
+ tokens = result;
100
+ }
101
+ }
102
+ return tokens;
103
+ }
91
104
  async load(file, from) {
92
105
  const path = from ? this.options.loader.resolve(from, file) : file;
93
106
  if (!this.cache.has(path)) {
94
- const { source, data } = await this.options.loader.load(path);
107
+ // Remove query and hash params from path before loading
108
+ const cleanPath = path
109
+ .split("?")[0]
110
+ .split("#")[0];
111
+ const { source, data } = await this.options.loader.load(cleanPath);
95
112
  const template = this.compile(source, path, data);
96
113
  this.cache.set(path, template);
97
114
  }
@@ -4,20 +4,13 @@ exports.parseTag = void 0;
4
4
  function tokenize(source) {
5
5
  const tokens = [];
6
6
  let type = "string";
7
- let trimNext = false;
8
7
  let position = 0;
9
8
  try {
10
9
  while (source.length > 0) {
11
10
  if (type === "string") {
12
11
  const index = source.indexOf("{{");
13
12
  const code = index === -1 ? source : source.slice(0, index);
14
- if (trimNext) {
15
- tokens.push([type, code.trimStart(), position]);
16
- trimNext = false;
17
- }
18
- else {
19
- tokens.push([type, code, position]);
20
- }
13
+ tokens.push([type, code, position]);
21
14
  if (index === -1) {
22
15
  break;
23
16
  }
@@ -44,39 +37,23 @@ function tokenize(source) {
44
37
  const lastIndex = indexes.length - 1;
45
38
  let tag;
46
39
  indexes.reduce((prev, curr, index) => {
47
- let code = source.slice(prev, curr - 2);
40
+ const code = source.slice(prev, curr - 2);
48
41
  // Tag
49
42
  if (index === 1) {
50
- // Left trim
51
- if (code.startsWith("-")) {
52
- code = code.slice(1);
53
- const lastToken = tokens[tokens.length - 1];
54
- lastToken[1] = lastToken[1].trimEnd();
55
- }
56
- // Right trim
57
- if (code.endsWith("-") && index === lastIndex) {
58
- code = code.slice(0, -1);
59
- trimNext = true;
60
- }
61
- tag = [type, code.trim(), position];
43
+ tag = [type, code, position];
62
44
  tokens.push(tag);
63
45
  return curr;
64
46
  }
65
- // Right trim
66
- if (index === lastIndex && code.endsWith("-")) {
67
- code = code.slice(0, -1);
68
- trimNext = true;
69
- }
70
47
  // Filters
71
- tokens.push(["filter", code.trim()]);
48
+ tokens.push(["filter", code]);
72
49
  return curr;
73
50
  });
74
51
  position += indexes[lastIndex];
75
52
  source = source.slice(indexes[lastIndex]);
76
53
  type = "string";
77
54
  // Search the closing echo tag {{ /echo }}
78
- if (tag?.[1] === "echo") {
79
- const end = source.match(/{{\s*\/echo\s*}}/);
55
+ if (tag?.[1].match(/^\-?\s*echo\s*\-?$/)) {
56
+ const end = source.match(/{{\-?\s*\/echo\s*\-?}}/);
80
57
  if (!end) {
81
58
  throw new Error("Unclosed echo tag");
82
59
  }