8000 GitHub - taq/repolho: Repolho generates HTML reports from .rep.erb templates with support for nested groupings, page breaks, headers/footers, and statistics (sum, avg, min, max). Works with ActiveRecord::Result, Relation, or arrays of hashes. · GitHub
[go: up one dir, main page]

Skip to content
taqPublic

About

Repolho generates HTML reports from .rep.erb templates with support for nested groupings, page breaks, headers/footers, and statistics (sum, avg, min, max). Works with ActiveRecord::Result, Relation, or arrays of hashes.

Resources

Stars

1 star

Watchers

0 watching

Forks

Latest commit

 

History

12 Commits

Folders and files

Repository files navigation

Repolho

Logo

Report generator for Ruby and Rails.

Given a .rep.erb template and a data source (ActiveRecord exec_query, Relation, array of models, or array of hashes), Repolho produces HTML with:

  • Nested groupings (<report-group>)
  • Page breaks (<report-page rows="N">)
  • Headers and footers at every level
  • Per-container statistics (sum, avg, min, max, count)
  • Optional group-header reprint on page break

Installation

# Gemfile
gem "repolho"
bundle install

Usage

Plain Ruby:

require "repolho"

rows = Order.connection.exec_query(sql)
# or
# rows = Order.joins(...).select(...).order(...)

report = Repolho::Main.new("app/views/reports/sales.rep.erb", rows, { company: "Acme" })
report.run

html = report.content.join

Or with Rails:

render html: html.html_safe
# or File.write("report.html", html)

Generating a template

Repolho::TemplateGenerator creates a .rep.erb file from a base template and column metadata. The base template must already contain a <report-page> element (the page is required, matching the runtime) and two markers:

  • <!-- REPORT:HEADERS --> — replaced by the generated <th> cells
  • <!-- REPORT:ROW --> — replaced by the generated <report-row>

Column names are taken from the data source without executing the full query when possible (ActiveRecord::Result#columns, a Relation's selected columns or model column names, an array of hashes/models, or an explicit array of column names).

# From a Relation or Result (columns only)
Repolho::TemplateGenerator.new(
  "app/views/reports/base.rep.erb",
  Order.where(status: "paid")
).write("app/views/reports/orders.rep.erb")

# Explicit columns + custom labels + numeric formatting
Repolho::TemplateGenerator.new(
  "app/views/reports/base.rep.erb",
  %w[id customer_name total],
  labels:  { "total" => "Total (R$)" },
  numeric: %w[total]
).write("app/views/reports/orders.rep.erb")

When running under Rails (or whenever I18n is available), header labels are looked up case-insensitively under activerecord.attributes.<model>.<column> (if a model: is given) or repolho.attributes.<column>. The lookup tries the original key, the downcase form and the upcase form. If no translation is found, a simple humanized name is used.

Base template skeleton:

<report>
  <report-page rows="40">
    <report-header>
      <tr>
        <!-- REPORT:HEADERS -->
      </tr>
    </report-header>

    <!-- REPORT:ROW -->

    <report-footer>
      <!-- optional page totals -->
    </report-footer>
  </report-page>
</report>

Template (.rep.erb)

Repolho uses a few custom elements inside a normal HTML document. Everything outside <report>…</report> is copied as-is (the previous and after sections). Everything inside the custom elements is processed as ERB.

Why <report> instead of <repolho>?

The element names are intentionally generic (<report>, <report-page>, <report-group>, etc.) rather than namespaced under the gem (<repolho>, <repolho-page>, …).

Using repolho would tie the markup to this specific tool. With report, the template describes a report structure, not a Ruby library. Someone working in another language or with another report engine can reuse the same markup (or a close subset of it) without carrying Repolho-specific names. The goal is a small, readable convention for report templates — not a proprietary dialect.

Custom elements

Element Purpose Attributes
<report> Root container of the report pagination (optional, "true"): enables client-side page navigation. Also accepts pagination-first, pagination-prev, pagination-next, pagination-last for custom labels.
orientation (optional, "portrait" or "landscape"): page orientation for print.
paper (optional, e.g. "A4", "Letter"): paper size for print. If either is present, a @media print style is injected at the end of <body>.
min-row-for-break (optional, integer): when a group has page-break-after="true", the forced break is skipped if the current page row number is less than or equal to this value. Prevents an extra break when a page has just started.
<report-page> Defines a page (required, only one) rows (required): max data rows per page
<report-group> Nested grouping id (required), attr (required), reprint-header-on-page-break (optional, "true"), page-break-after (optional, "true"): after the group ends and its footer is emitted on the current page, fill the remaining rows with blanks and force a page break so the next group starts on a fresh page. Respects min-row-for-break on <report>.
<report-header> Header of the current container —
<report-footer> Footer of the current container —
<report-row> Template for each data row (inside the innermost group) —
<report-last-page> Optional block rendered only at the end of the report, as additional rows on the last page. Counts toward the page row limit and can force a page break. —

Last page content (<report-last-page>)

Optional element that is rendered only after all data rows have been processed.
Its content is injected as normal rows into the page flow, so:

  • It counts toward the rows limit of <report-page>.
  • If it does not fit on the current page, a new page is opened automatically.
  • The page footer of the final page is rendered after this block.

Because the block is only ever emitted at the end, you do not need to check last_page? inside it.

You can print the overall report total from this block by using the second argument of the statistics methods (see below):

<report-last-page>
  <tr>
    <td colspan="4">Grand total</td>
    <td><%= format("%.2f", sum(:value, :report)) %></td>
  </tr>
  <tr>
    <td colspan="5">Final notes and signature</td>
  </tr>
</report-last-page>

Statistics context

Every container keeps its own statistics. By default sum, avg, count, min and max operate on the current container.

All statistics methods accept an optional second argument that selects another context:

Context Meaning
nil (default) Current container
:page The current page
:report The whole report (grand total)
a Container object That specific container

Examples:

<%= sum(:value) %>                 <!-- current group or page -->
<%= sum(:value, :page) %>          <!-- current page total -->
<%= sum(:value, :report) %>        <!-- overall report total -->
<%= report.sum(:value) %>          <!-- same as above, via the report object -->

The report object is available in every ERB context (headers, footers, rows and last-page content).

Allowed HTML inside page content

Each page is emitted as a <tbody class="repolho-page" data-page="N">. Because of HTML table rules, only <tr> elements may appear as direct children of that <tbody>.

Allowed Not allowed Why
<tr> (with <td> / <th>) <thead> <thead> cannot be a child of <tbody>. The browser moves it out and empties the page container.
<tfoot> Same reason.
<div>, <p>, <section>, etc. Invalid inside a table.

Use <tr> for headers and footers of pages and groups. Reserve <thead> / <tfoot> only for content that lives outside the page bodies (for example a report-level header that should appear before any page, if you place it before the first <report-page>).

Page wrapper

Repolho automatically wraps each page like this:

<tbody class="repolho-page" data-page="1" style="break-after: page; page-break-after: always;">
  <!-- only <tr> elements here -->
</tbody>

The data-page attribute and the break-after style are injected for you. You do not need to write the <tbody> yourself.

Client-side pagination

If you set pagination="true" on the root <report> element, Repolho appends a navigation bar just before the after section (still inside the table) and a small pure-ES6 script at the end of the document (inserted before </body> when present, otherwise appended). The bar is omitted when the report has only one page.

Optional attributes let you customize the labels:

Attribute Default Description
pagination-first First First page link text
pagination-prev < Previous page link text
pagination-next > Next page link text
pagination-last Last Last page link text
<report pagination="true"
        pagination-first="Primeira"
        pagination-prev="«"
        pagination-next="»"
        pagination-last="Última">
  ...
</report>

The generated controls look like:

<tbody id="report-pagination" data-max="N">
  <tr>
    <td colspan="200" id="report-pagination-links">
      <a href="#" id="report-first-page">First</a>
      <a href="#" id="report-prev-page"><</a>
      <input type="number" id="report-current-page" min="1" max="N" value="1"/>
      <a href="#" id="report-next-page">></a>
      <a href="#" id="report-last-page">Last</a>
    </td>
  </tr>
</tbody>

You can restyle the links and the input with normal CSS. The script is wrapped in try/catch so a failure does not break the rest of the page.

When pagination is enabled, page_number (available in ERB) holds the total number of pages after the report has been fully generated.

Template example

<!DOCTYPE html>
<html>
  <head>
    <title>Sales report</title>
  </head>
  <body>
    <table>
      <report>
        <report-header>
          <tr><th colspan="100"><%= @company %></th></tr>
          <tr><th colspan="100">Sales report</th></tr>
        </report-header>

        <report-page rows="40">
          <report-header>
            <tr>
              <td>Id</td>
              <td>Name</td>
              <td class="text-end">Value</td>
            </tr>
          </report-header>

          <report-footer>
            <tr>
              <td colspan="2">Page total</td>
              <td><%= format("%.2f", sum(:value)) %></td>
            </tr>
          </report-footer>

          <report-group id="state" attr="state" reprint-header-on-page-break="true">
            <report-header>
              <tr>
                <td>State:</td>
                <td colspan="100"><%= attr(:state) %></td>
              </tr>
            </report-header>
            <report-footer>
              <tr>
                <td colspan="2"><%= attr(:state) %> total</td>
                <td>
                  <%= format("%.2f", sum(:value)) %>
                  (max: <%= format("%.2f", max(:value)) %>)
                </td>
              </tr>
            </report-footer>

            <report-row>
              <tr>
                <td><%= attr(:id) %></td>
                <td><%= attr(:name) %></td>
                <td class="text-end"><%= format("%.2f", attr(:value)) %></td>
              </tr>
            </report-row>
          </report-group>
        </report-page>

        <report-footer>
          <tr>
            <td colspan="2">Grand total</td>
            <td><%= format("%.2f", sum(:value)) %></td>
          </tr>
        </report-footer>
      </report>
    </table>
  </body>
</html>

To preview the structure, rename the file to .html and open it in a browser (the custom tags will be ignored).

Methods available in ERB

Method Description
attr(:field) Value of the field on the current row
sum(:field) / sum(:field, context) Sum within the current container, or in the given context (:page, :report)
avg(:field) / avg(:field, context) Average
min(:field) / max(:field) (and with context) Minimum / maximum
count(:field) / count(:field, context) Number of values
number Current page or group number
report The root Report container (useful to call report.sum(:field))

Rails helpers

You can make Rails view helpers available inside the ERB templates by calling Repolho.include_rails_helpers!. By default it includes ActionView::Helpers::NumberHelper (so you get number_to_currency, number_with_precision, etc.).

Inside a Rails application

# config/initializers/repolho.rb
Repolho.include_rails_helpers!

After that you can use the helpers normally in any template:

<td class="text-end"><%= number_to_currency(attr(:value)) %></td>
<td><%= number_with_precision(sum(:value), precision: 2) %></td>

You can also pass extra modules:

Repolho.include_rails_helpers!(
  ActionView::Helpers::NumberHelper,
  ActionView::Helpers::TextHelper
)

Outside Rails

You need the corresponding gems. Directly requiring the individual helper file triggers an autoload problem in recent Rails versions, so load the helpers framework first:

# Gemfile
gem "actionview"

require "action_view"
require "action_view/helpers"          # loads NumberHelper and the rest
Repolho.include_rails_helpers!

Alternatively (and often simpler), use ActiveSupport::NumberHelper, which is designed to be used outside of a view context. The method names are almost the same (number_to_currency, number_to_rounded, etc.):

# Gemfile
gem "activesupport"

require "active_support/number_helper"
Repolho.include_rails_helpers!(ActiveSupport::NumberHelper)

Note: some method names differ slightly between the two modules (number_with_delimiter vs number_to_delimited). Prefer the ActiveSupport version when you are not inside Rails.

Instance variables from the Rails view (such as @current_template) are not automatically copied. Pass them explicitly through the locals hash:

Repolho::Main.new(path, data, current_template: @current_template, user: current_user)

They become @current_template and @user inside the templates.

Testing

The project uses Minitest.

# From the gem root
bundle install
bundle exec rake test
# or
ruby -Ilib:test test/stats_test.rb

Tests cover:

  • Stats (sum, avg, min, max, count, reset, numeric coercion)
  • Container (ERB rendering of headers/footers/rows, attribute access, stats delegation, status)
  • Page (row counting via <tr>, page open/close, page breaks, raw append)
  • Report (inheritance and composition)
  • Main (data normalization for hashes / ActiveRecord-like objects / models, section extraction, full HTML generation, groupings, page breaks, pagination, print CSS for orientation/paper, previous/after ERB)

Integration tests assert on the generated HTML structure (tbody.repolho-page, data attributes, totals, labels).

Signing

This gem is cryptographically signed. To verify the signature on install:

# Trust the public certificate (once)
gem cert --add <(curl -Ls https://raw.githubusercontent.com/taq/repolho/main/certs/taq.pem)

# Install with signature verification
gem install repolho -P HighSecurity

License

MIT

About

Repolho generates HTML reports from .rep.erb templates with support for nested groupings, page breaks, headers/footers, and statistics (sum, avg, min, max). Works with ActiveRecord::Result, Relation, or arrays of hashes.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

0