//// Template ergonomics helpers and migration-hint diagnostics for M39. import gleam/list import gleam/option.{type Option, None, Some} import lightspeed/component import lightspeed/component/template import lightspeed/component/template_compiler /// Attribute alias with migration guidance. pub type AttrAlias { AttrAlias(alias: String, canonical: String, migration_hint: String) } /// Slot default used to reduce repetitive template composition boilerplate. pub type SlotDefault { SlotDefault(name: String, rendered: component.Rendered) } /// Ergonomic schema wrapper around the template compiler schema. pub type ErgonomicSchema(assigns, slots) { ErgonomicSchema( schema: template_compiler.Schema(assigns, slots), attr_aliases: List(AttrAlias), slot_defaults: List(SlotDefault), ) } /// One deterministic compile diagnostic with actionable migration hint. pub type Diagnostic { Diagnostic(code: String, label: String, hint: String) } /// Ergonomic compile artifact with metadata. pub type Compiled(assigns, slots) { Compiled( compiled: template_compiler.CompiledTemplate(assigns, slots), applied_aliases: List(#(String, String)), applied_slot_defaults: List(String), signature: String, ) } /// Build one attr alias. pub fn attr_alias( alias: String, canonical: String, migration_hint: String, ) -> AttrAlias { AttrAlias(alias: alias, canonical: canonical, migration_hint: migration_hint) } /// Build one slot default from rendered content. pub fn slot_default(name: String, rendered: component.Rendered) -> SlotDefault { SlotDefault(name: name, rendered: rendered) } /// Build one slot default from raw HTML. pub fn slot_default_html(name: String, html: String) -> SlotDefault { SlotDefault(name: name, rendered: component.html(html)) } /// Build ergonomic schema wrapper. pub fn ergonomic_schema( schema: template_compiler.Schema(assigns, slots), attr_aliases: List(AttrAlias), slot_defaults: List(SlotDefault), ) -> ErgonomicSchema(assigns, slots) { ErgonomicSchema( schema: schema, attr_aliases: attr_aliases, slot_defaults: slot_defaults, ) } /// Build one routine schema that uses automatic slot passthrough. pub fn routine_schema( name: String, attrs: List(template_compiler.AttrSchema), slots: List(template_compiler.SlotSchema), build_assigns: fn(List(#(String, template_compiler.AttrValue))) -> Result(assigns, String), render: fn(assigns, List(#(String, component.Rendered))) -> component.Rendered, ) -> template_compiler.Schema(assigns, List(#(String, component.Rendered))) { template_compiler.schema( name, attrs, slots, build_assigns, fn(value) { Ok(value) }, template.function(fn(assigns, slots) { render(assigns, slots) }), ) } /// Convenience constructor for string attr input. pub fn string_attr( name: String, value: String, ) -> #(String, template_compiler.AttrValue) { #(name, template_compiler.StringAttr(value)) } /// Convenience constructor for int attr input. pub fn int_attr( name: String, value: Int, ) -> #(String, template_compiler.AttrValue) { #(name, template_compiler.IntAttr(value)) } /// Convenience constructor for bool attr input. pub fn bool_attr( name: String, value: Bool, ) -> #(String, template_compiler.AttrValue) { #(name, template_compiler.BoolAttr(value)) } /// Convenience constructor for slot input. pub fn slot( name: String, rendered: component.Rendered, ) -> #(String, component.Rendered) { #(name, rendered) } /// Convenience constructor for slot input from raw HTML. pub fn slot_html(name: String, html: String) -> #(String, component.Rendered) { #(name, component.html(html)) } /// Optional string accessor with fallback default. pub fn optional_string( attrs: List(#(String, template_compiler.AttrValue)), name: String, default: String, ) -> Result(String, String) { case find_attr(attrs, name) { None -> Ok(default) Some(template_compiler.StringAttr(value)) -> Ok(value) Some(other) -> Error( "invalid_attr_type:" <> name <> ":expected=string:actual=" <> value_kind_label(other), ) } } /// Optional int accessor with fallback default. pub fn optional_int( attrs: List(#(String, template_compiler.AttrValue)), name: String, default: Int, ) -> Result(Int, String) { case find_attr(attrs, name) { None -> Ok(default) Some(template_compiler.IntAttr(value)) -> Ok(value) Some(other) -> Error( "invalid_attr_type:" <> name <> ":expected=int:actual=" <> value_kind_label(other), ) } } /// Optional bool accessor with fallback default. pub fn optional_bool( attrs: List(#(String, template_compiler.AttrValue)), name: String, default: Bool, ) -> Result(Bool, String) { case find_attr(attrs, name) { None -> Ok(default) Some(template_compiler.BoolAttr(value)) -> Ok(value) Some(other) -> Error( "invalid_attr_type:" <> name <> ":expected=bool:actual=" <> value_kind_label(other), ) } } /// Return one required slot HTML as string. pub fn required_slot_html( slots: List(#(String, component.Rendered)), name: String, ) -> Result(String, String) { case template_compiler.require_slot(slots, name) { Ok(rendered) -> Ok(component.to_html(rendered)) Error(reason) -> Error(reason) } } /// Return one optional slot HTML as string, defaulting to empty. pub fn slot_html_or_empty( slots: List(#(String, component.Rendered)), name: String, ) -> String { case template_compiler.slot_rendered(slots, name) { Some(rendered) -> component.to_html(rendered) None -> "" } } /// Compile one ergonomic template invocation. pub fn compile( schema: ErgonomicSchema(assigns, slots), attrs: List(#(String, template_compiler.AttrValue)), slots: List(#(String, component.Rendered)), ) -> Result(Compiled(assigns, slots), List(Diagnostic)) { let #(normalized_attrs, applied_aliases) = apply_aliases(attrs, schema.attr_aliases, [], []) let #(normalized_slots, applied_defaults) = apply_slot_defaults(slots, schema.slot_defaults, []) case template_compiler.compile(schema.schema, normalized_attrs, normalized_slots) { Ok(compiled_template) -> Ok(Compiled( compiled: compiled_template, applied_aliases: list.reverse(applied_aliases), applied_slot_defaults: list.reverse(applied_defaults), signature: compile_signature( compiled_template, list.reverse(applied_aliases), list.reverse(applied_defaults), ), )) Error(diagnostics) -> Error( enhance_diagnostics( diagnostics, schema.schema, schema.attr_aliases, schema.slot_defaults, [], ), ) } } /// Stable diagnostic label. pub fn diagnostic_label(diagnostic: Diagnostic) -> String { diagnostic.code <> ":" <> diagnostic.label <> ":hint=" <> diagnostic.hint } /// Stable diagnostics signature. pub fn diagnostics_signature(diagnostics: List(Diagnostic)) -> String { join_with(";", list.map(diagnostics, diagnostic_label)) } /// Compiled artifact signature. pub fn signature(compiled: Compiled(assigns, slots)) -> String { compiled.signature } /// Compiled HTML. pub fn html(compiled: Compiled(assigns, slots)) -> String { template_compiler.html(compiled.compiled) } /// Compiled fingerprint. pub fn fingerprint(compiled: Compiled(assigns, slots)) -> String { template_compiler.fingerprint(compiled.compiled) } /// Underlying compiled template accessor. pub fn compiled_template( compiled: Compiled(assigns, slots), ) -> template_compiler.CompiledTemplate(assigns, slots) { compiled.compiled } /// Applied alias migrations. pub fn applied_aliases( compiled: Compiled(assigns, slots), ) -> List(#(String, String)) { compiled.applied_aliases } /// Applied slot defaults. pub fn applied_slot_defaults( compiled: Compiled(assigns, slots), ) -> List(String) { compiled.applied_slot_defaults } fn apply_aliases( attrs: List(#(String, template_compiler.AttrValue)), aliases: List(AttrAlias), normalized_rev: List(#(String, template_compiler.AttrValue)), applied_rev: List(#(String, String)), ) -> #(List(#(String, template_compiler.AttrValue)), List(#(String, String))) { case attrs { [] -> #(list.reverse(normalized_rev), applied_rev) [#(name, value), ..rest] -> case find_alias(aliases, name) { Some(alias_rule) -> apply_aliases( rest, aliases, [#(alias_rule.canonical, value), ..normalized_rev], [#(alias_rule.alias, alias_rule.canonical), ..applied_rev], ) None -> apply_aliases( rest, aliases, [#(name, value), ..normalized_rev], applied_rev, ) } } } fn apply_slot_defaults( slots: List(#(String, component.Rendered)), defaults: List(SlotDefault), applied_rev: List(String), ) -> #(List(#(String, component.Rendered)), List(String)) { apply_slot_defaults_loop(slots, defaults, applied_rev) } fn apply_slot_defaults_loop( slots: List(#(String, component.Rendered)), defaults: List(SlotDefault), applied_rev: List(String), ) -> #(List(#(String, component.Rendered)), List(String)) { case defaults { [] -> #(slots, applied_rev) [default, ..rest] -> case slot_exists(slots, default.name) { True -> apply_slot_defaults_loop(slots, rest, applied_rev) False -> apply_slot_defaults_loop( [#(default.name, default.rendered), ..slots], rest, [default.name, ..applied_rev], ) } } } fn enhance_diagnostics( diagnostics: List(template_compiler.Diagnostic), schema: template_compiler.Schema(assigns, slots), aliases: List(AttrAlias), slot_defaults: List(SlotDefault), acc_rev: List(Diagnostic), ) -> List(Diagnostic) { case diagnostics { [] -> list.reverse(acc_rev) [diagnostic, ..rest] -> enhance_diagnostics(rest, schema, aliases, slot_defaults, [ diagnostic_with_hint(diagnostic, schema, aliases, slot_defaults), ..acc_rev ]) } } fn diagnostic_with_hint( diagnostic: template_compiler.Diagnostic, schema: template_compiler.Schema(assigns, slots), aliases: List(AttrAlias), slot_defaults: List(SlotDefault), ) -> Diagnostic { let label = template_compiler.diagnostic_label(diagnostic) case diagnostic { template_compiler.MissingAttribute(_, attribute) -> Diagnostic( code: "missing_attribute", label: label, hint: missing_attribute_hint(attribute, aliases, schema), ) template_compiler.DuplicateAttribute(_, attribute) -> Diagnostic( code: "duplicate_attribute", label: label, hint: "keep a single `" <> attribute <> "` value", ) template_compiler.UnknownAttribute(_, attribute) -> Diagnostic( code: "unknown_attribute", label: label, hint: unknown_attribute_hint(attribute, aliases, schema), ) template_compiler.InvalidAttributeType(_, _, expected, _) -> Diagnostic( code: "invalid_attribute_type", label: label, hint: "use the `" <> expected_helper_label(expected) <> "` attr helper", ) template_compiler.MissingSlot(_, slot) -> Diagnostic( code: "missing_slot", label: label, hint: missing_slot_hint(slot, slot_defaults), ) template_compiler.DuplicateSlot(_, slot) -> Diagnostic( code: "duplicate_slot", label: label, hint: "keep a single `" <> slot <> "` slot entry", ) template_compiler.UnknownSlot(_, slot) -> Diagnostic( code: "unknown_slot", label: label, hint: unknown_slot_hint(slot, schema), ) template_compiler.AssignBuildFailed(_, _) -> Diagnostic( code: "assign_build_failed", label: label, hint: "verify typed assign mapping and migration adapters", ) template_compiler.SlotBuildFailed(_, _) -> Diagnostic( code: "slot_build_failed", label: label, hint: "verify slot mapping and optional-slot fallback handling", ) } } fn missing_attribute_hint( attribute: String, aliases: List(AttrAlias), schema: template_compiler.Schema(assigns, slots), ) -> String { case aliases_for_canonical(aliases, attribute) { [] -> "provide required attr `" <> attribute <> "` (known attrs: " <> join_with(",", schema_attr_names(schema.attrs)) <> ")" alias_names -> "provide required attr `" <> attribute <> "` or migrate from alias(es): " <> join_with(",", alias_names) } } fn unknown_attribute_hint( attribute: String, aliases: List(AttrAlias), schema: template_compiler.Schema(assigns, slots), ) -> String { case find_alias(aliases, attribute) { Some(alias_rule) -> "rename `" <> attribute <> "` to `" <> alias_rule.canonical <> "` (" <> alias_rule.migration_hint <> ")" None -> case first_similar_attr(attribute, schema.attrs) { Some(name) -> "unknown attr `" <> attribute <> "`, did you mean `" <> name <> "`?" None -> "unknown attr `" <> attribute <> "`, supported attrs: " <> join_with(",", schema_attr_names(schema.attrs)) } } } fn missing_slot_hint(slot: String, defaults: List(SlotDefault)) -> String { case slot_default_exists(defaults, slot) { True -> "slot `" <> slot <> "` is required; add it or keep default through `slot_default`" False -> "add required slot `" <> slot <> "`" } } fn unknown_slot_hint( slot: String, schema: template_compiler.Schema(assigns, slots), ) -> String { "unknown slot `" <> slot <> "`, supported slots: " <> join_with(",", schema_slot_names(schema.slots)) } fn expected_helper_label(kind: template_compiler.AttrKind) -> String { case kind { template_compiler.AttrString -> "string_attr" template_compiler.AttrInt -> "int_attr" template_compiler.AttrBool -> "bool_attr" } } fn compile_signature( compiled: template_compiler.CompiledTemplate(assigns, slots), applied_aliases: List(#(String, String)), applied_defaults: List(String), ) -> String { let alias_signature = case applied_aliases { [] -> "none" _ -> join_with( ",", list.map(applied_aliases, fn(value) { let #(alias, canonical) = value alias <> "->" <> canonical }), ) } let defaults_signature = case applied_defaults { [] -> "none" _ -> join_with(",", applied_defaults) } template_compiler.signature(compiled) <> "|aliases=" <> alias_signature <> "|defaults=" <> defaults_signature } fn find_alias(aliases: List(AttrAlias), name: String) -> Option(AttrAlias) { case aliases { [] -> None [alias_rule, ..rest] -> case alias_rule.alias == name { True -> Some(alias_rule) False -> find_alias(rest, name) } } } fn aliases_for_canonical( aliases: List(AttrAlias), canonical: String, ) -> List(String) { aliases_for_canonical_loop(aliases, canonical, []) } fn aliases_for_canonical_loop( aliases: List(AttrAlias), canonical: String, acc_rev: List(String), ) -> List(String) { case aliases { [] -> list.reverse(acc_rev) [alias_rule, ..rest] -> case alias_rule.canonical == canonical { True -> aliases_for_canonical_loop(rest, canonical, [ alias_rule.alias, ..acc_rev ]) False -> aliases_for_canonical_loop(rest, canonical, acc_rev) } } } fn find_attr( attrs: List(#(String, template_compiler.AttrValue)), name: String, ) -> Option(template_compiler.AttrValue) { case attrs { [] -> None [#(entry_name, entry_value), ..rest] -> case entry_name == name { True -> Some(entry_value) False -> find_attr(rest, name) } } } fn first_similar_attr( _name: String, attrs: List(template_compiler.AttrSchema), ) -> Option(String) { case attrs { [] -> None [attr, ..] -> Some(attr.name) } } fn slot_default_exists(defaults: List(SlotDefault), name: String) -> Bool { case defaults { [] -> False [default, ..rest] -> case default.name == name { True -> True False -> slot_default_exists(rest, name) } } } fn slot_exists( slots: List(#(String, component.Rendered)), expected: String, ) -> Bool { case slots { [] -> False [#(name, _), ..rest] -> case name == expected { True -> True False -> slot_exists(rest, expected) } } } fn schema_attr_names( attrs: List(template_compiler.AttrSchema), ) -> List(String) { attrs |> list.map(fn(attr) { attr.name }) } fn schema_slot_names( slots: List(template_compiler.SlotSchema), ) -> List(String) { slots |> list.map(fn(slot) { slot.name }) } fn value_kind_label(value: template_compiler.AttrValue) -> String { case value { template_compiler.StringAttr(_) -> "string" template_compiler.IntAttr(_) -> "int" template_compiler.BoolAttr(_) -> "bool" } } fn join_with(separator: String, values: List(String)) -> String { case values { [] -> "" [value] -> value [value, ..rest] -> value <> separator <> join_with(separator, rest) } }