@stll/folio-core 0.1.1 → 0.1.3
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/docx/headerFooterParser.js +3 -0
- package/dist/docx/headerFooterVerbatim.d.ts +19 -0
- package/dist/docx/headerFooterVerbatim.js +25 -0
- package/dist/docx/metafileRaster.d.ts +30 -0
- package/dist/docx/metafileRaster.js +80 -0
- package/dist/docx/paragraphParser.js +5 -1
- package/dist/docx/parser.d.ts +12 -2
- package/dist/docx/parser.js +31 -1
- package/dist/docx/serializer/headerFooterSerializer.js +3 -0
- package/dist/docx/styleParser.js +2 -2
- package/dist/docx/tableParser.js +2 -3
- package/dist/docx/unzip.js +7 -1
- package/dist/docx/xmlParser.d.ts +7 -1
- package/dist/docx/xmlParser.js +17 -1
- package/dist/layout-bridge/convert/headerFooterLayout.d.ts +3 -2
- package/dist/layout-bridge/convert/headerFooterLayout.js +39 -15
- package/dist/layout-bridge/convert/toFlowBlocks.js +1 -0
- package/dist/layout-engine/measure/lineBreaks.d.ts +10 -0
- package/dist/layout-engine/measure/lineBreaks.js +30 -0
- package/dist/layout-engine/measure/measureContainer.js +1 -1
- package/dist/layout-engine/measure/measureParagraph.js +23 -17
- package/dist/layout-engine/measure/measureWorker.js +1 -1
- package/dist/layout-engine/types.d.ts +2 -1
- package/dist/layout-painter/renderParagraph.js +2 -1
- package/dist/paged-layout/paragraphFlash.d.ts +17 -0
- package/dist/paged-layout/paragraphFlash.js +56 -0
- package/dist/paged-layout/paragraphFlashTypes.d.ts +18 -0
- package/dist/paged-layout/paragraphFlashTypes.js +0 -0
- package/dist/utils/headerFooter.js +2 -0
- package/package.json +1 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { collectXmlnsDeclarations, parseXml } from "./xmlParser.js";
|
|
2
2
|
import { parseWatermark } from "./watermarkParser.js";
|
|
3
3
|
import { parseFooterReference, parseFooterReferences, parseHeaderReference, parseHeaderReferences } from "./headerFooterRefParser.js";
|
|
4
|
+
import { assignHeaderFooterVerbatimXml } from "./headerFooterVerbatim.js";
|
|
4
5
|
import { parseBlockContent } from "./blockContentParser.js";
|
|
5
6
|
//#region src/docx/headerFooterParser.ts
|
|
6
7
|
/**
|
|
@@ -34,6 +35,7 @@ function parseHeader(headerXml, hdrFtrType = "default", styles = null, theme = n
|
|
|
34
35
|
inHeaderFooter: true,
|
|
35
36
|
rootXmlns: collectXmlnsDeclarations(rootElement)
|
|
36
37
|
});
|
|
38
|
+
assignHeaderFooterVerbatimXml(result, headerXml);
|
|
37
39
|
return result;
|
|
38
40
|
}
|
|
39
41
|
/**
|
|
@@ -74,6 +76,7 @@ function parseFooter(footerXml, hdrFtrType = "default", styles = null, theme = n
|
|
|
74
76
|
inHeaderFooter: true,
|
|
75
77
|
rootXmlns: collectXmlnsDeclarations(rootElement)
|
|
76
78
|
});
|
|
79
|
+
assignHeaderFooterVerbatimXml(result, footerXml);
|
|
77
80
|
return result;
|
|
78
81
|
}
|
|
79
82
|
/**
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { document_d_exports } from "../types/document.js";
|
|
2
|
+
|
|
3
|
+
//#region src/docx/headerFooterVerbatim.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Folio extension on {@link HeaderFooter}: original part XML captured at parse
|
|
6
|
+
* time so unedited headers/footers re-emit byte-identically on save (VML OLE
|
|
7
|
+
* wrappers, smart tags, and other constructs the model cannot fully represent).
|
|
8
|
+
* Cleared on first edit.
|
|
9
|
+
*/
|
|
10
|
+
type HeaderFooterWithVerbatim = document_d_exports.HeaderFooter & {
|
|
11
|
+
verbatimXml?: string; /** Fingerprint of modeled fields at parse time; verbatim replay is safe only while it matches. */
|
|
12
|
+
verbatimFingerprint?: string;
|
|
13
|
+
};
|
|
14
|
+
declare const getHeaderFooterVerbatimXml: (hf: document_d_exports.HeaderFooter) => string | undefined;
|
|
15
|
+
declare const canReplayHeaderFooterVerbatim: (hf: document_d_exports.HeaderFooter) => boolean;
|
|
16
|
+
declare const assignHeaderFooterVerbatimXml: (hf: document_d_exports.HeaderFooter, xml: string) => void;
|
|
17
|
+
declare const clearHeaderFooterVerbatimXml: (hf: document_d_exports.HeaderFooter) => void;
|
|
18
|
+
//#endregion
|
|
19
|
+
export { HeaderFooterWithVerbatim, assignHeaderFooterVerbatimXml, canReplayHeaderFooterVerbatim, clearHeaderFooterVerbatimXml, getHeaderFooterVerbatimXml };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
//#region src/docx/headerFooterVerbatim.ts
|
|
2
|
+
const headerFooterSerializationFingerprint = (hf) => JSON.stringify({
|
|
3
|
+
content: hf.content,
|
|
4
|
+
watermark: hf.watermark,
|
|
5
|
+
watermarkBlockIndex: hf.watermarkBlockIndex,
|
|
6
|
+
rawWatermarkXml: hf.rawWatermarkXml
|
|
7
|
+
});
|
|
8
|
+
const getHeaderFooterVerbatimXml = (hf) => hf.verbatimXml;
|
|
9
|
+
const canReplayHeaderFooterVerbatim = (hf) => {
|
|
10
|
+
const ext = hf;
|
|
11
|
+
if (!ext.verbatimXml || !ext.verbatimFingerprint) return false;
|
|
12
|
+
return ext.verbatimFingerprint === headerFooterSerializationFingerprint(hf);
|
|
13
|
+
};
|
|
14
|
+
const assignHeaderFooterVerbatimXml = (hf, xml) => {
|
|
15
|
+
const ext = hf;
|
|
16
|
+
ext.verbatimXml = xml;
|
|
17
|
+
ext.verbatimFingerprint = headerFooterSerializationFingerprint(hf);
|
|
18
|
+
};
|
|
19
|
+
const clearHeaderFooterVerbatimXml = (hf) => {
|
|
20
|
+
const ext = hf;
|
|
21
|
+
delete ext.verbatimXml;
|
|
22
|
+
delete ext.verbatimFingerprint;
|
|
23
|
+
};
|
|
24
|
+
//#endregion
|
|
25
|
+
export { assignHeaderFooterVerbatimXml, canReplayHeaderFooterVerbatim, clearHeaderFooterVerbatimXml, getHeaderFooterVerbatimXml };
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
//#region src/docx/metafileRaster.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Extract a browser-renderable raster (PNG/JPEG) embedded inside an
|
|
4
|
+
* EMF/WMF metafile.
|
|
5
|
+
*
|
|
6
|
+
* Word frequently stores header logos / OLE preview pictures as EMF — a
|
|
7
|
+
* Windows GDI metafile browsers cannot decode. In practice such an EMF almost
|
|
8
|
+
* always carries the actual artwork as a single embedded PNG or JPEG (an
|
|
9
|
+
* `EmfPlusObject` bitmap record, or a `StretchDIBits` payload). Rather than
|
|
10
|
+
* implement a GDI renderer, this scans the byte stream for a raster signature,
|
|
11
|
+
* brackets it to its container's end marker, and returns the slice. The caller
|
|
12
|
+
* uses it as the media entry's `dataUrl` so `<img>` just works; the original
|
|
13
|
+
* EMF bytes stay on `MediaFile.data` for round-trip.
|
|
14
|
+
*
|
|
15
|
+
* When no embedded raster is found this returns `null`; callers fall back to a
|
|
16
|
+
* sized placeholder and/or the host-supplied `mediaResolver` hook.
|
|
17
|
+
*/
|
|
18
|
+
type ExtractedRaster = {
|
|
19
|
+
bytes: Uint8Array;
|
|
20
|
+
mimeType: "image/png" | "image/jpeg";
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Scan a metafile (EMF or WMF) for an embedded browser-renderable raster.
|
|
24
|
+
* Returns the first PNG, else first JPEG found; `null` when none is present.
|
|
25
|
+
*/
|
|
26
|
+
declare function extractMetafileRaster(data: ArrayBuffer | Uint8Array): ExtractedRaster | null;
|
|
27
|
+
/** True for EMF/WMF MIME types — the formats browsers cannot render natively. */
|
|
28
|
+
declare function isMetafileMimeType(mimeType: string | undefined): boolean;
|
|
29
|
+
//#endregion
|
|
30
|
+
export { ExtractedRaster, extractMetafileRaster, isMetafileMimeType };
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
//#region src/docx/metafileRaster.ts
|
|
2
|
+
function indexOfBytes(haystack, needle, from = 0) {
|
|
3
|
+
outer: for (let i = from; i + needle.length <= haystack.length; i++) {
|
|
4
|
+
for (let j = 0; j < needle.length; j++) if (haystack[i + j] !== needle[j]) continue outer;
|
|
5
|
+
return i;
|
|
6
|
+
}
|
|
7
|
+
return -1;
|
|
8
|
+
}
|
|
9
|
+
function extractPng(bytes) {
|
|
10
|
+
const start = indexOfBytes(bytes, [
|
|
11
|
+
137,
|
|
12
|
+
80,
|
|
13
|
+
78,
|
|
14
|
+
71,
|
|
15
|
+
13,
|
|
16
|
+
10,
|
|
17
|
+
26,
|
|
18
|
+
10
|
|
19
|
+
]);
|
|
20
|
+
if (start < 0) return null;
|
|
21
|
+
let off = start + 8;
|
|
22
|
+
while (off + 12 <= bytes.length) {
|
|
23
|
+
const b0 = bytes[off];
|
|
24
|
+
const b1 = bytes[off + 1];
|
|
25
|
+
const b2 = bytes[off + 2];
|
|
26
|
+
const b3 = bytes[off + 3];
|
|
27
|
+
const len = b0 << 24 | b1 << 16 | b2 << 8 | b3;
|
|
28
|
+
const type = String.fromCharCode(bytes[off + 4], bytes[off + 5], bytes[off + 6], bytes[off + 7]);
|
|
29
|
+
const next = off + 12 + (len >>> 0);
|
|
30
|
+
if (type === "IEND") return bytes.slice(start, off + 12);
|
|
31
|
+
if (next <= off || next > bytes.length) break;
|
|
32
|
+
off = next;
|
|
33
|
+
}
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
function extractJpeg(bytes) {
|
|
37
|
+
const start = indexOfBytes(bytes, [
|
|
38
|
+
255,
|
|
39
|
+
216,
|
|
40
|
+
255
|
|
41
|
+
]);
|
|
42
|
+
if (start < 0) return null;
|
|
43
|
+
let end = -1;
|
|
44
|
+
for (let i = bytes.length - 2; i > start + 1; i--) if (bytes[i] === 255 && bytes[i + 1] === 217) {
|
|
45
|
+
end = i + 2;
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
return end > start ? bytes.slice(start, end) : null;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Scan a metafile (EMF or WMF) for an embedded browser-renderable raster.
|
|
52
|
+
* Returns the first PNG, else first JPEG found; `null` when none is present.
|
|
53
|
+
*/
|
|
54
|
+
function extractMetafileRaster(data) {
|
|
55
|
+
const bytes = data instanceof Uint8Array ? data : new Uint8Array(data);
|
|
56
|
+
if (bytes.length < 32) return null;
|
|
57
|
+
const png = extractPng(bytes);
|
|
58
|
+
if (png) return {
|
|
59
|
+
bytes: png,
|
|
60
|
+
mimeType: "image/png"
|
|
61
|
+
};
|
|
62
|
+
const jpeg = extractJpeg(bytes);
|
|
63
|
+
if (jpeg) return {
|
|
64
|
+
bytes: jpeg,
|
|
65
|
+
mimeType: "image/jpeg"
|
|
66
|
+
};
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
const METAFILE_MIME = /* @__PURE__ */ new Set([
|
|
70
|
+
"image/x-emf",
|
|
71
|
+
"image/emf",
|
|
72
|
+
"image/x-wmf",
|
|
73
|
+
"image/wmf"
|
|
74
|
+
]);
|
|
75
|
+
/** True for EMF/WMF MIME types — the formats browsers cannot render natively. */
|
|
76
|
+
function isMetafileMimeType(mimeType) {
|
|
77
|
+
return !!mimeType && METAFILE_MIME.has(mimeType);
|
|
78
|
+
}
|
|
79
|
+
//#endregion
|
|
80
|
+
export { extractMetafileRaster, isMetafileMimeType };
|
|
@@ -815,7 +815,11 @@ function parseParagraphContents(paraElement, styles, theme, _numbering, rels, me
|
|
|
815
815
|
parsedContent: parseParagraphContents(child, styles, theme, null, rels, media, "default", inScopeXmlns)
|
|
816
816
|
});
|
|
817
817
|
break;
|
|
818
|
-
case "smartTag":
|
|
818
|
+
case "smartTag": {
|
|
819
|
+
const inner = parseParagraphContents(child, styles, theme, null, rels, media, trackedContext, mergeXmlnsDeclarations(inScopeXmlns, child));
|
|
820
|
+
contents.push(...inner);
|
|
821
|
+
break;
|
|
822
|
+
}
|
|
819
823
|
case "moveFromRangeStart": {
|
|
820
824
|
const id = Number.parseInt(getAttribute(child, "w", "id") ?? "0", 10);
|
|
821
825
|
const name = getAttribute(child, "w", "name") ?? "";
|
package/dist/docx/parser.d.ts
CHANGED
|
@@ -7,6 +7,15 @@ import { DocxUnzipLimits } from "./unzip.js";
|
|
|
7
7
|
* Progress callback for tracking parsing stages
|
|
8
8
|
*/
|
|
9
9
|
type ProgressCallback = (stage: string, percent: number) => void;
|
|
10
|
+
/**
|
|
11
|
+
* Host hook for converting media the browser cannot render natively
|
|
12
|
+
* (EMF/WMF/TIFF) into a displayable `data:` or `blob:` URL. Receives the
|
|
13
|
+
* parsed {@link MediaFile} (original bytes on `.data`); return the replacement
|
|
14
|
+
* URL, or `null`/`undefined` to keep the built-in handling. Built-in handling
|
|
15
|
+
* already extracts an embedded PNG/JPEG from EMF/WMF when one exists; this
|
|
16
|
+
* hook is for vector-only metafiles where the host rasterizes server-side.
|
|
17
|
+
*/
|
|
18
|
+
type MediaResolver = (file: document_d_exports.MediaFile) => Promise<string | null | undefined>;
|
|
10
19
|
/**
|
|
11
20
|
* Parsing options
|
|
12
21
|
*/
|
|
@@ -18,7 +27,8 @@ type ParseOptions = {
|
|
|
18
27
|
detectVariables?: boolean; /** Security limits for DOCX ZIP extraction */
|
|
19
28
|
unzipLimits?: Partial<Omit<DocxUnzipLimits, "allowedMediaMimeTypes">> & {
|
|
20
29
|
allowedMediaMimeTypes?: Iterable<string>;
|
|
21
|
-
};
|
|
30
|
+
}; /** Optional async hook to override display URLs for non-browser media. */
|
|
31
|
+
mediaResolver?: MediaResolver;
|
|
22
32
|
};
|
|
23
33
|
/**
|
|
24
34
|
* Parse a DOCX file into a complete Document model
|
|
@@ -64,4 +74,4 @@ declare function getDocxSummary(buffer: ArrayBuffer): Promise<{
|
|
|
64
74
|
variableCount: number;
|
|
65
75
|
}>;
|
|
66
76
|
//#endregion
|
|
67
|
-
export { DocxParseError, ParseOptions, ProgressCallback, fullParseDocx, getDocxSummary, getDocxVariables, parseDocx, quickParseDocx };
|
|
77
|
+
export { DocxParseError, MediaResolver, ParseOptions, ProgressCallback, fullParseDocx, getDocxSummary, getDocxVariables, parseDocx, quickParseDocx };
|
package/dist/docx/parser.js
CHANGED
|
@@ -12,6 +12,7 @@ import { normalizeCommentReferences } from "./commentReferenceNormalization.js";
|
|
|
12
12
|
import { extractAllTemplateVariables, parseDocumentBody } from "./documentParser.js";
|
|
13
13
|
import { parseFooter, parseHeader } from "./headerFooterParser.js";
|
|
14
14
|
import { normalizeHeaderFooterReferences } from "./headerFooterReferenceNormalization.js";
|
|
15
|
+
import { extractMetafileRaster, isMetafileMimeType } from "./metafileRaster.js";
|
|
15
16
|
import { normalizeNumberingReferences } from "./numberingReferenceNormalization.js";
|
|
16
17
|
import { parseSettings } from "./settingsParser.js";
|
|
17
18
|
import { parseStylesPackage } from "./styleParser.js";
|
|
@@ -47,7 +48,7 @@ import { TaggedError } from "better-result";
|
|
|
47
48
|
*/
|
|
48
49
|
async function parseDocx(input, options = {}) {
|
|
49
50
|
const buffer = input instanceof ArrayBuffer ? input : await toArrayBuffer(input);
|
|
50
|
-
const { onProgress = () => {}, preloadFonts = true, parseHeadersFooters = true, parseNotes = true, detectVariables = true, unzipLimits } = options;
|
|
51
|
+
const { onProgress = () => {}, preloadFonts = true, parseHeadersFooters = true, parseNotes = true, detectVariables = true, unzipLimits, mediaResolver } = options;
|
|
51
52
|
const warnings = [];
|
|
52
53
|
try {
|
|
53
54
|
const timeStage = (_name, fn) => fn();
|
|
@@ -80,6 +81,7 @@ async function parseDocx(input, options = {}) {
|
|
|
80
81
|
onProgress("Parsed numbering", 35);
|
|
81
82
|
onProgress("Processing media files...", 35);
|
|
82
83
|
const media = await timeStageAsync("media", () => buildMediaMap(raw, rels));
|
|
84
|
+
if (mediaResolver) await timeStageAsync("mediaResolver", () => applyMediaResolver(media, mediaResolver));
|
|
83
85
|
onProgress("Processed media", 40);
|
|
84
86
|
onProgress("Parsing document body...", 40);
|
|
85
87
|
let documentBody = { content: [] };
|
|
@@ -196,6 +198,11 @@ var DocxParseError = class extends TaggedError("DocxParseError")() {};
|
|
|
196
198
|
/**
|
|
197
199
|
* Build media file map from raw content and relationships
|
|
198
200
|
*/
|
|
201
|
+
function copyBytesToArrayBuffer(bytes) {
|
|
202
|
+
const buffer = new ArrayBuffer(bytes.byteLength);
|
|
203
|
+
new Uint8Array(buffer).set(bytes);
|
|
204
|
+
return buffer;
|
|
205
|
+
}
|
|
199
206
|
async function buildMediaMap(raw, _rels) {
|
|
200
207
|
const media = /* @__PURE__ */ new Map();
|
|
201
208
|
for (const [path, data] of raw.media.entries()) {
|
|
@@ -217,6 +224,20 @@ async function buildMediaMap(raw, _rels) {
|
|
|
217
224
|
continue;
|
|
218
225
|
}
|
|
219
226
|
}
|
|
227
|
+
const raster = isMetafileMimeType(mimeType) ? extractMetafileRaster(data) : null;
|
|
228
|
+
if (raster) {
|
|
229
|
+
const mediaFile = {
|
|
230
|
+
path,
|
|
231
|
+
filename,
|
|
232
|
+
mimeType,
|
|
233
|
+
data,
|
|
234
|
+
dataUrl: mediaToDataUrl(copyBytesToArrayBuffer(raster.bytes), raster.mimeType)
|
|
235
|
+
};
|
|
236
|
+
media.set(path, mediaFile);
|
|
237
|
+
const normalizedPath = path.replace(/^word\//u, "");
|
|
238
|
+
if (normalizedPath !== path) media.set(normalizedPath, mediaFile);
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
220
241
|
const mediaFile = {
|
|
221
242
|
path,
|
|
222
243
|
filename,
|
|
@@ -230,6 +251,15 @@ async function buildMediaMap(raw, _rels) {
|
|
|
230
251
|
}
|
|
231
252
|
return media;
|
|
232
253
|
}
|
|
254
|
+
async function applyMediaResolver(media, resolver) {
|
|
255
|
+
const files = [...new Set(media.values())];
|
|
256
|
+
await Promise.all(files.map(async (file) => {
|
|
257
|
+
try {
|
|
258
|
+
const url = await resolver(file);
|
|
259
|
+
if (url) file.dataUrl = url;
|
|
260
|
+
} catch {}
|
|
261
|
+
}));
|
|
262
|
+
}
|
|
233
263
|
function attachLazyDataUrl(mediaFile) {
|
|
234
264
|
let cachedDataUrl;
|
|
235
265
|
Object.defineProperty(mediaFile, "dataUrl", {
|
|
@@ -2,6 +2,7 @@ import { escapeXml } from "./xmlUtils.js";
|
|
|
2
2
|
import { serializeBlockSdt } from "./blockSdtSerializer.js";
|
|
3
3
|
import { serializeParagraph } from "./paragraphSerializer.js";
|
|
4
4
|
import { serializeTable } from "./tableSerializer.js";
|
|
5
|
+
import { canReplayHeaderFooterVerbatim, getHeaderFooterVerbatimXml } from "../headerFooterVerbatim.js";
|
|
5
6
|
//#region src/docx/serializer/headerFooterSerializer.ts
|
|
6
7
|
const NAMESPACES = {
|
|
7
8
|
wpc: "http://schemas.microsoft.com/office/word/2010/wordprocessingCanvas",
|
|
@@ -45,6 +46,8 @@ function serializeBlock(block) {
|
|
|
45
46
|
* @returns Complete XML string for header*.xml or footer*.xml
|
|
46
47
|
*/
|
|
47
48
|
function serializeHeaderFooter(hf) {
|
|
49
|
+
const verbatim = getHeaderFooterVerbatimXml(hf);
|
|
50
|
+
if (verbatim && canReplayHeaderFooterVerbatim(hf)) return verbatim;
|
|
48
51
|
const rootTag = hf.type === "header" ? "w:hdr" : "w:ftr";
|
|
49
52
|
const nsDecl = buildNamespaceDeclarations();
|
|
50
53
|
const watermarkXml = serializeWatermarkParagraph(hf);
|
package/dist/docx/styleParser.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { findChild, findChildren, getAttribute, getLocalName, parseBooleanElement, parseNumericAttribute, parseXmlDocument } from "./xmlParser.js";
|
|
1
|
+
import { findChild, findChildren, getAttribute, getLocalName, parseBooleanElement, parseNumericAttribute, parseTableMeasurementValue, parseXmlDocument } from "./xmlParser.js";
|
|
2
2
|
import { BorderStyleSchema, ConditionalStyleTypeSchema, EmphasisMarkSchema, FontThemeSchema, HighlightColorSchema, LineSpacingRuleSchema, ParagraphAlignmentSchema, ShadingPatternSchema, StyleTypeSchema, TabLeaderSchema, TabStopAlignmentSchema, TableCellTextDirectionSchema, TableRowHeightRuleSchema, TableWidthTypeSchema, TextEffectSchema, ThemeColorSlotSchema, UnderlineStyleSchema, narrowEnum } from "./parserEnums.js";
|
|
3
3
|
import { resolveThemeFontRef } from "./themeParser.js";
|
|
4
4
|
import { mergeTextFormatting } from "../utils/textFormattingMerge.js";
|
|
@@ -360,9 +360,9 @@ function parseParagraphProperties(pPr, theme) {
|
|
|
360
360
|
*/
|
|
361
361
|
function parseTableMeasurement(element) {
|
|
362
362
|
if (!element) return;
|
|
363
|
-
const w = parseNumericAttribute(element, "w", "w");
|
|
364
363
|
const rawType = getAttribute(element, "w", "type");
|
|
365
364
|
const type = rawType === null ? "dxa" : narrowEnum(rawType, TableWidthTypeSchema);
|
|
365
|
+
const w = type ? parseTableMeasurementValue(element, type) : void 0;
|
|
366
366
|
if (w !== void 0 && type) return {
|
|
367
367
|
value: w,
|
|
368
368
|
type
|
package/dist/docx/tableParser.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { findChild, findChildByLocalName, findChildren, getAttribute, getChildElements, mergeXmlnsDeclarations, parseBooleanElement, parseNumericAttribute } from "./xmlParser.js";
|
|
1
|
+
import { findChild, findChildByLocalName, findChildren, getAttribute, getChildElements, mergeXmlnsDeclarations, parseBooleanElement, parseNumericAttribute, parseTableMeasurementValue } from "./xmlParser.js";
|
|
2
2
|
import { parseBookmarkEnd, parseBookmarkStart } from "./bookmarkParser.js";
|
|
3
3
|
import { BorderStyleSchema, FloatingTableXSpecSchema, FloatingTableYSpecSchema, ShadingPatternSchema, TableCellTextDirectionSchema, ThemeColorSlotSchema, narrowEnum } from "./parserEnums.js";
|
|
4
4
|
import { parseParagraph } from "./paragraphParser.js";
|
|
@@ -12,12 +12,11 @@ import { appendBookmarkMarkerToLastParagraphInBlocks, appendBookmarkMarkerToLast
|
|
|
12
12
|
*/
|
|
13
13
|
function parseTableMeasurement(element) {
|
|
14
14
|
if (!element) return;
|
|
15
|
-
const value = parseNumericAttribute(element, "w", "w") ?? 0;
|
|
16
15
|
const typeStr = getAttribute(element, "w", "type") ?? "dxa";
|
|
17
16
|
let type = "dxa";
|
|
18
17
|
if (typeStr === "auto" || typeStr === "dxa" || typeStr === "nil" || typeStr === "pct") type = typeStr;
|
|
19
18
|
return {
|
|
20
|
-
value,
|
|
19
|
+
value: parseTableMeasurementValue(element, type) ?? 0,
|
|
21
20
|
type
|
|
22
21
|
};
|
|
23
22
|
}
|
package/dist/docx/unzip.js
CHANGED
|
@@ -37,7 +37,9 @@ const DEFAULT_ALLOWED_MEDIA_MIME_TYPES = /* @__PURE__ */ new Set([
|
|
|
37
37
|
"image/gif",
|
|
38
38
|
"image/bmp",
|
|
39
39
|
"image/tiff",
|
|
40
|
-
"image/webp"
|
|
40
|
+
"image/webp",
|
|
41
|
+
"image/x-emf",
|
|
42
|
+
"image/x-wmf"
|
|
41
43
|
]);
|
|
42
44
|
const PRESERVABLE_MEDIA_MIME_TYPES = /* @__PURE__ */ new Set([
|
|
43
45
|
...DEFAULT_ALLOWED_MEDIA_MIME_TYPES,
|
|
@@ -279,6 +281,10 @@ function isMediaContentAllowed(data, mimeType) {
|
|
|
279
281
|
case "image/bmp": return bytes[0] === 66 && bytes[1] === 77;
|
|
280
282
|
case "image/webp": return bytes[0] === 82 && bytes[1] === 73 && bytes[2] === 70 && bytes[3] === 70 && bytes[8] === 87 && bytes[9] === 69 && bytes[10] === 66 && bytes[11] === 80;
|
|
281
283
|
case "image/tiff": return bytes[0] === 73 && bytes[1] === 73 || bytes[0] === 77 && bytes[1] === 77;
|
|
284
|
+
case "image/x-emf":
|
|
285
|
+
case "image/emf": return bytes.length >= 44 && bytes[0] === 1 && bytes[40] === 32 && bytes[41] === 69 && bytes[42] === 77 && bytes[43] === 70;
|
|
286
|
+
case "image/x-wmf":
|
|
287
|
+
case "image/wmf": return bytes.length >= 4 && (bytes[0] === 215 && bytes[1] === 205 && bytes[2] === 198 && bytes[3] === 154 || (bytes[0] === 1 || bytes[0] === 2) && bytes[1] === 0 && bytes[2] === 9 && bytes[3] === 0);
|
|
282
288
|
default: return false;
|
|
283
289
|
}
|
|
284
290
|
}
|
package/dist/docx/xmlParser.d.ts
CHANGED
|
@@ -237,6 +237,12 @@ declare function parseColorElement(element: XmlElement | null | undefined): {
|
|
|
237
237
|
* @returns Parsed number or undefined
|
|
238
238
|
*/
|
|
239
239
|
declare function parseNumericAttribute(element: XmlElement | null | undefined, namespace: string | null, name: string, scale?: number): number | undefined;
|
|
240
|
+
/**
|
|
241
|
+
* Parse `w:w` on a table width/height element. For `w:type="pct"`, producers
|
|
242
|
+
* sometimes emit human-readable percentages (`100%`) instead of 50ths-of-percent
|
|
243
|
+
* (`5000`); normalize those to the ECMA-376 unit the layout engine expects.
|
|
244
|
+
*/
|
|
245
|
+
declare function parseTableMeasurementValue(element: XmlElement | null | undefined, widthType: string): number | undefined;
|
|
240
246
|
/**
|
|
241
247
|
* Parse a boolean value from an attribute or element presence
|
|
242
248
|
*
|
|
@@ -294,4 +300,4 @@ declare function mergeXmlnsDeclarations(inherited: Record<string, string>, eleme
|
|
|
294
300
|
*/
|
|
295
301
|
declare function cloneWithXmlnsDeclarations(element: XmlElement, xmlnsDecls: Record<string, string>): XmlElement;
|
|
296
302
|
//#endregion
|
|
297
|
-
export { NAMESPACES, XmlElement, cloneWithXmlnsDeclarations, collectXmlnsDeclarations, elementToXml, findAllDeep, findByFullName, findChild, findChildByLocalName, findChildren, findChildrenByLocalName, findDeep, getAttribute, getAttributeAny, getAttributeAnyPrefix, getAttributes, getChildElements, getLocalName, getNamespacePrefix, getTextContent, hasChild, hasFlag, matchesName, mergeXmlnsDeclarations, parseBooleanElement, parseColorElement, parseNumericAttribute, parseXml, parseXmlDocument };
|
|
303
|
+
export { NAMESPACES, XmlElement, cloneWithXmlnsDeclarations, collectXmlnsDeclarations, elementToXml, findAllDeep, findByFullName, findChild, findChildByLocalName, findChildren, findChildrenByLocalName, findDeep, getAttribute, getAttributeAny, getAttributeAnyPrefix, getAttributes, getChildElements, getLocalName, getNamespacePrefix, getTextContent, hasChild, hasFlag, matchesName, mergeXmlnsDeclarations, parseBooleanElement, parseColorElement, parseNumericAttribute, parseTableMeasurementValue, parseXml, parseXmlDocument };
|
package/dist/docx/xmlParser.js
CHANGED
|
@@ -409,6 +409,22 @@ function parseNumericAttribute(element, namespace, name, scale = 1) {
|
|
|
409
409
|
return num * scale;
|
|
410
410
|
}
|
|
411
411
|
/**
|
|
412
|
+
* Parse `w:w` on a table width/height element. For `w:type="pct"`, producers
|
|
413
|
+
* sometimes emit human-readable percentages (`100%`) instead of 50ths-of-percent
|
|
414
|
+
* (`5000`); normalize those to the ECMA-376 unit the layout engine expects.
|
|
415
|
+
*/
|
|
416
|
+
function parseTableMeasurementValue(element, widthType) {
|
|
417
|
+
const raw = getAttribute(element, "w", "w");
|
|
418
|
+
if (raw === null) return;
|
|
419
|
+
const trimmed = raw.trim();
|
|
420
|
+
if (widthType === "pct" && trimmed.endsWith("%")) {
|
|
421
|
+
const pct = Number.parseFloat(trimmed.slice(0, -1));
|
|
422
|
+
if (!Number.isNaN(pct)) return Math.round(pct * 50);
|
|
423
|
+
}
|
|
424
|
+
const num = Number.parseInt(trimmed, 10);
|
|
425
|
+
return Number.isNaN(num) ? void 0 : num;
|
|
426
|
+
}
|
|
427
|
+
/**
|
|
412
428
|
* Parse a boolean value from an attribute or element presence
|
|
413
429
|
*
|
|
414
430
|
* OOXML boolean conventions:
|
|
@@ -524,4 +540,4 @@ function cloneWithXmlnsDeclarations(element, xmlnsDecls) {
|
|
|
524
540
|
};
|
|
525
541
|
}
|
|
526
542
|
//#endregion
|
|
527
|
-
export { NAMESPACES, cloneWithXmlnsDeclarations, collectXmlnsDeclarations, elementToXml, findAllDeep, findByFullName, findChild, findChildByLocalName, findChildren, findChildrenByLocalName, findDeep, getAttribute, getAttributeAny, getAttributeAnyPrefix, getAttributes, getChildElements, getLocalName, getNamespacePrefix, getTextContent, hasChild, hasFlag, matchesName, mergeXmlnsDeclarations, parseBooleanElement, parseColorElement, parseNumericAttribute, parseXml, parseXmlDocument };
|
|
543
|
+
export { NAMESPACES, cloneWithXmlnsDeclarations, collectXmlnsDeclarations, elementToXml, findAllDeep, findByFullName, findChild, findChildByLocalName, findChildren, findChildrenByLocalName, findDeep, getAttribute, getAttributeAny, getAttributeAnyPrefix, getAttributes, getChildElements, getLocalName, getNamespacePrefix, getTextContent, hasChild, hasFlag, matchesName, mergeXmlnsDeclarations, parseBooleanElement, parseColorElement, parseNumericAttribute, parseTableMeasurementValue, parseXml, parseXmlDocument };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { document_d_exports } from "../../types/document.js";
|
|
2
|
-
import { FlowBlock, HeaderFooterContent, ImageRun, Measure, PageMargins } from "../../layout-engine/types.js";
|
|
2
|
+
import { FlowBlock, HeaderFooterContent, ImageRun, ImageRunPosition, Measure, PageMargins } from "../../layout-engine/types.js";
|
|
3
3
|
import { MeasureBlocksFn } from "./footnoteLayout.js";
|
|
4
4
|
import { Node } from "prosemirror-model";
|
|
5
5
|
|
|
@@ -13,6 +13,7 @@ type HeaderFooterMetrics = {
|
|
|
13
13
|
margins: PageMargins;
|
|
14
14
|
};
|
|
15
15
|
declare function normalizeHeaderFooterMeasureBlocks(blocks: FlowBlock[]): FlowBlock[];
|
|
16
|
+
declare function resolveHeaderFooterPositionedVisualTop(position: ImageRunPosition | undefined, elementHeight: number, sourceY: number, flowHeight: number, metrics: HeaderFooterMetrics): number;
|
|
16
17
|
declare function resolveHeaderFooterVisualTop(run: ImageRun, paragraphY: number, flowHeight: number, metrics: HeaderFooterMetrics): number;
|
|
17
18
|
declare function calculateHeaderFooterVisualBounds(blocks: FlowBlock[], measures: Measure[], flowHeight: number, metrics: HeaderFooterMetrics): {
|
|
18
19
|
visualTop: number;
|
|
@@ -71,4 +72,4 @@ declare function convertHeaderFooterToContent(headerFooter: document_d_exports.H
|
|
|
71
72
|
*/
|
|
72
73
|
declare function convertHeaderFooterPmDocToContent(pmDoc: Node | null | undefined, contentWidth: number, metrics: HeaderFooterMetrics, options: Omit<ConvertHeaderFooterOptions, "styles">): HeaderFooterContent | undefined;
|
|
73
74
|
//#endregion
|
|
74
|
-
export { ConvertHeaderFooterOptions, HeaderFooterMetrics, calculateHeaderFooterMarginPushBounds, calculateHeaderFooterVisualBounds, convertHeaderFooterPmDocToContent, convertHeaderFooterToContent, normalizeHeaderFooterMeasureBlocks, resolveHeaderFooterVisualTop };
|
|
75
|
+
export { ConvertHeaderFooterOptions, HeaderFooterMetrics, calculateHeaderFooterMarginPushBounds, calculateHeaderFooterVisualBounds, convertHeaderFooterPmDocToContent, convertHeaderFooterToContent, normalizeHeaderFooterMeasureBlocks, resolveHeaderFooterPositionedVisualTop, resolveHeaderFooterVisualTop };
|
|
@@ -98,28 +98,43 @@ function normalizeTableBlock(block) {
|
|
|
98
98
|
function getPositionAlignment(axis) {
|
|
99
99
|
return axis?.align ?? axis?.alignment;
|
|
100
100
|
}
|
|
101
|
-
function
|
|
101
|
+
function resolveHeaderFooterPositionedVisualTop(position, elementHeight, sourceY, flowHeight, metrics) {
|
|
102
102
|
const flowTop = metrics.section === "header" ? metrics.margins.header ?? 48 : metrics.pageSize.h - (metrics.margins.footer ?? 48) - flowHeight;
|
|
103
|
-
const vertical =
|
|
104
|
-
if (!vertical) return
|
|
103
|
+
const vertical = position?.vertical;
|
|
104
|
+
if (!vertical) return sourceY;
|
|
105
105
|
const align = getPositionAlignment(vertical);
|
|
106
106
|
const offsetPx = vertical.posOffset !== void 0 ? emuToPixels(vertical.posOffset) : void 0;
|
|
107
107
|
if (vertical.relativeTo === "page") {
|
|
108
108
|
if (offsetPx !== void 0) return offsetPx - flowTop;
|
|
109
109
|
if (align === "top") return -flowTop;
|
|
110
|
-
if (align === "bottom") return metrics.pageSize.h -
|
|
111
|
-
if (align === "center") return (metrics.pageSize.h -
|
|
110
|
+
if (align === "bottom") return metrics.pageSize.h - elementHeight - flowTop;
|
|
111
|
+
if (align === "center") return (metrics.pageSize.h - elementHeight) / 2 - flowTop;
|
|
112
112
|
}
|
|
113
113
|
if (vertical.relativeTo === "margin") {
|
|
114
114
|
const marginTop = metrics.margins.top;
|
|
115
115
|
const marginHeight = metrics.pageSize.h - metrics.margins.top - metrics.margins.bottom;
|
|
116
116
|
if (offsetPx !== void 0) return marginTop + offsetPx - flowTop;
|
|
117
117
|
if (align === "top") return marginTop - flowTop;
|
|
118
|
-
if (align === "bottom") return marginTop + marginHeight -
|
|
119
|
-
if (align === "center") return marginTop + (marginHeight -
|
|
118
|
+
if (align === "bottom") return marginTop + marginHeight - elementHeight - flowTop;
|
|
119
|
+
if (align === "center") return marginTop + (marginHeight - elementHeight) / 2 - flowTop;
|
|
120
120
|
}
|
|
121
|
-
if (offsetPx !== void 0) return
|
|
122
|
-
return
|
|
121
|
+
if (offsetPx !== void 0) return sourceY + offsetPx;
|
|
122
|
+
return sourceY;
|
|
123
|
+
}
|
|
124
|
+
function resolveHeaderFooterVisualTop(run, paragraphY, flowHeight, metrics) {
|
|
125
|
+
return resolveHeaderFooterPositionedVisualTop(run.position, run.height, paragraphY, flowHeight, metrics);
|
|
126
|
+
}
|
|
127
|
+
function pageBandInHeaderFooterCoords(metrics, flowHeight) {
|
|
128
|
+
const flowTop = metrics.section === "header" ? metrics.margins.header ?? 48 : metrics.pageSize.h - (metrics.margins.footer ?? 48) - flowHeight;
|
|
129
|
+
return {
|
|
130
|
+
top: -flowTop,
|
|
131
|
+
bottom: metrics.pageSize.h - flowTop
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
/** True when a float's box overlaps the page band in header/footer coordinates. */
|
|
135
|
+
function floatIntersectsPageBand(blockTop, blockBottom, metrics, flowHeight) {
|
|
136
|
+
const band = pageBandInHeaderFooterCoords(metrics, flowHeight);
|
|
137
|
+
return blockBottom > band.top && blockTop < band.bottom;
|
|
123
138
|
}
|
|
124
139
|
function resolveHeaderFooterFloatingTableVisualTop(floating, measure, sourceY, flowHeight, metrics) {
|
|
125
140
|
const flowTop = metrics.section === "header" ? metrics.margins.header ?? 48 : metrics.pageSize.h - (metrics.margins.footer ?? 48) - flowHeight;
|
|
@@ -156,8 +171,10 @@ function calculateHeaderFooterVisualBounds(blocks, measures, flowHeight, metrics
|
|
|
156
171
|
if (run.kind !== "image") continue;
|
|
157
172
|
if (!run.position && !isFloatingImageRun(run)) continue;
|
|
158
173
|
const runTop = resolveHeaderFooterVisualTop(run, paragraphStartY, flowHeight, metrics);
|
|
174
|
+
const runBottom = runTop + run.height;
|
|
175
|
+
if (!floatIntersectsPageBand(runTop, runBottom, metrics, flowHeight)) continue;
|
|
159
176
|
visualTop = Math.min(visualTop, runTop);
|
|
160
|
-
visualBottom = Math.max(visualBottom,
|
|
177
|
+
visualBottom = Math.max(visualBottom, runBottom);
|
|
161
178
|
}
|
|
162
179
|
cursorY = paragraphBottomY;
|
|
163
180
|
} else {
|
|
@@ -178,11 +195,18 @@ function calculateHeaderFooterVisualBounds(blocks, measures, flowHeight, metrics
|
|
|
178
195
|
cursorY = blockBottomY;
|
|
179
196
|
} else if (block.kind === "table" && block.floating && measure.kind === "table") {
|
|
180
197
|
const blockTop = resolveHeaderFooterFloatingTableVisualTop(block.floating, measure, cursorY, flowHeight, metrics);
|
|
181
|
-
|
|
182
|
-
|
|
198
|
+
const blockBottom = blockTop + blockHeight;
|
|
199
|
+
if (floatIntersectsPageBand(blockTop, blockBottom, metrics, flowHeight)) {
|
|
200
|
+
visualTop = Math.min(visualTop, blockTop);
|
|
201
|
+
visualBottom = Math.max(visualBottom, blockBottom);
|
|
202
|
+
}
|
|
183
203
|
} else if (block.kind === "textBox" && isPositionedHeaderFooterTextBoxBlock(block) && measure.kind === "textBox") {
|
|
184
|
-
|
|
185
|
-
|
|
204
|
+
const blockTop = resolveHeaderFooterPositionedVisualTop(block.position, measure.height, cursorY, flowHeight, metrics);
|
|
205
|
+
const blockBottom = blockTop + measure.height;
|
|
206
|
+
if (floatIntersectsPageBand(blockTop, blockBottom, metrics, flowHeight)) {
|
|
207
|
+
visualTop = Math.min(visualTop, blockTop);
|
|
208
|
+
visualBottom = Math.max(visualBottom, blockBottom);
|
|
209
|
+
}
|
|
186
210
|
}
|
|
187
211
|
}
|
|
188
212
|
}
|
|
@@ -429,4 +453,4 @@ function serializeRunFmt(run) {
|
|
|
429
453
|
return JSON.stringify(out);
|
|
430
454
|
}
|
|
431
455
|
//#endregion
|
|
432
|
-
export { calculateHeaderFooterMarginPushBounds, calculateHeaderFooterVisualBounds, convertHeaderFooterPmDocToContent, convertHeaderFooterToContent, normalizeHeaderFooterMeasureBlocks, resolveHeaderFooterVisualTop };
|
|
456
|
+
export { calculateHeaderFooterMarginPushBounds, calculateHeaderFooterVisualBounds, convertHeaderFooterPmDocToContent, convertHeaderFooterToContent, normalizeHeaderFooterMeasureBlocks, resolveHeaderFooterPositionedVisualTop, resolveHeaderFooterVisualTop };
|
|
@@ -814,6 +814,7 @@ function convertParagraph(node, startPos, options) {
|
|
|
814
814
|
id: nextBlockId(),
|
|
815
815
|
runs,
|
|
816
816
|
attrs,
|
|
817
|
+
...pmAttrs.paraId ? { paraId: pmAttrs.paraId } : {},
|
|
817
818
|
...bookmarkNames && bookmarkNames.length > 0 ? { bookmarks: bookmarkNames } : {},
|
|
818
819
|
pmStart: startPos,
|
|
819
820
|
pmEnd: startPos + node.nodeSize
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
//#region src/layout-engine/measure/lineBreaks.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Indices in `text` where a line may end. Latin text breaks after
|
|
4
|
+
* whitespace or hyphen; CJK text can break after every ideograph so a
|
|
5
|
+
* partial line is filled instead of pushing the whole run to the next line.
|
|
6
|
+
*/
|
|
7
|
+
declare function findWordBreaks(text: string): number[];
|
|
8
|
+
declare function isBreakChar(char: string | undefined): boolean;
|
|
9
|
+
//#endregion
|
|
10
|
+
export { findWordBreaks, isBreakChar };
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { isCjkCodePoint } from "../../utils/scriptSegments.js";
|
|
2
|
+
//#region src/layout-engine/measure/lineBreaks.ts
|
|
3
|
+
/**
|
|
4
|
+
* Indices in `text` where a line may end. Latin text breaks after
|
|
5
|
+
* whitespace or hyphen; CJK text can break after every ideograph so a
|
|
6
|
+
* partial line is filled instead of pushing the whole run to the next line.
|
|
7
|
+
*/
|
|
8
|
+
function findWordBreaks(text) {
|
|
9
|
+
const breaks = [];
|
|
10
|
+
for (let index = 0; index < text.length;) {
|
|
11
|
+
const codePoint = text.codePointAt(index);
|
|
12
|
+
if (codePoint === void 0) break;
|
|
13
|
+
const charLength = codePoint > 65535 ? 2 : 1;
|
|
14
|
+
const char = text[index];
|
|
15
|
+
if (char === " " || char === "-" || char === " ") breaks.push(index + charLength);
|
|
16
|
+
else if (isCjkCodePoint(codePoint)) breaks.push(index + charLength);
|
|
17
|
+
index += charLength;
|
|
18
|
+
}
|
|
19
|
+
return breaks;
|
|
20
|
+
}
|
|
21
|
+
function isBreakChar(char) {
|
|
22
|
+
if (char === void 0) return false;
|
|
23
|
+
if (char === " " || char === "-" || char === " ") return true;
|
|
24
|
+
const codePoint = char.codePointAt(0);
|
|
25
|
+
if (codePoint === void 0) return false;
|
|
26
|
+
if (char >= "\udc00" && char <= "\udfff") return true;
|
|
27
|
+
return isCjkCodePoint(codePoint);
|
|
28
|
+
}
|
|
29
|
+
//#endregion
|
|
30
|
+
export { findWordBreaks, isBreakChar };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { setMeasureProvider } from "./measureProvider.js";
|
|
2
2
|
import { buildFontString, getResolvedData, ptToPx } from "./measureHelpers.js";
|
|
3
|
-
import { getCachedFontMetrics, getCachedTextWidth, getTextWidthCacheGeneration, setCachedFontMetrics, setCachedTextWidth } from "./cache.js";
|
|
4
3
|
import { hasCjk, isCjkCodePoint, segmentByScript } from "../../utils/scriptSegments.js";
|
|
4
|
+
import { getCachedFontMetrics, getCachedTextWidth, getTextWidthCacheGeneration, setCachedFontMetrics, setCachedTextWidth } from "./cache.js";
|
|
5
5
|
import { canPrefetchMeasurement, prefetchMeasurement } from "./measureWorker.js";
|
|
6
6
|
import { WORKER_FONT_FINGERPRINT_TEXT, countCodePoints } from "./measureWorkerProtocol.js";
|
|
7
7
|
import { panic } from "better-result";
|
|
@@ -8,6 +8,7 @@ import { isFloatingImageRun } from "../types.js";
|
|
|
8
8
|
import { clampFloatingWrapMargins } from "./clampFloatingWrapMargins.js";
|
|
9
9
|
import { getFloatingAvailableWidth, getFloatingMargins } from "./floatingZones.js";
|
|
10
10
|
import { getListMarkerInlineWidth } from "./listMarkerWidth.js";
|
|
11
|
+
import { findWordBreaks, isBreakChar } from "./lineBreaks.js";
|
|
11
12
|
//#region src/layout-engine/measure/measureParagraph.ts
|
|
12
13
|
/**
|
|
13
14
|
* Paragraph measurement module
|
|
@@ -207,18 +208,34 @@ function isEmptyTextRun(run) {
|
|
|
207
208
|
* desync measurer and painter on tab advance for paragraphs with a tab
|
|
208
209
|
* preceding a floating image.
|
|
209
210
|
*/
|
|
211
|
+
function isBlockLayoutImageRun(run) {
|
|
212
|
+
return run.wrapType === "topAndBottom" || run.displayMode === "block";
|
|
213
|
+
}
|
|
210
214
|
function measureInlineWidthAfterTab(runs, tabIndex, fieldValues) {
|
|
211
215
|
let width = 0;
|
|
212
216
|
for (let i = tabIndex + 1; i < runs.length; i++) {
|
|
213
217
|
const next = runs[i];
|
|
214
218
|
if (!next || isTabRun(next) || isLineBreakRun(next)) break;
|
|
219
|
+
if (isImageRun(next)) {
|
|
220
|
+
if (isBlockLayoutImageRun(next)) break;
|
|
221
|
+
if (!isFloatingImageRun(next)) width += inlineImageBoundingBox(next).width || 0;
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
215
224
|
if (isTextRun(next)) width += measureTextWidth(next.text || "", runToFontStyle(next));
|
|
216
225
|
else if (isFieldRun(next)) width += measureTextWidth(fieldMeasureText(next, fieldValues), runToFontStyle(next));
|
|
217
|
-
else if (isImageRun(next) && !isFloatingImageRun(next)) width += next.width || 0;
|
|
218
226
|
else if (isMathRun(next)) width += measureTextWidth(next.plainText || "[equation]", runToFontStyle(next));
|
|
219
227
|
}
|
|
220
228
|
return width;
|
|
221
229
|
}
|
|
230
|
+
function hasFollowingTabOnLine(runs, tabIndex) {
|
|
231
|
+
for (let i = tabIndex + 1; i < runs.length; i++) {
|
|
232
|
+
const next = runs[i];
|
|
233
|
+
if (!next || isLineBreakRun(next)) break;
|
|
234
|
+
if (isImageRun(next) && isBlockLayoutImageRun(next)) break;
|
|
235
|
+
if (isTabRun(next)) return true;
|
|
236
|
+
}
|
|
237
|
+
return false;
|
|
238
|
+
}
|
|
222
239
|
/**
|
|
223
240
|
* Width of the inline content preceding the first `.` in the runs that follow
|
|
224
241
|
* a tab, used to anchor `decimal` tab stops. Mirrors `getTextAfterTab` +
|
|
@@ -249,21 +266,6 @@ function measureDecimalPrefixWidthAfterTab(runs, tabIndex, fieldValues) {
|
|
|
249
266
|
if (decimalIndex === -1 || !firstRun) return 0;
|
|
250
267
|
return measureTextWidth(text.slice(0, decimalIndex), runToFontStyle(firstRun));
|
|
251
268
|
}
|
|
252
|
-
/**
|
|
253
|
-
* Find word break points in text
|
|
254
|
-
* Returns array of indices where words end (after space/punctuation)
|
|
255
|
-
*/
|
|
256
|
-
function findWordBreaks(text) {
|
|
257
|
-
const breaks = [];
|
|
258
|
-
for (let i = 0; i < text.length; i++) {
|
|
259
|
-
const char = text[i];
|
|
260
|
-
if (char === " " || char === "-" || char === " ") breaks.push(i + 1);
|
|
261
|
-
}
|
|
262
|
-
return breaks;
|
|
263
|
-
}
|
|
264
|
-
function isBreakChar(char) {
|
|
265
|
-
return char === " " || char === "-" || char === " ";
|
|
266
|
-
}
|
|
267
269
|
function isSpaceOrTab(char) {
|
|
268
270
|
return char === " " || char === " ";
|
|
269
271
|
}
|
|
@@ -551,13 +553,17 @@ function measureParagraph(block, maxWidth, options) {
|
|
|
551
553
|
const followingWidth = measureInlineWidthAfterTab(runs, runIndex, options?.fieldValues);
|
|
552
554
|
const decimalPrefixWidth = measureDecimalPrefixWidthAfterTab(runs, runIndex, options?.fieldValues);
|
|
553
555
|
const lineX = currentLine.width + currentLine.leftOffset;
|
|
554
|
-
const
|
|
556
|
+
const isFirstLine = lines.length === 0;
|
|
557
|
+
const contentX = indentLeft + (isFirstLine ? firstLineOffset + markerInlineWidth : 0) + lineX;
|
|
558
|
+
let tabWidth = calculateTabWidth(contentX, {
|
|
555
559
|
...attrs?.tabs !== void 0 ? { explicitStops: attrs.tabs } : {},
|
|
556
560
|
leftIndent: pixelsToTwips(indentLeft)
|
|
557
561
|
}, {
|
|
558
562
|
followingWidth,
|
|
559
563
|
decimalPrefixWidth
|
|
560
564
|
}).width;
|
|
565
|
+
const lineRightEdgeX = indentLeft + (isFirstLine ? firstLineOffset + markerInlineWidth : 0) + currentLine.availableWidth + currentLine.leftOffset;
|
|
566
|
+
if (!hasFollowingTabOnLine(runs, runIndex) && (tabWidth > 0 || followingWidth > 0) && contentX + tabWidth + followingWidth > lineRightEdgeX + WIDTH_TOLERANCE) tabWidth = Math.max(1, lineRightEdgeX - contentX - followingWidth);
|
|
561
567
|
if (currentLine.width + tabWidth > currentLine.availableWidth + WIDTH_TOLERANCE) {
|
|
562
568
|
startNewLine(runIndex, 0);
|
|
563
569
|
updateMaxFont(style);
|
|
@@ -82,7 +82,7 @@ function createTransport() {
|
|
|
82
82
|
return null;
|
|
83
83
|
}
|
|
84
84
|
try {
|
|
85
|
-
return new Worker(new URL("font-metrics.worker.
|
|
85
|
+
return new Worker(new URL("font-metrics.worker.js", import.meta.url), { type: "module" });
|
|
86
86
|
} catch {
|
|
87
87
|
return null;
|
|
88
88
|
}
|
|
@@ -365,7 +365,8 @@ type ParagraphAttrs = {
|
|
|
365
365
|
*/
|
|
366
366
|
type ParagraphBlock = {
|
|
367
367
|
kind: "paragraph";
|
|
368
|
-
id: BlockId;
|
|
368
|
+
id: BlockId; /** Stable Word `w14:paraId` / PM `paraId`, when available. */
|
|
369
|
+
paraId?: string;
|
|
369
370
|
runs: Run[];
|
|
370
371
|
attrs?: ParagraphAttrs;
|
|
371
372
|
/** Names of bookmarks anchored to this paragraph; used to map a bookmark to
|
|
@@ -5,13 +5,13 @@ import { calculateTabWidth } from "../prosemirror/utils/tabCalculator.js";
|
|
|
5
5
|
import { inlineImageBoundingBox, parseRotationDegrees, rotatedBoundingBox } from "../utils/rotationBoundingBox.js";
|
|
6
6
|
import { isFloatingImageRun } from "../layout-engine/types.js";
|
|
7
7
|
import { getListMarkerInlineWidth } from "../layout-engine/measure/listMarkerWidth.js";
|
|
8
|
+
import { hasCjk, segmentByScript } from "../utils/scriptSegments.js";
|
|
8
9
|
import { applySdtDataAttrs } from "./sdtBoundary.js";
|
|
9
10
|
import { applyImageVisualAttrs, hasImageVisualAttrs, wrapImageWithCrop } from "./renderImage.js";
|
|
10
11
|
import { ommlToMathml } from "../docx/mathToMathml.js";
|
|
11
12
|
import { evaluateFieldInstruction } from "../fields/evaluateField.js";
|
|
12
13
|
import { AUTHOR_COLORS, getAuthorColorIdx } from "../utils/authorColors.js";
|
|
13
14
|
import { detectBaseDirection } from "../utils/baseDirection.js";
|
|
14
|
-
import { hasCjk, segmentByScript } from "../utils/scriptSegments.js";
|
|
15
15
|
import { getAutomaticTextColorForBackground } from "./documentColors.js";
|
|
16
16
|
import { resolveImageLineAlign } from "./renderUtils.js";
|
|
17
17
|
//#region src/layout-painter/renderParagraph.ts
|
|
@@ -1075,6 +1075,7 @@ function renderParagraphFragment(fragment, block, measure, context, options = {}
|
|
|
1075
1075
|
fragmentEl.className = PARAGRAPH_CLASS_NAMES.fragment;
|
|
1076
1076
|
fragmentEl.style.position = "relative";
|
|
1077
1077
|
fragmentEl.dataset["blockId"] = String(fragment.blockId);
|
|
1078
|
+
if (block.paraId) fragmentEl.dataset["paraId"] = block.paraId;
|
|
1078
1079
|
fragmentEl.dataset["fromLine"] = String(fragment.fromLine);
|
|
1079
1080
|
fragmentEl.dataset["toLine"] = String(fragment.toLine);
|
|
1080
1081
|
applyPmPositions(fragmentEl, fragment.pmStart, fragment.pmEnd);
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { ParagraphHighlightOptions, ScrollToParaIdOptions } from "./paragraphFlashTypes.js";
|
|
2
|
+
|
|
3
|
+
//#region src/paged-layout/paragraphFlash.d.ts
|
|
4
|
+
/** Default color used by paragraph flashes. */
|
|
5
|
+
declare const DEFAULT_PARAGRAPH_FLASH_COLOR = "rgba(255, 235, 59, 0.55)";
|
|
6
|
+
/** Default duration for paragraph flashes. */
|
|
7
|
+
declare const DEFAULT_PARAGRAPH_FLASH_DURATION_MS = 1200;
|
|
8
|
+
/** CSS class applied to paragraph fragments during a transient flash. */
|
|
9
|
+
declare const PARAGRAPH_FLASH_CLASS_NAME = "folio-paragraph-flash";
|
|
10
|
+
/** Find all painted paragraph fragments with a stable `data-para-id`. */
|
|
11
|
+
declare const findParagraphFragmentsByParaId: (root: ParentNode, paraId: string) => HTMLElement[];
|
|
12
|
+
/** Apply a transient flash to a collection of paragraph elements. */
|
|
13
|
+
declare const flashParagraphElements: (elements: Iterable<HTMLElement>, options?: ParagraphHighlightOptions) => number;
|
|
14
|
+
/** Find paragraph fragments by `paraId` and flash them. */
|
|
15
|
+
declare const flashParagraphFragmentsByParaId: (root: ParentNode, paraId: string, options?: ParagraphHighlightOptions) => boolean;
|
|
16
|
+
//#endregion
|
|
17
|
+
export { DEFAULT_PARAGRAPH_FLASH_COLOR, DEFAULT_PARAGRAPH_FLASH_DURATION_MS, PARAGRAPH_FLASH_CLASS_NAME, type ParagraphHighlightOptions, type ScrollToParaIdOptions, findParagraphFragmentsByParaId, flashParagraphElements, flashParagraphFragmentsByParaId };
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
//#region src/paged-layout/paragraphFlash.ts
|
|
2
|
+
/** Default color used by paragraph flashes. */
|
|
3
|
+
const DEFAULT_PARAGRAPH_FLASH_COLOR = "rgba(255, 235, 59, 0.55)";
|
|
4
|
+
/** Default duration for paragraph flashes. */
|
|
5
|
+
const DEFAULT_PARAGRAPH_FLASH_DURATION_MS = 1200;
|
|
6
|
+
/** CSS class applied to paragraph fragments during a transient flash. */
|
|
7
|
+
const PARAGRAPH_FLASH_CLASS_NAME = "folio-paragraph-flash";
|
|
8
|
+
const timers = /* @__PURE__ */ new WeakMap();
|
|
9
|
+
const escapeAttributeValue = (value) => {
|
|
10
|
+
const cssGlobal = globalThis;
|
|
11
|
+
if (typeof cssGlobal.CSS?.escape === "function") return cssGlobal.CSS.escape(value);
|
|
12
|
+
return value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
|
|
13
|
+
};
|
|
14
|
+
const normalizedColor = (options) => {
|
|
15
|
+
return options?.color?.trim() || "rgba(255, 235, 59, 0.55)";
|
|
16
|
+
};
|
|
17
|
+
const normalizedDurationMs = (options) => {
|
|
18
|
+
const duration = options?.durationMs;
|
|
19
|
+
if (duration == null) return DEFAULT_PARAGRAPH_FLASH_DURATION_MS;
|
|
20
|
+
if (!Number.isFinite(duration) || duration < 0) return DEFAULT_PARAGRAPH_FLASH_DURATION_MS;
|
|
21
|
+
return duration;
|
|
22
|
+
};
|
|
23
|
+
/** Find all painted paragraph fragments with a stable `data-para-id`. */
|
|
24
|
+
const findParagraphFragmentsByParaId = (root, paraId) => {
|
|
25
|
+
if (!paraId || !paraId.trim()) return [];
|
|
26
|
+
const escaped = escapeAttributeValue(paraId);
|
|
27
|
+
return Array.from(root.querySelectorAll(`.layout-paragraph[data-para-id="${escaped}"]`));
|
|
28
|
+
};
|
|
29
|
+
/** Apply a transient flash to a collection of paragraph elements. */
|
|
30
|
+
const flashParagraphElements = (elements, options) => {
|
|
31
|
+
let count = 0;
|
|
32
|
+
const color = normalizedColor(options);
|
|
33
|
+
const durationMs = normalizedDurationMs(options);
|
|
34
|
+
for (const el of elements) {
|
|
35
|
+
count++;
|
|
36
|
+
const existingTimer = timers.get(el);
|
|
37
|
+
if (existingTimer !== void 0) clearTimeout(existingTimer);
|
|
38
|
+
el.classList.remove(PARAGRAPH_FLASH_CLASS_NAME);
|
|
39
|
+
el.offsetWidth;
|
|
40
|
+
el.style.setProperty("--folio-paragraph-flash-color", color);
|
|
41
|
+
el.style.setProperty("--folio-paragraph-flash-duration", `${durationMs}ms`);
|
|
42
|
+
el.classList.add(PARAGRAPH_FLASH_CLASS_NAME);
|
|
43
|
+
const timer = setTimeout(() => {
|
|
44
|
+
el.classList.remove(PARAGRAPH_FLASH_CLASS_NAME);
|
|
45
|
+
el.style.removeProperty("--folio-paragraph-flash-color");
|
|
46
|
+
el.style.removeProperty("--folio-paragraph-flash-duration");
|
|
47
|
+
timers.delete(el);
|
|
48
|
+
}, durationMs);
|
|
49
|
+
timers.set(el, timer);
|
|
50
|
+
}
|
|
51
|
+
return count;
|
|
52
|
+
};
|
|
53
|
+
/** Find paragraph fragments by `paraId` and flash them. */
|
|
54
|
+
const flashParagraphFragmentsByParaId = (root, paraId, options) => flashParagraphElements(findParagraphFragmentsByParaId(root, paraId), options) > 0;
|
|
55
|
+
//#endregion
|
|
56
|
+
export { DEFAULT_PARAGRAPH_FLASH_COLOR, DEFAULT_PARAGRAPH_FLASH_DURATION_MS, PARAGRAPH_FLASH_CLASS_NAME, findParagraphFragmentsByParaId, flashParagraphElements, flashParagraphFragmentsByParaId };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
//#region src/paged-layout/paragraphFlashTypes.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Option shapes for `scrollToParaId(paraId, { highlight })`.
|
|
4
|
+
*
|
|
5
|
+
* DOM-free so non-browser consumers can type-import without pulling in
|
|
6
|
+
* paragraph-flash DOM helpers.
|
|
7
|
+
*/
|
|
8
|
+
/** Customization for the transient paragraph flash applied by `scrollToParaId`. */
|
|
9
|
+
type ParagraphHighlightOptions = {
|
|
10
|
+
/** CSS color used for the transient paragraph flash. Defaults to yellow. */color?: string; /** How long the flash remains visible before it is removed. Defaults to 1200ms. */
|
|
11
|
+
durationMs?: number;
|
|
12
|
+
};
|
|
13
|
+
/** Optional reveal behavior for `scrollToParaId`. */
|
|
14
|
+
type ScrollToParaIdOptions = {
|
|
15
|
+
/** Flash rendered paragraph fragments after scrolling to the paragraph. */highlight?: ParagraphHighlightOptions;
|
|
16
|
+
};
|
|
17
|
+
//#endregion
|
|
18
|
+
export { ParagraphHighlightOptions, ScrollToParaIdOptions };
|
|
File without changes
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { clearHeaderFooterVerbatimXml } from "../docx/headerFooterVerbatim.js";
|
|
1
2
|
//#region src/utils/headerFooter.ts
|
|
2
3
|
const EMPTY_RESOLUTION = {
|
|
3
4
|
headerContent: null,
|
|
@@ -206,6 +207,7 @@ const saveHeaderFooterContent = ({ document, position, isFirstPage, activeRId, b
|
|
|
206
207
|
hdrFtrType: isFirstPage ? "first" : "default",
|
|
207
208
|
content: blocks
|
|
208
209
|
};
|
|
210
|
+
clearHeaderFooterVerbatimXml(updated);
|
|
209
211
|
const newMap = new Map(map);
|
|
210
212
|
newMap.set(activeRId, updated);
|
|
211
213
|
return {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stll/folio-core",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
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",
|