@coco-vexel/coco-office-oojson 0.0.0-stage → 1.0.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/LICENSE +202 -0
- package/README.md +99 -2
- package/THIRD_PARTY_NOTICES.md +1188 -0
- package/dist/oojson.global.js +30 -0
- package/package.json +45 -3
- package/src/color.mjs +38 -0
- package/src/extra.mjs +37 -0
- package/src/flat-opc.mjs +394 -0
- package/src/index.mjs +39 -0
- package/src/json-to-ooxml.mjs +605 -0
- package/src/long-tail-parts.mjs +424 -0
- package/src/numbering-to-ooxml.mjs +120 -0
- package/src/ooxml-drawing-to-json.mjs +209 -0
- package/src/ooxml-to-json.mjs +305 -0
- package/src/ppt-content.mjs +346 -0
- package/src/ppt-deck.mjs +248 -0
- package/src/styles-to-ooxml.mjs +302 -0
- package/src/writeback.mjs +1082 -0
- package/src/xml.mjs +66 -0
package/package.json
CHANGED
|
@@ -1,6 +1,48 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@coco-vexel/coco-office-oojson",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"description": "
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"license": "Apache-2.0",
|
|
5
|
+
"description": "OOJSON to OOXML / Flat OPC conversion and writeback bridge.",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./src/index.mjs",
|
|
9
|
+
"./package.json": "./package.json",
|
|
10
|
+
"./browser-global": "./dist/oojson.global.js"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@coco-vexel/coco-office": "^1.0.0"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"src",
|
|
17
|
+
"dist",
|
|
18
|
+
"LICENSE",
|
|
19
|
+
"THIRD_PARTY_NOTICES.md"
|
|
20
|
+
],
|
|
21
|
+
"main": "./src/index.mjs",
|
|
22
|
+
"repository": {
|
|
23
|
+
"type": "git",
|
|
24
|
+
"url": "https://github.com/coco-vexel/coco-office.git",
|
|
25
|
+
"directory": "packages/oojson"
|
|
26
|
+
},
|
|
27
|
+
"homepage": "https://github.com/coco-vexel/coco-office/tree/main/packages/oojson",
|
|
28
|
+
"bugs": {
|
|
29
|
+
"url": "https://github.com/coco-vexel/coco-office/issues"
|
|
30
|
+
},
|
|
31
|
+
"publishConfig": {
|
|
32
|
+
"access": "public"
|
|
33
|
+
},
|
|
34
|
+
"scripts": {
|
|
35
|
+
"build": "esbuild src/index.mjs --bundle --format=iife --global-name=oojson --outfile=dist/oojson.global.js --legal-comments=eof --minify",
|
|
36
|
+
"test:big": "node test/raw.mjs test/captured-big",
|
|
37
|
+
"test:styles": "node test/styles-roundtrip.mjs",
|
|
38
|
+
"test:drawing": "node test/ooxml-drawing.mjs",
|
|
39
|
+
"test:writeback": "node test/writeback-adapter.mjs",
|
|
40
|
+
"test:ppt": "node test/ppt-content.mjs && node test/ppt-deck.mjs",
|
|
41
|
+
"test:ooxml": "node test/ooxml-baseline.mjs",
|
|
42
|
+
"test": "corepack pnpm test:styles && corepack pnpm test:drawing && corepack pnpm test:writeback && corepack pnpm test:ppt && corepack pnpm test:ooxml",
|
|
43
|
+
"test:portable": "corepack pnpm test:styles && corepack pnpm test:drawing && corepack pnpm test:writeback && corepack pnpm test:ppt",
|
|
44
|
+
"probe:longtail": "node test/probe-longtail.mjs",
|
|
45
|
+
"diag": "node test/diag.mjs test/captured-big",
|
|
46
|
+
"capture": "python test/capture.py"
|
|
47
|
+
}
|
|
6
48
|
}
|
package/src/color.mjs
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// 颜色:ONLYOFFICE-JSON {auto,r,g,b} ↔ OOXML 值 "RRGGBB" | "auto"。两工具共享。
|
|
2
|
+
const hx = (n) => (n & 0xff).toString(16).padStart(2, "0").toUpperCase();
|
|
3
|
+
export const colorToOoxml = (c) => (!c || c.auto ? "auto" : hx(c.r) + hx(c.g) + hx(c.b));
|
|
4
|
+
export const colorToJson = (s) =>
|
|
5
|
+
!s || s === "auto"
|
|
6
|
+
? { auto: true, r: 0, g: 0, b: 0 }
|
|
7
|
+
: { auto: false, r: parseInt(s.slice(0, 2), 16), g: parseInt(s.slice(2, 4), 16), b: parseInt(s.slice(4, 6), 16) };
|
|
8
|
+
|
|
9
|
+
// auto 色在 OOXML 里只存 "auto"(不携带 rgb),但 ONLYOFFICE-JSON 的 auto 色带 r/g/b 渲染缓存值。
|
|
10
|
+
// 为字面无损:auto 且 rgb 非 0 时,把 rgb 旁挂进私有属性;读回时合并。OOXML 仍合法(w:val=auto)。
|
|
11
|
+
export const autoRgbAttr = (name, c) => {
|
|
12
|
+
const s = c && c.auto && (c.r || c.g || c.b) ? `${c.r},${c.g},${c.b}` : null;
|
|
13
|
+
return s ? ` ${name}="${s}"` : "";
|
|
14
|
+
};
|
|
15
|
+
export const mergeAutoRgb = (jc, rgbStr) => {
|
|
16
|
+
if (rgbStr && jc && jc.auto) { const p = String(rgbStr).split(",").map(Number); jc.r = p[0]; jc.g = p[1]; jc.b = p[2]; }
|
|
17
|
+
return jc;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
// highlight:ONLYOFFICE-JSON 用 RGB,OOXML 用 16 个命名色。RGB↔named(精确命中,否则最近邻)。
|
|
21
|
+
const HL_NAMED = { "000000": "black", "0000FF": "blue", "00FFFF": "cyan", "000080": "darkBlue", "008080": "darkCyan", "808080": "darkGray", "008000": "darkGreen", "800080": "darkMagenta", "800000": "darkRed", "808000": "darkYellow", "00FF00": "green", C0C0C0: "lightGray", FF00FF: "magenta", FF0000: "red", FFFFFF: "white", FFFF00: "yellow" };
|
|
22
|
+
const HL_HEX = Object.fromEntries(Object.entries(HL_NAMED).map(([h, n]) => [n, h]));
|
|
23
|
+
export function highlightToOoxml(c) {
|
|
24
|
+
if (!c || c.auto) return null;
|
|
25
|
+
const hex = colorToOoxml(c);
|
|
26
|
+
if (HL_NAMED[hex]) return HL_NAMED[hex];
|
|
27
|
+
let best = "none", bd = Infinity;
|
|
28
|
+
for (const [h, n] of Object.entries(HL_NAMED)) {
|
|
29
|
+
const dr = parseInt(h.slice(0, 2), 16) - c.r, dg = parseInt(h.slice(2, 4), 16) - c.g, db = parseInt(h.slice(4, 6), 16) - c.b;
|
|
30
|
+
const d = dr * dr + dg * dg + db * db;
|
|
31
|
+
if (d < bd) { bd = d; best = n; }
|
|
32
|
+
}
|
|
33
|
+
return best;
|
|
34
|
+
}
|
|
35
|
+
export function highlightToJson(named) {
|
|
36
|
+
if (!named || named === "none") return undefined;
|
|
37
|
+
return HL_HEX[named] ? { auto: false, ...colorToJson(HL_HEX[named]) } : undefined;
|
|
38
|
+
}
|
package/src/extra.mjs
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// 无损兜底层:把"未被忠实建模的剩余键"原样塞进私有属性 oo:x(JSON 文本,XML 转义),
|
|
2
|
+
// 读回时与模型解析结果合并。模型解析出的键覆盖兜底——反映 agent 对真实 OOXML 的编辑;
|
|
3
|
+
// 兜底补齐一切未建模/古怪默认键(drawing、inline、false toggle、空 tcBorders{} 等)。
|
|
4
|
+
// 这样"json→ooxml→json 不丢数据"是构造性保证,不依赖枚举了多少元素。
|
|
5
|
+
import { esc } from "./xml.mjs";
|
|
6
|
+
|
|
7
|
+
// 这些是读侧无条件重建的结构/常量键,永不进兜底。
|
|
8
|
+
const DROP = new Set(["type", "bFromDocument", "bPresentation"]);
|
|
9
|
+
|
|
10
|
+
/** 计算 obj 去掉 structural(递归解析负责) + 常量后的剩余键,拼成 ` name="..."`;空则 ""。
|
|
11
|
+
* name 默认 oo:x;同一元素需挂多份兜底时(如段落自身 + 其 endRun)用不同 name 区分。 */
|
|
12
|
+
export function extraAttr(obj, structural, name = "oo:x") {
|
|
13
|
+
if (!obj || typeof obj !== "object") return "";
|
|
14
|
+
const skip = structural ? new Set([...structural, ...DROP]) : DROP;
|
|
15
|
+
const left = {};
|
|
16
|
+
let has = false;
|
|
17
|
+
for (const k of Object.keys(obj)) {
|
|
18
|
+
if (skip.has(k)) continue;
|
|
19
|
+
left[k] = obj[k];
|
|
20
|
+
has = true;
|
|
21
|
+
}
|
|
22
|
+
return has ? ` ${name}="${esc(JSON.stringify(left))}"` : "";
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** 从节点的兜底属性(默认 oo:x)还原对象(解析器已反转义)。 */
|
|
26
|
+
export function readExtra(node, name = "oo:x") {
|
|
27
|
+
const s = node && node.attrs ? node.attrs[name] : null;
|
|
28
|
+
if (!s) return {};
|
|
29
|
+
try { return JSON.parse(s); } catch { return {}; }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** obj 是否只含 allowed 内的键(用于判断 run 内容项能否安全建模成真元素)。 */
|
|
33
|
+
export function onlyKeys(obj, allowed) {
|
|
34
|
+
const set = new Set(allowed);
|
|
35
|
+
for (const k of Object.keys(obj)) if (!set.has(k)) return false;
|
|
36
|
+
return true;
|
|
37
|
+
}
|
package/src/flat-opc.mjs
ADDED
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
// Tool 4: assemble oojson output (document.xml + package-level parts) into
|
|
2
|
+
// Flat OPC for SDK(@coco-vexel/coco-office) fromFlatOpc.
|
|
3
|
+
import { jsonToOoxml } from "./json-to-ooxml.mjs";
|
|
4
|
+
import { buildStylesXml } from "./styles-to-ooxml.mjs";
|
|
5
|
+
import { esc, attrStr } from "./xml.mjs";
|
|
6
|
+
|
|
7
|
+
const CT = {
|
|
8
|
+
document: "application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml",
|
|
9
|
+
styles: "application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml",
|
|
10
|
+
numbering: "application/vnd.openxmlformats-officedocument.wordprocessingml.numbering+xml",
|
|
11
|
+
theme: "application/vnd.openxmlformats-officedocument.theme+xml",
|
|
12
|
+
fontTable: "application/vnd.openxmlformats-officedocument.wordprocessingml.fontTable+xml",
|
|
13
|
+
settings: "application/vnd.openxmlformats-officedocument.wordprocessingml.settings+xml",
|
|
14
|
+
webSettings: "application/vnd.openxmlformats-officedocument.wordprocessingml.webSettings+xml",
|
|
15
|
+
rels: "application/vnd.openxmlformats-package.relationships+xml",
|
|
16
|
+
contentTypes: "application/vnd.openxmlformats-package.content-types+xml",
|
|
17
|
+
};
|
|
18
|
+
const REL = {
|
|
19
|
+
officeDocument: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument",
|
|
20
|
+
styles: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles",
|
|
21
|
+
numbering: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/numbering",
|
|
22
|
+
theme: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme",
|
|
23
|
+
fontTable: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/fontTable",
|
|
24
|
+
settings: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/settings",
|
|
25
|
+
webSettings: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/webSettings",
|
|
26
|
+
hyperlink: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink",
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
function pkgPart(name, contentType, innerXml) {
|
|
30
|
+
const inner = innerXml.replace(/^<\?xml[^>]*\?>/, "");
|
|
31
|
+
return (
|
|
32
|
+
`<pkg:part pkg:name="${esc(name)}" pkg:contentType="${esc(contentType)}">` +
|
|
33
|
+
`<pkg:xmlData>${inner}</pkg:xmlData></pkg:part>`
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function pkgBinaryPart(name, contentType, data) {
|
|
38
|
+
return (
|
|
39
|
+
`<pkg:part pkg:name="${esc(name)}" pkg:contentType="${esc(contentType)}" pkg:compression="store">` +
|
|
40
|
+
`<pkg:binaryData>${String(data).replace(/\s+/g, "")}</pkg:binaryData></pkg:part>`
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function relXml({ id, type, target, targetMode }) {
|
|
45
|
+
const mode = targetMode ? ` TargetMode="${esc(targetMode)}"` : "";
|
|
46
|
+
return `<Relationship Id="${esc(id)}" Type="${esc(type)}" Target="${esc(target)}"${mode}/>`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function relsPartNameForSource(source) {
|
|
50
|
+
const clean = String(source || "").replace(/^\//, "");
|
|
51
|
+
const slash = clean.lastIndexOf("/");
|
|
52
|
+
if (slash === -1) return `/_rels/${clean}.rels`;
|
|
53
|
+
return `/${clean.slice(0, slash)}/_rels/${clean.slice(slash + 1)}.rels`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function relationshipTargetForPart(source, targetPart) {
|
|
57
|
+
if (!targetPart.startsWith("/")) return targetPart;
|
|
58
|
+
const sourceDir = String(source || "").replace(/^\//, "").split("/").slice(0, -1);
|
|
59
|
+
const targetParts = targetPart.replace(/^\//, "").split("/");
|
|
60
|
+
while (sourceDir.length && targetParts[0] === sourceDir[0]) {
|
|
61
|
+
sourceDir.shift();
|
|
62
|
+
targetParts.shift();
|
|
63
|
+
}
|
|
64
|
+
return "../".repeat(sourceDir.length) + targetParts.join("/");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function normalizeExtraPart(part, fallbackIndex) {
|
|
68
|
+
if (!part || typeof part !== "object") return null;
|
|
69
|
+
const name = String(part.name ?? "").trim();
|
|
70
|
+
const contentType = String(part.contentType ?? "").trim();
|
|
71
|
+
if (!name || !contentType) return null;
|
|
72
|
+
return {
|
|
73
|
+
name: name.startsWith("/") ? name : `/${name}`,
|
|
74
|
+
contentType,
|
|
75
|
+
xml: part.xml,
|
|
76
|
+
binary: part.binary,
|
|
77
|
+
rel: part.rel ? {
|
|
78
|
+
source: part.rel.source ?? "/word/document.xml",
|
|
79
|
+
id: part.rel.id ?? `rIdExtra${fallbackIndex}`,
|
|
80
|
+
type: part.rel.type,
|
|
81
|
+
target: part.rel.target,
|
|
82
|
+
targetMode: part.rel.targetMode,
|
|
83
|
+
} : undefined,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function isExternalHyperlink(value) {
|
|
88
|
+
return /^(https?:|mailto:|ftp:)/i.test(String(value || ""));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function withDocumentHyperlinkRels(node, rels, state = { index: 1 }) {
|
|
92
|
+
if (Array.isArray(node)) return node.map((item) => withDocumentHyperlinkRels(item, rels, state));
|
|
93
|
+
if (!node || typeof node !== "object") return node;
|
|
94
|
+
const next = { ...node };
|
|
95
|
+
if (next.type === "hyperlink" && isExternalHyperlink(next.value) && !next.rId && !next.id && !next["r:id"]) {
|
|
96
|
+
const id = `rIdCocoHyperlink${state.index++}`;
|
|
97
|
+
next.rId = id;
|
|
98
|
+
rels.push(relXml({ id, type: REL.hyperlink, target: String(next.value), targetMode: "External" }));
|
|
99
|
+
}
|
|
100
|
+
if (Array.isArray(next.content)) next.content = withDocumentHyperlinkRels(next.content, rels, state);
|
|
101
|
+
if (next.content && typeof next.content === "object" && !Array.isArray(next.content)) next.content = withDocumentHyperlinkRels(next.content, rels, state);
|
|
102
|
+
return next;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function twoHex(n) {
|
|
106
|
+
const v = Number.isFinite(Number(n)) ? Math.max(0, Math.min(255, Number(n))) : 0;
|
|
107
|
+
return Math.round(v).toString(16).toUpperCase().padStart(2, "0");
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function rgbHex(rgba, fallback) {
|
|
111
|
+
if (!rgba || typeof rgba !== "object") return fallback;
|
|
112
|
+
return twoHex(rgba.red) + twoHex(rgba.green) + twoHex(rgba.blue);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function themeColorXml(slot, value, fallback) {
|
|
116
|
+
const color = value?.color || value;
|
|
117
|
+
if (color?.type === "sys") {
|
|
118
|
+
const lastClr = rgbHex(color.rgba, fallback.lastClr || fallback.val || "000000");
|
|
119
|
+
return `<a:${slot}><a:sysClr val="${esc(color.id || fallback.sys || "windowText")}" lastClr="${esc(lastClr)}"/></a:${slot}>`;
|
|
120
|
+
}
|
|
121
|
+
return `<a:${slot}><a:srgbClr val="${esc(rgbHex(color?.rgba, fallback.val))}"/></a:${slot}>`;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function fontName(theme, group, kind, fallback) {
|
|
125
|
+
return theme?.themeElements?.fontScheme?.[group]?.[kind] || fallback;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function fontSchemeXml(theme) {
|
|
129
|
+
const scheme = theme?.themeElements?.fontScheme;
|
|
130
|
+
const name = scheme?.name || "Office";
|
|
131
|
+
const majorLatin = fontName(theme, "majorFont", "latin", "Cambria");
|
|
132
|
+
const majorEa = fontName(theme, "majorFont", "ea", "宋体");
|
|
133
|
+
const majorCs = fontName(theme, "majorFont", "cs", "Times New Roman");
|
|
134
|
+
const minorLatin = fontName(theme, "minorFont", "latin", "Calibri");
|
|
135
|
+
const minorEa = fontName(theme, "minorFont", "ea", "宋体");
|
|
136
|
+
const minorCs = fontName(theme, "minorFont", "cs", "Arial");
|
|
137
|
+
return (
|
|
138
|
+
`<a:fontScheme name="${esc(name)}">` +
|
|
139
|
+
`<a:majorFont><a:latin typeface="${esc(majorLatin)}"/><a:ea typeface="${esc(majorEa)}"/><a:cs typeface="${esc(majorCs)}"/></a:majorFont>` +
|
|
140
|
+
`<a:minorFont><a:latin typeface="${esc(minorLatin)}"/><a:ea typeface="${esc(minorEa)}"/><a:cs typeface="${esc(minorCs)}"/></a:minorFont>` +
|
|
141
|
+
`</a:fontScheme>`
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function solidFillXml(fill, fallback = "phClr") {
|
|
146
|
+
const solid = fill?.solidFill ?? fill;
|
|
147
|
+
const color = solid?.schemeClr ?? solid?.schemeColor ?? solid?.color;
|
|
148
|
+
if (typeof color === "string") return `<a:solidFill><a:schemeClr val="${esc(color)}"/></a:solidFill>`;
|
|
149
|
+
if (color?.val || color?.id) return `<a:solidFill><a:schemeClr val="${esc(color.val || color.id)}"/></a:solidFill>`;
|
|
150
|
+
const srgb = solid?.srgbClr ?? solid?.rgb ?? rgbHex(color?.rgba, null);
|
|
151
|
+
if (srgb) return `<a:solidFill><a:srgbClr val="${esc(String(srgb).replace(/^#/, "").toUpperCase())}"/></a:solidFill>`;
|
|
152
|
+
return `<a:solidFill><a:schemeClr val="${esc(fallback)}"/></a:solidFill>`;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function colorChoiceXml(value, fallback = "phClr") {
|
|
156
|
+
const color = value?.schemeClr ?? value?.schemeColor ?? value?.color ?? value;
|
|
157
|
+
if (typeof color === "string") return `<a:schemeClr val="${esc(color)}"/>`;
|
|
158
|
+
if (color?.val || color?.id) return `<a:schemeClr val="${esc(color.val || color.id)}"/>`;
|
|
159
|
+
const srgb = value?.srgbClr ?? value?.rgb ?? rgbHex(color?.rgba ?? value?.rgba, null);
|
|
160
|
+
if (srgb) return `<a:srgbClr val="${esc(String(srgb).replace(/^#/, "").toUpperCase())}"/>`;
|
|
161
|
+
return `<a:schemeClr val="${esc(fallback)}"/>`;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function shadowXml(shadow) {
|
|
165
|
+
if (!shadow || typeof shadow !== "object") return "";
|
|
166
|
+
const attrs = {};
|
|
167
|
+
for (const key of ["blurRad", "dist", "dir", "sx", "sy", "kx", "ky", "algn", "rotWithShape"]) if (shadow[key] != null) attrs[key] = shadow[key];
|
|
168
|
+
return `<a:outerShdw${attrStr(attrs)}>${colorChoiceXml(shadow.solidFill ?? shadow.fill ?? shadow.color)}</a:outerShdw>`;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function effectStyleListXml(list) {
|
|
172
|
+
const items = Array.isArray(list) ? list : list ? [list] : [];
|
|
173
|
+
const styles = (items.length ? items : [{ effectLst: {} }]).map((style) => {
|
|
174
|
+
const effectLst = style.effectLst ?? style.effects ?? style;
|
|
175
|
+
const outer = effectLst.outerShdw ?? effectLst.outerShadow;
|
|
176
|
+
const effects = shadowXml(outer);
|
|
177
|
+
return `<a:effectStyle><a:effectLst>${effects}</a:effectLst></a:effectStyle>`;
|
|
178
|
+
}).join("");
|
|
179
|
+
return `<a:effectStyleLst>${styles}</a:effectStyleLst>`;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function fillStyleListXml(tag, list) {
|
|
183
|
+
const items = Array.isArray(list) ? list : list ? [list] : [];
|
|
184
|
+
const fills = (items.length ? items : [{ solidFill: { schemeClr: "phClr" } }]).map((fill) => solidFillXml(fill)).join("");
|
|
185
|
+
return `<a:${tag}>${fills}</a:${tag}>`;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function lineStyleListXml(list) {
|
|
189
|
+
const items = Array.isArray(list) ? list : list ? [list] : [];
|
|
190
|
+
const lines = (items.length ? items : [{ w: 9525, cap: "flat", cmpd: "sng", algn: "ctr", solidFill: { schemeClr: "phClr" } }]).map((line) => {
|
|
191
|
+
const attrs = { w: line.w ?? line.width ?? 9525, cap: line.cap ?? "flat", cmpd: line.cmpd ?? "sng", algn: line.algn ?? line.align ?? "ctr" };
|
|
192
|
+
return `<a:ln${attrStr(attrs)}>${solidFillXml(line.solidFill ? line : line.fill ?? line)}</a:ln>`;
|
|
193
|
+
}).join("");
|
|
194
|
+
return `<a:lnStyleLst>${lines}</a:lnStyleLst>`;
|
|
195
|
+
}
|
|
196
|
+
function fmtSchemeXml(theme) {
|
|
197
|
+
const fmt = theme?.themeElements?.fmtScheme;
|
|
198
|
+
const name = fmt?.name || "Office";
|
|
199
|
+
return (
|
|
200
|
+
`<a:fmtScheme name="${esc(name)}">` +
|
|
201
|
+
fillStyleListXml("fillStyleLst", fmt?.fillStyleLst ?? fmt?.fillStyles) +
|
|
202
|
+
lineStyleListXml(fmt?.lnStyleLst ?? fmt?.lineStyleLst ?? fmt?.lineStyles) +
|
|
203
|
+
effectStyleListXml(fmt?.effectStyleLst ?? fmt?.effectStyles) +
|
|
204
|
+
fillStyleListXml("bgFillStyleLst", fmt?.bgFillStyleLst ?? fmt?.bgFillStyles) +
|
|
205
|
+
`</a:fmtScheme>`
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
export function buildThemeXml(theme) {
|
|
209
|
+
const clr = theme?.themeElements?.clrScheme;
|
|
210
|
+
const name = theme?.name || "Office Theme";
|
|
211
|
+
const clrName = clr?.name || "Office";
|
|
212
|
+
return (
|
|
213
|
+
`<?xml version="1.0" encoding="UTF-8" standalone="yes"?>` +
|
|
214
|
+
`<a:theme xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" name="${esc(name)}">` +
|
|
215
|
+
`<a:themeElements>` +
|
|
216
|
+
`<a:clrScheme name="${esc(clrName)}">` +
|
|
217
|
+
themeColorXml("dk1", clr?.dk1, { sys: "windowText", lastClr: "000000" }) +
|
|
218
|
+
themeColorXml("lt1", clr?.lt1, { sys: "window", lastClr: "FFFFFF" }) +
|
|
219
|
+
themeColorXml("dk2", clr?.dk2, { val: "1F497D" }) +
|
|
220
|
+
themeColorXml("lt2", clr?.lt2, { val: "EEECE1" }) +
|
|
221
|
+
themeColorXml("accent1", clr?.accent1, { val: "4F81BD" }) +
|
|
222
|
+
themeColorXml("accent2", clr?.accent2, { val: "C0504D" }) +
|
|
223
|
+
themeColorXml("accent3", clr?.accent3, { val: "9BBB59" }) +
|
|
224
|
+
themeColorXml("accent4", clr?.accent4, { val: "8064A2" }) +
|
|
225
|
+
themeColorXml("accent5", clr?.accent5, { val: "4BACC6" }) +
|
|
226
|
+
themeColorXml("accent6", clr?.accent6, { val: "F79646" }) +
|
|
227
|
+
themeColorXml("hlink", clr?.hlink, { val: "0000FF" }) +
|
|
228
|
+
themeColorXml("folHlink", clr?.folHlink, { val: "800080" }) +
|
|
229
|
+
`</a:clrScheme>` +
|
|
230
|
+
fontSchemeXml(theme) +
|
|
231
|
+
fmtSchemeXml(theme) +
|
|
232
|
+
`</a:themeElements></a:theme>`
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export function buildFontTableXml(theme) {
|
|
237
|
+
const fonts = new Set([
|
|
238
|
+
"Calibri",
|
|
239
|
+
"Cambria",
|
|
240
|
+
"Arial",
|
|
241
|
+
"Times New Roman",
|
|
242
|
+
"宋体",
|
|
243
|
+
fontName(theme, "majorFont", "latin", null),
|
|
244
|
+
fontName(theme, "majorFont", "ea", null),
|
|
245
|
+
fontName(theme, "majorFont", "cs", null),
|
|
246
|
+
fontName(theme, "minorFont", "latin", null),
|
|
247
|
+
fontName(theme, "minorFont", "ea", null),
|
|
248
|
+
fontName(theme, "minorFont", "cs", null),
|
|
249
|
+
].filter(Boolean));
|
|
250
|
+
const entries = [...fonts].map((name) => {
|
|
251
|
+
const charset = /[\u3400-\u9fff]/.test(name) ? "86" : "00";
|
|
252
|
+
const family = /Times|Cambria|宋体/i.test(name) ? "roman" : "swiss";
|
|
253
|
+
return `<w:font w:name="${esc(name)}"><w:charset w:val="${charset}"/><w:family w:val="${family}"/></w:font>`;
|
|
254
|
+
}).join("");
|
|
255
|
+
return (
|
|
256
|
+
`<?xml version="1.0" encoding="UTF-8" standalone="yes"?>` +
|
|
257
|
+
`<w:fonts xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">` +
|
|
258
|
+
entries +
|
|
259
|
+
`</w:fonts>`
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
export function buildSettingsXml() {
|
|
263
|
+
return (
|
|
264
|
+
`<?xml version="1.0" encoding="UTF-8" standalone="yes"?>` +
|
|
265
|
+
`<w:settings xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">` +
|
|
266
|
+
`<w:zoom w:percent="100"/><w:defaultTabStop w:val="720"/>` +
|
|
267
|
+
`<w:compat><w:compatSetting w:name="compatibilityMode" w:uri="http://schemas.microsoft.com/office/word" w:val="15"/></w:compat>` +
|
|
268
|
+
`</w:settings>`
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export function buildWebSettingsXml() {
|
|
273
|
+
return (
|
|
274
|
+
`<?xml version="1.0" encoding="UTF-8" standalone="yes"?>` +
|
|
275
|
+
`<w:webSettings xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">` +
|
|
276
|
+
`<w:optimizeForBrowser/>` +
|
|
277
|
+
`</w:webSettings>`
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
export function buildDefaultWordParts(ooJsonDoc) {
|
|
282
|
+
const theme = ooJsonDoc?.theme;
|
|
283
|
+
return [
|
|
284
|
+
{ name: "/word/theme/theme1.xml", contentType: CT.theme, xml: buildThemeXml(theme), rel: { type: REL.theme, target: "theme/theme1.xml" } },
|
|
285
|
+
{ name: "/word/fontTable.xml", contentType: CT.fontTable, xml: buildFontTableXml(theme), rel: { type: REL.fontTable, target: "fontTable.xml" } },
|
|
286
|
+
{ name: "/word/settings.xml", contentType: CT.settings, xml: buildSettingsXml(), rel: { type: REL.settings, target: "settings.xml" } },
|
|
287
|
+
{ name: "/word/webSettings.xml", contentType: CT.webSettings, xml: buildWebSettingsXml(), rel: { type: REL.webSettings, target: "webSettings.xml" } },
|
|
288
|
+
];
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* oojson output -> Flat OPC package string (SDK fromFlatOpc can consume it).
|
|
293
|
+
*
|
|
294
|
+
* @param {object} ooJsonDoc ONLYOFFICE-JSON document object (ToJSON output)
|
|
295
|
+
* @param {object} [opts]
|
|
296
|
+
* @param {string} [opts.stylesXml] Built styles.xml.
|
|
297
|
+
* @param {string} [opts.numberingXml] Built numbering.xml.
|
|
298
|
+
* @param {boolean} [opts.includeDefaultParts=true] Add theme/fontTable/settings/webSettings.
|
|
299
|
+
* @param {Array} [opts.extraParts] Extra package parts for headers/footers/comments/media and future long tails.
|
|
300
|
+
* @returns {string} Flat OPC package
|
|
301
|
+
*/
|
|
302
|
+
export function jsonToFlatOpc(ooJsonDoc, opts = {}) {
|
|
303
|
+
const generatedDocRels = [];
|
|
304
|
+
const docForXml = withDocumentHyperlinkRels(ooJsonDoc, generatedDocRels);
|
|
305
|
+
const documentXml = jsonToOoxml(docForXml);
|
|
306
|
+
const { stylesXml, numberingXml } = opts;
|
|
307
|
+
const includeDefaultParts = opts.includeDefaultParts !== false;
|
|
308
|
+
const extraParts = [
|
|
309
|
+
...(includeDefaultParts ? buildDefaultWordParts(ooJsonDoc) : []),
|
|
310
|
+
...(opts.extraParts ?? []),
|
|
311
|
+
].map((part, i) => normalizeExtraPart(part, i + 1)).filter(Boolean);
|
|
312
|
+
|
|
313
|
+
const overrides = [
|
|
314
|
+
`<Override PartName="/word/document.xml" ContentType="${CT.document}"/>`,
|
|
315
|
+
];
|
|
316
|
+
if (stylesXml) overrides.push(`<Override PartName="/word/styles.xml" ContentType="${CT.styles}"/>`);
|
|
317
|
+
if (numberingXml) overrides.push(`<Override PartName="/word/numbering.xml" ContentType="${CT.numbering}"/>`);
|
|
318
|
+
for (const part of extraParts) {
|
|
319
|
+
overrides.push(`<Override PartName="${esc(part.name)}" ContentType="${esc(part.contentType)}"/>`);
|
|
320
|
+
}
|
|
321
|
+
const contentTypesXml =
|
|
322
|
+
`<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">` +
|
|
323
|
+
`<Default Extension="rels" ContentType="${CT.rels}"/>` +
|
|
324
|
+
`<Default Extension="xml" ContentType="application/xml"/>` +
|
|
325
|
+
overrides.join("") +
|
|
326
|
+
`</Types>`;
|
|
327
|
+
|
|
328
|
+
const rootRelsXml =
|
|
329
|
+
`<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">` +
|
|
330
|
+
`<Relationship Id="rId1" Type="${REL.officeDocument}" Target="word/document.xml"/>` +
|
|
331
|
+
`</Relationships>`;
|
|
332
|
+
|
|
333
|
+
const docRels = [];
|
|
334
|
+
let rid = 1;
|
|
335
|
+
if (stylesXml) docRels.push(`<Relationship Id="rId${rid++}" Type="${REL.styles}" Target="styles.xml"/>`);
|
|
336
|
+
if (numberingXml) docRels.push(`<Relationship Id="rId${rid++}" Type="${REL.numbering}" Target="numbering.xml"/>`);
|
|
337
|
+
docRels.push(...generatedDocRels);
|
|
338
|
+
const relGroups = new Map();
|
|
339
|
+
for (const part of extraParts) {
|
|
340
|
+
if (!part.rel || !part.rel.type) continue;
|
|
341
|
+
const source = part.rel.source || "/word/document.xml";
|
|
342
|
+
const target = part.rel.target ?? relationshipTargetForPart(source, part.name);
|
|
343
|
+
const entry = relXml({ id: part.rel.id ?? `rId${rid}`, type: part.rel.type, target, targetMode: part.rel.targetMode });
|
|
344
|
+
if (source === "/word/document.xml") {
|
|
345
|
+
docRels.push(entry);
|
|
346
|
+
rid++;
|
|
347
|
+
} else {
|
|
348
|
+
const list = relGroups.get(source) ?? [];
|
|
349
|
+
list.push(entry);
|
|
350
|
+
relGroups.set(source, list);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
const docRelsXml =
|
|
354
|
+
`<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">` +
|
|
355
|
+
docRels.join("") +
|
|
356
|
+
`</Relationships>`;
|
|
357
|
+
|
|
358
|
+
const parts = [
|
|
359
|
+
pkgPart("/[Content_Types].xml", CT.contentTypes, contentTypesXml),
|
|
360
|
+
pkgPart("/_rels/.rels", CT.rels, rootRelsXml),
|
|
361
|
+
pkgPart("/word/_rels/document.xml.rels", CT.rels, docRelsXml),
|
|
362
|
+
];
|
|
363
|
+
for (const [source, rels] of relGroups.entries()) {
|
|
364
|
+
const relsXml =
|
|
365
|
+
`<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">` +
|
|
366
|
+
rels.join("") +
|
|
367
|
+
`</Relationships>`;
|
|
368
|
+
parts.push(pkgPart(relsPartNameForSource(source), CT.rels, relsXml));
|
|
369
|
+
}
|
|
370
|
+
if (stylesXml) parts.push(pkgPart("/word/styles.xml", CT.styles, stylesXml));
|
|
371
|
+
if (numberingXml) parts.push(pkgPart("/word/numbering.xml", CT.numbering, numberingXml));
|
|
372
|
+
for (const part of extraParts) {
|
|
373
|
+
if (part.xml != null) parts.push(pkgPart(part.name, part.contentType, String(part.xml)));
|
|
374
|
+
else if (part.binary != null) parts.push(pkgBinaryPart(part.name, part.contentType, part.binary));
|
|
375
|
+
}
|
|
376
|
+
parts.push(pkgPart("/word/document.xml", CT.document, documentXml));
|
|
377
|
+
|
|
378
|
+
return (
|
|
379
|
+
`<?xml version="1.0" encoding="UTF-8" standalone="yes"?>` +
|
|
380
|
+
`<?mso-application progid="Word.Document"?>` +
|
|
381
|
+
`<pkg:package xmlns:pkg="http://schemas.microsoft.com/office/2006/xmlPackage">` +
|
|
382
|
+
parts.join("") +
|
|
383
|
+
`</pkg:package>`
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Convenience wrapper: ONLYOFFICE-JSON document + collected style definitions -> Flat OPC package.
|
|
389
|
+
* styleDefs shape is documented in styles-to-ooxml.buildStylesXml.
|
|
390
|
+
*/
|
|
391
|
+
export function jsonToFlatOpcWithStyles(ooJsonDoc, styleDefs, docDefaults, numberingXml, opts = {}) {
|
|
392
|
+
const stylesXml = styleDefs && styleDefs.length ? buildStylesXml(styleDefs, docDefaults) : undefined;
|
|
393
|
+
return jsonToFlatOpc(ooJsonDoc, { ...opts, stylesXml, numberingXml });
|
|
394
|
+
}
|
package/src/index.mjs
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// @coco-vexel/coco-office-oojson —— ONLYOFFICE-JSON ↔ OOXML(WordprocessingML body) 双向转换器,公共入口。
|
|
2
|
+
//
|
|
3
|
+
// 管道里的位置(coco-onlyoffice 宿主,复用 coco-office 的 OOXML 能力):
|
|
4
|
+
// 编辑器 ToJSON → ONLYOFFICE-JSON ──工具① jsonToOoxml──▶ OOXML → agent 改
|
|
5
|
+
// agent 改完的 OOXML ──工具② ooxmlToJson──▶ ONLYOFFICE-JSON → FromJSON 整篇灌回编辑器
|
|
6
|
+
//
|
|
7
|
+
// 无损保证:json→ooxml→json 在 484 篇真实文档上裸回环(零归一)零差异。
|
|
8
|
+
// 原理见 ./extra.mjs 与 README.md(双层:忠实建模 + 私有 oo:x 兜底)。
|
|
9
|
+
|
|
10
|
+
// 工具①:ONLYOFFICE-JSON → OOXML(document.xml)
|
|
11
|
+
export { jsonToOoxml } from "./json-to-ooxml.mjs";
|
|
12
|
+
// 工具②:OOXML → ONLYOFFICE-JSON(+ Gaps 缺口追踪)
|
|
13
|
+
export { ooxmlToJson, Gaps } from "./ooxml-to-json.mjs";
|
|
14
|
+
// 工具③:ONLYOFFICE 样式定义集 → styles.xml(重建 styleId 定义,让 core 判标题层级)
|
|
15
|
+
export { buildStylesXml, styleToXml, ooOutlineToOoxml } from "./styles-to-ooxml.mjs";
|
|
16
|
+
export { buildNumberingXml } from "./numbering-to-ooxml.mjs";
|
|
17
|
+
// 工具④:document.xml [+ styles/numbering] → Flat OPC 整包(喂 SDK fromFlatOpc)
|
|
18
|
+
export { buildDefaultWordParts, buildFontTableXml, buildSettingsXml, buildThemeXml, buildWebSettingsXml, jsonToFlatOpc, jsonToFlatOpcWithStyles } from "./flat-opc.mjs";
|
|
19
|
+
export { withPackageLongTailParts } from "./long-tail-parts.mjs";
|
|
20
|
+
export { flatOpcToOnlyOfficeWriteback } from "./writeback.mjs";
|
|
21
|
+
export {
|
|
22
|
+
applyPptTextMutationToOnlyOfficeContent,
|
|
23
|
+
onlyOfficePptContentToTextBody,
|
|
24
|
+
replaceOnlyOfficePptContentText,
|
|
25
|
+
setOnlyOfficePptContentParagraphs,
|
|
26
|
+
setOnlyOfficePptContentRunStyle,
|
|
27
|
+
setOnlyOfficePptContentStyle,
|
|
28
|
+
setOnlyOfficePptContentText,
|
|
29
|
+
} from "./ppt-content.mjs";
|
|
30
|
+
export {
|
|
31
|
+
applyPptModelToOnlyOfficePptDeck,
|
|
32
|
+
onlyOfficePptDeckToPptModel,
|
|
33
|
+
updateOnlyOfficePptDeckTextShape,
|
|
34
|
+
} from "./ppt-deck.mjs";
|
|
35
|
+
|
|
36
|
+
// 进阶/复用:底层件
|
|
37
|
+
export { parseXml, kid, kidsOf, esc, unesc, attrStr } from "./xml.mjs";
|
|
38
|
+
export { colorToOoxml, colorToJson, highlightToOoxml, highlightToJson } from "./color.mjs";
|
|
39
|
+
export { extraAttr, readExtra, onlyKeys } from "./extra.mjs";
|