Skip to content
This repository has been archived by the owner on Sep 26, 2024. It is now read-only.

Commit

Permalink
MOC-294 flag for recording styles (#193)
Browse files Browse the repository at this point in the history
Add optional flag for recording styles
  • Loading branch information
jonathankap authored Sep 25, 2024
1 parent cb5f88f commit 7ad3812
Show file tree
Hide file tree
Showing 4 changed files with 161 additions and 14 deletions.
8 changes: 6 additions & 2 deletions packages/reactor/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,15 @@ export interface AppliedModifications {
setHighlight(highlight: boolean): void;
}

export interface DomJsonExportNode {
export type DomJsonExportOptions = {
styles?: boolean;
}

export type DomJsonExportNode = {
tag: string;
visible: boolean;
text?: string;
attributes?: Record<string, string>;
attributes: Record<string, string>;
children?: DomJsonExportNode[];
}

Expand Down
41 changes: 39 additions & 2 deletions packages/reactor/main.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type {
AppliedModifications,
DomJsonExportNode,
DomJsonExportOptions,
ModificationRequest,
} from "./interfaces.js";
import { generateModifications } from "./modifications.js";
Expand Down Expand Up @@ -39,7 +40,11 @@ export async function modifyDom(
}
}

export const htmlElementToJson = (root: HTMLElement): DomJsonExportNode[] => {
export const htmlElementToJson = (root: HTMLElement, options?: DomJsonExportOptions): DomJsonExportNode[] => {
const stylesMap: {[key: string]: string} = {};
const styleIndex: { idx: number } = { idx: 1 };
const exportStyles = options?.styles ?? false;

function nodeToJson(node: Node): DomJsonExportNode {
if (node instanceof Text) {
return {
Expand All @@ -49,6 +54,7 @@ export const htmlElementToJson = (root: HTMLElement): DomJsonExportNode[] => {
node.parentElement.offsetHeight > 0
: false,
text: node.data,
attributes: {}
};
}

Expand All @@ -60,15 +66,36 @@ export const htmlElementToJson = (root: HTMLElement): DomJsonExportNode[] => {
element instanceof HTMLElement
? element.offsetWidth > 0 || element.offsetHeight > 0
: false,
attributes: {},
};

if (element.attributes.length > 0) {
obj.attributes = {};
for (const attr of Array.from(element.attributes)) {
obj.attributes[attr.name] = attr.value;
}
}

if (exportStyles) {
const styles = window.getComputedStyle(element);
if (styles.length > 0) {
const styleString = Array.from(styles)
.map((style) => `${style}: ${styles.getPropertyValue(style)}`)
.join('; ');
let styleClass = stylesMap[styleString];
if (!styleClass) {
styleClass = `mocksi-${styleIndex.idx}`;
stylesMap[styleString] = styleClass;
styleIndex.idx += 1;
}

if (obj.attributes['class']) {
obj.attributes['class'] += ' ' + styleClass;
} else {
obj.attributes['class'] = styleClass;
}
}
}

const children = Array.from(element.childNodes).filter(textElementFilter);

// special case: if the element has only one child, and that child is a text node, then
Expand Down Expand Up @@ -101,6 +128,16 @@ export const htmlElementToJson = (root: HTMLElement): DomJsonExportNode[] => {
.filter(textElementFilter)
.map((child) => nodeToJson(child));

if (exportStyles) {
const stylesString = Object.entries(stylesMap).map(([styleString, clazz]) => `.${clazz} { ${styleString} }`).join('\n');
json.push({
tag: 'style',
visible: false,
text: stylesString,
attributes: {}
})
}

return json;
};

Expand Down
5 changes: 3 additions & 2 deletions packages/reactor/reactor.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type {
AppliedModifications,
DomJsonExportNode,
DomJsonExportOptions,
Highlighter,
ModificationRequest,
} from "./interfaces.js";
Expand Down Expand Up @@ -122,7 +123,7 @@ class Reactor {
* @throws {Error} If the reactor is not attached and no element is specified.
* @return {DomJsonExportNode[]} An array of `DomJsonExportNode` objects representing the exported DOM.
*/
exportDOM(element: null | HTMLElement = null): DomJsonExportNode[] {
exportDOM(element: null | HTMLElement = null, options?: DomJsonExportOptions): DomJsonExportNode[] {
let useElement = element;

if (!useElement) {
Expand All @@ -133,7 +134,7 @@ class Reactor {
}
}

return htmlElementToJson(useElement);
return htmlElementToJson(useElement, options);
}

/**
Expand Down
121 changes: 113 additions & 8 deletions packages/reactor/tests/main.test.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
import { JSDOM } from "jsdom";
import { describe, expect, it } from "vitest";
import { describe, expect, it, beforeEach } from "vitest";
import type { ModificationRequest } from "../interfaces";
import { modifyDom, modifyHtml } from "../main";
import { modifyDom, modifyHtml, htmlElementToJson } from "../main";
import type { AppliedModificationsImpl } from "../modifications";

// Set up a mock DOM environment
const dom = new JSDOM("<!DOCTYPE html><html><body></body></html>");
global.document = dom.window.document;
// biome-ignore lint/suspicious/noExplicitAny: testing
global.window = dom.window as any;

describe("modifyHtml", () => {
let doc: Document;

// Vitest beforeEach function for setup
beforeEach(() => {
doc = window.document.implementation.createHTMLDocument("Test Document");
});

it("should replace text content", async () => {
const html = `<div id="user-info">Eliza Hart</div>`;
const userRequest = JSON.stringify({
Expand Down Expand Up @@ -330,4 +331,108 @@ describe("modifyHtml", () => {
expect(result).not.toContain("<p>New content</p>");
expect(result).toContain("<p>Old content</p>");
});

it('should convert a simple HTML element to JSON', async () => {
doc.body.innerHTML = '<div id="test" class="example">Hello World!</div>';
const json = htmlElementToJson(doc.body);

expect(json).toEqual([
{
tag: 'div',
visible: false,
attributes: {
id: 'test',
class: 'example',
},
text: 'Hello World!',
},
]);
});

it('should handle nested HTML elements', async () => {
doc.body.innerHTML = '<div id="test"><p>Hello</p><span>World!</span></div>';
const json = htmlElementToJson(doc.body);

expect(json).toEqual([
{
tag: 'div',
visible: false,
attributes: {
id: 'test',
},
children: [
{
attributes: {},
tag: 'p',
visible: false,
text: 'Hello',
},
{
attributes: {},
tag: 'span',
visible: false,
text: 'World!',
},
],
},
]);
});

it('should export styles when the option is set', async () => {
doc.body.innerHTML = '<div id="test" style="color: red; font-size: 24px;">Hello World!</div>';
const json = htmlElementToJson(doc.body, {styles: true});

expect(json).toEqual([
{
tag: 'div',
visible: false,
attributes: {
class: 'mocksi-1',
id: 'test',
style: 'color: red; font-size: 24px;',
},
text: 'Hello World!',
},
{
attributes: {},
tag: "style",
text: ".mocksi-1 { display: block; color: rgb(255, 0, 0); font-size: 24px; visibility: visible; pointer-events: auto; background-color: rgba(0, 0, 0, 0); border-block-start-color: rgb(255, 0, 0); border-block-end-color: rgb(255, 0, 0); border-inline-start-color: rgb(255, 0, 0); border-inline-end-color: rgb(255, 0, 0); border-top-color: rgb(255, 0, 0); border-right-color: rgb(255, 0, 0); border-bottom-color: rgb(255, 0, 0); border-left-color: rgb(255, 0, 0); caret-color: auto }",
visible: false
}
]);
});

it('should consolidate styles when they are the same', async () => {
doc.body.innerHTML = '<div id="test" style="color: red; font-size: 24px;">Hello World!</div><div id="test" style="color: red; font-size: 24px;">Hello World!</div>';
const json = htmlElementToJson(doc.body, {styles: true});

expect(json).toEqual([
{
tag: 'div',
visible: false,
attributes: {
class: 'mocksi-1',
id: 'test',
style: 'color: red; font-size: 24px;',
},
text: 'Hello World!',
},
{
tag: 'div',
visible: false,
attributes: {
class: 'mocksi-1',
id: 'test',
style: 'color: red; font-size: 24px;',
},
text: 'Hello World!',
},
{
attributes: {},
tag: "style",
text: ".mocksi-1 { display: block; color: rgb(255, 0, 0); font-size: 24px; visibility: visible; pointer-events: auto; background-color: rgba(0, 0, 0, 0); border-block-start-color: rgb(255, 0, 0); border-block-end-color: rgb(255, 0, 0); border-inline-start-color: rgb(255, 0, 0); border-inline-end-color: rgb(255, 0, 0); border-top-color: rgb(255, 0, 0); border-right-color: rgb(255, 0, 0); border-bottom-color: rgb(255, 0, 0); border-left-color: rgb(255, 0, 0); caret-color: auto }",
visible: false
}
]);
});
});

0 comments on commit 7ad3812

Please sign in to comment.