From 36a8e704ed97141be350d87a25ed3f36ae6a099b Mon Sep 17 00:00:00 2001 From: pezholio Date: Mon, 21 Oct 2024 13:23:54 +0100 Subject: [PATCH 1/5] Add an `EmbedExtractor` This allows us to extract embed codes, their uuids and their types when given a block of Govspeak. --- lib/govspeak.rb | 2 ++ lib/govspeak/embed_extractor.rb | 17 +++++++++ lib/govspeak/embedded_content.rb | 15 ++++++++ test/embed_extractor_test.rb | 59 ++++++++++++++++++++++++++++++++ 4 files changed, 93 insertions(+) create mode 100644 lib/govspeak/embed_extractor.rb create mode 100644 lib/govspeak/embedded_content.rb create mode 100644 test/embed_extractor_test.rb diff --git a/lib/govspeak.rb b/lib/govspeak.rb index 16890ed..e2ff764 100644 --- a/lib/govspeak.rb +++ b/lib/govspeak.rb @@ -14,6 +14,8 @@ require "govspeak/html_validator" require "govspeak/html_sanitizer" require "govspeak/blockquote_extra_quote_remover" +require "govspeak/embed_extractor" +require "govspeak/embedded_content" require "govspeak/post_processor" require "govspeak/link_extractor" require "govspeak/template_renderer" diff --git a/lib/govspeak/embed_extractor.rb b/lib/govspeak/embed_extractor.rb new file mode 100644 index 0000000..8dfe00d --- /dev/null +++ b/lib/govspeak/embed_extractor.rb @@ -0,0 +1,17 @@ +module Govspeak + class EmbedExtractor + def initialize(document) + @document = document + end + + def content_references + @content_references ||= @document.scan(EmbeddedContent::EMBED_REGEX).map { |match| + EmbeddedContent.new(document_type: match[1], content_id: match[2], embed_code: match[0]) + }.uniq + end + + def content_ids + @content_ids ||= content_references.map(&:content_id) + end + end +end diff --git a/lib/govspeak/embedded_content.rb b/lib/govspeak/embedded_content.rb new file mode 100644 index 0000000..e5de143 --- /dev/null +++ b/lib/govspeak/embedded_content.rb @@ -0,0 +1,15 @@ +module Govspeak + class EmbeddedContent + SUPPORTED_DOCUMENT_TYPES = %w[contact content_block_email_address].freeze + UUID_REGEX = /([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/ + EMBED_REGEX = /({{embed:(#{SUPPORTED_DOCUMENT_TYPES.join('|')}):#{UUID_REGEX}}})/ + + attr_reader :document_type, :content_id, :embed_code + + def initialize(document_type:, content_id:, embed_code:) + @document_type = document_type + @content_id = content_id + @embed_code = embed_code + end + end +end diff --git a/test/embed_extractor_test.rb b/test/embed_extractor_test.rb new file mode 100644 index 0000000..0afe41e --- /dev/null +++ b/test/embed_extractor_test.rb @@ -0,0 +1,59 @@ +require "test_helper" + +class EmbedExtractorTest < Minitest::Test + extend Minitest::Spec::DSL + + describe "EmbedExtractor" do + subject { Govspeak::EmbedExtractor.new(document) } + + describe "when there is no embedded content" do + let(:document) { "foo" } + + describe "#content_references" do + it "returns an empty array" do + assert_equal [], subject.content_references + end + end + + describe "#content_ids" do + it "returns an empty array" do + assert_equal [], subject.content_ids + end + end + end + + describe "when there is embedded content" do + let(:contact_uuid) { SecureRandom.uuid } + let(:content_block_email_address_uuid) { SecureRandom.uuid } + + let(:document) do + """ + {{embed:contact:#{contact_uuid}}} + {{embed:content_block_email_address:#{content_block_email_address_uuid}}} + """ + end + + describe "#content_references" do + it "returns all references" do + result = subject.content_references + + assert_equal 2, result.count + + assert_equal "contact", result[0].document_type + assert_equal contact_uuid, result[0].content_id + assert_equal "{{embed:contact:#{contact_uuid}}}", result[0].embed_code + + assert_equal "content_block_email_address", result[1].document_type + assert_equal content_block_email_address_uuid, result[1].content_id + assert_equal "{{embed:content_block_email_address:#{content_block_email_address_uuid}}}", result[1].embed_code + end + end + + describe "#content_ids" do + it "returns all uuids as an array" do + assert_equal [contact_uuid, content_block_email_address_uuid], subject.content_ids + end + end + end + end +end From d8577ba9e808cb770d281e7f29a6ce5243199e1b Mon Sep 17 00:00:00 2001 From: pezholio Date: Mon, 21 Oct 2024 14:35:35 +0100 Subject: [PATCH 2/5] Support embeds in Govspeak MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This adds a new `embeds` extenstion, which scans the body for the embed regex and uses the EmbedPresenter to return the appropriate text, depending on the content type, assuming an array of editions has been passed to the `embeds` argument. For now, we’re reimplementing the logic already in the Publishing API (along with some additional classes, IDs etc), but in future we can expand the presenter to support more complex logic. --- lib/govspeak.rb | 11 ++- lib/govspeak/presenters/embed_presenter.rb | 32 +++++++++ test/govspeak_embeds_test.rb | 84 ++++++++++++++++++++++ 3 files changed, 126 insertions(+), 1 deletion(-) create mode 100644 lib/govspeak/presenters/embed_presenter.rb create mode 100644 test/govspeak_embeds_test.rb diff --git a/lib/govspeak.rb b/lib/govspeak.rb index e2ff764..c05b77d 100644 --- a/lib/govspeak.rb +++ b/lib/govspeak.rb @@ -21,6 +21,7 @@ require "govspeak/template_renderer" require "govspeak/presenters/attachment_presenter" require "govspeak/presenters/contact_presenter" +require "govspeak/presenters/embed_presenter" require "govspeak/presenters/h_card_presenter" require "govspeak/presenters/image_presenter" require "govspeak/presenters/attachment_image_presenter" @@ -39,7 +40,7 @@ class Document @extensions = [] attr_accessor :images - attr_reader :attachments, :contacts, :links, :locale + attr_reader :attachments, :contacts, :links, :locale, :embeds def self.to_html(source, options = {}) new(source, options).to_html @@ -59,6 +60,7 @@ def initialize(source, options = {}) @attachments = Array.wrap(options.delete(:attachments)) @links = Array.wrap(options.delete(:links)) @contacts = Array.wrap(options.delete(:contacts)) + @embeds = Array.wrap(options.delete(:embeds)) @locale = options.fetch(:locale, "en") @options = { input: PARSER_CLASS_NAME, sanitize: true, @@ -257,6 +259,13 @@ def insert_strong_inside_p(body, parser = Govspeak::Document) render_image(AttachmentImagePresenter.new(attachment)) end + extension("embeds", Govspeak::EmbeddedContent::EMBED_REGEX) do |_embed_code, _document_type, content_id| + embed = embeds.detect { |e| e[:content_id] == content_id } + next "" unless embed + + EmbedPresenter.new(embed).render + end + # As of version 1.12.0 of Kramdown the block elements (div & figcaption) # inside this html block will have it's < > converted into HTML Entities # when ever this code is used inside block level elements. diff --git a/lib/govspeak/presenters/embed_presenter.rb b/lib/govspeak/presenters/embed_presenter.rb new file mode 100644 index 0000000..d115e70 --- /dev/null +++ b/lib/govspeak/presenters/embed_presenter.rb @@ -0,0 +1,32 @@ +require "action_view" +require "htmlentities" + +module Govspeak + class EmbedPresenter + include ActionView::Helpers::TagHelper + + attr_reader :embed + + def initialize(embed) + @embed = ActiveSupport::HashWithIndifferentAccess.new(embed) + end + + def content_id + embed[:content_id] + end + + def document_type + embed[:document_type] + end + + def render + body = if document_type == "content_block_email_address" + embed.dig(:details, :email_address) + else + embed[:title] + end + + content_tag(:span, body, class: "embed embed-#{document_type}", id: "embed_#{content_id}") + end + end +end diff --git a/test/govspeak_embeds_test.rb b/test/govspeak_embeds_test.rb new file mode 100644 index 0000000..01089fe --- /dev/null +++ b/test/govspeak_embeds_test.rb @@ -0,0 +1,84 @@ +require "test_helper" + +class GovspeakEmbedsTest < Minitest::Test + extend Minitest::Spec::DSL + + def compress_html(html) + html.gsub(/[\n\r]+\s*/, "") + end + + let(:content_id) { SecureRandom.uuid } + + it "renders an email address when present in options[:embeds]" do + embed = { + content_id:, + document_type: "content_block_email_address", + title: "foo", + details: { + email_address: "foo@example.com", + }, + } + govspeak = "{{embed:content_block_email_address:#{content_id}}}" + + rendered = Govspeak::Document.new(govspeak, embeds: [embed]).to_html + + expected = "

#{embed[:details][:email_address]}

" + + assert_equal compress_html(expected), compress_html(rendered) + end + + it "renders the title when the document type is a contact" do + embed = { + content_id:, + document_type: "contact", + title: "foo", + } + govspeak = "{{embed:contact:#{content_id}}}" + + rendered = Govspeak::Document.new(govspeak, embeds: [embed]).to_html + + expected = "

#{embed[:title]}

" + + assert_equal compress_html(expected), compress_html(rendered) + end + + it "ignores missing embeds" do + govspeak = "{{embed:contact:#{content_id}}}" + + rendered = Govspeak::Document.new(govspeak, embeds: []).to_html + + assert_equal compress_html(""), compress_html(rendered) + end + + it "supports multiple embeds" do + embeds = [ + { + content_id: SecureRandom.uuid, + document_type: "contact", + title: "foo", + }, + { + content_id: SecureRandom.uuid, + document_type: "content_block_email_address", + title: "foo", + details: { + email_address: "foo@example.com", + }, + }, + ] + + govspeak = %(Here is a contact: {{embed:contact:#{embeds[0][:content_id]}}} + +Here is an email address: {{embed:content_block_email_address:#{embeds[1][:content_id]}}} + ) + + rendered = Govspeak::Document.new(govspeak, embeds:).to_html + + expected = """ +

Here is a contact: #{embeds[0][:title]}

+

Here is an email address: #{embeds[1][:details][:email_address]}

+ """ + + assert_equal compress_html(expected), compress_html(rendered) + end +end From f9a649cc34c35ea6e71875e1187b1243c8505192 Mon Sep 17 00:00:00 2001 From: pezholio Date: Mon, 21 Oct 2024 14:57:05 +0100 Subject: [PATCH 3/5] Update documentation --- README.md | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/README.md b/README.md index 7a7c088..0720f58 100644 --- a/README.md +++ b/README.md @@ -608,6 +608,34 @@ will output ``` +### Content blocks + +Authors can embed different types of [supported content](https://github.com/alphagov/govspeak/blob/main/lib/govspeak/embedded_content.rb#L3) created by the Content Block Manager + +``` +{{embed:content_block_email_address:d308f561-e5ee-45b5-90b2-3ac36a23fad9}} +``` + +with options provided + +``` +{ + embeds: [ + { + content_id: "d308f561-e5ee-45b5-90b2-3ac36a23fad9", + title: "Government Digital Service", + details: { email_address: "test@example.com" }, + } + ] +} +``` + +will output + +```html +test@example.com +``` + ### Button An accessible way to add button links into content, that can also allow cross domain tracking with [Google Analytics](https://support.google.com/analytics/answer/7372977?hl=en) From 8ab55ddfb3a4b28b4132b6eb9373fe6131bb64d7 Mon Sep 17 00:00:00 2001 From: pezholio Date: Mon, 21 Oct 2024 16:13:29 +0100 Subject: [PATCH 4/5] Update Changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b921dcd..7888208 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +### 8.5.0 + +* Support embeds in Govspeak + ## 8.4.1 * Do not pin version of govuk_publishing_components From 10f37aad424f72c3be198459cfbcca19ada64835 Mon Sep 17 00:00:00 2001 From: pezholio Date: Mon, 21 Oct 2024 16:14:26 +0100 Subject: [PATCH 5/5] Bump version --- lib/govspeak/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/govspeak/version.rb b/lib/govspeak/version.rb index 327d1bb..58236eb 100644 --- a/lib/govspeak/version.rb +++ b/lib/govspeak/version.rb @@ -1,3 +1,3 @@ module Govspeak - VERSION = "8.4.1".freeze + VERSION = "8.5.0".freeze end