forked from PrivateCoffee/small
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
|
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
|
||||||
|
|
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 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:
|
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))
|
paragraphs.append(Paragraph(children=children))
|
||||||
|
|
||||||
return Page(
|
return Page(
|
||||||
|
|
|
@ -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 */
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
Loading…
Reference in a new issue