Skip to content

Commit fd77c40

Browse files
authored
Merge pull request #969 from sveltejs/css-preprocessing
Css preprocessing
2 parents 72bd23a + b856ac2 commit fd77c40

File tree

4 files changed

+246
-18
lines changed

4 files changed

+246
-18
lines changed

src/index.ts

Lines changed: 70 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ import generate from './generators/dom/index';
44
import generateSSR from './generators/server-side-rendering/index';
55
import { assign } from './shared/index.js';
66
import Stylesheet from './css/Stylesheet';
7-
import { Parsed, CompileOptions, Warning } from './interfaces';
7+
import { Parsed, CompileOptions, Warning, PreprocessOptions, Preprocessor } from './interfaces';
8+
import { SourceMap } from 'magic-string';
89

910
const version = '__VERSION__';
1011

@@ -34,9 +35,74 @@ function defaultOnerror(error: Error) {
3435
throw error;
3536
}
3637

38+
function parseAttributeValue(value: string) {
39+
return /^['"]/.test(value) ?
40+
value.slice(1, -1) :
41+
value;
42+
}
43+
44+
function parseAttributes(str: string) {
45+
const attrs = {};
46+
str.split(/\s+/).filter(Boolean).forEach(attr => {
47+
const [name, value] = attr.split('=');
48+
attrs[name] = value ? parseAttributeValue(value) : true;
49+
});
50+
return attrs;
51+
}
52+
53+
async function replaceTagContents(source, type: 'script' | 'style', preprocessor: Preprocessor) {
54+
const exp = new RegExp(`<${type}([\\S\\s]*?)>([\\S\\s]*?)<\\/${type}>`, 'ig');
55+
const match = exp.exec(source);
56+
57+
if (match) {
58+
const attributes: Record<string, string | boolean> = parseAttributes(match[1]);
59+
const content: string = match[2];
60+
const processed: { code: string, map?: SourceMap | string } = await preprocessor({
61+
content,
62+
attributes
63+
});
64+
65+
if (processed && processed.code) {
66+
return (
67+
source.slice(0, match.index) +
68+
`<${type}>${processed.code}</${type}>` +
69+
source.slice(match.index + match[0].length)
70+
);
71+
}
72+
}
73+
74+
return source;
75+
}
76+
77+
export async function preprocess(source: string, options: PreprocessOptions) {
78+
const { markup, style, script } = options;
79+
if (!!markup) {
80+
const processed: { code: string, map?: SourceMap | string } = await markup({ content: source });
81+
source = processed.code;
82+
}
83+
84+
if (!!style) {
85+
source = await replaceTagContents(source, 'style', style);
86+
}
87+
88+
if (!!script) {
89+
source = await replaceTagContents(source, 'script', script);
90+
}
91+
92+
return {
93+
// TODO return separated output, in future version where svelte.compile supports it:
94+
// style: { code: styleCode, map: styleMap },
95+
// script { code: scriptCode, map: scriptMap },
96+
// markup { code: markupCode, map: markupMap },
97+
98+
toString() {
99+
return source;
100+
}
101+
};
102+
}
103+
37104
export function compile(source: string, _options: CompileOptions) {
38105
const options = normalizeOptions(_options);
39-
40106
let parsed: Parsed;
41107

42108
try {
@@ -53,7 +119,7 @@ export function compile(source: string, _options: CompileOptions) {
53119
const compiler = options.generate === 'ssr' ? generateSSR : generate;
54120

55121
return compiler(parsed, source, stylesheet, options);
56-
}
122+
};
57123

58124
export function create(source: string, _options: CompileOptions = {}) {
59125
_options.format = 'eval';
@@ -65,7 +131,7 @@ export function create(source: string, _options: CompileOptions = {}) {
65131
}
66132

67133
try {
68-
return (0,eval)(compiled.code);
134+
return (0, eval)(compiled.code);
69135
} catch (err) {
70136
if (_options.onerror) {
71137
_options.onerror(err);

src/interfaces.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import {SourceMap} from 'magic-string';
2+
13
export interface Node {
24
start: number;
35
end: number;
@@ -78,4 +80,12 @@ export interface Visitor {
7880
export interface CustomElementOptions {
7981
tag?: string;
8082
props?: string[];
81-
}
83+
}
84+
85+
export interface PreprocessOptions {
86+
markup?: (options: {content: string}) => { code: string, map?: SourceMap | string };
87+
style?: Preprocessor;
88+
script?: Preprocessor;
89+
}
90+
91+
export type Preprocessor = (options: {content: string, attributes: Record<string, string | boolean>}) => { code: string, map?: SourceMap | string };

test/preprocess/index.js

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
import assert from 'assert';
2+
import {svelte} from '../helpers.js';
3+
4+
describe('preprocess', () => {
5+
it('preprocesses entire component', () => {
6+
const source = `
7+
<h1>Hello __NAME__!</h1>
8+
`;
9+
10+
const expected = `
11+
<h1>Hello world!</h1>
12+
`;
13+
14+
return svelte.preprocess(source, {
15+
markup: ({ content }) => {
16+
return {
17+
code: content.replace('__NAME__', 'world')
18+
};
19+
}
20+
}).then(processed => {
21+
assert.equal(processed.toString(), expected);
22+
});
23+
});
24+
25+
it('preprocesses style', () => {
26+
const source = `
27+
<div class='brand-color'>$brand</div>
28+
29+
<style>
30+
.brand-color {
31+
color: $brand;
32+
}
33+
</style>
34+
`;
35+
36+
const expected = `
37+
<div class='brand-color'>$brand</div>
38+
39+
<style>
40+
.brand-color {
41+
color: purple;
42+
}
43+
</style>
44+
`;
45+
46+
return svelte.preprocess(source, {
47+
style: ({ content }) => {
48+
return {
49+
code: content.replace('$brand', 'purple')
50+
};
51+
}
52+
}).then(processed => {
53+
assert.equal(processed.toString(), expected);
54+
});
55+
});
56+
57+
it('preprocesses style asynchronously', () => {
58+
const source = `
59+
<div class='brand-color'>$brand</div>
60+
61+
<style>
62+
.brand-color {
63+
color: $brand;
64+
}
65+
</style>
66+
`;
67+
68+
const expected = `
69+
<div class='brand-color'>$brand</div>
70+
71+
<style>
72+
.brand-color {
73+
color: purple;
74+
}
75+
</style>
76+
`;
77+
78+
return svelte.preprocess(source, {
79+
style: ({ content }) => {
80+
return Promise.resolve({
81+
code: content.replace('$brand', 'purple')
82+
});
83+
}
84+
}).then(processed => {
85+
assert.equal(processed.toString(), expected);
86+
});
87+
});
88+
89+
it('preprocesses script', () => {
90+
const source = `
91+
<script>
92+
console.log(__THE_ANSWER__);
93+
</script>
94+
`;
95+
96+
const expected = `
97+
<script>
98+
console.log(42);
99+
</script>
100+
`;
101+
102+
return svelte.preprocess(source, {
103+
script: ({ content }) => {
104+
return {
105+
code: content.replace('__THE_ANSWER__', '42')
106+
};
107+
}
108+
}).then(processed => {
109+
assert.equal(processed.toString(), expected);
110+
});
111+
});
112+
113+
it('parses attributes', () => {
114+
const source = `
115+
<style type='text/scss' data-foo="bar" bool></style>
116+
`;
117+
118+
return svelte.preprocess(source, {
119+
style: ({ attributes }) => {
120+
assert.deepEqual(attributes, {
121+
type: 'text/scss',
122+
'data-foo': 'bar',
123+
bool: true
124+
});
125+
}
126+
});
127+
});
128+
129+
it('ignores null/undefined returned from preprocessor', () => {
130+
const source = `
131+
<script>
132+
console.log('ignore me');
133+
</script>
134+
`;
135+
136+
const expected = `
137+
<script>
138+
console.log('ignore me');
139+
</script>
140+
`;
141+
142+
return svelte.preprocess(source, {
143+
script: () => null
144+
}).then(processed => {
145+
assert.equal(processed.toString(), expected);
146+
});
147+
});
148+
});

yarn.lock

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,8 @@ ajv@^4.9.1:
6464
json-stable-stringify "^1.0.1"
6565

6666
ajv@^5.1.0, ajv@^5.2.3, ajv@^5.3.0:
67-
version "5.5.0"
68-
resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.5.0.tgz#eb2840746e9dc48bd5e063a36e3fd400c5eab5a9"
67+
version "5.5.1"
68+
resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.5.1.tgz#b38bb8876d9e86bee994956a04e721e88b248eb2"
6969
dependencies:
7070
co "^4.6.0"
7171
fast-deep-equal "^1.0.0"
@@ -538,8 +538,8 @@ [email protected]:
538538
graceful-readlink ">= 1.0.0"
539539

540540
commander@^2.9.0:
541-
version "2.12.1"
542-
resolved "https://registry.yarnpkg.com/commander/-/commander-2.12.1.tgz#468635c4168d06145b9323356d1da84d14ac4a7a"
541+
version "2.12.2"
542+
resolved "https://registry.yarnpkg.com/commander/-/commander-2.12.2.tgz#0f5946c427ed9ec0d91a46bb9def53e54650e555"
543543

544544
commondir@^1.0.1:
545545
version "1.0.1"
@@ -887,8 +887,8 @@ eslint-scope@^3.7.1:
887887
estraverse "^4.1.1"
888888

889889
eslint@^4.3.0:
890-
version "4.12.0"
891-
resolved "https://registry.yarnpkg.com/eslint/-/eslint-4.12.0.tgz#a7ce78eba8cc8f2443acfbbc870cc31a65135884"
890+
version "4.12.1"
891+
resolved "https://registry.yarnpkg.com/eslint/-/eslint-4.12.1.tgz#5ec1973822b4a066b353770c3c6d69a2a188e880"
892892
dependencies:
893893
ajv "^5.3.0"
894894
babel-code-frame "^6.22.0"
@@ -1027,10 +1027,14 @@ extract-zip@^1.0.3:
10271027
mkdirp "0.5.0"
10281028
yauzl "2.4.1"
10291029

1030-
[email protected], extsprintf@^1.2.0:
1030+
10311031
version "1.3.0"
10321032
resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05"
10331033

1034+
extsprintf@^1.2.0:
1035+
version "1.4.0"
1036+
resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f"
1037+
10341038
fast-deep-equal@^1.0.0:
10351039
version "1.0.0"
10361040
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.0.0.tgz#96256a3bc975595eb36d82e9929d060d893439ff"
@@ -1600,8 +1604,8 @@ is-path-in-cwd@^1.0.0:
16001604
is-path-inside "^1.0.0"
16011605

16021606
is-path-inside@^1.0.0:
1603-
version "1.0.0"
1604-
resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-1.0.0.tgz#fc06e5a1683fbda13de667aff717bbc10a48f37f"
1607+
version "1.0.1"
1608+
resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-1.0.1.tgz#8ef5b7de50437a3fdca6b4e865ef7aa55cb48036"
16051609
dependencies:
16061610
path-is-inside "^1.0.1"
16071611

@@ -1872,8 +1876,8 @@ load-json-file@^2.0.0:
18721876
strip-bom "^3.0.0"
18731877

18741878
locate-character@^2.0.0:
1875-
version "2.0.1"
1876-
resolved "https://registry.yarnpkg.com/locate-character/-/locate-character-2.0.1.tgz#48f9599f342daf26f73db32f45941eae37bae391"
1879+
version "2.0.3"
1880+
resolved "https://registry.yarnpkg.com/locate-character/-/locate-character-2.0.3.tgz#85a5aedae26b3536c3e97016af164cdaa3ae5ae1"
18771881

18781882
locate-path@^2.0.0:
18791883
version "2.0.0"
@@ -3292,8 +3296,8 @@ typescript@^1.8.9:
32923296
resolved "https://registry.yarnpkg.com/typescript/-/typescript-1.8.10.tgz#b475d6e0dff0bf50f296e5ca6ef9fbb5c7320f1e"
32933297

32943298
typescript@^2.6.1:
3295-
version "2.6.1"
3296-
resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.6.1.tgz#ef39cdea27abac0b500242d6726ab90e0c846631"
3299+
version "2.6.2"
3300+
resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.6.2.tgz#3c5b6fd7f6de0914269027f03c0946758f7673a4"
32973301

32983302
uglify-js@^2.6:
32993303
version "2.8.29"

0 commit comments

Comments
 (0)