Class: BarefootJS::Context
- Inherits:
-
Object
- Object
- BarefootJS::Context
- Defined in:
- lib/barefoot_js.rb
Overview
Context is the bf object every compiled .erb template receives as a
local. One instance per render (root or child); render_child /
register_components_from_manifest construct a fresh child instance
per nested render, chaining scope/slot identity off the caller.
Constant Summary collapse
- NUMERIC_STRING_RE =
JS-compat callees -- invoked from generated ERB templates as
bf.json(val),bf.floor(val), etc. Numeric coercion follows JS semantics (NaN propagates; non-numeric input yields NaN rather than silently 0).jsonbubbles backend/marshalling errors loudly rather than producing an empty payload. /\A\s*[+-]?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?\s*\z/.freeze
- FORMAT_DATE_OFFSET_RE =
tzshapes FORMAT_DATE_OFFSET_RE matches: a fixed UTC offset±HH:MMwithin ECMA-402's valid range -- hours 00-23, minutes 00-59 ('+09:00','-05:30') -- one of the threetzshapesformat_dateaccepts (mirrors OFFSET_RE in packages/client/src/format-date.ts). An out-of-range shape ('+25:00') falls through to the tzdata lookup, fails it, and raises -- matching the JS reference's RangeError (#2344). /\A([+-])([01]\d|2[0-3]):([0-5]\d)\z/- FORMAT_DATE_TOKEN_RE =
Longest-match pattern-token alternation (mirrors TOKEN_RE in packages/client/src/format-date.ts). Order matters: Onigmo, like JS's regex engine, resolves alternation leftmost-first (not POSIX leftmost-longest), so every multi-char token must be listed before any shorter alternative it prefixes -- YYYY before (nothing shorter shares its prefix), MMMM before MMM before MM before M, DD before D, dddd before ddd. (#2334)
/YYYY|MMMM|MMM|MM|DD|dddd|ddd|M|D/- FORMAT_DATE_MONTHS_WIDE =
names-table section offsets (spec/template-helpers.md "format_date", #2334) -- mirrors MONTHS_WIDE/MONTHS_ABBR/WEEKDAYS_WIDE/WEEKDAYS_ABBR in packages/client/src/format-date.ts. 0- FORMAT_DATE_MONTHS_ABBR =
12- FORMAT_DATE_WEEKDAYS_WIDE =
24- FORMAT_DATE_WEEKDAYS_ABBR =
31
Class Method Summary collapse
-
.derive_vars_from_defaults(defaults, props) ⇒ Object
Derive template-var kvs from a manifest entry's
ssrDefaultssection.
Instance Method Summary collapse
-
#abs(value) ⇒ Object
Math.abs()(#2168 math-methods). - #async_boundary(id, fallback_html) ⇒ Object
- #async_resolve(id, content_html) ⇒ Object
-
#at(recv, i) ⇒ Object
Array.prototype.at(i)-- negative indices count from the end; out-of-bounds -> nil (renders as '' viah, matching JSundefined). -
#bool_str(value) ⇒ Object
Map a JS-boolean-shaped value to the JS
String(bool)form. - #ceil(value) ⇒ Object
-
#comment(text) ⇒ Object
----------------------------------------------------------------- Comment Markers -----------------------------------------------------------------.
-
#concat(a, b) ⇒ Object
Array.prototype.concat(other)-- merges two arrays in order into a new Array. -
#data_key_attr ⇒ Object
Emits
data-key="<key>"for a keyed loop item, else ''. -
#date(recv, op) ⇒ Object
date(recv, op)-- zero-argDate.prototypemethod lowering (#2274, spec entry "date"). -
#ends_with(recv, suffix, end_position = nil) ⇒ Object
String.prototype.endsWith(suffix, endPosition?). - #every(recv, pred) ⇒ Object
- #every_eval(recv, pred_json, param, base_env = {}) ⇒ Object
-
#filter(recv, pred) ⇒ Object
.filter(fn)/.every(fn)/.some(fn)/.find(fn)/.findIndex(fn)/.findLast(fn)/.findLastIndex(fn)-- legacy block-predicate path for shapes the compiler lowers to a native callable (e.g. a Kolon-style lambda literal). - #filter_eval(recv, pred_json, param, base_env = {}) ⇒ Object
- #find(recv, pred) ⇒ Object
- #find_eval(recv, pred_json, param, forward = true, base_env = {}) ⇒ Object
- #find_index(recv, pred) ⇒ Object
- #find_index_eval(recv, pred_json, param, forward = true, base_env = {}) ⇒ Object
- #find_last(recv, pred) ⇒ Object
- #find_last_index(recv, pred) ⇒ Object
-
#flat(recv, depth = 1) ⇒ Object
Array.prototype.flat(depth?)-- flatten nested arraysdepthlevels deep. -
#flat_dynamic(recv, depth) ⇒ Object
Array.prototype.flat(depth)wheredepthis a DYNAMIC value (#2094) -- e.g. -
#flat_map(recv, key_kind, key) ⇒ Object
Array.prototype.flatMap(fn)value-returning field projection: map each element through a self/field projection, then flatten one level. - #flat_map_eval(recv, proj_json, param, base_env = {}) ⇒ Object
-
#flat_map_tuple(recv, *specs) ⇒ Object
Array.prototype.flatMap(i => [i.a, i.b])-- array-literal tuple projection. - #floor(value) ⇒ Object
-
#format_date(recv, pattern, tz, names = []) ⇒ Object
format_date(recv, pattern, tz, names)(#2324/#2334, spec entry "format_date" in spec/template-helpers.md) -- the lowering target forformatDate(date, pattern, timeZone, names)(packages/client/src/format-date.ts, the JS-normative reference this must match byte-for-byte). -
#format_date_zone_offset(tz, t) ⇒ Object
Resolve a canonical IANA zone ID's UTC offset (whole seconds) at instant
tthrough the tzinfo gem (#2344) -- TZInfo resolves the exact-case canonical ID set every backend shares, with full historical transitions (LMT seconds included). -
#get(collection, key) ⇒ Object
Runtime-polymorphic element/key access for
arr[index]/hash[key](emitIndexAccessRuby, expr/operand.ts). -
#h(value) ⇒ Object
HTML-escaping helper for text interpolation (
<%= bf.h(expr) %>-- stdlib ERB does not auto-escape). -
#has_unsafe_style_value?(value) ⇒ Boolean
Mirrors Hono's own CSS-injection guard (
hono/jsx/utils.ts'shasUnsafeStyleValue-- the ORACLE a dynamicstyle={{...}}value must match, #2261): a hand-rolled structural scan for characters that could break out of a CSS declaration, NOT real CSSOM property validation. -
#hydration_attrs ⇒ Object
Emits
bf-h="<host>" bf-m="<slot>" bf-r=""conditionally. -
#includes(recv, elem) ⇒ Object
Array.prototype.includes(x)/String.prototype.includes(sub)share a method name in JS; dispatch on Ruby class the way BarefootJS.pm dispatches onref(). -
#index_of(recv, elem) ⇒ Object
Array.prototype.indexOf(x)/.lastIndexOf(x)-- value-equality search. -
#initialize(backend = nil) ⇒ Context
constructor
A new instance of Context.
-
#join(recv, sep = nil) ⇒ Object
Array.prototype.join(sep)with JS semantics: separator defaults to ",", undefined/null elements render as empty. - #json(value) ⇒ Object
- #last_index_of(recv, elem) ⇒ Object
-
#lc(s) ⇒ Object
String.prototype.toLowerCase()/.toUpperCase(). -
#length(recv) ⇒ Object
.lengthworks on both arrays (element count) and strings. - #map_eval(recv, proj_json, param, base_env = {}) ⇒ Object
- #max(a, b) ⇒ Object
-
#min(a, b) ⇒ Object
Math.min(a, b)/Math.max(a, b)-- two-arg forms only (#2168 math-methods). -
#number(value) ⇒ Object
JS
Number(v)mirror: numeric / boolean inputs convert as expected; non-numeric / nil yield real numeric NaN so downstream arithmetic propagates correctly (Math.floor(NaN) === NaN). - #pad_end(recv, target, pad_str = nil) ⇒ Object
-
#pad_start(recv, target, pad_str = nil) ⇒ Object
String.prototype.padStart/padEnd. - #props_attr ⇒ Object
- #provide_context(name, value) ⇒ Object
-
#query(base, *triples) ⇒ Object
queryHref(base, { ... })(#2042) -- build "base?k=v&..." from a flat list of (guard, key, value) triples. -
#reduce(recv, opts = {}) ⇒ Object
Fold an array into a scalar via the arithmetic-fold catalogue (legacy, pre-#2018 path;
reduce_evalbelow handles arbitrary reducer bodies). - #reduce_eval(recv, body_json, acc_name, item_name, init, direction = 'left', base_env = {}) ⇒ Object
-
#register_child_renderer(name, renderer) ⇒ Object
Register a renderer for
render_child(name, ...). -
#register_components_from_manifest(manifest, signal_init: {}) ⇒ Object
----------------------------------------------------------------- Bulk registration from build manifest -----------------------------------------------------------------.
-
#register_preload(path) ⇒ Object
Register a
<link rel="modulepreload">hint (mirrorsregister_scriptexactly: same dedup-by-path set, same insertion-order array, same lifetime/reset semantics, same no-output-string return so the compiled ERB template's<% bf.register_preload(...) %>produces no bytes where it sits). -
#register_script(path) ⇒ Object
----------------------------------------------------------------- Script Registration -----------------------------------------------------------------.
- #render_child(name, *args) ⇒ Object
-
#repeat(recv, count) ⇒ Object
String.prototype.repeat(n)-- a count <= 0 degrades to '' rather than raising (JS throws RangeError for negative counts; SSR templates degrade instead of dying mid-render). -
#replace(recv, pattern, replacement) ⇒ Object
String.prototype.replace(pattern, replacement)-- string-pattern form only, replacing the FIRST occurrence, literally (no regex metacharacters, no$1-style replacement interpolation). -
#replace_all(recv, pattern, replacement) ⇒ Object
String.prototype.replaceAll(pattern, replacement)-- string-pattern form only (#2182), replacing EVERY occurrence (the all-occurrences sibling ofreplaceabove). -
#reverse(recv) ⇒ Object
Array.prototype.reverse()/.toReversed()-- always returns a new Array (SSR renders a snapshot; the mutate-vs-copy JS distinction is moot here). - #revoke_context(name) ⇒ Object
- #round(value) ⇒ Object
-
#scope_attr ⇒ Object
bf-s is the addressable scope id only (#1249).
-
#scope_comment ⇒ Object
See spec/compiler.md "Slot identity" for the comment-scope wire format.
-
#scope_comment_end ⇒ Object
Paired end marker for
scope_comment, emitted after the fragment's last top-level node. -
#scripts ⇒ Object
Emits preload hints BEFORE script tags -- a hint that arrives after the script it describes is useless.
-
#search_params(query = '') ⇒ Object
search_params(query = '') -- request-scoped reader for the reactive searchParams() environment signal (router v0.5, #1922), built from a raw query string.
-
#slice(recv, start, end_ = nil) ⇒ Object
Array.prototype.slice(start, end?)ANDString.prototype.slice(thestring-slicedivergence) -- the adapter emits the samebf_slicecall for both receiver shapes (it can't disambiguate string vs. array at compile time), so this dispatches on Ruby class, mirroringincludesabove. - #some(recv, pred) ⇒ Object
- #some_eval(recv, pred_json, param, base_env = {}) ⇒ Object
-
#sort(recv, opts = {}) ⇒ Object
Array.prototype.sort(cmp)/.toSorted(cmp)-- fixed comparator catalogue (legacy, pre-#2018 path;sort_evalbelow handles arbitrary comparator bodies). -
#sort_eval(recv, cmp_json, param_a, param_b, base_env = {}) ⇒ Object
----------------------------------------------------------------- Evaluator-driven sort / reduce / higher-order predicates (#2018): the comparator / reducer / predicate body rides as a serialized- ParsedExpr JSON string and is evaluated per element, delegating to the shared BarefootJS::Evaluator.
-
#split(recv, sep = nil, limit = nil) ⇒ Object
String.prototype.split(sep)-- string -> Array. -
#spread_attrs(bag) ⇒ Object
----------------------------------------------------------------- JSX intrinsic-element spread (#1407) -----------------------------------------------------------------.
-
#starts_with(recv, prefix, position = nil) ⇒ Object
String.prototype.startsWith(prefix, position?). -
#streaming_bootstrap ⇒ Object
----------------------------------------------------------------- Streaming SSR (Out-of-Order) -----------------------------------------------------------------.
-
#string(value) ⇒ Object
JS
String(v)mirror, EXCEPTnilrenders as '' (not the literal "null") so an unset prop doesn't surface as literal text in user-facing HTML -- the same divergence the Go/Perl adapters document for theirstringhelper. -
#style_object(*pairs) ⇒ Object
Builds the CSS string for a
style={{...}}JSX object-literal attribute (#2261) --pairsalternates CSS key (always a compile-time-known literal), then value. - #text_end ⇒ Object
- #text_start(slot_id) ⇒ Object
-
#to_fixed(value, digits = 0) ⇒ Object
Number.prototype.toFixed(digits)-- fixed-decimal string with zero-padding, rounding half toward +Infinity (matchinground). -
#trim(recv) ⇒ Object
String.prototype.trim(). - #trim_end(recv) ⇒ Object
-
#trim_start(recv) ⇒ Object
String.prototype.trimStart()/.trimEnd()-- the one-sided siblings oftrimabove (#2183 follow-up), same\p{Space}regex restricted to one side. -
#truthy?(value) ⇒ Boolean
JS truthiness (falsy: false, nil, 0, NaN, "").
- #uc(s) ⇒ Object
- #use_context(name, default = nil) ⇒ Object
Constructor Details
#initialize(backend = nil) ⇒ Context
Returns a new instance of Context.
81 82 83 |
# File 'lib/barefoot_js.rb', line 81 def initialize(backend = nil) @backend = backend end |
Class Method Details
.derive_vars_from_defaults(defaults, props) ⇒ Object
Derive template-var kvs from a manifest entry's ssrDefaults section.
Each entry shape: { value:, propName:, isRestProps: }. For
isRestProps, the rest bag passes through unchanged (or the static
{} if the caller didn't supply one). For ordinary entries the
caller's props[propName] wins when present, otherwise the static
value does. propName-less entries (signal / memo locals) always
use the static value.
Public (not private_class_method): register_components_from_manifest
above uses it for the ui/* registry path, but a page that composes
flat (non-ui/*) components by hand -- e.g. the blog islands in the
Sinatra/xslate/Mojolicious integrations -- needs the exact same
ssrDefaults-seeding logic for its own manual register_child_renderer
calls. Mirrors the Perl runtime's BarefootJS::_derive_stash_from_defaults,
which is likewise callable from integration code (Perl has no enforced
privacy; the leading underscore is convention only) -- see
integrations/xslate/app.psgi's _register_blog_child and
integrations/mojolicious/app.pl's equivalent.
362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 |
# File 'lib/barefoot_js.rb', line 362 def self.derive_vars_from_defaults(defaults, props) extra = {} defaults.each do |name, d| unless d.is_a?(Hash) extra[name] = d next end if d[:isRestProps] extra[name] = props.key?(name) ? props[name] : d[:value] next end # `propName` rides in from the JSON manifest as a String -- JSON has # no symbol type, and the manifest's `symbolize_names: true` parse # only symbolizes hash KEYS, never string values. Runtime prop # hashes, meanwhile, are symbol-keyed because compiled ERB templates # pass `{ children: ... }` literals, so `props.key?(prop_name)` with # the String would always miss, silently falling back to the static # default for every manifest-registered child (e.g. `children` # rendering empty) (#2157). prop_name = d[:propName]&.to_sym extra[name] = if !prop_name.nil? && props.key?(prop_name) && !props[prop_name].nil? props[prop_name] else d[:value] end end extra end |
Instance Method Details
#abs(value) ⇒ Object
Math.abs() (#2168 math-methods).
513 514 515 516 |
# File 'lib/barefoot_js.rb', line 513 def abs(value) n = number(value) nan_number?(n) ? n : n.abs end |
#async_boundary(id, fallback_html) ⇒ Object
400 401 402 403 |
# File 'lib/barefoot_js.rb', line 400 def async_boundary(id, fallback_html) fallback_html = backend.materialize(fallback_html) %(<div bf-async="#{id}">#{fallback_html}</div>) end |
#async_resolve(id, content_html) ⇒ Object
405 406 407 |
# File 'lib/barefoot_js.rb', line 405 def async_resolve(id, content_html) %(<template bf-async-resolve="#{id}">#{content_html}</template><script>__bf_swap("#{id}")</script>) end |
#at(recv, i) ⇒ Object
Array.prototype.at(i) -- negative indices count from the end;
out-of-bounds -> nil (renders as '' via h, matching JS undefined).
935 936 937 938 939 940 941 942 943 944 945 946 947 |
# File 'lib/barefoot_js.rb', line 935 def at(recv, i) return nil unless recv.is_a?(Array) return nil if i.nil? idx = i.to_i len = recv.length return nil if len.zero? idx = len + idx if idx.negative? return nil if idx.negative? || idx >= len recv[idx] end |
#bool_str(value) ⇒ Object
Map a JS-boolean-shaped value to the JS String(bool) form. See
BarefootJS.pm's bool_str docstring for the boolean-only contract --
callers must have already classified the expression as boolean-
result; non-boolean attribute bindings never reach this helper.
179 180 181 |
# File 'lib/barefoot_js.rb', line 179 def bool_str(value) value ? 'true' : 'false' end |
#ceil(value) ⇒ Object
480 481 482 483 |
# File 'lib/barefoot_js.rb', line 480 def ceil(value) n = number(value) finite_number?(n) ? n.ceil : n end |
#comment(text) ⇒ Object
Comment Markers
171 172 173 |
# File 'lib/barefoot_js.rb', line 171 def comment(text) "<!--bf-#{text}-->" end |
#concat(a, b) ⇒ Object
Array.prototype.concat(other) -- merges two arrays in order into a
new Array. Non-array operands collapse to empty.
951 952 953 954 955 956 |
# File 'lib/barefoot_js.rb', line 951 def concat(a, b) out = [] out.concat(a) if a.is_a?(Array) out.concat(b) if b.is_a?(Array) out end |
#data_key_attr ⇒ Object
Emits data-key="<key>" for a keyed loop item, else ''. See
BarefootJS.pm's docstring on the client reconciliation contract.
116 117 118 119 120 121 122 |
# File 'lib/barefoot_js.rb', line 116 def data_key_attr k = _data_key return '' if k.nil? escaped = k.to_s.gsub('&', '&').gsub('"', '"') %( data-key="#{escaped}") end |
#date(recv, op) ⇒ Object
date(recv, op) -- zero-arg Date.prototype method lowering (#2274,
spec entry "date"). recv arrives as either this runtime's own Time
or an ISO-8601 String (a template prop may carry either depending on
how the host populated it), so both normalize through Time.iso8601
/ #utc to the same instant before dispatch. #mon is 1-based in
Ruby; only getUTCMonth subtracts 1 to match JS's 0-based month.
getTime sums whole milliseconds from tv_sec/tv_nsec rather than
rounding a Float ms value -- tv_nsec is always the non-negative
sub-second remainder (Ruby normalizes Time's internal rational),
even for a pre-epoch instant, so integer division here stays exact.
528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 |
# File 'lib/barefoot_js.rb', line 528 def date(recv, op) # A nil or unparseable receiver degrades to the zero value the # Go / Rust / Perl helpers document (empty string for toISOString, # 0 otherwise), rather than raising mid-render. t = if recv.is_a?(Time) recv else begin Time.iso8601(recv.to_s) rescue ArgumentError nil end end return (op == 'toISOString' ? '' : 0) if t.nil? t = t.utc case op when 'getUTCFullYear' then t.year when 'getUTCMonth' then t.mon - 1 when 'getUTCDate' then t.day when 'getUTCHours' then t.hour when 'getUTCMinutes' then t.min when 'getUTCSeconds' then t.sec when 'getTime' then (t.tv_sec * 1000) + (t.tv_nsec / 1_000_000) when 'toISOString' then t.strftime('%Y-%m-%dT%H:%M:%S.%LZ') else 0 end end |
#ends_with(recv, suffix, end_position = nil) ⇒ Object
String.prototype.endsWith(suffix, endPosition?).
1135 1136 1137 1138 1139 1140 1141 1142 1143 |
# File 'lib/barefoot_js.rb', line 1135 def ends_with(recv, suffix, end_position = nil) s = recv.nil? ? '' : string(recv) x = suffix.nil? ? '' : string(suffix) unless end_position.nil? e = clamp_index(end_position.to_i, s.length) s = s[0...e] end s.end_with?(x) end |
#every(recv, pred) ⇒ Object
730 731 732 733 734 |
# File 'lib/barefoot_js.rb', line 730 def every(recv, pred) return true unless recv.is_a?(Array) recv.all? { |item| pred.call(item) } end |
#every_eval(recv, pred_json, param, base_env = {}) ⇒ Object
1340 1341 1342 |
# File 'lib/barefoot_js.rb', line 1340 def every_eval(recv, pred_json, param, base_env = {}) Evaluator.every_json(recv, pred_json, param, base_env) end |
#filter(recv, pred) ⇒ Object
.filter(fn) / .every(fn) / .some(fn) / .find(fn) /
.findIndex(fn) / .findLast(fn) / .findLastIndex(fn) -- legacy
block-predicate path for shapes the compiler lowers to a native
callable (e.g. a Kolon-style lambda literal). pred is anything
responding to #call(item). The _eval family below is the
evaluator-driven generalisation used for arbitrary pure bodies.
724 725 726 727 728 |
# File 'lib/barefoot_js.rb', line 724 def filter(recv, pred) return [] unless recv.is_a?(Array) recv.select { |item| pred.call(item) } end |
#filter_eval(recv, pred_json, param, base_env = {}) ⇒ Object
1336 1337 1338 |
# File 'lib/barefoot_js.rb', line 1336 def filter_eval(recv, pred_json, param, base_env = {}) Evaluator.filter_json(recv, pred_json, param, base_env) end |
#find(recv, pred) ⇒ Object
742 743 744 745 746 |
# File 'lib/barefoot_js.rb', line 742 def find(recv, pred) return nil unless recv.is_a?(Array) recv.find { |item| pred.call(item) } end |
#find_eval(recv, pred_json, param, forward = true, base_env = {}) ⇒ Object
1348 1349 1350 |
# File 'lib/barefoot_js.rb', line 1348 def find_eval(recv, pred_json, param, forward = true, base_env = {}) Evaluator.find_json(recv, pred_json, param, forward, base_env) end |
#find_index(recv, pred) ⇒ Object
748 749 750 751 752 753 |
# File 'lib/barefoot_js.rb', line 748 def find_index(recv, pred) return -1 unless recv.is_a?(Array) recv.each_index { |i| return i if pred.call(recv[i]) } -1 end |
#find_index_eval(recv, pred_json, param, forward = true, base_env = {}) ⇒ Object
1352 1353 1354 |
# File 'lib/barefoot_js.rb', line 1352 def find_index_eval(recv, pred_json, param, forward = true, base_env = {}) Evaluator.find_index_json(recv, pred_json, param, forward, base_env) end |
#find_last(recv, pred) ⇒ Object
755 756 757 758 759 760 |
# File 'lib/barefoot_js.rb', line 755 def find_last(recv, pred) return nil unless recv.is_a?(Array) recv.reverse_each { |item| return item if pred.call(item) } nil end |
#find_last_index(recv, pred) ⇒ Object
762 763 764 765 766 767 |
# File 'lib/barefoot_js.rb', line 762 def find_last_index(recv, pred) return -1 unless recv.is_a?(Array) (recv.length - 1).downto(0) { |i| return i if pred.call(recv[i]) } -1 end |
#flat(recv, depth = 1) ⇒ Object
Array.prototype.flat(depth?) -- flatten nested arrays depth levels
deep. depth of -1 is the Infinity sentinel (flatten fully); 0
returns a shallow copy.
1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 |
# File 'lib/barefoot_js.rb', line 1000 def flat(recv, depth = 1) return [] unless recv.is_a?(Array) out = [] recv.each do |el| if !depth.zero? && el.is_a?(Array) out.concat(flat(el, depth.positive? ? depth - 1 : depth)) else out << el end end out end |
#flat_dynamic(recv, depth) ⇒ Object
Array.prototype.flat(depth) where depth is a DYNAMIC value (#2094)
-- e.g. items.flat(props.depth) -- rather than a compile-time
literal. Coerces depth via JS ToIntegerOrInfinity (truncate toward
zero; NaN/non-numeric -> 0; negative -> 0; +Infinity or a huge finite
value -> flatten fully) and delegates to flat.
This is a SEPARATE method from flat, not a smarter overload of it:
flat's depth parameter treats -1 as a compile-time SENTINEL
meaning "the source literally wrote Infinity" (the parser's own
normalisation, baked into the emitted template). A genuinely dynamic
depth value that happens to be -1 at render time means the
JS-correct OPPOSITE: .flat(-1) never recurses (same as .flat(0),
a shallow copy), because real JS only recurses when depth > 0.
Reusing flat's int contract for a raw dynamic value would silently
invert that case, so this coerces FIRST -- mapping a real +Infinity
/ huge finite value to flat's own -1 sentinel, and a real negative
value to 0 -- and only then delegates to flat's recursion. Mirrors
Go's FlatDynamicDepth/coerceFlatDepth (adapter-go-template/runtime/bf.go).
1032 1033 1034 |
# File 'lib/barefoot_js.rb', line 1032 def flat_dynamic(recv, depth) flat(recv, coerce_flat_depth(depth)) end |
#flat_map(recv, key_kind, key) ⇒ Object
Array.prototype.flatMap(fn) value-returning field projection: map
each element through a self/field projection, then flatten one level.
1038 1039 1040 1041 1042 1043 |
# File 'lib/barefoot_js.rb', line 1038 def flat_map(recv, key_kind, key) return [] unless recv.is_a?(Array) projected = recv.map { |el| key_kind == 'field' ? field_value(el, key) : el } flat(projected, 1) end |
#flat_map_eval(recv, proj_json, param, base_env = {}) ⇒ Object
1356 1357 1358 |
# File 'lib/barefoot_js.rb', line 1356 def flat_map_eval(recv, proj_json, param, base_env = {}) Evaluator.flat_map_json(recv, proj_json, param, base_env) end |
#flat_map_tuple(recv, *specs) ⇒ Object
Array.prototype.flatMap(i => [i.a, i.b]) -- array-literal tuple
projection. Each spec is [kind, key] (['self', ''] or
['field', 'a']).
1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 |
# File 'lib/barefoot_js.rb', line 1048 def flat_map_tuple(recv, *specs) return [] unless recv.is_a?(Array) out = [] recv.each do |el| specs.each do |kind, key| out << (kind == 'field' ? field_value(el, key) : el) end end out end |
#floor(value) ⇒ Object
475 476 477 478 |
# File 'lib/barefoot_js.rb', line 475 def floor(value) n = number(value) finite_number?(n) ? n.floor : n end |
#format_date(recv, pattern, tz, names = []) ⇒ Object
format_date(recv, pattern, tz, names) (#2324/#2334, spec entry
"format_date" in spec/template-helpers.md) -- the lowering target for
formatDate(date, pattern, timeZone, names)
(packages/client/src/format-date.ts, the JS-normative reference this
must match byte-for-byte). Total and deterministic: no locale, no host
timezone, no Time.now.
recv: the same receiver contract as date above -- a native Time or
an ISO-8601 string, normalized identically. A nil / unparseable
receiver renders '' (never raises mid-render).
tz (#2344): 'UTC', a range-valid fixed offset ±HH:MM (shifts the
instant by sign*(HH*60+MM) minutes), or a canonical IANA zone name
('Asia/Tokyo') resolved through tzdata via the tzinfo gem -- the
zone's UTC offset AT THE INSTANT being formatted (DST-aware,
historical-transition-aware, seconds precision: pre-standard LMT
offsets like Tokyo's +09:18:59 count). ANY other value -- an unknown
zone, a non-canonical spelling, a malformed or out-of-range offset
('+9:00', '+25:00') -- raises ArgumentError, aborting the render
loudly: the JS reference throws a RangeError there, and a silently
substituted timezone is the one failure mode this helper must not
have (the pre-#2344 normalize-to-UTC total function is gone). The
shifted instant's UTC calendar fields (not the original instant's)
are what pattern tokens read -- the shifted UTC clock face IS the
local clock face in that zone. Time
arithmetic here is exact (Ruby represents the instant as a Rational,
not a floor-divided epoch-millis integer), so there's no pre-1970
negative-epoch floor-division trap to dodge the way a naive
epoch-millis-div-86400000 implementation would hit; the shifted
Time#wday (0 = Sunday) is likewise exact across the epoch boundary,
so it doubles directly as the weekday-name table index.
names: the flat table (#2334) -- [0..11] wide months, [12..23]
abbreviated months, [24..30] wide weekdays (Sunday-first, matching
the shifted Time#wday), [31..37] abbreviated weekdays. The caller
owns the values (locale selection is never this method's job). An
index past a missing/short table renders '' rather than raising.
pattern: longest-match token substitution
(YYYY|MMMM|MMM|MM|DD|dddd|ddd|M|D); every other character --
including multi-byte ones like 年/月/日 -- passes through literally.
YYYY is abs(year) zero-padded to 4 digits, --prefixed for a
negative year; MM/DD zero-pad to 2; M/D are bare; MMMM/MMM
read the month's wide/abbreviated name; dddd/ddd read the shifted
weekday's wide/abbreviated name.
627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 |
# File 'lib/barefoot_js.rb', line 627 def format_date(recv, pattern, tz, names = []) t = if recv.is_a?(Time) recv else begin Time.iso8601(recv.to_s) rescue ArgumentError nil end end return '' if t.nil? tz_s = tz.to_s offset_seconds = 0 if tz_s != 'UTC' m = FORMAT_DATE_OFFSET_RE.match(tz_s) offset_seconds = if m (m[1] == '-' ? -1 : 1) * ((m[2].to_i * 60) + m[3].to_i) * 60 else format_date_zone_offset(tz_s, t) end end shifted = (t.utc + offset_seconds).utc year = shifted.year month = shifted.mon day = shifted.mday weekday = shifted.wday # 0 = Sunday, matching the table's Sunday-first layout yyyy = (year.negative? ? '-' : '') + year.abs.to_s.rjust(4, '0') name_at = ->(index) { (names[index] || '').to_s } pattern.gsub(FORMAT_DATE_TOKEN_RE) do |token| case token when 'YYYY' then yyyy when 'MMMM' then name_at.call(FORMAT_DATE_MONTHS_WIDE + month - 1) when 'MMM' then name_at.call(FORMAT_DATE_MONTHS_ABBR + month - 1) when 'MM' then format('%02d', month) when 'M' then month.to_s when 'DD' then format('%02d', day) when 'D' then day.to_s when 'dddd' then name_at.call(FORMAT_DATE_WEEKDAYS_WIDE + weekday) when 'ddd' then name_at.call(FORMAT_DATE_WEEKDAYS_ABBR + weekday) end end end |
#format_date_zone_offset(tz, t) ⇒ Object
Resolve a canonical IANA zone ID's UTC offset (whole seconds) at
instant t through the tzinfo gem (#2344) -- TZInfo resolves the
exact-case canonical ID set every backend shares, with full
historical transitions (LMT seconds included). The gem is required
lazily so the fixed-offset/'UTC' paths -- and every other helper --
keep working on a gem-less stdlib install; reaching a named zone
without the gem raises the same loud ArgumentError as an unknown
zone (never a silent UTC fallback).
683 684 685 686 687 688 689 690 691 692 693 694 695 696 |
# File 'lib/barefoot_js.rb', line 683 def format_date_zone_offset(tz, t) begin require 'tzinfo' rescue LoadError raise ArgumentError, "format_date: IANA timeZone #{tz.inspect} requires the tzinfo gem (~> 2.0)" end begin zone = TZInfo::Timezone.get(tz) rescue TZInfo::InvalidTimezoneIdentifier raise ArgumentError, "format_date: unresolvable timeZone #{tz.inspect}" end zone.period_for_utc(t.utc).observed_utc_offset end |
#get(collection, key) ⇒ Object
Runtime-polymorphic element/key access for arr[index] /
hash[key] (emitIndexAccessRuby, expr/operand.ts). Row hashes
deserialize with symbolize_names: true, so a dynamic key whose
runtime value is a String (e.g. a destructured .map() param used
as a key, tone[k]) misses a Symbol-keyed Hash outright -- and the
shared analyzer can't tell at compile time whether an index will
turn out to be a string key or a numeric index (it types a
destructured param {kind:'unknown'}), so no compile-time branch
can pick the right access form (#2491). Mirrors getFieldValue's
(Go adapter, bf.go) case-tolerant lookup and Twig's attribute():
for a Hash, try the key as-is, then as a Symbol, then as a String;
for an Array, index numerically; anything else has no lookup to
perform.
813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 |
# File 'lib/barefoot_js.rb', line 813 def get(collection, key) return nil if key.nil? case collection when Hash return collection[key] if collection.key?(key) sym = key.respond_to?(:to_sym) ? key.to_sym : nil return collection[sym] if sym && collection.key?(sym) str = key.to_s return collection[str] if collection.key?(str) nil when Array # Only an INTEGRAL index reads an element. `key.to_i` would be # wrong on both ends: it coerces a non-numeric String to 0 # (`arr["nope"]` must be nil, not the first element) and it # truncates a fractional number (JS `arr[1.2]` is a property # lookup, i.e. undefined -- not index 1). idx = case key when Integer then key when Numeric then (key % 1).zero? ? key.to_i : nil when String then (key =~ /\A-?\d+\z/ ? key.to_i : nil) end return nil if idx.nil? # JS-semantics negative index is "not found" (`arr[-1] === # undefined`), NOT Ruby's native from-the-end wraparound -- guard # it explicitly so this stays a superset of bracket access rather # than a divergent one. return nil if idx.negative? collection[idx] end end |
#h(value) ⇒ Object
HTML-escaping helper for text interpolation (<%= bf.h(expr) %> --
stdlib ERB does not auto-escape). JS-style stringification via
string (numbers per JS Number#toString, nil -> "", booleans ->
"true"/"false"), then HTML-escaped. A SafeString (already-finished
HTML forwarded from a parent's capture -- see that class's docstring)
passes through unescaped, matching Twig/Blade/Kolon's safe-string
bypass on their own auto-escaping {{ }}/<: :> output tags.
450 451 452 453 454 |
# File 'lib/barefoot_js.rb', line 450 def h(value) return value if value.is_a?(SafeString) html_escape(string(value)) end |
#has_unsafe_style_value?(value) ⇒ Boolean
Mirrors Hono's own CSS-injection guard (hono/jsx/utils.ts's
hasUnsafeStyleValue -- the ORACLE a dynamic style={{...}} value
must match, #2261): a hand-rolled structural scan for characters that
could break out of a CSS declaration, NOT real CSSOM property
validation. Ported byte-for-byte -- every character this scan tests
is ASCII, so scanning by byte agrees with Hono's UTF-16-code-unit
scan for every input; a multibyte UTF-8 sequence has no byte in the
ASCII range, so it can never spuriously match one of these
single-byte comparisons. Skips the reference implementation's regex
fast-path (a pure optimization -- the scan below already returns
false promptly for a clean value).
862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 |
# File 'lib/barefoot_js.rb', line 862 def has_unsafe_style_value?(value) bytes = value.b quote = 0 block_stack = [] i = 0 len = bytes.bytesize while i < len c = bytes.getbyte(i) if c == 92 # \ return true if i == len - 1 i += 1 elsif quote != 0 return true if c == 10 || c == 12 || c == 13 quote = 0 if c == quote elsif c == 47 && i + 1 < len && bytes.getbyte(i + 1) == 42 # "/*" endi = bytes.index('*/', i + 2) return true if endi.nil? i = endi + 1 elsif c == 34 || c == 39 # " or ' quote = c elsif c == 40 # ( block_stack.push(41) elsif c == 91 # [ block_stack.push(93) elsif c == 123 || c == 125 # { or } return true elsif c == 41 || c == 93 # ) or ] return true if block_stack.empty? || block_stack.last != c block_stack.pop elsif c == 59 && block_stack.empty? # ; return true end i += 1 end quote != 0 || !block_stack.empty? end |
#hydration_attrs ⇒ Object
Emits bf-h="<host>" bf-m="<slot>" bf-r="" conditionally. See
spec/compiler.md "Slot identity".
104 105 106 107 108 109 110 111 112 |
# File 'lib/barefoot_js.rb', line 104 def hydration_attrs parts = [] host = _bf_parent mount = _bf_mount parts << %(bf-h="#{host.gsub('"', '"')}") if host && !host.empty? parts << %(bf-m="#{mount.gsub('"', '"')}") if mount && !mount.empty? parts << 'bf-r=""' unless _is_child parts.join(' ') end |
#includes(recv, elem) ⇒ Object
Array.prototype.includes(x) / String.prototype.includes(sub)
share a method name in JS; dispatch on Ruby class the way BarefootJS.pm
dispatches on ref(). The Array arm scans with
Evaluator.same_value_zero? (SameValueZero: no cross-type coercion,
e.g. [2].includes("2") is false; NaN matches NaN) -- the same
algorithm the evaluator's serialized-callback array-method path uses
for .includes, so both positions agree.
709 710 711 712 713 714 715 716 |
# File 'lib/barefoot_js.rb', line 709 def includes(recv, elem) return recv.any? { |item| Evaluator.same_value_zero?(item, elem) } if recv.is_a?(Array) return false if recv.is_a?(Hash) s = recv.nil? ? '' : string(recv) needle = elem.nil? ? '' : string(elem) s.include?(needle) end |
#index_of(recv, elem) ⇒ Object
Array.prototype.indexOf(x) / .lastIndexOf(x) -- value-equality
search. Non-array receivers return -1.
925 926 927 |
# File 'lib/barefoot_js.rb', line 925 def index_of(recv, elem) array_index_of(recv, elem, false) end |
#join(recv, sep = nil) ⇒ Object
Array.prototype.join(sep) with JS semantics: separator defaults to
",", undefined/null elements render as empty.
780 781 782 783 784 785 |
# File 'lib/barefoot_js.rb', line 780 def join(recv, sep = nil) return '' unless recv.is_a?(Array) sep = ',' if sep.nil? recv.map { |el| string(el) }.join(sep) end |
#json(value) ⇒ Object
419 420 421 |
# File 'lib/barefoot_js.rb', line 419 def json(value) backend.encode_json(value) end |
#last_index_of(recv, elem) ⇒ Object
929 930 931 |
# File 'lib/barefoot_js.rb', line 929 def last_index_of(recv, elem) array_index_of(recv, elem, true) end |
#lc(s) ⇒ Object
String.prototype.toLowerCase() / .toUpperCase().
770 771 772 |
# File 'lib/barefoot_js.rb', line 770 def lc(s) s.nil? ? '' : string(s).downcase end |
#length(recv) ⇒ Object
.length works on both arrays (element count) and strings. The
string branch counts UTF-16 CODE UNITS, matching JS
String.prototype.length (#2255) -- NOT Ruby's native String#length
(Unicode codepoints). A codepoint outside the Basic Multilingual
Plane (astral, U+10000-U+10FFFF -- e.g. '👍') is a surrogate PAIR in
UTF-16, so it counts as 2, not 1; '日本語' is 3 either way (BMP-only).
793 794 795 796 797 798 |
# File 'lib/barefoot_js.rb', line 793 def length(recv) return recv.length if recv.is_a?(Array) return 0 if recv.is_a?(Hash) || recv.nil? string(recv).each_char.sum { |c| c.ord > 0xFFFF ? 2 : 1 } end |
#map_eval(recv, proj_json, param, base_env = {}) ⇒ Object
1360 1361 1362 |
# File 'lib/barefoot_js.rb', line 1360 def map_eval(recv, proj_json, param, base_env = {}) Evaluator.map_json(recv, proj_json, param, base_env) end |
#max(a, b) ⇒ Object
503 504 505 506 507 508 509 510 |
# File 'lib/barefoot_js.rb', line 503 def max(a, b) x = number(a) y = number(b) return x if nan_number?(x) return y if nan_number?(y) x > y ? x : y end |
#min(a, b) ⇒ Object
Math.min(a, b) / Math.max(a, b) -- two-arg forms only (#2168
math-methods). JS returns NaN if either operand is NaN. number()
may return a plain Integer (no #nan?), so guard like
finite_number? above rather than calling #nan? unconditionally.
494 495 496 497 498 499 500 501 |
# File 'lib/barefoot_js.rb', line 494 def min(a, b) x = number(a) y = number(b) return x if nan_number?(x) return y if nan_number?(y) x < y ? x : y end |
#number(value) ⇒ Object
JS Number(v) mirror: numeric / boolean inputs convert as expected;
non-numeric / nil yield real numeric NaN so downstream arithmetic
propagates correctly (Math.floor(NaN) === NaN).
466 467 468 469 470 471 472 473 |
# File 'lib/barefoot_js.rb', line 466 def number(value) return Float::NAN if value.nil? return value ? 1 : 0 if value.is_a?(TrueClass) || value.is_a?(FalseClass) return value if value.is_a?(Numeric) return Float(value.strip) if value.is_a?(String) && value.strip =~ NUMERIC_STRING_RE Float::NAN end |
#pad_end(recv, target, pad_str = nil) ⇒ Object
1245 1246 1247 |
# File 'lib/barefoot_js.rb', line 1245 def pad_end(recv, target, pad_str = nil) pad_string(recv.nil? ? '' : string(recv), target, pad_str, false) end |
#pad_start(recv, target, pad_str = nil) ⇒ Object
String.prototype.padStart / padEnd.
1241 1242 1243 |
# File 'lib/barefoot_js.rb', line 1241 def pad_start(recv, target, pad_str = nil) pad_string(recv.nil? ? '' : string(recv), target, pad_str, true) end |
#props_attr ⇒ Object
124 125 126 127 128 129 130 131 132 133 134 |
# File 'lib/barefoot_js.rb', line 124 def props_attr props = _props return '' unless props && !props.empty? # The JSON must be attribute-escaped: a raw `'` inside a string value # (e.g. a blog paragraph) terminates the single-quoted attribute and # truncates the hydration payload. The browser entity-decodes the # attribute value, so the client's JSON.parse sees the original text. json = html_escape(backend.encode_json(props)) %( bf-p='#{json}') end |
#provide_context(name, value) ⇒ Object
151 152 153 154 |
# File 'lib/barefoot_js.rb', line 151 def provide_context(name, value) CONTEXT_STACKS[name].push(value) '' end |
#query(base, *triples) ⇒ Object
queryHref(base, { ... }) (#2042) -- build "base?k=v&..." from a flat
list of (guard, key, value) triples. A pair is included iff its guard
is truthy AND its value is a non-empty string. A value may instead be
an Array, which APPENDS one pair per non-empty member. Repeating a key
overwrites the value at its first position (URLSearchParams.set
semantics); array members always append (.append semantics).
1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 1222 1223 1224 1225 1226 1227 1228 1229 |
# File 'lib/barefoot_js.rb', line 1198 def query(base, *triples) b = base.nil? ? '' : string(base) pairs = [] pos = {} i = 0 while i + 2 < triples.length guard, key, val = triples[i], triples[i + 1], triples[i + 2] i += 3 next unless truthy?(guard) k = key.nil? ? '' : string(key) if val.is_a?(Array) val.each do |m| sm = string(m) pairs << [k, sm] unless sm.empty? end next end v = val.nil? ? '' : string(val) next if v.empty? if pos.key?(k) pairs[pos[k]][1] = v else pos[k] = pairs.length pairs << [k, v] end end return b if pairs.empty? "#{b}?#{pairs.map { |pk, pv| "#{form_escape(pk)}=#{form_escape(pv)}" }.join('&')}" end |
#reduce(recv, opts = {}) ⇒ Object
Fold an array into a scalar via the arithmetic-fold catalogue
(legacy, pre-#2018 path; reduce_eval below handles arbitrary
reducer bodies). opts: { op: '+'|'*', key_kind:, key:, type: 'numeric'|'string', init:, direction: 'left'|'right' }.
1291 1292 1293 1294 1295 1296 1297 1298 1299 1300 1301 1302 1303 1304 1305 1306 1307 1308 1309 1310 1311 1312 1313 1314 1315 1316 1317 1318 |
# File 'lib/barefoot_js.rb', line 1291 def reduce(recv, opts = {}) op = opts[:op] || '+' key_kind = opts[:key_kind] || 'self' key = opts[:key] || '' type = opts[:type] || 'numeric' direction = opts[:direction] || 'left' items = recv.is_a?(Array) ? recv.dup : [] items.reverse! if direction == 'right' project = lambda { |item| key_kind == 'field' ? field_value(item, key) : item } if type == 'string' acc = opts[:init].nil? ? '' : string(opts[:init]) items.each { |item| acc += string(project.call(item)) } return acc end # `init` rides through the adapter as whatever literal the template # emits -- often a numeric-looking String (JSON-decoded, not a Ruby # numeric literal) -- so route it through the same numeric coercion # as every per-element projection rather than trusting its Ruby class. acc = opts[:init].nil? ? 0 : numeric_or_zero(opts[:init]) items.each do |item| n = numeric_or_zero(project.call(item)) acc = op == '*' ? acc * n : acc + n end acc end |
#reduce_eval(recv, body_json, acc_name, item_name, init, direction = 'left', base_env = {}) ⇒ Object
1332 1333 1334 |
# File 'lib/barefoot_js.rb', line 1332 def reduce_eval(recv, body_json, acc_name, item_name, init, direction = 'left', base_env = {}) Evaluator.fold_json(recv, body_json, acc_name, item_name, init, direction, base_env) end |
#register_child_renderer(name, renderer) ⇒ Object
Register a renderer for render_child(name, ...). renderer is
called as renderer.call(props_hash, invoking_bf) -- the invoking
Context matters because a renderer registered on the root may be
called from a nested child render, and the grandchild's scope / slot
identity must chain off the CALLER's scope id, not the registrant's
(#1897).
256 257 258 |
# File 'lib/barefoot_js.rb', line 256 def register_child_renderer(name, renderer) _child_renderers[name] = renderer end |
#register_components_from_manifest(manifest, signal_init: {}) ⇒ Object
Bulk registration from build manifest
vite build (via @barefootjs/erb/vite's barefoot() plugin) emits
dist/templates/manifest.json describing every component the page
might invoke. This walks that manifest and
registers one child renderer per UI registry entry (ui/<name>/index
-> slot key <name>), seeding each child's template vars from the
manifest's statically-derived ssrDefaults (prop destructure
defaults + signal/memo initial values). signal_init[slot_key] is an
opt-in override for defaults the static extractor can't see through.
288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 |
# File 'lib/barefoot_js.rb', line 288 def register_components_from_manifest(manifest, signal_init: {}) parent_scope = _scope_id parent = self manifest.each do |entry_name, entry| next if entry_name.to_s == '__barefoot__' m = entry_name.to_s.match(%r{\Aui/([^/]+)/index\z}) next unless m slot_key = m[1] marked = entry[:markedTemplate] || '' next if marked.empty? template_name = marked.sub(%r{\Atemplates/}, '').sub(/\.erb\z/, '') sig_init = signal_init[slot_key] manifest_defaults = entry[:ssrDefaults] register_child_renderer(slot_key, lambda do |props, caller| host = caller || parent host_scope = host._scope_id || parent_scope child_bf = self.class.new(parent.backend) slot_id = props.delete(:_bf_slot) data_key = props.delete(:key) child_bf._data_key(data_key) unless data_key.nil? child_bf._scope_id( slot_id ? "#{host_scope}_#{slot_id}" : "#{template_name}_#{rand.to_s[2, 6]}", ) child_bf._is_child(true) if slot_id child_bf._bf_parent(host_scope) child_bf._bf_mount(slot_id) end # Share the root registry so the child's own template can render # further imported components (#1897). child_bf._child_renderers(parent._child_renderers) child_bf._scripts(parent._scripts) child_bf._script_seen(parent._script_seen) child_bf._preloads(parent._preloads) child_bf._preload_seen(parent._preload_seen) extra = if sig_init sig_init.call(props) elsif manifest_defaults self.class.send(:derive_vars_from_defaults, manifest_defaults, props) else {} end html = parent.backend.render_named(template_name, child_bf, props.merge(extra)) html.chomp end) end end |
#register_preload(path) ⇒ Object
Register a <link rel="modulepreload"> hint (mirrors register_script
exactly: same dedup-by-path set, same insertion-order array, same
lifetime/reset semantics, same no-output-string return so the compiled
ERB template's <% bf.register_preload(...) %> produces no bytes
where it sits). The <link> itself is only ever emitted by scripts
below, never here -- a preload registration must never inject a node
into a component's own template output (see the previous attempt's
regression this guards against).
230 231 232 233 234 235 |
# File 'lib/barefoot_js.rb', line 230 def register_preload(path) return if _preload_seen.key?(path) _preload_seen[path] = true _preloads.push(path) end |
#register_script(path) ⇒ Object
Script Registration
215 216 217 218 219 220 |
# File 'lib/barefoot_js.rb', line 215 def register_script(path) return if _script_seen.key?(path) _script_seen[path] = true _scripts.push(path) end |
#render_child(name, *args) ⇒ Object
260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 |
# File 'lib/barefoot_js.rb', line 260 def render_child(name, *args) renderer = _child_renderers[name] raise "No renderer registered for child component '#{name}'" unless renderer # Accept both `render_child(name, k: v, ...)` (kwargs collapse into a # trailing Hash under Ruby's argument-splat rules) and the explicit # single-Hash form `render_child(name, { k: v })`. props = (args.length == 1 && args[0].is_a?(Hash)) ? args[0].dup : Hash[*args] # JSX children come in via the ERB backend's content-capture buffer # slice; materialize it through the backend so the child renderer sees # `props[:children]` as already-rendered HTML. Guard on `key?` so a # childless invocation doesn't gain a spurious `children: nil` key. props[:children] = backend.materialize(props[:children]) if props.key?(:children) renderer.call(props, self) end |
#repeat(recv, count) ⇒ Object
String.prototype.repeat(n) -- a count <= 0 degrades to '' rather
than raising (JS throws RangeError for negative counts; SSR
templates degrade instead of dying mid-render).
1234 1235 1236 1237 1238 |
# File 'lib/barefoot_js.rb', line 1234 def repeat(recv, count) s = recv.nil? ? '' : string(recv) n = count.nil? ? 0 : count.to_i n.positive? ? s * n : '' end |
#replace(recv, pattern, replacement) ⇒ Object
String.prototype.replace(pattern, replacement) -- string-pattern
form only, replacing the FIRST occurrence, literally (no regex
metacharacters, no $1-style replacement interpolation).
1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 |
# File 'lib/barefoot_js.rb', line 1148 def replace(recv, pattern, replacement) s = recv.nil? ? '' : string(recv) o = pattern.nil? ? '' : string(pattern) n = replacement.nil? ? '' : string(replacement) return n + s if o.empty? idx = s.index(o) return s if idx.nil? s[0...idx] + n + s[(idx + o.length)..] end |
#replace_all(recv, pattern, replacement) ⇒ Object
String.prototype.replaceAll(pattern, replacement) -- string-pattern
form only (#2182), replacing EVERY occurrence (the all-occurrences
sibling of replace above). Deliberately NOT String#gsub: Ruby's
gsub interprets \1 / \& backreference syntax in the replacement
even for a literal string pattern ("abc".gsub("b", "\\1") -> "ac",
not the literal "\1"), which would diverge from .replace's literal
splice above and from the other backends' literal treatment. The
index/splice loop keeps the replacement literal, matching replace.
An empty pattern inserts at every boundary, including before the
first and after the last character ("abc".replaceAll("", "X") ->
"XaXbXcX"), matching JS.
1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 |
# File 'lib/barefoot_js.rb', line 1171 def replace_all(recv, pattern, replacement) s = recv.nil? ? '' : string(recv) o = pattern.nil? ? '' : string(pattern) n = replacement.nil? ? '' : string(replacement) return ([''] + s.chars + ['']).join(n) if o.empty? # `+''` (not the frozen `''` literal under frozen_string_literal) # so `<<` can append in place instead of `+=` reallocating a new # string each iteration (quadratic for long inputs / many matches). out = +'' pos = 0 loop do idx = s.index(o, pos) break if idx.nil? out << s[pos...idx] << n pos = idx + o.length end out << s[pos..] end |
#reverse(recv) ⇒ Object
Array.prototype.reverse() / .toReversed() -- always returns a new
Array (SSR renders a snapshot; the mutate-vs-copy JS distinction is
moot here).
993 994 995 |
# File 'lib/barefoot_js.rb', line 993 def reverse(recv) recv.is_a?(Array) ? recv.reverse : [] end |
#revoke_context(name) ⇒ Object
156 157 158 159 160 |
# File 'lib/barefoot_js.rb', line 156 def revoke_context(name) stack = CONTEXT_STACKS[name] stack.pop unless stack.empty? '' end |
#round(value) ⇒ Object
485 486 487 488 |
# File 'lib/barefoot_js.rb', line 485 def round(value) n = number(value) finite_number?(n) ? (n + 0.5).floor : n end |
#scope_attr ⇒ Object
bf-s is the addressable scope id only (#1249).
98 99 100 |
# File 'lib/barefoot_js.rb', line 98 def scope_attr _scope_id || '' end |
#scope_comment ⇒ Object
See spec/compiler.md "Slot identity" for the comment-scope wire format.
192 193 194 195 196 197 198 199 200 201 |
# File 'lib/barefoot_js.rb', line 192 def scope_comment scope_id = _scope_id || '' host_segment = '' host = _bf_parent mount = _bf_mount host_segment = "|h=#{host}|m=#{mount || ''}" if host && !host.empty? props_json = '' props_json = "|#{backend.encode_json(_props)}" if _props && !_props.empty? "<!--bf-scope:#{scope_id}#{host_segment}#{props_json}-->" end |
#scope_comment_end ⇒ Object
Paired end marker for scope_comment, emitted after the fragment's
last top-level node. No host/props segments -- the client only needs
the scope id to close the boundary (#2289).
206 207 208 209 |
# File 'lib/barefoot_js.rb', line 206 def scope_comment_end scope_id = _scope_id || '' "<!--bf-/scope:#{scope_id}-->" end |
#scripts ⇒ Object
Emits preload hints BEFORE script tags -- a hint that arrives after the script it describes is useless. Same escaping (none; paths are caller-trusted resolved URLs) as the script tags below.
240 241 242 243 244 |
# File 'lib/barefoot_js.rb', line 240 def scripts = _preloads.map { |path| %(<link rel="modulepreload" crossorigin href="#{path}">) } = _scripts.map { |path| %(<script type="module" src="#{path}"></script>) } ( + ).join("\n") end |
#search_params(query = '') ⇒ Object
search_params(query = '') -- request-scoped reader for the reactive
searchParams() environment signal (router v0.5, #1922), built from a
raw query string. The compiled template reads it via
v[:searchParams].get('key').
89 90 91 |
# File 'lib/barefoot_js.rb', line 89 def search_params(query = '') SearchParams.new(query) end |
#slice(recv, start, end_ = nil) ⇒ Object
Array.prototype.slice(start, end?) AND String.prototype.slice
(the string-slice divergence) -- the adapter emits the same
bf_slice call for both receiver shapes (it can't disambiguate
string vs. array at compile time), so this dispatches on Ruby
class, mirroring includes above. Mirrors the Go/Perl bf_slice
/ slice arithmetic so adapter output stays symmetric.
String#length / #[] already index by character (not byte) for
a UTF-8-encoded string, matching JS except for astral-plane input
(the same divergence boundary every other adapter's pad/trim
helpers already accept).
968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 |
# File 'lib/barefoot_js.rb', line 968 def slice(recv, start, end_ = nil) return [] unless recv.is_a?(Array) || recv.is_a?(String) empty = recv.is_a?(String) ? '' : [] len = recv.length return empty if len.zero? s = start.nil? ? 0 : start.to_i s = len + s if s.negative? s = 0 if s.negative? s = len if s > len e = end_.nil? ? len : end_.to_i e = len + e if e.negative? e = 0 if e.negative? e = len if e > len return empty if s >= e recv[s...e] end |
#some(recv, pred) ⇒ Object
736 737 738 739 740 |
# File 'lib/barefoot_js.rb', line 736 def some(recv, pred) return false unless recv.is_a?(Array) recv.any? { |item| pred.call(item) } end |
#some_eval(recv, pred_json, param, base_env = {}) ⇒ Object
1344 1345 1346 |
# File 'lib/barefoot_js.rb', line 1344 def some_eval(recv, pred_json, param, base_env = {}) Evaluator.some_json(recv, pred_json, param, base_env) end |
#sort(recv, opts = {}) ⇒ Object
Array.prototype.sort(cmp) / .toSorted(cmp) -- fixed comparator
catalogue (legacy, pre-#2018 path; sort_eval below handles arbitrary
comparator bodies). opts[:keys] is a priority-ordered list of
{ key_kind:, key:, compare_type:, direction: }. Stable (ties break
on original index) and non-mutating.
1254 1255 1256 1257 1258 1259 1260 1261 1262 1263 1264 1265 1266 1267 1268 1269 1270 1271 1272 1273 1274 1275 1276 1277 1278 1279 1280 1281 1282 1283 1284 1285 |
# File 'lib/barefoot_js.rb', line 1254 def sort(recv, opts = {}) return [] unless recv.is_a?(Array) spec = (opts[:keys] || []).map do |k| { key_kind: k[:key_kind] || 'self', key: k[:key] || '', compare_type: k[:compare_type] || 'numeric', direction: k[:direction] || 'asc', } end return recv.dup if spec.empty? decorated = recv.each_with_index.map do |item, idx| keys = spec.map { |sp| sp[:key_kind] == 'field' ? field_value(item, sp[:key]) : item } [keys, item, idx] end sorted = decorated.sort do |x, y| result = 0 spec.each_index do |i| c = compare_sort_key(x[0][i], y[0][i], spec[i][:compare_type]) next if c.zero? result = spec[i][:direction] == 'desc' ? -c : c break end result.zero? ? (x[2] <=> y[2]) : result end sorted.map { |pair| pair[1] } end |
#sort_eval(recv, cmp_json, param_a, param_b, base_env = {}) ⇒ Object
Evaluator-driven sort / reduce / higher-order predicates (#2018):
the comparator / reducer / predicate body rides as a serialized-
ParsedExpr JSON string and is evaluated per element, delegating to
the shared BarefootJS::Evaluator. find_eval / find_index_eval
take a forward flag (false -> findLast / findLastIndex).
1328 1329 1330 |
# File 'lib/barefoot_js.rb', line 1328 def sort_eval(recv, cmp_json, param_a, param_b, base_env = {}) Evaluator.sort_by_json(recv, cmp_json, param_a, param_b, base_env) end |
#split(recv, sep = nil, limit = nil) ⇒ Object
String.prototype.split(sep) -- string -> Array. An empty separator
splits into individual characters; a nil separator returns the whole
string in a single-element Array; trailing empty fields are kept
(JS parity -- Ruby's String#split(str, -1) already matches this,
and (unlike a Regexp) a String separator is matched literally).
1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 |
# File 'lib/barefoot_js.rb', line 1100 def split(recv, sep = nil, limit = nil) s = (recv.nil? || recv.is_a?(Array) || recv.is_a?(Hash)) ? '' : string(recv) parts = if sep.nil? [s] elsif string(sep).empty? s.chars elsif s.empty? [''] else s.split(string(sep), -1) end unless limit.nil? n = limit.to_i if n.zero? parts = [] elsif n.positive? && n < parts.length parts = parts[0...n] end end parts end |
#spread_attrs(bag) ⇒ Object
JSX intrinsic-element spread (#1407)
Mirrors the JS spreadAttrs runtime and the Go/Perl adapters'
spread helpers so SSR output stays byte-equal across adapters.
Generated ERB templates invoke this as <%= bf.spread_attrs(bag) %>.
Skip rules: nil/false values, event handlers (on[A-Z]...), and
children. ref is intentionally NOT filtered (matches the JS
reference). Key remap: className -> class, htmlFor -> for; SVG
camelCase attrs preserved; other camelCase keys lowered to
kebab-case. style routes through style_to_css. Output is
deterministic: keys are sorted alphabetically before emission.
Unlike the Perl/Go ports, no boolean-sentinel detection is needed --
Ruby's true/false are real booleans, distinct from 0/1, so a
bag value's Ruby class alone tells JS-boolean from JS-number.
1382 1383 1384 1385 1386 1387 1388 1389 1390 1391 1392 1393 1394 1395 1396 1397 1398 1399 1400 1401 1402 1403 1404 1405 1406 1407 1408 1409 1410 1411 1412 1413 1414 1415 1416 1417 1418 1419 1420 1421 |
# File 'lib/barefoot_js.rb', line 1382 def spread_attrs(bag) return '' unless bag.is_a?(Hash) parts = [] bag.keys.sort_by(&:to_s).each do |key| key_s = key.to_s if key_s.length > 2 && key_s.start_with?('on') c = key_s[2] next if c.upcase == c end next if key_s == 'children' val = bag[key] next if val.nil? if val.is_a?(TrueClass) || val.is_a?(FalseClass) next unless val parts << to_attr_name(key_s) next end if key_s == 'style' css = style_to_css(val) next if css.nil? || css.empty? parts << %(style="#{html_escape(css)}") next end parts << %(#{to_attr_name(key_s)}="#{html_escape(string(val))}") end return '' if parts.empty? # Mark the result raw so the calling template's plain `<%=` doesn't # need a second escape pass (the backend decides how "raw" is # represented for its engine; ERB's own emit has no auto-escape, so # BarefootJS::Backend::Erb's `mark_raw` is the identity function). backend.mark_raw(parts.join(' ')) end |
#starts_with(recv, prefix, position = nil) ⇒ Object
String.prototype.startsWith(prefix, position?).
1124 1125 1126 1127 1128 1129 1130 1131 1132 |
# File 'lib/barefoot_js.rb', line 1124 def starts_with(recv, prefix, position = nil) s = recv.nil? ? '' : string(recv) p = prefix.nil? ? '' : string(prefix) unless position.nil? n = clamp_index(position.to_i, s.length) s = s[n..] || '' end s.start_with?(p) end |
#streaming_bootstrap ⇒ Object
Streaming SSR (Out-of-Order)
396 397 398 |
# File 'lib/barefoot_js.rb', line 396 def streaming_bootstrap %{<script>(function(){function s(id){var a=document.querySelector('[bf-async="'+id+'"]');var t=document.querySelector('template[bf-async-resolve="'+id+'"]');if(!a||!t)return;a.replaceChildren(t.content.cloneNode(true));a.removeAttribute('bf-async');t.remove();requestAnimationFrame(function(){if(window.__bf_hydrate)window.__bf_hydrate()})};window.__bf_swap=s})()</script>} end |
#string(value) ⇒ Object
JS String(v) mirror, EXCEPT nil renders as '' (not the literal
"null") so an unset prop doesn't surface as literal text in
user-facing HTML -- the same divergence the Go/Perl adapters document
for their string helper. This is the canonical JS-ish stringifier
used throughout this file (join, spread_attrs, h, reduce's string
fold, ...).
429 430 431 432 433 434 435 436 437 438 439 440 441 |
# File 'lib/barefoot_js.rb', line 429 def string(value) return '' if value.nil? return value ? 'true' : 'false' if value.is_a?(TrueClass) || value.is_a?(FalseClass) return Evaluator.number_to_string(value) if value.is_a?(Numeric) # JS `Array.prototype.toString` is `this.join(',')`, applied # recursively -- Ruby's `Array#to_s` (inspect-style, e.g. "[[1], [2]]") # would otherwise leak through here instead of the JS comma-join # (e.g. reached via `.flat(0)`'s shallow copy joined afterwards, # #2262). return value.map { |el| string(el) }.join(',') if value.is_a?(Array) value.to_s end |
#style_object(*pairs) ⇒ Object
Builds the CSS string for a style={{...}} JSX object-literal
attribute (#2261) -- pairs alternates CSS key (always a
compile-time-known literal), then value. A value that fails
has_unsafe_style_value? (after JS-String()-style stringification)
is DROPPED -- the whole key:value pair is omitted -- matching
Hono's oracle behavior exactly. The final joined string is STILL
HTML-escaped (mirroring Hono's own escapeToBuffer call on its
accumulated style string) -- a "safe" value can still carry a
literal "/'/& (e.g. a BALANCED-quote CSS string value like
"hello" passes the structural scan) that would otherwise break out
of the double-quoted style="..." attribute. Returns a SafeString
so bf.h (which the emitter still wraps every dynamic value with)
passes the already-escaped result through unchanged.
912 913 914 915 916 917 918 919 920 921 |
# File 'lib/barefoot_js.rb', line 912 def style_object(*pairs) parts = [] pairs.each_slice(2) do |key, value| v = string(value) next if has_unsafe_style_value?(v) parts << "#{html_escape(key)}:#{html_escape(v)}" end SafeString.new(parts.join(';')) end |
#text_end ⇒ Object
187 188 189 |
# File 'lib/barefoot_js.rb', line 187 def text_end '<!--/-->' end |
#text_start(slot_id) ⇒ Object
183 184 185 |
# File 'lib/barefoot_js.rb', line 183 def text_start(slot_id) "<!--bf:#{slot_id}-->" end |
#to_fixed(value, digits = 0) ⇒ Object
Number.prototype.toFixed(digits) -- fixed-decimal string with
zero-padding, rounding half toward +Infinity (matching round).
1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 |
# File 'lib/barefoot_js.rb', line 1084 def to_fixed(value, digits = 0) n = number(value) return 'NaN' if n.respond_to?(:nan?) && n.nan? return n.negative? ? '-Infinity' : 'Infinity' if n.respond_to?(:infinite?) && n.infinite? digits = 0 if digits.nil? || digits.negative? factor = 10.0**digits rounded = (n * factor + 0.5).floor format("%.#{digits}f", rounded / factor) end |
#trim(recv) ⇒ Object
String.prototype.trim().
1061 1062 1063 1064 1065 |
# File 'lib/barefoot_js.rb', line 1061 def trim(recv) return '' if recv.nil? || recv.is_a?(Array) || recv.is_a?(Hash) string(recv).gsub(/\A\p{Space}+|\p{Space}+\z/, '') end |
#trim_end(recv) ⇒ Object
1076 1077 1078 1079 1080 |
# File 'lib/barefoot_js.rb', line 1076 def trim_end(recv) return '' if recv.nil? || recv.is_a?(Array) || recv.is_a?(Hash) string(recv).sub(/\p{Space}+\z/, '') end |
#trim_start(recv) ⇒ Object
String.prototype.trimStart() / .trimEnd() -- the one-sided
siblings of trim above (#2183 follow-up), same \p{Space} regex
restricted to one side.
1070 1071 1072 1073 1074 |
# File 'lib/barefoot_js.rb', line 1070 def trim_start(recv) return '' if recv.nil? || recv.is_a?(Array) || recv.is_a?(Hash) string(recv).sub(/\A\p{Space}+/, '') end |
#truthy?(value) ⇒ Boolean
JS truthiness (falsy: false, nil, 0, NaN, ""). Delegates to the
shared evaluator so template-emitted conditionals
(if bf.truthy?(x)) and callback-body evaluation agree byte-for-byte.
459 460 461 |
# File 'lib/barefoot_js.rb', line 459 def truthy?(value) Evaluator.truthy?(value) end |
#uc(s) ⇒ Object
774 775 776 |
# File 'lib/barefoot_js.rb', line 774 def uc(s) s.nil? ? '' : string(s).upcase end |
#use_context(name, default = nil) ⇒ Object
162 163 164 165 |
# File 'lib/barefoot_js.rb', line 162 def use_context(name, default = nil) stack = CONTEXT_STACKS[name] stack.empty? ? default : stack.last end |