Class: BarefootJS::Context

Inherits:
Object
  • Object
show all
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). json bubbles backend/marshalling errors loudly rather than producing an empty payload.

/\A\s*[+-]?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?\s*\z/.freeze
FORMAT_DATE_OFFSET_RE =

tz shapes FORMAT_DATE_OFFSET_RE matches: a fixed UTC offset ±HH:MM ('+09:00', '-05:30') -- the ONLY non-UTC shape format_date recognizes (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

Instance Method Summary collapse

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_attrObject

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('&', '&amp;').gsub('"', '&quot;')
  %( 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).

Returns:

  • (Boolean)


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_attrsObject

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('"', '&quot;')}") if host && !host.empty?
  parts << %(bf-m="#{mount.gsub('"', '&quot;')}") 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_attrObject



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_attrObject

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_commentObject

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_endObject

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

#scriptsObject



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_bootstrapObject


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_endObject



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.

Returns:

  • (Boolean)


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