From e96a39448a45fc4f9825f6632e76abfba839b01a Mon Sep 17 00:00:00 2001 From: Kumi Date: Thu, 19 Sep 2024 18:45:30 +0200 Subject: [PATCH] feat: add support for iFrames and GitHub Gists in articles Enhanced the Article model to include IFrame and GithubGist nodes, enabling rendering of embedded content such as iframes and GitHub gists. Implemented a new GithubClient to fetch gist content and updated MediumClient to handle iframe and gist types. Added styles and template support for iframes and gists in articles. These changes improve the flexibility of article content, enabling richer media experiences. --- src/small/models/nodes.py | 16 +++++++++++++- src/small/services/github_client.py | 16 ++++++++++++++ src/small/services/medium_client.py | 28 ++++++++++++++++++++++-- src/small/static/css/style.css | 34 ++++++++++++++++++++++++++--- src/small/templates/article.html | 12 ++++++++++ src/small/views/article.py | 16 ++++++++++++++ 6 files changed, 116 insertions(+), 6 deletions(-) create mode 100644 src/small/services/github_client.py diff --git a/src/small/models/nodes.py b/src/small/models/nodes.py index 50b5f00..29bcc1d 100644 --- a/src/small/models/nodes.py +++ b/src/small/models/nodes.py @@ -16,9 +16,23 @@ class Image: height: int +@dataclass +class IFrame: + src: str + width: int + height: int + + +@dataclass +class GithubGist: + id: str + filename: str = None + content: str = None + + @dataclass class Paragraph: - children: List[Union[Text, Image]] + children: List[Union[Text, Image, IFrame, GithubGist]] @dataclass diff --git a/src/small/services/github_client.py b/src/small/services/github_client.py new file mode 100644 index 0000000..305efed --- /dev/null +++ b/src/small/services/github_client.py @@ -0,0 +1,16 @@ +import requests + + +class GithubClient: + @staticmethod + def get_gist(gist_id): + url = f"https://api.github.com/gists/{gist_id}" + response = requests.get(url) + if response.status_code == 200: + data = response.json() + files = data["files"] + return { + filename: file_data["content"] for filename, file_data in files.items() + } + else: + return None diff --git a/src/small/services/medium_client.py b/src/small/services/medium_client.py index 90444ec..e3b66e6 100644 --- a/src/small/services/medium_client.py +++ b/src/small/services/medium_client.py @@ -2,7 +2,7 @@ import requests from flask import url_for -from small.models.nodes import Page, Paragraph, Text, Image +from small.models.nodes import Page, Paragraph, Text, Image, IFrame, GithubGist from datetime import datetime @@ -30,6 +30,14 @@ class MediumClient: originalWidth originalHeight } + iframe { + mediaResource { + href + iframeSrc + iframeWidth + iframeHeight + } + } } } } @@ -42,6 +50,9 @@ class MediumClient: response = requests.post(url, json={"query": query}) data = response.json()["data"]["post"] + if not data: + return None + paragraphs = [] for p in data["content"]["bodyModel"]["paragraphs"]: if p["type"] == "IMG": @@ -57,8 +68,21 @@ class MediumClient: height=p["metadata"]["originalHeight"], ) ] + elif p["type"] == "IFRAME": + iframe = p["iframe"]["mediaResource"] + if "gist.github.com" in iframe["href"]: + gist_id = iframe["href"].split("/")[-1] + children = [GithubGist(id=gist_id)] + else: + children = [ + IFrame( + src=iframe["iframeSrc"] or iframe["href"], + width=iframe["iframeWidth"], + height=iframe["iframeHeight"], + ) + ] else: - children = [Text(content=p["text"].strip(), type=p["type"])] + children = [Text(content=p["text"], type=p["type"])] paragraphs.append(Paragraph(children=children)) return Page( diff --git a/src/small/static/css/style.css b/src/small/static/css/style.css index c21ffc9..6906d5a 100644 --- a/src/small/static/css/style.css +++ b/src/small/static/css/style.css @@ -72,11 +72,39 @@ article img { margin: 20px 0; } +/* Code Block Styles */ pre { background-color: #f4f4f4; - padding: 10px; - border-left: 3px solid #1a73e8; - overflow-x: auto; + border: 1px solid #ddd; + border-left: 3px solid #f36d33; + color: #666; + page-break-inside: avoid; + font-family: monospace; + font-size: 15px; + line-height: 1.6; + margin-bottom: 1.6em; + max-width: 100%; + overflow: auto; + padding: 1em 1.5em; + display: block; + word-wrap: break-word; +} + +/* IFrame Styles */ +iframe { + max-width: 100%; + border: none; + margin: 20px 0; +} + +/* GitHub Gist Styles */ +.gist { + margin: 20px 0; +} + +.gist h4 { + margin-bottom: 10px; + color: #333; } /* Error Page Styles */ diff --git a/src/small/templates/article.html b/src/small/templates/article.html index ad679a2..c9a8353 100644 --- a/src/small/templates/article.html +++ b/src/small/templates/article.html @@ -14,6 +14,18 @@

{{ child.alt }}

+ {% elif child.__class__.__name__ == 'IFrame' %} + + {% elif child.__class__.__name__ == 'GithubGist' %} + {% if child.content %} + {% for filename, content in child.content.items() %} +

{{ filename }}

+
{{ content }}
+ {% endfor %} + View Gist on GitHub + {% else %} +

Failed to load GitHub Gist

+ {% endif %} {% endif %} {% endfor %} {% endfor %} diff --git a/src/small/views/article.py b/src/small/views/article.py index f52aa08..ce2484f 100644 --- a/src/small/views/article.py +++ b/src/small/views/article.py @@ -1,6 +1,8 @@ from flask import Blueprint, render_template, abort +from werkzeug.exceptions import NotFound from small.services.medium_client import MediumClient +from small.services.github_client import GithubClient from small.utils.parse_article_id import parse_article_id bp = Blueprint("articles", __name__) @@ -14,7 +16,21 @@ def article(article_url): try: page = MediumClient.get_post(article_id) + + if not page: + abort(404) + + for paragraph in page.content: + for child in paragraph.children: + if child.__class__.__name__ == "GithubGist": + gist_id = child.id + gist = GithubClient.get_gist(gist_id) + child.content = gist + return render_template("article.html", page=page) except Exception as e: + if isinstance(e, NotFound): + raise + print(f"Error fetching article: {str(e)}") abort(500)