├── .document ├── Gemfile ├── .gitignore ├── .github ├── dependabot.yml └── workflows │ ├── sync-ruby.yml │ └── test.yml ├── bin ├── setup └── console ├── Rakefile ├── prettyprint.gemspec ├── LICENSE.txt ├── README.md ├── test └── test_prettyprint.rb └── lib └── prettyprint.rb /.document: -------------------------------------------------------------------------------- 1 | LICENSE.txt 2 | README.md 3 | lib/ 4 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem "rake" 4 | gem "test-unit" 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /_yardoc/ 4 | /coverage/ 5 | /doc/ 6 | /pkg/ 7 | /spec/reports/ 8 | /tmp/ 9 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: 'github-actions' 4 | directory: '/' 5 | schedule: 6 | interval: 'weekly' 7 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rake/testtask" 3 | 4 | Rake::TestTask.new(:test) do |t| 5 | t.libs << "test" 6 | t.libs << "lib" 7 | t.test_files = FileList["test/**/test_*.rb"] 8 | end 9 | 10 | task :default => :test 11 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "bundler/setup" 4 | require "prettyprint" 5 | 6 | # You can add fixtures and/or initialization code here to make experimenting 7 | # with your gem easier. You can also use a different console, if you like. 8 | 9 | # (If you use this, don't forget to add pry to your Gemfile!) 10 | # require "pry" 11 | # Pry.start 12 | 13 | require "irb" 14 | IRB.start(__FILE__) 15 | -------------------------------------------------------------------------------- /.github/workflows/sync-ruby.yml: -------------------------------------------------------------------------------- 1 | name: Sync ruby 2 | on: 3 | push: 4 | branches: [master] 5 | jobs: 6 | sync: 7 | name: Sync ruby 8 | runs-on: ubuntu-latest 9 | if: ${{ github.repository_owner == 'ruby' }} 10 | steps: 11 | - uses: actions/checkout@v6 12 | 13 | - name: Create GitHub App token 14 | id: app-token 15 | uses: actions/create-github-app-token@v2 16 | with: 17 | app-id: 2060836 18 | private-key: ${{ secrets.RUBY_SYNC_DEFAULT_GEMS_PRIVATE_KEY }} 19 | owner: ruby 20 | repositories: ruby 21 | 22 | - name: Sync to ruby/ruby 23 | uses: convictional/trigger-workflow-and-wait@v1.6.5 24 | with: 25 | owner: ruby 26 | repo: ruby 27 | workflow_file_name: sync_default_gems.yml 28 | github_token: ${{ steps.app-token.outputs.token }} 29 | ref: master 30 | client_payload: | 31 | {"gem":"${{ github.event.repository.name }}","before":"${{ github.event.before }}","after":"${{ github.event.after }}"} 32 | propagate_failure: true 33 | wait_interval: 10 34 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: 4 | push: 5 | pull_request: 6 | schedule: 7 | - cron: '15 3 * * *' 8 | 9 | jobs: 10 | ruby-versions: 11 | uses: ruby/actions/.github/workflows/ruby_versions.yml@master 12 | with: 13 | engine: cruby 14 | min_version: 2.5 15 | 16 | test: 17 | needs: ruby-versions 18 | strategy: 19 | matrix: 20 | ruby: ${{ fromJson(needs.ruby-versions.outputs.versions) }} 21 | os: [ ubuntu-latest, macos-latest, windows-latest ] 22 | exclude: 23 | - { os: macos-latest , ruby: 2.5 } 24 | - { os: windows-latest , ruby: head } 25 | include: 26 | - { os: windows-latest , ruby: mingw } 27 | - { os: windows-latest , ruby: mswin } 28 | runs-on: ${{ matrix.os }} 29 | steps: 30 | - uses: actions/checkout@v6 31 | - name: Set up Ruby 32 | uses: ruby/setup-ruby@v1 33 | with: 34 | ruby-version: ${{ matrix.ruby }} 35 | - name: Install dependencies 36 | run: bundle install 37 | - name: Build 38 | run: rake build 39 | - name: Run test 40 | run: rake test 41 | - name: Installation test 42 | run: gem install pkg/*.gem 43 | -------------------------------------------------------------------------------- /prettyprint.gemspec: -------------------------------------------------------------------------------- 1 | name = File.basename(__FILE__, ".gemspec") 2 | version = ["lib", Array.new(name.count("-")+1).join("/")].find do |dir| 3 | break File.foreach(File.join(__dir__, dir, "#{name.tr('-', '/')}.rb")) do |line| 4 | /^\s*VERSION\s*=\s*"(.*)"/ =~ line and break $1 5 | end rescue nil 6 | end 7 | 8 | Gem::Specification.new do |spec| 9 | spec.name = name 10 | spec.version = version 11 | spec.authors = ["Tanaka Akira"] 12 | spec.email = ["akr@fsij.org"] 13 | 14 | spec.summary = %q{Implements a pretty printing algorithm for readable structure.} 15 | spec.description = %q{Implements a pretty printing algorithm for readable structure.} 16 | spec.homepage = "https://github.com/ruby/prettyprint" 17 | spec.required_ruby_version = Gem::Requirement.new(">= 2.3.0") 18 | spec.licenses = ["Ruby", "BSD-2-Clause"] 19 | 20 | spec.metadata["homepage_uri"] = spec.homepage 21 | spec.metadata["source_code_uri"] = spec.homepage 22 | 23 | spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do 24 | `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } 25 | end 26 | spec.bindir = "exe" 27 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } 28 | spec.require_paths = ["lib"] 29 | end 30 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (C) 1993-2013 Yukihiro Matsumoto. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions 5 | are met: 6 | 1. Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | 2. Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | 12 | THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND 13 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 14 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 15 | ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE 16 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 17 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 18 | OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 19 | HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 20 | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 21 | OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 22 | SUCH DAMAGE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PrettyPrint 2 | 3 | This class implements a pretty printing algorithm. It finds line breaks and 4 | nice indentations for grouped structure. 5 | 6 | By default, the class assumes that primitive elements are strings and each 7 | byte in the strings have single column in width. But it can be used for 8 | other situations by giving suitable arguments for some methods: 9 | 10 | * newline object and space generation block for PrettyPrint.new 11 | * optional width argument for PrettyPrint#text 12 | * PrettyPrint#breakable 13 | 14 | There are several candidate uses: 15 | 16 | * text formatting using proportional fonts 17 | * multibyte characters which has columns different to number of bytes 18 | 19 | ## Installation 20 | 21 | Add this line to your application's Gemfile: 22 | 23 | ```ruby 24 | gem 'prettyprint' 25 | ``` 26 | 27 | And then execute: 28 | 29 | $ bundle install 30 | 31 | Or install it yourself as: 32 | 33 | $ gem install prettyprint 34 | 35 | ## Development 36 | 37 | After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. 38 | 39 | To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org). 40 | 41 | ## Contributing 42 | 43 | Bug reports and pull requests are welcome on GitHub at https://github.com/ruby/prettyprint. 44 | -------------------------------------------------------------------------------- /test/test_prettyprint.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'prettyprint' 4 | require 'test/unit' 5 | 6 | module PrettyPrintTest 7 | 8 | class WadlerExample < Test::Unit::TestCase # :nodoc: 9 | def setup 10 | @tree = Tree.new("aaaa", Tree.new("bbbbb", Tree.new("ccc"), 11 | Tree.new("dd")), 12 | Tree.new("eee"), 13 | Tree.new("ffff", Tree.new("gg"), 14 | Tree.new("hhh"), 15 | Tree.new("ii"))) 16 | end 17 | 18 | def hello(width) 19 | PrettyPrint.format(''.dup, width) {|hello| 20 | hello.group { 21 | hello.group { 22 | hello.group { 23 | hello.group { 24 | hello.text 'hello' 25 | hello.breakable; hello.text 'a' 26 | } 27 | hello.breakable; hello.text 'b' 28 | } 29 | hello.breakable; hello.text 'c' 30 | } 31 | hello.breakable; hello.text 'd' 32 | } 33 | } 34 | end 35 | 36 | def test_hello_00_06 37 | expected = <<'End'.chomp 38 | hello 39 | a 40 | b 41 | c 42 | d 43 | End 44 | assert_equal(expected, hello(0)) 45 | assert_equal(expected, hello(6)) 46 | end 47 | 48 | def test_hello_07_08 49 | expected = <<'End'.chomp 50 | hello a 51 | b 52 | c 53 | d 54 | End 55 | assert_equal(expected, hello(7)) 56 | assert_equal(expected, hello(8)) 57 | end 58 | 59 | def test_hello_09_10 60 | expected = <<'End'.chomp 61 | hello a b 62 | c 63 | d 64 | End 65 | out = hello(9); assert_equal(expected, out) 66 | out = hello(10); assert_equal(expected, out) 67 | end 68 | 69 | def test_hello_11_12 70 | expected = <<'End'.chomp 71 | hello a b c 72 | d 73 | End 74 | assert_equal(expected, hello(11)) 75 | assert_equal(expected, hello(12)) 76 | end 77 | 78 | def test_hello_13 79 | expected = <<'End'.chomp 80 | hello a b c d 81 | End 82 | assert_equal(expected, hello(13)) 83 | end 84 | 85 | def tree(width) 86 | PrettyPrint.format(''.dup, width) {|q| @tree.show(q)} 87 | end 88 | 89 | def test_tree_00_19 90 | expected = <<'End'.chomp 91 | aaaa[bbbbb[ccc, 92 | dd], 93 | eee, 94 | ffff[gg, 95 | hhh, 96 | ii]] 97 | End 98 | assert_equal(expected, tree(0)) 99 | assert_equal(expected, tree(19)) 100 | end 101 | 102 | def test_tree_20_22 103 | expected = <<'End'.chomp 104 | aaaa[bbbbb[ccc, dd], 105 | eee, 106 | ffff[gg, 107 | hhh, 108 | ii]] 109 | End 110 | assert_equal(expected, tree(20)) 111 | assert_equal(expected, tree(22)) 112 | end 113 | 114 | def test_tree_23_43 115 | expected = <<'End'.chomp 116 | aaaa[bbbbb[ccc, dd], 117 | eee, 118 | ffff[gg, hhh, ii]] 119 | End 120 | assert_equal(expected, tree(23)) 121 | assert_equal(expected, tree(43)) 122 | end 123 | 124 | def test_tree_44 125 | assert_equal(<<'End'.chomp, tree(44)) 126 | aaaa[bbbbb[ccc, dd], eee, ffff[gg, hhh, ii]] 127 | End 128 | end 129 | 130 | def tree_alt(width) 131 | PrettyPrint.format(''.dup, width) {|q| @tree.altshow(q)} 132 | end 133 | 134 | def test_tree_alt_00_18 135 | expected = <<'End'.chomp 136 | aaaa[ 137 | bbbbb[ 138 | ccc, 139 | dd 140 | ], 141 | eee, 142 | ffff[ 143 | gg, 144 | hhh, 145 | ii 146 | ] 147 | ] 148 | End 149 | assert_equal(expected, tree_alt(0)) 150 | assert_equal(expected, tree_alt(18)) 151 | end 152 | 153 | def test_tree_alt_19_20 154 | expected = <<'End'.chomp 155 | aaaa[ 156 | bbbbb[ ccc, dd ], 157 | eee, 158 | ffff[ 159 | gg, 160 | hhh, 161 | ii 162 | ] 163 | ] 164 | End 165 | assert_equal(expected, tree_alt(19)) 166 | assert_equal(expected, tree_alt(20)) 167 | end 168 | 169 | def test_tree_alt_20_49 170 | expected = <<'End'.chomp 171 | aaaa[ 172 | bbbbb[ ccc, dd ], 173 | eee, 174 | ffff[ gg, hhh, ii ] 175 | ] 176 | End 177 | assert_equal(expected, tree_alt(21)) 178 | assert_equal(expected, tree_alt(49)) 179 | end 180 | 181 | def test_tree_alt_50 182 | expected = <<'End'.chomp 183 | aaaa[ bbbbb[ ccc, dd ], eee, ffff[ gg, hhh, ii ] ] 184 | End 185 | assert_equal(expected, tree_alt(50)) 186 | end 187 | 188 | class Tree # :nodoc: 189 | def initialize(string, *children) 190 | @string = string 191 | @children = children 192 | end 193 | 194 | def show(q) 195 | q.group { 196 | q.text @string 197 | q.nest(@string.length) { 198 | unless @children.empty? 199 | q.text '[' 200 | q.nest(1) { 201 | first = true 202 | @children.each {|t| 203 | if first 204 | first = false 205 | else 206 | q.text ',' 207 | q.breakable 208 | end 209 | t.show(q) 210 | } 211 | } 212 | q.text ']' 213 | end 214 | } 215 | } 216 | end 217 | 218 | def altshow(q) 219 | q.group { 220 | q.text @string 221 | unless @children.empty? 222 | q.text '[' 223 | q.nest(2) { 224 | q.breakable 225 | first = true 226 | @children.each {|t| 227 | if first 228 | first = false 229 | else 230 | q.text ',' 231 | q.breakable 232 | end 233 | t.altshow(q) 234 | } 235 | } 236 | q.breakable 237 | q.text ']' 238 | end 239 | } 240 | end 241 | 242 | end 243 | end 244 | 245 | class StrictPrettyExample < Test::Unit::TestCase # :nodoc: 246 | def prog(width) 247 | PrettyPrint.format(''.dup, width) {|q| 248 | q.group { 249 | q.group {q.nest(2) { 250 | q.text "if"; q.breakable; 251 | q.group { 252 | q.nest(2) { 253 | q.group {q.text "a"; q.breakable; q.text "=="} 254 | q.breakable; q.text "b"}}}} 255 | q.breakable 256 | q.group {q.nest(2) { 257 | q.text "then"; q.breakable; 258 | q.group { 259 | q.nest(2) { 260 | q.group {q.text "a"; q.breakable; q.text "<<"} 261 | q.breakable; q.text "2"}}}} 262 | q.breakable 263 | q.group {q.nest(2) { 264 | q.text "else"; q.breakable; 265 | q.group { 266 | q.nest(2) { 267 | q.group {q.text "a"; q.breakable; q.text "+"} 268 | q.breakable; q.text "b"}}}}} 269 | } 270 | end 271 | 272 | def test_00_04 273 | expected = <<'End'.chomp 274 | if 275 | a 276 | == 277 | b 278 | then 279 | a 280 | << 281 | 2 282 | else 283 | a 284 | + 285 | b 286 | End 287 | assert_equal(expected, prog(0)) 288 | assert_equal(expected, prog(4)) 289 | end 290 | 291 | def test_05 292 | expected = <<'End'.chomp 293 | if 294 | a 295 | == 296 | b 297 | then 298 | a 299 | << 300 | 2 301 | else 302 | a + 303 | b 304 | End 305 | assert_equal(expected, prog(5)) 306 | end 307 | 308 | def test_06 309 | expected = <<'End'.chomp 310 | if 311 | a == 312 | b 313 | then 314 | a << 315 | 2 316 | else 317 | a + 318 | b 319 | End 320 | assert_equal(expected, prog(6)) 321 | end 322 | 323 | def test_07 324 | expected = <<'End'.chomp 325 | if 326 | a == 327 | b 328 | then 329 | a << 330 | 2 331 | else 332 | a + b 333 | End 334 | assert_equal(expected, prog(7)) 335 | end 336 | 337 | def test_08 338 | expected = <<'End'.chomp 339 | if 340 | a == b 341 | then 342 | a << 2 343 | else 344 | a + b 345 | End 346 | assert_equal(expected, prog(8)) 347 | end 348 | 349 | def test_09 350 | expected = <<'End'.chomp 351 | if a == b 352 | then 353 | a << 2 354 | else 355 | a + b 356 | End 357 | assert_equal(expected, prog(9)) 358 | end 359 | 360 | def test_10 361 | expected = <<'End'.chomp 362 | if a == b 363 | then 364 | a << 2 365 | else a + b 366 | End 367 | assert_equal(expected, prog(10)) 368 | end 369 | 370 | def test_11_31 371 | expected = <<'End'.chomp 372 | if a == b 373 | then a << 2 374 | else a + b 375 | End 376 | assert_equal(expected, prog(11)) 377 | assert_equal(expected, prog(15)) 378 | assert_equal(expected, prog(31)) 379 | end 380 | 381 | def test_32 382 | expected = <<'End'.chomp 383 | if a == b then a << 2 else a + b 384 | End 385 | assert_equal(expected, prog(32)) 386 | end 387 | 388 | end 389 | 390 | class TailGroup < Test::Unit::TestCase # :nodoc: 391 | def test_1 392 | out = PrettyPrint.format(''.dup, 10) {|q| 393 | q.group { 394 | q.group { 395 | q.text "abc" 396 | q.breakable 397 | q.text "def" 398 | } 399 | q.group { 400 | q.text "ghi" 401 | q.breakable 402 | q.text "jkl" 403 | } 404 | } 405 | } 406 | assert_equal("abc defghi\njkl", out) 407 | end 408 | end 409 | 410 | class NonString < Test::Unit::TestCase # :nodoc: 411 | def format(width) 412 | PrettyPrint.format([], width, 'newline', lambda {|n| "#{n} spaces"}) {|q| 413 | q.text(3, 3) 414 | q.breakable(1, 1) 415 | q.text(3, 3) 416 | } 417 | end 418 | 419 | def test_6 420 | assert_equal([3, "newline", "0 spaces", 3], format(6)) 421 | end 422 | 423 | def test_7 424 | assert_equal([3, 1, 3], format(7)) 425 | end 426 | 427 | end 428 | 429 | class Fill < Test::Unit::TestCase # :nodoc: 430 | def format(width) 431 | PrettyPrint.format(''.dup, width) {|q| 432 | q.group { 433 | q.text 'abc' 434 | q.fill_breakable 435 | q.text 'def' 436 | q.fill_breakable 437 | q.text 'ghi' 438 | q.fill_breakable 439 | q.text 'jkl' 440 | q.fill_breakable 441 | q.text 'mno' 442 | q.fill_breakable 443 | q.text 'pqr' 444 | q.fill_breakable 445 | q.text 'stu' 446 | } 447 | } 448 | end 449 | 450 | def test_00_06 451 | expected = <<'End'.chomp 452 | abc 453 | def 454 | ghi 455 | jkl 456 | mno 457 | pqr 458 | stu 459 | End 460 | assert_equal(expected, format(0)) 461 | assert_equal(expected, format(6)) 462 | end 463 | 464 | def test_07_10 465 | expected = <<'End'.chomp 466 | abc def 467 | ghi jkl 468 | mno pqr 469 | stu 470 | End 471 | assert_equal(expected, format(7)) 472 | assert_equal(expected, format(10)) 473 | end 474 | 475 | def test_11_14 476 | expected = <<'End'.chomp 477 | abc def ghi 478 | jkl mno pqr 479 | stu 480 | End 481 | assert_equal(expected, format(11)) 482 | assert_equal(expected, format(14)) 483 | end 484 | 485 | def test_15_18 486 | expected = <<'End'.chomp 487 | abc def ghi jkl 488 | mno pqr stu 489 | End 490 | assert_equal(expected, format(15)) 491 | assert_equal(expected, format(18)) 492 | end 493 | 494 | def test_19_22 495 | expected = <<'End'.chomp 496 | abc def ghi jkl mno 497 | pqr stu 498 | End 499 | assert_equal(expected, format(19)) 500 | assert_equal(expected, format(22)) 501 | end 502 | 503 | def test_23_26 504 | expected = <<'End'.chomp 505 | abc def ghi jkl mno pqr 506 | stu 507 | End 508 | assert_equal(expected, format(23)) 509 | assert_equal(expected, format(26)) 510 | end 511 | 512 | def test_27 513 | expected = <<'End'.chomp 514 | abc def ghi jkl mno pqr stu 515 | End 516 | assert_equal(expected, format(27)) 517 | end 518 | 519 | end 520 | 521 | end 522 | -------------------------------------------------------------------------------- /lib/prettyprint.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # 3 | # This class implements a pretty printing algorithm. It finds line breaks and 4 | # nice indentations for grouped structure. 5 | # 6 | # By default, the class assumes that primitive elements are strings and each 7 | # byte in the strings have single column in width. But it can be used for 8 | # other situations by giving suitable arguments for some methods: 9 | # * newline object and space generation block for PrettyPrint.new 10 | # * optional width argument for PrettyPrint#text 11 | # * PrettyPrint#breakable 12 | # 13 | # There are several candidate uses: 14 | # * text formatting using proportional fonts 15 | # * multibyte characters which has columns different to number of bytes 16 | # * non-string formatting 17 | # 18 | # == Bugs 19 | # * Box based formatting? 20 | # * Other (better) model/algorithm? 21 | # 22 | # Report any bugs at http://bugs.ruby-lang.org 23 | # 24 | # == References 25 | # Christian Lindig, Strictly Pretty, March 2000, 26 | # https://lindig.github.io/papers/strictly-pretty-2000.pdf 27 | # 28 | # Philip Wadler, A prettier printer, March 1998, 29 | # https://homepages.inf.ed.ac.uk/wadler/topics/language-design.html#prettier 30 | # 31 | # == Author 32 | # Tanaka Akira 33 | # 34 | class PrettyPrint 35 | 36 | # The version string 37 | VERSION = "0.2.0" 38 | 39 | # This is a convenience method which is same as follows: 40 | # 41 | # begin 42 | # q = PrettyPrint.new(output, maxwidth, newline, &genspace) 43 | # ... 44 | # q.flush 45 | # output 46 | # end 47 | # 48 | def PrettyPrint.format(output=''.dup, maxwidth=79, newline="\n", genspace=lambda {|n| ' ' * n}) 49 | q = PrettyPrint.new(output, maxwidth, newline, &genspace) 50 | yield q 51 | q.flush 52 | output 53 | end 54 | 55 | # This is similar to PrettyPrint::format but the result has no breaks. 56 | # 57 | # +maxwidth+, +newline+ and +genspace+ are ignored. 58 | # 59 | # The invocation of +breakable+ in the block doesn't break a line and is 60 | # treated as just an invocation of +text+. 61 | # 62 | def PrettyPrint.singleline_format(output=''.dup, maxwidth=nil, newline=nil, genspace=nil) 63 | q = SingleLine.new(output) 64 | yield q 65 | output 66 | end 67 | 68 | # Creates a buffer for pretty printing. 69 | # 70 | # +output+ is an output target. If it is not specified, '' is assumed. It 71 | # should have a << method which accepts the first argument +obj+ of 72 | # PrettyPrint#text, the first argument +sep+ of PrettyPrint#breakable, the 73 | # first argument +newline+ of PrettyPrint.new, and the result of a given 74 | # block for PrettyPrint.new. 75 | # 76 | # +maxwidth+ specifies maximum line length. If it is not specified, 79 is 77 | # assumed. However actual outputs may overflow +maxwidth+ if long 78 | # non-breakable texts are provided. 79 | # 80 | # +newline+ is used for line breaks. "\n" is used if it is not specified. 81 | # 82 | # The block is used to generate spaces. {|width| ' ' * width} is used if it 83 | # is not given. 84 | # 85 | def initialize(output=''.dup, maxwidth=79, newline="\n", &genspace) 86 | @output = output 87 | @maxwidth = maxwidth 88 | @newline = newline 89 | @genspace = genspace || lambda {|n| ' ' * n} 90 | 91 | @output_width = 0 92 | @buffer_width = 0 93 | @buffer = [] 94 | 95 | root_group = Group.new(0) 96 | @group_stack = [root_group] 97 | @group_queue = GroupQueue.new(root_group) 98 | @indent = 0 99 | end 100 | 101 | # The output object. 102 | # 103 | # This defaults to '', and should accept the << method 104 | attr_reader :output 105 | 106 | # The maximum width of a line, before it is separated in to a newline 107 | # 108 | # This defaults to 79, and should be an Integer 109 | attr_reader :maxwidth 110 | 111 | # The value that is appended to +output+ to add a new line. 112 | # 113 | # This defaults to "\n", and should be String 114 | attr_reader :newline 115 | 116 | # A lambda or Proc, that takes one argument, of an Integer, and returns 117 | # the corresponding number of spaces. 118 | # 119 | # By default this is: 120 | # lambda {|n| ' ' * n} 121 | attr_reader :genspace 122 | 123 | # The number of spaces to be indented 124 | attr_reader :indent 125 | 126 | # The PrettyPrint::GroupQueue of groups in stack to be pretty printed 127 | attr_reader :group_queue 128 | 129 | # Returns the group most recently added to the stack. 130 | # 131 | # Contrived example: 132 | # out = "" 133 | # => "" 134 | # q = PrettyPrint.new(out) 135 | # => #, @output_width=0, @buffer_width=0, @buffer=[], @group_stack=[#], @group_queue=#]]>, @indent=0> 136 | # q.group { 137 | # q.text q.current_group.inspect 138 | # q.text q.newline 139 | # q.group(q.current_group.depth + 1) { 140 | # q.text q.current_group.inspect 141 | # q.text q.newline 142 | # q.group(q.current_group.depth + 1) { 143 | # q.text q.current_group.inspect 144 | # q.text q.newline 145 | # q.group(q.current_group.depth + 1) { 146 | # q.text q.current_group.inspect 147 | # q.text q.newline 148 | # } 149 | # } 150 | # } 151 | # } 152 | # => 284 153 | # puts out 154 | # # 155 | # # 156 | # # 157 | # # 158 | def current_group 159 | @group_stack.last 160 | end 161 | 162 | # Breaks the buffer into lines that are shorter than #maxwidth 163 | def break_outmost_groups 164 | while @maxwidth < @output_width + @buffer_width 165 | return unless group = @group_queue.deq 166 | until group.breakables.empty? 167 | data = @buffer.shift 168 | @output_width = data.output(@output, @output_width) 169 | @buffer_width -= data.width 170 | end 171 | while !@buffer.empty? && Text === @buffer.first 172 | text = @buffer.shift 173 | @output_width = text.output(@output, @output_width) 174 | @buffer_width -= text.width 175 | end 176 | end 177 | end 178 | 179 | # This adds +obj+ as a text of +width+ columns in width. 180 | # 181 | # If +width+ is not specified, obj.length is used. 182 | # 183 | def text(obj, width=obj.length) 184 | if @buffer.empty? 185 | @output << obj 186 | @output_width += width 187 | else 188 | text = @buffer.last 189 | unless Text === text 190 | text = Text.new 191 | @buffer << text 192 | end 193 | text.add(obj, width) 194 | @buffer_width += width 195 | break_outmost_groups 196 | end 197 | end 198 | 199 | # This is similar to #breakable except 200 | # the decision to break or not is determined individually. 201 | # 202 | # Two #fill_breakable under a group may cause 4 results: 203 | # (break,break), (break,non-break), (non-break,break), (non-break,non-break). 204 | # This is different to #breakable because two #breakable under a group 205 | # may cause 2 results: 206 | # (break,break), (non-break,non-break). 207 | # 208 | # The text +sep+ is inserted if a line is not broken at this point. 209 | # 210 | # If +sep+ is not specified, " " is used. 211 | # 212 | # If +width+ is not specified, +sep.length+ is used. You will have to 213 | # specify this when +sep+ is a multibyte character, for example. 214 | # 215 | def fill_breakable(sep=' ', width=sep.length) 216 | group { breakable sep, width } 217 | end 218 | 219 | # This says "you can break a line here if necessary", and a +width+\-column 220 | # text +sep+ is inserted if a line is not broken at the point. 221 | # 222 | # If +sep+ is not specified, " " is used. 223 | # 224 | # If +width+ is not specified, +sep.length+ is used. You will have to 225 | # specify this when +sep+ is a multibyte character, for example. 226 | # 227 | def breakable(sep=' ', width=sep.length) 228 | group = @group_stack.last 229 | if group.break? 230 | flush 231 | @output << @newline 232 | @output << @genspace.call(@indent) 233 | @output_width = @indent 234 | @buffer_width = 0 235 | else 236 | @buffer << Breakable.new(sep, width, self) 237 | @buffer_width += width 238 | break_outmost_groups 239 | end 240 | end 241 | 242 | # Groups line break hints added in the block. The line break hints are all 243 | # to be used or not. 244 | # 245 | # If +indent+ is specified, the method call is regarded as nested by 246 | # nest(indent) { ... }. 247 | # 248 | # If +open_obj+ is specified, text open_obj, open_width is called 249 | # before grouping. If +close_obj+ is specified, text close_obj, 250 | # close_width is called after grouping. 251 | # 252 | def group(indent=0, open_obj='', close_obj='', open_width=open_obj.length, close_width=close_obj.length) 253 | text open_obj, open_width 254 | group_sub { 255 | nest(indent) { 256 | yield 257 | } 258 | } 259 | text close_obj, close_width 260 | end 261 | 262 | # Takes a block and queues a new group that is indented 1 level further. 263 | def group_sub 264 | group = Group.new(@group_stack.last.depth + 1) 265 | @group_stack.push group 266 | @group_queue.enq group 267 | begin 268 | yield 269 | ensure 270 | @group_stack.pop 271 | if group.breakables.empty? 272 | @group_queue.delete group 273 | end 274 | end 275 | end 276 | 277 | # Increases left margin after newline with +indent+ for line breaks added in 278 | # the block. 279 | # 280 | def nest(indent) 281 | @indent += indent 282 | begin 283 | yield 284 | ensure 285 | @indent -= indent 286 | end 287 | end 288 | 289 | # outputs buffered data. 290 | # 291 | def flush 292 | @buffer.each {|data| 293 | @output_width = data.output(@output, @output_width) 294 | } 295 | @buffer.clear 296 | @buffer_width = 0 297 | end 298 | 299 | # The Text class is the means by which to collect strings from objects. 300 | # 301 | # This class is intended for internal use of the PrettyPrint buffers. 302 | class Text # :nodoc: 303 | 304 | # Creates a new text object. 305 | # 306 | # This constructor takes no arguments. 307 | # 308 | # The workflow is to append a PrettyPrint::Text object to the buffer, and 309 | # being able to call the buffer.last() to reference it. 310 | # 311 | # As there are objects, use PrettyPrint::Text#add to include the objects 312 | # and the width to utilized by the String version of this object. 313 | def initialize 314 | @objs = [] 315 | @width = 0 316 | end 317 | 318 | # The total width of the objects included in this Text object. 319 | attr_reader :width 320 | 321 | # Render the String text of the objects that have been added to this Text object. 322 | # 323 | # Output the text to +out+, and increment the width to +output_width+ 324 | def output(out, output_width) 325 | @objs.each {|obj| out << obj} 326 | output_width + @width 327 | end 328 | 329 | # Include +obj+ in the objects to be pretty printed, and increment 330 | # this Text object's total width by +width+ 331 | def add(obj, width) 332 | @objs << obj 333 | @width += width 334 | end 335 | end 336 | 337 | # The Breakable class is used for breaking up object information 338 | # 339 | # This class is intended for internal use of the PrettyPrint buffers. 340 | class Breakable # :nodoc: 341 | 342 | # Create a new Breakable object. 343 | # 344 | # Arguments: 345 | # * +sep+ String of the separator 346 | # * +width+ Integer width of the +sep+ 347 | # * +q+ parent PrettyPrint object, to base from 348 | def initialize(sep, width, q) 349 | @obj = sep 350 | @width = width 351 | @pp = q 352 | @indent = q.indent 353 | @group = q.current_group 354 | @group.breakables.push self 355 | end 356 | 357 | # Holds the separator String 358 | # 359 | # The +sep+ argument from ::new 360 | attr_reader :obj 361 | 362 | # The width of +obj+ / +sep+ 363 | attr_reader :width 364 | 365 | # The number of spaces to indent. 366 | # 367 | # This is inferred from +q+ within PrettyPrint, passed in ::new 368 | attr_reader :indent 369 | 370 | # Render the String text of the objects that have been added to this 371 | # Breakable object. 372 | # 373 | # Output the text to +out+, and increment the width to +output_width+ 374 | def output(out, output_width) 375 | @group.breakables.shift 376 | if @group.break? 377 | out << @pp.newline 378 | out << @pp.genspace.call(@indent) 379 | @indent 380 | else 381 | @pp.group_queue.delete @group if @group.breakables.empty? 382 | out << @obj 383 | output_width + @width 384 | end 385 | end 386 | end 387 | 388 | # The Group class is used for making indentation easier. 389 | # 390 | # While this class does neither the breaking into newlines nor indentation, 391 | # it is used in a stack (as well as a queue) within PrettyPrint, to group 392 | # objects. 393 | # 394 | # For information on using groups, see PrettyPrint#group 395 | # 396 | # This class is intended for internal use of the PrettyPrint buffers. 397 | class Group # :nodoc: 398 | # Create a Group object 399 | # 400 | # Arguments: 401 | # * +depth+ - this group's relation to previous groups 402 | def initialize(depth) 403 | @depth = depth 404 | @breakables = [] 405 | @break = false 406 | end 407 | 408 | # This group's relation to previous groups 409 | attr_reader :depth 410 | 411 | # Array to hold the Breakable objects for this Group 412 | attr_reader :breakables 413 | 414 | # Makes a break for this Group, and returns true 415 | def break 416 | @break = true 417 | end 418 | 419 | # Boolean of whether this Group has made a break 420 | def break? 421 | @break 422 | end 423 | 424 | # Boolean of whether this Group has been queried for being first 425 | # 426 | # This is used as a predicate, and ought to be called first. 427 | def first? 428 | if defined? @first 429 | false 430 | else 431 | @first = false 432 | true 433 | end 434 | end 435 | end 436 | 437 | # The GroupQueue class is used for managing the queue of Group to be pretty 438 | # printed. 439 | # 440 | # This queue groups the Group objects, based on their depth. 441 | # 442 | # This class is intended for internal use of the PrettyPrint buffers. 443 | class GroupQueue # :nodoc: 444 | # Create a GroupQueue object 445 | # 446 | # Arguments: 447 | # * +groups+ - one or more PrettyPrint::Group objects 448 | def initialize(*groups) 449 | @queue = [] 450 | groups.each {|g| enq g} 451 | end 452 | 453 | # Enqueue +group+ 454 | # 455 | # This does not strictly append the group to the end of the queue, 456 | # but instead adds it in line, base on the +group.depth+ 457 | def enq(group) 458 | depth = group.depth 459 | @queue << [] until depth < @queue.length 460 | @queue[depth] << group 461 | end 462 | 463 | # Returns the outer group of the queue 464 | def deq 465 | @queue.each {|gs| 466 | (gs.length-1).downto(0) {|i| 467 | unless gs[i].breakables.empty? 468 | group = gs.slice!(i, 1).first 469 | group.break 470 | return group 471 | end 472 | } 473 | gs.each {|group| group.break} 474 | gs.clear 475 | } 476 | return nil 477 | end 478 | 479 | # Remote +group+ from this queue 480 | def delete(group) 481 | @queue[group.depth].delete(group) 482 | end 483 | end 484 | 485 | # PrettyPrint::SingleLine is used by PrettyPrint.singleline_format 486 | # 487 | # It is passed to be similar to a PrettyPrint object itself, by responding to: 488 | # * #text 489 | # * #breakable 490 | # * #nest 491 | # * #group 492 | # * #flush 493 | # * #first? 494 | # 495 | # but instead, the output has no line breaks 496 | # 497 | class SingleLine 498 | # Create a PrettyPrint::SingleLine object 499 | # 500 | # Arguments: 501 | # * +output+ - String (or similar) to store rendered text. Needs to respond to '<<' 502 | # * +maxwidth+ - Argument position expected to be here for compatibility. 503 | # This argument is a noop. 504 | # * +newline+ - Argument position expected to be here for compatibility. 505 | # This argument is a noop. 506 | def initialize(output, maxwidth=nil, newline=nil) 507 | @output = output 508 | @first = [true] 509 | end 510 | 511 | # Add +obj+ to the text to be output. 512 | # 513 | # +width+ argument is here for compatibility. It is a noop argument. 514 | def text(obj, width=nil) 515 | @output << obj 516 | end 517 | 518 | # Appends +sep+ to the text to be output. By default +sep+ is ' ' 519 | # 520 | # +width+ argument is here for compatibility. It is a noop argument. 521 | def breakable(sep=' ', width=nil) 522 | @output << sep 523 | end 524 | 525 | # Takes +indent+ arg, but does nothing with it. 526 | # 527 | # Yields to a block. 528 | def nest(indent) # :nodoc: 529 | yield 530 | end 531 | 532 | # Opens a block for grouping objects to be pretty printed. 533 | # 534 | # Arguments: 535 | # * +indent+ - noop argument. Present for compatibility. 536 | # * +open_obj+ - text appended before the &blok. Default is '' 537 | # * +close_obj+ - text appended after the &blok. Default is '' 538 | # * +open_width+ - noop argument. Present for compatibility. 539 | # * +close_width+ - noop argument. Present for compatibility. 540 | def group(indent=nil, open_obj='', close_obj='', open_width=nil, close_width=nil) 541 | @first.push true 542 | @output << open_obj 543 | yield 544 | @output << close_obj 545 | @first.pop 546 | end 547 | 548 | # Method present for compatibility, but is a noop 549 | def flush # :nodoc: 550 | end 551 | 552 | # This is used as a predicate, and ought to be called first. 553 | def first? 554 | result = @first[-1] 555 | @first[-1] = false 556 | result 557 | end 558 | end 559 | end 560 | --------------------------------------------------------------------------------