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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ddb40f5dac18c21c5661cf6b290eb55247b87666d01a1a4239c97cbde95fc251
4
- data.tar.gz: ac97b21064ffe1658dd62c5cd0df71fb748b2db88829e2f7a71fbd799302a065
3
+ metadata.gz: c39318054ec3245ba579a3c7f14ece0d8749cfd6bcf4209e90338ac48fe0294c
4
+ data.tar.gz: 05cfa41ce39280c39c6a278503a1f89248a2a057b96cda3c9e994dd831840864
5
5
  SHA512:
6
- metadata.gz: 9543258320235373bd87deb1b39aed672c9a2fa1f753f5d39951f4b28db048a2b627e2a9cfbf30df4db5f506d4469e641b2b437394b21192ab2e859914deaa06
7
- data.tar.gz: 54f1d379ba07ef8850f5e23f68d1d3b77e0ac29f01fb3e9b2e5b408e3c2b4c079d88049fb0e1b20f72a3f821d50d32b2a037541bd98accd0cdc41fc585cc16a7
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...main)**
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.5.1...HEAD
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 = own_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, so someone else's reflection is simply not found
151
- # it reveals nothing about what exists.
152
- def own_comment
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
- return own_comment unless Bible270.config.admin?(current_reader)
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, on this day. Anything
32
- # else is ignored rather than refused — the page still renders.
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
- current_reader.comments.where(day: @day, id: params[:edit]).pick(:id)
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
- # Only the writer may change their own words. An admin can remove a reflection
185
- # but not rewrite it: putting words in someone's mouth is worse than taking
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
- signed_in? && comment.reader_id == current_reader.id
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: (b270_can_edit?(comment) ? nil : "Delete #{comment.reader.display_name}'s reflection?") } } %>
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 source = textarea.value.slice(start, finish) || (action === "quote" ? "quoted text" : "list item");
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
- update(editor, textarea, replacement, start, finish, 0, replacement.length);
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))
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Bible270
4
- VERSION = '1.6.0'
4
+ VERSION = '1.6.1'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bible270
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.6.0
4
+ version: 1.6.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew vonderLuft