Skip to content

Commit

Permalink
Merge 064bd72 into 2568b2e
Browse files Browse the repository at this point in the history
  • Loading branch information
DylanPiercey committed Aug 26, 2019
2 parents 2568b2e + 064bd72 commit 2401a61
Show file tree
Hide file tree
Showing 7 changed files with 406 additions and 130 deletions.
16 changes: 7 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,14 +120,12 @@ visualHTML(div); // Returns the html below as string.
```

```html
<div
style="
transform: translateX(-100px);
width: 100px;
<div style="
background: red;
height: 200px;
background: red
"
>
transform: translateX(-100px);
width: 100px
">
<span style="color: #333">
Hello!
</span>
Expand All @@ -138,7 +136,7 @@ visualHTML(div); // Returns the html below as string.
</label>
<label>
Password:
<input />
<input type="password"/>
</label>
<label>
Remember Me:
Expand All @@ -151,7 +149,7 @@ visualHTML(div); // Returns the html below as string.
</div>
```

In the above output you can see that the majority of attributes have been removed, and styles are now included inline. The `type="checkbox"` is still present on the `Remember Me:` checkbox as it causes the browser to display the textbox differently. The default `type` for an `input` is `text`, and a `type="password"` is visually identical to `type="text"` unless you've styled it differently yourself in which case an inline style attribute would be present.
In the above output you can see that the majority of attributes have been removed, and styles are now included inline. The `type="text"` on the first `input` was removed since it is a default. All attributes and properties are also sorted alphabetically to be more stable.

## How is this different than x!?

Expand Down
56 changes: 28 additions & 28 deletions src/__tests__/examples.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,34 +44,34 @@ test("runs the first example", () => {
`
)
).toMatchInlineSnapshot(`
"<div style=\\"
transform: translateX(-100px);
width: 100px;
height: 200px;
background: red
\\">
<span style=\\"color: #333\\">
Hello!
</span>
<form>
<label>
Username:
<input/>
</label>
<label>
Password:
<input/>
</label>
<label>
Remember Me:
<input type=\\"checkbox\\"/>
</label>
<button>
Sign in
</button>
</form>
</div>"
`);
"<div style=\\"
background: red;
height: 200px;
transform: translateX(-100px);
width: 100px
\\">
<span style=\\"color: #333\\">
Hello!
</span>
<form>
<label>
Username:
<input/>
</label>
<label>
Password:
<input type=\\"password\\"/>
</label>
<label>
Remember Me:
<input type=\\"checkbox\\"/>
</label>
<button>
Sign in
</button>
</form>
</div>"
`);
});

test("works with diff snapshots", () => {
Expand Down
69 changes: 37 additions & 32 deletions src/__tests__/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,22 @@ afterEach(() => {
test("removes any properties that do not apply user agent styles", () => {
expect(
testHTML(`
<input type="text" class="other"/>
<input type="text" class="other" checked/>
`)
).toMatchInlineSnapshot(`"<input/>"`);
});

test("preserves any properties that do apply user agent styles", () => {
expect(
testHTML(`
<input type="search" class="other"/>
<input type="checkbox" class="other" checked/>
`)
).toMatchInlineSnapshot(`"<input type=\\"search\\"/>"`);
).toMatchInlineSnapshot(`
"<input
checked
type=\\"checkbox\\"
/>"
`);
});

test("preserves any inline styles", () => {
Expand All @@ -58,11 +63,11 @@ test("inline styles override applied styles", () => {
`
)
).toMatchInlineSnapshot(`
"<div style=\\"
color: green;
background: red
\\"/>"
`);
"<div style=\\"
background: red;
color: green
\\"/>"
`);
});

test("accounts for !important", () => {
Expand All @@ -80,8 +85,8 @@ test("accounts for !important", () => {
)
).toMatchInlineSnapshot(`
"<div style=\\"
color: green !important;
background: blue !important
background: blue !important;
color: green !important
\\"/>"
`);
});
Expand Down Expand Up @@ -135,12 +140,12 @@ test("supports multiple applied styles", () => {
`
)
).toMatchInlineSnapshot(`
"<div style=\\"
color: blue;
background-color: red;
font-size: 1rem
\\"/>"
`);
"<div style=\\"
background-color: red;
color: blue;
font-size: 1rem
\\"/>"
`);
});

test("includes children", () => {
Expand Down Expand Up @@ -275,22 +280,22 @@ test("includes pseudo elements", () => {
`;

expect(testHTML(html, styles)).toMatchInlineSnapshot(`
"<div>
<style scoped>
::selection {background: red}
::after {
content: \\"hello\\";
color: green
}
</style>
<span>
<style scoped>
::selection {background: blue}
</style>
Content
</span>
</div>"
`);
"<div>
<style scoped>
::after {
color: green;
content: \\"hello\\"
}
::selection {background: red}
</style>
<span>
<style scoped>
::selection {background: blue}
</style>
Content
</span>
</div>"
`);
});

function testHTML(html: string, styles: string = "") {
Expand Down
100 changes: 43 additions & 57 deletions src/attributes.ts
Original file line number Diff line number Diff line change
@@ -1,68 +1,54 @@
let FRAME: HTMLIFrameElement | null = null;
import { HTML_PROPERTIES } from "./html-properties";

/**
* Given an element, returns any attributes that have a cause a visual change.
* This works by copying the element to an iframe without any styles and
* testing the computed styles while toggling the attributes.
* This works by checking against a whitelist of known visual properties, and
* their related attribute name.
*/
export function getVisualAttributes(el: Element) {
let visualAttributes: Array<{ name: string; value: string }> | null = null;
const document = el.ownerDocument!;
FRAME = FRAME || document.createElement("iframe");

document.body.appendChild(FRAME);

const contentDocument = FRAME.contentDocument!;
const contentWindow = contentDocument.defaultView!;
const clone = contentDocument.importNode(el, false);
const { attributes } = clone;

contentDocument.body.appendChild(clone);

const defaultStyles = contentWindow.getComputedStyle(clone);

for (let i = attributes.length; i--; ) {
const attr = attributes[i];

if (attr.name === "style") {
continue;
let visualAttributes: Array<{
name: string;
value: string | boolean | null;
}> | null = null;
if (!el.namespaceURI || el.namespaceURI === "http://www.w3.org/1999/xhtml") {
// For HTML elements we look at a whitelist of properties and compare against the default value.
const defaults = el.ownerDocument!.createElement(el.localName);

for (const prop in HTML_PROPERTIES) {
const { alias, tests } = HTML_PROPERTIES[
prop as keyof typeof HTML_PROPERTIES
];
const name = alias || prop;
const value = el[prop];

if (value !== defaults[prop]) {
for (const test of tests) {
if (test(el)) {
(visualAttributes || (visualAttributes = [])).push({ name, value });
break;
}
}
}
}

clone.removeAttributeNode(attr);

if (
!computedStylesEqual(defaultStyles, contentWindow.getComputedStyle(clone))
) {
(visualAttributes || (visualAttributes = [])).push({
name: attr.name,
value: attr.value
});
} else {
// For other namespaces we assume all attributes are visual, except for a blacklist.
const { attributes } = el;

for (let i = 0, len = attributes.length; i < len; i++) {
const { name, value } = attributes[i];

if (
!(
(el.localName === "a" && /^(?:xmlns:)?href$/i.test(name)) ||
/^(?:class|id|style|lang|target|xmlns(?::.+)?|xlink:(?!href).+|xml:(?:lang|base)|on*|aria-*|data-*)$/i.test(
name
)
)
) {
(visualAttributes || (visualAttributes = [])).push({ name, value });
}
}

clone.setAttributeNode(attr);
}

contentDocument.body.removeChild(clone);
document.body.removeChild(FRAME);

return visualAttributes;
}

/**
* Checks if two CSSStyleDeclarations have the same styles applied.
*/
function computedStylesEqual(a: CSSStyleDeclaration, b: CSSStyleDeclaration) {
if (a.length !== b.length) {
return false;
}

for (let i = a.length; i--; ) {
const name = a[i];

if (a[name] !== b[name]) {
return false;
}
}

return true;
}
Loading

0 comments on commit 2401a61

Please sign in to comment.