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 within ECMA-402's valid range -- hours 00-23, minutes 00-59 ('+09:00', '-05:30') -- one of the three tz shapes format_date accepts (mirrors OFFSET_RE in packages/client/src/format-date.ts). An out-of-range shape ('+25:00') falls through to the tzdata lookup, fails it, and raises -- matching the JS reference's RangeError (#2344).

/\A([+-])([01]\d|2[0-3]):([0-5]\d)\z/
FORMAT_DATE_TOKEN_RE =

Longest-match pattern-token alternation (mirrors TOKEN_RE in packages/client/src/format-date.ts). Order matters: Onigmo, like JS's regex engine, resolves alternation leftmost-first (not POSIX leftmost-longest), so every multi-char token must be listed before any shorter alternative it prefixes -- YYYY before (nothing shorter shares its prefix), MMMM before MMM before MM before M, DD before D, dddd before ddd. (#2334)

/YYYY|MMMM|MMM|MM|DD|dddd|ddd|M|D/
FORMAT_DATE_MONTHS_WIDE =

names-table section offsets (spec/template-helpers.md "format_date", #2334) -- mirrors MONTHS_WIDE/MONTHS_ABBR/WEEKDAYS_WIDE/WEEKDAYS_ABBR in packages/client/src/format-date.ts.

0
FORMAT_DATE_MONTHS_ABBR =
12
FORMAT_DATE_WEEKDAYS_WIDE =
24
FORMAT_DATE_WEEKDAYS_ABBR =
31

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(backend = nil) ⇒ Context

Returns a new instance of Context.



81
82
83
# File 'lib/barefoot_js.rb', line 81

def initialize(backend = nil)
  @backend = backend
end

Class Method Details

.derive_vars_from_defaults(defaults, props) ⇒ Object

Derive template-var kvs from a manifest entry's ssrDefaults section. Each entry shape: { value:, propName:, isRestProps: }. For isRestProps, the rest bag passes through unchanged (or the static {} if the caller didn't supply one). For ordinary entries the caller's props[propName] wins when present, otherwise the static value does. propName-less entries (signal / memo locals) always use the static value.

Public (not private_class_method): register_components_from_manifest above uses it for the ui/* registry path, but a page that composes flat (non-ui/*) components by hand -- e.g. the blog islands in the Sinatra/xslate/Mojolicious integrations -- needs the exact same ssrDefaults-seeding logic for its own manual register_child_renderer calls. Mirrors the Perl runtime's BarefootJS::_derive_stash_from_defaults, which is likewise callable from integration code (Perl has no enforced privacy; the leading underscore is convention only) -- see integrations/xslate/app.psgi's _register_blog_child and integrations/mojolicious/app.pl's equivalent.



362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
# File 'lib/barefoot_js.rb', line 362

def self.derive_vars_from_defaults(defaults, props)
  extra = {}
  defaults.each do |name, d|
    unless d.is_a?(Hash)
      extra[name] = d
      next
    end
    if d[:isRestProps]
      extra[name] = props.key?(name) ? props[name] : d[:value]
      next
    end
    # `propName` rides in from the JSON manifest as a String -- JSON has
    # no symbol type, and the manifest's `symbolize_names: true` parse
    # only symbolizes hash KEYS, never string values. Runtime prop
    # hashes, meanwhile, are symbol-keyed because compiled ERB templates
    # pass `{ children: ... }` literals, so `props.key?(prop_name)` with
    # the String would always miss, silently falling back to the static
    # default for every manifest-registered child (e.g. `children`
    # rendering empty) (#2157).
    prop_name = d[:propName]&.to_sym
    extra[name] =
      if !prop_name.nil? && props.key?(prop_name) && !props[prop_name].nil?
        props[prop_name]
      else
        d[:value]
      end
  end
  extra
end

Instance Method Details

#abs(value) ⇒ Object

Math.abs() (#2168 math-methods).



513
514
515
516
# File 'lib/barefoot_js.rb', line 513

def abs(value)
  n = number(value)
  nan_number?(n) ? n : n.abs
end

#async_boundary(id, fallback_html) ⇒ Object



400
401
402
403
# File 'lib/barefoot_js.rb', line 400

def async_boundary(id, fallback_html)
  fallback_html = backend.materialize(fallback_html)
  %(<div bf-async="#{id}">#{fallback_html}</div>)
end

#async_resolve(id, content_html) ⇒ Object



405
406
407
# File 'lib/barefoot_js.rb', line 405

def async_resolve(id, content_html)
  %(<template bf-async-resolve="#{id}">#{content_html}</template><script>__bf_swap("#{id}")</script>)
end

#at(recv, i) ⇒ Object

Array.prototype.at(i) -- negative indices count from the end; out-of-bounds -> nil (renders as '' via h, matching JS undefined).



935
936
937
938
939
940
941
942
943
944
945
946
947
# File 'lib/barefoot_js.rb', line 935

def at(recv, i)
  return nil unless recv.is_a?(Array)
  return nil if i.nil?

  idx = i.to_i
  len = recv.length
  return nil if len.zero?

  idx = len + idx if idx.negative?
  return nil if idx.negative? || idx >= len

  recv[idx]
end

#bool_str(value) ⇒ Object

Map a JS-boolean-shaped value to the JS String(bool) form. See BarefootJS.pm's bool_str docstring for the boolean-only contract -- callers must have already classified the expression as boolean- result; non-boolean attribute bindings never reach this helper.



179
180
181
# File 'lib/barefoot_js.rb', line 179

def bool_str(value)
  value ? 'true' : 'false'
end

#ceil(value) ⇒ Object



480
481
482
483
# File 'lib/barefoot_js.rb', line 480

def ceil(value)
  n = number(value)
  finite_number?(n) ? n.ceil : n
end

#comment(text) ⇒ Object


Comment Markers



171
172
173
# File 'lib/barefoot_js.rb', line 171

def comment(text)
  "<!--bf-#{text}-->"
end

#concat(a, b) ⇒ Object

Array.prototype.concat(other) -- merges two arrays in order into a new Array. Non-array operands collapse to empty.



951
952
953
954
955
956
# File 'lib/barefoot_js.rb', line 951

def concat(a, b)
  out = []
  out.concat(a) if a.is_a?(Array)
  out.concat(b) if b.is_a?(Array)
  out
end

#data_key_attrObject

Emits data-key="<key>" for a keyed loop item, else ''. See BarefootJS.pm's docstring on the client reconciliation contract.



116
117
118
119
120
121
122
# File 'lib/barefoot_js.rb', line 116

def data_key_attr
  k = _data_key
  return '' if k.nil?

  escaped = k.to_s.gsub('&', '&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.



528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
# File 'lib/barefoot_js.rb', line 528

def date(recv, op)
  # A nil or unparseable receiver degrades to the zero value the
  # Go / Rust / Perl helpers document (empty string for toISOString,
  # 0 otherwise), rather than raising mid-render.
  t =
    if recv.is_a?(Time)
      recv
    else
      begin
        Time.iso8601(recv.to_s)
      rescue ArgumentError
        nil
      end
    end
  return (op == 'toISOString' ? '' : 0) if t.nil?
  t = t.utc
  case op
  when 'getUTCFullYear' then t.year
  when 'getUTCMonth' then t.mon - 1
  when 'getUTCDate' then t.day
  when 'getUTCHours' then t.hour
  when 'getUTCMinutes' then t.min
  when 'getUTCSeconds' then t.sec
  when 'getTime' then (t.tv_sec * 1000) + (t.tv_nsec / 1_000_000)
  when 'toISOString' then t.strftime('%Y-%m-%dT%H:%M:%S.%LZ')
  else 0
  end
end

#ends_with(recv, suffix, end_position = nil) ⇒ Object

String.prototype.endsWith(suffix, endPosition?).



1135
1136
1137
1138
1139
1140
1141
1142
1143
# File 'lib/barefoot_js.rb', line 1135

def ends_with(recv, suffix, end_position = nil)
  s = recv.nil? ? '' : string(recv)
  x = suffix.nil? ? '' : string(suffix)
  unless end_position.nil?
    e = clamp_index(end_position.to_i, s.length)
    s = s[0...e]
  end
  s.end_with?(x)
end

#every(recv, pred) ⇒ Object



730
731
732
733
734
# File 'lib/barefoot_js.rb', line 730

def every(recv, pred)
  return true unless recv.is_a?(Array)

  recv.all? { |item| pred.call(item) }
end

#every_eval(recv, pred_json, param, base_env = {}) ⇒ Object



1340
1341
1342
# File 'lib/barefoot_js.rb', line 1340

def every_eval(recv, pred_json, param, base_env = {})
  Evaluator.every_json(recv, pred_json, param, base_env)
end

#filter(recv, pred) ⇒ Object

.filter(fn) / .every(fn) / .some(fn) / .find(fn) / .findIndex(fn) / .findLast(fn) / .findLastIndex(fn) -- legacy block-predicate path for shapes the compiler lowers to a native callable (e.g. a Kolon-style lambda literal). pred is anything responding to #call(item). The _eval family below is the evaluator-driven generalisation used for arbitrary pure bodies.



724
725
726
727
728
# File 'lib/barefoot_js.rb', line 724

def filter(recv, pred)
  return [] unless recv.is_a?(Array)

  recv.select { |item| pred.call(item) }
end

#filter_eval(recv, pred_json, param, base_env = {}) ⇒ Object



1336
1337
1338
# File 'lib/barefoot_js.rb', line 1336

def filter_eval(recv, pred_json, param, base_env = {})
  Evaluator.filter_json(recv, pred_json, param, base_env)
end

#find(recv, pred) ⇒ Object



742
743
744
745
746
# File 'lib/barefoot_js.rb', line 742

def find(recv, pred)
  return nil unless recv.is_a?(Array)

  recv.find { |item| pred.call(item) }
end

#find_eval(recv, pred_json, param, forward = true, base_env = {}) ⇒ Object



1348
1349
1350
# File 'lib/barefoot_js.rb', line 1348

def find_eval(recv, pred_json, param, forward = true, base_env = {})
  Evaluator.find_json(recv, pred_json, param, forward, base_env)
end

#find_index(recv, pred) ⇒ Object



748
749
750
751
752
753
# File 'lib/barefoot_js.rb', line 748

def find_index(recv, pred)
  return -1 unless recv.is_a?(Array)

  recv.each_index { |i| return i if pred.call(recv[i]) }
  -1
end

#find_index_eval(recv, pred_json, param, forward = true, base_env = {}) ⇒ Object



1352
1353
1354
# File 'lib/barefoot_js.rb', line 1352

def find_index_eval(recv, pred_json, param, forward = true, base_env = {})
  Evaluator.find_index_json(recv, pred_json, param, forward, base_env)
end

#find_last(recv, pred) ⇒ Object



755
756
757
758
759
760
# File 'lib/barefoot_js.rb', line 755

def find_last(recv, pred)
  return nil unless recv.is_a?(Array)

  recv.reverse_each { |item| return item if pred.call(item) }
  nil
end

#find_last_index(recv, pred) ⇒ Object



762
763
764
765
766
767
# File 'lib/barefoot_js.rb', line 762

def find_last_index(recv, pred)
  return -1 unless recv.is_a?(Array)

  (recv.length - 1).downto(0) { |i| return i if pred.call(recv[i]) }
  -1
end

#flat(recv, depth = 1) ⇒ Object

Array.prototype.flat(depth?) -- flatten nested arrays depth levels deep. depth of -1 is the Infinity sentinel (flatten fully); 0 returns a shallow copy.



1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
# File 'lib/barefoot_js.rb', line 1000

def flat(recv, depth = 1)
  return [] unless recv.is_a?(Array)

  out = []
  recv.each do |el|
    if !depth.zero? && el.is_a?(Array)
      out.concat(flat(el, depth.positive? ? depth - 1 : depth))
    else
      out << el
    end
  end
  out
end

#flat_dynamic(recv, depth) ⇒ Object

Array.prototype.flat(depth) where depth is a DYNAMIC value (#2094) -- e.g. items.flat(props.depth) -- rather than a compile-time literal. Coerces depth via JS ToIntegerOrInfinity (truncate toward zero; NaN/non-numeric -> 0; negative -> 0; +Infinity or a huge finite value -> flatten fully) and delegates to flat.

This is a SEPARATE method from flat, not a smarter overload of it: flat's depth parameter treats -1 as a compile-time SENTINEL meaning "the source literally wrote Infinity" (the parser's own normalisation, baked into the emitted template). A genuinely dynamic depth value that happens to be -1 at render time means the JS-correct OPPOSITE: .flat(-1) never recurses (same as .flat(0), a shallow copy), because real JS only recurses when depth > 0. Reusing flat's int contract for a raw dynamic value would silently invert that case, so this coerces FIRST -- mapping a real +Infinity / huge finite value to flat's own -1 sentinel, and a real negative value to 0 -- and only then delegates to flat's recursion. Mirrors Go's FlatDynamicDepth/coerceFlatDepth (adapter-go-template/runtime/bf.go).



1032
1033
1034
# File 'lib/barefoot_js.rb', line 1032

def flat_dynamic(recv, depth)
  flat(recv, coerce_flat_depth(depth))
end

#flat_map(recv, key_kind, key) ⇒ Object

Array.prototype.flatMap(fn) value-returning field projection: map each element through a self/field projection, then flatten one level.



1038
1039
1040
1041
1042
1043
# File 'lib/barefoot_js.rb', line 1038

def flat_map(recv, key_kind, key)
  return [] unless recv.is_a?(Array)

  projected = recv.map { |el| key_kind == 'field' ? field_value(el, key) : el }
  flat(projected, 1)
end

#flat_map_eval(recv, proj_json, param, base_env = {}) ⇒ Object



1356
1357
1358
# File 'lib/barefoot_js.rb', line 1356

def flat_map_eval(recv, proj_json, param, base_env = {})
  Evaluator.flat_map_json(recv, proj_json, param, base_env)
end

#flat_map_tuple(recv, *specs) ⇒ Object

Array.prototype.flatMap(i => [i.a, i.b]) -- array-literal tuple projection. Each spec is [kind, key] (['self', ''] or ['field', 'a']).



1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
# File 'lib/barefoot_js.rb', line 1048

def flat_map_tuple(recv, *specs)
  return [] unless recv.is_a?(Array)

  out = []
  recv.each do |el|
    specs.each do |kind, key|
      out << (kind == 'field' ? field_value(el, key) : el)
    end
  end
  out
end

#floor(value) ⇒ Object



475
476
477
478
# File 'lib/barefoot_js.rb', line 475

def floor(value)
  n = number(value)
  finite_number?(n) ? n.floor : n
end

#format_date(recv, pattern, tz, names = []) ⇒ Object

format_date(recv, pattern, tz, names) (#2324/#2334, spec entry "format_date" in spec/template-helpers.md) -- the lowering target for formatDate(date, pattern, timeZone, names) (packages/client/src/format-date.ts, the JS-normative reference this must match byte-for-byte). Total and deterministic: no locale, no host timezone, no Time.now.

recv: the same receiver contract as date above -- a native Time or an ISO-8601 string, normalized identically. A nil / unparseable receiver renders '' (never raises mid-render).

tz (#2344): 'UTC', a range-valid fixed offset ±HH:MM (shifts the instant by sign*(HH*60+MM) minutes), or a canonical IANA zone name ('Asia/Tokyo') resolved through tzdata via the tzinfo gem -- the zone's UTC offset AT THE INSTANT being formatted (DST-aware, historical-transition-aware, seconds precision: pre-standard LMT offsets like Tokyo's +09:18:59 count). ANY other value -- an unknown zone, a non-canonical spelling, a malformed or out-of-range offset ('+9:00', '+25:00') -- raises ArgumentError, aborting the render loudly: the JS reference throws a RangeError there, and a silently substituted timezone is the one failure mode this helper must not have (the pre-#2344 normalize-to-UTC total function is gone). The shifted instant's UTC calendar fields (not the original instant's) are what pattern tokens read -- the shifted UTC clock face IS the local clock face in that zone. Time arithmetic here is exact (Ruby represents the instant as a Rational, not a floor-divided epoch-millis integer), so there's no pre-1970 negative-epoch floor-division trap to dodge the way a naive epoch-millis-div-86400000 implementation would hit; the shifted Time#wday (0 = Sunday) is likewise exact across the epoch boundary, so it doubles directly as the weekday-name table index.

names: the flat table (#2334) -- [0..11] wide months, [12..23] abbreviated months, [24..30] wide weekdays (Sunday-first, matching the shifted Time#wday), [31..37] abbreviated weekdays. The caller owns the values (locale selection is never this method's job). An index past a missing/short table renders '' rather than raising.

pattern: longest-match token substitution (YYYY|MMMM|MMM|MM|DD|dddd|ddd|M|D); every other character -- including multi-byte ones like 年/月/日 -- passes through literally. YYYY is abs(year) zero-padded to 4 digits, --prefixed for a negative year; MM/DD zero-pad to 2; M/D are bare; MMMM/MMM read the month's wide/abbreviated name; dddd/ddd read the shifted weekday's wide/abbreviated name.



627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
# File 'lib/barefoot_js.rb', line 627

def format_date(recv, pattern, tz, names = [])
  t =
    if recv.is_a?(Time)
      recv
    else
      begin
        Time.iso8601(recv.to_s)
      rescue ArgumentError
        nil
      end
    end
  return '' if t.nil?

  tz_s = tz.to_s
  offset_seconds = 0
  if tz_s != 'UTC'
    m = FORMAT_DATE_OFFSET_RE.match(tz_s)
    offset_seconds =
      if m
        (m[1] == '-' ? -1 : 1) * ((m[2].to_i * 60) + m[3].to_i) * 60
      else
        format_date_zone_offset(tz_s, t)
      end
  end

  shifted = (t.utc + offset_seconds).utc
  year = shifted.year
  month = shifted.mon
  day = shifted.mday
  weekday = shifted.wday # 0 = Sunday, matching the table's Sunday-first layout
  yyyy = (year.negative? ? '-' : '') + year.abs.to_s.rjust(4, '0')
  name_at = ->(index) { (names[index] || '').to_s }

  pattern.gsub(FORMAT_DATE_TOKEN_RE) do |token|
    case token
    when 'YYYY' then yyyy
    when 'MMMM' then name_at.call(FORMAT_DATE_MONTHS_WIDE + month - 1)
    when 'MMM' then name_at.call(FORMAT_DATE_MONTHS_ABBR + month - 1)
    when 'MM' then format('%02d', month)
    when 'M' then month.to_s
    when 'DD' then format('%02d', day)
    when 'D' then day.to_s
    when 'dddd' then name_at.call(FORMAT_DATE_WEEKDAYS_WIDE + weekday)
    when 'ddd' then name_at.call(FORMAT_DATE_WEEKDAYS_ABBR + weekday)
    end
  end
end

#format_date_zone_offset(tz, t) ⇒ Object

Resolve a canonical IANA zone ID's UTC offset (whole seconds) at instant t through the tzinfo gem (#2344) -- TZInfo resolves the exact-case canonical ID set every backend shares, with full historical transitions (LMT seconds included). The gem is required lazily so the fixed-offset/'UTC' paths -- and every other helper -- keep working on a gem-less stdlib install; reaching a named zone without the gem raises the same loud ArgumentError as an unknown zone (never a silent UTC fallback).



683
684
685
686
687
688
689
690
691
692
693
694
695
696
# File 'lib/barefoot_js.rb', line 683

def format_date_zone_offset(tz, t)
  begin
    require 'tzinfo'
  rescue LoadError
    raise ArgumentError,
          "format_date: IANA timeZone #{tz.inspect} requires the tzinfo gem (~> 2.0)"
  end
  begin
    zone = TZInfo::Timezone.get(tz)
  rescue TZInfo::InvalidTimezoneIdentifier
    raise ArgumentError, "format_date: unresolvable timeZone #{tz.inspect}"
  end
  zone.period_for_utc(t.utc).observed_utc_offset
end

#get(collection, key) ⇒ Object

Runtime-polymorphic element/key access for arr[index] / hash[key] (emitIndexAccessRuby, expr/operand.ts). Row hashes deserialize with symbolize_names: true, so a dynamic key whose runtime value is a String (e.g. a destructured .map() param used as a key, tone[k]) misses a Symbol-keyed Hash outright -- and the shared analyzer can't tell at compile time whether an index will turn out to be a string key or a numeric index (it types a destructured param {kind:'unknown'}), so no compile-time branch can pick the right access form (#2491). Mirrors getFieldValue's (Go adapter, bf.go) case-tolerant lookup and Twig's attribute(): for a Hash, try the key as-is, then as a Symbol, then as a String; for an Array, index numerically; anything else has no lookup to perform.



813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
# File 'lib/barefoot_js.rb', line 813

def get(collection, key)
  return nil if key.nil?

  case collection
  when Hash
    return collection[key] if collection.key?(key)

    sym = key.respond_to?(:to_sym) ? key.to_sym : nil
    return collection[sym] if sym && collection.key?(sym)

    str = key.to_s
    return collection[str] if collection.key?(str)

    nil
  when Array
    # Only an INTEGRAL index reads an element. `key.to_i` would be
    # wrong on both ends: it coerces a non-numeric String to 0
    # (`arr["nope"]` must be nil, not the first element) and it
    # truncates a fractional number (JS `arr[1.2]` is a property
    # lookup, i.e. undefined -- not index 1).
    idx =
      case key
      when Integer then key
      when Numeric then (key % 1).zero? ? key.to_i : nil
      when String then (key =~ /\A-?\d+\z/ ? key.to_i : nil)
      end
    return nil if idx.nil?

    # JS-semantics negative index is "not found" (`arr[-1] ===
    # undefined`), NOT Ruby's native from-the-end wraparound -- guard
    # it explicitly so this stays a superset of bracket access rather
    # than a divergent one.
    return nil if idx.negative?

    collection[idx]
  end
end

#h(value) ⇒ Object

HTML-escaping helper for text interpolation (<%= bf.h(expr) %> -- stdlib ERB does not auto-escape). JS-style stringification via string (numbers per JS Number#toString, nil -> "", booleans -> "true"/"false"), then HTML-escaped. A SafeString (already-finished HTML forwarded from a parent's capture -- see that class's docstring) passes through unescaped, matching Twig/Blade/Kolon's safe-string bypass on their own auto-escaping {{ }}/<: :> output tags.



450
451
452
453
454
# File 'lib/barefoot_js.rb', line 450

def h(value)
  return value if value.is_a?(SafeString)

  html_escape(string(value))
end

#has_unsafe_style_value?(value) ⇒ Boolean

Mirrors Hono's own CSS-injection guard (hono/jsx/utils.ts's hasUnsafeStyleValue -- the ORACLE a dynamic style={{...}} value must match, #2261): a hand-rolled structural scan for characters that could break out of a CSS declaration, NOT real CSSOM property validation. Ported byte-for-byte -- every character this scan tests is ASCII, so scanning by byte agrees with Hono's UTF-16-code-unit scan for every input; a multibyte UTF-8 sequence has no byte in the ASCII range, so it can never spuriously match one of these single-byte comparisons. Skips the reference implementation's regex fast-path (a pure optimization -- the scan below already returns false promptly for a clean value).

Returns:

  • (Boolean)


862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
# File 'lib/barefoot_js.rb', line 862

def has_unsafe_style_value?(value)
  bytes = value.b
  quote = 0
  block_stack = []
  i = 0
  len = bytes.bytesize
  while i < len
    c = bytes.getbyte(i)
    if c == 92 # \
      return true if i == len - 1
      i += 1
    elsif quote != 0
      return true if c == 10 || c == 12 || c == 13
      quote = 0 if c == quote
    elsif c == 47 && i + 1 < len && bytes.getbyte(i + 1) == 42 # "/*"
      endi = bytes.index('*/', i + 2)
      return true if endi.nil?
      i = endi + 1
    elsif c == 34 || c == 39 # " or '
      quote = c
    elsif c == 40 # (
      block_stack.push(41)
    elsif c == 91 # [
      block_stack.push(93)
    elsif c == 123 || c == 125 # { or }
      return true
    elsif c == 41 || c == 93 # ) or ]
      return true if block_stack.empty? || block_stack.last != c
      block_stack.pop
    elsif c == 59 && block_stack.empty? # ;
      return true
    end
    i += 1
  end
  quote != 0 || !block_stack.empty?
end

#hydration_attrsObject

Emits bf-h="<host>" bf-m="<slot>" bf-r="" conditionally. See spec/compiler.md "Slot identity".



104
105
106
107
108
109
110
111
112
# File 'lib/barefoot_js.rb', line 104

def hydration_attrs
  parts = []
  host = _bf_parent
  mount = _bf_mount
  parts << %(bf-h="#{host.gsub('"', '&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.



709
710
711
712
713
714
715
716
# File 'lib/barefoot_js.rb', line 709

def includes(recv, elem)
  return recv.any? { |item| Evaluator.same_value_zero?(item, elem) } if recv.is_a?(Array)
  return false if recv.is_a?(Hash)

  s = recv.nil? ? '' : string(recv)
  needle = elem.nil? ? '' : string(elem)
  s.include?(needle)
end

#index_of(recv, elem) ⇒ Object

Array.prototype.indexOf(x) / .lastIndexOf(x) -- value-equality search. Non-array receivers return -1.



925
926
927
# File 'lib/barefoot_js.rb', line 925

def index_of(recv, elem)
  array_index_of(recv, elem, false)
end

#join(recv, sep = nil) ⇒ Object

Array.prototype.join(sep) with JS semantics: separator defaults to ",", undefined/null elements render as empty.



780
781
782
783
784
785
# File 'lib/barefoot_js.rb', line 780

def join(recv, sep = nil)
  return '' unless recv.is_a?(Array)

  sep = ',' if sep.nil?
  recv.map { |el| string(el) }.join(sep)
end

#json(value) ⇒ Object



419
420
421
# File 'lib/barefoot_js.rb', line 419

def json(value)
  backend.encode_json(value)
end

#last_index_of(recv, elem) ⇒ Object



929
930
931
# File 'lib/barefoot_js.rb', line 929

def last_index_of(recv, elem)
  array_index_of(recv, elem, true)
end

#lc(s) ⇒ Object

String.prototype.toLowerCase() / .toUpperCase().



770
771
772
# File 'lib/barefoot_js.rb', line 770

def lc(s)
  s.nil? ? '' : string(s).downcase
end

#length(recv) ⇒ Object

.length works on both arrays (element count) and strings. The string branch counts UTF-16 CODE UNITS, matching JS String.prototype.length (#2255) -- NOT Ruby's native String#length (Unicode codepoints). A codepoint outside the Basic Multilingual Plane (astral, U+10000-U+10FFFF -- e.g. '👍') is a surrogate PAIR in UTF-16, so it counts as 2, not 1; '日本語' is 3 either way (BMP-only).



793
794
795
796
797
798
# File 'lib/barefoot_js.rb', line 793

def length(recv)
  return recv.length if recv.is_a?(Array)
  return 0 if recv.is_a?(Hash) || recv.nil?

  string(recv).each_char.sum { |c| c.ord > 0xFFFF ? 2 : 1 }
end

#map_eval(recv, proj_json, param, base_env = {}) ⇒ Object



1360
1361
1362
# File 'lib/barefoot_js.rb', line 1360

def map_eval(recv, proj_json, param, base_env = {})
  Evaluator.map_json(recv, proj_json, param, base_env)
end

#max(a, b) ⇒ Object



503
504
505
506
507
508
509
510
# File 'lib/barefoot_js.rb', line 503

def max(a, b)
  x = number(a)
  y = number(b)
  return x if nan_number?(x)
  return y if nan_number?(y)

  x > y ? x : y
end

#min(a, b) ⇒ Object

Math.min(a, b) / Math.max(a, b) -- two-arg forms only (#2168 math-methods). JS returns NaN if either operand is NaN. number() may return a plain Integer (no #nan?), so guard like finite_number? above rather than calling #nan? unconditionally.



494
495
496
497
498
499
500
501
# File 'lib/barefoot_js.rb', line 494

def min(a, b)
  x = number(a)
  y = number(b)
  return x if nan_number?(x)
  return y if nan_number?(y)

  x < y ? x : y
end

#number(value) ⇒ Object

JS Number(v) mirror: numeric / boolean inputs convert as expected; non-numeric / nil yield real numeric NaN so downstream arithmetic propagates correctly (Math.floor(NaN) === NaN).



466
467
468
469
470
471
472
473
# File 'lib/barefoot_js.rb', line 466

def number(value)
  return Float::NAN if value.nil?
  return value ? 1 : 0 if value.is_a?(TrueClass) || value.is_a?(FalseClass)
  return value if value.is_a?(Numeric)
  return Float(value.strip) if value.is_a?(String) && value.strip =~ NUMERIC_STRING_RE

  Float::NAN
end

#pad_end(recv, target, pad_str = nil) ⇒ Object



1245
1246
1247
# File 'lib/barefoot_js.rb', line 1245

def pad_end(recv, target, pad_str = nil)
  pad_string(recv.nil? ? '' : string(recv), target, pad_str, false)
end

#pad_start(recv, target, pad_str = nil) ⇒ Object

String.prototype.padStart / padEnd.



1241
1242
1243
# File 'lib/barefoot_js.rb', line 1241

def pad_start(recv, target, pad_str = nil)
  pad_string(recv.nil? ? '' : string(recv), target, pad_str, true)
end

#props_attrObject



124
125
126
127
128
129
130
131
132
133
134
# File 'lib/barefoot_js.rb', line 124

def props_attr
  props = _props
  return '' unless props && !props.empty?

  # The JSON must be attribute-escaped: a raw `'` inside a string value
  # (e.g. a blog paragraph) terminates the single-quoted attribute and
  # truncates the hydration payload. The browser entity-decodes the
  # attribute value, so the client's JSON.parse sees the original text.
  json = html_escape(backend.encode_json(props))
  %( bf-p='#{json}')
end

#provide_context(name, value) ⇒ Object



151
152
153
154
# File 'lib/barefoot_js.rb', line 151

def provide_context(name, value)
  CONTEXT_STACKS[name].push(value)
  ''
end

#query(base, *triples) ⇒ Object

queryHref(base, { ... }) (#2042) -- build "base?k=v&..." from a flat list of (guard, key, value) triples. A pair is included iff its guard is truthy AND its value is a non-empty string. A value may instead be an Array, which APPENDS one pair per non-empty member. Repeating a key overwrites the value at its first position (URLSearchParams.set semantics); array members always append (.append semantics).



1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
# File 'lib/barefoot_js.rb', line 1198

def query(base, *triples)
  b = base.nil? ? '' : string(base)
  pairs = []
  pos = {}
  i = 0
  while i + 2 < triples.length
    guard, key, val = triples[i], triples[i + 1], triples[i + 2]
    i += 3
    next unless truthy?(guard)

    k = key.nil? ? '' : string(key)
    if val.is_a?(Array)
      val.each do |m|
        sm = string(m)
        pairs << [k, sm] unless sm.empty?
      end
      next
    end
    v = val.nil? ? '' : string(val)
    next if v.empty?

    if pos.key?(k)
      pairs[pos[k]][1] = v
    else
      pos[k] = pairs.length
      pairs << [k, v]
    end
  end
  return b if pairs.empty?

  "#{b}?#{pairs.map { |pk, pv| "#{form_escape(pk)}=#{form_escape(pv)}" }.join('&')}"
end

#reduce(recv, opts = {}) ⇒ Object

Fold an array into a scalar via the arithmetic-fold catalogue (legacy, pre-#2018 path; reduce_eval below handles arbitrary reducer bodies). opts: { op: '+'|'*', key_kind:, key:, type: 'numeric'|'string', init:, direction: 'left'|'right' }.



1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
# File 'lib/barefoot_js.rb', line 1291

def reduce(recv, opts = {})
  op = opts[:op] || '+'
  key_kind = opts[:key_kind] || 'self'
  key = opts[:key] || ''
  type = opts[:type] || 'numeric'
  direction = opts[:direction] || 'left'

  items = recv.is_a?(Array) ? recv.dup : []
  items.reverse! if direction == 'right'
  project = lambda { |item| key_kind == 'field' ? field_value(item, key) : item }

  if type == 'string'
    acc = opts[:init].nil? ? '' : string(opts[:init])
    items.each { |item| acc += string(project.call(item)) }
    return acc
  end

  # `init` rides through the adapter as whatever literal the template
  # emits -- often a numeric-looking String (JSON-decoded, not a Ruby
  # numeric literal) -- so route it through the same numeric coercion
  # as every per-element projection rather than trusting its Ruby class.
  acc = opts[:init].nil? ? 0 : numeric_or_zero(opts[:init])
  items.each do |item|
    n = numeric_or_zero(project.call(item))
    acc = op == '*' ? acc * n : acc + n
  end
  acc
end

#reduce_eval(recv, body_json, acc_name, item_name, init, direction = 'left', base_env = {}) ⇒ Object



1332
1333
1334
# File 'lib/barefoot_js.rb', line 1332

def reduce_eval(recv, body_json, acc_name, item_name, init, direction = 'left', base_env = {})
  Evaluator.fold_json(recv, body_json, acc_name, item_name, init, direction, base_env)
end

#register_child_renderer(name, renderer) ⇒ Object

Register a renderer for render_child(name, ...). renderer is called as renderer.call(props_hash, invoking_bf) -- the invoking Context matters because a renderer registered on the root may be called from a nested child render, and the grandchild's scope / slot identity must chain off the CALLER's scope id, not the registrant's (#1897).



256
257
258
# File 'lib/barefoot_js.rb', line 256

def register_child_renderer(name, renderer)
  _child_renderers[name] = renderer
end

#register_components_from_manifest(manifest, signal_init: {}) ⇒ Object


Bulk registration from build manifest

vite build (via @barefootjs/erb/vite's barefoot() plugin) emits dist/templates/manifest.json describing every component the page might invoke. This walks that manifest and registers one child renderer per UI registry entry (ui/<name>/index -> slot key <name>), seeding each child's template vars from the manifest's statically-derived ssrDefaults (prop destructure defaults + signal/memo initial values). signal_init[slot_key] is an opt-in override for defaults the static extractor can't see through.



288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
# File 'lib/barefoot_js.rb', line 288

def register_components_from_manifest(manifest, signal_init: {})
  parent_scope = _scope_id
  parent = self

  manifest.each do |entry_name, entry|
    next if entry_name.to_s == '__barefoot__'

    m = entry_name.to_s.match(%r{\Aui/([^/]+)/index\z})
    next unless m

    slot_key = m[1]
    marked = entry[:markedTemplate] || ''
    next if marked.empty?

    template_name = marked.sub(%r{\Atemplates/}, '').sub(/\.erb\z/, '')
    sig_init = signal_init[slot_key]
    manifest_defaults = entry[:ssrDefaults]

    register_child_renderer(slot_key, lambda do |props, caller|
      host = caller || parent
      host_scope = host._scope_id || parent_scope
      child_bf = self.class.new(parent.backend)
      slot_id = props.delete(:_bf_slot)
      data_key = props.delete(:key)
      child_bf._data_key(data_key) unless data_key.nil?
      child_bf._scope_id(
        slot_id ? "#{host_scope}_#{slot_id}" : "#{template_name}_#{rand.to_s[2, 6]}",
      )
      child_bf._is_child(true)
      if slot_id
        child_bf._bf_parent(host_scope)
        child_bf._bf_mount(slot_id)
      end
      # Share the root registry so the child's own template can render
      # further imported components (#1897).
      child_bf._child_renderers(parent._child_renderers)
      child_bf._scripts(parent._scripts)
      child_bf._script_seen(parent._script_seen)
      child_bf._preloads(parent._preloads)
      child_bf._preload_seen(parent._preload_seen)

      extra =
        if sig_init
          sig_init.call(props)
        elsif manifest_defaults
          self.class.send(:derive_vars_from_defaults, manifest_defaults, props)
        else
          {}
        end

      html = parent.backend.render_named(template_name, child_bf, props.merge(extra))
      html.chomp
    end)
  end
end

#register_preload(path) ⇒ Object

Register a <link rel="modulepreload"> hint (mirrors register_script exactly: same dedup-by-path set, same insertion-order array, same lifetime/reset semantics, same no-output-string return so the compiled ERB template's <% bf.register_preload(...) %> produces no bytes where it sits). The <link> itself is only ever emitted by scripts below, never here -- a preload registration must never inject a node into a component's own template output (see the previous attempt's regression this guards against).



230
231
232
233
234
235
# File 'lib/barefoot_js.rb', line 230

def register_preload(path)
  return if _preload_seen.key?(path)

  _preload_seen[path] = true
  _preloads.push(path)
end

#register_script(path) ⇒ Object


Script Registration



215
216
217
218
219
220
# File 'lib/barefoot_js.rb', line 215

def register_script(path)
  return if _script_seen.key?(path)

  _script_seen[path] = true
  _scripts.push(path)
end

#render_child(name, *args) ⇒ Object



260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
# File 'lib/barefoot_js.rb', line 260

def render_child(name, *args)
  renderer = _child_renderers[name]
  raise "No renderer registered for child component '#{name}'" unless renderer

  # Accept both `render_child(name, k: v, ...)` (kwargs collapse into a
  # trailing Hash under Ruby's argument-splat rules) and the explicit
  # single-Hash form `render_child(name, { k: v })`.
  props = (args.length == 1 && args[0].is_a?(Hash)) ? args[0].dup : Hash[*args]
  # JSX children come in via the ERB backend's content-capture buffer
  # slice; materialize it through the backend so the child renderer sees
  # `props[:children]` as already-rendered HTML. Guard on `key?` so a
  # childless invocation doesn't gain a spurious `children: nil` key.
  props[:children] = backend.materialize(props[:children]) if props.key?(:children)
  renderer.call(props, self)
end

#repeat(recv, count) ⇒ Object

String.prototype.repeat(n) -- a count <= 0 degrades to '' rather than raising (JS throws RangeError for negative counts; SSR templates degrade instead of dying mid-render).



1234
1235
1236
1237
1238
# File 'lib/barefoot_js.rb', line 1234

def repeat(recv, count)
  s = recv.nil? ? '' : string(recv)
  n = count.nil? ? 0 : count.to_i
  n.positive? ? s * n : ''
end

#replace(recv, pattern, replacement) ⇒ Object

String.prototype.replace(pattern, replacement) -- string-pattern form only, replacing the FIRST occurrence, literally (no regex metacharacters, no $1-style replacement interpolation).



1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
# File 'lib/barefoot_js.rb', line 1148

def replace(recv, pattern, replacement)
  s = recv.nil? ? '' : string(recv)
  o = pattern.nil? ? '' : string(pattern)
  n = replacement.nil? ? '' : string(replacement)
  return n + s if o.empty?

  idx = s.index(o)
  return s if idx.nil?

  s[0...idx] + n + s[(idx + o.length)..]
end

#replace_all(recv, pattern, replacement) ⇒ Object

String.prototype.replaceAll(pattern, replacement) -- string-pattern form only (#2182), replacing EVERY occurrence (the all-occurrences sibling of replace above). Deliberately NOT String#gsub: Ruby's gsub interprets \1 / \& backreference syntax in the replacement even for a literal string pattern ("abc".gsub("b", "\\1") -> "ac", not the literal "\1"), which would diverge from .replace's literal splice above and from the other backends' literal treatment. The index/splice loop keeps the replacement literal, matching replace. An empty pattern inserts at every boundary, including before the first and after the last character ("abc".replaceAll("", "X") -> "XaXbXcX"), matching JS.



1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
# File 'lib/barefoot_js.rb', line 1171

def replace_all(recv, pattern, replacement)
  s = recv.nil? ? '' : string(recv)
  o = pattern.nil? ? '' : string(pattern)
  n = replacement.nil? ? '' : string(replacement)
  return ([''] + s.chars + ['']).join(n) if o.empty?

  # `+''` (not the frozen `''` literal under frozen_string_literal)
  # so `<<` can append in place instead of `+=` reallocating a new
  # string each iteration (quadratic for long inputs / many matches).
  out = +''
  pos = 0
  loop do
    idx = s.index(o, pos)
    break if idx.nil?

    out << s[pos...idx] << n
    pos = idx + o.length
  end
  out << s[pos..]
end

#reverse(recv) ⇒ Object

Array.prototype.reverse() / .toReversed() -- always returns a new Array (SSR renders a snapshot; the mutate-vs-copy JS distinction is moot here).



993
994
995
# File 'lib/barefoot_js.rb', line 993

def reverse(recv)
  recv.is_a?(Array) ? recv.reverse : []
end

#revoke_context(name) ⇒ Object



156
157
158
159
160
# File 'lib/barefoot_js.rb', line 156

def revoke_context(name)
  stack = CONTEXT_STACKS[name]
  stack.pop unless stack.empty?
  ''
end

#round(value) ⇒ Object



485
486
487
488
# File 'lib/barefoot_js.rb', line 485

def round(value)
  n = number(value)
  finite_number?(n) ? (n + 0.5).floor : n
end

#scope_attrObject

bf-s is the addressable scope id only (#1249).



98
99
100
# File 'lib/barefoot_js.rb', line 98

def scope_attr
  _scope_id || ''
end

#scope_commentObject

See spec/compiler.md "Slot identity" for the comment-scope wire format.



192
193
194
195
196
197
198
199
200
201
# File 'lib/barefoot_js.rb', line 192

def scope_comment
  scope_id = _scope_id || ''
  host_segment = ''
  host = _bf_parent
  mount = _bf_mount
  host_segment = "|h=#{host}|m=#{mount || ''}" if host && !host.empty?
  props_json = ''
  props_json = "|#{backend.encode_json(_props)}" if _props && !_props.empty?
  "<!--bf-scope:#{scope_id}#{host_segment}#{props_json}-->"
end

#scope_comment_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).



206
207
208
209
# File 'lib/barefoot_js.rb', line 206

def scope_comment_end
  scope_id = _scope_id || ''
  "<!--bf-/scope:#{scope_id}-->"
end

#scriptsObject

Emits preload hints BEFORE script tags -- a hint that arrives after the script it describes is useless. Same escaping (none; paths are caller-trusted resolved URLs) as the script tags below.



240
241
242
243
244
# File 'lib/barefoot_js.rb', line 240

def scripts
  preload_tags = _preloads.map { |path| %(<link rel="modulepreload" crossorigin href="#{path}">) }
  script_tags = _scripts.map { |path| %(<script type="module" src="#{path}"></script>) }
  (preload_tags + script_tags).join("\n")
end

#search_params(query = '') ⇒ Object

search_params(query = '') -- request-scoped reader for the reactive searchParams() environment signal (router v0.5, #1922), built from a raw query string. The compiled template reads it via v[:searchParams].get('key').



89
90
91
# File 'lib/barefoot_js.rb', line 89

def search_params(query = '')
  SearchParams.new(query)
end

#slice(recv, start, end_ = nil) ⇒ Object

Array.prototype.slice(start, end?) AND String.prototype.slice (the string-slice divergence) -- the adapter emits the same bf_slice call for both receiver shapes (it can't disambiguate string vs. array at compile time), so this dispatches on Ruby class, mirroring includes above. Mirrors the Go/Perl bf_slice / slice arithmetic so adapter output stays symmetric. String#length / #[] already index by character (not byte) for a UTF-8-encoded string, matching JS except for astral-plane input (the same divergence boundary every other adapter's pad/trim helpers already accept).



968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
# File 'lib/barefoot_js.rb', line 968

def slice(recv, start, end_ = nil)
  return [] unless recv.is_a?(Array) || recv.is_a?(String)

  empty = recv.is_a?(String) ? '' : []
  len = recv.length
  return empty if len.zero?

  s = start.nil? ? 0 : start.to_i
  s = len + s if s.negative?
  s = 0 if s.negative?
  s = len if s > len

  e = end_.nil? ? len : end_.to_i
  e = len + e if e.negative?
  e = 0 if e.negative?
  e = len if e > len

  return empty if s >= e

  recv[s...e]
end

#some(recv, pred) ⇒ Object



736
737
738
739
740
# File 'lib/barefoot_js.rb', line 736

def some(recv, pred)
  return false unless recv.is_a?(Array)

  recv.any? { |item| pred.call(item) }
end

#some_eval(recv, pred_json, param, base_env = {}) ⇒ Object



1344
1345
1346
# File 'lib/barefoot_js.rb', line 1344

def some_eval(recv, pred_json, param, base_env = {})
  Evaluator.some_json(recv, pred_json, param, base_env)
end

#sort(recv, opts = {}) ⇒ Object

Array.prototype.sort(cmp) / .toSorted(cmp) -- fixed comparator catalogue (legacy, pre-#2018 path; sort_eval below handles arbitrary comparator bodies). opts[:keys] is a priority-ordered list of { key_kind:, key:, compare_type:, direction: }. Stable (ties break on original index) and non-mutating.



1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
# File 'lib/barefoot_js.rb', line 1254

def sort(recv, opts = {})
  return [] unless recv.is_a?(Array)

  spec = (opts[:keys] || []).map do |k|
    {
      key_kind: k[:key_kind] || 'self',
      key: k[:key] || '',
      compare_type: k[:compare_type] || 'numeric',
      direction: k[:direction] || 'asc',
    }
  end
  return recv.dup if spec.empty?

  decorated = recv.each_with_index.map do |item, idx|
    keys = spec.map { |sp| sp[:key_kind] == 'field' ? field_value(item, sp[:key]) : item }
    [keys, item, idx]
  end

  sorted = decorated.sort do |x, y|
    result = 0
    spec.each_index do |i|
      c = compare_sort_key(x[0][i], y[0][i], spec[i][:compare_type])
      next if c.zero?

      result = spec[i][:direction] == 'desc' ? -c : c
      break
    end
    result.zero? ? (x[2] <=> y[2]) : result
  end

  sorted.map { |pair| pair[1] }
end

#sort_eval(recv, cmp_json, param_a, param_b, base_env = {}) ⇒ Object


Evaluator-driven sort / reduce / higher-order predicates (#2018): the comparator / reducer / predicate body rides as a serialized- ParsedExpr JSON string and is evaluated per element, delegating to the shared BarefootJS::Evaluator. find_eval / find_index_eval take a forward flag (false -> findLast / findLastIndex).



1328
1329
1330
# File 'lib/barefoot_js.rb', line 1328

def sort_eval(recv, cmp_json, param_a, param_b, base_env = {})
  Evaluator.sort_by_json(recv, cmp_json, param_a, param_b, base_env)
end

#split(recv, sep = nil, limit = nil) ⇒ Object

String.prototype.split(sep) -- string -> Array. An empty separator splits into individual characters; a nil separator returns the whole string in a single-element Array; trailing empty fields are kept (JS parity -- Ruby's String#split(str, -1) already matches this, and (unlike a Regexp) a String separator is matched literally).



1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
# File 'lib/barefoot_js.rb', line 1100

def split(recv, sep = nil, limit = nil)
  s = (recv.nil? || recv.is_a?(Array) || recv.is_a?(Hash)) ? '' : string(recv)
  parts =
    if sep.nil?
      [s]
    elsif string(sep).empty?
      s.chars
    elsif s.empty?
      ['']
    else
      s.split(string(sep), -1)
    end
  unless limit.nil?
    n = limit.to_i
    if n.zero?
      parts = []
    elsif n.positive? && n < parts.length
      parts = parts[0...n]
    end
  end
  parts
end

#spread_attrs(bag) ⇒ Object


JSX intrinsic-element spread (#1407)

Mirrors the JS spreadAttrs runtime and the Go/Perl adapters' spread helpers so SSR output stays byte-equal across adapters. Generated ERB templates invoke this as <%= bf.spread_attrs(bag) %>.

Skip rules: nil/false values, event handlers (on[A-Z]...), and children. ref is intentionally NOT filtered (matches the JS reference). Key remap: className -> class, htmlFor -> for; SVG camelCase attrs preserved; other camelCase keys lowered to kebab-case. style routes through style_to_css. Output is deterministic: keys are sorted alphabetically before emission.

Unlike the Perl/Go ports, no boolean-sentinel detection is needed -- Ruby's true/false are real booleans, distinct from 0/1, so a bag value's Ruby class alone tells JS-boolean from JS-number.



1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
# File 'lib/barefoot_js.rb', line 1382

def spread_attrs(bag)
  return '' unless bag.is_a?(Hash)

  parts = []
  bag.keys.sort_by(&:to_s).each do |key|
    key_s = key.to_s
    if key_s.length > 2 && key_s.start_with?('on')
      c = key_s[2]
      next if c.upcase == c
    end
    next if key_s == 'children'

    val = bag[key]
    next if val.nil?

    if val.is_a?(TrueClass) || val.is_a?(FalseClass)
      next unless val

      parts << to_attr_name(key_s)
      next
    end

    if key_s == 'style'
      css = style_to_css(val)
      next if css.nil? || css.empty?

      parts << %(style="#{html_escape(css)}")
      next
    end

    parts << %(#{to_attr_name(key_s)}="#{html_escape(string(val))}")
  end
  return '' if parts.empty?

  # Mark the result raw so the calling template's plain `<%=` doesn't
  # need a second escape pass (the backend decides how "raw" is
  # represented for its engine; ERB's own emit has no auto-escape, so
  # BarefootJS::Backend::Erb's `mark_raw` is the identity function).
  backend.mark_raw(parts.join(' '))
end

#starts_with(recv, prefix, position = nil) ⇒ Object

String.prototype.startsWith(prefix, position?).



1124
1125
1126
1127
1128
1129
1130
1131
1132
# File 'lib/barefoot_js.rb', line 1124

def starts_with(recv, prefix, position = nil)
  s = recv.nil? ? '' : string(recv)
  p = prefix.nil? ? '' : string(prefix)
  unless position.nil?
    n = clamp_index(position.to_i, s.length)
    s = s[n..] || ''
  end
  s.start_with?(p)
end

#streaming_bootstrapObject


Streaming SSR (Out-of-Order)



396
397
398
# File 'lib/barefoot_js.rb', line 396

def streaming_bootstrap
  %{<script>(function(){function s(id){var a=document.querySelector('[bf-async="'+id+'"]');var t=document.querySelector('template[bf-async-resolve="'+id+'"]');if(!a||!t)return;a.replaceChildren(t.content.cloneNode(true));a.removeAttribute('bf-async');t.remove();requestAnimationFrame(function(){if(window.__bf_hydrate)window.__bf_hydrate()})};window.__bf_swap=s})()</script>}
end

#string(value) ⇒ Object

JS String(v) mirror, EXCEPT nil renders as '' (not the literal "null") so an unset prop doesn't surface as literal text in user-facing HTML -- the same divergence the Go/Perl adapters document for their string helper. This is the canonical JS-ish stringifier used throughout this file (join, spread_attrs, h, reduce's string fold, ...).



429
430
431
432
433
434
435
436
437
438
439
440
441
# File 'lib/barefoot_js.rb', line 429

def string(value)
  return '' if value.nil?
  return value ? 'true' : 'false' if value.is_a?(TrueClass) || value.is_a?(FalseClass)
  return Evaluator.number_to_string(value) if value.is_a?(Numeric)
  # JS `Array.prototype.toString` is `this.join(',')`, applied
  # recursively -- Ruby's `Array#to_s` (inspect-style, e.g. "[[1], [2]]")
  # would otherwise leak through here instead of the JS comma-join
  # (e.g. reached via `.flat(0)`'s shallow copy joined afterwards,
  # #2262).
  return value.map { |el| string(el) }.join(',') if value.is_a?(Array)

  value.to_s
end

#style_object(*pairs) ⇒ Object

Builds the CSS string for a style={{...}} JSX object-literal attribute (#2261) -- pairs alternates CSS key (always a compile-time-known literal), then value. A value that fails has_unsafe_style_value? (after JS-String()-style stringification) is DROPPED -- the whole key:value pair is omitted -- matching Hono's oracle behavior exactly. The final joined string is STILL HTML-escaped (mirroring Hono's own escapeToBuffer call on its accumulated style string) -- a "safe" value can still carry a literal "/'/& (e.g. a BALANCED-quote CSS string value like "hello" passes the structural scan) that would otherwise break out of the double-quoted style="..." attribute. Returns a SafeString so bf.h (which the emitter still wraps every dynamic value with) passes the already-escaped result through unchanged.



912
913
914
915
916
917
918
919
920
921
# File 'lib/barefoot_js.rb', line 912

def style_object(*pairs)
  parts = []
  pairs.each_slice(2) do |key, value|
    v = string(value)
    next if has_unsafe_style_value?(v)

    parts << "#{html_escape(key)}:#{html_escape(v)}"
  end
  SafeString.new(parts.join(';'))
end

#text_endObject



187
188
189
# File 'lib/barefoot_js.rb', line 187

def text_end
  '<!--/-->'
end

#text_start(slot_id) ⇒ Object



183
184
185
# File 'lib/barefoot_js.rb', line 183

def text_start(slot_id)
  "<!--bf:#{slot_id}-->"
end

#to_fixed(value, digits = 0) ⇒ Object

Number.prototype.toFixed(digits) -- fixed-decimal string with zero-padding, rounding half toward +Infinity (matching round).



1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
# File 'lib/barefoot_js.rb', line 1084

def to_fixed(value, digits = 0)
  n = number(value)
  return 'NaN' if n.respond_to?(:nan?) && n.nan?
  return n.negative? ? '-Infinity' : 'Infinity' if n.respond_to?(:infinite?) && n.infinite?

  digits = 0 if digits.nil? || digits.negative?
  factor = 10.0**digits
  rounded = (n * factor + 0.5).floor
  format("%.#{digits}f", rounded / factor)
end

#trim(recv) ⇒ Object

String.prototype.trim().



1061
1062
1063
1064
1065
# File 'lib/barefoot_js.rb', line 1061

def trim(recv)
  return '' if recv.nil? || recv.is_a?(Array) || recv.is_a?(Hash)

  string(recv).gsub(/\A\p{Space}+|\p{Space}+\z/, '')
end

#trim_end(recv) ⇒ Object



1076
1077
1078
1079
1080
# File 'lib/barefoot_js.rb', line 1076

def trim_end(recv)
  return '' if recv.nil? || recv.is_a?(Array) || recv.is_a?(Hash)

  string(recv).sub(/\p{Space}+\z/, '')
end

#trim_start(recv) ⇒ Object

String.prototype.trimStart() / .trimEnd() -- the one-sided siblings of trim above (#2183 follow-up), same \p{Space} regex restricted to one side.



1070
1071
1072
1073
1074
# File 'lib/barefoot_js.rb', line 1070

def trim_start(recv)
  return '' if recv.nil? || recv.is_a?(Array) || recv.is_a?(Hash)

  string(recv).sub(/\A\p{Space}+/, '')
end

#truthy?(value) ⇒ Boolean

JS truthiness (falsy: false, nil, 0, NaN, ""). Delegates to the shared evaluator so template-emitted conditionals (if bf.truthy?(x)) and callback-body evaluation agree byte-for-byte.

Returns:

  • (Boolean)


459
460
461
# File 'lib/barefoot_js.rb', line 459

def truthy?(value)
  Evaluator.truthy?(value)
end

#uc(s) ⇒ Object



774
775
776
# File 'lib/barefoot_js.rb', line 774

def uc(s)
  s.nil? ? '' : string(s).upcase
end

#use_context(name, default = nil) ⇒ Object



162
163
164
165
# File 'lib/barefoot_js.rb', line 162

def use_context(name, default = nil)
  stack = CONTEXT_STACKS[name]
  stack.empty? ? default : stack.last
end