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%
)
-
# frozen_string_literal: true
-
-
1
require "zeitwerk"
-
-
1
Zeitwerk::Loader.new.then do |loader|
-
1
loader.inflector.inflect "cli" => "CLI"
-
1
loader.tag = File.basename __FILE__, ".rb"
-
1
loader.ignore "#{__dir__}/tocer/rake"
-
1
loader.push_dir __dir__
-
1
loader.setup
-
end
-
-
# Main namespace.
-
1
module Tocer
-
1
def self.loader registry = Zeitwerk::Registry
-
4
@loader ||= registry.loaders.each.find { |loader| loader.tag == File.basename(__FILE__, ".rb") }
-
end
-
end
-
# frozen_string_literal: true
-
-
1
Gem::Specification.new do |spec|
-
1
spec.name = "tocer"
-
1
spec.version = "19.5.0"
-
1
spec.authors = ["Brooke Kuhlmann"]
-
1
spec.email = ["brooke@alchemists.io"]
-
1
spec.homepage = "https://alchemists.io/projects/tocer"
-
1
spec.summary = "A command line interface for generating Markdown table of contents."
-
1
spec.license = "Hippocratic-2.1"
-
-
1
spec.metadata = {
-
"bug_tracker_uri" => "https://github.com/bkuhlmann/tocer/issues",
-
"changelog_uri" => "https://alchemists.io/projects/tocer/versions",
-
"homepage_uri" => "https://alchemists.io/projects/tocer",
-
"funding_uri" => "https://github.com/sponsors/bkuhlmann",
-
"label" => "Tocer",
-
"rubygems_mfa_required" => "true",
-
"source_code_uri" => "https://github.com/bkuhlmann/tocer"
-
}
-
-
1
spec.signing_key = Gem.default_key_path
-
1
spec.cert_chain = [Gem.default_cert_path]
-
-
1
spec.required_ruby_version = ">= 3.4"
-
1
spec.add_dependency "cogger", "~> 1.0"
-
1
spec.add_dependency "containable", "~> 1.1"
-
1
spec.add_dependency "core", "~> 2.5"
-
1
spec.add_dependency "dry-schema", "~> 1.13"
-
1
spec.add_dependency "etcher", "~> 3.0"
-
1
spec.add_dependency "infusible", "~> 4.0"
-
1
spec.add_dependency "refinements", "~> 13.6"
-
1
spec.add_dependency "runcom", "~> 12.0"
-
1
spec.add_dependency "sod", "~> 1.5"
-
1
spec.add_dependency "spek", "~> 4.0"
-
1
spec.add_dependency "zeitwerk", "~> 2.7"
-
-
1
spec.bindir = "exe"
-
1
spec.executables << "tocer"
-
1
spec.extra_rdoc_files = Dir["README*", "LICENSE*"]
-
1
spec.files = Dir["*.gemspec", "lib/**/*"]
-
end
-
# frozen_string_literal: true
-
-
1
require "forwardable"
-
-
1
module Tocer
-
# Builds table of contents for a Markdown document.
-
1
class Builder
-
1
extend Forwardable
-
1
include Dependencies[:settings]
-
-
1
CODE_BLOCK_PUNCTUATION = "```"
-
-
1
def_delegators :comment_block, :start_index, :finish_index, :comments, :prependable?
-
-
1
def initialize(
-
comment_block: Elements::CommentBlock.new,
-
transformer: Transformers::Finder.new,
-
**
-
)
-
33
@comment_block = comment_block
-
33
@transformer = transformer
-
33
@url_count = Hash.new 0
-
33
@code_block = false
-
33
super(**)
-
end
-
-
1
def unbuildable?(lines) = comment_block.empty?(lines) && headers(lines).empty?
-
-
1
def call lines
-
22
then: 6
else: 16
return "" if headers(lines).empty?
-
-
16
url_count.clear
-
16
assemble(lines).join
-
end
-
-
1
private
-
-
1
attr_reader :comment_block, :transformer, :url_count
-
1
attr_accessor :code_block
-
-
1
def assemble lines
-
[
-
16
"#{comment_block.start_tag}\n\n",
-
"#{settings.label}\n\n",
-
links(lines).join("\n"),
-
"\n\n#{comment_block.finish_tag}\n"
-
]
-
end
-
-
40
def links(lines) = headers(lines).map { |markdown| transform markdown }
-
-
1
def headers lines
-
44
lines.select do |line|
-
173
toggle_code_block line
-
173
line.start_with?(Parsers::Header::PUNCTUATION) && !code_block
-
end
-
end
-
-
1
def toggle_code_block line
-
173
else: 2
then: 171
return unless line.start_with? CODE_BLOCK_PUNCTUATION
-
-
2
self.code_block = !code_block
-
end
-
-
1
def transform markdown
-
39
transformer.call(markdown).then do |instance|
-
39
url = instance.url
-
39
link = instance.call url_suffix: url_suffix(url)
-
39
url_count[url] += 1
-
39
link
-
end
-
end
-
-
40
then: 37
else: 2
def url_suffix(url) = url_count[url].then { |count| count.zero? ? "" : count }
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "sod"
-
-
1
module Tocer
-
1
module CLI
-
1
module Actions
-
# Stores table of contents label.
-
1
class Label < Sod::Action
-
1
include Dependencies[:settings]
-
-
1
description "Set label."
-
-
1
on %w[-l --label], argument: "[TEXT]"
-
-
11
default { Container[:settings].label }
-
-
1
def call(label = default) = settings.label = label
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "sod"
-
-
1
module Tocer
-
1
module CLI
-
1
module Actions
-
# Stores table of contents file patterns.
-
1
class Pattern < Sod::Action
-
1
include Dependencies[:settings]
-
-
1
description "Set file patterns."
-
-
1
on %w[-p --patterns], argument: "[a,b,c]"
-
-
12
default { Container[:settings].patterns }
-
-
1
def call(patterns = default) = settings.patterns = Array(patterns)
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "sod"
-
-
1
module Tocer
-
1
module CLI
-
1
module Actions
-
# Stores table of contents root path.
-
1
class Root < Sod::Action
-
1
include Dependencies[:settings]
-
-
1
description "Set root directory."
-
-
1
on %w[-r --root], argument: "[PATH]"
-
-
11
default { Container[:settings].root_dir }
-
-
1
def call(path = default) = settings.root_dir = Pathname(path)
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "sod"
-
-
1
module Tocer
-
1
module CLI
-
1
module Commands
-
# Stores table of contents root path.
-
1
class Upsert < Sod::Command
-
1
handle "upsert"
-
-
1
description "Update/insert table of contents."
-
-
1
on Actions::Root
-
1
on Actions::Label
-
1
on Actions::Pattern
-
-
1
def initialize(runner: Runner.new, **)
-
8
@runner = runner
-
8
super(**)
-
end
-
-
1
def call = runner.call
-
-
1
private
-
-
1
attr_reader :runner
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "sod"
-
-
1
module Tocer
-
1
module CLI
-
# The main Command Line Interface (CLI) object.
-
1
class Shell
-
1
include Dependencies[:defaults_path, :xdg_config, :specification]
-
-
1
def initialize(context: Sod::Context, dsl: Sod, **)
-
6
super(**)
-
6
@context = context
-
6
@dsl = dsl
-
end
-
-
1
def call(...) = cli.call(...)
-
-
1
private
-
-
1
attr_reader :context, :dsl
-
-
1
def cli
-
6
context = build_context
-
-
6
dsl.new :tocer, banner: specification.banner do
-
6
on(Sod::Prefabs::Commands::Config, context:)
-
6
on Commands::Upsert
-
6
on(Sod::Prefabs::Actions::Version, context:)
-
6
on Sod::Prefabs::Actions::Help, self
-
end
-
end
-
-
1
def build_context
-
6
context[defaults_path:, xdg_config:, version_label: specification.labeled_version]
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "dry/schema"
-
1
require "etcher"
-
-
1
Dry::Schema.load_extensions :monads
-
-
1
module Tocer
-
1
module Configuration
-
1
Contract = Dry::Schema.Params do
-
1
required(:label).filled :string
-
1
required(:patterns).array :string
-
1
required(:root_dir).filled Etcher::Types::Pathname
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Tocer
-
1
module Configuration
-
# Defines the content of the configuration for use throughout the gem.
-
1
Model = Struct.new :label, :root_dir, :patterns do
-
1
def initialize(**)
-
51
super
-
51
self[:patterns] = Array patterns
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "cogger"
-
1
require "containable"
-
1
require "etcher"
-
1
require "runcom"
-
1
require "spek"
-
-
1
module Tocer
-
# Provides a global gem container for injection into other objects.
-
1
module Container
-
1
extend Containable
-
-
1
register :registry, as: :fresh do
-
50
Etcher::Registry.new(contract: Configuration::Contract, model: Configuration::Model)
-
.add_loader(:yaml, self[:defaults_path])
-
.add_loader(:yaml, self[:xdg_config].active)
-
end
-
-
2
register(:settings) { Etcher.call(self[:registry]).dup }
-
2
register(:defaults_path) { Pathname(__dir__).join("configuration/defaults.yml") }
-
2
register(:xdg_config) { Runcom::Config.new "tocer/configuration.yml" }
-
7
register(:specification) { Spek::Loader.call "#{__dir__}/../../tocer.gemspec" }
-
1
register(:logger) { Cogger.new id: :tocer }
-
1
register :io, STDOUT
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "infusible"
-
-
1
module Tocer
-
1
Dependencies = Infusible[Container]
-
end
-
# frozen_string_literal: true
-
-
1
module Tocer
-
1
module Elements
-
# Represents a table of contents start and finish comment block.
-
1
class CommentBlock
-
1
def self.index lines, id
-
265
lines.index { |line| line =~ /<!--.*#{Regexp.escape id}.*-->/ }
-
.to_i
-
end
-
-
1
def initialize start_id: "Tocer[start]",
-
finish_id: "Tocer[finish]",
-
message: "Auto-generated, don't remove."
-
47
@start_id = start_id
-
47
@finish_id = finish_id
-
47
@message = message
-
end
-
-
1
def comments = "#{start_tag}\n#{finish_tag}\n"
-
-
1
def start_index(lines) = self.class.index(lines, start_id)
-
-
1
def start_tag = comment(start_id, message)
-
-
1
def finish_index(lines) = self.class.index(lines, finish_id)
-
-
1
def finish_tag = comment(finish_id, message)
-
-
11
def empty?(lines) = (finish_index(lines) - start_index(lines)) == 1
-
-
1
def prependable?(lines) = start_index(lines).zero? && finish_index(lines).zero?
-
-
1
private
-
-
1
attr_reader :start_id, :finish_id, :message
-
-
1
def comment(id, message) = "<!-- #{id}: #{message} -->"
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Tocer
-
1
module Parsers
-
# Represents a Markdown header.
-
1
class Header
-
1
PUNCTUATION = "#"
-
-
1
def initialize markdown
-
70
@markdown = markdown
-
end
-
-
1
def prefix = String(markdown[/#{PUNCTUATION}{1,}/o])
-
-
1
def content = markdown[prefix.length + 1, markdown.length].strip
-
-
1
private
-
-
1
attr_reader :markdown
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "rake"
-
1
require "refinements/struct"
-
1
require "tocer"
-
-
1
module Tocer
-
1
module Rake
-
# Registers Rake tasks for use.
-
1
class Register
-
1
include ::Rake::DSL
-
1
include Dependencies[:settings]
-
-
1
using Refinements::Struct
-
-
1
def self.call = new.call
-
-
1
def initialize(runner: Runner.new, **)
-
4
@runner = runner
-
4
super(**)
-
end
-
-
1
def call
-
4
desc "Update/Insert Table of Contents"
-
4
task :toc, %i[label patterns] do |_task, arguments|
-
3
settings.with! arguments
-
3
runner.call
-
end
-
end
-
-
1
private
-
-
1
attr_reader :runner
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "refinements/pathname"
-
-
1
module Tocer
-
# Generates/updates Table of Contents for files in root path.
-
1
class Runner
-
1
include Dependencies[:settings, :io]
-
-
1
using Refinements::Pathname
-
-
1
def initialize(writer: Writer.new, **)
-
16
super(**)
-
16
@writer = writer
-
end
-
-
1
def call
-
12
settings.root_dir
-
.files(%({#{settings.patterns.join ","}}))
-
.each do |path|
-
7
io.puts " #{path}"
-
7
writer.call path
-
end
-
end
-
-
1
private
-
-
1
attr_reader :writer
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Tocer
-
1
module Transformers
-
# Finds appropriate header transformer for matching pattern.
-
1
class Finder
-
1
TRANSFORMERS = {/\[.+\]\(.+\)/ => Transformers::Link, /.*/ => Transformers::Text}.freeze
-
-
1
def initialize transformers: TRANSFORMERS
-
35
@transformers = transformers
-
end
-
-
1
def call markdown
-
41
transformers.find do |pattern, transformer|
-
78
then: 41
else: 37
break transformer.new markdown if pattern.match? markdown
-
end
-
end
-
-
1
private
-
-
1
attr_reader :transformers
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "core"
-
1
require "refinements/array"
-
-
1
module Tocer
-
1
module Transformers
-
# Transforms a Markdown header (embedded link) into a table of contents link.
-
1
class Link
-
1
using Refinements::Array
-
-
1
def initialize text, parser: Parsers::Header
-
17
@parser = parser.new text
-
end
-
-
1
def label = parser.content.gsub(embedded_link, embedded_link_label)
-
-
1
def url = label.downcase.gsub(/\s/, "-").gsub(/[^\w-]+/, "")
-
-
1
def call(url_suffix: "") = "#{indented_bullet}[#{label}](##{computed_url url_suffix})"
-
-
1
private
-
-
1
attr_reader :parser
-
-
1
def computed_url(suffix = Core::EMPTY_STRING) = [url, suffix.to_s].compress.join("-")
-
-
1
def embedded_link = "[#{embedded_link_label}](#{embedded_link_url})"
-
-
1
def embedded_link_label = parser.content[/\[(.*)\]/, 1]
-
-
1
def embedded_link_url = parser.content[/\((.*)\)/, 1]
-
-
1
def indented_bullet = prefix_to_spaces.gsub(/\s{2}$/, "- ")
-
-
1
def prefix_to_spaces = Array.new(parser.prefix.length, " ").join
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "core"
-
1
require "refinements/array"
-
-
1
module Tocer
-
1
module Transformers
-
# Transforms a Markdown header (plain text) into a table of contents link.
-
1
class Text
-
1
using Refinements::Array
-
-
1
def initialize text, parser: Parsers::Header
-
48
@parser = parser.new text
-
end
-
-
1
def label = parser.content
-
-
1
def url = label.downcase.gsub(/\s/, "-").gsub(/[^\w-]+/, "")
-
-
1
def call(url_suffix: "") = "#{indented_bullet}[#{label}](##{computed_url url_suffix})"
-
-
1
private
-
-
1
attr_reader :parser
-
-
1
def computed_url(suffix = Core::EMPTY_STRING) = [url, suffix.to_s].compress.join("-")
-
-
1
def indented_bullet = prefix_to_spaces.gsub(/\s{2}$/, "- ")
-
-
1
def prefix_to_spaces = Array.new(parser.prefix.length, " ").join
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "refinements/array"
-
1
require "refinements/pathname"
-
-
1
module Tocer
-
# Writes table of contents to a Markdown document.
-
# :reek:DataClump
-
1
class Writer
-
1
using Refinements::Array
-
1
using Refinements::Pathname
-
-
1
def self.add start_index:, old_lines:, new_lines:
-
9
then: 3
else: 6
computed_new_lines = start_index.zero? ? new_lines : new_lines + "\n"
-
9
old_lines.insert start_index, *computed_new_lines
-
end
-
-
1
def self.remove start_index, finish_index, lines
-
9
range = (start_index - 1)..finish_index
-
84
lines.reject.with_index { |_, index| range.include? index }
-
end
-
-
1
def initialize builder: Builder.new
-
24
@builder = builder
-
end
-
-
1
def call path
-
15
path.rewrite do |body|
-
15
lines = body.each_line.to_a
-
15
then: 9
else: 6
builder.prependable?(lines) ? prepend(lines) : replace(lines)
-
end
-
end
-
-
1
private
-
-
1
attr_reader :builder
-
-
1
def replace lines
-
6
start_index = builder.start_index lines
-
6
finish_index = builder.finish_index lines
-
6
klass = self.class
-
-
6
klass.add(
-
start_index:,
-
old_lines: klass.remove(start_index, finish_index, lines),
-
new_lines: new_lines(lines, finish_index)
-
).join
-
end
-
-
1
def new_lines lines, finish_index
-
6
then: 1
if builder.unbuildable? lines
-
1
builder.comments
-
else: 5
else
-
5
content lines[finish_index, lines.length]
-
end
-
end
-
-
1
def prepend(lines) = [content(lines), lines.join].compress.join("\n")
-
-
1
def content(lines) = builder.call lines
-
end
-
end