♻️ Atributos class e id explícitos en html!

Se usa `class="..."` e `id="..."` en lugar de `div.a #x`. El HTML
generado es idéntico.
This commit is contained in:
Manuel Cillero 2026-09-26 07:05:59 +02:00
parent 58d1fef4f1
commit 3f3634fdab
21 changed files with 80 additions and 74 deletions

View file

@ -53,14 +53,14 @@ impl Component for AdminFrame {
Ok(html! { Ok(html! {
(breadcrumbs) (breadcrumbs)
header.admin-header { header class="admin-header" {
h1.admin-page-title { (self.title().using(cx)) } h1 class="admin-page-title" { (self.title().using(cx)) }
(local_acts) (local_acts)
} }
@if !local_tasks.0.is_empty() { @if !local_tasks.0.is_empty() {
nav.admin-local-tasks { (local_tasks) } nav class="admin-local-tasks" { (local_tasks) }
} }
div.admin-content { div class="admin-content" {
(body) (body)
} }
}) })
@ -117,7 +117,7 @@ fn render_local_tasks(cx: &Context, current_path: &str) -> Markup {
return html! {}; return html! {};
} }
html! { html! {
ul.admin-tasks-list { ul class="admin-tasks-list" {
@for task in tasks { @for task in tasks {
@let is_active = current_path == task.path; @let is_active = current_path == task.path;
li class=(if is_active { "admin-task admin-task-active" } else { "admin-task" }) { li class=(if is_active { "admin-task admin-task-active" } else { "admin-task" }) {
@ -137,10 +137,12 @@ fn render_local_actions(cx: &Context, current_path: &str) -> Markup {
return html! {}; return html! {};
} }
html! { html! {
ul.admin-actions-list { ul class="admin-actions-list" {
@for action in actions { @for action in actions {
li.admin-action { li class="admin-action" {
a.admin-action-link href=(cx.route(action.url.as_str())) { (action.title.using(cx)) } a class="admin-action-link" href=(cx.route(action.url.as_str())) {
(action.title.using(cx))
}
} }
} }
} }

View file

@ -98,20 +98,20 @@ impl Component for ConfigForm {
Ok(html! { Ok(html! {
@if *self.saved() { @if *self.saved() {
div.admin-message."admin-message-ok" { (&saved_msg) } div class="admin-message admin-message-ok" { (&saved_msg) }
} }
@if *self.error() { @if *self.error() {
div.admin-message."admin-message-error" { (&error_msg) } div class="admin-message admin-message-error" { (&error_msg) }
} }
form.admin-config-form method="post" action=(action) { form class="admin-config-form" method="post" action=(action) {
@for (i, field) in schema.fields().iter().enumerate() { @for (i, field) in schema.fields().iter().enumerate() {
@let pf = &prepared[i]; @let pf = &prepared[i];
div.admin-form-field { div class="admin-form-field" {
label.admin-form-label for=(field.name()) { (field.label()) } label class="admin-form-label" for=(field.name()) { (field.label()) }
@match field.field_type() { @match field.field_type() {
SettingFieldType::Text { max_length } => { SettingFieldType::Text { max_length } => {
@if let Some(max) = max_length { @if let Some(max) = max_length {
input.admin-form-input input class="admin-form-input"
type="text" type="text"
id=(field.name()) id=(field.name())
name=(field.name()) name=(field.name())
@ -119,7 +119,7 @@ impl Component for ConfigForm {
maxlength=(max) maxlength=(max)
required[*field.required()]; required[*field.required()];
} @else { } @else {
input.admin-form-input input class="admin-form-input"
type="text" type="text"
id=(field.name()) id=(field.name())
name=(field.name()) name=(field.name())
@ -129,7 +129,7 @@ impl Component for ConfigForm {
} }
SettingFieldType::Number { min, max } => { SettingFieldType::Number { min, max } => {
@if let (Some(lo), Some(hi)) = (min, max) { @if let (Some(lo), Some(hi)) = (min, max) {
input.admin-form-input input class="admin-form-input"
type="number" type="number"
id=(field.name()) id=(field.name())
name=(field.name()) name=(field.name())
@ -138,7 +138,7 @@ impl Component for ConfigForm {
max=(hi) max=(hi)
required[*field.required()]; required[*field.required()];
} @else if let Some(lo) = min { } @else if let Some(lo) = min {
input.admin-form-input input class="admin-form-input"
type="number" type="number"
id=(field.name()) id=(field.name())
name=(field.name()) name=(field.name())
@ -146,7 +146,7 @@ impl Component for ConfigForm {
min=(lo) min=(lo)
required[*field.required()]; required[*field.required()];
} @else if let Some(hi) = max { } @else if let Some(hi) = max {
input.admin-form-input input class="admin-form-input"
type="number" type="number"
id=(field.name()) id=(field.name())
name=(field.name()) name=(field.name())
@ -154,7 +154,7 @@ impl Component for ConfigForm {
max=(hi) max=(hi)
required[*field.required()]; required[*field.required()];
} @else { } @else {
input.admin-form-input input class="admin-form-input"
type="number" type="number"
id=(field.name()) id=(field.name())
name=(field.name()) name=(field.name())
@ -163,7 +163,7 @@ impl Component for ConfigForm {
} }
} }
SettingFieldType::Boolean => { SettingFieldType::Boolean => {
input.admin-form-checkbox input class="admin-form-checkbox"
type="checkbox" type="checkbox"
id=(field.name()) id=(field.name())
name=(field.name()) name=(field.name())
@ -171,7 +171,7 @@ impl Component for ConfigForm {
checked[pf.checked]; checked[pf.checked];
} }
SettingFieldType::Select { options } => { SettingFieldType::Select { options } => {
select.admin-form-select select class="admin-form-select"
id=(field.name()) id=(field.name())
name=(field.name()) name=(field.name())
required[*field.required()] required[*field.required()]
@ -187,12 +187,12 @@ impl Component for ConfigForm {
} }
} }
@if let Some(help) = field.help_text() { @if let Some(help) = field.help_text() {
small.admin-form-help { (help) } small class="admin-form-help" { (help) }
} }
} }
} }
div.admin-form-actions { div class="admin-form-actions" {
button.admin-btn."admin-btn-primary" type="submit" { button class="admin-btn admin-btn-primary" type="submit" {
(&save_label) (&save_label)
} }
} }

View file

@ -58,7 +58,7 @@ async fn homepage(request: HttpRequest) -> Result<Markup, ErrorPage> {
button hx-get="/api/hello" hx-target="#result" { button hx-get="/api/hello" hx-target="#result" {
"Say hello" "Say hello"
} }
div #result {} div id="result" {}
})) }))
.render().await .render().await
} }
@ -79,7 +79,7 @@ async fn homepage(request: HttpRequest) -> Result<Markup, ErrorPage> {
Page::new(request) Page::new(request)
.with_child(Html::with(move |cx| html! { .with_child(Html::with(move |cx| html! {
button (props.unpack(cx)) { "Say hello" } button (props.unpack(cx)) { "Say hello" }
div #result {} div id="result" {}
})) }))
.render().await .render().await
} }

View file

@ -59,7 +59,7 @@ async fn homepage(request: HttpRequest) -> Result<Markup, ErrorPage> {
button hx-get="/api/hello" hx-target="#result" { button hx-get="/api/hello" hx-target="#result" {
"Say hello" "Say hello"
} }
div #result {} div id="result" {}
})) }))
.render().await .render().await
} }
@ -80,7 +80,7 @@ async fn homepage(request: HttpRequest) -> Result<Markup, ErrorPage> {
Page::new(request) Page::new(request)
.with_child(Html::with(move |cx| html! { .with_child(Html::with(move |cx| html! {
button (props.unpack(cx)) { "Say hello" } button (props.unpack(cx)) { "Say hello" }
div #result {} div id="result" {}
})) }))
.render().await .render().await
} }

View file

@ -21,7 +21,7 @@ use crate::hx;
/// use pagetop_htmx::prelude::*; /// use pagetop_htmx::prelude::*;
/// ///
/// async fn add_item() -> impl IntoResponse { /// async fn add_item() -> impl IntoResponse {
/// let new_item = html! { li #item-42 { "New item" } }; /// let new_item = html! { li id="item-42" { "New item" } };
/// ///
/// HtmxResponse::new(new_item) /// HtmxResponse::new(new_item)
/// .retarget("#list") /// .retarget("#list")
@ -123,7 +123,7 @@ impl HtmxResponse {
/// ///
/// # fn build_response(table: Markup, roles_count: i64) -> HtmxResponse { /// # fn build_response(table: Markup, roles_count: i64) -> HtmxResponse {
/// let counter = html! { /// let counter = html! {
/// span #roles-count hx-swap-oob="true" { (roles_count) } /// span id="roles-count" hx-swap-oob="true" { (roles_count) }
/// }; /// };
/// HtmxResponse::new(table).oob(counter) /// HtmxResponse::new(table).oob(counter)
/// # } /// # }

View file

@ -15,7 +15,7 @@ fn header<'a>(response: &'a web::Response, name: &str) -> Option<&'a str> {
#[pagetop::test] #[pagetop::test]
async fn new_renders_the_given_markup_with_an_html_content_type() { async fn new_renders_the_given_markup_with_an_html_content_type() {
let response = HtmxResponse::new(html! { li #item-42 { "New item" } }).into_response(); let response = HtmxResponse::new(html! { li id="item-42" { "New item" } }).into_response();
assert_eq!( assert_eq!(
header(&response, "content-type"), header(&response, "content-type"),
@ -41,8 +41,8 @@ async fn empty_has_no_body_but_keeps_the_html_content_type() {
#[pagetop::test] #[pagetop::test]
async fn oob_appends_markup_after_the_main_body() { async fn oob_appends_markup_after_the_main_body() {
let response = HtmxResponse::new(html! { li #item-42 { "New item" } }) let response = HtmxResponse::new(html! { li id="item-42" { "New item" } })
.oob(html! { span #item-count hx-swap-oob="true" { "1" } }) .oob(html! { span id="item-count" hx-swap-oob="true" { "1" } })
.into_response(); .into_response();
let body = web::test::read_body_text(response).await; let body = web::test::read_body_text(response).await;
@ -58,8 +58,8 @@ async fn oob_appends_markup_after_the_main_body() {
#[pagetop::test] #[pagetop::test]
async fn oob_can_be_called_several_times_to_accumulate_fragments() { async fn oob_can_be_called_several_times_to_accumulate_fragments() {
let response = HtmxResponse::new(html! { p { "Main" } }) let response = HtmxResponse::new(html! { p { "Main" } })
.oob(html! { span #a hx-swap-oob="true" { "A" } }) .oob(html! { span id="a" hx-swap-oob="true" { "A" } })
.oob(html! { span #b hx-swap-oob="true" { "B" } }) .oob(html! { span id="b" hx-swap-oob="true" { "B" } })
.into_response(); .into_response();
let body = web::test::read_body_text(response).await; let body = web::test::read_body_text(response).await;
@ -76,7 +76,7 @@ async fn oob_can_be_called_several_times_to_accumulate_fragments() {
#[pagetop::test] #[pagetop::test]
async fn oob_works_from_an_empty_response() { async fn oob_works_from_an_empty_response() {
let response = HtmxResponse::empty() let response = HtmxResponse::empty()
.oob(html! { span #item-count hx-swap-oob="true" { "0" } }) .oob(html! { span id="item-count" hx-swap-oob="true" { "0" } })
.into_response(); .into_response();
let body = web::test::read_body_text(response).await; let body = web::test::read_body_text(response).await;

View file

@ -22,7 +22,7 @@ use pagetop::prelude::*;
// formularios de autenticación y por los de administración. // formularios de autenticación y por los de administración.
pub(crate) fn error_banner(error: Option<Lc>) -> Html { pub(crate) fn error_banner(error: Option<Lc>) -> Html {
Html::with(move |cx| match &error { Html::with(move |cx| match &error {
Some(e) => html! { div.user-form-error role="alert" { (e.clone().using(cx)) } }, Some(e) => html! { div class="user-form-error" role="alert" { (e.clone().using(cx)) } },
None => html! {}, None => html! {},
}) })
} }

View file

@ -111,13 +111,13 @@ impl Component for RoleTable {
Ok(html! { Ok(html! {
div (self.props().unpack(cx)) { div (self.props().unpack(cx)) {
div.user-admin-actions { div class="user-admin-actions" {
a href=(new_href) { a href=(new_href) {
(Lc::t("btn-create-role", &LOCALES_USER).using(cx)) (Lc::t("btn-create-role", &LOCALES_USER).using(cx))
} }
} }
@if let Some(message) = self.message() { @if let Some(message) = self.message() {
div.user-form-error role="alert" { (message.clone().using(cx)) } div class="user-form-error" role="alert" { (message.clone().using(cx)) }
} }
(table.render(cx).await) (table.render(cx).await)
(pager) (pager)

View file

@ -87,7 +87,7 @@ impl Component for UserTable {
Ok(html! { Ok(html! {
div (self.props().unpack(cx)) { div (self.props().unpack(cx)) {
div.user-admin-actions { div class="user-admin-actions" {
a href=(new_href) { a href=(new_href) {
(Lc::t("btn-create-user", &LOCALES_USER).using(cx)) (Lc::t("btn-create-user", &LOCALES_USER).using(cx))
} }

View file

@ -69,8 +69,8 @@ impl Component for LoginForm {
let form = form.render(cx).await; let form = form.render(cx).await;
Ok(html! { Ok(html! {
div.user-login-page { div class="user-login-page" {
div.user-login-card { div class="user-login-card" {
(form) (form)
} }
} }
@ -82,13 +82,13 @@ fn links(allow_registration: bool) -> Html {
Html::with(move |cx| { Html::with(move |cx| {
html! { html! {
@if allow_registration { @if allow_registration {
p.user-register-link { p class="user-register-link" {
a href=(cx.route(REGISTER_PATH)) { a href=(cx.route(REGISTER_PATH)) {
(Lc::t("link-register", &LOCALES_USER).using(cx)) (Lc::t("link-register", &LOCALES_USER).using(cx))
} }
} }
} }
p.user-reset-link { p class="user-reset-link" {
a href=(cx.route(PASSWORD_RESET_PATH)) { a href=(cx.route(PASSWORD_RESET_PATH)) {
(Lc::t("link-forgot-password", &LOCALES_USER).using(cx)) (Lc::t("link-forgot-password", &LOCALES_USER).using(cx))
} }

View file

@ -19,10 +19,10 @@ impl Component for UserBlock {
Ok(if user.is_authenticated() { Ok(if user.is_authenticated() {
let display = user.display_name().unwrap_or("?"); let display = user.display_name().unwrap_or("?");
html! { html! {
nav.user-block { nav class="user-block" {
span.user-name { (display) } span class="user-name" { (display) }
" · " " · "
form.user-logout-inline method="post" action=(cx.route(LOGOUT_PATH)) { form class="user-logout-inline" method="post" action=(cx.route(LOGOUT_PATH)) {
button type="submit" { button type="submit" {
(Lc::t("btn-logout", &LOCALES_USER).using(cx)) (Lc::t("btn-logout", &LOCALES_USER).using(cx))
} }
@ -31,7 +31,7 @@ impl Component for UserBlock {
} }
} else { } else {
html! { html! {
nav.user-block { nav class="user-block" {
a href=(cx.route(LOGIN_PATH)) { a href=(cx.route(LOGIN_PATH)) {
(Lc::t("btn-login", &LOCALES_USER).using(cx)) (Lc::t("btn-login", &LOCALES_USER).using(cx))
} }

View file

@ -119,7 +119,7 @@ async fn profile_roles(roles: &[role::Model], cx: &mut Context) -> Block {
@if items.is_empty() { @if items.is_empty() {
"-" "-"
} @else { } @else {
ul.user-profile-roles { ul class="user-profile-roles" {
@for (label, system_badge) in &items { @for (label, system_badge) in &items {
li { li {
(label.as_str()) (label.as_str())

View file

@ -51,7 +51,7 @@ pub(crate) fn back_link(href: impl Into<RoutePath>) -> Html {
let href = href.into(); let href = href.into();
Html::with(move |cx| { Html::with(move |cx| {
html! { html! {
p.user-admin-back-link { p class="user-admin-back-link" {
a href=(href.clone()) { (Lc::t("link-back-to-list", &LOCALES_USER).using(cx)) } a href=(href.clone()) { (Lc::t("link-back-to-list", &LOCALES_USER).using(cx)) }
} }
} }

View file

@ -26,12 +26,12 @@ pub(crate) async fn list_get(request: HttpRequest) -> Result<Response, ErrorPage
content = content.with_child(Block::new().with_title(group_label.clone()).with_child( content = content.with_child(Block::new().with_title(group_label.clone()).with_child(
Html::with(move |cx| { Html::with(move |cx| {
html! { html! {
table.user-admin-table { table class="user-admin-table" {
tbody { tbody {
@for (key, label) in &items { @for (key, label) in &items {
tr { tr {
td { (label.using(cx)) } td { (label.using(cx)) }
td.user-admin-permission-key { (key) } td class="user-admin-permission-key" { (key) }
} }
} }
} }

View file

@ -361,12 +361,12 @@ fn role_view_permissions(groups: &PermissionGroups) -> Vec<Block> {
.with_title(group_label.clone()) .with_title(group_label.clone())
.with_child(Html::with(move |cx| { .with_child(Html::with(move |cx| {
html! { html! {
ul.user-admin-permission-list { ul class="user-admin-permission-list" {
@for (_key, label, granted) in &perms { @for (_key, label, granted) in &perms {
@if *granted { @if *granted {
li.user-admin-permission-granted { (label.using(cx)) } li class="user-admin-permission-granted" { (label.using(cx)) }
} @else { } @else {
li.user-admin-permission-missing { (label.using(cx)) } li class="user-admin-permission-missing" { (label.using(cx)) }
} }
} }
} }

View file

@ -92,7 +92,7 @@ fn search_bar(current_query: Option<String>) -> Html {
let value = current_query.unwrap_or_default(); let value = current_query.unwrap_or_default();
Html::with(move |cx| { Html::with(move |cx| {
html! { html! {
div.user-admin-search { div class="user-admin-search" {
input input
type="search" type="search"
id="user-admin-search-input" id="user-admin-search-input"
@ -588,7 +588,7 @@ async fn user_view_roles(roles: &[role::Model], cx: &mut Context) -> Block {
@if items.is_empty() { @if items.is_empty() {
"-" "-"
} @else { } @else {
table.user-admin-table { table class="user-admin-table" {
tbody { tbody {
@for (id, machine_name, label, system_badge) in &items { @for (id, machine_name, label, system_badge) in &items {
@let href = cx.route(role_path(*id, "view")).to_string(); @let href = cx.route(role_path(*id, "view")).to_string();

View file

@ -57,7 +57,7 @@ impl Component for Breadcrumb {
Ok(html! { Ok(html! {
nav (self.props().unpack(cx)) aria-label=[Lc::l("breadcrumb_label").lookup(cx)] { nav (self.props().unpack(cx)) aria-label=[Lc::l("breadcrumb_label").lookup(cx)] {
ol.breadcrumb { ol class="breadcrumb" {
@for crumb in self.crumbs() { @for crumb in self.crumbs() {
(crumb.render_crumb(cx)) (crumb.render_crumb(cx))
} }

View file

@ -77,7 +77,7 @@ impl Component for Template {
} }
Ok(html! { Ok(html! {
div.wrapper { div class="wrapper" {
(body) (body)
} }
}) })

View file

@ -240,7 +240,7 @@ impl Component for Pager {
@let items_per_page = self.items_per_page().max(1); @let items_per_page = self.items_per_page().max(1);
@let first = (page - 1) * items_per_page + 1; @let first = (page - 1) * items_per_page + 1;
@let last = (page * items_per_page).min(self.total_items()); @let last = (page * items_per_page).min(self.total_items());
span.pager-summary { span class="pager-summary" {
(Lc::l("pager_summary") (Lc::l("pager_summary")
.with_arg("first", first.to_string()) .with_arg("first", first.to_string())
.with_arg("last", last.to_string()) .with_arg("last", last.to_string())
@ -248,14 +248,16 @@ impl Component for Pager {
.using(cx)) .using(cx))
} }
} }
ul.pagination { ul class="pagination" {
@if show_prev_next { @if show_prev_next {
li.page-item.page-previous.disabled[first_disabled] { li.page-item.page-previous.disabled[first_disabled] {
a.page-link a class="page-link"
href=[(!first_disabled).then(|| Self::page_route(&route, page - 1))] href=[(!first_disabled).then(|| Self::page_route(&route, page - 1))]
aria-disabled=[first_disabled.then_some("true")] aria-disabled=[first_disabled.then_some("true")]
aria-label=[Lc::l("pager_previous_aria_label").lookup(cx)] { aria-label=[Lc::l("pager_previous_aria_label").lookup(cx)] {
span.page-link-icon { (Lc::l("pager_previous_label").using(cx)) } span class="page-link-icon" {
(Lc::l("pager_previous_label").using(cx))
}
} }
} }
} }
@ -264,7 +266,7 @@ impl Component for Pager {
PageItem::Number(n) => { PageItem::Number(n) => {
@let href = Self::page_route(&route, n); @let href = Self::page_route(&route, n);
li.page-item.active[n == page] { li.page-item.active[n == page] {
a.page-link a class="page-link"
href=(href) href=(href)
aria-current=[(n == page).then_some("page")] { aria-current=[(n == page).then_some("page")] {
(n.to_string()) (n.to_string())
@ -272,19 +274,21 @@ impl Component for Pager {
} }
} }
PageItem::Ellipsis => { PageItem::Ellipsis => {
li.page-item.page-ellipsis.disabled aria-hidden="true" { li class="page-item page-ellipsis disabled" aria-hidden="true" {
span.page-link { "…" } span class="page-link" { "…" }
} }
} }
} }
} }
@if show_prev_next { @if show_prev_next {
li.page-item.page-next.disabled[last_disabled] { li.page-item.page-next.disabled[last_disabled] {
a.page-link a class="page-link"
href=[(!last_disabled).then(|| Self::page_route(&route, page + 1))] href=[(!last_disabled).then(|| Self::page_route(&route, page + 1))]
aria-disabled=[last_disabled.then_some("true")] aria-disabled=[last_disabled.then_some("true")]
aria-label=[Lc::l("pager_next_aria_label").lookup(cx)] { aria-label=[Lc::l("pager_next_aria_label").lookup(cx)] {
span.page-link-icon { (Lc::l("pager_next_label").using(cx)) } span class="page-link-icon" {
(Lc::l("pager_next_label").using(cx))
}
} }
} }
} }

View file

@ -80,7 +80,7 @@ impl Component for Table {
let colspan = self.columns().len().max(1).to_string(); let colspan = self.columns().len().max(1).to_string();
Ok(html! { Ok(html! {
div.table-responsive { div class="table-responsive" {
table (self.props().unpack(cx)) { table (self.props().unpack(cx)) {
@if !self.columns().is_empty() { @if !self.columns().is_empty() {
thead { thead {
@ -108,7 +108,7 @@ impl Component for Table {
{ {
tbody { tbody {
tr { tr {
td.table-empty colspan=(colspan) { (empty) } td class="table-empty" colspan=(colspan) { (empty) }
} }
} }
} }

View file

@ -312,8 +312,8 @@ pub trait Theme: Extension + Send + Sync {
/// cx: &mut Context, /// cx: &mut Context,
/// ) -> Option<Result<Markup, ComponentError>> { /// ) -> Option<Result<Markup, ComponentError>> {
/// render_component!(component, { /// render_component!(component, {
/// Button => |btn| Ok(html! { button.btn.btn-primary { (btn.label()) } }), /// Button => |btn| Ok(html! { button class="btn btn-primary" { (btn.label()) } }),
/// Heading => |h| Ok(html! { h2.display-4 { (h.text()) } }), /// Heading => |h| Ok(html! { h2 class="display-4" { (h.text()) } }),
/// }) /// })
/// } /// }
/// ``` /// ```
@ -487,13 +487,13 @@ macro_rules! setup_component {
/// cx: &mut Context, /// cx: &mut Context,
/// ) -> Option<Result<Markup, ComponentError>> { /// ) -> Option<Result<Markup, ComponentError>> {
/// render_component!(component, { /// render_component!(component, {
/// Button => |btn| { Ok(html! { button.btn.btn-primary { (btn.label()) } }) }, /// Button => |btn| { Ok(html! { button class="btn btn-primary" { (btn.label()) } }) },
/// Heading => |h| self.render_heading(h, cx), /// Heading => |h| self.render_heading(h, cx),
/// }) /// })
/// } /// }
/// ///
/// fn render_heading(&self, h: &Heading, cx: &mut Context) -> Result<Markup, ComponentError> { /// fn render_heading(&self, h: &Heading, cx: &mut Context) -> Result<Markup, ComponentError> {
/// Ok(html! { h2.display-4 { (h.text()) } }) /// Ok(html! { h2 class="display-4" { (h.text()) } })
/// } /// }
/// ``` /// ```
/// ///