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
131 changes: 126 additions & 5 deletions bestax-mcp/data/bulma-classes.json

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions bestax-mcp/data/skills.json
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@
{
"id": "bulma-classes-component-map",
"file": "references/bulma-classes/component-map.md",
"bytes": 20298
"bytes": 21772
},
{
"id": "bulma-classes-prop-map",
Expand All @@ -113,7 +113,7 @@
{
"id": "bulma-classes-unmappables",
"file": "references/bulma-classes/unmappables.md",
"bytes": 21787
"bytes": 22365
},
{
"id": "css-migration",
Expand Down
32 changes: 32 additions & 0 deletions bestax-mcp/src/__tests__/bulma-classes-agree.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,38 @@ function planned(tag: string, tokens: string[]): Outcome {
}));
counted = counts.prop;
}
// One that builds its icons from props holds an icon, planned the way
// the codemod plans it first, which the lookup states as a condition.
if (entry?.status === 'mapped' && entry.buildsIcons && !childElements) {
const glyph = {
tag: 'i',
tokens: ['fas', 'fa-home'],
attributes: new Map(),
hasSpread: false,
isEmpty: true,
};
const iconAttributes = new Map([['aria-label', 'x']]);
const becomes = plan({
tag: 'span',
tokens: ['icon'],
attributes: iconAttributes,
hasSpread: false,
hasRef: false,
hasChildren: true,
soleChild: glyph,
}).conversion;
childElements = [
{
tag: 'span',
tokens: ['icon'],
attributes: iconAttributes,
hasSpread: false,
isEmpty: false,
...(becomes && { becomes }),
soleChild: glyph,
},
];
}
}
const result = plan({
tag,
Expand Down
17 changes: 17 additions & 0 deletions bestax-mcp/src/bulma-classes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,11 @@ export interface RootRecord {
* each a bare, empty `<tag>`.
*/
countsChildren: Counts | null;
/**
* The component builds its children from props: each `.icon` inside as an
* icon's props, and a bare `<span>` of text after one as its text.
*/
buildsIcons: boolean;
/** What each item in a list with this class becomes, found by where it sits. */
items: Items | null;
/** The component renders a bare `<tag>` around the element too. */
Expand Down Expand Up @@ -177,6 +182,8 @@ export type Element =
writes?: PropWrite[];
/** Renders the element's children itself, from a count. */
counts?: Counts;
/** Builds its icons from props, out of the element's children. */
buildsIcons?: true;
/** Renders this class only when no other of itself is around it. */
topLevel?: string;
/** What each item inside it becomes. */
Expand Down Expand Up @@ -430,6 +437,7 @@ export function lookupClasses(
absorbs: null,
writes: null,
countsChildren: null,
buildsIcons: false,
items: null,
parent: null,
rendersText: null,
Expand Down Expand Up @@ -628,6 +636,7 @@ export function lookupClasses(
absorbs: entry.absorbs ?? undefined,
writes: entry.writes ?? undefined,
counts: entry.countsChildren ?? undefined,
buildsIcons: entry.buildsIcons || undefined,
topLevel: entry.topLevelOnly && root ? root : undefined,
items: entry.items ?? undefined,
parent: entry.parent?.tag,
Expand Down Expand Up @@ -777,6 +786,14 @@ export function renderLookup(lookup: Lookup): string {
`\`${counts.prop}={N}\` and drop them. The codemod does that ` +
`when they're all the element holds.`
: '') +
(element.buildsIcons
? ` It builds its icons from props: write each \`.icon\` inside as ` +
`one icon's props, its <i>'s classes read as \`library\` and ` +
`\`name\` (\`fas fa-home\` as \`library: 'fa', name: 'home'\`) ` +
`and its \`aria-label\` as \`ariaLabel\`, with the bare <span> of ` +
`text after it as its text. One icon goes in \`iconProps\`, with ` +
`its text as the children, and more than one in \`items\`.`
: '') +
(element.topLevel
? ` It renders \`.${element.topLevel}\` only when no other ` +
`\`${element.target}\` is around it, so one inside another ` +
Expand Down
3 changes: 2 additions & 1 deletion bestax-migrate/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,8 @@ is the one with no library behind it, converting raw Bulma classes on plain JSX.
inside it in, a conversion, a fold into its only child (a `fold` wrapper such as
`.table-container`) or a TODO out; an entry that `absorbs` its only child (`.select`'s
`<select>`) converts together with it, written in the child's place, and one that
`countsChildren` (`.skeleton-lines`) writes their count and drops them, and one with a
`countsChildren` (`.skeleton-lines`) writes their count and drops them, one that `buildsIcons`
(`.icon-text`) takes its `.icon`s and the texts beside them as props, and one with a
`parent` (`.pagination-link`) takes the place of the bare `<li>` its target renders too; an entry
with `wrapsChildren` converts only beside one of its parts, so `transform.ts` plans children
before their parents; and an element with no class to go by, a menu list's `<li>`, is found
Expand Down
77 changes: 72 additions & 5 deletions bestax-migrate/e2e/bulma-classes-props.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,13 @@ import {
WRAPPERS,
type RootEntry,
} from '../src/sources/bulma-classes/class-map.js';
import { plan } from '../src/sources/bulma-classes/plan.js';
import { plan, type ChildFacts } from '../src/sources/bulma-classes/plan.js';
import {
GLYPHS,
iconChild,
iconProps,
textChild,
} from './support/icon-text.js';
import { typecheckTsxFiles } from './support/typecheck-tsx.js';

const VOID = new Set(['input', 'hr', 'img', 'br']);
Expand Down Expand Up @@ -85,7 +91,8 @@ function converted(
tokens: string[],
attributes: Array<[string, string | true]>,
child?: Inner,
conditional?: string[][]
conditional?: string[][],
children?: ChildFacts[]
): string | null {
const unique = [...new Map(attributes)];
// A root that wraps its children converts only beside one of its parts;
Expand Down Expand Up @@ -145,6 +152,11 @@ function converted(
}),
...(root?.status === 'mapped' &&
root.rendersText !== undefined && { text: root.rendersText }),
// One that builds its icons from props holds an icon and its text.
...(root?.status === 'mapped' &&
root.buildsIcons && {
childElements: children ?? [iconChild(['fas', 'fa-home']), textChild()],
}),
// One that renders its children from a count holds a few of them.
...(root?.status === 'mapped' &&
root.countsChildren && {
Expand Down Expand Up @@ -187,15 +199,36 @@ function converted(
...(conversion.className
? [jsxAttr('className', conversion.className)]
: []),
].join(' ');
];
const name = `B.${conversion.target}`;
// One that builds its icons from props is given them as one icon, with
// its text as the children, or as `items`.
const icons = conversion.icons;
if (icons) {
const inside = children ?? [iconChild(['fas', 'fa-home']), textChild()];
const built = icons.map(icon => iconProps(icon, inside[icon.index]));
const text = icons.length === 1 ? icons[0].text : undefined;
attrs.push(
icons.length === 1
? `iconProps={${JSON.stringify(built[0])}}`
: `items={${JSON.stringify(
icons.map((icon, index) => ({
iconProps: built[index],
...(icon.text !== undefined && { text: icon.text }),
}))
)}}`
);
return text === undefined
? `<${name} ${attrs.join(' ')} />`
: `<${name} ${attrs.join(' ')}>${text}</${name}>`;
}
// A target that renders the children itself closes itself, as the
// transform writes it, and so does one in place of a void child.
return VOID.has(tag) ||
conversion.rendersChildren ||
(absorbs && VOID.has(absorbs.tag))
? `<${name} ${attrs} />`
: `<${name} ${attrs}>x</${name}>`;
? `<${name} ${attrs.join(' ')} />`
: `<${name} ${attrs.join(' ')}>x</${name}>`;
}

describe('every bulma-classes conversion typechecks', () => {
Expand Down Expand Up @@ -296,6 +329,40 @@ describe('every bulma-classes conversion typechecks', () => {
}
}
}
// The icons it builds from props: each glyph's props, what a `.icon`'s
// own classes and attributes become, and more than one, as `items`.
if (entry.buildsIcons) {
const icons: ChildFacts[] = [
...GLYPHS.map(glyph => iconChild(glyph)),
...['is-small', 'is-medium', 'is-large', 'has-text-info', 'mt-2'].map(
token => iconChild(['fas', 'fa-home'], [token])
),
...COMMON.map(attribute =>
iconChild(['fas', 'fa-home'], [], {
'aria-label': 'x',
...Object.fromEntries([attribute]),
})
),
];
for (const icon of icons) {
add(converted(entry.tag!, [root], [], undefined, undefined, [icon]));
add(
converted(entry.tag!, [root], [], undefined, undefined, [
icon,
textChild(),
])
);
}
add(
converted(entry.tag!, [root], [], undefined, undefined, [
icons[0],
textChild('a'),
icons[1],
icons[2],
textChild('b'),
])
);
}
expect({ root, conversions: lines.length > 0 }).toEqual({
root,
conversions: true,
Expand Down
Loading
Loading