Skip to content

Commit 3c269e6

Browse files
committed
add preprocess feature
1 parent 5061335 commit 3c269e6

File tree

6 files changed

+237
-8
lines changed

6 files changed

+237
-8
lines changed

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,13 @@ This setting can be given the result of `require('.../path/to/svelte/compiler')`
104104

105105
The default is `require('svelte/compiler')` from wherever the plugin is installed to.
106106

107+
### `svelte3/preprocess`
108+
109+
You can use a preprocessor function to return custom AST info according to the original code.
110+
111+
For now this only supports `module` and `instance` scripts.
112+
113+
107114
## Using the CLI
108115

109116
It's probably a good idea to make sure you can lint from the command line before proceeding with configuring your editor.

src/block.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
11
import { get_offsets, dedent_code } from './utils.js';
22

3+
// find the index of the last element of an array matching a condition
4+
export const find_last_index = (array, cond) => {
5+
const idx = array.findIndex(item => !cond(item));
6+
return idx === -1 ? array.length - 1 : idx - 1;
7+
};
8+
9+
// find the last element of an array matching a condition
10+
export const find_last = (array, cond) => array[find_last_index(array, cond)];
11+
312
// return a new block
413
export const new_block = () => ({ transformed_code: '', line_offsets: null, translations: new Map() });
514

src/postprocess.js

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,31 @@
11
import { state, reset } from './state.js';
22
import { get_line_offsets } from './utils.js';
3+
import { find_last_index, find_last } from './block.js'
4+
5+
export const unmap = message => {
6+
for (let j = 0; j < 2; j++) {
7+
if (message[j ? 'endLine' : 'line']) {
8+
const mapping = find_last(state.mappings[message[j ? 'endLine' : 'line'] - 1], ([column]) => column < message[j ? 'endColumn' : 'column']);
9+
if (!mapping || mapping[1] !== 0) {
10+
return false;
11+
}
12+
message[j ? 'endLine' : 'line'] = mapping[2] + 1;
13+
message[j ? 'endColumn' : 'column'] += mapping[3] - mapping[0];
14+
}
15+
}
16+
if (message.fix) {
17+
for (let j = 0; j < 2; j++) {
18+
const line = find_last_index(state.post_line_offsets, offset => offset < message.fix.range[j]);
19+
const line_offset = state.post_line_offsets[line];
20+
const mapping = find_last(state.mappings[line], ([column]) => column < message.fix.range[j] - line_offset);
21+
if (!mapping || mapping[1] !== 0) {
22+
return false;
23+
}
24+
message.fix.range[j] += mapping[3] - mapping[0] + state.pre_line_offsets[mapping[2]] - line_offset;
25+
}
26+
}
27+
return true;
28+
};
329

430
// transform a linting message according to the module/instance script info we've gathered
531
const transform_message = ({ transformed_code }, { unoffsets, dedent, offsets, range }, message) => {
@@ -118,6 +144,9 @@ export const postprocess = blocks_messages => {
118144
}
119145
}
120146
}
147+
if (state.mappings) {
148+
state.messages = state.messages.filter(unmap);
149+
}
121150

122151
// sort messages and return
123152
const sorted_messages = state.messages.sort((a, b) => a.line - b.line || a.column - b.column);

src/preprocess.js

Lines changed: 188 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,89 @@
11
import { new_block, get_translation } from './block.js';
22
import { processor_options } from './processor_options.js';
3+
import { get_line_offsets } from './utils.js';
34
import { state } from './state.js';
45

6+
var charToInteger = {};
7+
var chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
8+
for (var i = 0; i < chars.length; i++) {
9+
charToInteger[chars.charCodeAt(i)] = i;
10+
}
11+
function decode(mappings) {
12+
var generatedCodeColumn = 0; // first field
13+
var sourceFileIndex = 0; // second field
14+
var sourceCodeLine = 0; // third field
15+
var sourceCodeColumn = 0; // fourth field
16+
var nameIndex = 0; // fifth field
17+
var decoded = [];
18+
var line = [];
19+
var segment = [];
20+
for (var i = 0, j = 0, shift = 0, value = 0, len = mappings.length; i < len; i++) {
21+
var c = mappings.charCodeAt(i);
22+
if (c === 44) { // ","
23+
if (segment.length)
24+
line.push(segment);
25+
segment = [];
26+
j = 0;
27+
}
28+
else if (c === 59) { // ";"
29+
if (segment.length)
30+
line.push(segment);
31+
segment = [];
32+
j = 0;
33+
decoded.push(line);
34+
line = [];
35+
generatedCodeColumn = 0;
36+
}
37+
else {
38+
var integer = charToInteger[c];
39+
if (integer === undefined) {
40+
throw new Error('Invalid character (' + String.fromCharCode(c) + ')');
41+
}
42+
var hasContinuationBit = integer & 32;
43+
integer &= 31;
44+
value += integer << shift;
45+
if (hasContinuationBit) {
46+
shift += 5;
47+
}
48+
else {
49+
var shouldNegate = value & 1;
50+
value >>>= 1;
51+
if (shouldNegate) {
52+
value = -value;
53+
if (value === 0)
54+
value = -0x80000000;
55+
}
56+
if (j == 0) {
57+
generatedCodeColumn += value;
58+
segment.push(generatedCodeColumn);
59+
}
60+
else if (j === 1) {
61+
sourceFileIndex += value;
62+
segment.push(sourceFileIndex);
63+
}
64+
else if (j === 2) {
65+
sourceCodeLine += value;
66+
segment.push(sourceCodeLine);
67+
}
68+
else if (j === 3) {
69+
sourceCodeColumn += value;
70+
segment.push(sourceCodeColumn);
71+
}
72+
else if (j === 4) {
73+
nameIndex += value;
74+
segment.push(nameIndex);
75+
}
76+
j++;
77+
value = shift = 0; // reset
78+
}
79+
}
80+
}
81+
if (segment.length)
82+
line.push(segment);
83+
decoded.push(line);
84+
return decoded;
85+
}
86+
587
let default_compiler;
688

789
// find the contextual name or names described by a particular node in the AST
@@ -23,7 +105,7 @@ const find_contextual_names = (compiler, node) => {
23105
};
24106

25107
// extract scripts to lint from component definition
26-
export const preprocess = text => {
108+
export const preprocess = (text, filename) => {
27109
const compiler = processor_options.custom_compiler || default_compiler || (default_compiler = require('svelte/compiler'));
28110
if (processor_options.ignore_styles) {
29111
// wipe the appropriate <style> tags in the file
@@ -40,10 +122,104 @@ export const preprocess = text => {
40122
return processor_options.ignore_styles(attrs) ? match.replace(/\S/g, ' ') : match;
41123
});
42124
}
43-
// get information about the component
44125
let result;
126+
let processedResult;
127+
let processedModule;
128+
let processedInstance;
129+
let processedStyle;
130+
let processedMarkup;
131+
let moduleExt = 'js';
132+
let instanceExt = 'js';
45133
try {
46-
result = compiler.compile(text, { generate: false, ...processor_options.compiler_options });
134+
// run preprocessor if present
135+
if (processor_options.svelte_preprocess) {
136+
const result = processor_options.svelte_preprocess(text, filename);
137+
if (result) {
138+
state.pre_line_offsets = get_line_offsets(text);
139+
processedResult = result.code;
140+
state.post_line_offsets = get_line_offsets(processedResult);
141+
if (result.mappings) {
142+
state.mappings = decode(result.mappings);
143+
}
144+
145+
processedMarkup = result.markup;
146+
147+
if (result.module) {
148+
processedModule = result.module;
149+
moduleExt = result.module.ext;
150+
}
151+
if (result.instance) {
152+
processedInstance = result.instance;
153+
instanceExt = result.instance.ext;
154+
}
155+
156+
processedStyle = result.style;
157+
158+
processor_options.named_blocks = true;
159+
}
160+
}
161+
// get information about the component
162+
result = compiler.compile(processedResult || text, { generate: false, ...processor_options.compiler_options });
163+
if (processedResult) {
164+
const { html, css, instance, module } = result.ast;
165+
166+
let styleDiff = processedStyle ? processedStyle.diff : 0;
167+
let markupDiff = processedMarkup ? processedMarkup.diff : 0;
168+
let moduleDiff = processedModule ? processedModule.diff : 0;
169+
let instanceDiff = processedInstance ? processedInstance.diff : 0;
170+
171+
let modulePreOffset = 0;
172+
let modulePostOffset = 0;
173+
if (module) {
174+
if (module.start > html.start) {
175+
modulePreOffset += markupDiff;
176+
}
177+
if (module.start > css.start) {
178+
modulePreOffset += styleDiff;
179+
}
180+
if (instance && module.start > instance.start) {
181+
modulePreOffset += instanceDiff;
182+
}
183+
184+
modulePostOffset = modulePreOffset + moduleDiff;
185+
}
186+
187+
let instancePreOffset = 0;
188+
let instancePostOffset = 0;
189+
if (instance) {
190+
if (instance.start > html.start) {
191+
instancePreOffset += markupDiff;
192+
}
193+
if (instance.start > css.start) {
194+
instancePreOffset += styleDiff;
195+
}
196+
if (module && instance.start > module.start) {
197+
instancePreOffset += moduleDiff;
198+
}
199+
200+
instancePostOffset = instancePreOffset + instanceDiff;
201+
}
202+
203+
if (module) {
204+
module.content.body = processedModule.ast.body;
205+
206+
module.start += modulePreOffset;
207+
module.end += modulePostOffset;
208+
209+
module.content.start += modulePreOffset;
210+
module.content.end += modulePostOffset;
211+
}
212+
213+
if (instance) {
214+
instance.content.body = processedInstance.ast.body;
215+
216+
instance.start += instancePreOffset;
217+
instance.end += instancePostOffset;
218+
219+
instance.content.start += instancePreOffset;
220+
instance.content.end += instancePostOffset;
221+
}
222+
}
47223
} catch ({ name, message, start, end }) {
48224
// convert the error to a linting message, store it, and return
49225
state.messages = [
@@ -79,12 +255,14 @@ export const preprocess = text => {
79255
if (ast.module) {
80256
// block for <script context='module'>
81257
const block = new_block();
82-
state.blocks.set('module.js', block);
258+
state.blocks.set(`module.${moduleExt}`, block);
83259

84260
get_translation(text, block, ast.module.content);
85261

86262
if (ast.instance) {
87-
block.transformed_code += text.slice(ast.instance.content.start, ast.instance.content.end);
263+
block.transformed_code += processedResult
264+
? processedInstance.original
265+
: text.slice(ast.instance.content.start, ast.instance.content.end);
88266
}
89267

90268
block.transformed_code += references_and_reassignments;
@@ -93,7 +271,7 @@ export const preprocess = text => {
93271
if (ast.instance) {
94272
// block for <script context='instance'>
95273
const block = new_block();
96-
state.blocks.set('instance.js', block);
274+
state.blocks.set(`instance.${instanceExt}`, block);
97275

98276
block.transformed_code = vars.filter(v => v.injected || v.module).map(v => `let ${v.name};`).join('');
99277

@@ -111,11 +289,13 @@ export const preprocess = text => {
111289

112290
const nodes_with_contextual_scope = new WeakSet();
113291
let in_quoted_attribute = false;
292+
const htmlText = processedResult || text;
293+
114294
compiler.walk(ast.html, {
115295
enter(node, parent, prop) {
116296
if (prop === 'expression') {
117297
return this.skip();
118-
} else if (prop === 'attributes' && '\'"'.includes(text[node.end - 1])) {
298+
} else if (prop === 'attributes' && '\'"'.includes(htmlText[node.end - 1])) {
119299
in_quoted_attribute = true;
120300
}
121301
contextual_names.length = 0;
@@ -136,7 +316,7 @@ export const preprocess = text => {
136316
if (node.expression && typeof node.expression === 'object') {
137317
// add the expression in question to the constructed string
138318
block.transformed_code += '(';
139-
get_translation(text, block, node.expression, { template: true, in_quoted_attribute });
319+
get_translation(htmlText, block, node.expression, { template: true, in_quoted_attribute });
140320
block.transformed_code += ');';
141321
}
142322
},

src/processor_options.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ Linter.prototype.verify = function(code, config, options) {
1717
processor_options.ignore_styles = settings['svelte3/ignore-styles'];
1818
processor_options.compiler_options = settings['svelte3/compiler-options'];
1919
processor_options.named_blocks = settings['svelte3/named-blocks'];
20+
processor_options.svelte_preprocess = settings['svelte3/preprocess'];
2021
// call original Linter#verify
2122
return verify.call(this, code, config, options);
2223
};

src/state.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ export const reset = () => {
44
messages: null,
55
var_names: null,
66
blocks: new Map(),
7+
pre_line_offsets: null,
8+
post_line_offsets: null,
9+
mappings: null,
710
};
811
};
912
reset();

0 commit comments

Comments
 (0)