diff --git a/Gemfile b/Gemfile index 5901ff05b..c0455d262 100644 --- a/Gemfile +++ b/Gemfile @@ -24,6 +24,7 @@ gem "thruster", require: false gem "bcrypt", "~> 3.1.7" gem "geared_pagination", "~> 1.2" gem "rqrcode" +gem "redcarpet" gem "rouge" gem "jbuilder" gem "actiontext-lexical", github: "basecamp/actiontext-lexical", branch: "initial" @@ -52,4 +53,3 @@ group :test do gem "webmock" gem "vcr" end - diff --git a/Gemfile.lock b/Gemfile.lock index af7a2f4f7..3a0a48e59 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -26,15 +26,16 @@ GIT GIT remote: https://github.com/crmne/ruby_llm.git - revision: 8412d51e869f1ee8e5229831a9a0bb5e9b8f3847 + revision: 87a86f70cb64e23d5b76994edf8080618f07e071 specs: - ruby_llm (1.2.0) + ruby_llm (1.3.0rc1) base64 event_stream_parser (~> 1) - faraday (~> 2) - faraday-multipart (~> 1) - faraday-net_http (~> 3) - faraday-retry (~> 2) + faraday (>= 1.10.0) + faraday-multipart (>= 1) + faraday-net_http (>= 1) + faraday-retry (>= 1) + marcel (~> 1.0) zeitwerk (~> 2) GIT @@ -205,7 +206,7 @@ GEM addressable (>= 2.5.0) globalid (1.2.1) activesupport (>= 6.1) - hashdiff (1.1.2) + hashdiff (1.2.0) i18n (1.14.7) concurrent-ruby (~> 1.0) image_processing (1.14.0) @@ -327,6 +328,7 @@ GEM rdoc (6.14.0) erb psych (>= 4.0.0) + redcarpet (3.6.1) regexp_parser (2.10.0) reline (0.6.1) io-console (~> 0.5) @@ -479,6 +481,7 @@ DEPENDENCIES puma (>= 5.0) rails! rails_structured_logging! + redcarpet rouge rqrcode rubocop-rails-omakase diff --git a/script/convert_markdowns_to_rich_text.rb b/script/convert_markdowns_to_rich_text.rb new file mode 100755 index 000000000..29e71b31b --- /dev/null +++ b/script/convert_markdowns_to_rich_text.rb @@ -0,0 +1,116 @@ +#!/usr/bin/env ruby + +require_relative "../config/environment" +require "redcarpet" +require "nokogiri" + +class ActionText::Markdown < ApplicationRecord + belongs_to :record, polymorphic: true +end + +class MarkdownToActionTextConverter + ATTACHMENT_URL_REGEX = %r{/u/(?[^\/\s\)]+)} + + def initialize(html) + @doc = Nokogiri::HTML::DocumentFragment.parse(html) + @attachments = [] + end + + def convert + process_images + process_links + [ @doc.to_html, @attachments ] + end + + private + + def process_images + @doc.css("img").each do |img| + src = img["src"].presence + if src && match = src.match(ATTACHMENT_URL_REGEX) + if (attachment = find_attachment(match[:slug])) + puts "REEMPLAZO #{build_attachment_node(attachment).to_html}" + img.replace(build_attachment_node(attachment)) + @attachments << attachment + end + end + end + end + + def process_links + @doc.css("a").each do |link| + href = link["href"].presence + + if href && match = href.match(ATTACHMENT_URL_REGEX) + if (attachment = find_attachment(match[:slug])) + link.replace(build_attachment_node(attachment)) + @attachments << attachment + end + end + end + end + + def build_attachment_node(attachment) + html = ActionText::Attachment.from_attachable(attachment).to_html + fragment = Nokogiri::HTML::DocumentFragment.parse(html) + + node = fragment.at_css("action-text-attachment") + node["url"] = Rails.application.routes.url_helpers.rails_blob_path(attachment.blob, only_path: true) + + fragment + end + + def find_attachment(slug) + ActiveStorage::Attachment.find_by(slug: slug) + end +end + +class RedcarpetRenderer + def self.render(markdown) + renderer = Redcarpet::Render::HTML.new + markdowner = Redcarpet::Markdown.new(renderer, + autolink: true, + tables: true, + fenced_code_blocks: true, + strikethrough: true, + superscript: true, + ) + markdowner.render(markdown.to_s) + end +end + +ApplicationRecord.with_each_tenant do |tenant| + puts "Processing tenant: #{tenant}" + + ActionText::Markdown.find_each do |markdown| + next unless markdown.record + + html = RedcarpetRenderer.render(markdown.content.to_s) + converter = MarkdownToActionTextConverter.new(html) + rich_text_html, attachments = converter.convert + + rich_text = ActionText::RichText.create!( + name: markdown.name, + record: markdown.record, + body: rich_text_html + ) + + attachments.each do |attachment| + attachment.update!(record: rich_text) + end + + puts "✓ Created rich text for #{markdown.record_type}##{markdown.record_id} (#{markdown.name})" + rescue => e + warn "✗ Failed to process markdown ##{markdown.id}: #{e.class} - #{e.message}" + end +end + +# create_table "action_text_markdowns", force: :cascade do |t| +# t.text "content", default: "", null: false +# t.datetime "created_at", null: false +# t.string "name", null: false +# t.integer "record_id", null: false +# t.string "record_type", null: false +# t.datetime "updated_at", null: false +# t.index ["record_type", "record_id"], name: "index_action_text_markdowns_on_record" +# end