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.
Files changed (485) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +216 -16
  3. package/config.yml +99 -127
  4. package/content/assets/edgepress/brand/avatar.png +0 -0
  5. package/content/assets/edgepress/brand/deploy-esa.svg +1 -0
  6. package/content/assets/edgepress/brand/home-hero-768.webp +0 -0
  7. package/content/assets/edgepress/brand/home-hero.png +0 -0
  8. package/content/assets/edgepress/brand/home-hero.webp +0 -0
  9. package/content/assets/edgepress/brand/mascot.png +0 -0
  10. package/content/assets/edgepress/brand/mascot.webp +0 -0
  11. package/content/assets/edgepress/brand/og-image.png +0 -0
  12. package/content/assets/edgepress/brand/og-image.webp +0 -0
  13. package/content/assets/edgepress/brand/sources/edgepress-avatar-master.png +0 -0
  14. package/content/assets/edgepress/brand/sources/export-manifest.json +181 -0
  15. package/content/assets/edgepress/brand/sources/prompts.json +12 -0
  16. package/content/assets/edgepress/favicon/apple-touch-icon.png +0 -0
  17. package/content/assets/edgepress/favicon/favicon-16.png +0 -0
  18. package/content/assets/edgepress/favicon/favicon-32.png +0 -0
  19. package/content/assets/edgepress/favicon/favicon-48.png +0 -0
  20. package/content/assets/edgepress/favicon/favicon-64.png +0 -0
  21. package/content/assets/edgepress/favicon/favicon.ico +0 -0
  22. package/content/assets/edgepress/icons/LICENSE.txt +43 -0
  23. package/content/assets/edgepress/icons/arrow-right.svg +16 -0
  24. package/content/assets/edgepress/icons/arrow-up-right.svg +16 -0
  25. package/content/assets/edgepress/icons/book-open.svg +16 -0
  26. package/content/assets/edgepress/icons/check-circle.svg +16 -0
  27. package/content/assets/edgepress/icons/check.svg +15 -0
  28. package/content/assets/edgepress/icons/cloud.svg +15 -0
  29. package/content/assets/edgepress/icons/code.svg +16 -0
  30. package/content/assets/edgepress/icons/cpu.svg +28 -0
  31. package/content/assets/edgepress/icons/file-text.svg +19 -0
  32. package/content/assets/edgepress/icons/github.svg +17 -0
  33. package/content/assets/edgepress/icons/globe.svg +17 -0
  34. package/content/assets/edgepress/icons/help-circle.svg +17 -0
  35. package/content/assets/edgepress/icons/home.svg +16 -0
  36. package/content/assets/edgepress/icons/layers.svg +17 -0
  37. package/content/assets/edgepress/icons/layout-grid.svg +18 -0
  38. package/content/assets/edgepress/icons/mail.svg +16 -0
  39. package/content/assets/edgepress/icons/newspaper.svg +18 -0
  40. package/content/assets/edgepress/icons/palette.svg +19 -0
  41. package/content/assets/edgepress/icons/plug.svg +18 -0
  42. package/content/assets/edgepress/icons/provenance.json +8 -0
  43. package/content/assets/edgepress/icons/puzzle.svg +15 -0
  44. package/content/assets/edgepress/icons/rocket.svg +18 -0
  45. package/content/assets/edgepress/icons/rss.svg +17 -0
  46. package/content/assets/edgepress/icons/search.svg +16 -0
  47. package/content/assets/edgepress/icons/settings.svg +16 -0
  48. package/content/assets/edgepress/icons/shield-check.svg +16 -0
  49. package/content/assets/edgepress/icons/sparkles.svg +18 -0
  50. package/content/assets/edgepress/icons/terminal.svg +16 -0
  51. package/content/assets/edgepress/icons/x.svg +16 -0
  52. package/{static → content/assets}/edgepress-markdown-guide.svg +1 -1
  53. package/content/assets/images/previews/edgepress-en.webp +0 -0
  54. package/content/assets/images/previews/edgepress-revisions-en.png +0 -0
  55. package/content/assets/images/previews/edgepress-workflow-en.90f4650b21013786.gif +0 -0
  56. package/content/assets/images/previews/edgepress-workflow-zh.41f1e0a685f47267.gif +0 -0
  57. package/content/assets/images/previews/edgepress-zh.webp +0 -0
  58. package/content/assets/images/previews/provenance.json +49 -0
  59. package/content/guides/markdown-media.md +38 -0
  60. package/content/guides/platforms.md +15 -0
  61. package/content/guides/product-demos.md +37 -0
  62. package/content/guides/zh-cn.md +62 -0
  63. package/content/pages/deployment/en.json +74 -0
  64. package/content/pages/deployment/zh-cn.json +74 -0
  65. package/content/pages/home/en.json +248 -0
  66. package/content/pages/home/zh-cn.json +248 -0
  67. package/content/pages/local-tools/en.json +14 -0
  68. package/content/pages/local-tools/zh-cn.json +14 -0
  69. package/content/pages/page-authoring/en.json +107 -0
  70. package/content/pages/page-authoring/zh-cn.json +107 -0
  71. package/content/pages/plugin-development/en.json +202 -0
  72. package/content/pages/plugin-development/zh-cn.json +202 -0
  73. package/content/pages/privacy-policy/en.json +171 -0
  74. package/content/pages/privacy-policy/zh-cn.json +171 -0
  75. package/content/pages/project-introduction/en.json +158 -0
  76. package/content/pages/project-introduction/zh-cn.json +158 -0
  77. package/content/pages/quick-start/en.json +325 -0
  78. package/content/pages/quick-start/zh-cn.json +325 -0
  79. package/content/pages/theme-development/en.json +353 -0
  80. package/content/pages/theme-development/zh-cn.json +349 -0
  81. package/content/posts/2026-09-24-edgepress-release-2026-9-1/en.md +1 -1
  82. package/content/posts/2026-09-24-edgepress-release-2026-9-1/zh-cn.md +1 -1
  83. package/content/posts/2026-10-03-edgepress-release-2026-10-3/en.md +16 -0
  84. package/content/posts/2026-10-03-edgepress-release-2026-10-3/zh-cn.md +16 -0
  85. package/content/posts/2026-10-03-edgepress-release-202610-4/en.md +22 -0
  86. package/content/posts/2026-10-03-edgepress-release-202610-4/zh-cn.md +20 -0
  87. package/content/posts/2026-10-03-edgepress-release-202610-5/en.md +15 -0
  88. package/content/posts/2026-10-03-edgepress-release-202610-5/zh-cn.md +15 -0
  89. package/edgeone.json +43 -0
  90. package/esa.jsonc +9 -0
  91. package/languages/base/en.json +75 -10
  92. package/languages/packs/zh-CN.json +74 -9
  93. package/netlify.toml +6 -0
  94. package/package.json +37 -6
  95. package/plugins/consent/index.js +10 -17
  96. package/project-compatibility.json +142 -25
  97. package/schemas/pages.schema.json +74 -0
  98. package/src/assets.js +146 -18
  99. package/src/browser-audit.js +158 -0
  100. package/src/build.js +34 -9
  101. package/src/cli.js +46 -41
  102. package/src/config.js +114 -63
  103. package/src/consent-csp.js +137 -0
  104. package/src/content.js +55 -30
  105. package/src/data-saver.js +194 -0
  106. package/src/deploy.js +28 -0
  107. package/src/document.js +44 -0
  108. package/src/embed.js +60 -0
  109. package/src/generators.js +101 -59
  110. package/src/i18n.js +19 -3
  111. package/src/icons.js +5 -4
  112. package/src/image-dimensions.js +17 -0
  113. package/src/local-tool.js +29 -0
  114. package/src/maintenance.js +25 -8
  115. package/src/markdown-fixture.js +20 -0
  116. package/src/markdown.js +80 -16
  117. package/src/media-assets.js +19 -0
  118. package/src/media-viewer.js +47 -0
  119. package/src/oembed.js +53 -0
  120. package/src/page-blocks.js +124 -54
  121. package/src/page-check.js +29 -39
  122. package/src/plugin-api.js +7 -1
  123. package/src/post-details.js +60 -0
  124. package/src/post-template.js +5 -0
  125. package/src/publication-time.js +47 -0
  126. package/src/report-path.js +19 -0
  127. package/src/report-visuals.js +91 -18
  128. package/src/revision-cards.js +30 -0
  129. package/src/revision-images.js +44 -0
  130. package/src/revisions.js +77 -0
  131. package/src/rss.js +100 -0
  132. package/src/service-block.js +18 -0
  133. package/src/theme.js +59 -9
  134. package/src/video-metadata.js +36 -0
  135. package/static/edgepress/data-saver.css +12 -0
  136. package/static/edgepress/data-saver.js +333 -0
  137. package/static/edgepress/document-viewer.js +35 -0
  138. package/static/edgepress/external-links-policy.js +14 -0
  139. package/static/edgepress/external-links.css +13 -0
  140. package/static/edgepress/external-links.js +120 -0
  141. package/static/edgepress/fonts/faces.css +2217 -0
  142. package/static/edgepress/fonts/noto-sans-OFL.txt +93 -0
  143. package/static/edgepress/fonts/noto-sans-cyrillic-ext-wght-normal.82ff72b28e6610f2.woff2 +0 -0
  144. package/static/edgepress/fonts/noto-sans-cyrillic-wght-normal.6ab64433de6077ca.woff2 +0 -0
  145. package/static/edgepress/fonts/noto-sans-devanagari-wght-normal.ea6a530b75d93de7.woff2 +0 -0
  146. package/static/edgepress/fonts/noto-sans-greek-ext-wght-normal.790b255c10c32e0b.woff2 +0 -0
  147. package/static/edgepress/fonts/noto-sans-greek-wght-normal.7f655207c2f6c140.woff2 +0 -0
  148. package/static/edgepress/fonts/noto-sans-latin-ext-wght-normal.6d5a79315528df19.woff2 +0 -0
  149. package/static/edgepress/fonts/noto-sans-latin-wght-normal.51ca196f49a33e79.woff2 +0 -0
  150. package/static/edgepress/fonts/noto-sans-sc-100-wght-normal.ad5f29028d5699a5.woff2 +0 -0
  151. package/static/edgepress/fonts/noto-sans-sc-101-wght-normal.e189b81fc22ca6af.woff2 +0 -0
  152. package/static/edgepress/fonts/noto-sans-sc-102-wght-normal.941c0c2971fc1d6d.woff2 +0 -0
  153. package/static/edgepress/fonts/noto-sans-sc-103-wght-normal.bafbd591af6493ff.woff2 +0 -0
  154. package/static/edgepress/fonts/noto-sans-sc-104-wght-normal.b0bf80ced89dd61b.woff2 +0 -0
  155. package/static/edgepress/fonts/noto-sans-sc-105-wght-normal.8d8b4e6f4a00621d.woff2 +0 -0
  156. package/static/edgepress/fonts/noto-sans-sc-106-wght-normal.bae74acc629d2e74.woff2 +0 -0
  157. package/static/edgepress/fonts/noto-sans-sc-107-wght-normal.a42eff5010de7b0d.woff2 +0 -0
  158. package/static/edgepress/fonts/noto-sans-sc-108-wght-normal.91882203459867be.woff2 +0 -0
  159. package/static/edgepress/fonts/noto-sans-sc-109-wght-normal.a91f8b038e5ce108.woff2 +0 -0
  160. package/static/edgepress/fonts/noto-sans-sc-110-wght-normal.bbca9081655c4523.woff2 +0 -0
  161. package/static/edgepress/fonts/noto-sans-sc-111-wght-normal.372be010ac0e87cb.woff2 +0 -0
  162. package/static/edgepress/fonts/noto-sans-sc-112-wght-normal.34a4580239eae8dc.woff2 +0 -0
  163. package/static/edgepress/fonts/noto-sans-sc-113-wght-normal.614ab8f616352583.woff2 +0 -0
  164. package/static/edgepress/fonts/noto-sans-sc-114-wght-normal.cf7b8344807a316e.woff2 +0 -0
  165. package/static/edgepress/fonts/noto-sans-sc-115-wght-normal.fb29957c7adccb9a.woff2 +0 -0
  166. package/static/edgepress/fonts/noto-sans-sc-116-wght-normal.a98dc5a726f82141.woff2 +0 -0
  167. package/static/edgepress/fonts/noto-sans-sc-117-wght-normal.d94b5af0c14838af.woff2 +0 -0
  168. package/static/edgepress/fonts/noto-sans-sc-118-wght-normal.76c9ddb85ae4578c.woff2 +0 -0
  169. package/static/edgepress/fonts/noto-sans-sc-119-wght-normal.b3b2374881f4cd93.woff2 +0 -0
  170. package/static/edgepress/fonts/noto-sans-sc-21-wght-normal.c1409c43e55ecff2.woff2 +0 -0
  171. package/static/edgepress/fonts/noto-sans-sc-22-wght-normal.77037ed42ff4e7b0.woff2 +0 -0
  172. package/static/edgepress/fonts/noto-sans-sc-23-wght-normal.38d0df21ac374e93.woff2 +0 -0
  173. package/static/edgepress/fonts/noto-sans-sc-24-wght-normal.8fa21e7e457acf3e.woff2 +0 -0
  174. package/static/edgepress/fonts/noto-sans-sc-25-wght-normal.f760b7a3871d214d.woff2 +0 -0
  175. package/static/edgepress/fonts/noto-sans-sc-26-wght-normal.be8386e7bd7facfe.woff2 +0 -0
  176. package/static/edgepress/fonts/noto-sans-sc-27-wght-normal.4895abcaddbd162e.woff2 +0 -0
  177. package/static/edgepress/fonts/noto-sans-sc-28-wght-normal.82540b6201a9b9ad.woff2 +0 -0
  178. package/static/edgepress/fonts/noto-sans-sc-29-wght-normal.ff21d8fcc60ce391.woff2 +0 -0
  179. package/static/edgepress/fonts/noto-sans-sc-30-wght-normal.1f76f6ffded00b9e.woff2 +0 -0
  180. package/static/edgepress/fonts/noto-sans-sc-31-wght-normal.e6c25be1bb08a63a.woff2 +0 -0
  181. package/static/edgepress/fonts/noto-sans-sc-32-wght-normal.0cda750d7d04a4e1.woff2 +0 -0
  182. package/static/edgepress/fonts/noto-sans-sc-33-wght-normal.b9732b5606bd87c5.woff2 +0 -0
  183. package/static/edgepress/fonts/noto-sans-sc-34-wght-normal.38aa326b4c84902a.woff2 +0 -0
  184. package/static/edgepress/fonts/noto-sans-sc-35-wght-normal.bd98226c8e0aa808.woff2 +0 -0
  185. package/static/edgepress/fonts/noto-sans-sc-36-wght-normal.0db1ca695a491d37.woff2 +0 -0
  186. package/static/edgepress/fonts/noto-sans-sc-37-wght-normal.8df409a4e0a237c2.woff2 +0 -0
  187. package/static/edgepress/fonts/noto-sans-sc-38-wght-normal.672d2d994994a3c3.woff2 +0 -0
  188. package/static/edgepress/fonts/noto-sans-sc-39-wght-normal.272d6d0e153f56e8.woff2 +0 -0
  189. package/static/edgepress/fonts/noto-sans-sc-4-wght-normal.9fafd93ac77b81ff.woff2 +0 -0
  190. package/static/edgepress/fonts/noto-sans-sc-40-wght-normal.5b9ef1ed48420aa7.woff2 +0 -0
  191. package/static/edgepress/fonts/noto-sans-sc-41-wght-normal.30aee47f660fd88a.woff2 +0 -0
  192. package/static/edgepress/fonts/noto-sans-sc-42-wght-normal.ec0014830db682b6.woff2 +0 -0
  193. package/static/edgepress/fonts/noto-sans-sc-43-wght-normal.43df2b5dad9bb835.woff2 +0 -0
  194. package/static/edgepress/fonts/noto-sans-sc-44-wght-normal.acaa6195041b1a92.woff2 +0 -0
  195. package/static/edgepress/fonts/noto-sans-sc-45-wght-normal.8cdb9903f770db21.woff2 +0 -0
  196. package/static/edgepress/fonts/noto-sans-sc-46-wght-normal.6590d0a6fe94571c.woff2 +0 -0
  197. package/static/edgepress/fonts/noto-sans-sc-47-wght-normal.b05afdd32b3bf722.woff2 +0 -0
  198. package/static/edgepress/fonts/noto-sans-sc-48-wght-normal.c9585f7d070700be.woff2 +0 -0
  199. package/static/edgepress/fonts/noto-sans-sc-49-wght-normal.4943895a4dfa5dcf.woff2 +0 -0
  200. package/static/edgepress/fonts/noto-sans-sc-5-wght-normal.e64f57937e3ae4fe.woff2 +0 -0
  201. package/static/edgepress/fonts/noto-sans-sc-50-wght-normal.13e51063d7743cfe.woff2 +0 -0
  202. package/static/edgepress/fonts/noto-sans-sc-51-wght-normal.fd4df1f02c2cb4d7.woff2 +0 -0
  203. package/static/edgepress/fonts/noto-sans-sc-52-wght-normal.06999a39c4c3a7ea.woff2 +0 -0
  204. package/static/edgepress/fonts/noto-sans-sc-53-wght-normal.767b7b9864658d91.woff2 +0 -0
  205. package/static/edgepress/fonts/noto-sans-sc-54-wght-normal.4cd00537a46f6f1a.woff2 +0 -0
  206. package/static/edgepress/fonts/noto-sans-sc-55-wght-normal.0118ec5d8c30b197.woff2 +0 -0
  207. package/static/edgepress/fonts/noto-sans-sc-56-wght-normal.a3e4a78929826930.woff2 +0 -0
  208. package/static/edgepress/fonts/noto-sans-sc-57-wght-normal.7623501009e4a6db.woff2 +0 -0
  209. package/static/edgepress/fonts/noto-sans-sc-58-wght-normal.56ae1a59a32e4a25.woff2 +0 -0
  210. package/static/edgepress/fonts/noto-sans-sc-59-wght-normal.7a377a40e99fd9bc.woff2 +0 -0
  211. package/static/edgepress/fonts/noto-sans-sc-6-wght-normal.17e15c4f0a5bfd92.woff2 +0 -0
  212. package/static/edgepress/fonts/noto-sans-sc-60-wght-normal.3657f66babdf6aff.woff2 +0 -0
  213. package/static/edgepress/fonts/noto-sans-sc-61-wght-normal.23919a2eb11d25dd.woff2 +0 -0
  214. package/static/edgepress/fonts/noto-sans-sc-62-wght-normal.a901edf25bc1cfae.woff2 +0 -0
  215. package/static/edgepress/fonts/noto-sans-sc-63-wght-normal.92f56f713cc0b62f.woff2 +0 -0
  216. package/static/edgepress/fonts/noto-sans-sc-64-wght-normal.4929c9f39a5a4bd9.woff2 +0 -0
  217. package/static/edgepress/fonts/noto-sans-sc-65-wght-normal.8daa61282fb62a11.woff2 +0 -0
  218. package/static/edgepress/fonts/noto-sans-sc-66-wght-normal.6479166925eb462b.woff2 +0 -0
  219. package/static/edgepress/fonts/noto-sans-sc-67-wght-normal.a6b0e11c4c761ffd.woff2 +0 -0
  220. package/static/edgepress/fonts/noto-sans-sc-68-wght-normal.65b77f1fe9d68dae.woff2 +0 -0
  221. package/static/edgepress/fonts/noto-sans-sc-69-wght-normal.b34f765cbbf4d02e.woff2 +0 -0
  222. package/static/edgepress/fonts/noto-sans-sc-70-wght-normal.a99d55c00e4f95b7.woff2 +0 -0
  223. package/static/edgepress/fonts/noto-sans-sc-71-wght-normal.3298bed30dc8d233.woff2 +0 -0
  224. package/static/edgepress/fonts/noto-sans-sc-72-wght-normal.6597209ca9707726.woff2 +0 -0
  225. package/static/edgepress/fonts/noto-sans-sc-73-wght-normal.e7ac1d4930eabd78.woff2 +0 -0
  226. package/static/edgepress/fonts/noto-sans-sc-74-wght-normal.1f0287be83a28e7a.woff2 +0 -0
  227. package/static/edgepress/fonts/noto-sans-sc-75-wght-normal.d9a4bb7bcc2be578.woff2 +0 -0
  228. package/static/edgepress/fonts/noto-sans-sc-76-wght-normal.5a019946bcadcec6.woff2 +0 -0
  229. package/static/edgepress/fonts/noto-sans-sc-77-wght-normal.7c445e85d04f5319.woff2 +0 -0
  230. package/static/edgepress/fonts/noto-sans-sc-78-wght-normal.7d4afb70f641781c.woff2 +0 -0
  231. package/static/edgepress/fonts/noto-sans-sc-79-wght-normal.5798753a721bb4ec.woff2 +0 -0
  232. package/static/edgepress/fonts/noto-sans-sc-80-wght-normal.db887159ca6e46f3.woff2 +0 -0
  233. package/static/edgepress/fonts/noto-sans-sc-81-wght-normal.71e5743bbd225b3d.woff2 +0 -0
  234. package/static/edgepress/fonts/noto-sans-sc-82-wght-normal.8c5094c3785fd04f.woff2 +0 -0
  235. package/static/edgepress/fonts/noto-sans-sc-83-wght-normal.4a60a1ccba905284.woff2 +0 -0
  236. package/static/edgepress/fonts/noto-sans-sc-84-wght-normal.3ed54e59ef4758e3.woff2 +0 -0
  237. package/static/edgepress/fonts/noto-sans-sc-85-wght-normal.e87e0a1342e1778c.woff2 +0 -0
  238. package/static/edgepress/fonts/noto-sans-sc-86-wght-normal.6d3ac3e20959dbcb.woff2 +0 -0
  239. package/static/edgepress/fonts/noto-sans-sc-87-wght-normal.0eeffd5daad5c17b.woff2 +0 -0
  240. package/static/edgepress/fonts/noto-sans-sc-88-wght-normal.b1d9582dfa268463.woff2 +0 -0
  241. package/static/edgepress/fonts/noto-sans-sc-89-wght-normal.8b437aeb9e9fcb5a.woff2 +0 -0
  242. package/static/edgepress/fonts/noto-sans-sc-90-wght-normal.83b0a59e0a5e045a.woff2 +0 -0
  243. package/static/edgepress/fonts/noto-sans-sc-91-wght-normal.382f72b98d4bb9d7.woff2 +0 -0
  244. package/static/edgepress/fonts/noto-sans-sc-97-wght-normal.5e52856457c5cdab.woff2 +0 -0
  245. package/static/edgepress/fonts/noto-sans-sc-98-wght-normal.ffded4e7fd560d93.woff2 +0 -0
  246. package/static/edgepress/fonts/noto-sans-sc-99-wght-normal.54e302c23f11a8fd.woff2 +0 -0
  247. package/static/edgepress/fonts/noto-sans-sc-OFL.txt +93 -0
  248. package/static/edgepress/fonts/noto-sans-sc-cyrillic-wght-normal.5932d441e7126669.woff2 +0 -0
  249. package/static/edgepress/fonts/noto-sans-sc-latin-ext-wght-normal.e316201f3fc493d1.woff2 +0 -0
  250. package/static/edgepress/fonts/noto-sans-sc-latin-wght-normal.c1d919c8701a86ed.woff2 +0 -0
  251. package/static/edgepress/fonts/noto-sans-sc-vietnamese-wght-normal.cba3d00c8db989f0.woff2 +0 -0
  252. package/static/edgepress/fonts/noto-sans-tc-0-wght-normal.63c21eda5d20203f.woff2 +0 -0
  253. package/static/edgepress/fonts/noto-sans-tc-100-wght-normal.87a7c76301c24a28.woff2 +0 -0
  254. package/static/edgepress/fonts/noto-sans-tc-101-wght-normal.c9e83fc4ffd53746.woff2 +0 -0
  255. package/static/edgepress/fonts/noto-sans-tc-102-wght-normal.9a00ae0e5735852b.woff2 +0 -0
  256. package/static/edgepress/fonts/noto-sans-tc-103-wght-normal.7cfe93b3221f4def.woff2 +0 -0
  257. package/static/edgepress/fonts/noto-sans-tc-104-wght-normal.2d5ac7497d84cb99.woff2 +0 -0
  258. package/static/edgepress/fonts/noto-sans-tc-105-wght-normal.0989885bb16bcd9b.woff2 +0 -0
  259. package/static/edgepress/fonts/noto-sans-tc-106-wght-normal.1faa921b035064e7.woff2 +0 -0
  260. package/static/edgepress/fonts/noto-sans-tc-107-wght-normal.2f6006745ae16d69.woff2 +0 -0
  261. package/static/edgepress/fonts/noto-sans-tc-108-wght-normal.a61e48c10a2cf4b9.woff2 +0 -0
  262. package/static/edgepress/fonts/noto-sans-tc-109-wght-normal.f28c767724736d2d.woff2 +0 -0
  263. package/static/edgepress/fonts/noto-sans-tc-110-wght-normal.4b04bd88c3dc7c5d.woff2 +0 -0
  264. package/static/edgepress/fonts/noto-sans-tc-111-wght-normal.b6251e1967932a52.woff2 +0 -0
  265. package/static/edgepress/fonts/noto-sans-tc-112-wght-normal.e264513b32f90660.woff2 +0 -0
  266. package/static/edgepress/fonts/noto-sans-tc-113-wght-normal.46cb347d88f3338e.woff2 +0 -0
  267. package/static/edgepress/fonts/noto-sans-tc-114-wght-normal.9f9e1e0d0ab2aeb4.woff2 +0 -0
  268. package/static/edgepress/fonts/noto-sans-tc-115-wght-normal.653dd2ab7d551b40.woff2 +0 -0
  269. package/static/edgepress/fonts/noto-sans-tc-116-wght-normal.4718bf3627191297.woff2 +0 -0
  270. package/static/edgepress/fonts/noto-sans-tc-117-wght-normal.b241a9409682af5f.woff2 +0 -0
  271. package/static/edgepress/fonts/noto-sans-tc-118-wght-normal.fda2800dc1c06852.woff2 +0 -0
  272. package/static/edgepress/fonts/noto-sans-tc-119-wght-normal.948af36bd63a72c7.woff2 +0 -0
  273. package/static/edgepress/fonts/noto-sans-tc-19-wght-normal.62191cdc3a10e60a.woff2 +0 -0
  274. package/static/edgepress/fonts/noto-sans-tc-20-wght-normal.65c90892c2e01657.woff2 +0 -0
  275. package/static/edgepress/fonts/noto-sans-tc-21-wght-normal.d3f59b094c334866.woff2 +0 -0
  276. package/static/edgepress/fonts/noto-sans-tc-22-wght-normal.b1983a041d7e4e1f.woff2 +0 -0
  277. package/static/edgepress/fonts/noto-sans-tc-23-wght-normal.16e2163b66c7732a.woff2 +0 -0
  278. package/static/edgepress/fonts/noto-sans-tc-24-wght-normal.72c8561976116537.woff2 +0 -0
  279. package/static/edgepress/fonts/noto-sans-tc-25-wght-normal.b2948d928ce4dd54.woff2 +0 -0
  280. package/static/edgepress/fonts/noto-sans-tc-26-wght-normal.d583236b90abedc9.woff2 +0 -0
  281. package/static/edgepress/fonts/noto-sans-tc-27-wght-normal.7d6073ba07597c9b.woff2 +0 -0
  282. package/static/edgepress/fonts/noto-sans-tc-28-wght-normal.517204d716b089cd.woff2 +0 -0
  283. package/static/edgepress/fonts/noto-sans-tc-29-wght-normal.10189a8f8ca38099.woff2 +0 -0
  284. package/static/edgepress/fonts/noto-sans-tc-30-wght-normal.695a71d72dfab3f7.woff2 +0 -0
  285. package/static/edgepress/fonts/noto-sans-tc-31-wght-normal.868d59473ef0e73c.woff2 +0 -0
  286. package/static/edgepress/fonts/noto-sans-tc-32-wght-normal.92efa2d4b3702ec0.woff2 +0 -0
  287. package/static/edgepress/fonts/noto-sans-tc-33-wght-normal.d2274bcd5636197d.woff2 +0 -0
  288. package/static/edgepress/fonts/noto-sans-tc-34-wght-normal.1ac8c3718ee2eed2.woff2 +0 -0
  289. package/static/edgepress/fonts/noto-sans-tc-35-wght-normal.00b3df730d5a4c73.woff2 +0 -0
  290. package/static/edgepress/fonts/noto-sans-tc-36-wght-normal.6963feb6c628b81f.woff2 +0 -0
  291. package/static/edgepress/fonts/noto-sans-tc-37-wght-normal.a1b4bd294af24cd1.woff2 +0 -0
  292. package/static/edgepress/fonts/noto-sans-tc-38-wght-normal.8edb2f7f2955921b.woff2 +0 -0
  293. package/static/edgepress/fonts/noto-sans-tc-39-wght-normal.37bf7394eb3148c6.woff2 +0 -0
  294. package/static/edgepress/fonts/noto-sans-tc-40-wght-normal.3efa655bc48f9133.woff2 +0 -0
  295. package/static/edgepress/fonts/noto-sans-tc-41-wght-normal.48d47014544ebd5d.woff2 +0 -0
  296. package/static/edgepress/fonts/noto-sans-tc-42-wght-normal.214c733a59d423e7.woff2 +0 -0
  297. package/static/edgepress/fonts/noto-sans-tc-43-wght-normal.c2c9876f138bb20f.woff2 +0 -0
  298. package/static/edgepress/fonts/noto-sans-tc-44-wght-normal.28928bfea29db06a.woff2 +0 -0
  299. package/static/edgepress/fonts/noto-sans-tc-45-wght-normal.bf3743f8155fa9d2.woff2 +0 -0
  300. package/static/edgepress/fonts/noto-sans-tc-46-wght-normal.06970903593781c3.woff2 +0 -0
  301. package/static/edgepress/fonts/noto-sans-tc-47-wght-normal.8932ed83a91794d4.woff2 +0 -0
  302. package/static/edgepress/fonts/noto-sans-tc-48-wght-normal.dfd9225628bcf234.woff2 +0 -0
  303. package/static/edgepress/fonts/noto-sans-tc-49-wght-normal.c27d341ad39d44eb.woff2 +0 -0
  304. package/static/edgepress/fonts/noto-sans-tc-50-wght-normal.01a0f4e7dfba0a56.woff2 +0 -0
  305. package/static/edgepress/fonts/noto-sans-tc-51-wght-normal.5a30022af3d30e0f.woff2 +0 -0
  306. package/static/edgepress/fonts/noto-sans-tc-52-wght-normal.4884f032342a3274.woff2 +0 -0
  307. package/static/edgepress/fonts/noto-sans-tc-53-wght-normal.537adcae44511270.woff2 +0 -0
  308. package/static/edgepress/fonts/noto-sans-tc-54-wght-normal.6bc8c7d3e47c6fde.woff2 +0 -0
  309. package/static/edgepress/fonts/noto-sans-tc-55-wght-normal.725a48711f1e1997.woff2 +0 -0
  310. package/static/edgepress/fonts/noto-sans-tc-56-wght-normal.f43ecaa73e0cd8fc.woff2 +0 -0
  311. package/static/edgepress/fonts/noto-sans-tc-57-wght-normal.15b24e9d42a3270f.woff2 +0 -0
  312. package/static/edgepress/fonts/noto-sans-tc-58-wght-normal.9b2d8a71667d3da6.woff2 +0 -0
  313. package/static/edgepress/fonts/noto-sans-tc-59-wght-normal.a68535a6b3289d89.woff2 +0 -0
  314. package/static/edgepress/fonts/noto-sans-tc-6-wght-normal.32cece1ae80c4f2b.woff2 +0 -0
  315. package/static/edgepress/fonts/noto-sans-tc-60-wght-normal.3b2c56a921d1cad4.woff2 +0 -0
  316. package/static/edgepress/fonts/noto-sans-tc-61-wght-normal.54f51f80a0740878.woff2 +0 -0
  317. package/static/edgepress/fonts/noto-sans-tc-62-wght-normal.ba029c5bb6c07bf1.woff2 +0 -0
  318. package/static/edgepress/fonts/noto-sans-tc-63-wght-normal.380919beec4c7f3d.woff2 +0 -0
  319. package/static/edgepress/fonts/noto-sans-tc-64-wght-normal.f96fe0be479d201f.woff2 +0 -0
  320. package/static/edgepress/fonts/noto-sans-tc-65-wght-normal.7d544b80701fe678.woff2 +0 -0
  321. package/static/edgepress/fonts/noto-sans-tc-66-wght-normal.fab669c07a1e3bee.woff2 +0 -0
  322. package/static/edgepress/fonts/noto-sans-tc-67-wght-normal.0001bb744743fdfa.woff2 +0 -0
  323. package/static/edgepress/fonts/noto-sans-tc-68-wght-normal.1a57eda03886b43c.woff2 +0 -0
  324. package/static/edgepress/fonts/noto-sans-tc-69-wght-normal.017fecc749bcd6d5.woff2 +0 -0
  325. package/static/edgepress/fonts/noto-sans-tc-7-wght-normal.cd0f0cac743dbbfe.woff2 +0 -0
  326. package/static/edgepress/fonts/noto-sans-tc-70-wght-normal.79fb9d70b9534ec7.woff2 +0 -0
  327. package/static/edgepress/fonts/noto-sans-tc-71-wght-normal.04a6ea581b5f1195.woff2 +0 -0
  328. package/static/edgepress/fonts/noto-sans-tc-72-wght-normal.d3a4eb35499157b7.woff2 +0 -0
  329. package/static/edgepress/fonts/noto-sans-tc-73-wght-normal.d81f8484404ee465.woff2 +0 -0
  330. package/static/edgepress/fonts/noto-sans-tc-74-wght-normal.4a5da9e0c2702f7d.woff2 +0 -0
  331. package/static/edgepress/fonts/noto-sans-tc-75-wght-normal.8b87f9c6c164513a.woff2 +0 -0
  332. package/static/edgepress/fonts/noto-sans-tc-76-wght-normal.a71463dbfd9fa7b5.woff2 +0 -0
  333. package/static/edgepress/fonts/noto-sans-tc-77-wght-normal.d094414ea752cd52.woff2 +0 -0
  334. package/static/edgepress/fonts/noto-sans-tc-78-wght-normal.04f7c97bd9cbc73b.woff2 +0 -0
  335. package/static/edgepress/fonts/noto-sans-tc-79-wght-normal.1ed089d037a02049.woff2 +0 -0
  336. package/static/edgepress/fonts/noto-sans-tc-8-wght-normal.d052677dfb5db5bd.woff2 +0 -0
  337. package/static/edgepress/fonts/noto-sans-tc-80-wght-normal.6d41ca59cfd50442.woff2 +0 -0
  338. package/static/edgepress/fonts/noto-sans-tc-81-wght-normal.f09bd0afa8a58114.woff2 +0 -0
  339. package/static/edgepress/fonts/noto-sans-tc-82-wght-normal.7920841c526b1b02.woff2 +0 -0
  340. package/static/edgepress/fonts/noto-sans-tc-83-wght-normal.0c6f7f82db8da0d8.woff2 +0 -0
  341. package/static/edgepress/fonts/noto-sans-tc-84-wght-normal.f37c0a853b31265e.woff2 +0 -0
  342. package/static/edgepress/fonts/noto-sans-tc-85-wght-normal.4173bff087653b82.woff2 +0 -0
  343. package/static/edgepress/fonts/noto-sans-tc-86-wght-normal.dac35605ebec15c4.woff2 +0 -0
  344. package/static/edgepress/fonts/noto-sans-tc-87-wght-normal.8ac85fdd0b15a6bd.woff2 +0 -0
  345. package/static/edgepress/fonts/noto-sans-tc-88-wght-normal.61625b0b21e8407f.woff2 +0 -0
  346. package/static/edgepress/fonts/noto-sans-tc-89-wght-normal.a1da3dccbf00698e.woff2 +0 -0
  347. package/static/edgepress/fonts/noto-sans-tc-90-wght-normal.bf08f0355222a782.woff2 +0 -0
  348. package/static/edgepress/fonts/noto-sans-tc-91-wght-normal.2ecf295bba2bfaa3.woff2 +0 -0
  349. package/static/edgepress/fonts/noto-sans-tc-92-wght-normal.36508326bb68115f.woff2 +0 -0
  350. package/static/edgepress/fonts/noto-sans-tc-97-wght-normal.c712899e0fc9e947.woff2 +0 -0
  351. package/static/edgepress/fonts/noto-sans-tc-98-wght-normal.13a648e521d18d7f.woff2 +0 -0
  352. package/static/edgepress/fonts/noto-sans-tc-99-wght-normal.7f8c9a777e64159a.woff2 +0 -0
  353. package/static/edgepress/fonts/noto-sans-tc-OFL.txt +93 -0
  354. package/static/edgepress/fonts/noto-sans-tc-cyrillic-wght-normal.dd0980ced8e46cb3.woff2 +0 -0
  355. package/static/edgepress/fonts/noto-sans-tc-latin-ext-wght-normal.817d96335dc52ce6.woff2 +0 -0
  356. package/static/edgepress/fonts/noto-sans-tc-latin-wght-normal.50cb5fb0a522cfaf.woff2 +0 -0
  357. package/static/edgepress/fonts/noto-sans-tc-vietnamese-wght-normal.2a77fb7b70fa304d.woff2 +0 -0
  358. package/static/edgepress/fonts/noto-sans-vietnamese-wght-normal.791e3f213c155a5b.woff2 +0 -0
  359. package/static/edgepress/fonts/noto-serif-OFL.txt +93 -0
  360. package/static/edgepress/fonts/noto-serif-cyrillic-ext-wght-normal.7c7afb20d33e30ec.woff2 +0 -0
  361. package/static/edgepress/fonts/noto-serif-cyrillic-wght-normal.54e3bd0c4a002b0f.woff2 +0 -0
  362. package/static/edgepress/fonts/noto-serif-greek-ext-wght-normal.9b3f4c4ff3adefd2.woff2 +0 -0
  363. package/static/edgepress/fonts/noto-serif-greek-wght-normal.1252037b6f1d6e86.woff2 +0 -0
  364. package/static/edgepress/fonts/noto-serif-latin-ext-wght-normal.2654f8d1261a420f.woff2 +0 -0
  365. package/static/edgepress/fonts/noto-serif-latin-wght-normal.46281456234014ce.woff2 +0 -0
  366. package/static/edgepress/fonts/noto-serif-math-wght-normal.d4a6aba2cb4636b3.woff2 +0 -0
  367. package/static/edgepress/fonts/noto-serif-vietnamese-wght-normal.fb6a5ed734c2fbe3.woff2 +0 -0
  368. package/static/edgepress/fonts/provenance.json +1356 -0
  369. package/static/edgepress/fonts.css +14 -0
  370. package/static/edgepress/image-viewer.js +23 -0
  371. package/static/edgepress/images.css +4 -0
  372. package/static/edgepress/link-directory.css +14 -0
  373. package/static/edgepress/local-time.js +12 -0
  374. package/static/edgepress/media-viewer.css +1 -0
  375. package/static/edgepress/media-viewer.js +111 -0
  376. package/static/edgepress/navigation-select.js +14 -0
  377. package/static/edgepress/oembed.js +129 -0
  378. package/static/edgepress/plugins/backend/external-api.js +4 -0
  379. package/static/edgepress/plugins/backend/external-widget.js +13 -0
  380. package/static/edgepress/plugins/consent/choices.js +47 -0
  381. package/static/edgepress/plugins/consent/manager.js +56 -74
  382. package/static/edgepress/project-demo.js +93 -0
  383. package/static/edgepress/search.js +2 -2
  384. package/static/edgepress/services-consent.js +3 -0
  385. package/static/edgepress/services.js +15 -0
  386. package/static/edgepress/webmcp.js +86 -0
  387. package/tests/assets-runtime.test.mjs +91 -0
  388. package/tests/audit.test.mjs +142 -0
  389. package/tests/brand-regression.test.mjs +110 -0
  390. package/tests/consent-browser.test.mjs +132 -0
  391. package/tests/consent-config.test.mjs +85 -0
  392. package/tests/consent-csp.test.mjs +90 -0
  393. package/tests/data-saver-loading.test.mjs +158 -0
  394. package/tests/data-saver-network.test.mjs +99 -0
  395. package/tests/data-saver.test.mjs +294 -0
  396. package/tests/document-package.test.mjs +28 -0
  397. package/tests/document-viewer.test.mjs +152 -0
  398. package/tests/edge-issues.test.mjs +65 -0
  399. package/tests/embed-syntax.test.mjs +68 -0
  400. package/tests/external-links.test.mjs +198 -0
  401. package/tests/fixtures/recording.mp4 +0 -0
  402. package/tests/fonts.test.mjs +99 -0
  403. package/tests/image-dimensions.test.mjs +15 -0
  404. package/tests/image-viewer.test.mjs +36 -0
  405. package/tests/json-pages.test.mjs +66 -0
  406. package/tests/link-directory.test.mjs +51 -0
  407. package/tests/local-tool.test.mjs +34 -0
  408. package/tests/markdown-media.test.mjs +119 -0
  409. package/tests/media-block.test.mjs +102 -0
  410. package/tests/navigation.test.mjs +87 -0
  411. package/tests/oembed-layout.test.mjs +98 -0
  412. package/tests/oembed.test.mjs +96 -0
  413. package/tests/page-authoring.test.mjs +27 -0
  414. package/tests/page-formatting.test.mjs +37 -0
  415. package/tests/post-list.test.mjs +96 -0
  416. package/tests/project-demo.test.mjs +73 -0
  417. package/tests/publication-time.test.mjs +43 -0
  418. package/tests/release-version.test.mjs +28 -0
  419. package/tests/revisions.test.mjs +93 -0
  420. package/tests/rss.test.mjs +54 -0
  421. package/tests/services.test.mjs +35 -0
  422. package/tests/theme-layout.test.mjs +95 -0
  423. package/tests/video-metadata.test.mjs +28 -0
  424. package/tests/webmcp.test.mjs +121 -0
  425. package/tests/writing.test.mjs +101 -0
  426. package/themes/atelier/assets/style.css +144 -22
  427. package/themes/atelier/layouts/partials/head.html +8 -1
  428. package/themes/atelier/layouts/partials/header.html +3 -1
  429. package/themes/default/assets/data-saver-theme.css +2 -0
  430. package/themes/default/assets/style.css +139 -23
  431. package/themes/default/layouts/partials/head.html +9 -2
  432. package/themes/default/layouts/partials/header.html +3 -1
  433. package/themes/default/theme.json +1 -0
  434. package/themes/folio/README.md +7 -0
  435. package/themes/folio/assets/data-saver-theme.css +3 -0
  436. package/themes/folio/assets/folio/navigation.js +10 -0
  437. package/themes/folio/assets/style.css +516 -0
  438. package/themes/folio/layouts/layout.html +15 -0
  439. package/themes/folio/layouts/partials/footer.html +6 -0
  440. package/themes/folio/layouts/partials/head.html +30 -0
  441. package/themes/folio/layouts/partials/header.html +8 -0
  442. package/themes/folio/theme.json +7 -0
  443. package/themes/signal/assets/style.css +143 -21
  444. package/themes/signal/layouts/partials/head.html +8 -1
  445. package/themes/signal/layouts/partials/header.html +3 -1
  446. package/tools/README.md +14 -0
  447. package/tools/assets/format-pages.mjs +56 -0
  448. package/tools/assets/generate-brand-assets.py +160 -0
  449. package/tools/assets/sync-fonts.mjs +71 -0
  450. package/tools/assets/sync-icons.mjs +17 -0
  451. package/tools/assets/video-preview.mjs +15 -0
  452. package/tools/recordings/capture-document-demo.mjs +147 -0
  453. package/tools/recordings/demo-workbench.mjs +57 -0
  454. package/tools/recordings/desktop-recording.mjs +92 -0
  455. package/tools/recordings/provision-vm.sh +18 -0
  456. package/tools/recordings/start-vm-desktop.sh +28 -0
  457. package/tools/recordings/vm-desktop.mjs +124 -0
  458. package/tools/verification/README.md +44 -0
  459. package/tools/verification/data-saver-site.mjs +405 -0
  460. package/tools/verification/page-performance.mjs +76 -0
  461. package/tools/verification/verify-brand-assets.py +52 -0
  462. package/tools/verification/verify-brand.mjs +180 -0
  463. package/vercel.json +36 -0
  464. package/wrangler.jsonc +5 -7
  465. package/content/pages/home/en.md +0 -99
  466. package/content/pages/home/zh-cn.md +0 -99
  467. package/content/pages/plugin-development/en.md +0 -109
  468. package/content/pages/plugin-development/zh-cn.md +0 -109
  469. package/content/pages/privacy-policy/en.md +0 -93
  470. package/content/pages/privacy-policy/zh-cn.md +0 -93
  471. package/content/pages/project-introduction/en.md +0 -88
  472. package/content/pages/project-introduction/zh-cn.md +0 -88
  473. package/content/pages/quick-start/en.md +0 -153
  474. package/content/pages/quick-start/zh-cn.md +0 -153
  475. package/content/pages/theme-development/en.md +0 -132
  476. package/content/pages/theme-development/zh-cn.md +0 -132
  477. package/src/worker.js +0 -139
  478. package/static/edgepress/brand/edgepress-mark.svg +0 -7
  479. package/static/edgepress/brand/mascot-209.webp +0 -0
  480. package/static/edgepress/brand/mascot-418.png +0 -0
  481. package/static/edgepress/brand/mascot-418.webp +0 -0
  482. package/static/edgepress/brand/mascot.png +0 -0
  483. package/static/edgepress/brand/og-image.png +0 -0
  484. package/static/edgepress/brand/og-image.svg +0 -46
  485. package/static/edgepress/icons.svg +0 -24
@@ -0,0 +1,294 @@
1
+ import { test } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { createServer } from 'node:http';
4
+ import { mkdtemp, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
5
+ import { tmpdir } from 'node:os';
6
+ import { resolve } from 'node:path';
7
+ import { loadConfig } from '../src/config.js';
8
+ import { loadLanguagePacks } from '../src/i18n.js';
9
+ import { createExtensions } from '../src/plugin-api.js';
10
+ import { collectAssets, rewriteAssetLinks } from '../src/assets.js';
11
+ import { renderLayout } from '../src/theme.js';
12
+ import { configureDataSaver, dataSaverHtml, dataSaverLayout } from '../src/data-saver.js';
13
+ import { chromium, firefox } from 'playwright';
14
+
15
+ test('data-saving variants are opt-in and require safe theme-owned assets', async () => {
16
+ const config = await loadConfig(process.cwd());
17
+ assert.equal(config.site.dataSaver.enabled, false);
18
+ assert.match(dataSaverHtml('<html><head></head><body>Text</body></html>', config), /"enabled":false/);
19
+ config.site.dataSaver.enabled = true;
20
+ await configureDataSaver(config);
21
+ assert.equal(config.dataSaverTheme.stylesheet, 'data-saver-theme.css');
22
+ assert.deepEqual(config.dataSaverTheme.scripts, []);
23
+ const directory = await mkdtemp(resolve(tmpdir(), 'edgepress-data-'));
24
+ try {
25
+ await mkdir(resolve(directory, 'assets'));
26
+ config.resolvedPaths.theme = directory;
27
+ for (const variant of [undefined, { stylesheet: '../outside.css', scripts: [] }, { stylesheet: 'light.css', scripts: ['https://outside.test/app.js'] }, { stylesheet: 'light.css', scripts: [], unknown: true }]) {
28
+ await writeFile(resolve(directory, 'theme.json'), JSON.stringify({ dataSaver: variant }));
29
+ await assert.rejects(configureDataSaver(config));
30
+ }
31
+ for (const setting of ['enabled: yes', 'debug: yes', 'mode: invalid', 'detectSlowConnection: 1', 'respectBrowserPreference: null', 'promptAfterMs: 99', 'promptAfterMs: 60001', 'promptAfterMs: 500.5', 'unknown: true']) {
32
+ await writeFile(resolve(directory, 'config.yml'), 'site:\n dataSaver:\n ' + setting + '\n');
33
+ await assert.rejects(loadConfig(directory), /site.dataSaver/);
34
+ }
35
+ } finally { await rm(directory, { recursive: true, force: true }); }
36
+ });
37
+
38
+ test('text mode defers actual requests, preserves external prompts and loads only requested pictures', async () => {
39
+ const config = await loadConfig(process.cwd());
40
+ await loadLanguagePacks(config);
41
+ config.site.dataSaver = { enabled: true, mode: 'auto', detectSlowConnection: true, respectBrowserPreference: true };
42
+ config.site.externalLinks = { enabled: true, trustedOrigins: [] };
43
+ await configureDataSaver(config);
44
+ const extensions = await createExtensions(config);
45
+ const assets = await collectAssets(config, { documentViewer: true });
46
+ const files = new Map(assets.assets.map(item => ['/' + item.path, item.content]));
47
+ let html;
48
+ const image = await readFile('content/assets/edgepress/favicon/favicon-16.png');
49
+ const server = createServer((request, response) => {
50
+ const path = new URL(request.url, 'http://localhost').pathname;
51
+ const type = path.endsWith('.js') ? 'text/javascript' : path.endsWith('.css') ? 'text/css' : path.endsWith('.png') ? 'image/png' : 'text/html';
52
+ const content = path === '/' ? html : path.endsWith('.png') ? image : files.get(path);
53
+ response.writeHead(content ? 200 : 404, { 'Content-Type': type }).end(content || '');
54
+ });
55
+ await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
56
+ const origin = 'http://127.0.0.1:' + server.address().port;
57
+ config.site.url = origin;
58
+ const body = '<section class="post-content"><h1>Readable text</h1><p>Keep this paragraph.</p><a href="https://news.example.test/story">Outside</a><picture><source srcset="/one.png 1x"><img src="/one.png" alt="First image" width="1" height="1"></picture><img src="/two.png" alt="Second image"><video src="/large.mp4" poster="/poster.png" preload="auto"></video><section data-edgepress-document data-document-src="/large.pdf" data-document-title="Document"><div data-document-mount></div></section></section>';
59
+ const chart = '<figure><img src="/chart.png" alt="Sales grew from 10 to 15 units in September; the increase is 50%."><figcaption>Chart context and original source.</figcaption></figure>';
60
+ html = dataSaverHtml(rewriteAssetLinks(await renderLayout(config, extensions, { locale: 'en', title: 'Text', urlPath: '/' }, body + chart), assets.urlMap), config, assets.urlMap);
61
+ const browser = await chromium.launch({ headless: true, args: ['--disable-extensions'] });
62
+ try {
63
+ for (const hints of [{ saveData: true, effectiveType: '4g' }]) {
64
+ const context = await browser.newContext({ viewport: { width: 390, height: 844 }, colorScheme: 'dark', reducedMotion: 'reduce' });
65
+ await context.addInitScript(hints => Object.defineProperty(navigator, 'connection', { get: () => hints }), hints);
66
+ const page = await context.newPage();
67
+ const requests = [];
68
+ page.on('request', request => requests.push(request.url()));
69
+ await page.goto(origin);
70
+ assert.equal(await page.getAttribute('html', 'data-edgepress-data-mode'), 'text');
71
+ await page.waitForFunction(() => document.documentElement.dataset.edgepressDataReady === 'true');
72
+ await page.locator('main').waitFor({ state: 'visible' });
73
+ assert.match(await page.locator('main').innerText(), /Keep this paragraph/);
74
+ assert.match(await page.locator('main').innerText(), /First image/);
75
+ assert.equal(await page.locator('[data-data-description]').filter({ hasText: 'First image' }).isVisible(), true);
76
+ assert.match(await page.locator('main').innerText(), /Sales grew from 10 to 15 units/);
77
+ assert.equal(await page.locator('figcaption').innerText(), 'Chart context and original source.');
78
+ assert(!requests.some(url => /chart\.png/.test(url)));
79
+ assert.doesNotMatch(await page.locator('body').innerText(), /<img|<source|<video/);
80
+ assert.equal(await page.locator('.edgepress-data-controls').count(), 0);
81
+ assert.equal(await page.locator('header button[data-data-mode]').count(), 0);
82
+ assert(!requests.some(url => /one\.png|two\.png|poster\.png|large\.(?:pdf|mp4)|woff|\/style\.|document-viewer.*\.js|image-viewer.*\.js/.test(url)));
83
+ assert(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth));
84
+ if (await page.locator('.privacy-close').isVisible()) await page.locator('.privacy-close').click();
85
+ await page.getByText('Outside', { exact: true }).click();
86
+ assert.equal(await page.locator('.edgepress-external-url').textContent(), 'https://news.example.test/story');
87
+ await page.keyboard.press('Escape');
88
+ assert.equal(await page.evaluate(() => document.activeElement.textContent), 'Outside');
89
+ await page.getByRole('button', { name: 'Load this media: First image', exact: true }).click();
90
+ await page.locator('main img[src="/one.png"]').evaluate(element => element.decode());
91
+ assert(requests.some(url => url.endsWith('/one.png')));
92
+ assert(!requests.some(url => /two\.png|poster\.png|large\.(?:pdf|mp4)|woff|\/style\.|document-viewer.*\.js/.test(url)));
93
+ await page.getByRole('button', { name: 'Load this media: Second image', exact: true }).click();
94
+ await page.locator('main img[src="/two.png"]').evaluate(element => element.decode());
95
+ assert(!requests.some(url => /large\.(?:pdf|mp4)|woff|\/style\./.test(url)));
96
+ await Promise.all([page.waitForNavigation(), page.getByRole('button', { name: 'Show the full page', exact: true }).click()]);
97
+ assert.equal(await page.getAttribute('html', 'data-edgepress-data-mode'), 'full');
98
+ await page.waitForFunction(() => document.activeElement.tagName === 'MAIN');
99
+ assert.equal(await page.locator('[data-data-return]').isVisible(), false);
100
+ assert(requests.some(url => /\/style\./.test(url)));
101
+ await context.close();
102
+ }
103
+ const noScript = await browser.newContext({ javaScriptEnabled: false });
104
+ const page = await noScript.newPage();
105
+ await page.goto(origin);
106
+ assert.equal(await page.locator('main img').count(), 3);
107
+ assert.equal(await page.locator('main a').getAttribute('href'), 'https://news.example.test/story');
108
+ await noScript.close();
109
+ const initial = html;
110
+ for (const scenario of [
111
+ { browserPreference: false, slow: true, hints: { saveData: true, effectiveType: '4g' }, expected: 'full' },
112
+ { browserPreference: true, slow: false, hints: { saveData: false, effectiveType: '2g' }, expected: 'full' },
113
+ { browserPreference: false, slow: false, hints: { saveData: true, effectiveType: '2g' }, expected: 'full' },
114
+ { browserPreference: true, slow: true, hints: null, expected: 'full' },
115
+ { browserPreference: true, slow: true, hints: { saveData: true, effectiveType: '2g' }, stored: 'full', expected: 'full' },
116
+ { browserPreference: false, slow: false, hints: null, stored: 'text', expected: 'text' }
117
+ ]) {
118
+ html = initial.replace('"detectSlowConnection":true', '"detectSlowConnection":' + scenario.slow).replace('"respectBrowserPreference":true', '"respectBrowserPreference":' + scenario.browserPreference);
119
+ const context = await browser.newContext();
120
+ await context.addInitScript(scenario => {
121
+ Object.defineProperty(navigator, 'connection', { get: () => scenario.hints });
122
+ if (scenario.stored) localStorage.setItem('edgepress-data-mode', scenario.stored);
123
+ }, scenario);
124
+ const page = await context.newPage();
125
+ await page.goto(origin);
126
+ assert.equal(await page.getAttribute('html', 'data-edgepress-data-mode'), scenario.expected);
127
+ if (scenario.expected === 'full') await page.waitForFunction(() => document.documentElement.dataset.edgepressDataReady === 'true');
128
+ await context.close();
129
+ }
130
+ html = initial;
131
+ const blocked = await browser.newContext({ forcedColors: 'active' });
132
+ await blocked.addInitScript(() => {
133
+ for (const name of ['localStorage', 'sessionStorage']) Object.defineProperty(window, name, { get() { throw new DOMException('Blocked', 'SecurityError'); } });
134
+ });
135
+ const fallback = await blocked.newPage();
136
+ await fallback.goto(origin + '/?data-mode=text');
137
+ assert.equal(await fallback.getAttribute('html', 'data-edgepress-data-mode'), 'text');
138
+ await fallback.getByRole('button', { name: 'Show the full page', exact: true }).focus();
139
+ await Promise.all([fallback.waitForNavigation(), fallback.keyboard.press('Enter')]);
140
+ assert.equal(new URL(fallback.url()).searchParams.get('data-mode'), 'full');
141
+ await fallback.waitForFunction(() => document.activeElement.tagName === 'MAIN');
142
+ await blocked.close();
143
+ const alternate = await firefox.launch({ headless: true });
144
+ try {
145
+ const page = await alternate.newPage();
146
+ const requests = [];
147
+ page.on('request', request => requests.push(request.url()));
148
+ await page.goto(origin + '/?data-mode=text');
149
+ assert.equal(await page.getAttribute('html', 'data-edgepress-data-mode'), 'text');
150
+ assert(!requests.some(url => /one\.png|two\.png|poster\.png|large\.(?:pdf|mp4)|woff|\/style\./.test(url)), JSON.stringify(requests));
151
+ await page.getByRole('button', { name: 'Load this media: Second image', exact: true }).click();
152
+ await page.locator('main img[src="/two.png"]').evaluate(element => element.decode());
153
+ } finally { await alternate.close(); }
154
+ } finally {
155
+ await browser.close();
156
+ await new Promise(resolve => server.close(resolve));
157
+ }
158
+ });
159
+
160
+ test('full mode restores preloaded styles before revealing a stable keyboard-accessible page', async () => {
161
+ const config = await loadConfig(process.cwd());
162
+ await loadLanguagePacks(config);
163
+ config.site.dataSaver = { enabled: true, mode: 'full', detectSlowConnection: false, respectBrowserPreference: false };
164
+ await configureDataSaver(config);
165
+ const script = await readFile('static/edgepress/data-saver.js');
166
+ const stylesheet = await readFile('static/edgepress/data-saver.css');
167
+ const html = dataSaverHtml('<html lang="en"><head><link rel="preload" as="style" href="/full.css"><link rel="stylesheet" href="/full.css"></head><body><main><h1>Stable page</h1><p>Readable content</p></main></body></html>', config);
168
+ const server = createServer((request, response) => {
169
+ const path = new URL(request.url, 'http://localhost').pathname;
170
+ if (path === '/full.css') {
171
+ setTimeout(() => response.writeHead(200, { 'Content-Type': 'text/css' }).end('main{padding:100px 20px}'), 150);
172
+ return;
173
+ }
174
+ if (path.endsWith('.js')) response.writeHead(200, { 'Content-Type': 'text/javascript' }).end(script);
175
+ else if (path.endsWith('.css')) response.writeHead(200, { 'Content-Type': 'text/css' }).end(stylesheet);
176
+ else response.writeHead(200, { 'Content-Type': 'text/html' }).end(html);
177
+ });
178
+ await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
179
+ const browser = await chromium.launch({ headless: true, args: ['--disable-extensions'] });
180
+ try {
181
+ const page = await browser.newPage();
182
+ await page.addInitScript(() => {
183
+ window.layoutShift = 0;
184
+ new PerformanceObserver(list => {
185
+ for (const entry of list.getEntries()) if (!entry.hadRecentInput) window.layoutShift += entry.value;
186
+ }).observe({ type: 'layout-shift', buffered: true });
187
+ });
188
+ await page.goto('http://127.0.0.1:' + server.address().port + '/?data-mode=full');
189
+ await page.waitForFunction(() => document.documentElement.dataset.edgepressDataReady === 'true');
190
+ assert.equal(await page.locator('main').evaluate(element => getComputedStyle(element).paddingTop), '100px');
191
+ assert.equal(await page.locator('link[rel="stylesheet"][href="/full.css"]').count(), 1);
192
+ await page.waitForTimeout(100);
193
+ assert(await page.evaluate(() => window.layoutShift <= 0.01));
194
+ } finally {
195
+ await browser.close();
196
+ await new Promise(resolve => server.close(resolve));
197
+ }
198
+ });
199
+
200
+ test('theme variants stay small and translated text is escaped without activating inline media', async () => {
201
+ const config = await loadConfig(process.cwd());
202
+ await loadLanguagePacks(config);
203
+ config.site.dataSaver.enabled = true;
204
+ await configureDataSaver(config);
205
+ const result = dataSaverHtml('<html lang="zh-CN"><head></head><body><p>正文</p><img src="/picture.png" alt="&lt;script&gt;"></body></html>', config);
206
+ assert.match(result, /文字省流版/);
207
+ assert.match(result, /&lt;script&gt;/);
208
+ assert.match(result, /<template data-edgepress-data-html>&lt;img/);
209
+ const unlabelled = dataSaverHtml('<html lang="en"><head></head><body><img src="/chart.png"><img src="/decoration.png" alt=""></body></html>', config);
210
+ assert.equal((unlabelled.match(/<span[^>]*data-data-decorative/g) || []).length, 1, 'Missing alt is not permission to discard information');
211
+ for (const theme of ['default', 'folio']) {
212
+ const stylesheet = await readFile('themes/' + theme + '/assets/data-saver-theme.css', 'utf8');
213
+ assert(Buffer.byteLength(stylesheet) < 2048);
214
+ assert.doesNotMatch(stylesheet, /@import|@font-face|url\(/);
215
+ }
216
+ });
217
+ test('shared text layout retains themed landmarks and resolves local imports without font or media requests', async () => {
218
+ const config = { site: { dataSaver: { enabled: true } }, dataSaverTheme: { layoutStyles: ['style.css'] } };
219
+ const items = [
220
+ { path: 'style.css', content: Buffer.from('@import "/edgepress/fonts.css";@import "parts/cards.css";@import "https://external.test/full.css";body{display:flex;flex-direction:column;color:var(--ink)}main{flex:1}nav{display:flex}footer{border-top:1px solid var(--line)}.hero{background:u\\72l("/hero.png");padding:2rem}') },
221
+ { path: 'parts/cards.css', content: Buffer.from('@font-face{font-family:Heavy;src:url("/heavy.woff2")}.post-list{display:grid;grid-template-columns:repeat(2,minmax(0,1fr))}.card{--cover:url("/cover.png");color:CanvasText}') }
222
+ ];
223
+ const result = await dataSaverLayout(config, items);
224
+ assert.equal(result.path, 'edgepress/data-saver-layout.css');
225
+ const css = result.content.toString();
226
+ assert.match(css, /nav\{display:flex\}/);
227
+ assert.match(css, /footer\{border-top/);
228
+ assert.match(css, /grid-template-columns/);
229
+ assert.doesNotMatch(css, /@import|@font-face|url\(|hero\.png|heavy\.woff2|external\.test/);
230
+ await assert.rejects(dataSaverLayout(config, [{ path: 'style.css', content: Buffer.from('@import "style.css";') }]), /Circular/);
231
+ await assert.rejects(dataSaverLayout(config, [{ path: 'style.css', content: Buffer.from('@import "missing.css";') }]), /Missing/);
232
+ config.site.dataSaver.enabled = false;
233
+ assert.equal(await dataSaverLayout(config, items), null);
234
+ });
235
+
236
+ test('only a delayed first screen offers text view, without an automatic switch or permanent selector', async () => {
237
+ const config = await loadConfig(process.cwd());
238
+ await loadLanguagePacks(config);
239
+ config.site.dataSaver = { enabled: true, mode: 'auto', detectSlowConnection: true, respectBrowserPreference: true, promptAfterMs: 1500 };
240
+ await configureDataSaver(config);
241
+ const script = await readFile('static/edgepress/data-saver.js');
242
+ const stylesheet = await readFile('static/edgepress/data-saver.css');
243
+ const image = await readFile('content/assets/edgepress/favicon/favicon-16.png');
244
+ let delay = 0;
245
+ const html = dataSaverHtml('<html lang="en"><head><link rel="stylesheet" href="/full.css"></head><body><header><nav><a href="/second/">Second page</a></nav></header><main><h1>Separate page</h1><img src="/slow.png" width="200" height="100" alt="Delayed image"><a href="/third/">Third page</a></main><footer>Original footer</footer></body></html>', config);
246
+ const server = createServer((request, response) => {
247
+ const path = new URL(request.url, 'http://localhost').pathname;
248
+ if (path === '/slow.png') { setTimeout(() => response.writeHead(200, { 'Content-Type': 'image/png' }).end(image), delay); return; }
249
+ const content = path.endsWith('.js') ? script : path === '/full.css' ? 'body{margin:0}header,footer{padding:1rem}main{padding:2rem}' : path.endsWith('.css') ? stylesheet : html;
250
+ response.writeHead(200, { 'Content-Type': path.endsWith('.js') ? 'text/javascript' : path.endsWith('.css') ? 'text/css' : 'text/html' }).end(content);
251
+ });
252
+ await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
253
+ const origin = 'http://127.0.0.1:' + server.address().port;
254
+ const browser = await chromium.launch({ headless: true, args: ['--disable-extensions'] });
255
+ try {
256
+ const fast = await browser.newPage();
257
+ await fast.addInitScript(() => Object.defineProperty(navigator, 'connection', { get: () => ({ effectiveType: '2g', saveData: false }) }));
258
+ await fast.goto(origin);
259
+ await fast.waitForTimeout(650);
260
+ assert.equal(await fast.getAttribute('html', 'data-edgepress-data-mode'), 'full');
261
+ assert.equal(await fast.locator('[data-data-offer]').isVisible(), false);
262
+ assert.equal(await fast.locator('[data-data-return]').isVisible(), false);
263
+ delay = 2500;
264
+ const slow = await browser.newPage();
265
+ await slow.goto(origin, { waitUntil: 'domcontentloaded' });
266
+ const offer = slow.locator('[data-data-offer]');
267
+ await offer.waitFor({ state: 'visible' });
268
+ assert.equal(await slow.getAttribute('html', 'data-edgepress-data-mode'), 'full');
269
+ assert.equal(await slow.locator('header').count(), 1);
270
+ assert.equal(await slow.locator('footer').count(), 1);
271
+ await offer.getByRole('button', { name: 'Keep waiting' }).focus();
272
+ await slow.keyboard.press('Escape');
273
+ assert.equal(await offer.isVisible(), true);
274
+ assert.equal(await slow.locator('main').isVisible(), false);
275
+ assert.equal(await slow.locator('[data-data-wait]').isVisible(), true);
276
+ assert.equal(await slow.evaluate(() => document.activeElement.hasAttribute('data-data-progress-label')), true);
277
+ const choose = await browser.newPage();
278
+ await choose.goto(origin, { waitUntil: 'domcontentloaded' });
279
+ await choose.getByRole('button', { name: 'Use text view', exact: true }).waitFor();
280
+ await choose.getByRole('button', { name: 'Use text view', exact: true }).focus();
281
+ await Promise.all([choose.waitForNavigation(), choose.keyboard.press('Enter')]);
282
+ assert.equal(await choose.getAttribute('html', 'data-edgepress-data-mode'), 'text');
283
+ await choose.waitForFunction(() => document.activeElement.tagName === 'MAIN');
284
+ assert.equal(await choose.locator('header nav a').textContent(), 'Second page');
285
+ assert.equal(await choose.locator('footer').textContent(), 'Original footer');
286
+ assert.doesNotMatch(await choose.locator('body').innerText(), /<img/);
287
+ const disabled = await browser.newPage();
288
+ await disabled.route('**/edgepress/data-saver.js', route => route.fulfill({ contentType: 'text/javascript', body: script }));
289
+ await disabled.route(origin + '/', route => route.fulfill({ contentType: 'text/html', body: html.replace('"detectSlowConnection":true', '"detectSlowConnection":false') }));
290
+ await disabled.goto(origin, { waitUntil: 'domcontentloaded' });
291
+ await disabled.waitForTimeout(500);
292
+ assert.equal(await disabled.locator('[data-data-offer]').isVisible(), false);
293
+ } finally { await browser.close(); await new Promise(resolve => server.close(resolve)); }
294
+ });
@@ -0,0 +1,28 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { readFile } from 'node:fs/promises';
4
+ import { createRequire } from 'node:module';
5
+ import { dirname, resolve } from 'node:path';
6
+
7
+ test('cloud dependency is pinned to a published source commit and excludes the removed PPT engine', async () => {
8
+ const manifest = JSON.parse(await readFile(new URL('../package.json', import.meta.url), 'utf8'));
9
+ const lock = JSON.parse(await readFile(new URL('../package-lock.json', import.meta.url), 'utf8'));
10
+ const source = manifest.dependencies['@jsw-teams/document-viewer'];
11
+ assert.match(source, /^https:\/\/codeload\.github\.com\/jsw-teams\/document-viewer\/tar\.gz\/[a-f0-9]{40}$/);
12
+ assert.equal(lock.packages['node_modules/@jsw-teams/document-viewer'].resolved, source);
13
+ assert.ok(!manifest.files.includes('vendor'));
14
+ assert.ok(!lock.packages['node_modules/@file-viewer/ppt']);
15
+ assert.equal(manifest.overrides['@file-viewer/doc'].dompurify, '3.4.17');
16
+ const sanitizerVersions = Object.entries(lock.packages).filter(([path]) => path.endsWith('node_modules/dompurify')).map(([, dependency]) => dependency.version);
17
+ assert.deepEqual(sanitizerVersions, ['3.4.17']);
18
+ const require = createRequire(import.meta.url);
19
+ const root = resolve(dirname(require.resolve('@jsw-teams/document-viewer')), '..');
20
+ const component = JSON.parse(await readFile(resolve(root, 'package.json'), 'utf8'));
21
+ assert.ok(!component.dependencies['@file-viewer/ppt']);
22
+ assert.equal(component.dependencies.cfb, '1.2.2');
23
+ for (const path of ['src/ppt/parser.js', 'src/ppt/worker.js', 'src/renderers/ppt.js', 'src/icons.js', 'src/sheets/columns.js', 'src/sheets/column-controls.js', 'src/lucide-license.txt', 'tools/assets.mjs', 'NOTICE.md']) {
24
+ const content = await readFile(resolve(root, path), 'utf8');
25
+ assert.ok(content.length);
26
+ if (/\.(?:js|mjs)$/.test(path)) assert.doesNotMatch(content, /@file-viewer\/ppt|__DOCUMENT_PPT_ASSETS__|Flyfish Viewer|vendor-ppt|soffice/);
27
+ }
28
+ });
@@ -0,0 +1,152 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { loadConfig } from '../src/config.js';
4
+ import { loadLanguagePacks } from '../src/i18n.js';
5
+ import { createExtensions } from '../src/plugin-api.js';
6
+ import { renderBlocks } from '../src/page-blocks.js';
7
+ import { renderMarkdown, plainText } from '../src/markdown.js';
8
+ import { parseDocument, renderDocument } from '../src/document.js';
9
+ import { collectAssets, rewriteAssetLinks } from '../src/assets.js';
10
+ import { renderLayout } from '../src/theme.js';
11
+ import { contentPermissions } from '../src/consent-csp.js';
12
+ import { createRequire } from 'node:module';
13
+ import { extname } from 'node:path';
14
+ import { readFile } from 'node:fs/promises';
15
+ import { chromium } from 'playwright';
16
+
17
+ async function configuration() {
18
+ const config = await loadConfig(process.cwd());
19
+ await loadLanguagePacks(config);
20
+ return config;
21
+ }
22
+
23
+ test('articles and Pages share inline document previews, escaping, no download links and inert code examples', async () => {
24
+ const config = await configuration();
25
+ const renderer = (await createExtensions(config)).renderer('.md');
26
+ const context = { config, locale: 'en' };
27
+ const source = '!document[Annual report](/documents/report.docx)';
28
+ const post = await renderer.render('Before\n' + source + '\nAfter', { config, document: { locale: 'en' } });
29
+ const text = await renderBlocks([{ columns: 1, cells: [[{ type: 'text', text: source }]] }], context);
30
+ const block = await renderBlocks([{ columns: 1, cells: [[{ type: 'document', title: 'Annual report', src: '/documents/report.docx' }]] }], context);
31
+ for (const html of [post, text, block]) {
32
+ assert.match(html, /data-edgepress-document/);
33
+ assert.match(html, /data-document-format="docx"/);
34
+ assert.doesNotMatch(html, /Download original|<a\b/);
35
+ assert.match(html, /data-document-mount/);
36
+ assert.doesNotMatch(html, /<figcaption>/);
37
+ assert.doesNotMatch(html, /<iframe|<p>\s*<figure/);
38
+ }
39
+ for (const example of ['`' + source + '`', '```md\n' + source + '\n```', '\\' + source]) {
40
+ assert.doesNotMatch(await renderer.render(example, { config, document: { locale: 'en' } }), /data-edgepress-document/);
41
+ }
42
+ assert.match(plainText(source), /Annual report/);
43
+ assert.doesNotMatch(plainText(source), /!document|report\.docx/);
44
+ assert.match(renderDocument({ title: '<script>title</script>', src: '/report.pdf' }, context), /&lt;script&gt;/);
45
+ assert.throws(() => renderDocument({ title: 'Unsafe', src: 'javascript:alert(1)', format: 'pdf' }, context));
46
+ assert.throws(() => renderDocument({ title: 'Invalid format', src: '/report.exe' }, context), /Unsupported/);
47
+ assert.throws(() => renderDocument({ title: 'Unknown field', src: '/report.pdf', scripts: true }, context), /Unsupported document field/);
48
+ assert.equal(parseDocument('!document[Shared](https://files.example/download/123){format=pdf service=documents}').format, 'pdf');
49
+ assert.throws(() => parseDocument('!document[Title](/a.pdf){script=evil}'));
50
+ assert.doesNotMatch(await renderMarkdown(source), /data-edgepress-document/);
51
+ });
52
+
53
+ test('remote direct-file URLs require a registered enabled consent service and cannot expand origin permissions', async () => {
54
+ const config = await configuration();
55
+ const context = { config, locale: 'en' };
56
+ const block = { title: 'Shared', src: 'https://files.example/download/123', format: 'xlsx', service: 'documents' };
57
+ assert.throws(() => renderDocument(block, context), /registered external-api/);
58
+ config.browserPlugins.services = [{ id: 'documents', enabled: true, provider: 'external-api', backendUrl: 'https://files.example', purpose: 'Read shared documents' }];
59
+ assert.match(renderDocument(block, context), /data-document-service="documents"/);
60
+ assert.throws(() => renderDocument({ ...block, src: 'https://unapproved.example/report.xlsx' }, context), /registered service origin/);
61
+ assert.throws(() => renderDocument({ ...block, previewSrc: 'https://unapproved.example/report.xlsx' }, context), /registered service origin/);
62
+ config.browserPlugins.services[0].enabled = false;
63
+ assert.doesNotMatch(renderDocument(block, context), /data-edgepress-document/);
64
+ assert.deepEqual(contentPermissions([{ body: '<template><figure data-edgepress-document></figure></template>' }]), {});
65
+ assert.deepEqual(contentPermissions([{ body: '<figure data-edgepress-document></figure>' }]), { 'script-src': ["'wasm-unsafe-eval'"], 'font-src': ['blob:'] });
66
+ });
67
+
68
+ test('document modules and Worker references are fingerprinted together, with no proprietary runtime', { timeout: 60000 }, async () => {
69
+ const config = await configuration();
70
+ const bundle = await collectAssets(config, { documentViewer: true });
71
+ const paths = new Set(bundle.assets.map(asset => '/' + asset.path));
72
+ for (const source of ['/edgepress/document-viewer/index.js', '/edgepress/document-viewer/pdf.worker.js', '/edgepress/document-viewer/ppt.worker.js', '/edgepress/document-viewer/styles.css']) {
73
+ assert.match(bundle.urlMap[source], /\.[a-f0-9]{16}\.(?:js|css)$/);
74
+ assert.ok(paths.has(bundle.urlMap[source]));
75
+ }
76
+ const scripts = bundle.assets.filter(asset => asset.content && asset.path.startsWith('edgepress/document-viewer/') && asset.path.endsWith('.js')).map(asset => asset.content.toString()).join('\n');
77
+ assert.match(scripts, /new URL\(["']\.\.\/pdf\.worker\.[a-f0-9]{16}\.js/);
78
+ assert.match(scripts, /new Worker\(new URL\(["']\.\.\/ppt\.worker\.[a-f0-9]{16}\.js/);
79
+ assert.doesNotMatch(scripts, /vendor-ppt|ppt-native\.wasm|Flyfish Viewer/);
80
+ const content = renderDocument({ title: 'PDF', src: '/report.pdf' }, { config, locale: 'en' });
81
+ const html = rewriteAssetLinks(await renderLayout(config, { filter: async (_name, value) => value }, { title: 'PDF', locale: 'en', urlPath: '/documents/' }, content), bundle.urlMap);
82
+ assert.match(html, /document-viewer\.[a-f0-9]{16}\.js/);
83
+ assert.match(html, /styles\.[a-f0-9]{16}\.css/);
84
+ assert.match(html, /edgepress-document-viewer-config/);
85
+ });
86
+
87
+ test('hashed EdgePress viewer previews shared URLs inline only after current consent', { timeout: 60000 }, async () => {
88
+ const config = await configuration();
89
+ config.site.url = 'https://site.example/';
90
+ config.browserPlugins.services = [{ id: 'documents', enabled: true, provider: 'external-api', backendUrl: 'https://files.example',
91
+ name: 'Shared documents', purpose: 'Read a selected shared document.', dataCategories: 'IP address and selected URL.',
92
+ recipient: 'File owner', retention: 'No viewer storage', privacyUrl: 'https://files.example/privacy/' }];
93
+ const bundle = await collectAssets(config, { documentViewer: true });
94
+ const extensions = await createExtensions(config);
95
+ const content = renderDocument({ title: 'Shared worksheet', src: 'https://files.example/download/123', format: 'xlsx', service: 'documents' }, { config, locale: 'en' });
96
+ const html = rewriteAssetLinks(await renderLayout(config, extensions, { title: 'Shared document', locale: 'en', urlPath: '/documents/' }, content), bundle.urlMap);
97
+ const assets = new Map(bundle.assets.map(asset => ['/' + asset.path, asset]));
98
+ const require = createRequire(import.meta.url);
99
+ const { utils, write } = createRequire(require.resolve('@jsw-teams/document-viewer'))('xlsx');
100
+ const workbook = utils.book_new();
101
+ utils.book_append_sheet(workbook, utils.aoa_to_sheet([['Shared preview is inline']]), 'Shared');
102
+ const bytes = write(workbook, { type: 'buffer', bookType: 'xlsx' });
103
+ const browser = await chromium.launch({ headless: true, args: ['--disable-extensions'] });
104
+ try {
105
+ const context = await browser.newContext({ viewport: { width: 320, height: 850 } });
106
+ const page = await context.newPage();
107
+ await page.addInitScript(() => {
108
+ window.documentTools = {};
109
+ Object.defineProperty(document, 'modelContext', { value: { registerTool(tool) { window.documentTools[tool.name] = tool; } } });
110
+ });
111
+ const errors = [];
112
+ let loads = 0;
113
+ page.on('pageerror', error => errors.push(error.message));
114
+ page.on('download', () => errors.push('Unexpected browser download'));
115
+ await context.route('**/*', async route => {
116
+ const url = new URL(route.request().url());
117
+ if (url.origin === 'https://files.example') {
118
+ assert.equal(url.pathname, '/download/123');
119
+ loads++;
120
+ return route.fulfill({ contentType: 'application/octet-stream', headers: { 'Access-Control-Allow-Origin': 'https://site.example', 'Content-Disposition': 'inline' }, body: bytes });
121
+ }
122
+ if (url.pathname === '/documents/') return route.fulfill({ contentType: 'text/html', body: html });
123
+ const asset = assets.get(url.pathname);
124
+ if (!asset) return route.fulfill({ status: 404, body: '' });
125
+ const types = { '.js': 'text/javascript', '.mjs': 'text/javascript', '.css': 'text/css', '.svg': 'image/svg+xml', '.woff2': 'font/woff2' };
126
+ return route.fulfill({ contentType: types[extname(url.pathname)] || 'application/octet-stream', body: asset.content || await readFile(asset.source) });
127
+ });
128
+ await page.goto('https://site.example/documents/');
129
+ await page.getByRole('button', { name: 'Preview document', exact: true }).waitFor();
130
+ assert.equal(loads, 0);
131
+ await page.evaluate(() => document.querySelector('.document-viewer button').click());
132
+ assert.equal(loads, 0);
133
+ assert.equal(await page.locator('iframe').count(), 0);
134
+ await page.waitForFunction(() => window.documentTools.edgepress_control_document);
135
+ await page.evaluate(() => window.documentTools.edgepress_control_document.execute({ document: '1', action: 'open' }));
136
+ assert.equal(loads, 0);
137
+ assert.equal(await page.locator('iframe').count(), 0);
138
+ await page.evaluate(async choiceUrl => {
139
+ const { makeChoice } = await import(choiceUrl);
140
+ const settings = JSON.parse(document.getElementById('edgepress-privacy-config').textContent);
141
+ localStorage.setItem('edgepress-privacy-selection', JSON.stringify(makeChoice(settings, ['documents'])));
142
+ document.querySelector('[data-consent-ui]')?.remove();
143
+ document.querySelector('.document-viewer button').click();
144
+ }, bundle.urlMap['/edgepress/plugins/consent/choices.js']);
145
+ await page.frameLocator('iframe').getByText('Shared preview is inline', { exact: true }).waitFor();
146
+ assert.equal(loads, 1);
147
+ await page.evaluate(() => { localStorage.removeItem('edgepress-privacy-selection'); dispatchEvent(new Event('storage')); });
148
+ assert.equal(await page.locator('iframe').count(), 0);
149
+ assert.deepEqual(errors, []);
150
+ await context.close();
151
+ } finally { await browser.close(); }
152
+ });
@@ -0,0 +1,65 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import {readFile} from 'node:fs/promises';
4
+ import {resolve,extname} from 'node:path';
5
+ import {fileURLToPath} from 'node:url';
6
+ import {chromium} from 'playwright';
7
+ import {loadConfig} from '../src/config.js';
8
+ import {loadLanguagePacks} from '../src/i18n.js';
9
+ import {createExtensions} from '../src/plugin-api.js';
10
+ import {renderLayout} from '../src/theme.js';
11
+ import {renderBlocks} from '../src/page-blocks.js';
12
+ import {reserveImageDimensions} from '../src/image-dimensions.js';
13
+
14
+ test('Edge reports no unnamed form fields or unsized lazy images in responsive pages',async()=>{
15
+ const root=fileURLToPath(new URL('../',import.meta.url));
16
+ const config=await loadConfig(root);await loadLanguagePacks(config);
17
+ config.site.url='https://edge-issues.invalid';
18
+ config.resolvedPaths.theme=config.realPaths.theme=resolve(root,'themes/default');
19
+ config.site.navigation=[{key:'projects',url:'/projects/',labels:{en:'Projects'},children:[{key:'demo',url:'/demo/',labels:{en:'Demo'}}]}];
20
+ config.site.footerNavigation=[{key:'more',url:'/more/',labels:{en:'More'},children:[{key:'help',url:'/help/',labels:{en:'Help'}}]}];
21
+ const blocks=await renderBlocks([{columns:2,cells:Array.from({length:2},()=>[{type:'media-text',mediaType:'image',src:'/preview.svg',alt:'Demo',title:'Demo',text:'A responsive local preview.',animationSrc:'/demo.mp4'}])}],{config,locale:'en',site:{posts:[],pages:[]}});
22
+ const html=reserveImageDimensions(await renderLayout(config,await createExtensions(config),{locale:'en',title:'Demo',urlPath:'/'},'<h1>Demo</h1><article class="page-builder">'+blocks+'</article>'),new Map([['/preview.svg',{width:1200,height:750}]]),'index.html',config.site.url);
23
+ const browser=await chromium.launch({headless:true,...(process.platform==='win32'?{channel:'msedge'}:{})});
24
+ try{
25
+ for(const width of [390,1440]){
26
+ const context=await browser.newContext({viewport:{width,height:900}});
27
+ await context.route('**/*',async route=>{
28
+ const url=new URL(route.request().url());assert.equal(url.origin,config.site.url);
29
+ if(url.pathname==='/')return route.fulfill({contentType:'text/html',body:html,headers:{'Content-Security-Policy':"default-src 'self'; style-src 'self'; script-src 'self'; img-src 'self'; font-src 'self'; connect-src 'self'; media-src 'self'; worker-src 'self' blob:"}});
30
+ // Delay decoding: layout and the browser audit must succeed before bytes arrive.
31
+ if(url.pathname==='/preview.svg')return;
32
+ const path=url.pathname==='/style.css'?resolve(root,'themes/default/assets/style.css'):resolve(root,'static','.'+url.pathname);
33
+ const types={'.css':'text/css','.js':'text/javascript','.woff2':'font/woff2','.svg':'image/svg+xml'};
34
+ try{return route.fulfill({body:await readFile(path),contentType:types[extname(path)]||'application/octet-stream'});}
35
+ catch{return route.fulfill({status:404,body:''});}
36
+ });
37
+ const page=await context.newPage(),cdp=await context.newCDPSession(page),issues=[];
38
+ cdp.on('Audits.issueAdded',({issue})=>{
39
+ if(issue.code==='ContentSecurityPolicyIssue'||issue.code==='LazyLoadImageIssue'||issue.details.genericIssueDetails?.errorType==='FormEmptyIdAndNameAttributesForInputError')issues.push(issue);
40
+ });
41
+ await cdp.send('Audits.enable');await page.goto(config.site.url,{waitUntil:'domcontentloaded'});
42
+ await page.locator('.privacy-panel').waitFor({state:'visible'});
43
+ await page.locator('[data-navigation-select]').first().waitFor({state:'visible'});
44
+ const fields=await page.locator('input,select,textarea').evaluateAll(nodes=>nodes.map(node=>node.id||node.name));
45
+ assert.ok(fields.length>=5);assert.ok(fields.every(Boolean));assert.equal(new Set(fields).size,fields.length);
46
+ for(const image of await page.locator('img[loading=lazy]').all()){
47
+ const box=await image.boundingBox();assert.ok(box.width>0&&box.height>0);
48
+ assert.ok(Math.abs(box.width/box.height-1200/750)<.01);
49
+ assert.equal(await image.evaluate(node=>node.naturalWidth),0);
50
+ }
51
+ // Exercise both runtime photo paths without contacting a provider.
52
+ const photos=await page.evaluate(async()=>{
53
+ const {trustedEmbed,trustedProviderEmbed}=await import('/edgepress/oembed.js');
54
+ const data={version:'1.0',type:'photo',url:location.origin+'/preview.svg',width:1200,height:750,title:'Photo'};
55
+ const images=[trustedEmbed(data,location.origin,document),trustedProviderEmbed(data,{backendUrl:location.origin},document),trustedEmbed({...data,width:undefined,height:undefined},location.origin,document)];
56
+ document.querySelector('main').append(...images);
57
+ return images.map(image=>({width:Number(image.getAttribute('width')),height:Number(image.getAttribute('height')),ratio:image.style.aspectRatio}));
58
+ });
59
+ assert.equal(photos[0].width,1200);assert.equal(photos[1].height,750);assert.match(photos[2].ratio,/4\s*\/\s*3/);
60
+ await page.evaluate(()=>new Promise(resolve=>requestAnimationFrame(()=>requestAnimationFrame(resolve))));
61
+ assert.deepEqual(issues,[],`${width}px Edge Issues`);
62
+ await context.close();
63
+ }
64
+ }finally{await browser.close();}
65
+ });
@@ -0,0 +1,68 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import {loadConfig} from '../src/config.js';
4
+ import {loadLanguagePacks} from '../src/i18n.js';
5
+ import {createExtensions} from '../src/plugin-api.js';
6
+ import {renderBlocks} from '../src/page-blocks.js';
7
+
8
+ test('posts and Pages share validated embed lines, with inert code examples and disabled services', async () => {
9
+ const config = await loadConfig(process.cwd());
10
+ await loadLanguagePacks(config);
11
+ const service = {id:'youtube', provider:'oembed', backendUrl:'https://www.youtube.com', sourceOrigins:['https://www.youtube.com'], embedOrigins:['https://www.youtube.com'], name:'YouTube', embedPathPattern:'^/watch$', embedTemplate:'https://www.youtube.com/embed/example'};
12
+ // The fixture uses a local template and never contacts a real provider.
13
+ delete service.embedPathPattern;
14
+ config.browserPlugins.services = [service];
15
+ const renderer = (await createExtensions(config)).renderer('.md');
16
+ const source = '!embed[youtube](https://www.youtube.com/watch?v=example)';
17
+ const article = await renderer.render('Before\n' + source + '\nAfter', {config, document:{locale:'en'}});
18
+ const page = await renderBlocks([{columns:1, cells:[[{type:'text', text:'Before\n' + source + '\nAfter\n<script>unsafe</script>'}]]}], {config, locale:'en'});
19
+ for (const html of [article, page]) {
20
+ assert.match(html, /data-edgepress-oembed="youtube"/);
21
+ assert.match(html, /data-oembed-data/);
22
+ assert.doesNotMatch(html, /<iframe|<script|<p>\s*<figure/);
23
+ assert.match(html, /Before/); assert.match(html, /After/);
24
+ }
25
+ assert.match(page, /&lt;script&gt;unsafe/);
26
+ for (const example of ['`' + source + '`', '```md\n' + source + '\n```', '\\' + source]) {
27
+ assert.doesNotMatch(await renderer.render(example, {config, document:{locale:'en'}}), /data-edgepress-oembed/);
28
+ }
29
+ for (const unsafe of ['https://evil.example/video', 'https://user:password@www.youtube.com/watch', 'http://www.youtube.com/watch']) {
30
+ await assert.rejects(renderer.render('!embed[youtube](' + unsafe + ')', {config, document:{locale:'en'}}), /registered HTTPS/);
31
+ }
32
+ await assert.rejects(renderer.render('!embed[unknown](https://www.youtube.com/watch)', {config, document:{locale:'en'}}), /registered oembed service/);
33
+ service.enabled = false;
34
+ const disabled = await renderer.render(source, {config, document:{locale:'en'}});
35
+ assert.match(disabled, /data-oembed-disabled/);
36
+ assert.doesNotMatch(disabled, /data-edgepress-oembed|data-oembed-data|<iframe/);
37
+ });
38
+
39
+
40
+ test('article summaries and search text omit embeds while retaining prose and code examples',async()=>{
41
+ const {renderMarkdownExcerpt,plainText}=await import('../src/markdown.js');
42
+ const source='!embed[youtube](https://www.youtube.com/watch?v=example)\n\nA quiet afternoon.';
43
+ assert.equal((await renderMarkdownExcerpt(source)).trim(),'<p>A quiet afternoon.</p>');
44
+ assert.equal(plainText(source),'A quiet afternoon.');
45
+ assert.match(plainText('`!embed[youtube](https://www.youtube.com/watch?v=example)`'),/!embed\[youtube\]/);
46
+ });
47
+
48
+
49
+ test('Markdown article links and excerpts resolve raw and encoded locale placeholders without rewriting other URLs',async()=>{
50
+ const {localizedContentUrl}=await import('../src/i18n.js');const {renderMarkdown,renderMarkdownExcerpt}=await import('../src/markdown.js');
51
+ const config={i18n:{defaultLocale:'en'}};
52
+ for(const locale of ['en','zh-CN']){
53
+ const resolve=value=>localizedContentUrl(config,locale,value),prefix=locale==='en'?'':'/'+locale;
54
+ for(const token of ['[launge]','%5Blaunge%5D']){
55
+ const source='[Guide](/'+token+'/edgepress/guide/)';
56
+ for(const render of [renderMarkdown,renderMarkdownExcerpt])assert.match(await render(source,{linkResolver:resolve}),new RegExp('href="'+prefix+'/edgepress/guide/"'));
57
+ }
58
+ assert.equal(resolve('https://example.com/[launge]/'),'https://example.com/[launge]/');assert.equal(resolve('/explicit/'),'/explicit/');
59
+ assert.throws(()=>resolve('/other/[launge]/'),/first site-path segment/);
60
+ }
61
+ });
62
+
63
+
64
+ test('revision comparisons keep localized links valid without loading historical embeds',async()=>{
65
+ const {renderRevisionCards}=await import('../src/revision-cards.js');const {localizedContentUrl}=await import('../src/i18n.js');
66
+ const html=await renderRevisionCards('-[Old guide](/[launge]/guide/)\n+[Current guide](/[launge]/edgepress/)',key=>key,null,{linkResolver:value=>localizedContentUrl({i18n:{defaultLocale:'en'}},'zh-CN',value)});
67
+ assert.match(html,/href="\/zh-CN\/guide\/"/);assert.match(html,/href="\/zh-CN\/edgepress\/"/);assert.doesNotMatch(html,/%5Blaunge|data-edgepress-oembed/);
68
+ });