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
package/src/page-blocks.js
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
|
+
import {renderServiceBlock} from './service-block.js';
|
|
2
|
+
import {renderLocalTool} from './local-tool.js';
|
|
3
|
+
import {renderDocument} from './document.js';
|
|
4
|
+
import {renderMediaViewer} from './media-viewer.js';
|
|
5
|
+
import {renderEmbed, renderPageText} from './embed.js';
|
|
1
6
|
import { renderMarkdownExcerpt } from './markdown.js';
|
|
2
|
-
import { translate, translateValue } from './i18n.js';
|
|
7
|
+
import { localizedContentUrl, translate, translateValue } from './i18n.js';
|
|
3
8
|
import { isIconName, renderIcon } from './icons.js';
|
|
4
9
|
import { postsForLocale } from './content.js';
|
|
10
|
+
import { categoryFilter, filterPostsByCategory, categorySlug, sortPinnedPosts } from './post-details.js';
|
|
5
11
|
|
|
6
12
|
const captchaProviders = new Set(['cloudflare-turnstile', 'google-recaptcha', 'hcaptcha']);
|
|
7
13
|
|
|
@@ -30,16 +36,7 @@ function safeUrl(value, label) {
|
|
|
30
36
|
}
|
|
31
37
|
|
|
32
38
|
function localizedSiteUrl(value, label, context) {
|
|
33
|
-
|
|
34
|
-
if (!url.startsWith('/')) return url;
|
|
35
|
-
const localeToken = /^\/\[launge\](?=\/|$)/i;
|
|
36
|
-
if (/\[launge\]/i.test(url) && !localeToken.test(url)) {
|
|
37
|
-
throw new Error(label + ' may use [launge] only as the first site-path segment');
|
|
38
|
-
}
|
|
39
|
-
if (!localeToken.test(url)) return url;
|
|
40
|
-
const suffix = url.replace(localeToken, '');
|
|
41
|
-
const prefix = context.locale === context.config.i18n.defaultLocale ? '' : '/' + context.locale;
|
|
42
|
-
return prefix + (suffix || '/');
|
|
39
|
+
return localizedContentUrl(context.config,context.locale,safeUrl(value,label),label);
|
|
43
40
|
}
|
|
44
41
|
|
|
45
42
|
function list(value, label, min = 1, max = 50) {
|
|
@@ -61,15 +58,14 @@ function imageCandidates(value, label) {
|
|
|
61
58
|
}).join(', ');
|
|
62
59
|
}
|
|
63
60
|
|
|
64
|
-
function enabledIntegration(config, id
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
if (!match) throw new Error('Page block requires a configured ' + category + ' service with id: ' + id);
|
|
61
|
+
function enabledIntegration(config, id) {
|
|
62
|
+
const match = config.browserPlugins.services.find(item => item.enabled !== false && item.id === id);
|
|
63
|
+
if (!match) throw new Error('Page block requires a configured service with id: ' + id);
|
|
68
64
|
return match;
|
|
69
65
|
}
|
|
70
66
|
|
|
71
|
-
function dateLabel(date, locale) {
|
|
72
|
-
return new Intl.DateTimeFormat(locale, {
|
|
67
|
+
function dateLabel(date, locale, timeZone = 'UTC', dateOnly = true) {
|
|
68
|
+
return new Intl.DateTimeFormat(locale, dateOnly?{dateStyle:'long',timeZone}:{year:'numeric',month:'long',day:'numeric',hour:'2-digit',minute:'2-digit',timeZoneName:'short',timeZone}).format(date);
|
|
73
69
|
}
|
|
74
70
|
|
|
75
71
|
function dateOnlyLabel(value, locale) {
|
|
@@ -77,21 +73,11 @@ function dateOnlyLabel(value, locale) {
|
|
|
77
73
|
}
|
|
78
74
|
|
|
79
75
|
function renderPrivacyServices(context) {
|
|
80
|
-
const
|
|
81
|
-
['tracking', translate(context.config, context.locale, 'pluginTracking')],
|
|
82
|
-
['statistics', translate(context.config, context.locale, 'pluginStatistics')],
|
|
83
|
-
['advertising', translate(context.config, context.locale, 'pluginAdvertising')],
|
|
84
|
-
['captcha', translate(context.config, context.locale, 'pluginCaptcha')]
|
|
85
|
-
];
|
|
86
|
-
const configured = groups.flatMap(([key, label]) => {
|
|
87
|
-
const section = context.config.browserPlugins[key];
|
|
88
|
-
return (section?.enabled ? section.services : []).map((service) => ({ ...service, category: label }));
|
|
89
|
-
});
|
|
76
|
+
const configured = context.config.browserPlugins.services.filter(service => service.enabled !== false);
|
|
90
77
|
if (!configured.length) {
|
|
91
|
-
return '<p class="privacy-empty">' + escapeHtml(text(context.emptyText, 'privacy-services.emptyText', 2000)) + '</p>';
|
|
78
|
+
return '<p class="privacy-services-empty">' + escapeHtml(text(context.emptyText, 'privacy-services.emptyText', 2000)) + '</p>';
|
|
92
79
|
}
|
|
93
|
-
return '<ul class="privacy-service-list">' + configured.map((service) => '<li><h3>' + escapeHtml(service.
|
|
94
|
-
translateValue(context.config, context.locale, service.name || service.provider)) + '</h3><p><strong>' +
|
|
80
|
+
return '<ul class="privacy-service-list">' + configured.map((service) => '<li><h3>' + escapeHtml(translateValue(context.config, context.locale, service.name || service.provider)) + '</h3><p><strong>' +
|
|
95
81
|
escapeHtml(translate(context.config, context.locale, 'servicePurpose')) + ':</strong> ' +
|
|
96
82
|
escapeHtml(translateValue(context.config, context.locale, service.purpose)) + '</p><p><strong>' +
|
|
97
83
|
escapeHtml(translate(context.config, context.locale, 'serviceDataCategories')) + ':</strong> ' +
|
|
@@ -109,23 +95,37 @@ async function renderLatestPosts(block, context) {
|
|
|
109
95
|
if (!Number.isInteger(count) || count < 1 || count > 12) throw new Error('latest-posts.count must be an integer from 1 to 12');
|
|
110
96
|
const paginate = block.paginate ?? false;
|
|
111
97
|
if (typeof paginate !== 'boolean') throw new Error('latest-posts.paginate must be a boolean');
|
|
112
|
-
|
|
98
|
+
if ('tag' in block) throw new Error('post-list.tag is no longer supported; use category/categories and show tags on article pages');
|
|
99
|
+
if ('category' in block && 'categories' in block) throw new Error('Use category or categories, not both');
|
|
100
|
+
const categories = categoryFilter(block.category ?? block.categories ?? (block.type === 'latest-posts' ? context.config.site?.archive?.categories : undefined));
|
|
101
|
+
const archiveCategories = categoryFilter(context.config.site?.archive?.categories);
|
|
102
|
+
const sameArchive = [...categories].sort().join('\0') === [...archiveCategories].sort().join('\0');
|
|
103
|
+
if (paginate && (block.type === 'post-list' || !sameArchive)) throw new Error('Homepage pagination must use the same categories as site.archive.categories');
|
|
104
|
+
const selected = filterPostsByCategory(postsForLocale(context.site.posts, context.locale, context.config.i18n.defaultLocale), categories);
|
|
105
|
+
const ordered = block.type === 'post-list' ? sortPinnedPosts(selected) : selected;
|
|
106
|
+
const posts = ordered.filter(post => !context.displayedPostBundles?.has(post.bundlePath));
|
|
107
|
+
const displayed = posts.slice(0, count);
|
|
108
|
+
for (const post of displayed) context.displayedPostBundles?.add(post.bundlePath);
|
|
113
109
|
const title = escapeHtml(text(block.title, 'latest-posts.title', 200));
|
|
114
110
|
const authorLabel = (post) => post.author ? '<span class="post-author">' + escapeHtml(translate(context.config, context.locale, 'postAuthor')
|
|
115
111
|
.replace('{author}', post.author)) + '</span>' : '';
|
|
116
|
-
const cards = (await Promise.all(
|
|
117
|
-
const excerpt = await renderMarkdownExcerpt(post.description || post.markdown, context.config.markdown);
|
|
112
|
+
const cards = (await Promise.all(displayed.map(async (post) => {
|
|
113
|
+
const excerpt = await renderMarkdownExcerpt(post.description || post.markdown, {...context.config.markdown,linkResolver:value=>localizedContentUrl(context.config,post.locale,value)});
|
|
118
114
|
return '<article class="post-card" lang="' + escapeHtml(post.locale) + '"><h3><a href="' + escapeHtml('/' + post.path.split('/').filter(Boolean).join('/') + (post.path.endsWith('/') ? '/' : '')) + '">' +
|
|
119
|
-
escapeHtml(post.title) + '</a></h3
|
|
120
|
-
'</time>' + (post.author ? '
|
|
115
|
+
escapeHtml(post.title) + '</a></h3>' + (block.type === 'post-list' && post.pinned ? '<span class="post-pinned">' + escapeHtml(translate(context.config, context.locale, 'postPinned')) + '</span>' : '') + '<p class="meta"><time data-local-time data-time-locale="' + escapeHtml(post.locale || context.locale) + '" data-date-only="' + (post.dateOnly === true) + '" datetime="' + (post.dateOnly ? post.date.toISOString().slice(0,10) : post.date.toISOString()) + '">' + escapeHtml(dateLabel(post.date, post.locale || context.locale, post.dateOnly ? 'UTC' : context.config.site.timeZone, post.dateOnly)) +
|
|
116
|
+
'</time>' + (post.author ? ' ' + authorLabel(post) : '') + '</p><div class="post-excerpt" lang="' + escapeHtml(post.locale) + '">' + excerpt + '</div></article>';
|
|
121
117
|
}))).join('');
|
|
122
118
|
const pagination = paginate && context.latestPostsPagination?.totalPages > 1
|
|
123
119
|
? '<nav class="pagination latest-posts-pagination" aria-label="' + escapeHtml(translate(context.config, context.locale, 'pagination')) + '">' +
|
|
124
120
|
'<a rel="next" href="' + escapeHtml(context.latestPostsPagination.olderUrl) + '">' + escapeHtml(translate(context.config, context.locale, 'older')) + '</a>' +
|
|
125
121
|
'<a href="' + escapeHtml(context.latestPostsPagination.archiveUrl) + '">' + escapeHtml(translate(context.config, context.locale, 'allPosts')) + '</a></nav>'
|
|
126
122
|
: '';
|
|
123
|
+
const morePath = categories.length === 1 ? '/[launge]/categories/' + categorySlug(categories[0]) + '/' : sameArchive ? '/[launge]/archives/' : '';
|
|
124
|
+
const categoryArchive = !paginate && morePath && posts.length > count
|
|
125
|
+
? '<p class="post-list-more"><a href="' + escapeHtml(localizedSiteUrl(morePath, 'post-list.categoryArchive', context)) + '">' +
|
|
126
|
+
escapeHtml(translate(context.config, context.locale, categories.length === 1 ? 'allCategoryPosts' : 'allPosts')) + '</a></p>' : '';
|
|
127
127
|
return '<section class="latest-posts"><h2>' + title + '</h2>' +
|
|
128
|
-
(cards ? '<div class="post-list">' + cards + '</div>' : '<p>' + escapeHtml(translate(context.config, context.locale, 'noPosts')) + '</p>') + pagination + '</section>';
|
|
128
|
+
(cards ? '<div class="post-list">' + cards + '</div>' : '<p>' + escapeHtml(translate(context.config, context.locale, 'noPosts')) + '</p>') + pagination + categoryArchive + '</section>';
|
|
129
129
|
}
|
|
130
130
|
|
|
131
131
|
function renderMediaText(block, context) {
|
|
@@ -138,7 +138,15 @@ function renderMediaText(block, context) {
|
|
|
138
138
|
if (!['left', 'right'].includes(placement)) throw new Error('media-text.placement must be left or right');
|
|
139
139
|
let media;
|
|
140
140
|
if (mediaType === 'image') {
|
|
141
|
-
|
|
141
|
+
const original=block.originalSrc?' data-original="'+escapeHtml(safeUrl(block.originalSrc,'media-text.originalSrc'))+'"':'';
|
|
142
|
+
media = '<img src="' + src + '"'+original+' alt="' + alt + '" loading="lazy" decoding="async">';
|
|
143
|
+
if (block.animationSrc) {
|
|
144
|
+
const animation = safeUrl(block.animationSrc, 'media-text.animationSrc');
|
|
145
|
+
if (!animation.startsWith('/') || !/\.(mp4|webm)$/i.test(animation)) throw new Error('media-text.animationSrc must be a local MP4 or WebM');
|
|
146
|
+
const title = escapeHtml(block.title || block.alt);
|
|
147
|
+
const seekName = 'edgepress-demo-seek-' + (context.demoCount = (context.demoCount || 0) + 1);
|
|
148
|
+
media = '<div class="project-demo" data-project-demo data-animation="' + escapeHtml(animation) + '" data-play="' + escapeHtml(translate(context.config, context.locale, 'demoPlay')) + '" data-resume="' + escapeHtml(translate(context.config, context.locale, 'demoResume')) + '" data-pause="' + escapeHtml(translate(context.config, context.locale, 'demoPause')) + '" data-replay="' + escapeHtml(translate(context.config, context.locale, 'demoReplay')) + '" data-loading="' + escapeHtml(translate(context.config, context.locale, 'demoLoading')) + '" data-error="' + escapeHtml(translate(context.config, context.locale, 'demoError')) + '"><div class="project-demo-media">' + media + '<video muted playsinline preload="none" hidden aria-label="' + title + '"></video></div><button type="button" data-demo-toggle aria-pressed="false" aria-label="' + escapeHtml(translate(context.config, context.locale, 'demoPlay')) + ': ' + title + '">' + escapeHtml(translate(context.config, context.locale, 'demoPlay')) + '</button><input name="' + seekName + '" class="project-demo-seek" data-demo-seek type="range" min="0" max="1" value="0" step="0.1" disabled aria-label="' + escapeHtml(translate(context.config, context.locale, 'demoSeek')) + ': ' + title + '"><span class="project-demo-time" data-demo-time>0:00 / 0:00</span><span class="project-demo-status" role="status"></span></div>';
|
|
149
|
+
}
|
|
142
150
|
} else {
|
|
143
151
|
if (!alt.trim()) throw new Error('media-text.alt must describe the video');
|
|
144
152
|
const videoSrc = escapeHtml(safeUrl(block.src, 'media-text.src'));
|
|
@@ -166,11 +174,28 @@ async function renderBlock(block, context, depth, index) {
|
|
|
166
174
|
if (depth > 4) throw new Error('Page block nesting may not exceed 4 levels');
|
|
167
175
|
const type = text(block.type, 'block.type', 40);
|
|
168
176
|
switch (type) {
|
|
177
|
+
case 'local-tool': return renderLocalTool(block, context);
|
|
178
|
+
case 'document': return renderDocument(block, context);
|
|
179
|
+
case 'media-viewer': return renderMediaViewer(block, context);
|
|
169
180
|
case 'hero': {
|
|
170
181
|
const heading = text(block.title, 'hero.title', 240);
|
|
171
182
|
const level = context.isHomepage && index === 0 ? 'h1' : 'h2';
|
|
172
183
|
const eyebrow = text(block.eyebrow, 'hero.eyebrow', 120, true);
|
|
173
184
|
const description = text(block.text, 'hero.text', 1000, true);
|
|
185
|
+
const bannerSrc = block.bannerSrc === undefined ? '' : safeUrl(block.bannerSrc, 'hero.bannerSrc');
|
|
186
|
+
const bannerAlt = block.bannerAlt === undefined ? '' : text(block.bannerAlt, 'hero.bannerAlt', 500);
|
|
187
|
+
if (Boolean(bannerSrc) !== Boolean(bannerAlt)) throw new Error('hero.bannerSrc and hero.bannerAlt must be provided together');
|
|
188
|
+
const bannerSrcset = block.bannerSrcset === undefined ? '' : imageCandidates(block.bannerSrcset, 'hero.bannerSrcset');
|
|
189
|
+
if (bannerSrcset && !bannerSrc) throw new Error('hero.bannerSrcset requires hero.bannerSrc');
|
|
190
|
+
const bannerWebpSrcset = block.bannerWebpSrcset === undefined ? '' : imageCandidates(block.bannerWebpSrcset, 'hero.bannerWebpSrcset');
|
|
191
|
+
if (bannerWebpSrcset && !bannerSrc) throw new Error('hero.bannerWebpSrcset requires hero.bannerSrc');
|
|
192
|
+
const bannerSizes = escapeHtml(text(block.bannerSizes ?? '(max-width: 620px) 90vw, 680px', 'hero.bannerSizes', 200));
|
|
193
|
+
const bannerWidth = block.bannerWidth;
|
|
194
|
+
const bannerHeight = block.bannerHeight;
|
|
195
|
+
if (bannerSrc && (!Number.isInteger(bannerWidth) || bannerWidth < 1 || bannerWidth > 10000 ||
|
|
196
|
+
!Number.isInteger(bannerHeight) || bannerHeight < 1 || bannerHeight > 10000)) {
|
|
197
|
+
throw new Error('hero banner dimensions must be integers from 1 to 10000');
|
|
198
|
+
}
|
|
174
199
|
const mascotSrc = block.mascotSrc === undefined ? '' : safeUrl(block.mascotSrc, 'hero.mascotSrc');
|
|
175
200
|
const mascotAlt = block.mascotAlt === undefined ? '' : text(block.mascotAlt, 'hero.mascotAlt', 500);
|
|
176
201
|
if (Boolean(mascotSrc) !== Boolean(mascotAlt)) throw new Error('hero.mascotSrc and hero.mascotAlt must be provided together');
|
|
@@ -202,9 +227,14 @@ async function renderBlock(block, context, depth, index) {
|
|
|
202
227
|
? '<picture class="hero-mascot-picture"><source type="image/webp" srcset="' + mascotWebpSrcset + '" sizes="' + mascotSizes + '">' + mascotImage + '</picture>'
|
|
203
228
|
: mascotImage;
|
|
204
229
|
const aside = mascot || highlightPanel ? '<div class="hero-aside">' + mascot + highlightPanel + '</div>' : '';
|
|
205
|
-
|
|
230
|
+
const bannerImage = bannerSrc ? '<img class="hero-banner" src="' + escapeHtml(bannerSrc) + '" alt="' + escapeHtml(bannerAlt) +
|
|
231
|
+
'" width="' + bannerWidth + '" height="' + bannerHeight + '"' + (bannerSrcset ? ' srcset="' + bannerSrcset +
|
|
232
|
+
'" sizes="(max-width: 620px) calc(100vw - 32px), (max-width: 1168px) calc(100vw - 48px), 1120px"' : '') +
|
|
233
|
+
' decoding="async" fetchpriority="high">' : '';
|
|
234
|
+
const banner = bannerWebpSrcset ? '<picture class="hero-banner-picture"><source type="image/webp" srcset="' + bannerWebpSrcset + '" sizes="' + bannerSizes + '">' + bannerImage + '</picture>' : bannerImage;
|
|
235
|
+
return '<section class="hero-block' + (aside ? ' hero-block--split' : '') + (banner ? ' hero-block--banner' : '') + '"><div class="hero-main">' +
|
|
206
236
|
(eyebrow ? '<p class="eyebrow">' + escapeHtml(eyebrow) + '</p>' : '') + '<' + level + '>' + escapeHtml(heading) + '</' + level + '>' +
|
|
207
|
-
(description ? '<p class="hero-copy">' + escapeHtml(description) + '</p>' : '') + action + '</div>' + aside + '</section>';
|
|
237
|
+
(description ? '<p class="hero-copy">' + escapeHtml(description) + '</p>' : '') + action + '</div>' + aside + banner + '</section>';
|
|
208
238
|
}
|
|
209
239
|
case 'section': {
|
|
210
240
|
const heading = text(block.title, 'section.title', 200, true);
|
|
@@ -242,7 +272,7 @@ async function renderBlock(block, context, depth, index) {
|
|
|
242
272
|
let paragraphs = block.paragraphs;
|
|
243
273
|
if (paragraphs === undefined && typeof block.text === 'string') paragraphs = block.text.split(/\n\s*\n/).filter(Boolean);
|
|
244
274
|
if (!Array.isArray(paragraphs) || paragraphs.length < 1 || paragraphs.length > 30) throw new Error('text needs a text value or 1 to 30 paragraphs');
|
|
245
|
-
return '<div class="text-widget">' + paragraphs.map(
|
|
275
|
+
return '<div class="text-widget">' + (await Promise.all(paragraphs.map(paragraph => renderPageText(text(paragraph, 'text paragraph', 4000), context)))).join('') + '</div>';
|
|
246
276
|
}
|
|
247
277
|
case 'media-text': return renderMediaText(block, context);
|
|
248
278
|
case 'display-text': {
|
|
@@ -289,7 +319,7 @@ async function renderBlock(block, context, depth, index) {
|
|
|
289
319
|
const rows = list(block.rows, 'data-table.rows', 1, 100);
|
|
290
320
|
const title = text(block.title, 'data-table.title', 200);
|
|
291
321
|
for (const row of rows) if (!Array.isArray(row) || row.length !== headers.length) throw new Error('Each data-table row must match the number of headers');
|
|
292
|
-
return '<div class="table-widget"><table><caption>' + escapeHtml(title) + '</caption><thead><tr>' + headers.map((item) =>
|
|
322
|
+
return '<div class="table-widget" tabindex="0" role="region" aria-label="'+escapeHtml(title)+'"><table><caption>' + escapeHtml(title) + '</caption><thead><tr>' + headers.map((item) =>
|
|
293
323
|
'<th scope="col">' + escapeHtml(text(item, 'data-table header', 200)) + '</th>').join('') + '</tr></thead><tbody>' +
|
|
294
324
|
rows.map((row) => '<tr>' + row.map((cell) => '<td>' + escapeHtml(text(cell, 'data-table cell', 1000)) + '</td>').join('') + '</tr>').join('') +
|
|
295
325
|
'</tbody></table></div>';
|
|
@@ -343,6 +373,22 @@ async function renderBlock(block, context, depth, index) {
|
|
|
343
373
|
'</span><span class="privacy-controller-separator" aria-hidden="true">·</span><span><strong>' + contactLabel +
|
|
344
374
|
':</strong> ' + contactValue + '</span></p>';
|
|
345
375
|
}
|
|
376
|
+
case 'link-directory': {
|
|
377
|
+
const items = list(block.items, 'link-directory.items', 1, 100);
|
|
378
|
+
const heading = text(block.title, 'link-directory.title', 200);
|
|
379
|
+
const entries = items.map((item) => {
|
|
380
|
+
const name = escapeHtml(text(item?.title, 'link-directory item title', 200));
|
|
381
|
+
const summary = escapeHtml(text(item?.text, 'link-directory item text', 1000, true));
|
|
382
|
+
const href = localizedSiteUrl(item?.url, 'link-directory item url', context);
|
|
383
|
+
const host = /^https?:\/\//i.test(href) ? new URL(href).host : '';
|
|
384
|
+
return '<li class="link-directory-entry"><a class="link-directory-link" href="' + escapeHtml(href) + '">' +
|
|
385
|
+
'<span class="link-directory-main"><strong class="link-directory-name">' + name + '</strong>' +
|
|
386
|
+
(summary ? '<span class="link-directory-summary">' + summary + '</span>' : '') + '</span>' +
|
|
387
|
+
'<span class="link-directory-destination" aria-hidden="true">' + (host ? '<span>' + escapeHtml(host) + '</span>' : '') +
|
|
388
|
+
'<span class="link-directory-arrow">↗</span></span></a></li>';
|
|
389
|
+
}).join('');
|
|
390
|
+
return '<section class="link-directory-section"><h2>' + escapeHtml(heading) + '</h2><ul class="link-directory-items" role="list">' + entries + '</ul></section>';
|
|
391
|
+
}
|
|
346
392
|
case 'feature-grid': {
|
|
347
393
|
const items = list(block.items, 'feature-grid.items', 1, 12);
|
|
348
394
|
const heading = text(block.title, 'feature-grid.title', 200);
|
|
@@ -362,7 +408,11 @@ async function renderBlock(block, context, depth, index) {
|
|
|
362
408
|
const src = safeUrl(block.src, 'image.src');
|
|
363
409
|
const alt = text(block.alt, 'image.alt', 500);
|
|
364
410
|
const caption = text(block.caption, 'image.caption', 500, true);
|
|
365
|
-
|
|
411
|
+
const original=block.originalSrc?' data-original="'+escapeHtml(safeUrl(block.originalSrc,'image.originalSrc'))+'"':'';
|
|
412
|
+
const srcset = block.srcset === undefined ? '' : imageCandidates(block.srcset, 'image.srcset');
|
|
413
|
+
const responsive = srcset ? ' srcset="' + srcset + '" sizes="' + escapeHtml(text(block.sizes ?? '(max-width: 640px) calc(100vw - 32px), 560px', 'image.sizes', 200)) + '"' : '';
|
|
414
|
+
if (block.sizes !== undefined && !srcset) throw new Error('image.sizes requires image.srcset');
|
|
415
|
+
return '<figure class="image-block"><img src="' + escapeHtml(src) + '"'+original+responsive+' alt="' + escapeHtml(alt) + '" loading="lazy" decoding="async">' +
|
|
366
416
|
(caption ? '<figcaption>' + escapeHtml(caption) + '</figcaption>' : '') + '</figure>';
|
|
367
417
|
}
|
|
368
418
|
case 'quote': {
|
|
@@ -384,14 +434,22 @@ async function renderBlock(block, context, depth, index) {
|
|
|
384
434
|
return '<section class="faq-block"><h2>' + title + '</h2>' + items.map((item) => '<details><summary>' +
|
|
385
435
|
escapeHtml(text(item?.question, 'faq.question', 500)) + '</summary><p>' + escapeHtml(text(item?.answer, 'faq.answer', 4000)) + '</p></details>').join('') + '</section>';
|
|
386
436
|
}
|
|
437
|
+
case 'oembed': return renderEmbed(block, context);
|
|
438
|
+
case 'service': {
|
|
439
|
+
const id = text(block.integration, 'service.integration', 64);
|
|
440
|
+
if (context.servicesRendered.has(id)) throw new Error('A page may contain only one block per service');
|
|
441
|
+
context.servicesRendered.add(id);
|
|
442
|
+
return renderServiceBlock(context.document, context.locale, context.config, id, true);
|
|
443
|
+
}
|
|
444
|
+
case 'post-list':
|
|
387
445
|
case 'latest-posts': return renderLatestPosts(block, context);
|
|
388
446
|
case 'ad-slot': {
|
|
389
|
-
const integration = enabledIntegration(context.config, block.integration
|
|
447
|
+
const integration = enabledIntegration(context.config, block.integration);
|
|
390
448
|
return '<div class="ad-slot" data-edgepress-ad="' + escapeHtml(integration.id) + '" aria-label="' + escapeHtml(text(block.label, 'ad-slot.label', 120)) + '"></div>';
|
|
391
449
|
}
|
|
392
450
|
case 'captcha': {
|
|
393
451
|
if (!captchaProviders.has(block.provider)) throw new Error('captcha.provider must be a supported consent-gated provider');
|
|
394
|
-
const integration = context.config.browserPlugins.
|
|
452
|
+
const integration = context.config.browserPlugins.services.find(item => item.enabled !== false && item.provider === block.provider);
|
|
395
453
|
if (!integration) throw new Error('captcha block needs a configured consent-gated integration: ' + block.provider);
|
|
396
454
|
const siteKey = integration.siteKey;
|
|
397
455
|
return '<div class="captcha-block" data-edgepress-captcha="' + escapeHtml(block.provider) + '" data-sitekey="' + escapeHtml(siteKey) + '" aria-label="' + escapeHtml(text(block.label, 'captcha.label', 120)) + '"></div>';
|
|
@@ -406,28 +464,40 @@ async function renderElements(blocks, context, depth = 0) {
|
|
|
406
464
|
for (let index = 0; index < blocks.length; index += 1) {
|
|
407
465
|
context.blockCount = (context.blockCount || 0) + 1;
|
|
408
466
|
if (context.blockCount > 200) throw new Error('A page may contain at most 200 blocks including nested blocks');
|
|
409
|
-
|
|
467
|
+
try {
|
|
468
|
+
rendered.push(await renderBlock(blocks[index], context, depth, index));
|
|
469
|
+
} catch (error) {
|
|
470
|
+
throw new Error('elements[' + index + '] (element ' + (index + 1) + '): ' + error.message, {cause: error});
|
|
471
|
+
}
|
|
410
472
|
}
|
|
411
473
|
return rendered.join('\n');
|
|
412
474
|
}
|
|
413
475
|
|
|
414
476
|
export async function renderBlocks(rows, context) {
|
|
477
|
+
context = {...context, displayedPostBundles:new Set(), servicesRendered:new Set(), localToolIds:new Set(), blockCount:0, demoCount:0};
|
|
415
478
|
if (!Array.isArray(rows) || rows.length > 100) throw new Error('Page layout needs an array of at most 100 rows');
|
|
416
479
|
const rendered = [];
|
|
417
|
-
for (const row of rows) {
|
|
480
|
+
for (const [rowIndex, row] of rows.entries()) {
|
|
481
|
+
const rowLabel = 'blocks[' + rowIndex + '] (row ' + (rowIndex + 1) + ')';
|
|
418
482
|
if (!row || typeof row !== 'object' || Array.isArray(row) || 'type' in row) {
|
|
419
|
-
throw new Error('Page layout rows start with a columns count; put element types inside cells');
|
|
483
|
+
throw new Error(rowLabel + ': Page layout rows start with a columns count; put element types inside cells');
|
|
420
484
|
}
|
|
421
|
-
for (const key of Object.keys(row)) if (!['columns', 'cells'].includes(key)) throw new Error('Unsupported page row option: ' + key);
|
|
485
|
+
for (const key of Object.keys(row)) if (!['columns', 'cells'].includes(key)) throw new Error(rowLabel + ': Unsupported page row option: ' + key);
|
|
422
486
|
const columns = row.columns;
|
|
423
|
-
if (!Number.isInteger(columns) || columns < 1 || columns > 4) throw new Error('Each page row needs a columns count from 1 to 4');
|
|
424
|
-
if (!Array.isArray(row.cells) || row.cells.length !== columns) throw new Error('Each row needs one cells entry per configured column');
|
|
487
|
+
if (!Number.isInteger(columns) || columns < 1 || columns > 4) throw new Error(rowLabel + ': Each page row needs a columns count from 1 to 4');
|
|
488
|
+
if (!Array.isArray(row.cells) || row.cells.length !== columns) throw new Error(rowLabel + ': Each row needs one cells entry per configured column (columns: ' + columns + '; cells: ' + (Array.isArray(row.cells) ? row.cells.length : 'not a list') + ')');
|
|
425
489
|
context.blockCount = (context.blockCount || 0) + 1;
|
|
426
490
|
if (context.blockCount > 200) throw new Error('A page may contain at most 200 blocks including nested blocks');
|
|
427
|
-
const cells =
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
491
|
+
const cells = [];
|
|
492
|
+
for (const [index,blocks] of row.cells.entries()) {
|
|
493
|
+
const cellLabel = rowLabel + '.cells[' + index + '] (cell ' + (index + 1) + ')';
|
|
494
|
+
if (!Array.isArray(blocks) || !blocks.length) throw new Error(cellLabel + ': Every page row cell needs at least one element block; use cells: [[{ type: text, text: "Hello" }]]');
|
|
495
|
+
try {
|
|
496
|
+
cells.push('<div class="page-builder-cell" data-cell="' + (index + 1) + '">' + await renderElements(blocks, context, 1) + '</div>');
|
|
497
|
+
} catch (error) {
|
|
498
|
+
throw new Error(cellLabel + ': ' + error.message, {cause: error});
|
|
499
|
+
}
|
|
500
|
+
}
|
|
431
501
|
rendered.push('<div class="page-builder-row" data-columns="' + columns + '">' + cells.join('') + '</div>');
|
|
432
502
|
}
|
|
433
503
|
return rendered.join('\n');
|
package/src/page-check.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import { access, lstat,
|
|
2
|
-
import { extname,
|
|
1
|
+
import { access, lstat, readFile, readdir } from 'node:fs/promises';
|
|
2
|
+
import { extname, relative, resolve, sep } from 'node:path';
|
|
3
3
|
import { addVisualEvidence } from './report-visuals.js';
|
|
4
4
|
import { renderMarkdown, renderMarkdownExcerpt } from './markdown.js';
|
|
5
|
+
import { markdownFixture } from './markdown-fixture.js';
|
|
6
|
+
import {reportDirectory} from './report-path.js';
|
|
5
7
|
|
|
6
8
|
async function htmlFiles(directory) {
|
|
7
9
|
let entries;
|
|
@@ -61,7 +63,7 @@ async function contentDocuments(output, config) {
|
|
|
61
63
|
return documents;
|
|
62
64
|
}
|
|
63
65
|
|
|
64
|
-
async function markdownFeatureCoverage(
|
|
66
|
+
export async function markdownFeatureCoverage() {
|
|
65
67
|
const expected = [
|
|
66
68
|
['headings 2 through 6', /<h2\b[\s\S]*?<h3\b[\s\S]*?<h4\b[\s\S]*?<h5\b[\s\S]*?<h6\b/i],
|
|
67
69
|
['Setext heading syntax', /<h2\b[^>]*>Setext (?:heading level two|二级标题示例)<\/h2>/i],
|
|
@@ -72,11 +74,11 @@ async function markdownFeatureCoverage(root, files, sourceByFile) {
|
|
|
72
74
|
['inline and fenced code', /<code\b[\s\S]*?<\/code>[\s\S]*?<pre\b[\s\S]*?<code\b[^>]*class="language-js"/i],
|
|
73
75
|
['tilde-fenced and four-space-indented code', /class="language-text"[\s\S]*?<pre\b[^>]*><code[^>]*>const indented =/i],
|
|
74
76
|
['code spans containing a literal backtick', (html) => html.includes('<code>`</code>')],
|
|
75
|
-
['escaped Markdown punctuation and character references',
|
|
77
|
+
['escaped Markdown punctuation and character references', (html) => html.includes('<p>*escaped punctuation* < &</p>')],
|
|
76
78
|
['inline and reference links', /href="\/quick-start\/"/i],
|
|
77
79
|
['GFM automatic links', /href="https:\/\/www\.markdownguide\.org\//i],
|
|
78
80
|
['angle-bracket, www, and email automatic links', /href="https:\/\/commonmark\.org"[\s\S]*?href="http:\/\/www\.example\.com"[\s\S]*?href="mailto:team@example\.com"/i],
|
|
79
|
-
['images with alternative text', /<img\b[^>]*src="\/edgepress-markdown-guide\.
|
|
81
|
+
['images with alternative text', /<img\b[^>]*src="\/edgepress-markdown-guide\.png"[^>]*alt="[^"]+"/i],
|
|
80
82
|
['image-shaped video Markdown syntax documented', (html) => html.includes('field-recording.mp4')],
|
|
81
83
|
['nested lists with inline formatting', /<li>[\s\S]*?<strong\b[\s\S]*?<ol\b[\s\S]*?<li>/i],
|
|
82
84
|
['task lists with accessible status names', /markdown-task-status" role="img" aria-label="[^"]+"/i],
|
|
@@ -94,34 +96,26 @@ async function markdownFeatureCoverage(root, files, sourceByFile) {
|
|
|
94
96
|
catch { unsafeVideoRejected = true; }
|
|
95
97
|
const videoChecks = [
|
|
96
98
|
['image-shaped video syntax renders an accessible native player with deferred media',
|
|
97
|
-
/<video
|
|
99
|
+
/<video\b(?=[^>]*\bcontrols\b)(?=[^>]*\bplaysinline\b)(?=[^>]*\bpreload="none")(?=[^>]*\baria-label="A kite crossing a field")[^>]*><source src="https:\/\/media\.example\.org\/field-recording\.mp4" type="video\/mp4"/i.test(videoHtml)],
|
|
98
100
|
['video title renders as a caption', /<figcaption>Wind test<\/figcaption>/i.test(videoHtml)],
|
|
99
101
|
['unsafe video URL is rejected', unsafeVideoRejected],
|
|
100
102
|
['post-list excerpt preserves inline Markdown formatting', /<strong>formatted<\/strong>[\s\S]*?<em>emphasis<\/em>[\s\S]*?<code>code<\/code>/i.test(excerptHtml)]
|
|
101
103
|
];
|
|
102
|
-
const
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
];
|
|
110
|
-
return { path: relative(root, file).replace(/\\/g, '/'), checks, passed: checks.filter((item) => item.passed).length };
|
|
111
|
-
});
|
|
104
|
+
const content = await renderMarkdown(markdownFixture);
|
|
105
|
+
const checks = [
|
|
106
|
+
...expected.map(([feature, matcher]) => ({ feature, passed: typeof matcher === 'function' ? matcher(content) : matcher.test(content) })),
|
|
107
|
+
...videoChecks.map(([feature, passed]) => ({ feature, passed }))
|
|
108
|
+
];
|
|
109
|
+
const documents = [{ path: 'Built-in renderer fixture (not published)', checks,
|
|
110
|
+
passed: checks.filter((item) => item.passed).length }];
|
|
112
111
|
return {
|
|
113
|
-
status:
|
|
114
|
-
checks:
|
|
112
|
+
status: checks.every((item) => item.passed) ? 'pass' : 'fail',
|
|
113
|
+
checks: checks.length,
|
|
115
114
|
passed: documents.reduce((total, document) => total + document.passed, 0),
|
|
116
115
|
documents
|
|
117
116
|
};
|
|
118
117
|
}
|
|
119
118
|
|
|
120
|
-
function isInside(root, path) {
|
|
121
|
-
const rel = relative(root, path);
|
|
122
|
-
return rel !== '..' && !rel.startsWith('..' + sep) && !isAbsolute(rel);
|
|
123
|
-
}
|
|
124
|
-
|
|
125
119
|
async function resolveLocalLink(output, pageFile, href) {
|
|
126
120
|
let url;
|
|
127
121
|
try { url = new URL(href, 'https://edgepress.invalid/' + relative(output, pageFile).split(sep).join('/')); }
|
|
@@ -184,10 +178,7 @@ export async function checkPages(config) {
|
|
|
184
178
|
pageIds.set(file, new Set([...html.matchAll(/\bid\s*=\s*["']([^"']+)["']/gi)].map((match) => match[1])));
|
|
185
179
|
metrics.htmlBytes += Buffer.byteLength(html);
|
|
186
180
|
}
|
|
187
|
-
const markdownRendering = await markdownFeatureCoverage(
|
|
188
|
-
if (!markdownRendering.documents.length) {
|
|
189
|
-
addFinding('error', 'markdown-rendering', 'content/posts', 'The Markdown syntax guide post was not generated.');
|
|
190
|
-
}
|
|
181
|
+
const markdownRendering = await markdownFeatureCoverage();
|
|
191
182
|
for (const document of markdownRendering.documents) {
|
|
192
183
|
for (const check of document.checks.filter((item) => !item.passed)) {
|
|
193
184
|
addFinding('error', 'markdown-rendering', document.path, 'Markdown syntax did not render: ' + check.feature + '.');
|
|
@@ -298,6 +289,8 @@ export async function checkPages(config) {
|
|
|
298
289
|
try { managerScript = await readFile(managerAsset, 'utf8'); }
|
|
299
290
|
catch { /* The missing manager is reported below. */ }
|
|
300
291
|
}
|
|
292
|
+
const choiceAsset = all.find(file => /edgepress[\\/]plugins[\\/]consent[\\/]choices\.[a-f0-9]{16}\.js$/i.test(file));
|
|
293
|
+
const choicesScript = choiceAsset ? await readFile(choiceAsset, 'utf8') : '';
|
|
301
294
|
const htmlWithConsent = files.filter((file) => {
|
|
302
295
|
const html = sourceByFile.get(file) || '';
|
|
303
296
|
return html.includes('edgepress-privacy-config') && /\/edgepress\/plugins\/consent\/manager\.[a-f0-9]{16}\.js/i.test(html);
|
|
@@ -309,7 +302,7 @@ export async function checkPages(config) {
|
|
|
309
302
|
managerScript.includes('serviceRetention') && managerScript.includes('servicePrivacyDetails') &&
|
|
310
303
|
managerScript.includes('integration.privacyUrl'));
|
|
311
304
|
consentCheck('stored choices are tied to proposed and effective dates',
|
|
312
|
-
managerScript.includes('proposedDate
|
|
305
|
+
managerScript.includes('makeChoice(') && /proposedDate:\s*consent\.proposedDate/.test(choicesScript) && /effectiveDate:\s*consent\.effectiveDate/.test(choicesScript));
|
|
313
306
|
consentCheck('new visitors start with every optional service off', managerScript.includes('checkbox.checked = false'));
|
|
314
307
|
consentCheck('accept and reject controls share the same component styling',
|
|
315
308
|
/\.privacy-manager \.privacy-panel \.privacy-accept\s*,\s*\.privacy-manager \.privacy-panel \.privacy-reject\s*\{[^}]*background:[^}]*\}/.test(styles));
|
|
@@ -395,22 +388,15 @@ export async function checkPages(config) {
|
|
|
395
388
|
hashedAssets,
|
|
396
389
|
issues,
|
|
397
390
|
scope: [
|
|
398
|
-
'
|
|
391
|
+
'Structural accessibility checks inspect generated HTML and CSS. Browser checks separately sample device emulation, the browser accessibility tree, and real Tab/Shift+Tab/Enter events when Chromium is available.',
|
|
399
392
|
'Page layout is measured with the consent manager hidden. The consent manager has separate component checks and screenshots, so its size does not change the page-content overflow result.',
|
|
400
393
|
'Agent-friendliness checks inspect metadata, structured data, internal links, robots.txt, sitemap.xml, and llms.txt. They do not guarantee crawler indexing or answer quality.',
|
|
401
394
|
'This automated report is not a legal-compliance assessment or a substitute for manual review.',
|
|
402
|
-
'
|
|
395
|
+
'Headless phone/tablet profiles and accessibility-tree inspection are simulations, not physical devices or NVDA, Narrator, VoiceOver, or TalkBack. Keyboard sampling does not cover every possible interaction; manual zoom, contrast and assistive-technology review remain necessary.',
|
|
396
|
+
'Markdown self-checks render a built-in fixture in memory without publishing tutorial articles or requiring a posts directory.'
|
|
403
397
|
]
|
|
404
398
|
};
|
|
405
|
-
const
|
|
406
|
-
await mkdir(requestedDirectory, { recursive: true });
|
|
407
|
-
const directoryInfo = await lstat(requestedDirectory);
|
|
408
|
-
if (directoryInfo.isSymbolicLink() || !directoryInfo.isDirectory()) {
|
|
409
|
-
throw new Error('Page-check report directory must be a real directory: ' + requestedDirectory);
|
|
410
|
-
}
|
|
411
|
-
const root = await realpath(config.root);
|
|
412
|
-
const directory = await realpath(requestedDirectory);
|
|
413
|
-
if (!isInside(root, directory)) throw new Error('Page-check report directory must stay inside the project root');
|
|
399
|
+
const directory = await reportDirectory(config.root);
|
|
414
400
|
const pdfPath = resolve(directory, 'page-check.pdf');
|
|
415
401
|
try {
|
|
416
402
|
const info = await lstat(pdfPath);
|
|
@@ -424,6 +410,10 @@ export async function checkPages(config) {
|
|
|
424
410
|
issues.push({ severity: 'warning', category: 'visual-evidence', page: 'report', message: report.visualEvidence.reason || 'PDF generation failed.' });
|
|
425
411
|
}
|
|
426
412
|
} catch (error) {
|
|
413
|
+
if (report.browserChecks && report.browserChecks.status !== 'pass' && report.browserChecks.status !== 'fail') {
|
|
414
|
+
report.browserChecks.status = report.browserChecks.results.length ? 'incomplete' : 'unavailable';
|
|
415
|
+
report.browserChecks.reason = error.message;
|
|
416
|
+
}
|
|
427
417
|
report.visualEvidence = { status: 'failed', reason: error.message };
|
|
428
418
|
issues.push({ severity: 'warning', category: 'visual-evidence', page: 'report', message: 'Screenshot or PDF generation failed: ' + error.message });
|
|
429
419
|
}
|
package/src/plugin-api.js
CHANGED
|
@@ -2,6 +2,9 @@ import { isAbsolute, relative, resolve, sep } from 'node:path';
|
|
|
2
2
|
import { realpath } from 'node:fs/promises';
|
|
3
3
|
import { pathToFileURL } from 'node:url';
|
|
4
4
|
import { renderMarkdown } from './markdown.js';
|
|
5
|
+
import {renderEmbed} from './embed.js';
|
|
6
|
+
import {renderDocument} from './document.js';
|
|
7
|
+
import {localizedContentUrl} from './i18n.js';
|
|
5
8
|
|
|
6
9
|
export async function createExtensions(config) {
|
|
7
10
|
const filters = new Map();
|
|
@@ -12,10 +15,13 @@ export async function createExtensions(config) {
|
|
|
12
15
|
const selected = context.config.i18n.translationsByLocale?.[locale];
|
|
13
16
|
return renderMarkdown(source, {
|
|
14
17
|
...context.config.markdown,
|
|
18
|
+
linkResolver:value=>localizedContentUrl(context.config,locale,value),
|
|
15
19
|
taskLabels: {
|
|
16
20
|
complete: selected?.markdownTaskComplete ?? base?.markdownTaskComplete ?? 'Task complete',
|
|
17
21
|
incomplete: selected?.markdownTaskIncomplete ?? base?.markdownTaskIncomplete ?? 'Task incomplete'
|
|
18
|
-
}
|
|
22
|
+
},
|
|
23
|
+
embedRenderer: block => renderEmbed(block, {config: context.config, locale}),
|
|
24
|
+
documentRenderer: block => renderDocument(block, {config: context.config, locale})
|
|
19
25
|
});
|
|
20
26
|
}, cacheable: true }]]);
|
|
21
27
|
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { plainText } from './markdown.js';
|
|
2
|
+
|
|
3
|
+
export function normalizeCategory(value) {
|
|
4
|
+
if (value === undefined || value === null || value === '') return 'uncategorized';
|
|
5
|
+
if (typeof value !== 'string' || value.length > 120 || !value.trim()) throw new Error('category must be a short, non-empty string');
|
|
6
|
+
const category = value.trim().normalize('NFKC').toLowerCase();
|
|
7
|
+
if (['blog', '博客'].includes(category)) return 'blog';
|
|
8
|
+
if (['uncategorized', 'uncategorised', '未分类', '未分類'].includes(category)) return 'uncategorized';
|
|
9
|
+
return category;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function categoryFilter(value) {
|
|
13
|
+
if (value === undefined) return [];
|
|
14
|
+
const values = typeof value === 'string' ? [value] : value;
|
|
15
|
+
if (!Array.isArray(values) || values.length > 20 || values.some(item => typeof item !== 'string' || !item.trim())) {
|
|
16
|
+
throw new Error('categories must be a category string or an array of at most 20 non-empty strings');
|
|
17
|
+
}
|
|
18
|
+
return [...new Set(values.map(normalizeCategory))];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function filterPostsByCategory(posts, value) {
|
|
22
|
+
const categories = categoryFilter(value);
|
|
23
|
+
return categories.length ? posts.filter(post => categories.includes(normalizeCategory(post.category))) : posts;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function categorySlug(value) {
|
|
27
|
+
const category = normalizeCategory(value);
|
|
28
|
+
if (/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(category)) return category;
|
|
29
|
+
return 'category-' + Array.from(category).map(char => char.codePointAt(0).toString(16)).join('-');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function safeAvatar(value, label = 'author avatar') {
|
|
33
|
+
if (value === undefined || value === null || value === '') return '';
|
|
34
|
+
if (typeof value !== 'string' || value.length > 2048 || /[\x00-\x20\\]/.test(value) || value.startsWith('//')) {
|
|
35
|
+
throw new Error(label + ' must be a safe site path or HTTPS URL');
|
|
36
|
+
}
|
|
37
|
+
const url = new URL(value, 'https://edgepress.invalid');
|
|
38
|
+
if (url.protocol !== 'https:' || url.username || url.password || (!value.startsWith('/') && !value.startsWith('https://'))) {
|
|
39
|
+
throw new Error(label + ' must be a safe site path or HTTPS URL');
|
|
40
|
+
}
|
|
41
|
+
return value;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function authorForPost(post, config) {
|
|
45
|
+
return { name: post.author || config.site.author?.name || config.site.seo?.author || '',
|
|
46
|
+
avatar: post.authorAvatar || config.site.author?.avatar || '' };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function readingMinutes(markdown) {
|
|
50
|
+
const content = plainText(markdown);
|
|
51
|
+
const cjk = /[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Hangul}]/gu;
|
|
52
|
+
const characters = (content.match(cjk) || []).length;
|
|
53
|
+
const words = (content.replace(cjk, ' ').match(/[\p{L}\p{N}]+(?:['’][\p{L}\p{N}]+)*/gu) || []).length;
|
|
54
|
+
return Math.max(1, Math.ceil(characters / 300 + words / 200));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function sortPinnedPosts(posts) {
|
|
58
|
+
return [...posts].sort((left,right) => Number(right.pinned === true) - Number(left.pinned === true) ||
|
|
59
|
+
right.date - left.date || left.bundlePath.localeCompare(right.bundlePath));
|
|
60
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// The CLI writes the metadata. Authors can start with ordinary Markdown prose.
|
|
2
|
+
export function postTemplate(title, date, locale, starter) {
|
|
3
|
+
return '---\ntitle: ' + JSON.stringify(title) + '\ndate: ' + date + '\nlang: ' + locale +
|
|
4
|
+
'\ncategory: uncategorized\ntags: []\n---\n\n' + starter + '\n';
|
|
5
|
+
}
|