Files
webstudio/packages/html-data/bin/elements.ts
T
Bogdan Chadkin 4dfdf50380 feat: migrate forms components to html elements (#5230)
Ref https://github.com/webstudio-is/webstudio/issues/3632

This is big one. So I switched to html elements whole forms section in
components. Though here I introduced some deviation from specification

**Textarea**

Initial value is represented as text content
```html
<textarea>content</textarea>
````

Though it is trickier to manage text content as state and all frameworks
rely on value property of textarea dom interface.

**Select**

Similar story. There is no value attribute and state is represented by
selected boolean attribute on options

```html
<select>
  <option value="value1" selected></option>
  <option value="value2"></option>
</select>
```

We replace selected attribute on option with value attribute on selected
when paste.

So basically all form elements are using "value" and "checked"
attributes which can be easily mapped to html and all frameworks.
2025-05-24 15:11:49 +03:00

100 lines
2.7 KiB
TypeScript

import { dirname } from "node:path";
import { mkdir, writeFile } from "node:fs/promises";
import {
findTags,
getTextContent,
loadHtmlIndices,
parseHtml,
} from "./crawler";
// Crawl WHATWG HTML.
const html = await loadHtmlIndices();
const document = parseHtml(html);
type Element = {
description: string;
categories: string[];
children: string[];
};
const elementsByTag: Record<string, Element> = {};
/**
* scrape elements table with content model
*/
{
const table = findTags(document, "table").find((table) => {
const [caption] = findTags(table, "caption");
return getTextContent(caption).toLowerCase().includes("list of elements");
});
const [tbody] = findTags(table, "tbody");
const rows = findTags(tbody, "tr");
const parseList = (text: string) => {
return text
.trim()
.split(/\s*;\s*/)
.map((item) => (item.endsWith("*") ? item.slice(0, -1) : item));
};
for (const row of rows) {
const elements = getTextContent(row.childNodes[0])
.trim()
.split(/\s*,\s*/)
.filter((tag) => {
// skip "SVG svg" amd "MathML math"
return !tag.includes(" ");
});
const description = getTextContent(row.childNodes[1]);
const categories = parseList(getTextContent(row.childNodes[2]));
let children = parseList(getTextContent(row.childNodes[4]));
for (const tag of elements) {
// textarea does not have value attribute and text content is used as initial value
// introduce fake value attribute to manage initial state similar to input
if (tag === "textarea") {
children = [];
}
elementsByTag[tag] = {
description,
categories,
children: children.includes("empty") ? [] : children,
};
}
}
}
const contentModel = `type Element = {
description: string;
categories: string[];
children: string[];
};
export const elementsByTag: Record<string, Element> = ${JSON.stringify(elementsByTag, null, 2)};
`;
const contentModelFile = "./src/__generated__/elements.ts";
await mkdir(dirname(contentModelFile), { recursive: true });
await writeFile(contentModelFile, contentModel);
const tags: string[] = [];
for (const [tag, element] of Object.entries(elementsByTag)) {
if (element.categories.includes("metadata")) {
continue;
}
tags.push(tag);
}
const getTagScore = (tag: string) => {
if (tag === "div") {
return 20;
}
if (tag === "span") {
return 10;
}
return 0;
};
// put div and span first
tags.sort((left, right) => getTagScore(right) - getTagScore(left));
const tagsContent = `export const tags: string[] = ${JSON.stringify(tags, null, 2)};
`;
const tagsFile = "../sdk/src/__generated__/tags.ts";
await mkdir(dirname(tagsFile), { recursive: true });
await writeFile(tagsFile, tagsContent);