Skip to main content

lumen_ir/
values.rs

1//! Shared value parsers used by both the HTML and CSS parsers.
2//!
3//! Each returns `Result<T, ParseError>`; on failure `ParseError::BadAttribute`
4//! carries the calling context (tag-name in HTML, selector text in CSS),
5//! the property name, and the raw value, so error messages are useful
6//! either side of the parser boundary.
7
8use crate::layout_ir::{Edges, LengthSpec, ParseError, Rgba};
9
10/// Length: `auto` | `<n>` | `<n>px` | `<n>%`.
11pub fn parse_length(ctx: &str, name: &str, value: &str) -> Result<LengthSpec, ParseError> {
12    let v = value.trim();
13    if v == "auto" {
14        return Ok(LengthSpec::Auto);
15    }
16    if let Some(rest) = v.strip_suffix('%') {
17        return Ok(LengthSpec::Percent(parse_num(ctx, name, value, rest)?));
18    }
19    if let Some(rest) = v.strip_suffix("px") {
20        return Ok(LengthSpec::Px(parse_num(ctx, name, value, rest)?));
21    }
22    Ok(LengthSpec::Px(parse_num(ctx, name, value, v)?))
23}
24
25/// `#rrggbb` or `#rrggbbaa`.
26pub fn parse_color(ctx: &str, name: &str, value: &str) -> Result<Rgba, ParseError> {
27    let v = value.trim();
28    let body = v
29        .strip_prefix('#')
30        .ok_or_else(|| bad(ctx, name, value, "expected '#rrggbb' or '#rrggbbaa'".into()))?;
31    if body.len() != 6 && body.len() != 8 {
32        return Err(bad(
33            ctx,
34            name,
35            value,
36            "expected '#rrggbb' or '#rrggbbaa'".into(),
37        ));
38    }
39    let pair = |i: usize| -> Result<u8, ParseError> {
40        u8::from_str_radix(&body[i..i + 2], 16)
41            .map_err(|e| bad(ctx, name, value, format!("bad hex: {e}")))
42    };
43    let r = pair(0)? as f32 / 255.0;
44    let g = pair(2)? as f32 / 255.0;
45    let b = pair(4)? as f32 / 255.0;
46    let a = if body.len() == 8 {
47        pair(6)? as f32 / 255.0
48    } else {
49        1.0
50    };
51    Ok(Rgba { r, g, b, a })
52}
53
54/// Parse a `bg=` value: a hex color, a gradient function, or an image named
55/// with `url(...)`. Gradient stops accept the CSS forms `<color>`
56/// (auto-distributed) or `<color> <offset%>` (explicit). Parsed stops are
57/// sorted by offset ascending so the renderer can hand them straight to
58/// peniko.
59pub fn parse_bg(
60    ctx: &str,
61    name: &str,
62    value: &str,
63) -> Result<crate::layout_ir::BgSpec, ParseError> {
64    use crate::layout_ir::BgSpec;
65    let trimmed = value.trim();
66    if let Some(path) = parse_url(ctx, name, value)? {
67        return Ok(BgSpec::Image(path));
68    }
69    if let Some(inner) = trimmed
70        .strip_prefix("linear-gradient(")
71        .and_then(|s| s.strip_suffix(')'))
72    {
73        return parse_linear_gradient(ctx, name, value, inner).map(|(angle, stops)| {
74            BgSpec::Linear {
75                angle_deg: angle,
76                stops,
77            }
78        });
79    }
80    if let Some(inner) = trimmed
81        .strip_prefix("radial-gradient(")
82        .and_then(|s| s.strip_suffix(')'))
83    {
84        return parse_radial_gradient(ctx, name, value, inner)
85            .map(|(radius, stops)| BgSpec::Radial { radius, stops });
86    }
87    if let Some(inner) = trimmed
88        .strip_prefix("conic-gradient(")
89        .and_then(|s| s.strip_suffix(')'))
90    {
91        return parse_conic_gradient(ctx, name, value, inner)
92            .map(|(from_deg, stops)| BgSpec::Conic { from_deg, stops });
93    }
94    Ok(BgSpec::Solid(parse_color(ctx, name, value)?))
95}
96
97/// The path inside a CSS `url(...)`, or `None` when `value` is not a
98/// `url()` at all. The path may be double-quoted, single-quoted, or bare;
99/// an empty one is an error.
100pub fn parse_url(ctx: &str, name: &str, value: &str) -> Result<Option<String>, ParseError> {
101    let trimmed = value.trim();
102    let Some(inner) = trimmed
103        .get(..4)
104        .filter(|head| head.eq_ignore_ascii_case("url("))
105        .and_then(|_| trimmed[4..].strip_suffix(')'))
106    else {
107        return Ok(None);
108    };
109    let inner = inner.trim();
110    let path = ['"', '\'']
111        .iter()
112        .find_map(|q| {
113            inner
114                .strip_prefix(*q)
115                .and_then(|rest| rest.strip_suffix(*q))
116        })
117        .unwrap_or(inner);
118    if path.is_empty() {
119        return Err(bad(ctx, name, value, "url() names no file".into()));
120    }
121    Ok(Some(path.to_string()))
122}
123
124/// An image fit: `fill`, `cover`, `contain`, `none`, or `scale-down`. The
125/// `fit` of an `<image>` and the `bg-fit` of a background image take the same
126/// values.
127pub fn parse_image_fit(
128    ctx: &str,
129    name: &str,
130    value: &str,
131) -> Result<crate::layout_ir::ImageFitSpec, ParseError> {
132    use crate::layout_ir::ImageFitSpec;
133    Ok(match value.trim() {
134        "fill" => ImageFitSpec::Fill,
135        "cover" => ImageFitSpec::Cover,
136        "contain" => ImageFitSpec::Contain,
137        "none" => ImageFitSpec::None,
138        "scale-down" => ImageFitSpec::ScaleDown,
139        other => {
140            return Err(bad(
141                ctx,
142                name,
143                value,
144                format!(
145                    "unknown fit '{other}' (supported: fill, cover, contain, none, scale-down)"
146                ),
147            ));
148        }
149    })
150}
151
152/// Parses `radial-gradient(<color1>, <color2 [stop%]>, ...)`. Every term is a colour stop; the radius is always 1.0.
153/// The centre is fixed at 50% / 50%; explicit position and ellipse shapes are not supported.
154fn parse_radial_gradient(
155    ctx: &str,
156    name: &str,
157    full_value: &str,
158    inner: &str,
159) -> Result<(f32, Vec<(f32, crate::layout_ir::Rgba)>), ParseError> {
160    let parts: Vec<&str> = inner
161        .split(',')
162        .map(|p| p.trim())
163        .filter(|p| !p.is_empty())
164        .collect();
165    if parts.len() < 2 {
166        return Err(bad(
167            ctx,
168            name,
169            full_value,
170            "radial-gradient needs at least two color stops".into(),
171        ));
172    }
173    let n = parts.len();
174    let mut stops: Vec<(f32, crate::layout_ir::Rgba)> = Vec::with_capacity(n);
175    for (i, raw) in parts.iter().enumerate() {
176        let mut bits = raw.split_whitespace();
177        let color_str = bits
178            .next()
179            .ok_or_else(|| bad(ctx, name, full_value, format!("empty stop #{}", i + 1)))?;
180        let color = parse_color(ctx, name, color_str)?;
181        let offset = match bits.next() {
182            Some(s) => parse_offset(ctx, name, full_value, s)?,
183            None => i as f32 / (n - 1).max(1) as f32,
184        };
185        stops.push((offset, color));
186    }
187    stops.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
188    Ok((1.0, stops))
189}
190
191/// `conic-gradient([from <angle>], <color1>, <color2>, ...)`.
192fn parse_conic_gradient(
193    ctx: &str,
194    name: &str,
195    full_value: &str,
196    inner: &str,
197) -> Result<(f32, Vec<(f32, crate::layout_ir::Rgba)>), ParseError> {
198    let parts: Vec<&str> = inner
199        .split(',')
200        .map(|p| p.trim())
201        .filter(|p| !p.is_empty())
202        .collect();
203    if parts.is_empty() {
204        return Err(bad(
205            ctx,
206            name,
207            full_value,
208            "conic-gradient needs at least one stop".into(),
209        ));
210    }
211    let (from_deg, stop_parts): (f32, &[&str]) = if let Some(rest) = parts[0].strip_prefix("from ")
212    {
213        let num = rest.trim().strip_suffix("deg").unwrap_or(rest.trim());
214        let v: f32 = num.trim().parse().map_err(|e| {
215            bad(
216                ctx,
217                name,
218                full_value,
219                format!("bad angle '{}': {e}", parts[0]),
220            )
221        })?;
222        (v, &parts[1..])
223    } else {
224        (0.0, &parts[..])
225    };
226    if stop_parts.len() < 2 {
227        return Err(bad(
228            ctx,
229            name,
230            full_value,
231            "conic-gradient needs at least two color stops".into(),
232        ));
233    }
234    let n = stop_parts.len();
235    let mut stops: Vec<(f32, crate::layout_ir::Rgba)> = Vec::with_capacity(n);
236    for (i, raw) in stop_parts.iter().enumerate() {
237        let mut bits = raw.split_whitespace();
238        let color_str = bits
239            .next()
240            .ok_or_else(|| bad(ctx, name, full_value, format!("empty stop #{}", i + 1)))?;
241        let color = parse_color(ctx, name, color_str)?;
242        let offset = match bits.next() {
243            Some(s) => parse_offset(ctx, name, full_value, s)?,
244            None => i as f32 / (n - 1).max(1) as f32,
245        };
246        stops.push((offset, color));
247    }
248    stops.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
249    Ok((from_deg, stops))
250}
251
252fn parse_linear_gradient(
253    ctx: &str,
254    name: &str,
255    full_value: &str,
256    inner: &str,
257) -> Result<(f32, Vec<(f32, crate::layout_ir::Rgba)>), ParseError> {
258    let parts: Vec<&str> = inner
259        .split(',')
260        .map(|p| p.trim())
261        .filter(|p| !p.is_empty())
262        .collect();
263    if parts.len() < 2 {
264        return Err(bad(
265            ctx,
266            name,
267            full_value,
268            "linear-gradient needs at least one direction and one stop".into(),
269        ));
270    }
271    let (angle_deg, stop_parts): (f32, &[&str]) = match parts[0].strip_suffix("deg") {
272        Some(num) => (
273            num.trim().parse::<f32>().map_err(|e| {
274                bad(
275                    ctx,
276                    name,
277                    full_value,
278                    format!("bad angle '{}': {e}", parts[0]),
279                )
280            })?,
281            &parts[1..],
282        ),
283        None => (180.0, &parts[..]), // default top-to-bottom
284    };
285    if stop_parts.len() < 2 {
286        return Err(bad(
287            ctx,
288            name,
289            full_value,
290            "linear-gradient needs at least two color stops".into(),
291        ));
292    }
293    let n = stop_parts.len();
294    let mut stops: Vec<(f32, crate::layout_ir::Rgba)> = Vec::with_capacity(n);
295    for (i, raw) in stop_parts.iter().enumerate() {
296        let mut bits = raw.split_whitespace();
297        let color_str = bits
298            .next()
299            .ok_or_else(|| bad(ctx, name, full_value, format!("empty stop #{}", i + 1)))?;
300        let color = parse_color(ctx, name, color_str)?;
301        let offset = match bits.next() {
302            Some(s) => parse_offset(ctx, name, full_value, s)?,
303            None => i as f32 / (n - 1).max(1) as f32,
304        };
305        stops.push((offset, color));
306    }
307    stops.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
308    Ok((angle_deg, stops))
309}
310
311fn parse_offset(ctx: &str, name: &str, full_value: &str, raw: &str) -> Result<f32, ParseError> {
312    if let Some(num) = raw.strip_suffix('%') {
313        let n: f32 = num
314            .trim()
315            .parse()
316            .map_err(|e: std::num::ParseFloatError| bad(ctx, name, full_value, e.to_string()))?;
317        Ok(n / 100.0)
318    } else {
319        raw.trim().parse::<f32>().map_err(|e| {
320            bad(
321                ctx,
322                name,
323                full_value,
324                format!("bad stop offset '{raw}': {e}"),
325            )
326        })
327    }
328}
329
330/// One edge term: `<n>` / `<n>px` (px) or `<n>%` (percent). Returned as
331/// `(px, pct)` - exactly one is meaningful: `pct = Some` => px is `0.0`
332/// and ignored downstream.
333fn parse_edge_term(
334    ctx: &str,
335    name: &str,
336    value: &str,
337    s: &str,
338) -> Result<(f32, Option<f32>), ParseError> {
339    let s = s.trim();
340    if let Some(rest) = s.strip_suffix('%') {
341        let p: f32 = rest
342            .trim()
343            .parse()
344            .map_err(|e: std::num::ParseFloatError| {
345                bad(ctx, name, value, format!("bad percent '{s}': {e}"))
346            })?;
347        return Ok((0.0, Some(p)));
348    }
349    Ok((parse_num(ctx, name, value, s)?, None))
350}
351
352/// Parse a CSS-style spacing shorthand, top-right-bottom-left.
353///
354/// * 1 value `n` -> every side `n`.
355/// * 2 values `v h` -> top/bottom = `v`, left/right = `h`.
356/// * 3 values `t h b` -> top, left/right, bottom.
357/// * 4 values `t r b l` -> explicit per-side.
358///
359/// Each term is `<n>` / `<n>px` / `<n>%` - percent terms land in the
360/// `pct_*` fields and resolve per CSS at layout time.
361pub fn parse_edges(ctx: &str, name: &str, value: &str) -> Result<Edges, ParseError> {
362    edges_with(
363        value,
364        |s| parse_edge_term(ctx, name, value, s),
365        |other| {
366            bad(
367                ctx,
368                name,
369                value,
370                format!("expected 1, 2, 3, or 4 numbers, got {other}"),
371            )
372        },
373    )
374}
375
376/// [`parse_edges`] for `inset`, where a term may also be `auto` (stored as
377/// `NaN`, the sentinel the layout backend reads as `auto`).
378pub fn parse_inset_edges(ctx: &str, name: &str, value: &str) -> Result<Edges, ParseError> {
379    edges_with(
380        value,
381        |s| {
382            if s.trim() == "auto" {
383                Ok((f32::NAN, None))
384            } else {
385                parse_edge_term(ctx, name, value, s)
386            }
387        },
388        |other| {
389            bad(
390                ctx,
391                name,
392                value,
393                format!("expected 1, 2, 3, or 4 numbers or `auto`, got {other}"),
394            )
395        },
396    )
397}
398
399/// One `inset` longhand term: a px number, or `auto` (`NaN`).
400pub fn parse_inset_term(ctx: &str, name: &str, value: &str) -> Result<f32, ParseError> {
401    if value.trim() == "auto" {
402        return Ok(f32::NAN);
403    }
404    parse_num(ctx, name, value, value.trim())
405}
406
407/// The top-right-bottom-left rotation shared by [`parse_edges`] and
408/// [`parse_inset_edges`]; `term` parses one side.
409fn edges_with(
410    value: &str,
411    term: impl Fn(&str) -> Result<(f32, Option<f32>), ParseError>,
412    arity: impl Fn(usize) -> ParseError,
413) -> Result<Edges, ParseError> {
414    let parts: Vec<&str> = value.split_whitespace().collect();
415    let n = |s: &str| term(s);
416    let (t, r, b, l) = match parts.len() {
417        1 => {
418            let v = n(parts[0])?;
419            (v, v, v, v)
420        }
421        2 => {
422            let v = n(parts[0])?;
423            let h = n(parts[1])?;
424            (v, h, v, h)
425        }
426        3 => {
427            let t = n(parts[0])?;
428            let h = n(parts[1])?;
429            let b = n(parts[2])?;
430            (t, h, b, h)
431        }
432        4 => (n(parts[0])?, n(parts[1])?, n(parts[2])?, n(parts[3])?),
433        other => return Err(arity(other)),
434    };
435    Ok(Edges {
436        top: t.0,
437        right: r.0,
438        bottom: b.0,
439        left: l.0,
440        pct_top: t.1,
441        pct_right: r.1,
442        pct_bottom: b.1,
443        pct_left: l.1,
444        ..Edges::default()
445    })
446}
447
448/// CSS border-width keyword / length term: `thin` (1px), `medium`
449/// (3px), `thick` (5px), or a px length. Percent is not valid CSS for
450/// border widths and is rejected.
451pub fn parse_border_width_term(
452    ctx: &str,
453    name: &str,
454    value: &str,
455    s: &str,
456) -> Result<f32, ParseError> {
457    match s.trim() {
458        "thin" => Ok(1.0),
459        "medium" => Ok(3.0),
460        "thick" => Ok(5.0),
461        t if t.ends_with('%') => Err(bad(
462            ctx,
463            name,
464            value,
465            "border widths cannot be percentages".into(),
466        )),
467        t => parse_num(ctx, name, value, t),
468    }
469}
470
471/// CSS `border-width` shorthand - 1-4 keyword/length terms in the
472/// standard top-right-bottom-left rotation.
473pub fn parse_border_width_edges(ctx: &str, name: &str, value: &str) -> Result<Edges, ParseError> {
474    let parts: Vec<&str> = value.split_whitespace().collect();
475    let n = |s: &str| parse_border_width_term(ctx, name, value, s);
476    match parts.len() {
477        1 => Ok(Edges::all(n(parts[0])?)),
478        2 => {
479            let v = n(parts[0])?;
480            let h = n(parts[1])?;
481            Ok(Edges {
482                top: v,
483                right: h,
484                bottom: v,
485                left: h,
486                ..Edges::default()
487            })
488        }
489        3 => {
490            let t = n(parts[0])?;
491            let h = n(parts[1])?;
492            let b = n(parts[2])?;
493            Ok(Edges {
494                top: t,
495                right: h,
496                bottom: b,
497                left: h,
498                ..Edges::default()
499            })
500        }
501        4 => Ok(Edges {
502            top: n(parts[0])?,
503            right: n(parts[1])?,
504            bottom: n(parts[2])?,
505            left: n(parts[3])?,
506            ..Edges::default()
507        }),
508        other => Err(bad(
509            ctx,
510            name,
511            value,
512            format!("expected 1, 2, 3, or 4 border widths, got {other}"),
513        )),
514    }
515}
516
517/// `border-style` keyword: `none` | `solid` (v1 subset). Other CSS
518/// styles (`dashed`, `dotted`, ...) are rejected so the declaration is
519/// skipped with a warning instead of silently misrendering.
520pub fn parse_border_style(
521    ctx: &str,
522    name: &str,
523    value: &str,
524) -> Result<crate::layout_ir::BorderStyleSpec, ParseError> {
525    use crate::layout_ir::BorderStyleSpec;
526    match value.trim() {
527        "none" | "hidden" => Ok(BorderStyleSpec::None),
528        "solid" => Ok(BorderStyleSpec::Solid),
529        other => Err(bad(
530            ctx,
531            name,
532            value,
533            format!("unsupported border-style '{other}' (supported: none, solid)"),
534        )),
535    }
536}
537
538/// Parsed result of a `border:`-family shorthand.
539pub struct BorderShorthand {
540    /// Width term, uniform on all sides. `None` = keep/default.
541    pub width: Option<f32>,
542    /// Style keyword. `None` = not authored in the shorthand.
543    pub style: Option<crate::layout_ir::BorderStyleSpec>,
544    /// Color term.
545    pub color: Option<crate::layout_ir::Rgba>,
546}
547
548/// CSS `border` shorthand: `<width> || <style> || <color>` in any order
549/// (e.g. `1px solid #444`). `border: none` clears the border.
550///
551/// Lenient extension: when the style keyword is omitted but a width or
552/// color is present (`border: 1px #444`), the style resolves to `solid`;
553/// the IR stores the normalized form, so a transpile target emits the
554/// explicit `1px solid #444` and renders identically in a real browser.
555pub fn parse_border_shorthand(
556    ctx: &str,
557    name: &str,
558    value: &str,
559) -> Result<BorderShorthand, ParseError> {
560    use crate::layout_ir::BorderStyleSpec;
561    let mut out = BorderShorthand {
562        width: None,
563        style: None,
564        color: None,
565    };
566    for tok in value.split_whitespace() {
567        match tok {
568            "none" | "hidden" => out.style = Some(BorderStyleSpec::None),
569            "solid" => out.style = Some(BorderStyleSpec::Solid),
570            "dashed" | "dotted" | "double" | "groove" | "ridge" | "inset" | "outset" => {
571                return Err(bad(
572                    ctx,
573                    name,
574                    value,
575                    format!("unsupported border-style '{tok}' (supported: none, solid)"),
576                ));
577            }
578            t if t.starts_with('#') => out.color = Some(parse_color(ctx, name, t)?),
579            t => out.width = Some(parse_border_width_term(ctx, name, value, t)?),
580        }
581    }
582    if out.style.is_none() && (out.width.is_some() || out.color.is_some()) {
583        out.style = Some(BorderStyleSpec::Solid);
584    }
585    if out.style.is_none() {
586        return Err(bad(
587            ctx,
588            name,
589            value,
590            "expected '<width> [solid|none] <#color>' (any order) or 'none'".into(),
591        ));
592    }
593    Ok(out)
594}
595
596/// CSS `font-weight`: `normal` (400), `bold` (700), or a number in
597/// `1..=1000` (CSS Fonts 4). The relative keywords `lighter` / `bolder`
598/// need the parent's computed weight and are rejected.
599pub fn parse_font_weight(ctx: &str, name: &str, value: &str) -> Result<u16, ParseError> {
600    match value.trim() {
601        "normal" => Ok(400),
602        "bold" => Ok(700),
603        "lighter" | "bolder" => Err(bad(
604            ctx,
605            name,
606            value,
607            "relative weights (lighter/bolder) are not supported - use a number 1..=1000".into(),
608        )),
609        v => {
610            let n = parse_num(ctx, name, value, v)?;
611            if !(1.0..=1000.0).contains(&n) {
612                return Err(bad(ctx, name, value, "font-weight must be 1..=1000".into()));
613            }
614            Ok(n.round() as u16)
615        }
616    }
617}
618
619/// CSS `border-radius` multi-value shorthand -> per-corner radii
620/// `[top-left, top-right, bottom-right, bottom-left]`:
621///
622/// * 1 value `a` -> `[a, a, a, a]`
623/// * 2 values `a b` -> `[a, b, a, b]`
624/// * 3 values `a b c` -> `[a, b, c, b]`
625/// * 4 values -> explicit per-corner.
626pub fn parse_corner_radii(ctx: &str, name: &str, value: &str) -> Result<[f32; 4], ParseError> {
627    let parts: Vec<&str> = value.split_whitespace().collect();
628    let n = |s: &str| parse_num(ctx, name, value, s);
629    Ok(match parts.as_slice() {
630        [a] => {
631            let a = n(a)?;
632            [a; 4]
633        }
634        [a, b] => {
635            let (a, b) = (n(a)?, n(b)?);
636            [a, b, a, b]
637        }
638        [a, b, c] => {
639            let (a, b, c) = (n(a)?, n(b)?, n(c)?);
640            [a, b, c, b]
641        }
642        [a, b, c, d] => [n(a)?, n(b)?, n(c)?, n(d)?],
643        other => {
644            return Err(bad(
645                ctx,
646                name,
647                value,
648                format!("expected 1, 2, 3, or 4 radii, got {}", other.len()),
649            ));
650        }
651    })
652}
653
654/// Parse a plain `f32`.
655pub fn parse_f32(ctx: &str, name: &str, value: &str) -> Result<f32, ParseError> {
656    parse_num(ctx, name, value, value.trim())
657}
658
659/// Parse a plain `i32`.
660pub fn parse_i32(ctx: &str, name: &str, value: &str) -> Result<i32, ParseError> {
661    value
662        .trim()
663        .parse()
664        .map_err(|e: std::num::ParseIntError| bad(ctx, name, value, e.to_string()))
665}
666
667fn parse_num(ctx: &str, name: &str, value: &str, s: &str) -> Result<f32, ParseError> {
668    let s = s.trim();
669    // Authors habitually write `8px` on numeric props (radius, font-size,
670    // gap, shadow offsets); px is the native unit, so accept and strip it.
671    let s = s.strip_suffix("px").map(str::trim_end).unwrap_or(s);
672    s.parse().map_err(|e: std::num::ParseFloatError| {
673        bad(ctx, name, value, format!("bad number '{s}': {e}"))
674    })
675}
676
677/// Construct a `BadAttribute` error.
678pub fn bad(ctx: &str, name: &str, value: &str, reason: String) -> ParseError {
679    ParseError::BadAttribute {
680        name: name.to_string(),
681        value: value.to_string(),
682        tag: ctx.to_string(),
683        reason,
684    }
685}