Skip to content

Commit 652e871

Browse files
committed
fix: allow special characters in key attribute
1 parent 8602e6c commit 652e871

File tree

4 files changed

+41
-6
lines changed

4 files changed

+41
-6
lines changed

.changeset/thirty-carrots-stand.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@qwik.dev/core': patch
3+
---
4+
5+
fix: allow special characters in key attribute

packages/qwik/src/core/client/vnode.ts

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1549,17 +1549,21 @@ const processVNodeData = (
15491549
peek: () => number,
15501550
consumeValue: () => string,
15511551
consume: () => number,
1552+
getChar: (idx: number) => number,
15521553
nextToConsumeIdx: number
15531554
) => void
15541555
) => {
15551556
let nextToConsumeIdx = 0;
15561557
let ch = 0;
15571558
let peekCh = 0;
1559+
const getChar = (idx: number) => {
1560+
return idx < vData.length ? vData.charCodeAt(idx) : 0;
1561+
};
15581562
const peek = () => {
15591563
if (peekCh !== 0) {
15601564
return peekCh;
15611565
} else {
1562-
return (peekCh = nextToConsumeIdx < vData.length ? vData.charCodeAt(nextToConsumeIdx) : 0);
1566+
return (peekCh = getChar(nextToConsumeIdx));
15631567
}
15641568
};
15651569
const consume = () => {
@@ -1584,7 +1588,7 @@ const processVNodeData = (
15841588
};
15851589

15861590
while (peek() !== 0) {
1587-
callback(peek, consumeValue, consume, nextToConsumeIdx);
1591+
callback(peek, consumeValue, consume, getChar, nextToConsumeIdx);
15881592
}
15891593
};
15901594

@@ -1835,7 +1839,7 @@ function materializeFromVNodeData(
18351839
let combinedText: string | null = null;
18361840
let container: ClientContainer | null = null;
18371841

1838-
processVNodeData(vData, (peek, consumeValue, consume, nextToConsumeIdx) => {
1842+
processVNodeData(vData, (peek, consumeValue, consume, getChar, nextToConsumeIdx) => {
18391843
if (isNumber(peek())) {
18401844
// Element counts get encoded as numbers.
18411845
while (
@@ -1876,7 +1880,16 @@ function materializeFromVNodeData(
18761880
} else if (peek() === VNodeDataChar.PROPS) {
18771881
vnode_setAttr(null, vParent, ELEMENT_PROPS, consumeValue());
18781882
} else if (peek() === VNodeDataChar.KEY) {
1879-
vnode_setAttr(null, vParent, ELEMENT_KEY, consumeValue());
1883+
const isEscapedValue = getChar(nextToConsumeIdx + 1) === VNodeDataChar.SEPARATOR;
1884+
let value;
1885+
if (isEscapedValue) {
1886+
consume();
1887+
value = decodeURI(consumeValue());
1888+
consume();
1889+
} else {
1890+
value = consumeValue();
1891+
}
1892+
vnode_setAttr(null, vParent, ELEMENT_KEY, value);
18801893
} else if (peek() === VNodeDataChar.SEQ) {
18811894
vnode_setAttr(null, vParent, ELEMENT_SEQ, consumeValue());
18821895
} else if (peek() === VNodeDataChar.SEQ_IDX) {

packages/qwik/src/core/tests/component.spec.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2811,7 +2811,13 @@ describe.each([
28112811
const Cmp = component$(() => {
28122812
const toggle = useSignal(true);
28132813

2814-
const places = ['Beaupré, Canada'];
2814+
const places = [
2815+
'Beaupré, Canada',
2816+
'Łódź, Poland',
2817+
'北京, China',
2818+
'|, Separator',
2819+
'||, Double Separator',
2820+
];
28152821
return (
28162822
<div>
28172823
<button

packages/qwik/src/server/ssr-container.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -702,6 +702,7 @@ class SSRContainer extends _SharedContainer implements ISSRContainer {
702702
for (let i = 0; i < fragmentAttrs.length; ) {
703703
const key = fragmentAttrs[i++] as string;
704704
let value = fragmentAttrs[i++] as string;
705+
let encodeValue = false;
705706
// if (key !== DEBUG_TYPE) continue;
706707
if (typeof value !== 'string') {
707708
const rootId = addRoot(value);
@@ -725,6 +726,7 @@ class SSRContainer extends _SharedContainer implements ISSRContainer {
725726
write(VNodeDataChar.PROPS_CHAR);
726727
break;
727728
case ELEMENT_KEY:
729+
encodeValue = true;
728730
write(VNodeDataChar.KEY_CHAR);
729731
break;
730732
case ELEMENT_SEQ:
@@ -751,7 +753,16 @@ class SSRContainer extends _SharedContainer implements ISSRContainer {
751753
write(key);
752754
write(VNodeDataChar.SEPARATOR_CHAR);
753755
}
754-
write(value);
756+
const encodedValue = encodeValue ? encodeURI(value) : value;
757+
const isEncoded = encodeValue ? encodedValue !== value : false;
758+
if (isEncoded) {
759+
// add separator only before and after the encoded value
760+
write(VNodeDataChar.SEPARATOR_CHAR);
761+
write(encodedValue);
762+
write(VNodeDataChar.SEPARATOR_CHAR);
763+
} else {
764+
write(value);
765+
}
755766
}
756767
}
757768

0 commit comments

Comments
 (0)