@stll/folio-core 0.18.0 → 0.19.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.
@@ -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 mappedFamily = OFFICE_FONT_FAMILY_MAP[normalized];
131
- if (mappedFamily) addLayoutFontFace(faces, mappedFamily, descriptor);
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}`, {
@@ -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
- /** Load an already-parsed document. */
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
- /** Load an already-parsed document. */
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
- const { history, onCompatibilityChange, setDocumentLoadState } = this.callbacks;
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.loadParsedDocument(doc);
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.18.0",
3
+ "version": "0.19.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",