Skip to content

Commit

Permalink
Merge pull request #9 from syeopite/update-npf
Browse files Browse the repository at this point in the history
Update npf-renderer to 0.12.0
  • Loading branch information
syeopite committed Feb 6, 2024
2 parents 714bed7 + 59646de commit 32a12c7
Show file tree
Hide file tree
Showing 20 changed files with 500 additions and 31 deletions.
2 changes: 1 addition & 1 deletion assets/css/base.css
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,7 @@ li.control-bar-action:hover {
transform: translateX(-50%);
overflow: hidden;
width: fit-content;
z-index: 1;
z-index: 2;
}

.control-bar-dropdown-menu li {
Expand Down
3 changes: 3 additions & 0 deletions assets/css/blog.css
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@
}

#banner {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 10px 10px 0px 0px;
}

Expand Down
62 changes: 61 additions & 1 deletion assets/css/post-layout.css
Original file line number Diff line number Diff line change
Expand Up @@ -84,4 +84,64 @@ a.inline-link, a.inline-mention {
color: #6b7280;
text-transform: uppercase;
font-size: 12px;
}
}

a.toggle-poll-results {
color: unset;
text-decoration: underline;
}

.poll-block {
gap: 15px;
border: unset;

padding: 0 10px;
margin: 10px 0;
}

.poll-block > header > h3 {
font-weight: normal;
margin: 0;
font-size: 24px;
}

.poll-choice {
border: 0;
border-radius: 10px;
background: #edeeee;
padding: 8px 18px;
overflow: hidden;
}

.poll-choice > .answer {
font-weight: 550;
}

.vote-proportion {
background: #dcdcdc;
border-radius: 10px 0px 0px 10px;
}

.poll-winner >.vote-proportion {
background: #BABABA;
}

.poll-block footer {
font-size: 15px;
margin-top: 0;
color: #6b7280;
}

.toggle-poll-results {
display: block;
margin-top: 10px;
margin-bottom: 20px;
}

.toggle-poll-results:hover {
font-weight: 700;
}

.separator {
font-size: 12px;
}
110 changes: 110 additions & 0 deletions assets/js/post.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
"use strict";

function requestPollResults(poll_element, pollId) {
return new Promise(function (resolve, reject) {
let post = poll_element.closest(".post");
const blogName = post.getElementsByClassName("blog-name")[0].innerHTML;
const postId = post.dataset.postId;

const pollResultsFetch = fetch(`/api/v1/poll/${blogName}/${postId}/${pollId}/results`);

pollResultsFetch.then((results) => {
return results.json();
}).then((parsed_results) => {
return resolve(parsed_results);
});
})
}

function fill_poll_results(poll_element, results) {
const sorted_poll_results = Object.entries(results.response.results).sort((a,b) => (a[1]-b[1])).reverse();

// First we must find the total number of votes and the winner(s) of the poll
let total_votes = 0;

// Answer ID to winner status and amount of votes
const processed_poll_results = {};
const winner_vote_count = sorted_poll_results[0][1];

for (let [answer_id, votes] of sorted_poll_results) {
processed_poll_results[answer_id] = {"is_winner": (winner_vote_count == votes), "votes": votes};
total_votes += votes
}

const answerIdChoiceElementArray = [];
const pollBody = poll_element.getElementsByClassName("poll-body")[0];

for (let choiceElement of pollBody.children) {
answerIdChoiceElementArray.push([choiceElement.dataset.answerId, choiceElement]);
}

for (let [answer_id, choiceElement] of answerIdChoiceElementArray) {
const answer_results = processed_poll_results[answer_id] || {"is_winner": false, "votes": 0};
const is_winner = answer_results.is_winner;
const answer_votes = answer_results.votes;

let numericalVoteProportion

if (answer_votes == 0 || total_votes == 0) {
numericalVoteProportion = 0;
} else {
numericalVoteProportion = answer_votes/total_votes;
}

const voteProportionElement = document.createElement("span");
voteProportionElement.classList.add("vote-proportion");
voteProportionElement["style"] = `width: ${((numericalVoteProportion) * 100).toFixed(3)}%;`;

const voteCountElement = document.createElement("span");
voteCountElement.classList.add("vote-count");

// A greater rounding precision is needed here
const comparison = Math.round((numericalVoteProportion) * 10000)/10000
if ((comparison > 0.001) || comparison == 0) {
voteCountElement.innerHTML = new Intl.NumberFormat("en-US", {style: "percent", maximumSignificantDigits: 3}).format(Math.round((numericalVoteProportion) * 1000)/1000);
} else {
voteCountElement.innerHTML = "< 0.1%";
}

if (is_winner) {
choiceElement.classList.add("poll-winner");
}

choiceElement.appendChild(voteProportionElement);
choiceElement.appendChild(voteCountElement);
}

const pollMetadataElement = poll_element.getElementsByClassName("poll-metadata")[0]

const totalVotesElement = document.createElement("span")
totalVotesElement.innerHTML = `${total_votes} votes`

const separatorElement = document.createElement("span")
separatorElement.innerHTML = '•'
separatorElement.classList.add("separator")

const pollTimestamp = pollMetadataElement.children[0]

const newMetadataContents = document.createDocumentFragment()
newMetadataContents.append(totalVotesElement, separatorElement, pollTimestamp.cloneNode(true))

pollTimestamp.replaceWith(newMetadataContents)

poll_element.classList.add("populated")
}

const pollBlocks = document.getElementsByClassName("poll-block");


function populate_polls() {
for (let poll of pollBlocks) {
if (poll.classList.contains("populated")) {
continue;
}

requestPollResults(poll, poll.dataset.pollId).then((answers) => {fill_poll_results(poll, answers)})
}
};

// TODO lazy load polls
populate_polls();
4 changes: 3 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,19 @@ frozenlist==1.4.1
html5tagger==1.3.0
httptools==0.6.1
idna==3.6
intervaltree==3.1.0
Jinja2==3.1.2
MarkupSafe==2.1.4
multidict==6.0.5
npf-renderer==0.11.1
npf-renderer==0.12.0
orjson==3.8.0
pycares==4.4.0
pycparser==2.21
PyYAML==6.0.1
sanic==23.3.0
sanic-ext==23.6.0
sanic-routing==23.6.0
sortedcontainers==2.4.0
tracerite==1.1.1
ujson==5.9.0
uvloop==0.19.0
Expand Down
172 changes: 172 additions & 0 deletions src/helpers/ext_npf_renderer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
"""Extensions to npf-renderer to allow asynchronous code and some other custom styling"""

import dominate
import npf_renderer

from .helpers import url_handler


class NPFParser(npf_renderer.parse.Parser):
def __init__(self, content, poll_callback=None):
super().__init__(content, poll_callback)

async def _parse_poll_block(self):
poll_id = self.current.get("clientId") or self.current.get("client_id")
if poll_id is None:
raise ValueError("Invalid poll ID")

question = self.current["question"]

answers = {}
for raw_ans in self.current["answers"]:
answer_id = raw_ans.get("clientId") or raw_ans.get("client_id")
answer_text = raw_ans.get("answerText") or raw_ans.get("answer_text")

if answer_id is None or answer_text is None:
raise ValueError("Invalid poll answer")

answers[answer_id] = answer_text

votes = None
total_votes = None

if self.poll_result_callback:
callback_response = await self.poll_result_callback(poll_id)

#{answer_id: vote_count}
raw_results = callback_response["results"].items()
processed_results = sorted(raw_results, key=lambda item: -item[1])

votes_dict = {}
total_votes = 0

for index, results in enumerate(processed_results):
vote_count = results[1]
total_votes += vote_count

if index == 0:
votes_dict[results[0]] = npf_renderer.objects.poll_block.PollResult(is_winner=True, vote_count=vote_count)
else:
votes_dict[results[0]] = npf_renderer.objects.poll_block.PollResult(is_winner=False, vote_count=vote_count)

votes = npf_renderer.objects.poll_block.PollResults(
timestamp=callback_response["timestamp"],
results=votes_dict
)

creation_timestamp = self.current["timestamp"]
expires_after = self.current["settings"]["expireAfter"]

return npf_renderer.objects.poll_block.PollBlock(
poll_id=poll_id,
question=question,
answers=answers,

creation_timestamp=int(creation_timestamp),
expires_after=int(expires_after),

votes=votes,
total_votes=total_votes,
)

async def __parse_block(self):
"""Parses a content block and appends the result to self.parsed_result
Works by routing specific content types to corresponding parse methods
"""

match self.current["type"]:
case "text":
block = self._parse_text()
case "image":
block = self._parse_image_block()
case "link":
block = self._parse_link_block()
case "audio":
block = self._parse_audio_block()
case "video":
block = self._parse_video_block()
case "poll":
block = await self._parse_poll_block()
case _:
block = unsupported.Unsupported(self.current["type"])

self.parsed_result.append(block)

async def parse(self):
"""Begins the parsing chain and returns the final list of parsed objects"""
while self.next():
await self.__parse_block()

return self.parsed_result


class NPFFormatter(npf_renderer.format.Formatter):
def __init__(self, content, layout=None, blog_name=None, post_id=None, *, url_handler=None, forbid_external_iframes=False):
super().__init__(content, layout, url_handler=url_handler, forbid_external_iframes=forbid_external_iframes)

# We store the blog and post ID as to be able to render a link to
# fetch poll results for JS disabled users
self.blog_name = blog_name
self.post_id = post_id

def _format_poll(self, block):
poll_html = super()._format_poll(block)
poll_html["data-poll-id"] = block.poll_id

poll_body = poll_html[1]
for index, answer_id in enumerate(block.answers.keys()):
poll_body[index]["data-answer-id"] = answer_id

if (self.blog_name and self.post_id) and not block.votes:
poll_footer = poll_html[2]
no_script_fallback = dominate.tags.noscript(
dominate.tags.a(
"See Results",
href=f"/{self.blog_name}/{self.post_id}?fetch_polls=true",
cls="toggle-poll-results"
)
)

poll_footer.children.insert(0, no_script_fallback)

return poll_html


async def format_npf(contents, layouts=None, blog_name=None, post_id=None,*, poll_callback=None):
"""Wrapper around npf_renderer.format_npf for extra functionalities
- Replaces internal Parser and Formatter with the modified variants above
- Accepts extra arguments to add additional details to formatted results
- Automatically sets Priviblur-specific rendering arguments
Arguments (new):
blog_name:
Name of the blog the post comes from. This is used to render links to the parent post
post_id:
Unique ID of the post. This is used to render links to the parent post
"""
try:
contents = await NPFParser(contents, poll_callback=poll_callback).parse()
if layouts:
layouts = npf_renderer.parse.LayoutParser(layouts).parse()

contains_render_errors = False

formatted = NPFFormatter(
contents, layouts,
blog_name=blog_name,
post_id=post_id,
url_handler=url_handler,
forbid_external_iframes=True,
).format()

except npf_renderer.exceptions.RenderErrorDisclaimerError as e:
contains_render_errors = True
formatted = e.rendered_result
assert formatted is not None
except Exception as e:
formatted = dominate.tags.div(cls="post-body has-error")
contains_render_errors = True

return contains_render_errors, formatted.render(pretty=False)
Loading

0 comments on commit 32a12c7

Please sign in to comment.