New Case Study:See how Anthropic automated 95% of dependency reviews with Socket.Learn More
Socket
Sign inDemoInstall
Socket

@popeindustries/lit-html-server

Package Overview
Dependencies
Maintainers
1
Versions
58
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@popeindustries/lit-html-server - npm Package Compare versions

Comparing version 1.0.0-rc.1 to 1.0.0-rc.2

src/string.js

2

directives/unsafe-html.js

@@ -20,3 +20,3 @@ 'use strict';

}
part.setValue(`${index_js.unsafeStringPrefix}${value}`);
part.setValue(`${index_js.unsafePrefixString}${value}`);
};

@@ -23,0 +23,0 @@ }

@@ -8,33 +8,16 @@ 'use strict';

/**
* @typedef { Array<string|Promise<any>> } TemplateResult - an array of template strings (or Promises) and their resolved values
* @property { boolean } isTemplateResult
* An empty string Buffer
*/
const emptyStringBuffer = Buffer.from('');
/**
* @typedef TemplateResultProcessor { import('./default-template-result-processor.js').TemplateResultProcessor }
* A value for strings that signals a Part to clear its content
*/
/**
* Determine if 'obj' is a template result
*
* @param { any } obj
* @returns { boolean }
*/
function isTemplateResult(obj) {
return Array.isArray(obj) && obj.isTemplateResult;
}
const nothingString = '__nothing-lit-html-server-string__';
/**
* Reduce a Template's strings and values to an array of resolved strings (or Promises)
*
* @param { Template } template
* @param { Array<any> } values
* @param { TemplateResultProcessor } processor
* @returns { TemplateResult }
* A prefix value for strings that should not be escaped
*/
function TemplateResult(template, values, processor) {
const result = processor.processTemplate(template, values);
const unsafePrefixString = '__unsafe-lit-html-server-string__';
result.isTemplateResult = true;
return result;
}
/**

@@ -225,12 +208,2 @@ * Determine if "promise" is a Promise instance

/**
* A value for strings that signals a Part to clear its content
*/
const nothingString = '__nothing-lit-html-server-string__';
/**
* A prefix value for strings that should not be escaped
*/
const unsafeStringPrefix = '__unsafe-lit-html-server-string__';
/**
* Base class interface for Node/Attribute parts

@@ -284,3 +257,3 @@ */

getValue(value) {
return resolveValue(value, this, true);
return resolveNodeValue(value, this);
}

@@ -298,3 +271,3 @@ }

* @param { string } name
* @param { Array<string> } strings
* @param { Array<Buffer> } strings
*/

@@ -306,6 +279,8 @@ constructor(name, strings) {

this.length = strings.length - 1;
this.prefix = Buffer.from(`${this.name}="`);
this.suffix = Buffer.from(`${this.strings[this.length]}"`);
}
/**
* Retrieve resolved string from passed "values".
* Retrieve resolved string Buffer from passed "values".
* Resolves to a single string, or Promise for a single string,

@@ -315,47 +290,45 @@ * even when responsible for multiple values.

* @param { Array<any> } values
* @returns { string|Promise<string> }
* @returns { Buffer|Promise<Buffer> }
*/
getValue(values) {
const strings = this.strings;
const endIndex = strings.length - 1;
const result = [`${this.name}="`];
let pending;
let chunks = [this.prefix];
let pendingChunks;
for (let i = 0; i < endIndex; i++) {
const string = strings[i];
let value = resolveValue(values[i], this, false);
for (let i = 0; i < this.length; i++) {
const string = this.strings[i];
let value = resolveAttributeValue(values[i], this);
result.push(string);
// Bail if 'nothing'
if (value === nothingString) {
return '';
return emptyStringBuffer;
}
chunks.push(string);
if (Buffer.isBuffer(value)) {
chunks.push(value);
} else if (isPromise(value)) {
if (pending === undefined) {
pending = [];
// Lazy init for uncommon scenario
if (pendingChunks === undefined) {
pendingChunks = [];
}
const index = result.push(value) - 1;
const index = chunks.push(value) - 1;
pending.push(
pendingChunks.push(
value.then((value) => {
result[index] = value;
chunks[index] = value;
})
);
} else if (Array.isArray(value)) {
result.push(value.join(''));
} else {
result.push(value);
chunks = chunks.concat(value);
}
}
result.push(`${strings[endIndex]}"`);
chunks.push(this.suffix);
if (pending !== undefined) {
// Flatten in case array returned from Promise
return Promise.all(pending).then(() =>
result.reduce((result, value) => result.concat(value), []).join('')
);
if (pendingChunks !== undefined) {
return Promise.all(pendingChunks).then(() => Buffer.concat(chunks));
}
return result.join('');
return Buffer.concat(chunks);
}

@@ -373,3 +346,3 @@ }

* @param { string } name
* @param { Array<string> } strings
* @param { Array<Buffer> } strings
* @throws error when multiple expressions

@@ -380,3 +353,9 @@ */

if (strings.length !== 2 || strings[0] !== '' || strings[1] !== '') {
this.name = Buffer.from(this.name);
if (
strings.length !== 2 ||
!strings[0].equals(emptyStringBuffer) ||
!strings[1].equals(emptyStringBuffer)
) {
throw Error('Boolean attributes can only contain a single expression');

@@ -387,6 +366,6 @@ }

/**
* Retrieve resolved string from passed "values".
* Retrieve resolved string Buffer from passed "values".
*
* @param { Array<any> } values
* @returns { string|Promise<string> }
* @returns { Buffer|Promise<Buffer> }
*/

@@ -401,6 +380,6 @@ getValue(values) {

if (isPromise(value)) {
return value.then((value) => (value ? this.name : ''));
return value.then((value) => (value ? this.name : emptyStringBuffer));
}
return value ? this.name : '';
return value ? this.name : emptyStringBuffer;
}

@@ -415,3 +394,3 @@ }

/**
* Retrieve resolved string from passed "values".
* Retrieve resolved string Buffer from passed "values".
* Properties have no server-side representation,

@@ -424,3 +403,3 @@ * so always returns an empty string.

getValue(/* values */) {
return '';
return emptyStringBuffer;
}

@@ -435,3 +414,3 @@ }

/**
* Retrieve resolved string from passed "values".
* Retrieve resolved string Buffer from passed "values".
* Event bindings have no server-side representation,

@@ -444,3 +423,3 @@ * so always returns an empty string.

getValue(/* values */) {
return '';
return emptyStringBuffer;
}

@@ -453,7 +432,6 @@ }

* @param { any } value
* @param { Part } part
* @param { boolean } ignoreNothingAndUndefined
* @param { AttributePart } part
* @returns { any }
*/
function resolveValue(value, part, ignoreNothingAndUndefined = true) {
function resolveAttributeValue(value, part) {
if (isDirective(value)) {

@@ -463,15 +441,66 @@ value = getDirectiveValue(value, part);

if (ignoreNothingAndUndefined && (value === nothingString || value === undefined)) {
return '';
if (value === nothingString) {
return value;
}
// Pass-through template result
if (isTemplateResult(value)) {
value = value.read();
}
if (isPrimitive(value)) {
const string = typeof value !== 'string' ? String(value) : value;
// Escape if not prefixed with unsafePrefixString, otherwise strip prefix
return Buffer.from(
string.indexOf(unsafePrefixString) === 0 ? string.slice(33) : escapeTextForBrowser(string)
);
} else if (Buffer.isBuffer(value)) {
return value;
} else if (isPrimitive(value)) {
} else if (isPromise(value)) {
return value.then((value) => resolveAttributeValue(value, part));
} else if (isSyncIterator(value)) {
if (!Array.isArray(value)) {
value = Array.from(value);
}
return Buffer.concat(
value.reduce((values, value) => {
value = resolveAttributeValue(value, part);
// Flatten
if (Array.isArray(value)) {
return values.concat(value);
}
values.push(value);
return values;
}, [])
);
} else {
throw Error('unknown AttributPart value', value);
}
}
/**
* Resolve "value" to string Buffer if possible
*
* @param { any } value
* @param { NodePart } part
* @returns { any }
*/
function resolveNodeValue(value, part) {
if (isDirective(value)) {
value = getDirectiveValue(value, part);
}
if (value === nothingString || value === undefined) {
return emptyStringBuffer;
}
if (isPrimitive(value)) {
const string = typeof value !== 'string' ? String(value) : value;
// Escape if not prefixed with unsafeStringPrefix, otherwise strip prefix
return string.indexOf(unsafeStringPrefix) === 0 ? string.slice(33) : escapeTextForBrowser(string);
// Escape if not prefixed with unsafePrefixString, otherwise strip prefix
return Buffer.from(
string.indexOf(unsafePrefixString) === 0 ? string.slice(33) : escapeTextForBrowser(string)
);
} else if (isTemplateResult(value) || Buffer.isBuffer(value)) {
return value;
} else if (isPromise(value)) {
return value.then((value) => resolveValue(value, part, ignoreNothingAndUndefined));
return value.then((value) => resolveNodeValue(value, part));
} else if (isSyncIterator(value)) {

@@ -482,4 +511,4 @@ if (!Array.isArray(value)) {

return value.reduce((values, value) => {
value = resolveValue(value, part, ignoreNothingAndUndefined);
// Allow nested template results to also be flattened by not checking isTemplateResult
value = resolveNodeValue(value, part);
// Flatten
if (Array.isArray(value)) {

@@ -492,3 +521,3 @@ return values.concat(value);

} else {
return value;
throw Error('unknown NodePart value', value);
}

@@ -512,3 +541,183 @@ }

const pool = [];
let id = 0;
/**
* Retrieve TemplateResult instance.
* Uses an object pool to recycle instances.
*
* @param { Template } template
* @param { Array<any> } values
* @returns { TemplateResult }
*/
function templateResult(template, values) {
let instance = pool.pop();
if (instance) {
instance.template = template;
instance.values = values;
} else {
instance = new TemplateResult(template, values);
}
return instance;
}
/**
* Determine whether "result" is a TemplateResult
*
* @param { TemplateResult } result
* @returns { boolean }
*/
function isTemplateResult(result) {
return result instanceof TemplateResult;
}
/**
* A class for consuming the combined static and dynamic parts of a lit-html Template.
* TemplateResults
*/
class TemplateResult {
/**
* Constructor
*
* @param { Template } template
* @param { Array<any> } values
*/
constructor(template, values) {
this.template = template;
this.values = values;
this.id = id++;
this.index = 0;
}
/**
* Consume template result content.
* *Note* that instances may only be read once,
* and will be destroyed upon completion.
*
* @param { boolean } deep - recursively resolve nested TemplateResults
* @returns { any }
*/
read(deep) {
let buffer = emptyStringBuffer;
let chunk, chunks;
while ((chunk = this.readChunk()) !== null) {
if (Buffer.isBuffer(chunk)) {
buffer = Buffer.concat([buffer, chunk]);
} else {
if (chunks === undefined) {
chunks = [];
}
buffer = reduce(buffer, chunks, chunk, deep);
}
}
if (chunks !== undefined) {
chunks.push(buffer);
return chunks.length > 1 ? chunks : chunks[0];
}
return buffer;
}
/**
* Consume template result content one chunk at a time.
* *Note* that instances may only be read once,
* and will be destroyed when the last chunk is read.
*
* @returns { any }
*/
readChunk() {
const isString = this.index % 2 === 0;
const index = (this.index / 2) | 0;
// Finished
if (!isString && index >= this.template.strings.length - 1) {
this.destroy();
return null;
}
this.index++;
if (isString) {
return this.template.strings[index];
}
const part = this.template.parts[index];
let value;
if (part instanceof AttributePart) {
// AttributeParts can have multiple values, so slice based on length
// (strings in-between values are already handled the instance)
if (part.length > 1) {
value = part.getValue(this.values.slice(index, index + part.length));
this.index += part.length;
} else {
value = part.getValue([this.values[index]]);
}
} else {
value = part.getValue(this.values[index]);
}
return value;
}
/**
* Destroy the instance,
* returning it to the object pool
*
* @param { boolean } permanent - permanently destroy instance and it's children
* @returns { void }
*/
destroy(permanent) {
if (this.values !== undefined) {
if (permanent) {
for (const value of this.values) {
if (isTemplateResult(value)) {
value.destroy(permanent);
}
}
}
this.values.length = 0;
}
this.values = undefined;
this.template = undefined;
this.index = 0;
if (!permanent) {
pool.push(this);
}
}
}
/**
* Commit "chunk" to string "buffer".
* Returns new "buffer" value.
*
* @param { Buffer } buffer
* @param { Array<any> } chunks
* @param { any } chunk
* @param { boolean } [deep]
* @returns { Buffer }
*/
function reduce(buffer, chunks, chunk, deep = false) {
if (Buffer.isBuffer(chunk)) {
return Buffer.concat([buffer, chunk]);
} else if (isTemplateResult(chunk)) {
if (deep) {
return reduce(buffer, chunks, chunk.read(deep), deep);
} else {
chunks.push(buffer, chunk);
return emptyStringBuffer;
}
} else if (Array.isArray(chunk)) {
return chunk.reduce((buffer, chunk) => reduce(buffer, chunks, chunk), buffer);
} else if (isPromise(chunk)) {
chunks.push(buffer, chunk);
return emptyStringBuffer;
}
}
/**
* @typedef TemplateProcessor

@@ -558,7 +767,9 @@ * @property { (name: string, strings: Array<string>) => AttributePart } handleAttributeExpressions

* @typedef TemplateResultProcessor
* @property { (template: Template, values: Array<any>) => TemplateResult } processTemplate
* @property { (renderer: TemplateResultRenderer, stack: Array<any>) => void } process
*/
/**
* Class representing the default template result processor.
* Class for the default TemplateResult processor
* used by Promise/Stream TemplateRenderers.
*
* @implements TemplateResultProcessor

@@ -568,45 +779,64 @@ */

/**
* Create template result array from "template" instance and dynamic "values".
* The returned array contains Template "strings" concatenated with known string "values",
* and any Promises that will eventually resolve asynchronous string "values".
* A synchronous template tree will reduce to an array containing a single string.
* An asynchronous template tree will reduce to an array of strings and Promises.
* Process "stack" and push chunks to "renderer"
*
* @param { Template } template
* @param { Array<any> } values
* @returns { TemplateResult }
* @param { TemplateResultRenderer } renderer
* @param { Array<any> } stack
*/
processTemplate(template, values) {
const { strings, parts } = template;
const endIndex = strings.length - 1;
const result = [];
let buffer = '';
process(renderer, stack) {
while (!renderer.awaitingPromise) {
let chunk = stack[0];
let breakLoop = false;
let popStack = true;
for (let i = 0; i < endIndex; i++) {
const string = strings[i];
const part = parts[i];
let value = values[i];
if (chunk === undefined) {
return renderer.push(null);
}
buffer += string;
if (isTemplateResult(chunk)) {
popStack = false;
chunk = getTemplateResultChunk(chunk, stack);
}
if (part instanceof AttributePart) {
// AttributeParts can have multiple values, so slice based on length
// (strings in-between values are already stored in the instance)
if (part.length > 1) {
value = part.getValue(values.slice(i, i + part.length));
i += part.length - 1;
// Skip if finished reading TemplateResult (null)
if (chunk !== null) {
if (Buffer.isBuffer(chunk)) {
if (!renderer.push(chunk)) {
breakLoop = true;
}
} else if (isPromise(chunk)) {
breakLoop = true;
renderer.awaitingPromise = true;
stack.unshift(chunk);
chunk
.then((chunk) => {
renderer.awaitingPromise = false;
stack[0] = chunk;
this.process(renderer, stack);
})
.catch((err) => {
destroy(stack);
renderer.destroy(err);
});
} else if (Array.isArray(chunk)) {
// An existing TemplateResult will have already set this to "false",
// so only remove existing Array if there is no active TemplateResult
if (popStack === true) {
popStack = false;
stack.shift();
}
stack.unshift(...chunk);
} else {
value = part.getValue([value]);
destroy(stack);
return renderer.destroy(Error('unknown chunk type:', chunk));
}
} else {
value = part.getValue(value);
}
buffer = reduce(buffer, result, value);
if (popStack) {
stack.shift();
}
if (breakLoop) {
break;
}
}
buffer += strings[endIndex];
result.push(buffer);
return result;
}

@@ -616,18 +846,14 @@ }

/**
* Commit value to string "buffer"
* Permanently destroy all remaining TemplateResults in "stack".
* (Triggered on error)
*
* @param { string } buffer
* @param { TemplateResult } result
* @param { any } value
* @returns { string }
* @param { Array<any> } stack
*/
function reduce(buffer, result, value) {
if (typeof value === 'string') {
buffer += value;
return buffer;
} else if (Array.isArray(value)) {
return value.reduce((buffer, value) => reduce(buffer, result, value), buffer);
} else if (isPromise(value)) {
result.push(buffer, value);
return '';
function destroy(stack) {
if (stack.length > 0) {
for (const chunk of stack) {
if (isTemplateResult(chunk)) {
chunk.destroy(true);
}
}
}

@@ -637,39 +863,26 @@ }

/**
* @typedef TemplateResult { import('./template-result.js).TemplateResult }
*/
/**
* Buffer strings from "result" and store them on "accumulator"
* Retrieve next chunk from "result".
* Adds nested TemplateResults to the stack if necessary.
*
* @param { TemplateResult } result
* @param { object } [accumulator]
* @returns { Promise<string> }
* @param { Array<any> } stack
*/
async function bufferResult(
result,
accumulator = {
buffer: '',
bufferChunk(chunk) {
this.buffer += chunk;
}
function getTemplateResultChunk(result, stack) {
let chunk = result.readChunk();
// Skip empty strings
if (Buffer.isBuffer(chunk) && chunk.length === 0) {
chunk = result.readChunk();
}
) {
let stack = result.slice();
let chunk;
accumulator.buffer = '';
while ((chunk = stack.shift()) !== undefined) {
if (typeof chunk === 'string') {
accumulator.bufferChunk(chunk);
} else if (Array.isArray(chunk)) {
stack = chunk.concat(stack);
} else if (isPromise(chunk)) {
stack.unshift(await chunk);
} else {
throw Error('unknown value type', chunk);
}
// Finished reading, dispose
if (chunk === null) {
stack.shift();
} else if (isTemplateResult(chunk)) {
// Add to top of stack
stack.unshift(chunk);
chunk = getTemplateResultChunk(chunk, stack);
}
return accumulator.buffer;
return chunk;
}

@@ -680,2 +893,8 @@

*/
/**
* @typedef TemplateResultProcessor { import('./default-template-result-processor.js).TemplateResultProcessor }
*/
/**
* @typedef TemplateResultRenderer { import('./default-template-result-renderer.js).TemplateResultRenderer }
*/

@@ -690,6 +909,32 @@ /**

* @param { TemplateResult } result
* @param { TemplateResultProcessor } processor
* @returns { Promise<string> }
*/
constructor(result) {
return bufferResult(result);
constructor(result, processor) {
return new Promise((resolve, reject) => {
let stack = [result];
let chunks = [];
processor.process(
{
awaitingPromise: false,
push(chunk) {
if (chunk === null) {
resolve(Buffer.concat(chunks).toString());
} else {
chunks.push(chunk);
}
return true;
},
destroy(err) {
chunks.length = 0;
chunks = undefined;
stack.length = 0;
stack = undefined;
reject(err);
}
},
stack
);
});
}

@@ -703,3 +948,5 @@ }

/**
* A custom Readable stream class that renders a TemplateResult
* A custom Readable stream class for rendering a template result to a stream
*
* @implements TemplateResultRenderer
*/

@@ -710,72 +957,22 @@ class StreamTemplateRenderer extends stream.Readable {

*
* @param { TemplateResult } result
* @param { object } [options] Readable options
* @see https://nodejs.org/api/stream.html#stream_new_stream_readable_options
* @param { TemplateResult } result - a template result returned from call to "html`...`"
* @param { TemplateResultProcessor } processor
* @returns { Readable }
*/
constructor(result, options) {
super({ autoDestroy: true, ...options });
constructor(result, processor) {
super({ autoDestroy: true });
this.canPushData = true;
this.done = false;
this.buffer = '';
this.index = 0;
bufferResult(result, this)
.then(() => {
this.done = true;
this._drainBuffer();
})
.catch((err) => {
this.destroy(err);
});
this.awaitingPromise = false;
this.processor = processor;
this.stack = [result];
}
/**
* Push "chunk" onto buffer
* (Called by "bufferResult" utility)
*
* @param { string } chunk
*/
bufferChunk(chunk) {
this.buffer += chunk;
this._drainBuffer();
}
/**
* Extend Readable.read()
*/
_read() {
this.canPushData = true;
this._drainBuffer();
this.processor.process(this, this.stack);
}
/**
* Write all buffered content to stream.
* Returns "false" if write triggered backpressure, otherwise "true".
*
* @returns { boolean }
*/
_drainBuffer() {
if (!this.canPushData) {
return false;
}
const bufferLength = this.buffer.length;
if (this.index < bufferLength) {
// Strictly speaking we shouldn't compare character length with byte length, but close enough
const length = Math.min(bufferLength - this.index, this.readableHighWaterMark);
const chunk = this.buffer.slice(this.index, this.index + length);
this.canPushData = this.push(chunk, 'utf8');
this.index += length;
} else if (this.done) {
this.push(null);
}
return this.canPushData;
}
/**
* Extend Readalbe.destroy()

@@ -786,6 +983,2 @@ *

_destroy(err) {
if (this.done) {
return;
}
if (err) {

@@ -796,6 +989,5 @@ this.emit('error', err);

this.canPushData = false;
this.done = true;
this.buffer = '';
this.index = 0;
this.stack.length = 0;
this.stack = [];
this.processor = null;
this.removeAllListeners();

@@ -914,3 +1106,3 @@ }

// Store any text between quote character and value
const attributeStrings = [suffix.slice(matchQuote.index + 1)];
const attributeStrings = [Buffer.from(suffix.slice(matchQuote.index + 1))];
let open = true;

@@ -926,6 +1118,6 @@ skip = 0;

if (closingQuoteIndex === -1) {
attributeStrings.push(attributeString);
attributeStrings.push(Buffer.from(attributeString));
skip++;
} else {
attributeStrings.push(attributeString.slice(0, closingQuoteIndex));
attributeStrings.push(Buffer.from(attributeString.slice(0, closingQuoteIndex)));
nextString = attributeString.slice(closingQuoteIndex + 1);

@@ -939,3 +1131,6 @@ i += skip;

} else {
part = processor.handleAttributeExpressions(name, ['', '']);
part = processor.handleAttributeExpressions(name, [
emptyStringBuffer,
emptyStringBuffer
]);
}

@@ -947,7 +1142,7 @@ }

this.strings.push(string);
this.strings.push(Buffer.from(string));
this.parts.push(part);
// Add placehholders for strings/parts that wil be skipped due to multple values in a single AttributePart
if (skip > 0) {
this.strings.push('');
this.strings.push(null);
this.parts.push(null);

@@ -958,3 +1153,3 @@ skip = 0;

this.strings.push(nextString);
this.strings.push(Buffer.from(nextString));
}

@@ -1011,3 +1206,3 @@ }

return TemplateResult(template, values, defaultTemplateResultProcessor);
return templateResult(template, values);
}

@@ -1017,20 +1212,20 @@

* Render a template result to a Readable stream
* *Note* that TemplateResults are single use, and can only be rendered once.
*
* @param { TemplateResult } result - a template result returned from call to "html`...`"
* @param { object } [options] - Readable stream options
* @see https://nodejs.org/api/stream.html#stream_new_stream_readable_options
* @returns { Readable }
*/
function renderToStream(result, options) {
return new StreamTemplateRenderer(result, options);
function renderToStream(result) {
return new StreamTemplateRenderer(result, defaultTemplateResultProcessor);
}
/**
* Render a template result to a string resolving Promise
* Render a template result to a string resolving Promise.
* *Note* that TemplateResults are single use, and can only be rendered once.
*
* @param { TemplateResult } result
* @param { TemplateResult } result - a template result returned from call to "html`...`"
* @returns { Promise<string> }
*/
function renderToString(result) {
return new PromiseTemplateRenderer(result);
return new PromiseTemplateRenderer(result, defaultTemplateResultProcessor);
}

@@ -1049,3 +1244,3 @@

exports.nothingString = nothingString;
exports.unsafeStringPrefix = unsafeStringPrefix;
exports.unsafePrefixString = unsafePrefixString;
exports.directive = directive;
{
"name": "@popeindustries/lit-html-server",
"version": "1.0.0-rc.1",
"version": "1.0.0-rc.2",
"description": "Render lit-html templates on the server",

@@ -31,2 +31,3 @@ "keywords": [

"lit-html": "^1.0.0-rc.2",
"lorem-ipsum": "^1.0.6",
"mocha": "^5.2.0",

@@ -45,3 +46,3 @@ "prettier": "^1.16.0",

"lint": "eslint './{directives,lib,test}/**/*.js'",
"perf": "node test/server.js & PID=$!; autocannon -c 100 -d 5 -p 10 http://localhost:3000 && kill $PID",
"perf": "node test/perf.js",
"precommit": "lint-staged",

@@ -48,0 +49,0 @@ "prepublishOnly": "npm run build",

@@ -6,5 +6,5 @@ [![NPM Version](https://img.shields.io/npm/v/@popeindustries/lit-html-server.svg?style=flat)](https://npmjs.org/package/@popeindustries/lit-html-server)

Render [**lit-html**](https://polymer.github.io/lit-html/) templates on the server as Node.js streams. Supports all **lit-html** types, special attribute expressions, and most of the standard directives.
Render [**lit-html**](https://polymer.github.io/lit-html/) templates on the server as Node.js streams. Supports all **lit-html** types, special attribute expressions, and many of the standard directives.
> Although based on **lit-html** semantics, **lit-html-server** is a great general purpose HTML template streaming library. Tagged template literals are a native JavaScript feature, and the HTML rendered is 100% standard markup, with no special syntax or client-side runtime required.
> Although based on **lit-html** semantics, **lit-html-server** is a great general purpose HTML template streaming library. Tagged template literals are a native JavaScript feature, and the HTML rendered is 100% standard markup, with no special syntax or client-side runtime required!

@@ -53,9 +53,16 @@ ## Usage

...and render:
...and render (plain HTTP server example, though similar for Express/Fastify/etc):
```js
const http = require('http');
const { renderToStream } = require('@popeindustries/lit-html-server');
// Returns a Node.js Readable stream which can be piped to `response`
renderToStream(layout({ title: 'Home', api: '/api/home' }));
http
.createServer((request, response) => {
const data = { title: 'Home', api: '/api/home' };
res.writeHead(200);
// Returns a Node.js Readable stream which can be piped to "response"
renderToStream(layout(data)).pipe(response);
}
```

@@ -90,3 +97,3 @@

```js
render(
renderToStream(
html`

@@ -108,2 +115,3 @@ <h1>Hello ${name}!</h1>

);
response.end(markup);
```

@@ -163,3 +171,3 @@

`;
//=> <input >
//=> <input />
```

@@ -231,2 +239,26 @@

- `cache(value)`: Enables fast switching between multiple templates by caching previous results. Since it's generally not desireable to cache between requests, this is a no-op:
```js
const cache = require('@popeindustries/lit-html-server/directives/cache.js');
cache(
loggedIn
? html`
You are logged in
`
: html`
Please log in
`
);
```
- `classMap(classInfo)`: applies css classes to the `class` attribute. 'classInfo' keys are added as class names if values are truthy:
```js
const classMap = require('@popeindustries/lit-html-server/directives/class-map.js');
html`
<div class="${classMap({ red: true })}"></div>
`;
```
- `guard(value, fn)`: no-op since re-rendering does not apply (renders result of `fn`):

@@ -277,2 +309,11 @@

- `styleMap(styleInfo)`: applies css properties to the `style` attribute. 'styleInfo' keys and values are added as style properties:
```js
const styleMap = require('@popeindustries/lit-html-server/directives/style-map.js');
html`
<div style="${styleMap({ color: 'red' })}"></div>
`;
```
- `until(...args)`: renders one of a series of values, including Promises, in priority order. Since it's not possible to render more than once in a server context, primitive sync values are prioritised over async Promises, unless there are no more pending values, in which case the last value is rendered regardless of type:

@@ -309,37 +350,4 @@

- `classMap(classInfo)`: applies css classes to the `class` attribute. 'classInfo' keys are added as class names if values are truthy:
```js
const classMap = require('@popeindustries/lit-html-server/directives/class-map.js');
html`
<div class="${classMap({ red: true })}"></div>
`;
```
- `styleMap(styleInfo)`: applies css properties to the `style` attribute. 'styleInfo' keys and values are added as style properties:
```js
const styleMap = require('@popeindustries/lit-html-server/directives/style-map.js');
html`
<div style="${styleMap({ color: 'red' })}"></div>
`;
```
- `cache(value)`: Enables fast switching between multiple templates by caching previous results. Since it's generally not desireable to cache between requests, this is a no-op:
```js
const cache = require('@popeindustries/lit-html-server/directives/cache.js');
cache(
loggedIn
? html`
You are logged in
`
: html`
Please log in
`
);
```
## Thanks!
Thanks to [Thomas Parslow](https://github.com/almost) for the [stream-template](https://github.com/almost/stream-template) library that was the basis for this streaming implementation, and thanks to [Justin Fagnani](https://github.com/justinfagnani) and the [team](https://github.com/Polymer/lit-html/graphs/contributors) behind the **lit-html** project!
/**
* @typedef TemplateResultProcessor
* @property { (template: Template, values: Array<any>) => TemplateResult } processTemplate
* @property { (renderer: TemplateResultRenderer, stack: Array<any>) => void } process
*/
/**
* @typedef Template { import('./template.js).Template }
* @typedef TemplateResult { import('./template-result.js).TemplateResult }
* @typedef TemplateResultRenderer
* @property { boolean } awaitingPromise
* @property { (chunk: Buffer) => boolean } push
* @property { (err: Error) => void } destroy
*/
import { AttributePart } from './parts.js';
import { isPromise } from './is.js';
import { isTemplateResult } from './template-result.js';
/**
* Class representing the default template result processor.
* Class for the default TemplateResult processor
* used by Promise/Stream TemplateRenderers.
*
* @implements TemplateResultProcessor

@@ -18,45 +22,80 @@ */

/**
* Create template result array from "template" instance and dynamic "values".
* The returned array contains Template "strings" concatenated with known string "values",
* and any Promises that will eventually resolve asynchronous string "values".
* A synchronous template tree will reduce to an array containing a single string.
* An asynchronous template tree will reduce to an array of strings and Promises.
* Process "stack" and push chunks to "renderer"
*
* @param { Template } template
* @param { Array<any> } values
* @returns { TemplateResult }
* @param { TemplateResultRenderer } renderer
* @param { Array<any> } stack
*/
processTemplate(template, values) {
const { strings, parts } = template;
const endIndex = strings.length - 1;
const result = [];
let buffer = '';
process(renderer, stack) {
while (!renderer.awaitingPromise) {
let chunk = stack[0];
let breakLoop = false;
let popStack = true;
for (let i = 0; i < endIndex; i++) {
const string = strings[i];
const part = parts[i];
let value = values[i];
if (chunk === undefined) {
return renderer.push(null);
}
buffer += string;
if (isTemplateResult(chunk)) {
popStack = false;
chunk = getTemplateResultChunk(chunk, stack);
}
if (part instanceof AttributePart) {
// AttributeParts can have multiple values, so slice based on length
// (strings in-between values are already stored in the instance)
if (part.length > 1) {
value = part.getValue(values.slice(i, i + part.length));
i += part.length - 1;
// Skip if finished reading TemplateResult (null)
if (chunk !== null) {
if (Buffer.isBuffer(chunk)) {
if (!renderer.push(chunk)) {
breakLoop = true;
}
} else if (isPromise(chunk)) {
breakLoop = true;
renderer.awaitingPromise = true;
stack.unshift(chunk);
chunk
.then((chunk) => {
renderer.awaitingPromise = false;
stack[0] = chunk;
this.process(renderer, stack);
})
.catch((err) => {
destroy(stack);
renderer.destroy(err);
});
} else if (Array.isArray(chunk)) {
// An existing TemplateResult will have already set this to "false",
// so only remove existing Array if there is no active TemplateResult
if (popStack === true) {
popStack = false;
stack.shift();
}
stack.unshift(...chunk);
} else {
value = part.getValue([value]);
destroy(stack);
return renderer.destroy(Error('unknown chunk type:', chunk));
}
} else {
value = part.getValue(value);
}
buffer = reduce(buffer, result, value);
if (popStack) {
stack.shift();
}
if (breakLoop) {
break;
}
}
}
}
buffer += strings[endIndex];
result.push(buffer);
return result;
/**
* Permanently destroy all remaining TemplateResults in "stack".
* (Triggered on error)
*
* @param { Array<any> } stack
*/
function destroy(stack) {
if (stack.length > 0) {
for (const chunk of stack) {
if (isTemplateResult(chunk)) {
chunk.destroy(true);
}
}
}

@@ -66,19 +105,26 @@ }

/**
* Commit value to string "buffer"
* Retrieve next chunk from "result".
* Adds nested TemplateResults to the stack if necessary.
*
* @param { string } buffer
* @param { TemplateResult } result
* @param { any } value
* @returns { string }
* @param { Array<any> } stack
*/
function reduce(buffer, result, value) {
if (typeof value === 'string') {
buffer += value;
return buffer;
} else if (Array.isArray(value)) {
return value.reduce((buffer, value) => reduce(buffer, result, value), buffer);
} else if (isPromise(value)) {
result.push(buffer, value);
return '';
function getTemplateResultChunk(result, stack) {
let chunk = result.readChunk();
// Skip empty strings
if (Buffer.isBuffer(chunk) && chunk.length === 0) {
chunk = result.readChunk();
}
// Finished reading, dispose
if (chunk === null) {
stack.shift();
} else if (isTemplateResult(chunk)) {
// Add to top of stack
stack.unshift(chunk);
chunk = getTemplateResultChunk(chunk, stack);
}
return chunk;
}

@@ -1,2 +0,2 @@

import { directive, unsafeStringPrefix } from '../index.js';
import { directive, unsafePrefixString } from '../index.js';

@@ -16,4 +16,4 @@ export const unsafeHTML = directive(unsafeHTMLDirective);

}
part.setValue(`${unsafeStringPrefix}${value}`);
part.setValue(`${unsafePrefixString}${value}`);
};
}

@@ -5,3 +5,3 @@ /**

*/
import { isTemplateResult, TemplateResult } from './template-result.js';
import { isTemplateResult, templateResult } from './template-result.js';
import { DefaultTemplateProcessor } from './default-template-processor.js';

@@ -13,3 +13,4 @@ import { DefaultTemplateResultProcessor } from './default-template-result-processor.js';

export { AttributePart, NodePart, nothingString, unsafeStringPrefix } from './parts.js';
export { AttributePart, NodePart } from './parts.js';
export { nothingString, unsafePrefixString } from './string.js';
export { directive } from './directive.js';

@@ -47,3 +48,3 @@ export {

return TemplateResult(template, values, defaultTemplateResultProcessor);
return templateResult(template, values);
}

@@ -53,20 +54,20 @@

* Render a template result to a Readable stream
* *Note* that TemplateResults are single use, and can only be rendered once.
*
* @param { TemplateResult } result - a template result returned from call to "html`...`"
* @param { object } [options] - Readable stream options
* @see https://nodejs.org/api/stream.html#stream_new_stream_readable_options
* @returns { Readable }
*/
function renderToStream(result, options) {
return new StreamTemplateRenderer(result, options);
function renderToStream(result) {
return new StreamTemplateRenderer(result, defaultTemplateResultProcessor);
}
/**
* Render a template result to a string resolving Promise
* Render a template result to a string resolving Promise.
* *Note* that TemplateResults are single use, and can only be rendered once.
*
* @param { TemplateResult } result
* @param { TemplateResult } result - a template result returned from call to "html`...`"
* @returns { Promise<string> }
*/
function renderToString(result) {
return new PromiseTemplateRenderer(result);
return new PromiseTemplateRenderer(result, defaultTemplateResultProcessor);
}

@@ -0,1 +1,2 @@

import { emptyStringBuffer, nothingString, unsafePrefixString } from './string.js';
import { isPrimitive, isPromise, isSyncIterator } from './is.js';

@@ -7,12 +8,2 @@ import escapeHTML from './escape.js';

/**
* A value for strings that signals a Part to clear its content
*/
export const nothingString = '__nothing-lit-html-server-string__';
/**
* A prefix value for strings that should not be escaped
*/
export const unsafeStringPrefix = '__unsafe-lit-html-server-string__';
/**
* Base class interface for Node/Attribute parts

@@ -66,3 +57,3 @@ */

getValue(value) {
return resolveValue(value, this, true);
return resolveNodeValue(value, this);
}

@@ -80,3 +71,3 @@ }

* @param { string } name
* @param { Array<string> } strings
* @param { Array<Buffer> } strings
*/

@@ -88,6 +79,8 @@ constructor(name, strings) {

this.length = strings.length - 1;
this.prefix = Buffer.from(`${this.name}="`);
this.suffix = Buffer.from(`${this.strings[this.length]}"`);
}
/**
* Retrieve resolved string from passed "values".
* Retrieve resolved string Buffer from passed "values".
* Resolves to a single string, or Promise for a single string,

@@ -97,47 +90,45 @@ * even when responsible for multiple values.

* @param { Array<any> } values
* @returns { string|Promise<string> }
* @returns { Buffer|Promise<Buffer> }
*/
getValue(values) {
const strings = this.strings;
const endIndex = strings.length - 1;
const result = [`${this.name}="`];
let pending;
let chunks = [this.prefix];
let pendingChunks;
for (let i = 0; i < endIndex; i++) {
const string = strings[i];
let value = resolveValue(values[i], this, false);
for (let i = 0; i < this.length; i++) {
const string = this.strings[i];
let value = resolveAttributeValue(values[i], this);
result.push(string);
// Bail if 'nothing'
if (value === nothingString) {
return '';
return emptyStringBuffer;
}
chunks.push(string);
if (Buffer.isBuffer(value)) {
chunks.push(value);
} else if (isPromise(value)) {
if (pending === undefined) {
pending = [];
// Lazy init for uncommon scenario
if (pendingChunks === undefined) {
pendingChunks = [];
}
const index = result.push(value) - 1;
const index = chunks.push(value) - 1;
pending.push(
pendingChunks.push(
value.then((value) => {
result[index] = value;
chunks[index] = value;
})
);
} else if (Array.isArray(value)) {
result.push(value.join(''));
} else {
result.push(value);
chunks = chunks.concat(value);
}
}
result.push(`${strings[endIndex]}"`);
chunks.push(this.suffix);
if (pending !== undefined) {
// Flatten in case array returned from Promise
return Promise.all(pending).then(() =>
result.reduce((result, value) => result.concat(value), []).join('')
);
if (pendingChunks !== undefined) {
return Promise.all(pendingChunks).then(() => Buffer.concat(chunks));
}
return result.join('');
return Buffer.concat(chunks);
}

@@ -155,3 +146,3 @@ }

* @param { string } name
* @param { Array<string> } strings
* @param { Array<Buffer> } strings
* @throws error when multiple expressions

@@ -162,3 +153,9 @@ */

if (strings.length !== 2 || strings[0] !== '' || strings[1] !== '') {
this.name = Buffer.from(this.name);
if (
strings.length !== 2 ||
!strings[0].equals(emptyStringBuffer) ||
!strings[1].equals(emptyStringBuffer)
) {
throw Error('Boolean attributes can only contain a single expression');

@@ -169,6 +166,6 @@ }

/**
* Retrieve resolved string from passed "values".
* Retrieve resolved string Buffer from passed "values".
*
* @param { Array<any> } values
* @returns { string|Promise<string> }
* @returns { Buffer|Promise<Buffer> }
*/

@@ -183,6 +180,6 @@ getValue(values) {

if (isPromise(value)) {
return value.then((value) => (value ? this.name : ''));
return value.then((value) => (value ? this.name : emptyStringBuffer));
}
return value ? this.name : '';
return value ? this.name : emptyStringBuffer;
}

@@ -197,3 +194,3 @@ }

/**
* Retrieve resolved string from passed "values".
* Retrieve resolved string Buffer from passed "values".
* Properties have no server-side representation,

@@ -206,3 +203,3 @@ * so always returns an empty string.

getValue(/* values */) {
return '';
return emptyStringBuffer;
}

@@ -217,3 +214,3 @@ }

/**
* Retrieve resolved string from passed "values".
* Retrieve resolved string Buffer from passed "values".
* Event bindings have no server-side representation,

@@ -226,3 +223,3 @@ * so always returns an empty string.

getValue(/* values */) {
return '';
return emptyStringBuffer;
}

@@ -235,7 +232,6 @@ }

* @param { any } value
* @param { Part } part
* @param { boolean } ignoreNothingAndUndefined
* @param { AttributePart } part
* @returns { any }
*/
function resolveValue(value, part, ignoreNothingAndUndefined = true) {
function resolveAttributeValue(value, part) {
if (isDirective(value)) {

@@ -245,15 +241,66 @@ value = getDirectiveValue(value, part);

if (ignoreNothingAndUndefined && (value === nothingString || value === undefined)) {
return '';
if (value === nothingString) {
return value;
}
// Pass-through template result
if (isTemplateResult(value)) {
value = value.read();
}
if (isPrimitive(value)) {
const string = typeof value !== 'string' ? String(value) : value;
// Escape if not prefixed with unsafePrefixString, otherwise strip prefix
return Buffer.from(
string.indexOf(unsafePrefixString) === 0 ? string.slice(33) : escapeHTML(string)
);
} else if (Buffer.isBuffer(value)) {
return value;
} else if (isPrimitive(value)) {
} else if (isPromise(value)) {
return value.then((value) => resolveAttributeValue(value, part));
} else if (isSyncIterator(value)) {
if (!Array.isArray(value)) {
value = Array.from(value);
}
return Buffer.concat(
value.reduce((values, value) => {
value = resolveAttributeValue(value, part);
// Flatten
if (Array.isArray(value)) {
return values.concat(value);
}
values.push(value);
return values;
}, [])
);
} else {
throw Error('unknown AttributPart value', value);
}
}
/**
* Resolve "value" to string Buffer if possible
*
* @param { any } value
* @param { NodePart } part
* @returns { any }
*/
function resolveNodeValue(value, part) {
if (isDirective(value)) {
value = getDirectiveValue(value, part);
}
if (value === nothingString || value === undefined) {
return emptyStringBuffer;
}
if (isPrimitive(value)) {
const string = typeof value !== 'string' ? String(value) : value;
// Escape if not prefixed with unsafeStringPrefix, otherwise strip prefix
return string.indexOf(unsafeStringPrefix) === 0 ? string.slice(33) : escapeHTML(string);
// Escape if not prefixed with unsafePrefixString, otherwise strip prefix
return Buffer.from(
string.indexOf(unsafePrefixString) === 0 ? string.slice(33) : escapeHTML(string)
);
} else if (isTemplateResult(value) || Buffer.isBuffer(value)) {
return value;
} else if (isPromise(value)) {
return value.then((value) => resolveValue(value, part, ignoreNothingAndUndefined));
return value.then((value) => resolveNodeValue(value, part));
} else if (isSyncIterator(value)) {

@@ -264,4 +311,4 @@ if (!Array.isArray(value)) {

return value.reduce((values, value) => {
value = resolveValue(value, part, ignoreNothingAndUndefined);
// Allow nested template results to also be flattened by not checking isTemplateResult
value = resolveNodeValue(value, part);
// Flatten
if (Array.isArray(value)) {

@@ -274,3 +321,3 @@ return values.concat(value);

} else {
return value;
throw Error('unknown NodePart value', value);
}

@@ -277,0 +324,0 @@ }

/**
* @typedef TemplateResult { import('./template-result.js).TemplateResult }
*/
import { bufferResult } from './template-result-bufferer.js';
/**
* @typedef TemplateResultProcessor { import('./default-template-result-processor.js).TemplateResultProcessor }
*/
/**
* @typedef TemplateResultRenderer { import('./default-template-result-renderer.js).TemplateResultRenderer }
*/

@@ -14,7 +19,33 @@ /**

* @param { TemplateResult } result
* @param { TemplateResultProcessor } processor
* @returns { Promise<string> }
*/
constructor(result) {
return bufferResult(result);
constructor(result, processor) {
return new Promise((resolve, reject) => {
let stack = [result];
let chunks = [];
processor.process(
{
awaitingPromise: false,
push(chunk) {
if (chunk === null) {
resolve(Buffer.concat(chunks).toString());
} else {
chunks.push(chunk);
}
return true;
},
destroy(err) {
chunks.length = 0;
chunks = undefined;
stack.length = 0;
stack = undefined;
reject(err);
}
},
stack
);
});
}
}
/**
* @typedef TemplateResult { import('./template-result.js).TemplateResult }
*/
import { bufferResult } from './template-result-bufferer.js';
/**
* @typedef TemplateResultProcessor { import('./default-template-result-processor.js).TemplateResultProcessor }
*/
/**
* @typedef TemplateResultRenderer { import('./default-template-result-renderer.js).TemplateResultRenderer }
*/
import { Readable } from 'stream';
/**
* A custom Readable stream class that renders a TemplateResult
* A custom Readable stream class for rendering a template result to a stream
*
* @implements TemplateResultRenderer
*/

@@ -14,72 +21,22 @@ export class StreamTemplateRenderer extends Readable {

*
* @param { TemplateResult } result
* @param { object } [options] Readable options
* @see https://nodejs.org/api/stream.html#stream_new_stream_readable_options
* @param { TemplateResult } result - a template result returned from call to "html`...`"
* @param { TemplateResultProcessor } processor
* @returns { Readable }
*/
constructor(result, options) {
super({ autoDestroy: true, ...options });
constructor(result, processor) {
super({ autoDestroy: true });
this.canPushData = true;
this.done = false;
this.buffer = '';
this.index = 0;
bufferResult(result, this)
.then(() => {
this.done = true;
this._drainBuffer();
})
.catch((err) => {
this.destroy(err);
});
this.awaitingPromise = false;
this.processor = processor;
this.stack = [result];
}
/**
* Push "chunk" onto buffer
* (Called by "bufferResult" utility)
*
* @param { string } chunk
*/
bufferChunk(chunk) {
this.buffer += chunk;
this._drainBuffer();
}
/**
* Extend Readable.read()
*/
_read() {
this.canPushData = true;
this._drainBuffer();
this.processor.process(this, this.stack);
}
/**
* Write all buffered content to stream.
* Returns "false" if write triggered backpressure, otherwise "true".
*
* @returns { boolean }
*/
_drainBuffer() {
if (!this.canPushData) {
return false;
}
const bufferLength = this.buffer.length;
if (this.index < bufferLength) {
// Strictly speaking we shouldn't compare character length with byte length, but close enough
const length = Math.min(bufferLength - this.index, this.readableHighWaterMark);
const chunk = this.buffer.slice(this.index, this.index + length);
this.canPushData = this.push(chunk, 'utf8');
this.index += length;
} else if (this.done) {
this.push(null);
}
return this.canPushData;
}
/**
* Extend Readalbe.destroy()

@@ -90,6 +47,2 @@ *

_destroy(err) {
if (this.done) {
return;
}
if (err) {

@@ -100,8 +53,7 @@ this.emit('error', err);

this.canPushData = false;
this.done = true;
this.buffer = '';
this.index = 0;
this.stack.length = 0;
this.stack = [];
this.processor = null;
this.removeAllListeners();
}
}

@@ -0,31 +1,183 @@

import { AttributePart } from './parts.js';
import { emptyStringBuffer } from './string.js';
import { isPromise } from './is.js';
const pool = [];
let id = 0;
/**
* @typedef { Array<string|Promise<any>> } TemplateResult - an array of template strings (or Promises) and their resolved values
* @property { boolean } isTemplateResult
* Retrieve TemplateResult instance.
* Uses an object pool to recycle instances.
*
* @param { Template } template
* @param { Array<any> } values
* @returns { TemplateResult }
*/
export function templateResult(template, values) {
let instance = pool.pop();
if (instance) {
instance.template = template;
instance.values = values;
} else {
instance = new TemplateResult(template, values);
}
return instance;
}
/**
* @typedef TemplateResultProcessor { import('./default-template-result-processor.js').TemplateResultProcessor }
*/
/**
* Determine if 'obj' is a template result
* Determine whether "result" is a TemplateResult
*
* @param { any } obj
* @param { TemplateResult } result
* @returns { boolean }
*/
export function isTemplateResult(obj) {
return Array.isArray(obj) && obj.isTemplateResult;
export function isTemplateResult(result) {
return result instanceof TemplateResult;
}
/**
* Reduce a Template's strings and values to an array of resolved strings (or Promises)
*
* @param { Template } template
* @param { Array<any> } values
* @param { TemplateResultProcessor } processor
* @returns { TemplateResult }
* A class for consuming the combined static and dynamic parts of a lit-html Template.
* TemplateResults
*/
export function TemplateResult(template, values, processor) {
const result = processor.processTemplate(template, values);
class TemplateResult {
/**
* Constructor
*
* @param { Template } template
* @param { Array<any> } values
*/
constructor(template, values) {
this.template = template;
this.values = values;
this.id = id++;
this.index = 0;
}
result.isTemplateResult = true;
return result;
/**
* Consume template result content.
* *Note* that instances may only be read once,
* and will be destroyed upon completion.
*
* @param { boolean } deep - recursively resolve nested TemplateResults
* @returns { any }
*/
read(deep) {
let buffer = emptyStringBuffer;
let chunk, chunks;
while ((chunk = this.readChunk()) !== null) {
if (Buffer.isBuffer(chunk)) {
buffer = Buffer.concat([buffer, chunk]);
} else {
if (chunks === undefined) {
chunks = [];
}
buffer = reduce(buffer, chunks, chunk, deep);
}
}
if (chunks !== undefined) {
chunks.push(buffer);
return chunks.length > 1 ? chunks : chunks[0];
}
return buffer;
}
/**
* Consume template result content one chunk at a time.
* *Note* that instances may only be read once,
* and will be destroyed when the last chunk is read.
*
* @returns { any }
*/
readChunk() {
const isString = this.index % 2 === 0;
const index = (this.index / 2) | 0;
// Finished
if (!isString && index >= this.template.strings.length - 1) {
this.destroy();
return null;
}
this.index++;
if (isString) {
return this.template.strings[index];
}
const part = this.template.parts[index];
let value;
if (part instanceof AttributePart) {
// AttributeParts can have multiple values, so slice based on length
// (strings in-between values are already handled the instance)
if (part.length > 1) {
value = part.getValue(this.values.slice(index, index + part.length));
this.index += part.length;
} else {
value = part.getValue([this.values[index]]);
}
} else {
value = part.getValue(this.values[index]);
}
return value;
}
/**
* Destroy the instance,
* returning it to the object pool
*
* @param { boolean } permanent - permanently destroy instance and it's children
* @returns { void }
*/
destroy(permanent) {
if (this.values !== undefined) {
if (permanent) {
for (const value of this.values) {
if (isTemplateResult(value)) {
value.destroy(permanent);
}
}
}
this.values.length = 0;
}
this.values = undefined;
this.template = undefined;
this.index = 0;
if (!permanent) {
pool.push(this);
}
}
}
/**
* Commit "chunk" to string "buffer".
* Returns new "buffer" value.
*
* @param { Buffer } buffer
* @param { Array<any> } chunks
* @param { any } chunk
* @param { boolean } [deep]
* @returns { Buffer }
*/
function reduce(buffer, chunks, chunk, deep = false) {
if (Buffer.isBuffer(chunk)) {
return Buffer.concat([buffer, chunk]);
} else if (isTemplateResult(chunk)) {
if (deep) {
return reduce(buffer, chunks, chunk.read(deep), deep);
} else {
chunks.push(buffer, chunk);
return emptyStringBuffer;
}
} else if (Array.isArray(chunk)) {
return chunk.reduce((buffer, chunk) => reduce(buffer, chunks, chunk), buffer);
} else if (isPromise(chunk)) {
chunks.push(buffer, chunk);
return emptyStringBuffer;
}
}
/**
* @typedef TemplateProcessor { import('./default-template-processor.js').TemplateProcessor }
*/
import { emptyStringBuffer } from './string.js';
import { lastAttributeNameRegex } from 'lit-html/lib/template.js';

@@ -66,3 +67,3 @@

// Store any text between quote character and value
const attributeStrings = [suffix.slice(matchQuote.index + 1)];
const attributeStrings = [Buffer.from(suffix.slice(matchQuote.index + 1))];
let open = true;

@@ -78,6 +79,6 @@ skip = 0;

if (closingQuoteIndex === -1) {
attributeStrings.push(attributeString);
attributeStrings.push(Buffer.from(attributeString));
skip++;
} else {
attributeStrings.push(attributeString.slice(0, closingQuoteIndex));
attributeStrings.push(Buffer.from(attributeString.slice(0, closingQuoteIndex)));
nextString = attributeString.slice(closingQuoteIndex + 1);

@@ -91,3 +92,6 @@ i += skip;

} else {
part = processor.handleAttributeExpressions(name, ['', '']);
part = processor.handleAttributeExpressions(name, [
emptyStringBuffer,
emptyStringBuffer
]);
}

@@ -99,7 +103,7 @@ }

this.strings.push(string);
this.strings.push(Buffer.from(string));
this.parts.push(part);
// Add placehholders for strings/parts that wil be skipped due to multple values in a single AttributePart
if (skip > 0) {
this.strings.push('');
this.strings.push(null);
this.parts.push(null);

@@ -110,3 +114,3 @@ skip = 0;

this.strings.push(nextString);
this.strings.push(Buffer.from(nextString));
}

@@ -113,0 +117,0 @@ }

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc