[IMP] app, compiler: introduce t-out

t-out automatically escaped content when it is a string not marked
with the `markup` function

t-out renders the raw content if it is a Block, or if it has been marked
with the `markup` funtion.

t-esc has been kept since it is safe and is optimized to render text nodes.

all t-raw calls are in fact the same as t-out.
This commit is contained in:
Lucas Perais (lpe)
2021-11-15 15:01:56 +01:00
committed by Aaron Bohy
parent 1761af9c24
commit b902edc1be
62 changed files with 1780 additions and 1306 deletions
+26
View File
@@ -1,5 +1,7 @@
import { BDom, multi, text, toggler } from "../blockdom";
import { validateProps } from "../component/props_validation";
import { Markup } from "../utils";
import { html } from "../blockdom/index";
/**
* This file contains utility functions that will be injected in each template,
@@ -94,6 +96,29 @@ function shallowEqual(l1: any[], l2: any[]): boolean {
return true;
}
/*
* Safely outputs `value` as a block depending on the nature of `value`
*/
export function safeOutput(value: any): ReturnType<typeof toggler> {
if (!value) {
return value;
}
let safeKey;
let block;
if (value instanceof Markup) {
safeKey = `string_safe`;
block = html(value as string);
} else if (typeof value === "string") {
safeKey = "string_unsafe";
block = text(value);
} else {
// Assuming it is a block
safeKey = "block_safe";
block = value;
}
return toggler(safeKey, block);
}
export const UTILS = {
withDefault,
zero: Symbol("zero"),
@@ -106,4 +131,5 @@ export const UTILS = {
shallowEqual,
toNumber,
validateProps,
safeOutput,
};
+7 -7
View File
@@ -15,7 +15,7 @@ import {
ASTTForEach,
ASTTif,
ASTTKey,
ASTTRaw,
ASTTOut,
ASTTSet,
ASTTranslation,
ASTType,
@@ -208,7 +208,7 @@ export class CodeGenerator {
// define blocks and utility functions
this.addLine(`let { text, createBlock, list, multi, html, toggler, component } = bdom;`);
this.addLine(
`let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber } = helpers;`
`let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers;`
);
if (this.shouldDefineAssign) {
this.addLine(`let assign = Object.assign;`);
@@ -377,8 +377,8 @@ export class CodeGenerator {
case ASTType.TEsc:
this.compileTEsc(ast, ctx);
break;
case ASTType.TRaw:
this.compileTRaw(ast, ctx);
case ASTType.TOut:
this.compileTOut(ast, ctx);
break;
case ASTType.TIf:
this.compileTIf(ast, ctx);
@@ -640,20 +640,20 @@ export class CodeGenerator {
}
}
compileTRaw(ast: ASTTRaw, ctx: Context) {
compileTOut(ast: ASTTOut, ctx: Context) {
let { block } = ctx;
if (block) {
this.insertAnchor(block);
}
block = this.createBlock(block, "html", ctx);
let expr = ast.expr === "0" ? "ctx[zero]" : compileExpr(ast.expr);
let expr = ast.expr === "0" ? "ctx[zero]" : `safeOutput(${compileExpr(ast.expr)})`;
if (ast.body) {
const nextId = BlockDescription.nextBlockId;
const subCtx: Context = createContext(ctx);
this.compileAST({ type: ASTType.Multi, content: ast.body }, subCtx);
expr = `withDefault(${expr}, b${nextId})`;
}
this.insertBlock(`html(${expr})`, block, ctx);
this.insertBlock(`${expr}`, block, ctx);
}
compileTIf(ast: ASTTif, ctx: Context, nextNode?: ASTDomNode) {
+22 -16
View File
@@ -11,7 +11,7 @@ export const enum ASTType {
TIf,
TSet,
TCall,
TRaw,
TOut,
TForEach,
TKey,
TComponent,
@@ -62,8 +62,8 @@ export interface ASTTEsc {
defaultValue: string;
}
export interface ASTTRaw {
type: ASTType.TRaw;
export interface ASTTOut {
type: ASTType.TOut;
expr: string;
body: AST[] | null;
}
@@ -156,7 +156,7 @@ export type AST =
| ASTTif
| ASTTSet
| ASTTCall
| ASTTRaw
| ASTTOut
| ASTTForEach
| ASTTKey
| ASTComponent
@@ -197,7 +197,7 @@ function parseNode(node: ChildNode, ctx: ParsingContext): AST | null {
parseTKey(node, ctx) ||
parseTTranslation(node, ctx) ||
parseTSlot(node, ctx) ||
parseTRawNode(node, ctx) ||
parseTOutNode(node, ctx) ||
parseComponent(node, ctx) ||
parseDOMNode(node, ctx) ||
parseTSetNode(node, ctx) ||
@@ -419,33 +419,39 @@ function parseTEscNode(node: Element, ctx: ParsingContext): AST | null {
}
// -----------------------------------------------------------------------------
// t-raw
// t-out
// -----------------------------------------------------------------------------
function parseTRawNode(node: Element, ctx: ParsingContext): AST | null {
if (!node.hasAttribute("t-raw")) {
function parseTOutNode(node: Element, ctx: ParsingContext): AST | null {
if (!node.hasAttribute("t-out") && !node.hasAttribute("t-raw")) {
return null;
}
const expr = node.getAttribute("t-raw")!;
if (node.hasAttribute("t-raw")) {
console.warn(
`t-raw has been deprecated in favor of t-out. If the value to render is not wrapped by the "markup" function, it will be escaped`
);
}
const expr = (node.getAttribute("t-out") || node.getAttribute("t-raw"))!;
node.removeAttribute("t-out");
node.removeAttribute("t-raw");
const tRaw: AST = { type: ASTType.TRaw, expr, body: null };
const tOut: AST = { type: ASTType.TOut, expr, body: null };
const ref = node.getAttribute("t-ref");
node.removeAttribute("t-ref");
const ast = parseNode(node, ctx);
if (!ast) {
return tRaw;
return tOut;
}
if (ast && ast.type === ASTType.DomNode) {
tRaw.body = ast.content.length ? ast.content : null;
if (ast.type === ASTType.DomNode) {
tOut.body = ast.content.length ? ast.content : null;
return {
...ast,
ref,
content: [tRaw],
content: [tOut],
};
}
return tRaw;
return tOut;
}
// -----------------------------------------------------------------------------
@@ -504,7 +510,7 @@ function parseTForEach(node: Element, ctx: ParsingContext): AST | null {
function hasNoComponent(ast: AST): boolean {
switch (ast.type) {
case ASTType.TComponent:
case ASTType.TRaw:
case ASTType.TOut:
case ASTType.TCall:
case ASTType.TCallBlock:
case ASTType.TSlot:
+1 -2
View File
@@ -11,7 +11,6 @@ import {
toggler,
} from "./blockdom";
import { mainEventHandler } from "./component/handler";
import { EventBus, whenReady, loadFile } from "./utils";
config.shouldNormalizeDom = false;
config.mainEventHandler = mainEventHandler;
@@ -58,7 +57,7 @@ export { Memo } from "./misc/memo";
export { css, xml } from "./tags";
export { useState } from "./reactivity";
export { useEffect, useEnv, useExternalListener, useRef, useSubEnv } from "./hooks";
export const utils = { EventBus, whenReady, loadFile };
export { EventBus, whenReady, loadFile, markup } from "./utils";
export {
onWillStart,
+3 -1
View File
@@ -128,7 +128,9 @@ function isTrackable(value: any): boolean {
value !== null &&
typeof value === "object" &&
!(value instanceof Date) &&
!(value instanceof Promise)
!(value instanceof Promise) &&
!(value instanceof String) &&
!(value instanceof Number)
);
}
+15
View File
@@ -21,3 +21,18 @@ export async function loadFile(url: string): Promise<string> {
}
return await result.text();
}
/*
* This class just transports the fact that a string is safe
* to be injected as HTML. Overriding a JS primitive is quite painful though
* so we need to redfine toString and valueOf.
*/
export class Markup extends String {}
/*
* Marks a value as safe, that is, a value that can be injected as HTML directly.
* It should be used to wrap the value passed to a t-out directive to allow a raw rendering.
*/
export function markup(value: any) {
return new Markup(value);
}