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.



79
80
81
# File 'lib/barefoot_js.rb', line 79

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

Class Method Details

.derive_vars_from_defaults(defaults, props) ⇒ Object

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

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



337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
# File 'lib/barefoot_js.rb', line 337

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

Instance Method Details

#abs(value) ⇒ Object

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



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

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

#async_boundary(id, fallback_html) ⇒ Object



375
376
377
378
# File 'lib/barefoot_js.rb', line 375

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

#async_resolve(id, content_html) ⇒ Object



380
381
382
# File 'lib/barefoot_js.rb', line 380

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

#at(recv, i) ⇒ Object

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



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

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

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

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

  recv[idx]
end

#bool_str(value) ⇒ Object

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



177
178
179
# File 'lib/barefoot_js.rb', line 177

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

#ceil(value) ⇒ Object



455
456
457
458
# File 'lib/barefoot_js.rb', line 455

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

#comment(text) ⇒ Object


Comment Markers



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

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

#concat(a, b) ⇒ Object

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



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

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

#data_key_attrObject

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



114
115
116
117
118
119
120
# File 'lib/barefoot_js.rb', line 114

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

  escaped = k.to_s.gsub('&', '&amp;').gsub('"', '&quot;')
  %( data-key="#{escaped}")
end

#date(recv, op) ⇒ Object

date(recv, op) -- zero-arg Date.prototype method lowering (#2274, spec entry "date"). recv arrives as either this runtime's own Time or an ISO-8601 String (a template prop may carry either depending on how the host populated it), so both normalize through Time.iso8601 / #utc to the same instant before dispatch. #mon is 1-based in Ruby; only getUTCMonth subtracts 1 to match JS's 0-based month. getTime sums whole milliseconds from tv_sec/tv_nsec rather than rounding a Float ms value -- tv_nsec is always the non-negative sub-second remainder (Ruby normalizes Time's internal rational), even for a pre-epoch instant, so integer division here stays exact.



503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
# File 'lib/barefoot_js.rb', line 503

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

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

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



1110
1111
1112
1113
1114
1115
1116
1117
1118
# File 'lib/barefoot_js.rb', line 1110

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



705
706
707
708
709
# File 'lib/barefoot_js.rb', line 705

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



1315
1316
1317
# File 'lib/barefoot_js.rb', line 1315

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.



699
700
701
702
703
# File 'lib/barefoot_js.rb', line 699

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



1311
1312
1313
# File 'lib/barefoot_js.rb', line 1311

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

#find(recv, pred) ⇒ Object



717
718
719
720
721
# File 'lib/barefoot_js.rb', line 717

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



1323
1324
1325
# File 'lib/barefoot_js.rb', line 1323

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



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

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



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

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



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

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



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

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.



975
976
977
978
979
980
981
982
983
984
985
986
987
# File 'lib/barefoot_js.rb', line 975

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



1007
1008
1009
# File 'lib/barefoot_js.rb', line 1007

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.



1013
1014
1015
1016
1017
1018
# File 'lib/barefoot_js.rb', line 1013

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



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

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



1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
# File 'lib/barefoot_js.rb', line 1023

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

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

#floor(value) ⇒ Object



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

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

#format_date(recv, pattern, tz, 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.



602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
# File 'lib/barefoot_js.rb', line 602

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



658
659
660
661
662
663
664
665
666
667
668
669
670
671
# File 'lib/barefoot_js.rb', line 658

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.



788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
# File 'lib/barefoot_js.rb', line 788

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.



425
426
427
428
429
# File 'lib/barefoot_js.rb', line 425

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

  html_escape(string(value))
end

#has_unsafe_style_value?(value) ⇒ Boolean

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

Returns:

  • (Boolean)


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
862
863
864
865
866
867
868
869
870
871
872
# File 'lib/barefoot_js.rb', line 837

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

#hydration_attrsObject

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



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

def hydration_attrs
  parts = []
  host = _bf_parent
  mount = _bf_mount
  parts << %(bf-h="#{host.gsub('"', '&quot;')}") if host && !host.empty?
  parts << %(bf-m="#{mount.gsub('"', '&quot;')}") if mount && !mount.empty?
  parts << 'bf-r=""' unless _is_child
  parts.join(' ')
end

#includes(recv, elem) ⇒ Object

Array.prototype.includes(x) / String.prototype.includes(sub) share a method name in JS; dispatch on Ruby class the way BarefootJS.pm dispatches on ref(). The Array arm scans with Evaluator.same_value_zero? (SameValueZero: no cross-type coercion, e.g. [2].includes("2") is false; NaN matches NaN) -- the same algorithm the evaluator's serialized-callback array-method path uses for .includes, so both positions agree.



684
685
686
687
688
689
690
691
# File 'lib/barefoot_js.rb', line 684

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.



900
901
902
# File 'lib/barefoot_js.rb', line 900

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.



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

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

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

#json(value) ⇒ Object



394
395
396
# File 'lib/barefoot_js.rb', line 394

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

#last_index_of(recv, elem) ⇒ Object



904
905
906
# File 'lib/barefoot_js.rb', line 904

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

#lc(s) ⇒ Object

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



745
746
747
# File 'lib/barefoot_js.rb', line 745

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



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

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



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

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

#max(a, b) ⇒ Object



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

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

  x > y ? x : y
end

#min(a, b) ⇒ Object

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



469
470
471
472
473
474
475
476
# File 'lib/barefoot_js.rb', line 469

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

  x < y ? x : y
end

#number(value) ⇒ Object

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



441
442
443
444
445
446
447
448
# File 'lib/barefoot_js.rb', line 441

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

  Float::NAN
end

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



1220
1221
1222
# File 'lib/barefoot_js.rb', line 1220

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.



1216
1217
1218
# File 'lib/barefoot_js.rb', line 1216

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

#props_attrObject



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

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

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

#provide_context(name, value) ⇒ Object



149
150
151
152
# File 'lib/barefoot_js.rb', line 149

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

#query(base, *triples) ⇒ Object

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



1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
# File 'lib/barefoot_js.rb', line 1173

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



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
# File 'lib/barefoot_js.rb', line 1266

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



1307
1308
1309
# File 'lib/barefoot_js.rb', line 1307

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

#register_child_renderer(name, renderer) ⇒ Object

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



234
235
236
# File 'lib/barefoot_js.rb', line 234

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

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


Bulk registration from build manifest

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



265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
# File 'lib/barefoot_js.rb', line 265

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

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

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

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

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

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

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

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

#register_script(path) ⇒ Object


Script Registration



213
214
215
216
217
218
# File 'lib/barefoot_js.rb', line 213

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

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

#render_child(name, *args) ⇒ Object



238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
# File 'lib/barefoot_js.rb', line 238

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

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

#repeat(recv, count) ⇒ Object

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



1209
1210
1211
1212
1213
# File 'lib/barefoot_js.rb', line 1209

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



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

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.



1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
# File 'lib/barefoot_js.rb', line 1146

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



968
969
970
# File 'lib/barefoot_js.rb', line 968

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

#revoke_context(name) ⇒ Object



154
155
156
157
158
# File 'lib/barefoot_js.rb', line 154

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

#round(value) ⇒ Object



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

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

#scope_attrObject

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



96
97
98
# File 'lib/barefoot_js.rb', line 96

def scope_attr
  _scope_id || ''
end

#scope_commentObject

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



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

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

#scope_comment_endObject

Paired end marker for scope_comment, emitted after the fragment's last top-level node. No host/props segments -- the client only needs the scope id to close the boundary (#2289).



204
205
206
207
# File 'lib/barefoot_js.rb', line 204

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

#scriptsObject



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

def scripts
  _scripts.map { |path| %(<script type="module" src="#{path}"></script>) }.join("\n")
end

#search_params(query = '') ⇒ Object

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



87
88
89
# File 'lib/barefoot_js.rb', line 87

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

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

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



943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
# File 'lib/barefoot_js.rb', line 943

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



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

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



1319
1320
1321
# File 'lib/barefoot_js.rb', line 1319

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.



1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
# File 'lib/barefoot_js.rb', line 1229

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



1303
1304
1305
# File 'lib/barefoot_js.rb', line 1303

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



1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
# File 'lib/barefoot_js.rb', line 1075

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.



1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
# File 'lib/barefoot_js.rb', line 1357

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



1099
1100
1101
1102
1103
1104
1105
1106
1107
# File 'lib/barefoot_js.rb', line 1099

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

#streaming_bootstrapObject


Streaming SSR (Out-of-Order)



371
372
373
# File 'lib/barefoot_js.rb', line 371

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

#string(value) ⇒ Object

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



404
405
406
407
408
409
410
411
412
413
414
415
416
# File 'lib/barefoot_js.rb', line 404

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

  value.to_s
end

#style_object(*pairs) ⇒ Object

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



887
888
889
890
891
892
893
894
895
896
# File 'lib/barefoot_js.rb', line 887

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

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

#text_endObject



185
186
187
# File 'lib/barefoot_js.rb', line 185

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

#text_start(slot_id) ⇒ Object



181
182
183
# File 'lib/barefoot_js.rb', line 181

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

#to_fixed(value, digits = 0) ⇒ Object

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



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

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



1036
1037
1038
1039
1040
# File 'lib/barefoot_js.rb', line 1036

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



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

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.



1045
1046
1047
1048
1049
# File 'lib/barefoot_js.rb', line 1045

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

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

#truthy?(value) ⇒ Boolean

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

Returns:

  • (Boolean)


434
435
436
# File 'lib/barefoot_js.rb', line 434

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

#uc(s) ⇒ Object



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

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

#use_context(name, default = nil) ⇒ Object



160
161
162
163
# File 'lib/barefoot_js.rb', line 160

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