Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 | 25x 11x 11x 11x 11x 11x 4x 4x 3x 3x 4x 4x 4x 4x 4x 3x 5x 19x 10x 15x 15x 18x 8x 8x 10x 10x 8x 19x 19x 5x 5x 3x 14x 2x 12x 19x 3x 3x 1x 19x 9x 5x 3x 2x 2x 2x 5x | /**
* LBC (Lightning Base Component) tag emitters
*
* HTML target / `compose: 'reference'` mode tag emission for `<lightning-*>`
* custom elements. Handles both root-level LBC elements (from
* `structure.component`) and child composed component references.
*
* Static props become literal HTML attributes; bound props are resolved
* from the active permutation state (variants, modifiers) or slot overrides.
*
* NOTE: This file currently also hosts a few generic helpers (`isReferenceMode`,
* `findSlotInChildren`, and the bound-prop resolution functions) that are
* target-agnostic. When additional targets land (e.g. JSX), those helpers
* should be lifted out into a shared module so each target only carries its
* own tag/attribute serializers. See the "Future Work" section of the RFC.
*/
import type {
StructureMetadata,
SlotMetadata,
ComposedComponentMetadata,
ResolvedUIF,
} from '@fds-uif/generator-base/browser';
import type { StaticGenerationContext } from './types.js';
import {
formatAttribute,
escapeAttributeValue,
toKebabCase,
toLbcTagName,
humanize,
indent,
type AttributeValue,
} from './attribute-utils.js';
export { toLbcTagName } from './attribute-utils.js';
/**
* True when the generator is in reference-composition mode — children that
* declare external library mappings render as library tags rather than
* inline expansion. For the HTML target, this is the LBC path: composed
* children with `structure.component` render as `<lightning-*>` tags.
*/
export function isReferenceMode(context: StaticGenerationContext): boolean {
return context.options.compose === 'reference';
}
/**
* Core LBC tag builder shared by root and child element renderers.
* Assembles a self-closing `<lightning-*>` tag with static and bound prop attributes.
*/
export function buildLbcTag(
componentName: string,
staticProps: Record<string, unknown> | undefined,
boundProps: Record<string, string> | undefined,
context: StaticGenerationContext,
indentLevel = 0,
): string {
const pad = indent(indentLevel);
const tagName = toLbcTagName(componentName);
const attrs = [...buildStaticPropAttributes(staticProps), ...buildBoundPropAttributes(boundProps, context)];
const attrStr = attrs.length > 0 ? ` ${attrs.join(' ')}` : '';
return `${pad}<${tagName}${attrStr}></${tagName}>`;
}
/**
* Build the root LBC element from a UIF with a `structure.component` mapping.
* Falls back to HTML fragment rendering when no component mapping exists.
*
* When `renderSlotChildren` is provided and returns non-empty content, the
* children are rendered inside the parent tag — supporting composition
* patterns like `<lightning-breadcrumbs><lightning-breadcrumb>...`.
*/
export function buildLbcRootElement(
uif: ResolvedUIF,
context: StaticGenerationContext,
fallback: (structure: StructureMetadata, ctx: StaticGenerationContext) => string,
renderSlotChildren?: (ctx: StaticGenerationContext, rootComponentName: string) => string,
): string {
const component = uif.structure?.component;
if (!component) return fallback(context.metadata.structure, context);
const tagName = toLbcTagName(component);
const attrs = [
...buildStaticPropAttributes(uif.structure.componentProps?.static),
...buildBoundPropAttributes(uif.structure.componentProps?.bound, context),
];
const attrStr = attrs.length > 0 ? ` ${attrs.join(' ')}` : '';
const rootBoundPropKeys = uif.structure.componentProps?.bound
? new Set(Object.values(uif.structure.componentProps.bound))
: undefined;
const childCtx = { ...context, indent: context.indent + 1, rootBoundPropKeys };
const childContent = renderSlotChildren?.(childCtx, component);
Iif (childContent) {
return `<${tagName}${attrStr}>\n${childContent}\n</${tagName}>`;
}
return `<${tagName}${attrStr}></${tagName}>`;
}
/**
* Render a composed component as an LBC custom element.
*/
export function renderLbcElement(
component: ComposedComponentMetadata,
context: StaticGenerationContext,
): string {
return buildLbcTag(
component.component,
component.props.static,
component.props.bound,
context,
context.indent,
);
}
/** Format static componentProps entries as `key="value"` attribute strings. */
export function buildStaticPropAttributes(staticProps: Record<string, unknown> | undefined): string[] {
if (!staticProps) return [];
return Object.entries(staticProps)
.map(([key, value]) => formatAttribute(toKebabCase(key), value as AttributeValue))
.filter((s): s is string => s !== null);
}
/** Resolve bound componentProps to their current values and format as attribute strings. */
export function buildBoundPropAttributes(
boundProps: Record<string, string> | undefined,
context: StaticGenerationContext,
): string[] {
if (!boundProps) return [];
const attrs: string[] = [];
for (const [lbcAttr, uifPropRef] of Object.entries(boundProps)) {
const formatted = resolveBoundPropDefault(toKebabCase(lbcAttr), uifPropRef, context);
if (formatted) attrs.push(formatted);
}
return attrs;
}
/**
* Resolve a single bound componentProp reference to an attribute string.
*
* Resolution order:
* 1. Active variant value (source: 'variant')
* 2. Active state/modifier (source: 'state' | 'modifier') — boolean, present when true
* 3. Slot override text or humanized placeholder (slot name reference)
*/
export function resolveBoundPropDefault(
lbcAttr: string,
uifPropRef: string,
context: StaticGenerationContext,
): string | null {
const prop = context.metadata.props.find((p) => p.name === uifPropRef);
if (prop?.source === 'variant') {
const val = context.state.variants[uifPropRef];
if (!val || val === prop.default) return null;
return `${lbcAttr}="${escapeAttributeValue(val)}"`;
}
if (prop?.source === 'state' || prop?.source === 'modifier') {
return context.state.modifiers[uifPropRef] ? lbcAttr : null;
}
const override = context.options.slotOverrides?.[uifPropRef];
if (override !== undefined) return override ? `${lbcAttr}="${escapeAttributeValue(override)}"` : null;
const slotMeta = findSlotInChildren(context.metadata.structure.children, uifPropRef);
if (!slotMeta) return null;
const placeholder = slotMeta.description ?? humanize(uifPropRef);
return `${lbcAttr}="${escapeAttributeValue(placeholder)}"`;
}
/** Walk the structure children to find a slot by name. */
export function findSlotInChildren(
children: StructureMetadata['children'],
slotName: string,
): SlotMetadata | null {
for (const child of children) {
if ('type' in child && child.type === 'slot' && child.name === slotName) {
return child;
}
Eif (!('type' in child)) {
const found = findSlotInChildren(child.children, slotName);
if (found) return found;
}
}
return null;
}
|