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:
parent
2e766781eb
commit
e96a39448a
6 changed files with 116 additions and 6 deletions
|
@ -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
|
||||
|
|
16
src/small/services/github_client.py
Normal file
16
src/small/services/github_client.py
Normal 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
|
|
@ -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 = [Text(content=p["text"].strip(), type=p["type"])]
|
||||
children = [
|
||||
IFrame(
|
||||
src=iframe["iframeSrc"] or iframe["href"],
|
||||
width=iframe["iframeWidth"],
|
||||
height=iframe["iframeHeight"],
|
||||
)
|
||||
]
|
||||
else:
|
||||
children = [Text(content=p["text"], type=p["type"])]
|
||||
paragraphs.append(Paragraph(children=children))
|
||||
|
||||
return Page(
|
||||
|
|
|
@ -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 */
|
||||
|
|
|
@ -14,6 +14,18 @@
|
|||
<p>
|
||||
<img src="{{ child.src }}" alt="{{ child.alt }}" width="{{ child.width }}" height="{{ child.height }}">
|
||||
</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 %}
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in a new issue