feat: add unkown font warning

This commit is contained in:
Steffen Becker committed 2026-07-11 13:20:53 +02:00
1 parent 1abdb428ba
commit b8233e894e
4 files changed
+57 -3

No files matched your search

+26 -3
View File
@@ -31,6 +31,7 @@
import { t } from './lib/i18n/i18n.svelte';
import { withShortcut } from './lib/i18n/shortcut';
import { localizeImportMessage } from './lib/i18n/importMessages';
import { unavailableFonts } from './lib/utils/fontDetect';
let editor: Editor | null = $state(null);
let tick: number = $state(0);
@@ -331,6 +332,18 @@
// Replace the document with a parsed .odt; adopt its geometry/header/footer and
// track the source handle (null for the fallback file input) so Save overwrites it.
// Distinct explicit fontFamily values (textStyle marks) anywhere in a TipTap JSON tree.
function collectFontFamilies(node: unknown, out: Set<string>): void {
if (!node || typeof node !== 'object') return;
const n = node as { marks?: { type?: string; attrs?: { fontFamily?: unknown } }[]; content?: unknown[] };
if (Array.isArray(n.marks)) {
for (const m of n.marks) {
if (m?.type === 'textStyle' && typeof m.attrs?.fontFamily === 'string') out.add(m.attrs.fontFamily);
}
}
if (Array.isArray(n.content)) for (const c of n.content) collectFontFamilies(c, out);
}
async function applyImport(bytes: Uint8Array, handle: FileSystemFileHandle | null, sourceName?: string) {
if (!editor) return;
try {
@@ -369,9 +382,19 @@
};
fileHandle = isTemplate ? null : handle;
if (result.warnings.length) {
console.warn('[import] Unsupported content in opened file:', result.warnings);
alert(t().dialogs.openedWithLimitations(result.warnings.map(localizeImportMessage).join('\n• ')));
// Warn about fonts the document uses but the browser can't render, so text
// silently shown in a substitute (Liberation Serif) is at least flagged.
const fontSet = new Set<string>();
collectFontFamilies(result.content, fontSet);
collectFontFamilies(result.header, fontSet);
collectFontFamilies(result.footer, fontSet);
const missingFonts = await unavailableFonts(fontSet);
const warnings = result.warnings.map(localizeImportMessage);
if (missingFonts.length) warnings.push(t().importWarn.missingFonts(missingFonts.join(', ')));
if (warnings.length) {
console.warn('[import] Opened file with limitations:', result.warnings, missingFonts);
alert(t().dialogs.openedWithLimitations(warnings.join('\n• ')));
}
} catch (err) {
console.error('[import] Failed to open file:', err);
+1
View File
@@ -356,6 +356,7 @@ const de: Messages = {
nestedTables: 'Verschachtelte Tabellen wurden in Absätze umgewandelt',
footnotes: 'Fuß- und Endnoten wurden entfernt',
comments: 'Kommentare wurden entfernt',
missingFonts: (fonts) => `Diese Schriftarten sind nicht installiert und wurden durch Ersatzschriften dargestellt: ${fonts}`,
},
importError: {
odtArchive: 'Keine gültige .odt-Datei (Archiv konnte nicht gelesen werden).',
+1
View File
@@ -361,6 +361,7 @@ const en = {
nestedTables: 'Nested tables were flattened to paragraphs',
footnotes: 'Footnotes and endnotes were removed',
comments: 'Comments were removed',
missingFonts: (fonts: string) => `These fonts aren't installed and were shown with a substitute: ${fonts}`,
},
importError: {
odtArchive: 'Not a valid .odt file (could not read the archive).',
+29
View File
@@ -106,6 +106,35 @@ export function detectAvailableFonts(candidates: readonly string[]): string[] {
return found;
}
// Families the app always renders via bundled @font-face twins, plus CSS generics —
// never flagged as missing. The bundled names must match the @font-face in global.css.
const BUNDLED_FONTS = ['Liberation Serif', 'Times New Roman', 'Calibri', 'Arial', 'Cambria', 'Courier New'];
const GENERIC_FAMILIES = ['serif', 'sans-serif', 'monospace', 'cursive', 'fantasy', 'system-ui'];
// Of the given family names, those the browser can't render (so text declaring them is
// silently substituted). Bundled families and generics are excluded; result is deduped.
export async function unavailableFonts(families: Iterable<string>): Promise<string[]> {
const ctx = getCtx();
if (!ctx) return [];
if (typeof document !== 'undefined' && document.fonts) {
try {
await Promise.all(BUNDLED_FONTS.map((f) => document.fonts.load(`12px "${f}"`)));
} catch { /* measure with whatever loaded */ }
}
baselineWidths = null;
const skip = new Set([...BUNDLED_FONTS, ...GENERIC_FAMILIES].map((f) => f.toLowerCase()));
const seen = new Set<string>();
const missing: string[] = [];
for (const raw of families) {
const family = raw.trim();
const key = family.toLowerCase();
if (!family || seen.has(key) || skip.has(key)) continue;
seen.add(key);
if (!isAvailable(ctx, family)) missing.push(family);
}
return missing;
}
interface LocalFontData { family: string }
interface QueryLocalFontsWindow {
queryLocalFonts?: () => Promise<LocalFontData[]>;