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.
This commit is contained in:
Kumi 2024-09-19 18:45:30 +02:00
parent 2e766781eb
commit e96a39448a
Signed by: kumi
GPG key ID: ECBCC9082395383F
6 changed files with 116 additions and 6 deletions

View file

@ -16,9 +16,23 @@ class Image:
height: int height: int
@dataclass
class IFrame:
src: str
width: int
height: int
@dataclass
class GithubGist:
id: str
filename: str = None
content: str = None
@dataclass @dataclass
class Paragraph: class Paragraph:
children: List[Union[Text, Image]] children: List[Union[Text, Image, IFrame, GithubGist]]
@dataclass @dataclass

View file

@ -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

View file

@ -2,7 +2,7 @@ import requests
from flask import url_for 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 from datetime import datetime
@ -30,6 +30,14 @@ class MediumClient:
originalWidth originalWidth
originalHeight originalHeight
} }
iframe {
mediaResource {
href
iframeSrc
iframeWidth
iframeHeight
}
}
} }
} }
} }
@ -42,6 +50,9 @@ class MediumClient:
response = requests.post(url, json={"query": query}) response = requests.post(url, json={"query": query})
data = response.json()["data"]["post"] data = response.json()["data"]["post"]
if not data:
return None
paragraphs = [] paragraphs = []
for p in data["content"]["bodyModel"]["paragraphs"]: for p in data["content"]["bodyModel"]["paragraphs"]:
if p["type"] == "IMG": if p["type"] == "IMG":
@ -57,8 +68,21 @@ class MediumClient:
height=p["metadata"]["originalHeight"], 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: else:
children = [Text(content=p["text"].strip(), type=p["type"])] children = [Text(content=p["text"], type=p["type"])]
paragraphs.append(Paragraph(children=children)) paragraphs.append(Paragraph(children=children))
return Page( return Page(

View file

@ -72,11 +72,39 @@ article img {
margin: 20px 0; margin: 20px 0;
} }
/* Code Block Styles */
pre { pre {
background-color: #f4f4f4; background-color: #f4f4f4;
padding: 10px; border: 1px solid #ddd;
border-left: 3px solid #1a73e8; border-left: 3px solid #f36d33;
overflow-x: auto; 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 */ /* Error Page Styles */

View file

@ -14,6 +14,18 @@
<p> <p>
<img src="{{ child.src }}" alt="{{ child.alt }}" width="{{ child.width }}" height="{{ child.height }}"> <img src="{{ child.src }}" alt="{{ child.alt }}" width="{{ child.width }}" height="{{ child.height }}">
</p> </p>
{% elif child.__class__.__name__ == 'IFrame' %}
<iframe src="{{ child.src }}" width="{{ child.width }}" height="{{ child.height }}" frameborder="0" allowfullscreen></iframe>
{% elif child.__class__.__name__ == 'GithubGist' %}
{% if child.content %}
{% for filename, content in child.content.items() %}
<h4>{{ filename }}</h4>
<pre><code>{{ content }}</code></pre>
{% endfor %}
<a href="{{ child.url }}">View Gist on GitHub</a>
{% else %}
<p>Failed to load GitHub Gist</p>
{% endif %}
{% endif %} {% endif %}
{% endfor %} {% endfor %}
{% endfor %} {% endfor %}

View file

@ -1,6 +1,8 @@
from flask import Blueprint, render_template, abort from flask import Blueprint, render_template, abort
from werkzeug.exceptions import NotFound
from small.services.medium_client import MediumClient from small.services.medium_client import MediumClient
from small.services.github_client import GithubClient
from small.utils.parse_article_id import parse_article_id from small.utils.parse_article_id import parse_article_id
bp = Blueprint("articles", __name__) bp = Blueprint("articles", __name__)
@ -14,7 +16,21 @@ def article(article_url):
try: try:
page = MediumClient.get_post(article_id) 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) return render_template("article.html", page=page)
except Exception as e: except Exception as e:
if isinstance(e, NotFound):
raise
print(f"Error fetching article: {str(e)}") print(f"Error fetching article: {str(e)}")
abort(500) abort(500)