Skip to content

Commit 24b8d45

Browse files
committed
parser instead of regex for preprocessor
1 parent b554e34 commit 24b8d45

File tree

23 files changed

+700
-65
lines changed

23 files changed

+700
-65
lines changed

site/content/docs/04-compile-time.md

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -204,12 +204,18 @@ result: {
204204
dependencies?: Array<string>
205205
}>,
206206
script?: (input: { content: string, markup: string, attributes: Record<string, string>, filename: string }) => Promise<{
207+
code: string,
208+
dependencies?: Array<string>,
209+
attributes?: Record<string, string | boolean>
210+
}>,
211+
expression?: (input: { content: string, markup: string, filename: string }) => Promise<{
207212
code: string,
208213
dependencies?: Array<string>
209214
}>,
210215
style?: (input: { content: string, markup: string, attributes: Record<string, string>, filename: string }) => Promise<{
211216
code: string,
212-
dependencies?: Array<string>
217+
dependencies?: Array<string>,
218+
attributes?: Record<string, string | boolean>
213219
}>
214220
}>,
215221
options?: {
@@ -224,7 +230,7 @@ The `preprocess` function provides convenient hooks for arbitrarily transforming
224230

225231
The first argument is the component source code. The second is an array of *preprocessors* (or a single preprocessor, if you only have one), where a preprocessor is an object with `markup`, `script` and `style` functions, each of which is optional.
226232

227-
Each `markup`, `script` or `style` function must return an object (or a Promise that resolves to an object) with a `code` property, representing the transformed source code, and an optional array of `dependencies`.
233+
Each `markup`, `script`, `expression` or `style` function must return an object (or a Promise that resolves to an object) with a `code` property, representing the transformed source code, an optional array of `dependencies`, and an optional object of `attributes`.
228234

229235
The `markup` function receives the entire component source text, along with the component's `filename` if it was specified in the third argument.
230236

@@ -254,10 +260,16 @@ const { code } = await svelte.preprocess(source, {
254260

255261
---
256262

257-
The `script` and `style` functions receive the contents of `<script>` and `<style>` elements respectively (`content`) as well as the entire component source text (`markup`). In addition to `filename`, they get an object of the element's attributes.
263+
The `script` and `style` functions receive the contents of `<script>` and `<style>` elements respectively (`content`), while the `expression` function receives the contents within the `{...}` expression (`content`).
264+
265+
The `script`, `style`, and `expression` functions receive the entire component source text (`markup`) as well as the name of the file (`filename`).
266+
267+
The `script` and `style` functions also get an object of the element's attributes (`attributes`).
258268

259269
If a `dependencies` array is returned, it will be included in the result object. This is used by packages like [rollup-plugin-svelte](https://github.com/sveltejs/rollup-plugin-svelte) to watch additional files for changes, in the case where your `<style>` tag has an `@import` (for example).
260270

271+
If an `attributes` object is returned, it will replace the attributes on the `<script>` or `<style >` tag.
272+
261273
```js
262274
const svelte = require('svelte/compiler');
263275
const sass = require('node-sass');

src/compiler/preprocess/index.ts

Lines changed: 42 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { RawSourceMap, DecodedSourceMap } from '@ampproject/remapping/dist/types
22
import { getLocator } from 'locate-character';
33
import { MappedCode, SourceLocation, parse_attached_sourcemap, sourcemap_add_offset, combine_sourcemaps } from '../utils/mapped_code';
44
import { decode_map } from './decode_sourcemap';
5+
import { Position } from './quick_parser';
56
import { replace_in_code, slice_source } from './replace_in_code';
67
import { MarkupPreprocessor, Source, Preprocessor, PreprocessorGroup, Processed } from './types';
78

@@ -89,89 +90,85 @@ function processed_content_to_code(processed: Processed, location: SourceLocatio
8990
return MappedCode.from_processed(processed.code, decoded_map);
9091
}
9192

93+
function stringify_attributes(attributes: Record<string, string | boolean>) {
94+
return Object.keys(attributes).map(key => {
95+
const value = attributes[key];
96+
if (typeof value === 'boolean') {
97+
if (value) {
98+
return key;
99+
}
100+
} else {
101+
const value_string = value.indexOf('"') > -1 ? `'${value}'` : `"${value}"`;
102+
return key + '=' + value_string;
103+
}
104+
}).filter(Boolean).join(' ');
105+
}
106+
92107
/**
93108
* Given the whole tag including content, return a `MappedCode`
94109
* representing the tag content replaced with `processed`.
95110
*/
96111
function processed_tag_to_code(
97112
processed: Processed,
98113
tag_name: 'style' | 'script',
99-
attributes: string,
114+
original_attributes: string,
115+
updated_attributes: string,
100116
source: Source
101117
): MappedCode {
102118
const { file_basename, get_location } = source;
103119

104120
const build_mapped_code = (code: string, offset: number) =>
105121
MappedCode.from_source(slice_source(code, offset, source));
106122

107-
const tag_open = `<${tag_name}${attributes || ''}>`;
123+
const original_tag_open = `<${tag_name}${original_attributes || ''}>`;
124+
const updated_tag_open = updated_attributes ? `<${tag_name} ${updated_attributes}>` : original_tag_open;
108125
const tag_close = `</${tag_name}>`;
109126

110-
const tag_open_code = build_mapped_code(tag_open, 0);
111-
const tag_close_code = build_mapped_code(tag_close, tag_open.length + source.source.length);
112-
113-
parse_attached_sourcemap(processed, tag_name);
114-
115-
const content_code = processed_content_to_code(processed, get_location(tag_open.length), file_basename);
127+
const tag_open_code = build_mapped_code(updated_tag_open, 0);
128+
const tag_close_code = build_mapped_code(tag_close, original_tag_open.length + source.source.length);
129+
const content_code = processed_content_to_code(processed, get_location(original_tag_open.length), file_basename);
116130

117131
return tag_open_code.concat(content_code).concat(tag_close_code);
118132
}
119133

120-
function parse_tag_attributes(str: string) {
121-
// note: won't work with attribute values containing spaces.
122-
return str
123-
.split(/\s+/)
124-
.filter(Boolean)
125-
.reduce((attrs, attr) => {
126-
const i = attr.indexOf('=');
127-
const [key, value] = i > 0 ? [attr.slice(0, i), attr.slice(i + 1)] : [attr];
128-
const [, unquoted] = (value && value.match(/^['"](.*)['"]$/)) || [];
129-
130-
return { ...attrs, [key]: unquoted ?? value ?? true };
131-
}, {});
132-
}
133-
134134
/**
135135
* Calculate the updates required to process all instances of the specified tag.
136136
*/
137137
async function process_tag(
138-
tag_name: 'style' | 'script',
138+
tag_name: 'style' | 'script' | 'expression',
139139
preprocessor: Preprocessor,
140140
source: Source
141141
): Promise<SourceUpdate> {
142142
const { filename, source: markup } = source;
143-
const tag_regex =
144-
tag_name === 'style'
145-
? /<!--[^]*?-->|<style(\s[^]*?)?(?:>([^]*?)<\/style>|\/>)/gi
146-
: /<!--[^]*?-->|<script(\s[^]*?)?(?:>([^]*?)<\/script>|\/>)/gi;
147-
148143
const dependencies: string[] = [];
149144

150145
async function process_single_tag(
151-
tag_with_content: string,
152-
attributes = '',
153-
content = '',
154-
tag_offset: number
146+
{ source: content, attributes, raw_attributes, offset, length }: Position
155147
): Promise<MappedCode> {
156-
const no_change = () => MappedCode.from_source(slice_source(tag_with_content, tag_offset, source));
148+
const no_change = () => MappedCode.from_source(slice_source(source.source.slice(offset, offset + length), offset, source));
157149

158150
if (!attributes && !content) return no_change();
159-
160151
const processed = await preprocessor({
161-
content: content || '',
162-
attributes: parse_tag_attributes(attributes || ''),
152+
content,
153+
attributes,
163154
markup,
164155
filename
165156
});
166157

167158
if (!processed) return no_change();
168159
if (processed.dependencies) dependencies.push(...processed.dependencies);
169-
if (!processed.map && processed.code === content) return no_change();
170-
171-
return processed_tag_to_code(processed, tag_name, attributes, slice_source(content, tag_offset, source));
160+
if (!processed.map && processed.code === content && !('attributes' in processed)) return no_change();
161+
162+
parse_attached_sourcemap(processed, tag_name);
163+
if (tag_name === 'expression') {
164+
return processed_content_to_code(processed, source.get_location(offset), source.file_basename);
165+
} else {
166+
const updated_attributes = ('attributes' in processed) ? stringify_attributes(processed.attributes) : null;
167+
return processed_tag_to_code(processed, tag_name, raw_attributes, updated_attributes, slice_source(content, offset, source));
168+
}
172169
}
173170

174-
const { string, map } = await replace_in_code(tag_regex, process_single_tag, source);
171+
const { string, map } = await replace_in_code(tag_name, process_single_tag, source);
175172

176173
return { string, map, dependencies };
177174
}
@@ -210,6 +207,7 @@ export default async function preprocess(
210207

211208
const markup = preprocessors.map(p => p.markup).filter(Boolean);
212209
const script = preprocessors.map(p => p.script).filter(Boolean);
210+
const expression = preprocessors.map(p => p.expression).filter(Boolean);
213211
const style = preprocessors.map(p => p.style).filter(Boolean);
214212

215213
const result = new PreprocessResult(source, filename);
@@ -225,6 +223,10 @@ export default async function preprocess(
225223
result.update_source(await process_tag('script', process, result));
226224
}
227225

226+
for (const process of expression) {
227+
result.update_source(await process_tag('expression', process, result));
228+
}
229+
228230
for (const preprocess of style) {
229231
result.update_source(await process_tag('style', preprocess, result));
230232
}

0 commit comments

Comments
 (0)