Skip to content

Commit afcd2c9

Browse files
refactor: code
1 parent 02d0188 commit afcd2c9

File tree

8 files changed

+580
-475
lines changed

8 files changed

+580
-475
lines changed

src/plugins/source-plugin.js

Lines changed: 176 additions & 475 deletions
Large diffs are not rendered by default.

src/utils.js

Lines changed: 370 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,376 @@ export function pluginRunner(plugins) {
1919
};
2020
}
2121

22+
function isASCIIWhitespace(character) {
23+
return (
24+
// Horizontal tab
25+
character === '\u0009' ||
26+
// New line
27+
character === '\u000A' ||
28+
// Form feed
29+
character === '\u000C' ||
30+
// Carriage return
31+
character === '\u000D' ||
32+
// Space
33+
character === '\u0020'
34+
);
35+
}
36+
37+
// (Don't use \s, to avoid matching non-breaking space)
38+
// eslint-disable-next-line no-control-regex
39+
const regexLeadingSpaces = /^[ \t\n\r\u000c]+/;
40+
// eslint-disable-next-line no-control-regex
41+
const regexLeadingCommasOrSpaces = /^[, \t\n\r\u000c]+/;
42+
// eslint-disable-next-line no-control-regex
43+
const regexLeadingNotSpaces = /^[^ \t\n\r\u000c]+/;
44+
const regexTrailingCommas = /[,]+$/;
45+
const regexNonNegativeInteger = /^\d+$/;
46+
47+
// ( Positive or negative or unsigned integers or decimals, without or without exponents.
48+
// Must include at least one digit.
49+
// According to spec tests any decimal point must be followed by a digit.
50+
// No leading plus sign is allowed.)
51+
// https://html.spec.whatwg.org/multipage/infrastructure.html#valid-floating-point-number
52+
const regexFloatingPoint = /^-?(?:[0-9]+|[0-9]*\.[0-9]+)(?:[eE][+-]?[0-9]+)?$/;
53+
54+
export function parseSrcset(input) {
55+
// 1. Let input be the value passed to this algorithm.
56+
const inputLength = input.length;
57+
58+
let url;
59+
let descriptors;
60+
let currentDescriptor;
61+
let state;
62+
let c;
63+
64+
// 2. Let position be a pointer into input, initially pointing at the start
65+
// of the string.
66+
let position = 0;
67+
let startUrlPosition;
68+
69+
// eslint-disable-next-line consistent-return
70+
function collectCharacters(regEx) {
71+
let chars;
72+
const match = regEx.exec(input.substring(position));
73+
74+
if (match) {
75+
[chars] = match;
76+
position += chars.length;
77+
78+
return chars;
79+
}
80+
}
81+
82+
// 3. Let candidates be an initially empty source set.
83+
const candidates = [];
84+
85+
// 4. Splitting loop: Collect a sequence of characters that are space
86+
// characters or U+002C COMMA characters. If any U+002C COMMA characters
87+
// were collected, that is a parse error.
88+
// eslint-disable-next-line no-constant-condition
89+
while (true) {
90+
collectCharacters(regexLeadingCommasOrSpaces);
91+
92+
// 5. If position is past the end of input, return candidates and abort these steps.
93+
if (position >= inputLength) {
94+
if (candidates.length === 0) {
95+
throw new Error('Must contain one or more image candidate strings');
96+
}
97+
98+
// (we're done, this is the sole return path)
99+
return candidates;
100+
}
101+
102+
// 6. Collect a sequence of characters that are not space characters,
103+
// and let that be url.
104+
startUrlPosition = position;
105+
url = collectCharacters(regexLeadingNotSpaces);
106+
107+
// 7. Let descriptors be a new empty list.
108+
descriptors = [];
109+
110+
// 8. If url ends with a U+002C COMMA character (,), follow these substeps:
111+
// (1). Remove all trailing U+002C COMMA characters from url. If this removed
112+
// more than one character, that is a parse error.
113+
if (url.slice(-1) === ',') {
114+
url = url.replace(regexTrailingCommas, '');
115+
116+
// (Jump ahead to step 9 to skip tokenization and just push the candidate).
117+
parseDescriptors();
118+
}
119+
// Otherwise, follow these substeps:
120+
else {
121+
tokenize();
122+
}
123+
124+
// 16. Return to the step labeled splitting loop.
125+
}
126+
127+
/**
128+
* Tokenizes descriptor properties prior to parsing
129+
* Returns undefined.
130+
*/
131+
function tokenize() {
132+
// 8.1. Descriptor tokenizer: Skip whitespace
133+
collectCharacters(regexLeadingSpaces);
134+
135+
// 8.2. Let current descriptor be the empty string.
136+
currentDescriptor = '';
137+
138+
// 8.3. Let state be in descriptor.
139+
state = 'in descriptor';
140+
141+
// eslint-disable-next-line no-constant-condition
142+
while (true) {
143+
// 8.4. Let c be the character at position.
144+
c = input.charAt(position);
145+
146+
// Do the following depending on the value of state.
147+
// For the purpose of this step, "EOF" is a special character representing
148+
// that position is past the end of input.
149+
150+
// In descriptor
151+
if (state === 'in descriptor') {
152+
// Do the following, depending on the value of c:
153+
154+
// Space character
155+
// If current descriptor is not empty, append current descriptor to
156+
// descriptors and let current descriptor be the empty string.
157+
// Set state to after descriptor.
158+
if (isASCIIWhitespace(c)) {
159+
if (currentDescriptor) {
160+
descriptors.push(currentDescriptor);
161+
currentDescriptor = '';
162+
state = 'after descriptor';
163+
}
164+
}
165+
// U+002C COMMA (,)
166+
// Advance position to the next character in input. If current descriptor
167+
// is not empty, append current descriptor to descriptors. Jump to the step
168+
// labeled descriptor parser.
169+
else if (c === ',') {
170+
position += 1;
171+
172+
if (currentDescriptor) {
173+
descriptors.push(currentDescriptor);
174+
}
175+
176+
parseDescriptors();
177+
178+
return;
179+
}
180+
// U+0028 LEFT PARENTHESIS (()
181+
// Append c to current descriptor. Set state to in parens.
182+
else if (c === '\u0028') {
183+
currentDescriptor += c;
184+
state = 'in parens';
185+
}
186+
// EOF
187+
// If current descriptor is not empty, append current descriptor to
188+
// descriptors. Jump to the step labeled descriptor parser.
189+
else if (c === '') {
190+
if (currentDescriptor) {
191+
descriptors.push(currentDescriptor);
192+
}
193+
194+
parseDescriptors();
195+
196+
return;
197+
198+
// Anything else
199+
// Append c to current descriptor.
200+
} else {
201+
currentDescriptor += c;
202+
}
203+
}
204+
// In parens
205+
else if (state === 'in parens') {
206+
// U+0029 RIGHT PARENTHESIS ())
207+
// Append c to current descriptor. Set state to in descriptor.
208+
if (c === ')') {
209+
currentDescriptor += c;
210+
state = 'in descriptor';
211+
}
212+
// EOF
213+
// Append current descriptor to descriptors. Jump to the step labeled
214+
// descriptor parser.
215+
else if (c === '') {
216+
descriptors.push(currentDescriptor);
217+
parseDescriptors();
218+
return;
219+
}
220+
// Anything else
221+
// Append c to current descriptor.
222+
else {
223+
currentDescriptor += c;
224+
}
225+
}
226+
// After descriptor
227+
else if (state === 'after descriptor') {
228+
// Do the following, depending on the value of c:
229+
if (isASCIIWhitespace(c)) {
230+
// Space character: Stay in this state.
231+
}
232+
// EOF: Jump to the step labeled descriptor parser.
233+
else if (c === '') {
234+
parseDescriptors();
235+
return;
236+
}
237+
// Anything else
238+
// Set state to in descriptor. Set position to the previous character in input.
239+
else {
240+
state = 'in descriptor';
241+
position -= 1;
242+
}
243+
}
244+
245+
// Advance position to the next character in input.
246+
position += 1;
247+
}
248+
}
249+
250+
/**
251+
* Adds descriptor properties to a candidate, pushes to the candidates array
252+
* @return undefined
253+
*/
254+
// Declared outside of the while loop so that it's only created once.
255+
function parseDescriptors() {
256+
// 9. Descriptor parser: Let error be no.
257+
let pError = false;
258+
259+
// 10. Let width be absent.
260+
// 11. Let density be absent.
261+
// 12. Let future-compat-h be absent. (We're implementing it now as h)
262+
let w;
263+
let d;
264+
let h;
265+
let i;
266+
const candidate = {};
267+
let desc;
268+
let lastChar;
269+
let value;
270+
let intVal;
271+
let floatVal;
272+
273+
// 13. For each descriptor in descriptors, run the appropriate set of steps
274+
// from the following list:
275+
for (i = 0; i < descriptors.length; i++) {
276+
desc = descriptors[i];
277+
278+
lastChar = desc[desc.length - 1];
279+
value = desc.substring(0, desc.length - 1);
280+
intVal = parseInt(value, 10);
281+
floatVal = parseFloat(value);
282+
283+
// If the descriptor consists of a valid non-negative integer followed by
284+
// a U+0077 LATIN SMALL LETTER W character
285+
if (regexNonNegativeInteger.test(value) && lastChar === 'w') {
286+
// If width and density are not both absent, then let error be yes.
287+
if (w || d) {
288+
pError = true;
289+
}
290+
291+
// Apply the rules for parsing non-negative integers to the descriptor.
292+
// If the result is zero, let error be yes.
293+
// Otherwise, let width be the result.
294+
if (intVal === 0) {
295+
pError = true;
296+
} else {
297+
w = intVal;
298+
}
299+
}
300+
// If the descriptor consists of a valid floating-point number followed by
301+
// a U+0078 LATIN SMALL LETTER X character
302+
else if (regexFloatingPoint.test(value) && lastChar === 'x') {
303+
// If width, density and future-compat-h are not all absent, then let error
304+
// be yes.
305+
if (w || d || h) {
306+
pError = true;
307+
}
308+
309+
// Apply the rules for parsing floating-point number values to the descriptor.
310+
// If the result is less than zero, let error be yes. Otherwise, let density
311+
// be the result.
312+
if (floatVal < 0) {
313+
pError = true;
314+
} else {
315+
d = floatVal;
316+
}
317+
}
318+
// If the descriptor consists of a valid non-negative integer followed by
319+
// a U+0068 LATIN SMALL LETTER H character
320+
else if (regexNonNegativeInteger.test(value) && lastChar === 'h') {
321+
// If height and density are not both absent, then let error be yes.
322+
if (h || d) {
323+
pError = true;
324+
}
325+
326+
// Apply the rules for parsing non-negative integers to the descriptor.
327+
// If the result is zero, let error be yes. Otherwise, let future-compat-h
328+
// be the result.
329+
if (intVal === 0) {
330+
pError = true;
331+
} else {
332+
h = intVal;
333+
}
334+
335+
// Anything else, Let error be yes.
336+
} else {
337+
pError = true;
338+
}
339+
}
340+
341+
// 15. If error is still no, then append a new image source to candidates whose
342+
// URL is url, associated with a width width if not absent and a pixel
343+
// density density if not absent. Otherwise, there is a parse error.
344+
if (!pError) {
345+
candidate.source = { value: url, startIndex: startUrlPosition };
346+
347+
if (w) {
348+
candidate.width = { value: w };
349+
}
350+
351+
if (d) {
352+
candidate.density = { value: d };
353+
}
354+
355+
if (h) {
356+
candidate.height = { value: h };
357+
}
358+
359+
candidates.push(candidate);
360+
} else {
361+
throw new Error(
362+
`Invalid srcset descriptor found in '${input}' at '${desc}'`
363+
);
364+
}
365+
}
366+
}
367+
368+
export function parseSrc(input) {
369+
if (!input) {
370+
throw new Error('Must be non-empty');
371+
}
372+
373+
let startIndex = 0;
374+
let value = input;
375+
376+
while (isASCIIWhitespace(value.substring(0, 1))) {
377+
startIndex += 1;
378+
value = value.substring(1, value.length);
379+
}
380+
381+
while (isASCIIWhitespace(value.substring(value.length - 1, value.length))) {
382+
value = value.substring(0, value.length - 1);
383+
}
384+
385+
if (!value) {
386+
throw new Error('Must be non-empty');
387+
}
388+
389+
return { value, startIndex };
390+
}
391+
22392
export function getFilter(filter, defaultFilter = null) {
23393
return (attribute, value, resourcePath) => {
24394
if (defaultFilter && !defaultFilter(value)) {

test/attributes-option.test.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,18 @@ describe("'attributes' option", () => {
2323
expect(getErrors(stats)).toMatchSnapshot('errors');
2424
});
2525

26+
it.skip('should handle the "include" type of tags', async () => {
27+
const compiler = getCompiler('include.js');
28+
const stats = await compile(compiler);
29+
30+
expect(getModuleSource('./include.html', stats)).toMatchSnapshot('module');
31+
expect(
32+
execute(readAsset('main.bundle.js', compiler, stats))
33+
).toMatchSnapshot('result');
34+
expect(getWarnings(stats)).toMatchSnapshot('warnings');
35+
expect(getErrors(stats)).toMatchSnapshot('errors');
36+
});
37+
2638
it('should handle "src" and "srcset" tags correctly', async () => {
2739
const compiler = getCompiler('sources.js');
2840
const stats = await compile(compiler);

test/fixtures/include-content.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<span>Text</span>

0 commit comments

Comments
 (0)