├── .formatter.exs ├── .github └── workflows │ └── elixir.yml ├── .gitignore ├── LICENSE ├── README.md ├── assets └── example.png ├── examples ├── barcodes.html ├── custom_fonts_and_styles.html ├── hello.html ├── invoice.html ├── list_of_people.html ├── page_numbering.html └── two_page_table.html ├── lib ├── base_template │ ├── base_template.html.eex │ ├── normalize.css │ └── paper.css ├── rapport.ex └── rapport │ ├── barcode.ex │ ├── font.ex │ ├── image.ex │ ├── page.ex │ ├── page_numbering.ex │ └── report.ex ├── mix.exs ├── mix.lock └── test ├── barcode_test.exs ├── example_templates ├── barcode_page.html.eex ├── charts_page.html.eex ├── charts_report.html.eex ├── invoice_page.html.eex ├── invoice_report.html.eex ├── list_of_people_cover_page.html.eex ├── list_of_people_page.html.eex ├── list_of_people_report.html.eex ├── page_numbering_page.html.eex ├── table_page.html.eex └── table_report.html.eex ├── example_test.exs ├── font_test.exs ├── fonts ├── font.woff2 ├── no.font └── tangerine.woff2 ├── image_test.exs ├── images ├── acme.png ├── gif.gif ├── jpg.jpg ├── logo │ ├── editablefile.ai │ ├── horizontal.png │ ├── horizontal.svg │ ├── vertical.png │ └── vertical.svg ├── no.image ├── png.png └── top_secret_stamp.png ├── page_numbering_test.exs ├── page_test.exs ├── rapport_test.exs ├── templates ├── empty.html.eex ├── hello.html.eex ├── list.html.eex ├── list_map.html.eex └── two_fields.html.eex └── test_helper.exs /.formatter.exs: -------------------------------------------------------------------------------- 1 | [ 2 | inputs: ["mix.exs", "{config,lib,test}/**/*.{ex,exs}"] 3 | ] 4 | -------------------------------------------------------------------------------- /.github/workflows/elixir.yml: -------------------------------------------------------------------------------- 1 | name: Elixir CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | 12 | name: Build and test 13 | runs-on: ubuntu-20.04 14 | 15 | steps: 16 | - uses: actions/checkout@v3 17 | - name: Set up Elixir 18 | uses: erlef/setup-beam@988e02bfe678367a02564f65ca2e37726dc0268f 19 | with: 20 | elixir-version: '1.16.0' # Define the elixir version [required] 21 | otp-version: '26.2.1' # Define the OTP version [required] 22 | - name: Restore dependencies cache 23 | uses: actions/cache@v3 24 | with: 25 | path: deps 26 | key: ${{ runner.os }}-mix-${{ hashFiles('**/mix.lock') }} 27 | restore-keys: ${{ runner.os }}-mix- 28 | - name: Install dependencies 29 | run: mix deps.get 30 | - name: Run tests 31 | run: mix test -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where 3rd-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | TODO.txt 22 | *.tar 23 | 24 | # Visual Studio code stuff 25 | /.vscode/ 26 | 27 | /.elixir_ls/ 28 | .DS_Store 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018 Richard Nyström 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

rapport

2 | 3 | Rapport aims to provide a robust set of modules to generate HTML reports that both looks good in the browser and when being printed. 4 | 5 | [![Elixir CI](https://github.com/ricn/rapport/actions/workflows/elixir.yml/badge.svg)](https://github.com/ricn/rapport/actions/workflows/elixir.yml) 6 | [![Hex.pm](https://img.shields.io/hexpm/v/rapport.svg)](https://hex.pm/packages/rapport) 7 | [![Coverage Status](https://coveralls.io/repos/github/ricn/rapport/badge.svg?branch=master)](https://coveralls.io/github/ricn/rapport?branch=master) 8 | 9 | ## Installation 10 | 11 | The package can be installed 12 | by adding `rapport` to your list of dependencies in `mix.exs`: 13 | 14 | ```elixir 15 | def deps do 16 | [ 17 | {:rapport, "~> 0.7"} 18 | ] 19 | end 20 | ``` 21 | 22 | ## Notable features 23 | * Specify paper size for the report 24 | * Specify rotation for the report 25 | * Image helpers 26 | * Font helpers 27 | * Page numbering 28 | * Add custom styling & even Javascript 29 | * Barcodes 30 | 31 | ## Hello world 32 | 33 | ```elixir 34 | page_template = "

<%= @hello %>

" 35 | html_report = 36 | Rapport.new 37 | |> Rapport.add_page(page_template, %{hello: "Hello world!"}) 38 | |> Rapport.save_to_file("/home/users/ricn/hello.html") 39 | ``` 40 | 41 | The snippet above generates a report containing only one page with a heading that says "Hello world!". 42 | 43 | [See example here](https://rawgit.com/ricn/rapport/master/examples/hello.html) 44 | 45 | ## More examples 46 | * [Custom fonts and styling](https://rawgit.com/ricn/rapport/master/examples/custom_fonts_and_styles.html) 47 | * [Invoice](https://rawgit.com/ricn/rapport/master/examples/invoice.html) 48 | * [Page numbering](https://rawgit.com/ricn/rapport/master/examples/page_numbering.html) 49 | * [List of people with cover page](https://rawgit.com/ricn/rapport/master/examples/list_of_people.html) 50 | * [Barcodes](https://rawgit.com/ricn/rapport/master/examples/barcodes.html) 51 | * More examples are coming... 52 | 53 | If you want to see how the examples has been created, you can look at the `example_test.exs` file in the test folder. 54 | 55 | ## Phoenix integration 56 | 57 | It's easy to Integrate Rapport with Phoenix. Just load the template as a module attribute, create the HTML for the report 58 | and send a response with the generated HTML: 59 | 60 | ```elixir 61 | defmodule ReportsWeb.ReportController do 62 | use ReportsWeb, :controller 63 | 64 | @page_template File.read!(Path.join(__DIR__, "../templates/report/hello.html.eex")) 65 | 66 | def hello(conn, _params) do 67 | html_report = 68 | Rapport.new 69 | |> Rapport.add_page(@page_template, %{hello: "Hello World!"}) 70 | |> Rapport.generate_html 71 | 72 | conn 73 | |> put_resp_content_type("text/html") 74 | |> send_resp(200, html_report) 75 | end 76 | end 77 | ``` 78 | 79 | ## Upcoming features 80 | * Charts 81 | * PDF conversion 82 | 83 | ## Credits 84 | 85 | The following people have contributed ideas, documentation, or code to Rapport: 86 | 87 | * Richard Nyström 88 | 89 | ## Contributing 90 | 91 | 1. Fork it 92 | 2. Create your feature branch (`git checkout -b my-new-feature`) 93 | 3. Commit your changes (`git commit -am 'Add some feature'`) 94 | 4. Push to the branch (`git push origin my-new-feature`) 95 | 5. Create new Pull Request 96 | -------------------------------------------------------------------------------- /assets/example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ricn/rapport/b5ebb91f3c41800de66f6a5e121d0f8c4637e747/assets/example.png -------------------------------------------------------------------------------- /examples/barcodes.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Report 6 | 8 | 101 | 102 | 103 | 104 | 105 |
106 |
107 | 108 |
109 |
110 | 111 |
112 |
113 | 114 |
115 |
116 | 117 |
118 | 119 |
120 | 121 |
122 | 123 |
124 | 125 | 126 | 127 | -------------------------------------------------------------------------------- /examples/custom_fonts_and_styles.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Report 6 | 8 | 101 | 102 | 117 | 118 | 119 | 120 |
121 |

Hello world!

122 |
123 | 124 | 125 | 126 | -------------------------------------------------------------------------------- /examples/hello.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Report 6 | 8 | 101 | 102 | 103 | 104 | 105 |
106 |

Hello world!

107 |
108 | 109 | 110 | 111 | -------------------------------------------------------------------------------- /examples/page_numbering.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Report 6 | 8 | 101 | 102 | 103 | 104 | 105 |
106 |

Random text

107 |

Quisquam magni sed voluptate eligendi optio numquam nulla sequi corporis! Saepe voluptatem dolore nesciunt. Id laudantium qui aut eaque fuga quia eum iure. Pariatur natus vel sint blanditiis!Ut rerum dolore numquam autem animi iure adipisci ratione qui. Quaerat harum ea quis qui non reiciendis vero? Consequatur qui molestias voluptas. Blanditiis voluptas non vel cumque illo corporis perferendis dolor! Iste aut impedit illo distinctio.Et id impedit voluptatem error. Numquam cupiditate aut dolores aperiam commodi quas porro impedit. Debitis ea libero voluptas molestiae sunt. In impedit ex iste. Repellat et amet ipsum reiciendis reprehenderit rem a iste.Nihil repellendus perferendis laborum soluta ut sed repellendus enim voluptatibus. Sit accusantium ad atque. Explicabo ex et deserunt culpa est accusantium. Est ut exercitationem quo aut debitis! Earum quaerat aut quasi omnis et est.Vel nesciunt vero suscipit unde accusamus est sed. Aut voluptas impedit sed laborum a dolore quasi eveniet eos. Nobis quasi quaerat ab et et nam optio repellendus.Ab non unde asperiores molestias aut. Nihil qui ut ut harum neque quisquam. Nisi temporibus aut consectetur quos praesentium. Officia dolorem debitis distinctio consequuntur. Minus consectetur quaerat at qui molestiae ut modi facere.Et qui modi dolor molestiae ducimus incidunt ut et aut. Illo omnis hic ut qui nihil molestias! Molestias ipsum a dolore natus nostrum. Voluptas itaque vel voluptate. Animi dolorem quis exercitationem praesentium.Vitae tempore et totam nulla et assumenda nemo! Non repellendus provident nemo accusamus? Eius error laboriosam recusandae corrupti perspiciatis et iusto corrupti. Non voluptas debitis reprehenderit.Autem quis accusantium sapiente ut eligendi beatae deleniti cum earum. Ut repudiandae sint porro delectus libero laboriosam autem deserunt voluptatem. Tempora quibusdam quis dignissimos sed et earum aliquam rerum. Et unde et cum perferendis commodi sint et.Porro ut dolores provident. Nihil odio tempora enim est aut ducimus.Dolorum quo veniam doloribus optio. Possimus tenetur qui eius aut est alias reiciendis. Ut qui dignissimos quibusdam vel dolorem? Reprehenderit nisi omnis sunt sit aliquam officiis facilis officia in. Aut tempore laboriosam cupiditate amet velit ex accusantium minus laborum.Aliquid nulla temporibus ut quo aut possimus ex? Unde facilis iusto ut quo itaque adipisci doloribus! Laboriosam maxime facere distinctio et praesentium ex. Molestias vitae est et ut reprehenderit eveniet sint. Ut quo quod nobis saepe ea.Dignissimos possimus suscipit dolorum cupiditate nobis non et consequatur. Facilis doloribus molestiae praesentium eveniet suscipit. Voluptas non aspernatur voluptate corrupti minima necessitatibus. Non corrupti labore vel impedit! Facere et impedit praesentium praesentium est qui enim.Natus consequatur aut consequatur est reiciendis ducimus veritatis et optio! Laboriosam error repellat amet laboriosam voluptate? Et delectus eos itaque quia doloremque magni. Voluptas et id aliquam non esse ipsa et laborum eos.Repellat sed veritatis odio. Reiciendis tempora ducimus saepe porro voluptatem modi non!Maiores veritatis autem necessitatibus. Ut incidunt est recusandae quibusdam veritatis aliquam?Natus quasi asperiores facilis officia et ipsum necessitatibus quidem. Non ut atque molestiae repellat commodi omnis ratione eius. Veritatis in perspiciatis molestiae numquam repellat. Consequatur non ipsam eos iste nesciunt culpa voluptatem ex.Dolorem nesciunt dignissimos ipsum dolor totam perferendis! Et vitae sed nulla repellat nihil? Magni et non accusamus earum nemo et sint voluptatum.Accusamus sunt dolor totam similique deserunt? Repellendus laborum a temporibus molestiae quia unde cum dolorem! Facere sequi ipsa non amet qui quia ad. Consequatur id aliquid sit est ex necessitatibus voluptatem! Sit est fuga et aut natus rerum et earum nesciunt!Fuga voluptatibus porro dignissimos vel rerum. Libero quaerat reprehenderit soluta omnis quia quia et dolores repudiandae. Enim molestiae dicta soluta totam ab. Quis aut eius dolor rem.Quaerat maxime voluptate molestiae? Eum sapiente corrupti ea temporibus et voluptatem nisi earum voluptates! Illum omnis fugit eveniet dolorem ipsum illum provident at! Explicabo maxime et et ut perspiciatis dolores veritatis id est.Incidunt odio magnam minus ad! Distinctio impedit sed neque quia explicabo perspiciatis.Porro molestiae ipsum ab ducimus expedita. Sed velit non hic aspernatur dolorum ipsa dicta natus ipsa. Unde minus quaerat vitae quaerat sit temporibus porro omnis. Culpa ut officiis laborum neque eum tempore? Omnis ratione dolorem minima.

108 | 109 | 1 of 4 110 |
111 |
112 |

Random text

113 |

Quisquam magni sed voluptate eligendi optio numquam nulla sequi corporis! Saepe voluptatem dolore nesciunt. Id laudantium qui aut eaque fuga quia eum iure. Pariatur natus vel sint blanditiis!Ut rerum dolore numquam autem animi iure adipisci ratione qui. Quaerat harum ea quis qui non reiciendis vero? Consequatur qui molestias voluptas. Blanditiis voluptas non vel cumque illo corporis perferendis dolor! Iste aut impedit illo distinctio.Et id impedit voluptatem error. Numquam cupiditate aut dolores aperiam commodi quas porro impedit. Debitis ea libero voluptas molestiae sunt. In impedit ex iste. Repellat et amet ipsum reiciendis reprehenderit rem a iste.Nihil repellendus perferendis laborum soluta ut sed repellendus enim voluptatibus. Sit accusantium ad atque. Explicabo ex et deserunt culpa est accusantium. Est ut exercitationem quo aut debitis! Earum quaerat aut quasi omnis et est.Vel nesciunt vero suscipit unde accusamus est sed. Aut voluptas impedit sed laborum a dolore quasi eveniet eos. Nobis quasi quaerat ab et et nam optio repellendus.Ab non unde asperiores molestias aut. Nihil qui ut ut harum neque quisquam. Nisi temporibus aut consectetur quos praesentium. Officia dolorem debitis distinctio consequuntur. Minus consectetur quaerat at qui molestiae ut modi facere.Et qui modi dolor molestiae ducimus incidunt ut et aut. Illo omnis hic ut qui nihil molestias! Molestias ipsum a dolore natus nostrum. Voluptas itaque vel voluptate. Animi dolorem quis exercitationem praesentium.Vitae tempore et totam nulla et assumenda nemo! Non repellendus provident nemo accusamus? Eius error laboriosam recusandae corrupti perspiciatis et iusto corrupti. Non voluptas debitis reprehenderit.Autem quis accusantium sapiente ut eligendi beatae deleniti cum earum. Ut repudiandae sint porro delectus libero laboriosam autem deserunt voluptatem. Tempora quibusdam quis dignissimos sed et earum aliquam rerum. Et unde et cum perferendis commodi sint et.Porro ut dolores provident. Nihil odio tempora enim est aut ducimus.Dolorum quo veniam doloribus optio. Possimus tenetur qui eius aut est alias reiciendis. Ut qui dignissimos quibusdam vel dolorem? Reprehenderit nisi omnis sunt sit aliquam officiis facilis officia in. Aut tempore laboriosam cupiditate amet velit ex accusantium minus laborum.Aliquid nulla temporibus ut quo aut possimus ex? Unde facilis iusto ut quo itaque adipisci doloribus! Laboriosam maxime facere distinctio et praesentium ex. Molestias vitae est et ut reprehenderit eveniet sint. Ut quo quod nobis saepe ea.Dignissimos possimus suscipit dolorum cupiditate nobis non et consequatur. Facilis doloribus molestiae praesentium eveniet suscipit. Voluptas non aspernatur voluptate corrupti minima necessitatibus. Non corrupti labore vel impedit! Facere et impedit praesentium praesentium est qui enim.Natus consequatur aut consequatur est reiciendis ducimus veritatis et optio! Laboriosam error repellat amet laboriosam voluptate? Et delectus eos itaque quia doloremque magni. Voluptas et id aliquam non esse ipsa et laborum eos.Repellat sed veritatis odio. Reiciendis tempora ducimus saepe porro voluptatem modi non!Maiores veritatis autem necessitatibus. Ut incidunt est recusandae quibusdam veritatis aliquam?Natus quasi asperiores facilis officia et ipsum necessitatibus quidem. Non ut atque molestiae repellat commodi omnis ratione eius. Veritatis in perspiciatis molestiae numquam repellat. Consequatur non ipsam eos iste nesciunt culpa voluptatem ex.Dolorem nesciunt dignissimos ipsum dolor totam perferendis! Et vitae sed nulla repellat nihil? Magni et non accusamus earum nemo et sint voluptatum.Accusamus sunt dolor totam similique deserunt? Repellendus laborum a temporibus molestiae quia unde cum dolorem! Facere sequi ipsa non amet qui quia ad. Consequatur id aliquid sit est ex necessitatibus voluptatem! Sit est fuga et aut natus rerum et earum nesciunt!Fuga voluptatibus porro dignissimos vel rerum. Libero quaerat reprehenderit soluta omnis quia quia et dolores repudiandae. Enim molestiae dicta soluta totam ab. Quis aut eius dolor rem.Quaerat maxime voluptate molestiae? Eum sapiente corrupti ea temporibus et voluptatem nisi earum voluptates! Illum omnis fugit eveniet dolorem ipsum illum provident at! Explicabo maxime et et ut perspiciatis dolores veritatis id est.Incidunt odio magnam minus ad! Distinctio impedit sed neque quia explicabo perspiciatis.Porro molestiae ipsum ab ducimus expedita. Sed velit non hic aspernatur dolorum ipsa dicta natus ipsa. Unde minus quaerat vitae quaerat sit temporibus porro omnis. Culpa ut officiis laborum neque eum tempore? Omnis ratione dolorem minima.

114 | 115 | 2 of 4 116 |
117 |
118 |

Random text

119 |

Quisquam magni sed voluptate eligendi optio numquam nulla sequi corporis! Saepe voluptatem dolore nesciunt. Id laudantium qui aut eaque fuga quia eum iure. Pariatur natus vel sint blanditiis!Ut rerum dolore numquam autem animi iure adipisci ratione qui. Quaerat harum ea quis qui non reiciendis vero? Consequatur qui molestias voluptas. Blanditiis voluptas non vel cumque illo corporis perferendis dolor! Iste aut impedit illo distinctio.Et id impedit voluptatem error. Numquam cupiditate aut dolores aperiam commodi quas porro impedit. Debitis ea libero voluptas molestiae sunt. In impedit ex iste. Repellat et amet ipsum reiciendis reprehenderit rem a iste.Nihil repellendus perferendis laborum soluta ut sed repellendus enim voluptatibus. Sit accusantium ad atque. Explicabo ex et deserunt culpa est accusantium. Est ut exercitationem quo aut debitis! Earum quaerat aut quasi omnis et est.Vel nesciunt vero suscipit unde accusamus est sed. Aut voluptas impedit sed laborum a dolore quasi eveniet eos. Nobis quasi quaerat ab et et nam optio repellendus.Ab non unde asperiores molestias aut. Nihil qui ut ut harum neque quisquam. Nisi temporibus aut consectetur quos praesentium. Officia dolorem debitis distinctio consequuntur. Minus consectetur quaerat at qui molestiae ut modi facere.Et qui modi dolor molestiae ducimus incidunt ut et aut. Illo omnis hic ut qui nihil molestias! Molestias ipsum a dolore natus nostrum. Voluptas itaque vel voluptate. Animi dolorem quis exercitationem praesentium.Vitae tempore et totam nulla et assumenda nemo! Non repellendus provident nemo accusamus? Eius error laboriosam recusandae corrupti perspiciatis et iusto corrupti. Non voluptas debitis reprehenderit.Autem quis accusantium sapiente ut eligendi beatae deleniti cum earum. Ut repudiandae sint porro delectus libero laboriosam autem deserunt voluptatem. Tempora quibusdam quis dignissimos sed et earum aliquam rerum. Et unde et cum perferendis commodi sint et.Porro ut dolores provident. Nihil odio tempora enim est aut ducimus.Dolorum quo veniam doloribus optio. Possimus tenetur qui eius aut est alias reiciendis. Ut qui dignissimos quibusdam vel dolorem? Reprehenderit nisi omnis sunt sit aliquam officiis facilis officia in. Aut tempore laboriosam cupiditate amet velit ex accusantium minus laborum.Aliquid nulla temporibus ut quo aut possimus ex? Unde facilis iusto ut quo itaque adipisci doloribus! Laboriosam maxime facere distinctio et praesentium ex. Molestias vitae est et ut reprehenderit eveniet sint. Ut quo quod nobis saepe ea.Dignissimos possimus suscipit dolorum cupiditate nobis non et consequatur. Facilis doloribus molestiae praesentium eveniet suscipit. Voluptas non aspernatur voluptate corrupti minima necessitatibus. Non corrupti labore vel impedit! Facere et impedit praesentium praesentium est qui enim.Natus consequatur aut consequatur est reiciendis ducimus veritatis et optio! Laboriosam error repellat amet laboriosam voluptate? Et delectus eos itaque quia doloremque magni. Voluptas et id aliquam non esse ipsa et laborum eos.Repellat sed veritatis odio. Reiciendis tempora ducimus saepe porro voluptatem modi non!Maiores veritatis autem necessitatibus. Ut incidunt est recusandae quibusdam veritatis aliquam?Natus quasi asperiores facilis officia et ipsum necessitatibus quidem. Non ut atque molestiae repellat commodi omnis ratione eius. Veritatis in perspiciatis molestiae numquam repellat. Consequatur non ipsam eos iste nesciunt culpa voluptatem ex.Dolorem nesciunt dignissimos ipsum dolor totam perferendis! Et vitae sed nulla repellat nihil? Magni et non accusamus earum nemo et sint voluptatum.Accusamus sunt dolor totam similique deserunt? Repellendus laborum a temporibus molestiae quia unde cum dolorem! Facere sequi ipsa non amet qui quia ad. Consequatur id aliquid sit est ex necessitatibus voluptatem! Sit est fuga et aut natus rerum et earum nesciunt!Fuga voluptatibus porro dignissimos vel rerum. Libero quaerat reprehenderit soluta omnis quia quia et dolores repudiandae. Enim molestiae dicta soluta totam ab. Quis aut eius dolor rem.Quaerat maxime voluptate molestiae? Eum sapiente corrupti ea temporibus et voluptatem nisi earum voluptates! Illum omnis fugit eveniet dolorem ipsum illum provident at! Explicabo maxime et et ut perspiciatis dolores veritatis id est.Incidunt odio magnam minus ad! Distinctio impedit sed neque quia explicabo perspiciatis.Porro molestiae ipsum ab ducimus expedita. Sed velit non hic aspernatur dolorum ipsa dicta natus ipsa. Unde minus quaerat vitae quaerat sit temporibus porro omnis. Culpa ut officiis laborum neque eum tempore? Omnis ratione dolorem minima.

120 | 121 | 3 of 4 122 |
123 |
124 |

Random text

125 |

Quisquam magni sed voluptate eligendi optio numquam nulla sequi corporis! Saepe voluptatem dolore nesciunt. Id laudantium qui aut eaque fuga quia eum iure. Pariatur natus vel sint blanditiis!Ut rerum dolore numquam autem animi iure adipisci ratione qui. Quaerat harum ea quis qui non reiciendis vero? Consequatur qui molestias voluptas. Blanditiis voluptas non vel cumque illo corporis perferendis dolor! Iste aut impedit illo distinctio.Et id impedit voluptatem error. Numquam cupiditate aut dolores aperiam commodi quas porro impedit. Debitis ea libero voluptas molestiae sunt. In impedit ex iste. Repellat et amet ipsum reiciendis reprehenderit rem a iste.Nihil repellendus perferendis laborum soluta ut sed repellendus enim voluptatibus. Sit accusantium ad atque. Explicabo ex et deserunt culpa est accusantium. Est ut exercitationem quo aut debitis! Earum quaerat aut quasi omnis et est.Vel nesciunt vero suscipit unde accusamus est sed. Aut voluptas impedit sed laborum a dolore quasi eveniet eos. Nobis quasi quaerat ab et et nam optio repellendus.Ab non unde asperiores molestias aut. Nihil qui ut ut harum neque quisquam. Nisi temporibus aut consectetur quos praesentium. Officia dolorem debitis distinctio consequuntur. Minus consectetur quaerat at qui molestiae ut modi facere.Et qui modi dolor molestiae ducimus incidunt ut et aut. Illo omnis hic ut qui nihil molestias! Molestias ipsum a dolore natus nostrum. Voluptas itaque vel voluptate. Animi dolorem quis exercitationem praesentium.Vitae tempore et totam nulla et assumenda nemo! Non repellendus provident nemo accusamus? Eius error laboriosam recusandae corrupti perspiciatis et iusto corrupti. Non voluptas debitis reprehenderit.Autem quis accusantium sapiente ut eligendi beatae deleniti cum earum. Ut repudiandae sint porro delectus libero laboriosam autem deserunt voluptatem. Tempora quibusdam quis dignissimos sed et earum aliquam rerum. Et unde et cum perferendis commodi sint et.Porro ut dolores provident. Nihil odio tempora enim est aut ducimus.Dolorum quo veniam doloribus optio. Possimus tenetur qui eius aut est alias reiciendis. Ut qui dignissimos quibusdam vel dolorem? Reprehenderit nisi omnis sunt sit aliquam officiis facilis officia in. Aut tempore laboriosam cupiditate amet velit ex accusantium minus laborum.Aliquid nulla temporibus ut quo aut possimus ex? Unde facilis iusto ut quo itaque adipisci doloribus! Laboriosam maxime facere distinctio et praesentium ex. Molestias vitae est et ut reprehenderit eveniet sint. Ut quo quod nobis saepe ea.Dignissimos possimus suscipit dolorum cupiditate nobis non et consequatur. Facilis doloribus molestiae praesentium eveniet suscipit. Voluptas non aspernatur voluptate corrupti minima necessitatibus. Non corrupti labore vel impedit! Facere et impedit praesentium praesentium est qui enim.Natus consequatur aut consequatur est reiciendis ducimus veritatis et optio! Laboriosam error repellat amet laboriosam voluptate? Et delectus eos itaque quia doloremque magni. Voluptas et id aliquam non esse ipsa et laborum eos.Repellat sed veritatis odio. Reiciendis tempora ducimus saepe porro voluptatem modi non!Maiores veritatis autem necessitatibus. Ut incidunt est recusandae quibusdam veritatis aliquam?Natus quasi asperiores facilis officia et ipsum necessitatibus quidem. Non ut atque molestiae repellat commodi omnis ratione eius. Veritatis in perspiciatis molestiae numquam repellat. Consequatur non ipsam eos iste nesciunt culpa voluptatem ex.Dolorem nesciunt dignissimos ipsum dolor totam perferendis! Et vitae sed nulla repellat nihil? Magni et non accusamus earum nemo et sint voluptatum.Accusamus sunt dolor totam similique deserunt? Repellendus laborum a temporibus molestiae quia unde cum dolorem! Facere sequi ipsa non amet qui quia ad. Consequatur id aliquid sit est ex necessitatibus voluptatem! Sit est fuga et aut natus rerum et earum nesciunt!Fuga voluptatibus porro dignissimos vel rerum. Libero quaerat reprehenderit soluta omnis quia quia et dolores repudiandae. Enim molestiae dicta soluta totam ab. Quis aut eius dolor rem.Quaerat maxime voluptate molestiae? Eum sapiente corrupti ea temporibus et voluptatem nisi earum voluptates! Illum omnis fugit eveniet dolorem ipsum illum provident at! Explicabo maxime et et ut perspiciatis dolores veritatis id est.Incidunt odio magnam minus ad! Distinctio impedit sed neque quia explicabo perspiciatis.Porro molestiae ipsum ab ducimus expedita. Sed velit non hic aspernatur dolorum ipsa dicta natus ipsa. Unde minus quaerat vitae quaerat sit temporibus porro omnis. Culpa ut officiis laborum neque eum tempore? Omnis ratione dolorem minima.

126 | 127 | 4 of 4 128 |
129 | 130 | 131 | 132 | -------------------------------------------------------------------------------- /examples/two_page_table.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Report 6 | 8 | 101 | 102 | 112 | 113 | 114 | 115 |
116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 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 | 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 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 |
#FirstnameLastnamePhoneE-mail
1JadaHermann847/431-3299leo_bartell@bayer.org
2EverardoStiedemann437-836-8403sheila1936@hammes.name
3ToreyKohler245.714.8354wilburn1900@murphy.org
4PearlineKiehn509-671-3767flo_frami@lesch.name
5StanSpencer555/736-6637dorian_gutmann@sanford.biz
6TiffanyKunze4398197284catalina_quitzon@hermann.info
7PercivalRunolfsdottir(448) 798-7202aurelio.klein@walsh.com
8JohannMurphy8164462011lonie_rogahn@emmerich.name
9AubreeGreen(532) 599-5851garrett_ritchie@pouros.name
10AlvahStark518/397-7693edna2037@block.biz
11ArielHartmann(924) 872-2193wilma2056@mckenzie.com
12KolbyHammes458.354.3007loren1998@windler.biz
13LelahGerhold220.263.1232queen1918@bednar.name
14PhyllisHansen863/613-5893rosella_douglas@luettgen.org
15LolaSchmeler618-222-8317wilma2053@mosciski.info
16LaurieBode204/504-0417carole1941@becker.com
17ElodyFriesen751.617.4716annamae_kohler@graham.biz
18SisterWilderman359-716-8037dorothy2017@sawayn.com
19PaoloPacocha6186439064sheila.carroll@sporer.net
20TobyHarber414/689-3827daron2067@doyle.name
21AlanYundt204-347-6175kathryn.davis@purdy.info
22SelinaSipes228/646-4058mateo1948@schneider.info
23JohnathonBeahan(939) 893-9753leda2066@roberts.name
24NapoleonOkuneva(480) 517-2138clementine_mitchell@keebler.info
25DenaOndricka(227) 633-9188darrel1962@swift.info
26SidLowe823/266-0305malika.moore@mclaughlin.name
27OsbaldoQuigley453/309-0956lucy_herman@klein.net
28NilsDietrich(711) 769-6944dusty2033@tillman.name
29TristianLehner476-642-4971jerry.thompson@kunze.name
30WilberRunte535/663-1037petra_mayert@halvorson.info
31MikeO'Conner8583277255gordon1972@raynor.net
32RyanSchneider817/766-2912margret.satterfield@lemke.org
33LeopoldoPadberg718.227.0701charity.auer@corkery.net
34JessBrakus987/738-1474talon2006@leuschke.net
35TravisBradtke(263) 528-3730karolann_mayer@abernathy.org
406 | 407 |
408 |
409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 419 | 420 | 421 | 422 | 423 | 424 | 425 | 426 | 427 | 428 | 429 | 430 | 431 | 432 | 433 | 434 | 435 | 436 | 437 | 438 | 439 | 440 | 441 | 442 | 443 | 444 | 445 | 446 | 447 | 448 | 449 | 450 | 451 | 452 | 453 | 454 | 455 | 456 | 457 | 458 | 459 | 460 | 461 | 462 | 463 | 464 | 465 | 466 | 467 | 468 | 469 | 470 | 471 | 472 | 473 | 474 | 475 | 476 | 477 | 478 | 479 | 480 | 481 | 482 | 483 | 484 | 485 | 486 | 487 | 488 | 489 | 490 | 491 | 492 | 493 | 494 | 495 | 496 | 497 | 498 | 499 | 500 | 501 | 502 | 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 | 531 | 532 | 533 | 534 | 535 | 536 | 537 | 538 | 539 | 540 | 541 | 542 | 543 | 544 | 545 | 546 | 547 | 548 | 549 | 550 | 551 | 552 | 553 | 554 | 555 | 556 | 557 | 558 | 559 | 560 | 561 | 562 | 563 | 564 | 565 | 566 | 567 | 568 | 569 | 570 | 571 | 572 | 573 | 574 | 575 | 576 | 577 | 578 | 579 | 580 | 581 | 582 | 583 | 584 | 585 | 586 | 587 | 588 | 589 | 590 | 591 | 592 | 593 | 594 | 595 | 596 | 597 | 598 | 599 | 600 | 601 | 602 | 603 | 604 | 605 | 606 | 607 | 608 | 609 | 610 | 611 | 612 | 613 | 614 | 615 | 616 | 617 | 618 |
#FirstnameLastnamePhoneE-mail
36CarletonMurazik267-787-2765jevon2015@grady.org
37AndyHeaney(921) 806-6027ervin2098@koch.com
38VelvaBradtke(308) 761-8310noah.beahan@volkman.org
39JamiePowlowski(357) 216-8978jaunita2069@hilll.org
40AdelaBeier310/681-8728isidro1909@zemlak.net
41RusselProsacco652/862-3649minnie2030@kub.info
42SamGutmann270/796-1805damian1991@heaney.name
43RustySpinka806-736-8175lauren2070@gorczany.name
44BetsyRenner(841) 259-4972wayne.huel@crist.biz
45PeytonFarrell533/708-9213jaylan2060@hamill.org
46AdrianaO'Conner548.259.1625stanford1910@beahan.info
47LazaroKuhn618/812-6462macey_welch@labadie.org
48OllieNolan(257) 521-3347alda.dooley@bauch.org
49EnochEmmerich6588678824brooks.prosacco@dooley.info
50EveretteSawayn4108546651kobe_wintheiser@kiehn.name
51CarletonStamm9264012591dayna1963@krajcik.biz
52CamrynBalistreri(322) 309-5799margot2017@dietrich.info
53LibbyCollier217.614.5254raphael2093@walter.com
54BrianaBergnaum843/977-9684bernard1985@gleichner.net
55ElliottHirthe(338) 554-9403bradly1965@mills.name
56WatsonRolfson949/692-9870jay_terry@oconnell.com
57RaymondRolfson574.917.8218rosamond2013@wehner.net
58AlexzanderJohns5047299531helga_bins@kling.info
59MossieHowe787-442-0828lula.mcdermott@wunsch.biz
60AlexanneHackett588/248-8723melody_treutel@bernhard.name
619 | 620 |
621 | 622 | 623 | 624 | -------------------------------------------------------------------------------- /lib/base_template/base_template.html.eex: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | <%= @title %> 6 | 7 | 8 | 9 | <%= @report_template %> 10 | 11 | 12 | <%= @pages %> 13 | 14 | 15 | -------------------------------------------------------------------------------- /lib/base_template/normalize.css: -------------------------------------------------------------------------------- 1 | /*! normalize.css v7.0.0 | MIT License | github.com/necolas/normalize.css */html{line-height:1.15;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,footer,header,nav,section{display:block}h1{font-size:2em;margin:.67em 0}figcaption,figure,main{display:block}figure{margin:1em 40px}hr{box-sizing:content-box;height:0;overflow:visible}pre{font-family:monospace,monospace;font-size:1em}a{background-color:transparent;-webkit-text-decoration-skip:objects}abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted}b,strong{font-weight:inherit}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace,monospace;font-size:1em}dfn{font-style:italic}mark{background-color:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}audio,video{display:inline-block}audio:not([controls]){display:none;height:0}img{border-style:none}svg:not(:root){overflow:hidden}button,input,optgroup,select,textarea{font-family:sans-serif;font-size:100%;line-height:1.15;margin:0}button,input{overflow:visible}button,select{text-transform:none}[type=reset],[type=submit],button,html [type=button]{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{border-style:none;padding:0}[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring,button:-moz-focusring{outline:1px dotted ButtonText}fieldset{padding:.35em .75em .625em}legend{box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}progress{display:inline-block;vertical-align:baseline}textarea{overflow:auto}[type=checkbox],[type=radio]{box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-cancel-button,[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}details,menu{display:block}summary{display:list-item}canvas{display:inline-block}template{display:none}[hidden]{display:none} 2 | -------------------------------------------------------------------------------- /lib/base_template/paper.css: -------------------------------------------------------------------------------- 1 | /*paper.css*/ 2 | 3 | @page { margin: 0 } 4 | body { margin: 0 } 5 | .sheet { 6 | margin: 0; 7 | overflow: hidden; 8 | position: relative; 9 | box-sizing: border-box; 10 | page-break-after: always; 11 | } 12 | 13 | /** Paper sizes **/ 14 | body.A3 .sheet { width: 297mm; height: 419mm } 15 | body.A3.landscape .sheet { width: 420mm; height: 296mm } 16 | body.A4 .sheet { width: 210mm; height: 296mm } 17 | body.A4.landscape .sheet { width: 297mm; height: 209mm } 18 | body.A5 .sheet { width: 148mm; height: 209mm } 19 | body.A5.landscape .sheet { width: 210mm; height: 147mm } 20 | 21 | body.half_letter .sheet { width: 140mm; height: 216mm } 22 | body.half_letter.landscape .sheet { width: 216mm; height: 140mm } 23 | body.letter .sheet { width: 216mm; height: 279mm } 24 | body.letter.landscape .sheet { width: 279mm; height: 216mm } 25 | body.legal .sheet { width: 216mm; height: 356mm } 26 | body.legal.landscape .sheet { width: 356mm; height: 216mm } 27 | body.junior_legal .sheet { width: 127mm; height: 203mm } 28 | body.junior_legal.landscape .sheet { width: 203mm; height: 127mm } 29 | body.ledger .sheet { width: 279mm; height: 432mm } 30 | body.ledger.landscape .sheet { width: 432mm; height: 279mm } 31 | 32 | /** Padding area **/ 33 | .sheet.padding-10mm { padding: 10mm } 34 | .sheet.padding-15mm { padding: 15mm } 35 | .sheet.padding-20mm { padding: 20mm } 36 | .sheet.padding-25mm { padding: 25mm } 37 | 38 | /** For screen preview **/ 39 | @media screen { 40 | body { background: #e0e0e0 } 41 | .sheet { 42 | background: white; 43 | box-shadow: 0 .5mm 2mm rgba(0,0,0,.3); 44 | margin-left: auto; 45 | margin-right: auto; 46 | margin-top: 5mm; 47 | margin-bottom: 5mm; 48 | } 49 | } 50 | 51 | /** Fix for Chrome issue #273306 **/ 52 | @media print { 53 | body.A3.landscape { width: 420mm } 54 | body.A3, body.A4.landscape { width: 297mm } 55 | body.A4, body.A5.landscape { width: 210mm } 56 | body.A5 { width: 148mm } 57 | 58 | body.half_letter { width: 140mm } 59 | body.half_letter.landscape { width: 216mm } 60 | body.letter { width: 216mm } 61 | body.letter.landscape { width: 279mm } 62 | body.legal { width: 216mm } 63 | body.legal.landscape { width: 356mm } 64 | body.junior_legal { width: 127mm } 65 | body.junior_legal.landscape { width: 203mm } 66 | body.ledger { width: 279mm } 67 | body.ledger.landscape { width: 432mm } 68 | } 69 | 70 | .page-numbering { 71 | position: absolute; 72 | } 73 | 74 | .bottom_left { 75 | bottom: 2em; 76 | left: 2em; 77 | } 78 | 79 | .bottom_right { 80 | bottom: 2em; 81 | right: 2em; 82 | } 83 | 84 | .top_right { 85 | top: 2em; 86 | right: 2em; 87 | } 88 | 89 | .top_left { 90 | top: 2em; 91 | left: 2em; 92 | } 93 | -------------------------------------------------------------------------------- /lib/rapport.ex: -------------------------------------------------------------------------------- 1 | defmodule Rapport do 2 | @moduledoc """ 3 | Rapport aims to provide a robust set of modules to generate 4 | HTML reports that both looks good in the browser and when being printed. 5 | """ 6 | 7 | alias Rapport.Report 8 | alias Rapport.Page 9 | alias Rapport.PageNumbering 10 | 11 | @normalize_css File.read!(Path.join(__DIR__, "base_template/normalize.css")) 12 | @paper_css File.read!(Path.join(__DIR__, "base_template/paper.css")) 13 | @base_template File.read!(Path.join(__DIR__, "base_template/base_template.html.eex")) 14 | 15 | @spec add_page(Report.t(), String.t(), map) :: Report.t() 16 | defdelegate add_page(report, page_template, fields), to: Page 17 | @spec add_page(Report.t(), Page.t()) :: Report.t() 18 | defdelegate add_page(report, page), to: Page 19 | @spec add_pages(Report.t(), list(Page.t())) :: Report.t() 20 | defdelegate add_pages(report, pages), to: Page 21 | 22 | @spec generate_pages([Page.t()], Report.padding()) :: String.t() 23 | @spec generate_pages([Page.t()], Report.padding(), Rapport.PageNumbering.t()) :: String.t() 24 | defdelegate generate_pages(pages, padding), to: Page 25 | defdelegate generate_pages(pages, padding, page_number_opts), to: Page 26 | 27 | @spec add_page_numbers(Report.t()) :: Report.t() 28 | @spec add_page_numbers(Report.t(), atom, any) :: Report.t() 29 | defdelegate add_page_numbers(report, page_number_position, formatter), to: PageNumbering 30 | @spec add_page_numbers(Report.t(), atom) :: Report.t() 31 | defdelegate add_page_numbers(report, page_number_position), to: PageNumbering 32 | defdelegate add_page_numbers(report), to: PageNumbering 33 | 34 | @spec new(String.t(), map()) :: Report.t() 35 | @doc """ 36 | Creates a new report. 37 | 38 | An optional EEx template can be passed to the `new` function. This template 39 | is meant to hold global things like styles, fonts etc that can be used on all 40 | pages thats added to the report. 41 | 42 | The `new` function sets the default paper size to `:A4`, the rotation 43 | to `:portrait`, the page padding to 10mm and the report title to "Report". 44 | Those defaults can easily be overridden by using `set_paper_size/2`, 45 | `set_rotation/2`, `set_padding/2` and `set_title/2`. 46 | 47 | Returns a `Rapport.Report` struct. 48 | 49 | ## Options 50 | 51 | * `template` - An optional EEx template for the report. 52 | * `fields` - A map with fields to assign to the EEx report template 53 | """ 54 | 55 | def new(template \\ "", fields \\ %{}) do 56 | %Report{ 57 | title: "Report", 58 | paper_size: :A4, 59 | rotation: :portrait, 60 | pages: [], 61 | template: template, 62 | padding: 10, 63 | fields: fields, 64 | page_number_opts: %PageNumbering{ 65 | add_page_numbers: false, 66 | page_number_position: :bottom_right, 67 | page_number_formatter: fn cnt_page, _tot_pages -> "#{cnt_page}" end 68 | } 69 | } 70 | end 71 | 72 | @spec set_title(Report.t(), String.t()) :: Report.t() 73 | @doc """ 74 | Sets the title for a report. This is the title of the generated html report. 75 | 76 | ## Options 77 | 78 | * `report` - The `Rapport.Report` you want to set the title for. 79 | * `title` - The new title 80 | """ 81 | 82 | def set_title(%Report{} = report, title) when is_binary(title) do 83 | Map.put(report, :title, title) 84 | end 85 | 86 | @spec set_paper_size( 87 | Report.t(), 88 | Report.paper_size() 89 | ) :: Report.t() 90 | @doc """ 91 | Sets the paper size for the report. 92 | 93 | It expects the paper size to be an atom and must be 94 | `:A4`, `:A3`, `:A5`, `:half_letter`, `:letter`, `:legal`, `:junior_legal` 95 | or `:ledger`, otherwise `ArgumentError` will be raised. 96 | 97 | ## Options 98 | 99 | * `report` - The `Rapport.Report` that you want set the paper size for 100 | * `paper_size` - The paper size. 101 | """ 102 | 103 | def set_paper_size(%Report{} = report, paper_size) do 104 | validate_list( 105 | paper_size, 106 | [:A4, :A3, :A5, :half_letter, :letter, :legal, :junior_legal, :ledger], 107 | "Invalid paper size" 108 | ) 109 | 110 | Map.put(report, :paper_size, paper_size) 111 | end 112 | 113 | @spec set_rotation(Report.t(), Report.rotation()) :: Report.t() 114 | @doc """ 115 | Sets the rotation for the report. 116 | 117 | It expects the rotation to an atom and must be `:portrait` or `:landscape`, 118 | otherwise `ArgumentError` will be raised. 119 | 120 | ## Options 121 | 122 | * `report` - The `Rapport.Report` that you want set the rotation for 123 | * `rotation` - The rotation. 124 | """ 125 | 126 | def set_rotation(%Report{} = report, rotation) do 127 | validate_list(rotation, [:portrait, :landscape], "Invalid rotation") 128 | Map.put(report, :rotation, rotation) 129 | end 130 | 131 | @spec set_padding(Report.t(), Report.padding()) :: Report.t() 132 | @doc """ 133 | Sets the padding (in millimeters) for the report. 134 | 135 | It expects the padding to be an integer and must be `10`, `15`, `20` or `25` mm, 136 | otherwise `ArgumentError` will be raised. 137 | 138 | ## Options 139 | 140 | * `report` - The `Rapport.Report` that you want set the padding for 141 | * `rotation` - The padding. 142 | """ 143 | 144 | def set_padding(%Report{} = report, padding) when is_integer(padding) do 145 | validate_list(padding, [10, 15, 20, 25], "Invalid padding") 146 | Map.put(report, :padding, padding) 147 | end 148 | 149 | @spec generate_html(Report.t()) :: String.t() 150 | @doc """ 151 | Generates HTML for the report. 152 | 153 | ## Options 154 | 155 | * `report` - The `Rapport.Report` that you want to generate to HTML. 156 | """ 157 | def generate_html(%Report{} = report) do 158 | paper_settings = paper_settings_css(report) 159 | add_page_numbers? = report.page_number_opts.add_page_numbers 160 | 161 | pages = 162 | case add_page_numbers? do 163 | true -> generate_pages(report.pages, report.padding, report.page_number_opts) 164 | false -> generate_pages(report.pages, report.padding) 165 | end 166 | 167 | report_template = EEx.eval_string(report.template, assigns: report.fields) 168 | 169 | assigns = [ 170 | title: report.title, 171 | paper_settings: paper_settings, 172 | normalize_css: @normalize_css, 173 | paper_css: @paper_css, 174 | pages: pages, 175 | report_template: report_template 176 | ] 177 | 178 | EEx.eval_string(@base_template, assigns: assigns) 179 | end 180 | 181 | @spec save_to_file(Report.t(), binary) :: :ok 182 | @doc """ 183 | Convenient function to save a report to file. 184 | 185 | ## Options 186 | * `report` - The `Rapport.Report` that you want to save to a HTML file 187 | * `file_path` - The path to the HTML file you want to save. 188 | """ 189 | def save_to_file(%Report{} = report, file_path) when is_binary(file_path) do 190 | html_report = generate_html(report) 191 | File.write!(file_path, html_report) 192 | end 193 | 194 | defp paper_settings_css(%Report{} = report) do 195 | paper_size = Atom.to_string(report.paper_size) 196 | rotation = Atom.to_string(report.rotation) 197 | if rotation == "portrait", do: paper_size, else: "#{paper_size} #{rotation}" 198 | end 199 | 200 | @spec validate_list(any, list(), String.t()) :: nil 201 | @doc false 202 | def validate_list(what, list, msg) do 203 | if what not in list, do: raise(ArgumentError, message: msg) 204 | end 205 | end 206 | -------------------------------------------------------------------------------- /lib/rapport/barcode.ex: -------------------------------------------------------------------------------- 1 | defmodule Rapport.Barcode do 2 | @spec create(:code128 | :code39 | :code93 | :itf | :ean13, String.t(), keyword) :: String.t() 3 | @doc """ 4 | Creates a barcode image (PNG) with the given text 5 | 6 | It expects the barcode type to be an :atom and it must be `:code39`, `:code93`, `code128` or `:itf`, 7 | otherwise `ArgumentError` will be raised. 8 | 9 | ## Options 10 | 11 | * `barcode_type` - The barcode type to use 12 | * `text` - The text to use to generate the barcode 13 | """ 14 | def create(barcode_type, text, opts \\ []) when is_binary(text) do 15 | case barcode_type do 16 | :code39 -> 17 | create_barcode(text, opts, Barlix.Code39) 18 | 19 | :code93 -> 20 | create_barcode(text, opts, Barlix.Code93) 21 | 22 | :code128 -> 23 | create_barcode(text, opts, Barlix.Code128) 24 | 25 | :itf -> 26 | create_barcode(text, opts, Barlix.ITF) 27 | 28 | :ean13 -> 29 | create_barcode(text, opts, Barlix.EAN13) 30 | 31 | _ -> 32 | raise ArgumentError, message: "Invalid barcode type" 33 | end 34 | end 35 | 36 | defp create_barcode(text, opts, barlix_module) do 37 | {:ok, png} = barlix_module.encode!(text) |> Barlix.PNG.print(opts) 38 | IO.iodata_to_binary(png) 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /lib/rapport/font.ex: -------------------------------------------------------------------------------- 1 | defmodule Rapport.Font do 2 | @spec as_data(String.t()) :: String.t() 3 | @doc """ 4 | Converts a font file (woff2) to a base64 encoded string that can be embedded: 5 | 6 | @font-face { 7 | src: url(data:font/woff2;charset=utf-8;base64,d09GRgA...kAAA==) format('woff') 8 | } 9 | 10 | Only woff2 format is supported at the moment 11 | 12 | ## Options 13 | 14 | * `font` - Font binary 15 | """ 16 | def as_data(font) do 17 | mime_type = detect_mime_type(font) 18 | encoded_font = Base.encode64(font) 19 | "data:#{mime_type};base64,#{encoded_font}" 20 | end 21 | 22 | defp detect_mime_type("wOF2" <> _), do: "font/woff2" 23 | defp detect_mime_type(_), do: raise(ArgumentError, message: "Invalid font") 24 | end 25 | -------------------------------------------------------------------------------- /lib/rapport/image.ex: -------------------------------------------------------------------------------- 1 | defmodule Rapport.Image do 2 | @spec as_data(String.t()) :: String.t() 3 | @doc """ 4 | Converts an image to a base64 encoded string that can be embedded in a image tag: 5 | 6 | Embedded Image 7 | 8 | Supported image types are JPEG, PNG and GIF. 9 | ## Options 10 | 11 | * `image` - Image binary 12 | """ 13 | def as_data(image) do 14 | mime_type = detect_mime_type(image) 15 | encoded_image = Base.encode64(image) 16 | "data:#{mime_type};base64,#{encoded_image}" 17 | end 18 | 19 | defp detect_mime_type(<<0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A>> <> _), do: "image/png" 20 | defp detect_mime_type(<<0xFF, 0xD8, 0xFF>> <> _), do: "image/jpeg" 21 | defp detect_mime_type("GIF87a" <> _), do: "image/gif" 22 | defp detect_mime_type("GIF89a" <> _), do: "image/gif" 23 | defp detect_mime_type(_), do: raise(ArgumentError, message: "Invalid image") 24 | end 25 | -------------------------------------------------------------------------------- /lib/rapport/page.ex: -------------------------------------------------------------------------------- 1 | defmodule Rapport.Page do 2 | defstruct template: nil, fields: nil 3 | @type t :: %Rapport.Page{template: String.t(), fields: map()} 4 | 5 | alias Rapport.Report 6 | alias Rapport.Page 7 | alias Rapport.PageNumbering 8 | 9 | @spec add_page(Rapport.Report.t(), String.t(), map) :: Rapport.Report.t() 10 | @doc """ 11 | Adds a new page to a report. 12 | 13 | ## Options 14 | 15 | * `report` - A `Rapport.Report` struct that you want to add the page to. 16 | * `page_template` - An EEx template for the page 17 | * `fields` - A map with fields that must be assigned to the EEx template 18 | """ 19 | 20 | def add_page(%Report{} = report, page_template, %{} = fields) when is_binary(page_template) do 21 | new_page = %Page{template: page_template, fields: fields} 22 | Map.put(report, :pages, [new_page | report.pages]) 23 | end 24 | 25 | @spec add_page(Rapport.Report.t(), Rapport.Page.t()) :: Rapport.Report.t() 26 | @doc """ 27 | Adds a new page to a report. 28 | 29 | ## Options 30 | 31 | * `report` - A `Rapport.Report` struct that you want to add the page to. 32 | * `page` - A `Rapport.Page` struct 33 | """ 34 | def add_page(%Report{} = report, %Page{} = page) do 35 | Map.put(report, :pages, [page | report.pages]) 36 | end 37 | 38 | @spec add_pages(Rapport.Report.t(), list(Rapport.Page.t())) :: Rapport.Report.t() 39 | @doc """ 40 | Adds a list of pages to a report. 41 | 42 | ## Options 43 | * `report` - A `Rapport.Report` struct that you want to add the page to. 44 | * `pages` - A list with `Rapport.Page` structs 45 | """ 46 | def add_pages(%Report{} = report, pages) when is_list(pages) do 47 | Map.put(report, :pages, pages ++ report.pages) 48 | end 49 | 50 | @spec generate_pages(list(Page.t()), Report.padding()) :: binary 51 | @doc false 52 | def generate_pages(pages, padding) when is_list(pages) do 53 | Enum.reverse(pages) 54 | |> Enum.map_join(fn page -> generate_page(page, padding) end) 55 | end 56 | 57 | @spec generate_pages(list(Page.t()), Report.padding(), PageNumbering.t()) :: String.t() 58 | @doc false 59 | def generate_pages(pages, padding, page_number_opts) when is_list(pages) do 60 | total_pages = Enum.count(pages) 61 | 62 | Enum.reverse(pages) 63 | |> Enum.with_index() 64 | |> Enum.map_join(fn {page, index} -> 65 | generate_page(page, padding, index + 1, total_pages, page_number_opts) 66 | end) 67 | end 68 | 69 | defp generate_page(p, padding) do 70 | EEx.eval_string(wrap_page(p.template, padding), assigns: p.fields) 71 | end 72 | 73 | defp generate_page(p, padding, page_number, total_pages, page_number_opts) do 74 | EEx.eval_string( 75 | wrap_page(p.template, padding, page_number, total_pages, page_number_opts), 76 | assigns: p.fields 77 | ) 78 | end 79 | 80 | defp wrap_page(template, padding) do 81 | padding_css = "padding-" <> Integer.to_string(padding) <> "mm" 82 | 83 | """ 84 |
85 | #{template} 86 |
87 | """ 88 | end 89 | 90 | defp wrap_page(template, padding, page_number, total_pages, page_number_opts) do 91 | padding_css = "padding-" <> Integer.to_string(padding) <> "mm" 92 | position = Atom.to_string(page_number_opts.page_number_position) 93 | formatter = page_number_opts.page_number_formatter 94 | 95 | page_number_tag = 96 | "#{formatter.(page_number, total_pages)}" 97 | 98 | """ 99 |
100 | #{template} 101 | #{page_number_tag} 102 |
103 | """ 104 | end 105 | end 106 | -------------------------------------------------------------------------------- /lib/rapport/page_numbering.ex: -------------------------------------------------------------------------------- 1 | defmodule Rapport.PageNumbering do 2 | defstruct add_page_numbers: nil, page_number_position: nil, page_number_formatter: nil 3 | 4 | @type t :: %Rapport.PageNumbering{ 5 | add_page_numbers: boolean(), 6 | page_number_position: :bottom_right | :bottom_left | :top_right | :top_left, 7 | page_number_formatter: function() 8 | } 9 | 10 | alias Rapport.Report 11 | 12 | @spec add_page_numbers( 13 | Rapport.Report.t(), 14 | :bottom_right | :bottom_left | :top_right | :top_left, 15 | function() 16 | ) :: Rapport.Report.t() 17 | @doc """ 18 | Adds page numbers to the pages 19 | 20 | It expects the page position to be an atom and must be `:bottom_right`, `:bottom_left`, `:top_right` or `:top_left`, 21 | otherwise `ArgumentError` will be raised. 22 | 23 | ## Options 24 | 25 | * `report` - The `Rapport.Report` that you want set the padding for 26 | * `page_number_position` - Where the page number will be positioned. 27 | """ 28 | def add_page_numbers( 29 | %Report{} = report, 30 | page_number_position \\ :bottom_right, 31 | formatter \\ fn cnt_page, _ -> "#{cnt_page}" end 32 | ) 33 | when is_atom(page_number_position) do 34 | Rapport.validate_list( 35 | page_number_position, 36 | [:bottom_right, :bottom_left, :top_right, :top_left], 37 | "Invalid page number position" 38 | ) 39 | 40 | opts = 41 | report.page_number_opts 42 | |> Map.put(:add_page_numbers, true) 43 | |> Map.put(:page_number_position, page_number_position) 44 | |> Map.put(:page_number_formatter, formatter) 45 | 46 | Map.put(report, :page_number_opts, opts) 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /lib/rapport/report.ex: -------------------------------------------------------------------------------- 1 | defmodule Rapport.Report do 2 | defstruct paper_size: nil, 3 | rotation: nil, 4 | title: nil, 5 | pages: nil, 6 | template: nil, 7 | padding: nil, 8 | fields: nil, 9 | page_number_opts: nil 10 | 11 | @type paper_size :: :A4 | :A3 | :A5 | :half_letter | :letter | :legal | :junior_legal | :ledger 12 | @type padding :: 10 | 15 | 20 | 25 13 | @type rotation :: :portrait | :landscape 14 | 15 | @type t :: %Rapport.Report{ 16 | paper_size: paper_size(), 17 | rotation: rotation(), 18 | title: String.t(), 19 | pages: list(Rapport.Page), 20 | template: String.t(), 21 | padding: padding(), 22 | fields: map(), 23 | page_number_opts: %Rapport.PageNumbering{} 24 | } 25 | end 26 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Rapport.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :rapport, 7 | version: "0.7.2", 8 | elixir: "~> 1.10", 9 | start_permanent: Mix.env() == :prod, 10 | deps: deps(), 11 | description: desc(), 12 | docs: docs(), 13 | package: package(), 14 | test_coverage: [tool: ExCoveralls], 15 | preferred_cli_env: [ 16 | coveralls: :test, 17 | "coveralls.detail": :test, 18 | "coveralls.post": :test, 19 | "coveralls.html": :test 20 | ] 21 | ] 22 | end 23 | 24 | # Run "mix help compile.app" to learn about applications. 25 | def application do 26 | [ 27 | extra_applications: [:logger] 28 | ] 29 | end 30 | 31 | defp docs do 32 | [extras: ["README.md"], main: "readme"] 33 | end 34 | 35 | # Run "mix help deps" to learn about dependencies. 36 | defp deps do 37 | [ 38 | {:excoveralls, "0.18.1", only: [:dev, :test]}, 39 | {:ex_doc, "0.32.1", only: :dev}, 40 | {:inch_ex, "2.0.0", only: :docs}, 41 | {:faker, "0.18.0", only: :test}, 42 | {:doctor, "0.21.0", only: :dev}, 43 | {:dialyxir, "1.4.3", only: [:dev], runtime: false}, 44 | {:credo, "1.7.5", only: [:dev, :test], runtime: false}, 45 | {:barlix, "0.6.3"}, 46 | {:uuid, "1.1.8"} 47 | ] 48 | end 49 | 50 | defp desc do 51 | """ 52 | Rapport aims to provide a robust set of modules to generate 53 | HTML reports that both looks good in the browser and when being printed. 54 | """ 55 | end 56 | 57 | defp package do 58 | [ 59 | files: ["lib", "mix.exs", "README*", "LICENSE*"], 60 | maintainers: ["Richard Nyström"], 61 | licenses: ["MIT"], 62 | links: %{ 63 | "GitHub" => "https://github.com/ricn/rapport", 64 | "Docs" => "http://hexdocs.pm/rapport" 65 | } 66 | ] 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "barlix": {:hex, :barlix, "0.6.3", "eca559b67772ff073f03feed60c6aab12e14f460d9dfe455377f684e5a218434", [:mix], [{:png, "~> 0.2", [hex: :png, repo: "hexpm", optional: false]}], "hexpm", "e2867d6d257b01da50914f44c5f2827bb7ffc8dfb69ffac91c3ac20ed2f9ae6b"}, 3 | "bunt": {:hex, :bunt, "0.2.1", "e2d4792f7bc0ced7583ab54922808919518d0e57ee162901a16a1b6664ef3b14", [:mix], [], "hexpm", "a330bfb4245239787b15005e66ae6845c9cd524a288f0d141c148b02603777a5"}, 4 | "certifi": {:hex, :certifi, "2.8.0", "d4fb0a6bb20b7c9c3643e22507e42f356ac090a1dcea9ab99e27e0376d695eba", [:rebar3], [], "hexpm", "6ac7efc1c6f8600b08d625292d4bbf584e14847ce1b6b5c44d983d273e1097ea"}, 5 | "credo": {:hex, :credo, "1.7.5", "643213503b1c766ec0496d828c90c424471ea54da77c8a168c725686377b9545", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "f799e9b5cd1891577d8c773d245668aa74a2fcd15eb277f51a0131690ebfb3fd"}, 6 | "decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"}, 7 | "dialyxir": {:hex, :dialyxir, "1.4.3", "edd0124f358f0b9e95bfe53a9fcf806d615d8f838e2202a9f430d59566b6b53b", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "bf2cfb75cd5c5006bec30141b131663299c661a864ec7fbbc72dfa557487a986"}, 8 | "doctor": {:hex, :doctor, "0.21.0", "20ef89355c67778e206225fe74913e96141c4d001cb04efdeba1a2a9704f1ab5", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm", "a227831daa79784eb24cdeedfa403c46a4cb7d0eab0e31232ec654314447e4e0"}, 9 | "earmark": {:hex, :earmark, "1.4.4", "4821b8d05cda507189d51f2caeef370cf1e18ca5d7dfb7d31e9cafe6688106a4", [:mix], [], "hexpm", "1f93aba7340574847c0f609da787f0d79efcab51b044bb6e242cae5aca9d264d"}, 10 | "earmark_parser": {:hex, :earmark_parser, "1.4.39", "424642f8335b05bb9eb611aa1564c148a8ee35c9c8a8bba6e129d51a3e3c6769", [:mix], [], "hexpm", "06553a88d1f1846da9ef066b87b57c6f605552cfbe40d20bd8d59cc6bde41944"}, 11 | "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, 12 | "ex_doc": {:hex, :ex_doc, "0.32.1", "21e40f939515373bcdc9cffe65f3b3543f05015ac6c3d01d991874129d173420", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.1", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "5142c9db521f106d61ff33250f779807ed2a88620e472ac95dc7d59c380113da"}, 13 | "excoveralls": {:hex, :excoveralls, "0.18.1", "a6f547570c6b24ec13f122a5634833a063aec49218f6fff27de9df693a15588c", [:mix], [{:castore, "~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "d65f79db146bb20399f23046015974de0079668b9abb2f5aac074d078da60b8d"}, 14 | "exjsx": {:hex, :exjsx, "4.0.0", "60548841e0212df401e38e63c0078ec57b33e7ea49b032c796ccad8cde794b5c", [:mix], [{:jsx, "~> 2.8.0", [hex: :jsx, repo: "hexpm", optional: false]}], "hexpm"}, 15 | "faker": {:hex, :faker, "0.18.0", "943e479319a22ea4e8e39e8e076b81c02827d9302f3d32726c5bf82f430e6e14", [:mix], [], "hexpm", "bfbdd83958d78e2788e99ec9317c4816e651ad05e24cfd1196ce5db5b3e81797"}, 16 | "file_system": {:hex, :file_system, "1.0.0", "b689cc7dcee665f774de94b5a832e578bd7963c8e637ef940cd44327db7de2cd", [:mix], [], "hexpm", "6752092d66aec5a10e662aefeed8ddb9531d79db0bc145bb8c40325ca1d8536d"}, 17 | "hackney": {:hex, :hackney, "1.18.0", "c4443d960bb9fba6d01161d01cd81173089686717d9490e5d3606644c48d121f", [:rebar3], [{:certifi, "~>2.8.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~>6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~>1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~>1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "9afcda620704d720db8c6a3123e9848d09c87586dc1c10479c42627b905b5c5e"}, 18 | "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, 19 | "inch_ex": {:hex, :inch_ex, "2.0.0", "24268a9284a1751f2ceda569cd978e1fa394c977c45c331bb52a405de544f4de", [:mix], [{:bunt, "~> 0.2", [hex: :bunt, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "96d0ec5ecac8cf63142d02f16b7ab7152cf0f0f1a185a80161b758383c9399a8"}, 20 | "jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"}, 21 | "jsx": {:hex, :jsx, "2.8.3", "a05252d381885240744d955fbe3cf810504eb2567164824e19303ea59eef62cf", [:mix, :rebar3], [], "hexpm"}, 22 | "makeup": {:hex, :makeup, "1.1.1", "fa0bc768698053b2b3869fa8a62616501ff9d11a562f3ce39580d60860c3a55e", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "5dc62fbdd0de44de194898b6710692490be74baa02d9d108bc29f007783b0b48"}, 23 | "makeup_elixir": {:hex, :makeup_elixir, "0.16.2", "627e84b8e8bf22e60a2579dad15067c755531fea049ae26ef1020cad58fe9578", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "41193978704763f6bbe6cc2758b84909e62984c7752b3784bd3c218bb341706b"}, 24 | "makeup_erlang": {:hex, :makeup_erlang, "0.1.5", "e0ff5a7c708dda34311f7522a8758e23bfcd7d8d8068dc312b5eb41c6fd76eba", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "94d2e986428585a21516d7d7149781480013c56e30c6a233534bedf38867a59a"}, 25 | "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, 26 | "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"}, 27 | "nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"}, 28 | "parse_trans": {:hex, :parse_trans, "3.3.1", "16328ab840cc09919bd10dab29e431da3af9e9e7e7e6f0089dd5a2d2820011d8", [:rebar3], [], "hexpm", "07cd9577885f56362d414e8c4c4e6bdf10d43a8767abb92d24cbe8b24c54888b"}, 29 | "png": {:hex, :png, "0.2.1", "b25c17c8dcdc40096d46ae2d7c2777c63fede48fa8c8312cc708c88554049d3b", [:rebar3], [], "hexpm", "279345e07108c604871a21f1c91f716810ab559af2b20d6f302e0a98265ef72e"}, 30 | "poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], [], "hexpm", "fec8660eb7733ee4117b85f55799fd3833eb769a6df71ccf8903e8dc5447cfce"}, 31 | "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"}, 32 | "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"}, 33 | "uuid": {:hex, :uuid, "1.1.8", "e22fc04499de0de3ed1116b770c7737779f226ceefa0badb3592e64d5cfb4eb9", [:mix], [], "hexpm", "c790593b4c3b601f5dc2378baae7efaf5b3d73c4c6456ba85759905be792f2ac"}, 34 | } 35 | -------------------------------------------------------------------------------- /test/barcode_test.exs: -------------------------------------------------------------------------------- 1 | defmodule BarcodeTest do 2 | use ExUnit.Case 3 | doctest Rapport.Barcode 4 | alias Rapport.Barcode 5 | 6 | describe "create" do 7 | test "must create code39 barcode" do 8 | binary = Barcode.create(:code39, "201731010101") 9 | assert <<0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A>> <> _ = binary 10 | end 11 | 12 | test "must create code39 barcode with options" do 13 | opts = [xdim: 2, height: 200, margin: 20] 14 | binary = Barcode.create(:code39, "201731010101", opts) 15 | assert <<0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A>> <> _ = binary 16 | end 17 | 18 | test "must create code93 barcode" do 19 | binary = Barcode.create(:code93, "201731010101") 20 | assert <<0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A>> <> _ = binary 21 | end 22 | 23 | test "must create code93 barcode with options" do 24 | opts = [xdim: 2, height: 200, margin: 20] 25 | binary = Barcode.create(:code93, "201731010101", opts) 26 | assert <<0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A>> <> _ = binary 27 | end 28 | 29 | test "must create code128 barcode" do 30 | binary = Barcode.create(:code128, "201731010101") 31 | assert <<0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A>> <> _ = binary 32 | end 33 | 34 | test "must create code128 barcode with options" do 35 | opts = [xdim: 2, height: 200, margin: 20] 36 | binary = Barcode.create(:code128, "201731010101", opts) 37 | assert <<0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A>> <> _ = binary 38 | end 39 | 40 | test "must create ITF barcode" do 41 | binary = Barcode.create(:itf, "201731010101") 42 | assert <<0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A>> <> _ = binary 43 | end 44 | 45 | test "must create ITF barcode with options" do 46 | opts = [xdim: 2, height: 200, margin: 20] 47 | binary = Barcode.create(:itf, "201731010101", opts) 48 | assert <<0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A>> <> _ = binary 49 | end 50 | 51 | test "must create EAN13 barcode" do 52 | binary = Barcode.create(:ean13, "2017310101011") 53 | assert <<0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A>> <> _ = binary 54 | end 55 | 56 | test "must create EAN13 barcode with options" do 57 | opts = [xdim: 2, height: 200, margin: 20] 58 | binary = Barcode.create(:ean13, "2017310101011", opts) 59 | assert <<0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A>> <> _ = binary 60 | end 61 | 62 | test "must raise error when barcode type is invalid" do 63 | assert_raise ArgumentError, ~r/^Invalid barcode type/, fn -> 64 | Barcode.create(:code3000, "201731010101") 65 | end 66 | end 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /test/example_templates/barcode_page.html.eex: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 |
5 | 6 |
7 |
8 | 9 |
10 |
11 | 12 |
13 | 14 |
15 | 16 |
17 | -------------------------------------------------------------------------------- /test/example_templates/charts_page.html.eex: -------------------------------------------------------------------------------- 1 |

Chart examples

2 | 3 |
4 |
5 |
6 | 15 | -------------------------------------------------------------------------------- /test/example_templates/charts_report.html.eex: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /test/example_templates/invoice_page.html.eex: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 18 | 19 | 20 | 21 | 38 | 39 | 40 | 41 | 44 | 45 | 48 | 49 | 50 | 51 | 54 | 55 | 58 | 59 | 60 | 61 | 64 | 65 | 68 | 69 | 70 | <%= for item <- @items do %> 71 | 72 | 73 | 74 | 75 | <% end %> 76 | 77 | 78 | 79 | 81 | 82 |
5 | 6 | 7 | 10 | 15 | 16 |
8 | 9 | 11 | Invoice #: <%= @number %>
12 | Created: <%= @created_at %>
13 | Due: <%= @due_at %> 14 |
17 |
22 | 23 | 24 | 29 | 30 | 35 | 36 |
25 | <%= @your_company_name %>
26 | <%= @your_company_address_line_1 %>
27 | <%= @your_company_address_line_2 %> 28 |
31 | <%= @customer_name %>
32 | <%= @customer_address_line_1 %>
33 | <%= @customer_address_line_2 %> 34 |
37 |
42 | Payment Method 43 | 46 | Check # 47 |
52 | <%= @payment_method.method %> 53 | 56 | <%= @payment_method.number %> 57 |
62 | Item 63 | 66 | Price 67 |
<%= item.name %>$<%= item.price %>
Total: $<%= @total_price %> 80 |
83 |
84 | -------------------------------------------------------------------------------- /test/example_templates/invoice_report.html.eex: -------------------------------------------------------------------------------- 1 | 75 | -------------------------------------------------------------------------------- /test/example_templates/list_of_people_cover_page.html.eex: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 5 |

All employees at Top Secret Company Inc

6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | <%= for row <- @cover_page_data do %> 14 | 15 | 16 | 17 | 18 | 19 | <% end %> 20 |
CityNumber of employeesNumber of pages
<%= row.city %><%= row.num_of_employees %><%= row.num_of_pages %>
21 | -------------------------------------------------------------------------------- /test/example_templates/list_of_people_page.html.eex: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 5 |

<%= List.first(@people).city %>

6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | <%= for person <- @people do %> 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | <% end %> 24 |
#FirstnameLastnamePhoneE-mail
<%= person.employee_no %><%= person.firstname %><%= person.lastname %><%= person.phone %><%= person.email %>
25 | -------------------------------------------------------------------------------- /test/example_templates/list_of_people_report.html.eex: -------------------------------------------------------------------------------- 1 | 2 | 3 | 46 | -------------------------------------------------------------------------------- /test/example_templates/page_numbering_page.html.eex: -------------------------------------------------------------------------------- 1 |

Random text

2 |

<%= @text %>

3 | -------------------------------------------------------------------------------- /test/example_templates/table_page.html.eex: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | <%= for person <- @people do %> 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | <% end %> 18 |
#FirstnameLastnamePhoneE-mail
<%= person.num %><%= person.firstname %><%= person.lastname %><%= person.phone %><%= person.email %>
19 | -------------------------------------------------------------------------------- /test/example_templates/table_report.html.eex: -------------------------------------------------------------------------------- 1 | 11 | -------------------------------------------------------------------------------- /test/example_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExampleTest do 2 | use ExUnit.Case 3 | @moduletag :external 4 | 5 | test "hello.html" do 6 | page_template = "

<%= @hello %>

" 7 | 8 | html_report = 9 | Rapport.new() 10 | |> Rapport.add_page(page_template, %{hello: "Hello world!"}) 11 | |> Rapport.generate_html() 12 | 13 | file = Path.join([File.cwd!(), "examples", "hello.html"]) 14 | File.write!(file, html_report) 15 | end 16 | 17 | test "custom_fonts_and_styles.html" do 18 | report_template = """ 19 | 34 | """ 35 | 36 | page_template = "

<%= @hello %>

" 37 | font = Rapport.Font.as_data(File.read!(Path.join(__DIR__, "fonts/tangerine.woff2"))) 38 | 39 | html_report = 40 | Rapport.new(report_template, %{font: font}) 41 | |> Rapport.add_page(page_template, %{hello: "Hello world!"}) 42 | |> Rapport.generate_html() 43 | 44 | file = Path.join([File.cwd!(), "examples", "custom_fonts_and_styles.html"]) 45 | File.write!(file, html_report) 46 | end 47 | 48 | test "two_page_table.html" do 49 | report_template = File.read!(Path.join(__DIR__, "example_templates/table_report.html.eex")) 50 | page_template = File.read!(Path.join(__DIR__, "example_templates/table_page.html.eex")) 51 | 52 | all_people = 53 | Enum.map(1..60, fn num -> 54 | %{ 55 | num: num, 56 | firstname: Faker.Person.first_name(), 57 | lastname: Faker.Person.last_name(), 58 | phone: Faker.Phone.EnUs.phone(), 59 | email: Faker.Internet.email() 60 | } 61 | end) 62 | 63 | number_of_people_per_page = 35 64 | report = Rapport.new(report_template) 65 | 66 | html_report = 67 | Enum.chunk_every(all_people, number_of_people_per_page) 68 | |> Enum.reduce(report, fn chunk, acc -> 69 | Rapport.add_page(acc, page_template, %{people: chunk}) 70 | end) 71 | |> Rapport.generate_html() 72 | 73 | file = Path.join([File.cwd!(), "examples", "two_page_table.html"]) 74 | File.write!(file, html_report) 75 | end 76 | 77 | test "invoice.html" do 78 | report_template = File.read!(Path.join(__DIR__, "example_templates/invoice_report.html.eex")) 79 | page_template = File.read!(Path.join(__DIR__, "example_templates/invoice_page.html.eex")) 80 | logo = Rapport.Image.as_data(File.read!(Path.join(__DIR__, "images/acme.png"))) 81 | 82 | invoice = %{ 83 | number: 1234, 84 | created_at: "2017-09-29", 85 | due_at: "2017-10-29", 86 | your_company_name: "Acme, Inc", 87 | your_company_address_line_1: "12345 Sunny Road", 88 | your_company_address_line_2: "Sunnyville, TX 12345", 89 | customer_name: "Customer, Inc", 90 | customer_address_line_1: "54321 Cloudy Road", 91 | customer_address_line_2: "Cloudlyville, NY 54321", 92 | payment_method: %{method: "Check", number: 1001}, 93 | items: [ 94 | %{name: "Website design", price: 300}, 95 | %{name: "Hosting (3 months)", price: 75}, 96 | %{name: "Domain name (1 year)", price: 10} 97 | ], 98 | total_price: 385, 99 | logo: logo 100 | } 101 | 102 | html_report = 103 | Rapport.new(report_template) 104 | |> Rapport.set_title("Invoice #1234") 105 | |> Rapport.add_page(page_template, invoice) 106 | |> Rapport.generate_html() 107 | 108 | file = Path.join([File.cwd!(), "examples", "invoice.html"]) 109 | File.write!(file, html_report) 110 | end 111 | 112 | test "list_of_people.html" do 113 | # This is a pretty advanced report that showcases how complex reports that are possible to create with 114 | # Rapport. The report starts with a cover page that show a summary of the rest of the report. The cover page 115 | # shows the city, how many people that belongs to the city and how many pages that is needed to list them. 116 | # The rest of the report lists all the people grouped by city and displays 12 people per page. 117 | 118 | report_template = 119 | File.read!(Path.join(__DIR__, "example_templates/list_of_people_report.html.eex")) 120 | 121 | cover_page_template = 122 | File.read!(Path.join(__DIR__, "example_templates/list_of_people_cover_page.html.eex")) 123 | 124 | people_page_template = 125 | File.read!(Path.join(__DIR__, "example_templates/list_of_people_page.html.eex")) 126 | 127 | top_secret_stamp_image = File.read!(Path.join(__DIR__, "images/top_secret_stamp.png")) 128 | 129 | cities = [ 130 | "New York", 131 | "San Francisco", 132 | "Los Angeles", 133 | "Miami", 134 | "Chicago", 135 | "Boston", 136 | "Detroit", 137 | "Houston" 138 | ] 139 | 140 | top_secret_stamp = Rapport.Image.as_data(top_secret_stamp_image) 141 | 142 | # Generates 250 random people and sorts them by city 143 | all_people = 144 | Enum.map(1..250, fn num -> 145 | %{ 146 | employee_no: 10000 + num, 147 | firstname: Faker.Person.first_name(), 148 | lastname: Faker.Person.last_name(), 149 | phone: Faker.Phone.EnUs.phone(), 150 | email: Faker.Internet.email(), 151 | city: Enum.at(cities, Enum.random(0..7)) 152 | } 153 | end) 154 | |> Enum.sort(&(&1.city <= &2.city)) 155 | 156 | people_per_page = 12 157 | 158 | # Creates the data for the cover page and sorts the result by city 159 | cover_page_data = 160 | Enum.map(cities, fn city -> 161 | num_of_employees = all_people |> Enum.filter(fn p -> p.city == city end) |> Enum.count() 162 | num_of_pages = round(Float.ceil(num_of_employees / people_per_page)) 163 | 164 | %{ 165 | city: city, 166 | num_of_employees: num_of_employees, 167 | num_of_pages: num_of_pages 168 | } 169 | end) 170 | |> Enum.sort(&(&1.city <= &2.city)) 171 | 172 | # Creates pages with all people that is chunked by city and all the people per city 173 | # is chunked every time we have 12 people 174 | pages_with_people = 175 | Enum.chunk_by(all_people, fn p -> p.city end) 176 | |> Enum.flat_map(fn people_per_city -> 177 | people_per_city 178 | |> Enum.chunk_every(people_per_page) 179 | |> Enum.map(fn people -> 180 | %Rapport.Page{ 181 | template: people_page_template, 182 | fields: %{people: people, stamp: top_secret_stamp} 183 | } 184 | end) 185 | end) 186 | |> Enum.reverse() 187 | 188 | # Creates an HTML report in landscape mode 189 | html_report = 190 | Rapport.new(report_template) 191 | |> Rapport.set_rotation(:landscape) 192 | |> Rapport.add_page(cover_page_template, %{cover_page_data: cover_page_data}) 193 | |> Rapport.add_pages(pages_with_people) 194 | |> Rapport.generate_html() 195 | 196 | file = Path.join([File.cwd!(), "examples", "list_of_people.html"]) 197 | File.write!(file, html_report) 198 | end 199 | 200 | test "page_numbering.html" do 201 | page_template = 202 | File.read!(Path.join(__DIR__, "example_templates/page_numbering_page.html.eex")) 203 | 204 | random_text = Enum.map_join(1..6, fn _ -> Faker.Lorem.paragraphs() end) 205 | fields = %{text: random_text} 206 | 207 | pages = Enum.map(1..4, fn _ -> %Rapport.Page{template: page_template, fields: fields} end) 208 | 209 | html_report = 210 | Rapport.new() 211 | |> Rapport.add_pages(pages) 212 | |> Rapport.add_page_numbers(:bottom_right, fn current_page, total_pages -> 213 | "#{current_page} of #{total_pages}" 214 | end) 215 | |> Rapport.generate_html() 216 | 217 | file = Path.join([File.cwd!(), "examples", "page_numbering.html"]) 218 | File.write!(file, html_report) 219 | end 220 | 221 | test "barcodes.html" do 222 | page_template = File.read!(Path.join(__DIR__, "example_templates/barcode_page.html.eex")) 223 | opts = [height: 100] 224 | text = "20171009213822" 225 | code39 = Rapport.Barcode.create(:code39, text, opts) |> Rapport.Image.as_data() 226 | code93 = Rapport.Barcode.create(:code93, text, opts) |> Rapport.Image.as_data() 227 | code128 = Rapport.Barcode.create(:code128, text, opts) |> Rapport.Image.as_data() 228 | itf = Rapport.Barcode.create(:itf, text, opts) |> Rapport.Image.as_data() 229 | ean13 = Rapport.Barcode.create(:ean13, "2017100921386", opts) |> Rapport.Image.as_data() 230 | 231 | html_report = 232 | Rapport.new() 233 | |> Rapport.add_page(page_template, %{ 234 | code39: code39, 235 | code93: code93, 236 | code128: code128, 237 | itf: itf, 238 | ean13: ean13, 239 | text: text 240 | }) 241 | |> Rapport.generate_html() 242 | 243 | file = Path.join([File.cwd!(), "examples", "barcodes.html"]) 244 | File.write!(file, html_report) 245 | end 246 | end 247 | -------------------------------------------------------------------------------- /test/font_test.exs: -------------------------------------------------------------------------------- 1 | defmodule FontTest do 2 | use ExUnit.Case 3 | doctest Rapport.Font 4 | alias Rapport.Font 5 | 6 | describe "as_data" do 7 | test "must convert woff2 font to correct data" do 8 | woff2 = File.read!(Path.join(__DIR__, "fonts/font.woff2")) 9 | data = Font.as_data(woff2) 10 | assert data =~ "font/woff2;base64" 11 | assert data =~ "d09GMgABAAAAACZ4" 12 | end 13 | 14 | test "must raise error when font is invalid" do 15 | assert_raise ArgumentError, ~r/^Invalid font/, fn -> 16 | no_font = File.read!(Path.join(__DIR__, "fonts/no.font")) 17 | Font.as_data(no_font) 18 | end 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /test/fonts/font.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ricn/rapport/b5ebb91f3c41800de66f6a5e121d0f8c4637e747/test/fonts/font.woff2 -------------------------------------------------------------------------------- /test/fonts/no.font: -------------------------------------------------------------------------------- 1 | This is not a font file. 2 | -------------------------------------------------------------------------------- /test/fonts/tangerine.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ricn/rapport/b5ebb91f3c41800de66f6a5e121d0f8c4637e747/test/fonts/tangerine.woff2 -------------------------------------------------------------------------------- /test/image_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ImageTest do 2 | use ExUnit.Case 3 | doctest Rapport.Image 4 | alias Rapport.Image 5 | 6 | describe "as_data" do 7 | test "must convert png image to correct data" do 8 | png = File.read!(Path.join(__DIR__, "images/png.png")) 9 | data = Image.as_data(png) 10 | assert data =~ "image/png;base64" 11 | assert data =~ "iVBORw0KGgoAAAANSUhEUgAAA" 12 | end 13 | 14 | test "must convert jpeg image to correct data" do 15 | jpg = File.read!(Path.join(__DIR__, "images/jpg.jpg")) 16 | data = Image.as_data(jpg) 17 | assert data =~ "image/jpeg;base64" 18 | assert data =~ "/9j/" 19 | end 20 | 21 | test "must convert gif image to correct data" do 22 | jpg = File.read!(Path.join(__DIR__, "images/gif.gif")) 23 | data = Image.as_data(jpg) 24 | assert data =~ "image/gif;base64" 25 | assert data =~ "R0lGODl" 26 | end 27 | 28 | test "must raise error when image is not an image" do 29 | assert_raise ArgumentError, ~r/^Invalid image/, fn -> 30 | no_image = File.read!(Path.join(__DIR__, "images/no.image")) 31 | Image.as_data(no_image) 32 | end 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /test/images/acme.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ricn/rapport/b5ebb91f3c41800de66f6a5e121d0f8c4637e747/test/images/acme.png -------------------------------------------------------------------------------- /test/images/gif.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ricn/rapport/b5ebb91f3c41800de66f6a5e121d0f8c4637e747/test/images/gif.gif -------------------------------------------------------------------------------- /test/images/jpg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ricn/rapport/b5ebb91f3c41800de66f6a5e121d0f8c4637e747/test/images/jpg.jpg -------------------------------------------------------------------------------- /test/images/logo/editablefile.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ricn/rapport/b5ebb91f3c41800de66f6a5e121d0f8c4637e747/test/images/logo/editablefile.ai -------------------------------------------------------------------------------- /test/images/logo/horizontal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ricn/rapport/b5ebb91f3c41800de66f6a5e121d0f8c4637e747/test/images/logo/horizontal.png -------------------------------------------------------------------------------- /test/images/logo/horizontal.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | 19 | 30 | 38 | 46 | 57 | 68 | 76 | 77 | 78 | 81 | 84 | 87 | 88 | 90 | 92 | 94 | 95 | 97 | 98 | 108 | 116 | 127 | 133 | 134 | 135 | 136 | 137 | -------------------------------------------------------------------------------- /test/images/logo/vertical.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ricn/rapport/b5ebb91f3c41800de66f6a5e121d0f8c4637e747/test/images/logo/vertical.png -------------------------------------------------------------------------------- /test/images/logo/vertical.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | 19 | 30 | 38 | 46 | 57 | 67 | 75 | 76 | 77 | 81 | 84 | 87 | 88 | 90 | 92 | 94 | 95 | 98 | 99 | 109 | 117 | 128 | 134 | 135 | 136 | 137 | 138 | -------------------------------------------------------------------------------- /test/images/no.image: -------------------------------------------------------------------------------- 1 | No Image 2 | -------------------------------------------------------------------------------- /test/images/png.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ricn/rapport/b5ebb91f3c41800de66f6a5e121d0f8c4637e747/test/images/png.png -------------------------------------------------------------------------------- /test/images/top_secret_stamp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ricn/rapport/b5ebb91f3c41800de66f6a5e121d0f8c4637e747/test/images/top_secret_stamp.png -------------------------------------------------------------------------------- /test/page_numbering_test.exs: -------------------------------------------------------------------------------- 1 | defmodule PageNumberingTest do 2 | use ExUnit.Case 3 | doctest Rapport.PageNumbering 4 | 5 | alias Rapport 6 | 7 | @hello_template File.read!(Path.join(__DIR__, "templates/hello.html.eex")) 8 | 9 | describe "add_page_numbers" do 10 | test "add page numbers without options" do 11 | # bottom right 12 | html_report = 13 | Rapport.new() 14 | |> Rapport.add_page(@hello_template, %{hello: "Page 1"}) 15 | |> Rapport.add_page(@hello_template, %{hello: "Page 2"}) 16 | |> Rapport.add_page_numbers() 17 | |> Rapport.generate_html() 18 | 19 | assert html_report =~ "1" 20 | assert html_report =~ "2" 21 | end 22 | 23 | test "add page numbers to bottom left corner" do 24 | html_report = 25 | Rapport.new() 26 | |> Rapport.add_page(@hello_template, %{hello: "Page 1"}) 27 | |> Rapport.add_page(@hello_template, %{hello: "Page 2"}) 28 | |> Rapport.add_page_numbers(:bottom_left) 29 | |> Rapport.generate_html() 30 | 31 | assert html_report =~ "1" 32 | assert html_report =~ "2" 33 | end 34 | 35 | test "add page numbers to top right corner" do 36 | html_report = 37 | Rapport.new() 38 | |> Rapport.add_page(@hello_template, %{hello: "Page 1"}) 39 | |> Rapport.add_page(@hello_template, %{hello: "Page 2"}) 40 | |> Rapport.add_page_numbers(:top_right) 41 | |> Rapport.generate_html() 42 | 43 | assert html_report =~ "1" 44 | assert html_report =~ "2" 45 | end 46 | 47 | test "add page numbers to top left corner" do 48 | html_report = 49 | Rapport.new() 50 | |> Rapport.add_page(@hello_template, %{hello: "Page 1"}) 51 | |> Rapport.add_page(@hello_template, %{hello: "Page 2"}) 52 | |> Rapport.add_page_numbers(:top_left) 53 | |> Rapport.generate_html() 54 | 55 | assert html_report =~ "1" 56 | assert html_report =~ "2" 57 | end 58 | 59 | test "raise error when invalid page number position is used" do 60 | assert_raise ArgumentError, ~r/^Invalid page number position/, fn -> 61 | Rapport.new() 62 | |> Rapport.add_page_numbers(:middle_middle) 63 | end 64 | end 65 | 66 | test "format page numbers as 1 (2)" do 67 | html_report = 68 | Rapport.new() 69 | |> Rapport.add_page(@hello_template, %{hello: "Page 1"}) 70 | |> Rapport.add_page(@hello_template, %{hello: "Page 2"}) 71 | |> Rapport.add_page_numbers(:bottom_right, fn current_page, total_pages -> 72 | "#{current_page} (#{total_pages})" 73 | end) 74 | |> Rapport.generate_html() 75 | 76 | assert html_report =~ "1 (2)" 77 | assert html_report =~ "2 (2)" 78 | end 79 | 80 | test "format page numbers as 1 of 2" do 81 | html_report = 82 | Rapport.new() 83 | |> Rapport.add_page(@hello_template, %{hello: "Page 1"}) 84 | |> Rapport.add_page(@hello_template, %{hello: "Page 2"}) 85 | |> Rapport.add_page_numbers(:bottom_right, fn current_page, total_pages -> 86 | "#{current_page} of #{total_pages}" 87 | end) 88 | |> Rapport.generate_html() 89 | 90 | assert html_report =~ "1 of 2" 91 | assert html_report =~ "2 of 2" 92 | end 93 | 94 | test "must not add page numbers if we don't want them" do 95 | html_report = 96 | Rapport.new() 97 | |> Rapport.add_page(@hello_template, %{hello: "Page 1"}) 98 | |> Rapport.add_page(@hello_template, %{hello: "Page 2"}) 99 | |> Rapport.generate_html() 100 | 101 | assert !String.contains?(html_report, "1") 102 | assert !String.contains?(html_report, "2") 103 | end 104 | end 105 | end 106 | -------------------------------------------------------------------------------- /test/page_test.exs: -------------------------------------------------------------------------------- 1 | defmodule PageTest do 2 | use ExUnit.Case 3 | doctest Rapport.Page 4 | 5 | alias Rapport.Page 6 | alias Rapport 7 | @hello_template File.read!(Path.join(__DIR__, "templates/hello.html.eex")) 8 | 9 | describe "add_page" do 10 | test "add one page with template and fields" do 11 | report = 12 | Rapport.new() 13 | |> Rapport.add_page(@hello_template, %{hello: "Hello world"}) 14 | 15 | assert length(report.pages) == 1 16 | assert List.first(report.pages).fields.hello == "Hello world" 17 | end 18 | 19 | test "add two pages with template and fields" do 20 | report = 21 | Rapport.new() 22 | |> Rapport.add_page(@hello_template, %{hello: "One"}) 23 | |> Rapport.add_page(@hello_template, %{hello: "Two"}) 24 | 25 | assert length(report.pages) == 2 26 | end 27 | 28 | test "add one page with a Page struct" do 29 | report = 30 | Rapport.new() 31 | |> Rapport.add_page(%Page{template: @hello_template, fields: %{hello: "Hello world"}}) 32 | 33 | assert length(report.pages) == 1 34 | assert List.first(report.pages).fields.hello == "Hello world" 35 | end 36 | 37 | test "add two pages using Page structs" do 38 | report = 39 | Rapport.new() 40 | |> Rapport.add_page(%Page{template: @hello_template, fields: %{hello: "One"}}) 41 | |> Rapport.add_page(%Page{template: @hello_template, fields: %{hello: "Two"}}) 42 | 43 | assert length(report.pages) == 2 44 | end 45 | end 46 | 47 | describe "add_pages" do 48 | test "add two pages" do 49 | list_of_pages = [ 50 | %Page{template: @hello_template, fields: %{hello: "One"}}, 51 | %Page{template: @hello_template, fields: %{hello: "Two"}} 52 | ] 53 | 54 | report = 55 | Rapport.new() 56 | |> Rapport.add_pages(list_of_pages) 57 | 58 | assert length(report.pages) == 2 59 | end 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /test/rapport_test.exs: -------------------------------------------------------------------------------- 1 | defmodule RapportTest do 2 | use ExUnit.Case 3 | doctest Rapport 4 | 5 | @hello_template File.read!(Path.join(__DIR__, "templates/hello.html.eex")) 6 | @two_fields_template File.read!(Path.join(__DIR__, "templates/two_fields.html.eex")) 7 | @list_template File.read!(Path.join(__DIR__, "templates/list.html.eex")) 8 | @list_map_template File.read!(Path.join(__DIR__, "templates/list_map.html.eex")) 9 | 10 | describe "new" do 11 | test "must set sane defaults" do 12 | report = Rapport.new() 13 | assert is_map(report) 14 | assert Map.has_key?(report, :title) 15 | assert Map.get(report, :title) == "Report" 16 | assert Map.has_key?(report, :paper_size) 17 | assert Map.has_key?(report, :rotation) 18 | assert Map.has_key?(report, :pages) 19 | assert Map.has_key?(report, :page_number_opts) 20 | assert is_list(Map.get(report, :pages)) 21 | end 22 | 23 | test "must allow report template" do 24 | style = """ 25 | 30 | """ 31 | 32 | page_template = """ 33 |
34 |

<%= @hello %>

35 |
36 | """ 37 | 38 | html_report = 39 | Rapport.new(style) 40 | |> Rapport.add_page(page_template, %{hello: "Hello!"}) 41 | |> Rapport.generate_html() 42 | 43 | assert html_report =~ "color: red;" 44 | end 45 | 46 | test "must allow fields to be set in the report template" do 47 | report_template = """ 48 | 53 | """ 54 | 55 | html_report = 56 | Rapport.new(report_template, %{color: "yellow"}) 57 | |> Rapport.generate_html() 58 | 59 | assert html_report =~ "color: yellow;" 60 | end 61 | end 62 | 63 | describe "generate_html" do 64 | test "must include normalize & paper css" do 65 | html_report = 66 | Rapport.new() 67 | |> Rapport.generate_html() 68 | 69 | assert html_report =~ "normalize.css v7.0.0" 70 | assert html_report =~ "paper.css" 71 | end 72 | 73 | test "must set paper size correctly" do 74 | html_report = 75 | Rapport.new() 76 | |> Rapport.set_paper_size(:A5) 77 | |> Rapport.generate_html() 78 | 79 | assert html_report =~ "" 80 | assert html_report =~ "" 81 | end 82 | 83 | test "must set rotation correctly" do 84 | html_report = 85 | Rapport.new() 86 | |> Rapport.set_rotation(:landscape) 87 | |> Rapport.generate_html() 88 | 89 | assert html_report =~ "" 90 | assert html_report =~ "" 91 | end 92 | 93 | test "must generate correct html with one field" do 94 | html_report = 95 | Rapport.new() 96 | |> Rapport.add_page(@hello_template, %{hello: "Hello World!"}) 97 | |> Rapport.generate_html() 98 | 99 | assert html_report =~ "
Hello World!
" 100 | end 101 | 102 | test "must generate correct html with several fields" do 103 | fields = %{first: "first!", second: "second!"} 104 | 105 | html_report = 106 | Rapport.new() 107 | |> Rapport.add_page(@two_fields_template, fields) 108 | |> Rapport.generate_html() 109 | 110 | assert html_report =~ "first!" 111 | assert html_report =~ "second!" 112 | end 113 | 114 | test "must generate html with lists" do 115 | list = ["one", "two", "three"] 116 | 117 | html_report = 118 | Rapport.new() 119 | |> Rapport.add_page(@list_template, %{list: list}) 120 | |> Rapport.generate_html() 121 | 122 | assert html_report =~ "one" 123 | assert html_report =~ "two" 124 | assert html_report =~ "three" 125 | end 126 | 127 | test "must generate html with maps" do 128 | people = [ 129 | %{firstname: "Richard", lastname: "Nyström", age: 33}, 130 | %{firstname: "Kristin", lastname: "Nyvall", age: 34}, 131 | %{firstname: "Nils", lastname: "Nyvall", age: 3} 132 | ] 133 | 134 | fields = %{people: people} 135 | 136 | html_report = 137 | Rapport.new() 138 | |> Rapport.add_page(@list_map_template, fields) 139 | |> Rapport.generate_html() 140 | 141 | assert html_report =~ "Richard" 142 | assert html_report =~ "Nyvall" 143 | assert html_report =~ "33" 144 | end 145 | end 146 | 147 | describe "set_title" do 148 | test "must set the title for the report" do 149 | html_report = 150 | Rapport.new() 151 | |> Rapport.set_title("My new title") 152 | |> Rapport.generate_html() 153 | 154 | assert html_report =~ "My new title" 155 | end 156 | end 157 | 158 | describe "set_paper_size" do 159 | test "must set paper size for the report" do 160 | html_report = 161 | Rapport.new() 162 | |> Rapport.set_paper_size(:A5) 163 | |> Rapport.generate_html() 164 | 165 | assert html_report =~ "" 166 | assert html_report =~ "" 167 | end 168 | 169 | test "must raise argument error when paper size is invalid" do 170 | assert_raise ArgumentError, ~r/^Invalid paper size/, fn -> 171 | Rapport.new() 172 | |> Rapport.set_paper_size(:WRONG) 173 | end 174 | end 175 | 176 | test "all allowed paper sizes" do 177 | all = [:A4, :A3, :A5, :half_letter, :letter, :legal, :junior_legal, :ledger] 178 | report = Rapport.new() 179 | 180 | Enum.each(all, fn paper_size -> 181 | assert Rapport.set_paper_size(report, paper_size).paper_size == paper_size 182 | end) 183 | end 184 | end 185 | 186 | describe "set_rotation" do 187 | test "must rotation for the report" do 188 | html_report = 189 | Rapport.new() 190 | |> Rapport.set_rotation(:landscape) 191 | |> Rapport.generate_html() 192 | 193 | assert html_report =~ "" 194 | assert html_report =~ "" 195 | end 196 | 197 | test "must raise argument error when rotation is invalid" do 198 | assert_raise ArgumentError, ~r/^Invalid rotation/, fn -> 199 | Rapport.new() 200 | |> Rapport.set_rotation(:nope) 201 | end 202 | end 203 | 204 | test "all allowed rotations" do 205 | all = [:portrait, :landscape] 206 | report = Rapport.new() 207 | 208 | Enum.each(all, fn rotation -> 209 | assert Rapport.set_rotation(report, rotation).rotation == rotation 210 | end) 211 | end 212 | end 213 | 214 | describe "set_padding" do 215 | test "must set padding on all pages" do 216 | html_report = 217 | Rapport.new() 218 | |> Rapport.set_padding(20) 219 | |> Rapport.add_page(@hello_template, %{hello: "hello"}) 220 | |> Rapport.generate_html() 221 | 222 | assert html_report =~ "
" 223 | end 224 | 225 | test "all allowed paddings" do 226 | all = [10, 15, 20, 25] 227 | report = Rapport.new() 228 | 229 | Enum.each(all, fn padding -> 230 | assert Rapport.set_padding(report, padding).padding == padding 231 | end) 232 | end 233 | 234 | test "must raise argument error when padding is invalid" do 235 | assert_raise ArgumentError, ~r/^Invalid padding/, fn -> 236 | Rapport.new() 237 | |> Rapport.set_padding(5) 238 | end 239 | end 240 | end 241 | 242 | describe "save_to_file" do 243 | test "must save the report to file" do 244 | random_file = Path.join([System.tmp_dir!(), "report_#{UUID.uuid4()}.html"]) 245 | 246 | Rapport.new() 247 | |> Rapport.add_page(@hello_template, %{hello: "hello"}) 248 | |> Rapport.save_to_file(random_file) 249 | 250 | assert File.exists?(random_file) 251 | assert File.read!(random_file) =~ "
hello
" 252 | end 253 | end 254 | end 255 | -------------------------------------------------------------------------------- /test/templates/empty.html.eex: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ricn/rapport/b5ebb91f3c41800de66f6a5e121d0f8c4637e747/test/templates/empty.html.eex -------------------------------------------------------------------------------- /test/templates/hello.html.eex: -------------------------------------------------------------------------------- 1 |
<%= @hello %>
2 | -------------------------------------------------------------------------------- /test/templates/list.html.eex: -------------------------------------------------------------------------------- 1 | <%= for item <- @list do %> 2 |

<%= item %>

3 | <% end %> 4 | -------------------------------------------------------------------------------- /test/templates/list_map.html.eex: -------------------------------------------------------------------------------- 1 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | <%= for person <- @people do %> 19 | 20 | 21 | 22 | 23 | 24 | <% end %> 25 |
FirstnameLastnameAge
<%= person.firstname %><%= person.lastname %><%= person.age %>
26 | -------------------------------------------------------------------------------- /test/templates/two_fields.html.eex: -------------------------------------------------------------------------------- 1 |
<%= @first %>
2 |
<%= @second %>
3 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.configure(exclude: [external: true]) 2 | ExUnit.start() 3 | --------------------------------------------------------------------------------