Skip to content

Commit b641dd8

Browse files
committed
feat(rendering): introduce media rendering classes for images, audio, video, and PDFs
1 parent 8cc50e7 commit b641dd8

File tree

12 files changed

+411
-294
lines changed

12 files changed

+411
-294
lines changed

lib/html2rss/rendering.rb

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# frozen_string_literal: true
2+
3+
module Html2rss
4+
# Namespace for HTML rendering logic, used to generate rich content such as
5+
# images, audio, video, or embedded documents for feed descriptions.
6+
#
7+
# @example
8+
# Html2rss::Rendering::ImageRenderer.new(...).to_html
9+
# Html2rss::Rendering::MediaRenderer.for(...)
10+
#
11+
# @see Html2rss::Rendering::DescriptionBuilder
12+
module Rendering
13+
end
14+
end
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# frozen_string_literal: true
2+
3+
module Html2rss
4+
module Rendering
5+
# Renders an HTML <audio> tag from a URL and title.
6+
class AudioRenderer
7+
def initialize(url:, type:)
8+
@url = url
9+
@type = type
10+
end
11+
12+
def to_html
13+
%(<audio controls preload="none" referrerpolicy="no-referrer" crossorigin="anonymous">
14+
<source src="#{@url}" type="#{@type}">
15+
</audio>)
16+
end
17+
end
18+
end
19+
end
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
# frozen_string_literal: true
2+
3+
require 'cgi'
4+
5+
module Html2rss
6+
module Rendering
7+
# Builds a sanitized article description from the base text, title, and optional media.
8+
class DescriptionBuilder
9+
def initialize(base:, title:, url:, enclosure:, image:)
10+
@base = base.to_s
11+
@title = title
12+
@url = url
13+
@enclosure = enclosure
14+
@image = image
15+
end
16+
17+
def call
18+
fragments = []
19+
fragments << media_renderer&.to_html
20+
fragments << processed_base_description
21+
22+
result = fragments.compact.join("\n").strip
23+
result.empty? ? nil : result
24+
end
25+
26+
private
27+
28+
def media_renderer
29+
MediaRenderer.for(enclosure: @enclosure, image: @image, title: @title)
30+
end
31+
32+
def processed_base_description
33+
text = RssBuilder::Article.remove_pattern_from_start(@base, @title)
34+
Html2rss::Selectors::PostProcessors::SanitizeHtml.get(text, @url)
35+
end
36+
end
37+
38+
# Selects the appropriate media renderer class based on the enclosure type or fallback image.
39+
class MediaRenderer
40+
def self.for(enclosure:, image:, title:)
41+
return ImageRenderer.new(url: image, title:) if enclosure.nil? && image
42+
return nil unless enclosure
43+
44+
new_from_enclosure(enclosure, title)
45+
end
46+
47+
def self.new_from_enclosure(enclosure, title)
48+
case enclosure.type
49+
when %r{^image/}
50+
ImageRenderer.new(url: enclosure.url, title:)
51+
when %r{^video/}
52+
VideoRenderer.new(url: enclosure.url, type: enclosure.type)
53+
when %r{^audio/}
54+
AudioRenderer.new(url: enclosure.url, type: enclosure.type)
55+
when 'application/pdf'
56+
PdfRenderer.new(url: enclosure.url)
57+
end
58+
end
59+
end
60+
61+
# Renders an HTML <img> tag from a URL and title.
62+
class ImageRenderer
63+
def initialize(url:, title:)
64+
@url = url
65+
@title = title
66+
end
67+
68+
def to_html
69+
%(<img src="#{@url}"
70+
alt="#{escaped_title}"
71+
title="#{escaped_title}"
72+
loading="lazy"
73+
referrerpolicy="no-referrer"
74+
decoding="async"
75+
crossorigin="anonymous">).delete("\n").gsub(/\s+/, ' ')
76+
end
77+
78+
private
79+
80+
def escaped_title
81+
CGI.escapeHTML(@title)
82+
end
83+
end
84+
85+
# Renders an HTML <video> tag with nested <source>.
86+
class VideoRenderer
87+
def initialize(url:, type:)
88+
@url = url
89+
@type = type
90+
end
91+
92+
def to_html
93+
%(<video controls preload="none" referrerpolicy="no-referrer" crossorigin="anonymous" playsinline>
94+
<source src="#{@url}" type="#{@type}">
95+
</video>)
96+
end
97+
end
98+
99+
# Renders an HTML <audio> tag with nested <source>.
100+
class AudioRenderer
101+
def initialize(url:, type:)
102+
@url = url
103+
@type = type
104+
end
105+
106+
def to_html
107+
%(<audio controls preload="none" referrerpolicy="no-referrer" crossorigin="anonymous">
108+
<source src="#{@url}" type="#{@type}">
109+
</audio>)
110+
end
111+
end
112+
113+
# Renders an HTML <iframe> tag for embedded PDF.
114+
class PdfRenderer
115+
def initialize(url:)
116+
@url = url
117+
end
118+
119+
def to_html
120+
%(<iframe src="#{@url}" width="100%" height="75vh"
121+
sandbox=""
122+
referrerpolicy="no-referrer"
123+
loading="lazy">
124+
</iframe>)
125+
end
126+
end
127+
end
128+
end
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# frozen_string_literal: true
2+
3+
require 'cgi'
4+
5+
module Html2rss
6+
module Rendering
7+
# Renders an HTML <img> tag from a URL and title.
8+
class ImageRenderer
9+
def initialize(url:, title:)
10+
@url = url
11+
@title = title
12+
end
13+
14+
def to_html
15+
%(<img src="#{@url}"
16+
alt="#{escaped_title}"
17+
title="#{escaped_title}"
18+
loading="lazy"
19+
referrerpolicy="no-referrer"
20+
decoding="async"
21+
crossorigin="anonymous">).delete("\n").gsub(/\s+/, ' ')
22+
end
23+
24+
private
25+
26+
def escaped_title
27+
CGI.escapeHTML(@title)
28+
end
29+
end
30+
end
31+
end
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# frozen_string_literal: true
2+
3+
module Html2rss
4+
module Rendering
5+
# Picks the appropriate media renderer based on the enclosure type or fallback image.
6+
class MediaRenderer
7+
def self.for(enclosure:, image:, title:)
8+
return ImageRenderer.new(url: image, title:) if enclosure.nil? && image
9+
return nil unless enclosure
10+
11+
new_from_enclosure(enclosure, title)
12+
end
13+
14+
def self.new_from_enclosure(enclosure, title)
15+
case enclosure.type
16+
when %r{^image/}
17+
ImageRenderer.new(url: enclosure.url, title:)
18+
when %r{^video/}
19+
VideoRenderer.new(url: enclosure.url, type: enclosure.type)
20+
when %r{^audio/}
21+
AudioRenderer.new(url: enclosure.url, type: enclosure.type)
22+
when 'application/pdf'
23+
PdfRenderer.new(url: enclosure.url)
24+
end
25+
end
26+
end
27+
end
28+
end
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# frozen_string_literal: true
2+
3+
module Html2rss
4+
module Rendering
5+
# Renders an HTML <iframe> for PDF documents.
6+
class PdfRenderer
7+
def initialize(url:)
8+
@url = url
9+
end
10+
11+
def to_html
12+
%(<iframe src="#{@url}" width="100%" height="75vh"
13+
sandbox=""
14+
referrerpolicy="no-referrer"
15+
loading="lazy">
16+
</iframe>)
17+
end
18+
end
19+
end
20+
end
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# frozen_string_literal: true
2+
3+
module Html2rss
4+
module Rendering
5+
# Renders an HTML <video> tag from a URL and type.
6+
class VideoRenderer
7+
def initialize(url:, type:)
8+
@url = url
9+
@type = type
10+
end
11+
12+
def to_html
13+
%(<video controls preload="none" referrerpolicy="no-referrer" crossorigin="anonymous" playsinline>
14+
<source src="#{@url}" type="#{@type}">
15+
</video>)
16+
end
17+
end
18+
end
19+
end

lib/html2rss/rss_builder/article.rb

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -74,9 +74,7 @@ def title
7474
end
7575

7676
def description
77-
return @description if defined?(@description)
78-
79-
@description = DescriptionBuilder.new(
77+
@description ||= Rendering::DescriptionBuilder.new(
8078
base: @to_h[:description],
8179
title:,
8280
url:,

lib/html2rss/rss_builder/description_builder.rb

Lines changed: 0 additions & 96 deletions
This file was deleted.

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy