ratatui_ruby 1.2.0 → 1.2.2

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 (260) hide show
  1. checksums.yaml +4 -4
  2. data/ext/ratatui_ruby/Cargo.lock +2 -1
  3. data/ext/ratatui_ruby/Cargo.toml +2 -1
  4. data/ext/ratatui_ruby/src/events.rs +157 -18
  5. data/lib/ratatui_ruby/version.rb +1 -1
  6. metadata +1 -255
  7. data/.builds/ruby-3.2.yml +0 -54
  8. data/.builds/ruby-3.3.yml +0 -54
  9. data/.builds/ruby-3.4.yml +0 -54
  10. data/.builds/ruby-4.0.0.yml +0 -54
  11. data/.pre-commit-config.yaml +0 -16
  12. data/.rubocop.yml +0 -10
  13. data/AGENTS.md +0 -147
  14. data/CHANGELOG.md +0 -751
  15. data/README.md +0 -187
  16. data/README.rdoc +0 -302
  17. data/Rakefile +0 -11
  18. data/Steepfile +0 -50
  19. data/doc/concepts/application_architecture.md +0 -321
  20. data/doc/concepts/application_testing.md +0 -193
  21. data/doc/concepts/async.md +0 -190
  22. data/doc/concepts/custom_widgets.md +0 -247
  23. data/doc/concepts/debugging.md +0 -401
  24. data/doc/concepts/event_handling.md +0 -162
  25. data/doc/concepts/interactive_design.md +0 -146
  26. data/doc/contributors/auditing/parity.md +0 -239
  27. data/doc/contributors/design/ruby_frontend.md +0 -448
  28. data/doc/contributors/design/rust_backend.md +0 -434
  29. data/doc/contributors/design.md +0 -11
  30. data/doc/contributors/developing_examples.md +0 -400
  31. data/doc/contributors/documentation_style.md +0 -121
  32. data/doc/contributors/index.md +0 -21
  33. data/doc/contributors/releasing.md +0 -215
  34. data/doc/contributors/todo/align/api_completeness_audit-finished.md +0 -381
  35. data/doc/contributors/todo/align/api_completeness_audit-unfinished.md +0 -200
  36. data/doc/contributors/todo/align/term.md +0 -351
  37. data/doc/contributors/todo/align/terminal.md +0 -647
  38. data/doc/contributors/todo/future_work.md +0 -169
  39. data/doc/contributors/upstream_requests/paragraph_span_rects.md +0 -259
  40. data/doc/contributors/upstream_requests/tab_rects.md +0 -173
  41. data/doc/contributors/upstream_requests/title_rects.md +0 -132
  42. data/doc/custom.css +0 -22
  43. data/doc/getting_started/quickstart.md +0 -291
  44. data/doc/getting_started/why.md +0 -93
  45. data/doc/images/app_all_events.png +0 -0
  46. data/doc/images/app_cli_rich_moments.gif +0 -0
  47. data/doc/images/app_color_picker.png +0 -0
  48. data/doc/images/app_debugging_showcase.gif +0 -0
  49. data/doc/images/app_debugging_showcase.png +0 -0
  50. data/doc/images/app_external_editor.gif +0 -0
  51. data/doc/images/app_login_form.png +0 -0
  52. data/doc/images/app_stateful_interaction.png +0 -0
  53. data/doc/images/verify_quickstart_dsl.png +0 -0
  54. data/doc/images/verify_quickstart_layout.png +0 -0
  55. data/doc/images/verify_quickstart_lifecycle.png +0 -0
  56. data/doc/images/verify_readme_usage.png +0 -0
  57. data/doc/images/widget_barchart.png +0 -0
  58. data/doc/images/widget_block.png +0 -0
  59. data/doc/images/widget_box.png +0 -0
  60. data/doc/images/widget_calendar.png +0 -0
  61. data/doc/images/widget_canvas.png +0 -0
  62. data/doc/images/widget_cell.png +0 -0
  63. data/doc/images/widget_center.png +0 -0
  64. data/doc/images/widget_chart.png +0 -0
  65. data/doc/images/widget_gauge.png +0 -0
  66. data/doc/images/widget_layout_split.png +0 -0
  67. data/doc/images/widget_line_gauge.png +0 -0
  68. data/doc/images/widget_list.png +0 -0
  69. data/doc/images/widget_map.png +0 -0
  70. data/doc/images/widget_overlay.png +0 -0
  71. data/doc/images/widget_popup.png +0 -0
  72. data/doc/images/widget_ratatui_logo.png +0 -0
  73. data/doc/images/widget_ratatui_mascot.png +0 -0
  74. data/doc/images/widget_rect.png +0 -0
  75. data/doc/images/widget_render.png +0 -0
  76. data/doc/images/widget_rich_text.png +0 -0
  77. data/doc/images/widget_scroll_text.png +0 -0
  78. data/doc/images/widget_scrollbar.png +0 -0
  79. data/doc/images/widget_sparkline.png +0 -0
  80. data/doc/images/widget_style_colors.png +0 -0
  81. data/doc/images/widget_table.png +0 -0
  82. data/doc/images/widget_tabs.png +0 -0
  83. data/doc/images/widget_text_width.png +0 -0
  84. data/doc/index.md +0 -34
  85. data/doc/troubleshooting/async.md +0 -4
  86. data/doc/troubleshooting/terminal_limitations.md +0 -131
  87. data/doc/troubleshooting/tui_output.md +0 -197
  88. data/examples/app_all_events/README.md +0 -114
  89. data/examples/app_all_events/app.rb +0 -98
  90. data/examples/app_all_events/model/app_model.rb +0 -159
  91. data/examples/app_all_events/model/event_color_cycle.rb +0 -43
  92. data/examples/app_all_events/model/event_entry.rb +0 -94
  93. data/examples/app_all_events/model/msg.rb +0 -39
  94. data/examples/app_all_events/model/timestamp.rb +0 -56
  95. data/examples/app_all_events/update.rb +0 -75
  96. data/examples/app_all_events/view/app_view.rb +0 -80
  97. data/examples/app_all_events/view/controls_view.rb +0 -54
  98. data/examples/app_all_events/view/counts_view.rb +0 -61
  99. data/examples/app_all_events/view/live_view.rb +0 -72
  100. data/examples/app_all_events/view/log_view.rb +0 -57
  101. data/examples/app_all_events/view.rb +0 -9
  102. data/examples/app_cli_rich_moments/README.md +0 -81
  103. data/examples/app_cli_rich_moments/app.rb +0 -189
  104. data/examples/app_color_picker/README.md +0 -156
  105. data/examples/app_color_picker/app.rb +0 -76
  106. data/examples/app_color_picker/clipboard.rb +0 -86
  107. data/examples/app_color_picker/color.rb +0 -193
  108. data/examples/app_color_picker/controls.rb +0 -92
  109. data/examples/app_color_picker/copy_dialog.rb +0 -168
  110. data/examples/app_color_picker/export_pane.rb +0 -128
  111. data/examples/app_color_picker/harmony.rb +0 -58
  112. data/examples/app_color_picker/input.rb +0 -176
  113. data/examples/app_color_picker/main_container.rb +0 -180
  114. data/examples/app_color_picker/palette.rb +0 -111
  115. data/examples/app_debugging_showcase/README.md +0 -119
  116. data/examples/app_debugging_showcase/app.rb +0 -318
  117. data/examples/app_external_editor/README.md +0 -62
  118. data/examples/app_external_editor/app.rb +0 -344
  119. data/examples/app_login_form/README.md +0 -58
  120. data/examples/app_login_form/app.rb +0 -109
  121. data/examples/app_stateful_interaction/README.md +0 -35
  122. data/examples/app_stateful_interaction/app.rb +0 -328
  123. data/examples/timeout_demo.rb +0 -45
  124. data/examples/verify_quickstart_dsl/README.md +0 -55
  125. data/examples/verify_quickstart_dsl/app.rb +0 -49
  126. data/examples/verify_quickstart_layout/README.md +0 -77
  127. data/examples/verify_quickstart_layout/app.rb +0 -73
  128. data/examples/verify_quickstart_lifecycle/README.md +0 -68
  129. data/examples/verify_quickstart_lifecycle/app.rb +0 -62
  130. data/examples/verify_readme_usage/README.md +0 -49
  131. data/examples/verify_readme_usage/app.rb +0 -42
  132. data/examples/verify_website_managed/README.md +0 -48
  133. data/examples/verify_website_managed/app.rb +0 -36
  134. data/examples/verify_website_menu/README.md +0 -60
  135. data/examples/verify_website_menu/app.rb +0 -84
  136. data/examples/verify_website_spinner/README.md +0 -44
  137. data/examples/verify_website_spinner/app.rb +0 -34
  138. data/examples/widget_barchart/README.md +0 -58
  139. data/examples/widget_barchart/app.rb +0 -240
  140. data/examples/widget_block/README.md +0 -44
  141. data/examples/widget_block/app.rb +0 -258
  142. data/examples/widget_box/README.md +0 -54
  143. data/examples/widget_box/app.rb +0 -255
  144. data/examples/widget_calendar/README.md +0 -48
  145. data/examples/widget_calendar/app.rb +0 -115
  146. data/examples/widget_canvas/README.md +0 -31
  147. data/examples/widget_canvas/app.rb +0 -130
  148. data/examples/widget_cell/README.md +0 -45
  149. data/examples/widget_cell/app.rb +0 -112
  150. data/examples/widget_center/README.md +0 -33
  151. data/examples/widget_center/app.rb +0 -118
  152. data/examples/widget_chart/README.md +0 -50
  153. data/examples/widget_chart/app.rb +0 -220
  154. data/examples/widget_gauge/README.md +0 -50
  155. data/examples/widget_gauge/app.rb +0 -229
  156. data/examples/widget_layout_split/README.md +0 -53
  157. data/examples/widget_layout_split/app.rb +0 -260
  158. data/examples/widget_line_gauge/README.md +0 -50
  159. data/examples/widget_line_gauge/app.rb +0 -219
  160. data/examples/widget_list/README.md +0 -58
  161. data/examples/widget_list/app.rb +0 -382
  162. data/examples/widget_map/README.md +0 -48
  163. data/examples/widget_map/app.rb +0 -95
  164. data/examples/widget_overlay/README.md +0 -45
  165. data/examples/widget_overlay/app.rb +0 -250
  166. data/examples/widget_popup/README.md +0 -45
  167. data/examples/widget_popup/app.rb +0 -106
  168. data/examples/widget_ratatui_logo/README.md +0 -43
  169. data/examples/widget_ratatui_logo/app.rb +0 -104
  170. data/examples/widget_ratatui_mascot/README.md +0 -43
  171. data/examples/widget_ratatui_mascot/app.rb +0 -95
  172. data/examples/widget_rect/README.md +0 -53
  173. data/examples/widget_rect/app.rb +0 -222
  174. data/examples/widget_render/README.md +0 -46
  175. data/examples/widget_render/app.rb +0 -186
  176. data/examples/widget_render/app.rbs +0 -41
  177. data/examples/widget_rich_text/README.md +0 -44
  178. data/examples/widget_rich_text/app.rb +0 -193
  179. data/examples/widget_scroll_text/README.md +0 -46
  180. data/examples/widget_scroll_text/app.rb +0 -109
  181. data/examples/widget_scrollbar/README.md +0 -46
  182. data/examples/widget_scrollbar/app.rb +0 -155
  183. data/examples/widget_sparkline/README.md +0 -51
  184. data/examples/widget_sparkline/app.rb +0 -277
  185. data/examples/widget_style_colors/README.md +0 -43
  186. data/examples/widget_style_colors/app.rb +0 -83
  187. data/examples/widget_table/README.md +0 -57
  188. data/examples/widget_table/app.rb +0 -285
  189. data/examples/widget_tabs/README.md +0 -50
  190. data/examples/widget_tabs/app.rb +0 -183
  191. data/examples/widget_text_width/README.md +0 -44
  192. data/examples/widget_text_width/app.rb +0 -117
  193. data/migrate_to_buffer.rb +0 -145
  194. data/mise.toml +0 -8
  195. data/tasks/autodoc/examples.rb +0 -87
  196. data/tasks/autodoc/member.rb +0 -58
  197. data/tasks/autodoc/name.rb +0 -21
  198. data/tasks/autodoc.rake +0 -21
  199. data/tasks/bump/bump_workflow.rb +0 -49
  200. data/tasks/bump/cargo_lockfile.rb +0 -21
  201. data/tasks/bump/changelog.rb +0 -104
  202. data/tasks/bump/header.rb +0 -32
  203. data/tasks/bump/history.rb +0 -32
  204. data/tasks/bump/links.rb +0 -69
  205. data/tasks/bump/manifest.rb +0 -33
  206. data/tasks/bump/patch_release.rb +0 -19
  207. data/tasks/bump/release_branch.rb +0 -17
  208. data/tasks/bump/release_from_trunk.rb +0 -49
  209. data/tasks/bump/repository.rb +0 -54
  210. data/tasks/bump/ruby_gem.rb +0 -29
  211. data/tasks/bump/sem_ver.rb +0 -44
  212. data/tasks/bump/unreleased_section.rb +0 -73
  213. data/tasks/bump.rake +0 -61
  214. data/tasks/doc/documentation.rb +0 -59
  215. data/tasks/doc/link/file_url.rb +0 -30
  216. data/tasks/doc/link/relative_path.rb +0 -61
  217. data/tasks/doc/link/web_url.rb +0 -55
  218. data/tasks/doc/link.rb +0 -52
  219. data/tasks/doc/link_audit.rb +0 -116
  220. data/tasks/doc/problem.rb +0 -40
  221. data/tasks/doc/source_file.rb +0 -93
  222. data/tasks/doc.rake +0 -905
  223. data/tasks/example_viewer.html.erb +0 -172
  224. data/tasks/extension.rake +0 -14
  225. data/tasks/license/headers_md.rb +0 -223
  226. data/tasks/license/headers_rb.rb +0 -210
  227. data/tasks/license/license_utils.rb +0 -130
  228. data/tasks/license/snippets_md.rb +0 -315
  229. data/tasks/license/snippets_rdoc.rb +0 -150
  230. data/tasks/license.rake +0 -91
  231. data/tasks/lint.rake +0 -170
  232. data/tasks/rbs_predicates/predicate_catalog.rb +0 -52
  233. data/tasks/rbs_predicates/predicate_tests.rb +0 -124
  234. data/tasks/rbs_predicates/rbs_signature.rb +0 -63
  235. data/tasks/rbs_predicates.rake +0 -31
  236. data/tasks/rdoc_config.rb +0 -29
  237. data/tasks/resources/build.yml.erb +0 -60
  238. data/tasks/resources/index.html.erb +0 -141
  239. data/tasks/resources/rubies.yml +0 -7
  240. data/tasks/sourcehut.rake +0 -122
  241. data/tasks/steep.rake +0 -11
  242. data/tasks/terminal_preview/app_screenshot.rb +0 -45
  243. data/tasks/terminal_preview/crash_report.rb +0 -54
  244. data/tasks/terminal_preview/example_app.rb +0 -27
  245. data/tasks/terminal_preview/launcher_script.rb +0 -48
  246. data/tasks/terminal_preview/preview_collection.rb +0 -60
  247. data/tasks/terminal_preview/preview_timing.rb +0 -24
  248. data/tasks/terminal_preview/safety_confirmation.rb +0 -58
  249. data/tasks/terminal_preview/saved_screenshot.rb +0 -56
  250. data/tasks/terminal_preview/system_appearance.rb +0 -13
  251. data/tasks/terminal_preview/terminal_window.rb +0 -138
  252. data/tasks/terminal_preview/window_id.rb +0 -16
  253. data/tasks/terminal_preview.rake +0 -30
  254. data/tasks/test.rake +0 -36
  255. data/tasks/website/index_page.rb +0 -30
  256. data/tasks/website/version.rb +0 -122
  257. data/tasks/website/version_menu.rb +0 -68
  258. data/tasks/website/versioned_documentation.rb +0 -83
  259. data/tasks/website/website.rb +0 -53
  260. data/tasks/website.rake +0 -28
@@ -1,130 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- #--
4
- # SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
5
- # SPDX-License-Identifier: AGPL-3.0-or-later
6
- #++
7
-
8
- # Shared utility for detecting contributors from git blame and Co-Authored-By trailers.
9
- #
10
- # This module provides methods to:
11
- # - Get all contributors (authors and co-authors) who touched specific lines
12
- # - Track the latest year each contributor touched those lines
13
- # - Parse Co-Authored-By trailers from commit messages
14
-
15
- require "open3"
16
- require "date"
17
-
18
- module LicenseUtils
19
- # Represents a contributor with their latest year of contribution
20
- Contributor = Data.define(:name, :email, :year)
21
-
22
- class << self
23
- # Get all contributors who touched lines in a file (or range of lines).
24
- # Returns a Hash of { "Name <email>" => year } mapping each contributor to their latest year.
25
- #
26
- # This considers both the commit author AND any Co-Authored-By trailers in commit messages.
27
- def get_contributors_for_lines(filepath, start_line = nil, end_line = nil)
28
- blame_cmd = if start_line && end_line
29
- %W[git blame -L #{start_line},#{end_line} --porcelain -- #{filepath}]
30
- else
31
- %W[git blame --porcelain -- #{filepath}]
32
- end
33
-
34
- output, _status = Open3.capture2(*blame_cmd)
35
-
36
- contributors = {} # "Name <email>" => year
37
- commit_cache = {} # commit_hash => { year:, author:, co_authors: [] }
38
-
39
- current_commit = nil
40
-
41
- output.each_line do |line|
42
- if line =~ /^([a-f0-9]{40})/
43
- current_commit = $1
44
- elsif line =~ /^author (.+)$/
45
- commit_cache[current_commit] ||= {}
46
- commit_cache[current_commit][:author_name] = $1
47
- elsif line =~ /^author-mail <(.+)>$/
48
- commit_cache[current_commit] ||= {}
49
- commit_cache[current_commit][:author_email] = $1
50
- elsif line =~ /^author-time (\d+)$/
51
- commit_cache[current_commit] ||= {}
52
- timestamp = $1.to_i
53
- commit_cache[current_commit][:year] = Time.at(timestamp).year
54
- end
55
- end
56
-
57
- # Now fetch co-authors for each unique commit
58
- commit_cache.each do |commit_hash, data|
59
- next if commit_hash == "0" * 40 # Skip uncommitted lines
60
-
61
- # Get commit message for Co-Authored-By parsing
62
- msg_output, _status = Open3.capture2("git", "log", "-1", "--format=%B", commit_hash)
63
- co_authors = parse_co_authors(msg_output)
64
- data[:co_authors] = co_authors
65
-
66
- # Add author
67
- if data[:author_name] && data[:author_email]
68
- key = "#{data[:author_name]} <#{data[:author_email]}>"
69
- year = data[:year] || Date.today.year
70
- contributors[key] = [contributors[key] || 0, year].max
71
- end
72
-
73
- # Add co-authors with same year as commit
74
- co_authors.each do |ca|
75
- key = "#{ca[:name]} <#{ca[:email]}>"
76
- year = data[:year] || Date.today.year
77
- contributors[key] = [contributors[key] || 0, year].max
78
- end
79
- end
80
-
81
- contributors
82
- end
83
-
84
- # Get YOUR latest year contribution to the file/lines.
85
- # your_identifiers should be an array of strings that identify you (name, email fragments).
86
- def get_your_latest_year(filepath, your_identifiers, start_line = nil, end_line = nil)
87
- contributors = get_contributors_for_lines(filepath, start_line, end_line)
88
-
89
- your_year = nil
90
- contributors.each do |contributor, year|
91
- if your_identifiers.any? { |id| contributor.include?(id) }
92
- your_year = [your_year || 0, year].max
93
- end
94
- end
95
-
96
- your_year || Date.today.year
97
- end
98
-
99
- # Get all contributors EXCEPT you, with their latest years.
100
- # Returns array of { name:, email:, year: }
101
- def get_other_contributors(filepath, your_identifiers, start_line = nil, end_line = nil)
102
- contributors = get_contributors_for_lines(filepath, start_line, end_line)
103
-
104
- others = []
105
- contributors.each do |contributor, year|
106
- next if your_identifiers.any? { |id| contributor.include?(id) }
107
-
108
- # Parse "Name <email>" format
109
- if contributor =~ /^(.+?)\s*<(.+)>$/
110
- others << { name: $1.strip, email: $2.strip, year: }
111
- end
112
- end
113
-
114
- others
115
- end
116
-
117
- private def parse_co_authors(message)
118
- co_authors = []
119
-
120
- message.each_line do |line|
121
- # Match "Co-Authored-By: Name <email>" (case insensitive)
122
- if line =~ /^Co-Authored-By:\s*(.+?)\s*<(.+?)>\s*$/i
123
- co_authors << { name: $1.strip, email: $2.strip }
124
- end
125
- end
126
-
127
- co_authors
128
- end
129
- end
130
- end
@@ -1,315 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- #--
4
- # SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
5
- # SPDX-License-Identifier: AGPL-3.0-or-later
6
- #++
7
-
8
- # Script to add SPDX snippet headers to fenced code blocks in markdown files.
9
- #
10
- # Usage: ruby tasks/license/snippets_md.rb [path...]
11
- #
12
- # If no paths are given, processes all .md files via git ls-files.
13
- #
14
- # Rules:
15
- # - Wraps all fenced code blocks (``` or ````) with SPDX snippet headers (MIT-0)
16
- # - For SYNC:START/SYNC:END blocks, wraps AROUND the sync markers (not inside)
17
- # - Uses git blame to determine the latest edit year for the code lines
18
- # - Skips blocks that are already properly wrapped with MIT-0 and Kerrick Long
19
- # - Removes malformed existing SPDX-Snippet blocks and replaces with correct ones
20
-
21
- require "open3"
22
- require "date"
23
-
24
- COPYRIGHT_HOLDER = "Kerrick Long"
25
- LICENSE = "MIT-0"
26
-
27
- # Files to skip entirely (relative paths from repo root)
28
- EXCLUDED_FILES = [
29
- "doc/contributors/v1.0.0_blockers.md",
30
- "doc/contributors/upstream_requests/tab_rects.md",
31
- "doc/contributors/upstream_requests/title_rects.md",
32
- ].freeze
33
-
34
- def get_latest_git_year(file, start_line, end_line)
35
- cmd = %W[git blame -L #{start_line},#{end_line} --date=short -- #{file}]
36
- output, _status = Open3.capture2(*cmd)
37
- years = output.scan(/(\d{4})-\d{2}-\d{2}/).flatten.map(&:to_i)
38
- years.empty? ? Date.today.year : years.max
39
- end
40
-
41
- def is_our_snippet_header?(lines, idx)
42
- # Check if the current SPDX-SnippetBegin block already has our copyright/license
43
- i = idx + 1
44
- has_our_copyright = false
45
- has_mit0 = false
46
-
47
- while i < lines.length && !lines[i].include?("-->")
48
- line = lines[i]
49
- has_our_copyright = true if line.include?(COPYRIGHT_HOLDER) && line.include?("SPDX-FileCopyrightText")
50
- has_mit0 = true if line.include?("MIT-0") && line.include?("SPDX-License-Identifier")
51
- i += 1
52
- end
53
-
54
- has_our_copyright && has_mit0
55
- end
56
-
57
- def find_snippet_end(lines, start_idx)
58
- i = start_idx
59
- while i < lines.length
60
- return i if lines[i].include?("SPDX-SnippetEnd")
61
- i += 1
62
- end
63
- nil
64
- end
65
-
66
- def process_file(filepath)
67
- # Skip excluded files
68
- return if EXCLUDED_FILES.any? { |excluded| filepath.end_with?(excluded) }
69
-
70
- content = File.read(filepath)
71
- lines = content.lines
72
-
73
- # Track code block ranges (to exclude from file header year calculation)
74
- code_block_ranges = []
75
- changes = []
76
- removals = [] # existing malformed SPDX snippet blocks to remove
77
- i = 0
78
-
79
- while i < lines.length
80
- line = lines[i]
81
-
82
- # Check if we're at an existing SPDX-SnippetBegin
83
- if line.include?("SPDX-SnippetBegin")
84
- snippet_start = i
85
- snippet_end = find_snippet_end(lines, i)
86
-
87
- if snippet_end
88
- # Check if this is already our proper snippet
89
- if is_our_snippet_header?(lines, i)
90
- # Skip this block entirely - it's already correct
91
- i = snippet_end + 1
92
- next
93
- else
94
- # Mark for removal - we'll re-wrap the inner content
95
- removals << { start: snippet_start, end: snippet_end }
96
- i = snippet_end + 1
97
- next
98
- end
99
- end
100
- end
101
-
102
- # Check for SYNC:START pattern
103
- if line =~ /<!--\s*SYNC:START/
104
- sync_start_line = i
105
- j = i + 1
106
- code_start = nil
107
- code_end = nil
108
- sync_end_line = nil
109
-
110
- while j < lines.length
111
- if lines[j] =~ /^(````*)(\w*)$/
112
- if code_start.nil?
113
- code_start = j
114
- else
115
- code_end = j
116
- end
117
- elsif lines[j] =~ /<!--\s*SYNC:END/
118
- sync_end_line = j
119
- break
120
- end
121
- j += 1
122
- end
123
-
124
- if code_start && code_end && sync_end_line
125
- year = get_latest_git_year(filepath, code_start + 1, code_end + 1)
126
- changes << {
127
- type: :sync_block,
128
- start: sync_start_line,
129
- end: sync_end_line,
130
- year:,
131
- }
132
- code_block_ranges << (code_start..code_end)
133
- i = sync_end_line + 1
134
- next
135
- end
136
- end
137
-
138
- # Check for standalone fenced code block
139
- if line =~ /^(````*)(\w*)$/
140
- fence_marker = $1
141
- fence_start = i
142
- re_end = /^#{Regexp.escape(fence_marker)}$/
143
-
144
- j = i + 1
145
- fence_end = nil
146
- while j < lines.length
147
- if lines[j] =~ re_end
148
- fence_end = j
149
- break
150
- end
151
- j += 1
152
- end
153
-
154
- if fence_end
155
- year = get_latest_git_year(filepath, fence_start + 1, fence_end + 1)
156
- changes << {
157
- type: :code_block,
158
- start: fence_start,
159
- end: fence_end,
160
- year:,
161
- }
162
- code_block_ranges << (fence_start..fence_end)
163
- i = fence_end + 1
164
- next
165
- end
166
- end
167
-
168
- i += 1
169
- end
170
-
171
- # Handle removals and additions
172
- has_changes = !changes.empty? || !removals.empty?
173
-
174
- # Remove existing malformed SPDX blocks (in reverse order)
175
- removals.sort_by { |r| -r[:start] }.each do |removal|
176
- # Remove the SnippetEnd line
177
- lines.delete_at(removal[:end])
178
- # Remove lines from SnippetBegin through the --> closing the comment
179
- close_idx = removal[:start]
180
- while close_idx < lines.length && !lines[close_idx].include?("-->")
181
- close_idx += 1
182
- end
183
- # Remove from start to close_idx inclusive
184
- (close_idx - removal[:start] + 1).times { lines.delete_at(removal[:start]) }
185
- end
186
-
187
- # Recalculate content after removals
188
- content = lines.join
189
- lines = content.lines
190
-
191
- # Re-scan for code blocks that need wrapping
192
- changes = []
193
- i = 0
194
-
195
- while i < lines.length
196
- line = lines[i]
197
-
198
- # Skip if already inside an SPDX-SnippetBegin block
199
- if line.include?("SPDX-SnippetBegin")
200
- while i < lines.length && !lines[i].include?("SPDX-SnippetEnd")
201
- i += 1
202
- end
203
- i += 1
204
- next
205
- end
206
-
207
- # Check for SYNC:START pattern
208
- if line =~ /<!--\s*SYNC:START/
209
- sync_start_line = i
210
- j = i + 1
211
- code_start = nil
212
- code_end = nil
213
- sync_end_line = nil
214
-
215
- while j < lines.length
216
- if lines[j] =~ /^(````*)(\w*)$/
217
- if code_start.nil?
218
- code_start = j
219
- else
220
- code_end = j
221
- end
222
- elsif lines[j] =~ /<!--\s*SYNC:END/
223
- sync_end_line = j
224
- break
225
- end
226
- j += 1
227
- end
228
-
229
- if code_start && code_end && sync_end_line
230
- year = get_latest_git_year(filepath, code_start + 1, code_end + 1)
231
- changes << {
232
- type: :sync_block,
233
- start: sync_start_line,
234
- end: sync_end_line,
235
- year:,
236
- }
237
- i = sync_end_line + 1
238
- next
239
- end
240
- end
241
-
242
- # Check for standalone fenced code block
243
- if line =~ /^(````*)(\w*)$/
244
- fence_marker = $1
245
- fence_start = i
246
- re_end = /^#{Regexp.escape(fence_marker)}$/
247
-
248
- j = i + 1
249
- fence_end = nil
250
- while j < lines.length
251
- if lines[j] =~ re_end
252
- fence_end = j
253
- break
254
- end
255
- j += 1
256
- end
257
-
258
- if fence_end
259
- year = get_latest_git_year(filepath, fence_start + 1, fence_end + 1)
260
- changes << {
261
- type: :code_block,
262
- start: fence_start,
263
- end: fence_end,
264
- year:,
265
- }
266
- i = fence_end + 1
267
- next
268
- end
269
- end
270
-
271
- i += 1
272
- end
273
-
274
- return if changes.empty? && !has_changes
275
-
276
- # Apply changes in reverse order to preserve line numbers
277
- changes.sort_by { |c| -c[:start] }.each do |change|
278
- # REUSE-IgnoreStart
279
- snippet_begin = "<!-- SPDX-SnippetBegin -->\n<!--\n SPDX-FileCopyrightText: #{change[:year]} #{COPYRIGHT_HOLDER}\n SPDX-License-Identifier: #{LICENSE}\n-->\n"
280
- snippet_end = "<!-- SPDX-SnippetEnd -->\n"
281
- # REUSE-IgnoreEnd
282
-
283
- # Insert end marker after the block
284
- lines.insert(change[:end] + 1, snippet_end)
285
- # Insert begin marker before the block
286
- lines.insert(change[:start], snippet_begin)
287
- end
288
-
289
- File.write(filepath, lines.join)
290
- puts "Updated: #{filepath} (#{changes.length} code block(s))"
291
- end
292
-
293
- def find_md_files(paths)
294
- # Use git ls-files to respect .gitignore
295
- if paths.empty?
296
- `git ls-files '*.md'`.split("\n")
297
- else
298
- paths.flat_map do |path|
299
- if File.directory?(path)
300
- `git ls-files '#{path}/**/*.md'`.split("\n")
301
- else
302
- path
303
- end
304
- end
305
- end
306
- end
307
-
308
- if __FILE__ == $0
309
- paths = ARGV.empty? ? [] : ARGV
310
- files = find_md_files(paths)
311
-
312
- files.each do |file|
313
- process_file(file)
314
- end
315
- end
@@ -1,150 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- #--
4
- # SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
5
- # SPDX-License-Identifier: AGPL-3.0-or-later
6
- #++
7
-
8
- # Script to add SPDX snippet headers to RDoc code examples in Ruby files.
9
- #
10
- # Usage: ruby scripts/add_spdx_rdoc_snippets.rb [path...]
11
- #
12
- # If no paths are given, processes all .rb files via git ls-files.
13
- #
14
- # Rules:
15
- # - Wraps RDoc code examples (indented comment lines) with SPDX snippet headers
16
- # - Uses #-- and #++ to hide the SPDX headers from RDoc rendering
17
- # - Uses git blame to determine the latest edit year for the code lines
18
- # - Skips blocks that are already wrapped with SPDX-SnippetBegin
19
-
20
- require "open3"
21
- require "date"
22
-
23
- COPYRIGHT_HOLDER = "Kerrick Long"
24
- LICENSE = "MIT-0"
25
-
26
- def get_latest_git_year(file, start_line, end_line)
27
- cmd = %W[git blame -L #{start_line},#{end_line} --date=short -- #{file}]
28
- output, _status = Open3.capture2(*cmd)
29
- years = output.scan(/(\d{4})-\d{2}-\d{2}/).flatten.map(&:to_i)
30
- years.empty? ? Date.today.year : years.max
31
- end
32
-
33
- def find_rdoc_code_blocks(lines)
34
- # Find all RDoc code blocks (indented comment lines)
35
- # Returns array of {start:, end:, indent:} where indent is the comment prefix
36
- blocks = []
37
- i = 0
38
-
39
- while i < lines.length
40
- line = lines[i]
41
-
42
- # Check if this is an indented code line in a comment
43
- # Pattern: optional leading whitespace, #, then 3+ spaces (RDoc code indent)
44
- if line =~ /^(\s*)#( +)(\S.*)$/
45
- prefix = $1 # leading whitespace before #
46
- block_start = i
47
-
48
- # Find the extent of this code block
49
- j = i
50
- while j < lines.length
51
- current = lines[j]
52
- # Code block continues if line is indented code OR empty comment line
53
- if current =~ /^#{Regexp.escape(prefix)}#( +|\s*$)/
54
- j += 1
55
- else
56
- break
57
- end
58
- end
59
-
60
- block_end = j - 1
61
-
62
- # Only count as a block if it has actual code (not just empty lines)
63
- has_code = (block_start..block_end).any? { |k| lines[k] =~ /^#{Regexp.escape(prefix)}# +\S/ }
64
-
65
- if has_code && block_end > block_start
66
- blocks << { start: block_start, end: block_end, prefix: }
67
- end
68
-
69
- i = j
70
- else
71
- i += 1
72
- end
73
- end
74
-
75
- blocks
76
- end
77
-
78
- def is_already_wrapped?(lines, block_start, prefix)
79
- # Check if the line before the block is #++ (meaning it's already wrapped)
80
- return false if block_start < 1
81
-
82
- prev_line = lines[block_start - 1]
83
- prev_line =~ /^#{Regexp.escape(prefix)}#\+\+\s*$/
84
- end
85
-
86
- def process_file(filepath)
87
- content = File.read(filepath)
88
- lines = content.lines
89
-
90
- blocks = find_rdoc_code_blocks(lines)
91
-
92
- # Filter out already-wrapped blocks
93
- blocks.reject! { |b| is_already_wrapped?(lines, b[:start], b[:prefix]) }
94
-
95
- return if blocks.empty?
96
-
97
- # Apply changes in reverse order to preserve line numbers
98
- blocks.sort_by { |b| -b[:start] }.each do |block|
99
- year = get_latest_git_year(filepath, block[:start] + 1, block[:end] + 1)
100
- prefix = block[:prefix]
101
-
102
- # Build the wrapper lines
103
- # REUSE-IgnoreStart
104
- begin_wrapper = [
105
- "#{prefix}#--\n",
106
- "#{prefix}# SPDX-SnippetBegin\n",
107
- "#{prefix}# SPDX-FileCopyrightText: #{year} #{COPYRIGHT_HOLDER}\n",
108
- "#{prefix}# SPDX-License-Identifier: #{LICENSE}\n",
109
- "#{prefix}#++\n",
110
- ]
111
-
112
- end_wrapper = [
113
- "#{prefix}#--\n",
114
- "#{prefix}# SPDX-SnippetEnd\n",
115
- "#{prefix}#++\n",
116
- ]
117
- # REUSE-IgnoreEnd
118
-
119
- # Insert end wrapper after the block
120
- lines.insert(block[:end] + 1, *end_wrapper)
121
- # Insert begin wrapper before the block
122
- lines.insert(block[:start], *begin_wrapper)
123
- end
124
-
125
- File.write(filepath, lines.join)
126
- puts "Updated: #{filepath} (#{blocks.length} code block(s))"
127
- end
128
-
129
- def find_rb_files(paths)
130
- if paths.empty?
131
- `git ls-files '*.rb'`.split("\n")
132
- else
133
- paths.flat_map do |path|
134
- if File.directory?(path)
135
- `git ls-files '#{path}/**/*.rb'`.split("\n")
136
- else
137
- path
138
- end
139
- end
140
- end
141
- end
142
-
143
- if __FILE__ == $0
144
- paths = ARGV.empty? ? [] : ARGV
145
- files = find_rb_files(paths)
146
-
147
- files.each do |file|
148
- process_file(file)
149
- end
150
- end
data/tasks/license.rake DELETED
@@ -1,91 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- #--
4
- # SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
5
- # SPDX-License-Identifier: AGPL-3.0-or-later
6
- #++
7
-
8
- namespace :license do
9
- namespace :headers do
10
- desc "Ensure markdown files have correct CC-BY-SA-4.0 headers"
11
- task :md, [:files] do |_t, args|
12
- files = args[:files] || ""
13
- ruby "tasks/license/headers_md.rb #{files}"
14
- end
15
-
16
- desc "Ensure Ruby files have correct AGPL-3.0-or-later headers"
17
- task :rb, [:files] do |_t, args|
18
- files = args[:files] || ""
19
- ruby "tasks/license/headers_rb.rb #{files}"
20
- end
21
-
22
- desc "Ensure all files have correct license headers"
23
- task :all do
24
- Rake::Task["license:headers:md"].invoke
25
- Rake::Task["license:headers:rb"].invoke
26
- end
27
- end
28
-
29
- namespace :snippets do
30
- desc "Add MIT-0 SPDX snippet headers to markdown fenced code blocks"
31
- task :md, [:files] do |_t, args|
32
- files = args[:files] || ""
33
- ruby "tasks/license/snippets_md.rb #{files}"
34
- end
35
-
36
- desc "Add MIT-0 SPDX snippet headers to RDoc code examples in Ruby files"
37
- task :rdoc, [:files] do |_t, args|
38
- files = args[:files] || "lib/"
39
- ruby "tasks/license/snippets_rdoc.rb #{files}"
40
- end
41
-
42
- desc "Add MIT-0 SPDX snippet headers to all code examples"
43
- task :all do
44
- Rake::Task["license:snippets:md"].invoke
45
- Rake::Task["license:snippets:rdoc"].invoke
46
- end
47
- end
48
-
49
- desc "Run all license tasks (headers + snippets)"
50
- task all: ["headers:all", "snippets:all"]
51
-
52
- desc "Run license tasks on changed files only (staged + unstaged)"
53
- task :new do
54
- # Get changed .md and .rb files (staged and unstaged)
55
- changed_md = `git diff --name-only --diff-filter=ACMR HEAD -- '*.md' 2>/dev/null`.split("\n")
56
- staged_md = `git diff --name-only --cached --diff-filter=ACMR -- '*.md' 2>/dev/null`.split("\n")
57
- changed_rb = `git diff --name-only --diff-filter=ACMR HEAD -- '*.rb' 2>/dev/null`.split("\n")
58
- staged_rb = `git diff --name-only --cached --diff-filter=ACMR -- '*.rb' 2>/dev/null`.split("\n")
59
-
60
- # Also get untracked new files
61
- untracked = `git ls-files --others --exclude-standard`.split("\n")
62
- untracked_md = untracked.select { |f| f.end_with?(".md") }
63
- untracked_rb = untracked.select { |f| f.end_with?(".rb") }
64
-
65
- md_files = (changed_md + staged_md + untracked_md).uniq.join(" ")
66
- rb_files = (changed_rb + staged_rb + untracked_rb).uniq
67
-
68
- # Filter rb files to only lib/
69
- lib_rb_files = rb_files.select { |f| f.start_with?("lib/") }.join(" ")
70
-
71
- if md_files.empty? && lib_rb_files.empty?
72
- puts "No changed .md or lib/*.rb files to process"
73
- else
74
- unless md_files.empty?
75
- puts "Processing #{md_files.split.count} changed .md file(s)..."
76
- Rake::Task["license:headers:md"].invoke(md_files)
77
- Rake::Task["license:headers:md"].reenable
78
- Rake::Task["license:snippets:md"].invoke(md_files)
79
- Rake::Task["license:snippets:md"].reenable
80
- end
81
-
82
- unless lib_rb_files.empty?
83
- puts "Processing #{lib_rb_files.split.count} changed lib/*.rb file(s)..."
84
- Rake::Task["license:headers:rb"].invoke(lib_rb_files)
85
- Rake::Task["license:headers:rb"].reenable
86
- Rake::Task["license:snippets:rdoc"].invoke(lib_rb_files)
87
- Rake::Task["license:snippets:rdoc"].reenable
88
- end
89
- end
90
- end
91
- end