code wiki / _hdl_build / nx_ad_banner.nx
nx_ad_banner.nx source
↩ module page · 205 lines · 14578 B
1// nx_ad_banner.nx -- LIB: the BANNER GENERATOR for the performance ad engine. Advertisers generate real
2// STATIC or DYNAMIC display banners (IAB-sized, themed) for placement in the one-ad-per-page slot.
3// EXTENDS nx_ad_serve's clean "Brought to you by" model from a text byline to a full creative, while
4// KEEPING every privacy/security invariant BY CONSTRUCTION:
5// * ZERO third-party JS -- no <script>/<iframe>/<object>/<embed>/src= ; DYNAMIC = pure CSS @keyframes
6// animation (no JS), so a banner can NEVER carry a surveillance auction or third-party script.
7// * SELF-CONTAINED -- no external fetch (no src=, no url(http...)) -> no off-site beacon. Visuals
8// are CSS (solid + accent) + text only; no <img src>. No per-user targeting -- "systems not people".
9// * BOUNDARY-ESCAPED -- sponsor/headline/body/cta are external input -> HTML-escaped (rule 12); the
10// click LINK is scheme-validated (https/http/ relative only) so a `javascript:`/`data:` URI XSS is
11// neutralised to '#'.
12// * SCOPED -- a per-banner class suffix scopes the inline <style> so multiple previews on
13// one builder page never collide.
14// DRY: composes nx_ad_serve (as_append / as_append_escaped / as_has_thirdparty_js / as_len / as_contains).
15// IAB sizes + themes are NAMED here (code-default tier of the config hierarchy); migrate to
16// adcfg:bannersizes in the sovereign store (nx_ad_store) as a follow-on. license_tier: ORIGINAL
17import "nx_ad_serve.nx"
18import "nx_syscalls.nx"
19
20const AB_STATIC: i64 = 0
21const AB_DYNAMIC: i64 = 1
22
23func ab_startswith(buf: *u8, pfx: *u8) -> i64 {
24 var i: i64 = 0
25 while pfx[i] != (0 as u8) { if buf[i] != pfx[i] { return 0 } i = i + 1 }
26 return 1
27}
28// append unsigned decimal v to out at off; return new offset.
29func ab_uitoa(out: *u8, off: i64, v: i64) -> i64 {
30 let t: *u8 = sys_mmap(24); var m: i64 = v; var k: i64 = 0
31 if m == 0 { t[0] = 48 as u8; k = 1 }
32 while m > 0 { t[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 }
33 var o: i64 = off; var q: i64 = k - 1
34 while q >= 0 { out[o] = t[q]; o = o + 1; q = q - 1 }
35 return o
36}
37// click-link safety: only https:// , http:// , or a site-relative '/path' (defeats javascript:/data: URIs).
38func ab_safe_link(link: *u8) -> i64 {
39 if ab_startswith(link, "https://" as *u8) == 1 { return 1 }
40 if ab_startswith(link, "http://" as *u8) == 1 { return 1 }
41 if link[0] == (47 as u8) { return 1 } // '/'
42 return 0
43}
44// 16-bit djb2 over business||headline -> 4 hex chars into out (NUL-terminated). Scopes the <style>.
45func ab_suffix(a: *u8, b: *u8, out: *u8) -> i64 {
46 var h: i64 = 5381
47 var i: i64 = 0
48 while a[i] != (0 as u8) { h = ((h * 33) + (a[i] as i64)) & 0xffffff; i = i + 1 }
49 var j: i64 = 0
50 while b[j] != (0 as u8) { h = ((h * 33) + (b[j] as i64)) & 0xffffff; j = j + 1 }
51 h = h & 0xffff
52 let hx: *u8 = "0123456789abcdef" as *u8
53 out[0] = hx[(h >> 12) & 15]; out[1] = hx[(h >> 8) & 15]; out[2] = hx[(h >> 4) & 15]; out[3] = hx[h & 15]
54 out[4] = 0 as u8
55 return 4
56}
57
58func ab_upper(c: i64) -> i64 { if c >= 97 { if c <= 122 { return c - 32 } } return c }
59// up to 2 uppercase initials from the business name (first letter of the first 2 words) -> out (NUL-term).
60func ab_initials(business: *u8, out: *u8) -> i64 {
61 var o: i64 = 0; var i: i64 = 0; var atword: i64 = 1; var go: i64 = 1
62 while go == 1 {
63 let c: i64 = business[i] as i64
64 if c == 0 { go = 0 } else {
65 if c == 32 { atword = 1 } else {
66 if atword == 1 {
67 var isl: i64 = 0
68 if c >= 65 { if c <= 90 { isl = 1 } }
69 if c >= 97 { if c <= 122 { isl = 1 } }
70 if isl == 1 { if o < 2 { out[o] = ab_upper(c) as u8; o = o + 1 } }
71 atword = 0
72 }
73 }
74 i = i + 1
75 if o >= 2 { go = 0 }
76 }
77 }
78 if o == 0 { out[0] = 78 as u8; o = 1 } // 'N' fallback
79 out[o] = 0 as u8
80 return o
81}
82
83// THE GENERATOR. Emit a slot-ready banner (NUL-terminated) into out; return byte length.
84// text args NUL-terminated; w/h = pixel size (IAB, supplied by caller/config); kind = AB_STATIC|AB_DYNAMIC;
85// theme_id 0=light 1=dark 2=warm.
86func ab_emit_banner(business: *u8, headline: *u8, body: *u8, cta: *u8, link: *u8,
87 w: i64, h: i64, kind: i64, theme_id: i64, out: *u8) -> i64 {
88 var bg: *u8 = "#ffffff"; var fg: *u8 = "#16202e"; var accent: *u8 = "#0b2545"; var actxt: *u8 = "#ffffff"
89 if theme_id == 1 { bg = "#0b2545"; fg = "#eaf1fb"; accent = "#4da3ff"; actxt = "#06122b" }
90 if theme_id == 2 { bg = "#fff7ed"; fg = "#7c2d12"; accent = "#ea580c"; actxt = "#ffffff" }
91
92 let sfx: *u8 = sys_mmap(8)
93 ab_suffix(business, headline, sfx)
94 var safe: i64 = ab_safe_link(link)
95 var o: i64 = 0
96
97 o = as_append(out, o, "<a class='nb-" as *u8); o = as_append(out, o, sfx)
98 o = as_append(out, o, "' href='" as *u8)
99 if safe == 1 { o = as_append_escaped(out, o, link, as_len(link)) } else { o = as_append(out, o, "#" as *u8) }
100 o = as_append(out, o, "' rel='nofollow noopener' role='note' aria-label='Advertisement'>" as *u8)
101
102 o = as_append(out, o, "<style>.nb-" as *u8); o = as_append(out, o, sfx)
103 o = as_append(out, o, "{display:flex;flex-direction:column;justify-content:center;box-sizing:border-box;width:" as *u8)
104 o = ab_uitoa(out, o, w); o = as_append(out, o, "px;height:" as *u8); o = ab_uitoa(out, o, h)
105 o = as_append(out, o, "px;padding:12px 14px;gap:6px;border-radius:10px;overflow:hidden;text-decoration:none;font-family:system-ui,-apple-system,Segoe UI,sans-serif;background:" as *u8)
106 o = as_append(out, o, bg); o = as_append(out, o, ";color:" as *u8); o = as_append(out, o, fg)
107 o = as_append(out, o, ";border:1px solid rgba(0,0,0,.12);position:relative}" as *u8)
108 o = as_append(out, o, ".nb-" as *u8); o = as_append(out, o, sfx); o = as_append(out, o, " .nb-spon{font-size:11px;letter-spacing:.04em;opacity:.7;text-transform:uppercase}" as *u8)
109 o = as_append(out, o, ".nb-" as *u8); o = as_append(out, o, sfx); o = as_append(out, o, " .nb-head{font-size:18px;font-weight:700;line-height:1.15}" as *u8)
110 o = as_append(out, o, ".nb-" as *u8); o = as_append(out, o, sfx); o = as_append(out, o, " .nb-body{font-size:13px;opacity:.9;line-height:1.3}" as *u8)
111 o = as_append(out, o, ".nb-" as *u8); o = as_append(out, o, sfx); o = as_append(out, o, " .nb-cta{align-self:flex-start;margin-top:4px;padding:6px 12px;border-radius:6px;font-size:13px;font-weight:600;background:" as *u8)
112 o = as_append(out, o, accent); o = as_append(out, o, ";color:" as *u8); o = as_append(out, o, actxt); o = as_append(out, o, "}" as *u8)
113 o = as_append(out, o, ".nb-" as *u8); o = as_append(out, o, sfx); o = as_append(out, o, " .nb-logo{position:absolute;top:10px;right:12px}" as *u8)
114 if kind == AB_DYNAMIC {
115 o = as_append(out, o, "@keyframes nbp-" as *u8); o = as_append(out, o, sfx); o = as_append(out, o, "{0%,100%{transform:scale(1)}50%{transform:scale(1.06)}}" as *u8)
116 o = as_append(out, o, ".nb-" as *u8); o = as_append(out, o, sfx); o = as_append(out, o, " .nb-cta{animation:nbp-" as *u8); o = as_append(out, o, sfx); o = as_append(out, o, " 1.6s ease-in-out infinite}" as *u8)
117 o = as_append(out, o, "@media (prefers-reduced-motion:reduce){.nb-" as *u8); o = as_append(out, o, sfx); o = as_append(out, o, " .nb-cta{animation:none}}" as *u8)
118 }
119 o = as_append(out, o, "</style>" as *u8)
120
121 // sovereign inline-SVG sponsor monogram (a real logo element; NO external fetch, NO script -> invariants hold)
122 let ini: *u8 = sys_mmap(8)
123 ab_initials(business, ini)
124 o = as_append(out, o, "<svg class='nb-logo' width='32' height='32' viewBox='0 0 36 36' aria-hidden='true'><circle cx='18' cy='18' r='18' fill='" as *u8)
125 o = as_append(out, o, accent)
126 o = as_append(out, o, "'/><text x='18' y='23' text-anchor='middle' font-family='system-ui,sans-serif' font-size='15' font-weight='700' fill='" as *u8)
127 o = as_append(out, o, actxt)
128 o = as_append(out, o, "'>" as *u8)
129 o = as_append(out, o, ini)
130 o = as_append(out, o, "</text></svg>" as *u8)
131
132 o = as_append(out, o, "<span class='nb-spon'>Brought to you by " as *u8)
133 o = as_append_escaped(out, o, business, as_len(business)); o = as_append(out, o, "</span>" as *u8)
134 o = as_append(out, o, "<span class='nb-head'>" as *u8); o = as_append_escaped(out, o, headline, as_len(headline)); o = as_append(out, o, "</span>" as *u8)
135 o = as_append(out, o, "<span class='nb-body'>" as *u8); o = as_append_escaped(out, o, body, as_len(body)); o = as_append(out, o, "</span>" as *u8)
136 o = as_append(out, o, "<span class='nb-cta'>" as *u8); o = as_append_escaped(out, o, cta, as_len(cta)); o = as_append(out, o, " →</span>" as *u8)
137 o = as_append(out, o, "</a>" as *u8)
138 out[o] = 0 as u8
139 return o
140}
141
142// ===== data-driven emitter consumption (the builder USES catalogued emitters) =====
143// substitution engine: {ACCENT}/{ACTXT}/{INI}/{S} in a template -> values.
144func ab_pl(tpl: *u8, i: i64, pat: *u8) -> i64 { var j: i64 = 0; while pat[j] != (0 as u8) { if tpl[i+j] != pat[j] { return 0 } j = j + 1 } return 1 }
145func ab_apply_tpl(out: *u8, off: i64, tpl: *u8, accent: *u8, actxt: *u8, ini: *u8, sfx: *u8) -> i64 {
146 var o: i64 = off; var i: i64 = 0
147 while tpl[i] != (0 as u8) {
148 var done: i64 = 0
149 if tpl[i] == (123 as u8) {
150 if ab_pl(tpl, i, "{ACCENT}" as *u8) == 1 { o = as_append(out, o, accent); i = i + 8; done = 1 }
151 if done == 0 { if ab_pl(tpl, i, "{ACTXT}" as *u8) == 1 { o = as_append(out, o, actxt); i = i + 7; done = 1 } }
152 if done == 0 { if ab_pl(tpl, i, "{INI}" as *u8) == 1 { o = as_append(out, o, ini); i = i + 5; done = 1 } }
153 if done == 0 { if ab_pl(tpl, i, "{S}" as *u8) == 1 { o = as_append(out, o, sfx); i = i + 3; done = 1 } }
154 }
155 if done == 0 { out[o] = tpl[i]; o = o + 1; i = i + 1 }
156 }
157 return o
158}
159// THE CORE RENDERER: explicit colors + an accent TEMPLATE + an animation TEMPLATE (data-driven; placeholders
160// substituted). The registry-driven path (nx_ad_emitter_apply) renders grown emitters through THIS one engine.
161// (ab_emit_banner above is the back-compat theme_id/monogram/pulse path; FOLD it onto this core next.)
162func ab_emit_banner_c(business: *u8, headline: *u8, body: *u8, cta: *u8, link: *u8,
163 w: i64, h: i64, bg: *u8, fg: *u8, accent: *u8, actxt: *u8,
164 accent_tpl: *u8, anim_tpl: *u8, out: *u8) -> i64 {
165 let sfx: *u8 = sys_mmap(8); ab_suffix(business, headline, sfx)
166 let ini: *u8 = sys_mmap(8); ab_initials(business, ini)
167 var safe: i64 = ab_safe_link(link)
168 var o: i64 = 0
169 o = as_append(out, o, "<a class='nb-" as *u8); o = as_append(out, o, sfx); o = as_append(out, o, "' href='" as *u8)
170 if safe == 1 { o = as_append_escaped(out, o, link, as_len(link)) } else { o = as_append(out, o, "#" as *u8) }
171 o = as_append(out, o, "' rel='nofollow noopener' role='note' aria-label='Advertisement'>" as *u8)
172 o = as_append(out, o, "<style>.nb-" as *u8); o = as_append(out, o, sfx)
173 o = as_append(out, o, "{display:flex;flex-direction:column;justify-content:center;box-sizing:border-box;width:" as *u8)
174 o = ab_uitoa(out, o, w); o = as_append(out, o, "px;height:" as *u8); o = ab_uitoa(out, o, h)
175 o = as_append(out, o, "px;padding:12px 14px;gap:6px;border-radius:10px;overflow:hidden;text-decoration:none;font-family:system-ui,-apple-system,Segoe UI,sans-serif;background:" as *u8)
176 o = as_append(out, o, bg); o = as_append(out, o, ";color:" as *u8); o = as_append(out, o, fg)
177 o = as_append(out, o, ";border:1px solid rgba(0,0,0,.12);position:relative}" as *u8)
178 o = as_append(out, o, ".nb-" as *u8); o = as_append(out, o, sfx); o = as_append(out, o, " .nb-spon{font-size:11px;letter-spacing:.04em;opacity:.7;text-transform:uppercase}" as *u8)
179 o = as_append(out, o, ".nb-" as *u8); o = as_append(out, o, sfx); o = as_append(out, o, " .nb-head{font-size:18px;font-weight:700;line-height:1.15}" as *u8)
180 o = as_append(out, o, ".nb-" as *u8); o = as_append(out, o, sfx); o = as_append(out, o, " .nb-body{font-size:13px;opacity:.9;line-height:1.3}" as *u8)
181 o = as_append(out, o, ".nb-" as *u8); o = as_append(out, o, sfx); o = as_append(out, o, " .nb-cta{align-self:flex-start;margin-top:4px;padding:6px 12px;border-radius:6px;font-size:13px;font-weight:600;background:" as *u8)
182 o = as_append(out, o, accent); o = as_append(out, o, ";color:" as *u8); o = as_append(out, o, actxt); o = as_append(out, o, "}" as *u8)
183 o = as_append(out, o, ".nb-" as *u8); o = as_append(out, o, sfx); o = as_append(out, o, " .nb-logo{position:absolute;top:10px;right:12px}" as *u8)
184 o = ab_apply_tpl(out, o, anim_tpl, accent, actxt, ini, sfx)
185 o = as_append(out, o, "</style>" as *u8)
186 o = ab_apply_tpl(out, o, accent_tpl, accent, actxt, ini, sfx)
187 o = as_append(out, o, "<span class='nb-spon'>Brought to you by " as *u8); o = as_append_escaped(out, o, business, as_len(business)); o = as_append(out, o, "</span>" as *u8)
188 o = as_append(out, o, "<span class='nb-head'>" as *u8); o = as_append_escaped(out, o, headline, as_len(headline)); o = as_append(out, o, "</span>" as *u8)
189 o = as_append(out, o, "<span class='nb-body'>" as *u8); o = as_append_escaped(out, o, body, as_len(body)); o = as_append(out, o, "</span>" as *u8)
190 o = as_append(out, o, "<span class='nb-cta'>" as *u8); o = as_append_escaped(out, o, cta, as_len(cta)); o = as_append(out, o, " →</span>" as *u8)
191 o = as_append(out, o, "</a>" as *u8)
192 out[o] = 0 as u8
193 return o
194}
195
196// IAB standard size names (code-default tier; w via *wout, h via *hout). 1 if known, 0 if not (defaults 300x250).
197func ab_iab_size(name: *u8, wout: *i64, hout: *i64) -> i64 {
198 if as_contains(name, as_len(name), "leaderboard" as *u8) == 1 { wout[0] = 728; hout[0] = 90; return 1 }
199 if as_contains(name, as_len(name), "rectangle" as *u8) == 1 { wout[0] = 300; hout[0] = 250; return 1 }
200 if as_contains(name, as_len(name), "mobile" as *u8) == 1 { wout[0] = 320; hout[0] = 50; return 1 }
201 if as_contains(name, as_len(name), "skyscraper" as *u8) == 1 { wout[0] = 160; hout[0] = 600; return 1 }
202 if as_contains(name, as_len(name), "halfpage" as *u8) == 1 { wout[0] = 300; hout[0] = 600; return 1 }
203 if as_contains(name, as_len(name), "billboard" as *u8) == 1 { wout[0] = 970; hout[0] = 250; return 1 }
204 wout[0] = 300; hout[0] = 250; return 0
205}