ratatui_ruby-devtools 0.4.0 → 0.4.1
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 +4 -4
- data/CHANGELOG.md +16 -0
- data/lib/ratatui_ruby/devtools/tasks/license/copyright.rb +21 -2
- data/lib/ratatui_ruby/devtools/tasks/license/markdown_comment.rb +26 -0
- data/lib/ratatui_ruby/devtools/tasks/license/markdown_document.rb +33 -0
- data/lib/ratatui_ruby/devtools/tasks/license/markdown_header_syntax.rb +2 -2
- data/lib/ratatui_ruby/devtools/tasks/license/markdown_snippet_syntax.rb +41 -83
- data/lib/ratatui_ruby/devtools/tasks/license/quoted_snippet.rb +26 -0
- data/lib/ratatui_ruby/devtools/tasks/license/rdoc_snippet_syntax.rb +32 -52
- data/lib/ratatui_ruby/devtools/tasks/license/ruby_comment_header.rb +2 -2
- data/lib/ratatui_ruby/devtools/tasks/license/rust_header_syntax.rb +2 -2
- data/lib/ratatui_ruby/devtools/tasks/license/snippet.rb +12 -5
- data/lib/ratatui_ruby/devtools/tasks/license/snippet_opening.rb +7 -2
- data/lib/ratatui_ruby/devtools/tasks/license/snippet_revision.rb +26 -9
- data/lib/ratatui_ruby/devtools/tasks/license/unwrapping.rb +20 -0
- data/lib/ratatui_ruby/devtools/tasks/license/wrapped_snippet.rb +21 -5
- data/lib/ratatui_ruby/devtools/version.rb +1 -1
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7c9a5440c5c524c14e18b914b7d5666c97f0e65384aac9952e5b8051d4459b44
|
|
4
|
+
data.tar.gz: f13332c056f0fd2becec90b040fc4f57c170e692efded028f9d49390874711c0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: fec0e188fc6ff2f9386c571f942e4838d855212dbdc9d18763fc4aca146ea07f742cf1f9867c1e74d0024ab907bd4c4821d6a2d42233166b6cdf65b10943abc9
|
|
7
|
+
data.tar.gz: 5e81a6e80457bd82db93223473b0091b7746f162f6669c88a23df40c1efecd5e951818554840f7120317fec0edc2368f37b8b31142a6210ac09009b02153ce1c
|
data/CHANGELOG.md
CHANGED
|
@@ -21,6 +21,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
21
21
|
|
|
22
22
|
### Removed
|
|
23
23
|
|
|
24
|
+
## [0.4.1] - 2026-10-24
|
|
25
|
+
|
|
26
|
+
### Added
|
|
27
|
+
|
|
28
|
+
### Changed
|
|
29
|
+
|
|
30
|
+
### Fixed
|
|
31
|
+
|
|
32
|
+
- **Year ranges in copyright notices were misread**: A notice with a range of years, such as `2016-2022 Jane Doe`, didn't count as a notice. A license header kept it but added a second notice for the same holder, and a snippet's renewed tags dropped it. A range now renews from its end, as in `2016-2024 Jane Doe`.
|
|
33
|
+
- **Snippet tasks relicensed quoted code**: `rake license:snippets:all` rewrapped a Markdown snippet under any license but MIT-0 as yours, and relicensed an RDoc one MIT-0, though a license like MIT requires keeping the quoted authors' notices. Such snippets now keep their notices and license.
|
|
34
|
+
- **Incomplete Markdown snippets lost their notices**: `rake license:snippets:md` rewrapped a Markdown snippet missing its license or copyright tag from git blame, dropping notices git can't see, such as code a contributor brought from elsewhere. It now completes the snippet's tags in place, as it already did for RDoc examples.
|
|
35
|
+
- **Stray comments after renewing Markdown snippets**: Renewing a Markdown snippet whose tags share one comment with `SPDX-SnippetBegin` left a stray `<!--` above the new wrapper. A snippet with a comment for each tag was rewrapped, leaving its old copyright comment behind. Both now renew in place.
|
|
36
|
+
|
|
37
|
+
### Removed
|
|
38
|
+
|
|
24
39
|
## [0.4.0] - 2026-10-23
|
|
25
40
|
|
|
26
41
|
### Added
|
|
@@ -156,6 +171,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
156
171
|
### Removed
|
|
157
172
|
|
|
158
173
|
[Unreleased]: https://github.com/setdef/ratatui_ruby-devtools/tree/HEAD
|
|
174
|
+
[0.4.1]: https://github.com/setdef/ratatui_ruby-devtools/releases/tag/v0.4.1
|
|
159
175
|
[0.4.0]: https://github.com/setdef/ratatui_ruby-devtools/releases/tag/v0.4.0
|
|
160
176
|
[0.3.0]: https://github.com/setdef/ratatui_ruby-devtools/releases/tag/v0.3.0
|
|
161
177
|
[0.2.1]: https://github.com/setdef/ratatui_ruby-devtools/releases/tag/v0.2.1
|
|
@@ -8,7 +8,22 @@
|
|
|
8
8
|
require "date"
|
|
9
9
|
require_relative "renewal"
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
# A notice's year is its latest. A range keeps the year it starts from, which
|
|
12
|
+
# is +since+.
|
|
13
|
+
class Copyright < Data.define(:year, :holder, :since) # :nodoc:
|
|
14
|
+
# Non-capturing, because the syntaxes read the holder from the group after
|
|
15
|
+
# it.
|
|
16
|
+
YEARS = /\d{4}(?:\s*-\s*\d{4})?/
|
|
17
|
+
|
|
18
|
+
def initialize(year:, holder:, since: nil)
|
|
19
|
+
super
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def self.parse(years, holder)
|
|
23
|
+
since, year = years.scan(/\d{4}/).map(&:to_i).minmax
|
|
24
|
+
new(year:, holder: holder.strip, since: (since if since < year))
|
|
25
|
+
end
|
|
26
|
+
|
|
12
27
|
def self.of(contribution)
|
|
13
28
|
new(year: contribution.year, holder: contribution.author.to_s)
|
|
14
29
|
end
|
|
@@ -42,6 +57,10 @@ class Copyright < Data.define(:year, :holder) # :nodoc:
|
|
|
42
57
|
end
|
|
43
58
|
|
|
44
59
|
def to_s
|
|
45
|
-
|
|
60
|
+
if since
|
|
61
|
+
"#{since}-#{year} #{holder}"
|
|
62
|
+
else
|
|
63
|
+
"#{year} #{holder}"
|
|
64
|
+
end
|
|
46
65
|
end
|
|
47
66
|
end
|
|
@@ -0,0 +1,26 @@
|
|
|
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
|
+
require_relative "snippet"
|
|
9
|
+
|
|
10
|
+
# An HTML comment in a Markdown document, from the line it opens on to the
|
|
11
|
+
# line it closes on, counting from 0.
|
|
12
|
+
class MarkdownComment < Data.define(:first, :last, :text) # :nodoc:
|
|
13
|
+
def span
|
|
14
|
+
first..last
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def holds_line?(index)
|
|
18
|
+
index.between?(first, last)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# SPDX tags that go with a wrapper's SnippetBegin, rather than opening or
|
|
22
|
+
# closing a wrapper of their own.
|
|
23
|
+
def tags?
|
|
24
|
+
text.include?("SPDX-") && !text.include?(Snippet::BEGIN_TAG) && !text.include?(Snippet::END_TAG)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
6
6
|
#++
|
|
7
7
|
|
|
8
|
+
require_relative "markdown_comment"
|
|
9
|
+
|
|
8
10
|
class MarkdownDocument < Data.define(:lines) # :nodoc:
|
|
9
11
|
FENCE = /^(````*)\w*$/
|
|
10
12
|
|
|
@@ -12,6 +14,32 @@ class MarkdownDocument < Data.define(:lines) # :nodoc:
|
|
|
12
14
|
lines[index].match?(FENCE)
|
|
13
15
|
end
|
|
14
16
|
|
|
17
|
+
def comment_opens?(index)
|
|
18
|
+
lines.fetch(index, "").include?("<!--")
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def comment_at(index)
|
|
22
|
+
last = comment_close(index)
|
|
23
|
+
MarkdownComment.new(first: index, last:, text: lines[index..last].join)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# The comment that line +index+ sits in, which can open on a line above it.
|
|
27
|
+
# A line in no comment stands alone, so prose after it stays out.
|
|
28
|
+
def comment_around(index)
|
|
29
|
+
first = index.downto(0).find { |earlier| comment_opens?(earlier) }
|
|
30
|
+
comment = first && comment_at(first)
|
|
31
|
+
if comment&.holds_line?(index)
|
|
32
|
+
comment
|
|
33
|
+
else
|
|
34
|
+
MarkdownComment.new(first: index, last: index, text: lines[index])
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# The comment that opens on the line after +comment+ closes, if one does.
|
|
39
|
+
def comment_after(comment)
|
|
40
|
+
comment_at(comment.last + 1) if comment_opens?(comment.last + 1)
|
|
41
|
+
end
|
|
42
|
+
|
|
15
43
|
# The line that closes the fence opening at +index+, when one does.
|
|
16
44
|
def fence_end(index)
|
|
17
45
|
marker = lines[index][FENCE, 1]
|
|
@@ -20,4 +48,9 @@ class MarkdownDocument < Data.define(:lines) # :nodoc:
|
|
|
20
48
|
closing = /^#{Regexp.escape(marker)}$/
|
|
21
49
|
(index + 1).upto(lines.length - 1).find { |later| lines[later].match?(closing) }
|
|
22
50
|
end
|
|
51
|
+
|
|
52
|
+
# A comment that never closes runs to the end of the document.
|
|
53
|
+
private def comment_close(index)
|
|
54
|
+
index.upto(lines.length - 1).find { |later| lines[later].include?("-->") } || (lines.length - 1)
|
|
55
|
+
end
|
|
23
56
|
end
|
|
@@ -15,7 +15,7 @@ require_relative "spdx_header"
|
|
|
15
15
|
class MarkdownHeaderSyntax # :nodoc:
|
|
16
16
|
LICENSE = "CC-BY-SA-4.0"
|
|
17
17
|
# REUSE-IgnoreStart
|
|
18
|
-
COPYRIGHT = /SPDX-FileCopyrightText:\s*(
|
|
18
|
+
COPYRIGHT = /SPDX-FileCopyrightText:\s*(#{Copyright::YEARS})\s+(.+)$/
|
|
19
19
|
LICENSE_LINE = /SPDX-License-Identifier:\s*(.+)$/
|
|
20
20
|
# REUSE-IgnoreEnd
|
|
21
21
|
|
|
@@ -46,7 +46,7 @@ class MarkdownHeaderSyntax # :nodoc:
|
|
|
46
46
|
return MissingHeader.new unless closing
|
|
47
47
|
|
|
48
48
|
comment = lines[..closing]
|
|
49
|
-
copyrights = comment.grep(COPYRIGHT) { Copyright.
|
|
49
|
+
copyrights = comment.grep(COPYRIGHT) { Copyright.parse($1, $2) }
|
|
50
50
|
license = comment.grep(LICENSE_LINE) { $1.strip }.last
|
|
51
51
|
return MissingHeader.new unless copyrights.any? || license
|
|
52
52
|
|
|
@@ -29,31 +29,37 @@ class MarkdownSnippetSyntax # :nodoc:
|
|
|
29
29
|
LICENSE
|
|
30
30
|
end
|
|
31
31
|
|
|
32
|
-
#
|
|
33
|
-
# held can be wrapped again, and with the copyright notices of the rest
|
|
34
|
-
# under the snippet tag.
|
|
35
|
-
def cleaned(lines)
|
|
36
|
-
_, removals = scan(lines, first_pass: true)
|
|
37
|
-
lines = lines.dup
|
|
38
|
-
removals.sort_by { |first, _| -first }.each do |first, last|
|
|
39
|
-
lines.delete_at(last)
|
|
40
|
-
(opening_end(lines, first) - first + 1).times { lines.delete_at(first) }
|
|
41
|
-
end
|
|
42
|
-
lines.each_index.select { |index| lines[index].include?("SPDX-SnippetBegin") }.each do |first|
|
|
43
|
-
first.upto(opening_end(lines, first)) { |index| lines[index] = Snippet.tag(lines[index]) }
|
|
44
|
-
end
|
|
45
|
-
lines
|
|
46
|
-
end
|
|
47
|
-
|
|
32
|
+
# A wrapper with no end leaves the rest of the document alone.
|
|
48
33
|
def snippets_in(lines)
|
|
49
|
-
|
|
50
|
-
|
|
34
|
+
document = MarkdownDocument.new(lines)
|
|
35
|
+
snippets = []
|
|
36
|
+
index = 0
|
|
37
|
+
while index < lines.length
|
|
38
|
+
if lines[index].include?(Snippet::BEGIN_TAG)
|
|
39
|
+
finish = index.upto(lines.length - 1).find { |later| lines[later].include?(Snippet::END_TAG) }
|
|
40
|
+
return snippets unless finish
|
|
41
|
+
|
|
42
|
+
closing = document.comment_around(finish).span
|
|
43
|
+
snippets.concat(wrapped_at(lines, document, document.comment_around(index).first, closing))
|
|
44
|
+
index = closing.last + 1
|
|
45
|
+
next
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
region = sync_region(lines, document, index) || fence_region(document, index)
|
|
49
|
+
if region
|
|
50
|
+
snippets << region
|
|
51
|
+
index = region.last + 1
|
|
52
|
+
else
|
|
53
|
+
index += 1
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
snippets
|
|
51
57
|
end
|
|
52
58
|
|
|
53
59
|
def opening(_snippet, copyrights)
|
|
54
60
|
# REUSE-IgnoreStart
|
|
55
61
|
[
|
|
56
|
-
"<!--
|
|
62
|
+
"<!-- #{Snippet::BEGIN_TAG} -->\n",
|
|
57
63
|
"<!--\n",
|
|
58
64
|
*copyrights.map { |copyright| " #{Snippet::COPYRIGHT_TAG}: #{copyright}\n" },
|
|
59
65
|
" SPDX-License-Identifier: #{LICENSE}\n",
|
|
@@ -63,64 +69,26 @@ class MarkdownSnippetSyntax # :nodoc:
|
|
|
63
69
|
end
|
|
64
70
|
|
|
65
71
|
def closing(_snippet)
|
|
66
|
-
#
|
|
67
|
-
["<!-- SPDX-SnippetEnd -->\n"]
|
|
68
|
-
# REUSE-IgnoreEnd
|
|
72
|
+
["<!-- #{Snippet::END_TAG} -->\n"]
|
|
69
73
|
end
|
|
70
74
|
|
|
71
|
-
# The last line of the opening
|
|
72
|
-
#
|
|
73
|
-
#
|
|
74
|
-
private def opening_end(
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
lines[first].include?("-->") && lines[first + 1]&.include?("<!--")
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
# A wrapper from its SnippetBegin to its SnippetEnd, with the code between
|
|
84
|
-
# its opening tags and its end.
|
|
85
|
-
private def wrapped_region(lines, first, finish)
|
|
86
|
-
opening = SnippetOpening.in(lines, first, opening_end(lines, first))
|
|
87
|
-
return if opening.last + 1 > finish - 1
|
|
88
|
-
|
|
89
|
-
WrappedSnippet.new(snippet: Snippet.around(first, finish, code: [opening.last + 1, finish - 1]), opening:)
|
|
75
|
+
# The last line of the opening at +first+: the comment holding its
|
|
76
|
+
# SnippetBegin, then each comment after it holding tags, whether one holds
|
|
77
|
+
# them all or each tag has its own.
|
|
78
|
+
private def opening_end(document, first)
|
|
79
|
+
comment = document.comment_at(first)
|
|
80
|
+
while (tags = document.comment_after(comment))&.tags?
|
|
81
|
+
comment = tags
|
|
82
|
+
end
|
|
83
|
+
comment.last
|
|
90
84
|
end
|
|
91
85
|
|
|
92
|
-
# The
|
|
93
|
-
#
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
removals = []
|
|
99
|
-
index = 0
|
|
100
|
-
while index < lines.length
|
|
101
|
-
if lines[index].include?("SPDX-SnippetBegin")
|
|
102
|
-
finish = index.upto(lines.length - 1).find { |later| lines[later].include?("SPDX-SnippetEnd") }
|
|
103
|
-
if !first_pass
|
|
104
|
-
wrapped = wrapped_region(lines, index, finish) if finish
|
|
105
|
-
regions << wrapped if wrapped
|
|
106
|
-
index = (finish || lines.length) + 1
|
|
107
|
-
next
|
|
108
|
-
elsif finish
|
|
109
|
-
removals << [index, finish] unless licensed?(lines, index)
|
|
110
|
-
index = finish + 1
|
|
111
|
-
next
|
|
112
|
-
end
|
|
113
|
-
end
|
|
114
|
-
|
|
115
|
-
region = sync_region(lines, document, index) || fence_region(document, index)
|
|
116
|
-
if region
|
|
117
|
-
regions << region
|
|
118
|
-
index = region.last + 1
|
|
119
|
-
else
|
|
120
|
-
index += 1
|
|
121
|
-
end
|
|
122
|
-
end
|
|
123
|
-
[regions, removals]
|
|
86
|
+
# The wrapper opening at +first+ and closing on the lines of +closing+. One
|
|
87
|
+
# around nothing has no code to credit, so it stays as it is.
|
|
88
|
+
private def wrapped_at(lines, document, first, closing)
|
|
89
|
+
opening = SnippetOpening.in(lines, first, opening_end(document, first))
|
|
90
|
+
snippet = Snippet.around(opening.last + 1, closing.first - 1)
|
|
91
|
+
snippet.empty? ? [] : [WrappedSnippet.found(snippet:, opening:, closing:, license: LICENSE)]
|
|
124
92
|
end
|
|
125
93
|
|
|
126
94
|
private def sync_region(lines, document, start)
|
|
@@ -146,14 +114,4 @@ class MarkdownSnippetSyntax # :nodoc:
|
|
|
146
114
|
finish = document.fence_end(start)
|
|
147
115
|
Snippet.around(start, finish) if finish
|
|
148
116
|
end
|
|
149
|
-
|
|
150
|
-
# A wrapper that already credits someone under MIT-0 stays, whoever it
|
|
151
|
-
# credits.
|
|
152
|
-
private def licensed?(lines, begin_line)
|
|
153
|
-
tags = lines[(begin_line + 1)..].take_while { |line| !line.include?("-->") }
|
|
154
|
-
# REUSE-IgnoreStart
|
|
155
|
-
tags.any? { |line| line.include?(Snippet::COPYRIGHT_TAG) || line.include?(Snippet::FILE_COPYRIGHT_TAG) } &&
|
|
156
|
-
tags.any? { |line| line.include?(LICENSE) && line.include?("SPDX-License-Identifier") }
|
|
157
|
-
# REUSE-IgnoreEnd
|
|
158
|
-
end
|
|
159
117
|
end
|
|
@@ -0,0 +1,26 @@
|
|
|
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
|
+
# Code quoted from elsewhere under its own license, such as MIT, which
|
|
9
|
+
# requires keeping its notices as they are, whatever the code looks like.
|
|
10
|
+
class QuotedSnippet < Data.define(:snippet, :opening) # :nodoc:
|
|
11
|
+
def first
|
|
12
|
+
snippet.first
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def covers?(other)
|
|
16
|
+
snippet.covers?(other)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def around_prose(_contents)
|
|
20
|
+
self
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def revise(revision)
|
|
24
|
+
revision.retag(opening)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -23,30 +23,16 @@ class RdocSnippetSyntax # :nodoc:
|
|
|
23
23
|
["*.rb"]
|
|
24
24
|
end
|
|
25
25
|
|
|
26
|
-
# Existing wrappers with their copyright notices under the snippet tag,
|
|
27
|
-
# without the blank lines that once followed their code inside them, and
|
|
28
|
-
# only around text RDoc shows as code.
|
|
29
|
-
def cleaned(lines)
|
|
30
|
-
in_tags = false
|
|
31
|
-
tagged = lines.map do |line|
|
|
32
|
-
in_tags = true if line.match?(/^\s*# SPDX-SnippetBegin\s*$/)
|
|
33
|
-
cleaned_line = in_tags ? Snippet.tag(line) : line
|
|
34
|
-
in_tags = false if line.match?(/^\s*#\+\+\s*$/)
|
|
35
|
-
cleaned_line
|
|
36
|
-
end
|
|
37
|
-
without_wrappers_around_prose(blanks_after_wrappers(tagged))
|
|
38
|
-
end
|
|
39
|
-
|
|
40
26
|
def license
|
|
41
27
|
LICENSE
|
|
42
28
|
end
|
|
43
29
|
|
|
44
|
-
#
|
|
45
|
-
# it. RDoc finds the examples that have no wrapper yet.
|
|
30
|
+
# RDoc finds the examples, and the ones no wrapper covers are bare.
|
|
46
31
|
def snippets_in(lines)
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
32
|
+
examples = examples(lines)
|
|
33
|
+
wrappers = wrapped_in(lines)
|
|
34
|
+
bare = examples.reject { |example| wrappers.any? { |wrapper| wrapper.covers?(example) } }
|
|
35
|
+
(wrappers.map { |wrapper| as_read(wrapper, examples) } + bare).sort_by(&:first)
|
|
50
36
|
end
|
|
51
37
|
|
|
52
38
|
def opening(snippet, copyrights)
|
|
@@ -54,7 +40,7 @@ class RdocSnippetSyntax # :nodoc:
|
|
|
54
40
|
# REUSE-IgnoreStart
|
|
55
41
|
[
|
|
56
42
|
"#{prefix}#--\n",
|
|
57
|
-
"#{prefix}#
|
|
43
|
+
"#{prefix}# #{Snippet::BEGIN_TAG}\n",
|
|
58
44
|
*copyrights.map { |copyright| "#{prefix}# #{Snippet::COPYRIGHT_TAG}: #{copyright}\n" },
|
|
59
45
|
"#{prefix}# SPDX-License-Identifier: #{LICENSE}\n",
|
|
60
46
|
"#{prefix}#++\n",
|
|
@@ -64,56 +50,50 @@ class RdocSnippetSyntax # :nodoc:
|
|
|
64
50
|
|
|
65
51
|
def closing(snippet)
|
|
66
52
|
prefix = snippet.prefix
|
|
67
|
-
# REUSE-IgnoreStart
|
|
68
53
|
[
|
|
69
54
|
"#{prefix}#--\n",
|
|
70
|
-
"#{prefix}#
|
|
55
|
+
"#{prefix}# #{Snippet::END_TAG}\n",
|
|
71
56
|
"#{prefix}#++\n",
|
|
72
57
|
]
|
|
73
|
-
# REUSE-IgnoreEnd
|
|
74
58
|
end
|
|
75
59
|
|
|
76
60
|
private def examples(lines)
|
|
77
61
|
RdocComment.all_in(lines).flat_map(&:examples)
|
|
78
62
|
end
|
|
79
63
|
|
|
80
|
-
# The
|
|
64
|
+
# The wrapper from each SnippetBegin to its SnippetEnd. Its code runs from
|
|
65
|
+
# the #++ to the last line before the SnippetEnd that isn't a blank comment,
|
|
66
|
+
# since RDoc ends an example there.
|
|
81
67
|
private def wrapped_in(lines)
|
|
82
68
|
lines.each_index.filter_map do |index|
|
|
83
|
-
next unless lines[index].match?(/^\s*#
|
|
69
|
+
next unless lines[index].match?(/^\s*# #{Regexp.escape(Snippet::BEGIN_TAG)}\s*$/)
|
|
84
70
|
|
|
85
71
|
prefix = lines[index][/\A\s*/]
|
|
86
72
|
opened = (index + 1).upto(lines.length - 1).find { |line| lines[line].match?(/^#{Regexp.escape(prefix)}#\+\+\s*$/) }
|
|
87
|
-
ended = opened && (opened + 1).upto(lines.length - 1).find { |line| lines[line].match?(/^\s*#
|
|
88
|
-
next unless ended
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
73
|
+
ended = opened && (opened + 1).upto(lines.length - 1).find { |line| lines[line].match?(/^\s*# #{Regexp.escape(Snippet::END_TAG)}\s*$/) }
|
|
74
|
+
next unless ended
|
|
75
|
+
|
|
76
|
+
code_end = ended - 2
|
|
77
|
+
code_end -= 1 while code_end > opened && lines[code_end].match?(/^\s*#\s*$/)
|
|
78
|
+
next unless code_end > opened
|
|
79
|
+
|
|
80
|
+
WrappedSnippet.found(
|
|
81
|
+
snippet: Snippet.around(opened + 1, code_end, prefix:),
|
|
82
|
+
opening: SnippetOpening.in(lines, index - 1, opened),
|
|
83
|
+
closing: (ended - 1)..(ended + 1),
|
|
84
|
+
license: LICENSE
|
|
85
|
+
)
|
|
92
86
|
end
|
|
93
87
|
end
|
|
94
88
|
|
|
95
|
-
# Wrappers once went around every indented
|
|
96
|
-
#
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
unwrapped.slice!(wrapped.opening.span)
|
|
104
|
-
end
|
|
105
|
-
end
|
|
106
|
-
|
|
107
|
-
# Blank comment lines after an example aren't part of it, as RDoc reads it.
|
|
108
|
-
private def blanks_after_wrappers(lines)
|
|
109
|
-
lines = lines.dup
|
|
110
|
-
lines.each_index do |index|
|
|
111
|
-
next unless lines[index].match?(/^\s*#--\s*$/) && lines[index + 1]&.match?(/^\s*# SPDX-SnippetEnd\s*$/)
|
|
112
|
-
|
|
113
|
-
first_blank = index
|
|
114
|
-
first_blank -= 1 while first_blank.positive? && lines[first_blank - 1].match?(/^\s*#\s*$/)
|
|
115
|
-
lines.insert(first_blank + 3, *lines.slice!(first_blank...index)) if first_blank < index
|
|
89
|
+
# The wrapper as RDoc reads it. Wrappers once went around every indented
|
|
90
|
+
# comment, list items' text included, so one that doesn't start at an
|
|
91
|
+
# example wraps prose.
|
|
92
|
+
private def as_read(wrapper, examples)
|
|
93
|
+
if examples.any? { |example| example.first == wrapper.first }
|
|
94
|
+
wrapper
|
|
95
|
+
else
|
|
96
|
+
wrapper.around_prose(examples.select { |example| wrapper.covers?(example) })
|
|
116
97
|
end
|
|
117
|
-
lines
|
|
118
98
|
end
|
|
119
99
|
end
|
|
@@ -14,7 +14,7 @@ require_relative "spdx_header"
|
|
|
14
14
|
# #++ that keep a Ruby header out of the docs.
|
|
15
15
|
module RubyCommentHeader # :nodoc:
|
|
16
16
|
# REUSE-IgnoreStart
|
|
17
|
-
COPYRIGHT = /^#\s*SPDX-FileCopyrightText:\s*(
|
|
17
|
+
COPYRIGHT = /^#\s*SPDX-FileCopyrightText:\s*(#{Copyright::YEARS})\s+(.+)$/
|
|
18
18
|
LICENSE = /^#\s*SPDX-License-Identifier:\s*(.+)$/
|
|
19
19
|
# REUSE-IgnoreEnd
|
|
20
20
|
|
|
@@ -27,7 +27,7 @@ module RubyCommentHeader # :nodoc:
|
|
|
27
27
|
lines.each_with_index do |line, index|
|
|
28
28
|
case line
|
|
29
29
|
when COPYRIGHT
|
|
30
|
-
copyrights << Copyright.
|
|
30
|
+
copyrights << Copyright.parse($1, $2)
|
|
31
31
|
last_tag = index
|
|
32
32
|
when LICENSE
|
|
33
33
|
license = $1.strip
|
|
@@ -15,7 +15,7 @@ class RustHeaderSyntax # :nodoc:
|
|
|
15
15
|
include DirectoryLicensing
|
|
16
16
|
|
|
17
17
|
# REUSE-IgnoreStart
|
|
18
|
-
COPYRIGHT = %r{^//\s*SPDX-FileCopyrightText:\s*(
|
|
18
|
+
COPYRIGHT = %r{^//\s*SPDX-FileCopyrightText:\s*(#{Copyright::YEARS})\s+(.+)$}
|
|
19
19
|
LICENSE = %r{^//\s*SPDX-License-Identifier:\s*(.+)$}
|
|
20
20
|
# REUSE-IgnoreEnd
|
|
21
21
|
|
|
@@ -34,7 +34,7 @@ class RustHeaderSyntax # :nodoc:
|
|
|
34
34
|
lines.each_with_index do |line, index|
|
|
35
35
|
case line
|
|
36
36
|
when COPYRIGHT
|
|
37
|
-
copyrights << Copyright.
|
|
37
|
+
copyrights << Copyright.parse($1, $2)
|
|
38
38
|
last_tag = index
|
|
39
39
|
when LICENSE
|
|
40
40
|
license = $1.strip
|
|
@@ -9,9 +9,12 @@
|
|
|
9
9
|
# counting from 0, behind the given comment prefix. Its year comes from the
|
|
10
10
|
# code lines alone, which can sit inside markers the wrapper goes around.
|
|
11
11
|
class Snippet < Data.define(:first, :last, :prefix, :code) # :nodoc:
|
|
12
|
+
# REUSE-IgnoreStart
|
|
13
|
+
BEGIN_TAG = "SPDX-SnippetBegin"
|
|
14
|
+
END_TAG = "SPDX-SnippetEnd"
|
|
15
|
+
|
|
12
16
|
# REUSE 3.3 requires copyright notices inside a snippet to use the snippet
|
|
13
17
|
# tag. Earlier wrappers used the file tag.
|
|
14
|
-
# REUSE-IgnoreStart
|
|
15
18
|
COPYRIGHT_TAG = "SPDX-SnippetCopyrightText"
|
|
16
19
|
FILE_COPYRIGHT_TAG = "SPDX-FileCopyrightText"
|
|
17
20
|
# REUSE-IgnoreEnd
|
|
@@ -30,12 +33,16 @@ class Snippet < Data.define(:first, :last, :prefix, :code) # :nodoc:
|
|
|
30
33
|
code.map { |line| line + 1 }
|
|
31
34
|
end
|
|
32
35
|
|
|
33
|
-
def
|
|
34
|
-
|
|
36
|
+
def empty?
|
|
37
|
+
last < first
|
|
35
38
|
end
|
|
36
39
|
|
|
37
|
-
def
|
|
38
|
-
|
|
40
|
+
def covers?(other)
|
|
41
|
+
other.first.between?(first, last)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def lines_in(file_lines)
|
|
45
|
+
file_lines[first..last]
|
|
39
46
|
end
|
|
40
47
|
|
|
41
48
|
def revise(revision)
|
|
@@ -13,17 +13,22 @@ require_relative "spdx_header"
|
|
|
13
13
|
# line counting from 0, with the notices they hold.
|
|
14
14
|
class SnippetOpening < Data.define(:first, :last, :header) # :nodoc:
|
|
15
15
|
# REUSE-IgnoreStart
|
|
16
|
-
COPYRIGHT = /SPDX-(?:Snippet|File)CopyrightText:\s*(
|
|
16
|
+
COPYRIGHT = /SPDX-(?:Snippet|File)CopyrightText:\s*(#{Copyright::YEARS})\s+(.+?)\s*(?:-->)?\s*$/
|
|
17
17
|
LICENSE = /SPDX-License-Identifier:\s*(\S+)/
|
|
18
18
|
# REUSE-IgnoreEnd
|
|
19
19
|
|
|
20
20
|
def self.in(lines, first, last)
|
|
21
21
|
tags = lines[first..last]
|
|
22
|
-
copyrights = tags.grep(COPYRIGHT) { Copyright.
|
|
22
|
+
copyrights = tags.grep(COPYRIGHT) { Copyright.parse($1, $2) }
|
|
23
23
|
license = tags.grep(LICENSE) { $1 }.last
|
|
24
24
|
new(first:, last:, header: SpdxHeader.new(copyrights:, license:))
|
|
25
25
|
end
|
|
26
26
|
|
|
27
|
+
# Tags under a license other than +license+ quote someone else's code.
|
|
28
|
+
def quotes?(license)
|
|
29
|
+
!header.license.nil? && header.license != license
|
|
30
|
+
end
|
|
31
|
+
|
|
27
32
|
def span
|
|
28
33
|
first..last
|
|
29
34
|
end
|
|
@@ -7,22 +7,22 @@
|
|
|
7
7
|
|
|
8
8
|
require_relative "blame"
|
|
9
9
|
require_relative "copyright"
|
|
10
|
+
require_relative "snippet"
|
|
10
11
|
|
|
11
12
|
# One file's snippets brought up to date: each bare snippet asks to be
|
|
12
|
-
# wrapped,
|
|
13
|
-
#
|
|
13
|
+
# wrapped, each wrapped one to be tidied and reopened, and each wrapper that
|
|
14
|
+
# comes off to be removed.
|
|
14
15
|
class SnippetRevision # :nodoc:
|
|
15
16
|
def initialize(path:, text:, syntax:, author:)
|
|
16
17
|
@path = path
|
|
17
18
|
@original = text
|
|
18
19
|
@syntax = syntax
|
|
19
20
|
@author = author
|
|
20
|
-
@lines =
|
|
21
|
+
@lines = text.lines
|
|
21
22
|
@snippets = syntax.snippets_in(@lines)
|
|
22
23
|
@notices = []
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
@blame = Blame.new(path:, contents: @lines.join)
|
|
24
|
+
@wrapped = 0
|
|
25
|
+
@blame = Blame.new(path:, contents: text)
|
|
26
26
|
end
|
|
27
27
|
|
|
28
28
|
# Revises from the last snippet back, so the earlier ones stay on the lines
|
|
@@ -44,11 +44,10 @@ class SnippetRevision # :nodoc:
|
|
|
44
44
|
def messages
|
|
45
45
|
return [] unless changed?
|
|
46
46
|
|
|
47
|
-
|
|
48
|
-
summary = if wrapped.zero?
|
|
47
|
+
summary = if @wrapped.zero?
|
|
49
48
|
"Updated: #{@path}"
|
|
50
49
|
else
|
|
51
|
-
"Updated: #{@path} (#{wrapped} code block(s))"
|
|
50
|
+
"Updated: #{@path} (#{@wrapped} code block(s))"
|
|
52
51
|
end
|
|
53
52
|
[*@notices.map { |notice| " #{notice}" }, summary]
|
|
54
53
|
end
|
|
@@ -65,6 +64,7 @@ class SnippetRevision # :nodoc:
|
|
|
65
64
|
copyrights = contributions(snippet).map { |contribution| Copyright.of(contribution) }.sort_by(&:holder)
|
|
66
65
|
@lines.insert(snippet.last + 1, *@syntax.closing(snippet))
|
|
67
66
|
@lines.insert(snippet.first, *@syntax.opening(snippet, copyrights))
|
|
67
|
+
@wrapped += 1
|
|
68
68
|
end
|
|
69
69
|
|
|
70
70
|
def reopen(snippet, renewal)
|
|
@@ -72,4 +72,21 @@ class SnippetRevision # :nodoc:
|
|
|
72
72
|
@lines[opening.span] = @syntax.opening(snippet, opening.copyrights)
|
|
73
73
|
@notices.concat(renewal.notices)
|
|
74
74
|
end
|
|
75
|
+
|
|
76
|
+
# A wrapper ends right after its code, so blank lines between them move
|
|
77
|
+
# below it.
|
|
78
|
+
def close(snippet, closing)
|
|
79
|
+
after = snippet.last + 1
|
|
80
|
+
between = @lines[after, closing.first - after]
|
|
81
|
+
@lines[after, closing.last + 1 - after] = @lines[closing] + between
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# REUSE 3.3 wants the snippet tag where older wrappers used the file tag.
|
|
85
|
+
def retag(opening)
|
|
86
|
+
@lines[opening.span] = @lines[opening.span].map { |line| Snippet.tag(line) }
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def remove(span)
|
|
90
|
+
@lines[span] = []
|
|
91
|
+
end
|
|
75
92
|
end
|
|
@@ -0,0 +1,20 @@
|
|
|
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
|
+
# A wrapper that comes off, with the snippets it held, which are revised on
|
|
9
|
+
# their own.
|
|
10
|
+
class Unwrapping < Data.define(:opening, :closing, :contents) # :nodoc:
|
|
11
|
+
def first
|
|
12
|
+
opening.first
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def revise(revision)
|
|
16
|
+
revision.remove(closing)
|
|
17
|
+
contents.reverse_each { |snippet| snippet.revise(revision) }
|
|
18
|
+
revision.remove(opening)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -5,25 +5,41 @@
|
|
|
5
5
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
6
6
|
#++
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
require_relative "quoted_snippet"
|
|
9
|
+
require_relative "unwrapping"
|
|
10
|
+
|
|
11
|
+
# A Snippet that already has its wrapper, opened by its SnippetOpening and
|
|
12
|
+
# closed on the lines of +closing+.
|
|
13
|
+
class WrappedSnippet < Data.define(:snippet, :opening, :closing) # :nodoc:
|
|
14
|
+
# Quoted code keeps its notices; the rest is ours to renew.
|
|
15
|
+
def self.found(snippet:, opening:, closing:, license:)
|
|
16
|
+
if opening.quotes?(license)
|
|
17
|
+
QuotedSnippet.new(snippet:, opening:)
|
|
18
|
+
else
|
|
19
|
+
new(snippet:, opening:, closing:)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
10
23
|
def first
|
|
11
24
|
snippet.first
|
|
12
25
|
end
|
|
13
26
|
|
|
14
27
|
def covers?(other)
|
|
15
|
-
|
|
28
|
+
snippet.covers?(other)
|
|
16
29
|
end
|
|
17
30
|
|
|
18
31
|
def blame_range
|
|
19
32
|
snippet.blame_range
|
|
20
33
|
end
|
|
21
34
|
|
|
22
|
-
|
|
23
|
-
|
|
35
|
+
# One of ours around prose comes off, with the examples it held.
|
|
36
|
+
def around_prose(contents)
|
|
37
|
+
Unwrapping.new(opening: opening.span, closing:, contents:)
|
|
24
38
|
end
|
|
25
39
|
|
|
26
40
|
def revise(revision)
|
|
41
|
+
revision.close(snippet, closing)
|
|
42
|
+
revision.retag(opening)
|
|
27
43
|
renewal = opening.renewal(revision.contributions(self), revision.license)
|
|
28
44
|
revision.reopen(snippet, renewal) if renewal.changed?
|
|
29
45
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ratatui_ruby-devtools
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.4.
|
|
4
|
+
version: 0.4.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Kerrick Long
|
|
@@ -220,11 +220,13 @@ files:
|
|
|
220
220
|
- lib/ratatui_ruby/devtools/tasks/license/header_revision.rb
|
|
221
221
|
- lib/ratatui_ruby/devtools/tasks/license/licensed_file.rb
|
|
222
222
|
- lib/ratatui_ruby/devtools/tasks/license/licensing.rb
|
|
223
|
+
- lib/ratatui_ruby/devtools/tasks/license/markdown_comment.rb
|
|
223
224
|
- lib/ratatui_ruby/devtools/tasks/license/markdown_document.rb
|
|
224
225
|
- lib/ratatui_ruby/devtools/tasks/license/markdown_files.rb
|
|
225
226
|
- lib/ratatui_ruby/devtools/tasks/license/markdown_header_syntax.rb
|
|
226
227
|
- lib/ratatui_ruby/devtools/tasks/license/markdown_snippet_syntax.rb
|
|
227
228
|
- lib/ratatui_ruby/devtools/tasks/license/missing_header.rb
|
|
229
|
+
- lib/ratatui_ruby/devtools/tasks/license/quoted_snippet.rb
|
|
228
230
|
- lib/ratatui_ruby/devtools/tasks/license/rbs_files.rb
|
|
229
231
|
- lib/ratatui_ruby/devtools/tasks/license/rbs_header_syntax.rb
|
|
230
232
|
- lib/ratatui_ruby/devtools/tasks/license/rdoc_comment.rb
|
|
@@ -245,6 +247,7 @@ files:
|
|
|
245
247
|
- lib/ratatui_ruby/devtools/tasks/license/spdx_header.rb
|
|
246
248
|
- lib/ratatui_ruby/devtools/tasks/license/unknown_file.rb
|
|
247
249
|
- lib/ratatui_ruby/devtools/tasks/license/unlicensed_files.rb
|
|
250
|
+
- lib/ratatui_ruby/devtools/tasks/license/unwrapping.rb
|
|
248
251
|
- lib/ratatui_ruby/devtools/tasks/license/wrapped_snippet.rb
|
|
249
252
|
- lib/ratatui_ruby/devtools/tasks/license_tasks.rb
|
|
250
253
|
- lib/ratatui_ruby/devtools/tasks/lint/gem_lockfile.rb
|