Skip to content
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
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useStore } from "@nanostores/react";
import { Select } from "@webstudio-is/design-system";
import { Box, Select, theme } from "@webstudio-is/design-system";
import { elementsByTag } from "@webstudio-is/html-data";
import { $selectedInstance } from "~/shared/awareness";
import { updateWebstudioData } from "~/shared/instance-utils";
import { type ControlProps, VerticalLayout } from "../shared";
Expand Down Expand Up @@ -39,6 +40,11 @@ export const TagControl = ({ meta, prop }: ControlProps<"tag">) => {
}
});
}}
getDescription={(item) => (
<Box css={{ width: theme.spacing[28] }}>
{elementsByTag[item].description}
</Box>
)}
/>
</VerticalLayout>
);
Expand Down
51 changes: 24 additions & 27 deletions apps/builder/app/shared/content-model.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
import {
categoriesByTag,
childrenCategoriesByTag,
} from "@webstudio-is/html-data";
import { elementsByTag } from "@webstudio-is/html-data";
import {
parseComponentName,
type ContentModel,
Expand Down Expand Up @@ -55,7 +52,7 @@ const isIntersected = (arrayA: string[], arrayB: string[]) => {
* so img can be put into links and buttons
*/
const isTagInteractive = (tag: string) => {
return tag !== "img" && categoriesByTag[tag].includes("interactive");
return tag !== "img" && elementsByTag[tag].categories.includes("interactive");
};

const isTagSatisfyingContentModel = ({
Expand Down Expand Up @@ -87,7 +84,7 @@ const isTagSatisfyingContentModel = ({
// valid way to nest interactive elements
if (
allowedCategories.includes("labelable") &&
categoriesByTag[tag].includes("labelable")
elementsByTag[tag].categories.includes("labelable")
) {
return true;
}
Expand All @@ -102,47 +99,47 @@ const isTagSatisfyingContentModel = ({
return false;
}
// instance matches parent constraints
return isIntersected(allowedCategories, categoriesByTag[tag]);
return isIntersected(allowedCategories, elementsByTag[tag].categories);
};

/**
* compute possible categories for tag children
*/
const getTagChildrenCategories = (
const getElementChildren = (
tag: undefined | string,
allowedCategories: undefined | string[]
) => {
// components without tag behave like transparent category
// and pass through parent constraints
let childrenCategories: string[] =
tag === undefined ? ["transparent"] : childrenCategoriesByTag[tag];
if (childrenCategories.includes("transparent") && allowedCategories) {
childrenCategories = allowedCategories;
let elementChildren: string[] =
tag === undefined ? ["transparent"] : elementsByTag[tag].children;
if (elementChildren.includes("transparent") && allowedCategories) {
elementChildren = allowedCategories;
}
// introduce custom non-interactive category to restrict nesting interactive elements
// like button > button or a > input
if (
tag &&
(isTagInteractive(tag) || allowedCategories?.includes("non-interactive"))
) {
childrenCategories = [...childrenCategories, "non-interactive"];
elementChildren = [...elementChildren, "non-interactive"];
}
// interactive exception, label > input or label > button are considered
// valid way to nest interactive elements
// pass through labelable to match controls with labelable category
if (tag === "label" || allowedCategories?.includes("labelable")) {
// stop passing through labelable to control children
// to prevent label > button > input
if (tag && categoriesByTag[tag].includes("labelable") === false) {
childrenCategories = [...childrenCategories, "labelable"];
if (tag && elementsByTag[tag].categories.includes("labelable") === false) {
elementChildren = [...elementChildren, "labelable"];
}
}
// introduce custom non-form category to restrict nesting form elements
// like form > div > form
if (tag === "form" || allowedCategories?.includes("non-form")) {
childrenCategories = [...childrenCategories, "non-form"];
elementChildren = [...elementChildren, "non-form"];
}
return childrenCategories;
return elementChildren;
};

/**
Expand Down Expand Up @@ -171,7 +168,7 @@ const computeAllowedCategories = ({
continue;
}
const tag = getTag({ instance, metas, props });
allowedCategories = getTagChildrenCategories(tag, allowedCategories);
allowedCategories = getElementChildren(tag, allowedCategories);
}
return allowedCategories;
};
Expand Down Expand Up @@ -375,7 +372,7 @@ export const isTreeSatisfyingContentModel = ({
}
let isSatisfying = isTagSatisfying && isComponentSatisfying;
const contentModel = getComponentContentModel(metas.get(instance.component));
allowedCategories = getTagChildrenCategories(tag, allowedCategories);
allowedCategories = getElementChildren(tag, allowedCategories);
allowedParentCategories = contentModel.children;
if (contentModel.descendants) {
allowedAncestorCategories ??= [];
Expand Down Expand Up @@ -584,11 +581,11 @@ export const findClosestContainer = ({
}
const tag = getTag({ instance, props, metas });
const meta = metas.get(instance.component);
const childrenCategories = tag ? childrenCategoriesByTag[tag] : undefined;
const childrenComponentCategories = getComponentContentModel(meta).children;
const elementChildren = tag ? elementsByTag[tag].children : undefined;
const componentChildren = getComponentContentModel(meta).children;
if (
childrenComponentCategories.length === 0 ||
(childrenCategories && childrenCategories.length === 0)
componentChildren.length === 0 ||
(elementChildren && elementChildren.length === 0)
) {
continue;
}
Expand Down Expand Up @@ -621,11 +618,11 @@ export const findClosestNonTextualContainer = ({
}
const tag = getTag({ instance, props, metas });
const meta = metas.get(instance.component);
const childrenCategories = tag ? childrenCategoriesByTag[tag] : undefined;
const childrenComponentCategories = getComponentContentModel(meta).children;
const elementChildren = tag ? elementsByTag[tag].children : undefined;
const componentChildren = getComponentContentModel(meta).children;
if (
childrenComponentCategories.length === 0 ||
(childrenCategories && childrenCategories.length === 0)
componentChildren.length === 0 ||
(elementChildren && elementChildren.length === 0)
) {
continue;
}
Expand Down
30 changes: 22 additions & 8 deletions packages/html-data/bin/elements.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,13 @@ import {
const html = await loadHtmlIndices();
const document = parseHtml(html);

const categoriesByTag: Record<string, string[]> = {};
const childrenCategoriesByTag: Record<string, string[]> = {};
type Element = {
description: string;
categories: string[];
children: string[];
};

const elementsByTag: Record<string, Element> = {};

/**
* scrape elements table with content model
Expand All @@ -39,18 +44,27 @@ const childrenCategoriesByTag: Record<string, string[]> = {};
// skip "SVG svg" amd "MathML math"
return !tag.includes(" ");
});
const description = getTextContent(row.childNodes[1]);
const categories = parseList(getTextContent(row.childNodes[2]));
const children = parseList(getTextContent(row.childNodes[4]));
for (const tag of elements) {
categoriesByTag[tag] = categories;
childrenCategoriesByTag[tag] = children.includes("empty") ? [] : children;
elementsByTag[tag] = {
description,
categories,
children: children.includes("empty") ? [] : children,
};
}
}
}

let contentModel = ``;
contentModel += `export const categoriesByTag: Record<string, string[]> = ${JSON.stringify(categoriesByTag, null, 2)};\n`;
contentModel += `export const childrenCategoriesByTag: Record<string, string[]> = ${JSON.stringify(childrenCategoriesByTag, null, 2)};\n`;
const contentModelFile = "./src/__generated__/content-model.ts";
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);
Loading