edgepress 2026.10.1 → 2026.10.13
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 +21 -0
- package/README.md +216 -16
- package/config.yml +99 -127
- package/content/assets/edgepress/brand/avatar.png +0 -0
- package/content/assets/edgepress/brand/deploy-esa.svg +1 -0
- package/content/assets/edgepress/brand/home-hero-768.webp +0 -0
- package/content/assets/edgepress/brand/home-hero.png +0 -0
- package/content/assets/edgepress/brand/home-hero.webp +0 -0
- package/content/assets/edgepress/brand/mascot.png +0 -0
- package/content/assets/edgepress/brand/mascot.webp +0 -0
- package/content/assets/edgepress/brand/og-image.png +0 -0
- package/content/assets/edgepress/brand/og-image.webp +0 -0
- package/content/assets/edgepress/brand/sources/edgepress-avatar-master.png +0 -0
- package/content/assets/edgepress/brand/sources/export-manifest.json +181 -0
- package/content/assets/edgepress/brand/sources/prompts.json +12 -0
- package/content/assets/edgepress/favicon/apple-touch-icon.png +0 -0
- package/content/assets/edgepress/favicon/favicon-16.png +0 -0
- package/content/assets/edgepress/favicon/favicon-32.png +0 -0
- package/content/assets/edgepress/favicon/favicon-48.png +0 -0
- package/content/assets/edgepress/favicon/favicon-64.png +0 -0
- package/content/assets/edgepress/favicon/favicon.ico +0 -0
- package/content/assets/edgepress/icons/LICENSE.txt +43 -0
- package/content/assets/edgepress/icons/arrow-right.svg +16 -0
- package/content/assets/edgepress/icons/arrow-up-right.svg +16 -0
- package/content/assets/edgepress/icons/book-open.svg +16 -0
- package/content/assets/edgepress/icons/check-circle.svg +16 -0
- package/content/assets/edgepress/icons/check.svg +15 -0
- package/content/assets/edgepress/icons/cloud.svg +15 -0
- package/content/assets/edgepress/icons/code.svg +16 -0
- package/content/assets/edgepress/icons/cpu.svg +28 -0
- package/content/assets/edgepress/icons/file-text.svg +19 -0
- package/content/assets/edgepress/icons/github.svg +17 -0
- package/content/assets/edgepress/icons/globe.svg +17 -0
- package/content/assets/edgepress/icons/help-circle.svg +17 -0
- package/content/assets/edgepress/icons/home.svg +16 -0
- package/content/assets/edgepress/icons/layers.svg +17 -0
- package/content/assets/edgepress/icons/layout-grid.svg +18 -0
- package/content/assets/edgepress/icons/mail.svg +16 -0
- package/content/assets/edgepress/icons/newspaper.svg +18 -0
- package/content/assets/edgepress/icons/palette.svg +19 -0
- package/content/assets/edgepress/icons/plug.svg +18 -0
- package/content/assets/edgepress/icons/provenance.json +8 -0
- package/content/assets/edgepress/icons/puzzle.svg +15 -0
- package/content/assets/edgepress/icons/rocket.svg +18 -0
- package/content/assets/edgepress/icons/rss.svg +17 -0
- package/content/assets/edgepress/icons/search.svg +16 -0
- package/content/assets/edgepress/icons/settings.svg +16 -0
- package/content/assets/edgepress/icons/shield-check.svg +16 -0
- package/content/assets/edgepress/icons/sparkles.svg +18 -0
- package/content/assets/edgepress/icons/terminal.svg +16 -0
- package/content/assets/edgepress/icons/x.svg +16 -0
- package/{static → content/assets}/edgepress-markdown-guide.svg +1 -1
- package/content/assets/images/previews/edgepress-en.webp +0 -0
- package/content/assets/images/previews/edgepress-revisions-en.png +0 -0
- package/content/assets/images/previews/edgepress-workflow-en.90f4650b21013786.gif +0 -0
- package/content/assets/images/previews/edgepress-workflow-zh.41f1e0a685f47267.gif +0 -0
- package/content/assets/images/previews/edgepress-zh.webp +0 -0
- package/content/assets/images/previews/provenance.json +49 -0
- package/content/guides/markdown-media.md +38 -0
- package/content/guides/platforms.md +15 -0
- package/content/guides/product-demos.md +37 -0
- package/content/guides/zh-cn.md +62 -0
- package/content/pages/deployment/en.json +74 -0
- package/content/pages/deployment/zh-cn.json +74 -0
- package/content/pages/home/en.json +248 -0
- package/content/pages/home/zh-cn.json +248 -0
- package/content/pages/local-tools/en.json +14 -0
- package/content/pages/local-tools/zh-cn.json +14 -0
- package/content/pages/page-authoring/en.json +107 -0
- package/content/pages/page-authoring/zh-cn.json +107 -0
- package/content/pages/plugin-development/en.json +202 -0
- package/content/pages/plugin-development/zh-cn.json +202 -0
- package/content/pages/privacy-policy/en.json +171 -0
- package/content/pages/privacy-policy/zh-cn.json +171 -0
- package/content/pages/project-introduction/en.json +158 -0
- package/content/pages/project-introduction/zh-cn.json +158 -0
- package/content/pages/quick-start/en.json +325 -0
- package/content/pages/quick-start/zh-cn.json +325 -0
- package/content/pages/theme-development/en.json +353 -0
- package/content/pages/theme-development/zh-cn.json +349 -0
- package/content/posts/2026-09-24-edgepress-release-2026-9-1/en.md +1 -1
- package/content/posts/2026-09-24-edgepress-release-2026-9-1/zh-cn.md +1 -1
- package/content/posts/2026-10-03-edgepress-release-2026-10-3/en.md +16 -0
- package/content/posts/2026-10-03-edgepress-release-2026-10-3/zh-cn.md +16 -0
- package/content/posts/2026-10-03-edgepress-release-202610-4/en.md +22 -0
- package/content/posts/2026-10-03-edgepress-release-202610-4/zh-cn.md +20 -0
- package/content/posts/2026-10-03-edgepress-release-202610-5/en.md +15 -0
- package/content/posts/2026-10-03-edgepress-release-202610-5/zh-cn.md +15 -0
- package/edgeone.json +43 -0
- package/esa.jsonc +9 -0
- package/languages/base/en.json +75 -10
- package/languages/packs/zh-CN.json +74 -9
- package/netlify.toml +6 -0
- package/package.json +37 -6
- package/plugins/consent/index.js +10 -17
- package/project-compatibility.json +142 -25
- package/schemas/pages.schema.json +74 -0
- package/src/assets.js +146 -18
- package/src/browser-audit.js +158 -0
- package/src/build.js +34 -9
- package/src/cli.js +46 -41
- package/src/config.js +114 -63
- package/src/consent-csp.js +137 -0
- package/src/content.js +55 -30
- package/src/data-saver.js +194 -0
- package/src/deploy.js +28 -0
- package/src/document.js +44 -0
- package/src/embed.js +60 -0
- package/src/generators.js +101 -59
- package/src/i18n.js +19 -3
- package/src/icons.js +5 -4
- package/src/image-dimensions.js +17 -0
- package/src/local-tool.js +29 -0
- package/src/maintenance.js +25 -8
- package/src/markdown-fixture.js +20 -0
- package/src/markdown.js +80 -16
- package/src/media-assets.js +19 -0
- package/src/media-viewer.js +47 -0
- package/src/oembed.js +53 -0
- package/src/page-blocks.js +124 -54
- package/src/page-check.js +29 -39
- package/src/plugin-api.js +7 -1
- package/src/post-details.js +60 -0
- package/src/post-template.js +5 -0
- package/src/publication-time.js +47 -0
- package/src/report-path.js +19 -0
- package/src/report-visuals.js +91 -18
- package/src/revision-cards.js +30 -0
- package/src/revision-images.js +44 -0
- package/src/revisions.js +77 -0
- package/src/rss.js +100 -0
- package/src/service-block.js +18 -0
- package/src/theme.js +59 -9
- package/src/video-metadata.js +36 -0
- package/static/edgepress/data-saver.css +12 -0
- package/static/edgepress/data-saver.js +333 -0
- package/static/edgepress/document-viewer.js +35 -0
- package/static/edgepress/external-links-policy.js +14 -0
- package/static/edgepress/external-links.css +13 -0
- package/static/edgepress/external-links.js +120 -0
- package/static/edgepress/fonts/faces.css +2217 -0
- package/static/edgepress/fonts/noto-sans-OFL.txt +93 -0
- package/static/edgepress/fonts/noto-sans-cyrillic-ext-wght-normal.82ff72b28e6610f2.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-cyrillic-wght-normal.6ab64433de6077ca.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-devanagari-wght-normal.ea6a530b75d93de7.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-greek-ext-wght-normal.790b255c10c32e0b.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-greek-wght-normal.7f655207c2f6c140.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-latin-ext-wght-normal.6d5a79315528df19.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-latin-wght-normal.51ca196f49a33e79.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-sc-100-wght-normal.ad5f29028d5699a5.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-sc-101-wght-normal.e189b81fc22ca6af.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-sc-102-wght-normal.941c0c2971fc1d6d.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-sc-103-wght-normal.bafbd591af6493ff.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-sc-104-wght-normal.b0bf80ced89dd61b.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-sc-105-wght-normal.8d8b4e6f4a00621d.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-sc-106-wght-normal.bae74acc629d2e74.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-sc-107-wght-normal.a42eff5010de7b0d.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-sc-108-wght-normal.91882203459867be.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-sc-109-wght-normal.a91f8b038e5ce108.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-sc-110-wght-normal.bbca9081655c4523.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-sc-111-wght-normal.372be010ac0e87cb.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-sc-112-wght-normal.34a4580239eae8dc.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-sc-113-wght-normal.614ab8f616352583.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-sc-114-wght-normal.cf7b8344807a316e.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-sc-115-wght-normal.fb29957c7adccb9a.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-sc-116-wght-normal.a98dc5a726f82141.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-sc-117-wght-normal.d94b5af0c14838af.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-sc-118-wght-normal.76c9ddb85ae4578c.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-sc-119-wght-normal.b3b2374881f4cd93.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-sc-21-wght-normal.c1409c43e55ecff2.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-sc-22-wght-normal.77037ed42ff4e7b0.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-sc-23-wght-normal.38d0df21ac374e93.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-sc-24-wght-normal.8fa21e7e457acf3e.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-sc-25-wght-normal.f760b7a3871d214d.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-sc-26-wght-normal.be8386e7bd7facfe.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-sc-27-wght-normal.4895abcaddbd162e.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-sc-28-wght-normal.82540b6201a9b9ad.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-sc-29-wght-normal.ff21d8fcc60ce391.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-sc-30-wght-normal.1f76f6ffded00b9e.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-sc-31-wght-normal.e6c25be1bb08a63a.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-sc-32-wght-normal.0cda750d7d04a4e1.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-sc-33-wght-normal.b9732b5606bd87c5.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-sc-34-wght-normal.38aa326b4c84902a.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-sc-35-wght-normal.bd98226c8e0aa808.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-sc-36-wght-normal.0db1ca695a491d37.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-sc-37-wght-normal.8df409a4e0a237c2.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-sc-38-wght-normal.672d2d994994a3c3.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-sc-39-wght-normal.272d6d0e153f56e8.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-sc-4-wght-normal.9fafd93ac77b81ff.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-sc-40-wght-normal.5b9ef1ed48420aa7.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-sc-41-wght-normal.30aee47f660fd88a.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-sc-42-wght-normal.ec0014830db682b6.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-sc-43-wght-normal.43df2b5dad9bb835.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-sc-44-wght-normal.acaa6195041b1a92.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-sc-45-wght-normal.8cdb9903f770db21.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-sc-46-wght-normal.6590d0a6fe94571c.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-sc-47-wght-normal.b05afdd32b3bf722.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-sc-48-wght-normal.c9585f7d070700be.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-sc-49-wght-normal.4943895a4dfa5dcf.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-sc-5-wght-normal.e64f57937e3ae4fe.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-sc-50-wght-normal.13e51063d7743cfe.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-sc-51-wght-normal.fd4df1f02c2cb4d7.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-sc-52-wght-normal.06999a39c4c3a7ea.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-sc-53-wght-normal.767b7b9864658d91.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-sc-54-wght-normal.4cd00537a46f6f1a.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-sc-55-wght-normal.0118ec5d8c30b197.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-sc-56-wght-normal.a3e4a78929826930.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-sc-57-wght-normal.7623501009e4a6db.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-sc-58-wght-normal.56ae1a59a32e4a25.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-sc-59-wght-normal.7a377a40e99fd9bc.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-sc-6-wght-normal.17e15c4f0a5bfd92.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-sc-60-wght-normal.3657f66babdf6aff.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-sc-61-wght-normal.23919a2eb11d25dd.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-sc-62-wght-normal.a901edf25bc1cfae.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-sc-63-wght-normal.92f56f713cc0b62f.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-sc-64-wght-normal.4929c9f39a5a4bd9.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-sc-65-wght-normal.8daa61282fb62a11.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-sc-66-wght-normal.6479166925eb462b.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-sc-67-wght-normal.a6b0e11c4c761ffd.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-sc-68-wght-normal.65b77f1fe9d68dae.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-sc-69-wght-normal.b34f765cbbf4d02e.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-sc-70-wght-normal.a99d55c00e4f95b7.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-sc-71-wght-normal.3298bed30dc8d233.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-sc-72-wght-normal.6597209ca9707726.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-sc-73-wght-normal.e7ac1d4930eabd78.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-sc-74-wght-normal.1f0287be83a28e7a.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-sc-75-wght-normal.d9a4bb7bcc2be578.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-sc-76-wght-normal.5a019946bcadcec6.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-sc-77-wght-normal.7c445e85d04f5319.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-sc-78-wght-normal.7d4afb70f641781c.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-sc-79-wght-normal.5798753a721bb4ec.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-sc-80-wght-normal.db887159ca6e46f3.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-sc-81-wght-normal.71e5743bbd225b3d.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-sc-82-wght-normal.8c5094c3785fd04f.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-sc-83-wght-normal.4a60a1ccba905284.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-sc-84-wght-normal.3ed54e59ef4758e3.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-sc-85-wght-normal.e87e0a1342e1778c.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-sc-86-wght-normal.6d3ac3e20959dbcb.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-sc-87-wght-normal.0eeffd5daad5c17b.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-sc-88-wght-normal.b1d9582dfa268463.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-sc-89-wght-normal.8b437aeb9e9fcb5a.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-sc-90-wght-normal.83b0a59e0a5e045a.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-sc-91-wght-normal.382f72b98d4bb9d7.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-sc-97-wght-normal.5e52856457c5cdab.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-sc-98-wght-normal.ffded4e7fd560d93.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-sc-99-wght-normal.54e302c23f11a8fd.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-sc-OFL.txt +93 -0
- package/static/edgepress/fonts/noto-sans-sc-cyrillic-wght-normal.5932d441e7126669.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-sc-latin-ext-wght-normal.e316201f3fc493d1.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-sc-latin-wght-normal.c1d919c8701a86ed.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-sc-vietnamese-wght-normal.cba3d00c8db989f0.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-tc-0-wght-normal.63c21eda5d20203f.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-tc-100-wght-normal.87a7c76301c24a28.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-tc-101-wght-normal.c9e83fc4ffd53746.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-tc-102-wght-normal.9a00ae0e5735852b.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-tc-103-wght-normal.7cfe93b3221f4def.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-tc-104-wght-normal.2d5ac7497d84cb99.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-tc-105-wght-normal.0989885bb16bcd9b.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-tc-106-wght-normal.1faa921b035064e7.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-tc-107-wght-normal.2f6006745ae16d69.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-tc-108-wght-normal.a61e48c10a2cf4b9.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-tc-109-wght-normal.f28c767724736d2d.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-tc-110-wght-normal.4b04bd88c3dc7c5d.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-tc-111-wght-normal.b6251e1967932a52.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-tc-112-wght-normal.e264513b32f90660.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-tc-113-wght-normal.46cb347d88f3338e.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-tc-114-wght-normal.9f9e1e0d0ab2aeb4.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-tc-115-wght-normal.653dd2ab7d551b40.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-tc-116-wght-normal.4718bf3627191297.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-tc-117-wght-normal.b241a9409682af5f.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-tc-118-wght-normal.fda2800dc1c06852.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-tc-119-wght-normal.948af36bd63a72c7.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-tc-19-wght-normal.62191cdc3a10e60a.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-tc-20-wght-normal.65c90892c2e01657.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-tc-21-wght-normal.d3f59b094c334866.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-tc-22-wght-normal.b1983a041d7e4e1f.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-tc-23-wght-normal.16e2163b66c7732a.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-tc-24-wght-normal.72c8561976116537.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-tc-25-wght-normal.b2948d928ce4dd54.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-tc-26-wght-normal.d583236b90abedc9.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-tc-27-wght-normal.7d6073ba07597c9b.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-tc-28-wght-normal.517204d716b089cd.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-tc-29-wght-normal.10189a8f8ca38099.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-tc-30-wght-normal.695a71d72dfab3f7.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-tc-31-wght-normal.868d59473ef0e73c.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-tc-32-wght-normal.92efa2d4b3702ec0.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-tc-33-wght-normal.d2274bcd5636197d.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-tc-34-wght-normal.1ac8c3718ee2eed2.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-tc-35-wght-normal.00b3df730d5a4c73.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-tc-36-wght-normal.6963feb6c628b81f.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-tc-37-wght-normal.a1b4bd294af24cd1.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-tc-38-wght-normal.8edb2f7f2955921b.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-tc-39-wght-normal.37bf7394eb3148c6.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-tc-40-wght-normal.3efa655bc48f9133.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-tc-41-wght-normal.48d47014544ebd5d.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-tc-42-wght-normal.214c733a59d423e7.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-tc-43-wght-normal.c2c9876f138bb20f.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-tc-44-wght-normal.28928bfea29db06a.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-tc-45-wght-normal.bf3743f8155fa9d2.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-tc-46-wght-normal.06970903593781c3.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-tc-47-wght-normal.8932ed83a91794d4.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-tc-48-wght-normal.dfd9225628bcf234.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-tc-49-wght-normal.c27d341ad39d44eb.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-tc-50-wght-normal.01a0f4e7dfba0a56.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-tc-51-wght-normal.5a30022af3d30e0f.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-tc-52-wght-normal.4884f032342a3274.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-tc-53-wght-normal.537adcae44511270.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-tc-54-wght-normal.6bc8c7d3e47c6fde.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-tc-55-wght-normal.725a48711f1e1997.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-tc-56-wght-normal.f43ecaa73e0cd8fc.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-tc-57-wght-normal.15b24e9d42a3270f.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-tc-58-wght-normal.9b2d8a71667d3da6.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-tc-59-wght-normal.a68535a6b3289d89.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-tc-6-wght-normal.32cece1ae80c4f2b.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-tc-60-wght-normal.3b2c56a921d1cad4.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-tc-61-wght-normal.54f51f80a0740878.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-tc-62-wght-normal.ba029c5bb6c07bf1.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-tc-63-wght-normal.380919beec4c7f3d.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-tc-64-wght-normal.f96fe0be479d201f.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-tc-65-wght-normal.7d544b80701fe678.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-tc-66-wght-normal.fab669c07a1e3bee.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-tc-67-wght-normal.0001bb744743fdfa.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-tc-68-wght-normal.1a57eda03886b43c.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-tc-69-wght-normal.017fecc749bcd6d5.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-tc-7-wght-normal.cd0f0cac743dbbfe.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-tc-70-wght-normal.79fb9d70b9534ec7.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-tc-71-wght-normal.04a6ea581b5f1195.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-tc-72-wght-normal.d3a4eb35499157b7.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-tc-73-wght-normal.d81f8484404ee465.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-tc-74-wght-normal.4a5da9e0c2702f7d.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-tc-75-wght-normal.8b87f9c6c164513a.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-tc-76-wght-normal.a71463dbfd9fa7b5.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-tc-77-wght-normal.d094414ea752cd52.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-tc-78-wght-normal.04f7c97bd9cbc73b.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-tc-79-wght-normal.1ed089d037a02049.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-tc-8-wght-normal.d052677dfb5db5bd.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-tc-80-wght-normal.6d41ca59cfd50442.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-tc-81-wght-normal.f09bd0afa8a58114.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-tc-82-wght-normal.7920841c526b1b02.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-tc-83-wght-normal.0c6f7f82db8da0d8.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-tc-84-wght-normal.f37c0a853b31265e.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-tc-85-wght-normal.4173bff087653b82.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-tc-86-wght-normal.dac35605ebec15c4.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-tc-87-wght-normal.8ac85fdd0b15a6bd.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-tc-88-wght-normal.61625b0b21e8407f.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-tc-89-wght-normal.a1da3dccbf00698e.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-tc-90-wght-normal.bf08f0355222a782.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-tc-91-wght-normal.2ecf295bba2bfaa3.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-tc-92-wght-normal.36508326bb68115f.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-tc-97-wght-normal.c712899e0fc9e947.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-tc-98-wght-normal.13a648e521d18d7f.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-tc-99-wght-normal.7f8c9a777e64159a.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-tc-OFL.txt +93 -0
- package/static/edgepress/fonts/noto-sans-tc-cyrillic-wght-normal.dd0980ced8e46cb3.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-tc-latin-ext-wght-normal.817d96335dc52ce6.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-tc-latin-wght-normal.50cb5fb0a522cfaf.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-tc-vietnamese-wght-normal.2a77fb7b70fa304d.woff2 +0 -0
- package/static/edgepress/fonts/noto-sans-vietnamese-wght-normal.791e3f213c155a5b.woff2 +0 -0
- package/static/edgepress/fonts/noto-serif-OFL.txt +93 -0
- package/static/edgepress/fonts/noto-serif-cyrillic-ext-wght-normal.7c7afb20d33e30ec.woff2 +0 -0
- package/static/edgepress/fonts/noto-serif-cyrillic-wght-normal.54e3bd0c4a002b0f.woff2 +0 -0
- package/static/edgepress/fonts/noto-serif-greek-ext-wght-normal.9b3f4c4ff3adefd2.woff2 +0 -0
- package/static/edgepress/fonts/noto-serif-greek-wght-normal.1252037b6f1d6e86.woff2 +0 -0
- package/static/edgepress/fonts/noto-serif-latin-ext-wght-normal.2654f8d1261a420f.woff2 +0 -0
- package/static/edgepress/fonts/noto-serif-latin-wght-normal.46281456234014ce.woff2 +0 -0
- package/static/edgepress/fonts/noto-serif-math-wght-normal.d4a6aba2cb4636b3.woff2 +0 -0
- package/static/edgepress/fonts/noto-serif-vietnamese-wght-normal.fb6a5ed734c2fbe3.woff2 +0 -0
- package/static/edgepress/fonts/provenance.json +1356 -0
- package/static/edgepress/fonts.css +14 -0
- package/static/edgepress/image-viewer.js +23 -0
- package/static/edgepress/images.css +4 -0
- package/static/edgepress/link-directory.css +14 -0
- package/static/edgepress/local-time.js +12 -0
- package/static/edgepress/media-viewer.css +1 -0
- package/static/edgepress/media-viewer.js +111 -0
- package/static/edgepress/navigation-select.js +14 -0
- package/static/edgepress/oembed.js +129 -0
- package/static/edgepress/plugins/backend/external-api.js +4 -0
- package/static/edgepress/plugins/backend/external-widget.js +13 -0
- package/static/edgepress/plugins/consent/choices.js +47 -0
- package/static/edgepress/plugins/consent/manager.js +56 -74
- package/static/edgepress/project-demo.js +93 -0
- package/static/edgepress/search.js +2 -2
- package/static/edgepress/services-consent.js +3 -0
- package/static/edgepress/services.js +15 -0
- package/static/edgepress/webmcp.js +86 -0
- package/tests/assets-runtime.test.mjs +91 -0
- package/tests/audit.test.mjs +142 -0
- package/tests/brand-regression.test.mjs +110 -0
- package/tests/consent-browser.test.mjs +132 -0
- package/tests/consent-config.test.mjs +85 -0
- package/tests/consent-csp.test.mjs +90 -0
- package/tests/data-saver-loading.test.mjs +158 -0
- package/tests/data-saver-network.test.mjs +99 -0
- package/tests/data-saver.test.mjs +294 -0
- package/tests/document-package.test.mjs +28 -0
- package/tests/document-viewer.test.mjs +152 -0
- package/tests/edge-issues.test.mjs +65 -0
- package/tests/embed-syntax.test.mjs +68 -0
- package/tests/external-links.test.mjs +198 -0
- package/tests/fixtures/recording.mp4 +0 -0
- package/tests/fonts.test.mjs +99 -0
- package/tests/image-dimensions.test.mjs +15 -0
- package/tests/image-viewer.test.mjs +36 -0
- package/tests/json-pages.test.mjs +66 -0
- package/tests/link-directory.test.mjs +51 -0
- package/tests/local-tool.test.mjs +34 -0
- package/tests/markdown-media.test.mjs +119 -0
- package/tests/media-block.test.mjs +102 -0
- package/tests/navigation.test.mjs +87 -0
- package/tests/oembed-layout.test.mjs +98 -0
- package/tests/oembed.test.mjs +96 -0
- package/tests/page-authoring.test.mjs +27 -0
- package/tests/page-formatting.test.mjs +37 -0
- package/tests/post-list.test.mjs +96 -0
- package/tests/project-demo.test.mjs +73 -0
- package/tests/publication-time.test.mjs +43 -0
- package/tests/release-version.test.mjs +28 -0
- package/tests/revisions.test.mjs +93 -0
- package/tests/rss.test.mjs +54 -0
- package/tests/services.test.mjs +35 -0
- package/tests/theme-layout.test.mjs +95 -0
- package/tests/video-metadata.test.mjs +28 -0
- package/tests/webmcp.test.mjs +121 -0
- package/tests/writing.test.mjs +101 -0
- package/themes/atelier/assets/style.css +144 -22
- package/themes/atelier/layouts/partials/head.html +8 -1
- package/themes/atelier/layouts/partials/header.html +3 -1
- package/themes/default/assets/data-saver-theme.css +2 -0
- package/themes/default/assets/style.css +139 -23
- package/themes/default/layouts/partials/head.html +9 -2
- package/themes/default/layouts/partials/header.html +3 -1
- package/themes/default/theme.json +1 -0
- package/themes/folio/README.md +7 -0
- package/themes/folio/assets/data-saver-theme.css +3 -0
- package/themes/folio/assets/folio/navigation.js +10 -0
- package/themes/folio/assets/style.css +516 -0
- package/themes/folio/layouts/layout.html +15 -0
- package/themes/folio/layouts/partials/footer.html +6 -0
- package/themes/folio/layouts/partials/head.html +30 -0
- package/themes/folio/layouts/partials/header.html +8 -0
- package/themes/folio/theme.json +7 -0
- package/themes/signal/assets/style.css +143 -21
- package/themes/signal/layouts/partials/head.html +8 -1
- package/themes/signal/layouts/partials/header.html +3 -1
- package/tools/README.md +14 -0
- package/tools/assets/format-pages.mjs +56 -0
- package/tools/assets/generate-brand-assets.py +160 -0
- package/tools/assets/sync-fonts.mjs +71 -0
- package/tools/assets/sync-icons.mjs +17 -0
- package/tools/assets/video-preview.mjs +15 -0
- package/tools/recordings/capture-document-demo.mjs +147 -0
- package/tools/recordings/demo-workbench.mjs +57 -0
- package/tools/recordings/desktop-recording.mjs +92 -0
- package/tools/recordings/provision-vm.sh +18 -0
- package/tools/recordings/start-vm-desktop.sh +28 -0
- package/tools/recordings/vm-desktop.mjs +124 -0
- package/tools/verification/README.md +44 -0
- package/tools/verification/data-saver-site.mjs +405 -0
- package/tools/verification/page-performance.mjs +76 -0
- package/tools/verification/verify-brand-assets.py +52 -0
- package/tools/verification/verify-brand.mjs +180 -0
- package/vercel.json +36 -0
- package/wrangler.jsonc +5 -7
- package/content/pages/home/en.md +0 -99
- package/content/pages/home/zh-cn.md +0 -99
- package/content/pages/plugin-development/en.md +0 -109
- package/content/pages/plugin-development/zh-cn.md +0 -109
- package/content/pages/privacy-policy/en.md +0 -93
- package/content/pages/privacy-policy/zh-cn.md +0 -93
- package/content/pages/project-introduction/en.md +0 -88
- package/content/pages/project-introduction/zh-cn.md +0 -88
- package/content/pages/quick-start/en.md +0 -153
- package/content/pages/quick-start/zh-cn.md +0 -153
- package/content/pages/theme-development/en.md +0 -132
- package/content/pages/theme-development/zh-cn.md +0 -132
- package/src/worker.js +0 -139
- package/static/edgepress/brand/edgepress-mark.svg +0 -7
- package/static/edgepress/brand/mascot-209.webp +0 -0
- package/static/edgepress/brand/mascot-418.png +0 -0
- package/static/edgepress/brand/mascot-418.webp +0 -0
- package/static/edgepress/brand/mascot.png +0 -0
- package/static/edgepress/brand/og-image.png +0 -0
- package/static/edgepress/brand/og-image.svg +0 -46
- package/static/edgepress/icons.svg +0 -24
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { readFile, readdir, mkdir, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { createRequire } from 'node:module';
|
|
5
|
+
import { basename, dirname, extname, resolve, sep } from 'node:path';
|
|
6
|
+
import { createServer } from 'node:http';
|
|
7
|
+
import { gzipSync } from 'node:zlib';
|
|
8
|
+
|
|
9
|
+
export function registerDataSaverTests(siteRoot, options = {}) {
|
|
10
|
+
const root = dirname(resolve(siteRoot));
|
|
11
|
+
const selected = [basename(resolve(siteRoot))];
|
|
12
|
+
const { chromium } = createRequire(resolve(siteRoot, 'package.json'))('playwright');
|
|
13
|
+
|
|
14
|
+
const types = { '.js': 'text/javascript', '.css': 'text/css', '.json': 'application/json', '.svg': 'image/svg+xml', '.webp': 'image/webp', '.png': 'image/png', '.jpg': 'image/jpeg', '.woff2': 'font/woff2', '.mp4': 'video/mp4', '.pdf': 'application/pdf' };
|
|
15
|
+
|
|
16
|
+
for (const site of selected) test(site + ' remains readable and themed without JavaScript or third-party requests',async()=>{
|
|
17
|
+
const require = createRequire(resolve(root,site,'package.json'));
|
|
18
|
+
const config = require('yaml').parse(await readFile(resolve(root,site,'config.yml'),'utf8'));
|
|
19
|
+
const directory = resolve(root,site,'dist');
|
|
20
|
+
const htmlFiles=async folder=>{
|
|
21
|
+
for(const entry of await readdir(folder,{withFileTypes:true})){
|
|
22
|
+
const file=resolve(folder,entry.name);
|
|
23
|
+
if(entry.isDirectory())await htmlFiles(file);
|
|
24
|
+
else if(entry.name.endsWith('.html'))assert((await readFile(file)).subarray(0,1024).includes(Buffer.from('<meta charset="utf-8">')),file);
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
await htmlFiles(directory);
|
|
28
|
+
const browser = await chromium.launch({headless:true,args:['--disable-extensions']});
|
|
29
|
+
try {
|
|
30
|
+
for (const [width,colorScheme] of [[390,'dark'],[1280,'light']]) {
|
|
31
|
+
const context = await browser.newContext({javaScriptEnabled:false,viewport:{width,height:844},colorScheme});
|
|
32
|
+
const requests = [];
|
|
33
|
+
await context.route('**/*',async route=>{
|
|
34
|
+
const url = new URL(route.request().url());
|
|
35
|
+
requests.push(url.href);
|
|
36
|
+
assert.equal(url.origin,config.site.url,'No-JS must not enable optional services');
|
|
37
|
+
const file = resolve(directory,url.pathname.slice(1)+(url.pathname.endsWith('/')?'index.html':''));
|
|
38
|
+
if (!file.startsWith(directory+sep)) return route.abort();
|
|
39
|
+
try { return await route.fulfill({contentType:types[extname(file)] || 'text/html; charset=utf-8',body:await readFile(file)}); }
|
|
40
|
+
catch { return route.fulfill({status:404,body:''}); }
|
|
41
|
+
});
|
|
42
|
+
const page = await context.newPage();
|
|
43
|
+
const paths = options.noJsPaths || ['/'];
|
|
44
|
+
for (const path of paths) {
|
|
45
|
+
await page.goto(config.site.url+path);
|
|
46
|
+
assert.equal(await page.locator('main').count(),1);
|
|
47
|
+
assert.equal(await page.locator('main').isVisible(),true);
|
|
48
|
+
assert((await page.locator('main').innerText()).trim().length > 10);
|
|
49
|
+
assert.equal(await page.locator('body>header,.site-shell>header').isVisible(),true);
|
|
50
|
+
assert.equal(await page.locator('body>footer,.site-shell>footer').isVisible(),true);
|
|
51
|
+
assert.equal(await page.locator('[data-data-wait]').isVisible(),false);
|
|
52
|
+
assert(await page.evaluate(()=>document.documentElement.scrollWidth<=innerWidth));
|
|
53
|
+
assert.notEqual(await page.locator('body').evaluate(element=>getComputedStyle(element).fontFamily),'"Times New Roman"');
|
|
54
|
+
assert(!requests.some(url=>/\.js(?:\?|$)|\/api(?:\?|$)/.test(url)),requests.join('\n'));
|
|
55
|
+
if (process.env.DATA_SAVER_SCREENSHOTS) {
|
|
56
|
+
await mkdir(process.env.DATA_SAVER_SCREENSHOTS,{recursive:true});
|
|
57
|
+
await page.screenshot({path:resolve(process.env.DATA_SAVER_SCREENSHOTS,site+'-nojs-'+width+(path.includes('/2026/')?'-article':'-home')+'.png'),fullPage:true});
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
await context.close();
|
|
61
|
+
}
|
|
62
|
+
}finally{await browser.close();}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
for (const site of selected) test(site + ' exposes progress and manual text switching on actual pages over cold weak connections', {timeout:180000}, async () => {
|
|
66
|
+
const directory = resolve(root,site,'dist');
|
|
67
|
+
const server = createServer(async (request,response) => {
|
|
68
|
+
const address = new URL(request.url,'http://localhost');
|
|
69
|
+
if (address.pathname === '/api') return response.writeHead(200,{'Content-Type':'application/json'}).end(JSON.stringify({user:null,isAdmin:false,items:[],next:null,loginAvailable:true}));
|
|
70
|
+
const file = resolve(directory,decodeURIComponent(address.pathname).replace(/^\//,'') + (address.pathname.endsWith('/') ? 'index.html' : ''));
|
|
71
|
+
if (!file.startsWith(directory + sep)) return response.writeHead(404).end();
|
|
72
|
+
try {
|
|
73
|
+
const body=await readFile(file),type=types[extname(file)] || 'text/html; charset=utf-8';
|
|
74
|
+
const compressed=/^(?:text\/|application\/json|image\/svg)/.test(type);
|
|
75
|
+
response.writeHead(200,{'Content-Type':type,'Cache-Control':'no-store',...(compressed?{'Content-Encoding':'gzip'}:{})}).end(compressed?gzipSync(body):body);
|
|
76
|
+
}
|
|
77
|
+
catch (error) { response.writeHead(error.code === 'ENOENT' ? 404 : 500).end(); }
|
|
78
|
+
});
|
|
79
|
+
await new Promise(resolve => server.listen(0,'127.0.0.1',resolve));
|
|
80
|
+
const origin = 'http://127.0.0.1:' + server.address().port;
|
|
81
|
+
const browser = await chromium.launch({headless:true,args:['--disable-extensions']});
|
|
82
|
+
const measurements = [];
|
|
83
|
+
try {
|
|
84
|
+
const paths = options.coldPaths || ['/'];
|
|
85
|
+
const profiles = [{bandwidthKBs:100,latencyMs:150},{bandwidthKBs:50,latencyMs:2000}];
|
|
86
|
+
for (const profile of profiles) for (const path of paths) {
|
|
87
|
+
const context = await browser.newContext({viewport:{width:390,height:844},colorScheme:path.includes('zh-TW') ? 'light' : 'dark'});
|
|
88
|
+
await context.addInitScript(() => Object.defineProperty(navigator,'connection',{get:() => ({saveData:true,effectiveType:'2g'})}));
|
|
89
|
+
await context.addInitScript(() => {
|
|
90
|
+
window.paintMeasurements = {largestContentfulPaint:0,lcpElement:''};
|
|
91
|
+
new PerformanceObserver(list=>{
|
|
92
|
+
const entry = list.getEntries().at(-1);
|
|
93
|
+
window.paintMeasurements = {largestContentfulPaint:entry.startTime,lcpElement:entry.element?.tagName || ''};
|
|
94
|
+
}).observe({type:'largest-contentful-paint',buffered:true});
|
|
95
|
+
});
|
|
96
|
+
await context.route(url => url.origin !== origin,route => route.abort());
|
|
97
|
+
const page = await context.newPage();
|
|
98
|
+
const session = await context.newCDPSession(page);
|
|
99
|
+
await session.send('Network.enable');
|
|
100
|
+
await session.send('Network.setCacheDisabled',{cacheDisabled:true});
|
|
101
|
+
await session.send('Network.emulateNetworkConditions',{offline:false,latency:profile.latencyMs,downloadThroughput:profile.bandwidthKBs*1024,uploadThroughput:profile.bandwidthKBs*1024});
|
|
102
|
+
let transferred = 0;
|
|
103
|
+
session.on('Network.loadingFinished',event => { transferred += event.encodedDataLength; });
|
|
104
|
+
const requests = [];
|
|
105
|
+
let navigations = 0;
|
|
106
|
+
let textRequestStart = 0;
|
|
107
|
+
page.on('request',request => {
|
|
108
|
+
if (request.isNavigationRequest() && request.frame() === page.mainFrame()) {
|
|
109
|
+
navigations++;
|
|
110
|
+
if (navigations === 2) textRequestStart = requests.length;
|
|
111
|
+
}
|
|
112
|
+
requests.push(request.url());
|
|
113
|
+
});
|
|
114
|
+
const started = performance.now();
|
|
115
|
+
await page.goto(origin + path,{waitUntil:'commit'});
|
|
116
|
+
const button = page.locator('[data-data-mode=text]');
|
|
117
|
+
await button.waitFor({state:'visible'});
|
|
118
|
+
const offeredAfterMs = Math.round(performance.now()-started);
|
|
119
|
+
assert(offeredAfterMs < 5000,site + path + ' entry too late: ' + offeredAfterMs);
|
|
120
|
+
assert.equal(await page.getAttribute('html','data-edgepress-data-mode'),'full');
|
|
121
|
+
assert.equal(await page.locator('[data-data-progress]').isVisible(),true);
|
|
122
|
+
const bounds = await page.locator('[data-data-offer]').boundingBox();
|
|
123
|
+
assert(bounds.x >= 0 && bounds.x + bounds.width <= 390);
|
|
124
|
+
if (process.env.DATA_SAVER_SCREENSHOTS) {
|
|
125
|
+
await mkdir(process.env.DATA_SAVER_SCREENSHOTS,{recursive:true});
|
|
126
|
+
await page.screenshot({path:resolve(process.env.DATA_SAVER_SCREENSHOTS,site + '-' + (path === '/' ? 'home' : path.includes('zh-SG') ? 'article-zh-SG' : path.includes('zh-TW') ? 'article-zh-TW' : 'article-en') + '-100KB-waiting.png')});
|
|
127
|
+
}
|
|
128
|
+
await button.focus();
|
|
129
|
+
await Promise.all([page.waitForNavigation({waitUntil:'domcontentloaded'}),page.keyboard.press('Enter')]);
|
|
130
|
+
await page.waitForFunction(() => document.documentElement.dataset.edgepressDataReady === 'true');
|
|
131
|
+
await page.locator('main').waitFor({state:'visible'});
|
|
132
|
+
await page.evaluate(() => new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve))));
|
|
133
|
+
const readableAfterMs = Math.round(performance.now()-started);
|
|
134
|
+
assert(readableAfterMs < 10000,site + path + ' text too late: ' + readableAfterMs);
|
|
135
|
+
assert.equal(await page.getAttribute('html','data-edgepress-data-mode'),'text');
|
|
136
|
+
assert.equal(await page.locator('body>header,.site-shell>header').isVisible(),true);
|
|
137
|
+
assert.equal(await page.locator('body>footer,.site-shell>footer').isVisible(),true);
|
|
138
|
+
assert((await page.locator('main').innerText()).trim().length > 10);
|
|
139
|
+
assert(!requests.slice(textRequestStart).some(url => /\.(?:woff2?|webp|jpe?g|mp4|pdf)(?:\?|$)|(?:document-viewer|image-viewer|gallery-runtime)\.[a-f0-9]+\.js/.test(url)),requests.join('\n'));
|
|
140
|
+
assert(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth));
|
|
141
|
+
const paint = await page.evaluate(()=>({...window.paintMeasurements,firstContentfulPaint:performance.getEntriesByName('first-contentful-paint')[0]?.startTime || 0}));
|
|
142
|
+
measurements.push({site,path,...profile,textEncoding:'gzip',offeredAfterMs,readableAfterMs,...paint,transferredBytes:transferred});
|
|
143
|
+
if (process.env.DATA_SAVER_SCREENSHOTS) await page.screenshot({path:resolve(process.env.DATA_SAVER_SCREENSHOTS,site + '-' + (path === '/' ? 'home' : path.includes('zh-SG') ? 'article-zh-SG' : path.includes('zh-TW') ? 'article-zh-TW' : 'article-en') + '-100KB-text.png'),fullPage:true});
|
|
144
|
+
await context.close();
|
|
145
|
+
}
|
|
146
|
+
console.log(JSON.stringify(measurements));
|
|
147
|
+
if (process.env.DATA_SAVER_SCREENSHOTS) await writeFile(resolve(process.env.DATA_SAVER_SCREENSHOTS,site + '-100KB.json'),JSON.stringify(measurements,null,2));
|
|
148
|
+
} finally {
|
|
149
|
+
await browser.close();
|
|
150
|
+
server.closeAllConnections();
|
|
151
|
+
await new Promise(resolve => server.close(resolve));
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
for (const site of selected) test(site + ' keeps a localized loading choice stable and disables its debug entry', async () => {
|
|
156
|
+
const require = createRequire(resolve(root,site,'package.json'));
|
|
157
|
+
const config = require('yaml').parse(await readFile(resolve(root,site,'config.yml'),'utf8'));
|
|
158
|
+
assert.equal(config.site.dataSaver.debug,false);
|
|
159
|
+
const locales = options.locales || [config.site.language];
|
|
160
|
+
const browser = await chromium.launch({headless:true,args:['--disable-extensions']});
|
|
161
|
+
try {
|
|
162
|
+
for (const locale of locales) {
|
|
163
|
+
const context = await browser.newContext({viewport:{width:390,height:844},colorScheme:'dark',reducedMotion:'reduce'});
|
|
164
|
+
let releaseStyles;
|
|
165
|
+
const released = new Promise(resolve => { releaseStyles = resolve; });
|
|
166
|
+
await context.route('**/*', async route => {
|
|
167
|
+
const address = new URL(route.request().url());
|
|
168
|
+
assert.equal(address.origin,config.site.url);
|
|
169
|
+
if (address.pathname === '/api') return route.fulfill({contentType:'application/json',body:JSON.stringify({user:null,isAdmin:false,items:[],next:null,loginAvailable:true})});
|
|
170
|
+
const file = resolve(root,site,'dist',decodeURIComponent(address.pathname).replace(/^\//,'') + (address.pathname.endsWith('/') ? 'index.html' : ''));
|
|
171
|
+
assert(file.startsWith(resolve(root,site,'dist') + sep));
|
|
172
|
+
try {
|
|
173
|
+
if (/\/style\.[a-f0-9]{16}\.css$/.test(address.pathname)) await released;
|
|
174
|
+
const buffer = await readFile(file);
|
|
175
|
+
const body = extname(file) === '.html' ? buffer.toString('utf8').replace(/"promptAfterMs":\d+/, '"promptAfterMs":300') : buffer;
|
|
176
|
+
return route.fulfill({contentType:types[extname(file)] || 'text/html; charset=utf-8',body});
|
|
177
|
+
} catch (error) { if (error.code !== 'ENOENT') throw error; return route.fulfill({status:404}); }
|
|
178
|
+
});
|
|
179
|
+
const page = await context.newPage();
|
|
180
|
+
const prefix = locale === config.site.language ? '' : '/' + locale;
|
|
181
|
+
const path = prefix + '/';
|
|
182
|
+
await page.goto(config.site.url + path,{waitUntil:'domcontentloaded'});
|
|
183
|
+
await page.locator('[data-data-offer]').waitFor({state:'visible'});
|
|
184
|
+
assert.equal(await page.locator('main').isVisible(),false);
|
|
185
|
+
const offer = page.locator('[data-data-offer]');
|
|
186
|
+
if (locale.startsWith('zh')) assert.match(await offer.innerText(),/文字省流版/);
|
|
187
|
+
if (locale === 'zh-TW') assert.match(await page.locator('[data-data-wait]').innerText(),/正在準備頁面|頁面樣式|首屏圖片/);
|
|
188
|
+
assert.equal(await page.locator('[data-data-progress]').isVisible(),true);
|
|
189
|
+
await page.locator('[data-data-mode=text]').focus();
|
|
190
|
+
releaseStyles();
|
|
191
|
+
await page.waitForTimeout(500);
|
|
192
|
+
assert.equal(await offer.isVisible(),true);
|
|
193
|
+
assert.equal(await page.locator('main').isVisible(),false);
|
|
194
|
+
const bounds = await offer.boundingBox();
|
|
195
|
+
assert(bounds.x >= 0 && bounds.x + bounds.width <= 390);
|
|
196
|
+
if (process.env.DATA_SAVER_SCREENSHOTS) {
|
|
197
|
+
await mkdir(process.env.DATA_SAVER_SCREENSHOTS,{recursive:true});
|
|
198
|
+
await page.screenshot({path:resolve(process.env.DATA_SAVER_SCREENSHOTS,site + '-' + locale + '-waiting.png')});
|
|
199
|
+
}
|
|
200
|
+
await Promise.all([page.waitForNavigation(),page.keyboard.press('Enter')]);
|
|
201
|
+
assert.equal(await page.getAttribute('html','data-edgepress-data-mode'),'text');
|
|
202
|
+
await page.waitForFunction(() => document.documentElement.dataset.edgepressDataReady === 'true');
|
|
203
|
+
assert.equal(await page.locator('[data-data-wait]').isVisible(),false);
|
|
204
|
+
await page.locator('main').waitFor({state:'visible'});
|
|
205
|
+
assert.equal(await page.locator('main').isVisible(),true);
|
|
206
|
+
assert.equal(await page.locator('body>header,.site-shell>header').isVisible(),true);
|
|
207
|
+
assert.equal(await page.locator('body>footer,.site-shell>footer').isVisible(),true);
|
|
208
|
+
await page.goto(config.site.url + path + '?data-save');
|
|
209
|
+
await page.waitForURL(config.site.url + prefix + '/404.html');
|
|
210
|
+
await page.waitForFunction(() => document.documentElement.dataset.edgepressDataReady === 'true');
|
|
211
|
+
await page.locator('main').waitFor({state:'visible'});
|
|
212
|
+
assert.match(await page.locator('main').innerText(),/404/);
|
|
213
|
+
await context.close();
|
|
214
|
+
}
|
|
215
|
+
} finally { await browser.close(); }
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
for (const site of selected) test(site + ' has explicitly chosen theme-owned text view, localized controls and retained external prompts', async () => {
|
|
220
|
+
const require = createRequire(resolve(root, site, 'package.json'));
|
|
221
|
+
const { parse } = require('yaml');
|
|
222
|
+
const config = parse(await readFile(resolve(root, site, 'config.yml'), 'utf8'));
|
|
223
|
+
assert.deepEqual(config.site.dataSaver, { enabled: true, debug: false, mode: 'auto', detectSlowConnection: true, respectBrowserPreference: false });
|
|
224
|
+
assert.equal(config.site.externalLinks.enabled, true);
|
|
225
|
+
const locales = options.locales || [config.site.language];
|
|
226
|
+
const browser = await chromium.launch({ headless: true, args: ['--disable-extensions'] });
|
|
227
|
+
try {
|
|
228
|
+
for (const locale of locales) for (const colorScheme of ['light', 'dark']) {
|
|
229
|
+
const context = await browser.newContext({ viewport: { width: 390, height: 844 }, colorScheme, reducedMotion: 'reduce' });
|
|
230
|
+
await context.addInitScript(() => Object.defineProperty(navigator, 'connection', { get: () => ({ saveData: true, effectiveType: '4g' }) }));
|
|
231
|
+
await context.addInitScript(() => localStorage.setItem('edgepress-data-mode','text'));
|
|
232
|
+
const requests = [], errors = [];
|
|
233
|
+
context.on('request', request => requests.push(request.url()));
|
|
234
|
+
const origin = config.site.url;
|
|
235
|
+
await context.route('**/*', async route => {
|
|
236
|
+
const url = new URL(route.request().url());
|
|
237
|
+
assert.equal(url.origin, origin, 'Text view must not request third-party services');
|
|
238
|
+
if (url.pathname === '/api') return route.fulfill({ contentType: 'application/json', body: JSON.stringify({ user: null, isAdmin: false, items: [], next: null }) });
|
|
239
|
+
const path = decodeURIComponent(url.pathname).replace(/^\//, '') + (url.pathname.endsWith('/') ? 'index.html' : '');
|
|
240
|
+
const file = resolve(root, site, 'dist', path);
|
|
241
|
+
assert(file.startsWith(resolve(root, site, 'dist') + sep));
|
|
242
|
+
try { await route.fulfill({ contentType: types[extname(file)] || 'text/html; charset=utf-8', body: await readFile(file) }); }
|
|
243
|
+
catch (error) { if (error.code !== 'ENOENT') throw error; await route.fulfill({ status: 404 }); }
|
|
244
|
+
});
|
|
245
|
+
const page = await context.newPage();
|
|
246
|
+
page.on('pageerror', error => errors.push(error.message));
|
|
247
|
+
const path = locale === config.site.language ? '/' : '/' + locale + '/';
|
|
248
|
+
await page.goto(origin + path);
|
|
249
|
+
await page.waitForFunction(() => document.documentElement.dataset.edgepressDataReady === 'true');
|
|
250
|
+
await page.locator('main').waitFor({state:'visible'});
|
|
251
|
+
assert.equal(await page.getAttribute('html', 'data-edgepress-data-mode'), 'text');
|
|
252
|
+
assert.equal(await page.locator('main').count(), 1);
|
|
253
|
+
await page.waitForFunction(() => document.querySelector('main').innerText.trim().length > 10);
|
|
254
|
+
assert((await page.locator('main').innerText()).trim().length > 10);
|
|
255
|
+
assert(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth));
|
|
256
|
+
assert(!requests.some(url => /\.(?:woff2?|webp|jpe?g|mp4|pdf)(?:\?|$)|\/style\.[a-f0-9]+\.css|(?:document-viewer|image-viewer|gallery-runtime)\.[a-f0-9]+\.js/.test(url)), requests.join('\n'));
|
|
257
|
+
const controls = page.locator('[data-data-return]');
|
|
258
|
+
assert.equal(await page.locator('.edgepress-data-controls').count(), 0);
|
|
259
|
+
assert.equal(await controls.locator('button').count(), 1);
|
|
260
|
+
assert.equal(await controls.locator('button').getAttribute('data-data-mode'), 'full');
|
|
261
|
+
if (locale.startsWith('zh')) assert.match(await controls.innerText(), /文字省流版/);
|
|
262
|
+
assert.equal(await page.locator('[data-data-offer]').isVisible(), false);
|
|
263
|
+
assert.doesNotMatch(await page.locator('body').innerText(), /<img\b|<picture\b|<source\b/);
|
|
264
|
+
assert.equal(await page.locator('body>header, .site-shell>header').count(), 1);
|
|
265
|
+
assert.equal(await page.locator('body>footer, .site-shell>footer').count(), 1);
|
|
266
|
+
assert(await page.locator('body>header, .site-shell>header').isVisible());
|
|
267
|
+
assert(await page.locator('body>footer, .site-shell>footer').isVisible());
|
|
268
|
+
for (const button of await controls.locator('button').all()) assert((await button.boundingBox()).height >= 44);
|
|
269
|
+
if (await page.locator('.privacy-close').isVisible()) await page.locator('.privacy-close').click();
|
|
270
|
+
await page.evaluate(() => {
|
|
271
|
+
const link = document.createElement('a'); link.href = 'https://outside.example.test/story?source=test'; link.textContent = 'External test'; document.querySelector('main').append(link);
|
|
272
|
+
});
|
|
273
|
+
await page.getByRole('link', { name: 'External test', exact: true }).click();
|
|
274
|
+
assert.equal(await page.locator('.edgepress-external-url').textContent(), 'https://outside.example.test/story?source=test');
|
|
275
|
+
await page.keyboard.press('Escape');
|
|
276
|
+
assert.equal(await page.evaluate(() => document.activeElement.textContent), 'External test');
|
|
277
|
+
assert.deepEqual(errors, []);
|
|
278
|
+
await context.close();
|
|
279
|
+
}
|
|
280
|
+
} finally { await browser.close(); }
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
for (const site of selected) test(site + ' preserves separate page shells, meaningful article cards and responsive full/text layouts', async () => {
|
|
284
|
+
const require = createRequire(resolve(root, site, 'package.json'));
|
|
285
|
+
const { parse } = require('yaml');
|
|
286
|
+
const config = parse(await readFile(resolve(root, site, 'config.yml'), 'utf8'));
|
|
287
|
+
const paths = options.paths || ['/'];
|
|
288
|
+
const locales = options.locales || [config.site.language];
|
|
289
|
+
const browser = await chromium.launch({ headless: true, args: ['--disable-extensions'] });
|
|
290
|
+
const evidence = [];
|
|
291
|
+
try {
|
|
292
|
+
for (const width of [320, 1280]) for (const colorScheme of ['light', 'dark']) for (const locale of locales) {
|
|
293
|
+
const prefix = locale === config.site.language ? '' : '/' + locale;
|
|
294
|
+
const context = await browser.newContext({ viewport: { width, height: 900 }, colorScheme, reducedMotion: 'reduce' });
|
|
295
|
+
await context.route('**/*', async route => {
|
|
296
|
+
const url = new URL(route.request().url());
|
|
297
|
+
assert.equal(url.origin, config.site.url);
|
|
298
|
+
if (url.pathname === '/api') return route.fulfill({ contentType: 'application/json', body: JSON.stringify({ user: null, isAdmin: false, items: [], next: null, loginAvailable: true }) });
|
|
299
|
+
const file = resolve(root, site, 'dist', decodeURIComponent(url.pathname).replace(/^\//, '') + (url.pathname.endsWith('/') ? 'index.html' : ''));
|
|
300
|
+
assert(file.startsWith(resolve(root, site, 'dist') + sep));
|
|
301
|
+
try { await route.fulfill({ contentType: types[extname(file)] || 'text/html; charset=utf-8', body: await readFile(file) }); }
|
|
302
|
+
catch (error) { if (error.code !== 'ENOENT') throw error; await route.fulfill({ status: 404 }); }
|
|
303
|
+
});
|
|
304
|
+
const page = await context.newPage();
|
|
305
|
+
const errors = [];
|
|
306
|
+
page.on('pageerror', error => errors.push(error.message));
|
|
307
|
+
for (const suffix of paths) {
|
|
308
|
+
const path = prefix + suffix;
|
|
309
|
+
const snapshots = {};
|
|
310
|
+
for (const mode of ['full', 'text']) {
|
|
311
|
+
await page.goto(config.site.url + path + '?data-mode=' + mode);
|
|
312
|
+
await page.waitForFunction(() => document.documentElement.dataset.edgepressDataReady === 'true');
|
|
313
|
+
await page.locator('main').waitFor({state:'visible'});
|
|
314
|
+
await page.evaluate(() => new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve))));
|
|
315
|
+
snapshots[mode] = await page.evaluate(() => {
|
|
316
|
+
const header = document.querySelector('body>header, .site-shell>header');
|
|
317
|
+
const footer = document.querySelector('body>footer, .site-shell>footer');
|
|
318
|
+
const main = document.querySelector('main');
|
|
319
|
+
const readable = main.cloneNode(true);
|
|
320
|
+
readable.querySelectorAll('[data-edgepress-data-media], template, noscript, script, button, input').forEach(element => element.remove());
|
|
321
|
+
const cardTitles = [...main.querySelectorAll('.post-card h2, .post-card h3')].map(element => element.textContent.trim());
|
|
322
|
+
return {
|
|
323
|
+
heading: main.querySelector('h1')?.textContent.trim(),
|
|
324
|
+
prose: readable.textContent.replace(/\s+/g, ' ').trim(),
|
|
325
|
+
headings: main.querySelectorAll('h1').length,
|
|
326
|
+
mainCount: document.querySelectorAll('main').length,
|
|
327
|
+
headerCount: document.querySelectorAll('body>header, .site-shell>header').length,
|
|
328
|
+
footerCount: document.querySelectorAll('body>footer, .site-shell>footer').length,
|
|
329
|
+
nav: [...header.querySelectorAll('a')].map(link => [link.textContent.trim(), link.getAttribute('href')]),
|
|
330
|
+
footerLinks: [...footer.querySelectorAll('a')].map(link => link.getAttribute('href')),
|
|
331
|
+
footerCopy: footer.querySelector('p')?.textContent.trim(),
|
|
332
|
+
mainWidth: main.getBoundingClientRect().width,
|
|
333
|
+
headerHeight: header.getBoundingClientRect().height,
|
|
334
|
+
footerAfterMain: footer.getBoundingClientRect().top >= main.getBoundingClientRect().bottom - 1,
|
|
335
|
+
overflow: document.documentElement.scrollWidth > innerWidth,
|
|
336
|
+
rawMarkup: /<img\b|<picture\b|<source\b/.test(document.body.innerText),
|
|
337
|
+
cards: cardTitles,
|
|
338
|
+
cardLinks: [...main.querySelectorAll('.post-card h2 a, .post-card h3 a')].map(link => link.getAttribute('href')),
|
|
339
|
+
cardMetadata: [...main.querySelectorAll('.post-card .meta')].map(element => element.textContent.trim()),
|
|
340
|
+
captions: [...main.querySelectorAll('figcaption')].map(element => element.textContent.trim()),
|
|
341
|
+
postListLayout: [...main.querySelectorAll('.post-list')].map(element => getComputedStyle(element).display),
|
|
342
|
+
embedPlaceholders: [...main.querySelectorAll('[data-oembed-placeholder]')].map(element => element.getBoundingClientRect().height),
|
|
343
|
+
embedNotices: [...main.querySelectorAll('[data-oembed-notice]')].map(element => element.textContent.trim()),
|
|
344
|
+
embedLinks: [...main.querySelectorAll('.edgepress-oembed figcaption a')].map(element => element.getAttribute('href'))
|
|
345
|
+
};
|
|
346
|
+
});
|
|
347
|
+
const snapshot = snapshots[mode];
|
|
348
|
+
assert.equal(snapshot.mainCount, 1, path + ' main');
|
|
349
|
+
assert.equal(snapshot.headerCount, 1, path + ' header');
|
|
350
|
+
assert.equal(snapshot.footerCount, 1, path + ' footer');
|
|
351
|
+
assert.equal(snapshot.headings, 1, path + ' h1');
|
|
352
|
+
assert.equal(snapshot.overflow, false, path + ' ' + mode + ' overflow');
|
|
353
|
+
assert.equal(snapshot.rawMarkup, false, path + ' raw HTML');
|
|
354
|
+
assert.equal(snapshot.footerAfterMain, true, path + ' footer outside main');
|
|
355
|
+
assert(snapshot.headerHeight < 300, path + ' navigation must not consume the screen');
|
|
356
|
+
assert(snapshot.postListLayout.every(layout => layout === 'grid'));
|
|
357
|
+
if (mode === 'text') assert(snapshot.embedPlaceholders.every(height => height === 0), path + ' text view has no empty player rectangle');
|
|
358
|
+
if (suffix.includes('/2026/10/')) assert(snapshot.embedPlaceholders.every(height => height === 0), path + ' full embed entry is compact before consent');
|
|
359
|
+
if (process.env.DATA_SAVER_SCREENSHOTS && (suffix === '/' || suffix === '/about/' || suffix === '/guide/' || suffix.includes('/2026/10/'))) {
|
|
360
|
+
await page.waitForTimeout(350);
|
|
361
|
+
if (await page.locator('.privacy-close').isVisible()) await page.locator('.privacy-close').click();
|
|
362
|
+
if (mode === 'full') {
|
|
363
|
+
const height = await page.evaluate(() => document.documentElement.scrollHeight);
|
|
364
|
+
for (let offset = 0; offset < height; offset += 700) {
|
|
365
|
+
await page.evaluate(offset => scrollTo({ top: offset, behavior: 'instant' }), offset);
|
|
366
|
+
await page.waitForTimeout(80);
|
|
367
|
+
}
|
|
368
|
+
await page.waitForFunction(() => [...document.images].filter(image => image.getBoundingClientRect().width > 0).every(image => image.complete));
|
|
369
|
+
const broken = await page.evaluate(() => [...document.images].filter(image => image.getBoundingClientRect().width > 0 && image.naturalWidth === 0).map(image => image.currentSrc));
|
|
370
|
+
assert.deepEqual(broken, [], path + ' full view image evidence');
|
|
371
|
+
}
|
|
372
|
+
await page.evaluate(() => scrollTo({ top: 0, behavior: 'instant' }));
|
|
373
|
+
await mkdir(process.env.DATA_SAVER_SCREENSHOTS, { recursive: true });
|
|
374
|
+
const filename = site + '-' + (prefix ? locale + '-' : '') + (suffix.replaceAll('/', '') || 'home') + '-' + width + '-' + colorScheme + '-' + mode + '.png';
|
|
375
|
+
await page.screenshot({ path: resolve(process.env.DATA_SAVER_SCREENSHOTS, filename), fullPage: true });
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
const full = snapshots.full, text = snapshots.text;
|
|
379
|
+
assert.equal(text.heading, full.heading, path + ' must remain the same page');
|
|
380
|
+
if (options.compareProse !== false) assert.equal(text.prose, full.prose, path + ' no prose lost when pictures are removed');
|
|
381
|
+
assert.deepEqual(text.footerLinks, full.footerLinks);
|
|
382
|
+
assert.equal(text.footerCopy, full.footerCopy);
|
|
383
|
+
assert.deepEqual(text.nav.map(link => link[1]), full.nav.map(link => link[1]));
|
|
384
|
+
assert.deepEqual(text.cards, full.cards, path + ' real article titles');
|
|
385
|
+
assert.deepEqual(text.cardLinks, full.cardLinks);
|
|
386
|
+
assert.deepEqual(text.cardMetadata, full.cardMetadata);
|
|
387
|
+
assert.deepEqual(text.captions, full.captions, path + ' image context and attribution');
|
|
388
|
+
assert.deepEqual(text.embedNotices, full.embedNotices, path + ' retained consent notices');
|
|
389
|
+
assert.deepEqual(text.embedLinks, full.embedLinks, path + ' retained video source links');
|
|
390
|
+
assert(Math.abs(full.mainWidth - text.mainWidth) < 2, path + ' shared content width');
|
|
391
|
+
evidence.push({ site, path, width, colorScheme, locale, ...text });
|
|
392
|
+
if (suffix === '/about/' || suffix === '/guide/') {
|
|
393
|
+
await page.locator('body>header a.brand, .site-shell>header a.brand').click();
|
|
394
|
+
assert.equal(new URL(page.url()).pathname, prefix + '/');
|
|
395
|
+
assert.equal(await page.locator('main').count(), 1);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
assert.deepEqual(errors, []);
|
|
399
|
+
await context.close();
|
|
400
|
+
}
|
|
401
|
+
if (process.env.DATA_SAVER_SCREENSHOTS) await writeFile(resolve(process.env.DATA_SAVER_SCREENSHOTS, site + '-layout.json'), JSON.stringify(evidence, null, 2));
|
|
402
|
+
} finally { await browser.close(); }
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { createServer } from 'node:http';
|
|
3
|
+
import { readFile, mkdir, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { createRequire } from 'node:module';
|
|
5
|
+
import { resolve, extname, sep, basename } from 'node:path';
|
|
6
|
+
import { gzipSync } from 'node:zlib';
|
|
7
|
+
|
|
8
|
+
const root = process.cwd();
|
|
9
|
+
const directory = resolve(process.env.PERFORMANCE_DIST || 'dist');
|
|
10
|
+
const require = createRequire(resolve(root,'package.json'));
|
|
11
|
+
const { chromium } = require('playwright');
|
|
12
|
+
const types = {'.js':'text/javascript','.css':'text/css','.json':'application/json','.svg':'image/svg+xml','.webp':'image/webp','.png':'image/png','.woff2':'font/woff2'};
|
|
13
|
+
const server = createServer(async (request,response) => {
|
|
14
|
+
const address = new URL(request.url,'http://localhost');
|
|
15
|
+
if (address.pathname === '/api') return response.writeHead(200,{'Content-Type':'application/json'}).end(JSON.stringify({user:null,isAdmin:false,items:[],next:null,loginAvailable:true}));
|
|
16
|
+
const file = resolve(directory,decodeURIComponent(address.pathname).slice(1)+(address.pathname.endsWith('/')?'index.html':''));
|
|
17
|
+
if (!file.startsWith(directory+sep)) return response.writeHead(404).end();
|
|
18
|
+
try {
|
|
19
|
+
const body = await readFile(file);
|
|
20
|
+
const type = types[extname(file)] || 'text/html; charset=utf-8';
|
|
21
|
+
const compressed = /^(?:text\/|application\/json|image\/svg)/.test(type);
|
|
22
|
+
response.writeHead(200,{'Content-Type':type,'Cache-Control':'no-store',...(compressed?{'Content-Encoding':'gzip'}:{})}).end(compressed?gzipSync(body):body);
|
|
23
|
+
} catch { response.writeHead(404).end(); }
|
|
24
|
+
});
|
|
25
|
+
await new Promise(resolve=>server.listen(0,'127.0.0.1',resolve));
|
|
26
|
+
const origin = 'http://127.0.0.1:'+server.address().port;
|
|
27
|
+
const browser = await chromium.launch({headless:true,args:['--disable-extensions']});
|
|
28
|
+
const paths = JSON.parse(process.env.PERFORMANCE_PATHS || '["/"]');
|
|
29
|
+
const results = [];
|
|
30
|
+
try {
|
|
31
|
+
for (const path of paths) for (const mode of ['full','text']) {
|
|
32
|
+
const context = await browser.newContext({viewport:{width:390,height:844},colorScheme:'dark'});
|
|
33
|
+
await context.addInitScript(mode=>{
|
|
34
|
+
localStorage.setItem('edgepress-data-mode',mode);
|
|
35
|
+
window.pagePaint = {largestContentfulPaint:0,lcpElement:''};
|
|
36
|
+
new PerformanceObserver(list=>{
|
|
37
|
+
const entry = list.getEntries().at(-1);
|
|
38
|
+
window.pagePaint = {largestContentfulPaint:entry.startTime,lcpElement:entry.element?.tagName || ''};
|
|
39
|
+
}).observe({type:'largest-contentful-paint',buffered:true});
|
|
40
|
+
},mode);
|
|
41
|
+
await context.route(url=>url.origin !== origin,route=>route.abort());
|
|
42
|
+
const page = await context.newPage();
|
|
43
|
+
const errors = [];
|
|
44
|
+
page.on('pageerror',error=>errors.push(error.message));
|
|
45
|
+
const session = await context.newCDPSession(page);
|
|
46
|
+
await session.send('Network.enable');
|
|
47
|
+
await session.send('Network.setCacheDisabled',{cacheDisabled:true});
|
|
48
|
+
await session.send('Network.emulateNetworkConditions',{offline:false,latency:150,downloadThroughput:100*1024,uploadThroughput:100*1024});
|
|
49
|
+
let transferredBytes = 0;
|
|
50
|
+
let completedRequests = 0;
|
|
51
|
+
session.on('Network.loadingFinished',event=>{transferredBytes+=event.encodedDataLength;completedRequests++;});
|
|
52
|
+
await page.goto(origin+path,{waitUntil:'domcontentloaded'});
|
|
53
|
+
await page.waitForFunction(()=>document.documentElement.dataset.edgepressDataReady==='true',null,{timeout:15000});
|
|
54
|
+
await page.locator('main').waitFor({state:'visible'});
|
|
55
|
+
const contentReadyMs = await page.evaluate(()=>performance.now());
|
|
56
|
+
assert((await page.locator('main').innerText()).trim().length>10);
|
|
57
|
+
assert(await page.evaluate(()=>document.documentElement.scrollWidth<=innerWidth));
|
|
58
|
+
await page.waitForTimeout(1200);
|
|
59
|
+
const paint = await page.evaluate(()=>({...window.pagePaint,firstContentfulPaint:performance.getEntriesByName('first-contentful-paint')[0]?.startTime || 0,domContentLoaded:performance.getEntriesByType('navigation')[0].domContentLoadedEventEnd}));
|
|
60
|
+
const measurement = {site:basename(root),path,mode,bandwidthKBs:100,latencyMs:150,contentReadyMs:Math.round(contentReadyMs),...paint,completedRequests,transferredBytes,errors};
|
|
61
|
+
results.push(measurement);
|
|
62
|
+
assert.deepEqual(errors,[]);
|
|
63
|
+
if (process.env.PERFORMANCE_OUTPUT) {
|
|
64
|
+
await mkdir(process.env.PERFORMANCE_OUTPUT,{recursive:true});
|
|
65
|
+
const name = basename(root)+'-'+(path==='/'?'home':path.includes('/2026/')?'article':'zh-SG')+'-'+mode;
|
|
66
|
+
await page.screenshot({path:resolve(process.env.PERFORMANCE_OUTPUT,name+'.png'),fullPage:true});
|
|
67
|
+
}
|
|
68
|
+
await context.close();
|
|
69
|
+
}
|
|
70
|
+
console.log(JSON.stringify(results,null,2));
|
|
71
|
+
if (process.env.PERFORMANCE_OUTPUT) await writeFile(resolve(process.env.PERFORMANCE_OUTPUT,basename(root)+'-performance.json'),JSON.stringify(results,null,2)+'\n');
|
|
72
|
+
} finally {
|
|
73
|
+
await browser.close();
|
|
74
|
+
server.closeAllConnections();
|
|
75
|
+
await new Promise(resolve=>server.close(resolve));
|
|
76
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
import hashlib
|
|
3
|
+
import json
|
|
4
|
+
import re
|
|
5
|
+
import sys
|
|
6
|
+
from PIL import Image, ImageDraw, ImageFont
|
|
7
|
+
|
|
8
|
+
SITES = [Path(arg).resolve() for arg in sys.argv[1:]] or [Path.cwd()]
|
|
9
|
+
names = re.findall(r"'([^']+)'", (Path(__file__).resolve().parents[2] / 'src/icons.js').read_text(encoding='utf-8').split('new Set([')[1].split(']);')[0])
|
|
10
|
+
assert len(names) == len(set(names)) == 28
|
|
11
|
+
for index, site in enumerate(SITES):
|
|
12
|
+
index = 1 if (site / 'content/assets/images/brand').is_dir() else 0
|
|
13
|
+
assets = site / 'content/assets'
|
|
14
|
+
brand = assets / ('images/brand' if index == 1 else 'edgepress/brand')
|
|
15
|
+
favicon = assets / ('favicon' if index == 1 else 'edgepress/favicon')
|
|
16
|
+
icon_dir = assets / 'edgepress/icons'
|
|
17
|
+
assert set(path.stem for path in icon_dir.glob('*.svg')) == set(names)
|
|
18
|
+
assert not any(path.suffix.lower() in {'.png', '.svg', '.webp', '.ico', '.jpg', '.jpeg', '.mp4', '.webm'} for path in (site / 'static').rglob('*') if path.is_file())
|
|
19
|
+
for path in icon_dir.glob('*.svg'):
|
|
20
|
+
assert '<svg' in path.read_text(encoding='utf-8'), path
|
|
21
|
+
assert (icon_dir / 'LICENSE.txt').is_file()
|
|
22
|
+
checked = []
|
|
23
|
+
for path in list(brand.rglob('*.png')) + list(brand.rglob('*.webp')) + list(favicon.glob('*.png')):
|
|
24
|
+
with Image.open(path) as im:
|
|
25
|
+
im.load()
|
|
26
|
+
assert im.format in {'PNG', 'WEBP'}, path
|
|
27
|
+
if path.name == 'og-image.png' or path.name == 'og-image.webp':
|
|
28
|
+
assert im.size == (1200, 630), path
|
|
29
|
+
elif path.name == 'avatar.png':
|
|
30
|
+
assert im.size == (512, 512), path
|
|
31
|
+
elif path.name == 'apple-touch-icon.png':
|
|
32
|
+
assert im.size == (180, 180), path
|
|
33
|
+
elif path.name.startswith('favicon-'):
|
|
34
|
+
size = int(path.stem.split('-')[1])
|
|
35
|
+
assert im.size == (size, size), path
|
|
36
|
+
elif path.parent == icon_dir:
|
|
37
|
+
assert im.size == (128, 128), path
|
|
38
|
+
elif path.name == 'home-hero.png' or path.name == 'home-hero.webp':
|
|
39
|
+
assert im.size == (1536, 1024), path
|
|
40
|
+
elif path.name == 'home-hero-768.webp':
|
|
41
|
+
assert im.size == (768, 512), path
|
|
42
|
+
alpha = im.getchannel('A').getextrema() if 'A' in im.getbands() else None
|
|
43
|
+
if path.name not in {'og-image.png', 'og-image.webp', 'apple-touch-icon.png'}:
|
|
44
|
+
assert alpha and alpha[0] == 0 and alpha[1] > 0, path
|
|
45
|
+
assert im.getpixel((0, 0))[-1] == 0, path
|
|
46
|
+
checked.append({'path': str(path.relative_to(site)).replace('\\', '/'), 'format': im.format,
|
|
47
|
+
'dimensions': list(im.size), 'alphaRange': list(alpha) if alpha else None,
|
|
48
|
+
'sha256': hashlib.sha256(path.read_bytes()).hexdigest()})
|
|
49
|
+
with Image.open(favicon / 'favicon.ico') as im:
|
|
50
|
+
assert im.ico.sizes() == {(16, 16), (32, 32), (48, 48), (64, 64)}
|
|
51
|
+
(site / 'content/assets/edgepress/brand/sources/export-manifest.json').write_text(json.dumps({'iconCount': len(names), 'assets': checked}, indent=2) + '\n', encoding='utf-8')
|
|
52
|
+
print(site.name + ': verified ' + str(len(checked)) + ' real raster assets, transparent channels and required dimensions.')
|