UNPKG

marko

Version:

UI Components + streaming, async, high performance, HTML templating for Node.js and the browser.

832 lines (691 loc) 19.2 kB
"use strict"; var EventEmitter = require("events-light"); var selfClosingTags = require("self-closing-tags"); var RenderResult = require("../RenderResult"); var parseHTML = require("../vdom/parse-html"); var BufferedWriter = require("./BufferedWriter"); var attrsHelper = require("./helpers/attrs"); var markoAttr = require("./helpers/data-marko"); var escapeXmlHelper = require("./helpers/escape-xml"); var StringWriter = require("./StringWriter"); var escapeXmlOrNullish = escapeXmlHelper.x; var escapeXmlString = escapeXmlHelper.___escapeXML; var missingSetTimeout = typeof setTimeout !== "function"; function noop() {} var voidWriter = { write: noop, script: noop, merge: noop, clear: noop, get: function () { return []; }, toString: function () { return ""; }, }; function State(root, stream, writer, events) { this.root = root; this.stream = stream; this.writer = writer; this.events = events; this.finished = false; } function escapeEndingComment(text) { return text.replace(/(--!?)>/g, "$1&gt;"); } function deferred() { let resolve; let reject; const promise = new Promise((res, rej) => { resolve = res; reject = rej; }); return { promise, resolve, reject }; } function AsyncStream(global, writer, parentOut) { if (parentOut === null) { throw new Error("illegal state"); } var finalGlobal = (this.attributes = global || {}); var originalStream; var state; if (parentOut) { state = parentOut._state; originalStream = state.stream; } else { var events = (finalGlobal.events /* deprecated */ = writer && writer.on ? writer : new EventEmitter()); if (writer) { originalStream = writer; writer = new BufferedWriter(writer); } else { writer = originalStream = new StringWriter(); } state = new State(this, originalStream, writer, events); writer.state = state; } finalGlobal.runtimeId = finalGlobal.runtimeId || "M"; this.global = finalGlobal; this.stream = originalStream; this._state = state; this._ended = false; this._remaining = 1; this._lastCount = 0; this._last = undefined; // Array this._parentOut = parentOut; this.data = {}; this.writer = writer; writer.stream = this; this._sync = false; this._stack = undefined; this.name = undefined; this._timeoutId = undefined; this._node = undefined; this._elStack = undefined; // Array this.___components = null; // ComponentsContext this.___assignedComponentDef = null; this.___assignedKey = null; this.___assignedCustomEvents = null; this.___isLast = false; } AsyncStream.DEFAULT_TIMEOUT = 10000; /** * If set to `true`, AsyncStream errors will include the full stack trace */ AsyncStream.INCLUDE_STACK = typeof process !== "undefined" && (!process.env.NODE_ENV || process.env.NODE_ENV === "development" || process.env.NODE_ENV === "dev"); AsyncStream.enableAsyncStackTrace = function () { AsyncStream.INCLUDE_STACK = true; }; var proto = (AsyncStream.prototype = { constructor: AsyncStream, ___host: typeof document === "object" && document, ___isOut: true, [Symbol.asyncIterator]() { if (this.___iterator) { return this.___iterator; } const originalWriter = this._state.writer; let buffer = ""; let iteratorNextFn; if (!originalWriter.stream) { // Writing has finished completely so we can use a simple iterator buffer = this.toString(); iteratorNextFn = () => { const value = buffer; buffer = ""; return { value, done: !value }; }; } else { let done = false; let error = noop; // Used as a identity object reference. let pending; const stream = { write(data) { buffer += data; }, end() { done = true; if (pending) { pending.resolve( buffer ? { value: buffer, done: false, } : { value: undefined, done: true, }, ); pending = undefined; } }, flush() { if (pending) { pending.resolve({ value: buffer, done: false, }); buffer = ""; pending = undefined; } }, }; this.once("error", (err) => { error = err; if (pending) { pending.reject(error); pending = undefined; } }); const writer = new BufferedWriter(stream); writer.stream = originalWriter.stream; writer.stream.writer = writer; writer.next = originalWriter.next; writer.state = this._state; writer.merge(originalWriter); writer.scheduleFlush(); this._state.stream = stream; this._state.writer = writer; iteratorNextFn = async () => { if (error !== noop) { throw error; } else if (buffer) { const value = buffer; buffer = ""; return { value, done: false }; } else if (done) { return { value: undefined, done: true }; } if (!pending) { pending = deferred(); } return pending.promise; }; } return (this.___iterator = { next: iteratorNextFn, return: iteratorReturnFn, throw: iteratorReturnFn, [Symbol.asyncIterator]() { return this; }, }); }, toReadable() { let cancelled = false; return new ReadableStream({ start: async (ctrl) => { const encoder = new TextEncoder(); try { for await (const chunk of this) { if (cancelled) { return; } ctrl.enqueue(encoder.encode(chunk)); } ctrl.close(); } catch (err) { if (!cancelled) { ctrl.error(err); } } }, cancel() { cancelled = true; }, }); }, sync: function () { this._sync = true; }, isSync: function () { return this._sync === true; }, write: function (str) { if (str != null) { this.writer.write(str.toString()); } return this; }, script: function (str) { if (str != null) { this.writer.script(str.toString()); } return this; }, ___getOutput: function () { return this._state.writer.toString(); }, /** * Legacy... */ getOutput: function () { return this.___getOutput(); }, toString: function () { return this._state.writer.toString(); }, ___getResult: function () { this._result = this._result || new RenderResult(this); return this._result; }, beginAsync: function (options) { if (this._sync) { throw new Error("beginAsync() not allowed when using renderSync()"); } var state = this._state; var currentWriter = this.writer; /* ┏━━━━━┓ this ┃ WAS ┃ ↓↑ ┗━━━━━┛ prevWriter → currentWriter → nextWriter */ var newWriter = new StringWriter(); var newStream = new AsyncStream(this.global, currentWriter, this); newWriter.state = state; this.writer = newWriter; newWriter.stream = this; newWriter.next = currentWriter.next; currentWriter.next = newWriter; /* ┏━━━━━┓ newStream this ┃ NOW ┃ ↓↑ ↓↑ ┗━━━━━┛ prevWriter → currentWriter → newWriter → nextWriter */ var timeout; var name; this._remaining++; if (options != null) { if (typeof options === "number") { timeout = options; } else { timeout = options.timeout; if (options.last === true) { if (timeout == null) { // Don't assign a timeout to last flush fragments // unless it is explicitly given a timeout timeout = 0; } this._lastCount++; newStream.___isLast = true; } name = options.name; } } if (missingSetTimeout) { timeout = 0; } else if (timeout == null) { timeout = AsyncStream.DEFAULT_TIMEOUT; } newStream._stack = AsyncStream.INCLUDE_STACK ? new Error() : null; newStream.name = name; if (timeout > 0) { newStream._timeoutId = setTimeout(function () { newStream.error( new Error( "Async fragment " + (name ? "(" + name + ") " : "") + "timed out after " + timeout + "ms", ), ); }, timeout); } state.events.emit("beginAsync", { out: newStream, parentOut: this, }); return newStream; }, _doFinish: function () { var state = this._state; state.finished = true; if (state.writer.end) { state.writer.end(); } if (state.events !== state.stream) { state.events.emit("finish", this.___getResult()); } }, end: function (data) { if (this._ended === true) { return; } this._ended = true; var remaining = --this._remaining; if (data != null) { this.write(data); } var currentWriter = this.writer; /* ┏━━━━━┓ this nextStream ┃ WAS ┃ ↓↑ ↓↑ ┗━━━━━┛ currentWriter → nextWriter → futureWriter */ // Prevent any more writes to the current steam this.writer = voidWriter; currentWriter.stream = null; // Flush the contents of nextWriter to the currentWriter this._flushNext(currentWriter); /* ┏━━━━━┓ this ╵ nextStream ┃ ┃ ↓ ╵ ↓↑ ┃ NOW ┃ voidWriter ╵ currentWriter → futureWriter ┃ ┃ ──────────────┴──────────────────────────────── ┗━━━━━┛ Flushed & garbage collected: nextWriter */ var parentOut = this._parentOut; if (parentOut === undefined) { if (remaining === 0) { this._doFinish(); } else if (remaining - this._lastCount === 0) { this._emitLast(); } } else { var timeoutId = this._timeoutId; if (timeoutId) { clearTimeout(timeoutId); } if (remaining === 0) { parentOut._handleChildDone(this); } else if (remaining - this._lastCount === 0) { this._emitLast(); } } return this; }, _handleChildDone: function (childOut) { var remaining = --this._remaining; if (remaining === 0) { var parentOut = this._parentOut; if (parentOut === undefined) { this._doFinish(); } else { parentOut._handleChildDone(this); } } else { if (childOut.___isLast) { this._lastCount--; } if (remaining - this._lastCount === 0) { this._emitLast(); } } }, _flushNext: function (currentWriter) { // It is possible that currentWriter is the // last writer in the chain, so let's make // sure there is a nextWriter to flush. var nextWriter = currentWriter.next; if (nextWriter) { // Flush the contents of nextWriter // to the currentWriter currentWriter.merge(nextWriter); // Remove nextWriter from the chain. // It has been flushed and can now be // garbage collected. currentWriter.next = nextWriter.next; // It's possible that nextWriter is the last // writer in the chain and its stream already // ended, so let's make sure nextStream exists. var nextStream = nextWriter.stream; if (nextStream) { // Point the nextStream to currentWriter nextStream.writer = currentWriter; currentWriter.stream = nextStream; } } }, on: function (event, callback) { var state = this._state; if (event === "finish" && state.finished === true) { callback(this.___getResult()); } else if (event === "last") { this.onLast(callback); } else { state.events.on(event, callback); } return this; }, once: function (event, callback) { var state = this._state; if (event === "finish" && state.finished === true) { callback(this.___getResult()); } else if (event === "last") { this.onLast(callback); } else { state.events.once(event, callback); } return this; }, onLast: function (callback) { var lastArray = this._last; if (lastArray === undefined) { this._last = [callback]; } else { lastArray.push(callback); } return this; }, _emitLast: function () { if (this._last) { var i = 0; var lastArray = this._last; this._last = undefined; (function next() { if (i === lastArray.length) { return; } var lastCallback = lastArray[i++]; lastCallback(next); if (lastCallback.length === 0) { next(); } })(); } }, emit: function (type, arg) { var events = this._state.events; switch (arguments.length) { case 1: events.emit(type); break; case 2: events.emit(type, arg); break; default: events.emit.apply(events, arguments); break; } return this; }, removeListener: function () { var events = this._state.events; events.removeListener.apply(events, arguments); return this; }, prependListener: function () { var events = this._state.events; events.prependListener.apply(events, arguments); return this; }, pipe: function (stream) { this._state.stream.pipe(stream); return this; }, error: function (e) { var name = this.name; var stack = this._stack; if (stack) stack = getNonMarkoStack(stack); if (!(e instanceof Error)) { e = new Error(JSON.stringify(e)); } if (name || stack) { e.message += "\nRendered by" + (name ? " " + name : "") + (stack ? ":\n" + stack : ""); } try { this.emit("error", e); } finally { // If there is no listener for the error event then it will // throw a new here. In order to ensure that the async fragment // is still properly ended we need to put the end() in a `finally` // block this.end(); } return this; }, flush: function () { var state = this._state; if (!state.finished) { var writer = state.writer; if (writer && writer.scheduleFlush) { writer.scheduleFlush(); } } return this; }, createOut: function () { var newOut = new AsyncStream(this.global); // Forward error events to the parent out. newOut.on("error", this.emit.bind(this, "error")); this._state.events.emit("beginDetachedAsync", { out: newOut, parentOut: this, }); return newOut; }, ___elementDynamic: function ( tagName, elementAttrs, key, componentDef, props, ) { var str = "<" + tagName + markoAttr( this, componentDef, props, key && key[0] === "@" ? key : undefined, ) + attrsHelper(elementAttrs); if (selfClosingTags.voidElements.indexOf(tagName) !== -1) { str += ">"; } else if (selfClosingTags.svgElements.indexOf(tagName) !== -1) { str += "/>"; } else { str += "></" + tagName + ">"; } this.write(str); }, element: function (tagName, elementAttrs, openTagOnly) { var str = "<" + tagName + attrsHelper(elementAttrs) + ">"; if (openTagOnly !== true) { str += "</" + tagName + ">"; } this.write(str); }, ___beginElementDynamic: function ( name, elementAttrs, key, componentDef, props, ) { var str = "<" + name + markoAttr(this, componentDef, props, key) + attrsHelper(elementAttrs) + ">"; this.write(str); if (this._elStack) { this._elStack.push(name); } else { this._elStack = [name]; } }, beginElement: function (name, elementAttrs) { var str = "<" + name + attrsHelper(elementAttrs) + ">"; this.write(str); if (this._elStack) { this._elStack.push(name); } else { this._elStack = [name]; } }, endElement: function () { var tagName = this._elStack.pop(); this.write("</" + tagName + ">"); }, comment: function (str) { this.write("<!--" + escapeEndingComment(str) + "-->"); }, text: function (str) { this.write(escapeXmlOrNullish(str)); }, bf: function (key, component, preserve) { if (preserve) { this.write("<!--F#" + escapeXmlString(key) + "-->"); } if (this._elStack) { this._elStack.push(preserve); } else { this._elStack = [preserve]; } }, ef: function () { var preserve = this._elStack.pop(); if (preserve) { this.write("<!--F/-->"); } }, ___getNode: function (host) { var node = this._node; if (!node) { var nextEl; var fragment; var html = this.___getOutput(); if (!host) host = this.___host; var doc = host.ownerDocument || host; if (html) { node = parseHTML(html); if (node && node.nextSibling) { // If there are multiple nodes, turn it into a document fragment. fragment = doc.createDocumentFragment(); do { nextEl = node.nextSibling; fragment.appendChild(node); } while ((node = nextEl)); node = fragment; } } // if HTML is empty use empty document fragment (so that we're returning a valid DOM node) this._node = node || doc.createDocumentFragment(); } return node; }, then: function (fn, fnErr) { // eslint-disable-next-line @typescript-eslint/no-this-alias var out = this; return new Promise(function (resolve, reject) { out.on("error", reject); out.on("finish", function (result) { resolve(result); }); }).then(fn, fnErr); }, catch: function (fnErr) { return this.then(undefined, fnErr); }, finally: function (fn) { return this.then(undefined, undefined).finally(fn); }, c: function (componentDef, key, customEvents) { this.___assignedComponentDef = componentDef; this.___assignedKey = key; this.___assignedCustomEvents = customEvents; }, }); // alias: proto.w = proto.write; proto.___endElement = proto.endElement; module.exports = AsyncStream; function getNonMarkoStack(error) { return error.stack .toString() .split("\n") .slice(1) .filter((line) => !/\/node_modules\/marko\//.test(line)) .join("\n"); } function iteratorReturnFn(value) { return Promise.resolve({ value, done: true, }); }