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
153 changes: 147 additions & 6 deletions bestax-mcp/data/bulma-classes.json

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions bestax-mcp/data/skills.json
Original file line number Diff line number Diff line change
Expand Up @@ -103,17 +103,17 @@
{
"id": "bulma-classes-component-map",
"file": "references/bulma-classes/component-map.md",
"bytes": 21772
"bytes": 23574
},
{
"id": "bulma-classes-prop-map",
"file": "references/bulma-classes/prop-map.md",
"bytes": 31763
"bytes": 32615
},
{
"id": "bulma-classes-unmappables",
"file": "references/bulma-classes/unmappables.md",
"bytes": 22365
"bytes": 23095
},
{
"id": "css-migration",
Expand Down
33 changes: 33 additions & 0 deletions bestax-mcp/src/__tests__/bulma-classes-agree.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ function planned(tag: string, tokens: string[]): Outcome {
let soleChild: ElementFacts['soleChild'];
let soleChildOf: ElementFacts['soleChildOf'];
let text: string | undefined;
let classesAround: string[] | undefined;
let childElements: ElementFacts['childElements'];
let counted: string | undefined;
for (const token of tokens) {
Expand Down Expand Up @@ -125,6 +126,37 @@ function planned(tag: string, tokens: string[]): Outcome {
}));
counted = counts.prop;
}
// One that renders the whole `.file` tree holds it, inside a `.field`,
// which the lookup states as a condition.
if (entry?.status === 'mapped' && entry.buildsFile && !childElements) {
const bare = (tag: string, token: string, extra = {}) => ({
tag,
tokens: [token],
attributes: new Map(),
hasSpread: false,
isEmpty: false,
...extra,
});
childElements = [
bare('label', 'file-label', {
children: [
bare('input', 'file-input', {
attributes: new Map([['type', 'file']]),
isEmpty: true,
}),
bare('span', 'file-cta', {
children: [
bare('span', 'file-label', {
staticContent: true,
text: 'Upload',
}),
],
}),
],
}),
];
classesAround = ['field'];
}
// 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) {
Expand Down Expand Up @@ -170,6 +202,7 @@ function planned(tag: string, tokens: string[]): Outcome {
childElements,
soleChildOf,
text,
classesAround,
});
if (result.conversion) {
const { target, props, className } = result.conversion;
Expand Down
18 changes: 18 additions & 0 deletions bestax-mcp/src/bulma-classes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,11 @@ export interface RootRecord {
* icon's props, and a bare `<span>` of text after one as its text.
*/
buildsIcons: boolean;
/**
* The component renders the whole `.file` tree from props, and a `.field`
* of its own around it outside a `Field`.
*/
buildsFile: 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 @@ -184,6 +189,8 @@ export type Element =
counts?: Counts;
/** Builds its icons from props, out of the element's children. */
buildsIcons?: true;
/** Renders the element's whole tree from props. */
buildsFile?: 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 @@ -438,6 +445,7 @@ export function lookupClasses(
writes: null,
countsChildren: null,
buildsIcons: false,
buildsFile: false,
items: null,
parent: null,
rendersText: null,
Expand Down Expand Up @@ -637,6 +645,7 @@ export function lookupClasses(
writes: entry.writes ?? undefined,
counts: entry.countsChildren ?? undefined,
buildsIcons: entry.buildsIcons || undefined,
buildsFile: entry.buildsFile || undefined,
topLevel: entry.topLevelOnly && root ? root : undefined,
items: entry.items ?? undefined,
parent: entry.parent?.tag,
Expand Down Expand Up @@ -794,6 +803,15 @@ export function renderLookup(lookup: Lookup): string {
`text after it as its text. One icon goes in \`iconProps\`, with ` +
`its text as the children, and more than one in \`items\`.`
: '') +
(element.buildsFile
? ` It renders the whole \`.file\` tree from props: write the ` +
`<input>'s attributes on it, the \`.file-label\` <span>'s ` +
`content as \`buttonLabel\` (left out for its default text), ` +
`each \`.file-icon\`'s content as \`iconLeft\` or ` +
`\`iconRight\`, and the \`.file-name\`'s text as \`fileName\` ` +
`beside \`hasName\`. Outside a \`Field\` it renders a ` +
`\`.field\` of its own around it.`
: '') +
(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 @@ -100,7 +100,8 @@ is the one with no library behind it, converting raw Bulma classes on plain JSX.
`.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, one that `buildsIcons`
(`.icon-text`) takes its `.icon`s and the texts beside them as props, and one with a
(`.icon-text`) takes its `.icon`s and the texts beside them as props, one that `buildsFile`
(`.file`) takes its whole tree as props, inside a `Field` only, 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
79 changes: 79 additions & 0 deletions bestax-migrate/e2e/bulma-classes-props.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
type RootEntry,
} from '../src/sources/bulma-classes/class-map.js';
import { plan, type ChildFacts } from '../src/sources/bulma-classes/plan.js';
import { fileChildren, type FileTree } from './support/file-tree.js';
import {
GLYPHS,
iconChild,
Expand Down Expand Up @@ -157,6 +158,12 @@ function converted(
root.buildsIcons && {
childElements: children ?? [iconChild(['fas', 'fa-home']), textChild()],
}),
// One that renders the whole `.file` tree holds it, inside a `.field`.
...(root?.status === 'mapped' &&
root.buildsFile && {
childElements: children ?? fileChildren(),
classesAround: ['field'],
}),
// One that renders its children from a count holds a few of them.
...(root?.status === 'mapped' &&
root.countsChildren && {
Expand Down Expand Up @@ -222,6 +229,37 @@ function converted(
? `<${name} ${attrs.join(' ')} />`
: `<${name} ${attrs.join(' ')}>${text}</${name}>`;
}
// One that renders the whole `.file` tree is given the <input>'s
// attributes and the parts, as the transform writes them.
const file = conversion.file;
if (file) {
const [label] = children ?? fileChildren();
const [input, cta] = label.children!;
const parts = cta.children!;
const jsx = (child: ChildFacts): string =>
`<${child.tag}${child.tokens ? ` className="${child.tokens.join(' ')}"` : ''} />`;
const content = (part: ChildFacts) =>
part.children ? `{${jsx(part.children[0])}}` : JSON.stringify(part.text);
for (const [attribute, value] of input.attributes) {
if (attribute === 'type') continue;
attrs.push(
conversion.numbers.includes(attribute)
? `${attribute}={${Number(value)}}`
: jsxAttr(attribute, value as string | true)
);
}
if (file.inputClassName !== null) {
attrs.push(jsxAttr('inputClassName', file.inputClassName));
}
for (const key of ['buttonLabel', 'iconLeft', 'iconRight'] as const) {
const index = file[key];
if (index !== undefined) attrs.push(`${key}=${content(parts[index])}`);
}
if (file.fileName !== undefined) {
attrs.push(jsxAttr('fileName', file.fileName));
}
return `<${name} ${attrs.join(' ')} />`;
}
// 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) ||
Expand Down Expand Up @@ -363,6 +401,47 @@ describe('every bulma-classes conversion typechecks', () => {
])
);
}
// The tree it renders from props: the <input>'s attributes it takes as
// its own, its extra classes, the icons, the default text and a name.
if (entry.buildsFile) {
const trees: FileTree[] = [
{ label: 'Choose a file\u2026' },
{ iconLeft: ['fas', 'fa-upload'], iconRight: ['fas', 'fa-check'] },
{ inputTokens: ['my-input'] },
...[
...COMMON,
['name', 'resume'],
['accept', 'image/*'],
['multiple', true],
['required', true],
['disabled', true],
].map(([attribute, value]): FileTree => ({
input: { [attribute as string]: value as string | true },
})),
];
for (const tree of trees) {
add(
converted(
entry.tag!,
[root],
[],
undefined,
undefined,
fileChildren(tree)
)
);
}
add(
converted(
entry.tag!,
[root, 'has-name'],
[],
undefined,
undefined,
fileChildren({ name: 'cv.pdf' })
)
);
}
expect({ root, conversions: lines.length > 0 }).toEqual({
root,
conversions: true,
Expand Down
Loading
Loading