# frozen_string_literal: true # Oppen. module Oppen # Wadler. class Wadler # @return [Config] # The printer's configuration, altering its behavior. attr_reader :config # @return [Integer] # the current indentation amount. attr_reader :current_indent # @return [String] # the new line string, e.g. `\n`. attr_reader :new_line # @return [Object] # the output string buffer. It should have a `write` and `string` methods. attr_reader :out # @return [Proc] # space generator, a callable. attr_reader :space_gen # @return [Array<Token>] # the tokens list that is being built. attr_reader :tokens # @return [String] # the whitespace character. Used to trim trailing whitespaces. attr_reader :whitespace # @return [Integer] # maximum line width. attr_reader :width # @param base_indent [Integer] # the starting indentation level for the whole printer. # @param config [Config] # to customize the printer's behavior. # @param indent [Integer] # the default indentation amount for {group} and {nest}. # @param new_line [String] # the new line String. # @param out [Object] # the output string buffer. It should have a `write` and `string` methods. # @param space_gen [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 whitespace [String] the whitespace character. Used to trim trailing whitespaces. # @param width [Integer] maximum line width desired. # # @see Token::Whitespace def initialize(base_indent: 0, config: Config.wadler, indent: 0, new_line: "\n", out: StringIO.new, space_gen: ' ', whitespace: ' ', width: 80) @config = config @current_indent = base_indent @indent = indent @new_line = new_line @out = out @space_gen = space_gen @tokens = [] @whitespace = whitespace @width = width end # Add missing {Token::Begin}, {Token::End} or {Token::EOF}. # # @return [Nil] def add_missing_begin_and_end tokens.unshift Oppen.begin_consistent(offset: 0) tokens << Oppen.end tokens << Oppen.eof if !tokens.last.is_a?(Oppen::Token::EOF) end # Call this to extract the final pretty-printed output. # # @return [String] def output add_missing_begin_and_end Oppen.print( tokens:, new_line:, config:, space: space_gen, out:, width:, ) end # Convert a list of tokens to its wadler representation. # # This method reverse engineers a tokens list to transform it into Wadler # printing commands. It can be particularly useful when debugging a black # box program. # # @option kwargs [Integer] :base_indent # the base indentation amount of the output. # @option kwargs [String] :printer_name # the name of the Wadler instance in the output. # # @example # out = Oppen::Wadler.new # out.text('Hello World!') # out.show_print_commands(out_name: 'out') # # # => # # out.group(:consistent, indent: 0) { # # out.text("Hello World!", width: 12) # # } # # @return [String] def show_print_commands(**kwargs) add_missing_begin_and_end Oppen.tokens_to_wadler(tokens, **kwargs) end # Create a new group. # # @param indent [Integer] # indentation. # @param delim [Nil|String|Symbol|Array<Nil, String, Symbol>] # delimiters, to be printed at the start and the end of the group: # - If it's nil, nothing will be printed # - If it's a Strings or a Symbol, it will be printed at both positions. # - If it's an Array of many items, the first two elements will be used # for the start and end of the group. # @param break_type [Token::BreakType] # break type. # # @yield # the block of text in a group. # # @example 1 String Delimiter # out = Oppen::Wadler.new # out # .text('a') # .group(indent: 2, delim: '|') { # out.break.text 'b' # } # puts out.output # # # => # # a # # | # # b # # | # # @example 1 Delimiter in Array # out = Oppen::Wadler.new # out # .text('a') # .group(indent: 2, delim: ['|']) { # out.break.text 'b' # } # puts out.output # # # => # # a # # | # # b # # @example 2 Delimiters # out = Oppen::Wadler.new # out # .text('a') # .group(indent: 2, delim: %i[{ }]) { # out.break.text 'b' # } # puts out.output # # # => # # a # # { # # b # # } # # @example Consistent Breaking # out = Oppen::Wadler.new # out.group(:consistent) { # out.text('a').break.text('b').breakable.text('c') # } # puts out.output # # # => # # a # # b # # c # # @example Inconsistent Breaking # out = Oppen::Wadler.new # out.group(:inconsistent) { # out.text('a').break.text('b').breakable.text('c') # } # puts out.output # # # => # # a # # b c # # @return [self] # # @see Oppen.begin_consistent # @see Oppen.begin_inconsistent def group(break_type = :consistent, delim: nil, indent: @indent) lft, rgt = case delim in nil then ['', ''] in String | Symbol then [delim, delim] in Array then delim.values_at(0, 1).map(&:to_s) end tokens << case break_type in :consistent Oppen.begin_consistent(offset: indent) in :inconsistent Oppen.begin_inconsistent(offset: indent) end if !lft.empty? self.break text lft end yield if !rgt.empty? self.break text rgt end tokens << Oppen.end self end # An alias for `group(:consistent, ...)` def consistent(...) group(:consistent, ...) end # An alias for `group(:inconsistent, ...)` def inconsistent(...) group(:inconsistent, ...) end # Create a new non-strict {group}. # # {group}s isolate breaking decisions, and in that sense they're considered # strict; e.g. when a breakable is transformed into an actual break, its # parent {group} might not get broken if the result could fit on the line. # # This is not the case with {nest}: if the same breakable was in a {nest}, the # {group} containing the {nest} will also be broken. # # @note indentation cannot happen if there are no breaks in the {nest}. # # @note a {nest} will not forcibly indent its content if the break type of # the enclosing {group} is `:inconsistent`. # # @param delim [Nil|String|Symbol|Array<Nil, String, Symbol>] # delimiters, to be printed at the start and the end of the group: # - `nil` is always the empty string. # - If it's a Strings or a Symbol, it will be printed at both positions. # - If it's an Array of many items, the first two elements will be used # for the start and end of the group. # @param indent [Integer] # indentation. # # @yield # the block of text in a nest. # # @example # out = Oppen::Wadler.new # out.nest(delim: %i[{ }], indent: 2) { # out.text('a').break.text('b') # } # puts out.output # # # => # # { # # a # # b # # } # # @return [self] def nest(delim: nil, indent: @indent) lft, rgt = case delim in nil then ['', ''] in String | Symbol then [delim, delim] in Array then delim.values_at(0, 1).map(&:to_s) end @current_indent += indent if !lft.empty? text lft self.break end begin yield ensure @current_indent -= indent end if !rgt.empty? self.break text rgt end self end # Create a new text element. # # @param value [String] # the value of the token. # # @return [self] def text(value, width: value.length) if config.trim_trailing_whitespaces? && value.match(/((?:#{Regexp.escape(whitespace)})+)\z/) match = Regexp.last_match(1) matched_length = match.length if value.length != matched_length tokens << Oppen.string(value[0...-matched_length], width: width - matched_length) end tokens << Oppen.whitespace(match) else tokens << Oppen.string(value, width:) end self end # Create a new breakable element. # # @param str [String] # the value of the token that will be displayed if no new line is needed. # @param line_continuation [String] # printed before the line break. # @param width [Integer] # the width of the token. # # @return [self] # # @see Wadler#break example on `line_continuation`. def breakable(str = ' ', line_continuation: '', width: str.length) tokens << Oppen.break(str, width:, line_continuation:, offset: current_indent) self end # Create a new break element. # # @param line_continuation [String] # printed before the line break. # # @example # out = Oppen::Wadler.new # out.text 'a' # out.break # out.text 'b' # out.break line_continuation: '#' # out.text 'c' # puts out.output # # # => # # a # # b# # # c # # @return [self] def break(line_continuation: '') tokens << Oppen.line_break(line_continuation:, offset: current_indent) self end # A convenient way to avoid breaking chains of calls. # # @example # out # .do { fn_call(fn_arg) } # .breakable # .text('=') # .breakable # .do { fn_call(fn_arg) } # # @yield to execute the passed block # # @return [self] def do yield self end # A means to wrap a piece of code in several ways. # # @example # out # .wrap { # # all printing instructions here will be deferred. # # they will be executed in `when` blocks by calling the `wrapped`. # out.text(...) # # ... # } # This is "wrapped". # .when(cond1){ |wrapped| # # when cond1 is true you execute this block. # out.text("before wrapped") # # call the wrapped # wrapped.call # # and continue printing # out.text("after wrapped) # } # .when(cond2){ |wrapped| # # and you cand define many conditions. # } # .end # # @example Calling `end` is not needed if there's another call after the last `when`: # out # .wrap{...} # This is "wrapped". # .when(cond1){ |wrapped| ... } # .when(cond2){ |wrapped| ... } # .text('foo') # # @return [Wrap] def wrap(&blk) Wrap.new(blk) end # Produce a separated list. # # @example Consistent Breaking # puts out.separate((1..3).map(&:to_s), ',') { |i| out.text i} # # # => # # 1, # # 2, # # 3 # # @example Inconsistent Breaking # puts out.separate((1..3).map(&:to_s), ',', break_type: :inconsistent) { |i| out.text i} # # # => # # 1, 2, # # 3 # # @param args [String] # a list of values. # @param sep [String] # a separator. # @param breakable [String|Nil] # adds a `breakable` after the separator. # @param break_pos [Symbol] # whether to break :before or :after the seraparator. # @param break_type [Symbol|Nil] # whether the break is :consistent or :inconsistent. # If nil is given, the tokens will not be surrounded by a group. # @param indent [Boolean|Integer] # - If `true`, indent by @indent. # - If an 'Integer', indent by its value. # @param force_break [Boolean] # adds a `break` after the separator. # @param line_continuation [String] # string to display before new line. # # @yield to execute the passed block. # # @return [self] def separate(args, sep, breakable: ' ', break_pos: :after, break_type: nil, indent: false, force_break: false, line_continuation: '') if args.is_a?(Enumerator) ? args.count == 1 : args.length == 1 yield(*args[0]) return self end first = true wrap { wrap { args&.each do |*as| if first breakable '' if !line_continuation.empty? && break_pos == :after first = false elsif break_pos == :after text sep breakable(breakable, line_continuation:) if breakable && !force_break self.break(line_continuation:) if force_break else breakable(breakable, line_continuation:) if breakable && !force_break self.break(line_continuation:) if force_break text sep end yield(*as) end } .when(break_type) { |body| group(break_type, indent: 0) { body.() } } .end } .when(indent) { |body| nest(indent: indent.is_a?(Integer) ? indent : @indent) { body.() } }.end breakable('', line_continuation:) if !line_continuation.empty? && !break_type self end # A shorhand for `text ' '`. # # @return [self] def space text ' ' end # Surround a block with +lft+ and +rgt+ # # @param lft [String] lft # left surrounding string. # @param rgt [String] rgt # right surrounding string. # # @yield the passed block to be surrounded with `lft` and `rgt`. # # @option opts [Boolean] :group (true) # whether to create a group enclosing `lft`, `rgt`, and the passed block. # @option opts [Boolean] :indent (@indent) # whether to indent the passed block. # @option opts [String] :lft_breakable ('') # left breakable string. # @option opts [Boolean] :lft_can_break (true) # injects `break` or `breakable` only if true; # i.e. `lft_breakable` will be ignored if false. # @option opts [Boolean] :lft_force_break (false) # force break instead of using `lft_breakable`. # @option opts [String] :rgt_breakable ('') # right breakable string. # @option opts [Boolean] :rgt_can_break (true) # injects `break` or `breakable` only if true. # i.e. `rgt_breakable` will be ignored if false. # @option opts [Boolean] :rgt_force_break (false) # force break instead of using `rgt_breakable`. # # @return [self] def surround(lft, rgt, **opts) group = opts.fetch(:group, true) group_open(break_type: :inconsistent) if group text lft if lft indent = opts.fetch(:indent, @indent) nest_open(indent:) lft_breakable = opts.fetch(:lft_breakable, '') lft_can_break = opts.fetch(:lft_can_break, true) lft_force_break = opts.fetch(:lft_force_break, false) if lft && lft_can_break if lft_force_break self.break else breakable lft_breakable end end if block_given? yield end nest_close rgt_breakable = opts.fetch(:rgt_breakable, '') rgt_can_break = opts.fetch(:rgt_can_break, true) rgt_force_break = opts.fetch(:rgt_force_break, false) if rgt if rgt_can_break if rgt_force_break self.break else breakable rgt_breakable end end text rgt end group_close if group self end # @!group Convenience Methods Built On {separate} # Separate args into lines. # # This is a wrapper around {separate} where `breakable: true`. # # @see [separate] def lines(*args, **kwargs, &) separate(*args, **kwargs.merge(force_break: true), &) end # Concatenates args. # # This is a wrapper around {separate} where `breakable: false`. # # @see [separate] def concat(*args, **kwargs, &) separate(*args, **kwargs.merge(breakable: false), &) end # @!endgroup # @!group Convenience Methods Built On {surround} # YARD doesn't drop into blocks, so we can't use metaprogramming # to generate all these functions, so we're copy-pastring. # {surround} with `< >`. New lines can appear after and before the delimiters. # # @param padding [String] ('') # Passed to `lft_breakable` and `rgt_breakable`. # # @return [self] def angles(padding: '', **kwargs, &block) surround( '<', '>', **kwargs.merge(lft_breakable: padding, rgt_breakable: padding), &block ) end # {surround} with `< >`. New lines cannot appear after and before the delimiters. # # @return [self] def angles_break_both(**kwargs, &) angles(**kwargs.merge(lft_force_break: true, rgt_force_break: true), &) end # {surround} with `< >`. New lines will appear after and before the delimiters. # # @return [self] def angles_break_none(**kwargs, &) angles(**kwargs.merge(lft_can_break: false, rgt_can_break: false), &) end # {surround} with `{ }`. New lines can appear after and before the delimiters. # # @param padding [String] ('') # Passed to `lft_breakable` and `rgt_breakable`. # # @return [self] def braces(padding: '', **kwargs, &block) surround( '{', '}', **kwargs.merge(lft_breakable: padding, rgt_breakable: padding), &block ) end # {surround} with `{ }`. New lines cannot appear after and before the delimiters. # # @return [self] def braces_break_both(**kwargs, &) braces(**kwargs.merge(lft_force_break: true, rgt_force_break: true), &) end # {surround} with `{ }`. New lines will appear after and before the delimiters. # # @return [self] def braces_break_none(**kwargs, &) braces(**kwargs.merge(lft_can_break: false, rgt_can_break: false), &) end # {surround} with `[ ]`. New lines can appear after and before the delimiters. # # @param padding [String] ('') # Passed to `lft_breakable` and `rgt_breakable`. # # @return [self] def brackets(padding: '', **kwargs, &block) surround( '[', ']', **kwargs.merge(lft_breakable: padding, rgt_breakable: padding), &block ) end # {surround} with `[ ]`. New lines cannot appear after and before the delimiters. # # @return [self] def brackets_break_both(**kwargs, &) brackets(**kwargs.merge(lft_force_break: true, rgt_force_break: true), &) end # {surround} with `[ ]`. New lines will appear after and before the delimiters. # # @return [self] def brackets_break_none(**kwargs, &) brackets(**kwargs.merge(lft_can_break: false, rgt_can_break: false), &) end # {surround} with `( )`. New lines can appear after and before the delimiters. # # @param padding [String] ('') # Passed to `lft_breakable` and `rgt_breakable`. # # @return [self] def parens(padding: '', **kwargs, &block) surround( '(', ')', **kwargs.merge(lft_breakable: padding, rgt_breakable: padding), &block ) end # {surround} with `( )`. New lines cannot appear after and before the delimiters. # # @return [self] def parens_break_both(**kwargs, &) parens(**kwargs.merge(lft_force_break: true, rgt_force_break: true), &) end # {surround} with `( )`. New lines will appear after and before the delimiters. # # @return [self] def parens_break_none(**kwargs, &) parens(**kwargs.merge(lft_can_break: false, rgt_can_break: false), &) end # {surround} with `` ` ` ``. New lines cannot appear after and before the delimiters # unless you specify it with `rgt_can_break` and `lft_can_break`. # # @return [self] def backticks(**kwargs, &) surround('`', '`', lft_can_break: false, rgt_can_break: false, **kwargs, &) end # {surround} with `" "`. New lines cannot appear after and before the delimiters # unless you specify it with `rgt_can_break` and `lft_can_break`. # # @return [self] def quote_double(**kwargs, &) surround('"', '"', lft_can_break: false, rgt_can_break: false, **kwargs, &) end # {surround} with `' '`. New lines cannot appear after and before the delimiters # unless you specify it with `rgt_can_break` and `lft_can_break`. # # @return [self] def quote_single(**kwargs, &) surround("'", "'", lft_can_break: false, rgt_can_break: false, **kwargs, &) end # Open a consistent group. # # @param break_type [Symbol] # `:consistent` or `:inconsistent` # @param indent [Integer] # the amount of indentation of the group. # # @return [self] # # @see Oppen.begin_consistent # @see Oppen.begin_inconsistent def group_open(break_type: :consistent, indent: 0) if %i[consistent inconsistent].none?(break_type) raise ArgumentError, '%s is not a valid type. Choose one: :consistent or :inconsistent' end tokens << Oppen.send(:"begin_#{break_type}", offset: indent) self end # Close a group. # # @return [self] def group_close tokens << Oppen.end self end # Open a consistent group and add indent amount. # # @param indent [Integer] # the amount of indentation of the group. # # @return [self] def indent_open(indent: @indent) @current_indent += indent group_open end # Close a group and subtract indent. # # @param indent [Integer] # the amount of indentation of the group. # # @return [self] def indent_close(indent: @indent) @current_indent -= indent group_close end # Open a nest by adding indent. # # @param indent [Integer] # the amount of indentation of the nest. # # @return [self] def nest_open(indent: @indent) @current_indent += indent self end # Close a nest by subtracting indent. # # @param indent [Integer] # the amount of indentation of the nest. # # @return [self] def nest_close(indent: @indent) @current_indent -= indent self end # @!endgroup # Helper class to allow conditional printing. class Wrap def initialize(blk) @wrapped = blk @wrapper = nil end # Conditional. def when(cond, &blk) if cond @wrapper = blk end self end # Flush. def end @wrapper ? @wrapper.(@wrapped) : @wrapped.() end # To re-enable chaining. def method_missing(meth, ...) self.end.send(meth, ...) end # To re-enable chaining. def respond_to_missing?(meth, include_private) self.end.respond_to_missing?(meth, include_private) end end end end