Skip to content

Commit f134553

Browse files
authored
feat: add descriptions to tags (#5164)
Ref #3632 Scraped descriptions from html spec to show in tag control. <img width="297" alt="Screenshot 2025-04-26 at 12 46 21" src="https://github.com/user-attachments/assets/444e319d-e24f-429e-b09d-1db95cc248d5" />
1 parent c1ec5be commit f134553

6 files changed

Lines changed: 703 additions & 335 deletions

File tree

apps/builder/app/builder/features/settings-panel/controls/tag-control.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { useStore } from "@nanostores/react";
2-
import { Select } from "@webstudio-is/design-system";
2+
import { Box, Select, theme } from "@webstudio-is/design-system";
3+
import { elementsByTag } from "@webstudio-is/html-data";
34
import { $selectedInstance } from "~/shared/awareness";
45
import { updateWebstudioData } from "~/shared/instance-utils";
56
import { type ControlProps, VerticalLayout } from "../shared";
@@ -39,6 +40,11 @@ export const TagControl = ({ meta, prop }: ControlProps<"tag">) => {
3940
}
4041
});
4142
}}
43+
getDescription={(item) => (
44+
<Box css={{ width: theme.spacing[28] }}>
45+
{elementsByTag[item].description}
46+
</Box>
47+
)}
4248
/>
4349
</VerticalLayout>
4450
);

apps/builder/app/shared/content-model.ts

Lines changed: 24 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,4 @@
1-
import {
2-
categoriesByTag,
3-
childrenCategoriesByTag,
4-
} from "@webstudio-is/html-data";
1+
import { elementsByTag } from "@webstudio-is/html-data";
52
import {
63
parseComponentName,
74
type ContentModel,
@@ -55,7 +52,7 @@ const isIntersected = (arrayA: string[], arrayB: string[]) => {
5552
* so img can be put into links and buttons
5653
*/
5754
const isTagInteractive = (tag: string) => {
58-
return tag !== "img" && categoriesByTag[tag].includes("interactive");
55+
return tag !== "img" && elementsByTag[tag].categories.includes("interactive");
5956
};
6057

6158
const isTagSatisfyingContentModel = ({
@@ -87,7 +84,7 @@ const isTagSatisfyingContentModel = ({
8784
// valid way to nest interactive elements
8885
if (
8986
allowedCategories.includes("labelable") &&
90-
categoriesByTag[tag].includes("labelable")
87+
elementsByTag[tag].categories.includes("labelable")
9188
) {
9289
return true;
9390
}
@@ -102,47 +99,47 @@ const isTagSatisfyingContentModel = ({
10299
return false;
103100
}
104101
// instance matches parent constraints
105-
return isIntersected(allowedCategories, categoriesByTag[tag]);
102+
return isIntersected(allowedCategories, elementsByTag[tag].categories);
106103
};
107104

108105
/**
109106
* compute possible categories for tag children
110107
*/
111-
const getTagChildrenCategories = (
108+
const getElementChildren = (
112109
tag: undefined | string,
113110
allowedCategories: undefined | string[]
114111
) => {
115112
// components without tag behave like transparent category
116113
// and pass through parent constraints
117-
let childrenCategories: string[] =
118-
tag === undefined ? ["transparent"] : childrenCategoriesByTag[tag];
119-
if (childrenCategories.includes("transparent") && allowedCategories) {
120-
childrenCategories = allowedCategories;
114+
let elementChildren: string[] =
115+
tag === undefined ? ["transparent"] : elementsByTag[tag].children;
116+
if (elementChildren.includes("transparent") && allowedCategories) {
117+
elementChildren = allowedCategories;
121118
}
122119
// introduce custom non-interactive category to restrict nesting interactive elements
123120
// like button > button or a > input
124121
if (
125122
tag &&
126123
(isTagInteractive(tag) || allowedCategories?.includes("non-interactive"))
127124
) {
128-
childrenCategories = [...childrenCategories, "non-interactive"];
125+
elementChildren = [...elementChildren, "non-interactive"];
129126
}
130127
// interactive exception, label > input or label > button are considered
131128
// valid way to nest interactive elements
132129
// pass through labelable to match controls with labelable category
133130
if (tag === "label" || allowedCategories?.includes("labelable")) {
134131
// stop passing through labelable to control children
135132
// to prevent label > button > input
136-
if (tag && categoriesByTag[tag].includes("labelable") === false) {
137-
childrenCategories = [...childrenCategories, "labelable"];
133+
if (tag && elementsByTag[tag].categories.includes("labelable") === false) {
134+
elementChildren = [...elementChildren, "labelable"];
138135
}
139136
}
140137
// introduce custom non-form category to restrict nesting form elements
141138
// like form > div > form
142139
if (tag === "form" || allowedCategories?.includes("non-form")) {
143-
childrenCategories = [...childrenCategories, "non-form"];
140+
elementChildren = [...elementChildren, "non-form"];
144141
}
145-
return childrenCategories;
142+
return elementChildren;
146143
};
147144

148145
/**
@@ -171,7 +168,7 @@ const computeAllowedCategories = ({
171168
continue;
172169
}
173170
const tag = getTag({ instance, metas, props });
174-
allowedCategories = getTagChildrenCategories(tag, allowedCategories);
171+
allowedCategories = getElementChildren(tag, allowedCategories);
175172
}
176173
return allowedCategories;
177174
};
@@ -375,7 +372,7 @@ export const isTreeSatisfyingContentModel = ({
375372
}
376373
let isSatisfying = isTagSatisfying && isComponentSatisfying;
377374
const contentModel = getComponentContentModel(metas.get(instance.component));
378-
allowedCategories = getTagChildrenCategories(tag, allowedCategories);
375+
allowedCategories = getElementChildren(tag, allowedCategories);
379376
allowedParentCategories = contentModel.children;
380377
if (contentModel.descendants) {
381378
allowedAncestorCategories ??= [];
@@ -584,11 +581,11 @@ export const findClosestContainer = ({
584581
}
585582
const tag = getTag({ instance, props, metas });
586583
const meta = metas.get(instance.component);
587-
const childrenCategories = tag ? childrenCategoriesByTag[tag] : undefined;
588-
const childrenComponentCategories = getComponentContentModel(meta).children;
584+
const elementChildren = tag ? elementsByTag[tag].children : undefined;
585+
const componentChildren = getComponentContentModel(meta).children;
589586
if (
590-
childrenComponentCategories.length === 0 ||
591-
(childrenCategories && childrenCategories.length === 0)
587+
componentChildren.length === 0 ||
588+
(elementChildren && elementChildren.length === 0)
592589
) {
593590
continue;
594591
}
@@ -621,11 +618,11 @@ export const findClosestNonTextualContainer = ({
621618
}
622619
const tag = getTag({ instance, props, metas });
623620
const meta = metas.get(instance.component);
624-
const childrenCategories = tag ? childrenCategoriesByTag[tag] : undefined;
625-
const childrenComponentCategories = getComponentContentModel(meta).children;
621+
const elementChildren = tag ? elementsByTag[tag].children : undefined;
622+
const componentChildren = getComponentContentModel(meta).children;
626623
if (
627-
childrenComponentCategories.length === 0 ||
628-
(childrenCategories && childrenCategories.length === 0)
624+
componentChildren.length === 0 ||
625+
(elementChildren && elementChildren.length === 0)
629626
) {
630627
continue;
631628
}

packages/html-data/bin/elements.ts

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,13 @@ import {
1212
const html = await loadHtmlIndices();
1313
const document = parseHtml(html);
1414

15-
const categoriesByTag: Record<string, string[]> = {};
16-
const childrenCategoriesByTag: Record<string, string[]> = {};
15+
type Element = {
16+
description: string;
17+
categories: string[];
18+
children: string[];
19+
};
20+
21+
const elementsByTag: Record<string, Element> = {};
1722

1823
/**
1924
* scrape elements table with content model
@@ -39,18 +44,27 @@ const childrenCategoriesByTag: Record<string, string[]> = {};
3944
// skip "SVG svg" amd "MathML math"
4045
return !tag.includes(" ");
4146
});
47+
const description = getTextContent(row.childNodes[1]);
4248
const categories = parseList(getTextContent(row.childNodes[2]));
4349
const children = parseList(getTextContent(row.childNodes[4]));
4450
for (const tag of elements) {
45-
categoriesByTag[tag] = categories;
46-
childrenCategoriesByTag[tag] = children.includes("empty") ? [] : children;
51+
elementsByTag[tag] = {
52+
description,
53+
categories,
54+
children: children.includes("empty") ? [] : children,
55+
};
4756
}
4857
}
4958
}
5059

51-
let contentModel = ``;
52-
contentModel += `export const categoriesByTag: Record<string, string[]> = ${JSON.stringify(categoriesByTag, null, 2)};\n`;
53-
contentModel += `export const childrenCategoriesByTag: Record<string, string[]> = ${JSON.stringify(childrenCategoriesByTag, null, 2)};\n`;
54-
const contentModelFile = "./src/__generated__/content-model.ts";
60+
const contentModel = `type Element = {
61+
description: string;
62+
categories: string[];
63+
children: string[];
64+
};
65+
66+
export const elementsByTag: Record<string, Element> = ${JSON.stringify(elementsByTag, null, 2)};
67+
`;
68+
const contentModelFile = "./src/__generated__/elements.ts";
5569
await mkdir(dirname(contentModelFile), { recursive: true });
5670
await writeFile(contentModelFile, contentModel);

0 commit comments

Comments
 (0)