/** * Copyright 2018 The Incremental DOM Authors. All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS-IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import { DEBUG } from "./global"; import { NameOrCtorDef } from "./types"; /** * Keeps track whether or not we are in an attributes declaration (after * elementOpenStart, but before elementOpenEnd). */ let inAttributes = false; /** * Keeps track whether or not we are in an element that should not have its * children cleared. */ let inSkip = false; /** * Keeps track of whether or not we are in a patch. */ let inPatch = false; /** * Asserts that a value exists and is not null or undefined. goog.asserts * is not used in order to avoid dependencies on external code. * @param val The value to assert is truthy. * @returns The value. */ function assert(val: T | null | undefined): T { if (DEBUG && !val) { throw new Error("Expected value to be defined"); } // eslint-disable-next-line @typescript-eslint/no-non-null-assertion return val!; } /** * Makes sure that there is a current patch context. * @param functionName The name of the caller, for the error message. */ function assertInPatch(functionName: string) { if (!inPatch) { throw new Error("Cannot call " + functionName + "() unless in patch."); } } /** * Makes sure that a patch closes every node that it opened. * @param openElement * @param root */ function assertNoUnclosedTags( openElement: Node | null, root: Node | DocumentFragment ) { if (openElement === root) { return; } let currentElement = openElement; const openTags: Array = []; while (currentElement && currentElement !== root) { openTags.push(currentElement.nodeName.toLowerCase()); currentElement = currentElement.parentNode; } throw new Error("One or more tags were not closed:\n" + openTags.join("\n")); } /** * Makes sure that node being outer patched has a parent node. * @param parent */ function assertPatchOuterHasParentNode(parent: Node | null) { if (!parent) { console.warn( "patchOuter requires the node have a parent if there is a key." ); } } /** * Makes sure that the caller is not where attributes are expected. * @param functionName The name of the caller, for the error message. */ function assertNotInAttributes(functionName: string) { if (inAttributes) { throw new Error( functionName + "() can not be called between " + "elementOpenStart() and elementOpenEnd()." ); } } /** * Makes sure that the caller is not inside an element that has declared skip. * @param functionName The name of the caller, for the error message. */ function assertNotInSkip(functionName: string) { if (inSkip) { throw new Error( functionName + "() may not be called inside an element " + "that has called skip()." ); } } /** * Makes sure that the caller is where attributes are expected. * @param functionName The name of the caller, for the error message. */ function assertInAttributes(functionName: string) { if (!inAttributes) { throw new Error( functionName + "() can only be called after calling " + "elementOpenStart()." ); } } /** * Makes sure the patch closes virtual attributes call */ function assertVirtualAttributesClosed() { if (inAttributes) { throw new Error( "elementOpenEnd() must be called after calling " + "elementOpenStart()." ); } } /** * Makes sure that tags are correctly nested. * @param currentNameOrCtor * @param nameOrCtor */ function assertCloseMatchesOpenTag( currentNameOrCtor: NameOrCtorDef, nameOrCtor: NameOrCtorDef ) { if (currentNameOrCtor !== nameOrCtor) { throw new Error( 'Received a call to close "' + nameOrCtor + '" but "' + currentNameOrCtor + '" was open.' ); } } /** * Makes sure that no children elements have been declared yet in the current * element. * @param functionName The name of the caller, for the error message. * @param previousNode */ function assertNoChildrenDeclaredYet( functionName: string, previousNode: Node | null ) { if (previousNode !== null) { throw new Error( functionName + "() must come before any child " + "declarations inside the current element." ); } } /** * Checks that a call to patchOuter actually patched the element. * @param maybeStartNode The value for the currentNode when the patch * started. * @param maybeCurrentNode The currentNode when the patch finished. * @param expectedNextNode The Node that is expected to follow the * currentNode after the patch; * @param expectedPrevNode The Node that is expected to preceed the * currentNode after the patch. */ function assertPatchElementNoExtras( maybeStartNode: Node | null, maybeCurrentNode: Node | null, expectedNextNode: Node | null, expectedPrevNode: Node | null ) { const startNode = assert(maybeStartNode); const currentNode = assert(maybeCurrentNode); const wasUpdated = currentNode.nextSibling === expectedNextNode && currentNode.previousSibling === expectedPrevNode; const wasChanged = currentNode.nextSibling === startNode.nextSibling && currentNode.previousSibling === expectedPrevNode; const wasRemoved = currentNode === startNode; if (!wasUpdated && !wasChanged && !wasRemoved) { throw new Error( "There must be exactly one top level call corresponding " + "to the patched element." ); } } /** * @param newContext The current patch context. */ function updatePatchContext(newContext: {} | null) { inPatch = newContext != null; } /** * Updates the state of being in an attribute declaration. * @param value Whether or not the patch is in an attribute declaration. * @return the previous value. */ function setInAttributes(value: boolean) { const previous = inAttributes; inAttributes = value; return previous; } /** * Updates the state of being in a skip element. * @param value Whether or not the patch is skipping the children of a * parent node. * @return the previous value. */ function setInSkip(value: boolean) { const previous = inSkip; inSkip = value; return previous; } export { assert, assertInPatch, assertNoUnclosedTags, assertNotInAttributes, assertInAttributes, assertCloseMatchesOpenTag, assertVirtualAttributesClosed, assertNoChildrenDeclaredYet, assertNotInSkip, assertPatchElementNoExtras, assertPatchOuterHasParentNode, setInAttributes, setInSkip, updatePatchContext };