From 634e6a8a1f05e378615b7ac688bf0e633ba89214 Mon Sep 17 00:00:00 2001 From: Olivier Auverlot Date: Mon, 4 Mar 2024 09:21:53 +0100 Subject: [PATCH 01/15] Produces a data flow in the RSS format --- .../sqlpage/migrations/37_rss.sql | 83 +++++++++++++++++++ sqlpage/templates/rss.handlebars | 17 ++++ 2 files changed, 100 insertions(+) create mode 100644 examples/official-site/sqlpage/migrations/37_rss.sql create mode 100644 sqlpage/templates/rss.handlebars diff --git a/examples/official-site/sqlpage/migrations/37_rss.sql b/examples/official-site/sqlpage/migrations/37_rss.sql new file mode 100644 index 00000000..e0a0a74c --- /dev/null +++ b/examples/official-site/sqlpage/migrations/37_rss.sql @@ -0,0 +1,83 @@ +-- Documentation for the RSS component +INSERT INTO component (name, description, icon, introduced_in_version) VALUES ( + 'rss', + 'Produces a data flow in the RSS format. To use this component, you must first returning an HTTP header with the "application/rss+xml" content type (see http_header component). Next, you must use the shell-empty component to avoid that SQLPage generates HTML code. TIPS: If you don''t want change the page shell, you can also use the _sqlpage_embed URL parameter.', + 'rss', + '0.20.0' +); + +INSERT INTO parameter (component,name,description,type,top_level,optional) VALUES ( + 'rss', + 'title', + 'Defines the title of the channel.', + 'TEXT', + TRUE, + FALSE +),( + 'rss', + 'link', + 'Defines the hyperlink to the channel.', + 'URL', + TRUE, + FALSE +),( + 'rss', + 'description', + 'Describes the channel.', + 'TEXT', + TRUE, + FALSE +),( + 'rss', + 'title', + 'Defines the title of the item.', + 'TEXT', + FALSE, + FALSE +),( + 'rss', + 'link', + 'Defines the hyperlink to the item', + 'URL', + FALSE, + FALSE +),( + 'rss', + 'description', + 'Describes the item', + 'TEXT', + FALSE, + FALSE +),( + 'rss', + 'pubdate', + 'Indicates when the item was published (RFC-822 date-time).', + 'TEXT', + FALSE, + TRUE +); + +-- Insert example(s) for the component +INSERT INTO example (component, description) +VALUES ( + 'rss', + ' +### An RSS chanel about SQLPage latest news. + +```sql +select + ''rss'' as component, + ''SQLPage blog'' as title, + ''https://sql.ophir.dev/blog.sql'' as link, + ''latest news about SQLpage'' as description; +select + ''Hello everyone !'' as title, + ''https://sql.ophir.dev/blog.sql?post=Come%20see%20me%20build%20twitter%20live%20on%20stage%20in%20Prague'' as link, + ''If some of you european SQLPagers are around Prague this december, I will be giving a talk about SQLPage at pgconf.eu on December 14th.'' as description; +select + ''3 solutions to the 3 layer problem'' as title, + ''https://sql.ophir.dev/blog.sql?post=3%20solutions%20to%20the%203%20layer%20problem'' as link, + ''Some interesting questions emerged from the article Repeating yourself.'' as description, + ''Mon, 04 Dec 2023 00:00:00 GMT'' as pubdate; +``` +'); \ No newline at end of file diff --git a/sqlpage/templates/rss.handlebars b/sqlpage/templates/rss.handlebars new file mode 100644 index 00000000..f8d588f9 --- /dev/null +++ b/sqlpage/templates/rss.handlebars @@ -0,0 +1,17 @@ + + + + {{title}} + {{link}} + {{description}} + {{#each_row}} + + {{title}} + {{link}} + + {{~#if pubdate}}{{pubdate}}{{/if}} + + {{/each_row}} + + + From 78be7fa229e44e1e27207ab35057ea951d4d2675 Mon Sep 17 00:00:00 2001 From: lovasoa Date: Mon, 4 Mar 2024 17:21:51 +0100 Subject: [PATCH 02/15] add documentation and rss reference --- examples/official-site/blog.sql | 1 + examples/official-site/index.sql | 3 ++- examples/official-site/rss.sql | 16 ++++++++++++++++ .../sqlpage/migrations/01_documentation.sql | 1 + .../official-site/sqlpage/migrations/37_rss.sql | 4 +++- sqlpage/templates/shell.handlebars | 3 +++ 6 files changed, 26 insertions(+), 2 deletions(-) create mode 100644 examples/official-site/rss.sql diff --git a/examples/official-site/blog.sql b/examples/official-site/blog.sql index a3789946..5936fdd3 100644 --- a/examples/official-site/blog.sql +++ b/examples/official-site/blog.sql @@ -10,6 +10,7 @@ select 'shell' as component, 'Poppins' as font, 'https://cdn.jsdelivr.net/npm/prismjs@1/components/prism-core.min.js' as javascript, 'https://cdn.jsdelivr.net/npm/prismjs@1/plugins/autoloader/prism-autoloader.min.js' as javascript, + './rss.sql' as rss, '/prism-tabler-theme.css' as css; SELECT 'text' AS component, diff --git a/examples/official-site/index.sql b/examples/official-site/index.sql index 7273b43e..b2498fb8 100644 --- a/examples/official-site/index.sql +++ b/examples/official-site/index.sql @@ -9,7 +9,8 @@ select 'shell' as component, 'blog' as menu_item, 'documentation' as menu_item, 19 as font_size, - 'Poppins' as font; + 'Poppins' as font, + './rss.sql' as rss; SELECT 'hero' as component, 'SQLPage' as title, diff --git a/examples/official-site/rss.sql b/examples/official-site/rss.sql new file mode 100644 index 00000000..f07a1087 --- /dev/null +++ b/examples/official-site/rss.sql @@ -0,0 +1,16 @@ +select 'http_header' as component, 'application/rss+xml' as "Content-Type"; +select 'shell-empty' as component; +select + 'rss' as component, + 'SQLPage blog' as title, + 'https://sql.ophir.dev/blog.sql' as link, + 'latest news about SQLpage' as description; +select + 'Hello everyone !' as title, + 'https://sql.ophir.dev/blog.sql?post=Come%20see%20me%20build%20twitter%20live%20on%20stage%20in%20Prague' as link, + 'If some of you european SQLPagers are around Prague this december, I will be giving a talk about SQLPage at pgconf.eu on December 14th.' as description; +select + '3 solutions to the 3 layer problem' as title, + 'https://sql.ophir.dev/blog.sql?post=3%20solutions%20to%20the%203%20layer%20problem' as link, + 'Some interesting questions emerged from the article Repeating yourself.' as description, + 'Mon, 04 Dec 2023 00:00:00 GMT' as pubdate; diff --git a/examples/official-site/sqlpage/migrations/01_documentation.sql b/examples/official-site/sqlpage/migrations/01_documentation.sql index a3876da0..abeb7a13 100644 --- a/examples/official-site/sqlpage/migrations/01_documentation.sql +++ b/examples/official-site/sqlpage/migrations/01_documentation.sql @@ -722,6 +722,7 @@ INSERT INTO parameter(component, name, description, type, top_level, optional) S ('link', 'The target of the link in the top navigation bar.', 'URL', TRUE, TRUE), ('css', 'The URL of a CSS file to load and apply to the page.', 'URL', TRUE, TRUE), ('javascript', 'The URL of a Javascript file to load and execute on the page.', 'URL', TRUE, TRUE), + ('rss', 'The URL of an RSS feed to display in the top navigation bar. You can use the rss component to generate the field.', 'URL', TRUE, TRUE), ('image', 'The URL of an image to display next to the page title.', 'URL', TRUE, TRUE), ('icon', 'Name of an icon (from tabler-icons.io) to display next to the title in the navigation bar.', 'ICON', TRUE, TRUE), ('menu_item', 'Adds a menu item in the navigation bar at the top of the page. The menu item will have the specified name, and will link to as .sql file of the same name. A dropdown can be generated by passing a json object with a `title` and `submenu` properties.', 'TEXT', TRUE, TRUE), diff --git a/examples/official-site/sqlpage/migrations/37_rss.sql b/examples/official-site/sqlpage/migrations/37_rss.sql index e0a0a74c..a44f2e07 100644 --- a/examples/official-site/sqlpage/migrations/37_rss.sql +++ b/examples/official-site/sqlpage/migrations/37_rss.sql @@ -1,7 +1,7 @@ -- Documentation for the RSS component INSERT INTO component (name, description, icon, introduced_in_version) VALUES ( 'rss', - 'Produces a data flow in the RSS format. To use this component, you must first returning an HTTP header with the "application/rss+xml" content type (see http_header component). Next, you must use the shell-empty component to avoid that SQLPage generates HTML code. TIPS: If you don''t want change the page shell, you can also use the _sqlpage_embed URL parameter.', + 'Produces a data flow in the RSS format. To use this component, you must first returning an HTTP header with the "application/rss+xml" content type (see http_header component). Next, you must use the shell-empty component to avoid that SQLPage generates HTML code.', 'rss', '0.20.0' ); @@ -65,6 +65,8 @@ VALUES ( ### An RSS chanel about SQLPage latest news. ```sql +select ''http_header'' as component, ''application/rss+xml'' as content_type; +select ''shell-empty'' as component; select ''rss'' as component, ''SQLPage blog'' as title, diff --git a/sqlpage/templates/shell.handlebars b/sqlpage/templates/shell.handlebars index 72d0c4b5..788cc12f 100644 --- a/sqlpage/templates/shell.handlebars +++ b/sqlpage/templates/shell.handlebars @@ -34,6 +34,9 @@ {{#if refresh}} {{/if}} + {{#if rss}} + + {{/if}} From d0f2ee996c753df73cc7452a016ae12ed968c70d Mon Sep 17 00:00:00 2001 From: lovasoa Date: Mon, 4 Mar 2024 18:38:18 +0100 Subject: [PATCH 03/15] more complete rss component, with support for podcasts --- .../sqlpage/migrations/37_rss.sql | 206 ++++++++++++++++-- sqlpage/templates/rss.handlebars | 54 +++-- 2 files changed, 228 insertions(+), 32 deletions(-) diff --git a/examples/official-site/sqlpage/migrations/37_rss.sql b/examples/official-site/sqlpage/migrations/37_rss.sql index a44f2e07..6d9f6e75 100644 --- a/examples/official-site/sqlpage/migrations/37_rss.sql +++ b/examples/official-site/sqlpage/migrations/37_rss.sql @@ -1,7 +1,9 @@ -- Documentation for the RSS component INSERT INTO component (name, description, icon, introduced_in_version) VALUES ( 'rss', - 'Produces a data flow in the RSS format. To use this component, you must first returning an HTTP header with the "application/rss+xml" content type (see http_header component). Next, you must use the shell-empty component to avoid that SQLPage generates HTML code.', + 'Produces a data flow in the RSS format. +Can be used to generate a podcast feed. +To use this component, you must first return an HTTP header with the "application/rss+xml" content type (see http_header component). Next, you must use the shell-empty component to avoid that SQLPage generates HTML code.', 'rss', '0.20.0' ); @@ -26,35 +28,203 @@ INSERT INTO parameter (component,name,description,type,top_level,optional) VALUE 'Describes the channel.', 'TEXT', TRUE, - FALSE + FALSE +),( + 'rss', + 'language', + 'Defines the language of the channel, specified in the ISO 639 format. For example, "en" for English, "fr" for French.', + 'TEXT', + TRUE, + TRUE +),( + 'rss', + 'category', + 'Defines the category of the channel. The value should be a string representing the category (e.g., "News", "Technology", etc.).', + 'TEXT', + TRUE, + TRUE +),( + 'rss', + 'explicit', + 'Indicates whether the channel contains explicit content. The value can be either TRUE or FALSE.', + 'BOOLEAN', + TRUE, + TRUE +),( + 'rss', + 'image_url', + 'Provides a URL linking to the artwork for the channel.', + 'URL', + TRUE, + TRUE +),( + 'rss', + 'author', + 'Defines the group, person, or people responsible for creating the channel.', + 'TEXT', + TRUE, + TRUE +),( + 'rss', + 'copyright', + 'Provides the copyright details for the channel.', + 'TEXT', + TRUE, + TRUE +),( + 'rss', + 'funding_url', + 'Specifies the donation/funding links for the channel. The content of the tag is the recommended string to be used with the link.', + 'URL', + TRUE, + TRUE +),( + 'rss', + 'type', + 'Specifies the channel as either episodic or serial. The value can be either "episodic" or "serial".', + 'TEXT', + TRUE, + TRUE +),( + 'rss', + 'complete', + 'Specifies that a channel is complete and will not post any more items in the future.', + 'BOOLEAN', + TRUE, + TRUE +),( + 'rss', + 'locked', + 'Tells podcast hosting platforms whether they are allowed to import this feed.', + 'BOOLEAN', + TRUE, + TRUE +),( + 'rss', + 'guid', + 'The globally unique identifier (GUID) for a channel. The value is a UUIDv5.', + 'TEXT', + TRUE, + TRUE ),( 'rss', 'title', - 'Defines the title of the item.', + 'Defines the title of the feed item (episode name, blog post title, etc.).', 'TEXT', FALSE, - FALSE + FALSE ),( 'rss', 'link', - 'Defines the hyperlink to the item', + 'Defines the hyperlink to the item (blog post URL, etc.).', 'URL', FALSE, - FALSE + FALSE ),( 'rss', 'description', 'Describes the item', 'TEXT', FALSE, - FALSE + FALSE ),( 'rss', 'pubdate', 'Indicates when the item was published (RFC-822 date-time).', 'TEXT', FALSE, - TRUE + TRUE +),( + 'rss', + 'enclosure_url', + 'The URL of the audio/video episode content.', + 'URL', + FALSE, + TRUE +),( + 'rss', + 'enclosure_length', + 'The length in bytes of the audio/video episode content.', + 'INTEGER', + FALSE, + TRUE +),( + 'rss', + 'enclosure_type', + 'The MIME media type of the audio/video episode content (e.g., "audio/mpeg", "audio/m4a", "video/m4v", "video/mp4").', + 'TEXT', + FALSE, + TRUE +),( + 'rss', + 'guid', + 'The globally unique identifier (GUID) for an item.', + 'TEXT', + FALSE, + TRUE +),( + 'rss', + 'episode', + 'The chronological number that is associated with an item.', + 'INTEGER', + FALSE, + TRUE +),( + 'rss', + 'season', + 'The chronological number associated with an item''s season.', + 'INTEGER', + FALSE, + TRUE +),( + 'rss', + 'episode_type', + 'Defines the type of content for a specific item. The value can be either "full", "trailer", or "bonus".', + 'TEXT', + FALSE, + TRUE +),( + 'rss', + 'block', + 'Prevents a specific item from appearing in podcast listening applications. The only valid value for this element is "yes".', + 'TEXT', + FALSE, + TRUE +),( + 'rss', + 'explicit', + 'Indicates whether the item contains explicit content. The value can be either TRUE or FALSE.', + 'BOOLEAN', + FALSE, + TRUE +),( + 'rss', + 'image_url', + 'Provides a URL linking to the artwork for the item.', + 'URL', + FALSE, + TRUE +),( + 'rss', + 'duration', + 'The duration of an item in seconds.', + 'INTEGER', + FALSE, + TRUE +),( + 'rss', + 'transcript_url', + 'A link to a transcript or closed captions file for the item.', + 'URL', + FALSE, + TRUE +),( + 'rss', + 'transcript_type', + 'The type of the transcript or closed captions file for the item (e.g., "text/plain", "text/html", "text/vtt", "application/json", "application/x-subrip").', + 'TEXT', + FALSE, + TRUE ); -- Insert example(s) for the component @@ -62,7 +232,7 @@ INSERT INTO example (component, description) VALUES ( 'rss', ' -### An RSS chanel about SQLPage latest news. +### An RSS channel about SQLPage latest news. ```sql select ''http_header'' as component, ''application/rss+xml'' as content_type; @@ -71,15 +241,17 @@ select ''rss'' as component, ''SQLPage blog'' as title, ''https://sql.ophir.dev/blog.sql'' as link, - ''latest news about SQLpage'' as description; + ''latest news about SQLpage'' as description, + ''en'' as language, + ''Technology'' as category, + FALSE as explicit, + ''https://sql.ophir.dev/favicon.ico'' as image_url, + ''Ophir Lojkine'' as author, + ''https://github.com/sponsors/lovasoa'' as funding_url, + ''episodic'' as type; select ''Hello everyone !'' as title, ''https://sql.ophir.dev/blog.sql?post=Come%20see%20me%20build%20twitter%20live%20on%20stage%20in%20Prague'' as link, - ''If some of you european SQLPagers are around Prague this december, I will be giving a talk about SQLPage at pgconf.eu on December 14th.'' as description; -select - ''3 solutions to the 3 layer problem'' as title, - ''https://sql.ophir.dev/blog.sql?post=3%20solutions%20to%20the%203%20layer%20problem'' as link, - ''Some interesting questions emerged from the article Repeating yourself.'' as description, - ''Mon, 04 Dec 2023 00:00:00 GMT'' as pubdate; -``` + ''If some of you european SQLPagers are around Prague this december, I will be giving a talk about SQLPage at pgconf.eu on December 14th.'' as description, + ''Mon, 04 Dec 2023 00:00:00 GMT'' as pubdate; '); \ No newline at end of file diff --git a/sqlpage/templates/rss.handlebars b/sqlpage/templates/rss.handlebars index f8d588f9..a865065d 100644 --- a/sqlpage/templates/rss.handlebars +++ b/sqlpage/templates/rss.handlebars @@ -1,17 +1,41 @@ - - - {{title}} - {{link}} - {{description}} - {{#each_row}} - - {{title}} - {{link}} - - {{~#if pubdate}}{{pubdate}}{{/if}} - - {{/each_row}} - + + + {{title}} + {{link}} + {{description}} + {{#if language}}{{language}}{{/if}} + {{#if category}}{{sub_category}}{{/if}} + {{#if explicit}}{{explicit}}{{/if}} + {{#if image_url}}{{/if}} + {{#if author}}{{author}}{{/if}} + {{#if copyright}}{{copyright}}{{/if}} + {{#if funding_url}}{{funding_text}}{{/if}} + {{#if type}}{{type}}{{/if}} + {{#if complete}}yes{{/if}} + {{#if locked}}yes{{/if}} + {{#if guid}}{{guid}}{{/if}} + {{#each_row}} + + {{title}} + {{link}} + + {{#if pubdate}}{{pubdate}}{{/if}} + {{#if enclosure_url}}{{/if}} + {{#if guid}}{{guid}}{{/if}} + {{#if episode}}{{episode}}{{/if}} + {{#if season}}{{season}}{{/if}} + {{#if episode_type}}{{episode_type}}{{/if}} + {{#if block}}{{block}}{{/if}} + {{#if explicit}}{{explicit}}{{/if}} + {{#if image_url}}{{/if}} + {{#if duration}}{{duration}}{{/if}} + {{#if transcript_url}}{{/if}} + + {{/each_row}} + - From 8d073d9e741eeb2b30af5d044862567c60800000 Mon Sep 17 00:00:00 2001 From: lovasoa Date: Mon, 4 Mar 2024 18:47:28 +0100 Subject: [PATCH 04/15] podcast changes --- .../sqlpage/migrations/37_rss.sql | 41 ++++++++++--------- sqlpage/templates/rss.handlebars | 8 ++-- 2 files changed, 26 insertions(+), 23 deletions(-) diff --git a/examples/official-site/sqlpage/migrations/37_rss.sql b/examples/official-site/sqlpage/migrations/37_rss.sql index 6d9f6e75..3e8cfb4c 100644 --- a/examples/official-site/sqlpage/migrations/37_rss.sql +++ b/examples/official-site/sqlpage/migrations/37_rss.sql @@ -129,7 +129,7 @@ INSERT INTO parameter (component,name,description,type,top_level,optional) VALUE FALSE ),( 'rss', - 'pubdate', + 'date', 'Indicates when the item was published (RFC-822 date-time).', 'TEXT', FALSE, @@ -137,7 +137,7 @@ INSERT INTO parameter (component,name,description,type,top_level,optional) VALUE ),( 'rss', 'enclosure_url', - 'The URL of the audio/video episode content.', + 'For podcast episodes, provides a URL linking to the audio/video episode content, in mp3, m4a, m4v, or mp4 format.', 'URL', FALSE, TRUE @@ -186,8 +186,8 @@ INSERT INTO parameter (component,name,description,type,top_level,optional) VALUE ),( 'rss', 'block', - 'Prevents a specific item from appearing in podcast listening applications. The only valid value for this element is "yes".', - 'TEXT', + 'Prevents a specific item from appearing in podcast listening applications.', + 'BOOLEAN', FALSE, TRUE ),( @@ -238,20 +238,23 @@ VALUES ( select ''http_header'' as component, ''application/rss+xml'' as content_type; select ''shell-empty'' as component; select - ''rss'' as component, - ''SQLPage blog'' as title, - ''https://sql.ophir.dev/blog.sql'' as link, - ''latest news about SQLpage'' as description, - ''en'' as language, - ''Technology'' as category, - FALSE as explicit, - ''https://sql.ophir.dev/favicon.ico'' as image_url, - ''Ophir Lojkine'' as author, - ''https://github.com/sponsors/lovasoa'' as funding_url, - ''episodic'' as type; + ''rss'' as component, + ''SQLPage blog'' as title, + ''https://sql.ophir.dev/blog.sql'' as link, + ''latest news about SQLpage'' as description, + ''en'' as language, + ''Technology'' as category, + FALSE as explicit, + ''https://sql.ophir.dev/favicon.ico'' as image_url, + ''Ophir Lojkine'' as author, + ''https://github.com/sponsors/lovasoa'' as funding_url, + ''episodic'' as type; select - ''Hello everyone !'' as title, - ''https://sql.ophir.dev/blog.sql?post=Come%20see%20me%20build%20twitter%20live%20on%20stage%20in%20Prague'' as link, - ''If some of you european SQLPagers are around Prague this december, I will be giving a talk about SQLPage at pgconf.eu on December 14th.'' as description, - ''Mon, 04 Dec 2023 00:00:00 GMT'' as pubdate; + ''Hello everyone !'' as title, + ''https://sql.ophir.dev/blog.sql?post=Come%20see%20me%20build%20twitter%20live%20on%20stage%20in%20Prague'' as link, + ''If some of you european SQLPagers are around Prague this december, I will be giving a talk about SQLPage at pgconf.eu on December 14th.'' as description, + ''http://127.0.0.1:8080/sqlpage_introduction_video.webm'' as enclosure_url, + 123456789 as enclosure_length, + ''video/webm'' as enclosure_type, + ''2023-12-04'' as date; '); \ No newline at end of file diff --git a/sqlpage/templates/rss.handlebars b/sqlpage/templates/rss.handlebars index a865065d..548fcb51 100644 --- a/sqlpage/templates/rss.handlebars +++ b/sqlpage/templates/rss.handlebars @@ -10,7 +10,7 @@ {{description}} {{#if language}}{{language}}{{/if}} {{#if category}}{{sub_category}}{{/if}} - {{#if explicit}}{{explicit}}{{/if}} + {{#if explicit}}true{{else}}false{{/if}} {{#if image_url}}{{/if}} {{#if author}}{{author}}{{/if}} {{#if copyright}}{{copyright}}{{/if}} @@ -24,14 +24,14 @@ {{title}} {{link}} - {{#if pubdate}}{{pubdate}}{{/if}} + {{#if date}}{{date}}{{/if}} {{#if enclosure_url}}{{/if}} {{#if guid}}{{guid}}{{/if}} {{#if episode}}{{episode}}{{/if}} {{#if season}}{{season}}{{/if}} {{#if episode_type}}{{episode_type}}{{/if}} - {{#if block}}{{block}}{{/if}} - {{#if explicit}}{{explicit}}{{/if}} + {{#if block}}yes{{/if}} + {{#if explicit}}true{{else}}false{{/if}} {{#if image_url}}{{/if}} {{#if duration}}{{duration}}{{/if}} {{#if transcript_url}}{{/if}} From d3ff7f8fc0d46dd2a099608aae620bd76c06f681 Mon Sep 17 00:00:00 2001 From: lovasoa Date: Mon, 4 Mar 2024 18:50:40 +0100 Subject: [PATCH 05/15] documentation --- examples/official-site/sqlpage/migrations/37_rss.sql | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/examples/official-site/sqlpage/migrations/37_rss.sql b/examples/official-site/sqlpage/migrations/37_rss.sql index 3e8cfb4c..05e9d9dd 100644 --- a/examples/official-site/sqlpage/migrations/37_rss.sql +++ b/examples/official-site/sqlpage/migrations/37_rss.sql @@ -257,4 +257,10 @@ select 123456789 as enclosure_length, ''video/webm'' as enclosure_type, ''2023-12-04'' as date; +``` + +Once you have your rss feed ready, you can submit it to podcast directories like +[Apple Podcasts](https://podcastsconnect.apple.com/my-podcasts), +[Spotify](https://podcasters.spotify.com/), +[Google Podcasts](https://podcastsmanager.google.com/)... '); \ No newline at end of file From dc7b5c917763bb8b0436f8b09438e4816a8076b8 Mon Sep 17 00:00:00 2001 From: lovasoa Date: Tue, 5 Mar 2024 12:03:00 +0100 Subject: [PATCH 06/15] format dates as rfc2822 --- sqlpage/templates/rss.handlebars | 2 +- src/templates.rs | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/sqlpage/templates/rss.handlebars b/sqlpage/templates/rss.handlebars index 548fcb51..b4bc2e58 100644 --- a/sqlpage/templates/rss.handlebars +++ b/sqlpage/templates/rss.handlebars @@ -24,7 +24,7 @@ {{title}} {{link}} - {{#if date}}{{date}}{{/if}} + {{#if date}}{{rfc2822_date date}}{{/if}} {{#if enclosure_url}}{{/if}} {{#if guid}}{{guid}}{{/if}} {{#if episode}}{{episode}}{{/if}} diff --git a/src/templates.rs b/src/templates.rs index 877b9314..2b4f08f9 100644 --- a/src/templates.rs +++ b/src/templates.rs @@ -307,6 +307,16 @@ impl AllTemplates { }); handlebars.register_helper("typeof", Box::new(typeof_helper)); + // rfc2822_date: take an ISO date and convert it to an RFC 2822 date + handlebars_helper!(rfc2822_date : |s: str| { + let Ok(date) = chrono::DateTime::parse_from_rfc3339(s) else { + log::error!("Invalid date: {}", s); + return Err(RenderErrorReason::InvalidParamType("date").into()); + }; + date.format("%a, %d %b %Y %T %z").to_string() + }); + handlebars.register_helper("rfc2822_date", Box::new(rfc2822_date)); + let mut this = Self { handlebars, split_templates: FileCache::new(), From 58a60b8de81ab0f346404311087684f346771ca7 Mon Sep 17 00:00:00 2001 From: lovasoa Date: Tue, 5 Mar 2024 16:43:42 +0100 Subject: [PATCH 07/15] extract template helpers --- src/lib.rs | 1 + src/template_helpers.rs | 318 ++++++++++++++++++++++++++++++++++++++++ src/templates.rs | 245 +------------------------------ 3 files changed, 322 insertions(+), 242 deletions(-) create mode 100644 src/template_helpers.rs diff --git a/src/lib.rs b/src/lib.rs index e20c2195..fec374b9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,6 +8,7 @@ pub mod file_cache; pub mod filesystem; pub mod render; pub mod templates; +pub mod template_helpers; pub mod utils; pub mod webserver; diff --git a/src/template_helpers.rs b/src/template_helpers.rs new file mode 100644 index 00000000..2b852dd3 --- /dev/null +++ b/src/template_helpers.rs @@ -0,0 +1,318 @@ +use std::borrow::Cow; + +use anyhow::Context as _; +use handlebars::{ + handlebars_helper, Context, Handlebars, PathAndJson, RenderError, RenderErrorReason, + Renderable, ScopedJson, +}; +use serde_json::Value as JsonValue; + +use crate::utils::static_filename; + +pub fn register_all_helpers(h: &mut Handlebars<'_>) { + register_helper(h, "stringify", stringify_helper); + register_helper(h, "parse_json", parse_json_helper); + + handlebars_helper!(default: |a: Json, b:Json| if a.is_null() {b} else {a}.clone()); + h.register_helper("default", Box::new(default)); + + register_helper(h, "entries", entries_helper); + + // delay helper: store a piece of information in memory that can be output later with flush_delayed + h.register_helper("delay", Box::new(delay_helper)); + h.register_helper("flush_delayed", Box::new(flush_delayed_helper)); + + handlebars_helper!(plus: |a: Json, b:Json| a.as_i64().unwrap_or_default() + b.as_i64().unwrap_or_default()); + h.register_helper("plus", Box::new(plus)); + + handlebars_helper!(minus: |a: Json, b:Json| a.as_i64().unwrap_or_default() - b.as_i64().unwrap_or_default()); + h.register_helper("minus", Box::new(minus)); + + h.register_helper("sum", Box::new(sum_helper)); + + handlebars_helper!(starts_with: |s: str, prefix:str| s.starts_with(prefix)); + h.register_helper("starts_with", Box::new(starts_with)); + + // to_array: convert a value to a single-element array. If the value is already an array, return it as-is. + register_helper(h, "to_array", to_array_helper); + + // array_contains: check if an array contains an element. If the first argument is not an array, it is compared to the second argument. + handlebars_helper!(array_contains: |array: Json, element: Json| match array { + JsonValue::Array(arr) => arr.contains(element), + other => other == element + }); + h.register_helper("array_contains", Box::new(array_contains)); + + // static_path helper: generate a path to a static file. Replaces sqpage.js by sqlpage..js + register_helper(h, "static_path", static_path_helper); + + // icon helper: generate an image with the specified icon + h.register_helper("icon_img", Box::new(icon_img_helper)); + + register_helper(h, "markdown", |x| { + let as_str = match x { + JsonValue::String(s) => Cow::Borrowed(s), + JsonValue::Array(arr) => Cow::Owned( + arr.iter() + .map(|v| v.as_str().unwrap_or_default()) + .collect::>() + .join("\n"), + ), + JsonValue::Null => Cow::Owned(String::new()), + other => Cow::Owned(other.to_string()), + }; + markdown::to_html_with_options(&as_str, &markdown::Options::gfm()) + .map(JsonValue::String) + .map_err(|e| anyhow::anyhow!("markdown error: {e}")) + }); + register_helper(h, "buildinfo", |x| match x { + JsonValue::String(s) if s == "CARGO_PKG_NAME" => Ok(env!("CARGO_PKG_NAME").into()), + JsonValue::String(s) if s == "CARGO_PKG_VERSION" => Ok(env!("CARGO_PKG_VERSION").into()), + other => Err(anyhow::anyhow!("unknown buildinfo key: {other:?}")), + }); + + register_helper(h, "typeof", |x| { + Ok(match x { + JsonValue::Null => "null", + JsonValue::Bool(_) => "boolean", + JsonValue::Number(_) => "number", + JsonValue::String(_) => "string", + JsonValue::Array(_) => "array", + JsonValue::Object(_) => "object", + } + .into()) + }); + + // rfc2822_date: take an ISO date and convert it to an RFC 2822 date + handlebars_helper!(rfc2822_date : |s: str| { + let Ok(date) = chrono::DateTime::parse_from_rfc3339(s) else { + log::error!("Invalid date: {}", s); + return Err(RenderErrorReason::InvalidParamType("date").into()); + }; + date.format("%a, %d %b %Y %T %z").to_string() + }); + h.register_helper("rfc2822_date", Box::new(rfc2822_date)); +} + +fn stringify_helper(v: &JsonValue) -> anyhow::Result { + Ok(v.to_string().into()) +} + +fn parse_json_helper(v: &JsonValue) -> Result { + Ok(match v { + serde_json::value::Value::String(s) => serde_json::from_str(s)?, + other => other.clone(), + }) +} + +fn entries_helper(v: &JsonValue) -> Result { + Ok(match v { + serde_json::value::Value::Object(map) => map + .into_iter() + .map(|(k, v)| serde_json::json!({"key": k, "value": v})) + .collect(), + serde_json::value::Value::Array(values) => values + .iter() + .enumerate() + .map(|(k, v)| serde_json::json!({"key": k, "value": v})) + .collect(), + _ => vec![], + } + .into()) +} + +fn to_array_helper(v: &JsonValue) -> Result { + Ok(match v { + JsonValue::Array(arr) => arr.clone(), + JsonValue::Null => vec![], + JsonValue::String(s) if s.starts_with('[') => { + if let Ok(JsonValue::Array(r)) = serde_json::from_str(s) { + r + } else { + vec![JsonValue::String(s.clone())] + } + } + other => vec![other.clone()], + } + .into()) +} + +fn static_path_helper(v: &JsonValue) -> anyhow::Result { + match v.as_str().with_context(|| "static_path: not a string")? { + "sqlpage.js" => Ok(static_filename!("sqlpage.js").into()), + "sqlpage.css" => Ok(static_filename!("sqlpage.css").into()), + "apexcharts.js" => Ok(static_filename!("apexcharts.js").into()), + other => Err(anyhow::anyhow!("unknown static file: {other:?}")), + } +} + +fn with_each_block<'a, 'reg, 'rc>( + rc: &'a mut handlebars::RenderContext<'reg, 'rc>, + mut action: impl FnMut(&mut handlebars::BlockContext<'rc>, bool) -> Result<(), RenderError>, +) -> Result<(), RenderError> { + let mut blks = Vec::new(); + while let Some(mut top) = rc.block_mut().map(std::mem::take) { + rc.pop_block(); + action(&mut top, rc.block().is_none())?; + blks.push(top); + } + while let Some(blk) = blks.pop() { + rc.push_block(blk); + } + Ok(()) +} + +pub(crate) const DELAYED_CONTENTS: &str = "_delayed_contents"; + +fn delay_helper<'reg, 'rc>( + h: &handlebars::Helper<'rc>, + r: &'reg Handlebars<'reg>, + ctx: &'rc Context, + rc: &mut handlebars::RenderContext<'reg, 'rc>, + _out: &mut dyn handlebars::Output, +) -> handlebars::HelperResult { + let inner = h + .template() + .ok_or(RenderErrorReason::BlockContentRequired)?; + let mut str_out = handlebars::StringOutput::new(); + inner.render(r, ctx, rc, &mut str_out)?; + let mut delayed_render = str_out.into_string()?; + with_each_block(rc, |block, is_last| { + if is_last { + let old_delayed_render = block + .get_local_var(DELAYED_CONTENTS) + .and_then(JsonValue::as_str) + .unwrap_or_default(); + delayed_render += old_delayed_render; + let contents = JsonValue::String(std::mem::take(&mut delayed_render)); + block.set_local_var(DELAYED_CONTENTS, contents); + } + Ok(()) + })?; + Ok(()) +} + +fn flush_delayed_helper<'reg, 'rc>( + _h: &handlebars::Helper<'rc>, + _r: &'reg Handlebars<'reg>, + _ctx: &'rc Context, + rc: &mut handlebars::RenderContext<'reg, 'rc>, + writer: &mut dyn handlebars::Output, +) -> handlebars::HelperResult { + with_each_block(rc, |block_context, _last| { + let delayed = block_context + .get_local_var(DELAYED_CONTENTS) + .and_then(JsonValue::as_str) + .filter(|s| !s.is_empty()); + if let Some(contents) = delayed { + writer.write(contents)?; + block_context.set_local_var(DELAYED_CONTENTS, JsonValue::Null); + } + Ok(()) + }) +} + +fn sum_helper<'reg, 'rc>( + helper: &handlebars::Helper<'rc>, + _r: &'reg Handlebars<'reg>, + _ctx: &'rc Context, + _rc: &mut handlebars::RenderContext<'reg, 'rc>, + writer: &mut dyn handlebars::Output, +) -> handlebars::HelperResult { + let mut sum = 0f64; + for v in helper.params() { + sum += v + .value() + .as_f64() + .ok_or(RenderErrorReason::InvalidParamType("number"))?; + } + write!(writer, "{sum}")?; + Ok(()) +} + +fn icon_img_helper<'reg, 'rc>( + helper: &handlebars::Helper<'rc>, + _r: &'reg Handlebars<'reg>, + _ctx: &'rc Context, + _rc: &mut handlebars::RenderContext<'reg, 'rc>, + writer: &mut dyn handlebars::Output, +) -> handlebars::HelperResult { + let null = handlebars::JsonValue::Null; + let params = [0, 1].map(|i| helper.params().get(i).map_or(&null, PathAndJson::value)); + let name = match params[0] { + JsonValue::String(s) => s, + other => { + log::debug!("icon_img: {other:?} is not an icon name, not rendering anything"); + return Ok(()); + } + }; + let size = params[1].as_u64().unwrap_or(24); + write!( + writer, + "", + static_filename!("tabler-icons.svg") + )?; + Ok(()) +} + +struct JFun { + name: &'static str, + fun: F, +} +impl handlebars::HelperDef for JFun anyhow::Result> { + fn call_inner<'reg: 'rc, 'rc>( + &self, + helper: &handlebars::Helper<'rc>, + _r: &'reg Handlebars<'reg>, + _: &'rc Context, + _rc: &mut handlebars::RenderContext<'reg, 'rc>, + ) -> Result, RenderError> { + let value = helper + .param(0) + .ok_or(RenderErrorReason::ParamNotFoundForIndex(self.name, 0))?; + let result = + (self.fun)(value.value()).map_err(|s| RenderErrorReason::Other(s.to_string()))?; + Ok(ScopedJson::Derived(result)) + } +} + +struct JFun2 { + name: &'static str, + fun: F, +} +impl handlebars::HelperDef for JFun2 JsonValue> { + fn call_inner<'reg: 'rc, 'rc>( + &self, + helper: &handlebars::Helper<'rc>, + _r: &'reg Handlebars<'reg>, + _: &'rc Context, + _rc: &mut handlebars::RenderContext<'reg, 'rc>, + ) -> Result, RenderError> { + let value = helper + .param(0) + .ok_or(RenderErrorReason::ParamNotFoundForIndex(self.name, 0))?; + let value2 = helper + .param(1) + .ok_or(RenderErrorReason::ParamNotFoundForIndex(self.name, 1))?; + Ok(ScopedJson::Derived((self.fun)( + value.value(), + value2.value(), + ))) + } +} + +fn register_helper(h: &mut Handlebars, name: &'static str, fun: F) +where + JFun: handlebars::HelperDef, + F: Send + Sync + 'static, +{ + h.register_helper(name, Box::new(JFun { name, fun })); +} + +fn register_helper2(h: &mut Handlebars, name: &'static str, fun: F) +where + JFun2: handlebars::HelperDef, + F: Send + Sync + 'static, +{ + h.register_helper(name, Box::new(JFun2 { name, fun })); +} diff --git a/src/templates.rs b/src/templates.rs index 2b4f08f9..3bd74d61 100644 --- a/src/templates.rs +++ b/src/templates.rs @@ -1,19 +1,12 @@ use crate::file_cache::AsyncFromStrWithState; -use crate::utils::static_filename; +use crate::template_helpers::register_all_helpers; use crate::{AppState, FileCache, TEMPLATES_DIR}; use async_trait::async_trait; -use handlebars::{ - handlebars_helper, template::TemplateElement, Context, Handlebars, JsonValue, RenderError, - Renderable, Template, -}; -use handlebars::{PathAndJson, RenderErrorReason}; +use handlebars::{template::TemplateElement, Handlebars, Template}; use include_dir::{include_dir, Dir}; -use std::borrow::Cow; use std::path::PathBuf; use std::sync::Arc; -pub(crate) const DELAYED_CONTENTS: &str = "_delayed_contents"; - pub struct SplitTemplate { pub before_list: Template, pub list_content: Template, @@ -79,244 +72,12 @@ pub struct AllTemplates { split_templates: FileCache, } -fn with_each_block<'a, 'reg, 'rc>( - rc: &'a mut handlebars::RenderContext<'reg, 'rc>, - mut action: impl FnMut(&mut handlebars::BlockContext<'rc>, bool) -> Result<(), RenderError>, -) -> Result<(), RenderError> { - let mut blks = Vec::new(); - while let Some(mut top) = rc.block_mut().map(std::mem::take) { - rc.pop_block(); - action(&mut top, rc.block().is_none())?; - blks.push(top); - } - while let Some(blk) = blks.pop() { - rc.push_block(blk); - } - Ok(()) -} - -fn delay_helper<'reg, 'rc>( - h: &handlebars::Helper<'rc>, - r: &'reg Handlebars<'reg>, - ctx: &'rc Context, - rc: &mut handlebars::RenderContext<'reg, 'rc>, - _out: &mut dyn handlebars::Output, -) -> handlebars::HelperResult { - let inner = h - .template() - .ok_or(RenderErrorReason::BlockContentRequired)?; - let mut str_out = handlebars::StringOutput::new(); - inner.render(r, ctx, rc, &mut str_out)?; - let mut delayed_render = str_out.into_string()?; - with_each_block(rc, |block, is_last| { - if is_last { - let old_delayed_render = block - .get_local_var(DELAYED_CONTENTS) - .and_then(JsonValue::as_str) - .unwrap_or_default(); - delayed_render += old_delayed_render; - let contents = JsonValue::String(std::mem::take(&mut delayed_render)); - block.set_local_var(DELAYED_CONTENTS, contents); - } - Ok(()) - })?; - Ok(()) -} - -fn flush_delayed_helper<'reg, 'rc>( - _h: &handlebars::Helper<'rc>, - _r: &'reg Handlebars<'reg>, - _ctx: &'rc Context, - rc: &mut handlebars::RenderContext<'reg, 'rc>, - writer: &mut dyn handlebars::Output, -) -> handlebars::HelperResult { - with_each_block(rc, |block_context, _last| { - let delayed = block_context - .get_local_var(DELAYED_CONTENTS) - .and_then(JsonValue::as_str) - .filter(|s| !s.is_empty()); - if let Some(contents) = delayed { - writer.write(contents)?; - block_context.set_local_var(DELAYED_CONTENTS, JsonValue::Null); - } - Ok(()) - }) -} - -fn sum_helper<'reg, 'rc>( - helper: &handlebars::Helper<'rc>, - _r: &'reg Handlebars<'reg>, - _ctx: &'rc Context, - _rc: &mut handlebars::RenderContext<'reg, 'rc>, - writer: &mut dyn handlebars::Output, -) -> handlebars::HelperResult { - let mut sum = 0f64; - for v in helper.params() { - sum += v - .value() - .as_f64() - .ok_or(RenderErrorReason::InvalidParamType("number"))?; - } - write!(writer, "{sum}")?; - Ok(()) -} - -fn icon_img_helper<'reg, 'rc>( - helper: &handlebars::Helper<'rc>, - _r: &'reg Handlebars<'reg>, - _ctx: &'rc Context, - _rc: &mut handlebars::RenderContext<'reg, 'rc>, - writer: &mut dyn handlebars::Output, -) -> handlebars::HelperResult { - let null = handlebars::JsonValue::Null; - let params = [0, 1].map(|i| helper.params().get(i).map_or(&null, PathAndJson::value)); - let name = match params[0] { - JsonValue::String(s) => s, - other => { - log::debug!("icon_img: {other:?} is not an icon name, not rendering anything"); - return Ok(()); - } - }; - let size = params[1].as_u64().unwrap_or(24); - write!( - writer, - "", - static_filename!("tabler-icons.svg") - )?; - Ok(()) -} - const STATIC_TEMPLATES: Dir = include_dir!("$CARGO_MANIFEST_DIR/sqlpage/templates"); impl AllTemplates { pub fn init() -> anyhow::Result { let mut handlebars = Handlebars::new(); - - handlebars_helper!(stringify: |v: Json| v.to_string()); - handlebars.register_helper("stringify", Box::new(stringify)); - - handlebars_helper!(parse_json: |v: Json| match v { - obj @ serde_json::value::Value::String(s) => - serde_json::from_str(s) - .unwrap_or_else(|_| { - log::warn!("Failed to parse JSON string: {}", s); - obj.clone() - }), - other => other.clone() - }); - handlebars.register_helper("parse_json", Box::new(parse_json)); - - handlebars_helper!(default: |a: Json, b:Json| if a.is_null() {b} else {a}.clone()); - handlebars.register_helper("default", Box::new(default)); - - handlebars_helper!(entries: |v: Json | match v { - serde_json::value::Value::Object(map) => - map.into_iter() - .map(|(k, v)| serde_json::json!({"key": k, "value": v})) - .collect(), - serde_json::value::Value::Array(values) => - values.iter() - .enumerate() - .map(|(k, v)| serde_json::json!({"key": k, "value": v})) - .collect(), - _ => vec![] - }); - - handlebars.register_helper("entries", Box::new(entries)); - - // delay helper: store a piece of information in memory that can be output later with flush_delayed - handlebars.register_helper("delay", Box::new(delay_helper)); - handlebars.register_helper("flush_delayed", Box::new(flush_delayed_helper)); - - handlebars_helper!(plus: |a: Json, b:Json| a.as_i64().unwrap_or_default() + b.as_i64().unwrap_or_default()); - handlebars.register_helper("plus", Box::new(plus)); - - handlebars_helper!(minus: |a: Json, b:Json| a.as_i64().unwrap_or_default() - b.as_i64().unwrap_or_default()); - handlebars.register_helper("minus", Box::new(minus)); - - handlebars.register_helper("sum", Box::new(sum_helper)); - - handlebars_helper!(starts_with: |s: str, prefix:str| s.starts_with(prefix)); - handlebars.register_helper("starts_with", Box::new(starts_with)); - - // to_array: convert a value to a single-element array. If the value is already an array, return it as-is. - handlebars_helper!(to_array: |x: Json| match x { - JsonValue::Array(arr) => arr.clone(), - JsonValue::Null => vec![], - JsonValue::String(s) if s.starts_with('[') => { - if let Ok(JsonValue::Array(r)) = serde_json::from_str(s) { - r - } else { - vec![JsonValue::String(s.clone())] - } - } - other => vec![other.clone()] - }); - handlebars.register_helper("to_array", Box::new(to_array)); - - // array_contains: check if an array contains an element. If the first argument is not an array, it is compared to the second argument. - handlebars_helper!(array_contains: |array: Json, element: Json| match array { - JsonValue::Array(arr) => arr.contains(element), - other => other == element - }); - handlebars.register_helper("array_contains", Box::new(array_contains)); - - // static_path helper: generate a path to a static file. Replaces sqpage.js by sqlpage..js - handlebars_helper!(static_path: |x: str| match x { - "sqlpage.js" => static_filename!("sqlpage.js"), - "sqlpage.css" => static_filename!("sqlpage.css"), - "apexcharts.js" => static_filename!("apexcharts.js"), - unknown => { - log::error!("Unknown static path: {}", unknown); - "!!unknown static path!!" - } - }); - handlebars.register_helper("static_path", Box::new(static_path)); - - // icon helper: generate an image with the specified icon - handlebars.register_helper("icon_img", Box::new(icon_img_helper)); - - handlebars_helper!(markdown_helper: |x: Json| { - let as_str = match x { - JsonValue::String(s) => Cow::Borrowed(s), - JsonValue::Array(arr) => Cow::Owned(arr.iter().map(|v|v.as_str().unwrap_or_default()).collect::>().join("\n")), - JsonValue::Null => Cow::Owned(String::new()), - other => Cow::Owned(other.to_string()) - }; - markdown::to_html_with_options(&as_str, &markdown::Options::gfm()) - .unwrap_or_else(|s|s) - }); - handlebars.register_helper("markdown", Box::new(markdown_helper)); - - handlebars_helper!(buildinfo_helper: |x: str| - match x { - "CARGO_PKG_NAME" => env!("CARGO_PKG_NAME"), - "CARGO_PKG_VERSION" => env!("CARGO_PKG_VERSION"), - _ => "!!unknown buildinfo key!!" - } - ); - handlebars.register_helper("buildinfo", Box::new(buildinfo_helper)); - - handlebars_helper!(typeof_helper: |x: Json| match x { - JsonValue::Null => "null", - JsonValue::Bool(_) => "boolean", - JsonValue::Number(_) => "number", - JsonValue::String(_) => "string", - JsonValue::Array(_) => "array", - JsonValue::Object(_) => "object", - }); - handlebars.register_helper("typeof", Box::new(typeof_helper)); - - // rfc2822_date: take an ISO date and convert it to an RFC 2822 date - handlebars_helper!(rfc2822_date : |s: str| { - let Ok(date) = chrono::DateTime::parse_from_rfc3339(s) else { - log::error!("Invalid date: {}", s); - return Err(RenderErrorReason::InvalidParamType("date").into()); - }; - date.format("%a, %d %b %Y %T %z").to_string() - }); - handlebars.register_helper("rfc2822_date", Box::new(rfc2822_date)); - + register_all_helpers(&mut handlebars); let mut this = Self { handlebars, split_templates: FileCache::new(), From 73224854fe6857e33e2c01b60df81a606e51186e Mon Sep 17 00:00:00 2001 From: lovasoa Date: Tue, 5 Mar 2024 17:18:09 +0100 Subject: [PATCH 08/15] extract more helpers --- src/template_helpers.rs | 150 +++++++++++++++++++--------------------- 1 file changed, 72 insertions(+), 78 deletions(-) diff --git a/src/template_helpers.rs b/src/template_helpers.rs index 2b852dd3..399ed992 100644 --- a/src/template_helpers.rs +++ b/src/template_helpers.rs @@ -9,14 +9,19 @@ use serde_json::Value as JsonValue; use crate::utils::static_filename; +/// Simple json to json helper +type H = fn(&JsonValue) -> JsonValue; +/// Simple json to json helper with error handling +type EH = fn(&JsonValue) -> anyhow::Result; + pub fn register_all_helpers(h: &mut Handlebars<'_>) { - register_helper(h, "stringify", stringify_helper); - register_helper(h, "parse_json", parse_json_helper); + register_helper(h, "stringify", stringify_helper as H); + register_helper(h, "parse_json", parse_json_helper as EH); handlebars_helper!(default: |a: Json, b:Json| if a.is_null() {b} else {a}.clone()); h.register_helper("default", Box::new(default)); - register_helper(h, "entries", entries_helper); + register_helper(h, "entries", entries_helper as EH); // delay helper: store a piece of information in memory that can be output later with flush_delayed h.register_helper("delay", Box::new(delay_helper)); @@ -34,7 +39,7 @@ pub fn register_all_helpers(h: &mut Handlebars<'_>) { h.register_helper("starts_with", Box::new(starts_with)); // to_array: convert a value to a single-element array. If the value is already an array, return it as-is. - register_helper(h, "to_array", to_array_helper); + register_helper(h, "to_array", to_array_helper as EH); // array_contains: check if an array contains an element. If the first argument is not an array, it is compared to the second argument. handlebars_helper!(array_contains: |array: Json, element: Json| match array { @@ -44,44 +49,15 @@ pub fn register_all_helpers(h: &mut Handlebars<'_>) { h.register_helper("array_contains", Box::new(array_contains)); // static_path helper: generate a path to a static file. Replaces sqpage.js by sqlpage..js - register_helper(h, "static_path", static_path_helper); + register_helper(h, "static_path", static_path_helper as EH); // icon helper: generate an image with the specified icon h.register_helper("icon_img", Box::new(icon_img_helper)); - register_helper(h, "markdown", |x| { - let as_str = match x { - JsonValue::String(s) => Cow::Borrowed(s), - JsonValue::Array(arr) => Cow::Owned( - arr.iter() - .map(|v| v.as_str().unwrap_or_default()) - .collect::>() - .join("\n"), - ), - JsonValue::Null => Cow::Owned(String::new()), - other => Cow::Owned(other.to_string()), - }; - markdown::to_html_with_options(&as_str, &markdown::Options::gfm()) - .map(JsonValue::String) - .map_err(|e| anyhow::anyhow!("markdown error: {e}")) - }); - register_helper(h, "buildinfo", |x| match x { - JsonValue::String(s) if s == "CARGO_PKG_NAME" => Ok(env!("CARGO_PKG_NAME").into()), - JsonValue::String(s) if s == "CARGO_PKG_VERSION" => Ok(env!("CARGO_PKG_VERSION").into()), - other => Err(anyhow::anyhow!("unknown buildinfo key: {other:?}")), - }); + register_helper(h, "markdown", markdown_helper as EH); + register_helper(h, "buildinfo", buildinfo_helper as EH); - register_helper(h, "typeof", |x| { - Ok(match x { - JsonValue::Null => "null", - JsonValue::Bool(_) => "boolean", - JsonValue::Number(_) => "number", - JsonValue::String(_) => "string", - JsonValue::Array(_) => "array", - JsonValue::Object(_) => "object", - } - .into()) - }); + register_helper(h, "typeof", typeof_helper as H); // rfc2822_date: take an ISO date and convert it to an RFC 2822 date handlebars_helper!(rfc2822_date : |s: str| { @@ -94,8 +70,8 @@ pub fn register_all_helpers(h: &mut Handlebars<'_>) { h.register_helper("rfc2822_date", Box::new(rfc2822_date)); } -fn stringify_helper(v: &JsonValue) -> anyhow::Result { - Ok(v.to_string().into()) +fn stringify_helper(v: &JsonValue) -> JsonValue { + v.to_string().into() } fn parse_json_helper(v: &JsonValue) -> Result { @@ -146,6 +122,43 @@ fn static_path_helper(v: &JsonValue) -> anyhow::Result { } } +fn typeof_helper(v: &JsonValue) -> JsonValue { + match v { + JsonValue::Null => "null", + JsonValue::Bool(_) => "boolean", + JsonValue::Number(_) => "number", + JsonValue::String(_) => "string", + JsonValue::Array(_) => "array", + JsonValue::Object(_) => "object", + } + .into() +} + +fn markdown_helper(x: &JsonValue) -> anyhow::Result { + let as_str = match x { + JsonValue::String(s) => Cow::Borrowed(s), + JsonValue::Array(arr) => Cow::Owned( + arr.iter() + .map(|v| v.as_str().unwrap_or_default()) + .collect::>() + .join("\n"), + ), + JsonValue::Null => Cow::Owned(String::new()), + other => Cow::Owned(other.to_string()), + }; + markdown::to_html_with_options(&as_str, &markdown::Options::gfm()) + .map(JsonValue::String) + .map_err(|e| anyhow::anyhow!("markdown error: {e}")) +} + +fn buildinfo_helper(x: &JsonValue) -> anyhow::Result { + match x { + JsonValue::String(s) if s == "CARGO_PKG_NAME" => Ok(env!("CARGO_PKG_NAME").into()), + JsonValue::String(s) if s == "CARGO_PKG_VERSION" => Ok(env!("CARGO_PKG_VERSION").into()), + other => Err(anyhow::anyhow!("unknown buildinfo key: {other:?}")), + } +} + fn with_each_block<'a, 'reg, 'rc>( rc: &'a mut handlebars::RenderContext<'reg, 'rc>, mut action: impl FnMut(&mut handlebars::BlockContext<'rc>, bool) -> Result<(), RenderError>, @@ -255,32 +268,27 @@ fn icon_img_helper<'reg, 'rc>( Ok(()) } -struct JFun { - name: &'static str, - fun: F, +trait CanHelp: Send + Sync + 'static { + fn call(&self, v: &JsonValue) -> Result; } -impl handlebars::HelperDef for JFun anyhow::Result> { - fn call_inner<'reg: 'rc, 'rc>( - &self, - helper: &handlebars::Helper<'rc>, - _r: &'reg Handlebars<'reg>, - _: &'rc Context, - _rc: &mut handlebars::RenderContext<'reg, 'rc>, - ) -> Result, RenderError> { - let value = helper - .param(0) - .ok_or(RenderErrorReason::ParamNotFoundForIndex(self.name, 0))?; - let result = - (self.fun)(value.value()).map_err(|s| RenderErrorReason::Other(s.to_string()))?; - Ok(ScopedJson::Derived(result)) + +impl CanHelp for fn(&JsonValue) -> JsonValue { + fn call(&self, v: &JsonValue) -> Result { + Ok(self(v)) + } +} + +impl CanHelp for fn(&JsonValue) -> anyhow::Result { + fn call(&self, v: &JsonValue) -> Result { + self(v).map_err(|e| e.to_string()) } } -struct JFun2 { +struct JFun { name: &'static str, fun: F, } -impl handlebars::HelperDef for JFun2 JsonValue> { +impl handlebars::HelperDef for JFun { fn call_inner<'reg: 'rc, 'rc>( &self, helper: &handlebars::Helper<'rc>, @@ -291,28 +299,14 @@ impl handlebars::HelperDef for JFun2 JsonValue> { let value = helper .param(0) .ok_or(RenderErrorReason::ParamNotFoundForIndex(self.name, 0))?; - let value2 = helper - .param(1) - .ok_or(RenderErrorReason::ParamNotFoundForIndex(self.name, 1))?; - Ok(ScopedJson::Derived((self.fun)( - value.value(), - value2.value(), - ))) + let result = self + .fun + .call(value.value()) + .map_err(|s| RenderErrorReason::Other(s.to_string()))?; + Ok(ScopedJson::Derived(result)) } } -fn register_helper(h: &mut Handlebars, name: &'static str, fun: F) -where - JFun: handlebars::HelperDef, - F: Send + Sync + 'static, -{ +fn register_helper(h: &mut Handlebars, name: &'static str, fun: impl CanHelp) { h.register_helper(name, Box::new(JFun { name, fun })); } - -fn register_helper2(h: &mut Handlebars, name: &'static str, fun: F) -where - JFun2: handlebars::HelperDef, - F: Send + Sync + 'static, -{ - h.register_helper(name, Box::new(JFun2 { name, fun })); -} From 994307d19482cc5b7ae73dee3a40cab2f15fad92 Mon Sep 17 00:00:00 2001 From: lovasoa Date: Tue, 5 Mar 2024 17:20:24 +0100 Subject: [PATCH 09/15] remove unnecassary error handling --- src/template_helpers.rs | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/template_helpers.rs b/src/template_helpers.rs index 399ed992..0f163e49 100644 --- a/src/template_helpers.rs +++ b/src/template_helpers.rs @@ -21,7 +21,7 @@ pub fn register_all_helpers(h: &mut Handlebars<'_>) { handlebars_helper!(default: |a: Json, b:Json| if a.is_null() {b} else {a}.clone()); h.register_helper("default", Box::new(default)); - register_helper(h, "entries", entries_helper as EH); + register_helper(h, "entries", entries_helper as H); // delay helper: store a piece of information in memory that can be output later with flush_delayed h.register_helper("delay", Box::new(delay_helper)); @@ -39,7 +39,7 @@ pub fn register_all_helpers(h: &mut Handlebars<'_>) { h.register_helper("starts_with", Box::new(starts_with)); // to_array: convert a value to a single-element array. If the value is already an array, return it as-is. - register_helper(h, "to_array", to_array_helper as EH); + register_helper(h, "to_array", to_array_helper as H); // array_contains: check if an array contains an element. If the first argument is not an array, it is compared to the second argument. handlebars_helper!(array_contains: |array: Json, element: Json| match array { @@ -81,8 +81,8 @@ fn parse_json_helper(v: &JsonValue) -> Result { }) } -fn entries_helper(v: &JsonValue) -> Result { - Ok(match v { +fn entries_helper(v: &JsonValue) -> JsonValue { + match v { serde_json::value::Value::Object(map) => map .into_iter() .map(|(k, v)| serde_json::json!({"key": k, "value": v})) @@ -94,11 +94,11 @@ fn entries_helper(v: &JsonValue) -> Result { .collect(), _ => vec![], } - .into()) + .into() } -fn to_array_helper(v: &JsonValue) -> Result { - Ok(match v { +fn to_array_helper(v: &JsonValue) -> JsonValue { + match v { JsonValue::Array(arr) => arr.clone(), JsonValue::Null => vec![], JsonValue::String(s) if s.starts_with('[') => { @@ -110,7 +110,7 @@ fn to_array_helper(v: &JsonValue) -> Result { } other => vec![other.clone()], } - .into()) + .into() } fn static_path_helper(v: &JsonValue) -> anyhow::Result { @@ -272,13 +272,13 @@ trait CanHelp: Send + Sync + 'static { fn call(&self, v: &JsonValue) -> Result; } -impl CanHelp for fn(&JsonValue) -> JsonValue { +impl CanHelp for H { fn call(&self, v: &JsonValue) -> Result { Ok(self(v)) } } -impl CanHelp for fn(&JsonValue) -> anyhow::Result { +impl CanHelp for EH { fn call(&self, v: &JsonValue) -> Result { self(v).map_err(|e| e.to_string()) } From 84eb722ce81a0fd993fa56ef822fb9fcdba849c4 Mon Sep 17 00:00:00 2001 From: lovasoa Date: Tue, 5 Mar 2024 17:20:35 +0100 Subject: [PATCH 10/15] fmt --- src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index fec374b9..38845be9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,8 +7,8 @@ pub mod app_config; pub mod file_cache; pub mod filesystem; pub mod render; -pub mod templates; pub mod template_helpers; +pub mod templates; pub mod utils; pub mod webserver; From f33e7e9f7d3679054607f60f651df939b222aa3f Mon Sep 17 00:00:00 2001 From: lovasoa Date: Tue, 5 Mar 2024 17:47:50 +0100 Subject: [PATCH 11/15] two-param helpers --- src/template_helpers.rs | 48 ++++++++++++++++++++++++++++------------- 1 file changed, 33 insertions(+), 15 deletions(-) diff --git a/src/template_helpers.rs b/src/template_helpers.rs index 0f163e49..4ec885b1 100644 --- a/src/template_helpers.rs +++ b/src/template_helpers.rs @@ -13,16 +13,14 @@ use crate::utils::static_filename; type H = fn(&JsonValue) -> JsonValue; /// Simple json to json helper with error handling type EH = fn(&JsonValue) -> anyhow::Result; +/// Helper that takes two arguments +type HH = fn(&JsonValue, &JsonValue) -> JsonValue; pub fn register_all_helpers(h: &mut Handlebars<'_>) { register_helper(h, "stringify", stringify_helper as H); register_helper(h, "parse_json", parse_json_helper as EH); - - handlebars_helper!(default: |a: Json, b:Json| if a.is_null() {b} else {a}.clone()); - h.register_helper("default", Box::new(default)); - + register_helper(h, "default", default_helper as HH); register_helper(h, "entries", entries_helper as H); - // delay helper: store a piece of information in memory that can be output later with flush_delayed h.register_helper("delay", Box::new(delay_helper)); h.register_helper("flush_delayed", Box::new(flush_delayed_helper)); @@ -81,6 +79,14 @@ fn parse_json_helper(v: &JsonValue) -> Result { }) } +fn default_helper(v: &JsonValue, default: &JsonValue) -> JsonValue { + if v.is_null() { + default.clone() + } else { + v.clone() + } +} + fn entries_helper(v: &JsonValue) -> JsonValue { match v { serde_json::value::Value::Object(map) => map @@ -269,18 +275,33 @@ fn icon_img_helper<'reg, 'rc>( } trait CanHelp: Send + Sync + 'static { - fn call(&self, v: &JsonValue) -> Result; + fn call(&self, v: &[PathAndJson]) -> Result; } impl CanHelp for H { - fn call(&self, v: &JsonValue) -> Result { - Ok(self(v)) + fn call(&self, args: &[PathAndJson]) -> Result { + match args { + [v] => Ok(self(v.value())), + _ => Err("expected one argument".to_string()), + } } } impl CanHelp for EH { - fn call(&self, v: &JsonValue) -> Result { - self(v).map_err(|e| e.to_string()) + fn call(&self, args: &[PathAndJson]) -> Result { + match args { + [v] => self(v.value()).map_err(|e| e.to_string()), + _ => Err("expected one argument".to_string()), + } + } +} + +impl CanHelp for HH { + fn call(&self, args: &[PathAndJson]) -> Result { + match args { + [a, b] => Ok(self(a.value(), b.value())), + _ => Err("expected two arguments".to_string()), + } } } @@ -296,13 +317,10 @@ impl handlebars::HelperDef for JFun { _: &'rc Context, _rc: &mut handlebars::RenderContext<'reg, 'rc>, ) -> Result, RenderError> { - let value = helper - .param(0) - .ok_or(RenderErrorReason::ParamNotFoundForIndex(self.name, 0))?; let result = self .fun - .call(value.value()) - .map_err(|s| RenderErrorReason::Other(s.to_string()))?; + .call(helper.params().as_slice()) + .map_err(|s| RenderErrorReason::Other(format!("{}: {}", self.name, s)))?; Ok(ScopedJson::Derived(result)) } } From 344dfc87b15dabeed8cf58245f8bdb5ada28c626 Mon Sep 17 00:00:00 2001 From: lovasoa Date: Tue, 5 Mar 2024 18:24:42 +0100 Subject: [PATCH 12/15] rfc2822_date_helper --- src/template_helpers.rs | 71 ++++++++++++++++++++++++++++------------- 1 file changed, 49 insertions(+), 22 deletions(-) diff --git a/src/template_helpers.rs b/src/template_helpers.rs index 4ec885b1..a773876e 100644 --- a/src/template_helpers.rs +++ b/src/template_helpers.rs @@ -24,17 +24,10 @@ pub fn register_all_helpers(h: &mut Handlebars<'_>) { // delay helper: store a piece of information in memory that can be output later with flush_delayed h.register_helper("delay", Box::new(delay_helper)); h.register_helper("flush_delayed", Box::new(flush_delayed_helper)); - - handlebars_helper!(plus: |a: Json, b:Json| a.as_i64().unwrap_or_default() + b.as_i64().unwrap_or_default()); - h.register_helper("plus", Box::new(plus)); - - handlebars_helper!(minus: |a: Json, b:Json| a.as_i64().unwrap_or_default() - b.as_i64().unwrap_or_default()); - h.register_helper("minus", Box::new(minus)); - + register_helper(h, "plus", plus_helper as HH); + register_helper(h, "plus", minus_helper as HH); h.register_helper("sum", Box::new(sum_helper)); - - handlebars_helper!(starts_with: |s: str, prefix:str| s.starts_with(prefix)); - h.register_helper("starts_with", Box::new(starts_with)); + register_helper(h, "starts_with", starts_with_helper as HH); // to_array: convert a value to a single-element array. If the value is already an array, return it as-is. register_helper(h, "to_array", to_array_helper as H); @@ -51,21 +44,10 @@ pub fn register_all_helpers(h: &mut Handlebars<'_>) { // icon helper: generate an image with the specified icon h.register_helper("icon_img", Box::new(icon_img_helper)); - register_helper(h, "markdown", markdown_helper as EH); register_helper(h, "buildinfo", buildinfo_helper as EH); - register_helper(h, "typeof", typeof_helper as H); - - // rfc2822_date: take an ISO date and convert it to an RFC 2822 date - handlebars_helper!(rfc2822_date : |s: str| { - let Ok(date) = chrono::DateTime::parse_from_rfc3339(s) else { - log::error!("Invalid date: {}", s); - return Err(RenderErrorReason::InvalidParamType("date").into()); - }; - date.format("%a, %d %b %Y %T %z").to_string() - }); - h.register_helper("rfc2822_date", Box::new(rfc2822_date)); + register_helper(h, "rfc2822_date", rfc2822_date_helper as EH); } fn stringify_helper(v: &JsonValue) -> JsonValue { @@ -87,6 +69,36 @@ fn default_helper(v: &JsonValue, default: &JsonValue) -> JsonValue { } } +fn plus_helper(a: &JsonValue, b: &JsonValue) -> JsonValue { + if let (Some(a), Some(b)) = (a.as_i64(), b.as_i64()) { + (a + b).into() + } else if let (Some(a), Some(b)) = (a.as_f64(), b.as_f64()) { + (a + b).into() + } else { + JsonValue::Null + } +} + +fn minus_helper(a: &JsonValue, b: &JsonValue) -> JsonValue { + if let (Some(a), Some(b)) = (a.as_i64(), b.as_i64()) { + (a - b).into() + } else if let (Some(a), Some(b)) = (a.as_f64(), b.as_f64()) { + (a - b).into() + } else { + JsonValue::Null + } +} + +fn starts_with_helper(a: &JsonValue, b: &JsonValue) -> JsonValue { + if let (Some(a), Some(b)) = (a.as_str(), b.as_str()) { + a.starts_with(b) + } else if let (Some(arr1), Some(arr2)) = (a.as_array(), b.as_array()) { + arr1.starts_with(arr2) + } else { + false + } + .into() +} fn entries_helper(v: &JsonValue) -> JsonValue { match v { serde_json::value::Value::Object(map) => map @@ -165,6 +177,21 @@ fn buildinfo_helper(x: &JsonValue) -> anyhow::Result { } } +// rfc2822_date: take an ISO date and convert it to an RFC 2822 date +fn rfc2822_date_helper(v: &JsonValue) -> anyhow::Result { + let date = match v { + JsonValue::String(s) => chrono::DateTime::parse_from_rfc3339(s)?, + JsonValue::Number(n) => { + chrono::DateTime::from_timestamp(n.as_i64().with_context(|| "not a timestamp")?, 0) + .with_context(|| "invalid timestamp")? + .into() + } + other => anyhow::bail!("expected a date, got {other:?}"), + }; + // format: Thu, 01 Jan 1970 00:00:00 +0000 + Ok(date.format("%a, %d %b %Y %T %z").to_string().into()) +} + fn with_each_block<'a, 'reg, 'rc>( rc: &'a mut handlebars::RenderContext<'reg, 'rc>, mut action: impl FnMut(&mut handlebars::BlockContext<'rc>, bool) -> Result<(), RenderError>, From 9896afe0522dd6639ed96693b1fd70ecf2b7bf12 Mon Sep 17 00:00:00 2001 From: lovasoa Date: Tue, 5 Mar 2024 18:28:38 +0100 Subject: [PATCH 13/15] test rfc date formatter --- src/template_helpers.rs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/template_helpers.rs b/src/template_helpers.rs index a773876e..dbdefc65 100644 --- a/src/template_helpers.rs +++ b/src/template_helpers.rs @@ -355,3 +355,14 @@ impl handlebars::HelperDef for JFun { fn register_helper(h: &mut Handlebars, name: &'static str, fun: impl CanHelp) { h.register_helper(name, Box::new(JFun { name, fun })); } + +#[test] +fn test_rfc2822_date() { + assert_eq!( + rfc2822_date_helper(&JsonValue::String("1970-01-02T03:04:05+02:00".into())) + .unwrap() + .as_str() + .unwrap(), + "Fri, 02 Jan 1970 03:04:05 +0200" + ); +} From 2eec29d8ae24e8217abc07e2d896e98f453fef91 Mon Sep 17 00:00:00 2001 From: lovasoa Date: Tue, 5 Mar 2024 19:31:20 +0100 Subject: [PATCH 14/15] accept dates with or without time --- examples/official-site/rss.sql | 34 +++++++++++-------- .../sqlpage/migrations/37_rss.sql | 7 ++++ sqlpage/templates/rss.handlebars | 3 +- src/template_helpers.rs | 19 +++++++++-- 4 files changed, 45 insertions(+), 18 deletions(-) diff --git a/examples/official-site/rss.sql b/examples/official-site/rss.sql index f07a1087..ca688063 100644 --- a/examples/official-site/rss.sql +++ b/examples/official-site/rss.sql @@ -1,16 +1,20 @@ -select 'http_header' as component, 'application/rss+xml' as "Content-Type"; +select 'http_header' as component, + 'application/rss+xml' as "Content-Type"; select 'shell-empty' as component; -select - 'rss' as component, - 'SQLPage blog' as title, - 'https://sql.ophir.dev/blog.sql' as link, - 'latest news about SQLpage' as description; -select - 'Hello everyone !' as title, - 'https://sql.ophir.dev/blog.sql?post=Come%20see%20me%20build%20twitter%20live%20on%20stage%20in%20Prague' as link, - 'If some of you european SQLPagers are around Prague this december, I will be giving a talk about SQLPage at pgconf.eu on December 14th.' as description; -select - '3 solutions to the 3 layer problem' as title, - 'https://sql.ophir.dev/blog.sql?post=3%20solutions%20to%20the%203%20layer%20problem' as link, - 'Some interesting questions emerged from the article Repeating yourself.' as description, - 'Mon, 04 Dec 2023 00:00:00 GMT' as pubdate; +select 'rss' as component, + 'SQLPage blog' as title, + 'https://sql.ophir.dev/blog.sql' as link, + 'latest news about SQLpage' as description, + 'en' as language, + 'https://sql.ophir.dev/rss.sql' as self_link, + 'Technology' as category, + '2de3f968-9928-5ec6-9653-6fc6fe382cfd' as guid; +SELECT title, + description, + CASE + WHEN external_url IS NOT NULL THEN external_url + ELSE 'https://sql.ophir.dev/blog.sql?post=' || title + END AS link, + created_at AS date +FROM blog_posts +ORDER BY created_at DESC; \ No newline at end of file diff --git a/examples/official-site/sqlpage/migrations/37_rss.sql b/examples/official-site/sqlpage/migrations/37_rss.sql index 05e9d9dd..0af9fa69 100644 --- a/examples/official-site/sqlpage/migrations/37_rss.sql +++ b/examples/official-site/sqlpage/migrations/37_rss.sql @@ -71,6 +71,13 @@ INSERT INTO parameter (component,name,description,type,top_level,optional) VALUE 'TEXT', TRUE, TRUE +),( + 'rss', + 'self_link', + 'URL of the RSS feed.', + 'URL', + TRUE, + TRUE ),( 'rss', 'funding_url', diff --git a/sqlpage/templates/rss.handlebars b/sqlpage/templates/rss.handlebars index b4bc2e58..901efe20 100644 --- a/sqlpage/templates/rss.handlebars +++ b/sqlpage/templates/rss.handlebars @@ -19,11 +19,12 @@ {{#if complete}}yes{{/if}} {{#if locked}}yes{{/if}} {{#if guid}}{{guid}}{{/if}} + {{#if self_link}}{{/if}} {{#each_row}} {{title}} {{link}} - + {{description}} {{#if date}}{{rfc2822_date date}}{{/if}} {{#if enclosure_url}}{{/if}} {{#if guid}}{{guid}}{{/if}} diff --git a/src/template_helpers.rs b/src/template_helpers.rs index dbdefc65..6d356382 100644 --- a/src/template_helpers.rs +++ b/src/template_helpers.rs @@ -179,8 +179,16 @@ fn buildinfo_helper(x: &JsonValue) -> anyhow::Result { // rfc2822_date: take an ISO date and convert it to an RFC 2822 date fn rfc2822_date_helper(v: &JsonValue) -> anyhow::Result { - let date = match v { - JsonValue::String(s) => chrono::DateTime::parse_from_rfc3339(s)?, + let date: chrono::DateTime = match v { + JsonValue::String(s) => { + // we accept both dates with and without time + chrono::DateTime::parse_from_rfc3339(s) + .or_else(|_| { + chrono::NaiveDate::parse_from_str(s, "%Y-%m-%d") + .map(|d| d.and_hms_opt(0, 0, 0).unwrap().and_utc().fixed_offset()) + }) + .with_context(|| format!("invalid date: {s}"))? + } JsonValue::Number(n) => { chrono::DateTime::from_timestamp(n.as_i64().with_context(|| "not a timestamp")?, 0) .with_context(|| "invalid timestamp")? @@ -365,4 +373,11 @@ fn test_rfc2822_date() { .unwrap(), "Fri, 02 Jan 1970 03:04:05 +0200" ); + assert_eq!( + rfc2822_date_helper(&JsonValue::String("1970-01-02".into())) + .unwrap() + .as_str() + .unwrap(), + "Fri, 02 Jan 1970 00:00:00 +0000" + ); } From 0ac70949d8a78cdd012d589c085ad3c7813b6966 Mon Sep 17 00:00:00 2001 From: lovasoa Date: Tue, 5 Mar 2024 19:41:03 +0100 Subject: [PATCH 15/15] remove unneeded line breaks --- examples/official-site/rss.sql | 3 ++- sqlpage/templates/rss.handlebars | 44 ++++++++++++++++---------------- 2 files changed, 24 insertions(+), 23 deletions(-) diff --git a/examples/official-site/rss.sql b/examples/official-site/rss.sql index ca688063..3bbe4153 100644 --- a/examples/official-site/rss.sql +++ b/examples/official-site/rss.sql @@ -15,6 +15,7 @@ SELECT title, WHEN external_url IS NOT NULL THEN external_url ELSE 'https://sql.ophir.dev/blog.sql?post=' || title END AS link, - created_at AS date + created_at AS date, + false AS explicit FROM blog_posts ORDER BY created_at DESC; \ No newline at end of file diff --git a/sqlpage/templates/rss.handlebars b/sqlpage/templates/rss.handlebars index 901efe20..779e911a 100644 --- a/sqlpage/templates/rss.handlebars +++ b/sqlpage/templates/rss.handlebars @@ -8,34 +8,34 @@ {{title}} {{link}} {{description}} - {{#if language}}{{language}}{{/if}} - {{#if category}}{{sub_category}}{{/if}} + {{~#if language}}{{language}}{{/if}} + {{~#if category}}{{sub_category}}{{/if}} {{#if explicit}}true{{else}}false{{/if}} - {{#if image_url}}{{/if}} - {{#if author}}{{author}}{{/if}} - {{#if copyright}}{{copyright}}{{/if}} - {{#if funding_url}}{{funding_text}}{{/if}} - {{#if type}}{{type}}{{/if}} - {{#if complete}}yes{{/if}} - {{#if locked}}yes{{/if}} - {{#if guid}}{{guid}}{{/if}} - {{#if self_link}}{{/if}} + {{~#if image_url}}{{/if}} + {{~#if author}}{{author}}{{/if}} + {{~#if copyright}}{{copyright}}{{/if}} + {{~#if funding_url}}{{funding_text}}{{/if}} + {{~#if type}}{{type}}{{/if}} + {{~#if complete}}yes{{/if}} + {{~#if locked}}yes{{/if}} + {{~#if guid}}{{guid}}{{/if}} + {{~#if self_link}}{{/if}} {{#each_row}} {{title}} {{link}} {{description}} - {{#if date}}{{rfc2822_date date}}{{/if}} - {{#if enclosure_url}}{{/if}} - {{#if guid}}{{guid}}{{/if}} - {{#if episode}}{{episode}}{{/if}} - {{#if season}}{{season}}{{/if}} - {{#if episode_type}}{{episode_type}}{{/if}} - {{#if block}}yes{{/if}} - {{#if explicit}}true{{else}}false{{/if}} - {{#if image_url}}{{/if}} - {{#if duration}}{{duration}}{{/if}} - {{#if transcript_url}}{{/if}} + {{~#if date}}{{rfc2822_date date}}{{/if}} + {{~#if enclosure_url}}{{/if}} + {{~#if guid}}{{guid}}{{/if}} + {{~#if episode}}{{episode}}{{/if}} + {{~#if season}}{{season}}{{/if}} + {{~#if episode_type}}{{episode_type}}{{/if}} + {{~#if block}}yes{{/if}} + {{~#if (not (eq explicit NULL))}}{{#if explicit}}true{{else}}false{{/if}}{{/if}} + {{~#if image_url}}{{/if}} + {{~#if duration}}{{duration}}{{/if}} + {{~#if transcript_url}}{{/if}} {{/each_row}}