mirror of
https://github.com/webstudio-is/webstudio.git
synced 2026-11-09 05:26:24 +09:00
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.
100 lines
2.7 KiB
TypeScript
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);
|