summaryrefslogtreecommitdiff
path: root/src/compiler/transform/postcss-scoped-styles/index.ts
diff options
context:
space:
mode:
authorGravatar Matthew Phillips <matthew@matthewphillips.info> 2021-04-09 14:09:13 -0400
committerGravatar GitHub <noreply@github.com> 2021-04-09 14:09:13 -0400
commitad9c3b1d8dbf1c3aff75497271347ed36ea38a0b (patch)
tree8e0aed5ea1783df8322e1db589e84f9579152ba3 /src/compiler/transform/postcss-scoped-styles/index.ts
parent084845f79d064626d5f5069ce7b945e3b44bdbd7 (diff)
downloadastro-ad9c3b1d8dbf1c3aff75497271347ed36ea38a0b.tar.gz
astro-ad9c3b1d8dbf1c3aff75497271347ed36ea38a0b.tar.zst
astro-ad9c3b1d8dbf1c3aff75497271347ed36ea38a0b.zip
Parse inner JSX as Astro (#67)
* Parse inner JSX as Astro This completes the compiler changes, updating the parser so that it parses inner "JSX" as Astro. It does this by finding the start and end of HTML tags and feeds that back into the parser. The result is a structure like this: ``` { type: 'MustacheTag', expression: [ { type: 'Expression', codeStart: 'colors.map(color => (', codeEnd: '}}' children: [ { type: 'Fragment', children: [ { type: 'Element', name: 'div' } ] } ] } ] } ``` There is a new Node type, `Expression`. Note that `MustacheTag` remains in the tree, all it contains is an Expression though. I could spend some time trying to remove it, there's just a few places that expect it to exist. * Update import to the transform * Transform prism components into expressions
Diffstat (limited to 'src/compiler/transform/postcss-scoped-styles/index.ts')
-rw-r--r--src/compiler/transform/postcss-scoped-styles/index.ts106
1 files changed, 106 insertions, 0 deletions
diff --git a/src/compiler/transform/postcss-scoped-styles/index.ts b/src/compiler/transform/postcss-scoped-styles/index.ts
new file mode 100644
index 000000000..23350869c
--- /dev/null
+++ b/src/compiler/transform/postcss-scoped-styles/index.ts
@@ -0,0 +1,106 @@
+import { Declaration, Plugin } from 'postcss';
+
+interface AstroScopedOptions {
+ className: string;
+}
+
+interface Selector {
+ start: number;
+ end: number;
+ value: string;
+}
+
+const CSS_SEPARATORS = new Set([' ', ',', '+', '>', '~']);
+const KEYFRAME_PERCENT = /\d+\.?\d*%/;
+
+/** HTML tags that should never get scoped classes */
+export const NEVER_SCOPED_TAGS = new Set<string>(['base', 'body', 'font', 'frame', 'frameset', 'head', 'html', 'link', 'meta', 'noframes', 'noscript', 'script', 'style', 'title']);
+
+/**
+ * Scope Rules
+ * Given a selector string (`.btn>span,.nav>span`), add an additional CSS class to every selector (`.btn.myClass>span.myClass,.nav.myClass>span.myClass`)
+ * @param {string} selector The minified selector string to parse. Cannot contain arbitrary whitespace (other than child selector syntax).
+ * @param {string} className The CSS class to apply.
+ */
+export function scopeRule(selector: string, className: string) {
+ // if this is a keyframe keyword, return original selector
+ if (selector === 'from' || selector === 'to' || KEYFRAME_PERCENT.test(selector)) {
+ return selector;
+ }
+
+ // For everything else, parse & scope
+ const c = className.replace(/^\.?/, '.'); // make sure class always has leading '.'
+ const selectors: Selector[] = [];
+ let ss = selector; // final output
+
+ // Pass 1: parse selector string; extract top-level selectors
+ {
+ let start = 0;
+ let lastValue = '';
+ let parensOpen = false;
+ for (let n = 0; n < ss.length; n++) {
+ const isEnd = n === selector.length - 1;
+ if (selector[n] === '(') parensOpen = true;
+ if (selector[n] === ')') parensOpen = false;
+ if (isEnd || (parensOpen === false && CSS_SEPARATORS.has(selector[n]))) {
+ lastValue = selector.substring(start, isEnd ? undefined : n);
+ if (!lastValue) continue;
+ selectors.push({ start, end: isEnd ? n + 1 : n, value: lastValue });
+ start = n + 1;
+ }
+ }
+ }
+
+ // Pass 2: starting from end, transform selectors w/ scoped class
+ for (let i = selectors.length - 1; i >= 0; i--) {
+ const { start, end, value } = selectors[i];
+ const head = ss.substring(0, start);
+ const tail = ss.substring(end);
+
+ // replace '*' with className
+ if (value === '*') {
+ ss = head + c + tail;
+ continue;
+ }
+
+ // leave :global() alone!
+ if (value.startsWith(':global(')) {
+ ss =
+ head +
+ ss
+ .substring(start, end)
+ .replace(/^:global\(/, '')
+ .replace(/\)$/, '') +
+ tail;
+ continue;
+ }
+
+ // don‘t scope body, title, etc.
+ if (NEVER_SCOPED_TAGS.has(value)) {
+ ss = head + value + tail;
+ continue;
+ }
+
+ // scope everything else
+ let newSelector = ss.substring(start, end);
+ const pseudoIndex = newSelector.indexOf(':');
+ if (pseudoIndex > 0) {
+ // if there‘s a pseudoclass (:focus)
+ ss = head + newSelector.substring(start, pseudoIndex) + c + newSelector.substr(pseudoIndex) + tail;
+ } else {
+ ss = head + newSelector + c + tail;
+ }
+ }
+
+ return ss;
+}
+
+/** PostCSS Scope plugin */
+export default function astroScopedStyles(options: AstroScopedOptions): Plugin {
+ return {
+ postcssPlugin: '@astro/postcss-scoped-styles',
+ Rule(rule) {
+ rule.selector = scopeRule(rule.selector, options.className);
+ },
+ };
+}