Title rewriting rules per feed and route #155
Labels
No labels
bug
devops
duplicate
enhancement
good first issue
layout
next major release
next minor release
question
research
testing
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Depends on
#170 Give routes a surrogate primary key
lvl0/fedi-feed-router
Reference: lvl0/fedi-feed-router#155
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Problem
Article titles carry the publisher's site name, published to Lemmy verbatim:
The
| VRT NWS Nieuwsrepeats on every post, eats title length, and tells the reader nothing.BelgaArticlePageParserandGuardianArticlePageParserhave the same shape — publishers append their site name toog:titleand<title>by convention, and that is what the parsers read.A fixed per-provider suffix string was considered and rejected: it only handles suffixes, only handles removal, and only the maintainer can change it. Users want prefixes gone too, and sometimes want a suffix replaced rather than dropped.
Approach
User-defined regex rules that rewrite the title at publish time, with a preview against real titles before saving.
Model
A
title_rulestable with a polymorphic owner (FeedorRoute), one rule per owner, enforced by a unique index on(owner_type, owner_id).owner_type,owner_idFeedorRoutepatternignore_caseiflagreplacement$1backreferencesPlus
routes.apply_title_rule, boolean, defaulttrue.This requires
routes.id(#170) —routescurrently has a composite primary key and cannot be a polymorphic owner.Precedence, resolved at publish
route.apply_title_rule === false→ title unchanged, whatever the feed saysRoute replaces feed, never merges. One place to look when a title comes out wrong.
Applied at publish, never at extraction
articles.titlestays pristine. Two routes from one feed must be able to produce different titles from one stored article, and editing a rule must change future publishes without a backfill. A bad rule is recoverable because the original survives.PublishRouteArticleAction::resolvePublishData()currently takes anArticle; it needs theRouteArticle(or the route) to resolve precedence. That signature change is part of this work.Regex handling
/— shown in the UI as non-editable affixes, so a user cannot break out or pass a modifieru(unicode) is always on and never exposed. Feeds are Dutch and French; without it a.can match half oféand emit corrupt UTF-8mandsare deliberately omitted — titles are single-lineFailure handling
preg_replacereturnsnullon failure (bad pattern, orpcre.backtrack_limitexceeded). Backtracking depends on the input, so a rule can validate at save and still fail later on one pathological title.On
null: publish the original title and record it visibly — never let the title become null. Silent fallback is not acceptable; see #164.UI
On the Feeds page and the Routes page:
/affixes, Ignore case checkbox, Replacement fieldpreg_replacereturnsnullis marked as an errorValidation runs on save regardless of whether Preview was pressed — the button is a convenience, not a gate.
Commit sequence
title_rules+routes.apply_title_rule)Open question
pcre.backtrack_limitandpcre.jitin the container are unverified — they bound the worst case for an untrusted pattern. Check before relying on them.Acceptance criteria
| VRT NWS Nieuwsgiven a feed ruleapply_title_rule = falsepublishes the raw title despite a feed rule$1works; an empty replacement stripspreg_replacereturningnullpublishes the original title and is recordeducasearticles.titleis never modifiedRelated
routes.id, required for the polymorphic ownerStrip the publisher suffix from article titles before publishingto Title rewriting rules per feed and routeOpen question resolved: PCRE limits measured
Measured in the dev container (
container_tinker), not assumed:Cost of a pathological pattern is bounded and small.
/(a|a)+$/uagainst inputs of 20–100 characters hits the limit in ~2.6ms, flat regardless of input length — the backtrack limit is a hard counter, so cost is capped by the limit, not by input size. Ten sample titles in the preview is ~28ms worst case.Note the textbook catastrophic pattern
/(a+)+b/udoes not blow up here at all (0.04ms, no error) — PCRE2's JIT and auto-possessification optimise it away. Constructing a genuinely pathological case took deliberate effort, which further bounds the practical risk.Failure is cleanly detectable:
preg_replacereturnsnullpreg_last_error()returnsPREG_BACKTRACK_LIMIT_ERROR(2), distinguishable from a malformed patternpreg_last_error_msg()gives"Backtrack limit exhausted"So no timeout guard, no limit tuning, and no separate protection for the preview endpoint are needed. The
nullcheck already in the design is sufficient. The preview button remains the right call — not because live-on-keystroke was dangerous, but because it is unnecessary work.The forced
uflag is a correctness requirement, demonstratedStripping one character from
Café:uCafÃuCafWithout
uthe pattern operates on bytes, splits the two-byteé, and emits invalid UTF-8 — mojibake published to Lemmy. Confirmsumust be always-on and never user-exposed.No-op vs error in the preview
Three outcomes, unambiguously distinguishable:
!== nulland!== original!== nulland=== original=== nullThe detection is trivial; the risk is presentational. A suffix rule legitimately will not match titles that lack the suffix, so rendering "no match" as a warning would make a correct rule look half-broken. Only a
nullreturn is an error.