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.



374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
# File 'lib/barefoot_js.rb', line 374

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



525
526
527
528
# File 'lib/barefoot_js.rb', line 525

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

#async_boundary(id, fallback_html) ⇒ Object



412
413
414
415
# File 'lib/barefoot_js.rb', line 412

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



417
418
419
# File 'lib/barefoot_js.rb', line 417

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



947
948
949
950
951
952
953
954
955
956
957
958
959
# File 'lib/barefoot_js.rb', line 947

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.



191
192
193
# File 'lib/barefoot_js.rb', line 191

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

#ceil(value) ⇒ Object



492
493
494
495
# File 'lib/barefoot_js.rb', line 492

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

#comment(text) ⇒ Object


Comment Markers



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

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.



963
964
965
966
967
968
# File 'lib/barefoot_js.rb', line 963

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.



540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
# File 'lib/barefoot_js.rb', line 540

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?).



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

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



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

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



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

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.



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

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



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

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

#find(recv, pred) ⇒ Object



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

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



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

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



760
761
762
763
764
765
# File 'lib/barefoot_js.rb', line 760

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



1364
1365
1366
# File 'lib/barefoot_js.rb', line 1364

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



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

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



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

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.



1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
# File 'lib/barefoot_js.rb', line 1012

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



1044
1045
1046
# File 'lib/barefoot_js.rb', line 1044

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.



1050
1051
1052
1053
1054
1055
# File 'lib/barefoot_js.rb', line 1050

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



1368
1369
1370
# File 'lib/barefoot_js.rb', line 1368

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']).



1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
# File 'lib/barefoot_js.rb', line 1060

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



487
488
489
490
# File 'lib/barefoot_js.rb', line 487

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.



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
674
675
676
677
678
679
680
681
682
683
684
685
# File 'lib/barefoot_js.rb', line 639

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



695
696
697
698
699
700
701
702
703
704
705
706
707
708
# File 'lib/barefoot_js.rb', line 695

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.



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
850
851
852
853
854
855
856
857
858
859
860
861
# File 'lib/barefoot_js.rb', line 825

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.



462
463
464
465
466
# File 'lib/barefoot_js.rb', line 462

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)


874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
# File 'lib/barefoot_js.rb', line 874

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.



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

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.



937
938
939
# File 'lib/barefoot_js.rb', line 937

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.



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

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



431
432
433
# File 'lib/barefoot_js.rb', line 431

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

#last_index_of(recv, elem) ⇒ Object



941
942
943
# File 'lib/barefoot_js.rb', line 941

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

#lc(s) ⇒ Object

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



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

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



805
806
807
808
809
810
# File 'lib/barefoot_js.rb', line 805

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



1372
1373
1374
# File 'lib/barefoot_js.rb', line 1372

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

#max(a, b) ⇒ Object



515
516
517
518
519
520
521
522
# File 'lib/barefoot_js.rb', line 515

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.



506
507
508
509
510
511
512
513
# File 'lib/barefoot_js.rb', line 506

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



478
479
480
481
482
483
484
485
# File 'lib/barefoot_js.rb', line 478

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



1257
1258
1259
# File 'lib/barefoot_js.rb', line 1257

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.



1253
1254
1255
# File 'lib/barefoot_js.rb', line 1253

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
135
136
137
138
139
140
141
142
143
144
145
146
# File 'lib/barefoot_js.rb', line 124

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

  # Exclude the internal `scope_id` key from the client hydration
  # payload: it is a server-side render detail (already carried by
  # `_scope_id` for bf-s/bf-h/bf-m emission) with zero client runtime
  # consumers -- the only bf-p parser (packages/client/src/runtime/
  # hydrate.ts's parseProps -> runInit) never reads it back out.
  # Filtered here, the single marshal boundary for bf-p, so it's
  # excluded no matter how a `scope_id` key ends up in the props Hash
  # (found via the bf-p semantic-comparison audit against the Hono
  # reference adapter, which never emits it).
  client_props = props.reject { |k, _| k.to_s == 'scope_id' }
  return '' if client_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(client_props))
  %( bf-p='#{json}')
end

#provide_context(name, value) ⇒ Object



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

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



1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
# File 'lib/barefoot_js.rb', line 1210

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' }.



1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
# File 'lib/barefoot_js.rb', line 1303

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



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

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



268
269
270
# File 'lib/barefoot_js.rb', line 268

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.



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
343
344
345
346
347
348
349
350
351
352
353
354
# File 'lib/barefoot_js.rb', line 300

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



242
243
244
245
246
247
# File 'lib/barefoot_js.rb', line 242

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

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

#register_script(path) ⇒ Object


Script Registration



227
228
229
230
231
232
# File 'lib/barefoot_js.rb', line 227

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

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

#render_child(name, *args) ⇒ Object



272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
# File 'lib/barefoot_js.rb', line 272

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



1246
1247
1248
1249
1250
# File 'lib/barefoot_js.rb', line 1246

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



1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
# File 'lib/barefoot_js.rb', line 1160

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.



1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
# File 'lib/barefoot_js.rb', line 1183

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



1005
1006
1007
# File 'lib/barefoot_js.rb', line 1005

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

#revoke_context(name) ⇒ Object



168
169
170
171
172
# File 'lib/barefoot_js.rb', line 168

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

#round(value) ⇒ Object



497
498
499
500
# File 'lib/barefoot_js.rb', line 497

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.



204
205
206
207
208
209
210
211
212
213
# File 'lib/barefoot_js.rb', line 204

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



218
219
220
221
# File 'lib/barefoot_js.rb', line 218

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.



252
253
254
255
256
# File 'lib/barefoot_js.rb', line 252

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



980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# File 'lib/barefoot_js.rb', line 980

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



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

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



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

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.



1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
# File 'lib/barefoot_js.rb', line 1266

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



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

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



1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
# File 'lib/barefoot_js.rb', line 1112

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.



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
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
# File 'lib/barefoot_js.rb', line 1394

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?).



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

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)



408
409
410
# File 'lib/barefoot_js.rb', line 408

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, ...).



441
442
443
444
445
446
447
448
449
450
451
452
453
# File 'lib/barefoot_js.rb', line 441

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.



924
925
926
927
928
929
930
931
932
933
# File 'lib/barefoot_js.rb', line 924

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



199
200
201
# File 'lib/barefoot_js.rb', line 199

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

#text_start(slot_id) ⇒ Object



195
196
197
# File 'lib/barefoot_js.rb', line 195

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



1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
# File 'lib/barefoot_js.rb', line 1096

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().



1073
1074
1075
1076
1077
# File 'lib/barefoot_js.rb', line 1073

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



1088
1089
1090
1091
1092
# File 'lib/barefoot_js.rb', line 1088

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.



1082
1083
1084
1085
1086
# File 'lib/barefoot_js.rb', line 1082

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)


471
472
473
# File 'lib/barefoot_js.rb', line 471

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

#uc(s) ⇒ Object



786
787
788
# File 'lib/barefoot_js.rb', line 786

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

#use_context(name, default = nil) ⇒ Object



174
175
176
177
# File 'lib/barefoot_js.rb', line 174

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