diff --git a/.github/scripts/package-lock.json b/.github/scripts/package-lock.json deleted file mode 100644 index 71260f688..000000000 --- a/.github/scripts/package-lock.json +++ /dev/null @@ -1,750 +0,0 @@ -{ - "name": "scripts", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "dependencies": { - "@anthropic-ai/claude-agent-sdk": "^0.2.42", - "@anthropic-ai/sdk": "^0.39.0" - } - }, - "node_modules/@anthropic-ai/claude-agent-sdk": { - "version": "0.2.42", - "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk/-/claude-agent-sdk-0.2.42.tgz", - "integrity": "sha512-/CugP7AjP57Dqtl2sbsDtxdbpQoPKIhjyF5WrTViGu4NHQdM+UikrRs4MhZ2jeotiC5R7iK9ZUN9SiBgcZ8oLw==", - "license": "SEE LICENSE IN README.md", - "engines": { - "node": ">=18.0.0" - }, - "optionalDependencies": { - "@img/sharp-darwin-arm64": "^0.33.5", - "@img/sharp-darwin-x64": "^0.33.5", - "@img/sharp-linux-arm": "^0.33.5", - "@img/sharp-linux-arm64": "^0.33.5", - "@img/sharp-linux-x64": "^0.33.5", - "@img/sharp-linuxmusl-arm64": "^0.33.5", - "@img/sharp-linuxmusl-x64": "^0.33.5", - "@img/sharp-win32-x64": "^0.33.5" - }, - "peerDependencies": { - "zod": "^4.0.0" - } - }, - "node_modules/@anthropic-ai/sdk": { - "version": "0.39.0", - "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.39.0.tgz", - "integrity": "sha512-eMyDIPRZbt1CCLErRCi3exlAvNkBtRe+kW5vvJyef93PmNr/clstYgHhtvmkxN82nlKgzyGPCyGxrm0JQ1ZIdg==", - "license": "MIT", - "dependencies": { - "@types/node": "^18.11.18", - "@types/node-fetch": "^2.6.4", - "abort-controller": "^3.0.0", - "agentkeepalive": "^4.2.1", - "form-data-encoder": "1.7.2", - "formdata-node": "^4.3.2", - "node-fetch": "^2.6.7" - } - }, - "node_modules/@img/sharp-darwin-arm64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz", - "integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-darwin-arm64": "1.0.4" - } - }, - "node_modules/@img/sharp-darwin-x64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz", - "integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.0.4" - } - }, - "node_modules/@img/sharp-libvips-darwin-arm64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz", - "integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==", - "cpu": [ - "arm64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "darwin" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz", - "integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==", - "cpu": [ - "x64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "darwin" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz", - "integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==", - "cpu": [ - "arm" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz", - "integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==", - "cpu": [ - "arm64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz", - "integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==", - "cpu": [ - "x64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz", - "integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==", - "cpu": [ - "arm64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz", - "integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==", - "cpu": [ - "x64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-linux-arm": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz", - "integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==", - "cpu": [ - "arm" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.0.5" - } - }, - "node_modules/@img/sharp-linux-arm64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz", - "integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.0.4" - } - }, - "node_modules/@img/sharp-linux-x64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz", - "integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.0.4" - } - }, - "node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz", - "integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.0.4" - } - }, - "node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz", - "integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.0.4" - } - }, - "node_modules/@img/sharp-win32-x64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz", - "integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@types/node": { - "version": "18.19.130", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.130.tgz", - "integrity": "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==", - "license": "MIT", - "dependencies": { - "undici-types": "~5.26.4" - } - }, - "node_modules/@types/node-fetch": { - "version": "2.6.13", - "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.13.tgz", - "integrity": "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==", - "license": "MIT", - "dependencies": { - "@types/node": "*", - "form-data": "^4.0.4" - } - }, - "node_modules/abort-controller": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", - "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", - "license": "MIT", - "dependencies": { - "event-target-shim": "^5.0.0" - }, - "engines": { - "node": ">=6.5" - } - }, - "node_modules/agentkeepalive": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", - "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", - "license": "MIT", - "dependencies": { - "humanize-ms": "^1.2.1" - }, - "engines": { - "node": ">= 8.0.0" - } - }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "license": "MIT" - }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "license": "MIT", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-set-tostringtag": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", - "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/event-target-shim": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", - "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/form-data": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", - "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", - "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/form-data-encoder": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.2.tgz", - "integrity": "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==", - "license": "MIT" - }, - "node_modules/formdata-node": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-4.4.1.tgz", - "integrity": "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==", - "license": "MIT", - "dependencies": { - "node-domexception": "1.0.0", - "web-streams-polyfill": "4.0.0-beta.3" - }, - "engines": { - "node": ">= 12.20" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-tostringtag": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "license": "MIT", - "dependencies": { - "has-symbols": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/humanize-ms": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", - "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", - "license": "MIT", - "dependencies": { - "ms": "^2.0.0" - } - }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/node-domexception": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", - "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", - "deprecated": "Use your platform's native DOMException instead", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/jimmywarting" - }, - { - "type": "github", - "url": "https://paypal.me/jimmywarting" - } - ], - "license": "MIT", - "engines": { - "node": ">=10.5.0" - } - }, - "node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "license": "MIT", - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, - "node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "license": "MIT" - }, - "node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", - "license": "MIT" - }, - "node_modules/web-streams-polyfill": { - "version": "4.0.0-beta.3", - "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz", - "integrity": "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==", - "license": "MIT", - "engines": { - "node": ">= 14" - } - }, - "node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "license": "BSD-2-Clause" - }, - "node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "license": "MIT", - "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - }, - "node_modules/zod": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", - "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", - "license": "MIT", - "peer": true, - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - } - } -} diff --git a/packages/layout-engine/contracts/src/index.ts b/packages/layout-engine/contracts/src/index.ts index f344c8c28..44f6bf242 100644 --- a/packages/layout-engine/contracts/src/index.ts +++ b/packages/layout-engine/contracts/src/index.ts @@ -1709,6 +1709,13 @@ export interface PositionMapping { readonly maps: readonly unknown[]; } +/** + * Rendering flow mode. + * - `paginated`: discrete page surfaces + * - `semantic`: continuous flow surface + */ +export type FlowMode = 'paginated' | 'semantic'; + export interface PainterDOM { paint(layout: Layout, mount: HTMLElement, mapping?: PositionMapping): void; /** diff --git a/packages/layout-engine/layout-bridge/src/incrementalLayout.ts b/packages/layout-engine/layout-bridge/src/incrementalLayout.ts index b6881d78f..2878d0dbd 100644 --- a/packages/layout-engine/layout-bridge/src/incrementalLayout.ts +++ b/packages/layout-engine/layout-bridge/src/incrementalLayout.ts @@ -6,6 +6,7 @@ import type { SectionMetadata, ParagraphBlock, ColumnLayout, + SectionBreakBlock, } from '@superdoc/contracts'; import { layoutDocument, @@ -15,6 +16,7 @@ import { computeDisplayPageNumber, resolvePageNumberTokens, type NumberingContext, + SEMANTIC_PAGE_HEIGHT_PX, } from '@superdoc/layout-engine'; import { remeasureParagraph } from './remeasure'; import { computeDirtyRegions } from './diff'; @@ -737,6 +739,15 @@ export async function incrementalLayout( }, previousMeasures?: Measure[] | null, ): Promise { + const isSemanticFlow = options.flowMode === 'semantic'; + + // In semantic mode, neutralize paginated-only inputs so downstream code + // doesn't need per-step guards. + if (isSemanticFlow) { + headerFooter = undefined; + nextBlocks = rewriteSectionBreaksForSemanticFlow(nextBlocks, options); + } + // Dirty region computation const dirtyStart = performance.now(); const dirty = computeDirtyRegions(previousBlocks, nextBlocks); @@ -755,7 +766,15 @@ export async function incrementalLayout( } const hasPreviousMeasures = Array.isArray(previousMeasures) && previousMeasures.length === previousBlocks.length; - const previousConstraints = hasPreviousMeasures ? resolveMeasurementConstraints(options, previousBlocks) : null; + // In semantic mode, the options-level semantic.contentWidth can change between + // renders (container resize) while the block content stays the same. Since + // previousConstraints is re-derived from the current options (not the options + // that produced the previous measures), it would incorrectly match the current + // constraints even when the previous measures were taken at a different width. + // Disable previous-pass measure reuse in semantic mode; the width-keyed + // measureCache still provides fast lookups for unchanged blocks. + const previousConstraints = + hasPreviousMeasures && !isSemanticFlow ? resolveMeasurementConstraints(options, previousBlocks) : null; const canReusePreviousMeasures = hasPreviousMeasures && previousConstraints?.measurementWidth === measurementWidth && @@ -1041,8 +1060,11 @@ export async function incrementalLayout( perfLog(`[Perf] 4.1.6 Pre-layout footers for height: ${(footerPreEnd - footerPreStart).toFixed(2)}ms`); } + // In semantic mode, nextBlocks were already rewritten during pre-processing. + const blocksForLayout = nextBlocks; + const layoutStart = performance.now(); - let layout = layoutDocument(nextBlocks, measures, { + let layout = layoutDocument(blocksForLayout, measures, { ...options, headerContentHeights, // Pass header heights to prevent overlap (per-variant) footerContentHeights, // Pass footer heights to prevent overlap (per-variant) @@ -1061,7 +1083,7 @@ export async function incrementalLayout( // Steps: paginate -> build numbering context -> resolve PAGE/NUMPAGES tokens // -> remeasure affected blocks -> re-paginate -> repeat until stable const maxIterations = 3; - let currentBlocks = nextBlocks; + let currentBlocks = blocksForLayout; let currentMeasures = measures; let iteration = 0; @@ -1072,7 +1094,7 @@ export async function incrementalLayout( let converged = true; // Only run token resolution if feature flag is enabled - if (FeatureFlags.BODY_PAGE_TOKENS) { + if (!isSemanticFlow && FeatureFlags.BODY_PAGE_TOKENS) { while (iteration < maxIterations) { // Build numbering context from current layout const sections = options.sectionMetadata ?? []; @@ -1184,7 +1206,7 @@ export async function incrementalLayout( let extraBlocks: FlowBlock[] | undefined; let extraMeasures: Measure[] | undefined; const footnotesInput = isFootnotesLayoutInput(options.footnotes) ? options.footnotes : null; - if (footnotesInput && footnotesInput.refs.length > 0 && footnotesInput.blocksById.size > 0) { + if (!isSemanticFlow && footnotesInput && footnotesInput.refs.length > 0 && footnotesInput.blocksById.size > 0) { const gap = typeof footnotesInput.gap === 'number' && Number.isFinite(footnotesInput.gap) ? footnotesInput.gap : 2; const topPadding = typeof footnotesInput.topPadding === 'number' && Number.isFinite(footnotesInput.topPadding) @@ -1870,6 +1892,40 @@ const DEFAULT_MARGINS = { top: 72, right: 72, bottom: 72, left: 72 }; export const normalizeMargin = (value: number | undefined, fallback: number): number => Number.isFinite(value) ? (value as number) : fallback; +/** + * Rewrites section break blocks so that `layoutDocument` uses the semantic page + * dimensions instead of the per-section DOCX page sizes. Without this, each + * section break carries its original narrow DOCX `pageSize` / `margins` / + * `columns`, and `layoutDocument` would switch `activePageSize` to those values + * — defeating the semantic flow's container-width–based layout. + * + * Only the block-level layout properties are overridden; everything else + * (numbering, header/footer refs, vAlign, orientation) is preserved. + */ +function rewriteSectionBreaksForSemanticFlow(blocks: FlowBlock[], options: LayoutOptions): FlowBlock[] { + const semanticPageSize = options.pageSize; + const semanticMargins = options.margins; + if (!semanticPageSize) return blocks; + if (!blocks.some((b) => b.kind === 'sectionBreak')) return blocks; + + return blocks.map((block) => { + if (block.kind !== 'sectionBreak') return block; + const sb = block as SectionBreakBlock; + return { + ...sb, + pageSize: { w: semanticPageSize.w, h: semanticPageSize.h }, + margins: { + ...sb.margins, + top: semanticMargins?.top, + right: semanticMargins?.right, + bottom: semanticMargins?.bottom, + left: semanticMargins?.left, + }, + columns: { count: 1, gap: 0 }, + }; + }); +} + /** * Resolves the maximum measurement constraints (width and height) needed for measuring blocks * across all sections in a document. @@ -1935,6 +1991,26 @@ export function resolveMeasurementConstraints( measurementWidth: number; measurementHeight: number; } { + if (options.flowMode === 'semantic') { + const semanticContentWidth = options.semantic?.contentWidth; + if (typeof semanticContentWidth === 'number' && Number.isFinite(semanticContentWidth) && semanticContentWidth > 0) { + const semanticTop = normalizeMargin( + options.semantic?.marginTop, + normalizeMargin(options.margins?.top, DEFAULT_MARGINS.top), + ); + const semanticBottom = normalizeMargin( + options.semantic?.marginBottom, + normalizeMargin(options.margins?.bottom, DEFAULT_MARGINS.bottom), + ); + const measurementHeight = Math.max(1, SEMANTIC_PAGE_HEIGHT_PX - (semanticTop + semanticBottom)); + const measurementWidth = Math.max(1, Math.floor(semanticContentWidth)); + return { + measurementWidth, + measurementHeight, + }; + } + } + const pageSize = options.pageSize ?? DEFAULT_PAGE_SIZE; const margins = { top: normalizeMargin(options.margins?.top, DEFAULT_MARGINS.top), diff --git a/packages/layout-engine/layout-bridge/src/index.ts b/packages/layout-engine/layout-bridge/src/index.ts index 8323118ba..9533edc0d 100644 --- a/packages/layout-engine/layout-bridge/src/index.ts +++ b/packages/layout-engine/layout-bridge/src/index.ts @@ -50,7 +50,7 @@ export { export type { HeaderFooterBatch, DigitBucket } from './layoutHeaderFooter'; export { findWordBoundaries, findParagraphBoundaries } from './text-boundaries'; export type { BoundaryRange } from './text-boundaries'; -export { incrementalLayout, measureCache } from './incrementalLayout'; +export { incrementalLayout, measureCache, normalizeMargin } from './incrementalLayout'; export type { HeaderFooterLayoutResult, IncrementalLayoutResult } from './incrementalLayout'; // Re-export computeDisplayPageNumber from layout-engine for section-aware page numbering export { computeDisplayPageNumber, type DisplayPageInfo } from '@superdoc/layout-engine'; diff --git a/packages/layout-engine/layout-bridge/test/incrementalLayout.semanticFlow.test.ts b/packages/layout-engine/layout-bridge/test/incrementalLayout.semanticFlow.test.ts new file mode 100644 index 000000000..7a611ae7d --- /dev/null +++ b/packages/layout-engine/layout-bridge/test/incrementalLayout.semanticFlow.test.ts @@ -0,0 +1,126 @@ +import { describe, expect, it, vi } from 'vitest'; + +import { incrementalLayout } from '../src/incrementalLayout'; + +import type { FlowBlock, Measure, SectionBreakBlock } from '@superdoc/contracts'; + +const makeParagraph = (id: string, text: string): FlowBlock => ({ + kind: 'paragraph', + id, + runs: [{ text, fontFamily: 'Arial', fontSize: 12 }], +}); + +const makeParagraphMeasure = (lineHeight: number, runLength: number, maxWidth: number): Measure => ({ + kind: 'paragraph', + lines: [ + { + fromRun: 0, + fromChar: 0, + toRun: 0, + toChar: runLength, + width: Math.min(maxWidth, runLength * 7), + ascent: lineHeight * 0.8, + descent: lineHeight * 0.2, + lineHeight, + maxWidth, + }, + ], + totalHeight: lineHeight, +}); + +describe('incrementalLayout semantic flow', () => { + it('rewrites section-break columns to single-column semantic width before layout', async () => { + const semanticMargins = { top: 24, right: 100, bottom: 36, left: 100 }; + const semanticContentWidth = 600; + const semanticPageWidth = semanticContentWidth + semanticMargins.left + semanticMargins.right; + + const firstSectionBreak: SectionBreakBlock = { + kind: 'sectionBreak', + id: 'sb-1', + type: 'continuous', + attrs: { isFirstSection: true, source: 'sectPr' }, + // Intentionally narrow + multi-column: would reduce paragraph fragment width + // without semantic rewrite in incrementalLayout. + pageSize: { w: 320, h: 900 }, + margins: { top: 12, right: 12, bottom: 12, left: 12 }, + columns: { count: 2, gap: 24 }, + }; + + const paragraph = makeParagraph('p-1', 'Semantic section rewrite keeps this paragraph full-width.'); + const paragraphTextLength = paragraph.kind === 'paragraph' ? paragraph.runs[0].text.length : 1; + + const measureBlock = vi.fn(async (block: FlowBlock, constraints: { maxWidth: number; maxHeight: number }) => { + if (block.kind !== 'paragraph') { + throw new Error(`Unexpected block kind in test measure: ${block.kind}`); + } + return makeParagraphMeasure(20, paragraphTextLength, constraints.maxWidth); + }); + + const result = await incrementalLayout( + [], + null, + [firstSectionBreak, paragraph], + { + flowMode: 'semantic', + pageSize: { w: semanticPageWidth, h: 900 }, + margins: semanticMargins, + semantic: { + contentWidth: semanticContentWidth, + marginTop: semanticMargins.top, + marginBottom: semanticMargins.bottom, + }, + }, + measureBlock, + ); + + const paragraphFragment = result.layout.pages + .flatMap((page) => page.fragments) + .find((fragment) => fragment.kind === 'para' && fragment.blockId === paragraph.id); + + expect(paragraphFragment).toBeDefined(); + expect(paragraphFragment?.width).toBe(semanticContentWidth); + }); + + it('skips header/footer layout work in semantic flow mode', async () => { + const paragraph = makeParagraph('body-1', 'Body content'); + const headerParagraph = makeParagraph('header-1', 'Header content'); + + const measureBlock = vi.fn(async (block: FlowBlock, constraints: { maxWidth: number; maxHeight: number }) => { + if (block.kind !== 'paragraph') { + throw new Error(`Unexpected block kind in test measure: ${block.kind}`); + } + const runLength = block.runs[0]?.text?.length ?? 1; + return makeParagraphMeasure(20, runLength, constraints.maxWidth); + }); + + const headerMeasure = vi.fn(async (block: FlowBlock, constraints: { maxWidth: number; maxHeight: number }) => { + if (block.kind !== 'paragraph') { + throw new Error(`Unexpected header block kind in test measure: ${block.kind}`); + } + const runLength = block.runs[0]?.text?.length ?? 1; + return makeParagraphMeasure(20, runLength, constraints.maxWidth); + }); + + const result = await incrementalLayout( + [], + null, + [paragraph], + { + flowMode: 'semantic', + pageSize: { w: 800, h: 900 }, + margins: { top: 40, right: 100, bottom: 40, left: 100 }, + semantic: { contentWidth: 600, marginTop: 40, marginBottom: 40 }, + }, + measureBlock, + { + headerBlocks: { default: [headerParagraph] }, + constraints: { width: 600, height: 80 }, + measure: headerMeasure, + }, + ); + + expect(result.headers).toBeUndefined(); + expect(result.footers).toBeUndefined(); + expect(headerMeasure).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/layout-engine/layout-bridge/test/resolveMeasurementConstraints.test.ts b/packages/layout-engine/layout-bridge/test/resolveMeasurementConstraints.test.ts index 21bb7e85f..efa50a097 100644 --- a/packages/layout-engine/layout-bridge/test/resolveMeasurementConstraints.test.ts +++ b/packages/layout-engine/layout-bridge/test/resolveMeasurementConstraints.test.ts @@ -277,6 +277,54 @@ describe('resolveMeasurementConstraints', () => { }); }); + describe('semantic flow constraints', () => { + it('uses semantic content width directly when provided', () => { + const options: LayoutOptions = { + flowMode: 'semantic', + pageSize: { w: 612, h: 792 }, + margins: { top: 72, right: 72, bottom: 72, left: 72 }, + semantic: { + contentWidth: 530, + marginTop: 40, + marginBottom: 50, + }, + }; + + const result = resolveMeasurementConstraints(options); + expect(result.measurementWidth).toBe(530); + expect(result.measurementHeight).toBe(999910); // 1_000_000 - (40 + 50) + }); + + it('normalizes fractional semantic content width to match layout rounding', () => { + const options: LayoutOptions = { + flowMode: 'semantic', + pageSize: { w: 612, h: 792 }, + margins: { top: 72, right: 72, bottom: 72, left: 72 }, + semantic: { + contentWidth: 530.9, + marginTop: 40, + marginBottom: 50, + }, + }; + + const result = resolveMeasurementConstraints(options); + expect(result.measurementWidth).toBe(530); + expect(result.measurementHeight).toBe(999910); + }); + + it('falls back to paginated constraints when semantic content width is missing', () => { + const options: LayoutOptions = { + flowMode: 'semantic', + pageSize: { w: 612, h: 792 }, + margins: { top: 72, right: 72, bottom: 72, left: 72 }, + }; + + const result = resolveMeasurementConstraints(options); + expect(result.measurementWidth).toBe(468); + expect(result.measurementHeight).toBe(648); + }); + }); + describe('column width calculations', () => { it('handles zero gap in multi-column layout', () => { const options: LayoutOptions = { diff --git a/packages/layout-engine/layout-engine/src/index.d.ts b/packages/layout-engine/layout-engine/src/index.d.ts index fcd5e70ab..88ae16292 100644 --- a/packages/layout-engine/layout-engine/src/index.d.ts +++ b/packages/layout-engine/layout-engine/src/index.d.ts @@ -1,6 +1,7 @@ import type { ColumnLayout, FlowBlock, + FlowMode, HeaderFooterLayout, Layout, Measure, @@ -23,8 +24,17 @@ export type LayoutOptions = { pageSize?: PageSize; margins?: Margins; columns?: ColumnLayout; + flowMode?: FlowMode; + semantic?: { + contentWidth?: number; + marginLeft?: number; + marginRight?: number; + marginTop?: number; + marginBottom?: number; + }; remeasureParagraph?: (block: ParagraphBlock, maxWidth: number, firstLineIndent?: number) => ParagraphMeasure; }; +export declare const SEMANTIC_PAGE_HEIGHT_PX = 1000000; export type HeaderFooterConstraints = { width: number; height: number; diff --git a/packages/layout-engine/layout-engine/src/index.ts b/packages/layout-engine/layout-engine/src/index.ts index 614b71ff3..a021eba3e 100644 --- a/packages/layout-engine/layout-engine/src/index.ts +++ b/packages/layout-engine/layout-engine/src/index.ts @@ -24,6 +24,7 @@ import type { DrawingMeasure, DrawingFragment, SectionNumbering, + FlowMode, } from '@superdoc/contracts'; import { createFloatingObjectManager, computeAnchorX } from './floating-objects.js'; import { computeNextSectionPropsAtBreak } from './section-props'; @@ -62,6 +63,12 @@ type NormalizedColumns = ColumnLayout & { width: number }; */ const DEFAULT_PARAGRAPH_LINE_HEIGHT_PX = 20; +/** + * Synthetic page height used in semantic flow mode to avoid pagination-driven clipping + * during measurement. A large finite value preserves stable measurement constraints. + */ +export const SEMANTIC_PAGE_HEIGHT_PX = 1_000_000; + /** * Type guard to check if a fragment has a height property. * Image, Drawing, and Table fragments all have a required height property. @@ -419,6 +426,14 @@ export type LayoutOptions = { pageSize?: PageSize; margins?: Margins; columns?: ColumnLayout; + flowMode?: FlowMode; + semantic?: { + contentWidth?: number; + marginLeft?: number; + marginRight?: number; + marginTop?: number; + marginBottom?: number; + }; remeasureParagraph?: (block: ParagraphBlock, maxWidth: number, firstLineIndent?: number) => ParagraphMeasure; sectionMetadata?: SectionMetadata[]; /** diff --git a/packages/layout-engine/painters/dom/src/index.ts b/packages/layout-engine/painters/dom/src/index.ts index 910e99532..ea8103cce 100644 --- a/packages/layout-engine/painters/dom/src/index.ts +++ b/packages/layout-engine/painters/dom/src/index.ts @@ -10,7 +10,7 @@ import type { } from '@superdoc/contracts'; import { DomPainter } from './renderer.js'; import type { PageStyles } from './styles.js'; -import type { RulerOptions } from './renderer.js'; +import type { RulerOptions, FlowMode } from './renderer.js'; // Re-export constants export { DOM_CLASS_NAMES } from './constants.js'; @@ -54,6 +54,7 @@ export { export type { PmPositionValidationStats } from './pm-position-validation.js'; export type LayoutMode = 'vertical' | 'horizontal' | 'book'; +export type { FlowMode } from './renderer.js'; export type PageDecorationPayload = { fragments: Fragment[]; height: number; @@ -81,6 +82,7 @@ export type DomPainterOptions = { measures: Measure[]; pageStyles?: PageStyles; layoutMode?: LayoutMode; + flowMode?: FlowMode; /** Gap between pages in pixels (default: 24px for vertical, 20px for horizontal) */ pageGap?: number; headerProvider?: PageDecorationProvider; @@ -98,7 +100,7 @@ export type DomPainterOptions = { overscan?: number; /** * Gap between pages used for spacer math (px). When set, container gap is overridden - * to this value during virtualization. Default approximates existing margin+gap look: 72. + * to this value during virtualization. Defaults to the effective `pageGap`. */ gap?: number; /** Optional mount padding-top override (px) used in scroll mapping; defaults to computed style. */ @@ -124,6 +126,7 @@ export const createDomPainter = ( const painter = new DomPainter(options.blocks, options.measures, { pageStyles: options.pageStyles, layoutMode: options.layoutMode, + flowMode: options.flowMode, pageGap: options.pageGap, headerProvider: options.headerProvider, footerProvider: options.footerProvider, diff --git a/packages/layout-engine/painters/dom/src/renderer.ts b/packages/layout-engine/painters/dom/src/renderer.ts index cb6b4ef03..89bb65d67 100644 --- a/packages/layout-engine/painters/dom/src/renderer.ts +++ b/packages/layout-engine/painters/dom/src/renderer.ts @@ -43,6 +43,7 @@ import type { TableAttrs, TableCellAttrs, PositionMapping, + FlowMode, } from '@superdoc/contracts'; import { calculateJustifySpacing, computeLinePmRange, shouldApplyJustify, SPACE_CHARS } from '@superdoc/contracts'; import { getPresetShapeSvg } from '@superdoc/preset-geometry'; @@ -262,6 +263,8 @@ function isMinimalWordLayout(value: unknown): value is MinimalWordLayout { * - 'book': Book-style layout with facing pages */ export type LayoutMode = 'vertical' | 'horizontal' | 'book'; +// FlowMode is re-exported from @superdoc/contracts +export type { FlowMode } from '@superdoc/contracts'; type PageDecorationPayload = { fragments: Fragment[]; @@ -308,6 +311,7 @@ export type RulerOptions = { type PainterOptions = { pageStyles?: PageStyles; layoutMode?: LayoutMode; + flowMode?: FlowMode; /** Gap between pages in pixels (default: 24px for vertical, 20px for horizontal) */ pageGap?: number; headerProvider?: PageDecorationProvider; @@ -316,7 +320,7 @@ type PainterOptions = { enabled?: boolean; window?: number; overscan?: number; - /** Virtualization gap override (defaults to 72px; independent of pageGap) */ + /** Virtualization gap override (defaults to 72px; independent of pageGap). */ gap?: number; paddingTop?: number; }; @@ -786,6 +790,7 @@ export class DomPainter { private currentLayout: Layout | null = null; private changedBlocks = new Set(); private readonly layoutMode: LayoutMode; + private readonly isSemanticFlow: boolean; private headerProvider?: PageDecorationProvider; private footerProvider?: PageDecorationProvider; private totalPages = 0; @@ -833,6 +838,7 @@ export class DomPainter { constructor(blocks: FlowBlock[], measures: Measure[], options: PainterOptions = {}) { this.options = options; this.layoutMode = options.layoutMode ?? 'vertical'; + this.isSemanticFlow = (options.flowMode ?? 'paginated') === 'semantic'; this.blockLookup = this.buildBlockLookup(blocks, measures); this.headerProvider = options.headerProvider; this.footerProvider = options.footerProvider; @@ -845,11 +851,12 @@ export class DomPainter { : defaultGap; // Initialize virtualization config (feature-flagged) - if (this.layoutMode === 'vertical' && options.virtualization?.enabled) { + if (!this.isSemanticFlow && this.layoutMode === 'vertical' && options.virtualization?.enabled) { this.virtualEnabled = true; this.virtualWindow = Math.max(1, options.virtualization.window ?? 5); this.virtualOverscan = Math.max(0, options.virtualization.overscan ?? 0); - // Virtualization gap: use explicit virtualization.gap if provided, otherwise default to virtualized gap (72px) + // Virtualization gap: use explicit virtualization.gap if provided, + // otherwise default to legacy virtualized gap (72px). const maybeGap = options.virtualization.gap; if (typeof maybeGap === 'number' && Number.isFinite(maybeGap)) { this.virtualGap = Math.max(0, maybeGap); @@ -1033,7 +1040,7 @@ export class DomPainter { ensureSdtContainerStyles(doc); ensureImageSelectionStyles(doc); ensureNativeSelectionStyles(doc); - if (this.options.ruler?.enabled) { + if (!this.isSemanticFlow && this.options.ruler?.enabled) { ensureRulerStyles(doc); } mount.classList.add(CLASS_NAMES.container); @@ -1046,6 +1053,22 @@ export class DomPainter { this.mount = mount; this.totalPages = layout.pages.length; + if (this.isSemanticFlow) { + // Semantic mode always renders as a single continuous surface. + applyStyles(mount, containerStyles); + mount.style.gap = '0px'; + mount.style.alignItems = 'stretch'; + if (!this.currentLayout || this.pageStates.length === 0) { + this.fullRender(layout); + } else { + this.patchLayout(layout); + } + this.currentLayout = layout; + this.changedBlocks.clear(); + this.currentMapping = null; + return; + } + const mode = this.layoutMode; if (mode === 'horizontal') { applyStyles(mount, containerStylesHorizontal); @@ -1467,10 +1490,11 @@ export class DomPainter { const el = this.doc.createElement('div'); el.classList.add(CLASS_NAMES.page); applyStyles(el, pageStyles(width, height, this.getEffectivePageStyles())); + this.applySemanticPageOverrides(el); el.dataset.layoutEpoch = String(this.layoutEpoch); - // Render per-page ruler if enabled - if (this.options.ruler?.enabled) { + // Render per-page ruler if enabled (suppressed in semantic flow mode) + if (!this.isSemanticFlow && this.options.ruler?.enabled) { const rulerEl = this.renderPageRuler(width, page); if (rulerEl) { el.appendChild(rulerEl); @@ -1572,6 +1596,7 @@ export class DomPainter { } private renderDecorationsForPage(pageEl: HTMLElement, page: Page): void { + if (this.isSemanticFlow) return; this.renderDecorationSection(pageEl, page, 'header'); this.renderDecorationSection(pageEl, page, 'footer'); } @@ -1790,6 +1815,7 @@ export class DomPainter { private patchPage(state: PageDomState, page: Page, pageSize: { w: number; h: number }): void { const pageEl = state.element; applyStyles(pageEl, pageStyles(pageSize.w, pageSize.h, this.getEffectivePageStyles())); + this.applySemanticPageOverrides(pageEl); pageEl.dataset.pageNumber = String(page.number); pageEl.dataset.layoutEpoch = String(this.layoutEpoch); // pageIndex is already set during creation and doesn't change during patch @@ -1934,6 +1960,7 @@ export class DomPainter { const el = this.doc.createElement('div'); el.classList.add(CLASS_NAMES.page); applyStyles(el, pageStyles(pageSize.w, pageSize.h, this.getEffectivePageStyles())); + this.applySemanticPageOverrides(el); el.dataset.layoutEpoch = String(this.layoutEpoch); const contextBase: FragmentRenderContext = { @@ -1960,7 +1987,25 @@ export class DomPainter { return { element: el, fragments: fragmentStates }; } + private applySemanticPageOverrides(el: HTMLElement): void { + if (this.isSemanticFlow) { + el.style.overflow = 'visible'; + el.style.width = '100%'; + el.style.minWidth = '100%'; + } + } + private getEffectivePageStyles(): PageStyles | undefined { + if (this.isSemanticFlow) { + const base = this.options.pageStyles ?? {}; + return { + ...base, + background: base.background ?? '#fff', + boxShadow: 'none', + border: 'none', + margin: '0', + }; + } if (this.virtualEnabled && this.layoutMode === 'vertical') { // Remove top/bottom margins to avoid double-counting with container gap during virtualization const base = this.options.pageStyles ?? {}; diff --git a/packages/layout-engine/painters/dom/src/virtualization.test.ts b/packages/layout-engine/painters/dom/src/virtualization.test.ts index add592c21..52ee90f70 100644 --- a/packages/layout-engine/painters/dom/src/virtualization.test.ts +++ b/packages/layout-engine/painters/dom/src/virtualization.test.ts @@ -1,4 +1,4 @@ -import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; import { createDomPainter } from './index.js'; import type { FlowBlock, Measure, Layout, Fragment, PageMargins } from '@superdoc/contracts'; @@ -392,4 +392,69 @@ describe('DomPainter virtualization (vertical)', () => { const firstIndexAfter = firstPageAfter ? Number(firstPageAfter.dataset.pageIndex) : -1; expect(firstIndexAfter).toBeGreaterThanOrEqual(firstIndexBefore); }); + + it('disables virtualization rendering paths in semantic flow mode', () => { + const painter = createDomPainter({ + blocks: [block], + measures: [measure], + flowMode: 'semantic', + virtualization: { enabled: true, window: 2, overscan: 0, gap: 72, paddingTop: 0 }, + }); + + const layout = makeLayout(8); + painter.paint(layout, mount); + + const pages = mount.querySelectorAll('.superdoc-page'); + expect(pages.length).toBe(8); + expect(mount.querySelector('[data-virtual-spacer="top"]')).toBeNull(); + expect(mount.querySelector('[data-virtual-spacer="bottom"]')).toBeNull(); + }); + + it('skips header/footer decoration providers in semantic flow mode', () => { + const headerProvider = vi.fn(() => ({ + height: 20, + offset: 0, + fragments: [ + { + kind: 'para', + blockId: block.id, + fromLine: 0, + toLine: 1, + x: 0, + y: 0, + width: 50, + }, + ], + })); + const footerProvider = vi.fn(() => ({ + height: 20, + offset: 0, + fragments: [ + { + kind: 'para', + blockId: block.id, + fromLine: 0, + toLine: 1, + x: 0, + y: 0, + width: 50, + }, + ], + })); + + const painter = createDomPainter({ + blocks: [block], + measures: [measure], + flowMode: 'semantic', + headerProvider, + footerProvider, + }); + + painter.paint(makeLayout(2), mount); + + expect(headerProvider).not.toHaveBeenCalled(); + expect(footerProvider).not.toHaveBeenCalled(); + expect(mount.querySelector('.superdoc-page-header')).toBeNull(); + expect(mount.querySelector('.superdoc-page-footer')).toBeNull(); + }); }); diff --git a/packages/super-editor/src/components/SuperEditor.vue b/packages/super-editor/src/components/SuperEditor.vue index b822d6128..c4a7533ed 100644 --- a/packages/super-editor/src/components/SuperEditor.vue +++ b/packages/super-editor/src/components/SuperEditor.vue @@ -1069,13 +1069,16 @@ onBeforeUnmount(() => { @@ -171,6 +199,51 @@ const closeSidebar = () => { gap: 12px; } +.dev-sidebar__section { + display: grid; + gap: 10px; +} + +.dev-sidebar__section + .dev-sidebar__section { + border-top: 1px solid rgba(148, 163, 184, 0.45); + margin-top: 4px; + padding-top: 18px; +} + +.dev-sidebar__section-title { + margin: 0; + display: inline-flex; + align-items: center; + gap: 8px; + font-size: 15px; + font-weight: 700; + color: #1e293b; +} + +.dev-sidebar__section-icon { + width: 16px; + height: 16px; + border-radius: 4px; + display: inline-flex; + align-items: center; + justify-content: center; + font-size: 10px; + font-weight: 700; + line-height: 1; +} + +.dev-sidebar__section-icon--layout { + border: 1px solid rgba(59, 130, 246, 0.5); + color: #1d4ed8; + background: rgba(59, 130, 246, 0.12); +} + +.dev-sidebar__section-icon--word { + border: 1px solid rgba(37, 99, 235, 0.6); + color: #ffffff; + background: #2563eb; +} + .dev-sidebar__actions { display: grid; gap: 8px;