@hirokisakabe/pom 8.4.0 → 8.5.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/dist/buildPptx.d.ts.map +1 -1
- package/dist/buildPptx.js +3 -1
- package/dist/buildPptx.js.map +1 -1
- package/dist/diagnostics.d.ts +1 -1
- package/dist/diagnostics.d.ts.map +1 -1
- package/dist/diagnostics.js.map +1 -1
- package/dist/validatePositioned/validatePositioned.js +92 -0
- package/dist/validatePositioned/validatePositioned.js.map +1 -0
- package/package.json +1 -1
package/dist/buildPptx.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"buildPptx.d.ts","names":[],"sources":["../src/buildPptx.ts"],"mappings":";;;;;
|
|
1
|
+
{"version":3,"file":"buildPptx.d.ts","names":[],"sources":["../src/buildPptx.ts"],"mappings":";;;;;UAmBiB,eAAA;EACf,IAAA,sBAA0B,OAAA;EAC1B,WAAA,EAAa,UAAU;AAAA;AAAA,iBAGH,SAAA,CACpB,GAAA,UACA,SAAA;EAAa,CAAA;EAAW,CAAA;AAAA,GACxB,OAAA;EACE,MAAA,GAAS,kBAAA;EACT,UAAA,GAAa,WAAA,GAAc,UAAA;EAC3B,eAAA,GAAkB,mBAAA;EAClB,OAAA;EACA,MAAA;AAAA,IAED,OAAA,CAAQ,eAAA"}
|
package/dist/buildPptx.js
CHANGED
|
@@ -9,6 +9,7 @@ import { parseMasterPptx } from "./parseMasterPptx.js";
|
|
|
9
9
|
import { parseXml } from "./parseXml/parseXml.js";
|
|
10
10
|
import { renderPptx } from "./renderPptx/renderPptx.js";
|
|
11
11
|
import { toPositioned } from "./toPositioned/toPositioned.js";
|
|
12
|
+
import { validatePositioned } from "./validatePositioned/validatePositioned.js";
|
|
12
13
|
//#region src/buildPptx.ts
|
|
13
14
|
async function buildPptx(xml, slideSize, options) {
|
|
14
15
|
const ctx = createBuildContext(options?.textMeasurement ?? "auto");
|
|
@@ -16,12 +17,13 @@ async function buildPptx(xml, slideSize, options) {
|
|
|
16
17
|
if (options?.master) ctx.gradientFills.reserveColors(JSON.stringify(options.master));
|
|
17
18
|
const nodes = parseXml(xml);
|
|
18
19
|
const positionedPages = [];
|
|
19
|
-
for (const node of nodes) {
|
|
20
|
+
for (const [slideIndex, node] of nodes.entries()) {
|
|
20
21
|
let map;
|
|
21
22
|
try {
|
|
22
23
|
if (options?.autoFit !== false) map = await autoFitSlide(node, slideSize, ctx);
|
|
23
24
|
else map = await calcYogaLayout(node, slideSize, ctx);
|
|
24
25
|
const positioned = await toPositioned(node, ctx, extractLayoutResults(map));
|
|
26
|
+
validatePositioned(positioned, slideSize, ctx, slideIndex);
|
|
25
27
|
positionedPages.push(positioned);
|
|
26
28
|
} finally {
|
|
27
29
|
if (map) freeYogaTree(map);
|
package/dist/buildPptx.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"buildPptx.js","names":[],"sources":["../src/buildPptx.ts"],"sourcesContent":["import { autoFitSlide } from \"./autoFit/autoFit.ts\";\nimport { createBuildContext } from \"./buildContext.ts\";\nimport { calcYogaLayout } from \"./calcYogaLayout/calcYogaLayout.ts\";\nimport type { TextMeasurementMode } from \"./calcYogaLayout/measureText.ts\";\nimport type { YogaNodeMap } from \"./calcYogaLayout/types.ts\";\nimport { extractLayoutResults } from \"./calcYogaLayout/types.ts\";\nimport type { Diagnostic } from \"./diagnostics.ts\";\nimport { DiagnosticsError } from \"./diagnostics.ts\";\nimport { parseMasterPptx } from \"./parseMasterPptx.ts\";\nimport { parseXml } from \"./parseXml/parseXml.ts\";\nimport { patchPptxWriteForGradientFills } from \"./renderPptx/gradientFills.ts\";\nimport { renderPptx } from \"./renderPptx/renderPptx.ts\";\nimport { freeYogaTree } from \"./shared/freeYogaTree.ts\";\nimport { toPositioned } from \"./toPositioned/toPositioned.ts\";\nimport { PositionedNode, SlideMasterOptions } from \"./types.ts\";\n\nexport type { TextMeasurementMode };\n\nexport interface BuildPptxResult {\n pptx: import(\"pptxgenjs\").default;\n diagnostics: Diagnostic[];\n}\n\nexport async function buildPptx(\n xml: string,\n slideSize: { w: number; h: number },\n options?: {\n master?: SlideMasterOptions;\n masterPptx?: ArrayBuffer | Uint8Array;\n textMeasurement?: TextMeasurementMode;\n autoFit?: boolean;\n strict?: boolean;\n },\n): Promise<BuildPptxResult> {\n const ctx = createBuildContext(options?.textMeasurement ?? \"auto\");\n\n // グラデーション後処理のマーカー色がユーザー指定色と衝突しないよう、\n // 入力 XML / master オプション中に現れる色を予約しておく\n ctx.gradientFills.reserveColors(xml);\n if (options?.master) {\n ctx.gradientFills.reserveColors(JSON.stringify(options.master));\n }\n\n const nodes = parseXml(xml);\n const positionedPages: PositionedNode[] = [];\n\n for (const node of nodes) {\n let map: YogaNodeMap | undefined;\n try {\n if (options?.autoFit !== false) {\n map = await autoFitSlide(node, slideSize, ctx);\n } else {\n map = await calcYogaLayout(node, slideSize, ctx);\n }\n const layoutMap = extractLayoutResults(map);\n const positioned = await toPositioned(node, ctx, layoutMap);\n positionedPages.push(positioned);\n } finally {\n if (map) freeYogaTree(map);\n }\n }\n\n // masterPptx から背景を抽出し、master オプションにマージ\n let master = options?.master;\n if (options?.masterPptx) {\n try {\n const bg = await parseMasterPptx(options.masterPptx);\n if (bg) {\n if (master) {\n // 明示的に background が指定されていない場合のみ、masterPptx の背景を使用\n if (!master.background) {\n master = { ...master, background: bg };\n }\n } else {\n master = { background: bg };\n }\n }\n } catch (e) {\n const message =\n e instanceof Error ? e.message : \"Unknown error parsing masterPptx\";\n ctx.diagnostics.add(\"MASTER_PPTX_PARSE_FAILED\", message);\n }\n }\n\n const pptx = await renderPptx(positionedPages, slideSize, ctx, master);\n\n // backgroundGradient 使用時は write/writeFile に gradFill 置換の後処理を仕込む\n patchPptxWriteForGradientFills(pptx, ctx.gradientFills);\n\n const diagnostics = ctx.diagnostics.items;\n\n if (options?.strict && diagnostics.length > 0) {\n throw new DiagnosticsError(diagnostics);\n }\n\n return { pptx, diagnostics };\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"buildPptx.js","names":[],"sources":["../src/buildPptx.ts"],"sourcesContent":["import { autoFitSlide } from \"./autoFit/autoFit.ts\";\nimport { createBuildContext } from \"./buildContext.ts\";\nimport { calcYogaLayout } from \"./calcYogaLayout/calcYogaLayout.ts\";\nimport type { TextMeasurementMode } from \"./calcYogaLayout/measureText.ts\";\nimport type { YogaNodeMap } from \"./calcYogaLayout/types.ts\";\nimport { extractLayoutResults } from \"./calcYogaLayout/types.ts\";\nimport type { Diagnostic } from \"./diagnostics.ts\";\nimport { DiagnosticsError } from \"./diagnostics.ts\";\nimport { parseMasterPptx } from \"./parseMasterPptx.ts\";\nimport { parseXml } from \"./parseXml/parseXml.ts\";\nimport { patchPptxWriteForGradientFills } from \"./renderPptx/gradientFills.ts\";\nimport { renderPptx } from \"./renderPptx/renderPptx.ts\";\nimport { freeYogaTree } from \"./shared/freeYogaTree.ts\";\nimport { toPositioned } from \"./toPositioned/toPositioned.ts\";\nimport { PositionedNode, SlideMasterOptions } from \"./types.ts\";\nimport { validatePositioned } from \"./validatePositioned/validatePositioned.ts\";\n\nexport type { TextMeasurementMode };\n\nexport interface BuildPptxResult {\n pptx: import(\"pptxgenjs\").default;\n diagnostics: Diagnostic[];\n}\n\nexport async function buildPptx(\n xml: string,\n slideSize: { w: number; h: number },\n options?: {\n master?: SlideMasterOptions;\n masterPptx?: ArrayBuffer | Uint8Array;\n textMeasurement?: TextMeasurementMode;\n autoFit?: boolean;\n strict?: boolean;\n },\n): Promise<BuildPptxResult> {\n const ctx = createBuildContext(options?.textMeasurement ?? \"auto\");\n\n // グラデーション後処理のマーカー色がユーザー指定色と衝突しないよう、\n // 入力 XML / master オプション中に現れる色を予約しておく\n ctx.gradientFills.reserveColors(xml);\n if (options?.master) {\n ctx.gradientFills.reserveColors(JSON.stringify(options.master));\n }\n\n const nodes = parseXml(xml);\n const positionedPages: PositionedNode[] = [];\n\n for (const [slideIndex, node] of nodes.entries()) {\n let map: YogaNodeMap | undefined;\n try {\n if (options?.autoFit !== false) {\n map = await autoFitSlide(node, slideSize, ctx);\n } else {\n map = await calcYogaLayout(node, slideSize, ctx);\n }\n const layoutMap = extractLayoutResults(map);\n const positioned = await toPositioned(node, ctx, layoutMap);\n validatePositioned(positioned, slideSize, ctx, slideIndex);\n positionedPages.push(positioned);\n } finally {\n if (map) freeYogaTree(map);\n }\n }\n\n // masterPptx から背景を抽出し、master オプションにマージ\n let master = options?.master;\n if (options?.masterPptx) {\n try {\n const bg = await parseMasterPptx(options.masterPptx);\n if (bg) {\n if (master) {\n // 明示的に background が指定されていない場合のみ、masterPptx の背景を使用\n if (!master.background) {\n master = { ...master, background: bg };\n }\n } else {\n master = { background: bg };\n }\n }\n } catch (e) {\n const message =\n e instanceof Error ? e.message : \"Unknown error parsing masterPptx\";\n ctx.diagnostics.add(\"MASTER_PPTX_PARSE_FAILED\", message);\n }\n }\n\n const pptx = await renderPptx(positionedPages, slideSize, ctx, master);\n\n // backgroundGradient 使用時は write/writeFile に gradFill 置換の後処理を仕込む\n patchPptxWriteForGradientFills(pptx, ctx.gradientFills);\n\n const diagnostics = ctx.diagnostics.items;\n\n if (options?.strict && diagnostics.length > 0) {\n throw new DiagnosticsError(diagnostics);\n }\n\n return { pptx, diagnostics };\n}\n"],"mappings":";;;;;;;;;;;;;AAwBA,eAAsB,UACpB,KACA,WACA,SAO0B;CAC1B,MAAM,MAAM,mBAAmB,SAAS,mBAAmB,MAAM;CAIjE,IAAI,cAAc,cAAc,GAAG;CACnC,IAAI,SAAS,QACX,IAAI,cAAc,cAAc,KAAK,UAAU,QAAQ,MAAM,CAAC;CAGhE,MAAM,QAAQ,SAAS,GAAG;CAC1B,MAAM,kBAAoC,CAAC;CAE3C,KAAK,MAAM,CAAC,YAAY,SAAS,MAAM,QAAQ,GAAG;EAChD,IAAI;EACJ,IAAI;GACF,IAAI,SAAS,YAAY,OACvB,MAAM,MAAM,aAAa,MAAM,WAAW,GAAG;QAE7C,MAAM,MAAM,eAAe,MAAM,WAAW,GAAG;GAGjD,MAAM,aAAa,MAAM,aAAa,MAAM,KAD1B,qBAAqB,GACkB,CAAC;GAC1D,mBAAmB,YAAY,WAAW,KAAK,UAAU;GACzD,gBAAgB,KAAK,UAAU;EACjC,UAAU;GACR,IAAI,KAAK,aAAa,GAAG;EAC3B;CACF;CAGA,IAAI,SAAS,SAAS;CACtB,IAAI,SAAS,YACX,IAAI;EACF,MAAM,KAAK,MAAM,gBAAgB,QAAQ,UAAU;EACnD,IAAI,IACF,IAAI;OAEE,CAAC,OAAO,YACV,SAAS;IAAE,GAAG;IAAQ,YAAY;GAAG;EAAA,OAGvC,SAAS,EAAE,YAAY,GAAG;CAGhC,SAAS,GAAG;EACV,MAAM,UACJ,aAAa,QAAQ,EAAE,UAAU;EACnC,IAAI,YAAY,IAAI,4BAA4B,OAAO;CACzD;CAGF,MAAM,OAAO,MAAM,WAAW,iBAAiB,WAAW,KAAK,MAAM;CAGrE,+BAA+B,MAAM,IAAI,aAAa;CAEtD,MAAM,cAAc,IAAI,YAAY;CAEpC,IAAI,SAAS,UAAU,YAAY,SAAS,GAC1C,MAAM,IAAI,iBAAiB,WAAW;CAGxC,OAAO;EAAE;EAAM;CAAY;AAC7B"}
|
package/dist/diagnostics.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
//#region src/diagnostics.d.ts
|
|
2
|
-
type DiagnosticCode = "IMAGE_MEASURE_FAILED" | "IMAGE_NOT_PREFETCHED" | "AUTOFIT_OVERFLOW" | "SCALE_BELOW_THRESHOLD" | "MASTER_PPTX_PARSE_FAILED" | "ARROW_REF_NOT_FOUND" | "DUPLICATE_NODE_ID" | "PER_SIDE_BORDER_WITH_RADIUS";
|
|
2
|
+
type DiagnosticCode = "IMAGE_MEASURE_FAILED" | "IMAGE_NOT_PREFETCHED" | "AUTOFIT_OVERFLOW" | "SCALE_BELOW_THRESHOLD" | "MASTER_PPTX_PARSE_FAILED" | "ARROW_REF_NOT_FOUND" | "DUPLICATE_NODE_ID" | "PER_SIDE_BORDER_WITH_RADIUS" | "NODE_OUT_OF_BOUNDS" | "NODE_OVERLAP";
|
|
3
3
|
interface Diagnostic {
|
|
4
4
|
code: DiagnosticCode;
|
|
5
5
|
message: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"diagnostics.d.ts","names":[],"sources":["../src/diagnostics.ts"],"mappings":";KAAY,cAAA;AAAA,
|
|
1
|
+
{"version":3,"file":"diagnostics.d.ts","names":[],"sources":["../src/diagnostics.ts"],"mappings":";KAAY,cAAA;AAAA,UAYK,UAAA;EACf,IAAA,EAAM,cAAc;EACpB,OAAA;AAAA;AAAA,cAWW,gBAAA,SAAyB,KAAA;EAAA,SACR,WAAA,EAAa,UAAA;cAAb,WAAA,EAAa,UAAA;AAAA"}
|
package/dist/diagnostics.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"diagnostics.js","names":[],"sources":["../src/diagnostics.ts"],"sourcesContent":["export type DiagnosticCode =\n | \"IMAGE_MEASURE_FAILED\"\n | \"IMAGE_NOT_PREFETCHED\"\n | \"AUTOFIT_OVERFLOW\"\n | \"SCALE_BELOW_THRESHOLD\"\n | \"MASTER_PPTX_PARSE_FAILED\"\n | \"ARROW_REF_NOT_FOUND\"\n | \"DUPLICATE_NODE_ID\"\n | \"PER_SIDE_BORDER_WITH_RADIUS\";\n\nexport interface Diagnostic {\n code: DiagnosticCode;\n message: string;\n}\n\nexport class DiagnosticCollector {\n readonly items: Diagnostic[] = [];\n\n add(code: DiagnosticCode, message: string): void {\n this.items.push({ code, message });\n }\n}\n\nexport class DiagnosticsError extends Error {\n constructor(public readonly diagnostics: Diagnostic[]) {\n const summary = diagnostics\n .map((d) => `[${d.code}] ${d.message}`)\n .join(\"\\n\");\n super(`Build completed with diagnostics:\\n${summary}`);\n this.name = \"DiagnosticsError\";\n }\n}\n"],"mappings":";
|
|
1
|
+
{"version":3,"file":"diagnostics.js","names":[],"sources":["../src/diagnostics.ts"],"sourcesContent":["export type DiagnosticCode =\n | \"IMAGE_MEASURE_FAILED\"\n | \"IMAGE_NOT_PREFETCHED\"\n | \"AUTOFIT_OVERFLOW\"\n | \"SCALE_BELOW_THRESHOLD\"\n | \"MASTER_PPTX_PARSE_FAILED\"\n | \"ARROW_REF_NOT_FOUND\"\n | \"DUPLICATE_NODE_ID\"\n | \"PER_SIDE_BORDER_WITH_RADIUS\"\n | \"NODE_OUT_OF_BOUNDS\"\n | \"NODE_OVERLAP\";\n\nexport interface Diagnostic {\n code: DiagnosticCode;\n message: string;\n}\n\nexport class DiagnosticCollector {\n readonly items: Diagnostic[] = [];\n\n add(code: DiagnosticCode, message: string): void {\n this.items.push({ code, message });\n }\n}\n\nexport class DiagnosticsError extends Error {\n constructor(public readonly diagnostics: Diagnostic[]) {\n const summary = diagnostics\n .map((d) => `[${d.code}] ${d.message}`)\n .join(\"\\n\");\n super(`Build completed with diagnostics:\\n${summary}`);\n this.name = \"DiagnosticsError\";\n }\n}\n"],"mappings":";AAiBA,IAAa,sBAAb,MAAiC;CAC/B,QAA+B,CAAC;CAEhC,IAAI,MAAsB,SAAuB;EAC/C,KAAK,MAAM,KAAK;GAAE;GAAM;EAAQ,CAAC;CACnC;AACF;AAEA,IAAa,mBAAb,cAAsC,MAAM;CACd;CAA5B,YAAY,aAA2C;EACrD,MAAM,UAAU,YACb,KAAK,MAAM,IAAI,EAAE,KAAK,IAAI,EAAE,SAAS,CAAC,CACtC,KAAK,IAAI;EACZ,MAAM,sCAAsC,SAAS;EAJ3B,KAAA,cAAA;EAK1B,KAAK,OAAO;CACd;AACF"}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { getNodeDef } from "../registry/nodeRegistry.js";
|
|
2
|
+
import "../registry/index.js";
|
|
3
|
+
//#region src/validatePositioned/validatePositioned.ts
|
|
4
|
+
/** 浮動小数点演算とサブピクセル丸めの揺れを誤検知しないための許容量 (px) */
|
|
5
|
+
const EPSILON = .5;
|
|
6
|
+
/**
|
|
7
|
+
* toPositioned 後の絶対座標ツリーを走査し、レイアウト上の問題を
|
|
8
|
+
* Diagnostic (警告) として報告する。ビルドは止めない。
|
|
9
|
+
*
|
|
10
|
+
* - NODE_OUT_OF_BOUNDS: ノードの矩形がスライド境界からはみ出している
|
|
11
|
+
* - NODE_OVERLAP: VStack / HStack 内の兄弟ノード同士が意図せず重なっている
|
|
12
|
+
*
|
|
13
|
+
* 意図的な重なり (Layer 配下・position="absolute"・負 margin / gap・
|
|
14
|
+
* zIndex 明示) は検出対象外とし、誤検知を避ける。
|
|
15
|
+
*/
|
|
16
|
+
function validatePositioned(node, slideSize, ctx, slideIndex) {
|
|
17
|
+
walk(node, describeNode(node));
|
|
18
|
+
function walk(n, path) {
|
|
19
|
+
let descendantOutOfBounds = false;
|
|
20
|
+
if (n.type === "vstack" || n.type === "hstack" || n.type === "layer") {
|
|
21
|
+
const children = n.children;
|
|
22
|
+
if (n.type !== "layer") reportSiblingOverlap(n, children, path);
|
|
23
|
+
children.forEach((child, i) => {
|
|
24
|
+
descendantOutOfBounds = walk(child, childPath(path, child, i)) || descendantOutOfBounds;
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
if (descendantOutOfBounds) return true;
|
|
28
|
+
return reportOutOfBounds(n, path);
|
|
29
|
+
}
|
|
30
|
+
function reportOutOfBounds(n, path) {
|
|
31
|
+
if (n.type === "line" || n.type === "arrow") return false;
|
|
32
|
+
if ("rotate" in n && n.rotate) return false;
|
|
33
|
+
const over = [];
|
|
34
|
+
if (-n.x > EPSILON) over.push(`left by ${fmt(-n.x)}px`);
|
|
35
|
+
if (-n.y > EPSILON) over.push(`top by ${fmt(-n.y)}px`);
|
|
36
|
+
if (n.x + n.w - slideSize.w > EPSILON) over.push(`right by ${fmt(n.x + n.w - slideSize.w)}px`);
|
|
37
|
+
if (n.y + n.h - slideSize.h > EPSILON) over.push(`bottom by ${fmt(n.y + n.h - slideSize.h)}px`);
|
|
38
|
+
if (over.length === 0) return false;
|
|
39
|
+
ctx.diagnostics.add("NODE_OUT_OF_BOUNDS", `slide ${slideIndex + 1}: ${path} (${fmtRect(n)}) extends beyond the slide bounds (${slideSize.w}x${slideSize.h}): ${over.join(", ")}`);
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
function reportSiblingOverlap(n, children, path) {
|
|
43
|
+
if ((n.gap ?? 0) < 0) return;
|
|
44
|
+
const candidates = children.map((child, i) => ({
|
|
45
|
+
child,
|
|
46
|
+
i
|
|
47
|
+
})).filter(({ child }) => !isIntentionalOverlap(child));
|
|
48
|
+
for (let a = 0; a < candidates.length; a++) for (let b = a + 1; b < candidates.length; b++) {
|
|
49
|
+
const { child: ca, i: ia } = candidates[a];
|
|
50
|
+
const { child: cb, i: ib } = candidates[b];
|
|
51
|
+
const w = Math.min(ca.x + ca.w, cb.x + cb.w) - Math.max(ca.x, cb.x);
|
|
52
|
+
const h = Math.min(ca.y + ca.h, cb.y + cb.h) - Math.max(ca.y, cb.y);
|
|
53
|
+
if (w > EPSILON && h > EPSILON) ctx.diagnostics.add("NODE_OVERLAP", `slide ${slideIndex + 1}: ${path}: children ${describeChild(ca, ia)} (${fmtRect(ca)}) and ${describeChild(cb, ib)} (${fmtRect(cb)}) overlap by ${fmt(w)}x${fmt(h)}px`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/** 意図的な重なりとして兄弟重なり検査から除外すべき子か */
|
|
58
|
+
function isIntentionalOverlap(n) {
|
|
59
|
+
if (n.position === "absolute") return true;
|
|
60
|
+
if (n.zIndex !== void 0) return true;
|
|
61
|
+
return hasNegativeMargin(n.margin);
|
|
62
|
+
}
|
|
63
|
+
function hasNegativeMargin(margin) {
|
|
64
|
+
if (margin === void 0) return false;
|
|
65
|
+
if (typeof margin === "number") return margin < 0;
|
|
66
|
+
return [
|
|
67
|
+
margin.top,
|
|
68
|
+
margin.right,
|
|
69
|
+
margin.bottom,
|
|
70
|
+
margin.left
|
|
71
|
+
].some((v) => (v ?? 0) < 0);
|
|
72
|
+
}
|
|
73
|
+
function describeNode(n) {
|
|
74
|
+
const tag = getNodeDef(n.type).tagName;
|
|
75
|
+
return n.id ? `<${tag} id="${n.id}">` : `<${tag}>`;
|
|
76
|
+
}
|
|
77
|
+
function describeChild(n, index) {
|
|
78
|
+
return `${describeNode(n)}[${index}]`;
|
|
79
|
+
}
|
|
80
|
+
function childPath(parentPath, child, index) {
|
|
81
|
+
return `${parentPath} > ${describeChild(child, index)}`;
|
|
82
|
+
}
|
|
83
|
+
function fmt(v) {
|
|
84
|
+
return Math.round(v * 10) / 10;
|
|
85
|
+
}
|
|
86
|
+
function fmtRect(n) {
|
|
87
|
+
return `x=${fmt(n.x)}, y=${fmt(n.y)}, w=${fmt(n.w)}, h=${fmt(n.h)}`;
|
|
88
|
+
}
|
|
89
|
+
//#endregion
|
|
90
|
+
export { validatePositioned };
|
|
91
|
+
|
|
92
|
+
//# sourceMappingURL=validatePositioned.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validatePositioned.js","names":[],"sources":["../../src/validatePositioned/validatePositioned.ts"],"sourcesContent":["import type { BuildContext } from \"../buildContext.ts\";\nimport { getNodeDef } from \"../registry/index.ts\";\nimport type { PositionedNode } from \"../types.ts\";\n\n/** 浮動小数点演算とサブピクセル丸めの揺れを誤検知しないための許容量 (px) */\nconst EPSILON = 0.5;\n\n/**\n * toPositioned 後の絶対座標ツリーを走査し、レイアウト上の問題を\n * Diagnostic (警告) として報告する。ビルドは止めない。\n *\n * - NODE_OUT_OF_BOUNDS: ノードの矩形がスライド境界からはみ出している\n * - NODE_OVERLAP: VStack / HStack 内の兄弟ノード同士が意図せず重なっている\n *\n * 意図的な重なり (Layer 配下・position=\"absolute\"・負 margin / gap・\n * zIndex 明示) は検出対象外とし、誤検知を避ける。\n */\nexport function validatePositioned(\n node: PositionedNode,\n slideSize: { w: number; h: number },\n ctx: BuildContext,\n slideIndex: number,\n): void {\n walk(node, describeNode(node));\n\n function walk(n: PositionedNode, path: string): boolean {\n // はみ出しの原因に最も近いノードを特定できるよう、子孫がはみ出して\n // いる場合は親 (巻き添えではみ出すコンテナ) の報告を抑制する\n let descendantOutOfBounds = false;\n\n if (n.type === \"vstack\" || n.type === \"hstack\" || n.type === \"layer\") {\n const children: PositionedNode[] = n.children;\n if (n.type !== \"layer\") {\n reportSiblingOverlap(n, children, path);\n }\n children.forEach((child, i) => {\n descendantOutOfBounds =\n walk(child, childPath(path, child, i)) || descendantOutOfBounds;\n });\n }\n\n if (descendantOutOfBounds) return true;\n return reportOutOfBounds(n, path);\n }\n\n function reportOutOfBounds(n: PositionedNode, path: string): boolean {\n // Line は線分座標 (x1,y1-x2,y2)、Arrow は id 参照ベースのため矩形判定の対象外\n if (n.type === \"line\" || n.type === \"arrow\") return false;\n // rotate は renderPptx でのみ適用され、回転後の境界はここでは分からない\n if (\"rotate\" in n && n.rotate) return false;\n\n const over: string[] = [];\n if (-n.x > EPSILON) over.push(`left by ${fmt(-n.x)}px`);\n if (-n.y > EPSILON) over.push(`top by ${fmt(-n.y)}px`);\n if (n.x + n.w - slideSize.w > EPSILON) {\n over.push(`right by ${fmt(n.x + n.w - slideSize.w)}px`);\n }\n if (n.y + n.h - slideSize.h > EPSILON) {\n over.push(`bottom by ${fmt(n.y + n.h - slideSize.h)}px`);\n }\n if (over.length === 0) return false;\n\n ctx.diagnostics.add(\n \"NODE_OUT_OF_BOUNDS\",\n `slide ${slideIndex + 1}: ${path} (${fmtRect(n)}) extends beyond the slide bounds (${slideSize.w}x${slideSize.h}): ${over.join(\", \")}`,\n );\n return true;\n }\n\n function reportSiblingOverlap(\n n: Extract<PositionedNode, { type: \"vstack\" | \"hstack\" }>,\n children: PositionedNode[],\n path: string,\n ): void {\n // 負 gap は重ねるための明示指定 (例: ProcessArrow 風の表現) なので対象外\n if ((n.gap ?? 0) < 0) return;\n\n const candidates = children\n .map((child, i) => ({ child, i }))\n .filter(({ child }) => !isIntentionalOverlap(child));\n\n for (let a = 0; a < candidates.length; a++) {\n for (let b = a + 1; b < candidates.length; b++) {\n const { child: ca, i: ia } = candidates[a];\n const { child: cb, i: ib } = candidates[b];\n const w = Math.min(ca.x + ca.w, cb.x + cb.w) - Math.max(ca.x, cb.x);\n const h = Math.min(ca.y + ca.h, cb.y + cb.h) - Math.max(ca.y, cb.y);\n if (w > EPSILON && h > EPSILON) {\n ctx.diagnostics.add(\n \"NODE_OVERLAP\",\n `slide ${slideIndex + 1}: ${path}: children ${describeChild(ca, ia)} (${fmtRect(ca)}) and ${describeChild(cb, ib)} (${fmtRect(cb)}) overlap by ${fmt(w)}x${fmt(h)}px`,\n );\n }\n }\n }\n }\n}\n\n/** 意図的な重なりとして兄弟重なり検査から除外すべき子か */\nfunction isIntentionalOverlap(n: PositionedNode): boolean {\n if (n.position === \"absolute\") return true;\n if (n.zIndex !== undefined) return true;\n return hasNegativeMargin(n.margin);\n}\n\nfunction hasNegativeMargin(margin: PositionedNode[\"margin\"]): boolean {\n if (margin === undefined) return false;\n if (typeof margin === \"number\") return margin < 0;\n return [margin.top, margin.right, margin.bottom, margin.left].some(\n (v) => (v ?? 0) < 0,\n );\n}\n\nfunction describeNode(n: PositionedNode): string {\n const tag = getNodeDef(n.type).tagName;\n return n.id ? `<${tag} id=\"${n.id}\">` : `<${tag}>`;\n}\n\nfunction describeChild(n: PositionedNode, index: number): string {\n return `${describeNode(n)}[${index}]`;\n}\n\nfunction childPath(\n parentPath: string,\n child: PositionedNode,\n index: number,\n): string {\n return `${parentPath} > ${describeChild(child, index)}`;\n}\n\nfunction fmt(v: number): number {\n return Math.round(v * 10) / 10;\n}\n\nfunction fmtRect(n: { x: number; y: number; w: number; h: number }): string {\n return `x=${fmt(n.x)}, y=${fmt(n.y)}, w=${fmt(n.w)}, h=${fmt(n.h)}`;\n}\n"],"mappings":"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"}
|