render mentions on a separate line from the reply content.
This commit is contained in:
1 parent
f8380db665
commit
880640d935
422 files changed
+2864
-920
No files matched your search
@@ -33,7 +33,13 @@ try:
|
||||
from twitter.media_refs import remote_markup
|
||||
from twitter.models import MediaItem, ThreadData, load_thread
|
||||
from twitter.paths import FROZEN, SCRATCH
|
||||
from twitter.render import render_branch, render_spine, title_text
|
||||
from twitter.render import (
|
||||
format_post_text,
|
||||
render_branch,
|
||||
render_spine,
|
||||
split_leading_mentions,
|
||||
title_text,
|
||||
)
|
||||
from twitter.slug import branch_file_name, date_prefix, thread_dir_name
|
||||
from twitter.tree import (
|
||||
by_id,
|
||||
@@ -58,7 +64,13 @@ except ImportError: # pragma: no cover - script-mode import
|
||||
from media_refs import remote_markup
|
||||
from models import MediaItem, ThreadData, load_thread
|
||||
from paths import FROZEN, SCRATCH
|
||||
from render import render_branch, render_spine, title_text
|
||||
from render import (
|
||||
format_post_text,
|
||||
render_branch,
|
||||
render_spine,
|
||||
split_leading_mentions,
|
||||
title_text,
|
||||
)
|
||||
from slug import branch_file_name, date_prefix, thread_dir_name
|
||||
from tree import (
|
||||
by_id,
|
||||
@@ -846,24 +858,55 @@ def _frontmatter_handle(text: str) -> str:
|
||||
return ""
|
||||
|
||||
|
||||
def _body_match_keys(body: str) -> set[str]:
|
||||
"""Keys used to pair a note body with ``post.text`` before or after mention split."""
|
||||
mentions, rest = split_leading_mentions(body)
|
||||
keys: set[str] = set()
|
||||
if rest:
|
||||
rest_first = _first_nonempty_line(rest)
|
||||
if rest_first:
|
||||
keys.add(rest_first)
|
||||
if mentions:
|
||||
keys.add(f"{mentions} {rest_first}")
|
||||
else:
|
||||
first = _first_nonempty_line(body)
|
||||
if first:
|
||||
keys.add(first)
|
||||
return keys
|
||||
|
||||
|
||||
def _post_match_keys(text: str) -> set[str]:
|
||||
mentions, rest = split_leading_mentions(text)
|
||||
keys: set[str] = set()
|
||||
first = _first_nonempty_line(text)
|
||||
if first:
|
||||
keys.add(first)
|
||||
if rest:
|
||||
rest_first = _first_nonempty_line(rest)
|
||||
if rest_first:
|
||||
keys.add(rest_first)
|
||||
if mentions:
|
||||
keys.add(f"{mentions} {rest_first}")
|
||||
return keys
|
||||
|
||||
|
||||
def match_post_for_body(
|
||||
body: str,
|
||||
posts: tuple,
|
||||
used: set[str],
|
||||
) -> object | None:
|
||||
"""Return one unused post whose first line matches ``body``.
|
||||
"""Return one unused post whose text matches ``body``.
|
||||
|
||||
Duplicate first lines are fine when every hit has the same handle.
|
||||
Mixed-handle collisions return ``None``.
|
||||
"""
|
||||
first = _first_nonempty_line(body)
|
||||
if not first:
|
||||
keys = _body_match_keys(body)
|
||||
if not keys:
|
||||
return None
|
||||
hits = [
|
||||
post
|
||||
for post in posts
|
||||
if post.post_id not in used
|
||||
and _first_nonempty_line(post.text) == first
|
||||
if post.post_id not in used and keys & _post_match_keys(post.text)
|
||||
]
|
||||
if not hits:
|
||||
return None
|
||||
@@ -898,16 +941,23 @@ def patch_note_text(text: str, posts: tuple) -> tuple[str, str, str]:
|
||||
used.add(post.post_id)
|
||||
if not handle and index == 0 and root_handle:
|
||||
handle = root_handle
|
||||
region = text[match.end():end]
|
||||
if not handle:
|
||||
unmatched.append(f"**{number}/**")
|
||||
pieces.append(match.group(0).rstrip())
|
||||
last = match.end()
|
||||
pieces.append(region)
|
||||
last = end
|
||||
continue
|
||||
new_line = f"**{number}/** @{handle}"
|
||||
if new_line != match.group(0).rstrip():
|
||||
changed = True
|
||||
formatted = format_post_text(body)
|
||||
if body and formatted != body:
|
||||
region = region.replace(body, formatted, 1)
|
||||
changed = True
|
||||
pieces.append(new_line)
|
||||
last = match.end()
|
||||
pieces.append(region)
|
||||
last = end
|
||||
pieces.append(text[last:])
|
||||
new_text = "".join(pieces)
|
||||
reason = ",".join(unmatched)
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
@@ -100,6 +101,35 @@ def _source_block(post: PostData) -> str:
|
||||
)
|
||||
|
||||
|
||||
_LEADING_MENTIONS: re.Pattern[str] = re.compile(
|
||||
r"^((?:@[A-Za-z0-9_]+(?:[ \t]+@[A-Za-z0-9_]+)*)[ \t]*)(.*)$",
|
||||
re.DOTALL,
|
||||
)
|
||||
|
||||
|
||||
def split_leading_mentions(text: str) -> tuple[str, str]:
|
||||
"""Split reply ``@handles`` at the start of a tweet from the rest."""
|
||||
raw = text or ""
|
||||
match = _LEADING_MENTIONS.match(raw)
|
||||
if match is None:
|
||||
return "", raw
|
||||
mentions = " ".join(match.group(1).split())
|
||||
rest = match.group(2).lstrip(" \t")
|
||||
if rest.startswith("\n"):
|
||||
rest = rest.lstrip("\n")
|
||||
return mentions, rest
|
||||
|
||||
|
||||
def format_post_text(text: str) -> str:
|
||||
"""Put leading reply mentions on their own line, then the tweet body."""
|
||||
mentions, rest = split_leading_mentions(text)
|
||||
if not mentions:
|
||||
return text
|
||||
if not rest:
|
||||
return mentions
|
||||
return f"{mentions}\n\n{rest}"
|
||||
|
||||
|
||||
def _post_block(
|
||||
n: int,
|
||||
post: PostData,
|
||||
@@ -109,7 +139,7 @@ def _post_block(
|
||||
"""Render one post body (``**N/** @handle`` + text + media + branch links)."""
|
||||
if not post.handle:
|
||||
raise ValueError(f"empty handle on post {post.post_id}")
|
||||
parts = [f"**{n}/** @{post.handle}", "", post.text]
|
||||
parts = [f"**{n}/** @{post.handle}", "", format_post_text(post.text)]
|
||||
media = (media_by_post or {}).get(post.post_id) or ()
|
||||
if media:
|
||||
parts.append("")
|
||||
|
||||
@@ -7,7 +7,13 @@ from unittest.mock import patch
|
||||
|
||||
from twitter.emit_archive import apply_relabel, format_relabel_plan, plan_relabel
|
||||
from twitter.models import PostData, PostMetrics, ThreadData
|
||||
from twitter.render import _post_block, render_branch, render_spine
|
||||
from twitter.render import (
|
||||
_post_block,
|
||||
format_post_text,
|
||||
render_branch,
|
||||
render_spine,
|
||||
split_leading_mentions,
|
||||
)
|
||||
from twitter.tw import build_parser, cmd_relabel
|
||||
|
||||
|
||||
@@ -44,6 +50,26 @@ class PostBlockAttributionTests(unittest.TestCase):
|
||||
with self.assertRaises(ValueError):
|
||||
_post_block(1, post("1", "", "text", None), None)
|
||||
|
||||
def test_leading_mentions_are_their_own_line(self) -> None:
|
||||
block = _post_block(
|
||||
19,
|
||||
post(
|
||||
"1898440015182729598",
|
||||
"SebAaltonen",
|
||||
"@NOTimothyLottes I said WG_RR_EN",
|
||||
"1",
|
||||
),
|
||||
None,
|
||||
)
|
||||
self.assertIn("**19/** @SebAaltonen\n\n@NOTimothyLottes\n\nI said WG_RR_EN", block)
|
||||
|
||||
def test_format_post_text_is_idempotent(self) -> None:
|
||||
original = "@AgileJebrim @SebAaltonen The higher the occupancy"
|
||||
once = format_post_text(original)
|
||||
self.assertEqual(once, "@AgileJebrim @SebAaltonen\n\nThe higher the occupancy")
|
||||
self.assertEqual(format_post_text(once), once)
|
||||
self.assertEqual(split_leading_mentions("holy truthnuke")[0], "")
|
||||
|
||||
|
||||
class RenderAttributionTests(unittest.TestCase):
|
||||
def test_mixed_spine_labels_each_post(self) -> None:
|
||||
@@ -223,7 +249,8 @@ class RelabelPatchTests(unittest.TestCase):
|
||||
(notes / "branch.md").write_text(
|
||||
"---\ndraft: false\n---\n\n"
|
||||
"**19/** @SebAaltonen\n\n"
|
||||
"@NOTimothyLottes I said WG_RR_EN\n\n"
|
||||
"@NOTimothyLottes\n\n"
|
||||
"I said WG_RR_EN\n\n"
|
||||
"**33/** @AgileJebrim\n\n"
|
||||
"On the flip side, it appears that using 128\n",
|
||||
encoding="utf-8",
|
||||
@@ -235,6 +262,29 @@ class RelabelPatchTests(unittest.TestCase):
|
||||
apply_relabel(plan)
|
||||
self.assertEqual((notes / "index.md").read_bytes(), before)
|
||||
|
||||
def test_patch_splits_leading_mentions(self) -> None:
|
||||
from twitter.emit_archive import patch_note_text
|
||||
|
||||
posts = (
|
||||
post(
|
||||
"1898440015182729598",
|
||||
"SebAaltonen",
|
||||
"@NOTimothyLottes I said WG_RR_EN",
|
||||
None,
|
||||
),
|
||||
)
|
||||
text = (
|
||||
"**19/** @SebAaltonen\n\n"
|
||||
"@NOTimothyLottes I said WG_RR_EN\n"
|
||||
)
|
||||
new, state, reason = patch_note_text(text, posts)
|
||||
self.assertEqual(state, "rewrite")
|
||||
self.assertEqual(reason, "")
|
||||
self.assertIn("@NOTimothyLottes\n\nI said WG_RR_EN", new)
|
||||
again, state2, _reason = patch_note_text(new, posts)
|
||||
self.assertEqual(state2, "noop")
|
||||
self.assertEqual(again, new)
|
||||
|
||||
def test_duplicate_first_line_same_handle_rewrites(self) -> None:
|
||||
from twitter.emit_archive import patch_note_text
|
||||
|
||||
|
||||
Reference in new issue
Block a user