Template.ts 3.61 KB
Newer Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
/*
 * This file is part of the TYPO3 CMS project.
 *
 * It is free software; you can redistribute it and/or modify it under
 * the terms of the GNU General Public License, either version 2
 * of the License, or any later version.
 *
 * For the full copyright and license information, please read the
 * LICENSE.txt file that was distributed with this source code.
 *
 * The TYPO3 project - inspiring people to share!
 */

import SecurityUtility = require('TYPO3/CMS/Core/SecurityUtility');

/**
 * Module: TYPO3/CMS/Backend/Element/Template
 *
 * @example
 * const value = 'Hello World';
 * const template = html`<div>${value}</div>`;
 * console.log(template.content);
 */
export class Template {
  private readonly securityUtility: SecurityUtility;
  private readonly strings: TemplateStringsArray;
  private readonly values: any[];
  private readonly unsafe: boolean;

  private closures: Map<string, Function>;

  public constructor(unsafe: boolean, strings: TemplateStringsArray, ...values: any[]) {
    this.securityUtility = new SecurityUtility();
    this.unsafe = unsafe;
    this.strings = strings;
    this.values = values;
    this.closures = new Map<string, Function>();
  }

  public getHtml(parentScope: Template = null): string {
    if (parentScope === null) {
      parentScope = this;
    }
    return this.strings
      .map((string: string, index: number) => {
        if (this.values[index] === undefined) {
          return string;
        }
        return string + this.getValue(this.values[index], parentScope);
      })
      .join('');
  }

  public getElement(): HTMLTemplateElement {
    const template = document.createElement('template');
    template.innerHTML = this.getHtml();
    return template;
  }

  public mountTo(renderRoot: HTMLElement | ShadowRoot, clear: boolean = false): void {
    if (clear) {
      renderRoot.innerHTML = '';
    }
    const fragment = this.getElement().content;
    const target = fragment.cloneNode(true) as DocumentFragment;
    const closurePattern = new RegExp('^@closure:(.+)$');
    target.querySelectorAll('[\\@click]').forEach((element: HTMLElement) => {
      const pointer = element.getAttribute('@click');
      const matches = closurePattern.exec(pointer);
      const closure = this.closures.get(matches[1]);
      if (matches === null || closure === null) {
        return;
      }
      element.removeAttribute('@click');
      element.addEventListener('click', (evt: Event) => closure.call(null, evt));
    });
    renderRoot.appendChild(target)
  }

  private getValue(value: any, parentScope: Template): string {
    if (value instanceof Array) {
      return value
        .map((item: any) => this.getValue(item, parentScope))
        .filter((item: string) => item !== '')
        .join('');
    }
    if (value instanceof Function) {
      const identifier = this.securityUtility.getRandomHexValue(20);
      parentScope.closures.set(identifier, value);
      return '@closure:' + identifier;
    }
    // @todo `value instanceof Template` is removed somewhere during minification
    if (value instanceof Template || value instanceof Object && value.constructor === this.constructor) {
      return value.getHtml(parentScope);
    }
    if (value instanceof Object) {
      return JSON.stringify(value);
    }
    if (this.unsafe) {
      return (value + '').trim();
    }
    return this.securityUtility.encodeHtml(value).trim();
  }
}

export const html = (strings: TemplateStringsArray, ...values: any[]): Template => {
  return new Template(false, strings, ...values);
}

export const unsafe = (strings: TemplateStringsArray, ...values: any[]): Template => {
  return new Template(true, strings, ...values);
}