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
package/src/assets.js CHANGED
@@ -1,8 +1,16 @@
1
1
  import { createHash } from 'node:crypto';
2
- import { copyFile, readdir, readFile, writeFile, mkdir } from 'node:fs/promises';
2
+ import { copyFile, readdir, readFile, writeFile, mkdir, stat } from 'node:fs/promises';
3
3
  import { dirname, extname, relative, resolve, sep } from 'node:path';
4
4
  import * as posix from 'node:path/posix';
5
+ import {fileURLToPath} from 'node:url';
5
6
  import { mapLimit } from './concurrency.js';
7
+ import {extendConsentPolicy,serializeConsentHeaders} from './consent-csp.js';
8
+ import {imageViewerAssets, mediaViewerAssets} from './media-assets.js';
9
+ import {documentViewerAssets} from '@jsw-teams/document-viewer/assets';
10
+ import {collectImageDimensions} from './image-dimensions.js';
11
+ import {collectVideoMetadata} from './video-metadata.js';
12
+ import {dataSaverLayout} from './data-saver.js';
13
+ import { build, transform } from 'esbuild';
6
14
 
7
15
  async function walk(directory) {
8
16
  let entries;
@@ -19,15 +27,15 @@ async function walk(directory) {
19
27
 
20
28
  function isCodeAsset(path) {
21
29
  const extension = extname(path).toLowerCase();
22
- return extension === '.css' || extension === '.js';
30
+ return extension === '.css' || extension === '.js' || extension === '.mjs';
23
31
  }
24
32
 
25
33
  function referencedPaths(path, content, known) {
26
34
  if (!isCodeAsset(path)) return [];
27
35
  const text = content.toString('utf8');
28
- const expression = extname(path).toLowerCase() === '.js'
29
- ? /\b(?:from\s*|import\s*(?:\(\s*)?)['"]([^'"]+)['"]/g
30
- : /(?:@import\s+(?:url\()?\s*|url\(\s*)['"]?([^'")\s;]+)['"]?\s*\)?/gi;
36
+ const expression = ['.js', '.mjs'].includes(extname(path).toLowerCase())
37
+ ? /\b(?:from\s*|import\s*(?:\(\s*)?|new\s+URL\s*\(\s*)['"]([^'"]+)['"]/g
38
+ : /(?:@import\s*(?:url\()?\s*|url\(\s*)['"]?([^'")\s;]+)['"]?\s*\)?/gi;
31
39
  const refs = [];
32
40
  for (const match of text.matchAll(expression)) {
33
41
  const specifier = match[1];
@@ -62,22 +70,22 @@ function rewriteCodeReferences(item, text, outputByOriginal) {
62
70
  return rewritten + tail;
63
71
  };
64
72
 
65
- if (extension === '.js') {
66
- const expression = /(\b(?:from\s*|import\s*(?:\(\s*)?))(['"])([^'"]+)\2/g;
73
+ if (extension === '.js' || extension === '.mjs') {
74
+ const expression = /(\b(?:from\s*|import\s*(?:\(\s*)?|new\s+URL\s*\(\s*))(['"])([^'"]+)\2/g;
67
75
  return text.replace(expression, (match, prefix, quote, specifier) => {
68
76
  const rewritten = rewriteSpecifier(specifier);
69
77
  return rewritten === null ? match : prefix + quote + rewritten + quote;
70
78
  });
71
79
  }
72
80
 
73
- const expression = /((?:@import\s+(?:url\()?\s*|url\(\s*))(['"]?)([^)'"\s;]+)(['"]?\s*\)?)/gi;
81
+ const expression = /((?:@import\s*(?:url\()?\s*|url\(\s*))(['"]?)([^)'"\s;]+)(['"]?\s*\)?)/gi;
74
82
  return text.replace(expression, (match, prefix, quote, specifier, suffix) => {
75
83
  const rewritten = rewriteSpecifier(specifier);
76
84
  return rewritten === null ? match : prefix + quote + rewritten + suffix;
77
85
  });
78
86
  }
79
87
 
80
- export async function collectAssets(config) {
88
+ export async function collectAssets(config, { documentViewer = false, mediaViewer = false } = {}) {
81
89
  const sources = [];
82
90
  const directories = [
83
91
  { root: config.resolvedPaths.static, name: 'static' },
@@ -91,6 +99,24 @@ export async function collectAssets(config) {
91
99
  sources.push({ source, path, sourceName: name });
92
100
  }
93
101
  }
102
+ // Library consumers do not need to copy framework runtime source into their
103
+ // repositories. Project-owned overrides keep precedence for existing sites.
104
+ const runtimeRoot=fileURLToPath(new URL('../static/edgepress/',import.meta.url));
105
+ const supplied=new Set(sources.map(item=>item.path));
106
+ if (documentViewer) for (const item of await documentViewerAssets('edgepress/document-viewer')) if (!supplied.has(item.path)) sources.push(item);
107
+ for(const item of await imageViewerAssets())if(!supplied.has(item.path))sources.push(item);
108
+ if (mediaViewer) for (const item of await mediaViewerAssets()) if (!supplied.has(item.path)) sources.push(item);
109
+ for(const source of await walk(runtimeRoot)) {
110
+ const path='edgepress/'+relative(runtimeRoot,source).split(sep).join('/');
111
+ if(!supplied.has(path))sources.push({source,path,sourceName:'EdgePress runtime'});
112
+ }
113
+ // Built-in navigation and page icons live with the package's content assets,
114
+ // not its browser scripts. Include only this library, not example-site media.
115
+ const iconsRoot=fileURLToPath(new URL('../content/assets/edgepress/icons/',import.meta.url));
116
+ for(const source of await walk(iconsRoot)) {
117
+ const path='edgepress/icons/'+relative(iconsRoot,source).split(sep).join('/');
118
+ if(!supplied.has(path))sources.push({source,path,sourceName:'EdgePress icons'});
119
+ }
94
120
 
95
121
  const headers = [];
96
122
  const assetSources = [];
@@ -100,11 +126,71 @@ export async function collectAssets(config) {
100
126
  }
101
127
  const [headerContents, items] = await Promise.all([
102
128
  mapLimit(headers, config.concurrency, async (item) => (await readFile(item.source)).toString('utf8')),
103
- mapLimit(assetSources, config.concurrency, async (item) => ({
104
- ...item,
105
- content: isCodeAsset(item.path) ? await readFile(item.source) : null
106
- }))
129
+ mapLimit(assetSources, config.concurrency, async (item) => {
130
+ if (!isCodeAsset(item.path)) return { ...item, content: null };
131
+ const source = item.content ?? await readFile(item.source);
132
+ const result = await transform(source.toString('utf8'), {
133
+ loader: item.path.endsWith('.css') ? 'css' : 'js',
134
+ minifyWhitespace: true,
135
+ minifySyntax: true,
136
+ minifyIdentifiers: item.path === 'edgepress/data-saver.js',
137
+ legalComments: 'inline',
138
+ charset: 'utf8',
139
+ sourcefile: item.path
140
+ });
141
+ return { ...item, content: Buffer.from(result.code) };
142
+ })
107
143
  ]);
144
+ const coreEntries = ['edgepress/plugins/consent/manager.js', 'edgepress/external-links.js'];
145
+ const sourceByPath = new Map(items.map(item => [item.path, item]));
146
+ const bundleControl = async entry => {
147
+ const item = sourceByPath.get(entry);
148
+ if (!item) return;
149
+ const output = await build({
150
+ entryPoints: [entry],
151
+ bundle: true,
152
+ write: false,
153
+ format: 'esm',
154
+ minifyWhitespace: true,
155
+ minifySyntax: true,
156
+ minifyIdentifiers: false,
157
+ legalComments: 'inline',
158
+ charset: 'utf8',
159
+ plugins: [{
160
+ name: 'local-core-controls',
161
+ setup(builder) {
162
+ builder.onResolve({filter:/.*/}, args => {
163
+ const target = args.kind === 'entry-point' ? args.path : args.path.startsWith('/') ? args.path.slice(1) : posix.normalize(posix.join(posix.dirname(args.importer),args.path));
164
+ if (args.kind === 'dynamic-import' || !sourceByPath.has(target)) {
165
+ const path = sourceByPath.has(target) ? '/' + target : args.path;
166
+ return {path,external:true};
167
+ }
168
+ return {path:target,namespace:'local-core-controls'};
169
+ });
170
+ builder.onLoad({filter:/.*/,namespace:'local-core-controls'}, args => ({contents:sourceByPath.get(args.path).content.toString('utf8'),loader:'js'}));
171
+ }
172
+ }]
173
+ });
174
+ item.content = Buffer.from(output.outputFiles[0].contents);
175
+ };
176
+ for (const entry of coreEntries) await bundleControl(entry);
177
+ if (config.site.dataSaver?.enabled) {
178
+ const candidates = [...coreEntries, 'edgepress/language-select.js', 'edgepress/local-time.js', 'edgepress/navigation-select.js', 'edgepress/services-consent.js'];
179
+ const scripts = candidates.filter(path => {
180
+ const source = sourceByPath.get(path)?.source;
181
+ return source && relative(runtimeRoot,source).split(sep).join('/') === path.slice('edgepress/'.length);
182
+ });
183
+ if (scripts.length) {
184
+ const entry = 'edgepress/data-saver-controls.js';
185
+ const item = {path:entry,content:Buffer.from(scripts.map(path => 'import "/' + path + '";').join('\n')),sourceName:'Data-saving core controls'};
186
+ sourceByPath.set(entry,item);
187
+ items.push(item);
188
+ await bundleControl(entry);
189
+ config.dataSaverCore = {path:'/' + entry,scripts:scripts.map(path => '/' + path)};
190
+ }
191
+ }
192
+ const layout = await dataSaverLayout(config, items);
193
+ if (layout) items.push(layout);
108
194
  const originals = new Map();
109
195
  for (const item of items) {
110
196
  const normalized = item.path.toLowerCase();
@@ -156,7 +242,7 @@ export async function collectAssets(config) {
156
242
  const urlMap = Object.create(null);
157
243
  for (const item of items) urlMap['/' + item.path] = '/' + outputByOriginal.get(item.path);
158
244
  config.assetManifest = urlMap;
159
- return { assets, urlMap, existingHeaders: headerContents.join('\n\n') };
245
+ return { assets, urlMap, imageDimensions:await collectImageDimensions(assets), videoMetadata:await collectVideoMetadata(assets), existingHeaders: extendConsentPolicy(headerContents.join('\n\n'),config) };
160
246
  }
161
247
 
162
248
  export async function writeAssets(bundle, output, concurrency = 8) {
@@ -166,12 +252,54 @@ export async function writeAssets(bundle, output, concurrency = 8) {
166
252
  if (asset.source) await copyFile(asset.source, target);
167
253
  else await writeFile(target, asset.content);
168
254
  });
255
+ // Cloudflare already revalidates unversioned assets by default. A broad cache
256
+ // rule would be concatenated with exact immutable rules instead of overridden.
169
257
  const securityRules = '/*\n X-Content-Type-Options: nosniff\n Referrer-Policy: strict-origin-when-cross-origin';
258
+ // Unicode fonts contain hundreds of subsets. Static hosts accept 100 header
259
+ // rules, so group a directory only when every WOFF2 file in it is hashed.
260
+ const fontsByDirectory = new Map();
261
+ for (const asset of bundle.assets.filter(asset => asset.path.endsWith('.woff2'))) {
262
+ const directory = posix.dirname(asset.path);
263
+ if (!fontsByDirectory.has(directory)) fontsByDirectory.set(directory, []);
264
+ fontsByDirectory.get(directory).push(asset.path);
265
+ }
266
+ const fontDirectories = new Set([...fontsByDirectory].filter(([, paths]) =>
267
+ paths.length > 1 && paths.every(path => /\.[a-f0-9]{16}\.woff2$/.test(path))
268
+ ).map(([directory]) => directory));
170
269
  const immutableRules = bundle.assets
171
- .filter((asset) => /\.[a-f0-9]{16}\.(?:css|js)$/i.test(asset.path))
172
- .map((asset) => '/' + asset.path + '\n Cache-Control: public, max-age=31556952, immutable');
173
- const headers = [bundle.existingHeaders.trim(), securityRules, ...immutableRules].filter(Boolean).join('\n\n') + '\n';
174
- await writeFile(resolve(output, '_headers'), headers, 'utf8');
270
+ .filter((asset) => /\.[a-f0-9]{16}\.(?:css|m?js|json|png|jpe?g|webp|avif|svg|gif|mp4|webm|woff2?)$/i.test(asset.path))
271
+ .filter(asset => !asset.path.endsWith('.woff2') || !fontDirectories.has(posix.dirname(asset.path)))
272
+ .map((asset) => '/' + asset.path + '\n Cache-Control: public, max-age=31536000, immutable');
273
+ for (const directory of fontDirectories) immutableRules.push('/' + (directory === '.' ? '' : directory + '/') + '*.woff2\n Cache-Control: public, max-age=31536000, immutable');
274
+ const documentDirectories = new Set(bundle.assets.map(asset => asset.path.match(/^(.*\/pdf-assets\.[a-f0-9]{16})\//)?.[1]).filter(Boolean));
275
+ for (const directory of documentDirectories) immutableRules.push('/' + directory + '/*\n Cache-Control: public, max-age=31536000, immutable');
276
+ const mediaRules = await Promise.all(bundle.assets.filter(asset=>/\.(mp4|webm)$/i.test(asset.path)).map(async asset=>{
277
+ const {size}=await stat(resolve(output,...asset.path.split('/')));
278
+ return '/'+asset.path+'\n Content-Length: '+size+'\n Accept-Ranges: bytes';
279
+ }));
280
+ if (immutableRules.length + mediaRules.length > 85) {
281
+ const immutablePath = /\.[a-f0-9]{16}\.(?:css|m?js|json|png|jpe?g|webp|avif|svg|gif|mp4|webm|woff2?)$/i;
282
+ const patterns = new Map();
283
+ for (const asset of bundle.assets.filter(asset => immutablePath.test(asset.path))) {
284
+ const directory = posix.dirname(asset.path);
285
+ const prefix = directory === '.' ? '' : directory + '/';
286
+ const suffix = posix.extname(asset.path);
287
+ const matching = bundle.assets.filter(candidate => candidate.path.startsWith(prefix) && candidate.path.endsWith(suffix));
288
+ if (matching.length > 1 && matching.every(candidate => immutablePath.test(candidate.path))) {
289
+ patterns.set('/' + prefix + '*' + suffix, matching.map(candidate => '/' + candidate.path));
290
+ }
291
+ }
292
+ for (const [pattern, paths] of [...patterns].sort((first, second) => second[1].length - first[1].length)) {
293
+ const exact = immutableRules.filter(rule => paths.includes(rule.split('\n')[0]));
294
+ if (exact.length < 2) continue;
295
+ for (const rule of exact) immutableRules.splice(immutableRules.indexOf(rule), 1);
296
+ immutableRules.push(pattern + '\n Cache-Control: public, max-age=31536000, immutable');
297
+ }
298
+ }
299
+ const headers = [bundle.existingHeaders.trim(), securityRules, ...immutableRules, ...mediaRules].filter(Boolean).join('\n\n') + '\n';
300
+ const count = headers.split(/\r?\n/).filter(line => line && !/^\s|^#/.test(line)).length;
301
+ if (count > 100) throw new Error('Static header rules exceed 100 after safe cache grouping; reduce custom rules or per-file media headers.');
302
+ await writeFile(resolve(output, '_headers'), serializeConsentHeaders(headers), 'utf8');
175
303
  }
176
304
 
177
305
  export function rewriteAssetLinks(html, urlMap) {
@@ -0,0 +1,158 @@
1
+ export const deviceProfiles = [
2
+ { name: 'desktop', width: 1440, height: 900, deviceScaleFactor: 1, mobile: false, touch: false },
3
+ { name: 'phone', width: 390, height: 844, deviceScaleFactor: 3, mobile: true, touch: true },
4
+ { name: 'tablet', width: 820, height: 1180, deviceScaleFactor: 2, mobile: true, touch: true }
5
+ ];
6
+
7
+ // CDP input targets only the isolated headless browser, never the user's desktop.
8
+ async function key(send, name, modifiers = 0) {
9
+ const code = name === 'Enter' ? 13 : 9;
10
+ await send('Input.dispatchKeyEvent', { type: name === 'Enter' ? 'keyDown' : 'rawKeyDown',
11
+ key: name, code: name, windowsVirtualKeyCode: code, nativeVirtualKeyCode: code, modifiers,
12
+ ...(name === 'Enter' ? { text: '\r', unmodifiedText: '\r' } : {}) });
13
+ await send('Input.dispatchKeyEvent', { type: 'keyUp', key: name, code: name,
14
+ windowsVirtualKeyCode: code, nativeVirtualKeyCode: code, modifiers });
15
+ }
16
+
17
+ async function evaluate(send, fn) {
18
+ const result = await send('Runtime.evaluate', { expression: '(' + fn.toString() + ')()', returnByValue: true, awaitPromise: true });
19
+ if (result.exceptionDetails) throw new Error(result.exceptionDetails.text || 'Browser audit evaluation failed');
20
+ return result.result.value;
21
+ }
22
+
23
+ export async function auditBrowser(send, profile) {
24
+ const checks = [];
25
+ const check = (group, name, passed, detail = '') => checks.push({ group, name, passed: Boolean(passed), ...(detail ? { detail } : {}) });
26
+ const trace = [];
27
+ const previous = await evaluate(send, () => {
28
+ const manager = document.querySelector('[data-consent-ui]');
29
+ const display = manager?.style.display;
30
+ if (manager) manager.style.display = 'none';
31
+ const candidates = [...document.querySelectorAll('a[href],button,input,select,textarea,summary,[tabindex]')];
32
+ const visual = element => {
33
+ const style = getComputedStyle(element);
34
+ return [style.backgroundColor, style.borderColor, style.color, style.textDecorationLine].join('|');
35
+ };
36
+ window.__edgepressKeyboardAudit = { candidates, elements: [...document.querySelectorAll('*')], visual,
37
+ before: new Map(candidates.map(element => [element, visual(element)])) };
38
+ document.activeElement?.blur();
39
+ return { display: display ?? '', href: location.href };
40
+ });
41
+ try {
42
+ const measured = await evaluate(send, () => ({ width: innerWidth, density: devicePixelRatio, touch: navigator.maxTouchPoints,
43
+ overflow: Math.max(document.documentElement.scrollWidth, document.body.scrollWidth) > innerWidth + 1,
44
+ mainCount: document.querySelectorAll('main,[role="main"]').length,
45
+ h1Count: document.querySelectorAll('h1').length,
46
+ heading: document.querySelector('h1')?.textContent.trim() || '',
47
+ language: document.documentElement.lang,
48
+ schemes: { dark: matchMedia('(prefers-color-scheme: dark)').matches }
49
+ }));
50
+ check('device', 'Viewport matches the emulated device', measured.width === profile.width, String(measured.width));
51
+ check('device', 'Pixel density matches the emulated device', Math.abs(measured.density - profile.deviceScaleFactor) < .01);
52
+ check('device', profile.touch ? 'Mobile touch emulation exposes five touch points' : 'Native desktop touch capability is recorded',
53
+ profile.touch ? measured.touch === 5 : true, String(measured.touch));
54
+ check('device', 'No page overflow at this device size', !measured.overflow);
55
+ check('device', 'System color preference is emulated', measured.schemes.dark === (profile.colorScheme === 'dark'));
56
+ check('structure', 'One main landmark', measured.mainCount === 1);
57
+ check('structure', 'One page-level h1', measured.h1Count === 1);
58
+ check('structure', 'Document language is declared', measured.language.trim());
59
+
60
+ const { nodes } = await send('Accessibility.getFullAXTree');
61
+ const exposed = nodes.filter(node => !node.ignored);
62
+ const role = node => node.role?.value;
63
+ const name = node => String(node.name?.value || '').trim();
64
+ check('semantics', 'Main landmark is exposed to assistive technology', exposed.filter(node => role(node) === 'main').length === 1);
65
+ check('semantics', 'Page heading has its accessible name and level', exposed.some(node => role(node) === 'heading' &&
66
+ name(node) === measured.heading && node.properties?.some(property => property.name === 'level' && property.value.value === 1)));
67
+ const navigation = exposed.filter(node => role(node) === 'navigation');
68
+ check('semantics', 'Exposed navigation landmarks have names', navigation.every(node => name(node)));
69
+ const namedRoles = new Set(['button', 'link', 'combobox', 'textbox', 'searchbox', 'checkbox', 'radio', 'switch', 'slider', 'spinbutton', 'image']);
70
+ const unnamed = exposed.filter(node => namedRoles.has(role(node)) && !name(node));
71
+ check('semantics', 'Exposed controls and meaningful images have names', unnamed.length === 0,
72
+ unnamed.length ? unnamed.map(node => role(node)).join(', ') : '');
73
+
74
+ await send('Emulation.setFocusEmulationEnabled', { enabled: true });
75
+ const focus = () => evaluate(send, async () => {
76
+ const element = document.activeElement;
77
+ if (!element || element === document.body || element === document.documentElement) return null;
78
+ // Native Tab can start a smooth scroll; observe its result without scrolling for it.
79
+ await new Promise(resolve=>{
80
+ const started=performance.now();
81
+ const visible=()=>{const box=element.getBoundingClientRect();if((box.bottom>0&&box.top<innerHeight&&box.right>0&&box.left<innerWidth)||performance.now()-started>1500)return resolve();requestAnimationFrame(visible);};
82
+ visible();
83
+ });
84
+ const { elements, before, visual } = window.__edgepressKeyboardAudit;
85
+ const style = getComputedStyle(element), box = element.getBoundingClientRect();
86
+ const transparent = value => value === 'transparent' || /rgba\([^)]*,\s*0\)$/.test(value);
87
+ const outline = style.outlineStyle !== 'none' && parseFloat(style.outlineWidth) > 0 && !transparent(style.outlineColor);
88
+ const indicator = outline || style.boxShadow !== 'none' || (before.has(element) && before.get(element) !== visual(element));
89
+ const hidden = element.closest('[hidden],[inert],[aria-hidden="true"]');
90
+ return { index: elements.indexOf(element), tag: element.tagName.toLowerCase(),
91
+ name: (element.getAttribute('aria-label') || element.innerText || element.labels?.[0]?.textContent || '').trim().slice(0, 160),
92
+ visible: !hidden && style.visibility === 'visible' && style.display !== 'none' && box.width > 0 && box.height > 0 &&
93
+ box.bottom > 0 && box.top < innerHeight && box.right > 0 && box.left < innerWidth,
94
+ indicator, skip: element.matches('a[href^="#"]') &&
95
+ document.getElementById(element.getAttribute('href').slice(1))?.matches('main,[role="main"]') };
96
+ });
97
+ let reverseChecked = false;
98
+ let unexpectedRepeat = false;
99
+ for (let step = 0; step < 12; step += 1) {
100
+ await key(send, 'Tab');
101
+ const current = await focus();
102
+ if (!current) break; // Normal wrap through browser chrome is not a focus trap.
103
+ if (trace.length && current.index === trace[0].index) {
104
+ // Mobile browser chrome may not appear as a focus stop. A natural wrap
105
+ // is valid only after visiting all currently tabbable page controls.
106
+ const expected = await evaluate(send, () => {
107
+ const { candidates, elements } = window.__edgepressKeyboardAudit;
108
+ const visible = candidates.filter(element => {
109
+ const style = getComputedStyle(element), box = element.getBoundingClientRect();
110
+ return element.tabIndex >= 0 && !element.matches(':disabled') && !element.closest('[hidden],[inert]') &&
111
+ style.visibility === 'visible' && box.width > 0 && box.height > 0;
112
+ });
113
+ return visible.filter(element => {
114
+ if (!element.matches('input[type="radio"]') || !element.name) return true;
115
+ const group = visible.filter(other => other.matches('input[type="radio"]') && other.name === element.name && other.form === element.form);
116
+ return element === (group.find(other => other.checked) || group[0]);
117
+ }).map(element => elements.indexOf(element));
118
+ });
119
+ const visited = new Set(trace.map(item => item.index));
120
+ const missing = expected.filter(index => !visited.has(index));
121
+ check('keyboard', 'Native page wrap does not skip tabbable controls', missing.length === 0, missing.length ? String(missing.length) + ' unvisited controls' : '');
122
+ break;
123
+ }
124
+ if (trace.some(item => item.index === current.index)) { unexpectedRepeat = true; break; }
125
+ trace.push(current);
126
+ check('keyboard', 'Tab step ' + (step + 1) + ' lands on visible content', current.visible, current.name);
127
+ check('keyboard', 'Tab step ' + (step + 1) + ' has a visible focus indicator', current.indicator, current.name);
128
+ if (step === 1) {
129
+ await key(send, 'Tab', 8); // Shift modifier, scoped to CDP.
130
+ const reverse = await focus();
131
+ check('keyboard', 'Shift+Tab returns to the previous focus target', reverse?.index === trace[0].index);
132
+ reverseChecked = true;
133
+ await key(send, 'Tab');
134
+ }
135
+ }
136
+ check('keyboard', 'Forward and backward keyboard navigation are available', trace.length >= 2 && reverseChecked);
137
+ check('keyboard', 'No unexpected focus cycle during the sampled Tab sequence', !unexpectedRepeat);
138
+ check('keyboard', 'First Tab reaches the skip-to-main link', trace[0]?.skip);
139
+ const hasSkip = await evaluate(send, () => {
140
+ const first = window.__edgepressKeyboardAudit.candidates.find(element => element.matches('a[href^="#"]') &&
141
+ document.getElementById(element.getAttribute('href').slice(1))?.matches('main,[role="main"]'));
142
+ first?.focus();
143
+ return Boolean(first && document.activeElement === first);
144
+ });
145
+ if (hasSkip) await key(send, 'Enter');
146
+ const skipReached = hasSkip && await evaluate(send, () => {
147
+ if (!location.hash) return false;
148
+ const target = document.getElementById(decodeURIComponent(location.hash.slice(1)));
149
+ return Boolean(target?.matches('main,[role="main"]') && target.getBoundingClientRect().top < innerHeight);
150
+ });
151
+ check('keyboard', 'Enter on the skip link reaches the main landmark', skipReached);
152
+ return { checks, trace, status: checks.every(item => item.passed) ? 'pass' : 'fail' };
153
+ } finally {
154
+ await send('Runtime.evaluate', { expression: '(() => { const manager = document.querySelector("[data-consent-ui]"); if (manager) manager.style.display = ' +
155
+ JSON.stringify(previous.display) + '; history.replaceState(history.state, "", ' + JSON.stringify(previous.href) +
156
+ '); delete window.__edgepressKeyboardAudit; })()' });
157
+ }
158
+ }
package/src/build.js CHANGED
@@ -5,14 +5,18 @@ import { hostname } from 'node:os';
5
5
  import { mapLimit } from './concurrency.js';
6
6
  import { loadConfig } from './config.js';
7
7
  import { readDocuments, sortPostsNewest } from './content.js';
8
- import { plainText } from './markdown.js';
8
+ import { plainText, hasMarkdownVideo } from './markdown.js';
9
9
  import { createExtensions } from './plugin-api.js';
10
10
  import { collectAssets, rewriteAssetLinks, writeAssets } from './assets.js';
11
11
  import { loadLanguagePacks } from './i18n.js';
12
+ import { contentPermissions, extendConsentPolicy, injectConsentPolicy } from './consent-csp.js';
12
13
  import { generateBuiltinRoutes } from './generators.js';
14
+ import {reserveImageDimensions,collectImageDimensions} from './image-dimensions.js';
15
+ import {reserveVideoMetadata} from './video-metadata.js';
16
+ import { dataSaverHtml, configureDataSaver } from './data-saver.js';
13
17
 
14
18
  const RENDER_CACHE_VERSION = 3;
15
- const MARKDOWN_SECURITY_POLICY_VERSION = 4;
19
+ const MARKDOWN_SECURITY_POLICY_VERSION = 5;
16
20
 
17
21
  async function readCache(file) {
18
22
  try {
@@ -28,8 +32,9 @@ async function readCache(file) {
28
32
 
29
33
  async function markdownRuntimeVersion(root) {
30
34
  const lockPath = resolve(root, 'package-lock.json');
31
- const rendererSource = await readFile(new URL('./markdown.js', import.meta.url));
32
- const implementation = createHash('sha256').update(rendererSource).digest('hex');
35
+ const rendererSources=await Promise.all(['markdown.js','embed.js','document.js','oembed.js','plugin-api.js','i18n.js'].map(file=>readFile(new URL('./'+file,import.meta.url))));
36
+ const digest=createHash('sha256');for(const source of rendererSources)digest.update(source);
37
+ const implementation=digest.digest('hex');
33
38
  try {
34
39
  const lock = JSON.parse(await readFile(lockPath, 'utf8'));
35
40
  const marked = lock.packages?.['node_modules/marked']?.version;
@@ -143,6 +148,16 @@ export async function acquireBuildLock(directory) {
143
148
  throw new Error('Unable to acquire the project build lock');
144
149
  }
145
150
 
151
+ async function renameOutput(source, target) {
152
+ for(let attempt=0;;attempt++) {
153
+ try{return await rename(source,target);}
154
+ catch(error){
155
+ if(process.platform!=='win32'||!['EPERM','EBUSY'].includes(error.code)||attempt>=5)throw error;
156
+ await new Promise(resolve=>setTimeout(resolve,200*2**attempt));
157
+ }
158
+ }
159
+ }
160
+
146
161
  async function commitOutput(stage, output) {
147
162
  const backup = output + '.backup-' + randomUUID();
148
163
  let movedPrevious = false;
@@ -152,15 +167,15 @@ async function commitOutput(stage, output) {
152
167
  if (info.isSymbolicLink() || !info.isDirectory()) {
153
168
  throw new Error('Build output must be a real directory: ' + output);
154
169
  }
155
- await rename(output, backup);
170
+ await renameOutput(output, backup);
156
171
  movedPrevious = true;
157
172
  } catch (error) {
158
173
  if (error.code !== 'ENOENT') throw error;
159
174
  }
160
- await rename(stage, output);
175
+ await renameOutput(stage, output);
161
176
  } catch (error) {
162
177
  if (movedPrevious) {
163
- try { await rename(backup, output); }
178
+ try { await renameOutput(backup, output); }
164
179
  catch (restoreError) {
165
180
  throw new Error('Build commit failed and the previous output could not be restored from ' + backup + ': ' + restoreError.message, { cause: error });
166
181
  }
@@ -180,6 +195,10 @@ async function writeRoutes(routes, stage, assetBundle, extensions, site, config)
180
195
  let body = await extensions.filter('route:body', route.body, { route, site, config });
181
196
  if (typeof body !== 'string') throw new Error('route:body filters must return a string for ' + route.path);
182
197
  body = rewriteAssetLinks(body, assetBundle.urlMap);
198
+ body = reserveImageDimensions(body, assetBundle.imageDimensions, route.path, site.config.site.url);
199
+ body = reserveVideoMetadata(body, assetBundle.videoMetadata, route.path, site.config.site.url);
200
+ body = dataSaverHtml(body, config, assetBundle.urlMap);
201
+ body = injectConsentPolicy(body, assetBundle.existingHeaders);
183
202
  await writeFile(file, body, 'utf8');
184
203
  });
185
204
  }
@@ -187,6 +206,7 @@ async function writeRoutes(routes, stage, assetBundle, extensions, site, config)
187
206
  export async function buildSite(root = process.cwd(), options = {}) {
188
207
  const started = Date.now();
189
208
  const config = await loadConfig(root);
209
+ await configureDataSaver(config);
190
210
  if (options.preview === true) {
191
211
  config.preview = true;
192
212
  config.previewBuildId = randomUUID();
@@ -198,10 +218,10 @@ export async function buildSite(root = process.cwd(), options = {}) {
198
218
  try {
199
219
  await loadLanguagePacks(config);
200
220
  const extensions = await createExtensions(config);
201
- const assetBundle = await collectAssets(config);
202
221
  const sourceDocuments = await readDocuments(config);
203
222
  const loadedDocuments = await extensions.filter('content:loaded', sourceDocuments, { config });
204
223
  if (!Array.isArray(loadedDocuments)) throw new Error('content:loaded filters must return an array');
224
+ const assetBundle = await collectAssets(config, { documentViewer: loadedDocuments.some(document => /!document\[|"type":"document"/.test(JSON.stringify(document))), mediaViewer: loadedDocuments.some(document => document.video || hasMarkdownVideo(document.markdown) || /"type":"media-viewer"/.test(JSON.stringify(document))) });
205
225
 
206
226
  const cacheFile = resolve(config.resolvedPaths.cache, 'render-cache.json');
207
227
  const oldCache = await readCache(cacheFile);
@@ -216,7 +236,7 @@ export async function buildSite(root = process.cwd(), options = {}) {
216
236
  }
217
237
  const signature = createHash('sha256')
218
238
  .update(document.markdown + '\n' + JSON.stringify(config.markdown) + '\n' + document.locale + '\n' +
219
- MARKDOWN_SECURITY_POLICY_VERSION + '\n' + runtimeVersion)
239
+ MARKDOWN_SECURITY_POLICY_VERSION + '\n' + runtimeVersion + '\n' + JSON.stringify(config.browserPlugins) + '\n' + config.site.url)
220
240
  .digest('hex');
221
241
  let html = renderer.cacheable ? oldCache[signature] : undefined;
222
242
  if (html === undefined) html = await renderer.render(document.markdown, { config, document });
@@ -229,6 +249,9 @@ export async function buildSite(root = process.cwd(), options = {}) {
229
249
 
230
250
  const now = new Date();
231
251
  const visible = documents.filter((document) => !document.draft && publishedByLocalDate(document, now));
252
+ const revisionAssets=new Map(visible.flatMap(document=>document.revisionAssets??[]).map(asset=>[asset.path,asset]));
253
+ assetBundle.assets.push(...revisionAssets.values());
254
+ for(const [path,size] of await collectImageDimensions([...revisionAssets.values()]))assetBundle.imageDimensions.set(path,size);
232
255
  let site = {
233
256
  config,
234
257
  posts: sortPostsNewest(visible.filter((document) => document.kind === 'posts')),
@@ -251,6 +274,8 @@ export async function buildSite(root = process.cwd(), options = {}) {
251
274
  }
252
275
  routes = await extensions.filter('routes:created', routes, { site, config });
253
276
  validateRoutes(routes, output, assetBundle);
277
+ config.contentCsp = contentPermissions(routes);
278
+ assetBundle.existingHeaders = extendConsentPolicy(assetBundle.existingHeaders, config);
254
279
 
255
280
  await mkdir(dirname(output), { recursive: true });
256
281
  await mkdir(stage, { recursive: false });