├── .editorconfig ├── .github └── workflows │ └── development.yml ├── .gitignore ├── .rspec ├── README.md ├── bake └── trenni │ ├── entities.rb │ └── parsers.rb ├── benchmark ├── call_vs_yield.rb ├── interpolation_vs_concat.rb └── io_vs_string.rb ├── entities.json ├── ext └── trenni │ ├── escape.c │ ├── escape.h │ ├── extconf.rb │ ├── markup.c │ ├── markup.h │ ├── markup.rl │ ├── query.c │ ├── query.h │ ├── query.rl │ ├── tag.c │ ├── tag.h │ ├── template.c │ ├── template.h │ ├── template.rl │ ├── trenni.c │ └── trenni.h ├── gems.rb ├── lib ├── trenni.rb └── trenni │ ├── buffer.rb │ ├── builder.rb │ ├── entities.rb │ ├── entities.trenni │ ├── error.rb │ ├── fallback │ ├── markup.rb │ ├── markup.rl │ ├── query.rb │ ├── query.rl │ ├── template.rb │ └── template.rl │ ├── markup.rb │ ├── native.rb │ ├── parse_delegate.rb │ ├── parsers.rb │ ├── query.rb │ ├── reference.rb │ ├── strings.rb │ ├── tag.rb │ ├── template.rb │ ├── uri.rb │ └── version.rb ├── parsers └── trenni │ ├── entities.rl │ ├── markup.rl │ ├── query.rl │ └── template.rl ├── spec ├── spec_helper.rb └── trenni │ ├── builder_spec.rb │ ├── corpus │ ├── large.rb │ └── large.xhtml │ ├── markup_parser_spec.rb │ ├── markup_performance_spec.rb │ ├── markup_spec.rb │ ├── parsers_performance_spec.rb │ ├── query_spec.rb │ ├── reference_spec.rb │ ├── strings_spec.rb │ ├── tag_spec.rb │ ├── template_error_spec.rb │ ├── template_performance_spec.rb │ ├── template_spec.rb │ ├── template_spec │ ├── basic.trenni │ ├── buffer.trenni │ ├── builder.trenni │ ├── capture.trenni │ ├── error.trenni │ ├── escaped.trenni │ ├── interpolations.trenni │ ├── large.erb │ ├── large.trenni │ ├── lines.trenni │ └── nested.trenni │ └── uri_spec.rb └── trenni.gemspec /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | indent_size = 2 6 | -------------------------------------------------------------------------------- /.github/workflows/development.yml: -------------------------------------------------------------------------------- 1 | name: Development 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | name: ${{matrix.ruby}} on ${{matrix.os}} 8 | runs-on: ${{matrix.os}}-latest 9 | continue-on-error: ${{matrix.experimental}} 10 | 11 | strategy: 12 | matrix: 13 | os: 14 | - ubuntu 15 | - macos 16 | 17 | ruby: 18 | - "2.6" 19 | - "2.7" 20 | - "3.0" 21 | 22 | experimental: [false] 23 | env: ["", "TRENNI_PREFER_FALLBACK=y"] 24 | 25 | include: 26 | - os: ubuntu 27 | ruby: truffleruby 28 | experimental: true 29 | - os: ubuntu 30 | ruby: jruby 31 | experimental: true 32 | - os: ubuntu 33 | ruby: head 34 | experimental: true 35 | 36 | steps: 37 | - uses: actions/checkout@v2 38 | - uses: ruby/setup-ruby@v1 39 | with: 40 | ruby-version: ${{matrix.ruby}} 41 | bundler-cache: true 42 | 43 | - name: Run tests 44 | timeout-minutes: 5 45 | run: | 46 | ${{matrix.env}} bundle exec bake trenni:parsers:generate 47 | ${{matrix.env}} bundle exec rspec 48 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .tags 2 | 3 | /.bundle/ 4 | /.yardoc 5 | /gems.locked 6 | /_yardoc/ 7 | /coverage/ 8 | /doc/ 9 | /pkg/ 10 | /spec/reports/ 11 | tmp/ 12 | 13 | .rspec_status 14 | .covered.db 15 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --format documentation 3 | --backtrace 4 | --warnings 5 | --require spec_helper -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Trenni 2 | 3 | > [!NOTE] 4 | > Trenni and all related gems are replaced by the [XRB](https://github.com/socketry/xrb). 5 | 6 | Trenni is a templating system built loosely on top of XHTML markup. It uses efficient native parsers where possible and compiles templates into efficient Ruby. It also includes a markup builder to assist with the generation of pleasantly formatted markup which is compatible with the included parsers. 7 | 8 | [![Development Status](https://github.com/ioquatix/trenni/workflows/Development/badge.svg)](https://github.com/ioquatix/trenni/actions?workflow=Development) 9 | 10 | ## Motivation 11 | 12 | Trenni was designed for [Utopia](https://github.com/ioquatix/utopia). When I originally looked at template engines, I was surprised by the level of complexity and the effort involved in processing a template to produce useful output. In particular, many template engines generate an AST and walk over it to generate output (e.g. ERB, at least at the time I last checked). This is exceedingly slow in Ruby. 13 | 14 | At the time (around 2008?) I was playing around with [ramaze](https://github.com/Ramaze/ramaze) and found a template engine I really liked the design of, called [ezamar](https://github.com/manveru/ezamar). The template compilation process actually generates Ruby code which can then be compiled and executed efficiently. Another engine, by the same author, [nagoro](https://github.com/manveru/nagoro), also provided some inspiration. 15 | 16 | More recently I was doing some investigation regarding using `eval` for executing the code. The problem is that it's [not possible to replace the binding](http://stackoverflow.com/questions/27391909/replace-evalcode-string-binding-with-lambda/27392437) of a `Proc` once it's created, so template engines that evaluate code in a given binding cannot use a compiled proc, they must parse the code every time. By using a `Proc` we can generate a Ruby function which *can* be compiled to a faster representation by the VM. 17 | 18 | In addition, I wanted a simple markup parser and builder for HTML style markup. These are used heavily by Utopia for implementing it's tag based evaluation. `Trenni::Builder` is a simple and efficient way to generate markup, it's not particularly notable, except that it doesn't use `method_missing` to [implement normal behaviour](https://github.com/sparklemotion/nokogiri/blob/b6679e928924529b56dcc0f3164224c040d14555/lib/nokogiri/xml/builder.rb#L355) which is [sort of slow](http://franck.verrot.fr/blog/2015/07/12/benchmarking-ruby-method-missing-and-define-method/). 19 | 20 | The 2nd release of Trenni in 2016 saw an overhaul of the internal parsers. I used [Ragel](http://www.colm.net/open-source/ragel/) to implement efficient event-based markup and template parsers, which can be compiled to both C and Ruby. This provides a native code path where possible giving speed-ups between 10x - 20x. In addition, the formal grammar is more robust. 21 | 22 | The 3rd release of Trenni in 2017 was primarily focused on performance, by moving more of the critical parsing, escaping and tag generation functions to C. In practical usage, this gave about a 40-50% improvement in performance overall. 23 | 24 | ## Is it fast? 25 | 26 | It's faster than Nokogiri for parsing markup: 27 | 28 | Trenni::Native 29 | Warming up -------------------------------------- 30 | Large (Trenni) 71.000 i/100ms 31 | Large (Nokogiri) 28.000 i/100ms 32 | Calculating ------------------------------------- 33 | Large (Trenni) 662.050 (± 3.9%) i/s - 3.337k in 5.048115s 34 | Large (Nokogiri) 266.878 (±10.9%) i/s - 1.316k in 5.008464s 35 | 36 | Comparison: 37 | Large (Trenni): 662.1 i/s 38 | Large (Nokogiri): 266.9 i/s - 2.48x slower 39 | 40 | It's significantly faster than ERB: 41 | 42 | Trenni::Template 43 | Warming up -------------------------------------- 44 | Trenni (object) 75.667k i/100ms 45 | ERB (binding) 6.940k i/100ms 46 | Calculating ------------------------------------- 47 | Trenni (object) 1.095M (± 7.9%) i/s - 5.448M in 5.007244s 48 | ERB (binding) 69.381k (± 7.1%) i/s - 347.000k in 5.027333s 49 | 50 | Comparison: 51 | Trenni (object): 1094979.9 i/s 52 | ERB (binding): 69381.1 i/s - 15.78x slower 53 | 54 | ## Installation 55 | 56 | Add this line to your application's Gemfile: 57 | 58 | gem 'trenni' 59 | 60 | And then execute: 61 | 62 | $ bundle 63 | 64 | Or install it yourself as: 65 | 66 | $ gem install trenni 67 | 68 | ## Usage 69 | 70 | ### Markup Parser 71 | 72 | The markup parser parses a loose super-set of HTML in a way that's useful for content processing, similar to an XSLT processor. It's designed to be faster and easier to use, and integrate directly into an output pipeline. 73 | 74 | To invoke the markup parser: 75 | 76 | ``` ruby 77 | require 'trenni' 78 | 79 | buffer = Trenni::Buffer(string) 80 | 81 | # Custom entities, or could use Trenni::Entities::HTML5 82 | entities = {'amp' => '&', 'lt' => '<', 'gt' => '>', 'quot' => '"'} 83 | 84 | # Modify this class to accumulate events or pass them on somewhere else. 85 | class Delegate 86 | # Called when encountering an open tag: `<` name 87 | def open_tag_begin(name, offset) 88 | end 89 | 90 | # Called when encountering an attribute after open_tag_begin 91 | def attribute(key, value) 92 | end 93 | 94 | # Called when encountering the end of the opening tag. 95 | def open_tag_end(self_closing) 96 | end 97 | 98 | # Called when encountering the closing tag: '' 99 | def close_tag(name, offset) 100 | end 101 | 102 | # Called with the full doctype: '' 103 | def doctype(string) 104 | end 105 | 106 | # Called with the full comment: '' 107 | def comment(string) 108 | end 109 | 110 | # Called with the parsed instruction: '' 111 | def instruction(string) 112 | end 113 | 114 | # Called with a cdata block: '' 115 | def cdata(string) 116 | end 117 | 118 | # Called with any arbitrary pcdata text (e.g. between tags). 119 | def text(string) 120 | end 121 | end 122 | 123 | # Do the actual work: 124 | Trenni::Parsers.parse_markup(buffer, Delegate.new, entities) 125 | ``` 126 | 127 | ### Templates 128 | 129 | Trenni templates work essentially the same way as all other templating systems: 130 | 131 | ``` ruby 132 | buffer = Trenni::Buffer('#{item}') 133 | template = Trenni::Template.new(buffer) 134 | 135 | items = 1..4 136 | 137 | template.to_string(items) # => "1234" 138 | ``` 139 | 140 | The code above demonstrate the only two constructs, `` and `#{output}`. 141 | 142 | Trenni doesn't support using `binding` for evaluation, as this is a slow code path. It uses `instance_exec` 143 | 144 | ### Builder 145 | 146 | Trenni can help construct XML/HTML using a simple DSL: 147 | 148 | ``` ruby 149 | Trenni::Builder.fragment do |builder| 150 | builder.inline 'p' do 151 | builder.tag 'strong' do 152 | builder.text 'Hello' 153 | end 154 | builder.text ' World' 155 | end 156 | end.to_s 157 | # => "

Hello World

" 158 | ``` 159 | 160 | ### Testing 161 | 162 | To test the Ruby parsers: 163 | 164 | rake generate_fallback_parsers && TRENNI_PREFER_FALLBACK=y rspec 165 | 166 | To test the native C parsers: 167 | 168 | rake generate_native_parsers && rake compile && rspec 169 | 170 | ### Benchmarks 171 | 172 | Trenni has a pure Ruby implemenation, with performance critical operations implemented natively. All critical code paths have benchmark specs. 173 | 174 | #### Parser Performance 175 | 176 | You can evaluate and compare template performance with ERB: 177 | 178 | rspec spec/trenni/parsers_performance_spec.rb 179 | 180 | Trenni::Native 181 | #parse_markup 182 | Warming up -------------------------------------- 183 | Large (Trenni) 64.000 i/100ms 184 | Large (Nokogiri) 30.000 i/100ms 185 | Calculating ------------------------------------- 186 | Large (Trenni) 637.720 (± 6.4%) i/s - 3.200k in 5.038187s 187 | Large (Nokogiri) 294.762 (± 5.8%) i/s - 1.470k in 5.004284s 188 | 189 | Comparison: 190 | Large (Trenni): 637.7 i/s 191 | Large (Nokogiri): 294.8 i/s - 2.16x slower 192 | 193 | should be fast to parse large documents 194 | #parse_template 195 | Warming up -------------------------------------- 196 | Large (Trenni) 7.791k i/100ms 197 | Large (ERB) 488.000 i/100ms 198 | Calculating ------------------------------------- 199 | Large (Trenni) 87.889k (± 9.5%) i/s - 436.296k in 5.024283s 200 | Large (ERB) 4.844k (± 5.6%) i/s - 24.400k in 5.053247s 201 | 202 | Comparison: 203 | Large (Trenni): 87889.4 i/s 204 | Large (ERB): 4844.5 i/s - 18.14x slower 205 | 206 | should have better performance using instance 207 | 208 | Finished in 28.2 seconds (files took 0.14204 seconds to load) 209 | 2 examples, 0 failures 210 | 211 | To run this with the pure ruby implementation, use `TRENNI_PREFER_FALLBACK=y rspec spec/trenni/parsers_performance_spec.rb`. 212 | 213 | #### Markup String Performance 214 | 215 | Markup safe strings require escaping characters. Doing this natively makes sense, and in MRI, `CGI.escape_html` is implemented in C. Strings that include characters that need to be escaped are a bit slower because a new string must be allocated and modified. So, we test these two cases. 216 | 217 | rspec spec/trenni/markup_performance_spec.rb 218 | 219 | Trenni::Markup 220 | Warming up -------------------------------------- 221 | General String 179.396k i/100ms 222 | Code String 85.050k i/100ms 223 | Calculating ------------------------------------- 224 | General String 4.773M (±10.0%) i/s - 23.680M in 5.027576s 225 | Code String 1.469M (± 5.7%) i/s - 7.399M in 5.052467s 226 | 227 | Comparison: 228 | General String: 4773201.3 i/s 229 | Code String: 1469345.5 i/s - 3.25x slower 230 | 231 | should be fast to parse large documents 232 | 233 | Finished in 14.11 seconds (files took 0.09696 seconds to load) 234 | 1 example, 0 failures 235 | 236 | #### Template Evaluation Performance 237 | 238 | Evaluating templates and generating output is critical to performance. You can compare Trenni with ERB. The primary factor affecting performance, is the number of interpolations, because each interpolation requires evaluation and concatenation. 239 | 240 | rspec spec/trenni/template_performance_spec.rb 241 | 242 | Trenni::Template 243 | Warming up -------------------------------------- 244 | Trenni 79.000 i/100ms 245 | Calculating ------------------------------------- 246 | Trenni 817.703 (± 7.7%) i/s - 4.108k in 5.071586s 247 | should be fast for lots of interpolations 248 | Warming up -------------------------------------- 249 | Trenni (object) 79.149k i/100ms 250 | ERB (binding) 5.416k i/100ms 251 | Calculating ------------------------------------- 252 | Trenni (object) 1.081M (± 3.7%) i/s - 5.461M in 5.059151s 253 | ERB (binding) 59.016k (± 4.7%) i/s - 297.880k in 5.058614s 254 | 255 | Comparison: 256 | Trenni (object): 1080909.2 i/s 257 | ERB (binding): 59016.3 i/s - 18.32x slower 258 | 259 | should be fast for basic templates 260 | Warming up -------------------------------------- 261 | Trenni 34.204k i/100ms 262 | Calculating ------------------------------------- 263 | Trenni 407.905k (± 9.0%) i/s - 2.018M in 5.001248s 264 | should be fast with capture 265 | 266 | Finished in 28.25 seconds (files took 0.09765 seconds to load) 267 | 3 examples, 0 failures 268 | 269 | ## See Also 270 | 271 | - [language-trenni](https://atom.io/packages/language-trenni) package for the [Atom text editor](https://atom.io). It provides syntax highlighting and integration when Trenni is used with the [utopia web framework](https://github.com/ioquatix/utopia). 272 | 273 | - [vim-trenni](https://github.com/huba/vim-trenni) package for Vim. 274 | 275 | - [Trenni Formatters](https://github.com/ioquatix/trenni-formatters) is a separate gem that uses `Trenni::Builder` to generate HTML forms easily. 276 | 277 | ## Contributing 278 | 279 | 1. Fork it 280 | 2. Create your feature branch (`git checkout -b my-new-feature`) 281 | 3. Commit your changes (`git commit -am 'Add some feature'`) 282 | 4. Push to the branch (`git push origin my-new-feature`) 283 | 5. Create new Pull Request 284 | 285 | ## License 286 | 287 | Released under the MIT license. 288 | 289 | Copyright, 2017, by [Samuel G. D. Williams](http://www.codeotaku.com/samuel-williams). 290 | 291 | Permission is hereby granted, free of charge, to any person obtaining a copy 292 | of this software and associated documentation files (the "Software"), to deal 293 | in the Software without restriction, including without limitation the rights 294 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 295 | copies of the Software, and to permit persons to whom the Software is 296 | furnished to do so, subject to the following conditions: 297 | 298 | The above copyright notice and this permission notice shall be included in 299 | all copies or substantial portions of the Software. 300 | 301 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 302 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 303 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 304 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 305 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 306 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 307 | THE SOFTWARE. 308 | -------------------------------------------------------------------------------- /bake/trenni/entities.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Fetch the HTML5 entities from w3.org and update the local cache. 4 | # @parameter force [Boolean] Whether to force regenerate the local cache. 5 | def fetch_entities(force: false) 6 | require 'json' 7 | require 'async' 8 | require 'async/http/internet' 9 | 10 | internet = Async::HTTP::Internet.new 11 | entites_json_path = self.entites_json_path 12 | 13 | if force || !File.exist?(entites_json_path) 14 | url = "https://www.w3.org/TR/html5/entities.json" 15 | 16 | Sync do 17 | File.write(entites_json_path, internet.get(url).read) 18 | end 19 | end 20 | 21 | return JSON.parse(File.read(entites_json_path)).delete_if{|string, _| !string.end_with? ';'} 22 | end 23 | 24 | # Consume the HTML5 entites and generate parsers to escape them. 25 | # @parameter wet [Boolean] Whether to write updated files. 26 | def update_entities(wet: false) 27 | require 'trenni/template' 28 | 29 | paths = { 30 | # 'ext/trenni/entities.rl' => 'ext/trenni/entities.trenni', 31 | 'lib/trenni/entities.rb' => 'lib/trenni/entities.trenni', 32 | } 33 | 34 | entities = self.fetch_entities 35 | 36 | paths.each do |output_path, template_path| 37 | template_path = File.expand_path(template_path, context.root) 38 | output_path = File.expand_path(output_path, context.root) 39 | 40 | template = Trenni::Template.load_file(template_path) 41 | 42 | output = template.to_string(entities) 43 | 44 | if wet 45 | File.write(output_path, output) 46 | else 47 | puts "*** #{output_path} ***" 48 | puts output 49 | end 50 | end 51 | end 52 | 53 | private 54 | 55 | def entites_json_path 56 | File.expand_path("entities.json", context.root) 57 | end 58 | -------------------------------------------------------------------------------- /bake/trenni/parsers.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Generate the pure Ruby parsers. 4 | def generate_fallback 5 | Dir.chdir(fallback_directory) do 6 | Dir.glob("*.rl").each do |parser_path| 7 | system("ragel", "-I", parsers_directory, "-R", parser_path, "-F1") 8 | end 9 | 10 | # sh("ruby-beautify", "--tabs", "--overwrite", *Dir.glob("*.rb")) 11 | end 12 | end 13 | 14 | # Generate the native C parsers. 15 | def generate_native 16 | Dir.chdir(native_directory) do 17 | Dir.glob("*.rl").each do |parser_path| 18 | system("ragel", "-I", parsers_directory, "-C", parser_path, "-G2") 19 | end 20 | end 21 | end 22 | 23 | # Compile the C extension. 24 | def compile 25 | system("rake", "compile", chdir: extensions_directory) 26 | end 27 | 28 | # Generate the parsers and compile them as required. 29 | def generate 30 | self.generate_native 31 | self.generate_fallback 32 | self.compile 33 | end 34 | 35 | # Generate a visualisation of the parsers. 36 | def visualize_parsers 37 | Dir.chdir(fallback_directory) do 38 | Dir.glob("*.rl").each do |parser_path| 39 | dot_path = parser_path + ".dot" 40 | system("ragel", "-I", parsers_directory, "-Vp", parser_path, "-o", dot_path) 41 | 42 | pdf_path = parser_path + ".pdf" 43 | system("dot", "-Tpdf", "-o", pdf_path, dot_path) 44 | 45 | system("open", pdf_path) rescue nil 46 | end 47 | end 48 | end 49 | 50 | private 51 | 52 | def parsers_directory 53 | File.expand_path("parsers", context.root) 54 | end 55 | 56 | def fallback_directory 57 | File.expand_path("lib/trenni/fallback", context.root) 58 | end 59 | 60 | def extensions_directory 61 | File.expand_path("ext", context.root) 62 | end 63 | 64 | def native_directory 65 | File.expand_path("ext/trenni", context.root) 66 | end 67 | -------------------------------------------------------------------------------- /benchmark/call_vs_yield.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'benchmark/ips' 4 | 5 | puts "Ruby #{RUBY_VERSION} at #{Time.now}" 6 | puts 7 | 8 | firstname = 'soundarapandian' 9 | middlename = 'rathinasamy' 10 | lastname = 'arumugam' 11 | 12 | def do_call(&block) 13 | block.call 14 | end 15 | 16 | def do_yield(&block) 17 | yield 18 | end 19 | 20 | def do_yield_without_block 21 | yield 22 | end 23 | 24 | existing_block = proc{} 25 | 26 | Benchmark.ips do |x| 27 | x.report("block.call") do |i| 28 | buffer = String.new 29 | 30 | while (i -= 1) > 0 31 | do_call(&existing_block) 32 | end 33 | end 34 | 35 | x.report("yield with block") do |i| 36 | buffer = String.new 37 | 38 | while (i -= 1) > 0 39 | do_yield(&existing_block) 40 | end 41 | end 42 | 43 | x.report("yield") do |i| 44 | buffer = String.new 45 | 46 | while (i -= 1) > 0 47 | do_yield_without_block(&existing_block) 48 | end 49 | end 50 | 51 | x.compare! 52 | end 53 | -------------------------------------------------------------------------------- /benchmark/interpolation_vs_concat.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'benchmark/ips' 4 | 5 | puts "Ruby #{RUBY_VERSION} at #{Time.now}" 6 | puts 7 | 8 | firstname = 'soundarapandian' 9 | middlename = 'rathinasamy' 10 | lastname = 'arumugam' 11 | 12 | Benchmark.ips do |x| 13 | x.report("String\#<<") do |i| 14 | buffer = String.new 15 | 16 | while (i -= 1) > 0 17 | buffer << 'Mr. ' << firstname << middlename << lastname << ' aka soundar' 18 | end 19 | end 20 | 21 | x.report("String interpolate") do |i| 22 | buffer = String.new 23 | 24 | while (i -= 1) > 0 25 | buffer << "Mr. #{firstname} #{middlename} #{lastname} aka soundar" 26 | end 27 | end 28 | 29 | x.compare! 30 | end 31 | -------------------------------------------------------------------------------- /benchmark/io_vs_string.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'benchmark/ips' 4 | require 'stringio' 5 | 6 | puts "Ruby #{RUBY_VERSION} at #{Time.now}" 7 | puts 8 | 9 | Benchmark.ips do |x| 10 | 11 | # These two tests look at the cost of simply writing to a buffer. 12 | 13 | x.report("String\#<<") do |i| 14 | buffer = String.new 15 | 16 | while (i -= 1) > 0 17 | buffer << "String #{i}" 18 | end 19 | end 20 | 21 | x.report("Array\#<<") do |i| 22 | buffer = [] 23 | 24 | while (i -= 1) > 0 25 | buffer << "String #{i}" 26 | end 27 | 28 | buffer.join 29 | end 30 | 31 | x.report("StringIO") do |i| 32 | buffer = StringIO.new 33 | 34 | while (i -= 1) > 0 35 | buffer << "String #{i}" 36 | end 37 | end 38 | 39 | x.compare! 40 | end 41 | 42 | # Calculating ------------------------------------- 43 | # String (Amortized) 91666 i/100ms 44 | # StringIO (Amortized) 69531 i/100ms 45 | # ------------------------------------------------- 46 | # String (Amortized) 2856024.9 (±8.8%) i/s - 14208230 in 5.017309s 47 | # StringIO (Amortized) 2424863.3 (±7.0%) i/s - 12098394 in 5.013982s 48 | # 49 | # Comparison: 50 | # String (Amortized): 2856024.9 i/s 51 | # StringIO (Amortized): 2424863.3 i/s - 1.18x slower 52 | 53 | # Adjust N to consider the cost of allocation vs the cost of appending. 54 | N = 5 55 | 56 | Benchmark.ips do |x| 57 | # These next two tests consider that multiple writes may be done per buffer allocation. 58 | 59 | x.report("String") do |i| 60 | i.times do 61 | buffer = String.new 62 | 63 | N.times do 64 | buffer << "String #{i}" 65 | end 66 | end 67 | end 68 | 69 | x.report("StringIO") do |i| 70 | i.times do 71 | buffer = StringIO.new 72 | 73 | N.times do 74 | buffer << "String #{i}" 75 | end 76 | end 77 | end 78 | 79 | x.compare! 80 | end 81 | 82 | # Calculating ------------------------------------- 83 | # String 36822 i/100ms 84 | # StringIO 32471 i/100ms 85 | # ------------------------------------------------- 86 | # String 445143.2 (±5.0%) i/s - 2246142 in 5.059017s 87 | # StringIO 328469.2 (±4.1%) i/s - 1656021 in 5.049919s 88 | # 89 | # Comparison: 90 | # String: 445143.2 i/s 91 | # StringIO: 328469.2 i/s - 1.36x slower 92 | -------------------------------------------------------------------------------- /ext/trenni/escape.c: -------------------------------------------------------------------------------- 1 | 2 | #include "escape.h" 3 | #include 4 | 5 | inline static int Trenni_Markup_is_markup(VALUE value) { 6 | if (RB_IMMEDIATE_P(value)) 7 | return 0; 8 | 9 | // This is a short-cut: 10 | if (rb_class_of(value) == rb_Trenni_MarkupString) { 11 | return 1; 12 | } 13 | 14 | return rb_funcall(value, id_is_a, 1, rb_Trenni_Markup) == Qtrue; 15 | } 16 | 17 | VALUE Trenni_MarkupString_raw(VALUE self, VALUE string) { 18 | string = rb_str_dup(string); 19 | 20 | rb_obj_reveal(string, rb_Trenni_MarkupString); 21 | 22 | return string; 23 | } 24 | 25 | // => [["<", 60, "3c"], [">", 62, "3e"], ["\"", 34, "22"], ["&", 38, "26"]] 26 | // static const uint32_t MASK = 0x3e3e3e3e; 27 | // 28 | // static const uint32_t MASK_LT = 0x3c3c3c3c; 29 | // static const uint32_t MASK_GT = 0x3e3e3e3e; 30 | // static const uint32_t MASK_QUOT = 0x22222222; 31 | // static const uint32_t MASK_AMP = 0x26262626; 32 | 33 | static inline const char * Trenni_Markup_index_symbol(const char * begin, const char * end) { 34 | const char * p = begin; 35 | 36 | while (p < end) { 37 | // if ((end - p) >= 4) { 38 | // // Do the next 4 characters contain anything we are interested in? 39 | // if ((*(const uint32_t *)p) & MASK_LT) { 40 | // p += 4; 41 | // 42 | // continue; 43 | // } 44 | // } 45 | 46 | switch (*p) { 47 | case '<': 48 | case '>': 49 | case '"': 50 | case '&': 51 | return p; 52 | } 53 | 54 | p += 1; 55 | } 56 | 57 | return end; 58 | } 59 | 60 | static inline void Trenni_Markup_append_entity(const char * p, VALUE buffer) { 61 | // What symbol are we looking at? 62 | switch (*p) { 63 | case '<': 64 | rb_str_cat_cstr(buffer, "<"); 65 | break; 66 | case '>': 67 | rb_str_cat_cstr(buffer, ">"); 68 | break; 69 | case '"': 70 | rb_str_cat_cstr(buffer, """); 71 | break; 72 | case '&': 73 | rb_str_cat_cstr(buffer, "&"); 74 | break; 75 | } 76 | } 77 | 78 | static inline VALUE Trenni_Markup_append_buffer(VALUE buffer, const char * s, const char * p, const char * end) { 79 | while (1) { 80 | // Append the non-symbol part: 81 | rb_str_buf_cat(buffer, s, p - s); 82 | 83 | // We escape early if there were no changes to be made: 84 | if (p == end) return buffer; 85 | 86 | Trenni_Markup_append_entity(p, buffer); 87 | 88 | s = p + 1; 89 | p = Trenni_Markup_index_symbol(s, end); 90 | } 91 | } 92 | 93 | // Escape and append a string to the output buffer. 94 | VALUE Trenni_Markup_append_string(VALUE buffer, VALUE string) { 95 | const char * begin = RSTRING_PTR(string); 96 | const char * end = begin + RSTRING_LEN(string); 97 | 98 | const char * s = begin; 99 | 100 | // There are two outcomes, either p is at end, or p points to a symbol: 101 | const char * p = Trenni_Markup_index_symbol(s, end); 102 | 103 | return Trenni_Markup_append_buffer(buffer, s, p, end); 104 | } 105 | 106 | VALUE Trenni_Markup_append(VALUE self, VALUE buffer, VALUE value) { 107 | if (value == Qnil) return Qnil; 108 | 109 | if (Trenni_Markup_is_markup(value)) { 110 | rb_str_append(buffer, value); 111 | } else { 112 | if (rb_type(value) != T_STRING) { 113 | value = rb_funcall(value, id_to_s, 0); 114 | } 115 | 116 | Trenni_Markup_append_string(buffer, value); 117 | } 118 | 119 | return buffer; 120 | } 121 | 122 | // Convert markup special characters to entities. May return the original string if no changes were made. 123 | VALUE Trenni_Markup_escape_string(VALUE self, VALUE string) { 124 | const char * begin = RSTRING_PTR(string); 125 | const char * end = begin + RSTRING_LEN(string); 126 | 127 | const char * s = begin; 128 | 129 | // There are two outcomes, either p is at end, or p points to a symbol: 130 | const char * p = Trenni_Markup_index_symbol(s, end); 131 | 132 | // We escape early if there were no changes to be made: 133 | if (p == end) return string; 134 | 135 | return Trenni_Markup_append_buffer(Trenni_buffer_for(string), s, p, end); 136 | } 137 | 138 | void Init_trenni_escape() { 139 | rb_Trenni_MarkupString = rb_define_class_under(rb_Trenni, "MarkupString", rb_cString); 140 | rb_gc_register_mark_object(rb_Trenni_MarkupString); 141 | 142 | rb_include_module(rb_Trenni_MarkupString, rb_Trenni_Markup); 143 | 144 | rb_undef_method(rb_class_of(rb_Trenni_Markup), "escape_string"); 145 | rb_define_singleton_method(rb_Trenni_Markup, "escape_string", Trenni_Markup_escape_string, 1); 146 | 147 | rb_undef_method(rb_class_of(rb_Trenni_Markup), "append"); 148 | rb_define_singleton_method(rb_Trenni_Markup, "append", Trenni_Markup_append, 2); 149 | 150 | rb_undef_method(rb_class_of(rb_Trenni_Markup), "raw"); 151 | rb_define_singleton_method(rb_Trenni_Markup, "raw", Trenni_MarkupString_raw, 1); 152 | } 153 | -------------------------------------------------------------------------------- /ext/trenni/escape.h: -------------------------------------------------------------------------------- 1 | 2 | #pragma once 3 | 4 | #include "trenni.h" 5 | 6 | void Init_trenni_escape(); 7 | 8 | // Given a string, replace it's class with Trenni::MarkupString so that it would be output as is. 9 | VALUE Trenni_MarkupString_raw(VALUE self, VALUE string); 10 | 11 | // Append any value to the output buffer efficiently, escaping entities as needed. 12 | VALUE Trenni_Markup_append(VALUE self, VALUE buffer, VALUE value); 13 | 14 | // Escape any entities in the given string. If no entities were found, might return the original string. 15 | VALUE Trenni_Markup_escape_string(VALUE self, VALUE string); 16 | -------------------------------------------------------------------------------- /ext/trenni/extconf.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Loads mkmf which is used to make makefiles for Ruby extensions 4 | require 'mkmf' 5 | 6 | $CFLAGS << " -O3 -std=c99" 7 | 8 | # Needed by Ruby 2.1 9 | have_func("rb_sym2str") 10 | have_func("rb_str_cat_cstr") 11 | have_func("rb_str_reserve") 12 | 13 | gem_name = File.basename(__dir__) 14 | extension_name = 'trenni' 15 | 16 | # The destination 17 | dir_config(extension_name) 18 | 19 | # Generate the makefile to compile the native binary into `lib`: 20 | create_makefile(File.join(gem_name, extension_name)) 21 | 22 | -------------------------------------------------------------------------------- /ext/trenni/markup.h: -------------------------------------------------------------------------------- 1 | 2 | #pragma once 3 | 4 | #include "trenni.h" 5 | 6 | VALUE Trenni_Native_parse_markup(VALUE self, VALUE buffer, VALUE delegate, VALUE entities); 7 | -------------------------------------------------------------------------------- /ext/trenni/markup.rl: -------------------------------------------------------------------------------- 1 | 2 | #include "markup.h" 3 | 4 | %%{ 5 | machine Trenni_markup_parser; 6 | 7 | # Track the location of an identifier (tag name, attribute name, etc) 8 | action identifier_begin { 9 | identifier.begin = p; 10 | } 11 | 12 | action identifier_end { 13 | identifier.end = p; 14 | } 15 | 16 | action pcdata_begin { 17 | pcdata = Qnil; 18 | has_entities = 0; 19 | } 20 | 21 | action pcdata_end { 22 | } 23 | 24 | action text_begin { 25 | } 26 | 27 | action text_end { 28 | rb_funcall(delegate, id_text, 1, Trenni_markup_safe(pcdata, has_entities)); 29 | } 30 | 31 | action characters_begin { 32 | characters.begin = p; 33 | } 34 | 35 | action characters_end { 36 | characters.end = p; 37 | 38 | Trenni_append_token(&pcdata, encoding, characters); 39 | } 40 | 41 | action entity_error { 42 | Trenni_raise_error("could not parse entity", buffer, p-s); 43 | } 44 | 45 | action entity_begin { 46 | entity.begin = p; 47 | } 48 | 49 | action entity_name { 50 | entity.end = p; 51 | 52 | has_entities = 1; 53 | 54 | Trenni_append(&pcdata, encoding, 55 | rb_funcall(entities, id_key_get, 1, Trenni_Token_string(entity, encoding)) 56 | ); 57 | } 58 | 59 | action entity_hex { 60 | entity.end = p; 61 | 62 | has_entities = 1; 63 | 64 | codepoint = strtoul(entity.begin, (char **)&entity.end, 16); 65 | 66 | Trenni_append_codepoint(&pcdata, encoding, codepoint); 67 | } 68 | 69 | action entity_number { 70 | entity.end = p; 71 | 72 | has_entities = 1; 73 | 74 | codepoint = strtoul(entity.begin, (char **)&entity.end, 10); 75 | 76 | Trenni_append_codepoint(&pcdata, encoding, codepoint); 77 | } 78 | 79 | action doctype_begin { 80 | doctype.begin = p; 81 | } 82 | 83 | action doctype_end { 84 | doctype.end = p; 85 | 86 | rb_funcall(delegate, id_doctype, 1, Trenni_Token_string(doctype, encoding)); 87 | } 88 | 89 | action doctype_error { 90 | Trenni_raise_error("could not parse doctype", buffer, p-s); 91 | } 92 | 93 | action comment_begin { 94 | comment.begin = p; 95 | } 96 | 97 | action comment_end { 98 | comment.end = p; 99 | 100 | rb_funcall(delegate, id_comment, 1, Trenni_Token_string(comment, encoding)); 101 | } 102 | 103 | action comment_error { 104 | Trenni_raise_error("could not parse comment", buffer, p-s); 105 | } 106 | 107 | action instruction_begin { 108 | instruction.begin = p; 109 | } 110 | 111 | action instruction_text_begin { 112 | } 113 | 114 | action instruction_text_end { 115 | } 116 | 117 | action instruction_end { 118 | instruction.end = p; 119 | 120 | rb_funcall(delegate, id_instruction, 1, Trenni_Token_string(instruction, encoding)); 121 | } 122 | 123 | action instruction_error { 124 | Trenni_raise_error("could not parse instruction", buffer, p-s); 125 | } 126 | 127 | action tag_name { 128 | // Reset self-closing state - we don't know yet. 129 | self_closing = 0; 130 | 131 | rb_funcall(delegate, id_open_tag_begin, 2, Trenni_Token_string(identifier, encoding), ULONG2NUM(identifier.begin-s)); 132 | } 133 | 134 | action tag_opening_begin { 135 | } 136 | 137 | action tag_self_closing { 138 | self_closing = 1; 139 | } 140 | 141 | action attribute_begin { 142 | has_value = 0; 143 | } 144 | 145 | action attribute_value { 146 | has_value = 1; 147 | } 148 | 149 | action attribute_empty { 150 | has_value = 2; 151 | } 152 | 153 | action attribute { 154 | if (has_value == 1) { 155 | rb_funcall(delegate, id_attribute, 2, Trenni_Token_string(identifier, encoding), Trenni_markup_safe(pcdata, has_entities)); 156 | } else if (has_value == 2) { 157 | rb_funcall(delegate, id_attribute, 2, Trenni_Token_string(identifier, encoding), empty_string); 158 | } else { 159 | rb_funcall(delegate, id_attribute, 2, Trenni_Token_string(identifier, encoding), Qtrue); 160 | } 161 | } 162 | 163 | action tag_opening_end { 164 | rb_funcall(delegate, id_open_tag_end, 1, self_closing == 1 ? Qtrue : Qfalse); 165 | } 166 | 167 | action tag_closing_begin { 168 | } 169 | 170 | action tag_closing_end { 171 | rb_funcall(delegate, id_close_tag, 2, Trenni_Token_string(identifier, encoding), ULONG2NUM(identifier.begin-s)); 172 | } 173 | 174 | action tag_error { 175 | Trenni_raise_error("could not parse tag", buffer, p-s); 176 | } 177 | 178 | action cdata_begin { 179 | cdata.begin = p; 180 | } 181 | 182 | action cdata_end { 183 | cdata.end = p; 184 | 185 | rb_funcall(delegate, id_cdata, 1, Trenni_Token_string(cdata, encoding)); 186 | } 187 | 188 | action cdata_error { 189 | Trenni_raise_error("could not parse cdata", buffer, p-s); 190 | } 191 | 192 | include markup "trenni/markup.rl"; 193 | 194 | write data; 195 | }%% 196 | 197 | VALUE Trenni_Native_parse_markup(VALUE self, VALUE buffer, VALUE delegate, VALUE entities) { 198 | VALUE string = rb_funcall(buffer, id_read, 0); 199 | 200 | rb_encoding *encoding = rb_enc_get(string); 201 | 202 | VALUE pcdata = Qnil; 203 | 204 | VALUE empty_string = rb_obj_freeze(rb_enc_str_new(0, 0, encoding)); 205 | 206 | const char *s, *p, *pe, *eof; 207 | unsigned long cs, top = 0, stack[2] = {0}; 208 | unsigned long codepoint = 0; 209 | 210 | Trenni_Token identifier = {0}, cdata = {0}, characters = {0}, entity = {0}, doctype = {0}, comment = {0}, instruction = {0}; 211 | unsigned self_closing = 0, has_value = 0, has_entities = 0; 212 | 213 | s = p = RSTRING_PTR(string); 214 | eof = pe = p + RSTRING_LEN(string); 215 | 216 | %%{ 217 | write init; 218 | write exec; 219 | }%% 220 | 221 | if (p != eof) { 222 | Trenni_raise_error("could not parse all input", buffer, p-s); 223 | } 224 | 225 | return Qnil; 226 | } 227 | -------------------------------------------------------------------------------- /ext/trenni/query.c: -------------------------------------------------------------------------------- 1 | 2 | #line 1 "query.rl" 3 | 4 | #include "query.h" 5 | 6 | 7 | #line 8 "query.c" 8 | static const int Trenni_query_parser_start = 12; 9 | static const int Trenni_query_parser_first_final = 12; 10 | static const int Trenni_query_parser_error = 0; 11 | 12 | static const int Trenni_query_parser_en_main = 12; 13 | 14 | 15 | #line 56 "query.rl" 16 | 17 | 18 | VALUE Trenni_Native_parse_query(VALUE self, VALUE buffer, VALUE delegate) { 19 | VALUE string = rb_funcall(buffer, id_read, 0); 20 | 21 | rb_encoding *encoding = rb_enc_get(string); 22 | 23 | const char *s, *p, *pe, *eof; 24 | unsigned long cs; 25 | 26 | Trenni_Token string_token = {0}, integer_token = {0}, value_token = {0}; 27 | unsigned encoded = 0; 28 | 29 | s = p = RSTRING_PTR(string); 30 | eof = pe = p + RSTRING_LEN(string); 31 | 32 | 33 | #line 34 "query.c" 34 | { 35 | cs = Trenni_query_parser_start; 36 | } 37 | 38 | #line 39 "query.c" 39 | { 40 | if ( p == pe ) 41 | goto _test_eof; 42 | switch ( cs ) 43 | { 44 | case 12: 45 | switch( (*p) ) { 46 | case 37: goto tr4; 47 | case 38: goto st0; 48 | case 43: goto tr5; 49 | case 61: goto st0; 50 | case 91: goto st0; 51 | case 93: goto st0; 52 | } 53 | if ( 48 <= (*p) && (*p) <= 57 ) 54 | goto tr6; 55 | goto tr3; 56 | tr3: 57 | #line 7 "query.rl" 58 | { 59 | string_token.begin = p; 60 | } 61 | goto st13; 62 | tr5: 63 | #line 7 "query.rl" 64 | { 65 | string_token.begin = p; 66 | } 67 | #line 49 "query.rl" 68 | { 69 | encoded = 1; 70 | } 71 | goto st13; 72 | tr8: 73 | #line 49 "query.rl" 74 | { 75 | encoded = 1; 76 | } 77 | goto st13; 78 | st13: 79 | if ( ++p == pe ) 80 | goto _test_eof13; 81 | case 13: 82 | #line 83 "query.c" 83 | switch( (*p) ) { 84 | case 37: goto tr7; 85 | case 38: goto tr24; 86 | case 43: goto tr8; 87 | case 61: goto tr25; 88 | case 91: goto tr26; 89 | case 93: goto st0; 90 | } 91 | goto st13; 92 | tr4: 93 | #line 7 "query.rl" 94 | { 95 | string_token.begin = p; 96 | } 97 | #line 49 "query.rl" 98 | { 99 | encoded = 1; 100 | } 101 | goto st1; 102 | tr7: 103 | #line 49 "query.rl" 104 | { 105 | encoded = 1; 106 | } 107 | goto st1; 108 | st1: 109 | if ( ++p == pe ) 110 | goto _test_eof1; 111 | case 1: 112 | #line 113 "query.c" 113 | if ( (*p) < 65 ) { 114 | if ( 48 <= (*p) && (*p) <= 57 ) 115 | goto st2; 116 | } else if ( (*p) > 70 ) { 117 | if ( 97 <= (*p) && (*p) <= 102 ) 118 | goto st2; 119 | } else 120 | goto st2; 121 | goto st0; 122 | st0: 123 | cs = 0; 124 | goto _out; 125 | st2: 126 | if ( ++p == pe ) 127 | goto _test_eof2; 128 | case 2: 129 | if ( (*p) < 65 ) { 130 | if ( 48 <= (*p) && (*p) <= 57 ) 131 | goto st13; 132 | } else if ( (*p) > 70 ) { 133 | if ( 97 <= (*p) && (*p) <= 102 ) 134 | goto st13; 135 | } else 136 | goto st13; 137 | goto st0; 138 | tr24: 139 | #line 11 "query.rl" 140 | { 141 | string_token.end = p; 142 | 143 | rb_funcall(delegate, id_string, 2, Trenni_Token_string(string_token, encoding), encoded ? Qtrue : Qfalse); 144 | 145 | encoded = 0; 146 | } 147 | #line 45 "query.rl" 148 | { 149 | rb_funcall(delegate, id_pair, 0); 150 | } 151 | goto st3; 152 | tr29: 153 | #line 33 "query.rl" 154 | { 155 | value_token.begin = p; 156 | } 157 | #line 37 "query.rl" 158 | { 159 | value_token.end = p; 160 | 161 | rb_funcall(delegate, id_assign, 2, Trenni_Token_string(value_token, encoding), encoded ? Qtrue : Qfalse); 162 | 163 | encoded = 0; 164 | } 165 | #line 45 "query.rl" 166 | { 167 | rb_funcall(delegate, id_pair, 0); 168 | } 169 | goto st3; 170 | tr32: 171 | #line 37 "query.rl" 172 | { 173 | value_token.end = p; 174 | 175 | rb_funcall(delegate, id_assign, 2, Trenni_Token_string(value_token, encoding), encoded ? Qtrue : Qfalse); 176 | 177 | encoded = 0; 178 | } 179 | #line 45 "query.rl" 180 | { 181 | rb_funcall(delegate, id_pair, 0); 182 | } 183 | goto st3; 184 | tr34: 185 | #line 45 "query.rl" 186 | { 187 | rb_funcall(delegate, id_pair, 0); 188 | } 189 | goto st3; 190 | tr37: 191 | #line 29 "query.rl" 192 | { 193 | rb_funcall(delegate, id_append, 0); 194 | } 195 | #line 45 "query.rl" 196 | { 197 | rb_funcall(delegate, id_pair, 0); 198 | } 199 | goto st3; 200 | st3: 201 | if ( ++p == pe ) 202 | goto _test_eof3; 203 | case 3: 204 | #line 205 "query.c" 205 | switch( (*p) ) { 206 | case 37: goto tr4; 207 | case 38: goto st0; 208 | case 43: goto tr5; 209 | case 61: goto st0; 210 | case 91: goto st0; 211 | case 93: goto st0; 212 | } 213 | if ( 48 <= (*p) && (*p) <= 57 ) 214 | goto tr6; 215 | goto tr3; 216 | tr6: 217 | #line 7 "query.rl" 218 | { 219 | string_token.begin = p; 220 | } 221 | #line 19 "query.rl" 222 | { 223 | integer_token.begin = p; 224 | } 225 | goto st4; 226 | st4: 227 | if ( ++p == pe ) 228 | goto _test_eof4; 229 | case 4: 230 | #line 231 "query.c" 231 | switch( (*p) ) { 232 | case 37: goto tr7; 233 | case 38: goto st0; 234 | case 43: goto tr8; 235 | case 61: goto st0; 236 | case 91: goto st0; 237 | case 93: goto st0; 238 | } 239 | if ( 48 <= (*p) && (*p) <= 57 ) 240 | goto st4; 241 | goto st13; 242 | tr25: 243 | #line 11 "query.rl" 244 | { 245 | string_token.end = p; 246 | 247 | rb_funcall(delegate, id_string, 2, Trenni_Token_string(string_token, encoding), encoded ? Qtrue : Qfalse); 248 | 249 | encoded = 0; 250 | } 251 | goto st14; 252 | tr38: 253 | #line 29 "query.rl" 254 | { 255 | rb_funcall(delegate, id_append, 0); 256 | } 257 | goto st14; 258 | st14: 259 | if ( ++p == pe ) 260 | goto _test_eof14; 261 | case 14: 262 | #line 263 "query.c" 263 | switch( (*p) ) { 264 | case 37: goto tr28; 265 | case 38: goto tr29; 266 | case 43: goto tr30; 267 | case 61: goto st0; 268 | case 91: goto st0; 269 | case 93: goto st0; 270 | } 271 | goto tr27; 272 | tr33: 273 | #line 49 "query.rl" 274 | { 275 | encoded = 1; 276 | } 277 | goto st15; 278 | tr27: 279 | #line 33 "query.rl" 280 | { 281 | value_token.begin = p; 282 | } 283 | goto st15; 284 | tr30: 285 | #line 33 "query.rl" 286 | { 287 | value_token.begin = p; 288 | } 289 | #line 49 "query.rl" 290 | { 291 | encoded = 1; 292 | } 293 | goto st15; 294 | st15: 295 | if ( ++p == pe ) 296 | goto _test_eof15; 297 | case 15: 298 | #line 299 "query.c" 299 | switch( (*p) ) { 300 | case 37: goto tr31; 301 | case 38: goto tr32; 302 | case 43: goto tr33; 303 | case 61: goto st0; 304 | case 91: goto st0; 305 | case 93: goto st0; 306 | } 307 | goto st15; 308 | tr31: 309 | #line 49 "query.rl" 310 | { 311 | encoded = 1; 312 | } 313 | goto st5; 314 | tr28: 315 | #line 33 "query.rl" 316 | { 317 | value_token.begin = p; 318 | } 319 | #line 49 "query.rl" 320 | { 321 | encoded = 1; 322 | } 323 | goto st5; 324 | st5: 325 | if ( ++p == pe ) 326 | goto _test_eof5; 327 | case 5: 328 | #line 329 "query.c" 329 | if ( (*p) < 65 ) { 330 | if ( 48 <= (*p) && (*p) <= 57 ) 331 | goto st6; 332 | } else if ( (*p) > 70 ) { 333 | if ( 97 <= (*p) && (*p) <= 102 ) 334 | goto st6; 335 | } else 336 | goto st6; 337 | goto st0; 338 | st6: 339 | if ( ++p == pe ) 340 | goto _test_eof6; 341 | case 6: 342 | if ( (*p) < 65 ) { 343 | if ( 48 <= (*p) && (*p) <= 57 ) 344 | goto st15; 345 | } else if ( (*p) > 70 ) { 346 | if ( 97 <= (*p) && (*p) <= 102 ) 347 | goto st15; 348 | } else 349 | goto st15; 350 | goto st0; 351 | tr26: 352 | #line 11 "query.rl" 353 | { 354 | string_token.end = p; 355 | 356 | rb_funcall(delegate, id_string, 2, Trenni_Token_string(string_token, encoding), encoded ? Qtrue : Qfalse); 357 | 358 | encoded = 0; 359 | } 360 | goto st7; 361 | st7: 362 | if ( ++p == pe ) 363 | goto _test_eof7; 364 | case 7: 365 | #line 366 "query.c" 366 | switch( (*p) ) { 367 | case 37: goto tr13; 368 | case 38: goto st0; 369 | case 43: goto tr14; 370 | case 61: goto st0; 371 | case 91: goto st0; 372 | case 93: goto st17; 373 | } 374 | if ( 48 <= (*p) && (*p) <= 57 ) 375 | goto tr15; 376 | goto tr12; 377 | tr12: 378 | #line 7 "query.rl" 379 | { 380 | string_token.begin = p; 381 | } 382 | goto st8; 383 | tr14: 384 | #line 7 "query.rl" 385 | { 386 | string_token.begin = p; 387 | } 388 | #line 49 "query.rl" 389 | { 390 | encoded = 1; 391 | } 392 | goto st8; 393 | tr19: 394 | #line 49 "query.rl" 395 | { 396 | encoded = 1; 397 | } 398 | goto st8; 399 | st8: 400 | if ( ++p == pe ) 401 | goto _test_eof8; 402 | case 8: 403 | #line 404 "query.c" 404 | switch( (*p) ) { 405 | case 37: goto tr18; 406 | case 38: goto st0; 407 | case 43: goto tr19; 408 | case 61: goto st0; 409 | case 91: goto st0; 410 | case 93: goto tr20; 411 | } 412 | goto st8; 413 | tr13: 414 | #line 7 "query.rl" 415 | { 416 | string_token.begin = p; 417 | } 418 | #line 49 "query.rl" 419 | { 420 | encoded = 1; 421 | } 422 | goto st9; 423 | tr18: 424 | #line 49 "query.rl" 425 | { 426 | encoded = 1; 427 | } 428 | goto st9; 429 | st9: 430 | if ( ++p == pe ) 431 | goto _test_eof9; 432 | case 9: 433 | #line 434 "query.c" 434 | if ( (*p) < 65 ) { 435 | if ( 48 <= (*p) && (*p) <= 57 ) 436 | goto st10; 437 | } else if ( (*p) > 70 ) { 438 | if ( 97 <= (*p) && (*p) <= 102 ) 439 | goto st10; 440 | } else 441 | goto st10; 442 | goto st0; 443 | st10: 444 | if ( ++p == pe ) 445 | goto _test_eof10; 446 | case 10: 447 | if ( (*p) < 65 ) { 448 | if ( 48 <= (*p) && (*p) <= 57 ) 449 | goto st8; 450 | } else if ( (*p) > 70 ) { 451 | if ( 97 <= (*p) && (*p) <= 102 ) 452 | goto st8; 453 | } else 454 | goto st8; 455 | goto st0; 456 | tr20: 457 | #line 11 "query.rl" 458 | { 459 | string_token.end = p; 460 | 461 | rb_funcall(delegate, id_string, 2, Trenni_Token_string(string_token, encoding), encoded ? Qtrue : Qfalse); 462 | 463 | encoded = 0; 464 | } 465 | goto st16; 466 | tr23: 467 | #line 23 "query.rl" 468 | { 469 | integer_token.end = p; 470 | 471 | rb_funcall(delegate, id_integer, 1, Trenni_Token_string(integer_token, encoding)); 472 | } 473 | goto st16; 474 | st16: 475 | if ( ++p == pe ) 476 | goto _test_eof16; 477 | case 16: 478 | #line 479 "query.c" 479 | switch( (*p) ) { 480 | case 38: goto tr34; 481 | case 61: goto st14; 482 | case 91: goto st7; 483 | } 484 | goto st0; 485 | tr15: 486 | #line 19 "query.rl" 487 | { 488 | integer_token.begin = p; 489 | } 490 | #line 7 "query.rl" 491 | { 492 | string_token.begin = p; 493 | } 494 | goto st11; 495 | st11: 496 | if ( ++p == pe ) 497 | goto _test_eof11; 498 | case 11: 499 | #line 500 "query.c" 500 | switch( (*p) ) { 501 | case 37: goto tr18; 502 | case 38: goto st0; 503 | case 43: goto tr19; 504 | case 61: goto st0; 505 | case 91: goto st0; 506 | case 93: goto tr23; 507 | } 508 | if ( 48 <= (*p) && (*p) <= 57 ) 509 | goto st11; 510 | goto st8; 511 | st17: 512 | if ( ++p == pe ) 513 | goto _test_eof17; 514 | case 17: 515 | switch( (*p) ) { 516 | case 38: goto tr37; 517 | case 61: goto tr38; 518 | } 519 | goto st0; 520 | } 521 | _test_eof13: cs = 13; goto _test_eof; 522 | _test_eof1: cs = 1; goto _test_eof; 523 | _test_eof2: cs = 2; goto _test_eof; 524 | _test_eof3: cs = 3; goto _test_eof; 525 | _test_eof4: cs = 4; goto _test_eof; 526 | _test_eof14: cs = 14; goto _test_eof; 527 | _test_eof15: cs = 15; goto _test_eof; 528 | _test_eof5: cs = 5; goto _test_eof; 529 | _test_eof6: cs = 6; goto _test_eof; 530 | _test_eof7: cs = 7; goto _test_eof; 531 | _test_eof8: cs = 8; goto _test_eof; 532 | _test_eof9: cs = 9; goto _test_eof; 533 | _test_eof10: cs = 10; goto _test_eof; 534 | _test_eof16: cs = 16; goto _test_eof; 535 | _test_eof11: cs = 11; goto _test_eof; 536 | _test_eof17: cs = 17; goto _test_eof; 537 | 538 | _test_eof: {} 539 | if ( p == eof ) 540 | { 541 | switch ( cs ) { 542 | case 16: 543 | #line 45 "query.rl" 544 | { 545 | rb_funcall(delegate, id_pair, 0); 546 | } 547 | break; 548 | case 13: 549 | #line 11 "query.rl" 550 | { 551 | string_token.end = p; 552 | 553 | rb_funcall(delegate, id_string, 2, Trenni_Token_string(string_token, encoding), encoded ? Qtrue : Qfalse); 554 | 555 | encoded = 0; 556 | } 557 | #line 45 "query.rl" 558 | { 559 | rb_funcall(delegate, id_pair, 0); 560 | } 561 | break; 562 | case 17: 563 | #line 29 "query.rl" 564 | { 565 | rb_funcall(delegate, id_append, 0); 566 | } 567 | #line 45 "query.rl" 568 | { 569 | rb_funcall(delegate, id_pair, 0); 570 | } 571 | break; 572 | case 15: 573 | #line 37 "query.rl" 574 | { 575 | value_token.end = p; 576 | 577 | rb_funcall(delegate, id_assign, 2, Trenni_Token_string(value_token, encoding), encoded ? Qtrue : Qfalse); 578 | 579 | encoded = 0; 580 | } 581 | #line 45 "query.rl" 582 | { 583 | rb_funcall(delegate, id_pair, 0); 584 | } 585 | break; 586 | case 14: 587 | #line 33 "query.rl" 588 | { 589 | value_token.begin = p; 590 | } 591 | #line 37 "query.rl" 592 | { 593 | value_token.end = p; 594 | 595 | rb_funcall(delegate, id_assign, 2, Trenni_Token_string(value_token, encoding), encoded ? Qtrue : Qfalse); 596 | 597 | encoded = 0; 598 | } 599 | #line 45 "query.rl" 600 | { 601 | rb_funcall(delegate, id_pair, 0); 602 | } 603 | break; 604 | #line 605 "query.c" 605 | } 606 | } 607 | 608 | _out: {} 609 | } 610 | 611 | #line 75 "query.rl" 612 | 613 | 614 | if (p != eof) { 615 | Trenni_raise_error("could not parse all input", buffer, p-s); 616 | } 617 | 618 | return Qnil; 619 | } 620 | -------------------------------------------------------------------------------- /ext/trenni/query.h: -------------------------------------------------------------------------------- 1 | 2 | #pragma once 3 | 4 | #include "trenni.h" 5 | 6 | VALUE Trenni_Native_parse_query(VALUE self, VALUE string, VALUE delegate); 7 | -------------------------------------------------------------------------------- /ext/trenni/query.rl: -------------------------------------------------------------------------------- 1 | 2 | #include "query.h" 3 | 4 | %%{ 5 | machine Trenni_query_parser; 6 | 7 | action string_begin { 8 | string_token.begin = p; 9 | } 10 | 11 | action string_end { 12 | string_token.end = p; 13 | 14 | rb_funcall(delegate, id_string, 2, Trenni_Token_string(string_token, encoding), encoded ? Qtrue : Qfalse); 15 | 16 | encoded = 0; 17 | } 18 | 19 | action integer_begin { 20 | integer_token.begin = p; 21 | } 22 | 23 | action integer_end { 24 | integer_token.end = p; 25 | 26 | rb_funcall(delegate, id_integer, 1, Trenni_Token_string(integer_token, encoding)); 27 | } 28 | 29 | action append { 30 | rb_funcall(delegate, id_append, 0); 31 | } 32 | 33 | action value_begin { 34 | value_token.begin = p; 35 | } 36 | 37 | action value_end { 38 | value_token.end = p; 39 | 40 | rb_funcall(delegate, id_assign, 2, Trenni_Token_string(value_token, encoding), encoded ? Qtrue : Qfalse); 41 | 42 | encoded = 0; 43 | } 44 | 45 | action pair { 46 | rb_funcall(delegate, id_pair, 0); 47 | } 48 | 49 | action encoded { 50 | encoded = 1; 51 | } 52 | 53 | include query "trenni/query.rl"; 54 | 55 | write data; 56 | }%% 57 | 58 | VALUE Trenni_Native_parse_query(VALUE self, VALUE buffer, VALUE delegate) { 59 | VALUE string = rb_funcall(buffer, id_read, 0); 60 | 61 | rb_encoding *encoding = rb_enc_get(string); 62 | 63 | const char *s, *p, *pe, *eof; 64 | unsigned long cs; 65 | 66 | Trenni_Token string_token = {0}, integer_token = {0}, value_token = {0}; 67 | unsigned encoded = 0; 68 | 69 | s = p = RSTRING_PTR(string); 70 | eof = pe = p + RSTRING_LEN(string); 71 | 72 | %%{ 73 | write init; 74 | write exec; 75 | }%% 76 | 77 | if (p != eof) { 78 | Trenni_raise_error("could not parse all input", buffer, p-s); 79 | } 80 | 81 | return Qnil; 82 | } 83 | -------------------------------------------------------------------------------- /ext/trenni/tag.c: -------------------------------------------------------------------------------- 1 | 2 | #include "escape.h" 3 | #include "tag.h" 4 | 5 | VALUE Trenni_Tag_split(VALUE self, VALUE qualified_name) { 6 | const char * begin = RSTRING_PTR(qualified_name); 7 | const char * end = RSTRING_END(qualified_name); 8 | 9 | const char * p = begin; 10 | 11 | while (p != end) { 12 | if (*p == ':') { 13 | return rb_ary_new_from_args(2, 14 | rb_enc_str_new(begin, p-begin, rb_enc_get(qualified_name)), 15 | rb_enc_str_new(p+1, end-p-1, rb_enc_get(qualified_name)) 16 | ); 17 | } 18 | 19 | p += 1; 20 | } 21 | 22 | return rb_ary_new_from_args(2, Qnil, qualified_name); 23 | } 24 | 25 | inline static int Trenni_Tag_valid_attributes(VALUE value) { 26 | return (rb_type(value) == T_HASH) || (rb_type(value) == T_ARRAY); 27 | } 28 | 29 | // Key can be either symbol or string. This method efficiently converts either to a string. 30 | inline static VALUE Trenni_Tag_key_string(VALUE key) { 31 | if (SYMBOL_P(key)) { 32 | return rb_sym2str(key); 33 | } 34 | 35 | StringValue(key); 36 | return key; 37 | } 38 | 39 | inline static VALUE Trenni_Tag_prefix_key(VALUE prefix, VALUE key) { 40 | VALUE buffer; 41 | 42 | if (prefix == Qnil) { 43 | return Trenni_Tag_key_string(key); 44 | } 45 | 46 | buffer = rb_str_dup(Trenni_Tag_key_string(prefix)); 47 | rb_str_cat_cstr(buffer, "-"); 48 | rb_str_append(buffer, Trenni_Tag_key_string(key)); 49 | 50 | return buffer; 51 | } 52 | 53 | VALUE Trenni_Tag_append_attributes(VALUE self, VALUE buffer, VALUE attributes, VALUE prefix); 54 | 55 | static void Trenni_Tag_append_tag_attribute(VALUE buffer, VALUE key, VALUE value, VALUE prefix) { 56 | // We skip over attributes with nil value: 57 | if (value == Qnil || value == Qfalse) return; 58 | 59 | // Modify key to contain the prefix: 60 | key = Trenni_Tag_prefix_key(prefix, key); 61 | 62 | if (Trenni_Tag_valid_attributes(value)) { 63 | Trenni_Tag_append_attributes(Qnil, buffer, value, key); 64 | } else { 65 | rb_str_cat_cstr(buffer, " "); 66 | rb_str_append(buffer, key); 67 | 68 | if (value != Qtrue) { 69 | rb_str_cat_cstr(buffer, "=\""); 70 | Trenni_Markup_append(Qnil, buffer, value); 71 | rb_str_cat_cstr(buffer, "\""); 72 | } 73 | } 74 | } 75 | 76 | typedef struct { 77 | VALUE buffer; 78 | VALUE prefix; 79 | } Trenni_Tag_Accumulation; 80 | 81 | static int Trenni_Tag_append_tag_attribute_foreach(VALUE key, VALUE value, VALUE _argument) { 82 | Trenni_Tag_Accumulation * argument = (Trenni_Tag_Accumulation *)_argument; 83 | 84 | Trenni_Tag_append_tag_attribute(argument->buffer, key, value, argument->prefix); 85 | 86 | return ST_CONTINUE; 87 | } 88 | 89 | VALUE Trenni_Tag_append_attributes(VALUE self, VALUE buffer, VALUE attributes, VALUE prefix) { 90 | int type = rb_type(attributes); 91 | 92 | if (type == T_HASH) { 93 | Trenni_Tag_Accumulation argument = {buffer, prefix}; 94 | rb_hash_foreach(attributes, &Trenni_Tag_append_tag_attribute_foreach, (VALUE)&argument); 95 | } else if (type == T_ARRAY) { 96 | long i; 97 | 98 | for (i = 0; i < RARRAY_LEN(attributes); i++) { 99 | VALUE attribute = RARRAY_AREF(attributes, i); 100 | VALUE key = RARRAY_AREF(attribute, 0); 101 | VALUE value = RARRAY_AREF(attribute, 1); 102 | 103 | Trenni_Tag_append_tag_attribute(buffer, key, value, prefix); 104 | } 105 | } else { 106 | rb_raise(rb_eArgError, "expected hash or array for attributes"); 107 | } 108 | 109 | return Qnil; 110 | } 111 | 112 | VALUE Trenni_Tag_append_tag(VALUE self, VALUE buffer, VALUE name, VALUE attributes, VALUE content) { 113 | StringValue(name); 114 | 115 | rb_str_cat_cstr(buffer, "<"); 116 | rb_str_buf_append(buffer, name); 117 | 118 | Trenni_Tag_append_attributes(self, buffer, attributes, Qnil); 119 | 120 | if (content == Qnil || content == Qfalse) { 121 | rb_str_cat_cstr(buffer, "/>"); 122 | } else { 123 | rb_str_cat_cstr(buffer, ">"); 124 | 125 | if (content != Qtrue) { 126 | Trenni_Markup_append(self, buffer, content); 127 | } 128 | 129 | rb_str_cat_cstr(buffer, ""); 132 | } 133 | 134 | return Qnil; 135 | } 136 | 137 | VALUE Trenni_Tag_format_tag(VALUE self, VALUE name, VALUE attributes, VALUE content) { 138 | rb_encoding *encoding = rb_enc_get(name); 139 | 140 | VALUE buffer = rb_enc_str_new(0, 0, encoding); 141 | rb_str_reserve(buffer, 256); 142 | 143 | Trenni_Tag_append_tag(self, buffer, name, attributes, content); 144 | 145 | return buffer; 146 | } 147 | 148 | VALUE Trenni_Tag_write_opening_tag(VALUE self, VALUE buffer) { 149 | VALUE name = rb_struct_getmember(self, id_name); 150 | VALUE attributes = rb_struct_getmember(self, id_attributes); 151 | VALUE closed = rb_struct_getmember(self, id_closed); 152 | 153 | StringValue(name); 154 | 155 | rb_str_reserve(buffer, RSTRING_LEN(name) + 256); 156 | 157 | rb_str_cat_cstr(buffer, "<"); 158 | rb_str_buf_append(buffer, name); 159 | 160 | Trenni_Tag_append_attributes(self, buffer, attributes, Qnil); 161 | 162 | if (closed == Qtrue) { 163 | rb_str_cat_cstr(buffer, "/>"); 164 | } else { 165 | rb_str_cat_cstr(buffer, ">"); 166 | } 167 | 168 | return Qnil; 169 | } 170 | 171 | VALUE Trenni_Tag_write_closing_tag(VALUE self, VALUE buffer) { 172 | VALUE name = rb_struct_getmember(self, id_name); 173 | 174 | StringValue(name); 175 | 176 | rb_str_reserve(buffer, RSTRING_LEN(name) + 3); 177 | 178 | rb_str_cat_cstr(buffer, ""); 181 | 182 | return Qnil; 183 | } 184 | 185 | void Init_trenni_tag() { 186 | rb_undef_method(rb_class_of(rb_Trenni_Tag), "append_attributes"); 187 | rb_define_singleton_method(rb_Trenni_Tag, "append_attributes", Trenni_Tag_append_attributes, 3); 188 | 189 | rb_undef_method(rb_class_of(rb_Trenni_Tag), "append_tag"); 190 | rb_define_singleton_method(rb_Trenni_Tag, "append_tag", Trenni_Tag_append_tag, 4); 191 | 192 | rb_undef_method(rb_class_of(rb_Trenni_Tag), "format_tag"); 193 | rb_define_singleton_method(rb_Trenni_Tag, "format_tag", Trenni_Tag_format_tag, 3); 194 | 195 | rb_undef_method(rb_class_of(rb_Trenni_Tag), "split"); 196 | rb_define_singleton_method(rb_Trenni_Tag, "split", Trenni_Tag_split, 1); 197 | 198 | rb_undef_method(rb_Trenni_Tag, "write_opening_tag"); 199 | rb_define_method(rb_Trenni_Tag, "write_opening_tag", Trenni_Tag_write_opening_tag, 1); 200 | 201 | rb_undef_method(rb_Trenni_Tag, "write_closing_tag"); 202 | rb_define_method(rb_Trenni_Tag, "write_closing_tag", Trenni_Tag_write_closing_tag, 1); 203 | } 204 | 205 | -------------------------------------------------------------------------------- /ext/trenni/tag.h: -------------------------------------------------------------------------------- 1 | 2 | #pragma once 3 | 4 | #include "trenni.h" 5 | 6 | void Init_trenni_tag(); 7 | 8 | // Split a qualified name `namespace:name` into it's components. Return `[nil, name]` if no namespace is present. 9 | // Usage: namespace, name = Trenni::Tag.split(qualified_name) 10 | VALUE Trenni_Tag_split(VALUE self, VALUE name); 11 | 12 | // Append attributes to the buffer, e.g. {data: {id: 10}} => ' data-id="10"' 13 | VALUE Trenni_Tag_append_attributes(VALUE self, VALUE buffer, VALUE attributes, VALUE prefix); 14 | // Append a full tag with content to the buffer. 15 | VALUE Trenni_Tag_append_tag(VALUE self, VALUE buffer, VALUE name, VALUE attributes, VALUE content); 16 | // Same as append but returns the result. Slightly less efficient. 17 | VALUE Trenni_Tag_format_tag(VALUE self, VALUE name, VALUE attributes, VALUE content); 18 | 19 | // Improve performance of Trenni::Tag#write_opening_tag and #write_closing_tag 20 | VALUE Trenni_Tag_write_opening_tag(VALUE self, VALUE buffer); 21 | VALUE Trenni_Tag_write_closing_tag(VALUE self, VALUE buffer); 22 | -------------------------------------------------------------------------------- /ext/trenni/template.h: -------------------------------------------------------------------------------- 1 | 2 | #pragma once 3 | 4 | #include "trenni.h" 5 | 6 | VALUE Trenni_Native_parse_template(VALUE self, VALUE buffer, VALUE delegate); 7 | -------------------------------------------------------------------------------- /ext/trenni/template.rl: -------------------------------------------------------------------------------- 1 | 2 | #include "template.h" 3 | 4 | %%{ 5 | machine Trenni_template_parser; 6 | 7 | action instruction_begin { 8 | instruction.begin = p; 9 | } 10 | 11 | action instruction_end { 12 | instruction.end = p; 13 | } 14 | 15 | action emit_instruction { 16 | rb_funcall(delegate, id_instruction, 1, Trenni_Token_string(instruction, encoding)); 17 | } 18 | 19 | action emit_instruction_line { 20 | rb_funcall(delegate, id_instruction, 2, Trenni_Token_string(instruction, encoding), newline); 21 | } 22 | 23 | action instruction_error { 24 | Trenni_raise_error("failed to parse instruction", buffer, p-s); 25 | } 26 | 27 | action expression_begin { 28 | expression.begin = p; 29 | } 30 | 31 | action expression_end { 32 | expression.end = p; 33 | } 34 | 35 | action emit_expression { 36 | rb_funcall(delegate, id_expression, 1, Trenni_Token_string(expression, encoding)); 37 | } 38 | 39 | action expression_error { 40 | Trenni_raise_error("failed to parse expression", buffer, p-s); 41 | } 42 | 43 | action emit_text { 44 | rb_funcall(delegate, id_text, 1, Trenni_string(ts, te, encoding)); 45 | } 46 | 47 | include template "trenni/template.rl"; 48 | 49 | write data; 50 | }%% 51 | 52 | VALUE Trenni_Native_parse_template(VALUE self, VALUE buffer, VALUE delegate) { 53 | VALUE string = rb_funcall(buffer, id_read, 0); 54 | 55 | rb_encoding *encoding = rb_enc_get(string); 56 | 57 | VALUE newline = rb_obj_freeze(rb_enc_str_new("\n", 1, encoding)); 58 | 59 | const char *s, *p, *pe, *eof, *ts, *te; 60 | unsigned long cs, act, top = 0, stack[32] = {0}; 61 | 62 | Trenni_Token expression = {0}, instruction = {0}; 63 | 64 | s = p = RSTRING_PTR(string); 65 | eof = pe = p + RSTRING_LEN(string); 66 | 67 | %%{ 68 | write init; 69 | write exec; 70 | }%% 71 | 72 | if (p != eof) { 73 | Trenni_raise_error("could not parse all input", buffer, p-s); 74 | } 75 | 76 | return Qnil; 77 | } 78 | -------------------------------------------------------------------------------- /ext/trenni/trenni.c: -------------------------------------------------------------------------------- 1 | 2 | #include "trenni.h" 3 | 4 | #include "markup.h" 5 | #include "template.h" 6 | #include "query.h" 7 | #include "tag.h" 8 | #include "escape.h" 9 | 10 | VALUE rb_Trenni = Qnil, rb_Trenni_Native = Qnil, rb_Trenni_Tag = Qnil, rb_Trenni_Markup = Qnil, rb_Trenni_MarkupString = Qnil, rb_Trenni_ParseError = Qnil; 11 | ID id_cdata, id_open_tag_begin, id_open_tag_end, id_attribute, id_close_tag, id_text, id_doctype, id_comment, id_instruction, id_read, id_expression, id_key_get, id_string, id_integer, id_append, id_assign, id_pair, id_new, id_name, id_attributes, id_closed, id_to_s, id_is_a; 12 | 13 | void Trenni_raise_error(const char * message, VALUE buffer, size_t offset) { 14 | VALUE exception = rb_funcall(rb_Trenni_ParseError, id_new, 3, rb_str_new_cstr(message), buffer, ULONG2NUM(offset)); 15 | 16 | rb_exc_raise(exception); 17 | } 18 | 19 | void Init_trenni() { 20 | id_open_tag_begin = rb_intern("open_tag_begin"); 21 | id_open_tag_end = rb_intern("open_tag_end"); 22 | id_close_tag = rb_intern("close_tag"); 23 | 24 | id_cdata = rb_intern("cdata"); 25 | id_attribute = rb_intern("attribute"); 26 | id_comment = rb_intern("comment"); 27 | id_text = rb_intern("text"); 28 | id_doctype = rb_intern("doctype"); 29 | id_instruction = rb_intern("instruction"); 30 | id_expression = rb_intern("expression"); 31 | 32 | id_read = rb_intern("read"); 33 | id_new = rb_intern("new"); 34 | 35 | id_name = rb_intern("name"); 36 | id_attributes = rb_intern("attributes"); 37 | id_closed = rb_intern("closed"); 38 | 39 | id_key_get = rb_intern("[]"); 40 | 41 | id_string = rb_intern("string"); 42 | id_integer = rb_intern("integer"); 43 | id_append = rb_intern("append"); 44 | id_assign = rb_intern("assign"); 45 | id_pair = rb_intern("pair"); 46 | 47 | id_to_s = rb_intern("to_s"); 48 | id_is_a = rb_intern("is_a?"); 49 | 50 | rb_Trenni = rb_define_module("Trenni"); 51 | rb_gc_register_mark_object(rb_Trenni); 52 | 53 | rb_Trenni_Markup = rb_define_module_under(rb_Trenni, "Markup"); 54 | rb_gc_register_mark_object(rb_Trenni_Markup); 55 | 56 | rb_Trenni_Native = rb_define_module_under(rb_Trenni, "Native"); 57 | rb_gc_register_mark_object(rb_Trenni_Native); 58 | 59 | Init_trenni_escape(); 60 | 61 | rb_Trenni_ParseError = rb_const_get_at(rb_Trenni, rb_intern("ParseError")); 62 | rb_gc_register_mark_object(rb_Trenni_ParseError); 63 | 64 | rb_define_module_function(rb_Trenni_Native, "parse_markup", Trenni_Native_parse_markup, 3); 65 | rb_define_module_function(rb_Trenni_Native, "parse_template", Trenni_Native_parse_template, 2); 66 | rb_define_module_function(rb_Trenni_Native, "parse_query", Trenni_Native_parse_query, 2); 67 | 68 | rb_Trenni_Tag = rb_const_get_at(rb_Trenni, rb_intern("Tag")); 69 | rb_gc_register_mark_object(rb_Trenni_Tag); 70 | 71 | Init_trenni_tag(); 72 | } 73 | -------------------------------------------------------------------------------- /ext/trenni/trenni.h: -------------------------------------------------------------------------------- 1 | 2 | #pragma once 3 | 4 | #include "ruby.h" 5 | #include 6 | 7 | // Used to efficiently convert symbols to strings (e.g. tag attribute keys). 8 | #ifndef HAVE_RB_SYM2STR 9 | #define rb_sym2str(sym) rb_id2str(SYM2ID(sym)) 10 | #endif 11 | 12 | // Consistent and meaningful append cstring to ruby string/buffer. 13 | #ifndef HAVE_RB_STR_CAT_CSTR 14 | #define rb_str_cat_cstr rb_str_cat2 15 | #endif 16 | 17 | // Prefer non-generic macro names where possible. 18 | #ifndef RB_IMMEDIATE_P 19 | #define RB_IMMEDIATE_P IMMEDIATE_P 20 | #endif 21 | 22 | // A helper to reserve a specific capacity of data for a buffer. 23 | #ifndef HAVE_RB_STR_RESERVE 24 | inline VALUE rb_str_reserve(VALUE string, long extra) { 25 | long actual = RSTRING_LEN(string); 26 | rb_str_resize(string, actual + extra); 27 | rb_str_set_len(string, actual); 28 | return string; 29 | } 30 | #endif 31 | 32 | // Modules and classes exposed by Trenni. 33 | extern VALUE 34 | rb_Trenni, 35 | rb_Trenni_Markup, 36 | rb_Trenni_Tag, 37 | rb_Trenni_MarkupString, 38 | rb_Trenni_Native, 39 | rb_Trenni_ParseError; 40 | 41 | // Symbols used for delegate callbacks and general function calls. 42 | extern ID 43 | id_cdata, 44 | id_open_tag_begin, 45 | id_open_tag_end, 46 | id_attribute, 47 | id_close_tag, 48 | id_text, 49 | id_doctype, 50 | id_comment, 51 | id_instruction, 52 | id_read, 53 | id_expression, 54 | id_key_get, 55 | id_new, 56 | id_name, 57 | id_integer, 58 | id_string, 59 | id_append, 60 | id_assign, 61 | id_pair, 62 | id_attributes, 63 | id_closed, 64 | id_to_s, 65 | id_is_a; 66 | 67 | // A convenient C string token class. 68 | typedef struct { 69 | const char * begin; 70 | const char * end; 71 | } Trenni_Token; 72 | 73 | // Convert a token to a Ruby string. 74 | static inline VALUE Trenni_Token_string(Trenni_Token token, rb_encoding * encoding) { 75 | return rb_enc_str_new(token.begin, token.end - token.begin, encoding); 76 | } 77 | 78 | // Convert a C string to a Ruby string. 79 | static inline VALUE Trenni_string(const char * begin, const char * end, rb_encoding * encoding) { 80 | return rb_enc_str_new(begin, end - begin, encoding); 81 | } 82 | 83 | // Create an empty buffer for the given input string. 84 | static inline VALUE Trenni_buffer_for(VALUE string) { 85 | VALUE buffer = rb_enc_str_new(0, 0, rb_enc_get(string)); 86 | 87 | rb_str_reserve(buffer, RSTRING_LEN(string) + 128); 88 | 89 | return buffer; 90 | } 91 | 92 | // Raise a parse error for the given input buffer at a specific offset. 93 | NORETURN(void Trenni_raise_error(const char * message, VALUE buffer, size_t offset)); 94 | 95 | // Append a string to a buffer. The buffer may or may not be initialized. 96 | static inline void Trenni_append(VALUE * buffer, rb_encoding * encoding, VALUE string) { 97 | if (*buffer == Qnil) { 98 | *buffer = rb_enc_str_new(0, 0, encoding); 99 | } 100 | 101 | rb_str_concat(*buffer, string); 102 | } 103 | 104 | // Append a token to a buffer. The buffer may or may not be initialized. 105 | static inline void Trenni_append_token(VALUE * buffer, rb_encoding * encoding, Trenni_Token token) { 106 | if (*buffer == Qnil) { 107 | // Allocate a buffer exactly the right size: 108 | *buffer = rb_enc_str_new(token.begin, token.end - token.begin, encoding); 109 | } else { 110 | // Append the characters to the existing buffer: 111 | rb_str_buf_cat(*buffer, token.begin, token.end - token.begin); 112 | } 113 | } 114 | 115 | // Append a (unicode) codepoint to a buffer. The buffer may or may not be initialized. 116 | static inline void Trenni_append_codepoint(VALUE * buffer, rb_encoding * encoding, unsigned long codepoint) { 117 | if (*buffer == Qnil) { 118 | *buffer = rb_enc_str_new(0, 0, encoding); 119 | } 120 | 121 | rb_str_concat(*buffer, ULONG2NUM(codepoint)); 122 | } 123 | 124 | // Convert the class of a string if there were no entities detected. 125 | static inline VALUE Trenni_markup_safe(VALUE string, unsigned has_entities) { 126 | if (!has_entities) { 127 | // Apparently should not use this to change klass, but it's exactly what we need here to make things lightning fast. 128 | rb_obj_reveal(string, rb_Trenni_MarkupString); 129 | } 130 | 131 | return string; 132 | } 133 | -------------------------------------------------------------------------------- /gems.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source 'https://rubygems.org' 4 | 5 | gemspec 6 | 7 | group :test do 8 | gem 'ruby-prof', platforms: [:mri] 9 | gem "benchmark-ips" 10 | 11 | gem "rack" 12 | 13 | # For comparisons: 14 | gem "nokogiri" 15 | end 16 | 17 | group :maintenance, optional: true do 18 | gem "bake-gem" 19 | gem "bake-modernize" 20 | end 21 | -------------------------------------------------------------------------------- /lib/trenni.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Copyright, 2012, by Samuel G. D. Williams. 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | # THE SOFTWARE. 22 | 23 | require_relative 'trenni/native' 24 | require_relative 'trenni/builder' 25 | require_relative 'trenni/template' 26 | 27 | require_relative 'trenni/reference' 28 | -------------------------------------------------------------------------------- /lib/trenni/buffer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Copyright, 2016, by Samuel G. D. Williams. 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | # THE SOFTWARE. 22 | 23 | module Trenni 24 | class Buffer 25 | def initialize(string, path: '') 26 | @string = string 27 | @path = path 28 | end 29 | 30 | attr :path 31 | 32 | def encoding 33 | @string.encoding 34 | end 35 | 36 | def read 37 | @string 38 | end 39 | 40 | def self.load_file(path) 41 | FileBuffer.new(path).freeze 42 | end 43 | 44 | def self.load(string) 45 | Buffer.new(string).freeze 46 | end 47 | 48 | def to_buffer 49 | self 50 | end 51 | end 52 | 53 | class FileBuffer 54 | def initialize(path) 55 | @path = path 56 | end 57 | 58 | def freeze 59 | return self if frozen? 60 | 61 | read 62 | 63 | super 64 | end 65 | 66 | attr :path 67 | 68 | def encoding 69 | read.encoding 70 | end 71 | 72 | def read 73 | @cache ||= File.read(@path).freeze 74 | end 75 | 76 | def to_buffer 77 | Buffer.new(self.read, @path) 78 | end 79 | end 80 | 81 | class IOBuffer 82 | def initialize(io, path: io.inspect) 83 | @io = io 84 | @path = path 85 | end 86 | 87 | def freeze 88 | return self if frozen? 89 | 90 | read 91 | 92 | super 93 | end 94 | 95 | attr :path 96 | 97 | def encoding 98 | read.encoding 99 | end 100 | 101 | def read 102 | @cache ||= @io.read.freeze 103 | end 104 | 105 | def to_buffer 106 | Buffer.new(self.read, path: @path) 107 | end 108 | end 109 | 110 | def self.Buffer(value) 111 | case value 112 | when String 113 | Buffer.new(value) 114 | when Buffer, FileBuffer, IOBuffer 115 | value 116 | else 117 | value.to_buffer 118 | end 119 | end 120 | end 121 | -------------------------------------------------------------------------------- /lib/trenni/builder.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Copyright, 2012, by Samuel G. D. Williams. 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | # THE SOFTWARE. 22 | 23 | require_relative 'markup' 24 | require_relative 'tag' 25 | 26 | module Trenni 27 | # Build markup quickly and efficiently. 28 | class Builder 29 | include Markup 30 | 31 | INDENT = "\t" 32 | 33 | class Fragment 34 | def initialize(block) 35 | @block = block 36 | @builder = nil 37 | end 38 | 39 | def call(builder) 40 | @block.call(builder) 41 | end 42 | 43 | def to_s 44 | unless @builder 45 | @builder = Builder.new 46 | 47 | self.call(@builder) 48 | end 49 | 50 | return @builder.to_s 51 | end 52 | 53 | def == other 54 | # This is a bit of a hack... but is required for existing specs to pass: 55 | self.to_s == other.to_s 56 | end 57 | end 58 | 59 | # A helper to generate fragments of markup. 60 | def self.fragment(output = nil, &block) 61 | if output.is_a?(Binding) 62 | output = Template.buffer(output) 63 | end 64 | 65 | if output.nil? 66 | return Fragment.new(block) 67 | end 68 | 69 | if output.is_a?(Builder) 70 | block.call(output) 71 | else 72 | block.call(Builder.new(output)) 73 | end 74 | 75 | return nil 76 | end 77 | 78 | def self.tag(name, content, **attributes) 79 | self.fragment do |builder| 80 | builder.inline(name, attributes) do 81 | builder.text(content) 82 | end 83 | end 84 | end 85 | 86 | def initialize(output = nil, indent: true, encoding: Encoding::UTF_8) 87 | # This field gets togged in #inline so we keep track of it separately from @indentation. 88 | @indent = indent 89 | 90 | # We don't need to use MarkupString here as Builder itself is considered markup and should be inserted directly into the output stream. 91 | @output = output || MarkupString.new.force_encoding(encoding) 92 | 93 | @level = [0] 94 | @children = [0] 95 | end 96 | 97 | attr :output 98 | 99 | def encoding 100 | @output.encoding 101 | end 102 | 103 | # Required for output to buffer. 104 | def to_str 105 | @output 106 | end 107 | 108 | alias to_s to_str 109 | 110 | def == other 111 | @output == String(other) 112 | end 113 | 114 | def indentation 115 | if @indent 116 | INDENT * (@level.size - 1) 117 | else 118 | '' 119 | end 120 | end 121 | 122 | def doctype(attributes = 'html') 123 | @output << "\n" 124 | end 125 | 126 | # Begin a block tag. 127 | def tag(name, attributes = {}, &block) 128 | full_tag(name, attributes, @indent, @indent, &block) 129 | end 130 | 131 | # Begin an inline tag. 132 | def inline_tag(name, attributes = {}, &block) 133 | original_indent = @indent 134 | 135 | full_tag(name, attributes, @indent, false) do 136 | @indent = false 137 | yield if block_given? 138 | end 139 | ensure 140 | @indent = original_indent 141 | end 142 | 143 | alias inline inline_tag 144 | 145 | def inline! 146 | original_indent = @indent 147 | @indent = false 148 | 149 | yield 150 | ensure 151 | @indent = original_indent 152 | end 153 | 154 | def text(content) 155 | return unless content 156 | 157 | if @indent 158 | @output << "\n" if @level.last > 0 159 | @output << indentation 160 | end 161 | 162 | Markup.append(@output, content) 163 | 164 | if @indent 165 | @output << "\n" 166 | end 167 | end 168 | 169 | def raw(content) 170 | @output << content 171 | end 172 | 173 | def <<(content) 174 | return unless content 175 | 176 | if content.is_a?(Fragment) 177 | inline! do 178 | content.call(self) 179 | end 180 | else 181 | Markup.append(@output, content) 182 | end 183 | end 184 | 185 | # Append pre-existing markup: 186 | def append(value) 187 | return unless value 188 | 189 | # The parent has one more child: 190 | @level[-1] += 1 191 | 192 | if @indent 193 | value.each_line.with_index do |line, i| 194 | @output << indentation << line 195 | end 196 | else 197 | @output << value 198 | end 199 | end 200 | 201 | protected 202 | 203 | # A normal block level/container tag. 204 | def full_tag(name, attributes, indent_outer, indent_inner, &block) 205 | if block_given? 206 | if indent_outer 207 | @output << "\n" if @level.last > 0 208 | @output << indentation 209 | end 210 | 211 | tag = Trenni::Tag.opened(name.to_s, attributes) 212 | tag.write_opening_tag(@output) 213 | @output << "\n" if indent_inner 214 | 215 | # The parent has one more child: 216 | @level[-1] += 1 217 | 218 | @level << 0 219 | 220 | yield 221 | 222 | children = @level.pop 223 | 224 | if indent_inner 225 | @output << "\n" if children > 0 226 | @output << indentation 227 | end 228 | 229 | tag.write_closing_tag(@output) 230 | else 231 | # The parent has one more child: 232 | @level[-1] += 1 233 | 234 | @output << indentation 235 | Trenni::Tag.append_tag(@output, name.to_s, attributes, nil) 236 | end 237 | end 238 | end 239 | end 240 | -------------------------------------------------------------------------------- /lib/trenni/entities.trenni: -------------------------------------------------------------------------------- 1 | # Copyright, 2016, by Samuel G. D. Williams. 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | 21 | module Trenni 22 | # We only support a small subset of markup entities. 23 | module Entities 24 | HTML5 = { 25 | 26 | #{string.gsub(/^&|;$/, '').inspect} => #{metadata['characters'].dump}, # #{metadata['characters'].inspect} 27 | 28 | } 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/trenni/error.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Copyright, 2012, by Samuel G. D. Williams. 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | # THE SOFTWARE. 22 | 23 | require_relative 'buffer' 24 | 25 | module Trenni 26 | class Error < StandardError 27 | end 28 | 29 | class ParseError < Error 30 | def initialize(message, buffer, offset) 31 | super(message) 32 | 33 | @buffer = buffer 34 | @offset = offset 35 | end 36 | 37 | def location 38 | @location ||= Location.new(@buffer.read, @offset) 39 | end 40 | 41 | attr :buffer 42 | attr :path 43 | 44 | def to_s 45 | "#{buffer.path}#{location}: #{super}\n#{location.line_text}" 46 | end 47 | end 48 | 49 | class Location 50 | def initialize(input, offset) 51 | raise ArgumentError.new("Offset #{index} is past end of input #{input.bytesize}") if offset > input.bytesize 52 | 53 | @offset = offset 54 | @line_index = 0 55 | line_offset = next_line_offset = 0 56 | 57 | input.each_line do |line| 58 | line_offset = next_line_offset 59 | next_line_offset += line.bytesize 60 | 61 | # Is our input offset within this line? 62 | if next_line_offset >= offset 63 | @line_text = line.chomp 64 | @line_range = line_offset...next_line_offset 65 | break 66 | else 67 | @line_index += 1 68 | end 69 | end 70 | end 71 | 72 | def to_i 73 | @offset 74 | end 75 | 76 | def to_s 77 | "[#{self.line_number}:#{self.line_offset}]" 78 | end 79 | 80 | # The line that contains the @offset (base 0 indexing). 81 | attr :line_index 82 | 83 | # The line index, but base-1. 84 | def line_number 85 | @line_index + 1 86 | end 87 | 88 | # The byte offset to the start of that line. 89 | attr :line_range 90 | 91 | # The number of bytes from the start of the line to the given offset in the input. 92 | def line_offset 93 | @offset - @line_range.min 94 | end 95 | 96 | attr :line_text 97 | end 98 | end 99 | -------------------------------------------------------------------------------- /lib/trenni/fallback/markup.rl: -------------------------------------------------------------------------------- 1 | # Copyright, 2016, by Samuel G. D. Williams. 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | 21 | %%{ 22 | machine markup; 23 | 24 | action identifier_begin { 25 | identifier_begin = p 26 | } 27 | 28 | action identifier_end { 29 | identifier_end = p 30 | } 31 | 32 | action pcdata_begin { 33 | pcdata = "" 34 | has_entities = false 35 | } 36 | 37 | action pcdata_end { 38 | } 39 | 40 | action text_begin { 41 | } 42 | 43 | action text_end { 44 | pcdata = MarkupString.raw(pcdata) unless has_entities 45 | 46 | delegate.text(pcdata) 47 | } 48 | 49 | action characters_begin { 50 | characters_begin = p 51 | } 52 | 53 | action characters_end { 54 | characters_end = p 55 | 56 | pcdata << data.byteslice(characters_begin...characters_end) 57 | } 58 | 59 | action entity_error { 60 | raise ParseError.new("could not parse entity", buffer, p) 61 | } 62 | 63 | action entity_begin { 64 | entity_begin = p 65 | } 66 | 67 | action entity_name { 68 | entity_end = p 69 | 70 | name = data.byteslice(entity_begin...entity_end) 71 | 72 | has_entities = true 73 | pcdata << entities[name] 74 | } 75 | 76 | action entity_hex { 77 | entity_end = p 78 | 79 | has_entities = true 80 | pcdata << data.byteslice(entity_begin...entity_end).to_i(16) 81 | } 82 | 83 | action entity_number { 84 | entity_end = p 85 | 86 | has_entities = true 87 | pcdata << data.byteslice(entity_begin...entity_end).to_i(10) 88 | } 89 | 90 | action doctype_begin { 91 | doctype_begin = p 92 | } 93 | 94 | action doctype_end { 95 | doctype_end = p 96 | 97 | delegate.doctype(data.byteslice(doctype_begin...doctype_end)) 98 | } 99 | 100 | action doctype_error { 101 | raise ParseError.new("could not parse doctype", buffer, p) 102 | } 103 | 104 | action comment_begin { 105 | comment_begin = p 106 | } 107 | 108 | action comment_end { 109 | comment_end = p 110 | 111 | delegate.comment(data.byteslice(comment_begin...comment_end)) 112 | } 113 | 114 | action comment_error { 115 | raise ParseError.new("could not parse comment", buffer, p) 116 | } 117 | 118 | action instruction_begin { 119 | instruction_begin = p 120 | } 121 | 122 | action instruction_text_begin { 123 | } 124 | 125 | action instruction_text_end { 126 | } 127 | 128 | action instruction_end { 129 | delegate.instruction(data.byteslice(instruction_begin, p-instruction_begin)) 130 | } 131 | 132 | action instruction_error { 133 | raise ParseError.new("could not parse instruction", buffer, p) 134 | } 135 | 136 | action tag_name { 137 | self_closing = false 138 | 139 | delegate.open_tag_begin(data.byteslice(identifier_begin...identifier_end), identifier_begin) 140 | } 141 | 142 | action tag_opening_begin { 143 | } 144 | 145 | action tag_self_closing { 146 | self_closing = true 147 | } 148 | 149 | action attribute_begin { 150 | has_value = false 151 | pcdata = "" 152 | } 153 | 154 | action attribute_value { 155 | has_value = true 156 | } 157 | 158 | action attribute_empty { 159 | has_value = true 160 | } 161 | 162 | action attribute { 163 | if has_value 164 | pcdata = MarkupString.raw(pcdata) unless has_entities 165 | 166 | value = pcdata 167 | else 168 | value = true 169 | end 170 | 171 | delegate.attribute(data.byteslice(identifier_begin...identifier_end), value) 172 | } 173 | 174 | action tag_opening_end { 175 | delegate.open_tag_end(self_closing) 176 | } 177 | 178 | action tag_closing_begin { 179 | } 180 | 181 | action tag_closing_end { 182 | delegate.close_tag(data.byteslice(identifier_begin...identifier_end), identifier_begin) 183 | } 184 | 185 | action tag_error { 186 | raise ParseError.new("could not parse tag", buffer, p) 187 | } 188 | 189 | action cdata_begin { 190 | cdata_begin = p 191 | } 192 | 193 | action cdata_end { 194 | cdata_end = p 195 | 196 | delegate.cdata(data.byteslice(cdata_begin...cdata_end)) 197 | } 198 | 199 | action cdata_error { 200 | raise ParseError.new("could not parse cdata", buffer, p) 201 | } 202 | 203 | # This magic ensures that we process bytes. 204 | getkey bytes[p]; 205 | 206 | include markup "trenni/markup.rl"; 207 | }%% 208 | 209 | require_relative '../error' 210 | 211 | module Trenni 212 | module Fallback 213 | %% write data; 214 | 215 | def self.parse_markup(buffer, delegate, entities) 216 | data = buffer.read 217 | encoding = buffer.encoding 218 | bytes = data.bytes 219 | 220 | p = 0 221 | # Must set pe here or it gets incorrectly set to data.length 222 | pe = eof = data.bytesize 223 | stack = [] 224 | 225 | pcdata = nil 226 | characters_begin = characters_end = nil 227 | entity_begin = entity_end = nil 228 | identifier_begin = identifier_end = nil 229 | doctype_begin = doctype_end = nil 230 | comment_begin = comment_end = nil 231 | instruction_begin = instruction_end = nil 232 | cdata_begin = cdata_end = nil 233 | has_entities = has_value = false 234 | 235 | %% write init; 236 | %% write exec; 237 | 238 | if p != eof 239 | raise ParseError.new("could not consume all input", buffer, p) 240 | end 241 | 242 | return nil 243 | end 244 | end 245 | end 246 | -------------------------------------------------------------------------------- /lib/trenni/fallback/query.rb: -------------------------------------------------------------------------------- 1 | 2 | # line 1 "query.rl" 3 | # Copyright, 2020, by Samuel G. D. Williams. 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | # THE SOFTWARE. 22 | 23 | 24 | # line 74 "query.rl" 25 | 26 | 27 | require_relative '../error' 28 | 29 | module Trenni 30 | module Fallback 31 | 32 | # line 33 "query.rb" 33 | class << self 34 | attr_accessor :_query_trans_keys 35 | private :_query_trans_keys, :_query_trans_keys= 36 | end 37 | self._query_trans_keys = [ 38 | 0, 0, 48, 102, 48, 102, 39 | 37, 93, 37, 93, 48, 40 | 102, 48, 102, 37, 93, 41 | 37, 93, 48, 102, 48, 102, 42 | 37, 93, 37, 93, 37, 43 | 93, 37, 93, 37, 93, 44 | 38, 91, 38, 61, 0 45 | ] 46 | 47 | class << self 48 | attr_accessor :_query_key_spans 49 | private :_query_key_spans, :_query_key_spans= 50 | end 51 | self._query_key_spans = [ 52 | 0, 55, 55, 57, 57, 55, 55, 57, 53 | 57, 55, 55, 57, 57, 57, 57, 57, 54 | 54, 24 55 | ] 56 | 57 | class << self 58 | attr_accessor :_query_index_offsets 59 | private :_query_index_offsets, :_query_index_offsets= 60 | end 61 | self._query_index_offsets = [ 62 | 0, 0, 56, 112, 170, 228, 284, 340, 63 | 398, 456, 512, 568, 626, 684, 742, 800, 64 | 858, 913 65 | ] 66 | 67 | class << self 68 | attr_accessor :_query_indicies 69 | private :_query_indicies, :_query_indicies= 70 | end 71 | self._query_indicies = [ 72 | 0, 0, 0, 0, 0, 0, 0, 0, 73 | 0, 0, 1, 1, 1, 1, 1, 1, 74 | 1, 0, 0, 0, 0, 0, 0, 1, 75 | 1, 1, 1, 1, 1, 1, 1, 1, 76 | 1, 1, 1, 1, 1, 1, 1, 1, 77 | 1, 1, 1, 1, 1, 1, 1, 1, 78 | 1, 0, 0, 0, 0, 0, 0, 1, 79 | 2, 2, 2, 2, 2, 2, 2, 2, 80 | 2, 2, 1, 1, 1, 1, 1, 1, 81 | 1, 2, 2, 2, 2, 2, 2, 1, 82 | 1, 1, 1, 1, 1, 1, 1, 1, 83 | 1, 1, 1, 1, 1, 1, 1, 1, 84 | 1, 1, 1, 1, 1, 1, 1, 1, 85 | 1, 2, 2, 2, 2, 2, 2, 1, 86 | 4, 1, 3, 3, 3, 3, 5, 3, 87 | 3, 3, 3, 6, 6, 6, 6, 6, 88 | 6, 6, 6, 6, 6, 3, 3, 3, 89 | 1, 3, 3, 3, 3, 3, 3, 3, 90 | 3, 3, 3, 3, 3, 3, 3, 3, 91 | 3, 3, 3, 3, 3, 3, 3, 3, 92 | 3, 3, 3, 3, 3, 3, 1, 3, 93 | 1, 3, 7, 1, 2, 2, 2, 2, 94 | 8, 2, 2, 2, 2, 9, 9, 9, 95 | 9, 9, 9, 9, 9, 9, 9, 2, 96 | 2, 2, 1, 2, 2, 2, 2, 2, 97 | 2, 2, 2, 2, 2, 2, 2, 2, 98 | 2, 2, 2, 2, 2, 2, 2, 2, 99 | 2, 2, 2, 2, 2, 2, 2, 2, 100 | 1, 2, 1, 2, 10, 10, 10, 10, 101 | 10, 10, 10, 10, 10, 10, 1, 1, 102 | 1, 1, 1, 1, 1, 10, 10, 10, 103 | 10, 10, 10, 1, 1, 1, 1, 1, 104 | 1, 1, 1, 1, 1, 1, 1, 1, 105 | 1, 1, 1, 1, 1, 1, 1, 1, 106 | 1, 1, 1, 1, 1, 10, 10, 10, 107 | 10, 10, 10, 1, 11, 11, 11, 11, 108 | 11, 11, 11, 11, 11, 11, 1, 1, 109 | 1, 1, 1, 1, 1, 11, 11, 11, 110 | 11, 11, 11, 1, 1, 1, 1, 1, 111 | 1, 1, 1, 1, 1, 1, 1, 1, 112 | 1, 1, 1, 1, 1, 1, 1, 1, 113 | 1, 1, 1, 1, 1, 11, 11, 11, 114 | 11, 11, 11, 1, 13, 1, 12, 12, 115 | 12, 12, 14, 12, 12, 12, 12, 15, 116 | 15, 15, 15, 15, 15, 15, 15, 15, 117 | 15, 12, 12, 12, 1, 12, 12, 12, 118 | 12, 12, 12, 12, 12, 12, 12, 12, 119 | 12, 12, 12, 12, 12, 12, 12, 12, 120 | 12, 12, 12, 12, 12, 12, 12, 12, 121 | 12, 12, 1, 12, 16, 12, 18, 1, 122 | 17, 17, 17, 17, 19, 17, 17, 17, 123 | 17, 17, 17, 17, 17, 17, 17, 17, 124 | 17, 17, 17, 17, 17, 17, 1, 17, 125 | 17, 17, 17, 17, 17, 17, 17, 17, 126 | 17, 17, 17, 17, 17, 17, 17, 17, 127 | 17, 17, 17, 17, 17, 17, 17, 17, 128 | 17, 17, 17, 17, 1, 17, 20, 17, 129 | 21, 21, 21, 21, 21, 21, 21, 21, 130 | 21, 21, 1, 1, 1, 1, 1, 1, 131 | 1, 21, 21, 21, 21, 21, 21, 1, 132 | 1, 1, 1, 1, 1, 1, 1, 1, 133 | 1, 1, 1, 1, 1, 1, 1, 1, 134 | 1, 1, 1, 1, 1, 1, 1, 1, 135 | 1, 21, 21, 21, 21, 21, 21, 1, 136 | 17, 17, 17, 17, 17, 17, 17, 17, 137 | 17, 17, 1, 1, 1, 1, 1, 1, 138 | 1, 17, 17, 17, 17, 17, 17, 1, 139 | 1, 1, 1, 1, 1, 1, 1, 1, 140 | 1, 1, 1, 1, 1, 1, 1, 1, 141 | 1, 1, 1, 1, 1, 1, 1, 1, 142 | 1, 17, 17, 17, 17, 17, 17, 1, 143 | 18, 1, 17, 17, 17, 17, 19, 17, 144 | 17, 17, 17, 22, 22, 22, 22, 22, 145 | 22, 22, 22, 22, 22, 17, 17, 17, 146 | 1, 17, 17, 17, 17, 17, 17, 17, 147 | 17, 17, 17, 17, 17, 17, 17, 17, 148 | 17, 17, 17, 17, 17, 17, 17, 17, 149 | 17, 17, 17, 17, 17, 17, 1, 17, 150 | 23, 17, 4, 1, 3, 3, 3, 3, 151 | 5, 3, 3, 3, 3, 6, 6, 6, 152 | 6, 6, 6, 6, 6, 6, 6, 3, 153 | 3, 3, 1, 3, 3, 3, 3, 3, 154 | 3, 3, 3, 3, 3, 3, 3, 3, 155 | 3, 3, 3, 3, 3, 3, 3, 3, 156 | 3, 3, 3, 3, 3, 3, 3, 3, 157 | 1, 3, 1, 3, 7, 24, 2, 2, 158 | 2, 2, 8, 2, 2, 2, 2, 2, 159 | 2, 2, 2, 2, 2, 2, 2, 2, 160 | 2, 2, 2, 2, 25, 2, 2, 2, 161 | 2, 2, 2, 2, 2, 2, 2, 2, 162 | 2, 2, 2, 2, 2, 2, 2, 2, 163 | 2, 2, 2, 2, 2, 2, 2, 2, 164 | 2, 2, 26, 2, 1, 2, 28, 29, 165 | 27, 27, 27, 27, 30, 27, 27, 27, 166 | 27, 27, 27, 27, 27, 27, 27, 27, 167 | 27, 27, 27, 27, 27, 27, 1, 27, 168 | 27, 27, 27, 27, 27, 27, 27, 27, 169 | 27, 27, 27, 27, 27, 27, 27, 27, 170 | 27, 27, 27, 27, 27, 27, 27, 27, 171 | 27, 27, 27, 27, 1, 27, 1, 27, 172 | 31, 32, 11, 11, 11, 11, 33, 11, 173 | 11, 11, 11, 11, 11, 11, 11, 11, 174 | 11, 11, 11, 11, 11, 11, 11, 11, 175 | 1, 11, 11, 11, 11, 11, 11, 11, 176 | 11, 11, 11, 11, 11, 11, 11, 11, 177 | 11, 11, 11, 11, 11, 11, 11, 11, 178 | 11, 11, 11, 11, 11, 11, 1, 11, 179 | 1, 11, 34, 1, 1, 1, 1, 1, 180 | 1, 1, 1, 1, 1, 1, 1, 1, 181 | 1, 1, 1, 1, 1, 1, 1, 1, 182 | 1, 35, 1, 1, 1, 1, 1, 1, 183 | 1, 1, 1, 1, 1, 1, 1, 1, 184 | 1, 1, 1, 1, 1, 1, 1, 1, 185 | 1, 1, 1, 1, 1, 1, 1, 36, 186 | 1, 37, 1, 1, 1, 1, 1, 1, 187 | 1, 1, 1, 1, 1, 1, 1, 1, 188 | 1, 1, 1, 1, 1, 1, 1, 1, 189 | 38, 1, 0 190 | ] 191 | 192 | class << self 193 | attr_accessor :_query_trans_targs 194 | private :_query_trans_targs, :_query_trans_targs= 195 | end 196 | self._query_trans_targs = [ 197 | 2, 0, 13, 13, 1, 13, 4, 1, 198 | 13, 4, 6, 15, 8, 9, 8, 11, 199 | 17, 8, 9, 8, 16, 10, 11, 16, 200 | 3, 14, 7, 15, 5, 3, 15, 5, 201 | 3, 15, 3, 14, 7, 3, 14 202 | ] 203 | 204 | class << self 205 | attr_accessor :_query_trans_actions 206 | private :_query_trans_actions, :_query_trans_actions= 207 | end 208 | self._query_trans_actions = [ 209 | 0, 0, 0, 1, 2, 2, 3, 4, 210 | 4, 0, 0, 0, 1, 2, 2, 5, 211 | 0, 0, 4, 4, 6, 0, 0, 7, 212 | 8, 6, 6, 10, 11, 9, 11, 4, 213 | 12, 4, 13, 0, 0, 14, 15 214 | ] 215 | 216 | class << self 217 | attr_accessor :_query_eof_actions 218 | private :_query_eof_actions, :_query_eof_actions= 219 | end 220 | self._query_eof_actions = [ 221 | 0, 0, 0, 0, 0, 0, 0, 0, 222 | 0, 0, 0, 0, 0, 8, 9, 12, 223 | 13, 14 224 | ] 225 | 226 | class << self 227 | attr_accessor :query_start 228 | end 229 | self.query_start = 12; 230 | class << self 231 | attr_accessor :query_first_final 232 | end 233 | self.query_first_final = 12; 234 | class << self 235 | attr_accessor :query_error 236 | end 237 | self.query_error = 0; 238 | 239 | class << self 240 | attr_accessor :query_en_main 241 | end 242 | self.query_en_main = 12; 243 | 244 | 245 | # line 81 "query.rl" 246 | 247 | def self.parse_query(buffer, delegate) 248 | data = buffer.read 249 | bytes = data.bytes 250 | 251 | p = 0 252 | pe = eof = data.bytesize 253 | stack = [] 254 | 255 | string_begin = string_end = nil 256 | integer_begin = integer_end = nil 257 | value_begin = value_end = nil 258 | encoded = false 259 | 260 | 261 | # line 262 "query.rb" 262 | begin 263 | p ||= 0 264 | pe ||= data.length 265 | cs = query_start 266 | end 267 | 268 | # line 96 "query.rl" 269 | 270 | # line 271 "query.rb" 271 | begin 272 | testEof = false 273 | _slen, _trans, _keys, _inds, _acts, _nacts = nil 274 | _goto_level = 0 275 | _resume = 10 276 | _eof_trans = 15 277 | _again = 20 278 | _test_eof = 30 279 | _out = 40 280 | while true 281 | if _goto_level <= 0 282 | if p == pe 283 | _goto_level = _test_eof 284 | next 285 | end 286 | if cs == 0 287 | _goto_level = _out 288 | next 289 | end 290 | end 291 | if _goto_level <= _resume 292 | _keys = cs << 1 293 | _inds = _query_index_offsets[cs] 294 | _slen = _query_key_spans[cs] 295 | _wide = ( bytes[p]) 296 | _trans = if ( _slen > 0 && 297 | _query_trans_keys[_keys] <= _wide && 298 | _wide <= _query_trans_keys[_keys + 1] 299 | ) then 300 | _query_indicies[ _inds + _wide - _query_trans_keys[_keys] ] 301 | else 302 | _query_indicies[ _inds + _slen ] 303 | end 304 | cs = _query_trans_targs[_trans] 305 | if _query_trans_actions[_trans] != 0 306 | case _query_trans_actions[_trans] 307 | when 1 then 308 | # line 24 "query.rl" 309 | begin 310 | 311 | string_begin = p 312 | end 313 | when 6 then 314 | # line 28 "query.rl" 315 | begin 316 | 317 | string_end = p 318 | 319 | delegate.string(data.byteslice(string_begin...string_end), encoded) 320 | 321 | encoded = false 322 | end 323 | when 7 then 324 | # line 40 "query.rl" 325 | begin 326 | 327 | integer_end = p 328 | 329 | delegate.integer(data.byteslice(integer_begin...integer_end)) 330 | end 331 | when 15 then 332 | # line 46 "query.rl" 333 | begin 334 | 335 | delegate.append 336 | end 337 | when 10 then 338 | # line 50 "query.rl" 339 | begin 340 | 341 | value_begin = p 342 | end 343 | when 13 then 344 | # line 62 "query.rl" 345 | begin 346 | 347 | delegate.pair 348 | end 349 | when 4 then 350 | # line 66 "query.rl" 351 | begin 352 | 353 | encoded = 1; 354 | end 355 | when 3 then 356 | # line 24 "query.rl" 357 | begin 358 | 359 | string_begin = p 360 | end 361 | # line 36 "query.rl" 362 | begin 363 | 364 | integer_begin = p 365 | end 366 | when 2 then 367 | # line 24 "query.rl" 368 | begin 369 | 370 | string_begin = p 371 | end 372 | # line 66 "query.rl" 373 | begin 374 | 375 | encoded = 1; 376 | end 377 | when 8 then 378 | # line 28 "query.rl" 379 | begin 380 | 381 | string_end = p 382 | 383 | delegate.string(data.byteslice(string_begin...string_end), encoded) 384 | 385 | encoded = false 386 | end 387 | # line 62 "query.rl" 388 | begin 389 | 390 | delegate.pair 391 | end 392 | when 5 then 393 | # line 36 "query.rl" 394 | begin 395 | 396 | integer_begin = p 397 | end 398 | # line 24 "query.rl" 399 | begin 400 | 401 | string_begin = p 402 | end 403 | when 14 then 404 | # line 46 "query.rl" 405 | begin 406 | 407 | delegate.append 408 | end 409 | # line 62 "query.rl" 410 | begin 411 | 412 | delegate.pair 413 | end 414 | when 11 then 415 | # line 50 "query.rl" 416 | begin 417 | 418 | value_begin = p 419 | end 420 | # line 66 "query.rl" 421 | begin 422 | 423 | encoded = 1; 424 | end 425 | when 12 then 426 | # line 54 "query.rl" 427 | begin 428 | 429 | value_end = p 430 | 431 | delegate.assign(data.byteslice(value_begin...value_end), encoded) 432 | 433 | encoded = false 434 | end 435 | # line 62 "query.rl" 436 | begin 437 | 438 | delegate.pair 439 | end 440 | when 9 then 441 | # line 50 "query.rl" 442 | begin 443 | 444 | value_begin = p 445 | end 446 | # line 54 "query.rl" 447 | begin 448 | 449 | value_end = p 450 | 451 | delegate.assign(data.byteslice(value_begin...value_end), encoded) 452 | 453 | encoded = false 454 | end 455 | # line 62 "query.rl" 456 | begin 457 | 458 | delegate.pair 459 | end 460 | # line 461 "query.rb" 461 | end 462 | end 463 | end 464 | if _goto_level <= _again 465 | if cs == 0 466 | _goto_level = _out 467 | next 468 | end 469 | p += 1 470 | if p != pe 471 | _goto_level = _resume 472 | next 473 | end 474 | end 475 | if _goto_level <= _test_eof 476 | if p == eof 477 | case _query_eof_actions[cs] 478 | when 13 then 479 | # line 62 "query.rl" 480 | begin 481 | 482 | delegate.pair 483 | end 484 | when 8 then 485 | # line 28 "query.rl" 486 | begin 487 | 488 | string_end = p 489 | 490 | delegate.string(data.byteslice(string_begin...string_end), encoded) 491 | 492 | encoded = false 493 | end 494 | # line 62 "query.rl" 495 | begin 496 | 497 | delegate.pair 498 | end 499 | when 14 then 500 | # line 46 "query.rl" 501 | begin 502 | 503 | delegate.append 504 | end 505 | # line 62 "query.rl" 506 | begin 507 | 508 | delegate.pair 509 | end 510 | when 12 then 511 | # line 54 "query.rl" 512 | begin 513 | 514 | value_end = p 515 | 516 | delegate.assign(data.byteslice(value_begin...value_end), encoded) 517 | 518 | encoded = false 519 | end 520 | # line 62 "query.rl" 521 | begin 522 | 523 | delegate.pair 524 | end 525 | when 9 then 526 | # line 50 "query.rl" 527 | begin 528 | 529 | value_begin = p 530 | end 531 | # line 54 "query.rl" 532 | begin 533 | 534 | value_end = p 535 | 536 | delegate.assign(data.byteslice(value_begin...value_end), encoded) 537 | 538 | encoded = false 539 | end 540 | # line 62 "query.rl" 541 | begin 542 | 543 | delegate.pair 544 | end 545 | # line 546 "query.rb" 546 | end 547 | end 548 | 549 | end 550 | if _goto_level <= _out 551 | break 552 | end 553 | end 554 | end 555 | 556 | # line 97 "query.rl" 557 | 558 | if p != eof 559 | raise ParseError.new("could not consume all input", buffer, p) 560 | end 561 | 562 | return nil 563 | end 564 | end 565 | end 566 | -------------------------------------------------------------------------------- /lib/trenni/fallback/query.rl: -------------------------------------------------------------------------------- 1 | # Copyright, 2020, by Samuel G. D. Williams. 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | 21 | %%{ 22 | machine query; 23 | 24 | action string_begin { 25 | string_begin = p 26 | } 27 | 28 | action string_end { 29 | string_end = p 30 | 31 | delegate.string(data.byteslice(string_begin...string_end), encoded) 32 | 33 | encoded = false 34 | } 35 | 36 | action integer_begin { 37 | integer_begin = p 38 | } 39 | 40 | action integer_end { 41 | integer_end = p 42 | 43 | delegate.integer(data.byteslice(integer_begin...integer_end)) 44 | } 45 | 46 | action append { 47 | delegate.append 48 | } 49 | 50 | action value_begin { 51 | value_begin = p 52 | } 53 | 54 | action value_end { 55 | value_end = p 56 | 57 | delegate.assign(data.byteslice(value_begin...value_end), encoded) 58 | 59 | encoded = false 60 | } 61 | 62 | action pair { 63 | delegate.pair 64 | } 65 | 66 | action encoded { 67 | encoded = 1; 68 | } 69 | 70 | # This magic ensures that we process bytes. 71 | getkey bytes[p]; 72 | 73 | include query "trenni/query.rl"; 74 | }%% 75 | 76 | require_relative '../error' 77 | 78 | module Trenni 79 | module Fallback 80 | %% write data; 81 | 82 | def self.parse_query(buffer, delegate) 83 | data = buffer.read 84 | bytes = data.bytes 85 | 86 | p = 0 87 | pe = eof = data.bytesize 88 | stack = [] 89 | 90 | string_begin = string_end = nil 91 | integer_begin = integer_end = nil 92 | value_begin = value_end = nil 93 | encoded = false 94 | 95 | %% write init; 96 | %% write exec; 97 | 98 | if p != eof 99 | raise ParseError.new("could not consume all input", buffer, p) 100 | end 101 | 102 | return nil 103 | end 104 | end 105 | end 106 | -------------------------------------------------------------------------------- /lib/trenni/fallback/template.rb: -------------------------------------------------------------------------------- 1 | 2 | # line 1 "template.rl" 3 | # Copyright, 2016, by Samuel G. D. Williams. 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | # THE SOFTWARE. 22 | 23 | 24 | # line 68 "template.rl" 25 | 26 | 27 | require_relative '../error' 28 | 29 | module Trenni 30 | module Fallback 31 | 32 | # line 33 "template.rb" 33 | class << self 34 | attr_accessor :_template_trans_keys 35 | private :_template_trans_keys, :_template_trans_keys= 36 | end 37 | self._template_trans_keys = [ 38 | 0, 0, 10, 60, 123, 123, 39 | 63, 63, 123, 123, 63, 40 | 63, 63, 63, 114, 114, 41 | 9, 32, 63, 63, 62, 62, 42 | 9, 32, 123, 123, 63, 43 | 63, 0, 127, 0, 127, 44 | 63, 63, 62, 62, 0, 127, 45 | 63, 63, 62, 62, 34, 46 | 125, 34, 35, 35, 125, 47 | 35, 125, 34, 39, 35, 125, 48 | 35, 125, 34, 123, 34, 49 | 123, 39, 39, 34, 125, 50 | 34, 125, 34, 35, 35, 125, 51 | 35, 125, 34, 39, 35, 52 | 125, 35, 125, 34, 123, 53 | 34, 123, 39, 39, 34, 125, 54 | 9, 60, 10, 60, 10, 55 | 60, 9, 60, 0, 0, 56 | 9, 32, 34, 39, 34, 35, 57 | 39, 39, 0, 0, 34, 58 | 39, 34, 35, 39, 39, 59 | 0, 0, 0 60 | ] 61 | 62 | class << self 63 | attr_accessor :_template_key_spans 64 | private :_template_key_spans, :_template_key_spans= 65 | end 66 | self._template_key_spans = [ 67 | 0, 51, 1, 1, 1, 1, 1, 1, 68 | 24, 1, 1, 24, 1, 1, 128, 128, 69 | 1, 1, 128, 1, 1, 92, 2, 91, 70 | 91, 6, 91, 91, 90, 90, 1, 92, 71 | 92, 2, 91, 91, 6, 91, 91, 90, 72 | 90, 1, 92, 52, 51, 51, 52, 0, 73 | 24, 6, 2, 1, 0, 6, 2, 1, 74 | 0 75 | ] 76 | 77 | class << self 78 | attr_accessor :_template_index_offsets 79 | private :_template_index_offsets, :_template_index_offsets= 80 | end 81 | self._template_index_offsets = [ 82 | 0, 0, 52, 54, 56, 58, 60, 62, 83 | 64, 89, 91, 93, 118, 120, 122, 251, 84 | 380, 382, 384, 513, 515, 517, 610, 613, 85 | 705, 797, 804, 896, 988, 1079, 1170, 1172, 86 | 1265, 1358, 1361, 1453, 1545, 1552, 1644, 1736, 87 | 1827, 1918, 1920, 2013, 2066, 2118, 2170, 2223, 88 | 2224, 2249, 2256, 2259, 2261, 2262, 2269, 2272, 89 | 2274 90 | ] 91 | 92 | class << self 93 | attr_accessor :_template_indicies 94 | private :_template_indicies, :_template_indicies= 95 | end 96 | self._template_indicies = [ 97 | 2, 1, 1, 1, 1, 1, 1, 1, 98 | 1, 1, 1, 1, 1, 1, 1, 1, 99 | 1, 1, 1, 1, 1, 1, 1, 1, 100 | 1, 3, 1, 1, 1, 1, 1, 1, 101 | 1, 1, 1, 1, 1, 1, 1, 1, 102 | 1, 1, 1, 1, 1, 1, 1, 1, 103 | 1, 1, 4, 1, 0, 1, 0, 1, 104 | 5, 6, 5, 6, 7, 6, 8, 5, 105 | 10, 10, 10, 10, 10, 5, 5, 5, 106 | 5, 5, 5, 5, 5, 5, 5, 5, 107 | 5, 5, 5, 5, 5, 5, 5, 10, 108 | 5, 12, 11, 13, 11, 13, 15, 13, 109 | 13, 13, 14, 14, 14, 14, 14, 14, 110 | 14, 14, 14, 14, 14, 14, 14, 14, 111 | 14, 14, 14, 14, 13, 14, 16, 6, 112 | 17, 6, 19, 19, 19, 19, 19, 19, 113 | 19, 19, 19, 19, 19, 19, 19, 19, 114 | 19, 19, 19, 19, 19, 19, 19, 19, 115 | 19, 19, 19, 19, 19, 19, 19, 19, 116 | 19, 19, 19, 19, 19, 19, 19, 19, 117 | 19, 19, 19, 19, 19, 19, 19, 18, 118 | 18, 19, 18, 18, 18, 18, 18, 18, 119 | 18, 18, 18, 18, 18, 19, 19, 19, 120 | 19, 19, 19, 18, 18, 18, 18, 18, 121 | 18, 18, 18, 18, 18, 18, 18, 18, 122 | 18, 18, 18, 18, 18, 18, 18, 18, 123 | 18, 18, 18, 18, 18, 19, 19, 19, 124 | 19, 18, 19, 18, 18, 18, 18, 18, 125 | 18, 18, 18, 18, 18, 18, 18, 18, 126 | 18, 18, 18, 18, 20, 18, 18, 18, 127 | 18, 18, 18, 18, 18, 19, 19, 19, 128 | 19, 19, 18, 19, 19, 19, 19, 19, 129 | 19, 19, 19, 19, 21, 21, 21, 21, 130 | 21, 19, 19, 19, 19, 19, 19, 19, 131 | 19, 19, 19, 19, 19, 19, 19, 19, 132 | 19, 19, 19, 21, 19, 19, 19, 19, 133 | 19, 19, 19, 19, 19, 19, 19, 19, 134 | 18, 18, 19, 18, 18, 18, 18, 18, 135 | 18, 18, 18, 18, 18, 18, 19, 19, 136 | 19, 19, 19, 19, 18, 18, 18, 18, 137 | 18, 18, 18, 18, 18, 18, 18, 18, 138 | 18, 18, 18, 18, 18, 18, 18, 18, 139 | 18, 18, 18, 18, 18, 18, 19, 19, 140 | 19, 19, 18, 19, 18, 18, 18, 18, 141 | 18, 18, 18, 18, 18, 18, 18, 18, 142 | 18, 18, 18, 18, 18, 18, 18, 18, 143 | 18, 18, 18, 18, 18, 18, 19, 19, 144 | 19, 19, 19, 18, 22, 21, 23, 21, 145 | 19, 19, 19, 19, 19, 19, 19, 19, 146 | 19, 24, 24, 24, 24, 24, 19, 19, 147 | 19, 19, 19, 19, 19, 19, 19, 19, 148 | 19, 19, 19, 19, 19, 19, 19, 19, 149 | 24, 19, 19, 19, 19, 19, 19, 19, 150 | 19, 19, 19, 19, 19, 18, 18, 19, 151 | 18, 18, 18, 18, 18, 18, 18, 18, 152 | 18, 18, 18, 19, 19, 19, 19, 19, 153 | 19, 18, 18, 18, 18, 18, 18, 18, 154 | 18, 18, 18, 18, 18, 18, 18, 18, 155 | 18, 18, 18, 18, 18, 18, 18, 18, 156 | 18, 18, 18, 19, 19, 19, 19, 18, 157 | 19, 18, 18, 18, 18, 18, 18, 18, 158 | 18, 18, 18, 18, 18, 18, 18, 18, 159 | 18, 18, 18, 18, 18, 18, 18, 18, 160 | 18, 18, 18, 19, 19, 19, 19, 19, 161 | 18, 26, 25, 27, 25, 29, 28, 28, 162 | 28, 28, 30, 28, 28, 28, 28, 28, 163 | 28, 28, 28, 28, 28, 28, 28, 28, 164 | 28, 28, 28, 28, 28, 28, 28, 28, 165 | 28, 28, 28, 28, 28, 28, 28, 28, 166 | 28, 28, 28, 28, 28, 28, 28, 28, 167 | 28, 28, 28, 28, 28, 28, 28, 28, 168 | 28, 28, 28, 28, 28, 28, 28, 28, 169 | 28, 28, 28, 28, 28, 28, 28, 28, 170 | 28, 28, 28, 28, 28, 28, 28, 28, 171 | 28, 28, 28, 28, 28, 28, 28, 28, 172 | 28, 28, 28, 28, 28, 28, 31, 28, 173 | 32, 28, 33, 34, 29, 35, 33, 33, 174 | 33, 36, 33, 33, 33, 33, 33, 33, 175 | 33, 33, 33, 33, 33, 33, 33, 33, 176 | 33, 33, 33, 33, 33, 33, 33, 33, 177 | 33, 33, 33, 33, 33, 33, 33, 33, 178 | 33, 33, 33, 33, 33, 33, 33, 33, 179 | 33, 33, 33, 33, 33, 33, 33, 33, 180 | 33, 33, 33, 33, 33, 33, 33, 33, 181 | 33, 33, 33, 33, 33, 33, 33, 33, 182 | 33, 33, 33, 33, 33, 33, 33, 33, 183 | 33, 33, 33, 33, 33, 33, 33, 33, 184 | 33, 33, 33, 33, 33, 37, 33, 38, 185 | 33, 35, 33, 33, 33, 36, 33, 33, 186 | 33, 33, 33, 33, 33, 33, 33, 33, 187 | 33, 33, 33, 33, 33, 33, 33, 33, 188 | 33, 33, 33, 33, 33, 33, 33, 33, 189 | 33, 33, 33, 33, 33, 33, 33, 33, 190 | 33, 33, 33, 33, 33, 33, 33, 33, 191 | 33, 33, 33, 33, 33, 33, 33, 33, 192 | 33, 33, 33, 33, 33, 33, 33, 33, 193 | 33, 33, 33, 33, 33, 33, 33, 33, 194 | 33, 33, 33, 33, 33, 33, 33, 33, 195 | 33, 33, 33, 33, 33, 33, 33, 33, 196 | 33, 39, 33, 38, 33, 40, 41, 36, 197 | 36, 36, 40, 36, 42, 40, 40, 40, 198 | 40, 40, 40, 40, 40, 40, 40, 40, 199 | 40, 40, 40, 40, 40, 40, 40, 40, 200 | 40, 40, 40, 40, 40, 40, 40, 40, 201 | 40, 40, 40, 40, 40, 40, 40, 40, 202 | 40, 40, 40, 40, 40, 40, 40, 40, 203 | 40, 40, 40, 40, 40, 40, 40, 40, 204 | 40, 40, 40, 40, 40, 40, 40, 40, 205 | 40, 40, 40, 40, 40, 40, 40, 40, 206 | 40, 40, 40, 40, 40, 40, 40, 40, 207 | 40, 40, 40, 40, 40, 40, 40, 40, 208 | 40, 40, 40, 40, 43, 40, 44, 40, 209 | 42, 40, 40, 40, 40, 40, 40, 40, 210 | 40, 40, 40, 40, 40, 40, 40, 40, 211 | 40, 40, 40, 40, 40, 40, 40, 40, 212 | 40, 40, 40, 40, 40, 40, 40, 40, 213 | 40, 40, 40, 40, 40, 40, 40, 40, 214 | 40, 40, 40, 40, 40, 40, 40, 40, 215 | 40, 40, 40, 40, 40, 40, 40, 40, 216 | 40, 40, 40, 40, 40, 40, 40, 40, 217 | 40, 40, 40, 40, 40, 40, 40, 40, 218 | 40, 40, 40, 40, 40, 40, 40, 40, 219 | 40, 40, 40, 40, 40, 40, 40, 40, 220 | 45, 40, 44, 40, 40, 41, 36, 36, 221 | 36, 40, 36, 36, 36, 36, 36, 36, 222 | 36, 36, 36, 36, 36, 36, 36, 36, 223 | 36, 36, 36, 36, 36, 36, 36, 36, 224 | 36, 36, 36, 36, 36, 36, 36, 36, 225 | 36, 36, 36, 36, 36, 36, 36, 36, 226 | 36, 36, 36, 36, 36, 36, 36, 36, 227 | 36, 36, 36, 36, 36, 36, 36, 36, 228 | 36, 36, 36, 36, 36, 36, 36, 36, 229 | 36, 36, 36, 36, 36, 36, 36, 36, 230 | 36, 36, 36, 36, 36, 36, 36, 36, 231 | 36, 36, 36, 36, 36, 46, 36, 33, 232 | 34, 29, 29, 29, 29, 29, 29, 29, 233 | 29, 29, 29, 29, 29, 29, 29, 29, 234 | 29, 29, 29, 29, 29, 29, 29, 29, 235 | 29, 29, 29, 29, 29, 29, 29, 29, 236 | 29, 29, 29, 29, 29, 29, 29, 29, 237 | 29, 29, 29, 29, 29, 29, 29, 29, 238 | 29, 29, 29, 29, 29, 29, 29, 29, 239 | 29, 29, 29, 29, 29, 29, 29, 29, 240 | 29, 29, 29, 29, 29, 29, 29, 29, 241 | 29, 29, 29, 29, 29, 29, 29, 29, 242 | 29, 29, 29, 29, 29, 29, 29, 29, 243 | 47, 29, 48, 30, 36, 48, 48, 48, 244 | 48, 48, 48, 48, 48, 48, 48, 48, 245 | 48, 48, 48, 48, 48, 48, 48, 48, 246 | 48, 48, 48, 48, 48, 48, 48, 48, 247 | 48, 48, 48, 48, 48, 48, 48, 48, 248 | 48, 48, 48, 48, 48, 48, 48, 48, 249 | 48, 48, 48, 48, 48, 48, 48, 48, 250 | 48, 48, 48, 48, 48, 48, 48, 48, 251 | 48, 48, 48, 48, 48, 48, 48, 48, 252 | 48, 48, 48, 48, 48, 48, 48, 48, 253 | 48, 48, 48, 48, 48, 48, 48, 48, 254 | 48, 48, 48, 48, 48, 49, 48, 50, 255 | 48, 52, 51, 51, 51, 51, 53, 51, 256 | 51, 51, 51, 51, 51, 51, 51, 51, 257 | 51, 51, 51, 51, 51, 51, 51, 51, 258 | 51, 51, 51, 51, 51, 51, 51, 51, 259 | 51, 51, 51, 51, 51, 51, 51, 51, 260 | 51, 51, 51, 51, 51, 51, 51, 51, 261 | 51, 51, 51, 51, 51, 51, 51, 51, 262 | 51, 51, 51, 51, 51, 51, 51, 51, 263 | 51, 51, 51, 51, 51, 51, 51, 51, 264 | 51, 51, 51, 51, 51, 51, 51, 51, 265 | 51, 51, 51, 51, 51, 51, 51, 51, 266 | 51, 51, 54, 51, 55, 51, 56, 57, 267 | 52, 58, 56, 56, 56, 59, 56, 56, 268 | 56, 56, 56, 56, 56, 56, 56, 56, 269 | 56, 56, 56, 56, 56, 56, 56, 56, 270 | 56, 56, 56, 56, 56, 56, 56, 56, 271 | 56, 56, 56, 56, 56, 56, 56, 56, 272 | 56, 56, 56, 56, 56, 56, 56, 56, 273 | 56, 56, 56, 56, 56, 56, 56, 56, 274 | 56, 56, 56, 56, 56, 56, 56, 56, 275 | 56, 56, 56, 56, 56, 56, 56, 56, 276 | 56, 56, 56, 56, 56, 56, 56, 56, 277 | 56, 56, 56, 56, 56, 56, 56, 56, 278 | 56, 60, 56, 61, 56, 58, 56, 56, 279 | 56, 59, 56, 56, 56, 56, 56, 56, 280 | 56, 56, 56, 56, 56, 56, 56, 56, 281 | 56, 56, 56, 56, 56, 56, 56, 56, 282 | 56, 56, 56, 56, 56, 56, 56, 56, 283 | 56, 56, 56, 56, 56, 56, 56, 56, 284 | 56, 56, 56, 56, 56, 56, 56, 56, 285 | 56, 56, 56, 56, 56, 56, 56, 56, 286 | 56, 56, 56, 56, 56, 56, 56, 56, 287 | 56, 56, 56, 56, 56, 56, 56, 56, 288 | 56, 56, 56, 56, 56, 56, 56, 56, 289 | 56, 56, 56, 56, 56, 62, 56, 61, 290 | 56, 63, 64, 59, 59, 59, 63, 59, 291 | 65, 63, 63, 63, 63, 63, 63, 63, 292 | 63, 63, 63, 63, 63, 63, 63, 63, 293 | 63, 63, 63, 63, 63, 63, 63, 63, 294 | 63, 63, 63, 63, 63, 63, 63, 63, 295 | 63, 63, 63, 63, 63, 63, 63, 63, 296 | 63, 63, 63, 63, 63, 63, 63, 63, 297 | 63, 63, 63, 63, 63, 63, 63, 63, 298 | 63, 63, 63, 63, 63, 63, 63, 63, 299 | 63, 63, 63, 63, 63, 63, 63, 63, 300 | 63, 63, 63, 63, 63, 63, 63, 63, 301 | 63, 63, 63, 63, 63, 63, 63, 63, 302 | 66, 63, 67, 63, 65, 63, 63, 63, 303 | 63, 63, 63, 63, 63, 63, 63, 63, 304 | 63, 63, 63, 63, 63, 63, 63, 63, 305 | 63, 63, 63, 63, 63, 63, 63, 63, 306 | 63, 63, 63, 63, 63, 63, 63, 63, 307 | 63, 63, 63, 63, 63, 63, 63, 63, 308 | 63, 63, 63, 63, 63, 63, 63, 63, 309 | 63, 63, 63, 63, 63, 63, 63, 63, 310 | 63, 63, 63, 63, 63, 63, 63, 63, 311 | 63, 63, 63, 63, 63, 63, 63, 63, 312 | 63, 63, 63, 63, 63, 63, 63, 63, 313 | 63, 63, 63, 63, 68, 63, 67, 63, 314 | 63, 64, 59, 59, 59, 63, 59, 59, 315 | 59, 59, 59, 59, 59, 59, 59, 59, 316 | 59, 59, 59, 59, 59, 59, 59, 59, 317 | 59, 59, 59, 59, 59, 59, 59, 59, 318 | 59, 59, 59, 59, 59, 59, 59, 59, 319 | 59, 59, 59, 59, 59, 59, 59, 59, 320 | 59, 59, 59, 59, 59, 59, 59, 59, 321 | 59, 59, 59, 59, 59, 59, 59, 59, 322 | 59, 59, 59, 59, 59, 59, 59, 59, 323 | 59, 59, 59, 59, 59, 59, 59, 59, 324 | 59, 59, 59, 59, 59, 59, 59, 59, 325 | 59, 69, 59, 56, 57, 52, 52, 52, 326 | 52, 52, 52, 52, 52, 52, 52, 52, 327 | 52, 52, 52, 52, 52, 52, 52, 52, 328 | 52, 52, 52, 52, 52, 52, 52, 52, 329 | 52, 52, 52, 52, 52, 52, 52, 52, 330 | 52, 52, 52, 52, 52, 52, 52, 52, 331 | 52, 52, 52, 52, 52, 52, 52, 52, 332 | 52, 52, 52, 52, 52, 52, 52, 52, 333 | 52, 52, 52, 52, 52, 52, 52, 52, 334 | 52, 52, 52, 52, 52, 52, 52, 52, 335 | 52, 52, 52, 52, 52, 52, 52, 52, 336 | 52, 52, 52, 52, 70, 52, 71, 53, 337 | 59, 71, 71, 71, 71, 71, 71, 71, 338 | 71, 71, 71, 71, 71, 71, 71, 71, 339 | 71, 71, 71, 71, 71, 71, 71, 71, 340 | 71, 71, 71, 71, 71, 71, 71, 71, 341 | 71, 71, 71, 71, 71, 71, 71, 71, 342 | 71, 71, 71, 71, 71, 71, 71, 71, 343 | 71, 71, 71, 71, 71, 71, 71, 71, 344 | 71, 71, 71, 71, 71, 71, 71, 71, 345 | 71, 71, 71, 71, 71, 71, 71, 71, 346 | 71, 71, 71, 71, 71, 71, 71, 71, 347 | 71, 71, 71, 71, 71, 71, 71, 71, 348 | 71, 72, 71, 73, 71, 74, 2, 74, 349 | 74, 74, 6, 6, 6, 6, 6, 6, 350 | 6, 6, 6, 6, 6, 6, 6, 6, 351 | 6, 6, 6, 6, 74, 6, 6, 75, 352 | 6, 6, 6, 6, 6, 6, 6, 6, 353 | 6, 6, 6, 6, 6, 6, 6, 6, 354 | 6, 6, 6, 6, 6, 6, 6, 6, 355 | 76, 6, 2, 6, 6, 6, 6, 6, 356 | 6, 6, 6, 6, 6, 6, 6, 6, 357 | 6, 6, 6, 6, 6, 6, 6, 6, 358 | 6, 6, 6, 78, 6, 6, 6, 6, 359 | 6, 6, 6, 6, 6, 6, 6, 6, 360 | 6, 6, 6, 6, 6, 6, 6, 6, 361 | 6, 6, 6, 6, 79, 6, 2, 1, 362 | 1, 1, 1, 1, 1, 1, 1, 1, 363 | 1, 1, 1, 1, 1, 1, 1, 1, 364 | 1, 1, 1, 1, 1, 1, 1, 3, 365 | 1, 1, 1, 1, 1, 1, 1, 1, 366 | 1, 1, 1, 1, 1, 1, 1, 1, 367 | 1, 1, 1, 1, 1, 1, 1, 1, 368 | 4, 1, 74, 2, 74, 74, 74, 6, 369 | 6, 6, 6, 6, 6, 6, 6, 6, 370 | 6, 6, 6, 6, 6, 6, 6, 6, 371 | 6, 74, 6, 6, 78, 6, 6, 6, 372 | 6, 6, 6, 6, 6, 6, 6, 6, 373 | 6, 6, 6, 6, 6, 6, 6, 6, 374 | 6, 6, 6, 6, 6, 81, 6, 82, 375 | 13, 15, 13, 13, 13, 83, 83, 83, 376 | 83, 83, 83, 83, 83, 83, 83, 83, 377 | 83, 83, 83, 83, 83, 83, 83, 13, 378 | 83, 40, 41, 36, 36, 36, 40, 36, 379 | 33, 34, 29, 48, 30, 84, 63, 64, 380 | 59, 59, 59, 63, 59, 56, 57, 52, 381 | 71, 53, 84, 0 382 | ] 383 | 384 | class << self 385 | attr_accessor :_template_trans_targs 386 | private :_template_trans_targs, :_template_trans_targs= 387 | end 388 | self._template_trans_targs = [ 389 | 43, 1, 45, 2, 3, 43, 44, 7, 390 | 8, 43, 9, 9, 10, 11, 43, 43, 391 | 47, 14, 15, 0, 18, 16, 17, 43, 392 | 19, 19, 20, 48, 21, 22, 30, 21, 393 | 52, 23, 29, 24, 25, 23, 50, 23, 394 | 26, 28, 27, 26, 49, 26, 25, 22, 395 | 31, 31, 51, 32, 33, 41, 32, 56, 396 | 34, 40, 35, 36, 34, 54, 34, 37, 397 | 39, 38, 37, 53, 37, 36, 33, 42, 398 | 42, 55, 46, 12, 13, 43, 4, 5, 399 | 43, 6, 43, 43, 0 400 | ] 401 | 402 | class << self 403 | attr_accessor :_template_trans_actions 404 | private :_template_trans_actions, :_template_trans_actions= 405 | end 406 | self._template_trans_actions = [ 407 | 1, 0, 2, 0, 0, 3, 2, 0, 408 | 0, 4, 5, 0, 6, 0, 7, 8, 409 | 0, 0, 0, 9, 0, 0, 0, 10, 410 | 5, 0, 6, 11, 0, 0, 0, 13, 411 | 14, 0, 0, 0, 0, 13, 14, 15, 412 | 0, 0, 0, 13, 14, 15, 16, 16, 413 | 0, 13, 14, 0, 0, 0, 13, 18, 414 | 0, 0, 0, 0, 13, 18, 15, 0, 415 | 0, 0, 13, 18, 15, 16, 16, 0, 416 | 13, 18, 20, 0, 0, 21, 0, 0, 417 | 22, 0, 23, 24, 0 418 | ] 419 | 420 | class << self 421 | attr_accessor :_template_to_state_actions 422 | private :_template_to_state_actions, :_template_to_state_actions= 423 | end 424 | self._template_to_state_actions = [ 425 | 0, 0, 0, 0, 0, 0, 0, 0, 426 | 0, 0, 0, 0, 0, 0, 0, 0, 427 | 0, 0, 0, 0, 0, 12, 12, 12, 428 | 0, 12, 12, 0, 0, 0, 0, 12, 429 | 12, 12, 12, 0, 12, 12, 0, 0, 430 | 0, 0, 12, 12, 0, 0, 0, 0, 431 | 0, 0, 0, 0, 0, 0, 0, 0, 432 | 0 433 | ] 434 | 435 | class << self 436 | attr_accessor :_template_from_state_actions 437 | private :_template_from_state_actions, :_template_from_state_actions= 438 | end 439 | self._template_from_state_actions = [ 440 | 0, 0, 0, 0, 0, 0, 0, 0, 441 | 0, 0, 0, 0, 0, 0, 0, 0, 442 | 0, 0, 0, 0, 0, 0, 0, 0, 443 | 0, 0, 0, 0, 0, 0, 0, 0, 444 | 0, 0, 0, 0, 0, 0, 0, 0, 445 | 0, 0, 0, 19, 0, 0, 0, 0, 446 | 0, 0, 0, 0, 0, 0, 0, 0, 447 | 0 448 | ] 449 | 450 | class << self 451 | attr_accessor :_template_eof_actions 452 | private :_template_eof_actions, :_template_eof_actions= 453 | end 454 | self._template_eof_actions = [ 455 | 0, 0, 0, 0, 0, 0, 0, 0, 456 | 0, 0, 0, 0, 0, 0, 9, 9, 457 | 9, 9, 9, 9, 9, 0, 0, 0, 458 | 0, 0, 0, 0, 0, 0, 0, 0, 459 | 17, 17, 17, 17, 17, 17, 17, 17, 460 | 17, 17, 17, 0, 0, 0, 0, 0, 461 | 0, 0, 0, 0, 0, 0, 0, 0, 462 | 0 463 | ] 464 | 465 | class << self 466 | attr_accessor :_template_eof_trans 467 | private :_template_eof_trans, :_template_eof_trans= 468 | end 469 | self._template_eof_trans = [ 470 | 0, 1, 1, 1, 6, 6, 6, 6, 471 | 10, 10, 10, 15, 0, 0, 0, 0, 472 | 0, 0, 0, 0, 0, 0, 0, 0, 473 | 0, 0, 0, 0, 0, 0, 0, 0, 474 | 0, 0, 0, 0, 0, 0, 0, 0, 475 | 0, 0, 0, 0, 78, 81, 78, 83, 476 | 84, 0, 0, 0, 0, 0, 0, 0, 477 | 0 478 | ] 479 | 480 | class << self 481 | attr_accessor :template_start 482 | end 483 | self.template_start = 43; 484 | class << self 485 | attr_accessor :template_first_final 486 | end 487 | self.template_first_final = 43; 488 | class << self 489 | attr_accessor :template_error 490 | end 491 | self.template_error = 0; 492 | 493 | class << self 494 | attr_accessor :template_en_parse_nested_expression 495 | end 496 | self.template_en_parse_nested_expression = 21; 497 | class << self 498 | attr_accessor :template_en_parse_expression 499 | end 500 | self.template_en_parse_expression = 32; 501 | class << self 502 | attr_accessor :template_en_main 503 | end 504 | self.template_en_main = 43; 505 | 506 | 507 | # line 75 "template.rl" 508 | 509 | def self.parse_template(buffer, delegate) 510 | data = buffer.read 511 | bytes = data.bytes 512 | 513 | p = 0 514 | pe = eof = data.bytesize 515 | stack = [] 516 | 517 | expression_begin = expression_end = nil 518 | instruction_begin = instruction_end = nil 519 | 520 | 521 | # line 522 "template.rb" 522 | begin 523 | p ||= 0 524 | pe ||= data.length 525 | cs = template_start 526 | top = 0 527 | ts = nil 528 | te = nil 529 | act = 0 530 | end 531 | 532 | # line 88 "template.rl" 533 | 534 | # line 535 "template.rb" 535 | begin 536 | testEof = false 537 | _slen, _trans, _keys, _inds, _acts, _nacts = nil 538 | _goto_level = 0 539 | _resume = 10 540 | _eof_trans = 15 541 | _again = 20 542 | _test_eof = 30 543 | _out = 40 544 | while true 545 | if _goto_level <= 0 546 | if p == pe 547 | _goto_level = _test_eof 548 | next 549 | end 550 | if cs == 0 551 | _goto_level = _out 552 | next 553 | end 554 | end 555 | if _goto_level <= _resume 556 | case _template_from_state_actions[cs] 557 | when 19 then 558 | # line 1 "NONE" 559 | begin 560 | ts = p 561 | end 562 | # line 563 "template.rb" 563 | end 564 | _keys = cs << 1 565 | _inds = _template_index_offsets[cs] 566 | _slen = _template_key_spans[cs] 567 | _wide = ( bytes[p]) 568 | _trans = if ( _slen > 0 && 569 | _template_trans_keys[_keys] <= _wide && 570 | _wide <= _template_trans_keys[_keys + 1] 571 | ) then 572 | _template_indicies[ _inds + _wide - _template_trans_keys[_keys] ] 573 | else 574 | _template_indicies[ _inds + _slen ] 575 | end 576 | end 577 | if _goto_level <= _eof_trans 578 | cs = _template_trans_targs[_trans] 579 | if _template_trans_actions[_trans] != 0 580 | case _template_trans_actions[_trans] 581 | when 5 then 582 | # line 24 "template.rl" 583 | begin 584 | 585 | instruction_begin = p 586 | end 587 | when 6 then 588 | # line 28 "template.rl" 589 | begin 590 | 591 | instruction_end = p 592 | end 593 | when 9 then 594 | # line 40 "template.rl" 595 | begin 596 | 597 | raise ParseError.new("failed to parse instruction", buffer, p) 598 | end 599 | when 16 then 600 | # line 13 "/home/samuel/Documents/ioquatix/trenni/parsers/trenni/template.rl" 601 | begin 602 | begin 603 | stack[top] = cs 604 | top+= 1 605 | cs = 21 606 | _goto_level = _again 607 | next 608 | end 609 | end 610 | when 13 then 611 | # line 17 "/home/samuel/Documents/ioquatix/trenni/parsers/trenni/template.rl" 612 | begin 613 | begin 614 | stack[top] = cs 615 | top+= 1 616 | cs = 21 617 | _goto_level = _again 618 | next 619 | end 620 | end 621 | when 14 then 622 | # line 20 "/home/samuel/Documents/ioquatix/trenni/parsers/trenni/template.rl" 623 | begin 624 | begin 625 | top -= 1 626 | cs = stack[top] 627 | _goto_level = _again 628 | next 629 | end 630 | end 631 | when 2 then 632 | # line 1 "NONE" 633 | begin 634 | te = p+1 635 | end 636 | when 8 then 637 | # line 36 "template.rl" 638 | begin 639 | te = p+1 640 | begin 641 | delegate.instruction(data.byteslice(instruction_begin...instruction_end), "\n") 642 | end 643 | end 644 | when 10 then 645 | # line 60 "template.rl" 646 | begin 647 | te = p+1 648 | begin 649 | delegate.text(data.byteslice(ts...te)) 650 | end 651 | end 652 | when 22 then 653 | # line 60 "template.rl" 654 | begin 655 | te = p 656 | p = p - 1; begin 657 | delegate.text(data.byteslice(ts...te)) 658 | end 659 | end 660 | when 24 then 661 | # line 32 "template.rl" 662 | begin 663 | te = p 664 | p = p - 1; begin 665 | delegate.instruction(data.byteslice(instruction_begin...instruction_end)) 666 | end 667 | end 668 | when 21 then 669 | # line 60 "template.rl" 670 | begin 671 | te = p 672 | p = p - 1; begin 673 | delegate.text(data.byteslice(ts...te)) 674 | end 675 | end 676 | when 1 then 677 | # line 60 "template.rl" 678 | begin 679 | begin p = ((te))-1; end 680 | begin 681 | delegate.text(data.byteslice(ts...te)) 682 | end 683 | end 684 | when 3 then 685 | # line 60 "template.rl" 686 | begin 687 | begin p = ((te))-1; end 688 | begin 689 | delegate.text(data.byteslice(ts...te)) 690 | end 691 | end 692 | when 7 then 693 | # line 1 "NONE" 694 | begin 695 | case act 696 | when 3 then 697 | begin begin p = ((te))-1; end 698 | 699 | delegate.instruction(data.byteslice(instruction_begin...instruction_end)) 700 | end 701 | when 6 then 702 | begin begin p = ((te))-1; end 703 | 704 | delegate.text(data.byteslice(ts...te)) 705 | end 706 | end 707 | end 708 | when 4 then 709 | # line 40 "template.rl" 710 | begin 711 | 712 | raise ParseError.new("failed to parse instruction", buffer, p) 713 | end 714 | # line 60 "template.rl" 715 | begin 716 | begin p = ((te))-1; end 717 | begin 718 | delegate.text(data.byteslice(ts...te)) 719 | end 720 | end 721 | when 23 then 722 | # line 44 "template.rl" 723 | begin 724 | 725 | expression_begin = p 726 | end 727 | # line 53 "/home/samuel/Documents/ioquatix/trenni/parsers/trenni/template.rl" 728 | begin 729 | te = p 730 | p = p - 1; begin cs = 32; end 731 | end 732 | when 15 then 733 | # line 13 "/home/samuel/Documents/ioquatix/trenni/parsers/trenni/template.rl" 734 | begin 735 | begin 736 | stack[top] = cs 737 | top+= 1 738 | cs = 21 739 | _goto_level = _again 740 | next 741 | end 742 | end 743 | # line 17 "/home/samuel/Documents/ioquatix/trenni/parsers/trenni/template.rl" 744 | begin 745 | begin 746 | stack[top] = cs 747 | top+= 1 748 | cs = 21 749 | _goto_level = _again 750 | next 751 | end 752 | end 753 | when 11 then 754 | # line 1 "NONE" 755 | begin 756 | te = p+1 757 | end 758 | # line 32 "template.rl" 759 | begin 760 | act = 3; end 761 | when 20 then 762 | # line 1 "NONE" 763 | begin 764 | te = p+1 765 | end 766 | # line 60 "template.rl" 767 | begin 768 | act = 6; end 769 | when 18 then 770 | # line 48 "template.rl" 771 | begin 772 | 773 | expression_end = p 774 | end 775 | # line 52 "template.rl" 776 | begin 777 | 778 | delegate.expression(data.byteslice(expression_begin...expression_end)) 779 | end 780 | # line 21 "/home/samuel/Documents/ioquatix/trenni/parsers/trenni/template.rl" 781 | begin 782 | cs = 43; end 783 | # line 784 "template.rb" 784 | end 785 | end 786 | end 787 | if _goto_level <= _again 788 | case _template_to_state_actions[cs] 789 | when 12 then 790 | # line 1 "NONE" 791 | begin 792 | ts = nil; end 793 | # line 794 "template.rb" 794 | end 795 | 796 | if cs == 0 797 | _goto_level = _out 798 | next 799 | end 800 | p += 1 801 | if p != pe 802 | _goto_level = _resume 803 | next 804 | end 805 | end 806 | if _goto_level <= _test_eof 807 | if p == eof 808 | if _template_eof_trans[cs] > 0 809 | _trans = _template_eof_trans[cs] - 1; 810 | _goto_level = _eof_trans 811 | next; 812 | end 813 | case _template_eof_actions[cs] 814 | when 9 then 815 | # line 40 "template.rl" 816 | begin 817 | 818 | raise ParseError.new("failed to parse instruction", buffer, p) 819 | end 820 | when 17 then 821 | # line 56 "template.rl" 822 | begin 823 | 824 | raise ParseError.new("failed to parse expression", buffer, p) 825 | end 826 | # line 827 "template.rb" 827 | end 828 | end 829 | 830 | end 831 | if _goto_level <= _out 832 | break 833 | end 834 | end 835 | end 836 | 837 | # line 89 "template.rl" 838 | 839 | if p != eof 840 | raise ParseError.new("could not consume all input", buffer, p) 841 | end 842 | 843 | return nil 844 | end 845 | end 846 | end 847 | -------------------------------------------------------------------------------- /lib/trenni/fallback/template.rl: -------------------------------------------------------------------------------- 1 | # Copyright, 2016, by Samuel G. D. Williams. 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | 21 | %%{ 22 | machine template; 23 | 24 | action instruction_begin { 25 | instruction_begin = p 26 | } 27 | 28 | action instruction_end { 29 | instruction_end = p 30 | } 31 | 32 | action emit_instruction { 33 | delegate.instruction(data.byteslice(instruction_begin...instruction_end)) 34 | } 35 | 36 | action emit_instruction_line { 37 | delegate.instruction(data.byteslice(instruction_begin...instruction_end), "\n") 38 | } 39 | 40 | action instruction_error { 41 | raise ParseError.new("failed to parse instruction", buffer, p) 42 | } 43 | 44 | action expression_begin { 45 | expression_begin = p 46 | } 47 | 48 | action expression_end { 49 | expression_end = p 50 | } 51 | 52 | action emit_expression { 53 | delegate.expression(data.byteslice(expression_begin...expression_end)) 54 | } 55 | 56 | action expression_error { 57 | raise ParseError.new("failed to parse expression", buffer, p) 58 | } 59 | 60 | action emit_text { 61 | delegate.text(data.byteslice(ts...te)) 62 | } 63 | 64 | # This magic ensures that we process bytes. 65 | getkey bytes[p]; 66 | 67 | include template "trenni/template.rl"; 68 | }%% 69 | 70 | require_relative '../error' 71 | 72 | module Trenni 73 | module Fallback 74 | %% write data; 75 | 76 | def self.parse_template(buffer, delegate) 77 | data = buffer.read 78 | bytes = data.bytes 79 | 80 | p = 0 81 | pe = eof = data.bytesize 82 | stack = [] 83 | 84 | expression_begin = expression_end = nil 85 | instruction_begin = instruction_end = nil 86 | 87 | %% write init; 88 | %% write exec; 89 | 90 | if p != eof 91 | raise ParseError.new("could not consume all input", buffer, p) 92 | end 93 | 94 | return nil 95 | end 96 | end 97 | end 98 | -------------------------------------------------------------------------------- /lib/trenni/markup.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Copyright, 2016, by Samuel G. D. Williams. 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | # THE SOFTWARE. 22 | 23 | require 'cgi' 24 | 25 | module Trenni 26 | # A wrapper which indicates that `value` can be appended to the output buffer without any changes. 27 | module Markup 28 | # Converts special characters `<`, `>`, `&`, and `"` into their equivalent entities. 29 | # @return [String] May return the original string if no changes were made. 30 | def self.escape_string(string) 31 | CGI.escape_html(string) 32 | end 33 | 34 | # Appends a string to the output buffer, escaping if if necessary. 35 | def self.append(buffer, value) 36 | if value.is_a? Markup 37 | buffer << value 38 | elsif value 39 | buffer << self.escape_string(value.to_s) 40 | end 41 | end 42 | end 43 | 44 | # Initialized from text which is escaped to use HTML entities. 45 | class MarkupString < String 46 | include Markup 47 | 48 | # @param string [String] the string value itself. 49 | # @param escape [Boolean] whether or not to escape the string. 50 | def initialize(string = nil, escape = true) 51 | if string 52 | if escape 53 | string = Markup.escape_string(string) 54 | end 55 | 56 | super(string) 57 | else 58 | super() 59 | end 60 | end 61 | 62 | # Generate a valid MarkupString withot any escaping. 63 | def self.raw(string) 64 | self.new(string, false) 65 | end 66 | end 67 | 68 | module Script 69 | def self.json(value) 70 | MarkupString.new(JSON.dump(value), false) 71 | end 72 | end 73 | end 74 | -------------------------------------------------------------------------------- /lib/trenni/native.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Copyright, 2016, by Samuel G. D. Williams. 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | # THE SOFTWARE. 22 | 23 | require_relative 'error' 24 | 25 | # Methods on the following classes may be replaced by native implementations: 26 | require_relative 'tag' 27 | 28 | begin 29 | # Load native code: 30 | require 'trenni/trenni' 31 | rescue LoadError 32 | warn "Could not load native implementation: #{$!}" if $VERBOSE 33 | end unless ENV['TRENNI_PREFER_FALLBACK'] 34 | -------------------------------------------------------------------------------- /lib/trenni/parse_delegate.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Copyright, 2012, by Samuel G. D. Williams. 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | # THE SOFTWARE. 22 | 23 | module Trenni 24 | # This is a sample delegate for capturing all events. It's only use is for testing. 25 | class ParseDelegate 26 | def initialize 27 | @events = [] 28 | end 29 | 30 | attr :events 31 | 32 | def method_missing(*args) 33 | @events << args 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/trenni/parsers.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative 'native' 4 | require_relative 'parse_delegate' 5 | 6 | if defined? Trenni::Native 7 | Trenni::Parsers = Trenni::Native 8 | else 9 | require_relative 'fallback/markup' 10 | require_relative 'fallback/template' 11 | require_relative 'fallback/query' 12 | 13 | Trenni::Parsers = Trenni::Fallback 14 | end 15 | -------------------------------------------------------------------------------- /lib/trenni/query.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Copyright, 2020, by Samuel G. D. Williams. 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | # THE SOFTWARE. 22 | 23 | require_relative 'buffer' 24 | 25 | require 'uri' 26 | 27 | module Trenni 28 | class Query < Hash 29 | def parse(buffer) 30 | Parsers.parse_query(buffer, Delegate.new(self)) 31 | end 32 | 33 | class Delegate 34 | def initialize(top = {}) 35 | @top = top 36 | 37 | @current = @top 38 | @index = nil 39 | end 40 | 41 | def string(key, encoded) 42 | if encoded 43 | key = ::URI.decode_www_form_component(key) 44 | end 45 | 46 | index(key.to_sym) 47 | end 48 | 49 | def integer(key) 50 | index(key.to_i) 51 | end 52 | 53 | def index(key) 54 | if @index 55 | @current = @current.fetch(@index) do 56 | @current[@index] = {} 57 | end 58 | end 59 | 60 | @index = key 61 | end 62 | 63 | def append 64 | if @index 65 | @current = @current.fetch(@index) do 66 | @current[@index] = [] 67 | end 68 | end 69 | 70 | @index = @current.size 71 | end 72 | 73 | def assign(value, encoded) 74 | if encoded 75 | value = ::URI.decode_www_form_component(value) 76 | end 77 | 78 | @current[@index] = value 79 | 80 | @current = @top 81 | @index = nil 82 | end 83 | 84 | def pair 85 | if @index 86 | @current[@index] = true 87 | end 88 | 89 | @current = @top 90 | @index = nil 91 | end 92 | end 93 | end 94 | end 95 | -------------------------------------------------------------------------------- /lib/trenni/reference.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Copyright, 2020, by Samuel G. D. Williams. 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | # THE SOFTWARE. 22 | 23 | require_relative 'query' 24 | 25 | module Trenni 26 | class Reference 27 | def initialize(path, query = {}, fragment: nil) 28 | @path = path.to_s 29 | @query = query 30 | @fragment = fragment 31 | end 32 | 33 | # The path component of the URI, e.g. /foo/bar/index.html 34 | attr :path 35 | 36 | # The query parameters. 37 | attr :query 38 | 39 | # A fragment identifier, the part after the '#' 40 | attr :fragment 41 | 42 | def append(buffer) 43 | buffer << escape_path(@path) 44 | 45 | unless @query.empty? 46 | buffer << '?' << query_string 47 | end 48 | 49 | if @fragment 50 | buffer << '#' << escape(@fragment) 51 | end 52 | 53 | return buffer 54 | end 55 | 56 | def to_str 57 | append(String.new) 58 | end 59 | 60 | alias to_s to_str 61 | 62 | private 63 | 64 | # According to https://tools.ietf.org/html/rfc3986#section-3.3, we escape non-pchar. 65 | NON_PCHAR = /([^ 66 | a-zA-Z0-9 67 | \-\._~ 68 | !\$&'\(\)\*\+,;= 69 | :@\/ 70 | ]+)/x.freeze 71 | 72 | def escape_path(path) 73 | encoding = path.encoding 74 | path.b.gsub(NON_PCHAR) do |m| 75 | '%' + m.unpack('H2' * m.bytesize).join('%').upcase 76 | end.force_encoding(encoding) 77 | end 78 | 79 | # Escapes a generic string, using percent encoding. 80 | def escape(string) 81 | encoding = string.encoding 82 | string.b.gsub(/([^a-zA-Z0-9_.\-]+)/) do |m| 83 | '%' + m.unpack('H2' * m.bytesize).join('%').upcase 84 | end.force_encoding(encoding) 85 | end 86 | 87 | def query_string 88 | build_nested_query(@query) 89 | end 90 | 91 | def build_nested_query(value, prefix = nil) 92 | case value 93 | when Array 94 | value.map { |v| 95 | build_nested_query(v, "#{prefix}[]") 96 | }.join("&") 97 | when Hash 98 | value.map { |k, v| 99 | build_nested_query(v, prefix ? "#{prefix}[#{escape(k.to_s)}]" : escape(k.to_s)) 100 | }.reject(&:empty?).join('&') 101 | when nil 102 | prefix 103 | else 104 | raise ArgumentError, "value must be a Hash" if prefix.nil? 105 | "#{prefix}=#{escape(value.to_s)}" 106 | end 107 | end 108 | end 109 | 110 | # Generate a URI from a path and user parameters. The path may contain a `#fragment` or `?query=parameters`. 111 | def self.Reference(path = '', **parameters) 112 | base, fragment = path.split('#', 2) 113 | path, query_string = base.split('?', 2) 114 | 115 | query = Query.new 116 | 117 | if query_string 118 | query.parse(Buffer.new(query_string)) 119 | end 120 | 121 | query.update(parameters) 122 | 123 | Reference.new(path, query, fragment: fragment) 124 | end 125 | end 126 | -------------------------------------------------------------------------------- /lib/trenni/strings.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Copyright, 2012, by Samuel G. D. Williams. 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | # THE SOFTWARE. 22 | 23 | module Trenni 24 | module Strings 25 | HTML_ESCAPE = {"&" => "&", "<" => "<", ">" => ">", "\"" => """} 26 | HTML_ESCAPE_PATTERN = Regexp.new("[" + Regexp.quote(HTML_ESCAPE.keys.join) + "]") 27 | 28 | def self.to_html(string) 29 | string.gsub(HTML_ESCAPE_PATTERN){|c| HTML_ESCAPE[c]} 30 | end 31 | 32 | def self.to_quoted_string(string) 33 | string = string.gsub('"', '\\"') 34 | string.gsub!(/\r/, "\\r") 35 | string.gsub!(/\n/, "\\n") 36 | 37 | return "\"#{string}\"" 38 | end 39 | 40 | # `value` must already be escaped. 41 | def self.to_attribute(key, value) 42 | %Q{#{key}="#{value}"} 43 | end 44 | 45 | def self.to_simple_attribute(key, strict) 46 | strict ? %Q{#{key}="#{key}"} : key.to_s 47 | end 48 | 49 | def self.to_title(string) 50 | string = string.gsub(/(^|[ \-_])(.)/){" " + $2.upcase} 51 | string.strip! 52 | 53 | return string 54 | end 55 | 56 | def self.to_snake(string) 57 | string = string.gsub("::", "") 58 | string.gsub!(/([A-Z]+)/){"_" + $1.downcase} 59 | string.sub!(/^_+/, "") 60 | 61 | return string 62 | end 63 | end 64 | end -------------------------------------------------------------------------------- /lib/trenni/tag.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Copyright, 2012, by Samuel G. D. Williams. 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | # THE SOFTWARE. 22 | 23 | require_relative 'markup' 24 | 25 | module Trenni 26 | # This represents an individual SGML tag, e.g. , or , with attributes. Attribute values must be escaped. 27 | Tag = Struct.new(:name, :closed, :attributes) do 28 | include Trenni::Markup 29 | 30 | def self.split(qualified_name) 31 | if i = qualified_name.index(':') 32 | return qualified_name.slice(0...i), qualified_name.slice(i+1..-1) 33 | else 34 | return nil, qualified_name 35 | end 36 | end 37 | 38 | def self.closed(name, attributes = {}) 39 | self.new(name, true, attributes) 40 | end 41 | 42 | def self.opened(name, attributes = {}) 43 | self.new(name, false, attributes) 44 | end 45 | 46 | def [] key 47 | attributes[key] 48 | end 49 | 50 | alias to_hash attributes 51 | 52 | def to_s(content = nil) 53 | self.class.format_tag(name, attributes, content || !closed) 54 | end 55 | 56 | alias to_str to_s 57 | 58 | def self_closed? 59 | closed 60 | end 61 | 62 | def write_opening_tag(buffer) 63 | buffer << '<' << name 64 | 65 | self.class.append_attributes(buffer, attributes, nil) 66 | 67 | if self_closed? 68 | buffer << '/>' 69 | else 70 | buffer << '>' 71 | end 72 | end 73 | 74 | def write_closing_tag(buffer) 75 | buffer << '' 76 | end 77 | 78 | def write(buffer, content = nil) 79 | self.class.append_tag(buffer, name, attributes, content || !closed) 80 | end 81 | 82 | def self.format_tag(name, attributes, content) 83 | buffer = String.new.force_encoding(name.encoding) 84 | 85 | self.append_tag(buffer, name, attributes, content) 86 | 87 | return buffer 88 | end 89 | 90 | def self.append_tag(buffer, name, attributes, content) 91 | buffer << '<' << name.to_s 92 | 93 | self.append_attributes(buffer, attributes, nil) 94 | 95 | if !content 96 | buffer << '/>' 97 | else 98 | buffer << '>' 99 | unless content == true 100 | Markup.append(buffer, content) 101 | end 102 | buffer << '' 103 | end 104 | 105 | return nil 106 | end 107 | 108 | # Convert a set of attributes into a string suitable for use within a . 109 | def self.append_attributes(buffer, attributes, prefix) 110 | attributes.each do |key, value| 111 | next unless value 112 | 113 | attribute_key = prefix ? "#{prefix}-#{key}" : key 114 | 115 | case value 116 | when Hash 117 | self.append_attributes(buffer, value, attribute_key) 118 | when Array 119 | self.append_attributes(buffer, value, attribute_key) 120 | when TrueClass 121 | buffer << ' ' << attribute_key.to_s 122 | else 123 | buffer << ' ' << attribute_key.to_s << '="' 124 | Markup.append(buffer, value) 125 | buffer << '"' 126 | end 127 | end 128 | 129 | return nil 130 | end 131 | end 132 | end 133 | -------------------------------------------------------------------------------- /lib/trenni/template.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Copyright, 2012, by Samuel G. D. Williams. 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | # THE SOFTWARE. 22 | 23 | require_relative 'parsers' 24 | require_relative 'markup' 25 | require_relative 'buffer' 26 | 27 | module Trenni 28 | # The output variable that will be used in templates: 29 | OUT = :_out 30 | BINDING = binding 31 | 32 | class Builder 33 | def capture(*arguments, &block) 34 | Template.capture(*arguments, output: self, &block) 35 | end 36 | end 37 | 38 | class Template 39 | # Returns the output produced by calling the given block. 40 | def self.capture(*arguments, output: nil, &block) 41 | scope = block.binding 42 | previous_output = scope.local_variable_get(OUT) 43 | 44 | output ||= previous_output.class.new(encoding: previous_output.encoding) 45 | scope.local_variable_set(OUT, output) 46 | 47 | begin 48 | block.call(*arguments) 49 | ensure 50 | scope.local_variable_set(OUT, previous_output) 51 | end 52 | 53 | return output 54 | end 55 | 56 | # Returns the buffer used for capturing output. 57 | def self.buffer(binding) 58 | binding.local_variable_get(OUT) 59 | end 60 | 61 | class Assembler 62 | def initialize(encoding: Encoding::UTF_8) 63 | @code = String.new.force_encoding(encoding) 64 | end 65 | 66 | attr :code 67 | 68 | # Output raw text to the template. 69 | def text(text) 70 | text = text.gsub("'", "\\\\'") 71 | @code << "#{OUT}<<'#{text}';" 72 | 73 | # This is an interesting approach, but it doens't preserve newlines or tabs as raw characters, so template line numbers don't match up. 74 | # @parts << "#{OUT}<<#{text.dump};" 75 | end 76 | 77 | # Output a ruby expression (or part of). 78 | def instruction(text, postfix = nil) 79 | @code << text << (postfix || ';') 80 | end 81 | 82 | # Output a string interpolation. 83 | def expression(text) 84 | # Double brackets are required here to handle expressions like #{foo rescue "bar"}. 85 | @code << "#{OUT}< 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | # THE SOFTWARE. 22 | 23 | module Trenni 24 | # This class is superceeded by `Trenni::Reference`. 25 | class URI 26 | def initialize(path, query_string, fragment, parameters) 27 | @path = path 28 | @query_string = query_string 29 | @fragment = fragment 30 | @parameters = parameters 31 | end 32 | 33 | # The path component of the URI, e.g. /foo/bar/index.html 34 | attr :path 35 | 36 | # The un-parsed query string of the URI, e.g. 'x=10&y=20' 37 | attr :query_string 38 | 39 | # A fragment identifier, the part after the '#' 40 | attr :fragment 41 | 42 | # User supplied parameters that will be appended to the query part. 43 | attr :parameters 44 | 45 | def append(buffer) 46 | if @query_string 47 | buffer << escape_path(@path) << '?' << query_string 48 | buffer << '&' << query_parameters if @parameters 49 | else 50 | buffer << escape_path(@path) 51 | buffer << '?' << query_parameters if @parameters 52 | end 53 | 54 | if @fragment 55 | buffer << '#' << escape(@fragment) 56 | end 57 | 58 | return buffer 59 | end 60 | 61 | def to_str 62 | append(String.new) 63 | end 64 | 65 | alias to_s to_str 66 | 67 | private 68 | 69 | # According to https://tools.ietf.org/html/rfc3986#section-3.3, we escape non-pchar. 70 | NON_PCHAR = /([^a-zA-Z0-9_\-\.~!$&'()*+,;=:@\/]+)/.freeze 71 | 72 | def escape_path(path) 73 | encoding = path.encoding 74 | path.b.gsub(NON_PCHAR) do |m| 75 | '%' + m.unpack('H2' * m.bytesize).join('%').upcase 76 | end.force_encoding(encoding) 77 | end 78 | 79 | # Escapes a generic string, using percent encoding. 80 | def escape(string) 81 | encoding = string.encoding 82 | string.b.gsub(/([^a-zA-Z0-9_.\-]+)/) do |m| 83 | '%' + m.unpack('H2' * m.bytesize).join('%').upcase 84 | end.force_encoding(encoding) 85 | end 86 | 87 | def query_parameters 88 | build_nested_query(@parameters) 89 | end 90 | 91 | def build_nested_query(value, prefix = nil) 92 | case value 93 | when Array 94 | value.map { |v| 95 | build_nested_query(v, "#{prefix}[]") 96 | }.join("&") 97 | when Hash 98 | value.map { |k, v| 99 | build_nested_query(v, prefix ? "#{prefix}[#{escape(k.to_s)}]" : escape(k.to_s)) 100 | }.reject(&:empty?).join('&') 101 | when nil 102 | prefix 103 | else 104 | raise ArgumentError, "value must be a Hash" if prefix.nil? 105 | "#{prefix}=#{escape(value.to_s)}" 106 | end 107 | end 108 | end 109 | 110 | # Generate a URI from a path and user parameters. The path may contain a `#fragment` or `?query=parameters`. 111 | def self.URI(path = '', parameters = nil) 112 | base, fragment = path.split('#', 2) 113 | path, query_string = base.split('?', 2) 114 | 115 | URI.new(path, query_string, fragment, parameters) 116 | end 117 | end 118 | -------------------------------------------------------------------------------- /lib/trenni/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Copyright, 2012, by Samuel G. D. Williams. 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | # THE SOFTWARE. 22 | 23 | module Trenni 24 | VERSION = "3.14.0" 25 | end 26 | -------------------------------------------------------------------------------- /parsers/trenni/entities.rl: -------------------------------------------------------------------------------- 1 | %%{ 2 | machine entities; 3 | 4 | parse_entity := ( 5 | [a-zA-Z0-9]+ >entity_begin %entity_name ';' | 6 | '#x' [0-9a-fA-F]+ >entity_begin %entity_hex ';' | 7 | '#' [0-9]+ >entity_begin %entity_number ';' 8 | ) @{fret;} @err(entity_error); 9 | 10 | entity = '&' @{fcall parse_entity;}; 11 | }%% 12 | -------------------------------------------------------------------------------- /parsers/trenni/markup.rl: -------------------------------------------------------------------------------- 1 | %%{ 2 | machine markup; 3 | 4 | unicode = any - ascii; 5 | identifier_character = unicode | [a-zA-Z0-9\-_\.:]; 6 | 7 | # > is called on entering, % is called on exiting. 8 | identifier = identifier_character+ >identifier_begin %identifier_end; 9 | 10 | cdata_text = (any* -- ']]>'); 11 | cdata = 'cdata_begin (cdata_text ']]>') %cdata_end @err(cdata_error); 12 | 13 | include entities "entities.rl"; 14 | 15 | pcdata_character = any - [<&]; 16 | pcdata_characters = pcdata_character+ >characters_begin %characters_end; 17 | pcdata = ((pcdata_characters | entity) $(pcdata,2) %(pcdata,1))+ %(pcdata,0) >pcdata_begin %pcdata_end; 18 | 19 | text = pcdata >text_begin %text_end; 20 | 21 | doctype_text = (any* -- '>'); 22 | doctype = 'doctype_begin (doctype_text '>') %doctype_end @err(doctype_error); 23 | 24 | comment_text = (any* -- '-->'); 25 | comment = '') %comment_end @err(comment_error); 26 | 27 | # Markup Instructions 28 | instruction_text = (any* -- '?>'); 29 | instruction = 'instruction_begin (identifier (space+ instruction_text) >instruction_text_begin %instruction_text_end '?>') %instruction_end @err(instruction_error); 30 | 31 | attribute_quoted_value = 32 | '"' (pcdata -- '"') '"' %attribute_value | '""' %attribute_empty | 33 | "'" (pcdata -- "'") "'" %attribute_value | "''" %attribute_empty; 34 | 35 | attribute = identifier >attribute_begin ('=' attribute_quoted_value)? %attribute; 36 | 37 | # The @err handler will be triggered if the parser finishes in any state except the final accepting state. 38 | tag_opening = '<' >tag_opening_begin (identifier %tag_name (space+ attribute)* space* ('/' >tag_self_closing)? '>') %tag_opening_end @err(tag_error); 39 | 40 | tag_closing = 'tag_closing_begin (identifier '>') %tag_closing_end @err(tag_error); 41 | 42 | main := (text | tag_opening | tag_closing | instruction | comment | doctype | cdata)**; 43 | }%% -------------------------------------------------------------------------------- /parsers/trenni/query.rl: -------------------------------------------------------------------------------- 1 | %%{ 2 | machine query; 3 | 4 | # An application/x-www-form-urlencoded parser based on the definition by WhatWG. 5 | # Based on https://url.spec.whatwg.org/#application/x-www-form-urlencoded 6 | pchar = any - [&=\[\]%+]; 7 | echar = pchar | ('+' | '%' xdigit xdigit) >encoded; 8 | 9 | integer = ([0-9]+) >integer_begin %integer_end; 10 | string = (echar+ - integer) >string_begin %string_end; 11 | 12 | value = (echar*) >value_begin %value_end; 13 | 14 | index = string ( 15 | '[' (integer | string) ']' 16 | )* ('[]' %append)?; 17 | 18 | pair = ( 19 | index ('=' value)? 20 | ) %pair; 21 | 22 | main := ((pair '&')* pair)?; 23 | }%% -------------------------------------------------------------------------------- /parsers/trenni/template.rl: -------------------------------------------------------------------------------- 1 | %%{ 2 | machine template; 3 | 4 | unicode = any - ascii; 5 | identifier = (unicode | [a-zA-Z0-9\-_\.:])+; 6 | 7 | newline = [\n]; 8 | 9 | expression_start = '#{' %expression_begin; 10 | 11 | # This expression handles both single quoted and double quoted strings in Ruby. As Ruby supports nested string interpolations, we need to handle this too. 12 | expression_quoted = 13 | '"' (any - '"' | '\"' | '#{' @{fcall parse_nested_expression;})* '"' | 14 | "'" (any - "'" | "\'")* "'" 15 | ; 16 | 17 | expression_nested = '{' @{fcall parse_nested_expression;}; 18 | expression_value = ([^"'{}]+ | expression_quoted | expression_nested)*; 19 | 20 | parse_nested_expression := expression_value '}' @{fret;}; 21 | parse_expression := (expression_value %expression_end '}') @err(expression_error) @emit_expression @{fnext main;}; 22 | 23 | pcdata = any - [#<] | '#' [^{] | '<' [^?]; 24 | 25 | text = (pcdata - newline)*; 26 | 27 | text_lines = ( 28 | text newline 29 | )*; 30 | 31 | # We are only interested in instructions that start with r: 32 | instruction = '])*) >instruction_begin %instruction_end 34 | '?>') @err(instruction_error); 35 | 36 | instruction_line = (space - newline)* instruction (space - newline)* newline; 37 | 38 | other_instruction = '])* '?>' 40 | ) @err(instruction_error); 41 | 42 | main := |* 43 | # Matches a full instruction line (consume whitespace and newline): 44 | instruction_line => emit_instruction_line; 45 | 46 | # Matches multiple lines of only text: 47 | text_lines => emit_text; 48 | 49 | # Matches a single instruction: 50 | instruction => emit_instruction; 51 | 52 | # Matches a single expression: #{foo} 53 | expression_start => {fnext parse_expression;}; 54 | 55 | other_instruction => emit_text; 56 | 57 | # Matches text: 58 | text => emit_text; 59 | *|; 60 | }%% -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'covered/rspec' 4 | require 'trenni' 5 | 6 | begin 7 | require 'ruby-prof' 8 | 9 | RSpec.shared_context "profile" do 10 | before(:all) do 11 | RubyProf.start 12 | end 13 | 14 | after(:all) do 15 | result = RubyProf.stop 16 | 17 | # Print a flat profile to text 18 | printer = RubyProf::FlatPrinter.new(result) 19 | printer.print(STDOUT) 20 | end 21 | end 22 | rescue LoadError 23 | RSpec.shared_context "profile" do 24 | before(:all) do 25 | puts "Profiling not supported on this platform." 26 | end 27 | end 28 | end 29 | 30 | RSpec.configure do |config| 31 | # Enable flags like --only-failures and --next-failure 32 | config.example_status_persistence_file_path = ".rspec_status" 33 | 34 | config.expect_with :rspec do |c| 35 | c.syntax = :expect 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /spec/trenni/builder_spec.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env rspec 2 | # frozen_string_literal: true 3 | 4 | # Copyright, 2012, by Samuel G. D. Williams. 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a copy 7 | # of this software and associated documentation files (the "Software"), to deal 8 | # in the Software without restriction, including without limitation the rights 9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | # copies of the Software, and to permit persons to whom the Software is 11 | # furnished to do so, subject to the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be included in 14 | # all copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | # THE SOFTWARE. 23 | 24 | require 'trenni/builder' 25 | 26 | RSpec.describe Trenni::Builder do 27 | it "should produce valid html" do 28 | subject.doctype 29 | subject.tag('html') do 30 | subject.tag('head') do 31 | subject.inline('title') do 32 | subject.text('Hello World') 33 | end 34 | end 35 | subject.tag('body') do 36 | end 37 | end 38 | 39 | expect(subject.output).to be == <<~HTML.chomp 40 | 41 | 42 | 43 | Hello World 44 | 45 | 46 | 47 | 48 | HTML 49 | end 50 | 51 | describe '.fragment' do 52 | it "should use an existing builder" do 53 | result = Trenni::Builder.fragment do |builder| 54 | end 55 | 56 | expect(result).to_not be_nil 57 | end 58 | 59 | it "should use an existing builder" do 60 | expect(Trenni::Builder).to receive(:new).and_call_original 61 | 62 | result = Trenni::Builder.fragment(subject) do |builder| 63 | end 64 | 65 | expect(result).to be_nil 66 | end 67 | end 68 | 69 | describe '#tag' do 70 | it "should format nested attributes" do 71 | subject.tag('div', data: {id: 10}) 72 | 73 | expect(subject.output).to be == '
' 74 | end 75 | 76 | it "should indent self-closing tag correctly" do 77 | builder = Trenni::Builder.new 78 | 79 | builder.tag('foo') {builder.tag('bar')} 80 | 81 | expect(builder.output).to be == <<~HTML.chomp 82 | 83 | 84 | 85 | HTML 86 | end 87 | 88 | it "should support compact attributes" do 89 | subject.tag :option, :required => true 90 | expect(subject.output).to be == %Q{
' 96 | end 97 | 98 | it "should order array attributes as specified" do 99 | subject.tag :t, [[:a, 10], [:b, 20]] 100 | expect(subject.output).to be == %Q{} 101 | end 102 | 103 | it "should order hash attributes as specified" do 104 | subject.tag :t, :b => 20, :a => 10 105 | expect(subject.output).to be == %Q{} 106 | end 107 | 108 | it "shouldn't output attributes with nil value" do 109 | subject.tag :t, [[:a, 10], [:b, nil]] 110 | expect(subject.output).to be == %Q{} 111 | end 112 | end 113 | 114 | describe '#inline' do 115 | it "should produce inline html" do 116 | subject.inline("div") do 117 | subject.tag("strong") do 118 | subject.text("Hello") 119 | end 120 | 121 | subject.text "World!" 122 | end 123 | 124 | expect(subject.output).to be == "
HelloWorld!
" 125 | end 126 | 127 | it "can inline fragments" do 128 | subject.inline! do 129 | subject.inline('a') do 130 | subject << "Hello" 131 | end 132 | 133 | subject.inline('a') do 134 | subject << "World" 135 | end 136 | end 137 | 138 | expect(subject.output).to be == "
HelloWorld" 139 | end 140 | 141 | it "escapes attributes and text correctly" do 142 | subject.inline :foo, :bar => %Q{"Hello World"} do 143 | subject.text %Q{if x < 10} 144 | end 145 | 146 | expect(subject.output).to be == %Q{if x < 10} 147 | end 148 | end 149 | 150 | describe '#<<' do 151 | it 'can append text' do 152 | subject << 'text' 153 | expect(subject.output).to be == "text" 154 | end 155 | 156 | it "doesn't append nil" do 157 | subject << nil 158 | expect(subject.output).to be == "" 159 | end 160 | end 161 | 162 | describe '#append' do 163 | it 'should be able to append nil' do 164 | expect{subject.append(nil)}.to_not raise_error 165 | end 166 | 167 | it 'should append existing markup' do 168 | subject.tag("outer") do 169 | subject.append("\n\t\n") 170 | end 171 | 172 | expect(subject.output).to be == <<~HTML.chomp 173 | 174 | 175 | 176 | 177 | 178 | HTML 179 | end 180 | end 181 | end 182 | -------------------------------------------------------------------------------- /spec/trenni/markup_parser_spec.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # Copyright, 2012, by Samuel G. D. Williams. 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a copy 7 | # of this software and associated documentation files (the "Software"), to deal 8 | # in the Software without restriction, including without limitation the rights 9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | # copies of the Software, and to permit persons to whom the Software is 11 | # furnished to do so, subject to the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be included in 14 | # all copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | # THE SOFTWARE. 23 | 24 | require 'trenni/parsers' 25 | require 'trenni/entities' 26 | require 'trenni/template' 27 | require 'trenni/markup' 28 | 29 | RSpec.shared_context "html parsers" do 30 | let(:delegate) {Trenni::ParseDelegate.new} 31 | let(:buffer) {Trenni::Buffer(subject)} 32 | let(:parsers) {Trenni::Parsers} 33 | let(:entities) {Trenni::Entities::HTML5} 34 | let(:events) {parsers.parse_markup(buffer, delegate, entities); delegate.events} 35 | end 36 | 37 | RSpec.shared_context "valid markup" do 38 | include_context "html parsers" 39 | 40 | it "should parse without error" do 41 | expect{events}.to_not raise_error 42 | end 43 | end 44 | 45 | RSpec.describe "
" do 46 | include_context "valid markup" 47 | 48 | it "should parse self-closing tag" do 49 | expect(events).to be == [ 50 | [:open_tag_begin, "br", 1], 51 | [:open_tag_end, true], 52 | ] 53 | end 54 | end 55 | 56 | RSpec.describe "" do 57 | include_context "valid markup" 58 | 59 | it "should parse doctype" do 60 | expect(events).to be == [ 61 | [:doctype, ""] 62 | ] 63 | end 64 | end 65 | 66 | RSpec.describe "" do 67 | include_context "valid markup" 68 | 69 | it "should parse instruction" do 70 | expect(events).to be == [ 71 | [:instruction, ""] 72 | ] 73 | end 74 | end 75 | 76 | RSpec.describe %Q{} do 77 | include_context "valid markup" 78 | 79 | it "should parse comment" do 80 | expect(events).to be == [ 81 | [:comment, ""] 82 | ] 83 | end 84 | end 85 | 86 | RSpec.describe "" do 87 | include_context "valid markup" 88 | 89 | it "should parse escaped attributes" do 90 | expect(events).to be == [ 91 | [:open_tag_begin, "tag", 1], 92 | [:attribute, "key", "A&B"], 93 | [:open_tag_end, true], 94 | ] 95 | end 96 | end 97 | 98 | RSpec.describe "Hello World" do 99 | include_context "valid markup" 100 | 101 | it "should parse tag with content" do 102 | expect(events).to be == [ 103 | [:open_tag_begin, "foo", 1], 104 | [:attribute, "bar", "20"], 105 | [:attribute, "baz", true], 106 | [:open_tag_end, false], 107 | [:text, "Hello World"], 108 | [:close_tag, "foo", 31], 109 | ] 110 | end 111 | 112 | it "should have same encoding" do 113 | expect(events[0][1].encoding).to be == subject.encoding 114 | expect(events[1][1].encoding).to be == subject.encoding 115 | expect(events[2][1].encoding).to be == subject.encoding 116 | expect(events[4][1].encoding).to be == subject.encoding 117 | expect(events[5][1].encoding).to be == subject.encoding 118 | end 119 | 120 | it "should track entities" do 121 | expect(events[1][2]).to be_kind_of Trenni::Markup 122 | expect(events[4][1]).to be_kind_of Trenni::Markup 123 | end 124 | end 125 | 126 | RSpec.describe "" do 127 | include_context "valid markup" 128 | 129 | it "should parse CDATA" do 130 | expect(events).to be == [ 131 | [:open_tag_begin, "test", 1], 132 | [:open_tag_end, false], 133 | [:cdata, ""], 134 | [:close_tag, "test", 31], 135 | ] 136 | end 137 | end 138 | 139 | RSpec.describe "" do 140 | include_context "valid markup" 141 | 142 | it "should parse empty attributes" do 143 | expect(events).to be == [ 144 | [:open_tag_begin, "foo", 1], 145 | [:attribute, "bar", ""], 146 | [:attribute, "baz", true], 147 | [:open_tag_end, true], 148 | ] 149 | end 150 | end 151 | 152 | RSpec.describe "

"

" do 153 | include_context "valid markup" 154 | 155 | let(:template_text) {%q{

#{events[3][1]}

}} 156 | let(:template_buffer) {Trenni::Buffer(template_text)} 157 | let(:template) {Trenni::MarkupTemplate.new(template_buffer)} 158 | 159 | it "should parse empty attributes" do 160 | expect(events).to be == [ 161 | [:open_tag_begin, "p", 1], 162 | [:attribute, "attr", "foo&bar"], 163 | [:open_tag_end, false], 164 | [:text, "\""], 165 | [:close_tag, "p", 30] 166 | ] 167 | end 168 | 169 | it "generates same output as input" do 170 | result = template.to_string(self) 171 | expect(result).to be == subject 172 | end 173 | 174 | it "should track entities" do 175 | expect(events[1][2]).to_not be_kind_of Trenni::Markup 176 | expect(events[3][1]).to_not be_kind_of Trenni::Markup 177 | end 178 | end 179 | 180 | RSpec.shared_examples "valid markup file" do |base| 181 | let(:xhtml_path) {File.join(__dir__, base + '.xhtml')} 182 | let(:events_path) {File.join(__dir__, base + '.rb')} 183 | 184 | subject {Trenni::FileBuffer.new(xhtml_path)} 185 | let(:expected_events) {eval(File.read(events_path), nil, events_path)} 186 | 187 | include_context "valid markup" 188 | 189 | def dump_events! 190 | File.open(events_path, "w+") do |output| 191 | output.puts "[" 192 | events.each do |event| 193 | output.puts "\t#{event.inspect}," 194 | end 195 | output.puts "]" 196 | end 197 | end 198 | 199 | it "should match events" do 200 | #dump_events! 201 | 202 | expected_events.each_with_index do |event, index| 203 | expect(events[index]).to be == event 204 | end 205 | end 206 | end 207 | 208 | RSpec.describe "corpus/large" do 209 | it_behaves_like "valid markup file", description 210 | end 211 | 212 | RSpec.shared_context "invalid markup" do 213 | include_context "html parsers" 214 | 215 | it "should fail to parse" do 216 | expect{events}.to raise_error Trenni::ParseError 217 | end 218 | end 219 | 220 | RSpec.describe "" do 225 | include_context "invalid markup" 226 | end 227 | 228 | RSpec.describe "

\nこんにちは World= 0 14 | CGI.escapeHTML(general_string) 15 | end 16 | end 17 | 18 | x.report("CGI.escapeHTML(code_string)") do |times| 19 | while (times -= 1) >= 0 20 | CGI.escapeHTML(code_string) 21 | end 22 | end 23 | 24 | x.report("Trenni::Markup.escape_string(general_string)") do |times| 25 | while (times -= 1) >= 0 26 | Trenni::Markup.escape_string(general_string) 27 | end 28 | end 29 | 30 | x.report("Trenni::Markup.escape_string(code_string)") do |times| 31 | while (times -= 1) >= 0 32 | Trenni::Markup.escape_string(code_string) 33 | end 34 | end 35 | 36 | x.compare! 37 | end 38 | end 39 | end -------------------------------------------------------------------------------- /spec/trenni/markup_spec.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env rspec 2 | # frozen_string_literal: true 3 | 4 | # Copyright, 2012, by Samuel G. D. Williams. 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a copy 7 | # of this software and associated documentation files (the "Software"), to deal 8 | # in the Software without restriction, including without limitation the rights 9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | # copies of the Software, and to permit persons to whom the Software is 11 | # furnished to do so, subject to the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be included in 14 | # all copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | # THE SOFTWARE. 23 | 24 | require 'trenni' 25 | require 'trenni/markup' 26 | 27 | RSpec.describe Trenni::MarkupString do 28 | let(:template) {Trenni::MarkupTemplate.load_file File.expand_path('template_spec/basic.trenni', __dir__)} 29 | 30 | let(:html_text) {"

Hello World

"} 31 | 32 | it "should escape unsafe text" do 33 | model = double(text: html_text) 34 | 35 | expect(template.to_string(model)).to be == "<h1>Hello World</h1>" 36 | end 37 | 38 | let(:safe_html_text) {Trenni::Builder.tag('h1', 'Hello World')} 39 | 40 | it "should not escape safe text" do 41 | model = double(text: safe_html_text) 42 | 43 | expect(template.to_string(model)).to be == html_text 44 | end 45 | 46 | it "should convert nil to empty string" do 47 | Trenni::Markup.append(subject, nil) 48 | 49 | expect(subject).to be_empty 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /spec/trenni/parsers_performance_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'benchmark/ips' 4 | require 'trenni/parsers' 5 | require 'trenni/entities' 6 | 7 | require 'trenni/query' 8 | require 'rack/utils' 9 | 10 | require 'nokogiri' 11 | 12 | RSpec.describe Trenni::Parsers do 13 | # include_context "profile" 14 | 15 | describe '#parse_markup' do 16 | let(:xhtml_path) {File.expand_path('corpus/large.xhtml', __dir__)} 17 | let(:xhtml_buffer) {Trenni::FileBuffer.new(xhtml_path)} 18 | let(:entities) {Trenni::Entities::HTML5} 19 | 20 | it "should be fast to parse large documents" do 21 | Benchmark.ips do |x| 22 | x.report("Large (Trenni)") do |times| 23 | delegate = Trenni::ParseDelegate.new 24 | 25 | while (times -= 1) >= 0 26 | Trenni::Parsers.parse_markup(xhtml_buffer, delegate, entities) 27 | 28 | delegate.events.clear 29 | end 30 | end 31 | 32 | x.report("Large (Nokogiri)") do |times| 33 | delegate = Trenni::ParseDelegate.new 34 | parser = Nokogiri::HTML::SAX::Parser.new(delegate) 35 | 36 | while (times -= 1) >= 0 37 | parser.parse(xhtml_buffer.read) 38 | 39 | delegate.events.clear 40 | end 41 | end 42 | 43 | x.compare! 44 | end 45 | end 46 | end 47 | 48 | describe '#parse_template' do 49 | let(:large_trenni_path) {File.expand_path('template_spec/large.trenni', __dir__)} 50 | let(:trenni_buffer) {Trenni::FileBuffer.new(large_trenni_path)} 51 | 52 | let(:large_erb_path) {File.expand_path('template_spec/large.erb', __dir__)} 53 | let(:erb_buffer) {Trenni::FileBuffer.new(large_erb_path)} 54 | 55 | it "should be fast to parse large templates" do 56 | Benchmark.ips do |x| 57 | x.report("Large (Trenni)") do |times| 58 | delegate = Trenni::ParseDelegate.new 59 | 60 | while (times -= 1) >= 0 61 | Trenni::Parsers.parse_template(trenni_buffer, delegate) 62 | 63 | delegate.events.clear 64 | end 65 | end 66 | 67 | x.report("Large (ERB)") do |times| 68 | while (times -= 1) >= 0 69 | ERB.new(erb_buffer.read) 70 | end 71 | end 72 | 73 | x.compare! 74 | end 75 | end 76 | end 77 | 78 | describe '#parse_query' do 79 | let(:string) {"foo=hi%20there&bar[blah]=123&bar[quux][0]=1&bar[quux][1]=2&bar[quux][2]=3"} 80 | 81 | it "should be fast to parse large query strings" do 82 | # query = Trenni::Query.new 83 | # query.parse(Trenni::Buffer.new string) 84 | # pp query 85 | # 86 | # pp Rack::Utils.parse_nested_query(string) 87 | 88 | Benchmark.ips do |x| 89 | x.report("Large (Trenni)") do |times| 90 | while (times -= 1) >= 0 91 | Trenni::Query.new.parse(Trenni::Buffer.new string) 92 | end 93 | end 94 | 95 | x.report("Large (Rack)") do |times| 96 | while (times -= 1) >= 0 97 | Rack::Utils.parse_nested_query(string) 98 | end 99 | end 100 | 101 | x.compare! 102 | end 103 | end 104 | end 105 | end 106 | -------------------------------------------------------------------------------- /spec/trenni/query_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Copyright, 2020, by Samuel G. D. Williams. 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | # THE SOFTWARE. 22 | 23 | require 'trenni/query' 24 | 25 | RSpec.describe Trenni::Query do 26 | def parse(string) 27 | subject.parse(Trenni::Buffer.new(string)) 28 | 29 | return subject 30 | end 31 | 32 | it "can parse query string with integer key" do 33 | expect(parse "q[0]=0").to be == {q: {0 => "0"}} 34 | end 35 | 36 | it "can parse query string with mixed integer/string key" do 37 | expect(parse "q[2d]=3d").to be == {q: {:'2d' => "3d"}} 38 | end 39 | 40 | it "can parse query string appending items to array" do 41 | expect(parse "q[]=a&q[]=b").to be == {q: ["a", "b"]} 42 | end 43 | 44 | it "can decode encoded keys" do 45 | expect(parse "hello+world=true").to be == {:"hello world" => "true"} 46 | end 47 | 48 | it "can decode encoded values" do 49 | expect(parse "message=hello+world").to be == {message: "hello world"} 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /spec/trenni/reference_spec.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # Copyright, 2012, by Samuel G. D. Williams. 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a copy 7 | # of this software and associated documentation files (the "Software"), to deal 8 | # in the Software without restriction, including without limitation the rights 9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | # copies of the Software, and to permit persons to whom the Software is 11 | # furnished to do so, subject to the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be included in 14 | # all copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | # THE SOFTWARE. 23 | 24 | require 'trenni/reference' 25 | 26 | RSpec.describe Trenni::Reference do 27 | describe Trenni::Reference('path with spaces/image.jpg') do 28 | it "encodes whitespace" do 29 | expect(subject.to_s).to be == "path%20with%20spaces/image.jpg" 30 | end 31 | end 32 | 33 | describe Trenni::Reference('path', array: [1, 2, 3]) do 34 | it "encodes array" do 35 | expect(subject.to_s).to be == "path?array[]=1&array[]=2&array[]=3" 36 | end 37 | end 38 | 39 | describe Trenni::Reference('path_with_underscores/image.jpg') do 40 | it "doesn't touch underscores" do 41 | expect(subject.to_s).to be == "path_with_underscores/image.jpg" 42 | end 43 | end 44 | 45 | describe Trenni::Reference('index', :'my name' => 'Bob Dole') do 46 | it "encodes query" do 47 | expect(subject.to_s).to be == "index?my%20name=Bob%20Dole" 48 | end 49 | end 50 | 51 | describe Trenni::Reference('index#All Your Base') do 52 | it "encodes fragment" do 53 | expect(subject.to_s).to be == "index\#All%20Your%20Base" 54 | end 55 | end 56 | 57 | describe Trenni::Reference('I/❤️/UNICODE', face: '😀') do 58 | it "encodes unicode" do 59 | expect(subject.to_s).to be == "I/%E2%9D%A4%EF%B8%8F/UNICODE?face=%F0%9F%98%80" 60 | end 61 | end 62 | 63 | it "can be an attribute" do 64 | tag = Trenni::Tag.closed('img', src: Trenni::Reference('image.jpg', x: 10)) 65 | 66 | expect(tag.to_s).to be == '' 67 | end 68 | 69 | describe Trenni::Reference("foo?bar=10&baz=20", yes: 'no') do 70 | it "can use existing query parameters" do 71 | expect(subject.to_s).to be == "foo?bar=10&baz=20&yes=no" 72 | end 73 | end 74 | 75 | describe Trenni::Reference("foo?yes=yes", yes: 'no') do 76 | it "overrides existing parameters" do 77 | expect(subject.to_s).to be == "foo?yes=no" 78 | end 79 | end 80 | 81 | describe Trenni::Reference('foo#frag') do 82 | it "can use existing fragment" do 83 | expect(subject.fragment).to be == "frag" 84 | expect(subject.to_s).to be == 'foo#frag' 85 | end 86 | end 87 | end 88 | -------------------------------------------------------------------------------- /spec/trenni/strings_spec.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # Copyright, 2012, by Samuel G. D. Williams. 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a copy 7 | # of this software and associated documentation files (the "Software"), to deal 8 | # in the Software without restriction, including without limitation the rights 9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | # copies of the Software, and to permit persons to whom the Software is 11 | # furnished to do so, subject to the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be included in 14 | # all copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | # THE SOFTWARE. 23 | 24 | require 'trenni/strings' 25 | 26 | module Trenni::StringsSpec 27 | describe Trenni::Strings do 28 | it "should escape html sensitive characters" do 29 | text = Trenni::Strings.to_html("") 30 | expect(text).to be == "<foobar>" 31 | 32 | text = Trenni::Strings.to_html(%q{"I'd like to do this & that :p", she said.}) 33 | expect(text).to be == %q{"I'd like to do this & that :p", she said.} 34 | end 35 | 36 | it "should generate quoted strings" do 37 | text = Trenni::Strings.to_quoted_string(%Q{"Hello World"}) 38 | expect(text).to be == %q{"\"Hello World\""} 39 | 40 | text = Trenni::Strings.to_quoted_string(%Q{"Hello\r\nWorld"}) 41 | expect(text).to be == %q{"\"Hello\r\nWorld\""} 42 | end 43 | 44 | it "should generate quoted attributes" do 45 | text = Trenni::Strings.to_attribute(:foo, 'bar') 46 | expect(text).to be == %Q{foo="bar"} 47 | 48 | text = Trenni::Strings.to_simple_attribute(:foo, false) 49 | expect(text).to be == %Q{foo} 50 | 51 | text = Trenni::Strings.to_simple_attribute(:foo, true) 52 | expect(text).to be == %Q{foo="foo"} 53 | end 54 | 55 | it "should generate nice titles" do 56 | text = Trenni::Strings.to_title("foo bar") 57 | expect(text).to be == "Foo Bar" 58 | end 59 | 60 | it "should generate nice identifiers" do 61 | text = Trenni::Strings.to_snake("Happy::Go::Lucky") 62 | expect(text).to be == "happy_go_lucky" 63 | end 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /spec/trenni/tag_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Copyright, 2012, by Samuel G. D. Williams. 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | # THE SOFTWARE. 22 | 23 | require 'trenni/tag' 24 | 25 | RSpec.describe Trenni::Tag.new("body", false, class: 'main') do 26 | it "should have name" do 27 | expect(subject.name).to be == "body" 28 | end 29 | 30 | it "should be open by default" do 31 | expect(subject).to_not be_self_closed 32 | end 33 | 34 | it "should have an attribute" do 35 | expect(subject.attributes).to include(:class) 36 | expect(subject[:class]).to be == 'main' 37 | expect(subject.to_s).to include('class="main"') 38 | end 39 | end 40 | 41 | RSpec.describe Trenni::Tag.new("button", true, 'onclick' => 'javascript:alert("Hello World")') do 42 | it "should have name" do 43 | expect(subject.name).to be == "button" 44 | end 45 | 46 | it "should have an attribute" do 47 | expect(subject.attributes).to include('onclick') 48 | end 49 | 50 | it "should generate valid string" do 51 | expect(subject.to_s).to be == '