From 3a0c73e5b3f101cc4481cef1bb06c3460edb82fa Mon Sep 17 00:00:00 2001 From: Jim Simon Date: Tue, 4 Nov 2025 17:18:17 -0500 Subject: [PATCH 1/2] [labs/ssr,lit-html,lit] Add direct-html directive and rendering support --- packages/labs/ssr/src/lib/render-value.ts | 16 ++-- packages/lit-html/package.json | 13 ++++ .../lit-html/src/directives/direct-html.ts | 77 +++++++++++++++++++ packages/lit/package.json | 4 + packages/lit/src/directives/direct-html.ts | 7 ++ 5 files changed, 112 insertions(+), 5 deletions(-) create mode 100644 packages/lit-html/src/directives/direct-html.ts create mode 100644 packages/lit/src/directives/direct-html.ts diff --git a/packages/labs/ssr/src/lib/render-value.ts b/packages/labs/ssr/src/lib/render-value.ts index 99a9323e24..f5adb032c1 100644 --- a/packages/labs/ssr/src/lib/render-value.ts +++ b/packages/labs/ssr/src/lib/render-value.ts @@ -768,15 +768,21 @@ export function renderValue( const result: ThunkedRenderResult = []; if (value != null && isTemplateResult(value)) { - if (hydratable) { + if (hydratable && (!('direct' in value) || !value.direct)) { result.push( `` ); } - result.push(() => - renderTemplateResult(value as TemplateResult, renderInfo) - ); - if (hydratable) { + if ('direct' in value && value.direct) { + result.push(() => { + return (value as TemplateResult).strings.join(''); + }); + } else { + result.push(() => + renderTemplateResult(value as TemplateResult, renderInfo) + ); + } + if (hydratable && (!('direct' in value) || !value.direct)) { result.push(``); } } else { diff --git a/packages/lit-html/package.json b/packages/lit-html/package.json index 9f693c627d..63f2f87d78 100644 --- a/packages/lit-html/package.json +++ b/packages/lit-html/package.json @@ -130,6 +130,19 @@ "development": "./development/directives/class-map.js", "default": "./directives/class-map.js" }, + "./directives/direct-html.js": { + "types": "./development/directives/direct-html.d.ts", + "browser": { + "development": "./development/directives/direct-html.js", + "default": "./directives/direct-html.js" + }, + "node": { + "development": "./node/development/directives/direct-html.js", + "default": "./node/directives/direct-html.js" + }, + "development": "./development/directives/direct-html.js", + "default": "./directives/direct-html.js" + }, "./directives/guard.js": { "types": "./development/directives/guard.d.ts", "browser": { diff --git a/packages/lit-html/src/directives/direct-html.ts b/packages/lit-html/src/directives/direct-html.ts new file mode 100644 index 0000000000..e34ef2aec6 --- /dev/null +++ b/packages/lit-html/src/directives/direct-html.ts @@ -0,0 +1,77 @@ +/** + * @license + * Copyright 2017 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */ + +import {nothing, TemplateResult, noChange} from '../lit-html.js'; +import {directive, Directive, PartInfo, PartType} from '../directive.js'; + +const HTML_RESULT = 1; + +export class DirectHTMLDirective extends Directive { + static directiveName = 'directHTML'; + static resultType = HTML_RESULT; + + private _value: unknown = nothing; + private _templateResult?: TemplateResult; + + constructor(partInfo: PartInfo) { + super(partInfo); + if (partInfo.type !== PartType.CHILD) { + throw new Error( + `${ + (this.constructor as typeof DirectHTMLDirective).directiveName + }() can only be used in child bindings` + ); + } + } + + render(value: string | typeof nothing | typeof noChange | undefined | null) { + if (value === nothing || value == null) { + this._templateResult = undefined; + return (this._value = value); + } + if (value === noChange) { + return value; + } + if (typeof value != 'string') { + throw new Error( + `${ + (this.constructor as typeof DirectHTMLDirective).directiveName + }() called with a non-string value` + ); + } + if (value === this._value) { + return this._templateResult; + } + this._value = value; + const strings = [value] as unknown as TemplateStringsArray; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (strings as any).raw = strings; + // WARNING: impersonating a TemplateResult like this is extremely + // dangerous. Third-party directives should not do this. + return (this._templateResult = { + // Cast to a known set of integers that satisfy ResultType so that we + // don't have to export ResultType and possibly encourage this pattern. + // This property needs to remain unminified. + ['_$litType$']: (this.constructor as typeof DirectHTMLDirective) + .resultType as 1 | 2, + strings, + values: [], + direct: true, + } as TemplateResult); + } +} + +/** + * Renders the result as HTML, rather than text. + * + * The values `undefined`, `null`, and `nothing`, will all result in no content + * (empty string) being rendered. + * + * Note, this is unsafe to use with any user-provided input that hasn't been + * sanitized or escaped, as it may lead to cross-site-scripting + * vulnerabilities. + */ +export const directHTML = directive(DirectHTMLDirective); diff --git a/packages/lit/package.json b/packages/lit/package.json index dce2db2b04..c941addfb8 100644 --- a/packages/lit/package.json +++ b/packages/lit/package.json @@ -93,6 +93,10 @@ "types": "./development/directives/class-map.d.ts", "default": "./directives/class-map.js" }, + "./directives/direct-html.js": { + "types": "./development/directives/direct-html.d.ts", + "default": "./directives/direct-html.js" + }, "./directives/guard.js": { "types": "./development/directives/guard.d.ts", "default": "./directives/guard.js" diff --git a/packages/lit/src/directives/direct-html.ts b/packages/lit/src/directives/direct-html.ts new file mode 100644 index 0000000000..728d8f23f1 --- /dev/null +++ b/packages/lit/src/directives/direct-html.ts @@ -0,0 +1,7 @@ +/** + * @license + * Copyright 2021 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */ + +export * from 'lit-html/directives/direct-html.js'; From e904b63c7d797553c208eb56736319461cec90c5 Mon Sep 17 00:00:00 2001 From: Jim Simon Date: Tue, 4 Nov 2025 17:29:42 -0500 Subject: [PATCH 2/2] Add a few more imports for builds and tests --- packages/lit-html/rollup.config.js | 1 + packages/lit-html/src/test/node-imports.ts | 1 + packages/lit/rollup.config.js | 1 + packages/lit/src/test/node-imports.ts | 1 + 4 files changed, 4 insertions(+) diff --git a/packages/lit-html/rollup.config.js b/packages/lit-html/rollup.config.js index 469e3006f4..9e94222a60 100644 --- a/packages/lit-html/rollup.config.js +++ b/packages/lit-html/rollup.config.js @@ -16,6 +16,7 @@ export const defaultConfig = (options = {}) => 'directives/cache', 'directives/choose', 'directives/class-map', + 'directives/direct-html', 'directives/guard', 'directives/if-defined', 'directives/join', diff --git a/packages/lit-html/src/test/node-imports.ts b/packages/lit-html/src/test/node-imports.ts index 718e77962f..a85690d9ff 100644 --- a/packages/lit-html/src/test/node-imports.ts +++ b/packages/lit-html/src/test/node-imports.ts @@ -13,6 +13,7 @@ import 'lit-html/directives/async-replace.js'; import 'lit-html/directives/cache.js'; import 'lit-html/directives/choose.js'; import 'lit-html/directives/class-map.js'; +import 'lit-html/directives/direct-html.js'; import 'lit-html/directives/guard.js'; import 'lit-html/directives/if-defined.js'; import 'lit-html/directives/join.js'; diff --git a/packages/lit/rollup.config.js b/packages/lit/rollup.config.js index 84b87e4057..f4e5c5f3b3 100644 --- a/packages/lit/rollup.config.js +++ b/packages/lit/rollup.config.js @@ -50,6 +50,7 @@ export default litProdConfig({ 'directives/cache', 'directives/choose', 'directives/class-map', + 'directives/direct-html', 'directives/guard', 'directives/if-defined', 'directives/join', diff --git a/packages/lit/src/test/node-imports.ts b/packages/lit/src/test/node-imports.ts index 6bbbae602a..f485579cac 100644 --- a/packages/lit/src/test/node-imports.ts +++ b/packages/lit/src/test/node-imports.ts @@ -25,6 +25,7 @@ import 'lit/directives/async-replace.js'; import 'lit/directives/cache.js'; import 'lit/directives/choose.js'; import 'lit/directives/class-map.js'; +import 'lit/directives/direct-html.js'; import 'lit/directives/guard.js'; import 'lit/directives/if-defined.js'; import 'lit/directives/join.js';