From ca1bba378db99e7688e3c64ce5461fcf2987789a Mon Sep 17 00:00:00 2001 From: Alteras1 <42795314+Alteras1@users.noreply.github.com.> Date: Wed, 20 Aug 2025 08:44:43 -0700 Subject: [PATCH 1/3] Minimum working roll on post --- app/model/rollmaster/roll.rb | 14 +++ db/.gitkeep | 0 .../20250811165607_create_rollmaster_rolls.rb | 15 +++ lib/rollmaster/handle_cooked_post_process.rb | 111 +++++++++++++++++- plugin.rb | 4 +- 5 files changed, 140 insertions(+), 4 deletions(-) create mode 100644 app/model/rollmaster/roll.rb delete mode 100644 db/.gitkeep create mode 100644 db/migrate/20250811165607_create_rollmaster_rolls.rb diff --git a/app/model/rollmaster/roll.rb b/app/model/rollmaster/roll.rb new file mode 100644 index 0000000..083d401 --- /dev/null +++ b/app/model/rollmaster/roll.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module ::Rollmaster + class Roll < ActiveRecord::Base + self.table_name = "rollmaster_rolls" + + # let Post to Roll association occur via the cooked text. + # Roll to Post association will be explicit for backtracking (i.e. auditing). + belongs_to :post + validates :raw, presence: true + validates :notation, presence: true + validates :result, presence: true + end +end diff --git a/db/.gitkeep b/db/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/db/migrate/20250811165607_create_rollmaster_rolls.rb b/db/migrate/20250811165607_create_rollmaster_rolls.rb new file mode 100644 index 0000000..c472740 --- /dev/null +++ b/db/migrate/20250811165607_create_rollmaster_rolls.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class CreateRollmasterRolls < ActiveRecord::Migration[7.2] + def change + create_table :rollmaster_rolls do |t| + t.integer :post_id + t.string :raw + t.string :notation + t.string :result + + t.timestamps + end + add_index :rollmaster_rolls, :post_id + end +end diff --git a/lib/rollmaster/handle_cooked_post_process.rb b/lib/rollmaster/handle_cooked_post_process.rb index b3ea412..3f410ef 100644 --- a/lib/rollmaster/handle_cooked_post_process.rb +++ b/lib/rollmaster/handle_cooked_post_process.rb @@ -1,15 +1,120 @@ # frozen_string_literal: true module ::Rollmaster + SELECTOR_QUERY = ".bb-rollmaster[data-notation]" + class HandleCookedPostProcess def self.process(doc, post) # Add your processing logic here - # parse the post content + # { raw, dom }[] + roll_elements = [] + + # parse the post content for rolls and flatten + doc + .css(SELECTOR_QUERY) + .each do |roll_element| + original_notation = roll_element.attribute("data-notation").value + + next if original_notation.blank? + + original_notation + .split(/\n/) + .each do |notation| + if notation.strip.empty? + roll_elements << { raw: "", dom: roll_element } # let us keep empty lines + else + roll_elements << { raw: notation, dom: roll_element } + end + end + roll_element.content = "" # clear the original notation + end + + return if roll_elements.empty? + + # { raw, dom, formatted, result, error }[] + roll_elements.each do |element| + notation = element[:raw] + next if notation.blank? + + element.merge!(process_roll(notation)) + end + + # { raw, dom, formatted, result, error, id }[] + match_rolls(roll_elements, post) if post.id? + save_rolls(roll_elements, post) - # check for existing dice rolls associated with post + p roll_elements + roll_elements + .group_by { |e| e[:dom] } + .each do |dom, rolls| + p "\n\n HELLO WORLD \n\n" + p dom + content = + rolls.map do |e| + if e[:error] + e[:raw] + elsif e[:raw].empty? + "" + else + e[:raw] + ": " + e[:result] # TODO: consider decorating with spans + end + end + p content + dom.content = CGI.unescapeHTML(content.join("\n")) + dom["data-roll-id"] = rolls.map { |e| e[:id] }.join(",") if rolls.any? { |e| e[:id] } + end + + true + end + + def self.process_roll(notation) + begin + formatted = Rollmaster::DiceEngine.format_notation(notation).first + final = Rollmaster::DiceEngine.roll(notation).first + { error: false, formatted: formatted, result: final } + rescue Rollmaster::DiceEngine::RollError => e + Rails.logger.warn("Rollmaster: Error formatting notation for post #{post.id}: #{e.message}") + { error: true, formatted: nil, result: e.message } + end + end + + def self.match_rolls(rolls, post) + existing_rolls = Rollmaster::Roll.where(post_id: post.id).to_a + return if existing_rolls.empty? + + rolls + .reject { |r| r[:raw].empty? || r[:error] } + .each do |roll| + existing_roll_idx = + existing_rolls.index { |r| r.raw == roll[:raw] || r.notation == roll[:formatted] } + next if existing_roll_idx.nil? + + existing_roll = existing_rolls[existing_roll_idx] + roll[:id] = existing_roll.id + roll[:result] = existing_roll.result # use existing roll result + existing_rolls.delete_at(existing_roll_idx) + end + end - # create new dice rolls for any new ones + def self.save_rolls(rolls, post) + rolls + .reject { |r| r[:raw].empty? || r[:error] } + .each do |roll| + if roll[:id] + existing_roll = Rollmaster::Roll.find(roll[:id]) + existing_roll.update!(raw: roll[:raw], notation: roll[:formatted]) + else + new_roll = + Rollmaster::Roll.create!( + post_id: post.id, + raw: roll[:raw], + notation: roll[:formatted], + result: roll[:result], + ) + roll[:id] = new_roll.id + end + end end end end diff --git a/plugin.rb b/plugin.rb index bad6c33..9e88b40 100644 --- a/plugin.rb +++ b/plugin.rb @@ -26,6 +26,8 @@ module ::Rollmaster # I don't think this is needed, but it doesn't hurt to be safe ::Rollmaster::DiceEngine.reset_context - on(:post_process_cooked) { |doc, post| ::Rollmaster::HandleCookedPostProcess.process(doc, post) } + on(:before_post_process_cooked) do |doc, post| + ::Rollmaster::HandleCookedPostProcess.process(doc, post) if SiteSetting.rollmaster_enabled + end # TODO: consider :chat_message_processed as well end From f5174e29984aab9f13d661634a6df042a04bced7 Mon Sep 17 00:00:00 2001 From: Alteras1 <42795314+Alteras1@users.noreply.github.com.> Date: Wed, 24 Sep 2025 14:24:38 -0700 Subject: [PATCH 2/3] Add unit tests --- lib/rollmaster/handle_cooked_post_process.rb | 4 -- plugin.rb | 1 + spec/integration/bbcode_spec.rb | 71 ++++++++++++++++++++ 3 files changed, 72 insertions(+), 4 deletions(-) create mode 100644 spec/integration/bbcode_spec.rb diff --git a/lib/rollmaster/handle_cooked_post_process.rb b/lib/rollmaster/handle_cooked_post_process.rb index 3f410ef..b63cef7 100644 --- a/lib/rollmaster/handle_cooked_post_process.rb +++ b/lib/rollmaster/handle_cooked_post_process.rb @@ -44,12 +44,9 @@ def self.process(doc, post) match_rolls(roll_elements, post) if post.id? save_rolls(roll_elements, post) - p roll_elements roll_elements .group_by { |e| e[:dom] } .each do |dom, rolls| - p "\n\n HELLO WORLD \n\n" - p dom content = rolls.map do |e| if e[:error] @@ -60,7 +57,6 @@ def self.process(doc, post) e[:raw] + ": " + e[:result] # TODO: consider decorating with spans end end - p content dom.content = CGI.unescapeHTML(content.join("\n")) dom["data-roll-id"] = rolls.map { |e| e[:id] }.join(",") if rolls.any? { |e| e[:id] } end diff --git a/plugin.rb b/plugin.rb index 9e88b40..9d86e74 100644 --- a/plugin.rb +++ b/plugin.rb @@ -29,5 +29,6 @@ module ::Rollmaster on(:before_post_process_cooked) do |doc, post| ::Rollmaster::HandleCookedPostProcess.process(doc, post) if SiteSetting.rollmaster_enabled end + # TODO: consider :chat_message_processed as well end diff --git a/spec/integration/bbcode_spec.rb b/spec/integration/bbcode_spec.rb new file mode 100644 index 0000000..75a3e14 --- /dev/null +++ b/spec/integration/bbcode_spec.rb @@ -0,0 +1,71 @@ +# frozen_string_literal: true + +RSpec.describe "Rollmaster BBCode integration", type: :integration do + before do + enable_current_plugin + Jobs.run_immediately! + end + + it "processes [roll] BBCode and creates a Roll record" do + post = Fabricate(:post, raw: <<~MD) + [roll]2d6[/roll] + MD + post.save + cpp = CookedPostProcessor.new(post) + cpp.post_process + + roll = ::Rollmaster::Roll.find_by(post_id: post.id) + + expect(roll).not_to be_nil + expect(roll.post_id).to eq(post.id) + expect(roll.raw).to eq("2d6") + expect(cpp.html).to include("data-roll-id=\"#{roll.id}\"") + end + + it "handles multiple [roll] BBCode in a single post" do + post = Fabricate(:post, raw: <<~MD) + Here are some rolls: + [roll]1d20[/roll] + [roll]3d8+2[/roll] + [roll]4d6kh3[/roll] + MD + post.save + cpp = CookedPostProcessor.new(post) + cpp.post_process + + rolls = ::Rollmaster::Roll.where(post_id: post.id).to_a + + expect(rolls.size).to eq(3) + expect(rolls.map(&:raw)).to contain_exactly("1d20", "3d8+2", "4d6kh3") + rolls.each { |roll| expect(cpp.html).to include("data-roll-id=\"#{roll.id}\"") } + end + + it "reuses existing rolls when a post is edited" do + post = Fabricate(:post, raw: <<~MD) + Initial roll: [roll]1d6[/roll] + MD + post.save + cpp = CookedPostProcessor.new(post) + cpp.post_process + + initial_roll = ::Rollmaster::Roll.find_by(post_id: post.id) + expect(initial_roll).not_to be_nil + expect(initial_roll.raw).to eq("1d6") + + # Edit the post to change the roll + post.raw = <<~MD + Updated rolls: + [roll]1d6[/roll] + [roll]1d4+1[/roll] + MD + post.save + cpp = CookedPostProcessor.new(post) + cpp.post_process + + rolls = ::Rollmaster::Roll.where(post_id: post.id).to_a + expect(rolls.size).to eq(2) + expect(rolls.map(&:raw)).to contain_exactly("1d6", "1d4+1") + expect(initial_roll.id).in?(rolls.map(&:id)) + rolls.each { |roll| expect(cpp.html).to include("data-roll-id=\"#{roll.id}\"") } + end +end From 2d4513c37af7e2dd7707a251958ad57ff8aa72ea Mon Sep 17 00:00:00 2001 From: Alteras1 <42795314+Alteras1@users.noreply.github.com.> Date: Wed, 24 Sep 2025 14:32:40 -0700 Subject: [PATCH 3/3] Add schema info --- app/model/rollmaster/roll.rb | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/app/model/rollmaster/roll.rb b/app/model/rollmaster/roll.rb index 083d401..2396146 100644 --- a/app/model/rollmaster/roll.rb +++ b/app/model/rollmaster/roll.rb @@ -12,3 +12,18 @@ class Roll < ActiveRecord::Base validates :result, presence: true end end + +# == Schema Information +# +# Table name: rollmaster_rolls +# +# id :bigint not null, primary key +# post_id :integer +# raw :string +# notation :string +# result :string +# created_at :datetime not null +# updated_at :datetime not null +# +# Indexes +# index_rollmaster_rolls_on_post_id (post_id)