diff --git a/packages/qwik/src/core/util/markers.ts b/packages/qwik/src/core/util/markers.ts index b1090f99a19..34856052594 100644 --- a/packages/qwik/src/core/util/markers.ts +++ b/packages/qwik/src/core/util/markers.ts @@ -32,6 +32,10 @@ export const QVersionAttr = 'q:version'; export const QBaseAttr = 'q:base'; export const QLocaleAttr = 'q:locale'; export const QManifestHashAttr = 'q:manifest-hash'; +export const QContainerIsland = 'q:container'; +export const QContainerIslandEnd = '/' + QContainerIsland; +export const QIgnore = 'q:ignore'; +export const QIgnoreEnd = '/' + QIgnore; export const QContainerAttr = 'q:container'; export const QContainerAttrEnd = '/' + QContainerAttr; diff --git a/packages/qwik/src/core/v2/client/process-vnode-data.ts b/packages/qwik/src/core/v2/client/process-vnode-data.ts index 679ab4e608c..597848a43af 100644 --- a/packages/qwik/src/core/v2/client/process-vnode-data.ts +++ b/packages/qwik/src/core/v2/client/process-vnode-data.ts @@ -26,10 +26,17 @@ import type { ContainerElement, ElementVNode, QDocument } from './types'; * *
...
* before - * + * * ... - * + * * after + * + * ... + * + *
some interactive island
+ * + * ... + * * * * @@ -50,6 +57,10 @@ export function processVNodeData(document: Document) { const Q_CONTAINER = 'q:container'; const Q_CONTAINER_END = '/' + Q_CONTAINER; const Q_PROPS_SEPARATOR = ':'; + const Q_IGNORE = 'q:ignore'; + const Q_IGNORE_END = '/' + Q_IGNORE; + const Q_CONTAINER_ISLAND = 'q:container-island'; + const Q_CONTAINER_ISLAND_END = '/' + Q_CONTAINER_ISLAND; const qDocument = document as QDocument; const vNodeDataMap = qDocument.qVNodeData || (qDocument.qVNodeData = new WeakMap()); @@ -83,12 +94,16 @@ export function processVNodeData(document: Document) { /////////////////////////////// const enum NodeType { - CONTAINER_MASK /* ******* */ = 0b0001, - ELEMENT /* ************** */ = 0b0010, // regular element - ELEMENT_CONTAINER /* **** */ = 0b0011, // container element need to descend into it - COMMENT_SKIP_START /* *** */ = 0b0101, // Comment but skip the content until COMMENT_SKIP_END - COMMENT_SKIP_END /* ***** */ = 0b1000, // Comment end - OTHER /* **************** */ = 0b0000, + CONTAINER_MASK /* ***************** */ = 0b00000001, + ELEMENT /* ************************ */ = 0b00000010, // regular element + ELEMENT_CONTAINER /* ************** */ = 0b00000011, // container element need to descend into it + COMMENT_SKIP_START /* ************* */ = 0b00000101, // Comment but skip the content until COMMENT_SKIP_END + COMMENT_SKIP_END /* *************** */ = 0b00001000, // Comment end + COMMENT_IGNORE_START /* *********** */ = 0b00010000, // Comment ignore, descend into children and skip the content until COMMENT_ISLAND_START + COMMENT_IGNORE_END /* ************* */ = 0b00100000, // Comment ignore end + COMMENT_ISLAND_START /* *********** */ = 0b01000001, // Comment island, count elements for parent container until COMMENT_ISLAND_END + COMMENT_ISLAND_END /* ************* */ = 0b10000000, // Comment island end + OTHER /* ************************** */ = 0b00000000, } /** @@ -108,8 +123,16 @@ export function processVNodeData(document: Document) { } } else if (nodeType === 8 /* Node.COMMENT_NODE */) { const nodeValue = node.nodeValue || ''; // nodeValue is monomorphic so it does not need fast path - if (nodeValue.startsWith(Q_CONTAINER)) { + if (nodeValue.startsWith(Q_CONTAINER_ISLAND)) { + return NodeType.COMMENT_ISLAND_START; + } else if (nodeValue.startsWith(Q_IGNORE)) { + return NodeType.COMMENT_IGNORE_START; + } else if (nodeValue.startsWith(Q_CONTAINER)) { return NodeType.COMMENT_SKIP_START; + } else if (nodeValue.startsWith(Q_CONTAINER_ISLAND_END)) { + return NodeType.COMMENT_ISLAND_END; + } else if (nodeValue.startsWith(Q_IGNORE_END)) { + return NodeType.COMMENT_IGNORE_END; } else if (nodeValue.startsWith(Q_CONTAINER_END)) { return NodeType.COMMENT_SKIP_END; } @@ -219,6 +242,24 @@ export function processVNodeData(document: Document) { container.qVNodeRefs!, prefix + ' ' ); + } else if (nodeType === NodeType.COMMENT_IGNORE_START) { + let islandNode = node; + do { + islandNode = walker.nextNode(); + if (!islandNode) { + throw new Error(`Island inside not found!`); + } + } while (getFastNodeType(islandNode) !== NodeType.COMMENT_ISLAND_START); + nextNode = null; + } else if (nodeType === NodeType.COMMENT_ISLAND_END) { + nextNode = node; + do { + nextNode = walker.nextNode(); + if (!nextNode) { + throw new Error(`Island container not closed!`); + } + } while (getFastNodeType(nextNode) !== NodeType.COMMENT_IGNORE_END); + nextNode = null; } else if (nodeType === NodeType.COMMENT_SKIP_START) { // If we are in a container, we need to skip the children. nextNode = node; diff --git a/packages/qwik/src/core/v2/client/process-vnode-data.unit.tsx b/packages/qwik/src/core/v2/client/process-vnode-data.unit.tsx index 5fd65397f32..6c0b10618fe 100644 --- a/packages/qwik/src/core/v2/client/process-vnode-data.unit.tsx +++ b/packages/qwik/src/core/v2/client/process-vnode-data.unit.tsx @@ -144,6 +144,37 @@ describe('processVnodeData', () => { ); }); }); + it('should not ignore island inside comment q:container', () => { + const [container1] = process(` + + + + Before + + FooBar! + + + + AbcdAbcd! + + After! + ${encodeVNode({ 2: 'G2', 4: 'FB' })} + + `); + expect(container1.rootVNode).toMatchVDOM( + + + + {'Before'} + + + {'After'} + {'!'} + + + + ); + }); }); const qContainerPaused = { 'q:container': 'paused' }; diff --git a/packages/qwik/src/core/v2/client/vnode.ts b/packages/qwik/src/core/v2/client/vnode.ts index 099e25b4477..3e733cc79ea 100644 --- a/packages/qwik/src/core/v2/client/vnode.ts +++ b/packages/qwik/src/core/v2/client/vnode.ts @@ -132,7 +132,11 @@ import { OnRenderProp, QContainerAttr, QContainerAttrEnd, + QContainerIsland, + QContainerIslandEnd, QCtxAttr, + QIgnore, + QIgnoreEnd, QScopedStyle, QSlot, QSlotParent, @@ -1283,6 +1287,9 @@ export const fastNextSibling = (node: Node | null): Node | null => { if (!_fastNextSibling) { _fastNextSibling = fastGetter(node, 'nextSibling')!; } + if (!_fastFirstChild) { + _fastFirstChild = fastGetter(node, 'firstChild')!; + } while (node) { node = _fastNextSibling.call(node); if (node !== null) { @@ -1290,7 +1297,12 @@ export const fastNextSibling = (node: Node | null): Node | null => { if (type === /* Node.TEXT_NODE */ 3 || type === /* Node.ELEMENT_NODE */ 1) { break; } else if (type === /* Node.COMMENT_NODE */ 8) { - if (node.nodeValue?.startsWith(QContainerAttr)) { + const nodeValue = node.nodeValue; + if (nodeValue?.startsWith(QIgnore)) { + return getNodeAfterCommentNode(node, QContainerIsland, _fastNextSibling, _fastFirstChild); + } else if (node.nodeValue?.startsWith(QContainerIslandEnd)) { + return getNodeAfterCommentNode(node, QIgnoreEnd, _fastNextSibling, _fastFirstChild); + } else if (nodeValue?.startsWith(QContainerAttr)) { while (node && (node = _fastNextSibling.call(node))) { if ( fastNodeType(node) === /* Node.COMMENT_NODE */ 8 && @@ -1306,6 +1318,41 @@ export const fastNextSibling = (node: Node | null): Node | null => { return node; }; +function getNodeAfterCommentNode( + node: Node | null, + commentValue: string, + nextSibling: NonNullable, + firstChild: NonNullable +): Node | null { + while (node) { + if (node.nodeValue?.startsWith(commentValue)) { + node = nextSibling.call(node) || null; + return node; + } + + let nextNode: Node | null = firstChild.call(node); + if (!nextNode) { + nextNode = nextSibling.call(node); + } + if (!nextNode) { + nextNode = fastParentNode(node); + if (nextNode) { + nextNode = nextSibling.call(nextNode); + } + } + node = nextNode; + } + return null; +} + +let _fastParentNode: ((this: Node) => Node | null) | null = null; +const fastParentNode = (node: Node): Node | null => { + if (!_fastParentNode) { + _fastParentNode = fastGetter(node, 'parentNode')!; + } + return _fastParentNode.call(node); +}; + let _fastFirstChild: ((this: Node) => Node | null) | null = null; const fastFirstChild = (node: Node | null): Node | null => { if (!_fastFirstChild) { diff --git a/packages/qwik/src/core/v2/shared/vnode-data-types.ts b/packages/qwik/src/core/v2/shared/vnode-data-types.ts index c4d4ed49f8e..f850b0ba94a 100644 --- a/packages/qwik/src/core/v2/shared/vnode-data-types.ts +++ b/packages/qwik/src/core/v2/shared/vnode-data-types.ts @@ -36,10 +36,10 @@ export const VNodeDataSeparator = { ADVANCE_1024: /* ****** */ 43, // `+` is vNodeData separator skipping 512. ADVANCE_2048_CH: /* * */ ',', // ',' is vNodeData separator skipping 1024. ADVANCE_2048: /* ****** */ 44, // ',' is vNodeData separator skipping 1024. - ADVANCE_4096_CH: /* * */ `-`, // `.` is vNodeData separator skipping 2048. - ADVANCE_4096: /* ****** */ 45, // `.` is vNodeData separator skipping 2048. - ADVANCE_8192_CH: /* * */ `.`, // `/` is vNodeData separator skipping 4096. - ADVANCE_8192: /* ****** */ 46, // `/` is vNodeData separator skipping 4096. + ADVANCE_4096_CH: /* * */ `-`, // `-` is vNodeData separator skipping 2048. + ADVANCE_4096: /* ****** */ 45, // `-` is vNodeData separator skipping 2048. + ADVANCE_8192_CH: /* * */ `.`, // `.` is vNodeData separator skipping 4096. + ADVANCE_8192: /* ****** */ 46, // `.` is vNodeData separator skipping 4096. }; /** VNodeDataChar contains information about the VNodeData used for encoding props */ diff --git a/packages/qwik/src/server/v2-ssr-container.ts b/packages/qwik/src/server/v2-ssr-container.ts index 6c2729cb0d4..e1f51ab5eea 100644 --- a/packages/qwik/src/server/v2-ssr-container.ts +++ b/packages/qwik/src/server/v2-ssr-container.ts @@ -570,36 +570,8 @@ class SSRContainer extends _SharedContainer implements ISSRContainer { * - `~` Store as reference for data deserialization. * - `!"#$%&'()*+'-./` are separators (sequential characters in ASCII table) * - * ## Attribute encoding: - * - * - `;` - `q:sstyle` - Style attribute. - * - `<` - `q:renderFn' - Component QRL render function (body) - * - `=` - `q:id` - ID of the element. - * - `>` - `q:props' - Component QRL Props - * - `?` - `q:sref` - Slot reference. - * - `@` - `q:key` - Element key. - * - `[` - `q:seq' - Seq value from `useSequentialScope()` - * - `\` - SKIP because `\` is used as escaping - * - `]` - `q:ctx' - Component context/props - * - `~` - `q:slot' - Slot name - * - * ## Separator Encoding: - * - * - `~` is a reference to the node. Save it. - * - `!` is vNodeData separator skipping 0. (ie next vNode) - * - `"` is vNodeData separator skipping 1. - * - `#` is vNodeData separator skipping 2. - * - `$` is vNodeData separator skipping 4. - * - `%` is vNodeData separator skipping 8. - * - `&` is vNodeData separator skipping 16. - * - `'` is vNodeData separator skipping 32. - * - `(` is vNodeData separator skipping 64. - * - `)` is vNodeData separator skipping 128. - * - `*` is vNodeData separator skipping 256. - * - `+` is vNodeData separator skipping 512. - * - `'` is vNodeData separator skipping 1024. - * - `.` is vNodeData separator skipping 2048. - * - `/` is vNodeData separator skipping 4096. + * Attribute and separators encoding described here: + * `packages/qwik/src/core/v2/shared/vnode-data-types.ts` * * NOTE: Not every element will need vNodeData. So we need to encode how many elements should be * skipped. By choosing different separators we can encode different numbers of elements to skip.