# frozen_string_literal: true require_relative 'oppen/mixins' require_relative 'oppen/printer' require_relative 'oppen/print_stack' require_relative 'oppen/scan_stack' require_relative 'oppen/token' require_relative 'oppen/version' require_relative 'wadler/print' # Oppen. module Oppen extend Mixins # Entry point of the pretty printer. # # @param config [Config] # to customize the printer's behavior. # @param new_line [String] # the delimiter between lines. # @param out [Object] # the output string buffer. It should have a `write` and `string` methods. # @param space [String, Proc] # indentation string or a string generator. # - If a `String`, spaces will be generated with the the lambda # `->(n){ space * n }`, where `n` is the number of columns to indent. # - If a `Proc`, it will receive `n` and it needs to return a `String`. # @param tokens [Array<Token>] the list of tokens to be printed. # @param width [Integer] maximum line width desired. # # @return [String] output of the pretty printer. def self.print(config: Config.oppen, new_line: "\n", out: StringIO.new, space: ' ', tokens: [], width: 80) printer = Printer.new width, new_line, config, space, out tokens.each do |token| printer.print token end printer.output end # Config. class Config attr_accessor :indent_anchor # @param eager_print [Boolean] # whether to eagerly print. # @param indent_anchor [Symbol] # the different ways of handling the indentation of nested groups. # - `:end_of_previous_line`: In the case of a new line in a nested group, # the next string token will be displayed with indentation = previous # line width + last group indentation. Defined in Oppen's paper. # # - `:current_offset`: When printing a new line in a nested group, the # next string token will be displayed with an indentation equal to the # sum of the indentations of all its parent groups. This is an # extension to Oppen's work. # # @param trim_trailing_whitespaces [Boolean] # whether to trim trailing whitespaces. # @param upsize_stack [Boolean] # whether to upsize stack when needed. # # @example `:end_of_previous_line` anchor # config = Oppen::Config.new(indent_anchor: :end_of_previous_line) # out = Oppen::Wadler.new config:, width: 13 # out.text 'And she said:' # out.group(indent: 4) { # out.group(indent: 4) { # out.break # out.text 'Hello, World!' # } # } # out.output # # # => # # And she said: # # Hello, World! # # @example `:current_offset anchor` # config = Oppen::Config.new(indent_anchor: :current_offset) # out = Oppen::Wadler.new config:, width: 13 # out.text 'And she said:' # out.group(indent: 4) { # out.group(indent: 4) { # out.break # out.text 'Hello, World!' # } # } # out.output # # # => # # And she said: # # Hello, World! def initialize(eager_print: false, indent_anchor: :end_of_previous_line, trim_trailing_whitespaces: false, upsize_stack: false) @eager_print = eager_print @indent_anchor = indent_anchor @trim_trailing_whitespaces = trim_trailing_whitespaces @upsize_stack = upsize_stack end # Print groups eagerly. # # @example # out = Oppen::Wadler.new(width: 13) # out.group { # out.group { # out.text 'abc' # out.breakable # out.text 'def' # } # out.group { # out.text 'ghi' # out.breakable # out.text 'jkl' # } # } # out.output # # # eager_print: false => # # abc # # defghi jkl # # # # eager_print: true => # # abc defghi # # jkl # # @return [Boolean] def eager_print? = @eager_print def trim_trailing_whitespaces? = @trim_trailing_whitespaces def upsize_stack? = @upsize_stack # Default configuration that provides printing behaviour identical to what's # been described by Oppen. # # @return [Config] def self.oppen new end # Configure the printer to behave more like # [ruby/prettyprint](https://github.com/ruby/prettyprint): # # 1. groups are printed eagerly (we try to flush on a group's close). # 2. The indentation is anchored on the left margin. # 3. Trailing whitespaces are removed. # # The name was amusingly chosen in reference to # [Wadler](https://homepages.inf.ed.ac.uk/wadler/papers/prettier/prettier.pdf)'s # work on pretty printing. # # @return [Config] def self.wadler(eager_print: true, trim_trailing_whitespaces: true, upsize_stack: true) new( eager_print:, indent_anchor: :current_offset, trim_trailing_whitespaces:, upsize_stack:, ) end end # @param value [String] # the string to print. # @param width [Integer] # the string's effective width. Useful when printing HTML, e.g. # `<span>value</span>`, where the effective width is that of the inner # text. # # @return [Token::String] # a new String token. def self.string(value, width: value.length) Token::String.new(value, width:) end # @return [Token::Whitespace] a new Whitespace token. def self.whitespace(value) Token::Whitespace.new(value, width: value.bytesize) end # @param str [String] # value shown if no new line is needed. # @param line_continuation [String] # printed before the line break. # @param offset [Integer] # additional indentation to be added to the current indentation level. # @param width [Integer] # the string's effective width. Useful when printing HTML, e.g. # `<span>value</span>`, where the effective width is that of the inner # text. # # @return [Token::Break] # a new Break token. # # @see Wadler#break example on `line_continuation`. def self.break(str = ' ', line_continuation: '', offset: 0, width: str.length) Token::Break.new(str, width:, line_continuation:, offset:) end # @param line_continuation [String] # printed before the line break. # @param offset [Integer] # additional indentation to be added to the current indentation level. # # @return [Token::LineBreak] # a new LineBreak token. # # @see Wadler#break example on `line_continuation`. def self.line_break(line_continuation: '', offset: 0) Token::LineBreak.new(line_continuation:, offset:) end # In a consistent group, the presence of a new line inside the group will # propagate to the other Break tokens in the group causing them all to act as # a new line. # # @param offset [Integer] # the additional indentation of the group. # # @return [Token::Begin] # a new consistent Begin token. # # @example Function Arguments # fun( # arg1, # arg2, # arg3, # arg4, # ) # # @see Wadler#group def self.begin_consistent(offset: 2) Token::Begin.new(break_type: :consistent, offset:) end # In an inconsistent group, the presence of a new line inside the group will # not propagate to the other Break tokens in the group letting them decide if # they need to act as a new line or not. # # @param offset [Integer] the additional indentation of the group. # # @return [Token::Begin] a new inconsistent Begin token. # # @example when used for the display of a function's arguments. # fun( # arg1, arg2, # arg3, arg4, # ) # # @see Wadler#group def self.begin_inconsistent(offset: 2) Token::Begin.new(break_type: :inconsistent, offset:) end # @return [Token::End] a new End token. def self.end Token::End.new end # @return [Token::EOF] a new EOF token. def self.eof Token::EOF.new end end