fix(sonar): locale-aware sorts, linear share and redaction patterns, focus only on controls

This commit is contained in:
Maurice
2026-10-22 18:48:51 +02:00
parent e291735c95
commit c8ce36e9af
14 changed files with 290 additions and 94 deletions
@@ -66,9 +66,9 @@ export default function AssistantFitCheck({ changes }: { changes: readonly DayCh
if (ok) return pill;
return (
<Tooltip label={check.slice(0, MAX_LINES).join('\n')} placement="top">
<span tabIndex={0} className="rounded-full outline-none focus-visible:ring-2 focus-visible:ring-accent">
<button type="button" className="rounded-full outline-none focus-visible:ring-2 focus-visible:ring-accent">
{pill}
</span>
</button>
</Tooltip>
);
}
@@ -76,6 +76,23 @@ describe('markdownToText', () => {
it('FE-AI-SHARE-002: a link whose text is its address stays one address', () => {
expect(markdownToText('[https://trek.app](https://trek.app)')).toBe('https://trek.app');
});
it('FE-AI-SHARE-006: pictures, odd links and divider rows read as before', () => {
expect(markdownToText('![Map](a.png) and [[nested]](u) and [a b](x y) and [](z) and [ok](u)')).toBe(
'Map and [[nested]](u) and [a b](x y) and [](z) and ok (u)'
);
expect(markdownToText('|:-|-:|\n| a | b |')).toBe('a, b');
expect(markdownToText('| - | x |')).toBe('-, x');
expect(markdownToText('# Wide heading\n- wide item')).toBe('Wide heading\n• wide item');
});
it('FE-AI-SHARE-007: long runs of brackets, spaces and dashes are read in one pass', () => {
const started = Date.now();
const long = 20000;
markdownToText(`${'['.repeat(long)}\n${'![a](b '.repeat(long / 7)}\n${'-'.repeat(long)}\n#${' '.repeat(long)}x`);
markdownToText(`|${' - |'.repeat(long / 4)} x`);
expect(Date.now() - started).toBeLessThan(2000);
});
});
describe('chatTextOf', () => {
@@ -14,8 +14,19 @@ export const SHARE_MAX_CHARS = 4800;
type Translate = (key: string, params?: Record<string, string | number>) => string;
/** A table's divider row (`|---|:--:|`), which has no words. */
const TABLE_RULE = /^\|?(?:\s*:?-+:?\s*\|)+\s*:?-*:?\s*$/;
/** What a divider cell holds once trimmed: dashes, with a colon at either end for alignment. */
const RULE_CELL = /^:?-*:?$/;
/** A table's divider row (`|---|:--:|`), which has no words. Read cell by cell, so no pattern backtracks over it. */
function isTableRule(line: string): boolean {
const cells = (line.startsWith('|') ? line.slice(1) : line).split('|');
const last = cells.pop() ?? '';
if (cells.length === 0 || !RULE_CELL.test(last.trim())) return false;
return cells.every((cell) => {
const trimmed = cell.trim();
return trimmed.includes('-') && RULE_CELL.test(trimmed);
});
}
/** One table row as its cells in a line. */
function tableRow(line: string): string {
@@ -28,11 +39,45 @@ function tableRow(line: string): string {
.join(', ');
}
/**
* Rewrites every `[text](url)` of a line (with a leading `!` for a picture), found by scanning:
* a `[` and the first `]` after it decide the match, so a long line of brackets is read once.
*/
function rewriteBrackets(line: string, picture: boolean, build: (text: string, url: string) => string): string {
let out = '';
let from = 0;
let paren = -1;
let open = line.indexOf('[');
while (open !== -1) {
if (picture && (open === 0 || line[open - 1] !== '!')) {
open = line.indexOf('[', open + 1);
continue;
}
const close = line.indexOf(']', open + 1);
if (close === -1) break;
let next = close;
if (line[close + 1] === '(') {
if (paren < close + 2) paren = line.indexOf(')', close + 2);
if (paren === -1) break;
let clean = true;
for (let at = close + 2; at < paren && clean; at++) clean = !/\s/.test(line.charAt(at));
const text = line.slice(open + 1, close);
const url = line.slice(close + 2, paren);
if (clean && (picture || (text !== '' && url !== ''))) {
out += line.slice(from, picture ? open - 1 : open) + build(text, url);
from = paren + 1;
next = paren;
}
}
open = line.indexOf('[', next + 1);
}
return out + line.slice(from);
}
/** The marks of one line of running text: emphasis, code, links and pictures. */
function inline(line: string): string {
return line
.replace(/!\[([^\]]*)\]\([^)\s]*\)/g, '$1')
.replace(/\[([^\]]+)\]\(([^)\s]+)\)/g, (_, text: string, url: string) => (text === url ? url : `${text} (${url})`))
const pictures = rewriteBrackets(line, true, (text) => text);
return rewriteBrackets(pictures, false, (text, url) => (text === url ? url : `${text} (${url})`))
.replace(/\*\*([^*]+)\*\*/g, '$1')
.replace(/__([^_]+)__/g, '$1')
.replace(/(^|[^\w*])\*([^*\s][^*]*)\*/g, '$1$2')
@@ -42,11 +87,11 @@ function inline(line: string): string {
/** One line of Markdown as text, or null for a line that only carries layout (a fence, a table rule). */
function plainLine(line: string): string | null {
if (/^\s*(?:```|~~~)/.test(line)) return null;
if (TABLE_RULE.test(line.trim())) return null;
const heading = /^\s{0,3}#{1,6}\s+(.*)$/.exec(line);
if (heading) return inline(heading[1] ?? '');
const bullet = /^(\s*)[-*+]\s+(.*)$/.exec(line);
if (bullet) return `${bullet[1] ?? ''}• ${inline(bullet[2] ?? '')}`;
if (isTableRule(line.trim())) return null;
const heading = /^\s{0,3}#{1,6}\s(.*)$/.exec(line);
if (heading) return inline((heading[1] ?? '').trimStart());
const bullet = /^(\s*)[-*+]\s(.*)$/.exec(line);
if (bullet) return `${bullet[1] ?? ''}• ${inline((bullet[2] ?? '').trimStart())}`;
const quote = /^\s*>\s?(.*)$/.exec(line);
if (quote) return inline(quote[1] ?? '');
if (line.trim().startsWith('|')) return tableRow(inline(line));
@@ -329,8 +329,10 @@ export function ChangeRows({ items, folded, excluded, disabled, undone, undoable
const Group = view?.Group;
const Run = view?.Run;
const body: ReactNode[] = [];
for (let i = 0; i < run.length; i++) {
let i = 0;
while (i < run.length) {
let end = i;
let next = i + 1;
while (end < run.length && runnable(run[end] as ChangeItem, view)) end++;
if (Run && end - i >= 2) {
const taken = run.slice(i, end);
@@ -342,8 +344,9 @@ export function ChangeRows({ items, folded, excluded, disabled, undone, undoable
</CardBoundary>
);
}
i = end - 1;
next = end;
} else body.push(row(run[i] as ChangeItem));
i = next;
}
if (!Group || !first) return body;
const group = (place: 'head' | 'foot') => (
@@ -75,7 +75,7 @@ function LabelChips({ labels }: { labels: readonly CollectionCardLabel[] }) {
))}
{rest.length > 0 && (
<Tooltip label={rest.map((label) => label.name).join(', ')} placement="top">
<span tabIndex={0} className="rounded-full outline-none focus-visible:ring-2 focus-visible:ring-accent">
<span className="rounded-full">
<WhiteBadge>+{rest.length}</WhiteBadge>
</span>
</Tooltip>