@jesscss/scss-parser 2.0.0-alpha.8 → 2.0.0-alpha.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/interp.ts DELETED
@@ -1,158 +0,0 @@
1
- /**
2
- * SCSS `#{…}` interpolation helpers for the functional grammar builders.
3
- * Mirrors productions/helpers.ts (Chevrotain) without the nested parser bootstrap.
4
- */
5
- import {
6
- Any,
7
- Expression,
8
- Interpolated,
9
- INTERPOLATION_PLACEHOLDER,
10
- Reference,
11
- isNode,
12
- N,
13
- Node,
14
- type LocationInfo
15
- } from '@jesscss/core';
16
-
17
- export type InterpRole = 'ident' | 'property' | 'any' | 'name';
18
-
19
- let parseScssFnLazy: ((input: string, rule?: string) => import('./grammar.js').ScssFnParseResult) | undefined;
20
-
21
- /** Wired from grammar.ts after `parseScssFn` is defined (breaks circular import). */
22
- export function setParseScssFnForInterp(
23
- fn: (input: string, rule?: string) => import('./grammar.js').ScssFnParseResult
24
- ): void {
25
- parseScssFnLazy = fn;
26
- }
27
-
28
- export function findScssInterpolationSpans(value: string): Array<{ start: number; end: number; content: string }> {
29
- const matches: Array<{ start: number; end: number; content: string }> = [];
30
- let i = 0;
31
- while (i < value.length) {
32
- if (value[i] === '#' && value[i + 1] === '{') {
33
- const start = i;
34
- i += 2;
35
- let depth = 1;
36
- const contentStart = i;
37
- while (i < value.length && depth > 0) {
38
- const ch = value[i]!;
39
- if (ch === '{') {
40
- depth++;
41
- } else if (ch === '}') {
42
- depth--;
43
- }
44
- i++;
45
- }
46
- if (depth === 0) {
47
- matches.push({ start, end: i, content: value.slice(contentStart, i - 1) });
48
- }
49
- } else {
50
- i++;
51
- }
52
- }
53
- return matches;
54
- }
55
-
56
- function unwrapSingleReference(n: Node): Reference | undefined {
57
- if (
58
- isNode(n, N.Reference)
59
- && n.options?.type === 'variable'
60
- && !n.target
61
- && typeof n.key === 'string'
62
- ) {
63
- return n;
64
- }
65
- return undefined;
66
- }
67
-
68
- function valueFromParseResult(
69
- r: import('./grammar.js').ScssFnParseResult,
70
- fallback: string,
71
- loc: LocationInfo
72
- ): Node {
73
- const root = r.tree;
74
- if (isNode(root, N.Rules) && root.rules.length > 0) {
75
- return root.rules[0]!;
76
- }
77
- if (root instanceof Node) {
78
- return root;
79
- }
80
- return new Any(fallback, { role: 'any' }, loc);
81
- }
82
-
83
- /** Parse a `#{…}` inner expression via the functional value grammar. */
84
- export function parseScssInterpExpr(expr: string, loc: LocationInfo): Node {
85
- const trimmed = expr.trim();
86
- if (!trimmed) {
87
- return new Any('', { role: 'any' }, loc);
88
- }
89
- if (!parseScssFnLazy) {
90
- throw new Error('parseScssFn not wired for interpolation (setParseScssFnForInterp)');
91
- }
92
- const r = parseScssFnLazy(trimmed, 'valueList');
93
- if (r.errors.length) {
94
- return new Any(trimmed, { role: 'any' }, loc);
95
- }
96
- const tree = valueFromParseResult(r, trimmed, loc);
97
- const ref = unwrapSingleReference(tree);
98
- if (ref && typeof ref.key === 'string') {
99
- return new Reference({ key: ref.key }, { type: 'variable', role: 'ident' }, loc);
100
- }
101
- if (isNode(tree, N.Reference)) {
102
- return new Expression(tree, undefined, loc);
103
- }
104
- return tree;
105
- }
106
-
107
- /**
108
- * Validate a `selector.parse("…")` argument through the functional selector
109
- * grammar. Returns `true` when the text is a well-formed selector list. Used to
110
- * gate lifting a `selector.*` call into a `SelectorCapture`; the capture keeps the
111
- * lean string payload (`SelectorCapture` supports a bare-string `SelectorLike`).
112
- */
113
- export function isValidScssSelectorList(selectorText: string): boolean {
114
- const trimmed = selectorText.trim();
115
- if (!trimmed) {
116
- return false;
117
- }
118
- if (!parseScssFnLazy) {
119
- throw new Error('parseScssFn not wired for interpolation (setParseScssFnForInterp)');
120
- }
121
- const r = parseScssFnLazy(trimmed, 'SelectorList');
122
- return r.errors.length === 0;
123
- }
124
-
125
- /** Turn a parsed expression into an interpolation replacement (name/ident slots). */
126
- export function toInterpReplacement(expr: Node, loc: LocationInfo): Node {
127
- const ref = unwrapSingleReference(expr);
128
- if (ref && typeof ref.key === 'string') {
129
- return new Reference({ key: ref.key }, { type: 'variable', role: 'ident' }, loc);
130
- }
131
- if (isNode(expr, N.Reference)) {
132
- return new Expression(expr, undefined, loc);
133
- }
134
- return expr;
135
- }
136
-
137
- /** Build an `Interpolated` node from a string containing `#{…}` runs. */
138
- export function buildScssInterpolatedFromString(
139
- value: string,
140
- loc: LocationInfo,
141
- role: InterpRole
142
- ): Any | Interpolated {
143
- const matches = findScssInterpolationSpans(value);
144
- if (matches.length === 0) {
145
- return new Any(value, { role }, loc);
146
- }
147
- const replacements: Node[] = [];
148
- let source = value;
149
- let offset = 0;
150
- for (const match of matches) {
151
- const adjustedStart = match.start - offset;
152
- const adjustedEnd = match.end - offset;
153
- source = source.slice(0, adjustedStart) + INTERPOLATION_PLACEHOLDER + source.slice(adjustedEnd);
154
- offset += (match.end - match.start) - INTERPOLATION_PLACEHOLDER.length;
155
- replacements.push(toInterpReplacement(parseScssInterpExpr(match.content, loc), loc));
156
- }
157
- return new Interpolated({ source, replacements }, { role }, loc);
158
- }
package/src/jess.ts DELETED
@@ -1,11 +0,0 @@
1
- export { ScssGrammar } from './builders.js';
2
- export {
3
- ScssParser,
4
- ScssParser as Parser,
5
- parseScssFn,
6
- type ScssFnParseResult,
7
- type ScssFnParseOptions,
8
- type ScssParserConfig,
9
- type ScssRules,
10
- type SyntacticContentAssistSuggestion
11
- } from './functional-parser.js';
@@ -1,105 +0,0 @@
1
- import {
2
- Ampersand,
3
- ComplexSelector,
4
- isNode,
5
- N,
6
- Nil,
7
- Ruleset,
8
- SelectorList,
9
- sourceSpanOf,
10
- type LocationInfo,
11
- type Node,
12
- type Rules,
13
- type Selector,
14
- type TreeContext
15
- } from '@jesscss/core';
16
-
17
- export function createNullParentAmpersand(context?: TreeContext, selector?: Selector): Ampersand {
18
- const location = selector ? sourceSpanOf(selector) : undefined;
19
- const nil = new Nil(undefined, undefined, location, context);
20
- const amp = new Ampersand(
21
- { selectorContainer: { selector: nil } },
22
- undefined,
23
- location,
24
- context
25
- );
26
- amp.adopt(nil);
27
- return amp;
28
- }
29
-
30
- function getNodeLocation(node: Node): LocationInfo | undefined {
31
- return sourceSpanOf(node);
32
- }
33
-
34
- export function prefixAtRootSelector(selector: Selector, context?: TreeContext): Selector {
35
- if (isNode(selector, N.SelectorList)) {
36
- const list = selector;
37
- return new SelectorList(
38
- list.value.map(item => prefixAtRootSelector(item, context)),
39
- undefined,
40
- getNodeLocation(selector),
41
- context
42
- );
43
- }
44
-
45
- const amp = createNullParentAmpersand(context, selector);
46
- if (isNode(selector, N.ComplexSelector)) {
47
- const complex = selector;
48
- return new ComplexSelector(
49
- [amp, ...complex.value],
50
- undefined,
51
- getNodeLocation(selector),
52
- context
53
- );
54
- }
55
-
56
- return new ComplexSelector([amp, selector], undefined, getNodeLocation(selector), context);
57
- }
58
-
59
- export function lowerPlainAtRootRules(rules: Rules, context?: TreeContext): void {
60
- const transformRule = (node: Node): Node => {
61
- if (isNode(node, N.Ruleset)) {
62
- const rs = node;
63
- if (!isNode(rs.selector, N.Nil)) {
64
- return new Ruleset({
65
- selector: prefixAtRootSelector(rs.selector, context),
66
- rules: rs.rules,
67
- ...(rs.guard !== undefined && { guard: rs.guard }),
68
- ...(rs.selectorBeforeExtend !== undefined && {
69
- selectorBeforeExtend: rs.selectorBeforeExtend
70
- })
71
- }, rs.options, sourceSpanOf(rs), context);
72
- }
73
- return node;
74
- }
75
-
76
- if (isNode(node, N.AtRule) && node.rules) {
77
- lowerPlainAtRootRules(node.rules, context);
78
- return node;
79
- }
80
-
81
- if (isNode(node, N.If)) {
82
- lowerPlainAtRootRules(node, context);
83
- if (node.else) {
84
- lowerPlainAtRootRules(node.else, context);
85
- }
86
- return node;
87
- }
88
-
89
- if (isNode(node, N.For)) {
90
- lowerPlainAtRootRules(node, context);
91
- return node;
92
- }
93
-
94
- if (isNode(node, N.While)) {
95
- lowerPlainAtRootRules(node, context);
96
- return node;
97
- }
98
-
99
- return node;
100
- };
101
-
102
- for (let i = 0; i < rules.rules.length; i++) {
103
- rules.rules[i] = transformRule(rules.rules[i]!);
104
- }
105
- }
@@ -1,191 +0,0 @@
1
- /**
2
- * Shared helpers for SCSS module-system at-rules in the functional parser.
3
- */
4
- /* eslint-disable @typescript-eslint/no-unsafe-type-assertion */
5
- import {
6
- Any,
7
- Quoted,
8
- isNode,
9
- N,
10
- sourceSpanOf,
11
- type Node,
12
- type LocationInfo,
13
- type ExtendSelectorKind,
14
- Url
15
- } from '@jesscss/core';
16
-
17
- export function isScriptUsePath(path: string): boolean {
18
- return path.endsWith('.js') || path.endsWith('.ts') || path.endsWith('.json');
19
- }
20
-
21
- export function defaultNamespaceFromPath(path: string): string | undefined {
22
- if (path.startsWith('sass:')) {
23
- const name = path.slice('sass:'.length);
24
- return name.split('/').filter(Boolean).pop();
25
- }
26
- const base = path.split('/').filter(Boolean).pop();
27
- if (!base) {
28
- return undefined;
29
- }
30
- const noExt = base.replace(/\.(scss|sass|css|jess|js|ts|json)$/i, '');
31
- return noExt || undefined;
32
- }
33
-
34
- export function quotedLike(original: Quoted, nextValue: string, loc?: LocationInfo): Quoted {
35
- const quote = original.options?.quote ?? '"';
36
- const escaped = original.options?.escaped;
37
- const nodeLoc: LocationInfo | undefined = loc ?? sourceSpanOf(original);
38
- return new Quoted(new Any(nextValue, { role: 'any' }), { quote, escaped }, nodeLoc);
39
- }
40
-
41
- /**
42
- * Detect the CSS `@import` ordering violations Sass parse-rejects (`error/wrong_order/*`).
43
- * The full media-query-list / `supports()` grammar is out of scope for the
44
- * scanned prelude, so this catches the clearly-invalid, low-false-positive
45
- * shapes on the raw prelude text (everything after `@import`, minus the path):
46
- *
47
- * 1. A bare media feature `(x: y)` (NOT a `fn(...)` call — hence the
48
- * no-ident-before-`(` guard) followed by anything other than `and` / `or` /
49
- * `,` / `;` / end. Catches `"a" (b: c) supports(d: e)`, `"a" (b: c) d`,
50
- * `"a" (b: c) d(e)`.
51
- * 2. A comma directly followed by a function call `ident(` — a new import item
52
- * can never be `supports(...)` or an unknown function. Catches
53
- * `"a" b, supports(c: d)`, `"a" b, c(d)`, and `"a", url(b)`.
54
- *
55
- * Not caught (documented as remaining): a string after a comma in media context
56
- * (`"a" b, "c"` — indistinguishable from a valid plain-import continuation without
57
- * modelling media-vs-plain state) and `supports()` value-syntax errors
58
- * (`supports(--a:)`).
59
- */
60
- export function checkImportPreludeOrder(
61
- preludeText: string,
62
- recordError: (message: string) => void
63
- ): void {
64
- const text = preludeText;
65
- // A bare media feature may be followed by `and`/`or` (query chain), `,`/`;`
66
- // (list / end), `{`/`}` (block bounds), or `)` (closes an enclosing
67
- // `supports(...)` / group). Anything else — `supports(`, an ident, a function —
68
- // is the wrong-order violation.
69
- const badFeatureOrder = /(?<![-\w])\([^()]*:[^()]*\)\s*(?!and(?![-\w])|or(?![-\w])|[,;{})])\S/i;
70
- const badCommaFunction = /,\s*[a-zA-Z][-\w]*\s*\(/;
71
- if (badFeatureOrder.test(text) || badCommaFunction.test(text)) {
72
- recordError('Invalid @import: a media-query list must not follow a media feature without `and`/`or`, and a comma-separated @import item must be a URL or string (not `supports(…)` or another function).');
73
- }
74
- }
75
-
76
- export function isPlainCssImportPath(rawPath: string): boolean {
77
- return /\.css(?:$|[?#])/i.test(rawPath)
78
- || /^[a-z]+:\/\//i.test(rawPath)
79
- || rawPath.startsWith('//');
80
- }
81
-
82
- export function isPlainCssImportPrelude(prelude: Node, extraText: string | undefined): boolean {
83
- if (prelude instanceof Url) {
84
- return true;
85
- }
86
- if (extraText && extraText.trim()) {
87
- return true;
88
- }
89
- if (isNode(prelude, N.Quoted)) {
90
- return isPlainCssImportPath(prelude.valueOf());
91
- }
92
- return true;
93
- }
94
-
95
- export function findDisallowedExtendSelector(
96
- selector: Node,
97
- allowed: readonly ExtendSelectorKind[]
98
- ): { kind: ExtendSelectorKind; selector: Node } | undefined {
99
- if (isNode(selector, N.SelectorList)) {
100
- for (const item of selector.value) {
101
- const disallowed = findDisallowedExtendSelector(item as Node, allowed);
102
- if (disallowed) {
103
- return disallowed;
104
- }
105
- }
106
- return undefined;
107
- }
108
- const kinds: ExtendSelectorKind[] = isNode(selector, N.BasicSelector)
109
- ? ['simple', 'basic']
110
- : isNode(selector, N.PseudoSelector)
111
- ? ['simple', 'pseudo']
112
- : isNode(selector, N.CompoundSelector)
113
- ? ['compound']
114
- : isNode(selector, N.ComplexSelector)
115
- ? ['complex']
116
- : ['simple'];
117
- if (isNode(selector, N.CompoundSelector) && selector.value.length === 1) {
118
- return findDisallowedExtendSelector(selector.value[0] as Node, allowed);
119
- }
120
- if (isNode(selector, N.ComplexSelector) && selector.value.length === 1) {
121
- return findDisallowedExtendSelector(selector.value[0] as Node, allowed);
122
- }
123
- if (kinds.some(k => allowed.includes(k))) {
124
- return undefined;
125
- }
126
- return { kind: kinds[0]!, selector };
127
- }
128
-
129
- export function validateExtendTarget(
130
- target: Node,
131
- allowed: readonly ExtendSelectorKind[] | undefined,
132
- recordError: (message: string) => void
133
- ): void {
134
- if (!allowed) {
135
- return;
136
- }
137
- const disallowed = findDisallowedExtendSelector(target, allowed);
138
- if (!disallowed) {
139
- return;
140
- }
141
- const kindList = allowed.length === 1 ? `${allowed[0]} value` : allowed.join(', ');
142
- recordError(
143
- `@extend only allows ${kindList}, but found ${disallowed.kind} selector "${disallowed.selector.valueOf()}".`
144
- );
145
- }
146
-
147
- export function checkForwardPreludeErrors(
148
- preludeExtra: string | undefined,
149
- recordError: (message: string) => void
150
- ): void {
151
- if (!preludeExtra?.trim()) {
152
- return;
153
- }
154
- // Normalize away loud/silent comments and collapse whitespace so a prefix form
155
- // interrupted by a comment or newline (`as /**/ a-*`, `as //\n a-*`) still
156
- // matches the rejection patterns below.
157
- const text = preludeExtra
158
- .replace(/\/\*[\s\S]*?\*\//g, ' ')
159
- .replace(/\/\/[^\n\r]*/g, ' ')
160
- .replace(/\s+/g, ' ')
161
- .trim();
162
- if (/\bas\s+\S+-\*/.test(text)) {
163
- recordError(
164
- '@forward with "as <prefix>-*" prefixing is not supported in Jess and will never be. Use explicit namespacing instead.'
165
- );
166
- }
167
- if (/\b(show|hide)\b/.test(text)) {
168
- recordError(
169
- '@forward with "show"/"hide" lists is not supported in Jess and will never be. Visibility control belongs to the module itself.'
170
- );
171
- }
172
- }
173
-
174
- export function isPlaceholderExtendTarget(target: Node | string): boolean {
175
- if (typeof target === 'string') {
176
- return target.startsWith('\\');
177
- }
178
- if (isNode(target, N.BasicSelector)) {
179
- return target.value.startsWith('\\');
180
- }
181
- if (isNode(target, N.SelectorList) && target.value.length === 1) {
182
- return isPlaceholderExtendTarget(target.value[0] as Node);
183
- }
184
- if (isNode(target, N.CompoundSelector) && target.value.length === 1) {
185
- return isPlaceholderExtendTarget(target.value[0] as Node);
186
- }
187
- if (isNode(target, N.ComplexSelector) && target.value.length === 1) {
188
- return isPlaceholderExtendTarget(target.value[0] as Node);
189
- }
190
- return false;
191
- }
@@ -1,105 +0,0 @@
1
- /**
2
- * SCSS value desugaring helpers for the functional grammar builders.
3
- * Ports productions/helpers.ts without the Chevrotain parser bootstrap.
4
- */
5
- import {
6
- Call,
7
- Reference,
8
- isNode,
9
- N,
10
- type LocationInfo,
11
- type Node
12
- } from '@jesscss/core';
13
-
14
- export function unwrapSingleSequence(n: Node): Node {
15
- if (isNode(n, N.Sequence) && n.value.length === 1) {
16
- return n.value[0]!;
17
- }
18
- return n;
19
- }
20
-
21
- export function toDeclKey(node: Node): string {
22
- return String(node.valueOf());
23
- }
24
-
25
- export function isValidIdentifierKey(key: string): boolean {
26
- return /^[a-zA-Z_-][a-zA-Z0-9_-]*$/.test(key);
27
- }
28
-
29
- export function makeNamespacedReference(
30
- parts: string[],
31
- finalType: 'variable' | 'function' | 'mixin' | 'mixin-ruleset',
32
- loc: LocationInfo
33
- ): Reference {
34
- let current: Reference = new Reference(parts[0]!, { type: 'variable' }, loc);
35
- for (let i = 1; i < parts.length; i++) {
36
- const isFinal = i === parts.length - 1;
37
- current = new Reference(
38
- { target: current, key: parts[i]! },
39
- { type: isFinal ? finalType : 'index' },
40
- loc
41
- );
42
- }
43
- return current;
44
- }
45
-
46
- export function desugarNamespacedCall(call: Call, loc: LocationInfo): Call {
47
- const { name, args } = call;
48
- if (typeof name !== 'string') {
49
- return call;
50
- }
51
- if (!name.includes('.')) {
52
- return call;
53
- }
54
- if (name === 'map.get') {
55
- return call;
56
- }
57
- const parts = name.split('.').filter(Boolean);
58
- if (parts.length < 2) {
59
- return call;
60
- }
61
- const ref = makeNamespacedReference(parts, 'function', loc);
62
- return new Call({ name: ref, args }, call.options, loc);
63
- }
64
-
65
- export function desugarMapLookup(call: Call, loc: LocationInfo): Node {
66
- const { name, args: argsList } = call;
67
- if (typeof name !== 'string') {
68
- return call;
69
- }
70
- if (name !== 'map-get' && name !== 'map.get') {
71
- return call;
72
- }
73
-
74
- const args = isNode(argsList, N.List) ? argsList.value : [];
75
- if (args.length < 2) {
76
- return call;
77
- }
78
-
79
- const mapExpr = unwrapSingleSequence(args[0]!);
80
- const keyArgs = args.slice(1).map(a => unwrapSingleSequence(a));
81
-
82
- const initialTarget =
83
- isNode(mapExpr, N.Reference)
84
- ? mapExpr
85
- : isNode(mapExpr, N.Call)
86
- ? mapExpr
87
- : undefined;
88
-
89
- if (!initialTarget) {
90
- return call;
91
- }
92
-
93
- let currentTarget: Reference | Call = initialTarget;
94
- for (const keyNode of keyArgs) {
95
- const keyStr = toDeclKey(keyNode);
96
- const useDeclaration = isValidIdentifierKey(keyStr);
97
- currentTarget = new Reference(
98
- { target: currentTarget, key: useDeclaration ? keyStr : keyNode },
99
- { type: useDeclaration ? 'declaration' : 'index' },
100
- loc
101
- );
102
- }
103
-
104
- return currentTarget;
105
- }