diff --git a/src/index.ts b/src/index.ts index 529be61618..081660d3d8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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__'; @@ -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 = 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}` + + 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 { @@ -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'; @@ -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); diff --git a/src/interfaces.ts b/src/interfaces.ts index 3a9280df3c..0d884f8472 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -1,3 +1,5 @@ +import {SourceMap} from 'magic-string'; + export interface Node { start: number; end: number; @@ -78,4 +80,12 @@ export interface Visitor { export interface CustomElementOptions { tag?: string; props?: string[]; -} \ No newline at end of file +} + +export interface PreprocessOptions { + markup?: (options: {content: string}) => { code: string, map?: SourceMap | string }; + style?: Preprocessor; + script?: Preprocessor; +} + +export type Preprocessor = (options: {content: string, attributes: Record}) => { code: string, map?: SourceMap | string }; diff --git a/test/preprocess/index.js b/test/preprocess/index.js new file mode 100644 index 0000000000..29eaff67f1 --- /dev/null +++ b/test/preprocess/index.js @@ -0,0 +1,148 @@ +import assert from 'assert'; +import {svelte} from '../helpers.js'; + +describe('preprocess', () => { + it('preprocesses entire component', () => { + const source = ` +

Hello __NAME__!

+ `; + + const expected = ` +

Hello world!

+ `; + + return svelte.preprocess(source, { + markup: ({ content }) => { + return { + code: content.replace('__NAME__', 'world') + }; + } + }).then(processed => { + assert.equal(processed.toString(), expected); + }); + }); + + it('preprocesses style', () => { + const source = ` +
$brand
+ + + `; + + const expected = ` +
$brand
+ + + `; + + 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 = ` +
$brand
+ + + `; + + const expected = ` +
$brand
+ + + `; + + 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 = ` + + `; + + const expected = ` + + `; + + 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 = ` + + `; + + 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 = ` + + `; + + const expected = ` + + `; + + return svelte.preprocess(source, { + script: () => null + }).then(processed => { + assert.equal(processed.toString(), expected); + }); + }); +}); \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 4a8c3799e3..c38c354a78 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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" @@ -538,8 +538,8 @@ commander@2.9.0: 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" @@ -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" @@ -1027,10 +1027,14 @@ extract-zip@^1.0.3: mkdirp "0.5.0" yauzl "2.4.1" -extsprintf@1.3.0, extsprintf@^1.2.0: +extsprintf@1.3.0: 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" @@ -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" @@ -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" @@ -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"