bible270 1.6.0 → 1.6.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 +8 -2
- data/app/controllers/bible270/comments_controller.rb +8 -10
- data/app/controllers/bible270/days_controller.rb +4 -3
- data/app/helpers/bible270/plan_helper.rb +5 -4
- data/app/views/bible270/comments/_comment.html.erb +1 -1
- data/app/views/bible270/shared/_markdown_toolbar.html.erb +28 -2
- data/lib/bible270/comment_formatter.rb +24 -0
- data/lib/bible270/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c39318054ec3245ba579a3c7f14ece0d8749cfd6bcf4209e90338ac48fe0294c
|
|
4
|
+
data.tar.gz: 05cfa41ce39280c39c6a278503a1f89248a2a057b96cda3c9e994dd831840864
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 759bbd980b0f1994719f0323b487bd86270fc3b0ae6489899c849a807f5cef9e0c528da104881a3602b68ab8d734aa9ff8b507019ebbd427641128bff4f55db6
|
|
7
|
+
data.tar.gz: 7c2c28f53b206dba13947676da4b7c9d2468fcfa7a1e202f2c0850602732fa11a0185462b37a6ee1965000d60dab5b7463b8f8e58a1552bf9ec0ea825c282470
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,10 @@ All notable changes to bible270. Format follows [Keep a Changelog](https://keepa
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
**[Full Changelog](https://github.com/avonderluft/bible270/compare/v1.6.0...main)**
|
|
8
|
+
|
|
9
|
+
## [1.6.0] - 2026-09-14
|
|
10
|
+
|
|
7
11
|
### Added
|
|
8
12
|
|
|
9
13
|
- let administrators sort the reader list by first name, last name, most completed days, or least completed days, and open the Rake task's progress report in a dismissable Reader Stats dialog that follows the current sort selection
|
|
@@ -12,12 +16,13 @@ All notable changes to bible270. Format follows [Keep a Changelog](https://keepa
|
|
|
12
16
|
### Changed
|
|
13
17
|
|
|
14
18
|
- send each broad reflection/reply notification once with all eligible subscribers in BCC, while keeping direct reply and mention notices individually addressed
|
|
19
|
+
- expand regression coverage for registration delivery and failure isolation, legacy reader names, custom footer composition, mention compatibility, and stale completion-animation schema caches
|
|
15
20
|
|
|
16
21
|
### Fixed
|
|
17
22
|
|
|
18
23
|
- normalize boolean column defaults reported by Rails adapters so migration reconciliation and tests work consistently across Rails 7.1 through 8.1
|
|
19
24
|
|
|
20
|
-
**[Full Changelog](https://github.com/avonderluft/bible270/compare/v1.5.1...
|
|
25
|
+
**[Full Changelog](https://github.com/avonderluft/bible270/compare/v1.5.1...v1.6.0)**
|
|
21
26
|
|
|
22
27
|
## [1.5.1] - 2026-09-11
|
|
23
28
|
|
|
@@ -696,7 +701,8 @@ This is the initial public release.
|
|
|
696
701
|
|
|
697
702
|
* Email sign-in requires working Action Mailer delivery in the host application. Set `config.mailer_from`; delivery is inline by default, or set `email_sign_in_deliver_later` when a queue backend is available.
|
|
698
703
|
|
|
699
|
-
[Unreleased]: https://github.com/avonderluft/bible270/compare/v1.
|
|
704
|
+
[Unreleased]: https://github.com/avonderluft/bible270/compare/v1.6.0...HEAD
|
|
705
|
+
[1.6.0]: https://github.com/avonderluft/bible270/releases/tag/v1.6.0
|
|
700
706
|
[1.5.1]: https://github.com/avonderluft/bible270/releases/tag/v1.5.1
|
|
701
707
|
[1.5.0]: https://github.com/avonderluft/bible270/releases/tag/v1.5.0
|
|
702
708
|
[1.4.2]: https://github.com/avonderluft/bible270/releases/tag/v1.4.2
|
|
@@ -52,7 +52,7 @@ module Bible270
|
|
|
52
52
|
def update
|
|
53
53
|
return unless require_reader!
|
|
54
54
|
|
|
55
|
-
@comment =
|
|
55
|
+
@comment = editable_comment
|
|
56
56
|
head :not_found and return unless @comment
|
|
57
57
|
|
|
58
58
|
@day = @comment.day
|
|
@@ -147,18 +147,16 @@ module Bible270
|
|
|
147
147
|
params.require(:comment).permit(*permitted)
|
|
148
148
|
end
|
|
149
149
|
|
|
150
|
-
# Scoped to the reader
|
|
151
|
-
#
|
|
152
|
-
def
|
|
153
|
-
current_reader.comments.find_by(id: params[:id])
|
|
150
|
+
# Scoped to the reader unless they are an administrator, so an ordinary reader
|
|
151
|
+
# cannot use editing or deletion to discover someone else's reflection.
|
|
152
|
+
def editable_comment
|
|
153
|
+
return current_reader.comments.find_by(id: params[:id]) unless Bible270.config.admin?(current_reader)
|
|
154
|
+
|
|
155
|
+
Comment.find_by(id: params[:id])
|
|
154
156
|
end
|
|
155
157
|
|
|
156
|
-
# An admin may remove any reflection; everyone else only their own. Editing
|
|
157
|
-
# stays with the writer either way.
|
|
158
158
|
def deletable_comment
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
Comment.find_by(id: params[:id])
|
|
159
|
+
editable_comment
|
|
162
160
|
end
|
|
163
161
|
end
|
|
164
162
|
end
|
|
@@ -28,12 +28,13 @@ module Bible270
|
|
|
28
28
|
|
|
29
29
|
private
|
|
30
30
|
|
|
31
|
-
# The reflection the reader has asked to edit: their own,
|
|
32
|
-
# else is ignored rather than refused
|
|
31
|
+
# The reflection the reader has asked to edit: their own, or any reflection
|
|
32
|
+
# when they are an administrator. Anything else is ignored rather than refused.
|
|
33
33
|
def editing_comment_id
|
|
34
34
|
return nil if params[:edit].blank? || current_reader.nil?
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
comments = Bible270.config.admin?(current_reader) ? Comment.all : current_reader.comments
|
|
37
|
+
comments.where(day: @day, id: params[:edit]).pick(:id)
|
|
37
38
|
end
|
|
38
39
|
|
|
39
40
|
# The reflection being replied to, if the link carried one and it is a
|
|
@@ -181,11 +181,12 @@ module Bible270
|
|
|
181
181
|
local&.strftime('%b %-d, %Y at %-I:%M %p')
|
|
182
182
|
end
|
|
183
183
|
|
|
184
|
-
#
|
|
185
|
-
#
|
|
186
|
-
# them away, and taking them away is already the moderator's job.
|
|
184
|
+
# Writers may revise their own words; administrators may also correct or
|
|
185
|
+
# moderate any reflection without changing its recorded author.
|
|
187
186
|
def b270_can_edit?(comment)
|
|
188
|
-
|
|
187
|
+
return false unless signed_in?
|
|
188
|
+
|
|
189
|
+
comment.reader_id == current_reader.id || Bible270.config.admin?(current_reader)
|
|
189
190
|
end
|
|
190
191
|
|
|
191
192
|
def b270_can_delete?(comment)
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
<%= button_to "delete", comment_path(comment), method: :delete, class: "b270-cdel",
|
|
26
26
|
form: { style: "display:inline",
|
|
27
27
|
data: { turbo_stream: true,
|
|
28
|
-
turbo_confirm: (
|
|
28
|
+
turbo_confirm: (comment.reader_id == current_reader.id ? nil : "Delete #{comment.reader.display_name}'s reflection?") } } %>
|
|
29
29
|
<% end %>
|
|
30
30
|
</div>
|
|
31
31
|
|
|
@@ -59,14 +59,39 @@
|
|
|
59
59
|
const start = textarea.value.lastIndexOf("\n", Math.max(selectionStart - 1, 0)) + 1;
|
|
60
60
|
const nextNewline = textarea.value.indexOf("\n", selectionEnd);
|
|
61
61
|
const finish = nextNewline === -1 ? textarea.value.length : nextNewline;
|
|
62
|
-
const
|
|
62
|
+
const existing = textarea.value.slice(start, finish);
|
|
63
|
+
const placeholder = action === "quote" ? "quoted text" : "list item";
|
|
64
|
+
const source = existing || placeholder;
|
|
63
65
|
const lines = source.split("\n");
|
|
64
66
|
const replacement = lines.map((line, index) => {
|
|
65
67
|
if (action === "quote") return `> ${line}`;
|
|
66
68
|
if (action === "number-list") return `${index + 1}. ${line}`;
|
|
67
69
|
return `- ${line}`;
|
|
68
70
|
}).join("\n");
|
|
69
|
-
|
|
71
|
+
const selectListPlaceholder = !existing && action.endsWith("-list");
|
|
72
|
+
const selectedStart = selectListPlaceholder ? replacement.indexOf(placeholder) : 0;
|
|
73
|
+
const selectedEnd = selectListPlaceholder ? selectedStart + placeholder.length : replacement.length;
|
|
74
|
+
update(editor, textarea, replacement, start, finish, selectedStart, selectedEnd);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function continueList(editor, textarea, event) {
|
|
78
|
+
if (event.key !== "Enter" || event.isComposing || event.shiftKey || event.ctrlKey ||
|
|
79
|
+
event.metaKey || event.altKey || textarea.selectionStart !== textarea.selectionEnd) return false;
|
|
80
|
+
|
|
81
|
+
const caret = textarea.selectionStart;
|
|
82
|
+
const lineStart = textarea.value.lastIndexOf("\n", Math.max(caret - 1, 0)) + 1;
|
|
83
|
+
const beforeCaret = textarea.value.slice(lineStart, caret);
|
|
84
|
+
const bullet = beforeCaret.match(/^(\s*)([-+*])\s+/);
|
|
85
|
+
const numbered = beforeCaret.match(/^(\s*)(\d+)([.)])\s+/);
|
|
86
|
+
let marker;
|
|
87
|
+
if (bullet) marker = `${bullet[1]}${bullet[2]} `;
|
|
88
|
+
else if (numbered) marker = `${numbered[1]}${Number(numbered[2]) + 1}${numbered[3]} `;
|
|
89
|
+
else return false;
|
|
90
|
+
|
|
91
|
+
event.preventDefault();
|
|
92
|
+
const insertion = `\n${marker}`;
|
|
93
|
+
update(editor, textarea, insertion, caret, caret, insertion.length, insertion.length);
|
|
94
|
+
return true;
|
|
70
95
|
}
|
|
71
96
|
|
|
72
97
|
function addLink(editor, textarea) {
|
|
@@ -212,6 +237,7 @@
|
|
|
212
237
|
formattingToggle.focus();
|
|
213
238
|
});
|
|
214
239
|
textarea.addEventListener("keydown", (event) => {
|
|
240
|
+
if (continueList(editor, textarea, event)) return;
|
|
215
241
|
if (!(event.ctrlKey || event.metaKey) || event.altKey) return;
|
|
216
242
|
const action = event.key.toLocaleLowerCase() === "b" ? "bold" :
|
|
217
243
|
event.key.toLocaleLowerCase() === "i" ? "italic" : null;
|
|
@@ -17,6 +17,7 @@ module Bible270
|
|
|
17
17
|
ALLOWED_TAGS = %w[p br strong em blockquote ul ol li a].freeze
|
|
18
18
|
ALLOWED_ATTRIBUTES = %w[href rel].freeze
|
|
19
19
|
LINK_REL = 'nofollow ugc noopener'
|
|
20
|
+
URL_PATTERN = %r{https?://[^\s<>"]*[^\s<>".,;:!?)\]\}]}i
|
|
20
21
|
SKIP_MENTIONS_IN = %w[a code pre].freeze
|
|
21
22
|
|
|
22
23
|
module_function
|
|
@@ -26,6 +27,7 @@ module Bible270
|
|
|
26
27
|
rendered = format == MARKDOWN ? markdown_html(source) : plain_html(source)
|
|
27
28
|
fragment = fragment_for(sanitize(rendered))
|
|
28
29
|
hard_wrap(fragment) if format == MARKDOWN
|
|
30
|
+
link_urls(fragment) if format == MARKDOWN
|
|
29
31
|
decorate_links(fragment)
|
|
30
32
|
link_mentions(fragment, &mention_path) if mention_path
|
|
31
33
|
fragment.to_html
|
|
@@ -94,6 +96,28 @@ module Bible270
|
|
|
94
96
|
end
|
|
95
97
|
private_class_method :hard_wrap
|
|
96
98
|
|
|
99
|
+
def link_urls(fragment)
|
|
100
|
+
fragment.xpath('.//text()').to_a.each do |node|
|
|
101
|
+
next if node.ancestors.any? { |ancestor| SKIP_MENTIONS_IN.include?(ancestor.name) }
|
|
102
|
+
next unless node.text.match?(URL_PATTERN)
|
|
103
|
+
|
|
104
|
+
replacement = Nokogiri::HTML::DocumentFragment.parse('')
|
|
105
|
+
cursor = 0
|
|
106
|
+
node.text.to_enum(:scan, URL_PATTERN).each do
|
|
107
|
+
match = Regexp.last_match
|
|
108
|
+
replacement.add_child(Nokogiri::XML::Text.new(node.text[cursor...match.begin(0)], fragment.document))
|
|
109
|
+
link = Nokogiri::XML::Node.new('a', fragment.document)
|
|
110
|
+
link['href'] = match[0]
|
|
111
|
+
link.content = match[0]
|
|
112
|
+
replacement.add_child(link)
|
|
113
|
+
cursor = match.end(0)
|
|
114
|
+
end
|
|
115
|
+
replacement.add_child(Nokogiri::XML::Text.new(node.text[cursor..].to_s, fragment.document))
|
|
116
|
+
node.replace(replacement)
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
private_class_method :link_urls
|
|
120
|
+
|
|
97
121
|
def decorate_links(fragment)
|
|
98
122
|
fragment.css('a:not([href])').each do |link|
|
|
99
123
|
link.replace(Nokogiri::XML::Text.new(link.text, fragment.document))
|
data/lib/bible270/version.rb
CHANGED