Something went wrong. Try again.
social media crossposting tool. 3rd time's the charm
mastodon misskey crossposting bluesky
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181from html.parser import HTMLParserfrom typing import override
from cross.tokens import LinkToken, TextToken, Tokenfrom util.splitter import canonical_label
class HTMLToTokensParser(HTMLParser): def __init__(self) -> None: super().__init__() self.tokens: list[Token] = []
self._tag_stack: dict[str, tuple[str, dict[str, str | None]]] = {} self.in_pre: bool = False self.in_code: bool = False
self.invisible: str | None = None self.invisible_depth: int = 0
self.list_stack: list[tuple[str, int]] = []
def handle_a_endtag(self): label, _attr = self._tag_stack["a"]
href = _attr.get("href") if href: if canonical_label(label, href): self.tokens.append(LinkToken(href=href)) else: self.tokens.append(LinkToken(href=href, label=label))
def append_text(self, text: str): self.tokens.append(TextToken(text=text))
def append_newline(self): if self.tokens: last_token = self.tokens[-1] if isinstance(last_token, TextToken) and not last_token.text.endswith("\n"): self.tokens.append(TextToken(text="\n"))
@override def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None: _attr = dict(attrs)
if self.invisible: if self.invisible == tag: self.invisible_depth += 1 return
cls = _attr.get("class", "") if cls and "quote-inline" in cls: self.invisible = tag return
match tag: case "a": self._tag_stack["a"] = ("", _attr) case "code": if not self.in_pre: self.append_text("`") self.in_code = True case "pre": self.append_newline() self.append_text("```\n") self.in_pre = True case "blockquote": self.append_newline() self.append_text("> ") case "strong" | "b": self.append_text("**") case "em" | "i": self.append_text("*") case "del" | "s": self.append_text("~~") case "br": self.append_text("\n") case "h1" | "h2" | "h3" | "h4" | "h5" | "h6": level = int(tag[1]) self.append_text("\n" + "#" * level + " ") case "ul" | "ol": if self.list_stack: self.append_text("\n") self.list_stack.append((tag, 0)) case "li": if self.list_stack: ltype, idx = self.list_stack[-1] indent = " " * (len(self.list_stack) - 1) self.append_text( f"{indent}- " if ltype == "ul" else f"{indent}{idx + 1}. " ) self.list_stack[-1] = (ltype, idx + 1) case _: # self.builder.extend(f"<{tag}>".encode("utf-8")) pass
@override def handle_endtag(self, tag: str) -> None: if self.invisible: if tag == self.invisible: if self.invisible_depth <= 0: self.invisible = None else: self.invisible_depth -= 1 return
match tag: case "a": if "a" in self._tag_stack: self.handle_a_endtag() del self._tag_stack["a"] case "code": if not self.in_pre and self.in_code: self.append_text("`") self.in_code = False case "pre": self.append_newline() self.append_text("```\n") self.in_pre = False case "blockquote": self.append_text("\n") case "strong" | "b": self.append_text("**") case "em" | "i": self.append_text("*") case "del" | "s": self.append_text("~~") case "p": self.append_text("\n\n") case "h1" | "h2" | "h3" | "h4" | "h5" | "h6": self.append_text("\n") case "ul" | "ol": self.list_stack.pop() if not self.list_stack: self.append_text("\n") case "li": self.append_text("\n") case _: # self.builder.extend(f"</{tag}>".encode("utf-8")) pass
@override def handle_data(self, data: str) -> None: if self.invisible: return
if self._tag_stack.get("a"): label, _attr = self._tag_stack.pop("a") self._tag_stack["a"] = (label + data, _attr) else: self.append_text(data)
def get_result(self) -> list[Token]: if not self.tokens: return []
combined: list[Token] = [] buffer: list[str] = []
def flush_buffer(): if buffer: merged = "".join(buffer) combined.append(TextToken(text=merged)) buffer.clear()
for token in self.tokens: if isinstance(token, TextToken): buffer.append(token.text) else: flush_buffer() combined.append(token)
flush_buffer()
if combined and isinstance(combined[-1], TextToken): if combined[-1].text.endswith("\n\n"): combined[-1] = TextToken(text=combined[-1].text[:-2])
if combined[-1].text.endswith("\n"): combined[-1] = TextToken(text=combined[-1].text[:-1]) return combined