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:MM('+09:00','-05:30') -- the ONLY non-UTC shapeformat_daterecognizes (mirrors OFFSET_RE in packages/client/src/format-date.ts). /\A([+-])(\d{2}):(\d{2})\z/- FORMAT_DATE_TOKEN_RE =
Longest-match pattern-token alternation (mirrors TOKEN_RE in packages/client/src/format-date.ts). Order matters: MM before M and DD before D so the 2-char token wins at a position where either could match -- Ruby's Onigmo engine, like JS's regex engine, resolves alternation leftmost-first (not POSIX leftmost-longest), so listing the longer alternative first is what makes "MM" consume both characters instead of two "M" matches.
/YYYY|MM|DD|M|D/
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) ⇒ Object
format_date(recv, pattern, tz)(#2324, spec entry "format_date" in spec/template-helpers.md) -- the lowering target forformatDate(date, pattern, timeZone)(packages/client/src/format-date.ts, the JS-normative reference this must match byte-for-byte). -
#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_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
-
#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.
79 80 81 |
# File 'lib/barefoot_js.rb', line 79 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.
337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 |
# File 'lib/barefoot_js.rb', line 337 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).
488 489 490 491 |
# File 'lib/barefoot_js.rb', line 488 def abs(value) n = number(value) nan_number?(n) ? n : n.abs end |
#async_boundary(id, fallback_html) ⇒ Object
375 376 377 378 |
# File 'lib/barefoot_js.rb', line 375 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
380 381 382 |
# File 'lib/barefoot_js.rb', line 380 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).
792 793 794 795 796 797 798 799 800 801 802 803 804 |
# File 'lib/barefoot_js.rb', line 792 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.
177 178 179 |
# File 'lib/barefoot_js.rb', line 177 def bool_str(value) value ? 'true' : 'false' end |
#ceil(value) ⇒ Object
455 456 457 458 |
# File 'lib/barefoot_js.rb', line 455 def ceil(value) n = number(value) finite_number?(n) ? n.ceil : n end |
#comment(text) ⇒ Object
Comment Markers
169 170 171 |
# File 'lib/barefoot_js.rb', line 169 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.
808 809 810 811 812 813 |
# File 'lib/barefoot_js.rb', line 808 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.
114 115 116 117 118 119 120 |
# File 'lib/barefoot_js.rb', line 114 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.
503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 |
# File 'lib/barefoot_js.rb', line 503 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?).
992 993 994 995 996 997 998 999 1000 |
# File 'lib/barefoot_js.rb', line 992 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
638 639 640 641 642 |
# File 'lib/barefoot_js.rb', line 638 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
1197 1198 1199 |
# File 'lib/barefoot_js.rb', line 1197 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.
632 633 634 635 636 |
# File 'lib/barefoot_js.rb', line 632 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
1193 1194 1195 |
# File 'lib/barefoot_js.rb', line 1193 def filter_eval(recv, pred_json, param, base_env = {}) Evaluator.filter_json(recv, pred_json, param, base_env) end |
#find(recv, pred) ⇒ Object
650 651 652 653 654 |
# File 'lib/barefoot_js.rb', line 650 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
1205 1206 1207 |
# File 'lib/barefoot_js.rb', line 1205 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
656 657 658 659 660 661 |
# File 'lib/barefoot_js.rb', line 656 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
1209 1210 1211 |
# File 'lib/barefoot_js.rb', line 1209 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
663 664 665 666 667 668 |
# File 'lib/barefoot_js.rb', line 663 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
670 671 672 673 674 675 |
# File 'lib/barefoot_js.rb', line 670 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.
857 858 859 860 861 862 863 864 865 866 867 868 869 |
# File 'lib/barefoot_js.rb', line 857 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).
889 890 891 |
# File 'lib/barefoot_js.rb', line 889 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.
895 896 897 898 899 900 |
# File 'lib/barefoot_js.rb', line 895 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
1213 1214 1215 |
# File 'lib/barefoot_js.rb', line 1213 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']).
905 906 907 908 909 910 911 912 913 914 915 |
# File 'lib/barefoot_js.rb', line 905 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
450 451 452 453 |
# File 'lib/barefoot_js.rb', line 450 def floor(value) n = number(value) finite_number?(n) ? n.floor : n end |
#format_date(recv, pattern, tz) ⇒ Object
format_date(recv, pattern, tz) (#2324, spec entry "format_date" in
spec/template-helpers.md) -- the lowering target for
formatDate(date, pattern, timeZone) (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: a fixed UTC offset ±HH:MM shifts the instant by
sign*(HH*60+MM) minutes; ANY other value -- 'UTC', an IANA zone name
('Asia/Tokyo'), or a malformed offset ('+9:00') -- normalizes to a
zero-minute shift (UTC), so every backend degrades identically instead
of dragging in host tzdata. 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 at that offset. 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.
pattern: longest-match token substitution (YYYY|MM|DD|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.
573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 |
# File 'lib/barefoot_js.rb', line 573 def format_date(recv, pattern, tz) t = if recv.is_a?(Time) recv else begin Time.iso8601(recv.to_s) rescue ArgumentError nil end end return '' if t.nil? m = FORMAT_DATE_OFFSET_RE.match(tz.to_s) offset_minutes = m ? (m[1] == '-' ? -1 : 1) * ((m[2].to_i * 60) + m[3].to_i) : 0 shifted = (t.utc + (offset_minutes * 60)).utc year = shifted.year month = shifted.mon day = shifted.mday yyyy = (year.negative? ? '-' : '') + year.abs.to_s.rjust(4, '0') pattern.gsub(FORMAT_DATE_TOKEN_RE) do |token| case token when 'YYYY' then yyyy when 'MM' then format('%02d', month) when 'M' then month.to_s when 'DD' then format('%02d', day) when 'D' then day.to_s end 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.
425 426 427 428 429 |
# File 'lib/barefoot_js.rb', line 425 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).
719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 |
# File 'lib/barefoot_js.rb', line 719 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".
102 103 104 105 106 107 108 109 110 |
# File 'lib/barefoot_js.rb', line 102 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.
617 618 619 620 621 622 623 624 |
# File 'lib/barefoot_js.rb', line 617 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.
782 783 784 |
# File 'lib/barefoot_js.rb', line 782 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.
688 689 690 691 692 693 |
# File 'lib/barefoot_js.rb', line 688 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
394 395 396 |
# File 'lib/barefoot_js.rb', line 394 def json(value) backend.encode_json(value) end |
#last_index_of(recv, elem) ⇒ Object
786 787 788 |
# File 'lib/barefoot_js.rb', line 786 def last_index_of(recv, elem) array_index_of(recv, elem, true) end |
#lc(s) ⇒ Object
String.prototype.toLowerCase() / .toUpperCase().
678 679 680 |
# File 'lib/barefoot_js.rb', line 678 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).
701 702 703 704 705 706 |
# File 'lib/barefoot_js.rb', line 701 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
1217 1218 1219 |
# File 'lib/barefoot_js.rb', line 1217 def map_eval(recv, proj_json, param, base_env = {}) Evaluator.map_json(recv, proj_json, param, base_env) end |
#max(a, b) ⇒ Object
478 479 480 481 482 483 484 485 |
# File 'lib/barefoot_js.rb', line 478 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.
469 470 471 472 473 474 475 476 |
# File 'lib/barefoot_js.rb', line 469 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).
441 442 443 444 445 446 447 448 |
# File 'lib/barefoot_js.rb', line 441 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
1102 1103 1104 |
# File 'lib/barefoot_js.rb', line 1102 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.
1098 1099 1100 |
# File 'lib/barefoot_js.rb', line 1098 def pad_start(recv, target, pad_str = nil) pad_string(recv.nil? ? '' : string(recv), target, pad_str, true) end |
#props_attr ⇒ Object
122 123 124 125 126 127 128 129 130 131 132 |
# File 'lib/barefoot_js.rb', line 122 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
149 150 151 152 |
# File 'lib/barefoot_js.rb', line 149 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).
1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 |
# File 'lib/barefoot_js.rb', line 1055 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' }.
1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 |
# File 'lib/barefoot_js.rb', line 1148 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
1189 1190 1191 |
# File 'lib/barefoot_js.rb', line 1189 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).
234 235 236 |
# File 'lib/barefoot_js.rb', line 234 def register_child_renderer(name, renderer) _child_renderers[name] = renderer end |
#register_components_from_manifest(manifest, signal_init: {}) ⇒ Object
Bulk registration from build manifest
bf build 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.
265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 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 |
# File 'lib/barefoot_js.rb', line 265 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) 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_script(path) ⇒ Object
Script Registration
213 214 215 216 217 218 |
# File 'lib/barefoot_js.rb', line 213 def register_script(path) return if _script_seen.key?(path) _script_seen[path] = true _scripts.push(path) end |
#render_child(name, *args) ⇒ Object
238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 |
# File 'lib/barefoot_js.rb', line 238 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).
1091 1092 1093 1094 1095 |
# File 'lib/barefoot_js.rb', line 1091 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).
1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 |
# File 'lib/barefoot_js.rb', line 1005 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.
1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 |
# File 'lib/barefoot_js.rb', line 1028 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).
850 851 852 |
# File 'lib/barefoot_js.rb', line 850 def reverse(recv) recv.is_a?(Array) ? recv.reverse : [] end |
#revoke_context(name) ⇒ Object
154 155 156 157 158 |
# File 'lib/barefoot_js.rb', line 154 def revoke_context(name) stack = CONTEXT_STACKS[name] stack.pop unless stack.empty? '' end |
#round(value) ⇒ Object
460 461 462 463 |
# File 'lib/barefoot_js.rb', line 460 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).
96 97 98 |
# File 'lib/barefoot_js.rb', line 96 def scope_attr _scope_id || '' end |
#scope_comment ⇒ Object
See spec/compiler.md "Slot identity" for the comment-scope wire format.
190 191 192 193 194 195 196 197 198 199 |
# File 'lib/barefoot_js.rb', line 190 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).
204 205 206 207 |
# File 'lib/barefoot_js.rb', line 204 def scope_comment_end scope_id = _scope_id || '' "<!--bf-/scope:#{scope_id}-->" end |
#scripts ⇒ Object
220 221 222 |
# File 'lib/barefoot_js.rb', line 220 def scripts _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').
87 88 89 |
# File 'lib/barefoot_js.rb', line 87 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).
825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 |
# File 'lib/barefoot_js.rb', line 825 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
644 645 646 647 648 |
# File 'lib/barefoot_js.rb', line 644 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
1201 1202 1203 |
# File 'lib/barefoot_js.rb', line 1201 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.
1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 |
# File 'lib/barefoot_js.rb', line 1111 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).
1185 1186 1187 |
# File 'lib/barefoot_js.rb', line 1185 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).
957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 |
# File 'lib/barefoot_js.rb', line 957 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.
1239 1240 1241 1242 1243 1244 1245 1246 1247 1248 1249 1250 1251 1252 1253 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 |
# File 'lib/barefoot_js.rb', line 1239 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?).
981 982 983 984 985 986 987 988 989 |
# File 'lib/barefoot_js.rb', line 981 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)
371 372 373 |
# File 'lib/barefoot_js.rb', line 371 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, ...).
404 405 406 407 408 409 410 411 412 413 414 415 416 |
# File 'lib/barefoot_js.rb', line 404 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.
769 770 771 772 773 774 775 776 777 778 |
# File 'lib/barefoot_js.rb', line 769 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
185 186 187 |
# File 'lib/barefoot_js.rb', line 185 def text_end '<!--/-->' end |
#text_start(slot_id) ⇒ Object
181 182 183 |
# File 'lib/barefoot_js.rb', line 181 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).
941 942 943 944 945 946 947 948 949 950 |
# File 'lib/barefoot_js.rb', line 941 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().
918 919 920 921 922 |
# File 'lib/barefoot_js.rb', line 918 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
933 934 935 936 937 |
# File 'lib/barefoot_js.rb', line 933 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.
927 928 929 930 931 |
# File 'lib/barefoot_js.rb', line 927 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.
434 435 436 |
# File 'lib/barefoot_js.rb', line 434 def truthy?(value) Evaluator.truthy?(value) end |
#uc(s) ⇒ Object
682 683 684 |
# File 'lib/barefoot_js.rb', line 682 def uc(s) s.nil? ? '' : string(s).upcase end |
#use_context(name, default = nil) ⇒ Object
160 161 162 163 |
# File 'lib/barefoot_js.rb', line 160 def use_context(name, default = nil) stack = CONTEXT_STACKS[name] stack.empty? ? default : stack.last end |