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,17 @@
1
+ import { readFile, copyFile, mkdir, readdir, unlink } from 'node:fs/promises';
2
+ import { fileURLToPath } from 'node:url';
3
+ import { resolve } from 'node:path';
4
+
5
+ const root = fileURLToPath(new URL('../../',import.meta.url));
6
+ const output = resolve(root,'content/assets/edgepress/icons');
7
+ const library = resolve(root,'node_modules/lucide-static');
8
+ const source = await readFile(resolve(root,'src/icons.js'),'utf8');
9
+ const names = [...source.slice(source.indexOf('new Set(['),source.indexOf(']);')).matchAll(/'([a-z0-9-]+)'/g)].map(match=>match[1]);
10
+ const mappings = {github:'code-xml'};
11
+ await mkdir(output,{recursive:true});
12
+ for(const name of names) await copyFile(resolve(library,'icons',(mappings[name]||name)+'.svg'),resolve(output,name+'.svg'));
13
+ await copyFile(resolve(library,'LICENSE'),resolve(output,'LICENSE.txt'));
14
+ const {version} = JSON.parse(await readFile(resolve(library,'package.json'),'utf8'));
15
+ await import('node:fs/promises').then(({writeFile}) => writeFile(resolve(output,'provenance.json'),JSON.stringify({library:'lucide-static',version,source:'https://github.com/lucide-icons/lucide',mappings},null,2)+'\n'));
16
+ for(const filename of await readdir(output)) if(filename.endsWith('.png')) await unlink(resolve(output,filename));
17
+ console.log('Copied '+names.length+' Lucide icons at '+version);
@@ -0,0 +1,15 @@
1
+ import {execFile} from 'node:child_process';
2
+ import {promisify} from 'node:util';
3
+ import {writeFile} from 'node:fs/promises';
4
+ import {basename, resolve} from 'node:path';
5
+ const run = promisify(execFile);
6
+ const file = process.argv[2];
7
+ if (!file || !/\.(?:mp4|webm|ogv|ogg)$/i.test(file)) throw new Error('Usage: node tools/assets/video-preview.mjs path/to/video.mp4 [frame-seconds] (requires FFmpeg)');
8
+ const video = resolve(file), poster = video + '.poster.jpg';
9
+ const {stdout} = await run('ffprobe', ['-v','error','-select_streams','v:0','-show_entries','format=duration:stream=width,height','-of','json',video]);
10
+ const info = JSON.parse(stdout), duration = Number(info.format.duration), {width, height} = info.streams[0];
11
+ const time = Number(process.argv[3] ?? Math.min(1, duration / 2));
12
+ if (!(Number.isFinite(time) && time >= 0 && time < duration)) throw new Error('Preview time must be inside the video');
13
+ await run('ffmpeg', ['-v','error','-y','-ss',String(time),'-i',video,'-frames:v','1','-vf','scale=min(960\\,iw):-2','-q:v','3',poster]);
14
+ await writeFile(video + '.json', JSON.stringify({duration, width, height, poster: basename(poster)}, null, 2) + '\n');
15
+ console.log('Created ' + basename(poster) + ' and ' + basename(video) + '.json');
@@ -0,0 +1,147 @@
1
+ import assert from 'node:assert/strict';
2
+ import { mkdir, readFile, writeFile } from 'node:fs/promises';
3
+ import { resolve, extname } from 'node:path';
4
+ import { createRequire } from 'node:module';
5
+ import { pathToFileURL } from 'node:url';
6
+ import { chromium } from 'playwright';
7
+ import { recordingContext, record, closeRecording, preparePage } from './desktop-recording.mjs';
8
+ import { pointAt, pointAtPosition, drag } from './vm-desktop.mjs';
9
+
10
+ const workspace = resolve(import.meta.dirname, '../../..');
11
+ const site = resolve(workspace, 'web/js.gripe');
12
+ const documents = resolve(process.env.DOCUMENT_DEMO_ROOT || resolve(site, 'content/assets/documents/doc-views'));
13
+ const require = createRequire(resolve(site, 'package.json'));
14
+ const engine = createRequire(require.resolve('edgepress/package.json'));
15
+ const { loadConfig } = await import(pathToFileURL(engine.resolve('edgepress/src/config.js')));
16
+ const { loadLanguagePacks } = await import(pathToFileURL(engine.resolve('edgepress/src/i18n.js')));
17
+ const { collectAssets, rewriteAssetLinks } = await import(pathToFileURL(engine.resolve('edgepress/src/assets.js')));
18
+ const { createExtensions } = await import(pathToFileURL(engine.resolve('edgepress/src/plugin-api.js')));
19
+ const { renderBlocks } = await import(pathToFileURL(engine.resolve('edgepress/src/page-blocks.js')));
20
+ const { renderLayout } = await import(pathToFileURL(engine.resolve('edgepress/src/theme.js')));
21
+ const names = ['sample-document-medium.docx', 'sample-document-medium.doc', 'sample-document-5-pages.pdf', 'sample-document.ppt', 'sample-presentation-10-slides.pptx', 'sample-spreadsheet-100-rows.xlsx', 'sample-spreadsheet-100-rows.xls'];
22
+ const files = new Map(await Promise.all(names.map(async name => [name, await readFile(resolve(documents, name))])));
23
+ const config = await loadConfig(site);
24
+ await loadLanguagePacks(config);
25
+ const bundle = await collectAssets(config, { documentViewer: true });
26
+ const extensions = await createExtensions(config);
27
+ const assets = new Map(bundle.assets.map(asset => ['/' + asset.path, asset]));
28
+ const artifact = resolve(workspace, 'edgepress/tools/.recordings');
29
+ const output = resolve(site, 'content/assets/images/previews');
30
+ await mkdir(output, { recursive: true });
31
+ const browser = await chromium.connectOverCDP('http://127.0.0.1:9222');
32
+ const pause = milliseconds => new Promise(done => setTimeout(done, milliseconds));
33
+ const frameTargetBox = (element, selector) => element.evaluate((frame, selector) => {
34
+ const outer = frame.getBoundingClientRect();
35
+ const inner = frame.contentDocument.querySelector(selector).getBoundingClientRect();
36
+ return { x: outer.x + frame.clientLeft + inner.x, y: outer.y + frame.clientTop + inner.y, width: inner.width, height: inner.height };
37
+ }, selector);
38
+ const entries = [];
39
+ try {
40
+ for (const locale of (process.env.RECORDING_LOCALES || 'en,zh-SG,zh-TW').split(',')) {
41
+ const rows = [...files.keys()].map(name => ({ columns: 1, cells: [[{ type: 'document', title: name, src: '/documents/doc-views/' + name }]] }));
42
+ const body = '<h1>doc-views</h1>' + await renderBlocks(rows, { config, locale });
43
+ const html = rewriteAssetLinks(await renderLayout(config, extensions, { title: 'doc-views', locale, urlPath: '/document-demo/' }, body), bundle.urlMap);
44
+ const context = await recordingContext(browser, { locale }, artifact);
45
+ const page = await context.newPage();
46
+ const errors = [];
47
+ page.on('pageerror', error => errors.push(error.message));
48
+ page.on('download', () => errors.push('Unexpected download'));
49
+ await context.route('**/*', async route => {
50
+ const url = new URL(route.request().url());
51
+ assert.equal(url.origin, new URL(config.site.url).origin);
52
+ if (url.pathname === '/document-demo/') return route.fulfill({ contentType: 'text/html', body: html });
53
+ if (url.pathname.startsWith('/documents/doc-views/')) return route.fulfill({ contentType: 'application/octet-stream', body: files.get(url.pathname.split('/').pop()) });
54
+ const asset = assets.get(url.pathname);
55
+ if (!asset) return route.fulfill({ status: 404, body: '' });
56
+ const types = { '.js': 'text/javascript', '.mjs': 'text/javascript', '.css': 'text/css', '.woff2': 'font/woff2', '.svg': 'image/svg+xml' };
57
+ return route.fulfill({ contentType: types[extname(url.pathname)] || 'application/octet-stream', body: asset.content || await readFile(asset.source) });
58
+ });
59
+ await preparePage(page);
60
+ await page.goto(config.site.url + '/document-demo/');
61
+ await page.locator('.privacy-reject').click();
62
+ const first = page.locator('.document-block').first();
63
+ await first.frameLocator('iframe').getByText('Sample Business Document', { exact: true }).waitFor();
64
+ const bullet = await first.frameLocator('iframe').getByText('Realistic structure: headings, lists, tables, and images', { exact: true }).first().evaluate(element => getComputedStyle(element.closest('p'), '::before').content);
65
+ assert.ok(bullet.includes('\u2022'), 'The recorded Word list must render its Unicode bullet without a proprietary font');
66
+ await first.scrollIntoViewIfNeeded();
67
+ await record(page, 'doc-views-' + locale.toLowerCase(), async () => {
68
+ await pause(2500);
69
+ const expand = first.locator('.document-viewer-expand');
70
+ await pointAt(page, expand); await expand.click(); await pause(1800);
71
+ await pointAt(page, expand); await expand.click(); await pause(600);
72
+ const zoom = first.getByRole('button', { name: locale === 'en' ? 'Zoom in' : '放大', exact: true });
73
+ await pointAt(page, zoom); await zoom.click(); await pause(1000);
74
+ await first.getByRole('button', { name: locale === 'en' ? 'Fit width' : locale === 'zh-SG' ? '适合宽度' : '符合寬度', exact: true }).click(); await pause(600);
75
+ await pointAt(page, first.locator('.document-viewer button').nth(1));
76
+ await first.locator('.document-viewer button').nth(1).click();
77
+ for (const index of [1, 2, 3, 4, 5, 6]) {
78
+ const block = page.locator('.document-block').nth(index);
79
+ await block.locator('iframe').waitFor();
80
+ const frame = block.frameLocator('iframe');
81
+ if (index === 1) await frame.locator('body').filter({ hasText: 'Sample Business Document' }).waitFor();
82
+ if (index === 2) {
83
+ await frame.getByText('Sample Business Document', { exact: true }).waitFor();
84
+ const painted = await frame.locator('.pdf-page img').first().evaluate(image => {
85
+ const canvas = document.createElement('canvas');
86
+ canvas.width = image.naturalWidth;
87
+ canvas.height = image.naturalHeight;
88
+ canvas.getContext('2d').drawImage(image, 0, 0);
89
+ const pixels = canvas.getContext('2d').getImageData(0, 0, canvas.width, canvas.height).data;
90
+ let ink = 0;
91
+ for (let offset = 0; offset < pixels.length; offset += 4) if (pixels[offset] < 128 && pixels[offset + 3]) ink++;
92
+ return { ink, width: canvas.width, height: canvas.height };
93
+ });
94
+ console.log('PDF canvas: ' + JSON.stringify(painted));
95
+ assert.ok(painted.ink > 100, 'The recorded PDF must paint visible content');
96
+ }
97
+ if ([3, 4].includes(index)) await frame.getByText('Sample Presentation', { exact: true }).waitFor();
98
+ if ([5, 6].includes(index)) await frame.getByText('Order ID', { exact: true }).waitFor();
99
+ await block.scrollIntoViewIfNeeded(); await pause(2200);
100
+ if (index === 2) {
101
+ for (let number = 2; number <= 8; number++) {
102
+ const section = frame.locator('[data-document-page="' + number + '"]');
103
+ await section.scrollIntoViewIfNeeded();
104
+ await section.locator('.pdf-page[aria-busy="false"] img').waitFor();
105
+ await pause(1600);
106
+ }
107
+ }
108
+ if (index === 3) {
109
+ const title = frame.locator('foreignObject').getByText('Sample Presentation', { exact: true });
110
+ assert.ok(await title.evaluate(element => element.getBoundingClientRect().height > 16), 'PPT title must be visibly readable');
111
+ await frame.locator('[data-document-page="2"]').scrollIntoViewIfNeeded();
112
+ await frame.getByText('Agenda', { exact: true }).waitFor();
113
+ assert.equal((await frame.locator('body').textContent()).includes('\uf0b7'), false, 'The recorded PPT bullets must not depend on a proprietary Symbol font');
114
+ await pause(2200);
115
+ }
116
+ if (index === 5) {
117
+ const address = block.locator('.document-viewer-formula input');
118
+ await pointAt(page, address); await address.fill('A90'); await address.press('Enter');
119
+ const cell = frame.locator('[data-row="89"][data-column="0"]');
120
+ await cell.waitFor();
121
+ const cellBox = await frameTargetBox(block.locator('iframe'), '[data-row="89"][data-column="0"]');
122
+ const cellPoint = { x: cellBox.x + cellBox.width / 2, y: cellBox.y + cellBox.height / 2 };
123
+ await pointAtPosition(page, cellPoint); await page.mouse.click(cellPoint.x, cellPoint.y); await pause(1800);
124
+ const summary = block.getByRole('tab', { name: 'Summary', exact: true });
125
+ await pointAt(page, summary); await summary.click(); await frame.getByText('595500', { exact: true }).waitFor(); await pause(2200);
126
+ const grip = frame.locator('[data-resize-column="0"]').first();
127
+ const width = Number(await grip.getAttribute('aria-valuenow'));
128
+ const position = await frameTargetBox(block.locator('iframe'), '[data-resize-column="0"]');
129
+ const from = { x: position.x + position.width / 2, y: position.y + position.height / 2 };
130
+ await pointAtPosition(page, from);
131
+ await drag(page, { from, to: { x: from.x + 100, y: from.y } });
132
+ assert.equal(Number(await grip.getAttribute('aria-valuenow')), width + 100);
133
+ await pause(1600);
134
+ const stretch = block.getByRole('button', { name: locale === 'en' ? 'Fill viewport' : locale === 'zh-SG' ? '铺满视口' : '填滿視口', exact: true });
135
+ await pointAt(page, stretch); await stretch.click(); await pause(1600);
136
+ const reset = block.getByRole('button', { name: locale === 'en' ? 'Restore column widths' : locale === 'zh-SG' ? '恢复原列宽' : '還原原欄寬', exact: true });
137
+ await pointAt(page, reset); await reset.click(); await pause(1000);
138
+ }
139
+ const close = block.locator('.document-viewer button').nth(1);
140
+ await pointAt(page, close); await close.click(); await pause(400);
141
+ }
142
+ });
143
+ assert.deepEqual(errors, []);
144
+ entries.push(...(await closeRecording(context, { artifact, output })).map(entry => ({ ...entry, kind: 'FFmpeg recording of actual Chromium in an isolated WSL2 desktop; automatically opened operator-supplied Office and eight-page PDF samples; no original-download links', source: 'edgepress/tools/recordings/capture-document-demo.mjs' })));
145
+ }
146
+ } finally { await browser.close(); }
147
+ await writeFile(resolve(artifact, 'document-demo.json'), JSON.stringify(entries, null, 2) + '\n');
@@ -0,0 +1,57 @@
1
+ // Recording-only workbench. Commands execute the real standalone EdgePress CLI;
2
+ // Geany edits the actual generated file. Nothing is installed into production.
3
+ import assert from 'node:assert/strict';
4
+ import {readFile,writeFile,mkdir,rm,cp} from 'node:fs/promises';
5
+ import {resolve,sep,extname} from 'node:path';
6
+ import {spawn} from 'node:child_process';
7
+ import {pathToFileURL} from 'node:url';
8
+ import {guest,guestPath,typeCommand,editFile,openApp,activate} from './vm-desktop.mjs';
9
+ import {parse,stringify} from 'yaml';
10
+
11
+ export async function workbench({artifact,packageRoot,photoPath,locale,context,embedding=false}){
12
+ const upstream=resolve(packageRoot),root=resolve(artifact,(embedding?'embedding-example-':'edgepress-example-')+locale);
13
+ assert.ok(root.startsWith(resolve(artifact)+sep));await rm(root,{recursive:true,force:true});await mkdir(root,{recursive:true});
14
+ const cli=resolve(upstream,'src/cli.js');
15
+ async function run(args){return new Promise((done,reject)=>{const child=spawn(process.execPath,[cli,...args],{cwd:root,windowsHide:true,env:{...process.env,WRANGLER_SEND_METRICS:'false'}});let output='';child.stdout.on('data',value=>output+=value);child.stderr.on('data',value=>output+=value);child.once('error',reject);child.once('exit',code=>code===0?done(output):reject(Error(output)));});}
16
+ await run(['init']);
17
+ const chinese=locale!=='en',title=embedding?(chinese?'散步中的瞬间':'Photos from our walk'):(chinese?'海边的下午':'An afternoon by the water');
18
+ const settings=parse(await readFile(resolve(root,'config.yml'),'utf8'));settings.site.title=chinese?'我的写作空间':'My writing space';settings.site.description=chinese?'用文字和照片记录生活。':'Stories and photographs from everyday life.';settings.site.navigation=settings.site.navigation.filter(item=>['home','posts','search'].includes(item.key));settings.site.footerNavigation=[];settings.plugins.consent.services=[];await writeFile(resolve(root,'config.yml'),stringify(settings));
19
+ await writeFile(resolve(root,'edgepress.config.mjs'),'export default '+JSON.stringify({paths:{content:'content',static:'static',theme:'themes/default',output:'dist',cache:'.recording-cache'},permalink:'/:year/:month/:slug/',i18n:{defaultLocale:locale,languagePacks:[chinese?'en':'zh-CN']},plugins:['plugins/consent/index.js']})+';\n');
20
+ if(chinese){await cp(resolve(root,'languages/packs/zh-CN.json'),resolve(root,'languages/base/zh-CN.json'));await cp(resolve(root,'languages/base/en.json'),resolve(root,'languages/packs/en.json'));}
21
+ // Use a small, real writing site so the recording starts at its article list.
22
+ await rm(resolve(root,'content/posts'),{recursive:true,force:true});await mkdir(resolve(root,'content/posts'),{recursive:true});
23
+ await rm(resolve(root,'content/pages'),{recursive:true,force:true});await mkdir(resolve(root,'content/pages/home'),{recursive:true});
24
+ await writeFile(resolve(root,'content/pages/home',locale.toLowerCase()+'.json'),JSON.stringify({title:settings.site.title,lang:locale,slug:'home',homepage:true,blocks:[{columns:1,cells:[[{type:'hero',eyebrow:chinese?'生活里的小事':'Notes from everyday life',title:settings.site.title,text:settings.site.description}]]},{columns:1,cells:[[{type:'post-list',title:chinese?'最近的文章':'Recent stories',category:'uncategorized',count:3}]]}]},null,2)+'\n');
25
+ if(embedding){settings.plugins.consent.services=[{id:'ishare',provider:'oembed',backendUrl:'https://ishare.js.gripe',enabled:true,name:chinese?'爱分享':'ishare',purpose:chinese?'在你同意后显示作者分享的图片与视频。':'Show the author’s shared photos and videos after you agree.',dataCategories:chinese?'请求的媒体和 IP 地址。':'Requested media and IP address.',recipient:'ishare',retention:chinese?'直到发布者删除。':'Until the publisher deletes it.',privacyUrl:'https://ishare.js.gripe/privacy/'}];await writeFile(resolve(root,'config.yml'),stringify(settings));}
26
+ await mkdir(resolve(root,'content/assets/photos'),{recursive:true});await cp(photoPath,resolve(root,'content/assets/photos/seaside.jpg'));
27
+ let file=null,port;
28
+ const terminalTitle='EdgePress '+locale;
29
+ const {loadConfig}=await import(pathToFileURL(resolve(upstream,'src/config.js')));const {readDocuments}=await import(pathToFileURL(resolve(upstream,'src/content.js')));
30
+ const rc=resolve(root,'.recording-bashrc');
31
+ const shellRoot="'"+guestPath(root).replaceAll("'","'\\''")+"'";
32
+ await writeFile(rc,'export PATH=/opt/recording/node_modules/.bin:/usr/local/bin:/usr/bin:/bin\ncd -- '+shellRoot+'\nexport WRANGLER_SEND_METRICS=false\nexport PS1="❯ "\n');
33
+ await guest(['ln','-s','/opt/recording/node_modules',guestPath(resolve(root,'node_modules'))]);
34
+ await openApp(['xterm','-T',terminalTitle,'-fa','Monospace','-fs','16','-bg','#17212c','-fg','#e8edf2','-geometry','106x30+60+40','-e','bash','--rcfile',guestPath(rc)],terminalTitle);
35
+ await new Promise(done=>setTimeout(done,800));
36
+ async function findPost(){const documents=await readDocuments(await loadConfig(root));const article=documents.find(item=>item.kind==='posts'&&item.title===title);if(article)file=article.file;return article;}
37
+ async function createPost(){await run(['new','post',title]);const article=await findPost();assert.ok(article);return file;}
38
+ async function newPost(){await typeCommand(root,terminalTitle,"edgepress new post '"+title+"'");for(let attempt=0;attempt<30;attempt++){const article=await findPost();if(article)return;await new Promise(done=>setTimeout(done,300));}throw Error('CLI did not create the post');}
39
+ async function editPost(body){assert.ok(file);const front=(await readFile(file,'utf8')).split('---')[1];await editFile(root,file,'---'+front+'---\n\n'+body+'\n');}
40
+ async function startServer({omitWait=task=>task()}={}){
41
+ const {createServer}=await import('node:net');const socket=createServer();await new Promise(done=>socket.listen(0,'127.0.0.1',done));port=socket.address().port;await new Promise(done=>socket.close(done));
42
+ await typeCommand(root,terminalTitle,'edgepress server --port '+port);
43
+ await new Promise(done=>setTimeout(done,1600));
44
+ await omitWait(async()=>{for(let attempt=0;attempt<180;attempt++){try{const response=await fetch('http://127.0.0.1:'+port+'/',{signal:AbortSignal.timeout(1200)});if(response.ok)return;}catch{}await new Promise(done=>setTimeout(done,500));}throw Error('Guest preview did not become ready');});
45
+ }
46
+ async function dispose(){for(const title of [terminalTitle,'Geany']){try{const ids=await guest(['xdotool','search','--onlyvisible','--name',title]);for(const id of ids.split(/\s+/)){const pid=await guest(['xdotool','getwindowpid',id]);if(/^\d+$/.test(pid))await guest(['kill','-TERM',pid]);}}catch{}}}
47
+ const content=chinese?'## 海边的下午\n\n走到海边时,雨刚停。潮水很轻,远处的山还藏在云里。\n\n![雨后的海岸](/photos/seaside.jpg)\n\n下次还想沿着这条路走远一点。':'## An afternoon by the water\n\nThe rain had just stopped. Small waves reached the shore, and the mountains were still tucked into the clouds.\n\n![The coast after the rain](/photos/seaside.jpg)\n\nNext time, I want to follow this path a little farther.';
48
+ await context.route('https://edgepress-demo.test/**',async route=>{
49
+ try{
50
+ assert.ok(port,'Start the real guest preview before opening the website');
51
+ const request=route.request(),url=new URL(request.url()),response=await fetch('http://127.0.0.1:'+port+url.pathname+url.search);
52
+ const headers=Object.fromEntries(response.headers);delete headers['content-encoding'];delete headers['content-length'];
53
+ return await route.fulfill({status:response.status,headers,body:Buffer.from(await response.arrayBuffer())});
54
+ }catch(error){if(!route.request().isNavigationRequest())return route.abort();throw error;}
55
+ });
56
+ return {title,content,createPost,newPost,editPost,startServer,dispose,activateTerminal:()=>activate(terminalTitle),async publishedPath(){const article=await findPost();assert.ok(article);return article.path;}};
57
+ }
@@ -0,0 +1,92 @@
1
+ // FFmpeg records the isolated VM display. Playwright drives only its browser.
2
+ import {mkdir, readFile, writeFile, cp} from 'node:fs/promises';
3
+ import {resolve} from 'node:path';
4
+ import {spawn, spawnSync} from 'node:child_process';
5
+ import {vm, guestPath} from './vm-desktop.mjs';
6
+ import {createHash} from 'node:crypto';
7
+
8
+ const sessions = new WeakMap();
9
+ function ffmpeg(args) {
10
+ const result = spawnSync('ffmpeg', ['-hide_banner', '-loglevel', 'error', '-y', ...args], {encoding:'utf8', windowsHide:true});
11
+ if (result.status !== 0) throw new Error(result.stderr || result.error?.message || 'FFmpeg failed');
12
+ }
13
+
14
+ export async function recordingContext(browser, options, artifact) {
15
+ // Let the real guest window determine layout instead of emulating a viewport
16
+ // that can clip controls while FFmpeg records the physical desktop.
17
+ const context = await browser.newContext({...options, viewport:null});
18
+ sessions.set(context, {artifact, clips:[]});
19
+ return context;
20
+ }
21
+
22
+ export async function preparePage(page) {
23
+ await page.bringToFront();
24
+ const cdp = await page.context().newCDPSession(page);
25
+ try {
26
+ const {windowId} = await cdp.send('Browser.getWindowForTarget');
27
+ await cdp.send('Browser.setWindowBounds', {windowId, bounds:{left:40, top:20, width:1360, height:855}});
28
+ } finally {
29
+ await cdp.detach();
30
+ }
31
+ }
32
+
33
+ export async function record(page, name, story, {foreground} = {}) {
34
+ const session = sessions.get(page.context());
35
+ if (!session) throw new Error('Use a recording context');
36
+ console.log('Recording ' + name);
37
+ await preparePage(page);
38
+ await foreground?.();
39
+ const directory = resolve(session.artifact, name);
40
+ await mkdir(directory, {recursive:true});
41
+ const raw = resolve(directory, 'raw.mkv');
42
+ const args = ['env','DISPLAY=:99','ffmpeg','-hide_banner','-loglevel','error','-y','-f','x11grab','-framerate','25','-video_size','1440x900','-i',':99.0','-c:v','libx264','-preset','ultrafast','-crf','18','-pix_fmt','yuv420p','-progress','pipe:1',guestPath(raw)];
43
+ const child = spawn('wsl.exe', ['-d',vm,'--exec',...args], {windowsHide:true, stdio:['pipe','pipe','pipe']});
44
+ let errors='', progress=''; child.stderr.on('data', chunk => errors += chunk);
45
+ child.stdin.on('error', error => { errors += error.message; });
46
+ const finished = new Promise((done,reject)=>{child.once('error',reject);child.once('exit',code=>code===0?done():reject(new Error(errors||'VM capture exited '+code)));});
47
+ finished.catch(()=>{}); // Readiness reports early failures before awaiting shutdown.
48
+ // Read FFmpeg's progress rather than creating a screenshot/capture loop.
49
+ try{await new Promise((done,reject)=>{const timeout=setTimeout(()=>reject(new Error('VM capture did not start: '+errors)),15000);child.stdout.on('data',chunk=>{progress+=chunk;if(/frame=\s*[1-9]/.test(progress)){clearTimeout(timeout);done();}});child.once('exit',()=>{clearTimeout(timeout);reject(new Error(errors));});});}catch(error){if(child.exitCode===null)child.stdin.write('q');await finished.catch(()=>{});throw error;}
50
+ const started=Date.now(), seconds=()=> (Date.now()-started)/1000, segments=[];
51
+ let start=0;
52
+ const omitWait=async task=>{segments.push([start,seconds()]);try{return await task();}finally{start=seconds();}};
53
+ let succeeded=false;
54
+ try{await story({omitWait});segments.push([start,seconds()]);succeeded=true;}
55
+ finally{child.stdin.write('q');await finished;}
56
+ if(succeeded)session.clips.push({raw,name,segments});
57
+ }
58
+
59
+ // Export the VM desktop master after the browser fixture has been disposed.
60
+ export async function closeRecording(context, {artifact, output}) {
61
+ await context.close();
62
+ const session = sessions.get(context);
63
+ const entries = [];
64
+ for (const {raw, name, segments} of session.clips) {
65
+ const directory = resolve(artifact, name);
66
+ await mkdir(directory, {recursive:true});
67
+ const movie = resolve(directory, 'demo.mp4');
68
+ const filters = segments.map(([start, end], index) => '[0:v]trim=start=' + start.toFixed(3) + ':end=' + end.toFixed(3) + ',setpts=PTS-STARTPTS[s' + index + ']').join(';') + ';' +
69
+ segments.map((_, index) => '[s' + index + ']').join('') + 'concat=n=' + segments.length + ':v=1:a=0,fps=25,scale=' + '1200' + ':-2:flags=lanczos[out]';
70
+ ffmpeg(['-i', raw, '-filter_complex', filters, '-map', '[out]', '-c:v', 'libx264', '-crf', '24', '-preset', 'medium', '-pix_fmt', 'yuv420p', '-movflags', '+faststart', '-an', movie]);
71
+ const gif = resolve(directory, 'demo.gif');
72
+ ffmpeg(['-i', movie, '-filter_complex', 'fps=8,scale=640:-2:flags=lanczos,split[a][b];[a]palettegen=max_colors=128:stats_mode=diff[p];[b][p]paletteuse=dither=bayer:bayer_scale=3:diff_mode=rectangle', '-loop', '-1', gif]);
73
+ const bytes = await readFile(gif), movieBytes = await readFile(movie);
74
+ const hash = data => createHash('sha256').update(data).digest('hex').slice(0, 16);
75
+ const file = name + '.' + hash(bytes) + '.gif', movieFile = name + '.' + hash(movieBytes) + '.mp4';
76
+ const probe=spawnSync('ffprobe',['-v','error','-show_entries','format=duration','-of','default=noprint_wrappers=1:nokey=1',movie],{encoding:'utf8',windowsHide:true});
77
+ if(probe.status!==0||!Number.isFinite(Number(probe.stdout)))throw new Error('Cannot verify exported duration');
78
+ const duration=Math.round(Number(probe.stdout)*1000);
79
+ const posterPath = resolve(directory, 'poster.webp');
80
+ ffmpeg(['-ss', String(name.startsWith('edgepress-') ? Math.max(0, duration / 1000 - 3) : 0), '-i', movie, '-frames:v', '1', '-quality', '90', posterPath]);
81
+ const poster = name + '.' + hash(await readFile(posterPath)) + '.webp';
82
+ for (const [source, target] of [[gif, file], [movie, movieFile], [posterPath, poster]]) await cp(source, resolve(output, target));
83
+ const entry = {name, file, poster, movie:movieFile, movieBytes:movieBytes.length, duration, bytes:bytes.length,
84
+ date:new Date().toISOString().slice(0, 10), kind:'FFmpeg recording of an isolated WSL2 desktop; '+(name.startsWith('edgepress-')||name.startsWith('ishare-sharing-')?'real terminal, editor and Chromium; preview startup wait shortened':'actual Chromium interface')+'; isolated service fixtures', source:'tools/recordings/capture-project-demos.mjs'};
85
+ entries.push(entry);
86
+ const manifest = resolve(artifact, 'manifest.json'), previous = await readFile(manifest, 'utf8').then(JSON.parse).catch(() => []);
87
+ await writeFile(manifest, JSON.stringify([...previous.filter(item => item.name !== name), entry], null, 2) + '\n');
88
+ console.log(JSON.stringify(entry));
89
+ }
90
+ sessions.delete(context);
91
+ return entries;
92
+ }
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ # Run inside a disposable Debian/Ubuntu VM, never on the host desktop.
4
+ export DEBIAN_FRONTEND=noninteractive
5
+ apt-get update -qq
6
+ apt-get install -y --no-install-recommends ca-certificates curl xz-utils xvfb openbox xterm geany chromium ffmpeg xdotool fonts-noto-cjk dbus-x11 xclip tint2 x11-xserver-utils
7
+ mkdir -p /etc/chromium/policies/managed
8
+ printf '{"TranslateEnabled":false}\n' > /etc/chromium/policies/managed/recording.json
9
+ mkdir -p /opt/recording
10
+ curl -fsSL https://nodejs.org/dist/v22.22.0/node-v22.22.0-linux-x64.tar.xz -o /opt/recording/node.tar.xz
11
+ tar -xJf /opt/recording/node.tar.xz -C /opt/recording
12
+ ln -sf /opt/recording/node-v22.22.0-linux-x64/bin/node /usr/local/bin/node
13
+ ln -sf /opt/recording/node-v22.22.0-linux-x64/bin/npm /usr/local/bin/npm
14
+ ln -sf /opt/recording/node-v22.22.0-linux-x64/bin/npx /usr/local/bin/npx
15
+ mkdir -p /root/.config/geany
16
+ if [ ! -f /root/.config/geany/geany.conf ]; then
17
+ printf "[geany]\neditor_font=Monospace 16\nline_wrapping=true\n" > /root/.config/geany/geany.conf
18
+ fi
@@ -0,0 +1,28 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ # DISPLAY :99 is private to the guest. Do not use WSLg/host desktop displays.
4
+ unset WAYLAND_DISPLAY DBUS_SESSION_BUS_ADDRESS XDG_RUNTIME_DIR
5
+ export DISPLAY=:99 LANG=C.UTF-8 GDK_BACKEND=x11 QT_QPA_PLATFORM=xcb
6
+ mkdir -p /opt/recording/logs
7
+ if ! pgrep -f 'Xvfb :99' >/dev/null; then
8
+ # A stopped disposable VM can leave X11 locks although its server is gone.
9
+ rm -f /tmp/.X99-lock /tmp/.X11-unix/X99
10
+ Xvfb :99 -screen 0 1440x900x24 -nolisten tcp >/opt/recording/logs/display.log 2>&1 &
11
+ fi
12
+ for attempt in {1..50}; do
13
+ if xdpyinfo -display :99 >/dev/null 2>&1; then break; fi
14
+ sleep 0.2
15
+ done
16
+ xdpyinfo -display :99 >/dev/null
17
+ if ! pgrep -x openbox >/dev/null; then nohup openbox >/opt/recording/logs/desktop.log 2>&1 </dev/null & fi
18
+ sleep 0.5
19
+ xsetroot -solid '#243343'
20
+ if ! curl -fsS http://127.0.0.1:9222/json/version >/dev/null 2>&1; then
21
+ nohup chromium --ozone-platform=x11 --no-sandbox --disable-dev-shm-usage --disable-gpu --disable-gpu-rasterization --disable-gpu-compositing --disable-extensions --no-first-run --no-default-browser-check --disable-features=Translate,TranslateUI --lang=en-US --password-store=basic --remote-debugging-port=9222 --remote-debugging-address=127.0.0.1 --user-data-dir=/opt/recording/browser --window-position=40,20 --window-size=1360,855 about:blank >/opt/recording/logs/browser.log 2>&1 </dev/null &
22
+ fi
23
+ for attempt in {1..50}; do
24
+ if curl -fsS http://127.0.0.1:9222/json/version >/dev/null 2>&1; then break; fi
25
+ sleep 0.2
26
+ done
27
+ curl -fsS http://127.0.0.1:9222/json/version >/dev/null
28
+ if [ "${1:-}" = '--keep-alive' ]; then exec sleep infinity; fi
@@ -0,0 +1,124 @@
1
+ import {spawn} from 'node:child_process';
2
+ import {writeFile,readFile} from 'node:fs/promises';
3
+ import {resolve,dirname} from 'node:path';
4
+ import {fileURLToPath} from 'node:url';
5
+
6
+ export const vm = process.env.RECORDING_VM || 'CodexRecordingVM';
7
+ const workspace=resolve(dirname(fileURLToPath(import.meta.url)),'../../..').replaceAll('\\','/');
8
+ export const guestPath = path => resolve(path).replaceAll('\\', '/').replace(workspace,process.env.RECORDING_GUEST_ROOT||'/opt/recording/workspace').replace(/^([a-z]):/i, (_, drive) => '/mnt/' + drive.toLowerCase());
9
+ function guestProcess(args) {
10
+ const quote = value => "'" + String(value).replaceAll("'", "'\\''") + "'";
11
+ const child = spawn('wsl.exe', ['-d', vm, '--exec', 'bash', '-s'], {windowsHide:true});
12
+ child.stdin.end('exec env -u WAYLAND_DISPLAY -u DBUS_SESSION_BUS_ADDRESS -u XDG_RUNTIME_DIR LANG=C.UTF-8 DISPLAY=:99 GDK_BACKEND=x11 QT_QPA_PLATFORM=xcb NO_AT_BRIDGE=1 ' + args.map(quote).join(' ') + '\n');
13
+ return child;
14
+ }
15
+ export async function openApp(args, title) {
16
+ const child = guestProcess(args);
17
+ let errors=''; child.stderr.on('data', chunk => errors += chunk); child.stdout.resume();
18
+ child.on('error', error => { errors += error.message; });
19
+ for (let attempt=0;attempt<40;attempt++) {
20
+ try { await activate(title); return child; } catch {}
21
+ if(child.exitCode!==null)throw new Error(args[0] + ' failed: ' + errors);
22
+ await new Promise(done=>setTimeout(done,200));
23
+ }
24
+ child.kill();throw new Error(args[0] + ' did not open: ' + errors);
25
+ }
26
+ export async function guest(args) {
27
+ return new Promise((done, reject) => {
28
+ // UTF-8 stdin avoids Windows/WSL command-line reparsing of paths and quotes.
29
+ const child = guestProcess(['timeout', '15s', ...args]);
30
+ let output = ''; child.stdout.on('data', chunk => output += chunk); child.stderr.on('data', chunk => output += chunk);
31
+ child.once('error', reject); child.once('exit', code => code === 0 ? done(output.trim()) : reject(new Error(args[0] + ' exited ' + code + ': ' + output)));
32
+ });
33
+ }
34
+ export async function activate(title) {
35
+ const id = (await guest(['xdotool', 'search', '--onlyvisible', '--name', title])).split(/\s+/).at(-1);
36
+ await guest(['xdotool', 'windowactivate', '--sync', id]);
37
+ }
38
+
39
+ // Move only the guest's real X11 pointer so FFmpeg records the same cursor the
40
+ // browser receives. CDP coordinates alone leave the desktop cursor behind.
41
+ export async function pointAt(page, locator) {
42
+ await locator.scrollIntoViewIfNeeded();
43
+ await new Promise(done => setTimeout(done, 250));
44
+ const box = await locator.boundingBox();
45
+ if (!box) throw new Error('Demo target is not visible');
46
+ const position = {x:box.x + box.width / 2, y:box.y + box.height / 2};
47
+ await pointAtPosition(page, position);
48
+ return position;
49
+ }
50
+
51
+ async function windowOrigin(page) {
52
+ return page.evaluate(() => ({
53
+ x:screenX + (outerWidth - innerWidth) / 2,
54
+ y:screenY + outerHeight - innerHeight
55
+ }));
56
+ }
57
+
58
+ export async function pointAtPosition(page, {x, y}) {
59
+ const window = await windowOrigin(page);
60
+ // --sync waits for a movement event forever when the pointer is already here.
61
+ await guest(['xdotool', 'mousemove', String(Math.round(window.x + x)), String(Math.round(window.y + y))]);
62
+ await page.mouse.move(x, y);
63
+ }
64
+
65
+ export async function drag(page, {from, to, steps=16, duration=900}) {
66
+ const window = await windowOrigin(page);
67
+ const coordinates = point => [String(Math.round(window.x + point.x)), String(Math.round(window.y + point.y))];
68
+ // Keep press, movement and release in one native event stream. Mixing CDP
69
+ // button state with X11 movement can cancel the actual browser drag.
70
+ const args = ['xdotool', 'mousemove', ...coordinates(from), 'mousedown', '1'];
71
+ for (let step=1; step<=steps; step++) {
72
+ args.push('sleep', String(duration / steps / 1000), 'mousemove', ...coordinates({
73
+ x:from.x + (to.x - from.x) * step / steps,
74
+ y:from.y + (to.y - from.y) * step / steps
75
+ }));
76
+ }
77
+ args.push('mouseup', '1');
78
+ await guest(args);
79
+ await page.mouse.move(to.x, to.y);
80
+ }
81
+
82
+ export async function chooseFiles(page, input) {
83
+ // The private guest Desktop contains only the two demo attachments. Native
84
+ // GTK coordinates are scoped to this controlled VM, never the host desktop.
85
+ await pointAt(page, input);
86
+ await guest(['xdotool', 'click', '1']);
87
+ await new Promise(done => setTimeout(done, 1000));
88
+ await activate('Open File');
89
+ const id = (await guest(['xdotool', 'search', '--onlyvisible', '--name', 'Open File'])).split(/\s+/).at(-1);
90
+ const geometry = Object.fromEntries((await guest(['xdotool', 'getwindowgeometry', '--shell', id])).split('\n').map(line => line.split('=')).map(([key, value]) => [key, Number(value)]));
91
+ const move = async (x, y) => guest(['xdotool', 'mousemove', String(Math.round(x)), String(Math.round(y)), 'click', '1']);
92
+ await move(geometry.X + 65, geometry.Y + 75); // Desktop bookmark.
93
+ await new Promise(done => setTimeout(done, 1200));
94
+ await move(geometry.X + 340, geometry.Y + 63); // First file row.
95
+ await guest(['xdotool', 'key', 'ctrl+a']);
96
+ await new Promise(done => setTimeout(done, 900));
97
+ // Click Open explicitly; Enter can activate GTK's Cancel default.
98
+ await move(geometry.X + geometry.WIDTH - 53, geometry.Y + geometry.HEIGHT - 43);
99
+ }
100
+ export async function typeCommand(root, title, command) {
101
+ const path = resolve(root, '.recording-command.txt');
102
+ await writeFile(path, command);
103
+ await activate(title);
104
+ await guest(['xdotool', 'type', '--clearmodifiers', '--delay', '70', '--file', guestPath(path)]);
105
+ await guest(['xdotool', 'key', 'Return']);
106
+ }
107
+ export async function editFile(root, file, source) {
108
+ const path = resolve(root, '.recording-editor.txt');
109
+ await writeFile(path, source);
110
+ await openApp(['geany', '-i', guestPath(file)], 'Geany');
111
+ const id=(await guest(['xdotool','search','--onlyvisible','--name','Geany'])).split(/\s+/).at(-1);
112
+ await guest(['xdotool','windowsize','--sync',id,'1200','780','windowmove',id,'100','70']);
113
+ // Keep the guest X11 selection owner alive until GTK has read the clipboard.
114
+ const clipboard=guestProcess(['xclip','-quiet','-selection','clipboard','-in',guestPath(path)]);
115
+ clipboard.stdout.resume();clipboard.stderr.resume();
116
+ try{
117
+ await new Promise(done=>setTimeout(done,300));
118
+ await guest(['xdotool', 'key', 'ctrl+a', 'ctrl+v']);
119
+ await new Promise(done => setTimeout(done, 1800));
120
+ await guest(['xdotool', 'key', 'ctrl+s']);
121
+ await new Promise(done=>setTimeout(done,300));
122
+ if((await readFile(file,'utf8')).replaceAll('\r\n','\n')!==source.replaceAll('\r\n','\n'))throw new Error('Guest editor did not save the expected document');
123
+ }finally{clipboard.kill();}
124
+ }
@@ -0,0 +1,44 @@
1
+ # Theme verification
2
+
3
+ Run these optional development tools from a built EdgePress site's root. They use isolated Playwright Chromium with extensions disabled and local static fixtures. Install Playwright in the site's development dependencies and run `npx playwright install chromium` first. No live service accounts or deployments are verified.
4
+
5
+ For a theme that declares `dataSaver` in `theme.json`, create `tests/data-saver-site.test.mjs`:
6
+
7
+ ```js
8
+ import {fileURLToPath} from 'node:url';
9
+ import {registerDataSaverTests} from 'edgepress/tools/verification/data-saver-site.mjs';
10
+
11
+ registerDataSaverTests(fileURLToPath(new URL('../', import.meta.url)), {
12
+ locales: ['en', 'zh-CN'],
13
+ paths: ['/', '/about/'],
14
+ noJsPaths: ['/'],
15
+ coldPaths: ['/'],
16
+ });
17
+ ```
18
+
19
+ The suite checks full/text layouts, readable no-JavaScript output, real throttled transfers, early keyboard switching, stable waiting-screen choices, localized controls, disabled production debug entry, and absence of premature third-party requests. Use actual paths and enabled locales from your site. It expects the operator's explicit full-view defaults:
20
+
21
+ ```yaml
22
+ site:
23
+ dataSaver:
24
+ enabled: true
25
+ debug: false
26
+ mode: auto
27
+ detectSlowConnection: true
28
+ respectBrowserPreference: false
29
+ ```
30
+
31
+ Full and text views retain prose by default. A dynamic application can explicitly set `compareProse: false` if its anonymous fixture differs, while keeping headings, navigation, captions, cards and geometry checks. API fixtures contain anonymous synthetic responses; application-specific interactions and consent checks belong in the site's own tests.
32
+
33
+ Run `node --test tests/data-saver-site.test.mjs`. `DATA_SAVER_SCREENSHOTS=tools/reports/data-saver` optionally retains screenshots and JSON measurements. The test does not enable data-saving support in themes that do not declare it.
34
+
35
+ For a cold-cache benchmark, add `tools/verification/page-performance.mjs`:
36
+
37
+ ```js
38
+ process.env.PERFORMANCE_PATHS ||= JSON.stringify(['/', '/about/']);
39
+ await import('edgepress/tools/verification/page-performance.mjs');
40
+ ```
41
+
42
+ Run it from the site root. Transfers use gzip, 100 KB/s and 150 ms latency. `PERFORMANCE_DIST` can select built output; `PERFORMANCE_OUTPUT=tools/reports/performance` retains evidence. Content-ready time, FCP and the observed LCP candidate are reported separately.
43
+
44
+ For Pages readability, import `formatPages` from `edgepress/tools/assets/format-pages.mjs`. `await formatPages(process.cwd())` lists proposed changes; `{write: true}` applies them. It converts legacy Markdown/YAML Pages to real, formatted `.json` files and sets a local `$schema` reference. Conversion verifies parsed field values and element order, checks all files before writing, refuses duplicate locale formats, and rejects nonempty Markdown bodies. VS Code's JSON outline and folding work on the actual page → row → cell → element hierarchy. From the framework checkout, `node tools/assets/format-pages.mjs --write` formats the example site.