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
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
-
#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).
718 719 720 721 722 723 724 725 726 727 728 729 730 |
# File 'lib/barefoot_js.rb', line 718 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.
734 735 736 737 738 739 |
# File 'lib/barefoot_js.rb', line 734 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?).
918 919 920 921 922 923 924 925 926 |
# File 'lib/barefoot_js.rb', line 918 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
564 565 566 567 568 |
# File 'lib/barefoot_js.rb', line 564 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
1123 1124 1125 |
# File 'lib/barefoot_js.rb', line 1123 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.
558 559 560 561 562 |
# File 'lib/barefoot_js.rb', line 558 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
1119 1120 1121 |
# File 'lib/barefoot_js.rb', line 1119 def filter_eval(recv, pred_json, param, base_env = {}) Evaluator.filter_json(recv, pred_json, param, base_env) end |
#find(recv, pred) ⇒ Object
576 577 578 579 580 |
# File 'lib/barefoot_js.rb', line 576 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
1131 1132 1133 |
# File 'lib/barefoot_js.rb', line 1131 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
582 583 584 585 586 587 |
# File 'lib/barefoot_js.rb', line 582 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
1135 1136 1137 |
# File 'lib/barefoot_js.rb', line 1135 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
589 590 591 592 593 594 |
# File 'lib/barefoot_js.rb', line 589 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
596 597 598 599 600 601 |
# File 'lib/barefoot_js.rb', line 596 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.
783 784 785 786 787 788 789 790 791 792 793 794 795 |
# File 'lib/barefoot_js.rb', line 783 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).
815 816 817 |
# File 'lib/barefoot_js.rb', line 815 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.
821 822 823 824 825 826 |
# File 'lib/barefoot_js.rb', line 821 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
1139 1140 1141 |
# File 'lib/barefoot_js.rb', line 1139 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']).
831 832 833 834 835 836 837 838 839 840 841 |
# File 'lib/barefoot_js.rb', line 831 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 |
#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).
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 674 675 676 677 678 679 680 |
# File 'lib/barefoot_js.rb', line 645 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.
543 544 545 546 547 548 549 550 |
# File 'lib/barefoot_js.rb', line 543 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.
708 709 710 |
# File 'lib/barefoot_js.rb', line 708 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.
614 615 616 617 618 619 |
# File 'lib/barefoot_js.rb', line 614 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
712 713 714 |
# File 'lib/barefoot_js.rb', line 712 def last_index_of(recv, elem) array_index_of(recv, elem, true) end |
#lc(s) ⇒ Object
String.prototype.toLowerCase() / .toUpperCase().
604 605 606 |
# File 'lib/barefoot_js.rb', line 604 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).
627 628 629 630 631 632 |
# File 'lib/barefoot_js.rb', line 627 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
1143 1144 1145 |
# File 'lib/barefoot_js.rb', line 1143 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
1028 1029 1030 |
# File 'lib/barefoot_js.rb', line 1028 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.
1024 1025 1026 |
# File 'lib/barefoot_js.rb', line 1024 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).
981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 |
# File 'lib/barefoot_js.rb', line 981 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' }.
1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 |
# File 'lib/barefoot_js.rb', line 1074 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
1115 1116 1117 |
# File 'lib/barefoot_js.rb', line 1115 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).
1017 1018 1019 1020 1021 |
# File 'lib/barefoot_js.rb', line 1017 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).
931 932 933 934 935 936 937 938 939 940 941 |
# File 'lib/barefoot_js.rb', line 931 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.
954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 |
# File 'lib/barefoot_js.rb', line 954 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).
776 777 778 |
# File 'lib/barefoot_js.rb', line 776 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).
751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 |
# File 'lib/barefoot_js.rb', line 751 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
570 571 572 573 574 |
# File 'lib/barefoot_js.rb', line 570 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
1127 1128 1129 |
# File 'lib/barefoot_js.rb', line 1127 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.
1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 |
# File 'lib/barefoot_js.rb', line 1037 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).
1111 1112 1113 |
# File 'lib/barefoot_js.rb', line 1111 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).
883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 |
# File 'lib/barefoot_js.rb', line 883 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.
1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 |
# File 'lib/barefoot_js.rb', line 1165 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?).
907 908 909 910 911 912 913 914 915 |
# File 'lib/barefoot_js.rb', line 907 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.
695 696 697 698 699 700 701 702 703 704 |
# File 'lib/barefoot_js.rb', line 695 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).
867 868 869 870 871 872 873 874 875 876 |
# File 'lib/barefoot_js.rb', line 867 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().
844 845 846 847 848 |
# File 'lib/barefoot_js.rb', line 844 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
859 860 861 862 863 |
# File 'lib/barefoot_js.rb', line 859 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.
853 854 855 856 857 |
# File 'lib/barefoot_js.rb', line 853 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
608 609 610 |
# File 'lib/barefoot_js.rb', line 608 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 |