Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Deep formatter in playground #2099

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Apr 28, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion 2 eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ export default tseslint.config(
},

{
files: ['__tests__/**/*', 'perf/*'],
files: ['__tests__/**/*', 'website/**/*.test.ts', 'perf/*'],
languageOptions: {
globals: pluginJest.environments.globals.globals,
},
Expand Down
2 changes: 1 addition & 1 deletion 2 jest.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ const config = {
transform: {
'^.+\\.(js|ts)$': '<rootDir>/resources/jestPreprocessor.js',
},
testRegex: '/__tests__/.*\\.(ts|js)$',
testRegex: ['/__tests__/.*\\.(ts|js)$', '/website/.*\\.test\\.(ts|js)$'],
testPathIgnorePatterns: ['/__tests__/ts-utils.ts'],
};

Expand Down
14 changes: 14 additions & 0 deletions 14 package-lock.json

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

1 change: 1 addition & 0 deletions 1 package.json
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@
"@codemirror/theme-one-dark": "^6.1.2",
"@codemirror/view": "^6.36.5",
"@eslint/js": "^9.20.0",
"@jdeniau/immutable-devtools": "^0.2.0",
"@jest/globals": "^29.7.0",
"@rollup/plugin-buble": "1.0.3",
"@rollup/plugin-commonjs": "28.0.2",
Expand Down
24 changes: 4 additions & 20 deletions 24 website/src/repl/FormatterOutput.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { toHTML } from 'jsonml-html';
import { useEffect, useRef, type JSX } from 'react';
import { Element, JsonMLElementList } from '../worker/jsonml-types';

/**
* immutable-devtools is a console custom formatter.
Expand All @@ -8,36 +9,19 @@ import { useEffect, useRef, type JSX } from 'react';
* The `jsonml-html` package can convert jsonml to HTML.
*/
type Props = {
output: {
header: Array<unknown>;
body?: Array<unknown>;
};
output: JsonMLElementList | Element;
};

export default function FormatterOutput({ output }: Props): JSX.Element {
const header = useRef<HTMLDivElement>(null);
const body = useRef<HTMLDivElement>(null);

const htmlHeader = toHTML(output.header);
const htmlHeader = toHTML(output);

useEffect(() => {
if (header.current && htmlHeader) {
header.current.replaceChildren(htmlHeader);
}
}, [htmlHeader]);

const htmlBody = output.body ? toHTML(output.body) : null;

useEffect(() => {
if (body.current) {
body.current.replaceChildren(htmlBody ?? '');
}
}, [htmlBody]);

return (
<>
<div ref={header}></div>
<div ref={body}></div>
</>
);
return <div ref={header}></div>;
}
16 changes: 10 additions & 6 deletions 16 website/src/repl/Repl.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,13 @@ import React, { useEffect, useRef, useState, type JSX } from 'react';
import { Editor } from './Editor';
import FormatterOutput from './FormatterOutput';
import './repl.css';
import { Element, JsonMLElementList } from '../worker/jsonml-types';

type Props = { defaultValue: string; onRun?: (code: string) => void };

function Repl({ defaultValue, onRun }: Props): JSX.Element {
const [code, setCode] = useState<string>(defaultValue);
const [output, setOutput] = useState<{
header: Array<unknown>;
body?: Array<unknown>;
}>({ header: [] });
const [output, setOutput] = useState<JsonMLElementList | Element>([]);
const workerRef = useRef<Worker | null>(null);

useEffect(() => {
Expand Down Expand Up @@ -41,9 +39,15 @@ function Repl({ defaultValue, onRun }: Props): JSX.Element {
workerRef.current.postMessage(code);
workerRef.current.onmessage = (event) => {
if (event.data.error) {
setOutput({ header: ['div', 'Error: ' + event.data.error] });
setOutput(['div', 'Error: ' + event.data.error]);
} else {
setOutput(event.data.output);
const { output } = event.data;

if (typeof output === 'object' && !Array.isArray(output)) {
setOutput(['div', { object: output }]);
} else {
setOutput(output);
}
}
};
}
Expand Down
104 changes: 104 additions & 0 deletions 104 website/src/worker/jsonml-types.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { describe, it, expect } from '@jest/globals';
import { Element, explodeElement } from './jsonml-types';

describe('explodeElement', () => {
it('should explode an element', () => {
expect(explodeElement(['div'])).toEqual({
tagName: 'div',
attributes: undefined,
children: [],
});
});

it('should explode an element with attributes', () => {
expect(explodeElement(['div', { id: 'test' }])).toEqual({
tagName: 'div',
attributes: { id: 'test' },
children: [],
});
});

it('should explode an element with children', () => {
expect(explodeElement(['div', { id: 'test' }, 'Hello'])).toEqual({
tagName: 'div',
attributes: { id: 'test' },
children: ['Hello'],
});
});

it('should explode an element with multiple children', () => {
expect(explodeElement(['div', { id: 'test' }, 'Hello', 'World'])).toEqual({
tagName: 'div',
attributes: { id: 'test' },
children: ['Hello', 'World'],
});
});

it('should explode an element without attributes with multiple children', () => {
expect(explodeElement(['div', 'Hello', 'World'])).toEqual({
tagName: 'div',
attributes: undefined,
children: ['Hello', 'World'],
});
});

it('should explode an element with a nested element', () => {
expect(explodeElement(['div', { id: 'test' }, ['span', 'Hello']])).toEqual({
tagName: 'div',
attributes: { id: 'test' },
children: [['span', 'Hello']],
});
});

it('should explode an element with a nested element with attributes', () => {
expect(
explodeElement([
'div',
{ id: 'test' },
['span', { class: 'test' }, 'Hello'],
])
).toEqual({
tagName: 'div',
attributes: { id: 'test' },
children: [['span', { class: 'test' }, 'Hello']],
});
});

it('should explode an element with a nested element with multiple children', () => {
expect(
explodeElement([
'div',
{ id: 'test' },
['span', 'Hello'],
['span', { id: 'world' }, 'World'],
])
).toEqual({
tagName: 'div',
attributes: { id: 'test' },
children: [
['span', 'Hello'],
['span', { id: 'world' }, 'World'],
],
});
});

it('should handle immutable list jsonml', () => {
const spanElement: Element = [
'span',
{ style: 'color: light-dark( #881391, #D48CE6)' },
'0: ',
];
const objectElement: Element = ['object', { object: ['a'] }];

const element: Element = ['li', spanElement, objectElement];

expect(explodeElement(element)).toEqual({
tagName: 'li',
attributes: undefined,
children: [
['span', { style: 'color: light-dark( #881391, #D48CE6)' }, '0: '],
['object', { object: ['a'] }],
],
});
});
});
55 changes: 51 additions & 4 deletions 55 website/src/worker/jsonml-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,67 @@
// Basic types
type TagName = string;
type AttributeName = string;
type AttributeValue = string | number | boolean | null;
type AttributeValue = string | number | boolean | null | object;

// Attributes
// type Attribute = [AttributeName, AttributeValue];
// type AttributeList = Attribute[];
export type Attributes = Record<AttributeName, AttributeValue>;

type ElementWithAttributes =
| [TagName, Attributes, ...Element[]] // [tag-name, attributes, element-list]
| [TagName, Attributes]; // [tag-name, attributes]

// Elements
export type Element =
| [TagName, Attributes, ...Element[]] // [tag-name, attributes, element-list]
| [TagName, Attributes] // [tag-name, attributes]
| ElementWithAttributes
| [TagName, ...Element[]] // [tag-name, element-list]
| [TagName] // [tag-name]
| string; // string

// Element list is just a list of elements
export type JsonMLElementList = Element[];
export type JsonMLElementList = Array<Element | JsonMLElementList>;

export function isElement(maybeElement: unknown): maybeElement is Element {
return (
typeof maybeElement === 'string' ||
(Array.isArray(maybeElement) &&
maybeElement.length >= 1 &&
typeof maybeElement[0] === 'string')
);
}

function hasAttributes(
maybeElementWithAttributes: Element
): maybeElementWithAttributes is ElementWithAttributes {
return (
Array.isArray(maybeElementWithAttributes) &&
typeof maybeElementWithAttributes[1] === 'object' &&
!Array.isArray(maybeElementWithAttributes[1])
);
}

type ExplodedElement = {
tagName: TagName;
attributes?: Attributes;
children: Element[];
};

export function explodeElement(element: Element): ExplodedElement {
if (typeof element === 'string') {
return { tagName: element, children: [] };
}

if (hasAttributes(element)) {
const [tagName, attributes, ...children] = element;

return { tagName, attributes, children };
}

const [tagName, attributes, ...children] = element;

return {
tagName,
children: [attributes, ...children].filter(isElement),
};
}
Loading
Morty Proxy This is a proxified and sanitized view of the page, visit original site.