|
1 | 1 | import { describe, expect, test } from "bun:test"; |
2 | 2 | import JSZip from "jszip"; |
3 | 3 |
|
| 4 | +import type { ImagePosition } from "../types/document"; |
4 | 5 | import { parseDocx } from "./parser"; |
5 | 6 | import { createEmptyDocx, repackDocx } from "./rezip"; |
6 | 7 |
|
@@ -97,6 +98,26 @@ const bodyShapeWrapTypes = ({ package: { document } }: ParsedDocx): string[] => |
97 | 98 | return wrapTypes; |
98 | 99 | }; |
99 | 100 |
|
| 101 | +const bodyShapePositions = ({ package: { document } }: ParsedDocx): ImagePosition[] => { |
| 102 | + const positions: ImagePosition[] = []; |
| 103 | + for (const block of document.content) { |
| 104 | + if (block.type !== "paragraph") { |
| 105 | + continue; |
| 106 | + } |
| 107 | + for (const content of block.content) { |
| 108 | + if (content.type !== "run") { |
| 109 | + continue; |
| 110 | + } |
| 111 | + for (const item of content.content) { |
| 112 | + if (item.type === "shape" && item.shape.position) { |
| 113 | + positions.push(item.shape.position); |
| 114 | + } |
| 115 | + } |
| 116 | + } |
| 117 | + } |
| 118 | + return positions; |
| 119 | +}; |
| 120 | + |
100 | 121 | const savedDocumentXml = async (buffer: ArrayBuffer): Promise<string> => { |
101 | 122 | const zip = await JSZip.loadAsync(buffer); |
102 | 123 | return zip.file("word/document.xml")!.async("text"); |
@@ -164,4 +185,50 @@ describe("text-box drawing ownership", () => { |
164 | 185 | const reopened = await parseDocx(saved, { preloadFonts: false }); |
165 | 186 | expect(bodyShapeWrapTypes(reopened)).toEqual(["inline"]); |
166 | 187 | }); |
| 188 | + |
| 189 | + test("positioned VML text boxes normalize every omitted axis offset before DrawingML conversion", async () => { |
| 190 | + const cases = [ |
| 191 | + { |
| 192 | + style: "top:10pt", |
| 193 | + expectedPosition: { |
| 194 | + horizontal: { relativeTo: "character", posOffset: 0 }, |
| 195 | + vertical: { relativeTo: "paragraph", posOffset: 127_000 }, |
| 196 | + }, |
| 197 | + }, |
| 198 | + { |
| 199 | + style: "left:20pt", |
| 200 | + expectedPosition: { |
| 201 | + horizontal: { relativeTo: "character", posOffset: 254_000 }, |
| 202 | + vertical: { relativeTo: "paragraph", posOffset: 0 }, |
| 203 | + }, |
| 204 | + }, |
| 205 | + { |
| 206 | + style: "", |
| 207 | + expectedPosition: { |
| 208 | + horizontal: { relativeTo: "character", posOffset: 0 }, |
| 209 | + vertical: { relativeTo: "paragraph", posOffset: 0 }, |
| 210 | + }, |
| 211 | + }, |
| 212 | + ] as const satisfies readonly { |
| 213 | + style: string; |
| 214 | + expectedPosition: ImagePosition; |
| 215 | + }[]; |
| 216 | + |
| 217 | + for (const { style, expectedPosition } of cases) { |
| 218 | + const source = await buildDocx(` |
| 219 | + <w:pict> |
| 220 | + <v:shape style="position:absolute;${style};width:2in;height:1in"> |
| 221 | + <v:textbox> |
| 222 | + <w:txbxContent><w:p><w:r><w:t>Legacy text</w:t></w:r></w:p></w:txbxContent> |
| 223 | + </v:textbox> |
| 224 | + </v:shape> |
| 225 | + </w:pict>`); |
| 226 | + const parsed = await parseDocx(source, { preloadFonts: false }); |
| 227 | + expect(bodyShapePositions(parsed)).toEqual([expectedPosition]); |
| 228 | + |
| 229 | + const saved = await repackDocx(parsed, { updateModifiedDate: false }); |
| 230 | + const reopened = await parseDocx(saved, { preloadFonts: false }); |
| 231 | + expect(bodyShapePositions(reopened)).toEqual([expectedPosition]); |
| 232 | + } |
| 233 | + }); |
167 | 234 | }); |
0 commit comments