From 6ea001417544ab71e484cf38191aed3671ce6dfd Mon Sep 17 00:00:00 2001
From: Gil Desmarais
Date: Sat, 17 Aug 2024 15:18:18 +0200
Subject: [PATCH 01/25] feat: add development? env check
---
app.rb | 7 +------
helpers/{error_handlers.rb => handle_error.rb} | 4 +++-
2 files changed, 4 insertions(+), 7 deletions(-)
rename helpers/{error_handlers.rb => handle_error.rb} (93%)
diff --git a/app.rb b/app.rb
index 37976e93..bbd46412 100644
--- a/app.rb
+++ b/app.rb
@@ -12,12 +12,7 @@ module Web
#
# It is built with [Roda](https://roda.jeremyevans.net/).
class App < Roda
- # TODO: move to helper
- def self.development?
- ENV['RACK_ENV'] == 'development'
- end
-
- def development? = self.class.development?
+ def self.development? = ENV['RACK_ENV'] == 'development'
opts[:check_dynamic_arity] = false
opts[:check_arity] = :warn
diff --git a/helpers/error_handlers.rb b/helpers/handle_error.rb
similarity index 93%
rename from helpers/error_handlers.rb
rename to helpers/handle_error.rb
index e5f93236..6571abe9 100644
--- a/helpers/error_handlers.rb
+++ b/helpers/handle_error.rb
@@ -19,11 +19,13 @@ def handle_error(error) # rubocop:disable Metrics/MethodLength
set_error_response('Internal Server Error', 500)
end
- @show_backtrace = ENV.fetch('RACK_ENV', nil) == 'development'
+ @show_backtrace = self.class.development?
@error = error
view 'error'
end
+ private
+
def set_error_response(page_title, status)
@page_title = page_title
response.status = status
From 1ffeb5054c6d383f658dd1ad76627a4743dced78 Mon Sep 17 00:00:00 2001
From: Gil Desmarais
Date: Sat, 17 Aug 2024 15:59:13 +0200
Subject: [PATCH 02/25] feat: expose endpoint which calls H2r.auto_source
---
Gemfile | 1 +
Gemfile.lock | 2 ++
README.md | 4 ++++
helpers/handle_error.rb | 2 ++
routes/auto_source.rb | 43 +++++++++++++++++++++++++++++++++++++++++
5 files changed, 52 insertions(+)
create mode 100644 routes/auto_source.rb
diff --git a/Gemfile b/Gemfile
index 87c56a70..ff80ee1d 100644
--- a/Gemfile
+++ b/Gemfile
@@ -11,6 +11,7 @@ gem 'html2rss-configs', github: 'html2rss/html2rss-configs'
# gem 'html2rss', path: '../html2rss'
# gem 'html2rss-configs', path: '../html2rss-configs'
+gem 'base64'
gem 'erubi'
gem 'parallel'
gem 'rack-cache'
diff --git a/Gemfile.lock b/Gemfile.lock
index 46bfddc5..4d4344a1 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -11,6 +11,7 @@ GEM
addressable (2.8.7)
public_suffix (>= 2.0.2, < 7.0)
ast (2.4.2)
+ base64 (0.2.0)
byebug (11.1.3)
concurrent-ruby (1.3.4)
crass (1.0.6)
@@ -151,6 +152,7 @@ PLATFORMS
x86_64-linux
DEPENDENCIES
+ base64
byebug
erubi
html2rss
diff --git a/README.md b/README.md
index d30ae229..a8c10c82 100644
--- a/README.md
+++ b/README.md
@@ -66,6 +66,10 @@ The [watchtower](https://containrrr.dev/watchtower/) service automatically pulls
The `docker-compose.yml` above contains a service description for watchtower.
+## How to use automatic feed generation
+
+1. add `FEATURE_AUTO_SOURCE_ENABLED=true` to your `docker-compose.yml` file
+
## How to use the included configs
html2rss-web comes with many feed configs out of the box. [See the file list of all configs.](https://github.com/html2rss/html2rss-configs/tree/master/lib/html2rss/configs)
diff --git a/helpers/handle_error.rb b/helpers/handle_error.rb
index 6571abe9..f9ccd37c 100644
--- a/helpers/handle_error.rb
+++ b/helpers/handle_error.rb
@@ -15,6 +15,8 @@ def handle_error(error) # rubocop:disable Metrics/MethodLength
when LocalConfig::NotFound,
Html2rss::Configs::ConfigNotFound
set_error_response('Feed config not found', 404)
+ when Html2rss::Error
+ set_error_response('Html2rss error', 422)
else
set_error_response('Internal Server Error', 500)
end
diff --git a/routes/auto_source.rb b/routes/auto_source.rb
new file mode 100644
index 00000000..5017182e
--- /dev/null
+++ b/routes/auto_source.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+require 'base64'
+
+module Html2rss
+ module Web
+ class App
+ if ENV['AUTO_SOURCE_ENABLED'].to_s == 'true'
+ hash_branch 'auto_source' do |r|
+ with_basic_auth(realm: 'Auto Source',
+ username: ENV.fetch('AUTO_SOURCE_USERNAME'),
+ password: ENV.fetch('AUTO_SOURCE_PASSWORD')) do
+ r.get 'test' do |_r|
+ 'AUTO'
+ end
+
+ r.on String, method: :get do |encoded_url|
+ url = Base64.urlsafe_decode64(encoded_url)
+
+ rss = Html2rss.auto_source(url)
+ ttl = (rss.channel.ttl || 60) * 60
+
+ HttpCache.expires(response, ttl, cache_control: 'private, max-age=0, must-revalidate')
+
+ response['Content-Type'] = 'application/rss+xml'
+
+ rss.to_s
+ end
+ end
+ end
+
+ else
+ # auto_source feature is disabled
+ hash_branch 'auto_source' do |r|
+ r.on do
+ response.status = 403
+ 'The auto source feature is disabled.'
+ end
+ end
+ end
+ end
+ end
+end
From 24734c7368396feea1d81cbb9a684aa36243e5ca Mon Sep 17 00:00:00 2001
From: Gil Desmarais
Date: Sat, 17 Aug 2024 17:10:31 +0200
Subject: [PATCH 03/25] feat: use ssrf_filter to request user provided urls
---
Gemfile | 1 +
Gemfile.lock | 5 ++++-
routes/auto_source.rb | 16 ++++++++--------
3 files changed, 13 insertions(+), 9 deletions(-)
diff --git a/Gemfile b/Gemfile
index ff80ee1d..07be6e75 100644
--- a/Gemfile
+++ b/Gemfile
@@ -18,6 +18,7 @@ gem 'rack-cache'
gem 'rack-timeout'
gem 'rack-unreloader'
gem 'roda'
+gem 'ssrf_filter'
gem 'tilt'
gem 'puma', require: false
diff --git a/Gemfile.lock b/Gemfile.lock
index 4d4344a1..83fc97a5 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -133,7 +133,9 @@ GEM
simplecov_json_formatter (~> 0.1)
simplecov-html (0.12.3)
simplecov_json_formatter (0.1.4)
- thor (1.3.2)
+ ssrf_filter (1.1.2)
+ strscan (3.1.0)
+ thor (1.3.1)
tilt (2.4.0)
tzinfo (2.0.6)
concurrent-ruby (~> 1.0)
@@ -171,6 +173,7 @@ DEPENDENCIES
rubocop-rspec
rubocop-thread_safety
simplecov
+ ssrf_filter
tilt
vcr
yard
diff --git a/routes/auto_source.rb b/routes/auto_source.rb
index 5017182e..17e1ee83 100644
--- a/routes/auto_source.rb
+++ b/routes/auto_source.rb
@@ -1,6 +1,9 @@
# frozen_string_literal: true
+require 'addressable'
require 'base64'
+require 'ssrf_filter'
+require 'html2rss'
module Html2rss
module Web
@@ -10,17 +13,14 @@ class App
with_basic_auth(realm: 'Auto Source',
username: ENV.fetch('AUTO_SOURCE_USERNAME'),
password: ENV.fetch('AUTO_SOURCE_PASSWORD')) do
- r.get 'test' do |_r|
- 'AUTO'
- end
-
r.on String, method: :get do |encoded_url|
- url = Base64.urlsafe_decode64(encoded_url)
+ url = Addressable::URI.parse(Base64.urlsafe_decode64(encoded_url))
+
+ rss = Html2rss::AutoSource.build_from_response(SsrfFilter.get(url), url)
- rss = Html2rss.auto_source(url)
- ttl = (rss.channel.ttl || 60) * 60
+ max_age = (rss.channel.ttl || 60) * 60
- HttpCache.expires(response, ttl, cache_control: 'private, max-age=0, must-revalidate')
+ HttpCache.expires(response, max_age, cache_control: 'private, must-revalidate')
response['Content-Type'] = 'application/rss+xml'
From 0070399b1c2d8a819524b83c64412c89ff1b8464 Mon Sep 17 00:00:00 2001
From: Gil Desmarais
Date: Sat, 17 Aug 2024 17:26:52 +0200
Subject: [PATCH 04/25] feat: use hash_branch_view_dir
---
app.rb | 2 +-
helpers/handle_error.rb | 2 ++
2 files changed, 3 insertions(+), 1 deletion(-)
diff --git a/app.rb b/app.rb
index bbd46412..e328fac9 100644
--- a/app.rb
+++ b/app.rb
@@ -48,7 +48,7 @@ def self.development? = ENV['RACK_ENV'] == 'development'
handle_error(error)
end
- plugin :hash_branches
+ plugin :hash_branch_view_subdir
plugin :public
plugin :render, escape: true, layout: 'layout'
plugin :typecast_params
diff --git a/helpers/handle_error.rb b/helpers/handle_error.rb
index f9ccd37c..97f905e1 100644
--- a/helpers/handle_error.rb
+++ b/helpers/handle_error.rb
@@ -23,6 +23,8 @@ def handle_error(error) # rubocop:disable Metrics/MethodLength
@show_backtrace = self.class.development?
@error = error
+
+ set_view_subdir nil
view 'error'
end
From 7588413b70818f887a829ba1a96ef38ce832fd25 Mon Sep 17 00:00:00 2001
From: Gil Desmarais
Date: Sun, 18 Aug 2024 13:08:40 +0200
Subject: [PATCH 05/25] feat: adapt to changed auto_source constructor
signature
---
routes/auto_source.rb | 30 ++++++++++++++++++++++++------
1 file changed, 24 insertions(+), 6 deletions(-)
diff --git a/routes/auto_source.rb b/routes/auto_source.rb
index 17e1ee83..ec629f89 100644
--- a/routes/auto_source.rb
+++ b/routes/auto_source.rb
@@ -9,18 +9,19 @@ module Html2rss
module Web
class App
if ENV['AUTO_SOURCE_ENABLED'].to_s == 'true'
+
hash_branch 'auto_source' do |r|
with_basic_auth(realm: 'Auto Source',
username: ENV.fetch('AUTO_SOURCE_USERNAME'),
password: ENV.fetch('AUTO_SOURCE_PASSWORD')) do
- r.on String, method: :get do |encoded_url|
- url = Addressable::URI.parse(Base64.urlsafe_decode64(encoded_url))
-
- rss = Html2rss::AutoSource.build_from_response(SsrfFilter.get(url), url)
+ r.get '/' do
+ view 'index', layout: '/layout'
+ end
- max_age = (rss.channel.ttl || 60) * 60
+ r.on String, method: :get do |encoded_url|
+ rss = build_auto_source_from_encoded_url(encoded_url)
- HttpCache.expires(response, max_age, cache_control: 'private, must-revalidate')
+ HttpCache.expires(response, ttl_in_seconds(rss), cache_control: 'private, must-revalidate')
response['Content-Type'] = 'application/rss+xml'
@@ -29,6 +30,23 @@ class App
end
end
+ private
+
+ def build_auto_source_from_encoded_url(encoded_url)
+ url = Addressable::URI.parse Base64.urlsafe_decode64(encoded_url)
+ request = SsrfFilter.get(url)
+
+ auto_source = Html2rss::AutoSource.new(url,
+ body: request.body,
+ headers: request.to_hash.transform_values(&:first))
+
+ auto_source.build
+ end
+
+ def ttl_in_seconds(rss, default_in_minutes: 60)
+ (rss.channel.ttl || default_in_minutes) * 60
+ end
+
else
# auto_source feature is disabled
hash_branch 'auto_source' do |r|
From b2da9124031324c12786d4b35670a50477d04e1f Mon Sep 17 00:00:00 2001
From: Gil Desmarais
Date: Mon, 19 Aug 2024 16:36:38 +0200
Subject: [PATCH 06/25] feat: add auto_source#index
---
app.rb | 4 ++--
public/auto_source.js | 33 +++++++++++++++++++++++++++++++++
public/rss.xsl | 2 +-
public/styles.css | 35 ++++++++++++++++++++++++++++++++++-
routes/auto_source.rb | 11 ++++++-----
views/auto_source/index.erb | 20 ++++++++++++++++++++
6 files changed, 96 insertions(+), 9 deletions(-)
create mode 100644 public/auto_source.js
create mode 100644 views/auto_source/index.erb
diff --git a/app.rb b/app.rb
index e328fac9..556a4f4f 100644
--- a/app.rb
+++ b/app.rb
@@ -2,7 +2,6 @@
require 'roda'
require 'rack/cache'
-
require_relative 'roda/roda_plugins/basic_auth'
module Html2rss
@@ -12,6 +11,8 @@ module Web
#
# It is built with [Roda](https://roda.jeremyevans.net/).
class App < Roda
+ CONTENT_TYPE_RSS = 'application/xml'
+
def self.development? = ENV['RACK_ENV'] == 'development'
opts[:check_dynamic_arity] = false
@@ -64,7 +65,6 @@ def self.development? = ENV['RACK_ENV'] == 'development'
route do |r|
r.public
-
r.hash_branches('')
r.root { view 'index' }
diff --git a/public/auto_source.js b/public/auto_source.js
new file mode 100644
index 00000000..0ec2097a
--- /dev/null
+++ b/public/auto_source.js
@@ -0,0 +1,33 @@
+// TODO: when a ?url=%s param is available, set the value of the url input field to %s
+const $form = document.querySelector("form");
+const $url = document.querySelector("#url");
+const $iframe = document.querySelector("iframe");
+const $rssUrl = document.querySelector("#rss_url");
+
+$url?.addEventListener("change", (event) => {
+ delete $iframe.src;
+ delete $rssUrl.value;
+});
+
+$form?.addEventListener("submit", async (event) => {
+ event.preventDefault();
+
+ if ($url && $rssUrl) {
+ const url = $url?.value;
+
+ if (!url || `${url}`.trim() === "" || !url.startsWith("http")) {
+ return;
+ }
+
+ const encodedUrl = btoa(url).replace(/=/g, "");
+ const baseUrl = new URL(window.location.origin);
+ const autoSourceUrl = `${baseUrl}auto_source/${encodedUrl}`;
+
+ $rssUrl.value = autoSourceUrl;
+ $rssUrl.select();
+ }
+});
+
+$rssUrl?.addEventListener("focus", () => {
+ $iframe.src = `${$rssUrl.value}#feed`;
+});
diff --git a/public/rss.xsl b/public/rss.xsl
index ab38a3e3..9cc65fe8 100644
--- a/public/rss.xsl
+++ b/public/rss.xsl
@@ -41,7 +41,7 @@
- Feed content preview
+ Feed content preview
-
diff --git a/public/styles.css b/public/styles.css
index 553bbb1e..3a38276a 100644
--- a/public/styles.css
+++ b/public/styles.css
@@ -3,6 +3,10 @@
--highlight: #ff9300;
}
+::selection {
+ background-color: var(--highlight);
+}
+
label {
font-weight: bold;
cursor: pointer;
@@ -57,7 +61,9 @@ body > h2 {
box-shadow: 0 0 0.25em;
border-radius: 0.25em;
border: 1px solid transparent;
- transition: border-color 0.2s, opacity 0.2s;
+ transition:
+ border-color 0.2s,
+ opacity 0.2s;
}
.aside-icon > a:hover > img,
@@ -65,3 +71,30 @@ body > h2 {
border-color: currentColor;
opacity: 0.9;
}
+
+.auto_source {
+ display: flex;
+ flex-direction: column;
+}
+
+.auto_source input {
+ width: 100%;
+ max-width: unset;
+}
+
+.auto_source > form {
+ display: flex;
+ flex-wrap: wrap;
+}
+
+.auto_source iframe:not([src]) {
+ display: none;
+}
+
+.auto_source iframe[src] {
+ display: block;
+ width: 100%;
+ min-height: 50em;
+ max-height: 80vh;
+ border: 0;
+}
diff --git a/routes/auto_source.rb b/routes/auto_source.rb
index ec629f89..959a4645 100644
--- a/routes/auto_source.rb
+++ b/routes/auto_source.rb
@@ -14,7 +14,7 @@ class App
with_basic_auth(realm: 'Auto Source',
username: ENV.fetch('AUTO_SOURCE_USERNAME'),
password: ENV.fetch('AUTO_SOURCE_PASSWORD')) do
- r.get '/' do
+ r.root do
view 'index', layout: '/layout'
end
@@ -23,7 +23,7 @@ class App
HttpCache.expires(response, ttl_in_seconds(rss), cache_control: 'private, must-revalidate')
- response['Content-Type'] = 'application/rss+xml'
+ response['Content-Type'] = CONTENT_TYPE_RSS
rss.to_s
end
@@ -35,10 +35,11 @@ class App
def build_auto_source_from_encoded_url(encoded_url)
url = Addressable::URI.parse Base64.urlsafe_decode64(encoded_url)
request = SsrfFilter.get(url)
+ headers = request.to_hash.transform_values(&:first)
- auto_source = Html2rss::AutoSource.new(url,
- body: request.body,
- headers: request.to_hash.transform_values(&:first))
+ auto_source = Html2rss::AutoSource.new(url, body: request.body, headers:)
+
+ auto_source.channel.stylesheets << Html2rss::RssBuilder::Stylesheet.new(href: './rss.xsl', type: 'text/xsl')
auto_source.build
end
diff --git a/views/auto_source/index.erb b/views/auto_source/index.erb
new file mode 100644
index 00000000..0c541cd9
--- /dev/null
+++ b/views/auto_source/index.erb
@@ -0,0 +1,20 @@
+
+
+
Auto Source
+
+ Generate an RSS feed from a website.
+
+
+
+
+
From 1d6fd452971fbc39de5845aefabc4de449422da6 Mon Sep 17 00:00:00 2001
From: Gil Desmarais
Date: Mon, 19 Aug 2024 16:37:00 +0200
Subject: [PATCH 07/25] feat: use content-type constant
---
helpers/handle_html2rss_configs.rb | 2 +-
helpers/handle_local_config_feeds.rb | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/helpers/handle_html2rss_configs.rb b/helpers/handle_html2rss_configs.rb
index e3103cce..de616276 100644
--- a/helpers/handle_html2rss_configs.rb
+++ b/helpers/handle_html2rss_configs.rb
@@ -7,7 +7,7 @@ def handle_html2rss_configs(request, _folder_name, _config_name_with_ext)
path = RequestPath.new(request)
Html2rssFacade.from_config(path.full_config_name, typecast_params) do |config|
- response['Content-Type'] = 'text/xml'
+ response['Content-Type'] = CONTENT_TYPE_RSS
HttpCache.expires(response, config.ttl * 60, cache_control: 'public')
end
end
diff --git a/helpers/handle_local_config_feeds.rb b/helpers/handle_local_config_feeds.rb
index 03662af6..533fabf1 100644
--- a/helpers/handle_local_config_feeds.rb
+++ b/helpers/handle_local_config_feeds.rb
@@ -7,7 +7,7 @@ def handle_local_config_feeds(request, _config_name_with_ext)
path = RequestPath.new(request)
Html2rssFacade.from_local_config(path.full_config_name, typecast_params) do |config|
- response['Content-Type'] = 'text/xml'
+ response['Content-Type'] = CONTENT_TYPE_RSS
HttpCache.expires(response, config.ttl * 60, cache_control: 'public')
end
end
From 8d98dffe47a128c14813ab10600f003c576ca243 Mon Sep 17 00:00:00 2001
From: Gil Desmarais
Date: Mon, 19 Aug 2024 16:37:15 +0200
Subject: [PATCH 08/25] feat: rack-timeout only when RACK_ENV!=development
---
config.ru | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/config.ru b/config.ru
index 270d545e..cf3e5368 100644
--- a/config.ru
+++ b/config.ru
@@ -4,8 +4,6 @@ require 'rubygems'
require 'bundler/setup'
require 'rack-timeout'
-use Rack::Timeout
-
dev = ENV.fetch('RACK_ENV', nil) == 'development'
requires = Dir['app/**/*.rb']
@@ -26,6 +24,8 @@ if dev
run Unreloader
else
+ use Rack::Timeout
+
require_relative 'app'
requires.each { |f| require_relative f }
From b2fedc8ad51104beacccae764a9b48f0cbe2a9d8 Mon Sep 17 00:00:00 2001
From: Gil Desmarais
Date: Mon, 19 Aug 2024 16:52:20 +0200
Subject: [PATCH 09/25] fix: rendering of iframe prevented
Signed-off-by: Gil Desmarais
---
app.rb | 6 +++---
public/auto_source.js | 2 +-
public/rss.xsl | 6 +++---
public/styles.css | 30 ++++++++++++++++++++++++------
routes/auto_source.rb | 2 +-
views/auto_source/index.erb | 9 +++++++--
6 files changed, 39 insertions(+), 16 deletions(-)
diff --git a/app.rb b/app.rb
index 556a4f4f..be2b2a73 100644
--- a/app.rb
+++ b/app.rb
@@ -29,16 +29,16 @@ def self.development? = ENV['RACK_ENV'] == 'development'
csp.script_src :self
csp.connect_src :self
csp.img_src :self
- csp.font_src :self
+ csp.font_src :self, 'data:'
csp.form_action :self
csp.base_uri :none
- csp.frame_ancestors :none
+ csp.frame_ancestors :self
+ csp.frame_src :self
csp.block_all_mixed_content
end
plugin :default_headers,
'Content-Type' => 'text/html',
- 'X-Frame-Options' => 'deny',
'X-Content-Type-Options' => 'nosniff',
'X-XSS-Protection' => '1; mode=block'
diff --git a/public/auto_source.js b/public/auto_source.js
index 0ec2097a..14e4032f 100644
--- a/public/auto_source.js
+++ b/public/auto_source.js
@@ -29,5 +29,5 @@ $form?.addEventListener("submit", async (event) => {
});
$rssUrl?.addEventListener("focus", () => {
- $iframe.src = `${$rssUrl.value}#feed`;
+ $iframe.src = `${$rssUrl.value}#items`;
});
diff --git a/public/rss.xsl b/public/rss.xsl
index 9cc65fe8..1d5258e9 100644
--- a/public/rss.xsl
+++ b/public/rss.xsl
@@ -41,12 +41,12 @@
- Feed content preview
-
+ Feed content preview
+
-
-
+
diff --git a/public/styles.css b/public/styles.css
index 3a38276a..4fca9e4a 100644
--- a/public/styles.css
+++ b/public/styles.css
@@ -7,10 +7,19 @@
background-color: var(--highlight);
}
+html {
+ box-sizing: border-box;
+}
+
+*,
+*::before,
+*::after {
+ box-sizing: inherit;
+}
+
label {
font-weight: bold;
cursor: pointer;
- margin-top: 0.5em;
}
input[type="text"] {
@@ -24,8 +33,8 @@ body > h2 {
.items {
list-style: none;
+ padding-top: 2em;
padding-left: 0;
- margin-top: 2em;
}
.items > li {
@@ -68,7 +77,7 @@ body > h2 {
.aside-icon > a:hover > img,
.aside-icon > a:focus > img {
- border-color: currentColor;
+ border-color: var(--highlight);
opacity: 0.9;
}
@@ -84,7 +93,18 @@ body > h2 {
.auto_source > form {
display: flex;
- flex-wrap: wrap;
+}
+
+.auto_source > form input[type="submit"] {
+ flex: 1 1 20em;
+ margin-right: 0;
+}
+
+.auto_source iframe {
+ margin-top: 2em;
+ border-color: var(--background);
+ border-style: groove;
+ border-radius: 6px;
}
.auto_source iframe:not([src]) {
@@ -93,8 +113,6 @@ body > h2 {
.auto_source iframe[src] {
display: block;
- width: 100%;
min-height: 50em;
max-height: 80vh;
- border: 0;
}
diff --git a/routes/auto_source.rb b/routes/auto_source.rb
index 959a4645..1ffac75e 100644
--- a/routes/auto_source.rb
+++ b/routes/auto_source.rb
@@ -39,7 +39,7 @@ def build_auto_source_from_encoded_url(encoded_url)
auto_source = Html2rss::AutoSource.new(url, body: request.body, headers:)
- auto_source.channel.stylesheets << Html2rss::RssBuilder::Stylesheet.new(href: './rss.xsl', type: 'text/xsl')
+ auto_source.channel.stylesheets << Html2rss::RssBuilder::Stylesheet.new(href: '/rss.xsl', type: 'text/xsl')
auto_source.build
end
diff --git a/views/auto_source/index.erb b/views/auto_source/index.erb
index 0c541cd9..a2ccf9d4 100644
--- a/views/auto_source/index.erb
+++ b/views/auto_source/index.erb
@@ -9,11 +9,16 @@
+
+
+
From 97a855600e0319f1354d4e3022fc9150fb3a7944 Mon Sep 17 00:00:00 2001
From: Gil Desmarais
Date: Mon, 19 Aug 2024 18:10:11 +0200
Subject: [PATCH 10/25] refactor: improve auto_source form handling and styles
---
public/auto_source.css | 57 +++++++++++++++
public/auto_source.js | 138 ++++++++++++++++++++++++++++++------
public/styles.css | 19 +++--
views/auto_source/index.erb | 2 +-
4 files changed, 182 insertions(+), 34 deletions(-)
create mode 100644 public/auto_source.css
diff --git a/public/auto_source.css b/public/auto_source.css
new file mode 100644
index 00000000..082e907a
--- /dev/null
+++ b/public/auto_source.css
@@ -0,0 +1,57 @@
+/* Auto Source Form */
+.auto_source {
+ display: flex;
+ flex-direction: column;
+}
+
+.auto_source input,
+.auto_source button {
+ width: 100%;
+ font-family: monospace;
+}
+
+.auto_source > form {
+ display: flex;
+ flex-direction: column;
+}
+
+.auto_source > form input[type="submit"] {
+ flex: 1 1;
+ margin-right: 0;
+}
+
+.auto_source > nav {
+ display: flex;
+ flex-direction: column;
+}
+
+@media screen and (min-width: 768px) {
+ .auto_source > nav {
+ justify-content: space-between;
+ flex-direction: row;
+ }
+}
+
+.auto_source > nav button {
+ margin-left: 0.25em;
+ margin-right: 0;
+ flex: 1;
+}
+
+.auto_source button.muted {
+ color: var(--text-muted);
+ border: 1px solid var(--background-alt);
+}
+
+.auto_source iframe {
+ margin-top: 2em;
+ border: 2px groove var(--highlight);
+ border-radius: 0.5em;
+ display: none; /* Hide by default */
+}
+
+.auto_source iframe[src] {
+ display: block;
+ min-height: 50em;
+ max-height: 80vh;
+}
diff --git a/public/auto_source.js b/public/auto_source.js
index 14e4032f..85eb66b7 100644
--- a/public/auto_source.js
+++ b/public/auto_source.js
@@ -1,33 +1,125 @@
-// TODO: when a ?url=%s param is available, set the value of the url input field to %s
-const $form = document.querySelector("form");
-const $url = document.querySelector("#url");
-const $iframe = document.querySelector("iframe");
-const $rssUrl = document.querySelector("#rss_url");
+class FormHandler {
+ constructor() {
+ // Initialize DOM elements
+ this.form = document.querySelector("form");
+ this.urlInput = document.querySelector("#url");
+ this.iframe = document.querySelector("iframe");
+ this.rssUrlInput = document.querySelector("#rss_url");
-$url?.addEventListener("change", (event) => {
- delete $iframe.src;
- delete $rssUrl.value;
-});
+ if (!this.form || !this.urlInput || !this.iframe || !this.rssUrlInput) {
+ console.error(
+ "One or more necessary form elements were not found in the DOM.",
+ );
+ return;
+ }
+
+ // Bind event handlers
+ this.initEventListeners();
+ this.setInitialUrl();
+ }
-$form?.addEventListener("submit", async (event) => {
- event.preventDefault();
+ /**
+ * Initializes event listeners for form elements.
+ */
+ initEventListeners() {
+ // Event listener for URL input change
+ this.urlInput.addEventListener("change", () => this.clearRssUrl());
- if ($url && $rssUrl) {
- const url = $url?.value;
+ // Event listener for form submit
+ this.form.addEventListener("submit", (event) =>
+ this.handleFormSubmit(event),
+ );
- if (!url || `${url}`.trim() === "" || !url.startsWith("http")) {
- return;
+ // Event listener for RSS URL input focus
+ this.rssUrlInput.addEventListener("focus", () => {
+ const strippedIframeSrc = this.iframe.src.replace("#items", "").trim();
+ if (this.rssUrlInput.value.trim() !== strippedIframeSrc) {
+ this.updateIframeSrc(this.rssUrlInput.value.trim());
+ }
+ });
+ }
+
+ /**
+ * Sets the initial URL from query parameter if it exists.
+ */
+ setInitialUrl() {
+ const params = new URLSearchParams(window.location.search);
+ const initialUrl = params.get("url");
+ if (initialUrl) {
+ this.urlInput.value = initialUrl;
}
+ }
+
+ /**
+ * Clears the RSS URL input value.
+ */
+ clearRssUrl() {
+ this.rssUrlInput.value = "";
+ }
+
+ /**
+ * Handles form submission.
+ * @param {Event} event - The form submit event.
+ */
+ handleFormSubmit(event) {
+ event.preventDefault();
+ const url = this.urlInput.value;
- const encodedUrl = btoa(url).replace(/=/g, "");
+ if (this.isValidUrl(url)) {
+ const encodedUrl = this.encodeUrl(url);
+ const autoSourceUrl = this.generateAutoSourceUrl(encodedUrl);
+
+ this.rssUrlInput.value = autoSourceUrl;
+ this.rssUrlInput.select();
+ }
+ }
+
+ /**
+ * Checks if the URL is valid and starts with "http".
+ * @param {string} url - The URL to validate.
+ * @returns {boolean} True if the URL is valid, false otherwise.
+ */
+ isValidUrl(url) {
+ try {
+ new URL(url);
+ return url.trim() !== "" && url.startsWith("http");
+ } catch (_) {
+ return false;
+ }
+ }
+
+ /**
+ * Encodes the URL using base64 encoding.
+ * @param {string} url - The URL to encode.
+ * @returns {string} The base64 encoded URL.
+ */
+ encodeUrl(url) {
+ return btoa(url).replace(/=/g, "");
+ }
+
+ /**
+ * Generates an auto-source URL.
+ * @param {string} encodedUrl - The base64 encoded URL.
+ * @returns {string} The generated auto-source URL.
+ */
+ generateAutoSourceUrl(encodedUrl) {
+ const BASE_URL = "auto_source"; // Use constant to avoid magic strings
const baseUrl = new URL(window.location.origin);
- const autoSourceUrl = `${baseUrl}auto_source/${encodedUrl}`;
+ return `${baseUrl}${BASE_URL}/${encodedUrl}`;
+ }
- $rssUrl.value = autoSourceUrl;
- $rssUrl.select();
+ /**
+ * Updates the iframe source.
+ * @param {string} rssUrlValue - The RSS URL value.
+ */
+ updateIframeSrc(rssUrlValue) {
+ this.iframe.src = rssUrlValue === "" ? "" : `${rssUrlValue}#items`;
}
-});
+}
-$rssUrl?.addEventListener("focus", () => {
- $iframe.src = `${$rssUrl.value}#items`;
-});
+// Initialize FormHandler when the document is ready
+if (document.readyState === "complete") {
+ new FormHandler();
+} else {
+ document.addEventListener("DOMContentLoaded", () => new FormHandler());
+}
diff --git a/public/styles.css b/public/styles.css
index 4fca9e4a..c2c6ed06 100644
--- a/public/styles.css
+++ b/public/styles.css
@@ -17,6 +17,7 @@ html {
box-sizing: inherit;
}
+/* General Styles */
label {
font-weight: bold;
cursor: pointer;
@@ -31,10 +32,10 @@ body > h2 {
margin-top: 2em;
}
+/* List Items */
.items {
list-style: none;
- padding-top: 2em;
- padding-left: 0;
+ padding: 2em 0 0 0;
}
.items > li {
@@ -52,11 +53,12 @@ body > h2 {
margin-top: 0;
}
-.item > li > div {
+.items > li > div {
font-size: 0.9em;
padding: 0.25em;
}
+/* Aside Icon */
.aside-icon {
position: fixed;
top: 0.5em;
@@ -81,6 +83,7 @@ body > h2 {
opacity: 0.9;
}
+/* Auto Source Form */
.auto_source {
display: flex;
flex-direction: column;
@@ -88,7 +91,6 @@ body > h2 {
.auto_source input {
width: 100%;
- max-width: unset;
}
.auto_source > form {
@@ -100,15 +102,12 @@ body > h2 {
margin-right: 0;
}
+/* Iframe Styles */
.auto_source iframe {
margin-top: 2em;
- border-color: var(--background);
- border-style: groove;
+ border: 1px groove var(--background);
border-radius: 6px;
-}
-
-.auto_source iframe:not([src]) {
- display: none;
+ display: none; /* Hide by default */
}
.auto_source iframe[src] {
diff --git a/views/auto_source/index.erb b/views/auto_source/index.erb
index a2ccf9d4..d648d42d 100644
--- a/views/auto_source/index.erb
+++ b/views/auto_source/index.erb
@@ -22,4 +22,4 @@
-
+
From f577f1beb56554f2fc2f8e266261d0893355e068 Mon Sep 17 00:00:00 2001
From: Gil Desmarais
Date: Mon, 19 Aug 2024 18:18:00 +0200
Subject: [PATCH 11/25] feat(auto_source): automatically submit form when ?url=
is present
---
public/auto_source.js | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/public/auto_source.js b/public/auto_source.js
index 85eb66b7..025b545b 100644
--- a/public/auto_source.js
+++ b/public/auto_source.js
@@ -47,6 +47,7 @@ class FormHandler {
const initialUrl = params.get("url");
if (initialUrl) {
this.urlInput.value = initialUrl;
+ this.form.dispatchEvent(new Event("submit"));
}
}
@@ -71,6 +72,10 @@ class FormHandler {
this.rssUrlInput.value = autoSourceUrl;
this.rssUrlInput.select();
+
+ if (window.location.search !== `?url=${url}`) {
+ window.history.pushState({}, "", `?url=${url}`);
+ }
}
}
From 621a154b3406168ad81f9312e59b3517dc1bbafd Mon Sep 17 00:00:00 2001
From: Gil Desmarais
Date: Tue, 20 Aug 2024 15:23:09 +0200
Subject: [PATCH 12/25] feat(auto_source): integrate content_for plugin and
enhance UX
Added `content_for` plugin to manage CSS and scripts. Implemented new bookmarklet, button handlers, and form improvements for better user interaction.
Signed-off-by: Gil Desmarais
---
app.rb | 1 +
public/auto_source.js | 393 ++++++++++++++++++++++++++----------
public/styles.css | 33 ---
views/auto_source/index.erb | 51 +++--
views/layout.erb | 2 +
5 files changed, 330 insertions(+), 150 deletions(-)
diff --git a/app.rb b/app.rb
index be2b2a73..a75142cd 100644
--- a/app.rb
+++ b/app.rb
@@ -51,6 +51,7 @@ def self.development? = ENV['RACK_ENV'] == 'development'
plugin :hash_branch_view_subdir
plugin :public
+ plugin :content_for
plugin :render, escape: true, layout: 'layout'
plugin :typecast_params
plugin :basic_auth
diff --git a/public/auto_source.js b/public/auto_source.js
index 025b545b..29b24e99 100644
--- a/public/auto_source.js
+++ b/public/auto_source.js
@@ -1,130 +1,313 @@
-class FormHandler {
- constructor() {
- // Initialize DOM elements
- this.form = document.querySelector("form");
- this.urlInput = document.querySelector("#url");
- this.iframe = document.querySelector("iframe");
- this.rssUrlInput = document.querySelector("#rss_url");
-
- if (!this.form || !this.urlInput || !this.iframe || !this.rssUrlInput) {
- console.error(
- "One or more necessary form elements were not found in the DOM.",
- );
+const autoSource = (function () {
+ const ALLOWED_ORIGINS = ["127.0.0.1", "::1"];
+
+ function assertAllowedOrigin() {
+ const allowedOrigin = ALLOWED_ORIGINS.includes(location.host.split(":")[0]);
+
+ if (allowedOrigin) {
return;
}
- // Bind event handlers
- this.initEventListeners();
- this.setInitialUrl();
+ if (location.protocol !== "https:") {
+ throw new Error("You must use HTTPS for the auto_source feature.");
+ }
}
- /**
- * Initializes event listeners for form elements.
- */
- initEventListeners() {
- // Event listener for URL input change
- this.urlInput.addEventListener("change", () => this.clearRssUrl());
-
- // Event listener for form submit
- this.form.addEventListener("submit", (event) =>
- this.handleFormSubmit(event),
- );
-
- // Event listener for RSS URL input focus
- this.rssUrlInput.addEventListener("focus", () => {
- const strippedIframeSrc = this.iframe.src.replace("#items", "").trim();
- if (this.rssUrlInput.value.trim() !== strippedIframeSrc) {
- this.updateIframeSrc(this.rssUrlInput.value.trim());
+ class Bookmarklet {
+ constructor() {
+ const $bookmarklet = document.querySelector("a#bookmarklet");
+
+ if (!$bookmarklet) {
+ console.error("Bookmarklet element not found in the DOM.");
+ return;
}
- });
- }
- /**
- * Sets the initial URL from query parameter if it exists.
- */
- setInitialUrl() {
- const params = new URLSearchParams(window.location.search);
- const initialUrl = params.get("url");
- if (initialUrl) {
- this.urlInput.value = initialUrl;
- this.form.dispatchEvent(new Event("submit"));
+ $bookmarklet.href = this.generateBookmarkletHref();
}
- }
- /**
- * Clears the RSS URL input value.
- */
- clearRssUrl() {
- this.rssUrlInput.value = "";
+ generateBookmarkletHref() {
+ const h2rUrl = new URL(window.location.origin);
+ h2rUrl.pathname = "auto_source/";
+ h2rUrl.search = `?url=`;
+ h2rUrl.hash = "";
+
+ return `javascript:window.location.href='${h2rUrl.toString()}'+window.location.href;`;
+ }
}
- /**
- * Handles form submission.
- * @param {Event} event - The form submit event.
- */
- handleFormSubmit(event) {
- event.preventDefault();
- const url = this.urlInput.value;
+ class FormHandler {
+ constructor() {
+ // Initialize DOM elements
+ this.form = document.querySelector("form");
+ this.urlInput = document.querySelector("#url");
+ this.iframe = document.querySelector("iframe");
+ this.rssUrlInput = document.querySelector("#rss_url");
+
+ if (!this.form || !this.urlInput || !this.iframe || !this.rssUrlInput) {
+ console.error("One or more necessary form elements were not found in the DOM.");
+ return;
+ }
+
+ // Bind event handlers
+ this.initEventListeners();
+ this.setInitialUrl();
+ }
- if (this.isValidUrl(url)) {
- const encodedUrl = this.encodeUrl(url);
- const autoSourceUrl = this.generateAutoSourceUrl(encodedUrl);
+ /**
+ * Initializes event listeners for form elements.
+ */
+ initEventListeners() {
+ // Event listener for URL input change
+ this.urlInput.addEventListener("change", () => this.clearRssUrl());
- this.rssUrlInput.value = autoSourceUrl;
- this.rssUrlInput.select();
+ // Event listener for form submit
+ this.form.addEventListener("submit", (event) => this.handleFormSubmit(event));
- if (window.location.search !== `?url=${url}`) {
- window.history.pushState({}, "", `?url=${url}`);
+ // Event listener for RSS URL input focus
+ this.rssUrlInput.addEventListener("focus", () => {
+ const strippedIframeSrc = this.iframe.src.replace("#items", "").trim();
+ if (this.rssUrlInput.value.trim() !== strippedIframeSrc) {
+ this.updateIframeSrc(this.rssUrlInput.value.trim());
+ }
+ });
+ }
+
+ /**
+ * Sets the initial URL from query parameter if it exists.
+ */
+ setInitialUrl() {
+ const params = new URLSearchParams(window.location.search);
+ const initialUrl = params.get("url");
+ if (initialUrl) {
+ this.urlInput.value = initialUrl;
+ this.form.dispatchEvent(new Event("submit"));
}
}
- }
- /**
- * Checks if the URL is valid and starts with "http".
- * @param {string} url - The URL to validate.
- * @returns {boolean} True if the URL is valid, false otherwise.
- */
- isValidUrl(url) {
- try {
- new URL(url);
- return url.trim() !== "" && url.startsWith("http");
- } catch (_) {
- return false;
+ /**
+ * Clears the RSS URL input value.
+ */
+ clearRssUrl() {
+ this.rssUrlInput.value = "";
+ }
+
+ /**
+ * Handles form submission.
+ * @param {Event} event - The form submit event.
+ */
+ handleFormSubmit(event) {
+ event.preventDefault();
+
+ this.rssUrlInput.value = "";
+ this.iframe.src = "";
+
+ const url = this.urlInput.value;
+
+ if (this.isValidUrl(url)) {
+ const encodedUrl = this.encodeUrl(url);
+ const autoSourceUrl = this.generateAutoSourceUrl(encodedUrl);
+
+ this.rssUrlInput.value = autoSourceUrl;
+ this.rssUrlInput.select();
+
+ if (window.location.search !== `?url=${url}`) {
+ window.history.pushState({}, "", `?url=${url}`);
+ }
+ }
+ }
+
+ /**
+ * Checks if the URL is valid and starts with "http".
+ * @param {string} url - The URL to validate.
+ * @returns {boolean} True if the URL is valid, false otherwise.
+ */
+ isValidUrl(url) {
+ try {
+ new URL(url);
+ return url.trim() !== "" && url.startsWith("http");
+ } catch (_) {
+ return false;
+ }
+ }
+
+ /**
+ * Encodes the URL using base64 encoding.
+ * @param {string} url - The URL to encode.
+ * @returns {string} The base64 encoded URL.
+ */
+ encodeUrl(url) {
+ return btoa(url).replace(/=/g, "");
+ }
+
+ /**
+ * Generates an auto-source URL.
+ * @param {string} encodedUrl - The base64 encoded URL.
+ * @returns {string} The generated auto-source URL.
+ */
+ generateAutoSourceUrl(encodedUrl) {
+ const BASE_URL = "auto_source"; // Use constant to avoid magic strings
+ const baseUrl = new URL(window.location.origin);
+ return `${baseUrl}${BASE_URL}/${encodedUrl}`;
+ }
+
+ /**
+ * Updates the iframe source.
+ * @param {string} rssUrlValue - The RSS URL value.
+ */
+ updateIframeSrc(rssUrlValue) {
+ this.iframe.src = rssUrlValue === "" ? "" : `${rssUrlValue}#items`;
}
}
- /**
- * Encodes the URL using base64 encoding.
- * @param {string} url - The URL to encode.
- * @returns {string} The base64 encoded URL.
- */
- encodeUrl(url) {
- return btoa(url).replace(/=/g, "");
+ class ButtonHandler {
+ constructor() {
+ // Cache necessary DOM elements
+ const copyButton = document.querySelector("#copy");
+ const gotoButton = document.querySelector("#goto");
+ const openInFeedButton = document.querySelector("#openInFeed");
+ const rssUrlField = document.querySelector("#rss_url");
+ const resetCredentialsButton = document.querySelector("#resetCredentials");
+
+ if (!copyButton || !gotoButton || !openInFeedButton || !rssUrlField || !resetCredentialsButton) {
+ console.error("One or more necessary button elements were not found in the DOM.");
+ return;
+ }
+
+ // Assign elements to instance variables
+ this.copyButton = copyButton;
+ this.gotoButton = gotoButton;
+ this.openInFeedButton = openInFeedButton;
+ this.rssUrlField = rssUrlField;
+ this.resetCredentialsButton = resetCredentialsButton;
+
+ // Initialize event listeners
+ this.initEventListeners();
+ }
+
+ /**
+ * Initializes event listeners for buttons.
+ */
+ initEventListeners() {
+ // Bind event handlers to the context of the class instance
+ this.copyButton.addEventListener("click", this.copyText.bind(this));
+ this.gotoButton.addEventListener("click", this.openLink.bind(this));
+ this.openInFeedButton.addEventListener("click", this.subscribeToFeed.bind(this));
+ this.resetCredentialsButton.addEventListener("click", this.resetCredentials.bind(this));
+ }
+
+ /**
+ * Copies the text from the text field to the clipboard.
+ */
+ async copyText() {
+ try {
+ const textToCopy = this.rssUrlField.value;
+ await navigator.clipboard.writeText(textToCopy);
+ console.log("Text copied to clipboard:", textToCopy);
+ } catch (error) {
+ console.error("Failed to copy text to clipboard:", error);
+ }
+ }
+
+ /**
+ * Opens the link specified in the text field.
+ */
+ openLink() {
+ const linkToOpen = this.rssUrlField?.value;
+
+ if (typeof linkToOpen === "string" && linkToOpen.trim() !== "") {
+ window.open(linkToOpen, "_blank", "noopener,noreferrer");
+ }
+ }
+
+ /**
+ * Subscribes to the feed specified in the text field.
+ */
+ async subscribeToFeed() {
+ assertAllowedOrigin();
+
+ const feedUrl = this.rssUrlField.value;
+ const storedUser = LocalStorageFacade.getOrAskUser("username");
+ const storedPassword = LocalStorageFacade.getOrAskUser("password");
+
+ const url = new URL(feedUrl);
+ url.username = storedUser;
+ url.password = storedPassword;
+
+ const feedUrlWithAuth = `feed:${url.toString()}`;
+
+ window.open(feedUrlWithAuth);
+ }
+
+ resetCredentials() {
+ ["username", "password"].forEach((key) => {
+ LocalStorageFacade.remove(key);
+ });
+
+ alert("Credentials have been reset. Click 'Subscribe' to re-enter credentials.");
+ }
}
- /**
- * Generates an auto-source URL.
- * @param {string} encodedUrl - The base64 encoded URL.
- * @returns {string} The generated auto-source URL.
- */
- generateAutoSourceUrl(encodedUrl) {
- const BASE_URL = "auto_source"; // Use constant to avoid magic strings
- const baseUrl = new URL(window.location.origin);
- return `${baseUrl}${BASE_URL}/${encodedUrl}`;
+ class LocalStorageFacade {
+ static get prefix() {
+ return "html2rss-web/auto_source/";
+ }
+
+ static get(key) {
+ key = LocalStorageFacade.encode(`${LocalStorageFacade.prefix}${key}`);
+
+ return LocalStorageFacade.decode(localStorage.getItem(key));
+ }
+
+ static set(key, value) {
+ key = LocalStorageFacade.encode(`${LocalStorageFacade.prefix}${key}`);
+
+ return localStorage.setItem(key, LocalStorageFacade.encode(value));
+ }
+
+ static remove(key) {
+ key = LocalStorageFacade.encode(`${LocalStorageFacade.prefix}${key}`);
+
+ return localStorage.removeItem(key);
+ }
+
+ static getOrAskUser(columnName) {
+ let value = LocalStorageFacade.get(columnName);
+
+ while (typeof value !== "string" || value === "") {
+ value = window.prompt(`Please enter your ${columnName}:`);
+
+ if (!value || value.trim() === "") {
+ alert(`Blank ${columnName} submitted. Try again!`);
+ } else {
+ LocalStorageFacade.set(columnName, value);
+ }
+ }
+
+ return value;
+ }
+
+ static encode(value) {
+ return btoa(value.trim()).replace(/=/g, "");
+ }
+
+ static decode(value) {
+ if (typeof value !== "string") {
+ return null;
+ }
+
+ return atob(value);
+ }
}
- /**
- * Updates the iframe source.
- * @param {string} rssUrlValue - The RSS URL value.
- */
- updateIframeSrc(rssUrlValue) {
- this.iframe.src = rssUrlValue === "" ? "" : `${rssUrlValue}#items`;
+ function init() {
+ new Bookmarklet();
+ new FormHandler();
+ new ButtonHandler();
}
-}
-
-// Initialize FormHandler when the document is ready
-if (document.readyState === "complete") {
- new FormHandler();
-} else {
- document.addEventListener("DOMContentLoaded", () => new FormHandler());
-}
+
+ return { init: init };
+})();
+
+document.readyState === "complete"
+ ? autoSource.init()
+ : document.addEventListener("DOMContentLoaded", autoSource.init());
diff --git a/public/styles.css b/public/styles.css
index c2c6ed06..5f86282f 100644
--- a/public/styles.css
+++ b/public/styles.css
@@ -82,36 +82,3 @@ body > h2 {
border-color: var(--highlight);
opacity: 0.9;
}
-
-/* Auto Source Form */
-.auto_source {
- display: flex;
- flex-direction: column;
-}
-
-.auto_source input {
- width: 100%;
-}
-
-.auto_source > form {
- display: flex;
-}
-
-.auto_source > form input[type="submit"] {
- flex: 1 1 20em;
- margin-right: 0;
-}
-
-/* Iframe Styles */
-.auto_source iframe {
- margin-top: 2em;
- border: 1px groove var(--background);
- border-radius: 6px;
- display: none; /* Hide by default */
-}
-
-.auto_source iframe[src] {
- display: block;
- min-height: 50em;
- max-height: 80vh;
-}
diff --git a/views/auto_source/index.erb b/views/auto_source/index.erb
index d648d42d..a3fe2cb2 100644
--- a/views/auto_source/index.erb
+++ b/views/auto_source/index.erb
@@ -1,25 +1,52 @@
-
+<% content_for :css do %>
+
+<% end %>
-Auto Source
-
- Generate an RSS feed from a website.
-
+<% content_for :scripts do %>
+
+<% end %>
+ Auto Source
+
+ Automatically generate an RSS feed from a website.
+
+
-
+
+
-
+
-
+
+
+
diff --git a/views/layout.erb b/views/layout.erb
index bdaef979..f5d2a77c 100644
--- a/views/layout.erb
+++ b/views/layout.erb
@@ -9,6 +9,7 @@
html2rss-web<% if @page_title %> - <%= @page_title %><% end %>
+ <%== content_for :css %>
<%== yield %>
+ <%== content_for :scripts %>