mirror of https://github.com/sveltejs/svelte
commit
9da0abbd70
@ -0,0 +1,5 @@
|
||||
---
|
||||
'svelte': patch
|
||||
---
|
||||
|
||||
fix: correctly scope CSS selectors with descendant combinators
|
||||
@ -0,0 +1,5 @@
|
||||
---
|
||||
"svelte": patch
|
||||
---
|
||||
|
||||
feat: implement support for `:is(...)` and `:where(...)`
|
||||
@ -0,0 +1,5 @@
|
||||
---
|
||||
'svelte': patch
|
||||
---
|
||||
|
||||
chore: treeshake unused store subscriptions in SSR mode
|
||||
@ -0,0 +1,5 @@
|
||||
---
|
||||
"svelte": patch
|
||||
---
|
||||
|
||||
fix: warn against accidental global event referenced
|
||||
@ -0,0 +1,5 @@
|
||||
---
|
||||
"svelte": patch
|
||||
---
|
||||
|
||||
fix: improve bind:this support for each blocks
|
||||
@ -0,0 +1,5 @@
|
||||
---
|
||||
"svelte": patch
|
||||
---
|
||||
|
||||
fix: improve global transition handling of effect cleardown
|
||||
@ -0,0 +1,5 @@
|
||||
---
|
||||
'svelte': patch
|
||||
---
|
||||
|
||||
breaking: replace `$derived.call` with `$derived.by`
|
||||
@ -0,0 +1,5 @@
|
||||
---
|
||||
'svelte': patch
|
||||
---
|
||||
|
||||
feat: implement nested CSS support
|
||||
@ -0,0 +1,5 @@
|
||||
---
|
||||
"svelte": patch
|
||||
---
|
||||
|
||||
feat: derive destructured derived objects values
|
||||
@ -0,0 +1,5 @@
|
||||
---
|
||||
"svelte": patch
|
||||
---
|
||||
|
||||
fix: improve global transition outro handling
|
||||
@ -0,0 +1,5 @@
|
||||
---
|
||||
"svelte": patch
|
||||
---
|
||||
|
||||
feat: add hydrate method, make hydration treeshakeable
|
||||
@ -0,0 +1,5 @@
|
||||
---
|
||||
"svelte": patch
|
||||
---
|
||||
|
||||
fix: prevent infinite loop when writing to store using shorthand
|
||||
@ -0,0 +1,5 @@
|
||||
---
|
||||
"svelte": patch
|
||||
---
|
||||
|
||||
breaking: remove `createRoot`, adjust `mount`/`hydrate` APIs, introduce `unmount`
|
||||
@ -0,0 +1,5 @@
|
||||
---
|
||||
"svelte": patch
|
||||
---
|
||||
|
||||
fix: add proper source map support
|
||||
@ -0,0 +1,5 @@
|
||||
---
|
||||
"svelte": patch
|
||||
---
|
||||
|
||||
fix: ensure inspect fires on prop changes
|
||||
@ -0,0 +1,5 @@
|
||||
---
|
||||
"svelte": patch
|
||||
---
|
||||
|
||||
fix: makes keyed each blocks consistent between dev and prod
|
||||
@ -0,0 +1,5 @@
|
||||
---
|
||||
'svelte': patch
|
||||
---
|
||||
|
||||
fix: subscribe to stores in `transition`,`animation`,`use` directives
|
||||
@ -0,0 +1,5 @@
|
||||
---
|
||||
'svelte': patch
|
||||
---
|
||||
|
||||
breaking: encapsulate/remove selectors inside `:is(...)` and `:where(...)`
|
||||
@ -1,424 +0,0 @@
|
||||
// @ts-nocheck TODO this has a bunch of type errors in strict mode which may or may not hint at bugs - check at some point
|
||||
|
||||
import remapping from '@ampproject/remapping';
|
||||
import { push_array } from './push_array.js';
|
||||
|
||||
/** @param {string} s */
|
||||
function last_line_length(s) {
|
||||
return s.length - s.lastIndexOf('\n') - 1;
|
||||
}
|
||||
|
||||
// mutate map in-place
|
||||
|
||||
/**
|
||||
* @param {import('@ampproject/remapping').DecodedSourceMap} map
|
||||
* @param {SourceLocation} offset
|
||||
* @param {number} source_index
|
||||
*/
|
||||
export function sourcemap_add_offset(map, offset, source_index) {
|
||||
if (map.mappings.length === 0) return;
|
||||
for (let line = 0; line < map.mappings.length; line++) {
|
||||
const segment_list = map.mappings[line];
|
||||
for (let segment = 0; segment < segment_list.length; segment++) {
|
||||
const seg = segment_list[segment];
|
||||
// shift only segments that belong to component source file
|
||||
if (seg[1] === source_index) {
|
||||
// also ensures that seg.length >= 4
|
||||
// shift column if it points at the first line
|
||||
if (seg[2] === 0) {
|
||||
seg[3] += offset.column;
|
||||
}
|
||||
// shift line
|
||||
seg[2] += offset.line;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @param {T[]} this_table
|
||||
* @param {T[]} other_table
|
||||
* @returns {[T[], number[], boolean, boolean]}
|
||||
*/
|
||||
function merge_tables(this_table, other_table) {
|
||||
const new_table = this_table.slice();
|
||||
const idx_map = [];
|
||||
other_table = other_table || [];
|
||||
let val_changed = false;
|
||||
for (const [other_idx, other_val] of other_table.entries()) {
|
||||
const this_idx = this_table.indexOf(other_val);
|
||||
if (this_idx >= 0) {
|
||||
idx_map[other_idx] = this_idx;
|
||||
} else {
|
||||
const new_idx = new_table.length;
|
||||
new_table[new_idx] = other_val;
|
||||
idx_map[other_idx] = new_idx;
|
||||
val_changed = true;
|
||||
}
|
||||
}
|
||||
let idx_changed = val_changed;
|
||||
if (val_changed) {
|
||||
if (
|
||||
idx_map.find(
|
||||
/**
|
||||
* @param {any} val
|
||||
* @param {any} idx
|
||||
*/ (val, idx) => val !== idx
|
||||
) === undefined
|
||||
) {
|
||||
// idx_map is identity map [0, 1, 2, 3, 4, ....]
|
||||
idx_changed = false;
|
||||
}
|
||||
}
|
||||
return [new_table, idx_map, val_changed, idx_changed];
|
||||
}
|
||||
|
||||
const regex_line_token = /([^\d\w\s]|\s+)/g;
|
||||
|
||||
export class MappedCode {
|
||||
/** @type {string} */
|
||||
string;
|
||||
|
||||
/** @type {import('@ampproject/remapping').DecodedSourceMap} */
|
||||
map;
|
||||
|
||||
/**
|
||||
* @param {any} string
|
||||
* @param {import('@ampproject/remapping').DecodedSourceMap} map
|
||||
*/
|
||||
constructor(string = '', map = null) {
|
||||
this.string = string;
|
||||
if (map) {
|
||||
this.map = /** @type {import('@ampproject/remapping').DecodedSourceMap} */ (map);
|
||||
} else {
|
||||
this.map = {
|
||||
version: 3,
|
||||
mappings: [],
|
||||
sources: [],
|
||||
names: []
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* concat in-place (mutable), return this (chainable)
|
||||
* will also mutate the `other` object
|
||||
* @param {MappedCode} other
|
||||
* @returns {import("C:/repos/svelte/svelte-octane/mapped_code.ts-to-jsdoc").MappedCode}
|
||||
*/
|
||||
concat(other) {
|
||||
// noop: if one is empty, return the other
|
||||
if (other.string === '') return this;
|
||||
if (this.string === '') {
|
||||
this.string = other.string;
|
||||
this.map = other.map;
|
||||
return this;
|
||||
}
|
||||
|
||||
// compute last line length before mutating
|
||||
const column_offset = last_line_length(this.string);
|
||||
|
||||
this.string += other.string;
|
||||
|
||||
const m1 = this.map;
|
||||
const m2 = other.map;
|
||||
|
||||
if (m2.mappings.length === 0) return this;
|
||||
|
||||
// combine sources and names
|
||||
const [sources, new_source_idx, sources_changed, sources_idx_changed] = merge_tables(
|
||||
m1.sources,
|
||||
m2.sources
|
||||
);
|
||||
const [names, new_name_idx, names_changed, names_idx_changed] = merge_tables(
|
||||
m1.names,
|
||||
m2.names
|
||||
);
|
||||
|
||||
if (sources_changed) m1.sources = sources;
|
||||
if (names_changed) m1.names = names;
|
||||
|
||||
// unswitched loops are faster
|
||||
if (sources_idx_changed && names_idx_changed) {
|
||||
for (let line = 0; line < m2.mappings.length; line++) {
|
||||
const segment_list = m2.mappings[line];
|
||||
for (let segment = 0; segment < segment_list.length; segment++) {
|
||||
const seg = segment_list[segment];
|
||||
if (seg[1] >= 0) seg[1] = new_source_idx[seg[1]];
|
||||
if (seg[4] >= 0) seg[4] = new_name_idx[seg[4]];
|
||||
}
|
||||
}
|
||||
} else if (sources_idx_changed) {
|
||||
for (let line = 0; line < m2.mappings.length; line++) {
|
||||
const segment_list = m2.mappings[line];
|
||||
for (let segment = 0; segment < segment_list.length; segment++) {
|
||||
const seg = segment_list[segment];
|
||||
if (seg[1] >= 0) seg[1] = new_source_idx[seg[1]];
|
||||
}
|
||||
}
|
||||
} else if (names_idx_changed) {
|
||||
for (let line = 0; line < m2.mappings.length; line++) {
|
||||
const segment_list = m2.mappings[line];
|
||||
for (let segment = 0; segment < segment_list.length; segment++) {
|
||||
const seg = segment_list[segment];
|
||||
if (seg[4] >= 0) seg[4] = new_name_idx[seg[4]];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// combine the mappings
|
||||
|
||||
// combine
|
||||
// 1. last line of first map
|
||||
// 2. first line of second map
|
||||
// columns of 2 must be shifted
|
||||
|
||||
if (m2.mappings.length > 0 && column_offset > 0) {
|
||||
const first_line = m2.mappings[0];
|
||||
for (let i = 0; i < first_line.length; i++) {
|
||||
first_line[i][0] += column_offset;
|
||||
}
|
||||
}
|
||||
|
||||
// combine last line + first line
|
||||
push_array(m1.mappings[m1.mappings.length - 1], m2.mappings.shift());
|
||||
|
||||
// append other lines
|
||||
push_array(m1.mappings, m2.mappings);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @static
|
||||
* @param {string} string
|
||||
* @param {import('@ampproject/remapping').DecodedSourceMap} [map]
|
||||
* @returns {import("C:/repos/svelte/svelte-octane/mapped_code.ts-to-jsdoc").MappedCode}
|
||||
*/
|
||||
static from_processed(string, map) {
|
||||
const line_count = string.split('\n').length;
|
||||
|
||||
if (map) {
|
||||
// ensure that count of source map mappings lines
|
||||
// is equal to count of generated code lines
|
||||
// (some tools may produce less)
|
||||
const missing_lines = line_count - map.mappings.length;
|
||||
for (let i = 0; i < missing_lines; i++) {
|
||||
map.mappings.push([]);
|
||||
}
|
||||
return new MappedCode(string, map);
|
||||
}
|
||||
|
||||
if (string === '') return new MappedCode();
|
||||
map = { version: 3, names: [], sources: [], mappings: [] };
|
||||
|
||||
// add empty SourceMapSegment[] for every line
|
||||
for (let i = 0; i < line_count; i++) map.mappings.push([]);
|
||||
return new MappedCode(string, map);
|
||||
}
|
||||
|
||||
/**
|
||||
* @static
|
||||
* @param {import('../preprocess/types.js').Source}params_0
|
||||
* @returns {import("C:/repos/svelte/svelte-octane/mapped_code.ts-to-jsdoc").MappedCode}
|
||||
*/
|
||||
static from_source({ source, file_basename, get_location }) {
|
||||
/** @type {SourceLocation} */
|
||||
let offset = get_location(0);
|
||||
|
||||
if (!offset) offset = { line: 0, column: 0 };
|
||||
|
||||
/** @type {import('@ampproject/remapping').DecodedSourceMap} */
|
||||
const map = { version: 3, names: [], sources: [file_basename], mappings: [] };
|
||||
if (source === '') return new MappedCode(source, map);
|
||||
|
||||
// we create a high resolution identity map here,
|
||||
// we know that it will eventually be merged with svelte's map,
|
||||
// at which stage the resolution will decrease.
|
||||
const line_list = source.split('\n');
|
||||
for (let line = 0; line < line_list.length; line++) {
|
||||
map.mappings.push([]);
|
||||
const token_list = line_list[line].split(regex_line_token);
|
||||
for (let token = 0, column = 0; token < token_list.length; token++) {
|
||||
if (token_list[token] === '') continue;
|
||||
map.mappings[line].push([column, 0, offset.line + line, column]);
|
||||
column += token_list[token].length;
|
||||
}
|
||||
}
|
||||
// shift columns in first line
|
||||
const segment_list = map.mappings[0];
|
||||
for (let segment = 0; segment < segment_list.length; segment++) {
|
||||
segment_list[segment][3] += offset.column;
|
||||
}
|
||||
return new MappedCode(source, map);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} filename
|
||||
* @param {Array<import('@ampproject/remapping').DecodedSourceMap | import('@ampproject/remapping').RawSourceMap>} sourcemap_list
|
||||
* @returns {import('@ampproject/remapping').RawSourceMap}
|
||||
*/
|
||||
export function combine_sourcemaps(filename, sourcemap_list) {
|
||||
if (sourcemap_list.length === 0) return null;
|
||||
|
||||
let map_idx = 1;
|
||||
|
||||
/** @type {import('@ampproject/remapping').RawSourceMap} */
|
||||
const map =
|
||||
sourcemap_list.slice(0, -1).find(/** @param {any} m */ (m) => m.sources.length !== 1) ===
|
||||
undefined
|
||||
? remapping(
|
||||
// use array interface
|
||||
// only the oldest sourcemap can have multiple sources
|
||||
sourcemap_list,
|
||||
() => null,
|
||||
true // skip optional field `sourcesContent`
|
||||
)
|
||||
: remapping(
|
||||
// use loader interface
|
||||
sourcemap_list[0], // last map
|
||||
|
||||
/** @type {import('@ampproject/remapping').SourceMapLoader} */ (
|
||||
(sourcefile) => {
|
||||
if (sourcefile === filename && sourcemap_list[map_idx]) {
|
||||
return sourcemap_list[map_idx++]; // idx 1, 2, ...
|
||||
// bundle file = branch node
|
||||
} else {
|
||||
return null; // source file = leaf node
|
||||
}
|
||||
}
|
||||
),
|
||||
true
|
||||
);
|
||||
|
||||
if (!map.file) delete map.file; // skip optional field `file`
|
||||
|
||||
// When source maps are combined and the leading map is empty, sources is not set.
|
||||
// Add the filename to the empty array in this case.
|
||||
// Further improvements to remapping may help address this as well https://github.com/ampproject/remapping/issues/116
|
||||
if (!map.sources.length) map.sources = [filename];
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
// browser vs node.js
|
||||
const b64enc =
|
||||
typeof btoa === 'function'
|
||||
? btoa /** @param {any} b */
|
||||
: (b) => Buffer.from(b).toString('base64');
|
||||
const b64dec =
|
||||
typeof atob === 'function'
|
||||
? atob /** @param {any} a */
|
||||
: (a) => Buffer.from(a, 'base64').toString();
|
||||
|
||||
/**
|
||||
* @param {string} filename
|
||||
* @param {import('magic-string').SourceMap} svelte_map
|
||||
* @param {string | import('@ampproject/remapping').DecodedSourceMap | import('@ampproject/remapping').RawSourceMap} preprocessor_map_input
|
||||
* @returns {import('magic-string').SourceMap}
|
||||
*/
|
||||
export function apply_preprocessor_sourcemap(filename, svelte_map, preprocessor_map_input) {
|
||||
if (!svelte_map || !preprocessor_map_input) return svelte_map;
|
||||
|
||||
const preprocessor_map =
|
||||
typeof preprocessor_map_input === 'string'
|
||||
? JSON.parse(preprocessor_map_input)
|
||||
: preprocessor_map_input;
|
||||
|
||||
const result_map = /** @type {import('@ampproject/remapping').RawSourceMap} */ (
|
||||
combine_sourcemaps(filename, [
|
||||
/** @type {import('@ampproject/remapping').RawSourceMap} */ (svelte_map),
|
||||
preprocessor_map
|
||||
])
|
||||
);
|
||||
|
||||
// Svelte expects a SourceMap which includes toUrl and toString. Instead of wrapping our output in a class,
|
||||
// we just tack on the extra properties.
|
||||
Object.defineProperties(result_map, {
|
||||
toString: {
|
||||
enumerable: false,
|
||||
value: function toString() {
|
||||
return JSON.stringify(this);
|
||||
}
|
||||
},
|
||||
toUrl: {
|
||||
enumerable: false,
|
||||
value: function toUrl() {
|
||||
return 'data:application/json;charset=utf-8;base64,' + b64enc(this.toString());
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return /** @type {import('magic-string').SourceMap} */ (result_map);
|
||||
}
|
||||
|
||||
const regex_data_uri = /data:(?:application|text)\/json;(?:charset[:=]\S+?;)?base64,(\S*)/;
|
||||
|
||||
// parse attached sourcemap in processed.code
|
||||
|
||||
/**
|
||||
* @param {import('../preprocess/types.js').Processed} processed
|
||||
* @param {'script' | 'style'} tag_name
|
||||
* @returns {void}
|
||||
*/
|
||||
export function parse_attached_sourcemap(processed, tag_name) {
|
||||
const r_in = '[#@]\\s*sourceMappingURL\\s*=\\s*(\\S*)';
|
||||
const regex =
|
||||
tag_name === 'script'
|
||||
? new RegExp('(?://' + r_in + ')|(?:/\\*' + r_in + '\\s*\\*/)$')
|
||||
: new RegExp('/\\*' + r_in + '\\s*\\*/$');
|
||||
|
||||
/** @param {any} message */
|
||||
function log_warning(message) {
|
||||
// code_start: help to find preprocessor
|
||||
const code_start =
|
||||
processed.code.length < 100 ? processed.code : processed.code.slice(0, 100) + ' [...]';
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(`warning: ${message}. processed.code = ${JSON.stringify(code_start)}`);
|
||||
}
|
||||
processed.code = processed.code.replace(
|
||||
regex,
|
||||
/**
|
||||
* @param {any} _
|
||||
* @param {any} match1
|
||||
* @param {any} match2
|
||||
*/ (_, match1, match2) => {
|
||||
const map_url = tag_name === 'script' ? match1 || match2 : match1;
|
||||
const map_data = (map_url.match(regex_data_uri) || [])[1];
|
||||
if (map_data) {
|
||||
// sourceMappingURL is data URL
|
||||
if (processed.map) {
|
||||
log_warning(
|
||||
'Not implemented. ' +
|
||||
'Found sourcemap in both processed.code and processed.map. ' +
|
||||
'Please update your preprocessor to return only one sourcemap.'
|
||||
);
|
||||
// ignore attached sourcemap
|
||||
return '';
|
||||
}
|
||||
processed.map = b64dec(map_data); // use attached sourcemap
|
||||
return ''; // remove from processed.code
|
||||
}
|
||||
// sourceMappingURL is path or URL
|
||||
if (!processed.map) {
|
||||
log_warning(
|
||||
`Found sourcemap path ${JSON.stringify(
|
||||
map_url
|
||||
)} in processed.code, but no sourcemap data. ` +
|
||||
'Please update your preprocessor to return sourcemap data directly.'
|
||||
);
|
||||
}
|
||||
// ignore sourcemap path
|
||||
return ''; // remove from processed.code
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {{
|
||||
* line: number;
|
||||
* column: number;
|
||||
* }} SourceLocation
|
||||
*/
|
||||
@ -1,845 +0,0 @@
|
||||
import { get_possible_values } from './gather_possible_values.js';
|
||||
import { regex_starts_with_whitespace, regex_ends_with_whitespace } from '../../patterns.js';
|
||||
import { error } from '../../../errors.js';
|
||||
|
||||
const NO_MATCH = 'NO_MATCH';
|
||||
const POSSIBLE_MATCH = 'POSSIBLE_MATCH';
|
||||
const UNKNOWN_SELECTOR = 'UNKNOWN_SELECTOR';
|
||||
|
||||
const NodeExist = /** @type {const} */ ({
|
||||
Probably: 0,
|
||||
Definitely: 1
|
||||
});
|
||||
|
||||
/** @typedef {typeof NodeExist[keyof typeof NodeExist]} NodeExistsValue */
|
||||
|
||||
const whitelist_attribute_selector = new Map([
|
||||
['details', new Set(['open'])],
|
||||
['dialog', new Set(['open'])]
|
||||
]);
|
||||
|
||||
export default class Selector {
|
||||
/** @type {import('#compiler').Css.Selector} */
|
||||
node;
|
||||
|
||||
/** @type {import('./Stylesheet.js').default} */
|
||||
stylesheet;
|
||||
|
||||
/** @type {Block[]} */
|
||||
blocks;
|
||||
|
||||
/** @type {Block[]} */
|
||||
local_blocks;
|
||||
|
||||
/** @type {boolean} */
|
||||
used;
|
||||
|
||||
/**
|
||||
* @param {import('#compiler').Css.Selector} node
|
||||
* @param {import('./Stylesheet.js').default} stylesheet
|
||||
*/
|
||||
constructor(node, stylesheet) {
|
||||
this.node = node;
|
||||
this.stylesheet = stylesheet;
|
||||
this.blocks = group_selectors(node);
|
||||
// take trailing :global(...) selectors out of consideration
|
||||
let i = this.blocks.length;
|
||||
while (i > 0) {
|
||||
if (!this.blocks[i - 1].global) break;
|
||||
i -= 1;
|
||||
}
|
||||
this.local_blocks = this.blocks.slice(0, i);
|
||||
const host_only = this.blocks.length === 1 && this.blocks[0].host;
|
||||
const root_only = this.blocks.length === 1 && this.blocks[0].root;
|
||||
this.used = this.local_blocks.length === 0 || host_only || root_only;
|
||||
}
|
||||
|
||||
/** @param {import('#compiler').RegularElement | import('#compiler').SvelteElement} node */
|
||||
apply(node) {
|
||||
/** @type {Array<{ node: import('#compiler').RegularElement | import('#compiler').SvelteElement; block: Block }>} */
|
||||
const to_encapsulate = [];
|
||||
apply_selector(this.local_blocks.slice(), node, to_encapsulate);
|
||||
if (to_encapsulate.length > 0) {
|
||||
to_encapsulate.forEach(({ node, block }) => {
|
||||
this.stylesheet.nodes_with_css_class.add(node);
|
||||
block.should_encapsulate = true;
|
||||
});
|
||||
this.used = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('magic-string').default} code
|
||||
* @param {string} modifier
|
||||
*/
|
||||
transform(code, modifier) {
|
||||
/** @param {import('#compiler').Css.SimpleSelector} selector */
|
||||
function remove_global_pseudo_class(selector) {
|
||||
code
|
||||
.remove(selector.start, selector.start + ':global('.length)
|
||||
.remove(selector.end - 1, selector.end);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Block} block
|
||||
* @param {string} modifier
|
||||
*/
|
||||
function encapsulate_block(block, modifier) {
|
||||
for (const selector of block.selectors) {
|
||||
if (selector.type === 'PseudoClassSelector' && selector.name === 'global') {
|
||||
remove_global_pseudo_class(selector);
|
||||
}
|
||||
}
|
||||
|
||||
let i = block.selectors.length;
|
||||
while (i--) {
|
||||
const selector = block.selectors[i];
|
||||
|
||||
if (selector.type === 'PseudoElementSelector' || selector.type === 'PseudoClassSelector') {
|
||||
if (selector.name !== 'root' && selector.name !== 'host') {
|
||||
if (i === 0) code.prependRight(selector.start, modifier);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (selector.type === 'TypeSelector' && selector.name === '*') {
|
||||
code.update(selector.start, selector.end, modifier);
|
||||
} else {
|
||||
code.appendLeft(selector.end, modifier);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let first = true;
|
||||
for (const block of this.blocks) {
|
||||
if (block.global) {
|
||||
remove_global_pseudo_class(block.selectors[0]);
|
||||
}
|
||||
|
||||
if (block.should_encapsulate) {
|
||||
// for the first occurrence, we use a classname selector, so that every
|
||||
// encapsulated selector gets a +0-1-0 specificity bump. thereafter,
|
||||
// we use a `:where` selector, which does not affect specificity
|
||||
encapsulate_block(block, first ? modifier : `:where(${modifier})`);
|
||||
first = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** @param {import('../../types.js').ComponentAnalysis} analysis */
|
||||
validate(analysis) {
|
||||
let start = 0;
|
||||
let end = this.blocks.length;
|
||||
for (; start < end; start += 1) {
|
||||
if (!this.blocks[start].global) break;
|
||||
}
|
||||
for (; end > start; end -= 1) {
|
||||
if (!this.blocks[end - 1].global) break;
|
||||
}
|
||||
for (let i = start; i < end; i += 1) {
|
||||
if (this.blocks[i].global) {
|
||||
error(this.blocks[i].selectors[0], 'invalid-css-global-placement');
|
||||
}
|
||||
}
|
||||
this.validate_global_with_multiple_selectors();
|
||||
this.validate_global_compound_selector();
|
||||
this.validate_invalid_combinator_without_selector(analysis);
|
||||
}
|
||||
|
||||
validate_global_with_multiple_selectors() {
|
||||
if (this.blocks.length === 1 && this.blocks[0].selectors.length === 1) {
|
||||
// standalone :global() with multiple selectors is OK
|
||||
return;
|
||||
}
|
||||
for (const block of this.blocks) {
|
||||
for (const selector of block.selectors) {
|
||||
if (
|
||||
selector.type === 'PseudoClassSelector' &&
|
||||
selector.name === 'global' &&
|
||||
selector.args !== null &&
|
||||
selector.args.children.length > 1
|
||||
) {
|
||||
error(selector, 'invalid-css-global-selector');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** @param {import('../../types.js').ComponentAnalysis} analysis */
|
||||
validate_invalid_combinator_without_selector(analysis) {
|
||||
for (let i = 0; i < this.blocks.length; i++) {
|
||||
const block = this.blocks[i];
|
||||
if (block.selectors.length === 0) {
|
||||
error(this.node, 'invalid-css-selector');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
validate_global_compound_selector() {
|
||||
for (const block of this.blocks) {
|
||||
if (block.selectors.length === 1) continue;
|
||||
|
||||
for (let i = 0; i < block.selectors.length; i++) {
|
||||
const selector = block.selectors[i];
|
||||
|
||||
if (selector.type === 'PseudoClassSelector' && selector.name === 'global') {
|
||||
const child = selector.args?.children[0].children[0];
|
||||
if (
|
||||
child?.type === 'TypeSelector' &&
|
||||
!/[.:#]/.test(child.name[0]) &&
|
||||
(i !== 0 ||
|
||||
block.selectors
|
||||
.slice(1)
|
||||
.some(
|
||||
(s) => s.type !== 'PseudoElementSelector' && s.type !== 'PseudoClassSelector'
|
||||
))
|
||||
) {
|
||||
error(selector, 'invalid-css-global-selector-list');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Block[]} blocks
|
||||
* @param {import('#compiler').RegularElement | import('#compiler').SvelteElement | null} node
|
||||
* @param {Array<{ node: import('#compiler').RegularElement | import('#compiler').SvelteElement; block: Block }>} to_encapsulate
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function apply_selector(blocks, node, to_encapsulate) {
|
||||
const block = blocks.pop();
|
||||
if (!block) return false;
|
||||
if (!node) {
|
||||
return (
|
||||
(block.global && blocks.every((block) => block.global)) || (block.host && blocks.length === 0)
|
||||
);
|
||||
}
|
||||
const applies = block_might_apply_to_node(block, node);
|
||||
|
||||
if (applies === NO_MATCH) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (applies === UNKNOWN_SELECTOR) {
|
||||
to_encapsulate.push({ node, block });
|
||||
return true;
|
||||
}
|
||||
|
||||
if (block.combinator) {
|
||||
if (block.combinator.type === 'Combinator' && block.combinator.name === ' ') {
|
||||
for (const ancestor_block of blocks) {
|
||||
if (ancestor_block.global) {
|
||||
continue;
|
||||
}
|
||||
if (ancestor_block.host) {
|
||||
to_encapsulate.push({ node, block });
|
||||
return true;
|
||||
}
|
||||
/** @type {import('#compiler').RegularElement | import('#compiler').SvelteElement | null} */
|
||||
let parent = node;
|
||||
while ((parent = get_element_parent(parent))) {
|
||||
if (block_might_apply_to_node(ancestor_block, parent) !== NO_MATCH) {
|
||||
to_encapsulate.push({ node: parent, block: ancestor_block });
|
||||
}
|
||||
}
|
||||
if (to_encapsulate.length) {
|
||||
to_encapsulate.push({ node, block });
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (blocks.every((block) => block.global)) {
|
||||
to_encapsulate.push({ node, block });
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
} else if (block.combinator.name === '>') {
|
||||
const has_global_parent = blocks.every((block) => block.global);
|
||||
if (has_global_parent || apply_selector(blocks, get_element_parent(node), to_encapsulate)) {
|
||||
to_encapsulate.push({ node, block });
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
} else if (block.combinator.name === '+' || block.combinator.name === '~') {
|
||||
const siblings = get_possible_element_siblings(node, block.combinator.name === '+');
|
||||
let has_match = false;
|
||||
// NOTE: if we have :global(), we couldn't figure out what is selected within `:global` due to the
|
||||
// css-tree limitation that does not parse the inner selector of :global
|
||||
// so unless we are sure there will be no sibling to match, we will consider it as matched
|
||||
const has_global = blocks.some((block) => block.global);
|
||||
if (has_global) {
|
||||
if (siblings.size === 0 && get_element_parent(node) !== null) {
|
||||
return false;
|
||||
}
|
||||
to_encapsulate.push({ node, block });
|
||||
return true;
|
||||
}
|
||||
for (const possible_sibling of siblings.keys()) {
|
||||
if (apply_selector(blocks.slice(), possible_sibling, to_encapsulate)) {
|
||||
to_encapsulate.push({ node, block });
|
||||
has_match = true;
|
||||
}
|
||||
}
|
||||
return has_match;
|
||||
}
|
||||
// TODO other combinators
|
||||
to_encapsulate.push({ node, block });
|
||||
return true;
|
||||
}
|
||||
to_encapsulate.push({ node, block });
|
||||
return true;
|
||||
}
|
||||
|
||||
const regex_backslash_and_following_character = /\\(.)/g;
|
||||
|
||||
/**
|
||||
* @param {Block} block
|
||||
* @param {import('#compiler').RegularElement | import('#compiler').SvelteElement} node
|
||||
* @returns {NO_MATCH | POSSIBLE_MATCH | UNKNOWN_SELECTOR}
|
||||
*/
|
||||
function block_might_apply_to_node(block, node) {
|
||||
if (block.host || block.root) return NO_MATCH;
|
||||
|
||||
let i = block.selectors.length;
|
||||
while (i--) {
|
||||
const selector = block.selectors[i];
|
||||
|
||||
if (selector.type === 'Percentage' || selector.type === 'Nth') continue;
|
||||
|
||||
const name = selector.name.replace(regex_backslash_and_following_character, '$1');
|
||||
|
||||
if (selector.type === 'PseudoClassSelector' && (name === 'host' || name === 'root')) {
|
||||
return NO_MATCH;
|
||||
}
|
||||
if (
|
||||
block.selectors.length === 1 &&
|
||||
selector.type === 'PseudoClassSelector' &&
|
||||
name === 'global'
|
||||
) {
|
||||
return NO_MATCH;
|
||||
}
|
||||
|
||||
if (selector.type === 'PseudoClassSelector' || selector.type === 'PseudoElementSelector') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (selector.type === 'AttributeSelector') {
|
||||
const whitelisted = whitelist_attribute_selector.get(node.name.toLowerCase());
|
||||
if (
|
||||
!whitelisted?.has(selector.name.toLowerCase()) &&
|
||||
!attribute_matches(
|
||||
node,
|
||||
selector.name,
|
||||
selector.value && unquote(selector.value),
|
||||
selector.matcher,
|
||||
selector.flags?.includes('i') ?? false
|
||||
)
|
||||
) {
|
||||
return NO_MATCH;
|
||||
}
|
||||
} else {
|
||||
if (selector.type === 'ClassSelector') {
|
||||
if (
|
||||
!attribute_matches(node, 'class', name, '~=', false) &&
|
||||
!node.attributes.some(
|
||||
(attribute) => attribute.type === 'ClassDirective' && attribute.name === name
|
||||
)
|
||||
) {
|
||||
return NO_MATCH;
|
||||
}
|
||||
} else if (selector.type === 'IdSelector') {
|
||||
if (!attribute_matches(node, 'id', name, '=', false)) return NO_MATCH;
|
||||
} else if (selector.type === 'TypeSelector') {
|
||||
if (
|
||||
node.name.toLowerCase() !== name.toLowerCase() &&
|
||||
name !== '*' &&
|
||||
node.type !== 'SvelteElement'
|
||||
) {
|
||||
return NO_MATCH;
|
||||
}
|
||||
} else {
|
||||
return UNKNOWN_SELECTOR;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return POSSIBLE_MATCH;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {any} operator
|
||||
* @param {any} expected_value
|
||||
* @param {any} case_insensitive
|
||||
* @param {any} value
|
||||
*/
|
||||
function test_attribute(operator, expected_value, case_insensitive, value) {
|
||||
if (case_insensitive) {
|
||||
expected_value = expected_value.toLowerCase();
|
||||
value = value.toLowerCase();
|
||||
}
|
||||
switch (operator) {
|
||||
case '=':
|
||||
return value === expected_value;
|
||||
case '~=':
|
||||
return value.split(/\s/).includes(expected_value);
|
||||
case '|=':
|
||||
return `${value}-`.startsWith(`${expected_value}-`);
|
||||
case '^=':
|
||||
return value.startsWith(expected_value);
|
||||
case '$=':
|
||||
return value.endsWith(expected_value);
|
||||
case '*=':
|
||||
return value.includes(expected_value);
|
||||
default:
|
||||
throw new Error("this shouldn't happen");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('#compiler').RegularElement | import('#compiler').SvelteElement} node
|
||||
* @param {string} name
|
||||
* @param {string | null} expected_value
|
||||
* @param {string | null} operator
|
||||
* @param {boolean} case_insensitive
|
||||
*/
|
||||
function attribute_matches(node, name, expected_value, operator, case_insensitive) {
|
||||
for (const attribute of node.attributes) {
|
||||
if (attribute.type === 'SpreadAttribute') return true;
|
||||
if (attribute.type === 'BindDirective' && attribute.name === name) return true;
|
||||
|
||||
if (attribute.type !== 'Attribute') continue;
|
||||
if (attribute.name.toLowerCase() !== name.toLowerCase()) continue;
|
||||
|
||||
if (attribute.value === true) return operator === null;
|
||||
if (expected_value === null) return true;
|
||||
|
||||
const chunks = attribute.value;
|
||||
if (chunks.length === 1) {
|
||||
const value = chunks[0];
|
||||
if (value.type === 'Text') {
|
||||
return test_attribute(operator, expected_value, case_insensitive, value.data);
|
||||
}
|
||||
}
|
||||
|
||||
const possible_values = new Set();
|
||||
|
||||
/** @type {string[]} */
|
||||
let prev_values = [];
|
||||
for (const chunk of chunks) {
|
||||
const current_possible_values = get_possible_values(chunk);
|
||||
|
||||
// impossible to find out all combinations
|
||||
if (!current_possible_values) return true;
|
||||
|
||||
if (prev_values.length > 0) {
|
||||
/** @type {string[]} */
|
||||
const start_with_space = [];
|
||||
|
||||
/** @type {string[]} */
|
||||
const remaining = [];
|
||||
|
||||
current_possible_values.forEach((current_possible_value) => {
|
||||
if (regex_starts_with_whitespace.test(current_possible_value)) {
|
||||
start_with_space.push(current_possible_value);
|
||||
} else {
|
||||
remaining.push(current_possible_value);
|
||||
}
|
||||
});
|
||||
if (remaining.length > 0) {
|
||||
if (start_with_space.length > 0) {
|
||||
prev_values.forEach((prev_value) => possible_values.add(prev_value));
|
||||
}
|
||||
|
||||
/** @type {string[]} */
|
||||
const combined = [];
|
||||
|
||||
prev_values.forEach((prev_value) => {
|
||||
remaining.forEach((value) => {
|
||||
combined.push(prev_value + value);
|
||||
});
|
||||
});
|
||||
prev_values = combined;
|
||||
start_with_space.forEach((value) => {
|
||||
if (regex_ends_with_whitespace.test(value)) {
|
||||
possible_values.add(value);
|
||||
} else {
|
||||
prev_values.push(value);
|
||||
}
|
||||
});
|
||||
continue;
|
||||
} else {
|
||||
prev_values.forEach((prev_value) => possible_values.add(prev_value));
|
||||
prev_values = [];
|
||||
}
|
||||
}
|
||||
current_possible_values.forEach((current_possible_value) => {
|
||||
if (regex_ends_with_whitespace.test(current_possible_value)) {
|
||||
possible_values.add(current_possible_value);
|
||||
} else {
|
||||
prev_values.push(current_possible_value);
|
||||
}
|
||||
});
|
||||
if (prev_values.length < current_possible_values.size) {
|
||||
prev_values.push(' ');
|
||||
}
|
||||
if (prev_values.length > 20) {
|
||||
// might grow exponentially, bail out
|
||||
return true;
|
||||
}
|
||||
}
|
||||
prev_values.forEach((prev_value) => possible_values.add(prev_value));
|
||||
|
||||
for (const value of possible_values) {
|
||||
if (test_attribute(operator, expected_value, case_insensitive, value)) return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/** @param {string} str */
|
||||
function unquote(str) {
|
||||
if ((str[0] === str[str.length - 1] && str[0] === "'") || str[0] === '"') {
|
||||
return str.slice(1, str.length - 1);
|
||||
}
|
||||
return str;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('#compiler').RegularElement | import('#compiler').SvelteElement} node
|
||||
* @returns {import('#compiler').RegularElement | import('#compiler').SvelteElement | null}
|
||||
*/
|
||||
function get_element_parent(node) {
|
||||
/** @type {import('#compiler').SvelteNode | null} */
|
||||
let parent = node;
|
||||
while (
|
||||
// @ts-expect-error TODO figure out a more elegant solution
|
||||
(parent = parent.parent) &&
|
||||
parent.type !== 'RegularElement' &&
|
||||
parent.type !== 'SvelteElement'
|
||||
);
|
||||
return parent ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the given node's previous sibling in the DOM
|
||||
*
|
||||
* The Svelte `<slot>` is just a placeholder and is not actually real. Any children nodes
|
||||
* in `<slot>` are 'flattened' and considered as the same level as the `<slot>`'s siblings
|
||||
*
|
||||
* e.g.
|
||||
* ```html
|
||||
* <h1>Heading 1</h1>
|
||||
* <slot>
|
||||
* <h2>Heading 2</h2>
|
||||
* </slot>
|
||||
* ```
|
||||
*
|
||||
* is considered to look like:
|
||||
* ```html
|
||||
* <h1>Heading 1</h1>
|
||||
* <h2>Heading 2</h2>
|
||||
* ```
|
||||
* @param {import('#compiler').SvelteNode} node
|
||||
* @returns {import('#compiler').SvelteNode}
|
||||
*/
|
||||
function find_previous_sibling(node) {
|
||||
/** @type {import('#compiler').SvelteNode} */
|
||||
let current_node = node;
|
||||
do {
|
||||
if (current_node.type === 'SlotElement') {
|
||||
const slot_children = current_node.fragment.nodes;
|
||||
if (slot_children.length > 0) {
|
||||
current_node = slot_children.slice(-1)[0]; // go to its last child first
|
||||
continue;
|
||||
}
|
||||
}
|
||||
while (
|
||||
// @ts-expect-error TODO
|
||||
!current_node.prev &&
|
||||
// @ts-expect-error TODO
|
||||
current_node.parent &&
|
||||
// @ts-expect-error TODO
|
||||
current_node.parent.type === 'SlotElement'
|
||||
) {
|
||||
// @ts-expect-error TODO
|
||||
current_node = current_node.parent;
|
||||
}
|
||||
// @ts-expect-error
|
||||
current_node = current_node.prev;
|
||||
} while (current_node && current_node.type === 'SlotElement');
|
||||
return current_node;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('#compiler').SvelteNode} node
|
||||
* @param {boolean} adjacent_only
|
||||
* @returns {Map<import('#compiler').RegularElement, NodeExistsValue>}
|
||||
*/
|
||||
function get_possible_element_siblings(node, adjacent_only) {
|
||||
/** @type {Map<import('#compiler').RegularElement, NodeExistsValue>} */
|
||||
const result = new Map();
|
||||
|
||||
/** @type {import('#compiler').SvelteNode} */
|
||||
let prev = node;
|
||||
while ((prev = find_previous_sibling(prev))) {
|
||||
if (prev.type === 'RegularElement') {
|
||||
if (
|
||||
!prev.attributes.find(
|
||||
(attr) => attr.type === 'Attribute' && attr.name.toLowerCase() === 'slot'
|
||||
)
|
||||
) {
|
||||
result.set(prev, NodeExist.Definitely);
|
||||
}
|
||||
if (adjacent_only) {
|
||||
break;
|
||||
}
|
||||
} else if (prev.type === 'EachBlock' || prev.type === 'IfBlock' || prev.type === 'AwaitBlock') {
|
||||
const possible_last_child = get_possible_last_child(prev, adjacent_only);
|
||||
add_to_map(possible_last_child, result);
|
||||
if (adjacent_only && has_definite_elements(possible_last_child)) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!prev || !adjacent_only) {
|
||||
/** @type {import('#compiler').SvelteNode | null} */
|
||||
let parent = node;
|
||||
|
||||
while (
|
||||
// @ts-expect-error TODO
|
||||
(parent = parent?.parent) &&
|
||||
(parent.type === 'EachBlock' || parent.type === 'IfBlock' || parent.type === 'AwaitBlock')
|
||||
) {
|
||||
const possible_siblings = get_possible_element_siblings(parent, adjacent_only);
|
||||
add_to_map(possible_siblings, result);
|
||||
|
||||
// @ts-expect-error
|
||||
if (parent.type === 'EachBlock' && !parent.fallback?.nodes.includes(node)) {
|
||||
// `{#each ...}<a /><b />{/each}` — `<b>` can be previous sibling of `<a />`
|
||||
add_to_map(get_possible_last_child(parent, adjacent_only), result);
|
||||
}
|
||||
|
||||
if (adjacent_only && has_definite_elements(possible_siblings)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('#compiler').EachBlock | import('#compiler').IfBlock | import('#compiler').AwaitBlock} block
|
||||
* @param {boolean} adjacent_only
|
||||
* @returns {Map<import('#compiler').RegularElement, NodeExistsValue>}
|
||||
*/
|
||||
function get_possible_last_child(block, adjacent_only) {
|
||||
/** @typedef {Map<import('#compiler').RegularElement, NodeExistsValue>} NodeMap */
|
||||
|
||||
/** @type {NodeMap} */
|
||||
const result = new Map();
|
||||
if (block.type === 'EachBlock') {
|
||||
/** @type {NodeMap} */
|
||||
const each_result = loop_child(block.body.nodes, adjacent_only);
|
||||
|
||||
/** @type {NodeMap} */
|
||||
const else_result = block.fallback
|
||||
? loop_child(block.fallback.nodes, adjacent_only)
|
||||
: new Map();
|
||||
const not_exhaustive = !has_definite_elements(else_result);
|
||||
if (not_exhaustive) {
|
||||
mark_as_probably(each_result);
|
||||
mark_as_probably(else_result);
|
||||
}
|
||||
add_to_map(each_result, result);
|
||||
add_to_map(else_result, result);
|
||||
} else if (block.type === 'IfBlock') {
|
||||
/** @type {NodeMap} */
|
||||
const if_result = loop_child(block.consequent.nodes, adjacent_only);
|
||||
|
||||
/** @type {NodeMap} */
|
||||
const else_result = block.alternate
|
||||
? loop_child(block.alternate.nodes, adjacent_only)
|
||||
: new Map();
|
||||
const not_exhaustive = !has_definite_elements(if_result) || !has_definite_elements(else_result);
|
||||
if (not_exhaustive) {
|
||||
mark_as_probably(if_result);
|
||||
mark_as_probably(else_result);
|
||||
}
|
||||
add_to_map(if_result, result);
|
||||
add_to_map(else_result, result);
|
||||
} else if (block.type === 'AwaitBlock') {
|
||||
/** @type {NodeMap} */
|
||||
const pending_result = block.pending
|
||||
? loop_child(block.pending.nodes, adjacent_only)
|
||||
: new Map();
|
||||
|
||||
/** @type {NodeMap} */
|
||||
const then_result = block.then ? loop_child(block.then.nodes, adjacent_only) : new Map();
|
||||
|
||||
/** @type {NodeMap} */
|
||||
const catch_result = block.catch ? loop_child(block.catch.nodes, adjacent_only) : new Map();
|
||||
const not_exhaustive =
|
||||
!has_definite_elements(pending_result) ||
|
||||
!has_definite_elements(then_result) ||
|
||||
!has_definite_elements(catch_result);
|
||||
if (not_exhaustive) {
|
||||
mark_as_probably(pending_result);
|
||||
mark_as_probably(then_result);
|
||||
mark_as_probably(catch_result);
|
||||
}
|
||||
add_to_map(pending_result, result);
|
||||
add_to_map(then_result, result);
|
||||
add_to_map(catch_result, result);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Map<import('#compiler').RegularElement, NodeExistsValue>} result
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function has_definite_elements(result) {
|
||||
if (result.size === 0) return false;
|
||||
for (const exist of result.values()) {
|
||||
if (exist === NodeExist.Definitely) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Map<import('#compiler').RegularElement, NodeExistsValue>} from
|
||||
* @param {Map<import('#compiler').RegularElement, NodeExistsValue>} to
|
||||
* @returns {void}
|
||||
*/
|
||||
function add_to_map(from, to) {
|
||||
from.forEach((exist, element) => {
|
||||
to.set(element, higher_existence(exist, to.get(element)));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {NodeExistsValue | undefined} exist1
|
||||
* @param {NodeExistsValue | undefined} exist2
|
||||
* @returns {NodeExistsValue}
|
||||
*/
|
||||
function higher_existence(exist1, exist2) {
|
||||
// @ts-expect-error TODO figure out if this is a bug
|
||||
if (exist1 === undefined || exist2 === undefined) return exist1 || exist2;
|
||||
return exist1 > exist2 ? exist1 : exist2;
|
||||
}
|
||||
|
||||
/** @param {Map<import('#compiler').RegularElement, NodeExistsValue>} result */
|
||||
function mark_as_probably(result) {
|
||||
for (const key of result.keys()) {
|
||||
result.set(key, NodeExist.Probably);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('#compiler').SvelteNode[]} children
|
||||
* @param {boolean} adjacent_only
|
||||
*/
|
||||
function loop_child(children, adjacent_only) {
|
||||
/** @type {Map<import('#compiler').RegularElement, NodeExistsValue>} */
|
||||
const result = new Map();
|
||||
for (let i = children.length - 1; i >= 0; i--) {
|
||||
const child = children[i];
|
||||
if (child.type === 'RegularElement') {
|
||||
result.set(child, NodeExist.Definitely);
|
||||
if (adjacent_only) {
|
||||
break;
|
||||
}
|
||||
} else if (
|
||||
child.type === 'EachBlock' ||
|
||||
child.type === 'IfBlock' ||
|
||||
child.type === 'AwaitBlock'
|
||||
) {
|
||||
const child_result = get_possible_last_child(child, adjacent_only);
|
||||
add_to_map(child_result, result);
|
||||
if (adjacent_only && has_definite_elements(child_result)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
class Block {
|
||||
/** @type {boolean} */
|
||||
host;
|
||||
|
||||
/** @type {boolean} */
|
||||
root;
|
||||
|
||||
/** @type {import('#compiler').Css.Combinator | null} */
|
||||
combinator;
|
||||
|
||||
/** @type {import('#compiler').Css.SimpleSelector[]} */
|
||||
selectors;
|
||||
|
||||
/** @type {number} */
|
||||
start;
|
||||
|
||||
/** @type {number} */
|
||||
end;
|
||||
|
||||
/** @type {boolean} */
|
||||
should_encapsulate;
|
||||
|
||||
/** @param {import('#compiler').Css.Combinator | null} combinator */
|
||||
constructor(combinator) {
|
||||
this.combinator = combinator;
|
||||
this.host = false;
|
||||
this.root = false;
|
||||
this.selectors = [];
|
||||
this.start = -1;
|
||||
this.end = -1;
|
||||
this.should_encapsulate = false;
|
||||
}
|
||||
|
||||
/** @param {import('#compiler').Css.SimpleSelector} selector */
|
||||
add(selector) {
|
||||
if (this.selectors.length === 0) {
|
||||
this.start = selector.start;
|
||||
this.host = selector.type === 'PseudoClassSelector' && selector.name === 'host';
|
||||
}
|
||||
this.root = this.root || (selector.type === 'PseudoClassSelector' && selector.name === 'root');
|
||||
this.selectors.push(selector);
|
||||
this.end = selector.end;
|
||||
}
|
||||
get global() {
|
||||
return (
|
||||
this.selectors.length >= 1 &&
|
||||
this.selectors[0].type === 'PseudoClassSelector' &&
|
||||
this.selectors[0].name === 'global' &&
|
||||
this.selectors.every(
|
||||
(selector) =>
|
||||
selector.type === 'PseudoClassSelector' || selector.type === 'PseudoElementSelector'
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/** @param {import('#compiler').Css.Selector} selector */
|
||||
function group_selectors(selector) {
|
||||
let block = new Block(null);
|
||||
const blocks = [block];
|
||||
|
||||
selector.children.forEach((child) => {
|
||||
if (child.type === 'Combinator') {
|
||||
block = new Block(child);
|
||||
blocks.push(block);
|
||||
} else {
|
||||
block.add(child);
|
||||
}
|
||||
});
|
||||
return blocks;
|
||||
}
|
||||
@ -1,550 +0,0 @@
|
||||
import MagicString from 'magic-string';
|
||||
import { walk } from 'zimmerframe';
|
||||
import Selector from './Selector.js';
|
||||
import hash from '../utils/hash.js';
|
||||
// import compiler_warnings from '../compiler_warnings.js';
|
||||
// import { extract_ignores_above_position } from '../utils/extract_svelte_ignore.js';
|
||||
import { push_array } from '../utils/push_array.js';
|
||||
import { create_attribute } from '../../nodes.js';
|
||||
|
||||
const regex_css_browser_prefix = /^-((webkit)|(moz)|(o)|(ms))-/;
|
||||
const regex_name_boundary = /^[\s,;}]$/;
|
||||
/**
|
||||
* @param {string} name
|
||||
* @returns {string}
|
||||
*/
|
||||
function remove_css_prefix(name) {
|
||||
return name.replace(regex_css_browser_prefix, '');
|
||||
}
|
||||
|
||||
/** @param {import('#compiler').Css.Atrule} node */
|
||||
const is_keyframes_node = (node) => remove_css_prefix(node.name) === 'keyframes';
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {import('#compiler').Css.Rule} node
|
||||
* @param {MagicString} code
|
||||
*/
|
||||
function escape_comment_close(node, code) {
|
||||
let escaped = false;
|
||||
let in_comment = false;
|
||||
|
||||
for (let i = node.start; i < node.end; i++) {
|
||||
if (escaped) {
|
||||
escaped = false;
|
||||
} else {
|
||||
const char = code.original[i];
|
||||
if (in_comment) {
|
||||
if (char === '*' && code.original[i + 1] === '/') {
|
||||
code.prependRight(++i, '\\');
|
||||
in_comment = false;
|
||||
}
|
||||
} else if (char === '\\') {
|
||||
escaped = true;
|
||||
} else if (char === '/' && code.original[++i] === '*') {
|
||||
in_comment = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Rule {
|
||||
/** @type {import('./Selector.js').default[]} */
|
||||
selectors;
|
||||
|
||||
/** @type {Declaration[]} */
|
||||
declarations;
|
||||
|
||||
/** @type {import('#compiler').Css.Rule} */
|
||||
node;
|
||||
|
||||
/** @type {Atrule | undefined} */
|
||||
parent;
|
||||
|
||||
/**
|
||||
* @param {import('#compiler').Css.Rule} node
|
||||
* @param {any} stylesheet
|
||||
* @param {Atrule | undefined} parent
|
||||
*/
|
||||
constructor(node, stylesheet, parent) {
|
||||
this.node = node;
|
||||
this.parent = parent;
|
||||
this.selectors = node.prelude.children.map((node) => new Selector(node, stylesheet));
|
||||
|
||||
this.declarations = /** @type {import('#compiler').Css.Declaration[]} */ (
|
||||
node.block.children
|
||||
).map((node) => new Declaration(node));
|
||||
}
|
||||
|
||||
/** @param {import('#compiler').RegularElement | import('#compiler').SvelteElement} node */
|
||||
apply(node) {
|
||||
this.selectors.forEach((selector) => selector.apply(node)); // TODO move the logic in here?
|
||||
}
|
||||
|
||||
/** @param {boolean} dev */
|
||||
is_used(dev) {
|
||||
if (this.parent && this.parent.node.type === 'Atrule' && is_keyframes_node(this.parent.node))
|
||||
return true;
|
||||
|
||||
// keep empty rules in dev, because it's convenient to
|
||||
// see them in devtools
|
||||
if (this.declarations.length === 0) return dev;
|
||||
|
||||
return this.selectors.some((s) => s.used);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('magic-string').default} code
|
||||
* @param {string} id
|
||||
* @param {Map<string, string>} keyframes
|
||||
*/
|
||||
transform(code, id, keyframes) {
|
||||
if (this.parent && this.parent.node.type === 'Atrule' && is_keyframes_node(this.parent.node)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const modifier = `.${id}`;
|
||||
this.selectors.forEach((selector) => selector.transform(code, modifier));
|
||||
this.declarations.forEach((declaration) => declaration.transform(code, keyframes));
|
||||
}
|
||||
|
||||
/** @param {import('../../types.js').ComponentAnalysis} analysis */
|
||||
validate(analysis) {
|
||||
this.selectors.forEach((selector) => {
|
||||
selector.validate(analysis);
|
||||
});
|
||||
}
|
||||
|
||||
/** @param {(selector: import('./Selector.js').default) => void} handler */
|
||||
warn_on_unused_selector(handler) {
|
||||
this.selectors.forEach((selector) => {
|
||||
if (!selector.used) handler(selector);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {MagicString} code
|
||||
* @param {boolean} dev
|
||||
*/
|
||||
prune(code, dev) {
|
||||
if (this.parent && this.parent.node.type === 'Atrule' && is_keyframes_node(this.parent.node)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// keep empty rules in dev, because it's convenient to
|
||||
// see them in devtools
|
||||
if (this.declarations.length === 0) {
|
||||
if (!dev) {
|
||||
code.prependRight(this.node.start, '/* (empty) ');
|
||||
code.appendLeft(this.node.end, '*/');
|
||||
escape_comment_close(this.node, code);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const used = this.selectors.filter((s) => s.used);
|
||||
|
||||
if (used.length === 0) {
|
||||
code.prependRight(this.node.start, '/* (unused) ');
|
||||
code.appendLeft(this.node.end, '*/');
|
||||
escape_comment_close(this.node, code);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (used.length < this.selectors.length) {
|
||||
let pruning = false;
|
||||
let last = this.selectors[0].node.start;
|
||||
|
||||
for (let i = 0; i < this.selectors.length; i += 1) {
|
||||
const selector = this.selectors[i];
|
||||
|
||||
if (selector.used === pruning) {
|
||||
if (pruning) {
|
||||
let i = selector.node.start;
|
||||
while (code.original[i] !== ',') i--;
|
||||
|
||||
code.overwrite(i, i + 1, '*/');
|
||||
} else {
|
||||
if (i === 0) {
|
||||
code.prependRight(selector.node.start, '/* (unused) ');
|
||||
} else {
|
||||
code.overwrite(last, selector.node.start, ' /* (unused) ');
|
||||
}
|
||||
}
|
||||
|
||||
pruning = !pruning;
|
||||
}
|
||||
|
||||
last = selector.node.end;
|
||||
}
|
||||
|
||||
if (pruning) {
|
||||
code.appendLeft(last, '*/');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Declaration {
|
||||
/** @type {import('#compiler').Css.Declaration} */
|
||||
node;
|
||||
|
||||
/** @param {import('#compiler').Css.Declaration} node */
|
||||
constructor(node) {
|
||||
this.node = node;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('magic-string').default} code
|
||||
* @param {Map<string, string>} keyframes
|
||||
*/
|
||||
transform(code, keyframes) {
|
||||
const property = this.node.property && remove_css_prefix(this.node.property.toLowerCase());
|
||||
if (property === 'animation' || property === 'animation-name') {
|
||||
let index = this.node.start + this.node.property.length + 1;
|
||||
let name = '';
|
||||
|
||||
while (index < code.original.length) {
|
||||
const character = code.original[index];
|
||||
|
||||
if (regex_name_boundary.test(character)) {
|
||||
const keyframe = keyframes.get(name);
|
||||
|
||||
if (keyframe) {
|
||||
code.update(index - name.length, index, keyframe);
|
||||
}
|
||||
|
||||
if (character === ';' || character === '}') {
|
||||
break;
|
||||
}
|
||||
|
||||
name = '';
|
||||
} else {
|
||||
name += character;
|
||||
}
|
||||
|
||||
index++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Atrule {
|
||||
/** @type {import('#compiler').Css.Atrule} */
|
||||
node;
|
||||
|
||||
/** @type {Array<Atrule | Rule>} */
|
||||
children;
|
||||
|
||||
/** @type {Declaration[]} */
|
||||
declarations;
|
||||
|
||||
/** @param {import('#compiler').Css.Atrule} node */
|
||||
constructor(node) {
|
||||
this.node = node;
|
||||
this.children = [];
|
||||
this.declarations = [];
|
||||
}
|
||||
|
||||
/** @param {import('#compiler').RegularElement | import('#compiler').SvelteElement} node */
|
||||
apply(node) {
|
||||
if (
|
||||
this.node.name === 'container' ||
|
||||
this.node.name === 'media' ||
|
||||
this.node.name === 'supports' ||
|
||||
this.node.name === 'layer'
|
||||
) {
|
||||
this.children.forEach((child) => {
|
||||
child.apply(node);
|
||||
});
|
||||
} else if (is_keyframes_node(this.node)) {
|
||||
/** @type {Rule[]} */ (this.children).forEach((rule) => {
|
||||
rule.selectors.forEach((selector) => {
|
||||
selector.used = true;
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/** @param {boolean} _dev */
|
||||
is_used(_dev) {
|
||||
return true; // TODO
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('magic-string').default} code
|
||||
* @param {string} id
|
||||
* @param {Map<string, string>} keyframes
|
||||
*/
|
||||
transform(code, id, keyframes) {
|
||||
if (is_keyframes_node(this.node)) {
|
||||
let start = this.node.start + this.node.name.length + 1;
|
||||
while (code.original[start] === ' ') start += 1;
|
||||
let end = start;
|
||||
while (code.original[end] !== '{' && code.original[end] !== ' ') end += 1;
|
||||
|
||||
if (this.node.prelude.startsWith('-global-')) {
|
||||
code.remove(start, start + 8);
|
||||
/** @type {Rule[]} */ (this.children).forEach((rule) => {
|
||||
rule.selectors.forEach((selector) => {
|
||||
selector.used = true;
|
||||
});
|
||||
});
|
||||
} else {
|
||||
const keyframe = /** @type {string} */ (keyframes.get(this.node.prelude));
|
||||
code.update(start, end, keyframe);
|
||||
}
|
||||
}
|
||||
this.children.forEach((child) => {
|
||||
child.transform(code, id, keyframes);
|
||||
});
|
||||
}
|
||||
|
||||
/** @param {import('../../types.js').ComponentAnalysis} analysis */
|
||||
validate(analysis) {
|
||||
this.children.forEach((child) => {
|
||||
child.validate(analysis);
|
||||
});
|
||||
}
|
||||
|
||||
/** @param {(selector: import('./Selector.js').default) => void} handler */
|
||||
warn_on_unused_selector(handler) {
|
||||
if (this.node.name !== 'media') return;
|
||||
this.children.forEach((child) => {
|
||||
child.warn_on_unused_selector(handler);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {MagicString} code
|
||||
* @param {boolean} dev
|
||||
*/
|
||||
prune(code, dev) {
|
||||
// TODO prune children
|
||||
}
|
||||
}
|
||||
|
||||
export default class Stylesheet {
|
||||
/** @type {import('#compiler').Style | null} */
|
||||
ast;
|
||||
|
||||
/** @type {string} */
|
||||
filename;
|
||||
|
||||
/** @type {boolean} */
|
||||
has_styles;
|
||||
|
||||
/** @type {string} */
|
||||
id;
|
||||
|
||||
/** @type {Array<Rule | Atrule>} */
|
||||
children = [];
|
||||
|
||||
/** @type {Map<string, string>} */
|
||||
keyframes = new Map();
|
||||
|
||||
/** @type {Set<import('#compiler').RegularElement | import('#compiler').SvelteElement>} */
|
||||
nodes_with_css_class = new Set();
|
||||
|
||||
/**
|
||||
* @param {{
|
||||
* ast: import('#compiler').Style | null;
|
||||
* filename: string;
|
||||
* component_name: string;
|
||||
* get_css_hash: import('#compiler').CssHashGetter;
|
||||
* }} params
|
||||
*/
|
||||
constructor({ ast, component_name, filename, get_css_hash }) {
|
||||
this.ast = ast;
|
||||
this.filename = filename;
|
||||
|
||||
if (!ast || ast.children.length === 0) {
|
||||
this.has_styles = false;
|
||||
this.id = '';
|
||||
return;
|
||||
}
|
||||
|
||||
this.id = get_css_hash({
|
||||
filename,
|
||||
name: component_name,
|
||||
css: ast.content.styles,
|
||||
hash
|
||||
});
|
||||
this.has_styles = true;
|
||||
|
||||
const state = {
|
||||
/** @type {Atrule | undefined} */
|
||||
atrule: undefined
|
||||
};
|
||||
|
||||
walk(/** @type {import('#compiler').Css.Node} */ (ast), state, {
|
||||
Atrule: (node, context) => {
|
||||
const atrule = new Atrule(node);
|
||||
|
||||
if (context.state.atrule) {
|
||||
context.state.atrule.children.push(atrule);
|
||||
} else {
|
||||
this.children.push(atrule);
|
||||
}
|
||||
|
||||
if (is_keyframes_node(node)) {
|
||||
if (!node.prelude.startsWith('-global-')) {
|
||||
this.keyframes.set(node.prelude, `${this.id}-${node.prelude}`);
|
||||
}
|
||||
} else if (node.block) {
|
||||
/** @type {Declaration[]} */
|
||||
const declarations = [];
|
||||
|
||||
for (const child of node.block.children) {
|
||||
if (child.type === 'Declaration') {
|
||||
declarations.push(new Declaration(child));
|
||||
}
|
||||
}
|
||||
|
||||
if (declarations.length > 0) {
|
||||
push_array(atrule.declarations, declarations);
|
||||
}
|
||||
}
|
||||
|
||||
context.next({
|
||||
...context.state,
|
||||
atrule
|
||||
});
|
||||
},
|
||||
Rule: (node, context) => {
|
||||
const rule = new Rule(node, this, context.state.atrule);
|
||||
if (context.state.atrule) {
|
||||
context.state.atrule.children.push(rule);
|
||||
} else {
|
||||
this.children.push(rule);
|
||||
}
|
||||
|
||||
context.next();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** @param {import('#compiler').RegularElement | import('#compiler').SvelteElement} node */
|
||||
apply(node) {
|
||||
if (!this.has_styles) return;
|
||||
for (let i = 0; i < this.children.length; i += 1) {
|
||||
const child = this.children[i];
|
||||
child.apply(node);
|
||||
}
|
||||
}
|
||||
/** @param {boolean} is_dom_mode */
|
||||
reify(is_dom_mode) {
|
||||
nodes: for (const node of this.nodes_with_css_class) {
|
||||
// Dynamic elements in dom mode always use spread for attributes and therefore shouldn't have a class attribute added to them
|
||||
// TODO this happens during the analysis phase, which shouldn't know anything about client vs server
|
||||
if (node.type === 'SvelteElement' && is_dom_mode) continue;
|
||||
|
||||
/** @type {import('#compiler').Attribute | undefined} */
|
||||
let class_attribute = undefined;
|
||||
|
||||
for (const attribute of node.attributes) {
|
||||
if (attribute.type === 'SpreadAttribute') {
|
||||
// The spread method appends the hash to the end of the class attribute on its own
|
||||
continue nodes;
|
||||
}
|
||||
|
||||
if (attribute.type !== 'Attribute') continue;
|
||||
if (attribute.name.toLowerCase() !== 'class') continue;
|
||||
|
||||
class_attribute = attribute;
|
||||
}
|
||||
|
||||
if (class_attribute && class_attribute.value !== true) {
|
||||
const chunks = class_attribute.value;
|
||||
|
||||
if (chunks.length === 1 && chunks[0].type === 'Text') {
|
||||
chunks[0].data += ` ${this.id}`;
|
||||
} else {
|
||||
chunks.push({
|
||||
type: 'Text',
|
||||
data: ` ${this.id}`,
|
||||
raw: ` ${this.id}`,
|
||||
start: -1,
|
||||
end: -1,
|
||||
parent: null
|
||||
});
|
||||
}
|
||||
} else {
|
||||
node.attributes.push(
|
||||
create_attribute('class', -1, -1, [
|
||||
{ type: 'Text', data: this.id, raw: this.id, parent: null, start: -1, end: -1 }
|
||||
])
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} file
|
||||
* @param {string} source
|
||||
* @param {boolean} dev
|
||||
*/
|
||||
render(file, source, dev) {
|
||||
// TODO neaten this up
|
||||
if (!this.ast) throw new Error('Unexpected error');
|
||||
|
||||
const code = new MagicString(source);
|
||||
|
||||
walk(/** @type {import('#compiler').Css.Node} */ (this.ast), null, {
|
||||
_: (node) => {
|
||||
code.addSourcemapLocation(node.start);
|
||||
code.addSourcemapLocation(node.end);
|
||||
}
|
||||
});
|
||||
|
||||
for (const child of this.children) {
|
||||
child.transform(code, this.id, this.keyframes);
|
||||
}
|
||||
|
||||
code.remove(0, this.ast.content.start);
|
||||
|
||||
for (const child of this.children) {
|
||||
child.prune(code, dev);
|
||||
}
|
||||
|
||||
code.remove(/** @type {number} */ (this.ast.content.end), source.length);
|
||||
|
||||
return {
|
||||
code: code.toString(),
|
||||
map: code.generateMap({
|
||||
includeContent: true,
|
||||
source: this.filename,
|
||||
file
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
/** @param {import('../../types.js').ComponentAnalysis} analysis */
|
||||
validate(analysis) {
|
||||
this.children.forEach((child) => {
|
||||
child.validate(analysis);
|
||||
});
|
||||
}
|
||||
|
||||
/** @param {import('../../types.js').ComponentAnalysis} analysis */
|
||||
warn_on_unused_selectors(analysis) {
|
||||
// const ignores = !this.ast
|
||||
// ? []
|
||||
// : extract_ignores_above_position(this.ast.css.start, this.ast.html.children);
|
||||
// analysis.push_ignores(ignores);
|
||||
// this.children.forEach((child) => {
|
||||
// child.warn_on_unused_selector((selector) => {
|
||||
// analysis.warn(selector.node, {
|
||||
// code: 'css-unused-selector',
|
||||
// message: `Unused CSS selector "${this.source.slice(
|
||||
// selector.node.start,
|
||||
// selector.node.end
|
||||
// )}"`
|
||||
// });
|
||||
// });
|
||||
// });
|
||||
// analysis.pop_ignores();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,158 @@
|
||||
import { walk } from 'zimmerframe';
|
||||
import { error } from '../../../errors.js';
|
||||
import { is_keyframes_node } from '../../css.js';
|
||||
import { merge } from '../../visitors.js';
|
||||
|
||||
/**
|
||||
* @typedef {import('zimmerframe').Visitors<
|
||||
* import('#compiler').Css.Node,
|
||||
* {
|
||||
* keyframes: string[];
|
||||
* rule: import('#compiler').Css.Rule | null;
|
||||
* }
|
||||
* >} Visitors
|
||||
*/
|
||||
|
||||
/** @param {import('#compiler').Css.RelativeSelector} relative_selector */
|
||||
function is_global(relative_selector) {
|
||||
const first = relative_selector.selectors[0];
|
||||
|
||||
return (
|
||||
first.type === 'PseudoClassSelector' &&
|
||||
first.name === 'global' &&
|
||||
relative_selector.selectors.every(
|
||||
(selector) =>
|
||||
selector.type === 'PseudoClassSelector' || selector.type === 'PseudoElementSelector'
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/** @type {Visitors} */
|
||||
const analysis_visitors = {
|
||||
Atrule(node, context) {
|
||||
if (is_keyframes_node(node)) {
|
||||
if (!node.prelude.startsWith('-global-')) {
|
||||
context.state.keyframes.push(node.prelude);
|
||||
}
|
||||
}
|
||||
},
|
||||
ComplexSelector(node, context) {
|
||||
context.next(); // analyse relevant selectors first
|
||||
|
||||
node.metadata.rule = context.state.rule;
|
||||
|
||||
node.metadata.used = node.children.every(
|
||||
({ metadata }) => metadata.is_global || metadata.is_host || metadata.is_root
|
||||
);
|
||||
},
|
||||
RelativeSelector(node, context) {
|
||||
node.metadata.is_global =
|
||||
node.selectors.length >= 1 &&
|
||||
node.selectors[0].type === 'PseudoClassSelector' &&
|
||||
node.selectors[0].name === 'global' &&
|
||||
node.selectors.every(
|
||||
(selector) =>
|
||||
selector.type === 'PseudoClassSelector' || selector.type === 'PseudoElementSelector'
|
||||
);
|
||||
|
||||
if (node.selectors.length === 1) {
|
||||
const first = node.selectors[0];
|
||||
node.metadata.is_host = first.type === 'PseudoClassSelector' && first.name === 'host';
|
||||
}
|
||||
|
||||
node.metadata.is_root = !!node.selectors.find(
|
||||
(child) => child.type === 'PseudoClassSelector' && child.name === 'root'
|
||||
);
|
||||
|
||||
context.next();
|
||||
},
|
||||
Rule(node, context) {
|
||||
node.metadata.parent_rule = context.state.rule;
|
||||
|
||||
context.next({
|
||||
...context.state,
|
||||
rule: node
|
||||
});
|
||||
|
||||
node.metadata.has_local_selectors = node.prelude.children.some((selector) => {
|
||||
return selector.children.some(
|
||||
({ metadata }) => !metadata.is_global && !metadata.is_host && !metadata.is_root
|
||||
);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/** @type {Visitors} */
|
||||
const validation_visitors = {
|
||||
ComplexSelector(node, context) {
|
||||
// ensure `:global(...)` is not used in the middle of a selector
|
||||
{
|
||||
const a = node.children.findIndex((child) => !is_global(child));
|
||||
const b = node.children.findLastIndex((child) => !is_global(child));
|
||||
|
||||
if (a !== b) {
|
||||
for (let i = a; i <= b; i += 1) {
|
||||
if (is_global(node.children[i])) {
|
||||
error(node.children[i].selectors[0], 'invalid-css-global-placement');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ensure `:global(...)`contains a single selector
|
||||
// (standalone :global() with multiple selectors is OK)
|
||||
if (node.children.length > 1 || node.children[0].selectors.length > 1) {
|
||||
for (const relative_selector of node.children) {
|
||||
for (const selector of relative_selector.selectors) {
|
||||
if (
|
||||
selector.type === 'PseudoClassSelector' &&
|
||||
selector.name === 'global' &&
|
||||
selector.args !== null &&
|
||||
selector.args.children.length > 1
|
||||
) {
|
||||
error(selector, 'invalid-css-global-selector');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ensure `:global(...)` is not part of a larger compound selector
|
||||
for (const relative_selector of node.children) {
|
||||
for (let i = 0; i < relative_selector.selectors.length; i++) {
|
||||
const selector = relative_selector.selectors[i];
|
||||
|
||||
if (selector.type === 'PseudoClassSelector' && selector.name === 'global') {
|
||||
const child = selector.args?.children[0].children[0];
|
||||
if (
|
||||
child?.selectors[0].type === 'TypeSelector' &&
|
||||
!/[.:#]/.test(child.selectors[0].name[0]) &&
|
||||
(i !== 0 ||
|
||||
relative_selector.selectors
|
||||
.slice(1)
|
||||
.some(
|
||||
(s) => s.type !== 'PseudoElementSelector' && s.type !== 'PseudoClassSelector'
|
||||
))
|
||||
) {
|
||||
error(selector, 'invalid-css-global-selector-list');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
NestingSelector(node, context) {
|
||||
const rule = /** @type {import('#compiler').Css.Rule} */ (context.state.rule);
|
||||
if (!rule.metadata.parent_rule) {
|
||||
error(node, 'invalid-nesting-selector');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const css_visitors = merge(analysis_visitors, validation_visitors);
|
||||
|
||||
/**
|
||||
* @param {import('#compiler').Css.StyleSheet} stylesheet
|
||||
* @param {import('../../types.js').ComponentAnalysis} analysis
|
||||
*/
|
||||
export function analyze_css(stylesheet, analysis) {
|
||||
walk(stylesheet, { keyframes: analysis.css.keyframes, rule: null }, css_visitors);
|
||||
}
|
||||
@ -0,0 +1,792 @@
|
||||
import { walk } from 'zimmerframe';
|
||||
import { get_possible_values } from './utils.js';
|
||||
import { regex_ends_with_whitespace, regex_starts_with_whitespace } from '../../patterns.js';
|
||||
import { error } from '../../../errors.js';
|
||||
|
||||
/**
|
||||
* @typedef {{
|
||||
* stylesheet: import('#compiler').Css.StyleSheet;
|
||||
* element: import('#compiler').RegularElement | import('#compiler').SvelteElement;
|
||||
* }} State
|
||||
*/
|
||||
/** @typedef {NODE_PROBABLY_EXISTS | NODE_DEFINITELY_EXISTS} NodeExistsValue */
|
||||
|
||||
const NODE_PROBABLY_EXISTS = 0;
|
||||
const NODE_DEFINITELY_EXISTS = 1;
|
||||
|
||||
const whitelist_attribute_selector = new Map([
|
||||
['details', ['open']],
|
||||
['dialog', ['open']]
|
||||
]);
|
||||
|
||||
/** @type {import('#compiler').Css.Combinator} */
|
||||
const descendant_combinator = {
|
||||
type: 'Combinator',
|
||||
name: ' ',
|
||||
start: -1,
|
||||
end: -1
|
||||
};
|
||||
|
||||
/** @type {import('#compiler').Css.RelativeSelector} */
|
||||
const nesting_selector = {
|
||||
type: 'RelativeSelector',
|
||||
start: -1,
|
||||
end: -1,
|
||||
combinator: null,
|
||||
selectors: [
|
||||
{
|
||||
type: 'NestingSelector',
|
||||
name: '&',
|
||||
start: -1,
|
||||
end: -1
|
||||
}
|
||||
],
|
||||
metadata: {
|
||||
is_global: false,
|
||||
is_host: false,
|
||||
is_root: false,
|
||||
scoped: false
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {import('#compiler').Css.StyleSheet} stylesheet
|
||||
* @param {import('#compiler').RegularElement | import('#compiler').SvelteElement} element
|
||||
*/
|
||||
export function prune(stylesheet, element) {
|
||||
walk(stylesheet, { stylesheet, element }, visitors);
|
||||
}
|
||||
|
||||
/** @type {import('zimmerframe').Visitors<import('#compiler').Css.Node, State>} */
|
||||
const visitors = {
|
||||
ComplexSelector(node, context) {
|
||||
const selectors = truncate(node);
|
||||
const inner = selectors[selectors.length - 1];
|
||||
|
||||
if (node.metadata.rule?.metadata.parent_rule) {
|
||||
const has_explicit_nesting_selector = selectors.some((selector) =>
|
||||
selector.selectors.some((s) => s.type === 'NestingSelector')
|
||||
);
|
||||
|
||||
if (!has_explicit_nesting_selector) {
|
||||
selectors[0] = {
|
||||
...selectors[0],
|
||||
combinator: descendant_combinator
|
||||
};
|
||||
|
||||
selectors.unshift(nesting_selector);
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
apply_selector(
|
||||
selectors,
|
||||
/** @type {import('#compiler').Css.Rule} */ (node.metadata.rule),
|
||||
context.state.element,
|
||||
context.state.stylesheet
|
||||
)
|
||||
) {
|
||||
mark(inner, context.state.element);
|
||||
node.metadata.used = true;
|
||||
}
|
||||
|
||||
// note: we don't call context.next() here, we only recurse into
|
||||
// selectors that don't belong to rules (i.e. inside `:is(...)` etc)
|
||||
// when we encounter them below
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Discard trailing `:global(...)` selectors, these are unused for scoping purposes
|
||||
* @param {import('#compiler').Css.ComplexSelector} node
|
||||
*/
|
||||
function truncate(node) {
|
||||
const i = node.children.findLastIndex(({ metadata }) => {
|
||||
return !metadata.is_global && !metadata.is_host && !metadata.is_root;
|
||||
});
|
||||
|
||||
return node.children.slice(0, i + 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('#compiler').Css.RelativeSelector[]} relative_selectors
|
||||
* @param {import('#compiler').Css.Rule} rule
|
||||
* @param {import('#compiler').RegularElement | import('#compiler').SvelteElement} element
|
||||
* @param {import('#compiler').Css.StyleSheet} stylesheet
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function apply_selector(relative_selectors, rule, element, stylesheet) {
|
||||
const parent_selectors = relative_selectors.slice();
|
||||
const relative_selector = parent_selectors.pop();
|
||||
|
||||
if (!relative_selector) return false;
|
||||
|
||||
const possible_match = relative_selector_might_apply_to_node(
|
||||
relative_selector,
|
||||
rule,
|
||||
element,
|
||||
stylesheet
|
||||
);
|
||||
|
||||
if (!possible_match) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (relative_selector.combinator) {
|
||||
const name = relative_selector.combinator.name;
|
||||
|
||||
switch (name) {
|
||||
case ' ':
|
||||
case '>': {
|
||||
let parent = /** @type {import('#compiler').TemplateNode | null} */ (element.parent);
|
||||
|
||||
let parent_matched = false;
|
||||
let crossed_component_boundary = false;
|
||||
|
||||
while (parent) {
|
||||
if (parent.type === 'Component' || parent.type === 'SvelteComponent') {
|
||||
crossed_component_boundary = true;
|
||||
}
|
||||
|
||||
if (parent.type === 'RegularElement' || parent.type === 'SvelteElement') {
|
||||
if (apply_selector(parent_selectors, rule, parent, stylesheet)) {
|
||||
// TODO the `name === ' '` causes false positives, but removing it causes false negatives...
|
||||
if (name === ' ' || crossed_component_boundary) {
|
||||
mark(parent_selectors[parent_selectors.length - 1], parent);
|
||||
}
|
||||
|
||||
parent_matched = true;
|
||||
}
|
||||
|
||||
if (name === '>') return parent_matched;
|
||||
}
|
||||
|
||||
parent = /** @type {import('#compiler').TemplateNode | null} */ (parent.parent);
|
||||
}
|
||||
|
||||
return parent_matched || parent_selectors.every((selector) => is_global(selector, rule));
|
||||
}
|
||||
|
||||
case '+':
|
||||
case '~': {
|
||||
const siblings = get_possible_element_siblings(element, name === '+');
|
||||
|
||||
let sibling_matched = false;
|
||||
|
||||
for (const possible_sibling of siblings.keys()) {
|
||||
if (apply_selector(parent_selectors, rule, possible_sibling, stylesheet)) {
|
||||
mark(relative_selector, element);
|
||||
sibling_matched = true;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
sibling_matched ||
|
||||
(get_element_parent(element) === null &&
|
||||
parent_selectors.every((selector) => is_global(selector, rule)))
|
||||
);
|
||||
}
|
||||
|
||||
default:
|
||||
// TODO other combinators
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// if this is the left-most non-global selector, mark it — we want
|
||||
// `x y z {...}` to become `x.blah y z.blah {...}`
|
||||
const parent = parent_selectors[parent_selectors.length - 1];
|
||||
if (!parent || is_global(parent, rule)) {
|
||||
mark(relative_selector, element);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark both the compound selector and the node it selects as encapsulated,
|
||||
* for transformation in a later step
|
||||
* @param {import('#compiler').Css.RelativeSelector} relative_selector
|
||||
* @param {import('#compiler').RegularElement | import('#compiler').SvelteElement} element
|
||||
*/
|
||||
function mark(relative_selector, element) {
|
||||
relative_selector.metadata.scoped = true;
|
||||
element.metadata.scoped = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns `true` if the relative selector is global, meaning
|
||||
* it's a `:global(...)` or `:host` or `:root` selector, or
|
||||
* is an `:is(...)` or `:where(...)` selector that contains
|
||||
* a global selector
|
||||
* @param {import('#compiler').Css.RelativeSelector} selector
|
||||
* @param {import('#compiler').Css.Rule} rule
|
||||
*/
|
||||
function is_global(selector, rule) {
|
||||
if (selector.metadata.is_global || selector.metadata.is_host || selector.metadata.is_root) {
|
||||
return true;
|
||||
}
|
||||
|
||||
for (const s of selector.selectors) {
|
||||
/** @type {import('#compiler').Css.SelectorList | null} */
|
||||
let selector_list = null;
|
||||
let owner = rule;
|
||||
|
||||
if (s.type === 'PseudoClassSelector') {
|
||||
if ((s.name === 'is' || s.name === 'where') && s.args) {
|
||||
selector_list = s.args;
|
||||
}
|
||||
}
|
||||
|
||||
if (s.type === 'NestingSelector') {
|
||||
owner = /** @type {import('#compiler').Css.Rule} */ (rule.metadata.parent_rule);
|
||||
selector_list = owner.prelude;
|
||||
}
|
||||
|
||||
const has_global_selectors = selector_list?.children.some((complex_selector) => {
|
||||
return complex_selector.children.every((relative_selector) =>
|
||||
is_global(relative_selector, owner)
|
||||
);
|
||||
});
|
||||
|
||||
if (!has_global_selectors) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
const regex_backslash_and_following_character = /\\(.)/g;
|
||||
|
||||
/**
|
||||
* Ensure that `element` satisfies each simple selector in `relative_selector`
|
||||
*
|
||||
* @param {import('#compiler').Css.RelativeSelector} relative_selector
|
||||
* @param {import('#compiler').Css.Rule} rule
|
||||
* @param {import('#compiler').RegularElement | import('#compiler').SvelteElement} element
|
||||
* @param {import('#compiler').Css.StyleSheet} stylesheet
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function relative_selector_might_apply_to_node(relative_selector, rule, element, stylesheet) {
|
||||
for (const selector of relative_selector.selectors) {
|
||||
if (selector.type === 'Percentage' || selector.type === 'Nth') continue;
|
||||
|
||||
const name = selector.name.replace(regex_backslash_and_following_character, '$1');
|
||||
|
||||
switch (selector.type) {
|
||||
case 'PseudoClassSelector': {
|
||||
if (name === 'host' || name === 'root') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (name === 'global' && relative_selector.selectors.length === 1) {
|
||||
const args = /** @type {import('#compiler').Css.SelectorList} */ (selector.args);
|
||||
const complex_selector = args.children[0];
|
||||
return apply_selector(complex_selector.children, rule, element, stylesheet);
|
||||
}
|
||||
|
||||
if ((name === 'is' || name === 'where') && selector.args) {
|
||||
let matched = false;
|
||||
|
||||
for (const complex_selector of selector.args.children) {
|
||||
if (apply_selector(truncate(complex_selector), rule, element, stylesheet)) {
|
||||
complex_selector.metadata.used = true;
|
||||
matched = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!matched) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 'PseudoElementSelector': {
|
||||
break;
|
||||
}
|
||||
|
||||
case 'AttributeSelector': {
|
||||
const whitelisted = whitelist_attribute_selector.get(element.name.toLowerCase());
|
||||
if (
|
||||
!whitelisted?.includes(selector.name.toLowerCase()) &&
|
||||
!attribute_matches(
|
||||
element,
|
||||
selector.name,
|
||||
selector.value && unquote(selector.value),
|
||||
selector.matcher,
|
||||
selector.flags?.includes('i') ?? false
|
||||
)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'ClassSelector': {
|
||||
if (
|
||||
!attribute_matches(element, 'class', name, '~=', false) &&
|
||||
!element.attributes.some(
|
||||
(attribute) => attribute.type === 'ClassDirective' && attribute.name === name
|
||||
)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 'IdSelector': {
|
||||
if (!attribute_matches(element, 'id', name, '=', false)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 'TypeSelector': {
|
||||
if (
|
||||
element.name.toLowerCase() !== name.toLowerCase() &&
|
||||
name !== '*' &&
|
||||
element.type !== 'SvelteElement'
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 'NestingSelector': {
|
||||
let matched = false;
|
||||
|
||||
const parent = /** @type {import('#compiler').Css.Rule} */ (rule.metadata.parent_rule);
|
||||
|
||||
for (const complex_selector of parent.prelude.children) {
|
||||
if (apply_selector(truncate(complex_selector), parent, element, stylesheet)) {
|
||||
complex_selector.metadata.used = true;
|
||||
matched = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!matched) {
|
||||
return false;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// possible match
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {any} operator
|
||||
* @param {any} expected_value
|
||||
* @param {any} case_insensitive
|
||||
* @param {any} value
|
||||
*/
|
||||
function test_attribute(operator, expected_value, case_insensitive, value) {
|
||||
if (case_insensitive) {
|
||||
expected_value = expected_value.toLowerCase();
|
||||
value = value.toLowerCase();
|
||||
}
|
||||
switch (operator) {
|
||||
case '=':
|
||||
return value === expected_value;
|
||||
case '~=':
|
||||
return value.split(/\s/).includes(expected_value);
|
||||
case '|=':
|
||||
return `${value}-`.startsWith(`${expected_value}-`);
|
||||
case '^=':
|
||||
return value.startsWith(expected_value);
|
||||
case '$=':
|
||||
return value.endsWith(expected_value);
|
||||
case '*=':
|
||||
return value.includes(expected_value);
|
||||
default:
|
||||
throw new Error("this shouldn't happen");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('#compiler').RegularElement | import('#compiler').SvelteElement} node
|
||||
* @param {string} name
|
||||
* @param {string | null} expected_value
|
||||
* @param {string | null} operator
|
||||
* @param {boolean} case_insensitive
|
||||
*/
|
||||
function attribute_matches(node, name, expected_value, operator, case_insensitive) {
|
||||
for (const attribute of node.attributes) {
|
||||
if (attribute.type === 'SpreadAttribute') return true;
|
||||
if (attribute.type === 'BindDirective' && attribute.name === name) return true;
|
||||
|
||||
if (attribute.type !== 'Attribute') continue;
|
||||
if (attribute.name.toLowerCase() !== name.toLowerCase()) continue;
|
||||
|
||||
if (attribute.value === true) return operator === null;
|
||||
if (expected_value === null) return true;
|
||||
|
||||
const chunks = attribute.value;
|
||||
if (chunks.length === 1) {
|
||||
const value = chunks[0];
|
||||
if (value.type === 'Text') {
|
||||
return test_attribute(operator, expected_value, case_insensitive, value.data);
|
||||
}
|
||||
}
|
||||
|
||||
const possible_values = new Set();
|
||||
|
||||
/** @type {string[]} */
|
||||
let prev_values = [];
|
||||
for (const chunk of chunks) {
|
||||
const current_possible_values = get_possible_values(chunk);
|
||||
|
||||
// impossible to find out all combinations
|
||||
if (!current_possible_values) return true;
|
||||
|
||||
if (prev_values.length > 0) {
|
||||
/** @type {string[]} */
|
||||
const start_with_space = [];
|
||||
|
||||
/** @type {string[]} */
|
||||
const remaining = [];
|
||||
|
||||
current_possible_values.forEach((current_possible_value) => {
|
||||
if (regex_starts_with_whitespace.test(current_possible_value)) {
|
||||
start_with_space.push(current_possible_value);
|
||||
} else {
|
||||
remaining.push(current_possible_value);
|
||||
}
|
||||
});
|
||||
if (remaining.length > 0) {
|
||||
if (start_with_space.length > 0) {
|
||||
prev_values.forEach((prev_value) => possible_values.add(prev_value));
|
||||
}
|
||||
|
||||
/** @type {string[]} */
|
||||
const combined = [];
|
||||
|
||||
prev_values.forEach((prev_value) => {
|
||||
remaining.forEach((value) => {
|
||||
combined.push(prev_value + value);
|
||||
});
|
||||
});
|
||||
prev_values = combined;
|
||||
start_with_space.forEach((value) => {
|
||||
if (regex_ends_with_whitespace.test(value)) {
|
||||
possible_values.add(value);
|
||||
} else {
|
||||
prev_values.push(value);
|
||||
}
|
||||
});
|
||||
continue;
|
||||
} else {
|
||||
prev_values.forEach((prev_value) => possible_values.add(prev_value));
|
||||
prev_values = [];
|
||||
}
|
||||
}
|
||||
current_possible_values.forEach((current_possible_value) => {
|
||||
if (regex_ends_with_whitespace.test(current_possible_value)) {
|
||||
possible_values.add(current_possible_value);
|
||||
} else {
|
||||
prev_values.push(current_possible_value);
|
||||
}
|
||||
});
|
||||
if (prev_values.length < current_possible_values.size) {
|
||||
prev_values.push(' ');
|
||||
}
|
||||
if (prev_values.length > 20) {
|
||||
// might grow exponentially, bail out
|
||||
return true;
|
||||
}
|
||||
}
|
||||
prev_values.forEach((prev_value) => possible_values.add(prev_value));
|
||||
|
||||
for (const value of possible_values) {
|
||||
if (test_attribute(operator, expected_value, case_insensitive, value)) return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/** @param {string} str */
|
||||
function unquote(str) {
|
||||
if ((str[0] === str[str.length - 1] && str[0] === "'") || str[0] === '"') {
|
||||
return str.slice(1, str.length - 1);
|
||||
}
|
||||
return str;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('#compiler').RegularElement | import('#compiler').SvelteElement} node
|
||||
* @returns {import('#compiler').RegularElement | import('#compiler').SvelteElement | null}
|
||||
*/
|
||||
function get_element_parent(node) {
|
||||
/** @type {import('#compiler').SvelteNode | null} */
|
||||
let parent = node;
|
||||
while (
|
||||
// @ts-expect-error TODO figure out a more elegant solution
|
||||
(parent = parent.parent) &&
|
||||
parent.type !== 'RegularElement' &&
|
||||
parent.type !== 'SvelteElement'
|
||||
);
|
||||
return parent ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the given node's previous sibling in the DOM
|
||||
*
|
||||
* The Svelte `<slot>` is just a placeholder and is not actually real. Any children nodes
|
||||
* in `<slot>` are 'flattened' and considered as the same level as the `<slot>`'s siblings
|
||||
*
|
||||
* e.g.
|
||||
* ```html
|
||||
* <h1>Heading 1</h1>
|
||||
* <slot>
|
||||
* <h2>Heading 2</h2>
|
||||
* </slot>
|
||||
* ```
|
||||
*
|
||||
* is considered to look like:
|
||||
* ```html
|
||||
* <h1>Heading 1</h1>
|
||||
* <h2>Heading 2</h2>
|
||||
* ```
|
||||
* @param {import('#compiler').SvelteNode} node
|
||||
* @returns {import('#compiler').SvelteNode}
|
||||
*/
|
||||
function find_previous_sibling(node) {
|
||||
/** @type {import('#compiler').SvelteNode} */
|
||||
let current_node = node;
|
||||
do {
|
||||
if (current_node.type === 'SlotElement') {
|
||||
const slot_children = current_node.fragment.nodes;
|
||||
if (slot_children.length > 0) {
|
||||
current_node = slot_children.slice(-1)[0]; // go to its last child first
|
||||
continue;
|
||||
}
|
||||
}
|
||||
while (
|
||||
// @ts-expect-error TODO
|
||||
!current_node.prev &&
|
||||
// @ts-expect-error TODO
|
||||
current_node.parent &&
|
||||
// @ts-expect-error TODO
|
||||
current_node.parent.type === 'SlotElement'
|
||||
) {
|
||||
// @ts-expect-error TODO
|
||||
current_node = current_node.parent;
|
||||
}
|
||||
// @ts-expect-error
|
||||
current_node = current_node.prev;
|
||||
} while (current_node && current_node.type === 'SlotElement');
|
||||
return current_node;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('#compiler').SvelteNode} node
|
||||
* @param {boolean} adjacent_only
|
||||
* @returns {Map<import('#compiler').RegularElement, NodeExistsValue>}
|
||||
*/
|
||||
function get_possible_element_siblings(node, adjacent_only) {
|
||||
/** @type {Map<import('#compiler').RegularElement, NodeExistsValue>} */
|
||||
const result = new Map();
|
||||
|
||||
/** @type {import('#compiler').SvelteNode} */
|
||||
let prev = node;
|
||||
while ((prev = find_previous_sibling(prev))) {
|
||||
if (prev.type === 'RegularElement') {
|
||||
if (
|
||||
!prev.attributes.find(
|
||||
(attr) => attr.type === 'Attribute' && attr.name.toLowerCase() === 'slot'
|
||||
)
|
||||
) {
|
||||
result.set(prev, NODE_DEFINITELY_EXISTS);
|
||||
}
|
||||
if (adjacent_only) {
|
||||
break;
|
||||
}
|
||||
} else if (prev.type === 'EachBlock' || prev.type === 'IfBlock' || prev.type === 'AwaitBlock') {
|
||||
const possible_last_child = get_possible_last_child(prev, adjacent_only);
|
||||
add_to_map(possible_last_child, result);
|
||||
if (adjacent_only && has_definite_elements(possible_last_child)) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!prev || !adjacent_only) {
|
||||
/** @type {import('#compiler').SvelteNode | null} */
|
||||
let parent = node;
|
||||
|
||||
while (
|
||||
// @ts-expect-error TODO
|
||||
(parent = parent?.parent) &&
|
||||
(parent.type === 'EachBlock' || parent.type === 'IfBlock' || parent.type === 'AwaitBlock')
|
||||
) {
|
||||
const possible_siblings = get_possible_element_siblings(parent, adjacent_only);
|
||||
add_to_map(possible_siblings, result);
|
||||
|
||||
// @ts-expect-error
|
||||
if (parent.type === 'EachBlock' && !parent.fallback?.nodes.includes(node)) {
|
||||
// `{#each ...}<a /><b />{/each}` — `<b>` can be previous sibling of `<a />`
|
||||
add_to_map(get_possible_last_child(parent, adjacent_only), result);
|
||||
}
|
||||
|
||||
if (adjacent_only && has_definite_elements(possible_siblings)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('#compiler').EachBlock | import('#compiler').IfBlock | import('#compiler').AwaitBlock} relative_selector
|
||||
* @param {boolean} adjacent_only
|
||||
* @returns {Map<import('#compiler').RegularElement, NodeExistsValue>}
|
||||
*/
|
||||
function get_possible_last_child(relative_selector, adjacent_only) {
|
||||
/** @typedef {Map<import('#compiler').RegularElement, NodeExistsValue>} NodeMap */
|
||||
|
||||
/** @type {NodeMap} */
|
||||
const result = new Map();
|
||||
if (relative_selector.type === 'EachBlock') {
|
||||
/** @type {NodeMap} */
|
||||
const each_result = loop_child(relative_selector.body.nodes, adjacent_only);
|
||||
|
||||
/** @type {NodeMap} */
|
||||
const else_result = relative_selector.fallback
|
||||
? loop_child(relative_selector.fallback.nodes, adjacent_only)
|
||||
: new Map();
|
||||
const not_exhaustive = !has_definite_elements(else_result);
|
||||
if (not_exhaustive) {
|
||||
mark_as_probably(each_result);
|
||||
mark_as_probably(else_result);
|
||||
}
|
||||
add_to_map(each_result, result);
|
||||
add_to_map(else_result, result);
|
||||
} else if (relative_selector.type === 'IfBlock') {
|
||||
/** @type {NodeMap} */
|
||||
const if_result = loop_child(relative_selector.consequent.nodes, adjacent_only);
|
||||
|
||||
/** @type {NodeMap} */
|
||||
const else_result = relative_selector.alternate
|
||||
? loop_child(relative_selector.alternate.nodes, adjacent_only)
|
||||
: new Map();
|
||||
const not_exhaustive = !has_definite_elements(if_result) || !has_definite_elements(else_result);
|
||||
if (not_exhaustive) {
|
||||
mark_as_probably(if_result);
|
||||
mark_as_probably(else_result);
|
||||
}
|
||||
add_to_map(if_result, result);
|
||||
add_to_map(else_result, result);
|
||||
} else if (relative_selector.type === 'AwaitBlock') {
|
||||
/** @type {NodeMap} */
|
||||
const pending_result = relative_selector.pending
|
||||
? loop_child(relative_selector.pending.nodes, adjacent_only)
|
||||
: new Map();
|
||||
|
||||
/** @type {NodeMap} */
|
||||
const then_result = relative_selector.then
|
||||
? loop_child(relative_selector.then.nodes, adjacent_only)
|
||||
: new Map();
|
||||
|
||||
/** @type {NodeMap} */
|
||||
const catch_result = relative_selector.catch
|
||||
? loop_child(relative_selector.catch.nodes, adjacent_only)
|
||||
: new Map();
|
||||
const not_exhaustive =
|
||||
!has_definite_elements(pending_result) ||
|
||||
!has_definite_elements(then_result) ||
|
||||
!has_definite_elements(catch_result);
|
||||
if (not_exhaustive) {
|
||||
mark_as_probably(pending_result);
|
||||
mark_as_probably(then_result);
|
||||
mark_as_probably(catch_result);
|
||||
}
|
||||
add_to_map(pending_result, result);
|
||||
add_to_map(then_result, result);
|
||||
add_to_map(catch_result, result);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Map<import('#compiler').RegularElement, NodeExistsValue>} result
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function has_definite_elements(result) {
|
||||
if (result.size === 0) return false;
|
||||
for (const exist of result.values()) {
|
||||
if (exist === NODE_DEFINITELY_EXISTS) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Map<import('#compiler').RegularElement, NodeExistsValue>} from
|
||||
* @param {Map<import('#compiler').RegularElement, NodeExistsValue>} to
|
||||
* @returns {void}
|
||||
*/
|
||||
function add_to_map(from, to) {
|
||||
from.forEach((exist, element) => {
|
||||
to.set(element, higher_existence(exist, to.get(element)));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {NodeExistsValue | undefined} exist1
|
||||
* @param {NodeExistsValue | undefined} exist2
|
||||
* @returns {NodeExistsValue}
|
||||
*/
|
||||
function higher_existence(exist1, exist2) {
|
||||
// @ts-expect-error TODO figure out if this is a bug
|
||||
if (exist1 === undefined || exist2 === undefined) return exist1 || exist2;
|
||||
return exist1 > exist2 ? exist1 : exist2;
|
||||
}
|
||||
|
||||
/** @param {Map<import('#compiler').RegularElement, NodeExistsValue>} result */
|
||||
function mark_as_probably(result) {
|
||||
for (const key of result.keys()) {
|
||||
result.set(key, NODE_PROBABLY_EXISTS);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('#compiler').SvelteNode[]} children
|
||||
* @param {boolean} adjacent_only
|
||||
*/
|
||||
function loop_child(children, adjacent_only) {
|
||||
/** @type {Map<import('#compiler').RegularElement, NodeExistsValue>} */
|
||||
const result = new Map();
|
||||
for (let i = children.length - 1; i >= 0; i--) {
|
||||
const child = children[i];
|
||||
if (child.type === 'RegularElement') {
|
||||
result.set(child, NODE_DEFINITELY_EXISTS);
|
||||
if (adjacent_only) {
|
||||
break;
|
||||
}
|
||||
} else if (
|
||||
child.type === 'EachBlock' ||
|
||||
child.type === 'IfBlock' ||
|
||||
child.type === 'AwaitBlock'
|
||||
) {
|
||||
const child_result = get_possible_last_child(child, adjacent_only);
|
||||
add_to_map(child_result, result);
|
||||
if (adjacent_only && has_definite_elements(child_result)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@ -0,0 +1,334 @@
|
||||
import MagicString from 'magic-string';
|
||||
import { walk } from 'zimmerframe';
|
||||
import { is_keyframes_node, regex_css_name_boundary, remove_css_prefix } from '../../css.js';
|
||||
import { merge_with_preprocessor_map } from '../../../utils/mapped_code.js';
|
||||
|
||||
/**
|
||||
* @typedef {{
|
||||
* code: MagicString;
|
||||
* dev: boolean;
|
||||
* hash: string;
|
||||
* selector: string;
|
||||
* keyframes: string[];
|
||||
* specificity: {
|
||||
* bumped: boolean
|
||||
* }
|
||||
* }} State
|
||||
*/
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} source
|
||||
* @param {import('../../types.js').ComponentAnalysis} analysis
|
||||
* @param {import('#compiler').ValidatedCompileOptions} options
|
||||
*/
|
||||
export function render_stylesheet(source, analysis, options) {
|
||||
const code = new MagicString(source);
|
||||
|
||||
/** @type {State} */
|
||||
const state = {
|
||||
code,
|
||||
dev: options.dev,
|
||||
hash: analysis.css.hash,
|
||||
selector: `.${analysis.css.hash}`,
|
||||
keyframes: analysis.css.keyframes,
|
||||
specificity: {
|
||||
bumped: false
|
||||
}
|
||||
};
|
||||
|
||||
const ast = /** @type {import('#compiler').Css.StyleSheet} */ (analysis.css.ast);
|
||||
|
||||
walk(/** @type {import('#compiler').Css.Node} */ (ast), state, visitors);
|
||||
|
||||
code.remove(0, ast.content.start);
|
||||
code.remove(/** @type {number} */ (ast.content.end), source.length);
|
||||
|
||||
const css = {
|
||||
code: code.toString(),
|
||||
map: code.generateMap({
|
||||
// include source content; makes it easier/more robust looking up the source map code
|
||||
includeContent: true,
|
||||
// generateMap takes care of calculating source relative to file
|
||||
source: options.filename,
|
||||
file: options.cssOutputFilename || options.filename
|
||||
})
|
||||
};
|
||||
|
||||
merge_with_preprocessor_map(css, options, css.map.sources[0]);
|
||||
|
||||
if (options.dev && options.css === 'injected' && css.code) {
|
||||
css.code += `\n/*# sourceMappingURL=${css.map.toUrl()} */`;
|
||||
}
|
||||
|
||||
return css;
|
||||
}
|
||||
|
||||
/** @type {import('zimmerframe').Visitors<import('#compiler').Css.Node, State>} */
|
||||
const visitors = {
|
||||
_: (node, context) => {
|
||||
context.state.code.addSourcemapLocation(node.start);
|
||||
context.state.code.addSourcemapLocation(node.end);
|
||||
context.next();
|
||||
},
|
||||
Atrule(node, { state, next }) {
|
||||
if (is_keyframes_node(node)) {
|
||||
let start = node.start + node.name.length + 1;
|
||||
while (state.code.original[start] === ' ') start += 1;
|
||||
let end = start;
|
||||
while (state.code.original[end] !== '{' && state.code.original[end] !== ' ') end += 1;
|
||||
|
||||
if (node.prelude.startsWith('-global-')) {
|
||||
state.code.remove(start, start + 8);
|
||||
} else {
|
||||
state.code.prependRight(start, `${state.hash}-`);
|
||||
}
|
||||
|
||||
return; // don't transform anything within
|
||||
}
|
||||
|
||||
next();
|
||||
},
|
||||
Declaration(node, { state, next }) {
|
||||
const property = node.property && remove_css_prefix(node.property.toLowerCase());
|
||||
if (property === 'animation' || property === 'animation-name') {
|
||||
let index = node.start + node.property.length + 1;
|
||||
let name = '';
|
||||
|
||||
while (index < state.code.original.length) {
|
||||
const character = state.code.original[index];
|
||||
|
||||
if (regex_css_name_boundary.test(character)) {
|
||||
if (state.keyframes.includes(name)) {
|
||||
state.code.prependRight(index - name.length, `${state.hash}-`);
|
||||
}
|
||||
|
||||
if (character === ';' || character === '}') {
|
||||
break;
|
||||
}
|
||||
|
||||
name = '';
|
||||
} else {
|
||||
name += character;
|
||||
}
|
||||
|
||||
index++;
|
||||
}
|
||||
}
|
||||
},
|
||||
Rule(node, { state, next }) {
|
||||
// keep empty rules in dev, because it's convenient to
|
||||
// see them in devtools
|
||||
if (!state.dev && is_empty(node)) {
|
||||
state.code.prependRight(node.start, '/* (empty) ');
|
||||
state.code.appendLeft(node.end, '*/');
|
||||
escape_comment_close(node, state.code);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!is_used(node)) {
|
||||
state.code.prependRight(node.start, '/* (unused) ');
|
||||
state.code.appendLeft(node.end, '*/');
|
||||
escape_comment_close(node, state.code);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
next();
|
||||
},
|
||||
SelectorList(node, { state, next, path }) {
|
||||
let pruning = false;
|
||||
let last = node.children[0].start;
|
||||
|
||||
for (let i = 0; i < node.children.length; i += 1) {
|
||||
const selector = node.children[i];
|
||||
|
||||
if (selector.metadata.used === pruning) {
|
||||
if (pruning) {
|
||||
let i = selector.start;
|
||||
while (state.code.original[i] !== ',') i--;
|
||||
|
||||
state.code.overwrite(i, i + 1, '*/');
|
||||
} else {
|
||||
if (i === 0) {
|
||||
state.code.prependRight(selector.start, '/* (unused) ');
|
||||
} else {
|
||||
state.code.overwrite(last, selector.start, ' /* (unused) ');
|
||||
}
|
||||
}
|
||||
|
||||
pruning = !pruning;
|
||||
}
|
||||
|
||||
last = selector.end;
|
||||
}
|
||||
|
||||
if (pruning) {
|
||||
state.code.appendLeft(last, '*/');
|
||||
}
|
||||
|
||||
// if we're in a `:is(...)` or whatever, keep existing specificity bump state
|
||||
let specificity = state.specificity;
|
||||
|
||||
// if this selector list belongs to a rule, require a specificity bump for the
|
||||
// first scoped selector but only if we're at the top level
|
||||
let parent = path.at(-1);
|
||||
if (parent?.type === 'Rule') {
|
||||
specificity = { bumped: false };
|
||||
|
||||
/** @type {import('#compiler').Css.Rule | null} */
|
||||
let rule = parent.metadata.parent_rule;
|
||||
|
||||
while (rule) {
|
||||
if (rule.metadata.has_local_selectors) {
|
||||
specificity = { bumped: true };
|
||||
break;
|
||||
}
|
||||
rule = rule.metadata.parent_rule;
|
||||
}
|
||||
}
|
||||
|
||||
next({ ...state, specificity });
|
||||
},
|
||||
ComplexSelector(node, context) {
|
||||
/** @param {import('#compiler').Css.SimpleSelector} selector */
|
||||
function remove_global_pseudo_class(selector) {
|
||||
context.state.code
|
||||
.remove(selector.start, selector.start + ':global('.length)
|
||||
.remove(selector.end - 1, selector.end);
|
||||
}
|
||||
|
||||
for (const relative_selector of node.children) {
|
||||
if (relative_selector.metadata.is_global) {
|
||||
remove_global_pseudo_class(relative_selector.selectors[0]);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (relative_selector.metadata.scoped) {
|
||||
if (relative_selector.selectors.length === 1) {
|
||||
// skip standalone :is/:where/& selectors
|
||||
const selector = relative_selector.selectors[0];
|
||||
if (
|
||||
selector.type === 'PseudoClassSelector' &&
|
||||
(selector.name === 'is' || selector.name === 'where')
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (relative_selector.selectors.every((s) => s.type === 'NestingSelector')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// for the first occurrence, we use a classname selector, so that every
|
||||
// encapsulated selector gets a +0-1-0 specificity bump. thereafter,
|
||||
// we use a `:where` selector, which does not affect specificity
|
||||
let modifier = context.state.selector;
|
||||
if (context.state.specificity.bumped) modifier = `:where(${modifier})`;
|
||||
|
||||
context.state.specificity.bumped = true;
|
||||
|
||||
// TODO err... can this happen?
|
||||
for (const selector of relative_selector.selectors) {
|
||||
if (selector.type === 'PseudoClassSelector' && selector.name === 'global') {
|
||||
remove_global_pseudo_class(selector);
|
||||
}
|
||||
}
|
||||
|
||||
let i = relative_selector.selectors.length;
|
||||
while (i--) {
|
||||
const selector = relative_selector.selectors[i];
|
||||
|
||||
if (
|
||||
selector.type === 'PseudoElementSelector' ||
|
||||
selector.type === 'PseudoClassSelector'
|
||||
) {
|
||||
if (selector.name !== 'root' && selector.name !== 'host') {
|
||||
if (i === 0) context.state.code.prependRight(selector.start, modifier);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (selector.type === 'TypeSelector' && selector.name === '*') {
|
||||
context.state.code.update(selector.start, selector.end, modifier);
|
||||
} else {
|
||||
context.state.code.appendLeft(selector.end, modifier);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
context.next();
|
||||
},
|
||||
PseudoClassSelector(node, context) {
|
||||
if (node.name === 'is' || node.name === 'where') {
|
||||
context.next();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/** @param {import('#compiler').Css.Rule} rule */
|
||||
function is_empty(rule) {
|
||||
for (const child of rule.block.children) {
|
||||
if (child.type === 'Declaration') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (child.type === 'Rule') {
|
||||
if (is_used(child) && !is_empty(child)) return false;
|
||||
}
|
||||
|
||||
if (child.type === 'Atrule') {
|
||||
return false; // TODO
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/** @param {import('#compiler').Css.Rule} rule */
|
||||
function is_used(rule) {
|
||||
for (const selector of rule.prelude.children) {
|
||||
if (selector.metadata.used) return true;
|
||||
}
|
||||
|
||||
for (const child of rule.block.children) {
|
||||
if (child.type === 'Rule' && is_used(child)) return true;
|
||||
|
||||
if (child.type === 'Atrule') {
|
||||
return true; // TODO
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {import('#compiler').Css.Rule} node
|
||||
* @param {MagicString} code
|
||||
*/
|
||||
function escape_comment_close(node, code) {
|
||||
let escaped = false;
|
||||
let in_comment = false;
|
||||
|
||||
for (let i = node.start; i < node.end; i++) {
|
||||
if (escaped) {
|
||||
escaped = false;
|
||||
} else {
|
||||
const char = code.original[i];
|
||||
if (in_comment) {
|
||||
if (char === '*' && code.original[i + 1] === '/') {
|
||||
code.prependRight(++i, '\\');
|
||||
in_comment = false;
|
||||
}
|
||||
} else if (char === '\\') {
|
||||
escaped = true;
|
||||
} else if (char === '/' && code.original[++i] === '*') {
|
||||
in_comment = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,13 @@
|
||||
const regex_css_browser_prefix = /^-((webkit)|(moz)|(o)|(ms))-/;
|
||||
export const regex_css_name_boundary = /^[\s,;}]$/;
|
||||
|
||||
/**
|
||||
* @param {string} name
|
||||
* @returns {string}
|
||||
*/
|
||||
export function remove_css_prefix(name) {
|
||||
return name.replace(regex_css_browser_prefix, '');
|
||||
}
|
||||
|
||||
/** @param {import('#compiler').Css.Atrule} node */
|
||||
export const is_keyframes_node = (node) => remove_css_prefix(node.name) === 'keyframes';
|
||||
@ -0,0 +1,4 @@
|
||||
|
||||
/* (unused) x y z {
|
||||
color: red;
|
||||
}*/
|
||||
@ -0,0 +1,9 @@
|
||||
<x>
|
||||
<z></z>
|
||||
</x>
|
||||
|
||||
<style>
|
||||
x y z {
|
||||
color: red;
|
||||
}
|
||||
</style>
|
||||
@ -1,3 +1,3 @@
|
||||
/* (empty) .foo.svelte-xyz {
|
||||
/* (empty) .foo {
|
||||
/* empty *\/
|
||||
}*/
|
||||
|
||||
@ -1,12 +1,5 @@
|
||||
import { test } from '../../test';
|
||||
|
||||
export default test({
|
||||
warnings: [
|
||||
{
|
||||
code: 'css-unused-selector',
|
||||
message: 'Unused CSS selector "a:global(.foo) > div"',
|
||||
start: { character: 91, column: 1, line: 8 },
|
||||
end: { character: 111, column: 21, line: 8 }
|
||||
}
|
||||
]
|
||||
warnings: []
|
||||
});
|
||||
|
||||
@ -1,3 +1,3 @@
|
||||
div > div.svelte-xyz {
|
||||
a > b > div.svelte-xyz {
|
||||
color: red;
|
||||
}
|
||||
|
||||
@ -1,3 +1,3 @@
|
||||
<div class="svelte-xyz">
|
||||
<div class="svelte-xyz"></div>
|
||||
</div>
|
||||
<div></div>
|
||||
</div>
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
<style>
|
||||
:global(div) > div {
|
||||
:global(a) > :global(b) > div {
|
||||
color: red;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div>
|
||||
<div />
|
||||
</div>
|
||||
</div>
|
||||
@ -1,3 +0,0 @@
|
||||
a > b > div.svelte-xyz {
|
||||
color: red;
|
||||
}
|
||||
@ -1,3 +0,0 @@
|
||||
<div class="svelte-xyz">
|
||||
<div class="svelte-xyz"></div>
|
||||
</div>
|
||||
@ -1,9 +0,0 @@
|
||||
<style>
|
||||
:global(a) > :global(b) > div {
|
||||
color: red;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div>
|
||||
<div />
|
||||
</div>
|
||||
@ -1,3 +1,3 @@
|
||||
/* (unused) .foo .bar {
|
||||
/* (unused) :global(.foo) .bar {
|
||||
color: red;
|
||||
}*/
|
||||
|
||||
@ -0,0 +1,4 @@
|
||||
|
||||
x.svelte-xyz :is(y:where(.svelte-xyz) /* (unused) z*/) {
|
||||
color: purple;
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
<x>
|
||||
<y></y>
|
||||
</x>
|
||||
|
||||
<style>
|
||||
x :is(y, z) {
|
||||
color: purple;
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,65 @@
|
||||
|
||||
.a.svelte-xyz {
|
||||
color: green;
|
||||
|
||||
/* implicit & */
|
||||
.b:where(.svelte-xyz) /* (unused) .unused*/ {
|
||||
color: green;
|
||||
|
||||
.c:where(.svelte-xyz) {
|
||||
color: green;
|
||||
}
|
||||
|
||||
/* (unused) .unused {
|
||||
color: red;
|
||||
|
||||
.c {
|
||||
color: red;
|
||||
}
|
||||
}*/
|
||||
}
|
||||
|
||||
/* (empty) .d {
|
||||
.unused {
|
||||
color: red;
|
||||
}
|
||||
}*/
|
||||
|
||||
/* explicit & */
|
||||
& .b:where(.svelte-xyz) {
|
||||
color: green;
|
||||
|
||||
/* (empty) .c {
|
||||
& & {
|
||||
color: red;
|
||||
}
|
||||
}*/
|
||||
}
|
||||
|
||||
& & {
|
||||
color: green;
|
||||
}
|
||||
|
||||
/* silly but valid */
|
||||
&& {
|
||||
color: green;
|
||||
}
|
||||
|
||||
.container:where(.svelte-xyz) & {
|
||||
color: green;
|
||||
}
|
||||
|
||||
/* (unused) &.b {
|
||||
color: red;
|
||||
}*/
|
||||
|
||||
/* (unused) .unused {
|
||||
color: red;
|
||||
}*/
|
||||
}
|
||||
|
||||
blah {
|
||||
.a.svelte-xyz {
|
||||
color: green;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,80 @@
|
||||
<div class="a">
|
||||
<div class="a"></div>
|
||||
|
||||
<div class="b">
|
||||
<div class="c"></div>
|
||||
</div>
|
||||
|
||||
<div class="d"></div>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<div class="a"></div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.a {
|
||||
color: green;
|
||||
|
||||
/* implicit & */
|
||||
.b, .unused {
|
||||
color: green;
|
||||
|
||||
.c {
|
||||
color: green;
|
||||
}
|
||||
|
||||
.unused {
|
||||
color: red;
|
||||
|
||||
.c {
|
||||
color: red;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.d {
|
||||
.unused {
|
||||
color: red;
|
||||
}
|
||||
}
|
||||
|
||||
/* explicit & */
|
||||
& .b {
|
||||
color: green;
|
||||
|
||||
.c {
|
||||
& & {
|
||||
color: red;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
& & {
|
||||
color: green;
|
||||
}
|
||||
|
||||
/* silly but valid */
|
||||
&& {
|
||||
color: green;
|
||||
}
|
||||
|
||||
.container & {
|
||||
color: green;
|
||||
}
|
||||
|
||||
&.b {
|
||||
color: red;
|
||||
}
|
||||
|
||||
.unused {
|
||||
color: red;
|
||||
}
|
||||
}
|
||||
|
||||
:global(blah) {
|
||||
.a {
|
||||
color: green;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -1,3 +1,3 @@
|
||||
div.svelte-xyz section p:where(.svelte-xyz) {
|
||||
div.svelte-xyz section:where(.svelte-xyz) p:where(.svelte-xyz) {
|
||||
color: red;
|
||||
}
|
||||
|
||||
@ -1 +1 @@
|
||||
<div class="svelte-xyz"><section><p class="svelte-xyz">this is styled</p></section></div>
|
||||
<div class="svelte-xyz"><section class="svelte-xyz"><p class="svelte-xyz">this is styled</p></section></div>
|
||||
|
||||
@ -1,12 +1,12 @@
|
||||
<a class="svelte-xyz">
|
||||
<b>
|
||||
<c>
|
||||
<b class="svelte-xyz">
|
||||
<c class="svelte-xyz">
|
||||
<span class="svelte-xyz">
|
||||
Big red Comic Sans
|
||||
</span>
|
||||
</span>
|
||||
<span class="foo svelte-xyz">
|
||||
Big red Comic Sans
|
||||
</span>
|
||||
</c>
|
||||
</b>
|
||||
</a>
|
||||
</a>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue