bestax-migrate 2.27.0 → 2.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/sources/_shared/polymorphic.d.ts.map +1 -1
- package/dist/sources/_shared/polymorphic.js +16 -1
- package/dist/sources/bloomer/mapping.d.ts.map +1 -1
- package/dist/sources/bloomer/mapping.js +9 -4
- package/dist/sources/bloomer/specials.d.ts.map +1 -1
- package/dist/sources/bloomer/specials.js +30 -3
- package/dist/sources/bulma-classes/class-map.d.ts +31 -10
- package/dist/sources/bulma-classes/class-map.d.ts.map +1 -1
- package/dist/sources/bulma-classes/class-map.js +48 -13
- package/dist/sources/bulma-classes/glyph.d.ts +20 -0
- package/dist/sources/bulma-classes/glyph.d.ts.map +1 -1
- package/dist/sources/bulma-classes/glyph.js +64 -0
- package/dist/sources/bulma-classes/html-assets.js +1 -1
- package/dist/sources/bulma-classes/plan.d.ts +40 -1
- package/dist/sources/bulma-classes/plan.d.ts.map +1 -1
- package/dist/sources/bulma-classes/plan.js +160 -24
- package/dist/sources/bulma-classes/transform.d.ts +2 -1
- package/dist/sources/bulma-classes/transform.d.ts.map +1 -1
- package/dist/sources/bulma-classes/transform.js +231 -33
- package/dist/sources/rbx/mapping.d.ts +10 -0
- package/dist/sources/rbx/mapping.d.ts.map +1 -1
- package/dist/sources/rbx/mapping.js +12 -6
- package/dist/sources/react-bulma-components/mapping.d.ts.map +1 -1
- package/dist/sources/react-bulma-components/mapping.js +18 -4
- package/package.json +2 -2
|
@@ -7,10 +7,14 @@
|
|
|
7
7
|
* renders exactly the markup the element did. Anything that would change the
|
|
8
8
|
* markup is either left in `className` (a class with no prop here) or refuses
|
|
9
9
|
* the whole element with a TODO (an attribute or tag the component would
|
|
10
|
-
* render differently).
|
|
10
|
+
* render differently). One exception, on purpose: Font Awesome's own
|
|
11
|
+
* `<i aria-hidden="true">` alone in a `.icon` that carries nothing assistive
|
|
12
|
+
* tech reads (the table's `hiddenMovesBeside`) converts to `Icon`, which writes that attribute
|
|
13
|
+
* on the `.icon` instead (`movesHidden`), since the glyph is hidden either
|
|
14
|
+
* way.
|
|
11
15
|
*/
|
|
12
16
|
import { FORWARDS_REF, HELPER_PROPS, HELPER_TOKENS, inTagSet, legacyHint, modifierFor, numberAttrsOf, placedFor, PRECEDENCE, rootFor, WRAPPER_OWN_PROPS, wrapperFor, } from './class-map.js';
|
|
13
|
-
import { readGlyph } from './glyph.js';
|
|
17
|
+
import { glyphProps, readDrawnGlyph, readGlyph, readHiddenGlyph, } from './glyph.js';
|
|
14
18
|
import { ruleId } from './rules.js';
|
|
15
19
|
/** The button text `File` renders when it's given none. */
|
|
16
20
|
const DEFAULT_FILE_LABEL = 'Choose a file\u2026';
|
|
@@ -227,7 +231,19 @@ export function plan(facts) {
|
|
|
227
231
|
: naming === 'unsure'
|
|
228
232
|
? { ...unnamed, ...entry.namedDefaults.defaults }
|
|
229
233
|
: unnamed;
|
|
230
|
-
|
|
234
|
+
// Font Awesome's own markup hides the <i> rather than the element around
|
|
235
|
+
// it, and a target that renders the glyph from props writes the same
|
|
236
|
+
// `aria-hidden` on the element in its place: it moves up, and the glyph is
|
|
237
|
+
// hidden either way. The one difference in markup a conversion makes, on
|
|
238
|
+
// purpose; the render tests compare it with the attribute moved up. Only
|
|
239
|
+
// onto an element that carries nothing assistive tech reads, by name and
|
|
240
|
+
// whatever the value: an expression may render as anything.
|
|
241
|
+
const moved = entry.readsGlyph &&
|
|
242
|
+
defaults['aria-hidden'] === 'true' &&
|
|
243
|
+
[...attributes.keys()].every(name => movesBeside(entry, name))
|
|
244
|
+
? readHiddenGlyph(facts.soleChild)
|
|
245
|
+
: undefined;
|
|
246
|
+
const missing = [...Object.entries(defaults), ...written].filter(([name]) => !attributes.has(name) && !(moved && name === 'aria-hidden'));
|
|
231
247
|
const drop = [];
|
|
232
248
|
for (const name of entry.untypedAttrs ?? []) {
|
|
233
249
|
if (!attributes.has(name))
|
|
@@ -316,7 +332,10 @@ export function plan(facts) {
|
|
|
316
332
|
}
|
|
317
333
|
if (missing.length > 0) {
|
|
318
334
|
const list = missing.map(([name, value]) => `\`${name}="${value}"\``);
|
|
319
|
-
|
|
335
|
+
const usual = `bestax \`${target}\` renders ${list.join(' and ')} when the element does not set ${missing.length === 1 ? 'it' : 'them'}; add ${missing.length === 1 ? 'it' : 'them'} here if that is what you want, then re-run`;
|
|
336
|
+
return refuse('defaults', target, entry.readsGlyph && naming === 'unnamed'
|
|
337
|
+
? hiddenAdvice(facts, entry, usual)
|
|
338
|
+
: usual);
|
|
320
339
|
}
|
|
321
340
|
for (const [name, rule] of Object.entries(entry.writesAttr ?? {})) {
|
|
322
341
|
const value = attributes.get(name);
|
|
@@ -326,8 +345,14 @@ export function plan(facts) {
|
|
|
326
345
|
return refuse('attr', name, `bestax \`${target}\` keeps a <${tag}>'s \`${name}\` only as ${orList(rule.keeps)}, and writes \`${name}="${rule.fallback}"\` in place of anything else, so this one converts only ${rule.implicit ? `with no \`${name}\` or as one of those` : 'as one of those, written out'}; keep this element as markup`);
|
|
327
346
|
}
|
|
328
347
|
}
|
|
348
|
+
// Only on a tag the target renders: on any other, the tag check below is
|
|
349
|
+
// the refusal, and a `drops` one would call an attribute inert that may not
|
|
350
|
+
// be (`type` on a <button>) and promise a conversion a re-run still refuses.
|
|
351
|
+
const tagReached = tag === entry.tag ||
|
|
352
|
+
entry.as === 'any' ||
|
|
353
|
+
(entry.as?.includes(tag) ?? false);
|
|
329
354
|
for (const [name, tags] of Object.entries(entry.dropsAttr ?? {})) {
|
|
330
|
-
if (attributes.has(name) && inTagSet(tags, tag)) {
|
|
355
|
+
if (tagReached && attributes.has(name) && inTagSet(tags, tag)) {
|
|
331
356
|
// Bulma greys out `.button[disabled]` on any tag, so there it is not
|
|
332
357
|
// inert: dropping it changes how the element looks.
|
|
333
358
|
const visible = name === 'disabled' && target === 'Button';
|
|
@@ -353,9 +378,6 @@ export function plan(facts) {
|
|
|
353
378
|
if (renders !== tag) {
|
|
354
379
|
const reachable = entry.as === 'any' || (entry.as?.includes(tag) ?? false);
|
|
355
380
|
if (!reachable) {
|
|
356
|
-
if (entry.otherTagsStay?.tags.includes(tag)) {
|
|
357
|
-
return { conversion: null, todos };
|
|
358
|
-
}
|
|
359
381
|
return refuse('tag', target, `bestax \`${target}\` renders ${tagsItReaches(entry)}, not a <${tag}>; keep the markup, or change the tag and re-run`);
|
|
360
382
|
}
|
|
361
383
|
as = tag;
|
|
@@ -441,6 +463,8 @@ export function plan(facts) {
|
|
|
441
463
|
if (entry.classNameReplaces && (rest.length > 0 || conditionalStays)) {
|
|
442
464
|
return refuse('attr', 'className', `bestax \`${target}\` writes a \`className\` it's given in place of \`.${root}\`, so this converts only with no other class; keep it as markup`);
|
|
443
465
|
}
|
|
466
|
+
// A glyph the target renders from props takes the place of the only child.
|
|
467
|
+
const glyph = moved ?? (entry.readsGlyph ? readGlyph(facts.soleChild) : undefined);
|
|
444
468
|
return {
|
|
445
469
|
conversion: {
|
|
446
470
|
target,
|
|
@@ -449,9 +473,11 @@ export function plan(facts) {
|
|
|
449
473
|
drop,
|
|
450
474
|
numbers,
|
|
451
475
|
...(absorbed ? { absorbs: absorbed } : {}),
|
|
452
|
-
...(counts || entry.rendersText !== undefined
|
|
476
|
+
...(counts || entry.rendersText !== undefined || glyph
|
|
453
477
|
? { rendersChildren: true }
|
|
454
478
|
: {}),
|
|
479
|
+
...(glyph ? { glyph } : {}),
|
|
480
|
+
...(moved ? { movesHidden: true } : {}),
|
|
455
481
|
...(parent ? { replacesParent: true } : {}),
|
|
456
482
|
...(conditional.length > 0 ? { conditional } : {}),
|
|
457
483
|
...(icons ? { icons } : {}),
|
|
@@ -555,6 +581,128 @@ function namingOf(entry, attributes) {
|
|
|
555
581
|
}
|
|
556
582
|
return given.includes(null) ? 'unsure' : 'unnamed';
|
|
557
583
|
}
|
|
584
|
+
/**
|
|
585
|
+
* Whether an element may carry `name` and still take Font Awesome's
|
|
586
|
+
* `aria-hidden` up off its <i>, by the table's `hiddenMovesBeside`, where a
|
|
587
|
+
* name ending in `*` stands for every attribute it starts.
|
|
588
|
+
*/
|
|
589
|
+
function movesBeside(entry, name) {
|
|
590
|
+
return (entry.readsGlyph?.hiddenMovesBeside ?? []).some(allowed => allowed.endsWith('*')
|
|
591
|
+
? name.startsWith(allowed.slice(0, -1))
|
|
592
|
+
: name === allowed);
|
|
593
|
+
}
|
|
594
|
+
/**
|
|
595
|
+
* What to do about an unnamed element whose target hides it with an
|
|
596
|
+
* `aria-hidden` it doesn't carry, and that renders a glyph from props
|
|
597
|
+
* (`Icon`), for its `defaults` TODO: from what it holds (Font Awesome's own
|
|
598
|
+
* `<i aria-hidden>`, the `<svg>` its script draws) and where it sits (a
|
|
599
|
+
* link or button its label names, or one nothing names). Each says what a
|
|
600
|
+
* re-run converts it to once the attribute is in place, when that's sure,
|
|
601
|
+
* and inside a `.control` with icons, the `Control` prop that renders it.
|
|
602
|
+
* `usual` is the message for anything else.
|
|
603
|
+
*/
|
|
604
|
+
function hiddenAdvice(facts, entry, usual) {
|
|
605
|
+
const target = entry.target;
|
|
606
|
+
const sole = facts.soleChild;
|
|
607
|
+
// Font Awesome's own markup hides the <i>, not the `.icon` around it.
|
|
608
|
+
const flag = sole?.attributes.get('aria-hidden');
|
|
609
|
+
const hiddenFont = sole?.tag === 'i' && (flag === 'true' || flag === true);
|
|
610
|
+
const drawn = readDrawnGlyph(sole);
|
|
611
|
+
// The element as a re-run would find it, the attribute in place (moved up
|
|
612
|
+
// off the <i> that carries it): the planner says what that converts to.
|
|
613
|
+
const attributes = new Map(facts.attributes).set('aria-hidden', 'true');
|
|
614
|
+
const retry = plan({
|
|
615
|
+
...facts,
|
|
616
|
+
attributes,
|
|
617
|
+
...(hiddenFont && {
|
|
618
|
+
soleChild: {
|
|
619
|
+
...sole,
|
|
620
|
+
attributes: new Map([...sole.attributes].filter(([name]) => name !== 'aria-hidden')),
|
|
621
|
+
},
|
|
622
|
+
}),
|
|
623
|
+
}).conversion;
|
|
624
|
+
// Only where nothing in it converts on its own, which would change what a
|
|
625
|
+
// re-run finds: an <i> the glyph takes the place of, or an <svg>.
|
|
626
|
+
const outcome = retry?.glyph
|
|
627
|
+
? `, and this converts to \`${target}\` with ${jsxProps(glyphProps(retry.glyph))}`
|
|
628
|
+
: retry && sole?.tag === 'svg'
|
|
629
|
+
? `, and this converts to \`${target}\` around the <svg> as it is`
|
|
630
|
+
: '';
|
|
631
|
+
const hint = controlHint(facts, retry, retry?.glyph ?? drawn);
|
|
632
|
+
// A bare <i> naming a glyph moves it up by itself, onto an element that
|
|
633
|
+
// carries nothing else assistive tech reads, so this one carries more.
|
|
634
|
+
const exposed = readHiddenGlyph(sole)
|
|
635
|
+
? [...facts.attributes.keys()].find(name => !movesBeside(entry, name))
|
|
636
|
+
: undefined;
|
|
637
|
+
if (exposed) {
|
|
638
|
+
return `bestax \`${target}\` takes Font Awesome's \`aria-hidden\` up off a bare <i> like this one only onto a \`.icon\` that carries nothing assistive tech reads, and this one carries \`${exposed}\`, which the move would hide, or, beside a \`role\` or \`tabIndex\`, leave with no \`aria-hidden\` at all; if the icon means something, give the \`.icon\` an \`aria-label\` and \`role="img"\`, and if it is decoration, drop \`${exposed}\`, then re-run`;
|
|
639
|
+
}
|
|
640
|
+
if (hiddenFont) {
|
|
641
|
+
// A bare <i> naming a glyph moves it up by itself, so this one isn't.
|
|
642
|
+
return `bestax \`${target}\` writes \`aria-hidden="true"\` on the \`.icon\`, and takes it from the <i> inside only when that <i> is empty, carries nothing else and names a glyph it renders from props, which this one doesn't; the \`.icon\` holds nothing else, so the attribute hides the same glyph there: move it up onto the \`.icon\`, then re-run${outcome}${hint}`;
|
|
643
|
+
}
|
|
644
|
+
if (drawn) {
|
|
645
|
+
return `this \`.icon\` holds the <svg> Font Awesome's script draws in place of an <i>, and bestax \`${target}\` writes \`aria-hidden="true"\` on the \`.icon\`, which this one doesn't carry; add it, then re-run${outcome}; or write \`${target}\` with ${jsxProps(glyphProps(drawn))} in its place, which renders the <i> the script draws from, and shows only where Font Awesome's stylesheet or script is loaded${hint}`;
|
|
646
|
+
}
|
|
647
|
+
const around = facts.clickableAround;
|
|
648
|
+
const label = around?.label;
|
|
649
|
+
const tag = around && `${/^[aeiou]/.test(around.tag) ? 'an' : 'a'} <${around.tag}>`;
|
|
650
|
+
if (label && label !== 'none') {
|
|
651
|
+
const by = label === 'text' ? 'its own text' : `its \`${label}\``;
|
|
652
|
+
return `this \`.icon\` sits in ${tag} that ${by} names, so the icon is decoration there, and bestax \`${target}\` writes \`aria-hidden="true"\` on an icon with no name of its own, which this one doesn't carry; add it, then re-run${outcome}${hint}`;
|
|
653
|
+
}
|
|
654
|
+
if (label === 'none' && readGlyph(sole)) {
|
|
655
|
+
return `nothing names the <${around.tag}> around this \`.icon\`: it has no \`aria-label\` and no text outside its icons, and a font icon's glyph is no name a screen reader can read; give the <${around.tag}> an \`aria-label\` that says what it does, then add \`aria-hidden="true"\` here, which bestax \`${target}\` writes on an icon with no name of its own, and re-run${outcome}${hint}`;
|
|
656
|
+
}
|
|
657
|
+
return `${usual}${outcome}${hint}`;
|
|
658
|
+
}
|
|
659
|
+
/**
|
|
660
|
+
* Inside a `.control` with icons on this element's side, the `Control` prop
|
|
661
|
+
* that renders the element once it's hidden as `converted` is: given the
|
|
662
|
+
* glyph, the props its classes write and nothing else, since `Control`
|
|
663
|
+
* writes the side's own class over any `className` it's given. Empty when
|
|
664
|
+
* the element carries anything more: another class, an attribute of its own
|
|
665
|
+
* or a class a condition adds.
|
|
666
|
+
*/
|
|
667
|
+
function controlHint(facts, converted, glyph) {
|
|
668
|
+
const side = converted?.className === 'is-left'
|
|
669
|
+
? 'Left'
|
|
670
|
+
: converted?.className === 'is-right'
|
|
671
|
+
? 'Right'
|
|
672
|
+
: undefined;
|
|
673
|
+
const has = `has-icons-${side?.toLowerCase()}`;
|
|
674
|
+
if (!glyph ||
|
|
675
|
+
!side ||
|
|
676
|
+
facts.conditional?.length ||
|
|
677
|
+
!facts.parentTokens?.includes('control') ||
|
|
678
|
+
!facts.parentTokens.includes(has) ||
|
|
679
|
+
facts.attributes.size > 0) {
|
|
680
|
+
return '';
|
|
681
|
+
}
|
|
682
|
+
const object = [...glyphProps(glyph), ...converted.props]
|
|
683
|
+
.map(([name, value]) => `${name}: ${value === true
|
|
684
|
+
? 'true'
|
|
685
|
+
: converted.numbers.includes(name)
|
|
686
|
+
? String(Number(value))
|
|
687
|
+
: valueText(value)}`)
|
|
688
|
+
.join(', ');
|
|
689
|
+
return `; or, since the \`.control\` around it has \`${has}\`, drop this \`.icon\` and give that \`Control\` \`icon${side}={{ ${object} }}\`, which it renders after its children as a \`.icon.is-${side.toLowerCase()}\` around the glyph's <i>, hidden the way \`Icon\` hides it`;
|
|
690
|
+
}
|
|
691
|
+
/** A glyph's props as JSX attributes, for a message. */
|
|
692
|
+
function jsxProps(props) {
|
|
693
|
+
const text = props
|
|
694
|
+
.map(([name, value]) => typeof value === 'string'
|
|
695
|
+
? `${name}="${value}"`
|
|
696
|
+
: `${name}={${valueText(value)}}`)
|
|
697
|
+
.join(' ');
|
|
698
|
+
return `\`${text}\``;
|
|
699
|
+
}
|
|
700
|
+
/** A prop's value as code, for a message: a list as an array of strings. */
|
|
701
|
+
function valueText(value) {
|
|
702
|
+
return typeof value === 'string'
|
|
703
|
+
? JSON.stringify(value)
|
|
704
|
+
: `[${value.map(item => JSON.stringify(item)).join(', ')}]`;
|
|
705
|
+
}
|
|
558
706
|
/**
|
|
559
707
|
* The icons a target that `buildsIcons` builds from an element's children:
|
|
560
708
|
* each `.icon` that converts to `Icon` on its own and holds a glyph `Icon`
|
|
@@ -583,25 +731,13 @@ function buildIcons(children, target) {
|
|
|
583
731
|
const loose = [...child.attributes.keys()].some(name => name === 'key' ||
|
|
584
732
|
name === 'ref' ||
|
|
585
733
|
(name.includes('-') && !name.startsWith('aria-')));
|
|
586
|
-
|
|
734
|
+
// The `.icon` read its glyph itself when it converted on its own.
|
|
735
|
+
const glyph = icon.glyph;
|
|
587
736
|
if (!glyph || loose)
|
|
588
737
|
return { why: shape };
|
|
589
|
-
const props = [
|
|
590
|
-
['library', glyph.library],
|
|
591
|
-
['name', glyph.name],
|
|
592
|
-
];
|
|
593
|
-
if (glyph.variant)
|
|
594
|
-
props.push(['variant', glyph.variant]);
|
|
595
|
-
const { features } = glyph;
|
|
596
|
-
if (features) {
|
|
597
|
-
props.push([
|
|
598
|
-
'features',
|
|
599
|
-
features.length === 1 ? features[0] : features,
|
|
600
|
-
]);
|
|
601
|
-
}
|
|
602
738
|
icons.push({
|
|
603
739
|
index,
|
|
604
|
-
props: [...
|
|
740
|
+
props: [...glyphProps(glyph), ...icon.props],
|
|
605
741
|
className: icon.className,
|
|
606
742
|
drop: icon.drop,
|
|
607
743
|
numbers: icon.numbers,
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
* Unlike the library sources there is no import to anchor on: a file is in
|
|
5
5
|
* scope when a lowercase JSX element carries a `className` with a Bulma class
|
|
6
6
|
* `class-map.ts` knows. Each such element goes through `plan()`, which
|
|
7
|
-
* converts it only when the component renders exactly the same markup
|
|
7
|
+
* converts it only when the component renders exactly the same markup, but
|
|
8
|
+
* for the one exception it states (Font Awesome's hidden `<i>`).
|
|
8
9
|
*
|
|
9
10
|
* Passes, in order:
|
|
10
11
|
* 1. the shared stylesheet-import pass (a no-op under the default `keep`)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"transform.d.ts","sourceRoot":"","sources":["../../../src/sources/bulma-classes/transform.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"transform.d.ts","sourceRoot":"","sources":["../../../src/sources/bulma-classes/transform.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,KAAK,EAAE,GAAG,EAAW,QAAQ,EAAE,MAAM,aAAa,CAAC;AAE1D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAkiCvD,MAAM,CAAC,OAAO,UAAU,SAAS,CAC/B,QAAQ,EAAE,QAAQ,EAClB,GAAG,EAAE,GAAG,EACR,OAAO,GAAE,gBAAqB,GAC7B,MAAM,GAAG,SAAS,CAs9BpB"}
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
* Unlike the library sources there is no import to anchor on: a file is in
|
|
5
5
|
* scope when a lowercase JSX element carries a `className` with a Bulma class
|
|
6
6
|
* `class-map.ts` knows. Each such element goes through `plan()`, which
|
|
7
|
-
* converts it only when the component renders exactly the same markup
|
|
7
|
+
* converts it only when the component renders exactly the same markup, but
|
|
8
|
+
* for the one exception it states (Font Awesome's hidden `<i>`).
|
|
8
9
|
*
|
|
9
10
|
* Passes, in order:
|
|
10
11
|
* 1. the shared stylesheet-import pass (a no-op under the default `keep`)
|
|
@@ -21,9 +22,10 @@ import { addTodo, findAttr, forgetJsxParens, jsxNameParts, literalValueOf, makeA
|
|
|
21
22
|
import { collectBoundNames, makeReserve, prefersTabs, resolvesToBinding, } from '../_shared/imports.js';
|
|
22
23
|
import { rewriteStylesheetImports } from '../_shared/css-imports.js';
|
|
23
24
|
import { BESTAX, buildBestaxImport, placeBestaxImport, seedBestaxImport, } from '../_shared/bestax-import.js';
|
|
25
|
+
import { glyphProps } from './glyph.js';
|
|
24
26
|
import { fileContextTodo, plan, } from './plan.js';
|
|
25
27
|
import { REACT_RUNTIMES, } from './project.js';
|
|
26
|
-
import { PLACED, placedFor, ROOTS, rootFor } from './class-map.js';
|
|
28
|
+
import { PLACED, placedFor, ROOTS, rootFor, WRAPPERS, } from './class-map.js';
|
|
27
29
|
import { ruleId } from './rules.js';
|
|
28
30
|
/**
|
|
29
31
|
* A part imported under its flat export (`MenuList`, `FieldLabel`) is the
|
|
@@ -103,13 +105,25 @@ function spliceClassName(element, replacement, carried = []) {
|
|
|
103
105
|
}
|
|
104
106
|
attrs.splice(attrs.indexOf(classAttr), 1, ...replacement);
|
|
105
107
|
}
|
|
106
|
-
/**
|
|
107
|
-
|
|
108
|
-
|
|
108
|
+
/**
|
|
109
|
+
* Put `lead`, then `props`, then what stays in `className`, where
|
|
110
|
+
* `className` was.
|
|
111
|
+
*/
|
|
112
|
+
function writeClassName(j, element, props, className, lead = []) {
|
|
113
|
+
const replacement = [
|
|
114
|
+
...lead,
|
|
115
|
+
...props.map(([name, value]) => makeAttr(j, name, value === true ? undefined : value)),
|
|
116
|
+
];
|
|
109
117
|
if (className)
|
|
110
118
|
replacement.push(makeAttr(j, 'className', className));
|
|
111
119
|
spliceClassName(element, replacement);
|
|
112
120
|
}
|
|
121
|
+
/** A glyph's props as JSX attributes, a list of features as an array. */
|
|
122
|
+
function glyphAttributes(j, glyph) {
|
|
123
|
+
return glyphProps(glyph).map(([name, value]) => typeof value === 'string'
|
|
124
|
+
? makeAttr(j, name, value)
|
|
125
|
+
: j.jsxAttribute(j.jsxIdentifier(name), j.jsxExpressionContainer(propValue(j, value, false))));
|
|
126
|
+
}
|
|
113
127
|
/**
|
|
114
128
|
* The same, for a `className` a joiner builds: the classes a prop now
|
|
115
129
|
* renders leave the call, each condition that became a prop moves onto it,
|
|
@@ -117,7 +131,7 @@ function writeClassName(j, element, props, className) {
|
|
|
117
131
|
* conditional, no `className` when nothing is left). Returns whether the
|
|
118
132
|
* call is gone.
|
|
119
133
|
*/
|
|
120
|
-
function writeJoined(j, element, conversion, joined) {
|
|
134
|
+
function writeJoined(j, element, conversion, joined, lead = []) {
|
|
121
135
|
const { call, statics, parts } = joined;
|
|
122
136
|
const kept = new Set(conversion.className?.split(' ') ?? []);
|
|
123
137
|
const carried = [];
|
|
@@ -164,6 +178,7 @@ function writeJoined(j, element, conversion, joined) {
|
|
|
164
178
|
classes.length > 0 ? [makeAttr(j, 'className', classes.join(' '))] : [];
|
|
165
179
|
}
|
|
166
180
|
spliceClassName(element, [
|
|
181
|
+
...lead,
|
|
167
182
|
...conversion.props.map(([name, value]) => makeAttr(j, name, value === true ? undefined : value)),
|
|
168
183
|
...conditions,
|
|
169
184
|
...rest,
|
|
@@ -734,6 +749,83 @@ function staticContent(element) {
|
|
|
734
749
|
child.expression.type === 'StringLiteral' &&
|
|
735
750
|
child.expression.value !== '')));
|
|
736
751
|
}
|
|
752
|
+
/** The static classes of a plain HTML element, or undefined. */
|
|
753
|
+
function htmlTokens(element) {
|
|
754
|
+
const name = element?.openingElement?.name;
|
|
755
|
+
if (name?.type !== 'JSXIdentifier' || !INTRINSIC.test(name.name)) {
|
|
756
|
+
return undefined;
|
|
757
|
+
}
|
|
758
|
+
const classAttr = findAttr(element, 'className');
|
|
759
|
+
const className = classAttr ? staticClassName(classAttr) : '';
|
|
760
|
+
return className === null
|
|
761
|
+
? undefined
|
|
762
|
+
: className.split(/\s+/).filter(Boolean);
|
|
763
|
+
}
|
|
764
|
+
/** Whether a component builds content from props rather than children. */
|
|
765
|
+
function buildsContent(entry) {
|
|
766
|
+
return (!!entry &&
|
|
767
|
+
(!!entry.buildsIcons ||
|
|
768
|
+
!!entry.buildsFile ||
|
|
769
|
+
entry.rendersText !== undefined ||
|
|
770
|
+
!!entry.countsChildren ||
|
|
771
|
+
!!entry.absorbs));
|
|
772
|
+
}
|
|
773
|
+
/**
|
|
774
|
+
* What names a link or button, as `ElementFacts.clickableAround` says, with
|
|
775
|
+
* `self` left out of its content: its own `aria-labelledby` or `aria-label`,
|
|
776
|
+
* written out and not empty, else static text in it that is not inside an
|
|
777
|
+
* icon or hidden, else nothing. Undefined when it can't be told: a name or
|
|
778
|
+
* content written as an expression, a `title`, or something inside that may
|
|
779
|
+
* name it some other way (`kindOf` says which, or an `alt`, an `<svg>` or a
|
|
780
|
+
* label of its own).
|
|
781
|
+
*/
|
|
782
|
+
function labelOf(clickable, self, kindOf) {
|
|
783
|
+
if (findAttr(clickable, 'title'))
|
|
784
|
+
return undefined;
|
|
785
|
+
for (const name of ['aria-labelledby', 'aria-label']) {
|
|
786
|
+
const attr = findAttr(clickable, name);
|
|
787
|
+
if (!attr)
|
|
788
|
+
continue;
|
|
789
|
+
const value = attributeValue(attr);
|
|
790
|
+
if (value === null)
|
|
791
|
+
return undefined;
|
|
792
|
+
if (typeof value === 'string' && value.trim() !== '')
|
|
793
|
+
return name;
|
|
794
|
+
}
|
|
795
|
+
let unsure = false;
|
|
796
|
+
const named = (node) => (node.children ?? []).some((child) => {
|
|
797
|
+
if (child === self || dropped(child))
|
|
798
|
+
return false;
|
|
799
|
+
if (child.type === 'JSXText') {
|
|
800
|
+
return /\S/.test(jsxTextRenders(child.value));
|
|
801
|
+
}
|
|
802
|
+
if (child.type === 'JSXFragment')
|
|
803
|
+
return named(child);
|
|
804
|
+
if (child.type !== 'JSXElement') {
|
|
805
|
+
if (isContent(child))
|
|
806
|
+
unsure = true;
|
|
807
|
+
return false;
|
|
808
|
+
}
|
|
809
|
+
const kind = kindOf(child);
|
|
810
|
+
if (kind === 'icon')
|
|
811
|
+
return false;
|
|
812
|
+
if (kind === 'opaque') {
|
|
813
|
+
unsure = true;
|
|
814
|
+
return false;
|
|
815
|
+
}
|
|
816
|
+
const hidden = findAttr(child, 'aria-hidden');
|
|
817
|
+
if (hidden && attributeValue(hidden) === 'true')
|
|
818
|
+
return false;
|
|
819
|
+
if (['alt', 'title', 'aria-label', 'aria-labelledby'].some(name => findAttr(child, name)) ||
|
|
820
|
+
child.openingElement.name.name === 'svg') {
|
|
821
|
+
unsure = true;
|
|
822
|
+
}
|
|
823
|
+
return named(child);
|
|
824
|
+
});
|
|
825
|
+
if (named(clickable))
|
|
826
|
+
return 'text';
|
|
827
|
+
return unsure ? undefined : 'none';
|
|
828
|
+
}
|
|
737
829
|
/**
|
|
738
830
|
* An element's content as one prop value: its one element, or its text, or
|
|
739
831
|
* a fragment of them all, each text written as the string it renders.
|
|
@@ -1012,6 +1104,121 @@ export default function transform(fileInfo, api, options = {}) {
|
|
|
1012
1104
|
readings.set(child, reading);
|
|
1013
1105
|
return reading;
|
|
1014
1106
|
};
|
|
1107
|
+
// A bestax component already in the file is read as the markup the table
|
|
1108
|
+
// says it renders, so a TODO that reads what is around an element says the
|
|
1109
|
+
// same once the elements around it have converted.
|
|
1110
|
+
/** The table's entry for a bestax component, with its root class. */
|
|
1111
|
+
const entryFor = (target) => {
|
|
1112
|
+
const root = target &&
|
|
1113
|
+
Object.keys(ROOTS).find(key => ROOTS[key].status === 'mapped' && ROOTS[key].target === target);
|
|
1114
|
+
return root ? { root, entry: ROOTS[root] } : undefined;
|
|
1115
|
+
};
|
|
1116
|
+
/**
|
|
1117
|
+
* The classes an element renders, as far as the file shows: a plain HTML
|
|
1118
|
+
* element's static ones, or a bestax component's root class and the flags
|
|
1119
|
+
* written out as `true` (`<Control hasIconsLeft>` as `control
|
|
1120
|
+
* has-icons-left`).
|
|
1121
|
+
*/
|
|
1122
|
+
const renderedTokens = (path) => {
|
|
1123
|
+
const html = htmlTokens(path.node);
|
|
1124
|
+
if (html)
|
|
1125
|
+
return html;
|
|
1126
|
+
const found = entryFor(bestaxTarget(path.node, path));
|
|
1127
|
+
if (!found)
|
|
1128
|
+
return undefined;
|
|
1129
|
+
const flags = Object.entries(found.entry.modifiers ?? {})
|
|
1130
|
+
.filter(([, modifier]) => {
|
|
1131
|
+
const [write, ...more] = modifier.writes;
|
|
1132
|
+
const attr = write && more.length === 0 && write.value === undefined
|
|
1133
|
+
? findAttr(path.node, write.prop)
|
|
1134
|
+
: undefined;
|
|
1135
|
+
return attr && attributeValue(attr) === true;
|
|
1136
|
+
})
|
|
1137
|
+
.map(([token]) => token);
|
|
1138
|
+
return [found.root, ...flags];
|
|
1139
|
+
};
|
|
1140
|
+
/**
|
|
1141
|
+
* The tag an element renders: its own, or a bestax component's, from a
|
|
1142
|
+
* static `as` or the table; an empty string for a bestax component whose
|
|
1143
|
+
* tag the table doesn't give, and undefined for any other component.
|
|
1144
|
+
*/
|
|
1145
|
+
const renderedTag = (path) => {
|
|
1146
|
+
const name = path.node.openingElement.name;
|
|
1147
|
+
if (name.type === 'JSXIdentifier' && INTRINSIC.test(name.name)) {
|
|
1148
|
+
return name.name;
|
|
1149
|
+
}
|
|
1150
|
+
const target = bestaxTarget(path.node, path);
|
|
1151
|
+
if (!target)
|
|
1152
|
+
return undefined;
|
|
1153
|
+
const as = findAttr(path.node, 'as');
|
|
1154
|
+
if (as) {
|
|
1155
|
+
const value = attributeValue(as);
|
|
1156
|
+
return typeof value === 'string' ? value : undefined;
|
|
1157
|
+
}
|
|
1158
|
+
return (entryFor(target)?.entry.tag ??
|
|
1159
|
+
Object.keys(WRAPPERS).find(tag => WRAPPERS[tag] === target) ??
|
|
1160
|
+
'');
|
|
1161
|
+
};
|
|
1162
|
+
/**
|
|
1163
|
+
* Where an element whose target renders a glyph sits, for its TODO: the
|
|
1164
|
+
* classes the element directly around it renders, and the nearest <a> or
|
|
1165
|
+
* <button> around it, reached through markup and bestax components only,
|
|
1166
|
+
* since any other component between could render anything, with what
|
|
1167
|
+
* names that.
|
|
1168
|
+
*/
|
|
1169
|
+
const glyphPlace = (elementPath) => {
|
|
1170
|
+
const parent = elementPath.parent;
|
|
1171
|
+
const parentTokens = parent?.node?.type === 'JSXElement' ? renderedTokens(parent) : undefined;
|
|
1172
|
+
let clickable;
|
|
1173
|
+
let tag;
|
|
1174
|
+
for (let up = elementPath.parent; up && !clickable; up = up.parent) {
|
|
1175
|
+
if (up.node?.type !== 'JSXElement')
|
|
1176
|
+
continue;
|
|
1177
|
+
tag = renderedTag(up);
|
|
1178
|
+
if (tag === undefined)
|
|
1179
|
+
break;
|
|
1180
|
+
if (tag === 'a' || tag === 'button')
|
|
1181
|
+
clickable = up;
|
|
1182
|
+
}
|
|
1183
|
+
const kindOf = (element) => {
|
|
1184
|
+
const tokens = htmlTokens(element);
|
|
1185
|
+
const entries = tokens
|
|
1186
|
+
? tokens.map(token => rootFor(token)).filter(Boolean)
|
|
1187
|
+
: [entryFor(bestaxTarget(element, clickable))?.entry];
|
|
1188
|
+
if (!tokens && !bestaxTarget(element, clickable))
|
|
1189
|
+
return 'opaque';
|
|
1190
|
+
if (entries.some(entry => entry?.readsGlyph))
|
|
1191
|
+
return 'icon';
|
|
1192
|
+
return entries.some(buildsContent) ? 'opaque' : 'open';
|
|
1193
|
+
};
|
|
1194
|
+
const label = clickable && labelOf(clickable.node, elementPath.node, kindOf);
|
|
1195
|
+
return {
|
|
1196
|
+
...(parentTokens && { parentTokens }),
|
|
1197
|
+
...(clickable && {
|
|
1198
|
+
clickableAround: { tag: tag, ...(label && { label }) },
|
|
1199
|
+
}),
|
|
1200
|
+
};
|
|
1201
|
+
};
|
|
1202
|
+
/**
|
|
1203
|
+
* An element that goes with the one around it, which renders it from
|
|
1204
|
+
* props, converts no more on its own, and nor does it when that one stays
|
|
1205
|
+
* markup for a person: a re-run has to find the markup it found. Only its
|
|
1206
|
+
* TODOs stay. It has a class to be found by, so it has been planned.
|
|
1207
|
+
*/
|
|
1208
|
+
const forget = (node) => {
|
|
1209
|
+
const own = { conversion: null, todos: planned.get(node).todos };
|
|
1210
|
+
planned.set(node, own);
|
|
1211
|
+
readings.delete(node);
|
|
1212
|
+
const at = elements.findIndex(found => found.path.node === node);
|
|
1213
|
+
if (at === -1)
|
|
1214
|
+
return;
|
|
1215
|
+
if (own.todos.length > 0) {
|
|
1216
|
+
elements[at] = { ...elements[at], plan: own, converts: false };
|
|
1217
|
+
}
|
|
1218
|
+
else {
|
|
1219
|
+
elements.splice(at, 1);
|
|
1220
|
+
}
|
|
1221
|
+
};
|
|
1015
1222
|
/** The element directly around this one, when this is its only child. */
|
|
1016
1223
|
const holderOf = (elementPath) => {
|
|
1017
1224
|
const up = elementPath.parent?.node;
|
|
@@ -1065,6 +1272,9 @@ export default function transform(fileInfo, api, options = {}) {
|
|
|
1065
1272
|
}),
|
|
1066
1273
|
...(textOf(element) !== undefined && { text: textOf(element) }),
|
|
1067
1274
|
};
|
|
1275
|
+
if (facts.tokens.some(token => rootFor(token)?.readsGlyph)) {
|
|
1276
|
+
Object.assign(facts, glyphPlace(elementPath));
|
|
1277
|
+
}
|
|
1068
1278
|
// A joiner call the codemod can read converts exactly: its fixed classes
|
|
1069
1279
|
// as for a static className, and each condition on a flag as its prop.
|
|
1070
1280
|
const joined = className === null
|
|
@@ -1085,6 +1295,7 @@ export default function transform(fileInfo, api, options = {}) {
|
|
|
1085
1295
|
}
|
|
1086
1296
|
let result = exact ?? plan(facts);
|
|
1087
1297
|
const builds = result.conversion?.icons;
|
|
1298
|
+
const glyphs = result.conversion?.glyph !== undefined;
|
|
1088
1299
|
const converts = result.conversion !== null || result.fold !== undefined;
|
|
1089
1300
|
const becomes = result.conversion?.target ?? result.fold?.target;
|
|
1090
1301
|
if (className === null && !exact && becomes) {
|
|
@@ -1117,27 +1328,14 @@ export default function transform(fileInfo, api, options = {}) {
|
|
|
1117
1328
|
inner.numbers.push(...result.fold.numbers);
|
|
1118
1329
|
}
|
|
1119
1330
|
if (builds) {
|
|
1120
|
-
// A `.icon` the target builds from props goes with the element
|
|
1121
|
-
// doesn't convert on its own, and nor does it when a computed
|
|
1122
|
-
// `className` leaves the element to a person: a re-run has to find
|
|
1123
|
-
// the markup it found.
|
|
1331
|
+
// A `.icon` the target builds from props goes with the element.
|
|
1124
1332
|
const inside = (element.children ?? []).filter((child) => !dropped(child));
|
|
1125
|
-
for (const icon of builds)
|
|
1126
|
-
|
|
1127
|
-
const own = { conversion: null, todos: planned.get(node).todos };
|
|
1128
|
-
planned.set(node, own);
|
|
1129
|
-
readings.delete(node);
|
|
1130
|
-
const at = elements.findIndex(found => found.path.node === node);
|
|
1131
|
-
if (at === -1)
|
|
1132
|
-
continue;
|
|
1133
|
-
if (own.todos.length > 0) {
|
|
1134
|
-
elements[at] = { ...elements[at], plan: own, converts: false };
|
|
1135
|
-
}
|
|
1136
|
-
else {
|
|
1137
|
-
elements.splice(at, 1);
|
|
1138
|
-
}
|
|
1139
|
-
}
|
|
1333
|
+
for (const icon of builds)
|
|
1334
|
+
forget(inside[icon.index]);
|
|
1140
1335
|
}
|
|
1336
|
+
// So does the <i> a glyph the target renders takes the place of.
|
|
1337
|
+
if (glyphs)
|
|
1338
|
+
forget(sole);
|
|
1141
1339
|
planned.set(element, result);
|
|
1142
1340
|
if (result.conversion || result.fold || result.todos.length > 0) {
|
|
1143
1341
|
elements.unshift({ path: elementPath, plan: result, converts });
|
|
@@ -1313,16 +1511,14 @@ export default function transform(fileInfo, api, options = {}) {
|
|
|
1313
1511
|
}
|
|
1314
1512
|
}
|
|
1315
1513
|
// A target that renders the element's children itself takes their place:
|
|
1316
|
-
// they go, and so does the closing tag, their comments
|
|
1317
|
-
//
|
|
1514
|
+
// they go, and so does the closing tag, their comments (in their tags
|
|
1515
|
+
// and attributes too: a glyph's <i> has a `className`) and its own
|
|
1516
|
+
// moving to the name that stays.
|
|
1318
1517
|
const gone = conversion.rendersChildren
|
|
1319
1518
|
? [
|
|
1320
1519
|
...(element.children ?? [])
|
|
1321
1520
|
.filter((node) => node.type === 'JSXElement')
|
|
1322
|
-
.flatMap((node) =>
|
|
1323
|
-
...(node.comments ?? []),
|
|
1324
|
-
...tagComments(node),
|
|
1325
|
-
]),
|
|
1521
|
+
.flatMap((node) => commentsWithin(node)),
|
|
1326
1522
|
...[element.closingElement, element.closingElement?.name].flatMap((node) => node?.comments ?? []),
|
|
1327
1523
|
]
|
|
1328
1524
|
: [];
|
|
@@ -1333,10 +1529,12 @@ export default function transform(fileInfo, api, options = {}) {
|
|
|
1333
1529
|
removeAttr(holder, findAttr(holder, name));
|
|
1334
1530
|
}
|
|
1335
1531
|
const joined = joins.get(element);
|
|
1532
|
+
// A glyph the target renders goes first, as the planner wrote it.
|
|
1533
|
+
const lead = conversion.glyph ? glyphAttributes(j, conversion.glyph) : [];
|
|
1336
1534
|
if (!joined) {
|
|
1337
|
-
writeClassName(j, element, conversion.props, conversion.className);
|
|
1535
|
+
writeClassName(j, element, conversion.props, conversion.className, lead);
|
|
1338
1536
|
}
|
|
1339
|
-
else if (writeJoined(j, element, conversion, joined)) {
|
|
1537
|
+
else if (writeJoined(j, element, conversion, joined, lead)) {
|
|
1340
1538
|
unjoined.add(joined.call.callee.name);
|
|
1341
1539
|
}
|
|
1342
1540
|
if (conversion.rendersChildren) {
|
|
@@ -13,6 +13,16 @@
|
|
|
13
13
|
* explicit conversions rather than guesses.
|
|
14
14
|
*/
|
|
15
15
|
import type { ComponentMapping, PropAction } from '../../types.js';
|
|
16
|
+
/**
|
|
17
|
+
* Components whose bestax counterpart really does accept `as`. An empty
|
|
18
|
+
* action passes the prop through untouched; its job is to claim the name so
|
|
19
|
+
* the universal `AS_TODO` never sees it.
|
|
20
|
+
*
|
|
21
|
+
* Several of these narrow `as` to a literal union (`Footer` is
|
|
22
|
+
* `'footer' | 'div'`, `Control` is `'div'`), so an `as={SomeComponent}` still
|
|
23
|
+
* surfaces — as a type error the user can see, rather than a silent rewrite.
|
|
24
|
+
*/
|
|
25
|
+
export declare const AS_OK: PropAction;
|
|
16
26
|
/**
|
|
17
27
|
* Helper props rbx mixes into every component via `HelpersProps`. Applied
|
|
18
28
|
* after each component's own prop map, to whatever attributes are left.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mapping.d.ts","sourceRoot":"","sources":["../../../src/sources/rbx/mapping.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAE,gBAAgB,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"mapping.d.ts","sourceRoot":"","sources":["../../../src/sources/rbx/mapping.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAE,gBAAgB,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAoBnE;;;;;;;;GAQG;AACH,eAAO,MAAM,KAAK,EAAE,UAAe,CAAC;AAEpC;;;;;;;;;GASG;AACH,eAAO,MAAM,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CA4CtD,CAAC;AAEF,uDAAuD;AACvD,eAAO,MAAM,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAOrD,CAAC;AAEF,2DAA2D;AAC3D,eAAO,MAAM,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAQvD,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,sBAAsB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAShE,CAAC;AAEF,eAAO,MAAM,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAutBpD,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CA2FhD,CAAC;AAEF,sDAAsD;AACtD,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,gBAAgB,GAAG,SAAS,CAM3E"}
|