Thanks to visit codestin.com
Credit goes to github.com

Skip to content

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 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 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 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 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 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 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 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 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