Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .claude/agents/perf-reviewer.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
name: perf-reviewer
description: Reviews a diff in ruby2html for rendering-speed regressions and missed optimizations (allocations, buffer sizing, method dispatch, C hot paths). Use proactively after changes to lib/gem/ruby2html/render.rb, ext/ruby2html/*, or the template handler, and before merging perf claims.
description: Reviews a diff in ruby2html for rendering-speed regressions and missed optimizations (allocations, buffer sizing, method dispatch, C hot paths). Use proactively after changes to apps/ruby2html/lib/gem/ruby2html/render.rb, apps/ruby2html/ext/ruby2html/*, or the template handler, and before merging perf claims.
tools: Read, Grep, Glob, Bash
---

Expand All @@ -15,6 +15,6 @@ Check, citing file:line:
- Caches: unbounded growth, keys that can collide (never cache by `Object#hash` alone).
- Template handler / ComponentHelper: work done per render that could be done at compile time.

Run only read-only or benchmark commands: `bundle exec rake compile`, `bundle exec rake bench`, `bundle exec ruby -Ilib/gem -Ilib bench/<script>.rb`. Never edit files, commit or push.
Run only read-only or benchmark commands: `bundle exec rake compile` and `bundle exec rake bench` (run from `apps/ruby2html`), and from the repo root `bundle exec ruby -Iapps/ruby2html/lib/gem -Iapps/ruby2html/lib apps/ruby2html/bench/<script>.rb`. Never edit files, commit or push.

Output: a ranked list (impact high/medium/low) with the evidence, and a before/after benchmark table if you ran one (same machine, record `ruby -v`). Say "no measurable change" when error bars overlap. No speculation without a pointer to code.
4 changes: 2 additions & 2 deletions .claude/agents/xss-auditor.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ tools: Read, Grep, Glob, Bash

You audit ruby2html for escaping correctness. Default must be: text content and attribute values escaped; raw output only for `html_safe?` strings via `plain` (and documented raw APIs such as `component`).

Read `AGENTS.md` and `llms.txt`, then the diff (`git diff` / `git diff main...HEAD`).
Read `AGENTS.md` and `apps/ruby2html/llms.txt`, then the diff (`git diff` / `git diff main...HEAD`).

Check, citing file:line:
- Every path that appends user data: tag content (positional and block return), attribute values, attribute keys, `plain`, block-return handling, helper wrappers (`link_to`, `button_to`, `image_tag`, `form_with` / FormBuilder proxy), template handler annotations.
Expand All @@ -16,6 +16,6 @@ Check, citing file:line:
- Caches returning output computed for different input.

Verify with a quick script when unsure, e.g.:
`bundle exec ruby -Ilib/gem -Ilib -e 'require "ruby2html"; puts Ruby2html::Render.new { div(title: %q("><script>)) { plain %q(<b>) } }.render'`
`bundle exec ruby -Iapps/ruby2html/lib/gem -Iapps/ruby2html/lib -e 'require "ruby2html"; puts Ruby2html::Render.new { div(title: %q("><script>)) { plain %q(<b>) } }.render'`

Never edit files, commit or push. Output: findings ranked critical/high/low with a minimal reproducing snippet and expected vs actual HTML, or an explicit "no issues found" with what you checked.
2 changes: 1 addition & 1 deletion .claude/commands/feature.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ $ARGUMENTS
4. Build primitive-first: one reusable primitive with its first real caller. No
abstraction before a consumer.
5. **Prove it.** `bin/setup` after any dependency change, then `bin/check`, which here is
`bundle exec rake compile && bin/rubocop --parallel && CI=1 bundle exec rspec && bin/brakeman --no-pager`.
`cd apps/ruby2html && bundle exec rake compile && ../../bin/rubocop --parallel . ../../bin && CI=1 bundle exec rspec && ../../bin/brakeman --no-pager`.
Red is not a pull request: fix the cause, never silence the gate. Add the failing test
first — a green run that never went red proves nothing.
6. **One pull request in flight, start to finish.** Open it, watch it to merged, run
Expand Down
4 changes: 3 additions & 1 deletion .claude/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,9 @@
"Bash(bin/*)"
],
"deny": [
"Read(./config/master.key)",
"Read(./apps/ruby2html/config/master.key)",
"Read(./apps/ruby2html/.env*)",
"Read(./apps/ruby2html/tmp/local_secret.txt)",
"Read(./.env*)",
"Read(./tmp/local_secret.txt)",
"Bash(git tag *)",
Expand Down
10 changes: 5 additions & 5 deletions .claude/skills/add-html-tag/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,17 @@ description: Add a new HTML element (standard, void, or hyphenated custom elemen

# Add an HTML tag to ruby2html

Tags are data: `Ruby2html::Render::HTML5_TAGS` / `VOID_ELEMENTS` in `lib/gem/ruby2html/render.rb`. At load, `define_tag(method_name, tag, void)` is called on both `Ruby2html::Native` (C) and `Ruby2html::PureRuby`. No C change is needed unless a limit is hit.
Tags are data: `Ruby2html::Render::HTML5_TAGS` / `VOID_ELEMENTS` in `apps/ruby2html/lib/gem/ruby2html/render.rb`. At load, `define_tag(method_name, tag, void)` is called on both `Ruby2html::Native` (C) and `Ruby2html::PureRuby`. No C change is needed unless a limit is hit.

## Steps

1. Add the tag as written in HTML (lowercase, hyphens allowed, e.g. `turbo-frame`) to `HTML5_TAGS`, keeping the list's ordering style. Method name = `tag.tr('-', '_')`.
2. Void element per the HTML spec (no closing tag)? Also add it to `VOID_ELEMENTS` (renders `<tag attrs />`, ignores content and block).
3. Limits (C, `ext/ruby2html/tag_render.c`): name must match `[a-z0-9-]` and be ≤ 32 bytes (`R2H_MAX_TAG_NAME`); at most 200 tags (`R2H_MAX_TAGS`, trampoline pool). Raising a limit is a C change: update the macros/trampoline list and recompile.
3. Limits (C, `apps/ruby2html/ext/ruby2html/tag_render.c`): name must match `[a-z0-9-]` and be ≤ 32 bytes (`R2H_MAX_TAG_NAME`); at most 200 tags (`R2H_MAX_TAGS`, trampoline pool). Raising a limit is a C change: update the macros/trampoline list and recompile.
4. Name clashes: the new method shadows any same-named method for code running on a `Render` or `Ruby2html::Component` (and wins over component methods/slots inside ViewComponent `html` blocks). Check `Render`/`Component` methods (`render`, `plain`, `component`, `content`, `helpers`, `output`, `current_output`, `link_to`, `image_tag`, `form_with`, `button_to`) and common Rails helpers. `TemplateCompiler` picks the tag up automatically (it reads `HTML5_TAGS`), so in `.html.rb` templates a receiverless call of that name will now be rewritten to the tag; search the demo app and specs for existing uses.
5. Specs in `spec/gem/` inside a `RenderSpecHelper.describe_each` group (runs Native and PureRuby): content, attributes, block, escaping; void form if void.
6. `bundle exec rake compile`, `CI=1 bundle exec rspec spec/gem`, `bundle exec rubocop lib/gem/ruby2html/render.rb`.
7. Docs: mention notable new tags in `llms.txt` (DSL rule 1); `### Added` under `## [Unreleased]` in `CHANGELOG.md`.
5. Specs in `apps/ruby2html/spec/gem/` inside a `RenderSpecHelper.describe_each` group (runs Native and PureRuby): content, attributes, block, escaping; void form if void.
6. Run from `apps/ruby2html` (bundle/rake commands run there): `bundle exec rake compile`, `CI=1 bundle exec rspec spec/gem`, `bundle exec rubocop lib/gem/ruby2html/render.rb`.
7. Docs: mention notable new tags in `apps/ruby2html/llms.txt` (DSL rule 1); `### Added` under `## [Unreleased]` in `CHANGELOG.md`.

## Notes

Expand Down
6 changes: 3 additions & 3 deletions .claude/skills/benchmark/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@ description: Measure ruby2html rendering performance and compare before/after a

# Benchmark ruby2html (before/after)

Project rule: speed over memory. Perf claims need numbers from the same machine, Ruby and YJIT state.
Project rule: speed over memory. Perf claims need numbers from the same machine, Ruby and YJIT state. Commands run from `apps/ruby2html` (the Rakefile, Gemfile and bundle config live there); from the repo root, the scripts below are `apps/ruby2html/bench/*.rb` and the request spec is `apps/ruby2html/spec/benchmarks/`.

## Tools

| Command | Measures |
|---|---|
| `bundle exec rake bench` | compiles, runs every `bench/*.rb` |
| `bundle exec rake bench` | compiles, runs every `apps/ruby2html/bench/*.rb` |
| `bundle exec ruby bench/render_bench.rb` | full page render vs Phlex / Erubi, plus allocations per render |
| `bundle exec ruby bench/tag_bench.rb` | per-tag cost for common call shapes |
| `bundle exec ruby bench/escape_bench.rb` | escaping throughput, clean/dirty, short/long |
Expand All @@ -23,7 +23,7 @@ Variants: `--yjit` (or `RUBY_YJIT_ENABLE=1`), `RUBY2HTML_DISABLE_EXT=1` (pure Ru

1. Record environment: `ruby -v` (shows `+YJIT`), `uname -m`, which variant flags were used.
2. **Baseline** on the base revision: `git worktree add "$SCRATCH/r2h-base" <rev>` (or `git stash`), `bundle exec rake compile` there, run the chosen command, save output to the scratch dir.
3. **Candidate**: back on the change, `bundle exec rake compile` (mandatory after `ext/` edits), identical command, save output.
3. **Candidate**: back on the change, `bundle exec rake compile` (mandatory after `apps/ruby2html/ext/` edits), identical command, save output.
4. Repeat each side at least twice if the delta is < 10%; overlapping benchmark-ips error bars = "no measurable change". Check allocation counts (render_bench) for regressions too.
5. Correctness: `CI=1 bundle exec rspec`.
6. Report a table: benchmark, before, after, ratio, error; environment line. Never round away regressions. Remove the worktree (`git worktree remove`).
Expand Down
22 changes: 11 additions & 11 deletions .claude/skills/debug-render/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,24 @@ description: Debug wrong or crashing ruby2html output - diff the C extension (Ru

# Debug ruby2html rendering

Two implementations exist and must match byte for byte: `Ruby2html::Native` (`ext/ruby2html/`) and `Ruby2html::PureRuby` (`lib/gem/ruby2html/render/pure_ruby.rb`). `Ruby2html::IMPLEMENTATION` says which one `Render` uses; `RUBY2HTML_DISABLE_EXT=1` forces PureRuby.
Two implementations exist and must match byte for byte: `Ruby2html::Native` (`apps/ruby2html/ext/ruby2html/`) and `Ruby2html::PureRuby` (`apps/ruby2html/lib/gem/ruby2html/render/pure_ruby.rb`). `Ruby2html::IMPLEMENTATION` says which one `Render` uses; `RUBY2HTML_DISABLE_EXT=1` forces PureRuby.

## 1. Which side is wrong?

```bash
bundle exec rake compile
cd apps/ruby2html && bundle exec rake compile && cd ../..
cat > "$SCRATCH/case.rb" <<'RUBY'
Ruby2html::Render.new { div(class: ['a', nil], data: { x_y: 1 }) { td { 1.5 } } }.render.to_str
RUBY
for mode in 0 1; do
RUBY2HTML_DISABLE_EXT=$mode bundle exec ruby -Ilib/gem -Ilib -e 'require "ruby2html"; require "active_support"; require "active_support/core_ext/string/output_safety"
RUBY2HTML_DISABLE_EXT=$mode bundle exec ruby -Iapps/ruby2html/lib/gem -Iapps/ruby2html/lib -e 'require "ruby2html"; require "active_support"; require "active_support/core_ext/string/output_safety"
out = eval(File.read(ARGV[0])); puts "#{Ruby2html::IMPLEMENTATION}: #{out.inspect} #{out.encoding}"' "$SCRATCH/case.rb"
done
```

(`$SCRATCH` = your scratch directory.) Drop the ActiveSupport requires to test the no-Rails path; `html_safe?` handling differs with/without it. Same-process comparison (what the specs do, see `spec/gem/render_spec_helper.rb`): `PureRuby` always gets the tag methods, so `pure = Class.new(Ruby2html::Render) { include Ruby2html::PureRuby }` renders with PureRuby while `Ruby2html::Render` uses Native: compare `pure.new(&blk).render` with `Ruby2html::Render.new(&blk).render`.
(`$SCRATCH` = your scratch directory.) Drop the ActiveSupport requires to test the no-Rails path; `html_safe?` handling differs with/without it. Same-process comparison (what the specs do, see `apps/ruby2html/spec/gem/render_spec_helper.rb`): `PureRuby` always gets the tag methods, so `pure = Class.new(Ruby2html::Render) { include Ruby2html::PureRuby }` renders with PureRuby while `Ruby2html::Render` uses Native: compare `pure.new(&blk).render` with `Ruby2html::Render.new(&blk).render`.

Decide against the documented rules (`llms.txt`, comments at the top of `content.c` / `attributes.c` / `tag_render.c`), not against whichever side "looks right". Fix the wrong side, then add the case to a `RenderSpecHelper.describe_each` group or `spec/gem/render_parity_spec.rb` (both run Native and PureRuby).
Decide against the documented rules (`apps/ruby2html/llms.txt`, comments at the top of `content.c` / `attributes.c` / `tag_render.c`), not against whichever side "looks right". Fix the wrong side, then add the case to a `RenderSpecHelper.describe_each` group or `apps/ruby2html/spec/gem/render_parity_spec.rb` (both run Native and PureRuby).

## 2. Minimize

Expand All @@ -35,25 +35,25 @@ Scalar vs SIMD escape path: `RUBY2HTML_NO_SIMD=1 bundle exec rake compile` and r
`bin/build-ext` builds the CI variants (CI runs the same commands in the `sanitizers` and `strict-ext` jobs):

```bash
bin/build-ext --sanitize # ASAN + UBSAN build into lib/ruby2html
bin/build-ext --sanitize # ASAN + UBSAN build into apps/ruby2html/lib/ruby2html
LD_PRELOAD="$(gcc -print-file-name=libasan.so):$(gcc -print-file-name=libubsan.so)" \
ASAN_OPTIONS=detect_leaks=0:use_sigaltstack=0 bundle exec ruby -Ilib/gem "$SCRATCH/repro.rb" # or: ... bundle exec rspec spec/gem
bundle exec rake clobber && bundle exec rake compile # ALWAYS rebuild normally afterwards
ASAN_OPTIONS=detect_leaks=0:use_sigaltstack=0 bundle exec ruby -Iapps/ruby2html/lib/gem "$SCRATCH/repro.rb" # or: ... bundle exec rspec apps/ruby2html/spec/gem
(cd apps/ruby2html && bundle exec rake clobber && bundle exec rake compile) # ALWAYS rebuild normally afterwards
bin/build-ext --werror # / --cc=clang --werror: warnings as errors
```

Without the script: `bundle exec rake compile -- --with-cflags="-fsanitize=address,undefined -fno-omit-frame-pointer -g -O1" --with-ldflags="-fsanitize=address,undefined"`.
Without the script (run from `apps/ruby2html`): `bundle exec rake compile -- --with-cflags="-fsanitize=address,undefined -fno-omit-frame-pointer -g -O1" --with-ldflags="-fsanitize=address,undefined"`.

`detect_leaks=0` is required (Ruby's own allocations look like leaks). macOS/clang: use `-fsanitize=address` and `DYLD_INSERT_LIBRARIES=$(clang -print-file-name=libclang_rt.asan_osx_dynamic.dylib)`.

GC-related bugs (missing `RB_GC_GUARD`, stale `RSTRING_PTR` after the buffer grows or after a yield) often only show under GC pressure: run the repro with `GC.stress = true` (slow; keep the input tiny), or in a loop with `GC.compact` between renders.

gdb: `gdb -ex run --args ruby -Ilib/gem -Ilib repro.rb`, build with `--with-cflags="-O0 -g"`.
gdb: `gdb -ex run --args ruby -Iapps/ruby2html/lib/gem -Iapps/ruby2html/lib repro.rb`, build with `--with-cflags="-O0 -g"`.

## 4. Rails-side issues

- Template output wrong but `Render.new { ... }` right: inspect the compiled source: `Ruby2html::RailsComponents::TemplateCompiler.compile(File.read(path)).source` (nil means Prism unavailable or parse error → closure fallback).
- Helper markup outside its tag: check `rails_components/output_capture.rb` (buffer swap during the helper's block).
- Helper markup outside its tag: check `apps/ruby2html/lib/gem/ruby2html/rails_components/output_capture.rb` (buffer swap during the helper's block).

## Done when

Expand Down
9 changes: 5 additions & 4 deletions .claude/skills/release/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ disable-model-invocation: true

Follows `docs/release.md`. Argument: target version `X.Y.Z` or `patch|minor|major`.

Merging a change to `lib/gem/ruby2html/version.rb` into `main` runs `.github/workflows/release.yml`,
Merging a change to `apps/ruby2html/lib/gem/ruby2html/version.rb` into `main` runs `.github/workflows/release.yml`,
which **publishes to RubyGems**, then tags and creates the GitHub release. So opening the release PR
is safe; **merging it is the publish action**.

Expand All @@ -21,9 +21,10 @@ is safe; **merging it is the publish action**.
2. **Bump**: `VERSION = 'X.Y.Z'`; CHANGELOG `## [X.Y.Z] - <today>` + empty `## [Unreleased]` + link
reference; `bin/changelog-notes X.Y.Z` prints the section.
3. **Refresh the issue-template versions placeholder** in `.github/ISSUE_TEMPLATE/bug_report.yml`:
gem version from `lib/gem/ruby2html/version.rb`, Ruby version from `.ruby-version`, Rails version
from `Gemfile.lock`. GitHub issue forms cannot derive the value at fill time, so it is refreshed here.
4. **Verify**: `bundle exec rake compile`, `CI=1 bundle exec rspec`, `bundle exec rubocop`,
gem version from `apps/ruby2html/lib/gem/ruby2html/version.rb`, Ruby version from
`apps/ruby2html/.ruby-version`, Rails version from `apps/ruby2html/Gemfile.lock`.
GitHub issue forms cannot derive the value at fill time, so it is refreshed here.
4. **Verify** (run from `apps/ruby2html`): `bundle exec rake compile`, `CI=1 bundle exec rspec`, `bundle exec rubocop`,
`bundle exec rake smoke`.
5. **PR**: branch `release/X.Y.Z`, one commit `release: X.Y.Z`, PR titled `release: X.Y.Z` with the
changelog section as the body, containing nothing else apart from the placeholder refresh in
Expand Down
10 changes: 5 additions & 5 deletions .claude/skills/review/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ description: Review checklist for ruby2html changes, especially ext/ (C) and the

# Review a ruby2html change

Scope: `git diff` (uncommitted) or `git diff main...HEAD`. Read `AGENTS.md` "Sharp edges / invariants" first. Report findings as file:line + why + fix; say explicitly which checklist sections passed.
Scope: `git diff` (uncommitted) or `git diff main...HEAD`. Read `AGENTS.md` "Sharp edges / invariants" first. Report findings as file:line + why + fix; say explicitly which checklist sections passed. Bundle/rake/rspec commands below run from `apps/ruby2html`; from the repo root those paths are `apps/ruby2html/ext/ruby2html/` and `apps/ruby2html/spec/gem`.

## C extension (`ext/ruby2html/`)
## C extension (`apps/ruby2html/ext/ruby2html/`)

- [ ] **GC safety**: every `VALUE` whose pointer (`RSTRING_PTR`, `RARRAY_CONST_PTR`) is used across a call that may allocate/yield/raise has `RB_GC_GUARD` after its last use. Temporaries from `rb_obj_as_string`, `rb_sym2str`, `rb_String`, `r2h_utf8_compatible` included.
- [ ] **Stale pointers**: no `RSTRING_PTR(buf)` cached across `r2h_buf_reserve`, `rb_yield*`, `rb_funcall` or any append (buffer may move).
Expand All @@ -27,7 +27,7 @@ Scope: `git diff` (uncommitted) or `git diff main...HEAD`. Read `AGENTS.md` "Sha

## Parity (Native vs PureRuby)

- [ ] Every rule change exists in both `ext/ruby2html/*.c` and `lib/gem/ruby2html/render/pure_ruby.rb`.
- [ ] Every rule change exists in both `apps/ruby2html/ext/ruby2html/*.c` and `apps/ruby2html/lib/gem/ruby2html/render/pure_ruby.rb`.
- [ ] Specs pass in both modes: `CI=1 bundle exec rspec spec/gem` and `RUBY2HTML_DISABLE_EXT=1 bundle exec rspec spec/gem`. Same exceptions too.
- [ ] Suspicious cases diffed with the `debug-render` skill.

Expand All @@ -45,6 +45,6 @@ Scope: `git diff` (uncommitted) or `git diff main...HEAD`. Read `AGENTS.md` "Sha

## Hygiene

- [ ] `bundle exec rubocop` clean; no Ruby syntax newer than the gemspec `required_ruby_version` floor in `lib/`.
- [ ] `bundle exec rubocop` clean; no Ruby syntax newer than the gemspec `required_ruby_version` floor in `apps/ruby2html/lib/`.
- [ ] CHANGELOG `## [Unreleased]` entry for user-visible changes (breaking → "Upgrading to 2.0").
- [ ] `llms.txt` updated if user-facing behavior changed.
- [ ] `apps/ruby2html/llms.txt` updated if user-facing behavior changed.
Loading
Loading