File size: 4,791 Bytes
89d9642
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""Score badges, chips and meters — the small visual vocabulary of the app.

Everything returns an HTML string rather than calling st.* directly, so the
pieces compose inside cards and columns.

A recurring rule here: whenever a Nutri-Score grade is shown, its *source* is
shown with it. Roughly 45% of the grades in the catalog were computed by
NutriWeb rather than published by Open Food Facts, and presenting the two
identically would overstate what we know.
"""

from __future__ import annotations

import html

from nutriweb.util import num, tag_set

GRADE_LABEL = {
    "a": "A", "b": "B", "c": "C", "d": "D", "e": "E",
}
NOVA_LABEL = {
    1: "Unprocessed",
    2: "Culinary ingredient",
    3: "Processed",
    4: "Ultra-processed",
}


def esc(value) -> str:
    return html.escape(str(value)) if value is not None else ""


def nutriscore_badge(grade: str | None, source: str | None = None) -> str:
    """Nutri-Score pill. `source` is 'off' or 'nutriweb'."""
    if not grade or str(grade).lower() not in GRADE_LABEL:
        return '<span class="nw-badge nw-badge-neutral">Nutri-Score n/a</span>'
    g = str(grade).lower()
    chip = ""
    if source == "nutriweb":
        chip = '<span class="nw-source" title="Computed by NutriWeb from the nutrition facts, because Open Food Facts has no grade for this product">est.</span>'
    elif source == "off":
        chip = '<span class="nw-source" title="Published by Open Food Facts">OFF</span>'
    return (
        f'<span class="nw-badge ns-{g}"><span class="k">Nutri-Score</span> '
        f"{GRADE_LABEL[g]}</span>{chip}"
    )


def nova_badge(nova) -> str:
    value = num(nova)
    if value is None or int(value) not in NOVA_LABEL:
        return ""
    n = int(value)
    tone = {1: "chip-good", 2: "chip-good", 3: "chip-warn", 4: "chip-danger"}[n]
    return f'<span class="nw-chip {tone}" title="NOVA {n}">{NOVA_LABEL[n]}</span>'


def eco_badge(grade) -> str:
    if not grade or str(grade).lower() not in GRADE_LABEL:
        return ""
    g = str(grade).lower()
    return f'<span class="nw-badge ns-{g}"><span class="k">Eco</span> {GRADE_LABEL[g]}</span>'


def badge_row(product: dict) -> str:
    parts = [
        nutriscore_badge(product.get("nutriscore_grade"), product.get("nutriscore_source")),
        eco_badge(product.get("environmental_score_grade")),
        nova_badge(product.get("nova_group")),
    ]
    return '<div class="nw-badges">' + "".join(p for p in parts if p) + "</div>"


def health_meter(score, confidence: str | None = None) -> str:
    """The 0-100 NutriWeb health score with a coloured bar."""
    value = num(score)
    if value is None:
        return '<div class="nw-health"><span class="m">Not enough data to score</span></div>'

    colour = (
        "var(--ns-a)" if value >= 75
        else "var(--ns-b)" if value >= 55
        else "var(--ns-c)" if value >= 40
        else "var(--ns-d)" if value >= 25
        else "var(--ns-e)"
    )
    note = f" · {esc(confidence)} confidence" if confidence else ""
    return (
        '<div class="nw-health">'
        f'<span class="v">{value:.0f}</span><span class="m">/100 health score{note}</span>'
        "</div>"
        f'<div class="nw-meter"><span style="width:{max(2, min(100, value)):.0f}%;'
        f'background:{colour}"></span></div>'
    )


def thumb(product: dict) -> str:
    url = product.get("image_url")
    if url and not isinstance(url, float):
        return f'<img class="nw-thumb" src="{esc(url)}" loading="lazy" alt="">'
    return '<div class="nw-thumb nw-thumb-empty">🥫</div>'


def chips(tags, tone: str = "", limit: int = 12, strip_prefix: bool = True) -> str:
    """Render OFF tags as chips: 'en:peanuts' -> 'peanuts'."""
    items = sorted(tag_set(tags)) if not isinstance(tags, list) else list(tags)
    if not items:
        return ""
    out = []
    for tag in items[:limit]:
        label = tag.split(":", 1)[-1].replace("-", " ") if strip_prefix else tag
        out.append(f'<span class="nw-chip {tone}">{esc(label)}</span>')
    if len(items) > limit:
        out.append(f'<span class="nw-chip">+{len(items) - limit} more</span>')
    return '<div class="nw-chips">' + "".join(out) + "</div>"


def title_of(product: dict) -> str:
    name = product.get("product_name")
    return esc(name) if name and not isinstance(name, float) else "Unnamed product"


def brand_of(product: dict) -> str:
    brands = product.get("brands")
    if not brands or isinstance(brands, float):
        return ""
    # OFF stores brands as a comma-separated string, often with duplicates.
    seen, out = set(), []
    for b in str(brands).split(","):
        b = b.strip()
        if b and b.lower() not in seen:
            seen.add(b.lower())
            out.append(b)
    return esc(", ".join(out[:2]))