diff --git a/glimmer-scoped-css/src/ast-transform.ts b/glimmer-scoped-css/src/ast-transform.ts index 0489a0d..b724d6d 100644 --- a/glimmer-scoped-css/src/ast-transform.ts +++ b/glimmer-scoped-css/src/ast-transform.ts @@ -9,6 +9,7 @@ import postcss from 'postcss'; import scopedStylesPlugin from './postcss-plugin'; import { basename } from 'path'; import { GlimmerScopedCSSOptions } from '.'; +import { encodeCSS } from './encoding'; type Env = WithJSUtils & { filename: string; @@ -76,7 +77,7 @@ export function generateScopedCSSPlugin( // TODO: hard coding the loader chain means we ignore the other // prevailing rules (and we're even assuming these loaders are // available) - let encodedCss = encodeURIComponent(btoa(outputCSS)); + let encodedCss = encodeCSS(outputCSS); jsutils.importForSideEffect( `./${basename(env.filename)}.${encodedCss}.glimmer-scoped.css` diff --git a/glimmer-scoped-css/src/encoding.ts b/glimmer-scoped-css/src/encoding.ts new file mode 100644 index 0000000..77c3335 --- /dev/null +++ b/glimmer-scoped-css/src/encoding.ts @@ -0,0 +1,21 @@ +/** + * These functions convert between arbitrary normally formatted CSS and + * URI-safe strings that are used as data-URI virtual imports. + */ + +// Adapted from https://developer.mozilla.org/en-US/docs/Web/API/Window/btoa#unicode_strings + +export function encodeCSS(plainCSSString: string) { + const binString = Array.from( + new TextEncoder().encode(plainCSSString), + (byte) => String.fromCodePoint(byte) + ).join(''); + return encodeURIComponent(btoa(binString)); +} + +export function decodeCSS(encodedCSSString: string) { + const binString = atob(decodeURIComponent(encodedCSSString)); + return new TextDecoder().decode( + Uint8Array.from(binString, (m) => m.codePointAt(0) as number) + ); +} diff --git a/glimmer-scoped-css/src/index.ts b/glimmer-scoped-css/src/index.ts index 3146f8f..0ba0f89 100644 --- a/glimmer-scoped-css/src/index.ts +++ b/glimmer-scoped-css/src/index.ts @@ -1,4 +1,5 @@ import { generateScopedCSSPlugin } from './ast-transform'; +import { decodeCSS } from './encoding'; export interface GlimmerScopedCSSOptions { noGlobal?: boolean; @@ -40,5 +41,5 @@ export function decodeScopedCSSRequest(request: string): { if (!m) { throw new Error(`not a scoped CSS request: ${request}`); } - return { fromFile: m[1]!, css: atob(decodeURIComponent(m[2]!)) }; + return { fromFile: m[1]!, css: decodeCSS(m[2]!) }; } diff --git a/test-app/app/components/multiple.gjs b/test-app/app/components/multiple.gjs index 4044f07..ee8b35e 100644 --- a/test-app/app/components/multiple.gjs +++ b/test-app/app/components/multiple.gjs @@ -8,6 +8,10 @@ const MultipleInner = ; diff --git a/test-app/tests/acceptance/scoped-css-test.ts b/test-app/tests/acceptance/scoped-css-test.ts index 55fc540..3cf041f 100644 --- a/test-app/tests/acceptance/scoped-css-test.ts +++ b/test-app/tests/acceptance/scoped-css-test.ts @@ -86,6 +86,17 @@ module('Acceptance | scoped css', function (hooks) { 'font-weight': '700', }); + const multipleInnerElement = find('[data-test-multiple-inner]'); + const multipleInnerElementBeforeStyle = getComputedStyle( + multipleInnerElement!, + ':before' + ); + + assert.strictEqual( + multipleInnerElementBeforeStyle.getPropertyValue('content'), + '"✓"' + ); + assert.dom('[data-test-multiple-outer]').hasStyle({ 'font-style': 'italic', 'font-weight': '900',