Skip to content

Css preprocessing #969

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 13 commits into from
Dec 3, 2017
74 changes: 70 additions & 4 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import generate from './generators/dom/index';
import generateSSR from './generators/server-side-rendering/index';
import { assign } from './shared/index.js';
import Stylesheet from './css/Stylesheet';
import { Parsed, CompileOptions, Warning } from './interfaces';
import { Parsed, CompileOptions, Warning, PreprocessOptions, Preprocessor } from './interfaces';
import { SourceMap } from 'magic-string';

const version = '__VERSION__';

Expand Down Expand Up @@ -34,9 +35,74 @@ function defaultOnerror(error: Error) {
throw error;
}

function parseAttributeValue(value: string) {
return /^['"]/.test(value) ?
value.slice(1, -1) :
value;
}

function parseAttributes(str: string) {
const attrs = {};
str.split(/\s+/).filter(Boolean).forEach(attr => {
const [name, value] = attr.split('=');
attrs[name] = value ? parseAttributeValue(value) : true;
});
return attrs;
}

async function replaceTagContents(source, type: 'script' | 'style', preprocessor: Preprocessor) {
const exp = new RegExp(`<${type}([\\S\\s]*?)>([\\S\\s]*?)<\\/${type}>`, 'ig');
const match = exp.exec(source);

if (match) {
const attributes: Record<string, string | boolean> = parseAttributes(match[1]);
const content: string = match[2];
const processed: { code: string, map?: SourceMap | string } = await preprocessor({
content,
attributes
});

if (processed && processed.code) {
return (
source.slice(0, match.index) +
`<${type}>${processed.code}</${type}>` +
source.slice(match.index + match[0].length)
);
}
}

return source;
}

export async function preprocess(source: string, options: PreprocessOptions) {
const { markup, style, script } = options;
if (!!markup) {
const processed: { code: string, map?: SourceMap | string } = await markup({ content: source });
source = processed.code;
}

if (!!style) {
source = await replaceTagContents(source, 'style', style);
}

if (!!script) {
source = await replaceTagContents(source, 'script', script);
}

return {
// TODO return separated output, in future version where svelte.compile supports it:
// style: { code: styleCode, map: styleMap },
// script { code: scriptCode, map: scriptMap },
// markup { code: markupCode, map: markupMap },

toString() {
return source;
}
};
}

export function compile(source: string, _options: CompileOptions) {
const options = normalizeOptions(_options);

let parsed: Parsed;

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

return compiler(parsed, source, stylesheet, options);
}
};

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

try {
return (0,eval)(compiled.code);
return (0, eval)(compiled.code);
} catch (err) {
if (_options.onerror) {
_options.onerror(err);
Expand Down
12 changes: 11 additions & 1 deletion src/interfaces.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import {SourceMap} from 'magic-string';

export interface Node {
start: number;
end: number;
Expand Down Expand Up @@ -78,4 +80,12 @@ export interface Visitor {
export interface CustomElementOptions {
tag?: string;
props?: string[];
}
}

export interface PreprocessOptions {
markup?: (options: {content: string}) => { code: string, map?: SourceMap | string };
style?: Preprocessor;
script?: Preprocessor;
}

export type Preprocessor = (options: {content: string, attributes: Record<string, string | boolean>}) => { code: string, map?: SourceMap | string };
148 changes: 148 additions & 0 deletions test/preprocess/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
import assert from 'assert';
import {svelte} from '../helpers.js';

describe('preprocess', () => {
it('preprocesses entire component', () => {
const source = `
<h1>Hello __NAME__!</h1>
`;

const expected = `
<h1>Hello world!</h1>
`;

return svelte.preprocess(source, {
markup: ({ content }) => {
return {
code: content.replace('__NAME__', 'world')
};
}
}).then(processed => {
assert.equal(processed.toString(), expected);
});
});

it('preprocesses style', () => {
const source = `
<div class='brand-color'>$brand</div>

<style>
.brand-color {
color: $brand;
}
</style>
`;

const expected = `
<div class='brand-color'>$brand</div>

<style>
.brand-color {
color: purple;
}
</style>
`;

return svelte.preprocess(source, {
style: ({ content }) => {
return {
code: content.replace('$brand', 'purple')
};
}
}).then(processed => {
assert.equal(processed.toString(), expected);
});
});

it('preprocesses style asynchronously', () => {
const source = `
<div class='brand-color'>$brand</div>

<style>
.brand-color {
color: $brand;
}
</style>
`;

const expected = `
<div class='brand-color'>$brand</div>

<style>
.brand-color {
color: purple;
}
</style>
`;

return svelte.preprocess(source, {
style: ({ content }) => {
return Promise.resolve({
code: content.replace('$brand', 'purple')
});
}
}).then(processed => {
assert.equal(processed.toString(), expected);
});
});

it('preprocesses script', () => {
const source = `
<script>
console.log(__THE_ANSWER__);
</script>
`;

const expected = `
<script>
console.log(42);
</script>
`;

return svelte.preprocess(source, {
script: ({ content }) => {
return {
code: content.replace('__THE_ANSWER__', '42')
};
}
}).then(processed => {
assert.equal(processed.toString(), expected);
});
});

it('parses attributes', () => {
const source = `
<style type='text/scss' data-foo="bar" bool></style>
`;

return svelte.preprocess(source, {
style: ({ attributes }) => {
assert.deepEqual(attributes, {
type: 'text/scss',
'data-foo': 'bar',
bool: true
});
}
});
});

it('ignores null/undefined returned from preprocessor', () => {
const source = `
<script>
console.log('ignore me');
</script>
`;

const expected = `
<script>
console.log('ignore me');
</script>
`;

return svelte.preprocess(source, {
script: () => null
}).then(processed => {
assert.equal(processed.toString(), expected);
});
});
});
30 changes: 17 additions & 13 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,8 @@ ajv@^4.9.1:
json-stable-stringify "^1.0.1"

ajv@^5.1.0, ajv@^5.2.3, ajv@^5.3.0:
version "5.5.0"
resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.5.0.tgz#eb2840746e9dc48bd5e063a36e3fd400c5eab5a9"
version "5.5.1"
resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.5.1.tgz#b38bb8876d9e86bee994956a04e721e88b248eb2"
dependencies:
co "^4.6.0"
fast-deep-equal "^1.0.0"
Expand Down Expand Up @@ -538,8 +538,8 @@ [email protected]:
graceful-readlink ">= 1.0.0"

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

commondir@^1.0.1:
version "1.0.1"
Expand Down Expand Up @@ -887,8 +887,8 @@ eslint-scope@^3.7.1:
estraverse "^4.1.1"

eslint@^4.3.0:
version "4.12.0"
resolved "https://registry.yarnpkg.com/eslint/-/eslint-4.12.0.tgz#a7ce78eba8cc8f2443acfbbc870cc31a65135884"
version "4.12.1"
resolved "https://registry.yarnpkg.com/eslint/-/eslint-4.12.1.tgz#5ec1973822b4a066b353770c3c6d69a2a188e880"
dependencies:
ajv "^5.3.0"
babel-code-frame "^6.22.0"
Expand Down Expand Up @@ -1027,10 +1027,14 @@ extract-zip@^1.0.3:
mkdirp "0.5.0"
yauzl "2.4.1"

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

extsprintf@^1.2.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f"

fast-deep-equal@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.0.0.tgz#96256a3bc975595eb36d82e9929d060d893439ff"
Expand Down Expand Up @@ -1600,8 +1604,8 @@ is-path-in-cwd@^1.0.0:
is-path-inside "^1.0.0"

is-path-inside@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-1.0.0.tgz#fc06e5a1683fbda13de667aff717bbc10a48f37f"
version "1.0.1"
resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-1.0.1.tgz#8ef5b7de50437a3fdca6b4e865ef7aa55cb48036"
dependencies:
path-is-inside "^1.0.1"

Expand Down Expand Up @@ -1872,8 +1876,8 @@ load-json-file@^2.0.0:
strip-bom "^3.0.0"

locate-character@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/locate-character/-/locate-character-2.0.1.tgz#48f9599f342daf26f73db32f45941eae37bae391"
version "2.0.3"
resolved "https://registry.yarnpkg.com/locate-character/-/locate-character-2.0.3.tgz#85a5aedae26b3536c3e97016af164cdaa3ae5ae1"

locate-path@^2.0.0:
version "2.0.0"
Expand Down Expand Up @@ -3292,8 +3296,8 @@ typescript@^1.8.9:
resolved "https://registry.yarnpkg.com/typescript/-/typescript-1.8.10.tgz#b475d6e0dff0bf50f296e5ca6ef9fbb5c7320f1e"

typescript@^2.6.1:
version "2.6.1"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.6.1.tgz#ef39cdea27abac0b500242d6726ab90e0c846631"
version "2.6.2"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.6.2.tgz#3c5b6fd7f6de0914269027f03c0946758f7673a4"

uglify-js@^2.6:
version "2.8.29"
Expand Down