Skip to content

Commit

Permalink
feat(v2): q-ignore and q-container-island implementation (#6721)
Browse files Browse the repository at this point in the history
  • Loading branch information
Varixo authored Jul 24, 2024
1 parent 37bfb74 commit ea2a48c
Show file tree
Hide file tree
Showing 6 changed files with 139 additions and 44 deletions.
4 changes: 4 additions & 0 deletions packages/qwik/src/core/util/markers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
59 changes: 50 additions & 9 deletions packages/qwik/src/core/v2/client/process-vnode-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,17 @@ import type { ContainerElement, ElementVNode, QDocument } from './types';
* </div>
* <div q:container="html">...</div>
* before
* <!--q:container="ABC"-->
* <!--q:container=ABC-->
* ...
* <!--/q:container="ABC"-->
* <!--/q:container-->
* after
* <!--q:ignore=FOO-->
* ...
* <!--q:container-island=BAR-->
* <div>some interactive island</div>
* <!--/q:container-island-->
* ...
* <!--/q:ignore-->
* <textarea q:container="text">...</textarea>
* <script type="qwik/vnode">...</script>
* </body>
Expand All @@ -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<Element, string>());
Expand Down Expand Up @@ -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,
}

/**
Expand All @@ -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;
}
Expand Down Expand Up @@ -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 <!--${node?.nodeValue}--> 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;
Expand Down
31 changes: 31 additions & 0 deletions packages/qwik/src/core/v2/client/process-vnode-data.unit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,37 @@ describe('processVnodeData', () => {
);
});
});
it('should not ignore island inside comment q:container', () => {
const [container1] = process(`
<html q:container="paused" :>
<head :></head>
<body :>
Before
<!--q:ignore=abc-->
Foo<i>Bar!</i>
<!--q:container-island=some-id-2-->
<button :>Click</button>
<!--/q:container-island-->
Abcd<b>Abcd!</b>
<!--/q:ignore-->
<b :>After!</b>
${encodeVNode({ 2: 'G2', 4: 'FB' })}
</body>
</html>`);
expect(container1.rootVNode).toMatchVDOM(
<html {...qContainerPaused}>
<head />
<body>
{'Before'}
<button>Click</button>
<b>
{'After'}
{'!'}
</b>
</body>
</html>
);
});
});

const qContainerPaused = { 'q:container': 'paused' };
Expand Down
49 changes: 48 additions & 1 deletion packages/qwik/src/core/v2/client/vnode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,11 @@ import {
OnRenderProp,
QContainerAttr,
QContainerAttrEnd,
QContainerIsland,
QContainerIslandEnd,
QCtxAttr,
QIgnore,
QIgnoreEnd,
QScopedStyle,
QSlot,
QSlotParent,
Expand Down Expand Up @@ -1283,14 +1287,22 @@ export const fastNextSibling = (node: Node | null): Node | null => {
if (!_fastNextSibling) {
_fastNextSibling = fastGetter<typeof _fastNextSibling>(node, 'nextSibling')!;
}
if (!_fastFirstChild) {
_fastFirstChild = fastGetter<typeof _fastFirstChild>(node, 'firstChild')!;
}
while (node) {
node = _fastNextSibling.call(node);
if (node !== null) {
const type = fastNodeType(node);
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 &&
Expand All @@ -1306,6 +1318,41 @@ export const fastNextSibling = (node: Node | null): Node | null => {
return node;
};

function getNodeAfterCommentNode(
node: Node | null,
commentValue: string,
nextSibling: NonNullable<typeof _fastNextSibling>,
firstChild: NonNullable<typeof _fastFirstChild>
): 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<typeof _fastParentNode>(node, 'parentNode')!;
}
return _fastParentNode.call(node);
};

let _fastFirstChild: ((this: Node) => Node | null) | null = null;
const fastFirstChild = (node: Node | null): Node | null => {
if (!_fastFirstChild) {
Expand Down
8 changes: 4 additions & 4 deletions packages/qwik/src/core/v2/shared/vnode-data-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down
32 changes: 2 additions & 30 deletions packages/qwik/src/server/v2-ssr-container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down

0 comments on commit ea2a48c

Please sign in to comment.