bestax-migrate 2.27.0 → 2.29.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.
@@ -162,7 +162,7 @@ export function fontAwesomeMessage(asset, quoted) {
162
162
  }
163
163
  const loads = `the page loads Font Awesome ${major} from \`${quoted}\``;
164
164
  if (major <= 4) {
165
- return `${loads}, whose classes are \`fa fa-<name>\`, and bestax's \`Icon\` writes \`fas fa-<name>\`, which ${major} doesn't style: ${INSTALL_FONT_AWESOME}, then either import \`css/v4-shims.min.css\` beside it, so the page's ${major} names keep working, or move them to the current names (\`fa-envelope-o\` is \`envelope\` with \`variant="regular"\`)`;
165
+ return `${loads}, whose classes are \`fa fa-<name>\`: the codemod keeps that \`fa\` on each icon it converts (\`variant="fa"\`), which ${major} styles, but bestax's \`Icon\` given no \`variant\` writes \`fas fa-<name>\`, which ${major} doesn't, so for icons written from here on, ${INSTALL_FONT_AWESOME}, then either import \`css/v4-shims.min.css\` beside it, so the page's ${major} names keep working, or move them to the current names (\`fa-envelope-o\` is \`envelope\` with \`variant="regular"\`)`;
166
166
  }
167
167
  if (major === 5) {
168
168
  return `${loads}: ${ICON_LOADS_NO_FONT} and writes the \`fas fa-<name>\` classes 5 reads, so the \`<link>\` keeps working from the app's index.html; installed from npm, \`@fortawesome/fontawesome-free\` takes 6 or 7 (bestax-bulma's optional peer range is \`${FONT_AWESOME_PEER_RANGE}\`, and an older one fails \`npm install\` with ERESOLVE), which still read the 5 names`;
@@ -7,8 +7,13 @@
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
  */
16
+ import { type Glyph } from './glyph.js';
12
17
  export interface ElementFacts {
13
18
  tag: string;
14
19
  /** The static className, split on whitespace, in order. */
@@ -102,6 +107,26 @@ export interface ElementFacts {
102
107
  * The string the element's content renders, when it is one static text.
103
108
  */
104
109
  text?: string;
110
+ /**
111
+ * The classes the element directly around this one renders: its static
112
+ * ones, or for a bestax component already in the file, its root class and
113
+ * the flags written out as `true`, so a re-run reads the same. What an
114
+ * entry that `readsGlyph` reads to point at `Control`'s icon props.
115
+ */
116
+ parentTokens?: readonly string[];
117
+ /**
118
+ * The nearest <a> or <button> around this element, a bestax component that
119
+ * renders one included, and what names it: its own `aria-label` or
120
+ * `aria-labelledby`, written out, text in it outside every icon, or `none`
121
+ * when nothing does. `label` is left out when it can't be told from here (a
122
+ * name or content written as an expression, or a component the codemod
123
+ * doesn't know inside). What an entry that `readsGlyph` reads to say
124
+ * whether an icon is decoration there.
125
+ */
126
+ clickableAround?: {
127
+ tag: string;
128
+ label?: 'aria-label' | 'aria-labelledby' | 'text' | 'none';
129
+ };
105
130
  }
106
131
  export interface ChildFacts {
107
132
  tag: string;
@@ -178,6 +203,20 @@ export interface Conversion {
178
203
  * they go, and the element closes itself.
179
204
  */
180
205
  rendersChildren?: true;
206
+ /**
207
+ * The glyph the target renders from props in place of the element's only
208
+ * child (`Icon`'s `<i>`), written before the props above. The child goes,
209
+ * as `rendersChildren`, which is set beside it, says.
210
+ */
211
+ glyph?: Glyph;
212
+ /**
213
+ * That child carried the `aria-hidden` the target writes on the element
214
+ * (Font Awesome's own `<i aria-hidden="true">`), so it moves up: the one
215
+ * way a conversion's markup differs from the element's, on purpose, since
216
+ * the glyph is hidden either way. The render tests compare it with the
217
+ * attribute moved up, and nothing else.
218
+ */
219
+ movesHidden?: true;
181
220
  /**
182
221
  * The target renders the bare element around this one too: the component
183
222
  * takes that element's place, with its `key`.
@@ -1 +1 @@
1
- {"version":3,"file":"plan.d.ts","sourceRoot":"","sources":["../../../src/sources/bulma-classes/plan.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"plan.d.ts","sourceRoot":"","sources":["../../../src/sources/bulma-classes/plan.ts"],"names":[],"mappings":"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"}
@@ -7,10 +7,15 @@
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';
18
+ import { aTag } from '../_shared/articles.js';
14
19
  import { ruleId } from './rules.js';
15
20
  /** The button text `File` renders when it's given none. */
16
21
  const DEFAULT_FILE_LABEL = 'Choose a file\u2026';
@@ -125,6 +130,24 @@ export function plan(facts) {
125
130
  conversion: null,
126
131
  todos: [...todos, { rule: ruleId(kind, token), message }],
127
132
  });
133
+ // ---- The tag, first ------------------------------------------------------
134
+ // On a tag the target can't render, the tag is the work that comes first:
135
+ // any other refusal would ask for work (an attribute added or dropped, a
136
+ // child moved) that a re-run then refuses on the tag anyway.
137
+ let renders = entry.tag;
138
+ if (entry.sizeDrivesTag && tag !== 'p') {
139
+ const size = propsFrom(entry, tokens, tag, root, facts.attributes).writes.get('size');
140
+ if (size !== undefined)
141
+ renders = `h${size}`;
142
+ }
143
+ let as;
144
+ if (renders !== tag) {
145
+ const reachable = entry.as === 'any' || (entry.as?.includes(tag) ?? false);
146
+ if (!reachable) {
147
+ return refuse('tag', target, `bestax \`${target}\` renders ${tagsItReaches(entry)}, not ${aTag(tag)}; keep the markup, or change the tag and re-run`);
148
+ }
149
+ as = tag;
150
+ }
128
151
  // ---- Refusals: the component would render something else ---------------
129
152
  if (facts.hasSpread) {
130
153
  return refuse('spread', target, `this element spreads props, which bestax \`${target}\` may read differently than the element did (a spread \`className\` merges with its classes instead of replacing them); convert it to \`${target}\` by hand`);
@@ -227,7 +250,19 @@ export function plan(facts) {
227
250
  : naming === 'unsure'
228
251
  ? { ...unnamed, ...entry.namedDefaults.defaults }
229
252
  : unnamed;
230
- const missing = [...Object.entries(defaults), ...written].filter(([name]) => !attributes.has(name));
253
+ // Font Awesome's own markup hides the <i> rather than the element around
254
+ // it, and a target that renders the glyph from props writes the same
255
+ // `aria-hidden` on the element in its place: it moves up, and the glyph is
256
+ // hidden either way. The one difference in markup a conversion makes, on
257
+ // purpose; the render tests compare it with the attribute moved up. Only
258
+ // onto an element that carries nothing assistive tech reads, by name and
259
+ // whatever the value: an expression may render as anything.
260
+ const moved = entry.readsGlyph &&
261
+ defaults['aria-hidden'] === 'true' &&
262
+ [...attributes.keys()].every(name => movesBeside(entry, name))
263
+ ? readHiddenGlyph(facts.soleChild)
264
+ : undefined;
265
+ const missing = [...Object.entries(defaults), ...written].filter(([name]) => !attributes.has(name) && !(moved && name === 'aria-hidden'));
231
266
  const drop = [];
232
267
  for (const name of entry.untypedAttrs ?? []) {
233
268
  if (!attributes.has(name))
@@ -316,14 +351,17 @@ export function plan(facts) {
316
351
  }
317
352
  if (missing.length > 0) {
318
353
  const list = missing.map(([name, value]) => `\`${name}="${value}"\``);
319
- return refuse('defaults', target, `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`);
354
+ 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`;
355
+ return refuse('defaults', target, entry.readsGlyph && naming === 'unnamed'
356
+ ? hiddenAdvice(facts, entry, usual)
357
+ : usual);
320
358
  }
321
359
  for (const [name, rule] of Object.entries(entry.writesAttr ?? {})) {
322
360
  const value = attributes.get(name);
323
361
  if (!rule.on.includes(tag) || value === undefined)
324
362
  continue;
325
363
  if (typeof value !== 'string' || !rule.keeps.includes(value)) {
326
- 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`);
364
+ return refuse('attr', name, `bestax \`${target}\` keeps ${aTag(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
365
  }
328
366
  }
329
367
  for (const [name, tags] of Object.entries(entry.dropsAttr ?? {})) {
@@ -334,32 +372,16 @@ export function plan(facts) {
334
372
  // A `name` on an <a> still names the target of a `#fragment` link.
335
373
  const anchored = name === 'name' && tag === 'a';
336
374
  return refuse('drops', target, visible
337
- ? `bestax \`${target}\` drops \`disabled\` on a <${tag}>, and Bulma styles a disabled \`.${rootLabel(facts.tokens)}\` on any tag, so converting would change how it looks; keep this element as markup`
375
+ ? `bestax \`${target}\` drops \`disabled\` on ${aTag(tag)}, and Bulma styles a disabled \`.${rootLabel(facts.tokens)}\` on any tag, so converting would change how it looks; keep this element as markup`
338
376
  : anchored
339
377
  ? `bestax \`${target}\` drops \`name\` on an <a>, where it still names the target of a \`#fragment\` link; keep this element as markup, or move the target to an \`id\`, then re-run`
340
- : `bestax \`${target}\` drops \`${name}\` on a <${tag}>, where it does nothing anyway; remove it, then re-run`);
378
+ : `bestax \`${target}\` drops \`${name}\` on ${aTag(tag)}, where it does nothing anyway; remove it, then re-run`);
341
379
  }
342
380
  }
343
381
  // ---- Tokens → props ---------------------------------------------------------
344
382
  const fromTokens = propsFrom(entry, tokens, tag, root, attributes);
345
383
  const { writes, converted } = fromTokens;
346
384
  numbers.push(...fromTokens.numbers);
347
- // ---- The tag ------------------------------------------------------------
348
- let renders = entry.tag;
349
- if (entry.sizeDrivesTag && tag !== 'p' && writes.has('size')) {
350
- renders = `h${writes.get('size')}`;
351
- }
352
- let as;
353
- if (renders !== tag) {
354
- const reachable = entry.as === 'any' || (entry.as?.includes(tag) ?? false);
355
- if (!reachable) {
356
- if (entry.otherTagsStay?.tags.includes(tag)) {
357
- return { conversion: null, todos };
358
- }
359
- return refuse('tag', target, `bestax \`${target}\` renders ${tagsItReaches(entry)}, not a <${tag}>; keep the markup, or change the tag and re-run`);
360
- }
361
- as = tag;
362
- }
363
385
  // Last, since what the children are can change once a refusal leaves
364
386
  // them to convert on their own, and a re-run has to find the same reason.
365
387
  let icons;
@@ -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 && aTag(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
- const glyph = readGlyph(child.soleChild);
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: [...props, ...icon.props],
740
+ props: [...glyphProps(glyph), ...icon.props],
605
741
  className: icon.className,
606
742
  drop: icon.drop,
607
743
  numbers: icon.numbers,
@@ -890,7 +1026,7 @@ function planFold(facts, wrapper, todos) {
890
1026
  todos: [...todos, { rule: ruleId(kind, token), message }],
891
1027
  });
892
1028
  if (facts.tag !== entry.tag) {
893
- return refuse('tag', target, `bestax ${renders} renders \`.${wrapper}\` on a <${entry.tag}>, not a <${facts.tag}>; keep the markup, or change the tag and re-run`);
1029
+ return refuse('tag', target, `bestax ${renders} renders \`.${wrapper}\` on ${aTag(entry.tag)}, not ${aTag(facts.tag)}; keep the markup, or change the tag and re-run`);
894
1030
  }
895
1031
  if (facts.hasSpread) {
896
1032
  return refuse('spread', target, `this element spreads props, and bestax ${renders} renders \`.${wrapper}\` with none of its own; keep it as markup`);
@@ -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;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,KAAK,EAAE,GAAG,EAAW,QAAQ,EAAE,MAAM,aAAa,CAAC;AAE1D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AA26BvD,MAAM,CAAC,OAAO,UAAU,SAAS,CAC/B,QAAQ,EAAE,QAAQ,EAClB,GAAG,EAAE,GAAG,EACR,OAAO,GAAE,gBAAqB,GAC7B,MAAM,GAAG,SAAS,CA02BpB"}
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"}