Skip to content

Commit

Permalink
feat: support media queries
Browse files Browse the repository at this point in the history
  • Loading branch information
natemoo-re committed Sep 11, 2023
1 parent 1067399 commit 7c93190
Show file tree
Hide file tree
Showing 5 changed files with 107 additions and 6 deletions.
17 changes: 17 additions & 0 deletions .changeset/beige-boats-float.md
@@ -0,0 +1,17 @@
---
"ultrahtml": minor
---

Add support for static media queries to `ultrahtml/transformers/inline`.

You may now pass an `env` value to the transformer, for example:

```js
import { transform } from "ultrahtml";
import inline from "ultrahtml/transformers/inline";

const output = await transform(input, [
// Acts as if the screen is 960px wide and 1280px tall
inline({ env: { width: 960, height: 1280 } }),
]);
```
1 change: 1 addition & 0 deletions package.json
Expand Up @@ -73,6 +73,7 @@
"prettier": "^2.5.1",
"pretty-bytes": "^6.0.0",
"stylis": "^4.1.2",
"media-query-fns": "^2.0.0",
"typescript": "^4.7.4",
"vitest": "^0.20.2"
}
Expand Down
14 changes: 14 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

43 changes: 37 additions & 6 deletions src/transformers/inline.ts
@@ -1,12 +1,14 @@
import { walkSync, ELEMENT_NODE, TEXT_NODE, Node, ElementNode } from "../index.js";
import { querySelectorAll, specificity } from "../selector.js";
import { compile } from "stylis";
import { type Element as CSSEntry, compile } from "stylis";
import { compileQuery, matches, type Environment } from 'media-query-fns';

export interface InlineOptions {
/** Emit `style` attributes as objects rather than strings. */
useObjectSyntax?: boolean;
useObjectSyntax: boolean;
env: Partial<Environment> & { width: number, height: number };
}
export default function inline(opts?: InlineOptions) {
export default function inline(opts?: Partial<InlineOptions>) {
const { useObjectSyntax = false } = opts ?? {};
return (doc: Node): Node => {
const style: string[] = useObjectSyntax ? [':where([style]) {}'] : [];
Expand All @@ -31,8 +33,9 @@ export default function inline(opts?: InlineOptions) {
const styles = style.join("\n");
const css = compile(styles);
const selectors = new Map<string, Record<string, string>>();
for (const rule of css) {
if (rule.type === "rule") {

function applyRule(rule: CSSEntry) {
if (rule.type === 'rule') {
const rules = Object.fromEntries(
(rule.children as unknown as Element[])
.map((child: any) => [child.props, child.children])
Expand All @@ -41,9 +44,23 @@ export default function inline(opts?: InlineOptions) {
const value = Object.assign(selectors.get(selector) ?? {}, rules);
selectors.set(selector, value);
}
} else if (rule.type === '@media' && opts?.env) {
const env = getEnvironment(opts.env);
const args = Array.isArray(rule.props) ? rule.props : [rule.props];
const queries = args.map(arg => compileQuery(arg))
for (const query of queries) {
if (matches(query, env)) {
for (const child of rule.children) {
applyRule(child as CSSEntry)
}
return;
}
}
}
}

for (const rule of css) {
applyRule(rule);
}
const rules = new Map<Node, Record<string, string>>();
for (const [selector, styles] of Array.from(selectors).sort(([a], [b]) => {
const $a = specificity(a);
Expand Down Expand Up @@ -87,3 +104,17 @@ export default function inline(opts?: InlineOptions) {
function isHttpURL(href: string): boolean {
return href.startsWith('http://') || href.startsWith('https://');
}

type AlwaysDefinedValues = "widthPx" | "heightPx" | "deviceWidthPx" | "deviceHeightPx" | "dppx";
type ResolvedEnvironment = Omit<Partial<Environment>, AlwaysDefinedValues> & Record<AlwaysDefinedValues, number>;
function getEnvironment(baseEnv: InlineOptions['env']): ResolvedEnvironment {
const { width, height, dppx = 1, widthPx = width, heightPx = height, deviceWidthPx = width * dppx, deviceHeightPx = height * dppx, ...env } = baseEnv;
return {
widthPx,
heightPx,
deviceWidthPx,
deviceHeightPx,
dppx,
...env
}
}
38 changes: 38 additions & 0 deletions test/transformers/inline.test.tsx
Expand Up @@ -34,6 +34,44 @@ describe("inline", () => {
`<div class="cool" style="color:red;">Hello world</div>`
);
});

it("inlines styles when @media is matched", async () => {
const input = `<div class="cool">Hello world</div>
<style>
.cool {
color: red;
}
@media (min-width: 960px) {
.cool {
color: green;
}
}
</style>`;
const output = await transform(input, [inline({ env: { width: 961, height: 1280 }})]);
expect(output.trim()).toEqual(
`<div class="cool" style="color:green;">Hello world</div>`
);
});

it("does not inline styles when @media cannot be matched", async () => {
const input = `<div class="cool">Hello world</div>
<style>
.cool {
color: red;
}
@media (min-width: 960px) {
.cool {
color: green;
}
}
</style>`;
const output = await transform(input, [inline({ env: { width: 959, height: 1280 }})]);
expect(output.trim()).toEqual(
`<div class="cool" style="color:red;">Hello world</div>`
);
});
});

describe("inline jsx", () => {
Expand Down

0 comments on commit 7c93190

Please sign in to comment.