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/content.js CHANGED
@@ -1,3 +1,6 @@
1
+ import {parsePublicationDate,publicationCalendar} from './publication-time.js';
2
+ import { readPostRevision } from './revisions.js';
3
+ import { normalizeCategory, safeAvatar, readingMinutes } from './post-details.js';
1
4
  import { readdir, readFile } from 'node:fs/promises';
2
5
  import { basename, dirname, extname, relative, resolve, sep } from 'node:path';
3
6
  import { parse as parseYaml } from 'yaml';
@@ -17,14 +20,15 @@ export function postsForLocale(posts, locale, defaultLocale) {
17
20
  const byBundle = new Map();
18
21
  for (const post of posts) {
19
22
  const current = byBundle.get(post.bundlePath);
20
- if (!current || post.locale === locale || (current.locale !== locale && post.locale === defaultLocale)) {
23
+ if (!current || post.locale === locale || (current.locale !== locale && post.locale === defaultLocale) ||
24
+ (current.locale !== locale && current.locale !== defaultLocale && post.locale !== defaultLocale && post.locale.localeCompare(current.locale) < 0)) {
21
25
  byBundle.set(post.bundlePath, post);
22
26
  }
23
27
  }
24
28
  return sortPostsNewest([...byBundle.values()]);
25
29
  }
26
30
 
27
- async function walkMarkdown(directory) {
31
+ async function walkContent(directory, kind) {
28
32
  let entries;
29
33
  try {
30
34
  entries = await readdir(directory, { withFileTypes: true });
@@ -36,8 +40,8 @@ async function walkMarkdown(directory) {
36
40
  const found = [];
37
41
  for (const entry of entries) {
38
42
  const fullPath = resolve(directory, entry.name);
39
- if (entry.isDirectory()) found.push(...await walkMarkdown(fullPath));
40
- else if (entry.isFile() && extname(entry.name).toLowerCase() === '.md') found.push(fullPath);
43
+ if (entry.isDirectory()) found.push(...await walkContent(fullPath, kind));
44
+ else if (entry.isFile() && (extname(entry.name).toLowerCase() === '.md' || (kind === 'pages' && extname(entry.name).toLowerCase() === '.json'))) found.push(fullPath);
41
45
  }
42
46
  return found;
43
47
  }
@@ -54,6 +58,15 @@ function splitFrontmatter(source, file) {
54
58
  return { metadata, markdown: lines.slice(end + 1).join('\n') };
55
59
  }
56
60
 
61
+ export function parseDocumentSource(source, file) {
62
+ if (extname(file).toLowerCase() !== '.json') return splitFrontmatter(source, file);
63
+ let metadata;
64
+ try { metadata = JSON.parse(source.replace(/^\uFEFF/, '')); }
65
+ catch (error) { throw new Error('Invalid page JSON in ' + file + ': ' + error.message, {cause: error}); }
66
+ if (!metadata || typeof metadata !== 'object' || Array.isArray(metadata)) throw new Error('Page JSON must be an object in ' + file);
67
+ return {metadata, markdown: ''};
68
+ }
69
+
57
70
  export function slugify(value) {
58
71
  const slug = String(value)
59
72
  .normalize('NFKC')
@@ -69,6 +82,10 @@ function normalizeTags(value) {
69
82
  }
70
83
 
71
84
  function normalizeAuthor(value, file) {
85
+ if (value && typeof value === 'object' && !Array.isArray(value)) {
86
+ if (Object.keys(value).some(key => !['name', 'avatar'].includes(key))) throw new Error('Unsupported author field in ' + file);
87
+ value = value.name;
88
+ }
72
89
  if (value === undefined || value === null || value === '') return '';
73
90
  if (typeof value !== 'string' || !value.trim() || value.length > 160) {
74
91
  throw new Error('author must be a non-empty string of at most 160 characters in ' + file);
@@ -98,24 +115,14 @@ function normalizeVideo(value, file) {
98
115
  return value;
99
116
  }
100
117
 
101
- function normalizeDate(value, filename, kind, file) {
102
- if (value instanceof Date) {
103
- if (!Number.isNaN(value.valueOf())) return value;
104
- throw new Error('Invalid date in ' + file);
118
+ function normalizeDate(value, filename, kind, file, timeZone) {
119
+ const fallback = filename.match(/^(\d{4}-\d{2}-\d{2})-/)?.[1];
120
+ if (value == null && !fallback) {
121
+ if (kind === 'posts') throw new Error('Posts need a valid date in front matter or a YYYY-MM-DD filename: ' + file);
122
+ return new Date(0);
105
123
  }
106
- if (value != null) {
107
- const date = new Date(value);
108
- if (!Number.isNaN(date.valueOf())) return date;
109
- throw new Error('Invalid date in ' + file);
110
- }
111
- const match = filename.match(/^(\d{4}-\d{2}-\d{2})-/);
112
- if (match) {
113
- const date = new Date(match[1] + 'T00:00:00Z');
114
- if (!Number.isNaN(date.valueOf()) && date.toISOString().slice(0, 10) === match[1]) return date;
115
- throw new Error('Invalid date in filename: ' + file);
116
- }
117
- if (kind === 'posts') throw new Error('Posts need a valid date in front matter or a YYYY-MM-DD filename: ' + file);
118
- return new Date(0);
124
+ try { return parsePublicationDate(value ?? fallback, timeZone); }
125
+ catch (error) { throw new Error(error.message + ' in ' + file); }
119
126
  }
120
127
 
121
128
  function normalizePagePath(value, file) {
@@ -134,11 +141,17 @@ export async function readDocuments(config) {
134
141
  const groups = [['posts', resolve(root, 'posts')], ['pages', resolve(root, 'pages')]];
135
142
  const files = [];
136
143
  for (const [kind, directory] of groups) {
137
- for (const file of await walkMarkdown(directory)) files.push({ kind, file });
144
+ const identities = new Set();
145
+ for (const file of await walkContent(directory, kind)) {
146
+ const identity = resolve(dirname(file), basename(file, extname(file)).toLowerCase());
147
+ if (identities.has(identity)) throw new Error('Duplicate language document; keep only one .json or .md file per page locale: ' + file);
148
+ identities.add(identity);
149
+ files.push({ kind, file });
150
+ }
138
151
  }
139
152
  return await mapLimit(files, config.concurrency, async ({ kind, file }) => {
140
153
  const source = await readFile(file, 'utf8');
141
- const { metadata, markdown } = splitFrontmatter(source, file);
154
+ const { metadata, markdown } = parseDocumentSource(source, file);
142
155
  const contentRoot = resolve(root, kind);
143
156
  const relativePath = relative(contentRoot, file).split(sep).join('/');
144
157
  const bundleDirectory = dirname(file);
@@ -150,13 +163,13 @@ export async function readDocuments(config) {
150
163
  const languageFileName = basename(file, extname(file));
151
164
  const languageFile = languageFileName.toLowerCase();
152
165
  const fileLocale = config.i18n.locales.find((locale) => locale.toLowerCase() === languageFile);
153
- if (!fileLocale || languageFileName !== languageFile || extname(file).toLowerCase() !== '.md') {
154
- throw new Error('Content language files must be named after a configured locale, such as en.md or zh-cn.md: ' + file);
166
+ if (!fileLocale || languageFileName !== languageFile) {
167
+ throw new Error('Content language files must be named after a configured locale, such as en.json or zh-cn.md: ' + file);
155
168
  }
156
169
  const declaredLocale = metadata.lang === undefined ? fileLocale :
157
170
  config.i18n.locales.find((locale) => locale.toLowerCase() === String(metadata.lang).toLowerCase());
158
171
  if (!declaredLocale) throw new Error('Unsupported lang value in ' + file + '; use the language filename and configure its language pack');
159
- if (declaredLocale !== fileLocale) throw new Error('Front matter lang must match the content filename locale in ' + file);
172
+ if (declaredLocale !== fileLocale) throw new Error('Content lang must match the content filename locale in ' + file);
160
173
  const locale = fileLocale;
161
174
  const fallbackSlug = bundlePath.replace(/\\/g, '/');
162
175
  const fallbackName = bundleName.replace(/^\d{4}-\d{2}-\d{2}-/, '');
@@ -176,16 +189,21 @@ export async function readDocuments(config) {
176
189
  if (kind === 'pages' && markdown.trim()) {
177
190
  throw new Error('Page body must be empty; move content into page blocks: ' + file);
178
191
  }
192
+ if (metadata.pinned !== undefined && typeof metadata.pinned !== 'boolean') throw new Error('pinned must be a boolean in ' + file);
193
+ if (metadata.embeds !== undefined) throw new Error('Use !embed[service](URL) in the content instead of front-matter embeds: ' + file);
179
194
  const rawSlug = metadata.slug ?? (kind === 'posts' ? fallbackName : fallbackSlug);
180
195
  const normalizedPagePath = kind === 'pages' && metadata.homepage !== true ? normalizePagePath(rawSlug, file) : '';
181
196
  const slug = slugify(String(rawSlug).split('/').pop());
182
- const date = normalizeDate(metadata.date, bundleName, kind, file);
197
+ const date = normalizeDate(metadata.date, bundleName, kind, file, config.site?.timeZone);
198
+ const calendar = metadata.date == null ? (bundleName.match(/^(\d{4}-\d{2}-\d{2})-/)?.[1] || '1970-01-01') : publicationCalendar(metadata.date,date,config.site?.timeZone);
199
+ if (metadata.showChanges !== undefined && typeof metadata.showChanges !== 'boolean') throw new Error('showChanges must be a boolean in ' + file);
200
+ const revision = kind === 'posts' ? await readPostRevision(file, source, metadata.showChanges !== false, resolve(root,'assets')) : {updated:null,changes:''};
183
201
  let path;
184
202
  if (kind === 'posts') {
185
203
  path = config.permalink
186
- .replace(/:year/g, String(date.getUTCFullYear()))
187
- .replace(/:month/g, String(date.getUTCMonth() + 1).padStart(2, '0'))
188
- .replace(/:day/g, String(date.getUTCDate()).padStart(2, '0'))
204
+ .replace(/:year/g, calendar.slice(0,4))
205
+ .replace(/:month/g, calendar.slice(5,7))
206
+ .replace(/:day/g, calendar.slice(8,10))
189
207
  .replace(/:slug/g, slug)
190
208
  .replace(/:title/g, slug)
191
209
  .replace(/^\/+|\/+$/g, '') + '/';
@@ -196,7 +214,14 @@ export async function readDocuments(config) {
196
214
  if (locale !== config.i18n.defaultLocale && metadata.homepage !== true) path = locale + '/' + path;
197
215
  return {
198
216
  kind, file, relativePath, bundlePath, title, slug, date, locale,
217
+ dateOnly: metadata.date == null || /^\d{4}-\d{2}-\d{2}$/.test(String(metadata.date)),
218
+ updated: revision.updated, changes: revision.changes, showChanges: metadata.showChanges !== false,
219
+ revisionImages: revision.revisionImages, revisionAssets: revision.revisionAssets??[],
199
220
  author: kind === 'posts' ? normalizeAuthor(metadata.author, file) : '',
221
+ authorAvatar: safeAvatar(metadata.authorAvatar ?? (typeof metadata.author === 'object' && metadata.author !== null ? metadata.author.avatar : undefined), 'author avatar in ' + file),
222
+ category: normalizeCategory(metadata.category),
223
+ pinned: kind === 'posts' && metadata.pinned === true,
224
+ readingMinutes: kind === 'posts' ? readingMinutes(markdown) : 0,
200
225
  videoId: (kind === 'posts' ? 'post-' : 'page-') + slugify(bundlePath + '-' + locale.toLowerCase()),
201
226
  video: kind === 'posts' ? normalizeVideo(metadata.video, file) : null,
202
227
  tags: normalizeTags(metadata.tags),
@@ -0,0 +1,194 @@
1
+ import { parseDocument } from 'htmlparser2';
2
+ import { localizedUrl, translate } from './i18n.js';
3
+ import { readFile } from 'node:fs/promises';
4
+ import { resolve } from 'node:path';
5
+ import * as posix from 'node:path/posix';
6
+ import postcss from 'postcss';
7
+ import { transform, transformSync } from 'esbuild';
8
+ import { readFileSync } from 'node:fs';
9
+ import { createHash } from 'node:crypto';
10
+
11
+ let inlineAssets;
12
+ export function dataSaverInlineAssets(config) {
13
+ if (!config.site?.dataSaver?.enabled) return null;
14
+ if (!inlineAssets) {
15
+ const script = transformSync(readFileSync(new URL('../static/edgepress/data-saver.js', import.meta.url), 'utf8'), { loader:'js', minify:true, charset:'utf8', legalComments:'inline' }).code;
16
+ const style = transformSync(readFileSync(new URL('../static/edgepress/data-saver.css', import.meta.url), 'utf8'), { loader:'css', minify:true, charset:'utf8', legalComments:'inline' }).code;
17
+ const hash = value => "'sha256-" + createHash('sha256').update(value).digest('base64') + "'";
18
+ inlineAssets = { script, style, scriptHash:hash(script), styleHash:hash(style) };
19
+ }
20
+ return inlineAssets;
21
+ }
22
+
23
+ const essentialStyles = /\/edgepress\/(?:external-links|data-saver|data-saver-layout)\.[a-f0-9]{16}\.css$/;
24
+ const optionalScripts = /\/edgepress\/(?:image-viewer|media-viewer|document-viewer|project-demo|webmcp|oembed|code-copy|post-toc)\.[a-f0-9]{16}\.js$/;
25
+ const escape = value => String(value).replace(/[&<>"']/g, character => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[character]);
26
+
27
+ export async function configureDataSaver(config) {
28
+ if (!config.site.dataSaver?.enabled) return;
29
+ const theme = JSON.parse(await readFile(resolve(config.resolvedPaths.theme, 'theme.json'), 'utf8'));
30
+ const variant = theme.dataSaver;
31
+ if (!variant || typeof variant !== 'object' || Array.isArray(variant) || typeof variant.stylesheet !== 'string' || !Array.isArray(variant.scripts) || variant.layoutStyles !== undefined && !Array.isArray(variant.layoutStyles) || Object.keys(variant).some(key => !['stylesheet', 'scripts', 'layoutStyles'].includes(key))) throw new Error('This theme must opt in with theme.json dataSaver: { stylesheet, scripts, layoutStyles? }');
32
+ for (const [file, extension] of [[variant.stylesheet, 'css'], ...variant.scripts.map(file => [file, 'js'])]) {
33
+ if (typeof file !== 'string' || !new RegExp('^[a-zA-Z0-9][a-zA-Z0-9_./-]*\\.' + extension + '$').test(file) || file.split('/').some(part => !part || part === '..' || part === '.')) throw new Error('Invalid data-saving theme asset');
34
+ await readFile(resolve(config.resolvedPaths.theme, 'assets', file));
35
+ }
36
+ for (const file of variant.layoutStyles || []) {
37
+ if (typeof file !== 'string' || !/^[a-zA-Z0-9][a-zA-Z0-9_./-]*\.css$/.test(file) || file.split('/').some(part => !part || part === '..' || part === '.')) throw new Error('Invalid data-saving layout stylesheet');
38
+ }
39
+ config.dataSaverTheme = variant;
40
+ }
41
+
42
+ export async function dataSaverLayout(config, items) {
43
+ if (!config.site.dataSaver?.enabled || !config.dataSaverTheme) return null;
44
+ const assets = new Map(items.map(item => [item.path, item]));
45
+ const compile = (path, parents = new Set()) => {
46
+ if (parents.has(path)) throw new Error('Circular data-saving layout import: ' + path);
47
+ if (/^edgepress\/fonts(?:\/|\.css$)/.test(path)) return postcss.root();
48
+ const asset = assets.get(path);
49
+ if (!asset?.content) throw new Error('Missing data-saving layout stylesheet: ' + path);
50
+ const normalized = transformSync(asset.content.toString('utf8'), { loader: 'css', minify: true, legalComments: 'inline' }).code;
51
+ const tree = postcss.parse(normalized, { from: path });
52
+ const chain = new Set([...parents, path]);
53
+ tree.walkAtRules(rule => {
54
+ if (rule.name.toLowerCase() === 'font-face') rule.remove();
55
+ else if (rule.name.toLowerCase() === 'import') {
56
+ const match = rule.params.match(/^(?:url\(\s*)?(['"])([^'"]+)\1\s*\)?\s*$/);
57
+ if (!match || /^(?:[a-z]+:|\/\/)/i.test(match[2])) { rule.remove(); return; }
58
+ const source = match[2].split(/[?#]/, 1)[0];
59
+ const target = source.startsWith('/') ? source.slice(1) : posix.normalize(posix.join(posix.dirname(path), source));
60
+ if (target.startsWith('../') || !target.endsWith('.css')) throw new Error('Invalid data-saving layout import');
61
+ rule.replaceWith(...compile(target, chain).nodes);
62
+ }
63
+ });
64
+ tree.walkDecls(declaration => {
65
+ if (/url\s*\(/i.test(declaration.value)) declaration.remove();
66
+ });
67
+ return tree;
68
+ };
69
+ const paths = [...(config.dataSaverTheme.layoutStyles || []), config.dataSaverTheme.stylesheet, ...(assets.has('edgepress/external-links.css') ? ['edgepress/external-links.css'] : [])].filter(Boolean);
70
+ const css = paths.map(file => compile(file).toString()).join('\n');
71
+ const result = await transform(css, { loader: 'css', minify: true, legalComments: 'inline' });
72
+ return { path: 'edgepress/data-saver-layout.css', content: Buffer.from(result.code), sourceName: 'Data-saving theme layout' };
73
+ }
74
+
75
+ function featureFor(source) {
76
+ if (/\/edgepress\/fonts(?:[./]|$)/.test(source)) return 'fonts';
77
+ if (/oembed/.test(source)) return 'embed';
78
+ if (/document-viewer/.test(source)) return 'document';
79
+ if (/image-viewer/.test(source)) return 'image';
80
+ if (/media-viewer/.test(source)) return 'gallery';
81
+ if (/project-demo/.test(source)) return 'demo';
82
+ return 'full';
83
+ }
84
+
85
+ function mediaLabel(node) {
86
+ const attributes = node.attribs || {};
87
+ const label = attributes.alt || attributes.title || attributes['aria-label'] || attributes['data-document-title'];
88
+ if (label) return label;
89
+ for (const child of node.children || []) {
90
+ if (['template', 'noscript'].includes(child.name)) continue;
91
+ const nested = mediaLabel(child);
92
+ if (nested) return nested;
93
+ }
94
+ return '';
95
+ }
96
+
97
+ export function dataSaverHtml(html, config, manifest = {}) {
98
+ if (!/<html\b/i.test(html)) return html;
99
+ if (!config.site.dataSaver?.enabled) {
100
+ if (config.site.dataSaver?.debug) return html;
101
+ const language = html.match(/<html\b[^>]*\blang=["']([^"']+)/i)?.[1] || config.i18n.defaultLocale;
102
+ const settings = JSON.stringify({ enabled: false, notFoundUrl: localizedUrl(config, language, '404.html') }).replace(/</g, '\\u003c');
103
+ const source = escape(manifest['/edgepress/data-saver.js'] || '/edgepress/data-saver.js');
104
+ return html.replace(/(<meta\b[^>]*charset[^>]*>|<head\b[^>]*>)/i, '$1<script id="edgepress-data-saver-config" type="application/json">' + settings + '</script><script src="' + source + '"></script>');
105
+ }
106
+ const document = parseDocument(html, { withStartIndices: true, withEndIndices: true });
107
+ const patches = [];
108
+ let head;
109
+ let body;
110
+ let firstImage;
111
+ const textScripts = [];
112
+ const fullStyles = [];
113
+ let locale = config.i18n.defaultLocale;
114
+ const visit = node => {
115
+ const name = node.name;
116
+ const attributes = node.attribs || {};
117
+ if (name === 'html') locale = attributes.lang || locale;
118
+ if (name === 'head') head = node;
119
+ if (name === 'body') body = node;
120
+ if (['template', 'noscript', 'svg'].includes(name)) return;
121
+ const original = html.slice(node.startIndex, node.endIndex + 1);
122
+ let resource = '';
123
+ if (name === 'link' && (['preload', 'modulepreload', 'prefetch', 'preconnect', 'dns-prefetch', 'stylesheet'].includes(attributes.rel))) resource = 'style';
124
+ if (name === 'script' && attributes.src) resource = 'script';
125
+ if (resource) {
126
+ if (resource === 'style' && attributes.rel === 'stylesheet' && featureFor(attributes.href || '') !== 'fonts' && !essentialStyles.test(attributes.href)) {
127
+ try {
128
+ if (new URL(attributes.href,config.site.url).origin === new URL(config.site.url).origin) fullStyles.push({href:attributes.href,rel:'preload',as:'style'});
129
+ } catch {}
130
+ }
131
+ const coreScripts = config.dataSaverCore?.scripts || [];
132
+ if (resource === 'script' && coreScripts.some(path => (manifest[path] || path) === attributes.src)) {
133
+ patches.push({ start:node.startIndex, end:node.endIndex + 1, text:'<noscript>' + original + '</noscript>' });
134
+ return;
135
+ }
136
+ if (resource === 'style' && essentialStyles.test(attributes.href)) {
137
+ patches.push({ start:node.startIndex, end:node.endIndex + 1, text:'<noscript>' + original + '</noscript>' });
138
+ return;
139
+ }
140
+ const core = resource === 'script' && !optionalScripts.test(attributes.src) && attributes['data-data-optional'] === undefined || resource === 'style' && essentialStyles.test(attributes.href);
141
+ if (core && resource === 'script') {
142
+ try {
143
+ if (new URL(attributes.src,config.site.url).origin === new URL(config.site.url).origin) textScripts.push({href:attributes.src,rel:attributes.type === 'module' ? 'modulepreload' : 'preload',...(attributes.type === 'module' ? {} : {as:'script'})});
144
+ } catch {}
145
+ }
146
+ patches.push({ start: node.startIndex, end: node.endIndex + 1, text: '<template data-edgepress-data-resource="' + resource + '" data-data-feature="' + (core ? 'core' : featureFor(attributes.src || attributes.href || '')) + '">' + original + '</template><noscript>' + original + '</noscript>' });
147
+ return;
148
+ }
149
+ if (['img', 'picture', 'video', 'audio', 'iframe'].includes(name) || attributes['data-edgepress-document'] !== undefined || attributes['data-edgepress-media-viewer'] !== undefined || attributes['data-edgepress-video'] !== undefined || attributes['data-project-demo'] !== undefined) {
150
+ const candidate = name === 'picture' ? node.children?.find(child => child.name === 'img')?.attribs : name === 'img' ? attributes : null;
151
+ if (!firstImage && candidate?.fetchpriority === 'high') {
152
+ const source = name === 'picture' ? node.children?.find(child => child.name === 'source')?.attribs : null;
153
+ const href = source?.srcset?.split(',')[0].trim().split(/\s+/)[0] || candidate.src;
154
+ try {
155
+ if (new URL(href,config.site.url).origin === new URL(config.site.url).origin) firstImage = {href,srcset:source?.srcset || candidate.srcset,sizes:source?.sizes || candidate.sizes,type:source?.type};
156
+ } catch {}
157
+ }
158
+ const label = mediaLabel(node);
159
+ const tag = name === 'img' || name === 'picture' ? 'span' : 'div';
160
+ const feature = attributes['data-edgepress-document'] !== undefined ? 'document' : attributes['data-edgepress-media-viewer'] !== undefined || attributes['data-edgepress-video'] !== undefined ? 'gallery' : attributes['data-project-demo'] !== undefined ? 'demo' : name === 'img' || name === 'picture' ? 'image' : 'media';
161
+ const classes = attributes.class || '';
162
+ const pictureImage = name === 'picture' ? node.children?.find(child => child.name === 'img')?.attribs : null;
163
+ const decorative = feature === 'image' && (attributes.alt === '' || pictureImage?.alt === '' || attributes['aria-hidden'] === 'true' || ['none', 'presentation'].includes(attributes.role) || /(?:^|\s)(?:navigation-icon|icon-library|brand-mark)(?:\s|$)/.test(classes));
164
+ const brand = feature === 'image' && /(?:^|\s)brand-wordmark(?:\s|$)/.test(classes);
165
+ patches.push({ start: node.startIndex, end: node.endIndex + 1, text: '<' + tag + ' class="edgepress-data-placeholder' + (brand ? ' edgepress-data-brand' : '') + '" data-edgepress-data-media data-data-feature="' + feature + '"' + (decorative ? ' data-data-decorative' : '') + (brand ? ' data-data-brand' : '') + '><span data-data-description>' + escape(label) + '</span><template data-edgepress-data-html>' + escape(original) + '</template><noscript>' + original + '</noscript></' + tag + '>' });
166
+ return;
167
+ }
168
+ for (const child of node.children || []) visit(child);
169
+ };
170
+ visit(document);
171
+ if (!head || !body) return html;
172
+ const keys = ['load', 'loading', 'loadingNotice', 'loadFailed', 'retry', 'progressStyles', 'progressControls', 'progressMedia', 'slowTitle', 'slowNotice', 'switchText', 'keepFull', 'returnFull', 'textNotice', 'missingDescription'];
173
+ const labels = Object.fromEntries(keys.map(key => [key, translate(config, locale, 'dataSaver' + key[0].toUpperCase() + key.slice(1))]));
174
+ const textPreloads = [{href:manifest['/edgepress/data-saver-layout.css'] || '/edgepress/data-saver-layout.css',rel:'preload',as:'style'}, ...(config.dataSaverCore ? [{href:manifest[config.dataSaverCore.path] || config.dataSaverCore.path,rel:'modulepreload'}] : []), ...textScripts];
175
+ const settings = JSON.stringify({ ...config.site.dataSaver, labels, textPreloads, fullPreloads:[...fullStyles,...textPreloads], firstImage, notFoundUrl: localizedUrl(config, locale, '404.html') }).replace(/</g, '\\u003c');
176
+ const asset = path => escape(manifest[path] || path);
177
+ const variant = config.dataSaverTheme;
178
+ if (!variant) throw new Error('Missing data-saving theme variant');
179
+ const coreStyle = path => '<template data-edgepress-data-resource="style" data-data-feature="core"><link rel="stylesheet" href="' + asset(path) + '"></template><noscript><link rel="stylesheet" href="' + asset(path) + '"></noscript>';
180
+ const inline = dataSaverInlineAssets(config);
181
+ const bundledControls = config.dataSaverCore ? '<template data-edgepress-data-resource="script" data-data-feature="core"><script type="module" src="' + asset(config.dataSaverCore.path) + '"></script></template>' : '';
182
+ const bootstrap = '<script id="edgepress-data-saver-config" type="application/json">' + settings + '</script><style data-edgepress-data-critical>' + inline.style + '</style><script data-edgepress-data-runtime="' + asset('/edgepress/data-saver.js') + '">' + inline.script + '</script>' + coreStyle('/edgepress/data-saver-layout.css') + bundledControls + variant.scripts.map(file => '<template data-edgepress-data-resource="script" data-data-feature="core"><script src="' + asset('/' + file) + '"></script></template><noscript><script defer src="' + asset('/' + file) + '"></script></noscript>').join('');
183
+ const charset = head.children?.find(node => node.name === 'meta' && node.attribs?.charset);
184
+ const headPosition = charset ? charset.endIndex + 1 : html.indexOf('>', head.startIndex) + 1;
185
+ patches.push({ start: headPosition, end: headPosition, text: bootstrap });
186
+ const waiting = '<div class="edgepress-data-wait" data-data-wait hidden><div class="edgepress-data-progress"><p role="status" data-data-progress-label>' + escape(labels.loading) + '</p><progress data-data-progress aria-label="' + escape(labels.loading) + '"></progress></div><aside class="edgepress-data-offer" data-data-offer aria-labelledby="edgepress-data-offer-title" hidden><h2 id="edgepress-data-offer-title">' + escape(labels.loading) + '</h2><p data-data-loading-notice>' + escape(labels.loadingNotice) + '</p><div><button type="button" data-data-mode="text">' + escape(labels.switchText) + '</button><button type="button" data-data-dismiss>' + escape(labels.keepFull) + '</button><button type="button" data-data-retry hidden>' + escape(labels.retry) + '</button></div></aside></div>';
187
+ const bodyPosition = html.indexOf('>', body.startIndex) + 1;
188
+ patches.push({ start: bodyPosition, end: bodyPosition, text: waiting });
189
+ const controls = '<aside class="edgepress-data-return" data-data-return hidden><span>' + escape(labels.textNotice) + '</span><button type="button" data-data-mode="full">' + escape(labels.returnFull) + '</button></aside>';
190
+ patches.push({ start: body.endIndex - 6, end: body.endIndex - 6, text: controls });
191
+ patches.sort((first, second) => second.start - first.start);
192
+ for (const patch of patches) html = html.slice(0, patch.start) + patch.text + html.slice(patch.end);
193
+ return html;
194
+ }
package/src/deploy.js ADDED
@@ -0,0 +1,28 @@
1
+ import {spawn} from 'node:child_process';
2
+ import {existsSync} from 'node:fs';
3
+ import {delimiter,dirname,resolve} from 'node:path';
4
+ export function deploymentCommand(platform, output = 'dist', args = []) {
5
+ switch (platform) {
6
+ case 'cloudflare': return ['wrangler', 'deploy', ...args];
7
+ case 'netlify': return ['netlify-cli', 'deploy', '--prod', '--dir', output, ...args];
8
+ case 'vercel': return ['vercel', 'deploy', '--prod', ...args];
9
+ case 'edgeone': return ['edgeone', 'pages', 'deploy', output, ...args];
10
+ case 'esa': return ['esa-cli', 'deploy', '--assets', output, ...args];
11
+ default: throw new Error('Platform must be cloudflare, netlify, vercel, edgeone, or esa');
12
+ }
13
+ }
14
+ export async function deploySite(platform, output, args) {
15
+ const command = deploymentCommand(platform, output, args);
16
+ // npm exec resolves platform CLIs locally or installs them; platform authentication remains in its CLI.
17
+ const windows = process.platform === 'win32';
18
+ const executable = windows ? process.execPath : 'npm';
19
+ const candidates = [process.env.npm_execpath?.replace(/npx-cli\.js$/i,'npm-cli.js'), ...[dirname(process.execPath),...(process.env.PATH || process.env.Path || '').split(delimiter)].map(directory=>resolve(directory,'node_modules/npm/bin/npm-cli.js'))];
20
+ const npmCli = windows ? candidates.find(candidate=>candidate && existsSync(candidate)) : null;
21
+ if(windows && !npmCli)throw new Error('npm CLI was not found. Install Node.js with npm or run deployment from npm run.');
22
+ const parameters = [...(windows ? [npmCli] : []), 'exec', '--yes', '--', ...command];
23
+ await new Promise((resolve, reject) => {
24
+ const child = spawn(executable, parameters, {stdio:'inherit', windowsHide:true});
25
+ child.once('error', reject);
26
+ child.once('exit', code => code === 0 ? resolve() : reject(new Error(platform + ' deployment failed: ' + code)));
27
+ });
28
+ }
@@ -0,0 +1,44 @@
1
+ import { documentFormat, documentUrl } from '@jsw-teams/document-viewer';
2
+ import { translate, translateValue } from './i18n.js';
3
+
4
+ const escape = value => String(value ?? '').replace(/[&<>"']/g, char => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[char]));
5
+
6
+ export function parseDocument(source) {
7
+ const match = String(source).match(/^ {0,3}!document\[([^\]\r\n]+)\]\(([^\s<>]+)\)(?:\{([^}\r\n]*)\})?[\t ]*(?:\r?\n|$)/i);
8
+ if (!match) return null;
9
+ const attributes = {};
10
+ for (const field of (match[3] || '').trim().split(/\s+/).filter(Boolean)) {
11
+ const parts = field.match(/^(format|service)=([a-z0-9_-]+)$/i);
12
+ if (!parts || Object.hasOwn(attributes, parts[1])) throw new Error('Document syntax accepts format=pdf and service=registered-id');
13
+ attributes[parts[1]] = parts[2];
14
+ }
15
+ return { raw: match[0], title: match[1], src: match[2], ...attributes };
16
+ }
17
+
18
+ export function renderDocument(block, { config, locale }) {
19
+ const allowed = new Set(['type', 'src', 'title', 'format', 'service', 'previewSrc', 'caption', 'raw', 'tokens']);
20
+ for (const key of Object.keys(block)) if (!allowed.has(key)) throw new Error('Unsupported document field: ' + key);
21
+ const title = String(block.title || '').trim();
22
+ if (!title || title.length > 500) throw new Error('Documents need a descriptive title of 1 to 500 characters');
23
+ const base = config.site?.url || 'https://document.invalid';
24
+ const source = documentUrl(block.src, base);
25
+ const preview = documentUrl(block.previewSrc || block.src, base);
26
+ const format = documentFormat(preview.href, block.format);
27
+ if (!format) throw new Error('Unsupported document format; use format for extensionless URLs');
28
+ let service = null;
29
+ const origins = [...new Set([source.origin, preview.origin].filter(origin => origin !== new URL(base).origin))];
30
+ if (block.service || origins.length) {
31
+ service = config.browserPlugins?.services?.find(item => item.id === block.service || (!block.service && item.provider === 'external-api' && item.backendUrl && origins.every(origin => origin === new URL(item.backendUrl).origin)));
32
+ if (!service || service.provider !== 'external-api') throw new Error('Remote documents need a registered external-api consent service');
33
+ const permitted = new Set([new URL(service.backendUrl).origin, ...(service.csp?.['connect-src'] || [])]);
34
+ if (origins.some(origin => !permitted.has(origin))) throw new Error('Document URL must use its registered service origin');
35
+ }
36
+ const unavailable = escape(translate(config, locale, 'documentViewerError'));
37
+ if (service?.enabled === false) return '<figure class="document-block"><figcaption>' + escape(title) + '</figcaption><p>' + unavailable + '</p></figure>';
38
+ const caption = block.caption ? '<p>' + escape(block.caption) + '</p>' : '';
39
+ const consent = service ? '<p>' + escape(translateValue(config, locale, service.purpose)) + '</p><button type="button" data-service-consent-settings>' + escape(translate(config, locale, 'privacySettings')) + '</button>' : '';
40
+ return '<figure class="document-block" aria-label="' + escape(title) + '" data-edgepress-document data-document-src="' + escape(block.src) + '" data-document-title="' + escape(title) + '" data-document-format="' + format + '"' +
41
+ (block.previewSrc ? ' data-document-preview="' + escape(block.previewSrc) + '"' : '') +
42
+ (service ? ' data-document-service="' + escape(service.id) + '"' : '') + '>' + consent +
43
+ '<div data-document-mount><p>' + escape(translate(config, locale, 'documentViewerLoading')) + '</p></div><noscript><p>' + unavailable + '</p></noscript>' + caption + '</figure>';
44
+ }
package/src/embed.js ADDED
@@ -0,0 +1,60 @@
1
+ import {sourceOrigins, resolveEmbed} from './oembed.js';
2
+ import {translate, translateValue} from './i18n.js';
3
+ import {parseDocument, renderDocument} from './document.js';
4
+
5
+ const escapeHtml = value => String(value ?? '').replace(/[&<>"']/g, char => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[char]));
6
+
7
+ // Embeds occupy their own line, so they never create figures inside paragraphs.
8
+ // Marked handles code fences and escaped examples before this block tokenizer.
9
+ export function parseEmbed(source) {
10
+ const match = String(source).match(/^ {0,3}!embed\[([a-z0-9_-]+)\]\(([^\s<>]+)\)[\t ]*(?:\r?\n|$)/i);
11
+ return match ? {raw: match[0], integration: match[1], url: match[2]} : null;
12
+ }
13
+
14
+ export async function renderEmbed(block, {config, locale}) {
15
+ const service = config.browserPlugins?.services?.find(item => item.id === block.integration);
16
+ if (!service || service.provider !== 'oembed') throw new Error('Embed requires a registered oembed service: ' + block.integration);
17
+ const address = new URL(block.url);
18
+ if (address.protocol !== 'https:' || !sourceOrigins(service).includes(address.origin) || address.username || address.password || /[\x00-\x20\\]/.test(block.url)) {
19
+ throw new Error('Embed URL must use its registered HTTPS service origin');
20
+ }
21
+ const provider = translateValue(config, locale, service.name || service.id);
22
+ let label = String(block.title || provider);
23
+ if (label.length > 200) throw new Error('Embed title must contain at most 200 characters');
24
+ const notice = key => escapeHtml(translate(config, locale, key).replace('{service}', provider));
25
+ const link = '<a href="' + escapeHtml(address.href) + '" target="_blank" rel="noopener noreferrer">' + escapeHtml(label) + '</a>';
26
+ const match = service.embedPathPattern ? address.pathname.match(new RegExp(service.embedPathPattern)) : null;
27
+ if (service.embedPathPattern && !match?.[1]) throw new Error('Embed URL does not match the configured content path');
28
+ for (const field of ['width', 'height']) {
29
+ if (block[field] !== undefined && (!Number.isInteger(block[field]) || block[field] < 60 || block[field] > 4096)) throw new Error('Embed ' + field + ' must be from 60 to 4096');
30
+ }
31
+ if (service.enabled === false) return '<figure class="edgepress-oembed" data-oembed-disabled><figcaption>' + link + '</figcaption><p role="status">' + notice('embedDisabled') + '</p></figure>';
32
+ const data = service.embedTemplate ? {version:'1.0', type:'rich', title:label, width:640, height:480, html:'<iframe src="' + escapeHtml(service.embedTemplate.replaceAll('{url}', encodeURIComponent(address.href)).replaceAll('{path}', address.pathname).replaceAll('{id}', encodeURIComponent(match?.[1] || ''))) + '"></iframe>'} :
33
+ service.oembedEndpoint ? await resolveEmbed(service, address.href, config) : undefined;
34
+ if (!block.title && data?.title) label = String(data.title).slice(0, 200);
35
+ const heading = '<a href="' + escapeHtml(address.href) + '" target="_blank" rel="noopener noreferrer">' + escapeHtml(label) + '</a>';
36
+ const dimensions = ' data-oembed-width="' + (block.width || data?.width || 640) + '" data-oembed-height="' + (block.height || data?.height || 480) + '"';
37
+ const caption = block.caption ? '<p class="oembed-caption">' + escapeHtml(block.caption) + '</p>' : '';
38
+ return '<figure class="edgepress-oembed" data-edgepress-oembed="' + escapeHtml(service.id) + '" data-oembed-url="' + escapeHtml(address.href) + '"' + dimensions +
39
+ (data === null ? ' data-oembed-unavailable="true"' : '') + '><figcaption>' + heading + '</figcaption><p data-oembed-notice role="status">' + notice(data === null ? 'embedUnavailable' : 'embedNeedsConsent') + '</p>' +
40
+ (data !== null ? '<button type="button" data-oembed-load>' + escapeHtml(translate(config, locale, 'loadMedia')) + '</button>' : '') +
41
+ '<div data-oembed-status><div class="oembed-placeholder" data-oembed-placeholder aria-hidden="true"></div></div>' + caption +
42
+ (data ? '<template data-oembed-data>' + escapeHtml(JSON.stringify(data)) + '</template>' : '') + '</figure>';
43
+ }
44
+
45
+ // Pages retain escaped text; only standalone embed lines receive special syntax.
46
+ export async function renderPageText(source, context) {
47
+ const output = [], lines = String(source).split(/\r?\n/);
48
+ let paragraph = [];
49
+ const flush = () => { if (paragraph.length) output.push('<p>' + escapeHtml(paragraph.join('\n')) + '</p>'); paragraph = []; };
50
+ for (const line of lines) {
51
+ const document = parseDocument(line);
52
+ if (document) { flush(); output.push(renderDocument(document, context)); continue; }
53
+ const embed = parseEmbed(line);
54
+ if (embed) { flush(); output.push(await renderEmbed(embed, context)); }
55
+ else if (!line.trim()) flush();
56
+ else paragraph.push(line);
57
+ }
58
+ flush();
59
+ return output.join('');
60
+ }