@stll/folio-core 0.18.0 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/controller/fontReadiness.js +12 -11
- package/dist/controller/hiddenEditorManager.d.ts +4 -5
- package/dist/controller/hiddenEditorManager.js +2 -8
- package/dist/managers/DocumentLoaderManager.d.ts +13 -1
- package/dist/managers/DocumentLoaderManager.js +11 -3
- package/dist/utils/fontResolver.d.ts +8 -1
- package/dist/utils/fontResolver.js +57 -1
- package/package.json +1 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { expectFontFamilyMarkAttrs } from "../prosemirror/attrs/index.js";
|
|
2
|
+
import { parseFontFamilyList, resolveFontFamily } from "../utils/fontResolver.js";
|
|
2
3
|
//#region src/controller/fontReadiness.ts
|
|
3
4
|
function getDocumentFontSet() {
|
|
4
5
|
if (typeof document === "undefined" || !("fonts" in document)) return null;
|
|
@@ -10,15 +11,6 @@ function documentFontsAreLoaded() {
|
|
|
10
11
|
}
|
|
11
12
|
const INITIAL_LAYOUT_FONT_TIMEOUT_MS = 2e3;
|
|
12
13
|
const DEFAULT_LAYOUT_FONT_FAMILY = "Calibri";
|
|
13
|
-
const OFFICE_FONT_FAMILY_MAP = {
|
|
14
|
-
Aptos: "Lato",
|
|
15
|
-
"Aptos Display": "Lato",
|
|
16
|
-
Arial: "Arimo",
|
|
17
|
-
Calibri: "Carlito",
|
|
18
|
-
Cambria: "Caladea",
|
|
19
|
-
"Times New Roman": "Tinos",
|
|
20
|
-
"Courier New": "Cousine"
|
|
21
|
-
};
|
|
22
14
|
const CSS_GENERIC_FONT_FAMILIES = /* @__PURE__ */ new Set([
|
|
23
15
|
"serif",
|
|
24
16
|
"sans-serif",
|
|
@@ -127,8 +119,17 @@ function addLayoutFontFamilyNameFace(faces, family, descriptor) {
|
|
|
127
119
|
const normalized = family.trim();
|
|
128
120
|
if (!normalized || CSS_GENERIC_FONT_FAMILIES.has(normalized)) return;
|
|
129
121
|
addLayoutFontFace(faces, normalized, descriptor);
|
|
130
|
-
const
|
|
131
|
-
|
|
122
|
+
for (const stackFamily of resolvedStackFamilies(normalized)) addLayoutFontFace(faces, stackFamily, descriptor);
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* The concrete families in a resolved CSS font stack, generics dropped.
|
|
126
|
+
*
|
|
127
|
+
* Parsed from the stack rather than read from a map because the stack is what
|
|
128
|
+
* the painter and the measurer put in `ctx.font` and `style.fontFamily`.
|
|
129
|
+
*/
|
|
130
|
+
function resolvedStackFamilies(family) {
|
|
131
|
+
const { cssFallback } = resolveFontFamily(family);
|
|
132
|
+
return parseFontFamilyList(cssFallback).filter((name) => !CSS_GENERIC_FONT_FAMILIES.has(name));
|
|
132
133
|
}
|
|
133
134
|
function addLayoutFontFace(faces, family, descriptor) {
|
|
134
135
|
faces.set(`${family}|${descriptor.style}|${descriptor.weight}`, {
|
|
@@ -61,12 +61,11 @@ type HiddenEditorManagerDeps = {
|
|
|
61
61
|
getPrecomputedInitialState: () => EditorState | null | undefined;
|
|
62
62
|
getReadOnly: () => boolean;
|
|
63
63
|
/**
|
|
64
|
-
*
|
|
65
|
-
* internal edits (so typing does not trigger an external
|
|
66
|
-
* distinct
|
|
67
|
-
* signature is used as a fallback when undefined.
|
|
64
|
+
* Identity of the loaded document as tracked by the adapter's loader: the
|
|
65
|
+
* same value across internal edits (so typing does not trigger an external
|
|
66
|
+
* re-sync) and a distinct value per load.
|
|
68
67
|
*/
|
|
69
|
-
|
|
68
|
+
getDocumentIdentity: () => string;
|
|
70
69
|
/** Document context for the API's `getDocument` (PM state -> Document). */
|
|
71
70
|
getDocumentContext: () => document_d_exports.Document | null;
|
|
72
71
|
onTransaction: (transaction: Transaction, newState: EditorState) => void;
|
|
@@ -152,11 +152,6 @@ function createHiddenEditorState(options) {
|
|
|
152
152
|
recordHiddenEditorPhase(reason, "editor-state", performance.now() - startedAt);
|
|
153
153
|
return state;
|
|
154
154
|
}
|
|
155
|
-
function getDocumentIdentity(doc) {
|
|
156
|
-
if (!doc) return "empty";
|
|
157
|
-
const meta = doc.package.properties;
|
|
158
|
-
return `${meta?.created ? String(meta.created) : ""}-${meta?.modified ? String(meta.modified) : ""}-${meta?.title ?? ""}`;
|
|
159
|
-
}
|
|
160
155
|
function syncHiddenEditorAccessibility(view, readOnly) {
|
|
161
156
|
const { dom } = view;
|
|
162
157
|
if (!dom.hasAttribute("tabindex")) dom.tabIndex = 0;
|
|
@@ -193,7 +188,6 @@ const createHiddenEditorManager = (deps) => {
|
|
|
193
188
|
let isInitialized = false;
|
|
194
189
|
let lastDocumentId = null;
|
|
195
190
|
let lastCollaborationFragment = null;
|
|
196
|
-
const currentDocumentIdentity = () => deps.getDocumentKey() ?? getDocumentIdentity(deps.getDocument());
|
|
197
191
|
const tryCreate = () => {
|
|
198
192
|
if (!requested || view !== null || isDestroying) return;
|
|
199
193
|
const host = deps.getHost();
|
|
@@ -264,7 +258,7 @@ const createHiddenEditorManager = (deps) => {
|
|
|
264
258
|
recordHiddenEditorPhase("mount", "editor-view", performance.now() - viewStartedAt);
|
|
265
259
|
syncHiddenEditorAccessibility(view, deps.getReadOnly());
|
|
266
260
|
isInitialized = true;
|
|
267
|
-
lastDocumentId =
|
|
261
|
+
lastDocumentId = deps.getDocumentIdentity();
|
|
268
262
|
lastCollaborationFragment = collaboration?.yXmlFragment ?? null;
|
|
269
263
|
deps.onEditorViewReady(view);
|
|
270
264
|
};
|
|
@@ -291,7 +285,7 @@ const createHiddenEditorManager = (deps) => {
|
|
|
291
285
|
const collaborationModules = deps.getCollaborationModules();
|
|
292
286
|
if (collaboration && !collaborationModules) return;
|
|
293
287
|
const document = deps.getDocument();
|
|
294
|
-
const currentDocId =
|
|
288
|
+
const currentDocId = deps.getDocumentIdentity();
|
|
295
289
|
const currentCollaborationFragment = collaboration?.yXmlFragment ?? null;
|
|
296
290
|
const collaborationSourceChanged = currentCollaborationFragment !== lastCollaborationFragment;
|
|
297
291
|
if (collaboration && !collaborationSourceChanged) return;
|
|
@@ -23,6 +23,13 @@ type DocumentLoaderHistory = {
|
|
|
23
23
|
type DocumentLoaderCallbacks = {
|
|
24
24
|
/** Editor history to reset/seed with the loaded document. */
|
|
25
25
|
history: DocumentLoaderHistory;
|
|
26
|
+
/**
|
|
27
|
+
* Receives the identity of the document that just landed in history. Every
|
|
28
|
+
* load allocates a fresh identity, so the adapter can tell an external load
|
|
29
|
+
* (a new file, or the same file re-parsed) from the loaded document being
|
|
30
|
+
* round-tripped back through its own state after an internal edit.
|
|
31
|
+
*/
|
|
32
|
+
setLoadedDocumentIdentity: (identity: string) => void;
|
|
26
33
|
/** Called when an unrecoverable parse error occurs. */
|
|
27
34
|
onError: ((error: Error) => void) | undefined;
|
|
28
35
|
/** Called after parsing to report whether editing can preserve fidelity. */
|
|
@@ -40,8 +47,13 @@ declare class DocumentLoaderManager {
|
|
|
40
47
|
setCallbacks(callbacks: DocumentLoaderCallbacks): void;
|
|
41
48
|
/** Reset color/UI state coupled to the previous document. */
|
|
42
49
|
resetForNewDocument(): void;
|
|
43
|
-
/**
|
|
50
|
+
/**
|
|
51
|
+
* Load an already-parsed document. Allocates a load generation like
|
|
52
|
+
* `loadBuffer`, so a buffer parse still in flight when this lands is
|
|
53
|
+
* discarded instead of overwriting the newer document.
|
|
54
|
+
*/
|
|
44
55
|
loadParsedDocument(doc: document_d_exports.Document): void;
|
|
56
|
+
private commitParsedDocument;
|
|
45
57
|
/**
|
|
46
58
|
* Parse and load a raw DOCX buffer. A monotonic generation counter discards
|
|
47
59
|
* the result (and any error) when a newer load started while this one was in
|
|
@@ -30,11 +30,19 @@ var DocumentLoaderManager = class {
|
|
|
30
30
|
resetAuthorColors();
|
|
31
31
|
this.callbacks.onReset();
|
|
32
32
|
}
|
|
33
|
-
/**
|
|
33
|
+
/**
|
|
34
|
+
* Load an already-parsed document. Allocates a load generation like
|
|
35
|
+
* `loadBuffer`, so a buffer parse still in flight when this lands is
|
|
36
|
+
* discarded instead of overwriting the newer document.
|
|
37
|
+
*/
|
|
34
38
|
loadParsedDocument(doc) {
|
|
35
|
-
|
|
39
|
+
this.commitParsedDocument(doc, ++this.loadGeneration);
|
|
40
|
+
}
|
|
41
|
+
commitParsedDocument(doc, generation) {
|
|
42
|
+
const { history, onCompatibilityChange, setDocumentLoadState, setLoadedDocumentIdentity } = this.callbacks;
|
|
36
43
|
this.resetForNewDocument();
|
|
37
44
|
history.reset(doc);
|
|
45
|
+
setLoadedDocumentIdentity(String(generation));
|
|
38
46
|
onCompatibilityChange?.(inspectDocxCompatibility(doc));
|
|
39
47
|
setDocumentLoadState({ status: "ready" });
|
|
40
48
|
if (doc.requiredFonts && doc.requiredFonts.length > 0) loadFontsWithMapping(doc.requiredFonts).catch(() => void 0);
|
|
@@ -61,7 +69,7 @@ var DocumentLoaderManager = class {
|
|
|
61
69
|
recordDocumentLoadPhase("docx-parse", performance.now() - parseStartedAt);
|
|
62
70
|
}
|
|
63
71
|
if (this.loadGeneration !== generation) return;
|
|
64
|
-
this.
|
|
72
|
+
this.commitParsedDocument(doc, generation);
|
|
65
73
|
} catch (error) {
|
|
66
74
|
if (this.loadGeneration !== generation) return;
|
|
67
75
|
const message = error instanceof Error ? error.message : "Failed to parse document";
|
|
@@ -48,6 +48,13 @@ declare const setGoogleFontsEnabled: (enabled: boolean) => void;
|
|
|
48
48
|
declare const getGoogleFontsEnabled: () => boolean;
|
|
49
49
|
declare const setEmbeddedFontFamilyMap: (map: ReadonlyMap<string, string> | null) => void;
|
|
50
50
|
declare function resolveFontFamily(docxFontName: string): ResolvedFont;
|
|
51
|
+
/**
|
|
52
|
+
* Split a CSS `font-family` list built by `quoteFontName` back into family
|
|
53
|
+
* names: the inverse of the quoting and escaping applied there. Quoted entries
|
|
54
|
+
* may contain commas, quotes (backslash-escaped), and the newline escapes
|
|
55
|
+
* `escapeQuotedFontName` emits; unquoted entries end at the next comma.
|
|
56
|
+
*/
|
|
57
|
+
declare function parseFontFamilyList(list: string): string[];
|
|
51
58
|
/**
|
|
52
59
|
* Resolve a theme font reference to actual font names
|
|
53
60
|
*
|
|
@@ -86,4 +93,4 @@ declare function getGoogleFontEquivalent(docxFontName: string): string | null;
|
|
|
86
93
|
*/
|
|
87
94
|
declare function hasGoogleFontEquivalent(docxFontName: string): boolean;
|
|
88
95
|
//#endregion
|
|
89
|
-
export { CJK_FALLBACK_FONT_FAMILY, DEFAULT_SINGLE_LINE_RATIO, ResolvedFont, buildFontFamilyString, getGoogleFontEquivalent, getGoogleFontsEnabled, getGoogleFontsToLoad, hasGoogleFontEquivalent, isCjkFont, resolveFontFamily, resolveThemeFont, setEmbeddedFontFamilyMap, setGoogleFontsEnabled };
|
|
96
|
+
export { CJK_FALLBACK_FONT_FAMILY, DEFAULT_SINGLE_LINE_RATIO, ResolvedFont, buildFontFamilyString, getGoogleFontEquivalent, getGoogleFontsEnabled, getGoogleFontsToLoad, hasGoogleFontEquivalent, isCjkFont, parseFontFamilyList, resolveFontFamily, resolveThemeFont, setEmbeddedFontFamilyMap, setGoogleFontsEnabled };
|
|
@@ -865,6 +865,62 @@ function quoteFontName(fontName) {
|
|
|
865
865
|
if (/[\s,'"()]/.test(fontName)) return `"${escapeQuotedFontName(fontName)}"`;
|
|
866
866
|
return fontName;
|
|
867
867
|
}
|
|
868
|
+
const CSS_NEWLINE_UNESCAPES = {
|
|
869
|
+
a: "\n",
|
|
870
|
+
d: "\r",
|
|
871
|
+
c: "\f"
|
|
872
|
+
};
|
|
873
|
+
/**
|
|
874
|
+
* Split a CSS `font-family` list built by `quoteFontName` back into family
|
|
875
|
+
* names: the inverse of the quoting and escaping applied there. Quoted entries
|
|
876
|
+
* may contain commas, quotes (backslash-escaped), and the newline escapes
|
|
877
|
+
* `escapeQuotedFontName` emits; unquoted entries end at the next comma.
|
|
878
|
+
*/
|
|
879
|
+
function parseFontFamilyList(list) {
|
|
880
|
+
const families = [];
|
|
881
|
+
let current = "";
|
|
882
|
+
let quote = null;
|
|
883
|
+
let quoted = false;
|
|
884
|
+
const push = () => {
|
|
885
|
+
const name = quoted ? current : current.trim();
|
|
886
|
+
if (name) families.push(name);
|
|
887
|
+
current = "";
|
|
888
|
+
quoted = false;
|
|
889
|
+
};
|
|
890
|
+
for (let index = 0; index < list.length; index += 1) {
|
|
891
|
+
const char = list[index];
|
|
892
|
+
if (quote === null) {
|
|
893
|
+
if (char === ",") push();
|
|
894
|
+
else if (quoted) continue;
|
|
895
|
+
else if ((char === "\"" || char === "'") && current.trim() === "") {
|
|
896
|
+
quote = char;
|
|
897
|
+
current = "";
|
|
898
|
+
} else current += char;
|
|
899
|
+
continue;
|
|
900
|
+
}
|
|
901
|
+
if (char === quote) {
|
|
902
|
+
quote = null;
|
|
903
|
+
quoted = true;
|
|
904
|
+
continue;
|
|
905
|
+
}
|
|
906
|
+
if (char !== "\\") {
|
|
907
|
+
current += char;
|
|
908
|
+
continue;
|
|
909
|
+
}
|
|
910
|
+
const escaped = list[index + 1];
|
|
911
|
+
if (escaped === void 0) continue;
|
|
912
|
+
index += 1;
|
|
913
|
+
const newline = CSS_NEWLINE_UNESCAPES[escaped];
|
|
914
|
+
if (newline === void 0) {
|
|
915
|
+
current += escaped;
|
|
916
|
+
continue;
|
|
917
|
+
}
|
|
918
|
+
current += newline;
|
|
919
|
+
if (list[index + 1] === " ") index += 1;
|
|
920
|
+
}
|
|
921
|
+
push();
|
|
922
|
+
return families;
|
|
923
|
+
}
|
|
868
924
|
/**
|
|
869
925
|
* Resolve a theme font reference to actual font names
|
|
870
926
|
*
|
|
@@ -928,4 +984,4 @@ function hasGoogleFontEquivalent(docxFontName) {
|
|
|
928
984
|
return (CJK_FONT_ALIASES[normalizedName] ?? normalizedName) in FONT_MAPPINGS;
|
|
929
985
|
}
|
|
930
986
|
//#endregion
|
|
931
|
-
export { CJK_FALLBACK_FONT_FAMILY, DEFAULT_SINGLE_LINE_RATIO, buildFontFamilyString, getGoogleFontEquivalent, getGoogleFontsEnabled, getGoogleFontsToLoad, hasGoogleFontEquivalent, isCjkFont, resolveFontFamily, resolveThemeFont, setEmbeddedFontFamilyMap, setGoogleFontsEnabled };
|
|
987
|
+
export { CJK_FALLBACK_FONT_FAMILY, DEFAULT_SINGLE_LINE_RATIO, buildFontFamilyString, getGoogleFontEquivalent, getGoogleFontsEnabled, getGoogleFontsToLoad, hasGoogleFontEquivalent, isCjkFont, parseFontFamilyList, resolveFontFamily, resolveThemeFont, setEmbeddedFontFamilyMap, setGoogleFontsEnabled };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stll/folio-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.20.0",
|
|
4
4
|
"description": "Headless, framework-neutral core of folio: the OOXML (.docx) parser, document model, ProseMirror integration, and page-layout engine. No React.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"document-model",
|