Synopsis : normalisation SÛRE des text ajouté par formulaire

- fins de ligne \r\n/\r → \n,
- blocs de lignes vides → \n\n, è
- espaces/tabulations/NBSP → espace unique,
- trim.
Les retours à la ligne simples intra-paragraphe sont LAISSÉS INTACTS :
une coupure de ligne peut être une vraie frontière de mot (→ espace)
ou une coupure du mot ("dyna\nmiques" → "dynamiques"),
indistinguables sans dictionnaire — automatiser corromprait l'entrée
("poursuivantsur").

Migration batch 045 (backfill des données existantes) RETIRÉE : le
nettoyage automatique fiable des synopsis existants est impossible.
Chaque modification est testée (277 tests PHPUnit).
This commit is contained in:
Pontoporeia
2026-09-18 16:26:36 +02:00
parent 6e1fc6a781
commit 0896c4b8c8
4 changed files with 122 additions and 5 deletions
File diff suppressed because one or more lines are too long
+2 -1
View File
@@ -1,13 +1,14 @@
# TODO # TODO
> Last updated: 2026-08-26 > Last updated: 2026-08-26
> Context: Docs refactor: verify docs/ accuracy against current xamxam codebase; update/refactor stale docs > Context: Fix end-user admin report: unexpected line breaks in some synopsis — resolved as undetectable/unfixable for existing data; only safe normalization on new inputs
## In Progress ## In Progress
## Pending ## Pending
## Completed ## Completed
- [x] #normalize-synopsis-whitespace & Passages à la ligne inopinés dans les synopsis : jugés INDÉTECTABLES/non-corrigeables automatiquement (une coupure de ligne interne peut être une vraie frontière de mot → espace, ou une coupure du mot → à réjoindre ; indistinguables sans dictionnaire, toute tentative corrompt l'entrée p.ex. "poursuivantsur"). Migration 045 SUPPRIMÉE ; dites à l'admin qu'il n'y a pas de nettoyage fiable des données existantes. Conservation de la NORMALISATION SÛRE sur les nouvelles saisies : fin de ligne \r\n/\r → \n, pertes de lignes vides multiples → \n\n, espaces/tabulations/NBSP → espace unique, trim — retour à la ligne simple intra-paragraphe laissé intact.
- [x] #audit-all-docs-and [!high] Audit all docs/ and classify accurate vs stale - [x] #audit-all-docs-and [!high] Audit all docs/ and classify accurate vs stale
- [x] #rewrite-development-md-to-match [!high] Rewrite development.md to match current just dev / app/ layout / PHPUnit - [x] #rewrite-development-md-to-match [!high] Rewrite development.md to match current just dev / app/ layout / PHPUnit
- [x] #rewrite-deployment-md-to-match [!high] Rewrite deployment.md to match just deploy / /var/www/xamxam/ / backup - [x] #rewrite-deployment-md-to-match [!high] Rewrite deployment.md to match just deploy / /var/www/xamxam/ / backup
+30 -3
View File
@@ -609,12 +609,39 @@ class ThesisCreateController
// ── Private: input helpers ──────────────────────────────────────────────── // ── Private: input helpers ────────────────────────────────────────────────
/** /**
* Trim and strip HTML tags from a string value. * Normalise a submitted string value.
* htmlspecialchars is applied at render time, not here. *
* Only the UNAMBIGUOUS cleanups are applied here. We deliberately do NOT
* auto-collapse or rejoin single newlines inside a paragraph: a line break
* can be either a real word boundary (should become a space) or a word split
* across the line ("dyna\nmiques", should be rejoined to "dynamiques"),
* and these are indistinguishable without a dictionary. Trying to guess
* corrupts valid input (e.g. "se poursuivant\nsur" → "se poursuivantsur"),
* so such breaks are left untouched and must be fixed by the author.
*
* Responsibilities:
* - strip HTML tags (htmlspecialchars is applied at render time, not here)
* - normalise all line endings to \n (handles \r\n and bare \r)
* - collapse runs of horizontal whitespace (spaces, tabs, NBSP) to one space
* - collapse multiple blank lines into a single paragraph separator (\n\n)
* - trim leading/trailing whitespace and blank lines
*/ */
private function sanitiseString(string $input): string private function sanitiseString(string $input): string
{ {
return strip_tags(trim($input)); $input = strip_tags($input);
// Normalise all line endings to \n (handles \r\n and bare \r).
$input = preg_replace('/\r\n|\r|\n/', "\n", $input);
// Collapse multiple blank lines into a single paragraph separator.
$input = preg_replace("/\n{2,}/u", "\n\n", $input);
// Collapse runs of horizontal whitespace (spaces, tabs, NBSP gaps, …)
// into a single space. Single newlines inside a paragraph are NOT
// touched, for the reason explained above.
$input = preg_replace('/[ \t\x0B\x00\xC2\xA0]+/u', ' ', $input);
return trim($input, " \t\n\r\0\x0B\xC2\xA0");
} }
/** /**
@@ -263,6 +263,96 @@ class ThesisCreateValidationTest extends TestCase
$this->assertStringContainsString('Bold synopsis', $data['synopsis']); $this->assertStringContainsString('Bold synopsis', $data['synopsis']);
} }
// ── Whitespace normalisation (safe, unambiguous subset only) ────────────────
// Single newlines inside a paragraph are deliberately LEFT untouched: they can
// be a word boundary (→ space) or a word split across a line ("dyna\nmiques"
// → "dynamiques") and are indistinguishable, so we never guess and risk
// corrupting valid input (e.g. "se poursuivant\nsur" → "poursuivantsur").
public function testSingleIntraParagraphNewlineIsLeftUntouched(): void
{
// A mid-word line break is ambiguous (could be a real split or a wrap),
// so we do not auto-alter it — the author is responsible for cleaning it.
$post = $this->validPost();
$post['synopsis'] = "…les dyn\namiques complexes…";
$data = $this->validate($post);
$this->assertSame("…les dyn\namiques complexes…", $data['synopsis']);
}
public function testWordWrapSingleNewlineNotMerged(): void
{
// We must NOT merge two real words across a line break ("poursuivantsur").
$post = $this->validPost();
$post['synopsis'] = "se poursuivant\nsur une seconde ligne";
$data = $this->validate($post);
$this->assertSame(
"se poursuivant\nsur une seconde ligne",
$data['synopsis']
);
}
public function testLegitimateParagraphBreakPreserved(): void
{
$post = $this->validPost();
$post['synopsis'] = "Premier paragraphe\n\nSecond paragraphe";
$data = $this->validate($post);
$this->assertSame(
"Premier paragraphe\n\nSecond paragraphe",
$data['synopsis']
);
}
public function testMultipleBlankLinesCollapseToSingleParagraphBreak(): void
{
$post = $this->validPost();
$post['synopsis'] = "A\n\n\n\nB";
$data = $this->validate($post);
$this->assertSame("A\n\nB", $data['synopsis']);
}
public function testMixedWindowsAndUnixLineEndingsNormalised(): void
{
$post = $this->validPost();
$post['synopsis'] = "une pause\r\n\r\ner une future";
$data = $this->validate($post);
// \r\n and \r line-endings all collapse to \n, and blank lines mark
// a single paragraph separator.
$this->assertSame("une pause\n\ner une future", $data['synopsis']);
}
public function testConsecutiveSpacesAndTabsCollapse(): void
{
$post = $this->validPost();
$post['synopsis'] = "Très espacé\t\ttab";
$data = $this->validate($post);
$this->assertSame('Très espacé tab', $data['synopsis']);
}
public function testNonBreakingSpacesNormalised(): void
{
$post = $this->validPost();
// Word pasted NBSP (U+00A0) between words must become a regular space.
$post['synopsis'] = "Premier\u{00A0}mot second mot";
$data = $this->validate($post);
$this->assertSame('Premier mot second mot', $data['synopsis']);
}
public function testSynopsisSingleLineFieldTrimmed(): void
{
$post = $this->validPost();
$post['synopsis'] = " texte qui flotte ";
$data = $this->validate($post);
$this->assertSame('texte qui flotte', $data['synopsis']);
}
// ── Author processing ──────────────────────────────────────────────────── // ── Author processing ────────────────────────────────────────────────────
public function testMultipleAuthorsAreSorted(): void public function testMultipleAuthorsAreSorted(): void