Skip to content

Commit f8c3693

Browse files
authored
Fix charset handling (#447)
* Fix @charset handling Fixes #436 Closes #438 * Ensure @charset statements are first
1 parent 443c112 commit f8c3693

8 files changed

+101
-18
lines changed

index.js

+23-6
Original file line numberDiff line numberDiff line change
@@ -107,10 +107,7 @@ function AtImport(options) {
107107

108108
// Strip additional statements.
109109
bundle.forEach(stmt => {
110-
if (stmt.type === "import") {
111-
stmt.node.parent = undefined
112-
styles.append(stmt.node)
113-
} else if (stmt.type === "media") {
110+
if (["charset", "import", "media"].includes(stmt.type)) {
114111
stmt.node.parent = undefined
115112
styles.append(stmt.node)
116113
} else if (stmt.type === "nodes") {
@@ -150,15 +147,33 @@ function AtImport(options) {
150147
}, Promise.resolve())
151148
})
152149
.then(() => {
150+
let charset
153151
const imports = []
154152
const bundle = []
155153

154+
function handleCharset(stmt) {
155+
if (!charset) charset = stmt
156+
// charsets aren't case-sensitive, so convert to lower case to compare
157+
else if (
158+
stmt.node.params.toLowerCase() !==
159+
charset.node.params.toLowerCase()
160+
) {
161+
throw new Error(
162+
`Incompatable @charset statements:
163+
${stmt.node.params} specified in ${stmt.node.source.input.file}
164+
${charset.node.params} specified in ${charset.node.source.input.file}`
165+
)
166+
}
167+
}
168+
156169
// squash statements and their children
157170
statements.forEach(stmt => {
158-
if (stmt.type === "import") {
171+
if (stmt.type === "charset") handleCharset(stmt)
172+
else if (stmt.type === "import") {
159173
if (stmt.children) {
160174
stmt.children.forEach((child, index) => {
161175
if (child.type === "import") imports.push(child)
176+
else if (child.type === "charset") handleCharset(child)
162177
else bundle.push(child)
163178
// For better output
164179
if (index === 0) child.parent = stmt
@@ -169,7 +184,9 @@ function AtImport(options) {
169184
}
170185
})
171186

172-
return imports.concat(bundle)
187+
return charset
188+
? [charset, ...imports.concat(bundle)]
189+
: imports.concat(bundle)
173190
})
174191
}
175192

lib/parse-statements.js

+19-12
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ module.exports = function (result, styles) {
2929
if (node.type === "atrule") {
3030
if (node.name === "import") stmt = parseImport(result, node)
3131
else if (node.name === "media") stmt = parseMedia(result, node)
32+
else if (node.name === "charset") stmt = parseCharset(result, node)
3233
}
3334

3435
if (stmt) {
@@ -64,20 +65,34 @@ function parseMedia(result, atRule) {
6465
}
6566
}
6667

68+
function parseCharset(result, atRule) {
69+
if (atRule.prev()) {
70+
return result.warn("@charset must precede all other statements", {
71+
node: atRule,
72+
})
73+
}
74+
return {
75+
type: "charset",
76+
node: atRule,
77+
media: [],
78+
}
79+
}
80+
6781
function parseImport(result, atRule) {
68-
let prev = getPrev(atRule)
82+
let prev = atRule.prev()
6983
if (prev) {
7084
do {
7185
if (
72-
prev.type !== "atrule" ||
73-
(prev.name !== "import" && prev.name !== "charset")
86+
prev.type !== "comment" &&
87+
(prev.type !== "atrule" ||
88+
(prev.name !== "import" && prev.name !== "charset"))
7489
) {
7590
return result.warn(
7691
"@import must precede all other statements (besides @charset)",
7792
{ node: atRule }
7893
)
7994
}
80-
prev = getPrev(prev)
95+
prev = prev.prev()
8196
} while (prev)
8297
}
8398

@@ -128,11 +143,3 @@ function parseImport(result, atRule) {
128143

129144
return stmt
130145
}
131-
132-
function getPrev(item) {
133-
let prev = item.prev()
134-
while (prev && prev.type === "comment") {
135-
prev = prev.prev()
136-
}
137-
return prev
138-
}

test/fixtures/charset-error.css

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
@charset "foobar";
2+
@import "imports/charset.css";

test/fixtures/charset-import.css

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
@charset "UTF-8";
2+
@import "test/fixtures/imports/foo.css";
3+
@import "test/fixtures/imports/charset.css";
4+
bar{}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
@charset "UTF-8";
2+
foo{}
3+
bar{}

test/fixtures/imports/charset.css

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
@charset "UTF-8";

test/import.js

+32
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,38 @@ test("should not fail with absolute and local import", t => {
4646
.then(result => t.is(result.css, "@import url('http://');\nfoo{}"))
4747
})
4848

49+
test("should keep @charset first", t => {
50+
const base = '@charset "UTF-8";\n@import url(http://);'
51+
return postcss()
52+
.use(atImport())
53+
.process(base, { from: undefined })
54+
.then(result => {
55+
t.is(result.warnings().length, 0)
56+
t.is(result.css, base)
57+
})
58+
})
59+
60+
test(
61+
"should handle multiple @charset statements",
62+
checkFixture,
63+
"charset-import"
64+
)
65+
66+
test("should error if incompatable @charset statements", t => {
67+
t.plan(2)
68+
const file = "test/fixtures/charset-error.css"
69+
return postcss()
70+
.use(atImport())
71+
.process(readFileSync(file), { from: file })
72+
.catch(err => {
73+
t.truthy(err)
74+
t.regex(
75+
err.message,
76+
/Incompatable @charset statements:.+specified in.+specified in.+/s
77+
)
78+
})
79+
})
80+
4981
test("should error when file not found", t => {
5082
t.plan(1)
5183
const file = "test/fixtures/imports/import-missing.css"

test/lint.js

+17
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,23 @@ test("should not warn when @charset or @import statement before", t => {
7878
})
7979
})
8080

81+
test("should warn when @charset is not first", t => {
82+
return Promise.all([
83+
processor.process(`a {} @charset "utf-8";`, { from: undefined }),
84+
processor.process(`@media {} @charset "utf-8";`, { from: undefined }),
85+
processor.process(`/* foo */ @charset "utf-8";`, { from: undefined }),
86+
processor.process(`@import "bar.css"; @charset "utf-8";`, {
87+
from: "test/fixtures/imports/foo.css",
88+
}),
89+
]).then(results => {
90+
results.forEach(result => {
91+
const warnings = result.warnings()
92+
t.is(warnings.length, 1)
93+
t.is(warnings[0].text, "@charset must precede all other statements")
94+
})
95+
})
96+
})
97+
8198
test("should warn when a user didn't close an import with ;", t => {
8299
return processor
83100
.process(`@import url('http://') :root{}`, { from: undefined })

0 commit comments

Comments
 (0)