loading
Generated 2025-11-07T23:03:05+00:00

All Files ( 100.0% covered at 8.2 hits/line )

20 files in total.
316 relevant lines, 316 lines covered and 0 lines missed. ( 100.0% )
14 total branches, 14 branches covered and 0 branches missed. ( 100.0% )
File % covered Lines Relevant Lines Lines covered Lines missed Avg. Hits / Line Branch Coverage Branches Covered branches Missed branches
lib/tocer.rb 100.00 % 18 10 10 0 1.30 100.00 % 0 0 0
lib/tocer/../../tocer.gemspec 100.00 % 42 27 27 0 1.00 100.00 % 0 0 0
lib/tocer/builder.rb 100.00 % 76 38 38 0 28.74 100.00 % 6 6 0
lib/tocer/cli/actions/label.rb 100.00 % 22 10 10 0 2.00 100.00 % 0 0 0
lib/tocer/cli/actions/pattern.rb 100.00 % 22 10 10 0 2.10 100.00 % 0 0 0
lib/tocer/cli/actions/root.rb 100.00 % 22 10 10 0 2.00 100.00 % 0 0 0
lib/tocer/cli/commands/upsert.rb 100.00 % 31 16 16 0 1.88 100.00 % 0 0 0
lib/tocer/cli/shell.rb 100.00 % 39 21 21 0 3.38 100.00 % 0 0 0
lib/tocer/configuration/contract.rb 100.00 % 16 9 9 0 1.00 100.00 % 0 0 0
lib/tocer/configuration/model.rb 100.00 % 13 6 6 0 17.67 100.00 % 0 0 0
lib/tocer/container.rb 100.00 % 27 16 16 0 4.63 100.00 % 0 0 0
lib/tocer/dependencies.rb 100.00 % 7 3 3 0 1.00 100.00 % 0 0 0
lib/tocer/elements/comment_block.rb 100.00 % 41 19 19 0 22.68 100.00 % 0 0 0
lib/tocer/parsers/header.rb 100.00 % 22 10 10 0 7.90 100.00 % 0 0 0
lib/tocer/rake/register.rb 100.00 % 36 20 20 0 1.80 100.00 % 0 0 0
lib/tocer/runner.rb 100.00 % 30 14 14 0 4.79 100.00 % 0 0 0
lib/tocer/transformers/finder.rb 100.00 % 24 11 11 0 14.73 100.00 % 2 2 0
lib/tocer/transformers/link.rb 100.00 % 39 19 19 0 1.84 100.00 % 0 0 0
lib/tocer/transformers/text.rb 100.00 % 33 16 16 0 3.94 100.00 % 0 0 0
lib/tocer/writer.rb 100.00 % 62 31 31 0 7.48 100.00 % 6 6 0

lib/tocer.rb

100.0% lines covered

100.0% branches covered

10 relevant lines. 10 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "zeitwerk"
  3. 1 Zeitwerk::Loader.new.then do |loader|
  4. 1 loader.inflector.inflect "cli" => "CLI"
  5. 1 loader.tag = File.basename __FILE__, ".rb"
  6. 1 loader.ignore "#{__dir__}/tocer/rake"
  7. 1 loader.push_dir __dir__
  8. 1 loader.setup
  9. end
  10. # Main namespace.
  11. 1 module Tocer
  12. 1 def self.loader registry = Zeitwerk::Registry
  13. 4 @loader ||= registry.loaders.each.find { |loader| loader.tag == File.basename(__FILE__, ".rb") }
  14. end
  15. end

lib/tocer/../../tocer.gemspec

100.0% lines covered

100.0% branches covered

27 relevant lines. 27 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 Gem::Specification.new do |spec|
  3. 1 spec.name = "tocer"
  4. 1 spec.version = "19.5.0"
  5. 1 spec.authors = ["Brooke Kuhlmann"]
  6. 1 spec.email = ["brooke@alchemists.io"]
  7. 1 spec.homepage = "https://alchemists.io/projects/tocer"
  8. 1 spec.summary = "A command line interface for generating Markdown table of contents."
  9. 1 spec.license = "Hippocratic-2.1"
  10. 1 spec.metadata = {
  11. "bug_tracker_uri" => "https://github.com/bkuhlmann/tocer/issues",
  12. "changelog_uri" => "https://alchemists.io/projects/tocer/versions",
  13. "homepage_uri" => "https://alchemists.io/projects/tocer",
  14. "funding_uri" => "https://github.com/sponsors/bkuhlmann",
  15. "label" => "Tocer",
  16. "rubygems_mfa_required" => "true",
  17. "source_code_uri" => "https://github.com/bkuhlmann/tocer"
  18. }
  19. 1 spec.signing_key = Gem.default_key_path
  20. 1 spec.cert_chain = [Gem.default_cert_path]
  21. 1 spec.required_ruby_version = ">= 3.4"
  22. 1 spec.add_dependency "cogger", "~> 1.0"
  23. 1 spec.add_dependency "containable", "~> 1.1"
  24. 1 spec.add_dependency "core", "~> 2.5"
  25. 1 spec.add_dependency "dry-schema", "~> 1.13"
  26. 1 spec.add_dependency "etcher", "~> 3.0"
  27. 1 spec.add_dependency "infusible", "~> 4.0"
  28. 1 spec.add_dependency "refinements", "~> 13.6"
  29. 1 spec.add_dependency "runcom", "~> 12.0"
  30. 1 spec.add_dependency "sod", "~> 1.5"
  31. 1 spec.add_dependency "spek", "~> 4.0"
  32. 1 spec.add_dependency "zeitwerk", "~> 2.7"
  33. 1 spec.bindir = "exe"
  34. 1 spec.executables << "tocer"
  35. 1 spec.extra_rdoc_files = Dir["README*", "LICENSE*"]
  36. 1 spec.files = Dir["*.gemspec", "lib/**/*"]
  37. end

lib/tocer/builder.rb

100.0% lines covered

100.0% branches covered

38 relevant lines. 38 lines covered and 0 lines missed.
6 total branches, 6 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "forwardable"
  3. 1 module Tocer
  4. # Builds table of contents for a Markdown document.
  5. 1 class Builder
  6. 1 extend Forwardable
  7. 1 include Dependencies[:settings]
  8. 1 CODE_BLOCK_PUNCTUATION = "```"
  9. 1 def_delegators :comment_block, :start_index, :finish_index, :comments, :prependable?
  10. 1 def initialize(
  11. comment_block: Elements::CommentBlock.new,
  12. transformer: Transformers::Finder.new,
  13. **
  14. )
  15. 33 @comment_block = comment_block
  16. 33 @transformer = transformer
  17. 33 @url_count = Hash.new 0
  18. 33 @code_block = false
  19. 33 super(**)
  20. end
  21. 1 def unbuildable?(lines) = comment_block.empty?(lines) && headers(lines).empty?
  22. 1 def call lines
  23. 22 then: 6 else: 16 return "" if headers(lines).empty?
  24. 16 url_count.clear
  25. 16 assemble(lines).join
  26. end
  27. 1 private
  28. 1 attr_reader :comment_block, :transformer, :url_count
  29. 1 attr_accessor :code_block
  30. 1 def assemble lines
  31. [
  32. 16 "#{comment_block.start_tag}\n\n",
  33. "#{settings.label}\n\n",
  34. links(lines).join("\n"),
  35. "\n\n#{comment_block.finish_tag}\n"
  36. ]
  37. end
  38. 40 def links(lines) = headers(lines).map { |markdown| transform markdown }
  39. 1 def headers lines
  40. 44 lines.select do |line|
  41. 173 toggle_code_block line
  42. 173 line.start_with?(Parsers::Header::PUNCTUATION) && !code_block
  43. end
  44. end
  45. 1 def toggle_code_block line
  46. 173 else: 2 then: 171 return unless line.start_with? CODE_BLOCK_PUNCTUATION
  47. 2 self.code_block = !code_block
  48. end
  49. 1 def transform markdown
  50. 39 transformer.call(markdown).then do |instance|
  51. 39 url = instance.url
  52. 39 link = instance.call url_suffix: url_suffix(url)
  53. 39 url_count[url] += 1
  54. 39 link
  55. end
  56. end
  57. 40 then: 37 else: 2 def url_suffix(url) = url_count[url].then { |count| count.zero? ? "" : count }
  58. end
  59. end

lib/tocer/cli/actions/label.rb

100.0% lines covered

100.0% branches covered

10 relevant lines. 10 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "sod"
  3. 1 module Tocer
  4. 1 module CLI
  5. 1 module Actions
  6. # Stores table of contents label.
  7. 1 class Label < Sod::Action
  8. 1 include Dependencies[:settings]
  9. 1 description "Set label."
  10. 1 on %w[-l --label], argument: "[TEXT]"
  11. 11 default { Container[:settings].label }
  12. 1 def call(label = default) = settings.label = label
  13. end
  14. end
  15. end
  16. end

lib/tocer/cli/actions/pattern.rb

100.0% lines covered

100.0% branches covered

10 relevant lines. 10 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "sod"
  3. 1 module Tocer
  4. 1 module CLI
  5. 1 module Actions
  6. # Stores table of contents file patterns.
  7. 1 class Pattern < Sod::Action
  8. 1 include Dependencies[:settings]
  9. 1 description "Set file patterns."
  10. 1 on %w[-p --patterns], argument: "[a,b,c]"
  11. 12 default { Container[:settings].patterns }
  12. 1 def call(patterns = default) = settings.patterns = Array(patterns)
  13. end
  14. end
  15. end
  16. end

lib/tocer/cli/actions/root.rb

100.0% lines covered

100.0% branches covered

10 relevant lines. 10 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "sod"
  3. 1 module Tocer
  4. 1 module CLI
  5. 1 module Actions
  6. # Stores table of contents root path.
  7. 1 class Root < Sod::Action
  8. 1 include Dependencies[:settings]
  9. 1 description "Set root directory."
  10. 1 on %w[-r --root], argument: "[PATH]"
  11. 11 default { Container[:settings].root_dir }
  12. 1 def call(path = default) = settings.root_dir = Pathname(path)
  13. end
  14. end
  15. end
  16. end

lib/tocer/cli/commands/upsert.rb

100.0% lines covered

100.0% branches covered

16 relevant lines. 16 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "sod"
  3. 1 module Tocer
  4. 1 module CLI
  5. 1 module Commands
  6. # Stores table of contents root path.
  7. 1 class Upsert < Sod::Command
  8. 1 handle "upsert"
  9. 1 description "Update/insert table of contents."
  10. 1 on Actions::Root
  11. 1 on Actions::Label
  12. 1 on Actions::Pattern
  13. 1 def initialize(runner: Runner.new, **)
  14. 8 @runner = runner
  15. 8 super(**)
  16. end
  17. 1 def call = runner.call
  18. 1 private
  19. 1 attr_reader :runner
  20. end
  21. end
  22. end
  23. end

lib/tocer/cli/shell.rb

100.0% lines covered

100.0% branches covered

21 relevant lines. 21 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "sod"
  3. 1 module Tocer
  4. 1 module CLI
  5. # The main Command Line Interface (CLI) object.
  6. 1 class Shell
  7. 1 include Dependencies[:defaults_path, :xdg_config, :specification]
  8. 1 def initialize(context: Sod::Context, dsl: Sod, **)
  9. 6 super(**)
  10. 6 @context = context
  11. 6 @dsl = dsl
  12. end
  13. 1 def call(...) = cli.call(...)
  14. 1 private
  15. 1 attr_reader :context, :dsl
  16. 1 def cli
  17. 6 context = build_context
  18. 6 dsl.new :tocer, banner: specification.banner do
  19. 6 on(Sod::Prefabs::Commands::Config, context:)
  20. 6 on Commands::Upsert
  21. 6 on(Sod::Prefabs::Actions::Version, context:)
  22. 6 on Sod::Prefabs::Actions::Help, self
  23. end
  24. end
  25. 1 def build_context
  26. 6 context[defaults_path:, xdg_config:, version_label: specification.labeled_version]
  27. end
  28. end
  29. end
  30. end

lib/tocer/configuration/contract.rb

100.0% lines covered

100.0% branches covered

9 relevant lines. 9 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "dry/schema"
  3. 1 require "etcher"
  4. 1 Dry::Schema.load_extensions :monads
  5. 1 module Tocer
  6. 1 module Configuration
  7. 1 Contract = Dry::Schema.Params do
  8. 1 required(:label).filled :string
  9. 1 required(:patterns).array :string
  10. 1 required(:root_dir).filled Etcher::Types::Pathname
  11. end
  12. end
  13. end

lib/tocer/configuration/model.rb

100.0% lines covered

100.0% branches covered

6 relevant lines. 6 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Tocer
  3. 1 module Configuration
  4. # Defines the content of the configuration for use throughout the gem.
  5. 1 Model = Struct.new :label, :root_dir, :patterns do
  6. 1 def initialize(**)
  7. 51 super
  8. 51 self[:patterns] = Array patterns
  9. end
  10. end
  11. end
  12. end

lib/tocer/container.rb

100.0% lines covered

100.0% branches covered

16 relevant lines. 16 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "cogger"
  3. 1 require "containable"
  4. 1 require "etcher"
  5. 1 require "runcom"
  6. 1 require "spek"
  7. 1 module Tocer
  8. # Provides a global gem container for injection into other objects.
  9. 1 module Container
  10. 1 extend Containable
  11. 1 register :registry, as: :fresh do
  12. 50 Etcher::Registry.new(contract: Configuration::Contract, model: Configuration::Model)
  13. .add_loader(:yaml, self[:defaults_path])
  14. .add_loader(:yaml, self[:xdg_config].active)
  15. end
  16. 2 register(:settings) { Etcher.call(self[:registry]).dup }
  17. 2 register(:defaults_path) { Pathname(__dir__).join("configuration/defaults.yml") }
  18. 2 register(:xdg_config) { Runcom::Config.new "tocer/configuration.yml" }
  19. 7 register(:specification) { Spek::Loader.call "#{__dir__}/../../tocer.gemspec" }
  20. 1 register(:logger) { Cogger.new id: :tocer }
  21. 1 register :io, STDOUT
  22. end
  23. end

lib/tocer/dependencies.rb

100.0% lines covered

100.0% branches covered

3 relevant lines. 3 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "infusible"
  3. 1 module Tocer
  4. 1 Dependencies = Infusible[Container]
  5. end

lib/tocer/elements/comment_block.rb

100.0% lines covered

100.0% branches covered

19 relevant lines. 19 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Tocer
  3. 1 module Elements
  4. # Represents a table of contents start and finish comment block.
  5. 1 class CommentBlock
  6. 1 def self.index lines, id
  7. 265 lines.index { |line| line =~ /<!--.*#{Regexp.escape id}.*-->/ }
  8. .to_i
  9. end
  10. 1 def initialize start_id: "Tocer[start]",
  11. finish_id: "Tocer[finish]",
  12. message: "Auto-generated, don't remove."
  13. 47 @start_id = start_id
  14. 47 @finish_id = finish_id
  15. 47 @message = message
  16. end
  17. 1 def comments = "#{start_tag}\n#{finish_tag}\n"
  18. 1 def start_index(lines) = self.class.index(lines, start_id)
  19. 1 def start_tag = comment(start_id, message)
  20. 1 def finish_index(lines) = self.class.index(lines, finish_id)
  21. 1 def finish_tag = comment(finish_id, message)
  22. 11 def empty?(lines) = (finish_index(lines) - start_index(lines)) == 1
  23. 1 def prependable?(lines) = start_index(lines).zero? && finish_index(lines).zero?
  24. 1 private
  25. 1 attr_reader :start_id, :finish_id, :message
  26. 1 def comment(id, message) = "<!-- #{id}: #{message} -->"
  27. end
  28. end
  29. end

lib/tocer/parsers/header.rb

100.0% lines covered

100.0% branches covered

10 relevant lines. 10 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Tocer
  3. 1 module Parsers
  4. # Represents a Markdown header.
  5. 1 class Header
  6. 1 PUNCTUATION = "#"
  7. 1 def initialize markdown
  8. 70 @markdown = markdown
  9. end
  10. 1 def prefix = String(markdown[/#{PUNCTUATION}{1,}/o])
  11. 1 def content = markdown[prefix.length + 1, markdown.length].strip
  12. 1 private
  13. 1 attr_reader :markdown
  14. end
  15. end
  16. end

lib/tocer/rake/register.rb

100.0% lines covered

100.0% branches covered

20 relevant lines. 20 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "rake"
  3. 1 require "refinements/struct"
  4. 1 require "tocer"
  5. 1 module Tocer
  6. 1 module Rake
  7. # Registers Rake tasks for use.
  8. 1 class Register
  9. 1 include ::Rake::DSL
  10. 1 include Dependencies[:settings]
  11. 1 using Refinements::Struct
  12. 1 def self.call = new.call
  13. 1 def initialize(runner: Runner.new, **)
  14. 4 @runner = runner
  15. 4 super(**)
  16. end
  17. 1 def call
  18. 4 desc "Update/Insert Table of Contents"
  19. 4 task :toc, %i[label patterns] do |_task, arguments|
  20. 3 settings.with! arguments
  21. 3 runner.call
  22. end
  23. end
  24. 1 private
  25. 1 attr_reader :runner
  26. end
  27. end
  28. end

lib/tocer/runner.rb

100.0% lines covered

100.0% branches covered

14 relevant lines. 14 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "refinements/pathname"
  3. 1 module Tocer
  4. # Generates/updates Table of Contents for files in root path.
  5. 1 class Runner
  6. 1 include Dependencies[:settings, :io]
  7. 1 using Refinements::Pathname
  8. 1 def initialize(writer: Writer.new, **)
  9. 16 super(**)
  10. 16 @writer = writer
  11. end
  12. 1 def call
  13. 12 settings.root_dir
  14. .files(%({#{settings.patterns.join ","}}))
  15. .each do |path|
  16. 7 io.puts " #{path}"
  17. 7 writer.call path
  18. end
  19. end
  20. 1 private
  21. 1 attr_reader :writer
  22. end
  23. end

lib/tocer/transformers/finder.rb

100.0% lines covered

100.0% branches covered

11 relevant lines. 11 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Tocer
  3. 1 module Transformers
  4. # Finds appropriate header transformer for matching pattern.
  5. 1 class Finder
  6. 1 TRANSFORMERS = {/\[.+\]\(.+\)/ => Transformers::Link, /.*/ => Transformers::Text}.freeze
  7. 1 def initialize transformers: TRANSFORMERS
  8. 35 @transformers = transformers
  9. end
  10. 1 def call markdown
  11. 41 transformers.find do |pattern, transformer|
  12. 78 then: 41 else: 37 break transformer.new markdown if pattern.match? markdown
  13. end
  14. end
  15. 1 private
  16. 1 attr_reader :transformers
  17. end
  18. end
  19. end

lib/tocer/transformers/link.rb

100.0% lines covered

100.0% branches covered

19 relevant lines. 19 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "core"
  3. 1 require "refinements/array"
  4. 1 module Tocer
  5. 1 module Transformers
  6. # Transforms a Markdown header (embedded link) into a table of contents link.
  7. 1 class Link
  8. 1 using Refinements::Array
  9. 1 def initialize text, parser: Parsers::Header
  10. 17 @parser = parser.new text
  11. end
  12. 1 def label = parser.content.gsub(embedded_link, embedded_link_label)
  13. 1 def url = label.downcase.gsub(/\s/, "-").gsub(/[^\w-]+/, "")
  14. 1 def call(url_suffix: "") = "#{indented_bullet}[#{label}](##{computed_url url_suffix})"
  15. 1 private
  16. 1 attr_reader :parser
  17. 1 def computed_url(suffix = Core::EMPTY_STRING) = [url, suffix.to_s].compress.join("-")
  18. 1 def embedded_link = "[#{embedded_link_label}](#{embedded_link_url})"
  19. 1 def embedded_link_label = parser.content[/\[(.*)\]/, 1]
  20. 1 def embedded_link_url = parser.content[/\((.*)\)/, 1]
  21. 1 def indented_bullet = prefix_to_spaces.gsub(/\s{2}$/, "- ")
  22. 1 def prefix_to_spaces = Array.new(parser.prefix.length, " ").join
  23. end
  24. end
  25. end

lib/tocer/transformers/text.rb

100.0% lines covered

100.0% branches covered

16 relevant lines. 16 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "core"
  3. 1 require "refinements/array"
  4. 1 module Tocer
  5. 1 module Transformers
  6. # Transforms a Markdown header (plain text) into a table of contents link.
  7. 1 class Text
  8. 1 using Refinements::Array
  9. 1 def initialize text, parser: Parsers::Header
  10. 48 @parser = parser.new text
  11. end
  12. 1 def label = parser.content
  13. 1 def url = label.downcase.gsub(/\s/, "-").gsub(/[^\w-]+/, "")
  14. 1 def call(url_suffix: "") = "#{indented_bullet}[#{label}](##{computed_url url_suffix})"
  15. 1 private
  16. 1 attr_reader :parser
  17. 1 def computed_url(suffix = Core::EMPTY_STRING) = [url, suffix.to_s].compress.join("-")
  18. 1 def indented_bullet = prefix_to_spaces.gsub(/\s{2}$/, "- ")
  19. 1 def prefix_to_spaces = Array.new(parser.prefix.length, " ").join
  20. end
  21. end
  22. end

lib/tocer/writer.rb

100.0% lines covered

100.0% branches covered

31 relevant lines. 31 lines covered and 0 lines missed.
6 total branches, 6 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "refinements/array"
  3. 1 require "refinements/pathname"
  4. 1 module Tocer
  5. # Writes table of contents to a Markdown document.
  6. # :reek:DataClump
  7. 1 class Writer
  8. 1 using Refinements::Array
  9. 1 using Refinements::Pathname
  10. 1 def self.add start_index:, old_lines:, new_lines:
  11. 9 then: 3 else: 6 computed_new_lines = start_index.zero? ? new_lines : new_lines + "\n"
  12. 9 old_lines.insert start_index, *computed_new_lines
  13. end
  14. 1 def self.remove start_index, finish_index, lines
  15. 9 range = (start_index - 1)..finish_index
  16. 84 lines.reject.with_index { |_, index| range.include? index }
  17. end
  18. 1 def initialize builder: Builder.new
  19. 24 @builder = builder
  20. end
  21. 1 def call path
  22. 15 path.rewrite do |body|
  23. 15 lines = body.each_line.to_a
  24. 15 then: 9 else: 6 builder.prependable?(lines) ? prepend(lines) : replace(lines)
  25. end
  26. end
  27. 1 private
  28. 1 attr_reader :builder
  29. 1 def replace lines
  30. 6 start_index = builder.start_index lines
  31. 6 finish_index = builder.finish_index lines
  32. 6 klass = self.class
  33. 6 klass.add(
  34. start_index:,
  35. old_lines: klass.remove(start_index, finish_index, lines),
  36. new_lines: new_lines(lines, finish_index)
  37. ).join
  38. end
  39. 1 def new_lines lines, finish_index
  40. 6 then: 1 if builder.unbuildable? lines
  41. 1 builder.comments
  42. else: 5 else
  43. 5 content lines[finish_index, lines.length]
  44. end
  45. end
  46. 1 def prepend(lines) = [content(lines), lines.join].compress.join("\n")
  47. 1 def content(lines) = builder.call lines
  48. end
  49. end