feat: add unkown font warning
This commit is contained in:
1 parent
1abdb428ba
commit
b8233e894e
4 files changed
+57
-3
No files matched your search
+26
-3
@@ -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);
|
||||
|
||||
@@ -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).',
|
||||
|
||||
@@ -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).',
|
||||
|
||||
@@ -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[]>;
|
||||
|
||||
Reference in new issue
Block a user