Skip to content

Commit 671373b

Browse files
committed
Shady Parts: Parsing and formatting functions
Part of #252
1 parent 375397a commit 671373b

File tree

2 files changed

+261
-0
lines changed

2 files changed

+261
-0
lines changed

packages/shadycss/src/shadow-parts.js

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
/**
2+
@license
3+
Copyright (c) 2020 The Polymer Project Authors. All rights reserved.
4+
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
5+
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
6+
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
7+
Code distributed by Google as part of the polymer project is also
8+
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
9+
*/
10+
11+
/**
12+
* Parse a CSS Shadow Parts "part" attribute into an array of part names.
13+
*
14+
* Example:
15+
* Given: "foo bar foo "
16+
* Returns: ["foo", "bar", "foo"]
17+
*
18+
* @param {?string} str The "part" attribute value.
19+
* @return {!Array<!string>} The part names. Order and duplicates are preserved.
20+
*/
21+
export function splitPartString(str) {
22+
if (!str) {
23+
return [];
24+
}
25+
return str.trim().split(/\s+/);
26+
}
27+
28+
/**
29+
* Parse a CSS Shadow Parts "exportparts" attribute into an array of inner/outer
30+
* part mappings.
31+
*
32+
* Example:
33+
* Given: "foo, bar:baz"
34+
* Returns: [
35+
* {inner:"foo", outer:"foo"},
36+
* {inner:"bar", outer:"baz"},
37+
* ]
38+
*
39+
* @param {?string} attr The "exportparts" attribute value.
40+
* @return {!Array<{inner: !string, outer: !string}>} The inner/outer mapping.
41+
* Order and duplicates are preserved.
42+
*/
43+
export function parseExportPartsAttribute(attr) {
44+
if (!attr) {
45+
return [];
46+
}
47+
const parts = [];
48+
for (const part of attr.split(/\s*,\s*/)) {
49+
const split = part.split(/\s*:\s*/);
50+
let inner, outer;
51+
if (split.length === 1) {
52+
inner = outer = split[0];
53+
} else if (split.length === 2) {
54+
inner = split[0];
55+
outer = split[1];
56+
} else {
57+
continue;
58+
}
59+
parts.push({ inner, outer });
60+
}
61+
return parts;
62+
}
63+
64+
/**
65+
* Regular expression to de-compose a ::part rule into interesting pieces. See
66+
* parsePartSelector for description of pieces.
67+
* [0 ][1 ][2 ] [3 ] [4 ]
68+
*/
69+
const PART_REGEX = /(.*?)([a-z]+-\w+)([^\s]*?)::part\((.*)?\)(::.*)?/;
70+
71+
/**
72+
* De-compose a ::part rule into interesting pieces.
73+
*
74+
* [0] combinators: Optional combinator selectors constraining the receiving
75+
* host.
76+
* [1] elementName: Required custom element name of the receiving host. Note
77+
* that ShadyCSS part support requires there to be an explicit custom
78+
* element name here, unlike native parts.
79+
* [2] selectors: Optional additional selectors constraining the receiving host.
80+
* [3] parts: The part name or names (whitespace-separated, this function does
81+
* not split them).
82+
* [4] pseudos: Optional pseudo-classes or pseudo-elements of the part.
83+
*
84+
* TODO(aomarks) Actually only "non-structural" pseudo-classes and
85+
* pseudo-elements are supported here. We should validate them to be more
86+
* spec-compliant.
87+
*
88+
* Example:
89+
* [0 ][1 ][2 ] [3 ] [4 ]
90+
* #parent > my-button.fancy::part(foo bar)::hover
91+
*
92+
* @param {!string} selector The selector.
93+
* @return {?{
94+
* combinators: !string,
95+
* elementName: !string,
96+
* selectors: !string,
97+
* parts: !string,
98+
* pseudos: !string
99+
* }}
100+
*/
101+
export function parsePartSelector(selector) {
102+
const match = selector.match(PART_REGEX);
103+
if (match === null) {
104+
return null;
105+
}
106+
const [, combinators, elementName, selectors, parts, pseudos] = match;
107+
return { combinators, elementName, selectors, parts, pseudos: pseudos || "" };
108+
}
109+
110+
/**
111+
* Format the shady-part attribute value for a part.
112+
*
113+
* Example:
114+
* Given: "x-app", "x-btn", "prt1"
115+
* Returns: "x-app:x-btn:prt1"
116+
*
117+
* @param {!string} providerScope Lowercase name of the custom element that
118+
* provides the part style, or "document" if the style comes from the main
119+
* document.
120+
* @param {!string} receiverScope Lowercase name of the custom element that
121+
* receives the part style.
122+
* @param {!string} partName Name of the part.
123+
* @return {!string} Value for the shady-part attribute.
124+
*/
125+
export function formatShadyPartAttribute(
126+
providerScope,
127+
receiverScope,
128+
partName
129+
) {
130+
return `${providerScope}:${receiverScope}:${partName}`;
131+
}
132+
133+
/**
134+
* Format the shady-part attribute CSS selector for a part rule.
135+
*
136+
* Example:
137+
* Given: "x-app", "x-btn", "prt1 prt2"
138+
* Returns: '[shady-part~="x-app:x-btn:prt1"][shady-part~="x-app:x-btn:prt2"]'
139+
*
140+
* @param {!string} providerScope Lowercase name of the custom element that
141+
* provides the part style, or "document" if the style comes from the main
142+
* document.
143+
* @param {!string} receiverScope Lowercase name of the custom element that
144+
* receives the part style.
145+
* @param {!string} partNames Whitespace-separated part list.
146+
* @return {!string} shady-part attribute CSS selector.
147+
*/
148+
export function formatShadyPartSelector(
149+
providerScope,
150+
receiverScope,
151+
partNames
152+
) {
153+
return splitPartString(partNames)
154+
.map((partName) => {
155+
const attr = formatShadyPartAttribute(
156+
providerScope,
157+
receiverScope,
158+
partName
159+
);
160+
return `[shady-part~="${attr}"]`;
161+
})
162+
.join("");
163+
}
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
<!DOCTYPE html>
2+
<head>
3+
<script>
4+
WCT = {
5+
waitFor: function (cb) {
6+
HTMLImports.whenReady(cb);
7+
},
8+
};
9+
</script>
10+
<script src="../test-flags.js"></script>
11+
<script src="../../node_modules/@webcomponents/html-imports/html-imports.min.js"></script>
12+
<script src="../../node_modules/wct-browser-legacy/browser.js"></script>
13+
</head>
14+
<body>
15+
<script type="module">
16+
import * as shadowParts from "../../node_modules/@webcomponents/shadycss/src/shadow-parts.js";
17+
18+
suite("shadow parts parsing and formatting", () => {
19+
test("splitPartString", () => {
20+
assert.deepEqual(shadowParts.splitPartString("foo bar foo "), [
21+
"foo",
22+
"bar",
23+
"foo",
24+
]);
25+
});
26+
27+
test("parseExportPartsAttribute", () => {
28+
assert.deepEqual(
29+
shadowParts.parseExportPartsAttribute("foo, bar:baz"),
30+
[
31+
{ inner: "foo", outer: "foo" },
32+
{ inner: "bar", outer: "baz" },
33+
]
34+
);
35+
});
36+
37+
suite("parsePartSelector", () => {
38+
test("minimal", () => {
39+
assert.deepEqual(
40+
shadowParts.parsePartSelector("my-button::part(foo)"),
41+
{
42+
combinators: "",
43+
elementName: "my-button",
44+
selectors: "",
45+
parts: "foo",
46+
pseudos: "",
47+
}
48+
);
49+
});
50+
51+
test("all components", () => {
52+
assert.deepEqual(
53+
shadowParts.parsePartSelector(
54+
"#parent > my-button.fancy::part(foo bar)::hover"
55+
),
56+
{
57+
combinators: "#parent > ",
58+
elementName: "my-button",
59+
selectors: ".fancy",
60+
parts: "foo bar",
61+
pseudos: "::hover",
62+
}
63+
);
64+
});
65+
66+
test("missing ::part selector", () => {
67+
assert.deepEqual(
68+
shadowParts.parsePartSelector("#parent > my-button.fancy::hover"),
69+
null
70+
);
71+
});
72+
73+
test("missing custom element name", () => {
74+
assert.deepEqual(
75+
shadowParts.parsePartSelector(
76+
"#parent > .fancy::part(foo bar)::hover"
77+
),
78+
null
79+
);
80+
});
81+
});
82+
83+
test("formatShadyPartAttribute", () => {
84+
assert.equal(
85+
shadowParts.formatShadyPartAttribute("x-app", "x-btn", "prt1"),
86+
"x-app:x-btn:prt1"
87+
);
88+
});
89+
90+
test("formatShadyPartSelector", () => {
91+
assert.equal(
92+
shadowParts.formatShadyPartSelector("x-app", "x-btn", "prt1 prt2 "),
93+
'[shady-part~="x-app:x-btn:prt1"][shady-part~="x-app:x-btn:prt2"]'
94+
);
95+
});
96+
});
97+
</script>
98+
</body>

0 commit comments

Comments
 (0)