refactor(dedup): consolidate byte / sanitize / clock / album-duration helpers (Phase L, part 2) (#692)

Findings 5-8 of the dedup audit:

- F5 byte formatters: appUpdaterHelpers.fmtBytes + ZipDownloadOverlay
  .formatMB route through the existing formatBytes; a new formatMb
  (always-MB) backs playlistDetailHelpers.formatSize, AlbumHeader and
  the 4 inline DeviceSyncPreSyncModal expressions. SongInfoModal.format
  Size is intentionally left — it uses decimal (1e6) divisors, not 1024.
- F6 sanitizeHtml: extracted to utils/sanitizeHtml.ts; AlbumHeader,
  ComposerDetail and the (now-empty, deleted) artistDetailHelpers use it
  directly. nowPlayingHelpers keeps its own export but now delegates to
  the shared sanitiser and only adds its trailing-link strip on top.
- F7 album duration: BecauseYouLikeRail's formatAlbumDuration drops in
  favour of the shared formatHumanHoursMinutes. Behaviour note: total
  minutes now floor instead of round (<=1 min display difference,
  matches every other caller).
- F8 clock time: extracted to utils/format/formatClockTime.ts;
  PlaybackDelayModal + QueueHeader use it (toLocaleTimeString and
  Intl.DateTimeFormat produced identical output).

Behaviour preserved except the two explicitly noted divergences (F7
round->floor; F5 appUpdater/Zip now show GB above 1 GB instead of a
large MB number).
This commit is contained in:
Frank Stellmacher
2026-05-14 15:19:42 +02:00
committed by GitHub
parent 0153435787
commit 4b1dd3c29f
15 changed files with 50 additions and 105 deletions
@@ -13,11 +13,6 @@ export function isNewer(a: string, b: string): boolean {
return false;
}
export function fmtBytes(n: number): string {
if (n < 1024 * 1024) return `${(n / 1024).toFixed(0)} KB`;
return `${(n / 1024 / 1024).toFixed(1)} MB`;
}
export interface GithubAsset {
name: string;
browser_download_url: string;
@@ -1,16 +0,0 @@
/** Strip dangerous tags/attributes from server-provided HTML */
export function sanitizeHtml(html: string): string {
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
doc.querySelectorAll('script, style, iframe, object, embed, form, input, button, select, base, meta, link').forEach(el => el.remove());
doc.querySelectorAll('*').forEach(el => {
Array.from(el.attributes).forEach(attr => {
const name = attr.name.toLowerCase();
const val = attr.value.toLowerCase().trim();
if (name.startsWith('on') || (name === 'href' && (val.startsWith('javascript:') || val.startsWith('data:'))) || (name === 'src' && (val.startsWith('javascript:') || val.startsWith('data:')))) {
el.removeAttribute(attr.name);
}
});
});
return doc.body.innerHTML;
}
@@ -1,4 +1,5 @@
import type { SubsonicSong } from '../../api/subsonicTypes';
import { sanitizeHtml as sanitizeHtmlBase } from '../sanitizeHtml';
export function formatTime(s: number): string {
if (!s || isNaN(s)) return '0:00';
@@ -22,21 +23,10 @@ export function formatTotalDuration(s: number): string {
return `${sec}s`;
}
/** Shared HTML sanitiser plus a now-playing-specific tweak: strip the trailing
* "Read more on Last.fm" style link so clamped bios end cleanly. */
export function sanitizeHtml(html: string): string {
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
doc.querySelectorAll('script, style, iframe, object, embed, form, input, button, select, base, meta, link').forEach(el => el.remove());
doc.querySelectorAll('*').forEach(el => {
Array.from(el.attributes).forEach(attr => {
const name = attr.name.toLowerCase();
const val = attr.value.toLowerCase().trim();
if (name.startsWith('on') || (name === 'href' && (val.startsWith('javascript:') || val.startsWith('data:'))) || (name === 'src' && (val.startsWith('javascript:') || val.startsWith('data:')))) {
el.removeAttribute(attr.name);
}
});
});
// Strip trailing "Read more on Last.fm" style links for cleaner clamped bios.
return doc.body.innerHTML.replace(/<a [^>]*>.*?<\/a>\.?\s*$/i, '').trim();
return sanitizeHtmlBase(html).replace(/<a [^>]*>.*?<\/a>\.?\s*$/i, '').trim();
}
export function isoToParts(iso: string): { month: string; day: string; weekday: string; time: string } | null {
@@ -1,5 +1,6 @@
import type { SubsonicSong } from '../../api/subsonicTypes';
import { formatHumanHoursMinutes } from '../format/formatHumanDuration';
import { formatMb } from '../format/formatBytes';
export function sanitizeFilename(name: string): string {
return name
@@ -10,8 +11,7 @@ export function sanitizeFilename(name: string): string {
}
export function formatSize(bytes?: number): string {
if (!bytes) return '';
return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
return bytes ? formatMb(bytes) : '';
}
export function totalDurationLabel(songs: SubsonicSong[]): string {