/**
* Canonical text normalization for the Oxy ecosystem.
*
* WHY THIS EXISTS
* ---------------
* Third-party text (federated actor display names, spoiler/CW text, image alt
* text, bios, `
` / `og:site_name` of scraped remote pages, …) arrives
* with the whitespace the remote author's markup happened to contain. A real
* bug: a remote page served
*
*
* Mi título
*
*
* and the extracted string — newlines and indentation included — was stored
* verbatim. Clients render text with React Native Web `Text`, which maps to
* CSS `white-space: pre-wrap`: unlike HTML, newlines and repeated spaces are
* NOT collapsed at render time, so the user saw a blank line and a six-space
* indent inside the link preview.
*
* Storage-time normalization is therefore the ONLY place this can be fixed:
* every renderer downstream is faithful by design.
*
* WHICH HELPER DO I USE?
* ----------------------
* - {@link normalizeInlineText} — the value is conceptually ONE LINE, and a
* line break in it is always an accident of the source markup: page titles,
* `siteName`, display names, image `alt`, handles, profile field labels.
* Every line break becomes a space.
*
* - {@link normalizeMultilineText} — the value is a BODY whose line breaks are
* the author's own paragraphs and must survive: post text, bios/summaries.
* Line breaks are preserved (capped at one blank line); only the surrounding
* whitespace noise is cleaned.
*
* Using the multiline helper on a title would keep the newlines and reproduce
* the original bug; using the inline helper on a post body would flatten the
* author's paragraphs into a single run-on line. Pick deliberately.
*
* Neither helper truncates, strips markup, or enforces a character policy —
* those are separate, product-specific concerns (see `isValidDisplayName` for
* the display-name character policy). These functions do exactly one thing:
* normalize whitespace and Unicode form.
*/
/** Any run of whitespace, including tabs, line breaks and Unicode spaces. */
const ANY_WHITESPACE_RUN = /\s+/g;
/**
* Every line-break form, unified to `\n` before the multiline passes run:
* CRLF (Windows), a lone CR (classic Mac / stray `\r`), and the Unicode LINE
* SEPARATOR (U+2028) / PARAGRAPH SEPARATOR (U+2029), which are mandatory breaks
* in Unicode and a well-known hazard in JSON/JS payloads. CRLF must be matched
* before the lone `\r` alternative or it would yield two breaks. The separators
* are matched by their code-point ESCAPES (`\u2028` = LINE SEPARATOR, `\u2029` =
* PARAGRAPH SEPARATOR) rather than by their Unicode Line_Separator /
* Paragraph_Separator property escapes: React Native's Hermes engine ships with
* Unicode property escapes compiled OUT and rejects those subcategory property
* names with "Invalid RegExp: Invalid property name", whereas the `\uXXXX`
* escapes are exactly equivalent (each property covers exactly its one code
* point), engine-portable, and drop the now-unneeded `u` flag. (A literal
* U+2028/U+2029 is itself a LineTerminator and cannot appear in a regex literal.)
*/
const LINE_BREAK_FORMS = /\r\n|\r|\u2028|\u2029/g;
/**
* A run of HORIZONTAL whitespace: any whitespace that is not a line break.
* `[^\S\n]` is "whitespace, minus `\n`" — it covers the tab, the vertical tab,
* the form feed and every Unicode space separator (NBSP U+00A0, the U+2000
* block, the ideographic space U+3000, the zero-width no-break space U+FEFF).
* Applied AFTER {@link LINE_BREAK_FORMS}, so no line break can hide in it.
*/
const HORIZONTAL_WHITESPACE_RUN = /[^\S\n]+/g;
/**
* Horizontal whitespace at the END of a line — the blank-line spoiler: it is
* what makes an "empty" line non-empty and hides it from {@link EXCESS_BLANK_LINES}.
*/
const TRAILING_HORIZONTAL_WHITESPACE = / +\n/g;
/**
* Horizontal whitespace at the START of a line: source-markup indentation. HTML
* collapses it by spec, so it is invisible where the text came from and carries
* no meaning — it only becomes visible once a client renders the value with
* `white-space: pre-wrap`.
*/
const LEADING_HORIZONTAL_WHITESPACE = /\n +/g;
/** Three or more line breaks: more than one blank line between paragraphs. */
const EXCESS_BLANK_LINES = /\n{3,}/g;
/**
* Normalize a SINGLE-LINE text value such as a page title or `siteName`,
* display names, image alt text, handles, profile field labels.
*
* A line break in such a value is never meaningful — it is an artifact of the
* markup the value was extracted from (`\n Título\n`) — and it
* survives into the UI because RN Web renders `Text` with `white-space:
* pre-wrap`. So ALL whitespace, line breaks included, collapses to one space.
*
* 1. NFC-normalize, so visually identical strings store and compare
* identically (a decomposed `e`+◌́ recomposes into `é`).
* 2. Collapse every run of whitespace — spaces, tabs, `\n`, `\r`, and Unicode
* spaces such as NBSP — to a single plain space.
* 3. Trim both ends.
*
* Length is NOT capped: a maximum length is a product rule of the specific
* field (see `MAX_DISPLAY_NAME_LENGTH`), not a property of text normalization.
* Markup is NOT stripped: run the caller's sanitizer first if the source can
* contain HTML.
*
* A value that is empty or whitespace-only returns `''`. Callers decide whether
* that means "omit the field" (`|| undefined`) or "store an empty string".
*
* Idempotent: `f(f(x)) === f(x)`.
*/
export function normalizeInlineText(value: string): string {
return value.normalize('NFC').replace(ANY_WHITESPACE_RUN, ' ').trim();
}
/**
* Normalize a MULTILINE text BODY: post text, bios, summaries — anywhere the
* line breaks are the author's paragraphs and must be preserved.
*
* Cleans the whitespace noise around those paragraphs without destroying them:
*
* 1. NFC-normalize (same rationale as {@link normalizeInlineText}).
* 2. Unify every line-break form (CRLF, lone CR, U+2028, U+2029) to `\n`.
* 3. Collapse runs of HORIZONTAL whitespace (spaces, tabs, NBSP and friends)
* to a single space. Line breaks are untouched.
* 4. Strip the horizontal whitespace at BOTH ends of every line.
* 5. Collapse three or more line breaks to exactly one blank line (`\n\n`).
* 6. Trim both ends of the value.
*
* STEP 4 MUST PRECEDE STEP 5 — this is the whole point of the function. A
* "blank" line that actually contains spaces (`"a\n \n \nb"`) breaks the
* run of `\n` characters, so a bare `\n{3,}` collapse (step 5 alone) never sees
* it and the extra blank lines survive into the UI. That is exactly the bug in
* federated post bodies. Trimming each line first turns those lines into real,
* empty lines, which step 5 then collapses.
*
* Every line is trimmed on BOTH sides, so a leading indent is removed outright
* rather than reduced to one space. Step 3 has already destroyed whatever indent
* the author wrote (`" Mundo"` → `" Mundo"`), so a surviving space would not
* be the author's intent — it would be an arbitrary remnant of exactly the
* source-markup indentation this function exists to erase, and `pre-wrap` renders
* it. Indentation is invisible in HTML by spec; it must be invisible here too.
*
* A value that is empty or whitespace-only returns `''`.
*
* Idempotent: `f(f(x)) === f(x)`.
*/
export function normalizeMultilineText(value: string): string {
return value
.normalize('NFC')
.replace(LINE_BREAK_FORMS, '\n')
.replace(HORIZONTAL_WHITESPACE_RUN, ' ')
.replace(TRAILING_HORIZONTAL_WHITESPACE, '\n')
.replace(LEADING_HORIZONTAL_WHITESPACE, '\n')
.replace(EXCESS_BLANK_LINES, '\n\n')
.trim();
}