ratatui_ruby-devtools 0.1.0
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.
- checksums.yaml +7 -0
- data/.builds/ruby-4.0.yml +38 -0
- data/.pre-commit-config.yaml +16 -0
- data/.rubocop.yml +8 -0
- data/AGENTS.md +72 -0
- data/CHANGELOG.md +23 -0
- data/LICENSE +661 -0
- data/LICENSES/AGPL-3.0-or-later.txt +661 -0
- data/LICENSES/CC-BY-SA-4.0.txt +427 -0
- data/LICENSES/CC0-1.0.txt +121 -0
- data/LICENSES/MIT-0.txt +16 -0
- data/LICENSES/MIT.txt +18 -0
- data/README.md +199 -0
- data/REUSE.toml +18 -0
- data/Rakefile +13 -0
- data/bin/agent_rake +13 -0
- data/bin/announce +13 -0
- data/bin/console +14 -0
- data/bin/consolidate_md +13 -0
- data/bin/hbs +13 -0
- data/bin/setup +17 -0
- data/doc/contributors/documentation_style.md +121 -0
- data/doc/custom.css +22 -0
- data/exe/agent_rake +96 -0
- data/exe/announce +1120 -0
- data/exe/consolidate_md +246 -0
- data/exe/hbs +670 -0
- data/exe/scaffold +662 -0
- data/lib/ratatui_ruby/devtools/tasks/autodoc/examples.rb +133 -0
- data/lib/ratatui_ruby/devtools/tasks/autodoc/member.rb +116 -0
- data/lib/ratatui_ruby/devtools/tasks/autodoc/name.rb +33 -0
- data/lib/ratatui_ruby/devtools/tasks/autodoc.rake +21 -0
- data/lib/ratatui_ruby/devtools/tasks/bump/cargo_lockfile.rb +38 -0
- data/lib/ratatui_ruby/devtools/tasks/bump/changelog.rb +67 -0
- data/lib/ratatui_ruby/devtools/tasks/bump/header.rb +43 -0
- data/lib/ratatui_ruby/devtools/tasks/bump/history.rb +50 -0
- data/lib/ratatui_ruby/devtools/tasks/bump/links.rb +78 -0
- data/lib/ratatui_ruby/devtools/tasks/bump/manifest.rb +63 -0
- data/lib/ratatui_ruby/devtools/tasks/bump/ruby_gem.rb +77 -0
- data/lib/ratatui_ruby/devtools/tasks/bump/sem_ver.rb +63 -0
- data/lib/ratatui_ruby/devtools/tasks/bump/unreleased_section.rb +75 -0
- data/lib/ratatui_ruby/devtools/tasks/bump.rake +80 -0
- data/lib/ratatui_ruby/devtools/tasks/cargo.rake +47 -0
- data/lib/ratatui_ruby/devtools/tasks/doc.rake +887 -0
- data/lib/ratatui_ruby/devtools/tasks/example_viewer.html.erb +172 -0
- data/lib/ratatui_ruby/devtools/tasks/license/headers_md.rb +276 -0
- data/lib/ratatui_ruby/devtools/tasks/license/headers_rb.rb +236 -0
- data/lib/ratatui_ruby/devtools/tasks/license/license_utils.rb +143 -0
- data/lib/ratatui_ruby/devtools/tasks/license/snippets_md.rb +353 -0
- data/lib/ratatui_ruby/devtools/tasks/license/snippets_rdoc.rb +186 -0
- data/lib/ratatui_ruby/devtools/tasks/license.rake +91 -0
- data/lib/ratatui_ruby/devtools/tasks/lint.rake +84 -0
- data/lib/ratatui_ruby/devtools/tasks/rdoc_config.rb +45 -0
- data/lib/ratatui_ruby/devtools/tasks/resources/build.yml.erb +54 -0
- data/lib/ratatui_ruby/devtools/tasks/resources/rubies.yml +7 -0
- data/lib/ratatui_ruby/devtools/tasks/reuse.rake +104 -0
- data/lib/ratatui_ruby/devtools/tasks/sourcehut.rake +94 -0
- data/lib/ratatui_ruby/devtools/tasks/test.rake +18 -0
- data/lib/ratatui_ruby/devtools/templates/.builds/ruby.yml.erb +47 -0
- data/lib/ratatui_ruby/devtools/templates/.gitignore.erb +18 -0
- data/lib/ratatui_ruby/devtools/templates/.pre-commit-config.yaml.erb +16 -0
- data/lib/ratatui_ruby/devtools/templates/.rubocop.yml.erb +8 -0
- data/lib/ratatui_ruby/devtools/templates/AGENTS.md.erb +65 -0
- data/lib/ratatui_ruby/devtools/templates/CHANGELOG.md.erb +18 -0
- data/lib/ratatui_ruby/devtools/templates/Gemfile.erb +32 -0
- data/lib/ratatui_ruby/devtools/templates/README.md.erb +127 -0
- data/lib/ratatui_ruby/devtools/templates/REUSE.toml.erb +33 -0
- data/lib/ratatui_ruby/devtools/templates/Rakefile.erb +29 -0
- data/lib/ratatui_ruby/devtools/templates/bin/console.erb +18 -0
- data/lib/ratatui_ruby/devtools/templates/bin/setup.erb +24 -0
- data/lib/ratatui_ruby/devtools/templates/doc/concepts/application_architecture.md.erb +16 -0
- data/lib/ratatui_ruby/devtools/templates/doc/concepts/application_testing.md.erb +49 -0
- data/lib/ratatui_ruby/devtools/templates/doc/custom.css.erb +24 -0
- data/lib/ratatui_ruby/devtools/templates/doc/getting_started/quickstart.md.erb +56 -0
- data/lib/ratatui_ruby/devtools/templates/doc/images/.gitkeep +0 -0
- data/lib/ratatui_ruby/devtools/templates/doc/index.md.erb +25 -0
- data/lib/ratatui_ruby/devtools/templates/exe/.gitkeep +0 -0
- data/lib/ratatui_ruby/devtools/templates/gemspec.erb +58 -0
- data/lib/ratatui_ruby/devtools/templates/mise.toml.erb +12 -0
- data/lib/ratatui_ruby/devtools/templates/tasks/example_viewer.html.erb +174 -0
- data/lib/ratatui_ruby/devtools/templates/tasks/resources/build.yml.erb +62 -0
- data/lib/ratatui_ruby/devtools/templates/tasks/resources/index.html.erb +46 -0
- data/lib/ratatui_ruby/devtools/templates/tasks/resources/rubies.yml.erb +9 -0
- data/lib/ratatui_ruby/devtools/templates/vendor/goodcop/base.yml +1047 -0
- data/lib/ratatui_ruby/devtools/version.rb +13 -0
- data/lib/ratatui_ruby/devtools.rb +137 -0
- data/mise.toml +7 -0
- data/sig/ratatui_ruby/devtools.rbs +15 -0
- data/vendor/goodcop/base.yml +1047 -0
- metadata +252 -0
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
<%#
|
|
2
|
+
SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
|
|
3
|
+
SPDX-License-Identifier: AGPL-3.0-or-later
|
|
4
|
+
%>
|
|
5
|
+
<!DOCTYPE html>
|
|
6
|
+
<html lang="en">
|
|
7
|
+
<head>
|
|
8
|
+
<meta charset="UTF-8">
|
|
9
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
10
|
+
<title><%= page_title %> - Example Viewer</title>
|
|
11
|
+
|
|
12
|
+
<script>
|
|
13
|
+
var rdoc_rel_prefix = "<%= doc_root_link.sub('index.html', '') %>";
|
|
14
|
+
var index_rel_prefix = "<%= '../' * (relative_path.split('/').size - 1) %>";
|
|
15
|
+
</script>
|
|
16
|
+
|
|
17
|
+
<script src="<%= '../' * (relative_path.split('/').size - 1) %>js/search_navigation.js" defer></script>
|
|
18
|
+
<script src="<%= '../' * (relative_path.split('/').size - 1) %>js/search_data.js" defer></script>
|
|
19
|
+
<script src="<%= '../' * (relative_path.split('/').size - 1) %>js/search_ranker.js" defer></script>
|
|
20
|
+
<script src="<%= '../' * (relative_path.split('/').size - 1) %>js/search_controller.js" defer></script>
|
|
21
|
+
<script src="<%= '../' * (relative_path.split('/').size - 1) %>js/aliki.js" defer></script>
|
|
22
|
+
|
|
23
|
+
<link href="<%= doc_root_link.sub('index.html', '') %>css/rdoc.css" rel="stylesheet">
|
|
24
|
+
<link href="<%= doc_root_link.sub('index.html', '') %>custom.css" rel="stylesheet">
|
|
25
|
+
</head>
|
|
26
|
+
<body class="file<%= ' has-toc' unless toc_items.empty? %>">
|
|
27
|
+
<%= icons_svg %>
|
|
28
|
+
<header class="top-navbar">
|
|
29
|
+
<div class="navbar-brand">
|
|
30
|
+
Example Viewer
|
|
31
|
+
</div>
|
|
32
|
+
|
|
33
|
+
<!-- Desktop search bar -->
|
|
34
|
+
<div class="navbar-search navbar-search-desktop" role="search">
|
|
35
|
+
<form action="#" method="get" accept-charset="utf-8">
|
|
36
|
+
<input id="search-field" role="combobox" aria-label="Search"
|
|
37
|
+
aria-autocomplete="list" aria-controls="search-results-desktop"
|
|
38
|
+
type="text" name="search" placeholder="Search (/) examples..."
|
|
39
|
+
spellcheck="false" autocomplete="off"
|
|
40
|
+
title="Type to search, Up and Down to navigate, Enter to load">
|
|
41
|
+
<ul id="search-results-desktop" aria-label="Search Results"
|
|
42
|
+
aria-busy="false" aria-expanded="false"
|
|
43
|
+
aria-atomic="false" class="initially-hidden search-results"></ul>
|
|
44
|
+
</form>
|
|
45
|
+
</div>
|
|
46
|
+
|
|
47
|
+
<!-- Mobile search icon button -->
|
|
48
|
+
<button id="search-toggle" class="navbar-search-mobile" aria-label="Open search" type="button">
|
|
49
|
+
<span aria-hidden="true">🔍</span>
|
|
50
|
+
</button>
|
|
51
|
+
|
|
52
|
+
<button id="theme-toggle" class="theme-toggle" aria-label="Switch to dark mode" type="button" onclick="cycleColorMode()">
|
|
53
|
+
<span class="theme-toggle-icon" aria-hidden="true">🌙</span>
|
|
54
|
+
</button>
|
|
55
|
+
</header>
|
|
56
|
+
|
|
57
|
+
<!-- Search Modal (Mobile) -->
|
|
58
|
+
<div id="search-modal" class="search-modal" hidden aria-modal="true" role="dialog" aria-label="Search">
|
|
59
|
+
<div class="search-modal-backdrop"></div>
|
|
60
|
+
<div class="search-modal-content">
|
|
61
|
+
<div class="search-modal-header">
|
|
62
|
+
<form class="search-modal-form" action="#" method="get" accept-charset="utf-8">
|
|
63
|
+
<span class="search-modal-icon" aria-hidden="true">🔍</span>
|
|
64
|
+
<input id="search-field-mobile" role="combobox" aria-label="Search"
|
|
65
|
+
aria-autocomplete="list" aria-controls="search-results-mobile"
|
|
66
|
+
type="text" name="search" placeholder="Search examples"
|
|
67
|
+
spellcheck="false" autocomplete="off">
|
|
68
|
+
<button type="button" class="search-modal-close" aria-label="Close search" id="search-modal-close">
|
|
69
|
+
<span aria-hidden="true">esc</span>
|
|
70
|
+
</button>
|
|
71
|
+
</form>
|
|
72
|
+
</div>
|
|
73
|
+
<div class="search-modal-body">
|
|
74
|
+
<ul id="search-results-mobile" aria-label="Search Results"
|
|
75
|
+
aria-busy="false" aria-expanded="false"
|
|
76
|
+
aria-atomic="false" class="search-results search-modal-results initially-hidden"></ul>
|
|
77
|
+
<div class="search-modal-empty">
|
|
78
|
+
<p>No recent searches</p>
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
|
|
84
|
+
<nav id="navigation" role="navigation">
|
|
85
|
+
<div id="fileindex-section" class="nav-section">
|
|
86
|
+
<details class="nav-section-collapsible" open>
|
|
87
|
+
<summary class="nav-section-header">
|
|
88
|
+
<span class="nav-section-icon">
|
|
89
|
+
<svg><use href="#icon-file"></use></svg>
|
|
90
|
+
</span>
|
|
91
|
+
<span class="nav-section-title">Examples</span>
|
|
92
|
+
<span class="nav-section-chevron">
|
|
93
|
+
<svg><use href="#icon-chevron"></use></svg>
|
|
94
|
+
</span>
|
|
95
|
+
</summary>
|
|
96
|
+
<ul class="nav-list">
|
|
97
|
+
<li><a href="<%= doc_root_link %>">← Back to Docs</a></li>
|
|
98
|
+
</ul>
|
|
99
|
+
</details>
|
|
100
|
+
</div>
|
|
101
|
+
<div class="nav-section">
|
|
102
|
+
<details class="nav-section-collapsible" open>
|
|
103
|
+
<summary class="nav-section-header">
|
|
104
|
+
<span class="nav-section-icon">
|
|
105
|
+
<svg><use href="#icon-layers"></use></svg>
|
|
106
|
+
</span>
|
|
107
|
+
<span class="nav-section-title">Files</span>
|
|
108
|
+
<span class="nav-section-chevron">
|
|
109
|
+
<svg><use href="#icon-chevron"></use></svg>
|
|
110
|
+
</span>
|
|
111
|
+
</summary>
|
|
112
|
+
<ul class="link-list nav-list">
|
|
113
|
+
<%= render_tree(tree_data, relative_path, current_file_html) %>
|
|
114
|
+
</ul>
|
|
115
|
+
</details>
|
|
116
|
+
</div>
|
|
117
|
+
</nav>
|
|
118
|
+
|
|
119
|
+
<main role="main">
|
|
120
|
+
<div class="breadcrumb">
|
|
121
|
+
<%= breadcrumb_path %>
|
|
122
|
+
</div>
|
|
123
|
+
<%= file_header_html %>
|
|
124
|
+
<div class="content">
|
|
125
|
+
<%= file_content_html %>
|
|
126
|
+
</div>
|
|
127
|
+
</main>
|
|
128
|
+
|
|
129
|
+
<% unless toc_items.empty? %>
|
|
130
|
+
<aside class="table-of-contents" role="complementary" aria-label="Table of Contents">
|
|
131
|
+
<div class="toc-sticky">
|
|
132
|
+
<div class="toc-list">
|
|
133
|
+
<h3>On This Page</h3>
|
|
134
|
+
<%= render_toc(toc_items) %>
|
|
135
|
+
</div>
|
|
136
|
+
</div>
|
|
137
|
+
</aside>
|
|
138
|
+
<% end %>
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
<script>
|
|
142
|
+
const modes = ['auto', 'light', 'dark'];
|
|
143
|
+
const icons = { auto: '🌓', light: '☀️', dark: '🌙' };
|
|
144
|
+
|
|
145
|
+
function setColorMode(mode) {
|
|
146
|
+
if (mode === 'auto') {
|
|
147
|
+
document.documentElement.removeAttribute('data-theme');
|
|
148
|
+
const systemTheme = (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) ? 'dark' : 'light';
|
|
149
|
+
document.documentElement.setAttribute('data-theme', systemTheme);
|
|
150
|
+
} else {
|
|
151
|
+
document.documentElement.setAttribute('data-theme', mode);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const icon = icons[mode];
|
|
155
|
+
const toggle = document.getElementById('theme-toggle');
|
|
156
|
+
toggle.querySelector('.theme-toggle-icon').textContent = icon;
|
|
157
|
+
|
|
158
|
+
localStorage.setItem('rdoc-theme', mode);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function cycleColorMode() {
|
|
162
|
+
const current = localStorage.getItem('rdoc-theme') || 'auto';
|
|
163
|
+
const currentIndex = modes.indexOf(current);
|
|
164
|
+
const nextMode = modes[(currentIndex + 1) % modes.length];
|
|
165
|
+
setColorMode(nextMode);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const savedMode = localStorage.getItem('rdoc-theme') || 'auto';
|
|
169
|
+
setColorMode(savedMode);
|
|
170
|
+
</script>
|
|
171
|
+
</body>
|
|
172
|
+
</html>
|
|
@@ -0,0 +1,276 @@
|
|
|
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
|
+
# Ensures markdown files have correct SPDX headers.
|
|
9
|
+
#
|
|
10
|
+
# Open source projects need license headers in every file. REUSE compliance
|
|
11
|
+
# requires SPDX format. Adding and updating these headers by hand is tedious.
|
|
12
|
+
# Years drift. Contributors are forgotten.
|
|
13
|
+
#
|
|
14
|
+
# This script processes markdown files. It adds CC-BY-SA-4.0 headers where
|
|
15
|
+
# missing. It updates copyright years based on git history. It preserves
|
|
16
|
+
# existing contributors.
|
|
17
|
+
#
|
|
18
|
+
# Run it as part of license:headers:md rake task.
|
|
19
|
+
#
|
|
20
|
+
# === Example
|
|
21
|
+
#
|
|
22
|
+
# ruby tasks/license/headers_md.rb README.md
|
|
23
|
+
# ruby tasks/license/headers_md.rb doc/
|
|
24
|
+
# ruby tasks/license/headers_md.rb # all .md files
|
|
25
|
+
#
|
|
26
|
+
# Rules:
|
|
27
|
+
# - Ensures file has CC-BY-SA-4.0 license header with YOUR copyright
|
|
28
|
+
# - Updates years for EXISTING contributors based on git blame + Co-Authored-By
|
|
29
|
+
# - Does NOT add new contributors from git history - only updates existing ones
|
|
30
|
+
# - Uses non-code-block lines for year calculation
|
|
31
|
+
# - Adds header with YOUR copyright if missing
|
|
32
|
+
|
|
33
|
+
require_relative "license_utils"
|
|
34
|
+
|
|
35
|
+
# Your name for copyright headers.
|
|
36
|
+
YOUR_NAME = "Kerrick Long"
|
|
37
|
+
|
|
38
|
+
# Your email for copyright headers.
|
|
39
|
+
YOUR_EMAIL = "me@kerricklong.com"
|
|
40
|
+
|
|
41
|
+
# Identifiers used to match your contributions in git history.
|
|
42
|
+
YOUR_IDENTIFIERS = [YOUR_NAME, YOUR_EMAIL].freeze
|
|
43
|
+
|
|
44
|
+
# Full copyright string for headers.
|
|
45
|
+
YOUR_COPYRIGHT = "#{YOUR_NAME} <#{YOUR_EMAIL}>"
|
|
46
|
+
|
|
47
|
+
# The SPDX license identifier for markdown documentation.
|
|
48
|
+
LICENSE = "CC-BY-SA-4.0"
|
|
49
|
+
|
|
50
|
+
# Identifies fenced code blocks in markdown content.
|
|
51
|
+
#
|
|
52
|
+
# Copyright years come from git blame. Code blocks contain pasted content,
|
|
53
|
+
# not original prose. Blaming code block lines produces wrong contributors.
|
|
54
|
+
# Exclude these ranges when calculating copyright years.
|
|
55
|
+
#
|
|
56
|
+
# [lines] Array of line strings from the file.
|
|
57
|
+
def find_code_blocks(lines)
|
|
58
|
+
blocks = []
|
|
59
|
+
i = 0
|
|
60
|
+
|
|
61
|
+
while i < lines.length
|
|
62
|
+
line = lines[i]
|
|
63
|
+
|
|
64
|
+
if line =~ /^(````*)(\w*)$/
|
|
65
|
+
fence_marker = $1
|
|
66
|
+
fence_start = i
|
|
67
|
+
re_end = /^#{Regexp.escape(fence_marker)}$/
|
|
68
|
+
|
|
69
|
+
j = i + 1
|
|
70
|
+
while j < lines.length
|
|
71
|
+
if lines[j] =~ re_end
|
|
72
|
+
blocks << { start: fence_start, end: j }
|
|
73
|
+
i = j
|
|
74
|
+
break
|
|
75
|
+
end
|
|
76
|
+
j += 1
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
i += 1
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
blocks
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Calculates line ranges outside code blocks.
|
|
87
|
+
#
|
|
88
|
+
# Copyright years come from git blame. Blaming the entire file includes code
|
|
89
|
+
# blocks. This function returns only prose ranges for accurate year lookup.
|
|
90
|
+
#
|
|
91
|
+
# [lines] Array of line strings from the file.
|
|
92
|
+
def get_non_code_line_ranges(lines)
|
|
93
|
+
header_end = 0
|
|
94
|
+
if lines[0]&.include?("<!--")
|
|
95
|
+
(0...(lines.length)).each do |i|
|
|
96
|
+
if lines[i].include?("-->")
|
|
97
|
+
header_end = i + 1
|
|
98
|
+
break
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
code_blocks = find_code_blocks(lines)
|
|
104
|
+
non_code_ranges = []
|
|
105
|
+
current_line = header_end
|
|
106
|
+
|
|
107
|
+
code_blocks.each do |block|
|
|
108
|
+
if current_line < block[:start]
|
|
109
|
+
non_code_ranges << [current_line + 1, block[:start]]
|
|
110
|
+
end
|
|
111
|
+
current_line = block[:end] + 1
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
if current_line < lines.length
|
|
115
|
+
non_code_ranges << [current_line + 1, lines.length]
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
non_code_ranges
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Extracts existing SPDX header from markdown content.
|
|
122
|
+
#
|
|
123
|
+
# Files may already have headers. Updating requires parsing existing copyright
|
|
124
|
+
# holders and years. This extracts them for comparison and update.
|
|
125
|
+
#
|
|
126
|
+
# [lines] Array of line strings from the file.
|
|
127
|
+
def parse_existing_header(lines)
|
|
128
|
+
return nil unless lines[0]&.include?("<!--")
|
|
129
|
+
|
|
130
|
+
header_end = nil
|
|
131
|
+
copyrights = []
|
|
132
|
+
license = nil
|
|
133
|
+
|
|
134
|
+
(0...(lines.length)).each do |i|
|
|
135
|
+
line = lines[i]
|
|
136
|
+
|
|
137
|
+
if line =~ /SPDX-FileCopyrightText:\s*(\d{4})\s+(.+)$/
|
|
138
|
+
copyrights << { year: $1.to_i, holder: $2.strip }
|
|
139
|
+
# REUSE-IgnoreStart
|
|
140
|
+
elsif line =~ /SPDX-License-Identifier:\s*(.+)$/
|
|
141
|
+
# REUSE-IgnoreEnd
|
|
142
|
+
license = $1.strip
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
if line.include?("-->")
|
|
146
|
+
header_end = i
|
|
147
|
+
break
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
return nil if header_end.nil?
|
|
152
|
+
return nil if copyrights.empty? && license.nil?
|
|
153
|
+
|
|
154
|
+
{ end_line: header_end, copyrights:, license: }
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Updates or adds SPDX headers for a single markdown file.
|
|
158
|
+
#
|
|
159
|
+
# Each file needs correct CC-BY-SA-4.0 headers with accurate copyright years.
|
|
160
|
+
# Processing involves reading, parsing, querying git, and rewriting. This
|
|
161
|
+
# function orchestrates that workflow.
|
|
162
|
+
#
|
|
163
|
+
# [filepath] Path to the markdown file.
|
|
164
|
+
def process_file(filepath)
|
|
165
|
+
content = File.read(filepath)
|
|
166
|
+
lines = content.lines
|
|
167
|
+
|
|
168
|
+
non_code_ranges = get_non_code_line_ranges(lines)
|
|
169
|
+
|
|
170
|
+
# Get contributors from non-code lines for year lookups
|
|
171
|
+
all_contributors = {}
|
|
172
|
+
non_code_ranges.each do |start_line, end_line|
|
|
173
|
+
range_contributors = LicenseUtils.get_contributors_for_lines(filepath, start_line, end_line)
|
|
174
|
+
range_contributors.each do |contributor, year|
|
|
175
|
+
all_contributors[contributor] = [all_contributors[contributor] || 0, year].max
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
your_year = nil
|
|
180
|
+
all_contributors.each do |contributor, year|
|
|
181
|
+
if YOUR_IDENTIFIERS.any? { |id| contributor.include?(id) }
|
|
182
|
+
your_year = [your_year || 0, year].max
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
your_year ||= Date.today.year
|
|
186
|
+
|
|
187
|
+
existing = parse_existing_header(lines)
|
|
188
|
+
|
|
189
|
+
if existing
|
|
190
|
+
# Only update years for EXISTING contributors
|
|
191
|
+
needs_update = false
|
|
192
|
+
updated_copyrights = []
|
|
193
|
+
|
|
194
|
+
existing[:copyrights].each do |c|
|
|
195
|
+
git_year = nil
|
|
196
|
+
all_contributors.each do |contributor, year|
|
|
197
|
+
if c[:holder].split.any? { |word| contributor.include?(word) }
|
|
198
|
+
git_year = [git_year || 0, year].max
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
if git_year && git_year != c[:year]
|
|
203
|
+
puts " Updated #{c[:holder].split.first}'s copyright year: #{c[:year]} -> #{git_year}"
|
|
204
|
+
updated_copyrights << { year: git_year, holder: c[:holder] }
|
|
205
|
+
needs_update = true
|
|
206
|
+
else
|
|
207
|
+
updated_copyrights << c
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
# Check if YOUR year needs updating
|
|
212
|
+
your_existing = updated_copyrights.find { |c| YOUR_IDENTIFIERS.any? { |id| c[:holder].include?(id) } }
|
|
213
|
+
if your_existing.nil?
|
|
214
|
+
puts " Adding your copyright"
|
|
215
|
+
updated_copyrights << { year: your_year, holder: YOUR_COPYRIGHT }
|
|
216
|
+
needs_update = true
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
if existing[:license] != LICENSE
|
|
220
|
+
puts " Fixing license: #{existing[:license]} -> #{LICENSE}"
|
|
221
|
+
needs_update = true
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
if needs_update
|
|
225
|
+
# REUSE-IgnoreStart
|
|
226
|
+
header_lines = ["<!--\n"]
|
|
227
|
+
updated_copyrights.each do |c|
|
|
228
|
+
header_lines << " SPDX-FileCopyrightText: #{c[:year]} #{c[:holder]}\n"
|
|
229
|
+
end
|
|
230
|
+
header_lines << " SPDX-License-Identifier: #{LICENSE}\n"
|
|
231
|
+
header_lines << "-->\n"
|
|
232
|
+
# REUSE-IgnoreEnd
|
|
233
|
+
|
|
234
|
+
remaining = lines[(existing[:end_line] + 1)..]
|
|
235
|
+
File.write(filepath, header_lines.join + remaining.join)
|
|
236
|
+
puts "Updated: #{filepath}"
|
|
237
|
+
end
|
|
238
|
+
else
|
|
239
|
+
# No header - add one with YOUR copyright only
|
|
240
|
+
# REUSE-IgnoreStart
|
|
241
|
+
header = "<!--\n SPDX-FileCopyrightText: #{your_year} #{YOUR_COPYRIGHT}\n SPDX-License-Identifier: #{LICENSE}\n-->\n"
|
|
242
|
+
# REUSE-IgnoreEnd
|
|
243
|
+
|
|
244
|
+
File.write(filepath, header + content)
|
|
245
|
+
puts "Added header: #{filepath}"
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
# Finds markdown files to process.
|
|
250
|
+
#
|
|
251
|
+
# License automation runs on file sets. Users may specify paths or want all
|
|
252
|
+
# files. This handles both cases using git ls-files for tracking.
|
|
253
|
+
#
|
|
254
|
+
# [paths] Explicit paths to process, or empty for all tracked .md files.
|
|
255
|
+
def find_md_files(paths)
|
|
256
|
+
if paths.empty?
|
|
257
|
+
`git ls-files '*.md'`.split("\n")
|
|
258
|
+
else
|
|
259
|
+
paths.flat_map do |path|
|
|
260
|
+
if File.directory?(path)
|
|
261
|
+
`git ls-files '#{path}/**/*.md'`.split("\n")
|
|
262
|
+
else
|
|
263
|
+
path
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
if __FILE__ == $0
|
|
270
|
+
paths = ARGV.empty? ? [] : ARGV
|
|
271
|
+
files = find_md_files(paths)
|
|
272
|
+
|
|
273
|
+
files.each do |file|
|
|
274
|
+
process_file(file)
|
|
275
|
+
end
|
|
276
|
+
end
|
|
@@ -0,0 +1,236 @@
|
|
|
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 ensure Ruby files have correct SPDX file headers.
|
|
9
|
+
#
|
|
10
|
+
# Usage: ruby tasks/license/headers_rb.rb [path...]
|
|
11
|
+
#
|
|
12
|
+
# If no paths are given, processes lib/, ext/, test/, examples/, tasks/, bin/.
|
|
13
|
+
#
|
|
14
|
+
# License selection by directory:
|
|
15
|
+
# - lib/, ext/, test/ → LGPL-3.0-or-later
|
|
16
|
+
# - examples/widget_*, examples/verify_* → MIT-0
|
|
17
|
+
# - examples/app_*, tasks/, bin/ → AGPL-3.0-or-later
|
|
18
|
+
|
|
19
|
+
require_relative "license_utils"
|
|
20
|
+
|
|
21
|
+
YOUR_NAME = "Kerrick Long"
|
|
22
|
+
YOUR_EMAIL = "me@kerricklong.com"
|
|
23
|
+
YOUR_IDENTIFIERS = [YOUR_NAME, YOUR_EMAIL].freeze
|
|
24
|
+
YOUR_COPYRIGHT = "#{YOUR_NAME} <#{YOUR_EMAIL}>"
|
|
25
|
+
|
|
26
|
+
# Selects the appropriate license based on file location.
|
|
27
|
+
#
|
|
28
|
+
# Different parts of the codebase have different licenses. Library code is
|
|
29
|
+
# LGPL. Examples are MIT-0 or AGPL. This function routes files to their
|
|
30
|
+
# correct license by path pattern.
|
|
31
|
+
#
|
|
32
|
+
# [filepath] Path to the Ruby file.
|
|
33
|
+
def license_for_file(filepath)
|
|
34
|
+
case filepath
|
|
35
|
+
when %r{^(lib|sig/ratatui_ruby|ext|test)/}
|
|
36
|
+
"LGPL-3.0-or-later"
|
|
37
|
+
when %r{^(examples|sig/examples)/(widget_|verify_)}
|
|
38
|
+
"MIT-0"
|
|
39
|
+
else
|
|
40
|
+
"AGPL-3.0-or-later"
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Extracts existing SPDX header from Ruby file content.
|
|
45
|
+
#
|
|
46
|
+
# Files may already have headers. Updating requires parsing existing copyright
|
|
47
|
+
# holders and years. This extracts them for comparison and update.
|
|
48
|
+
#
|
|
49
|
+
# [lines] Array of line strings from the file.
|
|
50
|
+
def parse_existing_header(lines)
|
|
51
|
+
# Returns { end_line:, copyrights: [{year:, holder:}], license: }
|
|
52
|
+
# REUSE-IgnoreStart
|
|
53
|
+
# Ruby files typically have:
|
|
54
|
+
# # frozen_string_literal: true
|
|
55
|
+
# (blank line)
|
|
56
|
+
# #--
|
|
57
|
+
# # SPDX-FileCopyrightText: YYYY Name
|
|
58
|
+
# # SPDX-License-Identifier: LICENSE
|
|
59
|
+
# #++
|
|
60
|
+
# REUSE-IgnoreEnd
|
|
61
|
+
|
|
62
|
+
copyrights = []
|
|
63
|
+
license = nil
|
|
64
|
+
header_end = nil
|
|
65
|
+
found_spdx = false
|
|
66
|
+
|
|
67
|
+
lines.each_with_index do |line, i|
|
|
68
|
+
if line =~ /^#\s*SPDX-FileCopyrightText:\s*(\d{4})\s+(.+)$/
|
|
69
|
+
copyrights << { year: $1.to_i, holder: $2.strip }
|
|
70
|
+
found_spdx = true
|
|
71
|
+
# REUSE-IgnoreStart
|
|
72
|
+
elsif line =~ /^#\s*SPDX-License-Identifier:\s*(.+)$/
|
|
73
|
+
# REUSE-IgnoreEnd
|
|
74
|
+
license = $1.strip
|
|
75
|
+
found_spdx = true
|
|
76
|
+
elsif line =~ /^#\+\+\s*$/ && found_spdx
|
|
77
|
+
header_end = i
|
|
78
|
+
break
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
return nil if copyrights.empty? && license.nil?
|
|
83
|
+
|
|
84
|
+
{ end_line: header_end || 0, copyrights:, license: }
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Updates or adds SPDX headers for a single Ruby file.
|
|
88
|
+
#
|
|
89
|
+
# Each file needs correct license headers with accurate copyright years.
|
|
90
|
+
# Processing involves reading, parsing, querying git, and rewriting. This
|
|
91
|
+
# function orchestrates that workflow.
|
|
92
|
+
#
|
|
93
|
+
# [filepath] Path to the Ruby file.
|
|
94
|
+
def process_file(filepath)
|
|
95
|
+
content = File.read(filepath)
|
|
96
|
+
lines = content.lines
|
|
97
|
+
|
|
98
|
+
target_license = license_for_file(filepath)
|
|
99
|
+
|
|
100
|
+
# Get contributors from git for year lookups
|
|
101
|
+
all_contributors = LicenseUtils.get_contributors_for_lines(filepath)
|
|
102
|
+
your_year = LicenseUtils.get_your_latest_year(filepath, YOUR_IDENTIFIERS)
|
|
103
|
+
|
|
104
|
+
existing = parse_existing_header(lines)
|
|
105
|
+
|
|
106
|
+
if existing
|
|
107
|
+
# File has existing header - only update years for EXISTING contributors
|
|
108
|
+
needs_update = false
|
|
109
|
+
updated_copyrights = []
|
|
110
|
+
|
|
111
|
+
existing[:copyrights].each do |c|
|
|
112
|
+
# Find this contributor's latest year from git
|
|
113
|
+
git_year = nil
|
|
114
|
+
all_contributors.each do |contributor, year|
|
|
115
|
+
if c[:holder].split.any? { |word| contributor.include?(word) }
|
|
116
|
+
git_year = [git_year || 0, year].max
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
if git_year && git_year != c[:year]
|
|
121
|
+
puts " Updated #{c[:holder].split.first}'s copyright year: #{c[:year]} -> #{git_year}"
|
|
122
|
+
updated_copyrights << { year: git_year, holder: c[:holder] }
|
|
123
|
+
needs_update = true
|
|
124
|
+
else
|
|
125
|
+
updated_copyrights << c
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Check if YOUR year needs updating (if you're a contributor)
|
|
130
|
+
your_existing = updated_copyrights.find { |c| YOUR_IDENTIFIERS.any? { |id| c[:holder].include?(id) } }
|
|
131
|
+
if your_existing.nil?
|
|
132
|
+
puts " Adding your copyright"
|
|
133
|
+
updated_copyrights << { year: your_year, holder: YOUR_COPYRIGHT }
|
|
134
|
+
needs_update = true
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Check license
|
|
138
|
+
if existing[:license] != target_license
|
|
139
|
+
puts " Fixing license: #{existing[:license]} -> #{target_license}"
|
|
140
|
+
needs_update = true
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
if needs_update
|
|
144
|
+
frozen_string = lines[0].include?("frozen_string_literal") ? lines[0] : nil
|
|
145
|
+
|
|
146
|
+
header_lines = []
|
|
147
|
+
header_lines << "# frozen_string_literal: true\n" unless frozen_string
|
|
148
|
+
header_lines << "\n" if frozen_string.nil? && !lines[0].strip.empty?
|
|
149
|
+
header_lines << "#--\n"
|
|
150
|
+
|
|
151
|
+
# REUSE-IgnoreStart
|
|
152
|
+
updated_copyrights.each do |c|
|
|
153
|
+
header_lines << "# SPDX-FileCopyrightText: #{c[:year]} #{c[:holder]}\n"
|
|
154
|
+
end
|
|
155
|
+
header_lines << "# SPDX-License-Identifier: #{target_license}\n"
|
|
156
|
+
# REUSE-IgnoreEnd
|
|
157
|
+
header_lines << "#++\n"
|
|
158
|
+
|
|
159
|
+
content_start = existing[:end_line] + 1
|
|
160
|
+
while content_start < lines.length && lines[content_start].strip.empty?
|
|
161
|
+
content_start += 1
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
remaining = lines[content_start..]
|
|
165
|
+
|
|
166
|
+
new_content = if frozen_string
|
|
167
|
+
"#{frozen_string}\n#{header_lines.join}\n#{remaining.join}"
|
|
168
|
+
else
|
|
169
|
+
"#{header_lines.join}\n#{remaining.join}"
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
File.write(filepath, new_content)
|
|
173
|
+
puts "Updated: #{filepath}"
|
|
174
|
+
end
|
|
175
|
+
else
|
|
176
|
+
# No header - add one with YOUR copyright only
|
|
177
|
+
frozen_line = lines[0]&.include?("frozen_string_literal") ? lines.shift : nil
|
|
178
|
+
|
|
179
|
+
header = []
|
|
180
|
+
header << "# frozen_string_literal: true\n\n" unless frozen_line
|
|
181
|
+
header << "#--\n"
|
|
182
|
+
# REUSE-IgnoreStart
|
|
183
|
+
header << "# SPDX-FileCopyrightText: #{your_year} #{YOUR_COPYRIGHT}\n"
|
|
184
|
+
header << "# SPDX-License-Identifier: #{target_license}\n"
|
|
185
|
+
# REUSE-IgnoreEnd
|
|
186
|
+
header << "#++\n\n"
|
|
187
|
+
|
|
188
|
+
if frozen_line
|
|
189
|
+
File.write(filepath, "#{frozen_line}\n#{header.join}#{lines.join}")
|
|
190
|
+
else
|
|
191
|
+
File.write(filepath, header.join + lines.join)
|
|
192
|
+
end
|
|
193
|
+
puts "Added header: #{filepath}"
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# Finds Ruby files to process.
|
|
198
|
+
#
|
|
199
|
+
# License automation runs on file sets. Users may specify paths or want all
|
|
200
|
+
# lib/ext/test files. This handles both cases using git ls-files for tracking.
|
|
201
|
+
#
|
|
202
|
+
# [paths] Explicit paths to process, or empty for default directories.
|
|
203
|
+
def find_rb_files(paths)
|
|
204
|
+
if paths.empty?
|
|
205
|
+
# Process all relevant directories
|
|
206
|
+
dirs = %w[lib ext test examples tasks bin sig]
|
|
207
|
+
files = dirs.flat_map do |dir|
|
|
208
|
+
# Include both root files and subdirectory files, for both .rb and .rbs
|
|
209
|
+
%w[rb rbs].flat_map do |ext|
|
|
210
|
+
root_files = `git ls-files '#{dir}/*.#{ext}' 2>/dev/null`.split("\n")
|
|
211
|
+
sub_files = `git ls-files '#{dir}/**/*.#{ext}' 2>/dev/null`.split("\n")
|
|
212
|
+
root_files + sub_files
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
files.uniq
|
|
216
|
+
else
|
|
217
|
+
paths.flat_map do |path|
|
|
218
|
+
if File.directory?(path)
|
|
219
|
+
rb_files = `git ls-files '#{path}/**/*.rb'`.split("\n")
|
|
220
|
+
rbs_files = `git ls-files '#{path}/**/*.rbs'`.split("\n")
|
|
221
|
+
rb_files + rbs_files
|
|
222
|
+
else
|
|
223
|
+
path
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
if __FILE__ == $0
|
|
230
|
+
paths = ARGV.empty? ? [] : ARGV
|
|
231
|
+
files = find_rb_files(paths)
|
|
232
|
+
|
|
233
|
+
files.each do |file|
|
|
234
|
+
process_file(file)
|
|
235
|
+
end
|
|
236
|
+
end
|