diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index a52d12ee..140bc030 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -3,4 +3,9 @@ class ApplicationController < ActionController::Base # Only allow modern browsers supporting webp images, web push, badges, import maps, CSS nesting, and CSS :has. allow_browser versions: :modern + + private + def set_active_storage_url_options + ActiveStorage::Current.url_options = { protocol: request.protocol, host: request.host, port: request.port } + end end diff --git a/app/controllers/books_controller.rb b/app/controllers/books_controller.rb index 83a0e113..4267dc1a 100644 --- a/app/controllers/books_controller.rb +++ b/app/controllers/books_controller.rb @@ -23,6 +23,14 @@ def create def show @leaves = @book.leaves.active.with_leafables.positioned + + respond_to do |format| + format.html + format.md do + set_active_storage_url_options + render plain: @book.to_markdown + end + end end def edit diff --git a/app/controllers/leafables_controller.rb b/app/controllers/leafables_controller.rb index bf472599..157d7efc 100644 --- a/app/controllers/leafables_controller.rb +++ b/app/controllers/leafables_controller.rb @@ -16,6 +16,13 @@ def create end def show + respond_to do |format| + format.html + format.md do + set_active_storage_url_options + render plain: @leaf.to_markdown + end + end end def edit diff --git a/app/models/book.rb b/app/models/book.rb index 93de9985..217779cf 100644 --- a/app/models/book.rb +++ b/app/models/book.rb @@ -12,4 +12,11 @@ class Book < ApplicationRecord def press(leafable, leaf_params) leaves.create! leaf_params.merge(leafable: leafable) end + + def to_markdown(url_options: ActiveStorage::Current.url_options) + [ "# #{title}", leaves.active.with_leafables.positioned.map { |leaf| leaf.to_markdown(url_options: url_options) } ] + .flatten + .reject(&:blank?) + .join("\n\n") + end end diff --git a/app/models/leaf.rb b/app/models/leaf.rb index fb7e68be..8b9df012 100644 --- a/app/models/leaf.rb +++ b/app/models/leaf.rb @@ -14,4 +14,8 @@ class Leaf < ApplicationRecord def slug title.parameterize.presence || "-" end + + def to_markdown(url_options: ActiveStorage::Current.url_options) + leafable.to_markdown(title: title, url_options: url_options) + end end diff --git a/app/models/page.rb b/app/models/page.rb index 5b4adba8..c1554932 100644 --- a/app/models/page.rb +++ b/app/models/page.rb @@ -16,6 +16,10 @@ def html_preview rendered_html(markdown_source.first(1024)) end + def to_markdown(title:, url_options: ActiveStorage::Current.url_options) + [ "### #{title}", expand_markdown_urls(markdown_source, url_options: url_options) ].join("\n\n") + end + private def plain_text html_body = rendered_html(markdown_source) @@ -29,4 +33,13 @@ def rendered_html(source) def markdown_source body.content.to_s end + + def expand_markdown_urls(content, url_options:) + return content if url_options.blank? + + content.gsub(%r{(?<=\()\/u\/([^\)\s]+)}) do + slug = Regexp.last_match(1) + Rails.application.routes.url_helpers.action_text_markdown_upload_url(slug, **url_options) + end + end end diff --git a/app/models/picture.rb b/app/models/picture.rb index 1a2acde7..bf155efe 100644 --- a/app/models/picture.rb +++ b/app/models/picture.rb @@ -4,4 +4,16 @@ class Picture < ApplicationRecord has_one_attached :image do |attachable| attachable.variant :large, resize_to_limit: [ 1500, 1500 ] end + + def to_markdown(title:, url_options: ActiveStorage::Current.url_options) + [ "### #{title}", image_markdown(url_options), caption.presence ].compact.join("\n\n") + end + + private + def image_markdown(url_options) + return unless image.attached? + + url = Rails.application.routes.url_helpers.rails_blob_url(image, **url_options) + "" + end end diff --git a/app/models/section.rb b/app/models/section.rb index f86f7046..700656b7 100644 --- a/app/models/section.rb +++ b/app/models/section.rb @@ -4,4 +4,8 @@ class Section < ApplicationRecord def searchable_content body end + + def to_markdown(title:, url_options: ActiveStorage::Current.url_options) + [ "## #{title}", body.to_s ].join("\n\n") + end end diff --git a/test/controllers/books_controller_test.rb b/test/controllers/books_controller_test.rb index 0f911e27..e814e24f 100644 --- a/test/controllers/books_controller_test.rb +++ b/test/controllers/books_controller_test.rb @@ -79,6 +79,19 @@ class BooksControllerTest < ActionDispatch::IntegrationTest assert_response :success end + test "show exports markdown" do + get book_slug_url(books(:handbook), format: :md) + + assert_response :success + assert_includes response.body, "# Handbook" + assert_includes response.body, "## The Welcome Section" + assert_includes response.body, "### Welcome to The Handbook!" + assert_includes response.body, "This is _such_ a great handbook." + + url = rails_blob_url(pictures(:reading).image, host: "www.example.com") + assert_includes response.body, "" + end + test "show includes OG metadata for public access" do get book_slug_url(books(:handbook)) assert_response :success diff --git a/test/controllers/leafables_controller_test.rb b/test/controllers/leafables_controller_test.rb index 9fa0c8ff..ffd8ad03 100644 --- a/test/controllers/leafables_controller_test.rb +++ b/test/controllers/leafables_controller_test.rb @@ -12,6 +12,14 @@ class LeafablesControllerTest < ActionDispatch::IntegrationTest assert_select "p", "This is such a great handbook." end + test "show markdown export" do + get leafable_slug_path(leaves(:welcome_page), format: :md) + + assert_response :success + assert_includes response.body, "### Welcome to The Handbook!" + assert_includes response.body, "This is _such_ a great handbook." + end + test "show with public access to a published book" do sign_out books(:handbook).update!(published: true) diff --git a/test/controllers/pages_controller_test.rb b/test/controllers/pages_controller_test.rb index 0b0ccec8..dfc58a15 100644 --- a/test/controllers/pages_controller_test.rb +++ b/test/controllers/pages_controller_test.rb @@ -12,6 +12,14 @@ class PagesControllerTest < ActionDispatch::IntegrationTest assert_select "h2", text: /Hello/ end + test "show as markdown" do + get leafable_path(leaves(:welcome_page), format: :md) + + assert_response :ok + assert_includes response.body, "### Welcome to The Handbook!" + assert_includes response.body, "This is _such_ a great handbook." + end + test "show sanitizes dangerous content" do get leafable_path(sample_page_leaf(%(
)))