charming 0.1.3 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/lib/charming/application.rb +19 -2
  3. data/lib/charming/cli.rb +3 -3
  4. data/lib/charming/controller/component_dispatching.rb +47 -3
  5. data/lib/charming/controller/focus.rb +123 -0
  6. data/lib/charming/controller/focus_management.rb +1 -1
  7. data/lib/charming/controller/rendering.rb +4 -15
  8. data/lib/charming/controller/session_state.rb +11 -0
  9. data/lib/charming/controller.rb +11 -2
  10. data/lib/charming/database/commands.rb +106 -0
  11. data/lib/charming/generators/database_installer.rb +154 -0
  12. data/lib/charming/generators/model_generator.rb +2 -10
  13. data/lib/charming/generators/name.rb +1 -1
  14. data/lib/charming/generators/view_generator.rb +1 -1
  15. data/lib/charming/presentation/components/form/field.rb +1 -1
  16. data/lib/charming/presentation/components/markdown.rb +7 -7
  17. data/lib/charming/presentation/layout/pane.rb +7 -0
  18. data/lib/charming/presentation/layout/rect.rb +5 -0
  19. data/lib/charming/presentation/layout/screen_layout.rb +7 -0
  20. data/lib/charming/presentation/layout/split.rb +7 -0
  21. data/lib/charming/presentation/markdown/render_context.rb +28 -10
  22. data/lib/charming/presentation/markdown/renderer.rb +264 -39
  23. data/lib/charming/presentation/markdown/style_config.rb +215 -0
  24. data/lib/charming/presentation/markdown/syntax_highlighter.rb +3 -2
  25. data/lib/charming/presentation/markdown.rb +2 -2
  26. data/lib/charming/presentation/view.rb +7 -0
  27. data/lib/charming/router.rb +3 -8
  28. data/lib/charming/runtime.rb +2 -0
  29. data/lib/charming/version.rb +1 -1
  30. data/lib/charming.rb +2 -2
  31. metadata +42 -9
  32. data/lib/charming/database_commands.rb +0 -103
  33. data/lib/charming/database_installer.rb +0 -152
  34. data/lib/charming/focus.rb +0 -121
  35. data/lib/charming/presentation/markdown/block_renderers.rb +0 -118
  36. data/lib/charming/presentation/markdown/inline_renderers.rb +0 -66
@@ -0,0 +1,215 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Charming
4
+ module Markdown
5
+ class StyleConfig
6
+ ELEMENTS = %i[
7
+ document paragraph block_quote list heading h1 h2 h3 h4 h5 h6 text
8
+ strikethrough emph strong hr item enumeration task link link_text image
9
+ image_text code code_block table definition_list definition_term
10
+ definition_description html_block html_span
11
+ ].freeze
12
+
13
+ BUILT_INS = {
14
+ notty: {
15
+ block_quote: {indent: 1, indent_token: "| "},
16
+ list: {level_indent: 2},
17
+ h1: {prefix: "# "},
18
+ h2: {prefix: "## "},
19
+ h3: {prefix: "### "},
20
+ h4: {prefix: "#### "},
21
+ h5: {prefix: "##### "},
22
+ h6: {prefix: "###### "},
23
+ emph: {block_prefix: "*", block_suffix: "*"},
24
+ strong: {block_prefix: "**", block_suffix: "**"},
25
+ strikethrough: {block_prefix: "~~", block_suffix: "~~"},
26
+ hr: {format: "--------"},
27
+ item: {block_prefix: "- "},
28
+ enumeration: {block_prefix: ". "},
29
+ task: {ticked: "[x] ", unticked: "[ ] "},
30
+ code: {block_prefix: "`", block_suffix: "`"},
31
+ code_block: {margin: 1},
32
+ table: {column_separator: "|", row_separator: "-"},
33
+ image_text: {format: "Image: {{text}} ->"}
34
+ },
35
+ dark: {
36
+ document: {color: "252"},
37
+ block_quote: {color: "244", indent: 1, indent_token: "│ "},
38
+ list: {level_indent: 2},
39
+ heading: {color: "39", bold: true},
40
+ h1: {prefix: " ", suffix: " ", color: "228", background_color: "63", bold: true},
41
+ h2: {prefix: "## "},
42
+ h3: {prefix: "### "},
43
+ h4: {prefix: "#### "},
44
+ h5: {prefix: "##### "},
45
+ h6: {prefix: "###### ", color: "35", bold: false},
46
+ strikethrough: {crossed_out: true},
47
+ emph: {italic: true},
48
+ strong: {bold: true},
49
+ hr: {color: "240", format: "--------"},
50
+ item: {block_prefix: "• "},
51
+ enumeration: {block_prefix: ". "},
52
+ task: {ticked: "[✓] ", unticked: "[ ] "},
53
+ link: {color: "30", underline: true},
54
+ link_text: {color: "35", bold: true},
55
+ image: {color: "212", underline: true},
56
+ image_text: {color: "243", format: "Image: {{text}} ->"},
57
+ code: {prefix: " ", suffix: " ", color: "203", background_color: "236"},
58
+ code_block: {color: "244", margin: 1},
59
+ table: {column_separator: "|", row_separator: "-"}
60
+ },
61
+ light: {
62
+ document: {color: "236"},
63
+ block_quote: {color: "244", indent: 1, indent_token: "│ "},
64
+ list: {level_indent: 2},
65
+ heading: {color: "25", bold: true},
66
+ h1: {prefix: " ", suffix: " ", color: "255", background_color: "33", bold: true},
67
+ h2: {prefix: "## "},
68
+ h3: {prefix: "### "},
69
+ h4: {prefix: "#### "},
70
+ h5: {prefix: "##### "},
71
+ h6: {prefix: "###### ", color: "30", bold: false},
72
+ strikethrough: {crossed_out: true},
73
+ emph: {italic: true},
74
+ strong: {bold: true},
75
+ hr: {color: "250", format: "--------"},
76
+ item: {block_prefix: "• "},
77
+ enumeration: {block_prefix: ". "},
78
+ task: {ticked: "[✓] ", unticked: "[ ] "},
79
+ link: {color: "25", underline: true},
80
+ link_text: {color: "90", bold: true},
81
+ image: {color: "162", underline: true},
82
+ image_text: {color: "244", format: "Image: {{text}} ->"},
83
+ code: {prefix: " ", suffix: " ", color: "161", background_color: "255"},
84
+ code_block: {color: "244", margin: 1},
85
+ table: {column_separator: "|", row_separator: "-"}
86
+ }
87
+ }.freeze
88
+
89
+ ATTRIBUTES = %i[bold faint italic underline reverse strikethrough].freeze
90
+
91
+ Style = Data.define(
92
+ :block_prefix, :block_suffix, :prefix, :suffix, :color, :background_color,
93
+ :bold, :faint, :italic, :underline, :reverse, :strikethrough, :format,
94
+ :indent, :indent_token, :margin, :level_indent, :ticked, :unticked,
95
+ :column_separator, :row_separator
96
+ ) do
97
+ def self.from(value)
98
+ value = symbolize_keys(value || {})
99
+ new(
100
+ block_prefix: value[:block_prefix].to_s,
101
+ block_suffix: value[:block_suffix].to_s,
102
+ prefix: value[:prefix].to_s,
103
+ suffix: value[:suffix].to_s,
104
+ color: normalize_color(value[:color] || value[:foreground] || value[:fg]),
105
+ background_color: normalize_color(value[:background_color] || value[:background] || value[:bg]),
106
+ bold: value[:bold],
107
+ faint: value[:faint],
108
+ italic: value[:italic],
109
+ underline: value[:underline],
110
+ reverse: value[:reverse] || value[:inverse],
111
+ strikethrough: value[:strikethrough] || value[:crossed_out],
112
+ format: value[:format].to_s,
113
+ indent: value[:indent]&.to_i,
114
+ indent_token: value[:indent_token]&.to_s,
115
+ margin: value[:margin]&.to_i,
116
+ level_indent: value[:level_indent]&.to_i,
117
+ ticked: value[:ticked]&.to_s,
118
+ unticked: value[:unticked]&.to_s,
119
+ column_separator: value[:column_separator]&.to_s,
120
+ row_separator: value[:row_separator]&.to_s
121
+ )
122
+ end
123
+
124
+ def inherit_visual(child)
125
+ child = self.class.from(child) unless child.is_a?(self.class)
126
+ self.class.new(**child.to_h.merge(
127
+ color: child.color || color,
128
+ background_color: child.background_color || background_color,
129
+ bold: child.bold.nil? ? bold : child.bold,
130
+ faint: child.faint.nil? ? faint : child.faint,
131
+ italic: child.italic.nil? ? italic : child.italic,
132
+ underline: child.underline.nil? ? underline : child.underline,
133
+ reverse: child.reverse.nil? ? reverse : child.reverse,
134
+ strikethrough: child.strikethrough.nil? ? strikethrough : child.strikethrough
135
+ ))
136
+ end
137
+
138
+ def render(value)
139
+ ansi_codes.apply("#{block_prefix}#{prefix}#{value}#{suffix}#{block_suffix}")
140
+ end
141
+
142
+ def apply_block_layout(value)
143
+ lines = value.to_s.lines(chomp: true)
144
+ lines = [""] if lines.empty?
145
+
146
+ if indent&.positive?
147
+ indentation = (indent_token || " ") * indent
148
+ lines = lines.map { |line| "#{indentation}#{line}" }
149
+ end
150
+
151
+ rendered = lines.join("\n")
152
+ return rendered unless margin.to_i.positive?
153
+
154
+ blank = Array.new(margin.to_i, "").join("\n")
155
+ "#{blank}\n#{rendered}\n#{blank}"
156
+ end
157
+
158
+ private
159
+
160
+ def ansi_codes
161
+ UI::ANSICodes.new(
162
+ attributes: ATTRIBUTES.select { |attribute| public_send(attribute) },
163
+ foreground: color,
164
+ background: background_color
165
+ )
166
+ end
167
+
168
+ def self.symbolize_keys(value)
169
+ value.to_h.each_with_object({}) { |(key, item), result| result[key.to_sym] = item }
170
+ end
171
+
172
+ def self.normalize_color(value)
173
+ return if value.nil?
174
+ return value if value.is_a?(Integer)
175
+ return value.to_i if value.to_s.match?(/\A\d+\z/)
176
+
177
+ value
178
+ end
179
+ end
180
+
181
+ def self.builtin(name)
182
+ key = name.to_s.tr("-", "_").to_sym
183
+ raise ArgumentError, "unknown markdown style: #{name.inspect}" unless BUILT_INS.key?(key)
184
+
185
+ from_hash(BUILT_INS.fetch(key))
186
+ end
187
+
188
+ def self.from(value)
189
+ return value if value.is_a?(self)
190
+ return builtin(value) if value.is_a?(String) || value.is_a?(Symbol)
191
+
192
+ from_hash(value || BUILT_INS.fetch(:dark))
193
+ end
194
+
195
+ def self.from_hash(value)
196
+ new(value.to_h)
197
+ end
198
+
199
+ def initialize(styles = {})
200
+ styles = styles.transform_keys(&:to_sym)
201
+ @styles = ELEMENTS.each_with_object({}) do |element, result|
202
+ result[element] = Style.from(styles[element] || {})
203
+ end.freeze
204
+ end
205
+
206
+ def [](name)
207
+ @styles.fetch(name.to_sym) { Style.from({}) }
208
+ end
209
+
210
+ def heading(level)
211
+ self[:heading].inherit_visual(self[:"h#{level}"])
212
+ end
213
+ end
214
+ end
215
+ end
@@ -10,8 +10,9 @@ module Charming
10
10
  # base style (muted italic for comments, title for keywords, etc.).
11
11
  class SyntaxHighlighter
12
12
  # *theme* is the active Charming theme. Defaults to UI::Theme.default.
13
- def initialize(theme: UI::Theme.default)
13
+ def initialize(theme: UI::Theme.default, style: nil)
14
14
  @theme = theme || UI::Theme.default
15
+ @style = style
15
16
  end
16
17
 
17
18
  # Highlights *code* (using Rouge) for the given *language* (auto-detected when nil)
@@ -27,7 +28,7 @@ module Charming
27
28
  private
28
29
 
29
30
  # The Charming theme used for token styling.
30
- attr_reader :theme
31
+ attr_reader :theme, :style
31
32
 
32
33
  # Picks a Rouge lexer for *language* and *code*, falling back to plain text.
33
34
  def lexer_for(language, code)
@@ -2,8 +2,8 @@
2
2
 
3
3
  module Charming
4
4
  # Markdown is the namespace for the Markdown rendering pipeline. Parsing is delegated to
5
- # Kramdown; per-block and per-inline element rendering is handled by `BlockRenderer`
6
- # and `InlineRenderer`; code blocks are highlighted by `SyntaxHighlighter` (Rouge-backed).
5
+ # Commonmarker; `Renderer` renders the AST, and code blocks are highlighted by
6
+ # `SyntaxHighlighter` (Rouge-backed).
7
7
  module Markdown
8
8
  end
9
9
  end
@@ -81,6 +81,7 @@ module Charming
81
81
  def screen_layout(background: nil, &)
82
82
  layout = Layout::Builder.build(screen: layout_screen, view: self, background: background, &)
83
83
  register_layout_focus(layout)
84
+ register_layout_mouse_targets(layout)
84
85
  layout.render
85
86
  end
86
87
 
@@ -129,5 +130,11 @@ module Charming
129
130
 
130
131
  assigns[:controller].focus.define_layout(layout.focusable_names)
131
132
  end
133
+
134
+ def register_layout_mouse_targets(layout)
135
+ return unless assigns[:controller]
136
+
137
+ assigns[:controller].register_mouse_targets(layout.mouse_targets)
138
+ end
132
139
  end
133
140
  end
@@ -108,20 +108,15 @@ module Charming
108
108
  end
109
109
  end
110
110
 
111
- # Splits a camel-case string into words for title derivation (e.g., "my_route" → ["my", "route"]).
112
- def camelize(value)
113
- value.split("_").map(&:capitalize).join
114
- end
115
-
116
111
  # Looks up a constant by name in Object. Used to resolve controller strings from route definitions.
117
112
  def constantize(name)
118
- Object.const_get(name)
113
+ ActiveSupport::Inflector.constantize(name)
119
114
  end
120
115
 
121
116
  # Builds the full controller constant name, prepending the namespace if present.
122
- # For example: "HomeController" with namespace "Admin" → "Admin::HomeController".
117
+ # For example: "home" with namespace "Admin" → "Admin::HomeController".
123
118
  def controller_constant_name(controller_name)
124
- name = "#{camelize(controller_name)}Controller"
119
+ name = "#{ActiveSupport::Inflector.camelize(controller_name)}Controller"
125
120
  @namespace.to_s.empty? ? name : "#{@namespace}::#{name}"
126
121
  end
127
122
 
@@ -186,6 +186,7 @@ module Charming
186
186
  def setup_terminal
187
187
  @backend.enter_alt_screen
188
188
  @backend.hide_cursor
189
+ @backend.enable_mouse_tracking if @backend.respond_to?(:enable_mouse_tracking)
189
190
  @backend.install_resize_handler if @backend.respond_to?(:install_resize_handler)
190
191
  end
191
192
 
@@ -200,6 +201,7 @@ module Charming
200
201
  # the cursor, and leaves the alternative screen buffer.
201
202
  def restore_terminal
202
203
  @backend.restore_resize_handler if @backend.respond_to?(:restore_resize_handler)
204
+ @backend.disable_mouse_tracking if @backend.respond_to?(:disable_mouse_tracking)
203
205
  @backend.show_cursor
204
206
  @backend.leave_alt_screen
205
207
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Charming
4
- VERSION = "0.1.3"
4
+ VERSION = "0.1.4"
5
5
  end
data/lib/charming.rb CHANGED
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "active_support/inflector"
4
+ require "logger"
3
5
  require "zeitwerk"
4
6
 
5
7
  loader = Zeitwerk::Loader.for_gem
@@ -9,8 +11,6 @@ loader.inflector.inflect(
9
11
  "ansi_codes" => "ANSICodes",
10
12
  "ansi_slicer" => "ANSISlicer",
11
13
  "border_painter" => "BorderPainter",
12
- "block_renderers" => "BlockRenderer",
13
- "inline_renderers" => "InlineRenderer",
14
14
  "render_context" => "RenderContext",
15
15
  "erb_handler" => "ErbHandler",
16
16
  "key_normalizer" => "KeyNormalizer",
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: charming
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.3
4
+ version: 0.1.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - pando
@@ -50,19 +50,53 @@ dependencies:
50
50
  - !ruby/object:Gem::Version
51
51
  version: 8.1.2
52
52
  - !ruby/object:Gem::Dependency
53
- name: kramdown
53
+ name: activesupport
54
54
  requirement: !ruby/object:Gem::Requirement
55
55
  requirements:
56
56
  - - "~>"
57
57
  - !ruby/object:Gem::Version
58
- version: '2.5'
58
+ version: '8.1'
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: 8.1.2
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '8.1'
69
+ - - ">="
70
+ - !ruby/object:Gem::Version
71
+ version: 8.1.2
72
+ - !ruby/object:Gem::Dependency
73
+ name: commonmarker
74
+ requirement: !ruby/object:Gem::Requirement
75
+ requirements:
76
+ - - "~>"
77
+ - !ruby/object:Gem::Version
78
+ version: '2.0'
79
+ type: :runtime
80
+ prerelease: false
81
+ version_requirements: !ruby/object:Gem::Requirement
82
+ requirements:
83
+ - - "~>"
84
+ - !ruby/object:Gem::Version
85
+ version: '2.0'
86
+ - !ruby/object:Gem::Dependency
87
+ name: logger
88
+ requirement: !ruby/object:Gem::Requirement
89
+ requirements:
90
+ - - "~>"
91
+ - !ruby/object:Gem::Version
92
+ version: '1.7'
59
93
  type: :runtime
60
94
  prerelease: false
61
95
  version_requirements: !ruby/object:Gem::Requirement
62
96
  requirements:
63
97
  - - "~>"
64
98
  - !ruby/object:Gem::Version
65
- version: '2.5'
99
+ version: '1.7'
66
100
  - !ruby/object:Gem::Dependency
67
101
  name: rouge
68
102
  requirement: !ruby/object:Gem::Requirement
@@ -210,23 +244,23 @@ files:
210
244
  - lib/charming/controller/command_palette.rb
211
245
  - lib/charming/controller/component_dispatching.rb
212
246
  - lib/charming/controller/dispatching.rb
247
+ - lib/charming/controller/focus.rb
213
248
  - lib/charming/controller/focus_management.rb
214
249
  - lib/charming/controller/rendering.rb
215
250
  - lib/charming/controller/session_state.rb
216
251
  - lib/charming/controller/sidebar_navigation.rb
217
- - lib/charming/database_commands.rb
218
- - lib/charming/database_installer.rb
252
+ - lib/charming/database/commands.rb
219
253
  - lib/charming/events/key_event.rb
220
254
  - lib/charming/events/mouse_event.rb
221
255
  - lib/charming/events/resize_event.rb
222
256
  - lib/charming/events/task_event.rb
223
257
  - lib/charming/events/timer_event.rb
224
- - lib/charming/focus.rb
225
258
  - lib/charming/generators/app_file_generator.rb
226
259
  - lib/charming/generators/app_generator.rb
227
260
  - lib/charming/generators/base.rb
228
261
  - lib/charming/generators/component_generator.rb
229
262
  - lib/charming/generators/controller_generator.rb
263
+ - lib/charming/generators/database_installer.rb
230
264
  - lib/charming/generators/error.rb
231
265
  - lib/charming/generators/model_generator.rb
232
266
  - lib/charming/generators/name.rb
@@ -305,10 +339,9 @@ files:
305
339
  - lib/charming/presentation/layout/screen_layout.rb
306
340
  - lib/charming/presentation/layout/split.rb
307
341
  - lib/charming/presentation/markdown.rb
308
- - lib/charming/presentation/markdown/block_renderers.rb
309
- - lib/charming/presentation/markdown/inline_renderers.rb
310
342
  - lib/charming/presentation/markdown/render_context.rb
311
343
  - lib/charming/presentation/markdown/renderer.rb
344
+ - lib/charming/presentation/markdown/style_config.rb
312
345
  - lib/charming/presentation/markdown/syntax_highlighter.rb
313
346
  - lib/charming/presentation/template_view.rb
314
347
  - lib/charming/presentation/templates.rb
@@ -1,103 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "fileutils"
4
-
5
- module Charming
6
- # DatabaseCommands implements the runtime side of `charming db:COMMAND` (other than
7
- # `db:install`, which lives in DatabaseInstaller). It loads the app's `config/database.rb`,
8
- # delegates the actual work to ActiveRecord, and prints a short status line on success.
9
- class DatabaseCommands
10
- # *command* is the subcommand string (e.g., "db:create"). *out* is the status-output
11
- # stream. *destination* is the app root for resolving `config/database.rb` and `db/`.
12
- def initialize(command, out:, destination:)
13
- @command = command
14
- @out = out
15
- @destination = destination
16
- end
17
-
18
- # Dispatches the configured command. Raises Generators::Error for unknown commands.
19
- def run
20
- case command
21
- when "db:create" then create
22
- when "db:migrate" then migrate
23
- when "db:rollback" then rollback
24
- when "db:drop" then drop
25
- when "db:seed" then seed
26
- else raise Generators::Error, "Unknown database command: #{command}"
27
- end
28
- end
29
-
30
- private
31
-
32
- # The subcommand, output stream, and app destination.
33
- attr_reader :command, :out, :destination
34
-
35
- # Creates the SQLite database file (touch) and establishes the connection.
36
- def create
37
- load_database
38
- FileUtils.mkdir_p(File.dirname(database_path)) if database_path
39
- FileUtils.touch(database_path) if database_path
40
- ActiveRecord::Base.connection
41
- out.puts "create #{relative_database_path}"
42
- end
43
-
44
- # Runs all pending migrations from `db/migrate`.
45
- def migrate
46
- load_database
47
- migration_context.migrate
48
- out.puts "migrate db/migrate"
49
- end
50
-
51
- # Rolls back the most recent migration.
52
- def rollback
53
- load_database
54
- migration_context.rollback(1)
55
- out.puts "rollback db/migrate"
56
- end
57
-
58
- # Disconnects ActiveRecord, then deletes the database file.
59
- def drop
60
- load_database
61
- ActiveRecord::Base.connection.disconnect!
62
- File.delete(database_path) if database_path && File.exist?(database_path)
63
- out.puts "drop #{relative_database_path}"
64
- end
65
-
66
- # Loads `db/seeds.rb` (raises if missing).
67
- def seed
68
- load_database
69
- seed_path = File.join(destination, "db", "seeds.rb")
70
- raise Generators::Error, "Missing file: db/seeds.rb" unless File.exist?(seed_path)
71
-
72
- load seed_path
73
- out.puts "seed db/seeds.rb"
74
- end
75
-
76
- # Loads the app's `config/database.rb` (raises if missing) which establishes the connection.
77
- def load_database
78
- database_config = File.join(destination, "config", "database.rb")
79
- raise Generators::Error, "Database support is not configured. Missing config/database.rb." unless File.exist?(database_config)
80
-
81
- require database_config
82
- end
83
-
84
- # The ActiveRecord migration context rooted at `db/migrate` inside the app.
85
- def migration_context
86
- ActiveRecord::MigrationContext.new(File.join(destination, "db", "migrate"))
87
- end
88
-
89
- # The configured database file path (nil when ActiveRecord isn't connected to a file).
90
- def database_path
91
- ActiveRecord::Base.connection_db_config.database
92
- end
93
-
94
- # The database path relative to the app root, used for human-friendly status output.
95
- def relative_database_path
96
- return "database" unless database_path
97
-
98
- base = File.realpath(destination)
99
- path = File.expand_path(database_path)
100
- path.start_with?("#{base}/") ? path.delete_prefix("#{base}/") : path
101
- end
102
- end
103
- end