Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(v2): q-ignore and q-container-island implementation #6721

Merged
merged 1 commit into from
Jul 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading