751 lines
18 KiB
TypeScript
751 lines
18 KiB
TypeScript
import path from "node:path";
|
||
import { MAXIMUM_DOCX_OUTPUT_BYTES } from "@md-to-pdf/core";
|
||
import {
|
||
OFFICE_RELATIONSHIP_NAMESPACE,
|
||
PACKAGE_RELATIONSHIP_NAMESPACE,
|
||
WORD_NAMESPACE,
|
||
firstDirectChild,
|
||
parseXmlPart,
|
||
type XmlElement
|
||
} from "./ooxml.js";
|
||
import {
|
||
MAXIMUM_GENERATED_DOCX_ENTRY_COUNT,
|
||
MAXIMUM_GENERATED_DOCX_UNCOMPRESSED_BYTES,
|
||
readDocxPackage
|
||
} from "./reference-package.js";
|
||
import {
|
||
validateGeneratedDocx,
|
||
type DynamicReferenceValidation
|
||
} from "./validator.js";
|
||
|
||
const OFFICE_MATH_NAMESPACE =
|
||
"http://schemas.openxmlformats.org/officeDocument/2006/math";
|
||
const WORDPROCESSING_DRAWING_NAMESPACE =
|
||
"http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing";
|
||
const IMAGE_RELATIONSHIP_TYPE =
|
||
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/image";
|
||
const HYPERLINK_RELATIONSHIP_TYPE =
|
||
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink";
|
||
const PNG_SIGNATURE = Uint8Array.of(
|
||
0x89,
|
||
0x50,
|
||
0x4e,
|
||
0x47,
|
||
0x0d,
|
||
0x0a,
|
||
0x1a,
|
||
0x0a
|
||
);
|
||
|
||
export interface DocxAcceptancePageExpectation {
|
||
widthTwips: number;
|
||
heightTwips: number;
|
||
orientation: "portrait" | "landscape";
|
||
marginsTwips: {
|
||
top: number;
|
||
right: number;
|
||
bottom: number;
|
||
left: number;
|
||
};
|
||
}
|
||
|
||
export interface DocxAcceptanceExpectation {
|
||
id: string;
|
||
page: DocxAcceptancePageExpectation;
|
||
requiredText: readonly string[];
|
||
requiredFootnoteText?: readonly string[];
|
||
minimumParagraphs?: number;
|
||
minimumTextRuns?: number;
|
||
minimumTables?: number;
|
||
minimumNumberedParagraphs?: number;
|
||
minimumHyperlinks?: number;
|
||
minimumFootnoteReferences?: number;
|
||
minimumMathObjects?: number;
|
||
minimumDrawings?: number;
|
||
minimumPngImages?: number;
|
||
requiredImageAltText?: readonly string[];
|
||
requiredStyleIds?: readonly string[];
|
||
requiredParagraphStyleIds?: readonly string[];
|
||
requirePageField?: boolean;
|
||
minimumSections?: number;
|
||
requireFirstSectionWithoutHeaderFooter?: boolean;
|
||
finalPageNumberStart?: number;
|
||
requireSectionPagesField?: boolean;
|
||
minimumFullWidthTables?: number;
|
||
maximumTextOccurrences?: Readonly<Record<string, number>>;
|
||
forbidInternalMarkers?: boolean;
|
||
}
|
||
|
||
export interface DocxAcceptanceReport {
|
||
id: string;
|
||
package: DynamicReferenceValidation;
|
||
fingerprint: string;
|
||
page: {
|
||
widthTwips: number;
|
||
heightTwips: number;
|
||
orientation: "portrait" | "landscape";
|
||
marginsTwips: {
|
||
top: number;
|
||
right: number;
|
||
bottom: number;
|
||
left: number;
|
||
};
|
||
};
|
||
structure: {
|
||
paragraphCount: number;
|
||
textRunCount: number;
|
||
tableCount: number;
|
||
numberedParagraphCount: number;
|
||
hyperlinkCount: number;
|
||
footnoteReferenceCount: number;
|
||
mathObjectCount: number;
|
||
drawingCount: number;
|
||
pngImageCount: number;
|
||
pageFieldCount: number;
|
||
sectionPageFieldCount: number;
|
||
sectionCount: number;
|
||
fullWidthTableCount: number;
|
||
internalMarkerCount: number;
|
||
altChunkCount: number;
|
||
};
|
||
imageAltText: string[];
|
||
styleIds: string[];
|
||
appliedParagraphStyleIds: string[];
|
||
checks: Record<string, true>;
|
||
}
|
||
|
||
interface Relationship {
|
||
id: string;
|
||
type: string;
|
||
target: string;
|
||
external: boolean;
|
||
}
|
||
|
||
function fail(id: string, check: string, detail: string): never {
|
||
throw new Error(`${id} 的 DOCX 自动验收失败:${check}(${detail})`);
|
||
}
|
||
|
||
function numericAttribute(
|
||
element: XmlElement,
|
||
localName: string,
|
||
id: string,
|
||
check: string
|
||
) {
|
||
const value = element.getAttributeNS(WORD_NAMESPACE, localName);
|
||
if (!value || !/^\d+$/u.test(value)) {
|
||
fail(id, check, `缺少有效的 w:${localName}`);
|
||
}
|
||
return Number(value);
|
||
}
|
||
|
||
function assertEqual(
|
||
id: string,
|
||
check: string,
|
||
actual: unknown,
|
||
expected: unknown
|
||
) {
|
||
if (actual !== expected) {
|
||
fail(
|
||
id,
|
||
check,
|
||
`期望 ${JSON.stringify(expected)},实际 ${JSON.stringify(actual)}`
|
||
);
|
||
}
|
||
}
|
||
|
||
function assertMinimum(
|
||
id: string,
|
||
check: string,
|
||
actual: number,
|
||
expected = 0
|
||
) {
|
||
if (actual < expected) {
|
||
fail(id, check, `期望至少 ${expected},实际 ${actual}`);
|
||
}
|
||
}
|
||
|
||
function parseRelationshipPart(
|
||
content: Uint8Array,
|
||
partName: string
|
||
) {
|
||
const document = parseXmlPart(content, partName);
|
||
const relationships: Relationship[] = [];
|
||
for (const element of Array.from(
|
||
document.getElementsByTagNameNS(
|
||
PACKAGE_RELATIONSHIP_NAMESPACE,
|
||
"Relationship"
|
||
)
|
||
)) {
|
||
const id = element.getAttribute("Id");
|
||
const type = element.getAttribute("Type");
|
||
const target = element.getAttribute("Target");
|
||
if (!id || !type || !target) {
|
||
throw new Error(`${partName} 包含不完整的关系声明`);
|
||
}
|
||
relationships.push({
|
||
id,
|
||
type,
|
||
target,
|
||
external:
|
||
(element.getAttribute("TargetMode") ?? "").toLowerCase() ===
|
||
"external"
|
||
});
|
||
}
|
||
return relationships;
|
||
}
|
||
|
||
function resolveDocumentTarget(target: string) {
|
||
return path.posix.normalize(
|
||
path.posix.join("word", target.replace(/^\/+/u, ""))
|
||
);
|
||
}
|
||
|
||
function hasPngSignature(content: Uint8Array) {
|
||
return (
|
||
content.byteLength >= PNG_SIGNATURE.byteLength &&
|
||
PNG_SIGNATURE.every((value, index) => content[index] === value)
|
||
);
|
||
}
|
||
|
||
function collectPageFieldCount(
|
||
entries: ReadonlyMap<string, Uint8Array>
|
||
) {
|
||
let count = 0;
|
||
for (const [partName, content] of entries) {
|
||
if (!/^word\/(?:document|header\d+|footer\d+)\.xml$/u.test(partName)) {
|
||
continue;
|
||
}
|
||
const document = parseXmlPart(content, partName);
|
||
for (const element of Array.from(
|
||
document.getElementsByTagNameNS(WORD_NAMESPACE, "instrText")
|
||
)) {
|
||
if (/\bPAGE\b/u.test(element.textContent ?? "")) {
|
||
count += 1;
|
||
}
|
||
}
|
||
}
|
||
return count;
|
||
}
|
||
|
||
function collectFieldCount(
|
||
entries: ReadonlyMap<string, Uint8Array>,
|
||
field: "SECTIONPAGES"
|
||
) {
|
||
let count = 0;
|
||
for (const [partName, content] of entries) {
|
||
if (!/^word\/(?:document|header\d+|footer\d+)\.xml$/u.test(partName)) {
|
||
continue;
|
||
}
|
||
const document = parseXmlPart(content, partName);
|
||
for (const element of Array.from(
|
||
document.getElementsByTagNameNS(WORD_NAMESPACE, "instrText")
|
||
)) {
|
||
if (
|
||
new RegExp(`\\b${field}\\b`, "u").test(
|
||
element.textContent ?? ""
|
||
)
|
||
) {
|
||
count += 1;
|
||
}
|
||
}
|
||
}
|
||
return count;
|
||
}
|
||
|
||
function countTextOccurrences(value: string, search: string) {
|
||
if (!search) {
|
||
return 0;
|
||
}
|
||
let count = 0;
|
||
let offset = 0;
|
||
while (offset <= value.length - search.length) {
|
||
const index = value.indexOf(search, offset);
|
||
if (index < 0) {
|
||
break;
|
||
}
|
||
count += 1;
|
||
offset = index + search.length;
|
||
}
|
||
return count;
|
||
}
|
||
|
||
export function inspectDocxAcceptance(
|
||
content: Uint8Array,
|
||
expectation: DocxAcceptanceExpectation
|
||
): DocxAcceptanceReport {
|
||
const packageValidation = validateGeneratedDocx(content);
|
||
const packageContent = readDocxPackage(content, {
|
||
maximumBytes: MAXIMUM_DOCX_OUTPUT_BYTES,
|
||
maximumEntryCount: MAXIMUM_GENERATED_DOCX_ENTRY_COUNT,
|
||
maximumUncompressedBytes:
|
||
MAXIMUM_GENERATED_DOCX_UNCOMPRESSED_BYTES
|
||
});
|
||
const entries = packageContent.entries;
|
||
const documentPart = entries.get("word/document.xml")!;
|
||
const document = parseXmlPart(documentPart, "word/document.xml");
|
||
const sections = Array.from(
|
||
document.getElementsByTagNameNS(WORD_NAMESPACE, "sectPr")
|
||
);
|
||
const section = sections.at(-1);
|
||
if (!section) {
|
||
fail(expectation.id, "page-section", "缺少最终节");
|
||
}
|
||
const pageSize = Array.from(
|
||
section.getElementsByTagNameNS(WORD_NAMESPACE, "pgSz")
|
||
)[0];
|
||
const pageMargins = Array.from(
|
||
section.getElementsByTagNameNS(WORD_NAMESPACE, "pgMar")
|
||
)[0];
|
||
if (!pageSize || !pageMargins) {
|
||
fail(expectation.id, "page-layout", "缺少纸张或页边距设置");
|
||
}
|
||
const actualPage = {
|
||
widthTwips: numericAttribute(
|
||
pageSize,
|
||
"w",
|
||
expectation.id,
|
||
"page-width"
|
||
),
|
||
heightTwips: numericAttribute(
|
||
pageSize,
|
||
"h",
|
||
expectation.id,
|
||
"page-height"
|
||
),
|
||
orientation:
|
||
pageSize.getAttributeNS(WORD_NAMESPACE, "orient") === "landscape"
|
||
? ("landscape" as const)
|
||
: ("portrait" as const),
|
||
marginsTwips: {
|
||
top: numericAttribute(
|
||
pageMargins,
|
||
"top",
|
||
expectation.id,
|
||
"margin-top"
|
||
),
|
||
right: numericAttribute(
|
||
pageMargins,
|
||
"right",
|
||
expectation.id,
|
||
"margin-right"
|
||
),
|
||
bottom: numericAttribute(
|
||
pageMargins,
|
||
"bottom",
|
||
expectation.id,
|
||
"margin-bottom"
|
||
),
|
||
left: numericAttribute(
|
||
pageMargins,
|
||
"left",
|
||
expectation.id,
|
||
"margin-left"
|
||
)
|
||
}
|
||
};
|
||
assertEqual(
|
||
expectation.id,
|
||
"page-width",
|
||
actualPage.widthTwips,
|
||
expectation.page.widthTwips
|
||
);
|
||
assertEqual(
|
||
expectation.id,
|
||
"page-height",
|
||
actualPage.heightTwips,
|
||
expectation.page.heightTwips
|
||
);
|
||
assertEqual(
|
||
expectation.id,
|
||
"page-orientation",
|
||
actualPage.orientation,
|
||
expectation.page.orientation
|
||
);
|
||
for (const side of ["top", "right", "bottom", "left"] as const) {
|
||
assertEqual(
|
||
expectation.id,
|
||
`margin-${side}`,
|
||
actualPage.marginsTwips[side],
|
||
expectation.page.marginsTwips[side]
|
||
);
|
||
}
|
||
assertMinimum(
|
||
expectation.id,
|
||
"sections",
|
||
sections.length,
|
||
expectation.minimumSections
|
||
);
|
||
if (expectation.requireFirstSectionWithoutHeaderFooter) {
|
||
const firstSection = sections[0]!;
|
||
const headerReferences =
|
||
firstSection.getElementsByTagNameNS(
|
||
WORD_NAMESPACE,
|
||
"headerReference"
|
||
).length;
|
||
const footerReferences =
|
||
firstSection.getElementsByTagNameNS(
|
||
WORD_NAMESPACE,
|
||
"footerReference"
|
||
).length;
|
||
assertEqual(
|
||
expectation.id,
|
||
"first-section-header-references",
|
||
headerReferences,
|
||
0
|
||
);
|
||
assertEqual(
|
||
expectation.id,
|
||
"first-section-footer-references",
|
||
footerReferences,
|
||
0
|
||
);
|
||
}
|
||
if (expectation.finalPageNumberStart !== undefined) {
|
||
const pageNumber = firstDirectChild(
|
||
section,
|
||
WORD_NAMESPACE,
|
||
"pgNumType"
|
||
);
|
||
if (!pageNumber) {
|
||
fail(
|
||
expectation.id,
|
||
"final-page-number-start",
|
||
"最终节缺少 w:pgNumType"
|
||
);
|
||
}
|
||
assertEqual(
|
||
expectation.id,
|
||
"final-page-number-start",
|
||
numericAttribute(
|
||
pageNumber,
|
||
"start",
|
||
expectation.id,
|
||
"final-page-number-start"
|
||
),
|
||
expectation.finalPageNumberStart
|
||
);
|
||
}
|
||
|
||
const paragraphCount = document.getElementsByTagNameNS(
|
||
WORD_NAMESPACE,
|
||
"p"
|
||
).length;
|
||
const textRunCount = document.getElementsByTagNameNS(
|
||
WORD_NAMESPACE,
|
||
"t"
|
||
).length;
|
||
const tableCount = document.getElementsByTagNameNS(
|
||
WORD_NAMESPACE,
|
||
"tbl"
|
||
).length;
|
||
const fullWidthTableCount = Array.from(
|
||
document.getElementsByTagNameNS(WORD_NAMESPACE, "tbl")
|
||
).filter((table) => {
|
||
const properties = firstDirectChild(
|
||
table,
|
||
WORD_NAMESPACE,
|
||
"tblPr"
|
||
);
|
||
const width = properties
|
||
? firstDirectChild(properties, WORD_NAMESPACE, "tblW")
|
||
: undefined;
|
||
return (
|
||
width?.getAttributeNS(WORD_NAMESPACE, "type") === "pct" &&
|
||
width.getAttributeNS(WORD_NAMESPACE, "w") === "5000"
|
||
);
|
||
}).length;
|
||
const numberedParagraphCount = document.getElementsByTagNameNS(
|
||
WORD_NAMESPACE,
|
||
"numPr"
|
||
).length;
|
||
const hyperlinkCount = document.getElementsByTagNameNS(
|
||
WORD_NAMESPACE,
|
||
"hyperlink"
|
||
).length;
|
||
const footnoteReferenceCount = document.getElementsByTagNameNS(
|
||
WORD_NAMESPACE,
|
||
"footnoteReference"
|
||
).length;
|
||
const mathObjectCount = document.getElementsByTagNameNS(
|
||
OFFICE_MATH_NAMESPACE,
|
||
"oMath"
|
||
).length;
|
||
const drawingCount = document.getElementsByTagNameNS(
|
||
WORD_NAMESPACE,
|
||
"drawing"
|
||
).length;
|
||
const altChunkCount = document.getElementsByTagNameNS(
|
||
WORD_NAMESPACE,
|
||
"altChunk"
|
||
).length;
|
||
assertMinimum(
|
||
expectation.id,
|
||
"paragraphs",
|
||
paragraphCount,
|
||
expectation.minimumParagraphs
|
||
);
|
||
assertMinimum(
|
||
expectation.id,
|
||
"text-runs",
|
||
textRunCount,
|
||
expectation.minimumTextRuns
|
||
);
|
||
assertMinimum(
|
||
expectation.id,
|
||
"tables",
|
||
tableCount,
|
||
expectation.minimumTables
|
||
);
|
||
assertMinimum(
|
||
expectation.id,
|
||
"full-width-tables",
|
||
fullWidthTableCount,
|
||
expectation.minimumFullWidthTables
|
||
);
|
||
assertMinimum(
|
||
expectation.id,
|
||
"numbering",
|
||
numberedParagraphCount,
|
||
expectation.minimumNumberedParagraphs
|
||
);
|
||
assertMinimum(
|
||
expectation.id,
|
||
"hyperlinks",
|
||
hyperlinkCount,
|
||
expectation.minimumHyperlinks
|
||
);
|
||
assertMinimum(
|
||
expectation.id,
|
||
"footnotes",
|
||
footnoteReferenceCount,
|
||
expectation.minimumFootnoteReferences
|
||
);
|
||
assertMinimum(
|
||
expectation.id,
|
||
"math",
|
||
mathObjectCount,
|
||
expectation.minimumMathObjects
|
||
);
|
||
assertMinimum(
|
||
expectation.id,
|
||
"drawings",
|
||
drawingCount,
|
||
expectation.minimumDrawings
|
||
);
|
||
assertEqual(expectation.id, "altChunk", altChunkCount, 0);
|
||
|
||
const bodyText = document.documentElement?.textContent ?? "";
|
||
for (const requiredText of expectation.requiredText) {
|
||
if (!bodyText.includes(requiredText)) {
|
||
fail(
|
||
expectation.id,
|
||
"editable-text",
|
||
`缺少文本 ${JSON.stringify(requiredText)}`
|
||
);
|
||
}
|
||
}
|
||
for (const [text, maximum] of Object.entries(
|
||
expectation.maximumTextOccurrences ?? {}
|
||
)) {
|
||
const actual = countTextOccurrences(bodyText, text);
|
||
if (actual > maximum) {
|
||
fail(
|
||
expectation.id,
|
||
"text-occurrences",
|
||
`${JSON.stringify(text)} 最多允许 ${maximum} 次,实际 ${actual} 次`
|
||
);
|
||
}
|
||
}
|
||
const internalMarkerCount = countTextOccurrences(
|
||
bodyText,
|
||
"MD_TO_PDF_"
|
||
);
|
||
if (expectation.forbidInternalMarkers) {
|
||
assertEqual(
|
||
expectation.id,
|
||
"internal-markers",
|
||
internalMarkerCount,
|
||
0
|
||
);
|
||
}
|
||
if ((expectation.requiredFootnoteText?.length ?? 0) > 0) {
|
||
const footnotesPart = entries.get("word/footnotes.xml");
|
||
if (!footnotesPart) {
|
||
fail(expectation.id, "footnote-text", "缺少 word/footnotes.xml");
|
||
}
|
||
const footnotes = parseXmlPart(
|
||
footnotesPart,
|
||
"word/footnotes.xml"
|
||
);
|
||
const footnoteText = footnotes.documentElement?.textContent ?? "";
|
||
for (const requiredText of expectation.requiredFootnoteText ?? []) {
|
||
if (!footnoteText.includes(requiredText)) {
|
||
fail(
|
||
expectation.id,
|
||
"footnote-text",
|
||
`缺少脚注文本 ${JSON.stringify(requiredText)}`
|
||
);
|
||
}
|
||
}
|
||
}
|
||
|
||
const relationshipPart = entries.get(
|
||
"word/_rels/document.xml.rels"
|
||
)!;
|
||
const relationships = parseRelationshipPart(
|
||
relationshipPart,
|
||
"word/_rels/document.xml.rels"
|
||
);
|
||
const hyperlinkRelationships = relationships.filter(
|
||
(relationship) =>
|
||
relationship.type === HYPERLINK_RELATIONSHIP_TYPE &&
|
||
relationship.external
|
||
).length;
|
||
assertMinimum(
|
||
expectation.id,
|
||
"hyperlink-relationships",
|
||
hyperlinkRelationships,
|
||
expectation.minimumHyperlinks
|
||
);
|
||
const imageRelationships = relationships.filter(
|
||
(relationship) =>
|
||
relationship.type === IMAGE_RELATIONSHIP_TYPE &&
|
||
!relationship.external
|
||
);
|
||
const pngParts = new Set<string>();
|
||
for (const relationship of imageRelationships) {
|
||
const partName = resolveDocumentTarget(relationship.target);
|
||
const image = entries.get(partName);
|
||
if (!image) {
|
||
fail(
|
||
expectation.id,
|
||
"image-relationship",
|
||
`${relationship.id} 指向缺失部件 ${partName}`
|
||
);
|
||
}
|
||
if (partName.toLowerCase().endsWith(".png") && hasPngSignature(image)) {
|
||
pngParts.add(partName);
|
||
}
|
||
}
|
||
assertMinimum(
|
||
expectation.id,
|
||
"png-images",
|
||
pngParts.size,
|
||
expectation.minimumPngImages
|
||
);
|
||
|
||
const imageAltText = Array.from(
|
||
document.getElementsByTagNameNS(
|
||
WORDPROCESSING_DRAWING_NAMESPACE,
|
||
"docPr"
|
||
)
|
||
)
|
||
.flatMap((element) => [
|
||
element.getAttribute("descr"),
|
||
element.getAttribute("title")
|
||
])
|
||
.filter((value): value is string => Boolean(value));
|
||
for (const requiredAltText of expectation.requiredImageAltText ?? []) {
|
||
if (!imageAltText.includes(requiredAltText)) {
|
||
fail(
|
||
expectation.id,
|
||
"image-alt-text",
|
||
`缺少替代文本 ${JSON.stringify(requiredAltText)}`
|
||
);
|
||
}
|
||
}
|
||
|
||
const styles = parseXmlPart(
|
||
entries.get("word/styles.xml")!,
|
||
"word/styles.xml"
|
||
);
|
||
const styleIds = Array.from(
|
||
styles.getElementsByTagNameNS(WORD_NAMESPACE, "style")
|
||
)
|
||
.map((element) =>
|
||
element.getAttributeNS(WORD_NAMESPACE, "styleId")
|
||
)
|
||
.filter((value): value is string => Boolean(value));
|
||
for (const requiredStyleId of expectation.requiredStyleIds ?? []) {
|
||
if (!styleIds.includes(requiredStyleId)) {
|
||
fail(
|
||
expectation.id,
|
||
"styles",
|
||
`缺少样式 ${requiredStyleId}`
|
||
);
|
||
}
|
||
}
|
||
const appliedParagraphStyleIds = Array.from(
|
||
document.getElementsByTagNameNS(WORD_NAMESPACE, "pStyle")
|
||
)
|
||
.map((element) =>
|
||
element.getAttributeNS(WORD_NAMESPACE, "val")
|
||
)
|
||
.filter((value): value is string => Boolean(value));
|
||
for (const requiredStyleId of
|
||
expectation.requiredParagraphStyleIds ?? []) {
|
||
if (!appliedParagraphStyleIds.includes(requiredStyleId)) {
|
||
fail(
|
||
expectation.id,
|
||
"applied-paragraph-styles",
|
||
`正文未使用样式 ${requiredStyleId}`
|
||
);
|
||
}
|
||
}
|
||
|
||
const pageFieldCount = collectPageFieldCount(entries);
|
||
const sectionPageFieldCount = collectFieldCount(
|
||
entries,
|
||
"SECTIONPAGES"
|
||
);
|
||
if (expectation.requirePageField) {
|
||
assertMinimum(expectation.id, "page-field", pageFieldCount, 1);
|
||
}
|
||
if (expectation.requireSectionPagesField) {
|
||
assertMinimum(
|
||
expectation.id,
|
||
"section-pages-field",
|
||
sectionPageFieldCount,
|
||
1
|
||
);
|
||
}
|
||
return {
|
||
id: expectation.id,
|
||
package: packageValidation,
|
||
fingerprint: packageContent.fingerprint,
|
||
page: actualPage,
|
||
structure: {
|
||
paragraphCount,
|
||
textRunCount,
|
||
tableCount,
|
||
numberedParagraphCount,
|
||
hyperlinkCount,
|
||
footnoteReferenceCount,
|
||
mathObjectCount,
|
||
drawingCount,
|
||
pngImageCount: pngParts.size,
|
||
pageFieldCount,
|
||
sectionPageFieldCount,
|
||
sectionCount: sections.length,
|
||
fullWidthTableCount,
|
||
internalMarkerCount,
|
||
altChunkCount
|
||
},
|
||
imageAltText,
|
||
styleIds,
|
||
appliedParagraphStyleIds,
|
||
checks: {
|
||
package: true,
|
||
page: true,
|
||
editableStructure: true,
|
||
relationships: true,
|
||
pngMedia: true,
|
||
styles: true,
|
||
sections: true,
|
||
tables: true,
|
||
textOccurrences: true,
|
||
noAltChunk: true
|
||
}
|
||
};
|
||
}
|